forked from quickshell/quickshell
Compare commits
566 commits
a35d3f9584
...
a5431dd02d
| Author | SHA1 | Date | |
|---|---|---|---|
| a5431dd02d | |||
| f0d5f48a82 | |||
| 1c026545e9 | |||
| 0416032a7c | |||
| 1644ed5e19 | |||
| 91c9db581e | |||
| ab096b7e78 | |||
| 448623de5a | |||
| dfededc901 | |||
| 4dad447570 | |||
|
|
3bbf39c67e | ||
|
|
f90bef2d99 | ||
| db77c71c21 | |||
| fcffbbced8 | |||
| 759bd721df | |||
| 63a6d27213 | |||
| 77de23bb71 | |||
| 7b417bb808 | |||
|
|
e55d519c28 | ||
| ecc4a1249d | |||
| 6572a7f61d | |||
| e885f4aec1 | |||
|
|
115d6717a8 | ||
| 91dcb41d22 | |||
| 201c559dcd | |||
| 78e3874ac6 | |||
| 986749cdb9 | |||
| 4d8055f1cd | |||
| a45fc03c7d | |||
|
|
c40074dd56 | ||
| 3dfb7d8827 | |||
| a2146f6394 | |||
| 5706c09e6f | |||
| 5ac9096c1c | |||
| 05fbead660 | |||
| 478aa2bda1 | |||
| cee1f5837e | |||
| 71334bfcaf | |||
| de25787451 | |||
| b011cd9d33 | |||
| 1e1ba93713 | |||
| 59d29bb254 | |||
| 9604302415 | |||
| 479ff58f84 | |||
| 3b4ebc5f16 | |||
| bb206e3a19 | |||
| 0c9c5be8dd | |||
| 49a3752b9d | |||
| 026aac3756 | |||
| d7079b7524 | |||
| 6f774af11e | |||
| 5703fbae21 | |||
| 07ea4de248 | |||
| 2629e211fa | |||
| b4c62b8ff9 | |||
| 1af08c0c52 | |||
| 4b35d7b51b | |||
| 3d594e16dd | |||
| 5d7e07508a | |||
| 87d99b866f | |||
| 7eff415b25 | |||
| 3cc7ced3a0 | |||
| fb37be7611 | |||
| 9708d8212a | |||
| 0e6518a706 | |||
| 86591f122d | |||
| f681e2016f | |||
| 1d02292fbf | |||
| f842b84a5a | |||
| d949f91347 | |||
| 27f97c3283 | |||
| 20c3da01f1 | |||
| 8be18c05ed | |||
| 98d09b5a36 | |||
| 8fc3e1cb6e | |||
| c17ea54371 | |||
|
|
362c8e1b69 | ||
| c115df8d34 | |||
| 02362c3e94 | |||
| 3d3b7f1c05 | |||
| 79b2204af8 | |||
| 95d0af8113 | |||
| 579d589290 | |||
| 9a30333405 | |||
| d9164578a2 | |||
| 20322484b9 | |||
| 0499518143 | |||
| 0140356d99 | |||
| 71fe3d9165 | |||
| 517143adf9 | |||
| 05b5eccf2e | |||
| 703a378908 | |||
| 09981a0498 | |||
| 2a8479d635 | |||
| 2b01a75679 | |||
| 0224fa942b | |||
| 91000a582b | |||
| dcd9e3aed8 | |||
|
|
ee570ec623 | ||
| 6b3d64e32a | |||
| aa547bad84 | |||
| d1df932d60 | |||
| ef077ddd24 | |||
| 2773e5468f | |||
| 4a0f6382b0 | |||
| 6d42d26c79 | |||
| adcef7fc30 | |||
| b67f92bc13 | |||
| 2e3c15f7a1 | |||
| 5ae8e4901a | |||
| cb195d4b2a | |||
| b898592db7 | |||
| ee31e5d226 | |||
| ec433d1a70 | |||
| 4472b27039 | |||
| e931b85464 | |||
| 05ed9ff74c | |||
| 7390ae28e4 | |||
| 73e673ea1c | |||
| abd9a3c5f8 | |||
| bf235d3d4d | |||
| 2bcd9e07fd | |||
| 428aec950e | |||
| 23ef14c31d | |||
| d872ea888d | |||
| c4a7d16478 | |||
| 89e796cb21 | |||
| 3cf96ecf97 | |||
| e135de9ec6 | |||
| 8b5b12b722 | |||
| 2e33ef5b7f | |||
| edfc4c681c | |||
| 644254d9ec | |||
| 5193426cd7 | |||
| 6026c4ce27 | |||
| c77a12d7bb | |||
| 56b4ef3d21 | |||
| 2e905f6447 | |||
| 61f00a0442 | |||
| 6dbc310df4 | |||
| 8124a63ee4 | |||
| 5c1d600e84 | |||
| 4d74851fd0 | |||
| 4d7d06bb9b | |||
| 325a51c82d | |||
| a05c0de53b | |||
| c1c24c2998 | |||
| 69c7f4fe77 | |||
| fee4942771 | |||
| cb69c2d016 | |||
| 48a56381a6 | |||
| baa9e5e074 | |||
| e342ba322e | |||
| 4ae0eae3da | |||
| e0cff677a5 | |||
| 6a8284dae3 | |||
| ead9141aca | |||
| ca26210cc4 | |||
| 8863bf55ff | |||
| 67524f9d8e | |||
| ed528268e0 | |||
|
|
3a97da0029 | ||
| 4ea77a8eb6 | |||
| 14aa1793df | |||
| 2028766e61 | |||
| d6a4ebc742 | |||
| fa74449139 | |||
| 69430e3873 | |||
| 392f56c40e | |||
| 67b2682604 | |||
| 8f11d60999 | |||
| 62ccab5d30 | |||
|
|
207e6114a3 | ||
| 1a20c39fba | |||
| 3b2d84caf0 | |||
| eabf79ebb6 | |||
| 0662c37d67 | |||
|
|
c5bea858a0 | ||
|
|
9534778a78 | ||
| d1a172751d | |||
| 1eabf5b3c3 | |||
| aeb347ba91 | |||
| c3ed3b0ee2 | |||
| 50026f0934 | |||
| fb326e0e9c | |||
| d58b7b5dcb | |||
| fb343ab639 | |||
| d3b1a65911 | |||
| 9506c1bb62 | |||
| 4f2610dece | |||
| 9417d6fa57 | |||
| 420529362f | |||
| 325be8857c | |||
| b289bfa504 | |||
| cdaff2967f | |||
| c6791cf1f2 | |||
| b73eff0e47 | |||
|
|
6a017d63d6 | ||
| 3c7dfcb220 | |||
| b336129c34 | |||
| bc73d35d03 | |||
| 6464ead0f1 | |||
| d6b58521e9 | |||
| d195ca7680 | |||
| ca79715cce | |||
| c2ed5bf559 | |||
| 6024c37492 | |||
| 6d8022b709 | |||
|
|
8b6aa624a2 | ||
| cd429142a4 | |||
| 918dd2392d | |||
| 2c411fce5a | |||
| 26d443aa50 | |||
| af86d5fd19 | |||
| 761d99d644 | |||
| fca058e66c | |||
| eaf854935b | |||
| f3b7171b25 | |||
| dc3a79600d | |||
| 47bcf8ee61 | |||
| 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 | |||
| 908ba3eef5 | |||
| 3e80c4a4fd | |||
| bba8cb8a7d | |||
| 87a884ca36 | |||
| e7cfb5cf37 | |||
| 3c0456a3c0 | |||
| d64bf59bb0 | |||
| 53d69fd2c0 | |||
| 4db28fe725 | |||
| 658f3cf411 | |||
| 61061644a5 | |||
| 7cc1b54587 | |||
| aa9f8cd001 | |||
| 74d1bb9bc2 | |||
| a1d82729bc | |||
| c71fdd62d0 | |||
| 1f49c55711 | |||
| ce4e697667 | |||
| c6e5a35745 | |||
| 31462b9797 | |||
| 97bcdbecc1 | |||
| 94a1140aab | |||
| 6eb68d2cd7 | |||
| 61812343f5 | |||
| 54bf485101 | |||
| 23d0c2e01d | |||
| a06af243ad | |||
| fd5b73adbb | |||
| 98318c4dcb | |||
| 0b2baea230 | |||
| ff8e252944 | |||
| 082c3c480f | |||
| c0847366dd | |||
| 6214ac1002 | |||
| d47a7f2cff | |||
| 8e530b6b77 | |||
| 8529a2eb22 | |||
| 54b3d338dc | |||
| 83afce7f68 | |||
| 439788fce0 | |||
| c3fe93efe6 | |||
| 3026d3400a | |||
| 9cbd5abd96 | |||
| 9625129844 | |||
| 055b191a67 | |||
| 3a0381dcbe | |||
| b6dc6967a1 | |||
| c6bf826031 | |||
| 4eb5dc5593 | |||
| f09f591e6a | |||
| 8e25c1cee0 | |||
| dd811ac423 | |||
| 31264ac7d1 | |||
| 9f38908bdf | |||
| 518977932d | |||
| 8d742e315e | |||
| 300c0d97fb | |||
| 5731af562b | |||
| 41803ee235 | |||
| 48156a55b3 | |||
| ffbdac9977 | |||
| 1687ff3614 | |||
| 211f454de9 | |||
| 463f9a297f | |||
| 9f6ef37f61 | |||
| c44041653c | |||
| 3480707e99 | |||
| 1e647cee51 | |||
| b675b3676c | |||
| 8cf0659444 | |||
| 7a15495e3f | |||
| 5f9bb9b46c | |||
| 31365dd179 | |||
| 3789709820 | |||
| 15cd78e30c | |||
| fc93591cab |
465 changed files with 48473 additions and 2543 deletions
15
.clang-tidy
15
.clang-tidy
|
|
@ -5,6 +5,9 @@ Checks: >
|
|||
-*,
|
||||
bugprone-*,
|
||||
-bugprone-easily-swappable-parameters,
|
||||
-bugprone-forward-declararion-namespace,
|
||||
-bugprone-forward-declararion-namespace,
|
||||
-bugprone-return-const-ref-from-parameter,
|
||||
concurrency-*,
|
||||
cppcoreguidelines-*,
|
||||
-cppcoreguidelines-owning-memory,
|
||||
|
|
@ -12,8 +15,11 @@ Checks: >
|
|||
-cppcoreguidelines-pro-bounds-constant-array-index,
|
||||
-cppcoreguidelines-avoid-const-or-ref-data-members,
|
||||
-cppcoreguidelines-non-private-member-variables-in-classes,
|
||||
google-build-using-namespace.
|
||||
google-explicit-constructor,
|
||||
-cppcoreguidelines-avoid-goto,
|
||||
-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,
|
||||
|
|
@ -25,6 +31,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,
|
||||
|
|
@ -35,6 +42,10 @@ Checks: >
|
|||
-readability-braces-around-statements,
|
||||
-readability-redundant-access-specifiers,
|
||||
-readability-else-after-return,
|
||||
-readability-container-data-pointer,
|
||||
-readability-implicit-bool-conversion,
|
||||
-readability-avoid-nested-conditional-operator,
|
||||
-readability-math-missing-parentheses,
|
||||
tidyfox-*,
|
||||
CheckOptions:
|
||||
performance-for-range-copy.WarnOnAllAutoCopies: true
|
||||
|
|
|
|||
|
|
@ -9,3 +9,10 @@ indent_style = tab
|
|||
[*.nix]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.scm]
|
||||
indent_style = space
|
||||
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.
|
||||
56
.github/workflows/build.yml
vendored
Normal file
56
.github/workflows/build.yml
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
name: Build
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
nix:
|
||||
name: Nix
|
||||
strategy:
|
||||
matrix:
|
||||
qtver: [qt6.9.0, qt6.8.3, qt6.8.2, qt6.8.1, qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0]
|
||||
compiler: [clang, gcc]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# Use cachix action over detsys for testing with act.
|
||||
# - uses: cachix/install-nix-action@v27
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
|
||||
- name: Download Dependencies
|
||||
run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).inputDerivation'
|
||||
|
||||
- name: Build
|
||||
run: nix-build --no-out-link --expr '(import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }'
|
||||
|
||||
archlinux:
|
||||
name: Archlinux
|
||||
runs-on: ubuntu-latest
|
||||
container: archlinux
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download Dependencies
|
||||
run: |
|
||||
pacman --noconfirm --noprogressbar -Syyu
|
||||
pacman --noconfirm --noprogressbar -Sy \
|
||||
base-devel \
|
||||
cmake \
|
||||
ninja \
|
||||
pkgconf \
|
||||
qt6-base \
|
||||
qt6-declarative \
|
||||
qt6-svg \
|
||||
qt6-wayland \
|
||||
qt6-shadertools \
|
||||
wayland-protocols \
|
||||
wayland \
|
||||
libdrm \
|
||||
libxcb \
|
||||
libpipewire \
|
||||
cli11 \
|
||||
jemalloc
|
||||
|
||||
- name: Build
|
||||
# breakpad is annoying to build in ci due to makepkg not running as root
|
||||
run: |
|
||||
cmake -GNinja -B build -DCRASH_REPORTER=OFF
|
||||
cmake --build build
|
||||
25
.github/workflows/lint.yml
vendored
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: LC_ALL=en_US.UTF-8 LC_CTYPE=en_US.UTF-8 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
|
||||
251
BUILD.md
Normal file
251
BUILD.md
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
# 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)
|
||||
- `spirv-tools` (build-time)
|
||||
- `pkg-config` (build-time)
|
||||
- `cli11` (static library)
|
||||
|
||||
Build time dependencies and static libraries don't have to exist at runtime,
|
||||
however build time dependencies must be compiled for the architecture of
|
||||
the builder, while static libraries must be compiled for the architecture
|
||||
of the target.
|
||||
|
||||
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` (static library)
|
||||
|
||||
### 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` (build time)
|
||||
- `wayland-protocols` (static library)
|
||||
|
||||
Note that one or both of `wayland-scanner` and `wayland-protocols` may be bundled
|
||||
with you distro's wayland package.
|
||||
|
||||
#### Wlroots Layershell
|
||||
Enables wlroots layershell integration through the [zwlr-layer-shell-v1] protocol,
|
||||
enabling use cases such as bars overlays and backgrounds.
|
||||
This feature has no extra dependencies.
|
||||
|
||||
To disable: `-DWAYLAND_WLR_LAYERSHELL=OFF`
|
||||
|
||||
[zwlr-layer-shell-v1]: https://wayland.app/protocols/wlr-layer-shell-unstable-v1
|
||||
|
||||
#### Session Lock
|
||||
Enables session lock support through the [ext-session-lock-v1] protocol,
|
||||
which allows quickshell to be used as a session lock under compatible wayland compositors.
|
||||
|
||||
To disable: `-DWAYLAND_SESSION_LOCK=OFF`
|
||||
|
||||
[ext-session-lock-v1]: https://wayland.app/protocols/ext-session-lock-v1
|
||||
|
||||
|
||||
#### Foreign Toplevel Management
|
||||
Enables management of windows of other clients through the [zwlr-foreign-toplevel-management-v1] protocol,
|
||||
which allows quickshell to be used as a session lock under compatible wayland compositors.
|
||||
|
||||
[zwlr-foreign-toplevel-management-v1]: https://wayland.app/protocols/wlr-foreign-toplevel-management-unstable-v1
|
||||
|
||||
To disable: `-DWAYLAND_TOPLEVEL_MANAGEMENT=OFF`
|
||||
|
||||
#### Screencopy
|
||||
Enables streaming video from monitors and toplevel windows through various protocols.
|
||||
|
||||
To disable: `-DSCREENCOPY=OFF`
|
||||
|
||||
Dependencies:
|
||||
- `libdrm`
|
||||
- `libgbm`
|
||||
|
||||
Specific protocols can also be disabled:
|
||||
- `DSCREENCOPY_ICC=OFF` - Disable screencopy via [ext-image-copy-capture-v1]
|
||||
- `DSCREENCOPY_WLR=OFF` - Disable screencopy via [zwlr-screencopy-v1]
|
||||
- `DSCREENCOPY_HYPRLAND_TOPLEVEL=OFF` - Disable screencopy via [hyprland-toplevel-export-v1]
|
||||
|
||||
[ext-image-copy-capture-v1]:https://wayland.app/protocols/ext-image-copy-capture-v1
|
||||
[zwlr-screencopy-v1]: https://wayland.app/protocols/wlr-screencopy-unstable-v1
|
||||
[hyprland-toplevel-export-v1]: https://wayland.app/protocols/hyprland-toplevel-export-v1
|
||||
|
||||
### X11
|
||||
This feature enables x11 support. Currently this implements panel windows for X11 similarly
|
||||
to the wlroots layershell above.
|
||||
|
||||
To disable: `-DX11=OFF`
|
||||
|
||||
Dependencies: `libxcb`
|
||||
|
||||
### Pipewire
|
||||
This features enables viewing and management of pipewire nodes.
|
||||
|
||||
To disable: `-DSERVICE_PIPEWIRE=OFF`
|
||||
|
||||
Dependencies: `libpipewire`
|
||||
|
||||
### StatusNotifier / System Tray
|
||||
This feature enables system tray support using the status notifier dbus protocol.
|
||||
|
||||
To disable: `-DSERVICE_STATUS_NOTIFIER=OFF`
|
||||
|
||||
Dependencies: `qt6dbus` (usually part of qt6base)
|
||||
|
||||
### MPRIS
|
||||
This feature enables access to MPRIS compatible media players using its dbus protocol.
|
||||
|
||||
To disable: `-DSERVICE_MPRIS=OFF`
|
||||
|
||||
Dependencies: `qt6dbus` (usually part of qt6base)
|
||||
|
||||
### PAM
|
||||
This feature enables PAM integration for user authentication.
|
||||
|
||||
To disable: `-DSERVICE_PAM=OFF`
|
||||
|
||||
Dependencies: `pam`
|
||||
|
||||
### Hyprland
|
||||
This feature enables hyprland specific integrations. It requires wayland support
|
||||
but has no extra dependencies.
|
||||
|
||||
To disable: `-DHYPRLAND=OFF`
|
||||
|
||||
#### Hyprland Global Shortcuts
|
||||
Enables creation of global shortcuts under hyprland through the [hyprland-global-shortcuts-v1]
|
||||
protocol. Generally a much nicer alternative to using unix sockets to implement the same thing.
|
||||
This feature has no extra dependencies.
|
||||
|
||||
To disable: `-DHYPRLAND_GLOBAL_SHORTCUTS=OFF`
|
||||
|
||||
[hyprland-global-shortcuts-v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-global-shortcuts-v1.xml
|
||||
|
||||
#### Hyprland Focus Grab
|
||||
Enables windows to grab focus similarly to a context menu under hyprland through the
|
||||
[hyprland-focus-grab-v1] protocol. This feature has no extra dependencies.
|
||||
|
||||
To disable: `-DHYPRLAND_FOCUS_GRAB=OFF`
|
||||
|
||||
[hyprland-focus-grab-v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-focus-grab-v1.xml
|
||||
|
||||
### i3/Sway
|
||||
Enables i3 and Sway specific features, does not have any dependency on Wayland or x11.
|
||||
|
||||
To disable: `-DI3=OFF`
|
||||
|
||||
#### i3/Sway IPC
|
||||
Enables interfacing with i3 and Sway's IPC.
|
||||
|
||||
To disable: `-DI3_IPC=OFF`
|
||||
|
||||
## Building
|
||||
*For developers and prospective contributors: See [CONTRIBUTING.md](CONTRIBUTING.md).*
|
||||
|
||||
Only `ninja` builds are tested, but makefiles may work.
|
||||
|
||||
#### 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
|
||||
```
|
||||
143
CMakeLists.txt
143
CMakeLists.txt
|
|
@ -1,36 +1,94 @@
|
|||
cmake_minimum_required(VERSION 3.20)
|
||||
project(quickshell VERSION "0.1.0")
|
||||
project(quickshell VERSION "0.2.0" LANGUAGES CXX C)
|
||||
|
||||
set(QT_MIN_VERSION "6.6.0")
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
option(TESTS "Build tests" OFF)
|
||||
set(QS_BUILD_OPTIONS "")
|
||||
|
||||
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)
|
||||
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 " Build tests: ${BUILD_TESTING}")
|
||||
message(STATUS " Sockets: ${SOCKETS}")
|
||||
message(STATUS " Wayland: ${WAYLAND}")
|
||||
if (WAYLAND)
|
||||
message(STATUS " Wlroots Layershell: ${WAYLAND_WLR_LAYERSHELL}")
|
||||
message(STATUS " Session Lock: ${WAYLAND_SESSION_LOCK}")
|
||||
message(STATUS " 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})
|
||||
|
||||
boption(CRASH_REPORTER "Crash Handling" ON)
|
||||
boption(USE_JEMALLOC "Use jemalloc" ON)
|
||||
boption(SOCKETS "Unix Sockets" ON)
|
||||
boption(WAYLAND "Wayland" ON)
|
||||
boption(WAYLAND_WLR_LAYERSHELL " Wlroots Layer-Shell" ON REQUIRES WAYLAND)
|
||||
boption(WAYLAND_SESSION_LOCK " Session Lock" ON REQUIRES WAYLAND)
|
||||
boption(WAYLAND_TOPLEVEL_MANAGEMENT " Foreign Toplevel Management" ON REQUIRES WAYLAND)
|
||||
boption(HYPRLAND " Hyprland" ON REQUIRES WAYLAND)
|
||||
boption(HYPRLAND_IPC " Hyprland IPC" ON REQUIRES HYPRLAND)
|
||||
boption(HYPRLAND_GLOBAL_SHORTCUTS " Hyprland Global Shortcuts" ON REQUIRES HYPRLAND)
|
||||
boption(HYPRLAND_FOCUS_GRAB " Hyprland Focus Grabbing" ON REQUIRES HYPRLAND)
|
||||
boption(HYPRLAND_SURFACE_EXTENSIONS " Hyprland Surface Extensions" ON REQUIRES HYPRLAND)
|
||||
boption(SCREENCOPY " Screencopy" ON REQUIRES WAYLAND)
|
||||
boption(SCREENCOPY_ICC " Image Copy Capture" ON REQUIRES WAYLAND)
|
||||
boption(SCREENCOPY_WLR " Wlroots Screencopy" ON REQUIRES WAYLAND)
|
||||
boption(SCREENCOPY_HYPRLAND_TOPLEVEL " Hyprland Toplevel Export" ON REQUIRES WAYLAND)
|
||||
boption(X11 "X11" ON)
|
||||
boption(I3 "I3/Sway" ON)
|
||||
boption(I3_IPC " I3/Sway IPC" ON REQUIRES I3)
|
||||
boption(SERVICE_STATUS_NOTIFIER "System Tray" ON)
|
||||
boption(SERVICE_PIPEWIRE "PipeWire" ON)
|
||||
boption(SERVICE_MPRIS "Mpris" ON)
|
||||
boption(SERVICE_PAM "Pam" ON)
|
||||
boption(SERVICE_GREETD "Greetd" ON)
|
||||
boption(SERVICE_UPOWER "UPower" ON)
|
||||
boption(SERVICE_NOTIFICATIONS "Notifications" ON)
|
||||
boption(BLUETOOTH "Bluetooth" ON)
|
||||
|
||||
include(cmake/install-qml-module.cmake)
|
||||
include(cmake/util.cmake)
|
||||
|
||||
add_compile_options(-Wall -Wextra -Wno-vla-cxx-extension)
|
||||
|
||||
# pipewire defines this, breaking PCH
|
||||
add_compile_definitions(_REENTRANT)
|
||||
|
||||
if (FRAME_POINTERS)
|
||||
add_compile_options(-fno-omit-frame-pointer)
|
||||
endif()
|
||||
|
||||
if (NOT DEFINED GIT_REVISION)
|
||||
execute_process(
|
||||
COMMAND git rev-parse HEAD
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE GIT_REVISION
|
||||
)
|
||||
if (ASAN)
|
||||
add_compile_options(-fsanitize=address)
|
||||
add_link_options(-fsanitize=address)
|
||||
endif()
|
||||
|
||||
add_compile_options(-Wall -Wextra)
|
||||
|
||||
# nix workaround
|
||||
if (CMAKE_EXPORT_COMPILE_COMMANDS)
|
||||
set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES ${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES})
|
||||
|
|
@ -41,34 +99,61 @@ if (NOT CMAKE_BUILD_TYPE)
|
|||
set(CMAKE_BUILD_TYPE Debug)
|
||||
endif()
|
||||
|
||||
set(QT_DEPS Qt6::Gui Qt6::Qml Qt6::Quick Qt6::QuickControls2)
|
||||
set(QT_FPDEPS Gui Qml Quick QuickControls2)
|
||||
set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets ShaderTools)
|
||||
|
||||
include(cmake/pch.cmake)
|
||||
|
||||
if (BUILD_TESTING)
|
||||
enable_testing()
|
||||
add_definitions(-DQS_TEST)
|
||||
list(APPEND QT_FPDEPS Test)
|
||||
endif()
|
||||
|
||||
if (SOCKETS)
|
||||
list(APPEND QT_DEPS Qt6::Network)
|
||||
list(APPEND QT_FPDEPS Network)
|
||||
endif()
|
||||
|
||||
if (WAYLAND)
|
||||
list(APPEND QT_DEPS Qt6::WaylandClient Qt6::WaylandClientPrivate)
|
||||
list(APPEND QT_FPDEPS WaylandClient)
|
||||
endif()
|
||||
|
||||
if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH)
|
||||
set(DBUS ON)
|
||||
endif()
|
||||
|
||||
if (DBUS)
|
||||
list(APPEND QT_FPDEPS DBus)
|
||||
endif()
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS})
|
||||
|
||||
set(CMAKE_AUTOUIC OFF)
|
||||
qt_standard_project_setup(REQUIRES 6.6)
|
||||
set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules)
|
||||
|
||||
add_subdirectory(src/core)
|
||||
add_subdirectory(src/io)
|
||||
add_subdirectory(src)
|
||||
|
||||
if (WAYLAND)
|
||||
add_subdirectory(src/wayland)
|
||||
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(TARGETS quickshell RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
install(CODE "
|
||||
execute_process(
|
||||
COMMAND ${CMAKE_COMMAND} -E create_symlink \
|
||||
${CMAKE_INSTALL_FULL_BINDIR}/quickshell \$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/qs
|
||||
)
|
||||
")
|
||||
|
||||
install(
|
||||
FILES ${CMAKE_SOURCE_DIR}/assets/org.quickshell.desktop
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications
|
||||
)
|
||||
|
||||
install(
|
||||
FILES ${CMAKE_SOURCE_DIR}/assets/quickshell.svg
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps
|
||||
RENAME org.quickshell.svg
|
||||
)
|
||||
|
|
|
|||
235
CONTRIBUTING.md
Normal file
235
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
# 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`.
|
||||
|
||||
#### Style preferences not caught by clang-format
|
||||
These are flexible. You can ignore them if it looks or works better to
|
||||
for one reason or another.
|
||||
|
||||
Use `auto` if the type of a variable can be deduced automatically, instead of
|
||||
redeclaring the returned value's type. Additionally, auto should be used when a
|
||||
constructor takes arguments.
|
||||
|
||||
```cpp
|
||||
auto x = <expr>; // ok
|
||||
auto x = QString::number(3); // ok
|
||||
QString x; // ok
|
||||
QString x = "foo"; // ok
|
||||
auto x = QString("foo"); // ok
|
||||
|
||||
auto x = QString(); // avoid
|
||||
QString x(); // avoid
|
||||
QString x("foo"); // avoid
|
||||
```
|
||||
|
||||
Put newlines around logical units of code, and after closing braces. If the
|
||||
most reasonable logical unit of code takes only a single line, it should be
|
||||
merged into the next single line logical unit if applicable.
|
||||
```cpp
|
||||
// multiple units
|
||||
auto x = <expr>; // unit 1
|
||||
auto y = <expr>; // unit 2
|
||||
|
||||
auto x = <expr>; // unit 1
|
||||
emit this->y(); // unit 2
|
||||
|
||||
auto x1 = <expr>; // unit 1
|
||||
auto x2 = <expr>; // unit 1
|
||||
auto x3 = <expr>; // unit 1
|
||||
|
||||
auto y1 = <expr>; // unit 2
|
||||
auto y2 = <expr>; // unit 2
|
||||
auto y3 = <expr>; // unit 2
|
||||
|
||||
// one unit
|
||||
auto x = <expr>;
|
||||
if (x...) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// if more than one variable needs to be used then add a newline
|
||||
auto x = <expr>;
|
||||
auto y = <expr>;
|
||||
|
||||
if (x && y) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Class formatting:
|
||||
```cpp
|
||||
//! Doc comment summary
|
||||
/// Doc comment body
|
||||
class Foo: public QObject {
|
||||
// The Q_OBJECT macro comes first. Macros are ; terminated.
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
QML_CLASSINFO(...);
|
||||
// Properties must stay on a single line or the doc generator won't be able to pick them up
|
||||
Q_PROPERTY(...);
|
||||
/// Doc comment
|
||||
Q_PROPERTY(...);
|
||||
/// Doc comment
|
||||
Q_PROPERTY(...);
|
||||
|
||||
public:
|
||||
// Classes should have explicit constructors if they aren't intended to
|
||||
// implicitly cast. The constructor can be inline in the header if it has no body.
|
||||
explicit Foo(QObject* parent = nullptr): QObject(parent) {}
|
||||
|
||||
// Instance functions if applicable.
|
||||
static Foo* instance();
|
||||
|
||||
// Member functions unrelated to properties come next
|
||||
void function();
|
||||
void function();
|
||||
void function();
|
||||
|
||||
// Then Q_INVOKABLEs
|
||||
Q_INVOKABLE function();
|
||||
/// Doc comment
|
||||
Q_INVOKABLE function();
|
||||
/// Doc comment
|
||||
Q_INVOKABLE function();
|
||||
|
||||
// Then property related functions, in the order (bindable, getter, setter).
|
||||
// Related functions may be included here as well. Function bodies may be inline
|
||||
// if they are a single expression. There should be a newline between each
|
||||
// property's methods.
|
||||
[[nodiscard]] QBindable<T> bindableFoo() { return &this->bFoo; }
|
||||
[[nodiscard]] T foo() const { return this->foo; }
|
||||
void setFoo();
|
||||
|
||||
[[nodiscard]] T bar() const { return this->foo; }
|
||||
void setBar();
|
||||
|
||||
signals:
|
||||
// Signals that are not property change related go first.
|
||||
// Property change signals go in property definition order.
|
||||
void asd();
|
||||
void asd2();
|
||||
void fooChanged();
|
||||
void barChanged();
|
||||
|
||||
public slots:
|
||||
// generally Q_INVOKABLEs are preferred to public slots.
|
||||
void slot();
|
||||
|
||||
private slots:
|
||||
// ...
|
||||
|
||||
private:
|
||||
// statics, then functions, then fields
|
||||
static const foo BAR;
|
||||
static void foo();
|
||||
|
||||
void foo();
|
||||
void bar();
|
||||
|
||||
// property related members are prefixed with `m`.
|
||||
QString mFoo;
|
||||
QString bar;
|
||||
|
||||
// Bindables go last and should be prefixed with `b`.
|
||||
Q_OBJECT_BINDABLE_PROPERTY(Foo, QString, bFoo, &Foo::fooChanged);
|
||||
};
|
||||
```
|
||||
|
||||
### 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-changed
|
||||
```
|
||||
|
||||
If the linter is complaining about something that you think it should not,
|
||||
please disable the lint in your MR and explain your reasoning if it isn't obvious.
|
||||
|
||||
### 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.
|
||||
10
Justfile
10
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 -j$(nproc) -q0 --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
|
||||
|
||||
lint-ci:
|
||||
find src -type f -name "*.cpp" -print0 | parallel -j$(nproc) -q0 --no-notice --will-cite --tty clang-tidy --load={{ env_var("TIDYFOX") }}
|
||||
|
||||
lint-changed:
|
||||
git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
|
||||
|
||||
configure target='debug' *FLAGS='':
|
||||
cmake -GNinja -B {{builddir}} \
|
||||
|
|
@ -26,7 +32,7 @@ clean:
|
|||
rm -rf {{builddir}}
|
||||
|
||||
run *ARGS='': build
|
||||
{{builddir}}/src/core/quickshell {{ARGS}}
|
||||
{{builddir}}/src/quickshell {{ARGS}}
|
||||
|
||||
test *ARGS='': build
|
||||
ctest --test-dir {{builddir}} --output-on-failure {{ARGS}}
|
||||
|
|
|
|||
110
README.md
110
README.md
|
|
@ -1,107 +1,13 @@
|
|||
# quickshell
|
||||
# Quickshell
|
||||
See the [website](https://quickshell.outfoxxed.me) for more information
|
||||
and installation instructions.
|
||||
|
||||
Simple and flexbile QtQuick based desktop shell toolkit.
|
||||
This repo is hosted at:
|
||||
- https://git.outfoxxed.me/quickshell/quickshell
|
||||
- https://github.com/quickshell-mirror/quickshell
|
||||
|
||||
Hosts: [outfoxxed's gitea], [github]
|
||||
|
||||
[outfoxxed's gitea]: https://git.outfoxxed.me/outfoxxed/quickshell
|
||||
[github]: https://github.com/outfoxxed/quickshell
|
||||
|
||||
Documentation can be built from the [quickshell-docs](https://git.outfoxxed.me/outfoxxed/quickshell-docs) repo,
|
||||
though is currently pretty lacking.
|
||||
|
||||
Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples)
|
||||
repo.
|
||||
|
||||
Both the documentation and examples are included as submodules with revisions that work with the current
|
||||
version of quickshell.
|
||||
|
||||
You can clone everything with
|
||||
```
|
||||
$ git clone --recursive https://git.outfoxxed.me/outfoxxed/quickshell.git
|
||||
```
|
||||
|
||||
Or clone missing submodules later with
|
||||
```
|
||||
$ git submodule update --init --recursive
|
||||
```
|
||||
|
||||
# Installation
|
||||
|
||||
## Nix
|
||||
This repo has a nix flake you can use to install the package directly:
|
||||
|
||||
```nix
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||
|
||||
quickshell = {
|
||||
url = "git+https://git.outfoxxed.me/outfoxxed/quickshell";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Quickshell's binary is available at `quickshell.packages.<system>.default` to be added to
|
||||
lists such as `environment.systemPackages` or `home.packages`.
|
||||
|
||||
## Manual
|
||||
|
||||
If not using nix, you'll have to build from source.
|
||||
|
||||
### Dependencies
|
||||
To build quickshell at all, you will need the following packages (names may vary by distro)
|
||||
|
||||
- just
|
||||
- cmake
|
||||
- pkg-config
|
||||
- ninja
|
||||
- Qt6 [ QtBase, QtDeclarative ]
|
||||
|
||||
To build with wayland support you will additionally need:
|
||||
- wayland
|
||||
- wayland-scanner (may be part of wayland on some distros)
|
||||
- wayland-protocols
|
||||
- Qt6 [ QtWayland ]
|
||||
|
||||
### Building
|
||||
|
||||
To make a release build of quickshell run:
|
||||
```sh
|
||||
$ just release
|
||||
```
|
||||
|
||||
If 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
|
||||
|
||||
|
|
|
|||
7
assets/org.quickshell.desktop
Normal file
7
assets/org.quickshell.desktop
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[Desktop Entry]
|
||||
Version=1.5
|
||||
Type=Application
|
||||
NoDisplay=true
|
||||
|
||||
Name=Quickshell
|
||||
Icon=org.quickshell
|
||||
1
assets/quickshell.svg
Normal file
1
assets/quickshell.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="724.635" height="724.635"><path fill="#359757" stroke="#359757" stroke-linecap="square" stroke-linejoin="round" stroke-width="74.755" d="m37.378 160.237 122.859-122.86h527.02v527.02l-122.86 122.86H37.378Z"/><path fill="#fff" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="12.201" d="M323.051 96.412a268.74 268.74 0 0 0-3.51.542c-4.052 14.481-7.815 29.941-14.904 42.692a230.02 230.02 0 0 0-59.406 24.679c-14.036-3.974-27.647-12.214-40.766-19.562a268.788 268.788 0 0 0-60.035 60.16c7.376 13.105 15.645 26.698 19.648 40.726a230.02 230.02 0 0 0-24.554 59.458c-12.735 7.115-28.186 10.913-42.66 14.994a268.789 268.789 0 0 0 .09 84.992c14.48 4.05 29.941 7.814 42.691 14.903a230.02 230.02 0 0 0 24.68 59.406c-3.974 14.037-12.215 27.647-19.563 40.766a268.788 268.788 0 0 0 60.161 60.035c13.104-7.376 26.696-15.645 40.725-19.648a230.02 230.02 0 0 0 59.457 24.555c7.116 12.735 10.913 28.186 14.995 42.659a268.788 268.788 0 0 0 84.99-.09c4.052-14.482 7.817-29.941 14.906-42.691a230.02 230.02 0 0 0 59.405-24.68c14.037 3.974 33.069 17.638 46.188 24.986a268.788 268.788 0 0 0 60.035-60.161c-7.376-13.105-21.068-32.12-25.071-46.149a230.02 230.02 0 0 0 24.555-59.457c12.735-7.116 28.186-10.912 42.659-14.993a268.788 268.788 0 0 0-.089-84.993c-14.482-4.051-29.942-7.814-42.692-14.904a230.02 230.02 0 0 0-24.68-59.405c3.974-14.037 12.216-27.647 19.565-40.767a268.788 268.788 0 0 0-60.161-60.035c-13.105 7.376-26.698 15.645-40.726 19.649a230.02 230.02 0 0 0-59.458-24.555c-7.115-12.735-10.913-28.187-14.994-42.66a268.788 268.788 0 0 0-81.481-.452zm15.778 106.85c58.282-8.328 116.455 15.865 151.643 63.065 35.19 47.2 41.766 109.86 17.144 163.337l-41.728-22.688s-38.558 31.44-57.344 63.012l23.893 36.326a160.78 160.78 0 0 1-46.633 15.058c-87.854 12.99-169.6-47.708-182.573-135.564-12.974-87.855 47.74-169.59 135.598-182.546Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
1
changelog/v0.1.0.md
Normal file
1
changelog/v0.1.0.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
Initial release
|
||||
84
changelog/v0.2.0.md
Normal file
84
changelog/v0.2.0.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
## Breaking Changes
|
||||
|
||||
- Files outside of the shell directory can no longer be referenced with relative paths, e.g. '../../foo.png'.
|
||||
- PanelWindow's Automatic exclusion mode now adds an exclusion zone for panels with a single anchor.
|
||||
- `QT_QUICK_CONTROLS_STYLE` and `QT_STYLE_OVERRIDE` are ignored unless `//@ pragma RespectSystemStyle` is set.
|
||||
|
||||
## New Features
|
||||
|
||||
### Root-Relative Imports
|
||||
|
||||
Quickshell 0.2 comes with a new method to import QML modules which is supported by QMLLS.
|
||||
This replaces "root:/" imports for QML modules.
|
||||
|
||||
The new syntax is `import qs.path.to.module`, where `path/to/module` is the path to
|
||||
a module/subdirectory relative to the config root (`qs`).
|
||||
|
||||
### Better LSP support
|
||||
|
||||
LSP support for Singletons and Root-Relative imports can be enabled by creating a file named
|
||||
`.qmlls.ini` in the shell root directory. Quickshell will detect this file and automatically
|
||||
populate it with an LSP configuration. This file should be gitignored in your configuration,
|
||||
as it is system dependent.
|
||||
|
||||
The generated configuration also includes QML import paths available to Quickshell, meaning
|
||||
QMLLS no longer requires the `-E` flag.
|
||||
|
||||
### Bluetooth Module
|
||||
|
||||
Quickshell can now manage your bluetooth devices through BlueZ. While authenticated pairing
|
||||
has not landed in 0.2, support for connecting and disconnecting devices, basic device information,
|
||||
and non-authenticated pairing are now supported.
|
||||
|
||||
### Other Features
|
||||
|
||||
- Added `HyprlandToplevel` and related toplevel/window management APIs in the Hyprland module.
|
||||
- Added `Quickshell.execDetached()`, which spawns a detached process without a `Process` object.
|
||||
- Added `Process.exec()` for easier reconfiguration of process commands when starting them.
|
||||
- Added `FloatingWindow.title`, which allows changing the title of a floating window.
|
||||
- Added `signal QsWindow.closed()`, fired when a window is closed externally.
|
||||
- Added support for inline replies in notifications, when supported by applications.
|
||||
- Added `DesktopEntry.startupWmClass` and `DesktopEntry.heuristicLookup()` to better identify toplevels.
|
||||
- Added `DesktopEntry.command` which can be run as an alternative to `DesktopEntry.execute()`.
|
||||
- Added `//@ pragma Internal`, which makes a QML component impossible to import outside of its module.
|
||||
- Added dead instance selection for some subcommands, such as `qs log` and `qs list`.
|
||||
|
||||
## Other Changes
|
||||
|
||||
- `Quickshell.shellRoot` has been renamed to `Quickshell.shellDir`.
|
||||
- PanelWindow margins opposite the window's anchorpoint are now added to exclusion zone.
|
||||
- stdout/stderr or detached processes and executed desktop entries are now hidden by default.
|
||||
- Various warnings caused by other applications Quickshell communicates with over D-BUS have been hidden in logs.
|
||||
- Quickshell's new logo is now shown in any floating windows.
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- Fixed pipewire device volume and mute states not updating before the device has been used.
|
||||
- Fixed a crash when changing the volume of any pipewire device on a sound card another removed device was using.
|
||||
- Fixed a crash when accessing a removed previous default pipewire node from the default sink/source changed signals.
|
||||
- Fixed session locks crashing if all monitors are disconnected.
|
||||
- Fixed session locks crashing if unsupported by the compositor.
|
||||
- Fixed a crash when creating a session lock and destroying it before acknowledged by the compositor.
|
||||
- Fixed window input masks not updating after a reload.
|
||||
- Fixed PanelWindows being unconfigurable unless `screen` was set under X11.
|
||||
- Fixed a crash when anchoring a popup to a zero sized `Item`.
|
||||
- Fixed `FileView` crashing if `watchChanges` was used.
|
||||
- Fixed `SocketServer` sockets disappearing after a reload.
|
||||
- Fixed `ScreencopyView` having incorrect rotation when displaying a rotated monitor.
|
||||
- Fixed `MarginWrapperManager` breaking pixel alignment of child items when centering.
|
||||
- Fixed `IpcHandler`, `NotificationServer` and `GlobalShortcut` not activating with certain QML structures.
|
||||
- Fixed tracking of QML incubator destruction and deregistration, which occasionally caused crashes.
|
||||
- Fixed FloatingWindows being constrained to the smallest window manager supported size unless max size was set.
|
||||
- Fixed `MprisPlayer.lengthSupported` not updating reactively.
|
||||
- Fixed normal tray icon being ignored when status is `NeedsAttention` and no attention icon is provided.
|
||||
- Fixed `HyprlandWorkspace.activate()` sending invalid commands to Hyprland for named or special workspaces.
|
||||
- Fixed file watcher occasionally breaking when using VSCode to edit QML files.
|
||||
- Fixed crashes when screencopy buffer creation fails.
|
||||
- Fixed a crash when wayland layer surfaces are recreated for the same window.
|
||||
- Fixed the `QsWindow` attached object not working when using `WlrLayershell` directly.
|
||||
- Fixed a crash when attempting to create a window without available VRAM.
|
||||
- Fixed OOM crash when failing to write to detailed log file.
|
||||
- Prevented distro logging configurations for Qt from interfering with Quickshell commands.
|
||||
- Removed the "QProcess destroyed for running process" warning when destroying `Process` objects.
|
||||
- Fixed `ColorQuantizer` printing a pointer to an error message instead of an error message.
|
||||
- Fixed notification pixmap rowstride warning showing for correct rowstrides.
|
||||
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
|
||||
78
ci/nix-checkouts.nix
Normal file
78
ci/nix-checkouts.nix
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
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_9_0 = byCommit {
|
||||
commit = "546c545bd0594809a28ab7e869b5f80dd7243ef6";
|
||||
sha256 = "0562lbi67a9brfwzpqs4n3l0i8zvgla368aakcy5mghr7ps80567";
|
||||
};
|
||||
|
||||
qt6_8_3 = byCommit {
|
||||
commit = "374e6bcc403e02a35e07b650463c01a52b13a7c8";
|
||||
sha256 = "1ck2d7q1f6k58qg47bc07036h9gmc2mqmqlgrv67k3frgplfhfga";
|
||||
};
|
||||
|
||||
qt6_8_2 = byCommit {
|
||||
commit = "97be9fbfc7a8a794bb51bd5dfcbfad5fad860512";
|
||||
sha256 = "1sqh6kb8yg9yw6brkkb3n4y3vpbx8fnx45skyikqdqj2xs76v559";
|
||||
};
|
||||
|
||||
qt6_8_1 = byCommit {
|
||||
commit = "4a66c00fcb3f85ddad658b8cfa2e870063ce60b5";
|
||||
sha256 = "1fcvr67s7366bk8czzwhr12zsq60izl5iq4znqbm44pzyq9pf8rq";
|
||||
};
|
||||
|
||||
qt6_8_0 = byCommit {
|
||||
commit = "352f462ad9d2aa2cde75fdd8f1734e86402a3ff6";
|
||||
sha256 = "02zfgkr9fpd6iwfh6dcr3m6fnx61jppm3v081f3brvkqwmmz7zq1";
|
||||
};
|
||||
|
||||
qt6_7_3 = byCommit {
|
||||
commit = "273673e839189c26130d48993d849a84199523e6";
|
||||
sha256 = "0aca369hdxb8j0vx9791anyzy4m65zckx0lriicqhp95kv9q6m7z";
|
||||
};
|
||||
|
||||
qt6_7_2 = byCommit {
|
||||
commit = "841f166ff96fc2f3ecd1c0cc08072633033d41bf";
|
||||
sha256 = "0d7p0cp7zjiadhpa6sdafxvrpw4lnmb1h673w17q615vm1yaasvy";
|
||||
};
|
||||
|
||||
qt6_7_1 = byCommit {
|
||||
commit = "69bee9866a4e2708b3153fdb61c1425e7857d6b8";
|
||||
sha256 = "1an4sha4jsa29dvc4n9mqxbq8jjwg7frl0rhy085g73m7l1yx0lj";
|
||||
};
|
||||
|
||||
qt6_7_0 = byCommit {
|
||||
commit = "4fbbc17ccf11bc80002b19b31387c9c80276f076";
|
||||
sha256 = "09lhgdqlx8j9a7vpdcf8sddlhbzjq0s208spfmxfjdn14fvx8k0j";
|
||||
};
|
||||
|
||||
qt6_6_3 = byCommit {
|
||||
commit = "8f1a3fbaa92f1d59b09f2d24af6a607b5a280071";
|
||||
sha256 = "0322zwxvmg8v2wkm03xpk6mqmmbfjgrhc9prcx0zd36vjl6jmi18";
|
||||
};
|
||||
|
||||
qt6_6_2 = byCommit {
|
||||
commit = "0bb9cfbd69459488576a0ef3c0e0477bedc3a29e";
|
||||
sha256 = "172ww486jm1mczk9id78s32p7ps9m9qgisml286flc8jffb6yad8";
|
||||
};
|
||||
|
||||
qt6_6_1 = byCommit {
|
||||
commit = "8eecc3342103c38eea666309a7c0d90d403a039a";
|
||||
sha256 = "1lakc0immsgrpz3basaysdvd0sx01r0mcbyymx6id12fk0404z5r";
|
||||
};
|
||||
|
||||
qt6_6_0 = byCommit {
|
||||
commit = "1ded005f95a43953112ffc54b39593ea2f16409f";
|
||||
sha256 = "1xvyd3lj81hak9j53mrhdsqx78x5v2ppv8m2s54qa2099anqgm0f";
|
||||
};
|
||||
}
|
||||
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()
|
||||
127
default.nix
127
default.nix
|
|
@ -3,13 +3,24 @@
|
|||
nix-gitignore,
|
||||
pkgs,
|
||||
keepDebugInfo,
|
||||
stdenv ? (keepDebugInfo pkgs.stdenv),
|
||||
buildStdenv ? pkgs.clangStdenv,
|
||||
|
||||
pkg-config,
|
||||
cmake,
|
||||
ninja,
|
||||
spirv-tools,
|
||||
qt6,
|
||||
breakpad,
|
||||
jemalloc,
|
||||
cli11,
|
||||
wayland,
|
||||
wayland-protocols,
|
||||
wayland-scanner,
|
||||
xorg,
|
||||
libdrm,
|
||||
libgbm ? null,
|
||||
pipewire,
|
||||
pam,
|
||||
|
||||
gitRev ? (let
|
||||
headExists = builtins.pathExists ./.git/HEAD;
|
||||
|
|
@ -21,51 +32,101 @@
|
|||
then builtins.readFile ./.git/refs/heads/${builtins.elemAt matches 0}
|
||||
else headContent)
|
||||
else "unknown"),
|
||||
debug ? false,
|
||||
enableWayland ? true,
|
||||
}: stdenv.mkDerivation {
|
||||
pname = "quickshell${lib.optionalString debug "-debug"}";
|
||||
version = "0.1.0";
|
||||
src = nix-gitignore.gitignoreSource [] ./.;
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
debug ? false,
|
||||
withCrashReporter ? true,
|
||||
withJemalloc ? true, # masks heap fragmentation
|
||||
withQtSvg ? true,
|
||||
withWayland ? true,
|
||||
withX11 ? true,
|
||||
withPipewire ? true,
|
||||
withPam ? true,
|
||||
withHyprland ? true,
|
||||
withI3 ? true,
|
||||
}: let
|
||||
unwrapped = buildStdenv.mkDerivation {
|
||||
pname = "quickshell${lib.optionalString debug "-debug"}";
|
||||
version = "0.2.0";
|
||||
src = nix-gitignore.gitignoreSource "/default.nix\n" ./.;
|
||||
|
||||
dontWrapQtApps = true; # see wrappers
|
||||
|
||||
nativeBuildInputs = [
|
||||
cmake
|
||||
ninja
|
||||
qt6.wrapQtAppsHook
|
||||
] ++ (lib.optionals enableWayland [
|
||||
qt6.qtshadertools
|
||||
spirv-tools
|
||||
pkg-config
|
||||
wayland-protocols
|
||||
wayland-scanner
|
||||
]);
|
||||
]
|
||||
++ lib.optional withWayland wayland-scanner;
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
buildInputs = [
|
||||
qt6.qtbase
|
||||
qt6.qtdeclarative
|
||||
] ++ (lib.optionals enableWayland [ qt6.qtwayland wayland ]);
|
||||
cli11
|
||||
]
|
||||
++ lib.optional withQtSvg qt6.qtsvg
|
||||
++ lib.optional withCrashReporter breakpad
|
||||
++ lib.optional withJemalloc jemalloc
|
||||
++ lib.optionals withWayland [ qt6.qtwayland wayland wayland-protocols ]
|
||||
++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm ]
|
||||
++ 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.cmakeFeature "DISTRIBUTOR" "Official-Nix-Flake")
|
||||
(lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix)
|
||||
(lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true)
|
||||
(lib.cmakeFeature "GIT_REVISION" gitRev)
|
||||
(lib.cmakeBool "CRASH_REPORTER" withCrashReporter)
|
||||
(lib.cmakeBool "USE_JEMALLOC" withJemalloc)
|
||||
(lib.cmakeBool "WAYLAND" withWayland)
|
||||
(lib.cmakeBool "SCREENCOPY" (libgbm != null))
|
||||
(lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire)
|
||||
(lib.cmakeBool "SERVICE_PAM" withPam)
|
||||
(lib.cmakeBool "HYPRLAND" withHyprland)
|
||||
(lib.cmakeBool "I3" withI3)
|
||||
];
|
||||
|
||||
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";
|
||||
homepage = "https://quickshell.org";
|
||||
description = "Flexbile QtQuick based desktop shell toolkit";
|
||||
license = licenses.lgpl3Only;
|
||||
platforms = platforms.linux;
|
||||
mainProgram = "quickshell";
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
wrapper = unwrapped.stdenv.mkDerivation {
|
||||
inherit (unwrapped) version meta buildInputs;
|
||||
pname = "${unwrapped.pname}-wrapped";
|
||||
|
||||
nativeBuildInputs = unwrapped.nativeBuildInputs ++ [ qt6.wrapQtAppsHook ];
|
||||
|
||||
dontUnpack = true;
|
||||
dontConfigure = true;
|
||||
dontBuild = true;
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out
|
||||
cp -r ${unwrapped}/* $out
|
||||
'';
|
||||
|
||||
passthru = {
|
||||
unwrapped = unwrapped;
|
||||
withModules = modules: wrapper.overrideAttrs (prev: {
|
||||
buildInputs = prev.buildInputs ++ modules;
|
||||
});
|
||||
};
|
||||
};
|
||||
in wrapper
|
||||
|
|
|
|||
1
docs
1
docs
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 70989dc619bcdc29dc4880b4ff5257d6ad188a18
|
||||
1
examples
1
examples
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 9c83cc248c968b18a827b4fa4c616a8d362176e1
|
||||
6
flake.lock
generated
6
flake.lock
generated
|
|
@ -2,11 +2,11 @@
|
|||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1709237383,
|
||||
"narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=",
|
||||
"lastModified": 1749285348,
|
||||
"narHash": "sha256-frdhQvPbmDYaScPFiCnfdh3B/Vh81Uuoo0w5TkWmmjU=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8",
|
||||
"rev": "3e3afe5174c561dee0df6f2c2b2236990146329f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
|||
10
flake.nix
10
flake.nix
|
|
@ -4,12 +4,16 @@
|
|||
};
|
||||
|
||||
outputs = { self, nixpkgs }: let
|
||||
forEachSystem = fn: nixpkgs.lib.genAttrs
|
||||
[ "x86_64-linux" "aarch64-linux" ]
|
||||
forEachSystem = fn:
|
||||
nixpkgs.lib.genAttrs
|
||||
nixpkgs.lib.platforms.linux
|
||||
(system: fn system nixpkgs.legacyPackages.${system});
|
||||
in {
|
||||
packages = forEachSystem (system: pkgs: rec {
|
||||
quickshell = import ./package.nix { inherit pkgs; };
|
||||
quickshell = pkgs.callPackage ./default.nix {
|
||||
gitRev = self.rev or self.dirtyRev;
|
||||
};
|
||||
|
||||
default = quickshell;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
{ pkgs ? import <nixpkgs> {}, ... }: pkgs.callPackage ./default.nix {}
|
||||
77
quickshell.scm
Normal file
77
quickshell.scm
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
(define-module (quickshell)
|
||||
#:use-module ((guix licenses) #:prefix license:)
|
||||
#:use-module (gnu packages cpp)
|
||||
#:use-module (gnu packages freedesktop)
|
||||
#:use-module (gnu packages gcc)
|
||||
#:use-module (gnu packages gl)
|
||||
#:use-module (gnu packages jemalloc)
|
||||
#:use-module (gnu packages linux)
|
||||
#:use-module (gnu packages ninja)
|
||||
#:use-module (gnu packages pkg-config)
|
||||
#:use-module (gnu packages qt)
|
||||
#:use-module (gnu packages vulkan)
|
||||
#:use-module (gnu packages xdisorg)
|
||||
#:use-module (gnu packages xorg)
|
||||
#:use-module (guix build-system cmake)
|
||||
#:use-module (guix download)
|
||||
#:use-module (guix gexp)
|
||||
#:use-module (guix git-download)
|
||||
#:use-module (guix packages)
|
||||
#:use-module (guix packages)
|
||||
#:use-module (guix utils))
|
||||
|
||||
(define-public quickshell-git
|
||||
(package
|
||||
(name "quickshell")
|
||||
(version "git")
|
||||
(source (local-file "." "quickshell-checkout"
|
||||
#:recursive? #t
|
||||
#:select? (or (git-predicate (current-source-directory))
|
||||
(const #t))))
|
||||
(build-system cmake-build-system)
|
||||
(propagated-inputs (list qtbase qtdeclarative qtsvg))
|
||||
(native-inputs (list ninja
|
||||
gcc-14
|
||||
pkg-config
|
||||
qtshadertools
|
||||
spirv-tools
|
||||
wayland-protocols
|
||||
cli11))
|
||||
(inputs (list jemalloc
|
||||
libdrm
|
||||
libxcb
|
||||
libxkbcommon
|
||||
linux-pam
|
||||
mesa
|
||||
pipewire
|
||||
qtbase
|
||||
qtdeclarative
|
||||
qtwayland
|
||||
vulkan-headers
|
||||
wayland))
|
||||
(arguments
|
||||
(list #:tests? #f
|
||||
#:configure-flags
|
||||
#~(list "-GNinja"
|
||||
"-DDISTRIBUTOR=\"In-tree Guix channel\""
|
||||
"-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=NO"
|
||||
;; Breakpad is not currently packaged for Guix.
|
||||
"-DCRASH_REPORTER=OFF")
|
||||
#:phases
|
||||
#~(modify-phases %standard-phases
|
||||
(replace 'build (lambda _ (invoke "cmake" "--build" ".")))
|
||||
(replace 'install (lambda _ (invoke "cmake" "--install" ".")))
|
||||
(add-after 'install 'wrap-program
|
||||
(lambda* (#:key inputs #:allow-other-keys)
|
||||
(wrap-program (string-append #$output "/bin/quickshell")
|
||||
`("QML_IMPORT_PATH" ":"
|
||||
= (,(getenv "QML_IMPORT_PATH")))))))))
|
||||
(home-page "https://quickshell.outfoxxed.me")
|
||||
(synopsis "QtQuick-based desktop shell toolkit")
|
||||
(description
|
||||
"Quickshell is a flexible QtQuick-based toolkit for creating and
|
||||
customizing toolbars, notification centers, and other desktop
|
||||
environment tools in a live programming environment.")
|
||||
(license license:lgpl3)))
|
||||
|
||||
quickshell-git
|
||||
|
|
@ -10,18 +10,17 @@
|
|||
rev = "1f062cc198d1112d13e5128fa1f2ee3dbffe613b";
|
||||
sha256 = "kbt0Zc1qHE5fhqBkKz8iue+B+ZANjF1AR/RdgmX1r0I=";
|
||||
}) { inherit pkgs; };
|
||||
in pkgs.mkShell {
|
||||
in pkgs.mkShell.override { stdenv = quickshell.stdenv; } {
|
||||
inputsFrom = [ quickshell ];
|
||||
|
||||
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)
|
||||
|
|
|
|||
35
src/CMakeLists.txt
Normal file
35
src/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
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)
|
||||
add_subdirectory(ui)
|
||||
|
||||
if (CRASH_REPORTER)
|
||||
add_subdirectory(crash)
|
||||
endif()
|
||||
|
||||
if (DBUS)
|
||||
add_subdirectory(dbus)
|
||||
endif()
|
||||
|
||||
if (WAYLAND)
|
||||
add_subdirectory(wayland)
|
||||
endif()
|
||||
|
||||
if (X11)
|
||||
add_subdirectory(x11)
|
||||
endif()
|
||||
|
||||
add_subdirectory(services)
|
||||
|
||||
if (BLUETOOTH)
|
||||
add_subdirectory(bluetooth)
|
||||
endif()
|
||||
42
src/bluetooth/CMakeLists.txt
Normal file
42
src/bluetooth/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
set_source_files_properties(org.bluez.Adapter.xml PROPERTIES
|
||||
CLASSNAME DBusBluezAdapterInterface
|
||||
)
|
||||
|
||||
set_source_files_properties(org.bluez.Device.xml PROPERTIES
|
||||
CLASSNAME DBusBluezDeviceInterface
|
||||
)
|
||||
|
||||
qt_add_dbus_interface(DBUS_INTERFACES
|
||||
org.bluez.Adapter.xml
|
||||
dbus_adapter
|
||||
)
|
||||
|
||||
qt_add_dbus_interface(DBUS_INTERFACES
|
||||
org.bluez.Device.xml
|
||||
dbus_device
|
||||
)
|
||||
|
||||
qt_add_library(quickshell-bluetooth STATIC
|
||||
adapter.cpp
|
||||
bluez.cpp
|
||||
device.cpp
|
||||
${DBUS_INTERFACES}
|
||||
)
|
||||
|
||||
qt_add_qml_module(quickshell-bluetooth
|
||||
URI Quickshell.Bluetooth
|
||||
VERSION 0.1
|
||||
DEPENDENCIES QtQml
|
||||
)
|
||||
|
||||
install_qml_module(quickshell-bluetooth)
|
||||
|
||||
# dbus headers
|
||||
target_include_directories(quickshell-bluetooth PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
target_link_libraries(quickshell-bluetooth PRIVATE Qt::Qml Qt::DBus)
|
||||
qs_add_link_dependencies(quickshell-bluetooth quickshell-dbus)
|
||||
|
||||
qs_module_pch(quickshell-bluetooth SET dbus)
|
||||
|
||||
target_link_libraries(quickshell PRIVATE quickshell-bluetoothplugin)
|
||||
224
src/bluetooth/adapter.cpp
Normal file
224
src/bluetooth/adapter.cpp
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
#include "adapter.hpp"
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdbusconnection.h>
|
||||
#include <qdbusextratypes.h>
|
||||
#include <qdbuspendingcall.h>
|
||||
#include <qdbuspendingreply.h>
|
||||
#include <qdebug.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qstring.h>
|
||||
#include <qstringliteral.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "../core/logcat.hpp"
|
||||
#include "../dbus/properties.hpp"
|
||||
#include "dbus_adapter.h"
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
namespace {
|
||||
QS_LOGGING_CATEGORY(logAdapter, "quickshell.bluetooth.adapter", QtWarningMsg);
|
||||
}
|
||||
|
||||
QString BluetoothAdapterState::toString(BluetoothAdapterState::Enum state) {
|
||||
switch (state) {
|
||||
case BluetoothAdapterState::Disabled: return QStringLiteral("Disabled");
|
||||
case BluetoothAdapterState::Enabled: return QStringLiteral("Enabled");
|
||||
case BluetoothAdapterState::Enabling: return QStringLiteral("Enabling");
|
||||
case BluetoothAdapterState::Disabling: return QStringLiteral("Disabling");
|
||||
case BluetoothAdapterState::Blocked: return QStringLiteral("Blocked");
|
||||
default: return QStringLiteral("Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
BluetoothAdapter::BluetoothAdapter(const QString& path, QObject* parent): QObject(parent) {
|
||||
this->mInterface =
|
||||
new DBusBluezAdapterInterface("org.bluez", path, QDBusConnection::systemBus(), this);
|
||||
|
||||
if (!this->mInterface->isValid()) {
|
||||
qCWarning(logAdapter) << "Could not create DBus interface for adapter at" << path;
|
||||
this->mInterface = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
this->properties.setInterface(this->mInterface);
|
||||
}
|
||||
|
||||
QString BluetoothAdapter::adapterId() const {
|
||||
auto path = this->path();
|
||||
return path.sliced(path.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
void BluetoothAdapter::setEnabled(bool enabled) {
|
||||
if (enabled == this->bEnabled) return;
|
||||
|
||||
if (enabled && this->bState == BluetoothAdapterState::Blocked) {
|
||||
qCCritical(logAdapter) << "Cannot enable adapter because it is blocked by rfkill.";
|
||||
return;
|
||||
}
|
||||
|
||||
this->bEnabled = enabled;
|
||||
this->pEnabled.write();
|
||||
}
|
||||
|
||||
void BluetoothAdapter::setDiscoverable(bool discoverable) {
|
||||
if (discoverable == this->bDiscoverable) return;
|
||||
this->bDiscoverable = discoverable;
|
||||
this->pDiscoverable.write();
|
||||
}
|
||||
|
||||
void BluetoothAdapter::setDiscovering(bool discovering) {
|
||||
if (discovering) {
|
||||
this->startDiscovery();
|
||||
} else {
|
||||
this->stopDiscovery();
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothAdapter::setDiscoverableTimeout(quint32 timeout) {
|
||||
if (timeout == this->bDiscoverableTimeout) return;
|
||||
this->bDiscoverableTimeout = timeout;
|
||||
this->pDiscoverableTimeout.write();
|
||||
}
|
||||
|
||||
void BluetoothAdapter::setPairable(bool pairable) {
|
||||
if (pairable == this->bPairable) return;
|
||||
this->bPairable = pairable;
|
||||
this->pPairable.write();
|
||||
}
|
||||
|
||||
void BluetoothAdapter::setPairableTimeout(quint32 timeout) {
|
||||
if (timeout == this->bPairableTimeout) return;
|
||||
this->bPairableTimeout = timeout;
|
||||
this->pPairableTimeout.write();
|
||||
}
|
||||
|
||||
void BluetoothAdapter::addInterface(const QString& interface, const QVariantMap& properties) {
|
||||
if (interface == "org.bluez.Adapter1") {
|
||||
this->properties.updatePropertySet(properties, false);
|
||||
qCDebug(logAdapter) << "Updated Adapter properties for" << this;
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothAdapter::removeDevice(const QString& devicePath) {
|
||||
qCDebug(logAdapter) << "Removing device" << devicePath << "from adapter" << this;
|
||||
|
||||
auto reply = this->mInterface->RemoveDevice(QDBusObjectPath(devicePath));
|
||||
|
||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QDBusPendingCallWatcher::finished,
|
||||
this,
|
||||
[this, devicePath](QDBusPendingCallWatcher* watcher) {
|
||||
const QDBusPendingReply<> reply = *watcher;
|
||||
|
||||
if (reply.isError()) {
|
||||
qCWarning(logAdapter).nospace()
|
||||
<< "Failed to remove device " << devicePath << " from adapter" << this << ": "
|
||||
<< reply.error().message();
|
||||
} else {
|
||||
qCDebug(logAdapter) << "Successfully removed device" << devicePath << "from adapter"
|
||||
<< this;
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void BluetoothAdapter::startDiscovery() {
|
||||
if (this->bDiscovering) return;
|
||||
qCDebug(logAdapter) << "Starting discovery for adapter" << this;
|
||||
|
||||
auto reply = this->mInterface->StartDiscovery();
|
||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QDBusPendingCallWatcher::finished,
|
||||
this,
|
||||
[this](QDBusPendingCallWatcher* watcher) {
|
||||
const QDBusPendingReply<> reply = *watcher;
|
||||
|
||||
if (reply.isError()) {
|
||||
qCWarning(logAdapter).nospace()
|
||||
<< "Failed to start discovery on adapter" << this << ": " << reply.error().message();
|
||||
} else {
|
||||
qCDebug(logAdapter) << "Successfully started discovery on adapter" << this;
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void BluetoothAdapter::stopDiscovery() {
|
||||
if (!this->bDiscovering) return;
|
||||
qCDebug(logAdapter) << "Stopping discovery for adapter" << this;
|
||||
|
||||
auto reply = this->mInterface->StopDiscovery();
|
||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QDBusPendingCallWatcher::finished,
|
||||
this,
|
||||
[this](QDBusPendingCallWatcher* watcher) {
|
||||
const QDBusPendingReply<> reply = *watcher;
|
||||
|
||||
if (reply.isError()) {
|
||||
qCWarning(logAdapter).nospace()
|
||||
<< "Failed to stop discovery on adapter " << this << ": " << reply.error().message();
|
||||
} else {
|
||||
qCDebug(logAdapter) << "Successfully stopped discovery on adapter" << this;
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
|
||||
namespace qs::dbus {
|
||||
|
||||
using namespace qs::bluetooth;
|
||||
|
||||
DBusResult<BluetoothAdapterState::Enum>
|
||||
DBusDataTransform<BluetoothAdapterState::Enum>::fromWire(const Wire& wire) {
|
||||
if (wire == QStringLiteral("off")) {
|
||||
return BluetoothAdapterState::Disabled;
|
||||
} else if (wire == QStringLiteral("on")) {
|
||||
return BluetoothAdapterState::Enabled;
|
||||
} else if (wire == QStringLiteral("off-enabling")) {
|
||||
return BluetoothAdapterState::Enabling;
|
||||
} else if (wire == QStringLiteral("on-disabling")) {
|
||||
return BluetoothAdapterState::Disabling;
|
||||
} else if (wire == QStringLiteral("off-blocked")) {
|
||||
return BluetoothAdapterState::Blocked;
|
||||
} else {
|
||||
return QDBusError(
|
||||
QDBusError::InvalidArgs,
|
||||
QString("Invalid BluetoothAdapterState: %1").arg(wire)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace qs::dbus
|
||||
|
||||
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothAdapter* adapter) {
|
||||
auto saver = QDebugStateSaver(debug);
|
||||
|
||||
if (adapter) {
|
||||
debug.nospace() << "BluetoothAdapter(" << static_cast<const void*>(adapter)
|
||||
<< ", path=" << adapter->path() << ")";
|
||||
} else {
|
||||
debug << "BluetoothAdapter(nullptr)";
|
||||
}
|
||||
|
||||
return debug;
|
||||
}
|
||||
173
src/bluetooth/adapter.hpp
Normal file
173
src/bluetooth/adapter.hpp
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "../core/doc.hpp"
|
||||
#include "../core/model.hpp"
|
||||
#include "../dbus/properties.hpp"
|
||||
#include "dbus_adapter.h"
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
///! Power state of a Bluetooth adapter.
|
||||
class BluetoothAdapterState: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
QML_SINGLETON;
|
||||
|
||||
public:
|
||||
enum Enum : quint8 {
|
||||
/// The adapter is powered off.
|
||||
Disabled = 0,
|
||||
/// The adapter is powered on.
|
||||
Enabled = 1,
|
||||
/// The adapter is transitioning from off to on.
|
||||
Enabling = 2,
|
||||
/// The adapter is transitioning from on to off.
|
||||
Disabling = 3,
|
||||
/// The adapter is blocked by rfkill.
|
||||
Blocked = 4,
|
||||
};
|
||||
Q_ENUM(Enum);
|
||||
|
||||
Q_INVOKABLE static QString toString(BluetoothAdapterState::Enum state);
|
||||
};
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
|
||||
namespace qs::dbus {
|
||||
|
||||
template <>
|
||||
struct DBusDataTransform<qs::bluetooth::BluetoothAdapterState::Enum> {
|
||||
using Wire = QString;
|
||||
using Data = qs::bluetooth::BluetoothAdapterState::Enum;
|
||||
static DBusResult<Data> fromWire(const Wire& wire);
|
||||
};
|
||||
|
||||
} // namespace qs::dbus
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
class BluetoothAdapter;
|
||||
class BluetoothDevice;
|
||||
|
||||
///! A Bluetooth adapter
|
||||
class BluetoothAdapter: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
QML_UNCREATABLE("");
|
||||
// clang-format off
|
||||
/// System provided name of the adapter. See @@adapterId for the internal identifier.
|
||||
Q_PROPERTY(QString name READ default NOTIFY nameChanged BINDABLE bindableName);
|
||||
/// True if the adapter is currently enabled. More detailed state is available from @@state.
|
||||
Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged);
|
||||
/// Detailed power state of the adapter.
|
||||
Q_PROPERTY(BluetoothAdapterState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState);
|
||||
/// True if the adapter can be discovered by other bluetooth devices.
|
||||
Q_PROPERTY(bool discoverable READ discoverable WRITE setDiscoverable NOTIFY discoverableChanged);
|
||||
/// Timeout in seconds for how long the adapter stays discoverable after @@discoverable is set to true.
|
||||
/// A value of 0 means the adapter stays discoverable forever.
|
||||
Q_PROPERTY(quint32 discoverableTimeout READ discoverableTimeout WRITE setDiscoverableTimeout NOTIFY discoverableTimeoutChanged);
|
||||
/// True if the adapter is scanning for new devices.
|
||||
Q_PROPERTY(bool discovering READ discovering WRITE setDiscovering NOTIFY discoveringChanged);
|
||||
/// True if the adapter is accepting incoming pairing requests.
|
||||
///
|
||||
/// This only affects incoming pairing requests and should typically only be changed
|
||||
/// by system settings applications. Defaults to true.
|
||||
Q_PROPERTY(bool pairable READ pairable WRITE setPairable NOTIFY pairableChanged);
|
||||
/// Timeout in seconds for how long the adapter stays pairable after @@pairable is set to true.
|
||||
/// A value of 0 means the adapter stays pairable forever. Defaults to 0.
|
||||
Q_PROPERTY(quint32 pairableTimeout READ pairableTimeout WRITE setPairableTimeout NOTIFY pairableTimeoutChanged);
|
||||
/// Bluetooth devices connected to this adapter.
|
||||
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::bluetooth::BluetoothDevice>*);
|
||||
Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT);
|
||||
/// The internal ID of the adapter (e.g., "hci0").
|
||||
Q_PROPERTY(QString adapterId READ adapterId CONSTANT);
|
||||
/// DBus path of the adapter under the `org.bluez` system service.
|
||||
Q_PROPERTY(QString dbusPath READ path CONSTANT);
|
||||
// clang-format on
|
||||
|
||||
public:
|
||||
explicit BluetoothAdapter(const QString& path, QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] bool isValid() const { return this->mInterface->isValid(); }
|
||||
[[nodiscard]] QString path() const { return this->mInterface->path(); }
|
||||
[[nodiscard]] QString adapterId() const;
|
||||
|
||||
[[nodiscard]] bool enabled() const { return this->bEnabled; }
|
||||
void setEnabled(bool enabled);
|
||||
|
||||
[[nodiscard]] bool discoverable() const { return this->bDiscoverable; }
|
||||
void setDiscoverable(bool discoverable);
|
||||
|
||||
[[nodiscard]] bool discovering() const { return this->bDiscovering; }
|
||||
void setDiscovering(bool discovering);
|
||||
|
||||
[[nodiscard]] quint32 discoverableTimeout() const { return this->bDiscoverableTimeout; }
|
||||
void setDiscoverableTimeout(quint32 timeout);
|
||||
|
||||
[[nodiscard]] bool pairable() const { return this->bPairable; }
|
||||
void setPairable(bool pairable);
|
||||
|
||||
[[nodiscard]] quint32 pairableTimeout() const { return this->bPairableTimeout; }
|
||||
void setPairableTimeout(quint32 timeout);
|
||||
|
||||
[[nodiscard]] QBindable<QString> bindableName() { return &this->bName; }
|
||||
[[nodiscard]] QBindable<bool> bindableEnabled() { return &this->bEnabled; }
|
||||
[[nodiscard]] QBindable<BluetoothAdapterState::Enum> bindableState() { return &this->bState; }
|
||||
[[nodiscard]] QBindable<bool> bindableDiscoverable() { return &this->bDiscoverable; }
|
||||
[[nodiscard]] QBindable<quint32> bindableDiscoverableTimeout() {
|
||||
return &this->bDiscoverableTimeout;
|
||||
}
|
||||
[[nodiscard]] QBindable<bool> bindableDiscovering() { return &this->bDiscovering; }
|
||||
[[nodiscard]] QBindable<bool> bindablePairable() { return &this->bPairable; }
|
||||
[[nodiscard]] QBindable<quint32> bindablePairableTimeout() { return &this->bPairableTimeout; }
|
||||
[[nodiscard]] ObjectModel<BluetoothDevice>* devices() { return &this->mDevices; }
|
||||
|
||||
void addInterface(const QString& interface, const QVariantMap& properties);
|
||||
void removeDevice(const QString& devicePath);
|
||||
|
||||
void startDiscovery();
|
||||
void stopDiscovery();
|
||||
|
||||
signals:
|
||||
void nameChanged();
|
||||
void enabledChanged();
|
||||
void stateChanged();
|
||||
void discoverableChanged();
|
||||
void discoverableTimeoutChanged();
|
||||
void discoveringChanged();
|
||||
void pairableChanged();
|
||||
void pairableTimeoutChanged();
|
||||
|
||||
private:
|
||||
DBusBluezAdapterInterface* mInterface = nullptr;
|
||||
ObjectModel<BluetoothDevice> mDevices {this};
|
||||
|
||||
// clang-format off
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, QString, bName, &BluetoothAdapter::nameChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bEnabled, &BluetoothAdapter::enabledChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, BluetoothAdapterState::Enum, bState, &BluetoothAdapter::stateChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bDiscoverable, &BluetoothAdapter::discoverableChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, quint32, bDiscoverableTimeout, &BluetoothAdapter::discoverableTimeoutChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bDiscovering, &BluetoothAdapter::discoveringChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bPairable, &BluetoothAdapter::pairableChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, quint32, bPairableTimeout, &BluetoothAdapter::pairableTimeoutChanged);
|
||||
|
||||
QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothAdapter, properties);
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pName, bName, properties, "Alias");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pEnabled, bEnabled, properties, "Powered");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pState, bState, properties, "PowerState");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscoverable, bDiscoverable, properties, "Discoverable");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscoverableTimeout, bDiscoverableTimeout, properties, "DiscoverableTimeout");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscovering, bDiscovering, properties, "Discovering");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pPairable, bPairable, properties, "Pairable");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pPairableTimeout, bPairableTimeout, properties, "PairableTimeout");
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
|
||||
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothAdapter* adapter);
|
||||
168
src/bluetooth/bluez.cpp
Normal file
168
src/bluetooth/bluez.cpp
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
#include "bluez.hpp"
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdbusconnection.h>
|
||||
#include <qdbusextratypes.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qobject.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "../core/logcat.hpp"
|
||||
#include "../dbus/dbus_objectmanager_types.hpp"
|
||||
#include "../dbus/objectmanager.hpp"
|
||||
#include "adapter.hpp"
|
||||
#include "device.hpp"
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
namespace {
|
||||
QS_LOGGING_CATEGORY(logBluetooth, "quickshell.bluetooth", QtWarningMsg);
|
||||
}
|
||||
|
||||
Bluez* Bluez::instance() {
|
||||
static auto* instance = new Bluez();
|
||||
return instance;
|
||||
}
|
||||
|
||||
Bluez::Bluez() { this->init(); }
|
||||
|
||||
void Bluez::updateDefaultAdapter() {
|
||||
const auto& adapters = this->mAdapters.valueList();
|
||||
this->bDefaultAdapter = adapters.empty() ? nullptr : adapters.first();
|
||||
}
|
||||
|
||||
void Bluez::init() {
|
||||
qCDebug(logBluetooth) << "Connecting to BlueZ";
|
||||
|
||||
auto bus = QDBusConnection::systemBus();
|
||||
|
||||
if (!bus.isConnected()) {
|
||||
qCWarning(logBluetooth) << "Could not connect to DBus. Bluetooth integration is not available.";
|
||||
return;
|
||||
}
|
||||
|
||||
this->objectManager = new qs::dbus::DBusObjectManager(this);
|
||||
|
||||
QObject::connect(
|
||||
this->objectManager,
|
||||
&qs::dbus::DBusObjectManager::interfacesAdded,
|
||||
this,
|
||||
&Bluez::onInterfacesAdded
|
||||
);
|
||||
|
||||
QObject::connect(
|
||||
this->objectManager,
|
||||
&qs::dbus::DBusObjectManager::interfacesRemoved,
|
||||
this,
|
||||
&Bluez::onInterfacesRemoved
|
||||
);
|
||||
|
||||
if (!this->objectManager->setInterface("org.bluez", "/", bus)) {
|
||||
qCDebug(logBluetooth) << "BlueZ is not running. Bluetooth integration will not work.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Bluez::onInterfacesAdded(
|
||||
const QDBusObjectPath& path,
|
||||
const DBusObjectManagerInterfaces& interfaces
|
||||
) {
|
||||
if (auto* adapter = this->mAdapterMap.value(path.path())) {
|
||||
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
|
||||
adapter->addInterface(interface, properties);
|
||||
}
|
||||
} else if (auto* device = this->mDeviceMap.value(path.path())) {
|
||||
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
|
||||
device->addInterface(interface, properties);
|
||||
}
|
||||
} else if (interfaces.contains("org.bluez.Adapter1")) {
|
||||
auto* adapter = new BluetoothAdapter(path.path(), this);
|
||||
|
||||
if (!adapter->isValid()) {
|
||||
qCWarning(logBluetooth) << "Adapter path is not valid, cannot track: " << device;
|
||||
delete adapter;
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(logBluetooth) << "Tracked new adapter" << adapter;
|
||||
|
||||
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
|
||||
adapter->addInterface(interface, properties);
|
||||
}
|
||||
|
||||
for (auto* device: this->mDevices.valueList()) {
|
||||
if (device->adapterPath() == path) {
|
||||
adapter->devices()->insertObject(device);
|
||||
qCDebug(logBluetooth) << "Added tracked device" << device << "to new adapter" << adapter;
|
||||
emit device->adapterChanged();
|
||||
}
|
||||
}
|
||||
|
||||
this->mAdapterMap.insert(path.path(), adapter);
|
||||
this->mAdapters.insertObject(adapter);
|
||||
this->updateDefaultAdapter();
|
||||
} else if (interfaces.contains("org.bluez.Device1")) {
|
||||
auto* device = new BluetoothDevice(path.path(), this);
|
||||
|
||||
if (!device->isValid()) {
|
||||
qCWarning(logBluetooth) << "Device path is not valid, cannot track: " << device;
|
||||
delete device;
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(logBluetooth) << "Tracked new device" << device;
|
||||
|
||||
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
|
||||
device->addInterface(interface, properties);
|
||||
}
|
||||
|
||||
if (auto* adapter = device->adapter()) {
|
||||
adapter->devices()->insertObject(device);
|
||||
qCDebug(logBluetooth) << "Added device" << device << "to adapter" << adapter;
|
||||
}
|
||||
|
||||
this->mDeviceMap.insert(path.path(), device);
|
||||
this->mDevices.insertObject(device);
|
||||
}
|
||||
}
|
||||
|
||||
void Bluez::onInterfacesRemoved(const QDBusObjectPath& path, const QStringList& interfaces) {
|
||||
if (auto* adapter = this->mAdapterMap.value(path.path())) {
|
||||
if (interfaces.contains("org.bluez.Adapter1")) {
|
||||
qCDebug(logBluetooth) << "Adapter removed:" << adapter;
|
||||
|
||||
this->mAdapterMap.remove(path.path());
|
||||
this->mAdapters.removeObject(adapter);
|
||||
this->updateDefaultAdapter();
|
||||
delete adapter;
|
||||
}
|
||||
} else if (auto* device = this->mDeviceMap.value(path.path())) {
|
||||
if (interfaces.contains("org.bluez.Device1")) {
|
||||
qCDebug(logBluetooth) << "Device removed:" << device;
|
||||
|
||||
if (auto* adapter = device->adapter()) {
|
||||
adapter->devices()->removeObject(device);
|
||||
}
|
||||
|
||||
this->mDeviceMap.remove(path.path());
|
||||
this->mDevices.removeObject(device);
|
||||
delete device;
|
||||
} else {
|
||||
for (const auto& interface: interfaces) {
|
||||
device->removeInterface(interface);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BluezQml::BluezQml() {
|
||||
QObject::connect(
|
||||
Bluez::instance(),
|
||||
&Bluez::defaultAdapterChanged,
|
||||
this,
|
||||
&BluezQml::defaultAdapterChanged
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
98
src/bluetooth/bluez.hpp
Normal file
98
src/bluetooth/bluez.hpp
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
#pragma once
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qhash.h>
|
||||
#include <qobject.h>
|
||||
#include <qproperty.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "../core/doc.hpp"
|
||||
#include "../core/model.hpp"
|
||||
#include "../dbus/dbus_objectmanager_types.hpp"
|
||||
#include "../dbus/objectmanager.hpp"
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
class BluetoothAdapter;
|
||||
class BluetoothDevice;
|
||||
|
||||
class Bluez: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
[[nodiscard]] ObjectModel<BluetoothAdapter>* adapters() { return &this->mAdapters; }
|
||||
[[nodiscard]] ObjectModel<BluetoothDevice>* devices() { return &this->mDevices; }
|
||||
|
||||
[[nodiscard]] BluetoothAdapter* adapter(const QString& path) {
|
||||
return this->mAdapterMap.value(path);
|
||||
}
|
||||
|
||||
static Bluez* instance();
|
||||
|
||||
signals:
|
||||
void defaultAdapterChanged();
|
||||
|
||||
private slots:
|
||||
void
|
||||
onInterfacesAdded(const QDBusObjectPath& path, const DBusObjectManagerInterfaces& interfaces);
|
||||
void onInterfacesRemoved(const QDBusObjectPath& path, const QStringList& interfaces);
|
||||
void updateDefaultAdapter();
|
||||
|
||||
private:
|
||||
explicit Bluez();
|
||||
void init();
|
||||
|
||||
qs::dbus::DBusObjectManager* objectManager = nullptr;
|
||||
QHash<QString, BluetoothAdapter*> mAdapterMap;
|
||||
QHash<QString, BluetoothDevice*> mDeviceMap;
|
||||
ObjectModel<BluetoothAdapter> mAdapters {this};
|
||||
ObjectModel<BluetoothDevice> mDevices {this};
|
||||
|
||||
public:
|
||||
Q_OBJECT_BINDABLE_PROPERTY(
|
||||
Bluez,
|
||||
BluetoothAdapter*,
|
||||
bDefaultAdapter,
|
||||
&Bluez::defaultAdapterChanged
|
||||
);
|
||||
};
|
||||
|
||||
///! Bluetooth manager
|
||||
/// Provides access to bluetooth devices and adapters.
|
||||
class BluezQml: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_NAMED_ELEMENT(Bluetooth);
|
||||
QML_SINGLETON;
|
||||
// clang-format off
|
||||
/// The default bluetooth adapter. Usually there is only one.
|
||||
Q_PROPERTY(BluetoothAdapter* defaultAdapter READ default NOTIFY defaultAdapterChanged BINDABLE bindableDefaultAdapter);
|
||||
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::bluetooth::BluetoothAdapter>*);
|
||||
/// A list of all bluetooth adapters. See @@defaultAdapter for the default.
|
||||
Q_PROPERTY(UntypedObjectModel* adapters READ adapters CONSTANT);
|
||||
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::bluetooth::BluetoothDevice>*);
|
||||
/// A list of all connected bluetooth devices across all adapters.
|
||||
/// See @@BluetoothAdapter.devices for the devices connected to a single adapter.
|
||||
Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT);
|
||||
// clang-format on
|
||||
|
||||
signals:
|
||||
void defaultAdapterChanged();
|
||||
|
||||
public:
|
||||
explicit BluezQml();
|
||||
|
||||
[[nodiscard]] static ObjectModel<BluetoothAdapter>* adapters() {
|
||||
return Bluez::instance()->adapters();
|
||||
}
|
||||
|
||||
[[nodiscard]] static ObjectModel<BluetoothDevice>* devices() {
|
||||
return Bluez::instance()->devices();
|
||||
}
|
||||
|
||||
[[nodiscard]] static QBindable<BluetoothAdapter*> bindableDefaultAdapter() {
|
||||
return &Bluez::instance()->bDefaultAdapter;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
319
src/bluetooth/device.cpp
Normal file
319
src/bluetooth/device.cpp
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
#include "device.hpp"
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdbusconnection.h>
|
||||
#include <qdbuspendingcall.h>
|
||||
#include <qdbuspendingreply.h>
|
||||
#include <qdebug.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qstring.h>
|
||||
#include <qstringliteral.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "../core/logcat.hpp"
|
||||
#include "../dbus/properties.hpp"
|
||||
#include "adapter.hpp"
|
||||
#include "bluez.hpp"
|
||||
#include "dbus_device.h"
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
namespace {
|
||||
QS_LOGGING_CATEGORY(logDevice, "quickshell.bluetooth.device", QtWarningMsg);
|
||||
}
|
||||
|
||||
QString BluetoothDeviceState::toString(BluetoothDeviceState::Enum state) {
|
||||
switch (state) {
|
||||
case BluetoothDeviceState::Disconnected: return QStringLiteral("Disconnected");
|
||||
case BluetoothDeviceState::Connected: return QStringLiteral("Connected");
|
||||
case BluetoothDeviceState::Disconnecting: return QStringLiteral("Disconnecting");
|
||||
case BluetoothDeviceState::Connecting: return QStringLiteral("Connecting");
|
||||
default: return QStringLiteral("Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
BluetoothDevice::BluetoothDevice(const QString& path, QObject* parent): QObject(parent) {
|
||||
this->mInterface =
|
||||
new DBusBluezDeviceInterface("org.bluez", path, QDBusConnection::systemBus(), this);
|
||||
|
||||
if (!this->mInterface->isValid()) {
|
||||
qCWarning(logDevice) << "Could not create DBus interface for device at" << path;
|
||||
delete this->mInterface;
|
||||
this->mInterface = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
this->properties.setInterface(this->mInterface);
|
||||
}
|
||||
|
||||
BluetoothAdapter* BluetoothDevice::adapter() const {
|
||||
return Bluez::instance()->adapter(this->bAdapterPath.value().path());
|
||||
}
|
||||
|
||||
void BluetoothDevice::setConnected(bool connected) {
|
||||
if (connected == this->bConnected) return;
|
||||
|
||||
if (connected) {
|
||||
this->connect();
|
||||
} else {
|
||||
this->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothDevice::setTrusted(bool trusted) {
|
||||
if (trusted == this->bTrusted) return;
|
||||
this->bTrusted = trusted;
|
||||
this->pTrusted.write();
|
||||
}
|
||||
|
||||
void BluetoothDevice::setBlocked(bool blocked) {
|
||||
if (blocked == this->bBlocked) return;
|
||||
this->bBlocked = blocked;
|
||||
this->pBlocked.write();
|
||||
}
|
||||
|
||||
void BluetoothDevice::setName(const QString& name) {
|
||||
if (name == this->bName) return;
|
||||
this->bName = name;
|
||||
this->pName.write();
|
||||
}
|
||||
|
||||
void BluetoothDevice::setWakeAllowed(bool wakeAllowed) {
|
||||
if (wakeAllowed == this->bWakeAllowed) return;
|
||||
this->bWakeAllowed = wakeAllowed;
|
||||
this->pWakeAllowed.write();
|
||||
}
|
||||
|
||||
void BluetoothDevice::connect() {
|
||||
if (this->bConnected) {
|
||||
qCCritical(logDevice) << "Device" << this << "is already connected";
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->bState == BluetoothDeviceState::Connecting) {
|
||||
qCCritical(logDevice) << "Device" << this << "is already connecting";
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(logDevice) << "Connecting to device" << this;
|
||||
this->bState = BluetoothDeviceState::Connecting;
|
||||
|
||||
auto reply = this->mInterface->Connect();
|
||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QDBusPendingCallWatcher::finished,
|
||||
this,
|
||||
[this](QDBusPendingCallWatcher* watcher) {
|
||||
const QDBusPendingReply<> reply = *watcher;
|
||||
|
||||
if (reply.isError()) {
|
||||
qCWarning(logDevice).nospace()
|
||||
<< "Failed to connect to device " << this << ": " << reply.error().message();
|
||||
|
||||
this->bState = this->bConnected ? BluetoothDeviceState::Connected
|
||||
: BluetoothDeviceState::Disconnected;
|
||||
} else {
|
||||
qCDebug(logDevice) << "Successfully connected to to device" << this;
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void BluetoothDevice::disconnect() {
|
||||
if (!this->bConnected) {
|
||||
qCCritical(logDevice) << "Device" << this << "is already disconnected";
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->bState == BluetoothDeviceState::Disconnecting) {
|
||||
qCCritical(logDevice) << "Device" << this << "is already disconnecting";
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(logDevice) << "Disconnecting from device" << this;
|
||||
this->bState = BluetoothDeviceState::Disconnecting;
|
||||
|
||||
auto reply = this->mInterface->Disconnect();
|
||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QDBusPendingCallWatcher::finished,
|
||||
this,
|
||||
[this](QDBusPendingCallWatcher* watcher) {
|
||||
const QDBusPendingReply<> reply = *watcher;
|
||||
|
||||
if (reply.isError()) {
|
||||
qCWarning(logDevice).nospace()
|
||||
<< "Failed to disconnect from device " << this << ": " << reply.error().message();
|
||||
|
||||
this->bState = this->bConnected ? BluetoothDeviceState::Connected
|
||||
: BluetoothDeviceState::Disconnected;
|
||||
} else {
|
||||
qCDebug(logDevice) << "Successfully disconnected from from device" << this;
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void BluetoothDevice::pair() {
|
||||
if (this->bPaired) {
|
||||
qCCritical(logDevice) << "Device" << this << "is already paired";
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->bPairing) {
|
||||
qCCritical(logDevice) << "Device" << this << "is already pairing";
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(logDevice) << "Pairing with device" << this;
|
||||
this->bPairing = true;
|
||||
|
||||
auto reply = this->mInterface->Pair();
|
||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QDBusPendingCallWatcher::finished,
|
||||
this,
|
||||
[this](QDBusPendingCallWatcher* watcher) {
|
||||
const QDBusPendingReply<> reply = *watcher;
|
||||
if (reply.isError()) {
|
||||
qCWarning(logDevice).nospace()
|
||||
<< "Failed to pair with device " << this << ": " << reply.error().message();
|
||||
} else {
|
||||
qCDebug(logDevice) << "Successfully initiated pairing with device" << this;
|
||||
}
|
||||
|
||||
this->bPairing = false;
|
||||
delete watcher;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void BluetoothDevice::cancelPair() {
|
||||
if (!this->bPairing) {
|
||||
qCCritical(logDevice) << "Device" << this << "is not currently pairing";
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(logDevice) << "Cancelling pairing with device" << this;
|
||||
|
||||
auto reply = this->mInterface->CancelPairing();
|
||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QDBusPendingCallWatcher::finished,
|
||||
this,
|
||||
[this](QDBusPendingCallWatcher* watcher) {
|
||||
const QDBusPendingReply<> reply = *watcher;
|
||||
if (reply.isError()) {
|
||||
qCWarning(logDevice) << "Failed to cancel pairing with device" << this << ":"
|
||||
<< reply.error().message();
|
||||
} else {
|
||||
qCDebug(logDevice) << "Successfully cancelled pairing with device" << this;
|
||||
}
|
||||
|
||||
this->bPairing = false;
|
||||
delete watcher;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void BluetoothDevice::forget() {
|
||||
if (!this->mInterface || !this->mInterface->isValid()) {
|
||||
qCCritical(logDevice) << "Cannot forget - device interface is invalid";
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto* adapter = Bluez::instance()->adapter(this->bAdapterPath.value().path())) {
|
||||
qCDebug(logDevice) << "Forgetting device" << this << "via adapter" << adapter;
|
||||
adapter->removeDevice(this->path());
|
||||
} else {
|
||||
qCCritical(logDevice) << "Could not find adapter for path" << this->bAdapterPath.value().path()
|
||||
<< "to forget from";
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothDevice::addInterface(const QString& interface, const QVariantMap& properties) {
|
||||
if (interface == "org.bluez.Device1") {
|
||||
this->properties.updatePropertySet(properties, false);
|
||||
qCDebug(logDevice) << "Updated Device properties for" << this;
|
||||
} else if (interface == "org.bluez.Battery1") {
|
||||
if (!this->mBatteryInterface) {
|
||||
this->mBatteryInterface = new QDBusInterface(
|
||||
"org.bluez",
|
||||
this->path(),
|
||||
"org.bluez.Battery1",
|
||||
QDBusConnection::systemBus(),
|
||||
this
|
||||
);
|
||||
|
||||
if (!this->mBatteryInterface->isValid()) {
|
||||
qCWarning(logDevice) << "Could not create Battery interface for device at" << this;
|
||||
delete this->mBatteryInterface;
|
||||
this->mBatteryInterface = nullptr;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this->batteryProperties.setInterface(this->mBatteryInterface);
|
||||
this->batteryProperties.updatePropertySet(properties, false);
|
||||
|
||||
emit this->batteryAvailableChanged();
|
||||
qCDebug(logDevice) << "Updated Battery properties for" << this;
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothDevice::removeInterface(const QString& interface) {
|
||||
if (interface == "org.bluez.Battery1" && this->mBatteryInterface) {
|
||||
this->batteryProperties.setInterface(nullptr);
|
||||
delete this->mBatteryInterface;
|
||||
this->mBatteryInterface = nullptr;
|
||||
this->bBattery = 0;
|
||||
|
||||
emit this->batteryAvailableChanged();
|
||||
qCDebug(logDevice) << "Battery interface removed from device" << this;
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothDevice::onConnectedChanged() {
|
||||
this->bState =
|
||||
this->bConnected ? BluetoothDeviceState::Connected : BluetoothDeviceState::Disconnected;
|
||||
emit this->connectedChanged();
|
||||
}
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
|
||||
namespace qs::dbus {
|
||||
|
||||
using namespace qs::bluetooth;
|
||||
|
||||
DBusResult<qreal> DBusDataTransform<BatteryPercentage>::fromWire(quint8 percentage) {
|
||||
return DBusResult(percentage * 0.01);
|
||||
}
|
||||
|
||||
} // namespace qs::dbus
|
||||
|
||||
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothDevice* device) {
|
||||
auto saver = QDebugStateSaver(debug);
|
||||
|
||||
if (device) {
|
||||
debug.nospace() << "BluetoothDevice(" << static_cast<const void*>(device)
|
||||
<< ", path=" << device->path() << ")";
|
||||
} else {
|
||||
debug << "BluetoothDevice(nullptr)";
|
||||
}
|
||||
|
||||
return debug;
|
||||
}
|
||||
225
src/bluetooth/device.hpp
Normal file
225
src/bluetooth/device.hpp
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "../dbus/properties.hpp"
|
||||
#include "dbus_device.h"
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
///! Connection state of a Bluetooth device.
|
||||
class BluetoothDeviceState: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
QML_SINGLETON;
|
||||
|
||||
public:
|
||||
enum Enum : quint8 {
|
||||
/// The device is not connected.
|
||||
Disconnected = 0,
|
||||
/// The device is connected.
|
||||
Connected = 1,
|
||||
/// The device is disconnecting.
|
||||
Disconnecting = 2,
|
||||
/// The device is connecting.
|
||||
Connecting = 3,
|
||||
};
|
||||
Q_ENUM(Enum);
|
||||
|
||||
Q_INVOKABLE static QString toString(BluetoothDeviceState::Enum state);
|
||||
};
|
||||
|
||||
struct BatteryPercentage {};
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
|
||||
namespace qs::dbus {
|
||||
|
||||
template <>
|
||||
struct DBusDataTransform<qs::bluetooth::BatteryPercentage> {
|
||||
using Wire = quint8;
|
||||
using Data = qreal;
|
||||
static DBusResult<Data> fromWire(Wire percentage);
|
||||
};
|
||||
|
||||
} // namespace qs::dbus
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
class BluetoothAdapter;
|
||||
|
||||
///! A tracked Bluetooth device.
|
||||
class BluetoothDevice: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
QML_UNCREATABLE("");
|
||||
// clang-format off
|
||||
/// MAC address of the device.
|
||||
Q_PROPERTY(QString address READ default NOTIFY addressChanged BINDABLE bindableAddress);
|
||||
/// The name of the Bluetooth device. This property may be written to create an alias, or set to
|
||||
/// an empty string to fall back to the device provided name.
|
||||
///
|
||||
/// See @@deviceName for the name provided by the device.
|
||||
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged);
|
||||
/// The name of the Bluetooth device, ignoring user provided aliases. See also @@name
|
||||
/// which returns a user provided alias if set.
|
||||
Q_PROPERTY(QString deviceName READ default NOTIFY deviceNameChanged BINDABLE bindableDeviceName);
|
||||
/// System icon representing the device type. Use @@Quickshell.Quickshell.iconPath() to display this in an image.
|
||||
Q_PROPERTY(QString icon READ default NOTIFY iconChanged BINDABLE bindableIcon);
|
||||
/// Connection state of the device.
|
||||
Q_PROPERTY(BluetoothDeviceState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState);
|
||||
/// True if the device is currently connected to the computer.
|
||||
///
|
||||
/// Setting this property is equivalent to calling @@connect() and @@disconnect().
|
||||
///
|
||||
/// > [!NOTE] @@state provides more detailed information if required.
|
||||
Q_PROPERTY(bool connected READ connected WRITE setConnected NOTIFY connectedChanged);
|
||||
/// True if the device is paired to the computer.
|
||||
///
|
||||
/// > [!NOTE] @@pair() can be used to pair a device, however you must @@forget() the device to unpair it.
|
||||
Q_PROPERTY(bool paired READ default NOTIFY pairedChanged BINDABLE bindablePaired);
|
||||
/// True if pairing information is stored for future connections.
|
||||
Q_PROPERTY(bool bonded READ default NOTIFY bondedChanged BINDABLE bindableBonded);
|
||||
/// True if the device is currently being paired.
|
||||
///
|
||||
/// > [!NOTE] @@cancelPair() can be used to cancel the pairing process.
|
||||
Q_PROPERTY(bool pairing READ pairing NOTIFY pairingChanged);
|
||||
/// True if the device is considered to be trusted by the system.
|
||||
/// Trusted devices are allowed to reconnect themselves to the system without intervention.
|
||||
Q_PROPERTY(bool trusted READ trusted WRITE setTrusted NOTIFY trustedChanged);
|
||||
/// True if the device is blocked from connecting.
|
||||
/// If a device is blocked, any connection attempts will be immediately rejected by the system.
|
||||
Q_PROPERTY(bool blocked READ blocked WRITE setBlocked NOTIFY blockedChanged);
|
||||
/// True if the device is allowed to wake up the host system from suspend.
|
||||
Q_PROPERTY(bool wakeAllowed READ wakeAllowed WRITE setWakeAllowed NOTIFY wakeAllowedChanged);
|
||||
/// True if the connected device reports its battery level. Battery level can be accessed via @@battery.
|
||||
Q_PROPERTY(bool batteryAvailable READ batteryAvailable NOTIFY batteryAvailableChanged);
|
||||
/// Battery level of the connected device, from `0.0` to `1.0`. Only valid if @@batteryAvailable is true.
|
||||
Q_PROPERTY(qreal battery READ default NOTIFY batteryChanged BINDABLE bindableBattery);
|
||||
/// The Bluetooth adapter this device belongs to.
|
||||
Q_PROPERTY(BluetoothAdapter* adapter READ adapter NOTIFY adapterChanged);
|
||||
/// DBus path of the device under the `org.bluez` system service.
|
||||
Q_PROPERTY(QString dbusPath READ path CONSTANT);
|
||||
// clang-format on
|
||||
|
||||
public:
|
||||
explicit BluetoothDevice(const QString& path, QObject* parent = nullptr);
|
||||
|
||||
/// Attempt to connect to the device.
|
||||
Q_INVOKABLE void connect();
|
||||
/// Disconnect from the device.
|
||||
Q_INVOKABLE void disconnect();
|
||||
/// Attempt to pair the device.
|
||||
///
|
||||
/// > [!NOTE] @@paired and @@pairing return the current pairing status of the device.
|
||||
Q_INVOKABLE void pair();
|
||||
/// Cancel an active pairing attempt.
|
||||
Q_INVOKABLE void cancelPair();
|
||||
/// Forget the device.
|
||||
Q_INVOKABLE void forget();
|
||||
|
||||
[[nodiscard]] bool isValid() const { return this->mInterface && this->mInterface->isValid(); }
|
||||
[[nodiscard]] QString path() const {
|
||||
return this->mInterface ? this->mInterface->path() : QString();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool batteryAvailable() const { return this->mBatteryInterface != nullptr; }
|
||||
[[nodiscard]] BluetoothAdapter* adapter() const;
|
||||
[[nodiscard]] QDBusObjectPath adapterPath() const { return this->bAdapterPath.value(); }
|
||||
|
||||
[[nodiscard]] bool connected() const { return this->bConnected; }
|
||||
void setConnected(bool connected);
|
||||
|
||||
[[nodiscard]] bool trusted() const { return this->bTrusted; }
|
||||
void setTrusted(bool trusted);
|
||||
|
||||
[[nodiscard]] bool blocked() const { return this->bBlocked; }
|
||||
void setBlocked(bool blocked);
|
||||
|
||||
[[nodiscard]] QString name() const { return this->bName; }
|
||||
void setName(const QString& name);
|
||||
|
||||
[[nodiscard]] bool wakeAllowed() const { return this->bWakeAllowed; }
|
||||
void setWakeAllowed(bool wakeAllowed);
|
||||
|
||||
[[nodiscard]] bool pairing() const { return this->bPairing; }
|
||||
|
||||
[[nodiscard]] QBindable<QString> bindableAddress() { return &this->bAddress; }
|
||||
[[nodiscard]] QBindable<QString> bindableDeviceName() { return &this->bDeviceName; }
|
||||
[[nodiscard]] QBindable<QString> bindableName() { return &this->bName; }
|
||||
[[nodiscard]] QBindable<bool> bindableConnected() { return &this->bConnected; }
|
||||
[[nodiscard]] QBindable<bool> bindablePaired() { return &this->bPaired; }
|
||||
[[nodiscard]] QBindable<bool> bindableBonded() { return &this->bBonded; }
|
||||
[[nodiscard]] QBindable<bool> bindableTrusted() { return &this->bTrusted; }
|
||||
[[nodiscard]] QBindable<bool> bindableBlocked() { return &this->bBlocked; }
|
||||
[[nodiscard]] QBindable<bool> bindableWakeAllowed() { return &this->bWakeAllowed; }
|
||||
[[nodiscard]] QBindable<QString> bindableIcon() { return &this->bIcon; }
|
||||
[[nodiscard]] QBindable<qreal> bindableBattery() { return &this->bBattery; }
|
||||
[[nodiscard]] QBindable<BluetoothDeviceState::Enum> bindableState() { return &this->bState; }
|
||||
|
||||
void addInterface(const QString& interface, const QVariantMap& properties);
|
||||
void removeInterface(const QString& interface);
|
||||
|
||||
signals:
|
||||
void addressChanged();
|
||||
void deviceNameChanged();
|
||||
void nameChanged();
|
||||
void connectedChanged();
|
||||
void stateChanged();
|
||||
void pairedChanged();
|
||||
void bondedChanged();
|
||||
void pairingChanged();
|
||||
void trustedChanged();
|
||||
void blockedChanged();
|
||||
void wakeAllowedChanged();
|
||||
void iconChanged();
|
||||
void batteryAvailableChanged();
|
||||
void batteryChanged();
|
||||
void adapterChanged();
|
||||
|
||||
private:
|
||||
void onConnectedChanged();
|
||||
|
||||
DBusBluezDeviceInterface* mInterface = nullptr;
|
||||
QDBusInterface* mBatteryInterface = nullptr;
|
||||
|
||||
// clang-format off
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bAddress, &BluetoothDevice::addressChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bDeviceName, &BluetoothDevice::deviceNameChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bName, &BluetoothDevice::nameChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bConnected, &BluetoothDevice::onConnectedChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bPaired, &BluetoothDevice::pairedChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bBonded, &BluetoothDevice::bondedChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bTrusted, &BluetoothDevice::trustedChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bBlocked, &BluetoothDevice::blockedChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bWakeAllowed, &BluetoothDevice::wakeAllowedChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bIcon, &BluetoothDevice::iconChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QDBusObjectPath, bAdapterPath);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, qreal, bBattery, &BluetoothDevice::batteryChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, BluetoothDeviceState::Enum, bState, &BluetoothDevice::stateChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bPairing, &BluetoothDevice::pairingChanged);
|
||||
|
||||
QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothDevice, properties);
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pAddress, bAddress, properties, "Address");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pDeviceName, bDeviceName, properties, "Name");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pName, bName, properties, "Alias");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pConnected, bConnected, properties, "Connected");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pPaired, bPaired, properties, "Paired");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pBonded, bBonded, properties, "Bonded");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pTrusted, bTrusted, properties, "Trusted");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pBlocked, bBlocked, properties, "Blocked");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pWakeAllowed, bWakeAllowed, properties, "WakeAllowed");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pIcon, bIcon, properties, "Icon");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pAdapterPath, bAdapterPath, properties, "Adapter");
|
||||
|
||||
QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothDevice, batteryProperties);
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, BatteryPercentage, pBattery, bBattery, batteryProperties, "Percentage", true);
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
|
||||
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothDevice* device);
|
||||
12
src/bluetooth/module.md
Normal file
12
src/bluetooth/module.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
name = "Quickshell.Bluetooth"
|
||||
description = "Bluetooth API"
|
||||
headers = [
|
||||
"bluez.hpp",
|
||||
"adapter.hpp",
|
||||
"device.hpp",
|
||||
]
|
||||
-----
|
||||
This module exposes Bluetooth management APIs provided by the BlueZ DBus interface.
|
||||
Both DBus and BlueZ must be running to use it.
|
||||
|
||||
See the @@Quickshell.Bluetooth.Bluetooth singleton.
|
||||
9
src/bluetooth/org.bluez.Adapter.xml
Normal file
9
src/bluetooth/org.bluez.Adapter.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<node>
|
||||
<interface name="org.bluez.Adapter1">
|
||||
<method name="StartDiscovery"/>
|
||||
<method name="StopDiscovery"/>
|
||||
<method name="RemoveDevice">
|
||||
<arg name="device" type="o"/>
|
||||
</method>
|
||||
</interface>
|
||||
</node>
|
||||
8
src/bluetooth/org.bluez.Device.xml
Normal file
8
src/bluetooth/org.bluez.Device.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<node>
|
||||
<interface name="org.bluez.Device1">
|
||||
<method name="Connect"/>
|
||||
<method name="Disconnect"/>
|
||||
<method name="Pair"/>
|
||||
<method name="CancelPairing"/>
|
||||
</interface>
|
||||
</node>
|
||||
200
src/bluetooth/test/manual/test.qml
Normal file
200
src/bluetooth/test/manual/test.qml
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Bluetooth
|
||||
|
||||
FloatingWindow {
|
||||
color: contentItem.palette.window
|
||||
|
||||
ListView {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
model: Bluetooth.adapters
|
||||
|
||||
delegate: WrapperRectangle {
|
||||
width: parent.width
|
||||
color: "transparent"
|
||||
border.color: palette.button
|
||||
border.width: 1
|
||||
margin: 5
|
||||
|
||||
ColumnLayout {
|
||||
Label { text: `Adapter: ${modelData.name} (${modelData.adapterId})` }
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
CheckBox {
|
||||
text: "Enable"
|
||||
checked: modelData.enabled
|
||||
onToggled: modelData.enabled = checked
|
||||
}
|
||||
|
||||
Label {
|
||||
color: modelData.state === BluetoothAdapterState.Blocked ? palette.errorText : palette.placeholderText
|
||||
text: BluetoothAdapterState.toString(modelData.state)
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
text: "Discoverable"
|
||||
checked: modelData.discoverable
|
||||
onToggled: modelData.discoverable = checked
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
text: "Discovering"
|
||||
checked: modelData.discovering
|
||||
onToggled: modelData.discovering = checked
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
text: "Pairable"
|
||||
checked: modelData.pairable
|
||||
onToggled: modelData.pairable = checked
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Label { text: "Discoverable timeout:" }
|
||||
|
||||
SpinBox {
|
||||
from: 0
|
||||
to: 3600
|
||||
value: modelData.discoverableTimeout
|
||||
onValueModified: modelData.discoverableTimeout = value
|
||||
textFromValue: time => time === 0 ? "∞" : time + "s"
|
||||
}
|
||||
|
||||
Label { text: "Pairable timeout:" }
|
||||
|
||||
SpinBox {
|
||||
from: 0
|
||||
to: 3600
|
||||
value: modelData.pairableTimeout
|
||||
onValueModified: modelData.pairableTimeout = value
|
||||
textFromValue: time => time === 0 ? "∞" : time + "s"
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: modelData.devices
|
||||
|
||||
WrapperRectangle {
|
||||
Layout.fillWidth: true
|
||||
color: palette.button
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
margin: 5
|
||||
|
||||
RowLayout {
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
RowLayout {
|
||||
IconImage {
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: height
|
||||
source: Quickshell.iconPath(modelData.icon)
|
||||
}
|
||||
|
||||
TextField {
|
||||
text: modelData.name
|
||||
font.bold: true
|
||||
background: null
|
||||
readOnly: false
|
||||
selectByMouse: true
|
||||
onEditingFinished: modelData.name = text
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: modelData.name && modelData.name !== modelData.deviceName
|
||||
text: `(${modelData.deviceName})`
|
||||
color: palette.placeholderText
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Label {
|
||||
text: modelData.address
|
||||
color: palette.placeholderText
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: modelData.batteryAvailable
|
||||
text: `| Battery: ${Math.round(modelData.battery * 100)}%`
|
||||
color: palette.placeholderText
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Label {
|
||||
text: BluetoothDeviceState.toString(modelData.state)
|
||||
|
||||
color: modelData.connected ? palette.link : palette.placeholderText
|
||||
}
|
||||
|
||||
Label {
|
||||
text: modelData.pairing ? "Pairing" : (modelData.paired ? "Paired" : "Not Paired")
|
||||
color: modelData.paired || modelData.pairing ? palette.link : palette.placeholderText
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: modelData.bonded
|
||||
text: "| Bonded"
|
||||
color: palette.link
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
text: "Trusted"
|
||||
checked: modelData.trusted
|
||||
onToggled: modelData.trusted = checked
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
text: "Blocked"
|
||||
checked: modelData.blocked
|
||||
onToggled: modelData.blocked = checked
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
text: "Wake Allowed"
|
||||
checked: modelData.wakeAllowed
|
||||
onToggled: modelData.wakeAllowed = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
text: modelData.connected ? "Disconnect" : "Connect"
|
||||
onClicked: modelData.connected = !modelData.connected
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
text: modelData.pairing ? "Cancel" : (modelData.paired ? "Forget" : "Pair")
|
||||
onClicked: {
|
||||
if (modelData.pairing) {
|
||||
modelData.cancelPair();
|
||||
} else if (modelData.paired) {
|
||||
modelData.forget();
|
||||
} else {
|
||||
modelData.pair();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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,23 +1,62 @@
|
|||
qt_add_executable(quickshell
|
||||
main.cpp
|
||||
qt_add_library(quickshell-core STATIC
|
||||
plugin.cpp
|
||||
shell.cpp
|
||||
variants.cpp
|
||||
rootwrapper.cpp
|
||||
proxywindow.cpp
|
||||
reload.cpp
|
||||
rootwrapper.cpp
|
||||
qmlglobal.cpp
|
||||
qmlscreen.cpp
|
||||
watcher.cpp
|
||||
region.cpp
|
||||
persistentprops.cpp
|
||||
windowinterface.cpp
|
||||
floatingwindow.cpp
|
||||
panelinterface.cpp
|
||||
singleton.cpp
|
||||
generation.cpp
|
||||
scan.cpp
|
||||
qsintercept.cpp
|
||||
incubator.cpp
|
||||
lazyloader.cpp
|
||||
easingcurve.cpp
|
||||
iconimageprovider.cpp
|
||||
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
|
||||
colorquantizer.cpp
|
||||
toolsupport.cpp
|
||||
)
|
||||
|
||||
set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}")
|
||||
qt_add_qml_module(quickshell 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 PRIVATE ${QT_DEPS})
|
||||
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)
|
||||
|
||||
if (BUILD_TESTING)
|
||||
add_subdirectory(test)
|
||||
endif()
|
||||
|
|
|
|||
258
src/core/boundcomponent.cpp
Normal file
258
src/core/boundcomponent.cpp
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
#include "boundcomponent.hpp"
|
||||
#include <utility>
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qlogging.h>
|
||||
#include <qmetaobject.h>
|
||||
#include <qobject.h>
|
||||
#include <qobjectdefs.h>
|
||||
#include <qqmlcomponent.h>
|
||||
#include <qqmlcontext.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qqmlerror.h>
|
||||
#include <qquickitem.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "incubator.hpp"
|
||||
|
||||
QObject* BoundComponent::item() const { return this->object; }
|
||||
QQmlComponent* BoundComponent::sourceComponent() const { return this->mComponent; }
|
||||
|
||||
void BoundComponent::setSourceComponent(QQmlComponent* component) {
|
||||
if (component == this->mComponent) return;
|
||||
|
||||
if (this->componentCompleted) {
|
||||
qWarning() << "BoundComponent.component cannot be set after creation";
|
||||
return;
|
||||
}
|
||||
this->disconnectComponent();
|
||||
|
||||
this->ownsComponent = false;
|
||||
this->mComponent = component;
|
||||
if (component != nullptr) {
|
||||
QObject::connect(component, &QObject::destroyed, this, &BoundComponent::onComponentDestroyed);
|
||||
}
|
||||
|
||||
emit this->sourceComponentChanged();
|
||||
}
|
||||
|
||||
void BoundComponent::disconnectComponent() {
|
||||
if (this->mComponent == nullptr) return;
|
||||
|
||||
if (this->ownsComponent) {
|
||||
delete this->mComponent;
|
||||
} else {
|
||||
QObject::disconnect(this->mComponent, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
this->mComponent = nullptr;
|
||||
}
|
||||
|
||||
void BoundComponent::onComponentDestroyed() { this->mComponent = nullptr; }
|
||||
QString BoundComponent::source() const { return this->mSource; }
|
||||
|
||||
void BoundComponent::setSource(QString source) {
|
||||
if (source == this->mSource) return;
|
||||
|
||||
if (this->componentCompleted) {
|
||||
qWarning() << "BoundComponent.url cannot be set after creation";
|
||||
return;
|
||||
}
|
||||
|
||||
auto* context = QQmlEngine::contextForObject(this);
|
||||
auto* component = new QQmlComponent(context->engine(), context->resolvedUrl(source), this);
|
||||
|
||||
if (component->isError()) {
|
||||
qWarning() << component->errorString().toStdString().c_str();
|
||||
delete component;
|
||||
} else {
|
||||
this->disconnectComponent();
|
||||
this->ownsComponent = true;
|
||||
this->mSource = std::move(source);
|
||||
this->mComponent = component;
|
||||
|
||||
emit this->sourceChanged();
|
||||
emit this->sourceComponentChanged();
|
||||
}
|
||||
}
|
||||
|
||||
bool BoundComponent::bindValues() const { return this->mBindValues; }
|
||||
|
||||
void BoundComponent::setBindValues(bool bindValues) {
|
||||
if (this->componentCompleted) {
|
||||
qWarning() << "BoundComponent.bindValues cannot be set after creation";
|
||||
return;
|
||||
}
|
||||
|
||||
this->mBindValues = bindValues;
|
||||
emit this->bindValuesChanged();
|
||||
}
|
||||
|
||||
void BoundComponent::componentComplete() {
|
||||
this->QQuickItem::componentComplete();
|
||||
this->componentCompleted = true;
|
||||
this->tryCreate();
|
||||
}
|
||||
|
||||
void BoundComponent::tryCreate() {
|
||||
if (this->mComponent == nullptr) {
|
||||
qWarning() << "BoundComponent has no component";
|
||||
return;
|
||||
}
|
||||
|
||||
auto initialProperties = QVariantMap();
|
||||
|
||||
const auto* metaObject = this->metaObject();
|
||||
for (auto i = metaObject->propertyOffset(); i < metaObject->propertyCount(); i++) {
|
||||
const auto prop = metaObject->property(i);
|
||||
|
||||
if (prop.isReadable()) {
|
||||
initialProperties.insert(prop.name(), prop.read(this));
|
||||
}
|
||||
}
|
||||
|
||||
this->incubator = new QsQmlIncubator(QsQmlIncubator::AsynchronousIfNested, this);
|
||||
this->incubator->setInitialProperties(initialProperties);
|
||||
|
||||
// clang-format off
|
||||
QObject::connect(this->incubator, &QsQmlIncubator::completed, this, &BoundComponent::onIncubationCompleted);
|
||||
QObject::connect(this->incubator, &QsQmlIncubator::failed, this, &BoundComponent::onIncubationFailed);
|
||||
// clang-format on
|
||||
|
||||
this->mComponent->create(*this->incubator, QQmlEngine::contextForObject(this));
|
||||
}
|
||||
|
||||
void BoundComponent::onIncubationCompleted() {
|
||||
this->object = this->incubator->object();
|
||||
delete this->incubator;
|
||||
this->disconnectComponent();
|
||||
|
||||
this->object->setParent(this);
|
||||
this->mItem = qobject_cast<QQuickItem*>(this->object);
|
||||
|
||||
const auto* metaObject = this->metaObject();
|
||||
const auto* objectMetaObject = this->object->metaObject();
|
||||
|
||||
if (this->mBindValues) {
|
||||
for (auto i = metaObject->propertyOffset(); i < metaObject->propertyCount(); i++) {
|
||||
const auto prop = metaObject->property(i);
|
||||
|
||||
if (prop.isReadable() && prop.hasNotifySignal()) {
|
||||
const auto objectPropIndex = objectMetaObject->indexOfProperty(prop.name());
|
||||
|
||||
if (objectPropIndex == -1) {
|
||||
qWarning() << "property" << prop.name()
|
||||
<< "defined on BoundComponent but not on its contained object.";
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto objectProp = objectMetaObject->property(objectPropIndex);
|
||||
if (objectProp.isWritable()) {
|
||||
auto* proxy = new BoundComponentPropertyProxy(this, this->object, prop, objectProp);
|
||||
proxy->onNotified(); // any changes that might've happened before connection
|
||||
} else {
|
||||
qWarning() << "property" << prop.name()
|
||||
<< "defined on BoundComponent is not writable for its contained object.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (auto i = metaObject->methodOffset(); i < metaObject->methodCount(); i++) {
|
||||
const auto method = metaObject->method(i);
|
||||
|
||||
if (method.name().startsWith("on") && method.name().length() > 2) {
|
||||
auto sig = QString(method.methodSignature()).sliced(2);
|
||||
if (!sig[0].isUpper()) continue;
|
||||
sig[0] = sig[0].toLower();
|
||||
auto name = sig.sliced(0, sig.indexOf('('));
|
||||
|
||||
auto mostViableSignal = QMetaMethod();
|
||||
for (auto i = 0; i < objectMetaObject->methodCount(); i++) {
|
||||
const auto method = objectMetaObject->method(i);
|
||||
if (method.methodSignature() == sig) {
|
||||
mostViableSignal = method;
|
||||
break;
|
||||
}
|
||||
|
||||
if (method.name() == name) {
|
||||
if (mostViableSignal.isValid()) {
|
||||
qWarning() << "Multiple candidates, so none will be attached for signal" << name;
|
||||
goto next;
|
||||
}
|
||||
|
||||
mostViableSignal = method;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mostViableSignal.isValid()) {
|
||||
qWarning() << "Function" << method.name() << "appears to be a signal handler for" << name
|
||||
<< "but it does not match any signals on the target object";
|
||||
goto next;
|
||||
}
|
||||
|
||||
QMetaObject::connect(
|
||||
this->object,
|
||||
mostViableSignal.methodIndex(),
|
||||
this,
|
||||
method.methodIndex()
|
||||
);
|
||||
}
|
||||
|
||||
next:;
|
||||
}
|
||||
|
||||
if (this->mItem != nullptr) {
|
||||
this->mItem->setParentItem(this);
|
||||
|
||||
// clang-format off
|
||||
QObject::connect(this, &QQuickItem::widthChanged, this, &BoundComponent::updateSize);
|
||||
QObject::connect(this, &QQuickItem::heightChanged, this, &BoundComponent::updateSize);
|
||||
QObject::connect(this->mItem, &QQuickItem::implicitWidthChanged, this, &BoundComponent::updateImplicitSize);
|
||||
QObject::connect(this->mItem, &QQuickItem::implicitHeightChanged, this, &BoundComponent::updateImplicitSize);
|
||||
// clang-format on
|
||||
|
||||
this->updateImplicitSize();
|
||||
this->updateSize();
|
||||
}
|
||||
|
||||
emit this->loaded();
|
||||
}
|
||||
|
||||
void BoundComponent::onIncubationFailed() {
|
||||
qWarning() << "Failed to create BoundComponent";
|
||||
|
||||
for (auto& error: this->incubator->errors()) {
|
||||
qWarning() << error;
|
||||
}
|
||||
|
||||
delete this->incubator;
|
||||
this->disconnectComponent();
|
||||
}
|
||||
|
||||
void BoundComponent::updateSize() { this->mItem->setSize(this->size()); }
|
||||
|
||||
void BoundComponent::updateImplicitSize() {
|
||||
this->setImplicitWidth(this->mItem->implicitWidth());
|
||||
this->setImplicitHeight(this->mItem->implicitHeight());
|
||||
}
|
||||
|
||||
BoundComponentPropertyProxy::BoundComponentPropertyProxy(
|
||||
QObject* from,
|
||||
QObject* to,
|
||||
QMetaProperty fromProperty,
|
||||
QMetaProperty toProperty
|
||||
)
|
||||
: QObject(from)
|
||||
, from(from)
|
||||
, to(to)
|
||||
, fromProperty(fromProperty)
|
||||
, toProperty(toProperty) {
|
||||
const auto* metaObject = this->metaObject();
|
||||
auto method = metaObject->indexOfSlot("onNotified()");
|
||||
QMetaObject::connect(from, fromProperty.notifySignal().methodIndex(), this, method);
|
||||
}
|
||||
|
||||
void BoundComponentPropertyProxy::onNotified() {
|
||||
this->toProperty.write(this->to, this->fromProperty.read(this->from));
|
||||
}
|
||||
125
src/core/boundcomponent.hpp
Normal file
125
src/core/boundcomponent.hpp
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
#pragma once
|
||||
|
||||
#include <qmetaobject.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlcomponent.h>
|
||||
#include <qqmlparserstatus.h>
|
||||
#include <qquickitem.h>
|
||||
#include <qsignalmapper.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "incubator.hpp"
|
||||
|
||||
///! Component loader that allows setting initial properties.
|
||||
/// Component loader that allows setting initial properties, primarily useful for
|
||||
/// escaping cyclic dependency errors.
|
||||
///
|
||||
/// Properties defined on the BoundComponent will be applied to its loaded component,
|
||||
/// including required properties, and will remain reactive. Functions created with
|
||||
/// the names of signal handlers will also be attached to signals of the loaded component.
|
||||
///
|
||||
/// ```qml {filename="MyComponent.qml"}
|
||||
/// MouseArea {
|
||||
/// required property color color;
|
||||
/// width: 100
|
||||
/// height: 100
|
||||
///
|
||||
/// Rectangle {
|
||||
/// anchors.fill: parent
|
||||
/// color: parent.color
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ```qml
|
||||
/// BoundComponent {
|
||||
/// source: "MyComponent.qml"
|
||||
///
|
||||
/// // this is the same as assigning to `color` on MyComponent if loaded normally.
|
||||
/// property color color: "red";
|
||||
///
|
||||
/// // this will be triggered when the `clicked` signal from the MouseArea is sent.
|
||||
/// function onClicked() {
|
||||
/// color = "blue";
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
class BoundComponent: public QQuickItem {
|
||||
Q_OBJECT;
|
||||
// clang-format off
|
||||
/// The loaded component. Will be null until it has finished loading.
|
||||
Q_PROPERTY(QObject* item READ item NOTIFY loaded);
|
||||
/// The source to load, as a Component.
|
||||
Q_PROPERTY(QQmlComponent* sourceComponent READ sourceComponent WRITE setSourceComponent NOTIFY sourceComponentChanged);
|
||||
/// The source to load, as a Url.
|
||||
Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged);
|
||||
/// If property values should be bound after they are initially set. Defaults to `true`.
|
||||
Q_PROPERTY(bool bindValues READ bindValues WRITE setBindValues NOTIFY bindValuesChanged);
|
||||
Q_PROPERTY(qreal implicitWidth READ implicitWidth NOTIFY implicitWidthChanged);
|
||||
Q_PROPERTY(qreal implicitHeight READ implicitHeight NOTIFY implicitHeightChanged);
|
||||
// clang-format on
|
||||
QML_ELEMENT;
|
||||
|
||||
public:
|
||||
explicit BoundComponent(QQuickItem* parent = nullptr): QQuickItem(parent) {}
|
||||
|
||||
void componentComplete() override;
|
||||
|
||||
[[nodiscard]] QObject* item() const;
|
||||
|
||||
[[nodiscard]] QQmlComponent* sourceComponent() const;
|
||||
void setSourceComponent(QQmlComponent* sourceComponent);
|
||||
|
||||
[[nodiscard]] QString source() const;
|
||||
void setSource(QString source);
|
||||
|
||||
[[nodiscard]] bool bindValues() const;
|
||||
void setBindValues(bool bindValues);
|
||||
|
||||
signals:
|
||||
void loaded();
|
||||
void sourceComponentChanged();
|
||||
void sourceChanged();
|
||||
void bindValuesChanged();
|
||||
|
||||
private slots:
|
||||
void onComponentDestroyed();
|
||||
void onIncubationCompleted();
|
||||
void onIncubationFailed();
|
||||
void updateSize();
|
||||
void updateImplicitSize();
|
||||
|
||||
private:
|
||||
void disconnectComponent();
|
||||
void tryCreate();
|
||||
|
||||
QString mSource;
|
||||
bool mBindValues = true;
|
||||
QQmlComponent* mComponent = nullptr;
|
||||
bool ownsComponent = false;
|
||||
QsQmlIncubator* incubator = nullptr;
|
||||
QObject* object = nullptr;
|
||||
QQuickItem* mItem = nullptr;
|
||||
bool componentCompleted = false;
|
||||
};
|
||||
|
||||
class BoundComponentPropertyProxy: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
BoundComponentPropertyProxy(
|
||||
QObject* from,
|
||||
QObject* to,
|
||||
QMetaProperty fromProperty,
|
||||
QMetaProperty toProperty
|
||||
);
|
||||
|
||||
public slots:
|
||||
void onNotified();
|
||||
|
||||
private:
|
||||
QObject* from;
|
||||
QObject* to;
|
||||
QMetaProperty fromProperty;
|
||||
QMetaProperty toProperty;
|
||||
};
|
||||
88
src/core/clock.cpp
Normal file
88
src/core/clock.cpp
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
#include "clock.hpp"
|
||||
|
||||
#include <qdatetime.h>
|
||||
#include <qobject.h>
|
||||
#include <qtimer.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
SystemClock::SystemClock(QObject* parent): QObject(parent) {
|
||||
QObject::connect(&this->timer, &QTimer::timeout, this, &SystemClock::onTimeout);
|
||||
this->update();
|
||||
}
|
||||
|
||||
bool SystemClock::enabled() const { return this->mEnabled; }
|
||||
|
||||
void SystemClock::setEnabled(bool enabled) {
|
||||
if (enabled == this->mEnabled) return;
|
||||
this->mEnabled = enabled;
|
||||
emit this->enabledChanged();
|
||||
this->update();
|
||||
}
|
||||
|
||||
SystemClock::Enum SystemClock::precision() const { return this->mPrecision; }
|
||||
|
||||
void SystemClock::setPrecision(SystemClock::Enum precision) {
|
||||
if (precision == this->mPrecision) return;
|
||||
this->mPrecision = precision;
|
||||
emit this->precisionChanged();
|
||||
this->update();
|
||||
}
|
||||
|
||||
void SystemClock::onTimeout() {
|
||||
this->setTime(this->targetTime);
|
||||
this->schedule(this->targetTime);
|
||||
}
|
||||
|
||||
void SystemClock::update() {
|
||||
if (this->mEnabled) {
|
||||
this->setTime(QDateTime::fromMSecsSinceEpoch(0));
|
||||
this->schedule(QDateTime::fromMSecsSinceEpoch(0));
|
||||
} else {
|
||||
this->timer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
void SystemClock::setTime(const QDateTime& targetTime) {
|
||||
auto currentTime = QDateTime::currentDateTime();
|
||||
auto offset = currentTime.msecsTo(targetTime);
|
||||
this->currentTime = offset > -500 && offset < 500 ? targetTime : currentTime;
|
||||
|
||||
auto time = this->currentTime.time();
|
||||
this->currentTime.setTime(QTime(
|
||||
this->mPrecision >= SystemClock::Hours ? time.hour() : 0,
|
||||
this->mPrecision >= SystemClock::Minutes ? time.minute() : 0,
|
||||
this->mPrecision >= SystemClock::Seconds ? time.second() : 0
|
||||
));
|
||||
|
||||
emit this->dateChanged();
|
||||
}
|
||||
|
||||
void SystemClock::schedule(const QDateTime& targetTime) {
|
||||
auto secondPrecision = this->mPrecision >= SystemClock::Seconds;
|
||||
auto minutePrecision = this->mPrecision >= SystemClock::Minutes;
|
||||
auto hourPrecision = this->mPrecision >= SystemClock::Hours;
|
||||
|
||||
auto currentTime = QDateTime::currentDateTime();
|
||||
|
||||
auto offset = currentTime.msecsTo(targetTime);
|
||||
|
||||
// timer skew
|
||||
auto nextTime = offset > 0 && offset < 500 ? targetTime : currentTime;
|
||||
|
||||
auto baseTimeT = nextTime.time();
|
||||
nextTime.setTime(QTime(
|
||||
hourPrecision ? baseTimeT.hour() : 0,
|
||||
minutePrecision ? baseTimeT.minute() : 0,
|
||||
secondPrecision ? baseTimeT.second() : 0
|
||||
));
|
||||
|
||||
if (secondPrecision) nextTime = nextTime.addSecs(1);
|
||||
else if (minutePrecision) nextTime = nextTime.addSecs(60);
|
||||
else if (hourPrecision) nextTime = nextTime.addSecs(3600);
|
||||
|
||||
auto delay = currentTime.msecsTo(nextTime);
|
||||
|
||||
this->timer.start(static_cast<qint32>(delay));
|
||||
this->targetTime = nextTime;
|
||||
}
|
||||
91
src/core/clock.hpp
Normal file
91
src/core/clock.hpp
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
#pragma once
|
||||
|
||||
#include <qdatetime.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtimer.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
///! System clock accessor.
|
||||
/// SystemClock is a view into the system's clock.
|
||||
/// It updates at hour, minute, or second intervals depending on @@precision.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```qml
|
||||
/// SystemClock {
|
||||
/// id: clock
|
||||
/// precision: SystemClock.Seconds
|
||||
/// }
|
||||
///
|
||||
/// @@QtQuick.Text {
|
||||
/// text: Qt.formatDateTime(clock.date, "hh:mm:ss - yyyy-MM-dd")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// > [!WARNING] Clock updates will trigger within 50ms of the system clock changing,
|
||||
/// > however this can be either before or after the clock changes (+-50ms). If you
|
||||
/// > need a date object, use @@date instead of constructing a new one, or the time
|
||||
/// > of the constructed object could be off by up to a second.
|
||||
class SystemClock: public QObject {
|
||||
Q_OBJECT;
|
||||
/// If the clock should update. Defaults to true.
|
||||
///
|
||||
/// Setting enabled to false pauses the clock.
|
||||
Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged);
|
||||
/// The precision the clock should measure at. Defaults to `SystemClock.Seconds`.
|
||||
Q_PROPERTY(SystemClock::Enum precision READ precision WRITE setPrecision NOTIFY precisionChanged);
|
||||
/// The current date and time.
|
||||
///
|
||||
/// > [!TIP] You can use @@QtQml.Qt.formatDateTime() to get the time as a string in
|
||||
/// > your format of choice.
|
||||
Q_PROPERTY(QDateTime date READ date NOTIFY dateChanged);
|
||||
/// The current hour.
|
||||
Q_PROPERTY(quint32 hours READ hours NOTIFY dateChanged);
|
||||
/// The current minute, or 0 if @@precision is `SystemClock.Hours`.
|
||||
Q_PROPERTY(quint32 minutes READ minutes NOTIFY dateChanged);
|
||||
/// The current second, or 0 if @@precision is `SystemClock.Hours` or `SystemClock.Minutes`.
|
||||
Q_PROPERTY(quint32 seconds READ seconds NOTIFY dateChanged);
|
||||
QML_ELEMENT;
|
||||
|
||||
public:
|
||||
// must be named enum until docgen is ready to handle member enums better
|
||||
enum Enum : quint8 {
|
||||
Hours = 1,
|
||||
Minutes = 2,
|
||||
Seconds = 3,
|
||||
};
|
||||
Q_ENUM(Enum);
|
||||
|
||||
explicit SystemClock(QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] bool enabled() const;
|
||||
void setEnabled(bool enabled);
|
||||
|
||||
[[nodiscard]] SystemClock::Enum precision() const;
|
||||
void setPrecision(SystemClock::Enum precision);
|
||||
|
||||
[[nodiscard]] QDateTime date() const { return this->currentTime; }
|
||||
[[nodiscard]] quint32 hours() const { return this->currentTime.time().hour(); }
|
||||
[[nodiscard]] quint32 minutes() const { return this->currentTime.time().minute(); }
|
||||
[[nodiscard]] quint32 seconds() const { return this->currentTime.time().second(); }
|
||||
|
||||
signals:
|
||||
void enabledChanged();
|
||||
void precisionChanged();
|
||||
void dateChanged();
|
||||
|
||||
private slots:
|
||||
void onTimeout();
|
||||
|
||||
private:
|
||||
bool mEnabled = true;
|
||||
SystemClock::Enum mPrecision = SystemClock::Seconds;
|
||||
QTimer timer;
|
||||
QDateTime currentTime;
|
||||
QDateTime targetTime;
|
||||
|
||||
void update();
|
||||
void setTime(const QDateTime& targetTime);
|
||||
void schedule(const QDateTime& targetTime);
|
||||
};
|
||||
242
src/core/colorquantizer.cpp
Normal file
242
src/core/colorquantizer.cpp
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
#include "colorquantizer.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
#include <qatomic.h>
|
||||
#include <qcolor.h>
|
||||
#include <qdatetime.h>
|
||||
#include <qimage.h>
|
||||
#include <qlist.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qminmax.h>
|
||||
#include <qnamespace.h>
|
||||
#include <qnumeric.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmllist.h>
|
||||
#include <qrgb.h>
|
||||
#include <qthreadpool.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "logcat.hpp"
|
||||
|
||||
namespace {
|
||||
QS_LOGGING_CATEGORY(logColorQuantizer, "quickshell.colorquantizer", QtWarningMsg);
|
||||
}
|
||||
|
||||
ColorQuantizerOperation::ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize)
|
||||
: source(source)
|
||||
, maxDepth(depth)
|
||||
, rescaleSize(rescaleSize) {
|
||||
setAutoDelete(false);
|
||||
}
|
||||
|
||||
void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCancel) {
|
||||
if (shouldCancel.loadAcquire() || source->isEmpty()) return;
|
||||
|
||||
colors.clear();
|
||||
|
||||
auto image = QImage(source->toLocalFile());
|
||||
if ((image.width() > rescaleSize || image.height() > rescaleSize) && rescaleSize > 0) {
|
||||
image = image.scaled(
|
||||
static_cast<int>(rescaleSize),
|
||||
static_cast<int>(rescaleSize),
|
||||
Qt::KeepAspectRatio,
|
||||
Qt::SmoothTransformation
|
||||
);
|
||||
}
|
||||
|
||||
if (image.isNull()) {
|
||||
qCWarning(logColorQuantizer) << "Failed to load image from" << source->toString();
|
||||
return;
|
||||
}
|
||||
|
||||
QList<QColor> pixels;
|
||||
for (int y = 0; y != image.height(); ++y) {
|
||||
for (int x = 0; x != image.width(); ++x) {
|
||||
auto pixel = image.pixel(x, y);
|
||||
if (qAlpha(pixel) == 0) continue;
|
||||
|
||||
pixels.append(QColor::fromRgb(pixel));
|
||||
}
|
||||
}
|
||||
|
||||
auto startTime = QDateTime::currentDateTime();
|
||||
|
||||
colors = quantization(pixels, 0);
|
||||
|
||||
auto endTime = QDateTime::currentDateTime();
|
||||
auto milliseconds = startTime.msecsTo(endTime);
|
||||
qCDebug(logColorQuantizer) << "Color Quantization took: " << milliseconds << "ms";
|
||||
}
|
||||
|
||||
QList<QColor> ColorQuantizerOperation::quantization(
|
||||
QList<QColor>& rgbValues,
|
||||
qreal depth,
|
||||
const QAtomicInteger<bool>& shouldCancel
|
||||
) {
|
||||
if (shouldCancel.loadAcquire()) return QList<QColor>();
|
||||
|
||||
if (depth >= maxDepth || rgbValues.isEmpty()) {
|
||||
if (rgbValues.isEmpty()) return QList<QColor>();
|
||||
|
||||
auto totalR = 0;
|
||||
auto totalG = 0;
|
||||
auto totalB = 0;
|
||||
|
||||
for (const auto& color: rgbValues) {
|
||||
if (shouldCancel.loadAcquire()) return QList<QColor>();
|
||||
|
||||
totalR += color.red();
|
||||
totalG += color.green();
|
||||
totalB += color.blue();
|
||||
}
|
||||
|
||||
auto avgColor = QColor(
|
||||
qRound(totalR / static_cast<double>(rgbValues.size())),
|
||||
qRound(totalG / static_cast<double>(rgbValues.size())),
|
||||
qRound(totalB / static_cast<double>(rgbValues.size()))
|
||||
);
|
||||
|
||||
return QList<QColor>() << avgColor;
|
||||
}
|
||||
|
||||
auto dominantChannel = findBiggestColorRange(rgbValues);
|
||||
std::ranges::sort(rgbValues, [dominantChannel](const auto& a, const auto& b) {
|
||||
if (dominantChannel == 'r') return a.red() < b.red();
|
||||
else if (dominantChannel == 'g') return a.green() < b.green();
|
||||
return a.blue() < b.blue();
|
||||
});
|
||||
|
||||
auto mid = rgbValues.size() / 2;
|
||||
|
||||
auto leftHalf = rgbValues.mid(0, mid);
|
||||
auto rightHalf = rgbValues.mid(mid);
|
||||
|
||||
QList<QColor> result;
|
||||
result.append(quantization(leftHalf, depth + 1));
|
||||
result.append(quantization(rightHalf, depth + 1));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
char ColorQuantizerOperation::findBiggestColorRange(const QList<QColor>& rgbValues) {
|
||||
if (rgbValues.isEmpty()) return 'r';
|
||||
|
||||
auto rMin = 255;
|
||||
auto gMin = 255;
|
||||
auto bMin = 255;
|
||||
auto rMax = 0;
|
||||
auto gMax = 0;
|
||||
auto bMax = 0;
|
||||
|
||||
for (const auto& color: rgbValues) {
|
||||
rMin = qMin(rMin, color.red());
|
||||
gMin = qMin(gMin, color.green());
|
||||
bMin = qMin(bMin, color.blue());
|
||||
|
||||
rMax = qMax(rMax, color.red());
|
||||
gMax = qMax(gMax, color.green());
|
||||
bMax = qMax(bMax, color.blue());
|
||||
}
|
||||
|
||||
auto rRange = rMax - rMin;
|
||||
auto gRange = gMax - gMin;
|
||||
auto bRange = bMax - bMin;
|
||||
|
||||
auto biggestRange = qMax(rRange, qMax(gRange, bRange));
|
||||
if (biggestRange == rRange) {
|
||||
return 'r';
|
||||
} else if (biggestRange == gRange) {
|
||||
return 'g';
|
||||
} else {
|
||||
return 'b';
|
||||
}
|
||||
}
|
||||
|
||||
void ColorQuantizerOperation::finishRun() {
|
||||
QMetaObject::invokeMethod(this, &ColorQuantizerOperation::finished, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void ColorQuantizerOperation::finished() {
|
||||
emit this->done(colors);
|
||||
delete this;
|
||||
}
|
||||
|
||||
void ColorQuantizerOperation::run() {
|
||||
if (!this->shouldCancel) {
|
||||
this->quantizeImage();
|
||||
|
||||
if (this->shouldCancel.loadAcquire()) {
|
||||
qCDebug(logColorQuantizer) << "Color quantization" << this << "cancelled";
|
||||
}
|
||||
}
|
||||
|
||||
this->finishRun();
|
||||
}
|
||||
|
||||
void ColorQuantizerOperation::tryCancel() { this->shouldCancel.storeRelease(true); }
|
||||
|
||||
void ColorQuantizer::componentComplete() {
|
||||
componentCompleted = true;
|
||||
if (!mSource.isEmpty()) quantizeAsync();
|
||||
}
|
||||
|
||||
void ColorQuantizer::setSource(const QUrl& source) {
|
||||
if (mSource != source) {
|
||||
mSource = source;
|
||||
emit this->sourceChanged();
|
||||
|
||||
if (this->componentCompleted && !mSource.isEmpty()) quantizeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
void ColorQuantizer::setDepth(qreal depth) {
|
||||
if (mDepth != depth) {
|
||||
mDepth = depth;
|
||||
emit this->depthChanged();
|
||||
|
||||
if (this->componentCompleted) quantizeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
void ColorQuantizer::setRescaleSize(int rescaleSize) {
|
||||
if (mRescaleSize != rescaleSize) {
|
||||
mRescaleSize = rescaleSize;
|
||||
emit this->rescaleSizeChanged();
|
||||
|
||||
if (this->componentCompleted) quantizeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
void ColorQuantizer::operationFinished(const QList<QColor>& result) {
|
||||
bColors = result;
|
||||
this->liveOperation = nullptr;
|
||||
emit this->colorsChanged();
|
||||
}
|
||||
|
||||
void ColorQuantizer::quantizeAsync() {
|
||||
if (this->liveOperation) this->cancelAsync();
|
||||
|
||||
qCDebug(logColorQuantizer) << "Starting color quantization asynchronously";
|
||||
this->liveOperation = new ColorQuantizerOperation(&mSource, mDepth, mRescaleSize);
|
||||
|
||||
QObject::connect(
|
||||
this->liveOperation,
|
||||
&ColorQuantizerOperation::done,
|
||||
this,
|
||||
&ColorQuantizer::operationFinished
|
||||
);
|
||||
|
||||
QThreadPool::globalInstance()->start(this->liveOperation);
|
||||
}
|
||||
|
||||
void ColorQuantizer::cancelAsync() {
|
||||
if (!this->liveOperation) return;
|
||||
|
||||
this->liveOperation->tryCancel();
|
||||
QThreadPool::globalInstance()->waitForDone();
|
||||
|
||||
QObject::disconnect(this->liveOperation, nullptr, this, nullptr);
|
||||
this->liveOperation = nullptr;
|
||||
}
|
||||
128
src/core/colorquantizer.hpp
Normal file
128
src/core/colorquantizer.hpp
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
#pragma once
|
||||
|
||||
#include <qlist.h>
|
||||
#include <qobject.h>
|
||||
#include <qproperty.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qqmlparserstatus.h>
|
||||
#include <qrunnable.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
#include <qurl.h>
|
||||
|
||||
class ColorQuantizerOperation
|
||||
: public QObject
|
||||
, public QRunnable {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
explicit ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize);
|
||||
|
||||
void run() override;
|
||||
void tryCancel();
|
||||
|
||||
signals:
|
||||
void done(QList<QColor> colors);
|
||||
|
||||
private slots:
|
||||
void finished();
|
||||
|
||||
private:
|
||||
static char findBiggestColorRange(const QList<QColor>& rgbValues);
|
||||
|
||||
void quantizeImage(const QAtomicInteger<bool>& shouldCancel = false);
|
||||
|
||||
QList<QColor> quantization(
|
||||
QList<QColor>& rgbValues,
|
||||
qreal depth,
|
||||
const QAtomicInteger<bool>& shouldCancel = false
|
||||
);
|
||||
|
||||
void finishRun();
|
||||
|
||||
QAtomicInteger<bool> shouldCancel = false;
|
||||
QList<QColor> colors;
|
||||
QUrl* source;
|
||||
qreal maxDepth;
|
||||
qreal rescaleSize;
|
||||
};
|
||||
|
||||
///! Color Quantization Utility
|
||||
/// A color quantization utility used for getting prevalent colors in an image, by
|
||||
/// averaging out the image's color data recursively.
|
||||
///
|
||||
/// #### Example
|
||||
/// ```qml
|
||||
/// ColorQuantizer {
|
||||
/// id: colorQuantizer
|
||||
/// source: Qt.resolvedUrl("./yourImage.png")
|
||||
/// depth: 3 // Will produce 8 colors (2³)
|
||||
/// rescaleSize: 64 // Rescale to 64x64 for faster processing
|
||||
/// }
|
||||
/// ```
|
||||
class ColorQuantizer
|
||||
: public QObject
|
||||
, public QQmlParserStatus {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
Q_INTERFACES(QQmlParserStatus);
|
||||
/// Access the colors resulting from the color quantization performed.
|
||||
/// > [!NOTE] The amount of colors returned from the quantization is determined by
|
||||
/// > the property depth, specifically 2ⁿ where n is the depth.
|
||||
Q_PROPERTY(QList<QColor> colors READ default NOTIFY colorsChanged BINDABLE bindableColors);
|
||||
|
||||
/// Path to the image you'd like to run the color quantization on.
|
||||
Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged);
|
||||
|
||||
/// Max depth for the color quantization. Each level of depth represents another
|
||||
/// binary split of the color space
|
||||
Q_PROPERTY(qreal depth READ depth WRITE setDepth NOTIFY depthChanged);
|
||||
|
||||
/// The size to rescale the image to, when rescaleSize is 0 then no scaling will be done.
|
||||
/// > [!NOTE] Results from color quantization doesn't suffer much when rescaling, it's
|
||||
/// > reccommended to rescale, otherwise the quantization process will take much longer.
|
||||
Q_PROPERTY(qreal rescaleSize READ rescaleSize WRITE setRescaleSize NOTIFY rescaleSizeChanged);
|
||||
|
||||
public:
|
||||
explicit ColorQuantizer(QObject* parent = nullptr): QObject(parent) {}
|
||||
|
||||
void componentComplete() override;
|
||||
void classBegin() override {}
|
||||
|
||||
[[nodiscard]] QBindable<QList<QColor>> bindableColors() { return &this->bColors; }
|
||||
|
||||
[[nodiscard]] QUrl source() const { return mSource; }
|
||||
void setSource(const QUrl& source);
|
||||
|
||||
[[nodiscard]] qreal depth() const { return mDepth; }
|
||||
void setDepth(qreal depth);
|
||||
|
||||
[[nodiscard]] qreal rescaleSize() const { return mRescaleSize; }
|
||||
void setRescaleSize(int rescaleSize);
|
||||
|
||||
signals:
|
||||
void colorsChanged();
|
||||
void sourceChanged();
|
||||
void depthChanged();
|
||||
void rescaleSizeChanged();
|
||||
|
||||
public slots:
|
||||
void operationFinished(const QList<QColor>& result);
|
||||
|
||||
private:
|
||||
void quantizeAsync();
|
||||
void cancelAsync();
|
||||
|
||||
bool componentCompleted = false;
|
||||
ColorQuantizerOperation* liveOperation = nullptr;
|
||||
QUrl mSource;
|
||||
qreal mDepth = 0;
|
||||
qreal mRescaleSize = 0;
|
||||
|
||||
Q_OBJECT_BINDABLE_PROPERTY(
|
||||
ColorQuantizer,
|
||||
QList<QColor>,
|
||||
bColors,
|
||||
&ColorQuantizer::colorsChanged
|
||||
);
|
||||
};
|
||||
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();
|
||||
|
||||
} // namespace qs
|
||||
13
src/core/common.hpp
Normal file
13
src/core/common.hpp
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
#include <qdatetime.h>
|
||||
#include <qprocess.h>
|
||||
|
||||
namespace qs {
|
||||
|
||||
struct Common {
|
||||
static const QDateTime LAUNCH_TIME;
|
||||
static inline QProcessEnvironment INITIAL_ENVIRONMENT = {}; // NOLINT
|
||||
};
|
||||
|
||||
} // namespace qs
|
||||
422
src/core/desktopentry.cpp
Normal file
422
src/core/desktopentry.cpp
Normal file
|
|
@ -0,0 +1,422 @@
|
|||
#include "desktopentry.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
#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 <qstringview.h>
|
||||
#include <qtenvironmentvariables.h>
|
||||
#include <ranges>
|
||||
|
||||
#include "../io/processcore.hpp"
|
||||
#include "logcat.hpp"
|
||||
#include "model.hpp"
|
||||
#include "qmlglobal.hpp"
|
||||
|
||||
namespace {
|
||||
QS_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg);
|
||||
}
|
||||
|
||||
struct Locale {
|
||||
explicit Locale() = default;
|
||||
|
||||
explicit Locale(const QString& string) {
|
||||
auto territoryIdx = string.indexOf('_');
|
||||
auto codesetIdx = string.indexOf('.');
|
||||
auto modifierIdx = string.indexOf('@');
|
||||
|
||||
auto parseEnd = string.length();
|
||||
|
||||
if (modifierIdx != -1) {
|
||||
this->modifier = string.sliced(modifierIdx + 1, parseEnd - modifierIdx - 1);
|
||||
parseEnd = modifierIdx;
|
||||
}
|
||||
|
||||
if (codesetIdx != -1) {
|
||||
parseEnd = codesetIdx;
|
||||
}
|
||||
|
||||
if (territoryIdx != -1) {
|
||||
this->territory = string.sliced(territoryIdx + 1, parseEnd - territoryIdx - 1);
|
||||
parseEnd = territoryIdx;
|
||||
}
|
||||
|
||||
this->language = string.sliced(0, parseEnd);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool isValid() const { return !this->language.isEmpty(); }
|
||||
|
||||
[[nodiscard]] int matchScore(const Locale& other) const {
|
||||
if (this->language != other.language) return 0;
|
||||
auto territoryMatches = !this->territory.isEmpty() && this->territory == other.territory;
|
||||
auto modifierMatches = !this->modifier.isEmpty() && this->modifier == other.modifier;
|
||||
|
||||
auto score = 1;
|
||||
if (territoryMatches) score += 2;
|
||||
if (modifierMatches) score += 1;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
static const Locale& system() {
|
||||
static Locale* locale = nullptr; // NOLINT
|
||||
|
||||
if (locale == nullptr) {
|
||||
auto lstr = qEnvironmentVariable("LC_MESSAGES");
|
||||
if (lstr.isEmpty()) lstr = qEnvironmentVariable("LANG");
|
||||
locale = new Locale(lstr);
|
||||
}
|
||||
|
||||
return *locale;
|
||||
}
|
||||
|
||||
QString language;
|
||||
QString territory;
|
||||
QString modifier;
|
||||
};
|
||||
|
||||
// NOLINTNEXTLINE(misc-use-internal-linkage)
|
||||
QDebug operator<<(QDebug debug, const Locale& locale) {
|
||||
auto saver = QDebugStateSaver(debug);
|
||||
debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory
|
||||
<< ", modifier" << locale.modifier << ')';
|
||||
|
||||
return debug;
|
||||
}
|
||||
|
||||
void DesktopEntry::parseEntry(const QString& text) {
|
||||
const auto& system = Locale::system();
|
||||
|
||||
auto groupName = QString();
|
||||
auto entries = QHash<QString, QPair<Locale, QString>>();
|
||||
|
||||
auto finishCategory = [this, &groupName, &entries]() {
|
||||
if (groupName == "Desktop Entry") {
|
||||
if (entries["Type"].second != "Application") return;
|
||||
if (entries.contains("Hidden") && entries["Hidden"].second == "true") return;
|
||||
|
||||
for (const auto& [key, pair]: entries.asKeyValueRange()) {
|
||||
auto& [_, value] = pair;
|
||||
this->mEntries.insert(key, value);
|
||||
|
||||
if (key == "Name") this->mName = value;
|
||||
else if (key == "GenericName") this->mGenericName = value;
|
||||
else if (key == "StartupWMClass") this->mStartupClass = 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;
|
||||
this->mCommand = DesktopEntry::parseExecString(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;
|
||||
action->mCommand = DesktopEntry::parseExecString(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->mCommand, this->mWorkingDirectory);
|
||||
}
|
||||
|
||||
bool DesktopEntry::isValid() const { return !this->mName.isEmpty(); }
|
||||
bool DesktopEntry::noDisplay() const { return this->mNoDisplay; }
|
||||
|
||||
QVector<DesktopAction*> DesktopEntry::actions() const { return this->mActions.values(); }
|
||||
|
||||
QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
|
||||
QVector<QString> arguments;
|
||||
QString currentArgument;
|
||||
auto parsingString = false;
|
||||
auto escape = 0;
|
||||
auto percent = false;
|
||||
|
||||
for (auto c: execString) {
|
||||
if (escape == 0 && c == u'\\') {
|
||||
escape = 1;
|
||||
} else if (parsingString) {
|
||||
if (c == '\\') {
|
||||
escape++;
|
||||
if (escape == 4) {
|
||||
currentArgument += '\\';
|
||||
escape = 0;
|
||||
}
|
||||
} else if (escape != 0) {
|
||||
if (escape != 2) {
|
||||
// Technically this is an illegal state, but the spec has a terrible double escape
|
||||
// rule in strings for no discernable reason. Assuming someone might understandably
|
||||
// misunderstand it, treat it as a normal escape and log it.
|
||||
qCWarning(logDesktopEntry).noquote()
|
||||
<< "Illegal escape sequence in desktop entry exec string:" << execString;
|
||||
}
|
||||
|
||||
currentArgument += c;
|
||||
escape = 0;
|
||||
} else if (c == u'"' || c == u'\'') {
|
||||
parsingString = false;
|
||||
} else {
|
||||
currentArgument += c;
|
||||
}
|
||||
} else if (escape != 0) {
|
||||
currentArgument += c;
|
||||
escape = 0;
|
||||
} else if (percent) {
|
||||
if (c == '%') {
|
||||
currentArgument += '%';
|
||||
} // else discard
|
||||
|
||||
percent = false;
|
||||
} else if (c == '%') {
|
||||
percent = true;
|
||||
} else if (c == u'"' || c == u'\'') {
|
||||
parsingString = true;
|
||||
} else if (c == u' ') {
|
||||
if (!currentArgument.isEmpty()) {
|
||||
arguments.push_back(currentArgument);
|
||||
currentArgument.clear();
|
||||
}
|
||||
} else {
|
||||
currentArgument += c;
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentArgument.isEmpty()) {
|
||||
arguments.push_back(currentArgument);
|
||||
currentArgument.clear();
|
||||
}
|
||||
|
||||
return arguments;
|
||||
}
|
||||
|
||||
void DesktopEntry::doExec(const QList<QString>& execString, const QString& workingDirectory) {
|
||||
qs::io::process::ProcessContext ctx;
|
||||
ctx.setCommand(execString);
|
||||
ctx.setWorkingDirectory(workingDirectory);
|
||||
QuickshellGlobal::execDetached(ctx);
|
||||
}
|
||||
|
||||
void DesktopAction::execute() const {
|
||||
DesktopEntry::doExec(this->mCommand, this->entry->mWorkingDirectory);
|
||||
}
|
||||
|
||||
DesktopEntryManager::DesktopEntryManager() {
|
||||
this->scanDesktopEntries();
|
||||
this->populateApplications();
|
||||
}
|
||||
|
||||
void DesktopEntryManager::scanDesktopEntries() {
|
||||
QList<QString> dataPaths;
|
||||
|
||||
if (qEnvironmentVariableIsSet("XDG_DATA_HOME")) {
|
||||
dataPaths.push_back(qEnvironmentVariable("XDG_DATA_HOME"));
|
||||
} else if (qEnvironmentVariableIsSet("HOME")) {
|
||||
dataPaths.push_back(qEnvironmentVariable("HOME") + "/.local/share");
|
||||
}
|
||||
|
||||
if (qEnvironmentVariableIsSet("XDG_DATA_DIRS")) {
|
||||
auto var = qEnvironmentVariable("XDG_DATA_DIRS");
|
||||
dataPaths += var.split(u':', Qt::SkipEmptyParts);
|
||||
} else {
|
||||
dataPaths.push_back("/usr/local/share");
|
||||
dataPaths.push_back("/usr/share");
|
||||
}
|
||||
|
||||
qCDebug(logDesktopEntry) << "Creating desktop entry scanners";
|
||||
|
||||
for (auto& path: std::ranges::reverse_view(dataPaths)) {
|
||||
auto p = QDir(path).filePath("applications");
|
||||
auto file = QFileInfo(p);
|
||||
|
||||
if (!file.isDir()) {
|
||||
qCDebug(logDesktopEntry) << "Not scanning path" << p << "as it is not a directory";
|
||||
continue;
|
||||
}
|
||||
|
||||
qCDebug(logDesktopEntry) << "Scanning path" << p;
|
||||
this->scanPath(p);
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopEntryManager::populateApplications() {
|
||||
for (auto& entry: this->desktopEntries.values()) {
|
||||
if (!entry->noDisplay()) this->mApplications.insertObject(entry);
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) {
|
||||
auto entries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
|
||||
|
||||
for (auto& entry: entries) {
|
||||
if (entry.isDir()) this->scanPath(entry.absoluteFilePath(), prefix + dir.dirName() + "-");
|
||||
else if (entry.isFile()) {
|
||||
auto path = entry.filePath();
|
||||
if (!path.endsWith(".desktop")) {
|
||||
qCDebug(logDesktopEntry) << "Skipping file" << path << "as it has no .desktop extension";
|
||||
continue;
|
||||
}
|
||||
|
||||
auto file = QFile(path);
|
||||
if (!file.open(QFile::ReadOnly)) {
|
||||
qCDebug(logDesktopEntry) << "Could not open file" << path;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8);
|
||||
auto lowerId = id.toLower();
|
||||
|
||||
auto text = QString::fromUtf8(file.readAll());
|
||||
auto* dentry = new DesktopEntry(id, this);
|
||||
dentry->parseEntry(text);
|
||||
|
||||
if (!dentry->isValid()) {
|
||||
qCDebug(logDesktopEntry) << "Skipping desktop entry" << path;
|
||||
delete dentry;
|
||||
continue;
|
||||
}
|
||||
|
||||
qCDebug(logDesktopEntry) << "Found desktop entry" << id << "at" << path;
|
||||
|
||||
auto conflictingId = this->desktopEntries.contains(id);
|
||||
|
||||
if (conflictingId) {
|
||||
qCDebug(logDesktopEntry) << "Replacing old entry for" << id;
|
||||
delete this->desktopEntries.value(id);
|
||||
this->desktopEntries.remove(id);
|
||||
this->lowercaseDesktopEntries.remove(lowerId);
|
||||
}
|
||||
|
||||
this->desktopEntries.insert(id, dentry);
|
||||
|
||||
if (this->lowercaseDesktopEntries.contains(lowerId)) {
|
||||
qCInfo(logDesktopEntry).nospace()
|
||||
<< "Multiple desktop entries have the same lowercased id " << lowerId
|
||||
<< ". This can cause ambiguity when byId requests are not made with the correct case "
|
||||
"already.";
|
||||
|
||||
this->lowercaseDesktopEntries.remove(lowerId);
|
||||
}
|
||||
|
||||
this->lowercaseDesktopEntries.insert(lowerId, dentry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DesktopEntryManager* DesktopEntryManager::instance() {
|
||||
static auto* instance = new DesktopEntryManager(); // NOLINT
|
||||
return instance;
|
||||
}
|
||||
|
||||
DesktopEntry* DesktopEntryManager::byId(const QString& id) {
|
||||
if (auto* entry = this->desktopEntries.value(id)) {
|
||||
return entry;
|
||||
} else if (auto* entry = this->lowercaseDesktopEntries.value(id.toLower())) {
|
||||
return entry;
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
DesktopEntry* DesktopEntryManager::heuristicLookup(const QString& name) {
|
||||
if (auto* entry = this->byId(name)) return entry;
|
||||
|
||||
auto list = this->desktopEntries.values();
|
||||
|
||||
auto iter = std::ranges::find_if(list, [&](const DesktopEntry* entry) {
|
||||
return name == entry->mStartupClass;
|
||||
});
|
||||
|
||||
if (iter != list.end()) return *iter;
|
||||
|
||||
iter = std::ranges::find_if(list, [&](const DesktopEntry* entry) {
|
||||
return name.toLower() == entry->mStartupClass.toLower();
|
||||
});
|
||||
|
||||
if (iter != list.end()) return *iter;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ObjectModel<DesktopEntry>* DesktopEntryManager::applications() { return &this->mApplications; }
|
||||
|
||||
DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); }
|
||||
|
||||
DesktopEntry* DesktopEntries::byId(const QString& id) {
|
||||
return DesktopEntryManager::instance()->byId(id);
|
||||
}
|
||||
|
||||
DesktopEntry* DesktopEntries::heuristicLookup(const QString& name) {
|
||||
return DesktopEntryManager::instance()->heuristicLookup(name);
|
||||
}
|
||||
|
||||
ObjectModel<DesktopEntry>* DesktopEntries::applications() {
|
||||
return DesktopEntryManager::instance()->applications();
|
||||
}
|
||||
204
src/core/desktopentry.hpp
Normal file
204
src/core/desktopentry.hpp
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
#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);
|
||||
/// Initial class or app id the app intends to use. May be useful for matching running apps
|
||||
/// to desktop entries.
|
||||
Q_PROPERTY(QString startupClass MEMBER mStartupClass 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.
|
||||
///
|
||||
/// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run.
|
||||
Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
|
||||
/// The parsed `Exec` command in the desktop entry.
|
||||
///
|
||||
/// The entry can be run with @@execute(), or by using this command in
|
||||
/// @@Quickshell.Quickshell.execDetached() or @@Quickshell.Io.Process.
|
||||
/// If used in `execDetached` or a `Process`, @@workingDirectory should also be passed to
|
||||
/// the invoked process. See @@execute() for details.
|
||||
///
|
||||
/// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true.
|
||||
Q_PROPERTY(QVector<QString> command MEMBER mCommand 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.
|
||||
///
|
||||
/// This is equivalent to calling @@Quickshell.Quickshell.execDetached() with @@command
|
||||
/// and @@DesktopEntry.workingDirectory as shown below:
|
||||
///
|
||||
/// ```qml
|
||||
/// Quickshell.execDetached({
|
||||
/// command: desktopEntry.command,
|
||||
/// workingDirectory: desktopEntry.workingDirectory,
|
||||
/// });
|
||||
/// ```
|
||||
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 QList<QString>& execString, const QString& workingDirectory);
|
||||
|
||||
public:
|
||||
QString mId;
|
||||
QString mName;
|
||||
QString mGenericName;
|
||||
QString mStartupClass;
|
||||
bool mNoDisplay = false;
|
||||
QString mComment;
|
||||
QString mIcon;
|
||||
QString mExecString;
|
||||
QVector<QString> mCommand;
|
||||
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 action.
|
||||
///
|
||||
/// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run.
|
||||
Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
|
||||
/// The parsed `Exec` command in the action.
|
||||
///
|
||||
/// The entry can be run with @@execute(), or by using this command in
|
||||
/// @@Quickshell.Quickshell.execDetached() or @@Quickshell.Io.Process.
|
||||
/// If used in `execDetached` or a `Process`, @@DesktopEntry.workingDirectory should also be passed to
|
||||
/// the invoked process.
|
||||
///
|
||||
/// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true.
|
||||
Q_PROPERTY(QVector<QString> command MEMBER mCommand 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.
|
||||
///
|
||||
/// This is equivalent to calling @@Quickshell.Quickshell.execDetached() with @@command
|
||||
/// and @@DesktopEntry.workingDirectory.
|
||||
Q_INVOKABLE void execute() const;
|
||||
|
||||
private:
|
||||
DesktopEntry* entry;
|
||||
QString mId;
|
||||
QString mName;
|
||||
QString mIcon;
|
||||
QString mExecString;
|
||||
QVector<QString> mCommand;
|
||||
QHash<QString, QString> mEntries;
|
||||
|
||||
friend class DesktopEntry;
|
||||
};
|
||||
|
||||
class DesktopEntryManager: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
void scanDesktopEntries();
|
||||
|
||||
[[nodiscard]] DesktopEntry* byId(const QString& id);
|
||||
[[nodiscard]] DesktopEntry* heuristicLookup(const QString& name);
|
||||
|
||||
[[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.
|
||||
///
|
||||
/// While this function requires an exact match, @@heuristicLookup() will correctly
|
||||
/// find an entry more often and is generally more useful.
|
||||
Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id);
|
||||
/// Look up a desktop entry by name using heuristics. Unlike @@byId(),
|
||||
/// if no exact matches are found this function will try to guess - potentially incorrectly.
|
||||
/// May return null.
|
||||
Q_INVOKABLE [[nodiscard]] static DesktopEntry* heuristicLookup(const QString& name);
|
||||
|
||||
[[nodiscard]] static ObjectModel<DesktopEntry>* applications();
|
||||
};
|
||||
|
|
@ -9,3 +9,15 @@
|
|||
// make the type visible in the docs even if not a QML_ELEMENT
|
||||
#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)
|
||||
|
|
|
|||
33
src/core/easingcurve.cpp
Normal file
33
src/core/easingcurve.cpp
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#include "easingcurve.hpp"
|
||||
#include <utility>
|
||||
|
||||
#include <qeasingcurve.h>
|
||||
#include <qpoint.h>
|
||||
#include <qrect.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
qreal EasingCurve::valueAt(qreal x) const { return this->mCurve.valueForProgress(x); }
|
||||
|
||||
qreal EasingCurve::interpolate(qreal x, qreal a, qreal b) const {
|
||||
return a + (b - a) * this->valueAt(x);
|
||||
}
|
||||
|
||||
QPointF EasingCurve::interpolate(qreal x, const QPointF& a, const QPointF& b) const {
|
||||
return QPointF(this->interpolate(x, a.x(), b.x()), this->interpolate(x, a.y(), b.y()));
|
||||
}
|
||||
|
||||
QRectF EasingCurve::interpolate(qreal x, const QRectF& a, const QRectF& b) const {
|
||||
return QRectF(
|
||||
this->interpolate(x, a.topLeft(), b.topLeft()),
|
||||
this->interpolate(x, a.bottomRight(), b.bottomRight())
|
||||
);
|
||||
}
|
||||
|
||||
QEasingCurve EasingCurve::curve() const { return this->mCurve; }
|
||||
|
||||
void EasingCurve::setCurve(QEasingCurve curve) {
|
||||
if (this->mCurve == curve) return;
|
||||
this->mCurve = std::move(curve);
|
||||
emit this->curveChanged();
|
||||
}
|
||||
40
src/core/easingcurve.hpp
Normal file
40
src/core/easingcurve.hpp
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
#pragma once
|
||||
|
||||
#include <qeasingcurve.h>
|
||||
#include <qobject.h>
|
||||
#include <qpoint.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qrect.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
///! Easing curve.
|
||||
/// Directly accessible easing curve as used in property animations.
|
||||
class EasingCurve: public QObject {
|
||||
Q_OBJECT;
|
||||
/// Easing curve settings. Works exactly the same as
|
||||
/// [PropertyAnimation.easing](https://doc.qt.io/qt-6/qml-qtquick-propertyanimation.html#easing-prop).
|
||||
Q_PROPERTY(QEasingCurve curve READ curve WRITE setCurve NOTIFY curveChanged);
|
||||
QML_ELEMENT;
|
||||
|
||||
public:
|
||||
EasingCurve(QObject* parent = nullptr): QObject(parent) {}
|
||||
|
||||
/// Returns the Y value for the given X value on the curve
|
||||
/// from 0.0 to 1.0.
|
||||
Q_INVOKABLE [[nodiscard]] qreal valueAt(qreal x) const;
|
||||
/// Interpolates between two values using the given X coordinate.
|
||||
Q_INVOKABLE [[nodiscard]] qreal interpolate(qreal x, qreal a, qreal b) const;
|
||||
/// Interpolates between two points using the given X coordinate.
|
||||
Q_INVOKABLE [[nodiscard]] QPointF interpolate(qreal x, const QPointF& a, const QPointF& b) const;
|
||||
/// Interpolates two rects using the given X coordinate.
|
||||
Q_INVOKABLE [[nodiscard]] QRectF interpolate(qreal x, const QRectF& a, const QRectF& b) const;
|
||||
|
||||
[[nodiscard]] QEasingCurve curve() const;
|
||||
void setCurve(QEasingCurve curve);
|
||||
|
||||
signals:
|
||||
void curveChanged();
|
||||
|
||||
private:
|
||||
QEasingCurve mCurve;
|
||||
};
|
||||
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;
|
||||
};
|
||||
16
src/core/enginecontext.hpp
Normal file
16
src/core/enginecontext.hpp
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#pragma once
|
||||
|
||||
#include "qsintercept.hpp"
|
||||
#include "scan.hpp"
|
||||
#include "singleton.hpp"
|
||||
|
||||
class EngineContext {
|
||||
public:
|
||||
explicit EngineContext(const QmlScanner& scanner);
|
||||
|
||||
private:
|
||||
const QmlScanner& scanner;
|
||||
QQmlEngine engine;
|
||||
QsInterceptNetworkAccessManagerFactory interceptFactory;
|
||||
SingletonRegistry singletonRegistry;
|
||||
};
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
#include "floatingwindow.hpp"
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qqmllist.h>
|
||||
#include <qquickitem.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "proxywindow.hpp"
|
||||
#include "windowinterface.hpp"
|
||||
|
||||
void ProxyFloatingWindow::setWidth(qint32 width) {
|
||||
if (this->window == nullptr || !this->window->isVisible()) {
|
||||
this->ProxyWindowBase::setWidth(width);
|
||||
}
|
||||
}
|
||||
|
||||
void ProxyFloatingWindow::setHeight(qint32 height) {
|
||||
if (this->window == nullptr || !this->window->isVisible()) {
|
||||
this->ProxyWindowBase::setHeight(height);
|
||||
}
|
||||
}
|
||||
|
||||
// FloatingWindowInterface
|
||||
|
||||
FloatingWindowInterface::FloatingWindowInterface(QObject* parent)
|
||||
: WindowInterface(parent)
|
||||
, window(new ProxyFloatingWindow(this)) {
|
||||
// clang-format off
|
||||
QObject::connect(this->window, &ProxyWindowBase::windowConnected, this, &FloatingWindowInterface::windowConnected);
|
||||
QObject::connect(this->window, &ProxyWindowBase::visibleChanged, this, &FloatingWindowInterface::visibleChanged);
|
||||
QObject::connect(this->window, &ProxyWindowBase::heightChanged, this, &FloatingWindowInterface::heightChanged);
|
||||
QObject::connect(this->window, &ProxyWindowBase::widthChanged, this, &FloatingWindowInterface::widthChanged);
|
||||
QObject::connect(this->window, &ProxyWindowBase::screenChanged, this, &FloatingWindowInterface::screenChanged);
|
||||
QObject::connect(this->window, &ProxyWindowBase::colorChanged, this, &FloatingWindowInterface::colorChanged);
|
||||
QObject::connect(this->window, &ProxyWindowBase::maskChanged, this, &FloatingWindowInterface::maskChanged);
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
void FloatingWindowInterface::onReload(QObject* oldInstance) {
|
||||
auto* old = qobject_cast<FloatingWindowInterface*>(oldInstance);
|
||||
this->window->onReload(old != nullptr ? old->window : nullptr);
|
||||
}
|
||||
|
||||
QQmlListProperty<QObject> FloatingWindowInterface::data() { return this->window->data(); }
|
||||
QQuickItem* FloatingWindowInterface::contentItem() const { return this->window->contentItem(); }
|
||||
|
||||
// NOLINTBEGIN
|
||||
#define proxyPair(type, get, set) \
|
||||
type FloatingWindowInterface::get() const { return this->window->get(); } \
|
||||
void FloatingWindowInterface::set(type value) { this->window->set(value); }
|
||||
|
||||
proxyPair(bool, isVisible, setVisible);
|
||||
proxyPair(qint32, width, setWidth);
|
||||
proxyPair(qint32, height, setHeight);
|
||||
proxyPair(QuickshellScreenInfo*, screen, setScreen);
|
||||
proxyPair(QColor, color, setColor);
|
||||
proxyPair(PendingRegion*, mask, setMask);
|
||||
|
||||
#undef proxyPair
|
||||
#undef proxySet
|
||||
#undef proxyGet
|
||||
// NOLINTEND
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "proxywindow.hpp"
|
||||
|
||||
class ProxyFloatingWindow: public ProxyWindowBase {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
explicit ProxyFloatingWindow(QObject* parent = nullptr): ProxyWindowBase(parent) {}
|
||||
|
||||
// Setting geometry while the window is visible makes the content item shrinks but not the window
|
||||
// which is awful so we disable it for floating windows.
|
||||
void setWidth(qint32 width) override;
|
||||
void setHeight(qint32 height) override;
|
||||
};
|
||||
|
||||
///! Standard floating window.
|
||||
class FloatingWindowInterface: public WindowInterface {
|
||||
Q_OBJECT;
|
||||
QML_NAMED_ELEMENT(FloatingWindow);
|
||||
|
||||
public:
|
||||
explicit FloatingWindowInterface(QObject* parent = nullptr);
|
||||
|
||||
void onReload(QObject* oldInstance) override;
|
||||
|
||||
[[nodiscard]] QQuickItem* contentItem() const override;
|
||||
|
||||
// NOLINTBEGIN
|
||||
[[nodiscard]] bool isVisible() const override;
|
||||
void setVisible(bool visible) override;
|
||||
|
||||
[[nodiscard]] qint32 width() const override;
|
||||
void setWidth(qint32 width) override;
|
||||
|
||||
[[nodiscard]] qint32 height() const override;
|
||||
void setHeight(qint32 height) override;
|
||||
|
||||
[[nodiscard]] QuickshellScreenInfo* screen() const override;
|
||||
void setScreen(QuickshellScreenInfo* screen) override;
|
||||
|
||||
[[nodiscard]] QColor color() const override;
|
||||
void setColor(QColor color) override;
|
||||
|
||||
[[nodiscard]] PendingRegion* mask() const override;
|
||||
void setMask(PendingRegion* mask) override;
|
||||
|
||||
[[nodiscard]] QQmlListProperty<QObject> data() override;
|
||||
// NOLINTEND
|
||||
|
||||
private:
|
||||
ProxyFloatingWindow* window;
|
||||
};
|
||||
413
src/core/generation.cpp
Normal file
413
src/core/generation.cpp
Normal file
|
|
@ -0,0 +1,413 @@
|
|||
#include "generation.hpp"
|
||||
#include <utility>
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qcoreapplication.h>
|
||||
#include <qdebug.h>
|
||||
#include <qdir.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qfilesystemwatcher.h>
|
||||
#include <qhash.h>
|
||||
#include <qlist.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qnamespace.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlcontext.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qqmlerror.h>
|
||||
#include <qqmlincubator.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "iconimageprovider.hpp"
|
||||
#include "imageprovider.hpp"
|
||||
#include "incubator.hpp"
|
||||
#include "logcat.hpp"
|
||||
#include "plugin.hpp"
|
||||
#include "qsintercept.hpp"
|
||||
#include "reload.hpp"
|
||||
#include "scan.hpp"
|
||||
|
||||
namespace {
|
||||
QS_LOGGING_CATEGORY(logScene, "scene");
|
||||
}
|
||||
|
||||
static QHash<const QQmlEngine*, EngineGeneration*> g_generations; // NOLINT
|
||||
|
||||
EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
|
||||
: rootPath(rootPath)
|
||||
, scanner(std::move(scanner))
|
||||
, urlInterceptor(this->rootPath)
|
||||
, interceptNetFactory(this->rootPath, this->scanner.fileIntercepts)
|
||||
, engine(new QQmlEngine()) {
|
||||
g_generations.insert(this->engine, this);
|
||||
|
||||
this->engine->setOutputWarningsToStandardError(false);
|
||||
QObject::connect(this->engine, &QQmlEngine::warnings, this, &EngineGeneration::onEngineWarnings);
|
||||
|
||||
this->engine->addUrlInterceptor(&this->urlInterceptor);
|
||||
this->engine->addImportPath("qs:@/");
|
||||
|
||||
this->engine->setNetworkAccessManagerFactory(&this->interceptNetFactory);
|
||||
this->engine->setIncubationController(&this->delayedIncubationController);
|
||||
|
||||
this->engine->addImageProvider("icon", new IconImageProvider());
|
||||
this->engine->addImageProvider("qsimage", new QsImageProvider());
|
||||
this->engine->addImageProvider("qspixmap", new QsPixmapProvider());
|
||||
|
||||
QsEnginePlugin::runConstructGeneration(*this);
|
||||
}
|
||||
|
||||
EngineGeneration::EngineGeneration(): EngineGeneration(QDir(), QmlScanner()) {}
|
||||
|
||||
EngineGeneration::~EngineGeneration() {
|
||||
if (this->engine != nullptr) {
|
||||
qFatal() << this << "destroyed without calling destroy()";
|
||||
}
|
||||
}
|
||||
|
||||
void EngineGeneration::destroy() {
|
||||
if (this->destroying) return;
|
||||
this->destroying = true;
|
||||
|
||||
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]() {
|
||||
// prevent further js execution between garbage collection and engine destruction.
|
||||
this->engine->setInterrupted(true);
|
||||
|
||||
g_generations.remove(this->engine);
|
||||
|
||||
// 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
|
||||
qCDebug(logIncubator) << "Locking incubation controllers of old generation" << old;
|
||||
old->incubationControllersLocked = true;
|
||||
old->assignIncubationController();
|
||||
}
|
||||
|
||||
QObject::connect(this->engine, &QQmlEngine::quit, this, &EngineGeneration::quit);
|
||||
QObject::connect(this->engine, &QQmlEngine::exit, this, &EngineGeneration::exit);
|
||||
|
||||
if (auto* reloadable = qobject_cast<Reloadable*>(this->root)) {
|
||||
reloadable->reload(old ? old->root : nullptr);
|
||||
}
|
||||
|
||||
this->singletonRegistry.onReload(old == nullptr ? nullptr : &old->singletonRegistry);
|
||||
this->reloadComplete = true;
|
||||
emit this->reloadFinished();
|
||||
|
||||
if (old != nullptr) {
|
||||
QObject::connect(old, &QObject::destroyed, this, [this]() { this->postReload(); });
|
||||
old->destroy();
|
||||
} else {
|
||||
this->postReload();
|
||||
}
|
||||
}
|
||||
|
||||
void EngineGeneration::postReload() {
|
||||
// This can be called on a generation during its destruction.
|
||||
if (this->engine == nullptr || this->root == nullptr) return;
|
||||
|
||||
QsEnginePlugin::runOnReload();
|
||||
|
||||
emit this->firePostReload();
|
||||
QObject::disconnect(this, &EngineGeneration::firePostReload, nullptr, nullptr);
|
||||
}
|
||||
|
||||
void EngineGeneration::setWatchingFiles(bool watching) {
|
||||
if (watching) {
|
||||
if (this->watcher == nullptr) {
|
||||
this->watcher = new QFileSystemWatcher();
|
||||
|
||||
for (auto& file: this->scanner.scannedFiles) {
|
||||
this->watcher->addPath(file);
|
||||
this->watcher->addPath(QFileInfo(file).dir().absolutePath());
|
||||
}
|
||||
|
||||
for (auto& file: this->extraWatchedFiles) {
|
||||
this->watcher->addPath(file);
|
||||
this->watcher->addPath(QFileInfo(file).dir().absolutePath());
|
||||
}
|
||||
|
||||
QObject::connect(
|
||||
this->watcher,
|
||||
&QFileSystemWatcher::fileChanged,
|
||||
this,
|
||||
&EngineGeneration::onFileChanged
|
||||
);
|
||||
|
||||
QObject::connect(
|
||||
this->watcher,
|
||||
&QFileSystemWatcher::directoryChanged,
|
||||
this,
|
||||
&EngineGeneration::onDirectoryChanged
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (this->watcher != nullptr) {
|
||||
this->watcher->deleteLater();
|
||||
this->watcher = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool EngineGeneration::setExtraWatchedFiles(const QVector<QString>& files) {
|
||||
this->extraWatchedFiles.clear();
|
||||
for (const auto& file: files) {
|
||||
if (!this->scanner.scannedFiles.contains(file)) {
|
||||
this->extraWatchedFiles.append(file);
|
||||
}
|
||||
}
|
||||
|
||||
if (this->watcher) {
|
||||
this->setWatchingFiles(false);
|
||||
this->setWatchingFiles(true);
|
||||
}
|
||||
|
||||
return !this->extraWatchedFiles.isEmpty();
|
||||
}
|
||||
|
||||
void EngineGeneration::onFileChanged(const QString& name) {
|
||||
if (!this->watcher->files().contains(name)) {
|
||||
this->deletedWatchedFiles.push_back(name);
|
||||
} else {
|
||||
// some editors (e.g vscode) perform file saving in two steps: truncate + write
|
||||
// ignore the first event (truncate) with size 0 to prevent incorrect live reloading
|
||||
auto fileInfo = QFileInfo(name);
|
||||
if (fileInfo.isFile() && fileInfo.size() == 0) return;
|
||||
|
||||
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.
|
||||
auto* obj = dynamic_cast<QObject*>(controller);
|
||||
if (!obj) {
|
||||
qCWarning(logIncubator) << "Could not register incubation controller as it is not a QObject"
|
||||
<< controller;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
QObject::connect(
|
||||
obj,
|
||||
&QObject::destroyed,
|
||||
this,
|
||||
&EngineGeneration::incubationControllerDestroyed,
|
||||
Qt::UniqueConnection
|
||||
);
|
||||
|
||||
this->incubationControllers.push_back(obj);
|
||||
qCDebug(logIncubator) << "Registered incubation controller" << obj << "to generation" << this;
|
||||
|
||||
// This function can run during destruction.
|
||||
if (this->engine == nullptr) return;
|
||||
|
||||
if (this->engine->incubationController() == &this->delayedIncubationController) {
|
||||
this->assignIncubationController();
|
||||
}
|
||||
}
|
||||
|
||||
// Multiple controllers may be destroyed at once. Dynamic casts must be performed before working
|
||||
// with any controllers. The QQmlIncubationController destructor will already have run by the
|
||||
// point QObject::destroyed is called, so we can't cast to that.
|
||||
void EngineGeneration::deregisterIncubationController(QQmlIncubationController* controller) {
|
||||
auto* obj = dynamic_cast<QObject*>(controller);
|
||||
if (!obj) {
|
||||
qCCritical(logIncubator) << "Deregistering incubation controller which is not a QObject, "
|
||||
"however only QObject controllers should be registered.";
|
||||
}
|
||||
|
||||
QObject::disconnect(obj, nullptr, this, nullptr);
|
||||
|
||||
if (this->incubationControllers.removeOne(obj)) {
|
||||
qCDebug(logIncubator) << "Deregistered incubation controller" << obj << "from" << this;
|
||||
} else {
|
||||
qCCritical(logIncubator) << "Failed to deregister incubation controller" << obj << "from"
|
||||
<< this << "as it was not registered to begin with";
|
||||
qCCritical(logIncubator) << "Current registered incuabation controllers"
|
||||
<< this->incubationControllers;
|
||||
}
|
||||
|
||||
// This function can run during destruction.
|
||||
if (this->engine == nullptr) return;
|
||||
|
||||
if (this->engine->incubationController() == controller) {
|
||||
qCDebug(logIncubator
|
||||
) << "Destroyed incubation controller was currently active, reassigning from pool";
|
||||
this->assignIncubationController();
|
||||
}
|
||||
}
|
||||
|
||||
void EngineGeneration::incubationControllerDestroyed() {
|
||||
auto* sender = this->sender();
|
||||
|
||||
if (this->incubationControllers.removeAll(sender) != 0) {
|
||||
qCDebug(logIncubator) << "Destroyed incubation controller" << sender << "deregistered from"
|
||||
<< this;
|
||||
} else {
|
||||
qCCritical(logIncubator) << "Destroyed incubation controller" << sender
|
||||
<< "was not registered, but its destruction was observed by" << this;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// This function can run during destruction.
|
||||
if (this->engine == nullptr) return;
|
||||
|
||||
if (dynamic_cast<QObject*>(this->engine->incubationController()) == sender) {
|
||||
qCDebug(logIncubator
|
||||
) << "Destroyed incubation controller was currently active, reassigning from pool";
|
||||
this->assignIncubationController();
|
||||
}
|
||||
}
|
||||
|
||||
void EngineGeneration::onEngineWarnings(const QList<QQmlError>& warnings) {
|
||||
for (const auto& error: warnings) {
|
||||
const auto& url = error.url();
|
||||
auto rel = url.scheme() == "qs" && url.path().startsWith("@/qs/") ? "@" % url.path().sliced(5)
|
||||
: url.toString();
|
||||
|
||||
QString objectName;
|
||||
auto desc = error.description();
|
||||
if (auto i = desc.indexOf(": "); i != -1 && desc.startsWith("QML ")) {
|
||||
objectName = desc.first(i) + " at ";
|
||||
desc = desc.sliced(i + 2);
|
||||
}
|
||||
|
||||
qCWarning(logScene).noquote().nospace()
|
||||
<< objectName << rel << '[' << error.line() << ':' << error.column() << "]: " << desc;
|
||||
}
|
||||
}
|
||||
|
||||
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->incubationControllersLocked || this->incubationControllers.isEmpty()) {
|
||||
controller = &this->delayedIncubationController;
|
||||
} else {
|
||||
controller = dynamic_cast<QQmlIncubationController*>(this->incubationControllers.first());
|
||||
}
|
||||
|
||||
qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation"
|
||||
<< this
|
||||
<< "fallback:" << (controller == &this->delayedIncubationController);
|
||||
|
||||
this->engine->setIncubationController(controller);
|
||||
}
|
||||
|
||||
EngineGeneration* EngineGeneration::currentGeneration() {
|
||||
if (g_generations.size() == 1) {
|
||||
return *g_generations.begin();
|
||||
} else return nullptr;
|
||||
}
|
||||
|
||||
EngineGeneration* EngineGeneration::findEngineGeneration(const QQmlEngine* engine) {
|
||||
return g_generations.value(engine);
|
||||
}
|
||||
|
||||
EngineGeneration* EngineGeneration::findObjectGeneration(const QObject* object) {
|
||||
// Objects can still attempt to find their generation after it has been destroyed.
|
||||
// if (g_generations.size() == 1) return EngineGeneration::currentGeneration();
|
||||
|
||||
while (object != nullptr) {
|
||||
auto* context = QQmlEngine::contextForObject(object);
|
||||
|
||||
if (context != nullptr) {
|
||||
if (auto* generation = EngineGeneration::findEngineGeneration(context->engine())) {
|
||||
return generation;
|
||||
}
|
||||
}
|
||||
|
||||
object = object->parent();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
100
src/core/generation.hpp
Normal file
100
src/core/generation.hpp
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
#pragma once
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdir.h>
|
||||
#include <qfilesystemwatcher.h>
|
||||
#include <qhash.h>
|
||||
#include <qlist.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qqmlerror.h>
|
||||
#include <qqmlincubator.h>
|
||||
#include <qtclasshelpermacros.h>
|
||||
|
||||
#include "incubator.hpp"
|
||||
#include "qsintercept.hpp"
|
||||
#include "scan.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();
|
||||
explicit EngineGeneration(const QDir& rootPath, QmlScanner scanner);
|
||||
~EngineGeneration() override;
|
||||
Q_DISABLE_COPY_MOVE(EngineGeneration);
|
||||
|
||||
// assumes root has been initialized, consumes old generation
|
||||
void onReload(EngineGeneration* old);
|
||||
void setWatchingFiles(bool watching);
|
||||
bool setExtraWatchedFiles(const QVector<QString>& files);
|
||||
|
||||
void registerIncubationController(QQmlIncubationController* controller);
|
||||
void deregisterIncubationController(QQmlIncubationController* controller);
|
||||
|
||||
// takes ownership
|
||||
void registerExtension(const void* key, EngineGenerationExt* extension);
|
||||
EngineGenerationExt* findExtension(const void* key);
|
||||
|
||||
static EngineGeneration* findEngineGeneration(const QQmlEngine* engine);
|
||||
static EngineGeneration* findObjectGeneration(const QObject* object);
|
||||
|
||||
// Returns the current generation if there is only one generation,
|
||||
// otherwise null.
|
||||
static EngineGeneration* currentGeneration();
|
||||
|
||||
RootWrapper* wrapper = nullptr;
|
||||
QDir rootPath;
|
||||
QmlScanner scanner;
|
||||
QsUrlInterceptor urlInterceptor;
|
||||
QsInterceptNetworkAccessManagerFactory interceptNetFactory;
|
||||
QQmlEngine* engine = nullptr;
|
||||
QObject* root = nullptr;
|
||||
SingletonRegistry singletonRegistry;
|
||||
QFileSystemWatcher* watcher = nullptr;
|
||||
QVector<QString> deletedWatchedFiles;
|
||||
QVector<QString> extraWatchedFiles;
|
||||
DelayedQmlIncubationController delayedIncubationController;
|
||||
bool reloadComplete = false;
|
||||
QuickshellGlobal* qsgInstance = nullptr;
|
||||
|
||||
void destroy();
|
||||
void shutdown();
|
||||
|
||||
signals:
|
||||
void filesChanged();
|
||||
void reloadFinished();
|
||||
void firePostReload();
|
||||
|
||||
public slots:
|
||||
void quit();
|
||||
void exit(int code);
|
||||
|
||||
private slots:
|
||||
void onFileChanged(const QString& name);
|
||||
void onDirectoryChanged();
|
||||
void incubationControllerDestroyed();
|
||||
static void onEngineWarnings(const QList<QQmlError>& warnings);
|
||||
|
||||
private:
|
||||
void postReload();
|
||||
void assignIncubationController();
|
||||
QVector<QObject*> incubationControllers;
|
||||
bool incubationControllersLocked = false;
|
||||
QHash<const void*, EngineGenerationExt*> extensions;
|
||||
|
||||
bool destroying = false;
|
||||
bool shouldTerminate = false;
|
||||
int exitCode = 0;
|
||||
};
|
||||
84
src/core/iconimageprovider.cpp
Normal file
84
src/core/iconimageprovider.cpp
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
#include "iconimageprovider.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
#include <qcolor.h>
|
||||
#include <qicon.h>
|
||||
#include <qlogging.h>
|
||||
#include <qpainter.h>
|
||||
#include <qpixmap.h>
|
||||
#include <qsize.h>
|
||||
#include <qstring.h>
|
||||
|
||||
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);
|
||||
path = id.sliced(splitIdx + 6);
|
||||
qWarning() << "Searching custom icon paths is not yet supported. Icon path will be ignored for"
|
||||
<< id;
|
||||
} else {
|
||||
splitIdx = id.indexOf("?fallback=");
|
||||
if (splitIdx != -1) {
|
||||
iconName = id.sliced(0, splitIdx);
|
||||
fallbackName = id.sliced(splitIdx + 10);
|
||||
} else {
|
||||
iconName = id;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
auto pixmap = icon.pixmap(targetSize.width(), targetSize.height());
|
||||
|
||||
if (pixmap.isNull()) {
|
||||
qWarning() << "Could not load icon" << id << "at size" << targetSize << "from request";
|
||||
pixmap = IconImageProvider::missingPixmap(targetSize);
|
||||
}
|
||||
|
||||
if (size != nullptr) *size = pixmap.size();
|
||||
return pixmap;
|
||||
}
|
||||
|
||||
QPixmap IconImageProvider::missingPixmap(const QSize& size) {
|
||||
auto width = size.width() % 2 == 0 ? size.width() : size.width() + 1;
|
||||
auto height = size.height() % 2 == 0 ? size.height() : size.height() + 1;
|
||||
width = std::max(width, 2);
|
||||
height = std::max(height, 2);
|
||||
|
||||
auto pixmap = QPixmap(width, height);
|
||||
pixmap.fill(QColorConstants::Black);
|
||||
auto painter = QPainter(&pixmap);
|
||||
|
||||
auto halfWidth = width / 2;
|
||||
auto halfHeight = height / 2;
|
||||
auto purple = QColor(0xd900d8);
|
||||
painter.fillRect(halfWidth, 0, halfWidth, halfHeight, purple);
|
||||
painter.fillRect(0, halfHeight, halfWidth, halfHeight, purple);
|
||||
return pixmap;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
19
src/core/iconimageprovider.hpp
Normal file
19
src/core/iconimageprovider.hpp
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#pragma once
|
||||
|
||||
#include <qpixmap.h>
|
||||
#include <qquickimageprovider.h>
|
||||
|
||||
class IconImageProvider: public QQuickImageProvider {
|
||||
public:
|
||||
explicit IconImageProvider(): QQuickImageProvider(QQuickImageProvider::Pixmap) {}
|
||||
|
||||
QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override;
|
||||
|
||||
static QPixmap missingPixmap(const QSize& size);
|
||||
|
||||
static QString requestString(
|
||||
const QString& icon,
|
||||
const QString& path = QString(),
|
||||
const QString& fallback = QString()
|
||||
);
|
||||
};
|
||||
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);
|
||||
93
src/core/imageprovider.cpp
Normal file
93
src/core/imageprovider.cpp
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
#include "imageprovider.hpp"
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdebug.h>
|
||||
#include <qimage.h>
|
||||
#include <qlogging.h>
|
||||
#include <qmap.h>
|
||||
#include <qobject.h>
|
||||
#include <qpixmap.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
namespace {
|
||||
QMap<QString, QsImageHandle*> liveImages; // NOLINT
|
||||
quint32 handleIndex = 0; // NOLINT
|
||||
} // namespace
|
||||
|
||||
void parseReq(const QString& req, QString& target, QString& param) {
|
||||
auto splitIdx = req.indexOf('/');
|
||||
if (splitIdx != -1) {
|
||||
target = req.sliced(0, splitIdx);
|
||||
param = req.sliced(splitIdx + 1);
|
||||
} else {
|
||||
target = req;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QsImageHandle::QsImageHandle(QQmlImageProviderBase::ImageType type)
|
||||
: type(type)
|
||||
, id(QString::number(++handleIndex)) {
|
||||
liveImages.insert(this->id, this);
|
||||
}
|
||||
|
||||
QsImageHandle::~QsImageHandle() { liveImages.remove(this->id); }
|
||||
|
||||
QString QsImageHandle::url() const {
|
||||
QString url = "image://";
|
||||
if (this->type == QQmlImageProviderBase::Image) url += "qsimage";
|
||||
else if (this->type == QQmlImageProviderBase::Pixmap) url += "qspixmap";
|
||||
url += "/" + this->id;
|
||||
return url;
|
||||
}
|
||||
|
||||
QImage
|
||||
QsImageHandle::requestImage(const QString& /*unused*/, QSize* /*unused*/, const QSize& /*unused*/) {
|
||||
qWarning() << "Image handle" << this << "does not provide QImages";
|
||||
return QImage();
|
||||
}
|
||||
|
||||
QPixmap QsImageHandle::
|
||||
requestPixmap(const QString& /*unused*/, QSize* /*unused*/, const QSize& /*unused*/) {
|
||||
qWarning() << "Image handle" << this << "does not provide QPixmaps";
|
||||
return QPixmap();
|
||||
}
|
||||
|
||||
QImage QsImageProvider::requestImage(const QString& id, QSize* size, const QSize& requestedSize) {
|
||||
QString target;
|
||||
QString param;
|
||||
parseReq(id, target, param);
|
||||
|
||||
auto* handle = liveImages.value(target);
|
||||
if (handle != nullptr) {
|
||||
return handle->requestImage(param, size, requestedSize);
|
||||
} else {
|
||||
qWarning() << "Requested image from unknown handle" << id;
|
||||
return QImage();
|
||||
}
|
||||
}
|
||||
|
||||
QPixmap
|
||||
QsPixmapProvider::requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) {
|
||||
QString target;
|
||||
QString param;
|
||||
parseReq(id, target, param);
|
||||
|
||||
auto* handle = liveImages.value(target);
|
||||
if (handle != nullptr) {
|
||||
return handle->requestPixmap(param, size, requestedSize);
|
||||
} else {
|
||||
qWarning() << "Requested image from unknown handle" << id;
|
||||
return QPixmap();
|
||||
}
|
||||
}
|
||||
|
||||
QString QsIndexedImageHandle::url() const {
|
||||
return this->QsImageHandle::url() % '/' % QString::number(this->changeIndex);
|
||||
}
|
||||
|
||||
void QsIndexedImageHandle::imageChanged() { ++this->changeIndex; }
|
||||
48
src/core/imageprovider.hpp
Normal file
48
src/core/imageprovider.hpp
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#pragma once
|
||||
|
||||
#include <qimage.h>
|
||||
#include <qmap.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qquickimageprovider.h>
|
||||
#include <qtclasshelpermacros.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class QsImageProvider: public QQuickImageProvider {
|
||||
public:
|
||||
explicit QsImageProvider(): QQuickImageProvider(QQuickImageProvider::Image) {}
|
||||
QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override;
|
||||
};
|
||||
|
||||
class QsPixmapProvider: public QQuickImageProvider {
|
||||
public:
|
||||
explicit QsPixmapProvider(): QQuickImageProvider(QQuickImageProvider::Pixmap) {}
|
||||
QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override;
|
||||
};
|
||||
|
||||
class QsImageHandle {
|
||||
public:
|
||||
explicit QsImageHandle(QQmlImageProviderBase::ImageType type);
|
||||
virtual ~QsImageHandle();
|
||||
Q_DISABLE_COPY_MOVE(QsImageHandle);
|
||||
|
||||
[[nodiscard]] virtual QString url() const;
|
||||
|
||||
virtual QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize);
|
||||
virtual QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize);
|
||||
|
||||
private:
|
||||
QQmlImageProviderBase::ImageType type;
|
||||
QString id;
|
||||
};
|
||||
|
||||
class QsIndexedImageHandle: public QsImageHandle {
|
||||
public:
|
||||
explicit QsIndexedImageHandle(QQmlImageProviderBase::ImageType type): QsImageHandle(type) {}
|
||||
|
||||
[[nodiscard]] QString url() const override;
|
||||
void imageChanged();
|
||||
|
||||
private:
|
||||
quint32 changeIndex = 0;
|
||||
};
|
||||
17
src/core/incubator.cpp
Normal file
17
src/core/incubator.cpp
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#include "incubator.hpp"
|
||||
|
||||
#include <qlogging.h>
|
||||
#include <qqmlincubator.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "logcat.hpp"
|
||||
|
||||
QS_LOGGING_CATEGORY(logIncubator, "quickshell.incubator", QtWarningMsg);
|
||||
|
||||
void QsQmlIncubator::statusChanged(QQmlIncubator::Status status) {
|
||||
switch (status) {
|
||||
case QQmlIncubator::Ready: emit this->completed(); break;
|
||||
case QQmlIncubator::Error: emit this->failed(); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
31
src/core/incubator.hpp
Normal file
31
src/core/incubator.hpp
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qqmlincubator.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "logcat.hpp"
|
||||
|
||||
QS_DECLARE_LOGGING_CATEGORY(logIncubator);
|
||||
|
||||
class QsQmlIncubator
|
||||
: public QObject
|
||||
, public QQmlIncubator {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
explicit QsQmlIncubator(QsQmlIncubator::IncubationMode mode, QObject* parent = nullptr)
|
||||
: QObject(parent)
|
||||
, QQmlIncubator(mode) {}
|
||||
|
||||
void statusChanged(QQmlIncubator::Status status) override;
|
||||
|
||||
signals:
|
||||
void completed();
|
||||
void failed();
|
||||
};
|
||||
|
||||
class DelayedQmlIncubationController: public QQmlIncubationController {
|
||||
// Do nothing.
|
||||
// This ensures lazy loaders don't start blocking before onReload creates windows.
|
||||
};
|
||||
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 << info.pid;
|
||||
return stream;
|
||||
}
|
||||
|
||||
QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) {
|
||||
stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime >> info.pid;
|
||||
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
|
||||
|
||||
}
|
||||
41
src/core/instanceinfo.hpp
Normal file
41
src/core/instanceinfo.hpp
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
#pragma once
|
||||
|
||||
#include <qdatetime.h>
|
||||
#include <qlogging.h>
|
||||
#include <qstring.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
struct InstanceInfo {
|
||||
QString instanceId;
|
||||
QString configPath;
|
||||
QString shellId;
|
||||
QDateTime launchTime;
|
||||
pid_t pid = -1;
|
||||
|
||||
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
|
||||
200
src/core/lazyloader.cpp
Normal file
200
src/core/lazyloader.cpp
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
#include "lazyloader.hpp"
|
||||
#include <utility>
|
||||
|
||||
#include <qlogging.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlcomponent.h>
|
||||
#include <qqmlcontext.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qqmlincubator.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "incubator.hpp"
|
||||
#include "reload.hpp"
|
||||
|
||||
void LazyLoader::onReload(QObject* oldInstance) {
|
||||
auto* old = qobject_cast<LazyLoader*>(oldInstance);
|
||||
|
||||
this->incubateIfReady(true);
|
||||
|
||||
if (old != nullptr && old->mItem != nullptr && this->incubator != nullptr) {
|
||||
this->incubator->forceCompletion();
|
||||
}
|
||||
|
||||
if (this->mItem != nullptr) {
|
||||
if (auto* reloadable = qobject_cast<Reloadable*>(this->mItem)) {
|
||||
reloadable->reload(old == nullptr ? nullptr : old->mItem);
|
||||
} else {
|
||||
Reloadable::reloadRecursive(this->mItem, old);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QObject* LazyLoader::item() {
|
||||
if (this->isLoading()) this->setActive(true);
|
||||
return this->mItem;
|
||||
}
|
||||
|
||||
void LazyLoader::setItem(QObject* item) {
|
||||
if (item == this->mItem) return;
|
||||
|
||||
if (this->mItem != nullptr) {
|
||||
this->mItem->deleteLater();
|
||||
}
|
||||
|
||||
this->mItem = item;
|
||||
|
||||
if (item != nullptr) {
|
||||
item->setParent(this);
|
||||
}
|
||||
|
||||
this->targetActive = this->isActive();
|
||||
|
||||
emit this->itemChanged();
|
||||
emit this->activeChanged();
|
||||
}
|
||||
|
||||
bool LazyLoader::isLoading() const { return this->incubator != nullptr; }
|
||||
|
||||
void LazyLoader::setLoading(bool loading) {
|
||||
if (loading == this->targetLoading || this->isActive()) return;
|
||||
this->targetLoading = loading;
|
||||
|
||||
if (loading) {
|
||||
this->incubateIfReady();
|
||||
} else if (this->mItem != nullptr) {
|
||||
this->mItem->deleteLater();
|
||||
this->mItem = nullptr;
|
||||
} else if (this->incubator != nullptr) {
|
||||
delete this->incubator;
|
||||
this->incubator = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool LazyLoader::isActive() const { return this->mItem != nullptr; }
|
||||
|
||||
void LazyLoader::setActive(bool active) {
|
||||
if (active == this->targetActive) return;
|
||||
this->targetActive = active;
|
||||
|
||||
if (active) {
|
||||
if (this->isLoading()) {
|
||||
this->incubator->forceCompletion();
|
||||
} else if (!this->isActive()) {
|
||||
this->incubateIfReady();
|
||||
}
|
||||
} else if (this->isActive()) {
|
||||
this->setItem(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void LazyLoader::setActiveAsync(bool active) {
|
||||
if (active == (this->targetActive || this->targetLoading)) return;
|
||||
if (active) this->setLoading(true);
|
||||
else this->setActive(false);
|
||||
}
|
||||
|
||||
QQmlComponent* LazyLoader::component() const {
|
||||
return this->cleanupComponent ? nullptr : this->mComponent;
|
||||
}
|
||||
|
||||
void LazyLoader::setComponent(QQmlComponent* component) {
|
||||
if (this->cleanupComponent) this->setSource(nullptr);
|
||||
if (component == this->mComponent) return;
|
||||
this->cleanupComponent = false;
|
||||
|
||||
if (this->mComponent != nullptr) {
|
||||
QObject::disconnect(this->mComponent, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
this->mComponent = component;
|
||||
|
||||
if (component != nullptr) {
|
||||
QObject::connect(
|
||||
this->mComponent,
|
||||
&QObject::destroyed,
|
||||
this,
|
||||
&LazyLoader::onComponentDestroyed
|
||||
);
|
||||
}
|
||||
|
||||
emit this->componentChanged();
|
||||
}
|
||||
|
||||
void LazyLoader::onComponentDestroyed() {
|
||||
this->mComponent = nullptr;
|
||||
// todo: figure out what happens to the incubator
|
||||
}
|
||||
|
||||
QString LazyLoader::source() const { return this->mSource; }
|
||||
|
||||
void LazyLoader::setSource(QString source) {
|
||||
if (!this->cleanupComponent) this->setComponent(nullptr);
|
||||
if (source == this->mSource) return;
|
||||
this->cleanupComponent = true;
|
||||
|
||||
this->mSource = std::move(source);
|
||||
delete this->mComponent;
|
||||
|
||||
if (!this->mSource.isEmpty()) {
|
||||
auto* context = QQmlEngine::contextForObject(this);
|
||||
this->mComponent = new QQmlComponent(
|
||||
context == nullptr ? nullptr : context->engine(),
|
||||
context == nullptr ? this->mSource : context->resolvedUrl(this->mSource)
|
||||
);
|
||||
|
||||
if (this->mComponent->isError()) {
|
||||
qWarning() << this->mComponent->errorString().toStdString().c_str();
|
||||
delete this->mComponent;
|
||||
this->mComponent = nullptr;
|
||||
}
|
||||
} else {
|
||||
this->mComponent = nullptr;
|
||||
}
|
||||
|
||||
emit this->sourceChanged();
|
||||
}
|
||||
|
||||
void LazyLoader::incubateIfReady(bool overrideReloadCheck) {
|
||||
if (!(this->reloadComplete || overrideReloadCheck) || !(this->targetLoading || this->targetActive)
|
||||
|| this->mComponent == nullptr || this->incubator != nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this->incubator = new QsQmlIncubator(
|
||||
this->targetActive ? QQmlIncubator::Synchronous : QQmlIncubator::Asynchronous,
|
||||
this
|
||||
);
|
||||
|
||||
// clang-format off
|
||||
QObject::connect(this->incubator, &QsQmlIncubator::completed, this, &LazyLoader::onIncubationCompleted);
|
||||
QObject::connect(this->incubator, &QsQmlIncubator::failed, this, &LazyLoader::onIncubationFailed);
|
||||
// clang-format on
|
||||
|
||||
emit this->loadingChanged();
|
||||
|
||||
this->mComponent->create(*this->incubator, QQmlEngine::contextForObject(this->mComponent));
|
||||
}
|
||||
|
||||
void LazyLoader::onIncubationCompleted() {
|
||||
this->setItem(this->incubator->object());
|
||||
// The incubator is not necessarily inert at the time of this callback,
|
||||
// so deleteLater is required.
|
||||
this->incubator->deleteLater();
|
||||
this->incubator = nullptr;
|
||||
this->targetLoading = false;
|
||||
emit this->loadingChanged();
|
||||
}
|
||||
|
||||
void LazyLoader::onIncubationFailed() {
|
||||
qWarning() << "Failed to create LazyLoader component";
|
||||
|
||||
for (auto& error: this->incubator->errors()) {
|
||||
qWarning() << error;
|
||||
}
|
||||
|
||||
delete this->incubator;
|
||||
this->targetLoading = false;
|
||||
emit this->loadingChanged();
|
||||
}
|
||||
173
src/core/lazyloader.hpp
Normal file
173
src/core/lazyloader.hpp
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
#pragma once
|
||||
|
||||
#include <QtQml/qqmlcomponent.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlincubator.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "incubator.hpp"
|
||||
#include "reload.hpp"
|
||||
|
||||
///! Asynchronous component loader.
|
||||
/// The LazyLoader can be used to prepare components that don't need to be
|
||||
/// created immediately, such as windows that aren't visible until triggered
|
||||
/// by another action. It works on creating the component in the gaps between
|
||||
/// frame rendering to prevent blocking the interface thread.
|
||||
/// It can also be used to preserve memory by loading components only
|
||||
/// when you need them and unloading them afterward.
|
||||
///
|
||||
/// Note that when reloading the UI due to changes, lazy loaders will always
|
||||
/// load synchronously so windows can be reused.
|
||||
///
|
||||
/// #### Example
|
||||
/// The following example creates a PopupWindow asynchronously as the bar loads.
|
||||
/// This means the bar can be shown onscreen before the popup is ready, however
|
||||
/// trying to show the popup before it has finished loading in the background
|
||||
/// will cause the UI thread to block.
|
||||
///
|
||||
/// ```qml
|
||||
/// import QtQuick
|
||||
/// import QtQuick.Controls
|
||||
/// import Quickshell
|
||||
///
|
||||
/// ShellRoot {
|
||||
/// PanelWindow {
|
||||
/// id: window
|
||||
/// height: 50
|
||||
///
|
||||
/// anchors {
|
||||
/// bottom: true
|
||||
/// left: true
|
||||
/// right: true
|
||||
/// }
|
||||
///
|
||||
/// LazyLoader {
|
||||
/// id: popupLoader
|
||||
///
|
||||
/// // start loading immediately
|
||||
/// loading: true
|
||||
///
|
||||
/// // this window will be loaded in the background during spare
|
||||
/// // frame time unless active is set to true, where it will be
|
||||
/// // loaded in the foreground
|
||||
/// PopupWindow {
|
||||
/// // position the popup above the button
|
||||
/// parentWindow: window
|
||||
/// relativeX: window.width / 2 - width / 2
|
||||
/// relativeY: -height
|
||||
///
|
||||
/// // some heavy component here
|
||||
///
|
||||
/// width: 200
|
||||
/// height: 200
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// Button {
|
||||
/// anchors.centerIn: parent
|
||||
/// text: "show popup"
|
||||
///
|
||||
/// // accessing popupLoader.item will force the loader to
|
||||
/// // finish loading on the UI thread if it isn't finished yet.
|
||||
/// onClicked: popupLoader.item.visible = !popupLoader.item.visible
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// > [!WARNING] Components that internally load other components must explicitly
|
||||
/// > support asynchronous loading to avoid blocking.
|
||||
/// >
|
||||
/// > 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.
|
||||
///
|
||||
/// > [!WARNING] LazyLoaders do not start loading before the first window is created,
|
||||
/// > meaning if you create all windows inside of lazy loaders, none of them will ever load.
|
||||
class LazyLoader: public Reloadable {
|
||||
Q_OBJECT;
|
||||
/// The fully loaded item if the loader is @@loading or @@active, or `null`
|
||||
/// if neither @@loading nor @@active.
|
||||
///
|
||||
/// Note that the item is owned by the LazyLoader, and destroying the LazyLoader
|
||||
/// will destroy the item.
|
||||
///
|
||||
/// > [!WARNING] If you access the `item` of a loader that is currently loading,
|
||||
/// > it will block as if you had set `active` to true immediately beforehand.
|
||||
/// >
|
||||
/// > You can instead set @@loading and listen to @@activeChanged(s) signal to
|
||||
/// > ensure loading happens asynchronously.
|
||||
Q_PROPERTY(QObject* item READ item NOTIFY itemChanged);
|
||||
/// If the loader is actively loading.
|
||||
///
|
||||
/// If the component is not loaded, setting this property to true will start
|
||||
/// loading it asynchronously. If the component is already loaded, setting
|
||||
/// this property has no effect.
|
||||
///
|
||||
/// See also: @@activeAsync.
|
||||
Q_PROPERTY(bool loading READ isLoading WRITE setLoading NOTIFY loadingChanged);
|
||||
/// If the component is fully loaded.
|
||||
///
|
||||
/// Setting this property to `true` will force the component to load to completion,
|
||||
/// blocking the UI, and setting it to `false` will destroy the component, requiring
|
||||
/// it to be loaded again.
|
||||
///
|
||||
/// 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. 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.
|
||||
Q_PROPERTY(QQmlComponent* component READ component WRITE setComponent NOTIFY componentChanged);
|
||||
/// 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;
|
||||
|
||||
public:
|
||||
void onReload(QObject* oldInstance) override;
|
||||
|
||||
[[nodiscard]] bool isActive() const;
|
||||
void setActive(bool active);
|
||||
void setActiveAsync(bool active);
|
||||
|
||||
[[nodiscard]] bool isLoading() const;
|
||||
void setLoading(bool loading);
|
||||
|
||||
[[nodiscard]] QObject* item();
|
||||
void setItem(QObject* item);
|
||||
|
||||
[[nodiscard]] QQmlComponent* component() const;
|
||||
void setComponent(QQmlComponent* component);
|
||||
|
||||
[[nodiscard]] QString source() const;
|
||||
void setSource(QString source);
|
||||
|
||||
signals:
|
||||
void activeChanged();
|
||||
void loadingChanged();
|
||||
void itemChanged();
|
||||
void sourceChanged();
|
||||
void componentChanged();
|
||||
|
||||
private slots:
|
||||
void onIncubationCompleted();
|
||||
void onIncubationFailed();
|
||||
void onComponentDestroyed();
|
||||
|
||||
private:
|
||||
void incubateIfReady(bool overrideReloadCheck = false);
|
||||
void waitForObjectCreation();
|
||||
|
||||
bool targetLoading = false;
|
||||
bool targetActive = false;
|
||||
QObject* mItem = nullptr;
|
||||
QString mSource;
|
||||
QQmlComponent* mComponent = nullptr;
|
||||
QsQmlIncubator* incubator = nullptr;
|
||||
bool cleanupComponent = false;
|
||||
};
|
||||
28
src/core/logcat.hpp
Normal file
28
src/core/logcat.hpp
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#pragma once
|
||||
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
|
||||
namespace qs::log {
|
||||
void initLogCategoryLevel(const char* name, QtMsgType defaultLevel = QtDebugMsg);
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||
#define QS_DECLARE_LOGGING_CATEGORY(name) \
|
||||
namespace qslogcat { \
|
||||
Q_DECLARE_LOGGING_CATEGORY(name); \
|
||||
} \
|
||||
const QLoggingCategory& name()
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||
#define QS_LOGGING_CATEGORY(name, category, ...) \
|
||||
namespace qslogcat { \
|
||||
Q_LOGGING_CATEGORY(name, category __VA_OPT__(, __VA_ARGS__)); \
|
||||
} \
|
||||
const QLoggingCategory& name() { \
|
||||
static auto* init = []() { \
|
||||
qs::log::initLogCategoryLevel(category __VA_OPT__(, __VA_ARGS__)); \
|
||||
return &qslogcat::name; \
|
||||
}(); \
|
||||
return (init) (); \
|
||||
}
|
||||
957
src/core/logging.cpp
Normal file
957
src/core/logging.cpp
Normal file
|
|
@ -0,0 +1,957 @@
|
|||
#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 "logcat.hpp"
|
||||
#include "logging_p.hpp"
|
||||
#include "logging_qtprivate.cpp" // NOLINT
|
||||
#include "paths.hpp"
|
||||
#include "ringbuf.hpp"
|
||||
|
||||
QS_LOGGING_CATEGORY(logBare, "quickshell.bare");
|
||||
|
||||
namespace qs::log {
|
||||
using namespace qt_logging_registry;
|
||||
|
||||
QS_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."));
|
||||
|
||||
CategoryFilter filter;
|
||||
|
||||
// We don't respect log filters for qs logs because some distros like to ship
|
||||
// default configs that hide everything. QT_LOGGING_RULES is considered via the filter list.
|
||||
if (isQs) {
|
||||
// QtDebugMsg == 0, so default
|
||||
auto defaultLevel = instance->defaultLevels.value(categoryName);
|
||||
|
||||
filter = CategoryFilter();
|
||||
// clang-format off
|
||||
filter.debug = instance->mDefaultLevel == QtDebugMsg || defaultLevel == QtDebugMsg;
|
||||
filter.info = filter.debug || instance->mDefaultLevel == QtInfoMsg || defaultLevel == QtInfoMsg;
|
||||
filter.warn = filter.info || instance->mDefaultLevel == QtWarningMsg || defaultLevel == QtWarningMsg;
|
||||
filter.critical = filter.warn || instance->mDefaultLevel == QtCriticalMsg || defaultLevel == QtCriticalMsg;
|
||||
// clang-format on
|
||||
} else if (instance->lastCategoryFilter) {
|
||||
instance->lastCategoryFilter(category);
|
||||
filter = CategoryFilter(category);
|
||||
}
|
||||
|
||||
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;
|
||||
// Load QT_LOGGING_RULES because we ignore the last category filter for QS messages
|
||||
// due to disk config files.
|
||||
parser.setContent(qEnvironmentVariable("QT_LOGGING_RULES"));
|
||||
instance->rules = new QList(parser.rules());
|
||||
parser.setContent(rules);
|
||||
instance->rules->append(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 initLogCategoryLevel(const char* name, QtMsgType defaultLevel) {
|
||||
LogManager::instance()->defaultLevels.insert(QLatin1StringView(name), defaultLevel);
|
||||
}
|
||||
|
||||
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" << detailedPath;
|
||||
}
|
||||
|
||||
// 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 && !this->detailedFile->flush())) {
|
||||
if (this->detailedFile) {
|
||||
qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs.";
|
||||
}
|
||||
|
||||
this->detailedWriter.setDevice(nullptr);
|
||||
this->detailedFile->close();
|
||||
this->detailedFile = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
154
src/core/logging.hpp
Normal file
154
src/core/logging.hpp
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <qbytearrayview.h>
|
||||
#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>
|
||||
|
||||
#include "logcat.hpp"
|
||||
|
||||
QS_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<QLatin1StringView, QtMsgType> defaultLevels;
|
||||
QHash<const void*, CategoryFilter> sparseFilters;
|
||||
QHash<QLatin1StringView, CategoryFilter> allFilters;
|
||||
|
||||
QTextStream stdoutStream;
|
||||
LoggingThreadProxy threadProxy;
|
||||
|
||||
friend void initLogCategoryLevel(const char* name, QtMsgType defaultLevel);
|
||||
};
|
||||
|
||||
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
|
||||
139
src/core/logging_qtprivate.cpp
Normal file
139
src/core/logging_qtprivate.cpp
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
// 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 "logcat.hpp"
|
||||
#include "logging_qtprivate.hpp"
|
||||
|
||||
namespace qs::log {
|
||||
QS_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
|
||||
47
src/core/logging_qtprivate.hpp
Normal file
47
src/core/logging_qtprivate.hpp
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
#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>
|
||||
|
||||
#include "logcat.hpp"
|
||||
|
||||
namespace qs::log {
|
||||
QS_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,257 +0,0 @@
|
|||
#include <iostream>
|
||||
|
||||
#include <qcommandlineoption.h>
|
||||
#include <qcommandlineparser.h>
|
||||
#include <qdir.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qguiapplication.h>
|
||||
#include <qlogging.h>
|
||||
#include <qobject.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 main(int argc, char** argv) {
|
||||
const auto app = QGuiApplication(argc, argv);
|
||||
QGuiApplication::setApplicationName("quickshell");
|
||||
QGuiApplication::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);
|
||||
|
||||
QString configFilePath;
|
||||
{
|
||||
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) {
|
||||
qFatal() << "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) {
|
||||
qFatal() << "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
|
||||
}
|
||||
}
|
||||
|
||||
qFatal() << "configuration" << configName << "not found in manifest" << manifestPath;
|
||||
return -1;
|
||||
} else if (manifestPathLevel < 2) {
|
||||
qFatal() << "cannot open config manifest at" << manifestPath;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto basePathInfo = QFileInfo(basePath);
|
||||
if (!basePathInfo.exists()) {
|
||||
qFatal() << "base path does not exist:" << basePath;
|
||||
return -1;
|
||||
} else if (!QFileInfo(basePathInfo.canonicalFilePath()).isDir()) {
|
||||
qFatal() << "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
|
||||
}
|
||||
}
|
||||
|
||||
qFatal() << "no directory named " << configName << "found in base path" << basePath;
|
||||
return -1;
|
||||
}
|
||||
haspath:;
|
||||
} else {
|
||||
configFilePath = basePath;
|
||||
}
|
||||
|
||||
auto configFile = QFileInfo(configFilePath);
|
||||
if (!configFile.exists()) {
|
||||
qFatal() << "config path does not exist:" << configFilePath;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (configFile.isDir()) {
|
||||
configFilePath = QDir(configFilePath).filePath("shell.qml");
|
||||
}
|
||||
|
||||
configFile = QFileInfo(configFilePath);
|
||||
if (!configFile.exists()) {
|
||||
qFatal() << "no shell.qml found in config path:" << configFilePath;
|
||||
return -1;
|
||||
} else if (configFile.isDir()) {
|
||||
qFatal() << "shell.qml is a directory:" << configFilePath;
|
||||
return -1;
|
||||
}
|
||||
|
||||
configFilePath = QFileInfo(configFilePath).canonicalFilePath();
|
||||
configFile = QFileInfo(configFilePath);
|
||||
if (!configFile.exists()) {
|
||||
qFatal() << "config file does not exist:" << configFilePath;
|
||||
return -1;
|
||||
} else if (configFile.isDir()) {
|
||||
qFatal() << "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)) {
|
||||
QDir::setCurrent(parser.value(workdirOption));
|
||||
}
|
||||
|
||||
QuickshellPlugin::initPlugins();
|
||||
|
||||
// Base window transparency appears to be additive.
|
||||
// Use a fully transparent window with a colored rect.
|
||||
QQuickWindow::setDefaultAlphaBuffer(true);
|
||||
|
||||
auto root = RootWrapper(configFilePath);
|
||||
|
||||
return QGuiApplication::exec();
|
||||
}
|
||||
81
src/core/model.cpp
Normal file
81
src/core/model.cpp
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
#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"}};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
126
src/core/model.hpp
Normal file
126
src/core/model.hpp
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
|
||||
#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(QList<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]] QList<QObject*> values() const { return this->valuesList; }
|
||||
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 insertObjectSorted(T* object, const std::function<bool(T*, T*)>& compare) {
|
||||
auto& list = this->valueList();
|
||||
auto iter = list.begin();
|
||||
|
||||
while (iter != list.end()) {
|
||||
if (!compare(object, *iter)) break;
|
||||
++iter;
|
||||
}
|
||||
|
||||
auto idx = iter - list.begin();
|
||||
this->UntypedObjectModel::insertObject(object, idx);
|
||||
}
|
||||
|
||||
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,10 +7,28 @@ headers = [
|
|||
"shell.hpp",
|
||||
"variants.hpp",
|
||||
"region.hpp",
|
||||
"proxywindow.hpp",
|
||||
"../window/proxywindow.hpp",
|
||||
"persistentprops.hpp",
|
||||
"windowinterface.hpp",
|
||||
"panelinterface.hpp",
|
||||
"floatingwindow.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",
|
||||
"scriptmodel.hpp",
|
||||
"colorquantizer.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;
|
||||
};
|
||||
425
src/core/paths.cpp
Normal file
425
src/core/paths.cpp
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
#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 <qpair.h>
|
||||
#include <qstandardpaths.h>
|
||||
#include <qtenvironmentvariables.h>
|
||||
#include <qtversionchecks.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "instanceinfo.hpp"
|
||||
#include "logcat.hpp"
|
||||
|
||||
namespace {
|
||||
QS_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg);
|
||||
}
|
||||
|
||||
QsPaths* QsPaths::instance() {
|
||||
static auto* instance = new QsPaths(); // NOLINT
|
||||
return instance;
|
||||
}
|
||||
|
||||
void QsPaths::init(QString shellId, QString pathId, QString dataOverride, QString stateOverride) {
|
||||
auto* instance = QsPaths::instance();
|
||||
instance->shellId = std::move(shellId);
|
||||
instance->pathId = std::move(pathId);
|
||||
instance->shellDataOverride = std::move(dataOverride);
|
||||
instance->shellStateOverride = std::move(stateOverride);
|
||||
}
|
||||
|
||||
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::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;
|
||||
}
|
||||
|
||||
QDir* QsPaths::shellVfsDir() {
|
||||
if (this->shellVfsState == DirState::Unknown) {
|
||||
if (auto* baseRunDir = this->baseRunDir()) {
|
||||
this->mShellVfsDir = QDir(baseRunDir->filePath("vfs"));
|
||||
this->mShellVfsDir = QDir(this->mShellVfsDir.filePath(this->shellId));
|
||||
|
||||
qCDebug(logPaths) << "Initialized runtime vfs path:" << this->mShellVfsDir.path();
|
||||
|
||||
if (!this->mShellVfsDir.mkpath(".")) {
|
||||
qCCritical(logPaths) << "Could not create runtime vfs directory at"
|
||||
<< this->mShellVfsDir.path();
|
||||
this->shellVfsState = DirState::Failed;
|
||||
} else {
|
||||
this->shellVfsState = DirState::Ready;
|
||||
}
|
||||
} else {
|
||||
qCCritical(logPaths) << "Could not create shell runtime vfs path as it was not possible to "
|
||||
"create the base runtime path.";
|
||||
|
||||
this->shellVfsState = DirState::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->shellVfsState == DirState::Failed) return nullptr;
|
||||
else return &this->mShellVfsDir;
|
||||
}
|
||||
|
||||
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.";
|
||||
}
|
||||
}
|
||||
|
||||
QDir QsPaths::shellDataDir() {
|
||||
if (this->shellDataState == DirState::Unknown) {
|
||||
QDir dir;
|
||||
if (this->shellDataOverride.isEmpty()) {
|
||||
dir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
|
||||
dir = QDir(dir.filePath("by-shell"));
|
||||
dir = QDir(dir.filePath(this->shellId));
|
||||
} else {
|
||||
auto basedir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
|
||||
dir = QDir(this->shellDataOverride.replace("$BASE", basedir));
|
||||
}
|
||||
|
||||
this->mShellDataDir = dir;
|
||||
|
||||
qCDebug(logPaths) << "Initialized data path:" << dir.path();
|
||||
|
||||
if (!dir.mkpath(".")) {
|
||||
qCCritical(logPaths) << "Could not create data directory at" << dir.path();
|
||||
|
||||
this->shellDataState = DirState::Failed;
|
||||
} else {
|
||||
this->shellDataState = DirState::Ready;
|
||||
}
|
||||
}
|
||||
|
||||
// Returning no path on fail might result in files being written in unintended locations.
|
||||
return this->mShellDataDir;
|
||||
}
|
||||
|
||||
QDir QsPaths::shellStateDir() {
|
||||
if (this->shellStateState == DirState::Unknown) {
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
|
||||
QDir dir;
|
||||
if (qEnvironmentVariableIsSet("XDG_STATE_HOME")) {
|
||||
dir = QDir(qEnvironmentVariable("XDG_STATE_HOME"));
|
||||
} else {
|
||||
auto home = QDir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation));
|
||||
dir = QDir(home.filePath(".local/state"));
|
||||
}
|
||||
|
||||
if (this->shellStateOverride.isEmpty()) {
|
||||
dir = QDir(dir.filePath("quickshell/by-shell"));
|
||||
dir = QDir(dir.filePath(this->shellId));
|
||||
} else {
|
||||
dir = QDir(this->shellStateOverride.replace("$BASE", dir.path()));
|
||||
}
|
||||
#else
|
||||
QDir dir;
|
||||
if (this->shellStateOverride.isEmpty()) {
|
||||
dir = QDir(QStandardPaths::writableLocation(QStandardPaths::StateLocation));
|
||||
dir = QDir(dir.filePath("by-shell"));
|
||||
dir = QDir(dir.filePath(this->shellId));
|
||||
} else {
|
||||
auto basedir = QStandardPaths::writableLocation(QStandardPaths::GenericStateLocation);
|
||||
dir = QDir(this->shellStateOverride.replace("$BASE", basedir));
|
||||
}
|
||||
#endif
|
||||
this->mShellStateDir = dir;
|
||||
|
||||
qCDebug(logPaths) << "Initialized state path:" << dir.path();
|
||||
|
||||
if (!dir.mkpath(".")) {
|
||||
qCCritical(logPaths) << "Could not create state directory at" << dir.path();
|
||||
|
||||
this->shellStateState = DirState::Failed;
|
||||
} else {
|
||||
this->shellStateState = DirState::Ready;
|
||||
}
|
||||
}
|
||||
|
||||
// Returning no path on fail might result in files being written in unintended locations.
|
||||
return this->mShellStateDir;
|
||||
}
|
||||
|
||||
QDir QsPaths::shellCacheDir() {
|
||||
if (this->shellCacheState == DirState::Unknown) {
|
||||
auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
|
||||
dir = QDir(dir.filePath("by-shell"));
|
||||
dir = QDir(dir.filePath(this->shellId));
|
||||
this->mShellCacheDir = dir;
|
||||
|
||||
qCDebug(logPaths) << "Initialized cache path:" << dir.path();
|
||||
|
||||
if (!dir.mkpath(".")) {
|
||||
qCCritical(logPaths) << "Could not create cache directory at" << dir.path();
|
||||
|
||||
this->shellCacheState = DirState::Failed;
|
||||
} else {
|
||||
this->shellCacheState = DirState::Ready;
|
||||
}
|
||||
}
|
||||
|
||||
// Returning no path on fail might result in files being written in unintended locations.
|
||||
return this->mShellCacheDir;
|
||||
}
|
||||
|
||||
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, bool allowDead) {
|
||||
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
|
||||
auto isLocked = lock.l_type != F_UNLCK;
|
||||
|
||||
if (!isLocked && !allowDead) return false;
|
||||
|
||||
if (info) {
|
||||
info->pid = isLocked ? lock.l_pid : -1;
|
||||
|
||||
auto stream = QDataStream(&file);
|
||||
stream >> info->instance;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QPair<QVector<InstanceLockInfo>, QVector<InstanceLockInfo>>
|
||||
QsPaths::collectInstances(const QString& path) {
|
||||
qCDebug(logPaths) << "Collecting instances from" << path;
|
||||
auto liveInstances = QVector<InstanceLockInfo>();
|
||||
auto deadInstances = 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, true)) {
|
||||
qCDebug(logPaths).nospace() << "Found instance " << info.instance.instanceId << " (pid "
|
||||
<< info.pid << ") at " << path;
|
||||
|
||||
if (info.pid == -1) {
|
||||
deadInstances.push_back(info);
|
||||
} else {
|
||||
liveInstances.push_back(info);
|
||||
}
|
||||
} else {
|
||||
qCDebug(logPaths) << "Skipped potential instance at" << path;
|
||||
}
|
||||
}
|
||||
|
||||
return qMakePair(liveInstances, deadInstances);
|
||||
}
|
||||
68
src/core/paths.hpp
Normal file
68
src/core/paths.hpp
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
#pragma once
|
||||
#include <qdatetime.h>
|
||||
#include <qdir.h>
|
||||
#include <qpair.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, QString dataOverride, QString stateOverride);
|
||||
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, bool allowDead = false);
|
||||
static QPair<QVector<InstanceLockInfo>, QVector<InstanceLockInfo>>
|
||||
collectInstances(const QString& path);
|
||||
|
||||
QDir* baseRunDir();
|
||||
QDir* shellRunDir();
|
||||
QDir* shellVfsDir();
|
||||
QDir* instanceRunDir();
|
||||
void linkRunDir();
|
||||
void linkPathDir();
|
||||
void createLock();
|
||||
|
||||
QDir shellDataDir();
|
||||
QDir shellStateDir();
|
||||
QDir shellCacheDir();
|
||||
|
||||
private:
|
||||
enum class DirState : quint8 {
|
||||
Unknown = 0,
|
||||
Ready = 1,
|
||||
Failed = 2,
|
||||
};
|
||||
|
||||
QString shellId;
|
||||
QString pathId;
|
||||
QDir mBaseRunDir;
|
||||
QDir mShellRunDir;
|
||||
QDir mShellVfsDir;
|
||||
QDir mInstanceRunDir;
|
||||
DirState baseRunState = DirState::Unknown;
|
||||
DirState shellRunState = DirState::Unknown;
|
||||
DirState shellVfsState = DirState::Unknown;
|
||||
DirState instanceRunState = DirState::Unknown;
|
||||
|
||||
QDir mShellDataDir;
|
||||
QDir mShellStateDir;
|
||||
QDir mShellCacheDir;
|
||||
DirState shellDataState = DirState::Unknown;
|
||||
DirState shellStateState = DirState::Unknown;
|
||||
DirState shellCacheState = DirState::Unknown;
|
||||
|
||||
QString shellDataOverride;
|
||||
QString shellStateOverride;
|
||||
};
|
||||
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
|
||||
|
|
@ -3,31 +3,36 @@
|
|||
|
||||
#include <qvector.h> // NOLINT (what??)
|
||||
|
||||
static QVector<QuickshellPlugin*> plugins; // NOLINT
|
||||
#include "generation.hpp"
|
||||
|
||||
void QuickshellPlugin::registerPlugin(QuickshellPlugin& plugin) { plugins.push_back(&plugin); }
|
||||
static QVector<QsEnginePlugin*> plugins; // NOLINT
|
||||
|
||||
void QuickshellPlugin::initPlugins() {
|
||||
plugins.erase(
|
||||
std::remove_if(
|
||||
plugins.begin(),
|
||||
plugins.end(),
|
||||
[](QuickshellPlugin* plugin) { return !plugin->applies(); }
|
||||
),
|
||||
plugins.end()
|
||||
);
|
||||
void QsEnginePlugin::registerPlugin(QsEnginePlugin& plugin) { plugins.push_back(&plugin); }
|
||||
|
||||
for (QuickshellPlugin* plugin: plugins) {
|
||||
void QsEnginePlugin::initPlugins() {
|
||||
plugins.removeIf([](QsEnginePlugin* plugin) { return !plugin->applies(); });
|
||||
|
||||
std::ranges::sort(plugins, [](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::runOnReload() {
|
||||
for (QuickshellPlugin* plugin: plugins) {
|
||||
void QsEnginePlugin::runConstructGeneration(EngineGeneration& generation) {
|
||||
for (QsEnginePlugin* plugin: plugins) {
|
||||
plugin->constructGeneration(generation);
|
||||
}
|
||||
}
|
||||
|
||||
void QsEnginePlugin::runOnReload() {
|
||||
for (QsEnginePlugin* plugin: plugins) {
|
||||
plugin->onReload();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,23 +2,30 @@
|
|||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qfunctionpointer.h>
|
||||
#include <qlist.h>
|
||||
|
||||
class QuickshellPlugin {
|
||||
class EngineGeneration;
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
|
|
@ -26,6 +33,6 @@ public:
|
|||
#define QS_REGISTER_PLUGIN(clazz) \
|
||||
[[gnu::constructor]] void qsInitPlugin() { \
|
||||
static clazz plugin; \
|
||||
QuickshellPlugin::registerPlugin(plugin); \
|
||||
QsEnginePlugin::registerPlugin(plugin); \
|
||||
}
|
||||
// NOLINTEND
|
||||
|
|
|
|||
386
src/core/popupanchor.cpp
Normal file
386
src/core/popupanchor.cpp
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
#include "popupanchor.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qlogging.h>
|
||||
#include <qobject.h>
|
||||
#include <qquickitem.h>
|
||||
#include <qsize.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qvectornd.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(); }
|
||||
|
||||
QWindow* PopupAnchor::backingWindow() const {
|
||||
return this->mProxyWindow ? this->mProxyWindow->backingWindow() : nullptr;
|
||||
}
|
||||
|
||||
void PopupAnchor::setWindowInternal(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::setWindow(QObject* window) {
|
||||
this->setItem(nullptr);
|
||||
this->setWindowInternal(window);
|
||||
}
|
||||
|
||||
void PopupAnchor::setItem(QQuickItem* item) {
|
||||
if (item == this->mItem) return;
|
||||
|
||||
if (this->mItem) {
|
||||
QObject::disconnect(this->mItem, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
this->mItem = item;
|
||||
this->onItemWindowChanged();
|
||||
|
||||
if (item) {
|
||||
QObject::connect(item, &QObject::destroyed, this, &PopupAnchor::onItemDestroyed);
|
||||
QObject::connect(item, &QQuickItem::windowChanged, this, &PopupAnchor::onItemWindowChanged);
|
||||
}
|
||||
}
|
||||
|
||||
void PopupAnchor::onWindowDestroyed() {
|
||||
this->mWindow = nullptr;
|
||||
this->mProxyWindow = nullptr;
|
||||
emit this->windowChanged();
|
||||
emit this->backingWindowVisibilityChanged();
|
||||
}
|
||||
|
||||
void PopupAnchor::onItemDestroyed() {
|
||||
this->mItem = nullptr;
|
||||
emit this->itemChanged();
|
||||
this->setWindowInternal(nullptr);
|
||||
}
|
||||
|
||||
void PopupAnchor::onItemWindowChanged() {
|
||||
if (auto* window = qobject_cast<ProxiedWindow*>(this->mItem->window())) {
|
||||
this->setWindowInternal(window->proxy());
|
||||
} else {
|
||||
this->setWindowInternal(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void PopupAnchor::setRect(Box rect) {
|
||||
if (rect.w <= 0) rect.w = 1;
|
||||
if (rect.h <= 0) rect.h = 1;
|
||||
if (rect == this->mUserRect) return;
|
||||
|
||||
this->mUserRect = rect;
|
||||
emit this->rectChanged();
|
||||
|
||||
this->setWindowRect(rect.qrect().marginsRemoved(this->mMargins.qmargins()));
|
||||
}
|
||||
|
||||
void PopupAnchor::setMargins(Margins margins) {
|
||||
if (margins == this->mMargins) return;
|
||||
|
||||
this->mMargins = margins;
|
||||
emit this->marginsChanged();
|
||||
|
||||
this->setWindowRect(this->mUserRect.qrect().marginsRemoved(margins.qmargins()));
|
||||
}
|
||||
|
||||
void PopupAnchor::setWindowRect(QRect rect) {
|
||||
if (rect.width() <= 0) rect.setWidth(1);
|
||||
if (rect.height() <= 0) rect.setHeight(1);
|
||||
if (rect == this->state.rect) return;
|
||||
|
||||
this->state.rect = rect;
|
||||
emit this->windowRectChanged();
|
||||
}
|
||||
|
||||
void PopupAnchor::resetRect() { this->mUserRect = Box(); }
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void PopupAnchor::updateAnchor() {
|
||||
if (this->mItem && this->mProxyWindow) {
|
||||
auto baseRect =
|
||||
this->mUserRect.isEmpty() ? this->mItem->boundingRect() : this->mUserRect.qrect();
|
||||
|
||||
auto rect = this->mProxyWindow->contentItem()->mapFromItem(
|
||||
this->mItem,
|
||||
baseRect.marginsRemoved(this->mMargins.qmargins())
|
||||
);
|
||||
|
||||
if (rect.width() < 1) rect.setWidth(1);
|
||||
if (rect.height() < 1) rect.setHeight(1);
|
||||
|
||||
this->setWindowRect(rect.toRect());
|
||||
}
|
||||
|
||||
emit this->anchoring();
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
anchor->updateAnchor();
|
||||
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->windowRect().translated(parentGeometry.topLeft());
|
||||
|
||||
auto anchorEdges = anchor->edges();
|
||||
auto anchorGravity = anchor->gravity();
|
||||
|
||||
auto width = windowGeometry.width();
|
||||
auto height = windowGeometry.height();
|
||||
|
||||
auto anchorX = anchorEdges.testFlag(Edges::Left) ? anchorRectGeometry.left()
|
||||
: anchorEdges.testFlag(Edges::Right) ? anchorRectGeometry.right()
|
||||
: anchorRectGeometry.center().x();
|
||||
|
||||
auto anchorY = anchorEdges.testFlag(Edges::Top) ? anchorRectGeometry.top()
|
||||
: anchorEdges.testFlag(Edges::Bottom) ? anchorRectGeometry.bottom()
|
||||
: anchorRectGeometry.center().y();
|
||||
|
||||
auto calcEffectiveX = [&](Edges::Flags anchorGravity, int anchorX) {
|
||||
auto ex = anchorGravity.testFlag(Edges::Left) ? anchorX - windowGeometry.width()
|
||||
: anchorGravity.testFlag(Edges::Right) ? anchorX - 1
|
||||
: anchorX - windowGeometry.width() / 2;
|
||||
|
||||
return ex + 1;
|
||||
};
|
||||
|
||||
auto calcEffectiveY = [&](Edges::Flags anchorGravity, int anchorY) {
|
||||
auto ey = anchorGravity.testFlag(Edges::Top) ? anchorY - windowGeometry.height()
|
||||
: anchorGravity.testFlag(Edges::Bottom) ? anchorY - 1
|
||||
: anchorY - windowGeometry.height() / 2;
|
||||
|
||||
return ey + 1;
|
||||
};
|
||||
|
||||
auto calcRemainingWidth = [&](int effectiveX) {
|
||||
auto width = windowGeometry.width();
|
||||
if (effectiveX < screenGeometry.left()) {
|
||||
auto diff = screenGeometry.left() - effectiveX;
|
||||
effectiveX = screenGeometry.left();
|
||||
width -= diff;
|
||||
}
|
||||
|
||||
auto effectiveX2 = effectiveX + width;
|
||||
if (effectiveX2 > screenGeometry.right()) {
|
||||
width -= effectiveX2 - screenGeometry.right() - 1;
|
||||
}
|
||||
|
||||
return QPair<int, int>(effectiveX, width);
|
||||
};
|
||||
|
||||
auto calcRemainingHeight = [&](int effectiveY) {
|
||||
auto height = windowGeometry.height();
|
||||
if (effectiveY < screenGeometry.left()) {
|
||||
auto diff = screenGeometry.top() - effectiveY;
|
||||
effectiveY = screenGeometry.top();
|
||||
height -= diff;
|
||||
}
|
||||
|
||||
auto effectiveY2 = effectiveY + height;
|
||||
if (effectiveY2 > screenGeometry.bottom()) {
|
||||
height -= effectiveY2 - screenGeometry.bottom() - 1;
|
||||
}
|
||||
|
||||
return QPair<int, int>(effectiveY, height);
|
||||
};
|
||||
|
||||
auto effectiveX = calcEffectiveX(anchorGravity, anchorX);
|
||||
auto effectiveY = calcEffectiveY(anchorGravity, anchorY);
|
||||
|
||||
if (adjustment.testFlag(PopupAdjustment::FlipX)) {
|
||||
const bool flip = (anchorGravity.testFlag(Edges::Left) && effectiveX < screenGeometry.left())
|
||||
|| (anchorGravity.testFlag(Edges::Right)
|
||||
&& effectiveX + windowGeometry.width() > screenGeometry.right());
|
||||
|
||||
if (flip) {
|
||||
auto newAnchorGravity = anchorGravity ^ (Edges::Left | Edges::Right);
|
||||
|
||||
auto newAnchorX = anchorEdges.testFlags(Edges::Left) ? anchorRectGeometry.right()
|
||||
: anchorEdges.testFlags(Edges::Right) ? anchorRectGeometry.left()
|
||||
: anchorX;
|
||||
|
||||
auto newEffectiveX = calcEffectiveX(newAnchorGravity, newAnchorX);
|
||||
|
||||
// TODO IN HL: pick constraint monitor based on anchor rect position in window
|
||||
|
||||
// if the available width when flipped is more than the available width without flipping then flip
|
||||
if (calcRemainingWidth(newEffectiveX).second > calcRemainingWidth(effectiveX).second) {
|
||||
anchorGravity = newAnchorGravity;
|
||||
anchorX = newAnchorX;
|
||||
effectiveX = newEffectiveX;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (adjustment.testFlag(PopupAdjustment::FlipY)) {
|
||||
const bool flip = (anchorGravity.testFlag(Edges::Top) && effectiveY < screenGeometry.top())
|
||||
|| (anchorGravity.testFlag(Edges::Bottom)
|
||||
&& effectiveY + windowGeometry.height() > screenGeometry.bottom());
|
||||
|
||||
if (flip) {
|
||||
auto newAnchorGravity = anchorGravity ^ (Edges::Top | Edges::Bottom);
|
||||
|
||||
auto newAnchorY = anchorEdges.testFlags(Edges::Top) ? anchorRectGeometry.bottom()
|
||||
: anchorEdges.testFlags(Edges::Bottom) ? anchorRectGeometry.top()
|
||||
: anchorY;
|
||||
|
||||
auto newEffectiveY = calcEffectiveY(newAnchorGravity, newAnchorY);
|
||||
|
||||
// if the available width when flipped is more than the available width without flipping then flip
|
||||
if (calcRemainingHeight(newEffectiveY).second > calcRemainingHeight(effectiveY).second) {
|
||||
anchorGravity = newAnchorGravity;
|
||||
anchorY = newAnchorY;
|
||||
effectiveY = newEffectiveY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Slide order is important for the case where the window is too large to fit on screen.
|
||||
if (adjustment.testFlag(PopupAdjustment::SlideX)) {
|
||||
if (effectiveX + windowGeometry.width() > screenGeometry.right()) {
|
||||
effectiveX = screenGeometry.right() - windowGeometry.width() + 1;
|
||||
}
|
||||
|
||||
effectiveX = std::max(effectiveX, screenGeometry.left());
|
||||
}
|
||||
|
||||
if (adjustment.testFlag(PopupAdjustment::SlideY)) {
|
||||
if (effectiveY + windowGeometry.height() > screenGeometry.bottom()) {
|
||||
effectiveY = screenGeometry.bottom() - windowGeometry.height() + 1;
|
||||
}
|
||||
|
||||
effectiveY = std::max(effectiveY, screenGeometry.top());
|
||||
}
|
||||
|
||||
if (adjustment.testFlag(PopupAdjustment::ResizeX)) {
|
||||
auto [newX, newWidth] = calcRemainingWidth(effectiveX);
|
||||
effectiveX = newX;
|
||||
width = newWidth;
|
||||
}
|
||||
|
||||
if (adjustment.testFlag(PopupAdjustment::ResizeY)) {
|
||||
auto [newY, newHeight] = calcRemainingHeight(effectiveY);
|
||||
effectiveY = newY;
|
||||
height = newHeight;
|
||||
}
|
||||
|
||||
window->setGeometry({effectiveX, effectiveY, width, height});
|
||||
}
|
||||
|
||||
bool PopupPositioner::shouldRepositionOnMove() const { return true; }
|
||||
|
||||
PopupPositioner* PopupPositioner::instance() {
|
||||
if (POSITIONER == nullptr) {
|
||||
POSITIONER = new PopupPositioner();
|
||||
}
|
||||
|
||||
return POSITIONER;
|
||||
}
|
||||
|
||||
void PopupPositioner::setInstance(PopupPositioner* instance) {
|
||||
delete POSITIONER;
|
||||
POSITIONER = instance;
|
||||
}
|
||||
214
src/core/popupanchor.hpp
Normal file
214
src/core/popupanchor.hpp
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <qflags.h>
|
||||
#include <qnamespace.h>
|
||||
#include <qobject.h>
|
||||
#include <qpoint.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qquickitem.h>
|
||||
#include <qsize.h>
|
||||
#include <qtclasshelpermacros.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
#include <qvariant.h>
|
||||
#include <qvectornd.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;
|
||||
|
||||
QRect 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. Setting this property unsets @@item.
|
||||
Q_PROPERTY(QObject* window READ window WRITE setWindow NOTIFY windowChanged);
|
||||
/// The item to anchor / attach the popup to. Setting this property unsets @@window.
|
||||
///
|
||||
/// The popup's position relative to its parent window is only calculated when it is
|
||||
/// initially shown (directly before @@anchoring(s) is emitted), meaning its anchor
|
||||
/// rectangle will be set relative to the item's position in the window at that time.
|
||||
/// @@updateAnchor() can be called to update the anchor rectangle if the item's position
|
||||
/// has changed.
|
||||
///
|
||||
/// > [!NOTE] If a more flexible way to position a popup relative to an item is needed,
|
||||
/// > set @@window to the item's parent window, and handle the @@anchoring signal to
|
||||
/// > position the popup relative to the window's contentItem.
|
||||
Q_PROPERTY(QQuickItem* item READ item WRITE setItem NOTIFY itemChanged);
|
||||
/// The anchorpoints the popup will attach to, relative to @@item or @@window.
|
||||
/// Which anchors will be used is determined by the @@edges, @@gravity, and @@adjustment.
|
||||
///
|
||||
/// If using @@item, the default anchor rectangle matches the dimensions of the item.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// [coordinate mapping functions]: https://doc.qt.io/qt-6/qml-qtquick-item.html#mapFromItem-method
|
||||
Q_PROPERTY(Box rect READ rect WRITE setRect RESET resetRect NOTIFY rectChanged);
|
||||
/// A margin applied to the anchor rect.
|
||||
///
|
||||
/// This is most useful when @@item is used and @@rect is left at its default
|
||||
/// value (matching the Item's dimensions).
|
||||
Q_PROPERTY(Margins margins READ margins WRITE setMargins NOTIFY marginsChanged);
|
||||
/// 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) {}
|
||||
|
||||
/// Update the popup's anchor rect relative to its parent window.
|
||||
///
|
||||
/// If anchored to an item, popups anchors will not automatically follow
|
||||
/// the item if its position changes. This function can be called to
|
||||
/// recalculate the anchors.
|
||||
Q_INVOKABLE void updateAnchor();
|
||||
|
||||
[[nodiscard]] bool isDirty() const;
|
||||
void markClean();
|
||||
void markDirty();
|
||||
|
||||
[[nodiscard]] QObject* window() const { return this->mWindow; }
|
||||
[[nodiscard]] ProxyWindowBase* proxyWindow() const { return this->mProxyWindow; }
|
||||
[[nodiscard]] QWindow* backingWindow() const;
|
||||
void setWindowInternal(QObject* window);
|
||||
void setWindow(QObject* window);
|
||||
|
||||
[[nodiscard]] QQuickItem* item() const { return this->mItem; }
|
||||
void setItem(QQuickItem* item);
|
||||
|
||||
[[nodiscard]] QRect windowRect() const { return this->state.rect; }
|
||||
void setWindowRect(QRect rect);
|
||||
|
||||
[[nodiscard]] Box rect() const { return this->mUserRect; }
|
||||
void setRect(Box rect);
|
||||
void resetRect();
|
||||
|
||||
[[nodiscard]] Margins margins() const { return this->mMargins; }
|
||||
void setMargins(Margins margins);
|
||||
|
||||
[[nodiscard]] Edges::Flags edges() const { return this->state.edges; }
|
||||
void setEdges(Edges::Flags edges);
|
||||
|
||||
[[nodiscard]] Edges::Flags gravity() const { return this->state.gravity; }
|
||||
void setGravity(Edges::Flags gravity);
|
||||
|
||||
[[nodiscard]] PopupAdjustment::Flags adjustment() const { return this->state.adjustment; }
|
||||
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();
|
||||
void itemChanged();
|
||||
QSDOC_HIDE void backingWindowVisibilityChanged();
|
||||
QSDOC_HIDE void windowRectChanged();
|
||||
void rectChanged();
|
||||
void marginsChanged();
|
||||
void edgesChanged();
|
||||
void gravityChanged();
|
||||
void adjustmentChanged();
|
||||
|
||||
private slots:
|
||||
void onWindowDestroyed();
|
||||
void onItemDestroyed();
|
||||
void onItemWindowChanged();
|
||||
|
||||
private:
|
||||
QObject* mWindow = nullptr;
|
||||
QQuickItem* mItem = nullptr;
|
||||
ProxyWindowBase* mProxyWindow = nullptr;
|
||||
PopupAnchorState state;
|
||||
Box mUserRect;
|
||||
Margins mMargins;
|
||||
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,223 +0,0 @@
|
|||
#include "proxywindow.hpp"
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qqmllist.h>
|
||||
#include <qquickitem.h>
|
||||
#include <qquickwindow.h>
|
||||
#include <qregion.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
#include <qwindow.h>
|
||||
|
||||
#include "qmlscreen.hpp"
|
||||
#include "region.hpp"
|
||||
#include "reload.hpp"
|
||||
|
||||
ProxyWindowBase::ProxyWindowBase(QObject* parent)
|
||||
: Reloadable(parent)
|
||||
, mContentItem(new QQuickItem()) {
|
||||
QQmlEngine::setObjectOwnership(this->mContentItem, QQmlEngine::CppOwnership);
|
||||
this->mContentItem->setParent(this);
|
||||
|
||||
QObject::connect(this, &ProxyWindowBase::widthChanged, this, &ProxyWindowBase::onWidthChanged);
|
||||
QObject::connect(this, &ProxyWindowBase::heightChanged, this, &ProxyWindowBase::onHeightChanged);
|
||||
}
|
||||
|
||||
ProxyWindowBase::~ProxyWindowBase() {
|
||||
if (this->window != nullptr) {
|
||||
this->window->deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
void ProxyWindowBase::onReload(QObject* oldInstance) {
|
||||
this->window = this->createWindow(oldInstance);
|
||||
this->setupWindow();
|
||||
|
||||
Reloadable::reloadRecursive(this->mContentItem, oldInstance);
|
||||
|
||||
this->mContentItem->setParentItem(this->window->contentItem());
|
||||
this->mContentItem->setWidth(this->width());
|
||||
this->mContentItem->setHeight(this->height());
|
||||
|
||||
// without this the dangling screen pointer wont be updated to a real screen
|
||||
emit this->screenChanged();
|
||||
|
||||
emit this->windowConnected();
|
||||
this->window->setVisible(this->mVisible);
|
||||
}
|
||||
|
||||
QQuickWindow* ProxyWindowBase::createWindow(QObject* oldInstance) {
|
||||
auto* old = qobject_cast<ProxyWindowBase*>(oldInstance);
|
||||
|
||||
if (old == nullptr || old->window == nullptr) {
|
||||
return new QQuickWindow();
|
||||
} else {
|
||||
return old->disownWindow();
|
||||
}
|
||||
}
|
||||
|
||||
void ProxyWindowBase::setupWindow() {
|
||||
// clang-format off
|
||||
QObject::connect(this->window, &QWindow::visibilityChanged, this, &ProxyWindowBase::visibleChanged);
|
||||
QObject::connect(this->window, &QWindow::widthChanged, this, &ProxyWindowBase::widthChanged);
|
||||
QObject::connect(this->window, &QWindow::heightChanged, this, &ProxyWindowBase::heightChanged);
|
||||
QObject::connect(this->window, &QWindow::screenChanged, this, &ProxyWindowBase::screenChanged);
|
||||
QObject::connect(this->window, &QQuickWindow::colorChanged, this, &ProxyWindowBase::colorChanged);
|
||||
|
||||
QObject::connect(this, &ProxyWindowBase::maskChanged, this, &ProxyWindowBase::onMaskChanged);
|
||||
QObject::connect(this, &ProxyWindowBase::widthChanged, this, &ProxyWindowBase::onMaskChanged);
|
||||
QObject::connect(this, &ProxyWindowBase::heightChanged, this, &ProxyWindowBase::onMaskChanged);
|
||||
// clang-format on
|
||||
|
||||
this->window->setScreen(this->mScreen);
|
||||
this->setWidth(this->mWidth);
|
||||
this->setHeight(this->mHeight);
|
||||
this->setColor(this->mColor);
|
||||
this->updateMask();
|
||||
}
|
||||
|
||||
QQuickWindow* ProxyWindowBase::disownWindow() {
|
||||
QObject::disconnect(this->window, nullptr, this, nullptr);
|
||||
|
||||
this->mContentItem->setParentItem(nullptr);
|
||||
|
||||
auto* window = this->window;
|
||||
this->window = nullptr;
|
||||
return window;
|
||||
}
|
||||
|
||||
QQuickWindow* ProxyWindowBase::backingWindow() const { return this->window; }
|
||||
QQuickItem* ProxyWindowBase::contentItem() const { return this->mContentItem; }
|
||||
|
||||
bool ProxyWindowBase::isVisible() const {
|
||||
if (this->window == nullptr) return this->mVisible;
|
||||
else return this->window->isVisible();
|
||||
}
|
||||
|
||||
void ProxyWindowBase::setVisible(bool visible) {
|
||||
if (this->window == nullptr) {
|
||||
this->mVisible = visible;
|
||||
emit this->visibleChanged();
|
||||
} else this->window->setVisible(visible);
|
||||
}
|
||||
|
||||
qint32 ProxyWindowBase::width() const {
|
||||
if (this->window == nullptr) return this->mWidth;
|
||||
else return this->window->width();
|
||||
}
|
||||
|
||||
void ProxyWindowBase::setWidth(qint32 width) {
|
||||
if (this->window == nullptr) {
|
||||
this->mWidth = width;
|
||||
emit this->widthChanged();
|
||||
} else this->window->setWidth(width);
|
||||
}
|
||||
|
||||
qint32 ProxyWindowBase::height() const {
|
||||
if (this->window == nullptr) return this->mHeight;
|
||||
else return this->window->height();
|
||||
}
|
||||
|
||||
void ProxyWindowBase::setHeight(qint32 height) {
|
||||
if (this->window == nullptr) {
|
||||
this->mHeight = height;
|
||||
emit this->heightChanged();
|
||||
} else this->window->setHeight(height);
|
||||
}
|
||||
|
||||
void ProxyWindowBase::setScreen(QuickshellScreenInfo* screen) {
|
||||
if (this->mScreen != nullptr) {
|
||||
QObject::disconnect(this->mScreen, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
auto* qscreen = screen == nullptr ? nullptr : screen->screen;
|
||||
if (qscreen != nullptr) {
|
||||
QObject::connect(qscreen, &QObject::destroyed, this, &ProxyWindowBase::onScreenDestroyed);
|
||||
}
|
||||
|
||||
if (this->window == nullptr) {
|
||||
this->mScreen = qscreen;
|
||||
emit this->screenChanged();
|
||||
} else this->window->setScreen(qscreen);
|
||||
}
|
||||
|
||||
void ProxyWindowBase::onScreenDestroyed() { this->mScreen = nullptr; }
|
||||
|
||||
QuickshellScreenInfo* ProxyWindowBase::screen() const {
|
||||
QScreen* qscreen = nullptr;
|
||||
|
||||
if (this->window == nullptr) {
|
||||
if (this->mScreen != nullptr) qscreen = this->mScreen;
|
||||
} else {
|
||||
qscreen = this->window->screen();
|
||||
}
|
||||
|
||||
return new QuickshellScreenInfo(
|
||||
const_cast<ProxyWindowBase*>(this), // NOLINT
|
||||
qscreen
|
||||
);
|
||||
}
|
||||
|
||||
QColor ProxyWindowBase::color() const {
|
||||
if (this->window == nullptr) return this->mColor;
|
||||
else return this->window->color();
|
||||
}
|
||||
|
||||
void ProxyWindowBase::setColor(QColor color) {
|
||||
if (this->window == nullptr) {
|
||||
this->mColor = color;
|
||||
emit this->colorChanged();
|
||||
} else this->window->setColor(color);
|
||||
}
|
||||
|
||||
PendingRegion* ProxyWindowBase::mask() const { return this->mMask; }
|
||||
|
||||
void ProxyWindowBase::setMask(PendingRegion* mask) {
|
||||
if (mask == this->mMask) return;
|
||||
|
||||
if (this->mMask != nullptr) {
|
||||
QObject::disconnect(this->mMask, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
this->mMask = mask;
|
||||
|
||||
if (mask != nullptr) {
|
||||
mask->setParent(this);
|
||||
QObject::connect(mask, &QObject::destroyed, this, &ProxyWindowBase::onMaskDestroyed);
|
||||
QObject::connect(mask, &PendingRegion::changed, this, &ProxyWindowBase::maskChanged);
|
||||
}
|
||||
|
||||
emit this->maskChanged();
|
||||
}
|
||||
|
||||
void ProxyWindowBase::onMaskChanged() {
|
||||
if (this->window != nullptr) this->updateMask();
|
||||
}
|
||||
|
||||
void ProxyWindowBase::onMaskDestroyed() {
|
||||
this->mMask = nullptr;
|
||||
emit this->maskChanged();
|
||||
}
|
||||
|
||||
void ProxyWindowBase::updateMask() {
|
||||
QRegion mask;
|
||||
if (this->mMask != nullptr) {
|
||||
// if left as the default, dont combine it with the whole window area, leave it as is.
|
||||
if (this->mMask->mIntersection == Intersection::Combine) {
|
||||
mask = this->mMask->build();
|
||||
} else {
|
||||
auto windowRegion = QRegion(QRect(0, 0, this->width(), this->height()));
|
||||
mask = this->mMask->applyTo(windowRegion);
|
||||
}
|
||||
}
|
||||
|
||||
this->window->setMask(mask);
|
||||
}
|
||||
|
||||
QQmlListProperty<QObject> ProxyWindowBase::data() {
|
||||
return this->mContentItem->property("data").value<QQmlListProperty<QObject>>();
|
||||
}
|
||||
|
||||
void ProxyWindowBase::onWidthChanged() { this->mContentItem->setWidth(this->width()); }
|
||||
void ProxyWindowBase::onHeightChanged() { this->mContentItem->setHeight(this->height()); }
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue