Compare commits
997 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
860e721709 | ||
|
|
88ece93db2 | ||
|
|
2d290bf5f7 | ||
|
|
891c8660e3 | ||
|
|
a734235cd0 | ||
|
|
8fdc6a0f72 | ||
|
|
603f499355 | ||
|
|
2b8b97ab3b | ||
|
|
458ef3c0d5 | ||
|
|
c4008e3899 | ||
|
|
6c3299ad10 | ||
|
|
6fe498ce19 | ||
|
|
4e67f26576 | ||
|
|
b2d46ab759 | ||
|
|
0d3cc917fa | ||
|
|
ac591da6c5 | ||
|
|
c7709b5f21 | ||
|
|
e6370904cd | ||
|
|
e412cee52f | ||
|
|
c3019230ae | ||
|
|
c7ab350cbd | ||
|
|
b65d82d895 | ||
|
|
89eb5ecde6 | ||
|
|
b374f167ef | ||
|
|
28026a4c37 | ||
|
|
b8bce3d421 | ||
|
|
6fba3457f7 | ||
|
|
07a6a16011 | ||
|
|
6b61599633 | ||
|
|
1bd093db7f | ||
|
|
3d9295856c | ||
|
|
a1aabd02f5 | ||
|
|
ae2d3eddd6 | ||
|
|
b75c358f54 | ||
|
|
0972a55aad | ||
|
|
112f71b633 | ||
|
|
e67d7166de | ||
|
|
6e88118ca9 | ||
|
|
75b7f0fcb0 | ||
|
|
47f72d9498 | ||
|
|
85d7dc2506 | ||
|
|
1305efec24 | ||
|
|
8af8bf2e2e | ||
|
|
abd6a66297 | ||
|
|
2e9a812513 | ||
|
|
8d845e7cd0 | ||
|
|
a1dcef8dec | ||
|
|
38e0bb8e64 | ||
|
|
8811cb3d13 | ||
|
|
a872682eb8 | ||
|
|
46b8317330 | ||
|
|
8204460112 | ||
|
|
292337dc00 | ||
|
|
0b790c219d | ||
|
|
7acca17b83 | ||
|
|
166da9191e | ||
|
|
de6b7c6470 | ||
|
|
a92b4b311a | ||
|
|
3a6bf8d299 | ||
|
|
cdca7c1d83 | ||
|
|
6f1ae43d62 | ||
|
|
eb26aa10f7 | ||
|
|
cdfb110007 | ||
|
|
b7d8f92414 | ||
|
|
b625df6484 | ||
|
|
2c3eb6efda | ||
|
|
8e034cd912 | ||
|
|
03bdfdb340 | ||
|
|
95d2dbe3fc | ||
|
|
071100459f | ||
|
|
0da59954cd | ||
|
|
2e63f93d41 | ||
|
|
c6ee99375d | ||
|
|
ed6562475d | ||
|
|
a2caebb8e5 | ||
|
|
03698e7bb9 | ||
|
|
d8db086127 | ||
|
|
339505abe3 | ||
|
|
b52451fde5 | ||
|
|
ac1902c76a | ||
|
|
93c674f356 | ||
|
|
5f3add5d99 | ||
|
|
937675ebb3 | ||
|
|
47ef62beb3 | ||
|
|
593a0bfc2c | ||
|
|
abe51f4928 | ||
|
|
33a75d042d | ||
|
|
2a62a13b16 | ||
|
|
4edeedd5ad | ||
|
|
8a17c047c9 | ||
|
|
899595ec5c | ||
|
|
dbf1020636 | ||
|
|
7def695c0e | ||
|
|
b51a87a981 | ||
|
|
3da2682111 | ||
|
|
758f2f2e55 | ||
|
|
26a27c3393 | ||
|
|
9d9bfb54e1 | ||
|
|
ab5b1e4d82 | ||
|
|
8cb9f04a22 | ||
|
|
22bc5a3bff | ||
|
|
f5982f41a2 | ||
|
|
9a80d51b10 | ||
|
|
fa838ecdb1 | ||
|
|
c0d6780c3d | ||
|
|
8935f9a0f9 | ||
|
|
b8b97c46a0 | ||
|
|
f2bbf70f93 | ||
|
|
ecf468f78f | ||
|
|
0b35fc1d2d | ||
|
|
94c5d73a61 | ||
|
|
f399a6d9f5 | ||
|
|
44fd859aec | ||
|
|
53d0c3943d | ||
|
|
d80f923802 | ||
|
|
1b861d7b7b | ||
|
|
65933208ec | ||
|
|
5df218a789 | ||
|
|
2f7a834b55 | ||
|
|
eca301553e | ||
|
|
91efa38101 | ||
|
|
acfe94f736 | ||
|
|
97bfcbb9e8 | ||
|
|
9e47d91be2 | ||
|
|
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 | ||
|
|
b9ae772987 | ||
|
|
4265290a0f | ||
|
|
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
|
||||
@@ -4,7 +4,7 @@
|
||||
"mOnPrimary": "#11111b",
|
||||
"mSecondary": "#fab387",
|
||||
"mOnSecondary": "#11111b",
|
||||
"mTertiary": "#a6e3a1",
|
||||
"mTertiary": "#94e2d5",
|
||||
"mOnTertiary": "#11111b",
|
||||
"mError": "#f38ba8",
|
||||
"mOnError": "#11111b",
|
||||
@@ -16,19 +16,19 @@
|
||||
"mShadow": "#11111b"
|
||||
},
|
||||
"light": {
|
||||
"mPrimary": "#9349ef",
|
||||
"mPrimary": "#8839ef",
|
||||
"mOnPrimary": "#eff1f5",
|
||||
"mSecondary": "#f67525",
|
||||
"mSecondary": "#fe640b",
|
||||
"mOnSecondary": "#eff1f5",
|
||||
"mTertiary": "#40b635",
|
||||
"mTertiary": "#40a02b",
|
||||
"mOnTertiary": "#eff1f5",
|
||||
"mError": "#f38ba8",
|
||||
"mOnError": "#11111b",
|
||||
"mError": "#d20f39",
|
||||
"mOnError": "#dce0e8",
|
||||
"mSurface": "#eff1f5",
|
||||
"mOnSurface": "#4c4f69",
|
||||
"mSurfaceVariant": "#ccd0da",
|
||||
"mOnSurfaceVariant": "#6c6f85",
|
||||
"mOutline": "#aeb5c4",
|
||||
"mOutline": "#a5adcb",
|
||||
"mShadow": "#dce0e8"
|
||||
}
|
||||
}
|
||||
|
||||
34
Assets/ColorScheme/Everforest.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"dark": {
|
||||
"mPrimary": "#D3C6AA",
|
||||
"mOnPrimary": "#232A2E",
|
||||
"mSecondary": "#D3C6AA",
|
||||
"mOnSecondary": "#232A2E",
|
||||
"mTertiary": "#9DA9A0",
|
||||
"mOnTertiary": "#232A2E",
|
||||
"mError": "#E67E80",
|
||||
"mOnError": "#232A2E",
|
||||
"mSurface": "#232A2E",
|
||||
"mOnSurface": "#859289",
|
||||
"mSurfaceVariant": "#2D353B",
|
||||
"mOnSurfaceVariant": "#D3C6AA",
|
||||
"mOutline": "#D3C6AA",
|
||||
"mShadow": "#475258"
|
||||
},
|
||||
"light": {
|
||||
"mPrimary": "#434F55",
|
||||
"mOnPrimary": "#D3C6AA",
|
||||
"mSecondary": "#232a2e",
|
||||
"mOnSecondary": "#D3C6AA",
|
||||
"mTertiary": "#333c43",
|
||||
"mOnTertiary": "#9DA9A0",
|
||||
"mError": "#E66868",
|
||||
"mOnError": "#9DA9A0",
|
||||
"mSurface": "#BEC5B2",
|
||||
"mOnSurface": "#333C43",
|
||||
"mSurfaceVariant": "#9DA9A0",
|
||||
"mOnSurfaceVariant": "#232A2E",
|
||||
"mOutline": "#232A2E",
|
||||
"mShadow": "#ECF5ED"
|
||||
}
|
||||
}
|
||||
34
Assets/ColorScheme/Kanagawa.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"dark": {
|
||||
"mPrimary": "#76946a",
|
||||
"mOnPrimary": "#1f1f28",
|
||||
"mSecondary": "#c0a36e",
|
||||
"mOnSecondary": "#1f1f28",
|
||||
"mTertiary": "#7e9cd8",
|
||||
"mOnTertiary": "#1f1f28",
|
||||
"mError": "#c34043",
|
||||
"mOnError": "#1f1f28",
|
||||
"mSurface": "#1f1f28",
|
||||
"mOnSurface": "#717c7c",
|
||||
"mSurfaceVariant": "#2a2a37",
|
||||
"mOnSurfaceVariant": "#c8c093",
|
||||
"mOutline": "#363646",
|
||||
"mShadow": "#1f1f28"
|
||||
},
|
||||
"light": {
|
||||
"mPrimary": "#6f894e",
|
||||
"mOnPrimary": "#f2ecbc",
|
||||
"mSecondary": "#77713f",
|
||||
"mOnSecondary": "#f2ecbc",
|
||||
"mTertiary": "#4d699b",
|
||||
"mOnTertiary": "#f2ecbc",
|
||||
"mError": "#c84053",
|
||||
"mOnError": "#f2ecbc",
|
||||
"mSurface": "#f2ecbc",
|
||||
"mOnSurface": "#8a8980",
|
||||
"mSurfaceVariant": "#e5ddb0",
|
||||
"mOnSurfaceVariant": "#545464",
|
||||
"mOutline": "#cfc49c",
|
||||
"mShadow": "#f2ecbc"
|
||||
}
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
{
|
||||
"dark": {
|
||||
"mPrimary": "#ff9e64",
|
||||
"mOnPrimary": "#1a1b26",
|
||||
"mSecondary": "#e0af68",
|
||||
"mOnSecondary": "#1a1b26",
|
||||
"mTertiary": "#7aa2f7",
|
||||
"mOnTertiary": "#1a1b26",
|
||||
"mPrimary": "#7aa2f7",
|
||||
"mOnPrimary": "#16161e",
|
||||
"mSecondary": "#bb9af7",
|
||||
"mOnSecondary": "#16161e",
|
||||
"mTertiary": "#9ece6a",
|
||||
"mOnTertiary": "#16161e",
|
||||
"mError": "#f7768e",
|
||||
"mOnError": "#1a1b26",
|
||||
"mOnError": "#16161e",
|
||||
"mSurface": "#1a1b26",
|
||||
"mOnSurface": "#a9b1d6",
|
||||
"mSurfaceVariant": "#292e42",
|
||||
"mOnSurfaceVariant": "#787c99",
|
||||
"mOutline": "#3d4462",
|
||||
"mShadow": "#1a1b26"
|
||||
"mOnSurface": "#c0caf5",
|
||||
"mSurfaceVariant": "#24283b",
|
||||
"mOnSurfaceVariant": "#9aa5ce",
|
||||
"mOutline": "#565f89",
|
||||
"mShadow": "#15161e"
|
||||
},
|
||||
"light": {
|
||||
"mPrimary": "#fd5d00",
|
||||
"mOnPrimary": "#e6e7ed",
|
||||
"mSecondary": "#bb8027",
|
||||
"mOnSecondary": "#e6e7ed",
|
||||
"mTertiary": "#4a80f4",
|
||||
"mOnTertiary": "#e6e7ed",
|
||||
"mError": "#965027",
|
||||
"mOnError": "#e6e7ed",
|
||||
"mSurface": "#e6e7ed",
|
||||
"mOnSurface": "#343b58",
|
||||
"mSurfaceVariant": "#d5d6db",
|
||||
"mOnSurfaceVariant": "#40434f",
|
||||
"mOutline": "#babbc3",
|
||||
"mShadow": "#c0caf5"
|
||||
"mPrimary": "#2e7de9",
|
||||
"mOnPrimary": "#e1e2e7",
|
||||
"mSecondary": "#9854f1",
|
||||
"mOnSecondary": "#e1e2e7",
|
||||
"mTertiary": "#587539",
|
||||
"mOnTertiary": "#e1e2e7",
|
||||
"mError": "#f52a65",
|
||||
"mOnError": "#e1e2e7",
|
||||
"mSurface": "#e1e2e7",
|
||||
"mOnSurface": "#3760bf",
|
||||
"mSurfaceVariant": "#d0d5e3",
|
||||
"mOnSurfaceVariant": "#6172b0",
|
||||
"mOutline": "#b4b5b9",
|
||||
"mShadow": "#a8aecb"
|
||||
}
|
||||
}
|
||||
|
||||
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
82
Assets/Matugen/Matugen.qml
Normal file
@@ -0,0 +1,82 @@
|
||||
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"')
|
||||
}
|
||||
if (Settings.data.matugen.pywalfox) {
|
||||
lines.push("\n[templates.pywalfox]")
|
||||
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/pywalfox.json"')
|
||||
lines.push('output_path = "~/.cache/wal/colors.json"')
|
||||
lines.push('post_hook = "pywalfox update"')
|
||||
}
|
||||
|
||||
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}}
|
||||
|
||||
22
Assets/Matugen/templates/pywalfox.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"wallpaper": "{{image}}",
|
||||
"alpha": "100",
|
||||
"colors": {
|
||||
"color0": "{{colors.background.default.hex}}",
|
||||
"color1": "",
|
||||
"color2": "",
|
||||
"color3": "",
|
||||
"color4": "",
|
||||
"color5": "",
|
||||
"color6": "",
|
||||
"color7": "",
|
||||
"color8": "",
|
||||
"color9": "",
|
||||
"color10": "{{colors.primary.default.hex}}",
|
||||
"color11": "",
|
||||
"color12": "",
|
||||
"color13": "{{colors.surface_bright.default.hex}}",
|
||||
"color14": "",
|
||||
"color15": "{{colors.on_surface.default.hex}}"
|
||||
}
|
||||
}
|
||||
113
Assets/Matugen/templates/vesktop.css
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* @name noctalia
|
||||
* @description Original theme: midnight | A dark, rounded discord theme.
|
||||
* @author refact0r
|
||||
* @version 1.6.2
|
||||
* @invite nz87hXyvcy
|
||||
* @website https://github.com/refact0r/midnight-discord
|
||||
* @source https://github.com/refact0r/midnight-discord/blob/master/midnight.theme.css
|
||||
* @authorId 508863359777505290
|
||||
* @authorLink https://www.refact0r.dev
|
||||
*/
|
||||
|
||||
/* IMPORTANT: make sure to enable dark mode in discord settings for the theme to apply properly!!! */
|
||||
|
||||
@import url('https://refact0r.github.io/midnight-discord/build/midnight.css');
|
||||
|
||||
/* customize things here */
|
||||
:root {
|
||||
/* font, change to 'gg sans' for default discord font*/
|
||||
--font: 'figtree';
|
||||
|
||||
/* top left corner text */
|
||||
--corner-text: 'Midnight';
|
||||
|
||||
/* color of status indicators and window controls */
|
||||
--online-indicator: {{colors.inverse_primary.default.hex}}; /* change to #23a55a for default green */
|
||||
--dnd-indicator: {{colors.error.default.hex}}; /* change to #f13f43 for default red */
|
||||
--idle-indicator: {{colors.tertiary_container.default.hex}}; /* change to #f0b232 for default yellow */
|
||||
--streaming-indicator: {{colors.on_primary.default.hex}}; /* change to #593695 for default purple */
|
||||
|
||||
/* accent colors */
|
||||
--accent-1: {{colors.tertiary.default.hex}}; /* links */
|
||||
--accent-2: {{colors.primary.default.hex}}; /* general unread/mention elements, some icons when active */
|
||||
--accent-3: {{colors.primary.default.hex}}; /* accent buttons */
|
||||
--accent-4: {{colors.surface_bright.default.hex}}; /* accent buttons when hovered */
|
||||
--accent-5: {{colors.primary_fixed_dim.default.hex}}; /* accent buttons when clicked */
|
||||
--mention: {{colors.surface.default.hex}}; /* mentions & mention messages */
|
||||
--mention-hover: {{colors.surface_bright.default.hex}}; /* mentions & mention messages when hovered */
|
||||
|
||||
/* text colors */
|
||||
--text-0: {{colors.surface.default.hex}}; /* text on colored elements */
|
||||
--text-1: {{colors.on_surface.default.hex}}; /* other normally white text */
|
||||
--text-2: {{colors.on_surface.default.hex}}; /* headings and important text */
|
||||
--text-3: {{colors.on_surface_variant.default.hex}}; /* normal text */
|
||||
--text-4: {{colors.on_surface_variant.default.hex}}; /* icon buttons and channels */
|
||||
--text-5: {{colors.outline.default.hex}}; /* muted channels/chats and timestamps */
|
||||
|
||||
/* background and dark colors */
|
||||
--bg-1: {{colors.primary.default.hex}}; /* dark buttons when clicked */
|
||||
--bg-2: {{colors.surface_container_high.default.hex}}; /* dark buttons */
|
||||
--bg-3: {{colors.surface_container_low.default.hex}}; /* spacing, secondary elements */
|
||||
--bg-4: {{colors.surface.default.hex}}; /* main background color */
|
||||
--hover: {{colors.surface_bright.default.hex}}; /* channels and buttons when hovered */
|
||||
--active: {{colors.surface_bright.default.hex}}; /* channels and buttons when clicked or selected */
|
||||
--message-hover: {{colors.surface_bright.default.hex}}; /* messages when hovered */
|
||||
|
||||
/* amount of spacing and padding */
|
||||
--spacing: 12px;
|
||||
|
||||
/* animations */
|
||||
/* ALL ANIMATIONS CAN BE DISABLED WITH REDUCED MOTION IN DISCORD SETTINGS */
|
||||
--list-item-transition: 0.2s ease; /* channels/members/settings hover transition */
|
||||
--unread-bar-transition: 0.2s ease; /* unread bar moving into view transition */
|
||||
--moon-spin-transition: 0.4s ease; /* moon icon spin */
|
||||
--icon-spin-transition: 1s ease; /* round icon button spin (settings, emoji, etc.) */
|
||||
|
||||
/* corner roundness (border-radius) */
|
||||
--roundness-xl: 22px; /* roundness of big panel outer corners */
|
||||
--roundness-l: 20px; /* popout panels */
|
||||
--roundness-m: 16px; /* smaller panels, images, embeds */
|
||||
--roundness-s: 12px; /* members, settings inputs */
|
||||
--roundness-xs: 10px; /* channels, buttons */
|
||||
--roundness-xxs: 8px; /* searchbar, small elements */
|
||||
|
||||
/* direct messages moon icon */
|
||||
/* change to block to show, none to hide */
|
||||
--discord-icon: none; /* discord icon */
|
||||
--moon-icon: block; /* moon icon */
|
||||
--moon-icon-url: url('https://upload.wikimedia.org/wikipedia/commons/c/c4/Font_Awesome_5_solid_moon.svg'); /* custom icon url */
|
||||
--moon-icon-size: auto;
|
||||
|
||||
/* filter uncolorable elements to fit theme */
|
||||
/* (just set to none, they're too much work to configure) */
|
||||
--login-bg-filter: saturate(0.3) hue-rotate(-15deg) brightness(0.4); /* login background artwork */
|
||||
--green-to-accent-3-filter: hue-rotate(56deg) saturate(1.43); /* add friend page explore icon */
|
||||
--blurple-to-accent-3-filter: hue-rotate(304deg) saturate(0.84) brightness(1.2); /* add friend page school icon */
|
||||
}
|
||||
|
||||
/* Selected chat/friend text */
|
||||
.selected_f5eb4b,
|
||||
.selected_f6f816 .link_d8bfb3 {
|
||||
color: var(--text-0) !important;
|
||||
background: var(--accent-3) !important;
|
||||
}
|
||||
|
||||
.selected_f6f816 .link_d8bfb3 * {
|
||||
color: var(--text-0) !important;
|
||||
fill: var(--text-0) !important;
|
||||
}
|
||||
|
||||
/* Make channel name text less visible (darker) */
|
||||
.name__2ea32 {
|
||||
color: var(--text-5) !important;
|
||||
opacity: 0.7 !important;
|
||||
}
|
||||
|
||||
/* Make unread channel names brighter */
|
||||
.link__2ea32[aria-label*="unread"] .name__2ea32 {
|
||||
color: var(--text-2) !important;
|
||||
opacity: 1 !important;
|
||||
font-weight: 600 !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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6134
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,148 @@ 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Upgrade the density of the bar so the look stay the same for people who upgrade.
|
||||
if (adapter.settingsVersion == 2) {
|
||||
adapter.bar.density = "comfortable"
|
||||
adapter.settingsVersion++
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
function upgradeWidget(widget) {
|
||||
// Backup the widget definition before altering
|
||||
const widgetBefore = JSON.stringify(widget)
|
||||
|
||||
switch (widget.id) {
|
||||
// Get back to global settings for these two clock settings
|
||||
case "Clock":
|
||||
if (widget.use12HourClock !== undefined) {
|
||||
adapter.location.use12hourFormat = widget.use12HourClock
|
||||
delete widget.use12HourClock
|
||||
}
|
||||
if (widget.reverseDayMonth !== undefined) {
|
||||
adapter.location.monthBeforeDay = widget.reverseDayMonth
|
||||
delete widget.reverseDayMonth
|
||||
}
|
||||
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()
|
||||
|
||||
// Ensure wallpapers are restored after settings have been loaded
|
||||
WallpaperService.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 +210,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,43 +248,83 @@ Singleton {
|
||||
JsonAdapter {
|
||||
id: adapter
|
||||
|
||||
property int settingsVersion: 3
|
||||
|
||||
// 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: []
|
||||
property string density: "default" // "compact", "default", "comfortable"
|
||||
property bool showCapsule: true
|
||||
|
||||
// Floating bar settings
|
||||
property bool floating: false
|
||||
property real marginVertical: 0.25
|
||||
property real marginHorizontal: 0.25
|
||||
|
||||
// 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"
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
// general
|
||||
property JsonObject general: JsonObject {
|
||||
property string avatarImage: defaultAvatar
|
||||
property bool dimDesktop: false
|
||||
property bool dimDesktop: true
|
||||
property bool showScreenCorners: false
|
||||
property bool forceBlackScreenCorners: false
|
||||
property real radiusRatio: 1.0
|
||||
property real screenRadiusRatio: 1.0
|
||||
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 use12hourFormat: false
|
||||
property bool monthBeforeDay: false
|
||||
property bool showWeekNumberInCalendar: false
|
||||
}
|
||||
|
||||
// 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 +337,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
|
||||
property bool sortByMostUsed: true
|
||||
}
|
||||
|
||||
// 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 +379,34 @@ Singleton {
|
||||
|
||||
// notifications
|
||||
property JsonObject notifications: JsonObject {
|
||||
property bool doNotDisturb: false
|
||||
property list<string> monitors: []
|
||||
// Last time the user opened the notification history (ms since e899999999999998poch)
|
||||
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: ""
|
||||
}
|
||||
|
||||
// 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 +416,40 @@ 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 pywalfox: 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,18 +60,41 @@ Singleton {
|
||||
property real opacityFull: 1.0
|
||||
|
||||
// Animation duration (ms)
|
||||
property int animationFast: 150
|
||||
property int animationNormal: 300
|
||||
property int animationSlow: 450
|
||||
|
||||
// Dimensions
|
||||
property int barHeight: 36
|
||||
property int capsuleHeight: (barHeight * 0.73)
|
||||
property int baseWidgetSize: 32
|
||||
property int sliderWidth: 200
|
||||
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)
|
||||
|
||||
// Delays
|
||||
property int tooltipDelay: 300
|
||||
property int tooltipDelayLong: 1200
|
||||
property int pillDelay: 500
|
||||
|
||||
// Settings widgets base size
|
||||
property real baseWidgetSize: 33
|
||||
property real sliderWidth: 200
|
||||
|
||||
// Bar Dimensions
|
||||
property real barHeight: {
|
||||
if (Settings.data.bar.density === "compact") {
|
||||
return (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 27 : 25
|
||||
}
|
||||
if (Settings.data.bar.density === "default") {
|
||||
return (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 33 : 31
|
||||
}
|
||||
if (Settings.data.bar.density === "comfortable") {
|
||||
return (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 39 : 37
|
||||
}
|
||||
}
|
||||
property real capsuleHeight: {
|
||||
if (Settings.data.bar.density === "compact") {
|
||||
return barHeight * 0.85
|
||||
}
|
||||
if (Settings.data.bar.density === "default") {
|
||||
return barHeight * 0.82
|
||||
}
|
||||
if (Settings.data.bar.density === "comfortable") {
|
||||
return barHeight * 0.73
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(monthBeforeDay = 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}, ` + (monthBeforeDay ? `${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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: Settings.data.general.forceBlackScreenCorners ? Qt.rgba(0, 0, 0, 1) : 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,205 @@ 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
|
||||
// When bar is floating, corners should be at screen edges (no margins)
|
||||
// When bar is not floating, respect bar margins as before
|
||||
top: !Settings.data.bar.floating && ((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: !Settings.data.bar.floating && ((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: !Settings.data.bar.floating && ((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: !Settings.data.bar.floating && ((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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,119 +8,233 @@ import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Notification
|
||||
import qs.Modules.Bar.Extras
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
sourceComponent: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? verticalBarComponent : horizontalBarComponent
|
||||
}
|
||||
|
||||
// For vertical bars
|
||||
Component {
|
||||
id: verticalBarComponent
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
// Top section (left widgets)
|
||||
ColumnLayout {
|
||||
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: BarWidgetLoader {
|
||||
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
|
||||
}
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Center section (center widgets)
|
||||
ColumnLayout {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * root.scaling
|
||||
|
||||
Repeater {
|
||||
model: Settings.data.bar.widgets.center
|
||||
delegate: BarWidgetLoader {
|
||||
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
|
||||
}
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom section (right widgets)
|
||||
ColumnLayout {
|
||||
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: BarWidgetLoader {
|
||||
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
|
||||
}
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Center Section - Dynamic Widgets
|
||||
Row {
|
||||
id: centerSection
|
||||
// For horizontal bars
|
||||
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: BarWidgetLoader {
|
||||
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
|
||||
}
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
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: BarWidgetLoader {
|
||||
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
|
||||
}
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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: BarWidgetLoader {
|
||||
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
|
||||
}
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
111
Modules/Bar/Extras/BarPill.qml
Normal file
@@ -0,0 +1,111 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string icon: ""
|
||||
property string text: ""
|
||||
property string suffix: ""
|
||||
property string tooltipText: ""
|
||||
property bool autoHide: false
|
||||
property bool forceOpen: false
|
||||
property bool forceClose: false
|
||||
property bool disableOpen: false
|
||||
property bool rightOpen: false
|
||||
property bool hovered: false
|
||||
property bool compact: false
|
||||
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
|
||||
|
||||
signal shown
|
||||
signal hidden
|
||||
signal entered
|
||||
signal exited
|
||||
signal clicked
|
||||
signal rightClicked
|
||||
signal middleClicked
|
||||
signal wheel(int delta)
|
||||
|
||||
// Dynamic sizing based on loaded component
|
||||
width: pillLoader.item ? pillLoader.item.width : 0
|
||||
height: pillLoader.item ? pillLoader.item.height : 0
|
||||
|
||||
// Loader to switch between vertical and horizontal pill implementations
|
||||
Loader {
|
||||
id: pillLoader
|
||||
sourceComponent: isVerticalBar ? verticalPillComponent : horizontalPillComponent
|
||||
|
||||
Component {
|
||||
id: verticalPillComponent
|
||||
BarPillVertical {
|
||||
icon: root.icon
|
||||
text: root.text
|
||||
suffix: root.suffix
|
||||
tooltipText: root.tooltipText
|
||||
autoHide: root.autoHide
|
||||
forceOpen: root.forceOpen
|
||||
forceClose: root.forceClose
|
||||
disableOpen: root.disableOpen
|
||||
rightOpen: root.rightOpen
|
||||
hovered: root.hovered
|
||||
compact: root.compact
|
||||
onShown: root.shown()
|
||||
onHidden: root.hidden()
|
||||
onEntered: root.entered()
|
||||
onExited: root.exited()
|
||||
onClicked: root.clicked()
|
||||
onRightClicked: root.rightClicked()
|
||||
onMiddleClicked: root.middleClicked()
|
||||
onWheel: delta => root.wheel(delta)
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: horizontalPillComponent
|
||||
BarPillHorizontal {
|
||||
icon: root.icon
|
||||
text: root.text
|
||||
suffix: root.suffix
|
||||
tooltipText: root.tooltipText
|
||||
autoHide: root.autoHide
|
||||
forceOpen: root.forceOpen
|
||||
forceClose: root.forceClose
|
||||
disableOpen: root.disableOpen
|
||||
rightOpen: root.rightOpen
|
||||
hovered: root.hovered
|
||||
compact: root.compact
|
||||
onShown: root.shown()
|
||||
onHidden: root.hidden()
|
||||
onEntered: root.entered()
|
||||
onExited: root.exited()
|
||||
onClicked: root.clicked()
|
||||
onRightClicked: root.rightClicked()
|
||||
onMiddleClicked: root.middleClicked()
|
||||
onWheel: delta => root.wheel(delta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (pillLoader.item && pillLoader.item.show) {
|
||||
pillLoader.item.show()
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (pillLoader.item && pillLoader.item.hide) {
|
||||
pillLoader.item.hide()
|
||||
}
|
||||
}
|
||||
|
||||
function showDelayed() {
|
||||
if (pillLoader.item && pillLoader.item.showDelayed) {
|
||||
pillLoader.item.showDelayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,66 +2,86 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string icon: ""
|
||||
property string text: ""
|
||||
property string suffix: ""
|
||||
property string tooltipText: ""
|
||||
property color pillColor: Color.mSurfaceVariant
|
||||
property color textColor: Color.mOnSurface
|
||||
property color iconCircleColor: Color.mPrimary
|
||||
property color iconTextColor: Color.mSurface
|
||||
property color collapsedIconColor: Color.mOnSurface
|
||||
property real sizeMultiplier: 0.8
|
||||
property bool autoHide: false
|
||||
property bool forceOpen: false
|
||||
property bool forceClose: false
|
||||
property bool disableOpen: false
|
||||
property bool rightOpen: false
|
||||
property bool hovered: false
|
||||
property bool compact: false
|
||||
|
||||
// Effective shown state (true if hovered/animated open or forced)
|
||||
readonly property bool effectiveShown: forceOpen || showPill
|
||||
readonly property bool revealed: forceOpen || showPill
|
||||
|
||||
signal shown
|
||||
signal hidden
|
||||
signal entered
|
||||
signal exited
|
||||
signal clicked
|
||||
signal rightClicked
|
||||
signal middleClicked
|
||||
signal wheel(int delta)
|
||||
|
||||
// Internal state
|
||||
property bool showPill: false
|
||||
property bool shouldAnimateHide: false
|
||||
|
||||
// Exposed width logic
|
||||
readonly property int pillHeight: Style.baseWidgetSize * sizeMultiplier * scaling
|
||||
readonly property int iconSize: Style.baseWidgetSize * sizeMultiplier * scaling
|
||||
readonly property int pillPaddingHorizontal: Style.marginM * scaling
|
||||
readonly property int pillOverlap: iconSize * 0.5
|
||||
readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap)
|
||||
readonly property int pillHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
readonly property int pillPaddingHorizontal: Math.round(Style.capsuleHeight * 0.2 * scaling)
|
||||
readonly property int pillOverlap: Math.round(Style.capsuleHeight * 0.5 * scaling)
|
||||
readonly property int pillMaxWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap)
|
||||
|
||||
width: iconSize + (effectiveShown ? maxPillWidth - pillOverlap : 0)
|
||||
readonly property real iconSize: Math.max(1, compact ? pillHeight * 0.65 : pillHeight * 0.48)
|
||||
readonly property real textSize: Math.max(1, compact ? pillHeight * 0.45 : pillHeight * 0.33)
|
||||
|
||||
width: pillHeight + Math.max(0, pill.width - pillOverlap)
|
||||
height: pillHeight
|
||||
|
||||
Rectangle {
|
||||
id: pill
|
||||
width: effectiveShown ? maxPillWidth : 1
|
||||
width: revealed ? pillMaxWidth : 1
|
||||
height: pillHeight
|
||||
x: (iconCircle.x + iconCircle.width / 2) - width
|
||||
opacity: effectiveShown ? Style.opacityFull : Style.opacityNone
|
||||
color: pillColor
|
||||
topLeftRadius: pillHeight * 0.5
|
||||
bottomLeftRadius: pillHeight * 0.5
|
||||
|
||||
x: rightOpen ? (iconCircle.x + iconCircle.width / 2) : // Opens right
|
||||
(iconCircle.x + iconCircle.width / 2) - width // Opens left
|
||||
|
||||
opacity: revealed ? Style.opacityFull : Style.opacityNone
|
||||
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
|
||||
topLeftRadius: rightOpen ? 0 : pillHeight * 0.5
|
||||
bottomLeftRadius: rightOpen ? 0 : pillHeight * 0.5
|
||||
topRightRadius: rightOpen ? pillHeight * 0.5 : 0
|
||||
bottomRightRadius: rightOpen ? pillHeight * 0.5 : 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
NText {
|
||||
id: textItem
|
||||
anchors.centerIn: parent
|
||||
text: root.text
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: {
|
||||
// Better text horizontal centering
|
||||
var centerX = (parent.width - width) / 2
|
||||
var offset = rightOpen ? Style.marginXS * scaling : -Style.marginXS * scaling
|
||||
if (forceOpen) {
|
||||
// If its force open, the icon disc background is the same color as the bg pill move text slightly
|
||||
offset += rightOpen ? -Style.marginXXS * scaling : Style.marginXXS * scaling
|
||||
}
|
||||
return centerX + offset
|
||||
}
|
||||
text: root.text + root.suffix
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: textSize
|
||||
font.weight: Style.fontWeightBold
|
||||
color: textColor
|
||||
visible: effectiveShown
|
||||
color: forceOpen ? Color.mOnSurface : Color.mPrimary
|
||||
visible: revealed
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
@@ -82,13 +102,13 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
id: iconCircle
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
width: pillHeight
|
||||
height: pillHeight
|
||||
radius: width * 0.5
|
||||
// When forced shown, match pill background; otherwise use accent when hovered
|
||||
color: forceOpen ? pillColor : (showPill ? iconCircleColor : Color.mSurfaceVariant)
|
||||
color: hovered ? Color.mTertiary : Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
|
||||
x: rightOpen ? 0 : (parent.width - width)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
@@ -98,11 +118,13 @@ Item {
|
||||
}
|
||||
|
||||
NIcon {
|
||||
text: root.icon
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
// When forced shown, use pill text color; otherwise accent color when hovered
|
||||
color: forceOpen ? textColor : (showPill ? iconTextColor : Color.mOnSurface)
|
||||
anchors.centerIn: parent
|
||||
icon: root.icon
|
||||
font.pointSize: iconSize
|
||||
color: hovered ? Color.mOnTertiary : Color.mOnSurface
|
||||
// Center horizontally
|
||||
x: (iconCircle.width - width) / 2
|
||||
// Center vertically accounting for font metrics
|
||||
y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +135,7 @@ Item {
|
||||
target: pill
|
||||
property: "width"
|
||||
from: 1
|
||||
to: maxPillWidth
|
||||
to: pillMaxWidth
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
@@ -153,7 +175,7 @@ Item {
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "width"
|
||||
from: maxPillWidth
|
||||
from: pillMaxWidth
|
||||
to: 1
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InCubic
|
||||
@@ -194,7 +216,9 @@ Item {
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onEntered: {
|
||||
hovered = true
|
||||
root.entered()
|
||||
tooltip.show()
|
||||
if (disableOpen) {
|
||||
@@ -205,18 +229,23 @@ Item {
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
hovered = false
|
||||
root.exited()
|
||||
if (!forceOpen) {
|
||||
hide()
|
||||
}
|
||||
tooltip.hide()
|
||||
}
|
||||
onClicked: {
|
||||
root.clicked()
|
||||
onClicked: function (mouse) {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
root.clicked()
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
root.rightClicked()
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
root.middleClicked()
|
||||
}
|
||||
}
|
||||
onWheel: wheel => {
|
||||
root.wheel(wheel.angleDelta.y)
|
||||
}
|
||||
onWheel: wheel => root.wheel(wheel.angleDelta.y)
|
||||
}
|
||||
|
||||
function show() {
|
||||
335
Modules/Bar/Extras/BarPillVertical.qml
Normal file
@@ -0,0 +1,335 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string icon: ""
|
||||
property string text: ""
|
||||
property string suffix: ""
|
||||
property string tooltipText: ""
|
||||
property bool autoHide: false
|
||||
property bool forceOpen: false
|
||||
property bool forceClose: false
|
||||
property bool disableOpen: false
|
||||
property bool rightOpen: false
|
||||
property bool hovered: false
|
||||
property bool compact: false
|
||||
|
||||
// Bar position detection for pill direction
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
|
||||
|
||||
// Determine pill direction based on section position
|
||||
readonly property bool openDownward: rightOpen
|
||||
readonly property bool openUpward: !rightOpen
|
||||
|
||||
// Effective shown state (true if animated open or forced, but not if force closed)
|
||||
readonly property bool revealed: !forceClose && (forceOpen || showPill)
|
||||
|
||||
signal shown
|
||||
signal hidden
|
||||
signal entered
|
||||
signal exited
|
||||
signal clicked
|
||||
signal rightClicked
|
||||
signal middleClicked
|
||||
signal wheel(int delta)
|
||||
|
||||
// Internal state
|
||||
property bool showPill: false
|
||||
property bool shouldAnimateHide: false
|
||||
|
||||
// Sizing logic for vertical bars
|
||||
readonly property int buttonSize: Math.round(Style.capsuleHeight * scaling)
|
||||
readonly property int pillHeight: buttonSize
|
||||
readonly property int pillPaddingVertical: 3 * 2 * scaling // Very precise adjustment don't replace by Style.margin
|
||||
readonly property int pillOverlap: buttonSize * 0.5
|
||||
readonly property int maxPillWidth: buttonSize
|
||||
readonly property int maxPillHeight: Math.max(1, textItem.implicitHeight + pillPaddingVertical * 4)
|
||||
|
||||
readonly property real iconSize: Math.max(1, compact ? pillHeight * 0.65 : pillHeight * 0.48)
|
||||
readonly property real textSize: Math.max(1, compact ? pillHeight * 0.38 : pillHeight * 0.33)
|
||||
|
||||
// For vertical bars: width is just icon size, height includes pill space
|
||||
width: buttonSize
|
||||
height: revealed ? (buttonSize + maxPillHeight - pillOverlap) : buttonSize
|
||||
|
||||
Rectangle {
|
||||
id: pill
|
||||
width: revealed ? maxPillWidth : 1
|
||||
height: revealed ? maxPillHeight : 1
|
||||
|
||||
// Position based on direction - center the pill relative to the icon
|
||||
x: 0
|
||||
y: openUpward ? (iconCircle.y + iconCircle.height / 2 - height) : (iconCircle.y + iconCircle.height / 2)
|
||||
|
||||
opacity: revealed ? Style.opacityFull : Style.opacityNone
|
||||
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
|
||||
// Radius logic for vertical expansion - rounded on the side that connects to icon
|
||||
topLeftRadius: openUpward ? buttonSize * 0.5 : 0
|
||||
bottomLeftRadius: openDownward ? buttonSize * 0.5 : 0
|
||||
topRightRadius: openUpward ? buttonSize * 0.5 : 0
|
||||
bottomRightRadius: openDownward ? buttonSize * 0.5 : 0
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
NText {
|
||||
id: textItem
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: {
|
||||
var offset = openDownward ? pillPaddingVertical * 0.75 : -pillPaddingVertical * 0.75
|
||||
if (forceOpen) {
|
||||
// If its force open, the icon disc background is the same color as the bg pill move text slightly
|
||||
offset += rightOpen ? -Style.marginXXS * scaling : Style.marginXXS * scaling
|
||||
}
|
||||
return offset
|
||||
}
|
||||
text: root.text + root.suffix
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: textSize
|
||||
font.weight: Style.fontWeightMedium
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: forceOpen ? Color.mOnSurface : Color.mPrimary
|
||||
visible: revealed
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
enabled: showAnim.running || hideAnim.running
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
enabled: showAnim.running || hideAnim.running
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
enabled: showAnim.running || hideAnim.running
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: iconCircle
|
||||
width: buttonSize
|
||||
height: buttonSize
|
||||
radius: width * 0.5
|
||||
color: hovered ? Color.mTertiary : Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
|
||||
// Icon positioning based on direction
|
||||
x: 0
|
||||
y: openUpward ? (parent.height - height) : 0
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: root.icon
|
||||
font.pointSize: iconSize
|
||||
color: hovered ? Color.mOnTertiary : Color.mOnSurface
|
||||
// Center horizontally
|
||||
x: (iconCircle.width - width) / 2
|
||||
// Center vertically accounting for font metrics
|
||||
y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: showAnim
|
||||
running: false
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "width"
|
||||
from: 1
|
||||
to: maxPillWidth
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "height"
|
||||
from: 1
|
||||
to: maxPillHeight
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
onStarted: {
|
||||
showPill = true
|
||||
}
|
||||
onStopped: {
|
||||
delayedHideAnim.start()
|
||||
root.shown()
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: delayedHideAnim
|
||||
running: false
|
||||
PauseAnimation {
|
||||
duration: 2500
|
||||
}
|
||||
ScriptAction {
|
||||
script: if (shouldAnimateHide) {
|
||||
hideAnim.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: hideAnim
|
||||
running: false
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "width"
|
||||
from: maxPillWidth
|
||||
to: 1
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "height"
|
||||
from: maxPillHeight
|
||||
to: 1
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
onStopped: {
|
||||
showPill = false
|
||||
shouldAnimateHide = false
|
||||
root.hidden()
|
||||
}
|
||||
}
|
||||
|
||||
NTooltip {
|
||||
id: tooltip
|
||||
target: pill
|
||||
text: root.tooltipText
|
||||
positionLeft: barPosition === "right"
|
||||
positionRight: barPosition === "left"
|
||||
positionAbove: Settings.data.bar.position === "bottom"
|
||||
delay: Style.tooltipDelayLong
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: showTimer
|
||||
interval: Style.pillDelay
|
||||
onTriggered: {
|
||||
if (!showPill) {
|
||||
showAnim.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onEntered: {
|
||||
hovered = true
|
||||
root.entered()
|
||||
tooltip.show()
|
||||
if (disableOpen || forceClose) {
|
||||
return
|
||||
}
|
||||
if (!forceOpen) {
|
||||
showDelayed()
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
hovered = false
|
||||
root.exited()
|
||||
if (!forceOpen && !forceClose) {
|
||||
hide()
|
||||
}
|
||||
tooltip.hide()
|
||||
}
|
||||
onClicked: function (mouse) {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
root.clicked()
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
root.rightClicked()
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
root.middleClicked()
|
||||
}
|
||||
}
|
||||
onWheel: wheel => root.wheel(wheel.angleDelta.y)
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (!showPill) {
|
||||
shouldAnimateHide = autoHide
|
||||
showAnim.start()
|
||||
} else {
|
||||
hideAnim.stop()
|
||||
delayedHideAnim.restart()
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (forceOpen) {
|
||||
return
|
||||
}
|
||||
if (showPill) {
|
||||
hideAnim.start()
|
||||
}
|
||||
showTimer.stop()
|
||||
}
|
||||
|
||||
function showDelayed() {
|
||||
if (!showPill) {
|
||||
shouldAnimateHide = autoHide
|
||||
showTimer.start()
|
||||
} else {
|
||||
hideAnim.stop()
|
||||
delayedHideAnim.restart()
|
||||
}
|
||||
}
|
||||
|
||||
onForceOpenChanged: {
|
||||
if (forceOpen) {
|
||||
// Immediately lock open without animations
|
||||
showAnim.stop()
|
||||
hideAnim.stop()
|
||||
delayedHideAnim.stop()
|
||||
showPill = true
|
||||
} else {
|
||||
hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
76
Modules/Bar/Extras/BarWidgetLoader.qml
Normal file
@@ -0,0 +1,76 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Services
|
||||
import qs.Commons
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string widgetId: ""
|
||||
property var widgetProps: ({})
|
||||
property string screenName: widgetProps.screen ? widgetProps.screen.name : ""
|
||||
property string section: widgetProps.section || ""
|
||||
property int sectionIndex: widgetProps.sectionWidgetIndex || 0
|
||||
|
||||
Connections {
|
||||
target: ScalingService
|
||||
function onScaleChanged(aScreenName, scale) {
|
||||
if (loader.item && loader.item.screen && aScreenName === screenName) {
|
||||
loader.item['scaling'] = scale
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't reserve space unless the loaded widget is really visible
|
||||
implicitWidth: loader.item ? loader.item.visible ? loader.item.implicitWidth : 0 : 0
|
||||
implicitHeight: loader.item ? loader.item.visible ? loader.item.implicitHeight : 0 : 0
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
|
||||
anchors.fill: parent
|
||||
active: Settings.isLoaded && widgetId !== ""
|
||||
sourceComponent: {
|
||||
if (!active) {
|
||||
return null
|
||||
}
|
||||
return BarWidgetRegistry.getWidget(widgetId)
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (item && widgetProps) {
|
||||
// Apply properties to loaded widget
|
||||
for (var prop in widgetProps) {
|
||||
if (item.hasOwnProperty(prop)) {
|
||||
item[prop] = widgetProps[prop]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register this widget instance with BarService
|
||||
if (screenName && section) {
|
||||
BarService.registerWidget(screenName, section, widgetId, sectionIndex, item)
|
||||
}
|
||||
|
||||
if (item.hasOwnProperty("onLoaded")) {
|
||||
item.onLoaded()
|
||||
}
|
||||
|
||||
//Logger.log("BarWidgetLoader", "Loaded", widgetId, "on screen", item.screen.name)
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
// Unregister when destroyed
|
||||
if (screenName && section) {
|
||||
BarService.unregisterWidget(screenName, section, widgetId, sectionIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error handling
|
||||
onWidgetIdChanged: {
|
||||
if (widgetId && !BarWidgetRegistry.hasWidget(widgetId)) {
|
||||
Logger.warn("BarWidgetLoader", "Widget not found in bar registry:", widgetId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,11 +174,11 @@ PopupWindow {
|
||||
}
|
||||
|
||||
NIcon {
|
||||
text: modelData?.hasChildren ? "menu" : ""
|
||||
icon: modelData?.hasChildren ? "menu" : ""
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: modelData?.hasChildren ?? false
|
||||
color: Color.mOnSurface
|
||||
color: (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,9 +220,32 @@ PopupWindow {
|
||||
const submenuWidth = menuWidth * scaling // Assuming a similar width as the parent
|
||||
const overlap = 4 * scaling // A small overlap to bridge the mouse path
|
||||
|
||||
// Check if there's enough space on the right
|
||||
// Determine submenu opening direction based on bar position and available space
|
||||
let openLeft = false
|
||||
|
||||
// Check bar position first
|
||||
const barPosition = Settings.data.bar.position
|
||||
const globalPos = entry.mapToGlobal(0, 0)
|
||||
const openLeft = (globalPos.x + entry.width + submenuWidth > Screen.width)
|
||||
|
||||
if (barPosition === "right") {
|
||||
// Bar is on the right, prefer opening submenus to the left
|
||||
openLeft = true
|
||||
} else if (barPosition === "left") {
|
||||
// Bar is on the left, prefer opening submenus to the right
|
||||
openLeft = false
|
||||
} else {
|
||||
// Bar is horizontal (top/bottom) or undefined, use space-based logic
|
||||
openLeft = (globalPos.x + entry.width + submenuWidth > screen.width)
|
||||
|
||||
// Secondary check: ensure we don't open off-screen
|
||||
if (openLeft && globalPos.x - submenuWidth < 0) {
|
||||
// Would open off the left edge, force right opening
|
||||
openLeft = false
|
||||
} else if (!openLeft && globalPos.x + entry.width + submenuWidth > screen.width) {
|
||||
// Would open off the right edge, force left opening
|
||||
openLeft = true
|
||||
}
|
||||
}
|
||||
|
||||
// Position with overlap
|
||||
const anchorX = openLeft ? -submenuWidth + overlap : entry.width - overlap
|
||||
@@ -223,7 +256,8 @@ PopupWindow {
|
||||
"anchorItem": entry,
|
||||
"anchorX": anchorX,
|
||||
"anchorY": 0,
|
||||
"isSubMenu": true
|
||||
"isSubMenu": true,
|
||||
"screen": screen
|
||||
})
|
||||
|
||||
if (entry.subMenu) {
|
||||
|
||||
@@ -2,95 +2,172 @@ 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
|
||||
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property bool compact: (Settings.data.bar.density === "compact")
|
||||
|
||||
implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)
|
||||
implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * 0.8 * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling)
|
||||
|
||||
readonly property real textSize: {
|
||||
var base = isVertical ? width : height
|
||||
return Math.max(1, compact ? base * 0.43 : base * 0.33)
|
||||
}
|
||||
|
||||
readonly property real iconSize: textSize * 1.25
|
||||
|
||||
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.capsuleHeight * 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)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
anchors.left: (barPosition === "top" || barPosition === "bottom") ? parent.left : undefined
|
||||
anchors.top: (barPosition === "left" || barPosition === "right") ? parent.top : undefined
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
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: width / 2
|
||||
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
|
||||
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.capsuleHeight * 0.75 * scaling
|
||||
Layout.preferredHeight: Style.capsuleHeight * 0.75 * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: getTitle() !== "" && showIcon
|
||||
|
||||
IconImage {
|
||||
id: windowIcon
|
||||
@@ -99,26 +176,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 +219,84 @@ 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.capsuleHeight * 0.75 * scaling
|
||||
height: Style.capsuleHeight * 0.75 * scaling
|
||||
anchors.centerIn: parent
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,97 +5,122 @@ import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Bar.Extras
|
||||
|
||||
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
|
||||
|
||||
NPill {
|
||||
id: pill
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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"
|
||||
// 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)
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
BarPill {
|
||||
id: pill
|
||||
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
rightOpen: BarService.getPillDirection(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,17 @@ 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
|
||||
colorBg: Color.mSurfaceVariant
|
||||
baseSize: Style.capsuleHeight
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
colorBg: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -4,12 +4,33 @@ import qs.Commons
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Bar.Extras
|
||||
|
||||
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 +46,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 +54,45 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
NPill {
|
||||
Timer {
|
||||
id: hideTimerAfterChange
|
||||
interval: 2500
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: pill.hide()
|
||||
}
|
||||
|
||||
BarPill {
|
||||
id: pill
|
||||
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
rightOpen: BarService.getPillDirection(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) + "%\nRight click for settings.\nScroll to modify brightness."
|
||||
}
|
||||
|
||||
onWheel: function (angle) {
|
||||
@@ -86,8 +107,15 @@ Item {
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.Brightness
|
||||
settingsPanel.open(screen)
|
||||
var settingsPanel = PanelService.getPanel("settingsPanel")
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.Display
|
||||
settingsPanel.open()
|
||||
}
|
||||
|
||||
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,204 @@ 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)
|
||||
color: Color.mSurfaceVariant
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
// Clock Icon with attached calendar
|
||||
NClock {
|
||||
id: clock
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
NTooltip {
|
||||
id: tooltip
|
||||
text: Time.dateString
|
||||
target: clock
|
||||
positionAbove: Settings.data.bar.position === "bottom"
|
||||
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 compact: (Settings.data.bar.density === "compact")
|
||||
readonly property bool use12h: Settings.data.location.use12hourFormat
|
||||
readonly property bool monthBeforeDay: Settings.data.location.monthBeforeDay
|
||||
|
||||
// Resolve settings: try user settings or defaults from BarWidgetRegistry
|
||||
readonly property string displayFormat: widgetSettings.displayFormat !== undefined ? widgetSettings.displayFormat : widgetMetadata.displayFormat
|
||||
|
||||
// Use compact mode for vertical bars
|
||||
readonly property bool verticalMode: barPosition === "left" || barPosition === "right"
|
||||
|
||||
implicitWidth: verticalMode ? Math.round(Style.capsuleHeight * scaling) : Math.round(layout.implicitWidth + Style.marginM * 2 * scaling)
|
||||
implicitHeight: verticalMode ? Math.round(Style.capsuleHeight * 2.5 * scaling) : Math.round(Style.capsuleHeight * scaling) // Match BarPill
|
||||
|
||||
radius: Math.round(Style.radiusS * scaling)
|
||||
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
|
||||
Item {
|
||||
id: clockContainer
|
||||
anchors.fill: parent
|
||||
anchors.margins: compact ? 0 : Style.marginXS * scaling
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
anchors.centerIn: parent
|
||||
spacing: verticalMode ? -2 * scaling : -3 * scaling
|
||||
|
||||
// Compact mode for vertical bars - Time section (HH, MM)
|
||||
Repeater {
|
||||
model: verticalMode ? 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 (verticalMode) {
|
||||
// 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 += " - " + (monthBeforeDay ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`)
|
||||
}
|
||||
|
||||
return timeStr
|
||||
}
|
||||
}
|
||||
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: verticalMode ? 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: verticalMode
|
||||
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: verticalMode ? 2 : 0
|
||||
NText {
|
||||
readonly property var now: Time.date
|
||||
|
||||
text: {
|
||||
if (verticalMode) {
|
||||
// Compact mode: date section (last 2 lines)
|
||||
switch (index) {
|
||||
case 0:
|
||||
return monthBeforeDay ? (now.getMonth() + 1).toString().padStart(2, '0') : now.getDate().toString().padStart(2, '0')
|
||||
case 1:
|
||||
return monthBeforeDay ? now.getDate().toString().padStart(2, '0') : (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: !verticalMode && (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 monthBeforeDay ? `${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(monthBeforeDay)}.`
|
||||
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 +217,7 @@ Rectangle {
|
||||
}
|
||||
onClicked: {
|
||||
tooltip.hide()
|
||||
PanelService.getPanel("calendarPanel")?.toggle(screen)
|
||||
PanelService.getPanel("calendarPanel")?.toggle(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
139
Modules/Bar/Widgets/CustomButton.qml
Normal file
@@ -0,0 +1,139 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Modules.Bar.Extras
|
||||
|
||||
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 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 string textCommand: widgetSettings.textCommand !== undefined ? widgetSettings.textCommand : (widgetMetadata.textCommand || "")
|
||||
readonly property int textIntervalMs: widgetSettings.textIntervalMs !== undefined ? widgetSettings.textIntervalMs : (widgetMetadata.textIntervalMs || 3000)
|
||||
readonly property bool hasExec: (leftClickExec || rightClickExec || middleClickExec)
|
||||
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
|
||||
BarPill {
|
||||
id: pill
|
||||
|
||||
rightOpen: BarService.getPillDirection(root)
|
||||
icon: customIcon
|
||||
text: _dynamicText
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
autoHide: false
|
||||
forceOpen: _dynamicText !== ""
|
||||
forceClose: false
|
||||
disableOpen: true
|
||||
tooltipText: {
|
||||
if (!hasExec) {
|
||||
return "Custom Button - Configure in settings"
|
||||
} else {
|
||||
var lines = []
|
||||
if (leftClickExec !== "") {
|
||||
lines.push(`Left click: ${leftClickExec}.`)
|
||||
}
|
||||
if (rightClickExec !== "") {
|
||||
lines.push(`Right click: ${rightClickExec}.`)
|
||||
}
|
||||
if (middleClickExec !== "") {
|
||||
lines.push(`Middle click: ${middleClickExec}.`)
|
||||
}
|
||||
return lines.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: root.onClicked()
|
||||
onRightClicked: root.onRightClicked()
|
||||
onMiddleClicked: root.onMiddleClicked()
|
||||
}
|
||||
|
||||
// Internal state for dynamic text
|
||||
property string _dynamicText: ""
|
||||
|
||||
// Periodically run the text command (if set)
|
||||
Timer {
|
||||
id: refreshTimer
|
||||
interval: Math.max(250, textIntervalMs)
|
||||
repeat: true
|
||||
running: (textCommand && textCommand.length > 0)
|
||||
triggeredOnStart: true
|
||||
onTriggered: {
|
||||
if (!textCommand || textCommand.length === 0)
|
||||
return
|
||||
if (textProc.running)
|
||||
return
|
||||
textProc.command = ["sh", "-lc", textCommand]
|
||||
textProc.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: textProc
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {}
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
var out = String(stdout.text || "").trim()
|
||||
if (out.indexOf("\n") !== -1) {
|
||||
out = out.split("\n")[0]
|
||||
}
|
||||
_dynamicText = out
|
||||
}
|
||||
}
|
||||
|
||||
function 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()
|
||||
}
|
||||
}
|
||||
|
||||
function onRightClicked() {
|
||||
if (rightClickExec) {
|
||||
Quickshell.execDetached(["sh", "-c", rightClickExec])
|
||||
Logger.log("CustomButton", `Executing command: ${rightClickExec}`)
|
||||
}
|
||||
}
|
||||
|
||||
function onMiddleClicked() {
|
||||
if (middleClickExec) {
|
||||
Quickshell.execDetached(["sh", "-c", middleClickExec])
|
||||
Logger.log("CustomButton", `Executing command: ${middleClickExec}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
21
Modules/Bar/Widgets/DarkModeToggle.qml
Normal file
@@ -0,0 +1,21 @@
|
||||
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."
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
baseSize: Style.capsuleHeight
|
||||
colorBg: Settings.data.colorSchemes.darkMode ? (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent) : Color.mPrimary
|
||||
colorFg: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mOnPrimary
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
onClicked: Settings.data.colorSchemes.darkMode = !Settings.data.colorSchemes.darkMode
|
||||
}
|
||||
22
Modules/Bar/Widgets/KeepAwake.qml
Normal file
@@ -0,0 +1,22 @@
|
||||
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
|
||||
|
||||
baseSize: Style.capsuleHeight
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off"
|
||||
tooltipText: IdleInhibitorService.isInhibited ? "Disable keep awake" : "Enable keep awake"
|
||||
colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
|
||||
colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
onClicked: IdleInhibitorService.manualToggle()
|
||||
}
|
||||
@@ -1,32 +1,56 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Bar.Extras
|
||||
|
||||
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 {
|
||||
BarPill {
|
||||
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
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
rightOpen: BarService.getPillDirection(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,65 @@ 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 compact: (Settings.data.bar.density === "compact")
|
||||
|
||||
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 +75,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)
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
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: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
|
||||
// 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 +110,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 +208,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
|
||||
}
|
||||
}
|
||||
|
||||
124
Modules/Bar/Widgets/Microphone.qml
Normal file
@@ -0,0 +1,124 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Commons
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Bar.Extras
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
BarPill {
|
||||
id: pill
|
||||
rightOpen: BarService.getPillDirection(root)
|
||||
icon: getIcon()
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
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 to toggle mute.\nRight click for settings.\nScroll to modify volume."
|
||||
|
||||
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"])
|
||||
}
|
||||
}
|
||||
}
|
||||
43
Modules/Bar/Widgets/NightLight.qml
Normal file
@@ -0,0 +1,43 @@
|
||||
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
|
||||
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
baseSize: Style.capsuleHeight
|
||||
colorBg: Settings.data.nightLight.forced ? Color.mPrimary : (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
|
||||
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,78 @@ 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"
|
||||
colorBg: Color.mSurfaceVariant
|
||||
// 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
|
||||
}
|
||||
|
||||
baseSize: Style.capsuleHeight
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
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: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
|
||||
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: 2 * scaling
|
||||
anchors.topMargin: 1 * scaling
|
||||
z: 2
|
||||
active: showUnreadBadge && (!hideWhenZero || computeUnreadCount() > 0)
|
||||
sourceComponent: Rectangle {
|
||||
id: badge
|
||||
readonly property int count: computeUnreadCount()
|
||||
height: 8 * scaling
|
||||
width: height
|
||||
radius: height / 2
|
||||
color: Color.mError
|
||||
border.color: Color.mSurface
|
||||
border.width: 1
|
||||
visible: count > 0 || !hideWhenZero
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,51 +10,17 @@ 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
|
||||
|
||||
sizeMultiplier: 0.8
|
||||
visible: hasPP
|
||||
baseSize: Style.capsuleHeight
|
||||
visible: PowerProfileService.available
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
function profileName() {
|
||||
if (!hasPP)
|
||||
return "Unknown"
|
||||
if (powerProfiles.profile === PowerProfile.Performance)
|
||||
return "Performance"
|
||||
if (powerProfiles.profile === PowerProfile.Balanced)
|
||||
return "Balanced"
|
||||
if (powerProfiles.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
|
||||
}
|
||||
|
||||
icon: root.profileIcon()
|
||||
tooltipText: root.profileName()
|
||||
colorBg: Color.mSurfaceVariant
|
||||
colorFg: Color.mOnSurface
|
||||
icon: PowerProfileService.getIcon()
|
||||
tooltipText: `Current power profile is "${PowerProfileService.getName()}".`
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
colorBg: (PowerProfileService.profile === PowerProfile.Balanced) ? (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent) : Color.mPrimary
|
||||
colorFg: (PowerProfileService.profile === PowerProfile.Balanced) ? Color.mOnSurface : Color.mOnPrimary
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
onClicked: root.changeProfile()
|
||||
onClicked: PowerProfileService.cycleProfile()
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
baseSize: Style.capsuleHeight
|
||||
icon: "power"
|
||||
tooltipText: "Power Settings"
|
||||
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
|
||||
colorFg: Color.mError
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
onClicked: PanelService.getPanel("powerPanel")?.toggle()
|
||||
}
|
||||
@@ -8,14 +8,14 @@ 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."
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
baseSize: Style.capsuleHeight
|
||||
colorBg: Color.mPrimary
|
||||
colorFg: Color.mOnPrimary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: ScreenRecorderService.toggleRecording()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick.Effects
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
@@ -7,17 +10,47 @@ 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
|
||||
|
||||
colorBg: Color.mSurfaceVariant
|
||||
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."
|
||||
baseSize: Style.capsuleHeight
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
|
||||
colorFg: Color.mOnSurface
|
||||
colorBgHover: useDistroLogo ? Color.mSurfaceVariant : Color.mTertiary
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
colorBorderHover: useDistroLogo ? Color.mTertiary : Color.transparent
|
||||
onClicked: PanelService.getPanel("sidePanel")?.toggle(this)
|
||||
onRightClicked: PanelService.getPanel("settingsPanel")?.toggle()
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: PanelService.getPanel("sidePanel")?.toggle(screen)
|
||||
IconImage {
|
||||
id: logo
|
||||
anchors.centerIn: parent
|
||||
width: root.width * 0.8
|
||||
height: width
|
||||
source: useDistroLogo ? DistroLogoService.osLogo : ""
|
||||
visible: useDistroLogo && source !== ""
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
}
|
||||
}
|
||||
|
||||
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,305 @@
|
||||
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
|
||||
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property bool compact: (Settings.data.bar.density === "compact")
|
||||
|
||||
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
|
||||
|
||||
readonly property real textSize: {
|
||||
var base = isVertical ? width * 0.82 : height
|
||||
return Math.max(1, compact ? base * 0.43 : base * 0.33)
|
||||
}
|
||||
|
||||
readonly property real iconSize: textSize * 1.25
|
||||
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: isVertical ? Math.round(Style.capsuleHeight * scaling) : Math.round(mainGrid.implicitWidth + Style.marginM * 2 * scaling)
|
||||
implicitHeight: isVertical ? Math.round(mainGrid.implicitHeight + Style.marginM * 2 * scaling) : Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
|
||||
GridLayout {
|
||||
id: mainGrid
|
||||
anchors.centerIn: parent
|
||||
|
||||
// Dynamic layout based on bar orientation
|
||||
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: isVertical ? -1 : 1
|
||||
columns: isVertical ? 1 : -1
|
||||
|
||||
rowSpacing: isVertical ? (Style.marginS * scaling) : (Style.marginXS * scaling)
|
||||
columnSpacing: isVertical ? (Style.marginXS * scaling) : (Style.marginXS * scaling)
|
||||
|
||||
// CPU Usage Component
|
||||
Item {
|
||||
id: mainContainer
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.marginS * scaling
|
||||
anchors.rightMargin: Style.marginS * scaling
|
||||
Layout.preferredWidth: cpuUsageContent.implicitWidth
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
|
||||
visible: showCpuUsage
|
||||
|
||||
Row {
|
||||
id: row
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
Row {
|
||||
id: cpuUsageLayout
|
||||
spacing: Style.marginXS * scaling
|
||||
GridLayout {
|
||||
id: cpuUsageContent
|
||||
anchors.centerIn: parent
|
||||
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: isVertical ? 2 : 1
|
||||
columns: isVertical ? 1 : 2
|
||||
rowSpacing: Style.marginXXS * scaling
|
||||
columnSpacing: 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
|
||||
}
|
||||
NText {
|
||||
text: isVertical ? `${Math.round(SystemStatService.cpuUsage)}%` : `${SystemStatService.cpuUsage}%`
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: textSize
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
Layout.row: isVertical ? 0 : 0
|
||||
Layout.column: isVertical ? 0 : 1
|
||||
}
|
||||
|
||||
// CPU Temperature Component
|
||||
Row {
|
||||
id: cpuTempLayout
|
||||
// spacing is thin here to compensate for the vertical thermometer icon
|
||||
spacing: Style.marginXXS * scaling
|
||||
NIcon {
|
||||
icon: "cpu-usage"
|
||||
font.pointSize: iconSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: isVertical ? 1 : 0
|
||||
Layout.column: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIcon {
|
||||
text: "thermometer"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
// CPU Temperature Component
|
||||
Item {
|
||||
Layout.preferredWidth: cpuTempContent.implicitWidth
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.alignment: isVertical ? Qt.AlignHCenter : 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
|
||||
}
|
||||
GridLayout {
|
||||
id: cpuTempContent
|
||||
anchors.centerIn: parent
|
||||
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: isVertical ? 2 : 1
|
||||
columns: isVertical ? 1 : 2
|
||||
rowSpacing: Style.marginXXS * scaling
|
||||
columnSpacing: Style.marginXXS * scaling
|
||||
|
||||
NText {
|
||||
text: isVertical ? `${SystemStatService.cpuTemp}°` : `${SystemStatService.cpuTemp}°C`
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: textSize
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
Layout.row: isVertical ? 0 : 0
|
||||
Layout.column: isVertical ? 0 : 1
|
||||
}
|
||||
|
||||
// Memory Usage Component
|
||||
Row {
|
||||
id: memoryUsageLayout
|
||||
spacing: Style.marginXS * scaling
|
||||
NIcon {
|
||||
icon: "cpu-temperature"
|
||||
font.pointSize: iconSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: isVertical ? 1 : 0
|
||||
Layout.column: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIcon {
|
||||
text: "memory"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
// Memory Usage Component
|
||||
Item {
|
||||
Layout.preferredWidth: memoryContent.implicitWidth
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.alignment: isVertical ? Qt.AlignHCenter : 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
|
||||
GridLayout {
|
||||
id: memoryContent
|
||||
anchors.centerIn: parent
|
||||
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: isVertical ? 2 : 1
|
||||
columns: isVertical ? 1 : 2
|
||||
rowSpacing: Style.marginXXS * scaling
|
||||
columnSpacing: Style.marginXXS * scaling
|
||||
|
||||
NText {
|
||||
text: {
|
||||
if (showMemoryAsPercent) {
|
||||
return `${SystemStatService.memPercent}%`
|
||||
} else {
|
||||
return isVertical ? `${Math.round(SystemStatService.memGb)}G` : `${SystemStatService.memGb}G`
|
||||
}
|
||||
}
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: textSize
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
Layout.row: isVertical ? 0 : 0
|
||||
Layout.column: isVertical ? 0 : 1
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "memory"
|
||||
font.pointSize: iconSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: isVertical ? 1 : 0
|
||||
Layout.column: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Network Download Speed Component
|
||||
Item {
|
||||
Layout.preferredWidth: downloadContent.implicitWidth
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
|
||||
visible: showNetworkStats
|
||||
|
||||
GridLayout {
|
||||
id: downloadContent
|
||||
anchors.centerIn: parent
|
||||
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: isVertical ? 2 : 1
|
||||
columns: isVertical ? 1 : 2
|
||||
rowSpacing: Style.marginXXS * scaling
|
||||
columnSpacing: isVertical ? (Style.marginXXS * scaling) : (Style.marginXS * scaling)
|
||||
|
||||
NText {
|
||||
text: isVertical ? SystemStatService.formatCompactSpeed(SystemStatService.rxSpeed) : SystemStatService.formatSpeed(SystemStatService.rxSpeed)
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: textSize
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
Layout.row: isVertical ? 0 : 0
|
||||
Layout.column: isVertical ? 0 : 1
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "download-speed"
|
||||
font.pointSize: iconSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: isVertical ? 1 : 0
|
||||
Layout.column: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Network Upload Speed Component
|
||||
Item {
|
||||
Layout.preferredWidth: uploadContent.implicitWidth
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
|
||||
visible: showNetworkStats
|
||||
|
||||
GridLayout {
|
||||
id: uploadContent
|
||||
anchors.centerIn: parent
|
||||
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: isVertical ? 2 : 1
|
||||
columns: isVertical ? 1 : 2
|
||||
rowSpacing: Style.marginXXS * scaling
|
||||
columnSpacing: isVertical ? (Style.marginXXS * scaling) : (Style.marginXS * scaling)
|
||||
|
||||
NText {
|
||||
text: isVertical ? SystemStatService.formatCompactSpeed(SystemStatService.txSpeed) : SystemStatService.formatSpeed(SystemStatService.txSpeed)
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: textSize
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
Layout.row: isVertical ? 0 : 0
|
||||
Layout.column: isVertical ? 0 : 1
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "upload-speed"
|
||||
font.pointSize: iconSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: isVertical ? 1 : 0
|
||||
Layout.column: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disk Usage Component (primary drive)
|
||||
Item {
|
||||
Layout.preferredWidth: diskContent.implicitWidth
|
||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
|
||||
visible: showDiskUsage
|
||||
|
||||
GridLayout {
|
||||
id: diskContent
|
||||
anchors.centerIn: parent
|
||||
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: isVertical ? 2 : 1
|
||||
columns: isVertical ? 1 : 2
|
||||
rowSpacing: Style.marginXXS * scaling
|
||||
columnSpacing: isVertical ? (Style.marginXXS * scaling) : (Style.marginXS * scaling)
|
||||
|
||||
NText {
|
||||
text: `${SystemStatService.diskPercent}%`
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: textSize
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
Layout.row: isVertical ? 0 : 0
|
||||
Layout.column: isVertical ? 0 : 1
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "storage"
|
||||
font.pointSize: iconSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: isVertical ? 1 : 0
|
||||
Layout.column: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
114
Modules/Bar/Widgets/Taskbar.qml
Normal file
@@ -0,0 +1,114 @@
|
||||
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 bool isVerticalBar: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
|
||||
readonly property bool compact: (Settings.data.bar.density === "compact")
|
||||
readonly property real itemSize: compact ? Style.capsuleHeight * 0.9 * scaling : Style.capsuleHeight * 0.8 * scaling
|
||||
|
||||
// Always visible when there are toplevels
|
||||
implicitWidth: isVerticalBar ? Math.round(Style.capsuleHeight * scaling) : taskbarLayout.implicitWidth + Style.marginM * scaling * 2
|
||||
implicitHeight: isVerticalBar ? taskbarLayout.implicitHeight + Style.marginM * scaling * 2 : Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
|
||||
GridLayout {
|
||||
id: taskbarLayout
|
||||
anchors.fill: parent
|
||||
anchors {
|
||||
leftMargin: isVerticalBar ? undefined : Style.marginM * scaling
|
||||
rightMargin: isVerticalBar ? undefined : Style.marginM * scaling
|
||||
topMargin: compact ? 0 : isVerticalBar ? Style.marginM * scaling : undefined
|
||||
bottomMargin: compact ? 0 : isVerticalBar ? Style.marginM * scaling : undefined
|
||||
}
|
||||
|
||||
// Configure GridLayout to behave like RowLayout or ColumnLayout
|
||||
rows: isVerticalBar ? -1 : 1 // -1 means unlimited
|
||||
columns: isVerticalBar ? 1 : -1 // -1 means unlimited
|
||||
|
||||
rowSpacing: isVerticalBar ? Style.marginXXS * root.scaling : 0
|
||||
columnSpacing: isVerticalBar ? 0 : 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: parent.width
|
||||
height: parent.height
|
||||
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: parent.width
|
||||
height: parent.height
|
||||
source: AppIcons.iconForAppId(taskbarItem.modelData.appId)
|
||||
smooth: true
|
||||
asynchronous: 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,38 @@ Rectangle {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.scale(screen)
|
||||
readonly property real itemSize: 24 * scaling
|
||||
property real scaling: 1.0
|
||||
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property bool compact: (Settings.data.bar.density === "compact")
|
||||
readonly property real itemSize: isVertical ? width * 0.75 : height * 0.85
|
||||
|
||||
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
|
||||
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
|
||||
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 +113,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 +161,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,28 +1,51 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Commons
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Bar.Extras
|
||||
|
||||
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
|
||||
@@ -49,27 +72,39 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
NPill {
|
||||
BarPill {
|
||||
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) {
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
rightOpen: BarService.getPillDirection(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 to toggle mute.\nRight click for settings.\nScroll to modify volume."
|
||||
|
||||
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,29 +11,20 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
colorBg: Color.mSurfaceVariant
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
baseSize: Style.capsuleHeight
|
||||
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
|
||||
colorFg: Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
|
||||
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,39 @@ 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 bool isVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property bool compact: (Settings.data.bar.density === "compact")
|
||||
readonly property real baseDimensionRatio: {
|
||||
const b = compact ? 0.85 : 0.65
|
||||
if (widgetSettings.labelMode === "none") {
|
||||
return b * 0.75
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
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,26 +53,50 @@ 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: {
|
||||
implicitWidth: isVertical ? Math.round(Style.barHeight * scaling) : computeWidth()
|
||||
implicitHeight: isVertical ? computeHeight() : Math.round(Style.barHeight * scaling)
|
||||
|
||||
function getWorkspaceWidth(ws) {
|
||||
const d = Style.capsuleHeight * root.baseDimensionRatio
|
||||
if (ws.isFocused)
|
||||
return d * 2.5
|
||||
else
|
||||
return d
|
||||
}
|
||||
|
||||
function getWorkspaceHeight(ws) {
|
||||
const d = Style.capsuleHeight * root.baseDimensionRatio
|
||||
if (ws.isFocused)
|
||||
return d * 3
|
||||
else
|
||||
return d
|
||||
}
|
||||
|
||||
function computeWidth() {
|
||||
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 += getWorkspaceWidth(ws)
|
||||
}
|
||||
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills
|
||||
total += horizontalPadding * 2
|
||||
return total
|
||||
return Math.round(total)
|
||||
}
|
||||
|
||||
function computeHeight() {
|
||||
let total = 0
|
||||
for (var i = 0; i < localWorkspaces.count; i++) {
|
||||
const ws = localWorkspaces.get(i)
|
||||
total += getWorkspaceHeight(ws)
|
||||
}
|
||||
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills
|
||||
total += horizontalPadding * 2
|
||||
return Math.round(total)
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
@@ -53,6 +108,7 @@ Item {
|
||||
}
|
||||
|
||||
onScreenChanged: refreshWorkspaces()
|
||||
onHideUnoccupiedChanged: refreshWorkspaces()
|
||||
|
||||
Connections {
|
||||
target: WorkspaceService
|
||||
@@ -67,11 +123,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 +163,7 @@ Item {
|
||||
property: "masterProgress"
|
||||
from: 0.0
|
||||
to: 1.0
|
||||
duration: 1000
|
||||
duration: Style.animationSlow * 2
|
||||
easing.type: Easing.OutQuint
|
||||
}
|
||||
PropertyAction {
|
||||
@@ -120,54 +180,68 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
id: workspaceBackground
|
||||
width: parent.width - Style.marginS * scaling * 2
|
||||
|
||||
height: Math.round(Style.capsuleHeight * scaling)
|
||||
width: isVertical ? Math.round(Style.capsuleHeight * scaling) : parent.width
|
||||
height: isVertical ? parent.height : Math.round(Style.capsuleHeight * scaling)
|
||||
radius: Math.round(Style.radiusM * scaling)
|
||||
color: Color.mSurfaceVariant
|
||||
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
|
||||
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: !isVertical
|
||||
|
||||
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)
|
||||
}
|
||||
width: root.getWorkspaceWidth(model)
|
||||
height: Style.capsuleHeight * root.baseDimensionRatio
|
||||
|
||||
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 ? workspacePillContainer.height * 0.45 : workspacePillContainer.height * 0.42
|
||||
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 +249,6 @@ Item {
|
||||
return Color.mError
|
||||
if (model.isActive || model.isOccupied)
|
||||
return Color.mSecondary
|
||||
if (model.isUrgent)
|
||||
return Color.mError
|
||||
|
||||
return Color.mOutline
|
||||
}
|
||||
@@ -260,4 +332,148 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical layout for left/right bars
|
||||
Column {
|
||||
id: pillColumn
|
||||
spacing: spacingBetweenPills
|
||||
anchors.horizontalCenter: workspaceBackground.horizontalCenter
|
||||
y: horizontalPadding
|
||||
visible: isVertical
|
||||
|
||||
Repeater {
|
||||
id: workspaceRepeaterVertical
|
||||
model: localWorkspaces
|
||||
Item {
|
||||
id: workspacePillContainerVertical
|
||||
width: Style.capsuleHeight * root.baseDimensionRatio
|
||||
height: root.getWorkspaceHeight(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 ? workspacePillContainerVertical.width * 0.45 : workspacePillContainerVertical.width * 0.42
|
||||
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 ? "busy" : 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
|
||||
baseSize: Style.baseWidgetSize * 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."
|
||||
baseSize: Style.baseWidgetSize * 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,8 @@ import qs.Widgets
|
||||
NPanel {
|
||||
id: root
|
||||
|
||||
panelWidth: 340 * scaling
|
||||
panelHeight: 320 * scaling
|
||||
panelAnchorRight: true
|
||||
preferredWidth: Settings.data.location.showWeekNumberInCalendar ? 350 : 330
|
||||
preferredHeight: 320
|
||||
|
||||
// Main Column
|
||||
panelContent: ColumnLayout {
|
||||
@@ -28,8 +27,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 +46,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()
|
||||
@@ -61,7 +60,7 @@ NPanel {
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginS * scaling
|
||||
Layout.bottomMargin: Style.marginM * scaling
|
||||
Layout.bottomMargin: Style.marginL * scaling
|
||||
}
|
||||
|
||||
// Columns label (respects locale's first day of week)
|
||||
@@ -69,62 +68,172 @@ NPanel {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.marginS * scaling // Align with grid
|
||||
Layout.rightMargin: Style.marginS * scaling
|
||||
Layout.bottomMargin: Style.marginM * scaling
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: 7
|
||||
// Week header spacer or label (same width as week number column)
|
||||
Item {
|
||||
visible: Settings.data.location.showWeekNumberInCalendar
|
||||
Layout.preferredWidth: visible ? Style.baseWidgetSize * scaling : 0
|
||||
|
||||
NText {
|
||||
text: {
|
||||
// Use the locale's first day of week setting
|
||||
let firstDay = Qt.locale().firstDayOfWeek
|
||||
let dayIndex = (firstDay + index) % 7
|
||||
return Qt.locale().dayName(dayIndex, Locale.ShortFormat)
|
||||
}
|
||||
color: Color.mSecondary
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
anchors.centerIn: parent
|
||||
text: "Week"
|
||||
color: Color.mOutline
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
font.weight: Style.fontWeightRegular
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: Style.baseWidgetSize * scaling
|
||||
}
|
||||
}
|
||||
|
||||
// Day name headers - now properly aligned with calendar grid
|
||||
GridLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
columns: 7
|
||||
rows: 1
|
||||
columnSpacing: 0
|
||||
rowSpacing: 0
|
||||
|
||||
Repeater {
|
||||
model: 7
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: Style.baseWidgetSize * scaling
|
||||
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
// Use the locale's first day of week setting
|
||||
let firstDay = Qt.locale().firstDayOfWeek
|
||||
let dayIndex = (firstDay + index) % 7
|
||||
return Qt.locale().dayName(dayIndex, Locale.ShortFormat)
|
||||
}
|
||||
color: Color.mSecondary
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Grids: days
|
||||
MonthGrid {
|
||||
id: grid
|
||||
|
||||
// Grids: days with optional week numbers
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true // Take remaining space
|
||||
Layout.fillHeight: true
|
||||
Layout.leftMargin: Style.marginS * scaling
|
||||
Layout.rightMargin: Style.marginS * scaling
|
||||
spacing: 0
|
||||
month: Time.date.getMonth()
|
||||
year: Time.date.getFullYear()
|
||||
locale: Qt.locale() // Use system locale
|
||||
|
||||
delegate: Rectangle {
|
||||
width: (Style.baseWidgetSize * scaling)
|
||||
height: (Style.baseWidgetSize * scaling)
|
||||
radius: Style.radiusS * scaling
|
||||
color: model.today ? Color.mPrimary : Color.transparent
|
||||
// Week numbers column (only visible when enabled)
|
||||
GridLayout {
|
||||
visible: Settings.data.location.showWeekNumberInCalendar
|
||||
Layout.preferredWidth: visible ? Style.baseWidgetSize * scaling : 0
|
||||
Layout.fillHeight: true
|
||||
columns: 1
|
||||
rows: 6
|
||||
columnSpacing: 0
|
||||
rowSpacing: 0
|
||||
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
text: model.day
|
||||
color: model.today ? Color.mOnPrimary : Color.mOnSurface
|
||||
opacity: model.month === grid.month ? Style.opacityHeavy : Style.opacityLight
|
||||
font.pointSize: (Style.fontSizeM * scaling)
|
||||
font.weight: model.today ? Style.fontWeightBold : Style.fontWeightRegular
|
||||
Repeater {
|
||||
model: 6 // Maximum 6 weeks in a month view
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Color.transparent
|
||||
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
color: Color.mOutline
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
text: {
|
||||
// Calculate the first day shown in the calendar grid
|
||||
let firstDay = new Date(grid.year, grid.month, 1)
|
||||
let firstDayOfWeek = Qt.locale().firstDayOfWeek
|
||||
let startOffset = (firstDay.getDay() - firstDayOfWeek + 7) % 7
|
||||
let gridStartDate = new Date(grid.year, grid.month, 1 - startOffset)
|
||||
|
||||
// Get the date for the start of this specific row
|
||||
let rowDate = new Date(gridStartDate)
|
||||
rowDate.setDate(gridStartDate.getDate() + (index * 7))
|
||||
|
||||
// Calculate week number based on the Thursday of the visual row
|
||||
// This correctly handles rows that span two different ISO weeks.
|
||||
let thursdayOfRow = new Date(rowDate)
|
||||
let offsetToThursday = (4 - thursdayOfRow.getDay() + 7) % 7
|
||||
thursdayOfRow.setDate(thursdayOfRow.getDate() + offsetToThursday)
|
||||
|
||||
// Check if this row is visible (contains days from current month)
|
||||
let rowEndDate = new Date(rowDate)
|
||||
rowEndDate.setDate(rowDate.getDate() + 6)
|
||||
|
||||
if (rowDate.getMonth() === grid.month || rowEndDate.getMonth() === grid.month || (rowDate.getMonth() < grid.month && rowEndDate.getMonth() > grid.month)) {
|
||||
return `${getISOWeekNumber(thursdayOfRow)}`
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
// The actual calendar grid
|
||||
MonthGrid {
|
||||
id: grid
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 0
|
||||
month: Time.date.getMonth()
|
||||
year: Time.date.getFullYear()
|
||||
locale: Qt.locale()
|
||||
|
||||
delegate: Rectangle {
|
||||
width: Style.baseWidgetSize * scaling
|
||||
height: Style.baseWidgetSize * scaling
|
||||
radius: Style.radiusS * scaling
|
||||
color: model.today ? Color.mPrimary : Color.transparent
|
||||
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
text: model.day
|
||||
color: model.today ? Color.mOnPrimary : Color.mOnSurface
|
||||
opacity: model.month === grid.month ? Style.opacityHeavy : Style.opacityLight
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: model.today ? Style.fontWeightBold : Style.fontWeightRegular
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getISOWeekNumber(date) {
|
||||
// Create a copy of the date and normalize to noon to prevent DST issues
|
||||
const targetDate = new Date(date.getTime())
|
||||
targetDate.setHours(12, 0, 0, 0)
|
||||
|
||||
// Roll the date to the Thursday of the week.
|
||||
// getDay() is 0 for Sunday, we want Monday to be 1 and Sunday to be 7.
|
||||
const dayOfWeek = targetDate.getDay() || 7
|
||||
targetDate.setDate(targetDate.getDate() - dayOfWeek + 4)
|
||||
|
||||
// Get the first day of that Thursday's year
|
||||
const yearStart = new Date(targetDate.getFullYear(), 0, 1)
|
||||
|
||||
// Calculate the difference in days and find the week number
|
||||
const dayOfYear = ((targetDate - yearStart) / 86400000) + 1
|
||||
return Math.ceil(dayOfYear / 7)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,37 @@
|
||||
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])
|
||||
// Will attempt to open the panel next to the bar button if any.
|
||||
notificationHistoryPanel.toggle(BarService.lookupWidget("NotificationHistory"))
|
||||
}
|
||||
function toggleDoNotDisturb() {// TODO
|
||||
function toggleDND() {
|
||||
Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,45 +42,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 +78,72 @@ 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])
|
||||
// Will attempt to open the panel next to the bar button if any.
|
||||
sidePanel.toggle(BarService.lookupWidget("SidePanelToggle"))
|
||||
}
|
||||
}
|
||||
|
||||
// Wallpaper IPC: trigger a new random wallpaper
|
||||
IpcHandler {
|
||||
target: "wallpaper"
|
||||
function toggle() {
|
||||
if (Settings.data.wallpaper.enabled) {
|
||||
wallpaperSelector.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
177
Modules/Launcher/Plugins/ApplicationsPlugin.qml
Normal file
@@ -0,0 +1,177 @@
|
||||
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: []
|
||||
|
||||
// Persistent usage tracking stored in cacheDir
|
||||
property string usageFilePath: Settings.cacheDir + "launcher_app_usage.json"
|
||||
|
||||
// Debounced saver to avoid excessive IO
|
||||
Timer {
|
||||
id: saveTimer
|
||||
interval: 750
|
||||
repeat: false
|
||||
onTriggered: usageFile.writeAdapter()
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: usageFile
|
||||
path: usageFilePath
|
||||
printErrors: false
|
||||
watchChanges: false
|
||||
|
||||
onLoadFailed: function (error) {
|
||||
if (error.toString().includes("No such file") || error === 2) {
|
||||
writeAdapter()
|
||||
}
|
||||
}
|
||||
|
||||
onAdapterUpdated: saveTimer.start()
|
||||
|
||||
JsonAdapter {
|
||||
id: usageAdapter
|
||||
// key: app id/command, value: integer count
|
||||
property var counts: ({})
|
||||
}
|
||||
}
|
||||
|
||||
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, optionally sorted by usage
|
||||
let sorted
|
||||
if (Settings.data.appLauncher.sortByMostUsed) {
|
||||
sorted = entries.slice().sort((a, b) => {
|
||||
const ua = getUsageCount(a)
|
||||
const ub = getUsageCount(b)
|
||||
if (ub !== ua)
|
||||
return ub - ua
|
||||
return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase())
|
||||
})
|
||||
} else {
|
||||
sorted = entries.slice().sort((a, b) => (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase()))
|
||||
}
|
||||
return sorted.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 () {
|
||||
// Close the launcher/NPanel immediately without any animations.
|
||||
// Ensures we are not preventing the future focusing of the app
|
||||
launcher.closeCompleted()
|
||||
|
||||
Logger.log("ApplicationsPlugin", `Launching: ${app.name}`)
|
||||
// Record usage and persist asynchronously
|
||||
if (Settings.data.appLauncher.sortByMostUsed)
|
||||
recordUsage(app)
|
||||
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 {
|
||||
Logger.log("ApplicationsPlugin", `Could not launch: ${app.name}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// Usage tracking helpers
|
||||
function getAppKey(app) {
|
||||
if (app && app.id)
|
||||
return String(app.id)
|
||||
if (app && app.command && app.command.join)
|
||||
return app.command.join(" ")
|
||||
return String(app && app.name ? app.name : "unknown")
|
||||
}
|
||||
|
||||
function getUsageCount(app) {
|
||||
const key = getAppKey(app)
|
||||
const m = usageAdapter && usageAdapter.counts ? usageAdapter.counts : null
|
||||
if (!m)
|
||||
return 0
|
||||
const v = m[key]
|
||||
return typeof v === 'number' && isFinite(v) ? v : 0
|
||||
}
|
||||
|
||||
function recordUsage(app) {
|
||||
const key = getAppKey(app)
|
||||
if (!usageAdapter.counts)
|
||||
usageAdapter.counts = ({})
|
||||
const current = getUsageCount(app)
|
||||
usageAdapter.counts[key] = current + 1
|
||||
// Trigger save via debounced timer
|
||||
saveTimer.restart()
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ Scope {
|
||||
property bool unlockInProgress: false
|
||||
property bool showFailure: false
|
||||
property string errorMessage: ""
|
||||
property string infoMessage: ""
|
||||
property bool pamAvailable: typeof PamContext !== "undefined"
|
||||
|
||||
onCurrentTextChanged: {
|
||||
@@ -28,12 +29,6 @@ Scope {
|
||||
return
|
||||
}
|
||||
|
||||
if (currentText === "") {
|
||||
errorMessage = "Password required"
|
||||
showFailure = true
|
||||
return
|
||||
}
|
||||
|
||||
root.unlockInProgress = true
|
||||
errorMessage = ""
|
||||
showFailure = false
|
||||
@@ -48,11 +43,12 @@ 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
|
||||
} else {
|
||||
infoMessage = message
|
||||
}
|
||||
|
||||
if (responseRequired) {
|
||||
|
||||
@@ -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,319 @@ 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
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
Layout.fillWidth: true
|
||||
maximumLineCount: 3
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
NText {
|
||||
text: model.body || ""
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurface
|
||||
textFormat: Text.PlainText
|
||||
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."
|
||||
baseSize: Style.baseWidgetSize * 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,9 @@ import qs.Widgets
|
||||
NPanel {
|
||||
id: root
|
||||
|
||||
panelWidth: 380 * scaling
|
||||
panelHeight: 500 * scaling
|
||||
panelAnchorRight: true
|
||||
preferredWidth: 380
|
||||
preferredHeight: 500
|
||||
panelKeyboardFocus: true
|
||||
|
||||
panelContent: Rectangle {
|
||||
id: notificationRect
|
||||
@@ -25,12 +25,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 +45,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."
|
||||
baseSize: Style.baseWidgetSize * 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."
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: {
|
||||
NotificationService.clearHistory()
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: "Close"
|
||||
sizeMultiplier: 0.8
|
||||
tooltipText: "Close."
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: {
|
||||
root.close()
|
||||
}
|
||||
@@ -65,42 +77,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 +132,46 @@ 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: 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: Color.mPrimary
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.Wrap
|
||||
width: parent.width - 60
|
||||
Layout.fillWidth: true
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
@@ -141,25 +179,29 @@ NPanel {
|
||||
NText {
|
||||
text: (body || "").substring(0, 150)
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface
|
||||
color: Color.mOnSurface
|
||||
textFormat: Text.PlainText
|
||||
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: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
// Trash icon button
|
||||
// Delete button
|
||||
NIconButton {
|
||||
icon: "delete"
|
||||
tooltipText: "Delete Notification"
|
||||
sizeMultiplier: 0.7
|
||||
icon: "trash"
|
||||
tooltipText: "Delete notification."
|
||||
baseSize: Style.baseWidgetSize * 0.7
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
onClicked: {
|
||||
Logger.log("NotificationHistory", "Removing notification:", summary)
|
||||
@@ -172,7 +214,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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
527
Modules/SettingsPanel/Bar/BarSectionEditor.qml
Normal file
@@ -0,0 +1,527 @@
|
||||
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: []
|
||||
|
||||
readonly property real miniButtonSize: Style.baseWidgetSize * 0.65
|
||||
|
||||
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"
|
||||
baseSize: miniButtonSize
|
||||
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"
|
||||
baseSize: miniButtonSize
|
||||
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
|
||||
}
|
||||
}
|
||||
49
Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml
Normal file
@@ -0,0 +1,49 @@
|
||||
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
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.displayFormat = valueDisplayFormat
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
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
|
||||
settings.textCommand = textCommandInput.text
|
||||
settings.textIntervalMs = parseInt(textIntervalInput.text || textIntervalInput.placeholderText, 10)
|
||||
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
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Dynamic Text"
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: textCommandInput
|
||||
Layout.fillWidth: true
|
||||
label: "Text Command"
|
||||
description: "Shell command to run periodically (first line becomes the text)."
|
||||
placeholderText: "echo \"Hello World\""
|
||||
text: widgetData?.textCommand || widgetMetadata.textCommand
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: textIntervalInput
|
||||
Layout.fillWidth: true
|
||||
label: "Refresh Interval"
|
||||
description: "Interval in milliseconds."
|
||||
placeholderText: String(widgetMetadata.textIntervalMs || 3000)
|
||||
text: widgetData && widgetData.textIntervalMs !== undefined ? String(widgetData.textIntervalMs) : ""
|
||||
}
|
||||
}
|
||||