forked from quickshell/quickshell
Compare commits
270 commits
908ba3eef5
...
66b9917e70
Author | SHA1 | Date | |
---|---|---|---|
|
66b9917e70 | ||
|
3a40174ed6 | ||
|
08836ca1f3 | ||
|
2f194b7894 | ||
|
611cd76abc | ||
|
27840db7a6 | ||
|
a053373d57 | ||
|
3fc1c914c7 | ||
|
be5e5fc4a5 | ||
|
ded3708762 | ||
|
69d13967c9 | ||
|
ccf885081c | ||
|
70be74e80d | ||
|
2d05c7a89e | ||
|
26280b34b4 | ||
|
af14a416c1 | ||
|
cb05e9a327 | ||
|
8882f7ca50 | ||
|
59298f6507 | ||
|
fd87be1355 | ||
|
b6a79fe99c | ||
|
539692bc11 | ||
|
87a57b7a2c | ||
|
e3d003e7ab | ||
|
6f9993394a | ||
![]() |
31adcaac76 | ||
![]() |
84ce47b6d3 | ||
|
2571766d3b | ||
|
e957e88ccb | ||
|
9b409c0e38 | ||
|
cb426973d7 | ||
|
57a5d8e1ed | ||
|
c21df95087 | ||
|
2996e40ff9 | ||
|
afa1b6f88b | ||
|
a8901fde67 | ||
|
8d63006bba | ||
|
5b01ec032e | ||
|
f0aca2030e | ||
|
5301227ec1 | ||
|
ec143d6119 | ||
|
324fe9274d | ||
|
b43b4a06d0 | ||
|
0e9e593078 | ||
|
ac50767873 | ||
|
f53e6fb515 | ||
|
ff55ac874b | ||
|
d4deb11216 | ||
|
a13c9d91b5 | ||
|
abb900b7ff | ||
|
e2ef7b7982 | ||
|
db9e633197 | ||
|
1955deee74 | ||
|
4163713bc4 | ||
|
dca75b7d6a | ||
|
8450543e09 | ||
|
dbaaf55eb6 | ||
|
eb5a5b8b67 | ||
|
6ceee06884 | ||
|
66b494d760 | ||
|
f4066cb4ed | ||
|
ee93306312 | ||
|
033e810871 | ||
|
401ee4cec6 | ||
|
79fca3cab8 | ||
|
36174854ad | ||
|
fdc13023b7 | ||
|
68ba5005ce | ||
|
d2667369e1 | ||
|
7db3772641 | ||
|
29d31f5d3b | ||
|
36d1dbeb69 | ||
|
0445eee33a | ||
|
60dfa67ec7 | ||
|
0dd19d4a18 | ||
|
2c0e46cedb | ||
|
74f371850d | ||
|
b528be9426 | ||
|
92252c36a3 | ||
|
7ffce72b31 | ||
|
1168879d6d | ||
|
2e18340995 | ||
|
cdeec6ee83 | ||
|
98cdb87181 | ||
|
746b0e70d7 | ||
|
a931adf033 | ||
|
9980f8587e | ||
|
4e48c6eefb | ||
|
1adad9e822 | ||
|
4c2d7a7e41 | ||
|
89d04f34a5 | ||
|
23f59ec4c3 | ||
|
8e40112d14 | ||
|
3ed39b2a79 | ||
|
fbaec141c0 | ||
|
fdc78ae16f | ||
|
f889f08901 | ||
|
7f9762be53 | ||
|
931aca5392 | ||
|
bd8978375b | ||
|
7a283089b1 | ||
|
c57ac4b1f2 | ||
|
08966f91c5 | ||
|
84e3f04f3c | ||
|
bdc9fe958b | ||
|
01f2be057e | ||
|
abe0327e67 | ||
|
accdc59a1c | ||
|
293341c9e1 | ||
|
5e2fb14551 | ||
|
3690812919 | ||
|
01f6331cb7 | ||
|
9d21a01153 | ||
|
47ec85ffef | ||
|
01deefe241 | ||
|
a82fbf40c2 | ||
|
c78381f6d0 | ||
|
f810c63ffc | ||
|
19d74595d6 | ||
|
2c485e415d | ||
|
8cdb41317f | ||
|
85be3861ce | ||
|
3a1eec0ed5 | ||
|
465d5402f2 | ||
|
397476244c | ||
|
6cb7d894ab | ||
|
95245cb6a5 | ||
|
94e881e6b0 | ||
|
da043e092a | ||
|
13b6eeaa22 | ||
|
3edb3f4efa | ||
|
60349f1894 | ||
|
77c5a2d569 | ||
|
f6ad617b67 | ||
|
a116f39c63 | ||
|
af29bc277e | ||
|
9967e2e03b | ||
|
e327d6750d | ||
|
79b22af093 | ||
|
c60871a7fb | ||
![]() |
b40d4147e0 | ||
|
f95e7dbaf6 | ||
|
fe1d15e8f6 | ||
|
5040f3796c | ||
|
5a038f085d | ||
|
e223408143 | ||
|
f89c504b55 | ||
|
1d2bf5d7b4 | ||
|
815867c178 | ||
|
22c397bbb0 | ||
|
23cd6cd9e1 | ||
|
683d92a05f | ||
|
14852700cb | ||
|
5f4d7f89db | ||
|
53b8f1ee0b | ||
|
c2b4610acb | ||
|
0fc98652a8 | ||
|
291179ede2 | ||
|
bdbf5b9af9 | ||
|
8364e94d26 | ||
|
7c7326ec52 | ||
|
38ba3fff24 | ||
|
6bf4826ae7 | ||
|
46f48f2f87 | ||
|
533b389742 | ||
|
d582bb7b57 | ||
|
79b2fea52e | ||
|
2c87cc3803 | ||
|
cb2862eca9 | ||
|
9555b201fe | ||
|
a4903eaefc | ||
|
76744c903a | ||
|
ba1e535f9c | ||
|
8873a06962 | ||
|
3a8e67e8ab | ||
|
abc0201f6e | ||
|
d9f66e63a3 | ||
|
18563b1273 | ||
|
4b2e569e94 | ||
|
58c3718287 | ||
|
6b9b1fcb53 | ||
|
54350277be | ||
|
acdbe73c10 | ||
|
a71a6fb3ac | ||
|
60388f10ca | ||
|
ebfa8ec448 | ||
|
14910b1b60 | ||
|
a9e4720fae | ||
|
dfcf533424 | ||
|
aa3f7daea2 | ||
|
6367b56f55 | ||
|
e48af44607 | ||
|
d1c33d48cd | ||
|
e9cacbd92d | ||
|
c4cc662bcc | ||
|
e23923d9a2 | ||
|
609834d8f2 | ||
|
7c5632ef5f | ||
|
d630cc7f76 | ||
|
79cbfba48a | ||
|
c758421af6 | ||
|
49b309247d | ||
|
bb33c9a0c4 | ||
![]() |
24f54f579f | ||
|
497c9c4e50 | ||
|
db23c0264a | ||
|
fdbb490537 | ||
|
b4be383695 | ||
|
ec362637b8 | ||
|
c31bbea837 | ||
|
d8b900ed0b | ||
|
8547d12396 | ||
|
d7149d5641 | ||
|
c78c86425d | ||
|
09d8a7a07d | ||
|
d8fa9e7bb3 | ||
|
c56a3ec966 | ||
|
b6612bd56c | ||
|
3573663ab6 | ||
|
72956185bd | ||
|
59cf60d83e | ||
|
6efa05a8eb | ||
|
3033cba52d | ||
|
8ec245ac66 | ||
|
71a65c4d3c | ||
|
9e58077c61 | ||
|
3991726b9b | ||
|
ae762f5c6e | ||
|
e89035b18c | ||
|
b5c8774a79 | ||
|
7e5d128a91 | ||
|
f655875547 | ||
|
ce5ddbf8ba | ||
|
d8b72b4c31 | ||
|
523de78796 | ||
|
67783ec24c | ||
|
b5b9c1f6c3 | ||
|
5d1def3e49 | ||
|
bc349998df | ||
|
ef1a4134f0 | ||
|
d14ca70984 | ||
|
be237b6ab5 | ||
|
37fecfc990 | ||
|
b1f5a5eb94 | ||
|
9d5dd402b9 | ||
|
29f02d837d | ||
|
7d20b472dd | ||
|
bd504daf56 | ||
|
238ca8cf0b | ||
|
a8506edbb9 | ||
|
d56c07ceb3 | ||
|
84bb4098ad | ||
|
6c9526761c | ||
|
7feae55ebe | ||
|
569c40494d | ||
|
0519acf1d6 | ||
|
33fac67798 | ||
|
7ad3671dd1 | ||
|
4e92d82992 | ||
|
5a84e73442 | ||
|
06240ccf80 | ||
|
5016dbf0d4 | ||
|
6326f60ce2 | ||
|
ac339cb23b | ||
|
f2df3da596 | ||
|
ed3708f5cb | ||
|
af45502913 | ||
|
4ee9ac7f7c | ||
|
3b6d1c3bd8 | ||
|
73cfeba61b |
.clang-tidy.editorconfig
.github
.gitignore.gitmodulesBUILD.mdCMakeLists.txtCONTRIBUTING.mdJustfileREADME.mdci
cmake
default.nixdocsexamplesflake.lockflake.nixshell.nixsrc
CMakeLists.txt
build
core
CMakeLists.txtclock.cppclock.hppcommon.cppcommon.hppdesktopentry.cppdesktopentry.hppdoc.hppelapsedtimer.cppelapsedtimer.hppgeneration.cppgeneration.hppiconimageprovider.cppiconimageprovider.hppiconprovider.cppiconprovider.hppinstanceinfo.cppinstanceinfo.hpplazyloader.cpplazyloader.hpplogging.cpplogging.hpplogging_p.hpplogging_qtprivate.cpplogging_qtprivate.hppmain.cppmain.hppmodel.cppmodel.hppmodule.mdobjectrepeater.cppobjectrepeater.hpppaths.cpppaths.hppplatformmenu.cppplatformmenu.hppplatformmenu_p.hppplugin.cppplugin.hpppopupanchor.cpppopupanchor.hpppopupwindow.cppqmlglobal.cppqmlglobal.hppqmlscreen.hppqsintercept.cppqsintercept.hppqsmenu.cppqsmenu.hppqsmenuanchor.cppqsmenuanchor.hppregion.hppreload.cppreload.hppretainable.cppretainable.hppringbuf.hpprootwrapper.cpprootwrapper.hppscan.cppscan.hppscriptmodel.cppscriptmodel.hppshell.hpp
test
transformwatcher.cpptransformwatcher.hpp
11
.clang-tidy
11
.clang-tidy
|
@ -5,6 +5,8 @@ Checks: >
|
|||
-*,
|
||||
bugprone-*,
|
||||
-bugprone-easily-swappable-parameters,
|
||||
-bugprone-forward-declararion-namespace,
|
||||
-bugprone-forward-declararion-namespace,
|
||||
concurrency-*,
|
||||
cppcoreguidelines-*,
|
||||
-cppcoreguidelines-owning-memory,
|
||||
|
@ -13,8 +15,10 @@ Checks: >
|
|||
-cppcoreguidelines-avoid-const-or-ref-data-members,
|
||||
-cppcoreguidelines-non-private-member-variables-in-classes,
|
||||
-cppcoreguidelines-avoid-goto,
|
||||
google-build-using-namespace.
|
||||
google-explicit-constructor,
|
||||
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
|
||||
-cppcoreguidelines-avoid-do-while,
|
||||
-cppcoreguidelines-pro-type-reinterpret-cast,
|
||||
-cppcoreguidelines-pro-type-vararg,
|
||||
google-global-names-in-headers,
|
||||
google-readability-casting,
|
||||
google-runtime-int,
|
||||
|
@ -26,6 +30,7 @@ Checks: >
|
|||
-modernize-return-braced-init-list,
|
||||
-modernize-use-trailing-return-type,
|
||||
performance-*,
|
||||
-performance-avoid-endl,
|
||||
portability-std-allocator-const,
|
||||
readability-*,
|
||||
-readability-function-cognitive-complexity,
|
||||
|
@ -37,6 +42,8 @@ Checks: >
|
|||
-readability-redundant-access-specifiers,
|
||||
-readability-else-after-return,
|
||||
-readability-container-data-pointer,
|
||||
-readability-implicit-bool-conversion,
|
||||
-readability-avoid-nested-conditional-operator,
|
||||
tidyfox-*,
|
||||
CheckOptions:
|
||||
performance-for-range-copy.WarnOnAllAutoCopies: true
|
||||
|
|
|
@ -9,3 +9,7 @@ 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
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
blank_issues_enabled: true
|
82
.github/ISSUE_TEMPLATE/crash.yml
vendored
Normal file
82
.github/ISSUE_TEMPLATE/crash.yml
vendored
Normal file
|
@ -0,0 +1,82 @@
|
|||
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.
|
55
.github/workflows/build.yml
vendored
Normal file
55
.github/workflows/build.yml
vendored
Normal file
|
@ -0,0 +1,55 @@
|
|||
name: Build
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
nix:
|
||||
name: Nix
|
||||
strategy:
|
||||
matrix:
|
||||
qtver: [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 \
|
||||
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
Normal file
25
.github/workflows/lint.yml
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
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,3 +1,7 @@
|
|||
# related repos
|
||||
/docs
|
||||
/examples
|
||||
|
||||
# build files
|
||||
/result
|
||||
/build/
|
||||
|
|
6
.gitmodules
vendored
6
.gitmodules
vendored
|
@ -1,6 +0,0 @@
|
|||
[submodule "docs"]
|
||||
path = docs
|
||||
url = https://git.outfoxxed.me/outfoxxed/quickshell-docs
|
||||
[submodule "examples"]
|
||||
path = examples
|
||||
url = https://git.outfoxxed.me/outfoxxed/quickshell-examples
|
225
BUILD.md
Normal file
225
BUILD.md
Normal file
|
@ -0,0 +1,225 @@
|
|||
# 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`
|
||||
|
||||
### 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
|
||||
```
|
156
CMakeLists.txt
156
CMakeLists.txt
|
@ -5,50 +5,76 @@ set(QT_MIN_VERSION "6.6.0")
|
|||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
option(BUILD_TESTING "Build tests" OFF)
|
||||
option(ASAN "Enable ASAN" OFF)
|
||||
option(FRAME_POINTERS "Always keep frame pointers" ${ASAN})
|
||||
set(QS_BUILD_OPTIONS "")
|
||||
|
||||
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(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON)
|
||||
option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON)
|
||||
option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON)
|
||||
option(SERVICE_PIPEWIRE "PipeWire service" ON)
|
||||
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}")
|
||||
|
||||
message(STATUS "Quickshell configuration")
|
||||
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 " PipeWire: ${SERVICE_PIPEWIRE}")
|
||||
message(STATUS " Hyprland: ${HYPRLAND}")
|
||||
if (HYPRLAND)
|
||||
message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}")
|
||||
message(STATUS " Global Shortcuts: ${HYPRLAND_GLOBAL_SHORTCUTS}")
|
||||
endif()
|
||||
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})
|
||||
|
||||
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()
|
||||
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(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)
|
||||
|
||||
include(cmake/install-qml-module.cmake)
|
||||
include(cmake/util.cmake)
|
||||
|
||||
add_compile_options(-Wall -Wextra)
|
||||
|
||||
# pipewire defines this, breaking PCH
|
||||
add_compile_definitions(_REENTRANT)
|
||||
|
||||
if (FRAME_POINTERS)
|
||||
add_compile_options(-fno-omit-frame-pointer)
|
||||
endif()
|
||||
|
@ -68,8 +94,9 @@ if (NOT CMAKE_BUILD_TYPE)
|
|||
set(CMAKE_BUILD_TYPE Debug)
|
||||
endif()
|
||||
|
||||
set(QT_DEPS Qt6::Gui Qt6::Qml Qt6::Quick Qt6::QuickControls2 Qt6::Widgets)
|
||||
set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets)
|
||||
set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets ShaderTools)
|
||||
|
||||
include(cmake/pch.cmake)
|
||||
|
||||
if (BUILD_TESTING)
|
||||
enable_testing()
|
||||
|
@ -78,58 +105,39 @@ 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)
|
||||
if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS)
|
||||
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)
|
||||
|
||||
# pch breaks clang-tidy..... somehow
|
||||
if (NOT NO_PCH)
|
||||
file(GENERATE
|
||||
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/pchstub.cpp
|
||||
CONTENT "// intentionally empty"
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
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)
|
||||
|
||||
if (USE_JEMALLOC)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
# IMPORTED_TARGET not working for some reason
|
||||
pkg_check_modules(JEMALLOC REQUIRED jemalloc)
|
||||
target_link_libraries(quickshell PRIVATE ${JEMALLOC_LIBRARIES})
|
||||
endif()
|
||||
|
||||
install(CODE "
|
||||
execute_process(
|
||||
COMMAND ${CMAKE_COMMAND} -E create_symlink \
|
||||
${CMAKE_INSTALL_FULL_BINDIR}/quickshell \$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/qs
|
||||
)
|
||||
")
|
||||
|
|
102
CONTRIBUTING.md
Normal file
102
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,102 @@
|
|||
# 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,7 +4,13 @@ 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 -q0 --eta clang-tidy --load={{ env_var("TIDYFOX") }}
|
||||
find src -type f -name "*.cpp" -print0 | parallel -q0 --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
|
||||
|
||||
lint-ci:
|
||||
find src -type f -name "*.cpp" -print0 | parallel -q0 --no-notice --will-cite --tty clang-tidy --load={{ env_var("TIDYFOX") }}
|
||||
|
||||
lint-changed:
|
||||
git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel --no-notice --will-cite --tty --bar 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>
|
||||
|
||||
Simple and flexbile QtQuick based desktop shell toolkit.
|
||||
Flexbile QtQuick based desktop shell toolkit.
|
||||
|
||||
Hosted on: [outfoxxed's gitea], [github]
|
||||
|
||||
|
@ -11,21 +11,14 @@ Hosted on: [outfoxxed's gitea], [github]
|
|||
Documentation available at [quickshell.outfoxxed.me](https://quickshell.outfoxxed.me) or
|
||||
can be built from the [quickshell-docs](https://git.outfoxxed.me/outfoxxed/quickshell-docs) repo.
|
||||
|
||||
Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples)
|
||||
Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples)
|
||||
repo.
|
||||
|
||||
Both the documentation and examples are included as submodules with revisions that work with the current
|
||||
version of quickshell.
|
||||
# Breaking Changes
|
||||
Quickshell is still in alpha and there will be breaking changes.
|
||||
|
||||
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
|
||||
```
|
||||
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.
|
||||
|
||||
# Installation
|
||||
|
||||
|
@ -39,6 +32,9 @@ 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";
|
||||
};
|
||||
};
|
||||
|
@ -48,75 +44,45 @@ This repo has a nix flake you can use to install the package directly:
|
|||
Quickshell's binary is available at `quickshell.packages.<system>.default` to be added to
|
||||
lists such as `environment.systemPackages` or `home.packages`.
|
||||
|
||||
`quickshell.packages.<system>.nvidia` is also available for nvidia users which fixes some
|
||||
common crashes.
|
||||
The package contains several features detailed in [BUILD.md](BUILD.md) which can be enabled
|
||||
or disabled with overrides:
|
||||
|
||||
```nix
|
||||
quickshell.packages.<system>.default.override {
|
||||
withJemalloc = true;
|
||||
withQtSvg = true;
|
||||
withWayland = true;
|
||||
withX11 = true;
|
||||
withPipewire = true;
|
||||
withPam = true;
|
||||
withHyprland = true;
|
||||
}
|
||||
```
|
||||
|
||||
Note: by default this package is built with clang as it is significantly faster.
|
||||
|
||||
## Manual
|
||||
## Arch (AUR)
|
||||
Quickshell has a third party [AUR package] available under the same name.
|
||||
It is not managed by us and should be looked over before use.
|
||||
|
||||
If not using nix, you'll have to build from source.
|
||||
[AUR package]: https://aur.archlinux.org/packages/quickshell
|
||||
|
||||
### Dependencies
|
||||
To build quickshell at all, you will need the following packages (names may vary by distro)
|
||||
> [!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.
|
||||
|
||||
- just
|
||||
- cmake
|
||||
- pkg-config
|
||||
- ninja
|
||||
- Qt6 [ QtBase, QtDeclarative ]
|
||||
## 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.
|
||||
|
||||
To build with wayland support you will additionally need:
|
||||
- wayland
|
||||
- wayland-scanner (may be part of wayland on some distros)
|
||||
- wayland-protocols
|
||||
- Qt6 [ QtWayland ]
|
||||
[Fedora COPR package]: https://copr.fedorainfracloud.org/coprs/errornointernet/quickshell
|
||||
|
||||
### Building
|
||||
## Anything else
|
||||
See [BUILD.md](BUILD.md) for instructions on building and packaging quickshell.
|
||||
|
||||
To make a release build of quickshell run:
|
||||
```sh
|
||||
$ just release
|
||||
```
|
||||
|
||||
If running an nvidia GPU, instead run:
|
||||
```sh
|
||||
$ just configure release -DNVIDIA_COMPAT=ON
|
||||
$ just build
|
||||
```
|
||||
|
||||
(These commands are just aliases for cmake commands you can run directly,
|
||||
see the Justfile for more information.)
|
||||
|
||||
If you have all the dependencies installed and they are in expected
|
||||
locations this will build correctly.
|
||||
|
||||
To install to /usr/local/bin run as root (usually `sudo`) in the same folder:
|
||||
```
|
||||
$ just install
|
||||
```
|
||||
|
||||
### Building (Nix)
|
||||
|
||||
You can build directly using the provided nix flake or nix package.
|
||||
```
|
||||
nix build
|
||||
nix build -f package.nix # calls default.nix with a basic callPackage expression
|
||||
```
|
||||
|
||||
# Development
|
||||
|
||||
For nix there is a devshell available from `shell.nix` and as a devShell
|
||||
output from the flake.
|
||||
|
||||
The Justfile contains various useful aliases:
|
||||
- `just configure [<debug|release> [extra cmake args]]`
|
||||
- `just build` (runs configure for debug mode)
|
||||
- `just run [args]`
|
||||
- `just clean`
|
||||
- `just test [args]` (configure with `-DBUILD_TESTING=ON` first)
|
||||
- `just fmt`
|
||||
- `just lint`
|
||||
# Contributing / Development
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
||||
|
||||
#### License
|
||||
|
||||
|
|
8
ci/matrix.nix
Normal file
8
ci/matrix.nix
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
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
|
58
ci/nix-checkouts.nix
Normal file
58
ci/nix-checkouts.nix
Normal file
|
@ -0,0 +1,58 @@
|
|||
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_0 = byCommit {
|
||||
commit = "23e89b7da85c3640bbc2173fe04f4bd114342367";
|
||||
sha256 = "1b2v6y3bja4br5ribh9lj6xzz2k81dggz708b2mib83rwb509wyb";
|
||||
};
|
||||
|
||||
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";
|
||||
};
|
||||
}
|
7
ci/variations.nix
Normal file
7
ci/variations.nix
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
clangStdenv,
|
||||
gccStdenv,
|
||||
}: {
|
||||
clang = { buildStdenv = clangStdenv; };
|
||||
gcc = { buildStdenv = gccStdenv; };
|
||||
}
|
89
cmake/install-qml-module.cmake
Normal file
89
cmake/install-qml-module.cmake
Normal file
|
@ -0,0 +1,89 @@
|
|||
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()
|
85
cmake/pch.cmake
Normal file
85
cmake/pch.cmake
Normal file
|
@ -0,0 +1,85 @@
|
|||
# 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>
|
||||
)
|
29
cmake/util.cmake
Normal file
29
cmake/util.cmake
Normal file
|
@ -0,0 +1,29 @@
|
|||
# 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,13 +3,20 @@
|
|||
nix-gitignore,
|
||||
pkgs,
|
||||
keepDebugInfo,
|
||||
buildStdenv ? pkgs.clang17Stdenv,
|
||||
buildStdenv ? pkgs.clangStdenv,
|
||||
|
||||
cmake,
|
||||
ninja,
|
||||
qt6,
|
||||
spirv-tools,
|
||||
cli11,
|
||||
breakpad,
|
||||
jemalloc,
|
||||
wayland,
|
||||
wayland-protocols,
|
||||
xorg,
|
||||
pipewire,
|
||||
pam,
|
||||
|
||||
gitRev ? (let
|
||||
headExists = builtins.pathExists ./.git/HEAD;
|
||||
|
@ -23,10 +30,15 @@
|
|||
else "unknown"),
|
||||
|
||||
debug ? false,
|
||||
enableWayland ? true,
|
||||
enablePipewire ? true,
|
||||
nvidiaCompat ? false,
|
||||
svgSupport ? true, # you almost always want this
|
||||
withCrashReporter ? true,
|
||||
withJemalloc ? true, # masks heap fragmentation
|
||||
withQtSvg ? true,
|
||||
withWayland ? true,
|
||||
withX11 ? true,
|
||||
withPipewire ? true,
|
||||
withPam ? true,
|
||||
withHyprland ? true,
|
||||
withI3 ? true,
|
||||
}: buildStdenv.mkDerivation {
|
||||
pname = "quickshell${lib.optionalString debug "-debug"}";
|
||||
version = "0.1.0";
|
||||
|
@ -35,45 +47,54 @@
|
|||
nativeBuildInputs = with pkgs; [
|
||||
cmake
|
||||
ninja
|
||||
qt6.qtshadertools
|
||||
spirv-tools
|
||||
qt6.wrapQtAppsHook
|
||||
] ++ (lib.optionals enableWayland [
|
||||
pkg-config
|
||||
] ++ (lib.optionals withWayland [
|
||||
wayland-protocols
|
||||
wayland-scanner
|
||||
]);
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
buildInputs = [
|
||||
qt6.qtbase
|
||||
qt6.qtdeclarative
|
||||
cli11
|
||||
]
|
||||
++ (lib.optionals enableWayland [ qt6.qtwayland wayland ])
|
||||
++ (lib.optionals svgSupport [ qt6.qtsvg ])
|
||||
++ (lib.optionals enablePipewire [ pipewire ]);
|
||||
++ lib.optional withCrashReporter breakpad
|
||||
++ lib.optional withJemalloc jemalloc
|
||||
++ lib.optional withQtSvg qt6.qtsvg
|
||||
++ lib.optionals withWayland [ qt6.qtwayland wayland ]
|
||||
++ lib.optional withX11 xorg.libxcb
|
||||
++ lib.optional withPam pam
|
||||
++ lib.optional withPipewire pipewire;
|
||||
|
||||
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
|
||||
'';
|
||||
cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
|
||||
|
||||
cmakeFlags = [
|
||||
"-DGIT_REVISION=${gitRev}"
|
||||
] ++ lib.optional (!enableWayland) "-DWAYLAND=OFF"
|
||||
++ lib.optional nvidiaCompat "-DNVIDIA_COMPAT=ON"
|
||||
++ lib.optional (!enablePipewire) "-DSERVICE_PIPEWIRE=OFF";
|
||||
(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 "SERVICE_PIPEWIRE" withPipewire)
|
||||
(lib.cmakeBool "SERVICE_PAM" withPam)
|
||||
(lib.cmakeBool "HYPRLAND" withHyprland)
|
||||
(lib.cmakeBool "I3" withI3)
|
||||
];
|
||||
|
||||
buildPhase = "ninjaBuildPhase";
|
||||
enableParallelBuilding = true;
|
||||
dontStrip = true;
|
||||
# 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;
|
||||
|
||||
meta = with lib; {
|
||||
homepage = "https://git.outfoxxed.me/outfoxxed/quickshell";
|
||||
description = "Simple and flexbile QtQuick based desktop shell toolkit";
|
||||
description = "Flexbile QtQuick based desktop shell toolkit";
|
||||
license = licenses.lgpl3Only;
|
||||
platforms = platforms.linux;
|
||||
};
|
||||
|
|
1
docs
1
docs
|
@ -1 +0,0 @@
|
|||
Subproject commit ff5da84a8b258a9b2caaf978ddb6de23635d8903
|
1
examples
1
examples
|
@ -1 +0,0 @@
|
|||
Subproject commit b9e744b50673304dfddb68f3da2a2e906d028b96
|
|
@ -2,11 +2,11 @@
|
|||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1709237383,
|
||||
"narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=",
|
||||
"lastModified": 1732014248,
|
||||
"narHash": "sha256-y/MEyuJ5oBWrWAic/14LaIr/u5E0wRVzyYsouYY3W6w=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8",
|
||||
"rev": "23e89b7da85c3640bbc2173fe04f4bd114342367",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
@ -12,10 +12,8 @@
|
|||
quickshell = pkgs.callPackage ./default.nix {
|
||||
gitRev = self.rev or self.dirtyRev;
|
||||
};
|
||||
quickshell-nvidia = quickshell.override { nvidiaCompat = true; };
|
||||
|
||||
default = quickshell;
|
||||
nvidia = quickshell-nvidia;
|
||||
});
|
||||
|
||||
devShells = forEachSystem (system: pkgs: rec {
|
||||
|
|
|
@ -15,13 +15,12 @@ in pkgs.mkShell.override { stdenv = quickshell.stdenv; } {
|
|||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
just
|
||||
clang-tools_17
|
||||
clang-tools
|
||||
parallel
|
||||
makeWrapper
|
||||
];
|
||||
|
||||
TIDYFOX = "${tidyfox}/lib/libtidyfox.so";
|
||||
QTWAYLANDSCANNER = quickshell.QTWAYLANDSCANNER;
|
||||
|
||||
shellHook = ''
|
||||
export CMAKE_BUILD_PARALLEL_LEVEL=$(nproc)
|
||||
|
|
|
@ -2,8 +2,18 @@ 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)
|
||||
|
@ -11,6 +21,10 @@ endif()
|
|||
|
||||
if (WAYLAND)
|
||||
add_subdirectory(wayland)
|
||||
endif ()
|
||||
endif()
|
||||
|
||||
if (X11)
|
||||
add_subdirectory(x11)
|
||||
endif()
|
||||
|
||||
add_subdirectory(services)
|
||||
|
|
26
src/build/CMakeLists.txt
Normal file
26
src/build/CMakeLists.txt
Normal file
|
@ -0,0 +1,26 @@
|
|||
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})
|
12
src/build/build.hpp.in
Normal file
12
src/build/build.hpp.in
Normal file
|
@ -0,0 +1,12 @@
|
|||
#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,20 +1,14 @@
|
|||
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
|
||||
|
@ -26,13 +20,38 @@ 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
|
||||
)
|
||||
|
||||
set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}")
|
||||
qt_add_qml_module(quickshell-core URI Quickshell VERSION 0.1)
|
||||
qt_add_qml_module(quickshell-core
|
||||
URI Quickshell
|
||||
VERSION 0.1
|
||||
DEPENDENCIES QtQuick
|
||||
OPTIONAL_IMPORTS Quickshell._Window
|
||||
DEFAULT_IMPORTS Quickshell._Window
|
||||
)
|
||||
|
||||
target_link_libraries(quickshell-core PRIVATE ${QT_DEPS})
|
||||
qs_pch(quickshell-core)
|
||||
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 PRIVATE quickshell-coreplugin)
|
||||
|
||||
|
|
97
src/core/clock.cpp
Normal file
97
src/core/clock.cpp
Normal file
|
@ -0,0 +1,97 @@
|
|||
#include "clock.hpp"
|
||||
|
||||
#include <qdatetime.h>
|
||||
#include <qobject.h>
|
||||
#include <qtimer.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "util.hpp"
|
||||
|
||||
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);
|
||||
auto dtime = offset > -500 && offset < 500 ? targetTime : currentTime;
|
||||
auto time = dtime.time();
|
||||
|
||||
auto secondPrecision = this->mPrecision >= SystemClock::Seconds;
|
||||
auto secondChanged = this->setSeconds(secondPrecision ? time.second() : 0);
|
||||
|
||||
auto minutePrecision = this->mPrecision >= SystemClock::Minutes;
|
||||
auto minuteChanged = this->setMinutes(minutePrecision ? time.minute() : 0);
|
||||
|
||||
auto hourPrecision = this->mPrecision >= SystemClock::Hours;
|
||||
auto hourChanged = this->setHours(hourPrecision ? time.hour() : 0);
|
||||
|
||||
DropEmitter::call(secondChanged, minuteChanged, hourChanged);
|
||||
}
|
||||
|
||||
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(
|
||||
{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;
|
||||
}
|
||||
|
||||
DEFINE_MEMBER_GETSET(SystemClock, hours, setHours);
|
||||
DEFINE_MEMBER_GETSET(SystemClock, minutes, setMinutes);
|
||||
DEFINE_MEMBER_GETSET(SystemClock, seconds, setSeconds);
|
72
src/core/clock.hpp
Normal file
72
src/core/clock.hpp
Normal file
|
@ -0,0 +1,72 @@
|
|||
#pragma once
|
||||
|
||||
#include <qdatetime.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtimer.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "util.hpp"
|
||||
|
||||
///! System clock accessor.
|
||||
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 hour.
|
||||
Q_PROPERTY(quint32 hours READ hours NOTIFY hoursChanged);
|
||||
/// The current minute, or 0 if @@precision is `SystemClock.Hours`.
|
||||
Q_PROPERTY(quint32 minutes READ minutes NOTIFY minutesChanged);
|
||||
/// The current second, or 0 if @@precision is `SystemClock.Hours` or `SystemClock.Minutes`.
|
||||
Q_PROPERTY(quint32 seconds READ seconds NOTIFY secondsChanged);
|
||||
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);
|
||||
|
||||
signals:
|
||||
void enabledChanged();
|
||||
void precisionChanged();
|
||||
void hoursChanged();
|
||||
void minutesChanged();
|
||||
void secondsChanged();
|
||||
|
||||
private slots:
|
||||
void onTimeout();
|
||||
|
||||
private:
|
||||
bool mEnabled = true;
|
||||
SystemClock::Enum mPrecision = SystemClock::Seconds;
|
||||
quint32 mHours = 0;
|
||||
quint32 mMinutes = 0;
|
||||
quint32 mSeconds = 0;
|
||||
QTimer timer;
|
||||
QDateTime targetTime;
|
||||
|
||||
void update();
|
||||
void setTime(const QDateTime& targetTime);
|
||||
void schedule(const QDateTime& targetTime);
|
||||
|
||||
DECLARE_PRIVATE_MEMBER(SystemClock, hours, setHours, mHours, hoursChanged);
|
||||
DECLARE_PRIVATE_MEMBER(SystemClock, minutes, setMinutes, mMinutes, minutesChanged);
|
||||
DECLARE_PRIVATE_MEMBER(SystemClock, seconds, setSeconds, mSeconds, secondsChanged);
|
||||
};
|
9
src/core/common.cpp
Normal file
9
src/core/common.cpp
Normal file
|
@ -0,0 +1,9 @@
|
|||
#include "common.hpp"
|
||||
|
||||
#include <qdatetime.h>
|
||||
|
||||
namespace qs {
|
||||
|
||||
const QDateTime Common::LAUNCH_TIME = QDateTime::currentDateTime();
|
||||
|
||||
}
|
11
src/core/common.hpp
Normal file
11
src/core/common.hpp
Normal file
|
@ -0,0 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
#include <qdatetime.h>
|
||||
|
||||
namespace qs {
|
||||
|
||||
struct Common {
|
||||
static const QDateTime LAUNCH_TIME;
|
||||
};
|
||||
|
||||
} // namespace qs
|
389
src/core/desktopentry.cpp
Normal file
389
src/core/desktopentry.cpp
Normal file
|
@ -0,0 +1,389 @@
|
|||
#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"
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
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'"') {
|
||||
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'"') {
|
||||
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.path(), 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 = new 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();
|
||||
}
|
155
src/core/desktopentry.hpp
Normal file
155
src/core/desktopentry.hpp
Normal file
|
@ -0,0 +1,155 @@
|
|||
#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,5 +10,14 @@
|
|||
#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)
|
||||
|
|
22
src/core/elapsedtimer.cpp
Normal file
22
src/core/elapsedtimer.cpp
Normal file
|
@ -0,0 +1,22 @@
|
|||
#include "elapsedtimer.hpp"
|
||||
|
||||
#include <qtypes.h>
|
||||
|
||||
ElapsedTimer::ElapsedTimer() { this->timer.start(); }
|
||||
|
||||
qreal ElapsedTimer::elapsed() { return static_cast<qreal>(this->elapsedNs()) / 1000000000.0; }
|
||||
|
||||
qreal ElapsedTimer::restart() { return static_cast<qreal>(this->restartNs()) / 1000000000.0; }
|
||||
|
||||
qint64 ElapsedTimer::elapsedMs() { return this->timer.elapsed(); }
|
||||
|
||||
qint64 ElapsedTimer::restartMs() { return this->timer.restart(); }
|
||||
|
||||
qint64 ElapsedTimer::elapsedNs() { return this->timer.nsecsElapsed(); }
|
||||
|
||||
qint64 ElapsedTimer::restartNs() {
|
||||
// see qelapsedtimer.cpp
|
||||
auto old = this->timer;
|
||||
this->timer.start();
|
||||
return old.durationTo(this->timer).count();
|
||||
}
|
45
src/core/elapsedtimer.hpp
Normal file
45
src/core/elapsedtimer.hpp
Normal file
|
@ -0,0 +1,45 @@
|
|||
#pragma once
|
||||
|
||||
#include <qelapsedtimer.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
///! Measures time between events
|
||||
/// The ElapsedTimer measures time since its last restart, and is useful
|
||||
/// for determining the time between events that don't supply it.
|
||||
class ElapsedTimer: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
|
||||
public:
|
||||
explicit ElapsedTimer();
|
||||
|
||||
/// Return the number of seconds since the timer was last
|
||||
/// started or restarted, with nanosecond precision.
|
||||
Q_INVOKABLE qreal elapsed();
|
||||
|
||||
/// Restart the timer, returning the number of seconds since
|
||||
/// the timer was last started or restarted, with nanosecond precision.
|
||||
Q_INVOKABLE qreal restart();
|
||||
|
||||
/// Return the number of milliseconds since the timer was last
|
||||
/// started or restarted.
|
||||
Q_INVOKABLE qint64 elapsedMs();
|
||||
|
||||
/// Restart the timer, returning the number of milliseconds since
|
||||
/// the timer was last started or restarted.
|
||||
Q_INVOKABLE qint64 restartMs();
|
||||
|
||||
/// Return the number of nanoseconds since the timer was last
|
||||
/// started or restarted.
|
||||
Q_INVOKABLE qint64 elapsedNs();
|
||||
|
||||
/// Restart the timer, returning the number of nanoseconds since
|
||||
/// the timer was last started or restarted.
|
||||
Q_INVOKABLE qint64 restartNs();
|
||||
|
||||
private:
|
||||
QElapsedTimer timer;
|
||||
};
|
|
@ -4,6 +4,8 @@
|
|||
#include <qcontainerfwd.h>
|
||||
#include <qcoreapplication.h>
|
||||
#include <qdebug.h>
|
||||
#include <qdir.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qfilesystemwatcher.h>
|
||||
#include <qhash.h>
|
||||
#include <qlogging.h>
|
||||
|
@ -12,7 +14,6 @@
|
|||
#include <qqmlcontext.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qqmlincubator.h>
|
||||
#include <qtimer.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "iconimageprovider.hpp"
|
||||
|
@ -23,10 +24,12 @@
|
|||
#include "reload.hpp"
|
||||
#include "scan.hpp"
|
||||
|
||||
static QHash<QQmlEngine*, EngineGeneration*> g_generations; // NOLINT
|
||||
static QHash<const QQmlEngine*, EngineGeneration*> g_generations; // NOLINT
|
||||
|
||||
EngineGeneration::EngineGeneration(QmlScanner scanner)
|
||||
: scanner(std::move(scanner))
|
||||
EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
|
||||
: rootPath(rootPath)
|
||||
, scanner(std::move(scanner))
|
||||
, urlInterceptor(this->rootPath)
|
||||
, interceptNetFactory(this->scanner.qmldirIntercepts)
|
||||
, engine(new QQmlEngine()) {
|
||||
g_generations.insert(this->engine, this);
|
||||
|
@ -39,56 +42,93 @@ EngineGeneration::EngineGeneration(QmlScanner scanner)
|
|||
this->engine->addImageProvider("qsimage", new QsImageProvider());
|
||||
this->engine->addImageProvider("qspixmap", new QsPixmapProvider());
|
||||
|
||||
QuickshellPlugin::runConstructGeneration(*this);
|
||||
QsEnginePlugin::runConstructGeneration(*this);
|
||||
}
|
||||
|
||||
EngineGeneration::~EngineGeneration() {
|
||||
g_generations.remove(this->engine);
|
||||
delete this->engine;
|
||||
if (this->engine != nullptr) {
|
||||
qFatal() << this << "destroyed without calling destroy()";
|
||||
}
|
||||
}
|
||||
|
||||
void EngineGeneration::destroy() {
|
||||
// Multiple generations can detect a reload at the same time.
|
||||
delete this->watcher;
|
||||
this->watcher = nullptr;
|
||||
if (this->destroying) return;
|
||||
this->destroying = true;
|
||||
|
||||
// Yes all of this is actually necessary.
|
||||
if (this->engine != nullptr && this->root != 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) {
|
||||
QObject::connect(this->root, &QObject::destroyed, this, [this]() {
|
||||
// The timer seems to fix *one* of the possible qml item destructor crashes.
|
||||
QTimer::singleShot(0, [this]() {
|
||||
// Garbage is not collected during engine destruction.
|
||||
this->engine->collectGarbage();
|
||||
// prevent further js execution between garbage collection and engine destruction.
|
||||
this->engine->setInterrupted(true);
|
||||
|
||||
QObject::connect(this->engine, &QObject::destroyed, this, [this]() { delete this; });
|
||||
g_generations.remove(this->engine);
|
||||
|
||||
// Even after all of that there's still multiple failing assertions and segfaults.
|
||||
// Pray you don't hit one.
|
||||
// Note: it appeats *some* of the crashes are related to values owned by the generation.
|
||||
// Test by commenting the connect() above.
|
||||
this->engine->deleteLater();
|
||||
this->engine = nullptr;
|
||||
});
|
||||
// Garbage is not collected during engine destruction.
|
||||
this->engine->collectGarbage();
|
||||
|
||||
delete this->engine;
|
||||
this->engine = nullptr;
|
||||
|
||||
auto terminate = this->shouldTerminate;
|
||||
auto code = this->exitCode;
|
||||
delete this;
|
||||
|
||||
if (terminate) QCoreApplication::exit(code);
|
||||
});
|
||||
|
||||
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
|
||||
old->incubationControllers.clear();
|
||||
qCDebug(logIncubator) << "Locking incubation controllers of old generation" << old;
|
||||
old->incubationControllersLocked = true;
|
||||
old->assignIncubationController();
|
||||
}
|
||||
|
||||
auto* app = QCoreApplication::instance();
|
||||
QObject::connect(this->engine, &QQmlEngine::quit, app, &QCoreApplication::quit);
|
||||
QObject::connect(this->engine, &QQmlEngine::exit, app, &QCoreApplication::exit);
|
||||
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);
|
||||
}
|
||||
|
||||
this->root->reload(old == nullptr ? nullptr : old->root);
|
||||
this->singletonRegistry.onReload(old == nullptr ? nullptr : &old->singletonRegistry);
|
||||
this->reloadComplete = true;
|
||||
emit this->reloadFinished();
|
||||
|
@ -105,7 +145,7 @@ void EngineGeneration::postReload() {
|
|||
// This can be called on a generation during its destruction.
|
||||
if (this->engine == nullptr || this->root == nullptr) return;
|
||||
|
||||
QuickshellPlugin::runOnReload();
|
||||
QsEnginePlugin::runOnReload();
|
||||
PostReloadHook::postReloadTree(this->root);
|
||||
this->singletonRegistry.onPostReload();
|
||||
}
|
||||
|
@ -117,13 +157,21 @@ void EngineGeneration::setWatchingFiles(bool watching) {
|
|||
|
||||
for (auto& file: this->scanner.scannedFiles) {
|
||||
this->watcher->addPath(file);
|
||||
this->watcher->addPath(QFileInfo(file).dir().absolutePath());
|
||||
}
|
||||
|
||||
QObject::connect(
|
||||
this->watcher,
|
||||
&QFileSystemWatcher::fileChanged,
|
||||
this,
|
||||
&EngineGeneration::filesChanged
|
||||
&EngineGeneration::onFileChanged
|
||||
);
|
||||
|
||||
QObject::connect(
|
||||
this->watcher,
|
||||
&QFileSystemWatcher::directoryChanged,
|
||||
this,
|
||||
&EngineGeneration::onDirectoryChanged
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
@ -134,28 +182,44 @@ void EngineGeneration::setWatchingFiles(bool watching) {
|
|||
}
|
||||
}
|
||||
|
||||
void EngineGeneration::registerIncubationController(QQmlIncubationController* controller) {
|
||||
auto* obj = dynamic_cast<QObject*>(controller);
|
||||
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) {
|
||||
// We only want controllers that we can swap out if destroyed.
|
||||
// This happens if the window owning the active controller dies.
|
||||
if (obj == nullptr) {
|
||||
qCDebug(logIncubator) << "Could not register incubation controller as it is not a QObject"
|
||||
<< controller;
|
||||
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;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this->incubationControllers.push_back({controller, obj});
|
||||
|
||||
QObject::connect(
|
||||
obj,
|
||||
&QObject::destroyed,
|
||||
this,
|
||||
&EngineGeneration::incubationControllerDestroyed
|
||||
);
|
||||
|
||||
qCDebug(logIncubator) << "Registered incubation controller" << controller;
|
||||
this->incubationControllers.push_back(controller);
|
||||
qCDebug(logIncubator) << "Registered incubation controller" << controller << "to generation"
|
||||
<< this;
|
||||
|
||||
// This function can run during destruction.
|
||||
if (this->engine == nullptr) return;
|
||||
|
@ -166,22 +230,20 @@ void EngineGeneration::registerIncubationController(QQmlIncubationController* co
|
|||
}
|
||||
|
||||
void EngineGeneration::deregisterIncubationController(QQmlIncubationController* controller) {
|
||||
QObject* obj = nullptr;
|
||||
this->incubationControllers.removeIf([&](QPair<QQmlIncubationController*, QObject*> other) {
|
||||
if (controller == other.first) {
|
||||
obj = other.second;
|
||||
return true;
|
||||
} else return false;
|
||||
});
|
||||
|
||||
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 {
|
||||
if (auto* obj = dynamic_cast<QObject*>(controller)) {
|
||||
QObject::disconnect(obj, nullptr, this, nullptr);
|
||||
qCDebug(logIncubator) << "Deregistered incubation controller" << controller;
|
||||
} else {
|
||||
qCCritical(logIncubator) << "Deregistering incubation controller which is not a QObject, "
|
||||
"however only QObject controllers should be registered.";
|
||||
}
|
||||
|
||||
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;
|
||||
} else {
|
||||
qCDebug(logIncubator) << "Deregistered incubation controller" << controller << "from" << this;
|
||||
}
|
||||
|
||||
// This function can run during destruction.
|
||||
|
@ -196,22 +258,25 @@ void EngineGeneration::deregisterIncubationController(QQmlIncubationController*
|
|||
|
||||
void EngineGeneration::incubationControllerDestroyed() {
|
||||
auto* sender = this->sender();
|
||||
QQmlIncubationController* controller = nullptr;
|
||||
|
||||
this->incubationControllers.removeIf([&](QPair<QQmlIncubationController*, QObject*> other) {
|
||||
if (sender == other.second) {
|
||||
controller = other.first;
|
||||
return true;
|
||||
} else return false;
|
||||
});
|
||||
auto* controller = dynamic_cast<QQmlIncubationController*>(sender);
|
||||
|
||||
if (controller == nullptr) {
|
||||
qCCritical(logIncubator) << "Destroyed incubation controller" << this->sender()
|
||||
<< "could not be identified, this may cause memory corruption";
|
||||
qCCritical(logIncubator) << "Destroyed incubation controller" << sender << "is not known to"
|
||||
<< this << ", 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 {
|
||||
qCDebug(logIncubator) << "Destroyed incubation controller" << controller << "deregistered";
|
||||
qCCritical(logIncubator) << "Destroyed incubation controller" << controller
|
||||
<< "was not registered, but its destruction was observed by" << this;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// This function can run during destruction.
|
||||
|
@ -224,23 +289,64 @@ 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;
|
||||
|
||||
qCDebug(logIncubator) << "Assigning incubation controller to engine:" << controller
|
||||
if (this->incubationControllersLocked || this->incubationControllers.isEmpty()) {
|
||||
controller = &this->delayedIncubationController;
|
||||
} else {
|
||||
controller = this->incubationControllers.first();
|
||||
}
|
||||
|
||||
qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation"
|
||||
<< this
|
||||
<< "fallback:" << (controller == &this->delayedIncubationController);
|
||||
|
||||
this->engine->setIncubationController(controller);
|
||||
}
|
||||
|
||||
EngineGeneration* EngineGeneration::findObjectGeneration(QObject* object) {
|
||||
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();
|
||||
|
||||
while (object != nullptr) {
|
||||
auto* context = QQmlEngine::contextForObject(object);
|
||||
|
||||
if (context != nullptr) {
|
||||
if (auto* generation = g_generations.value(context->engine())) {
|
||||
if (auto* generation = EngineGeneration::findEngineGeneration(context->engine())) {
|
||||
return generation;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,34 @@
|
|||
#pragma once
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdir.h>
|
||||
#include <qfilesystemwatcher.h>
|
||||
#include <qhash.h>
|
||||
#include <qobject.h>
|
||||
#include <qpair.h>
|
||||
#include <qqmlengine.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(QmlScanner scanner);
|
||||
explicit EngineGeneration(const QDir& rootPath, QmlScanner scanner);
|
||||
~EngineGeneration() override;
|
||||
Q_DISABLE_COPY_MOVE(EngineGeneration);
|
||||
|
||||
|
@ -30,30 +39,55 @@ public:
|
|||
void registerIncubationController(QQmlIncubationController* controller);
|
||||
void deregisterIncubationController(QQmlIncubationController* controller);
|
||||
|
||||
static EngineGeneration* findObjectGeneration(QObject* object);
|
||||
// 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();
|
||||
|
||||
RootWrapper* wrapper = nullptr;
|
||||
QDir rootPath;
|
||||
QmlScanner scanner;
|
||||
QsUrlInterceptor urlInterceptor;
|
||||
QsInterceptNetworkAccessManagerFactory interceptNetFactory;
|
||||
QQmlEngine* engine = nullptr;
|
||||
ShellRoot* root = nullptr;
|
||||
QObject* 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<QPair<QQmlIncubationController*, QObject*>> incubationControllers;
|
||||
QVector<QQmlIncubationController*> incubationControllers;
|
||||
bool incubationControllersLocked = false;
|
||||
QHash<const void*, EngineGenerationExt*> extensions;
|
||||
|
||||
bool destroying = false;
|
||||
bool shouldTerminate = false;
|
||||
int exitCode = 0;
|
||||
};
|
||||
|
|
|
@ -11,7 +11,9 @@
|
|||
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);
|
||||
|
@ -19,10 +21,17 @@ 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 {
|
||||
iconName = id;
|
||||
splitIdx = id.indexOf("?fallback=");
|
||||
if (splitIdx != -1) {
|
||||
iconName = id.sliced(0, splitIdx);
|
||||
fallbackName = id.sliced(splitIdx + 10);
|
||||
} else {
|
||||
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);
|
||||
|
@ -55,12 +64,20 @@ QPixmap IconImageProvider::missingPixmap(const QSize& size) {
|
|||
return pixmap;
|
||||
}
|
||||
|
||||
QString IconImageProvider::requestString(const QString& icon, const QString& path) {
|
||||
QString IconImageProvider::requestString(
|
||||
const QString& icon,
|
||||
const QString& path,
|
||||
const QString& fallback
|
||||
) {
|
||||
auto req = "image://icon/" + icon;
|
||||
|
||||
if (!path.isEmpty()) {
|
||||
req += "?path=" + path;
|
||||
}
|
||||
|
||||
if (!fallback.isEmpty()) {
|
||||
req += "?fallback=" + fallback;
|
||||
}
|
||||
|
||||
return req;
|
||||
}
|
||||
|
|
|
@ -10,5 +10,10 @@ 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);
|
||||
|
||||
static QString requestString(
|
||||
const QString& icon,
|
||||
const QString& path = QString(),
|
||||
const QString& fallback = QString()
|
||||
);
|
||||
};
|
||||
|
|
105
src/core/iconprovider.cpp
Normal file
105
src/core/iconprovider.cpp
Normal file
|
@ -0,0 +1,105 @@
|
|||
#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);
|
||||
}
|
8
src/core/iconprovider.hpp
Normal file
8
src/core/iconprovider.hpp
Normal file
|
@ -0,0 +1,8 @@
|
|||
#pragma once
|
||||
|
||||
#include <qicon.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qurl.h>
|
||||
|
||||
QIcon getEngineImageAsIcon(QQmlEngine* engine, const QUrl& url);
|
||||
QIcon getCurrentEngineImageAsIcon(const QUrl& url);
|
35
src/core/instanceinfo.cpp
Normal file
35
src/core/instanceinfo.cpp
Normal file
|
@ -0,0 +1,35 @@
|
|||
#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
|
||||
|
||||
}
|
39
src/core/instanceinfo.hpp
Normal file
39
src/core/instanceinfo.hpp
Normal file
|
@ -0,0 +1,39 @@
|
|||
#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,7 +179,9 @@ void LazyLoader::incubateIfReady(bool overrideReloadCheck) {
|
|||
|
||||
void LazyLoader::onIncubationCompleted() {
|
||||
this->setItem(this->incubator->object());
|
||||
delete this->incubator;
|
||||
// The incubator is not necessarily inert at the time of this callback,
|
||||
// so deleteLater is required.
|
||||
this->incubator->deleteLater();
|
||||
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](../variants) does not corrently support asynchronous
|
||||
/// > Notably, @@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` or `active`.
|
||||
/// The fully loaded item if the loader is @@loading or @@active, or `null`
|
||||
/// if neither @@loading nor @@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 the `activeChanged` signal to
|
||||
/// > You can instead set @@loading and listen to @@activeChanged(s) 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](#prop.activeAsync).
|
||||
/// See also: @@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](#prop.activeAsync).
|
||||
/// See also: @@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](#prop.loading). Reading it or setting it to false will behanve
|
||||
/// the same as [active](#prop.active).
|
||||
/// @@loading. Reading it or setting it to false will behanve
|
||||
/// the same as @@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;
|
||||
|
|
937
src/core/logging.cpp
Normal file
937
src/core/logging.cpp
Normal file
|
@ -0,0 +1,937 @@
|
|||
#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
|
148
src/core/logging.hpp
Normal file
148
src/core/logging.hpp
Normal file
|
@ -0,0 +1,148 @@
|
|||
#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;
|
190
src/core/logging_p.hpp
Normal file
190
src/core/logging_p.hpp
Normal file
|
@ -0,0 +1,190 @@
|
|||
#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
|
138
src/core/logging_qtprivate.cpp
Normal file
138
src/core/logging_qtprivate.cpp
Normal file
|
@ -0,0 +1,138 @@
|
|||
// 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
|
45
src/core/logging_qtprivate.hpp
Normal file
45
src/core/logging_qtprivate.hpp
Normal file
|
@ -0,0 +1,45 @@
|
|||
#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
|
|
@ -1,331 +0,0 @@
|
|||
#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;
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
int qs_main(int argc, char** argv); // NOLINT
|
98
src/core/model.cpp
Normal file
98
src/core/model.cpp
Normal file
|
@ -0,0 +1,98 @@
|
|||
#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;
|
||||
}
|
111
src/core/model.hpp
Normal file
111
src/core/model.hpp
Normal file
|
@ -0,0 +1,111 @@
|
|||
#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,16 +7,26 @@ headers = [
|
|||
"shell.hpp",
|
||||
"variants.hpp",
|
||||
"region.hpp",
|
||||
"proxywindow.hpp",
|
||||
"../window/proxywindow.hpp",
|
||||
"persistentprops.hpp",
|
||||
"windowinterface.hpp",
|
||||
"panelinterface.hpp",
|
||||
"floatingwindow.hpp",
|
||||
"popupwindow.hpp",
|
||||
"../window/windowinterface.hpp",
|
||||
"../window/panelinterface.hpp",
|
||||
"../window/floatingwindow.hpp",
|
||||
"../window/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",
|
||||
]
|
||||
-----
|
||||
|
|
190
src/core/objectrepeater.cpp
Normal file
190
src/core/objectrepeater.cpp
Normal file
|
@ -0,0 +1,190 @@
|
|||
#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;
|
||||
}
|
85
src/core/objectrepeater.hpp
Normal file
85
src/core/objectrepeater.hpp
Normal file
|
@ -0,0 +1,85 @@
|
|||
#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;
|
||||
};
|
308
src/core/paths.cpp
Normal file
308
src/core/paths.cpp
Normal file
|
@ -0,0 +1,308 @@
|
|||
#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"
|
||||
|
||||
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;
|
||||
}
|
51
src/core/paths.hpp
Normal file
51
src/core/paths.hpp
Normal file
|
@ -0,0 +1,51 @@
|
|||
#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;
|
||||
};
|
324
src/core/platformmenu.cpp
Normal file
324
src/core/platformmenu.cpp
Normal file
|
@ -0,0 +1,324 @@
|
|||
#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
|
63
src/core/platformmenu.hpp
Normal file
63
src/core/platformmenu.hpp
Normal file
|
@ -0,0 +1,63 @@
|
|||
#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
|
19
src/core/platformmenu_p.hpp
Normal file
19
src/core/platformmenu_p.hpp
Normal file
|
@ -0,0 +1,19 @@
|
|||
#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,37 +5,41 @@
|
|||
|
||||
#include "generation.hpp"
|
||||
|
||||
static QVector<QuickshellPlugin*> plugins; // NOLINT
|
||||
static QVector<QsEnginePlugin*> plugins; // NOLINT
|
||||
|
||||
void QuickshellPlugin::registerPlugin(QuickshellPlugin& plugin) { plugins.push_back(&plugin); }
|
||||
void QsEnginePlugin::registerPlugin(QsEnginePlugin& plugin) { plugins.push_back(&plugin); }
|
||||
|
||||
void QuickshellPlugin::initPlugins() {
|
||||
void QsEnginePlugin::initPlugins() {
|
||||
plugins.erase(
|
||||
std::remove_if(
|
||||
plugins.begin(),
|
||||
plugins.end(),
|
||||
[](QuickshellPlugin* plugin) { return !plugin->applies(); }
|
||||
[](QsEnginePlugin* plugin) { return !plugin->applies(); }
|
||||
),
|
||||
plugins.end()
|
||||
);
|
||||
|
||||
for (QuickshellPlugin* plugin: plugins) {
|
||||
std::sort(plugins.begin(), plugins.end(), [](QsEnginePlugin* a, QsEnginePlugin* b) {
|
||||
return b->dependencies().contains(a->name());
|
||||
});
|
||||
|
||||
for (QsEnginePlugin* plugin: plugins) {
|
||||
plugin->init();
|
||||
}
|
||||
|
||||
for (QuickshellPlugin* plugin: plugins) {
|
||||
for (QsEnginePlugin* plugin: plugins) {
|
||||
plugin->registerTypes();
|
||||
}
|
||||
}
|
||||
|
||||
void QuickshellPlugin::runConstructGeneration(EngineGeneration& generation) {
|
||||
for (QuickshellPlugin* plugin: plugins) {
|
||||
void QsEnginePlugin::runConstructGeneration(EngineGeneration& generation) {
|
||||
for (QsEnginePlugin* plugin: plugins) {
|
||||
plugin->constructGeneration(generation);
|
||||
}
|
||||
}
|
||||
|
||||
void QuickshellPlugin::runOnReload() {
|
||||
for (QuickshellPlugin* plugin: plugins) {
|
||||
void QsEnginePlugin::runOnReload() {
|
||||
for (QsEnginePlugin* plugin: plugins) {
|
||||
plugin->onReload();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,25 +2,28 @@
|
|||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qfunctionpointer.h>
|
||||
#include <qlist.h>
|
||||
|
||||
class EngineGeneration;
|
||||
|
||||
class QuickshellPlugin {
|
||||
class QsEnginePlugin {
|
||||
public:
|
||||
QuickshellPlugin() = default;
|
||||
virtual ~QuickshellPlugin() = default;
|
||||
QuickshellPlugin(QuickshellPlugin&&) = delete;
|
||||
QuickshellPlugin(const QuickshellPlugin&) = delete;
|
||||
void operator=(QuickshellPlugin&&) = delete;
|
||||
void operator=(const QuickshellPlugin&) = delete;
|
||||
QsEnginePlugin() = default;
|
||||
virtual ~QsEnginePlugin() = default;
|
||||
QsEnginePlugin(QsEnginePlugin&&) = delete;
|
||||
QsEnginePlugin(const QsEnginePlugin&) = delete;
|
||||
void operator=(QsEnginePlugin&&) = delete;
|
||||
void operator=(const QsEnginePlugin&) = 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(QuickshellPlugin& plugin);
|
||||
static void registerPlugin(QsEnginePlugin& plugin);
|
||||
static void initPlugins();
|
||||
static void runConstructGeneration(EngineGeneration& generation);
|
||||
static void runOnReload();
|
||||
|
@ -30,6 +33,6 @@ public:
|
|||
#define QS_REGISTER_PLUGIN(clazz) \
|
||||
[[gnu::constructor]] void qsInitPlugin() { \
|
||||
static clazz plugin; \
|
||||
QuickshellPlugin::registerPlugin(plugin); \
|
||||
QsEnginePlugin::registerPlugin(plugin); \
|
||||
}
|
||||
// NOLINTEND
|
||||
|
|
322
src/core/popupanchor.cpp
Normal file
322
src/core/popupanchor.cpp
Normal file
|
@ -0,0 +1,322 @@
|
|||
#include "popupanchor.hpp"
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
if (effectiveX < screenGeometry.left()) {
|
||||
effectiveX = screenGeometry.left();
|
||||
}
|
||||
}
|
||||
|
||||
if (adjustment.testFlag(PopupAdjustment::SlideY)) {
|
||||
if (effectiveY + windowGeometry.height() > screenGeometry.bottom()) {
|
||||
effectiveY = screenGeometry.bottom() - windowGeometry.height() + 1;
|
||||
}
|
||||
|
||||
if (effectiveY < screenGeometry.top()) {
|
||||
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;
|
||||
}
|
174
src/core/popupanchor.hpp
Normal file
174
src/core/popupanchor.hpp
Normal file
|
@ -0,0 +1,174 @@
|
|||
#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);
|
||||
};
|
|
@ -1,161 +0,0 @@
|
|||
#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);
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
#include <qcoreapplication.h>
|
||||
#include <qdir.h>
|
||||
#include <qguiapplication.h>
|
||||
#include <qicon.h>
|
||||
#include <qjsengine.h>
|
||||
#include <qlogging.h>
|
||||
#include <qobject.h>
|
||||
|
@ -19,6 +20,7 @@
|
|||
#include <unistd.h>
|
||||
|
||||
#include "generation.hpp"
|
||||
#include "iconimageprovider.hpp"
|
||||
#include "qmlscreen.hpp"
|
||||
#include "rootwrapper.hpp"
|
||||
|
||||
|
@ -165,6 +167,12 @@ 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();
|
||||
}
|
||||
|
@ -187,3 +195,27 @@ 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,6 +98,11 @@ 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.
|
||||
|
@ -110,40 +115,62 @@ class QuickshellGlobal: public QObject {
|
|||
public:
|
||||
[[nodiscard]] qint32 processId() const;
|
||||
|
||||
QuickshellGlobal(QObject* parent = nullptr);
|
||||
|
||||
QQmlListProperty<QuickshellScreenInfo> screens();
|
||||
|
||||
/// Reload the shell from the [ShellRoot].
|
||||
/// Reload the shell.
|
||||
///
|
||||
/// `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.
|
||||
///
|
||||
/// [Reloadable]: ../reloadable
|
||||
/// See @@Reloadable for more information on what can be reloaded and how.
|
||||
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);
|
||||
};
|
||||
|
|
|
@ -12,17 +12,14 @@
|
|||
|
||||
// unfortunately QQuickScreenInfo is private.
|
||||
|
||||
/// Monitor object useful for setting the monitor for a [ShellWindow]
|
||||
/// Monitor object useful for setting the monitor for a @@QsWindow
|
||||
/// 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 [Screen] type.
|
||||
///
|
||||
/// [ShellWindow]: ../shellwindow
|
||||
/// [Screen]: https://doc.qt.io/qt-6/qml-qtquick-screen.html
|
||||
/// Due to some technical limitations, it was not possible to reuse the native qml @@QtQuick.Screen type.
|
||||
class QuickshellScreenInfo: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_NAMED_ELEMENT(ShellScreen);
|
||||
|
|
|
@ -16,7 +16,22 @@
|
|||
|
||||
Q_LOGGING_CATEGORY(logQsIntercept, "quickshell.interceptor", QtWarningMsg);
|
||||
|
||||
QUrl QsUrlInterceptor::intercept(const QUrl& url, QQmlAbstractUrlInterceptor::DataType type) {
|
||||
QUrl QsUrlInterceptor::intercept(
|
||||
const QUrl& originalUrl,
|
||||
QQmlAbstractUrlInterceptor::DataType type
|
||||
) {
|
||||
auto url = originalUrl;
|
||||
|
||||
if (url.scheme() == "root") {
|
||||
url.setScheme("qsintercept");
|
||||
|
||||
auto path = url.path();
|
||||
if (path.startsWith('/')) path = path.sliced(1);
|
||||
url.setPath(this->configRoot.filePath(path));
|
||||
|
||||
qCDebug(logQsIntercept) << "Rewrote root intercept" << originalUrl << "to" << url;
|
||||
}
|
||||
|
||||
// Some types such as Image take into account where they are loading from, and force
|
||||
// asynchronous loading over a network. qsintercept is considered to be over a network.
|
||||
if (type == QQmlAbstractUrlInterceptor::DataType::UrlString && url.scheme() == "qsintercept") {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include <qdir.h>
|
||||
#include <qhash.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qnetworkaccessmanager.h>
|
||||
|
@ -13,7 +14,12 @@ Q_DECLARE_LOGGING_CATEGORY(logQsIntercept);
|
|||
|
||||
class QsUrlInterceptor: public QQmlAbstractUrlInterceptor {
|
||||
public:
|
||||
QUrl intercept(const QUrl& url, QQmlAbstractUrlInterceptor::DataType type) override;
|
||||
explicit QsUrlInterceptor(const QDir& configRoot): configRoot(configRoot) {}
|
||||
|
||||
QUrl intercept(const QUrl& originalUrl, QQmlAbstractUrlInterceptor::DataType type) override;
|
||||
|
||||
private:
|
||||
QDir configRoot;
|
||||
};
|
||||
|
||||
class QsInterceptDataReply: public QNetworkReply {
|
||||
|
|
110
src/core/qsmenu.cpp
Normal file
110
src/core/qsmenu.cpp
Normal file
|
@ -0,0 +1,110 @@
|
|||
#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
|
163
src/core/qsmenu.hpp
Normal file
163
src/core/qsmenu.hpp
Normal file
|
@ -0,0 +1,163 @@
|
|||
#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
|
125
src/core/qsmenuanchor.cpp
Normal file
125
src/core/qsmenuanchor.cpp
Normal file
|
@ -0,0 +1,125 @@
|
|||
#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
|
86
src/core/qsmenuanchor.hpp
Normal file
86
src/core/qsmenuanchor.hpp
Normal file
|
@ -0,0 +1,86 @@
|
|||
#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
|
|
@ -9,12 +9,13 @@
|
|||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
/// Shape of a Region.
|
||||
///! Shape of a Region.
|
||||
/// See @@Region.shape.
|
||||
namespace RegionShape { // NOLINT
|
||||
Q_NAMESPACE;
|
||||
QML_ELEMENT;
|
||||
|
||||
enum Enum {
|
||||
enum Enum : quint8 {
|
||||
Rect = 0,
|
||||
Ellipse = 1,
|
||||
};
|
||||
|
@ -23,11 +24,12 @@ Q_ENUM_NS(Enum);
|
|||
} // namespace RegionShape
|
||||
|
||||
///! Intersection strategy for Regions.
|
||||
/// See @@Region.intersection.
|
||||
namespace Intersection { // NOLINT
|
||||
Q_NAMESPACE;
|
||||
QML_ELEMENT;
|
||||
|
||||
enum Enum {
|
||||
enum Enum : quint8 {
|
||||
/// 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`)
|
||||
|
@ -44,6 +46,7 @@ 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`.
|
||||
|
@ -52,16 +55,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.
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#include <qcontainerfwd.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmllist.h>
|
||||
#include <qtimer.h>
|
||||
|
||||
#include "generation.hpp"
|
||||
|
||||
|
@ -12,8 +13,19 @@ 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) this->reload();
|
||||
else {
|
||||
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 {
|
||||
QObject::connect(
|
||||
this->engineGeneration,
|
||||
&EngineGeneration::reloadFinished,
|
||||
|
@ -40,6 +52,7 @@ 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);
|
||||
|
@ -86,7 +99,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->onReload(oldInstance);
|
||||
reloadable->reload(oldInstance);
|
||||
} else if (newObj != nullptr) {
|
||||
Reloadable::reloadChildrenRecursive(newObj, oldRoot);
|
||||
}
|
||||
|
|
|
@ -11,10 +11,7 @@ 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]
|
||||
///
|
||||
/// [ProxyWindowBase]: ../proxywindowbase
|
||||
/// [PersistentProperties]: ../persistentproperties
|
||||
/// Some examples are @@ProxyWindowBase and @@PersistentProperties
|
||||
class Reloadable
|
||||
: public QObject
|
||||
, public QQmlParserStatus {
|
||||
|
@ -28,7 +25,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
|
||||
|
@ -74,6 +71,7 @@ public:
|
|||
|
||||
private slots:
|
||||
void onReloadFinished();
|
||||
void onGenerationDestroyed();
|
||||
|
||||
protected:
|
||||
// Called unconditionally in the reload phase, with nullptr if no source could be determined.
|
||||
|
@ -86,10 +84,9 @@ private:
|
|||
};
|
||||
|
||||
///! Scope that propagates reloads to child items in order.
|
||||
/// Convenience type equivalent to setting `reloadableId` on properties in a
|
||||
/// QtObject instance.
|
||||
/// Convenience type equivalent to setting @@Reloadable.reloadableId for all children.
|
||||
///
|
||||
/// Note that this does not work for visible `Item`s (all widgets).
|
||||
/// Note that this does not work for visible @@QtQuick.Item$s (all widgets).
|
||||
///
|
||||
/// ```qml
|
||||
/// ShellRoot {
|
||||
|
|
163
src/core/retainable.cpp
Normal file
163
src/core/retainable.cpp
Normal file
|
@ -0,0 +1,163 @@
|
|||
#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(); }
|
162
src/core/retainable.hpp
Normal file
162
src/core/retainable.hpp
Normal file
|
@ -0,0 +1,162 @@
|
|||
#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;
|
||||
};
|
169
src/core/ringbuf.hpp
Normal file
169
src/core/ringbuf.hpp
Normal file
|
@ -0,0 +1,169 @@
|
|||
#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,16 +8,19 @@
|
|||
#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)
|
||||
RootWrapper::RootWrapper(QString rootPath, QString shellId)
|
||||
: 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);
|
||||
|
@ -34,18 +37,16 @@ RootWrapper::RootWrapper(QString rootPath)
|
|||
RootWrapper::~RootWrapper() {
|
||||
// event loop may no longer be running so deleteLater is not an option
|
||||
if (this->generation != nullptr) {
|
||||
delete this->generation->root;
|
||||
this->generation->root = nullptr;
|
||||
this->generation->shutdown();
|
||||
}
|
||||
|
||||
delete this->generation;
|
||||
}
|
||||
|
||||
void RootWrapper::reloadGraph(bool hard) {
|
||||
auto scanner = QmlScanner();
|
||||
auto rootPath = QFileInfo(this->rootPath).dir();
|
||||
auto scanner = QmlScanner(rootPath);
|
||||
scanner.scanQmlFile(this->rootPath);
|
||||
|
||||
auto* generation = new EngineGeneration(std::move(scanner));
|
||||
auto* generation = new EngineGeneration(rootPath, std::move(scanner));
|
||||
generation->wrapper = this;
|
||||
|
||||
// todo: move into EngineGeneration
|
||||
|
@ -60,33 +61,49 @@ void RootWrapper::reloadGraph(bool hard) {
|
|||
url.setScheme("qsintercept");
|
||||
auto component = QQmlComponent(generation->engine, url);
|
||||
|
||||
auto* obj = component.beginCreate(generation->engine->rootContext());
|
||||
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);
|
||||
}
|
||||
|
||||
if (obj == nullptr) {
|
||||
qWarning() << component.errorString().toStdString().c_str();
|
||||
qWarning() << "failed to create root component";
|
||||
delete generation;
|
||||
return;
|
||||
}
|
||||
|
||||
auto* newRoot = qobject_cast<ShellRoot*>(obj);
|
||||
if (newRoot == nullptr) {
|
||||
qWarning() << "root component was not a Quickshell.ShellRoot";
|
||||
delete obj;
|
||||
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;
|
||||
}
|
||||
|
||||
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) delete this->generation;
|
||||
|
||||
if (hard && this->generation) {
|
||||
this->generation->destroy();
|
||||
}
|
||||
|
||||
this->generation = generation;
|
||||
|
||||
qInfo() << "Configuration Loaded";
|
||||
|
||||
QObject::connect(this->generation, &QObject::destroyed, this, &RootWrapper::generationDestroyed);
|
||||
QObject::connect(
|
||||
this->generation,
|
||||
&EngineGeneration::filesChanged,
|
||||
|
@ -95,8 +112,14 @@ 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,18 +12,20 @@ class RootWrapper: public QObject {
|
|||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
explicit RootWrapper(QString rootPath);
|
||||
explicit RootWrapper(QString rootPath, QString shellId);
|
||||
~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,7 +103,15 @@ bool QmlScanner::scanQmlFile(const QString& path) {
|
|||
this->scanDir(currentdir.path());
|
||||
|
||||
for (auto& import: imports) {
|
||||
auto ipath = currentdir.filePath(import);
|
||||
QString ipath;
|
||||
if (import.startsWith("root:")) {
|
||||
auto path = import.sliced(5);
|
||||
if (path.startsWith('/')) path = path.sliced(1);
|
||||
ipath = this->rootPath.filePath(path);
|
||||
} else {
|
||||
ipath = currentdir.filePath(import);
|
||||
}
|
||||
|
||||
auto cpath = QFileInfo(ipath).canonicalFilePath();
|
||||
|
||||
if (cpath.isEmpty()) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdir.h>
|
||||
#include <qhash.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qvector.h>
|
||||
|
@ -10,6 +11,8 @@ Q_DECLARE_LOGGING_CATEGORY(logQmlScanner);
|
|||
// expects canonical paths
|
||||
class QmlScanner {
|
||||
public:
|
||||
QmlScanner(const QDir& rootPath): rootPath(rootPath) {}
|
||||
|
||||
void scanDir(const QString& path);
|
||||
// returns if the file has a singleton
|
||||
bool scanQmlFile(const QString& path);
|
||||
|
@ -17,4 +20,7 @@ public:
|
|||
QVector<QString> scannedDirs;
|
||||
QVector<QString> scannedFiles;
|
||||
QHash<QString, QString> qmldirIntercepts;
|
||||
|
||||
private:
|
||||
QDir rootPath;
|
||||
};
|
||||
|
|
133
src/core/scriptmodel.cpp
Normal file
133
src/core/scriptmodel.cpp
Normal file
|
@ -0,0 +1,133 @@
|
|||
#include "scriptmodel.hpp"
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
|
||||
#include <qabstractitemmodel.h>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qlist.h>
|
||||
#include <qnamespace.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtversionchecks.h>
|
||||
#include <qtypes.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
void ScriptModel::updateValuesUnique(const QVariantList& newValues) {
|
||||
this->mValues.reserve(newValues.size());
|
||||
|
||||
auto iter = this->mValues.begin();
|
||||
auto newIter = newValues.begin();
|
||||
|
||||
while (true) {
|
||||
if (newIter == newValues.end()) {
|
||||
if (iter == this->mValues.end()) break;
|
||||
|
||||
auto startIndex = static_cast<qint32>(newValues.length());
|
||||
auto endIndex = static_cast<qint32>(this->mValues.length() - 1);
|
||||
|
||||
this->beginRemoveRows(QModelIndex(), startIndex, endIndex);
|
||||
this->mValues.erase(iter, this->mValues.end());
|
||||
this->endRemoveRows();
|
||||
|
||||
break;
|
||||
} else if (iter == this->mValues.end()) {
|
||||
// Prior branch ensures length is at least 1.
|
||||
auto startIndex = static_cast<qint32>(this->mValues.length());
|
||||
auto endIndex = static_cast<qint32>(newValues.length() - 1);
|
||||
|
||||
this->beginInsertRows(QModelIndex(), startIndex, endIndex);
|
||||
this->mValues.append(newValues.sliced(startIndex));
|
||||
this->endInsertRows();
|
||||
|
||||
break;
|
||||
} else if (*newIter != *iter) {
|
||||
auto oldIter = std::find(iter, this->mValues.end(), *newIter);
|
||||
|
||||
if (oldIter != this->mValues.end()) {
|
||||
if (std::find(newIter, newValues.end(), *iter) == newValues.end()) {
|
||||
// Remove any entries we would otherwise move around that aren't in the new list.
|
||||
auto startIter = iter;
|
||||
|
||||
do {
|
||||
++iter;
|
||||
} while (iter != this->mValues.end()
|
||||
&& std::find(newIter, newValues.end(), *iter) == newValues.end());
|
||||
|
||||
auto index = static_cast<qint32>(std::distance(this->mValues.begin(), iter));
|
||||
auto startIndex = static_cast<qint32>(std::distance(this->mValues.begin(), startIter));
|
||||
|
||||
this->beginRemoveRows(QModelIndex(), startIndex, index - 1);
|
||||
iter = this->mValues.erase(startIter, iter);
|
||||
this->endRemoveRows();
|
||||
} else {
|
||||
// Advance iters to capture a whole move sequence as a single operation if possible.
|
||||
auto oldStartIter = oldIter;
|
||||
do {
|
||||
++oldIter;
|
||||
++newIter;
|
||||
} while (oldIter != this->mValues.end() && newIter != newValues.end()
|
||||
&& *oldIter == *newIter);
|
||||
|
||||
auto index = static_cast<qint32>(std::distance(this->mValues.begin(), iter));
|
||||
auto oldStartIndex =
|
||||
static_cast<qint32>(std::distance(this->mValues.begin(), oldStartIter));
|
||||
auto oldIndex = static_cast<qint32>(std::distance(this->mValues.begin(), oldIter));
|
||||
auto len = oldIndex - oldStartIndex;
|
||||
|
||||
this->beginMoveRows(QModelIndex(), oldStartIndex, oldIndex - 1, QModelIndex(), index);
|
||||
|
||||
// While it is possible to optimize this further, it is currently not worth the time.
|
||||
for (auto i = 0; i != len; i++) {
|
||||
this->mValues.move(oldStartIndex + i, index + i);
|
||||
}
|
||||
|
||||
iter = this->mValues.begin() + (index + len);
|
||||
this->endMoveRows();
|
||||
}
|
||||
} else {
|
||||
auto startNewIter = newIter;
|
||||
|
||||
do {
|
||||
newIter++;
|
||||
} while (newIter != newValues.end()
|
||||
&& std::find(iter, this->mValues.end(), *newIter) == this->mValues.end());
|
||||
|
||||
auto index = static_cast<qint32>(std::distance(this->mValues.begin(), iter));
|
||||
auto newIndex = static_cast<qint32>(std::distance(newValues.begin(), newIter));
|
||||
auto startNewIndex = static_cast<qint32>(std::distance(newValues.begin(), startNewIter));
|
||||
auto len = newIndex - startNewIndex;
|
||||
|
||||
this->beginInsertRows(QModelIndex(), index, index + len - 1);
|
||||
#if QT_VERSION <= QT_VERSION_CHECK(6, 8, 0)
|
||||
this->mValues.resize(this->mValues.length() + len);
|
||||
#else
|
||||
this->mValues.resizeForOverwrite(this->mValues.length() + len);
|
||||
#endif
|
||||
iter = this->mValues.begin() + index; // invalidated
|
||||
std::move_backward(iter, this->mValues.end() - len, this->mValues.end());
|
||||
iter = std::copy(startNewIter, newIter, iter);
|
||||
this->endInsertRows();
|
||||
}
|
||||
} else {
|
||||
++iter;
|
||||
++newIter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptModel::setValues(const QVariantList& newValues) {
|
||||
if (newValues == this->mValues) return;
|
||||
this->updateValuesUnique(newValues);
|
||||
emit this->valuesChanged();
|
||||
}
|
||||
|
||||
qint32 ScriptModel::rowCount(const QModelIndex& parent) const {
|
||||
if (parent != QModelIndex()) return 0;
|
||||
return static_cast<qint32>(this->mValues.length());
|
||||
}
|
||||
|
||||
QVariant ScriptModel::data(const QModelIndex& index, qint32 role) const {
|
||||
if (role != Qt::UserRole) return QVariant();
|
||||
return this->mValues.at(index.row());
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> ScriptModel::roleNames() const { return {{Qt::UserRole, "modelData"}}; }
|
74
src/core/scriptmodel.hpp
Normal file
74
src/core/scriptmodel.hpp
Normal file
|
@ -0,0 +1,74 @@
|
|||
#pragma once
|
||||
|
||||
#include <qabstractitemmodel.h>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qproperty.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
///! QML model reflecting a javascript expression
|
||||
/// ScriptModel is a QML [Data Model] that generates model operations based on changes
|
||||
/// to a javascript expression attached to @@values.
|
||||
///
|
||||
/// ### When should I use this
|
||||
/// ScriptModel should be used when you would otherwise use a javascript expression as a model,
|
||||
/// [QAbstractItemModel] is accepted, and the data is likely to change over the lifetime of the program.
|
||||
///
|
||||
/// When directly using a javascript expression as a model, types like @@QtQuick.Repeater or @@QtQuick.ListView
|
||||
/// will destroy all created delegates, and re-create the entire list. In the case of @@QtQuick.ListView this
|
||||
/// will also prevent animations from working. If you wrap your expression with ScriptModel, only new items
|
||||
/// will be created, and ListView animations will work as expected.
|
||||
///
|
||||
/// ### Example
|
||||
/// ```qml
|
||||
/// // Will cause all delegates to be re-created every time filterText changes.
|
||||
/// @@QtQuick.Repeater {
|
||||
/// model: myList.filter(entry => entry.name.startsWith(filterText))
|
||||
/// delegate: // ...
|
||||
/// }
|
||||
///
|
||||
/// // Will add and remove delegates only when required.
|
||||
/// @@QtQuick.Repeater {
|
||||
/// model: ScriptModel {
|
||||
/// values: myList.filter(entry => entry.name.startsWith(filterText))
|
||||
/// }
|
||||
///
|
||||
/// delegate: // ...
|
||||
/// }
|
||||
/// ```
|
||||
class ScriptModel: public QAbstractListModel {
|
||||
Q_OBJECT;
|
||||
/// The list of values to reflect in the model.
|
||||
/// > [!WARNING] ScriptModel currently only works with lists of *unique* values.
|
||||
/// > There must not be any duplicates in the given list, or behavior of the model is undefined.
|
||||
///
|
||||
/// > [!TIP] @@ObjectModel$s supplied by Quickshell types will only contain unique values,
|
||||
/// > and can be used like so:
|
||||
/// >
|
||||
/// > ```qml
|
||||
/// > ScriptModel {
|
||||
/// > values: DesktopEntries.applications.values.filter(...)
|
||||
/// > }
|
||||
/// > ```
|
||||
/// >
|
||||
/// > Note that we are using @@DesktopEntries.values because it will cause @@ScriptModel.values
|
||||
/// > to receive an update on change.
|
||||
Q_PROPERTY(QVariantList values READ values WRITE setValues NOTIFY valuesChanged);
|
||||
QML_ELEMENT;
|
||||
|
||||
public:
|
||||
[[nodiscard]] const QVariantList& values() const { return this->mValues; }
|
||||
void setValues(const QVariantList& newValues);
|
||||
|
||||
[[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;
|
||||
|
||||
signals:
|
||||
void valuesChanged();
|
||||
|
||||
private:
|
||||
QVariantList mValues;
|
||||
|
||||
void updateValuesUnique(const QVariantList& newValues);
|
||||
};
|
|
@ -8,7 +8,7 @@
|
|||
#include "qmlglobal.hpp"
|
||||
#include "reload.hpp"
|
||||
|
||||
///! Root config element
|
||||
///! Optional root config element, allowing some settings to be specified inline.
|
||||
class ShellRoot: public ReloadPropagator {
|
||||
Q_OBJECT;
|
||||
Q_PROPERTY(QuickshellSettings* settings READ settings CONSTANT);
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
function (qs_test name)
|
||||
add_executable(${name} ${ARGN})
|
||||
target_link_libraries(${name} PRIVATE ${QT_DEPS} Qt6::Test quickshell-core)
|
||||
target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-core quickshell-window)
|
||||
add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $<TARGET_FILE:${name}>)
|
||||
endfunction()
|
||||
|
||||
qs_test(popupwindow popupwindow.cpp)
|
||||
qs_test(transformwatcher transformwatcher.cpp)
|
||||
qs_test(ringbuffer ringbuf.cpp)
|
||||
qs_test(scriptmodel scriptmodel.cpp)
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class TestPopupWindow: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
private slots:
|
||||
void initiallyVisible();
|
||||
void reloadReparent();
|
||||
void reloadUnparent();
|
||||
void invisibleWithoutParent();
|
||||
void moveWithParent();
|
||||
void attachParentLate();
|
||||
void reparentLate();
|
||||
void xMigrationFix();
|
||||
};
|
125
src/core/test/ringbuf.cpp
Normal file
125
src/core/test/ringbuf.cpp
Normal file
|
@ -0,0 +1,125 @@
|
|||
#include "ringbuf.hpp"
|
||||
#include <utility>
|
||||
|
||||
#include <qlogging.h>
|
||||
#include <qtest.h>
|
||||
#include <qtestcase.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "../ringbuf.hpp"
|
||||
|
||||
TestObject::TestObject(quint32* count): count(count) {
|
||||
(*this->count)++;
|
||||
qDebug() << "Created TestObject" << this << "- count is now" << *this->count;
|
||||
}
|
||||
|
||||
TestObject::~TestObject() {
|
||||
(*this->count)--;
|
||||
qDebug() << "Destroyed TestObject" << this << "- count is now" << *this->count;
|
||||
}
|
||||
|
||||
void TestRingBuffer::fill() {
|
||||
quint32 counter = 0;
|
||||
auto rb = RingBuffer<TestObject>(3);
|
||||
QCOMPARE(rb.capacity(), 3);
|
||||
|
||||
qInfo() << "adding test objects";
|
||||
auto* n1 = &rb.emplace(&counter);
|
||||
auto* n2 = &rb.emplace(&counter);
|
||||
auto* n3 = &rb.emplace(&counter);
|
||||
QCOMPARE(counter, 3);
|
||||
QCOMPARE(rb.size(), 3);
|
||||
QCOMPARE(&rb.at(0), n3);
|
||||
QCOMPARE(&rb.at(1), n2);
|
||||
QCOMPARE(&rb.at(2), n1);
|
||||
|
||||
qInfo() << "replacing last object with new one";
|
||||
auto* n4 = &rb.emplace(&counter);
|
||||
QCOMPARE(counter, 3);
|
||||
QCOMPARE(rb.size(), 3);
|
||||
QCOMPARE(&rb.at(0), n4);
|
||||
QCOMPARE(&rb.at(1), n3);
|
||||
QCOMPARE(&rb.at(2), n2);
|
||||
|
||||
qInfo() << "replacing the rest";
|
||||
auto* n5 = &rb.emplace(&counter);
|
||||
auto* n6 = &rb.emplace(&counter);
|
||||
QCOMPARE(counter, 3);
|
||||
QCOMPARE(rb.size(), 3);
|
||||
QCOMPARE(&rb.at(0), n6);
|
||||
QCOMPARE(&rb.at(1), n5);
|
||||
QCOMPARE(&rb.at(2), n4);
|
||||
|
||||
qInfo() << "clearing buffer";
|
||||
rb.clear();
|
||||
QCOMPARE(counter, 0);
|
||||
QCOMPARE(rb.size(), 0);
|
||||
}
|
||||
|
||||
void TestRingBuffer::clearPartial() {
|
||||
quint32 counter = 0;
|
||||
auto rb = RingBuffer<TestObject>(2);
|
||||
|
||||
qInfo() << "adding object to buffer";
|
||||
auto* n1 = &rb.emplace(&counter);
|
||||
QCOMPARE(counter, 1);
|
||||
QCOMPARE(rb.size(), 1);
|
||||
QCOMPARE(&rb.at(0), n1);
|
||||
|
||||
qInfo() << "clearing buffer";
|
||||
rb.clear();
|
||||
QCOMPARE(counter, 0);
|
||||
QCOMPARE(rb.size(), 0);
|
||||
}
|
||||
|
||||
void TestRingBuffer::move() {
|
||||
quint32 counter = 0;
|
||||
|
||||
{
|
||||
auto rb1 = RingBuffer<TestObject>(1);
|
||||
|
||||
qInfo() << "adding object to first buffer";
|
||||
auto* n1 = &rb1.emplace(&counter);
|
||||
QCOMPARE(counter, 1);
|
||||
QCOMPARE(rb1.size(), 1);
|
||||
QCOMPARE(&rb1.at(0), n1);
|
||||
|
||||
qInfo() << "move constructing new buffer";
|
||||
auto rb2 = RingBuffer<TestObject>(std::move(rb1));
|
||||
QCOMPARE(counter, 1);
|
||||
QCOMPARE(rb2.size(), 1);
|
||||
QCOMPARE(&rb2.at(0), n1);
|
||||
|
||||
qInfo() << "move assigning new buffer";
|
||||
auto rb3 = RingBuffer<TestObject>();
|
||||
rb3 = std::move(rb2);
|
||||
QCOMPARE(counter, 1);
|
||||
QCOMPARE(rb3.size(), 1);
|
||||
QCOMPARE(&rb3.at(0), n1);
|
||||
}
|
||||
|
||||
QCOMPARE(counter, 0);
|
||||
}
|
||||
|
||||
void TestRingBuffer::hashLookup() {
|
||||
auto hb = HashBuffer<int>(3);
|
||||
|
||||
qInfo() << "inserting 1,2,3 into HashBuffer";
|
||||
hb.emplace(1);
|
||||
hb.emplace(2);
|
||||
hb.emplace(3);
|
||||
|
||||
qInfo() << "checking lookups";
|
||||
QCOMPARE(hb.indexOf(3), 0);
|
||||
QCOMPARE(hb.indexOf(2), 1);
|
||||
QCOMPARE(hb.indexOf(1), 2);
|
||||
|
||||
qInfo() << "adding 4";
|
||||
hb.emplace(4);
|
||||
QCOMPARE(hb.indexOf(4), 0);
|
||||
QCOMPARE(hb.indexOf(3), 1);
|
||||
QCOMPARE(hb.indexOf(2), 2);
|
||||
QCOMPARE(hb.indexOf(1), -1);
|
||||
}
|
||||
|
||||
QTEST_MAIN(TestRingBuffer);
|
27
src/core/test/ringbuf.hpp
Normal file
27
src/core/test/ringbuf.hpp
Normal file
|
@ -0,0 +1,27 @@
|
|||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qtclasshelpermacros.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
class TestObject {
|
||||
public:
|
||||
explicit TestObject(quint32* count);
|
||||
~TestObject();
|
||||
Q_DISABLE_COPY_MOVE(TestObject);
|
||||
|
||||
private:
|
||||
quint32* count;
|
||||
};
|
||||
|
||||
class TestRingBuffer: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
private slots:
|
||||
static void fill();
|
||||
static void clearPartial();
|
||||
static void move();
|
||||
|
||||
static void hashLookup();
|
||||
};
|
179
src/core/test/scriptmodel.cpp
Normal file
179
src/core/test/scriptmodel.cpp
Normal file
|
@ -0,0 +1,179 @@
|
|||
#include "scriptmodel.hpp"
|
||||
|
||||
#include <qabstractitemmodel.h>
|
||||
#include <qabstractitemmodeltester.h>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdebug.h>
|
||||
#include <qlist.h>
|
||||
#include <qlogging.h>
|
||||
#include <qobject.h>
|
||||
#include <qsignalspy.h>
|
||||
#include <qstring.h>
|
||||
#include <qtest.h>
|
||||
#include <qtestcase.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "../scriptmodel.hpp"
|
||||
|
||||
using OpList = QList<ModelOperation>;
|
||||
|
||||
bool ModelOperation::operator==(const ModelOperation& other) const {
|
||||
return other.operation == this->operation && other.index == this->index
|
||||
&& other.length == this->length && other.destIndex == this->destIndex;
|
||||
}
|
||||
|
||||
QDebug& operator<<(QDebug& debug, const ModelOperation& op) {
|
||||
auto saver = QDebugStateSaver(debug);
|
||||
debug.nospace();
|
||||
|
||||
switch (op.operation) {
|
||||
case ModelOperation::Insert: debug << "Insert"; break;
|
||||
case ModelOperation::Remove: debug << "Remove"; break;
|
||||
case ModelOperation::Move: debug << "Move"; break;
|
||||
}
|
||||
|
||||
debug << "(i: " << op.index << ", l: " << op.length;
|
||||
|
||||
if (op.destIndex != -1) {
|
||||
debug << ", d: " << op.destIndex;
|
||||
}
|
||||
|
||||
debug << ')';
|
||||
|
||||
return debug;
|
||||
}
|
||||
|
||||
QDebug& operator<<(QDebug& debug, const QVariantList& list) {
|
||||
auto str = QString();
|
||||
|
||||
for (const auto& var: list) {
|
||||
if (var.canConvert<QChar>()) {
|
||||
str += var.value<QChar>();
|
||||
} else {
|
||||
qFatal() << "QVariantList debug overridden in test";
|
||||
}
|
||||
}
|
||||
|
||||
debug << str;
|
||||
return debug;
|
||||
}
|
||||
|
||||
void TestScriptModel::unique_data() {
|
||||
QTest::addColumn<QString>("oldstr");
|
||||
QTest::addColumn<QString>("newstr");
|
||||
QTest::addColumn<OpList>("operations");
|
||||
|
||||
QTest::addRow("append") << "ABCD" << "ABCDEFG" << OpList({{ModelOperation::Insert, 4, 3}});
|
||||
|
||||
QTest::addRow("prepend") << "EFG" << "ABCDEFG" << OpList({{ModelOperation::Insert, 0, 4}});
|
||||
|
||||
QTest::addRow("insert") << "ABFG" << "ABCDEFG" << OpList({{ModelOperation::Insert, 2, 3}});
|
||||
|
||||
QTest::addRow("chop") << "ABCDEFG" << "ABCD" << OpList({{ModelOperation::Remove, 4, 3}});
|
||||
|
||||
QTest::addRow("slice") << "ABCDEFG" << "DEFG" << OpList({{ModelOperation::Remove, 0, 3}});
|
||||
|
||||
QTest::addRow("remove_mid") << "ABCDEFG" << "ABFG" << OpList({{ModelOperation::Remove, 2, 3}});
|
||||
|
||||
QTest::addRow("move_single") << "ABCDEFG" << "AFBCDEG"
|
||||
<< OpList({{ModelOperation::Move, 5, 1, 1}});
|
||||
|
||||
QTest::addRow("move_range") << "ABCDEFG" << "ADEFBCG"
|
||||
<< OpList({{ModelOperation::Move, 3, 3, 1}});
|
||||
|
||||
// beginning to end is the same operation
|
||||
QTest::addRow("move_end_to_beginning")
|
||||
<< "ABCDEFG" << "EFGABCD" << OpList({{ModelOperation::Move, 4, 3, 0}});
|
||||
|
||||
QTest::addRow("move_overlapping")
|
||||
<< "ABCDEFG" << "ABDEFCG" << OpList({{ModelOperation::Move, 3, 3, 2}});
|
||||
|
||||
// Ensure iterators arent skipping anything at the end of operations by performing
|
||||
// multiple back to back.
|
||||
|
||||
QTest::addRow("insert_state_ok") << "ABCDEFG" << "ABXXEFG"
|
||||
<< OpList({
|
||||
{ModelOperation::Insert, 2, 2}, // ABXXCDEFG
|
||||
{ModelOperation::Remove, 4, 2}, // ABXXEFG
|
||||
});
|
||||
|
||||
QTest::addRow("remove_state_ok") << "ABCDEFG" << "ABFGE"
|
||||
<< OpList({
|
||||
{ModelOperation::Remove, 2, 2}, // ABEFG
|
||||
{ModelOperation::Move, 3, 2, 2}, // ABFGE
|
||||
});
|
||||
|
||||
QTest::addRow("move_state_ok") << "ABCDEFG" << "ABEFXYCDG"
|
||||
<< OpList({
|
||||
{ModelOperation::Move, 4, 2, 2}, // ABEFCDG
|
||||
{ModelOperation::Insert, 4, 2}, // ABEFXYCDG
|
||||
});
|
||||
}
|
||||
|
||||
void TestScriptModel::unique() {
|
||||
QFETCH(const QString, oldstr);
|
||||
QFETCH(const QString, newstr);
|
||||
QFETCH(OpList, operations);
|
||||
|
||||
auto strToVariantList = [](const QString& str) -> QVariantList {
|
||||
QVariantList list;
|
||||
|
||||
for (auto c: str) {
|
||||
list.emplace_back(c);
|
||||
}
|
||||
|
||||
return list;
|
||||
};
|
||||
|
||||
auto oldlist = strToVariantList(oldstr);
|
||||
auto newlist = strToVariantList(newstr);
|
||||
|
||||
auto model = ScriptModel();
|
||||
auto modelTester = QAbstractItemModelTester(&model);
|
||||
|
||||
OpList actualOperations;
|
||||
|
||||
auto onInsert = [&](const QModelIndex& parent, int first, int last) {
|
||||
QCOMPARE(parent, QModelIndex());
|
||||
actualOperations << ModelOperation(ModelOperation::Insert, first, last - first + 1);
|
||||
};
|
||||
|
||||
auto onRemove = [&](const QModelIndex& parent, int first, int last) {
|
||||
QCOMPARE(parent, QModelIndex());
|
||||
actualOperations << ModelOperation(ModelOperation::Remove, first, last - first + 1);
|
||||
};
|
||||
|
||||
auto onMove = [&](const QModelIndex& sourceParent,
|
||||
int sourceStart,
|
||||
int sourceEnd,
|
||||
const QModelIndex& destParent,
|
||||
int destStart) {
|
||||
QCOMPARE(sourceParent, QModelIndex());
|
||||
QCOMPARE(destParent, QModelIndex());
|
||||
actualOperations << ModelOperation(
|
||||
ModelOperation::Move,
|
||||
sourceStart,
|
||||
sourceEnd - sourceStart + 1,
|
||||
destStart
|
||||
);
|
||||
};
|
||||
|
||||
QObject::connect(&model, &QAbstractItemModel::rowsInserted, &model, onInsert);
|
||||
QObject::connect(&model, &QAbstractItemModel::rowsRemoved, &model, onRemove);
|
||||
QObject::connect(&model, &QAbstractItemModel::rowsMoved, &model, onMove);
|
||||
|
||||
model.setValues(oldlist);
|
||||
QCOMPARE_EQ(model.values(), oldlist);
|
||||
QCOMPARE_EQ(
|
||||
actualOperations,
|
||||
OpList({{ModelOperation::Insert, 0, static_cast<qint32>(oldlist.length())}})
|
||||
);
|
||||
|
||||
actualOperations.clear();
|
||||
|
||||
model.setValues(newlist);
|
||||
QCOMPARE_EQ(model.values(), newlist);
|
||||
QCOMPARE_EQ(actualOperations, operations);
|
||||
}
|
||||
|
||||
QTEST_MAIN(TestScriptModel);
|
37
src/core/test/scriptmodel.hpp
Normal file
37
src/core/test/scriptmodel.hpp
Normal file
|
@ -0,0 +1,37 @@
|
|||
#pragma once
|
||||
|
||||
#include <qdebug.h>
|
||||
#include <qobject.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
struct ModelOperation {
|
||||
enum Enum : quint8 {
|
||||
Insert,
|
||||
Remove,
|
||||
Move,
|
||||
};
|
||||
|
||||
ModelOperation(Enum operation, qint32 index, qint32 length, qint32 destIndex = -1)
|
||||
: operation(operation)
|
||||
, index(index)
|
||||
, length(length)
|
||||
, destIndex(destIndex) {}
|
||||
|
||||
Enum operation;
|
||||
qint32 index = 0;
|
||||
qint32 length = 0;
|
||||
qint32 destIndex = -1;
|
||||
|
||||
[[nodiscard]] bool operator==(const ModelOperation& other) const;
|
||||
};
|
||||
|
||||
QDebug& operator<<(QDebug& debug, const ModelOperation& op);
|
||||
|
||||
class TestScriptModel: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
private slots:
|
||||
static void unique_data(); // NOLINT
|
||||
static void unique();
|
||||
};
|
|
@ -7,6 +7,7 @@
|
|||
#include <qobject.h>
|
||||
#include <qquickitem.h>
|
||||
#include <qquickwindow.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
void TransformWatcher::resolveChains(QQuickItem* a, QQuickItem* b, QQuickItem* commonParent) {
|
||||
if (a == nullptr || b == nullptr) return;
|
||||
|
@ -82,7 +83,10 @@ void TransformWatcher::linkItem(QQuickItem* item) const {
|
|||
|
||||
QObject::connect(item, &QQuickItem::parentChanged, this, &TransformWatcher::recalcChains);
|
||||
QObject::connect(item, &QQuickItem::windowChanged, this, &TransformWatcher::recalcChains);
|
||||
QObject::connect(item, &QObject::destroyed, this, &TransformWatcher::recalcChains);
|
||||
|
||||
if (item != this->mA && item != this->mB) {
|
||||
QObject::connect(item, &QObject::destroyed, this, &TransformWatcher::itemDestroyed);
|
||||
}
|
||||
}
|
||||
|
||||
void TransformWatcher::linkChains() {
|
||||
|
@ -103,6 +107,18 @@ void TransformWatcher::unlinkChains() {
|
|||
for (auto* item: this->childChain) {
|
||||
QObject::disconnect(item, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
// relink a and b destruction notifications
|
||||
if (this->mA != nullptr) {
|
||||
QObject::connect(this->mA, &QObject::destroyed, this, &TransformWatcher::aDestroyed);
|
||||
}
|
||||
|
||||
if (this->mB != nullptr) {
|
||||
QObject::connect(this->mB, &QObject::destroyed, this, &TransformWatcher::bDestroyed);
|
||||
}
|
||||
|
||||
this->parentChain.clear();
|
||||
this->childChain.clear();
|
||||
}
|
||||
|
||||
void TransformWatcher::recalcChains() {
|
||||
|
@ -111,26 +127,57 @@ void TransformWatcher::recalcChains() {
|
|||
this->linkChains();
|
||||
}
|
||||
|
||||
void TransformWatcher::itemDestroyed() {
|
||||
auto destroyed =
|
||||
this->parentChain.removeOne(this->sender()) || this->childChain.removeOne(this->sender());
|
||||
|
||||
if (destroyed) this->recalcChains();
|
||||
}
|
||||
|
||||
QQuickItem* TransformWatcher::a() const { return this->mA; }
|
||||
|
||||
void TransformWatcher::setA(QQuickItem* a) {
|
||||
if (this->mA == a) return;
|
||||
if (this->mA != nullptr) QObject::disconnect(this->mA, nullptr, this, nullptr);
|
||||
this->mA = a;
|
||||
|
||||
if (this->mA != nullptr) {
|
||||
QObject::connect(this->mA, &QObject::destroyed, this, &TransformWatcher::aDestroyed);
|
||||
}
|
||||
|
||||
this->recalcChains();
|
||||
}
|
||||
|
||||
void TransformWatcher::aDestroyed() {
|
||||
this->mA = nullptr;
|
||||
this->unlinkChains();
|
||||
emit this->aChanged();
|
||||
}
|
||||
|
||||
QQuickItem* TransformWatcher::b() const { return this->mB; }
|
||||
|
||||
void TransformWatcher::setB(QQuickItem* b) {
|
||||
if (this->mB == b) return;
|
||||
if (this->mB != nullptr) QObject::disconnect(this->mB, nullptr, this, nullptr);
|
||||
this->mB = b;
|
||||
|
||||
if (this->mB != nullptr) {
|
||||
QObject::connect(this->mB, &QObject::destroyed, this, &TransformWatcher::bDestroyed);
|
||||
}
|
||||
|
||||
this->recalcChains();
|
||||
}
|
||||
|
||||
void TransformWatcher::bDestroyed() {
|
||||
this->mB = nullptr;
|
||||
this->unlinkChains();
|
||||
emit this->bChanged();
|
||||
}
|
||||
|
||||
QQuickItem* TransformWatcher::commonParent() const { return this->mCommonParent; }
|
||||
|
||||
void TransformWatcher::setCommonParent(QQuickItem* commonParent) {
|
||||
if (this->mCommonParent == commonParent) return;
|
||||
this->mCommonParent = commonParent;
|
||||
this->resolveChains();
|
||||
this->recalcChains();
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ class TestTransformWatcher;
|
|||
|
||||
///! Monitor of all geometry changes between two objects.
|
||||
/// The TransformWatcher monitors all properties that affect the geometry
|
||||
/// of two `Item`s relative to eachother.
|
||||
/// of two @@QtQuick.Item$s relative to eachother.
|
||||
///
|
||||
/// > [!INFO] The algorithm responsible for determining the relationship
|
||||
/// > between `a` and `b` is biased towards `a` being a parent of `b`,
|
||||
|
@ -60,6 +60,9 @@ signals:
|
|||
|
||||
private slots:
|
||||
void recalcChains();
|
||||
void itemDestroyed();
|
||||
void aDestroyed();
|
||||
void bDestroyed();
|
||||
|
||||
private:
|
||||
void resolveChains(QQuickItem* a, QQuickItem* b, QQuickItem* commonParent);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue