forked from quickshell/quickshell
Compare commits
1 commit
master
...
hyprland-f
Author | SHA1 | Date | |
---|---|---|---|
6e9bb4183c |
376 changed files with 2112 additions and 33400 deletions
14
.clang-tidy
14
.clang-tidy
|
@ -5,9 +5,6 @@ Checks: >
|
|||
-*,
|
||||
bugprone-*,
|
||||
-bugprone-easily-swappable-parameters,
|
||||
-bugprone-forward-declararion-namespace,
|
||||
-bugprone-forward-declararion-namespace,
|
||||
-bugprone-return-const-ref-from-parameter,
|
||||
concurrency-*,
|
||||
cppcoreguidelines-*,
|
||||
-cppcoreguidelines-owning-memory,
|
||||
|
@ -16,10 +13,8 @@ Checks: >
|
|||
-cppcoreguidelines-avoid-const-or-ref-data-members,
|
||||
-cppcoreguidelines-non-private-member-variables-in-classes,
|
||||
-cppcoreguidelines-avoid-goto,
|
||||
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
|
||||
-cppcoreguidelines-avoid-do-while,
|
||||
-cppcoreguidelines-pro-type-reinterpret-cast,
|
||||
-cppcoreguidelines-pro-type-vararg,
|
||||
google-build-using-namespace.
|
||||
google-explicit-constructor,
|
||||
google-global-names-in-headers,
|
||||
google-readability-casting,
|
||||
google-runtime-int,
|
||||
|
@ -31,7 +26,6 @@ Checks: >
|
|||
-modernize-return-braced-init-list,
|
||||
-modernize-use-trailing-return-type,
|
||||
performance-*,
|
||||
-performance-avoid-endl,
|
||||
portability-std-allocator-const,
|
||||
readability-*,
|
||||
-readability-function-cognitive-complexity,
|
||||
|
@ -42,10 +36,6 @@ Checks: >
|
|||
-readability-braces-around-statements,
|
||||
-readability-redundant-access-specifiers,
|
||||
-readability-else-after-return,
|
||||
-readability-container-data-pointer,
|
||||
-readability-implicit-bool-conversion,
|
||||
-readability-avoid-nested-conditional-operator,
|
||||
-readability-math-missing-parentheses,
|
||||
tidyfox-*,
|
||||
CheckOptions:
|
||||
performance-for-range-copy.WarnOnAllAutoCopies: true
|
||||
|
|
|
@ -9,7 +9,3 @@ indent_style = tab
|
|||
[*.nix]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
|
1
.github/ISSUE_TEMPLATE/config.yml
vendored
1
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -1 +0,0 @@
|
|||
blank_issues_enabled: true
|
82
.github/ISSUE_TEMPLATE/crash.yml
vendored
82
.github/ISSUE_TEMPLATE/crash.yml
vendored
|
@ -1,82 +0,0 @@
|
|||
name: Crash Report
|
||||
description: Quickshell has crashed
|
||||
labels: ["bug", "crash"]
|
||||
body:
|
||||
- type: textarea
|
||||
id: crashinfo
|
||||
attributes:
|
||||
label: General crash information
|
||||
description: |
|
||||
Paste the contents of the `info.txt` file in your crash folder here.
|
||||
value: "<details> <summary>General information</summary>
|
||||
|
||||
|
||||
```
|
||||
|
||||
<Paste the contents of the file here inside of the triple backticks>
|
||||
|
||||
```
|
||||
|
||||
|
||||
</details>"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: userinfo
|
||||
attributes:
|
||||
label: What caused the crash
|
||||
description: |
|
||||
Any information likely to help debug the crash. What were you doing when the crash occurred,
|
||||
what changes did you make, can you get it to happen again?
|
||||
- type: textarea
|
||||
id: dump
|
||||
attributes:
|
||||
label: Minidump
|
||||
description: |
|
||||
Attach `minidump.dmp.log` here. If it is too big to upload, compress it.
|
||||
|
||||
You may skip this step if quickshell crashed while processing a password
|
||||
or other sensitive information. If you skipped it write why instead.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Log file
|
||||
description: |
|
||||
Attach `log.qslog.log` here. If it is too big to upload, compress it.
|
||||
|
||||
You can preview the log if you'd like using `quickshell read-log <path-to-log>`.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: config
|
||||
attributes:
|
||||
label: Configuration
|
||||
description: |
|
||||
Attach your configuration here, preferrably in full (not just one file).
|
||||
Compress it into a zip, tar, etc.
|
||||
|
||||
This will help us reproduce the crash ourselves.
|
||||
- type: textarea
|
||||
id: bt
|
||||
attributes:
|
||||
label: Backtrace
|
||||
description: |
|
||||
If you have gdb installed and use systemd, or otherwise know how to get a backtrace,
|
||||
we would appreciate one. (You may have gdb installed without knowing it)
|
||||
|
||||
1. Run `coredumpctl debug <pid>` where `pid` is the number shown after "Crashed process ID"
|
||||
in the crash reporter.
|
||||
2. Once it loads, type `bt -full` (then enter)
|
||||
3. Copy the output and attach it as a file or in a spoiler.
|
||||
- type: textarea
|
||||
id: exe
|
||||
attributes:
|
||||
label: Executable
|
||||
description: |
|
||||
If the crash folder contains a executable.txt file, upload it here. If not you can ignore this field.
|
||||
If it is too big to upload, compress it.
|
||||
|
||||
Note: executable.txt is the quickshell binary. It has a .txt extension due to github's limitations on
|
||||
filetypes.
|
56
.github/workflows/build.yml
vendored
56
.github/workflows/build.yml
vendored
|
@ -1,56 +0,0 @@
|
|||
name: Build
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
nix:
|
||||
name: Nix
|
||||
strategy:
|
||||
matrix:
|
||||
qtver: [qt6.8.1, qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0]
|
||||
compiler: [clang, gcc]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# Use cachix action over detsys for testing with act.
|
||||
# - uses: cachix/install-nix-action@v27
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
|
||||
- name: Download Dependencies
|
||||
run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).inputDerivation'
|
||||
|
||||
- name: Build
|
||||
run: nix-build --no-out-link --expr '(import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }'
|
||||
|
||||
archlinux:
|
||||
name: Archlinux
|
||||
runs-on: ubuntu-latest
|
||||
container: archlinux
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download Dependencies
|
||||
run: |
|
||||
pacman --noconfirm --noprogressbar -Syyu
|
||||
pacman --noconfirm --noprogressbar -Sy \
|
||||
base-devel \
|
||||
cmake \
|
||||
ninja \
|
||||
pkgconf \
|
||||
qt6-base \
|
||||
qt6-declarative \
|
||||
qt6-svg \
|
||||
qt6-wayland \
|
||||
qt6-shadertools \
|
||||
wayland-protocols \
|
||||
wayland \
|
||||
libdrm \
|
||||
libxcb \
|
||||
libpipewire \
|
||||
cli11 \
|
||||
jemalloc
|
||||
|
||||
- name: Build
|
||||
# breakpad is annoying to build in ci due to makepkg not running as root
|
||||
run: |
|
||||
cmake -GNinja -B build -DCRASH_REPORTER=OFF
|
||||
cmake --build build
|
25
.github/workflows/lint.yml
vendored
25
.github/workflows/lint.yml
vendored
|
@ -1,25 +0,0 @@
|
|||
name: Lint
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# Use cachix action over detsys for testing with act.
|
||||
# - uses: cachix/install-nix-action@v27
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: nicknovitski/nix-develop@v1
|
||||
|
||||
- name: Check formatting
|
||||
run: clang-format -Werror --dry-run src/**/*.{cpp,hpp}
|
||||
|
||||
# required for lint
|
||||
- name: Build
|
||||
run: |
|
||||
just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON
|
||||
just build
|
||||
|
||||
- name: Run lints
|
||||
run: just lint-ci
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,7 +1,3 @@
|
|||
# related repos
|
||||
/docs
|
||||
/examples
|
||||
|
||||
# build files
|
||||
/result
|
||||
/build/
|
||||
|
|
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
[submodule "docs"]
|
||||
path = docs
|
||||
url = https://git.outfoxxed.me/outfoxxed/quickshell-docs
|
||||
[submodule "examples"]
|
||||
path = examples
|
||||
url = https://git.outfoxxed.me/outfoxxed/quickshell-examples
|
243
BUILD.md
243
BUILD.md
|
@ -1,243 +0,0 @@
|
|||
# Build instructions
|
||||
Instructions for building from source and distro packagers. We highly recommend
|
||||
distro packagers read through this page fully.
|
||||
|
||||
## Packaging
|
||||
If you are packaging quickshell for official or unofficial distribution channels,
|
||||
such as a distro package repository, user repository, or other shared build location,
|
||||
please set the following CMake flags.
|
||||
|
||||
`-DDISTRIBUTOR="your distribution platform"`
|
||||
|
||||
Please make this descriptive enough to identify your specific package, for example:
|
||||
- `Official Nix Flake`
|
||||
- `AUR (quickshell-git)`
|
||||
- `Nixpkgs`
|
||||
- `Fedora COPR (errornointernet/quickshell)`
|
||||
|
||||
`-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=YES/NO`
|
||||
|
||||
If we can retrieve binaries and debug information for the package without actually running your
|
||||
distribution (e.g. from an website), and you would like to strip the binary, please set this to `YES`.
|
||||
|
||||
If we cannot retrieve debug information, please set this to `NO` and
|
||||
**ensure you aren't distributing stripped (non debuggable) binaries**.
|
||||
|
||||
In both cases you should build with `-DCMAKE_BUILD_TYPE=RelWithDebInfo` (then split or keep the debuginfo).
|
||||
|
||||
### QML Module dir
|
||||
Currently all QML modules are statically linked to quickshell, but this is where
|
||||
tooling information will go.
|
||||
|
||||
`-DINSTALL_QML_PREFIX="path/to/qml"`
|
||||
|
||||
`-DINSTALL_QMLDIR="/full/path/to/qml"`
|
||||
|
||||
`INSTALL_QML_PREFIX` works the same as `INSTALL_QMLDIR`, except it prepends `CMAKE_INSTALL_PREFIX`. You usually want this.
|
||||
|
||||
## Dependencies
|
||||
Quickshell has a set of base dependencies you will always need, names vary by distro:
|
||||
|
||||
- `cmake`
|
||||
- `qt6base`
|
||||
- `qt6declarative`
|
||||
- `qtshadertools` (build-time only)
|
||||
- `spirv-tools` (build-time only)
|
||||
- `pkg-config` (build-time only)
|
||||
- `cli11`
|
||||
|
||||
On some distros, private Qt headers are in separate packages which you may have to install.
|
||||
We currently require private headers for the following libraries:
|
||||
|
||||
- `qt6declarative`
|
||||
- `qt6wayland`
|
||||
|
||||
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.
|
||||
|
||||
### Crash Reporter
|
||||
The crash reporter catches crashes, restarts quickshell when it crashes,
|
||||
and collects useful crash information in one place. Leaving this enabled will
|
||||
enable us to fix bugs far more easily.
|
||||
|
||||
To disable: `-DCRASH_REPORTER=OFF`
|
||||
|
||||
Dependencies: `google-breakpad`
|
||||
|
||||
### 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`
|
||||
|
||||
#### Screencopy
|
||||
Enables streaming video from monitors and toplevel windows through various protocols.
|
||||
|
||||
To disable: `-DSCREENCOPY=OFF`
|
||||
|
||||
Dependencies:
|
||||
- `libdrm`
|
||||
- `libgbm`
|
||||
|
||||
Specific protocols can also be disabled:
|
||||
- `DSCREENCOPY_ICC=OFF` - Disable screencopy via [ext-image-copy-capture-v1]
|
||||
- `DSCREENCOPY_WLR=OFF` - Disable screencopy via [zwlr-screencopy-v1]
|
||||
- `DSCREENCOPY_HYPRLAND_TOPLEVEL=OFF` - Disable screencopy via [hyprland-toplevel-export-v1]
|
||||
|
||||
[ext-image-copy-capture-v1]:https://wayland.app/protocols/ext-image-copy-capture-v1
|
||||
[zwlr-screencopy-v1]: https://wayland.app/protocols/wlr-screencopy-unstable-v1
|
||||
[hyprland-toplevel-export-v1]: https://wayland.app/protocols/hyprland-toplevel-export-v1
|
||||
|
||||
### 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)
|
||||
|
||||
### PAM
|
||||
This feature enables PAM integration for user authentication.
|
||||
|
||||
To disable: `-DSERVICE_PAM=OFF`
|
||||
|
||||
Dependencies: `pam`
|
||||
|
||||
### 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 under 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
|
||||
|
||||
### i3/Sway
|
||||
Enables i3 and Sway specific features, does not have any dependency on Wayland or x11.
|
||||
|
||||
To disable: `-DI3=OFF`
|
||||
|
||||
#### i3/Sway IPC
|
||||
Enables interfacing with i3 and Sway's IPC.
|
||||
|
||||
To disable: `-DI3_IPC=OFF`
|
||||
|
||||
## 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.
|
||||
|
||||
#### Building
|
||||
```sh
|
||||
$ cmake --build build
|
||||
```
|
||||
|
||||
#### Installing
|
||||
```sh
|
||||
$ cmake --install build
|
||||
```
|
150
CMakeLists.txt
150
CMakeLists.txt
|
@ -5,79 +5,41 @@ set(QT_MIN_VERSION "6.6.0")
|
|||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(QS_BUILD_OPTIONS "")
|
||||
option(BUILD_TESTING "Build tests" OFF)
|
||||
option(ASAN "Enable ASAN" OFF)
|
||||
option(FRAME_POINTERS "Always keep frame pointers" ${ASAN})
|
||||
|
||||
function(boption VAR NAME DEFAULT)
|
||||
cmake_parse_arguments(PARSE_ARGV 3 arg "" "REQUIRES" "")
|
||||
|
||||
option(${VAR} ${NAME} ${DEFAULT})
|
||||
|
||||
set(STATUS "${VAR}_status")
|
||||
set(EFFECTIVE "${VAR}_effective")
|
||||
set(${STATUS} ${${VAR}})
|
||||
set(${EFFECTIVE} ${${VAR}})
|
||||
|
||||
if (${${VAR}} AND DEFINED arg_REQUIRES)
|
||||
set(REQUIRED_EFFECTIVE "${arg_REQUIRES}_effective")
|
||||
if (NOT ${${REQUIRED_EFFECTIVE}})
|
||||
set(${STATUS} "OFF (Requires ${arg_REQUIRES})")
|
||||
set(${EFFECTIVE} OFF)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(${EFFECTIVE} "${${EFFECTIVE}}" PARENT_SCOPE)
|
||||
|
||||
message(STATUS " ${NAME}: ${${STATUS}}")
|
||||
|
||||
string(APPEND QS_BUILD_OPTIONS "\\n ${NAME}: ${${STATUS}}")
|
||||
set(QS_BUILD_OPTIONS "${QS_BUILD_OPTIONS}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
set(DISTRIBUTOR "Unset" CACHE STRING "Distributor")
|
||||
string(APPEND QS_BUILD_OPTIONS " Distributor: ${DISTRIBUTOR}")
|
||||
option(NVIDIA_COMPAT "Workarounds for nvidia gpus" OFF)
|
||||
option(SOCKETS "Enable unix socket support" ON)
|
||||
option(WAYLAND "Enable wayland support" ON)
|
||||
option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON)
|
||||
option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON)
|
||||
option(HYPRLAND "Support hyprland specific features" ON)
|
||||
option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON)
|
||||
|
||||
message(STATUS "Quickshell configuration")
|
||||
message(STATUS " Distributor: ${DISTRIBUTOR}")
|
||||
boption(DISTRIBUTOR_DEBUGINFO_AVAILABLE "Distributor provided debuginfo" NO)
|
||||
boption(NO_PCH "Disable precompild headers (dev)" OFF)
|
||||
boption(BUILD_TESTING "Build tests (dev)" OFF)
|
||||
boption(ASAN "ASAN (dev)" OFF) # note: better output with gcc than clang
|
||||
boption(FRAME_POINTERS "Keep Frame Pointers (dev)" ${ASAN})
|
||||
message(STATUS " NVIDIA workarounds: ${NVIDIA_COMPAT}")
|
||||
message(STATUS " Build tests: ${BUILD_TESTING}")
|
||||
message(STATUS " Sockets: ${SOCKETS}")
|
||||
message(STATUS " Wayland: ${WAYLAND}")
|
||||
if (WAYLAND)
|
||||
message(STATUS " Wlroots Layershell: ${WAYLAND_WLR_LAYERSHELL}")
|
||||
message(STATUS " Session Lock: ${WAYLAND_SESSION_LOCK}")
|
||||
endif ()
|
||||
message(STATUS " Services")
|
||||
message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}")
|
||||
message(STATUS " Hyprland: ${HYPRLAND}")
|
||||
|
||||
boption(CRASH_REPORTER "Crash Handling" ON)
|
||||
boption(USE_JEMALLOC "Use jemalloc" ON)
|
||||
boption(SOCKETS "Unix Sockets" ON)
|
||||
boption(WAYLAND "Wayland" ON)
|
||||
boption(WAYLAND_WLR_LAYERSHELL " Wlroots Layer-Shell" ON REQUIRES WAYLAND)
|
||||
boption(WAYLAND_SESSION_LOCK " Session Lock" ON REQUIRES WAYLAND)
|
||||
boption(WAYLAND_TOPLEVEL_MANAGEMENT " Foreign Toplevel Management" ON REQUIRES WAYLAND)
|
||||
boption(HYPRLAND " Hyprland" ON REQUIRES WAYLAND)
|
||||
boption(HYPRLAND_IPC " Hyprland IPC" ON REQUIRES HYPRLAND)
|
||||
boption(HYPRLAND_GLOBAL_SHORTCUTS " Hyprland Global Shortcuts" ON REQUIRES HYPRLAND)
|
||||
boption(HYPRLAND_FOCUS_GRAB " Hyprland Focus Grabbing" ON REQUIRES HYPRLAND)
|
||||
boption(HYPRLAND_SURFACE_EXTENSIONS " Hyprland Surface Extensions" ON REQUIRES HYPRLAND)
|
||||
boption(SCREENCOPY " Screencopy" ON REQUIRES WAYLAND)
|
||||
boption(SCREENCOPY_ICC " Image Copy Capture" ON REQUIRES WAYLAND)
|
||||
boption(SCREENCOPY_WLR " Wlroots Screencopy" ON REQUIRES WAYLAND)
|
||||
boption(SCREENCOPY_HYPRLAND_TOPLEVEL " Hyprland Toplevel Export" ON REQUIRES WAYLAND)
|
||||
boption(X11 "X11" ON)
|
||||
boption(I3 "I3/Sway" ON)
|
||||
boption(I3_IPC " I3/Sway IPC" ON REQUIRES I3)
|
||||
boption(SERVICE_STATUS_NOTIFIER "System Tray" ON)
|
||||
boption(SERVICE_PIPEWIRE "PipeWire" ON)
|
||||
boption(SERVICE_MPRIS "Mpris" ON)
|
||||
boption(SERVICE_PAM "Pam" ON)
|
||||
boption(SERVICE_GREETD "Greetd" ON)
|
||||
boption(SERVICE_UPOWER "UPower" ON)
|
||||
boption(SERVICE_NOTIFICATIONS "Notifications" ON)
|
||||
if (NOT DEFINED GIT_REVISION)
|
||||
execute_process(
|
||||
COMMAND git rev-parse HEAD
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE GIT_REVISION
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
endif()
|
||||
|
||||
include(cmake/install-qml-module.cmake)
|
||||
include(cmake/util.cmake)
|
||||
|
||||
add_compile_options(-Wall -Wextra -Wno-vla-cxx-extension)
|
||||
|
||||
# pipewire defines this, breaking PCH
|
||||
add_compile_definitions(_REENTRANT)
|
||||
add_compile_options(-Wall -Wextra)
|
||||
|
||||
if (FRAME_POINTERS)
|
||||
add_compile_options(-fno-omit-frame-pointer)
|
||||
|
@ -98,9 +60,8 @@ if (NOT CMAKE_BUILD_TYPE)
|
|||
set(CMAKE_BUILD_TYPE Debug)
|
||||
endif()
|
||||
|
||||
set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets ShaderTools)
|
||||
|
||||
include(cmake/pch.cmake)
|
||||
set(QT_DEPS Qt6::Gui Qt6::Qml Qt6::Quick Qt6::QuickControls2 Qt6::Widgets)
|
||||
set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets)
|
||||
|
||||
if (BUILD_TESTING)
|
||||
enable_testing()
|
||||
|
@ -109,39 +70,58 @@ if (BUILD_TESTING)
|
|||
endif()
|
||||
|
||||
if (SOCKETS)
|
||||
list(APPEND QT_DEPS Qt6::Network)
|
||||
list(APPEND QT_FPDEPS Network)
|
||||
endif()
|
||||
|
||||
if (WAYLAND)
|
||||
list(APPEND QT_DEPS Qt6::WaylandClient Qt6::WaylandClientPrivate)
|
||||
list(APPEND QT_FPDEPS WaylandClient)
|
||||
endif()
|
||||
|
||||
if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS)
|
||||
if (SERVICE_STATUS_NOTIFIER)
|
||||
set(DBUS ON)
|
||||
endif()
|
||||
|
||||
if (DBUS)
|
||||
list(APPEND QT_DEPS Qt6::DBus)
|
||||
list(APPEND QT_FPDEPS DBus)
|
||||
endif()
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS})
|
||||
|
||||
set(CMAKE_AUTOUIC OFF)
|
||||
qt_standard_project_setup(REQUIRES 6.6)
|
||||
set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules)
|
||||
|
||||
add_subdirectory(src)
|
||||
# pch breaks clang-tidy..... somehow
|
||||
if (NOT NO_PCH)
|
||||
file(GENERATE
|
||||
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/pchstub.cpp
|
||||
CONTENT "// intentionally empty"
|
||||
)
|
||||
|
||||
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})
|
||||
add_library(qt-pch ${CMAKE_CURRENT_BINARY_DIR}/pchstub.cpp)
|
||||
target_link_libraries(qt-pch PRIVATE ${QT_DEPS})
|
||||
target_precompile_headers(qt-pch PUBLIC
|
||||
<memory>
|
||||
<qobject.h>
|
||||
<qqmlengine.h>
|
||||
<qlist.h>
|
||||
<qcolor.h>
|
||||
<qquickitem.h>
|
||||
<qevent.h>
|
||||
)
|
||||
endif()
|
||||
|
||||
install(CODE "
|
||||
execute_process(
|
||||
COMMAND ${CMAKE_COMMAND} -E create_symlink \
|
||||
${CMAKE_INSTALL_FULL_BINDIR}/quickshell \$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/qs
|
||||
)
|
||||
")
|
||||
function (qs_pch target)
|
||||
if (NOT NO_PCH)
|
||||
target_precompile_headers(${target} REUSE_FROM qt-pch)
|
||||
target_link_libraries(${target} PRIVATE ${QT_DEPS}) # required for gcc to accept the pch on plugin targets
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
if (NVIDIA_COMPAT)
|
||||
add_compile_definitions(NVIDIA_COMPAT)
|
||||
endif()
|
||||
|
||||
add_subdirectory(src)
|
||||
|
|
102
CONTRIBUTING.md
102
CONTRIBUTING.md
|
@ -1,102 +0,0 @@
|
|||
# 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.
|
||||
You can reference other types using the `@@[Module.][Type.][member]` shorthand
|
||||
where all parts are optional. If module or type are not specified they will
|
||||
be inferred as the current module. Member can be a `property`, `function()` or `signal(s)`.
|
||||
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.
|
8
Justfile
8
Justfile
|
@ -4,13 +4,7 @@ fmt:
|
|||
find src -type f \( -name "*.cpp" -o -name "*.hpp" \) -print0 | xargs -0 clang-format -i
|
||||
|
||||
lint:
|
||||
find src -type f -name "*.cpp" -print0 | parallel -j$(nproc) -q0 --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
|
||||
|
||||
lint-ci:
|
||||
find src -type f -name "*.cpp" -print0 | parallel -j$(nproc) -q0 --no-notice --will-cite --tty clang-tidy --load={{ env_var("TIDYFOX") }}
|
||||
|
||||
lint-changed:
|
||||
git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
|
||||
find src -type f -name "*.cpp" -print0 | parallel -q0 --eta clang-tidy --load={{ env_var("TIDYFOX") }}
|
||||
|
||||
configure target='debug' *FLAGS='':
|
||||
cmake -GNinja -B {{builddir}} \
|
||||
|
|
112
README.md
112
README.md
|
@ -1,7 +1,7 @@
|
|||
# quickshell
|
||||
<a href="https://matrix.to/#/#quickshell:outfoxxed.me"><img src="https://img.shields.io/badge/Join%20the%20matrix%20room-%23quickshell:outfoxxed.me-0dbd8b?logo=matrix&style=flat-square"></a>
|
||||
|
||||
Flexbile QtQuick based desktop shell toolkit.
|
||||
Simple and flexbile QtQuick based desktop shell toolkit.
|
||||
|
||||
Hosted on: [outfoxxed's gitea], [github]
|
||||
|
||||
|
@ -11,14 +11,21 @@ Hosted on: [outfoxxed's gitea], [github]
|
|||
Documentation available at [quickshell.outfoxxed.me](https://quickshell.outfoxxed.me) or
|
||||
can be built from the [quickshell-docs](https://git.outfoxxed.me/outfoxxed/quickshell-docs) repo.
|
||||
|
||||
Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples)
|
||||
Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples)
|
||||
repo.
|
||||
|
||||
# Breaking Changes
|
||||
Quickshell is still in alpha and there will be breaking changes.
|
||||
Both the documentation and examples are included as submodules with revisions that work with the current
|
||||
version of quickshell.
|
||||
|
||||
Commits with breaking qml api changes will contain a `!` at the end of the scope
|
||||
(`thing!: foo`) and the commit description will contain details about the broken api.
|
||||
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
|
||||
|
||||
|
@ -32,9 +39,6 @@ This repo has a nix flake you can use to install the package directly:
|
|||
|
||||
quickshell = {
|
||||
url = "git+https://git.outfoxxed.me/outfoxxed/quickshell";
|
||||
|
||||
# THIS IS IMPORTANT
|
||||
# Mismatched system dependencies will lead to crashes and other issues.
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
@ -44,45 +48,75 @@ This repo has a nix flake you can use to install the package directly:
|
|||
Quickshell's binary is available at `quickshell.packages.<system>.default` to be added to
|
||||
lists such as `environment.systemPackages` or `home.packages`.
|
||||
|
||||
The package contains several features detailed in [BUILD.md](BUILD.md) which can be enabled
|
||||
or disabled with overrides:
|
||||
|
||||
```nix
|
||||
quickshell.packages.<system>.default.override {
|
||||
withJemalloc = true;
|
||||
withQtSvg = true;
|
||||
withWayland = true;
|
||||
withX11 = true;
|
||||
withPipewire = true;
|
||||
withPam = true;
|
||||
withHyprland = true;
|
||||
}
|
||||
```
|
||||
`quickshell.packages.<system>.nvidia` is also available for nvidia users which fixes some
|
||||
common crashes.
|
||||
|
||||
Note: by default this package is built with clang as it is significantly faster.
|
||||
|
||||
## Arch (AUR)
|
||||
Quickshell has a third party [AUR package] available under the same name.
|
||||
It is not managed by us and should be looked over before use.
|
||||
## Manual
|
||||
|
||||
[AUR package]: https://aur.archlinux.org/packages/quickshell
|
||||
If not using nix, you'll have to build from source.
|
||||
|
||||
> [!CAUTION]
|
||||
> The AUR provides no way to force the quickshell package to rebuild when the Qt version changes.
|
||||
> If you experience crashes after updating Qt, please try rebuilding Quickshell against the
|
||||
> current Qt version before opening an issue.
|
||||
### Dependencies
|
||||
To build quickshell at all, you will need the following packages (names may vary by distro)
|
||||
|
||||
## Fedora (COPR)
|
||||
Quickshell has a third party [Fedora COPR package] available under the same name.
|
||||
It is not managed by us and should be looked over before use.
|
||||
- just
|
||||
- cmake
|
||||
- pkg-config
|
||||
- ninja
|
||||
- Qt6 [ QtBase, QtDeclarative ]
|
||||
|
||||
[Fedora COPR package]: https://copr.fedorainfracloud.org/coprs/errornointernet/quickshell
|
||||
To build with wayland support you will additionally need:
|
||||
- wayland
|
||||
- wayland-scanner (may be part of wayland on some distros)
|
||||
- wayland-protocols
|
||||
- Qt6 [ QtWayland ]
|
||||
|
||||
## Anything else
|
||||
See [BUILD.md](BUILD.md) for instructions on building and packaging quickshell.
|
||||
### Building
|
||||
|
||||
# Contributing / Development
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
||||
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
|
||||
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
qtver,
|
||||
compiler,
|
||||
}: let
|
||||
nixpkgs = (import ./nix-checkouts.nix).${builtins.replaceStrings ["."] ["_"] qtver};
|
||||
compilerOverride = (nixpkgs.callPackage ./variations.nix {}).${compiler};
|
||||
pkg = (nixpkgs.callPackage ../default.nix {}).override compilerOverride;
|
||||
in pkg
|
|
@ -1,63 +0,0 @@
|
|||
let
|
||||
byCommit = {
|
||||
commit,
|
||||
sha256,
|
||||
}: import (builtins.fetchTarball {
|
||||
name = "nixpkgs-${commit}";
|
||||
url = "https://github.com/nixos/nixpkgs/archive/${commit}.tar.gz";
|
||||
inherit sha256;
|
||||
}) {};
|
||||
in {
|
||||
# For old qt versions, grab the commit before the version bump that has all the patches
|
||||
# instead of the bumped version.
|
||||
|
||||
qt6_8_1 = byCommit {
|
||||
commit = "3df3c47c19dc90fec35359e89ffb52b34d2b0e94";
|
||||
sha256 = "1lhlm7czhwwys5ak6ngb5li6bxddilb9479k9nkss502kw8hwjyz";
|
||||
};
|
||||
|
||||
qt6_8_0 = byCommit {
|
||||
commit = "352f462ad9d2aa2cde75fdd8f1734e86402a3ff6";
|
||||
sha256 = "02zfgkr9fpd6iwfh6dcr3m6fnx61jppm3v081f3brvkqwmmz7zq1";
|
||||
};
|
||||
|
||||
qt6_7_3 = byCommit {
|
||||
commit = "273673e839189c26130d48993d849a84199523e6";
|
||||
sha256 = "0aca369hdxb8j0vx9791anyzy4m65zckx0lriicqhp95kv9q6m7z";
|
||||
};
|
||||
|
||||
qt6_7_2 = byCommit {
|
||||
commit = "841f166ff96fc2f3ecd1c0cc08072633033d41bf";
|
||||
sha256 = "0d7p0cp7zjiadhpa6sdafxvrpw4lnmb1h673w17q615vm1yaasvy";
|
||||
};
|
||||
|
||||
qt6_7_1 = byCommit {
|
||||
commit = "69bee9866a4e2708b3153fdb61c1425e7857d6b8";
|
||||
sha256 = "1an4sha4jsa29dvc4n9mqxbq8jjwg7frl0rhy085g73m7l1yx0lj";
|
||||
};
|
||||
|
||||
qt6_7_0 = byCommit {
|
||||
commit = "4fbbc17ccf11bc80002b19b31387c9c80276f076";
|
||||
sha256 = "09lhgdqlx8j9a7vpdcf8sddlhbzjq0s208spfmxfjdn14fvx8k0j";
|
||||
};
|
||||
|
||||
qt6_6_3 = byCommit {
|
||||
commit = "8f1a3fbaa92f1d59b09f2d24af6a607b5a280071";
|
||||
sha256 = "0322zwxvmg8v2wkm03xpk6mqmmbfjgrhc9prcx0zd36vjl6jmi18";
|
||||
};
|
||||
|
||||
qt6_6_2 = byCommit {
|
||||
commit = "0bb9cfbd69459488576a0ef3c0e0477bedc3a29e";
|
||||
sha256 = "172ww486jm1mczk9id78s32p7ps9m9qgisml286flc8jffb6yad8";
|
||||
};
|
||||
|
||||
qt6_6_1 = byCommit {
|
||||
commit = "8eecc3342103c38eea666309a7c0d90d403a039a";
|
||||
sha256 = "1lakc0immsgrpz3basaysdvd0sx01r0mcbyymx6id12fk0404z5r";
|
||||
};
|
||||
|
||||
qt6_6_0 = byCommit {
|
||||
commit = "1ded005f95a43953112ffc54b39593ea2f16409f";
|
||||
sha256 = "1xvyd3lj81hak9j53mrhdsqx78x5v2ppv8m2s54qa2099anqgm0f";
|
||||
};
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
clangStdenv,
|
||||
gccStdenv,
|
||||
}: {
|
||||
clang = { buildStdenv = clangStdenv; };
|
||||
gcc = { buildStdenv = gccStdenv; };
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
set(INSTALL_QMLDIR "" CACHE STRING "QML install dir")
|
||||
set(INSTALL_QML_PREFIX "" CACHE STRING "QML install prefix")
|
||||
|
||||
# There doesn't seem to be a standard cross-distro qml install path.
|
||||
if ("${INSTALL_QMLDIR}" STREQUAL "" AND "${INSTALL_QML_PREFIX}" STREQUAL "")
|
||||
message(WARNING "Neither INSTALL_QMLDIR nor INSTALL_QML_PREFIX is set. QML modules will not be installed.")
|
||||
else()
|
||||
if ("${INSTALL_QMLDIR}" STREQUAL "")
|
||||
set(QML_FULL_INSTALLDIR "${CMAKE_INSTALL_PREFIX}/${INSTALL_QML_PREFIX}")
|
||||
else()
|
||||
set(QML_FULL_INSTALLDIR "${INSTALL_QMLDIR}")
|
||||
endif()
|
||||
|
||||
message(STATUS "QML install dir: ${QML_FULL_INSTALLDIR}")
|
||||
endif()
|
||||
|
||||
# Install a given target as a QML module. This is mostly pulled from ECM, as there does not seem
|
||||
# to be an official way to do it.
|
||||
# see https://github.com/KDE/extra-cmake-modules/blob/fe0f606bf7f222e36f7560fd7a2c33ef993e23bb/modules/ECMQmlModule6.cmake#L160
|
||||
function(install_qml_module arg_TARGET)
|
||||
if (NOT DEFINED QML_FULL_INSTALLDIR)
|
||||
return()
|
||||
endif()
|
||||
|
||||
qt_query_qml_module(${arg_TARGET}
|
||||
URI module_uri
|
||||
VERSION module_version
|
||||
PLUGIN_TARGET module_plugin_target
|
||||
TARGET_PATH module_target_path
|
||||
QMLDIR module_qmldir
|
||||
TYPEINFO module_typeinfo
|
||||
QML_FILES module_qml_files
|
||||
RESOURCES module_resources
|
||||
)
|
||||
|
||||
set(module_dir "${QML_FULL_INSTALLDIR}/${module_target_path}")
|
||||
|
||||
if (NOT TARGET "${module_plugin_target}")
|
||||
message(FATAL_ERROR "install_qml_modules called for a target without a plugin")
|
||||
endif()
|
||||
|
||||
get_target_property(target_type "${arg_TARGET}" TYPE)
|
||||
if (NOT "${target_type}" STREQUAL "STATIC_LIBRARY")
|
||||
install(
|
||||
TARGETS "${arg_TARGET}"
|
||||
LIBRARY DESTINATION "${module_dir}"
|
||||
RUNTIME DESTINATION "${module_dir}"
|
||||
)
|
||||
|
||||
install(
|
||||
TARGETS "${module_plugin_target}"
|
||||
LIBRARY DESTINATION "${module_dir}"
|
||||
RUNTIME DESTINATION "${module_dir}"
|
||||
)
|
||||
endif()
|
||||
|
||||
install(FILES "${module_qmldir}" DESTINATION "${module_dir}")
|
||||
install(FILES "${module_typeinfo}" DESTINATION "${module_dir}")
|
||||
|
||||
# Install QML files
|
||||
list(LENGTH module_qml_files num_files)
|
||||
if (NOT "${module_qml_files}" MATCHES "NOTFOUND" AND ${num_files} GREATER 0)
|
||||
qt_query_qml_module(${arg_TARGET} QML_FILES_DEPLOY_PATHS qml_files_deploy_paths)
|
||||
|
||||
math(EXPR last_index "${num_files} - 1")
|
||||
foreach(i RANGE 0 ${last_index})
|
||||
list(GET module_qml_files ${i} src_file)
|
||||
list(GET qml_files_deploy_paths ${i} deploy_path)
|
||||
get_filename_component(dst_name "${deploy_path}" NAME)
|
||||
get_filename_component(dest_dir "${deploy_path}" DIRECTORY)
|
||||
install(FILES "${src_file}" DESTINATION "${module_dir}/${dest_dir}" RENAME "${dst_name}")
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
# Install resources
|
||||
list(LENGTH module_resources num_files)
|
||||
if (NOT "${module_resources}" MATCHES "NOTFOUND" AND ${num_files} GREATER 0)
|
||||
qt_query_qml_module(${arg_TARGET} RESOURCES_DEPLOY_PATHS resources_deploy_paths)
|
||||
|
||||
math(EXPR last_index "${num_files} - 1")
|
||||
foreach(i RANGE 0 ${last_index})
|
||||
list(GET module_resources ${i} src_file)
|
||||
list(GET resources_deploy_paths ${i} deploy_path)
|
||||
get_filename_component(dst_name "${deploy_path}" NAME)
|
||||
get_filename_component(dest_dir "${deploy_path}" DIRECTORY)
|
||||
install(FILES "${src_file}" DESTINATION "${module_dir}/${dest_dir}" RENAME "${dst_name}")
|
||||
endforeach()
|
||||
endif()
|
||||
endfunction()
|
|
@ -1,85 +0,0 @@
|
|||
# pch breaks clang-tidy..... somehow
|
||||
if (NOT NO_PCH)
|
||||
file(GENERATE
|
||||
OUTPUT ${CMAKE_BINARY_DIR}/pchstub.cpp
|
||||
CONTENT "// intentionally empty"
|
||||
)
|
||||
endif()
|
||||
|
||||
function (qs_pch target)
|
||||
if (NO_PCH)
|
||||
return()
|
||||
endif()
|
||||
|
||||
cmake_parse_arguments(PARSE_ARGV 1 arg "" "SET" "")
|
||||
|
||||
if ("${arg_SET}" STREQUAL "")
|
||||
set(arg_SET "common")
|
||||
endif()
|
||||
|
||||
target_precompile_headers(${target} REUSE_FROM "qs-pchset-${arg_SET}")
|
||||
endfunction()
|
||||
|
||||
function (qs_module_pch target)
|
||||
qs_pch(${target} ${ARGN})
|
||||
qs_pch("${target}plugin" SET plugin)
|
||||
qs_pch("${target}plugin_init" SET plugin)
|
||||
endfunction()
|
||||
|
||||
function (qs_add_pchset SETNAME)
|
||||
if (NO_PCH)
|
||||
return()
|
||||
endif()
|
||||
|
||||
cmake_parse_arguments(PARSE_ARGV 1 arg "" "" "HEADERS;DEPENDENCIES")
|
||||
|
||||
set(LIBNAME "qs-pchset-${SETNAME}")
|
||||
|
||||
add_library(${LIBNAME} ${CMAKE_BINARY_DIR}/pchstub.cpp)
|
||||
target_link_libraries(${LIBNAME} ${arg_DEPENDENCIES})
|
||||
target_precompile_headers(${LIBNAME} PUBLIC ${arg_HEADERS})
|
||||
endfunction()
|
||||
|
||||
set(COMMON_PCH_SET
|
||||
<chrono>
|
||||
<memory>
|
||||
<vector>
|
||||
<qdebug.h>
|
||||
<qobject.h>
|
||||
<qmetatype.h>
|
||||
<qstring.h>
|
||||
<qchar.h>
|
||||
<qlist.h>
|
||||
<qabstractitemmodel.h>
|
||||
)
|
||||
|
||||
qs_add_pchset(common
|
||||
DEPENDENCIES Qt::Quick
|
||||
HEADERS ${COMMON_PCH_SET}
|
||||
)
|
||||
|
||||
qs_add_pchset(large
|
||||
DEPENDENCIES Qt::Quick
|
||||
HEADERS
|
||||
${COMMON_PCH_SET}
|
||||
<qiodevice.h>
|
||||
<qevent.h>
|
||||
<qcoreapplication.h>
|
||||
<qqmlengine.h>
|
||||
<qquickitem.h>
|
||||
<qquickwindow.h>
|
||||
<qcolor.h>
|
||||
<qdir.h>
|
||||
<qtimer.h>
|
||||
<qabstractitemmodel.h>
|
||||
)
|
||||
|
||||
|
||||
# including qplugin.h directly will cause required symbols to disappear
|
||||
qs_add_pchset(plugin
|
||||
DEPENDENCIES Qt::Qml
|
||||
HEADERS
|
||||
<qobject.h>
|
||||
<qjsonobject.h>
|
||||
<qpointer.h>
|
||||
)
|
|
@ -1,29 +0,0 @@
|
|||
# Adds a dependency hint to the link order, but does not block build on the dependency.
|
||||
function (qs_add_link_dependencies target)
|
||||
set_property(
|
||||
TARGET ${target}
|
||||
APPEND PROPERTY INTERFACE_LINK_LIBRARIES
|
||||
${ARGN}
|
||||
)
|
||||
endfunction()
|
||||
|
||||
function (qs_append_qmldir target text)
|
||||
get_property(qmldir_content TARGET ${target} PROPERTY _qt_internal_qmldir_content)
|
||||
|
||||
if ("${qmldir_content}" STREQUAL "")
|
||||
message(WARNING "qs_append_qmldir depends on private Qt cmake code, which has broken.")
|
||||
return()
|
||||
endif()
|
||||
|
||||
set_property(TARGET ${target} APPEND_STRING PROPERTY _qt_internal_qmldir_content ${text})
|
||||
endfunction()
|
||||
|
||||
# DEPENDENCIES introduces a cmake dependency which we don't need with static modules.
|
||||
# This greatly improves comp speed by not introducing those dependencies.
|
||||
function (qs_add_module_deps_light target)
|
||||
foreach (dep IN LISTS ARGN)
|
||||
string(APPEND qmldir_extra "depends ${dep}\n")
|
||||
endforeach()
|
||||
|
||||
qs_append_qmldir(${target} "${qmldir_extra}")
|
||||
endfunction()
|
77
default.nix
77
default.nix
|
@ -3,22 +3,13 @@
|
|||
nix-gitignore,
|
||||
pkgs,
|
||||
keepDebugInfo,
|
||||
buildStdenv ? pkgs.clangStdenv,
|
||||
buildStdenv ? pkgs.clang17Stdenv,
|
||||
|
||||
cmake,
|
||||
ninja,
|
||||
qt6,
|
||||
spirv-tools,
|
||||
cli11,
|
||||
breakpad,
|
||||
jemalloc,
|
||||
wayland,
|
||||
wayland-protocols,
|
||||
libdrm,
|
||||
libgbm ? null,
|
||||
xorg,
|
||||
pipewire,
|
||||
pam,
|
||||
|
||||
gitRev ? (let
|
||||
headExists = builtins.pathExists ./.git/HEAD;
|
||||
|
@ -32,15 +23,9 @@
|
|||
else "unknown"),
|
||||
|
||||
debug ? false,
|
||||
withCrashReporter ? true,
|
||||
withJemalloc ? true, # masks heap fragmentation
|
||||
withQtSvg ? true,
|
||||
withWayland ? true,
|
||||
withX11 ? true,
|
||||
withPipewire ? true,
|
||||
withPam ? true,
|
||||
withHyprland ? true,
|
||||
withI3 ? true,
|
||||
enableWayland ? true,
|
||||
nvidiaCompat ? false,
|
||||
svgSupport ? true, # you almost always want this
|
||||
}: buildStdenv.mkDerivation {
|
||||
pname = "quickshell${lib.optionalString debug "-debug"}";
|
||||
version = "0.1.0";
|
||||
|
@ -49,55 +34,43 @@
|
|||
nativeBuildInputs = with pkgs; [
|
||||
cmake
|
||||
ninja
|
||||
qt6.qtshadertools
|
||||
spirv-tools
|
||||
qt6.wrapQtAppsHook
|
||||
] ++ (lib.optionals enableWayland [
|
||||
pkg-config
|
||||
] ++ (lib.optionals withWayland [
|
||||
wayland-protocols
|
||||
wayland-scanner
|
||||
]);
|
||||
|
||||
buildInputs = [
|
||||
buildInputs = with pkgs; [
|
||||
qt6.qtbase
|
||||
qt6.qtdeclarative
|
||||
cli11
|
||||
]
|
||||
++ lib.optional withCrashReporter breakpad
|
||||
++ lib.optional withJemalloc jemalloc
|
||||
++ lib.optional withQtSvg qt6.qtsvg
|
||||
++ lib.optionals withWayland ([ qt6.qtwayland wayland ] ++ (if libgbm != null then [ libdrm libgbm ] else []))
|
||||
++ lib.optional withX11 xorg.libxcb
|
||||
++ lib.optional withPam pam
|
||||
++ lib.optional withPipewire pipewire;
|
||||
++ (lib.optionals enableWayland [ qt6.qtwayland wayland ])
|
||||
++ (lib.optionals svgSupport [ qt6.qtsvg ]);
|
||||
|
||||
cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
|
||||
QTWAYLANDSCANNER = lib.optionalString enableWayland "${qt6.qtwayland}/libexec/qtwaylandscanner";
|
||||
|
||||
configurePhase = let
|
||||
cmakeBuildType = if debug
|
||||
then "Debug"
|
||||
else "RelWithDebInfo";
|
||||
in ''
|
||||
cmakeBuildType=${cmakeBuildType} # qt6 setup hook resets this for some godforsaken reason
|
||||
cmakeConfigurePhase
|
||||
'';
|
||||
|
||||
cmakeFlags = [
|
||||
(lib.cmakeFeature "DISTRIBUTOR" "Official-Nix-Flake")
|
||||
(lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix)
|
||||
(lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true)
|
||||
(lib.cmakeFeature "GIT_REVISION" gitRev)
|
||||
(lib.cmakeBool "CRASH_REPORTER" withCrashReporter)
|
||||
(lib.cmakeBool "USE_JEMALLOC" withJemalloc)
|
||||
(lib.cmakeBool "WAYLAND" withWayland)
|
||||
(lib.cmakeBool "SCREENCOPY" (libgbm != null))
|
||||
(lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire)
|
||||
(lib.cmakeBool "SERVICE_PAM" withPam)
|
||||
(lib.cmakeBool "HYPRLAND" withHyprland)
|
||||
(lib.cmakeBool "I3" withI3)
|
||||
];
|
||||
"-DGIT_REVISION=${gitRev}"
|
||||
] ++ lib.optional (!enableWayland) "-DWAYLAND=OFF"
|
||||
++ lib.optional nvidiaCompat "-DNVIDIA_COMPAT=ON";
|
||||
|
||||
# How to get debuginfo in gdb from a release build:
|
||||
# 1. build `quickshell.debug`
|
||||
# 2. set NIX_DEBUG_INFO_DIRS="<quickshell.debug store path>/lib/debug"
|
||||
# 3. launch gdb / coredumpctl and debuginfo will work
|
||||
separateDebugInfo = !debug;
|
||||
dontStrip = debug;
|
||||
buildPhase = "ninjaBuildPhase";
|
||||
enableParallelBuilding = true;
|
||||
dontStrip = true;
|
||||
|
||||
meta = with lib; {
|
||||
homepage = "https://git.outfoxxed.me/outfoxxed/quickshell";
|
||||
description = "Flexbile QtQuick based desktop shell toolkit";
|
||||
description = "Simple and flexbile QtQuick based desktop shell toolkit";
|
||||
license = licenses.lgpl3Only;
|
||||
platforms = platforms.linux;
|
||||
};
|
||||
|
|
1
docs
Submodule
1
docs
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 149b784a5a4c40ada67cb9f6af5a5350678ab6d4
|
1
examples
Submodule
1
examples
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit b9e744b50673304dfddb68f3da2a2e906d028b96
|
6
flake.lock
generated
6
flake.lock
generated
|
@ -2,11 +2,11 @@
|
|||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1736012469,
|
||||
"narHash": "sha256-/qlNWm/IEVVH7GfgAIyP6EsVZI6zjAx1cV5zNyrs+rI=",
|
||||
"lastModified": 1709237383,
|
||||
"narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "8f3e1f807051e32d8c95cd12b9b421623850a34d",
|
||||
"rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
@ -12,8 +12,10 @@
|
|||
quickshell = pkgs.callPackage ./default.nix {
|
||||
gitRev = self.rev or self.dirtyRev;
|
||||
};
|
||||
quickshell-nvidia = quickshell.override { nvidiaCompat = true; };
|
||||
|
||||
default = quickshell;
|
||||
nvidia = quickshell-nvidia;
|
||||
});
|
||||
|
||||
devShells = forEachSystem (system: pkgs: rec {
|
||||
|
|
|
@ -15,12 +15,13 @@ in pkgs.mkShell.override { stdenv = quickshell.stdenv; } {
|
|||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
just
|
||||
clang-tools
|
||||
clang-tools_17
|
||||
parallel
|
||||
makeWrapper
|
||||
];
|
||||
|
||||
TIDYFOX = "${tidyfox}/lib/libtidyfox.so";
|
||||
QTWAYLANDSCANNER = quickshell.QTWAYLANDSCANNER;
|
||||
|
||||
shellHook = ''
|
||||
export CMAKE_BUILD_PARALLEL_LEVEL=$(nproc)
|
||||
|
|
|
@ -2,18 +2,8 @@ qt_add_executable(quickshell main.cpp)
|
|||
|
||||
install(TARGETS quickshell RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
|
||||
add_subdirectory(build)
|
||||
add_subdirectory(launch)
|
||||
add_subdirectory(core)
|
||||
add_subdirectory(debug)
|
||||
add_subdirectory(ipc)
|
||||
add_subdirectory(window)
|
||||
add_subdirectory(io)
|
||||
add_subdirectory(widgets)
|
||||
|
||||
if (CRASH_REPORTER)
|
||||
add_subdirectory(crash)
|
||||
endif()
|
||||
|
||||
if (DBUS)
|
||||
add_subdirectory(dbus)
|
||||
|
@ -21,10 +11,6 @@ endif()
|
|||
|
||||
if (WAYLAND)
|
||||
add_subdirectory(wayland)
|
||||
endif()
|
||||
|
||||
if (X11)
|
||||
add_subdirectory(x11)
|
||||
endif()
|
||||
endif ()
|
||||
|
||||
add_subdirectory(services)
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
add_library(quickshell-build INTERFACE)
|
||||
|
||||
if (NOT DEFINED GIT_REVISION)
|
||||
execute_process(
|
||||
COMMAND git rev-parse HEAD
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE GIT_REVISION
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
endif()
|
||||
|
||||
if (CRASH_REPORTER)
|
||||
set(CRASH_REPORTER_DEF 1)
|
||||
else()
|
||||
set(CRASH_REPORTER_DEF 0)
|
||||
endif()
|
||||
|
||||
if (DISTRIBUTOR_DEBUGINFO_AVAILABLE)
|
||||
set(DEBUGINFO_AVAILABLE 1)
|
||||
else()
|
||||
set(DEBUGINFO_AVAILABLE 0)
|
||||
endif()
|
||||
|
||||
configure_file(build.hpp.in build.hpp @ONLY ESCAPE_QUOTES)
|
||||
|
||||
target_include_directories(quickshell-build INTERFACE ${CMAKE_CURRENT_BINARY_DIR})
|
|
@ -1,12 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
// NOLINTBEGIN
|
||||
#define GIT_REVISION "@GIT_REVISION@"
|
||||
#define DISTRIBUTOR "@DISTRIBUTOR@"
|
||||
#define DISTRIBUTOR_DEBUGINFO_AVAILABLE @DEBUGINFO_AVAILABLE@
|
||||
#define CRASH_REPORTER @CRASH_REPORTER_DEF@
|
||||
#define BUILD_TYPE "@CMAKE_BUILD_TYPE@"
|
||||
#define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)"
|
||||
#define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@"
|
||||
#define BUILD_CONFIGURATION "@QS_BUILD_OPTIONS@"
|
||||
// NOLINTEND
|
|
@ -1,14 +1,20 @@
|
|||
qt_add_library(quickshell-core STATIC
|
||||
main.cpp
|
||||
plugin.cpp
|
||||
shell.cpp
|
||||
variants.cpp
|
||||
rootwrapper.cpp
|
||||
proxywindow.cpp
|
||||
reload.cpp
|
||||
rootwrapper.cpp
|
||||
qmlglobal.cpp
|
||||
qmlscreen.cpp
|
||||
region.cpp
|
||||
persistentprops.cpp
|
||||
windowinterface.cpp
|
||||
floatingwindow.cpp
|
||||
panelinterface.cpp
|
||||
popupwindow.cpp
|
||||
singleton.cpp
|
||||
generation.cpp
|
||||
scan.cpp
|
||||
|
@ -20,38 +26,13 @@ qt_add_library(quickshell-core STATIC
|
|||
imageprovider.cpp
|
||||
transformwatcher.cpp
|
||||
boundcomponent.cpp
|
||||
model.cpp
|
||||
elapsedtimer.cpp
|
||||
desktopentry.cpp
|
||||
objectrepeater.cpp
|
||||
platformmenu.cpp
|
||||
qsmenu.cpp
|
||||
retainable.cpp
|
||||
popupanchor.cpp
|
||||
types.cpp
|
||||
qsmenuanchor.cpp
|
||||
clock.cpp
|
||||
logging.cpp
|
||||
paths.cpp
|
||||
instanceinfo.cpp
|
||||
common.cpp
|
||||
iconprovider.cpp
|
||||
scriptmodel.cpp
|
||||
)
|
||||
|
||||
qt_add_qml_module(quickshell-core
|
||||
URI Quickshell
|
||||
VERSION 0.1
|
||||
DEPENDENCIES QtQuick
|
||||
OPTIONAL_IMPORTS Quickshell._Window
|
||||
DEFAULT_IMPORTS Quickshell._Window
|
||||
)
|
||||
set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}")
|
||||
qt_add_qml_module(quickshell-core URI Quickshell VERSION 0.1)
|
||||
|
||||
install_qml_module(quickshell-core)
|
||||
|
||||
target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::Widgets)
|
||||
|
||||
qs_module_pch(quickshell-core SET large)
|
||||
target_link_libraries(quickshell-core PRIVATE ${QT_DEPS})
|
||||
qs_pch(quickshell-core)
|
||||
|
||||
target_link_libraries(quickshell PRIVATE quickshell-coreplugin)
|
||||
|
||||
|
|
|
@ -1,88 +0,0 @@
|
|||
#include "clock.hpp"
|
||||
|
||||
#include <qdatetime.h>
|
||||
#include <qobject.h>
|
||||
#include <qtimer.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
SystemClock::SystemClock(QObject* parent): QObject(parent) {
|
||||
QObject::connect(&this->timer, &QTimer::timeout, this, &SystemClock::onTimeout);
|
||||
this->update();
|
||||
}
|
||||
|
||||
bool SystemClock::enabled() const { return this->mEnabled; }
|
||||
|
||||
void SystemClock::setEnabled(bool enabled) {
|
||||
if (enabled == this->mEnabled) return;
|
||||
this->mEnabled = enabled;
|
||||
emit this->enabledChanged();
|
||||
this->update();
|
||||
}
|
||||
|
||||
SystemClock::Enum SystemClock::precision() const { return this->mPrecision; }
|
||||
|
||||
void SystemClock::setPrecision(SystemClock::Enum precision) {
|
||||
if (precision == this->mPrecision) return;
|
||||
this->mPrecision = precision;
|
||||
emit this->precisionChanged();
|
||||
this->update();
|
||||
}
|
||||
|
||||
void SystemClock::onTimeout() {
|
||||
this->setTime(this->targetTime);
|
||||
this->schedule(this->targetTime);
|
||||
}
|
||||
|
||||
void SystemClock::update() {
|
||||
if (this->mEnabled) {
|
||||
this->setTime(QDateTime::fromMSecsSinceEpoch(0));
|
||||
this->schedule(QDateTime::fromMSecsSinceEpoch(0));
|
||||
} else {
|
||||
this->timer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
void SystemClock::setTime(const QDateTime& targetTime) {
|
||||
auto currentTime = QDateTime::currentDateTime();
|
||||
auto offset = currentTime.msecsTo(targetTime);
|
||||
this->currentTime = offset > -500 && offset < 500 ? targetTime : currentTime;
|
||||
|
||||
auto time = this->currentTime.time();
|
||||
this->currentTime.setTime(QTime(
|
||||
this->mPrecision >= SystemClock::Hours ? time.hour() : 0,
|
||||
this->mPrecision >= SystemClock::Minutes ? time.minute() : 0,
|
||||
this->mPrecision >= SystemClock::Seconds ? time.second() : 0
|
||||
));
|
||||
|
||||
emit this->dateChanged();
|
||||
}
|
||||
|
||||
void SystemClock::schedule(const QDateTime& targetTime) {
|
||||
auto secondPrecision = this->mPrecision >= SystemClock::Seconds;
|
||||
auto minutePrecision = this->mPrecision >= SystemClock::Minutes;
|
||||
auto hourPrecision = this->mPrecision >= SystemClock::Hours;
|
||||
|
||||
auto currentTime = QDateTime::currentDateTime();
|
||||
|
||||
auto offset = currentTime.msecsTo(targetTime);
|
||||
|
||||
// timer skew
|
||||
auto nextTime = offset > 0 && offset < 500 ? targetTime : currentTime;
|
||||
|
||||
auto baseTimeT = nextTime.time();
|
||||
nextTime.setTime(QTime(
|
||||
hourPrecision ? baseTimeT.hour() : 0,
|
||||
minutePrecision ? baseTimeT.minute() : 0,
|
||||
secondPrecision ? baseTimeT.second() : 0
|
||||
));
|
||||
|
||||
if (secondPrecision) nextTime = nextTime.addSecs(1);
|
||||
else if (minutePrecision) nextTime = nextTime.addSecs(60);
|
||||
else if (hourPrecision) nextTime = nextTime.addSecs(3600);
|
||||
|
||||
auto delay = currentTime.msecsTo(nextTime);
|
||||
|
||||
this->timer.start(static_cast<qint32>(delay));
|
||||
this->targetTime = nextTime;
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <qdatetime.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtimer.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
///! System clock accessor.
|
||||
/// SystemClock is a view into the system's clock.
|
||||
/// It updates at hour, minute, or second intervals depending on @@precision.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```qml
|
||||
/// SystemClock {
|
||||
/// id: clock
|
||||
/// precision: SystemClock.Seconds
|
||||
/// }
|
||||
///
|
||||
/// @@QtQuick.Text {
|
||||
/// text: Qt.formatDateTime(clock.date, "hh:mm:ss - yyyy-MM-dd")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// > [!WARNING] Clock updates will trigger within 50ms of the system clock changing,
|
||||
/// > however this can be either before or after the clock changes (+-50ms). If you
|
||||
/// > need a date object, use @@date instead of constructing a new one, or the time
|
||||
/// > of the constructed object could be off by up to a second.
|
||||
class SystemClock: public QObject {
|
||||
Q_OBJECT;
|
||||
/// If the clock should update. Defaults to true.
|
||||
///
|
||||
/// Setting enabled to false pauses the clock.
|
||||
Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged);
|
||||
/// The precision the clock should measure at. Defaults to `SystemClock.Seconds`.
|
||||
Q_PROPERTY(SystemClock::Enum precision READ precision WRITE setPrecision NOTIFY precisionChanged);
|
||||
/// The current date and time.
|
||||
///
|
||||
/// > [!TIP] You can use @@QtQml.Qt.formatDateTime() to get the time as a string in
|
||||
/// > your format of choice.
|
||||
Q_PROPERTY(QDateTime date READ date NOTIFY dateChanged);
|
||||
/// The current hour.
|
||||
Q_PROPERTY(quint32 hours READ hours NOTIFY dateChanged);
|
||||
/// The current minute, or 0 if @@precision is `SystemClock.Hours`.
|
||||
Q_PROPERTY(quint32 minutes READ minutes NOTIFY dateChanged);
|
||||
/// The current second, or 0 if @@precision is `SystemClock.Hours` or `SystemClock.Minutes`.
|
||||
Q_PROPERTY(quint32 seconds READ seconds NOTIFY dateChanged);
|
||||
QML_ELEMENT;
|
||||
|
||||
public:
|
||||
// must be named enum until docgen is ready to handle member enums better
|
||||
enum Enum : quint8 {
|
||||
Hours = 1,
|
||||
Minutes = 2,
|
||||
Seconds = 3,
|
||||
};
|
||||
Q_ENUM(Enum);
|
||||
|
||||
explicit SystemClock(QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] bool enabled() const;
|
||||
void setEnabled(bool enabled);
|
||||
|
||||
[[nodiscard]] SystemClock::Enum precision() const;
|
||||
void setPrecision(SystemClock::Enum precision);
|
||||
|
||||
[[nodiscard]] QDateTime date() const { return this->currentTime; }
|
||||
[[nodiscard]] quint32 hours() const { return this->currentTime.time().hour(); }
|
||||
[[nodiscard]] quint32 minutes() const { return this->currentTime.time().minute(); }
|
||||
[[nodiscard]] quint32 seconds() const { return this->currentTime.time().second(); }
|
||||
|
||||
signals:
|
||||
void enabledChanged();
|
||||
void precisionChanged();
|
||||
void dateChanged();
|
||||
|
||||
private slots:
|
||||
void onTimeout();
|
||||
|
||||
private:
|
||||
bool mEnabled = true;
|
||||
SystemClock::Enum mPrecision = SystemClock::Seconds;
|
||||
QTimer timer;
|
||||
QDateTime currentTime;
|
||||
QDateTime targetTime;
|
||||
|
||||
void update();
|
||||
void setTime(const QDateTime& targetTime);
|
||||
void schedule(const QDateTime& targetTime);
|
||||
};
|
|
@ -1,9 +0,0 @@
|
|||
#include "common.hpp"
|
||||
|
||||
#include <qdatetime.h>
|
||||
|
||||
namespace qs {
|
||||
|
||||
const QDateTime Common::LAUNCH_TIME = QDateTime::currentDateTime();
|
||||
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <qdatetime.h>
|
||||
|
||||
namespace qs {
|
||||
|
||||
struct Common {
|
||||
static const QDateTime LAUNCH_TIME;
|
||||
};
|
||||
|
||||
} // namespace qs
|
|
@ -1,391 +0,0 @@
|
|||
#include "desktopentry.hpp"
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdebug.h>
|
||||
#include <qdir.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qhash.h>
|
||||
#include <qlist.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qnamespace.h>
|
||||
#include <qobject.h>
|
||||
#include <qpair.h>
|
||||
#include <qprocess.h>
|
||||
#include <qstringview.h>
|
||||
#include <qtenvironmentvariables.h>
|
||||
#include <ranges>
|
||||
|
||||
#include "model.hpp"
|
||||
|
||||
namespace {
|
||||
Q_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg);
|
||||
}
|
||||
|
||||
struct Locale {
|
||||
explicit Locale() = default;
|
||||
|
||||
explicit Locale(const QString& string) {
|
||||
auto territoryIdx = string.indexOf('_');
|
||||
auto codesetIdx = string.indexOf('.');
|
||||
auto modifierIdx = string.indexOf('@');
|
||||
|
||||
auto parseEnd = string.length();
|
||||
|
||||
if (modifierIdx != -1) {
|
||||
this->modifier = string.sliced(modifierIdx + 1, parseEnd - modifierIdx - 1);
|
||||
parseEnd = modifierIdx;
|
||||
}
|
||||
|
||||
if (codesetIdx != -1) {
|
||||
parseEnd = codesetIdx;
|
||||
}
|
||||
|
||||
if (territoryIdx != -1) {
|
||||
this->territory = string.sliced(territoryIdx + 1, parseEnd - territoryIdx - 1);
|
||||
parseEnd = territoryIdx;
|
||||
}
|
||||
|
||||
this->language = string.sliced(0, parseEnd);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool isValid() const { return !this->language.isEmpty(); }
|
||||
|
||||
[[nodiscard]] int matchScore(const Locale& other) const {
|
||||
if (this->language != other.language) return 0;
|
||||
auto territoryMatches = !this->territory.isEmpty() && this->territory == other.territory;
|
||||
auto modifierMatches = !this->modifier.isEmpty() && this->modifier == other.modifier;
|
||||
|
||||
auto score = 1;
|
||||
if (territoryMatches) score += 2;
|
||||
if (modifierMatches) score += 1;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
static const Locale& system() {
|
||||
static Locale* locale = nullptr; // NOLINT
|
||||
|
||||
if (locale == nullptr) {
|
||||
auto lstr = qEnvironmentVariable("LC_MESSAGES");
|
||||
if (lstr.isEmpty()) lstr = qEnvironmentVariable("LANG");
|
||||
locale = new Locale(lstr);
|
||||
}
|
||||
|
||||
return *locale;
|
||||
}
|
||||
|
||||
QString language;
|
||||
QString territory;
|
||||
QString modifier;
|
||||
};
|
||||
|
||||
// NOLINTNEXTLINE(misc-use-internal-linkage)
|
||||
QDebug operator<<(QDebug debug, const Locale& locale) {
|
||||
auto saver = QDebugStateSaver(debug);
|
||||
debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory
|
||||
<< ", modifier" << locale.modifier << ')';
|
||||
|
||||
return debug;
|
||||
}
|
||||
|
||||
void DesktopEntry::parseEntry(const QString& text) {
|
||||
const auto& system = Locale::system();
|
||||
|
||||
auto groupName = QString();
|
||||
auto entries = QHash<QString, QPair<Locale, QString>>();
|
||||
|
||||
auto finishCategory = [this, &groupName, &entries]() {
|
||||
if (groupName == "Desktop Entry") {
|
||||
if (entries["Type"].second != "Application") return;
|
||||
if (entries.contains("Hidden") && entries["Hidden"].second == "true") return;
|
||||
|
||||
for (const auto& [key, pair]: entries.asKeyValueRange()) {
|
||||
auto& [_, value] = pair;
|
||||
this->mEntries.insert(key, value);
|
||||
|
||||
if (key == "Name") this->mName = value;
|
||||
else if (key == "GenericName") this->mGenericName = value;
|
||||
else if (key == "NoDisplay") this->mNoDisplay = value == "true";
|
||||
else if (key == "Comment") this->mComment = value;
|
||||
else if (key == "Icon") this->mIcon = value;
|
||||
else if (key == "Exec") this->mExecString = value;
|
||||
else if (key == "Path") this->mWorkingDirectory = value;
|
||||
else if (key == "Terminal") this->mTerminal = value == "true";
|
||||
else if (key == "Categories") this->mCategories = value.split(u';', Qt::SkipEmptyParts);
|
||||
else if (key == "Keywords") this->mKeywords = value.split(u';', Qt::SkipEmptyParts);
|
||||
}
|
||||
} else if (groupName.startsWith("Desktop Action ")) {
|
||||
auto actionName = groupName.sliced(16);
|
||||
auto* action = new DesktopAction(actionName, this);
|
||||
|
||||
for (const auto& [key, pair]: entries.asKeyValueRange()) {
|
||||
const auto& [_, value] = pair;
|
||||
action->mEntries.insert(key, value);
|
||||
|
||||
if (key == "Name") action->mName = value;
|
||||
else if (key == "Icon") action->mIcon = value;
|
||||
else if (key == "Exec") action->mExecString = value;
|
||||
}
|
||||
|
||||
this->mActions.insert(actionName, action);
|
||||
}
|
||||
|
||||
entries.clear();
|
||||
};
|
||||
|
||||
for (auto& line: text.split(u'\n', Qt::SkipEmptyParts)) {
|
||||
if (line.startsWith(u'#')) continue;
|
||||
|
||||
if (line.startsWith(u'[') && line.endsWith(u']')) {
|
||||
finishCategory();
|
||||
groupName = line.sliced(1, line.length() - 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto splitIdx = line.indexOf(u'=');
|
||||
if (splitIdx == -1) {
|
||||
qCWarning(logDesktopEntry) << "Encountered invalid line in desktop entry (no =)" << line;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto key = line.sliced(0, splitIdx);
|
||||
const auto& value = line.sliced(splitIdx + 1);
|
||||
|
||||
auto localeIdx = key.indexOf('[');
|
||||
Locale locale;
|
||||
if (localeIdx != -1 && localeIdx != key.length() - 1) {
|
||||
locale = Locale(key.sliced(localeIdx + 1, key.length() - localeIdx - 2));
|
||||
key = key.sliced(0, localeIdx);
|
||||
}
|
||||
|
||||
if (entries.contains(key)) {
|
||||
const auto& old = entries.value(key);
|
||||
|
||||
auto oldScore = system.matchScore(old.first);
|
||||
auto newScore = system.matchScore(locale);
|
||||
|
||||
if (newScore > oldScore || (oldScore == 0 && !locale.isValid())) {
|
||||
entries.insert(key, qMakePair(locale, value));
|
||||
}
|
||||
} else {
|
||||
entries.insert(key, qMakePair(locale, value));
|
||||
}
|
||||
}
|
||||
|
||||
finishCategory();
|
||||
}
|
||||
|
||||
void DesktopEntry::execute() const {
|
||||
DesktopEntry::doExec(this->mExecString, this->mWorkingDirectory);
|
||||
}
|
||||
|
||||
bool DesktopEntry::isValid() const { return !this->mName.isEmpty(); }
|
||||
bool DesktopEntry::noDisplay() const { return this->mNoDisplay; }
|
||||
|
||||
QVector<DesktopAction*> DesktopEntry::actions() const { return this->mActions.values(); }
|
||||
|
||||
QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
|
||||
QVector<QString> arguments;
|
||||
QString currentArgument;
|
||||
auto parsingString = false;
|
||||
auto escape = 0;
|
||||
auto percent = false;
|
||||
|
||||
for (auto c: execString) {
|
||||
if (escape == 0 && c == u'\\') {
|
||||
escape = 1;
|
||||
} else if (parsingString) {
|
||||
if (c == '\\') {
|
||||
escape++;
|
||||
if (escape == 4) {
|
||||
currentArgument += '\\';
|
||||
escape = 0;
|
||||
}
|
||||
} else if (escape != 0) {
|
||||
if (escape != 2) {
|
||||
// Technically this is an illegal state, but the spec has a terrible double escape
|
||||
// rule in strings for no discernable reason. Assuming someone might understandably
|
||||
// misunderstand it, treat it as a normal escape and log it.
|
||||
qCWarning(logDesktopEntry).noquote()
|
||||
<< "Illegal escape sequence in desktop entry exec string:" << execString;
|
||||
}
|
||||
|
||||
currentArgument += c;
|
||||
escape = 0;
|
||||
} else if (c == u'"' || c == u'\'') {
|
||||
parsingString = false;
|
||||
} else {
|
||||
currentArgument += c;
|
||||
}
|
||||
} else if (escape != 0) {
|
||||
currentArgument += c;
|
||||
escape = 0;
|
||||
} else if (percent) {
|
||||
if (c == '%') {
|
||||
currentArgument += '%';
|
||||
} // else discard
|
||||
|
||||
percent = false;
|
||||
} else if (c == '%') {
|
||||
percent = true;
|
||||
} else if (c == u'"' || c == u'\'') {
|
||||
parsingString = true;
|
||||
} else if (c == u' ') {
|
||||
if (!currentArgument.isEmpty()) {
|
||||
arguments.push_back(currentArgument);
|
||||
currentArgument.clear();
|
||||
}
|
||||
} else {
|
||||
currentArgument += c;
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentArgument.isEmpty()) {
|
||||
arguments.push_back(currentArgument);
|
||||
currentArgument.clear();
|
||||
}
|
||||
|
||||
return arguments;
|
||||
}
|
||||
|
||||
void DesktopEntry::doExec(const QString& execString, const QString& workingDirectory) {
|
||||
auto args = DesktopEntry::parseExecString(execString);
|
||||
if (args.isEmpty()) {
|
||||
qCWarning(logDesktopEntry) << "Tried to exec string" << execString << "which parsed as empty.";
|
||||
return;
|
||||
}
|
||||
|
||||
auto process = QProcess();
|
||||
process.setProgram(args.at(0));
|
||||
process.setArguments(args.sliced(1));
|
||||
if (!workingDirectory.isEmpty()) process.setWorkingDirectory(workingDirectory);
|
||||
process.startDetached();
|
||||
}
|
||||
|
||||
void DesktopAction::execute() const {
|
||||
DesktopEntry::doExec(this->mExecString, this->entry->mWorkingDirectory);
|
||||
}
|
||||
|
||||
DesktopEntryManager::DesktopEntryManager() {
|
||||
this->scanDesktopEntries();
|
||||
this->populateApplications();
|
||||
}
|
||||
|
||||
void DesktopEntryManager::scanDesktopEntries() {
|
||||
QList<QString> dataPaths;
|
||||
|
||||
if (qEnvironmentVariableIsSet("XDG_DATA_DIRS")) {
|
||||
auto var = qEnvironmentVariable("XDG_DATA_DIRS");
|
||||
dataPaths = var.split(u':', Qt::SkipEmptyParts);
|
||||
} else {
|
||||
dataPaths.push_back("/usr/local/share");
|
||||
dataPaths.push_back("/usr/share");
|
||||
}
|
||||
|
||||
qCDebug(logDesktopEntry) << "Creating desktop entry scanners";
|
||||
|
||||
for (auto& path: std::ranges::reverse_view(dataPaths)) {
|
||||
auto p = QDir(path).filePath("applications");
|
||||
auto file = QFileInfo(p);
|
||||
|
||||
if (!file.isDir()) {
|
||||
qCDebug(logDesktopEntry) << "Not scanning path" << p << "as it is not a directory";
|
||||
continue;
|
||||
}
|
||||
|
||||
qCDebug(logDesktopEntry) << "Scanning path" << p;
|
||||
this->scanPath(p);
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopEntryManager::populateApplications() {
|
||||
for (auto& entry: this->desktopEntries.values()) {
|
||||
if (!entry->noDisplay()) this->mApplications.insertObject(entry);
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) {
|
||||
auto entries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
|
||||
|
||||
for (auto& entry: entries) {
|
||||
if (entry.isDir()) this->scanPath(entry.absoluteFilePath(), prefix + dir.dirName() + "-");
|
||||
else if (entry.isFile()) {
|
||||
auto path = entry.filePath();
|
||||
if (!path.endsWith(".desktop")) {
|
||||
qCDebug(logDesktopEntry) << "Skipping file" << path << "as it has no .desktop extension";
|
||||
continue;
|
||||
}
|
||||
|
||||
auto file = QFile(path);
|
||||
if (!file.open(QFile::ReadOnly)) {
|
||||
qCDebug(logDesktopEntry) << "Could not open file" << path;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8);
|
||||
auto lowerId = id.toLower();
|
||||
|
||||
auto text = QString::fromUtf8(file.readAll());
|
||||
auto* dentry = new DesktopEntry(id, this);
|
||||
dentry->parseEntry(text);
|
||||
|
||||
if (!dentry->isValid()) {
|
||||
qCDebug(logDesktopEntry) << "Skipping desktop entry" << path;
|
||||
delete dentry;
|
||||
continue;
|
||||
}
|
||||
|
||||
qCDebug(logDesktopEntry) << "Found desktop entry" << id << "at" << path;
|
||||
|
||||
auto conflictingId = this->desktopEntries.contains(id);
|
||||
|
||||
if (conflictingId) {
|
||||
qCDebug(logDesktopEntry) << "Replacing old entry for" << id;
|
||||
delete this->desktopEntries.value(id);
|
||||
this->desktopEntries.remove(id);
|
||||
this->lowercaseDesktopEntries.remove(lowerId);
|
||||
}
|
||||
|
||||
this->desktopEntries.insert(id, dentry);
|
||||
|
||||
if (this->lowercaseDesktopEntries.contains(lowerId)) {
|
||||
qCInfo(logDesktopEntry).nospace()
|
||||
<< "Multiple desktop entries have the same lowercased id " << lowerId
|
||||
<< ". This can cause ambiguity when byId requests are not made with the correct case "
|
||||
"already.";
|
||||
|
||||
this->lowercaseDesktopEntries.remove(lowerId);
|
||||
}
|
||||
|
||||
this->lowercaseDesktopEntries.insert(lowerId, dentry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DesktopEntryManager* DesktopEntryManager::instance() {
|
||||
static auto* instance = new DesktopEntryManager(); // NOLINT
|
||||
return instance;
|
||||
}
|
||||
|
||||
DesktopEntry* DesktopEntryManager::byId(const QString& id) {
|
||||
if (auto* entry = this->desktopEntries.value(id)) {
|
||||
return entry;
|
||||
} else if (auto* entry = this->lowercaseDesktopEntries.value(id.toLower())) {
|
||||
return entry;
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
ObjectModel<DesktopEntry>* DesktopEntryManager::applications() { return &this->mApplications; }
|
||||
|
||||
DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); }
|
||||
|
||||
DesktopEntry* DesktopEntries::byId(const QString& id) {
|
||||
return DesktopEntryManager::instance()->byId(id);
|
||||
}
|
||||
|
||||
ObjectModel<DesktopEntry>* DesktopEntries::applications() {
|
||||
return DesktopEntryManager::instance()->applications();
|
||||
}
|
|
@ -1,155 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdir.h>
|
||||
#include <qhash.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "doc.hpp"
|
||||
#include "model.hpp"
|
||||
|
||||
class DesktopAction;
|
||||
|
||||
/// A desktop entry. See @@DesktopEntries for details.
|
||||
class DesktopEntry: public QObject {
|
||||
Q_OBJECT;
|
||||
Q_PROPERTY(QString id MEMBER mId CONSTANT);
|
||||
/// Name of the specific application, such as "Firefox".
|
||||
Q_PROPERTY(QString name MEMBER mName CONSTANT);
|
||||
/// Short description of the application, such as "Web Browser". May be empty.
|
||||
Q_PROPERTY(QString genericName MEMBER mGenericName CONSTANT);
|
||||
/// If true, this application should not be displayed in menus and launchers.
|
||||
Q_PROPERTY(bool noDisplay MEMBER mNoDisplay CONSTANT);
|
||||
/// Long description of the application, such as "View websites on the internet". May be empty.
|
||||
Q_PROPERTY(QString comment MEMBER mComment CONSTANT);
|
||||
/// Name of the icon associated with this application. May be empty.
|
||||
Q_PROPERTY(QString icon MEMBER mIcon CONSTANT);
|
||||
/// The raw `Exec` string from the desktop entry. You probably want @@execute().
|
||||
Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
|
||||
/// The working directory to execute from.
|
||||
Q_PROPERTY(QString workingDirectory MEMBER mWorkingDirectory CONSTANT);
|
||||
/// If the application should run in a terminal.
|
||||
Q_PROPERTY(bool runInTerminal MEMBER mTerminal CONSTANT);
|
||||
Q_PROPERTY(QVector<QString> categories MEMBER mCategories CONSTANT);
|
||||
Q_PROPERTY(QVector<QString> keywords MEMBER mKeywords CONSTANT);
|
||||
Q_PROPERTY(QVector<DesktopAction*> actions READ actions CONSTANT);
|
||||
QML_ELEMENT;
|
||||
QML_UNCREATABLE("DesktopEntry instances must be retrieved from DesktopEntries");
|
||||
|
||||
public:
|
||||
explicit DesktopEntry(QString id, QObject* parent): QObject(parent), mId(std::move(id)) {}
|
||||
|
||||
void parseEntry(const QString& text);
|
||||
|
||||
/// Run the application. Currently ignores @@runInTerminal and field codes.
|
||||
Q_INVOKABLE void execute() const;
|
||||
|
||||
[[nodiscard]] bool isValid() const;
|
||||
[[nodiscard]] bool noDisplay() const;
|
||||
[[nodiscard]] QVector<DesktopAction*> actions() const;
|
||||
|
||||
// currently ignores all field codes.
|
||||
static QVector<QString> parseExecString(const QString& execString);
|
||||
static void doExec(const QString& execString, const QString& workingDirectory);
|
||||
|
||||
public:
|
||||
QString mId;
|
||||
QString mName;
|
||||
QString mGenericName;
|
||||
bool mNoDisplay = false;
|
||||
QString mComment;
|
||||
QString mIcon;
|
||||
QString mExecString;
|
||||
QString mWorkingDirectory;
|
||||
bool mTerminal = false;
|
||||
QVector<QString> mCategories;
|
||||
QVector<QString> mKeywords;
|
||||
|
||||
private:
|
||||
QHash<QString, QString> mEntries;
|
||||
QHash<QString, DesktopAction*> mActions;
|
||||
|
||||
friend class DesktopAction;
|
||||
};
|
||||
|
||||
/// An action of a @@DesktopEntry$.
|
||||
class DesktopAction: public QObject {
|
||||
Q_OBJECT;
|
||||
Q_PROPERTY(QString id MEMBER mId CONSTANT);
|
||||
Q_PROPERTY(QString name MEMBER mName CONSTANT);
|
||||
Q_PROPERTY(QString icon MEMBER mIcon CONSTANT);
|
||||
/// The raw `Exec` string from the desktop entry. You probably want @@execute().
|
||||
Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
|
||||
QML_ELEMENT;
|
||||
QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry");
|
||||
|
||||
public:
|
||||
explicit DesktopAction(QString id, DesktopEntry* entry)
|
||||
: QObject(entry)
|
||||
, entry(entry)
|
||||
, mId(std::move(id)) {}
|
||||
|
||||
/// Run the application. Currently ignores @@DesktopEntry.runInTerminal and field codes.
|
||||
Q_INVOKABLE void execute() const;
|
||||
|
||||
private:
|
||||
DesktopEntry* entry;
|
||||
QString mId;
|
||||
QString mName;
|
||||
QString mIcon;
|
||||
QString mExecString;
|
||||
QHash<QString, QString> mEntries;
|
||||
|
||||
friend class DesktopEntry;
|
||||
};
|
||||
|
||||
class DesktopEntryManager: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
void scanDesktopEntries();
|
||||
|
||||
[[nodiscard]] DesktopEntry* byId(const QString& id);
|
||||
|
||||
[[nodiscard]] ObjectModel<DesktopEntry>* applications();
|
||||
|
||||
static DesktopEntryManager* instance();
|
||||
|
||||
private:
|
||||
explicit DesktopEntryManager();
|
||||
|
||||
void populateApplications();
|
||||
void scanPath(const QDir& dir, const QString& prefix = QString());
|
||||
|
||||
QHash<QString, DesktopEntry*> desktopEntries;
|
||||
QHash<QString, DesktopEntry*> lowercaseDesktopEntries;
|
||||
ObjectModel<DesktopEntry> mApplications {this};
|
||||
};
|
||||
|
||||
///! Desktop entry index.
|
||||
/// Index of desktop entries according to the [desktop entry specification].
|
||||
///
|
||||
/// Primarily useful for looking up icons and metadata from an id, as there is
|
||||
/// currently no mechanism for usage based sorting of entries and other launcher niceties.
|
||||
///
|
||||
/// [desktop entry specification]: https://specifications.freedesktop.org/desktop-entry-spec/latest/
|
||||
class DesktopEntries: public QObject {
|
||||
Q_OBJECT;
|
||||
/// All desktop entries of type Application that are not Hidden or NoDisplay.
|
||||
QSDOC_TYPE_OVERRIDE(ObjectModel<DesktopEntry>*);
|
||||
Q_PROPERTY(UntypedObjectModel* applications READ applications CONSTANT);
|
||||
QML_ELEMENT;
|
||||
QML_SINGLETON;
|
||||
|
||||
public:
|
||||
explicit DesktopEntries();
|
||||
|
||||
/// Look up a desktop entry by name. Includes NoDisplay entries. May return null.
|
||||
Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id);
|
||||
|
||||
[[nodiscard]] static ObjectModel<DesktopEntry>* applications();
|
||||
};
|
|
@ -10,14 +10,5 @@
|
|||
#define QSDOC_ELEMENT
|
||||
#define QSDOC_NAMED_ELEMENT(name)
|
||||
|
||||
// unmark uncreatable (will be overlayed by other types)
|
||||
#define QSDOC_CREATABLE
|
||||
|
||||
// change the cname used for this type
|
||||
#define QSDOC_CNAME(name)
|
||||
|
||||
// overridden properties
|
||||
#define QSDOC_PROPERTY_OVERRIDE(...)
|
||||
|
||||
// override types of properties for docs
|
||||
#define QSDOC_TYPE_OVERRIDE(type)
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
#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();
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
#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;
|
||||
};
|
|
@ -32,12 +32,10 @@ FloatingWindowInterface::FloatingWindowInterface(QObject* parent)
|
|||
QObject::connect(this->window, &ProxyWindowBase::backerVisibilityChanged, this, &FloatingWindowInterface::backingWindowVisibleChanged);
|
||||
QObject::connect(this->window, &ProxyWindowBase::heightChanged, this, &FloatingWindowInterface::heightChanged);
|
||||
QObject::connect(this->window, &ProxyWindowBase::widthChanged, this, &FloatingWindowInterface::widthChanged);
|
||||
QObject::connect(this->window, &ProxyWindowBase::devicePixelRatioChanged, this, &FloatingWindowInterface::devicePixelRatioChanged);
|
||||
QObject::connect(this->window, &ProxyWindowBase::screenChanged, this, &FloatingWindowInterface::screenChanged);
|
||||
QObject::connect(this->window, &ProxyWindowBase::windowTransformChanged, this, &FloatingWindowInterface::windowTransformChanged);
|
||||
QObject::connect(this->window, &ProxyWindowBase::colorChanged, this, &FloatingWindowInterface::colorChanged);
|
||||
QObject::connect(this->window, &ProxyWindowBase::maskChanged, this, &FloatingWindowInterface::maskChanged);
|
||||
QObject::connect(this->window, &ProxyWindowBase::surfaceFormatChanged, this, &FloatingWindowInterface::surfaceFormatChanged);
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
|
@ -51,13 +49,10 @@ void FloatingWindowInterface::onReload(QObject* oldInstance) {
|
|||
QQmlListProperty<QObject> FloatingWindowInterface::data() { return this->window->data(); }
|
||||
ProxyWindowBase* FloatingWindowInterface::proxyWindow() const { return this->window; }
|
||||
QQuickItem* FloatingWindowInterface::contentItem() const { return this->window->contentItem(); }
|
||||
|
||||
bool FloatingWindowInterface::isBackingWindowVisible() const {
|
||||
return this->window->isVisibleDirect();
|
||||
}
|
||||
|
||||
qreal FloatingWindowInterface::devicePixelRatio() const { return this->window->devicePixelRatio(); }
|
||||
|
||||
// NOLINTBEGIN
|
||||
#define proxyPair(type, get, set) \
|
||||
type FloatingWindowInterface::get() const { return this->window->get(); } \
|
||||
|
@ -69,7 +64,6 @@ proxyPair(qint32, height, setHeight);
|
|||
proxyPair(QuickshellScreenInfo*, screen, setScreen);
|
||||
proxyPair(QColor, color, setColor);
|
||||
proxyPair(PendingRegion*, mask, setMask);
|
||||
proxyPair(QsSurfaceFormat, surfaceFormat, setSurfaceFormat);
|
||||
|
||||
#undef proxyPair
|
||||
// NOLINTEND
|
|
@ -4,7 +4,6 @@
|
|||
#include <qtmetamacros.h>
|
||||
|
||||
#include "proxywindow.hpp"
|
||||
#include "windowinterface.hpp"
|
||||
|
||||
class ProxyFloatingWindow: public ProxyWindowBase {
|
||||
Q_OBJECT;
|
||||
|
@ -18,7 +17,7 @@ public:
|
|||
void setHeight(qint32 height) override;
|
||||
};
|
||||
|
||||
///! Standard toplevel operating system window that looks like any other application.
|
||||
///! Standard floating window.
|
||||
class FloatingWindowInterface: public WindowInterface {
|
||||
Q_OBJECT;
|
||||
QML_NAMED_ELEMENT(FloatingWindow);
|
||||
|
@ -42,8 +41,6 @@ public:
|
|||
[[nodiscard]] qint32 height() const override;
|
||||
void setHeight(qint32 height) override;
|
||||
|
||||
[[nodiscard]] virtual qreal devicePixelRatio() const override;
|
||||
|
||||
[[nodiscard]] QuickshellScreenInfo* screen() const override;
|
||||
void setScreen(QuickshellScreenInfo* screen) override;
|
||||
|
||||
|
@ -53,9 +50,6 @@ public:
|
|||
[[nodiscard]] PendingRegion* mask() const override;
|
||||
void setMask(PendingRegion* mask) override;
|
||||
|
||||
[[nodiscard]] QsSurfaceFormat surfaceFormat() const override;
|
||||
void setSurfaceFormat(QsSurfaceFormat mask) override;
|
||||
|
||||
[[nodiscard]] QQmlListProperty<QObject> data() override;
|
||||
// NOLINTEND
|
||||
|
|
@ -4,8 +4,6 @@
|
|||
#include <qcontainerfwd.h>
|
||||
#include <qcoreapplication.h>
|
||||
#include <qdebug.h>
|
||||
#include <qdir.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qfilesystemwatcher.h>
|
||||
#include <qhash.h>
|
||||
#include <qlogging.h>
|
||||
|
@ -14,6 +12,7 @@
|
|||
#include <qqmlcontext.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qqmlincubator.h>
|
||||
#include <qtimer.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "iconimageprovider.hpp"
|
||||
|
@ -24,12 +23,10 @@
|
|||
#include "reload.hpp"
|
||||
#include "scan.hpp"
|
||||
|
||||
static QHash<const QQmlEngine*, EngineGeneration*> g_generations; // NOLINT
|
||||
static QHash<QQmlEngine*, EngineGeneration*> g_generations; // NOLINT
|
||||
|
||||
EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
|
||||
: rootPath(rootPath)
|
||||
, scanner(std::move(scanner))
|
||||
, urlInterceptor(this->rootPath)
|
||||
EngineGeneration::EngineGeneration(QmlScanner scanner)
|
||||
: scanner(std::move(scanner))
|
||||
, interceptNetFactory(this->scanner.qmldirIntercepts)
|
||||
, engine(new QQmlEngine()) {
|
||||
g_generations.insert(this->engine, this);
|
||||
|
@ -42,93 +39,56 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
|
|||
this->engine->addImageProvider("qsimage", new QsImageProvider());
|
||||
this->engine->addImageProvider("qspixmap", new QsPixmapProvider());
|
||||
|
||||
QsEnginePlugin::runConstructGeneration(*this);
|
||||
QuickshellPlugin::runConstructGeneration(*this);
|
||||
}
|
||||
|
||||
EngineGeneration::~EngineGeneration() {
|
||||
if (this->engine != nullptr) {
|
||||
qFatal() << this << "destroyed without calling destroy()";
|
||||
}
|
||||
g_generations.remove(this->engine);
|
||||
delete this->engine;
|
||||
}
|
||||
|
||||
void EngineGeneration::destroy() {
|
||||
if (this->destroying) return;
|
||||
this->destroying = true;
|
||||
// Multiple generations can detect a reload at the same time.
|
||||
delete this->watcher;
|
||||
this->watcher = nullptr;
|
||||
|
||||
if (this->watcher != nullptr) {
|
||||
// Multiple generations can detect a reload at the same time.
|
||||
QObject::disconnect(this->watcher, nullptr, this, nullptr);
|
||||
this->watcher->deleteLater();
|
||||
this->watcher = nullptr;
|
||||
}
|
||||
|
||||
for (auto* extension: this->extensions.values()) {
|
||||
delete extension;
|
||||
}
|
||||
|
||||
if (this->root != nullptr) {
|
||||
// Yes all of this is actually necessary.
|
||||
if (this->engine != nullptr && this->root != nullptr) {
|
||||
QObject::connect(this->root, &QObject::destroyed, this, [this]() {
|
||||
// prevent further js execution between garbage collection and engine destruction.
|
||||
this->engine->setInterrupted(true);
|
||||
// The timer seems to fix *one* of the possible qml item destructor crashes.
|
||||
QTimer::singleShot(0, [this]() {
|
||||
// Garbage is not collected during engine destruction.
|
||||
this->engine->collectGarbage();
|
||||
|
||||
g_generations.remove(this->engine);
|
||||
QObject::connect(this->engine, &QObject::destroyed, this, [this]() { delete this; });
|
||||
|
||||
// Garbage is not collected during engine destruction.
|
||||
this->engine->collectGarbage();
|
||||
|
||||
delete this->engine;
|
||||
this->engine = nullptr;
|
||||
|
||||
auto terminate = this->shouldTerminate;
|
||||
auto code = this->exitCode;
|
||||
delete this;
|
||||
|
||||
if (terminate) QCoreApplication::exit(code);
|
||||
// Even after all of that there's still multiple failing assertions and segfaults.
|
||||
// Pray you don't hit one.
|
||||
// Note: it appeats *some* of the crashes are related to values owned by the generation.
|
||||
// Test by commenting the connect() above.
|
||||
this->engine->deleteLater();
|
||||
this->engine = nullptr;
|
||||
});
|
||||
});
|
||||
|
||||
this->root->deleteLater();
|
||||
this->root = nullptr;
|
||||
} else {
|
||||
g_generations.remove(this->engine);
|
||||
|
||||
// the engine has never been used, no need to clean up
|
||||
delete this->engine;
|
||||
this->engine = nullptr;
|
||||
|
||||
auto terminate = this->shouldTerminate;
|
||||
auto code = this->exitCode;
|
||||
delete this;
|
||||
|
||||
if (terminate) QCoreApplication::exit(code);
|
||||
}
|
||||
}
|
||||
|
||||
void EngineGeneration::shutdown() {
|
||||
if (this->destroying) return;
|
||||
|
||||
delete this->root;
|
||||
this->root = nullptr;
|
||||
delete this->engine;
|
||||
this->engine = nullptr;
|
||||
delete this;
|
||||
}
|
||||
|
||||
void EngineGeneration::onReload(EngineGeneration* old) {
|
||||
if (old != nullptr) {
|
||||
// if the old generation holds the window incubation controller as the
|
||||
// new generation acquires it then incubators will hang intermittently
|
||||
qCDebug(logIncubator) << "Locking incubation controllers of old generation" << old;
|
||||
old->incubationControllersLocked = true;
|
||||
old->incubationControllers.clear();
|
||||
old->assignIncubationController();
|
||||
}
|
||||
|
||||
QObject::connect(this->engine, &QQmlEngine::quit, this, &EngineGeneration::quit);
|
||||
QObject::connect(this->engine, &QQmlEngine::exit, this, &EngineGeneration::exit);
|
||||
|
||||
if (auto* reloadable = qobject_cast<Reloadable*>(this->root)) {
|
||||
reloadable->reload(old ? old->root : nullptr);
|
||||
}
|
||||
auto* app = QCoreApplication::instance();
|
||||
QObject::connect(this->engine, &QQmlEngine::quit, app, &QCoreApplication::quit);
|
||||
QObject::connect(this->engine, &QQmlEngine::exit, app, &QCoreApplication::exit);
|
||||
|
||||
this->root->reload(old == nullptr ? nullptr : old->root);
|
||||
this->singletonRegistry.onReload(old == nullptr ? nullptr : &old->singletonRegistry);
|
||||
this->reloadComplete = true;
|
||||
emit this->reloadFinished();
|
||||
|
@ -145,7 +105,7 @@ void EngineGeneration::postReload() {
|
|||
// This can be called on a generation during its destruction.
|
||||
if (this->engine == nullptr || this->root == nullptr) return;
|
||||
|
||||
QsEnginePlugin::runOnReload();
|
||||
QuickshellPlugin::runOnReload();
|
||||
PostReloadHook::postReloadTree(this->root);
|
||||
this->singletonRegistry.onPostReload();
|
||||
}
|
||||
|
@ -157,21 +117,13 @@ void EngineGeneration::setWatchingFiles(bool watching) {
|
|||
|
||||
for (auto& file: this->scanner.scannedFiles) {
|
||||
this->watcher->addPath(file);
|
||||
this->watcher->addPath(QFileInfo(file).dir().absolutePath());
|
||||
}
|
||||
|
||||
QObject::connect(
|
||||
this->watcher,
|
||||
&QFileSystemWatcher::fileChanged,
|
||||
this,
|
||||
&EngineGeneration::onFileChanged
|
||||
);
|
||||
|
||||
QObject::connect(
|
||||
this->watcher,
|
||||
&QFileSystemWatcher::directoryChanged,
|
||||
this,
|
||||
&EngineGeneration::onDirectoryChanged
|
||||
&EngineGeneration::filesChanged
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
@ -182,44 +134,28 @@ void EngineGeneration::setWatchingFiles(bool watching) {
|
|||
}
|
||||
}
|
||||
|
||||
void EngineGeneration::onFileChanged(const QString& name) {
|
||||
if (!this->watcher->files().contains(name)) {
|
||||
this->deletedWatchedFiles.push_back(name);
|
||||
} else {
|
||||
emit this->filesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void EngineGeneration::onDirectoryChanged() {
|
||||
// try to find any files that were just deleted from a replace operation
|
||||
for (auto& file: this->deletedWatchedFiles) {
|
||||
if (QFileInfo(file).exists()) {
|
||||
emit this->filesChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EngineGeneration::registerIncubationController(QQmlIncubationController* controller) {
|
||||
auto* obj = dynamic_cast<QObject*>(controller);
|
||||
|
||||
// We only want controllers that we can swap out if destroyed.
|
||||
// This happens if the window owning the active controller dies.
|
||||
if (auto* obj = dynamic_cast<QObject*>(controller)) {
|
||||
QObject::connect(
|
||||
obj,
|
||||
&QObject::destroyed,
|
||||
this,
|
||||
&EngineGeneration::incubationControllerDestroyed
|
||||
);
|
||||
} else {
|
||||
qCWarning(logIncubator) << "Could not register incubation controller as it is not a QObject"
|
||||
<< controller;
|
||||
if (obj == nullptr) {
|
||||
qCDebug(logIncubator) << "Could not register incubation controller as it is not a QObject"
|
||||
<< controller;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this->incubationControllers.push_back(controller);
|
||||
qCDebug(logIncubator) << "Registered incubation controller" << controller << "to generation"
|
||||
<< this;
|
||||
this->incubationControllers.push_back({controller, obj});
|
||||
|
||||
QObject::connect(
|
||||
obj,
|
||||
&QObject::destroyed,
|
||||
this,
|
||||
&EngineGeneration::incubationControllerDestroyed
|
||||
);
|
||||
|
||||
qCDebug(logIncubator) << "Registered incubation controller" << controller;
|
||||
|
||||
// This function can run during destruction.
|
||||
if (this->engine == nullptr) return;
|
||||
|
@ -230,20 +166,22 @@ void EngineGeneration::registerIncubationController(QQmlIncubationController* co
|
|||
}
|
||||
|
||||
void EngineGeneration::deregisterIncubationController(QQmlIncubationController* controller) {
|
||||
if (auto* obj = dynamic_cast<QObject*>(controller)) {
|
||||
QObject::disconnect(obj, nullptr, this, nullptr);
|
||||
} else {
|
||||
qCCritical(logIncubator) << "Deregistering incubation controller which is not a QObject, "
|
||||
"however only QObject controllers should be registered.";
|
||||
}
|
||||
QObject* obj = nullptr;
|
||||
this->incubationControllers.removeIf([&](QPair<QQmlIncubationController*, QObject*> other) {
|
||||
if (controller == other.first) {
|
||||
obj = other.second;
|
||||
return true;
|
||||
} else return false;
|
||||
});
|
||||
|
||||
if (!this->incubationControllers.removeOne(controller)) {
|
||||
qCCritical(logIncubator) << "Failed to deregister incubation controller" << controller << "from"
|
||||
<< this << "as it was not registered to begin with";
|
||||
qCCritical(logIncubator) << "Current registered incuabation controllers"
|
||||
<< this->incubationControllers;
|
||||
if (obj == nullptr) {
|
||||
qCWarning(logIncubator) << "Failed to deregister incubation controller" << controller
|
||||
<< "as it was not registered to begin with";
|
||||
qCWarning(logIncubator) << "Current registered incuabation controllers"
|
||||
<< this->incubationControllers;
|
||||
} else {
|
||||
qCDebug(logIncubator) << "Deregistered incubation controller" << controller << "from" << this;
|
||||
QObject::disconnect(obj, nullptr, this, nullptr);
|
||||
qCDebug(logIncubator) << "Deregistered incubation controller" << controller;
|
||||
}
|
||||
|
||||
// This function can run during destruction.
|
||||
|
@ -258,25 +196,22 @@ void EngineGeneration::deregisterIncubationController(QQmlIncubationController*
|
|||
|
||||
void EngineGeneration::incubationControllerDestroyed() {
|
||||
auto* sender = this->sender();
|
||||
auto* controller = dynamic_cast<QQmlIncubationController*>(sender);
|
||||
QQmlIncubationController* controller = nullptr;
|
||||
|
||||
this->incubationControllers.removeIf([&](QPair<QQmlIncubationController*, QObject*> other) {
|
||||
if (sender == other.second) {
|
||||
controller = other.first;
|
||||
return true;
|
||||
} else return false;
|
||||
});
|
||||
|
||||
if (controller == nullptr) {
|
||||
qCCritical(logIncubator) << "Destroyed incubation controller" << sender << "is not known to"
|
||||
<< this << ", this may cause memory corruption";
|
||||
qCCritical(logIncubator) << "Destroyed incubation controller" << this->sender()
|
||||
<< "could not be identified, this may cause memory corruption";
|
||||
qCCritical(logIncubator) << "Current registered incuabation controllers"
|
||||
<< this->incubationControllers;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->incubationControllers.removeOne(controller)) {
|
||||
qCDebug(logIncubator) << "Destroyed incubation controller" << controller << "deregistered from"
|
||||
<< this;
|
||||
} else {
|
||||
qCCritical(logIncubator) << "Destroyed incubation controller" << controller
|
||||
<< "was not registered, but its destruction was observed by" << this;
|
||||
|
||||
return;
|
||||
qCDebug(logIncubator) << "Destroyed incubation controller" << controller << "deregistered";
|
||||
}
|
||||
|
||||
// This function can run during destruction.
|
||||
|
@ -289,64 +224,23 @@ void EngineGeneration::incubationControllerDestroyed() {
|
|||
}
|
||||
}
|
||||
|
||||
void EngineGeneration::registerExtension(const void* key, EngineGenerationExt* extension) {
|
||||
if (this->extensions.contains(key)) {
|
||||
delete this->extensions.value(key);
|
||||
}
|
||||
|
||||
this->extensions.insert(key, extension);
|
||||
}
|
||||
|
||||
EngineGenerationExt* EngineGeneration::findExtension(const void* key) {
|
||||
return this->extensions.value(key);
|
||||
}
|
||||
|
||||
void EngineGeneration::quit() {
|
||||
this->shouldTerminate = true;
|
||||
this->destroy();
|
||||
}
|
||||
|
||||
void EngineGeneration::exit(int code) {
|
||||
this->shouldTerminate = true;
|
||||
this->exitCode = code;
|
||||
this->destroy();
|
||||
}
|
||||
|
||||
void EngineGeneration::assignIncubationController() {
|
||||
QQmlIncubationController* controller = nullptr;
|
||||
if (this->incubationControllers.isEmpty()) controller = &this->delayedIncubationController;
|
||||
else controller = this->incubationControllers.first().first;
|
||||
|
||||
if (this->incubationControllersLocked || this->incubationControllers.isEmpty()) {
|
||||
controller = &this->delayedIncubationController;
|
||||
} else {
|
||||
controller = this->incubationControllers.first();
|
||||
}
|
||||
|
||||
qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation"
|
||||
<< this
|
||||
qCDebug(logIncubator) << "Assigning incubation controller to engine:" << controller
|
||||
<< "fallback:" << (controller == &this->delayedIncubationController);
|
||||
|
||||
this->engine->setIncubationController(controller);
|
||||
}
|
||||
|
||||
EngineGeneration* EngineGeneration::currentGeneration() {
|
||||
if (g_generations.size() == 1) {
|
||||
return *g_generations.begin();
|
||||
} else return nullptr;
|
||||
}
|
||||
|
||||
EngineGeneration* EngineGeneration::findEngineGeneration(const QQmlEngine* engine) {
|
||||
return g_generations.value(engine);
|
||||
}
|
||||
|
||||
EngineGeneration* EngineGeneration::findObjectGeneration(const QObject* object) {
|
||||
// Objects can still attempt to find their generation after it has been destroyed.
|
||||
// if (g_generations.size() == 1) return EngineGeneration::currentGeneration();
|
||||
|
||||
EngineGeneration* EngineGeneration::findObjectGeneration(QObject* object) {
|
||||
while (object != nullptr) {
|
||||
auto* context = QQmlEngine::contextForObject(object);
|
||||
|
||||
if (context != nullptr) {
|
||||
if (auto* generation = EngineGeneration::findEngineGeneration(context->engine())) {
|
||||
if (auto* generation = g_generations.value(context->engine())) {
|
||||
return generation;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,34 +1,25 @@
|
|||
#pragma once
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdir.h>
|
||||
#include <qfilesystemwatcher.h>
|
||||
#include <qhash.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qpair.h>
|
||||
#include <qqmlincubator.h>
|
||||
#include <qtclasshelpermacros.h>
|
||||
|
||||
#include "incubator.hpp"
|
||||
#include "qsintercept.hpp"
|
||||
#include "scan.hpp"
|
||||
#include "shell.hpp"
|
||||
#include "singleton.hpp"
|
||||
|
||||
class RootWrapper;
|
||||
class QuickshellGlobal;
|
||||
|
||||
class EngineGenerationExt {
|
||||
public:
|
||||
EngineGenerationExt() = default;
|
||||
virtual ~EngineGenerationExt() = default;
|
||||
Q_DISABLE_COPY_MOVE(EngineGenerationExt);
|
||||
};
|
||||
|
||||
class EngineGeneration: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
explicit EngineGeneration(const QDir& rootPath, QmlScanner scanner);
|
||||
explicit EngineGeneration(QmlScanner scanner);
|
||||
~EngineGeneration() override;
|
||||
Q_DISABLE_COPY_MOVE(EngineGeneration);
|
||||
|
||||
|
@ -39,55 +30,30 @@ public:
|
|||
void registerIncubationController(QQmlIncubationController* controller);
|
||||
void deregisterIncubationController(QQmlIncubationController* controller);
|
||||
|
||||
// takes ownership
|
||||
void registerExtension(const void* key, EngineGenerationExt* extension);
|
||||
EngineGenerationExt* findExtension(const void* key);
|
||||
|
||||
static EngineGeneration* findEngineGeneration(const QQmlEngine* engine);
|
||||
static EngineGeneration* findObjectGeneration(const QObject* object);
|
||||
|
||||
// Returns the current generation if there is only one generation,
|
||||
// otherwise null.
|
||||
static EngineGeneration* currentGeneration();
|
||||
static EngineGeneration* findObjectGeneration(QObject* object);
|
||||
|
||||
RootWrapper* wrapper = nullptr;
|
||||
QDir rootPath;
|
||||
QmlScanner scanner;
|
||||
QsUrlInterceptor urlInterceptor;
|
||||
QsInterceptNetworkAccessManagerFactory interceptNetFactory;
|
||||
QQmlEngine* engine = nullptr;
|
||||
QObject* root = nullptr;
|
||||
ShellRoot* root = nullptr;
|
||||
SingletonRegistry singletonRegistry;
|
||||
QFileSystemWatcher* watcher = nullptr;
|
||||
QVector<QString> deletedWatchedFiles;
|
||||
DelayedQmlIncubationController delayedIncubationController;
|
||||
bool reloadComplete = false;
|
||||
QuickshellGlobal* qsgInstance = nullptr;
|
||||
|
||||
void destroy();
|
||||
void shutdown();
|
||||
|
||||
signals:
|
||||
void filesChanged();
|
||||
void reloadFinished();
|
||||
|
||||
public slots:
|
||||
void quit();
|
||||
void exit(int code);
|
||||
|
||||
private slots:
|
||||
void onFileChanged(const QString& name);
|
||||
void onDirectoryChanged();
|
||||
void incubationControllerDestroyed();
|
||||
|
||||
private:
|
||||
void postReload();
|
||||
void assignIncubationController();
|
||||
QVector<QQmlIncubationController*> incubationControllers;
|
||||
bool incubationControllersLocked = false;
|
||||
QHash<const void*, EngineGenerationExt*> extensions;
|
||||
|
||||
bool destroying = false;
|
||||
bool shouldTerminate = false;
|
||||
int exitCode = 0;
|
||||
QVector<QPair<QQmlIncubationController*, QObject*>> incubationControllers;
|
||||
};
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#include "iconimageprovider.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
#include <qcolor.h>
|
||||
#include <qicon.h>
|
||||
|
@ -12,9 +11,7 @@
|
|||
QPixmap
|
||||
IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) {
|
||||
QString iconName;
|
||||
QString fallbackName;
|
||||
QString path;
|
||||
|
||||
auto splitIdx = id.indexOf("?path=");
|
||||
if (splitIdx != -1) {
|
||||
iconName = id.sliced(0, splitIdx);
|
||||
|
@ -22,17 +19,10 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re
|
|||
qWarning() << "Searching custom icon paths is not yet supported. Icon path will be ignored for"
|
||||
<< id;
|
||||
} else {
|
||||
splitIdx = id.indexOf("?fallback=");
|
||||
if (splitIdx != -1) {
|
||||
iconName = id.sliced(0, splitIdx);
|
||||
fallbackName = id.sliced(splitIdx + 10);
|
||||
} else {
|
||||
iconName = id;
|
||||
}
|
||||
iconName = id;
|
||||
}
|
||||
|
||||
auto icon = QIcon::fromTheme(iconName);
|
||||
if (icon.isNull()) icon = QIcon::fromTheme(fallbackName);
|
||||
|
||||
auto targetSize = requestedSize.isValid() ? requestedSize : QSize(100, 100);
|
||||
if (targetSize.width() == 0 || targetSize.height() == 0) targetSize = QSize(2, 2);
|
||||
|
@ -50,8 +40,8 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re
|
|||
QPixmap IconImageProvider::missingPixmap(const QSize& size) {
|
||||
auto width = size.width() % 2 == 0 ? size.width() : size.width() + 1;
|
||||
auto height = size.height() % 2 == 0 ? size.height() : size.height() + 1;
|
||||
width = std::max(width, 2);
|
||||
height = std::max(height, 2);
|
||||
if (width < 2) width = 2;
|
||||
if (height < 2) height = 2;
|
||||
|
||||
auto pixmap = QPixmap(width, height);
|
||||
pixmap.fill(QColorConstants::Black);
|
||||
|
@ -65,20 +55,12 @@ QPixmap IconImageProvider::missingPixmap(const QSize& size) {
|
|||
return pixmap;
|
||||
}
|
||||
|
||||
QString IconImageProvider::requestString(
|
||||
const QString& icon,
|
||||
const QString& path,
|
||||
const QString& fallback
|
||||
) {
|
||||
QString IconImageProvider::requestString(const QString& icon, const QString& path) {
|
||||
auto req = "image://icon/" + icon;
|
||||
|
||||
if (!path.isEmpty()) {
|
||||
req += "?path=" + path;
|
||||
}
|
||||
|
||||
if (!fallback.isEmpty()) {
|
||||
req += "?fallback=" + fallback;
|
||||
}
|
||||
|
||||
return req;
|
||||
}
|
||||
|
|
|
@ -10,10 +10,5 @@ public:
|
|||
QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override;
|
||||
|
||||
static QPixmap missingPixmap(const QSize& size);
|
||||
|
||||
static QString requestString(
|
||||
const QString& icon,
|
||||
const QString& path = QString(),
|
||||
const QString& fallback = QString()
|
||||
);
|
||||
static QString requestString(const QString& icon, const QString& path);
|
||||
};
|
||||
|
|
|
@ -1,105 +0,0 @@
|
|||
#include "iconprovider.hpp"
|
||||
#include <utility>
|
||||
|
||||
#include <qicon.h>
|
||||
#include <qiconengine.h>
|
||||
#include <qlogging.h>
|
||||
#include <qobject.h>
|
||||
#include <qpixmap.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qquickimageprovider.h>
|
||||
#include <qrect.h>
|
||||
#include <qsize.h>
|
||||
#include <qstring.h>
|
||||
|
||||
#include "generation.hpp"
|
||||
|
||||
// QMenu re-calls pixmap() every time the mouse moves so its important to cache it.
|
||||
class PixmapCacheIconEngine: public QIconEngine {
|
||||
void paint(
|
||||
QPainter* /*unused*/,
|
||||
const QRect& /*unused*/,
|
||||
QIcon::Mode /*unused*/,
|
||||
QIcon::State /*unused*/
|
||||
) override {
|
||||
qFatal(
|
||||
) << "Unexpected icon paint request bypassed pixmap method. Please report this as a bug.";
|
||||
}
|
||||
|
||||
QPixmap pixmap(const QSize& size, QIcon::Mode /*unused*/, QIcon::State /*unused*/) override {
|
||||
if (this->lastPixmap.isNull() || size != this->lastSize) {
|
||||
this->lastPixmap = this->createPixmap(size);
|
||||
this->lastSize = size;
|
||||
}
|
||||
|
||||
return this->lastPixmap;
|
||||
}
|
||||
|
||||
virtual QPixmap createPixmap(const QSize& size) = 0;
|
||||
|
||||
private:
|
||||
QSize lastSize;
|
||||
QPixmap lastPixmap;
|
||||
};
|
||||
|
||||
class ImageProviderIconEngine: public PixmapCacheIconEngine {
|
||||
public:
|
||||
explicit ImageProviderIconEngine(QQuickImageProvider* provider, QString id)
|
||||
: provider(provider)
|
||||
, id(std::move(id)) {}
|
||||
|
||||
QPixmap createPixmap(const QSize& size) override {
|
||||
if (this->provider->imageType() == QQmlImageProviderBase::Pixmap) {
|
||||
return this->provider->requestPixmap(this->id, nullptr, size);
|
||||
} else if (this->provider->imageType() == QQmlImageProviderBase::Image) {
|
||||
auto image = this->provider->requestImage(this->id, nullptr, size);
|
||||
return QPixmap::fromImage(image);
|
||||
} else {
|
||||
qFatal() << "Unexpected ImageProviderIconEngine image type" << this->provider->imageType();
|
||||
return QPixmap(); // never reached, satisfies lint
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] QIconEngine* clone() const override {
|
||||
return new ImageProviderIconEngine(this->provider, this->id);
|
||||
}
|
||||
|
||||
private:
|
||||
QQuickImageProvider* provider;
|
||||
QString id;
|
||||
};
|
||||
|
||||
QIcon getEngineImageAsIcon(QQmlEngine* engine, const QUrl& url) {
|
||||
if (!engine || url.isEmpty()) return QIcon();
|
||||
|
||||
auto scheme = url.scheme();
|
||||
if (scheme == "image") {
|
||||
auto providerName = url.authority();
|
||||
auto path = url.path();
|
||||
if (!path.isEmpty()) path = path.sliced(1);
|
||||
|
||||
auto* provider = qobject_cast<QQuickImageProvider*>(engine->imageProvider(providerName));
|
||||
|
||||
if (provider == nullptr) {
|
||||
qWarning() << "iconByUrl failed: no provider found for" << url;
|
||||
return QIcon();
|
||||
}
|
||||
|
||||
if (provider->imageType() == QQmlImageProviderBase::Pixmap
|
||||
|| provider->imageType() == QQmlImageProviderBase::Image)
|
||||
{
|
||||
return QIcon(new ImageProviderIconEngine(provider, path));
|
||||
}
|
||||
|
||||
} else {
|
||||
qWarning() << "iconByUrl failed: unsupported scheme" << scheme << "in path" << url;
|
||||
}
|
||||
|
||||
return QIcon();
|
||||
}
|
||||
|
||||
QIcon getCurrentEngineImageAsIcon(const QUrl& url) {
|
||||
auto* generation = EngineGeneration::currentGeneration();
|
||||
if (!generation) return QIcon();
|
||||
return getEngineImageAsIcon(generation->engine, url);
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <qicon.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qurl.h>
|
||||
|
||||
QIcon getEngineImageAsIcon(QQmlEngine* engine, const QUrl& url);
|
||||
QIcon getCurrentEngineImageAsIcon(const QUrl& url);
|
|
@ -1,6 +1,5 @@
|
|||
#include "imageprovider.hpp"
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdebug.h>
|
||||
#include <qimage.h>
|
||||
#include <qlogging.h>
|
||||
|
@ -8,30 +7,17 @@
|
|||
#include <qobject.h>
|
||||
#include <qpixmap.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
namespace {
|
||||
static QMap<QString, QsImageHandle*> liveImages; // NOLINT
|
||||
|
||||
namespace {
|
||||
QMap<QString, QsImageHandle*> liveImages; // NOLINT
|
||||
quint32 handleIndex = 0; // NOLINT
|
||||
} // namespace
|
||||
|
||||
void parseReq(const QString& req, QString& target, QString& param) {
|
||||
auto splitIdx = req.indexOf('/');
|
||||
if (splitIdx != -1) {
|
||||
target = req.sliced(0, splitIdx);
|
||||
param = req.sliced(splitIdx + 1);
|
||||
} else {
|
||||
target = req;
|
||||
QsImageHandle::QsImageHandle(QQmlImageProviderBase::ImageType type, QObject* parent)
|
||||
: QObject(parent)
|
||||
, type(type) {
|
||||
{
|
||||
auto dbg = QDebug(&this->id);
|
||||
dbg.nospace() << static_cast<void*>(this);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QsImageHandle::QsImageHandle(QQmlImageProviderBase::ImageType type)
|
||||
: type(type)
|
||||
, id(QString::number(++handleIndex)) {
|
||||
liveImages.insert(this->id, this);
|
||||
}
|
||||
|
||||
|
@ -57,6 +43,16 @@ QPixmap QsImageHandle::
|
|||
return QPixmap();
|
||||
}
|
||||
|
||||
void parseReq(const QString& req, QString& target, QString& param) {
|
||||
auto splitIdx = req.indexOf('/');
|
||||
if (splitIdx != -1) {
|
||||
target = req.sliced(0, splitIdx);
|
||||
param = req.sliced(splitIdx + 1);
|
||||
} else {
|
||||
target = req;
|
||||
}
|
||||
}
|
||||
|
||||
QImage QsImageProvider::requestImage(const QString& id, QSize* size, const QSize& requestedSize) {
|
||||
QString target;
|
||||
QString param;
|
||||
|
@ -85,9 +81,3 @@ QsPixmapProvider::requestPixmap(const QString& id, QSize* size, const QSize& req
|
|||
return QPixmap();
|
||||
}
|
||||
}
|
||||
|
||||
QString QsIndexedImageHandle::url() const {
|
||||
return this->QsImageHandle::url() % '/' % QString::number(this->changeIndex);
|
||||
}
|
||||
|
||||
void QsIndexedImageHandle::imageChanged() { ++this->changeIndex; }
|
||||
|
|
|
@ -20,13 +20,15 @@ public:
|
|||
QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override;
|
||||
};
|
||||
|
||||
class QsImageHandle {
|
||||
class QsImageHandle: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
explicit QsImageHandle(QQmlImageProviderBase::ImageType type);
|
||||
virtual ~QsImageHandle();
|
||||
explicit QsImageHandle(QQmlImageProviderBase::ImageType type, QObject* parent = nullptr);
|
||||
~QsImageHandle() override;
|
||||
Q_DISABLE_COPY_MOVE(QsImageHandle);
|
||||
|
||||
[[nodiscard]] virtual QString url() const;
|
||||
[[nodiscard]] QString url() const;
|
||||
|
||||
virtual QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize);
|
||||
virtual QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize);
|
||||
|
@ -35,14 +37,3 @@ private:
|
|||
QQmlImageProviderBase::ImageType type;
|
||||
QString id;
|
||||
};
|
||||
|
||||
class QsIndexedImageHandle: public QsImageHandle {
|
||||
public:
|
||||
explicit QsIndexedImageHandle(QQmlImageProviderBase::ImageType type): QsImageHandle(type) {}
|
||||
|
||||
[[nodiscard]] QString url() const override;
|
||||
void imageChanged();
|
||||
|
||||
private:
|
||||
quint32 changeIndex = 0;
|
||||
};
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
#include "instanceinfo.hpp"
|
||||
|
||||
#include <qdatastream.h>
|
||||
|
||||
QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) {
|
||||
stream << info.instanceId << info.configPath << info.shellId << info.launchTime;
|
||||
return stream;
|
||||
}
|
||||
|
||||
QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) {
|
||||
stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime;
|
||||
return stream;
|
||||
}
|
||||
|
||||
QDataStream& operator<<(QDataStream& stream, const RelaunchInfo& info) {
|
||||
stream << info.instance << info.noColor << info.timestamp << info.sparseLogsOnly
|
||||
<< info.defaultLogLevel << info.logRules;
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
QDataStream& operator>>(QDataStream& stream, RelaunchInfo& info) {
|
||||
stream >> info.instance >> info.noColor >> info.timestamp >> info.sparseLogsOnly
|
||||
>> info.defaultLogLevel >> info.logRules;
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
InstanceInfo InstanceInfo::CURRENT = {}; // NOLINT
|
||||
|
||||
namespace qs::crash {
|
||||
|
||||
CrashInfo CrashInfo::INSTANCE = {}; // NOLINT
|
||||
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <qdatetime.h>
|
||||
#include <qlogging.h>
|
||||
#include <qstring.h>
|
||||
|
||||
struct InstanceInfo {
|
||||
QString instanceId;
|
||||
QString configPath;
|
||||
QString shellId;
|
||||
QDateTime launchTime;
|
||||
|
||||
static InstanceInfo CURRENT; // NOLINT
|
||||
};
|
||||
|
||||
struct RelaunchInfo {
|
||||
InstanceInfo instance;
|
||||
bool noColor = false;
|
||||
bool timestamp = false;
|
||||
bool sparseLogsOnly = false;
|
||||
QtMsgType defaultLogLevel = QtWarningMsg;
|
||||
QString logRules;
|
||||
};
|
||||
|
||||
QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info);
|
||||
QDataStream& operator>>(QDataStream& stream, InstanceInfo& info);
|
||||
|
||||
QDataStream& operator<<(QDataStream& stream, const RelaunchInfo& info);
|
||||
QDataStream& operator>>(QDataStream& stream, RelaunchInfo& info);
|
||||
|
||||
namespace qs::crash {
|
||||
|
||||
struct CrashInfo {
|
||||
int logFd = -1;
|
||||
|
||||
static CrashInfo INSTANCE; // NOLINT
|
||||
};
|
||||
|
||||
} // namespace qs::crash
|
|
@ -179,9 +179,7 @@ void LazyLoader::incubateIfReady(bool overrideReloadCheck) {
|
|||
|
||||
void LazyLoader::onIncubationCompleted() {
|
||||
this->setItem(this->incubator->object());
|
||||
// The incubator is not necessarily inert at the time of this callback,
|
||||
// so deleteLater is required.
|
||||
this->incubator->deleteLater();
|
||||
delete this->incubator;
|
||||
this->incubator = nullptr;
|
||||
this->targetLoading = false;
|
||||
emit this->loadingChanged();
|
||||
|
|
|
@ -79,7 +79,7 @@
|
|||
/// > [!WARNING] Components that internally load other components must explicitly
|
||||
/// > support asynchronous loading to avoid blocking.
|
||||
/// >
|
||||
/// > Notably, @@Variants does not corrently support asynchronous
|
||||
/// > Notably, [Variants](../variants) does not corrently support asynchronous
|
||||
/// > loading, meaning using it inside a LazyLoader will block similarly to not
|
||||
/// > having a loader to start with.
|
||||
///
|
||||
|
@ -87,8 +87,8 @@
|
|||
/// > meaning if you create all windows inside of lazy loaders, none of them will ever load.
|
||||
class LazyLoader: public Reloadable {
|
||||
Q_OBJECT;
|
||||
/// The fully loaded item if the loader is @@loading or @@active, or `null`
|
||||
/// if neither @@loading nor @@active.
|
||||
/// The fully loaded item if the loader is `loading` or `active`, or `null`
|
||||
/// if neither `loading` or `active`.
|
||||
///
|
||||
/// Note that the item is owned by the LazyLoader, and destroying the LazyLoader
|
||||
/// will destroy the item.
|
||||
|
@ -96,7 +96,7 @@ class LazyLoader: public Reloadable {
|
|||
/// > [!WARNING] If you access the `item` of a loader that is currently loading,
|
||||
/// > it will block as if you had set `active` to true immediately beforehand.
|
||||
/// >
|
||||
/// > You can instead set @@loading and listen to @@activeChanged(s) signal to
|
||||
/// > You can instead set `loading` and listen to the `activeChanged` signal to
|
||||
/// > ensure loading happens asynchronously.
|
||||
Q_PROPERTY(QObject* item READ item NOTIFY itemChanged);
|
||||
/// If the loader is actively loading.
|
||||
|
@ -105,7 +105,7 @@ class LazyLoader: public Reloadable {
|
|||
/// loading it asynchronously. If the component is already loaded, setting
|
||||
/// this property has no effect.
|
||||
///
|
||||
/// See also: @@activeAsync.
|
||||
/// See also: [activeAsync](#prop.activeAsync).
|
||||
Q_PROPERTY(bool loading READ isLoading WRITE setLoading NOTIFY loadingChanged);
|
||||
/// If the component is fully loaded.
|
||||
///
|
||||
|
@ -113,17 +113,17 @@ class LazyLoader: public Reloadable {
|
|||
/// blocking the UI, and setting it to `false` will destroy the component, requiring
|
||||
/// it to be loaded again.
|
||||
///
|
||||
/// See also: @@activeAsync.
|
||||
/// See also: [activeAsync](#prop.activeAsync).
|
||||
Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged);
|
||||
/// If the component is fully loaded.
|
||||
///
|
||||
/// Setting this property to true will asynchronously load the component similarly to
|
||||
/// @@loading. Reading it or setting it to false will behanve
|
||||
/// the same as @@active.
|
||||
/// [loading](#prop.loading). Reading it or setting it to false will behanve
|
||||
/// the same as [active](#prop.active).
|
||||
Q_PROPERTY(bool activeAsync READ isActive WRITE setActiveAsync NOTIFY activeChanged);
|
||||
/// The component to load. Mutually exclusive to @@source.
|
||||
/// The component to load. Mutually exclusive to `source`.
|
||||
Q_PROPERTY(QQmlComponent* component READ component WRITE setComponent NOTIFY componentChanged);
|
||||
/// The URI to load the component from. Mutually exclusive to @@component.
|
||||
/// The URI to load the component from. Mutually exclusive to `component`.
|
||||
Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged);
|
||||
Q_CLASSINFO("DefaultProperty", "component");
|
||||
QML_ELEMENT;
|
||||
|
|
|
@ -1,937 +0,0 @@
|
|||
#include "logging.hpp"
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <qbytearrayview.h>
|
||||
#include <qcoreapplication.h>
|
||||
#include <qdatetime.h>
|
||||
#include <qendian.h>
|
||||
#include <qfilesystemwatcher.h>
|
||||
#include <qhash.h>
|
||||
#include <qhashfunctions.h>
|
||||
#include <qlist.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qnamespace.h>
|
||||
#include <qobject.h>
|
||||
#include <qobjectdefs.h>
|
||||
#include <qpair.h>
|
||||
#include <qstring.h>
|
||||
#include <qstringview.h>
|
||||
#include <qsysinfo.h>
|
||||
#include <qtenvironmentvariables.h>
|
||||
#include <qtextstream.h>
|
||||
#include <qthread.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/sendfile.h>
|
||||
|
||||
#include "instanceinfo.hpp"
|
||||
#include "logging_p.hpp"
|
||||
#include "logging_qtprivate.cpp" // NOLINT
|
||||
#include "paths.hpp"
|
||||
#include "ringbuf.hpp"
|
||||
|
||||
Q_LOGGING_CATEGORY(logBare, "quickshell.bare");
|
||||
|
||||
namespace qs::log {
|
||||
using namespace qt_logging_registry;
|
||||
|
||||
Q_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg);
|
||||
|
||||
bool LogMessage::operator==(const LogMessage& other) const {
|
||||
// note: not including time
|
||||
return this->type == other.type && this->category == other.category && this->body == other.body;
|
||||
}
|
||||
|
||||
size_t qHash(const LogMessage& message) {
|
||||
return qHash(message.type) ^ qHash(message.category) ^ qHash(message.body);
|
||||
}
|
||||
|
||||
void LogMessage::formatMessage(
|
||||
QTextStream& stream,
|
||||
const LogMessage& msg,
|
||||
bool color,
|
||||
bool timestamp,
|
||||
const QString& prefix
|
||||
) {
|
||||
if (!prefix.isEmpty()) {
|
||||
if (color) stream << "\033[90m";
|
||||
stream << '[' << prefix << ']';
|
||||
if (timestamp) stream << ' ';
|
||||
if (color) stream << "\033[0m";
|
||||
}
|
||||
|
||||
if (timestamp) {
|
||||
if (color) stream << "\033[90m";
|
||||
stream << msg.time.toString("yyyy-MM-dd hh:mm:ss.zzz");
|
||||
}
|
||||
|
||||
if (msg.category == "quickshell.bare") {
|
||||
if (!prefix.isEmpty()) stream << ' ';
|
||||
stream << msg.body;
|
||||
} else {
|
||||
if (color) {
|
||||
switch (msg.type) {
|
||||
case QtDebugMsg: stream << "\033[34m DEBUG"; break;
|
||||
case QtInfoMsg: stream << "\033[32m INFO"; break;
|
||||
case QtWarningMsg: stream << "\033[33m WARN"; break;
|
||||
case QtCriticalMsg: stream << "\033[31m ERROR"; break;
|
||||
case QtFatalMsg: stream << "\033[31m FATAL"; break;
|
||||
}
|
||||
} else {
|
||||
switch (msg.type) {
|
||||
case QtDebugMsg: stream << " DEBUG"; break;
|
||||
case QtInfoMsg: stream << " INFO"; break;
|
||||
case QtWarningMsg: stream << " WARN"; break;
|
||||
case QtCriticalMsg: stream << " ERROR"; break;
|
||||
case QtFatalMsg: stream << " FATAL"; break;
|
||||
}
|
||||
}
|
||||
|
||||
const auto isDefault = msg.category == "default";
|
||||
|
||||
if (color && !isDefault && msg.type != QtFatalMsg) stream << "\033[97m";
|
||||
|
||||
if (!isDefault) {
|
||||
stream << ' ' << msg.category;
|
||||
}
|
||||
|
||||
if (color && msg.type != QtFatalMsg) stream << "\033[0m";
|
||||
|
||||
stream << ": " << msg.body;
|
||||
|
||||
if (color && msg.type == QtFatalMsg) stream << "\033[0m";
|
||||
}
|
||||
}
|
||||
|
||||
bool CategoryFilter::shouldDisplay(QtMsgType type) const {
|
||||
switch (type) {
|
||||
case QtDebugMsg: return this->debug;
|
||||
case QtInfoMsg: return this->info;
|
||||
case QtWarningMsg: return this->warn;
|
||||
case QtCriticalMsg: return this->critical;
|
||||
default: return true;
|
||||
}
|
||||
}
|
||||
|
||||
void CategoryFilter::apply(QLoggingCategory* category) const {
|
||||
category->setEnabled(QtDebugMsg, this->debug);
|
||||
category->setEnabled(QtInfoMsg, this->info);
|
||||
category->setEnabled(QtWarningMsg, this->warn);
|
||||
category->setEnabled(QtCriticalMsg, this->critical);
|
||||
}
|
||||
|
||||
void CategoryFilter::applyRule(
|
||||
QLatin1StringView category,
|
||||
const qt_logging_registry::QLoggingRule& rule
|
||||
) {
|
||||
auto filterpass = rule.pass(category, QtDebugMsg);
|
||||
if (filterpass != 0) this->debug = filterpass > 0;
|
||||
|
||||
filterpass = rule.pass(category, QtInfoMsg);
|
||||
if (filterpass != 0) this->info = filterpass > 0;
|
||||
|
||||
filterpass = rule.pass(category, QtWarningMsg);
|
||||
if (filterpass != 0) this->warn = filterpass > 0;
|
||||
|
||||
filterpass = rule.pass(category, QtCriticalMsg);
|
||||
if (filterpass != 0) this->critical = filterpass > 0;
|
||||
}
|
||||
|
||||
LogManager::LogManager(): stdoutStream(stdout) {}
|
||||
|
||||
void LogManager::messageHandler(
|
||||
QtMsgType type,
|
||||
const QMessageLogContext& context,
|
||||
const QString& msg
|
||||
) {
|
||||
auto message = LogMessage(type, QLatin1StringView(context.category), msg.toUtf8());
|
||||
|
||||
auto* self = LogManager::instance();
|
||||
|
||||
auto display = true;
|
||||
|
||||
const auto* key = static_cast<const void*>(context.category);
|
||||
|
||||
if (self->sparseFilters.contains(key)) {
|
||||
display = self->sparseFilters.value(key).shouldDisplay(type);
|
||||
}
|
||||
|
||||
if (display) {
|
||||
LogMessage::formatMessage(
|
||||
self->stdoutStream,
|
||||
message,
|
||||
self->colorLogs,
|
||||
self->timestampLogs,
|
||||
self->prefix
|
||||
);
|
||||
|
||||
self->stdoutStream << Qt::endl;
|
||||
}
|
||||
|
||||
emit self->logMessage(message, display);
|
||||
}
|
||||
|
||||
void LogManager::filterCategory(QLoggingCategory* category) {
|
||||
auto* instance = LogManager::instance();
|
||||
|
||||
auto categoryName = QLatin1StringView(category->categoryName());
|
||||
auto isQs = categoryName.startsWith(QLatin1StringView("quickshell."));
|
||||
|
||||
if (instance->lastCategoryFilter) {
|
||||
instance->lastCategoryFilter(category);
|
||||
}
|
||||
|
||||
auto filter = CategoryFilter(category);
|
||||
|
||||
if (isQs) {
|
||||
filter.debug = filter.debug || instance->mDefaultLevel == QtDebugMsg;
|
||||
filter.info = filter.debug || instance->mDefaultLevel == QtInfoMsg;
|
||||
filter.warn = filter.info || instance->mDefaultLevel == QtWarningMsg;
|
||||
filter.critical = filter.warn || instance->mDefaultLevel == QtCriticalMsg;
|
||||
}
|
||||
|
||||
for (const auto& rule: *instance->rules) {
|
||||
filter.applyRule(categoryName, rule);
|
||||
}
|
||||
|
||||
if (isQs && !instance->sparse) {
|
||||
// We assume the category name pointer will always be the same and be comparable in the message handler.
|
||||
instance->sparseFilters.insert(static_cast<const void*>(category->categoryName()), filter);
|
||||
|
||||
// all enabled by default
|
||||
CategoryFilter().apply(category);
|
||||
} else {
|
||||
filter.apply(category);
|
||||
}
|
||||
|
||||
instance->allFilters.insert(categoryName, filter);
|
||||
}
|
||||
|
||||
LogManager* LogManager::instance() {
|
||||
static auto* instance = new LogManager(); // NOLINT
|
||||
return instance;
|
||||
}
|
||||
|
||||
void LogManager::init(
|
||||
bool color,
|
||||
bool timestamp,
|
||||
bool sparseOnly,
|
||||
QtMsgType defaultLevel,
|
||||
const QString& rules,
|
||||
const QString& prefix
|
||||
) {
|
||||
auto* instance = LogManager::instance();
|
||||
instance->colorLogs = color;
|
||||
instance->timestampLogs = timestamp;
|
||||
instance->sparse = sparseOnly;
|
||||
instance->prefix = prefix;
|
||||
instance->mDefaultLevel = defaultLevel;
|
||||
instance->mRulesString = rules;
|
||||
|
||||
{
|
||||
QLoggingSettingsParser parser;
|
||||
parser.setContent(rules);
|
||||
instance->rules = new QList(parser.rules());
|
||||
}
|
||||
|
||||
qInstallMessageHandler(&LogManager::messageHandler);
|
||||
|
||||
instance->lastCategoryFilter = QLoggingCategory::installFilter(&LogManager::filterCategory);
|
||||
|
||||
qCDebug(logLogging) << "Creating offthread logger...";
|
||||
auto* thread = new QThread();
|
||||
instance->threadProxy.moveToThread(thread);
|
||||
thread->start();
|
||||
|
||||
QMetaObject::invokeMethod(
|
||||
&instance->threadProxy,
|
||||
&LoggingThreadProxy::initInThread,
|
||||
Qt::BlockingQueuedConnection
|
||||
);
|
||||
|
||||
qCDebug(logLogging) << "Logger initialized.";
|
||||
}
|
||||
|
||||
void LogManager::initFs() {
|
||||
QMetaObject::invokeMethod(
|
||||
&LogManager::instance()->threadProxy,
|
||||
"initFs",
|
||||
Qt::BlockingQueuedConnection
|
||||
);
|
||||
}
|
||||
|
||||
QString LogManager::rulesString() const { return this->mRulesString; }
|
||||
QtMsgType LogManager::defaultLevel() const { return this->mDefaultLevel; }
|
||||
bool LogManager::isSparse() const { return this->sparse; }
|
||||
|
||||
CategoryFilter LogManager::getFilter(QLatin1StringView category) {
|
||||
return this->allFilters.value(category);
|
||||
}
|
||||
|
||||
void LoggingThreadProxy::initInThread() {
|
||||
this->logging = new ThreadLogging(this);
|
||||
this->logging->init();
|
||||
}
|
||||
|
||||
void LoggingThreadProxy::initFs() { this->logging->initFs(); }
|
||||
|
||||
void ThreadLogging::init() {
|
||||
auto logMfd = memfd_create("quickshell:logs", 0);
|
||||
|
||||
if (logMfd == -1) {
|
||||
qCCritical(logLogging) << "Failed to create memfd for initial log storage"
|
||||
<< qt_error_string(-1);
|
||||
}
|
||||
|
||||
auto dlogMfd = memfd_create("quickshell:detailedlogs", 0);
|
||||
|
||||
if (dlogMfd == -1) {
|
||||
qCCritical(logLogging) << "Failed to create memfd for initial detailed log storage"
|
||||
<< qt_error_string(-1);
|
||||
}
|
||||
|
||||
if (logMfd != -1) {
|
||||
this->file = new QFile();
|
||||
this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle);
|
||||
this->fileStream.setDevice(this->file);
|
||||
}
|
||||
|
||||
if (dlogMfd != -1) {
|
||||
crash::CrashInfo::INSTANCE.logFd = dlogMfd;
|
||||
|
||||
this->detailedFile = new QFile();
|
||||
// buffered by WriteBuffer
|
||||
this->detailedFile->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle);
|
||||
this->detailedWriter.setDevice(this->detailedFile);
|
||||
|
||||
if (!this->detailedWriter.writeHeader()) {
|
||||
qCCritical(logLogging) << "Could not write header for detailed logs.";
|
||||
this->detailedWriter.setDevice(nullptr);
|
||||
delete this->detailedFile;
|
||||
this->detailedFile = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// This connection is direct so it works while the event loop is destroyed between
|
||||
// QCoreApplication delete and Q(Gui)Application launch.
|
||||
QObject::connect(
|
||||
LogManager::instance(),
|
||||
&LogManager::logMessage,
|
||||
this,
|
||||
&ThreadLogging::onMessage,
|
||||
Qt::DirectConnection
|
||||
);
|
||||
|
||||
qCDebug(logLogging) << "Created memfd" << logMfd << "for early logs.";
|
||||
qCDebug(logLogging) << "Created memfd" << dlogMfd << "for early detailed logs.";
|
||||
}
|
||||
|
||||
void ThreadLogging::initFs() {
|
||||
qCDebug(logLogging) << "Starting filesystem logging...";
|
||||
auto* runDir = QsPaths::instance()->instanceRunDir();
|
||||
|
||||
if (!runDir) {
|
||||
qCCritical(logLogging
|
||||
) << "Could not start filesystem logging as the runtime directory could not be created.";
|
||||
return;
|
||||
}
|
||||
|
||||
auto path = runDir->filePath("log.log");
|
||||
auto detailedPath = runDir->filePath("log.qslog");
|
||||
auto* file = new QFile(path);
|
||||
auto* detailedFile = new QFile(detailedPath);
|
||||
|
||||
if (!file->open(QFile::ReadWrite | QFile::Truncate)) {
|
||||
qCCritical(logLogging
|
||||
) << "Could not start filesystem logger as the log file could not be created:"
|
||||
<< path;
|
||||
delete file;
|
||||
file = nullptr;
|
||||
} else {
|
||||
qInfo() << "Saving logs to" << path;
|
||||
}
|
||||
|
||||
// buffered by WriteBuffer
|
||||
if (!detailedFile->open(QFile::ReadWrite | QFile::Truncate | QFile::Unbuffered)) {
|
||||
qCCritical(logLogging
|
||||
) << "Could not start detailed filesystem logger as the log file could not be created:"
|
||||
<< detailedPath;
|
||||
delete detailedFile;
|
||||
detailedFile = nullptr;
|
||||
} else {
|
||||
auto lock = flock {
|
||||
.l_type = F_WRLCK,
|
||||
.l_whence = SEEK_SET,
|
||||
.l_start = 0,
|
||||
.l_len = 0,
|
||||
.l_pid = 0,
|
||||
};
|
||||
|
||||
if (fcntl(detailedFile->handle(), F_SETLK, &lock) != 0) { // NOLINT
|
||||
qCWarning(logLogging) << "Unable to set lock marker on detailed log file. --follow from "
|
||||
"other instances will not work.";
|
||||
}
|
||||
|
||||
qCInfo(logLogging) << "Saving detailed logs to" << path;
|
||||
}
|
||||
|
||||
qCDebug(logLogging) << "Copying memfd logs to log file...";
|
||||
|
||||
if (file) {
|
||||
auto* oldFile = this->file;
|
||||
if (oldFile) {
|
||||
oldFile->seek(0);
|
||||
sendfile(file->handle(), oldFile->handle(), nullptr, oldFile->size());
|
||||
}
|
||||
|
||||
this->file = file;
|
||||
this->fileStream.setDevice(file);
|
||||
delete oldFile;
|
||||
}
|
||||
|
||||
if (detailedFile) {
|
||||
auto* oldFile = this->detailedFile;
|
||||
if (oldFile) {
|
||||
oldFile->seek(0);
|
||||
sendfile(detailedFile->handle(), oldFile->handle(), nullptr, oldFile->size());
|
||||
}
|
||||
|
||||
crash::CrashInfo::INSTANCE.logFd = detailedFile->handle();
|
||||
|
||||
this->detailedFile = detailedFile;
|
||||
this->detailedWriter.setDevice(detailedFile);
|
||||
|
||||
if (!oldFile) {
|
||||
if (!this->detailedWriter.writeHeader()) {
|
||||
qCCritical(logLogging) << "Could not write header for detailed logs.";
|
||||
this->detailedWriter.setDevice(nullptr);
|
||||
delete this->detailedFile;
|
||||
this->detailedFile = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
delete oldFile;
|
||||
}
|
||||
|
||||
qCDebug(logLogging) << "Switched logging to disk logs.";
|
||||
|
||||
auto* logManager = LogManager::instance();
|
||||
QObject::disconnect(logManager, &LogManager::logMessage, this, &ThreadLogging::onMessage);
|
||||
|
||||
QObject::connect(
|
||||
logManager,
|
||||
&LogManager::logMessage,
|
||||
this,
|
||||
&ThreadLogging::onMessage,
|
||||
Qt::QueuedConnection
|
||||
);
|
||||
|
||||
qCDebug(logLogging) << "Switched threaded logger to queued eventloop connection.";
|
||||
}
|
||||
|
||||
void ThreadLogging::onMessage(const LogMessage& msg, bool showInSparse) {
|
||||
if (showInSparse) {
|
||||
if (this->fileStream.device() == nullptr) return;
|
||||
LogMessage::formatMessage(this->fileStream, msg, false, true);
|
||||
this->fileStream << Qt::endl;
|
||||
}
|
||||
|
||||
if (this->detailedWriter.write(msg)) {
|
||||
this->detailedFile->flush();
|
||||
} else if (this->detailedFile != nullptr) {
|
||||
qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs.";
|
||||
}
|
||||
}
|
||||
|
||||
CompressedLogType compressedTypeOf(QtMsgType type) {
|
||||
switch (type) {
|
||||
case QtDebugMsg: return CompressedLogType::Debug;
|
||||
case QtInfoMsg: return CompressedLogType::Info;
|
||||
case QtWarningMsg: return CompressedLogType::Warn;
|
||||
case QtCriticalMsg:
|
||||
case QtFatalMsg: return CompressedLogType::Critical;
|
||||
}
|
||||
|
||||
return CompressedLogType::Info; // unreachable under normal conditions
|
||||
}
|
||||
|
||||
QtMsgType typeOfCompressed(CompressedLogType type) {
|
||||
switch (type) {
|
||||
case CompressedLogType::Debug: return QtDebugMsg;
|
||||
case CompressedLogType::Info: return QtInfoMsg;
|
||||
case CompressedLogType::Warn: return QtWarningMsg;
|
||||
case CompressedLogType::Critical: return QtCriticalMsg;
|
||||
}
|
||||
|
||||
return QtInfoMsg; // unreachable under normal conditions
|
||||
}
|
||||
|
||||
void WriteBuffer::setDevice(QIODevice* device) { this->device = device; }
|
||||
bool WriteBuffer::hasDevice() const { return this->device; }
|
||||
|
||||
bool WriteBuffer::flush() {
|
||||
auto written = this->device->write(this->buffer);
|
||||
auto success = written == this->buffer.length();
|
||||
this->buffer.clear();
|
||||
return success;
|
||||
}
|
||||
|
||||
void WriteBuffer::writeBytes(const char* data, qsizetype length) {
|
||||
this->buffer.append(data, length);
|
||||
}
|
||||
|
||||
void WriteBuffer::writeU8(quint8 data) { this->writeBytes(reinterpret_cast<char*>(&data), 1); }
|
||||
|
||||
void WriteBuffer::writeU16(quint16 data) {
|
||||
data = qToLittleEndian(data);
|
||||
this->writeBytes(reinterpret_cast<char*>(&data), 2);
|
||||
}
|
||||
|
||||
void WriteBuffer::writeU32(quint32 data) {
|
||||
data = qToLittleEndian(data);
|
||||
this->writeBytes(reinterpret_cast<char*>(&data), 4);
|
||||
}
|
||||
|
||||
void WriteBuffer::writeU64(quint64 data) {
|
||||
data = qToLittleEndian(data);
|
||||
this->writeBytes(reinterpret_cast<char*>(&data), 8);
|
||||
}
|
||||
|
||||
void DeviceReader::setDevice(QIODevice* device) { this->device = device; }
|
||||
bool DeviceReader::hasDevice() const { return this->device; }
|
||||
|
||||
bool DeviceReader::readBytes(char* data, qsizetype length) {
|
||||
return this->device->read(data, length) == length;
|
||||
}
|
||||
|
||||
qsizetype DeviceReader::peekBytes(char* data, qsizetype length) {
|
||||
return this->device->peek(data, length);
|
||||
}
|
||||
|
||||
bool DeviceReader::skip(qsizetype length) { return this->device->skip(length) == length; }
|
||||
|
||||
bool DeviceReader::readU8(quint8* data) {
|
||||
return this->readBytes(reinterpret_cast<char*>(data), 1);
|
||||
}
|
||||
|
||||
bool DeviceReader::readU16(quint16* data) {
|
||||
return this->readBytes(reinterpret_cast<char*>(data), 2);
|
||||
}
|
||||
|
||||
bool DeviceReader::readU32(quint32* data) {
|
||||
return this->readBytes(reinterpret_cast<char*>(data), 4);
|
||||
}
|
||||
|
||||
bool DeviceReader::readU64(quint64* data) {
|
||||
return this->readBytes(reinterpret_cast<char*>(data), 8);
|
||||
}
|
||||
|
||||
void EncodedLogWriter::setDevice(QIODevice* target) { this->buffer.setDevice(target); }
|
||||
void EncodedLogReader::setDevice(QIODevice* source) { this->reader.setDevice(source); }
|
||||
|
||||
constexpr quint8 LOG_VERSION = 2;
|
||||
|
||||
bool EncodedLogWriter::writeHeader() {
|
||||
this->buffer.writeU8(LOG_VERSION);
|
||||
return this->buffer.flush();
|
||||
}
|
||||
|
||||
bool EncodedLogReader::readHeader(bool* success, quint8* version, quint8* readerVersion) {
|
||||
if (!this->reader.readU8(version)) return false;
|
||||
*success = *version == LOG_VERSION;
|
||||
*readerVersion = LOG_VERSION;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EncodedLogWriter::write(const LogMessage& message) {
|
||||
if (!this->buffer.hasDevice()) return false;
|
||||
|
||||
LogMessage* prevMessage = nullptr;
|
||||
auto index = this->recentMessages.indexOf(message, &prevMessage);
|
||||
|
||||
// If its a dupe, save memory by reusing the buffer of the first message and letting
|
||||
// the new one be deallocated.
|
||||
auto body = prevMessage ? prevMessage->body : message.body;
|
||||
this->recentMessages.emplace(message.type, message.category, body, message.time);
|
||||
|
||||
if (index != -1) {
|
||||
auto secondDelta = this->lastMessageTime.secsTo(message.time);
|
||||
|
||||
if (secondDelta < 16 && index < 16) {
|
||||
this->writeOp(EncodedLogOpcode::RecentMessageShort);
|
||||
this->buffer.writeU8(index | (secondDelta << 4));
|
||||
} else {
|
||||
this->writeOp(EncodedLogOpcode::RecentMessageLong);
|
||||
this->buffer.writeU8(index);
|
||||
this->writeVarInt(secondDelta);
|
||||
}
|
||||
|
||||
goto finish;
|
||||
} else {
|
||||
auto categoryId = this->getOrCreateCategory(message.category);
|
||||
this->writeVarInt(categoryId);
|
||||
|
||||
auto writeFullTimestamp = [this, &message]() {
|
||||
this->buffer.writeU64(message.time.toSecsSinceEpoch());
|
||||
};
|
||||
|
||||
if (message.type == QtFatalMsg) {
|
||||
this->buffer.writeU8(0xff);
|
||||
writeFullTimestamp();
|
||||
} else {
|
||||
quint8 field = compressedTypeOf(message.type);
|
||||
|
||||
auto secondDelta = this->lastMessageTime.secsTo(message.time);
|
||||
if (secondDelta >= 0x1d) {
|
||||
// 0x1d = followed by delta int
|
||||
// 0x1e = followed by epoch delta int
|
||||
field |= (secondDelta < 0xffff ? 0x1d : 0x1e) << 3;
|
||||
} else {
|
||||
field |= secondDelta << 3;
|
||||
}
|
||||
|
||||
this->buffer.writeU8(field);
|
||||
|
||||
if (secondDelta >= 0x1d) {
|
||||
if (secondDelta > 0xffff) {
|
||||
writeFullTimestamp();
|
||||
} else {
|
||||
this->writeVarInt(secondDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this->writeString(message.body);
|
||||
}
|
||||
|
||||
finish:
|
||||
// copy with second precision
|
||||
this->lastMessageTime = QDateTime::fromSecsSinceEpoch(message.time.toSecsSinceEpoch());
|
||||
return this->buffer.flush();
|
||||
}
|
||||
|
||||
bool EncodedLogReader::read(LogMessage* slot) {
|
||||
start:
|
||||
quint32 next = 0;
|
||||
if (!this->readVarInt(&next)) return false;
|
||||
|
||||
if (next < EncodedLogOpcode::BeginCategories) {
|
||||
if (next == EncodedLogOpcode::RegisterCategory) {
|
||||
if (!this->registerCategory()) return false;
|
||||
goto start;
|
||||
} else if (next == EncodedLogOpcode::RecentMessageShort
|
||||
|| next == EncodedLogOpcode::RecentMessageLong)
|
||||
{
|
||||
quint8 index = 0;
|
||||
quint32 secondDelta = 0;
|
||||
|
||||
if (next == EncodedLogOpcode::RecentMessageShort) {
|
||||
quint8 field = 0;
|
||||
if (!this->reader.readU8(&field)) return false;
|
||||
index = field & 0xf;
|
||||
secondDelta = field >> 4;
|
||||
} else {
|
||||
if (!this->reader.readU8(&index)) return false;
|
||||
if (!this->readVarInt(&secondDelta)) return false;
|
||||
}
|
||||
|
||||
if (index >= this->recentMessages.size()) return false;
|
||||
*slot = this->recentMessages.at(index);
|
||||
this->lastMessageTime = this->lastMessageTime.addSecs(static_cast<qint64>(secondDelta));
|
||||
slot->time = this->lastMessageTime;
|
||||
}
|
||||
} else {
|
||||
auto categoryId = next - EncodedLogOpcode::BeginCategories;
|
||||
auto category = this->categories.value(categoryId);
|
||||
|
||||
quint8 field = 0;
|
||||
if (!this->reader.readU8(&field)) return false;
|
||||
|
||||
auto msgType = QtDebugMsg;
|
||||
quint64 secondDelta = 0;
|
||||
auto needsTimeRead = false;
|
||||
|
||||
if (field == 0xff) {
|
||||
msgType = QtFatalMsg;
|
||||
needsTimeRead = true;
|
||||
} else {
|
||||
msgType = typeOfCompressed(static_cast<CompressedLogType>(field & 0x07));
|
||||
secondDelta = field >> 3;
|
||||
|
||||
if (secondDelta == 0x1d) {
|
||||
quint32 slot = 0;
|
||||
if (!this->readVarInt(&slot)) return false;
|
||||
secondDelta = slot;
|
||||
} else if (secondDelta == 0x1e) {
|
||||
needsTimeRead = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsTimeRead) {
|
||||
if (!this->reader.readU64(&secondDelta)) return false;
|
||||
}
|
||||
|
||||
this->lastMessageTime = this->lastMessageTime.addSecs(static_cast<qint64>(secondDelta));
|
||||
|
||||
QByteArray body;
|
||||
if (!this->readString(&body)) return false;
|
||||
|
||||
*slot = LogMessage(msgType, QLatin1StringView(category.first), body, this->lastMessageTime);
|
||||
slot->readCategoryId = categoryId;
|
||||
}
|
||||
|
||||
this->recentMessages.emplace(*slot);
|
||||
return true;
|
||||
}
|
||||
|
||||
CategoryFilter EncodedLogReader::categoryFilterById(quint16 id) {
|
||||
return this->categories.value(id).second;
|
||||
}
|
||||
|
||||
void EncodedLogWriter::writeOp(EncodedLogOpcode opcode) { this->buffer.writeU8(opcode); }
|
||||
|
||||
void EncodedLogWriter::writeVarInt(quint32 n) {
|
||||
if (n < 0xff) {
|
||||
this->buffer.writeU8(n);
|
||||
} else if (n < 0xffff) {
|
||||
this->buffer.writeU8(0xff);
|
||||
this->buffer.writeU16(n);
|
||||
} else {
|
||||
this->buffer.writeU8(0xff);
|
||||
this->buffer.writeU16(0xffff);
|
||||
this->buffer.writeU32(n);
|
||||
}
|
||||
}
|
||||
|
||||
bool EncodedLogReader::readVarInt(quint32* slot) {
|
||||
auto bytes = std::array<quint8, 7>();
|
||||
auto readLength = this->reader.peekBytes(reinterpret_cast<char*>(bytes.data()), 7);
|
||||
|
||||
if (bytes[0] != 0xff && readLength >= 1) {
|
||||
auto n = *reinterpret_cast<quint8*>(bytes.data());
|
||||
if (!this->reader.skip(1)) return false;
|
||||
*slot = qFromLittleEndian(n);
|
||||
} else if ((bytes[1] != 0xff || bytes[2] != 0xff) && readLength >= 3) {
|
||||
auto n = *reinterpret_cast<quint16*>(bytes.data() + 1);
|
||||
if (!this->reader.skip(3)) return false;
|
||||
*slot = qFromLittleEndian(n);
|
||||
} else if (readLength == 7) {
|
||||
auto n = *reinterpret_cast<quint32*>(bytes.data() + 3);
|
||||
if (!this->reader.skip(7)) return false;
|
||||
*slot = qFromLittleEndian(n);
|
||||
} else return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void EncodedLogWriter::writeString(QByteArrayView bytes) {
|
||||
this->writeVarInt(bytes.length());
|
||||
this->buffer.writeBytes(bytes.constData(), bytes.length());
|
||||
}
|
||||
|
||||
bool EncodedLogReader::readString(QByteArray* slot) {
|
||||
quint32 length = 0;
|
||||
if (!this->readVarInt(&length)) return false;
|
||||
|
||||
*slot = QByteArray(length, Qt::Uninitialized);
|
||||
auto r = this->reader.readBytes(slot->data(), slot->size());
|
||||
return r;
|
||||
}
|
||||
|
||||
quint16 EncodedLogWriter::getOrCreateCategory(QLatin1StringView category) {
|
||||
if (this->categories.contains(category)) {
|
||||
return this->categories.value(category);
|
||||
} else {
|
||||
this->writeOp(EncodedLogOpcode::RegisterCategory);
|
||||
// id is implicitly the next available id
|
||||
this->writeString(category);
|
||||
|
||||
auto id = this->nextCategory++;
|
||||
this->categories.insert(category, id);
|
||||
|
||||
auto filter = LogManager::instance()->getFilter(category);
|
||||
quint8 flags = 0;
|
||||
flags |= filter.debug << 0;
|
||||
flags |= filter.info << 1;
|
||||
flags |= filter.warn << 2;
|
||||
flags |= filter.critical << 3;
|
||||
|
||||
this->buffer.writeU8(flags);
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
bool EncodedLogReader::registerCategory() {
|
||||
QByteArray name;
|
||||
quint8 flags = 0;
|
||||
if (!this->readString(&name)) return false;
|
||||
if (!this->reader.readU8(&flags)) return false;
|
||||
|
||||
CategoryFilter filter;
|
||||
filter.debug = (flags >> 0) & 1;
|
||||
filter.info = (flags >> 1) & 1;
|
||||
filter.warn = (flags >> 2) & 1;
|
||||
filter.critical = (flags >> 3) & 1;
|
||||
|
||||
this->categories.append(qMakePair(name, filter));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LogReader::initialize() {
|
||||
this->reader.setDevice(this->file);
|
||||
|
||||
bool readable = false;
|
||||
quint8 logVersion = 0;
|
||||
quint8 readerVersion = 0;
|
||||
if (!this->reader.readHeader(&readable, &logVersion, &readerVersion)) {
|
||||
qCritical() << "Failed to read log header.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!readable) {
|
||||
qCritical() << "This log was encoded with version" << logVersion
|
||||
<< "of the quickshell log encoder, which cannot be decoded by the current "
|
||||
"version of quickshell, with log version"
|
||||
<< readerVersion;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LogReader::continueReading() {
|
||||
auto color = LogManager::instance()->colorLogs;
|
||||
auto tailRing = RingBuffer<LogMessage>(this->remainingTail);
|
||||
|
||||
LogMessage message;
|
||||
auto stream = QTextStream(stdout);
|
||||
auto readCursor = this->file->pos();
|
||||
while (this->reader.read(&message)) {
|
||||
readCursor = this->file->pos();
|
||||
|
||||
CategoryFilter filter;
|
||||
if (this->filters.contains(message.readCategoryId)) {
|
||||
filter = this->filters.value(message.readCategoryId);
|
||||
} else {
|
||||
filter = this->reader.categoryFilterById(message.readCategoryId);
|
||||
|
||||
for (const auto& rule: this->rules) {
|
||||
filter.applyRule(message.category, rule);
|
||||
}
|
||||
|
||||
this->filters.insert(message.readCategoryId, filter);
|
||||
}
|
||||
|
||||
if (filter.shouldDisplay(message.type)) {
|
||||
if (this->remainingTail == 0) {
|
||||
LogMessage::formatMessage(stream, message, color, this->timestamps);
|
||||
stream << '\n';
|
||||
} else {
|
||||
tailRing.emplace(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this->remainingTail != 0) {
|
||||
for (auto i = tailRing.size() - 1; i != -1; i--) {
|
||||
auto& message = tailRing.at(i);
|
||||
LogMessage::formatMessage(stream, message, color, this->timestamps);
|
||||
stream << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
stream << Qt::flush;
|
||||
|
||||
if (this->file->pos() != readCursor) {
|
||||
qCritical() << "An error occurred parsing the end of this log file.";
|
||||
qCritical() << "Remaining data:" << this->file->readAll();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void LogFollower::FcntlWaitThread::run() {
|
||||
auto lock = flock {
|
||||
.l_type = F_RDLCK, // won't block other read locks when we take it
|
||||
.l_whence = SEEK_SET,
|
||||
.l_start = 0,
|
||||
.l_len = 0,
|
||||
.l_pid = 0,
|
||||
};
|
||||
|
||||
auto r = fcntl(this->follower->reader->file->handle(), F_SETLKW, &lock); // NOLINT
|
||||
|
||||
if (r != 0) {
|
||||
qCWarning(logLogging).nospace()
|
||||
<< "Failed to wait for write locks to be removed from log file with error code " << errno
|
||||
<< ": " << qt_error_string();
|
||||
}
|
||||
}
|
||||
|
||||
bool LogFollower::follow() {
|
||||
QObject::connect(&this->waitThread, &QThread::finished, this, &LogFollower::onFileLocked);
|
||||
|
||||
QObject::connect(
|
||||
&this->fileWatcher,
|
||||
&QFileSystemWatcher::fileChanged,
|
||||
this,
|
||||
&LogFollower::onFileChanged
|
||||
);
|
||||
|
||||
this->fileWatcher.addPath(this->path);
|
||||
this->waitThread.start();
|
||||
|
||||
auto r = QCoreApplication::exec();
|
||||
return r == 0;
|
||||
}
|
||||
|
||||
void LogFollower::onFileChanged() {
|
||||
if (!this->reader->continueReading()) {
|
||||
QCoreApplication::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void LogFollower::onFileLocked() {
|
||||
if (!this->reader->continueReading()) {
|
||||
QCoreApplication::exit(1);
|
||||
} else {
|
||||
QCoreApplication::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
bool readEncodedLogs(
|
||||
QFile* file,
|
||||
const QString& path,
|
||||
bool timestamps,
|
||||
int tail,
|
||||
bool follow,
|
||||
const QString& rulespec
|
||||
) {
|
||||
QList<QLoggingRule> rules;
|
||||
|
||||
{
|
||||
QLoggingSettingsParser parser;
|
||||
parser.setContent(rulespec);
|
||||
rules = parser.rules();
|
||||
}
|
||||
|
||||
auto reader = LogReader(file, timestamps, tail, rules);
|
||||
|
||||
if (!reader.initialize()) return false;
|
||||
if (!reader.continueReading()) return false;
|
||||
|
||||
if (follow) {
|
||||
auto follower = LogFollower(&reader, path);
|
||||
return follower.follow();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace qs::log
|
|
@ -1,148 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdatetime.h>
|
||||
#include <qfile.h>
|
||||
#include <qhash.h>
|
||||
#include <qlatin1stringview.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qobject.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(logBare);
|
||||
|
||||
namespace qs::log {
|
||||
|
||||
struct LogMessage {
|
||||
explicit LogMessage() = default;
|
||||
|
||||
explicit LogMessage(
|
||||
QtMsgType type,
|
||||
QLatin1StringView category,
|
||||
QByteArray body,
|
||||
QDateTime time = QDateTime::currentDateTime()
|
||||
)
|
||||
: type(type)
|
||||
, time(std::move(time))
|
||||
, category(category)
|
||||
, body(std::move(body)) {}
|
||||
|
||||
bool operator==(const LogMessage& other) const;
|
||||
|
||||
QtMsgType type = QtDebugMsg;
|
||||
QDateTime time;
|
||||
QLatin1StringView category;
|
||||
QByteArray body;
|
||||
quint16 readCategoryId = 0;
|
||||
|
||||
static void formatMessage(
|
||||
QTextStream& stream,
|
||||
const LogMessage& msg,
|
||||
bool color,
|
||||
bool timestamp,
|
||||
const QString& prefix = ""
|
||||
);
|
||||
};
|
||||
|
||||
size_t qHash(const LogMessage& message);
|
||||
|
||||
class ThreadLogging;
|
||||
|
||||
class LoggingThreadProxy: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
explicit LoggingThreadProxy() = default;
|
||||
|
||||
public slots:
|
||||
void initInThread();
|
||||
void initFs();
|
||||
|
||||
private:
|
||||
ThreadLogging* logging = nullptr;
|
||||
};
|
||||
|
||||
namespace qt_logging_registry {
|
||||
class QLoggingRule;
|
||||
}
|
||||
|
||||
struct CategoryFilter {
|
||||
explicit CategoryFilter() = default;
|
||||
explicit CategoryFilter(QLoggingCategory* category)
|
||||
: debug(category->isDebugEnabled())
|
||||
, info(category->isInfoEnabled())
|
||||
, warn(category->isWarningEnabled())
|
||||
, critical(category->isCriticalEnabled()) {}
|
||||
|
||||
[[nodiscard]] bool shouldDisplay(QtMsgType type) const;
|
||||
void apply(QLoggingCategory* category) const;
|
||||
void applyRule(QLatin1StringView category, const qt_logging_registry::QLoggingRule& rule);
|
||||
|
||||
bool debug = true;
|
||||
bool info = true;
|
||||
bool warn = true;
|
||||
bool critical = true;
|
||||
};
|
||||
|
||||
class LogManager: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
static void init(
|
||||
bool color,
|
||||
bool timestamp,
|
||||
bool sparseOnly,
|
||||
QtMsgType defaultLevel,
|
||||
const QString& rules,
|
||||
const QString& prefix = ""
|
||||
);
|
||||
|
||||
static void initFs();
|
||||
static LogManager* instance();
|
||||
|
||||
bool colorLogs = true;
|
||||
bool timestampLogs = false;
|
||||
|
||||
[[nodiscard]] QString rulesString() const;
|
||||
[[nodiscard]] QtMsgType defaultLevel() const;
|
||||
[[nodiscard]] bool isSparse() const;
|
||||
|
||||
[[nodiscard]] CategoryFilter getFilter(QLatin1StringView category);
|
||||
|
||||
signals:
|
||||
void logMessage(LogMessage msg, bool showInSparse);
|
||||
|
||||
private:
|
||||
explicit LogManager();
|
||||
static void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg);
|
||||
|
||||
static void filterCategory(QLoggingCategory* category);
|
||||
|
||||
QLoggingCategory::CategoryFilter lastCategoryFilter = nullptr;
|
||||
bool sparse = false;
|
||||
QString prefix;
|
||||
QString mRulesString;
|
||||
QList<qt_logging_registry::QLoggingRule>* rules = nullptr;
|
||||
QtMsgType mDefaultLevel = QtWarningMsg;
|
||||
QHash<const void*, CategoryFilter> sparseFilters;
|
||||
QHash<QLatin1StringView, CategoryFilter> allFilters;
|
||||
|
||||
QTextStream stdoutStream;
|
||||
LoggingThreadProxy threadProxy;
|
||||
};
|
||||
|
||||
bool readEncodedLogs(
|
||||
QFile* file,
|
||||
const QString& path,
|
||||
bool timestamps,
|
||||
int tail,
|
||||
bool follow,
|
||||
const QString& rulespec
|
||||
);
|
||||
|
||||
} // namespace qs::log
|
||||
|
||||
using LogManager = qs::log::LogManager;
|
|
@ -1,190 +0,0 @@
|
|||
#pragma once
|
||||
#include <utility>
|
||||
|
||||
#include <qbytearrayview.h>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qfile.h>
|
||||
#include <qfilesystemwatcher.h>
|
||||
#include <qlogging.h>
|
||||
#include <qobject.h>
|
||||
#include <qthread.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "logging.hpp"
|
||||
#include "logging_qtprivate.hpp"
|
||||
#include "ringbuf.hpp"
|
||||
|
||||
namespace qs::log {
|
||||
|
||||
enum EncodedLogOpcode : quint8 {
|
||||
RegisterCategory = 0,
|
||||
RecentMessageShort,
|
||||
RecentMessageLong,
|
||||
BeginCategories,
|
||||
};
|
||||
|
||||
enum CompressedLogType : quint8 {
|
||||
Debug = 0,
|
||||
Info = 1,
|
||||
Warn = 2,
|
||||
Critical = 3,
|
||||
};
|
||||
|
||||
CompressedLogType compressedTypeOf(QtMsgType type);
|
||||
QtMsgType typeOfCompressed(CompressedLogType type);
|
||||
|
||||
class WriteBuffer {
|
||||
public:
|
||||
void setDevice(QIODevice* device);
|
||||
[[nodiscard]] bool hasDevice() const;
|
||||
[[nodiscard]] bool flush();
|
||||
void writeBytes(const char* data, qsizetype length);
|
||||
void writeU8(quint8 data);
|
||||
void writeU16(quint16 data);
|
||||
void writeU32(quint32 data);
|
||||
void writeU64(quint64 data);
|
||||
|
||||
private:
|
||||
QIODevice* device = nullptr;
|
||||
QByteArray buffer;
|
||||
};
|
||||
|
||||
class DeviceReader {
|
||||
public:
|
||||
void setDevice(QIODevice* device);
|
||||
[[nodiscard]] bool hasDevice() const;
|
||||
[[nodiscard]] bool readBytes(char* data, qsizetype length);
|
||||
// peek UP TO length
|
||||
[[nodiscard]] qsizetype peekBytes(char* data, qsizetype length);
|
||||
[[nodiscard]] bool skip(qsizetype length);
|
||||
[[nodiscard]] bool readU8(quint8* data);
|
||||
[[nodiscard]] bool readU16(quint16* data);
|
||||
[[nodiscard]] bool readU32(quint32* data);
|
||||
[[nodiscard]] bool readU64(quint64* data);
|
||||
|
||||
private:
|
||||
QIODevice* device = nullptr;
|
||||
};
|
||||
|
||||
class EncodedLogWriter {
|
||||
public:
|
||||
void setDevice(QIODevice* target);
|
||||
[[nodiscard]] bool writeHeader();
|
||||
[[nodiscard]] bool write(const LogMessage& message);
|
||||
|
||||
private:
|
||||
void writeOp(EncodedLogOpcode opcode);
|
||||
void writeVarInt(quint32 n);
|
||||
void writeString(QByteArrayView bytes);
|
||||
quint16 getOrCreateCategory(QLatin1StringView category);
|
||||
|
||||
WriteBuffer buffer;
|
||||
|
||||
QHash<QLatin1StringView, quint16> categories;
|
||||
quint16 nextCategory = EncodedLogOpcode::BeginCategories;
|
||||
|
||||
QDateTime lastMessageTime = QDateTime::fromSecsSinceEpoch(0);
|
||||
HashBuffer<LogMessage> recentMessages {256};
|
||||
};
|
||||
|
||||
class EncodedLogReader {
|
||||
public:
|
||||
void setDevice(QIODevice* source);
|
||||
[[nodiscard]] bool readHeader(bool* success, quint8* logVersion, quint8* readerVersion);
|
||||
// WARNING: log messages written to the given slot are invalidated when the log reader is destroyed.
|
||||
[[nodiscard]] bool read(LogMessage* slot);
|
||||
[[nodiscard]] CategoryFilter categoryFilterById(quint16 id);
|
||||
|
||||
private:
|
||||
[[nodiscard]] bool readVarInt(quint32* slot);
|
||||
[[nodiscard]] bool readString(QByteArray* slot);
|
||||
[[nodiscard]] bool registerCategory();
|
||||
|
||||
DeviceReader reader;
|
||||
QVector<QPair<QByteArray, CategoryFilter>> categories;
|
||||
QDateTime lastMessageTime = QDateTime::fromSecsSinceEpoch(0);
|
||||
RingBuffer<LogMessage> recentMessages {256};
|
||||
};
|
||||
|
||||
class ThreadLogging: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
explicit ThreadLogging(QObject* parent): QObject(parent) {}
|
||||
|
||||
void init();
|
||||
void initFs();
|
||||
void setupFileLogging();
|
||||
|
||||
private slots:
|
||||
void onMessage(const LogMessage& msg, bool showInSparse);
|
||||
|
||||
private:
|
||||
QFile* file = nullptr;
|
||||
QTextStream fileStream;
|
||||
QFile* detailedFile = nullptr;
|
||||
EncodedLogWriter detailedWriter;
|
||||
};
|
||||
|
||||
class LogFollower;
|
||||
|
||||
class LogReader {
|
||||
public:
|
||||
explicit LogReader(
|
||||
QFile* file,
|
||||
bool timestamps,
|
||||
int tail,
|
||||
QList<qt_logging_registry::QLoggingRule> rules
|
||||
)
|
||||
: file(file)
|
||||
, timestamps(timestamps)
|
||||
, remainingTail(tail)
|
||||
, rules(std::move(rules)) {}
|
||||
|
||||
bool initialize();
|
||||
bool continueReading();
|
||||
|
||||
private:
|
||||
QFile* file;
|
||||
EncodedLogReader reader;
|
||||
bool timestamps;
|
||||
int remainingTail;
|
||||
QHash<quint16, CategoryFilter> filters;
|
||||
QList<qt_logging_registry::QLoggingRule> rules;
|
||||
|
||||
friend class LogFollower;
|
||||
};
|
||||
|
||||
class LogFollower: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
explicit LogFollower(LogReader* reader, QString path): reader(reader), path(std::move(path)) {}
|
||||
|
||||
bool follow();
|
||||
|
||||
private slots:
|
||||
void onFileChanged();
|
||||
void onFileLocked();
|
||||
|
||||
private:
|
||||
LogReader* reader;
|
||||
QString path;
|
||||
QFileSystemWatcher fileWatcher;
|
||||
|
||||
class FcntlWaitThread: public QThread {
|
||||
public:
|
||||
explicit FcntlWaitThread(LogFollower* follower): follower(follower) {}
|
||||
|
||||
protected:
|
||||
void run() override;
|
||||
|
||||
private:
|
||||
LogFollower* follower;
|
||||
};
|
||||
|
||||
FcntlWaitThread waitThread {this};
|
||||
};
|
||||
|
||||
} // namespace qs::log
|
|
@ -1,138 +0,0 @@
|
|||
// The logging rule parser from qloggingregistry_p.h and qloggingregistry.cpp.
|
||||
|
||||
// Was unable to properly link the functions when directly using the headers (which we depend
|
||||
// on anyway), so below is a slightly stripped down copy. Making the originals link would
|
||||
// be preferable.
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <qbytearrayview.h>
|
||||
#include <qchar.h>
|
||||
#include <qflags.h>
|
||||
#include <qlist.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qstringtokenizer.h>
|
||||
#include <qstringview.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "logging_qtprivate.hpp"
|
||||
|
||||
namespace qs::log {
|
||||
Q_DECLARE_LOGGING_CATEGORY(logLogging);
|
||||
|
||||
namespace qt_logging_registry {
|
||||
|
||||
class QLoggingSettingsParser {
|
||||
public:
|
||||
void setContent(QStringView content);
|
||||
|
||||
[[nodiscard]] QList<QLoggingRule> rules() const { return this->mRules; }
|
||||
|
||||
private:
|
||||
void parseNextLine(QStringView line);
|
||||
|
||||
private:
|
||||
QList<QLoggingRule> mRules;
|
||||
};
|
||||
|
||||
void QLoggingSettingsParser::setContent(QStringView content) {
|
||||
this->mRules.clear();
|
||||
for (auto line: qTokenize(content, u';')) this->parseNextLine(line);
|
||||
}
|
||||
|
||||
void QLoggingSettingsParser::parseNextLine(QStringView line) {
|
||||
// Remove whitespace at start and end of line:
|
||||
line = line.trimmed();
|
||||
|
||||
const qsizetype equalPos = line.indexOf(u'=');
|
||||
if (equalPos != -1) {
|
||||
if (line.lastIndexOf(u'=') == equalPos) {
|
||||
const auto key = line.left(equalPos).trimmed();
|
||||
const QStringView pattern = key;
|
||||
const auto valueStr = line.mid(equalPos + 1).trimmed();
|
||||
int value = -1;
|
||||
if (valueStr == QString("true")) value = 1;
|
||||
else if (valueStr == QString("false")) value = 0;
|
||||
QLoggingRule rule(pattern, (value == 1));
|
||||
if (rule.flags != 0 && (value != -1)) this->mRules.append(std::move(rule));
|
||||
else
|
||||
qCWarning(logLogging, "Ignoring malformed logging rule: '%s'", line.toUtf8().constData());
|
||||
} else {
|
||||
qCWarning(logLogging, "Ignoring malformed logging rule: '%s'", line.toUtf8().constData());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QLoggingRule::QLoggingRule(QStringView pattern, bool enabled): messageType(-1), enabled(enabled) {
|
||||
this->parse(pattern);
|
||||
}
|
||||
|
||||
void QLoggingRule::parse(QStringView pattern) {
|
||||
QStringView p;
|
||||
|
||||
// strip trailing ".messagetype"
|
||||
if (pattern.endsWith(QString(".debug"))) {
|
||||
p = pattern.chopped(6); // strlen(".debug")
|
||||
this->messageType = QtDebugMsg;
|
||||
} else if (pattern.endsWith(QString(".info"))) {
|
||||
p = pattern.chopped(5); // strlen(".info")
|
||||
this->messageType = QtInfoMsg;
|
||||
} else if (pattern.endsWith(QString(".warning"))) {
|
||||
p = pattern.chopped(8); // strlen(".warning")
|
||||
this->messageType = QtWarningMsg;
|
||||
} else if (pattern.endsWith(QString(".critical"))) {
|
||||
p = pattern.chopped(9); // strlen(".critical")
|
||||
this->messageType = QtCriticalMsg;
|
||||
} else {
|
||||
p = pattern;
|
||||
}
|
||||
|
||||
const QChar asterisk = u'*';
|
||||
if (!p.contains(asterisk)) {
|
||||
this->flags = FullText;
|
||||
} else {
|
||||
if (p.endsWith(asterisk)) {
|
||||
this->flags |= LeftFilter;
|
||||
p = p.chopped(1);
|
||||
}
|
||||
if (p.startsWith(asterisk)) {
|
||||
this->flags |= RightFilter;
|
||||
p = p.mid(1);
|
||||
}
|
||||
if (p.contains(asterisk)) // '*' only supported at start/end
|
||||
this->flags = PatternFlags();
|
||||
}
|
||||
|
||||
this->category = p.toString();
|
||||
}
|
||||
|
||||
int QLoggingRule::pass(QLatin1StringView cat, QtMsgType msgType) const {
|
||||
// check message type
|
||||
if (this->messageType > -1 && this->messageType != msgType) return 0;
|
||||
|
||||
if (this->flags == FullText) {
|
||||
// full match
|
||||
if (this->category == cat) return (this->enabled ? 1 : -1);
|
||||
else return 0;
|
||||
}
|
||||
|
||||
const qsizetype idx = cat.indexOf(this->category);
|
||||
if (idx >= 0) {
|
||||
if (this->flags == MidFilter) {
|
||||
// matches somewhere
|
||||
return (this->enabled ? 1 : -1);
|
||||
} else if (this->flags == LeftFilter) {
|
||||
// matches left
|
||||
if (idx == 0) return (this->enabled ? 1 : -1);
|
||||
} else if (this->flags == RightFilter) {
|
||||
// matches right
|
||||
if (idx == (cat.size() - this->category.size())) return (this->enabled ? 1 : -1);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace qt_logging_registry
|
||||
|
||||
} // namespace qs::log
|
|
@ -1,45 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
// The logging rule parser from qloggingregistry_p.h and qloggingregistry.cpp.
|
||||
|
||||
// Was unable to properly link the functions when directly using the headers (which we depend
|
||||
// on anyway), so below is a slightly stripped down copy. Making the originals link would
|
||||
// be preferable.
|
||||
|
||||
#include <qflags.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qstringview.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
namespace qs::log {
|
||||
Q_DECLARE_LOGGING_CATEGORY(logLogging);
|
||||
|
||||
namespace qt_logging_registry {
|
||||
|
||||
class QLoggingRule {
|
||||
public:
|
||||
QLoggingRule();
|
||||
QLoggingRule(QStringView pattern, bool enabled);
|
||||
[[nodiscard]] int pass(QLatin1StringView categoryName, QtMsgType type) const;
|
||||
|
||||
enum PatternFlag : quint8 {
|
||||
FullText = 0x1,
|
||||
LeftFilter = 0x2,
|
||||
RightFilter = 0x4,
|
||||
MidFilter = LeftFilter | RightFilter
|
||||
};
|
||||
Q_DECLARE_FLAGS(PatternFlags, PatternFlag)
|
||||
|
||||
QString category;
|
||||
int messageType;
|
||||
PatternFlags flags;
|
||||
bool enabled;
|
||||
|
||||
private:
|
||||
void parse(QStringView pattern);
|
||||
};
|
||||
|
||||
} // namespace qt_logging_registry
|
||||
|
||||
} // namespace qs::log
|
331
src/core/main.cpp
Normal file
331
src/core/main.cpp
Normal file
|
@ -0,0 +1,331 @@
|
|||
#include "main.hpp"
|
||||
#include <iostream>
|
||||
|
||||
#include <qapplication.h>
|
||||
#include <qcommandlineoption.h>
|
||||
#include <qcommandlineparser.h>
|
||||
#include <qcoreapplication.h>
|
||||
#include <qdir.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qguiapplication.h>
|
||||
#include <qhash.h>
|
||||
#include <qlogging.h>
|
||||
#include <qquickwindow.h>
|
||||
#include <qstandardpaths.h>
|
||||
#include <qstring.h>
|
||||
#include <qtenvironmentvariables.h>
|
||||
#include <qtextstream.h>
|
||||
#include <qtpreprocessorsupport.h>
|
||||
|
||||
#include "plugin.hpp"
|
||||
#include "rootwrapper.hpp"
|
||||
|
||||
int qs_main(int argc, char** argv) {
|
||||
QString configFilePath;
|
||||
QString workingDirectory;
|
||||
|
||||
auto useQApplication = false;
|
||||
auto nativeTextRendering = false;
|
||||
auto desktopSettingsAware = true;
|
||||
QHash<QString, QString> envOverrides;
|
||||
|
||||
{
|
||||
const auto app = QCoreApplication(argc, argv);
|
||||
QCoreApplication::setApplicationName("quickshell");
|
||||
QCoreApplication::setApplicationVersion("0.1.0 (" GIT_REVISION ")");
|
||||
|
||||
QCommandLineParser parser;
|
||||
parser.addHelpOption();
|
||||
parser.addVersionOption();
|
||||
|
||||
// clang-format off
|
||||
auto currentOption = QCommandLineOption("current", "Print information about the manifest and defaults.");
|
||||
auto manifestOption = QCommandLineOption({"m", "manifest"}, "Path to a configuration manifest.", "path");
|
||||
auto configOption = QCommandLineOption({"c", "config"}, "Name of a configuration in the manifest.", "name");
|
||||
auto pathOption = QCommandLineOption({"p", "path"}, "Path to a configuration file.", "path");
|
||||
auto workdirOption = QCommandLineOption({"d", "workdir"}, "Initial working directory.", "path");
|
||||
// clang-format on
|
||||
|
||||
parser.addOption(currentOption);
|
||||
parser.addOption(manifestOption);
|
||||
parser.addOption(configOption);
|
||||
parser.addOption(pathOption);
|
||||
parser.addOption(workdirOption);
|
||||
parser.process(app);
|
||||
|
||||
{
|
||||
auto printCurrent = parser.isSet(currentOption);
|
||||
|
||||
// NOLINTBEGIN
|
||||
#define CHECK(rname, name, level, label, expr) \
|
||||
QString name = expr; \
|
||||
if (rname.isEmpty() && !name.isEmpty()) { \
|
||||
rname = name; \
|
||||
rname##Level = level; \
|
||||
if (!printCurrent) goto label; \
|
||||
}
|
||||
|
||||
#define OPTSTR(name) (name.isEmpty() ? "(unset)" : name.toStdString())
|
||||
// NOLINTEND
|
||||
|
||||
QString basePath;
|
||||
int basePathLevel = 0;
|
||||
Q_UNUSED(basePathLevel);
|
||||
{
|
||||
// NOLINTBEGIN
|
||||
// clang-format off
|
||||
CHECK(basePath, envBasePath, 0, foundbase, qEnvironmentVariable("QS_BASE_PATH"));
|
||||
CHECK(basePath, defaultBasePath, 0, foundbase, QDir(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)).filePath("quickshell"));
|
||||
// clang-format on
|
||||
// NOLINTEND
|
||||
|
||||
if (printCurrent) {
|
||||
// clang-format off
|
||||
std::cout << "Base path: " << OPTSTR(basePath) << "\n";
|
||||
std::cout << " - Environment (QS_BASE_PATH): " << OPTSTR(envBasePath) << "\n";
|
||||
std::cout << " - Default: " << OPTSTR(defaultBasePath) << "\n";
|
||||
// clang-format on
|
||||
}
|
||||
}
|
||||
foundbase:;
|
||||
|
||||
QString configPath;
|
||||
int configPathLevel = 10;
|
||||
{
|
||||
// NOLINTBEGIN
|
||||
CHECK(configPath, optionConfigPath, 0, foundpath, parser.value(pathOption));
|
||||
CHECK(configPath, envConfigPath, 1, foundpath, qEnvironmentVariable("QS_CONFIG_PATH"));
|
||||
// NOLINTEND
|
||||
|
||||
if (printCurrent) {
|
||||
// clang-format off
|
||||
std::cout << "\nConfig path: " << OPTSTR(configPath) << "\n";
|
||||
std::cout << " - Option: " << OPTSTR(optionConfigPath) << "\n";
|
||||
std::cout << " - Environment (QS_CONFIG_PATH): " << OPTSTR(envConfigPath) << "\n";
|
||||
// clang-format on
|
||||
}
|
||||
}
|
||||
foundpath:;
|
||||
|
||||
QString manifestPath;
|
||||
int manifestPathLevel = 10;
|
||||
{
|
||||
// NOLINTBEGIN
|
||||
// clang-format off
|
||||
CHECK(manifestPath, optionManifestPath, 0, foundmf, parser.value(manifestOption));
|
||||
CHECK(manifestPath, envManifestPath, 1, foundmf, qEnvironmentVariable("QS_MANIFEST"));
|
||||
CHECK(manifestPath, defaultManifestPath, 2, foundmf, QDir(basePath).filePath("manifest.conf"));
|
||||
// clang-format on
|
||||
// NOLINTEND
|
||||
|
||||
if (printCurrent) {
|
||||
// clang-format off
|
||||
std::cout << "\nManifest path: " << OPTSTR(manifestPath) << "\n";
|
||||
std::cout << " - Option: " << OPTSTR(optionManifestPath) << "\n";
|
||||
std::cout << " - Environment (QS_MANIFEST): " << OPTSTR(envManifestPath) << "\n";
|
||||
std::cout << " - Default: " << OPTSTR(defaultManifestPath) << "\n";
|
||||
// clang-format on
|
||||
}
|
||||
}
|
||||
foundmf:;
|
||||
|
||||
QString configName;
|
||||
int configNameLevel = 10;
|
||||
{
|
||||
// NOLINTBEGIN
|
||||
CHECK(configName, optionConfigName, 0, foundname, parser.value(configOption));
|
||||
CHECK(configName, envConfigName, 1, foundname, qEnvironmentVariable("QS_CONFIG_NAME"));
|
||||
// NOLINTEND
|
||||
|
||||
if (printCurrent) {
|
||||
// clang-format off
|
||||
std::cout << "\nConfig name: " << OPTSTR(configName) << "\n";
|
||||
std::cout << " - Option: " << OPTSTR(optionConfigName) << "\n";
|
||||
std::cout << " - Environment (QS_CONFIG_NAME): " << OPTSTR(envConfigName) << "\n\n";
|
||||
// clang-format on
|
||||
}
|
||||
}
|
||||
foundname:;
|
||||
|
||||
if (configPathLevel == 0 && configNameLevel == 0) {
|
||||
qCritical() << "Pass only one of --path or --config";
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!configPath.isEmpty() && configPathLevel <= configNameLevel) {
|
||||
configFilePath = configPath;
|
||||
} else if (!configName.isEmpty()) {
|
||||
if (!manifestPath.isEmpty()) {
|
||||
auto file = QFile(manifestPath);
|
||||
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
auto stream = QTextStream(&file);
|
||||
while (!stream.atEnd()) {
|
||||
auto line = stream.readLine();
|
||||
if (line.trimmed().startsWith("#")) continue;
|
||||
if (line.trimmed().isEmpty()) continue;
|
||||
|
||||
auto split = line.split('=');
|
||||
if (split.length() != 2) {
|
||||
qCritical() << "manifest line not in expected format 'name = relativepath':"
|
||||
<< line;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (split[0].trimmed() == configName) {
|
||||
configFilePath = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed());
|
||||
goto haspath; // NOLINT
|
||||
}
|
||||
}
|
||||
|
||||
qCritical() << "configuration" << configName << "not found in manifest" << manifestPath;
|
||||
return -1;
|
||||
} else if (manifestPathLevel < 2) {
|
||||
qCritical() << "cannot open config manifest at" << manifestPath;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto basePathInfo = QFileInfo(basePath);
|
||||
if (!basePathInfo.exists()) {
|
||||
qCritical() << "base path does not exist:" << basePath;
|
||||
return -1;
|
||||
} else if (!QFileInfo(basePathInfo.canonicalFilePath()).isDir()) {
|
||||
qCritical() << "base path is not a directory" << basePath;
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto dir = QDir(basePath);
|
||||
for (auto& entry: dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot)) {
|
||||
if (entry == configName) {
|
||||
configFilePath = dir.filePath(entry);
|
||||
goto haspath; // NOLINT
|
||||
}
|
||||
}
|
||||
|
||||
qCritical() << "no directory named " << configName << "found in base path" << basePath;
|
||||
return -1;
|
||||
}
|
||||
haspath:;
|
||||
} else {
|
||||
configFilePath = basePath;
|
||||
}
|
||||
|
||||
auto configFile = QFileInfo(configFilePath);
|
||||
if (!configFile.exists()) {
|
||||
qCritical() << "config path does not exist:" << configFilePath;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (configFile.isDir()) {
|
||||
configFilePath = QDir(configFilePath).filePath("shell.qml");
|
||||
}
|
||||
|
||||
configFile = QFileInfo(configFilePath);
|
||||
if (!configFile.exists()) {
|
||||
qCritical() << "no shell.qml found in config path:" << configFilePath;
|
||||
return -1;
|
||||
} else if (configFile.isDir()) {
|
||||
qCritical() << "shell.qml is a directory:" << configFilePath;
|
||||
return -1;
|
||||
}
|
||||
|
||||
configFilePath = QFileInfo(configFilePath).canonicalFilePath();
|
||||
configFile = QFileInfo(configFilePath);
|
||||
if (!configFile.exists()) {
|
||||
qCritical() << "config file does not exist:" << configFilePath;
|
||||
return -1;
|
||||
} else if (configFile.isDir()) {
|
||||
qCritical() << "config file is a directory:" << configFilePath;
|
||||
return -1;
|
||||
}
|
||||
|
||||
#undef CHECK
|
||||
#undef OPTSTR
|
||||
|
||||
qInfo() << "config file path:" << configFilePath;
|
||||
|
||||
if (printCurrent) return 0;
|
||||
}
|
||||
|
||||
if (!QFile(configFilePath).exists()) {
|
||||
qCritical() << "config file does not exist";
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (parser.isSet(workdirOption)) {
|
||||
workingDirectory = parser.value(workdirOption);
|
||||
}
|
||||
|
||||
auto file = QFile(configFilePath);
|
||||
if (!file.open(QFile::ReadOnly | QFile::Text)) {
|
||||
qCritical() << "could not open config file";
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto stream = QTextStream(&file);
|
||||
while (!stream.atEnd()) {
|
||||
auto line = stream.readLine().trimmed();
|
||||
if (line.startsWith("//@ pragma ")) {
|
||||
auto pragma = line.sliced(11).trimmed();
|
||||
|
||||
if (pragma == "UseQApplication") useQApplication = true;
|
||||
else if (pragma == "NativeTextRendering") nativeTextRendering = true;
|
||||
else if (pragma == "IgnoreSystemSettings") desktopSettingsAware = false;
|
||||
else if (pragma.startsWith("Env ")) {
|
||||
auto envPragma = pragma.sliced(4);
|
||||
auto splitIdx = envPragma.indexOf('=');
|
||||
|
||||
if (splitIdx == -1) {
|
||||
qCritical() << "Env pragma" << pragma << "not in the form 'VAR = VALUE'";
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto var = envPragma.sliced(0, splitIdx).trimmed();
|
||||
auto val = envPragma.sliced(splitIdx + 1).trimmed();
|
||||
envOverrides.insert(var, val);
|
||||
} else {
|
||||
qCritical() << "Unrecognized pragma" << pragma;
|
||||
return -1;
|
||||
}
|
||||
} else if (line.startsWith("import")) break;
|
||||
}
|
||||
|
||||
file.close();
|
||||
}
|
||||
|
||||
for (auto [var, val]: envOverrides.asKeyValueRange()) {
|
||||
qputenv(var.toUtf8(), val.toUtf8());
|
||||
}
|
||||
|
||||
QGuiApplication::setDesktopSettingsAware(desktopSettingsAware);
|
||||
|
||||
QGuiApplication* app = nullptr;
|
||||
|
||||
if (useQApplication) {
|
||||
app = new QApplication(argc, argv);
|
||||
} else {
|
||||
app = new QGuiApplication(argc, argv);
|
||||
}
|
||||
|
||||
if (!workingDirectory.isEmpty()) {
|
||||
QDir::setCurrent(workingDirectory);
|
||||
}
|
||||
|
||||
QuickshellPlugin::initPlugins();
|
||||
|
||||
// Base window transparency appears to be additive.
|
||||
// Use a fully transparent window with a colored rect.
|
||||
QQuickWindow::setDefaultAlphaBuffer(true);
|
||||
|
||||
if (nativeTextRendering) {
|
||||
QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering);
|
||||
}
|
||||
|
||||
auto root = RootWrapper(configFilePath);
|
||||
QGuiApplication::setQuitOnLastWindowClosed(false);
|
||||
|
||||
auto code = QGuiApplication::exec();
|
||||
delete app;
|
||||
return code;
|
||||
}
|
3
src/core/main.hpp
Normal file
3
src/core/main.hpp
Normal file
|
@ -0,0 +1,3 @@
|
|||
#pragma once
|
||||
|
||||
int qs_main(int argc, char** argv); // NOLINT
|
|
@ -1,98 +0,0 @@
|
|||
#include "model.hpp"
|
||||
|
||||
#include <qabstractitemmodel.h>
|
||||
#include <qhash.h>
|
||||
#include <qnamespace.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 != Qt::UserRole) return QVariant();
|
||||
return QVariant::fromValue(this->valuesList.at(index.row()));
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> UntypedObjectModel::roleNames() const {
|
||||
return {{Qt::UserRole, "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, iindex);
|
||||
|
||||
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, iindex);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void UntypedObjectModel::diffUpdate(const QVector<QObject*>& newValues) {
|
||||
for (qsizetype i = 0; i < this->valuesList.length();) {
|
||||
if (newValues.contains(this->valuesList.at(i))) i++;
|
||||
else this->removeAt(i);
|
||||
}
|
||||
|
||||
qsizetype oi = 0;
|
||||
for (auto* object: newValues) {
|
||||
if (this->valuesList.length() == oi || this->valuesList.at(oi) != object) {
|
||||
this->insertObject(object, oi);
|
||||
}
|
||||
|
||||
oi++;
|
||||
}
|
||||
}
|
||||
|
||||
qsizetype UntypedObjectModel::indexOf(QObject* object) { return this->valuesList.indexOf(object); }
|
||||
|
||||
UntypedObjectModel* UntypedObjectModel::emptyInstance() {
|
||||
static auto* instance = new UntypedObjectModel(nullptr); // NOLINT
|
||||
return instance;
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <bit>
|
||||
#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);
|
||||
|
||||
static UntypedObjectModel* emptyInstance();
|
||||
|
||||
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);
|
||||
|
||||
// Assumes only one instance of a specific value
|
||||
void diffUpdate(const QVector<QObject*>& newValues);
|
||||
|
||||
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]] QVector<T*>& valueList() { return *std::bit_cast<QVector<T*>*>(&this->valuesList); }
|
||||
|
||||
[[nodiscard]] const QVector<T*>& valueList() const {
|
||||
return *std::bit_cast<const QVector<T*>*>(&this->valuesList);
|
||||
}
|
||||
|
||||
void insertObject(T* object, qsizetype index = -1) {
|
||||
this->UntypedObjectModel::insertObject(object, index);
|
||||
}
|
||||
|
||||
void removeObject(const T* object) { this->UntypedObjectModel::removeObject(object); }
|
||||
|
||||
// Assumes only one instance of a specific value
|
||||
void diffUpdate(const QVector<T*>& newValues) {
|
||||
this->UntypedObjectModel::diffUpdate(*std::bit_cast<const QVector<QObject*>*>(&newValues));
|
||||
}
|
||||
|
||||
static ObjectModel<T>* emptyInstance() {
|
||||
return static_cast<ObjectModel<T>*>(UntypedObjectModel::emptyInstance());
|
||||
}
|
||||
};
|
|
@ -7,27 +7,16 @@ headers = [
|
|||
"shell.hpp",
|
||||
"variants.hpp",
|
||||
"region.hpp",
|
||||
"../window/proxywindow.hpp",
|
||||
"proxywindow.hpp",
|
||||
"persistentprops.hpp",
|
||||
"../window/windowinterface.hpp",
|
||||
"../window/panelinterface.hpp",
|
||||
"../window/floatingwindow.hpp",
|
||||
"../window/popupwindow.hpp",
|
||||
"windowinterface.hpp",
|
||||
"panelinterface.hpp",
|
||||
"floatingwindow.hpp",
|
||||
"popupwindow.hpp",
|
||||
"singleton.hpp",
|
||||
"lazyloader.hpp",
|
||||
"easingcurve.hpp",
|
||||
"transformwatcher.hpp",
|
||||
"boundcomponent.hpp",
|
||||
"model.hpp",
|
||||
"elapsedtimer.hpp",
|
||||
"desktopentry.hpp",
|
||||
"objectrepeater.hpp",
|
||||
"qsmenu.hpp",
|
||||
"retainable.hpp",
|
||||
"popupanchor.hpp",
|
||||
"types.hpp",
|
||||
"qsmenuanchor.hpp",
|
||||
"clock.hpp",
|
||||
"scriptmodel.hpp",
|
||||
]
|
||||
-----
|
||||
|
|
|
@ -1,190 +0,0 @@
|
|||
#include "objectrepeater.hpp"
|
||||
#include <utility>
|
||||
|
||||
#include <qabstractitemmodel.h>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qhash.h>
|
||||
#include <qlogging.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlcomponent.h>
|
||||
#include <qqmlcontext.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qqmllist.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
QVariant ObjectRepeater::model() const { return this->mModel; }
|
||||
|
||||
void ObjectRepeater::setModel(QVariant model) {
|
||||
if (model == this->mModel) return;
|
||||
|
||||
if (this->itemModel != nullptr) {
|
||||
QObject::disconnect(this->itemModel, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
this->mModel = std::move(model);
|
||||
emit this->modelChanged();
|
||||
this->reloadElements();
|
||||
}
|
||||
|
||||
void ObjectRepeater::onModelDestroyed() {
|
||||
this->mModel.clear();
|
||||
this->itemModel = nullptr;
|
||||
emit this->modelChanged();
|
||||
this->reloadElements();
|
||||
}
|
||||
|
||||
QQmlComponent* ObjectRepeater::delegate() const { return this->mDelegate; }
|
||||
|
||||
void ObjectRepeater::setDelegate(QQmlComponent* delegate) {
|
||||
if (delegate == this->mDelegate) return;
|
||||
|
||||
if (this->mDelegate != nullptr) {
|
||||
QObject::disconnect(this->mDelegate, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
this->mDelegate = delegate;
|
||||
|
||||
if (delegate != nullptr) {
|
||||
QObject::connect(
|
||||
this->mDelegate,
|
||||
&QObject::destroyed,
|
||||
this,
|
||||
&ObjectRepeater::onDelegateDestroyed
|
||||
);
|
||||
}
|
||||
|
||||
emit this->delegateChanged();
|
||||
this->reloadElements();
|
||||
}
|
||||
|
||||
void ObjectRepeater::onDelegateDestroyed() {
|
||||
this->mDelegate = nullptr;
|
||||
emit this->delegateChanged();
|
||||
this->reloadElements();
|
||||
}
|
||||
|
||||
void ObjectRepeater::reloadElements() {
|
||||
for (auto i = this->valuesList.length() - 1; i >= 0; i--) {
|
||||
this->removeComponent(i);
|
||||
}
|
||||
|
||||
if (this->mDelegate == nullptr || !this->mModel.isValid()) return;
|
||||
|
||||
if (this->mModel.canConvert<QAbstractItemModel*>()) {
|
||||
auto* model = this->mModel.value<QAbstractItemModel*>();
|
||||
this->itemModel = model;
|
||||
|
||||
this->insertModelElements(model, 0, model->rowCount() - 1); // -1 is fine
|
||||
|
||||
// clang-format off
|
||||
QObject::connect(model, &QObject::destroyed, this, &ObjectRepeater::onModelDestroyed);
|
||||
QObject::connect(model, &QAbstractItemModel::rowsInserted, this, &ObjectRepeater::onModelRowsInserted);
|
||||
QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, &ObjectRepeater::onModelRowsRemoved);
|
||||
QObject::connect(model, &QAbstractItemModel::rowsMoved, this, &ObjectRepeater::onModelRowsMoved);
|
||||
QObject::connect(model, &QAbstractItemModel::modelAboutToBeReset, this, &ObjectRepeater::onModelAboutToBeReset);
|
||||
// clang-format on
|
||||
} else if (this->mModel.canConvert<QQmlListReference>()) {
|
||||
auto values = this->mModel.value<QQmlListReference>();
|
||||
auto len = values.count();
|
||||
|
||||
for (auto i = 0; i != len; i++) {
|
||||
this->insertComponent(i, {{"modelData", QVariant::fromValue(values.at(i))}});
|
||||
}
|
||||
} else if (this->mModel.canConvert<QVector<QVariant>>()) {
|
||||
auto values = this->mModel.value<QVector<QVariant>>();
|
||||
|
||||
for (auto& value: values) {
|
||||
this->insertComponent(this->valuesList.length(), {{"modelData", value}});
|
||||
}
|
||||
} else {
|
||||
qCritical() << this
|
||||
<< "Cannot create components as the model is not compatible:" << this->mModel;
|
||||
}
|
||||
}
|
||||
|
||||
void ObjectRepeater::insertModelElements(QAbstractItemModel* model, int first, int last) {
|
||||
auto roles = model->roleNames();
|
||||
auto roleDataVec = QVector<QModelRoleData>();
|
||||
for (auto id: roles.keys()) {
|
||||
roleDataVec.push_back(QModelRoleData(id));
|
||||
}
|
||||
|
||||
auto values = QModelRoleDataSpan(roleDataVec);
|
||||
auto props = QVariantMap();
|
||||
|
||||
for (auto i = first; i != last + 1; i++) {
|
||||
auto index = model->index(i, 0);
|
||||
model->multiData(index, values);
|
||||
|
||||
for (auto [id, name]: roles.asKeyValueRange()) {
|
||||
props.insert(name, *values.dataForRole(id));
|
||||
}
|
||||
|
||||
this->insertComponent(i, props);
|
||||
|
||||
props.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void ObjectRepeater::onModelRowsInserted(const QModelIndex& parent, int first, int last) {
|
||||
if (parent != QModelIndex()) return;
|
||||
|
||||
this->insertModelElements(this->itemModel, first, last);
|
||||
}
|
||||
|
||||
void ObjectRepeater::onModelRowsRemoved(const QModelIndex& parent, int first, int last) {
|
||||
if (parent != QModelIndex()) return;
|
||||
|
||||
for (auto i = last; i != first - 1; i--) {
|
||||
this->removeComponent(i);
|
||||
}
|
||||
}
|
||||
|
||||
void ObjectRepeater::onModelRowsMoved(
|
||||
const QModelIndex& sourceParent,
|
||||
int sourceStart,
|
||||
int sourceEnd,
|
||||
const QModelIndex& destParent,
|
||||
int destStart
|
||||
) {
|
||||
auto hasSource = sourceParent != QModelIndex();
|
||||
auto hasDest = destParent != QModelIndex();
|
||||
|
||||
if (!hasSource && !hasDest) return;
|
||||
|
||||
if (hasSource) {
|
||||
this->onModelRowsRemoved(sourceParent, sourceStart, sourceEnd);
|
||||
}
|
||||
|
||||
if (hasDest) {
|
||||
this->onModelRowsInserted(destParent, destStart, destStart + (sourceEnd - sourceStart));
|
||||
}
|
||||
}
|
||||
|
||||
void ObjectRepeater::onModelAboutToBeReset() {
|
||||
auto last = static_cast<int>(this->valuesList.length() - 1);
|
||||
this->onModelRowsRemoved(QModelIndex(), 0, last); // -1 is fine
|
||||
}
|
||||
|
||||
void ObjectRepeater::insertComponent(qsizetype index, const QVariantMap& properties) {
|
||||
auto* context = QQmlEngine::contextForObject(this);
|
||||
auto* instance = this->mDelegate->createWithInitialProperties(properties, context);
|
||||
|
||||
if (instance == nullptr) {
|
||||
qWarning().noquote() << this->mDelegate->errorString();
|
||||
qWarning() << this << "failed to create object for model data" << properties;
|
||||
} else {
|
||||
QQmlEngine::setObjectOwnership(instance, QQmlEngine::CppOwnership);
|
||||
instance->setParent(this);
|
||||
}
|
||||
|
||||
this->insertObject(instance, index);
|
||||
}
|
||||
|
||||
void ObjectRepeater::removeComponent(qsizetype index) {
|
||||
auto* instance = this->valuesList.at(index);
|
||||
this->removeAt(index);
|
||||
delete instance;
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <qabstractitemmodel.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlcomponent.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
#include "model.hpp"
|
||||
|
||||
///! A Repeater / for loop / map for non Item derived objects.
|
||||
/// > [!ERROR] Removed in favor of @@QtQml.Models.Instantiator
|
||||
///
|
||||
/// The ObjectRepeater creates instances of the provided delegate for every entry in the
|
||||
/// given model, similarly to a @@QtQuick.Repeater but for non visual types.
|
||||
class ObjectRepeater: public ObjectModel<QObject> {
|
||||
Q_OBJECT;
|
||||
/// The model providing data to the ObjectRepeater.
|
||||
///
|
||||
/// Currently accepted model types are `list<T>` lists, javascript arrays,
|
||||
/// and [QAbstractListModel] derived models, though only one column will be repeated
|
||||
/// from the latter.
|
||||
///
|
||||
/// Note: @@ObjectModel is a [QAbstractListModel] with a single column.
|
||||
///
|
||||
/// [QAbstractListModel]: https://doc.qt.io/qt-6/qabstractlistmodel.html
|
||||
Q_PROPERTY(QVariant model READ model WRITE setModel NOTIFY modelChanged);
|
||||
/// The delegate component to repeat.
|
||||
///
|
||||
/// The delegate is given the same properties as in a Repeater, except `index` which
|
||||
/// is not currently implemented.
|
||||
///
|
||||
/// If the model is a `list<T>` or javascript array, a `modelData` property will be
|
||||
/// exposed containing the entry from the model. If the model is a [QAbstractListModel],
|
||||
/// the roles from the model will be exposed.
|
||||
///
|
||||
/// Note: @@ObjectModel has a single role named `modelData` for compatibility with normal lists.
|
||||
///
|
||||
/// [QAbstractListModel]: https://doc.qt.io/qt-6/qabstractlistmodel.html
|
||||
Q_PROPERTY(QQmlComponent* delegate READ delegate WRITE setDelegate NOTIFY delegateChanged);
|
||||
Q_CLASSINFO("DefaultProperty", "delegate");
|
||||
QML_ELEMENT;
|
||||
QML_UNCREATABLE("ObjectRepeater has been removed in favor of QtQml.Models.Instantiator.");
|
||||
|
||||
public:
|
||||
explicit ObjectRepeater(QObject* parent = nullptr): ObjectModel(parent) {}
|
||||
|
||||
[[nodiscard]] QVariant model() const;
|
||||
void setModel(QVariant model);
|
||||
|
||||
[[nodiscard]] QQmlComponent* delegate() const;
|
||||
void setDelegate(QQmlComponent* delegate);
|
||||
|
||||
signals:
|
||||
void modelChanged();
|
||||
void delegateChanged();
|
||||
|
||||
private slots:
|
||||
void onDelegateDestroyed();
|
||||
void onModelDestroyed();
|
||||
void onModelRowsInserted(const QModelIndex& parent, int first, int last);
|
||||
void onModelRowsRemoved(const QModelIndex& parent, int first, int last);
|
||||
|
||||
void onModelRowsMoved(
|
||||
const QModelIndex& sourceParent,
|
||||
int sourceStart,
|
||||
int sourceEnd,
|
||||
const QModelIndex& destParent,
|
||||
int destStart
|
||||
);
|
||||
|
||||
void onModelAboutToBeReset();
|
||||
|
||||
private:
|
||||
void reloadElements();
|
||||
void insertModelElements(QAbstractItemModel* model, int first, int last);
|
||||
void insertComponent(qsizetype index, const QVariantMap& properties);
|
||||
void removeComponent(qsizetype index);
|
||||
|
||||
QVariant mModel;
|
||||
QAbstractItemModel* itemModel = nullptr;
|
||||
QQmlComponent* mDelegate = nullptr;
|
||||
};
|
|
@ -2,9 +2,8 @@
|
|||
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "../core/doc.hpp"
|
||||
#include "doc.hpp"
|
||||
#include "windowinterface.hpp"
|
||||
|
||||
class Anchors {
|
||||
|
@ -13,8 +12,7 @@ class Anchors {
|
|||
Q_PROPERTY(bool right MEMBER mRight);
|
||||
Q_PROPERTY(bool top MEMBER mTop);
|
||||
Q_PROPERTY(bool bottom MEMBER mBottom);
|
||||
QML_VALUE_TYPE(panelAnchors);
|
||||
QML_STRUCTURED_VALUE;
|
||||
QML_VALUE_TYPE(anchors);
|
||||
|
||||
public:
|
||||
[[nodiscard]] bool horizontalConstraint() const noexcept { return this->mLeft && this->mRight; }
|
||||
|
@ -41,8 +39,7 @@ class Margins {
|
|||
Q_PROPERTY(qint32 right MEMBER mRight);
|
||||
Q_PROPERTY(qint32 top MEMBER mTop);
|
||||
Q_PROPERTY(qint32 bottom MEMBER mBottom);
|
||||
QML_VALUE_TYPE(panelMargins);
|
||||
QML_STRUCTURED_VALUE;
|
||||
QML_VALUE_TYPE(margins);
|
||||
|
||||
public:
|
||||
[[nodiscard]] bool operator==(const Margins& other) const noexcept {
|
||||
|
@ -60,13 +57,11 @@ public:
|
|||
qint32 mBottom = 0;
|
||||
};
|
||||
|
||||
///! Panel exclusion mode
|
||||
/// See @@PanelWindow.exclusionMode.
|
||||
namespace ExclusionMode { // NOLINT
|
||||
Q_NAMESPACE;
|
||||
QML_ELEMENT;
|
||||
|
||||
enum Enum : quint8 {
|
||||
enum Enum {
|
||||
/// Respect the exclusion zone of other shell layers and optionally set one
|
||||
Normal = 0,
|
||||
/// Ignore exclusion zones of other shell layers. You cannot set an exclusion zone in this mode.
|
||||
|
@ -116,24 +111,14 @@ class PanelWindowInterface: public WindowInterface {
|
|||
/// > [!INFO] Only applies to edges with anchors
|
||||
Q_PROPERTY(Margins margins READ margins WRITE setMargins NOTIFY marginsChanged);
|
||||
/// The amount of space reserved for the shell layer relative to its anchors.
|
||||
/// Setting this property sets @@exclusionMode to `ExclusionMode.Normal`.
|
||||
/// Setting this property sets `exclusionMode` to `Normal`.
|
||||
///
|
||||
/// > [!INFO] Either 1 or 3 anchors are required for the zone to take effect.
|
||||
Q_PROPERTY(qint32 exclusiveZone READ exclusiveZone WRITE setExclusiveZone NOTIFY exclusiveZoneChanged);
|
||||
/// Defaults to `ExclusionMode.Auto`.
|
||||
Q_PROPERTY(ExclusionMode::Enum exclusionMode READ exclusionMode WRITE setExclusionMode NOTIFY exclusionModeChanged);
|
||||
/// If the panel should render above standard windows. Defaults to true.
|
||||
///
|
||||
/// Note: On Wayland this property corrosponds to @@Quickshell.Wayland.WlrLayershell.layer.
|
||||
Q_PROPERTY(bool aboveWindows READ aboveWindows WRITE setAboveWindows NOTIFY aboveWindowsChanged);
|
||||
/// If the panel should accept keyboard focus. Defaults to false.
|
||||
///
|
||||
/// Note: On Wayland this property corrosponds to @@Quickshell.Wayland.WlrLayershell.keyboardFocus.
|
||||
Q_PROPERTY(bool focusable READ focusable WRITE setFocusable NOTIFY focusableChanged);
|
||||
// clang-format on
|
||||
QML_NAMED_ELEMENT(PanelWindow);
|
||||
QML_UNCREATABLE("No PanelWindow backend loaded.");
|
||||
QSDOC_CREATABLE;
|
||||
QSDOC_NAMED_ELEMENT(PanelWindow);
|
||||
|
||||
public:
|
||||
explicit PanelWindowInterface(QObject* parent = nullptr): WindowInterface(parent) {}
|
||||
|
@ -150,17 +135,9 @@ public:
|
|||
[[nodiscard]] virtual ExclusionMode::Enum exclusionMode() const = 0;
|
||||
virtual void setExclusionMode(ExclusionMode::Enum exclusionMode) = 0;
|
||||
|
||||
[[nodiscard]] virtual bool aboveWindows() const = 0;
|
||||
virtual void setAboveWindows(bool aboveWindows) = 0;
|
||||
|
||||
[[nodiscard]] virtual bool focusable() const = 0;
|
||||
virtual void setFocusable(bool focusable) = 0;
|
||||
|
||||
signals:
|
||||
void anchorsChanged();
|
||||
void marginsChanged();
|
||||
void exclusiveZoneChanged();
|
||||
void exclusionModeChanged();
|
||||
void aboveWindowsChanged();
|
||||
void focusableChanged();
|
||||
};
|
|
@ -1,310 +0,0 @@
|
|||
#include "paths.hpp"
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <utility>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdatastream.h>
|
||||
#include <qdir.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qstandardpaths.h>
|
||||
#include <qtenvironmentvariables.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "instanceinfo.hpp"
|
||||
|
||||
namespace {
|
||||
Q_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg);
|
||||
}
|
||||
|
||||
QsPaths* QsPaths::instance() {
|
||||
static auto* instance = new QsPaths(); // NOLINT
|
||||
return instance;
|
||||
}
|
||||
|
||||
void QsPaths::init(QString shellId, QString pathId) {
|
||||
auto* instance = QsPaths::instance();
|
||||
instance->shellId = std::move(shellId);
|
||||
instance->pathId = std::move(pathId);
|
||||
}
|
||||
|
||||
QDir QsPaths::crashDir(const QString& id) {
|
||||
auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
|
||||
dir = QDir(dir.filePath("crashes"));
|
||||
dir = QDir(dir.filePath(id));
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
QString QsPaths::basePath(const QString& id) {
|
||||
auto path = QsPaths::instance()->baseRunDir()->filePath("by-id");
|
||||
path = QDir(path).filePath(id);
|
||||
return path;
|
||||
}
|
||||
|
||||
QString QsPaths::ipcPath(const QString& id) {
|
||||
return QDir(QsPaths::basePath(id)).filePath("ipc.sock");
|
||||
}
|
||||
|
||||
QDir* QsPaths::cacheDir() {
|
||||
if (this->cacheState == DirState::Unknown) {
|
||||
auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
|
||||
dir = QDir(dir.filePath(this->shellId));
|
||||
this->mCacheDir = dir;
|
||||
|
||||
qCDebug(logPaths) << "Initialized cache path:" << dir.path();
|
||||
|
||||
if (!dir.mkpath(".")) {
|
||||
qCCritical(logPaths) << "Could not create cache directory at" << dir.path();
|
||||
|
||||
this->cacheState = DirState::Failed;
|
||||
} else {
|
||||
this->cacheState = DirState::Ready;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->cacheState == DirState::Failed) return nullptr;
|
||||
else return &this->mCacheDir;
|
||||
}
|
||||
|
||||
QDir* QsPaths::baseRunDir() {
|
||||
if (this->baseRunState == DirState::Unknown) {
|
||||
auto runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR");
|
||||
if (runtimeDir.isEmpty()) {
|
||||
runtimeDir = QString("/run/user/$1").arg(getuid());
|
||||
qCInfo(logPaths) << "XDG_RUNTIME_DIR was not set, defaulting to" << runtimeDir;
|
||||
}
|
||||
|
||||
this->mBaseRunDir = QDir(runtimeDir);
|
||||
this->mBaseRunDir = QDir(this->mBaseRunDir.filePath("quickshell"));
|
||||
qCDebug(logPaths) << "Initialized base runtime path:" << this->mBaseRunDir.path();
|
||||
|
||||
if (!this->mBaseRunDir.mkpath(".")) {
|
||||
qCCritical(logPaths) << "Could not create base runtime directory at"
|
||||
<< this->mBaseRunDir.path();
|
||||
|
||||
this->baseRunState = DirState::Failed;
|
||||
} else {
|
||||
this->baseRunState = DirState::Ready;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->baseRunState == DirState::Failed) return nullptr;
|
||||
else return &this->mBaseRunDir;
|
||||
}
|
||||
|
||||
QDir* QsPaths::shellRunDir() {
|
||||
if (this->shellRunState == DirState::Unknown) {
|
||||
if (auto* baseRunDir = this->baseRunDir()) {
|
||||
this->mShellRunDir = QDir(baseRunDir->filePath("by-shell"));
|
||||
this->mShellRunDir = QDir(this->mShellRunDir.filePath(this->shellId));
|
||||
|
||||
qCDebug(logPaths) << "Initialized runtime path:" << this->mShellRunDir.path();
|
||||
|
||||
if (!this->mShellRunDir.mkpath(".")) {
|
||||
qCCritical(logPaths) << "Could not create runtime directory at"
|
||||
<< this->mShellRunDir.path();
|
||||
this->shellRunState = DirState::Failed;
|
||||
} else {
|
||||
this->shellRunState = DirState::Ready;
|
||||
}
|
||||
} else {
|
||||
qCCritical(logPaths) << "Could not create shell runtime path as it was not possible to "
|
||||
"create the base runtime path.";
|
||||
|
||||
this->shellRunState = DirState::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->shellRunState == DirState::Failed) return nullptr;
|
||||
else return &this->mShellRunDir;
|
||||
}
|
||||
|
||||
QDir* QsPaths::instanceRunDir() {
|
||||
if (this->instanceRunState == DirState::Unknown) {
|
||||
auto* runDir = this->baseRunDir();
|
||||
|
||||
if (!runDir) {
|
||||
qCCritical(logPaths) << "Cannot create instance runtime directory as main runtim directory "
|
||||
"could not be created.";
|
||||
this->instanceRunState = DirState::Failed;
|
||||
} else {
|
||||
auto byIdDir = QDir(runDir->filePath("by-id"));
|
||||
|
||||
this->mInstanceRunDir = byIdDir.filePath(InstanceInfo::CURRENT.instanceId);
|
||||
|
||||
qCDebug(logPaths) << "Initialized instance runtime path:" << this->mInstanceRunDir.path();
|
||||
|
||||
if (!this->mInstanceRunDir.mkpath(".")) {
|
||||
qCCritical(logPaths) << "Could not create instance runtime directory at"
|
||||
<< this->mInstanceRunDir.path();
|
||||
this->instanceRunState = DirState::Failed;
|
||||
} else {
|
||||
this->instanceRunState = DirState::Ready;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this->shellRunState == DirState::Failed) return nullptr;
|
||||
else return &this->mInstanceRunDir;
|
||||
}
|
||||
|
||||
void QsPaths::linkRunDir() {
|
||||
if (auto* runDir = this->instanceRunDir()) {
|
||||
auto pidDir = QDir(this->baseRunDir()->filePath("by-pid"));
|
||||
auto* shellDir = this->shellRunDir();
|
||||
|
||||
if (!shellDir) {
|
||||
qCCritical(logPaths
|
||||
) << "Could not create by-id symlink as the shell runtime path could not be created.";
|
||||
} else {
|
||||
auto shellPath = shellDir->filePath(runDir->dirName());
|
||||
|
||||
QFile::remove(shellPath);
|
||||
auto r =
|
||||
symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, shellPath.toStdString().c_str());
|
||||
|
||||
if (r != 0) {
|
||||
qCCritical(logPaths).nospace()
|
||||
<< "Could not create id symlink to " << runDir->path() << " at " << shellPath
|
||||
<< " with error code " << errno << ": " << qt_error_string();
|
||||
} else {
|
||||
qCDebug(logPaths) << "Created shellid symlink" << shellPath << "to instance runtime path"
|
||||
<< runDir->path();
|
||||
}
|
||||
}
|
||||
|
||||
if (!pidDir.mkpath(".")) {
|
||||
qCCritical(logPaths) << "Could not create PID symlink directory.";
|
||||
} else {
|
||||
auto pidPath = pidDir.filePath(QString::number(getpid()));
|
||||
|
||||
QFile::remove(pidPath);
|
||||
auto r =
|
||||
symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, pidPath.toStdString().c_str());
|
||||
|
||||
if (r != 0) {
|
||||
qCCritical(logPaths).nospace()
|
||||
<< "Could not create PID symlink to " << runDir->path() << " at " << pidPath
|
||||
<< " with error code " << errno << ": " << qt_error_string();
|
||||
} else {
|
||||
qCDebug(logPaths) << "Created PID symlink" << pidPath << "to instance runtime path"
|
||||
<< runDir->path();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
qCCritical(logPaths) << "Could not create PID symlink to runtime directory, as the runtime "
|
||||
"directory could not be created.";
|
||||
}
|
||||
}
|
||||
|
||||
void QsPaths::linkPathDir() {
|
||||
if (auto* runDir = this->shellRunDir()) {
|
||||
auto pathDir = QDir(this->baseRunDir()->filePath("by-path"));
|
||||
|
||||
if (!pathDir.mkpath(".")) {
|
||||
qCCritical(logPaths) << "Could not create path symlink directory.";
|
||||
return;
|
||||
}
|
||||
|
||||
auto linkPath = pathDir.filePath(this->pathId);
|
||||
|
||||
QFile::remove(linkPath);
|
||||
auto r =
|
||||
symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, linkPath.toStdString().c_str());
|
||||
|
||||
if (r != 0) {
|
||||
qCCritical(logPaths).nospace()
|
||||
<< "Could not create path symlink to " << runDir->path() << " at " << linkPath
|
||||
<< " with error code " << errno << ": " << qt_error_string();
|
||||
} else {
|
||||
qCDebug(logPaths) << "Created path symlink" << linkPath << "to shell runtime path"
|
||||
<< runDir->path();
|
||||
}
|
||||
} else {
|
||||
qCCritical(logPaths) << "Could not create path symlink to shell runtime directory, as the "
|
||||
"shell runtime directory could not be created.";
|
||||
}
|
||||
}
|
||||
|
||||
void QsPaths::createLock() {
|
||||
if (auto* runDir = this->instanceRunDir()) {
|
||||
auto path = runDir->filePath("instance.lock");
|
||||
auto* file = new QFile(path); // leaked
|
||||
|
||||
if (!file->open(QFile::ReadWrite | QFile::Truncate)) {
|
||||
qCCritical(logPaths) << "Could not create instance lock at" << path;
|
||||
return;
|
||||
}
|
||||
|
||||
auto lock = flock {
|
||||
.l_type = F_WRLCK,
|
||||
.l_whence = SEEK_SET,
|
||||
.l_start = 0,
|
||||
.l_len = 0,
|
||||
.l_pid = 0,
|
||||
};
|
||||
|
||||
if (fcntl(file->handle(), F_SETLK, &lock) != 0) { // NOLINT
|
||||
qCCritical(logPaths).nospace() << "Could not lock instance lock at " << path
|
||||
<< " with error code " << errno << ": " << qt_error_string();
|
||||
} else {
|
||||
auto stream = QDataStream(file);
|
||||
stream << InstanceInfo::CURRENT;
|
||||
file->flush();
|
||||
qCDebug(logPaths) << "Created instance lock at" << path;
|
||||
}
|
||||
} else {
|
||||
qCCritical(logPaths
|
||||
) << "Could not create instance lock, as the instance runtime directory could not be created.";
|
||||
}
|
||||
}
|
||||
|
||||
bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info) {
|
||||
auto file = QFile(QDir(path).filePath("instance.lock"));
|
||||
if (!file.open(QFile::ReadOnly)) return false;
|
||||
|
||||
auto lock = flock {
|
||||
.l_type = F_WRLCK,
|
||||
.l_whence = SEEK_SET,
|
||||
.l_start = 0,
|
||||
.l_len = 0,
|
||||
.l_pid = 0,
|
||||
};
|
||||
|
||||
fcntl(file.handle(), F_GETLK, &lock); // NOLINT
|
||||
if (lock.l_type == F_UNLCK) return false;
|
||||
|
||||
if (info) {
|
||||
info->pid = lock.l_pid;
|
||||
|
||||
auto stream = QDataStream(&file);
|
||||
stream >> info->instance;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<InstanceLockInfo> QsPaths::collectInstances(const QString& path) {
|
||||
qCDebug(logPaths) << "Collecting instances from" << path;
|
||||
auto instances = QVector<InstanceLockInfo>();
|
||||
auto dir = QDir(path);
|
||||
|
||||
InstanceLockInfo info;
|
||||
for (auto& entry: dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
|
||||
auto path = dir.filePath(entry);
|
||||
|
||||
if (QsPaths::checkLock(path, &info)) {
|
||||
qCDebug(logPaths).nospace() << "Found live instance " << info.instance.instanceId << " (pid "
|
||||
<< info.pid << ") at " << path;
|
||||
|
||||
instances.push_back(info);
|
||||
} else {
|
||||
qCDebug(logPaths) << "Skipped dead instance at" << path;
|
||||
}
|
||||
}
|
||||
|
||||
return instances;
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
#pragma once
|
||||
#include <qdatetime.h>
|
||||
#include <qdir.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "instanceinfo.hpp"
|
||||
|
||||
struct InstanceLockInfo {
|
||||
pid_t pid = -1;
|
||||
InstanceInfo instance;
|
||||
};
|
||||
|
||||
QDataStream& operator<<(QDataStream& stream, const InstanceLockInfo& info);
|
||||
QDataStream& operator>>(QDataStream& stream, InstanceLockInfo& info);
|
||||
|
||||
class QsPaths {
|
||||
public:
|
||||
static QsPaths* instance();
|
||||
static void init(QString shellId, QString pathId);
|
||||
static QDir crashDir(const QString& id);
|
||||
static QString basePath(const QString& id);
|
||||
static QString ipcPath(const QString& id);
|
||||
static bool checkLock(const QString& path, InstanceLockInfo* info = nullptr);
|
||||
static QVector<InstanceLockInfo> collectInstances(const QString& path);
|
||||
|
||||
QDir* cacheDir();
|
||||
QDir* baseRunDir();
|
||||
QDir* shellRunDir();
|
||||
QDir* instanceRunDir();
|
||||
void linkRunDir();
|
||||
void linkPathDir();
|
||||
void createLock();
|
||||
|
||||
private:
|
||||
enum class DirState : quint8 {
|
||||
Unknown = 0,
|
||||
Ready = 1,
|
||||
Failed = 2,
|
||||
};
|
||||
|
||||
QString shellId;
|
||||
QString pathId;
|
||||
QDir mCacheDir;
|
||||
QDir mBaseRunDir;
|
||||
QDir mShellRunDir;
|
||||
QDir mInstanceRunDir;
|
||||
DirState cacheState = DirState::Unknown;
|
||||
DirState baseRunState = DirState::Unknown;
|
||||
DirState shellRunState = DirState::Unknown;
|
||||
DirState instanceRunState = DirState::Unknown;
|
||||
};
|
|
@ -1,324 +0,0 @@
|
|||
#include "platformmenu.hpp"
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
|
||||
#include <qaction.h>
|
||||
#include <qactiongroup.h>
|
||||
#include <qapplication.h>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qcoreapplication.h>
|
||||
#include <qicon.h>
|
||||
#include <qlogging.h>
|
||||
#include <qmenu.h>
|
||||
#include <qnamespace.h>
|
||||
#include <qobject.h>
|
||||
#include <qpoint.h>
|
||||
#include <qqmllist.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qwindow.h>
|
||||
|
||||
#include "../window/proxywindow.hpp"
|
||||
#include "../window/windowinterface.hpp"
|
||||
#include "iconprovider.hpp"
|
||||
#include "model.hpp"
|
||||
#include "platformmenu_p.hpp"
|
||||
#include "popupanchor.hpp"
|
||||
#include "qsmenu.hpp"
|
||||
|
||||
namespace qs::menu::platform {
|
||||
|
||||
namespace {
|
||||
QVector<std::function<void(PlatformMenuQMenu*)>> CREATION_HOOKS; // NOLINT
|
||||
PlatformMenuQMenu* ACTIVE_MENU = nullptr; // NOLINT
|
||||
} // namespace
|
||||
|
||||
PlatformMenuQMenu::~PlatformMenuQMenu() {
|
||||
if (this == ACTIVE_MENU) {
|
||||
ACTIVE_MENU = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void PlatformMenuQMenu::setVisible(bool visible) {
|
||||
if (visible) {
|
||||
for (auto& hook: CREATION_HOOKS) {
|
||||
hook(this);
|
||||
}
|
||||
} else {
|
||||
if (this == ACTIVE_MENU) {
|
||||
ACTIVE_MENU = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
this->QMenu::setVisible(visible);
|
||||
}
|
||||
|
||||
PlatformMenuEntry::PlatformMenuEntry(QsMenuEntry* menu): QObject(menu), menu(menu) {
|
||||
this->relayout();
|
||||
|
||||
// clang-format off
|
||||
QObject::connect(menu, &QsMenuEntry::enabledChanged, this, &PlatformMenuEntry::onEnabledChanged);
|
||||
QObject::connect(menu, &QsMenuEntry::textChanged, this, &PlatformMenuEntry::onTextChanged);
|
||||
QObject::connect(menu, &QsMenuEntry::iconChanged, this, &PlatformMenuEntry::onIconChanged);
|
||||
QObject::connect(menu, &QsMenuEntry::buttonTypeChanged, this, &PlatformMenuEntry::onButtonTypeChanged);
|
||||
QObject::connect(menu, &QsMenuEntry::checkStateChanged, this, &PlatformMenuEntry::onCheckStateChanged);
|
||||
QObject::connect(menu, &QsMenuEntry::hasChildrenChanged, this, &PlatformMenuEntry::relayoutParent);
|
||||
QObject::connect(menu->children(), &UntypedObjectModel::valuesChanged, this, &PlatformMenuEntry::relayout);
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
PlatformMenuEntry::~PlatformMenuEntry() {
|
||||
this->clearChildren();
|
||||
delete this->qaction;
|
||||
delete this->qmenu;
|
||||
}
|
||||
|
||||
void PlatformMenuEntry::registerCreationHook(std::function<void(PlatformMenuQMenu*)> hook) {
|
||||
CREATION_HOOKS.push_back(std::move(hook));
|
||||
}
|
||||
|
||||
bool PlatformMenuEntry::display(QObject* parentWindow, int relativeX, int relativeY) {
|
||||
QWindow* window = nullptr;
|
||||
|
||||
if (qobject_cast<QApplication*>(QCoreApplication::instance()) == nullptr) {
|
||||
qCritical() << "Cannot display PlatformMenuEntry as quickshell was not started in "
|
||||
"QApplication mode.";
|
||||
qCritical() << "To use platform menus, add `//@ pragma UseQApplication` to the top of your "
|
||||
"root QML file and restart quickshell.";
|
||||
return false;
|
||||
} else if (this->qmenu == nullptr) {
|
||||
qCritical() << "Cannot display PlatformMenuEntry as it is not a menu.";
|
||||
return false;
|
||||
} else if (parentWindow == nullptr) {
|
||||
qCritical() << "Cannot display PlatformMenuEntry with null parent window.";
|
||||
return false;
|
||||
} else if (auto* proxy = qobject_cast<ProxyWindowBase*>(parentWindow)) {
|
||||
window = proxy->backingWindow();
|
||||
} else if (auto* interface = qobject_cast<WindowInterface*>(parentWindow)) {
|
||||
window = interface->proxyWindow()->backingWindow();
|
||||
} else {
|
||||
qCritical() << "PlatformMenuEntry.display() must be called with a window.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (window == nullptr) {
|
||||
qCritical() << "Cannot display PlatformMenuEntry from a parent window that is not visible.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ACTIVE_MENU && this->qmenu != ACTIVE_MENU) {
|
||||
ACTIVE_MENU->close();
|
||||
}
|
||||
|
||||
ACTIVE_MENU = this->qmenu;
|
||||
|
||||
auto point = window->mapToGlobal(QPoint(relativeX, relativeY));
|
||||
|
||||
this->qmenu->createWinId();
|
||||
this->qmenu->windowHandle()->setTransientParent(window);
|
||||
|
||||
// Skips screen edge repositioning so it can be left to the compositor on wayland.
|
||||
this->qmenu->targetPosition = point;
|
||||
this->qmenu->popup(point);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlatformMenuEntry::display(PopupAnchor* anchor) {
|
||||
if (qobject_cast<QApplication*>(QCoreApplication::instance()) == nullptr) {
|
||||
qCritical() << "Cannot display PlatformMenuEntry as quickshell was not started in "
|
||||
"QApplication mode.";
|
||||
qCritical() << "To use platform menus, add `//@ pragma UseQApplication` to the top of your "
|
||||
"root QML file and restart quickshell.";
|
||||
return false;
|
||||
} else if (!anchor->backingWindow() || !anchor->backingWindow()->isVisible()) {
|
||||
qCritical() << "Cannot display PlatformMenuEntry on anchor without visible window.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ACTIVE_MENU && this->qmenu != ACTIVE_MENU) {
|
||||
ACTIVE_MENU->close();
|
||||
}
|
||||
|
||||
ACTIVE_MENU = this->qmenu;
|
||||
|
||||
this->qmenu->createWinId();
|
||||
this->qmenu->windowHandle()->setTransientParent(anchor->backingWindow());
|
||||
|
||||
// Update the window geometry to the menu's actual dimensions so reposition
|
||||
// can accurately adjust it if applicable for the current platform.
|
||||
this->qmenu->windowHandle()->setGeometry({{0, 0}, this->qmenu->sizeHint()});
|
||||
|
||||
PopupPositioner::instance()->reposition(anchor, this->qmenu->windowHandle(), false);
|
||||
|
||||
// Open the menu at the position determined by the popup positioner.
|
||||
this->qmenu->popup(this->qmenu->windowHandle()->position());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlatformMenuEntry::relayout() {
|
||||
if (qobject_cast<QApplication*>(QCoreApplication::instance()) == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->menu->hasChildren()) {
|
||||
delete this->qaction;
|
||||
this->qaction = nullptr;
|
||||
|
||||
if (this->qmenu == nullptr) {
|
||||
this->qmenu = new PlatformMenuQMenu();
|
||||
QObject::connect(this->qmenu, &QMenu::aboutToShow, this, &PlatformMenuEntry::onAboutToShow);
|
||||
QObject::connect(this->qmenu, &QMenu::aboutToHide, this, &PlatformMenuEntry::onAboutToHide);
|
||||
} else {
|
||||
this->clearChildren();
|
||||
}
|
||||
|
||||
this->qmenu->setTitle(this->menu->text());
|
||||
|
||||
auto icon = this->menu->icon();
|
||||
if (!icon.isEmpty()) {
|
||||
this->qmenu->setIcon(getCurrentEngineImageAsIcon(icon));
|
||||
}
|
||||
|
||||
const auto& children = this->menu->children()->valueList();
|
||||
auto len = children.count();
|
||||
for (auto i = 0; i < len; i++) {
|
||||
auto* child = children.at(i);
|
||||
|
||||
auto* instance = new PlatformMenuEntry(child);
|
||||
QObject::connect(instance, &QObject::destroyed, this, &PlatformMenuEntry::onChildDestroyed);
|
||||
|
||||
QObject::connect(
|
||||
instance,
|
||||
&PlatformMenuEntry::relayoutParent,
|
||||
this,
|
||||
&PlatformMenuEntry::relayout
|
||||
);
|
||||
|
||||
this->childEntries.push_back(instance);
|
||||
instance->addToQMenu(this->qmenu);
|
||||
}
|
||||
} else if (!this->menu->isSeparator()) {
|
||||
this->clearChildren();
|
||||
delete this->qmenu;
|
||||
this->qmenu = nullptr;
|
||||
|
||||
if (this->qaction == nullptr) {
|
||||
this->qaction = new QAction(this);
|
||||
|
||||
QObject::connect(
|
||||
this->qaction,
|
||||
&QAction::triggered,
|
||||
this,
|
||||
&PlatformMenuEntry::onActionTriggered
|
||||
);
|
||||
}
|
||||
|
||||
this->qaction->setText(this->menu->text());
|
||||
|
||||
auto icon = this->menu->icon();
|
||||
if (!icon.isEmpty()) {
|
||||
this->qaction->setIcon(getCurrentEngineImageAsIcon(icon));
|
||||
}
|
||||
|
||||
this->qaction->setEnabled(this->menu->enabled());
|
||||
this->qaction->setCheckable(this->menu->buttonType() != QsMenuButtonType::None);
|
||||
|
||||
if (this->menu->buttonType() == QsMenuButtonType::RadioButton) {
|
||||
if (!this->qactiongroup) this->qactiongroup = new QActionGroup(this);
|
||||
this->qaction->setActionGroup(this->qactiongroup);
|
||||
}
|
||||
|
||||
this->qaction->setChecked(this->menu->checkState() != Qt::Unchecked);
|
||||
} else {
|
||||
delete this->qmenu;
|
||||
delete this->qaction;
|
||||
this->qmenu = nullptr;
|
||||
this->qaction = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void PlatformMenuEntry::onAboutToShow() { this->menu->ref(); }
|
||||
|
||||
void PlatformMenuEntry::onAboutToHide() {
|
||||
this->menu->unref();
|
||||
emit this->closed();
|
||||
}
|
||||
|
||||
void PlatformMenuEntry::onActionTriggered() {
|
||||
auto* action = qobject_cast<PlatformMenuEntry*>(this->sender()->parent());
|
||||
emit action->menu->triggered();
|
||||
}
|
||||
|
||||
void PlatformMenuEntry::onChildDestroyed() { this->childEntries.removeOne(this->sender()); }
|
||||
|
||||
void PlatformMenuEntry::onEnabledChanged() {
|
||||
if (this->qaction != nullptr) {
|
||||
this->qaction->setEnabled(this->menu->enabled());
|
||||
}
|
||||
}
|
||||
|
||||
void PlatformMenuEntry::onTextChanged() {
|
||||
if (this->qmenu != nullptr) {
|
||||
this->qmenu->setTitle(this->menu->text());
|
||||
} else if (this->qaction != nullptr) {
|
||||
this->qaction->setText(this->menu->text());
|
||||
}
|
||||
}
|
||||
|
||||
void PlatformMenuEntry::onIconChanged() {
|
||||
if (this->qmenu == nullptr && this->qaction == nullptr) return;
|
||||
|
||||
auto iconName = this->menu->icon();
|
||||
QIcon icon;
|
||||
|
||||
if (!iconName.isEmpty()) {
|
||||
icon = getCurrentEngineImageAsIcon(iconName);
|
||||
}
|
||||
|
||||
if (this->qmenu != nullptr) {
|
||||
this->qmenu->setIcon(icon);
|
||||
} else if (this->qaction != nullptr) {
|
||||
this->qaction->setIcon(icon);
|
||||
}
|
||||
}
|
||||
|
||||
void PlatformMenuEntry::onButtonTypeChanged() {
|
||||
if (this->qaction != nullptr) {
|
||||
QActionGroup* group = nullptr;
|
||||
|
||||
if (this->menu->buttonType() == QsMenuButtonType::RadioButton) {
|
||||
if (!this->qactiongroup) this->qactiongroup = new QActionGroup(this);
|
||||
group = this->qactiongroup;
|
||||
}
|
||||
|
||||
this->qaction->setActionGroup(group);
|
||||
}
|
||||
}
|
||||
|
||||
void PlatformMenuEntry::onCheckStateChanged() {
|
||||
if (this->qaction != nullptr) {
|
||||
this->qaction->setChecked(this->menu->checkState() != Qt::Unchecked);
|
||||
}
|
||||
}
|
||||
|
||||
void PlatformMenuEntry::clearChildren() {
|
||||
for (auto* child: this->childEntries) {
|
||||
delete child;
|
||||
}
|
||||
|
||||
this->childEntries.clear();
|
||||
}
|
||||
|
||||
void PlatformMenuEntry::addToQMenu(PlatformMenuQMenu* menu) {
|
||||
if (this->qmenu != nullptr) {
|
||||
menu->addMenu(this->qmenu);
|
||||
this->qmenu->containingMenu = menu;
|
||||
} else if (this->qaction != nullptr) {
|
||||
menu->addAction(this->qaction);
|
||||
} else {
|
||||
menu->addSeparator();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace qs::menu::platform
|
|
@ -1,63 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <qaction.h>
|
||||
#include <qactiongroup.h>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qqmllist.h>
|
||||
#include <qtclasshelpermacros.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "../core/popupanchor.hpp"
|
||||
#include "qsmenu.hpp"
|
||||
|
||||
namespace qs::menu::platform {
|
||||
|
||||
class PlatformMenuQMenu;
|
||||
|
||||
class PlatformMenuEntry: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
explicit PlatformMenuEntry(QsMenuEntry* menu);
|
||||
~PlatformMenuEntry() override;
|
||||
Q_DISABLE_COPY_MOVE(PlatformMenuEntry);
|
||||
|
||||
bool display(QObject* parentWindow, int relativeX, int relativeY);
|
||||
bool display(PopupAnchor* anchor);
|
||||
|
||||
static void registerCreationHook(std::function<void(PlatformMenuQMenu*)> hook);
|
||||
|
||||
signals:
|
||||
void closed();
|
||||
void relayoutParent();
|
||||
|
||||
public slots:
|
||||
void relayout();
|
||||
|
||||
private slots:
|
||||
void onAboutToShow();
|
||||
void onAboutToHide();
|
||||
void onActionTriggered();
|
||||
void onChildDestroyed();
|
||||
void onEnabledChanged();
|
||||
void onTextChanged();
|
||||
void onIconChanged();
|
||||
void onButtonTypeChanged();
|
||||
void onCheckStateChanged();
|
||||
|
||||
private:
|
||||
void clearChildren();
|
||||
void addToQMenu(PlatformMenuQMenu* menu);
|
||||
|
||||
QsMenuEntry* menu;
|
||||
PlatformMenuQMenu* qmenu = nullptr;
|
||||
QAction* qaction = nullptr;
|
||||
QActionGroup* qactiongroup = nullptr;
|
||||
QVector<PlatformMenuEntry*> childEntries;
|
||||
};
|
||||
|
||||
} // namespace qs::menu::platform
|
|
@ -1,19 +0,0 @@
|
|||
#pragma once
|
||||
#include <qmenu.h>
|
||||
#include <qpoint.h>
|
||||
|
||||
namespace qs::menu::platform {
|
||||
|
||||
class PlatformMenuQMenu: public QMenu {
|
||||
public:
|
||||
explicit PlatformMenuQMenu() = default;
|
||||
~PlatformMenuQMenu() override;
|
||||
Q_DISABLE_COPY_MOVE(PlatformMenuQMenu);
|
||||
|
||||
void setVisible(bool visible) override;
|
||||
|
||||
PlatformMenuQMenu* containingMenu = nullptr;
|
||||
QPoint targetPosition;
|
||||
};
|
||||
|
||||
} // namespace qs::menu::platform
|
|
@ -5,34 +5,37 @@
|
|||
|
||||
#include "generation.hpp"
|
||||
|
||||
static QVector<QsEnginePlugin*> plugins; // NOLINT
|
||||
static QVector<QuickshellPlugin*> plugins; // NOLINT
|
||||
|
||||
void QsEnginePlugin::registerPlugin(QsEnginePlugin& plugin) { plugins.push_back(&plugin); }
|
||||
void QuickshellPlugin::registerPlugin(QuickshellPlugin& plugin) { plugins.push_back(&plugin); }
|
||||
|
||||
void QsEnginePlugin::initPlugins() {
|
||||
plugins.removeIf([](QsEnginePlugin* plugin) { return !plugin->applies(); });
|
||||
void QuickshellPlugin::initPlugins() {
|
||||
plugins.erase(
|
||||
std::remove_if(
|
||||
plugins.begin(),
|
||||
plugins.end(),
|
||||
[](QuickshellPlugin* plugin) { return !plugin->applies(); }
|
||||
),
|
||||
plugins.end()
|
||||
);
|
||||
|
||||
std::ranges::sort(plugins, [](QsEnginePlugin* a, QsEnginePlugin* b) {
|
||||
return b->dependencies().contains(a->name());
|
||||
});
|
||||
|
||||
for (QsEnginePlugin* plugin: plugins) {
|
||||
for (QuickshellPlugin* plugin: plugins) {
|
||||
plugin->init();
|
||||
}
|
||||
|
||||
for (QsEnginePlugin* plugin: plugins) {
|
||||
for (QuickshellPlugin* plugin: plugins) {
|
||||
plugin->registerTypes();
|
||||
}
|
||||
}
|
||||
|
||||
void QsEnginePlugin::runConstructGeneration(EngineGeneration& generation) {
|
||||
for (QsEnginePlugin* plugin: plugins) {
|
||||
void QuickshellPlugin::runConstructGeneration(EngineGeneration& generation) {
|
||||
for (QuickshellPlugin* plugin: plugins) {
|
||||
plugin->constructGeneration(generation);
|
||||
}
|
||||
}
|
||||
|
||||
void QsEnginePlugin::runOnReload() {
|
||||
for (QsEnginePlugin* plugin: plugins) {
|
||||
void QuickshellPlugin::runOnReload() {
|
||||
for (QuickshellPlugin* plugin: plugins) {
|
||||
plugin->onReload();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,28 +2,25 @@
|
|||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qfunctionpointer.h>
|
||||
#include <qlist.h>
|
||||
|
||||
class EngineGeneration;
|
||||
|
||||
class QsEnginePlugin {
|
||||
class QuickshellPlugin {
|
||||
public:
|
||||
QsEnginePlugin() = default;
|
||||
virtual ~QsEnginePlugin() = default;
|
||||
QsEnginePlugin(QsEnginePlugin&&) = delete;
|
||||
QsEnginePlugin(const QsEnginePlugin&) = delete;
|
||||
void operator=(QsEnginePlugin&&) = delete;
|
||||
void operator=(const QsEnginePlugin&) = delete;
|
||||
QuickshellPlugin() = default;
|
||||
virtual ~QuickshellPlugin() = default;
|
||||
QuickshellPlugin(QuickshellPlugin&&) = delete;
|
||||
QuickshellPlugin(const QuickshellPlugin&) = delete;
|
||||
void operator=(QuickshellPlugin&&) = delete;
|
||||
void operator=(const QuickshellPlugin&) = delete;
|
||||
|
||||
virtual QString name() { return QString(); }
|
||||
virtual QList<QString> dependencies() { return {}; }
|
||||
virtual bool applies() { return true; }
|
||||
virtual void init() {}
|
||||
virtual void registerTypes() {}
|
||||
virtual void constructGeneration(EngineGeneration& /*unused*/) {} // NOLINT
|
||||
virtual void onReload() {}
|
||||
|
||||
static void registerPlugin(QsEnginePlugin& plugin);
|
||||
static void registerPlugin(QuickshellPlugin& plugin);
|
||||
static void initPlugins();
|
||||
static void runConstructGeneration(EngineGeneration& generation);
|
||||
static void runOnReload();
|
||||
|
@ -33,6 +30,6 @@ public:
|
|||
#define QS_REGISTER_PLUGIN(clazz) \
|
||||
[[gnu::constructor]] void qsInitPlugin() { \
|
||||
static clazz plugin; \
|
||||
QsEnginePlugin::registerPlugin(plugin); \
|
||||
QuickshellPlugin::registerPlugin(plugin); \
|
||||
}
|
||||
// NOLINTEND
|
||||
|
|
|
@ -1,319 +0,0 @@
|
|||
#include "popupanchor.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qlogging.h>
|
||||
#include <qobject.h>
|
||||
#include <qsize.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qwindow.h>
|
||||
|
||||
#include "../window/proxywindow.hpp"
|
||||
#include "../window/windowinterface.hpp"
|
||||
#include "types.hpp"
|
||||
|
||||
bool PopupAnchorState::operator==(const PopupAnchorState& other) const {
|
||||
return this->rect == other.rect && this->edges == other.edges && this->gravity == other.gravity
|
||||
&& this->adjustment == other.adjustment && this->anchorpoint == other.anchorpoint
|
||||
&& this->size == other.size;
|
||||
}
|
||||
|
||||
bool PopupAnchor::isDirty() const {
|
||||
return !this->lastState.has_value() || this->state != this->lastState.value();
|
||||
}
|
||||
|
||||
void PopupAnchor::markClean() { this->lastState = this->state; }
|
||||
void PopupAnchor::markDirty() { this->lastState.reset(); }
|
||||
|
||||
QObject* PopupAnchor::window() const { return this->mWindow; }
|
||||
ProxyWindowBase* PopupAnchor::proxyWindow() const { return this->mProxyWindow; }
|
||||
|
||||
QWindow* PopupAnchor::backingWindow() const {
|
||||
return this->mProxyWindow ? this->mProxyWindow->backingWindow() : nullptr;
|
||||
}
|
||||
|
||||
void PopupAnchor::setWindow(QObject* window) {
|
||||
if (window == this->mWindow) return;
|
||||
|
||||
if (this->mWindow) {
|
||||
QObject::disconnect(this->mWindow, nullptr, this, nullptr);
|
||||
QObject::disconnect(this->mProxyWindow, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
if (window) {
|
||||
if (auto* proxy = qobject_cast<ProxyWindowBase*>(window)) {
|
||||
this->mProxyWindow = proxy;
|
||||
} else if (auto* interface = qobject_cast<WindowInterface*>(window)) {
|
||||
this->mProxyWindow = interface->proxyWindow();
|
||||
} else {
|
||||
qWarning() << "Tried to set popup anchor window to" << window
|
||||
<< "which is not a quickshell window.";
|
||||
goto setnull;
|
||||
}
|
||||
|
||||
this->mWindow = window;
|
||||
|
||||
QObject::connect(this->mWindow, &QObject::destroyed, this, &PopupAnchor::onWindowDestroyed);
|
||||
|
||||
QObject::connect(
|
||||
this->mProxyWindow,
|
||||
&ProxyWindowBase::backerVisibilityChanged,
|
||||
this,
|
||||
&PopupAnchor::backingWindowVisibilityChanged
|
||||
);
|
||||
|
||||
emit this->windowChanged();
|
||||
emit this->backingWindowVisibilityChanged();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setnull:
|
||||
if (this->mWindow) {
|
||||
this->mWindow = nullptr;
|
||||
this->mProxyWindow = nullptr;
|
||||
|
||||
emit this->windowChanged();
|
||||
emit this->backingWindowVisibilityChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void PopupAnchor::onWindowDestroyed() {
|
||||
this->mWindow = nullptr;
|
||||
this->mProxyWindow = nullptr;
|
||||
emit this->windowChanged();
|
||||
emit this->backingWindowVisibilityChanged();
|
||||
}
|
||||
|
||||
Box PopupAnchor::rect() const { return this->state.rect; }
|
||||
|
||||
void PopupAnchor::setRect(Box rect) {
|
||||
if (rect == this->state.rect) return;
|
||||
if (rect.w <= 0) rect.w = 1;
|
||||
if (rect.h <= 0) rect.h = 1;
|
||||
|
||||
this->state.rect = rect;
|
||||
emit this->rectChanged();
|
||||
}
|
||||
|
||||
Edges::Flags PopupAnchor::edges() const { return this->state.edges; }
|
||||
|
||||
void PopupAnchor::setEdges(Edges::Flags edges) {
|
||||
if (edges == this->state.edges) return;
|
||||
|
||||
if (Edges::isOpposing(edges)) {
|
||||
qWarning() << "Cannot set opposing edges for anchor edges. Tried to set" << edges;
|
||||
return;
|
||||
}
|
||||
|
||||
this->state.edges = edges;
|
||||
emit this->edgesChanged();
|
||||
}
|
||||
|
||||
Edges::Flags PopupAnchor::gravity() const { return this->state.gravity; }
|
||||
|
||||
void PopupAnchor::setGravity(Edges::Flags gravity) {
|
||||
if (gravity == this->state.gravity) return;
|
||||
|
||||
if (Edges::isOpposing(gravity)) {
|
||||
qWarning() << "Cannot set opposing edges for anchor gravity. Tried to set" << gravity;
|
||||
return;
|
||||
}
|
||||
|
||||
this->state.gravity = gravity;
|
||||
emit this->gravityChanged();
|
||||
}
|
||||
|
||||
PopupAdjustment::Flags PopupAnchor::adjustment() const { return this->state.adjustment; }
|
||||
|
||||
void PopupAnchor::setAdjustment(PopupAdjustment::Flags adjustment) {
|
||||
if (adjustment == this->state.adjustment) return;
|
||||
this->state.adjustment = adjustment;
|
||||
emit this->adjustmentChanged();
|
||||
}
|
||||
|
||||
void PopupAnchor::updatePlacement(const QPoint& anchorpoint, const QSize& size) {
|
||||
this->state.anchorpoint = anchorpoint;
|
||||
this->state.size = size;
|
||||
}
|
||||
|
||||
static PopupPositioner* POSITIONER = nullptr; // NOLINT
|
||||
|
||||
void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool onlyIfDirty) {
|
||||
auto* parentWindow = window->transientParent();
|
||||
if (!parentWindow) {
|
||||
qFatal() << "Cannot reposition popup that does not have a transient parent.";
|
||||
}
|
||||
|
||||
auto parentGeometry = parentWindow->geometry();
|
||||
auto windowGeometry = window->geometry();
|
||||
|
||||
emit anchor->anchoring();
|
||||
anchor->updatePlacement(parentGeometry.topLeft(), windowGeometry.size());
|
||||
|
||||
if (onlyIfDirty && !anchor->isDirty()) return;
|
||||
anchor->markClean();
|
||||
|
||||
auto adjustment = anchor->adjustment();
|
||||
auto screenGeometry = parentWindow->screen()->geometry();
|
||||
auto anchorRectGeometry = anchor->rect().qrect().translated(parentGeometry.topLeft());
|
||||
|
||||
auto anchorEdges = anchor->edges();
|
||||
auto anchorGravity = anchor->gravity();
|
||||
|
||||
auto width = windowGeometry.width();
|
||||
auto height = windowGeometry.height();
|
||||
|
||||
auto anchorX = anchorEdges.testFlag(Edges::Left) ? anchorRectGeometry.left()
|
||||
: anchorEdges.testFlag(Edges::Right) ? anchorRectGeometry.right()
|
||||
: anchorRectGeometry.center().x();
|
||||
|
||||
auto anchorY = anchorEdges.testFlag(Edges::Top) ? anchorRectGeometry.top()
|
||||
: anchorEdges.testFlag(Edges::Bottom) ? anchorRectGeometry.bottom()
|
||||
: anchorRectGeometry.center().y();
|
||||
|
||||
auto calcEffectiveX = [&](Edges::Flags anchorGravity, int anchorX) {
|
||||
auto ex = anchorGravity.testFlag(Edges::Left) ? anchorX - windowGeometry.width()
|
||||
: anchorGravity.testFlag(Edges::Right) ? anchorX - 1
|
||||
: anchorX - windowGeometry.width() / 2;
|
||||
|
||||
return ex + 1;
|
||||
};
|
||||
|
||||
auto calcEffectiveY = [&](Edges::Flags anchorGravity, int anchorY) {
|
||||
auto ey = anchorGravity.testFlag(Edges::Top) ? anchorY - windowGeometry.height()
|
||||
: anchorGravity.testFlag(Edges::Bottom) ? anchorY - 1
|
||||
: anchorY - windowGeometry.height() / 2;
|
||||
|
||||
return ey + 1;
|
||||
};
|
||||
|
||||
auto calcRemainingWidth = [&](int effectiveX) {
|
||||
auto width = windowGeometry.width();
|
||||
if (effectiveX < screenGeometry.left()) {
|
||||
auto diff = screenGeometry.left() - effectiveX;
|
||||
effectiveX = screenGeometry.left();
|
||||
width -= diff;
|
||||
}
|
||||
|
||||
auto effectiveX2 = effectiveX + width;
|
||||
if (effectiveX2 > screenGeometry.right()) {
|
||||
width -= effectiveX2 - screenGeometry.right() - 1;
|
||||
}
|
||||
|
||||
return QPair<int, int>(effectiveX, width);
|
||||
};
|
||||
|
||||
auto calcRemainingHeight = [&](int effectiveY) {
|
||||
auto height = windowGeometry.height();
|
||||
if (effectiveY < screenGeometry.left()) {
|
||||
auto diff = screenGeometry.top() - effectiveY;
|
||||
effectiveY = screenGeometry.top();
|
||||
height -= diff;
|
||||
}
|
||||
|
||||
auto effectiveY2 = effectiveY + height;
|
||||
if (effectiveY2 > screenGeometry.bottom()) {
|
||||
height -= effectiveY2 - screenGeometry.bottom() - 1;
|
||||
}
|
||||
|
||||
return QPair<int, int>(effectiveY, height);
|
||||
};
|
||||
|
||||
auto effectiveX = calcEffectiveX(anchorGravity, anchorX);
|
||||
auto effectiveY = calcEffectiveY(anchorGravity, anchorY);
|
||||
|
||||
if (adjustment.testFlag(PopupAdjustment::FlipX)) {
|
||||
const bool flip = (anchorGravity.testFlag(Edges::Left) && effectiveX < screenGeometry.left())
|
||||
|| (anchorGravity.testFlag(Edges::Right)
|
||||
&& effectiveX + windowGeometry.width() > screenGeometry.right());
|
||||
|
||||
if (flip) {
|
||||
auto newAnchorGravity = anchorGravity ^ (Edges::Left | Edges::Right);
|
||||
|
||||
auto newAnchorX = anchorEdges.testFlags(Edges::Left) ? anchorRectGeometry.right()
|
||||
: anchorEdges.testFlags(Edges::Right) ? anchorRectGeometry.left()
|
||||
: anchorX;
|
||||
|
||||
auto newEffectiveX = calcEffectiveX(newAnchorGravity, newAnchorX);
|
||||
|
||||
// TODO IN HL: pick constraint monitor based on anchor rect position in window
|
||||
|
||||
// if the available width when flipped is more than the available width without flipping then flip
|
||||
if (calcRemainingWidth(newEffectiveX).second > calcRemainingWidth(effectiveX).second) {
|
||||
anchorGravity = newAnchorGravity;
|
||||
anchorX = newAnchorX;
|
||||
effectiveX = newEffectiveX;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (adjustment.testFlag(PopupAdjustment::FlipY)) {
|
||||
const bool flip = (anchorGravity.testFlag(Edges::Top) && effectiveY < screenGeometry.top())
|
||||
|| (anchorGravity.testFlag(Edges::Bottom)
|
||||
&& effectiveY + windowGeometry.height() > screenGeometry.bottom());
|
||||
|
||||
if (flip) {
|
||||
auto newAnchorGravity = anchorGravity ^ (Edges::Top | Edges::Bottom);
|
||||
|
||||
auto newAnchorY = anchorEdges.testFlags(Edges::Top) ? anchorRectGeometry.bottom()
|
||||
: anchorEdges.testFlags(Edges::Bottom) ? anchorRectGeometry.top()
|
||||
: anchorY;
|
||||
|
||||
auto newEffectiveY = calcEffectiveY(newAnchorGravity, newAnchorY);
|
||||
|
||||
// if the available width when flipped is more than the available width without flipping then flip
|
||||
if (calcRemainingHeight(newEffectiveY).second > calcRemainingHeight(effectiveY).second) {
|
||||
anchorGravity = newAnchorGravity;
|
||||
anchorY = newAnchorY;
|
||||
effectiveY = newEffectiveY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Slide order is important for the case where the window is too large to fit on screen.
|
||||
if (adjustment.testFlag(PopupAdjustment::SlideX)) {
|
||||
if (effectiveX + windowGeometry.width() > screenGeometry.right()) {
|
||||
effectiveX = screenGeometry.right() - windowGeometry.width() + 1;
|
||||
}
|
||||
|
||||
effectiveX = std::max(effectiveX, screenGeometry.left());
|
||||
}
|
||||
|
||||
if (adjustment.testFlag(PopupAdjustment::SlideY)) {
|
||||
if (effectiveY + windowGeometry.height() > screenGeometry.bottom()) {
|
||||
effectiveY = screenGeometry.bottom() - windowGeometry.height() + 1;
|
||||
}
|
||||
|
||||
effectiveY = std::max(effectiveY, screenGeometry.top());
|
||||
}
|
||||
|
||||
if (adjustment.testFlag(PopupAdjustment::ResizeX)) {
|
||||
auto [newX, newWidth] = calcRemainingWidth(effectiveX);
|
||||
effectiveX = newX;
|
||||
width = newWidth;
|
||||
}
|
||||
|
||||
if (adjustment.testFlag(PopupAdjustment::ResizeY)) {
|
||||
auto [newY, newHeight] = calcRemainingHeight(effectiveY);
|
||||
effectiveY = newY;
|
||||
height = newHeight;
|
||||
}
|
||||
|
||||
window->setGeometry({effectiveX, effectiveY, width, height});
|
||||
}
|
||||
|
||||
bool PopupPositioner::shouldRepositionOnMove() const { return true; }
|
||||
|
||||
PopupPositioner* PopupPositioner::instance() {
|
||||
if (POSITIONER == nullptr) {
|
||||
POSITIONER = new PopupPositioner();
|
||||
}
|
||||
|
||||
return POSITIONER;
|
||||
}
|
||||
|
||||
void PopupPositioner::setInstance(PopupPositioner* instance) {
|
||||
delete POSITIONER;
|
||||
POSITIONER = instance;
|
||||
}
|
|
@ -1,174 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <qflags.h>
|
||||
#include <qnamespace.h>
|
||||
#include <qobject.h>
|
||||
#include <qpoint.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qsize.h>
|
||||
#include <qtclasshelpermacros.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
#include <qwindow.h>
|
||||
|
||||
#include "../window/proxywindow.hpp"
|
||||
#include "doc.hpp"
|
||||
#include "types.hpp"
|
||||
|
||||
///! Adjustment strategy for popups that do not fit on screen.
|
||||
/// Adjustment strategy for popups. See @@PopupAnchor.adjustment.
|
||||
///
|
||||
/// Adjustment flags can be combined with the `|` operator.
|
||||
///
|
||||
/// `Flip` will be applied first, then `Slide`, then `Resize`.
|
||||
namespace PopupAdjustment { // NOLINT
|
||||
Q_NAMESPACE;
|
||||
QML_ELEMENT;
|
||||
|
||||
enum Enum : quint8 {
|
||||
None = 0,
|
||||
/// If the X axis is constrained, the popup will slide along the X axis until it fits onscreen.
|
||||
SlideX = 1,
|
||||
/// If the Y axis is constrained, the popup will slide along the Y axis until it fits onscreen.
|
||||
SlideY = 2,
|
||||
/// Alias for `SlideX | SlideY`.
|
||||
Slide = SlideX | SlideY,
|
||||
/// If the X axis is constrained, the popup will invert its horizontal gravity if any.
|
||||
FlipX = 4,
|
||||
/// If the Y axis is constrained, the popup will invert its vertical gravity if any.
|
||||
FlipY = 8,
|
||||
/// Alias for `FlipX | FlipY`.
|
||||
Flip = FlipX | FlipY,
|
||||
/// If the X axis is constrained, the width of the popup will be reduced to fit on screen.
|
||||
ResizeX = 16,
|
||||
/// If the Y axis is constrained, the height of the popup will be reduced to fit on screen.
|
||||
ResizeY = 32,
|
||||
/// Alias for `ResizeX | ResizeY`
|
||||
Resize = ResizeX | ResizeY,
|
||||
/// Alias for `Flip | Slide | Resize`.
|
||||
All = Slide | Flip | Resize,
|
||||
};
|
||||
Q_ENUM_NS(Enum);
|
||||
Q_DECLARE_FLAGS(Flags, Enum);
|
||||
|
||||
} // namespace PopupAdjustment
|
||||
|
||||
Q_DECLARE_OPERATORS_FOR_FLAGS(PopupAdjustment::Flags);
|
||||
|
||||
struct PopupAnchorState {
|
||||
bool operator==(const PopupAnchorState& other) const;
|
||||
|
||||
Box rect = {0, 0, 1, 1};
|
||||
Edges::Flags edges = Edges::Top | Edges::Left;
|
||||
Edges::Flags gravity = Edges::Bottom | Edges::Right;
|
||||
PopupAdjustment::Flags adjustment = PopupAdjustment::Slide;
|
||||
QPoint anchorpoint;
|
||||
QSize size;
|
||||
};
|
||||
|
||||
///! Anchorpoint or positioner for popup windows.
|
||||
class PopupAnchor: public QObject {
|
||||
Q_OBJECT;
|
||||
// clang-format off
|
||||
/// The window to anchor / attach the popup to.
|
||||
Q_PROPERTY(QObject* window READ window WRITE setWindow NOTIFY windowChanged);
|
||||
/// The anchorpoints the popup will attach to. Which anchors will be used is
|
||||
/// determined by the @@edges, @@gravity, and @@adjustment.
|
||||
///
|
||||
/// If you leave @@edges, @@gravity and @@adjustment at their default values,
|
||||
/// setting more than `x` and `y` does not matter. The anchor rect cannot
|
||||
/// be smaller than 1x1 pixels.
|
||||
///
|
||||
/// > [!INFO] To position a popup relative to an item inside a window,
|
||||
/// > you can use [coordinate mapping functions] (note the warning below).
|
||||
///
|
||||
/// > [!WARNING] Using [coordinate mapping functions] in a binding to
|
||||
/// > this property will position the anchor incorrectly.
|
||||
/// > If you want to use them, do so in @@anchoring(s), or use
|
||||
/// > @@TransformWatcher if you need real-time updates to mapped coordinates.
|
||||
///
|
||||
/// [coordinate mapping functions]: https://doc.qt.io/qt-6/qml-qtquick-item.html#mapFromItem-method
|
||||
Q_PROPERTY(Box rect READ rect WRITE setRect NOTIFY rectChanged);
|
||||
/// The point on the anchor rectangle the popup should anchor to.
|
||||
/// Opposing edges suchs as `Edges.Left | Edges.Right` are not allowed.
|
||||
///
|
||||
/// Defaults to `Edges.Top | Edges.Left`.
|
||||
Q_PROPERTY(Edges::Flags edges READ edges WRITE setEdges NOTIFY edgesChanged);
|
||||
/// The direction the popup should expand towards, relative to the anchorpoint.
|
||||
/// Opposing edges suchs as `Edges.Left | Edges.Right` are not allowed.
|
||||
///
|
||||
/// Defaults to `Edges.Bottom | Edges.Right`.
|
||||
Q_PROPERTY(Edges::Flags gravity READ gravity WRITE setGravity NOTIFY gravityChanged);
|
||||
/// The strategy used to adjust the popup's position if it would otherwise not fit on screen,
|
||||
/// based on the anchor @@rect, preferred @@edges, and @@gravity.
|
||||
///
|
||||
/// See the documentation for @@PopupAdjustment for details.
|
||||
Q_PROPERTY(PopupAdjustment::Flags adjustment READ adjustment WRITE setAdjustment NOTIFY adjustmentChanged);
|
||||
// clang-format on
|
||||
QML_ELEMENT;
|
||||
QML_UNCREATABLE("");
|
||||
|
||||
public:
|
||||
explicit PopupAnchor(QObject* parent): QObject(parent) {}
|
||||
|
||||
[[nodiscard]] bool isDirty() const;
|
||||
void markClean();
|
||||
void markDirty();
|
||||
|
||||
[[nodiscard]] QObject* window() const;
|
||||
[[nodiscard]] ProxyWindowBase* proxyWindow() const;
|
||||
[[nodiscard]] QWindow* backingWindow() const;
|
||||
void setWindow(QObject* window);
|
||||
|
||||
[[nodiscard]] Box rect() const;
|
||||
void setRect(Box rect);
|
||||
|
||||
[[nodiscard]] Edges::Flags edges() const;
|
||||
void setEdges(Edges::Flags edges);
|
||||
|
||||
[[nodiscard]] Edges::Flags gravity() const;
|
||||
void setGravity(Edges::Flags gravity);
|
||||
|
||||
[[nodiscard]] PopupAdjustment::Flags adjustment() const;
|
||||
void setAdjustment(PopupAdjustment::Flags adjustment);
|
||||
|
||||
void updatePlacement(const QPoint& anchorpoint, const QSize& size);
|
||||
|
||||
signals:
|
||||
/// Emitted when this anchor is about to be used. Mostly useful for modifying
|
||||
/// the anchor @@rect using [coordinate mapping functions], which are not reactive.
|
||||
///
|
||||
/// [coordinate mapping functions]: https://doc.qt.io/qt-6/qml-qtquick-item.html#mapFromItem-method
|
||||
void anchoring();
|
||||
|
||||
void windowChanged();
|
||||
QSDOC_HIDE void backingWindowVisibilityChanged();
|
||||
void rectChanged();
|
||||
void edgesChanged();
|
||||
void gravityChanged();
|
||||
void adjustmentChanged();
|
||||
|
||||
private slots:
|
||||
void onWindowDestroyed();
|
||||
|
||||
private:
|
||||
QObject* mWindow = nullptr;
|
||||
ProxyWindowBase* mProxyWindow = nullptr;
|
||||
PopupAnchorState state;
|
||||
std::optional<PopupAnchorState> lastState;
|
||||
};
|
||||
|
||||
class PopupPositioner {
|
||||
public:
|
||||
explicit PopupPositioner() = default;
|
||||
virtual ~PopupPositioner() = default;
|
||||
Q_DISABLE_COPY_MOVE(PopupPositioner);
|
||||
|
||||
virtual void reposition(PopupAnchor* anchor, QWindow* window, bool onlyIfDirty = true);
|
||||
[[nodiscard]] virtual bool shouldRepositionOnMove() const;
|
||||
|
||||
static PopupPositioner* instance();
|
||||
static void setInstance(PopupPositioner* instance);
|
||||
};
|
161
src/core/popupwindow.cpp
Normal file
161
src/core/popupwindow.cpp
Normal file
|
@ -0,0 +1,161 @@
|
|||
#include "popupwindow.hpp"
|
||||
|
||||
#include <qlogging.h>
|
||||
#include <qnamespace.h>
|
||||
#include <qobject.h>
|
||||
#include <qquickwindow.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "proxywindow.hpp"
|
||||
#include "qmlscreen.hpp"
|
||||
#include "windowinterface.hpp"
|
||||
|
||||
ProxyPopupWindow::ProxyPopupWindow(QObject* parent): ProxyWindowBase(parent) {
|
||||
this->mVisible = false;
|
||||
}
|
||||
|
||||
void ProxyPopupWindow::completeWindow() {
|
||||
this->ProxyWindowBase::completeWindow();
|
||||
|
||||
this->window->setFlag(Qt::ToolTip);
|
||||
this->updateTransientParent();
|
||||
}
|
||||
|
||||
void ProxyPopupWindow::postCompleteWindow() { this->ProxyWindowBase::setVisible(this->mVisible); }
|
||||
|
||||
bool ProxyPopupWindow::deleteOnInvisible() const {
|
||||
// Currently crashes in normal mode, do not have the time to debug it now.
|
||||
return true;
|
||||
}
|
||||
|
||||
qint32 ProxyPopupWindow::x() const {
|
||||
// QTBUG-121550
|
||||
auto basepos = this->mParentProxyWindow == nullptr ? 0 : this->mParentProxyWindow->x();
|
||||
return basepos + this->mRelativeX;
|
||||
}
|
||||
|
||||
void ProxyPopupWindow::setParentWindow(QObject* parent) {
|
||||
if (parent == this->mParentWindow) return;
|
||||
|
||||
if (this->mParentWindow != nullptr) {
|
||||
QObject::disconnect(this->mParentWindow, nullptr, this, nullptr);
|
||||
QObject::disconnect(this->mParentProxyWindow, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
if (parent == nullptr) {
|
||||
this->mParentWindow = nullptr;
|
||||
this->mParentProxyWindow = nullptr;
|
||||
} else {
|
||||
if (auto* proxy = qobject_cast<ProxyWindowBase*>(parent)) {
|
||||
this->mParentProxyWindow = proxy;
|
||||
} else if (auto* interface = qobject_cast<WindowInterface*>(parent)) {
|
||||
this->mParentProxyWindow = interface->proxyWindow();
|
||||
} else {
|
||||
qWarning() << "Tried to set popup parent window to something that is not a quickshell window:"
|
||||
<< parent;
|
||||
this->mParentWindow = nullptr;
|
||||
this->mParentProxyWindow = nullptr;
|
||||
this->updateTransientParent();
|
||||
return;
|
||||
}
|
||||
|
||||
this->mParentWindow = parent;
|
||||
|
||||
// clang-format off
|
||||
QObject::connect(this->mParentWindow, &QObject::destroyed, this, &ProxyPopupWindow::onParentDestroyed);
|
||||
|
||||
QObject::connect(this->mParentProxyWindow, &ProxyWindowBase::xChanged, this, &ProxyPopupWindow::updateX);
|
||||
QObject::connect(this->mParentProxyWindow, &ProxyWindowBase::yChanged, this, &ProxyPopupWindow::updateY);
|
||||
QObject::connect(this->mParentProxyWindow, &ProxyWindowBase::backerVisibilityChanged, this, &ProxyPopupWindow::onParentUpdated);
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
this->updateTransientParent();
|
||||
}
|
||||
|
||||
QObject* ProxyPopupWindow::parentWindow() const { return this->mParentWindow; }
|
||||
|
||||
void ProxyPopupWindow::updateTransientParent() {
|
||||
this->updateX();
|
||||
this->updateY();
|
||||
|
||||
if (this->window != nullptr) {
|
||||
this->window->setTransientParent(
|
||||
this->mParentProxyWindow == nullptr ? nullptr : this->mParentProxyWindow->backingWindow()
|
||||
);
|
||||
}
|
||||
|
||||
this->updateVisible();
|
||||
}
|
||||
|
||||
void ProxyPopupWindow::onParentUpdated() { this->updateTransientParent(); }
|
||||
|
||||
void ProxyPopupWindow::onParentDestroyed() {
|
||||
this->mParentWindow = nullptr;
|
||||
this->mParentProxyWindow = nullptr;
|
||||
this->updateVisible();
|
||||
emit this->parentWindowChanged();
|
||||
}
|
||||
|
||||
void ProxyPopupWindow::setScreen(QuickshellScreenInfo* /*unused*/) {
|
||||
qWarning() << "Cannot set screen of popup window, as that is controlled by the parent window";
|
||||
}
|
||||
|
||||
void ProxyPopupWindow::setVisible(bool visible) {
|
||||
if (visible == this->wantsVisible) return;
|
||||
this->wantsVisible = visible;
|
||||
this->updateVisible();
|
||||
}
|
||||
|
||||
void ProxyPopupWindow::updateVisible() {
|
||||
auto target = this->wantsVisible && this->mParentWindow != nullptr
|
||||
&& this->mParentProxyWindow->isVisibleDirect();
|
||||
|
||||
if (target && this->window != nullptr && !this->window->isVisible()) {
|
||||
this->updateX(); // QTBUG-121550
|
||||
}
|
||||
|
||||
this->ProxyWindowBase::setVisible(target);
|
||||
}
|
||||
|
||||
void ProxyPopupWindow::setRelativeX(qint32 x) {
|
||||
if (x == this->mRelativeX) return;
|
||||
this->mRelativeX = x;
|
||||
this->updateX();
|
||||
}
|
||||
|
||||
qint32 ProxyPopupWindow::relativeX() const { return this->mRelativeX; }
|
||||
|
||||
void ProxyPopupWindow::setRelativeY(qint32 y) {
|
||||
if (y == this->mRelativeY) return;
|
||||
this->mRelativeY = y;
|
||||
this->updateY();
|
||||
}
|
||||
|
||||
qint32 ProxyPopupWindow::relativeY() const { return this->mRelativeY; }
|
||||
|
||||
void ProxyPopupWindow::updateX() {
|
||||
if (this->mParentWindow == nullptr || this->window == nullptr) return;
|
||||
|
||||
auto target = this->x() - 1; // QTBUG-121550
|
||||
|
||||
auto reshow = this->isVisibleDirect() && (this->window->x() != target && this->x() != target);
|
||||
if (reshow) this->setVisibleDirect(false);
|
||||
if (this->window != nullptr) this->window->setX(target);
|
||||
if (reshow && this->wantsVisible) this->setVisibleDirect(true);
|
||||
}
|
||||
|
||||
void ProxyPopupWindow::updateY() {
|
||||
if (this->mParentWindow == nullptr || this->window == nullptr) return;
|
||||
|
||||
auto target = this->mParentProxyWindow->y() + this->relativeY();
|
||||
|
||||
auto reshow = this->isVisibleDirect() && this->window->y() != target;
|
||||
if (reshow) {
|
||||
this->setVisibleDirect(false);
|
||||
this->updateX(); // QTBUG-121550
|
||||
}
|
||||
if (this->window != nullptr) this->window->setY(target);
|
||||
if (reshow && this->wantsVisible) this->setVisibleDirect(true);
|
||||
}
|
|
@ -6,10 +6,9 @@
|
|||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "../core/doc.hpp"
|
||||
#include "../core/popupanchor.hpp"
|
||||
#include "../core/qmlscreen.hpp"
|
||||
#include "doc.hpp"
|
||||
#include "proxywindow.hpp"
|
||||
#include "qmlscreen.hpp"
|
||||
#include "windowinterface.hpp"
|
||||
|
||||
///! Popup window.
|
||||
|
@ -30,9 +29,9 @@
|
|||
/// }
|
||||
///
|
||||
/// PopupWindow {
|
||||
/// anchor.window: toplevel
|
||||
/// anchor.rect.x: parentWindow.width / 2 - width / 2
|
||||
/// anchor.rect.y: parentWindow.height
|
||||
/// parentWindow: toplevel
|
||||
/// relativeX: parentWindow.width / 2 - width / 2
|
||||
/// relativeY: parentWindow.height
|
||||
/// width: 500
|
||||
/// height: 500
|
||||
/// visible: true
|
||||
|
@ -43,37 +42,15 @@ class ProxyPopupWindow: public ProxyWindowBase {
|
|||
QSDOC_BASECLASS(WindowInterface);
|
||||
Q_OBJECT;
|
||||
// clang-format off
|
||||
/// > [!ERROR] Deprecated in favor of `anchor.window`.
|
||||
///
|
||||
/// The parent window of this popup.
|
||||
///
|
||||
/// Changing this property reparents the popup.
|
||||
Q_PROPERTY(QObject* parentWindow READ parentWindow WRITE setParentWindow NOTIFY parentWindowChanged);
|
||||
/// > [!ERROR] Deprecated in favor of `anchor.rect.x`.
|
||||
///
|
||||
/// The X position of the popup relative to the parent window.
|
||||
Q_PROPERTY(qint32 relativeX READ relativeX WRITE setRelativeX NOTIFY relativeXChanged);
|
||||
/// > [!ERROR] Deprecated in favor of `anchor.rect.y`.
|
||||
///
|
||||
/// The Y position of the popup relative to the parent window.
|
||||
Q_PROPERTY(qint32 relativeY READ relativeY WRITE setRelativeY NOTIFY relativeYChanged);
|
||||
/// The popup's anchor / positioner relative to another window. The popup will not be
|
||||
/// shown until it has a valid anchor relative to a window and @@visible is true.
|
||||
///
|
||||
/// You can set properties of the anchor like so:
|
||||
/// ```qml
|
||||
/// PopupWindow {
|
||||
/// anchor.window: parentwindow
|
||||
/// // or
|
||||
/// anchor {
|
||||
/// window: parentwindow
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
Q_PROPERTY(PopupAnchor* anchor READ anchor CONSTANT);
|
||||
/// If the window is shown or hidden. Defaults to false.
|
||||
///
|
||||
/// The popup will not be shown until @@anchor is valid, regardless of this property.
|
||||
QSDOC_PROPERTY_OVERRIDE(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged);
|
||||
/// The screen that the window currently occupies.
|
||||
///
|
||||
|
@ -87,10 +64,13 @@ public:
|
|||
|
||||
void completeWindow() override;
|
||||
void postCompleteWindow() override;
|
||||
[[nodiscard]] bool deleteOnInvisible() const override;
|
||||
|
||||
void setScreen(QuickshellScreenInfo* screen) override;
|
||||
void setVisible(bool visible) override;
|
||||
|
||||
[[nodiscard]] qint32 x() const override;
|
||||
|
||||
[[nodiscard]] QObject* parentWindow() const;
|
||||
void setParentWindow(QObject* parent);
|
||||
|
||||
|
@ -100,23 +80,25 @@ public:
|
|||
[[nodiscard]] qint32 relativeY() const;
|
||||
void setRelativeY(qint32 y);
|
||||
|
||||
[[nodiscard]] PopupAnchor* anchor();
|
||||
|
||||
signals:
|
||||
void parentWindowChanged();
|
||||
void relativeXChanged();
|
||||
void relativeYChanged();
|
||||
|
||||
private slots:
|
||||
void onVisibleChanged();
|
||||
void onParentUpdated();
|
||||
void reposition();
|
||||
void onParentDestroyed();
|
||||
void updateX();
|
||||
void updateY();
|
||||
|
||||
private:
|
||||
QQuickWindow* parentBackingWindow();
|
||||
void updateTransientParent();
|
||||
void updateVisible();
|
||||
|
||||
PopupAnchor mAnchor {this};
|
||||
QObject* mParentWindow = nullptr;
|
||||
ProxyWindowBase* mParentProxyWindow = nullptr;
|
||||
qint32 mRelativeX = 0;
|
||||
qint32 mRelativeY = 0;
|
||||
bool wantsVisible = false;
|
||||
};
|
|
@ -1,45 +1,35 @@
|
|||
#include "proxywindow.hpp"
|
||||
|
||||
#include <private/qquickwindow_p.h>
|
||||
#include <qcoreevent.h>
|
||||
#include <qevent.h>
|
||||
#include <qguiapplication.h>
|
||||
#include <qnamespace.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlcontext.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qqmlinfo.h>
|
||||
#include <qqmllist.h>
|
||||
#include <qquickitem.h>
|
||||
#include <qquickwindow.h>
|
||||
#include <qregion.h>
|
||||
#include <qsurfaceformat.h>
|
||||
#include <qtenvironmentvariables.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
#include <qvariant.h>
|
||||
#include <qwindow.h>
|
||||
|
||||
#include "../core/generation.hpp"
|
||||
#include "../core/qmlglobal.hpp"
|
||||
#include "../core/qmlscreen.hpp"
|
||||
#include "../core/region.hpp"
|
||||
#include "../core/reload.hpp"
|
||||
#include "../debug/lint.hpp"
|
||||
#include "generation.hpp"
|
||||
#include "qmlglobal.hpp"
|
||||
#include "qmlscreen.hpp"
|
||||
#include "region.hpp"
|
||||
#include "reload.hpp"
|
||||
#include "windowinterface.hpp"
|
||||
|
||||
ProxyWindowBase::ProxyWindowBase(QObject* parent)
|
||||
: Reloadable(parent)
|
||||
, mContentItem(new ProxyWindowContentItem()) {
|
||||
, mContentItem(new QQuickItem()) {
|
||||
QQmlEngine::setObjectOwnership(this->mContentItem, QQmlEngine::CppOwnership);
|
||||
this->mContentItem->setParent(this);
|
||||
|
||||
// clang-format off
|
||||
QObject::connect(this->mContentItem, &ProxyWindowContentItem::polished, this, &ProxyWindowBase::onPolished);
|
||||
|
||||
QObject::connect(this, &ProxyWindowBase::widthChanged, this, &ProxyWindowBase::onWidthChanged);
|
||||
QObject::connect(this, &ProxyWindowBase::heightChanged, this, &ProxyWindowBase::onHeightChanged);
|
||||
|
||||
QObject::connect(this, &ProxyWindowBase::maskChanged, this, &ProxyWindowBase::onMaskChanged);
|
||||
QObject::connect(this, &ProxyWindowBase::widthChanged, this, &ProxyWindowBase::onMaskChanged);
|
||||
QObject::connect(this, &ProxyWindowBase::heightChanged, this, &ProxyWindowBase::onMaskChanged);
|
||||
|
||||
|
@ -51,12 +41,12 @@ ProxyWindowBase::ProxyWindowBase(QObject* parent)
|
|||
// clang-format on
|
||||
}
|
||||
|
||||
ProxyWindowBase::~ProxyWindowBase() { this->deleteWindow(true); }
|
||||
ProxyWindowBase::~ProxyWindowBase() { this->deleteWindow(); }
|
||||
|
||||
void ProxyWindowBase::onReload(QObject* oldInstance) {
|
||||
this->window = this->retrieveWindow(oldInstance);
|
||||
auto wasVisible = this->window != nullptr && this->window->isVisible();
|
||||
this->ensureQWindow();
|
||||
if (this->window == nullptr) this->window = new QQuickWindow();
|
||||
|
||||
// The qml engine will leave the WindowInterface as owner of everything
|
||||
// nested in an item, so we have to make sure the interface's children
|
||||
|
@ -81,73 +71,25 @@ void ProxyWindowBase::onReload(QObject* oldInstance) {
|
|||
emit this->windowConnected();
|
||||
this->postCompleteWindow();
|
||||
|
||||
if (wasVisible && this->isVisibleDirect()) {
|
||||
emit this->backerVisibilityChanged();
|
||||
this->runLints();
|
||||
}
|
||||
if (wasVisible && this->isVisibleDirect()) emit this->backerVisibilityChanged();
|
||||
}
|
||||
|
||||
void ProxyWindowBase::postCompleteWindow() { this->setVisible(this->mVisible); }
|
||||
|
||||
ProxiedWindow* ProxyWindowBase::createQQuickWindow() { return new ProxiedWindow(this); }
|
||||
|
||||
void ProxyWindowBase::ensureQWindow() {
|
||||
auto format = QSurfaceFormat::defaultFormat();
|
||||
|
||||
{
|
||||
// match QtQuick's default format, including env var controls
|
||||
static const auto useDepth = qEnvironmentVariableIsEmpty("QSG_NO_DEPTH_BUFFER");
|
||||
static const auto useStencil = qEnvironmentVariableIsEmpty("QSG_NO_STENCIL_BUFFER");
|
||||
static const auto enableDebug = qEnvironmentVariableIsSet("QSG_OPENGL_DEBUG");
|
||||
static const auto disableVSync = qEnvironmentVariableIsSet("QSG_NO_VSYNC");
|
||||
|
||||
if (useDepth && format.depthBufferSize() == -1) format.setDepthBufferSize(24);
|
||||
else if (!useDepth) format.setDepthBufferSize(0);
|
||||
|
||||
if (useStencil && format.stencilBufferSize() == -1) format.setStencilBufferSize(8);
|
||||
else if (!useStencil) format.setStencilBufferSize(0);
|
||||
|
||||
auto opaque = this->qsSurfaceFormat.opaqueModified ? this->qsSurfaceFormat.opaque
|
||||
: this->mColor.alpha() >= 255;
|
||||
|
||||
if (opaque) format.setAlphaBufferSize(0);
|
||||
else format.setAlphaBufferSize(8);
|
||||
|
||||
if (enableDebug) format.setOption(QSurfaceFormat::DebugContext);
|
||||
if (disableVSync) format.setSwapInterval(0);
|
||||
|
||||
format.setSwapBehavior(QSurfaceFormat::DoubleBuffer);
|
||||
format.setRedBufferSize(8);
|
||||
format.setGreenBufferSize(8);
|
||||
format.setBlueBufferSize(8);
|
||||
}
|
||||
|
||||
this->mSurfaceFormat = format;
|
||||
|
||||
auto useOldWindow = this->window != nullptr;
|
||||
|
||||
if (useOldWindow) {
|
||||
if (this->window->requestedFormat() != format) {
|
||||
useOldWindow = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (useOldWindow) return;
|
||||
delete this->window;
|
||||
this->window = this->createQQuickWindow();
|
||||
this->window->setFormat(format);
|
||||
}
|
||||
QQuickWindow* ProxyWindowBase::createQQuickWindow() { return new QQuickWindow(); }
|
||||
|
||||
void ProxyWindowBase::createWindow() {
|
||||
this->ensureQWindow();
|
||||
if (this->window != nullptr) return;
|
||||
this->window = this->createQQuickWindow();
|
||||
|
||||
this->connectWindow();
|
||||
this->completeWindow();
|
||||
emit this->windowConnected();
|
||||
}
|
||||
|
||||
void ProxyWindowBase::deleteWindow(bool keepItemOwnership) {
|
||||
void ProxyWindowBase::deleteWindow() {
|
||||
if (this->window != nullptr) emit this->windowDestroyed();
|
||||
if (auto* window = this->disownWindow(keepItemOwnership)) {
|
||||
if (auto* window = this->disownWindow()) {
|
||||
if (auto* generation = EngineGeneration::findObjectGeneration(this)) {
|
||||
generation->deregisterIncubationController(window->incubationController());
|
||||
}
|
||||
|
@ -156,21 +98,19 @@ void ProxyWindowBase::deleteWindow(bool keepItemOwnership) {
|
|||
}
|
||||
}
|
||||
|
||||
ProxiedWindow* ProxyWindowBase::disownWindow(bool keepItemOwnership) {
|
||||
QQuickWindow* ProxyWindowBase::disownWindow() {
|
||||
if (this->window == nullptr) return nullptr;
|
||||
|
||||
QObject::disconnect(this->window, nullptr, this, nullptr);
|
||||
|
||||
if (!keepItemOwnership) {
|
||||
this->mContentItem->setParentItem(nullptr);
|
||||
}
|
||||
this->mContentItem->setParentItem(nullptr);
|
||||
|
||||
auto* window = this->window;
|
||||
this->window = nullptr;
|
||||
return window;
|
||||
}
|
||||
|
||||
ProxiedWindow* ProxyWindowBase::retrieveWindow(QObject* oldInstance) {
|
||||
QQuickWindow* ProxyWindowBase::retrieveWindow(QObject* oldInstance) {
|
||||
auto* old = qobject_cast<ProxyWindowBase*>(oldInstance);
|
||||
return old == nullptr ? nullptr : old->disownWindow();
|
||||
}
|
||||
|
@ -182,8 +122,6 @@ void ProxyWindowBase::connectWindow() {
|
|||
generation->registerIncubationController(this->window->incubationController());
|
||||
}
|
||||
|
||||
this->window->setProxy(this);
|
||||
|
||||
// clang-format off
|
||||
QObject::connect(this->window, &QWindow::visibilityChanged, this, &ProxyWindowBase::visibleChanged);
|
||||
QObject::connect(this->window, &QWindow::xChanged, this, &ProxyWindowBase::xChanged);
|
||||
|
@ -192,8 +130,6 @@ void ProxyWindowBase::connectWindow() {
|
|||
QObject::connect(this->window, &QWindow::heightChanged, this, &ProxyWindowBase::heightChanged);
|
||||
QObject::connect(this->window, &QWindow::screenChanged, this, &ProxyWindowBase::screenChanged);
|
||||
QObject::connect(this->window, &QQuickWindow::colorChanged, this, &ProxyWindowBase::colorChanged);
|
||||
QObject::connect(this->window, &ProxiedWindow::exposed, this, &ProxyWindowBase::runLints);
|
||||
QObject::connect(this->window, &ProxiedWindow::devicePixelRatioChanged, this, &ProxyWindowBase::devicePixelRatioChanged);
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
|
@ -208,12 +144,9 @@ void ProxyWindowBase::completeWindow() {
|
|||
this->setColor(this->mColor);
|
||||
this->updateMask();
|
||||
|
||||
// notify initial / post-connection geometry
|
||||
// notify initial x and y positions
|
||||
emit this->xChanged();
|
||||
emit this->yChanged();
|
||||
emit this->widthChanged();
|
||||
emit this->heightChanged();
|
||||
emit this->devicePixelRatioChanged();
|
||||
|
||||
this->mContentItem->setParentItem(this->window->contentItem());
|
||||
this->mContentItem->setWidth(this->width());
|
||||
|
@ -223,7 +156,16 @@ void ProxyWindowBase::completeWindow() {
|
|||
emit this->screenChanged();
|
||||
}
|
||||
|
||||
bool ProxyWindowBase::deleteOnInvisible() const { return false; }
|
||||
bool ProxyWindowBase::deleteOnInvisible() const {
|
||||
#ifdef NVIDIA_COMPAT
|
||||
// Nvidia drivers and Qt do not play nice when hiding and showing a window
|
||||
// so for nvidia compatibility we can never reuse windows if they have been
|
||||
// hidden.
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
QQuickWindow* ProxyWindowBase::backingWindow() const { return this->window; }
|
||||
QQuickItem* ProxyWindowBase::contentItem() const { return this->mContentItem; }
|
||||
|
@ -249,7 +191,6 @@ void ProxyWindowBase::setVisibleDirect(bool visible) {
|
|||
|
||||
if (visible) {
|
||||
this->createWindow();
|
||||
this->polishItems();
|
||||
this->window->setVisible(true);
|
||||
emit this->backerVisibilityChanged();
|
||||
} else {
|
||||
|
@ -260,35 +201,11 @@ void ProxyWindowBase::setVisibleDirect(bool visible) {
|
|||
}
|
||||
}
|
||||
} else if (this->window != nullptr) {
|
||||
if (visible) this->polishItems();
|
||||
this->window->setVisible(visible);
|
||||
emit this->backerVisibilityChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void ProxyWindowBase::schedulePolish() {
|
||||
if (this->isVisibleDirect()) {
|
||||
this->mContentItem->polish();
|
||||
}
|
||||
}
|
||||
|
||||
void ProxyWindowBase::polishItems() {
|
||||
// Due to QTBUG-126704, layouts in invisible windows don't update their dimensions.
|
||||
// Usually this isn't an issue, but it is when the size of a window is based on the size
|
||||
// of its content, and that content is in a layout.
|
||||
//
|
||||
// This hack manually polishes the item tree right before showing the window so it will
|
||||
// always be created with the correct size.
|
||||
QQuickWindowPrivate::get(this->window)->polishItems();
|
||||
}
|
||||
|
||||
void ProxyWindowBase::runLints() {
|
||||
if (!this->ranLints) {
|
||||
qs::debug::lintItemTree(this->mContentItem);
|
||||
this->ranLints = true;
|
||||
}
|
||||
}
|
||||
|
||||
qint32 ProxyWindowBase::x() const {
|
||||
if (this->window == nullptr) return 0;
|
||||
else return this->window->x();
|
||||
|
@ -324,61 +241,52 @@ void ProxyWindowBase::setHeight(qint32 height) {
|
|||
}
|
||||
|
||||
void ProxyWindowBase::setScreen(QuickshellScreenInfo* screen) {
|
||||
auto* qscreen = screen == nullptr ? nullptr : screen->screen;
|
||||
auto newMScreen = this->mScreen != qscreen;
|
||||
|
||||
if (this->mScreen && newMScreen) {
|
||||
if (this->mScreen != nullptr) {
|
||||
QObject::disconnect(this->mScreen, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
if (this->qscreen() != qscreen) {
|
||||
this->mScreen = qscreen;
|
||||
if (this->window == nullptr) {
|
||||
emit this->screenChanged();
|
||||
} else if (qscreen) {
|
||||
auto reshow = this->isVisibleDirect();
|
||||
if (reshow) this->setVisibleDirect(false);
|
||||
if (this->window != nullptr) this->window->setScreen(qscreen);
|
||||
if (reshow) this->setVisibleDirect(true);
|
||||
}
|
||||
auto* qscreen = screen == nullptr ? nullptr : screen->screen;
|
||||
if (qscreen == this->mScreen) return;
|
||||
|
||||
if (qscreen != nullptr) {
|
||||
QObject::connect(qscreen, &QObject::destroyed, this, &ProxyWindowBase::onScreenDestroyed);
|
||||
}
|
||||
|
||||
if (qscreen && newMScreen) {
|
||||
QObject::connect(this->mScreen, &QObject::destroyed, this, &ProxyWindowBase::onScreenDestroyed);
|
||||
if (this->window == nullptr) {
|
||||
this->mScreen = qscreen;
|
||||
emit this->screenChanged();
|
||||
} else {
|
||||
auto reshow = this->isVisibleDirect();
|
||||
if (reshow) this->setVisibleDirect(false);
|
||||
if (this->window != nullptr) this->window->setScreen(qscreen);
|
||||
if (reshow) this->setVisibleDirect(true);
|
||||
}
|
||||
}
|
||||
|
||||
void ProxyWindowBase::onScreenDestroyed() { this->mScreen = nullptr; }
|
||||
|
||||
QScreen* ProxyWindowBase::qscreen() const {
|
||||
if (this->window) return this->window->screen();
|
||||
if (this->mScreen) return this->mScreen;
|
||||
return QGuiApplication::primaryScreen();
|
||||
}
|
||||
|
||||
QuickshellScreenInfo* ProxyWindowBase::screen() const {
|
||||
return QuickshellTracked::instance()->screenInfo(this->qscreen());
|
||||
}
|
||||
|
||||
QColor ProxyWindowBase::color() const { return this->mColor; }
|
||||
|
||||
void ProxyWindowBase::setColor(QColor color) {
|
||||
this->mColor = color;
|
||||
QScreen* qscreen = nullptr;
|
||||
|
||||
if (this->window == nullptr) {
|
||||
if (color != this->mColor) emit this->colorChanged();
|
||||
if (this->mScreen != nullptr) qscreen = this->mScreen;
|
||||
} else {
|
||||
auto premultiplied = QColor::fromRgbF(
|
||||
color.redF() * color.alphaF(),
|
||||
color.greenF() * color.alphaF(),
|
||||
color.blueF() * color.alphaF(),
|
||||
color.alphaF()
|
||||
);
|
||||
|
||||
this->window->setColor(premultiplied);
|
||||
// setColor also modifies the alpha buffer size of the surface format
|
||||
this->window->setFormat(this->mSurfaceFormat);
|
||||
qscreen = this->window->screen();
|
||||
}
|
||||
|
||||
return QuickshellTracked::instance()->screenInfo(qscreen);
|
||||
}
|
||||
|
||||
QColor ProxyWindowBase::color() const {
|
||||
if (this->window == nullptr) return this->mColor;
|
||||
else return this->window->color();
|
||||
}
|
||||
|
||||
void ProxyWindowBase::setColor(QColor color) {
|
||||
if (this->window == nullptr) {
|
||||
this->mColor = color;
|
||||
emit this->colorChanged();
|
||||
} else this->window->setColor(color);
|
||||
}
|
||||
|
||||
PendingRegion* ProxyWindowBase::mask() const { return this->mMask; }
|
||||
|
@ -393,44 +301,37 @@ void ProxyWindowBase::setMask(PendingRegion* mask) {
|
|||
this->mMask = mask;
|
||||
|
||||
if (mask != nullptr) {
|
||||
mask->setParent(this);
|
||||
QObject::connect(mask, &QObject::destroyed, this, &ProxyWindowBase::onMaskDestroyed);
|
||||
QObject::connect(mask, &PendingRegion::changed, this, &ProxyWindowBase::onMaskChanged);
|
||||
QObject::connect(mask, &PendingRegion::changed, this, &ProxyWindowBase::maskChanged);
|
||||
}
|
||||
|
||||
this->onMaskChanged();
|
||||
emit this->maskChanged();
|
||||
}
|
||||
|
||||
void ProxyWindowBase::setSurfaceFormat(QsSurfaceFormat format) {
|
||||
if (format == this->qsSurfaceFormat) return;
|
||||
if (this->window != nullptr) {
|
||||
qmlWarning(this) << "Cannot set window surface format.";
|
||||
return;
|
||||
}
|
||||
|
||||
this->qsSurfaceFormat = format;
|
||||
emit this->surfaceFormatChanged();
|
||||
}
|
||||
|
||||
qreal ProxyWindowBase::devicePixelRatio() const {
|
||||
if (this->window != nullptr) return this->window->devicePixelRatio();
|
||||
if (this->mScreen != nullptr) return this->mScreen->devicePixelRatio();
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
void ProxyWindowBase::onMaskChanged() {
|
||||
if (this->window != nullptr) this->updateMask();
|
||||
}
|
||||
|
||||
void ProxyWindowBase::onMaskDestroyed() {
|
||||
this->mMask = nullptr;
|
||||
this->onMaskChanged();
|
||||
emit this->maskChanged();
|
||||
}
|
||||
|
||||
void ProxyWindowBase::updateMask() {
|
||||
this->pendingPolish.inputMask = true;
|
||||
this->schedulePolish();
|
||||
QRegion mask;
|
||||
if (this->mMask != nullptr) {
|
||||
// if left as the default, dont combine it with the whole window area, leave it as is.
|
||||
if (this->mMask->mIntersection == Intersection::Combine) {
|
||||
mask = this->mMask->build();
|
||||
} else {
|
||||
auto windowRegion = QRegion(QRect(0, 0, this->width(), this->height()));
|
||||
mask = this->mMask->applyTo(windowRegion);
|
||||
}
|
||||
}
|
||||
|
||||
this->window->setFlag(Qt::WindowTransparentForInput, this->mMask != nullptr && mask.isEmpty());
|
||||
this->window->setMask(mask);
|
||||
}
|
||||
|
||||
QQmlListProperty<QObject> ProxyWindowBase::data() {
|
||||
|
@ -439,57 +340,3 @@ QQmlListProperty<QObject> ProxyWindowBase::data() {
|
|||
|
||||
void ProxyWindowBase::onWidthChanged() { this->mContentItem->setWidth(this->width()); }
|
||||
void ProxyWindowBase::onHeightChanged() { this->mContentItem->setHeight(this->height()); }
|
||||
|
||||
ProxyWindowAttached::ProxyWindowAttached(QQuickItem* parent): QsWindowAttached(parent) {
|
||||
this->updateWindow();
|
||||
}
|
||||
|
||||
QObject* ProxyWindowAttached::window() const { return this->mWindow; }
|
||||
QQuickItem* ProxyWindowAttached::contentItem() const { return this->mWindow->contentItem(); }
|
||||
|
||||
void ProxyWindowAttached::updateWindow() {
|
||||
auto* window = static_cast<QQuickItem*>(this->parent())->window(); // NOLINT
|
||||
|
||||
if (auto* proxy = qobject_cast<ProxiedWindow*>(window)) {
|
||||
this->setWindow(proxy->proxy());
|
||||
} else {
|
||||
this->setWindow(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void ProxyWindowAttached::setWindow(ProxyWindowBase* window) {
|
||||
if (window == this->mWindow) return;
|
||||
this->mWindow = window;
|
||||
emit this->windowChanged();
|
||||
}
|
||||
|
||||
bool ProxiedWindow::event(QEvent* event) {
|
||||
if (event->type() == QEvent::DevicePixelRatioChange) {
|
||||
emit this->devicePixelRatioChanged();
|
||||
}
|
||||
|
||||
return this->QQuickWindow::event(event);
|
||||
}
|
||||
|
||||
void ProxiedWindow::exposeEvent(QExposeEvent* event) {
|
||||
this->QQuickWindow::exposeEvent(event);
|
||||
emit this->exposed();
|
||||
}
|
||||
|
||||
void ProxyWindowContentItem::updatePolish() { emit this->polished(); }
|
||||
|
||||
void ProxyWindowBase::onPolished() {
|
||||
if (this->pendingPolish.inputMask) {
|
||||
QRegion mask;
|
||||
if (this->mMask != nullptr) {
|
||||
mask = this->mMask->applyTo(QRect(0, 0, this->width(), this->height()));
|
||||
}
|
||||
|
||||
this->window->setFlag(Qt::WindowTransparentForInput, this->mMask != nullptr && mask.isEmpty());
|
||||
this->window->setMask(mask);
|
||||
|
||||
this->pendingPolish.inputMask = false;
|
||||
}
|
||||
|
||||
emit this->polished();
|
||||
}
|
|
@ -9,19 +9,15 @@
|
|||
#include <qqmlparserstatus.h>
|
||||
#include <qquickitem.h>
|
||||
#include <qquickwindow.h>
|
||||
#include <qsurfaceformat.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
#include <qwindow.h>
|
||||
|
||||
#include "../core/qmlscreen.hpp"
|
||||
#include "../core/region.hpp"
|
||||
#include "../core/reload.hpp"
|
||||
#include "qmlglobal.hpp"
|
||||
#include "qmlscreen.hpp"
|
||||
#include "region.hpp"
|
||||
#include "reload.hpp"
|
||||
#include "windowinterface.hpp"
|
||||
|
||||
class ProxiedWindow;
|
||||
class ProxyWindowContentItem;
|
||||
|
||||
// Proxy to an actual window exposing a limited property set with the ability to
|
||||
// transfer it to a new window.
|
||||
|
||||
|
@ -31,7 +27,6 @@ class ProxyWindowContentItem;
|
|||
/// [FloatingWindow]: ../floatingwindow
|
||||
class ProxyWindowBase: public Reloadable {
|
||||
Q_OBJECT;
|
||||
// clang-format off
|
||||
/// The QtQuick window backing this window.
|
||||
///
|
||||
/// > [!WARNING] Do not expect values set via this property to work correctly.
|
||||
|
@ -44,15 +39,12 @@ class ProxyWindowBase: public Reloadable {
|
|||
Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged);
|
||||
Q_PROPERTY(qint32 width READ width WRITE setWidth NOTIFY widthChanged);
|
||||
Q_PROPERTY(qint32 height READ height WRITE setHeight NOTIFY heightChanged);
|
||||
Q_PROPERTY(qreal devicePixelRatio READ devicePixelRatio NOTIFY devicePixelRatioChanged);
|
||||
Q_PROPERTY(QuickshellScreenInfo* screen READ screen WRITE setScreen NOTIFY screenChanged);
|
||||
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged);
|
||||
Q_PROPERTY(PendingRegion* mask READ mask WRITE setMask NOTIFY maskChanged);
|
||||
Q_PROPERTY(QObject* windowTransform READ windowTransform NOTIFY windowTransformChanged);
|
||||
Q_PROPERTY(bool backingWindowVisible READ isVisibleDirect NOTIFY backerVisibilityChanged);
|
||||
Q_PROPERTY(QsSurfaceFormat surfaceFormat READ surfaceFormat WRITE setSurfaceFormat NOTIFY surfaceFormatChanged);
|
||||
Q_PROPERTY(QQmlListProperty<QObject> data READ data);
|
||||
// clang-format on
|
||||
Q_CLASSINFO("DefaultProperty", "data");
|
||||
|
||||
public:
|
||||
|
@ -65,15 +57,14 @@ public:
|
|||
void operator=(ProxyWindowBase&&) = delete;
|
||||
|
||||
void onReload(QObject* oldInstance) override;
|
||||
void ensureQWindow();
|
||||
void createWindow();
|
||||
void deleteWindow(bool keepItemOwnership = false);
|
||||
void deleteWindow();
|
||||
|
||||
// Disown the backing window and delete all its children.
|
||||
virtual ProxiedWindow* disownWindow(bool keepItemOwnership = false);
|
||||
virtual QQuickWindow* disownWindow();
|
||||
|
||||
virtual ProxiedWindow* retrieveWindow(QObject* oldInstance);
|
||||
virtual ProxiedWindow* createQQuickWindow();
|
||||
virtual QQuickWindow* retrieveWindow(QObject* oldInstance);
|
||||
virtual QQuickWindow* createQQuickWindow();
|
||||
virtual void connectWindow();
|
||||
virtual void completeWindow();
|
||||
virtual void postCompleteWindow();
|
||||
|
@ -87,8 +78,6 @@ public:
|
|||
virtual void setVisible(bool visible);
|
||||
virtual void setVisibleDirect(bool visible);
|
||||
|
||||
void schedulePolish();
|
||||
|
||||
[[nodiscard]] virtual qint32 x() const;
|
||||
[[nodiscard]] virtual qint32 y() const;
|
||||
|
||||
|
@ -98,10 +87,7 @@ public:
|
|||
[[nodiscard]] virtual qint32 height() const;
|
||||
virtual void setHeight(qint32 height);
|
||||
|
||||
[[nodiscard]] qreal devicePixelRatio() const;
|
||||
|
||||
[[nodiscard]] QScreen* qscreen() const;
|
||||
[[nodiscard]] QuickshellScreenInfo* screen() const;
|
||||
[[nodiscard]] virtual QuickshellScreenInfo* screen() const;
|
||||
virtual void setScreen(QuickshellScreenInfo* screen);
|
||||
|
||||
[[nodiscard]] QColor color() const;
|
||||
|
@ -110,9 +96,6 @@ public:
|
|||
[[nodiscard]] PendingRegion* mask() const;
|
||||
virtual void setMask(PendingRegion* mask);
|
||||
|
||||
[[nodiscard]] QsSurfaceFormat surfaceFormat() const { return this->qsSurfaceFormat; }
|
||||
void setSurfaceFormat(QsSurfaceFormat format);
|
||||
|
||||
[[nodiscard]] QObject* windowTransform() const { return nullptr; } // NOLINT
|
||||
|
||||
[[nodiscard]] QQmlListProperty<QObject> data();
|
||||
|
@ -126,13 +109,10 @@ signals:
|
|||
void yChanged();
|
||||
void widthChanged();
|
||||
void heightChanged();
|
||||
void devicePixelRatioChanged();
|
||||
void windowTransformChanged();
|
||||
void screenChanged();
|
||||
void colorChanged();
|
||||
void maskChanged();
|
||||
void surfaceFormatChanged();
|
||||
void polished();
|
||||
|
||||
protected slots:
|
||||
virtual void onWidthChanged();
|
||||
|
@ -140,8 +120,6 @@ protected slots:
|
|||
void onMaskChanged();
|
||||
void onMaskDestroyed();
|
||||
void onScreenDestroyed();
|
||||
void onPolished();
|
||||
void runLints();
|
||||
|
||||
protected:
|
||||
bool mVisible = true;
|
||||
|
@ -150,69 +128,10 @@ protected:
|
|||
QScreen* mScreen = nullptr;
|
||||
QColor mColor = Qt::white;
|
||||
PendingRegion* mMask = nullptr;
|
||||
ProxiedWindow* window = nullptr;
|
||||
ProxyWindowContentItem* mContentItem = nullptr;
|
||||
QQuickWindow* window = nullptr;
|
||||
QQuickItem* mContentItem = nullptr;
|
||||
bool reloadComplete = false;
|
||||
bool ranLints = false;
|
||||
QsSurfaceFormat qsSurfaceFormat;
|
||||
QSurfaceFormat mSurfaceFormat;
|
||||
|
||||
struct {
|
||||
bool inputMask : 1 = false;
|
||||
} pendingPolish;
|
||||
|
||||
private:
|
||||
void polishItems();
|
||||
void updateMask();
|
||||
};
|
||||
|
||||
class ProxyWindowAttached: public QsWindowAttached {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
explicit ProxyWindowAttached(QQuickItem* parent);
|
||||
|
||||
[[nodiscard]] QObject* window() const override;
|
||||
[[nodiscard]] QQuickItem* contentItem() const override;
|
||||
|
||||
protected:
|
||||
void updateWindow() override;
|
||||
|
||||
private:
|
||||
ProxyWindowBase* mWindow = nullptr;
|
||||
|
||||
void setWindow(ProxyWindowBase* window);
|
||||
};
|
||||
|
||||
class ProxiedWindow: public QQuickWindow {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
explicit ProxiedWindow(ProxyWindowBase* proxy, QWindow* parent = nullptr)
|
||||
: QQuickWindow(parent)
|
||||
, mProxy(proxy) {}
|
||||
|
||||
[[nodiscard]] ProxyWindowBase* proxy() const { return this->mProxy; }
|
||||
void setProxy(ProxyWindowBase* proxy) { this->mProxy = proxy; }
|
||||
|
||||
signals:
|
||||
void exposed();
|
||||
void devicePixelRatioChanged();
|
||||
|
||||
protected:
|
||||
bool event(QEvent* event) override;
|
||||
void exposeEvent(QExposeEvent* event) override;
|
||||
|
||||
private:
|
||||
ProxyWindowBase* mProxy;
|
||||
};
|
||||
|
||||
class ProxyWindowContentItem: public QQuickItem {
|
||||
Q_OBJECT;
|
||||
|
||||
signals:
|
||||
void polished();
|
||||
|
||||
protected:
|
||||
void updatePolish() override;
|
||||
};
|
|
@ -5,7 +5,6 @@
|
|||
#include <qcoreapplication.h>
|
||||
#include <qdir.h>
|
||||
#include <qguiapplication.h>
|
||||
#include <qicon.h>
|
||||
#include <qjsengine.h>
|
||||
#include <qlogging.h>
|
||||
#include <qobject.h>
|
||||
|
@ -20,7 +19,6 @@
|
|||
#include <unistd.h>
|
||||
|
||||
#include "generation.hpp"
|
||||
#include "iconimageprovider.hpp"
|
||||
#include "qmlscreen.hpp"
|
||||
#include "rootwrapper.hpp"
|
||||
|
||||
|
@ -167,12 +165,6 @@ void QuickshellGlobal::reload(bool hard) {
|
|||
root->reloadGraph(hard);
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::shellRoot() const {
|
||||
auto* generation = EngineGeneration::findObjectGeneration(this);
|
||||
// already canonical
|
||||
return generation->rootPath.path();
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::workingDirectory() const { // NOLINT
|
||||
return QuickshellSettings::instance()->workingDirectory();
|
||||
}
|
||||
|
@ -195,27 +187,3 @@ QVariant QuickshellGlobal::env(const QString& variable) { // NOLINT
|
|||
|
||||
return qEnvironmentVariable(vstr.data());
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::iconPath(const QString& icon) {
|
||||
return IconImageProvider::requestString(icon);
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::iconPath(const QString& icon, bool check) {
|
||||
if (check && QIcon::fromTheme(icon).isNull()) return "";
|
||||
return IconImageProvider::requestString(icon);
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::iconPath(const QString& icon, const QString& fallback) {
|
||||
return IconImageProvider::requestString(icon, "", fallback);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -98,11 +98,6 @@ class QuickshellGlobal: public QObject {
|
|||
/// This creates an instance of your window once on every screen.
|
||||
/// As screens are added or removed your window will be created or destroyed on those screens.
|
||||
Q_PROPERTY(QQmlListProperty<QuickshellScreenInfo> screens READ screens NOTIFY screensChanged);
|
||||
/// The full path to the root directory of your shell.
|
||||
///
|
||||
/// The root directory is the folder containing the entrypoint to your shell, often referred
|
||||
/// to as `shell.qml`.
|
||||
Q_PROPERTY(QString shellRoot READ shellRoot CONSTANT);
|
||||
/// Quickshell's working directory. Defaults to whereever quickshell was launched from.
|
||||
Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged);
|
||||
/// If true then the configuration will be reloaded whenever any files change.
|
||||
|
@ -115,62 +110,40 @@ class QuickshellGlobal: public QObject {
|
|||
public:
|
||||
[[nodiscard]] qint32 processId() const;
|
||||
|
||||
QuickshellGlobal(QObject* parent = nullptr);
|
||||
|
||||
QQmlListProperty<QuickshellScreenInfo> screens();
|
||||
|
||||
/// Reload the shell.
|
||||
/// Reload the shell from the [ShellRoot].
|
||||
///
|
||||
/// `hard` - perform a hard reload. If this is false, Quickshell will attempt to reuse windows
|
||||
/// that already exist. If true windows will be recreated.
|
||||
///
|
||||
/// See @@Reloadable for more information on what can be reloaded and how.
|
||||
/// See [Reloadable] for more information on what can be reloaded and how.
|
||||
///
|
||||
/// [Reloadable]: ../reloadable
|
||||
Q_INVOKABLE void reload(bool hard);
|
||||
|
||||
/// Returns the string value of an environment variable or null if it is not set.
|
||||
Q_INVOKABLE QVariant env(const QString& variable);
|
||||
|
||||
/// Returns a string usable for a @@QtQuick.Image.source for a given system icon.
|
||||
///
|
||||
/// > [!INFO] By default, icons are loaded from the theme selected by the qt platform theme,
|
||||
/// > which means they should match with all other qt applications on your system.
|
||||
/// >
|
||||
/// > If you want to use a different icon theme, you can put `//@ pragma IconTheme <name>`
|
||||
/// > at the top of your root config file or set the `QS_ICON_THEME` variable to the name
|
||||
/// > of your icon theme.
|
||||
Q_INVOKABLE static QString iconPath(const QString& icon);
|
||||
/// Setting the `check` parameter of `iconPath` to true will return an empty string
|
||||
/// if the icon does not exist, instead of an image showing a missing texture.
|
||||
Q_INVOKABLE static QString iconPath(const QString& icon, bool check);
|
||||
/// Setting the `fallback` parameter of `iconPath` will attempt to load the fallback
|
||||
/// icon if the requested one could not be loaded.
|
||||
Q_INVOKABLE static QString iconPath(const QString& icon, const QString& fallback);
|
||||
|
||||
[[nodiscard]] QString shellRoot() const;
|
||||
|
||||
[[nodiscard]] QString workingDirectory() const;
|
||||
void setWorkingDirectory(QString workingDirectory);
|
||||
|
||||
[[nodiscard]] bool watchFiles() const;
|
||||
void setWatchFiles(bool watchFiles);
|
||||
|
||||
static QuickshellGlobal* create(QQmlEngine* engine, QJSEngine* /*unused*/);
|
||||
|
||||
signals:
|
||||
/// Sent when the last window is closed.
|
||||
///
|
||||
/// To make the application exit when the last window is closed run `Qt.quit()`.
|
||||
void lastWindowClosed();
|
||||
/// The reload sequence has completed successfully.
|
||||
void reloadCompleted();
|
||||
/// The reload sequence has failed.
|
||||
void reloadFailed(QString errorString);
|
||||
|
||||
void screensChanged();
|
||||
void workingDirectoryChanged();
|
||||
void watchFilesChanged();
|
||||
|
||||
private:
|
||||
QuickshellGlobal(QObject* parent = nullptr);
|
||||
|
||||
static qsizetype screensCount(QQmlListProperty<QuickshellScreenInfo>* prop);
|
||||
static QuickshellScreenInfo* screenAt(QQmlListProperty<QuickshellScreenInfo>* prop, qsizetype i);
|
||||
};
|
||||
|
|
|
@ -42,24 +42,6 @@ QString QuickshellScreenInfo::name() const {
|
|||
return this->screen->name();
|
||||
}
|
||||
|
||||
QString QuickshellScreenInfo::model() const {
|
||||
if (this->screen == nullptr) {
|
||||
this->warnDangling();
|
||||
return "{ NULL SCREEN }";
|
||||
}
|
||||
|
||||
return this->screen->model();
|
||||
}
|
||||
|
||||
QString QuickshellScreenInfo::serialNumber() const {
|
||||
if (this->screen == nullptr) {
|
||||
this->warnDangling();
|
||||
return "{ NULL SCREEN }";
|
||||
}
|
||||
|
||||
return this->screen->serialNumber();
|
||||
}
|
||||
|
||||
qint32 QuickshellScreenInfo::x() const {
|
||||
if (this->screen == nullptr) {
|
||||
this->warnDangling();
|
||||
|
|
|
@ -12,14 +12,17 @@
|
|||
|
||||
// unfortunately QQuickScreenInfo is private.
|
||||
|
||||
/// Monitor object useful for setting the monitor for a @@QsWindow
|
||||
/// Monitor object useful for setting the monitor for a [ShellWindow]
|
||||
/// or querying information about the monitor.
|
||||
///
|
||||
/// > [!WARNING] If the monitor is disconnected than any stored copies of its ShellMonitor will
|
||||
/// > be marked as dangling and all properties will return default values.
|
||||
/// > Reconnecting the monitor will not reconnect it to the ShellMonitor object.
|
||||
///
|
||||
/// Due to some technical limitations, it was not possible to reuse the native qml @@QtQuick.Screen type.
|
||||
/// Due to some technical limitations, it was not possible to reuse the native qml [Screen] type.
|
||||
///
|
||||
/// [ShellWindow]: ../shellwindow
|
||||
/// [Screen]: https://doc.qt.io/qt-6/qml-qtquick-screen.html
|
||||
class QuickshellScreenInfo: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_NAMED_ELEMENT(ShellScreen);
|
||||
|
@ -29,10 +32,6 @@ class QuickshellScreenInfo: public QObject {
|
|||
///
|
||||
/// Usually something like `DP-1`, `HDMI-1`, `eDP-1`.
|
||||
Q_PROPERTY(QString name READ name CONSTANT);
|
||||
/// The model of the screen as seen by the operating system.
|
||||
Q_PROPERTY(QString model READ model CONSTANT);
|
||||
/// The serial number of the screen as seen by the operating system.
|
||||
Q_PROPERTY(QString serialNumber READ serialNumber CONSTANT);
|
||||
Q_PROPERTY(qint32 x READ x NOTIFY geometryChanged);
|
||||
Q_PROPERTY(qint32 y READ y NOTIFY geometryChanged);
|
||||
Q_PROPERTY(qint32 width READ width NOTIFY geometryChanged);
|
||||
|
@ -44,7 +43,7 @@ class QuickshellScreenInfo: public QObject {
|
|||
/// The ratio between physical pixels and device-independent (scaled) pixels.
|
||||
Q_PROPERTY(qreal devicePixelRatio READ devicePixelRatio NOTIFY physicalPixelDensityChanged);
|
||||
Q_PROPERTY(Qt::ScreenOrientation orientation READ orientation NOTIFY orientationChanged);
|
||||
Q_PROPERTY(Qt::ScreenOrientation primaryOrientation READ primaryOrientation NOTIFY primaryOrientationChanged);
|
||||
Q_PROPERTY(Qt::ScreenOrientation primatyOrientation READ primaryOrientation NOTIFY primaryOrientationChanged);
|
||||
// clang-format on
|
||||
|
||||
public:
|
||||
|
@ -53,8 +52,6 @@ public:
|
|||
bool operator==(QuickshellScreenInfo& other) const;
|
||||
|
||||
[[nodiscard]] QString name() const;
|
||||
[[nodiscard]] QString model() const;
|
||||
[[nodiscard]] QString serialNumber() const;
|
||||
[[nodiscard]] qint32 x() const;
|
||||
[[nodiscard]] qint32 y() const;
|
||||
[[nodiscard]] qint32 width() const;
|
||||
|
|
|
@ -16,22 +16,7 @@
|
|||
|
||||
Q_LOGGING_CATEGORY(logQsIntercept, "quickshell.interceptor", QtWarningMsg);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
QUrl QsUrlInterceptor::intercept(const QUrl& url, QQmlAbstractUrlInterceptor::DataType type) {
|
||||
// Some types such as Image take into account where they are loading from, and force
|
||||
// asynchronous loading over a network. qsintercept is considered to be over a network.
|
||||
if (type == QQmlAbstractUrlInterceptor::DataType::UrlString && url.scheme() == "qsintercept") {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include <qdir.h>
|
||||
#include <qhash.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qnetworkaccessmanager.h>
|
||||
|
@ -14,12 +13,7 @@ Q_DECLARE_LOGGING_CATEGORY(logQsIntercept);
|
|||
|
||||
class QsUrlInterceptor: public QQmlAbstractUrlInterceptor {
|
||||
public:
|
||||
explicit QsUrlInterceptor(const QDir& configRoot): configRoot(configRoot) {}
|
||||
|
||||
QUrl intercept(const QUrl& originalUrl, QQmlAbstractUrlInterceptor::DataType type) override;
|
||||
|
||||
private:
|
||||
QDir configRoot;
|
||||
QUrl intercept(const QUrl& url, QQmlAbstractUrlInterceptor::DataType type) override;
|
||||
};
|
||||
|
||||
class QsInterceptDataReply: public QNetworkReply {
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
#include "qsmenu.hpp"
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmllist.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "model.hpp"
|
||||
#include "platformmenu.hpp"
|
||||
|
||||
using namespace qs::menu::platform;
|
||||
|
||||
namespace qs::menu {
|
||||
|
||||
QString QsMenuButtonType::toString(QsMenuButtonType::Enum value) {
|
||||
switch (value) {
|
||||
case QsMenuButtonType::None: return "None";
|
||||
case QsMenuButtonType::CheckBox: return "CheckBox";
|
||||
case QsMenuButtonType::RadioButton: return "RadioButton";
|
||||
default: return "Invalid button type";
|
||||
}
|
||||
}
|
||||
|
||||
QsMenuEntry* QsMenuEntry::menu() { return this; }
|
||||
|
||||
void QsMenuEntry::display(QObject* parentWindow, int relativeX, int relativeY) {
|
||||
auto* platform = new PlatformMenuEntry(this);
|
||||
|
||||
QObject::connect(platform, &PlatformMenuEntry::closed, platform, [=]() {
|
||||
platform->deleteLater();
|
||||
});
|
||||
|
||||
auto success = platform->display(parentWindow, relativeX, relativeY);
|
||||
if (!success) delete platform;
|
||||
}
|
||||
|
||||
void QsMenuEntry::ref() {
|
||||
this->refcount++;
|
||||
if (this->refcount == 1) emit this->opened();
|
||||
}
|
||||
|
||||
void QsMenuEntry::unref() {
|
||||
this->refcount--;
|
||||
if (this->refcount == 0) emit this->closed();
|
||||
}
|
||||
|
||||
ObjectModel<QsMenuEntry>* QsMenuEntry::children() {
|
||||
return ObjectModel<QsMenuEntry>::emptyInstance();
|
||||
}
|
||||
|
||||
QsMenuOpener::~QsMenuOpener() {
|
||||
if (this->mMenu) {
|
||||
if (this->mMenu->menu()) this->mMenu->menu()->unref();
|
||||
this->mMenu->unrefHandle();
|
||||
}
|
||||
}
|
||||
|
||||
QsMenuHandle* QsMenuOpener::menu() const { return this->mMenu; }
|
||||
|
||||
void QsMenuOpener::setMenu(QsMenuHandle* menu) {
|
||||
if (menu == this->mMenu) return;
|
||||
|
||||
if (this->mMenu != nullptr) {
|
||||
QObject::disconnect(this->mMenu, nullptr, this, nullptr);
|
||||
|
||||
if (this->mMenu->menu()) {
|
||||
QObject::disconnect(this->mMenu->menu(), nullptr, this, nullptr);
|
||||
this->mMenu->menu()->unref();
|
||||
}
|
||||
|
||||
this->mMenu->unrefHandle();
|
||||
}
|
||||
|
||||
this->mMenu = menu;
|
||||
|
||||
if (menu != nullptr) {
|
||||
auto onMenuChanged = [this, menu]() {
|
||||
if (menu->menu()) {
|
||||
menu->menu()->ref();
|
||||
}
|
||||
|
||||
emit this->childrenChanged();
|
||||
};
|
||||
|
||||
QObject::connect(menu, &QObject::destroyed, this, &QsMenuOpener::onMenuDestroyed);
|
||||
QObject::connect(menu, &QsMenuHandle::menuChanged, this, onMenuChanged);
|
||||
|
||||
if (menu->menu()) onMenuChanged();
|
||||
menu->refHandle();
|
||||
}
|
||||
|
||||
emit this->menuChanged();
|
||||
emit this->childrenChanged();
|
||||
}
|
||||
|
||||
void QsMenuOpener::onMenuDestroyed() {
|
||||
this->mMenu = nullptr;
|
||||
emit this->menuChanged();
|
||||
emit this->childrenChanged();
|
||||
}
|
||||
|
||||
ObjectModel<QsMenuEntry>* QsMenuOpener::children() {
|
||||
if (this->mMenu && this->mMenu->menu()) {
|
||||
return this->mMenu->menu()->children();
|
||||
} else {
|
||||
return ObjectModel<QsMenuEntry>::emptyInstance();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace qs::menu
|
|
@ -1,163 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qnamespace.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qqmllist.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "doc.hpp"
|
||||
#include "model.hpp"
|
||||
|
||||
namespace qs::menu {
|
||||
|
||||
///! Button type associated with a QsMenuEntry.
|
||||
/// See @@QsMenuEntry.buttonType.
|
||||
class QsMenuButtonType: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
QML_SINGLETON;
|
||||
|
||||
public:
|
||||
enum Enum : quint8 {
|
||||
/// This menu item does not have a checkbox or a radiobutton associated with it.
|
||||
None = 0,
|
||||
/// This menu item should draw a checkbox.
|
||||
CheckBox = 1,
|
||||
/// This menu item should draw a radiobutton.
|
||||
RadioButton = 2,
|
||||
};
|
||||
Q_ENUM(Enum);
|
||||
|
||||
Q_INVOKABLE static QString toString(qs::menu::QsMenuButtonType::Enum value);
|
||||
};
|
||||
|
||||
class QsMenuEntry;
|
||||
|
||||
///! Menu handle for QsMenuOpener
|
||||
/// See @@QsMenuOpener.
|
||||
class QsMenuHandle: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
QML_UNCREATABLE("");
|
||||
|
||||
public:
|
||||
explicit QsMenuHandle(QObject* parent): QObject(parent) {}
|
||||
|
||||
virtual void refHandle() {};
|
||||
virtual void unrefHandle() {};
|
||||
|
||||
[[nodiscard]] virtual QsMenuEntry* menu() = 0;
|
||||
|
||||
signals:
|
||||
void menuChanged();
|
||||
};
|
||||
|
||||
class QsMenuEntry: public QsMenuHandle {
|
||||
Q_OBJECT;
|
||||
/// If this menu item should be rendered as a separator between other items.
|
||||
///
|
||||
/// No other properties have a meaningful value when @@isSeparator is true.
|
||||
Q_PROPERTY(bool isSeparator READ isSeparator NOTIFY isSeparatorChanged);
|
||||
Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged);
|
||||
/// Text of the menu item.
|
||||
Q_PROPERTY(QString text READ text NOTIFY textChanged);
|
||||
/// Url of the menu item's icon or `""` if it doesn't have one.
|
||||
///
|
||||
/// This can be passed to [Image.source](https://doc.qt.io/qt-6/qml-qtquick-image.html#source-prop)
|
||||
/// as shown below.
|
||||
///
|
||||
/// ```qml
|
||||
/// Image {
|
||||
/// source: menuItem.icon
|
||||
/// // To get the best image quality, set the image source size to the same size
|
||||
/// // as the rendered image.
|
||||
/// sourceSize.width: width
|
||||
/// sourceSize.height: height
|
||||
/// }
|
||||
/// ```
|
||||
Q_PROPERTY(QString icon READ icon NOTIFY iconChanged);
|
||||
/// If this menu item has an associated checkbox or radiobutton.
|
||||
Q_PROPERTY(qs::menu::QsMenuButtonType::Enum buttonType READ buttonType NOTIFY buttonTypeChanged);
|
||||
/// The check state of the checkbox or radiobutton if applicable, as a
|
||||
/// [Qt.CheckState](https://doc.qt.io/qt-6/qt.html#CheckState-enum).
|
||||
Q_PROPERTY(Qt::CheckState checkState READ checkState NOTIFY checkStateChanged);
|
||||
/// If this menu item has children that can be accessed through a @@QsMenuOpener$.
|
||||
Q_PROPERTY(bool hasChildren READ hasChildren NOTIFY hasChildrenChanged);
|
||||
QML_ELEMENT;
|
||||
QML_UNCREATABLE("QsMenuEntry cannot be directly created");
|
||||
|
||||
public:
|
||||
explicit QsMenuEntry(QObject* parent): QsMenuHandle(parent) {}
|
||||
|
||||
[[nodiscard]] QsMenuEntry* menu() override;
|
||||
|
||||
/// Display a platform menu at the given location relative to the parent window.
|
||||
Q_INVOKABLE void display(QObject* parentWindow, qint32 relativeX, qint32 relativeY);
|
||||
|
||||
[[nodiscard]] virtual bool isSeparator() const { return false; }
|
||||
[[nodiscard]] virtual bool enabled() const { return true; }
|
||||
[[nodiscard]] virtual QString text() const { return ""; }
|
||||
[[nodiscard]] virtual QString icon() const { return ""; }
|
||||
[[nodiscard]] virtual QsMenuButtonType::Enum buttonType() const { return QsMenuButtonType::None; }
|
||||
[[nodiscard]] virtual Qt::CheckState checkState() const { return Qt::Unchecked; }
|
||||
[[nodiscard]] virtual bool hasChildren() const { return false; }
|
||||
|
||||
void ref();
|
||||
void unref();
|
||||
|
||||
[[nodiscard]] virtual ObjectModel<QsMenuEntry>* children();
|
||||
|
||||
signals:
|
||||
/// Send a trigger/click signal to the menu entry.
|
||||
void triggered();
|
||||
|
||||
QSDOC_HIDE void opened();
|
||||
QSDOC_HIDE void closed();
|
||||
|
||||
void isSeparatorChanged();
|
||||
void enabledChanged();
|
||||
void textChanged();
|
||||
void iconChanged();
|
||||
void buttonTypeChanged();
|
||||
void checkStateChanged();
|
||||
void hasChildrenChanged();
|
||||
|
||||
private:
|
||||
qsizetype refcount = 0;
|
||||
};
|
||||
|
||||
///! Provides access to children of a QsMenuEntry
|
||||
class QsMenuOpener: public QObject {
|
||||
Q_OBJECT;
|
||||
/// The menu to retrieve children from.
|
||||
Q_PROPERTY(qs::menu::QsMenuHandle* menu READ menu WRITE setMenu NOTIFY menuChanged);
|
||||
/// The children of the given menu.
|
||||
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::menu::QsMenuEntry>*);
|
||||
Q_PROPERTY(UntypedObjectModel* children READ children NOTIFY childrenChanged);
|
||||
QML_ELEMENT;
|
||||
|
||||
public:
|
||||
explicit QsMenuOpener(QObject* parent = nullptr): QObject(parent) {}
|
||||
~QsMenuOpener() override;
|
||||
Q_DISABLE_COPY_MOVE(QsMenuOpener);
|
||||
|
||||
[[nodiscard]] QsMenuHandle* menu() const;
|
||||
void setMenu(QsMenuHandle* menu);
|
||||
|
||||
[[nodiscard]] ObjectModel<QsMenuEntry>* children();
|
||||
|
||||
signals:
|
||||
void menuChanged();
|
||||
void childrenChanged();
|
||||
|
||||
private slots:
|
||||
void onMenuDestroyed();
|
||||
|
||||
private:
|
||||
QsMenuHandle* mMenu = nullptr;
|
||||
};
|
||||
|
||||
} // namespace qs::menu
|
|
@ -1,125 +0,0 @@
|
|||
#include "qsmenuanchor.hpp"
|
||||
|
||||
#include <qapplication.h>
|
||||
#include <qcoreapplication.h>
|
||||
#include <qlogging.h>
|
||||
#include <qobject.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "platformmenu.hpp"
|
||||
#include "popupanchor.hpp"
|
||||
#include "qsmenu.hpp"
|
||||
|
||||
using qs::menu::platform::PlatformMenuEntry;
|
||||
|
||||
namespace qs::menu {
|
||||
|
||||
QsMenuAnchor::~QsMenuAnchor() { this->onClosed(); }
|
||||
|
||||
void QsMenuAnchor::open() {
|
||||
if (qobject_cast<QApplication*>(QCoreApplication::instance()) == nullptr) {
|
||||
qCritical() << "Cannot call QsMenuAnchor.open() as quickshell was not started in "
|
||||
"QApplication mode.";
|
||||
qCritical() << "To use platform menus, add `//@ pragma UseQApplication` to the top of your "
|
||||
"root QML file and restart quickshell.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->mOpen) {
|
||||
qCritical() << "Cannot call QsMenuAnchor.open() as it is already open.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->mMenu) {
|
||||
qCritical() << "Cannot open QsMenuAnchor with no menu attached.";
|
||||
return;
|
||||
}
|
||||
|
||||
this->mOpen = true;
|
||||
|
||||
if (this->mMenu->menu()) this->onMenuChanged();
|
||||
QObject::connect(this->mMenu, &QsMenuHandle::menuChanged, this, &QsMenuAnchor::onMenuChanged);
|
||||
this->mMenu->refHandle();
|
||||
|
||||
emit this->visibleChanged();
|
||||
}
|
||||
|
||||
void QsMenuAnchor::onMenuChanged() {
|
||||
// close menu if the path changes
|
||||
if (this->platformMenu || !this->mMenu->menu()) {
|
||||
this->onClosed();
|
||||
return;
|
||||
}
|
||||
|
||||
this->platformMenu = new PlatformMenuEntry(this->mMenu->menu());
|
||||
QObject::connect(this->platformMenu, &PlatformMenuEntry::closed, this, &QsMenuAnchor::onClosed);
|
||||
|
||||
auto success = this->platformMenu->display(&this->mAnchor);
|
||||
if (!success) this->onClosed();
|
||||
else emit this->opened();
|
||||
}
|
||||
|
||||
void QsMenuAnchor::close() {
|
||||
if (!this->mOpen) {
|
||||
qCritical() << "Cannot close QsMenuAnchor as it isn't open.";
|
||||
return;
|
||||
}
|
||||
|
||||
this->onClosed();
|
||||
}
|
||||
|
||||
void QsMenuAnchor::onClosed() {
|
||||
if (!this->mOpen) return;
|
||||
|
||||
this->mOpen = false;
|
||||
|
||||
if (this->platformMenu) {
|
||||
this->platformMenu->deleteLater();
|
||||
this->platformMenu = nullptr;
|
||||
}
|
||||
|
||||
if (this->mMenu) {
|
||||
QObject::disconnect(
|
||||
this->mMenu,
|
||||
&QsMenuHandle::menuChanged,
|
||||
this,
|
||||
&QsMenuAnchor::onMenuChanged
|
||||
);
|
||||
|
||||
this->mMenu->unrefHandle();
|
||||
}
|
||||
|
||||
emit this->closed();
|
||||
emit this->visibleChanged();
|
||||
}
|
||||
|
||||
PopupAnchor* QsMenuAnchor::anchor() { return &this->mAnchor; }
|
||||
|
||||
QsMenuHandle* QsMenuAnchor::menu() const { return this->mMenu; }
|
||||
|
||||
void QsMenuAnchor::setMenu(QsMenuHandle* menu) {
|
||||
if (menu == this->mMenu) return;
|
||||
|
||||
if (this->mMenu != nullptr) {
|
||||
if (this->platformMenu != nullptr) this->platformMenu->deleteLater();
|
||||
QObject::disconnect(this->mMenu, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
this->mMenu = menu;
|
||||
|
||||
if (menu != nullptr) {
|
||||
QObject::connect(menu, &QObject::destroyed, this, &QsMenuAnchor::onMenuDestroyed);
|
||||
}
|
||||
|
||||
emit this->menuChanged();
|
||||
}
|
||||
|
||||
bool QsMenuAnchor::isVisible() const { return this->mOpen; }
|
||||
|
||||
void QsMenuAnchor::onMenuDestroyed() {
|
||||
this->mMenu = nullptr;
|
||||
this->onClosed();
|
||||
emit this->menuChanged();
|
||||
}
|
||||
|
||||
} // namespace qs::menu
|
|
@ -1,86 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtclasshelpermacros.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "platformmenu.hpp"
|
||||
#include "popupanchor.hpp"
|
||||
#include "qsmenu.hpp"
|
||||
|
||||
namespace qs::menu {
|
||||
|
||||
///! Display anchor for platform menus.
|
||||
class QsMenuAnchor: public QObject {
|
||||
Q_OBJECT;
|
||||
/// The menu's anchor / positioner relative to another window. The menu will not be
|
||||
/// shown until it has a valid anchor.
|
||||
///
|
||||
/// > [!INFO] *The following is subject to change and NOT a guarantee of future behavior.*
|
||||
/// >
|
||||
/// > A snapshot of the anchor at the time @@opened(s) is emitted will be
|
||||
/// > used to position the menu. Additional changes to the anchor after this point
|
||||
/// > will not affect the placement of the menu.
|
||||
///
|
||||
/// You can set properties of the anchor like so:
|
||||
/// ```qml
|
||||
/// QsMenuAnchor {
|
||||
/// anchor.window: parentwindow
|
||||
/// // or
|
||||
/// anchor {
|
||||
/// window: parentwindow
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
Q_PROPERTY(PopupAnchor* anchor READ anchor CONSTANT);
|
||||
/// The menu that should be displayed on this anchor.
|
||||
///
|
||||
/// See also: @@Quickshell.Services.SystemTray.SystemTrayItem.menu.
|
||||
Q_PROPERTY(qs::menu::QsMenuHandle* menu READ menu WRITE setMenu NOTIFY menuChanged);
|
||||
/// If the menu is currently open and visible.
|
||||
///
|
||||
/// See also: @@open(), @@close().
|
||||
Q_PROPERTY(bool visible READ isVisible NOTIFY visibleChanged);
|
||||
QML_ELEMENT;
|
||||
|
||||
public:
|
||||
explicit QsMenuAnchor(QObject* parent = nullptr): QObject(parent) {}
|
||||
~QsMenuAnchor() override;
|
||||
Q_DISABLE_COPY_MOVE(QsMenuAnchor);
|
||||
|
||||
/// Open the given menu on this menu Requires that @@anchor is valid.
|
||||
Q_INVOKABLE void open();
|
||||
/// Close the open menu.
|
||||
Q_INVOKABLE void close();
|
||||
|
||||
[[nodiscard]] PopupAnchor* anchor();
|
||||
|
||||
[[nodiscard]] QsMenuHandle* menu() const;
|
||||
void setMenu(QsMenuHandle* menu);
|
||||
|
||||
[[nodiscard]] bool isVisible() const;
|
||||
|
||||
signals:
|
||||
/// Sent when the menu is displayed onscreen which may be after @@visible
|
||||
/// becomes true.
|
||||
void opened();
|
||||
/// Sent when the menu is closed.
|
||||
void closed();
|
||||
|
||||
void menuChanged();
|
||||
void visibleChanged();
|
||||
|
||||
private slots:
|
||||
void onMenuChanged();
|
||||
void onMenuDestroyed();
|
||||
|
||||
private:
|
||||
void onClosed();
|
||||
|
||||
PopupAnchor mAnchor {this};
|
||||
QsMenuHandle* mMenu = nullptr;
|
||||
bool mOpen = false;
|
||||
platform::PlatformMenuEntry* platformMenu = nullptr;
|
||||
};
|
||||
|
||||
} // namespace qs::menu
|
|
@ -8,7 +8,6 @@
|
|||
#include <qregion.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
#include <qvectornd.h>
|
||||
|
||||
PendingRegion::PendingRegion(QObject* parent): QObject(parent) {
|
||||
QObject::connect(this, &PendingRegion::shapeChanged, this, &PendingRegion::changed);
|
||||
|
@ -106,19 +105,8 @@ QRegion PendingRegion::applyTo(QRegion& region) const {
|
|||
return region;
|
||||
}
|
||||
|
||||
QRegion PendingRegion::applyTo(const QRect& rect) const {
|
||||
// if left as the default, dont combine it with the whole rect area, leave it as is.
|
||||
if (this->mIntersection == Intersection::Combine) {
|
||||
return this->build();
|
||||
} else {
|
||||
auto baseRegion = QRegion(rect);
|
||||
return this->applyTo(baseRegion);
|
||||
}
|
||||
}
|
||||
|
||||
void PendingRegion::regionsAppend(QQmlListProperty<PendingRegion>* prop, PendingRegion* region) {
|
||||
auto* self = static_cast<PendingRegion*>(prop->object); // NOLINT
|
||||
if (!region) return;
|
||||
|
||||
QObject::connect(region, &QObject::destroyed, self, &PendingRegion::onChildDestroyed);
|
||||
QObject::connect(region, &PendingRegion::changed, self, &PendingRegion::childrenChanged);
|
||||
|
|
|
@ -9,13 +9,12 @@
|
|||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
///! Shape of a Region.
|
||||
/// See @@Region.shape.
|
||||
/// Shape of a Region.
|
||||
namespace RegionShape { // NOLINT
|
||||
Q_NAMESPACE;
|
||||
QML_ELEMENT;
|
||||
|
||||
enum Enum : quint8 {
|
||||
enum Enum {
|
||||
Rect = 0,
|
||||
Ellipse = 1,
|
||||
};
|
||||
|
@ -24,12 +23,11 @@ Q_ENUM_NS(Enum);
|
|||
} // namespace RegionShape
|
||||
|
||||
///! Intersection strategy for Regions.
|
||||
/// See @@Region.intersection.
|
||||
namespace Intersection { // NOLINT
|
||||
Q_NAMESPACE;
|
||||
QML_ELEMENT;
|
||||
|
||||
enum Enum : quint8 {
|
||||
enum Enum {
|
||||
/// Combine this region, leaving a union of this and the other region. (opposite of `Subtract`)
|
||||
Combine = 0,
|
||||
/// Subtract this region, cutting this region out of the other. (opposite of `Combine`)
|
||||
|
@ -46,7 +44,6 @@ Q_ENUM_NS(Enum);
|
|||
} // namespace Intersection
|
||||
|
||||
///! A composable region used as a mask.
|
||||
/// See @@QsWindow.mask.
|
||||
class PendingRegion: public QObject {
|
||||
Q_OBJECT;
|
||||
/// Defaults to `Rect`.
|
||||
|
@ -55,16 +52,16 @@ class PendingRegion: public QObject {
|
|||
Q_PROPERTY(Intersection::Enum intersection MEMBER mIntersection NOTIFY intersectionChanged);
|
||||
|
||||
/// The item that determines the geometry of the region.
|
||||
/// `item` overrides @@x, @@y, @@width and @@height.
|
||||
/// `item` overrides `x`, `y`, `width` and `height`.
|
||||
Q_PROPERTY(QQuickItem* item MEMBER mItem WRITE setItem NOTIFY itemChanged);
|
||||
|
||||
/// Defaults to 0. Does nothing if @@item is set.
|
||||
/// Defaults to 0. Does nothing if `item` is set.
|
||||
Q_PROPERTY(qint32 x MEMBER mX NOTIFY xChanged);
|
||||
/// Defaults to 0. Does nothing if @@item is set.
|
||||
/// Defaults to 0. Does nothing if `item` is set.
|
||||
Q_PROPERTY(qint32 y MEMBER mY NOTIFY yChanged);
|
||||
/// Defaults to 0. Does nothing if @@item is set.
|
||||
/// Defaults to 0. Does nothing if `item` is set.
|
||||
Q_PROPERTY(qint32 width MEMBER mWidth NOTIFY widthChanged);
|
||||
/// Defaults to 0. Does nothing if @@item is set.
|
||||
/// Defaults to 0. Does nothing if `item` is set.
|
||||
Q_PROPERTY(qint32 height MEMBER mHeight NOTIFY heightChanged);
|
||||
|
||||
/// Regions to apply on top of this region.
|
||||
|
@ -96,7 +93,6 @@ public:
|
|||
[[nodiscard]] bool empty() const;
|
||||
[[nodiscard]] QRegion build() const;
|
||||
[[nodiscard]] QRegion applyTo(QRegion& region) const;
|
||||
[[nodiscard]] QRegion applyTo(const QRect& rect) const;
|
||||
|
||||
RegionShape::Enum mShape = RegionShape::Rect;
|
||||
Intersection::Enum mIntersection = Intersection::Combine;
|
||||
|
@ -110,11 +106,6 @@ signals:
|
|||
void widthChanged();
|
||||
void heightChanged();
|
||||
void childrenChanged();
|
||||
|
||||
/// Triggered when the region's geometry changes.
|
||||
///
|
||||
/// In some cases the region does not update automatically.
|
||||
/// In those cases you can emit this signal manually.
|
||||
void changed();
|
||||
|
||||
private slots:
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
#include <qcontainerfwd.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmllist.h>
|
||||
#include <qtimer.h>
|
||||
|
||||
#include "generation.hpp"
|
||||
|
||||
|
@ -13,19 +12,8 @@ void Reloadable::componentComplete() {
|
|||
if (this->engineGeneration != nullptr) {
|
||||
// When called this way there is no chance a reload will have old data,
|
||||
// but this will at least help prevent weird behaviors due to never getting a reload.
|
||||
if (this->engineGeneration->reloadComplete) {
|
||||
// Delayed due to Component.onCompleted running after QQmlParserStatus::componentComplete.
|
||||
QTimer::singleShot(0, this, &Reloadable::onReloadFinished);
|
||||
|
||||
// This only matters for preventing the above timer from UAFing the generation,
|
||||
// so it isn't connected anywhere else.
|
||||
QObject::connect(
|
||||
this->engineGeneration,
|
||||
&QObject::destroyed,
|
||||
this,
|
||||
&Reloadable::onGenerationDestroyed
|
||||
);
|
||||
} else {
|
||||
if (this->engineGeneration->reloadComplete) this->reload();
|
||||
else {
|
||||
QObject::connect(
|
||||
this->engineGeneration,
|
||||
&EngineGeneration::reloadFinished,
|
||||
|
@ -52,7 +40,6 @@ void Reloadable::reload(QObject* oldInstance) {
|
|||
}
|
||||
|
||||
void Reloadable::onReloadFinished() { this->reload(nullptr); }
|
||||
void Reloadable::onGenerationDestroyed() { this->engineGeneration = nullptr; }
|
||||
|
||||
void ReloadPropagator::onReload(QObject* oldInstance) {
|
||||
auto* old = qobject_cast<ReloadPropagator*>(oldInstance);
|
||||
|
@ -99,7 +86,7 @@ void Reloadable::reloadRecursive(QObject* newObj, QObject* oldRoot) {
|
|||
|
||||
// pass handling to the child's onReload, which should call back into reloadRecursive,
|
||||
// with its oldInstance becoming the new oldRoot.
|
||||
reloadable->reload(oldInstance);
|
||||
reloadable->onReload(oldInstance);
|
||||
} else if (newObj != nullptr) {
|
||||
Reloadable::reloadChildrenRecursive(newObj, oldRoot);
|
||||
}
|
||||
|
|
|
@ -11,7 +11,10 @@ class EngineGeneration;
|
|||
|
||||
///! The base class of all types that can be reloaded.
|
||||
/// Reloadables will attempt to take specific state from previous config revisions if possible.
|
||||
/// Some examples are @@ProxyWindowBase and @@PersistentProperties
|
||||
/// Some examples are [ProxyWindowBase] and [PersistentProperties]
|
||||
///
|
||||
/// [ProxyWindowBase]: ../proxywindowbase
|
||||
/// [PersistentProperties]: ../persistentproperties
|
||||
class Reloadable
|
||||
: public QObject
|
||||
, public QQmlParserStatus {
|
||||
|
@ -25,7 +28,7 @@ class Reloadable
|
|||
/// this object in the current revision, and facilitate smoother reloading.
|
||||
///
|
||||
/// Note that identifiers are scoped, and will try to do the right thing in context.
|
||||
/// For example if you have a @@Variants wrapping an object with an identified element inside,
|
||||
/// For example if you have a `Variants` wrapping an object with an identified element inside,
|
||||
/// a scope is created at the variant level.
|
||||
///
|
||||
/// ```qml
|
||||
|
@ -71,7 +74,6 @@ public:
|
|||
|
||||
private slots:
|
||||
void onReloadFinished();
|
||||
void onGenerationDestroyed();
|
||||
|
||||
protected:
|
||||
// Called unconditionally in the reload phase, with nullptr if no source could be determined.
|
||||
|
@ -84,9 +86,10 @@ private:
|
|||
};
|
||||
|
||||
///! Scope that propagates reloads to child items in order.
|
||||
/// Convenience type equivalent to setting @@Reloadable.reloadableId for all children.
|
||||
/// Convenience type equivalent to setting `reloadableId` on properties in a
|
||||
/// QtObject instance.
|
||||
///
|
||||
/// Note that this does not work for visible @@QtQuick.Item$s (all widgets).
|
||||
/// Note that this does not work for visible `Item`s (all widgets).
|
||||
///
|
||||
/// ```qml
|
||||
/// ShellRoot {
|
||||
|
|
|
@ -1,163 +0,0 @@
|
|||
#include "retainable.hpp"
|
||||
|
||||
#include <qlogging.h>
|
||||
#include <qobject.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
RetainableHook* RetainableHook::getHook(QObject* object, bool create) {
|
||||
auto v = object->property("__qs_retainable");
|
||||
|
||||
if (v.canConvert<RetainableHook*>()) {
|
||||
return v.value<RetainableHook*>();
|
||||
} else if (create) {
|
||||
auto* retainable = dynamic_cast<Retainable*>(object);
|
||||
if (!retainable) return nullptr;
|
||||
|
||||
auto* hook = new RetainableHook(object);
|
||||
hook->retainableFacet = retainable;
|
||||
retainable->hook = hook;
|
||||
|
||||
object->setProperty("__qs_retainable", QVariant::fromValue(hook));
|
||||
|
||||
return hook;
|
||||
} else return nullptr;
|
||||
}
|
||||
|
||||
RetainableHook* RetainableHook::qmlAttachedProperties(QObject* object) {
|
||||
return RetainableHook::getHook(object, true);
|
||||
}
|
||||
|
||||
void RetainableHook::ref() { this->refcount++; }
|
||||
|
||||
void RetainableHook::unref() {
|
||||
this->refcount--;
|
||||
if (this->refcount == 0) this->unlocked();
|
||||
}
|
||||
|
||||
void RetainableHook::lock() {
|
||||
this->explicitRefcount++;
|
||||
this->ref();
|
||||
}
|
||||
|
||||
void RetainableHook::unlock() {
|
||||
if (this->explicitRefcount < 1) {
|
||||
qWarning() << "Retainable object" << this->parent()
|
||||
<< "unlocked more times than it was locked!";
|
||||
} else {
|
||||
this->explicitRefcount--;
|
||||
this->unref();
|
||||
}
|
||||
}
|
||||
|
||||
void RetainableHook::forceUnlock() { this->unlocked(); }
|
||||
|
||||
bool RetainableHook::isRetained() const { return !this->inactive; }
|
||||
|
||||
void RetainableHook::unlocked() {
|
||||
if (this->inactive) return;
|
||||
|
||||
emit this->aboutToDestroy();
|
||||
this->retainableFacet->retainFinished();
|
||||
}
|
||||
|
||||
void Retainable::retainedDestroy() {
|
||||
this->retaining = true;
|
||||
|
||||
auto* hook = RetainableHook::getHook(dynamic_cast<QObject*>(this), false);
|
||||
|
||||
if (hook) {
|
||||
// let all signal handlers run before acting on changes
|
||||
emit hook->dropped();
|
||||
hook->inactive = false;
|
||||
|
||||
if (hook->refcount == 0) hook->unlocked();
|
||||
else emit hook->retainedChanged();
|
||||
} else {
|
||||
this->retainFinished();
|
||||
}
|
||||
}
|
||||
|
||||
bool Retainable::isRetained() const { return this->retaining; }
|
||||
|
||||
void Retainable::retainFinished() {
|
||||
// a normal delete tends to cause deref errors in a listview.
|
||||
dynamic_cast<QObject*>(this)->deleteLater();
|
||||
}
|
||||
|
||||
RetainableLock::~RetainableLock() {
|
||||
if (this->mEnabled && this->mObject) {
|
||||
this->hook->unref();
|
||||
}
|
||||
}
|
||||
|
||||
QObject* RetainableLock::object() const { return this->mObject; }
|
||||
|
||||
void RetainableLock::setObject(QObject* object) {
|
||||
if (object == this->mObject) return;
|
||||
|
||||
if (this->mObject) {
|
||||
QObject::disconnect(this->mObject, nullptr, this, nullptr);
|
||||
if (this->hook->isRetained()) emit this->retainedChanged();
|
||||
this->hook->unref();
|
||||
}
|
||||
|
||||
this->mObject = nullptr;
|
||||
this->hook = nullptr;
|
||||
|
||||
if (object) {
|
||||
if (auto* hook = RetainableHook::getHook(object, true)) {
|
||||
this->mObject = object;
|
||||
this->hook = hook;
|
||||
|
||||
QObject::connect(object, &QObject::destroyed, this, &RetainableLock::onObjectDestroyed);
|
||||
QObject::connect(hook, &RetainableHook::dropped, this, &RetainableLock::dropped);
|
||||
QObject::connect(
|
||||
hook,
|
||||
&RetainableHook::aboutToDestroy,
|
||||
this,
|
||||
&RetainableLock::aboutToDestroy
|
||||
);
|
||||
QObject::connect(
|
||||
hook,
|
||||
&RetainableHook::retainedChanged,
|
||||
this,
|
||||
&RetainableLock::retainedChanged
|
||||
);
|
||||
if (hook->isRetained()) emit this->retainedChanged();
|
||||
|
||||
hook->ref();
|
||||
} else {
|
||||
qCritical() << "Tried to set non retainable object" << object << "as the target of" << this;
|
||||
}
|
||||
}
|
||||
|
||||
emit this->objectChanged();
|
||||
}
|
||||
|
||||
void RetainableLock::onObjectDestroyed() {
|
||||
this->mObject = nullptr;
|
||||
this->hook = nullptr;
|
||||
|
||||
emit this->objectChanged();
|
||||
}
|
||||
|
||||
bool RetainableLock::locked() const { return this->mEnabled; }
|
||||
|
||||
void RetainableLock::setLocked(bool locked) {
|
||||
if (locked == this->mEnabled) return;
|
||||
|
||||
this->mEnabled = locked;
|
||||
|
||||
if (this->mObject) {
|
||||
if (locked) this->hook->ref();
|
||||
else {
|
||||
if (this->hook->isRetained()) emit this->retainedChanged();
|
||||
this->hook->unref();
|
||||
}
|
||||
}
|
||||
|
||||
emit this->lockedChanged();
|
||||
}
|
||||
|
||||
bool RetainableLock::isRetained() const { return this->mObject && this->hook->isRetained(); }
|
|
@ -1,162 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtclasshelpermacros.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class Retainable;
|
||||
|
||||
///! Attached object for types that can have delayed destruction.
|
||||
/// Retainable works as an attached property that allows objects to be
|
||||
/// kept around (retained) after they would normally be destroyed, which
|
||||
/// is especially useful for things like exit transitions.
|
||||
///
|
||||
/// An object that is retainable will have @@Retainable as an attached property.
|
||||
/// All retainable objects will say that they are retainable on their respective
|
||||
/// typeinfo pages.
|
||||
///
|
||||
/// > [!INFO] Working directly with @@Retainable is often overly complicated and
|
||||
/// > error prone. For this reason @@RetainableLock should
|
||||
/// > usually be used instead.
|
||||
class RetainableHook: public QObject {
|
||||
Q_OBJECT;
|
||||
/// If the object is currently in a retained state.
|
||||
Q_PROPERTY(bool retained READ isRetained NOTIFY retainedChanged);
|
||||
QML_ATTACHED(RetainableHook);
|
||||
QML_NAMED_ELEMENT(Retainable);
|
||||
QML_UNCREATABLE("Retainable can only be used as an attached object.");
|
||||
|
||||
public:
|
||||
static RetainableHook* getHook(QObject* object, bool create = false);
|
||||
|
||||
void destroyOnRelease();
|
||||
|
||||
void ref();
|
||||
void unref();
|
||||
|
||||
/// Hold a lock on the object so it cannot be destroyed.
|
||||
///
|
||||
/// A counter is used to ensure you can lock the object from multiple places
|
||||
/// and it will not be unlocked until the same number of unlocks as locks have occurred.
|
||||
///
|
||||
/// > [!WARNING] It is easy to forget to unlock a locked object.
|
||||
/// > Doing so will create what is effectively a memory leak.
|
||||
/// >
|
||||
/// > Using @@RetainableLock is recommended as it will help
|
||||
/// > avoid this scenario and make misuse more obvious.
|
||||
Q_INVOKABLE void lock();
|
||||
/// Remove a lock on the object. See @@lock() for more information.
|
||||
Q_INVOKABLE void unlock();
|
||||
/// Forcibly remove all locks, destroying the object.
|
||||
///
|
||||
/// @@unlock() should usually be preferred.
|
||||
Q_INVOKABLE void forceUnlock();
|
||||
|
||||
[[nodiscard]] bool isRetained() const;
|
||||
|
||||
static RetainableHook* qmlAttachedProperties(QObject* object);
|
||||
|
||||
signals:
|
||||
/// This signal is sent when the object would normally be destroyed.
|
||||
///
|
||||
/// If all signal handlers return and no locks are in place, the object will be destroyed.
|
||||
/// If at least one lock is present the object will be retained until all are removed.
|
||||
void dropped();
|
||||
/// This signal is sent immediately before the object is destroyed.
|
||||
/// At this point destruction cannot be interrupted.
|
||||
void aboutToDestroy();
|
||||
|
||||
void retainedChanged();
|
||||
|
||||
private:
|
||||
explicit RetainableHook(QObject* parent): QObject(parent) {}
|
||||
|
||||
void unlocked();
|
||||
|
||||
uint refcount = 0;
|
||||
// tracked separately so a warning can be given when unlock is called too many times,
|
||||
// without affecting other lock sources such as RetainableLock.
|
||||
uint explicitRefcount = 0;
|
||||
Retainable* retainableFacet = nullptr;
|
||||
bool inactive = true;
|
||||
|
||||
friend class Retainable;
|
||||
};
|
||||
|
||||
class Retainable {
|
||||
public:
|
||||
Retainable() = default;
|
||||
virtual ~Retainable() = default;
|
||||
Q_DISABLE_COPY_MOVE(Retainable);
|
||||
|
||||
void retainedDestroy();
|
||||
[[nodiscard]] bool isRetained() const;
|
||||
|
||||
protected:
|
||||
virtual void retainFinished();
|
||||
|
||||
private:
|
||||
RetainableHook* hook = nullptr;
|
||||
bool retaining = false;
|
||||
|
||||
friend class RetainableHook;
|
||||
};
|
||||
|
||||
///! A helper for easily using Retainable.
|
||||
/// A RetainableLock provides extra safety and ease of use for locking
|
||||
/// @@Retainable objects. A retainable object can be locked by multiple
|
||||
/// locks at once, and each lock re-exposes relevant properties
|
||||
/// of the retained objects.
|
||||
///
|
||||
/// #### Example
|
||||
/// The code below will keep a retainable object alive for as long as the
|
||||
/// RetainableLock exists.
|
||||
///
|
||||
/// ```qml
|
||||
/// RetainableLock {
|
||||
/// object: aRetainableObject
|
||||
/// locked: true
|
||||
/// }
|
||||
/// ```
|
||||
class RetainableLock: public QObject {
|
||||
Q_OBJECT;
|
||||
/// The object to lock. Must be @@Retainable.
|
||||
Q_PROPERTY(QObject* object READ object WRITE setObject NOTIFY objectChanged);
|
||||
/// If the object should be locked.
|
||||
Q_PROPERTY(bool locked READ locked WRITE setLocked NOTIFY lockedChanged);
|
||||
/// If the object is currently in a retained state.
|
||||
Q_PROPERTY(bool retained READ isRetained NOTIFY retainedChanged);
|
||||
QML_ELEMENT;
|
||||
|
||||
public:
|
||||
explicit RetainableLock(QObject* parent = nullptr): QObject(parent) {}
|
||||
~RetainableLock() override;
|
||||
Q_DISABLE_COPY_MOVE(RetainableLock);
|
||||
|
||||
[[nodiscard]] QObject* object() const;
|
||||
void setObject(QObject* object);
|
||||
|
||||
[[nodiscard]] bool locked() const;
|
||||
void setLocked(bool locked);
|
||||
|
||||
[[nodiscard]] bool isRetained() const;
|
||||
|
||||
signals:
|
||||
/// Rebroadcast of the object's @@Retainable.dropped(s).
|
||||
void dropped();
|
||||
/// Rebroadcast of the object's @@Retainable.aboutToDestroy(s).
|
||||
void aboutToDestroy();
|
||||
void retainedChanged();
|
||||
|
||||
void objectChanged();
|
||||
void lockedChanged();
|
||||
|
||||
private slots:
|
||||
void onObjectDestroyed();
|
||||
|
||||
private:
|
||||
QObject* mObject = nullptr;
|
||||
RetainableHook* hook = nullptr;
|
||||
bool mEnabled = false;
|
||||
};
|
|
@ -1,169 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <new>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qhashfunctions.h>
|
||||
#include <qtclasshelpermacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
// NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic)
|
||||
|
||||
// capacity 0 buffer cannot be inserted into, only replaced with =
|
||||
// this is NOT exception safe for constructors
|
||||
template <typename T>
|
||||
class RingBuffer {
|
||||
public:
|
||||
explicit RingBuffer() = default;
|
||||
explicit RingBuffer(qsizetype capacity): mCapacity(capacity) {
|
||||
if (capacity > 0) this->createData();
|
||||
}
|
||||
|
||||
~RingBuffer() { this->deleteData(); }
|
||||
|
||||
Q_DISABLE_COPY(RingBuffer);
|
||||
|
||||
explicit RingBuffer(RingBuffer&& other) noexcept { *this = std::move(other); }
|
||||
|
||||
RingBuffer& operator=(RingBuffer&& other) noexcept {
|
||||
this->deleteData();
|
||||
this->data = other.data;
|
||||
this->head = other.head;
|
||||
this->mSize = other.mSize;
|
||||
this->mCapacity = other.mCapacity;
|
||||
other.data = nullptr;
|
||||
other.head = -1;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// undefined if capacity is 0
|
||||
template <typename... Args>
|
||||
T& emplace(Args&&... args) {
|
||||
auto i = (this->head + 1) % this->mCapacity;
|
||||
|
||||
if (this->indexIsAllocated(i)) {
|
||||
this->data[i].~T();
|
||||
}
|
||||
|
||||
auto* slot = &this->data[i];
|
||||
new (&this->data[i]) T(std::forward<Args>(args)...);
|
||||
|
||||
this->head = i;
|
||||
if (this->mSize != this->mCapacity) this->mSize = i + 1;
|
||||
|
||||
return *slot;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
if (this->head == -1) return;
|
||||
|
||||
auto i = this->head;
|
||||
|
||||
do {
|
||||
i = (i + 1) % this->mSize;
|
||||
this->data[i].~T();
|
||||
} while (i != this->head);
|
||||
|
||||
this->mSize = 0;
|
||||
this->head = -1;
|
||||
}
|
||||
|
||||
// negative indexes and >size indexes are undefined
|
||||
[[nodiscard]] T& at(qsizetype i) {
|
||||
auto bufferI = (this->head - i) % this->mCapacity;
|
||||
if (bufferI < 0) bufferI += this->mCapacity;
|
||||
return this->data[bufferI];
|
||||
}
|
||||
|
||||
[[nodiscard]] const T& at(qsizetype i) const {
|
||||
return const_cast<RingBuffer<T>*>(this)->at(i); // NOLINT
|
||||
}
|
||||
|
||||
[[nodiscard]] qsizetype size() const { return this->mSize; }
|
||||
[[nodiscard]] qsizetype capacity() const { return this->mCapacity; }
|
||||
|
||||
private:
|
||||
void createData() {
|
||||
if (this->data != nullptr) return;
|
||||
this->data =
|
||||
static_cast<T*>(::operator new(this->mCapacity * sizeof(T), std::align_val_t {alignof(T)}));
|
||||
}
|
||||
|
||||
void deleteData() {
|
||||
this->clear();
|
||||
::operator delete(this->data, std::align_val_t {alignof(T)});
|
||||
this->data = nullptr;
|
||||
}
|
||||
|
||||
bool indexIsAllocated(qsizetype index) {
|
||||
return this->mSize == this->mCapacity || index <= this->head;
|
||||
}
|
||||
|
||||
T* data = nullptr;
|
||||
qsizetype mCapacity = 0;
|
||||
qsizetype head = -1;
|
||||
qsizetype mSize = 0;
|
||||
};
|
||||
|
||||
// ring buffer with the ability to look up elements by hash (single bucket)
|
||||
template <typename T>
|
||||
class HashBuffer {
|
||||
public:
|
||||
explicit HashBuffer() = default;
|
||||
explicit HashBuffer(qsizetype capacity): ring(capacity) {}
|
||||
~HashBuffer() = default;
|
||||
|
||||
Q_DISABLE_COPY(HashBuffer);
|
||||
explicit HashBuffer(HashBuffer&& other) noexcept: ring(other.ring) {}
|
||||
|
||||
HashBuffer& operator=(HashBuffer&& other) noexcept {
|
||||
this->ring = other.ring;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// returns the index of the given value or -1 if missing
|
||||
[[nodiscard]] qsizetype indexOf(const T& value, T** slot = nullptr) {
|
||||
auto hash = qHash(value);
|
||||
|
||||
for (auto i = 0; i < this->size(); i++) {
|
||||
auto& v = this->ring.at(i);
|
||||
if (hash == v.first && value == v.second) {
|
||||
if (slot != nullptr) *slot = &v.second;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
[[nodiscard]] qsizetype indexOf(const T& value, T const** slot = nullptr) const {
|
||||
return const_cast<HashBuffer<T>*>(this)->indexOf(value, slot); // NOLINT
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
T& emplace(Args&&... args) {
|
||||
auto& entry = this->ring.emplace(
|
||||
std::piecewise_construct,
|
||||
std::forward_as_tuple(0),
|
||||
std::forward_as_tuple(std::forward<Args>(args)...)
|
||||
);
|
||||
|
||||
entry.first = qHash(entry.second);
|
||||
return entry.second;
|
||||
}
|
||||
|
||||
void clear() { this->ring.clear(); }
|
||||
|
||||
// negative indexes and >size indexes are undefined
|
||||
[[nodiscard]] T& at(qsizetype i) { return this->ring.at(i).second; }
|
||||
[[nodiscard]] const T& at(qsizetype i) const { return this->ring.at(i).second; }
|
||||
[[nodiscard]] qsizetype size() const { return this->ring.size(); }
|
||||
[[nodiscard]] qsizetype capacity() const { return this->ring.capacity(); }
|
||||
|
||||
private:
|
||||
RingBuffer<std::pair<size_t, T>> ring;
|
||||
};
|
||||
|
||||
// NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic)
|
|
@ -8,19 +8,16 @@
|
|||
#include <qobject.h>
|
||||
#include <qqmlcomponent.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qquickitem.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qurl.h>
|
||||
|
||||
#include "../window/floatingwindow.hpp"
|
||||
#include "generation.hpp"
|
||||
#include "qmlglobal.hpp"
|
||||
#include "scan.hpp"
|
||||
#include "shell.hpp"
|
||||
|
||||
RootWrapper::RootWrapper(QString rootPath, QString shellId)
|
||||
RootWrapper::RootWrapper(QString rootPath)
|
||||
: QObject(nullptr)
|
||||
, rootPath(std::move(rootPath))
|
||||
, shellId(std::move(shellId))
|
||||
, originalWorkingDirectory(QDir::current().absolutePath()) {
|
||||
// clang-format off
|
||||
QObject::connect(QuickshellSettings::instance(), &QuickshellSettings::watchFilesChanged, this, &RootWrapper::onWatchFilesChanged);
|
||||
|
@ -37,16 +34,18 @@ RootWrapper::RootWrapper(QString rootPath, QString shellId)
|
|||
RootWrapper::~RootWrapper() {
|
||||
// event loop may no longer be running so deleteLater is not an option
|
||||
if (this->generation != nullptr) {
|
||||
this->generation->shutdown();
|
||||
delete this->generation->root;
|
||||
this->generation->root = nullptr;
|
||||
}
|
||||
|
||||
delete this->generation;
|
||||
}
|
||||
|
||||
void RootWrapper::reloadGraph(bool hard) {
|
||||
auto rootPath = QFileInfo(this->rootPath).dir();
|
||||
auto scanner = QmlScanner(rootPath);
|
||||
auto scanner = QmlScanner();
|
||||
scanner.scanQmlFile(this->rootPath);
|
||||
|
||||
auto* generation = new EngineGeneration(rootPath, std::move(scanner));
|
||||
auto* generation = new EngineGeneration(std::move(scanner));
|
||||
generation->wrapper = this;
|
||||
|
||||
// todo: move into EngineGeneration
|
||||
|
@ -61,49 +60,33 @@ void RootWrapper::reloadGraph(bool hard) {
|
|||
url.setScheme("qsintercept");
|
||||
auto component = QQmlComponent(generation->engine, url);
|
||||
|
||||
auto* newRoot = component.beginCreate(generation->engine->rootContext());
|
||||
|
||||
if (newRoot == nullptr) {
|
||||
const QString error = "failed to create root component\n" + component.errorString();
|
||||
qWarning().noquote() << error;
|
||||
generation->destroy();
|
||||
|
||||
if (this->generation != nullptr && this->generation->qsgInstance != nullptr) {
|
||||
emit this->generation->qsgInstance->reloadFailed(error);
|
||||
}
|
||||
auto* obj = component.beginCreate(generation->engine->rootContext());
|
||||
|
||||
if (obj == nullptr) {
|
||||
qWarning() << component.errorString().toStdString().c_str();
|
||||
qWarning() << "failed to create root component";
|
||||
delete generation;
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto* item = qobject_cast<QQuickItem*>(newRoot)) {
|
||||
auto* window = new FloatingWindowInterface();
|
||||
item->setParent(window);
|
||||
item->setParentItem(window->contentItem());
|
||||
window->setWidth(static_cast<int>(item->width()));
|
||||
window->setHeight(static_cast<int>(item->height()));
|
||||
newRoot = window;
|
||||
auto* newRoot = qobject_cast<ShellRoot*>(obj);
|
||||
if (newRoot == nullptr) {
|
||||
qWarning() << "root component was not a Quickshell.ShellRoot";
|
||||
delete obj;
|
||||
delete generation;
|
||||
return;
|
||||
}
|
||||
|
||||
generation->root = newRoot;
|
||||
|
||||
component.completeCreate();
|
||||
|
||||
if (this->generation) {
|
||||
QObject::disconnect(this->generation, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
auto isReload = this->generation != nullptr;
|
||||
generation->onReload(hard ? nullptr : this->generation);
|
||||
|
||||
if (hard && this->generation) {
|
||||
this->generation->destroy();
|
||||
}
|
||||
|
||||
if (hard) delete this->generation;
|
||||
this->generation = generation;
|
||||
|
||||
qInfo() << "Configuration Loaded";
|
||||
|
||||
QObject::connect(this->generation, &QObject::destroyed, this, &RootWrapper::generationDestroyed);
|
||||
QObject::connect(
|
||||
this->generation,
|
||||
&EngineGeneration::filesChanged,
|
||||
|
@ -112,14 +95,8 @@ void RootWrapper::reloadGraph(bool hard) {
|
|||
);
|
||||
|
||||
this->onWatchFilesChanged();
|
||||
|
||||
if (isReload && this->generation->qsgInstance != nullptr) {
|
||||
emit this->generation->qsgInstance->reloadCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
void RootWrapper::generationDestroyed() { this->generation = nullptr; }
|
||||
|
||||
void RootWrapper::onWatchFilesChanged() {
|
||||
auto watchFiles = QuickshellSettings::instance()->watchFiles();
|
||||
if (this->generation != nullptr) {
|
||||
|
|
|
@ -12,20 +12,18 @@ class RootWrapper: public QObject {
|
|||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
explicit RootWrapper(QString rootPath, QString shellId);
|
||||
explicit RootWrapper(QString rootPath);
|
||||
~RootWrapper() override;
|
||||
Q_DISABLE_COPY_MOVE(RootWrapper);
|
||||
|
||||
void reloadGraph(bool hard);
|
||||
|
||||
private slots:
|
||||
void generationDestroyed();
|
||||
void onWatchFilesChanged();
|
||||
void onWatchedFilesChanged();
|
||||
|
||||
private:
|
||||
QString rootPath;
|
||||
QString shellId;
|
||||
EngineGeneration* generation = nullptr;
|
||||
QString originalWorkingDirectory;
|
||||
};
|
||||
|
|
|
@ -103,15 +103,7 @@ bool QmlScanner::scanQmlFile(const QString& path) {
|
|||
this->scanDir(currentdir.path());
|
||||
|
||||
for (auto& import: imports) {
|
||||
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 ipath = currentdir.filePath(import);
|
||||
auto cpath = QFileInfo(ipath).canonicalFilePath();
|
||||
|
||||
if (cpath.isEmpty()) {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdir.h>
|
||||
#include <qhash.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qvector.h>
|
||||
|
@ -11,8 +10,6 @@ Q_DECLARE_LOGGING_CATEGORY(logQmlScanner);
|
|||
// expects canonical paths
|
||||
class QmlScanner {
|
||||
public:
|
||||
QmlScanner(const QDir& rootPath): rootPath(rootPath) {}
|
||||
|
||||
void scanDir(const QString& path);
|
||||
// returns if the file has a singleton
|
||||
bool scanQmlFile(const QString& path);
|
||||
|
@ -20,7 +17,4 @@ public:
|
|||
QVector<QString> scannedDirs;
|
||||
QVector<QString> scannedFiles;
|
||||
QHash<QString, QString> qmldirIntercepts;
|
||||
|
||||
private:
|
||||
QDir rootPath;
|
||||
};
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue