Compare commits
871 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8872002225 | ||
|
|
519a85b251 | ||
|
|
5aa7ff7e91 | ||
|
|
5ce5659b38 | ||
|
|
00459606ce | ||
|
|
da1081700a | ||
|
|
7e965262f5 | ||
|
|
19312d94c3 | ||
|
|
118323e6b5 | ||
|
|
aed81e82b0 | ||
|
|
76c167c2c2 | ||
|
|
852e2fa4d1 | ||
|
|
17dceffff6 | ||
|
|
b589f37e0b | ||
|
|
682c6af231 | ||
|
|
d71226c6bd | ||
|
|
1f3725faf8 | ||
|
|
efcec1b2f9 | ||
|
|
651322aef0 | ||
|
|
a0a3a58668 | ||
|
|
b3abe44d65 | ||
|
|
5b603472bd | ||
|
|
57b0fe8a21 | ||
|
|
f5561da3cc | ||
|
|
11f6475b9f | ||
|
|
fb2c5e0470 | ||
|
|
b1764fddc8 | ||
|
|
bb7f957e44 | ||
|
|
0682315c9d | ||
|
|
dd100597ed | ||
|
|
6bc6380ee1 | ||
|
|
966089e471 | ||
|
|
3956461254 | ||
|
|
02d114a05e | ||
|
|
3764edafa8 | ||
|
|
1cd0376381 | ||
|
|
2b154e2cdb | ||
|
|
933dfc402b | ||
|
|
34d037d7dc | ||
|
|
76b6626073 | ||
|
|
d348cfc2b0 | ||
|
|
f9d7de2e3c | ||
|
|
2ea00fffa5 | ||
|
|
b163dab241 | ||
|
|
af0f4818d8 | ||
|
|
8b6c7632af | ||
|
|
d6d51d24c9 | ||
|
|
0c6aea7154 | ||
|
|
a61b2edd07 | ||
|
|
c108e7707a | ||
|
|
f3123ba5b1 | ||
|
|
c09a93af48 | ||
|
|
2a262999ce | ||
|
|
7d952dc226 | ||
|
|
3cb838b455 | ||
|
|
7594651e05 | ||
|
|
0d611fc891 | ||
|
|
8982909fae | ||
|
|
132b331c7c | ||
|
|
85cef214c8 | ||
|
|
80b4dad199 | ||
|
|
aadbc9596d | ||
|
|
0949d154c1 | ||
|
|
a86a0d33c1 | ||
|
|
e6372a2473 | ||
|
|
e3d9ab5679 | ||
|
|
ccd7458ea3 | ||
|
|
d41b59d563 | ||
|
|
290ba4ac03 | ||
|
|
1ee14df915 | ||
|
|
76376a9783 | ||
|
|
880ac93662 | ||
|
|
1157c8e21d | ||
|
|
2082cfe7c7 | ||
|
|
9a9f2886e0 | ||
|
|
0035fbcc4e | ||
|
|
5ca2c2a095 | ||
|
|
46103062d0 | ||
|
|
78a41c236c | ||
|
|
9dfac69e9e | ||
|
|
de72236fe5 | ||
|
|
101e3125a9 | ||
|
|
b443c9f492 | ||
|
|
2a1e7832d6 | ||
|
|
8c815146e6 | ||
|
|
acae2b8c21 | ||
|
|
004836fc8f | ||
|
|
b51f2d16cb | ||
|
|
6fba9d9f22 | ||
|
|
335e38d461 | ||
|
|
ee50d84a53 | ||
|
|
e706dabef3 | ||
|
|
dcedae46e5 | ||
|
|
f27f9d35b0 | ||
|
|
4f5acb7114 | ||
|
|
25ba27cbdd | ||
|
|
74da975ed4 | ||
|
|
6f6a5b364a | ||
|
|
f670f88804 | ||
|
|
814cb774a6 | ||
|
|
50d8b54adf | ||
|
|
ae931b791f | ||
|
|
dd4641eedd | ||
|
|
b66bb46fc1 | ||
|
|
5079fc78d3 | ||
|
|
7d2eaa46e6 | ||
|
|
1043eaa39f | ||
|
|
f16798f6e3 | ||
|
|
96acb1a679 | ||
|
|
0f93797ab3 | ||
|
|
3186a84d6b | ||
|
|
d3ee66d845 | ||
|
|
a8837283ab | ||
|
|
6fe0784c00 | ||
|
|
59ce164b40 | ||
|
|
70144eb06f | ||
|
|
5136af5d95 | ||
|
|
ff42244c6d | ||
|
|
3a2bb40117 | ||
|
|
bcd3100849 | ||
|
|
c8886629ad | ||
|
|
be4a69f6e0 | ||
|
|
62b12d5436 | ||
|
|
99e75d51b8 | ||
|
|
079c8f0803 | ||
|
|
16f87cbfa3 | ||
|
|
380f31fbd9 | ||
|
|
28677d6888 | ||
|
|
07e94b0f0e | ||
|
|
307318918d | ||
|
|
3f6662182e | ||
|
|
be532fa146 | ||
|
|
722a59da80 | ||
|
|
3c97acf00f | ||
|
|
2d4fa59c41 | ||
|
|
c5ca758d3e | ||
|
|
6f70a98b83 | ||
|
|
424594a11a | ||
|
|
f5ac42c692 | ||
|
|
675f96d0e6 | ||
|
|
9570688294 | ||
|
|
626b745ce3 | ||
|
|
4afb98cf4c | ||
|
|
df2a9a246d | ||
|
|
d80e9ba3d5 | ||
|
|
130c68b3e2 | ||
|
|
6eea4a17a4 | ||
|
|
40dc8633ec | ||
|
|
12ac91d125 | ||
|
|
87d86911d7 | ||
|
|
2872a7b5c9 | ||
|
|
4067896434 | ||
|
|
78443451e4 | ||
|
|
719f5a20e7 | ||
|
|
d8b12e6d6b | ||
|
|
9a0746d737 | ||
|
|
77f8b3937c | ||
|
|
3f4313635a | ||
|
|
004d92a85d | ||
|
|
720c17258b | ||
|
|
a8b312f3a7 | ||
|
|
4d6361dfe5 | ||
|
|
1f75819795 | ||
|
|
50ddd2916c | ||
|
|
d30e14f611 | ||
|
|
227b0dd962 | ||
|
|
ac61086c95 | ||
|
|
0980f65751 | ||
|
|
7aa3da2ff4 | ||
|
|
83fbb8f95d | ||
|
|
a029463527 | ||
|
|
baafe54d13 | ||
|
|
a1cbd35202 | ||
|
|
1337a35a1e | ||
|
|
61006fbed0 | ||
|
|
eff4337d35 | ||
|
|
f0733f19dd | ||
|
|
818df48787 | ||
|
|
0eedfba071 | ||
|
|
2dc9e2f212 | ||
|
|
62a3b343cf | ||
|
|
76be93a84d | ||
|
|
b59c56170e | ||
|
|
c9285d8c5b | ||
|
|
b157d855a8 | ||
|
|
82ac49ce85 | ||
|
|
7247a26586 | ||
|
|
be0b568f1f | ||
|
|
e4b54e518c | ||
|
|
6ea1e2b4c7 | ||
|
|
5b4c57eae2 | ||
|
|
6e5efc3244 | ||
|
|
271a887bbf | ||
|
|
0571ba7325 | ||
|
|
c2f6c39016 | ||
|
|
2de2908509 | ||
|
|
99d56687ef | ||
|
|
434b8273f0 | ||
|
|
663382c81c | ||
|
|
3f388bdb4b | ||
|
|
0a4317f712 | ||
|
|
b9dbbf7bdd | ||
|
|
6ed9a8c5ae | ||
|
|
73de564bb6 | ||
|
|
1f62cdedb5 | ||
|
|
7ed0e894ec | ||
|
|
d39a9a85bf | ||
|
|
d16d1c1d26 | ||
|
|
291ffac102 | ||
|
|
2b18ed3c41 | ||
|
|
3b50efc7d0 | ||
|
|
d91a635781 | ||
|
|
4afe2d8448 | ||
|
|
74fce51c2d | ||
|
|
44cdbfe5d7 | ||
|
|
4fbb8314eb | ||
|
|
851a5a6f58 | ||
|
|
833808152e | ||
|
|
84706cab4b | ||
|
|
e571f26583 | ||
|
|
16cea533da | ||
|
|
b1f501f3f9 | ||
|
|
afcba942c7 | ||
|
|
5e6f77f875 | ||
|
|
1f9247c429 | ||
|
|
d089966249 | ||
|
|
b2d629e6a1 | ||
|
|
032087b611 | ||
|
|
22dd2bf75c | ||
|
|
7adc7f43cc | ||
|
|
a38f49cb35 | ||
|
|
ca7684c944 | ||
|
|
955369ab13 | ||
|
|
48f6c0705b | ||
|
|
43eec0e387 | ||
|
|
b1f9609cd3 | ||
|
|
4f731b67d1 | ||
|
|
6549b0fc57 | ||
|
|
ffb972f7c6 | ||
|
|
6ed2daa386 | ||
|
|
8a4042913b | ||
|
|
fb3c2f3bb2 | ||
|
|
5dc4ba504c | ||
|
|
c31dc75c63 | ||
|
|
1f0be929d7 | ||
|
|
56ea1f92fa | ||
|
|
5042d4d747 | ||
|
|
144406ae0e | ||
|
|
3d51f758f8 | ||
|
|
107f6fdfce | ||
|
|
e4d499b550 | ||
|
|
9c3726bdb1 | ||
|
|
a4534b1611 | ||
|
|
e4cad6ed20 | ||
|
|
5f1cfb9072 | ||
|
|
a6ccc8b0da | ||
|
|
fe139c208a | ||
|
|
64c1e842f9 | ||
|
|
cfd7dec04d | ||
|
|
a00676f5db | ||
|
|
61cf7ab843 | ||
|
|
d76d1c628a | ||
|
|
f6b3f6d2ec | ||
|
|
24863c2527 | ||
|
|
ecd6141739 | ||
|
|
ee44920fa4 | ||
|
|
864cbfcfab | ||
|
|
73541eec49 | ||
|
|
87425efa88 | ||
|
|
4455074493 | ||
|
|
5e23476089 | ||
|
|
1232c0268c | ||
|
|
ed9ee65885 | ||
|
|
bc7fe21d27 | ||
|
|
f7b0a28b1e | ||
|
|
b422a419cd | ||
|
|
663f3abff5 | ||
|
|
3c9ce6f8b5 | ||
|
|
c9a128e439 | ||
|
|
e8f356f5ac | ||
|
|
94d64a91b8 | ||
|
|
fdfe9ea2e1 | ||
|
|
d4d7b06b64 | ||
|
|
56d87ecfcf | ||
|
|
76ef2469e8 | ||
|
|
16bd4b41dc | ||
|
|
0e4b79fd16 | ||
|
|
ad73f11b69 | ||
|
|
bacd65b274 | ||
|
|
1f8c55d581 | ||
|
|
ccdb4e0664 | ||
|
|
c77784b5c1 | ||
|
|
74cf71755b | ||
|
|
a4107c87c0 | ||
|
|
8da2cdf430 | ||
|
|
7e93e29f66 | ||
|
|
b2e11137d4 | ||
|
|
d086d64d5f | ||
|
|
fa970986dc | ||
|
|
4c9e89915e | ||
|
|
97c7fd8073 | ||
|
|
29167de546 | ||
|
|
d6f629d4bb | ||
|
|
b13c40e238 | ||
|
|
170fbea7a4 | ||
|
|
08d2747f1e | ||
|
|
b91112fc7a | ||
|
|
ea6b8e0c02 | ||
|
|
404a1d3e8b | ||
|
|
6f1b88e76d | ||
|
|
6169f88d90 | ||
|
|
6f4a4bb764 | ||
|
|
242ae17d0a | ||
|
|
736979c4dc | ||
|
|
4b775fc29d | ||
|
|
ed78b6b3f5 | ||
|
|
a0494d1759 | ||
|
|
1c0c4e955a | ||
|
|
b639c3632d | ||
|
|
6c93b1b768 | ||
|
|
d05255c15b | ||
|
|
59ef26af1c | ||
|
|
33c6ade8f8 | ||
|
|
8c115b8bb0 | ||
|
|
3271fa1d23 | ||
|
|
b43b065cf2 | ||
|
|
66a4618d09 | ||
|
|
983e3c5cbe | ||
|
|
c02d3e3d22 | ||
|
|
c0900b105b | ||
|
|
b6166a2a7c | ||
|
|
38928abab7 | ||
|
|
849f3c52d7 | ||
|
|
f9e55c8f8d | ||
|
|
993a7965fd | ||
|
|
d4f6462e8a | ||
|
|
8bfde2f6d8 | ||
|
|
b3eea2215d | ||
|
|
4d7bc811c4 | ||
|
|
74ec5ea606 | ||
|
|
dda0266798 | ||
|
|
99d9dbe218 | ||
|
|
89c7f05782 | ||
|
|
d9c36a81c4 | ||
|
|
91747c71f2 | ||
|
|
5a1231a17e | ||
|
|
517c7c97d4 | ||
|
|
45af873a6f | ||
|
|
c01167c9da | ||
|
|
a68b3f49b0 | ||
|
|
e03042c411 | ||
|
|
3065bec6c9 | ||
|
|
dae1d12b6f | ||
|
|
c4846cd977 | ||
|
|
f95c9b76d4 | ||
|
|
fb01392bc3 | ||
|
|
498ee478e7 | ||
|
|
53ff6cc21a | ||
|
|
ba33451957 | ||
|
|
d6e253fe7f | ||
|
|
c32a8a863a | ||
|
|
4ba0f8d958 | ||
|
|
e4e2ed41b4 | ||
|
|
888ba108e0 | ||
|
|
c14eb95dba | ||
|
|
dc0ef93680 | ||
|
|
a2ea3c116d | ||
|
|
4578aad0bc | ||
|
|
57448f100c | ||
|
|
835f88d71e | ||
|
|
291d919b9f | ||
|
|
adac96ee84 | ||
|
|
9010a1668b | ||
|
|
f27608947c | ||
|
|
fb2d42da57 | ||
|
|
2bc1d53b18 | ||
|
|
36d3a50f21 | ||
|
|
9bc6479c92 | ||
|
|
56993d3c00 | ||
|
|
86c6135def | ||
|
|
1bb1015fdf | ||
|
|
ac43b6d78a | ||
|
|
809f16c27e | ||
|
|
7860c41959 | ||
|
|
fc1ee9fb2f | ||
|
|
5bc8f410e7 | ||
|
|
3d9ef8c2ed | ||
|
|
0e53ce3ac0 | ||
|
|
4131e6503b | ||
|
|
0aaf78fc51 | ||
|
|
977b2d9e7c | ||
|
|
e76b2c5497 | ||
|
|
8658e11c1d | ||
|
|
b3e4486699 | ||
|
|
2398961473 | ||
|
|
a57bfeba31 | ||
|
|
2f416a87f0 | ||
|
|
9a6c98c134 | ||
|
|
35ca346246 | ||
|
|
0fd9ac15cd | ||
|
|
ae12d77e29 | ||
|
|
9065257961 | ||
|
|
561b55cb9e | ||
|
|
4f871296ae | ||
|
|
55b74ad38f | ||
|
|
8426e36f46 | ||
|
|
85d94aca01 | ||
|
|
39c7089cbc | ||
|
|
eb072ff88a | ||
|
|
0c4046b993 | ||
|
|
90cd5467fe | ||
|
|
05bfb6fc37 | ||
|
|
966b2410d3 | ||
|
|
8ec1ad7255 | ||
|
|
1efa1f4aa3 | ||
|
|
0a48e5f34f | ||
|
|
ad305b3754 | ||
|
|
78cb7d4c15 | ||
|
|
7b5c97f38a | ||
|
|
59bf98e04c | ||
|
|
7feab63e5b | ||
|
|
5d7e168a57 | ||
|
|
8038b7f6a0 | ||
|
|
2533c52e27 | ||
|
|
cf624f4d65 | ||
|
|
a4c98f1382 | ||
|
|
4768485974 | ||
|
|
9a14a5cc10 | ||
|
|
cbffc1a14c | ||
|
|
25e1c6e759 | ||
|
|
e41c35cb5b | ||
|
|
078e111ecd | ||
|
|
01aeceddf4 | ||
|
|
93a3bc2090 | ||
|
|
28b0536916 | ||
|
|
86734f17c4 | ||
|
|
94293e4c63 | ||
|
|
f06d0f4e1e | ||
|
|
a5fc9d9ca9 | ||
|
|
c85a309aeb | ||
|
|
4cac584409 | ||
|
|
b30d3df15c | ||
|
|
6f69654816 | ||
|
|
8fedd7612d | ||
|
|
c16e6e7423 | ||
|
|
c8a056f332 | ||
|
|
60950fb461 | ||
|
|
a3aba8d0db | ||
|
|
f9a48becce | ||
|
|
3140039ccb | ||
|
|
56fedcf495 | ||
|
|
783e9fb140 | ||
|
|
b69d6f57d4 | ||
|
|
125d844e3b | ||
|
|
f04ac180f0 | ||
|
|
1cab452352 | ||
|
|
f3d1d15b61 | ||
|
|
0915071299 | ||
|
|
b787080715 | ||
|
|
a69a6eda4d | ||
|
|
dd757c2114 | ||
|
|
eedea01679 | ||
|
|
0567da94dd | ||
|
|
de92c989f2 | ||
|
|
507843be21 | ||
|
|
b9c1a8a54f | ||
|
|
35283a6923 | ||
|
|
9ae78eda45 | ||
|
|
cc8a24f445 | ||
|
|
5910c65bcf | ||
|
|
e5aee79d47 | ||
|
|
a249e15c58 | ||
|
|
cdcfe328d2 | ||
|
|
784300f690 | ||
|
|
8ad2bef2f5 | ||
|
|
2bd30947fc | ||
|
|
84e8793a29 | ||
|
|
be1643c5b8 | ||
|
|
b00f058eac | ||
|
|
8bab23cfec | ||
|
|
7b26e38f33 | ||
|
|
9e6bd3be76 | ||
|
|
97b016b21b | ||
|
|
84fdb7c647 | ||
|
|
6d70944fc8 | ||
|
|
fcb4fa1b59 | ||
|
|
e69086f1a6 | ||
|
|
fa22607c2c | ||
|
|
9168eba07b | ||
|
|
5d11e37687 | ||
|
|
4ea903b333 | ||
|
|
8fd805815d | ||
|
|
c055690a9b | ||
|
|
dcf146a097 | ||
|
|
e3f50c0ce2 | ||
|
|
c394368dc5 | ||
|
|
1f9c54438a | ||
|
|
f303f305af | ||
|
|
5f1f3dce4a | ||
|
|
f84889ca13 | ||
|
|
b778a80c79 | ||
|
|
0bf632a4b1 | ||
|
|
321c513682 | ||
|
|
9db6a0d438 | ||
|
|
a9affb5ae4 | ||
|
|
46bc8939b4 | ||
|
|
fe6ecf7daf | ||
|
|
a72b896c5f | ||
|
|
a91d790074 | ||
|
|
ac21deefa4 | ||
|
|
37eefe3663 | ||
|
|
2e082ed8b1 | ||
|
|
c1bec66151 | ||
|
|
d53a404bf1 | ||
|
|
7ed4c209fe | ||
|
|
83205d57d9 | ||
|
|
43bb3bdd0c | ||
|
|
cde3f088d1 | ||
|
|
3e7ebf44f3 | ||
|
|
ac7092943c | ||
|
|
e7bbb7fc00 | ||
|
|
2fda29c185 | ||
|
|
2793863689 | ||
|
|
7fafda4747 | ||
|
|
bb0f1e84ce | ||
|
|
3ceba43802 | ||
|
|
f8ed4f48cf | ||
|
|
d319ab9bfc | ||
|
|
902cdc39e0 | ||
|
|
00d3f81aa1 | ||
|
|
d8c91a942f | ||
|
|
30e1c2d2b3 | ||
|
|
4229721774 | ||
|
|
4a45e73125 | ||
|
|
9e819084af | ||
|
|
f39dd2aa1c | ||
|
|
4f3e0bdb1e | ||
|
|
21383b03c5 | ||
|
|
17944211d5 | ||
|
|
1f919e4469 | ||
|
|
06a11f003b | ||
|
|
807867ef42 | ||
|
|
598bc48957 | ||
|
|
7f34ca4122 | ||
|
|
291cd5130d | ||
|
|
280952aae3 | ||
|
|
3ba6899e69 | ||
|
|
65f73bb1ba | ||
|
|
392f0e14b2 | ||
|
|
1e81a89a1a | ||
|
|
11a13ce589 | ||
|
|
24620210fe | ||
|
|
7b2d490ba7 | ||
|
|
20b29f98a7 | ||
|
|
132dbce3a3 | ||
|
|
ded133d164 | ||
|
|
7548ffc191 | ||
|
|
1599ee5682 | ||
|
|
40b57c2df0 | ||
|
|
c6e56d4264 | ||
|
|
7141a91994 | ||
|
|
742a600e38 | ||
|
|
80a2e69eaa | ||
|
|
a7ce6737ec | ||
|
|
dfd7edc540 | ||
|
|
ac65d19809 | ||
|
|
520da3e915 | ||
|
|
d79011355c | ||
|
|
5859270ad0 | ||
|
|
26dc143b1d | ||
|
|
76a8e644e0 | ||
|
|
8d05cb9f3b | ||
|
|
7f5d70bcc8 | ||
|
|
eea1586772 | ||
|
|
6740959866 | ||
|
|
f9c1fa78aa | ||
|
|
f385b24e8c | ||
|
|
508c1407be | ||
|
|
b908dc0ed2 | ||
|
|
6c041fb27f | ||
|
|
63a545736c | ||
|
|
91b355689c | ||
|
|
3e598cf1cd | ||
|
|
9781005a21 | ||
|
|
468272d4c9 | ||
|
|
d5e83aa9de | ||
|
|
69a5f0c2c0 | ||
|
|
f9194dd741 | ||
|
|
fff7cbde22 | ||
|
|
b796a00374 | ||
|
|
cb7b1d92c6 | ||
|
|
fac72b257c | ||
|
|
e6a1bc6e27 | ||
|
|
65794b52ec | ||
|
|
9a4317739b | ||
|
|
de32b86f7c | ||
|
|
5a1faa0fd4 | ||
|
|
57d912efc8 | ||
|
|
87067f7062 | ||
|
|
210bbac583 | ||
|
|
81d3bad747 | ||
|
|
2ddb14a95f | ||
|
|
934c8c61b3 | ||
|
|
459bb59dd5 | ||
|
|
f1c9ed9caa | ||
|
|
5d950b0a5e | ||
|
|
6f78079bc5 | ||
|
|
e3d62388f7 | ||
|
|
5fef9cfe6b | ||
|
|
4a4bec5aec | ||
|
|
4193d3c87c | ||
|
|
0fd83498ea | ||
|
|
00c94755c5 | ||
|
|
e8c2042290 | ||
|
|
6bcb85137b | ||
|
|
d910f30ed1 | ||
|
|
3e8a87c6d6 | ||
|
|
ecb7a9d448 | ||
|
|
40edc38756 | ||
|
|
d1f5d301c2 | ||
|
|
102aca0fa0 | ||
|
|
b0917f5a25 | ||
|
|
5488063490 | ||
|
|
ad125d7af9 | ||
|
|
4510762a35 | ||
|
|
c7ee627110 | ||
|
|
330eac08cb | ||
|
|
bb1d56121d | ||
|
|
3683d3c29b | ||
|
|
0736862a2c | ||
|
|
3891c7008a | ||
|
|
5c729b25b4 | ||
|
|
3151b1634c | ||
|
|
2498f0273d | ||
|
|
40579e1b80 | ||
|
|
2bd6d23467 | ||
|
|
eb7401d693 | ||
|
|
0f5bbb961d | ||
|
|
fa82dea4d5 | ||
|
|
46ef2b6e53 | ||
|
|
4a799b755f | ||
|
|
dda031e73b | ||
|
|
2f8472f720 | ||
|
|
4520ed3cbf | ||
|
|
1ecc3d9744 | ||
|
|
fcf627c30b | ||
|
|
fdf67ab512 | ||
|
|
b1daf2e8bc | ||
|
|
6ecbdda121 | ||
|
|
3f0374e1f2 | ||
|
|
cb345c2364 | ||
|
|
d0b957e998 | ||
|
|
7a2fa4a773 | ||
|
|
8a198fd707 | ||
|
|
7ba3870c82 | ||
|
|
ff0c83a04c | ||
|
|
9a8046b99f | ||
|
|
92b37df962 | ||
|
|
ab4359b624 | ||
|
|
ab5b877dc3 | ||
|
|
23a41ff3c6 | ||
|
|
68a44b6ef7 | ||
|
|
51ea837cd0 | ||
|
|
6f2d5c2752 | ||
|
|
d912c2a090 | ||
|
|
21876857fc | ||
|
|
53405c13af | ||
|
|
abb5f385d9 | ||
|
|
4ad851fdd2 | ||
|
|
8509845381 | ||
|
|
d7eea7fdae | ||
|
|
8395b2640e | ||
|
|
1eae0eb3d4 | ||
|
|
91ffa4a9fd | ||
|
|
58b93c9d22 | ||
|
|
6d2f4d51a2 | ||
|
|
7b63b6900d | ||
|
|
1e52e7ca40 | ||
|
|
2ebdc74f15 | ||
|
|
724e55c37d | ||
|
|
51f1923e22 | ||
|
|
714f6c058f | ||
|
|
560f601190 | ||
|
|
6deb039906 | ||
|
|
f19eaf689b | ||
|
|
80f6570f04 | ||
|
|
d883096971 | ||
|
|
87f9afbd85 | ||
|
|
9bb5241e49 | ||
|
|
65601cb855 | ||
|
|
ef86570b24 | ||
|
|
821c262a93 | ||
|
|
1c323675d1 | ||
|
|
2c9e675ba4 | ||
|
|
2d5bebb969 | ||
|
|
a97913fd63 | ||
|
|
9264306a36 | ||
|
|
d2ac174427 | ||
|
|
d5a8a0d72f | ||
|
|
36bfbe10ab | ||
|
|
7ace02dd46 | ||
|
|
125a3ace08 | ||
|
|
3c7d03ada9 | ||
|
|
477d38d928 | ||
|
|
d36bcb1d4d | ||
|
|
4c79999a65 | ||
|
|
cdfed0fe94 | ||
|
|
6af915983c | ||
|
|
da266792df | ||
|
|
91afdf7f13 | ||
|
|
26fc6098dc | ||
|
|
3496169c68 | ||
|
|
299add4a15 | ||
|
|
5ab76c98e5 | ||
|
|
f5b4984295 | ||
|
|
a38665fa0d | ||
|
|
cf27ff10c0 | ||
|
|
8f3f520ef4 | ||
|
|
c4e4f78336 | ||
|
|
2f9eb28596 | ||
|
|
63e90a5c17 | ||
|
|
61d13a6cab | ||
|
|
a2ecc67643 | ||
|
|
f679999453 | ||
|
|
5b8d7dbff5 | ||
|
|
9bbdf5f6f6 | ||
|
|
812ddf2ebb | ||
|
|
db3ea7ed73 | ||
|
|
c37ef867a1 | ||
|
|
7c6c908076 | ||
|
|
c510afdc28 | ||
|
|
861e207fb6 | ||
|
|
c601e45436 | ||
|
|
e79c163dd9 | ||
|
|
c770b97649 | ||
|
|
5dedf5c1b5 | ||
|
|
85bd0ed2f8 | ||
|
|
cd6a183c28 | ||
|
|
3cc8c8fb03 | ||
|
|
42408572ab | ||
|
|
c3956c5894 | ||
|
|
b2e9058a2f | ||
|
|
bc28b11763 | ||
|
|
cbd71bec49 | ||
|
|
6ac172fe02 | ||
|
|
8ebcfa4bc6 | ||
|
|
3f4cec1719 | ||
|
|
156146fd9a | ||
|
|
e86e7344f3 | ||
|
|
8d9f206c45 | ||
|
|
39d8d8bcfa | ||
|
|
a719db4d0d | ||
|
|
a845067cf0 | ||
|
|
82d71d65fa | ||
|
|
a699cfb958 | ||
|
|
92b24c6eb2 | ||
|
|
d57092feae | ||
|
|
6c4b495a75 | ||
|
|
d0b7ccf302 | ||
|
|
e237bd04ff | ||
|
|
2a686b55c4 | ||
|
|
cdc3b18071 | ||
|
|
c8860a3a9d | ||
|
|
57a67bf4df | ||
|
|
f932d580af | ||
|
|
eadcb3f22b | ||
|
|
a6d722f9a9 | ||
|
|
f10280c8bb | ||
|
|
a6848be4c2 | ||
|
|
f510c1922d | ||
|
|
0562dbbbf9 | ||
|
|
85b92d9c6f | ||
|
|
de465ebcba | ||
|
|
8302285388 | ||
|
|
b502161b11 | ||
|
|
1206be34dc | ||
|
|
c6cf5a0fab | ||
|
|
d6df496216 | ||
|
|
68874e8680 | ||
|
|
67f0c482b3 | ||
|
|
1ceec16102 | ||
|
|
9d03006aaa | ||
|
|
f7f21f9716 | ||
|
|
124d9becc6 | ||
|
|
50777ef32f | ||
|
|
563a151277 | ||
|
|
6f7528c87a | ||
|
|
ae0228dc25 | ||
|
|
a1f87c50bc | ||
|
|
74e65d75cb | ||
|
|
56967d4c0c | ||
|
|
2950862e34 | ||
|
|
f4ecdd5af3 | ||
|
|
dd456edf90 | ||
|
|
e1f1addb35 | ||
|
|
94e59592f0 | ||
|
|
4cd94f0426 | ||
|
|
c99f470e34 | ||
|
|
45b0fbeb4a | ||
|
|
76f0368a64 | ||
|
|
da80f47921 | ||
|
|
fa7c19d5de | ||
|
|
74f54b3d76 | ||
|
|
2f49643e51 | ||
|
|
fabdf67da7 | ||
|
|
fc71f61000 | ||
|
|
aa8a72a9d8 | ||
|
|
a1dcaa2683 | ||
|
|
f533e2a547 | ||
|
|
496ca05b9d | ||
|
|
f399463a99 | ||
|
|
44c98553dc | ||
|
|
57a8f5f0b3 | ||
|
|
ab7c5678c6 | ||
|
|
c323985f03 | ||
|
|
024f35496c | ||
|
|
4d91096ab9 | ||
|
|
8148c0fa29 | ||
|
|
620b3e3abc | ||
|
|
22af8e91cc | ||
|
|
9dcefa4357 | ||
|
|
634d78456d | ||
|
|
8140ddc2ff | ||
|
|
71cfbc8c0a | ||
|
|
6a9dee38ef | ||
|
|
9ee31e3a6a | ||
|
|
7379bcb5b6 | ||
|
|
246c475dbe | ||
|
|
864631f967 | ||
|
|
4fe5681917 | ||
|
|
fe8e5a0464 | ||
|
|
7c914b88a3 | ||
|
|
256cd06e5c | ||
|
|
f3ae0101d7 | ||
|
|
52efc632f8 | ||
|
|
f3f0f611cb | ||
|
|
07fcd29842 | ||
|
|
863107670c | ||
|
|
c2ca05b117 | ||
|
|
3c39ea192b | ||
|
|
1533b2d3a1 | ||
|
|
7bcb227d7b | ||
|
|
178ad2ac8a | ||
|
|
bdd981e15c | ||
|
|
a61526543d | ||
|
|
1ab3463e6d | ||
|
|
269b2765cd | ||
|
|
d2563db5a0 | ||
|
|
fcedb65119 | ||
|
|
18b79913bd | ||
|
|
9fb4aff635 | ||
|
|
38efdc8f36 | ||
|
|
75700e3309 | ||
|
|
48e57a2122 | ||
|
|
d791705afa | ||
|
|
38e3d9909f | ||
|
|
2a234f5a88 | ||
|
|
5f00266df7 | ||
|
|
c917d7dccb | ||
|
|
54c39ab8a3 | ||
|
|
b19fb316d9 | ||
|
|
7a849806fb | ||
|
|
01ccb771e6 | ||
|
|
c6683712a4 | ||
|
|
d2b202c25f | ||
|
|
dfbefcd9d2 | ||
|
|
938c4b2d25 | ||
|
|
81182aa65b | ||
|
|
487158739d | ||
|
|
7899b124b7 | ||
|
|
e1d623be9c |
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Report a bug from noctalia-shell
|
||||
title: "[Bug] "
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
### Description
|
||||
A clear and concise description of the bug.
|
||||
|
||||
### Steps to Reproduce
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
3. See the error.
|
||||
|
||||
### Expected Behavior
|
||||
Explain what you expected to happen.
|
||||
|
||||
### Screenshots
|
||||
Add screenshots if applicable.
|
||||
|
||||
### Environment
|
||||
- Distro: [e.g., CachyOS, NixOS, Arch, ...]
|
||||
- Compositor: [ e.g., Hyprland, Niri, ...]
|
||||
- Noctalia-shell Version: [e.g., 1.0.0, available in About tab]
|
||||
|
||||
### Additional Context
|
||||
Add any other context about the problem here.
|
||||
12
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
blank_issues_enabled: false
|
||||
issue_templates:
|
||||
- name: "Bug Report"
|
||||
description: "Report a bug in the system."
|
||||
title: "[Bug]: "
|
||||
labels: ["bug"]
|
||||
body: "./ISSUE_TEMPLATE/bug_report.md"
|
||||
- name: "Feature Request"
|
||||
description: "Propose a new feature or improvement."
|
||||
title: "[Feature]: "
|
||||
labels: ["enhancement"]
|
||||
body: "./ISSUE_TEMPLATE/feature_request.md"
|
||||
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest a new feature or improvement
|
||||
title: "[Feature] "
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
### Feature Description
|
||||
What feature would you like to see?
|
||||
|
||||
### Why Is This Needed?
|
||||
Explain the problem or need for this feature.
|
||||
|
||||
### Suggested Solutions
|
||||
Describe how this feature could be implemented.
|
||||
|
||||
### Additional Context
|
||||
Add any relevant screenshots, links, or resources.
|
||||
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.qmlls.ini
|
||||
34
Assets/ColorScheme/Monochrome.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"dark": {
|
||||
"mPrimary": "#aaaaaa",
|
||||
"mOnPrimary": "#111111",
|
||||
"mSecondary": "#a7a7a7",
|
||||
"mOnSecondary": "#111111",
|
||||
"mTertiary": "#cccccc",
|
||||
"mOnTertiary": "#111111",
|
||||
"mError": "#dddddd",
|
||||
"mOnError": "#111111",
|
||||
"mSurface": "#111111",
|
||||
"mOnSurface": "#828282",
|
||||
"mSurfaceVariant": "#191919",
|
||||
"mOnSurfaceVariant": "#5d5d5d",
|
||||
"mOutline": "#3c3c3c",
|
||||
"mShadow": "#000000"
|
||||
},
|
||||
"light": {
|
||||
"mPrimary": "#555555",
|
||||
"mOnPrimary": "#eeeeee",
|
||||
"mSecondary": "#505058",
|
||||
"mOnSecondary": "#eeeeee",
|
||||
"mTertiary": "#333333",
|
||||
"mOnTertiary": "#eeeeee",
|
||||
"mError": "#222222",
|
||||
"mOnError": "#efefef",
|
||||
"mSurface": "#d4d4d4",
|
||||
"mOnSurface": "#696969",
|
||||
"mSurfaceVariant": "#e8e8e8",
|
||||
"mOnSurfaceVariant": "#9e9e9e",
|
||||
"mOutline": "#c3c3c3",
|
||||
"mShadow": "#fafafa"
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,34 @@
|
||||
{
|
||||
"dark": {
|
||||
"mPrimary": "#ebbcba",
|
||||
"mOnPrimary": "#1f1d2e",
|
||||
"mOnPrimary": "#191724",
|
||||
"mSecondary": "#9ccfd8",
|
||||
"mOnSecondary": "#1f1d2e",
|
||||
"mTertiary": "#f6c177",
|
||||
"mOnTertiary": "#1f1d2e",
|
||||
"mOnSecondary": "#191724",
|
||||
"mTertiary": "#524f67",
|
||||
"mOnTertiary": "#e0def4",
|
||||
"mError": "#eb6f92",
|
||||
"mOnError": "#1f1d2e",
|
||||
"mSurface": "#1f1d2e",
|
||||
"mOnError": "#191724",
|
||||
"mSurface": "#191724",
|
||||
"mOnSurface": "#e0def4",
|
||||
"mSurfaceVariant": "#26233a",
|
||||
"mOnSurfaceVariant": "#908caa",
|
||||
"mOutline": "#403d52",
|
||||
"mShadow": "#1f1d2e"
|
||||
"mShadow": "#191724"
|
||||
},
|
||||
"light": {
|
||||
"mPrimary": "#d46e6b",
|
||||
"mPrimary": "#d7827e",
|
||||
"mOnPrimary": "#faf4ed",
|
||||
"mSecondary": "#56949f",
|
||||
"mOnSecondary": "#faf4ed",
|
||||
"mTertiary": "#31748f",
|
||||
"mOnTertiary": "#232136",
|
||||
"mTertiary": "#cecacd",
|
||||
"mOnTertiary": "#575279",
|
||||
"mError": "#b4637a",
|
||||
"mOnError": "#f2e9e1",
|
||||
"mSurface": "#e0def4",
|
||||
"mOnSurface": "#232136",
|
||||
"mSurfaceVariant": "#bcb8e7",
|
||||
"mOnError": "#faf4ed",
|
||||
"mSurface": "#faf4ed",
|
||||
"mOnSurface": "#575279",
|
||||
"mSurfaceVariant": "#f2e9e1",
|
||||
"mOnSurfaceVariant": "#797593",
|
||||
"mOutline": "#9893a5",
|
||||
"mShadow": "#575279"
|
||||
"mOutline": "#dfdad9",
|
||||
"mShadow": "#faf4ed"
|
||||
}
|
||||
}
|
||||
|
||||
16
Assets/Fonts/tabler/tabler-icons-license.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
Tabler Licenses - Detailed Usage Rights and Guidelines
|
||||
|
||||
This is a legal agreement between you, the Purchaser, and Tabler. Purchasing or downloading of any Tabler product (Tabler Admin Template, Tabler Icons, Tabler Emails, Tabler Illustrations), constitutes your acceptance of the terms of this license, Tabler terms of service and Tabler private policy.
|
||||
|
||||
Tabler Admin Template and Tabler Icons License*
|
||||
Tabler Admin Template and Tabler Icons are available under MIT License.
|
||||
|
||||
Copyright (c) 2018-2025 Tabler
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
See more at Tabler Admin Template MIT License See more at Tabler Icons MIT License
|
||||
BIN
Assets/Fonts/tabler/tabler-icons.ttf
Normal file
76
Assets/Matugen/Matugen.qml
Normal file
@@ -0,0 +1,76 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
|
||||
// Central place to define which templates we generate and where they write.
|
||||
// Users can extend it by dropping additional templates into:
|
||||
// - Assets/Matugen/templates/
|
||||
// - ~/.config/matugen/ (when enableUserTemplates is true)
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Build the base TOML using current settings
|
||||
function buildConfigToml() {
|
||||
var lines = []
|
||||
lines.push("[config]")
|
||||
|
||||
// Always include noctalia colors output for the shell
|
||||
lines.push("[templates.noctalia]")
|
||||
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/noctalia.json"')
|
||||
lines.push('output_path = "' + Settings.configDir + 'colors.json"')
|
||||
|
||||
if (Settings.data.matugen.gtk4) {
|
||||
lines.push("\n[templates.gtk4]")
|
||||
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/gtk4.css"')
|
||||
lines.push('output_path = "~/.config/gtk-4.0/gtk.css"')
|
||||
}
|
||||
if (Settings.data.matugen.gtk3) {
|
||||
lines.push("\n[templates.gtk3]")
|
||||
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/gtk3.css"')
|
||||
lines.push('output_path = "~/.config/gtk-3.0/gtk.css"')
|
||||
}
|
||||
if (Settings.data.matugen.qt6) {
|
||||
lines.push("\n[templates.qt6]")
|
||||
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/qtct.conf"')
|
||||
lines.push('output_path = "~/.config/qt6ct/colors/noctalia.conf"')
|
||||
}
|
||||
if (Settings.data.matugen.qt5) {
|
||||
lines.push("\n[templates.qt5]")
|
||||
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/qtct.conf"')
|
||||
lines.push('output_path = "~/.config/qt5ct/colors/noctalia.conf"')
|
||||
}
|
||||
if (Settings.data.matugen.kitty) {
|
||||
lines.push("\n[templates.kitty]")
|
||||
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/kitty.conf"')
|
||||
lines.push('output_path = "~/.config/kitty/themes/noctalia.conf"')
|
||||
lines.push("post_hook = 'kitty +kitten themes --reload-in=all noctalia'")
|
||||
}
|
||||
if (Settings.data.matugen.ghostty) {
|
||||
lines.push("\n[templates.ghostty]")
|
||||
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/ghostty.conf"')
|
||||
lines.push('output_path = "~/.config/ghostty/themes/noctalia"')
|
||||
lines.push("post_hook = \"grep -q '^theme *= *' ~/.config/ghostty/config; and sed -i 's/^theme *= *.*/theme = noctalia/' ~/.config/ghostty/config; or echo 'theme = noctalia' >> ~/.config/ghostty/config\"")
|
||||
}
|
||||
if (Settings.data.matugen.foot) {
|
||||
lines.push("\n[templates.foot]")
|
||||
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/foot.conf"')
|
||||
lines.push('output_path = "~/.config/foot/themes/noctalia"')
|
||||
lines.push('post_hook = "sed -i /themes/d ~/.config/foot/foot.ini && echo include=~/.config/foot/themes/noctalia >> ~/.config/foot/foot.ini"')
|
||||
}
|
||||
if (Settings.data.matugen.fuzzel) {
|
||||
lines.push("\n[templates.fuzzel]")
|
||||
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/fuzzel.conf"')
|
||||
lines.push('output_path = "~/.config/fuzzel/themes/noctalia"')
|
||||
lines.push('post_hook = "sed -i /themes/d ~/.config/fuzzel/fuzzel.ini && echo include=~/.config/fuzzel/themes/noctalia >> ~/.config/fuzzel/fuzzel.ini"')
|
||||
}
|
||||
if (Settings.data.matugen.vesktop) {
|
||||
lines.push("\n[templates.vesktop]")
|
||||
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/vesktop.css"')
|
||||
lines.push('output_path = "~/.config/vesktop/themes/noctalia.theme.css"')
|
||||
}
|
||||
|
||||
return lines.join("\n") + "\n"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
# Base: only write Noctalia colors.json for the shell
|
||||
[config]
|
||||
|
||||
[templates.noctalia]
|
||||
input_path = "templates/noctalia.json"
|
||||
output_path = "~/.config/noctalia/colors.json"
|
||||
@@ -1,30 +0,0 @@
|
||||
# This file configures how matugen generates colors from wallpapers for Noctalia
|
||||
[config]
|
||||
|
||||
|
||||
[templates.noctalia]
|
||||
input_path = "templates/noctalia.json"
|
||||
output_path = "~/.config/noctalia/colors.json"
|
||||
|
||||
# GTK 4 (libadwaita) variables override
|
||||
[templates.gtk4]
|
||||
input_path = "templates/gtk4.css"
|
||||
output_path = "~/.config/gtk-4.0/gtk.css"
|
||||
|
||||
# GTK 3 named-colors fallback for legacy apps
|
||||
[templates.gtk3]
|
||||
input_path = "templates/gtk3.css"
|
||||
output_path = "~/.config/gtk-3.0/gtk.css"
|
||||
|
||||
# Qt6ct color scheme (can also be used by qt5ct in many distros)
|
||||
[templates.qt6]
|
||||
input_path = "templates/qtct.conf"
|
||||
output_path = "~/.config/qt6ct/colors/noctalia.conf"
|
||||
|
||||
[templates.qt5]
|
||||
input_path = "templates/qtct.conf"
|
||||
output_path = "~/.config/qt5ct/colors/noctalia.conf"
|
||||
|
||||
[templates.kitty]
|
||||
input_path = "templates/kitty.conf"
|
||||
output_path = "~/.config/kitty/noctalia.conf"
|
||||
30
Assets/Matugen/templates/foot.conf
Normal file
@@ -0,0 +1,30 @@
|
||||
[colors]
|
||||
background={{ colors.background.default.hex_stripped }}
|
||||
foreground={{ colors.on_surface.default.hex_stripped }}
|
||||
regular0={{ colors.surface.default.hex_stripped }}
|
||||
regular1={{ colors.error.default.hex_stripped }}
|
||||
regular2={{ colors.primary.default.hex_stripped }}
|
||||
regular3={{ colors.tertiary.default.hex_stripped }}
|
||||
regular4={{ colors.on_primary_container.default.hex_stripped }}
|
||||
regular5={{ colors.on_secondary_container.default.hex_stripped }}
|
||||
regular6={{ colors.secondary.default.hex_stripped }}
|
||||
regular7={{ colors.on_surface.default.hex_stripped }}
|
||||
bright0={{ colors.surface_bright.default.hex_stripped }}
|
||||
bright1={{ colors.error.default.hex_stripped }}
|
||||
bright2={{ colors.primary.default.hex_stripped }}
|
||||
bright3={{ colors.tertiary.default.hex_stripped }}
|
||||
bright4={{ colors.on_primary_container.default.hex_stripped }}
|
||||
bright5={{ colors.on_secondary_container.default.hex_stripped }}
|
||||
bright6={{ colors.secondary.default.hex_stripped }}
|
||||
bright7={{ colors.on_surface.default.hex_stripped }}
|
||||
dim0=45475A
|
||||
dim1=F38BA8
|
||||
dim2=A6E3A1
|
||||
dim3=F9E2AF
|
||||
dim4=89B4FA
|
||||
dim5=F5C2E7
|
||||
dim6=94E2D5
|
||||
dim7=BAC2DE
|
||||
selection-foreground={{ colors.primary.default.hex_stripped }}
|
||||
selection-background={{ colors.on_primary.default.hex_stripped }}
|
||||
cursor={{ colors.surface_variant.default.hex_stripped }} {{ colors.on_surface.default.hex_stripped }}
|
||||
15
Assets/Matugen/templates/fuzzel.conf
Normal file
@@ -0,0 +1,15 @@
|
||||
# Fuzzel Colors
|
||||
# Generated with Matugen
|
||||
|
||||
[colors]
|
||||
background={{colors.background.default.hex_stripped}}CC
|
||||
text={{colors.on_surface.default.hex_stripped}}ff
|
||||
prompt={{colors.secondary.default.hex_stripped}}ff
|
||||
placeholder={{colors.tertiary.default.hex_stripped}}ff
|
||||
input={{colors.primary.default.hex_stripped}}ff
|
||||
match={{colors.tertiary.default.hex_stripped}}ff
|
||||
selection={{colors.primary.default.hex_stripped}}80
|
||||
selection-text={{colors.on_surface.default.hex_stripped}}ff
|
||||
selection-match={{colors.on_primary.default.hex_stripped}}ff
|
||||
counter={{colors.secondary.default.hex_stripped}}ff
|
||||
border={{colors.primary.default.hex_stripped}}ff
|
||||
23
Assets/Matugen/templates/ghostty.conf
Normal file
@@ -0,0 +1,23 @@
|
||||
palette = 0={{colors.surface.default.hex}}
|
||||
palette = 1={{colors.error.default.hex}}
|
||||
palette = 2={{colors.tertiary.default.hex}}
|
||||
palette = 3={{colors.secondary.default.hex}}
|
||||
palette = 4={{colors.primary.default.hex}}
|
||||
palette = 5={{colors.primary.default.hex}}
|
||||
palette = 6={{colors.secondary.default.hex}}
|
||||
palette = 7={{colors.on_background.default.hex}}
|
||||
palette = 8={{colors.outline.default.hex}}
|
||||
palette = 9={{colors.secondary_fixed_dim.default.hex}}
|
||||
palette = 10={{colors.tertiary_container.default.hex}}
|
||||
palette = 11={{colors.surface_container.default.hex}}
|
||||
palette = 12={{colors.primary_container.default.hex}}
|
||||
palette = 13={{colors.on_primary_container.default.hex}}
|
||||
palette = 14={{colors.surface_variant.default.hex}}
|
||||
palette = 15={{colors.on_background.default.hex}}
|
||||
|
||||
cursor-color = {{colors.primary.default.hex}}
|
||||
cursor-text = {{colors.on_surface.default.hex}}
|
||||
foreground = {{colors.on_surface.default.hex}}
|
||||
background = {{colors.surface.default.hex}}
|
||||
selection-foreground = {{colors.on_secondary.default.hex}}
|
||||
selection-background = {{colors.secondary_fixed_dim.default.hex}}
|
||||
@@ -1,40 +1,25 @@
|
||||
cursor {{colors.on_surface.default.hex}}
|
||||
cursor_text_color {{colors.on_surface_variant.default.hex}}
|
||||
color0 {{colors.surface.default.hex}}
|
||||
color1 {{colors.error.default.hex}}
|
||||
color2 {{colors.tertiary.default.hex}}
|
||||
color3 {{colors.secondary.default.hex}}
|
||||
color4 {{colors.primary.default.hex}}
|
||||
color5 {{colors.primary.default.hex}}
|
||||
color6 {{colors.secondary.default.hex}}
|
||||
color7 {{colors.on_background.default.hex}}
|
||||
color8 {{colors.outline.default.hex}}
|
||||
color9 {{colors.secondary_fixed_dim.default.hex}}
|
||||
color10 {{colors.tertiary_container.default.hex}}
|
||||
color11 {{colors.surface_container.default.hex}}
|
||||
color12 {{colors.primary_container.default.hex}}
|
||||
color13 {{colors.on_primary_container.default.hex}}
|
||||
color14 {{colors.surface_variant.default.hex}}
|
||||
color15 {{colors.on_background.default.hex}}
|
||||
|
||||
cursor {{colors.primary.default.hex}}
|
||||
cursor_text_color {{colors.on_surface.default.hex}}
|
||||
|
||||
foreground {{colors.on_surface.default.hex}}
|
||||
background {{colors.surface.default.hex}}
|
||||
selection_foreground {{colors.on_secondary.default.hex}}
|
||||
selection_background {{colors.secondary.default.hex}}
|
||||
selection_background {{colors.secondary_fixed_dim.default.hex}}
|
||||
url_color {{colors.primary.default.hex}}
|
||||
|
||||
# black
|
||||
color0 {{colors.surface_container.default.hex}}
|
||||
color8 {{colors.on_surface_variant.default.hex}}
|
||||
|
||||
# red
|
||||
color1 {{colors.error.default.hex}}
|
||||
color9 {{colors.on_error.default.hex}}
|
||||
|
||||
# green
|
||||
color2 {{colors.tertiary.default.hex}}
|
||||
color10 {{colors.on_tertiary.default.hex}}
|
||||
|
||||
# yellow
|
||||
color3 {{colors.secondary.default.hex}}
|
||||
color11 {{colors.on_secondary.default.hex}}
|
||||
|
||||
# blue
|
||||
color4 {{colors.primary.default.hex}}
|
||||
color12 {{colors.on_primary.default.hex}}
|
||||
|
||||
# magenta
|
||||
color5 {{colors.surface_container.default.hex}}
|
||||
color13 {{colors.on_surface.default.hex}}
|
||||
|
||||
# cyan
|
||||
color6 {{colors.outline_variant.default.hex}}
|
||||
color14 {{colors.on_surface_variant.default.hex}}
|
||||
|
||||
# white
|
||||
color7 {{colors.on_surface_variant.default.hex}}
|
||||
color15 {{colors.on_surface.default.hex}}
|
||||
|
||||
572
Assets/Matugen/templates/vesktop.css
Normal file
@@ -0,0 +1,572 @@
|
||||
/*
|
||||
* Vesktop Theme
|
||||
* Generated with Matugen
|
||||
* Base was taken from https://github.com/catppuccin/discord <3
|
||||
*/
|
||||
|
||||
|
||||
/* Dark Theme */
|
||||
.visual-refresh.theme-dark,
|
||||
.visual-refresh .theme-dark {
|
||||
/* Brand Colors */
|
||||
--brand-experiment: {{colors.primary.default.hex}};
|
||||
--bg-brand: {{colors.primary.default.hex}};
|
||||
--brand-500: {{colors.primary.default.hex}} !important;
|
||||
--text-link: {{colors.primary.default.hex}} !important;
|
||||
--text-brand: {{colors.primary.default.hex}};
|
||||
--control-brand-foreground: {{colors.primary.default.hex}};
|
||||
--control-brand-foreground-new: {{colors.primary.default.hex}};
|
||||
--mention-foreground: {{colors.primary.default.hex}};
|
||||
--mention-background: {{colors.primary.default.hex}}20;
|
||||
--focus-primary: {{colors.primary.default.hex}};
|
||||
--logo-primary: {{colors.on_surface.default.hex}};
|
||||
--badge-brand-bg: {{colors.primary.default.hex}};
|
||||
--badge-brand-text: {{colors.on_primary.default.hex}};
|
||||
|
||||
/* Text Colors */
|
||||
--header-primary: {{colors.on_surface.default.hex}} !important;
|
||||
--header-secondary: {{colors.on_surface_variant.default.hex}} !important;
|
||||
--text-normal: {{colors.on_surface.default.hex}} !important;
|
||||
--text-default: {{colors.on_surface.default.hex}};
|
||||
--text-muted: {{colors.on_surface_variant.default.hex}} !important;
|
||||
--text-primary: {{colors.on_surface.default.hex}};
|
||||
--text-secondary: {{colors.on_surface_variant.default.hex}};
|
||||
--text-tertiary: {{colors.on_surface_variant.default.hex}} !important;
|
||||
--interactive-normal: {{colors.on_surface.default.hex}} !important;
|
||||
--interactive-muted: {{colors.on_surface_variant.default.hex}};
|
||||
--interactive-hover: {{colors.on_surface.default.hex}};
|
||||
--interactive-active: {{colors.on_surface.default.hex}};
|
||||
|
||||
/* Main Background Colors - Bar color (mSurface) colors.surface.default.hex*/
|
||||
--background-primary: {{colors.surface_variant.default.hex}} !important;
|
||||
--background-floating: {{colors.surface_variant.default.hex}} !important;
|
||||
--background-surface-high: {{colors.surface_variant.default.hex}} !important;
|
||||
--modal-background: {{colors.surface_variant.default.hex}} !important;
|
||||
--app-background-frame: {{colors.surface_variant.default.hex}} !important;
|
||||
--home-background: {{colors.surface_variant.default.hex}} !important;
|
||||
--chat-background: {{colors.surface_variant.default.hex}} !important;
|
||||
--chat-background-default: {{colors.surface_variant.default.hex}} !important;
|
||||
--chat-input-container-background: {{colors.surface_container.default.hex}} !important;
|
||||
|
||||
/* Secondary Background Colors - Workspace color (mSurfaceVariant) */
|
||||
--background-secondary: {{colors.surface.default.hex}} !important;
|
||||
--background-secondary-alt: {{colors.surface.default.hex}} !important;
|
||||
--background-surface-higher: {{colors.surface.default.hex}} !important;
|
||||
--background-base-low: {{colors.surface.default.hex}} !important;
|
||||
--background-base-lower: {{colors.surface.default.hex}} !important;
|
||||
--channeltextarea-background: {{colors.surface_container.default.hex}} !important;
|
||||
--modal-footer-background: {{colors.surface.default.hex}} !important;
|
||||
|
||||
/* New Messages Banner */
|
||||
--background-mentioned: {{colors.primary.default.hex}}15 !important;
|
||||
--background-mentioned-hover: {{colors.primary.default.hex}}20 !important;
|
||||
--text-mentioned: {{colors.on_surface.default.hex}} !important;
|
||||
--text-mentioned-hover: {{colors.on_surface.default.hex}} !important;
|
||||
--text-mentioned-link: {{colors.primary.default.hex}} !important;
|
||||
|
||||
/* Additional Discord-specific variables for new messages banner */
|
||||
--background-message-automod: {{colors.primary.default.hex}}15 !important;
|
||||
--background-message-automod-hover: {{colors.primary.default.hex}}20 !important;
|
||||
--background-message-highlight: {{colors.primary.default.hex}}15 !important;
|
||||
--background-message-highlight-hover: {{colors.primary.default.hex}}20 !important;
|
||||
|
||||
/* Discord unread messages banner specific variables */
|
||||
--background-mentioned: {{colors.primary.default.hex}}15 !important;
|
||||
--background-mentioned-hover: {{colors.primary.default.hex}}20 !important;
|
||||
--text-mentioned: {{colors.on_surface.default.hex}} !important;
|
||||
--text-mentioned-hover: {{colors.on_surface.default.hex}} !important;
|
||||
--text-mentioned-link: {{colors.primary.default.hex}} !important;
|
||||
|
||||
/* Additional Discord banner text variables */
|
||||
--text-normal: {{colors.on_surface.default.hex}} !important;
|
||||
--text-default: {{colors.on_surface.default.hex}} !important;
|
||||
--text-primary: {{colors.on_surface.default.hex}} !important;
|
||||
--text-secondary: {{colors.on_surface_variant.default.hex}} !important;
|
||||
--text-tertiary: {{colors.on_surface_variant.default.hex}} !important;
|
||||
--text-muted: {{colors.on_surface_variant.default.hex}} !important;
|
||||
--interactive-normal: {{colors.on_surface.default.hex}} !important;
|
||||
--interactive-muted: {{colors.on_surface_variant.default.hex}} !important;
|
||||
|
||||
/* Additional Discord banner variables */
|
||||
--background-message-automod: {{colors.primary.default.hex}}15 !important;
|
||||
--background-message-automod-hover: {{colors.primary.default.hex}}20 !important;
|
||||
--background-message-highlight: {{colors.primary.default.hex}}15 !important;
|
||||
--background-message-highlight-hover: {{colors.primary.default.hex}}20 !important;
|
||||
--background-message-hover: {{colors.surface_variant.default.hex}}50 !important;
|
||||
--background-modifier-hover: {{colors.surface_variant.default.hex}}80 !important;
|
||||
--background-modifier-selected: {{colors.primary.default.hex}}20 !important;
|
||||
--background-modifier-accent: {{colors.primary.default.hex}}30 !important;
|
||||
--background-modifier-active: {{colors.primary.default.hex}}25 !important;
|
||||
|
||||
/* Chat Input Improvements */
|
||||
--text-input-background: {{colors.surface_container.default.hex}} !important;
|
||||
--text-input-border: {{colors.outline.default.hex}} !important;
|
||||
--text-input-border-hover: {{colors.primary.default.hex}} !important;
|
||||
|
||||
/* Additional Discord-specific input variables */
|
||||
--deprecated-text-input-bg: {{colors.surface_container.default.hex}} !important;
|
||||
--deprecated-text-input-border: {{colors.outline.default.hex}} !important;
|
||||
--deprecated-text-input-border-hover: {{colors.primary.default.hex}} !important;
|
||||
--input-background: {{colors.surface_container.default.hex}} !important;
|
||||
--input-border: {{colors.outline.default.hex}} !important;
|
||||
--input-placeholder-text: {{colors.on_surface_variant.default.hex}} !important;
|
||||
|
||||
/* Elevated/Container Backgrounds */
|
||||
--background-tertiary: {{colors.surface_container.default.hex}} !important;
|
||||
--background-accent: {{colors.surface_container.default.hex}} !important;
|
||||
--background-surface-highest: {{colors.surface_container_high.default.hex}} !important;
|
||||
--background-base-lowest: {{colors.surface_container.default.hex}} !important;
|
||||
|
||||
/* Border Colors */
|
||||
--border-faint: {{colors.outline_variant.default.hex}};
|
||||
--border-strong: {{colors.surface_container.default.hex}};
|
||||
--border-normal: {{colors.surface_container_high.default.hex}};
|
||||
--border-subtle: {{colors.surface.default.hex}} !important;
|
||||
--chat-border: {{colors.surface_container_high.default.hex}};
|
||||
|
||||
/* Status Colors */
|
||||
--status-positive: {{colors.tertiary.default.hex}};
|
||||
--status-positive-background: {{colors.tertiary.default.hex}};
|
||||
--status-positive-text: {{colors.on_tertiary.default.hex}};
|
||||
--text-positive: {{colors.tertiary.default.hex}};
|
||||
--text-feedback-positive: {{colors.tertiary.default.hex}};
|
||||
--background-feedback-positive: {{colors.tertiary.default.hex}}20;
|
||||
--info-positive-background: {{colors.tertiary.default.hex}}20;
|
||||
--info-positive-foreground: {{colors.tertiary.default.hex}};
|
||||
--info-positive-text: {{colors.on_surface.default.hex}};
|
||||
|
||||
--status-warning: {{colors.secondary.default.hex}};
|
||||
--status-warning-background: {{colors.secondary.default.hex}};
|
||||
--status-warning-text: {{colors.on_secondary.default.hex}};
|
||||
--text-warning: {{colors.secondary.default.hex}};
|
||||
--text-feedback-warning: {{colors.secondary.default.hex}};
|
||||
--background-feedback-warning: {{colors.secondary.default.hex}}20;
|
||||
--info-warning-background: {{colors.secondary.default.hex}}20;
|
||||
--info-warning-foreground: {{colors.secondary.default.hex}};
|
||||
--info-warning-text: {{colors.on_surface.default.hex}};
|
||||
|
||||
--status-danger: {{colors.error.default.hex}};
|
||||
--status-danger-background: {{colors.error.default.hex}};
|
||||
--status-danger-text: {{colors.on_error.default.hex}};
|
||||
--text-danger: {{colors.error.default.hex}};
|
||||
--text-feedback-critical: {{colors.error.default.hex}};
|
||||
--background-feedback-critical: {{colors.error.default.hex}}20;
|
||||
--info-danger-background: {{colors.error.default.hex}}20;
|
||||
--info-danger-foreground: {{colors.error.default.hex}};
|
||||
--info-danger-text: {{colors.on_surface.default.hex}};
|
||||
|
||||
/* Button Colors */
|
||||
--button-secondary-background: {{colors.surface_variant.default.hex}} !important;
|
||||
--button-secondary-background-hover: {{colors.surface_container.default.hex}};
|
||||
--button-secondary-background-active: {{colors.surface_container.default.hex}};
|
||||
--button-secondary-background-disabled: {{colors.surface_variant.default.hex}};
|
||||
--button-secondary-text: {{colors.on_surface.default.hex}} !important;
|
||||
|
||||
--button-filled-brand-text: {{colors.on_primary.default.hex}};
|
||||
--button-filled-brand-background: {{colors.primary.default.hex}};
|
||||
--button-filled-brand-background-hover: {{colors.primary.default.hex}};
|
||||
--button-filled-brand-background-active: {{colors.primary.default.hex}};
|
||||
|
||||
/* Input Colors */
|
||||
--input-background: {{colors.surface_container.default.hex}};
|
||||
--input-border: {{colors.outline.default.hex}};
|
||||
--input-placeholder-text: {{colors.on_surface_variant.default.hex}};
|
||||
|
||||
/* Scrollbar Colors */
|
||||
--scrollbar-thin-thumb: {{colors.primary.default.hex}};
|
||||
--scrollbar-thin-track: transparent;
|
||||
--scrollbar-auto-thumb: {{colors.primary.default.hex}};
|
||||
--scrollbar-auto-track: {{colors.surface_container_high.default.hex}};
|
||||
--scrollbar-auto-scrollbar-color-thumb: {{colors.primary.default.hex}};
|
||||
--scrollbar-auto-scrollbar-color-track: {{colors.surface_container_high.default.hex}};
|
||||
|
||||
/* Icon Colors */
|
||||
--icon-muted: {{colors.on_surface_variant.default.hex}};
|
||||
--icon-default: {{colors.on_surface.default.hex}};
|
||||
--icon-primary: {{colors.on_surface.default.hex}};
|
||||
--icon-secondary: {{colors.on_surface_variant.default.hex}};
|
||||
--icon-tertiary: {{colors.on_surface_variant.default.hex}} !important;
|
||||
|
||||
/* Channel Colors */
|
||||
--channels-default: {{colors.on_surface_variant.default.hex}} !important;
|
||||
--channel-icon: {{colors.on_surface_variant.default.hex}} !important;
|
||||
--channel-text-area-placeholder: {{colors.on_surface.default.hex}}80;
|
||||
|
||||
/* Selection and Hover States */
|
||||
--background-modifier-hover: {{colors.surface_variant.default.hex}}80;
|
||||
--background-modifier-selected: {{colors.primary.default.hex}}20 !important;
|
||||
--background-modifier-accent: {{colors.primary.default.hex}}30;
|
||||
--background-modifier-active: {{colors.primary.default.hex}}25 !important;
|
||||
--background-message-hover: {{colors.surface_variant.default.hex}}50 !important;
|
||||
--background-message-highlight: {{colors.primary.default.hex}}15;
|
||||
--background-message-highlight-hover: {{colors.primary.default.hex}}20;
|
||||
|
||||
/* Code Block - Use workspace background */
|
||||
--background-code: {{colors.surface_container.default.hex}};
|
||||
--textbox-markdown-syntax: {{colors.on_surface_variant.default.hex}};
|
||||
|
||||
/* Spoiler */
|
||||
--spoiler-revealed-background: {{colors.surface_container.default.hex}};
|
||||
--spoiler-hidden-background: {{colors.surface_variant.default.hex}};
|
||||
|
||||
/* White/Black Overrides */
|
||||
--white: {{colors.on_surface.default.hex}};
|
||||
--white-400: {{colors.on_surface.default.hex}};
|
||||
--white-500: {{colors.on_surface.default.hex}};
|
||||
--white-600: {{colors.on_surface_variant.default.hex}};
|
||||
--white-700: {{colors.on_surface_variant.default.hex}};
|
||||
--black-500: {{colors.surface_container_high.default.hex}};
|
||||
|
||||
/* Force styling for Discord unread messages banner */
|
||||
--unread-bar-background: {{colors.primary.default.hex}}15 !important;
|
||||
--unread-bar-text: {{colors.on_surface.default.hex}} !important;
|
||||
--unread-bar-hover: {{colors.primary.default.hex}}20 !important;
|
||||
|
||||
/* Additional Discord unread bar variables */
|
||||
--background-mentioned: {{colors.primary.default.hex}}15 !important;
|
||||
--background-mentioned-hover: {{colors.primary.default.hex}}20 !important;
|
||||
--text-mentioned: {{colors.on_surface.default.hex}} !important;
|
||||
--text-mentioned-hover: {{colors.on_surface.default.hex}} !important;
|
||||
--text-mentioned-link: {{colors.primary.default.hex}} !important;
|
||||
|
||||
/* Discord banner specific variables */
|
||||
--background-message-automod: {{colors.primary.default.hex}}15 !important;
|
||||
--background-message-automod-hover: {{colors.primary.default.hex}}20 !important;
|
||||
--background-message-highlight: {{colors.primary.default.hex}}15 !important;
|
||||
--background-message-highlight-hover: {{colors.primary.default.hex}}20 !important;
|
||||
|
||||
/* Discord unread bar specific variables */
|
||||
--background-mentioned: {{colors.primary.default.hex}}15 !important;
|
||||
--background-mentioned-hover: {{colors.primary.default.hex}}20 !important;
|
||||
--text-mentioned: {{colors.on_surface.default.hex}} !important;
|
||||
--text-mentioned-hover: {{colors.on_surface.default.hex}} !important;
|
||||
--text-mentioned-link: {{colors.primary.default.hex}} !important;
|
||||
|
||||
/* Additional Discord text variables that might affect the banner */
|
||||
--text-normal: {{colors.on_surface.default.hex}} !important;
|
||||
--text-default: {{colors.on_surface.default.hex}} !important;
|
||||
--text-primary: {{colors.on_surface.default.hex}} !important;
|
||||
--text-secondary: {{colors.on_surface_variant.default.hex}} !important;
|
||||
--text-tertiary: {{colors.on_surface_variant.default.hex}} !important;
|
||||
--text-muted: {{colors.on_surface_variant.default.hex}} !important;
|
||||
--interactive-normal: {{colors.on_surface.default.hex}} !important;
|
||||
--interactive-muted: {{colors.on_surface_variant.default.hex}} !important;
|
||||
|
||||
/* Force styling for Discord chat input */
|
||||
--chat-input-background: {{colors.surface_container.default.hex}} !important;
|
||||
--chat-input-placeholder: {{colors.on_surface_variant.default.hex}} !important;
|
||||
|
||||
/* Discord unread messages banner specific variables */
|
||||
--new-messages-bar-background: {{colors.surface_container.default.hex}} !important;
|
||||
--new-messages-bar-text: {{colors.on_surface.default.hex}} !important;
|
||||
--new-messages-bar-hover: {{colors.surface_container_high.default.hex}} !important;
|
||||
--bar-button-background: {{colors.surface_container.default.hex}} !important;
|
||||
--bar-button-text: {{colors.on_surface.default.hex}} !important;
|
||||
--bar-button-hover: {{colors.surface_container_high.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark ::selection,
|
||||
.visual-refresh .theme-dark ::selection {
|
||||
background-color: {{colors.primary.default.hex}};
|
||||
}
|
||||
|
||||
/* Force Discord unread messages banner styling */
|
||||
.visual-refresh.theme-dark .newMessagesBar__0f481,
|
||||
.visual-refresh.theme-dark .barButtonMain__0f481,
|
||||
.visual-refresh.theme-dark .barButtonBase__0f481,
|
||||
.visual-refresh.theme-dark .span__0f481 {
|
||||
background-color: {{colors.surface_container.default.hex}} !important;
|
||||
color: {{colors.on_surface.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .newMessagesBar__0f481:hover,
|
||||
.visual-refresh.theme-dark .barButtonMain__0f481:hover,
|
||||
.visual-refresh.theme-dark .barButtonBase__0f481:hover {
|
||||
background-color: {{colors.surface_container_high.default.hex}} !important;
|
||||
}
|
||||
|
||||
/* Force Discord chat input styling */
|
||||
.visual-refresh.theme-dark .channelTextArea-rNsIhG,
|
||||
.visual-refresh.theme-dark .channelTextArea-rNsIhG *,
|
||||
.visual-refresh.theme-dark .scrollableContainer-2NUZem,
|
||||
.visual-refresh.theme-dark [data-slate-editor="true"] {
|
||||
background-color: {{colors.surface_container.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark [data-slate-editor="true"]::placeholder,
|
||||
.visual-refresh.theme-dark .channelTextArea-rNsIhG [data-slate-editor="true"]::placeholder {
|
||||
color: {{colors.on_surface_variant.default.hex}} !important;
|
||||
}
|
||||
|
||||
/* Discord Emoji Picker Theming */
|
||||
.visual-refresh.theme-dark .contentWrapper__08434,
|
||||
.visual-refresh.theme-dark .emojiPicker_c0e32c,
|
||||
.visual-refresh.theme-dark .wrapper_c0e32c {
|
||||
background-color: {{colors.surface.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .nav__08434,
|
||||
.visual-refresh.theme-dark .navList__08434 {
|
||||
background-color: {{colors.surface.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .navButton__08434 {
|
||||
background-color: {{colors.surface.default.hex}} !important;
|
||||
color: {{colors.on_surface_variant.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .navButtonActive__08434 {
|
||||
background-color: {{colors.surface.default.hex}} !important;
|
||||
color: {{colors.on_surface.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .searchBar_c0e32c,
|
||||
.visual-refresh.theme-dark .input_a45028 {
|
||||
background-color: {{colors.surface.default.hex}} !important;
|
||||
color: {{colors.on_surface.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .input_a45028::placeholder {
|
||||
color: {{colors.on_surface_variant.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .header_c656ac,
|
||||
.visual-refresh.theme-dark .header__14245,
|
||||
.visual-refresh.theme-dark .wrapper__14245 {
|
||||
background-color: {{colors.surface_variant.default.hex}} !important;
|
||||
color: {{colors.on_surface.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .headerLabel__14245 {
|
||||
color: {{colors.on_surface.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .interactive__14245 {
|
||||
color: {{colors.on_surface.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .header__14245 {
|
||||
color: {{colors.on_surface.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .header__14245 * {
|
||||
color: {{colors.on_surface.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .headerIcon__14245 svg,
|
||||
.visual-refresh.theme-dark .headerCollapseIcon__14245 svg {
|
||||
color: {{colors.on_surface.default.hex}} !important;
|
||||
fill: {{colors.on_surface.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .emojiItem_fc7141 {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .emojiItem_fc7141:hover {
|
||||
background-color: {{colors.surface_container.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .emojiItemSelected_fc7141 {
|
||||
background-color: {{colors.primary.default.hex}}20 !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .inspector_aeaaeb {
|
||||
background-color: {{colors.surface_container.default.hex}} !important;
|
||||
color: {{colors.on_surface.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .categoryList_c0e32c {
|
||||
background-color: {{colors.surface.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .categoryItem_b9ee0c {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .categoryItem_b9ee0c:hover {
|
||||
background-color: {{colors.surface_variant.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .categoryItemDefaultCategorySelected_b9ee0c {
|
||||
background-color: {{colors.surface_variant.default.hex}} !important;
|
||||
}
|
||||
|
||||
/* Additional Discord emoji picker elements */
|
||||
.visual-refresh.theme-dark .navItem__08434 {
|
||||
background-color: {{colors.surface_variant.default.hex}} !important;
|
||||
color: {{colors.on_surface_variant.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .navItem__08434:hover {
|
||||
background-color: {{colors.surface_container.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .stickersNavItem__08434 {
|
||||
color: {{colors.on_surface_variant.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .wrapper__14245 {
|
||||
background-color: {{colors.surface_variant.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .headerLabel__14245 {
|
||||
color: {{colors.on_surface.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .headerIcon__14245 svg,
|
||||
.visual-refresh.theme-dark .headerCollapseIcon__14245 svg {
|
||||
color: {{colors.on_surface_variant.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .interactive__14245:hover {
|
||||
background-color: {{colors.surface_container.default.hex}} !important;
|
||||
}
|
||||
|
||||
/* Chat input styling */
|
||||
.visual-refresh.theme-dark .scrollableContainer__74017,
|
||||
.visual-refresh.theme-dark .themedBackground__74017,
|
||||
.visual-refresh.theme-dark .inner__74017,
|
||||
.visual-refresh.theme-dark .textArea__74017,
|
||||
.visual-refresh.theme-dark .slateContainer_ec4baf,
|
||||
.visual-refresh.theme-dark .markup__75297,
|
||||
.visual-refresh.theme-dark .editor__1b31f,
|
||||
.visual-refresh.theme-dark .slateTextArea_ec4baf {
|
||||
background-color: {{colors.surface_container.default.hex}} !important;
|
||||
color: {{colors.on_surface.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .emptyText__1464f {
|
||||
color: {{colors.on_surface_variant.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .placeholder__1b31f {
|
||||
color: {{colors.on_surface_variant.default.hex}} !important;
|
||||
}
|
||||
|
||||
/* Message content styling */
|
||||
.visual-refresh.theme-dark .messageContent_c19a55 {
|
||||
color: {{colors.on_surface.default.hex}} !important;
|
||||
background-color: {{colors.surface.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .messageContent_c19a55 .markup__75297 {
|
||||
color: {{colors.on_surface.default.hex}} !important;
|
||||
background-color: {{colors.surface.default.hex}} !important;
|
||||
}
|
||||
|
||||
/* Message background styling */
|
||||
.visual-refresh.theme-dark .message__5126c,
|
||||
.visual-refresh.theme-dark .cozyMessage__5126c,
|
||||
.visual-refresh.theme-dark .wrapper_c19a55,
|
||||
.visual-refresh.theme-dark .contents_c19a55 {
|
||||
background-color: {{colors.surface.default.hex}} !important;
|
||||
}
|
||||
|
||||
/* Message hover effects */
|
||||
.visual-refresh.theme-dark .message__5126c:hover {
|
||||
background-color: {{colors.surface_variant.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .message__5126c:hover * {
|
||||
color: {{colors.on_surface.default.hex}} !important;
|
||||
}
|
||||
|
||||
/* Remove Discord's native quote/reply bar */
|
||||
.visual-refresh.theme-dark .message__5126c::before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .message__5126c.hasReply_c19a55::before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Channel styling - darker text for read channels */
|
||||
.visual-refresh.theme-dark .link__2ea32 .name__2ea32 {
|
||||
color: {{colors.outline.default.hex}} !important;
|
||||
}
|
||||
|
||||
/* Unread channels keep normal color */
|
||||
.visual-refresh.theme-dark .link__2ea32[aria-label*="unread"] .name__2ea32 {
|
||||
color: {{colors.on_surface.default.hex}} !important;
|
||||
}
|
||||
|
||||
/* Search input styling */
|
||||
.visual-refresh.theme-dark .inner_a45028 {
|
||||
background-color: {{colors.surface_variant.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .input_a45028 {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .input_a45028::placeholder {
|
||||
color: {{colors.on_surface_variant.default.hex}} !important;
|
||||
}
|
||||
|
||||
/* Chat input placeholder styling */
|
||||
.visual-refresh.theme-dark .emptyText__1464f {
|
||||
color: {{colors.on_surface_variant.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .slateTextArea_ec4baf > div:first-child .emptyText__1464f::before {
|
||||
content: "send a message" !important;
|
||||
color: {{colors.on_surface_variant.default.hex}} !important;
|
||||
}
|
||||
|
||||
/* Hide placeholder when input is focused */
|
||||
.visual-refresh.theme-dark .slateTextArea_ec4baf:focus .emptyText__1464f::before,
|
||||
.visual-refresh.theme-dark .markup__75297:focus .emptyText__1464f::before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.visual-refresh.theme-dark .message__5126c:hover .messageContent_c19a55,
|
||||
.visual-refresh.theme-dark .message__5126c:hover .markup__75297,
|
||||
.visual-refresh.theme-dark .message__5126c:hover .header_c19a55,
|
||||
.visual-refresh.theme-dark .message__5126c:hover .headerText_c19a55,
|
||||
.visual-refresh.theme-dark .message__5126c:hover .username_c19a55,
|
||||
.visual-refresh.theme-dark .message__5126c:hover .timestamp_c19a55 {
|
||||
background-color: {{colors.surface_variant.default.hex}} !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.visual-refresh.theme-dark .categoryIcon_b9ee0c svg {
|
||||
color: {{colors.on_surface_variant.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .unicodeShortcut_b9ee0c {
|
||||
background-color: {{colors.surface_container.default.hex}} !important;
|
||||
color: {{colors.on_surface.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .unicodeShortcut_b9ee0c:hover {
|
||||
background-color: {{colors.surface_container_high.default.hex}} !important;
|
||||
}
|
||||
|
||||
.visual-refresh.theme-dark .unicodeShortcut_b9ee0c svg {
|
||||
color: {{colors.on_surface.default.hex}} !important;
|
||||
}
|
||||
|
||||
/* Number badge styling */
|
||||
.visual-refresh.theme-dark .numberBadge__2b1f5 {
|
||||
color: {{colors.surface.default.hex}} !important;
|
||||
background-color: {{colors.primary.default.hex}} !important;
|
||||
}
|
||||
|
||||
/* New badge styling */
|
||||
.visual-refresh.theme-dark .newBadge__4ed1a {
|
||||
color: {{colors.surface.default.hex}} !important;
|
||||
background-color: {{colors.primary.default.hex}} !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
BIN
Assets/Screenshots/noctalia-dark-1.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
Assets/Screenshots/noctalia-dark-2.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
Assets/Screenshots/noctalia-dark-3.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
Assets/Screenshots/noctalia-light-1.png
Normal file
|
After Width: | Height: | Size: 4.8 MiB |
BIN
Assets/Screenshots/noctalia-light-2.png
Normal file
|
After Width: | Height: | Size: 4.0 MiB |
BIN
Assets/Screenshots/noctalia-light-3.png
Normal file
|
After Width: | Height: | Size: 3.7 MiB |
|
Before Width: | Height: | Size: 373 KiB |
BIN
Assets/Wallpaper/noctalia.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
@@ -4,4 +4,4 @@
|
||||
# Can be installed from AUR "qmlfmt-git"
|
||||
# Requires qt6-5compat
|
||||
|
||||
find . -name "*.qml" -print -exec qmlfmt -e -b 120 -t 2 -i 2 -w {} \;
|
||||
find . -name "*.qml" -print -exec qmlfmt -e -b 360 -t 2 -i 2 -w {} \;
|
||||
|
||||
36
Bin/shaders-compile.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Directory containing the source shaders.
|
||||
SOURCE_DIR="Shaders/frag/"
|
||||
|
||||
# Directory where the compiled shaders will be saved.
|
||||
DEST_DIR="Shaders/qsb/"
|
||||
|
||||
# Check if the source directory exists.
|
||||
if [ ! -d "$SOURCE_DIR" ]; then
|
||||
echo "Source directory $SOURCE_DIR not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create the destination directory if it doesn't exist.
|
||||
mkdir -p "$DEST_DIR"
|
||||
|
||||
# Loop through all files in the source directory ending with .frag
|
||||
for shader in "$SOURCE_DIR"*.frag; do
|
||||
# Check if a file was found (to handle the case of no .frag files).
|
||||
if [ -f "$shader" ]; then
|
||||
# Get the base name of the file (e.g., wp_fade).
|
||||
shader_name=$(basename "$shader" .frag)
|
||||
|
||||
# Construct the output path for the compiled shader.
|
||||
output_path="$DEST_DIR$shader_name.frag.qsb"
|
||||
|
||||
# Construct and run the qsb command.
|
||||
qsb --qt6 -o "$output_path" "$shader"
|
||||
|
||||
# Print a message to confirm compilation.
|
||||
echo "Compiled $shader to $output_path"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Shader compilation complete."
|
||||
@@ -1,219 +0,0 @@
|
||||
#!/usr/bin/env -S bash
|
||||
|
||||
# A Bash script to monitor system stats and output them in JSON format.
|
||||
|
||||
# --- Configuration ---
|
||||
# Default sleep duration in seconds. Can be overridden by the first argument.
|
||||
SLEEP_DURATION=3
|
||||
|
||||
# --- Argument Parsing ---
|
||||
# Check if a command-line argument is provided for the sleep duration.
|
||||
if [[ -n "$1" ]]; then
|
||||
# Basic validation to ensure the argument is a number (integer or float).
|
||||
if [[ "$1" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
|
||||
SLEEP_DURATION=$1
|
||||
else
|
||||
# Output to stderr if the format is invalid.
|
||||
echo "Warning: Invalid duration format '$1'. Using default of ${SLEEP_DURATION}s." >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Global Cache Variables ---
|
||||
# These variables will store the discovered CPU temperature sensor path and type
|
||||
# to avoid searching for it on every loop iteration.
|
||||
TEMP_SENSOR_PATH=""
|
||||
TEMP_SENSOR_TYPE=""
|
||||
|
||||
# --- Data Collection Functions ---
|
||||
|
||||
#
|
||||
# Gets memory usage in GB, MB, and as a percentage.
|
||||
#
|
||||
get_memory_info() {
|
||||
awk '
|
||||
/MemTotal/ {total=$2}
|
||||
/MemAvailable/ {available=$2}
|
||||
END {
|
||||
if (total > 0) {
|
||||
usage_kb = total - available
|
||||
usage_gb = usage_kb / 1000000
|
||||
usage_percent = (usage_kb / total) * 100
|
||||
printf "%.1f %.0f\n", usage_gb, usage_percent
|
||||
} else {
|
||||
# Fallback if /proc/meminfo is unreadable or empty.
|
||||
print "0.0 0 0"
|
||||
}
|
||||
}
|
||||
' /proc/meminfo
|
||||
}
|
||||
|
||||
#
|
||||
# Gets the usage percentage of the root filesystem ("/").
|
||||
#
|
||||
get_disk_usage() {
|
||||
# df gets disk usage. --output=pcent shows only the percentage for the root path.
|
||||
# tail -1 gets the data line, and tr removes the '%' sign and whitespace.
|
||||
df --output=pcent / | tail -1 | tr -d ' %'
|
||||
}
|
||||
|
||||
#
|
||||
# Calculates current CPU usage over a short interval.
|
||||
#
|
||||
get_cpu_usage() {
|
||||
# Read all 10 CPU time fields to prevent errors on newer kernels.
|
||||
read -r cpu prev_user prev_nice prev_system prev_idle prev_iowait prev_irq prev_softirq prev_steal prev_guest prev_guest_nice < /proc/stat
|
||||
|
||||
# Calculate previous total and idle times.
|
||||
local prev_total_idle=$((prev_idle + prev_iowait))
|
||||
local prev_total=$((prev_user + prev_nice + prev_system + prev_idle + prev_iowait + prev_irq + prev_softirq + prev_steal + prev_guest + prev_guest_nice))
|
||||
|
||||
# Wait for a short period.
|
||||
sleep 0.05
|
||||
|
||||
# Read all 10 CPU time fields again for the second measurement.
|
||||
read -r cpu user nice system idle iowait irq softirq steal guest guest_nice < /proc/stat
|
||||
|
||||
# Calculate new total and idle times.
|
||||
local total_idle=$((idle + iowait))
|
||||
local total=$((user + nice + system + idle + iowait + irq + softirq + steal + guest + guest_nice))
|
||||
|
||||
# Add a check to prevent division by zero if total hasn't changed.
|
||||
if (( total <= prev_total )); then
|
||||
echo "0.0"
|
||||
return
|
||||
fi
|
||||
|
||||
# Calculate the difference over the interval.
|
||||
local diff_total=$((total - prev_total))
|
||||
local diff_idle=$((total_idle - prev_total_idle))
|
||||
|
||||
# Use awk for floating-point calculation and print the percentage.
|
||||
awk -v total="$diff_total" -v idle="$diff_idle" '
|
||||
BEGIN {
|
||||
if (total > 0) {
|
||||
# Formula: 100 * (Total - Idle) / Total
|
||||
usage = 100 * (total - idle) / total
|
||||
printf "%.1f\n", usage
|
||||
} else {
|
||||
print "0.0"
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
#
|
||||
# Finds and returns the CPU temperature in degrees Celsius.
|
||||
# Caches the sensor path for efficiency.
|
||||
#
|
||||
get_cpu_temp() {
|
||||
# If the sensor path hasn't been found yet, search for it.
|
||||
if [[ -z "$TEMP_SENSOR_PATH" ]]; then
|
||||
for dir in /sys/class/hwmon/hwmon*; do
|
||||
# Check if the 'name' file exists and read it.
|
||||
if [[ -f "$dir/name" ]]; then
|
||||
local name
|
||||
name=$(<"$dir/name")
|
||||
# Check for supported sensor types.
|
||||
if [[ "$name" == "coretemp" || "$name" == "k10temp" || "$name" == "zenpower" ]]; then
|
||||
TEMP_SENSOR_PATH=$dir
|
||||
TEMP_SENSOR_TYPE=$name
|
||||
break # Found it, no need to keep searching.
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# If after searching no sensor was found, return 0.
|
||||
if [[ -z "$TEMP_SENSOR_PATH" ]]; then
|
||||
echo 0
|
||||
return
|
||||
fi
|
||||
|
||||
# --- Get temp based on sensor type ---
|
||||
if [[ "$TEMP_SENSOR_TYPE" == "coretemp" ]]; then
|
||||
# For Intel 'coretemp', average all available temperature sensors.
|
||||
local total_temp=0
|
||||
local sensor_count=0
|
||||
|
||||
# Use a for loop with a glob to iterate over all temp input files.
|
||||
# This is more efficient than 'find' for this simple case.
|
||||
for temp_file in "$TEMP_SENSOR_PATH"/temp*_input; do
|
||||
# The glob returns the pattern itself if no files match,
|
||||
# so we must check if the file actually exists.
|
||||
if [[ -f "$temp_file" ]]; then
|
||||
total_temp=$((total_temp + $(<"$temp_file")))
|
||||
sensor_count=$((sensor_count + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if (( sensor_count > 0 )); then
|
||||
# Use awk for the final division to handle potential floating point numbers
|
||||
# and convert from millidegrees to integer degrees Celsius.
|
||||
awk -v total="$total_temp" -v count="$sensor_count" 'BEGIN { print int(total / count / 1000) }'
|
||||
else
|
||||
# If no sensor files were found, return 0.
|
||||
echo 0
|
||||
fi
|
||||
|
||||
elif [[ "$TEMP_SENSOR_TYPE" == "k10temp" ]]; then
|
||||
# For AMD 'k10temp', find the 'Tctl' sensor, which is the control temperature.
|
||||
local tctl_input=""
|
||||
for label_file in "$TEMP_SENSOR_PATH"/temp*_label; do
|
||||
if [[ -f "$label_file" ]] && [[ $(<"$label_file") == "Tctl" ]]; then
|
||||
# The input file has the same name but with '_input' instead of '_label'.
|
||||
tctl_input="${label_file%_label}_input"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -f "$tctl_input" ]]; then
|
||||
# Read the temperature and convert from millidegrees to degrees.
|
||||
echo "$(( $(<"$tctl_input") / 1000 ))"
|
||||
else
|
||||
echo 0 # Fallback
|
||||
fi
|
||||
elif [[ "$TEMP_SENSOR_TYPE" == "zenpower" ]]; then
|
||||
# For zenpower, read the first available temp sensor
|
||||
for temp_file in "$TEMP_SENSOR_PATH"/temp*_input; do
|
||||
if [[ -f "$temp_file" ]]; then
|
||||
local temp_value
|
||||
temp_value=$(cat "$temp_file" | tr -d '\n\r') # Remove any newlines
|
||||
echo "$((temp_value / 1000))"
|
||||
return
|
||||
fi
|
||||
done
|
||||
echo 0
|
||||
|
||||
if [[ -f "$tctl_input" ]]; then
|
||||
# Read the temperature and convert from millidegrees to degrees.
|
||||
echo "$(($(<"$tctl_input") / 1000))"
|
||||
else
|
||||
echo 0 # Fallback
|
||||
fi
|
||||
else
|
||||
echo 0 # Should not happen if cache logic is correct.
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Main Loop ---
|
||||
# This loop runs indefinitely, gathering and printing stats.
|
||||
while true; do
|
||||
# Call the functions to gather all the data.
|
||||
# get_memory_info
|
||||
read -r mem_gb mem_per <<< "$(get_memory_info)"
|
||||
|
||||
# Command substitution captures the single output from the other functions.
|
||||
disk_per=$(get_disk_usage)
|
||||
cpu_usage=$(get_cpu_usage)
|
||||
cpu_temp=$(get_cpu_temp)
|
||||
|
||||
# Use printf to format the final JSON output string, adding the mem_mb key.
|
||||
printf '{"cpu": "%s", "cputemp": "%s", "memgb":"%s", "memper": "%s", "diskper": "%s"}\n' \
|
||||
"$cpu_usage" \
|
||||
"$cpu_temp" \
|
||||
"$mem_gb" \
|
||||
"$mem_per" \
|
||||
"$disk_per"
|
||||
|
||||
# Wait for the specified duration before the next update.
|
||||
sleep "$SLEEP_DURATION"
|
||||
done
|
||||
@@ -9,3 +9,24 @@ for i in {1..8}; do
|
||||
done
|
||||
|
||||
echo "All notifications sent!"
|
||||
|
||||
# Additional tests for icon/image handling
|
||||
if command -v notify-send >/dev/null 2>&1; then
|
||||
echo "Sending icon/image tests..."
|
||||
|
||||
# 1) Themed icon name
|
||||
notify-send -i dialog-information "Icon name test" "Should resolve from theme (dialog-information)"
|
||||
|
||||
# 2) Absolute path if a sample image exists
|
||||
SAMPLE_IMG="/usr/share/pixmaps/debian-logo.png"
|
||||
if [ -f "$SAMPLE_IMG" ]; then
|
||||
notify-send -i "$SAMPLE_IMG" "Absolute path test" "Should show the provided image path"
|
||||
fi
|
||||
|
||||
# 3) file:// URL form
|
||||
if [ -f "$SAMPLE_IMG" ]; then
|
||||
notify-send -i "file://$SAMPLE_IMG" "file:// URL test" "Should display after stripping scheme"
|
||||
fi
|
||||
|
||||
echo "Icon/image tests sent!"
|
||||
fi
|
||||
|
||||
53
Commons/AppIcons.qml
Normal file
@@ -0,0 +1,53 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function iconFromName(iconName, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable"
|
||||
try {
|
||||
if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) {
|
||||
const p = Quickshell.iconPath(iconName, fallback)
|
||||
if (p && p !== "")
|
||||
return p
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
// ignore and fall back
|
||||
}
|
||||
try {
|
||||
return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : ""
|
||||
} catch (e2) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve icon path for a DesktopEntries appId - safe on missing entries
|
||||
function iconForAppId(appId, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable"
|
||||
if (!appId)
|
||||
return iconFromName(fallback, fallback)
|
||||
try {
|
||||
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
|
||||
return iconFromName(fallback, fallback)
|
||||
const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup(appId) : DesktopEntries.byId(appId)
|
||||
const name = entry && entry.icon ? entry.icon : ""
|
||||
return iconFromName(name || fallback, fallback)
|
||||
} catch (e) {
|
||||
return iconFromName(fallback, fallback)
|
||||
}
|
||||
}
|
||||
|
||||
// Distro logo helper (absolute path or empty string)
|
||||
function distroLogoPath() {
|
||||
try {
|
||||
return (typeof OSInfo !== 'undefined' && OSInfo.distroIconPath) ? OSInfo.distroIconPath : ""
|
||||
} catch (e) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,14 +44,6 @@ Singleton {
|
||||
|
||||
property color transparent: "transparent"
|
||||
|
||||
// -----------
|
||||
function applyOpacity(color, opacity) {
|
||||
// Convert color to string and apply opacity
|
||||
if (!color)
|
||||
return "transparent"
|
||||
return color.toString().replace("#", "#" + opacity)
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
// Default colors: RosePine
|
||||
QtObject {
|
||||
@@ -110,7 +102,8 @@ Singleton {
|
||||
// FileView to load custom colors data from colors.json
|
||||
FileView {
|
||||
id: customColorsFile
|
||||
path: Settings.configDir + "colors.json"
|
||||
path: Settings.directoriesCreated ? (Settings.configDir + "colors.json") : undefined
|
||||
printErrors: false
|
||||
watchChanges: true
|
||||
onFileChanged: {
|
||||
Logger.log("Color", "Reloading colors from disk")
|
||||
@@ -120,6 +113,13 @@ Singleton {
|
||||
Logger.log("Color", "Writing colors to disk")
|
||||
writeAdapter()
|
||||
}
|
||||
|
||||
// Trigger initial load when path changes from empty to actual path
|
||||
onPathChanged: {
|
||||
if (path !== undefined) {
|
||||
reload()
|
||||
}
|
||||
}
|
||||
onLoadFailed: function (error) {
|
||||
if (error.toString().includes("No such file") || error === 2) {
|
||||
// File doesn't exist, create it with default values
|
||||
|
||||
@@ -1,43 +1,49 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Commons.IconsSets
|
||||
|
||||
Singleton {
|
||||
id: icons
|
||||
id: root
|
||||
|
||||
function iconFromName(iconName, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable"
|
||||
try {
|
||||
if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) {
|
||||
const p = Quickshell.iconPath(iconName, fallback)
|
||||
if (p && p !== "")
|
||||
return p
|
||||
}
|
||||
} catch (e) {
|
||||
// Expose the font family name for easy access
|
||||
readonly property string fontFamily: fontLoader.name
|
||||
readonly property string defaultIcon: TablerIcons.defaultIcon
|
||||
readonly property var icons: TablerIcons.icons
|
||||
readonly property var aliases: TablerIcons.aliases
|
||||
readonly property string fontPath: "/Assets/Fonts/tabler/tabler-icons.ttf"
|
||||
|
||||
// ignore and fall back
|
||||
}
|
||||
try {
|
||||
return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : ""
|
||||
} catch (e2) {
|
||||
return ""
|
||||
}
|
||||
Component.onCompleted: {
|
||||
Logger.log("Icons", "Service started")
|
||||
}
|
||||
|
||||
// Resolve icon path for a DesktopEntries appId - safe on missing entries
|
||||
function iconForAppId(appId, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable"
|
||||
if (!appId)
|
||||
return iconFromName(fallback, fallback)
|
||||
try {
|
||||
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
|
||||
return iconFromName(fallback, fallback)
|
||||
const entry = DesktopEntries.byId(appId)
|
||||
const name = entry && entry.icon ? entry.icon : ""
|
||||
return iconFromName(name || fallback, fallback)
|
||||
} catch (e) {
|
||||
return iconFromName(fallback, fallback)
|
||||
function get(iconName) {
|
||||
// Check in aliases first
|
||||
if (aliases[iconName] !== undefined) {
|
||||
iconName = aliases[iconName]
|
||||
}
|
||||
|
||||
// Find the appropriate codepoint
|
||||
return icons[iconName]
|
||||
}
|
||||
|
||||
FontLoader {
|
||||
id: fontLoader
|
||||
source: Quickshell.shellDir + fontPath
|
||||
}
|
||||
|
||||
// Monitor font loading status
|
||||
Connections {
|
||||
target: fontLoader
|
||||
function onStatusChanged() {
|
||||
if (fontLoader.status === FontLoader.Ready) {
|
||||
Logger.log("Icons", "Font loaded successfully:", fontFamily)
|
||||
} else if (fontLoader.status === FontLoader.Error) {
|
||||
Logger.error("Icons", "Font failed to load")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6133
Commons/IconsSets/TablerIcons.qml
Normal file
205
Commons/KeyboardLayout.qml
Normal file
@@ -0,0 +1,205 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
// Comprehensive language name to ISO code mapping
|
||||
property var languageMap: {
|
||||
"english"// English variants
|
||||
: "us",
|
||||
"american": "us",
|
||||
"united states": "us",
|
||||
"us english": "us",
|
||||
"british": "gb",
|
||||
"uk": "ua",
|
||||
"united kingdom"// FIXED: Ukrainian language code should map to Ukraine
|
||||
: "gb",
|
||||
"english (uk)": "gb",
|
||||
"canadian": "ca",
|
||||
"canada": "ca",
|
||||
"canadian english": "ca",
|
||||
"australian": "au",
|
||||
"australia": "au",
|
||||
"swedish"// Nordic countries
|
||||
: "se",
|
||||
"svenska": "se",
|
||||
"sweden": "se",
|
||||
"norwegian": "no",
|
||||
"norsk": "no",
|
||||
"norway": "no",
|
||||
"danish": "dk",
|
||||
"dansk": "dk",
|
||||
"denmark": "dk",
|
||||
"finnish": "fi",
|
||||
"suomi": "fi",
|
||||
"finland": "fi",
|
||||
"icelandic": "is",
|
||||
"íslenska": "is",
|
||||
"iceland": "is",
|
||||
"german"// Western/Central European Germanic
|
||||
: "de",
|
||||
"deutsch": "de",
|
||||
"germany": "de",
|
||||
"austrian": "at",
|
||||
"austria": "at",
|
||||
"österreich": "at",
|
||||
"swiss": "ch",
|
||||
"switzerland": "ch",
|
||||
"schweiz": "ch",
|
||||
"suisse": "ch",
|
||||
"dutch": "nl",
|
||||
"nederlands": "nl",
|
||||
"netherlands": "nl",
|
||||
"holland": "nl",
|
||||
"belgian": "be",
|
||||
"belgium": "be",
|
||||
"belgië": "be",
|
||||
"belgique": "be",
|
||||
"french"// Romance languages (Western/Southern Europe)
|
||||
: "fr",
|
||||
"français": "fr",
|
||||
"france": "fr",
|
||||
"canadian french": "ca",
|
||||
"spanish": "es",
|
||||
"español": "es",
|
||||
"spain": "es",
|
||||
"castilian": "es",
|
||||
"italian": "it",
|
||||
"italiano": "it",
|
||||
"italy": "it",
|
||||
"portuguese": "pt",
|
||||
"português": "pt",
|
||||
"portugal": "pt",
|
||||
"catalan": "ad",
|
||||
"català": "ad",
|
||||
"andorra": "ad",
|
||||
"romanian"// Eastern European Romance
|
||||
: "ro",
|
||||
"română": "ro",
|
||||
"romania": "ro",
|
||||
"russian"// Slavic languages (Eastern Europe)
|
||||
: "ru",
|
||||
"русский": "ru",
|
||||
"russia": "ru",
|
||||
"polish": "pl",
|
||||
"polski": "pl",
|
||||
"poland": "pl",
|
||||
"czech": "cz",
|
||||
"čeština": "cz",
|
||||
"czech republic": "cz",
|
||||
"slovak": "sk",
|
||||
"slovenčina": "sk",
|
||||
"slovakia": "sk",
|
||||
"uk": "ua",
|
||||
"ukrainian"// Ukrainian language code
|
||||
: "ua",
|
||||
"українська": "ua",
|
||||
"ukraine": "ua",
|
||||
"bulgarian": "bg",
|
||||
"български": "bg",
|
||||
"bulgaria": "bg",
|
||||
"serbian": "rs",
|
||||
"srpski": "rs",
|
||||
"serbia": "rs",
|
||||
"croatian": "hr",
|
||||
"hrvatski": "hr",
|
||||
"croatia": "hr",
|
||||
"slovenian": "si",
|
||||
"slovenščina": "si",
|
||||
"slovenia": "si",
|
||||
"bosnian": "ba",
|
||||
"bosanski": "ba",
|
||||
"bosnia": "ba",
|
||||
"macedonian": "mk",
|
||||
"македонски": "mk",
|
||||
"macedonia": "mk",
|
||||
"irish"// Celtic languages (Western Europe)
|
||||
: "ie",
|
||||
"gaeilge": "ie",
|
||||
"ireland": "ie",
|
||||
"welsh": "gb",
|
||||
"cymraeg": "gb",
|
||||
"wales": "gb",
|
||||
"scottish": "gb",
|
||||
"gàidhlig": "gb",
|
||||
"scotland": "gb",
|
||||
"estonian"// Baltic languages (Northern Europe)
|
||||
: "ee",
|
||||
"eesti": "ee",
|
||||
"estonia": "ee",
|
||||
"latvian": "lv",
|
||||
"latviešu": "lv",
|
||||
"latvia": "lv",
|
||||
"lithuanian": "lt",
|
||||
"lietuvių": "lt",
|
||||
"lithuania": "lt",
|
||||
"hungarian"// Other European languages
|
||||
: "hu",
|
||||
"magyar": "hu",
|
||||
"hungary": "hu",
|
||||
"greek": "gr",
|
||||
"ελληνικά": "gr",
|
||||
"greece": "gr",
|
||||
"albanian": "al",
|
||||
"shqip": "al",
|
||||
"albania": "al",
|
||||
"maltese": "mt",
|
||||
"malti": "mt",
|
||||
"malta": "mt",
|
||||
"turkish"// West/Southwest Asian languages
|
||||
: "tr",
|
||||
"türkçe": "tr",
|
||||
"turkey": "tr",
|
||||
"arabic": "ar",
|
||||
"العربية": "ar",
|
||||
"arab": "ar",
|
||||
"hebrew": "il",
|
||||
"עברית": "il",
|
||||
"israel": "il",
|
||||
"brazilian"// South American languages
|
||||
: "br",
|
||||
"brazilian portuguese": "br",
|
||||
"brasil": "br",
|
||||
"brazil": "br",
|
||||
"japanese"// East Asian languages
|
||||
: "jp",
|
||||
"日本語": "jp",
|
||||
"japan": "jp",
|
||||
"korean": "kr",
|
||||
"한국어": "kr",
|
||||
"korea": "kr",
|
||||
"south korea": "kr",
|
||||
"chinese": "cn",
|
||||
"中文": "cn",
|
||||
"china": "cn",
|
||||
"simplified chinese": "cn",
|
||||
"traditional chinese": "tw",
|
||||
"taiwan": "tw",
|
||||
"繁體中文": "tw",
|
||||
"thai"// Southeast Asian languages
|
||||
: "th",
|
||||
"ไทย": "th",
|
||||
"thailand": "th",
|
||||
"vietnamese": "vn",
|
||||
"tiếng việt": "vn",
|
||||
"vietnam": "vn",
|
||||
"hindi"// South Asian languages
|
||||
: "in",
|
||||
"हिन्दी": "in",
|
||||
"india": "in",
|
||||
"afrikaans"// African languages
|
||||
: "za",
|
||||
"south africa": "za",
|
||||
"south african": "za",
|
||||
"qwerty"// Layout variants
|
||||
: "us",
|
||||
"dvorak": "us",
|
||||
"colemak": "us",
|
||||
"workman": "us",
|
||||
"azerty": "fr",
|
||||
"norman": "fr",
|
||||
"qwertz": "de"
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,14 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function _getStackTrace() {
|
||||
try {
|
||||
throw new Error("Stack trace")
|
||||
} catch (e) {
|
||||
return e.stack
|
||||
}
|
||||
}
|
||||
|
||||
function log(...args) {
|
||||
var msg = _formatMessage(...args)
|
||||
console.log(msg)
|
||||
@@ -31,4 +39,20 @@ Singleton {
|
||||
var msg = _formatMessage(...args)
|
||||
console.error(msg)
|
||||
}
|
||||
|
||||
function callStack() {
|
||||
var stack = _getStackTrace()
|
||||
Logger.log("Debug", "--------------------------")
|
||||
Logger.log("Debug", "Current call stack")
|
||||
// Split the stack into lines and log each one
|
||||
var stackLines = stack.split('\n')
|
||||
for (var i = 0; i < stackLines.length; i++) {
|
||||
var line = stackLines[i].trim() // Remove leading/trailing whitespace
|
||||
if (line.length > 0) {
|
||||
// Only log non-empty lines
|
||||
Logger.log("Debug", `- ${line}`)
|
||||
}
|
||||
}
|
||||
Logger.log("Debug", "--------------------------")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,24 +13,28 @@ Singleton {
|
||||
// Default config directory: ~/.config/noctalia
|
||||
// Default cache directory: ~/.cache/noctalia
|
||||
property string shellName: "noctalia"
|
||||
property string configDir: Quickshell.env("NOCTALIA_CONFIG_DIR") || (Quickshell.env("XDG_CONFIG_HOME")
|
||||
|| Quickshell.env(
|
||||
"HOME") + "/.config") + "/" + shellName + "/"
|
||||
property string cacheDir: Quickshell.env("NOCTALIA_CACHE_DIR") || (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env(
|
||||
"HOME") + "/.cache") + "/" + shellName + "/"
|
||||
property string configDir: Quickshell.env("NOCTALIA_CONFIG_DIR") || (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/" + shellName + "/"
|
||||
property string cacheDir: Quickshell.env("NOCTALIA_CACHE_DIR") || (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env("HOME") + "/.cache") + "/" + shellName + "/"
|
||||
property string cacheDirImages: cacheDir + "images/"
|
||||
|
||||
property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (configDir + "settings.json")
|
||||
|
||||
property string defaultWallpaper: Qt.resolvedUrl("../Assets/Tests/wallpaper.png")
|
||||
property string defaultAvatar: Quickshell.env("HOME") + "/.face"
|
||||
property string defaultVideosDirectory: Quickshell.env("HOME") + "/Videos"
|
||||
property string defaultLocation: "Tokyo"
|
||||
property string defaultWallpapersDirectory: Quickshell.env("HOME") + "/Pictures/Wallpapers"
|
||||
property string defaultWallpaper: Quickshell.shellDir + "/Assets/Wallpaper/noctalia.png"
|
||||
|
||||
// Used to access via Settings.data.xxx.yyy
|
||||
property alias data: adapter
|
||||
readonly property alias data: adapter
|
||||
|
||||
// Flag to prevent unnecessary wallpaper calls during reloads
|
||||
property bool isInitialLoad: true
|
||||
property bool isLoaded: false
|
||||
property bool directoriesCreated: false
|
||||
|
||||
// Signal emitted when settings are loaded after startupcale changes
|
||||
signal settingsLoaded
|
||||
|
||||
// -----------------------------------------------------
|
||||
// Function to validate monitor configurations
|
||||
function validateMonitorConfigurations() {
|
||||
var availableScreenNames = []
|
||||
@@ -51,24 +55,159 @@ Singleton {
|
||||
}
|
||||
}
|
||||
if (!hasValidBarMonitor) {
|
||||
Logger.log("Settings",
|
||||
"No configured bar monitors found on system, clearing bar monitor list to show on all screens")
|
||||
Logger.warn("Settings", "No configured bar monitors found on system, clearing bar monitor list to show on all screens")
|
||||
adapter.bar.monitors = []
|
||||
} else {
|
||||
Logger.log("Settings", "Found valid bar monitors, keeping configuration")
|
||||
|
||||
//Logger.log("Settings", "Found valid bar monitors, keeping configuration")
|
||||
}
|
||||
} else {
|
||||
Logger.log("Settings", "Bar monitor list is empty, will show on all available screens")
|
||||
|
||||
//Logger.log("Settings", "Bar monitor list is empty, will show on all available screens")
|
||||
}
|
||||
}
|
||||
Item {
|
||||
Component.onCompleted: {
|
||||
|
||||
// ensure settings dir exists
|
||||
Quickshell.execDetached(["mkdir", "-p", configDir])
|
||||
Quickshell.execDetached(["mkdir", "-p", cacheDir])
|
||||
Quickshell.execDetached(["mkdir", "-p", cacheDirImages])
|
||||
// -----------------------------------------------------
|
||||
// If the settings structure has changed, ensure
|
||||
// backward compatibility by upgrading the settings
|
||||
function upgradeSettingsData() {
|
||||
|
||||
const sections = ["left", "center", "right"]
|
||||
|
||||
// -----------------
|
||||
// 1st. check our settings are not super old, when we only had the widget type as a plain string
|
||||
for (var s = 0; s < sections.length; s++) {
|
||||
const sectionName = sections[s]
|
||||
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
|
||||
var widget = adapter.bar.widgets[sectionName][i]
|
||||
if (typeof widget === "string") {
|
||||
adapter.bar.widgets[sectionName][i] = {
|
||||
"id": widget
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------
|
||||
// 2nd. remove any non existing widget type
|
||||
for (var s = 0; s < sections.length; s++) {
|
||||
const sectionName = sections[s]
|
||||
const widgets = adapter.bar.widgets[sectionName]
|
||||
// Iterate backward through the widgets array, so it does not break when removing a widget
|
||||
for (var i = widgets.length - 1; i >= 0; i--) {
|
||||
var widget = widgets[i]
|
||||
if (!BarWidgetRegistry.hasWidget(widget.id)) {
|
||||
widgets.splice(i, 1)
|
||||
Logger.warn(`Settings`, `Deleted invalid widget ${widget.id}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------
|
||||
// 3nd. migrate global settings to user settings
|
||||
for (var s = 0; s < sections.length; s++) {
|
||||
const sectionName = sections[s]
|
||||
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
|
||||
var widget = adapter.bar.widgets[sectionName][i]
|
||||
|
||||
// Check if widget registry supports user settings, if it does not, then there is nothing to do
|
||||
const reg = BarWidgetRegistry.widgetMetadata[widget.id]
|
||||
if ((reg === undefined) || (reg.allowUserSettings === undefined) || !reg.allowUserSettings) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (upgradeWidget(widget)) {
|
||||
Logger.log("Settings", `Upgraded ${widget.id} widget:`, JSON.stringify(widget))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
function upgradeWidget(widget) {
|
||||
// Backup the widget definition before altering
|
||||
const widgetBefore = JSON.stringify(widget)
|
||||
|
||||
// Migrate old bar settings to proper per widget settings
|
||||
switch (widget.id) {
|
||||
case "ActiveWindow":
|
||||
widget.showIcon = widget.showIcon !== undefined ? widget.showIcon : adapter.bar.showActiveWindowIcon
|
||||
break
|
||||
case "Battery":
|
||||
widget.alwaysShowPercentage = widget.alwaysShowPercentage !== undefined ? widget.alwaysShowPercentage : adapter.bar.alwaysShowBatteryPercentage
|
||||
break
|
||||
case "Clock":
|
||||
widget.use12HourClock = widget.use12HourClock !== undefined ? widget.use12HourClock : adapter.location.use12HourClock
|
||||
widget.reverseDayMonth = widget.reverseDayMonth !== undefined ? widget.reverseDayMonth : adapter.location.reverseDayMonth
|
||||
if (widget.showDate !== undefined) {
|
||||
widget.displayFormat = "time-date"
|
||||
} else if (widget.showSeconds) {
|
||||
widget.displayFormat = "time-seconds"
|
||||
}
|
||||
delete widget.showDate
|
||||
delete widget.showSeconds
|
||||
break
|
||||
case "MediaMini":
|
||||
widget.showAlbumArt = widget.showAlbumArt !== undefined ? widget.showAlbumArt : adapter.audio.showMiniplayerAlbumArt
|
||||
widget.showVisualizer = widget.showVisualizer !== undefined ? widget.showVisualizer : adapter.audio.showMiniplayerCava
|
||||
break
|
||||
case "SidePanelToggle":
|
||||
widget.useDistroLogo = widget.useDistroLogo !== undefined ? widget.useDistroLogo : adapter.bar.useDistroLogo
|
||||
break
|
||||
case "SystemMonitor":
|
||||
widget.showNetworkStats = widget.showNetworkStats !== undefined ? widget.showNetworkStats : adapter.bar.showNetworkStats
|
||||
break
|
||||
case "Workspace":
|
||||
widget.labelMode = widget.labelMode !== undefined ? widget.labelMode : adapter.bar.showWorkspaceLabel
|
||||
break
|
||||
}
|
||||
|
||||
// Inject missing default setting (metaData) from BarWidgetRegistry
|
||||
const keys = Object.keys(BarWidgetRegistry.widgetMetadata[widget.id])
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
const k = keys[i]
|
||||
if (k === "id" || k === "allowUserSettings") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (widget[k] === undefined) {
|
||||
widget[k] = BarWidgetRegistry.widgetMetadata[widget.id][k]
|
||||
}
|
||||
}
|
||||
|
||||
// Compare settings, to detect if something has been upgraded
|
||||
const widgetAfter = JSON.stringify(widget)
|
||||
return (widgetAfter !== widgetBefore)
|
||||
}
|
||||
// -----------------------------------------------------
|
||||
// Kickoff essential services
|
||||
function kickOffServices() {
|
||||
// Ensure our location singleton is created as soon as possible so we start fetching weather asap
|
||||
LocationService.init()
|
||||
|
||||
NightLightService.apply()
|
||||
|
||||
ColorSchemeService.init()
|
||||
|
||||
MatugenService.init()
|
||||
|
||||
FontService.init()
|
||||
|
||||
HooksService.init()
|
||||
|
||||
BluetoothService.init()
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// Ensure directories exist before FileView tries to read files
|
||||
Component.onCompleted: {
|
||||
// ensure settings dir exists
|
||||
Quickshell.execDetached(["mkdir", "-p", configDir])
|
||||
Quickshell.execDetached(["mkdir", "-p", cacheDir])
|
||||
Quickshell.execDetached(["mkdir", "-p", cacheDirImages])
|
||||
|
||||
// Mark directories as created and trigger file loading
|
||||
directoriesCreated = true
|
||||
}
|
||||
|
||||
// Don't write settings to disk immediately
|
||||
@@ -82,30 +221,34 @@ Singleton {
|
||||
|
||||
FileView {
|
||||
id: settingsFileView
|
||||
path: settingsFile
|
||||
path: directoriesCreated ? settingsFile : undefined
|
||||
printErrors: false
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
onAdapterUpdated: saveTimer.start()
|
||||
Component.onCompleted: function () {
|
||||
reload()
|
||||
|
||||
// Trigger initial load when path changes from empty to actual path
|
||||
onPathChanged: {
|
||||
if (path !== undefined) {
|
||||
reload()
|
||||
}
|
||||
}
|
||||
onLoaded: function () {
|
||||
Qt.callLater(function () {
|
||||
if (isInitialLoad) {
|
||||
Logger.log("Settings", "OnLoaded")
|
||||
// Only set wallpaper on initial load, not on reloads
|
||||
if (adapter.wallpaper.current !== "") {
|
||||
Logger.log("Settings", "Set current wallpaper", adapter.wallpaper.current)
|
||||
WallpaperService.setCurrentWallpaper(adapter.wallpaper.current, true)
|
||||
}
|
||||
if (!isLoaded) {
|
||||
Logger.log("Settings", "----------------------------")
|
||||
Logger.log("Settings", "Settings loaded successfully")
|
||||
|
||||
// Validate monitor configurations, only once
|
||||
// if none of the configured monitors exist, clear the lists
|
||||
validateMonitorConfigurations()
|
||||
}
|
||||
upgradeSettingsData()
|
||||
|
||||
isInitialLoad = false
|
||||
})
|
||||
validateMonitorConfigurations()
|
||||
|
||||
kickOffServices()
|
||||
|
||||
isLoaded = true
|
||||
|
||||
// Emit the signal
|
||||
root.settingsLoaded()
|
||||
}
|
||||
}
|
||||
onLoadFailed: function (error) {
|
||||
if (error.toString().includes("No such file") || error === 2)
|
||||
@@ -116,20 +259,61 @@ Singleton {
|
||||
JsonAdapter {
|
||||
id: adapter
|
||||
|
||||
property int settingsVersion: 2
|
||||
|
||||
// bar
|
||||
property JsonObject bar: JsonObject {
|
||||
property string position: "top" // Possible values: "top", "bottom"
|
||||
property bool showActiveWindowIcon: true
|
||||
property bool alwaysShowBatteryPercentage: false
|
||||
property string position: "top" // "top", "bottom", "left", or "right"
|
||||
property real backgroundOpacity: 1.0
|
||||
property list<string> monitors: []
|
||||
|
||||
// Floating bar settings
|
||||
property bool floating: false
|
||||
property real marginVertical: 0.25
|
||||
property real marginHorizontal: 0.25
|
||||
|
||||
property bool showActiveWindowIcon: true // TODO: delete
|
||||
property bool alwaysShowBatteryPercentage: false // TODO: delete
|
||||
property bool showNetworkStats: false // TODO: delete
|
||||
property bool useDistroLogo: false // TODO: delete
|
||||
property string showWorkspaceLabel: "none" // TODO: delete
|
||||
|
||||
// Widget configuration for modular bar system
|
||||
property JsonObject widgets
|
||||
widgets: JsonObject {
|
||||
property list<string> left: ["SystemMonitor", "ActiveWindow", "MediaMini"]
|
||||
property list<string> center: ["Workspace"]
|
||||
property list<string> right: ["ScreenRecorderIndicator", "Tray", "ArchUpdater", "NotificationHistory", "WiFi", "Bluetooth", "Battery", "Volume", "Brightness", "Clock", "SidePanelToggle"]
|
||||
property list<var> left: [{
|
||||
"id": "SystemMonitor"
|
||||
}, {
|
||||
"id": "ActiveWindow"
|
||||
}, {
|
||||
"id": "MediaMini"
|
||||
}]
|
||||
property list<var> center: [{
|
||||
"id": "Workspace"
|
||||
}]
|
||||
property list<var> right: [{
|
||||
"id": "ScreenRecorderIndicator"
|
||||
}, {
|
||||
"id": "Tray"
|
||||
}, {
|
||||
"id": "NotificationHistory"
|
||||
}, {
|
||||
"id": "WiFi"
|
||||
}, {
|
||||
"id": "Bluetooth"
|
||||
}, {
|
||||
"id": "Battery"
|
||||
}, {
|
||||
"id": "Volume"
|
||||
}, {
|
||||
"id": "Brightness"
|
||||
}, {
|
||||
"id": "NightLight"
|
||||
}, {
|
||||
"id": "Clock"
|
||||
}, {
|
||||
"id": "SidePanelToggle"
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,20 +323,24 @@ Singleton {
|
||||
property bool dimDesktop: false
|
||||
property bool showScreenCorners: false
|
||||
property real radiusRatio: 1.0
|
||||
property real screenRadiusRatio: 1.0
|
||||
// Animation speed multiplier (0.1x - 2.0x)
|
||||
property real animationSpeed: 1.0
|
||||
}
|
||||
|
||||
// location
|
||||
property JsonObject location: JsonObject {
|
||||
property string name: "Tokyo"
|
||||
property string name: defaultLocation
|
||||
property bool useFahrenheit: false
|
||||
property bool reverseDayMonth: false
|
||||
property bool use12HourClock: false
|
||||
property bool showDateWithClock: false
|
||||
|
||||
property bool reverseDayMonth: false // TODO: delete
|
||||
property bool use12HourClock: false // TODO: delete
|
||||
property bool showDateWithClock: false // TODO: delete
|
||||
}
|
||||
|
||||
// screen recorder
|
||||
property JsonObject screenRecorder: JsonObject {
|
||||
property string directory: "~/Videos"
|
||||
property string directory: defaultVideosDirectory
|
||||
property int frameRate: 60
|
||||
property string audioCodec: "opus"
|
||||
property string videoCodec: "h264"
|
||||
@@ -165,38 +353,37 @@ Singleton {
|
||||
|
||||
// wallpaper
|
||||
property JsonObject wallpaper: JsonObject {
|
||||
property string directory: "/usr/share/wallpapers"
|
||||
property string current: ""
|
||||
property bool isRandom: false
|
||||
property int randomInterval: 300
|
||||
property JsonObject swww
|
||||
|
||||
onDirectoryChanged: WallpaperService.listWallpapers()
|
||||
onIsRandomChanged: WallpaperService.toggleRandomWallpaper()
|
||||
onRandomIntervalChanged: WallpaperService.restartRandomWallpaperTimer()
|
||||
|
||||
swww: JsonObject {
|
||||
property bool enabled: false
|
||||
property string resizeMethod: "crop"
|
||||
property int transitionFps: 60
|
||||
property string transitionType: "random"
|
||||
property real transitionDuration: 1.1
|
||||
}
|
||||
property bool enabled: true
|
||||
property string directory: defaultWallpapersDirectory
|
||||
property bool enableMultiMonitorDirectories: false
|
||||
property bool setWallpaperOnAllMonitors: true
|
||||
property string fillMode: "crop"
|
||||
property color fillColor: "#000000"
|
||||
property bool randomEnabled: false
|
||||
property int randomIntervalSec: 300 // 5 min
|
||||
property int transitionDuration: 1500 // 1500 ms
|
||||
property string transitionType: "random"
|
||||
property real transitionEdgeSmoothness: 0.05
|
||||
property list<var> monitors: []
|
||||
}
|
||||
|
||||
// applauncher
|
||||
property JsonObject appLauncher: JsonObject {
|
||||
// When disabled, Launcher hides clipboard command and ignores cliphist
|
||||
property bool enableClipboardHistory: true
|
||||
// Position: center, top_left, top_right, bottom_left, bottom_right
|
||||
property bool enableClipboardHistory: false
|
||||
// Position: center, top_left, top_right, bottom_left, bottom_right, bottom_center, top_center
|
||||
property string position: "center"
|
||||
property real backgroundOpacity: 1.0
|
||||
property list<string> pinnedExecs: []
|
||||
property bool useApp2Unit: false
|
||||
}
|
||||
|
||||
// dock
|
||||
property JsonObject dock: JsonObject {
|
||||
property bool autoHide: false
|
||||
property bool exclusive: false
|
||||
property real backgroundOpacity: 1.0
|
||||
property real floatingRatio: 1.0
|
||||
property list<string> monitors: []
|
||||
}
|
||||
|
||||
@@ -208,36 +395,37 @@ Singleton {
|
||||
|
||||
// notifications
|
||||
property JsonObject notifications: JsonObject {
|
||||
property bool doNotDisturb: false
|
||||
property list<string> monitors: []
|
||||
// Last time the user opened the notification history (ms since epoch)
|
||||
property real lastSeenTs: 0
|
||||
// Duration settings for different urgency levels (in seconds)
|
||||
property int lowUrgencyDuration: 3
|
||||
property int normalUrgencyDuration: 8
|
||||
property int criticalUrgencyDuration: 15
|
||||
}
|
||||
|
||||
// audio
|
||||
property JsonObject audio: JsonObject {
|
||||
property bool showMiniplayerAlbumArt: false
|
||||
property bool showMiniplayerCava: false
|
||||
property string visualizerType: "linear"
|
||||
property int volumeStep: 5
|
||||
property int cavaFrameRate: 60
|
||||
property string visualizerType: "linear"
|
||||
property list<string> mprisBlacklist: []
|
||||
property string preferredPlayer: ""
|
||||
|
||||
property bool showMiniplayerAlbumArt: false // TODO: delete
|
||||
property bool showMiniplayerCava: false // TODO: delete
|
||||
}
|
||||
|
||||
// ui
|
||||
property JsonObject ui: JsonObject {
|
||||
property string fontDefault: "Roboto" // Default font for all text
|
||||
property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal
|
||||
property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays
|
||||
|
||||
// Legacy compatibility
|
||||
property string fontFamily: fontDefault // Keep for backward compatibility
|
||||
|
||||
// Idle inhibitor state
|
||||
property string fontDefault: "Roboto"
|
||||
property string fontFixed: "DejaVu Sans Mono"
|
||||
property string fontBillboard: "Inter"
|
||||
property list<var> monitorsScaling: []
|
||||
property bool idleInhibitorEnabled: false
|
||||
}
|
||||
|
||||
// Scaling (not stored inside JsonObject, or it crashes)
|
||||
property var monitorsScaling: {
|
||||
|
||||
}
|
||||
|
||||
// brightness
|
||||
property JsonObject brightness: JsonObject {
|
||||
property int brightnessStep: 5
|
||||
@@ -247,8 +435,39 @@ Singleton {
|
||||
property bool useWallpaperColors: false
|
||||
property string predefinedScheme: ""
|
||||
property bool darkMode: true
|
||||
// External app theming (GTK & Qt)
|
||||
property bool themeApps: false
|
||||
}
|
||||
|
||||
// matugen templates toggles
|
||||
property JsonObject matugen: JsonObject {
|
||||
// Per-template flags to control dynamic config generation
|
||||
property bool gtk4: false
|
||||
property bool gtk3: false
|
||||
property bool qt6: false
|
||||
property bool qt5: false
|
||||
property bool kitty: false
|
||||
property bool ghostty: false
|
||||
property bool foot: false
|
||||
property bool fuzzel: false
|
||||
property bool vesktop: false
|
||||
property bool enableUserTemplates: false
|
||||
}
|
||||
|
||||
// night light
|
||||
property JsonObject nightLight: JsonObject {
|
||||
property bool enabled: false
|
||||
property bool forced: false
|
||||
property bool autoSchedule: true
|
||||
property string nightTemp: "4000"
|
||||
property string dayTemp: "6500"
|
||||
property string manualSunrise: "06:30"
|
||||
property string manualSunset: "18:30"
|
||||
}
|
||||
|
||||
// hooks
|
||||
property JsonObject hooks: JsonObject {
|
||||
property bool enabled: false
|
||||
property string wallpaperChange: ""
|
||||
property string darkModeChange: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ Singleton {
|
||||
*/
|
||||
|
||||
// Font size
|
||||
property real fontSizeXXS: 8
|
||||
property real fontSizeXS: 9
|
||||
property real fontSizeS: 10
|
||||
property real fontSizeM: 11
|
||||
@@ -28,11 +29,15 @@ Singleton {
|
||||
property int fontWeightBold: 700
|
||||
|
||||
// Radii
|
||||
property int radiusXXS: 4 * Settings.data.general.radiusRatio
|
||||
property int radiusXS: 8 * Settings.data.general.radiusRatio
|
||||
property int radiusS: 12 * Settings.data.general.radiusRatio
|
||||
property int radiusM: 16 * Settings.data.general.radiusRatio
|
||||
property int radiusL: 20 * Settings.data.general.radiusRatio
|
||||
|
||||
//screen Radii
|
||||
property int screenRadius: 20 * Settings.data.general.screenRadiusRatio
|
||||
|
||||
// Border
|
||||
property int borderS: 1
|
||||
property int borderM: 2
|
||||
@@ -55,14 +60,15 @@ Singleton {
|
||||
property real opacityFull: 1.0
|
||||
|
||||
// Animation duration (ms)
|
||||
property int animationFast: 150
|
||||
property int animationNormal: 300
|
||||
property int animationSlow: 450
|
||||
property int animationFast: Math.round(150 / Settings.data.general.animationSpeed)
|
||||
property int animationNormal: Math.round(300 / Settings.data.general.animationSpeed)
|
||||
property int animationSlow: Math.round(450 / Settings.data.general.animationSpeed)
|
||||
property int animationSlowest: Math.round(750 / Settings.data.general.animationSpeed)
|
||||
|
||||
// Dimensions
|
||||
property int barHeight: 36
|
||||
property int barHeight: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 39 : 37
|
||||
property int capsuleHeight: (barHeight * 0.73)
|
||||
property int baseWidgetSize: 32
|
||||
property int baseWidgetSize: (barHeight * 0.9)
|
||||
property int sliderWidth: 200
|
||||
|
||||
// Delays
|
||||
|
||||
@@ -9,51 +9,38 @@ Singleton {
|
||||
id: root
|
||||
|
||||
property var date: new Date()
|
||||
property string time: {
|
||||
let timeFormat = Settings.data.location.use12HourClock ? "h:mm AP" : "HH:mm"
|
||||
let timeString = Qt.formatDateTime(date, timeFormat)
|
||||
|
||||
if (Settings.data.location.showDateWithClock) {
|
||||
let dayName = date.toLocaleDateString(Qt.locale(), "ddd")
|
||||
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
|
||||
let day = date.getDate()
|
||||
let month = date.toLocaleDateString(Qt.locale(), "MMM")
|
||||
return timeString + " - " + dayName + ", " + day + " " + month
|
||||
}
|
||||
|
||||
return timeString
|
||||
// Returns a Unix Timestamp (in seconds)
|
||||
readonly property int timestamp: {
|
||||
return Math.floor(date / 1000)
|
||||
}
|
||||
readonly property string dateString: {
|
||||
|
||||
function formatDate(reverseDayMonth = true) {
|
||||
let now = date
|
||||
let dayName = now.toLocaleDateString(Qt.locale(), "ddd")
|
||||
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
|
||||
let day = now.getDate()
|
||||
let suffix
|
||||
if (day > 3 && day < 21)
|
||||
suffix = 'th'
|
||||
suffix = 'th'
|
||||
else
|
||||
switch (day % 10) {
|
||||
switch (day % 10) {
|
||||
case 1:
|
||||
suffix = "st"
|
||||
break
|
||||
suffix = "st"
|
||||
break
|
||||
case 2:
|
||||
suffix = "nd"
|
||||
break
|
||||
suffix = "nd"
|
||||
break
|
||||
case 3:
|
||||
suffix = "rd"
|
||||
break
|
||||
suffix = "rd"
|
||||
break
|
||||
default:
|
||||
suffix = "th"
|
||||
}
|
||||
suffix = "th"
|
||||
}
|
||||
let month = now.toLocaleDateString(Qt.locale(), "MMMM")
|
||||
let year = now.toLocaleDateString(Qt.locale(), "yyyy")
|
||||
return `${dayName}, `
|
||||
+ (Settings.data.location.reverseDayMonth ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`)
|
||||
}
|
||||
|
||||
// Returns a Unix Timestamp (in seconds)
|
||||
readonly property int timestamp: {
|
||||
return Math.floor(date / 1000)
|
||||
return `${dayName}, ` + (reverseDayMonth ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -77,23 +64,34 @@ Singleton {
|
||||
}
|
||||
|
||||
// Format an easy to read approximate duration ex: 4h32m
|
||||
// Used to display the time remaining on the Battery widget
|
||||
// Used to display the time remaining on the Battery widget, computer uptime, etc..
|
||||
function formatVagueHumanReadableDuration(totalSeconds) {
|
||||
const hours = Math.floor(totalSeconds / 3600)
|
||||
const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60)
|
||||
const seconds = totalSeconds - (hours * 3600) - (minutes * 60)
|
||||
if (typeof totalSeconds !== 'number' || totalSeconds < 0) {
|
||||
return '0s'
|
||||
}
|
||||
|
||||
var str = ""
|
||||
if (hours) {
|
||||
str += hours.toString() + "h"
|
||||
}
|
||||
if (minutes) {
|
||||
str += minutes.toString() + "m"
|
||||
}
|
||||
// Floor the input to handle decimal seconds
|
||||
totalSeconds = Math.floor(totalSeconds)
|
||||
|
||||
const days = Math.floor(totalSeconds / 86400)
|
||||
const hours = Math.floor((totalSeconds % 86400) / 3600)
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||
const seconds = totalSeconds % 60
|
||||
|
||||
const parts = []
|
||||
if (days)
|
||||
parts.push(`${days}d`)
|
||||
if (hours)
|
||||
parts.push(`${hours}h`)
|
||||
if (minutes)
|
||||
parts.push(`${minutes}m`)
|
||||
|
||||
// Only show seconds if no hours and no minutes
|
||||
if (!hours && !minutes) {
|
||||
str += seconds.toString() + "s"
|
||||
parts.push(`${seconds}s`)
|
||||
}
|
||||
return str
|
||||
|
||||
return parts.join('')
|
||||
}
|
||||
|
||||
Timer {
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
NPanel {
|
||||
id: root
|
||||
panelWidth: 380 * scaling
|
||||
panelHeight: 500 * scaling
|
||||
panelAnchorRight: true
|
||||
|
||||
// Auto-refresh when service updates
|
||||
Connections {
|
||||
target: ArchUpdaterService
|
||||
function onUpdatePackagesChanged() {
|
||||
// Force UI update when packages change
|
||||
if (root.visible) {
|
||||
// Small delay to ensure data is fully updated
|
||||
Qt.callLater(() => {
|
||||
// Force a UI update by triggering a property change
|
||||
ArchUpdaterService.updatePackages = ArchUpdaterService.updatePackages
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
panelContent: Rectangle {
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusL * scaling
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NIcon {
|
||||
text: "system_update"
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "System Updates"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.family: Settings.data.ui.fontDefault
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: "Close"
|
||||
sizeMultiplier: 0.8
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Update summary
|
||||
Text {
|
||||
text: ArchUpdaterService.updatePackages.length + " package" + (ArchUpdaterService.updatePackages.length
|
||||
!== 1 ? "s" : "") + " can be updated"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.family: Settings.data.ui.fontDefault
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Package selection info
|
||||
Text {
|
||||
text: ArchUpdaterService.selectedPackagesCount + " of " + ArchUpdaterService.updatePackages.length + " packages selected"
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.family: Settings.data.ui.fontDefault
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Package list
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusM * scaling
|
||||
|
||||
ListView {
|
||||
id: packageListView
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS * scaling
|
||||
clip: true
|
||||
model: ArchUpdaterService.updatePackages
|
||||
spacing: Style.marginXS * scaling
|
||||
|
||||
delegate: Rectangle {
|
||||
width: packageListView.width
|
||||
height: 50 * scaling
|
||||
color: Color.transparent
|
||||
radius: Style.radiusS * scaling
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
// Checkbox for selection
|
||||
NIconButton {
|
||||
id: checkbox
|
||||
icon: "check_box_outline_blank"
|
||||
onClicked: {
|
||||
const isSelected = ArchUpdaterService.isPackageSelected(modelData.name)
|
||||
if (isSelected) {
|
||||
ArchUpdaterService.togglePackageSelection(modelData.name)
|
||||
icon = "check_box_outline_blank"
|
||||
colorFg = Color.mOnSurfaceVariant
|
||||
} else {
|
||||
ArchUpdaterService.togglePackageSelection(modelData.name)
|
||||
icon = "check_box"
|
||||
colorFg = Color.mPrimary
|
||||
}
|
||||
}
|
||||
colorBg: Color.transparent
|
||||
colorFg: Color.mOnSurfaceVariant
|
||||
Layout.preferredWidth: 30 * scaling
|
||||
Layout.preferredHeight: 30 * scaling
|
||||
|
||||
Component.onCompleted: {
|
||||
// Set initial state
|
||||
if (ArchUpdaterService.isPackageSelected(modelData.name)) {
|
||||
icon = "check_box"
|
||||
colorFg = Color.mPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Package info
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginXXS * scaling
|
||||
|
||||
Text {
|
||||
text: modelData.name
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.family: Settings.data.ui.fontDefault
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.oldVersion + " → " + modelData.newVersion
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.family: Settings.data.ui.fontDefault
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
policy: ScrollBar.AsNeeded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NIconButton {
|
||||
icon: "refresh"
|
||||
tooltipText: "Check for updates"
|
||||
onClicked: {
|
||||
ArchUpdaterService.doPoll()
|
||||
}
|
||||
colorBg: Color.mSurfaceVariant
|
||||
colorFg: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 35 * scaling
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "system_update"
|
||||
tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update all packages"
|
||||
enabled: !ArchUpdaterService.updateInProgress
|
||||
onClicked: {
|
||||
ArchUpdaterService.runUpdate()
|
||||
root.close()
|
||||
}
|
||||
colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : Color.mPrimary
|
||||
colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : Color.mOnPrimary
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 35 * scaling
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "settings"
|
||||
tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update selected packages"
|
||||
enabled: !ArchUpdaterService.updateInProgress && ArchUpdaterService.selectedPackagesCount > 0
|
||||
onClicked: {
|
||||
if (ArchUpdaterService.selectedPackagesCount > 0) {
|
||||
ArchUpdaterService.runSelectiveUpdate()
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : (ArchUpdaterService.selectedPackagesCount
|
||||
> 0 ? Color.mSecondary : Color.mSurfaceVariant)
|
||||
colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : (ArchUpdaterService.selectedPackagesCount
|
||||
> 0 ? Color.mOnSecondary : Color.mOnSurfaceVariant)
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 35 * scaling
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,28 +3,77 @@ import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Widgets
|
||||
|
||||
Loader {
|
||||
active: !Settings.data.wallpaper.swww.enabled
|
||||
Variants {
|
||||
id: backgroundVariants
|
||||
model: Quickshell.screens
|
||||
|
||||
sourceComponent: Variants {
|
||||
model: Quickshell.screens
|
||||
delegate: Loader {
|
||||
|
||||
delegate: PanelWindow {
|
||||
required property ShellScreen modelData
|
||||
property string wallpaperSource: WallpaperService.currentWallpaper !== ""
|
||||
&& !Settings.data.wallpaper.swww.enabled ? WallpaperService.currentWallpaper : ""
|
||||
required property ShellScreen modelData
|
||||
|
||||
visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled
|
||||
active: Settings.isLoaded && modelData && Settings.data.wallpaper.enabled
|
||||
|
||||
// Force update when SWWW setting changes
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
sourceComponent: PanelWindow {
|
||||
id: root
|
||||
|
||||
} else {
|
||||
// Internal state management
|
||||
property string transitionType: "fade"
|
||||
property real transitionProgress: 0
|
||||
|
||||
readonly property real edgeSmoothness: Settings.data.wallpaper.transitionEdgeSmoothness
|
||||
readonly property var allTransitions: WallpaperService.allTransitions
|
||||
readonly property bool transitioning: transitionAnimation.running
|
||||
|
||||
// Wipe direction: 0=left, 1=right, 2=up, 3=down
|
||||
property real wipeDirection: 0
|
||||
|
||||
// Disc
|
||||
property real discCenterX: 0.5
|
||||
property real discCenterY: 0.5
|
||||
|
||||
// Stripe
|
||||
property real stripesCount: 16
|
||||
property real stripesAngle: 0
|
||||
|
||||
// Used to debounce wallpaper changes
|
||||
property string futureWallpaper: ""
|
||||
|
||||
// Fillmode default is "crop"
|
||||
property real fillMode: 1.0
|
||||
property vector4d fillColor: Qt.vector4d(Settings.data.wallpaper.fillColor.r, Settings.data.wallpaper.fillColor.g, Settings.data.wallpaper.fillColor.b, 1.0)
|
||||
|
||||
// On startup assign wallpaper immediately
|
||||
Component.onCompleted: {
|
||||
fillMode = WallpaperService.getFillModeUniform()
|
||||
|
||||
var path = modelData ? WallpaperService.getWallpaper(modelData.name) : ""
|
||||
setWallpaperImmediate(path)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Settings.data.wallpaper
|
||||
function onFillModeChanged() {
|
||||
fillMode = WallpaperService.getFillModeUniform()
|
||||
}
|
||||
}
|
||||
|
||||
// External state management
|
||||
Connections {
|
||||
target: WallpaperService
|
||||
function onWallpaperChanged(screenName, path) {
|
||||
if (screenName === modelData.name) {
|
||||
|
||||
// Update wallpaper display
|
||||
// Set wallpaper immediately on startup
|
||||
futureWallpaper = path
|
||||
debounceTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
color: Color.transparent
|
||||
screen: modelData
|
||||
WlrLayershell.layer: WlrLayer.Background
|
||||
@@ -38,18 +87,222 @@ Loader {
|
||||
left: true
|
||||
}
|
||||
|
||||
margins {
|
||||
top: 0
|
||||
Timer {
|
||||
id: debounceTimer
|
||||
interval: 333
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
changeWallpaper()
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
source: wallpaperSource
|
||||
visible: wallpaperSource !== ""
|
||||
cache: true
|
||||
id: currentWallpaper
|
||||
source: ""
|
||||
smooth: true
|
||||
mipmap: false
|
||||
visible: false
|
||||
cache: false
|
||||
asynchronous: true
|
||||
}
|
||||
|
||||
Image {
|
||||
id: nextWallpaper
|
||||
source: ""
|
||||
smooth: true
|
||||
mipmap: false
|
||||
visible: false
|
||||
cache: false
|
||||
asynchronous: true
|
||||
}
|
||||
|
||||
// Fade or None transition shader
|
||||
ShaderEffect {
|
||||
id: fadeShader
|
||||
anchors.fill: parent
|
||||
visible: transitionType === "fade" || transitionType === "none"
|
||||
|
||||
property variant source1: currentWallpaper
|
||||
property variant source2: nextWallpaper
|
||||
property real progress: root.transitionProgress
|
||||
|
||||
// Fill mode properties
|
||||
property real fillMode: root.fillMode
|
||||
property vector4d fillColor: root.fillColor
|
||||
property real imageWidth1: source1.sourceSize.width
|
||||
property real imageHeight1: source1.sourceSize.height
|
||||
property real imageWidth2: source2.sourceSize.width
|
||||
property real imageHeight2: source2.sourceSize.height
|
||||
property real screenWidth: width
|
||||
property real screenHeight: height
|
||||
|
||||
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_fade.frag.qsb")
|
||||
}
|
||||
|
||||
// Wipe transition shader
|
||||
ShaderEffect {
|
||||
id: wipeShader
|
||||
anchors.fill: parent
|
||||
visible: transitionType === "wipe"
|
||||
|
||||
property variant source1: currentWallpaper
|
||||
property variant source2: nextWallpaper
|
||||
property real progress: root.transitionProgress
|
||||
property real smoothness: root.edgeSmoothness
|
||||
property real direction: root.wipeDirection
|
||||
|
||||
// Fill mode properties
|
||||
property real fillMode: root.fillMode
|
||||
property vector4d fillColor: root.fillColor
|
||||
property real imageWidth1: source1.sourceSize.width
|
||||
property real imageHeight1: source1.sourceSize.height
|
||||
property real imageWidth2: source2.sourceSize.width
|
||||
property real imageHeight2: source2.sourceSize.height
|
||||
property real screenWidth: width
|
||||
property real screenHeight: height
|
||||
|
||||
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_wipe.frag.qsb")
|
||||
}
|
||||
|
||||
// Disc reveal transition shader
|
||||
ShaderEffect {
|
||||
id: discShader
|
||||
anchors.fill: parent
|
||||
visible: transitionType === "disc"
|
||||
|
||||
property variant source1: currentWallpaper
|
||||
property variant source2: nextWallpaper
|
||||
property real progress: root.transitionProgress
|
||||
property real smoothness: root.edgeSmoothness
|
||||
property real aspectRatio: root.width / root.height
|
||||
property real centerX: root.discCenterX
|
||||
property real centerY: root.discCenterY
|
||||
|
||||
// Fill mode properties
|
||||
property real fillMode: root.fillMode
|
||||
property vector4d fillColor: root.fillColor
|
||||
property real imageWidth1: source1.sourceSize.width
|
||||
property real imageHeight1: source1.sourceSize.height
|
||||
property real imageWidth2: source2.sourceSize.width
|
||||
property real imageHeight2: source2.sourceSize.height
|
||||
property real screenWidth: width
|
||||
property real screenHeight: height
|
||||
|
||||
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_disc.frag.qsb")
|
||||
}
|
||||
|
||||
// Diagonal stripes transition shader
|
||||
ShaderEffect {
|
||||
id: stripesShader
|
||||
anchors.fill: parent
|
||||
visible: transitionType === "stripes"
|
||||
|
||||
property variant source1: currentWallpaper
|
||||
property variant source2: nextWallpaper
|
||||
property real progress: root.transitionProgress
|
||||
property real smoothness: root.edgeSmoothness
|
||||
property real aspectRatio: root.width / root.height
|
||||
property real stripeCount: root.stripesCount
|
||||
property real angle: root.stripesAngle
|
||||
|
||||
// Fill mode properties
|
||||
property real fillMode: root.fillMode
|
||||
property vector4d fillColor: root.fillColor
|
||||
property real imageWidth1: source1.sourceSize.width
|
||||
property real imageHeight1: source1.sourceSize.height
|
||||
property real imageWidth2: source2.sourceSize.width
|
||||
property real imageHeight2: source2.sourceSize.height
|
||||
property real screenWidth: width
|
||||
property real screenHeight: height
|
||||
|
||||
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_stripes.frag.qsb")
|
||||
}
|
||||
|
||||
// Animation for the transition progress
|
||||
NumberAnimation {
|
||||
id: transitionAnimation
|
||||
target: root
|
||||
property: "transitionProgress"
|
||||
from: 0.0
|
||||
to: 1.0
|
||||
// The stripes shader feels faster visually, we make it a bit slower here.
|
||||
duration: transitionType == "stripes" ? Settings.data.wallpaper.transitionDuration * 1.6 : Settings.data.wallpaper.transitionDuration
|
||||
easing.type: Easing.InOutCubic
|
||||
onFinished: {
|
||||
// Swap images after transition completes
|
||||
currentWallpaper.source = nextWallpaper.source
|
||||
nextWallpaper.source = ""
|
||||
transitionProgress = 0.0
|
||||
Qt.callLater(() => {
|
||||
currentWallpaper.asynchronous = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function setWallpaperImmediate(source) {
|
||||
transitionAnimation.stop()
|
||||
transitionProgress = 0.0
|
||||
currentWallpaper.source = source
|
||||
nextWallpaper.source = ""
|
||||
}
|
||||
|
||||
function setWallpaperWithTransition(source) {
|
||||
if (source === currentWallpaper.source) {
|
||||
return
|
||||
}
|
||||
|
||||
if (transitioning) {
|
||||
// We are interrupting a transition
|
||||
transitionAnimation.stop()
|
||||
transitionProgress = 0
|
||||
currentWallpaper.source = nextWallpaper.source
|
||||
nextWallpaper.source = ""
|
||||
}
|
||||
|
||||
nextWallpaper.source = source
|
||||
currentWallpaper.asynchronous = false
|
||||
transitionAnimation.start()
|
||||
}
|
||||
|
||||
// Main method that actually trigger the wallpaper change
|
||||
function changeWallpaper() {
|
||||
// Get the transitionType from the settings
|
||||
transitionType = Settings.data.wallpaper.transitionType
|
||||
|
||||
if (transitionType == "random") {
|
||||
var index = Math.floor(Math.random() * allTransitions.length)
|
||||
transitionType = allTransitions[index]
|
||||
}
|
||||
|
||||
// Ensure the transition type really exists
|
||||
if (transitionType !== "none" && !allTransitions.includes(transitionType)) {
|
||||
transitionType = "fade"
|
||||
}
|
||||
|
||||
//Logger.log("Background", "New wallpaper: ", futureWallpaper, "On:", modelData.name, "Transition:", transitionType)
|
||||
switch (transitionType) {
|
||||
case "none":
|
||||
setWallpaperImmediate(futureWallpaper)
|
||||
break
|
||||
case "wipe":
|
||||
wipeDirection = Math.random() * 4
|
||||
setWallpaperWithTransition(futureWallpaper)
|
||||
break
|
||||
case "disc":
|
||||
discCenterX = Math.random()
|
||||
discCenterY = Math.random()
|
||||
setWallpaperWithTransition(futureWallpaper)
|
||||
break
|
||||
case "stripes":
|
||||
stripesCount = Math.round(Math.random() * 20 + 4)
|
||||
stripesAngle = Math.random() * 360
|
||||
setWallpaperWithTransition(futureWallpaper)
|
||||
break
|
||||
default:
|
||||
setWallpaperWithTransition(futureWallpaper)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,24 +6,34 @@ import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Loader {
|
||||
active: CompositorService.isNiri
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
Component.onCompleted: {
|
||||
if (CompositorService.isNiri) {
|
||||
Logger.log("Overview", "Loading Overview component for Niri")
|
||||
}
|
||||
}
|
||||
delegate: Loader {
|
||||
required property ShellScreen modelData
|
||||
|
||||
sourceComponent: Variants {
|
||||
model: Quickshell.screens
|
||||
active: Settings.isLoaded && CompositorService.isNiri && modelData && Settings.data.wallpaper.enabled
|
||||
|
||||
delegate: PanelWindow {
|
||||
required property ShellScreen modelData
|
||||
property string wallpaperSource: WallpaperService.currentWallpaper !== ""
|
||||
&& !Settings.data.wallpaper.swww.enabled ? WallpaperService.currentWallpaper : ""
|
||||
property string wallpaper: ""
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
Component.onCompleted: {
|
||||
if (modelData) {
|
||||
Logger.log("Overview", "Loading Overview component for Niri on", modelData.name)
|
||||
}
|
||||
wallpaper = modelData ? WallpaperService.getWallpaper(modelData.name) : ""
|
||||
}
|
||||
|
||||
// External state management
|
||||
Connections {
|
||||
target: WallpaperService
|
||||
function onWallpaperChanged(screenName, path) {
|
||||
if (screenName === modelData.name) {
|
||||
wallpaper = path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled
|
||||
color: Color.transparent
|
||||
screen: modelData
|
||||
WlrLayershell.layer: WlrLayer.Background
|
||||
@@ -39,29 +49,27 @@ Loader {
|
||||
|
||||
Image {
|
||||
id: bgImage
|
||||
|
||||
anchors.fill: parent
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
source: wallpaperSource
|
||||
cache: true
|
||||
source: wallpaper
|
||||
smooth: true
|
||||
mipmap: false
|
||||
visible: wallpaperSource !== ""
|
||||
cache: false
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
id: overviewBgBlur
|
||||
|
||||
anchors.fill: parent
|
||||
source: bgImage
|
||||
autoPaddingEnabled: false
|
||||
blurEnabled: true
|
||||
blur: 0.48
|
||||
blurMax: 128
|
||||
}
|
||||
|
||||
// Make the overview darker
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(Color.mSurface.r, Color.mSurface.g, Color.mSurface.b, 0.5)
|
||||
color: Settings.data.colorSchemes.darkMode ? Qt.alpha(Color.mSurface, Style.opacityMedium) : Qt.alpha(Color.mOnSurface, Style.opacityMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Loader {
|
||||
active: Settings.data.general.showScreenCorners
|
||||
active: Settings.data.general.showScreenCorners && !Settings.data.bar.floating
|
||||
|
||||
sourceComponent: Variants {
|
||||
model: Quickshell.screens
|
||||
@@ -16,23 +16,26 @@ Loader {
|
||||
id: root
|
||||
|
||||
required property ShellScreen modelData
|
||||
readonly property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: ScalingService.getScreenScale(screen)
|
||||
screen: modelData
|
||||
|
||||
// Visible color
|
||||
property color ringColor: Qt.rgba(Color.mSurface.r, Color.mSurface.g, Color.mSurface.b,
|
||||
Settings.data.bar.backgroundOpacity)
|
||||
// The amount subtracted from full size for the inner cutout
|
||||
// Inner size = full size - borderWidth (per axis)
|
||||
property int borderWidth: Style.borderM
|
||||
// Rounded radius for the inner cutout
|
||||
property int innerRadius: 20
|
||||
property color cornerColor: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity)
|
||||
property real cornerRadius: Style.screenRadius * scaling
|
||||
property real cornerSize: Style.screenRadius * scaling
|
||||
|
||||
Connections {
|
||||
target: ScalingService
|
||||
function onScaleChanged(screenName, scale) {
|
||||
if (screenName === screen.name) {
|
||||
scaling = scale
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
color: Color.transparent
|
||||
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.namespace: "quickshell-corner"
|
||||
// Do not take keyboard focus and make the surface click-through
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
@@ -43,111 +46,203 @@ Loader {
|
||||
}
|
||||
|
||||
margins {
|
||||
top: ((modelData && Settings.data.bar.monitors.includes(modelData.name))
|
||||
|| (Settings.data.bar.monitors.length === 0))
|
||||
&& Settings.data.bar.position === "top" ? Math.floor(Style.barHeight * scaling) : 0
|
||||
bottom: ((modelData && Settings.data.bar.monitors.includes(modelData.name))
|
||||
|| (Settings.data.bar.monitors.length === 0))
|
||||
&& Settings.data.bar.position === "bottom" ? Math.floor(Style.barHeight * scaling) : 0
|
||||
}
|
||||
|
||||
// Source we want to show only as a ring
|
||||
Rectangle {
|
||||
id: overlaySource
|
||||
|
||||
anchors.fill: parent
|
||||
color: root.ringColor
|
||||
}
|
||||
|
||||
// Texture for overlaySource
|
||||
ShaderEffectSource {
|
||||
id: overlayTexture
|
||||
|
||||
anchors.fill: parent
|
||||
sourceItem: overlaySource
|
||||
hideSource: true
|
||||
live: true
|
||||
visible: false
|
||||
}
|
||||
|
||||
// Mask via Canvas: paint opaque white, then punch rounded inner hole
|
||||
Canvas {
|
||||
id: maskSource
|
||||
|
||||
anchors.fill: parent
|
||||
antialiasing: true
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
onPaint: {
|
||||
const ctx = getContext("2d")
|
||||
ctx.reset()
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
// Solid white base (alpha=1)
|
||||
ctx.globalCompositeOperation = "source-over"
|
||||
ctx.fillStyle = "#ffffffff"
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
// Punch hole using destination-out with rounded rect path
|
||||
const x = Math.round(root.borderWidth / 2)
|
||||
const y = Math.round(root.borderWidth / 2)
|
||||
const w = Math.max(0, width - root.borderWidth)
|
||||
const h = Math.max(0, height - root.borderWidth)
|
||||
const r = Math.max(0, Math.min(root.innerRadius, Math.min(w, h) / 2))
|
||||
ctx.globalCompositeOperation = "destination-out"
|
||||
ctx.fillStyle = "#ffffffff"
|
||||
ctx.beginPath()
|
||||
// rounded rectangle path using arcTo
|
||||
ctx.moveTo(x + r, y)
|
||||
ctx.lineTo(x + w - r, y)
|
||||
ctx.arcTo(x + w, y, x + w, y + r, r)
|
||||
ctx.lineTo(x + w, y + h - r)
|
||||
ctx.arcTo(x + w, y + h, x + w - r, y + h, r)
|
||||
ctx.lineTo(x + r, y + h)
|
||||
ctx.arcTo(x, y + h, x, y + h - r, r)
|
||||
ctx.lineTo(x, y + r)
|
||||
ctx.arcTo(x, y, x + r, y, r)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
}
|
||||
onWidthChanged: requestPaint()
|
||||
onHeightChanged: requestPaint()
|
||||
}
|
||||
|
||||
// Repaint mask when properties change
|
||||
Connections {
|
||||
function onBorderWidthChanged() {
|
||||
maskSource.requestPaint()
|
||||
}
|
||||
|
||||
function onRingColorChanged() {}
|
||||
|
||||
function onInnerRadiusChanged() {
|
||||
maskSource.requestPaint()
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
|
||||
// Texture for maskSource; hides the original
|
||||
ShaderEffectSource {
|
||||
id: maskTexture
|
||||
|
||||
anchors.fill: parent
|
||||
sourceItem: maskSource
|
||||
hideSource: true
|
||||
live: true
|
||||
visible: false
|
||||
}
|
||||
|
||||
// Apply mask to show only the ring area
|
||||
MultiEffect {
|
||||
anchors.fill: parent
|
||||
source: overlayTexture
|
||||
maskEnabled: true
|
||||
maskSource: maskTexture
|
||||
maskInverted: false
|
||||
maskSpreadAtMax: 0.75
|
||||
top: ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "top" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
|
||||
bottom: ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "bottom" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
|
||||
left: ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "left" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
|
||||
right: ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "right" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
|
||||
}
|
||||
|
||||
mask: Region {}
|
||||
|
||||
// Top-left concave corner
|
||||
Canvas {
|
||||
id: topLeftCorner
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
width: cornerSize
|
||||
height: cornerSize
|
||||
antialiasing: true
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
smooth: false
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d")
|
||||
if (!ctx)
|
||||
return
|
||||
|
||||
ctx.reset()
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
// Fill the entire area with the corner color
|
||||
ctx.fillStyle = root.cornerColor
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
|
||||
// Cut out the rounded corner using destination-out
|
||||
ctx.globalCompositeOperation = "destination-out"
|
||||
ctx.fillStyle = "#ffffff"
|
||||
ctx.beginPath()
|
||||
ctx.arc(width, height, root.cornerRadius, 0, 2 * Math.PI)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
onWidthChanged: if (available)
|
||||
requestPaint()
|
||||
onHeightChanged: if (available)
|
||||
requestPaint()
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onCornerColorChanged() {
|
||||
if (topLeftCorner.available)
|
||||
topLeftCorner.requestPaint()
|
||||
}
|
||||
function onCornerRadiusChanged() {
|
||||
if (topLeftCorner.available)
|
||||
topLeftCorner.requestPaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Top-right concave corner
|
||||
Canvas {
|
||||
id: topRightCorner
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
width: cornerSize
|
||||
height: cornerSize
|
||||
antialiasing: true
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
smooth: true
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d")
|
||||
if (!ctx)
|
||||
return
|
||||
|
||||
ctx.reset()
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
ctx.fillStyle = root.cornerColor
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
|
||||
ctx.globalCompositeOperation = "destination-out"
|
||||
ctx.fillStyle = "#ffffff"
|
||||
ctx.beginPath()
|
||||
ctx.arc(0, height, root.cornerRadius, 0, 2 * Math.PI)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
onWidthChanged: if (available)
|
||||
requestPaint()
|
||||
onHeightChanged: if (available)
|
||||
requestPaint()
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onCornerColorChanged() {
|
||||
if (topRightCorner.available)
|
||||
topRightCorner.requestPaint()
|
||||
}
|
||||
function onCornerRadiusChanged() {
|
||||
if (topRightCorner.available)
|
||||
topRightCorner.requestPaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom-left concave corner
|
||||
Canvas {
|
||||
id: bottomLeftCorner
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
width: cornerSize
|
||||
height: cornerSize
|
||||
antialiasing: true
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
smooth: true
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d")
|
||||
if (!ctx)
|
||||
return
|
||||
|
||||
ctx.reset()
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
ctx.fillStyle = root.cornerColor
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
|
||||
ctx.globalCompositeOperation = "destination-out"
|
||||
ctx.fillStyle = "#ffffff"
|
||||
ctx.beginPath()
|
||||
ctx.arc(width, 0, root.cornerRadius, 0, 2 * Math.PI)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
onWidthChanged: if (available)
|
||||
requestPaint()
|
||||
onHeightChanged: if (available)
|
||||
requestPaint()
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onCornerColorChanged() {
|
||||
if (bottomLeftCorner.available)
|
||||
bottomLeftCorner.requestPaint()
|
||||
}
|
||||
function onCornerRadiusChanged() {
|
||||
if (bottomLeftCorner.available)
|
||||
bottomLeftCorner.requestPaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom-right concave corner
|
||||
Canvas {
|
||||
id: bottomRightCorner
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
width: cornerSize
|
||||
height: cornerSize
|
||||
antialiasing: true
|
||||
renderTarget: Canvas.FramebufferObject
|
||||
smooth: true
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d")
|
||||
if (!ctx)
|
||||
return
|
||||
|
||||
ctx.reset()
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
ctx.fillStyle = root.cornerColor
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
|
||||
ctx.globalCompositeOperation = "destination-out"
|
||||
ctx.fillStyle = "#ffffff"
|
||||
ctx.beginPath()
|
||||
ctx.arc(0, 0, root.cornerRadius, 0, 2 * Math.PI)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
onWidthChanged: if (available)
|
||||
requestPaint()
|
||||
onHeightChanged: if (available)
|
||||
requestPaint()
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onCornerColorChanged() {
|
||||
if (bottomRightCorner.available)
|
||||
bottomRightCorner.requestPaint()
|
||||
}
|
||||
function onCornerRadiusChanged() {
|
||||
if (bottomRightCorner.available)
|
||||
bottomRightCorner.requestPaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,115 +12,235 @@ import qs.Modules.Notification
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
delegate: PanelWindow {
|
||||
delegate: Loader {
|
||||
id: root
|
||||
|
||||
required property ShellScreen modelData
|
||||
readonly property real scaling: ScalingService.scale(screen)
|
||||
screen: modelData
|
||||
property real scaling: ScalingService.getScreenScale(modelData)
|
||||
|
||||
WlrLayershell.namespace: "noctalia-bar"
|
||||
|
||||
implicitHeight: Style.barHeight * scaling
|
||||
color: Color.transparent
|
||||
|
||||
// If no bar activated in settings, then show them all
|
||||
visible: modelData ? (Settings.data.bar.monitors.includes(modelData.name)
|
||||
|| (Settings.data.bar.monitors.length === 0)) : false
|
||||
|
||||
anchors {
|
||||
top: Settings.data.bar.position === "top"
|
||||
bottom: Settings.data.bar.position === "bottom"
|
||||
left: true
|
||||
right: true
|
||||
Connections {
|
||||
target: ScalingService
|
||||
function onScaleChanged(screenName, scale) {
|
||||
if ((modelData !== null) && (screenName === modelData.name)) {
|
||||
scaling = scale
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
active: Settings.isLoaded && modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false
|
||||
|
||||
// Background fill
|
||||
Rectangle {
|
||||
id: bar
|
||||
sourceComponent: PanelWindow {
|
||||
screen: modelData || null
|
||||
|
||||
WlrLayershell.namespace: "noctalia-bar"
|
||||
|
||||
implicitHeight: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? screen.height : Math.round(Style.barHeight * scaling)
|
||||
implicitWidth: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? Math.round(Style.barHeight * scaling) : screen.width
|
||||
color: Color.transparent
|
||||
|
||||
anchors {
|
||||
top: Settings.data.bar.position === "top" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
|
||||
bottom: Settings.data.bar.position === "bottom" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
|
||||
left: Settings.data.bar.position === "left" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
|
||||
right: Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
|
||||
}
|
||||
|
||||
// Floating bar margins - only apply when floating is enabled
|
||||
margins {
|
||||
top: Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
|
||||
bottom: Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
|
||||
left: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
|
||||
right: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(Color.mSurface.r, Color.mSurface.g, Color.mSurface.b, Settings.data.bar.backgroundOpacity)
|
||||
layer.enabled: true
|
||||
}
|
||||
clip: true
|
||||
|
||||
// ------------------------------
|
||||
// Left Section - Dynamic Widgets
|
||||
Row {
|
||||
id: leftSection
|
||||
// Background fill with shadow
|
||||
Rectangle {
|
||||
id: bar
|
||||
|
||||
height: parent.height
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.marginS * scaling
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
anchors.fill: parent
|
||||
color: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity)
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.left
|
||||
delegate: Loader {
|
||||
active: true
|
||||
sourceComponent: NWidgetLoader {
|
||||
widgetName: modelData
|
||||
widgetProps: {
|
||||
"screen": screen
|
||||
// Floating bar rounded corners
|
||||
radius: Settings.data.bar.floating ? Style.radiusL : 0
|
||||
}
|
||||
|
||||
// For vertical bars, use a single column layout
|
||||
Loader {
|
||||
id: verticalBarLayout
|
||||
anchors.fill: parent
|
||||
visible: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
|
||||
sourceComponent: verticalBarComponent
|
||||
}
|
||||
|
||||
// For horizontal bars, use the original three-section layout
|
||||
Loader {
|
||||
id: horizontalBarLayout
|
||||
anchors.fill: parent
|
||||
visible: Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
|
||||
sourceComponent: horizontalBarComponent
|
||||
}
|
||||
|
||||
// Main layout components
|
||||
Component {
|
||||
id: verticalBarComponent
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
// Top section (left widgets)
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Style.marginM * root.scaling
|
||||
spacing: Style.marginS * root.scaling
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.left
|
||||
delegate: NWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
"screen": root.modelData || null,
|
||||
"scaling": ScalingService.getScreenScale(screen),
|
||||
"widgetId": modelData.id,
|
||||
"section": "left",
|
||||
"sectionWidgetIndex": index,
|
||||
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
|
||||
}
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Center section (center widgets)
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * root.scaling
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.center
|
||||
delegate: NWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
"screen": root.modelData || null,
|
||||
"scaling": ScalingService.getScreenScale(screen),
|
||||
"widgetId": modelData.id,
|
||||
"section": "center",
|
||||
"sectionWidgetIndex": index,
|
||||
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
|
||||
}
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom section (right widgets)
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Style.marginM * root.scaling
|
||||
spacing: Style.marginS * root.scaling
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.right
|
||||
delegate: NWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
"screen": root.modelData || null,
|
||||
"scaling": ScalingService.getScreenScale(screen),
|
||||
"widgetId": modelData.id,
|
||||
"section": "right",
|
||||
"sectionWidgetIndex": index,
|
||||
"sectionWidgetsCount": Settings.data.bar.widgets.right.length
|
||||
}
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Center Section - Dynamic Widgets
|
||||
Row {
|
||||
id: centerSection
|
||||
Component {
|
||||
id: horizontalBarComponent
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
height: parent.height
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
// Left Section
|
||||
RowLayout {
|
||||
id: leftSection
|
||||
objectName: "leftSection"
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.marginS * root.scaling
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * root.scaling
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.center
|
||||
delegate: Loader {
|
||||
active: true
|
||||
sourceComponent: NWidgetLoader {
|
||||
widgetName: modelData
|
||||
widgetProps: {
|
||||
"screen": screen
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.left
|
||||
delegate: NWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
"screen": root.modelData || null,
|
||||
"scaling": ScalingService.getScreenScale(screen),
|
||||
"widgetId": modelData.id,
|
||||
"section": "left",
|
||||
"sectionWidgetIndex": index,
|
||||
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Right Section - Dynamic Widgets
|
||||
Row {
|
||||
id: rightSection
|
||||
// Center Section
|
||||
RowLayout {
|
||||
id: centerSection
|
||||
objectName: "centerSection"
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * root.scaling
|
||||
|
||||
height: parent.height
|
||||
anchors.right: bar.right
|
||||
anchors.rightMargin: Style.marginS * scaling
|
||||
anchors.verticalCenter: bar.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.right
|
||||
delegate: Loader {
|
||||
active: true
|
||||
sourceComponent: NWidgetLoader {
|
||||
widgetName: modelData
|
||||
widgetProps: {
|
||||
"screen": screen
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.center
|
||||
delegate: NWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
"screen": root.modelData || null,
|
||||
"scaling": ScalingService.getScreenScale(screen),
|
||||
"widgetId": modelData.id,
|
||||
"section": "center",
|
||||
"sectionWidgetIndex": index,
|
||||
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Right Section
|
||||
RowLayout {
|
||||
id: rightSection
|
||||
objectName: "rightSection"
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Style.marginS * root.scaling
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * root.scaling
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.right
|
||||
delegate: NWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
"screen": root.modelData || null,
|
||||
"scaling": ScalingService.getScreenScale(screen),
|
||||
"widgetId": modelData.id,
|
||||
"section": "right",
|
||||
"sectionWidgetIndex": index,
|
||||
"sectionWidgetsCount": Settings.data.bar.widgets.right.length
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,24 @@ PopupWindow {
|
||||
property real anchorY
|
||||
property bool isSubMenu: false
|
||||
property bool isHovered: rootMouseArea.containsMouse
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.getScreenScale(screen)
|
||||
|
||||
Connections {
|
||||
target: ScalingService
|
||||
function onScaleChanged(screenName, scale) {
|
||||
if ((screen != null) && (screenName === screen.name)) {
|
||||
scaling = scale
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property int menuWidth: 180
|
||||
|
||||
implicitWidth: menuWidth * scaling
|
||||
|
||||
// Use the content height of the Flickable for implicit height
|
||||
implicitHeight: Math.min(Screen.height * 0.9, flickable.contentHeight + (Style.marginM * 2 * scaling))
|
||||
implicitHeight: Math.min(screen ? screen.height * 0.9 : Screen.height * 0.9, flickable.contentHeight + (Style.marginS * 2 * scaling))
|
||||
visible: false
|
||||
color: Color.transparent
|
||||
anchor.item: anchorItem
|
||||
@@ -147,8 +158,7 @@ PopupWindow {
|
||||
NText {
|
||||
id: text
|
||||
Layout.fillWidth: true
|
||||
color: (modelData?.enabled
|
||||
?? true) ? (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface) : Color.mOnSurfaceVariant
|
||||
color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface) : Color.mOnSurfaceVariant
|
||||
text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..."
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
@@ -164,7 +174,7 @@ PopupWindow {
|
||||
}
|
||||
|
||||
NIcon {
|
||||
text: modelData?.hasChildren ? "menu" : ""
|
||||
icon: modelData?.hasChildren ? "menu" : ""
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: modelData?.hasChildren ?? false
|
||||
@@ -212,7 +222,7 @@ PopupWindow {
|
||||
|
||||
// Check if there's enough space on the right
|
||||
const globalPos = entry.mapToGlobal(0, 0)
|
||||
const openLeft = (globalPos.x + entry.width + submenuWidth > Screen.width)
|
||||
const openLeft = (globalPos.x + entry.width + submenuWidth > (screen ? screen.width : Screen.width))
|
||||
|
||||
// Position with overlap
|
||||
const anchorX = openLeft ? -submenuWidth + overlap : entry.width - overlap
|
||||
@@ -223,7 +233,8 @@ PopupWindow {
|
||||
"anchorItem": entry,
|
||||
"anchorX": anchorX,
|
||||
"anchorY": 0,
|
||||
"isSubMenu": true
|
||||
"isSubMenu": true,
|
||||
"screen": screen
|
||||
})
|
||||
|
||||
if (entry.subMenu) {
|
||||
|
||||
@@ -2,95 +2,160 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Row {
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property bool showingFullTitle: false
|
||||
property int lastWindowIndex: -1
|
||||
property real scaling: 1.0
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
visible: getTitle() !== ""
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
// Timer to hide full title after window switch
|
||||
Timer {
|
||||
id: fullTitleTimer
|
||||
interval: 2000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
showingFullTitle = false
|
||||
}
|
||||
}
|
||||
|
||||
// Update text when window changes
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onActiveWindowChanged() {
|
||||
// Check if window actually changed
|
||||
if (CompositorService.focusedWindowIndex !== lastWindowIndex) {
|
||||
lastWindowIndex = CompositorService.focusedWindowIndex
|
||||
showingFullTitle = true
|
||||
fullTitleTimer.restart()
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon
|
||||
|
||||
// 6% of total width
|
||||
readonly property real minWidth: Math.max(1, screen.width * 0.06)
|
||||
readonly property real maxWidth: minWidth * 2
|
||||
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)
|
||||
implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling)
|
||||
|
||||
function getTitle() {
|
||||
// Use the service's focusedWindowTitle property which is updated immediately
|
||||
// when WindowOpenedOrChanged events are received
|
||||
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
|
||||
try {
|
||||
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
|
||||
} catch (e) {
|
||||
Logger.warn("ActiveWindow", "Error getting title:", e)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
visible: getTitle() !== ""
|
||||
|
||||
function calculatedVerticalHeight() {
|
||||
// Use standard widget height like other widgets
|
||||
return Math.round(Style.capsuleHeight * scaling)
|
||||
}
|
||||
|
||||
function calculatedHorizontalWidth() {
|
||||
let total = Style.marginM * 2 * scaling // internal padding
|
||||
|
||||
if (showIcon) {
|
||||
total += Style.baseWidgetSize * 0.5 * scaling + 2 * scaling // icon + spacing
|
||||
}
|
||||
|
||||
// Calculate actual text width more accurately
|
||||
const title = getTitle()
|
||||
if (title !== "") {
|
||||
// Estimate text width: average character width * number of characters
|
||||
const avgCharWidth = Style.fontSizeS * scaling * 0.6 // rough estimate
|
||||
const titleWidth = Math.min(title.length * avgCharWidth, 80 * scaling)
|
||||
total += titleWidth
|
||||
}
|
||||
|
||||
// Row layout handles spacing between widgets
|
||||
return Math.max(total, Style.capsuleHeight * scaling) // Minimum width
|
||||
}
|
||||
|
||||
function getAppIcon() {
|
||||
const focusedWindow = CompositorService.getFocusedWindow()
|
||||
if (!focusedWindow || !focusedWindow.appId)
|
||||
return ""
|
||||
try {
|
||||
// Try CompositorService first
|
||||
const focusedWindow = CompositorService.getFocusedWindow()
|
||||
if (focusedWindow && focusedWindow.appId) {
|
||||
try {
|
||||
const idValue = focusedWindow.appId
|
||||
const normalizedId = (typeof idValue === 'string') ? idValue : String(idValue)
|
||||
const iconResult = AppIcons.iconForAppId(normalizedId.toLowerCase())
|
||||
if (iconResult && iconResult !== "") {
|
||||
return iconResult
|
||||
}
|
||||
} catch (iconError) {
|
||||
Logger.warn("ActiveWindow", "Error getting icon from CompositorService:", iconError)
|
||||
}
|
||||
}
|
||||
|
||||
return Icons.iconForAppId(focusedWindow.appId)
|
||||
// Fallback to ToplevelManager
|
||||
if (ToplevelManager && ToplevelManager.activeToplevel) {
|
||||
try {
|
||||
const activeToplevel = ToplevelManager.activeToplevel
|
||||
if (activeToplevel.appId) {
|
||||
const idValue2 = activeToplevel.appId
|
||||
const normalizedId2 = (typeof idValue2 === 'string') ? idValue2 : String(idValue2)
|
||||
const iconResult2 = AppIcons.iconForAppId(normalizedId2.toLowerCase())
|
||||
if (iconResult2 && iconResult2 !== "") {
|
||||
return iconResult2
|
||||
}
|
||||
}
|
||||
} catch (fallbackError) {
|
||||
Logger.warn("ActiveWindow", "Error getting icon from ToplevelManager:", fallbackError)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
} catch (e) {
|
||||
Logger.warn("ActiveWindow", "Error in getAppIcon:", e)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// A hidden text element to safely measure the full title width
|
||||
// A hidden text element to safely measure the full title width
|
||||
NText {
|
||||
id: fullTitleMetrics
|
||||
visible: false
|
||||
text: titleText.text
|
||||
font: titleText.font
|
||||
text: getTitle()
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
// Let the Rectangle size itself based on its content (the Row)
|
||||
id: windowTitleRect
|
||||
visible: root.visible
|
||||
width: row.width + Style.marginM * scaling * 2
|
||||
height: Math.round(Style.capsuleHeight * scaling)
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling)
|
||||
height: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Item {
|
||||
id: mainContainer
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.marginS * scaling
|
||||
anchors.rightMargin: Style.marginS * scaling
|
||||
anchors.leftMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
|
||||
anchors.rightMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
|
||||
clip: true
|
||||
|
||||
Row {
|
||||
id: row
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginXS * scaling
|
||||
// Horizontal layout for top/bottom bars
|
||||
RowLayout {
|
||||
id: horizontalLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: 2 * scaling
|
||||
visible: barPosition === "top" || barPosition === "bottom"
|
||||
|
||||
// Window icon
|
||||
Item {
|
||||
width: Style.fontSizeL * scaling * 1.2
|
||||
height: Style.fontSizeL * scaling * 1.2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: getTitle() !== "" && Settings.data.bar.showActiveWindowIcon
|
||||
Layout.preferredWidth: Style.baseWidgetSize * 0.5 * scaling
|
||||
Layout.preferredHeight: Style.baseWidgetSize * 0.5 * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: getTitle() !== "" && showIcon
|
||||
|
||||
IconImage {
|
||||
id: windowIcon
|
||||
@@ -99,26 +164,41 @@ Row {
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
visible: source !== ""
|
||||
|
||||
// Handle loading errors gracefully
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error) {
|
||||
Logger.warn("ActiveWindow", "Failed to load icon:", source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
id: titleText
|
||||
|
||||
// If hovered or just switched window, show up to 400 pixels
|
||||
// If not hovered show up to 150 pixels
|
||||
width: (showingFullTitle || mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth,
|
||||
400 * scaling) : Math.min(
|
||||
fullTitleMetrics.contentWidth, 150 * scaling)
|
||||
Layout.preferredWidth: {
|
||||
try {
|
||||
if (mouseArea.containsMouse) {
|
||||
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
|
||||
} else {
|
||||
return Math.round(Math.min(fullTitleMetrics.contentWidth, 80 * scaling)) // Limited width for horizontal bars
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.warn("ActiveWindow", "Error calculating width:", e)
|
||||
return 80 * scaling
|
||||
}
|
||||
}
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
text: getTitle()
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
elide: mouseArea.containsMouse ? Text.ElideNone : Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mSecondary
|
||||
color: Color.mPrimary
|
||||
clip: true
|
||||
|
||||
Behavior on width {
|
||||
Behavior on Layout.preferredWidth {
|
||||
NumberAnimation {
|
||||
duration: Style.animationSlow
|
||||
easing.type: Easing.InOutCubic
|
||||
@@ -127,12 +207,85 @@ Row {
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical layout for left/right bars - icon only
|
||||
Item {
|
||||
id: verticalLayout
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Style.marginXS * scaling * 2
|
||||
height: parent.height - Style.marginXS * scaling * 2
|
||||
visible: barPosition === "left" || barPosition === "right"
|
||||
|
||||
// Window icon
|
||||
Item {
|
||||
width: Style.baseWidgetSize * 0.5 * scaling
|
||||
height: Style.baseWidgetSize * 0.5 * scaling
|
||||
anchors.centerIn: parent
|
||||
visible: getTitle() !== "" && showIcon
|
||||
|
||||
IconImage {
|
||||
id: windowIconVertical
|
||||
anchors.fill: parent
|
||||
source: getAppIcon()
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
visible: source !== ""
|
||||
|
||||
// Handle loading errors gracefully
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error) {
|
||||
Logger.warn("ActiveWindow", "Failed to load icon:", source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mouse area for hover detection
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onEntered: {
|
||||
if (barPosition === "left" || barPosition === "right") {
|
||||
tooltip.show()
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
if (barPosition === "left" || barPosition === "right") {
|
||||
tooltip.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hover tooltip with full title (only for vertical bars)
|
||||
NTooltip {
|
||||
id: tooltip
|
||||
target: verticalLayout
|
||||
text: getTitle()
|
||||
positionLeft: barPosition === "right"
|
||||
positionRight: barPosition === "left"
|
||||
delay: 500
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onActiveWindowChanged() {
|
||||
try {
|
||||
windowIcon.source = Qt.binding(getAppIcon)
|
||||
windowIconVertical.source = Qt.binding(getAppIcon)
|
||||
} catch (e) {
|
||||
Logger.warn("ActiveWindow", "Error in onActiveWindowChanged:", e)
|
||||
}
|
||||
}
|
||||
function onWindowListChanged() {
|
||||
try {
|
||||
windowIcon.source = Qt.binding(getAppIcon)
|
||||
windowIconVertical.source = Qt.binding(getAppIcon)
|
||||
} catch (e) {
|
||||
Logger.warn("ActiveWindow", "Error in onWindowListChanged:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
|
||||
sizeMultiplier: 0.8
|
||||
colorBg: Color.mSurfaceVariant
|
||||
colorFg: Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
|
||||
// Enhanced icon states with better visual feedback
|
||||
icon: {
|
||||
if (ArchUpdaterService.busy)
|
||||
return "sync"
|
||||
if (ArchUpdaterService.updatePackages.length > 0) {
|
||||
// Show different icons based on update count
|
||||
const count = ArchUpdaterService.updatePackages.length
|
||||
if (count > 50)
|
||||
return "system_update_alt" // Many updates
|
||||
if (count > 10)
|
||||
return "system_update" // Moderate updates
|
||||
return "system_update" // Few updates
|
||||
}
|
||||
return "task_alt"
|
||||
}
|
||||
|
||||
// Enhanced tooltip with more information
|
||||
tooltipText: {
|
||||
if (ArchUpdaterService.busy)
|
||||
return "Checking for updates…"
|
||||
|
||||
var count = ArchUpdaterService.updatePackages.length
|
||||
if (count === 0)
|
||||
return "System is up to date ✓"
|
||||
|
||||
var header = count === 1 ? "One package can be upgraded:" : (count + " packages can be upgraded:")
|
||||
|
||||
var list = ArchUpdaterService.updatePackages || []
|
||||
var s = ""
|
||||
var limit = Math.min(list.length, 8)
|
||||
// Reduced to 8 for better readability
|
||||
for (var i = 0; i < limit; ++i) {
|
||||
var p = list[i]
|
||||
s += (i ? "\n" : "") + (p.name + ": " + p.oldVersion + " → " + p.newVersion)
|
||||
}
|
||||
if (list.length > 8)
|
||||
s += "\n… and " + (list.length - 8) + " more"
|
||||
|
||||
return header + "\n\n" + s + "\n\nClick to update system"
|
||||
}
|
||||
|
||||
// Enhanced click behavior with confirmation
|
||||
onClicked: {
|
||||
if (ArchUpdaterService.busy)
|
||||
return
|
||||
|
||||
if (ArchUpdaterService.updatePackages.length > 0) {
|
||||
// Show confirmation dialog for updates
|
||||
PanelService.getPanel("archUpdaterPanel").toggle(screen)
|
||||
} else {
|
||||
// Just refresh if no updates available
|
||||
ArchUpdaterService.doPoll()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,92 +10,115 @@ Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: 1.0
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
|
||||
readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode
|
||||
readonly property real warningThreshold: widgetSettings.warningThreshold !== undefined ? widgetSettings.warningThreshold : widgetMetadata.warningThreshold
|
||||
|
||||
// Test mode
|
||||
readonly property bool testMode: false
|
||||
readonly property int testPercent: 100
|
||||
readonly property bool testCharging: false
|
||||
|
||||
// Main properties
|
||||
readonly property var battery: UPower.displayDevice
|
||||
readonly property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent)
|
||||
readonly property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0)
|
||||
readonly property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false)
|
||||
property bool hasNotifiedLowBattery: false
|
||||
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
|
||||
// Helper to evaluate and possibly notify
|
||||
function maybeNotify(percent, charging) {
|
||||
// Only notify once we are a below threshold
|
||||
if (!charging && !root.hasNotifiedLowBattery && percent <= warningThreshold) {
|
||||
root.hasNotifiedLowBattery = true
|
||||
ToastService.showWarning("Low Battery", `Battery is at ${Math.round(percent)}%. Please connect the charger.`)
|
||||
} else if (root.hasNotifiedLowBattery && (charging || percent > warningThreshold + 5)) {
|
||||
// Reset when charging starts or when battery recovers 5% above threshold
|
||||
root.hasNotifiedLowBattery = false
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for battery changes
|
||||
Connections {
|
||||
target: UPower.displayDevice
|
||||
function onPercentageChanged() {
|
||||
var currentPercent = UPower.displayDevice.percentage * 100
|
||||
var isCharging = UPower.displayDevice.state === UPowerDeviceState.Charging
|
||||
root.maybeNotify(currentPercent, isCharging)
|
||||
}
|
||||
|
||||
function onStateChanged() {
|
||||
var isCharging = UPower.displayDevice.state === UPowerDeviceState.Charging
|
||||
// Reset notification flag when charging starts
|
||||
if (isCharging) {
|
||||
root.hasNotifiedLowBattery = false
|
||||
}
|
||||
// Also re-evaluate maybeNotify, as state might have changed
|
||||
var currentPercent = UPower.displayDevice.percentage * 100
|
||||
root.maybeNotify(currentPercent, isCharging)
|
||||
}
|
||||
}
|
||||
|
||||
NPill {
|
||||
id: pill
|
||||
|
||||
// Test mode
|
||||
property bool testMode: false
|
||||
property int testPercent: 49
|
||||
property bool testCharging: false
|
||||
|
||||
property var battery: UPower.displayDevice
|
||||
property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent)
|
||||
property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0)
|
||||
property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false)
|
||||
|
||||
// Choose icon based on charge and charging state
|
||||
function batteryIcon() {
|
||||
|
||||
if (!isReady || !battery.isLaptopBattery)
|
||||
return "battery_android_alert"
|
||||
|
||||
if (charging)
|
||||
return "battery_android_bolt"
|
||||
|
||||
if (percent >= 95)
|
||||
return "battery_android_full"
|
||||
|
||||
// Hardcoded battery symbols
|
||||
if (percent >= 85)
|
||||
return "battery_android_6"
|
||||
if (percent >= 70)
|
||||
return "battery_android_5"
|
||||
if (percent >= 55)
|
||||
return "battery_android_4"
|
||||
if (percent >= 40)
|
||||
return "battery_android_3"
|
||||
if (percent >= 25)
|
||||
return "battery_android_2"
|
||||
if (percent >= 10)
|
||||
return "battery_android_1"
|
||||
if (percent >= 0)
|
||||
return "battery_android_0"
|
||||
}
|
||||
|
||||
icon: batteryIcon()
|
||||
text: (isReady && battery.isLaptopBattery) ? Math.round(percent) + "%" : "-"
|
||||
textColor: charging ? Color.mPrimary : Color.mOnSurface
|
||||
forceOpen: isReady && battery.isLaptopBattery && Settings.data.bar.alwaysShowBatteryPercentage
|
||||
disableOpen: (!isReady || !battery.isLaptopBattery)
|
||||
rightOpen: BarWidgetRegistry.getNPillDirection(root)
|
||||
icon: testMode ? BatteryService.getIcon(testPercent, testCharging, true) : BatteryService.getIcon(percent, charging, isReady)
|
||||
text: (isReady || testMode) ? Math.round(percent) : "-"
|
||||
suffix: "%"
|
||||
autoHide: false
|
||||
forceOpen: isReady && (testMode || battery.isLaptopBattery) && displayMode === "alwaysShow"
|
||||
forceClose: displayMode === "alwaysHide"
|
||||
disableOpen: (!isReady || (!testMode && !battery.isLaptopBattery))
|
||||
tooltipText: {
|
||||
let lines = []
|
||||
|
||||
if (testMode) {
|
||||
lines.push("Time Left: " + Time.formatVagueHumanReadableDuration(12345))
|
||||
lines.push(`Time left: ${Time.formatVagueHumanReadableDuration(12345)}.`)
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
if (!isReady || !battery.isLaptopBattery) {
|
||||
return "No Battery Detected"
|
||||
return "No battery detected."
|
||||
}
|
||||
|
||||
if (battery.timeToEmpty > 0) {
|
||||
lines.push("Time Left: " + Time.formatVagueHumanReadableDuration(battery.timeToEmpty))
|
||||
lines.push(`Time left: ${Time.formatVagueHumanReadableDuration(battery.timeToEmpty)}.`)
|
||||
}
|
||||
|
||||
if (battery.timeToFull > 0) {
|
||||
lines.push("Time Until Full: " + Time.formatVagueHumanReadableDuration(battery.timeToFull))
|
||||
lines.push(`Time until full: ${Time.formatVagueHumanReadableDuration(battery.timeToFull)}.`)
|
||||
}
|
||||
|
||||
if (battery.changeRate !== undefined) {
|
||||
const rate = battery.changeRate
|
||||
if (rate > 0) {
|
||||
lines.push(charging ? "Charging Rate: " + rate.toFixed(2) + " W" : "Discharging Rate: " + rate.toFixed(
|
||||
2) + " W")
|
||||
lines.push(charging ? "Charging rate: " + rate.toFixed(2) + " W." : "Discharging rate: " + rate.toFixed(2) + " W.")
|
||||
} else if (rate < 0) {
|
||||
lines.push("Discharging Rate: " + Math.abs(rate).toFixed(2) + " W")
|
||||
lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W.")
|
||||
} else {
|
||||
lines.push("Estimating...")
|
||||
}
|
||||
} else {
|
||||
lines.push(charging ? "Charging" : "Discharging")
|
||||
lines.push(charging ? "Charging." : "Discharging.")
|
||||
}
|
||||
|
||||
if (battery.healthPercentage !== undefined && battery.healthPercentage > 0) {
|
||||
lines.push("Health: " + Math.round(battery.healthPercentage) + "%")
|
||||
}
|
||||
|
||||
@@ -11,25 +11,16 @@ NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: 1.0
|
||||
|
||||
visible: Settings.data.network.bluetoothEnabled
|
||||
sizeMultiplier: 0.8
|
||||
sizeRatio: 0.8
|
||||
colorBg: Color.mSurfaceVariant
|
||||
colorFg: Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
|
||||
icon: {
|
||||
// Show different icons based on connection status
|
||||
if (BluetoothService.pairedDevices.length > 0) {
|
||||
return "bluetooth_connected"
|
||||
} else if (BluetoothService.discovering) {
|
||||
return "bluetooth_searching"
|
||||
} else {
|
||||
return "bluetooth"
|
||||
}
|
||||
}
|
||||
tooltipText: "Bluetooth Devices"
|
||||
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(screen)
|
||||
icon: Settings.data.network.bluetoothEnabled ? "bluetooth" : "bluetooth-off"
|
||||
tooltipText: "Bluetooth devices."
|
||||
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
|
||||
onRightClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,27 @@ Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: 1.0
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
|
||||
readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode
|
||||
|
||||
// Used to avoid opening the pill on Quickshell startup
|
||||
property bool firstBrightnessReceived: false
|
||||
@@ -25,8 +45,7 @@ Item {
|
||||
function getIcon() {
|
||||
var monitor = getMonitor()
|
||||
var brightness = monitor ? monitor.brightness : 0
|
||||
return brightness <= 0 ? "brightness_1" : brightness < 0.33 ? "brightness_low" : brightness
|
||||
< 0.66 ? "brightness_medium" : "brightness_high"
|
||||
return brightness <= 0.5 ? "brightness-low" : "brightness-high"
|
||||
}
|
||||
|
||||
// Connection used to open the pill when brightness changes
|
||||
@@ -34,44 +53,44 @@ Item {
|
||||
target: getMonitor()
|
||||
ignoreUnknownSignals: true
|
||||
function onBrightnessUpdated() {
|
||||
Logger.log("Bar-Brightness", "OnBrightnessUpdated")
|
||||
var monitor = getMonitor()
|
||||
if (!monitor)
|
||||
return
|
||||
var currentBrightness = monitor.brightness
|
||||
|
||||
// Ignore if this is the first time or if brightness hasn't actually changed
|
||||
// Ignore if this is the first time we receive an update.
|
||||
// Most likely service just kicked off.
|
||||
if (!firstBrightnessReceived) {
|
||||
firstBrightnessReceived = true
|
||||
monitor.lastBrightness = currentBrightness
|
||||
return
|
||||
}
|
||||
|
||||
// Only show pill if brightness actually changed (not just loaded from settings)
|
||||
if (Math.abs(currentBrightness - monitor.lastBrightness) > 0.1) {
|
||||
pill.show()
|
||||
}
|
||||
|
||||
monitor.lastBrightness = currentBrightness
|
||||
pill.show()
|
||||
hideTimerAfterChange.restart()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hideTimerAfterChange
|
||||
interval: 2500
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: pill.hide()
|
||||
}
|
||||
|
||||
NPill {
|
||||
id: pill
|
||||
|
||||
rightOpen: BarWidgetRegistry.getNPillDirection(root)
|
||||
icon: getIcon()
|
||||
iconCircleColor: Color.mPrimary
|
||||
collapsedIconColor: Color.mOnSurface
|
||||
autoHide: false // Important to be false so we can hover as long as we want
|
||||
text: {
|
||||
var monitor = getMonitor()
|
||||
return monitor ? (Math.round(monitor.brightness * 100) + "%") : ""
|
||||
return monitor ? Math.round(monitor.brightness * 100) : ""
|
||||
}
|
||||
suffix: text.length > 0 ? "%" : "-"
|
||||
forceOpen: displayMode === "alwaysShow"
|
||||
forceClose: displayMode === "alwaysHide"
|
||||
tooltipText: {
|
||||
var monitor = getMonitor()
|
||||
if (!monitor)
|
||||
return ""
|
||||
return "Brightness: " + Math.round(monitor.brightness * 100) + "%\nMethod: " + monitor.method
|
||||
+ "\nLeft click for advanced settings.\nScroll up/down to change brightness."
|
||||
return "Brightness: " + Math.round(monitor.brightness * 100) + "%\nMethod: " + monitor.method + "\nLeft click for advanced settings.\nScroll up/down to change brightness."
|
||||
}
|
||||
|
||||
onWheel: function (angle) {
|
||||
@@ -85,9 +104,10 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.Brightness
|
||||
settingsPanel.open(screen)
|
||||
onRightClicked: {
|
||||
var settingsPanel = PanelService.getPanel("settingsPanel")
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.Display
|
||||
settingsPanel.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
@@ -8,26 +9,205 @@ Rectangle {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: 1.0
|
||||
|
||||
implicitWidth: clock.width + Style.marginM * 2 * scaling
|
||||
implicitHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
|
||||
// Resolve settings: try user settings or defaults from BarWidgetRegistry
|
||||
readonly property bool use12h: widgetSettings.use12HourClock !== undefined ? widgetSettings.use12HourClock : widgetMetadata.use12HourClock
|
||||
readonly property bool reverseDayMonth: widgetSettings.reverseDayMonth !== undefined ? widgetSettings.reverseDayMonth : widgetMetadata.reverseDayMonth
|
||||
readonly property string displayFormat: widgetSettings.displayFormat !== undefined ? widgetSettings.displayFormat : widgetMetadata.displayFormat
|
||||
|
||||
// Use compact mode for vertical bars
|
||||
readonly property bool useCompactMode: barPosition === "left" || barPosition === "right"
|
||||
|
||||
implicitWidth: useCompactMode ? Math.round(Style.capsuleHeight * scaling) : Math.round(layout.implicitWidth + Style.marginM * 2 * scaling)
|
||||
implicitHeight: useCompactMode ? Math.round(Style.capsuleHeight * 2.5 * scaling) : Math.round(Style.capsuleHeight * scaling)
|
||||
|
||||
radius: Math.round(Style.radiusS * scaling)
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
// Clock Icon with attached calendar
|
||||
NClock {
|
||||
id: clock
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
Item {
|
||||
id: clockContainer
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginXS * scaling
|
||||
|
||||
NTooltip {
|
||||
id: tooltip
|
||||
text: Time.dateString
|
||||
target: clock
|
||||
positionAbove: Settings.data.bar.position === "bottom"
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
anchors.centerIn: parent
|
||||
spacing: useCompactMode ? -2 * scaling : -3 * scaling
|
||||
|
||||
// Compact mode for vertical bars - Time section (HH, MM)
|
||||
Repeater {
|
||||
model: useCompactMode ? 2 : 1
|
||||
NText {
|
||||
readonly property bool showSeconds: (displayFormat === "time-seconds")
|
||||
readonly property bool inlineDate: (displayFormat === "time-date")
|
||||
readonly property var now: Time.date
|
||||
|
||||
text: {
|
||||
if (useCompactMode) {
|
||||
// Compact mode: time section (first 2 lines)
|
||||
switch (index) {
|
||||
case 0:
|
||||
// Hours
|
||||
if (use12h) {
|
||||
const hours = now.getHours()
|
||||
const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours)
|
||||
return displayHours.toString().padStart(2, '0')
|
||||
} else {
|
||||
return now.getHours().toString().padStart(2, '0')
|
||||
}
|
||||
case 1:
|
||||
// Minutes
|
||||
return now.getMinutes().toString().padStart(2, '0')
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
} else {
|
||||
// Normal mode: single line with time
|
||||
let timeStr = ""
|
||||
|
||||
if (use12h) {
|
||||
// 12-hour format with proper padding and consistent spacing
|
||||
const hours = now.getHours()
|
||||
const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours)
|
||||
const paddedHours = displayHours.toString().padStart(2, '0')
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0')
|
||||
const ampm = hours < 12 ? 'AM' : 'PM'
|
||||
|
||||
if (showSeconds) {
|
||||
const seconds = now.getSeconds().toString().padStart(2, '0')
|
||||
timeStr = `${paddedHours}:${minutes}:${seconds} ${ampm}`
|
||||
} else {
|
||||
timeStr = `${paddedHours}:${minutes} ${ampm}`
|
||||
}
|
||||
} else {
|
||||
// 24-hour format with padding
|
||||
const hours = now.getHours().toString().padStart(2, '0')
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0')
|
||||
|
||||
if (showSeconds) {
|
||||
const seconds = now.getSeconds().toString().padStart(2, '0')
|
||||
timeStr = `${hours}:${minutes}:${seconds}`
|
||||
} else {
|
||||
timeStr = `${hours}:${minutes}`
|
||||
}
|
||||
}
|
||||
|
||||
// Add inline date if needed
|
||||
if (inlineDate) {
|
||||
let dayName = now.toLocaleDateString(Qt.locale(), "ddd")
|
||||
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
|
||||
const day = now.getDate().toString().padStart(2, '0')
|
||||
let month = now.toLocaleDateString(Qt.locale(), "MMM")
|
||||
timeStr += " - " + (reverseDayMonth ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`)
|
||||
}
|
||||
|
||||
return timeStr
|
||||
}
|
||||
}
|
||||
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: useCompactMode ? Style.fontSizeXXS * scaling : Style.fontSizeXS * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
// Separator line for compact mode (between time and date)
|
||||
Rectangle {
|
||||
visible: useCompactMode
|
||||
Layout.preferredWidth: 20 * scaling
|
||||
Layout.preferredHeight: 2 * scaling
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: 3 * scaling
|
||||
Layout.bottomMargin: 3 * scaling
|
||||
color: Color.mPrimary
|
||||
opacity: 0.3
|
||||
radius: 1 * scaling
|
||||
}
|
||||
|
||||
// Compact mode for vertical bars - Date section (DD, MM)
|
||||
Repeater {
|
||||
model: useCompactMode ? 2 : 0
|
||||
NText {
|
||||
readonly property var now: Time.date
|
||||
|
||||
text: {
|
||||
if (useCompactMode) {
|
||||
// Compact mode: date section (last 2 lines)
|
||||
switch (index) {
|
||||
case 0:
|
||||
// Day
|
||||
return now.getDate().toString().padStart(2, '0')
|
||||
case 1:
|
||||
// Month
|
||||
return (now.getMonth() + 1).toString().padStart(2, '0')
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
// Second line for normal mode (date)
|
||||
NText {
|
||||
visible: !useCompactMode && (displayFormat === "time-date-short")
|
||||
text: {
|
||||
const now = Time.date
|
||||
const day = now.getDate().toString().padStart(2, '0')
|
||||
const month = (now.getMonth() + 1).toString().padStart(2, '0')
|
||||
return reverseDayMonth ? `${month}/${day}` : `${day}/${month}`
|
||||
}
|
||||
|
||||
// Enable fixed-width font for consistent spacing
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
font.weight: Style.fontWeightRegular
|
||||
color: Color.mPrimary
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NTooltip {
|
||||
id: tooltip
|
||||
text: `${Time.formatDate(reverseDayMonth)}.`
|
||||
target: clockContainer
|
||||
positionAbove: Settings.data.bar.position === "bottom"
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: clockMouseArea
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
onEntered: {
|
||||
if (!PanelService.getPanel("calendarPanel")?.active) {
|
||||
tooltip.show()
|
||||
@@ -38,7 +218,7 @@ Rectangle {
|
||||
}
|
||||
onClicked: {
|
||||
tooltip.hide()
|
||||
PanelService.getPanel("calendarPanel")?.toggle(screen)
|
||||
PanelService.getPanel("calendarPanel")?.toggle(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
89
Modules/Bar/Widgets/CustomButton.qml
Normal file
@@ -0,0 +1,89 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.SettingsPanel
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
// Widget properties passed from Bar.qml
|
||||
property var screen
|
||||
property real scaling: 1.0
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
// Use settings or defaults from BarWidgetRegistry
|
||||
readonly property string customIcon: widgetSettings.icon || widgetMetadata.icon
|
||||
readonly property string leftClickExec: widgetSettings.leftClickExec || widgetMetadata.leftClickExec
|
||||
readonly property string rightClickExec: widgetSettings.rightClickExec || widgetMetadata.rightClickExec
|
||||
readonly property string middleClickExec: widgetSettings.middleClickExec || widgetMetadata.middleClickExec
|
||||
readonly property bool hasExec: (leftClickExec || rightClickExec || middleClickExec)
|
||||
|
||||
enabled: hasExec
|
||||
allowClickWhenDisabled: true // we want to be able to open config with left click when its not setup properly
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
sizeRatio: 0.8
|
||||
icon: customIcon
|
||||
tooltipText: {
|
||||
if (!hasExec) {
|
||||
return "Custom Button - Configure in settings"
|
||||
} else {
|
||||
var lines = []
|
||||
if (leftClickExec !== "") {
|
||||
lines.push(`Left click: <i>${leftClickExec}</i>.`)
|
||||
}
|
||||
if (rightClickExec !== "") {
|
||||
lines.push(`Right click: <i>${rightClickExec}</i>.`)
|
||||
}
|
||||
if (middleClickExec !== "") {
|
||||
lines.push(`Middle click: <i>${middleClickExec}</i>.`)
|
||||
}
|
||||
return lines.join("<br/>")
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (leftClickExec) {
|
||||
Quickshell.execDetached(["sh", "-c", leftClickExec])
|
||||
Logger.log("CustomButton", `Executing command: ${leftClickExec}`)
|
||||
} else if (!hasExec) {
|
||||
// No script was defined, open settings
|
||||
var settingsPanel = PanelService.getPanel("settingsPanel")
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.Bar
|
||||
settingsPanel.open()
|
||||
}
|
||||
}
|
||||
|
||||
onRightClicked: {
|
||||
if (rightClickExec) {
|
||||
Quickshell.execDetached(["sh", "-c", rightClickExec])
|
||||
Logger.log("CustomButton", `Executing command: ${rightClickExec}`)
|
||||
}
|
||||
}
|
||||
|
||||
onMiddleClicked: {
|
||||
if (middleClickExec) {
|
||||
Quickshell.execDetached(["sh", "-c", middleClickExec])
|
||||
Logger.log("CustomButton", `Executing command: ${middleClickExec}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Modules/Bar/Widgets/DarkModeToggle.qml
Normal file
@@ -0,0 +1,23 @@
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
icon: "dark-mode"
|
||||
tooltipText: "Toggle light/dark mode"
|
||||
sizeRatio: 0.8
|
||||
|
||||
colorBg: Settings.data.colorSchemes.darkMode ? Color.mSurfaceVariant : Color.mPrimary
|
||||
colorFg: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mOnPrimary
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: Settings.data.colorSchemes.darkMode = !Settings.data.colorSchemes.darkMode
|
||||
}
|
||||
24
Modules/Bar/Widgets/KeepAwake.qml
Normal file
@@ -0,0 +1,24 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
sizeRatio: 0.8
|
||||
|
||||
icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off"
|
||||
tooltipText: IdleInhibitorService.isInhibited ? "Disable keep awake" : "Enable keep awake"
|
||||
colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mSurfaceVariant
|
||||
colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
onClicked: {
|
||||
IdleInhibitorService.manualToggle()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Io
|
||||
@@ -6,27 +7,48 @@ import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Row {
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: 1.0
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode
|
||||
|
||||
// Use the shared service for keyboard layout
|
||||
property string currentLayout: KeyboardLayoutService.currentLayout
|
||||
|
||||
width: pill.width
|
||||
height: pill.height
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
|
||||
NPill {
|
||||
id: pill
|
||||
icon: "keyboard_alt"
|
||||
iconCircleColor: Color.mPrimary
|
||||
collapsedIconColor: Color.mOnSurface
|
||||
autoHide: false // Important to be false so we can hover as long as we want
|
||||
text: currentLayout
|
||||
tooltipText: "Keyboard Layout: " + currentLayout
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
rightOpen: BarWidgetRegistry.getNPillDirection(root)
|
||||
icon: "keyboard"
|
||||
autoHide: false // Important to be false so we can hover as long as we want
|
||||
text: currentLayout.toUpperCase()
|
||||
tooltipText: "Keyboard layout: " + currentLayout.toUpperCase()
|
||||
forceOpen: root.displayMode === "forceOpen"
|
||||
forceClose: root.displayMode === "alwaysHide"
|
||||
onClicked: {
|
||||
|
||||
// You could open keyboard settings here if needed
|
||||
|
||||
@@ -7,21 +7,64 @@ import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Row {
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: 1.0
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
visible: MediaService.currentPlayer !== null && MediaService.canPlay
|
||||
width: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
|
||||
readonly property bool showAlbumArt: (widgetSettings.showAlbumArt !== undefined) ? widgetSettings.showAlbumArt : widgetMetadata.showAlbumArt
|
||||
readonly property bool showVisualizer: (widgetSettings.showVisualizer !== undefined) ? widgetSettings.showVisualizer : widgetMetadata.showVisualizer
|
||||
readonly property string visualizerType: (widgetSettings.visualizerType !== undefined && widgetSettings.visualizerType !== "") ? widgetSettings.visualizerType : widgetMetadata.visualizerType
|
||||
|
||||
// 6% of total width
|
||||
readonly property real minWidth: Math.max(1, screen.width * 0.06)
|
||||
readonly property real maxWidth: minWidth * 2
|
||||
|
||||
function getTitle() {
|
||||
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
|
||||
}
|
||||
|
||||
function calculatedVerticalHeight() {
|
||||
return Math.round(Style.baseWidgetSize * 0.8 * scaling)
|
||||
}
|
||||
|
||||
function calculatedHorizontalWidth() {
|
||||
let total = Style.marginM * 2 * scaling // internal padding
|
||||
if (showAlbumArt) {
|
||||
total += 18 * scaling + 2 * scaling // album art + spacing
|
||||
} else {
|
||||
total += Style.fontSizeL * scaling + 2 * scaling // icon + spacing
|
||||
}
|
||||
total += Math.min(fullTitleMetrics.contentWidth, maxWidth * scaling) // title text
|
||||
// Row layout handles spacing between widgets
|
||||
return total
|
||||
}
|
||||
|
||||
implicitHeight: visible ? ((barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)) : 0
|
||||
implicitWidth: visible ? ((barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (rowLayout.implicitWidth + Style.marginM * 2 * scaling)) : 0
|
||||
|
||||
visible: MediaService.currentPlayer !== null && MediaService.canPlay
|
||||
|
||||
// A hidden text element to safely measure the full title width
|
||||
NText {
|
||||
id: fullTitleMetrics
|
||||
@@ -31,26 +74,32 @@ Row {
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
// Let the Rectangle size itself based on its content (the Row)
|
||||
width: row.width + Style.marginM * scaling * 2
|
||||
|
||||
height: Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
id: mediaMini
|
||||
visible: root.visible
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (rowLayout.implicitWidth + Style.marginM * 2 * scaling)
|
||||
height: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : Math.round(Style.capsuleHeight * scaling)
|
||||
radius: (barPosition === "left" || barPosition === "right") ? width / 2 : Math.round(Style.radiusM * scaling)
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
// Used to anchor the tooltip, so the tooltip does not move when the content expands
|
||||
Item {
|
||||
id: anchor
|
||||
height: parent.height
|
||||
width: 200 * scaling
|
||||
}
|
||||
|
||||
Item {
|
||||
id: mainContainer
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.marginS * scaling
|
||||
anchors.rightMargin: Style.marginS * scaling
|
||||
anchors.leftMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
|
||||
anchors.rightMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
|
||||
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "linear"
|
||||
&& MediaService.isPlaying
|
||||
active: showVisualizer && visualizerType == "linear" && MediaService.isPlaying
|
||||
z: 0
|
||||
|
||||
sourceComponent: LinearSpectrum {
|
||||
@@ -60,108 +109,96 @@ Row {
|
||||
fillColor: Color.mOnSurfaceVariant
|
||||
opacity: 0.4
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored"
|
||||
&& MediaService.isPlaying
|
||||
z: 0
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: showVisualizer && visualizerType == "mirrored" && MediaService.isPlaying
|
||||
z: 0
|
||||
|
||||
sourceComponent: MirroredSpectrum {
|
||||
width: mainContainer.width - Style.marginS * scaling
|
||||
height: mainContainer.height - Style.marginS * scaling
|
||||
values: CavaService.values
|
||||
fillColor: Color.mOnSurfaceVariant
|
||||
opacity: 0.4
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "wave"
|
||||
&& MediaService.isPlaying
|
||||
z: 0
|
||||
|
||||
sourceComponent: WaveSpectrum {
|
||||
width: mainContainer.width - Style.marginS * scaling
|
||||
height: mainContainer.height - Style.marginS * scaling
|
||||
values: CavaService.values
|
||||
fillColor: Color.mOnSurfaceVariant
|
||||
opacity: 0.4
|
||||
}
|
||||
sourceComponent: MirroredSpectrum {
|
||||
width: mainContainer.width - Style.marginS * scaling
|
||||
height: mainContainer.height - Style.marginS * scaling
|
||||
values: CavaService.values
|
||||
fillColor: Color.mOnSurfaceVariant
|
||||
opacity: 0.4
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: row
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginXS * scaling
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: showVisualizer && visualizerType == "wave" && MediaService.isPlaying
|
||||
z: 0
|
||||
|
||||
sourceComponent: WaveSpectrum {
|
||||
width: mainContainer.width - Style.marginS * scaling
|
||||
height: mainContainer.height - Style.marginS * scaling
|
||||
values: CavaService.values
|
||||
fillColor: Color.mOnSurfaceVariant
|
||||
opacity: 0.4
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal layout for top/bottom bars
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
visible: barPosition === "top" || barPosition === "bottom"
|
||||
z: 1 // Above the visualizer
|
||||
|
||||
NIcon {
|
||||
id: windowIcon
|
||||
text: MediaService.isPlaying ? "pause" : "play_arrow"
|
||||
icon: MediaService.isPlaying ? "media-pause" : "media-play"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: !Settings.data.audio.showMiniplayerAlbumArt && getTitle() !== "" && !trackArt.visible
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: !showAlbumArt && getTitle() !== "" && !trackArt.visible
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: Settings.data.audio.showMiniplayerAlbumArt
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: showAlbumArt
|
||||
spacing: 0
|
||||
|
||||
Rectangle {
|
||||
width: 18 * scaling
|
||||
height: 18 * scaling
|
||||
radius: width * 0.5
|
||||
color: Color.transparent
|
||||
antialiasing: true
|
||||
clip: true
|
||||
Item {
|
||||
Layout.preferredWidth: Math.round(18 * scaling)
|
||||
Layout.preferredHeight: Math.round(18 * scaling)
|
||||
|
||||
NImageCircled {
|
||||
id: trackArt
|
||||
visible: MediaService.trackArtUrl.toString() !== ""
|
||||
anchors.fill: parent
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: scaling
|
||||
imagePath: MediaService.trackArtUrl
|
||||
fallbackIcon: MediaService.isPlaying ? "pause" : "play_arrow"
|
||||
fallbackIcon: MediaService.isPlaying ? "media-pause" : "media-play"
|
||||
fallbackIconSize: 10 * scaling
|
||||
borderWidth: 0
|
||||
border.color: Color.transparent
|
||||
}
|
||||
|
||||
// Fallback icon when no album art available
|
||||
NIcon {
|
||||
id: windowIconFallback
|
||||
text: MediaService.isPlaying ? "pause" : "play_arrow"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: getTitle() !== "" && !trackArt.visible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
id: titleText
|
||||
|
||||
// If hovered or just switched window, show up to 400 pixels
|
||||
// If not hovered show up to 150 pixels
|
||||
width: (mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth,
|
||||
400 * scaling) : Math.min(fullTitleMetrics.contentWidth,
|
||||
150 * scaling)
|
||||
Layout.preferredWidth: {
|
||||
if (mouseArea.containsMouse) {
|
||||
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
|
||||
} else {
|
||||
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling))
|
||||
}
|
||||
}
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
text: getTitle()
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mTertiary
|
||||
color: Color.mSecondary
|
||||
|
||||
Behavior on width {
|
||||
Behavior on Layout.preferredWidth {
|
||||
NumberAnimation {
|
||||
duration: Style.animationSlow
|
||||
easing.type: Easing.InOutCubic
|
||||
@@ -170,14 +207,92 @@ Row {
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical layout for left/right bars - icon only
|
||||
Item {
|
||||
id: verticalLayout
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Style.marginM * scaling * 2
|
||||
height: parent.height - Style.marginM * scaling * 2
|
||||
visible: barPosition === "left" || barPosition === "right"
|
||||
z: 1 // Above the visualizer
|
||||
|
||||
// Media icon
|
||||
Item {
|
||||
width: Style.baseWidgetSize * 0.5 * scaling
|
||||
height: Style.baseWidgetSize * 0.5 * scaling
|
||||
anchors.centerIn: parent
|
||||
visible: getTitle() !== ""
|
||||
|
||||
NIcon {
|
||||
id: mediaIconVertical
|
||||
anchors.fill: parent
|
||||
icon: MediaService.isPlaying ? "media-pause" : "media-play"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mouse area for hover detection
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: MediaService.playPause()
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
MediaService.playPause()
|
||||
} else if (mouse.button == Qt.RightButton) {
|
||||
MediaService.next()
|
||||
// Need to hide the tooltip instantly
|
||||
tooltip.visible = false
|
||||
} else if (mouse.button == Qt.MiddleButton) {
|
||||
MediaService.previous()
|
||||
// Need to hide the tooltip instantly
|
||||
tooltip.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
onEntered: {
|
||||
if (barPosition === "left" || barPosition === "right") {
|
||||
tooltip.show()
|
||||
} else if (tooltip.text !== "") {
|
||||
tooltip.show()
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
if (barPosition === "left" || barPosition === "right") {
|
||||
tooltip.hide()
|
||||
} else {
|
||||
tooltip.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NTooltip {
|
||||
id: tooltip
|
||||
text: {
|
||||
if (barPosition === "left" || barPosition === "right") {
|
||||
return getTitle()
|
||||
} else {
|
||||
var str = ""
|
||||
if (MediaService.canGoNext) {
|
||||
str += "Right click for next.\n"
|
||||
}
|
||||
if (MediaService.canGoPrevious) {
|
||||
str += "Middle click for previous."
|
||||
}
|
||||
return str
|
||||
}
|
||||
}
|
||||
target: (barPosition === "left" || barPosition === "right") ? verticalLayout : anchor
|
||||
positionLeft: barPosition === "right"
|
||||
positionRight: barPosition === "left"
|
||||
positionAbove: Settings.data.bar.position === "bottom"
|
||||
delay: 500
|
||||
}
|
||||
}
|
||||
|
||||
122
Modules/Bar/Widgets/Microphone.qml
Normal file
@@ -0,0 +1,122 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Commons
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
|
||||
readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode
|
||||
|
||||
// Used to avoid opening the pill on Quickshell startup
|
||||
property bool firstInputVolumeReceived: false
|
||||
property int wheelAccumulator: 0
|
||||
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
|
||||
function getIcon() {
|
||||
if (AudioService.inputMuted) {
|
||||
return "microphone-mute"
|
||||
}
|
||||
return (AudioService.inputVolume <= Number.EPSILON) ? "microphone-mute" : "microphone"
|
||||
}
|
||||
|
||||
// Connection used to open the pill when input volume changes
|
||||
Connections {
|
||||
target: AudioService.source?.audio ? AudioService.source?.audio : null
|
||||
function onVolumeChanged() {
|
||||
// Logger.log("Bar:Microphone", "onInputVolumeChanged")
|
||||
if (!firstInputVolumeReceived) {
|
||||
// Ignore the first volume change
|
||||
firstInputVolumeReceived = true
|
||||
} else {
|
||||
pill.show()
|
||||
externalHideTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connection used to open the pill when input mute state changes
|
||||
Connections {
|
||||
target: AudioService.source?.audio ? AudioService.source?.audio : null
|
||||
function onMutedChanged() {
|
||||
// Logger.log("Bar:Microphone", "onInputMutedChanged")
|
||||
if (!firstInputVolumeReceived) {
|
||||
// Ignore the first mute change
|
||||
firstInputVolumeReceived = true
|
||||
} else {
|
||||
pill.show()
|
||||
externalHideTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: externalHideTimer
|
||||
running: false
|
||||
interval: 1500
|
||||
onTriggered: {
|
||||
pill.hide()
|
||||
}
|
||||
}
|
||||
|
||||
NPill {
|
||||
id: pill
|
||||
rightOpen: BarWidgetRegistry.getNPillDirection(root)
|
||||
icon: getIcon()
|
||||
autoHide: false // Important to be false so we can hover as long as we want
|
||||
text: Math.floor(AudioService.inputVolume * 100)
|
||||
suffix: "%"
|
||||
forceOpen: displayMode === "alwaysShow"
|
||||
forceClose: displayMode === "alwaysHide"
|
||||
tooltipText: "Microphone: " + Math.round(AudioService.inputVolume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute."
|
||||
|
||||
onWheel: function (delta) {
|
||||
wheelAccumulator += delta
|
||||
if (wheelAccumulator >= 120) {
|
||||
wheelAccumulator = 0
|
||||
AudioService.setInputVolume(AudioService.inputVolume + AudioService.stepVolume)
|
||||
} else if (wheelAccumulator <= -120) {
|
||||
wheelAccumulator = 0
|
||||
AudioService.setInputVolume(AudioService.inputVolume - AudioService.stepVolume)
|
||||
}
|
||||
}
|
||||
onClicked: {
|
||||
AudioService.setInputMuted(!AudioService.inputMuted)
|
||||
}
|
||||
onRightClicked: {
|
||||
var settingsPanel = PanelService.getPanel("settingsPanel")
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.Audio
|
||||
settingsPanel.open()
|
||||
}
|
||||
onMiddleClicked: {
|
||||
Quickshell.execDetached(["pwvucontrol"])
|
||||
}
|
||||
}
|
||||
}
|
||||
42
Modules/Bar/Widgets/NightLight.qml
Normal file
@@ -0,0 +1,42 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
sizeRatio: 0.8
|
||||
colorBg: Settings.data.nightLight.forced ? Color.mPrimary : Color.mSurfaceVariant
|
||||
colorFg: Settings.data.nightLight.forced ? Color.mOnPrimary : Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
|
||||
icon: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "nightlight-forced" : "nightlight-on") : "nightlight-off"
|
||||
tooltipText: `Night light: ${Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "forced." : "enabled.") : "disabled."}\nLeft click to cycle (disabled → normal → forced).\nRight click to access settings.`
|
||||
onClicked: {
|
||||
if (!Settings.data.nightLight.enabled) {
|
||||
Settings.data.nightLight.enabled = true
|
||||
Settings.data.nightLight.forced = false
|
||||
} else if (Settings.data.nightLight.enabled && !Settings.data.nightLight.forced) {
|
||||
Settings.data.nightLight.forced = true
|
||||
} else {
|
||||
Settings.data.nightLight.enabled = false
|
||||
Settings.data.nightLight.forced = false
|
||||
}
|
||||
}
|
||||
|
||||
onRightClicked: {
|
||||
var settingsPanel = PanelService.getPanel("settingsPanel")
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.Display
|
||||
settingsPanel.open()
|
||||
}
|
||||
}
|
||||
@@ -11,14 +11,86 @@ NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: 1.0
|
||||
|
||||
sizeMultiplier: 0.8
|
||||
icon: "notifications"
|
||||
tooltipText: "Notification History"
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
readonly property bool showUnreadBadge: (widgetSettings.showUnreadBadge !== undefined) ? widgetSettings.showUnreadBadge : widgetMetadata.showUnreadBadge
|
||||
readonly property bool hideWhenZero: (widgetSettings.hideWhenZero !== undefined) ? widgetSettings.hideWhenZero : widgetMetadata.hideWhenZero
|
||||
|
||||
function lastSeenTs() {
|
||||
return Settings.data.notifications?.lastSeenTs || 0
|
||||
}
|
||||
|
||||
function computeUnreadCount() {
|
||||
var since = lastSeenTs()
|
||||
var count = 0
|
||||
var model = NotificationService.historyModel
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
var item = model.get(i)
|
||||
var ts = item.timestamp instanceof Date ? item.timestamp.getTime() : item.timestamp
|
||||
if (ts > since)
|
||||
count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
sizeRatio: 0.8
|
||||
icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell"
|
||||
tooltipText: Settings.data.notifications.doNotDisturb ? "Notification history.\nRight-click to disable 'Do Not Disturb'." : "Notification history.\nRight-click to enable 'Do Not Disturb'."
|
||||
colorBg: Color.mSurfaceVariant
|
||||
colorFg: Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(screen)
|
||||
|
||||
onClicked: {
|
||||
var panel = PanelService.getPanel("notificationHistoryPanel")
|
||||
panel?.toggle(this)
|
||||
Settings.data.notifications.lastSeenTs = Time.timestamp * 1000
|
||||
}
|
||||
|
||||
onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
|
||||
|
||||
Loader {
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.rightMargin: -4 * scaling
|
||||
anchors.topMargin: -4 * scaling
|
||||
z: 2
|
||||
active: showUnreadBadge && (!hideWhenZero || computeUnreadCount() > 0)
|
||||
sourceComponent: Rectangle {
|
||||
id: badge
|
||||
readonly property int count: computeUnreadCount()
|
||||
readonly property string label: count <= 99 ? String(count) : "99+"
|
||||
readonly property real pad: 8 * scaling
|
||||
height: 16 * scaling
|
||||
width: Math.max(height, textNode.implicitWidth + pad)
|
||||
radius: height / 2
|
||||
color: Color.mError
|
||||
border.color: Color.mSurface
|
||||
border.width: 1
|
||||
visible: count > 0 || !hideWhenZero
|
||||
NText {
|
||||
id: textNode
|
||||
anchors.centerIn: parent
|
||||
text: badge.label
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnError
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,50 +10,44 @@ NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property var powerProfiles: PowerProfiles
|
||||
readonly property bool hasPP: powerProfiles.hasPerformanceProfile
|
||||
property real scaling: 1.0
|
||||
readonly property bool hasPP: PowerProfileService.available
|
||||
|
||||
sizeMultiplier: 0.8
|
||||
sizeRatio: 0.8
|
||||
visible: hasPP
|
||||
|
||||
function profileIcon() {
|
||||
if (!hasPP)
|
||||
return "balance"
|
||||
if (powerProfiles.profile === PowerProfile.Performance)
|
||||
return "speed"
|
||||
if (powerProfiles.profile === PowerProfile.Balanced)
|
||||
return "balance"
|
||||
if (powerProfiles.profile === PowerProfile.PowerSaver)
|
||||
return "eco"
|
||||
return "balanced"
|
||||
if (PowerProfileService.profile === PowerProfile.Performance)
|
||||
return "performance"
|
||||
if (PowerProfileService.profile === PowerProfile.Balanced)
|
||||
return "balanced"
|
||||
if (PowerProfileService.profile === PowerProfile.PowerSaver)
|
||||
return "powersaver"
|
||||
}
|
||||
|
||||
function profileName() {
|
||||
if (!hasPP)
|
||||
return "Unknown"
|
||||
if (powerProfiles.profile === PowerProfile.Performance)
|
||||
if (PowerProfileService.profile === PowerProfile.Performance)
|
||||
return "Performance"
|
||||
if (powerProfiles.profile === PowerProfile.Balanced)
|
||||
if (PowerProfileService.profile === PowerProfile.Balanced)
|
||||
return "Balanced"
|
||||
if (powerProfiles.profile === PowerProfile.PowerSaver)
|
||||
if (PowerProfileService.profile === PowerProfile.PowerSaver)
|
||||
return "Power Saver"
|
||||
}
|
||||
|
||||
function changeProfile() {
|
||||
if (!hasPP)
|
||||
return
|
||||
if (powerProfiles.profile === PowerProfile.Performance)
|
||||
powerProfiles.profile = PowerProfile.PowerSaver
|
||||
else if (powerProfiles.profile === PowerProfile.Balanced)
|
||||
powerProfiles.profile = PowerProfile.Performance
|
||||
else if (powerProfiles.profile === PowerProfile.PowerSaver)
|
||||
powerProfiles.profile = PowerProfile.Balanced
|
||||
PowerProfileService.cycleProfile()
|
||||
}
|
||||
|
||||
icon: root.profileIcon()
|
||||
tooltipText: root.profileName()
|
||||
colorBg: Color.mSurfaceVariant
|
||||
colorFg: Color.mOnSurface
|
||||
colorBg: (PowerProfileService.profile === PowerProfile.Balanced) ? Color.mSurfaceVariant : Color.mPrimary
|
||||
colorFg: (PowerProfileService.profile === PowerProfile.Balanced) ? Color.mOnSurface : Color.mOnPrimary
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
onClicked: root.changeProfile()
|
||||
|
||||
23
Modules/Bar/Widgets/PowerToggle.qml
Normal file
@@ -0,0 +1,23 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
sizeRatio: 0.8
|
||||
|
||||
icon: "power"
|
||||
tooltipText: "Power Settings"
|
||||
colorBg: Color.mSurfaceVariant
|
||||
colorFg: Color.mError
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
onClicked: PanelService.getPanel("powerPanel")?.toggle()
|
||||
}
|
||||
@@ -8,12 +8,12 @@ NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: 1.0
|
||||
|
||||
visible: ScreenRecorderService.isRecording
|
||||
icon: "videocam"
|
||||
tooltipText: "Screen Recording Active\nClick To Stop Recording"
|
||||
sizeMultiplier: 0.8
|
||||
icon: "camera-video"
|
||||
tooltipText: "Screen recording is active\nClick to stop recording"
|
||||
sizeRatio: 0.8
|
||||
colorBg: Color.mPrimary
|
||||
colorFg: Color.mOnPrimary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick.Effects
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
@@ -7,11 +10,30 @@ NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: 1.0
|
||||
|
||||
icon: "widgets"
|
||||
tooltipText: "Open Side Panel"
|
||||
sizeMultiplier: 0.8
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
readonly property bool useDistroLogo: (widgetSettings.useDistroLogo !== undefined) ? widgetSettings.useDistroLogo : widgetMetadata.useDistroLogo
|
||||
|
||||
icon: useDistroLogo ? "" : "noctalia"
|
||||
tooltipText: "Open side panel."
|
||||
sizeRatio: 0.8
|
||||
|
||||
colorBg: Color.mSurfaceVariant
|
||||
colorFg: Color.mOnSurface
|
||||
@@ -19,5 +41,26 @@ NIconButton {
|
||||
colorBorderHover: Color.transparent
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: PanelService.getPanel("sidePanel")?.toggle(screen)
|
||||
onClicked: PanelService.getPanel("sidePanel")?.toggle(this)
|
||||
onRightClicked: PanelService.getPanel("settingsPanel")?.toggle()
|
||||
|
||||
IconImage {
|
||||
id: logo
|
||||
anchors.centerIn: parent
|
||||
width: root.width * 0.6
|
||||
height: width
|
||||
source: useDistroLogo ? DistroLogoService.osLogo : ""
|
||||
visible: useDistroLogo && source !== ""
|
||||
smooth: true
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: logo
|
||||
source: logo
|
||||
//visible: logo.visible
|
||||
colorization: 1
|
||||
brightness: 1
|
||||
saturation: 1
|
||||
colorizationColor: root.hovering ? Color.mSurfaceVariant : Color.mOnSurface
|
||||
}
|
||||
}
|
||||
|
||||
40
Modules/Bar/Widgets/Spacer.qml
Normal file
@@ -0,0 +1,40 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Widget properties passed from Bar.qml
|
||||
property var screen
|
||||
property real scaling: 1.0
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
// Use settings or defaults from BarWidgetRegistry
|
||||
readonly property int spacerWidth: widgetSettings.width !== undefined ? widgetSettings.width : widgetMetadata.width
|
||||
|
||||
// Set the width based on user settings
|
||||
implicitWidth: spacerWidth * scaling
|
||||
implicitHeight: Style.barHeight * scaling
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
}
|
||||
@@ -1,98 +1,426 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Row {
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: 1.0
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
Rectangle {
|
||||
// Let the Rectangle size itself based on its content (the Row)
|
||||
width: row.width + Style.marginM * scaling * 2
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
height: Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Color.mSurfaceVariant
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
readonly property bool showCpuUsage: (widgetSettings.showCpuUsage !== undefined) ? widgetSettings.showCpuUsage : widgetMetadata.showCpuUsage
|
||||
readonly property bool showCpuTemp: (widgetSettings.showCpuTemp !== undefined) ? widgetSettings.showCpuTemp : widgetMetadata.showCpuTemp
|
||||
readonly property bool showMemoryUsage: (widgetSettings.showMemoryUsage !== undefined) ? widgetSettings.showMemoryUsage : widgetMetadata.showMemoryUsage
|
||||
readonly property bool showMemoryAsPercent: (widgetSettings.showMemoryAsPercent !== undefined) ? widgetSettings.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent
|
||||
readonly property bool showNetworkStats: (widgetSettings.showNetworkStats !== undefined) ? widgetSettings.showNetworkStats : widgetMetadata.showNetworkStats
|
||||
readonly property bool showDiskUsage: (widgetSettings.showDiskUsage !== undefined) ? widgetSettings.showDiskUsage : widgetMetadata.showDiskUsage
|
||||
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : Math.round(horizontalLayout.implicitWidth + Style.marginM * 2 * scaling)
|
||||
implicitHeight: (barPosition === "left" || barPosition === "right") ? Math.round(verticalLayout.implicitHeight + Style.marginM * 2 * scaling) : Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
// Horizontal layout for top/bottom bars
|
||||
RowLayout {
|
||||
id: horizontalLayout
|
||||
anchors.centerIn: parent
|
||||
anchors.leftMargin: Style.marginM * scaling
|
||||
anchors.rightMargin: Style.marginM * scaling
|
||||
spacing: Style.marginXS * scaling
|
||||
visible: barPosition === "top" || barPosition === "bottom"
|
||||
|
||||
// CPU Usage Component
|
||||
Item {
|
||||
id: mainContainer
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.marginS * scaling
|
||||
anchors.rightMargin: Style.marginS * scaling
|
||||
Layout.preferredWidth: cpuUsageRow.implicitWidth
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: showCpuUsage
|
||||
|
||||
Row {
|
||||
id: row
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
Row {
|
||||
id: cpuUsageLayout
|
||||
spacing: Style.marginXS * scaling
|
||||
RowLayout {
|
||||
id: cpuUsageRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginXXS * scaling
|
||||
|
||||
NIcon {
|
||||
id: cpuUsageIcon
|
||||
text: "speed"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
id: cpuUsageText
|
||||
text: `${SystemStatService.cpuUsage}%`
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
NIcon {
|
||||
icon: "cpu-usage"
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
// CPU Temperature Component
|
||||
Row {
|
||||
id: cpuTempLayout
|
||||
// spacing is thin here to compensate for the vertical thermometer icon
|
||||
spacing: Style.marginXXS * scaling
|
||||
NText {
|
||||
text: `${SystemStatService.cpuUsage}%`
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIcon {
|
||||
text: "thermometer"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
// CPU Temperature Component
|
||||
Item {
|
||||
Layout.preferredWidth: cpuTempRow.implicitWidth
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: showCpuTemp
|
||||
|
||||
NText {
|
||||
text: `${SystemStatService.cpuTemp}°C`
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
RowLayout {
|
||||
id: cpuTempRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginXXS * scaling
|
||||
|
||||
NIcon {
|
||||
icon: "cpu-temperature"
|
||||
// Fire is so tall, we need to make it smaller
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
// Memory Usage Component
|
||||
Row {
|
||||
id: memoryUsageLayout
|
||||
spacing: Style.marginXS * scaling
|
||||
NText {
|
||||
text: `${SystemStatService.cpuTemp}°C`
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIcon {
|
||||
text: "memory"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
// Memory Usage Component
|
||||
Item {
|
||||
Layout.preferredWidth: memoryUsageRow.implicitWidth
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: showMemoryUsage
|
||||
|
||||
NText {
|
||||
text: `${SystemStatService.memoryUsageGb}G`
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
RowLayout {
|
||||
id: memoryUsageRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginXXS * scaling
|
||||
|
||||
NIcon {
|
||||
icon: "memory"
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: showMemoryAsPercent ? `${SystemStatService.memPercent}%` : `${SystemStatService.memGb}G`
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Network Download Speed Component
|
||||
Item {
|
||||
Layout.preferredWidth: networkDownloadRow.implicitWidth
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: showNetworkStats
|
||||
|
||||
RowLayout {
|
||||
id: networkDownloadRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginXS * scaling
|
||||
|
||||
NIcon {
|
||||
icon: "download-speed"
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Network Upload Speed Component
|
||||
Item {
|
||||
Layout.preferredWidth: networkUploadRow.implicitWidth
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: showNetworkStats
|
||||
|
||||
RowLayout {
|
||||
id: networkUploadRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginXS * scaling
|
||||
|
||||
NIcon {
|
||||
icon: "upload-speed"
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disk Usage Component (primary drive)
|
||||
Item {
|
||||
Layout.preferredWidth: diskUsageRow.implicitWidth
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: showDiskUsage
|
||||
|
||||
RowLayout {
|
||||
id: diskUsageRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginXS * scaling
|
||||
|
||||
NIcon {
|
||||
icon: "storage"
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: `${SystemStatService.diskPercent}%`
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical layout for left/right bars
|
||||
ColumnLayout {
|
||||
id: verticalLayout
|
||||
anchors.centerIn: parent
|
||||
anchors.topMargin: Style.marginS * scaling
|
||||
anchors.bottomMargin: Style.marginS * scaling
|
||||
width: Math.round(28 * scaling)
|
||||
spacing: Style.marginS * scaling
|
||||
visible: barPosition === "left" || barPosition === "right"
|
||||
|
||||
// CPU Usage Component
|
||||
Item {
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.preferredWidth: Math.round(28 * scaling)
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: showCpuUsage
|
||||
|
||||
Column {
|
||||
id: cpuUsageRowVertical
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginXXS * scaling
|
||||
|
||||
NText {
|
||||
text: `${Math.round(SystemStatService.cpuUsage)}%`
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "cpu-usage"
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CPU Temperature Component
|
||||
Item {
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.preferredWidth: Math.round(28 * scaling)
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: showCpuTemp
|
||||
|
||||
Column {
|
||||
id: cpuTempRowVertical
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginXXS * scaling
|
||||
|
||||
NText {
|
||||
text: `${SystemStatService.cpuTemp}°`
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "cpu-temperature"
|
||||
// Fire is so tall, we need to make it smaller
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Memory Usage Component
|
||||
Item {
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.preferredWidth: Math.round(28 * scaling)
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: showMemoryUsage
|
||||
|
||||
Column {
|
||||
id: memoryUsageRowVertical
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginXXS * scaling
|
||||
|
||||
NText {
|
||||
text: showMemoryAsPercent ? `${SystemStatService.memPercent}%` : `${Math.round(SystemStatService.memGb)}G`
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "memory"
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Network Download Speed Component
|
||||
Item {
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.preferredWidth: Math.round(28 * scaling)
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: showNetworkStats
|
||||
|
||||
Column {
|
||||
id: networkDownloadRowVertical
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginXXS * scaling
|
||||
|
||||
NIcon {
|
||||
icon: "download-speed"
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Network Upload Speed Component
|
||||
Item {
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.preferredWidth: Math.round(28 * scaling)
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: showNetworkStats
|
||||
|
||||
Column {
|
||||
id: networkUploadRowVertical
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginXXS * scaling
|
||||
|
||||
NIcon {
|
||||
icon: "upload-speed"
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disk Usage Component (primary drive)
|
||||
Item {
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.preferredWidth: Math.round(28 * scaling)
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: showDiskUsage
|
||||
|
||||
ColumnLayout {
|
||||
id: diskUsageRowVertical
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginXXS * scaling
|
||||
|
||||
NIcon {
|
||||
icon: "storage"
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: `${SystemStatService.diskPercent}%`
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
101
Modules/Bar/Widgets/Taskbar.qml
Normal file
@@ -0,0 +1,101 @@
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
readonly property real itemSize: Style.baseWidgetSize * 0.8 * scaling
|
||||
|
||||
// Always visible when there are toplevels
|
||||
implicitWidth: taskbarLayout.implicitWidth + Style.marginM * scaling * 2
|
||||
implicitHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
RowLayout {
|
||||
id: taskbarLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginXXS * root.scaling
|
||||
|
||||
Repeater {
|
||||
model: ToplevelManager && ToplevelManager.toplevels ? ToplevelManager.toplevels : []
|
||||
delegate: Item {
|
||||
id: taskbarItem
|
||||
required property Toplevel modelData
|
||||
property Toplevel toplevel: modelData
|
||||
property bool isActive: ToplevelManager.activeToplevel === modelData
|
||||
|
||||
Layout.preferredWidth: root.itemSize
|
||||
Layout.preferredHeight: root.itemSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
Rectangle {
|
||||
id: iconBackground
|
||||
anchors.centerIn: parent
|
||||
width: root.itemSize * 0.75
|
||||
height: root.itemSize * 0.75
|
||||
color: taskbarItem.isActive ? Color.mPrimary : root.color
|
||||
border.width: 0
|
||||
radius: Math.round(Style.radiusXS * root.scaling)
|
||||
border.color: "transparent"
|
||||
z: -1
|
||||
|
||||
IconImage {
|
||||
id: appIcon
|
||||
anchors.centerIn: parent
|
||||
width: Style.marginL * root.scaling
|
||||
height: Style.marginL * root.scaling
|
||||
source: AppIcons.iconForAppId(taskbarItem.modelData.appId)
|
||||
smooth: true
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
|
||||
onPressed: function (mouse) {
|
||||
if (!taskbarItem.modelData)
|
||||
return
|
||||
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
try {
|
||||
taskbarItem.modelData.activate()
|
||||
} catch (error) {
|
||||
Logger.error("Taskbar", "Failed to activate toplevel: " + error)
|
||||
}
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
try {
|
||||
taskbarItem.modelData.close()
|
||||
} catch (error) {
|
||||
Logger.error("Taskbar", "Failed to close toplevel: " + error)
|
||||
}
|
||||
}
|
||||
}
|
||||
onEntered: taskbarTooltip.show()
|
||||
onExited: taskbarTooltip.hide()
|
||||
}
|
||||
|
||||
NTooltip {
|
||||
id: taskbarTooltip
|
||||
text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown App."
|
||||
target: taskbarItem
|
||||
positionAbove: Settings.data.bar.position === "bottom"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,27 +14,37 @@ Rectangle {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: 1.0
|
||||
|
||||
readonly property real itemSize: 24 * scaling
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
|
||||
|
||||
function onLoaded() {
|
||||
// When the widget is fully initialized with its props set the screen for the trayMenu
|
||||
if (trayMenu.item) {
|
||||
trayMenu.item.screen = screen
|
||||
}
|
||||
}
|
||||
|
||||
visible: SystemTray.items.values.length > 0
|
||||
implicitWidth: tray.width + Style.marginM * scaling * 2
|
||||
implicitHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
implicitWidth: isVertical ? Math.round(Style.capsuleHeight * scaling) : (trayFlow.implicitWidth + Style.marginS * scaling * 2)
|
||||
implicitHeight: isVertical ? (trayFlow.implicitHeight + Style.marginS * scaling * 2) : Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Row {
|
||||
id: tray
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
Flow {
|
||||
id: trayFlow
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginS * scaling
|
||||
flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: SystemTray.items
|
||||
|
||||
delegate: Item {
|
||||
width: itemSize
|
||||
height: itemSize
|
||||
@@ -102,9 +112,21 @@ Rectangle {
|
||||
if (modelData.hasMenu && modelData.menu && trayMenu.item) {
|
||||
trayPanel.open()
|
||||
|
||||
// Anchor the menu to the tray icon item (parent) and position it below the icon
|
||||
const menuX = (width / 2) - (trayMenu.item.width / 2)
|
||||
const menuY = (Style.barHeight * scaling)
|
||||
// Position menu based on bar position
|
||||
let menuX, menuY
|
||||
if (barPosition === "left") {
|
||||
// For left bar: position menu to the right of the bar
|
||||
menuX = width + Style.marginM * scaling
|
||||
menuY = 0
|
||||
} else if (barPosition === "right") {
|
||||
// For right bar: position menu to the left of the bar
|
||||
menuX = -trayMenu.item.width - Style.marginM * scaling
|
||||
menuY = 0
|
||||
} else {
|
||||
// For horizontal bars: center horizontally and position below
|
||||
menuX = (width / 2) - (trayMenu.item.width / 2)
|
||||
menuY = Math.round(Style.barHeight * scaling)
|
||||
}
|
||||
trayMenu.item.menu = modelData.menu
|
||||
trayMenu.item.showAt(parent, menuX, menuY)
|
||||
} else {
|
||||
@@ -138,13 +160,14 @@ Rectangle {
|
||||
|
||||
function open() {
|
||||
visible = true
|
||||
|
||||
PanelService.willOpenPanel(trayPanel)
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible = false
|
||||
trayMenu.item.hideMenu()
|
||||
if (trayMenu.item) {
|
||||
trayMenu.item.hideMenu()
|
||||
}
|
||||
}
|
||||
|
||||
// Clicking outside of the rectangle to close
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Commons
|
||||
import qs.Modules.SettingsPanel
|
||||
@@ -10,19 +11,40 @@ Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: 1.0
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
|
||||
readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode
|
||||
|
||||
// Used to avoid opening the pill on Quickshell startup
|
||||
property bool firstVolumeReceived: false
|
||||
property int wheelAccumulator: 0
|
||||
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
|
||||
function getIcon() {
|
||||
if (AudioService.muted) {
|
||||
return "volume_off"
|
||||
return "volume-mute"
|
||||
}
|
||||
return AudioService.volume <= Number.EPSILON ? "volume_off" : (AudioService.volume < 0.33 ? "volume_down" : "volume_up")
|
||||
return (AudioService.volume <= Number.EPSILON) ? "volume-zero" : (AudioService.volume <= 0.5) ? "volume-low" : "volume-high"
|
||||
}
|
||||
|
||||
// Connection used to open the pill when volume changes
|
||||
@@ -51,25 +73,36 @@ Item {
|
||||
|
||||
NPill {
|
||||
id: pill
|
||||
icon: getIcon()
|
||||
iconCircleColor: Color.mPrimary
|
||||
collapsedIconColor: Color.mOnSurface
|
||||
autoHide: false // Important to be false so we can hover as long as we want
|
||||
text: Math.floor(AudioService.volume * 100) + "%"
|
||||
tooltipText: "Volume: " + Math.round(
|
||||
AudioService.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume."
|
||||
|
||||
onWheel: function (angle) {
|
||||
if (angle > 0) {
|
||||
rightOpen: BarWidgetRegistry.getNPillDirection(root)
|
||||
icon: getIcon()
|
||||
autoHide: false // Important to be false so we can hover as long as we want
|
||||
text: Math.floor(AudioService.volume * 100)
|
||||
suffix: "%"
|
||||
forceOpen: displayMode === "alwaysShow"
|
||||
forceClose: displayMode === "alwaysHide"
|
||||
tooltipText: "Volume: " + Math.round(AudioService.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute."
|
||||
|
||||
onWheel: function (delta) {
|
||||
wheelAccumulator += delta
|
||||
if (wheelAccumulator >= 120) {
|
||||
wheelAccumulator = 0
|
||||
AudioService.increaseVolume()
|
||||
} else if (angle < 0) {
|
||||
} else if (wheelAccumulator <= -120) {
|
||||
wheelAccumulator = 0
|
||||
AudioService.decreaseVolume()
|
||||
}
|
||||
}
|
||||
onClicked: {
|
||||
AudioService.setOutputMuted(!AudioService.muted)
|
||||
}
|
||||
onRightClicked: {
|
||||
var settingsPanel = PanelService.getPanel("settingsPanel")
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.AudioService
|
||||
settingsPanel.open(screen)
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.Audio
|
||||
settingsPanel.open()
|
||||
}
|
||||
onMiddleClicked: {
|
||||
Quickshell.execDetached(["pwvucontrol"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,19 +11,9 @@ NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property real scaling: 1.0
|
||||
|
||||
visible: Settings.data.network.wifiEnabled
|
||||
|
||||
sizeMultiplier: 0.8
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.log("WiFi", "Widget component completed")
|
||||
Logger.log("WiFi", "NetworkService available:", !!NetworkService)
|
||||
if (NetworkService) {
|
||||
Logger.log("WiFi", "NetworkService.networks available:", !!NetworkService.networks)
|
||||
}
|
||||
}
|
||||
sizeRatio: 0.8
|
||||
|
||||
colorBg: Color.mSurfaceVariant
|
||||
colorFg: Color.mOnSurface
|
||||
@@ -32,8 +22,9 @@ NIconButton {
|
||||
|
||||
icon: {
|
||||
try {
|
||||
if (NetworkService.ethernet)
|
||||
return "lan"
|
||||
if (NetworkService.ethernetConnected) {
|
||||
return "ethernet"
|
||||
}
|
||||
let connected = false
|
||||
let signalStrength = 0
|
||||
for (const net in NetworkService.networks) {
|
||||
@@ -43,19 +34,13 @@ NIconButton {
|
||||
break
|
||||
}
|
||||
}
|
||||
return connected ? NetworkService.signalIcon(signalStrength) : "wifi_find"
|
||||
return connected ? NetworkService.signalIcon(signalStrength) : "wifi-off"
|
||||
} catch (error) {
|
||||
Logger.error("WiFi", "Error getting icon:", error)
|
||||
Logger.error("Wi-Fi", "Error getting icon:", error)
|
||||
return "signal_wifi_bad"
|
||||
}
|
||||
}
|
||||
tooltipText: "Network / WiFi"
|
||||
onClicked: {
|
||||
try {
|
||||
Logger.log("WiFi", "Button clicked, toggling panel")
|
||||
PanelService.getPanel("wifiPanel")?.toggle(screen)
|
||||
} catch (error) {
|
||||
Logger.error("WiFi", "Error toggling panel:", error)
|
||||
}
|
||||
}
|
||||
tooltipText: "Manage Wi-Fi."
|
||||
onClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
|
||||
onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
|
||||
}
|
||||
|
||||
@@ -11,8 +11,30 @@ import qs.Services
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen: null
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0) {
|
||||
var widgets = Settings.data.bar.widgets[section]
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
|
||||
readonly property string labelMode: (widgetSettings.labelMode !== undefined) ? widgetSettings.labelMode : widgetMetadata.labelMode
|
||||
readonly property bool hideUnoccupied: (widgetSettings.hideUnoccupied !== undefined) ? widgetSettings.hideUnoccupied : widgetMetadata.hideUnoccupied
|
||||
|
||||
property bool isDestroying: false
|
||||
property bool hovered: false
|
||||
@@ -22,22 +44,48 @@ Item {
|
||||
property bool effectsActive: false
|
||||
property color effectColor: Color.mPrimary
|
||||
|
||||
property int horizontalPadding: Math.round(16 * scaling)
|
||||
property int spacingBetweenPills: Math.round(8 * scaling)
|
||||
property int horizontalPadding: Math.round(Style.marginS * scaling)
|
||||
property int spacingBetweenPills: Math.round(Style.marginXS * scaling)
|
||||
|
||||
signal workspaceChanged(int workspaceId, color accentColor)
|
||||
|
||||
implicitHeight: Math.round(36 * scaling)
|
||||
implicitWidth: {
|
||||
implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)
|
||||
implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.barHeight * scaling) : calculatedHorizontalWidth()
|
||||
|
||||
function calculatedWsWidth(ws) {
|
||||
if (ws.isFocused)
|
||||
return Math.round(44 * scaling)
|
||||
else if (ws.isActive)
|
||||
return Math.round(28 * scaling)
|
||||
else
|
||||
return Math.round(20 * scaling)
|
||||
}
|
||||
|
||||
function calculatedWsHeight(ws) {
|
||||
if (ws.isFocused)
|
||||
return Math.round(44 * scaling)
|
||||
else if (ws.isActive)
|
||||
return Math.round(28 * scaling)
|
||||
else
|
||||
return Math.round(20 * scaling)
|
||||
}
|
||||
|
||||
function calculatedVerticalHeight() {
|
||||
let total = 0
|
||||
for (var i = 0; i < localWorkspaces.count; i++) {
|
||||
const ws = localWorkspaces.get(i)
|
||||
if (ws.isFocused)
|
||||
total += Math.round(44 * scaling)
|
||||
else if (ws.isActive)
|
||||
total += Math.round(28 * scaling)
|
||||
else
|
||||
total += Math.round(16 * scaling)
|
||||
total += calculatedWsHeight(ws)
|
||||
}
|
||||
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills
|
||||
total += horizontalPadding * 2
|
||||
return total
|
||||
}
|
||||
|
||||
function calculatedHorizontalWidth() {
|
||||
let total = 0
|
||||
for (var i = 0; i < localWorkspaces.count; i++) {
|
||||
const ws = localWorkspaces.get(i)
|
||||
total += calculatedWsWidth(ws)
|
||||
}
|
||||
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills
|
||||
total += horizontalPadding * 2
|
||||
@@ -53,6 +101,7 @@ Item {
|
||||
}
|
||||
|
||||
onScreenChanged: refreshWorkspaces()
|
||||
onHideUnoccupiedChanged: refreshWorkspaces()
|
||||
|
||||
Connections {
|
||||
target: WorkspaceService
|
||||
@@ -67,11 +116,15 @@ Item {
|
||||
for (var i = 0; i < WorkspaceService.workspaces.count; i++) {
|
||||
const ws = WorkspaceService.workspaces.get(i)
|
||||
if (ws.output.toLowerCase() === screen.name.toLowerCase()) {
|
||||
if (hideUnoccupied && !ws.isOccupied && !ws.isFocused) {
|
||||
continue
|
||||
}
|
||||
localWorkspaces.append(ws)
|
||||
}
|
||||
}
|
||||
}
|
||||
workspaceRepeater.model = localWorkspaces
|
||||
workspaceRepeaterHorizontal.model = localWorkspaces
|
||||
workspaceRepeaterVertical.model = localWorkspaces
|
||||
updateWorkspaceFocus()
|
||||
}
|
||||
|
||||
@@ -103,7 +156,7 @@ Item {
|
||||
property: "masterProgress"
|
||||
from: 0.0
|
||||
to: 1.0
|
||||
duration: 1000
|
||||
duration: Style.animationSlow * 2
|
||||
easing.type: Easing.OutQuint
|
||||
}
|
||||
PropertyAction {
|
||||
@@ -120,54 +173,69 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
id: workspaceBackground
|
||||
width: parent.width - Style.marginS * scaling * 2
|
||||
|
||||
height: Math.round(Style.capsuleHeight * scaling)
|
||||
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : parent.width
|
||||
height: (barPosition === "left" || barPosition === "right") ? parent.height : Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
shadowColor: Color.mShadow
|
||||
shadowVerticalOffset: 0
|
||||
shadowHorizontalOffset: 0
|
||||
shadowOpacity: 0.10
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal layout for top/bottom bars
|
||||
Row {
|
||||
id: pillRow
|
||||
spacing: spacingBetweenPills
|
||||
anchors.verticalCenter: workspaceBackground.verticalCenter
|
||||
width: root.width - horizontalPadding * 2
|
||||
x: horizontalPadding
|
||||
visible: barPosition === "top" || barPosition === "bottom"
|
||||
|
||||
Repeater {
|
||||
id: workspaceRepeater
|
||||
id: workspaceRepeaterHorizontal
|
||||
model: localWorkspaces
|
||||
Item {
|
||||
id: workspacePillContainer
|
||||
height: Math.round(12 * scaling)
|
||||
width: {
|
||||
if (model.isFocused)
|
||||
return Math.round(44 * scaling)
|
||||
else if (model.isActive)
|
||||
return Math.round(28 * scaling)
|
||||
else
|
||||
return Math.round(16 * scaling)
|
||||
}
|
||||
height: (labelMode !== "none") ? Math.round(18 * scaling) : Math.round(14 * scaling)
|
||||
width: root.calculatedWsWidth(model)
|
||||
|
||||
Rectangle {
|
||||
id: workspacePill
|
||||
id: pill
|
||||
anchors.fill: parent
|
||||
radius: {
|
||||
if (model.isFocused)
|
||||
return Math.round(12 * scaling)
|
||||
else
|
||||
// half of focused height (if you want to animate this too)
|
||||
return Math.round(6 * scaling)
|
||||
|
||||
Loader {
|
||||
active: (labelMode !== "none")
|
||||
sourceComponent: Component {
|
||||
Text {
|
||||
x: (pill.width - width) / 2
|
||||
y: (pill.height - height) / 2 + (height - contentHeight) / 2
|
||||
text: {
|
||||
if (labelMode === "name" && model.name && model.name.length > 0) {
|
||||
return model.name.substring(0, 2)
|
||||
} else {
|
||||
return model.idx.toString()
|
||||
}
|
||||
}
|
||||
font.pointSize: model.isFocused ? Style.fontSizeXS * scaling : Style.fontSizeXXS * scaling
|
||||
font.capitalization: Font.AllUppercase
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.weight: Style.fontWeightBold
|
||||
wrapMode: Text.Wrap
|
||||
color: {
|
||||
if (model.isFocused)
|
||||
return Color.mOnPrimary
|
||||
if (model.isUrgent)
|
||||
return Color.mOnError
|
||||
if (model.isActive || model.isOccupied)
|
||||
return Color.mOnSecondary
|
||||
|
||||
return Color.mOnSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
radius: width * 0.5
|
||||
color: {
|
||||
if (model.isFocused)
|
||||
return Color.mPrimary
|
||||
@@ -175,8 +243,6 @@ Item {
|
||||
return Color.mError
|
||||
if (model.isActive || model.isOccupied)
|
||||
return Color.mSecondary
|
||||
if (model.isUrgent)
|
||||
return Color.mError
|
||||
|
||||
return Color.mOutline
|
||||
}
|
||||
@@ -260,4 +326,149 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical layout for left/right bars
|
||||
Column {
|
||||
id: pillColumn
|
||||
spacing: spacingBetweenPills
|
||||
anchors.horizontalCenter: workspaceBackground.horizontalCenter
|
||||
height: root.height - horizontalPadding * 2
|
||||
y: horizontalPadding
|
||||
visible: barPosition === "left" || barPosition === "right"
|
||||
|
||||
Repeater {
|
||||
id: workspaceRepeaterVertical
|
||||
model: localWorkspaces
|
||||
Item {
|
||||
id: workspacePillContainerVertical
|
||||
width: (labelMode !== "none") ? Math.round(18 * scaling) : Math.round(14 * scaling)
|
||||
height: root.calculatedWsHeight(model)
|
||||
|
||||
Rectangle {
|
||||
id: pillVertical
|
||||
anchors.fill: parent
|
||||
|
||||
Loader {
|
||||
active: (labelMode !== "none")
|
||||
sourceComponent: Component {
|
||||
Text {
|
||||
x: (pillVertical.width - width) / 2
|
||||
y: (pillVertical.height - height) / 2 + (height - contentHeight) / 2
|
||||
text: {
|
||||
if (labelMode === "name" && model.name && model.name.length > 0) {
|
||||
return model.name.substring(0, 2)
|
||||
} else {
|
||||
return model.idx.toString()
|
||||
}
|
||||
}
|
||||
font.pointSize: model.isFocused ? Style.fontSizeXS * scaling : Style.fontSizeXXS * scaling
|
||||
font.capitalization: Font.AllUppercase
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.weight: Style.fontWeightBold
|
||||
wrapMode: Text.Wrap
|
||||
color: {
|
||||
if (model.isFocused)
|
||||
return Color.mOnPrimary
|
||||
if (model.isUrgent)
|
||||
return Color.mOnError
|
||||
if (model.isActive || model.isOccupied)
|
||||
return Color.mOnSecondary
|
||||
|
||||
return Color.mOnSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
radius: width * 0.5
|
||||
color: {
|
||||
if (model.isFocused)
|
||||
return Color.mPrimary
|
||||
if (model.isUrgent)
|
||||
return Color.mError
|
||||
if (model.isActive || model.isOccupied)
|
||||
return Color.mSecondary
|
||||
|
||||
return Color.mOutline
|
||||
}
|
||||
scale: model.isFocused ? 1.0 : 0.9
|
||||
z: 0
|
||||
|
||||
MouseArea {
|
||||
id: pillMouseAreaVertical
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
WorkspaceService.switchToWorkspace(model.idx)
|
||||
}
|
||||
hoverEnabled: true
|
||||
}
|
||||
// Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
Behavior on radius {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
// Burst effect overlay for focused pill (smaller outline)
|
||||
Rectangle {
|
||||
id: pillBurstVertical
|
||||
anchors.centerIn: workspacePillContainerVertical
|
||||
width: workspacePillContainerVertical.width + 18 * root.masterProgress * scale
|
||||
height: workspacePillContainerVertical.height + 18 * root.masterProgress * scale
|
||||
radius: width / 2
|
||||
color: Color.transparent
|
||||
border.color: root.effectColor
|
||||
border.width: Math.max(1, Math.round((2 + 6 * (1.0 - root.masterProgress)) * scaling))
|
||||
opacity: root.effectsActive && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0
|
||||
visible: root.effectsActive && model.isFocused
|
||||
z: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
221
Modules/BluetoothPanel/BluetoothDevicesList.qml
Normal file
@@ -0,0 +1,221 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property string tooltipText: ""
|
||||
property var model: {
|
||||
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NText {
|
||||
text: root.label
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mSecondary
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.fillWidth: true
|
||||
visible: root.model.length > 0
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: deviceList
|
||||
Layout.fillWidth: true
|
||||
model: root.model
|
||||
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||
|
||||
Rectangle {
|
||||
id: device
|
||||
|
||||
readonly property bool canConnect: BluetoothService.canConnect(modelData)
|
||||
readonly property bool canDisconnect: BluetoothService.canDisconnect(modelData)
|
||||
readonly property bool isBusy: BluetoothService.isDeviceBusy(modelData)
|
||||
|
||||
function getContentColor(defaultColor = Color.mOnSurface) {
|
||||
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
|
||||
return Color.mPrimary
|
||||
if (modelData.blocked)
|
||||
return Color.mError
|
||||
return defaultColor
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: deviceLayout.implicitHeight + (Style.marginM * scaling * 2)
|
||||
radius: Style.radiusM * scaling
|
||||
color: Color.mSurface
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
border.color: getContentColor(Color.mOutline)
|
||||
|
||||
RowLayout {
|
||||
id: deviceLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
// One device BT icon
|
||||
NIcon {
|
||||
icon: BluetoothService.getDeviceIcon(modelData)
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
color: getContentColor(Color.mOnSurface)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginXXS * scaling
|
||||
|
||||
// Device name
|
||||
NText {
|
||||
text: modelData.name || modelData.deviceName
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
elide: Text.ElideRight
|
||||
color: getContentColor(Color.mOnSurface)
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Status
|
||||
NText {
|
||||
text: BluetoothService.getStatusString(modelData)
|
||||
visible: text !== ""
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: getContentColor(Color.mOnSurfaceVariant)
|
||||
}
|
||||
|
||||
// Signal Strength
|
||||
RowLayout {
|
||||
visible: modelData.signalStrength !== undefined
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginXS * scaling
|
||||
|
||||
// Device signal strength - "Unknown" when not connected
|
||||
NText {
|
||||
text: BluetoothService.getSignalStrength(modelData)
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: getContentColor(Color.mOnSurfaceVariant)
|
||||
}
|
||||
|
||||
NIcon {
|
||||
visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
|
||||
text: BluetoothService.getSignalIcon(modelData)
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: getContentColor(Color.mOnSurface)
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
|
||||
text: (modelData.signalStrength !== undefined && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: getContentColor(Color.mOnSurface)
|
||||
}
|
||||
}
|
||||
|
||||
// Battery
|
||||
NText {
|
||||
visible: modelData.batteryAvailable
|
||||
text: BluetoothService.getBattery(modelData)
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: getContentColor(Color.mOnSurfaceVariant)
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer to push connect button to the right
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Call to action
|
||||
NButton {
|
||||
id: button
|
||||
visible: (modelData.state !== BluetoothDeviceState.Connecting)
|
||||
enabled: (canConnect || canDisconnect) && !isBusy
|
||||
outlined: !button.hovered
|
||||
fontSize: Style.fontSizeXS * scaling
|
||||
fontWeight: Style.fontWeightMedium
|
||||
backgroundColor: {
|
||||
if (device.canDisconnect && !isBusy) {
|
||||
return Color.mError
|
||||
}
|
||||
return Color.mPrimary
|
||||
}
|
||||
tooltipText: root.tooltipText
|
||||
text: {
|
||||
if (modelData.pairing) {
|
||||
return "Pairing..."
|
||||
}
|
||||
if (modelData.blocked) {
|
||||
return "Blocked"
|
||||
}
|
||||
if (modelData.connected) {
|
||||
return "Disconnect"
|
||||
}
|
||||
return "Connect"
|
||||
}
|
||||
icon: (isBusy ? "hourglass-split" : null)
|
||||
onClicked: {
|
||||
if (modelData.connected) {
|
||||
BluetoothService.disconnectDevice(modelData)
|
||||
} else {
|
||||
BluetoothService.connectDeviceWithTrust(modelData)
|
||||
}
|
||||
}
|
||||
onRightClicked: {
|
||||
BluetoothService.forgetDevice(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MouseArea {
|
||||
|
||||
// id: availableDeviceArea
|
||||
// acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
// anchors.fill: parent
|
||||
// hoverEnabled: true
|
||||
// cursorShape: (canConnect || canDisconnect)
|
||||
// && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
|
||||
// onEntered: {
|
||||
// if (root.tooltipText && !isBusy) {
|
||||
// tooltip.show()
|
||||
// }
|
||||
// }
|
||||
// onExited: {
|
||||
// if (root.tooltipText && !isBusy) {
|
||||
// tooltip.hide()
|
||||
// }
|
||||
// }
|
||||
// onClicked: function (mouse) {
|
||||
|
||||
// if (!modelData || modelData.pairing) {
|
||||
// return
|
||||
// }
|
||||
|
||||
// if (root.tooltipText && !isBusy) {
|
||||
// tooltip.hide()
|
||||
// }
|
||||
|
||||
// if (mouse.button === Qt.LeftButton) {
|
||||
// if (modelData.connected) {
|
||||
// BluetoothService.disconnectDevice(modelData)
|
||||
// } else {
|
||||
// BluetoothService.connectDeviceWithTrust(modelData)
|
||||
// }
|
||||
// } else if (mouse.button === Qt.RightButton) {
|
||||
// BluetoothService.forgetDevice(modelData)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,9 @@ import qs.Widgets
|
||||
NPanel {
|
||||
id: root
|
||||
|
||||
panelWidth: 380 * scaling
|
||||
panelHeight: 500 * scaling
|
||||
panelAnchorRight: true
|
||||
preferredWidth: 380
|
||||
preferredHeight: 500
|
||||
panelKeyboardFocus: true
|
||||
|
||||
panelContent: Rectangle {
|
||||
color: Color.transparent
|
||||
@@ -29,7 +29,7 @@ NPanel {
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NIcon {
|
||||
text: "bluetooth"
|
||||
icon: "bluetooth"
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
color: Color.mPrimary
|
||||
}
|
||||
@@ -42,10 +42,18 @@ NPanel {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: bluetoothSwitch
|
||||
checked: Settings.data.network.bluetoothEnabled
|
||||
onToggled: checked => BluetoothService.setBluetoothEnabled(checked)
|
||||
baseSize: Style.baseWidgetSize * 0.65 * scaling
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop_circle" : "refresh"
|
||||
enabled: Settings.data.network.bluetoothEnabled
|
||||
icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "refresh"
|
||||
tooltipText: "Refresh Devices"
|
||||
sizeMultiplier: 0.8
|
||||
sizeRatio: 0.8
|
||||
onClicked: {
|
||||
if (BluetoothService.adapter) {
|
||||
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering
|
||||
@@ -55,8 +63,8 @@ NPanel {
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: "Close"
|
||||
sizeMultiplier: 0.8
|
||||
tooltipText: "Close."
|
||||
sizeRatio: 0.8
|
||||
onClicked: {
|
||||
root.close()
|
||||
}
|
||||
@@ -67,286 +75,126 @@ NPanel {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
id: scrollView
|
||||
|
||||
Rectangle {
|
||||
visible: !(BluetoothService.adapter && BluetoothService.adapter.enabled)
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Color.transparent
|
||||
|
||||
// Center the content within this rectangle
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NIcon {
|
||||
icon: "bluetooth-off"
|
||||
font.pointSize: 64 * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Bluetooth is disabled"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Enable Bluetooth to see available devices."
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NScrollView {
|
||||
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
clip: true
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
|
||||
// Available devices
|
||||
Column {
|
||||
id: column
|
||||
contentWidth: availableWidth
|
||||
|
||||
ColumnLayout {
|
||||
width: parent.width
|
||||
spacing: Style.marginM * scaling
|
||||
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NText {
|
||||
text: "Available Devices"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurface
|
||||
font.weight: Style.fontWeightMedium
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
|
||||
// Connected devices
|
||||
BluetoothDevicesList {
|
||||
label: "Connected devices"
|
||||
property var items: {
|
||||
if (!BluetoothService.adapter || !Bluetooth.devices)
|
||||
return []
|
||||
|
||||
var filtered = Bluetooth.devices.values.filter(dev => {
|
||||
return dev && !dev.paired && !dev.pairing && !dev.blocked
|
||||
&& (dev.signalStrength === undefined
|
||||
|| dev.signalStrength > 0)
|
||||
})
|
||||
var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && dev.connected)
|
||||
return BluetoothService.sortDevices(filtered)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
property bool canConnect: BluetoothService.canConnect(modelData)
|
||||
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
|
||||
|
||||
width: parent.width
|
||||
height: 70
|
||||
radius: Style.radiusM * scaling
|
||||
color: {
|
||||
if (availableDeviceArea.containsMouse && !isBusy)
|
||||
return Color.mTertiary
|
||||
|
||||
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
|
||||
return Color.mPrimary
|
||||
|
||||
if (modelData.blocked)
|
||||
return Color.mError
|
||||
|
||||
return Color.mSurfaceVariant
|
||||
}
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.marginM * scaling
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
// One device BT icon
|
||||
NIcon {
|
||||
text: BluetoothService.getDeviceIcon(modelData)
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
color: {
|
||||
if (availableDeviceArea.containsMouse)
|
||||
return Color.mOnTertiary
|
||||
|
||||
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
|
||||
return Color.mOnPrimary
|
||||
|
||||
if (modelData.blocked)
|
||||
return Color.mOnError
|
||||
|
||||
return Color.mOnSurface
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: Style.marginXXS * scaling
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// One device name
|
||||
NText {
|
||||
text: modelData.name || modelData.deviceName
|
||||
font.pointSize: Style.fonttSizeMedium * scaling
|
||||
elide: Text.ElideRight
|
||||
color: {
|
||||
if (availableDeviceArea.containsMouse)
|
||||
return Color.mOnTertiary
|
||||
|
||||
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
|
||||
return Color.mOnPrimary
|
||||
|
||||
if (modelData.blocked)
|
||||
return Color.mOnError
|
||||
|
||||
return Color.mOnSurface
|
||||
}
|
||||
font.weight: Style.fontWeightMedium
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Style.marginXS * scaling
|
||||
|
||||
Row {
|
||||
spacing: Style.marginS * spacing
|
||||
|
||||
// One device signal strength - "Unknown" when not connected
|
||||
NText {
|
||||
text: {
|
||||
if (modelData.pairing)
|
||||
return "Pairing..."
|
||||
|
||||
if (modelData.blocked)
|
||||
return "Blocked"
|
||||
|
||||
return BluetoothService.getSignalStrength(modelData)
|
||||
}
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: {
|
||||
if (availableDeviceArea.containsMouse)
|
||||
return Color.mOnTertiary
|
||||
|
||||
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
|
||||
return Color.mOnPrimary
|
||||
|
||||
if (modelData.blocked)
|
||||
return Color.mOnError
|
||||
|
||||
return Color.mOnSurface
|
||||
}
|
||||
}
|
||||
|
||||
NIcon {
|
||||
text: BluetoothService.getSignalIcon(modelData)
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: {
|
||||
if (availableDeviceArea.containsMouse)
|
||||
return Color.mOnTertiary
|
||||
|
||||
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
|
||||
return Color.mOnPrimary
|
||||
|
||||
if (modelData.blocked)
|
||||
return Color.mOnError
|
||||
|
||||
return Color.mOnSurface
|
||||
}
|
||||
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0
|
||||
&& !modelData.pairing && !modelData.blocked
|
||||
}
|
||||
|
||||
NText {
|
||||
text: (modelData.signalStrength !== undefined
|
||||
&& modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: {
|
||||
if (availableDeviceArea.containsMouse)
|
||||
return Color.mOnTertiary
|
||||
|
||||
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
|
||||
return Color.mOnPrimary
|
||||
|
||||
if (modelData.blocked)
|
||||
return Color.mOnError
|
||||
|
||||
return Color.mOnSurface
|
||||
}
|
||||
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0
|
||||
&& !modelData.pairing && !modelData.blocked
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 80 * scaling
|
||||
height: 28 * scaling
|
||||
radius: Style.radiusM * scaling
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Style.marginM * scaling
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: modelData.state !== BluetoothDeviceState.Connecting
|
||||
color: Color.transparent
|
||||
|
||||
border.color: {
|
||||
if (availableDeviceArea.containsMouse) {
|
||||
return Color.mOnTertiary
|
||||
} else {
|
||||
return Color.mPrimary
|
||||
}
|
||||
}
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
opacity: canConnect || isBusy ? 1 : 0.5
|
||||
|
||||
// On device connect button
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
if (modelData.pairing)
|
||||
return "Pairing..."
|
||||
|
||||
if (modelData.blocked)
|
||||
return "Blocked"
|
||||
|
||||
return "Connect"
|
||||
}
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: {
|
||||
if (availableDeviceArea.containsMouse) {
|
||||
return Color.mOnTertiary
|
||||
} else {
|
||||
return Color.mPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: availableDeviceArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
|
||||
enabled: canConnect && !isBusy
|
||||
onClicked: {
|
||||
if (modelData)
|
||||
BluetoothService.connectDeviceWithTrust(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
model: items
|
||||
visible: items.length > 0
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Fallback if nothing available
|
||||
Column {
|
||||
width: parent.width
|
||||
// Known devices
|
||||
BluetoothDevicesList {
|
||||
label: "Known devices"
|
||||
tooltipText: "Left click to connect.\nRight click to forget."
|
||||
property var items: {
|
||||
if (!BluetoothService.adapter || !Bluetooth.devices)
|
||||
return []
|
||||
var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.connected && (dev.paired || dev.trusted))
|
||||
return BluetoothService.sortDevices(filtered)
|
||||
}
|
||||
model: items
|
||||
visible: items.length > 0
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Available devices
|
||||
BluetoothDevicesList {
|
||||
label: "Available devices"
|
||||
property var items: {
|
||||
if (!BluetoothService.adapter || !Bluetooth.devices)
|
||||
return []
|
||||
var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.paired && !dev.trusted)
|
||||
return BluetoothService.sortDevices(filtered)
|
||||
}
|
||||
model: items
|
||||
visible: items.length > 0
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Fallback - No devices, scanning
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Style.marginM * scaling
|
||||
visible: {
|
||||
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
|
||||
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) {
|
||||
return false
|
||||
}
|
||||
|
||||
var availableCount = Bluetooth.devices.values.filter(dev => {
|
||||
return dev && !dev.paired && !dev.pairing
|
||||
&& !dev.blocked
|
||||
&& (dev.signalStrength === undefined
|
||||
|| dev.signalStrength > 0)
|
||||
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0)
|
||||
}).length
|
||||
return availableCount === 0
|
||||
return (availableCount === 0)
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Style.marginM * scaling
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Style.marginXS * scaling
|
||||
|
||||
NIcon {
|
||||
text: "sync"
|
||||
font.pointSize: Style.fontSizeXLL * 1.5 * scaling
|
||||
icon: "refresh"
|
||||
font.pointSize: Style.fontSizeXXL * 1.5 * scaling
|
||||
color: Color.mPrimary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
RotationAnimation on rotation {
|
||||
running: true
|
||||
loops: Animation.Infinite
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 2000
|
||||
duration: Style.animationSlow * 4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,45 +202,22 @@ NPanel {
|
||||
text: "Scanning for devices..."
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurface
|
||||
font.weight: Style.fontWeightMedium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Make sure your device is in pairing mode"
|
||||
text: "Make sure your device is in pairing mode."
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "No devices found. Put your device in pairing mode and click Start Scanning."
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
visible: {
|
||||
if (!BluetoothService.adapter || !Bluetooth.devices)
|
||||
return true
|
||||
|
||||
var availableCount = Bluetooth.devices.values.filter(dev => {
|
||||
return dev && !dev.paired && !dev.pairing
|
||||
&& !dev.blocked
|
||||
&& (dev.signalStrength === undefined
|
||||
|| dev.signalStrength > 0)
|
||||
}).length
|
||||
return availableCount === 0 && !BluetoothService.adapter.discovering
|
||||
}
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
// This item takes up all the remaining vertical space.
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ import qs.Widgets
|
||||
NPanel {
|
||||
id: root
|
||||
|
||||
panelWidth: 340 * scaling
|
||||
panelHeight: 320 * scaling
|
||||
panelAnchorRight: true
|
||||
preferredWidth: 340
|
||||
preferredHeight: 320
|
||||
panelAnchorRight: Settings.data.bar.position === "right"
|
||||
|
||||
// Main Column
|
||||
panelContent: ColumnLayout {
|
||||
@@ -28,8 +28,8 @@ NPanel {
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NIconButton {
|
||||
icon: "chevron_left"
|
||||
tooltipText: "Previous Month"
|
||||
icon: "chevron-left"
|
||||
tooltipText: "Previous month"
|
||||
onClicked: {
|
||||
let newDate = new Date(grid.year, grid.month - 1, 1)
|
||||
grid.year = newDate.getFullYear()
|
||||
@@ -47,8 +47,8 @@ NPanel {
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "chevron_right"
|
||||
tooltipText: "Next Month"
|
||||
icon: "chevron-right"
|
||||
tooltipText: "Next month"
|
||||
onClicked: {
|
||||
let newDate = new Date(grid.year, grid.month + 1, 1)
|
||||
grid.year = newDate.getFullYear()
|
||||
|
||||
@@ -9,286 +9,385 @@ import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Loader {
|
||||
active: (Settings.data.dock.monitors.length > 0)
|
||||
sourceComponent: Component {
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
PanelWindow {
|
||||
id: dockWindow
|
||||
delegate: Item {
|
||||
required property ShellScreen modelData
|
||||
property real scaling: ScalingService.getScreenScale(modelData)
|
||||
|
||||
Connections {
|
||||
target: ScalingService
|
||||
function onScaleChanged(screenName, scale) {
|
||||
if (screenName === modelData.name) {
|
||||
scaling = scale
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shared properties between peek and dock windows
|
||||
readonly property bool autoHide: Settings.data.dock.autoHide
|
||||
readonly property int hideDelay: 500
|
||||
readonly property int showDelay: 100
|
||||
readonly property int hideAnimationDuration: Style.animationFast
|
||||
readonly property int showAnimationDuration: Style.animationFast
|
||||
readonly property int peekHeight: 1 // no scaling for peek
|
||||
readonly property int iconSize: 36 * scaling
|
||||
readonly property int floatingMargin: Settings.data.dock.floatingRatio * Style.marginL * scaling
|
||||
|
||||
// Bar detection and positioning properties
|
||||
readonly property bool hasBar: modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false
|
||||
readonly property bool barAtBottom: hasBar && Settings.data.bar.position === "bottom"
|
||||
readonly property int barHeight: Style.barHeight * scaling
|
||||
|
||||
// Shared state between windows
|
||||
property bool dockHovered: false
|
||||
property bool anyAppHovered: false
|
||||
property bool hidden: autoHide
|
||||
property bool peekHovered: false
|
||||
|
||||
// Separate property to control Loader - stays true during animations
|
||||
property bool dockLoaded: !autoHide // Start loaded if autoHide is off
|
||||
|
||||
// Timer to unload dock after hide animation completes
|
||||
Timer {
|
||||
id: unloadTimer
|
||||
interval: hideAnimationDuration + 50 // Add small buffer
|
||||
onTriggered: {
|
||||
if (hidden && autoHide) {
|
||||
dockLoaded = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Timer for auto-hide delay
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: hideDelay
|
||||
onTriggered: {
|
||||
if (autoHide && !dockHovered && !anyAppHovered && !peekHovered) {
|
||||
hidden = true
|
||||
unloadTimer.restart() // Start unload timer when hiding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Timer for show delay
|
||||
Timer {
|
||||
id: showTimer
|
||||
interval: showDelay
|
||||
onTriggered: {
|
||||
if (autoHide) {
|
||||
dockLoaded = true // Load dock immediately
|
||||
hidden = false // Then trigger show animation
|
||||
unloadTimer.stop() // Cancel any pending unload
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for autoHide setting changes
|
||||
onAutoHideChanged: {
|
||||
if (!autoHide) {
|
||||
hidden = false
|
||||
dockLoaded = true
|
||||
hideTimer.stop()
|
||||
showTimer.stop()
|
||||
unloadTimer.stop()
|
||||
} else {
|
||||
hidden = true
|
||||
unloadTimer.restart() // Schedule unload after animation
|
||||
}
|
||||
}
|
||||
|
||||
// PEEK WINDOW - Always visible when auto-hide is enabled
|
||||
Loader {
|
||||
active: Settings.isLoaded && modelData && Settings.data.dock.monitors.includes(modelData.name) && autoHide
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
id: peekWindow
|
||||
|
||||
required property ShellScreen modelData
|
||||
readonly property real scaling: ScalingService.scale(screen)
|
||||
screen: modelData
|
||||
|
||||
// Auto-hide properties - make reactive to settings changes
|
||||
property bool autoHide: Settings.data.dock.autoHide || (Settings.data.bar.position === "bottom")
|
||||
property bool hidden: autoHide
|
||||
property int hideDelay: 500
|
||||
property int showDelay: 100
|
||||
property int hideAnimationDuration: Style.animationFast
|
||||
property int showAnimationDuration: Style.animationFast
|
||||
property int peekHeight: 2
|
||||
property int fullHeight: dockContainer.height
|
||||
property int iconSize: 36
|
||||
|
||||
// Track hover state
|
||||
property bool dockHovered: false
|
||||
property bool anyAppHovered: false
|
||||
|
||||
// Dock is only shown if explicitely toggled
|
||||
visible: modelData ? Settings.data.dock.monitors.includes(modelData.name) : false
|
||||
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
anchors.bottom: true
|
||||
anchors.left: true
|
||||
anchors.right: true
|
||||
focusable: false
|
||||
color: Color.transparent
|
||||
implicitHeight: iconSize * 1.4 * scaling
|
||||
|
||||
// Watch for autoHide setting changes
|
||||
onAutoHideChanged: {
|
||||
if (!autoHide) {
|
||||
// If auto-hide is disabled, show the dock
|
||||
hidden = false
|
||||
hideTimer.stop()
|
||||
showTimer.stop()
|
||||
} else {
|
||||
// If auto-hide is enabled, start hidden
|
||||
hidden = true
|
||||
}
|
||||
}
|
||||
WlrLayershell.namespace: "noctalia-dock-peek"
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Auto // Always exclusive
|
||||
|
||||
// Timer for auto-hide delay
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: hideDelay
|
||||
onTriggered: {
|
||||
if (autoHide && !dockHovered && !anyAppHovered) {
|
||||
hidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
implicitHeight: peekHeight
|
||||
|
||||
// Timer for show delay
|
||||
Timer {
|
||||
id: showTimer
|
||||
interval: showDelay
|
||||
onTriggered: hidden = false
|
||||
}
|
||||
|
||||
// Behavior for smooth hide/show animations
|
||||
Behavior on margins.bottom {
|
||||
NumberAnimation {
|
||||
duration: hidden ? hideAnimationDuration : showAnimationDuration
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: barAtBottom ? Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity) : Color.transparent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: screenEdgeMouseArea
|
||||
x: 0
|
||||
y: modelData && modelData.geometry ? modelData.geometry.height - (fullHeight + 10 * scaling) : 0
|
||||
width: screen.width
|
||||
height: fullHeight + 10 * scaling
|
||||
id: peekArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
propagateComposedEvents: true
|
||||
|
||||
onEntered: {
|
||||
if (autoHide && hidden) {
|
||||
peekHovered = true
|
||||
if (hidden) {
|
||||
showTimer.start()
|
||||
}
|
||||
}
|
||||
|
||||
onExited: {
|
||||
if (autoHide && !hidden && !dockHovered && !anyAppHovered) {
|
||||
hideTimer.start()
|
||||
peekHovered = false
|
||||
if (!hidden && !dockHovered && !anyAppHovered) {
|
||||
hideTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
margins.bottom: hidden ? -(fullHeight - peekHeight) : 0
|
||||
// DOCK WINDOW
|
||||
Loader {
|
||||
active: Settings.isLoaded && modelData && Settings.data.dock.monitors.includes(modelData.name) && dockLoaded && ToplevelManager && (ToplevelManager.toplevels.values.length > 0)
|
||||
|
||||
Rectangle {
|
||||
id: dockContainer
|
||||
width: dock.width + 48 * scaling
|
||||
height: iconSize * 1.4 * scaling
|
||||
color: Color.mSurface
|
||||
sourceComponent: PanelWindow {
|
||||
id: dockWindow
|
||||
|
||||
screen: modelData
|
||||
|
||||
focusable: false
|
||||
color: Color.transparent
|
||||
|
||||
WlrLayershell.namespace: "noctalia-dock-main"
|
||||
WlrLayershell.exclusionMode: Settings.data.dock.exclusive ? ExclusionMode.Auto : ExclusionMode.Ignore
|
||||
|
||||
// Size to fit the dock container exactly
|
||||
implicitWidth: dockContainerWrapper.width
|
||||
implicitHeight: dockContainerWrapper.height
|
||||
|
||||
// Position above the bar if it's at bottom
|
||||
anchors.bottom: true
|
||||
|
||||
margins.bottom: {
|
||||
switch (Settings.data.bar.position) {
|
||||
case "bottom":
|
||||
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling + floatingMargin : floatingMargin)
|
||||
default:
|
||||
return floatingMargin
|
||||
}
|
||||
}
|
||||
|
||||
// Rectangle {
|
||||
// anchors.fill: parent
|
||||
// color: "#000FF0"
|
||||
// z: -1
|
||||
// }
|
||||
|
||||
// Wrapper item for scale/opacity animations
|
||||
Item {
|
||||
id: dockContainerWrapper
|
||||
width: dockContainer.width
|
||||
height: dockContainer.height
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
topLeftRadius: Style.radiusL * scaling
|
||||
topRightRadius: Style.radiusL * scaling
|
||||
|
||||
MouseArea {
|
||||
id: dockMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
propagateComposedEvents: true
|
||||
// Apply animations to this wrapper
|
||||
opacity: hidden ? 0 : 1
|
||||
scale: hidden ? 0.85 : 1
|
||||
|
||||
onEntered: {
|
||||
dockHovered = true
|
||||
if (autoHide) {
|
||||
showTimer.stop()
|
||||
hideTimer.stop()
|
||||
hidden = false
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
dockHovered = false
|
||||
// Only start hide timer if we're not hovering over any app
|
||||
if (autoHide && !anyAppHovered) {
|
||||
hideTimer.start()
|
||||
}
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: hidden ? hideAnimationDuration : showAnimationDuration
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: dock
|
||||
width: runningAppsRow.width
|
||||
height: parent.height - (20 * scaling)
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: hidden ? hideAnimationDuration : showAnimationDuration
|
||||
easing.type: hidden ? Easing.InQuad : Easing.OutBack
|
||||
easing.overshoot: hidden ? 0 : 1.05
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: dockContainer
|
||||
width: dockLayout.implicitWidth + Style.marginM * scaling * 2
|
||||
height: Math.round(iconSize * 1.5)
|
||||
color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity)
|
||||
anchors.centerIn: parent
|
||||
radius: Style.radiusL * scaling
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
border.color: Qt.alpha(Color.mOutline, Settings.data.dock.backgroundOpacity)
|
||||
|
||||
NTooltip {
|
||||
id: appTooltip
|
||||
visible: false
|
||||
positionAbove: true
|
||||
MouseArea {
|
||||
id: dockMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onEntered: {
|
||||
dockHovered = true
|
||||
if (autoHide) {
|
||||
showTimer.stop()
|
||||
hideTimer.stop()
|
||||
unloadTimer.stop() // Cancel unload if hovering
|
||||
}
|
||||
}
|
||||
|
||||
onExited: {
|
||||
dockHovered = false
|
||||
if (autoHide && !anyAppHovered && !peekHovered) {
|
||||
hideTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getAppIcon(toplevel: Toplevel): string {
|
||||
if (!toplevel)
|
||||
return ""
|
||||
return Icons.iconForAppId(toplevel.appId?.toLowerCase())
|
||||
}
|
||||
|
||||
Row {
|
||||
id: runningAppsRow
|
||||
spacing: Style.marginL * scaling
|
||||
height: parent.height
|
||||
Item {
|
||||
id: dock
|
||||
width: dockLayout.implicitWidth
|
||||
height: parent.height - (Style.marginM * 2 * scaling)
|
||||
anchors.centerIn: parent
|
||||
|
||||
Repeater {
|
||||
model: ToplevelManager ? ToplevelManager.toplevels : null
|
||||
function getAppIcon(toplevel: Toplevel): string {
|
||||
if (!toplevel)
|
||||
return ""
|
||||
return AppIcons.iconForAppId(toplevel.appId?.toLowerCase())
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
id: appButton
|
||||
width: iconSize * scaling
|
||||
height: iconSize * scaling
|
||||
color: Color.transparent
|
||||
radius: Style.radiusM * scaling
|
||||
RowLayout {
|
||||
id: dockLayout
|
||||
spacing: Style.marginM * scaling
|
||||
Layout.preferredHeight: parent.height
|
||||
anchors.centerIn: parent
|
||||
|
||||
property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData
|
||||
property bool hovered: appMouseArea.containsMouse
|
||||
property string appId: modelData ? modelData.appId : ""
|
||||
property string appTitle: modelData ? modelData.title : ""
|
||||
Repeater {
|
||||
model: ToplevelManager ? ToplevelManager.toplevels : null
|
||||
|
||||
// Hover background
|
||||
Rectangle {
|
||||
id: hoverBackground
|
||||
anchors.fill: parent
|
||||
color: appButton.hovered ? Color.mSurfaceVariant : Color.transparent
|
||||
radius: parent.radius
|
||||
opacity: appButton.hovered ? 0.8 : 0
|
||||
delegate: Item {
|
||||
id: appButton
|
||||
Layout.preferredWidth: iconSize
|
||||
Layout.preferredHeight: iconSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData
|
||||
property bool hovered: appMouseArea.containsMouse
|
||||
property string appId: modelData ? modelData.appId : ""
|
||||
property string appTitle: modelData ? modelData.title : ""
|
||||
|
||||
// Individual tooltip for this app
|
||||
NTooltip {
|
||||
id: appTooltip
|
||||
target: appButton
|
||||
positionAbove: true
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
|
||||
// The icon
|
||||
Image {
|
||||
id: appIcon
|
||||
width: iconSize * scaling
|
||||
height: iconSize * scaling
|
||||
anchors.centerIn: parent
|
||||
source: dock.getAppIcon(modelData)
|
||||
visible: source.toString() !== ""
|
||||
smooth: true
|
||||
mipmap: false
|
||||
antialiasing: false
|
||||
fillMode: Image.PreserveAspectFit
|
||||
Image {
|
||||
id: appIcon
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
anchors.centerIn: parent
|
||||
source: dock.getAppIcon(modelData)
|
||||
visible: source.toString() !== ""
|
||||
sourceSize.width: iconSize * 2
|
||||
sourceSize.height: iconSize * 2
|
||||
smooth: true
|
||||
mipmap: true
|
||||
antialiasing: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
cache: true
|
||||
|
||||
scale: appButton.hovered ? 1.1 : 1.0
|
||||
scale: appButton.hovered ? 1.15 : 1.0
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back if no icon
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
visible: !appIcon.visible
|
||||
text: "question_mark"
|
||||
font.family: "Material Symbols Rounded"
|
||||
font.pointSize: iconSize * 0.7 * scaling
|
||||
color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
|
||||
scale: appButton.hovered ? 1.1 : 1.0
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: appMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
|
||||
|
||||
onEntered: {
|
||||
anyAppHovered = true
|
||||
const appName = appButton.appTitle || appButton.appId || "Unknown"
|
||||
appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName
|
||||
appTooltip.target = appButton
|
||||
appTooltip.isVisible = true
|
||||
if (autoHide) {
|
||||
showTimer.stop()
|
||||
hideTimer.stop()
|
||||
hidden = false
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
easing.overshoot: 1.2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: {
|
||||
anyAppHovered = false
|
||||
appTooltip.hide()
|
||||
// Only start hide timer if we're not hovering over the dock
|
||||
if (autoHide && !dockHovered) {
|
||||
hideTimer.start()
|
||||
// Fall back if no icon
|
||||
NIcon {
|
||||
anchors.centerIn: parent
|
||||
visible: !appIcon.visible
|
||||
icon: "question-mark"
|
||||
font.pointSize: iconSize * 0.7
|
||||
color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
scale: appButton.hovered ? 1.15 : 1.0
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutBack
|
||||
easing.overshoot: 1.2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: function (mouse) {
|
||||
if (mouse.button === Qt.MiddleButton && modelData?.close) {
|
||||
modelData.close()
|
||||
MouseArea {
|
||||
id: appMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
|
||||
|
||||
onEntered: {
|
||||
anyAppHovered = true
|
||||
const appName = appButton.appTitle || appButton.appId || "Unknown"
|
||||
appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName
|
||||
appTooltip.isVisible = true
|
||||
if (autoHide) {
|
||||
showTimer.stop()
|
||||
hideTimer.stop()
|
||||
unloadTimer.stop() // Cancel unload if hovering app
|
||||
}
|
||||
}
|
||||
if (mouse.button === Qt.LeftButton && modelData?.activate) {
|
||||
modelData.activate()
|
||||
|
||||
onExited: {
|
||||
anyAppHovered = false
|
||||
appTooltip.hide()
|
||||
if (autoHide && !dockHovered && !peekHovered) {
|
||||
hideTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: function (mouse) {
|
||||
if (mouse.button === Qt.MiddleButton && modelData?.close) {
|
||||
modelData.close()
|
||||
}
|
||||
if (mouse.button === Qt.LeftButton && modelData?.activate) {
|
||||
modelData.activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: isActive
|
||||
width: iconSize * 0.75
|
||||
height: 4 * scaling
|
||||
color: Color.mPrimary
|
||||
radius: Style.radiusXS
|
||||
anchors.top: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.topMargin: Style.marginXXS * scaling
|
||||
// Active indicator
|
||||
Rectangle {
|
||||
visible: isActive
|
||||
width: iconSize * 0.2
|
||||
height: iconSize * 0.1
|
||||
color: Color.mPrimary
|
||||
radius: Style.radiusXS * scaling
|
||||
anchors.top: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
// Pulse animation for active indicator
|
||||
SequentialAnimation on opacity {
|
||||
running: isActive
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
to: 0.6
|
||||
duration: Style.animationSlowest
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 1.0
|
||||
duration: Style.animationSlowest
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,36 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
IpcHandler {
|
||||
target: "screenRecorder"
|
||||
function toggle() {
|
||||
if (ScreenRecorderService.isAvailable) {
|
||||
ScreenRecorderService.toggleRecording()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "settings"
|
||||
function toggle() {
|
||||
settingsPanel.toggle(Quickshell.screens[0])
|
||||
settingsPanel.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "notifications"
|
||||
function toggleHistory() {
|
||||
notificationHistoryPanel.toggle(Quickshell.screens[0])
|
||||
notificationHistoryPanel.toggle()
|
||||
}
|
||||
function toggleDoNotDisturb() {// TODO
|
||||
function toggleDND() {
|
||||
Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,45 +41,18 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "appLauncher"
|
||||
function toggle() {
|
||||
launcherPanel.toggle(Quickshell.screens[0])
|
||||
}
|
||||
function clipboard() {
|
||||
launcherPanel.toggle(Quickshell.screens[0])
|
||||
// Use the setSearchText function to set clipboard mode
|
||||
Qt.callLater(() => {
|
||||
launcherPanel.setSearchText(">clip ")
|
||||
})
|
||||
}
|
||||
function calculator() {
|
||||
launcherPanel.toggle(Quickshell.screens[0])
|
||||
// Use the setSearchText function to set calculator mode
|
||||
Qt.callLater(() => {
|
||||
launcherPanel.setSearchText(">calc ")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "launcher"
|
||||
function toggle() {
|
||||
launcherPanel.toggle(Quickshell.screens[0])
|
||||
launcherPanel.toggle()
|
||||
}
|
||||
function clipboard() {
|
||||
launcherPanel.toggle(Quickshell.screens[0])
|
||||
// Use the setSearchText function to set clipboard mode
|
||||
Qt.callLater(() => {
|
||||
launcherPanel.setSearchText(">clip ")
|
||||
})
|
||||
launcherPanel.setSearchText(">clip ")
|
||||
launcherPanel.toggle()
|
||||
}
|
||||
function calculator() {
|
||||
launcherPanel.toggle(Quickshell.screens[0])
|
||||
// Use the setSearchText function to set calculator mode
|
||||
Qt.callLater(() => {
|
||||
launcherPanel.setSearchText(">calc ")
|
||||
})
|
||||
launcherPanel.setSearchText(">calc ")
|
||||
launcherPanel.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,17 +77,65 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "darkMode"
|
||||
function toggle() {
|
||||
Settings.data.colorSchemes.darkMode = !Settings.data.colorSchemes.darkMode
|
||||
}
|
||||
function setDark() {
|
||||
Settings.data.colorSchemes.darkMode = true
|
||||
}
|
||||
function setLight() {
|
||||
Settings.data.colorSchemes.darkMode = false
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "volume"
|
||||
function increase() {
|
||||
AudioService.increaseVolume()
|
||||
}
|
||||
function decrease() {
|
||||
AudioService.decreaseVolume()
|
||||
}
|
||||
function muteOutput() {
|
||||
AudioService.setOutputMuted(!AudioService.muted)
|
||||
}
|
||||
function muteInput() {
|
||||
if (AudioService.source?.ready && AudioService.source?.audio) {
|
||||
AudioService.source.audio.muted = !AudioService.source.audio.muted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "powerPanel"
|
||||
function toggle() {
|
||||
powerPanel.toggle(Quickshell.screens[0])
|
||||
powerPanel.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "sidePanel"
|
||||
function toggle() {
|
||||
sidePanel.toggle(Quickshell.screens[0])
|
||||
sidePanel.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
// Wallpaper IPC: trigger a new random wallpaper
|
||||
IpcHandler {
|
||||
target: "wallpaper"
|
||||
function random() {
|
||||
if (Settings.data.wallpaper.enabled) {
|
||||
WallpaperService.setRandomWallpaper()
|
||||
}
|
||||
}
|
||||
|
||||
function set(path: string, screen: string) {
|
||||
if (screen === "all" || screen === "") {
|
||||
screen = undefined
|
||||
}
|
||||
WallpaperService.changeWallpaper(path, screen)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
|
||||
import "../../Helpers/AdvancedMath.js" as AdvancedMath
|
||||
|
||||
QtObject {
|
||||
id: calculator
|
||||
|
||||
// Function to evaluate mathematical expressions
|
||||
function evaluate(expression) {
|
||||
if (!expression || expression.trim() === "") {
|
||||
return {
|
||||
"isValid": false,
|
||||
"result": "",
|
||||
"displayResult": "",
|
||||
"error": "Empty expression"
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Try advanced math first
|
||||
if (typeof AdvancedMath !== 'undefined') {
|
||||
const result = AdvancedMath.evaluate(expression.trim())
|
||||
const displayResult = AdvancedMath.formatResult(result)
|
||||
|
||||
return {
|
||||
"isValid": true,
|
||||
"result": result,
|
||||
"displayResult": displayResult,
|
||||
"expression": expression,
|
||||
"error": ""
|
||||
}
|
||||
} else {
|
||||
// Fallback to basic evaluation
|
||||
Logger.warn("Calculator", "AdvancedMath not available, using basic eval")
|
||||
|
||||
// Basic preprocessing for common functions
|
||||
var processed = expression.trim(
|
||||
).replace(/\bpi\b/gi,
|
||||
Math.PI).replace(/\be\b/gi,
|
||||
Math.E).replace(/\bsqrt\s*\(/g,
|
||||
'Math.sqrt(').replace(/\bsin\s*\(/g,
|
||||
'Math.sin(').replace(/\bcos\s*\(/g,
|
||||
'Math.cos(').replace(/\btan\s*\(/g, 'Math.tan(').replace(/\blog\s*\(/g, 'Math.log10(').replace(/\bln\s*\(/g, 'Math.log(').replace(/\bexp\s*\(/g, 'Math.exp(').replace(/\bpow\s*\(/g, 'Math.pow(').replace(/\babs\s*\(/g, 'Math.abs(')
|
||||
|
||||
// Sanitize and evaluate
|
||||
if (!/^[0-9+\-*/().\s\w,]+$/.test(processed)) {
|
||||
throw new Error("Invalid characters in expression")
|
||||
}
|
||||
|
||||
const result = eval(processed)
|
||||
|
||||
if (!isFinite(result) || isNaN(result)) {
|
||||
throw new Error("Invalid result")
|
||||
}
|
||||
|
||||
const displayResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/, '')
|
||||
|
||||
return {
|
||||
"isValid": true,
|
||||
"result": result,
|
||||
"displayResult": displayResult,
|
||||
"expression": expression,
|
||||
"error": ""
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
"isValid": false,
|
||||
"result": "",
|
||||
"displayResult": "",
|
||||
"error": error.message || error.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate calculator entry for display
|
||||
function createEntry(expression, searchContext = "") {
|
||||
const evaluation = evaluate(expression)
|
||||
|
||||
if (!evaluation.isValid) {
|
||||
return {
|
||||
"isCalculator": true,
|
||||
"name": "Invalid expression",
|
||||
"content": evaluation.error,
|
||||
"icon": "error",
|
||||
"execute": function () {// Do nothing for invalid expressions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const displayName = searchContext
|
||||
=== "calc" ? `${expression} = ${evaluation.displayResult}` : `${expression} = ${evaluation.displayResult}`
|
||||
|
||||
return {
|
||||
"isCalculator": true,
|
||||
"name": displayName,
|
||||
"result": evaluation.result,
|
||||
"expr": expression,
|
||||
"displayResult": evaluation.displayResult,
|
||||
"icon": "calculate",
|
||||
"execute": function () {
|
||||
Quickshell.clipboardText = evaluation.displayResult
|
||||
// Also copy using shell command for better compatibility
|
||||
Quickshell.execDetached(
|
||||
["sh", "-lc", `printf %s ${evaluation.displayResult} | wl-copy -t text/plain;charset=utf-8`])
|
||||
Quickshell.execDetached(
|
||||
["notify-send", "Calculator", `${expression} = ${evaluation.displayResult} (copied to clipboard)`])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create placeholder entry for empty calculator mode
|
||||
function createPlaceholderEntry() {
|
||||
return {
|
||||
"isCalculator": true,
|
||||
"name": "Calculator",
|
||||
"content": "Try: sqrt(16), sin(1), cos(0), pi*2, exp(1), pow(2,8), abs(-5)",
|
||||
"icon": "calculate",
|
||||
"execute": function () {// Do nothing for placeholder
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process calculator queries
|
||||
function processQuery(query, searchContext = "") {
|
||||
const results = []
|
||||
|
||||
if (searchContext === "calc") {
|
||||
// Handle ">calc" mode
|
||||
const expr = query.slice(5).trim()
|
||||
if (expr && expr !== "") {
|
||||
results.push(createEntry(expr, "calc"))
|
||||
} else {
|
||||
results.push(createPlaceholderEntry())
|
||||
}
|
||||
} else if (query.startsWith(">") && query.length > 1 && !query.startsWith(">clip") && !query.startsWith(">calc")) {
|
||||
// Handle direct math expressions after ">"
|
||||
const mathExpr = query.slice(1).trim()
|
||||
const evaluation = evaluate(mathExpr)
|
||||
|
||||
if (evaluation.isValid) {
|
||||
results.push(createEntry(mathExpr, "direct"))
|
||||
}
|
||||
// If invalid, don't add anything - let it fall through to regular search
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
|
||||
QtObject {
|
||||
id: clipboardHistory
|
||||
|
||||
function parseImageMeta(preview) {
|
||||
const re = /\[\[\s*binary data\s+([\d\.]+\s*(?:KiB|MiB|GiB|B))\s+(\w+)\s+(\d+)x(\d+)\s*\]\]/i
|
||||
const m = (preview || "").match(re)
|
||||
if (!m)
|
||||
return null
|
||||
return {
|
||||
"size": m[1],
|
||||
"fmt": (m[2] || "").toUpperCase(),
|
||||
"w": Number(m[3]),
|
||||
"h": Number(m[4])
|
||||
}
|
||||
}
|
||||
|
||||
function formatTextPreview(preview) {
|
||||
const normalized = (preview || "").replace(/\s+/g, ' ').trim()
|
||||
const lines = normalized.split(/\n+/)
|
||||
const title = (lines[0] || "Text").slice(0, 60)
|
||||
let subtitle = ""
|
||||
if (lines.length > 1) {
|
||||
subtitle = lines[1].slice(0, 80)
|
||||
} else {
|
||||
subtitle = `${normalized.length} chars`
|
||||
}
|
||||
return {
|
||||
"title": title,
|
||||
"subtitle": subtitle
|
||||
}
|
||||
}
|
||||
|
||||
function createClipboardEntry(item) {
|
||||
if (item.isImage) {
|
||||
const meta = parseImageMeta(item.preview)
|
||||
const title = meta ? `Image ${meta.w}×${meta.h}` : "Image"
|
||||
const subtitle = meta ? `${meta.size} · ${meta.fmt}` : (item.preview || "")
|
||||
return {
|
||||
"isClipboard": true,
|
||||
"name": title,
|
||||
"content": subtitle,
|
||||
"icon": "image",
|
||||
"type": 'image',
|
||||
"id": item.id,
|
||||
"mime": item.mime
|
||||
}
|
||||
} else {
|
||||
const parts = formatTextPreview(item.preview)
|
||||
return {
|
||||
"isClipboard": true,
|
||||
"name": parts.title,
|
||||
"content": parts.subtitle,
|
||||
"icon": "content_paste",
|
||||
"type": 'text',
|
||||
"id": item.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createEmptyEntry() {
|
||||
return {
|
||||
"isClipboard": true,
|
||||
"name": "No clipboard history",
|
||||
"content": "No matching clipboard entries found",
|
||||
"icon": "content_paste_off",
|
||||
"execute": function () {}
|
||||
}
|
||||
}
|
||||
|
||||
function processQuery(query, items) {
|
||||
const results = []
|
||||
if (!query.startsWith(">clip")) {
|
||||
return results
|
||||
}
|
||||
|
||||
const searchTerm = query.slice(5).trim().toLowerCase()
|
||||
|
||||
// Dependency hook without side effects
|
||||
const _rev = CliphistService.revision
|
||||
const source = items || CliphistService.items
|
||||
|
||||
source.forEach(function (item) {
|
||||
const hay = (item.preview || "").toLowerCase()
|
||||
if (!searchTerm || hay.indexOf(searchTerm) !== -1) {
|
||||
const entry = createClipboardEntry(item)
|
||||
// Attach execute at this level to avoid duplicating functions
|
||||
entry.execute = function () {
|
||||
CliphistService.copyToClipboard(item.id)
|
||||
}
|
||||
results.push(entry)
|
||||
}
|
||||
})
|
||||
|
||||
if (results.length === 0) {
|
||||
results.push(createEmptyEntry())
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
CliphistService.list(100)
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
CliphistService.wipeAll()
|
||||
}
|
||||
}
|
||||
@@ -1,406 +1,334 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
import "../../Helpers/FuzzySort.js" as Fuzzysort
|
||||
|
||||
NPanel {
|
||||
id: root
|
||||
panelWidth: Math.min(700 * scaling, screen?.width * 0.75)
|
||||
panelHeight: Math.min(550 * scaling, screen?.height * 0.8)
|
||||
// Positioning derives from Settings.data.bar.position for vertical (top/bottom)
|
||||
// and from Settings.data.appLauncher.position for horizontal vs center.
|
||||
// Options: center, top_left, top_right, bottom_left, bottom_right
|
||||
|
||||
// Panel configuration
|
||||
preferredWidth: 500
|
||||
preferredWidthRatio: 0.3
|
||||
preferredHeight: 600
|
||||
preferredHeightRatio: 0.5
|
||||
|
||||
panelKeyboardFocus: true
|
||||
panelBackgroundColor: Qt.alpha(Color.mSurface, Settings.data.appLauncher.backgroundOpacity)
|
||||
|
||||
// Positioning
|
||||
readonly property string launcherPosition: Settings.data.appLauncher.position
|
||||
panelAnchorCentered: launcherPosition === "center"
|
||||
panelAnchorLeft: launcherPosition !== "center" && (launcherPosition.endsWith("_left"))
|
||||
panelAnchorRight: launcherPosition !== "center" && (launcherPosition.endsWith("_right"))
|
||||
panelAnchorHorizontalCenter: launcherPosition === "center" || launcherPosition.endsWith("_center")
|
||||
panelAnchorVerticalCenter: launcherPosition === "center"
|
||||
panelAnchorLeft: launcherPosition !== "center" && launcherPosition.endsWith("_left")
|
||||
panelAnchorRight: launcherPosition !== "center" && launcherPosition.endsWith("_right")
|
||||
panelAnchorBottom: launcherPosition.startsWith("bottom_")
|
||||
panelAnchorTop: launcherPosition.startsWith("top_")
|
||||
|
||||
// Properties
|
||||
// Core state
|
||||
property string searchText: ""
|
||||
property bool shouldResetCursor: false
|
||||
property int selectedIndex: 0
|
||||
property var results: []
|
||||
property var plugins: []
|
||||
property var activePlugin: null
|
||||
|
||||
// Add function to set search text programmatically
|
||||
readonly property int badgeSize: Math.round(Style.baseWidgetSize * 1.6 * scaling)
|
||||
readonly property int entryHeight: Math.round(badgeSize + Style.marginM * 2 * scaling)
|
||||
|
||||
// Public API for plugins
|
||||
function setSearchText(text) {
|
||||
searchText = text
|
||||
// The searchInput will automatically update via the text binding
|
||||
// Focus and cursor position will be handled by the TextField's Component.onCompleted
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
// Reset state when panel opens to avoid sticky modes
|
||||
if (searchText === "") {
|
||||
searchText = ""
|
||||
selectedIndex = 0
|
||||
// Plugin registration
|
||||
function registerPlugin(plugin) {
|
||||
plugins.push(plugin)
|
||||
plugin.launcher = root
|
||||
if (plugin.init)
|
||||
plugin.init()
|
||||
}
|
||||
|
||||
// Search handling
|
||||
function updateResults() {
|
||||
results = []
|
||||
activePlugin = null
|
||||
|
||||
// Check for command mode
|
||||
if (searchText.startsWith(">")) {
|
||||
// Find plugin that handles this command
|
||||
for (let plugin of plugins) {
|
||||
if (plugin.handleCommand && plugin.handleCommand(searchText)) {
|
||||
activePlugin = plugin
|
||||
results = plugin.getResults(searchText)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Show available commands if just ">"
|
||||
if (searchText === ">" && !activePlugin) {
|
||||
for (let plugin of plugins) {
|
||||
if (plugin.commands) {
|
||||
results = results.concat(plugin.commands())
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Regular search - let plugins contribute results
|
||||
for (let plugin of plugins) {
|
||||
if (plugin.handleSearch) {
|
||||
const pluginResults = plugin.getResults(searchText)
|
||||
results = results.concat(pluginResults)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectedIndex = 0
|
||||
}
|
||||
|
||||
onSearchTextChanged: updateResults()
|
||||
|
||||
// Lifecycle
|
||||
onOpened: {
|
||||
// Notify plugins
|
||||
for (let plugin of plugins) {
|
||||
if (plugin.onOpened)
|
||||
plugin.onOpened()
|
||||
}
|
||||
updateResults()
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
// Reset search bar when launcher is closed
|
||||
// Reset search text
|
||||
searchText = ""
|
||||
selectedIndex = 0
|
||||
shouldResetCursor = true
|
||||
}
|
||||
|
||||
// Import modular components
|
||||
Calculator {
|
||||
id: calculator
|
||||
}
|
||||
|
||||
ClipboardHistory {
|
||||
id: clipboardHistory
|
||||
}
|
||||
|
||||
// Poll cliphist while in clipboard mode to keep entries fresh
|
||||
Timer {
|
||||
id: clipRefreshTimer
|
||||
interval: 2000
|
||||
repeat: true
|
||||
running: Settings.data.appLauncher.enableClipboardHistory && searchText.startsWith(">clip")
|
||||
onTriggered: clipboardHistory.refresh()
|
||||
}
|
||||
|
||||
// Properties
|
||||
property var desktopEntries: DesktopEntries.applications.values
|
||||
property int selectedIndex: 0
|
||||
|
||||
// Refresh clipboard when user starts typing clipboard commands
|
||||
onSearchTextChanged: {
|
||||
if (Settings.data.appLauncher.enableClipboardHistory && searchText.startsWith(">clip")) {
|
||||
clipboardHistory.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
// Main filtering logic
|
||||
property var filteredEntries: {
|
||||
// Explicit dependency so changes to items/decoded images retrigger this binding
|
||||
const _clipItems = Settings.data.appLauncher.enableClipboardHistory ? CliphistService.items : []
|
||||
const _clipRev = Settings.data.appLauncher.enableClipboardHistory ? CliphistService.revision : 0
|
||||
|
||||
var query = searchText ? searchText.toLowerCase() : ""
|
||||
if (Settings.data.appLauncher.enableClipboardHistory && query.startsWith(">clip")) {
|
||||
return clipboardHistory.processQuery(query, _clipItems)
|
||||
}
|
||||
|
||||
if (!desktopEntries || desktopEntries.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Filter out entries that shouldn't be displayed
|
||||
var visibleEntries = desktopEntries.filter(entry => {
|
||||
if (!entry || entry.noDisplay) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
var results = []
|
||||
|
||||
// Handle special commands
|
||||
if (query === ">") {
|
||||
results.push({
|
||||
"isCommand": true,
|
||||
"name": ">calc",
|
||||
"content": "Calculator - evaluate mathematical expressions",
|
||||
"icon": "calculate",
|
||||
"execute": executeCalcCommand
|
||||
})
|
||||
if (Settings.data.appLauncher.enableClipboardHistory) {
|
||||
results.push({
|
||||
"isCommand": true,
|
||||
"name": ">clip",
|
||||
"content": "Clipboard history - browse and restore clipboard items",
|
||||
"icon": "content_paste",
|
||||
"execute": executeClipCommand
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// Handle calculator
|
||||
if (query.startsWith(">calc")) {
|
||||
return calculator.processQuery(query, "calc")
|
||||
}
|
||||
|
||||
// Handle direct math expressions after ">"
|
||||
if (query.startsWith(">") && query.length > 1 && (!Settings.data.appLauncher.enableClipboardHistory
|
||||
|| !query.startsWith(">clip")) && !query.startsWith(">calc")) {
|
||||
const mathResults = calculator.processQuery(query, "direct")
|
||||
if (mathResults.length > 0) {
|
||||
return mathResults
|
||||
}
|
||||
// If math evaluation fails, fall through to regular search
|
||||
}
|
||||
|
||||
// Regular app search
|
||||
if (!query) {
|
||||
results = results.concat(visibleEntries.sort(function (a, b) {
|
||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase())
|
||||
}))
|
||||
} else {
|
||||
var fuzzyResults = Fuzzysort.go(query, visibleEntries, {
|
||||
"keys": ["name", "comment", "genericName"]
|
||||
})
|
||||
results = results.concat(fuzzyResults.map(function (r) {
|
||||
return r.obj
|
||||
}))
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// Command execution functions
|
||||
function executeCalcCommand() {
|
||||
setSearchText(">calc ")
|
||||
}
|
||||
|
||||
function executeClipCommand() {
|
||||
setSearchText(">clip ")
|
||||
}
|
||||
|
||||
// Navigation functions
|
||||
function selectNext() {
|
||||
if (filteredEntries.length > 0) {
|
||||
selectedIndex = Math.min(selectedIndex + 1, filteredEntries.length - 1)
|
||||
}
|
||||
}
|
||||
|
||||
function selectPrev() {
|
||||
if (filteredEntries.length > 0) {
|
||||
selectedIndex = Math.max(selectedIndex - 1, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function selectNextPage() {
|
||||
if (filteredEntries.length > 0) {
|
||||
const delegateHeight = 65 * scaling + (Style.marginXXS * scaling)
|
||||
const page = Math.max(1, Math.floor(appsList.height / delegateHeight))
|
||||
selectedIndex = Math.min(selectedIndex + page, filteredEntries.length - 1)
|
||||
}
|
||||
}
|
||||
function selectPrevPage() {
|
||||
if (filteredEntries.length > 0) {
|
||||
const delegateHeight = 65 * scaling + (Style.marginXXS * scaling)
|
||||
const page = Math.max(1, Math.floor(appsList.height / delegateHeight))
|
||||
selectedIndex = Math.max(selectedIndex - page, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function activateSelected() {
|
||||
if (filteredEntries.length === 0)
|
||||
return
|
||||
|
||||
var modelData = filteredEntries[selectedIndex]
|
||||
if (modelData && modelData.execute) {
|
||||
if (modelData.isCommand) {
|
||||
modelData.execute()
|
||||
return
|
||||
} else {
|
||||
modelData.execute()
|
||||
}
|
||||
root.close()
|
||||
// Notify plugins
|
||||
for (let plugin of plugins) {
|
||||
if (plugin.onClosed)
|
||||
plugin.onClosed()
|
||||
}
|
||||
}
|
||||
|
||||
// Load plugins
|
||||
Component.onCompleted: {
|
||||
Logger.log("Launcher", "Component completed")
|
||||
Logger.log("Launcher", "DesktopEntries available:", typeof DesktopEntries !== 'undefined')
|
||||
if (typeof DesktopEntries !== 'undefined') {
|
||||
Logger.log("Launcher", "DesktopEntries.entries:",
|
||||
DesktopEntries.entries ? DesktopEntries.entries.length : 'undefined')
|
||||
// Load applications plugin
|
||||
const appsPlugin = Qt.createComponent("Plugins/ApplicationsPlugin.qml").createObject(this)
|
||||
if (appsPlugin) {
|
||||
registerPlugin(appsPlugin)
|
||||
Logger.log("Launcher", "Registered: ApplicationsPlugin")
|
||||
} else {
|
||||
Logger.error("Launcher", "Failed to load ApplicationsPlugin")
|
||||
}
|
||||
// Start clipboard refresh immediately on open if enabled
|
||||
if (Settings.data.appLauncher.enableClipboardHistory) {
|
||||
clipboardHistory.refresh()
|
||||
|
||||
// Load calculator plugin
|
||||
const calcPlugin = Qt.createComponent("Plugins/CalculatorPlugin.qml").createObject(this)
|
||||
if (calcPlugin) {
|
||||
registerPlugin(calcPlugin)
|
||||
Logger.log("Launcher", "Registered: CalculatorPlugin")
|
||||
} else {
|
||||
Logger.error("Launcher", "Failed to load CalculatorPlugin")
|
||||
}
|
||||
|
||||
// Load clipboard history plugin
|
||||
const clipboardPlugin = Qt.createComponent("Plugins/ClipboardPlugin.qml").createObject(this)
|
||||
if (clipboardPlugin) {
|
||||
registerPlugin(clipboardPlugin)
|
||||
Logger.log("Launcher", "Registered: ClipboardPlugin")
|
||||
} else {
|
||||
Logger.error("Launcher", "Failed to load ClipboardPlugin")
|
||||
}
|
||||
}
|
||||
|
||||
// Main content container
|
||||
// UI
|
||||
panelContent: Rectangle {
|
||||
id: ui
|
||||
color: Color.transparent
|
||||
|
||||
// ---------------------
|
||||
// Navigation
|
||||
function selectNext() {
|
||||
if (results.length > 0) {
|
||||
// Clamp the index to not exceed the last item
|
||||
selectedIndex = Math.min(selectedIndex + 1, results.length - 1)
|
||||
}
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
if (results.length > 0) {
|
||||
// Clamp the index to not go below the first item (0)
|
||||
selectedIndex = Math.max(selectedIndex - 1, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function selectFirst() {
|
||||
selectedIndex = 0
|
||||
}
|
||||
|
||||
function selectLast() {
|
||||
if (results.length > 0) {
|
||||
selectedIndex = results.length - 1
|
||||
} else {
|
||||
selectedIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
function selectNextPage() {
|
||||
if (results.length > 0) {
|
||||
const page = Math.max(1, Math.floor(resultsList.height / entryHeight))
|
||||
selectedIndex = Math.min(selectedIndex + page, results.length - 1)
|
||||
}
|
||||
}
|
||||
function selectPreviousPage() {
|
||||
if (results.length > 0) {
|
||||
const page = Math.max(1, Math.floor(resultsList.height / entryHeight))
|
||||
selectedIndex = Math.max(selectedIndex - page, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function activate() {
|
||||
if (results.length > 0 && results[selectedIndex]) {
|
||||
const item = results[selectedIndex]
|
||||
if (item.onActivate) {
|
||||
item.onActivate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+K"
|
||||
onActivated: ui.selectPrevious()
|
||||
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+J"
|
||||
onActivated: ui.selectNext()
|
||||
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "PgDown" // or "PageDown"
|
||||
onActivated: ui.selectNextPage()
|
||||
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "PgUp" // or "PageUp"
|
||||
onActivated: ui.selectPreviousPage()
|
||||
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Home"
|
||||
onActivated: ui.selectFirst()
|
||||
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "End"
|
||||
onActivated: ui.selectLast()
|
||||
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Search bar
|
||||
Rectangle {
|
||||
NTextInput {
|
||||
id: searchInput
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.barHeight * scaling
|
||||
Layout.bottomMargin: Style.marginM * scaling
|
||||
radius: Style.radiusM * scaling
|
||||
color: Color.mSurface
|
||||
border.color: searchInput.activeFocus ? Color.mSecondary : Color.mOutline
|
||||
border.width: Math.max(1, searchInput.activeFocus ? Style.borderM * scaling : Style.borderS * scaling)
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM * scaling
|
||||
fontSize: Style.fontSizeL * scaling
|
||||
fontWeight: Style.fontWeightSemiBold
|
||||
|
||||
NIcon {
|
||||
id: searchIcon
|
||||
text: "search"
|
||||
font.pointSize: Style.fontSizeXL * scaling
|
||||
color: searchInput.activeFocus ? Color.mPrimary : Color.mOnSurface
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
text: searchText
|
||||
placeholderText: "Search entries... or use > for commands"
|
||||
|
||||
TextField {
|
||||
id: searchInput
|
||||
placeholderText: searchText === "" ? "Search applications... (use > to view commands)" : "Search applications..."
|
||||
color: Color.mOnSurface
|
||||
placeholderTextColor: Color.mOnSurfaceVariant
|
||||
background: null
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
anchors.left: searchIcon.right
|
||||
anchors.leftMargin: Style.marginS * scaling
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: searchText
|
||||
onTextChanged: {
|
||||
// Update the parent searchText property
|
||||
if (searchText !== text) {
|
||||
searchText = text
|
||||
onTextChanged: searchText = text
|
||||
|
||||
Component.onCompleted: {
|
||||
if (searchInput.inputItem && searchInput.inputItem.visible) {
|
||||
searchInput.inputItem.forceActiveFocus()
|
||||
|
||||
// Override the TextField's default Home/End behavior
|
||||
searchInput.inputItem.Keys.priority = Keys.BeforeItem
|
||||
searchInput.inputItem.Keys.onPressed.connect(function (event) {
|
||||
// Intercept Home and End BEFORE the TextField handles them
|
||||
if (event.key === Qt.Key_Home) {
|
||||
ui.selectFirst()
|
||||
event.accepted = true
|
||||
return
|
||||
} else if (event.key === Qt.Key_End) {
|
||||
ui.selectLast()
|
||||
event.accepted = true
|
||||
return
|
||||
}
|
||||
// Defer selectedIndex reset to avoid binding loops
|
||||
Qt.callLater(() => selectedIndex = 0)
|
||||
|
||||
// Reset cursor position if needed
|
||||
if (shouldResetCursor && text === "") {
|
||||
cursorPosition = 0
|
||||
shouldResetCursor = false
|
||||
}
|
||||
}
|
||||
selectedTextColor: Color.mOnSurface
|
||||
selectionColor: Color.mPrimary
|
||||
padding: 0
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
Component.onCompleted: {
|
||||
// Focus the search bar by default and set cursor position
|
||||
Qt.callLater(() => {
|
||||
selectedIndex = 0
|
||||
searchInput.forceActiveFocus()
|
||||
// Set cursor to end if there's already text
|
||||
if (searchText && searchText.length > 0) {
|
||||
searchInput.cursorPosition = searchText.length
|
||||
}
|
||||
})
|
||||
}
|
||||
Keys.onDownPressed: selectNext()
|
||||
Keys.onUpPressed: selectPrev()
|
||||
Keys.onEnterPressed: activateSelected()
|
||||
Keys.onReturnPressed: activateSelected()
|
||||
Keys.onEscapePressed: root.close()
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_PageDown) {
|
||||
appsList.cancelFlick()
|
||||
root.selectNextPage()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_PageUp) {
|
||||
appsList.cancelFlick()
|
||||
root.selectPrevPage()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Home) {
|
||||
appsList.cancelFlick()
|
||||
selectedIndex = 0
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_End) {
|
||||
appsList.cancelFlick()
|
||||
if (filteredEntries.length > 0) {
|
||||
selectedIndex = filteredEntries.length - 1
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
switch (event.key) {
|
||||
case Qt.Key_J:
|
||||
appsList.cancelFlick()
|
||||
root.selectNext()
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_K:
|
||||
appsList.cancelFlick()
|
||||
root.selectPrev()
|
||||
event.accepted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.width {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
})
|
||||
searchInput.inputItem.Keys.onDownPressed.connect(function (event) {
|
||||
ui.selectNext()
|
||||
})
|
||||
searchInput.inputItem.Keys.onUpPressed.connect(function (event) {
|
||||
ui.selectPrevious()
|
||||
})
|
||||
searchInput.inputItem.Keys.onReturnPressed.connect(function (event) {
|
||||
ui.activate()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Applications list
|
||||
ListView {
|
||||
id: appsList
|
||||
// Results list
|
||||
NListView {
|
||||
id: resultsList
|
||||
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
spacing: Style.marginXXS * scaling
|
||||
model: filteredEntries
|
||||
|
||||
model: results
|
||||
currentIndex: selectedIndex
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
maximumFlickVelocity: 2500
|
||||
flickDeceleration: 2000
|
||||
|
||||
clip: true
|
||||
cacheBuffer: resultsList.height * 2
|
||||
onCurrentIndexChanged: {
|
||||
cancelFlick()
|
||||
if (currentIndex >= 0) {
|
||||
positionViewAtIndex(currentIndex, ListView.Contain)
|
||||
}
|
||||
}
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
policy: ScrollBar.AsNeeded
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
width: appsList.width - Style.marginS * scaling
|
||||
height: 65 * scaling
|
||||
id: entry
|
||||
|
||||
property bool isSelected: mouseArea.containsMouse || (index === selectedIndex)
|
||||
|
||||
// Property to reliably track the current item's ID.
|
||||
// This changes whenever the delegate is recycled for a new item.
|
||||
property var currentClipboardId: modelData.isImage ? modelData.clipboardId : ""
|
||||
|
||||
// When this delegate is assigned a new image item, trigger the decode.
|
||||
onCurrentClipboardIdChanged: {
|
||||
// Check if it's a valid ID and if the data isn't already cached.
|
||||
if (currentClipboardId && !ClipboardService.getImageData(currentClipboardId)) {
|
||||
ClipboardService.decodeToDataUrl(currentClipboardId, modelData.mime, null)
|
||||
}
|
||||
}
|
||||
|
||||
width: resultsList.width - Style.marginS * scaling
|
||||
height: entryHeight
|
||||
radius: Style.radiusM * scaling
|
||||
property bool isSelected: index === selectedIndex
|
||||
color: (appCardArea.containsMouse || isSelected) ? Color.mSecondary : Color.mSurface
|
||||
color: entry.isSelected ? Color.mTertiary : Color.mSurface
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.width {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCirc
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,84 +337,128 @@ NPanel {
|
||||
anchors.margins: Style.marginM * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// App/clipboard icon with background
|
||||
// Icon badge or Image preview
|
||||
Rectangle {
|
||||
Layout.preferredWidth: Style.baseWidgetSize * 1.25 * scaling
|
||||
Layout.preferredHeight: Style.baseWidgetSize * 1.25 * scaling
|
||||
radius: Style.radiusS * scaling
|
||||
color: appCardArea.containsMouse ? Qt.darker(Color.mPrimary, 1.1) : Color.mSurfaceVariant
|
||||
property bool iconLoaded: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand)
|
||||
|| (iconImg.status === Image.Ready && iconImg.source !== ""
|
||||
&& iconImg.status !== Image.Error && iconImg.source !== "")
|
||||
visible: !searchText.startsWith(">calc")
|
||||
Layout.preferredWidth: badgeSize
|
||||
Layout.preferredHeight: badgeSize
|
||||
radius: Style.radiusM * scaling
|
||||
color: Color.mSurfaceVariant
|
||||
clip: true
|
||||
|
||||
// Clipboard image display (pull from cache)
|
||||
Image {
|
||||
id: clipboardImage
|
||||
// Image preview for clipboard images
|
||||
NImageRounded {
|
||||
id: imagePreview
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginXS * scaling
|
||||
visible: modelData.type === 'image'
|
||||
source: modelData.type === 'image' ? (CliphistService.imageDataById[modelData.id] || "") : ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
cache: true
|
||||
visible: modelData.isImage
|
||||
imageRadius: Style.radiusM * scaling
|
||||
|
||||
// This property creates a dependency on the service's revision counter
|
||||
readonly property int _rev: ClipboardService.revision
|
||||
|
||||
// Fetches from the service's cache.
|
||||
// The dependency on `_rev` ensures this binding is re-evaluated when the cache is updated.
|
||||
imagePath: {
|
||||
_rev
|
||||
return ClipboardService.getImageData(modelData.clipboardId) || ""
|
||||
}
|
||||
|
||||
// Loading indicator
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: parent.status === Image.Loading
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
running: true
|
||||
width: Style.baseWidgetSize * 0.5 * scaling
|
||||
height: width
|
||||
}
|
||||
}
|
||||
|
||||
// Error fallback
|
||||
onStatusChanged: status => {
|
||||
if (status === Image.Error) {
|
||||
iconLoader.visible = true
|
||||
imagePreview.visible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: iconImg
|
||||
// Icon fallback
|
||||
Loader {
|
||||
id: iconLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginXS * scaling
|
||||
asynchronous: true
|
||||
source: modelData.isCalculator ? "" : modelData.isClipboard ? "" : modelData.isCommand ? modelData.icon : Icons.iconFromName(
|
||||
modelData.icon,
|
||||
"application-x-executable")
|
||||
visible: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand || parent.iconLoaded)
|
||||
&& modelData.type !== 'image'
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginXS * scaling
|
||||
radius: Style.radiusXS * scaling
|
||||
color: Color.mPrimary
|
||||
opacity: Style.opacityMedium
|
||||
visible: !parent.iconLoaded
|
||||
|
||||
visible: !modelData.isImage || imagePreview.status === Image.Error
|
||||
active: visible
|
||||
|
||||
sourceComponent: Component {
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
source: modelData.icon ? AppIcons.iconFromName(modelData.icon, "application-x-executable") : ""
|
||||
visible: modelData.icon && source !== ""
|
||||
asynchronous: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback text if no icon and no image
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
visible: !parent.iconLoaded && !(modelData.isCalculator || modelData.isClipboard || modelData.isCommand)
|
||||
visible: !imagePreview.visible && !iconLoader.visible
|
||||
text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?"
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
// Image type indicator overlay
|
||||
Rectangle {
|
||||
visible: modelData.isImage && imagePreview.visible
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 2 * scaling
|
||||
width: formatLabel.width + 6 * scaling
|
||||
height: formatLabel.height + 2 * scaling
|
||||
radius: Style.radiusM * scaling
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
NText {
|
||||
id: formatLabel
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
if (!modelData.isImage)
|
||||
return ""
|
||||
const desc = modelData.description || ""
|
||||
const parts = desc.split(" • ")
|
||||
return parts[0] || "IMG"
|
||||
}
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// App info
|
||||
// Text content
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginXXS * scaling
|
||||
spacing: 0 * scaling
|
||||
|
||||
NText {
|
||||
text: modelData.name || "Unknown"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: (appCardArea.containsMouse || isSelected) ? Color.mOnPrimary : Color.mOnSurface
|
||||
color: entry.isSelected ? Color.mOnTertiary : Color.mOnSurface
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NText {
|
||||
text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : modelData.isClipboard ? modelData.content : modelData.isCommand ? modelData.content : (modelData.genericName || modelData.comment || "")
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: (appCardArea.containsMouse || isSelected) ? Color.mOnPrimary : Color.mOnSurface
|
||||
text: modelData.description || ""
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: entry.isSelected ? Color.mOnTertiary : Color.mOnSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
visible: text !== ""
|
||||
@@ -495,41 +467,34 @@ NPanel {
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: appCardArea
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
selectedIndex = index
|
||||
activateSelected()
|
||||
ui.activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No results message
|
||||
NText {
|
||||
text: searchText.trim() !== "" ? "No applications found" : "No applications available"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurface
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
visible: filteredEntries.length === 0
|
||||
}
|
||||
|
||||
// Results count
|
||||
// Status
|
||||
NText {
|
||||
text: searchText.startsWith(
|
||||
">clip") ? (Settings.data.appLauncher.enableClipboardHistory ? `${filteredEntries.length} clipboard item${filteredEntries.length !== 1 ? 's' : ''}` : `Clipboard history is disabled`) : searchText.startsWith(
|
||||
">calc") ? `${filteredEntries.length} result${filteredEntries.length
|
||||
!== 1 ? 's' : ''}` : `${filteredEntries.length} application${filteredEntries.length
|
||||
!== 1 ? 's' : ''}`
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mOnSurface
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
visible: searchText.trim() !== ""
|
||||
text: {
|
||||
if (results.length === 0)
|
||||
return searchText ? "No results" : ""
|
||||
const prefix = activePlugin?.name ? `${activePlugin.name}: ` : ""
|
||||
return prefix + `${results.length} result${results.length !== 1 ? 's' : ''}`
|
||||
}
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
horizontalAlignment: Text.AlignCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
100
Modules/Launcher/Plugins/ApplicationsPlugin.qml
Normal file
@@ -0,0 +1,100 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import "../../../Helpers/FuzzySort.js" as Fuzzysort
|
||||
|
||||
Item {
|
||||
property var launcher: null
|
||||
property string name: "Applications"
|
||||
property bool handleSearch: true
|
||||
property var entries: []
|
||||
|
||||
function init() {
|
||||
loadApplications()
|
||||
}
|
||||
|
||||
function onOpened() {
|
||||
// Refresh apps when launcher opens
|
||||
loadApplications()
|
||||
}
|
||||
|
||||
function loadApplications() {
|
||||
if (typeof DesktopEntries === 'undefined') {
|
||||
Logger.warn("ApplicationsPlugin", "DesktopEntries service not available")
|
||||
return
|
||||
}
|
||||
|
||||
const allApps = DesktopEntries.applications.values || []
|
||||
entries = allApps.filter(app => app && !app.noDisplay)
|
||||
Logger.log("ApplicationsPlugin", `Loaded ${entries.length} applications`)
|
||||
}
|
||||
|
||||
function getResults(query) {
|
||||
if (!entries || entries.length === 0)
|
||||
return []
|
||||
|
||||
if (!query || query.trim() === "") {
|
||||
// Return all apps alphabetically
|
||||
return entries.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())).map(app => createResultEntry(app))
|
||||
}
|
||||
|
||||
// Use fuzzy search if available, fallback to simple search
|
||||
if (typeof Fuzzysort !== 'undefined') {
|
||||
const fuzzyResults = Fuzzysort.go(query, entries, {
|
||||
"keys": ["name", "comment", "genericName"],
|
||||
"threshold": -1000,
|
||||
"limit": 20
|
||||
})
|
||||
|
||||
return fuzzyResults.map(result => createResultEntry(result.obj))
|
||||
} else {
|
||||
// Fallback to simple search
|
||||
const searchTerm = query.toLowerCase()
|
||||
return entries.filter(app => {
|
||||
const name = (app.name || "").toLowerCase()
|
||||
const comment = (app.comment || "").toLowerCase()
|
||||
const generic = (app.genericName || "").toLowerCase()
|
||||
return name.includes(searchTerm) || comment.includes(searchTerm) || generic.includes(searchTerm)
|
||||
}).sort((a, b) => {
|
||||
// Prioritize name matches
|
||||
const aName = a.name.toLowerCase()
|
||||
const bName = b.name.toLowerCase()
|
||||
const aStarts = aName.startsWith(searchTerm)
|
||||
const bStarts = bName.startsWith(searchTerm)
|
||||
if (aStarts && !bStarts)
|
||||
return -1
|
||||
if (!aStarts && bStarts)
|
||||
return 1
|
||||
return aName.localeCompare(bName)
|
||||
}).slice(0, 20).map(app => createResultEntry(app))
|
||||
}
|
||||
}
|
||||
|
||||
function createResultEntry(app) {
|
||||
return {
|
||||
"name": app.name || "Unknown",
|
||||
"description": app.genericName || app.comment || "",
|
||||
"icon": app.icon || "application-x-executable",
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
Logger.log("ApplicationsPlugin", `Launching: ${app.name}`)
|
||||
|
||||
if (Settings.data.appLauncher.useApp2Unit && app.id) {
|
||||
Logger.log("ApplicationsPlugin", `Using app2unit for: ${app.id}`)
|
||||
if (app.runInTerminal)
|
||||
Quickshell.execDetached(["app2unit", "--", app.id + ".desktop"])
|
||||
else
|
||||
Quickshell.execDetached(["app2unit", "--"].concat(app.command))
|
||||
} else if (app.execute) {
|
||||
app.execute()
|
||||
} else if (app.exec) {
|
||||
// Fallback to manual execution
|
||||
Process.execute(app.exec)
|
||||
}
|
||||
launcher.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
105
Modules/Launcher/Plugins/CalculatorPlugin.qml
Normal file
@@ -0,0 +1,105 @@
|
||||
import QtQuick
|
||||
import qs.Services
|
||||
import "../../../Helpers/AdvancedMath.js" as AdvancedMath
|
||||
|
||||
Item {
|
||||
property var launcher: null
|
||||
property string name: "Calculator"
|
||||
|
||||
function handleCommand(query) {
|
||||
// Handle >calc command or direct math expressions after >
|
||||
return query.startsWith(">calc") || (query.startsWith(">") && query.length > 1 && isMathExpression(query.substring(1)))
|
||||
}
|
||||
|
||||
function commands() {
|
||||
return [{
|
||||
"name": ">calc",
|
||||
"description": "Calculator - evaluate mathematical expressions",
|
||||
"icon": "accessories-calculator",
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
launcher.setSearchText(">calc ")
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
function getResults(query) {
|
||||
let expression = ""
|
||||
|
||||
if (query.startsWith(">calc")) {
|
||||
expression = query.substring(5).trim()
|
||||
} else if (query.startsWith(">")) {
|
||||
expression = query.substring(1).trim()
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
|
||||
if (!expression) {
|
||||
return [{
|
||||
"name": "Calculator",
|
||||
"description": "Enter a mathematical expression",
|
||||
"icon": "accessories-calculator",
|
||||
"isImage": false,
|
||||
"onActivate": function () {}
|
||||
}]
|
||||
}
|
||||
|
||||
try {
|
||||
let result = AdvancedMath.evaluate(expression.trim())
|
||||
|
||||
return [{
|
||||
"name": AdvancedMath.formatResult(result),
|
||||
"description": `${expression} = ${result}`,
|
||||
"icon": "accessories-calculator",
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
// TODO: copy entry to clipboard via ClipHist
|
||||
launcher.close()
|
||||
}
|
||||
}]
|
||||
} catch (error) {
|
||||
return [{
|
||||
"name": "Error",
|
||||
"description": error.message || "Invalid expression",
|
||||
"icon": "dialog-error",
|
||||
"isImage": false,
|
||||
"onActivate": function () {}
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
function evaluateExpression(expr) {
|
||||
// Sanitize input - only allow safe characters
|
||||
const sanitized = expr.replace(/[^0-9\+\-\*\/\(\)\.\s\%]/g, '')
|
||||
if (sanitized !== expr) {
|
||||
throw new Error("Invalid characters in expression")
|
||||
}
|
||||
|
||||
// Don't allow empty expressions
|
||||
if (!sanitized.trim()) {
|
||||
throw new Error("Empty expression")
|
||||
}
|
||||
|
||||
try {
|
||||
// Use Function constructor for safe evaluation
|
||||
// This is safer than eval() but still evaluate math
|
||||
const result = Function('"use strict"; return (' + sanitized + ')')()
|
||||
|
||||
// Check for valid result
|
||||
if (!isFinite(result)) {
|
||||
throw new Error("Result is not a finite number")
|
||||
}
|
||||
|
||||
// Round to reasonable precision to avoid floating point issues
|
||||
return Math.round(result * 1000000000) / 1000000000
|
||||
} catch (e) {
|
||||
throw new Error("Invalid mathematical expression")
|
||||
}
|
||||
}
|
||||
|
||||
function isMathExpression(expr) {
|
||||
// Check if string looks like a math expression
|
||||
// Allow digits, operators, parentheses, decimal points, and whitespace
|
||||
return /^[\d\s\+\-\*\/\(\)\.\%]+$/.test(expr)
|
||||
}
|
||||
}
|
||||
268
Modules/Launcher/Plugins/ClipboardPlugin.qml
Normal file
@@ -0,0 +1,268 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Plugin metadata
|
||||
property string name: "Clipboard History"
|
||||
property var launcher: null
|
||||
|
||||
// Plugin capabilities
|
||||
property bool handleSearch: false // Don't handle regular search
|
||||
|
||||
// Internal state
|
||||
property bool isWaitingForData: false
|
||||
property bool gotResults: false
|
||||
property string lastSearchText: ""
|
||||
|
||||
// Listen for clipboard data updates
|
||||
Connections {
|
||||
target: ClipboardService
|
||||
function onListCompleted() {
|
||||
if (gotResults && (lastSearchText === searchText)) {
|
||||
// Do not update results after the first fetch.
|
||||
// This will avoid the list resetting every 2seconds when the service updates.
|
||||
return
|
||||
}
|
||||
// Refresh results if we're waiting for data or if clipboard plugin is active
|
||||
if (isWaitingForData || (launcher && launcher.searchText.startsWith(">clip"))) {
|
||||
isWaitingForData = false
|
||||
gotResults = true
|
||||
if (launcher) {
|
||||
launcher.updateResults()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize plugin
|
||||
function init() {
|
||||
Logger.log("ClipboardPlugin", "Initialized")
|
||||
// Pre-load clipboard data if service is active
|
||||
if (ClipboardService.active) {
|
||||
ClipboardService.list(100)
|
||||
}
|
||||
}
|
||||
|
||||
// Called when launcher opens
|
||||
function onOpened() {
|
||||
isWaitingForData = true
|
||||
gotResults = false
|
||||
lastSearchText = ""
|
||||
|
||||
// Refresh clipboard history when launcher opens
|
||||
if (ClipboardService.active) {
|
||||
ClipboardService.list(100)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this plugin handles the command
|
||||
function handleCommand(searchText) {
|
||||
return searchText.startsWith(">clip")
|
||||
}
|
||||
|
||||
// Return available commands when user types ">"
|
||||
function commands() {
|
||||
return [{
|
||||
"name": ">clip",
|
||||
"description": "Search clipboard history",
|
||||
"icon": "text-x-generic",
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
launcher.setSearchText(">clip ")
|
||||
}
|
||||
}, {
|
||||
"name": ">clip clear",
|
||||
"description": "Clear all clipboard history",
|
||||
"icon": "text-x-generic",
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
ClipboardService.wipeAll()
|
||||
launcher.close()
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
// Get search results
|
||||
function getResults(searchText) {
|
||||
if (!searchText.startsWith(">clip")) {
|
||||
return []
|
||||
}
|
||||
|
||||
lastSearchText = searchText
|
||||
const results = []
|
||||
const query = searchText.slice(5).trim()
|
||||
|
||||
// Check if clipboard service is not active
|
||||
if (!ClipboardService.active) {
|
||||
return [{
|
||||
"name": "Clipboard History Disabled",
|
||||
"description": "Enable clipboard history in settings or install cliphist",
|
||||
"icon": "view-refresh",
|
||||
"isImage": false,
|
||||
"onActivate": function () {}
|
||||
}]
|
||||
}
|
||||
|
||||
// Special command: clear
|
||||
if (query === "clear") {
|
||||
return [{
|
||||
"name": "Clear Clipboard History",
|
||||
"description": "Remove all items from clipboard history",
|
||||
"icon": "delete_sweep",
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
ClipboardService.wipeAll()
|
||||
launcher.close()
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
// Show loading state if data is being loaded
|
||||
if (ClipboardService.loading || isWaitingForData) {
|
||||
return [{
|
||||
"name": "Loading clipboard history...",
|
||||
"description": "Please wait",
|
||||
"icon": "view-refresh",
|
||||
"isImage": false,
|
||||
"onActivate": function () {}
|
||||
}]
|
||||
}
|
||||
|
||||
// Get clipboard items
|
||||
const items = ClipboardService.items || []
|
||||
|
||||
// If no items and we haven't tried loading yet, trigger a load
|
||||
if (items.count === 0 && !ClipboardService.loading) {
|
||||
isWaitingForData = true
|
||||
ClipboardService.list(100)
|
||||
return [{
|
||||
"name": "Loading clipboard history...",
|
||||
"description": "Please wait",
|
||||
"icon": "view-refresh",
|
||||
"isImage": false,
|
||||
"onActivate": function () {}
|
||||
}]
|
||||
}
|
||||
|
||||
// Search clipboard items
|
||||
const searchTerm = query.toLowerCase()
|
||||
|
||||
// Filter and format results
|
||||
items.forEach(function (item) {
|
||||
const preview = (item.preview || "").toLowerCase()
|
||||
|
||||
// Skip if search term doesn't match
|
||||
if (searchTerm && preview.indexOf(searchTerm) === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
// Format the result based on type
|
||||
let entry
|
||||
if (item.isImage) {
|
||||
entry = formatImageEntry(item)
|
||||
} else {
|
||||
entry = formatTextEntry(item)
|
||||
}
|
||||
|
||||
// Add activation handler
|
||||
entry.onActivate = function () {
|
||||
ClipboardService.copyToClipboard(item.id)
|
||||
launcher.close()
|
||||
}
|
||||
|
||||
results.push(entry)
|
||||
})
|
||||
|
||||
// Show empty state if no results
|
||||
if (results.length === 0) {
|
||||
results.push({
|
||||
"name": searchTerm ? "No matching clipboard items" : "Clipboard is empty",
|
||||
"description": searchTerm ? `No items containing "${query}"` : "Copy something to see it here",
|
||||
"icon": "text-x-generic",
|
||||
"isImage": false,
|
||||
"onActivate": function () {// Do nothing
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//Logger.log("ClipboardPlugin", `Returning ${results.length} results for query: "${query}"`)
|
||||
return results
|
||||
}
|
||||
|
||||
// Helper: Format image clipboard entry
|
||||
function formatImageEntry(item) {
|
||||
const meta = parseImageMeta(item.preview)
|
||||
|
||||
// The launcher's delegate will now be responsible for fetching the image data.
|
||||
// This function's role is to provide the necessary metadata for that request.
|
||||
return {
|
||||
"name": meta ? `Image ${meta.w}×${meta.h}` : "Image",
|
||||
"description": meta ? `${meta.fmt} • ${meta.size}` : item.mime || "Image data",
|
||||
"icon": "image",
|
||||
"isImage": true,
|
||||
"imageWidth": meta ? meta.w : 0,
|
||||
"imageHeight": meta ? meta.h : 0,
|
||||
"clipboardId": item.id,
|
||||
"mime": item.mime
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Format text clipboard entry with preview
|
||||
function formatTextEntry(item) {
|
||||
const preview = (item.preview || "").trim()
|
||||
const lines = preview.split('\n').filter(l => l.trim())
|
||||
|
||||
// Use first line as title, limit length
|
||||
let title = lines[0] || "Empty text"
|
||||
if (title.length > 60) {
|
||||
title = title.substring(0, 57) + "..."
|
||||
}
|
||||
|
||||
// Use second line or character count as description
|
||||
let description = ""
|
||||
if (lines.length > 1) {
|
||||
description = lines[1]
|
||||
if (description.length > 80) {
|
||||
description = description.substring(0, 77) + "..."
|
||||
}
|
||||
} else {
|
||||
const chars = preview.length
|
||||
const words = preview.split(/\s+/).length
|
||||
description = `${chars} characters, ${words} word${words !== 1 ? 's' : ''}`
|
||||
}
|
||||
|
||||
return {
|
||||
"name": title,
|
||||
"description": description,
|
||||
"icon": "text-x-generic",
|
||||
"isImage": false
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Parse image metadata from preview string
|
||||
function parseImageMeta(preview) {
|
||||
const re = /\[\[\s*binary data\s+([\d\.]+\s*(?:KiB|MiB|GiB|B))\s+(\w+)\s+(\d+)x(\d+)\s*\]\]/i
|
||||
const match = (preview || "").match(re)
|
||||
|
||||
if (!match) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
"size": match[1],
|
||||
"fmt": (match[2] || "").toUpperCase(),
|
||||
"w": Number(match[3]),
|
||||
"h": Number(match[4])
|
||||
}
|
||||
}
|
||||
|
||||
// Public method to get image data for a clipboard item
|
||||
// This can be called by the launcher when rendering
|
||||
function getImageForItem(clipboardId) {
|
||||
return ClipboardService.getImageData ? ClipboardService.getImageData(clipboardId) : null
|
||||
}
|
||||
}
|
||||
@@ -48,8 +48,7 @@ Scope {
|
||||
user: Quickshell.env("USER")
|
||||
|
||||
onPamMessage: {
|
||||
Logger.log("LockContext", "PAM message:", message, "isError:", messageIsError, "responseRequired:",
|
||||
responseRequired)
|
||||
Logger.log("LockContext", "PAM message:", message, "isError:", messageIsError, "responseRequired:", responseRequired)
|
||||
|
||||
if (messageIsError) {
|
||||
errorMessage = message
|
||||
|
||||
@@ -12,12 +12,11 @@ import qs.Widgets
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
PanelWindow {
|
||||
delegate: Loader {
|
||||
id: root
|
||||
|
||||
required property ShellScreen modelData
|
||||
readonly property real scaling: ScalingService.scale(screen)
|
||||
screen: modelData
|
||||
readonly property real scaling: ScalingService.getScreenScale(modelData)
|
||||
|
||||
// Access the notification model from the service
|
||||
property ListModel notificationModel: NotificationService.notificationModel
|
||||
@@ -25,195 +24,317 @@ Variants {
|
||||
// Track notifications being removed for animation
|
||||
property var removingNotifications: ({})
|
||||
|
||||
color: Color.transparent
|
||||
|
||||
// If no notification display activated in settings, then show them all
|
||||
visible: modelData ? (Settings.data.notifications.monitors.includes(modelData.name)
|
||||
|| (Settings.data.notifications.monitors.length === 0))
|
||||
&& (NotificationService.notificationModel.count > 0) : false
|
||||
active: Settings.isLoaded && modelData && (NotificationService.notificationModel.count > 0) ? (Settings.data.notifications.monitors.includes(modelData.name) || (Settings.data.notifications.monitors.length === 0)) : false
|
||||
|
||||
// Position based on bar location
|
||||
anchors.top: Settings.data.bar.position === "top"
|
||||
anchors.bottom: Settings.data.bar.position === "bottom"
|
||||
anchors.right: true
|
||||
margins.top: Settings.data.bar.position === "top" ? (Style.barHeight + Style.marginM) * scaling : 0
|
||||
margins.bottom: Settings.data.bar.position === "bottom" ? (Style.barHeight + Style.marginM) * scaling : 0
|
||||
margins.right: Style.marginM * scaling
|
||||
implicitWidth: 360 * scaling
|
||||
implicitHeight: Math.min(notificationStack.implicitHeight, (NotificationService.maxVisible * 120) * scaling)
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
visible: (NotificationService.notificationModel.count > 0)
|
||||
|
||||
// Connect to animation signal from service
|
||||
Component.onCompleted: {
|
||||
NotificationService.animateAndRemove.connect(function (notification, index) {
|
||||
// Prefer lookup by identity to avoid index mismatches
|
||||
var delegate = null
|
||||
if (notificationStack.children && notificationStack.children.length > 0) {
|
||||
for (var i = 0; i < notificationStack.children.length; i++) {
|
||||
var child = notificationStack.children[i]
|
||||
if (child && child.model && child.model.rawNotification === notification) {
|
||||
delegate = child
|
||||
break
|
||||
}
|
||||
}
|
||||
sourceComponent: PanelWindow {
|
||||
screen: modelData
|
||||
color: Color.transparent
|
||||
|
||||
// Position based on bar location - always at top
|
||||
anchors.top: true
|
||||
anchors.right: Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
|
||||
anchors.left: Settings.data.bar.position === "left"
|
||||
|
||||
margins.top: {
|
||||
switch (Settings.data.bar.position) {
|
||||
case "top":
|
||||
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
|
||||
default:
|
||||
return Style.marginM * scaling
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to index if identity lookup failed
|
||||
if (!delegate && notificationStack.children && notificationStack.children[index]) {
|
||||
delegate = notificationStack.children[index]
|
||||
margins.bottom: {
|
||||
switch (Settings.data.bar.position) {
|
||||
case "bottom":
|
||||
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
if (delegate && delegate.animateOut) {
|
||||
delegate.animateOut()
|
||||
} else {
|
||||
// As a last resort, force-remove without animation to avoid stuck popups
|
||||
NotificationService.forceRemoveNotification(notification)
|
||||
margins.left: {
|
||||
switch (Settings.data.bar.position) {
|
||||
case "left":
|
||||
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Main notification container
|
||||
Column {
|
||||
id: notificationStack
|
||||
// Position based on bar location
|
||||
anchors.top: Settings.data.bar.position === "top" ? parent.top : undefined
|
||||
anchors.bottom: Settings.data.bar.position === "bottom" ? parent.bottom : undefined
|
||||
anchors.right: parent.right
|
||||
spacing: Style.marginS * scaling
|
||||
width: 360 * scaling
|
||||
visible: true
|
||||
margins.right: {
|
||||
switch (Settings.data.bar.position) {
|
||||
case "right":
|
||||
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0)
|
||||
case "top":
|
||||
case "bottom":
|
||||
return Style.marginM * scaling
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Multiple notifications display
|
||||
Repeater {
|
||||
model: notificationModel
|
||||
delegate: Rectangle {
|
||||
width: 360 * scaling
|
||||
height: Math.max(80 * scaling, contentColumn.implicitHeight + (Style.marginM * 2 * scaling))
|
||||
clip: true
|
||||
radius: Style.radiusM * scaling
|
||||
border.color: Color.mPrimary
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
color: Color.mSurface
|
||||
implicitWidth: 360 * scaling
|
||||
implicitHeight: Math.min(notificationStack.implicitHeight, (NotificationService.maxVisible * 120) * scaling)
|
||||
//WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
// Animation properties
|
||||
property real scaleValue: 0.8
|
||||
property real opacityValue: 0.0
|
||||
property bool isRemoving: false
|
||||
|
||||
// Scale and fade-in animation
|
||||
scale: scaleValue
|
||||
opacity: opacityValue
|
||||
|
||||
// Animate in when the item is created
|
||||
Component.onCompleted: {
|
||||
scaleValue = 1.0
|
||||
opacityValue = 1.0
|
||||
}
|
||||
|
||||
// Animate out when being removed
|
||||
function animateOut() {
|
||||
isRemoving = true
|
||||
scaleValue = 0.8
|
||||
opacityValue = 0.0
|
||||
}
|
||||
|
||||
// Timer for delayed removal after animation
|
||||
Timer {
|
||||
id: removalTimer
|
||||
interval: Style.animationSlow
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
NotificationService.forceRemoveNotification(model.rawNotification)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this notification is being removed
|
||||
onIsRemovingChanged: {
|
||||
if (isRemoving) {
|
||||
// Remove from model after animation completes
|
||||
removalTimer.start()
|
||||
}
|
||||
}
|
||||
|
||||
// Animation behaviors
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Style.animationSlow
|
||||
easing.type: Easing.OutExpo
|
||||
//easing.type: Easing.OutBack looks better but notification get clipped on all sides
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginS * scaling
|
||||
NText {
|
||||
text: (model.appName || model.desktopEntry) || "Unknown App"
|
||||
color: Color.mSecondary
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
// Connect to animation signal from service
|
||||
Component.onCompleted: {
|
||||
NotificationService.animateAndRemove.connect(function (notification, index) {
|
||||
// Prefer lookup by identity to avoid index mismatches
|
||||
var delegate = null
|
||||
if (notificationStack && notificationStack.children && notificationStack.children.length > 0) {
|
||||
for (var i = 0; i < notificationStack.children.length; i++) {
|
||||
var child = notificationStack.children[i]
|
||||
if (child && child.model && child.model.rawNotification === notification) {
|
||||
delegate = child
|
||||
break
|
||||
}
|
||||
Rectangle {
|
||||
width: 6 * scaling
|
||||
height: 6 * scaling
|
||||
radius: Style.radiusXS * scaling
|
||||
color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to index if identity lookup failed
|
||||
if (!delegate && notificationStack && notificationStack.children && notificationStack.children[index]) {
|
||||
delegate = notificationStack.children[index]
|
||||
}
|
||||
|
||||
if (delegate && delegate.animateOut) {
|
||||
delegate.animateOut()
|
||||
} else {
|
||||
// As a last resort, force-remove without animation to avoid stuck popups
|
||||
NotificationService.forceRemoveNotification(notification)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Main notification container
|
||||
ColumnLayout {
|
||||
id: notificationStack
|
||||
// Position based on bar location - always at top
|
||||
anchors.top: parent.top
|
||||
anchors.right: (Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom") ? parent.right : undefined
|
||||
anchors.left: Settings.data.bar.position === "left" ? parent.left : undefined
|
||||
spacing: Style.marginS * scaling
|
||||
width: 360 * scaling
|
||||
visible: true
|
||||
|
||||
// Multiple notifications display
|
||||
Repeater {
|
||||
model: notificationModel
|
||||
delegate: Rectangle {
|
||||
Layout.preferredWidth: 360 * scaling
|
||||
Layout.preferredHeight: notificationLayout.implicitHeight + (Style.marginL * 2 * scaling)
|
||||
Layout.maximumHeight: Layout.preferredHeight
|
||||
clip: true
|
||||
radius: Style.radiusL * scaling
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
color: Color.mSurface
|
||||
|
||||
// Animation properties
|
||||
property real scaleValue: 0.8
|
||||
property real opacityValue: 0.0
|
||||
property bool isRemoving: false
|
||||
|
||||
// Right-click to dismiss
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
animateOut()
|
||||
}
|
||||
}
|
||||
Item {
|
||||
}
|
||||
|
||||
// Scale and fade-in animation
|
||||
scale: scaleValue
|
||||
opacity: opacityValue
|
||||
|
||||
// Animate in when the item is created
|
||||
Component.onCompleted: {
|
||||
scaleValue = 1.0
|
||||
opacityValue = 1.0
|
||||
}
|
||||
|
||||
// Animate out when being removed
|
||||
function animateOut() {
|
||||
isRemoving = true
|
||||
scaleValue = 0.8
|
||||
opacityValue = 0.0
|
||||
}
|
||||
|
||||
// Timer for delayed removal after animation
|
||||
Timer {
|
||||
id: removalTimer
|
||||
interval: Style.animationSlow
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
NotificationService.forceRemoveNotification(model.rawNotification)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this notification is being removed
|
||||
onIsRemovingChanged: {
|
||||
if (isRemoving) {
|
||||
// Remove from model after animation completes
|
||||
removalTimer.start()
|
||||
}
|
||||
}
|
||||
|
||||
// Animation behaviors
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Style.animationSlow
|
||||
easing.type: Easing.OutExpo
|
||||
//easing.type: Easing.OutBack looks better but notification get clipped on all sides
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: notificationLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM * scaling
|
||||
anchors.rightMargin: (Style.marginM + 32) * scaling // Leave space for close button
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Header section with app name and timestamp
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NText {
|
||||
text: `${(model.appName || model.desktopEntry) || "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}`
|
||||
color: Color.mSecondary
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 6 * scaling
|
||||
Layout.preferredHeight: 6 * scaling
|
||||
radius: Style.radiusXS * scaling
|
||||
color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
NText {
|
||||
text: NotificationService.formatTimestamp(model.timestamp)
|
||||
color: Color.mOnSurface
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
|
||||
// Main content section
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Image
|
||||
NImageCircled {
|
||||
Layout.preferredWidth: 40 * scaling
|
||||
Layout.preferredHeight: 40 * scaling
|
||||
Layout.alignment: Qt.AlignTop
|
||||
imagePath: model.image && model.image !== "" ? model.image : ""
|
||||
borderColor: Color.transparent
|
||||
borderWidth: 0
|
||||
visible: (model.image && model.image !== "")
|
||||
}
|
||||
|
||||
// Text content
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NText {
|
||||
text: model.summary || "No summary"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSurface
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
Layout.fillWidth: true
|
||||
maximumLineCount: 3
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
NText {
|
||||
text: model.body || ""
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurface
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
Layout.fillWidth: true
|
||||
maximumLineCount: 5
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notification actions
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
visible: model.rawNotification && model.rawNotification.actions && model.rawNotification.actions.length > 0
|
||||
|
||||
property var notificationActions: model.rawNotification ? model.rawNotification.actions : []
|
||||
|
||||
Repeater {
|
||||
model: parent.notificationActions
|
||||
|
||||
delegate: NButton {
|
||||
text: {
|
||||
var actionText = modelData.text || "Open"
|
||||
// If text contains comma, take the part after the comma (the display text)
|
||||
if (actionText.includes(",")) {
|
||||
return actionText.split(",")[1] || actionText
|
||||
}
|
||||
return actionText
|
||||
}
|
||||
fontSize: Style.fontSizeS * scaling
|
||||
backgroundColor: Color.mPrimary
|
||||
textColor: Color.mOnPrimary
|
||||
hoverColor: Color.mSecondary
|
||||
pressColor: Color.mTertiary
|
||||
outlined: false
|
||||
customHeight: 32 * scaling
|
||||
Layout.preferredHeight: 32 * scaling
|
||||
|
||||
onClicked: {
|
||||
if (modelData && modelData.invoke) {
|
||||
modelData.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer to push buttons to the left if needed
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: model.summary || "No summary"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
wrapMode: Text.Wrap
|
||||
width: 300 * scaling
|
||||
maximumLineCount: 3
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
// Close button positioned absolutely
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: "Close."
|
||||
sizeRatio: 0.6
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Style.marginM * scaling
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Style.marginM * scaling
|
||||
|
||||
NText {
|
||||
text: model.body || ""
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mOnSurface
|
||||
wrapMode: Text.Wrap
|
||||
width: 300 * scaling
|
||||
maximumLineCount: 5
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: "Close"
|
||||
sizeMultiplier: 0.8
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Style.marginS * scaling
|
||||
|
||||
onClicked: {
|
||||
animateOut()
|
||||
onClicked: {
|
||||
animateOut()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,10 @@ import qs.Widgets
|
||||
NPanel {
|
||||
id: root
|
||||
|
||||
panelWidth: 380 * scaling
|
||||
panelHeight: 500 * scaling
|
||||
panelAnchorRight: true
|
||||
preferredWidth: 380
|
||||
preferredHeight: 500
|
||||
panelAnchorRight: Settings.data.bar.position === "right"
|
||||
panelKeyboardFocus: true
|
||||
|
||||
panelContent: Rectangle {
|
||||
id: notificationRect
|
||||
@@ -25,12 +26,13 @@ NPanel {
|
||||
anchors.margins: Style.marginL * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Header section
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NIcon {
|
||||
text: "notifications"
|
||||
icon: "bell"
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
color: Color.mPrimary
|
||||
}
|
||||
@@ -44,16 +46,27 @@ NPanel {
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "delete"
|
||||
tooltipText: "Clear History"
|
||||
sizeMultiplier: 0.8
|
||||
onClicked: NotificationService.clearHistory()
|
||||
icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell"
|
||||
tooltipText: Settings.data.notifications.doNotDisturb ? "'Do Not Disturb' is enabled." : "'Do Not Disturb' is disabled."
|
||||
sizeRatio: 0.8
|
||||
onClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
|
||||
onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "trash"
|
||||
tooltipText: "Clear history"
|
||||
sizeRatio: 0.8
|
||||
onClicked: {
|
||||
NotificationService.clearHistory()
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: "Close"
|
||||
sizeMultiplier: 0.8
|
||||
tooltipText: "Close."
|
||||
sizeRatio: 0.8
|
||||
onClicked: {
|
||||
root.close()
|
||||
}
|
||||
@@ -65,42 +78,54 @@ NPanel {
|
||||
}
|
||||
|
||||
// Empty state when no notifications
|
||||
Item {
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: NotificationService.historyModel.count === 0
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginM * scaling
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
NIcon {
|
||||
text: "notifications_off"
|
||||
font.pointSize: Style.fontSizeXXXL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
NIcon {
|
||||
icon: "bell-off"
|
||||
font.pointSize: 64 * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "No notifications"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
NText {
|
||||
text: "No notifications"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Notifications will appear here when you receive them"
|
||||
font.pointSize: Style.fontSizeNormal * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
NText {
|
||||
text: "Your notifications will show up here as they arrive."
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
// Notification list
|
||||
NListView {
|
||||
id: notificationList
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
|
||||
model: NotificationService.historyModel
|
||||
spacing: Style.marginM * scaling
|
||||
clip: true
|
||||
@@ -108,32 +133,45 @@ NPanel {
|
||||
visible: NotificationService.historyModel.count > 0
|
||||
|
||||
delegate: Rectangle {
|
||||
width: notificationList ? notificationList.width : 380 * scaling
|
||||
height: Math.max(80, notificationContent.height + 30)
|
||||
width: notificationList.width
|
||||
height: notificationLayout.implicitHeight + (Style.marginM * scaling * 2)
|
||||
radius: Style.radiusM * scaling
|
||||
color: notificationMouseArea.containsMouse ? Color.mSecondary : Color.mSurfaceVariant
|
||||
color: notificationMouseArea.containsMouse ? Color.mTertiary : Color.mSurfaceVariant
|
||||
border.color: Qt.alpha(Color.mOutline, Style.opacityMedium)
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
RowLayout {
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: Style.marginM * scaling
|
||||
}
|
||||
id: notificationLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Notification content
|
||||
Column {
|
||||
id: notificationContent
|
||||
// App icon (same style as popup)
|
||||
NImageCircled {
|
||||
Layout.preferredWidth: 28 * scaling
|
||||
Layout.preferredHeight: 28 * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
// Prefer stable themed icons over transient image paths
|
||||
imagePath: (appIcon && appIcon !== "") ? (AppIcons.iconFromName(appIcon, "application-x-executable") || appIcon) : ((AppIcons.iconForAppId(desktopEntry || appName, "application-x-executable") || (image && image !== "" ? image : AppIcons.iconFromName("application-x-executable", "application-x-executable"))))
|
||||
borderColor: Color.transparent
|
||||
borderWidth: 0
|
||||
visible: true
|
||||
}
|
||||
|
||||
// Notification content column
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.maximumWidth: notificationList.width - (Style.marginM * scaling * 4) // Account for margins and delete button
|
||||
spacing: Style.marginXXS * scaling
|
||||
|
||||
NText {
|
||||
text: (summary || "No summary").substring(0, 100)
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Font.Medium
|
||||
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mPrimary
|
||||
color: notificationMouseArea.containsMouse ? Color.mOnTertiary : Color.mPrimary
|
||||
wrapMode: Text.Wrap
|
||||
width: parent.width - 60
|
||||
Layout.fillWidth: true
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
@@ -141,25 +179,28 @@ NPanel {
|
||||
NText {
|
||||
text: (body || "").substring(0, 150)
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface
|
||||
color: notificationMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface
|
||||
wrapMode: Text.Wrap
|
||||
width: parent.width - 60
|
||||
Layout.fillWidth: true
|
||||
maximumLineCount: 3
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
NText {
|
||||
text: NotificationService.formatTimestamp(timestamp)
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface
|
||||
color: notificationMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
// Trash icon button
|
||||
// Delete button
|
||||
NIconButton {
|
||||
icon: "delete"
|
||||
tooltipText: "Delete Notification"
|
||||
sizeMultiplier: 0.7
|
||||
icon: "trash"
|
||||
tooltipText: "Delete notification"
|
||||
sizeRatio: 0.7
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
onClicked: {
|
||||
Logger.log("NotificationHistory", "Removing notification:", summary)
|
||||
@@ -172,7 +213,7 @@ NPanel {
|
||||
MouseArea {
|
||||
id: notificationMouseArea
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: Style.marginL * 3 * scaling
|
||||
anchors.rightMargin: Style.marginXL * scaling
|
||||
hoverEnabled: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
@@ -12,9 +13,11 @@ import qs.Widgets
|
||||
NPanel {
|
||||
id: root
|
||||
|
||||
panelWidth: 440 * scaling
|
||||
panelHeight: 380 * scaling
|
||||
panelAnchorCentered: true
|
||||
preferredWidth: 440
|
||||
preferredHeight: 410
|
||||
panelAnchorHorizontalCenter: true
|
||||
panelAnchorVerticalCenter: true
|
||||
panelKeyboardFocus: true
|
||||
|
||||
// Timer properties
|
||||
property int timerDuration: 9000 // 9 seconds
|
||||
@@ -22,9 +25,44 @@ NPanel {
|
||||
property bool timerActive: false
|
||||
property int timeRemaining: 0
|
||||
|
||||
// Cancel timer when panel is closing
|
||||
// Navigation properties
|
||||
property int selectedIndex: 0
|
||||
readonly property var powerOptions: [{
|
||||
"action": "lock",
|
||||
"icon": "lock",
|
||||
"title": "Lock",
|
||||
"subtitle": "Lock your session"
|
||||
}, {
|
||||
"action": "suspend",
|
||||
"icon": "suspend",
|
||||
"title": "Suspend",
|
||||
"subtitle": "Put the system to sleep"
|
||||
}, {
|
||||
"action": "reboot",
|
||||
"icon": "reboot",
|
||||
"title": "Reboot",
|
||||
"subtitle": "Restart the system"
|
||||
}, {
|
||||
"action": "logout",
|
||||
"icon": "logout",
|
||||
"title": "Logout",
|
||||
"subtitle": "End your session"
|
||||
}, {
|
||||
"action": "shutdown",
|
||||
"icon": "shutdown",
|
||||
"title": "Shutdown",
|
||||
"subtitle": "Turn off the system",
|
||||
"isShutdown": true
|
||||
}]
|
||||
|
||||
// Lifecycle handlers
|
||||
onOpened: {
|
||||
selectedIndex = 0
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
cancelTimer()
|
||||
selectedIndex = 0
|
||||
}
|
||||
|
||||
// Timer management
|
||||
@@ -78,6 +116,38 @@ NPanel {
|
||||
root.close()
|
||||
}
|
||||
|
||||
// Navigation functions
|
||||
function selectNext() {
|
||||
if (powerOptions.length > 0) {
|
||||
selectedIndex = Math.min(selectedIndex + 1, powerOptions.length - 1)
|
||||
}
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
if (powerOptions.length > 0) {
|
||||
selectedIndex = Math.max(selectedIndex - 1, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function selectFirst() {
|
||||
selectedIndex = 0
|
||||
}
|
||||
|
||||
function selectLast() {
|
||||
if (powerOptions.length > 0) {
|
||||
selectedIndex = powerOptions.length - 1
|
||||
} else {
|
||||
selectedIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
function activate() {
|
||||
if (powerOptions.length > 0 && powerOptions[selectedIndex]) {
|
||||
const option = powerOptions[selectedIndex]
|
||||
startTimer(option.action)
|
||||
}
|
||||
}
|
||||
|
||||
// Countdown timer
|
||||
Timer {
|
||||
id: countdownTimer
|
||||
@@ -92,8 +162,93 @@ NPanel {
|
||||
}
|
||||
|
||||
panelContent: Rectangle {
|
||||
id: ui
|
||||
color: Color.transparent
|
||||
|
||||
// Keyboard shortcuts
|
||||
Shortcut {
|
||||
sequence: "Ctrl+K"
|
||||
onActivated: ui.selectPrevious()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+J"
|
||||
onActivated: ui.selectNext()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Up"
|
||||
onActivated: ui.selectPrevious()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Down"
|
||||
onActivated: ui.selectNext()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Home"
|
||||
onActivated: ui.selectFirst()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "End"
|
||||
onActivated: ui.selectLast()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Return"
|
||||
onActivated: ui.activate()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Enter"
|
||||
onActivated: ui.activate()
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Escape"
|
||||
onActivated: {
|
||||
if (timerActive) {
|
||||
cancelTimer()
|
||||
} else {
|
||||
cancelTimer()
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
context: Qt.WidgetShortcut
|
||||
enabled: root.opened
|
||||
}
|
||||
|
||||
// Navigation functions
|
||||
function selectNext() {
|
||||
root.selectNext()
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
root.selectPrevious()
|
||||
}
|
||||
|
||||
function selectFirst() {
|
||||
root.selectFirst()
|
||||
}
|
||||
|
||||
function selectLast() {
|
||||
root.selectLast()
|
||||
}
|
||||
|
||||
function activate() {
|
||||
root.activate()
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Style.marginL * scaling
|
||||
@@ -108,8 +263,7 @@ NPanel {
|
||||
Layout.preferredHeight: Style.baseWidgetSize * 0.8 * scaling
|
||||
|
||||
NText {
|
||||
text: timerActive ? `${pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1)} in ${Math.ceil(
|
||||
timeRemaining / 1000)} seconds...` : "Power Options"
|
||||
text: timerActive ? `${pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1)} in ${Math.ceil(timeRemaining / 1000)} seconds...` : "Power Menu"
|
||||
font.weight: Style.fontWeightBold
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: timerActive ? Color.mPrimary : Color.mOnSurface
|
||||
@@ -122,10 +276,10 @@ NPanel {
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: timerActive ? "back_hand" : "close"
|
||||
icon: timerActive ? "stop" : "close"
|
||||
tooltipText: timerActive ? "Cancel Timer" : "Close"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
colorBg: timerActive ? Color.applyOpacity(Color.mError, "20") : Color.transparent
|
||||
colorBg: timerActive ? Qt.alpha(Color.mError, 0.08) : Color.transparent
|
||||
colorFg: timerActive ? Color.mError : Color.mOnSurface
|
||||
onClicked: {
|
||||
if (timerActive) {
|
||||
@@ -138,60 +292,30 @@ NPanel {
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Power options
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Lock Screen
|
||||
PowerButton {
|
||||
Layout.fillWidth: true
|
||||
icon: "lock_outline"
|
||||
title: "Lock"
|
||||
subtitle: "Lock your session"
|
||||
onClicked: startTimer("lock")
|
||||
pending: timerActive && pendingAction === "lock"
|
||||
}
|
||||
|
||||
// Suspend
|
||||
PowerButton {
|
||||
Layout.fillWidth: true
|
||||
icon: "bedtime"
|
||||
title: "Suspend"
|
||||
subtitle: "Put the system to sleep"
|
||||
onClicked: startTimer("suspend")
|
||||
pending: timerActive && pendingAction === "suspend"
|
||||
}
|
||||
|
||||
// Reboot
|
||||
PowerButton {
|
||||
Layout.fillWidth: true
|
||||
icon: "refresh"
|
||||
title: "Reboot"
|
||||
subtitle: "Restart the system"
|
||||
onClicked: startTimer("reboot")
|
||||
pending: timerActive && pendingAction === "reboot"
|
||||
}
|
||||
|
||||
// Logout
|
||||
PowerButton {
|
||||
Layout.fillWidth: true
|
||||
icon: "exit_to_app"
|
||||
title: "Logout"
|
||||
subtitle: "End your session"
|
||||
onClicked: startTimer("logout")
|
||||
pending: timerActive && pendingAction === "logout"
|
||||
}
|
||||
|
||||
// Shutdown
|
||||
PowerButton {
|
||||
Layout.fillWidth: true
|
||||
icon: "power_settings_new"
|
||||
title: "Shutdown"
|
||||
subtitle: "Turn off the system"
|
||||
onClicked: startTimer("shutdown")
|
||||
pending: timerActive && pendingAction === "shutdown"
|
||||
isShutdown: true
|
||||
Repeater {
|
||||
model: powerOptions
|
||||
delegate: PowerButton {
|
||||
Layout.fillWidth: true
|
||||
icon: modelData.icon
|
||||
title: modelData.title
|
||||
subtitle: modelData.subtitle
|
||||
isShutdown: modelData.isShutdown || false
|
||||
isSelected: index === selectedIndex
|
||||
onClicked: {
|
||||
selectedIndex = index
|
||||
startTimer(modelData.action)
|
||||
}
|
||||
pending: timerActive && pendingAction === modelData.action
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -206,16 +330,19 @@ NPanel {
|
||||
property string subtitle: ""
|
||||
property bool pending: false
|
||||
property bool isShutdown: false
|
||||
property bool isSelected: false
|
||||
|
||||
signal clicked
|
||||
|
||||
height: Style.baseWidgetSize * 1.6 * scaling
|
||||
radius: Style.radiusS * scaling
|
||||
color: {
|
||||
if (pending)
|
||||
return Color.applyOpacity(Color.mPrimary, "20")
|
||||
if (mouseArea.containsMouse)
|
||||
return Color.mSecondary
|
||||
if (pending) {
|
||||
return Qt.alpha(Color.mPrimary, 0.08)
|
||||
}
|
||||
if (isSelected || mouseArea.containsMouse) {
|
||||
return Color.mTertiary
|
||||
}
|
||||
return Color.transparent
|
||||
}
|
||||
|
||||
@@ -237,14 +364,13 @@ NPanel {
|
||||
id: iconElement
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: buttonRoot.icon
|
||||
icon: buttonRoot.icon
|
||||
color: {
|
||||
|
||||
if (buttonRoot.pending)
|
||||
return Color.mPrimary
|
||||
if (buttonRoot.isShutdown && !mouseArea.containsMouse)
|
||||
if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse)
|
||||
return Color.mError
|
||||
if (mouseArea.containsMouse)
|
||||
if (buttonRoot.isSelected || mouseArea.containsMouse)
|
||||
return Color.mOnTertiary
|
||||
return Color.mOnSurface
|
||||
}
|
||||
@@ -261,7 +387,7 @@ NPanel {
|
||||
}
|
||||
|
||||
// Text content in the middle
|
||||
Column {
|
||||
ColumnLayout {
|
||||
anchors.left: iconElement.right
|
||||
anchors.right: pendingIndicator.visible ? pendingIndicator.left : parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -276,9 +402,9 @@ NPanel {
|
||||
color: {
|
||||
if (buttonRoot.pending)
|
||||
return Color.mPrimary
|
||||
if (buttonRoot.isShutdown && !mouseArea.containsMouse)
|
||||
if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse)
|
||||
return Color.mError
|
||||
if (mouseArea.containsMouse)
|
||||
if (buttonRoot.isSelected || mouseArea.containsMouse)
|
||||
return Color.mOnTertiary
|
||||
return Color.mOnSurface
|
||||
}
|
||||
@@ -301,14 +427,15 @@ NPanel {
|
||||
color: {
|
||||
if (buttonRoot.pending)
|
||||
return Color.mPrimary
|
||||
if (buttonRoot.isShutdown && !mouseArea.containsMouse)
|
||||
if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse)
|
||||
return Color.mError
|
||||
if (mouseArea.containsMouse)
|
||||
if (buttonRoot.isSelected || mouseArea.containsMouse)
|
||||
return Color.mOnTertiary
|
||||
return Color.mOnSurfaceVariant
|
||||
}
|
||||
opacity: Style.opacityHeavy
|
||||
wrapMode: Text.WordWrap
|
||||
wrapMode: Text.NoWrap
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
525
Modules/SettingsPanel/Bar/BarSectionEditor.qml
Normal file
@@ -0,0 +1,525 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
NBox {
|
||||
id: root
|
||||
|
||||
property string sectionName: ""
|
||||
property string sectionId: ""
|
||||
property var widgetModel: []
|
||||
property var availableWidgets: []
|
||||
|
||||
signal addWidget(string widgetId, string section)
|
||||
signal removeWidget(string section, int index)
|
||||
signal reorderWidget(string section, int fromIndex, int toIndex)
|
||||
signal updateWidgetSettings(string section, int index, var settings)
|
||||
signal dragPotentialStarted
|
||||
signal dragPotentialEnded
|
||||
|
||||
color: Color.mSurface
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: {
|
||||
var widgetCount = widgetModel.length
|
||||
if (widgetCount === 0)
|
||||
return 140 * scaling
|
||||
|
||||
var availableWidth = parent.width
|
||||
var avgWidgetWidth = 150 * scaling
|
||||
var widgetsPerRow = Math.max(1, Math.floor(availableWidth / avgWidgetWidth))
|
||||
var rows = Math.ceil(widgetCount / widgetsPerRow)
|
||||
|
||||
return (50 + 20 + (rows * 48) + ((rows - 1) * Style.marginS) + 20) * scaling
|
||||
}
|
||||
|
||||
// Generate widget color from name checksum
|
||||
function getWidgetColor(widget) {
|
||||
const totalSum = JSON.stringify(widget).split('').reduce((acc, character) => {
|
||||
return acc + character.charCodeAt(0)
|
||||
}, 0)
|
||||
switch (totalSum % 6) {
|
||||
case 0:
|
||||
return [Color.mPrimary, Color.mOnPrimary]
|
||||
case 1:
|
||||
return [Color.mSecondary, Color.mOnSecondary]
|
||||
case 2:
|
||||
return [Color.mTertiary, Color.mOnTertiary]
|
||||
case 3:
|
||||
return [Color.mError, Color.mOnError]
|
||||
case 4:
|
||||
return [Color.mOnSurface, Color.mSurface]
|
||||
case 5:
|
||||
return [Color.mOnSurfaceVariant, Color.mSurfaceVariant]
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NText {
|
||||
text: sectionName + " Section"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
NComboBox {
|
||||
id: comboBox
|
||||
model: availableWidgets
|
||||
label: ""
|
||||
description: ""
|
||||
placeholder: "Select a widget to add..."
|
||||
onSelected: key => comboBox.currentKey = key
|
||||
popupHeight: 340 * scaling
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "add"
|
||||
|
||||
colorBg: Color.mPrimary
|
||||
colorFg: Color.mOnPrimary
|
||||
colorBgHover: Color.mSecondary
|
||||
colorFgHover: Color.mOnSecondary
|
||||
enabled: comboBox.currentKey !== ""
|
||||
tooltipText: "Add widget to section"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.leftMargin: Style.marginS * scaling
|
||||
onClicked: {
|
||||
if (comboBox.currentKey !== "") {
|
||||
addWidget(comboBox.currentKey, sectionId)
|
||||
comboBox.currentKey = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Drag and Drop Widget Area
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.minimumHeight: 65 * scaling
|
||||
clip: false // Don't clip children so ghost can move freely
|
||||
|
||||
Flow {
|
||||
id: widgetFlow
|
||||
anchors.fill: parent
|
||||
spacing: Style.marginS * scaling
|
||||
flow: Flow.LeftToRight
|
||||
|
||||
Repeater {
|
||||
model: widgetModel
|
||||
delegate: Rectangle {
|
||||
id: widgetItem
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
width: widgetContent.implicitWidth + Style.marginL * scaling
|
||||
height: Style.baseWidgetSize * 1.15 * scaling
|
||||
radius: Style.radiusL * scaling
|
||||
color: root.getWidgetColor(modelData)[0]
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
// Store the widget index for drag operations
|
||||
property int widgetIndex: index
|
||||
readonly property int buttonsWidth: Math.round(20 * scaling)
|
||||
readonly property int buttonsCount: 1 + BarWidgetRegistry.widgetHasUserSettings(modelData.id)
|
||||
|
||||
// Visual feedback during drag
|
||||
opacity: flowDragArea.draggedIndex === index ? 0.5 : 1.0
|
||||
scale: flowDragArea.draggedIndex === index ? 0.95 : 1.0
|
||||
z: flowDragArea.draggedIndex === index ? 1000 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: widgetContent
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.marginXXS * scaling
|
||||
|
||||
NText {
|
||||
text: modelData.id
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: root.getWidgetColor(modelData)[1]
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
elide: Text.ElideRight
|
||||
Layout.preferredWidth: 80 * scaling
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 0
|
||||
Layout.preferredWidth: buttonsCount * buttonsWidth
|
||||
|
||||
Loader {
|
||||
active: BarWidgetRegistry.widgetHasUserSettings(modelData.id)
|
||||
sourceComponent: NIconButton {
|
||||
icon: "settings"
|
||||
sizeRatio: 0.6
|
||||
colorBorder: Qt.alpha(Color.mOutline, Style.opacityLight)
|
||||
colorBg: Color.mOnSurface
|
||||
colorFg: Color.mOnPrimary
|
||||
colorBgHover: Qt.alpha(Color.mOnPrimary, Style.opacityLight)
|
||||
colorFgHover: Color.mOnPrimary
|
||||
onClicked: {
|
||||
var component = Qt.createComponent(Qt.resolvedUrl("BarWidgetSettingsDialog.qml"))
|
||||
function instantiateAndOpen() {
|
||||
var dialog = component.createObject(root, {
|
||||
"widgetIndex": index,
|
||||
"widgetData": modelData,
|
||||
"widgetId": modelData.id,
|
||||
"parent": Overlay.overlay
|
||||
})
|
||||
if (dialog) {
|
||||
dialog.open()
|
||||
} else {
|
||||
Logger.error("BarSectionEditor", "Failed to create settings dialog instance")
|
||||
}
|
||||
}
|
||||
if (component.status === Component.Ready) {
|
||||
instantiateAndOpen()
|
||||
} else if (component.status === Component.Error) {
|
||||
Logger.error("BarSectionEditor", component.errorString())
|
||||
} else {
|
||||
component.statusChanged.connect(function () {
|
||||
if (component.status === Component.Ready) {
|
||||
instantiateAndOpen()
|
||||
} else if (component.status === Component.Error) {
|
||||
Logger.error("BarSectionEditor", component.errorString())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
sizeRatio: 0.6
|
||||
colorBorder: Qt.alpha(Color.mOutline, Style.opacityLight)
|
||||
colorBg: Color.mOnSurface
|
||||
colorFg: Color.mOnPrimary
|
||||
colorBgHover: Qt.alpha(Color.mOnPrimary, Style.opacityLight)
|
||||
colorFgHover: Color.mOnPrimary
|
||||
onClicked: {
|
||||
removeWidget(sectionId, index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ghost/Clone widget for dragging
|
||||
Rectangle {
|
||||
id: dragGhost
|
||||
width: 0
|
||||
height: Style.baseWidgetSize * 1.15 * scaling
|
||||
radius: Style.radiusL * scaling
|
||||
color: Color.transparent
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
opacity: 0.7
|
||||
visible: flowDragArea.dragStarted
|
||||
z: 2000
|
||||
clip: false // Ensure ghost isn't clipped
|
||||
|
||||
Text {
|
||||
id: ghostText
|
||||
anchors.centerIn: parent
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
}
|
||||
|
||||
// Drop indicator - visual feedback for where the widget will be inserted
|
||||
Rectangle {
|
||||
id: dropIndicator
|
||||
width: 3 * scaling
|
||||
height: Style.baseWidgetSize * 1.15 * scaling
|
||||
radius: width / 2
|
||||
color: Color.mPrimary
|
||||
opacity: 0
|
||||
visible: opacity > 0
|
||||
z: 1999
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
id: pulseAnimation
|
||||
running: false
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
to: 1
|
||||
duration: 400
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 0.6
|
||||
duration: 400
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: 100
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: 100
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MouseArea for drag and drop
|
||||
MouseArea {
|
||||
id: flowDragArea
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
|
||||
acceptedButtons: Qt.LeftButton
|
||||
preventStealing: false
|
||||
propagateComposedEvents: false
|
||||
hoverEnabled: true // Always track mouse for drag operations
|
||||
|
||||
property point startPos: Qt.point(0, 0)
|
||||
property bool dragStarted: false
|
||||
property bool potentialDrag: false // Track if we're in a potential drag interaction
|
||||
property int draggedIndex: -1
|
||||
property real dragThreshold: 15 * scaling
|
||||
property Item draggedWidget: null
|
||||
property int dropTargetIndex: -1
|
||||
property var draggedModelData: null
|
||||
|
||||
// Drop position calculation
|
||||
function updateDropIndicator(mouseX, mouseY) {
|
||||
if (!dragStarted || draggedIndex === -1) {
|
||||
dropIndicator.opacity = 0
|
||||
pulseAnimation.running = false
|
||||
return
|
||||
}
|
||||
|
||||
let bestIndex = -1
|
||||
let bestPosition = null
|
||||
let minDistance = Infinity
|
||||
|
||||
// Check position relative to each widget
|
||||
for (var i = 0; i < widgetModel.length; i++) {
|
||||
if (i === draggedIndex)
|
||||
continue
|
||||
|
||||
const widget = widgetFlow.children[i]
|
||||
if (!widget || widget.widgetIndex === undefined)
|
||||
continue
|
||||
|
||||
// Check distance to left edge (insert before)
|
||||
const leftDist = Math.sqrt(Math.pow(mouseX - widget.x, 2) + Math.pow(mouseY - (widget.y + widget.height / 2), 2))
|
||||
|
||||
// Check distance to right edge (insert after)
|
||||
const rightDist = Math.sqrt(Math.pow(mouseX - (widget.x + widget.width), 2) + Math.pow(mouseY - (widget.y + widget.height / 2), 2))
|
||||
|
||||
if (leftDist < minDistance) {
|
||||
minDistance = leftDist
|
||||
bestIndex = i
|
||||
bestPosition = Qt.point(widget.x - dropIndicator.width / 2 - Style.marginXS * scaling, widget.y)
|
||||
}
|
||||
|
||||
if (rightDist < minDistance) {
|
||||
minDistance = rightDist
|
||||
bestIndex = i + 1
|
||||
bestPosition = Qt.point(widget.x + widget.width + Style.marginXS * scaling - dropIndicator.width / 2, widget.y)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we should insert at position 0 (very beginning)
|
||||
if (widgetModel.length > 0 && draggedIndex !== 0) {
|
||||
const firstWidget = widgetFlow.children[0]
|
||||
if (firstWidget) {
|
||||
const dist = Math.sqrt(Math.pow(mouseX, 2) + Math.pow(mouseY - firstWidget.y, 2))
|
||||
if (dist < minDistance && mouseX < firstWidget.x + firstWidget.width / 2) {
|
||||
minDistance = dist
|
||||
bestIndex = 0
|
||||
bestPosition = Qt.point(Math.max(0, firstWidget.x - dropIndicator.width - Style.marginS * scaling), firstWidget.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only show indicator if we're close enough and it's a different position
|
||||
if (minDistance < 80 * scaling && bestIndex !== -1) {
|
||||
// Adjust index if we're moving forward
|
||||
let adjustedIndex = bestIndex
|
||||
if (bestIndex > draggedIndex) {
|
||||
adjustedIndex = bestIndex - 1
|
||||
}
|
||||
|
||||
// Don't show if it's the same position
|
||||
if (adjustedIndex === draggedIndex) {
|
||||
dropIndicator.opacity = 0
|
||||
pulseAnimation.running = false
|
||||
dropTargetIndex = -1
|
||||
return
|
||||
}
|
||||
|
||||
dropTargetIndex = adjustedIndex
|
||||
if (bestPosition) {
|
||||
dropIndicator.x = bestPosition.x
|
||||
dropIndicator.y = bestPosition.y
|
||||
dropIndicator.opacity = 1
|
||||
if (!pulseAnimation.running) {
|
||||
pulseAnimation.running = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dropIndicator.opacity = 0
|
||||
pulseAnimation.running = false
|
||||
dropTargetIndex = -1
|
||||
}
|
||||
}
|
||||
|
||||
onPressed: mouse => {
|
||||
startPos = Qt.point(mouse.x, mouse.y)
|
||||
dragStarted = false
|
||||
potentialDrag = false
|
||||
draggedIndex = -1
|
||||
draggedWidget = null
|
||||
dropTargetIndex = -1
|
||||
draggedModelData = null
|
||||
|
||||
// Find which widget was clicked
|
||||
for (var i = 0; i < widgetModel.length; i++) {
|
||||
const widget = widgetFlow.children[i]
|
||||
if (widget && widget.widgetIndex !== undefined) {
|
||||
if (mouse.x >= widget.x && mouse.x <= widget.x + widget.width && mouse.y >= widget.y && mouse.y <= widget.y + widget.height) {
|
||||
|
||||
const localX = mouse.x - widget.x
|
||||
const buttonsStartX = widget.width - (widget.buttonsCount * widget.buttonsWidth)
|
||||
|
||||
if (localX < buttonsStartX) {
|
||||
// This is a draggable area - prevent panel close immediately
|
||||
draggedIndex = widget.widgetIndex
|
||||
draggedWidget = widget
|
||||
draggedModelData = widget.modelData
|
||||
potentialDrag = true
|
||||
preventStealing = true
|
||||
|
||||
// Signal that interaction started (prevents panel close)
|
||||
root.dragPotentialStarted()
|
||||
break
|
||||
} else {
|
||||
// This is a button area - let the click through
|
||||
mouse.accepted = false
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPositionChanged: mouse => {
|
||||
if (draggedIndex !== -1 && potentialDrag) {
|
||||
const deltaX = mouse.x - startPos.x
|
||||
const deltaY = mouse.y - startPos.y
|
||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
|
||||
|
||||
if (!dragStarted && distance > dragThreshold) {
|
||||
dragStarted = true
|
||||
|
||||
// Setup ghost widget
|
||||
if (draggedWidget) {
|
||||
dragGhost.width = draggedWidget.width
|
||||
dragGhost.color = root.getWidgetColor(draggedModelData)[0]
|
||||
ghostText.text = draggedModelData.id
|
||||
}
|
||||
}
|
||||
|
||||
if (dragStarted) {
|
||||
// Move ghost widget
|
||||
dragGhost.x = mouse.x - dragGhost.width / 2
|
||||
dragGhost.y = mouse.y - dragGhost.height / 2
|
||||
|
||||
// Update drop indicator
|
||||
updateDropIndicator(mouse.x, mouse.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: mouse => {
|
||||
if (dragStarted && dropTargetIndex !== -1 && dropTargetIndex !== draggedIndex) {
|
||||
// Perform the reorder
|
||||
reorderWidget(sectionId, draggedIndex, dropTargetIndex)
|
||||
}
|
||||
|
||||
// Always signal end of interaction if we started one
|
||||
if (potentialDrag) {
|
||||
root.dragPotentialEnded()
|
||||
}
|
||||
|
||||
// Reset everything
|
||||
dragStarted = false
|
||||
potentialDrag = false
|
||||
draggedIndex = -1
|
||||
draggedWidget = null
|
||||
dropTargetIndex = -1
|
||||
draggedModelData = null
|
||||
preventStealing = false
|
||||
dropIndicator.opacity = 0
|
||||
pulseAnimation.running = false
|
||||
dragGhost.width = 0
|
||||
}
|
||||
|
||||
onExited: {
|
||||
if (dragStarted) {
|
||||
// Hide drop indicator when mouse leaves, but keep ghost visible
|
||||
dropIndicator.opacity = 0
|
||||
pulseAnimation.running = false
|
||||
}
|
||||
}
|
||||
|
||||
onCanceled: {
|
||||
// Handle cancel (e.g., ESC key pressed during drag)
|
||||
if (potentialDrag) {
|
||||
root.dragPotentialEnded()
|
||||
}
|
||||
|
||||
// Reset everything
|
||||
dragStarted = false
|
||||
potentialDrag = false
|
||||
draggedIndex = -1
|
||||
draggedWidget = null
|
||||
dropTargetIndex = -1
|
||||
draggedModelData = null
|
||||
preventStealing = false
|
||||
dropIndicator.opacity = 0
|
||||
pulseAnimation.running = false
|
||||
dragGhost.width = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
136
Modules/SettingsPanel/Bar/BarWidgetSettingsDialog.qml
Normal file
@@ -0,0 +1,136 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
import "./WidgetSettings" as WidgetSettings
|
||||
|
||||
// Widget Settings Dialog Component
|
||||
Popup {
|
||||
id: settingsPopup
|
||||
|
||||
property int widgetIndex: -1
|
||||
property var widgetData: null
|
||||
property string widgetId: ""
|
||||
|
||||
// Center popup in parent
|
||||
x: (parent.width - width) * 0.5
|
||||
y: (parent.height - height) * 0.5
|
||||
|
||||
width: 500 * scaling
|
||||
height: content.implicitHeight + padding * 2
|
||||
padding: Style.marginXL * scaling
|
||||
modal: true
|
||||
|
||||
background: Rectangle {
|
||||
id: bgRect
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusL * scaling
|
||||
border.color: Color.mPrimary
|
||||
border.width: Style.borderM * scaling
|
||||
}
|
||||
|
||||
// Load settings when popup opens with data
|
||||
onOpened: {
|
||||
if (widgetData && widgetId) {
|
||||
loadWidgetSettings()
|
||||
}
|
||||
}
|
||||
|
||||
function loadWidgetSettings() {
|
||||
const widgetSettingsMap = {
|
||||
"ActiveWindow": "WidgetSettings/ActiveWindowSettings.qml",
|
||||
"Battery": "WidgetSettings/BatterySettings.qml",
|
||||
"Brightness": "WidgetSettings/BrightnessSettings.qml",
|
||||
"Clock": "WidgetSettings/ClockSettings.qml",
|
||||
"CustomButton": "WidgetSettings/CustomButtonSettings.qml",
|
||||
"KeyboardLayout": "WidgetSettings/KeyboardLayoutSettings.qml",
|
||||
"MediaMini": "WidgetSettings/MediaMiniSettings.qml",
|
||||
"Microphone": "WidgetSettings/MicrophoneSettings.qml",
|
||||
"NotificationHistory": "WidgetSettings/NotificationHistorySettings.qml",
|
||||
"Workspace": "WidgetSettings/WorkspaceSettings.qml",
|
||||
"SidePanelToggle": "WidgetSettings/SidePanelToggleSettings.qml",
|
||||
"Spacer": "WidgetSettings/SpacerSettings.qml",
|
||||
"SystemMonitor": "WidgetSettings/SystemMonitorSettings.qml",
|
||||
"Volume": "WidgetSettings/VolumeSettings.qml"
|
||||
}
|
||||
|
||||
const source = widgetSettingsMap[widgetId]
|
||||
if (source) {
|
||||
// Use setSource to pass properties at creation time
|
||||
settingsLoader.setSource(source, {
|
||||
"widgetData": widgetData,
|
||||
"widgetMetadata": BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
width: parent.width
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Title
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NText {
|
||||
text: `${settingsPopup.widgetId} Settings`
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
onClicked: settingsPopup.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Separator
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: Color.mOutline
|
||||
}
|
||||
|
||||
// Settings based on widget type
|
||||
// Will be triggered via settingsLoader.setSource()
|
||||
Loader {
|
||||
id: settingsLoader
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginM * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: "Cancel"
|
||||
outlined: true
|
||||
onClicked: settingsPopup.close()
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: "Apply"
|
||||
icon: "check"
|
||||
onClicked: {
|
||||
if (settingsLoader.item && settingsLoader.item.saveSettings) {
|
||||
var newSettings = settingsLoader.item.saveSettings()
|
||||
root.updateWidgetSettings(sectionId, settingsPopup.widgetIndex, newSettings)
|
||||
settingsPopup.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
// Local state
|
||||
property bool valueShowIcon: widgetData.showIcon !== undefined ? widgetData.showIcon : widgetMetadata.showIcon
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.showIcon = valueShowIcon
|
||||
return settings
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: showIcon
|
||||
Layout.fillWidth: true
|
||||
label: "Show app icon"
|
||||
checked: root.valueShowIcon
|
||||
onToggled: checked => root.valueShowIcon = checked
|
||||
}
|
||||
}
|
||||
58
Modules/SettingsPanel/Bar/WidgetSettings/BatterySettings.qml
Normal file
@@ -0,0 +1,58 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
// Local state
|
||||
property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
|
||||
property int valueWarningThreshold: widgetData.warningThreshold !== undefined ? widgetData.warningThreshold : widgetMetadata.warningThreshold
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.displayMode = valueDisplayMode
|
||||
settings.warningThreshold = valueWarningThreshold
|
||||
return settings
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: "Display mode"
|
||||
description: "Choose how you'd like this value to appear."
|
||||
minimumWidth: 134 * scaling
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
key: "onhover"
|
||||
name: "On Hover"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysShow"
|
||||
name: "Always Show"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysHide"
|
||||
name: "Always Hide"
|
||||
}
|
||||
}
|
||||
currentKey: root.valueDisplayMode
|
||||
onSelected: key => root.valueDisplayMode = key
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
label: "Low battery warning threshold"
|
||||
description: "Show a warning when battery falls below this percentage."
|
||||
value: valueWarningThreshold
|
||||
suffix: "%"
|
||||
minimum: 5
|
||||
maximum: 50
|
||||
onValueChanged: valueWarningThreshold = value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
// Local state
|
||||
property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.displayMode = valueDisplayMode
|
||||
return settings
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: "Display mode"
|
||||
description: "Choose how you'd like this value to appear."
|
||||
minimumWidth: 134 * scaling
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
key: "onhover"
|
||||
name: "On Hover"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysShow"
|
||||
name: "Always Show"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysHide"
|
||||
name: "Always Hide"
|
||||
}
|
||||
}
|
||||
currentKey: valueDisplayMode
|
||||
onSelected: key => valueDisplayMode = key
|
||||
}
|
||||
}
|
||||
65
Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml
Normal file
@@ -0,0 +1,65 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
// Local state
|
||||
property string valueDisplayFormat: widgetData.displayFormat !== undefined ? widgetData.displayFormat : widgetMetadata.displayFormat
|
||||
property bool valueUse12h: widgetData.use12HourClock !== undefined ? widgetData.use12HourClock : widgetMetadata.use12HourClock
|
||||
property bool valueReverseDayMonth: widgetData.reverseDayMonth !== undefined ? widgetData.reverseDayMonth : widgetMetadata.reverseDayMonth
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.displayFormat = valueDisplayFormat
|
||||
settings.use12HourClock = valueUse12h
|
||||
settings.reverseDayMonth = valueReverseDayMonth
|
||||
return settings
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: "Display Format"
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
key: "time"
|
||||
name: "HH:mm"
|
||||
}
|
||||
ListElement {
|
||||
key: "time-seconds"
|
||||
name: "HH:mm:ss"
|
||||
}
|
||||
ListElement {
|
||||
key: "time-date"
|
||||
name: "HH:mm - Date"
|
||||
}
|
||||
ListElement {
|
||||
key: "time-date-short"
|
||||
name: "HH:mm - Short Date"
|
||||
}
|
||||
}
|
||||
currentKey: valueDisplayFormat
|
||||
onSelected: key => valueDisplayFormat = key
|
||||
minimumWidth: 230 * scaling
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Use 12-hour clock"
|
||||
checked: valueUse12h
|
||||
onToggled: checked => valueUse12h = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Reverse day and month"
|
||||
checked: valueReverseDayMonth
|
||||
onToggled: checked => valueReverseDayMonth = checked
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.icon = iconInput.text
|
||||
settings.leftClickExec = leftClickExecInput.text
|
||||
settings.rightClickExec = rightClickExecInput.text
|
||||
settings.middleClickExec = middleClickExecInput.text
|
||||
return settings
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: iconInput
|
||||
Layout.fillWidth: true
|
||||
label: "Icon Name"
|
||||
description: "Select an icon from the library."
|
||||
placeholderText: "Enter icon name (e.g., cat, gear, house, ...)"
|
||||
text: widgetData?.icon || widgetMetadata.icon
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginS * scaling
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
icon: iconInput.text
|
||||
visible: iconInput.text !== ""
|
||||
}
|
||||
NButton {
|
||||
text: "Browse"
|
||||
onClicked: iconPicker.open()
|
||||
}
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: iconPicker
|
||||
modal: true
|
||||
width: {
|
||||
var w = Math.round(Math.max(Screen.width * 0.35, 900) * scaling)
|
||||
w = Math.min(w, Screen.width - Style.marginL * 2)
|
||||
return w
|
||||
}
|
||||
height: {
|
||||
var h = Math.round(Math.max(Screen.height * 0.65, 700) * scaling)
|
||||
h = Math.min(h, Screen.height - Style.barHeight * scaling - Style.marginL * 2)
|
||||
return h
|
||||
}
|
||||
anchors.centerIn: Overlay.overlay
|
||||
padding: Style.marginXL * scaling
|
||||
|
||||
property string query: ""
|
||||
property string selectedIcon: ""
|
||||
property var allIcons: Object.keys(Icons.icons)
|
||||
property var filteredIcons: allIcons.filter(function (name) {
|
||||
return query === "" || name.toLowerCase().indexOf(query.toLowerCase()) !== -1
|
||||
})
|
||||
readonly property int columns: 6
|
||||
readonly property int cellW: Math.floor(grid.width / columns)
|
||||
readonly property int cellH: Math.round(cellW * 0.7 + 36 * scaling)
|
||||
|
||||
background: Rectangle {
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusL * scaling
|
||||
border.color: Color.mPrimary
|
||||
border.width: Style.borderM * scaling
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Title row
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
NText {
|
||||
text: "Icon Picker"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
onClicked: iconPicker.close()
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
NTextInput {
|
||||
Layout.fillWidth: true
|
||||
label: "Search"
|
||||
placeholderText: "Search (e.g., arrow, battery, cloud)"
|
||||
text: iconPicker.query
|
||||
onTextChanged: iconPicker.query = text.trim().toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
// Icon grid
|
||||
NScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AlwaysOn
|
||||
|
||||
GridView {
|
||||
id: grid
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM * scaling
|
||||
cellWidth: iconPicker.cellW
|
||||
cellHeight: iconPicker.cellH
|
||||
model: iconPicker.filteredIcons
|
||||
delegate: Rectangle {
|
||||
width: grid.cellWidth
|
||||
height: grid.cellHeight
|
||||
radius: Style.radiusS * scaling
|
||||
clip: true
|
||||
color: (iconPicker.selectedIcon === modelData) ? Qt.alpha(Color.mPrimary, 0.15) : "transparent"
|
||||
border.color: (iconPicker.selectedIcon === modelData) ? Color.mPrimary : Qt.rgba(0, 0, 0, 0)
|
||||
border.width: (iconPicker.selectedIcon === modelData) ? Style.borderS * scaling : 0
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: iconPicker.selectedIcon = modelData
|
||||
onDoubleClicked: {
|
||||
iconPicker.selectedIcon = modelData
|
||||
iconInput.text = iconPicker.selectedIcon
|
||||
iconPicker.close()
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredHeight: 4 * scaling
|
||||
}
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
icon: modelData
|
||||
font.pointSize: 42 * scaling
|
||||
}
|
||||
NText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginXS * scaling
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
maximumLineCount: 1
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: Color.mOnSurfaceVariant
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
text: modelData
|
||||
}
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
NButton {
|
||||
text: "Cancel"
|
||||
outlined: true
|
||||
onClicked: iconPicker.close()
|
||||
}
|
||||
NButton {
|
||||
text: "Apply"
|
||||
icon: "check"
|
||||
enabled: iconPicker.selectedIcon !== ""
|
||||
onClicked: {
|
||||
iconInput.text = iconPicker.selectedIcon
|
||||
iconPicker.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: leftClickExecInput
|
||||
Layout.fillWidth: true
|
||||
label: "Left Click Command"
|
||||
placeholderText: "Enter command to execute (app or custom script)"
|
||||
text: widgetData?.leftClickExec || widgetMetadata.leftClickExec
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: rightClickExecInput
|
||||
Layout.fillWidth: true
|
||||
label: "Right Click Command"
|
||||
placeholderText: "Enter command to execute (app or custom script)"
|
||||
text: widgetData?.rightClickExec || widgetMetadata.rightClickExec
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: middleClickExecInput
|
||||
Layout.fillWidth: true
|
||||
label: "Middle Click Command"
|
||||
placeholderText: "Enter command to execute (app or custom script)"
|
||||
text: widgetData.middleClickExec || widgetMetadata.middleClickExec
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
// Local state
|
||||
property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.displayMode = valueDisplayMode
|
||||
return settings
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: "Display mode"
|
||||
description: "Choose how you'd like this value to appear."
|
||||
minimumWidth: 134 * scaling
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
key: "onhover"
|
||||
name: "On Hover"
|
||||
}
|
||||
ListElement {
|
||||
key: "forceOpen"
|
||||
name: "Force Open"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysHide"
|
||||
name: "Always Hide"
|
||||
}
|
||||
}
|
||||
currentKey: valueDisplayMode
|
||||
onSelected: key => valueDisplayMode = key
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
// Local state
|
||||
property bool valueShowAlbumArt: widgetData.showAlbumArt !== undefined ? widgetData.showAlbumArt : widgetMetadata.showAlbumArt
|
||||
property bool valueShowVisualizer: widgetData.showVisualizer !== undefined ? widgetData.showVisualizer : widgetMetadata.showVisualizer
|
||||
property string valueVisualizerType: widgetData.visualizerType || widgetMetadata.visualizerType
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.showAlbumArt = valueShowAlbumArt
|
||||
settings.showVisualizer = valueShowVisualizer
|
||||
settings.visualizerType = valueVisualizerType
|
||||
return settings
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Show album art"
|
||||
checked: valueShowAlbumArt
|
||||
onToggled: checked => valueShowAlbumArt = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Show visualizer"
|
||||
checked: valueShowVisualizer
|
||||
onToggled: checked => valueShowVisualizer = checked
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
visible: valueShowVisualizer
|
||||
label: "Visualizer type"
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
key: "linear"
|
||||
name: "Linear"
|
||||
}
|
||||
ListElement {
|
||||
key: "mirrored"
|
||||
name: "Mirrored"
|
||||
}
|
||||
ListElement {
|
||||
key: "wave"
|
||||
name: "Wave"
|
||||
}
|
||||
}
|
||||
currentKey: valueVisualizerType
|
||||
onSelected: key => valueVisualizerType = key
|
||||
minimumWidth: 200 * scaling
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
// Local state
|
||||
property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.displayMode = valueDisplayMode
|
||||
return settings
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: "Display mode"
|
||||
description: "Choose how you'd like this value to appear."
|
||||
minimumWidth: 134 * scaling
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
key: "onhover"
|
||||
name: "On Hover"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysShow"
|
||||
name: "Always Show"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysHide"
|
||||
name: "Always Hide"
|
||||
}
|
||||
}
|
||||
currentKey: valueDisplayMode
|
||||
onSelected: key => valueDisplayMode = key
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
// Local state
|
||||
property bool valueShowUnreadBadge: widgetData.showUnreadBadge !== undefined ? widgetData.showUnreadBadge : widgetMetadata.showUnreadBadge
|
||||
property bool valueHideWhenZero: widgetData.hideWhenZero !== undefined ? widgetData.hideWhenZero : widgetMetadata.hideWhenZero
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.showUnreadBadge = valueShowUnreadBadge
|
||||
settings.hideWhenZero = valueHideWhenZero
|
||||
return settings
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Show unread badge"
|
||||
checked: valueShowUnreadBadge
|
||||
onToggled: checked => valueShowUnreadBadge = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Hide badge when zero"
|
||||
checked: valueHideWhenZero
|
||||
onToggled: checked => valueHideWhenZero = checked
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
// Local state
|
||||
property bool valueUseDistroLogo: widgetData.useDistroLogo !== undefined ? widgetData.useDistroLogo : widgetMetadata.useDistroLogo
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.useDistroLogo = valueUseDistroLogo
|
||||
return settings
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Use distro logo instead of icon"
|
||||
checked: valueUseDistroLogo
|
||||
onToggled: checked => valueUseDistroLogo = checked
|
||||
}
|
||||
}
|
||||
30
Modules/SettingsPanel/Bar/WidgetSettings/SpacerSettings.qml
Normal file
@@ -0,0 +1,30 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.width = parseInt(widthInput.text) || widgetMetadata.width
|
||||
return settings
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: widthInput
|
||||
Layout.fillWidth: true
|
||||
label: "Width"
|
||||
description: "Spacing width in pixels"
|
||||
text: widgetData.width || widgetMetadata.width
|
||||
placeholderText: "Enter width in pixels"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
// Local, editable state for checkboxes
|
||||
property bool valueShowCpuUsage: widgetData.showCpuUsage !== undefined ? widgetData.showCpuUsage : widgetMetadata.showCpuUsage
|
||||
property bool valueShowCpuTemp: widgetData.showCpuTemp !== undefined ? widgetData.showCpuTemp : widgetMetadata.showCpuTemp
|
||||
property bool valueShowMemoryUsage: widgetData.showMemoryUsage !== undefined ? widgetData.showMemoryUsage : widgetMetadata.showMemoryUsage
|
||||
property bool valueShowMemoryAsPercent: widgetData.showMemoryAsPercent !== undefined ? widgetData.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent
|
||||
property bool valueShowNetworkStats: widgetData.showNetworkStats !== undefined ? widgetData.showNetworkStats : widgetMetadata.showNetworkStats
|
||||
property bool valueShowDiskUsage: widgetData.showDiskUsage !== undefined ? widgetData.showDiskUsage : widgetMetadata.showDiskUsage
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.showCpuUsage = valueShowCpuUsage
|
||||
settings.showCpuTemp = valueShowCpuTemp
|
||||
settings.showMemoryUsage = valueShowMemoryUsage
|
||||
settings.showMemoryAsPercent = valueShowMemoryAsPercent
|
||||
settings.showNetworkStats = valueShowNetworkStats
|
||||
settings.showDiskUsage = valueShowDiskUsage
|
||||
return settings
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: showCpuUsage
|
||||
Layout.fillWidth: true
|
||||
label: "CPU usage"
|
||||
checked: valueShowCpuUsage
|
||||
onToggled: checked => valueShowCpuUsage = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: showCpuTemp
|
||||
Layout.fillWidth: true
|
||||
label: "CPU temperature"
|
||||
checked: valueShowCpuTemp
|
||||
onToggled: checked => valueShowCpuTemp = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: showMemoryUsage
|
||||
Layout.fillWidth: true
|
||||
label: "Memory usage"
|
||||
checked: valueShowMemoryUsage
|
||||
onToggled: checked => valueShowMemoryUsage = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: showMemoryAsPercent
|
||||
Layout.fillWidth: true
|
||||
label: "Memory as percentage"
|
||||
checked: valueShowMemoryAsPercent
|
||||
onToggled: checked => valueShowMemoryAsPercent = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: showNetworkStats
|
||||
Layout.fillWidth: true
|
||||
label: "Network traffic"
|
||||
checked: valueShowNetworkStats
|
||||
onToggled: checked => valueShowNetworkStats = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: showDiskUsage
|
||||
Layout.fillWidth: true
|
||||
label: "Storage usage"
|
||||
checked: valueShowDiskUsage
|
||||
onToggled: checked => valueShowDiskUsage = checked
|
||||
}
|
||||
}
|
||||
46
Modules/SettingsPanel/Bar/WidgetSettings/VolumeSettings.qml
Normal file
@@ -0,0 +1,46 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
// Local state
|
||||
property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.displayMode = valueDisplayMode
|
||||
return settings
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: "Display mode"
|
||||
description: "Choose how you'd like this value to appear."
|
||||
minimumWidth: 134 * scaling
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
key: "onhover"
|
||||
name: "On Hover"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysShow"
|
||||
name: "Always Show"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysHide"
|
||||
name: "Always Hide"
|
||||
}
|
||||
}
|
||||
currentKey: valueDisplayMode
|
||||
onSelected: key => valueDisplayMode = key
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.labelMode = labelModeCombo.currentKey
|
||||
settings.hideUnoccupied = hideUnoccupiedToggle.checked
|
||||
return settings
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
id: labelModeCombo
|
||||
|
||||
label: "Label Mode"
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
key: "none"
|
||||
name: "None"
|
||||
}
|
||||
ListElement {
|
||||
key: "index"
|
||||
name: "Index"
|
||||
}
|
||||
ListElement {
|
||||
key: "name"
|
||||
name: "Name"
|
||||
}
|
||||
}
|
||||
currentKey: widgetData.labelMode || widgetMetadata.labelMode
|
||||
onSelected: key => labelModeCombo.currentKey = key
|
||||
minimumWidth: 200 * scaling
|
||||
}
|
||||
|
||||
NToggle {
|
||||
id: hideUnoccupiedToggle
|
||||
label: "Hide unoccupied"
|
||||
description: "Don't display workspaces without windows."
|
||||
checked: widgetData.hideUnoccupied
|
||||
onToggled: checked => hideUnoccupiedToggle.checked = checked
|
||||
}
|
||||
}
|
||||