Compare commits
490
Commits
release/3.7
...
v3.12.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7024a4c0f | ||
|
|
31c7e3a60f | ||
|
|
43cfe29eeb | ||
|
|
4921804283 | ||
|
|
ae4c152a8b | ||
|
|
e436163be7 | ||
|
|
9b8e65449b | ||
|
|
fbb52f4e8a | ||
|
|
704eb32048 | ||
|
|
730d9b4ce8 | ||
|
|
c7b316ecba | ||
|
|
6df5a02c3f | ||
|
|
1b9fde3d60 | ||
|
|
f435cda6a3 | ||
|
|
4a69fde6e6 | ||
|
|
294a0cd7da | ||
|
|
e057e20b3c | ||
|
|
8c0ca7c731 | ||
|
|
0d591a5429 | ||
|
|
900d7595d5 | ||
|
|
5e18c0aad4 | ||
|
|
f51f982f80 | ||
|
|
7c5430424f | ||
|
|
5909c73c48 | ||
|
|
3166c1d405 | ||
|
|
27c73b4434 | ||
|
|
705f978c43 | ||
|
|
67432bf614 | ||
|
|
378c06656d | ||
|
|
e2d6904269 | ||
|
|
8ef2f8be1b | ||
|
|
584f0f39bc | ||
|
|
6529a3bb03 | ||
|
|
0e6f43ccba | ||
|
|
94b10a8b29 | ||
|
|
dc60af6d96 | ||
|
|
272c5d1b57 | ||
|
|
5bfe945fd0 | ||
|
|
3e8a295cf2 | ||
|
|
0d717b84c4 | ||
|
|
13672a57ff | ||
|
|
b19151a811 | ||
|
|
c7d9bc696a | ||
|
|
f426ea48fa | ||
|
|
8b9eeb68f7 | ||
|
|
413069d334 | ||
|
|
bab81da466 | ||
|
|
1138d063d2 | ||
|
|
041eedb765 | ||
|
|
48046cd416 | ||
|
|
9372c0903c | ||
|
|
ee79c50dea | ||
|
|
ef3c1b8926 | ||
|
|
ade057b819 | ||
|
|
2b1de479e5 | ||
|
|
5502a4efad | ||
|
|
5c36c00479 | ||
|
|
bb92b5ff8a | ||
|
|
8b90464d30 | ||
|
|
aaea473325 | ||
|
|
e8f9f093f4 | ||
|
|
9dc493dd02 | ||
|
|
5cf142df0e | ||
|
|
2a6cd17a3c | ||
|
|
285e361a5f | ||
|
|
0b70dc6d22 | ||
|
|
c8e2802373 | ||
|
|
9ac32a308a | ||
|
|
85cc11be1e | ||
|
|
568fa354ee | ||
|
|
2728be1014 | ||
|
|
388473850d | ||
|
|
1a5b19e7fe | ||
|
|
8d3fa105f7 | ||
|
|
ed43c07d88 | ||
|
|
586dee4f1b | ||
|
|
917d11009f | ||
|
|
4fa07661bb | ||
|
|
b1b90e0855 | ||
|
|
76370099e3 | ||
|
|
2e5e21affa | ||
|
|
cceef4f8f8 | ||
|
|
68c291e23a | ||
|
|
1e42034a0b | ||
|
|
53e90ea0f4 | ||
|
|
cd989b1058 | ||
|
|
5bc5ae8d15 | ||
|
|
c8183eaa85 | ||
|
|
ae522e0027 | ||
|
|
b6fcbb831c | ||
|
|
f3fed74fbc | ||
|
|
010abf037e | ||
|
|
9202cd6d61 | ||
|
|
e54fca4acc | ||
|
|
f6658567ea | ||
|
|
ffb1300081 | ||
|
|
3187c28df0 | ||
|
|
fb0919f3fb | ||
|
|
b418c9d33b | ||
|
|
d2a9024fc8 | ||
|
|
5e5d777dae | ||
|
|
e25e7c4dbd | ||
|
|
313eec4c10 | ||
|
|
a0fc43ca1e | ||
|
|
b37ec8dd1f | ||
|
|
167577b9c6 | ||
|
|
1c1fd8b1a4 | ||
|
|
77a86ef963 | ||
|
|
32a741e2de | ||
|
|
d1ead74177 | ||
|
|
80ba1ec1a7 | ||
|
|
25fef6b5c2 | ||
|
|
64ce1854f5 | ||
|
|
fb4f0be899 | ||
|
|
eea072cea5 | ||
|
|
b0cd184e22 | ||
|
|
8b3e801a44 | ||
|
|
04bb089f3a | ||
|
|
4bd0cbc88f | ||
|
|
9c4c7bd786 | ||
|
|
f79d560605 | ||
|
|
90e6782193 | ||
|
|
ca071fd924 | ||
|
|
2e26b14322 | ||
|
|
b7fbebae43 | ||
|
|
33dc39e59e | ||
|
|
a70b8d7baa | ||
|
|
64821600c5 | ||
|
|
dc993b871f | ||
|
|
a0bd64ad1f | ||
|
|
10df7a862b | ||
|
|
9ea6a97aef | ||
|
|
dec2e4ec7a | ||
|
|
8d504cd194 | ||
|
|
2918f19482 | ||
|
|
18f3e2870b | ||
|
|
9f2874c12b | ||
|
|
3fdf21fcd5 | ||
|
|
c9e435c129 | ||
|
|
26125f4002 | ||
|
|
93be558e84 | ||
|
|
e1be52f0f0 | ||
|
|
359a90636a | ||
|
|
d9b31ffd18 | ||
|
|
4b72639969 | ||
|
|
aecbf2e476 | ||
|
|
52d8c44320 | ||
|
|
5d57052ea2 | ||
|
|
570e1e2029 | ||
|
|
5b11202a9c | ||
|
|
c2d5d1b5f6 | ||
|
|
ef5849eba1 | ||
|
|
c2c542af3c | ||
|
|
792412b4c7 | ||
|
|
2b8e8306d7 | ||
|
|
84bcb01fe1 | ||
|
|
253bdf974e | ||
|
|
5839382c64 | ||
|
|
ab9c45549b | ||
|
|
cb413fa40a | ||
|
|
982c062a01 | ||
|
|
9f097149b2 | ||
|
|
cb50c89fb9 | ||
|
|
7fb2b9b734 | ||
|
|
b933fbcd6b | ||
|
|
c880e4befb | ||
|
|
575d908dba | ||
|
|
ecf188ad18 | ||
|
|
0b3cb9964a | ||
|
|
c175f1c1b3 | ||
|
|
dbacf096c6 | ||
|
|
c7b9b73292 | ||
|
|
19079ac123 | ||
|
|
2a66880a58 | ||
|
|
8b91251337 | ||
|
|
0c2742a895 | ||
|
|
a8692c3893 | ||
|
|
6cf757e440 | ||
|
|
c0f4143618 | ||
|
|
73b30974bf | ||
|
|
879f6cfeff | ||
|
|
0fc8222e63 | ||
|
|
5c2bb1cc25 | ||
|
|
c1d0bb973a | ||
|
|
c45cd74b67 | ||
|
|
05251cb4e3 | ||
|
|
ffbbde9d8f | ||
|
|
c75bcd97b2 | ||
|
|
0cc0b21d5f | ||
|
|
a524ba8506 | ||
|
|
17be25c036 | ||
|
|
5a518cec03 | ||
|
|
0adf4fcf32 | ||
|
|
c4ed6b065b | ||
|
|
0c4805df21 | ||
|
|
27972b987e | ||
|
|
fe11e5ff1e | ||
|
|
e3231859ea | ||
|
|
90a795b2c8 | ||
|
|
46f7e4db66 | ||
|
|
d956c04f2a | ||
|
|
59e147227d | ||
|
|
4cbe874c81 | ||
|
|
e9e6ef1f20 | ||
|
|
d2ae41c9ae | ||
|
|
79a1c3af8a | ||
|
|
c06f898d39 | ||
|
|
b09b915d16 | ||
|
|
179ccb7805 | ||
|
|
f7987a91ce | ||
|
|
e68b96f67d | ||
|
|
7f6379960b | ||
|
|
317b92c31e | ||
|
|
35e65921f9 | ||
|
|
95eeae4c42 | ||
|
|
165d77df30 | ||
|
|
65a4b6159d | ||
|
|
6f690d2522 | ||
|
|
5a34ac0d2b | ||
|
|
0ccb352b7e | ||
|
|
6d8add6d54 | ||
|
|
7ab22b0e07 | ||
|
|
20249ec4bf | ||
|
|
db38b4d532 | ||
|
|
ba5e63395f | ||
|
|
55cbb6cd2c | ||
|
|
8aecbe89f9 | ||
|
|
0a0476b6d3 | ||
|
|
38612e5d6c | ||
|
|
66e75fdc9d | ||
|
|
1e76733564 | ||
|
|
370d0ce78b | ||
|
|
6f402cd218 | ||
|
|
e778d223a7 | ||
|
|
db5aa0c869 | ||
|
|
8077b0bae1 | ||
|
|
e8c6c07e6a | ||
|
|
c1396c05aa | ||
|
|
9ca95c7f17 | ||
|
|
fb2d53664b | ||
|
|
6fd6022210 | ||
|
|
83782f5628 | ||
|
|
a19f147a6a | ||
|
|
85ce3d9a7a | ||
|
|
886a1f1207 | ||
|
|
8d6245eb38 | ||
|
|
7d76fea48d | ||
|
|
600bd6e9ee | ||
|
|
ccc64db805 | ||
|
|
ea407be5f0 | ||
|
|
9b9471da49 | ||
|
|
b1402d3821 | ||
|
|
62f0d0a1c0 | ||
|
|
2865f2ffca | ||
|
|
3860e68caa | ||
|
|
74d0a89820 | ||
|
|
91e4c85a28 | ||
|
|
a05b24cfd7 | ||
|
|
72e3925696 | ||
|
|
9ea419c05b | ||
|
|
3dc1631207 | ||
|
|
ab5ef6e0ff | ||
|
|
61113d5c08 | ||
|
|
d73d7330eb | ||
|
|
45a85e006b | ||
|
|
06cce5259b | ||
|
|
3e393982e0 | ||
|
|
db2e2d89ee | ||
|
|
a3d9e29350 | ||
|
|
682d88df02 | ||
|
|
b5defd31ed | ||
|
|
553e67bda7 | ||
|
|
95bb669ffc | ||
|
|
505752476c | ||
|
|
c723acfe30 | ||
|
|
2fd7076ccf | ||
|
|
0705465ba4 | ||
|
|
7a4649f0cc | ||
|
|
8b8a62ccf1 | ||
|
|
243f3e7371 | ||
|
|
4ad368560c | ||
|
|
c6c302e3d6 | ||
|
|
686f59e21b | ||
|
|
f1bd4bffb7 | ||
|
|
18a8556997 | ||
|
|
f10c842aa4 | ||
|
|
74d379a09c | ||
|
|
3dd1d5bd62 | ||
|
|
3247b77410 | ||
|
|
ce9194057a | ||
|
|
04cf5eca59 | ||
|
|
86d65e96ec | ||
|
|
1262a760b8 | ||
|
|
acd41e5885 | ||
|
|
f22568d3e8 | ||
|
|
11d182f907 | ||
|
|
e25e58cfad | ||
|
|
2e4cc76d6a | ||
|
|
02e9d807cd | ||
|
|
8923be50ec | ||
|
|
a07c0d9746 | ||
|
|
e56c4643de | ||
|
|
fb942d1099 | ||
|
|
3842b6df32 | ||
|
|
fadd9580a2 | ||
|
|
110a58b3cd | ||
|
|
5a25ba6663 | ||
|
|
0013a52a5c | ||
|
|
05d37e3911 | ||
|
|
bc55da0720 | ||
|
|
57a582e2cb | ||
|
|
7c0faf2143 | ||
|
|
fe0515ed28 | ||
|
|
0c657ef382 | ||
|
|
4ce6588f0c | ||
|
|
d083ff9048 | ||
|
|
54f9902849 | ||
|
|
2e572d2b02 | ||
|
|
ac9f337d4e | ||
|
|
078c2ad98b | ||
|
|
830d9a05e7 | ||
|
|
7dfeebff39 | ||
|
|
05a414c93f | ||
|
|
16a432125b | ||
|
|
573e25ecdf | ||
|
|
821cc6d768 | ||
|
|
c13b5e15d4 | ||
|
|
3eea76784d | ||
|
|
22c66b2790 | ||
|
|
fcabe9ca36 | ||
|
|
2af3c8f714 | ||
|
|
253440844e | ||
|
|
cc87f6cdd1 | ||
|
|
a77ef95ce4 | ||
|
|
7fb7b00836 | ||
|
|
3252de2f3f | ||
|
|
9b0b975490 | ||
|
|
449acbc7dc | ||
|
|
1c82e4e1e1 | ||
|
|
77cdc2108f | ||
|
|
cc092f535b | ||
|
|
13930f8dcc | ||
|
|
ebad1f67ce | ||
|
|
d3d46f6a29 | ||
|
|
6f1db67e9a | ||
|
|
0d7ece819d | ||
|
|
7f852080cd | ||
|
|
e0360c2b38 | ||
|
|
3bf0eb2a16 | ||
|
|
84b428b57c | ||
|
|
41895e97e1 | ||
|
|
f839dc192c | ||
|
|
24250a6239 | ||
|
|
a3ab2b52b6 | ||
|
|
c4d4c6fcf3 | ||
|
|
f77f005f88 | ||
|
|
9ec494664c | ||
|
|
dc7c883962 | ||
|
|
567bd15997 | ||
|
|
25c9ef0f27 | ||
|
|
d45d994307 | ||
|
|
01b7a6b95a | ||
|
|
a0593c03c6 | ||
|
|
91f05315aa | ||
|
|
84b8fff37b | ||
|
|
3a9cc2b9bb | ||
|
|
056f30f6f5 | ||
|
|
2d6e01fcf9 | ||
|
|
b6d95c3ebb | ||
|
|
29e40ec1a5 | ||
|
|
497789603a | ||
|
|
8d3cb66d17 | ||
|
|
0613b342f0 | ||
|
|
18000fae30 | ||
|
|
d2c985491a | ||
|
|
f7f0d55504 | ||
|
|
210e127ac7 | ||
|
|
36748a3a07 | ||
|
|
cf667b2b8d | ||
|
|
a3f7a230e7 | ||
|
|
9dd8084be2 | ||
|
|
9cf5a5e5db | ||
|
|
8efa54dd11 | ||
|
|
7902020300 | ||
|
|
e72c0bbb50 | ||
|
|
ef3df92592 | ||
|
|
04b300f453 | ||
|
|
e455ce4c84 | ||
|
|
456d13959e | ||
|
|
7ac22ad567 | ||
|
|
ed4fec6c42 | ||
|
|
c321453399 | ||
|
|
aae02bef50 | ||
|
|
89c9a93aab | ||
|
|
74ec4aaae8 | ||
|
|
669a9dfc57 | ||
|
|
a1e75e48da | ||
|
|
75292d2a52 | ||
|
|
6b6dee6cfa | ||
|
|
6558b3b2f9 | ||
|
|
68fc2d7cd9 | ||
|
|
cda45db222 | ||
|
|
7325b36e8c | ||
|
|
6fa6f23ad4 | ||
|
|
8351ee4094 | ||
|
|
310a344f96 | ||
|
|
3b78ec9664 | ||
|
|
cfcbe6e962 | ||
|
|
fe809ad397 | ||
|
|
78a78f4fe7 | ||
|
|
6ca6429114 | ||
|
|
4a3d330f12 | ||
|
|
ac71e66fe1 | ||
|
|
3c5e4f15d9 | ||
|
|
1c37fbc97a | ||
|
|
8823ae8ee4 | ||
|
|
a7b952fc4d | ||
|
|
63ceb3c6c8 | ||
|
|
ab6e8692ad | ||
|
|
97ada51489 | ||
|
|
8e9c2f27a1 | ||
|
|
8563db3654 | ||
|
|
2695b5c42f | ||
|
|
bed87425e7 | ||
|
|
d6ad1cbdf7 | ||
|
|
da5bbd2b22 | ||
|
|
5c726913e4 | ||
|
|
02bd9e9d2d | ||
|
|
458793f0c0 | ||
|
|
0543da31cc | ||
|
|
a74dc98ce6 | ||
|
|
9dfc6155c4 | ||
|
|
e4f10d7cd8 | ||
|
|
a21a8d5045 | ||
|
|
59994bdeed | ||
|
|
a95e4c65ee | ||
|
|
df7783c568 | ||
|
|
e5df2034fd | ||
|
|
f13d53d6da | ||
|
|
75c4fb6550 | ||
|
|
44869da20e | ||
|
|
756dce0d6f | ||
|
|
4a4fa07db4 | ||
|
|
17bc003698 | ||
|
|
80b2242e4d | ||
|
|
47ccda048c | ||
|
|
ae144aa7b8 | ||
|
|
343c09b6ff | ||
|
|
1bbc6ef777 | ||
|
|
f4c6a4f891 | ||
|
|
733b2c90c4 | ||
|
|
81b61523df | ||
|
|
d93f4d6bbb | ||
|
|
f0a16b0cd9 | ||
|
|
783cadcf77 | ||
|
|
cbb4b0c51b | ||
|
|
c4d9803177 | ||
|
|
2ce4fcabcb | ||
|
|
3a30e5b048 | ||
|
|
49b0a99ced | ||
|
|
8f2c860242 | ||
|
|
8c10775de9 | ||
|
|
b5bf7ce4d0 | ||
|
|
bd6a6fa9b6 | ||
|
|
1c0ae3918d | ||
|
|
c6ce2df731 | ||
|
|
18985c0b63 | ||
|
|
322e29642d | ||
|
|
a86e5d7bf5 | ||
|
|
c90eaa2937 | ||
|
|
defacb3554 | ||
|
|
0a66863272 | ||
|
|
d90df221bc | ||
|
|
fd7f909561 | ||
|
|
a921a11be6 | ||
|
|
7fbc99f3eb | ||
|
|
7e40c799e5 | ||
|
|
f2032ab7a8 | ||
|
|
d7c6a23dbe | ||
|
|
27e1e021d7 | ||
|
|
2516fb5d69 | ||
|
|
d66c080dea | ||
|
|
94ca81035b | ||
|
|
1ab414e702 | ||
|
|
16f6e89e45 | ||
|
|
22f6bfd845 | ||
|
|
c4823b3e55 | ||
|
|
e34ec9f801 | ||
|
|
d2e87ba024 | ||
|
|
ffb50423af |
No files matched your search
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: QQ 群
|
||||
url: https://docs.hmcl.net/groups.html
|
||||
about: Hello Minecraft! Launcher 的官方 QQ 交流群。
|
||||
- name: Discord 服务器
|
||||
url: https://discord.gg/jVvC7HfM6U
|
||||
about: Hello Minecraft! Launcher 的官方 Discord 服务器。
|
||||
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: QQ 群
|
||||
url: https://docs.hmcl.net/groups.html
|
||||
about: Hello Minecraft! Launcher 的官方 QQ 交流群。
|
||||
@@ -9,15 +9,15 @@ body:
|
||||
value: |
|
||||
提交前请确认:
|
||||
|
||||
* 该问题确实是 **HMCL 的错误**,而**不是 Minecraft 非正常退出**,如果你的 Minecraft 非正常退出,请前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 服务器](https://discord.gg/jVvC7HfM6U) 获取帮助。
|
||||
* 你的启动器版本是**最新的预览版本**,可以点击 [此处](https://zkitefly.github.io/HMCL-Snapshot-Update/) 下载最新预览版本。
|
||||
* 该问题确实是 **HMCL 的错误**,而**不是 Minecraft 非正常退出**。如果你的 Minecraft 非正常退出,请前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 服务器](https://discord.gg/jVvC7HfM6U) 获取帮助。
|
||||
* 你的启动器版本是**最新的预览版本**。你可以从 [GitHub Actions](https://github.com/HMCL-dev/HMCL/actions/workflows/gradle.yml?query=branch%3Amain+event%3Apush) 或 [nightly.link](https://nightly.link/HMCL-dev/HMCL/workflows/gradle/main) 下载最新预览版本。
|
||||
|
||||
如果你的问题并不属于上述两类,你可以选取另一种 Issue 类型,或者直接前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 服务器](https://discord.gg/jVvC7HfM6U) 获取帮助。
|
||||
|
||||
Before submitting, please confirm:
|
||||
|
||||
* The issue is indeed **a bug of HMCL**, not **Minecraft abnormal exit**. If your Minecraft exits abnormally, please go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord server](https://discord.gg/jVvC7HfM6U) for help.
|
||||
* Your launcher is the **latest nightly build**. You can click [here](https://zkitefly.github.io/HMCL-Snapshot-Update/en) to download the latest nightly build.
|
||||
* Your launcher is the **latest nightly build**. You can download the latest nightly build from [GitHub Actions](https://github.com/HMCL-dev/HMCL/actions/workflows/gradle.yml?query=branch%3Amain+event%3Apush) or [nightly.link](https://nightly.link/HMCL-dev/HMCL/workflows/gradle/main).
|
||||
|
||||
If your issue does not fall into the above two categories, you can choose another type of issue or directly go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord server](https://discord.gg/jVvC7HfM6U) for help.
|
||||
- type: textarea
|
||||
@@ -38,7 +38,7 @@ body:
|
||||
attributes:
|
||||
label: 启动器崩溃报告 / 启动器日志文件 | Launcher Crash Report / Launcher Log File
|
||||
description: |
|
||||
如果你的启动器崩溃了,请将崩溃报告填入(或将文件拖入)下方。
|
||||
如果你的启动器崩溃了,请将崩溃报告填入 (或将文件拖入) 下方。
|
||||
如果你的启动器没有崩溃,请在遇到问题后**不要退出启动器**,在启动器的 “设置 → 通用 → 调试” 一栏中点击 “导出启动器日志”,并将导出的日志拖到下方的输入栏中。
|
||||
**请注意:启动器崩溃报告或日志文件是诊断问题的重要依据,请务必上传!**
|
||||
|
||||
|
||||
@@ -14,12 +14,16 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
with:
|
||||
cache-cleanup: never
|
||||
- name: Check Codes
|
||||
run: ./gradlew checkstyle checkTranslations --no-daemon --parallel --stacktrace
|
||||
@@ -1,119 +0,0 @@
|
||||
name: Check Update
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# schedule:
|
||||
# - cron: '30 * * * *'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
dev-check-update:
|
||||
if: ${{ github.repository_owner == 'HMCL-dev' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '25'
|
||||
- name: Fetch tags
|
||||
run: git fetch --all --tags
|
||||
- name: Fetch last version
|
||||
run: ./gradlew checkUpdateDev --no-daemon --info --stacktrace
|
||||
- name: Check for existing tags
|
||||
run: if [ -z "$(git tag -l "$HMCL_TAG_NAME")" ]; then echo "continue=true" >> $GITHUB_ENV; fi
|
||||
- name: Download artifacts
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.exe"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.exe.sha256"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar.sha256"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh.sha256"
|
||||
- name: Generate release note
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
echo "The full changelogs can be found on the website: https://docs.hmcl.net/changelog/dev.html" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "*Notice: changelogs are written in Chinese.*" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "| File Name | SHA-256 Checksum |" >> RELEASE_NOTE
|
||||
echo "| --- | --- |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.exe]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.jar]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.sh]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> RELEASE_NOTE
|
||||
env:
|
||||
GH_DOWNLOAD_BASE_URL: https://github.com/HMCL-dev/HMCL/releases/download
|
||||
- name: Create release
|
||||
if: ${{ env.continue == 'true' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
body_path: RELEASE_NOTE
|
||||
files: |
|
||||
HMCL-${{ env.HMCL_VERSION }}.exe
|
||||
HMCL-${{ env.HMCL_VERSION }}.jar
|
||||
HMCL-${{ env.HMCL_VERSION }}.sh
|
||||
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
|
||||
name: ${{ env.HMCL_TAG_NAME }}
|
||||
tag_name: ${{ env.HMCL_TAG_NAME }}
|
||||
prerelease: true
|
||||
stable-check-update:
|
||||
if: ${{ github.repository_owner == 'HMCL-dev' }}
|
||||
needs: dev-check-update
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '25'
|
||||
- name: Fetch tags
|
||||
run: git fetch --all --tags
|
||||
|
||||
- name: Fetch last version
|
||||
run: ./gradlew checkUpdateStable --no-daemon --info --stacktrace
|
||||
- name: Check for existing tags
|
||||
run: if ! git tag -l | grep -q "$HMCL_TAG_NAME"; then echo "continue=true" >> $GITHUB_ENV; fi
|
||||
- name: Download artifacts
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.exe"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.exe.sha256"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar.sha256"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh.sha256"
|
||||
- name: Generate release note
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
echo "**This version is a stable version.**" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "The full changelogs can be found on the website: https://docs.hmcl.net/changelog/stable.html" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "*Notice: changelogs are written in Chinese.*" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "| File Name | SHA-256 Checksum |" >> RELEASE_NOTE
|
||||
echo "| --- | --- |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.exe]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.jar]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.sh]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> RELEASE_NOTE
|
||||
env:
|
||||
GH_DOWNLOAD_BASE_URL: https://github.com/HMCL-dev/HMCL/releases/download
|
||||
- name: Create release
|
||||
if: ${{ env.continue == 'true' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
body_path: RELEASE_NOTE
|
||||
files: |
|
||||
HMCL-${{ env.HMCL_VERSION }}.exe
|
||||
HMCL-${{ env.HMCL_VERSION }}.jar
|
||||
HMCL-${{ env.HMCL_VERSION }}.sh
|
||||
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
|
||||
name: ${{ env.HMCL_TAG_NAME }}
|
||||
tag_name: ${{ env.HMCL_TAG_NAME }}
|
||||
prerelease: true
|
||||
@@ -1,21 +0,0 @@
|
||||
name: Sync to Gitee
|
||||
|
||||
on:
|
||||
push
|
||||
|
||||
jobs:
|
||||
run:
|
||||
if: ${{ github.repository_owner == 'HMCL-dev' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Mirror GitHub to Gitee
|
||||
uses: Yikun/hub-mirror-action@v1.4
|
||||
with:
|
||||
src: github/HMCL-dev
|
||||
dst: gitee/huanghongxun
|
||||
static_list: 'HMCL'
|
||||
force_update: true
|
||||
debug: true
|
||||
dst_key: ${{ secrets.GITEE_SYNC_BOT_PRIVATE_KEY }}
|
||||
dst_token: ${{ secrets.GITEE_SYNC_BOT_TOKEN }}
|
||||
cache_path: /github/workspace/hub-mirror-cache
|
||||
@@ -12,39 +12,39 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
with:
|
||||
cache-cleanup: never
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build --no-daemon
|
||||
run: ./gradlew build --no-daemon --parallel
|
||||
env:
|
||||
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
|
||||
MICROSOFT_AUTH_SECRET: ${{ secrets.MICROSOFT_AUTH_SECRET }}
|
||||
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
|
||||
- name: Get short SHA
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
- name: Upload JAR
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}-jar
|
||||
path: |
|
||||
HMCL/build/libs/HMCL-*.jar
|
||||
HMCL/build/libs/HMCL-*.jar.sha256
|
||||
path: HMCL/build/libs/HMCL-*.jar
|
||||
archive: false
|
||||
- name: Upload EXE
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}-exe
|
||||
path: |
|
||||
HMCL/build/libs/HMCL-*.exe
|
||||
HMCL/build/libs/HMCL-*.exe.sha256
|
||||
path: HMCL/build/libs/HMCL-*.exe
|
||||
archive: false
|
||||
- name: Upload SH
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}-sh
|
||||
path: |
|
||||
HMCL/build/libs/HMCL-*.sh
|
||||
HMCL/build/libs/HMCL-*.sh.sha256
|
||||
path: HMCL/build/libs/HMCL-*.sh
|
||||
archive: false
|
||||
@@ -0,0 +1,33 @@
|
||||
name: Mirror Repository
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
|
||||
concurrency:
|
||||
group: mirror-repository
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
mirror:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: Gitee
|
||||
repo: gitee.com/huanghongxun/HMCL
|
||||
user: 'hmcl-sync'
|
||||
token: 'GITEE_SYNC_TOKEN'
|
||||
- name: CNB
|
||||
repo: cnb.cool/HMCL-dev/HMCL
|
||||
user: 'cnb'
|
||||
token: 'CNB_SYNC_TOKEN'
|
||||
name: Mirror to ${{ matrix.name }}
|
||||
if: ${{ github.repository == 'HMCL-dev/HMCL' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Mirror GitHub to ${{ matrix.name }}
|
||||
run: |
|
||||
git clone --mirror "https://github.com/${{ github.repository }}.git" -- repo
|
||||
cd repo
|
||||
git push -f --prune "https://${{ matrix.user }}:${{ secrets[matrix.token] }}@${{ matrix.repo }}.git" "refs/heads/*:refs/heads/*" "refs/tags/*:refs/tags/*"
|
||||
@@ -0,0 +1,125 @@
|
||||
name: Create Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# schedule:
|
||||
# - cron: '30 * * * *'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
check-update:
|
||||
if: ${{ github.repository_owner == 'HMCL-dev' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 1
|
||||
matrix:
|
||||
include:
|
||||
- channel: dev
|
||||
task: checkUpdateDev
|
||||
- channel: stable
|
||||
task: checkUpdateStable
|
||||
runs-on: ubuntu-latest
|
||||
name: check-update-${{ matrix.channel }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '25'
|
||||
- name: Fetch last version
|
||||
run: ./gradlew ${{ matrix.task }} --no-daemon --info --stacktrace
|
||||
- name: Check for existing tags
|
||||
run: if [ -z "$(git tag -l "$HMCL_TAG_NAME")" ]; then echo "continue=true" >> $GITHUB_ENV; fi
|
||||
- name: Download artifacts
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.exe"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.exe.sha256"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar.sha256"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh.sha256"
|
||||
- name: Verify artifacts
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
export JAR_SHA256=$(cat HMCL-$HMCL_VERSION.jar.sha256 | tr -d '\n')
|
||||
export EXE_SHA256=$(cat HMCL-$HMCL_VERSION.exe.sha256 | tr -d '\n')
|
||||
export SH_SHA256=$(cat HMCL-$HMCL_VERSION.sh.sha256 | tr -d '\n')
|
||||
|
||||
echo "$JAR_SHA256 HMCL-$HMCL_VERSION.jar" | sha256sum -c
|
||||
echo "$EXE_SHA256 HMCL-$HMCL_VERSION.exe" | sha256sum -c
|
||||
echo "$SH_SHA256 HMCL-$HMCL_VERSION.sh" | sha256sum -c
|
||||
- name: Generate release note
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
# GitHub Release Note
|
||||
echo " **[Changelog](https://docs.hmcl.net/changelog/${{ matrix.channel }}.html#HMCL-$HMCL_VERSION)** (Chinese)" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "| File | SHA-256 Checksum |" >> RELEASE_NOTE
|
||||
echo "| --- | --- |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.exe]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.jar]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.sh]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> RELEASE_NOTE
|
||||
|
||||
# CNB Release Note
|
||||
echo "[更新日志](https://docs.hmcl.net/changelog/${{ matrix.channel }}.html#HMCL-$HMCL_VERSION)" >> CNB_RELEASE_NOTE
|
||||
echo "" >> CNB_RELEASE_NOTE
|
||||
echo "| 文件 | SHA-256 校验码 |" >> CNB_RELEASE_NOTE
|
||||
echo "| :--- | --- |" >> CNB_RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.exe]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> CNB_RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.jar]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> CNB_RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.sh]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> CNB_RELEASE_NOTE
|
||||
env:
|
||||
GH_DOWNLOAD_BASE_URL: https://github.com/${{ github.repository }}/releases/download
|
||||
CNB_DOWNLOAD_BASE_URL: https://cnb.cool/HMCL-dev/HMCL/-/releases/download
|
||||
- name: Create GitHub release
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
gh release create "${{ env.HMCL_TAG_NAME }}" \
|
||||
"HMCL-${{ env.HMCL_VERSION }}.exe" \
|
||||
"HMCL-${{ env.HMCL_VERSION }}.jar" \
|
||||
"HMCL-${{ env.HMCL_VERSION }}.sh" \
|
||||
--target "${{ env.HMCL_COMMIT_SHA }}" \
|
||||
--title "${{ env.HMCL_TAG_NAME }}" \
|
||||
--notes-file RELEASE_NOTE \
|
||||
--prerelease
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Install git-cnb
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: go install "cnb.cool/looc/git-cnb@$GIT_CNB_VERSION"
|
||||
env:
|
||||
GIT_CNB_VERSION: '1.1.2'
|
||||
- name: Create CNB release
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
echo "Uploading tags to CNB"
|
||||
git fetch --tags
|
||||
git push "https://cnb:${{ secrets.CNB_SYNC_TOKEN }}@cnb.cool/$CNB_REPO.git" "$HMCL_TAG_NAME"
|
||||
|
||||
echo "Creating CNB release"
|
||||
~/go/bin/git-cnb release create \
|
||||
--repo "$CNB_REPO" \
|
||||
--tag "$HMCL_TAG_NAME" \
|
||||
--name "HMCL $HMCL_VERSION" \
|
||||
--body "$(cat CNB_RELEASE_NOTE)" \
|
||||
--prerelease true
|
||||
|
||||
echo "Uploading HMCL-$HMCL_VERSION.jar"
|
||||
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "$HMCL_TAG_NAME" --file-name "HMCL-$HMCL_VERSION.jar"
|
||||
|
||||
echo "Uploading HMCL-$HMCL_VERSION.exe"
|
||||
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "$HMCL_TAG_NAME" --file-name "HMCL-$HMCL_VERSION.exe"
|
||||
|
||||
echo "Uploading HMCL-$HMCL_VERSION.sh"
|
||||
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "$HMCL_TAG_NAME" --file-name "HMCL-$HMCL_VERSION.sh"
|
||||
env:
|
||||
CNB_TOKEN: ${{ secrets.CNB_SYNC_TOKEN }}
|
||||
CNB_REPO: HMCL-dev/HMCL
|
||||
@@ -15,10 +15,14 @@ hs_err_pid*
|
||||
NVIDIA
|
||||
minecraft-exported-crash-info*
|
||||
hmcl-exported-logs-*
|
||||
terracotta-log-*
|
||||
/.java/
|
||||
/.local/
|
||||
/.cache/
|
||||
|
||||
# IANA Language Subtag Registry
|
||||
language-subtag-registry
|
||||
|
||||
# gradle build
|
||||
/build/
|
||||
/HMCL/build/
|
||||
|
||||
+41
-8
@@ -1,3 +1,4 @@
|
||||
import org.jackhuang.hmcl.gradle.TerracottaConfigUpgradeTask
|
||||
import org.jackhuang.hmcl.gradle.ci.GitHubActionUtils
|
||||
import org.jackhuang.hmcl.gradle.ci.JenkinsUtils
|
||||
import org.jackhuang.hmcl.gradle.l10n.CheckTranslations
|
||||
@@ -27,7 +28,6 @@ val versionType = System.getenv("VERSION_TYPE") ?: if (isOfficial) "nightly" els
|
||||
val versionRoot = System.getenv("VERSION_ROOT") ?: projectConfig.getProperty("versionRoot") ?: "3"
|
||||
|
||||
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
|
||||
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
|
||||
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
|
||||
|
||||
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE") ?: ""
|
||||
@@ -58,6 +58,8 @@ dependencies {
|
||||
implementation("libs:JFoenix")
|
||||
implementation(libs.twelvemonkeys.imageio.webp)
|
||||
implementation(libs.java.info)
|
||||
implementation(libs.monet.fx)
|
||||
implementation(libs.nayuki.qrcodegen)
|
||||
|
||||
if (launcherExe.isBlank()) {
|
||||
implementation(libs.hmclauncher)
|
||||
@@ -117,10 +119,6 @@ tasks.checkstyleMain {
|
||||
exclude("**/org/jackhuang/hmcl/ui/image/apng/**")
|
||||
}
|
||||
|
||||
tasks.compileJava {
|
||||
options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
|
||||
}
|
||||
|
||||
val addOpens = listOf(
|
||||
"java.base/java.lang",
|
||||
"java.base/java.lang.reflect",
|
||||
@@ -128,15 +126,25 @@ val addOpens = listOf(
|
||||
"javafx.base/com.sun.javafx.binding",
|
||||
"javafx.base/com.sun.javafx.event",
|
||||
"javafx.base/com.sun.javafx.runtime",
|
||||
"javafx.base/javafx.beans.property",
|
||||
"javafx.graphics/javafx.css",
|
||||
"javafx.graphics/javafx.stage",
|
||||
"javafx.graphics/javafx.scene",
|
||||
"javafx.graphics/com.sun.glass.ui",
|
||||
"javafx.graphics/com.sun.javafx.stage",
|
||||
"javafx.graphics/com.sun.javafx.util",
|
||||
"javafx.graphics/com.sun.prism",
|
||||
"javafx.controls/com.sun.javafx.scene.control",
|
||||
"javafx.controls/com.sun.javafx.scene.control.behavior",
|
||||
"javafx.graphics/com.sun.javafx.tk.quantum",
|
||||
"javafx.controls/javafx.scene.control.skin",
|
||||
"jdk.attach/sun.tools.attach",
|
||||
)
|
||||
|
||||
tasks.compileJava {
|
||||
options.compilerArgs.addAll(addOpens.map { "--add-exports=$it=ALL-UNNAMED" })
|
||||
}
|
||||
|
||||
val hmclProperties = buildList {
|
||||
add("hmcl.version" to project.version.toString())
|
||||
add("hmcl.add-opens" to addOpens.joinToString(" "))
|
||||
@@ -145,7 +153,6 @@ val hmclProperties = buildList {
|
||||
}
|
||||
add("hmcl.version.type" to versionType)
|
||||
add("hmcl.microsoft.auth.id" to microsoftAuthId)
|
||||
add("hmcl.microsoft.auth.secret" to microsoftAuthSecret)
|
||||
add("hmcl.curseforge.apikey" to curseForgeApiKey)
|
||||
add("hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!)
|
||||
}
|
||||
@@ -202,7 +209,8 @@ tasks.shadowJar {
|
||||
"Main-Class" to "org.jackhuang.hmcl.Main",
|
||||
"Multi-Release" to "true",
|
||||
"Add-Opens" to addOpens.joinToString(" "),
|
||||
"Enable-Native-Access" to "ALL-UNNAMED"
|
||||
"Enable-Native-Access" to "ALL-UNNAMED",
|
||||
"Enable-Final-Field-Mutation" to "ALL-UNNAMED",
|
||||
)
|
||||
|
||||
if (launcherExe.isNotBlank()) {
|
||||
@@ -233,6 +241,11 @@ tasks.processResources {
|
||||
from(upsideDownTranslate.map { it.outputFile })
|
||||
from(createLocaleNamesResourceBundle.map { it.outputDirectory })
|
||||
}
|
||||
|
||||
inputs.property("terracotta_version", libs.versions.terracotta)
|
||||
doLast {
|
||||
upgradeTerracottaConfig.get().checkValid()
|
||||
}
|
||||
}
|
||||
|
||||
val makeExecutables by tasks.registering {
|
||||
@@ -335,7 +348,7 @@ tasks.register<JavaExec>("run") {
|
||||
classpath = files(jarPath)
|
||||
workingDir = rootProject.rootDir
|
||||
|
||||
val vmOptions = parseToolOptions(System.getenv("HMCL_JAVA_OPTS"))
|
||||
val vmOptions = parseToolOptions(System.getenv("HMCL_JAVA_OPTS") ?: "-Xmx1g")
|
||||
if (vmOptions.none { it.startsWith("-Dhmcl.offline.auth.restricted=") })
|
||||
vmOptions += "-Dhmcl.offline.auth.restricted=false"
|
||||
|
||||
@@ -355,6 +368,26 @@ tasks.register<JavaExec>("run") {
|
||||
}
|
||||
}
|
||||
|
||||
// terracotta
|
||||
|
||||
val upgradeTerracottaConfig = tasks.register<TerracottaConfigUpgradeTask>("upgradeTerracottaConfig") {
|
||||
val destination = layout.projectDirectory.file("src/main/resources/assets/terracotta.json")
|
||||
val source = layout.projectDirectory.file("terracotta-template.json");
|
||||
|
||||
classifiers.set(listOf(
|
||||
"windows-x86_64", "windows-arm64",
|
||||
"macos-x86_64", "macos-arm64",
|
||||
"linux-x86_64", "linux-arm64", "linux-loongarch64", "linux-riscv64",
|
||||
"freebsd-x86_64"
|
||||
))
|
||||
|
||||
version.set(libs.versions.terracotta)
|
||||
downloadURL.set($$"https://github.com/burningtnt/Terracotta/releases/download/v${version}/terracotta-${version}-${classifier}-pkg.tar.gz")
|
||||
|
||||
templateFile.set(source)
|
||||
outputFile.set(destination)
|
||||
}
|
||||
|
||||
// Check Translations
|
||||
|
||||
tasks.register<CheckTranslations>("checkTranslations") {
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
//
|
||||
// Source code recreated from a .class file by IntelliJ IDEA
|
||||
// (powered by Fernflower decompiler)
|
||||
//
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.converters.ButtonTypeConverter;
|
||||
import com.jfoenix.skins.JFXButtonSkin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.css.CssMetaData;
|
||||
import javafx.css.SimpleStyleableObjectProperty;
|
||||
import javafx.css.Styleable;
|
||||
import javafx.css.StyleableObjectProperty;
|
||||
import javafx.css.StyleableProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Labeled;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.paint.Paint;
|
||||
|
||||
public class JFXButton extends Button {
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-button";
|
||||
|
||||
private List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
||||
|
||||
public JFXButton() {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
public JFXButton(String text) {
|
||||
super(text);
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
public JFXButton(String text, Node graphic) {
|
||||
super(text, graphic);
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new JFXButtonSkin(this);
|
||||
}
|
||||
|
||||
private final ObjectProperty<Paint> ripplerFill = new SimpleObjectProperty<>(this, "ripplerFill", null);
|
||||
|
||||
public final ObjectProperty<Paint> ripplerFillProperty() {
|
||||
return this.ripplerFill;
|
||||
}
|
||||
|
||||
public final Paint getRipplerFill() {
|
||||
return this.ripplerFillProperty().get();
|
||||
}
|
||||
|
||||
public final void setRipplerFill(Paint ripplerFill) {
|
||||
this.ripplerFillProperty().set(ripplerFill);
|
||||
}
|
||||
|
||||
private final StyleableObjectProperty<ButtonType> buttonType = new SimpleStyleableObjectProperty<>(
|
||||
JFXButton.StyleableProperties.BUTTON_TYPE, this, "buttonType", JFXButton.ButtonType.FLAT);
|
||||
|
||||
public ButtonType getButtonType() {
|
||||
return this.buttonType == null ? JFXButton.ButtonType.FLAT : this.buttonType.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<ButtonType> buttonTypeProperty() {
|
||||
return this.buttonType;
|
||||
}
|
||||
|
||||
public void setButtonType(ButtonType type) {
|
||||
this.buttonType.set(type);
|
||||
}
|
||||
|
||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
||||
if (this.STYLEABLES == null) {
|
||||
List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
||||
styleables.addAll(getClassCssMetaData());
|
||||
styleables.addAll(Labeled.getClassCssMetaData());
|
||||
this.STYLEABLES = List.copyOf(styleables);
|
||||
}
|
||||
|
||||
return this.STYLEABLES;
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return JFXButton.StyleableProperties.CHILD_STYLEABLES;
|
||||
}
|
||||
|
||||
protected void layoutChildren() {
|
||||
super.layoutChildren();
|
||||
this.setNeedsLayout(false);
|
||||
}
|
||||
|
||||
public enum ButtonType {
|
||||
FLAT,
|
||||
RAISED;
|
||||
}
|
||||
|
||||
private static final class StyleableProperties {
|
||||
private static final CssMetaData<JFXButton, ButtonType> BUTTON_TYPE;
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
||||
|
||||
static {
|
||||
BUTTON_TYPE = new CssMetaData<>("-jfx-button-type", ButtonTypeConverter.getInstance(), JFXButton.ButtonType.FLAT) {
|
||||
public boolean isSettable(JFXButton control) {
|
||||
return control.buttonType == null || !control.buttonType.isBound();
|
||||
}
|
||||
|
||||
public StyleableProperty<ButtonType> getStyleableProperty(JFXButton control) {
|
||||
return control.buttonTypeProperty();
|
||||
}
|
||||
};
|
||||
List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
||||
Collections.addAll(styleables, BUTTON_TYPE);
|
||||
CHILD_STYLEABLES = List.copyOf(styleables);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.skins.JFXCheckBoxSkin;
|
||||
import javafx.css.*;
|
||||
import javafx.css.converter.PaintConverter;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/// JFXCheckBox is the material design implementation of a checkbox.
|
||||
/// it shows ripple effect and a custom selection animation.
|
||||
///
|
||||
/// @author Shadi Shaheen
|
||||
/// @version 1.0
|
||||
/// @since 2016-03-09
|
||||
public class JFXCheckBox extends CheckBox {
|
||||
|
||||
public JFXCheckBox(String text) {
|
||||
super(text);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public JFXCheckBox() {
|
||||
initialize();
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new JFXCheckBoxSkin(this);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Stylesheet Handling *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/// Initialize the style class to 'jfx-check-box'.
|
||||
///
|
||||
/// This is the selector class from which CSS can be used to style
|
||||
/// this control.
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-check-box";
|
||||
|
||||
/// checkbox color property when selected
|
||||
private StyleableObjectProperty<Paint> checkedColor;
|
||||
|
||||
private static final Color DEFAULT_CHECKED_COLOR = Color.valueOf("#0F9D58");
|
||||
|
||||
public StyleableObjectProperty<Paint> checkedColorProperty() {
|
||||
if (checkedColor == null) {
|
||||
checkedColor = new SimpleStyleableObjectProperty<>(StyleableProperties.CHECKED_COLOR,
|
||||
JFXCheckBox.this,
|
||||
"checkedColor",
|
||||
DEFAULT_CHECKED_COLOR);
|
||||
}
|
||||
return this.checkedColor;
|
||||
}
|
||||
|
||||
public Paint getCheckedColor() {
|
||||
return checkedColor == null ? DEFAULT_CHECKED_COLOR : checkedColor.get();
|
||||
}
|
||||
|
||||
public void setCheckedColor(Paint color) {
|
||||
this.checkedColor.set(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* checkbox color property when not selected
|
||||
*/
|
||||
private StyleableObjectProperty<Paint> unCheckedColor;
|
||||
|
||||
private static final Color DEFAULT_UNCHECKED_COLOR = Color.valueOf("#5A5A5A");
|
||||
|
||||
public StyleableObjectProperty<Paint> unCheckedColorProperty() {
|
||||
if (unCheckedColor == null) {
|
||||
unCheckedColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNCHECKED_COLOR,
|
||||
JFXCheckBox.this,
|
||||
"unCheckedColor",
|
||||
DEFAULT_UNCHECKED_COLOR);
|
||||
}
|
||||
return this.unCheckedColor;
|
||||
}
|
||||
|
||||
public Paint getUnCheckedColor() {
|
||||
return unCheckedColor == null ? DEFAULT_UNCHECKED_COLOR : unCheckedColor.get();
|
||||
}
|
||||
|
||||
public void setUnCheckedColor(Paint color) {
|
||||
this.unCheckedColor.set(color);
|
||||
}
|
||||
|
||||
private static final class StyleableProperties {
|
||||
private static final CssMetaData<JFXCheckBox, Paint> CHECKED_COLOR =
|
||||
new CssMetaData<>("-jfx-checked-color",
|
||||
PaintConverter.getInstance(), DEFAULT_CHECKED_COLOR) {
|
||||
@Override
|
||||
public boolean isSettable(JFXCheckBox control) {
|
||||
return control.checkedColor == null || !control.checkedColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(JFXCheckBox control) {
|
||||
return control.checkedColorProperty();
|
||||
}
|
||||
};
|
||||
private static final CssMetaData<JFXCheckBox, Paint> UNCHECKED_COLOR =
|
||||
new CssMetaData<>("-jfx-unchecked-color",
|
||||
PaintConverter.getInstance(), DEFAULT_UNCHECKED_COLOR) {
|
||||
@Override
|
||||
public boolean isSettable(JFXCheckBox control) {
|
||||
return control.unCheckedColor == null || !control.unCheckedColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(JFXCheckBox control) {
|
||||
return control.unCheckedColorProperty();
|
||||
}
|
||||
};
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
||||
|
||||
static {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables =
|
||||
new ArrayList<>(CheckBox.getClassCssMetaData());
|
||||
Collections.addAll(styleables,
|
||||
CHECKED_COLOR,
|
||||
UNCHECKED_COLOR
|
||||
);
|
||||
CHILD_STYLEABLES = List.copyOf(styleables);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
||||
return getClassCssMetaData();
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.CHILD_STYLEABLES;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.utils.JFXNodeUtils;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
/**
|
||||
* JFXClippedPane is a StackPane that clips its content if exceeding the pane bounds.
|
||||
*
|
||||
* @author Shadi Shaheen
|
||||
* @version 1.0
|
||||
* @since 2018-06-02
|
||||
*/
|
||||
public class JFXClippedPane extends StackPane {
|
||||
|
||||
private final Region clip = new Region();
|
||||
|
||||
public JFXClippedPane() {
|
||||
super();
|
||||
init();
|
||||
}
|
||||
|
||||
public JFXClippedPane(Node... children) {
|
||||
super(children);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
setClip(clip);
|
||||
clip.setBackground(new Background(new BackgroundFill(Color.BLACK, new CornerRadii(2), Insets.EMPTY)));
|
||||
backgroundProperty().addListener(observable -> JFXNodeUtils.updateBackground(getBackground(), clip));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren() {
|
||||
super.layoutChildren();
|
||||
clip.resizeRelocate(0, 0, getWidth(), getHeight());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.skins.JFXColorPickerSkin;
|
||||
import javafx.css.CssMetaData;
|
||||
import javafx.css.SimpleStyleableBooleanProperty;
|
||||
import javafx.css.Styleable;
|
||||
import javafx.css.StyleableBooleanProperty;
|
||||
import javafx.css.converter.BooleanConverter;
|
||||
import javafx.scene.control.ColorPicker;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JFXColorPicker is the metrial design implementation of color picker.
|
||||
*
|
||||
* @author Shadi Shaheen
|
||||
* @version 1.0
|
||||
* @since 2016-03-09
|
||||
*/
|
||||
public class JFXColorPicker extends ColorPicker {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public JFXColorPicker() {
|
||||
initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public JFXColorPicker(Color color) {
|
||||
super(color);
|
||||
initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new JFXColorPickerSkin(this);
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the style class to 'jfx-color-picker'.
|
||||
* <p>
|
||||
* This is the selector class from which CSS can be used to style
|
||||
* this control.
|
||||
*/
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-color-picker";
|
||||
|
||||
private double[] preDefinedColors = null;
|
||||
|
||||
public double[] getPreDefinedColors() {
|
||||
return preDefinedColors;
|
||||
}
|
||||
|
||||
public void setPreDefinedColors(double[] preDefinedColors) {
|
||||
this.preDefinedColors = preDefinedColors;
|
||||
}
|
||||
|
||||
/**
|
||||
* disable animation on button action
|
||||
*/
|
||||
private final StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION,
|
||||
JFXColorPicker.this,
|
||||
"disableAnimation",
|
||||
false);
|
||||
|
||||
public final StyleableBooleanProperty disableAnimationProperty() {
|
||||
return this.disableAnimation;
|
||||
}
|
||||
|
||||
public final Boolean isDisableAnimation() {
|
||||
return disableAnimation != null && this.disableAnimationProperty().get();
|
||||
}
|
||||
|
||||
public final void setDisableAnimation(final Boolean disabled) {
|
||||
this.disableAnimationProperty().set(disabled);
|
||||
}
|
||||
|
||||
private static final class StyleableProperties {
|
||||
|
||||
private static final CssMetaData<JFXColorPicker, Boolean> DISABLE_ANIMATION =
|
||||
new CssMetaData<JFXColorPicker, Boolean>("-jfx-disable-animation",
|
||||
BooleanConverter.getInstance(), false) {
|
||||
@Override
|
||||
public boolean isSettable(JFXColorPicker control) {
|
||||
return control.disableAnimation == null || !control.disableAnimation.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableBooleanProperty getStyleableProperty(JFXColorPicker control) {
|
||||
return control.disableAnimationProperty();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
||||
|
||||
static {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables =
|
||||
new ArrayList<>(ColorPicker.getClassCssMetaData());
|
||||
Collections.addAll(styleables, DISABLE_ANIMATION);
|
||||
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
||||
return getClassCssMetaData();
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.CHILD_STYLEABLES;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.converters.base.NodeConverter;
|
||||
import com.jfoenix.skins.JFXComboBoxListViewSkin;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.*;
|
||||
import javafx.css.converter.BooleanConverter;
|
||||
import javafx.css.converter.PaintConverter;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu;
|
||||
|
||||
/**
|
||||
* JFXComboBox is the material design implementation of a combobox.
|
||||
*
|
||||
* @author Shadi Shaheen
|
||||
* @version 1.0
|
||||
* @since 2016-03-09
|
||||
*/
|
||||
public class JFXComboBox<T> extends ComboBox<T> {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public JFXComboBox() {
|
||||
initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public JFXComboBox(ObservableList<T> items) {
|
||||
super(items);
|
||||
initialize();
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
this.setCellFactory(listView -> new JFXListCell<T>() {
|
||||
@Override
|
||||
public void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
updateDisplayText(this, item, empty);
|
||||
}
|
||||
});
|
||||
|
||||
// had to refactor the code out of the skin class to allow
|
||||
// customization of the button cell
|
||||
this.setButtonCell(new ListCell<T>() {
|
||||
{
|
||||
// fixed clearing the combo box value is causing
|
||||
// java prompt text to be shown because the button cell is not updated
|
||||
JFXComboBox.this.valueProperty().addListener(observable -> {
|
||||
if (JFXComboBox.this.getValue() == null) {
|
||||
updateItem(null, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(T item, boolean empty) {
|
||||
updateDisplayText(this, item, empty);
|
||||
this.setVisible(item != null || !empty);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
useJFXContextMenu(editorProperty().get());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new JFXComboBoxListViewSkin<T>(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the style class to 'jfx-combo-box'.
|
||||
* <p>
|
||||
* This is the selector class from which CSS can be used to style
|
||||
* this control.
|
||||
*/
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-combo-box";
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Node Converter Property *
|
||||
* *
|
||||
**************************************************************************/
|
||||
/**
|
||||
* Converts the user-typed input (when the ComboBox is
|
||||
* {@link #editableProperty() editable}) to an object of type T, such that
|
||||
* the input may be retrieved via the {@link #valueProperty() value} property.
|
||||
*/
|
||||
public ObjectProperty<NodeConverter<T>> nodeConverterProperty() {
|
||||
return nodeConverter;
|
||||
}
|
||||
|
||||
private ObjectProperty<NodeConverter<T>> nodeConverter = new SimpleObjectProperty<>(this, "nodeConverter",
|
||||
JFXComboBox.<T>defaultNodeConverter());
|
||||
|
||||
public final void setNodeConverter(NodeConverter<T> value) {
|
||||
nodeConverterProperty().set(value);
|
||||
}
|
||||
|
||||
public final NodeConverter<T> getNodeConverter() {
|
||||
return nodeConverterProperty().get();
|
||||
}
|
||||
|
||||
private static <T> NodeConverter<T> defaultNodeConverter() {
|
||||
return new NodeConverter<T>() {
|
||||
@Override
|
||||
public Node toNode(T object) {
|
||||
if (object == null) {
|
||||
return null;
|
||||
}
|
||||
StackPane selectedValueContainer = new StackPane();
|
||||
selectedValueContainer.getStyleClass().add("combo-box-selected-value-container");
|
||||
selectedValueContainer.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null)));
|
||||
Label selectedValueLabel = object instanceof Label ? new Label(((Label) object).getText()) : new Label(
|
||||
object.toString());
|
||||
selectedValueLabel.setTextFill(Color.BLACK);
|
||||
selectedValueContainer.getChildren().add(selectedValueLabel);
|
||||
StackPane.setAlignment(selectedValueLabel, Pos.CENTER_LEFT);
|
||||
StackPane.setMargin(selectedValueLabel, new Insets(0, 0, 0, 5));
|
||||
return selectedValueContainer;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public T fromNode(Node node) {
|
||||
return (T) node;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(T object) {
|
||||
if (object == null) {
|
||||
return null;
|
||||
}
|
||||
if (object instanceof Label) {
|
||||
return ((Label) object).getText();
|
||||
}
|
||||
return object.toString();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private boolean updateDisplayText(ListCell<T> cell, T item, boolean empty) {
|
||||
if (empty) {
|
||||
// create empty cell
|
||||
if (cell == null) {
|
||||
return true;
|
||||
}
|
||||
cell.setGraphic(null);
|
||||
cell.setText(null);
|
||||
return true;
|
||||
} else if (item instanceof Node) {
|
||||
Node currentNode = cell.getGraphic();
|
||||
Node newNode = (Node) item;
|
||||
// create a node from the selected node of the listview
|
||||
// using JFXComboBox {@link #nodeConverterProperty() NodeConverter})
|
||||
NodeConverter<T> nc = this.getNodeConverter();
|
||||
Node node = nc == null ? null : nc.toNode(item);
|
||||
if (currentNode == null || !currentNode.equals(newNode)) {
|
||||
cell.setText(null);
|
||||
cell.setGraphic(node == null ? newNode : node);
|
||||
}
|
||||
return node == null;
|
||||
} else {
|
||||
// run item through StringConverter if it isn't null
|
||||
StringConverter<T> c = this.getConverter();
|
||||
String s = item == null ? this.getPromptText() : (c == null ? item.toString() : c.toString(item));
|
||||
cell.setText(s);
|
||||
cell.setGraphic(null);
|
||||
return s == null || s.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* styleable Properties *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/**
|
||||
* set true to show a float the prompt text when focusing the field
|
||||
*/
|
||||
private StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT,
|
||||
JFXComboBox.this,
|
||||
"lableFloat",
|
||||
false);
|
||||
|
||||
public final StyleableBooleanProperty labelFloatProperty() {
|
||||
return this.labelFloat;
|
||||
}
|
||||
|
||||
public final boolean isLabelFloat() {
|
||||
return this.labelFloatProperty().get();
|
||||
}
|
||||
|
||||
public final void setLabelFloat(final boolean labelFloat) {
|
||||
this.labelFloatProperty().set(labelFloat);
|
||||
}
|
||||
|
||||
/**
|
||||
* default color used when the field is unfocused
|
||||
*/
|
||||
private StyleableObjectProperty<Paint> unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR,
|
||||
JFXComboBox.this,
|
||||
"unFocusColor",
|
||||
Color.rgb(77,
|
||||
77,
|
||||
77));
|
||||
|
||||
public Paint getUnFocusColor() {
|
||||
return unFocusColor == null ? Color.rgb(77, 77, 77) : unFocusColor.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> unFocusColorProperty() {
|
||||
return this.unFocusColor;
|
||||
}
|
||||
|
||||
public void setUnFocusColor(Paint color) {
|
||||
this.unFocusColor.set(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* default color used when the field is focused
|
||||
*/
|
||||
private StyleableObjectProperty<Paint> focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR,
|
||||
JFXComboBox.this,
|
||||
"focusColor",
|
||||
Color.valueOf("#4059A9"));
|
||||
|
||||
public Paint getFocusColor() {
|
||||
return focusColor == null ? Color.valueOf("#4059A9") : focusColor.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> focusColorProperty() {
|
||||
return this.focusColor;
|
||||
}
|
||||
|
||||
public void setFocusColor(Paint color) {
|
||||
this.focusColor.set(color);
|
||||
}
|
||||
|
||||
private final static class StyleableProperties {
|
||||
private static final CssMetaData<JFXComboBox<?>, Paint> UNFOCUS_COLOR = new CssMetaData<JFXComboBox<?>, Paint>(
|
||||
"-jfx-unfocus-color",
|
||||
PaintConverter.getInstance(),
|
||||
Color.valueOf("#A6A6A6")) {
|
||||
@Override
|
||||
public boolean isSettable(JFXComboBox<?> control) {
|
||||
return control.unFocusColor == null || !control.unFocusColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(JFXComboBox<?> control) {
|
||||
return control.unFocusColorProperty();
|
||||
}
|
||||
};
|
||||
private static final CssMetaData<JFXComboBox<?>, Paint> FOCUS_COLOR = new CssMetaData<JFXComboBox<?>, Paint>(
|
||||
"-jfx-focus-color",
|
||||
PaintConverter.getInstance(),
|
||||
Color.valueOf("#3f51b5")) {
|
||||
@Override
|
||||
public boolean isSettable(JFXComboBox<?> control) {
|
||||
return control.focusColor == null || !control.focusColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(JFXComboBox<?> control) {
|
||||
return control.focusColorProperty();
|
||||
}
|
||||
};
|
||||
private static final CssMetaData<JFXComboBox<?>, Boolean> LABEL_FLOAT = new CssMetaData<JFXComboBox<?>, Boolean>(
|
||||
"-jfx-label-float",
|
||||
BooleanConverter.getInstance(),
|
||||
false) {
|
||||
@Override
|
||||
public boolean isSettable(JFXComboBox<?> control) {
|
||||
return control.labelFloat == null || !control.labelFloat.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableBooleanProperty getStyleableProperty(JFXComboBox<?> control) {
|
||||
return control.labelFloatProperty();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
||||
|
||||
static {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(
|
||||
Control.getClassCssMetaData());
|
||||
Collections.addAll(styleables, UNFOCUS_COLOR, FOCUS_COLOR, LABEL_FLOAT);
|
||||
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
}
|
||||
|
||||
// inherit the styleable properties from parent
|
||||
private List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
||||
if (STYLEABLES == null) {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(
|
||||
Control.getClassCssMetaData());
|
||||
styleables.addAll(getClassCssMetaData());
|
||||
styleables.addAll(Control.getClassCssMetaData());
|
||||
STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
return STYLEABLES;
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.CHILD_STYLEABLES;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,533 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.controls.events.JFXDialogEvent;
|
||||
import com.jfoenix.converters.DialogTransitionConverter;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import com.jfoenix.transitions.CachedTransition;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.animation.Transition;
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ObjectPropertyBase;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.css.CssMetaData;
|
||||
import javafx.css.SimpleStyleableObjectProperty;
|
||||
import javafx.css.Styleable;
|
||||
import javafx.css.StyleableObjectProperty;
|
||||
import javafx.css.StyleableProperty;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.CacheHint;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.SnapshotParameters;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/// Note: for JFXDialog to work properly, the root node **MUST**
|
||||
/// be of type [StackPane]
|
||||
///
|
||||
/// @author Shadi Shaheen
|
||||
/// @version 1.0
|
||||
/// @since 2016-03-09
|
||||
@DefaultProperty(value = "content")
|
||||
public class JFXDialog extends StackPane {
|
||||
|
||||
private static final double INITIAL_SCALE = 0.8;
|
||||
|
||||
// public static enum JFXDialogLayout{PLAIN, HEADING, ACTIONS, BACKDROP};
|
||||
public enum DialogTransition {
|
||||
CENTER, NONE
|
||||
}
|
||||
|
||||
private StackPane contentHolder;
|
||||
|
||||
private double offsetX = 0;
|
||||
private double offsetY = 0;
|
||||
|
||||
private StackPane dialogContainer;
|
||||
private Region content;
|
||||
private Transition animation;
|
||||
|
||||
private final EventHandler<? super MouseEvent> closeHandler = e -> close();
|
||||
|
||||
/// creates empty JFXDialog control with CENTER animation type
|
||||
public JFXDialog() {
|
||||
this(null, null, DialogTransition.CENTER);
|
||||
}
|
||||
|
||||
/// creates empty JFXDialog control with a specified animation type
|
||||
public JFXDialog(DialogTransition transition) {
|
||||
this(null, null, transition);
|
||||
}
|
||||
|
||||
/// creates JFXDialog control with a specified animation type, the animation type
|
||||
/// can be one of the following:
|
||||
///
|
||||
/// - CENTER
|
||||
/// - TOP
|
||||
/// - RIGHT
|
||||
/// - BOTTOM
|
||||
/// - LEFT
|
||||
///
|
||||
/// @param dialogContainer is the parent of the dialog, it
|
||||
/// @param content the content of dialog
|
||||
/// @param transitionType the animation type
|
||||
public JFXDialog(StackPane dialogContainer, Region content, DialogTransition transitionType) {
|
||||
initialize();
|
||||
setContent(content);
|
||||
setDialogContainer(dialogContainer);
|
||||
this.transitionType.set(transitionType);
|
||||
// init change listeners
|
||||
initChangeListeners();
|
||||
}
|
||||
|
||||
/// creates JFXDialog control with a specified animation type that
|
||||
/// is closed when clicking on the overlay, the animation type
|
||||
/// can be one of the following:
|
||||
///
|
||||
/// - CENTER
|
||||
/// - TOP
|
||||
/// - RIGHT
|
||||
/// - BOTTOM
|
||||
/// - LEFT
|
||||
public JFXDialog(StackPane dialogContainer, Region content, DialogTransition transitionType, boolean overlayClose) {
|
||||
setOverlayClose(overlayClose);
|
||||
initialize();
|
||||
setContent(content);
|
||||
setDialogContainer(dialogContainer);
|
||||
this.transitionType.set(transitionType);
|
||||
// init change listeners
|
||||
initChangeListeners();
|
||||
}
|
||||
|
||||
private void initChangeListeners() {
|
||||
overlayCloseProperty().addListener((o, oldVal, newVal) -> {
|
||||
if (newVal) {
|
||||
this.addEventHandler(MouseEvent.MOUSE_PRESSED, closeHandler);
|
||||
} else {
|
||||
this.removeEventHandler(MouseEvent.MOUSE_PRESSED, closeHandler);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.setVisible(false);
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
this.transitionType.addListener((o, oldVal, newVal) -> {
|
||||
animation = getShowAnimation(transitionType.get());
|
||||
});
|
||||
|
||||
contentHolder = new StackPane();
|
||||
contentHolder.setBackground(new Background(new BackgroundFill(Color.WHITE, new CornerRadii(2), null)));
|
||||
JFXDepthManager.setDepth(contentHolder, 4);
|
||||
contentHolder.setPickOnBounds(false);
|
||||
// ensure stackpane is never resized beyond it's preferred size
|
||||
contentHolder.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
|
||||
this.getChildren().add(contentHolder);
|
||||
this.getStyleClass().add("jfx-dialog-overlay-pane");
|
||||
StackPane.setAlignment(contentHolder, Pos.CENTER);
|
||||
this.setBackground(new Background(new BackgroundFill(Color.rgb(0, 0, 0, 0.1), null, null)));
|
||||
// close the dialog if clicked on the overlay pane
|
||||
if (overlayClose.get()) {
|
||||
this.addEventHandler(MouseEvent.MOUSE_PRESSED, closeHandler);
|
||||
}
|
||||
// prevent propagating the events to overlay pane
|
||||
contentHolder.addEventHandler(MouseEvent.ANY, Event::consume);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Setters / Getters *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/// @return the dialog container
|
||||
public StackPane getDialogContainer() {
|
||||
return dialogContainer;
|
||||
}
|
||||
|
||||
/// set the dialog container
|
||||
/// Note: the dialog container must be StackPane, its the container for the dialog to be shown in.
|
||||
public void setDialogContainer(StackPane dialogContainer) {
|
||||
if (dialogContainer != null) {
|
||||
this.dialogContainer = dialogContainer;
|
||||
// FIXME: need to be improved to consider only the parent boundary
|
||||
offsetX = dialogContainer.getBoundsInLocal().getWidth();
|
||||
offsetY = dialogContainer.getBoundsInLocal().getHeight();
|
||||
animation = getShowAnimation(transitionType.get());
|
||||
}
|
||||
}
|
||||
|
||||
/// @return dialog content node
|
||||
public Region getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
/// set the content of the dialog
|
||||
public void setContent(Region content) {
|
||||
if (content != null) {
|
||||
this.content = content;
|
||||
this.content.setPickOnBounds(false);
|
||||
contentHolder.getChildren().setAll(content);
|
||||
}
|
||||
}
|
||||
|
||||
/// indicates whether the dialog will close when clicking on the overlay or not
|
||||
private final BooleanProperty overlayClose = new SimpleBooleanProperty(true);
|
||||
|
||||
public final BooleanProperty overlayCloseProperty() {
|
||||
return this.overlayClose;
|
||||
}
|
||||
|
||||
public final boolean isOverlayClose() {
|
||||
return this.overlayCloseProperty().get();
|
||||
}
|
||||
|
||||
public final void setOverlayClose(final boolean overlayClose) {
|
||||
this.overlayCloseProperty().set(overlayClose);
|
||||
}
|
||||
|
||||
/// if sets to true, the content of dialog container will be cached and replaced with an image
|
||||
/// when displaying the dialog (better performance).
|
||||
/// this is recommended if the content behind the dialog will not change during the showing
|
||||
/// period
|
||||
private final BooleanProperty cacheContainer = new SimpleBooleanProperty(false);
|
||||
|
||||
public boolean isCacheContainer() {
|
||||
return cacheContainer.get();
|
||||
}
|
||||
|
||||
public BooleanProperty cacheContainerProperty() {
|
||||
return cacheContainer;
|
||||
}
|
||||
|
||||
public void setCacheContainer(boolean cacheContainer) {
|
||||
this.cacheContainer.set(cacheContainer);
|
||||
}
|
||||
|
||||
/// it will show the dialog in the specified container
|
||||
public void show(StackPane dialogContainer) {
|
||||
this.setDialogContainer(dialogContainer);
|
||||
showDialog();
|
||||
}
|
||||
|
||||
private ArrayList<Node> tempContent;
|
||||
|
||||
/**
|
||||
* show the dialog inside its parent container
|
||||
*/
|
||||
public void show() {
|
||||
this.setDialogContainer(dialogContainer);
|
||||
showDialog();
|
||||
}
|
||||
|
||||
private void showDialog() {
|
||||
if (dialogContainer == null) {
|
||||
throw new RuntimeException("ERROR: JFXDialog container is not set!");
|
||||
}
|
||||
if (isCacheContainer()) {
|
||||
tempContent = new ArrayList<>(dialogContainer.getChildren());
|
||||
|
||||
SnapshotParameters snapShotparams = new SnapshotParameters();
|
||||
snapShotparams.setFill(Color.TRANSPARENT);
|
||||
WritableImage temp = dialogContainer.snapshot(snapShotparams,
|
||||
new WritableImage((int) dialogContainer.getWidth(),
|
||||
(int) dialogContainer.getHeight()));
|
||||
ImageView tempImage = new ImageView(temp);
|
||||
tempImage.setCache(true);
|
||||
tempImage.setCacheHint(CacheHint.SPEED);
|
||||
dialogContainer.getChildren().setAll(tempImage, this);
|
||||
} else {
|
||||
//prevent error if opening an already opened dialog
|
||||
dialogContainer.getChildren().remove(this);
|
||||
tempContent = null;
|
||||
dialogContainer.getChildren().add(this);
|
||||
}
|
||||
|
||||
if (animation != null) {
|
||||
animation.play();
|
||||
} else {
|
||||
setVisible(true);
|
||||
setOpacity(1);
|
||||
Event.fireEvent(JFXDialog.this, new JFXDialogEvent(JFXDialogEvent.OPENED));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* close the dialog
|
||||
*/
|
||||
public void close() {
|
||||
if (animation != null) {
|
||||
animation.setRate(-2);
|
||||
animation.play();
|
||||
animation.setOnFinished(e -> {
|
||||
closeDialog();
|
||||
});
|
||||
} else {
|
||||
setOpacity(0);
|
||||
setVisible(false);
|
||||
closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private void closeDialog() {
|
||||
resetProperties();
|
||||
Event.fireEvent(JFXDialog.this, new JFXDialogEvent(JFXDialogEvent.CLOSED));
|
||||
if (tempContent == null) {
|
||||
dialogContainer.getChildren().remove(this);
|
||||
} else {
|
||||
dialogContainer.getChildren().setAll(tempContent);
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Transitions *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
private Transition getShowAnimation(DialogTransition transitionType) {
|
||||
Transition animation = null;
|
||||
if (contentHolder != null) {
|
||||
animation = switch (transitionType) {
|
||||
case CENTER -> {
|
||||
contentHolder.setScaleX(INITIAL_SCALE);
|
||||
contentHolder.setScaleY(INITIAL_SCALE);
|
||||
yield new CenterTransition();
|
||||
}
|
||||
case NONE -> {
|
||||
contentHolder.setScaleX(1);
|
||||
contentHolder.setScaleY(1);
|
||||
contentHolder.setTranslateX(0);
|
||||
contentHolder.setTranslateY(0);
|
||||
yield null;
|
||||
}
|
||||
};
|
||||
}
|
||||
if (animation != null) {
|
||||
animation.setOnFinished(finish ->
|
||||
Event.fireEvent(JFXDialog.this, new JFXDialogEvent(JFXDialogEvent.OPENED)));
|
||||
}
|
||||
return animation;
|
||||
}
|
||||
|
||||
private void resetProperties() {
|
||||
this.setVisible(false);
|
||||
contentHolder.setTranslateX(0);
|
||||
contentHolder.setTranslateY(0);
|
||||
contentHolder.setScaleX(1);
|
||||
contentHolder.setScaleY(1);
|
||||
}
|
||||
|
||||
private final class CenterTransition extends CachedTransition {
|
||||
private static final Interpolator INTERPOLATOR = Motion.EMPHASIZED_DECELERATE;
|
||||
|
||||
CenterTransition() {
|
||||
super(contentHolder, new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(contentHolder.scaleXProperty(), INITIAL_SCALE, INTERPOLATOR),
|
||||
new KeyValue(contentHolder.scaleYProperty(), INITIAL_SCALE, INTERPOLATOR),
|
||||
new KeyValue(JFXDialog.this.visibleProperty(), false, Motion.LINEAR)
|
||||
),
|
||||
new KeyFrame(Duration.millis(10),
|
||||
new KeyValue(JFXDialog.this.visibleProperty(), true, Motion.LINEAR),
|
||||
new KeyValue(JFXDialog.this.opacityProperty(), 0, INTERPOLATOR)
|
||||
),
|
||||
new KeyFrame(Motion.EXTRA_LONG4,
|
||||
new KeyValue(contentHolder.scaleXProperty(), 1, INTERPOLATOR),
|
||||
new KeyValue(contentHolder.scaleYProperty(), 1, INTERPOLATOR),
|
||||
new KeyValue(JFXDialog.this.visibleProperty(), true, Motion.LINEAR),
|
||||
new KeyValue(JFXDialog.this.opacityProperty(), 1, INTERPOLATOR)
|
||||
))
|
||||
);
|
||||
// reduce the number to increase the shifting , increase number to reduce shifting
|
||||
setCycleDuration(Duration.seconds(0.4));
|
||||
setDelay(Duration.ZERO);
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Stylesheet Handling *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/// Initialize the style class to 'jfx-dialog'.
|
||||
///
|
||||
/// This is the selector class from which CSS can be used to style
|
||||
/// this control.
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-dialog";
|
||||
|
||||
/// dialog transition type property, it can be one of the following:
|
||||
///
|
||||
/// - CENTER
|
||||
/// - TOP
|
||||
/// - RIGHT
|
||||
/// - BOTTOM
|
||||
/// - LEFT
|
||||
/// - NONE
|
||||
private final StyleableObjectProperty<DialogTransition> transitionType = new SimpleStyleableObjectProperty<>(
|
||||
StyleableProperties.DIALOG_TRANSITION,
|
||||
JFXDialog.this,
|
||||
"dialogTransition",
|
||||
DialogTransition.CENTER);
|
||||
|
||||
public DialogTransition getTransitionType() {
|
||||
return transitionType == null ? DialogTransition.CENTER : transitionType.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<DialogTransition> transitionTypeProperty() {
|
||||
return this.transitionType;
|
||||
}
|
||||
|
||||
public void setTransitionType(DialogTransition transition) {
|
||||
this.transitionType.set(transition);
|
||||
}
|
||||
|
||||
private static final class StyleableProperties {
|
||||
private static final CssMetaData<JFXDialog, DialogTransition> DIALOG_TRANSITION =
|
||||
new CssMetaData<JFXDialog, DialogTransition>("-jfx-dialog-transition",
|
||||
DialogTransitionConverter.getInstance(),
|
||||
DialogTransition.CENTER) {
|
||||
@Override
|
||||
public boolean isSettable(JFXDialog control) {
|
||||
return control.transitionType == null || !control.transitionType.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<DialogTransition> getStyleableProperty(JFXDialog control) {
|
||||
return control.transitionTypeProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
||||
|
||||
static {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables =
|
||||
new ArrayList<>(StackPane.getClassCssMetaData());
|
||||
Collections.addAll(styleables,
|
||||
DIALOG_TRANSITION
|
||||
);
|
||||
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
|
||||
return getClassCssMetaData();
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.CHILD_STYLEABLES;
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Custom Events *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
private final ObjectProperty<EventHandler<? super JFXDialogEvent>> onDialogClosedProperty = new ObjectPropertyBase<EventHandler<? super JFXDialogEvent>>() {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setEventHandler(JFXDialogEvent.CLOSED, get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return JFXDialog.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "onClosed";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines a function to be called when the dialog is closed.
|
||||
* Note: it will be triggered after the close animation is finished.
|
||||
*/
|
||||
public ObjectProperty<EventHandler<? super JFXDialogEvent>> onDialogClosedProperty() {
|
||||
return onDialogClosedProperty;
|
||||
}
|
||||
|
||||
public void setOnDialogClosed(EventHandler<? super JFXDialogEvent> handler) {
|
||||
onDialogClosedProperty().set(handler);
|
||||
}
|
||||
|
||||
public EventHandler<? super JFXDialogEvent> getOnDialogClosed() {
|
||||
return onDialogClosedProperty().get();
|
||||
}
|
||||
|
||||
private final ObjectProperty<EventHandler<? super JFXDialogEvent>> onDialogOpenedProperty = new ObjectPropertyBase<>() {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setEventHandler(JFXDialogEvent.OPENED, get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return JFXDialog.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "onOpened";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines a function to be called when the dialog is opened.
|
||||
* Note: it will be triggered after the show animation is finished.
|
||||
*/
|
||||
public ObjectProperty<EventHandler<? super JFXDialogEvent>> onDialogOpenedProperty() {
|
||||
return onDialogOpenedProperty;
|
||||
}
|
||||
|
||||
public void setOnDialogOpened(EventHandler<? super JFXDialogEvent> handler) {
|
||||
onDialogOpenedProperty().set(handler);
|
||||
}
|
||||
|
||||
public EventHandler<? super JFXDialogEvent> getOnDialogOpened() {
|
||||
return onDialogOpenedProperty().get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
//
|
||||
// Source code recreated from a .class file by IntelliJ IDEA
|
||||
// (powered by Fernflower decompiler)
|
||||
//
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
public class JFXDialogLayout extends StackPane {
|
||||
private final StackPane heading = new StackPane();
|
||||
private final StackPane body = new StackPane();
|
||||
private final FlowPane actions = new FlowPane() {
|
||||
protected double computeMinWidth(double height) {
|
||||
if (this.getContentBias() == Orientation.HORIZONTAL) {
|
||||
double maxPref = 0.0;
|
||||
for (Node child : this.getChildren()) {
|
||||
if (child.isManaged()) {
|
||||
maxPref = Math.max(maxPref, child.minWidth(-1.0));
|
||||
}
|
||||
}
|
||||
|
||||
Insets insets = this.getInsets();
|
||||
return insets.getLeft() + this.snapSizeX(maxPref) + insets.getRight();
|
||||
} else {
|
||||
return this.computePrefWidth(height);
|
||||
}
|
||||
}
|
||||
|
||||
protected double computeMinHeight(double width) {
|
||||
if (this.getContentBias() == Orientation.VERTICAL) {
|
||||
double maxPref = 0.0;
|
||||
|
||||
for (Node child : this.getChildren()) {
|
||||
if (child.isManaged()) {
|
||||
maxPref = Math.max(maxPref, child.minHeight(-1.0));
|
||||
}
|
||||
}
|
||||
|
||||
Insets insets = this.getInsets();
|
||||
return insets.getTop() + this.snapSizeY(maxPref) + insets.getBottom();
|
||||
} else {
|
||||
return this.computePrefHeight(width);
|
||||
}
|
||||
}
|
||||
};
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-dialog-layout";
|
||||
|
||||
public JFXDialogLayout() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
|
||||
VBox layout = new VBox();
|
||||
|
||||
this.heading.getStyleClass().add("jfx-layout-heading");
|
||||
this.heading.getStyleClass().add("title");
|
||||
|
||||
this.body.getStyleClass().add("jfx-layout-body");
|
||||
this.body.prefHeightProperty().bind(this.prefHeightProperty());
|
||||
this.body.prefWidthProperty().bind(this.prefWidthProperty());
|
||||
|
||||
this.actions.getStyleClass().add("jfx-layout-actions");
|
||||
|
||||
layout.getChildren().setAll(this.heading, this.body, this.actions);
|
||||
|
||||
this.getChildren().add(layout);
|
||||
}
|
||||
|
||||
public ObservableList<Node> getHeading() {
|
||||
return this.heading.getChildren();
|
||||
}
|
||||
|
||||
public void setHeading(Node... titleContent) {
|
||||
this.heading.getChildren().setAll(titleContent);
|
||||
}
|
||||
|
||||
public ObservableList<Node> getBody() {
|
||||
return this.body.getChildren();
|
||||
}
|
||||
|
||||
public void setBody(Node... body) {
|
||||
this.body.getChildren().setAll(body);
|
||||
}
|
||||
|
||||
public ObservableList<Node> getActions() {
|
||||
return this.actions.getChildren();
|
||||
}
|
||||
|
||||
public void setActions(Node... actions) {
|
||||
this.actions.getChildren().setAll(actions);
|
||||
}
|
||||
|
||||
public void setActions(List<? extends Node> actions) {
|
||||
this.actions.getChildren().setAll(actions);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.utils.JFXNodeUtils;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.shape.Shape;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/// material design implementation of ListCell
|
||||
///
|
||||
/// By default, JFXListCell will try to create a graphic node for the cell,
|
||||
/// to override it you need to set graphic to null in [#updateItem(Object, boolean)] method.
|
||||
///
|
||||
/// NOTE: passive nodes (Labels and Shapes) will be set to mouse transparent in order to
|
||||
/// show the ripple effect upon clicking , to change this behavior you can override the
|
||||
/// method {[#makeChildrenTransparent()]
|
||||
///
|
||||
/// @author Shadi Shaheen
|
||||
/// @version 1.0
|
||||
/// @since 2016-03-09
|
||||
public class JFXListCell<T> extends ListCell<T> {
|
||||
|
||||
protected JFXRippler cellRippler = new JFXRippler(this) {
|
||||
@Override
|
||||
protected Node getMask() {
|
||||
Region clip = new Region();
|
||||
JFXNodeUtils.updateBackground(JFXListCell.this.getBackground(), clip);
|
||||
double width = control.getLayoutBounds().getWidth();
|
||||
double height = control.getLayoutBounds().getHeight();
|
||||
clip.resize(width, height);
|
||||
return clip;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void positionControl(Node control) {
|
||||
// do nothing
|
||||
}
|
||||
};
|
||||
|
||||
protected Node cellContent;
|
||||
private Rectangle clip;
|
||||
|
||||
private Timeline gapAnimation;
|
||||
private boolean playExpandAnimation = false;
|
||||
private boolean selectionChanged = false;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public JFXListCell() {
|
||||
initialize();
|
||||
initListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* init listeners to update the vertical gap / selection animation
|
||||
*/
|
||||
private void initListeners() {
|
||||
listViewProperty().addListener((listObj, oldList, newList) -> {
|
||||
if (newList instanceof JFXListView<?> listView) {
|
||||
listView.currentVerticalGapProperty().addListener((o, oldVal, newVal) -> {
|
||||
cellRippler.rippler.setClip(null);
|
||||
if (newVal.doubleValue() != 0) {
|
||||
playExpandAnimation = true;
|
||||
getListView().requestLayout();
|
||||
} else {
|
||||
// fake expand state
|
||||
double gap = clip.getY() * 2;
|
||||
gapAnimation = new Timeline(
|
||||
new KeyFrame(Duration.millis(240),
|
||||
new KeyValue(this.translateYProperty(),
|
||||
-gap / 2 - (gap * (getIndex())),
|
||||
Interpolator.EASE_BOTH)
|
||||
));
|
||||
gapAnimation.play();
|
||||
gapAnimation.setOnFinished((finish) -> {
|
||||
requestLayout();
|
||||
Platform.runLater(() -> getListView().requestLayout());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
selectedProperty().addListener((o, oldVal, newVal) -> {
|
||||
if (newVal) {
|
||||
selectionChanged = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren() {
|
||||
super.layoutChildren();
|
||||
cellRippler.resizeRelocate(0, 0, getWidth(), getHeight());
|
||||
double gap = getGap();
|
||||
|
||||
if (clip == null) {
|
||||
clip = new Rectangle(0, gap / 2, getWidth(), getHeight() - gap);
|
||||
setClip(clip);
|
||||
} else {
|
||||
if (gap != 0) {
|
||||
if (playExpandAnimation || selectionChanged) {
|
||||
// fake list collapse state
|
||||
if (playExpandAnimation) {
|
||||
this.setTranslateY(-gap / 2 + (-gap * (getIndex())));
|
||||
clip.setY(gap / 2);
|
||||
clip.setHeight(getHeight() - gap);
|
||||
gapAnimation = new Timeline(new KeyFrame(Duration.millis(240),
|
||||
new KeyValue(this.translateYProperty(),
|
||||
0,
|
||||
Interpolator.EASE_BOTH)));
|
||||
playExpandAnimation = false;
|
||||
} else if (selectionChanged) {
|
||||
clip.setY(0);
|
||||
clip.setHeight(getHeight());
|
||||
gapAnimation = new Timeline(
|
||||
new KeyFrame(Duration.millis(240),
|
||||
new KeyValue(clip.yProperty(), gap / 2, Interpolator.EASE_BOTH),
|
||||
new KeyValue(clip.heightProperty(), getHeight() - gap, Interpolator.EASE_BOTH)
|
||||
));
|
||||
}
|
||||
playExpandAnimation = false;
|
||||
selectionChanged = false;
|
||||
gapAnimation.play();
|
||||
} else {
|
||||
if (gapAnimation != null) {
|
||||
gapAnimation.stop();
|
||||
}
|
||||
this.setTranslateY(0);
|
||||
clip.setY(gap / 2);
|
||||
clip.setHeight(getHeight() - gap);
|
||||
}
|
||||
} else {
|
||||
this.setTranslateY(0);
|
||||
clip.setY(0);
|
||||
clip.setHeight(getHeight());
|
||||
}
|
||||
clip.setX(0);
|
||||
clip.setWidth(getWidth());
|
||||
}
|
||||
if (!getChildren().contains(cellRippler)) {
|
||||
makeChildrenTransparent();
|
||||
getChildren().add(0, cellRippler);
|
||||
cellRippler.rippler.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* this method is used to set some nodes in cell content as mouse transparent nodes
|
||||
* so clicking on them will trigger the ripple effect.
|
||||
*/
|
||||
protected void makeChildrenTransparent() {
|
||||
for (Node child : getChildren()) {
|
||||
if (child instanceof Label) {
|
||||
Set<Node> texts = child.lookupAll("Text");
|
||||
for (Node text : texts) {
|
||||
text.setMouseTransparent(true);
|
||||
}
|
||||
} else if (child instanceof Shape) {
|
||||
child.setMouseTransparent(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
// remove empty (Trailing cells)
|
||||
setMouseTransparent(true);
|
||||
setStyle("-fx-background-color:TRANSPARENT;");
|
||||
} else {
|
||||
setMouseTransparent(false);
|
||||
setStyle(null);
|
||||
if (item instanceof Node newNode) {
|
||||
setText(null);
|
||||
Node currentNode = getGraphic();
|
||||
if (currentNode == null || !currentNode.equals(newNode)) {
|
||||
cellContent = newNode;
|
||||
cellRippler.rippler.cacheRippleClip(false);
|
||||
// build the Cell node
|
||||
// RIPPLER ITEM : in case if the list item has its own rippler bind the list rippler and item rippler properties
|
||||
if (newNode instanceof JFXRippler newRippler) {
|
||||
// build cell container from exisiting rippler
|
||||
cellRippler.ripplerFillProperty().bind(newRippler.ripplerFillProperty());
|
||||
cellRippler.maskTypeProperty().bind(newRippler.maskTypeProperty());
|
||||
cellRippler.positionProperty().bind(newRippler.positionProperty());
|
||||
cellContent = newRippler.getControl();
|
||||
}
|
||||
((Region) cellContent).setMaxHeight(cellContent.prefHeight(-1));
|
||||
setGraphic(cellContent);
|
||||
}
|
||||
} else {
|
||||
setText(item == null ? "null" : item.toString());
|
||||
setGraphic(null);
|
||||
}
|
||||
// show cell tooltip if it's toggled in JFXListView
|
||||
if (getListView() instanceof JFXListView<?> listView && listView.isShowTooltip()) {
|
||||
if (item instanceof Label label) {
|
||||
setTooltip(new Tooltip(label.getText()));
|
||||
} else if (getText() != null) {
|
||||
setTooltip(new Tooltip(getText()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stylesheet Handling *
|
||||
|
||||
/**
|
||||
* Initialize the style class to 'jfx-list-cell'.
|
||||
* <p>
|
||||
* This is the selector class from which CSS can be used to style
|
||||
* this control.
|
||||
*/
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-list-cell";
|
||||
|
||||
private void initialize() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
this.setPadding(new Insets(8, 12, 8, 12));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefHeight(double width) {
|
||||
double gap = getGap();
|
||||
return super.computePrefHeight(width) + gap;
|
||||
}
|
||||
|
||||
private double getGap() {
|
||||
return (getListView() instanceof JFXListView<?> listView)
|
||||
? (listView.isExpanded() ? listView.currentVerticalGapProperty().get() : 0)
|
||||
: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.skins.JFXListViewSkin;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.css.*;
|
||||
import javafx.css.converter.BooleanConverter;
|
||||
import javafx.css.converter.SizeConverter;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/// Material design implementation of List View
|
||||
///
|
||||
/// @author Shadi Shaheen
|
||||
/// @version 1.0
|
||||
/// @since 2016-03-09
|
||||
public class JFXListView<T> extends ListView<T> {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public JFXListView() {
|
||||
this.setCellFactory(listView -> new JFXListCell<>());
|
||||
initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new JFXListViewSkin<>(this);
|
||||
}
|
||||
|
||||
private IntegerProperty depth;
|
||||
|
||||
public IntegerProperty depthProperty() {
|
||||
if (depth == null) {
|
||||
depth = new SimpleIntegerProperty(this, "depth", 0);
|
||||
}
|
||||
return depth;
|
||||
}
|
||||
|
||||
public int getDepth() {
|
||||
return depth != null ? depth.get() : 0;
|
||||
}
|
||||
|
||||
public void setDepth(int depth) {
|
||||
depthProperty().set(depth);
|
||||
}
|
||||
|
||||
private DoubleProperty currentVerticalGap;
|
||||
|
||||
DoubleProperty currentVerticalGapProperty() {
|
||||
if (currentVerticalGap == null) {
|
||||
currentVerticalGap = new SimpleDoubleProperty(this, "currentVerticalGap");
|
||||
}
|
||||
return currentVerticalGap;
|
||||
}
|
||||
|
||||
private void updateVerticalGap() {
|
||||
if (isExpanded()) {
|
||||
currentVerticalGapProperty().set(verticalGap.get());
|
||||
} else {
|
||||
currentVerticalGapProperty().set(0);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* this only works if the items were labels / strings
|
||||
*/
|
||||
private BooleanProperty showTooltip;
|
||||
|
||||
public final BooleanProperty showTooltipProperty() {
|
||||
if (showTooltip == null) {
|
||||
showTooltip = new SimpleBooleanProperty(this, "showTooltip", false);
|
||||
}
|
||||
return this.showTooltip;
|
||||
}
|
||||
|
||||
public final boolean isShowTooltip() {
|
||||
return showTooltip != null && showTooltip.get();
|
||||
}
|
||||
|
||||
public final void setShowTooltip(final boolean showTooltip) {
|
||||
this.showTooltipProperty().set(showTooltip);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Stylesheet Handling *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/// Initialize the style class to 'jfx-list-view'.
|
||||
///
|
||||
/// This is the selector class from which CSS can be used to style
|
||||
/// this control.
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-list-view";
|
||||
|
||||
private void initialize() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
/**
|
||||
* propagate mouse events to the parent node ( e.g. to allow dragging while clicking on the list)
|
||||
*/
|
||||
public void propagateMouseEventsToParent() {
|
||||
this.addEventHandler(MouseEvent.ANY, e -> {
|
||||
e.consume();
|
||||
this.getParent().fireEvent(e);
|
||||
});
|
||||
}
|
||||
|
||||
private StyleableDoubleProperty verticalGap;
|
||||
|
||||
public StyleableDoubleProperty verticalGapProperty() {
|
||||
if (this.verticalGap == null) {
|
||||
this.verticalGap = new StyleableDoubleProperty(0.0) {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return JFXListView.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "verticalGap";
|
||||
}
|
||||
|
||||
@Override
|
||||
public CssMetaData<? extends Styleable, Number> getCssMetaData() {
|
||||
return StyleableProperties.VERTICAL_GAP;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
updateVerticalGap();
|
||||
}
|
||||
};
|
||||
}
|
||||
return this.verticalGap;
|
||||
}
|
||||
|
||||
public Double getVerticalGap() {
|
||||
return verticalGap == null ? 0.0 : verticalGap.get();
|
||||
}
|
||||
|
||||
public void setVerticalGap(Double gap) {
|
||||
verticalGapProperty().set(gap);
|
||||
}
|
||||
|
||||
private StyleableBooleanProperty expanded;
|
||||
|
||||
public StyleableBooleanProperty expandedProperty() {
|
||||
if (expanded == null) {
|
||||
expanded = new StyleableBooleanProperty(false) {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return JFXListView.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "expanded";
|
||||
}
|
||||
|
||||
@Override
|
||||
public CssMetaData<? extends Styleable, Boolean> getCssMetaData() {
|
||||
return StyleableProperties.EXPANDED;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
updateVerticalGap();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return this.expanded;
|
||||
}
|
||||
|
||||
public Boolean isExpanded() {
|
||||
return expanded != null && expanded.get();
|
||||
}
|
||||
|
||||
public void setExpanded(Boolean expanded) {
|
||||
expandedProperty().set(expanded);
|
||||
}
|
||||
|
||||
private static final class StyleableProperties {
|
||||
private static final CssMetaData<JFXListView<?>, Number> VERTICAL_GAP =
|
||||
new CssMetaData<>("-jfx-vertical-gap",
|
||||
SizeConverter.getInstance(), 0) {
|
||||
@Override
|
||||
public boolean isSettable(JFXListView<?> control) {
|
||||
return control.verticalGap == null || !control.verticalGap.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableDoubleProperty getStyleableProperty(JFXListView<?> control) {
|
||||
return control.verticalGapProperty();
|
||||
}
|
||||
};
|
||||
private static final CssMetaData<JFXListView<?>, Boolean> EXPANDED =
|
||||
new CssMetaData<>("-jfx-expanded",
|
||||
BooleanConverter.getInstance(), false) {
|
||||
@Override
|
||||
public boolean isSettable(JFXListView<?> control) {
|
||||
// it's only settable if the List is not shown yet
|
||||
return control.getHeight() == 0 && (control.expanded == null || !control.expanded.isBound());
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableBooleanProperty getStyleableProperty(JFXListView<?> control) {
|
||||
return control.expandedProperty();
|
||||
}
|
||||
};
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
||||
|
||||
static {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables =
|
||||
new ArrayList<>(ListView.getClassCssMetaData());
|
||||
Collections.addAll(styleables, VERTICAL_GAP, EXPANDED);
|
||||
CHILD_STYLEABLES = List.copyOf(styleables);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
||||
return getClassCssMetaData();
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.CHILD_STYLEABLES;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.skins.JFXPasswordFieldSkin;
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.*;
|
||||
import javafx.css.converter.BooleanConverter;
|
||||
import javafx.css.converter.PaintConverter;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.PasswordField;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu;
|
||||
|
||||
/**
|
||||
* JFXPasswordField is the material design implementation of a password Field.
|
||||
*
|
||||
* @author Shadi Shaheen
|
||||
* @version 1.0
|
||||
* @since 2016-03-09
|
||||
*/
|
||||
public class JFXPasswordField extends PasswordField {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public JFXPasswordField() {
|
||||
initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new JFXPasswordFieldSkin(this);
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
if ("dalvik".equals(System.getProperty("java.vm.name").toLowerCase(Locale.ROOT))) {
|
||||
this.setStyle("-fx-skin: \"com.jfoenix.android.skins.JFXPasswordFieldSkinAndroid\";");
|
||||
}
|
||||
|
||||
|
||||
useJFXContextMenu(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the style class to 'jfx-password-field'.
|
||||
* <p>
|
||||
* This is the selector class from which CSS can be used to style
|
||||
* this control.
|
||||
*/
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-password-field";
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Properties *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/**
|
||||
* holds the current active validator on the password field in case of validation error
|
||||
*/
|
||||
private ReadOnlyObjectWrapper<ValidatorBase> activeValidator = new ReadOnlyObjectWrapper<>();
|
||||
|
||||
public ValidatorBase getActiveValidator() {
|
||||
return activeValidator == null ? null : activeValidator.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<ValidatorBase> activeValidatorProperty() {
|
||||
return this.activeValidator.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
/**
|
||||
* list of validators that will validate the password value upon calling
|
||||
* {{@link #validate()}
|
||||
*/
|
||||
private ObservableList<ValidatorBase> validators = FXCollections.observableArrayList();
|
||||
|
||||
public ObservableList<ValidatorBase> getValidators() {
|
||||
return validators;
|
||||
}
|
||||
|
||||
public void setValidators(ValidatorBase... validators) {
|
||||
this.validators.addAll(validators);
|
||||
}
|
||||
|
||||
/**
|
||||
* validates the password value using the list of validators provided by the user
|
||||
* {{@link #setValidators(ValidatorBase...)}
|
||||
*
|
||||
* @return true if the value is valid else false
|
||||
*/
|
||||
public boolean validate() {
|
||||
for (ValidatorBase validator : validators) {
|
||||
if (validator.getSrcControl() == null) {
|
||||
validator.setSrcControl(this);
|
||||
}
|
||||
validator.validate();
|
||||
if (validator.getHasErrors()) {
|
||||
activeValidator.set(validator);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
activeValidator.set(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void resetValidation() {
|
||||
pseudoClassStateChanged(ValidatorBase.PSEUDO_CLASS_ERROR, false);
|
||||
activeValidator.set(null);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* styleable Properties *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/**
|
||||
* set true to show a float the prompt text when focusing the field
|
||||
*/
|
||||
private StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT, JFXPasswordField.this, "lableFloat", false);
|
||||
|
||||
public final StyleableBooleanProperty labelFloatProperty() {
|
||||
return this.labelFloat;
|
||||
}
|
||||
|
||||
public final boolean isLabelFloat() {
|
||||
return this.labelFloatProperty().get();
|
||||
}
|
||||
|
||||
public final void setLabelFloat(final boolean labelFloat) {
|
||||
this.labelFloatProperty().set(labelFloat);
|
||||
}
|
||||
|
||||
/**
|
||||
* default color used when the field is unfocused
|
||||
*/
|
||||
private StyleableObjectProperty<Paint> unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR, JFXPasswordField.this, "unFocusColor", Color.rgb(77, 77, 77));
|
||||
|
||||
public Paint getUnFocusColor() {
|
||||
return unFocusColor == null ? Color.rgb(77, 77, 77) : unFocusColor.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> unFocusColorProperty() {
|
||||
return this.unFocusColor;
|
||||
}
|
||||
|
||||
public void setUnFocusColor(Paint color) {
|
||||
this.unFocusColor.set(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* default color used when the field is focused
|
||||
*/
|
||||
private StyleableObjectProperty<Paint> focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR, JFXPasswordField.this, "focusColor", Color.valueOf("#4059A9"));
|
||||
|
||||
public Paint getFocusColor() {
|
||||
return focusColor == null ? Color.valueOf("#4059A9") : focusColor.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> focusColorProperty() {
|
||||
return this.focusColor;
|
||||
}
|
||||
|
||||
public void setFocusColor(Paint color) {
|
||||
this.focusColor.set(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* disable animation on validation
|
||||
*/
|
||||
private StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION, JFXPasswordField.this, "disableAnimation", false);
|
||||
|
||||
public final StyleableBooleanProperty disableAnimationProperty() {
|
||||
return this.disableAnimation;
|
||||
}
|
||||
|
||||
public final Boolean isDisableAnimation() {
|
||||
return disableAnimation != null && this.disableAnimationProperty().get();
|
||||
}
|
||||
|
||||
public final void setDisableAnimation(final Boolean disabled) {
|
||||
this.disableAnimationProperty().set(disabled);
|
||||
}
|
||||
|
||||
private final static class StyleableProperties {
|
||||
private static final CssMetaData<JFXPasswordField, Paint> UNFOCUS_COLOR = new CssMetaData<JFXPasswordField, Paint>("-jfx-unfocus-color", PaintConverter.getInstance(), Color.valueOf("#A6A6A6")) {
|
||||
@Override
|
||||
public boolean isSettable(JFXPasswordField control) {
|
||||
return control.unFocusColor == null || !control.unFocusColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(JFXPasswordField control) {
|
||||
return control.unFocusColorProperty();
|
||||
}
|
||||
};
|
||||
private static final CssMetaData<JFXPasswordField, Paint> FOCUS_COLOR = new CssMetaData<JFXPasswordField, Paint>("-jfx-focus-color", PaintConverter.getInstance(), Color.valueOf("#3f51b5")) {
|
||||
@Override
|
||||
public boolean isSettable(JFXPasswordField control) {
|
||||
return control.focusColor == null || !control.focusColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(JFXPasswordField control) {
|
||||
return control.focusColorProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final CssMetaData<JFXPasswordField, Boolean> LABEL_FLOAT = new CssMetaData<JFXPasswordField, Boolean>("-jfx-label-float", BooleanConverter.getInstance(), false) {
|
||||
@Override
|
||||
public boolean isSettable(JFXPasswordField control) {
|
||||
return control.labelFloat == null || !control.labelFloat.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableBooleanProperty getStyleableProperty(JFXPasswordField control) {
|
||||
return control.labelFloatProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final CssMetaData<JFXPasswordField, Boolean> DISABLE_ANIMATION = new CssMetaData<JFXPasswordField, Boolean>("-fx-disable-animation", BooleanConverter.getInstance(), false) {
|
||||
@Override
|
||||
public boolean isSettable(JFXPasswordField control) {
|
||||
return control.disableAnimation == null || !control.disableAnimation.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableBooleanProperty getStyleableProperty(JFXPasswordField control) {
|
||||
return control.disableAnimationProperty();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
||||
|
||||
static {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
||||
Collections.addAll(styleables, UNFOCUS_COLOR, FOCUS_COLOR, LABEL_FLOAT, DISABLE_ANIMATION);
|
||||
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
}
|
||||
|
||||
// inherit the styleable properties from parent
|
||||
private List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
||||
if (STYLEABLES == null) {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
||||
styleables.addAll(getClassCssMetaData());
|
||||
styleables.addAll(TextField.getClassCssMetaData());
|
||||
STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
return STYLEABLES;
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.CHILD_STYLEABLES;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.skins.JFXPopupSkin;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.NodeOrientation;
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.PopupControl;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.stage.Window;
|
||||
|
||||
/// JFXPopup is the material design implementation of a popup.
|
||||
///
|
||||
/// @author Shadi Shaheen
|
||||
/// @version 2.0
|
||||
/// @since 2017-03-01
|
||||
@DefaultProperty(value = "popupContent")
|
||||
public class JFXPopup extends PopupControl {
|
||||
|
||||
public enum PopupHPosition {
|
||||
RIGHT,
|
||||
LEFT;
|
||||
|
||||
public PopupHPosition getOpposite() {
|
||||
return (this == RIGHT) ? LEFT : RIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
public enum PopupVPosition {
|
||||
TOP, BOTTOM
|
||||
}
|
||||
|
||||
/// Creates empty popup.
|
||||
public JFXPopup() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
/// creates popup with a specified container and content
|
||||
///
|
||||
/// @param content the node that will be shown in the popup
|
||||
public JFXPopup(Region content) {
|
||||
setPopupContent(content);
|
||||
initialize();
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.setAutoFix(false);
|
||||
this.setAutoHide(true);
|
||||
this.setHideOnEscape(true);
|
||||
this.setConsumeAutoHidingEvents(false);
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new JFXPopupSkin(this);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Setters / Getters *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
private final ObjectProperty<Region> popupContent = new SimpleObjectProperty<>(new Pane());
|
||||
|
||||
public final ObjectProperty<Region> popupContentProperty() {
|
||||
return this.popupContent;
|
||||
}
|
||||
|
||||
public final Region getPopupContent() {
|
||||
return this.popupContentProperty().get();
|
||||
}
|
||||
|
||||
public final void setPopupContent(final Region popupContent) {
|
||||
this.popupContentProperty().set(popupContent);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Public API *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/// show the popup using the default position
|
||||
public void show(Node node) {
|
||||
this.show(node, PopupVPosition.TOP, PopupHPosition.LEFT, 0, 0);
|
||||
}
|
||||
|
||||
/// show the popup according to the specified position
|
||||
///
|
||||
/// @param vAlign can be TOP/BOTTOM
|
||||
/// @param hAlign can be LEFT/RIGHT
|
||||
public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign) {
|
||||
this.show(node, vAlign, hAlign, 0, 0);
|
||||
}
|
||||
|
||||
/// show the popup according to the specified position with a certain offset
|
||||
///
|
||||
/// @param vAlign can be TOP/BOTTOM
|
||||
/// @param hAlign can be LEFT/RIGHT
|
||||
/// @param initOffsetX on the x-axis
|
||||
/// @param initOffsetY on the y-axis
|
||||
public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign, double initOffsetX, double initOffsetY) {
|
||||
show(node, vAlign, hAlign, initOffsetX, initOffsetY, false);
|
||||
}
|
||||
|
||||
public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign, double initOffsetX, double initOffsetY, boolean attachToNode) {
|
||||
if (!isShowing()) {
|
||||
Scene scene = node.getScene();
|
||||
if (scene == null || scene.getWindow() == null) {
|
||||
throw new IllegalStateException("Can not show popup. The node must be attached to a scene/window.");
|
||||
}
|
||||
Window parent = scene.getWindow();
|
||||
final Point2D origin = node.localToScene(0, 0);
|
||||
|
||||
boolean isRTL = node.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT;
|
||||
|
||||
double anchorX = parent.getX() + scene.getX() + origin.getX() + (hAlign == PopupHPosition.RIGHT ? ((Region) node).getWidth() : 0);
|
||||
double anchorY = parent.getY() + origin.getY() + scene.getY() + (vAlign == PopupVPosition.BOTTOM ? ((Region) node).getHeight() : 0);
|
||||
|
||||
if (attachToNode)
|
||||
this.show(node, anchorX, anchorY);
|
||||
else
|
||||
this.show(parent, anchorX, anchorY);
|
||||
|
||||
((JFXPopupSkin) getSkin()).reset(vAlign, isRTL ? hAlign.getOpposite() : hAlign, isRTL ? -initOffsetX : initOffsetX, initOffsetY);
|
||||
Platform.runLater(() -> ((JFXPopupSkin) getSkin()).animate());
|
||||
}
|
||||
}
|
||||
|
||||
public void show(Window window, double x, double y, PopupVPosition vAlign, PopupHPosition hAlign, double initOffsetX, double initOffsetY) {
|
||||
if (!isShowing()) {
|
||||
if (window == null) {
|
||||
throw new IllegalStateException("Can not show popup. The node must be attached to a scene/window.");
|
||||
}
|
||||
Window parent = window;
|
||||
final double anchorX = parent.getX() + x + initOffsetX;
|
||||
final double anchorY = parent.getY() + y + initOffsetY;
|
||||
this.show(parent, anchorX, anchorY);
|
||||
((JFXPopupSkin) getSkin()).reset(vAlign, hAlign, initOffsetX, initOffsetY);
|
||||
Platform.runLater(() -> ((JFXPopupSkin) getSkin()).animate());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hide() {
|
||||
super.hide();
|
||||
((JFXPopupSkin) getSkin()).init();
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Stylesheet Handling *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/// Initialize the style class to 'jfx-popup'.
|
||||
///
|
||||
/// This is the selector class from which CSS can be used to style
|
||||
/// this control.
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-popup";
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.skins.JFXProgressBarSkin;
|
||||
import javafx.scene.control.ProgressBar;
|
||||
import javafx.scene.control.Skin;
|
||||
|
||||
/// JFXProgressBar is the material design implementation of a progress bar.
|
||||
///
|
||||
/// @author Shadi Shaheen
|
||||
/// @version 1.0
|
||||
/// @since 2016-03-09
|
||||
public class JFXProgressBar extends ProgressBar {
|
||||
/// Initialize the style class to 'jfx-progress-bar'.
|
||||
///
|
||||
/// This is the selector class from which CSS can be used to style
|
||||
/// this control.
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-progress-bar";
|
||||
|
||||
public JFXProgressBar() {
|
||||
initialize();
|
||||
}
|
||||
|
||||
public JFXProgressBar(double progress) {
|
||||
super(progress);
|
||||
initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new JFXProgressBarSkin(this);
|
||||
}
|
||||
|
||||
private boolean smoothProgress = true;
|
||||
|
||||
public boolean isSmoothProgress() {
|
||||
return smoothProgress;
|
||||
}
|
||||
|
||||
public void setSmoothProgress(boolean smoothProgress) {
|
||||
this.smoothProgress = smoothProgress;
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
setPrefWidth(200);
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.skins.JFXRadioButtonSkin;
|
||||
import javafx.css.*;
|
||||
import javafx.css.converter.ColorConverter;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/// JFXRadioButton is the material design implementation of a radio button.
|
||||
///
|
||||
/// @author Bashir Elias & Shadi Shaheen
|
||||
/// @version 1.0
|
||||
/// @since 2016-03-09
|
||||
public class JFXRadioButton extends RadioButton {
|
||||
|
||||
public JFXRadioButton(String text) {
|
||||
super(text);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public JFXRadioButton() {
|
||||
initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new JFXRadioButtonSkin(this);
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
/// Initialize the style class to 'jfx-radio-button'.
|
||||
///
|
||||
/// This is the selector class from which CSS can be used to style
|
||||
/// this control.
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-radio-button";
|
||||
|
||||
/// default color used when the radio button is selected
|
||||
private StyleableObjectProperty<Color> selectedColor;
|
||||
|
||||
private static final Color DEFAULT_SELECTED_COLOR = Color.valueOf("#0F9D58");
|
||||
|
||||
public final StyleableObjectProperty<Color> selectedColorProperty() {
|
||||
if (selectedColor == null) {
|
||||
selectedColor = new SimpleStyleableObjectProperty<>(StyleableProperties.SELECTED_COLOR,
|
||||
JFXRadioButton.this,
|
||||
"selectedColor",
|
||||
DEFAULT_SELECTED_COLOR);
|
||||
}
|
||||
return this.selectedColor;
|
||||
}
|
||||
|
||||
public final Color getSelectedColor() {
|
||||
return selectedColor == null ? DEFAULT_SELECTED_COLOR : this.selectedColorProperty().get();
|
||||
}
|
||||
|
||||
public final void setSelectedColor(final Color selectedColor) {
|
||||
this.selectedColorProperty().set(selectedColor);
|
||||
}
|
||||
|
||||
/// default color used when the radio button is not selected
|
||||
private StyleableObjectProperty<Color> unSelectedColor;
|
||||
|
||||
private static final Color DEFAULT_UNSELECTED_COLOR = Color.valueOf("#5A5A5A");
|
||||
|
||||
public final StyleableObjectProperty<Color> unSelectedColorProperty() {
|
||||
if (unSelectedColor == null) {
|
||||
unSelectedColor = new SimpleStyleableObjectProperty<>(
|
||||
StyleableProperties.UNSELECTED_COLOR,
|
||||
JFXRadioButton.this,
|
||||
"unSelectedColor",
|
||||
DEFAULT_UNSELECTED_COLOR);
|
||||
}
|
||||
return this.unSelectedColor;
|
||||
}
|
||||
|
||||
public final Color getUnSelectedColor() {
|
||||
return unSelectedColor == null ? DEFAULT_UNSELECTED_COLOR : this.unSelectedColorProperty().get();
|
||||
}
|
||||
|
||||
public final void setUnSelectedColor(final Color unSelectedColor) {
|
||||
this.unSelectedColorProperty().set(unSelectedColor);
|
||||
}
|
||||
|
||||
private static final class StyleableProperties {
|
||||
private static final CssMetaData<JFXRadioButton, Color> SELECTED_COLOR =
|
||||
new CssMetaData<JFXRadioButton, Color>("-jfx-selected-color",
|
||||
ColorConverter.getInstance(), DEFAULT_SELECTED_COLOR) {
|
||||
@Override
|
||||
public boolean isSettable(JFXRadioButton control) {
|
||||
return control.selectedColor == null || !control.selectedColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Color> getStyleableProperty(JFXRadioButton control) {
|
||||
return control.selectedColorProperty();
|
||||
}
|
||||
};
|
||||
private static final CssMetaData<JFXRadioButton, Color> UNSELECTED_COLOR =
|
||||
new CssMetaData<JFXRadioButton, Color>("-jfx-unselected-color",
|
||||
ColorConverter.getInstance(), DEFAULT_UNSELECTED_COLOR) {
|
||||
@Override
|
||||
public boolean isSettable(JFXRadioButton control) {
|
||||
return control.unSelectedColor == null || !control.unSelectedColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Color> getStyleableProperty(JFXRadioButton control) {
|
||||
return control.unSelectedColorProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
||||
|
||||
static {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables =
|
||||
new ArrayList<>(RadioButton.getClassCssMetaData());
|
||||
Collections.addAll(styleables,
|
||||
SELECTED_COLOR,
|
||||
UNSELECTED_COLOR
|
||||
);
|
||||
CHILD_STYLEABLES = List.copyOf(styleables);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
||||
return getClassCssMetaData();
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.CHILD_STYLEABLES;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,814 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.converters.RipplerMaskTypeConverter;
|
||||
import com.jfoenix.utils.JFXNodeUtils;
|
||||
import javafx.animation.*;
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.css.*;
|
||||
import javafx.css.converter.BooleanConverter;
|
||||
import javafx.css.converter.PaintConverter;
|
||||
import javafx.css.converter.SizeConverter;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.scene.CacheHint;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Circle;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.shape.Shape;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* JFXRippler is the material design implementation of a ripple effect.
|
||||
* the ripple effect can be applied to any node in the scene. JFXRippler is
|
||||
* a {@link StackPane} container that holds a specified node (control node) and a ripple generator.
|
||||
* <p>
|
||||
* UPDATE NOTES:
|
||||
* - fireEventProgrammatically(Event) method has been removed as the ripple controller is
|
||||
* the control itself, so you can trigger manual ripple by firing mouse event on the control
|
||||
* instead of JFXRippler
|
||||
*
|
||||
* @author Shadi Shaheen
|
||||
* @version 1.0
|
||||
* @since 2016-03-09
|
||||
*/
|
||||
@DefaultProperty(value = "control")
|
||||
public class JFXRippler extends StackPane {
|
||||
public enum RipplerPos {
|
||||
FRONT, BACK
|
||||
}
|
||||
|
||||
public enum RipplerMask {
|
||||
CIRCLE, RECT, FIT
|
||||
}
|
||||
|
||||
protected RippleGenerator rippler;
|
||||
protected Pane ripplerPane;
|
||||
protected Node control;
|
||||
|
||||
protected static final double RIPPLE_MAX_RADIUS = 300;
|
||||
private static final Interpolator RIPPLE_INTERPOLATOR = Interpolator.SPLINE(0.0825,
|
||||
0.3025,
|
||||
0.0875,
|
||||
0.9975); //0.1, 0.54, 0.28, 0.95);
|
||||
|
||||
private boolean forceOverlay = false;
|
||||
|
||||
/// creates empty rippler node
|
||||
public JFXRippler() {
|
||||
this(null, RipplerMask.RECT, RipplerPos.FRONT);
|
||||
}
|
||||
|
||||
/// creates a rippler for the specified control
|
||||
public JFXRippler(Node control) {
|
||||
this(control, RipplerMask.RECT, RipplerPos.FRONT);
|
||||
}
|
||||
|
||||
/// creates a rippler for the specified control
|
||||
///
|
||||
/// @param pos can be either FRONT/BACK (position the ripple effect infront of or behind the control)
|
||||
public JFXRippler(Node control, RipplerPos pos) {
|
||||
this(control, RipplerMask.RECT, pos);
|
||||
}
|
||||
|
||||
/// creates a rippler for the specified control and apply the specified mask to it
|
||||
///
|
||||
/// @param mask can be either rectangle/cricle
|
||||
public JFXRippler(Node control, RipplerMask mask) {
|
||||
this(control, mask, RipplerPos.FRONT);
|
||||
}
|
||||
|
||||
/// creates a rippler for the specified control, mask and position.
|
||||
///
|
||||
/// @param mask can be either rectangle/cricle
|
||||
/// @param pos can be either FRONT/BACK (position the ripple effect infront of or behind the control)
|
||||
public JFXRippler(Node control, RipplerMask mask, RipplerPos pos) {
|
||||
initialize();
|
||||
|
||||
setMaskType(mask);
|
||||
setPosition(pos);
|
||||
createRippleUI();
|
||||
setControl(control);
|
||||
|
||||
// listen to control position changed
|
||||
position.addListener(observable -> updateControlPosition());
|
||||
|
||||
setPickOnBounds(false);
|
||||
setCache(true);
|
||||
setCacheHint(CacheHint.SPEED);
|
||||
setCacheShape(true);
|
||||
}
|
||||
|
||||
protected final void createRippleUI() {
|
||||
// create rippler panels
|
||||
rippler = new RippleGenerator();
|
||||
ripplerPane = new StackPane();
|
||||
ripplerPane.setMouseTransparent(true);
|
||||
ripplerPane.getChildren().add(rippler);
|
||||
getChildren().add(ripplerPane);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Setters / Getters *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
public void setControl(Node control) {
|
||||
if (control != null) {
|
||||
this.control = control;
|
||||
// position control
|
||||
positionControl(control);
|
||||
// add control listeners to generate / release ripples
|
||||
initControlListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Override this method to create JFXRippler for a control outside the ripple
|
||||
protected void positionControl(Node control) {
|
||||
if (this.position.get() == RipplerPos.BACK) {
|
||||
getChildren().add(control);
|
||||
} else {
|
||||
getChildren().add(0, control);
|
||||
}
|
||||
}
|
||||
|
||||
protected void updateControlPosition() {
|
||||
if (this.position.get() == RipplerPos.BACK) {
|
||||
ripplerPane.toBack();
|
||||
} else {
|
||||
ripplerPane.toFront();
|
||||
}
|
||||
}
|
||||
|
||||
public Node getControl() {
|
||||
return control;
|
||||
}
|
||||
|
||||
// methods that can be changed by extending the rippler class
|
||||
|
||||
/// generate the clipping mask
|
||||
///
|
||||
/// @return the mask node
|
||||
protected Node getMask() {
|
||||
double borderWidth = ripplerPane.getBorder() != null ? ripplerPane.getBorder().getInsets().getTop() : 0;
|
||||
Bounds bounds = control.getBoundsInParent();
|
||||
double width = control.getLayoutBounds().getWidth();
|
||||
double height = control.getLayoutBounds().getHeight();
|
||||
double diffMinX = Math.abs(control.getBoundsInLocal().getMinX() - control.getLayoutBounds().getMinX());
|
||||
double diffMinY = Math.abs(control.getBoundsInLocal().getMinY() - control.getLayoutBounds().getMinY());
|
||||
double diffMaxX = Math.abs(control.getBoundsInLocal().getMaxX() - control.getLayoutBounds().getMaxX());
|
||||
double diffMaxY = Math.abs(control.getBoundsInLocal().getMaxY() - control.getLayoutBounds().getMaxY());
|
||||
Node mask;
|
||||
switch (getMaskType()) {
|
||||
case RECT:
|
||||
mask = new Rectangle(bounds.getMinX() + diffMinX - snappedLeftInset(),
|
||||
bounds.getMinY() + diffMinY - snappedTopInset(),
|
||||
width - 2 * borderWidth,
|
||||
height - 2 * borderWidth); // -0.1 to prevent resizing the anchor pane
|
||||
break;
|
||||
case CIRCLE:
|
||||
double radius = Math.min((width / 2) - 2 * borderWidth, (height / 2) - 2 * borderWidth);
|
||||
mask = new Circle((bounds.getMinX() + diffMinX + bounds.getMaxX() - diffMaxX) / 2 - snappedLeftInset(),
|
||||
(bounds.getMinY() + diffMinY + bounds.getMaxY() - diffMaxY) / 2 - snappedTopInset(),
|
||||
radius,
|
||||
Color.BLUE);
|
||||
break;
|
||||
case FIT:
|
||||
mask = new Region();
|
||||
if (control instanceof Shape) {
|
||||
((Region) mask).setShape((Shape) control);
|
||||
} else if (control instanceof Region) {
|
||||
((Region) mask).setShape(((Region) control).getShape());
|
||||
JFXNodeUtils.updateBackground(((Region) control).getBackground(), (Region) mask);
|
||||
}
|
||||
mask.resize(width, height);
|
||||
mask.relocate(bounds.getMinX() + diffMinX, bounds.getMinY() + diffMinY);
|
||||
break;
|
||||
default:
|
||||
mask = new Rectangle(bounds.getMinX() + diffMinX - snappedLeftInset(),
|
||||
bounds.getMinY() + diffMinY - snappedTopInset(),
|
||||
width - 2 * borderWidth,
|
||||
height - 2 * borderWidth); // -0.1 to prevent resizing the anchor pane
|
||||
break;
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
/**
|
||||
* compute the ripple radius
|
||||
*
|
||||
* @return the ripple radius size
|
||||
*/
|
||||
protected double computeRippleRadius() {
|
||||
double width2 = control.getLayoutBounds().getWidth() * control.getLayoutBounds().getWidth();
|
||||
double height2 = control.getLayoutBounds().getHeight() * control.getLayoutBounds().getHeight();
|
||||
return Math.min(Math.sqrt(width2 + height2), RIPPLE_MAX_RADIUS) * 1.1 + 5;
|
||||
}
|
||||
|
||||
protected void setOverLayBounds(Rectangle overlay) {
|
||||
overlay.setWidth(control.getLayoutBounds().getWidth());
|
||||
overlay.setHeight(control.getLayoutBounds().getHeight());
|
||||
}
|
||||
|
||||
/**
|
||||
* init mouse listeners on the control
|
||||
*/
|
||||
protected void initControlListeners() {
|
||||
// if the control got resized the overlay rect must be rest
|
||||
control.layoutBoundsProperty().addListener(observable -> resetRippler());
|
||||
if (getChildren().contains(control)) {
|
||||
control.boundsInParentProperty().addListener(observable -> resetRippler());
|
||||
}
|
||||
control.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
|
||||
if (event.getButton() == MouseButton.PRIMARY)
|
||||
createRipple(event.getX(), event.getY());
|
||||
});
|
||||
// create fade out transition for the ripple
|
||||
control.addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
|
||||
if (event.getButton() == MouseButton.PRIMARY)
|
||||
releaseRipple();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* creates Ripple effect
|
||||
*/
|
||||
protected void createRipple(double x, double y) {
|
||||
if (!isRipplerDisabled()) {
|
||||
rippler.setGeneratorCenterX(x);
|
||||
rippler.setGeneratorCenterY(y);
|
||||
rippler.createRipple();
|
||||
}
|
||||
}
|
||||
|
||||
protected void releaseRipple() {
|
||||
rippler.releaseRipple();
|
||||
}
|
||||
|
||||
/**
|
||||
* creates Ripple effect in the center of the control
|
||||
*
|
||||
* @return a runnable to release the ripple when needed
|
||||
*/
|
||||
public Runnable createManualRipple() {
|
||||
if (!isRipplerDisabled()) {
|
||||
rippler.setGeneratorCenterX(control.getLayoutBounds().getWidth() / 2);
|
||||
rippler.setGeneratorCenterY(control.getLayoutBounds().getHeight() / 2);
|
||||
rippler.createRipple();
|
||||
return () -> {
|
||||
// create fade out transition for the ripple
|
||||
releaseRipple();
|
||||
};
|
||||
}
|
||||
return () -> {
|
||||
};
|
||||
}
|
||||
|
||||
/// show/hide the ripple overlay
|
||||
///
|
||||
/// @param forceOverlay used to hold the overlay after ripple action
|
||||
public void setOverlayVisible(boolean visible, boolean forceOverlay) {
|
||||
this.forceOverlay = forceOverlay;
|
||||
setOverlayVisible(visible);
|
||||
}
|
||||
|
||||
/// show/hide the ripple overlay
|
||||
/// NOTE: setting overlay visibility to false will reset forceOverlay to false
|
||||
public void setOverlayVisible(boolean visible) {
|
||||
if (visible) {
|
||||
showOverlay();
|
||||
} else {
|
||||
forceOverlay = false;
|
||||
hideOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* this method will be set to private in future versions of JFoenix,
|
||||
* user the method {@link #setOverlayVisible(boolean)}
|
||||
*/
|
||||
public void showOverlay() {
|
||||
if (rippler.overlayRect != null) {
|
||||
rippler.overlayRect.outAnimation.stop();
|
||||
}
|
||||
rippler.createOverlay();
|
||||
rippler.overlayRect.inAnimation.play();
|
||||
}
|
||||
|
||||
public void hideOverlay() {
|
||||
if (!forceOverlay) {
|
||||
if (rippler.overlayRect != null) {
|
||||
rippler.overlayRect.inAnimation.stop();
|
||||
}
|
||||
if (rippler.overlayRect != null) {
|
||||
rippler.overlayRect.outAnimation.play();
|
||||
}
|
||||
} else {
|
||||
System.err.println("Ripple Overlay is forced!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates ripples on the screen every 0.3 seconds or whenever
|
||||
* the createRipple method is called. Ripples grow and fade out
|
||||
* over 0.6 seconds
|
||||
*/
|
||||
protected final class RippleGenerator extends Group {
|
||||
|
||||
private double generatorCenterX = 0;
|
||||
private double generatorCenterY = 0;
|
||||
private OverLayRipple overlayRect;
|
||||
private final AtomicBoolean generating = new AtomicBoolean(false);
|
||||
private boolean cacheRipplerClip = false;
|
||||
private boolean resetClip = false;
|
||||
private final Queue<Ripple> ripplesQueue = new LinkedList<>();
|
||||
|
||||
RippleGenerator() {
|
||||
// improve in performance, by preventing
|
||||
// redrawing the parent when the ripple effect is triggered
|
||||
this.setManaged(false);
|
||||
this.setCache(true);
|
||||
this.setCacheHint(CacheHint.SPEED);
|
||||
}
|
||||
|
||||
void createRipple() {
|
||||
if (!generating.getAndSet(true)) {
|
||||
// create overlay once then change its color later
|
||||
createOverlay();
|
||||
if (this.getClip() == null || (getChildren().size() == 1 && !cacheRipplerClip) || resetClip) {
|
||||
this.setClip(getMask());
|
||||
}
|
||||
this.resetClip = false;
|
||||
|
||||
// create the ripple effect
|
||||
final Ripple ripple = new Ripple(generatorCenterX, generatorCenterY);
|
||||
getChildren().add(ripple);
|
||||
ripplesQueue.add(ripple);
|
||||
|
||||
// animate the ripple
|
||||
overlayRect.outAnimation.stop();
|
||||
overlayRect.inAnimation.play();
|
||||
ripple.inAnimation.play();
|
||||
}
|
||||
}
|
||||
|
||||
private void releaseRipple() {
|
||||
Ripple ripple = ripplesQueue.poll();
|
||||
if (ripple != null) {
|
||||
ripple.inAnimation.stop();
|
||||
ripple.outAnimation = new Timeline(
|
||||
new KeyFrame(Duration.millis(Math.min(800, (0.9 * 500) / ripple.getScaleX()))
|
||||
, ripple.outKeyValues));
|
||||
ripple.outAnimation.setOnFinished((event) -> getChildren().remove(ripple));
|
||||
ripple.outAnimation.play();
|
||||
if (generating.getAndSet(false)) {
|
||||
if (overlayRect != null) {
|
||||
overlayRect.inAnimation.stop();
|
||||
if (!forceOverlay) {
|
||||
overlayRect.outAnimation.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void cacheRippleClip(boolean cached) {
|
||||
cacheRipplerClip = cached;
|
||||
}
|
||||
|
||||
void createOverlay() {
|
||||
if (overlayRect == null) {
|
||||
overlayRect = new OverLayRipple();
|
||||
overlayRect.setClip(getMask());
|
||||
getChildren().add(0, overlayRect);
|
||||
overlayRect.fillProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
if (getRipplerFill() instanceof Color fill) {
|
||||
return new Color(fill.getRed(),
|
||||
fill.getGreen(),
|
||||
fill.getBlue(),
|
||||
0.2);
|
||||
} else {
|
||||
return Color.TRANSPARENT;
|
||||
}
|
||||
}, ripplerFillProperty()));
|
||||
}
|
||||
}
|
||||
|
||||
void setGeneratorCenterX(double generatorCenterX) {
|
||||
this.generatorCenterX = generatorCenterX;
|
||||
}
|
||||
|
||||
void setGeneratorCenterY(double generatorCenterY) {
|
||||
this.generatorCenterY = generatorCenterY;
|
||||
}
|
||||
|
||||
private final class OverLayRipple extends Rectangle {
|
||||
// Overlay ripple animations
|
||||
Animation inAnimation = new Timeline(new KeyFrame(Duration.millis(300),
|
||||
new KeyValue(opacityProperty(), 1, Interpolator.EASE_IN)));
|
||||
|
||||
Animation outAnimation = new Timeline(new KeyFrame(Duration.millis(300),
|
||||
new KeyValue(opacityProperty(), 0, Interpolator.EASE_OUT)));
|
||||
|
||||
OverLayRipple() {
|
||||
super();
|
||||
setOverLayBounds(this);
|
||||
this.getStyleClass().add("jfx-rippler-overlay");
|
||||
// update initial position
|
||||
if (JFXRippler.this.getChildrenUnmodifiable().contains(control)) {
|
||||
double diffMinX = Math.abs(control.getBoundsInLocal().getMinX() - control.getLayoutBounds().getMinX());
|
||||
double diffMinY = Math.abs(control.getBoundsInLocal().getMinY() - control.getLayoutBounds().getMinY());
|
||||
Bounds bounds = control.getBoundsInParent();
|
||||
this.setX(bounds.getMinX() + diffMinX - snappedLeftInset());
|
||||
this.setY(bounds.getMinY() + diffMinY - snappedTopInset());
|
||||
}
|
||||
// set initial attributes
|
||||
setOpacity(0);
|
||||
setCache(true);
|
||||
setCacheHint(CacheHint.SPEED);
|
||||
setCacheShape(true);
|
||||
setManaged(false);
|
||||
}
|
||||
}
|
||||
|
||||
private final class Ripple extends Circle {
|
||||
|
||||
KeyValue[] outKeyValues;
|
||||
Animation outAnimation = null;
|
||||
Animation inAnimation = null;
|
||||
|
||||
private Ripple(double centerX, double centerY) {
|
||||
super(centerX,
|
||||
centerY,
|
||||
getRipplerRadius() == Region.USE_COMPUTED_SIZE ?
|
||||
computeRippleRadius() : getRipplerRadius(), null);
|
||||
setCache(true);
|
||||
setCacheHint(CacheHint.SPEED);
|
||||
setCacheShape(true);
|
||||
setManaged(false);
|
||||
setSmooth(true);
|
||||
|
||||
KeyValue[] inKeyValues = new KeyValue[isRipplerRecenter() ? 4 : 2];
|
||||
outKeyValues = new KeyValue[isRipplerRecenter() ? 5 : 3];
|
||||
|
||||
inKeyValues[0] = new KeyValue(scaleXProperty(), 0.9, RIPPLE_INTERPOLATOR);
|
||||
inKeyValues[1] = new KeyValue(scaleYProperty(), 0.9, RIPPLE_INTERPOLATOR);
|
||||
|
||||
outKeyValues[0] = new KeyValue(this.scaleXProperty(), 1, RIPPLE_INTERPOLATOR);
|
||||
outKeyValues[1] = new KeyValue(this.scaleYProperty(), 1, RIPPLE_INTERPOLATOR);
|
||||
outKeyValues[2] = new KeyValue(this.opacityProperty(), 0, RIPPLE_INTERPOLATOR);
|
||||
|
||||
if (isRipplerRecenter()) {
|
||||
double dx = (control.getLayoutBounds().getWidth() / 2 - centerX) / 1.55;
|
||||
double dy = (control.getLayoutBounds().getHeight() / 2 - centerY) / 1.55;
|
||||
inKeyValues[2] = outKeyValues[3] = new KeyValue(translateXProperty(),
|
||||
Math.signum(dx) * Math.min(Math.abs(dx),
|
||||
this.getRadius() / 2),
|
||||
RIPPLE_INTERPOLATOR);
|
||||
inKeyValues[3] = outKeyValues[4] = new KeyValue(translateYProperty(),
|
||||
Math.signum(dy) * Math.min(Math.abs(dy),
|
||||
this.getRadius() / 2),
|
||||
RIPPLE_INTERPOLATOR);
|
||||
}
|
||||
inAnimation = new Timeline(new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(scaleXProperty(),
|
||||
0,
|
||||
RIPPLE_INTERPOLATOR),
|
||||
new KeyValue(scaleYProperty(),
|
||||
0,
|
||||
RIPPLE_INTERPOLATOR),
|
||||
new KeyValue(translateXProperty(),
|
||||
0,
|
||||
RIPPLE_INTERPOLATOR),
|
||||
new KeyValue(translateYProperty(),
|
||||
0,
|
||||
RIPPLE_INTERPOLATOR),
|
||||
new KeyValue(opacityProperty(),
|
||||
1,
|
||||
RIPPLE_INTERPOLATOR)
|
||||
), new KeyFrame(Duration.millis(900), inKeyValues));
|
||||
|
||||
setScaleX(0);
|
||||
setScaleY(0);
|
||||
if (getRipplerFill() instanceof Color fill) {
|
||||
Color circleColor = new Color(fill.getRed(),
|
||||
fill.getGreen(),
|
||||
fill.getBlue(),
|
||||
0.3);
|
||||
setStroke(circleColor);
|
||||
setFill(circleColor);
|
||||
} else {
|
||||
setStroke(getRipplerFill());
|
||||
setFill(getRipplerFill());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
getChildren().clear();
|
||||
rippler.overlayRect = null;
|
||||
generating.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void resetOverLay() {
|
||||
if (rippler.overlayRect != null) {
|
||||
rippler.overlayRect.inAnimation.stop();
|
||||
final RippleGenerator.OverLayRipple oldOverlay = rippler.overlayRect;
|
||||
rippler.overlayRect.outAnimation.setOnFinished((finish) -> rippler.getChildren().remove(oldOverlay));
|
||||
rippler.overlayRect.outAnimation.play();
|
||||
rippler.overlayRect = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void resetClip() {
|
||||
this.rippler.resetClip = true;
|
||||
}
|
||||
|
||||
protected void resetRippler() {
|
||||
resetOverLay();
|
||||
resetClip();
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Stylesheet Handling *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/**
|
||||
* Initialize the style class to 'jfx-rippler'.
|
||||
* <p>
|
||||
* This is the selector class from which CSS can be used to style
|
||||
* this control.
|
||||
*/
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-rippler";
|
||||
|
||||
private void initialize() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
/**
|
||||
* the ripple recenter property, by default it's false.
|
||||
* if true the ripple effect will show gravitational pull to the center of its control
|
||||
*/
|
||||
private StyleableBooleanProperty ripplerRecenter;
|
||||
|
||||
public boolean isRipplerRecenter() {
|
||||
return ripplerRecenter != null && ripplerRecenter.get();
|
||||
}
|
||||
|
||||
public StyleableBooleanProperty ripplerRecenterProperty() {
|
||||
if (this.ripplerRecenter == null) {
|
||||
this.ripplerRecenter = new SimpleStyleableBooleanProperty(
|
||||
StyleableProperties.RIPPLER_RECENTER,
|
||||
JFXRippler.this,
|
||||
"ripplerRecenter",
|
||||
false);
|
||||
}
|
||||
return this.ripplerRecenter;
|
||||
}
|
||||
|
||||
public void setRipplerRecenter(boolean recenter) {
|
||||
ripplerRecenterProperty().set(recenter);
|
||||
}
|
||||
|
||||
/**
|
||||
* the ripple radius size, by default it will be automatically computed.
|
||||
*/
|
||||
private StyleableDoubleProperty ripplerRadius;
|
||||
|
||||
public double getRipplerRadius() {
|
||||
return ripplerRadius == null ? Region.USE_COMPUTED_SIZE : ripplerRadius.get();
|
||||
}
|
||||
|
||||
public StyleableDoubleProperty ripplerRadiusProperty() {
|
||||
if (this.ripplerRadius == null) {
|
||||
this.ripplerRadius = new SimpleStyleableDoubleProperty(
|
||||
StyleableProperties.RIPPLER_RADIUS,
|
||||
JFXRippler.this,
|
||||
"ripplerRadius",
|
||||
Region.USE_COMPUTED_SIZE);
|
||||
}
|
||||
return this.ripplerRadius;
|
||||
}
|
||||
|
||||
public void setRipplerRadius(double radius) {
|
||||
ripplerRadiusProperty().set(radius);
|
||||
}
|
||||
|
||||
private static final Color DEFAULT_RIPPLER_FILL = Color.rgb(0, 200, 255);
|
||||
|
||||
/**
|
||||
* the default color of the ripple effect
|
||||
*/
|
||||
private StyleableObjectProperty<Paint> ripplerFill;
|
||||
|
||||
public Paint getRipplerFill() {
|
||||
return ripplerFill == null ? DEFAULT_RIPPLER_FILL : ripplerFill.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> ripplerFillProperty() {
|
||||
if (this.ripplerFill == null) {
|
||||
this.ripplerFill = new SimpleStyleableObjectProperty<>(StyleableProperties.RIPPLER_FILL,
|
||||
JFXRippler.this,
|
||||
"ripplerFill",
|
||||
DEFAULT_RIPPLER_FILL);
|
||||
}
|
||||
return this.ripplerFill;
|
||||
}
|
||||
|
||||
public void setRipplerFill(Paint color) {
|
||||
ripplerFillProperty().set(color);
|
||||
}
|
||||
|
||||
/// mask property used for clipping the rippler.
|
||||
/// can be either CIRCLE/RECT
|
||||
private StyleableObjectProperty<RipplerMask> maskType;
|
||||
|
||||
public RipplerMask getMaskType() {
|
||||
return maskType == null ? RipplerMask.RECT : maskType.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<RipplerMask> maskTypeProperty() {
|
||||
if (this.maskType == null) {
|
||||
this.maskType = new SimpleStyleableObjectProperty<>(
|
||||
StyleableProperties.MASK_TYPE,
|
||||
JFXRippler.this,
|
||||
"maskType",
|
||||
RipplerMask.RECT);
|
||||
}
|
||||
return this.maskType;
|
||||
}
|
||||
|
||||
public void setMaskType(RipplerMask type) {
|
||||
if (this.maskType != null || type != RipplerMask.RECT)
|
||||
maskTypeProperty().set(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* the ripple disable, by default it's false.
|
||||
* if true the ripple effect will be hidden
|
||||
*/
|
||||
private StyleableBooleanProperty ripplerDisabled;
|
||||
|
||||
public boolean isRipplerDisabled() {
|
||||
return ripplerDisabled != null && ripplerDisabled.get();
|
||||
}
|
||||
|
||||
public StyleableBooleanProperty ripplerDisabledProperty() {
|
||||
if (this.ripplerDisabled == null) {
|
||||
this.ripplerDisabled = new SimpleStyleableBooleanProperty(
|
||||
StyleableProperties.RIPPLER_DISABLED,
|
||||
JFXRippler.this,
|
||||
"ripplerDisabled",
|
||||
false);
|
||||
}
|
||||
return this.ripplerDisabled;
|
||||
}
|
||||
|
||||
public void setRipplerDisabled(boolean disabled) {
|
||||
ripplerDisabledProperty().set(disabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* indicates whether the ripple effect is infront of or behind the node
|
||||
*/
|
||||
protected ObjectProperty<RipplerPos> position = new SimpleObjectProperty<>();
|
||||
|
||||
public void setPosition(RipplerPos pos) {
|
||||
this.position.set(pos);
|
||||
}
|
||||
|
||||
public RipplerPos getPosition() {
|
||||
return position == null ? RipplerPos.FRONT : position.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<RipplerPos> positionProperty() {
|
||||
return this.position;
|
||||
}
|
||||
|
||||
private static final class StyleableProperties {
|
||||
private static final CssMetaData<JFXRippler, Boolean> RIPPLER_RECENTER =
|
||||
new CssMetaData<>("-jfx-rippler-recenter",
|
||||
BooleanConverter.getInstance(), false) {
|
||||
@Override
|
||||
public boolean isSettable(JFXRippler control) {
|
||||
return control.ripplerRecenter == null || !control.ripplerRecenter.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Boolean> getStyleableProperty(JFXRippler control) {
|
||||
return control.ripplerRecenterProperty();
|
||||
}
|
||||
};
|
||||
private static final CssMetaData<JFXRippler, Boolean> RIPPLER_DISABLED =
|
||||
new CssMetaData<>("-jfx-rippler-disabled",
|
||||
BooleanConverter.getInstance(), false) {
|
||||
@Override
|
||||
public boolean isSettable(JFXRippler control) {
|
||||
return control.ripplerDisabled == null || !control.ripplerDisabled.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Boolean> getStyleableProperty(JFXRippler control) {
|
||||
return control.ripplerDisabledProperty();
|
||||
}
|
||||
};
|
||||
private static final CssMetaData<JFXRippler, Paint> RIPPLER_FILL =
|
||||
new CssMetaData<>("-jfx-rippler-fill",
|
||||
PaintConverter.getInstance(), DEFAULT_RIPPLER_FILL) {
|
||||
@Override
|
||||
public boolean isSettable(JFXRippler control) {
|
||||
return control.ripplerFill == null || !control.ripplerFill.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(JFXRippler control) {
|
||||
return control.ripplerFillProperty();
|
||||
}
|
||||
};
|
||||
private static final CssMetaData<JFXRippler, Number> RIPPLER_RADIUS =
|
||||
new CssMetaData<>("-jfx-rippler-radius",
|
||||
SizeConverter.getInstance(), Region.USE_COMPUTED_SIZE) {
|
||||
@Override
|
||||
public boolean isSettable(JFXRippler control) {
|
||||
return control.ripplerRadius == null || !control.ripplerRadius.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Number> getStyleableProperty(JFXRippler control) {
|
||||
return control.ripplerRadiusProperty();
|
||||
}
|
||||
};
|
||||
private static final CssMetaData<JFXRippler, RipplerMask> MASK_TYPE =
|
||||
new CssMetaData<>("-jfx-mask-type",
|
||||
RipplerMaskTypeConverter.getInstance(), RipplerMask.RECT) {
|
||||
@Override
|
||||
public boolean isSettable(JFXRippler control) {
|
||||
return control.maskType == null || !control.maskType.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<RipplerMask> getStyleableProperty(JFXRippler control) {
|
||||
return control.maskTypeProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
||||
|
||||
static {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables =
|
||||
new ArrayList<>(StackPane.getClassCssMetaData());
|
||||
Collections.addAll(styleables,
|
||||
RIPPLER_RECENTER,
|
||||
RIPPLER_RADIUS,
|
||||
RIPPLER_FILL,
|
||||
MASK_TYPE,
|
||||
RIPPLER_DISABLED
|
||||
);
|
||||
STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
|
||||
return getClassCssMetaData();
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.STYLEABLES;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.converters.IndicatorPositionConverter;
|
||||
import com.jfoenix.skins.JFXSliderSkin;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.css.*;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.Slider;
|
||||
import javafx.util.Callback;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/// JFXSlider is the material design implementation of a slider.
|
||||
///
|
||||
/// @author Bashir Elias & Shadi Shaheen
|
||||
/// @version 1.0
|
||||
/// @since 2016-03-09
|
||||
public class JFXSlider extends Slider {
|
||||
|
||||
public JFXSlider() {
|
||||
super(0, 100, 50);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public JFXSlider(double min, double max, double value) {
|
||||
super(min, max, value);
|
||||
initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new JFXSliderSkin(this);
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
public enum IndicatorPosition {
|
||||
LEFT, RIGHT
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Properties *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/// String binding factory for the slider value.
|
||||
/// Sets a custom string for the value text (by default, it shows the value rounded to the nearest whole number).
|
||||
///
|
||||
///
|
||||
/// For example, to have the value displayed as a percentage (assuming the slider has a range of (0, 100)):
|
||||
/// ```java
|
||||
/// JFXSlider mySlider = ...
|
||||
/// mySlider.setValueFactory(slider ->
|
||||
/// Bindings.createStringBinding(
|
||||
/// () -> ((int) slider.getValue()) + "%",
|
||||
/// slider.valueProperty()
|
||||
/// )
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// NOTE: might be replaced later with a call back to create the animated thumb node
|
||||
///
|
||||
/// @param callback a callback to create the string value binding
|
||||
private ObjectProperty<Callback<JFXSlider, StringBinding>> valueFactory;
|
||||
|
||||
public final ObjectProperty<Callback<JFXSlider, StringBinding>> valueFactoryProperty() {
|
||||
if (valueFactory == null) {
|
||||
valueFactory = new SimpleObjectProperty<>(this, "valueFactory");
|
||||
}
|
||||
return valueFactory;
|
||||
}
|
||||
|
||||
/// @return the current slider value factory
|
||||
public final Callback<JFXSlider, StringBinding> getValueFactory() {
|
||||
return valueFactory == null ? null : valueFactory.get();
|
||||
}
|
||||
|
||||
/// sets custom string binding for the slider text value
|
||||
///
|
||||
/// @param valueFactory a callback to create the string value binding
|
||||
public final void setValueFactory(final Callback<JFXSlider, StringBinding> valueFactory) {
|
||||
this.valueFactoryProperty().set(valueFactory);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Stylesheet Handling *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/// Initialize the style class to 'jfx-slider'.
|
||||
///
|
||||
/// This is the selector class from which CSS can be used to style
|
||||
/// this control.
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-slider";
|
||||
|
||||
/// indicates the position of the slider indicator, can be
|
||||
/// either LEFT or RIGHT
|
||||
private StyleableObjectProperty<IndicatorPosition> indicatorPosition;
|
||||
|
||||
public StyleableObjectProperty<IndicatorPosition> indicatorPositionProperty() {
|
||||
if (indicatorPosition == null) {
|
||||
indicatorPosition = new SimpleStyleableObjectProperty<>(
|
||||
StyleableProperties.INDICATOR_POSITION,
|
||||
JFXSlider.this,
|
||||
"indicatorPosition",
|
||||
IndicatorPosition.LEFT);
|
||||
}
|
||||
return this.indicatorPosition;
|
||||
}
|
||||
|
||||
public IndicatorPosition getIndicatorPosition() {
|
||||
return indicatorPosition == null ? IndicatorPosition.LEFT : indicatorPosition.get();
|
||||
}
|
||||
|
||||
public void setIndicatorPosition(IndicatorPosition pos) {
|
||||
indicatorPositionProperty().set(pos);
|
||||
}
|
||||
|
||||
private static final class StyleableProperties {
|
||||
private static final CssMetaData<JFXSlider, IndicatorPosition> INDICATOR_POSITION = new CssMetaData<>(
|
||||
"-jfx-indicator-position",
|
||||
IndicatorPositionConverter.getInstance(),
|
||||
IndicatorPosition.LEFT) {
|
||||
@Override
|
||||
public boolean isSettable(JFXSlider control) {
|
||||
return control.indicatorPosition == null || !control.indicatorPosition.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<IndicatorPosition> getStyleableProperty(JFXSlider control) {
|
||||
return control.indicatorPositionProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
||||
|
||||
static {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(
|
||||
Slider.getClassCssMetaData());
|
||||
Collections.addAll(styleables, INDICATOR_POSITION);
|
||||
CHILD_STYLEABLES = List.copyOf(styleables);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
||||
return getClassCssMetaData();
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.CHILD_STYLEABLES;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventType;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/// "Snackbars provide brief messages about app processes at the bottom of the screen"
|
||||
/// (<a href="https://material.io/design/components/snackbars.html#">Material Design Guidelines</a>).
|
||||
///
|
||||
/// To show a snackbar you need to
|
||||
/// <ol>
|
||||
/// - Have a [Pane] (snackbarContainer) to show the snackbar on top of. Register it in
|
||||
/// [the JFXSnackbar constructor][#JFXSnackbar(Pane)] or using the [#registerSnackbarContainer(Pane)] method.
|
||||
/// - Have or create a [JFXSnackbar]. - Having one snackbar where you pass all your
|
||||
/// [SnackbarEvents][JFXSnackbar.SnackbarEvent] will ensure that the [enqueuemethod][JFXSnackbar#enqueue(SnackbarEvent)] works as intended.
|
||||
///
|
||||
/// - Have something to show in the snackbar. A [JFXSnackbarLayout] is nice and pretty,
|
||||
/// but any arbitrary [Node] will do.
|
||||
/// - Create a [SnackbarEvent][JFXSnackbar.SnackbarEvent] specifying the contents and the
|
||||
/// duration.
|
||||
/// </ol>
|
||||
///
|
||||
/// Finally, with all those things prepared, show your snackbar using
|
||||
/// [snackbar.enqueue(snackbarEvent);][JFXSnackbar#enqueue(SnackbarEvent)].
|
||||
///
|
||||
/// It's most convenient to create functions to do most of this (creating the layout and event) with the default
|
||||
/// settings; that way all you need to do to show a snackbar is specify the message or just the message and the duration.
|
||||
///
|
||||
/// @see <a href="https://material.io/design/components/snackbars.html#"> The Material Design Snackbar</a>
|
||||
public class JFXSnackbar extends Group {
|
||||
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-snackbar";
|
||||
|
||||
private Pane snackbarContainer;
|
||||
private final ChangeListener<? super Number> sizeListener = (o, oldVal, newVal) -> refreshPopup();
|
||||
private final WeakChangeListener<? super Number> weakSizeListener = new WeakChangeListener<>(sizeListener);
|
||||
|
||||
private final AtomicBoolean processingQueue = new AtomicBoolean(false);
|
||||
private final ConcurrentLinkedQueue<SnackbarEvent> eventQueue = new ConcurrentLinkedQueue<>();
|
||||
private final ConcurrentHashMap.KeySetView<Object, Boolean> eventsSet = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private final Pane content;
|
||||
private PseudoClass activePseudoClass = null;
|
||||
private PauseTransition pauseTransition;
|
||||
|
||||
/// This constructor assumes that you will eventually call the [#registerSnackbarContainer(Pane)] method before
|
||||
/// calling the [#enqueue(SnackbarEvent)] method. Otherwise, how will the snackbar know where to show itself?
|
||||
///
|
||||
///
|
||||
/// "Snackbars provide brief messages about app processes at the bottom of the screen"
|
||||
/// (<a href="https://material.io/design/components/snackbars.html#">Material Design Guidelines</a>).
|
||||
///
|
||||
/// To show a snackbar you need to
|
||||
///
|
||||
/// - Have a [Pane] (snackbarContainer) to show the snackbar on top of. Register it in
|
||||
/// [the JFXSnackbar constructor][#JFXSnackbar(Pane)] or using the [#registerSnackbarContainer(Pane)] method.
|
||||
/// - Have or create a [JFXSnackbar]. - Having one snackbar where you pass all your
|
||||
/// [SnackbarEvents][JFXSnackbar.SnackbarEvent] will ensure that the [enqueuemethod][JFXSnackbar#enqueue(SnackbarEvent)] works as intended.
|
||||
/// - Have something to show in the snackbar. A [JFXSnackbarLayout] is nice and pretty,
|
||||
/// but any arbitrary [Node] will do.
|
||||
/// - Create a [SnackbarEvent][JFXSnackbar.SnackbarEvent] specifying the contents and the
|
||||
/// duration.
|
||||
///
|
||||
/// Finally, with all those things prepared, show your snackbar using
|
||||
/// [snackbar.enqueue(snackbarEvent);][JFXSnackbar#enqueue(SnackbarEvent)].
|
||||
///
|
||||
public JFXSnackbar() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
/// "Snackbars provide brief messages about app processes at the bottom of the screen"
|
||||
/// (<a href="https://material.io/design/components/snackbars.html#">Material Design Guidelines</a>).
|
||||
///
|
||||
/// To show a snackbar you need to
|
||||
///
|
||||
/// - Have a [Pane] (snackbarContainer) to show the snackbar on top of. Register it in
|
||||
/// [the JFXSnackbar constructor][#JFXSnackbar(Pane)] or using the [#registerSnackbarContainer(Pane)] method.
|
||||
/// - Have or create a [JFXSnackbar]. - Having one snackbar where you pass all your
|
||||
/// [SnackbarEvents][JFXSnackbar.SnackbarEvent] will ensure that the [enqueuemethod][JFXSnackbar#enqueue(SnackbarEvent)] works as intended.
|
||||
/// - Have something to show in the snackbar. A [JFXSnackbarLayout] is nice and pretty,
|
||||
/// but any arbitrary [Node] will do.
|
||||
/// - Create a [SnackbarEvent][JFXSnackbar.SnackbarEvent] specifying the contents and the
|
||||
/// duration.
|
||||
///
|
||||
/// Finally, with all those things prepared, show your snackbar using
|
||||
/// [snackbar.enqueue(snackbarEvent);][JFXSnackbar#enqueue(SnackbarEvent)].
|
||||
///
|
||||
/// @param snackbarContainer where the snackbar will appear. Using a single snackbar instead of many, will ensure that
|
||||
/// the [#enqueue(SnackbarEvent)] method works correctly.
|
||||
public JFXSnackbar(Pane snackbarContainer) {
|
||||
initialize();
|
||||
content = new StackPane();
|
||||
content.getStyleClass().add("jfx-snackbar-content");
|
||||
//wrap the content in a group so that the content is managed inside its own container
|
||||
//but the group is not managed in the snackbarContainer so it does not affect any layout calculations
|
||||
getChildren().add(content);
|
||||
setManaged(false);
|
||||
setVisible(false);
|
||||
|
||||
// register the container before resizing it
|
||||
registerSnackbarContainer(snackbarContainer);
|
||||
|
||||
// resize the popup if its layout has been changed
|
||||
layoutBoundsProperty().addListener((o, oldVal, newVal) -> refreshPopup());
|
||||
|
||||
addEventHandler(SnackbarEvent.SNACKBAR, this::enqueue);
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
// Setters / Getters
|
||||
|
||||
public Pane getPopupContainer() {
|
||||
return snackbarContainer;
|
||||
}
|
||||
|
||||
public void setPrefWidth(double width) {
|
||||
content.setPrefWidth(width);
|
||||
}
|
||||
|
||||
public double getPrefWidth() {
|
||||
return content.getPrefWidth();
|
||||
}
|
||||
|
||||
// Public API
|
||||
|
||||
public void registerSnackbarContainer(Pane snackbarContainer) {
|
||||
if (snackbarContainer != null) {
|
||||
if (this.snackbarContainer != null) {
|
||||
//since listeners are added the container should be properly registered/unregistered
|
||||
throw new IllegalArgumentException("Snackbar Container already set");
|
||||
}
|
||||
this.snackbarContainer = snackbarContainer;
|
||||
this.snackbarContainer.getChildren().add(this);
|
||||
this.snackbarContainer.heightProperty().addListener(weakSizeListener);
|
||||
this.snackbarContainer.widthProperty().addListener(weakSizeListener);
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterSnackbarContainer(Pane snackbarContainer) {
|
||||
if (snackbarContainer != null) {
|
||||
if (this.snackbarContainer == null) {
|
||||
throw new IllegalArgumentException("Snackbar Container not set");
|
||||
}
|
||||
this.snackbarContainer.getChildren().remove(this);
|
||||
this.snackbarContainer.heightProperty().removeListener(weakSizeListener);
|
||||
this.snackbarContainer.widthProperty().removeListener(weakSizeListener);
|
||||
this.snackbarContainer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void show(SnackbarEvent event) {
|
||||
content.getChildren().setAll(event.getContent());
|
||||
openAnimation = getTimeline(event.getTimeout());
|
||||
if (event.getPseudoClass() != null) {
|
||||
activePseudoClass = event.getPseudoClass();
|
||||
content.pseudoClassStateChanged(activePseudoClass, true);
|
||||
}
|
||||
openAnimation.play();
|
||||
}
|
||||
|
||||
private Timeline openAnimation = null;
|
||||
|
||||
private Timeline getTimeline(Duration timeout) {
|
||||
Timeline animation;
|
||||
animation = new Timeline(
|
||||
new KeyFrame(
|
||||
Duration.ZERO,
|
||||
e -> {
|
||||
this.toBack();
|
||||
this.setVisible(false);
|
||||
},
|
||||
new KeyValue(this.translateYProperty(), this.getLayoutBounds().getHeight(), Motion.EASE),
|
||||
new KeyValue(this.opacityProperty(), 0, Motion.EASE)
|
||||
),
|
||||
new KeyFrame(
|
||||
Duration.millis(10),
|
||||
e -> {
|
||||
this.toFront();
|
||||
this.setVisible(true);
|
||||
}
|
||||
),
|
||||
new KeyFrame(Duration.millis(300),
|
||||
new KeyValue(this.opacityProperty(), 1, Motion.EASE),
|
||||
new KeyValue(this.translateYProperty(), 0, Motion.EASE)
|
||||
)
|
||||
);
|
||||
animation.setCycleCount(1);
|
||||
pauseTransition = Duration.INDEFINITE.equals(timeout) ? null : new PauseTransition(timeout);
|
||||
if (pauseTransition != null) {
|
||||
animation.setOnFinished(finish -> {
|
||||
pauseTransition.setOnFinished(done -> {
|
||||
pauseTransition = null;
|
||||
eventsSet.remove(currentEvent);
|
||||
currentEvent = eventQueue.peek();
|
||||
close();
|
||||
});
|
||||
pauseTransition.play();
|
||||
});
|
||||
}
|
||||
return animation;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (openAnimation != null) {
|
||||
openAnimation.stop();
|
||||
}
|
||||
if (this.isVisible()) {
|
||||
Timeline closeAnimation = new Timeline(
|
||||
new KeyFrame(
|
||||
Duration.ZERO,
|
||||
e -> this.toFront(),
|
||||
new KeyValue(this.opacityProperty(), 1, Motion.EASE),
|
||||
new KeyValue(this.translateYProperty(), 0, Motion.EASE)
|
||||
),
|
||||
new KeyFrame(
|
||||
Duration.millis(290),
|
||||
e -> this.setVisible(true)
|
||||
),
|
||||
new KeyFrame(Duration.millis(300),
|
||||
e -> {
|
||||
this.toBack();
|
||||
this.setVisible(false);
|
||||
},
|
||||
new KeyValue(this.translateYProperty(), this.getLayoutBounds().getHeight(), Motion.EASE),
|
||||
new KeyValue(this.opacityProperty(), 0, Motion.EASE)
|
||||
)
|
||||
);
|
||||
closeAnimation.setCycleCount(1);
|
||||
closeAnimation.setOnFinished(e -> {
|
||||
resetPseudoClass();
|
||||
processSnackbar();
|
||||
});
|
||||
closeAnimation.play();
|
||||
}
|
||||
}
|
||||
|
||||
private SnackbarEvent currentEvent = null;
|
||||
|
||||
public SnackbarEvent getCurrentEvent() {
|
||||
return currentEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows {@link SnackbarEvent SnackbarEvents} one by one. The next event will be shown after the current event's duration.
|
||||
*
|
||||
* @param event the {@link SnackbarEvent event} to put in the queue.
|
||||
*/
|
||||
public void enqueue(SnackbarEvent event) {
|
||||
synchronized (this) {
|
||||
if (!eventsSet.contains(event)) {
|
||||
eventsSet.add(event);
|
||||
eventQueue.offer(event);
|
||||
} else if (currentEvent == event && pauseTransition != null) {
|
||||
pauseTransition.playFromStart();
|
||||
}
|
||||
}
|
||||
if (processingQueue.compareAndSet(false, true)) {
|
||||
Platform.runLater(() -> {
|
||||
currentEvent = eventQueue.poll();
|
||||
if (currentEvent != null) {
|
||||
show(currentEvent);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void resetPseudoClass() {
|
||||
if (activePseudoClass != null) {
|
||||
content.pseudoClassStateChanged(activePseudoClass, false);
|
||||
activePseudoClass = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void processSnackbar() {
|
||||
currentEvent = eventQueue.poll();
|
||||
if (currentEvent != null) {
|
||||
eventsSet.remove(currentEvent);
|
||||
show(currentEvent);
|
||||
} else {
|
||||
//The enqueue method and this listener should be executed sequentially on the FX Thread so there
|
||||
//should not be a race condition
|
||||
processingQueue.getAndSet(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshPopup() {
|
||||
if (snackbarContainer == null) {
|
||||
return;
|
||||
}
|
||||
Bounds contentBound = this.getLayoutBounds();
|
||||
double offsetX = Math.ceil(snackbarContainer.getWidth() / 2) - Math.ceil(contentBound.getWidth() / 2);
|
||||
double offsetY = snackbarContainer.getHeight() - contentBound.getHeight();
|
||||
this.setLayoutX(offsetX);
|
||||
this.setLayoutY(offsetY);
|
||||
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Event API
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// Specifies _what_ and _how long_ to show a [JFXSnackbar].
|
||||
///
|
||||
/// The _what_ can be any arbitrary [Node]; the [JFXSnackbarLayout] is a great choice.
|
||||
///
|
||||
/// The _how long_ is specified in the form of a [javafx.util.Duration][javafx.util.Duration], not to be
|
||||
/// confused with the [java.time.Duration].
|
||||
public static class SnackbarEvent extends Event {
|
||||
|
||||
public static final EventType<SnackbarEvent> SNACKBAR = new EventType<>(Event.ANY, "SNACKBAR");
|
||||
|
||||
/// The amount of time the snackbar will show for, if not otherwise specified.
|
||||
///
|
||||
/// It's 1.5 seconds.
|
||||
public static Duration DEFAULT_DURATION = Duration.seconds(1.5);
|
||||
|
||||
private final Node content;
|
||||
private final PseudoClass pseudoClass;
|
||||
private final Duration timeout;
|
||||
|
||||
/// Creates a [SnackbarEvent] with the [default duration][#DEFAULT_DURATION] and no pseudoClass.
|
||||
///
|
||||
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
|
||||
public SnackbarEvent(Node content) {
|
||||
this(content, DEFAULT_DURATION, null);
|
||||
}
|
||||
|
||||
/// Creates a [SnackbarEvent] with the [default duration][#DEFAULT_DURATION]; you specify the contents and
|
||||
/// pseudoClass.
|
||||
///
|
||||
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
|
||||
public SnackbarEvent(Node content, PseudoClass pseudoClass) {
|
||||
this(content, DEFAULT_DURATION, pseudoClass);
|
||||
}
|
||||
|
||||
/// Creates a SnackbarEvent with no pseudoClass; you specify the contents and duration.
|
||||
/// pseudoClass.
|
||||
///
|
||||
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
|
||||
/// @param timeout the amount of time you want the snackbar to show for.
|
||||
public SnackbarEvent(Node content, Duration timeout) {
|
||||
this(content, timeout, null);
|
||||
}
|
||||
|
||||
/// Creates a SnackbarEvent; you specify the contents, duration and pseudoClass.
|
||||
///
|
||||
/// If you don't need so much customization, try one of the other constructors.
|
||||
///
|
||||
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
|
||||
/// @param timeout the amount of time you want the snackbar to show for.
|
||||
public SnackbarEvent(Node content, Duration timeout, PseudoClass pseudoClass) {
|
||||
super(SNACKBAR);
|
||||
this.content = content;
|
||||
this.pseudoClass = pseudoClass;
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public Node getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public PseudoClass getPseudoClass() {
|
||||
return pseudoClass;
|
||||
}
|
||||
|
||||
public Duration getTimeout() {
|
||||
return timeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public EventType<? extends SnackbarEvent> getEventType() {
|
||||
return (EventType<? extends SnackbarEvent>) super.getEventType();
|
||||
}
|
||||
|
||||
public boolean isPersistent() {
|
||||
return Duration.INDEFINITE.equals(getTimeout());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
/// JFXSnackbarLayout default layout for snackbar content
|
||||
///
|
||||
/// @author Shadi Shaheen
|
||||
/// @version 1.0
|
||||
/// @since 2018-11-16
|
||||
public class JFXSnackbarLayout extends BorderPane {
|
||||
|
||||
private final Label toast;
|
||||
private JFXButton action;
|
||||
private final StackPane actionContainer;
|
||||
|
||||
public JFXSnackbarLayout(String message) {
|
||||
this(message, null, null);
|
||||
}
|
||||
|
||||
public JFXSnackbarLayout(String message, String actionText, EventHandler<ActionEvent> actionHandler) {
|
||||
initialize();
|
||||
|
||||
toast = new Label();
|
||||
toast.setMinWidth(Control.USE_PREF_SIZE);
|
||||
toast.getStyleClass().add("jfx-snackbar-toast");
|
||||
toast.setWrapText(true);
|
||||
toast.setText(message);
|
||||
StackPane toastContainer = new StackPane(toast);
|
||||
toastContainer.setPadding(new Insets(20));
|
||||
actionContainer = new StackPane();
|
||||
actionContainer.setPadding(new Insets(0, 10, 0, 0));
|
||||
|
||||
toast.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> {
|
||||
if (getPrefWidth() == -1) {
|
||||
return getPrefWidth();
|
||||
}
|
||||
double actionWidth = actionContainer.isVisible() ? actionContainer.getWidth() : 0.0;
|
||||
return prefWidthProperty().get() - actionWidth;
|
||||
}, prefWidthProperty(), actionContainer.widthProperty(), actionContainer.visibleProperty()));
|
||||
|
||||
setLeft(toastContainer);
|
||||
setRight(actionContainer);
|
||||
|
||||
if (actionText != null) {
|
||||
action = new JFXButton();
|
||||
action.setText(actionText);
|
||||
action.setOnAction(actionHandler);
|
||||
action.setMinWidth(Control.USE_PREF_SIZE);
|
||||
action.setButtonType(JFXButton.ButtonType.FLAT);
|
||||
action.getStyleClass().add("jfx-snackbar-action");
|
||||
// actions will be added upon showing the snackbar if needed
|
||||
actionContainer.getChildren().add(action);
|
||||
|
||||
if (!actionText.isEmpty()) {
|
||||
action.setVisible(true);
|
||||
actionContainer.setVisible(true);
|
||||
actionContainer.setManaged(true);
|
||||
// to force updating the layout bounds
|
||||
action.setText("");
|
||||
action.setText(actionText);
|
||||
action.setOnAction(actionHandler);
|
||||
} else {
|
||||
actionContainer.setVisible(false);
|
||||
actionContainer.setManaged(false);
|
||||
action.setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-snackbar-layout";
|
||||
|
||||
public String getToast() {
|
||||
return toast.getText();
|
||||
}
|
||||
|
||||
public void setToast(String toast) {
|
||||
this.toast.setText(toast);
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.skins.JFXSpinnerSkin;
|
||||
import javafx.css.CssMetaData;
|
||||
import javafx.css.SimpleStyleableDoubleProperty;
|
||||
import javafx.css.Styleable;
|
||||
import javafx.css.StyleableDoubleProperty;
|
||||
import javafx.css.converter.SizeConverter;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.control.Skin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/// JFXSpinner is the material design implementation of a loading spinner.
|
||||
///
|
||||
/// @author Bashir Elias & Shadi Shaheen
|
||||
/// @version 1.0
|
||||
/// @since 2016-03-09
|
||||
public class JFXSpinner extends ProgressIndicator {
|
||||
|
||||
public static final double INDETERMINATE_PROGRESS = ProgressIndicator.INDETERMINATE_PROGRESS;
|
||||
|
||||
public JFXSpinner() {
|
||||
this(INDETERMINATE_PROGRESS);
|
||||
}
|
||||
|
||||
public JFXSpinner(double progress) {
|
||||
super(progress);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new JFXSpinnerSkin(this);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Stylesheet Handling *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/// Initialize the style class to 'jfx-spinner'.
|
||||
///
|
||||
/// This is the selector class from which CSS can be used to style
|
||||
/// this control.
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-spinner";
|
||||
|
||||
private static final double DEFAULT_RADIUS = 16.0;
|
||||
|
||||
/**
|
||||
* specifies the radius of the spinner node, by default it's set to `16.0`
|
||||
*/
|
||||
private StyleableDoubleProperty radius;
|
||||
|
||||
public final StyleableDoubleProperty radiusProperty() {
|
||||
if (this.radius == null) {
|
||||
this.radius = new SimpleStyleableDoubleProperty(StyleableProperties.RADIUS,
|
||||
JFXSpinner.this,
|
||||
"radius",
|
||||
DEFAULT_RADIUS);
|
||||
}
|
||||
return this.radius;
|
||||
}
|
||||
|
||||
public final double getRadius() {
|
||||
return radius != null ? radius.get() : DEFAULT_RADIUS;
|
||||
}
|
||||
|
||||
public final void setRadius(final double radius) {
|
||||
this.radiusProperty().set(radius);
|
||||
}
|
||||
|
||||
public double getStartingAngle() {
|
||||
return 360 - ThreadLocalRandom.current().nextDouble() * 720;
|
||||
}
|
||||
|
||||
private static final class StyleableProperties {
|
||||
private static final CssMetaData<JFXSpinner, Number> RADIUS =
|
||||
new CssMetaData<>("-jfx-radius",
|
||||
SizeConverter.getInstance(), DEFAULT_RADIUS) {
|
||||
@Override
|
||||
public boolean isSettable(JFXSpinner control) {
|
||||
return control.radius == null || !control.radius.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableDoubleProperty getStyleableProperty(JFXSpinner control) {
|
||||
return control.radiusProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
||||
|
||||
static {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables =
|
||||
new ArrayList<>(ProgressIndicator.getClassCssMetaData());
|
||||
Collections.addAll(styleables, RADIUS);
|
||||
CHILD_STYLEABLES = List.copyOf(styleables);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
||||
return getClassCssMetaData();
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.CHILD_STYLEABLES;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.skins.JFXTextAreaSkin;
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.*;
|
||||
import javafx.css.converter.BooleanConverter;
|
||||
import javafx.css.converter.PaintConverter;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.TextArea;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu;
|
||||
|
||||
/**
|
||||
* JFXTextArea is the material design implementation of a text area.
|
||||
*
|
||||
* @author Shadi Shaheen
|
||||
* @version 1.0
|
||||
* @since 2016-03-09
|
||||
*/
|
||||
public class JFXTextArea extends TextArea {
|
||||
/**
|
||||
* Initialize the style class to 'jfx-text-field'.
|
||||
* <p>
|
||||
* This is the selector class from which CSS can be used to style
|
||||
* this control.
|
||||
*/
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-text-area";
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public JFXTextArea() {
|
||||
initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public JFXTextArea(String text) {
|
||||
super(text);
|
||||
initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new JFXTextAreaSkin(this);
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
if ("dalvik".equalsIgnoreCase(System.getProperty("java.vm.name"))) {
|
||||
this.setStyle("-fx-skin: \"com.jfoenix.android.skins.JFXTextAreaSkinAndroid\";");
|
||||
}
|
||||
|
||||
useJFXContextMenu(this);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Properties *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/**
|
||||
* holds the current active validator on the text area in case of validation error
|
||||
*/
|
||||
private ReadOnlyObjectWrapper<ValidatorBase> activeValidator = new ReadOnlyObjectWrapper<>();
|
||||
|
||||
public ValidatorBase getActiveValidator() {
|
||||
return activeValidator == null ? null : activeValidator.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<ValidatorBase> activeValidatorProperty() {
|
||||
return this.activeValidator.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
/**
|
||||
* list of validators that will validate the text value upon calling
|
||||
* {{@link #validate()}
|
||||
*/
|
||||
private ObservableList<ValidatorBase> validators = FXCollections.observableArrayList();
|
||||
|
||||
public ObservableList<ValidatorBase> getValidators() {
|
||||
return validators;
|
||||
}
|
||||
|
||||
public void setValidators(ValidatorBase... validators) {
|
||||
this.validators.addAll(validators);
|
||||
}
|
||||
|
||||
/**
|
||||
* validates the text value using the list of validators provided by the user
|
||||
* {{@link #setValidators(ValidatorBase...)}
|
||||
*
|
||||
* @return true if the value is valid else false
|
||||
*/
|
||||
public boolean validate() {
|
||||
for (ValidatorBase validator : validators) {
|
||||
if (validator.getSrcControl() == null) {
|
||||
validator.setSrcControl(this);
|
||||
}
|
||||
validator.validate();
|
||||
if (validator.getHasErrors()) {
|
||||
activeValidator.set(validator);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
activeValidator.set(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void resetValidation() {
|
||||
pseudoClassStateChanged(ValidatorBase.PSEUDO_CLASS_ERROR, false);
|
||||
activeValidator.set(null);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* styleable Properties *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/**
|
||||
* set true to show a float the prompt text when focusing the field
|
||||
*/
|
||||
private StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT, JFXTextArea.this, "lableFloat", false);
|
||||
|
||||
public final StyleableBooleanProperty labelFloatProperty() {
|
||||
return this.labelFloat;
|
||||
}
|
||||
|
||||
public final boolean isLabelFloat() {
|
||||
return this.labelFloatProperty().get();
|
||||
}
|
||||
|
||||
public final void setLabelFloat(final boolean labelFloat) {
|
||||
this.labelFloatProperty().set(labelFloat);
|
||||
}
|
||||
|
||||
/**
|
||||
* default color used when the text area is unfocused
|
||||
*/
|
||||
private StyleableObjectProperty<Paint> unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR, JFXTextArea.this, "unFocusColor", Color.rgb(77, 77, 77));
|
||||
|
||||
public Paint getUnFocusColor() {
|
||||
return unFocusColor == null ? Color.rgb(77, 77, 77) : unFocusColor.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> unFocusColorProperty() {
|
||||
return this.unFocusColor;
|
||||
}
|
||||
|
||||
public void setUnFocusColor(Paint color) {
|
||||
this.unFocusColor.set(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* default color used when the text area is focused
|
||||
*/
|
||||
private StyleableObjectProperty<Paint> focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR, JFXTextArea.this, "focusColor", Color.valueOf("#4059A9"));
|
||||
|
||||
public Paint getFocusColor() {
|
||||
return focusColor == null ? Color.valueOf("#4059A9") : focusColor.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> focusColorProperty() {
|
||||
return this.focusColor;
|
||||
}
|
||||
|
||||
public void setFocusColor(Paint color) {
|
||||
this.focusColor.set(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* disable animation on validation
|
||||
*/
|
||||
private StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION, JFXTextArea.this, "disableAnimation", false);
|
||||
|
||||
public final StyleableBooleanProperty disableAnimationProperty() {
|
||||
return this.disableAnimation;
|
||||
}
|
||||
|
||||
public final Boolean isDisableAnimation() {
|
||||
return disableAnimation != null && this.disableAnimationProperty().get();
|
||||
}
|
||||
|
||||
public final void setDisableAnimation(final Boolean disabled) {
|
||||
this.disableAnimationProperty().set(disabled);
|
||||
}
|
||||
|
||||
private final static class StyleableProperties {
|
||||
private static final CssMetaData<JFXTextArea, Paint> UNFOCUS_COLOR = new CssMetaData<JFXTextArea, Paint>("-jfx-unfocus-color", PaintConverter.getInstance(), Color.rgb(77, 77, 77)) {
|
||||
@Override
|
||||
public boolean isSettable(JFXTextArea control) {
|
||||
return control.unFocusColor == null || !control.unFocusColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(JFXTextArea control) {
|
||||
return control.unFocusColorProperty();
|
||||
}
|
||||
};
|
||||
private static final CssMetaData<JFXTextArea, Paint> FOCUS_COLOR = new CssMetaData<JFXTextArea, Paint>("-jfx-focus-color", PaintConverter.getInstance(), Color.valueOf("#4059A9")) {
|
||||
@Override
|
||||
public boolean isSettable(JFXTextArea control) {
|
||||
return control.focusColor == null || !control.focusColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(JFXTextArea control) {
|
||||
return control.focusColorProperty();
|
||||
}
|
||||
};
|
||||
private static final CssMetaData<JFXTextArea, Boolean> LABEL_FLOAT = new CssMetaData<JFXTextArea, Boolean>("-jfx-label-float", BooleanConverter.getInstance(), false) {
|
||||
@Override
|
||||
public boolean isSettable(JFXTextArea control) {
|
||||
return control.labelFloat == null || !control.labelFloat.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableBooleanProperty getStyleableProperty(JFXTextArea control) {
|
||||
return control.labelFloatProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final CssMetaData<JFXTextArea, Boolean> DISABLE_ANIMATION = new CssMetaData<JFXTextArea, Boolean>("-jfx-disable-animation", BooleanConverter.getInstance(), false) {
|
||||
@Override
|
||||
public boolean isSettable(JFXTextArea control) {
|
||||
return control.disableAnimation == null || !control.disableAnimation.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableBooleanProperty getStyleableProperty(JFXTextArea control) {
|
||||
return control.disableAnimationProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
||||
|
||||
static {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
||||
Collections.addAll(styleables, UNFOCUS_COLOR, FOCUS_COLOR, LABEL_FLOAT, DISABLE_ANIMATION);
|
||||
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
}
|
||||
|
||||
// inherit the styleable properties from parent
|
||||
private List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
||||
if (STYLEABLES == null) {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
||||
styleables.addAll(getClassCssMetaData());
|
||||
styleables.addAll(TextArea.getClassCssMetaData());
|
||||
STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
return STYLEABLES;
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.CHILD_STYLEABLES;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.skins.JFXTextFieldSkin;
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.*;
|
||||
import javafx.css.converter.BooleanConverter;
|
||||
import javafx.css.converter.PaintConverter;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu;
|
||||
|
||||
/**
|
||||
* JFXTextField is the material design implementation of a text Field.
|
||||
*
|
||||
* @author Shadi Shaheen
|
||||
* @version 1.0
|
||||
* @since 2016-03-09
|
||||
*/
|
||||
public class JFXTextField extends TextField {
|
||||
/**
|
||||
* Initialize the style class to 'jfx-text-field'.
|
||||
* <p>
|
||||
* This is the selector class from which CSS can be used to style
|
||||
* this control.
|
||||
*/
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-text-field";
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public JFXTextField() {
|
||||
initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public JFXTextField(String text) {
|
||||
super(text);
|
||||
initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new JFXTextFieldSkin(this);
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
if ("dalvik".equalsIgnoreCase(System.getProperty("java.vm.name"))) {
|
||||
this.setStyle("-fx-skin: \"com.jfoenix.android.skins.JFXTextFieldSkinAndroid\";");
|
||||
}
|
||||
|
||||
useJFXContextMenu(this);
|
||||
}
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/5822
|
||||
// TODO: This method may no longer be needed after we update JFXTextFieldSkin
|
||||
@Override
|
||||
protected void layoutChildren() {
|
||||
super.layoutChildren();
|
||||
this.setNeedsLayout(false);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Properties *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/**
|
||||
* holds the current active validator on the text field in case of validation error
|
||||
*/
|
||||
private ReadOnlyObjectWrapper<ValidatorBase> activeValidator = new ReadOnlyObjectWrapper<>();
|
||||
|
||||
public ValidatorBase getActiveValidator() {
|
||||
return activeValidator == null ? null : activeValidator.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<ValidatorBase> activeValidatorProperty() {
|
||||
return this.activeValidator.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
/**
|
||||
* list of validators that will validate the text value upon calling
|
||||
* {{@link #validate()}
|
||||
*/
|
||||
private ObservableList<ValidatorBase> validators = FXCollections.observableArrayList();
|
||||
|
||||
public ObservableList<ValidatorBase> getValidators() {
|
||||
return validators;
|
||||
}
|
||||
|
||||
public void setValidators(ValidatorBase... validators) {
|
||||
this.validators.addAll(validators);
|
||||
}
|
||||
|
||||
/**
|
||||
* validates the text value using the list of validators provided by the user
|
||||
* {{@link #setValidators(ValidatorBase...)}
|
||||
*
|
||||
* @return true if the value is valid else false
|
||||
*/
|
||||
public boolean validate() {
|
||||
for (ValidatorBase validator : validators) {
|
||||
if (validator.getSrcControl() == null) {
|
||||
validator.setSrcControl(this);
|
||||
}
|
||||
validator.validate();
|
||||
if (validator.getHasErrors()) {
|
||||
activeValidator.set(validator);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
activeValidator.set(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void resetValidation() {
|
||||
pseudoClassStateChanged(ValidatorBase.PSEUDO_CLASS_ERROR, false);
|
||||
activeValidator.set(null);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* styleable Properties *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/**
|
||||
* set true to show a float the prompt text when focusing the field
|
||||
*/
|
||||
private StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT, JFXTextField.this, "lableFloat", false);
|
||||
|
||||
public final StyleableBooleanProperty labelFloatProperty() {
|
||||
return this.labelFloat;
|
||||
}
|
||||
|
||||
public final boolean isLabelFloat() {
|
||||
return this.labelFloatProperty().get();
|
||||
}
|
||||
|
||||
public final void setLabelFloat(final boolean labelFloat) {
|
||||
this.labelFloatProperty().set(labelFloat);
|
||||
}
|
||||
|
||||
/**
|
||||
* default color used when the field is unfocused
|
||||
*/
|
||||
private StyleableObjectProperty<Paint> unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR, JFXTextField.this, "unFocusColor", Color.rgb(77, 77, 77));
|
||||
|
||||
public Paint getUnFocusColor() {
|
||||
return unFocusColor == null ? Color.rgb(77, 77, 77) : unFocusColor.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> unFocusColorProperty() {
|
||||
return this.unFocusColor;
|
||||
}
|
||||
|
||||
public void setUnFocusColor(Paint color) {
|
||||
this.unFocusColor.set(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* default color used when the field is focused
|
||||
*/
|
||||
private StyleableObjectProperty<Paint> focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR, JFXTextField.this, "focusColor", Color.valueOf("#4059A9"));
|
||||
|
||||
public Paint getFocusColor() {
|
||||
return focusColor == null ? Color.valueOf("#4059A9") : focusColor.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> focusColorProperty() {
|
||||
return this.focusColor;
|
||||
}
|
||||
|
||||
public void setFocusColor(Paint color) {
|
||||
this.focusColor.set(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* disable animation on validation
|
||||
*/
|
||||
private StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION, JFXTextField.this, "disableAnimation", false);
|
||||
|
||||
public final StyleableBooleanProperty disableAnimationProperty() {
|
||||
return this.disableAnimation;
|
||||
}
|
||||
|
||||
public final Boolean isDisableAnimation() {
|
||||
return disableAnimation != null && this.disableAnimationProperty().get();
|
||||
}
|
||||
|
||||
public final void setDisableAnimation(final Boolean disabled) {
|
||||
this.disableAnimationProperty().set(disabled);
|
||||
}
|
||||
|
||||
private final static class StyleableProperties {
|
||||
private static final CssMetaData<JFXTextField, Paint> UNFOCUS_COLOR = new CssMetaData<JFXTextField, Paint>("-jfx-unfocus-color", PaintConverter.getInstance(), Color.valueOf("#A6A6A6")) {
|
||||
@Override
|
||||
public boolean isSettable(JFXTextField control) {
|
||||
return control.unFocusColor == null || !control.unFocusColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(JFXTextField control) {
|
||||
return control.unFocusColorProperty();
|
||||
}
|
||||
};
|
||||
private static final CssMetaData<JFXTextField, Paint> FOCUS_COLOR = new CssMetaData<JFXTextField, Paint>("-jfx-focus-color", PaintConverter.getInstance(), Color.valueOf("#3f51b5")) {
|
||||
@Override
|
||||
public boolean isSettable(JFXTextField control) {
|
||||
return control.focusColor == null || !control.focusColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(JFXTextField control) {
|
||||
return control.focusColorProperty();
|
||||
}
|
||||
};
|
||||
private static final CssMetaData<JFXTextField, Boolean> LABEL_FLOAT = new CssMetaData<JFXTextField, Boolean>("-jfx-label-float", BooleanConverter.getInstance(), false) {
|
||||
@Override
|
||||
public boolean isSettable(JFXTextField control) {
|
||||
return control.labelFloat == null || !control.labelFloat.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableBooleanProperty getStyleableProperty(JFXTextField control) {
|
||||
return control.labelFloatProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final CssMetaData<JFXTextField, Boolean> DISABLE_ANIMATION = new CssMetaData<JFXTextField, Boolean>("-jfx-disable-animation", BooleanConverter.getInstance(), false) {
|
||||
@Override
|
||||
public boolean isSettable(JFXTextField control) {
|
||||
return control.disableAnimation == null || !control.disableAnimation.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableBooleanProperty getStyleableProperty(JFXTextField control) {
|
||||
return control.disableAnimationProperty();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
||||
|
||||
static {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
||||
Collections.addAll(styleables, UNFOCUS_COLOR, FOCUS_COLOR, LABEL_FLOAT, DISABLE_ANIMATION);
|
||||
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
}
|
||||
|
||||
// inherit the styleable properties from parent
|
||||
private List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
||||
if (STYLEABLES == null) {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
||||
styleables.addAll(getClassCssMetaData());
|
||||
styleables.addAll(TextField.getClassCssMetaData());
|
||||
STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
return STYLEABLES;
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.CHILD_STYLEABLES;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.skins.JFXToggleButtonSkin;
|
||||
import javafx.css.*;
|
||||
import javafx.css.converter.BooleanConverter;
|
||||
import javafx.css.converter.PaintConverter;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.ToggleButton;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JFXToggleButton is the material design implementation of a toggle button.
|
||||
* important CSS Selectors:
|
||||
* <p>
|
||||
* .jfx-toggle-button{
|
||||
* -fx-toggle-color: color-value;
|
||||
* -fx-untoggle-color: color-value;
|
||||
* -fx-toggle-line-color: color-value;
|
||||
* -fx-untoggle-line-color: color-value;
|
||||
* }
|
||||
* <p>
|
||||
* To change the rippler color when toggled:
|
||||
* <p>
|
||||
* .jfx-toggle-button .jfx-rippler{
|
||||
* -fx-rippler-fill: color-value;
|
||||
* }
|
||||
* <p>
|
||||
* .jfx-toggle-button:selected .jfx-rippler{
|
||||
* -fx-rippler-fill: color-value;
|
||||
* }
|
||||
*
|
||||
* @author Shadi Shaheen
|
||||
* @version 1.0
|
||||
* @since 2016-03-09
|
||||
*/
|
||||
public class JFXToggleButton extends ToggleButton {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public JFXToggleButton() {
|
||||
initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new JFXToggleButtonSkin(this);
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
// it's up for the user to add this behavior
|
||||
// toggleColor.addListener((o, oldVal, newVal) -> {
|
||||
// // update line color in case not set by the user
|
||||
// if(newVal instanceof Color)
|
||||
// toggleLineColor.set(((Color)newVal).desaturate().desaturate().brighter());
|
||||
// });
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* styleable Properties *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/**
|
||||
* Initialize the style class to 'jfx-toggle-button'.
|
||||
* <p>
|
||||
* This is the selector class from which CSS can be used to style
|
||||
* this control.
|
||||
*/
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-toggle-button";
|
||||
|
||||
/**
|
||||
* default color used when the button is toggled
|
||||
*/
|
||||
private final StyleableObjectProperty<Paint> toggleColor = new SimpleStyleableObjectProperty<>(StyleableProperties.TOGGLE_COLOR,
|
||||
JFXToggleButton.this,
|
||||
"toggleColor",
|
||||
Color.valueOf(
|
||||
"#009688"));
|
||||
|
||||
public Paint getToggleColor() {
|
||||
return toggleColor == null ? Color.valueOf("#009688") : toggleColor.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> toggleColorProperty() {
|
||||
return this.toggleColor;
|
||||
}
|
||||
|
||||
public void setToggleColor(Paint color) {
|
||||
this.toggleColor.set(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* default color used when the button is not toggled
|
||||
*/
|
||||
private StyleableObjectProperty<Paint> untoggleColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNTOGGLE_COLOR,
|
||||
JFXToggleButton.this,
|
||||
"unToggleColor",
|
||||
Color.valueOf(
|
||||
"#FAFAFA"));
|
||||
|
||||
public Paint getUnToggleColor() {
|
||||
return untoggleColor == null ? Color.valueOf("#FAFAFA") : untoggleColor.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> unToggleColorProperty() {
|
||||
return this.untoggleColor;
|
||||
}
|
||||
|
||||
public void setUnToggleColor(Paint color) {
|
||||
this.untoggleColor.set(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* default line color used when the button is toggled
|
||||
*/
|
||||
private final StyleableObjectProperty<Paint> toggleLineColor = new SimpleStyleableObjectProperty<>(
|
||||
StyleableProperties.TOGGLE_LINE_COLOR,
|
||||
JFXToggleButton.this,
|
||||
"toggleLineColor",
|
||||
Color.valueOf("#77C2BB"));
|
||||
|
||||
public Paint getToggleLineColor() {
|
||||
return toggleLineColor == null ? Color.valueOf("#77C2BB") : toggleLineColor.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> toggleLineColorProperty() {
|
||||
return this.toggleLineColor;
|
||||
}
|
||||
|
||||
public void setToggleLineColor(Paint color) {
|
||||
this.toggleLineColor.set(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* default line color used when the button is not toggled
|
||||
*/
|
||||
private final StyleableObjectProperty<Paint> untoggleLineColor = new SimpleStyleableObjectProperty<>(
|
||||
StyleableProperties.UNTOGGLE_LINE_COLOR,
|
||||
JFXToggleButton.this,
|
||||
"unToggleLineColor",
|
||||
Color.valueOf("#999999"));
|
||||
|
||||
public Paint getUnToggleLineColor() {
|
||||
return untoggleLineColor == null ? Color.valueOf("#999999") : untoggleLineColor.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> unToggleLineColorProperty() {
|
||||
return this.untoggleLineColor;
|
||||
}
|
||||
|
||||
public void setUnToggleLineColor(Paint color) {
|
||||
this.untoggleLineColor.set(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default size of the toggle button.
|
||||
*/
|
||||
private final StyleableDoubleProperty size = new SimpleStyleableDoubleProperty(
|
||||
StyleableProperties.SIZE,
|
||||
JFXToggleButton.this,
|
||||
"size",
|
||||
10.0);
|
||||
|
||||
public double getSize() {
|
||||
return size.get();
|
||||
}
|
||||
|
||||
public StyleableDoubleProperty sizeProperty() {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
public void setSize(double size) {
|
||||
this.size.set(size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the visual indicator for focus
|
||||
*/
|
||||
private final StyleableBooleanProperty disableVisualFocus = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_VISUAL_FOCUS,
|
||||
JFXToggleButton.this,
|
||||
"disableVisualFocus",
|
||||
false);
|
||||
|
||||
public final StyleableBooleanProperty disableVisualFocusProperty() {
|
||||
return this.disableVisualFocus;
|
||||
}
|
||||
|
||||
public final Boolean isDisableVisualFocus() {
|
||||
return disableVisualFocus != null && this.disableVisualFocusProperty().get();
|
||||
}
|
||||
|
||||
public final void setDisableVisualFocus(final Boolean disabled) {
|
||||
this.disableVisualFocusProperty().set(disabled);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* disable animation on button action
|
||||
*/
|
||||
private final StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION,
|
||||
JFXToggleButton.this,
|
||||
"disableAnimation",
|
||||
!AnimationUtils.isAnimationEnabled());
|
||||
|
||||
public final StyleableBooleanProperty disableAnimationProperty() {
|
||||
return this.disableAnimation;
|
||||
}
|
||||
|
||||
public final Boolean isDisableAnimation() {
|
||||
return disableAnimation != null && this.disableAnimationProperty().get();
|
||||
}
|
||||
|
||||
public final void setDisableAnimation(final Boolean disabled) {
|
||||
this.disableAnimationProperty().set(disabled);
|
||||
}
|
||||
|
||||
private static final class StyleableProperties {
|
||||
private static final CssMetaData<JFXToggleButton, Paint> TOGGLE_COLOR =
|
||||
new CssMetaData<>("-jfx-toggle-color",
|
||||
PaintConverter.getInstance(), Color.valueOf("#009688")) {
|
||||
@Override
|
||||
public boolean isSettable(JFXToggleButton control) {
|
||||
return control.toggleColor == null || !control.toggleColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(JFXToggleButton control) {
|
||||
return control.toggleColorProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final CssMetaData<JFXToggleButton, Paint> UNTOGGLE_COLOR =
|
||||
new CssMetaData<>("-jfx-untoggle-color",
|
||||
PaintConverter.getInstance(), Color.valueOf("#FAFAFA")) {
|
||||
@Override
|
||||
public boolean isSettable(JFXToggleButton control) {
|
||||
return control.untoggleColor == null || !control.untoggleColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(JFXToggleButton control) {
|
||||
return control.unToggleColorProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final CssMetaData<JFXToggleButton, Paint> TOGGLE_LINE_COLOR =
|
||||
new CssMetaData<>("-jfx-toggle-line-color",
|
||||
PaintConverter.getInstance(), Color.valueOf("#77C2BB")) {
|
||||
@Override
|
||||
public boolean isSettable(JFXToggleButton control) {
|
||||
return control.toggleLineColor == null || !control.toggleLineColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(JFXToggleButton control) {
|
||||
return control.toggleLineColorProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final CssMetaData<JFXToggleButton, Paint> UNTOGGLE_LINE_COLOR =
|
||||
new CssMetaData<>("-jfx-untoggle-line-color",
|
||||
PaintConverter.getInstance(), Color.valueOf("#999999")) {
|
||||
@Override
|
||||
public boolean isSettable(JFXToggleButton control) {
|
||||
return control.untoggleLineColor == null || !control.untoggleLineColor.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(JFXToggleButton control) {
|
||||
return control.unToggleLineColorProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final CssMetaData<JFXToggleButton, Number> SIZE =
|
||||
new CssMetaData<>("-jfx-size",
|
||||
StyleConverter.getSizeConverter(), 10.0) {
|
||||
@Override
|
||||
public boolean isSettable(JFXToggleButton control) {
|
||||
return !control.size.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Number> getStyleableProperty(JFXToggleButton control) {
|
||||
return control.sizeProperty();
|
||||
}
|
||||
};
|
||||
private static final CssMetaData<JFXToggleButton, Boolean> DISABLE_VISUAL_FOCUS =
|
||||
new CssMetaData<>("-jfx-disable-visual-focus",
|
||||
BooleanConverter.getInstance(), false) {
|
||||
@Override
|
||||
public boolean isSettable(JFXToggleButton control) {
|
||||
return control.disableVisualFocus == null || !control.disableVisualFocus.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableBooleanProperty getStyleableProperty(JFXToggleButton control) {
|
||||
return control.disableVisualFocusProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final CssMetaData<JFXToggleButton, Boolean> DISABLE_ANIMATION =
|
||||
new CssMetaData<>("-jfx-disable-animation",
|
||||
BooleanConverter.getInstance(), false) {
|
||||
@Override
|
||||
public boolean isSettable(JFXToggleButton control) {
|
||||
return control.disableAnimation == null || !control.disableAnimation.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableBooleanProperty getStyleableProperty(JFXToggleButton control) {
|
||||
return control.disableAnimationProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
||||
|
||||
static {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables =
|
||||
new ArrayList<>(ToggleButton.getClassCssMetaData());
|
||||
Collections.addAll(styleables,
|
||||
SIZE,
|
||||
TOGGLE_COLOR,
|
||||
UNTOGGLE_COLOR,
|
||||
TOGGLE_LINE_COLOR,
|
||||
UNTOGGLE_LINE_COLOR,
|
||||
DISABLE_VISUAL_FOCUS,
|
||||
DISABLE_ANIMATION
|
||||
);
|
||||
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
||||
return getClassCssMetaData();
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.CHILD_STYLEABLES;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.utils.JFXNodeUtils;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.TreeCell;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/// JFXTreeCell is simple material design implementation of a tree cell.
|
||||
///
|
||||
/// @author Shadi Shaheen
|
||||
/// @version 1.0
|
||||
/// @since 2017-02-15
|
||||
public class JFXTreeCell<T> extends TreeCell<T> {
|
||||
|
||||
protected JFXRippler cellRippler = new JFXRippler(this) {
|
||||
@Override
|
||||
protected Node getMask() {
|
||||
Region clip = new Region();
|
||||
JFXNodeUtils.updateBackground(JFXTreeCell.this.getBackground(), clip);
|
||||
double width = control.getLayoutBounds().getWidth();
|
||||
double height = control.getLayoutBounds().getHeight();
|
||||
clip.resize(width, height);
|
||||
return clip;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void positionControl(Node control) {
|
||||
// do nothing
|
||||
}
|
||||
};
|
||||
private HBox hbox;
|
||||
private final StackPane selectedPane = new StackPane();
|
||||
|
||||
private final InvalidationListener treeItemGraphicInvalidationListener = observable -> updateDisplay(getItem(),
|
||||
isEmpty());
|
||||
private final WeakInvalidationListener weakTreeItemGraphicListener = new WeakInvalidationListener(
|
||||
treeItemGraphicInvalidationListener);
|
||||
|
||||
private WeakReference<TreeItem<T>> treeItemRef;
|
||||
|
||||
public JFXTreeCell() {
|
||||
selectedPane.getStyleClass().add("selection-bar");
|
||||
selectedPane.setBackground(new Background(new BackgroundFill(Color.RED, CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
selectedPane.setPrefWidth(3);
|
||||
selectedPane.setMouseTransparent(true);
|
||||
selectedProperty().addListener((o, oldVal, newVal) -> selectedPane.setVisible(newVal ? true : false));
|
||||
|
||||
final InvalidationListener treeItemInvalidationListener = observable -> {
|
||||
TreeItem<T> oldTreeItem = treeItemRef == null ? null : treeItemRef.get();
|
||||
if (oldTreeItem != null) {
|
||||
oldTreeItem.graphicProperty().removeListener(weakTreeItemGraphicListener);
|
||||
}
|
||||
|
||||
TreeItem<T> newTreeItem = getTreeItem();
|
||||
if (newTreeItem != null) {
|
||||
newTreeItem.graphicProperty().addListener(weakTreeItemGraphicListener);
|
||||
treeItemRef = new WeakReference<>(newTreeItem);
|
||||
}
|
||||
};
|
||||
final WeakInvalidationListener weakTreeItemListener = new WeakInvalidationListener(treeItemInvalidationListener);
|
||||
treeItemProperty().addListener(weakTreeItemListener);
|
||||
if (getTreeItem() != null) {
|
||||
getTreeItem().graphicProperty().addListener(weakTreeItemGraphicListener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren() {
|
||||
super.layoutChildren();
|
||||
if (!getChildren().contains(selectedPane)) {
|
||||
getChildren().add(0, cellRippler);
|
||||
cellRippler.rippler.clear();
|
||||
getChildren().add(0, selectedPane);
|
||||
}
|
||||
cellRippler.resizeRelocate(0, 0, getWidth(), getHeight());
|
||||
cellRippler.releaseRipple();
|
||||
selectedPane.resizeRelocate(0, 0, selectedPane.prefWidth(-1), getHeight());
|
||||
selectedPane.setVisible(isSelected());
|
||||
}
|
||||
|
||||
private void updateDisplay(T item, boolean empty) {
|
||||
if (item == null || empty) {
|
||||
hbox = null;
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
TreeItem<T> treeItem = getTreeItem();
|
||||
if (treeItem != null && treeItem.getGraphic() != null) {
|
||||
if (item instanceof Node) {
|
||||
setText(null);
|
||||
if (hbox == null) {
|
||||
hbox = new HBox(3);
|
||||
}
|
||||
hbox.getChildren().setAll(treeItem.getGraphic(), (Node) item);
|
||||
setGraphic(hbox);
|
||||
} else {
|
||||
hbox = null;
|
||||
setText(item.toString());
|
||||
setGraphic(treeItem.getGraphic());
|
||||
}
|
||||
} else {
|
||||
hbox = null;
|
||||
if (item instanceof Node) {
|
||||
setText(null);
|
||||
setGraphic((Node) item);
|
||||
} else {
|
||||
setText(item.toString());
|
||||
setGraphic(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
updateDisplay(item, empty);
|
||||
setMouseTransparent(item == null || empty);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import javafx.scene.control.TreeItem;
|
||||
import javafx.scene.control.TreeView;
|
||||
|
||||
/// JFXTreeView is the material design implementation of a TreeView
|
||||
/// with expand/collapse animation and selection indicator.
|
||||
///
|
||||
/// @author Shadi Shaheen
|
||||
/// @version 1.0
|
||||
/// @since 2017-02-15
|
||||
public class JFXTreeView<T> extends TreeView<T> {
|
||||
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-tree-view";
|
||||
|
||||
public JFXTreeView() {
|
||||
init();
|
||||
}
|
||||
|
||||
public JFXTreeView(TreeItem<T> root) {
|
||||
super(root);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
this.setCellFactory((view) -> new JFXTreeCell<>());
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls.behavior;
|
||||
|
||||
import com.sun.javafx.scene.control.behavior.ComboBoxBaseBehavior;
|
||||
import javafx.scene.control.ComboBoxBase;
|
||||
import javafx.scene.control.PopupControl;
|
||||
|
||||
/**
|
||||
* @author Shadi Shaheen
|
||||
* @version 2.0
|
||||
* @since 2017-10-05
|
||||
*/
|
||||
public class JFXGenericPickerBehavior<T> extends ComboBoxBaseBehavior<T> {
|
||||
|
||||
public JFXGenericPickerBehavior(ComboBoxBase<T> var1) {
|
||||
super(var1);
|
||||
}
|
||||
|
||||
public void onAutoHide(PopupControl var1) {
|
||||
if (!var1.isShowing() && this.getNode().isShowing()) {
|
||||
this.getNode().hide();
|
||||
}
|
||||
if (!this.getNode().isShowing()) {
|
||||
super.onAutoHide(var1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls.datamodels.treetable;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.control.TreeTableColumn;
|
||||
|
||||
/// data model that is used in JFXTreeTableView, it's used to implement
|
||||
/// the grouping feature.
|
||||
///
|
||||
/// **Note:** the data object used in JFXTreeTableView **must** extends this class
|
||||
///
|
||||
/// @param <T> is the concrete object of the Tree table
|
||||
/// @author Shadi Shaheen
|
||||
/// @version 1.0
|
||||
/// @since 2016-03-09
|
||||
public class RecursiveTreeObject<T> {
|
||||
|
||||
/// grouped children objects
|
||||
private ObservableList<T> children = FXCollections.observableArrayList();
|
||||
|
||||
public ObservableList<T> getChildren() {
|
||||
return children;
|
||||
}
|
||||
|
||||
public void setChildren(ObservableList<T> children) {
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
/// Whether or not the object is grouped by a specified tree table column
|
||||
ObjectProperty<TreeTableColumn<T, ?>> groupedColumn = new SimpleObjectProperty<>();
|
||||
|
||||
public final ObjectProperty<TreeTableColumn<T, ?>> groupedColumnProperty() {
|
||||
return this.groupedColumn;
|
||||
}
|
||||
|
||||
public final TreeTableColumn<T, ?> getGroupedColumn() {
|
||||
return this.groupedColumnProperty().get();
|
||||
}
|
||||
|
||||
public final void setGroupedColumn(final TreeTableColumn<T, ?> groupedColumn) {
|
||||
this.groupedColumnProperty().set(groupedColumn);
|
||||
}
|
||||
|
||||
/// the value that must be shown when grouped
|
||||
ObjectProperty<Object> groupedValue = new SimpleObjectProperty<>();
|
||||
|
||||
public final ObjectProperty<Object> groupedValueProperty() {
|
||||
return this.groupedValue;
|
||||
}
|
||||
|
||||
public final Object getGroupedValue() {
|
||||
return this.groupedValueProperty().get();
|
||||
}
|
||||
|
||||
public final void setGroupedValue(final Object groupedValue) {
|
||||
this.groupedValueProperty().set(groupedValue);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls.events;
|
||||
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventType;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
/// JFXDialog events, used exclusively by the following methods:
|
||||
///
|
||||
/// - [com.jfoenix.controls.JFXDialog#show()]
|
||||
/// - [com.jfoenix.controls.JFXDialog#close()]
|
||||
///
|
||||
/// @author Shadi Shaheen
|
||||
/// @version 1.0
|
||||
/// @since 2016-03-09
|
||||
public class JFXDialogEvent extends Event {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/// This event occurs when a JFXDialog is closed, no longer visible to the user
|
||||
/// ( after the exit animation ends )
|
||||
public static final EventType<JFXDialogEvent> CLOSED = new EventType<>(Event.ANY, "JFX_DIALOG_CLOSED");
|
||||
|
||||
/// This event occurs when a JFXDialog is opened, visible to the user
|
||||
/// ( after the entrance animation ends )
|
||||
public static final EventType<JFXDialogEvent> OPENED = new EventType<>(Event.ANY, "JFX_DIALOG_OPENED");
|
||||
|
||||
/// Construct a new JFXDialog `Event` with the specified event type
|
||||
///
|
||||
/// @param eventType the event type
|
||||
public JFXDialogEvent(EventType<? extends Event> eventType) {
|
||||
super(eventType);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright (c) 2016 JFoenix
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package com.jfoenix.effects;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.effect.BlurType;
|
||||
import javafx.scene.effect.DropShadow;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
/**
|
||||
* it will create a shadow effect for a given node and a specified depth level.
|
||||
* depth levels are {0,1,2,3,4,5}
|
||||
*
|
||||
* @author Shadi Shaheen
|
||||
* @version 1.0
|
||||
* @since 2016-03-09
|
||||
*/
|
||||
public final class JFXDepthManager {
|
||||
private JFXDepthManager() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
private static final DropShadow[] depth = new DropShadow[] {
|
||||
new DropShadow(BlurType.GAUSSIAN, Color.TRANSPARENT, 0, 0, 0, 0),
|
||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.12), 4, 0, 0, 0),
|
||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.16), 8, 0, 0, 4),
|
||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.20), 12, 0, 0, 4),
|
||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.24), 16, 0, 0, 4),
|
||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.28), 20, 0, 0, 8)
|
||||
};
|
||||
|
||||
/**
|
||||
* this method is used to add shadow effect to the node,
|
||||
* however the shadow is not real ( gets affected with node transformations)
|
||||
* <p>
|
||||
* use {@link #createMaterialNode(Node, int)} instead to generate a real shadow
|
||||
*/
|
||||
public static void setDepth(Node control, int level) {
|
||||
level = level < 0 ? 0 : level;
|
||||
level = level > 5 ? 5 : level;
|
||||
control.setEffect(new DropShadow(BlurType.GAUSSIAN,
|
||||
depth[level].getColor(),
|
||||
depth[level].getRadius(),
|
||||
depth[level].getSpread(),
|
||||
depth[level].getOffsetX(),
|
||||
depth[level].getOffsetY()));
|
||||
}
|
||||
|
||||
public static int getLevels() {
|
||||
return depth.length;
|
||||
}
|
||||
|
||||
public static DropShadow getShadowAt(int level) {
|
||||
return depth[level];
|
||||
}
|
||||
|
||||
/**
|
||||
* this method will generate a new container node that prevent
|
||||
* control transformation to be applied to the shadow effect
|
||||
* (which makes it looks as a real shadow)
|
||||
*/
|
||||
public static Node createMaterialNode(Node control, int level) {
|
||||
Node container = new Pane(control) {
|
||||
@Override
|
||||
protected double computeMaxWidth(double height) {
|
||||
return computePrefWidth(height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computeMaxHeight(double width) {
|
||||
return computePrefHeight(width);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefWidth(double height) {
|
||||
return control.prefWidth(height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefHeight(double width) {
|
||||
return control.prefHeight(width);
|
||||
}
|
||||
};
|
||||
container.getStyleClass().add("depth-container");
|
||||
container.setPickOnBounds(false);
|
||||
level = level < 0 ? 0 : level;
|
||||
level = level > 5 ? 5 : level;
|
||||
container.setEffect(new DropShadow(BlurType.GAUSSIAN,
|
||||
depth[level].getColor(),
|
||||
depth[level].getRadius(),
|
||||
depth[level].getSpread(),
|
||||
depth[level].getOffsetX(),
|
||||
depth[level].getOffsetY()));
|
||||
return container;
|
||||
}
|
||||
|
||||
public static void pop(Node control) {
|
||||
control.setEffect(new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.26), 5, 0.05, 0, 1));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
//
|
||||
// Source code recreated from a .class file by IntelliJ IDEA
|
||||
// (powered by Fernflower decompiler)
|
||||
//
|
||||
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.adapters.skins.ButtonSkin;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXRippler;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import com.jfoenix.transitions.CachedTransition;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.animation.Transition;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.effect.DropShadow;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Shape;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public class JFXButtonSkin extends ButtonSkin {
|
||||
private final StackPane buttonContainer = new StackPane();
|
||||
private final JFXRippler buttonRippler = new JFXRippler(new StackPane()) {
|
||||
protected Node getMask() {
|
||||
StackPane mask = new StackPane();
|
||||
mask.shapeProperty().bind(JFXButtonSkin.this.buttonContainer.shapeProperty());
|
||||
mask.backgroundProperty().bind(Bindings.createObjectBinding(() -> new Background(
|
||||
new BackgroundFill(Color.WHITE,
|
||||
JFXButtonSkin.this.buttonContainer.backgroundProperty().get() != null && !JFXButtonSkin.this.buttonContainer.getBackground().getFills().isEmpty()
|
||||
? JFXButtonSkin.this.buttonContainer.getBackground().getFills().get(0).getRadii()
|
||||
: JFXButtonSkin.this.defaultRadii, JFXButtonSkin.this.buttonContainer.backgroundProperty().get() != null && !JFXButtonSkin.this.buttonContainer.getBackground().getFills().isEmpty() ? JFXButtonSkin.this.buttonContainer.getBackground().getFills().get(0).getInsets() : Insets.EMPTY)),
|
||||
JFXButtonSkin.this.buttonContainer.backgroundProperty()));
|
||||
mask.resize(JFXButtonSkin.this.buttonContainer.getWidth() - JFXButtonSkin.this.buttonContainer.snappedRightInset() - JFXButtonSkin.this.buttonContainer.snappedLeftInset(), JFXButtonSkin.this.buttonContainer.getHeight() - JFXButtonSkin.this.buttonContainer.snappedBottomInset() - JFXButtonSkin.this.buttonContainer.snappedTopInset());
|
||||
return mask;
|
||||
}
|
||||
|
||||
private void initListeners() {
|
||||
this.ripplerPane.setOnMousePressed((event) -> {
|
||||
if (JFXButtonSkin.this.releaseManualRippler != null) {
|
||||
JFXButtonSkin.this.releaseManualRippler.run();
|
||||
}
|
||||
|
||||
JFXButtonSkin.this.releaseManualRippler = null;
|
||||
this.createRipple(event.getX(), event.getY());
|
||||
});
|
||||
}
|
||||
};
|
||||
private Transition clickedAnimation;
|
||||
private final CornerRadii defaultRadii = new CornerRadii(3.0);
|
||||
private boolean invalid = true;
|
||||
private Runnable releaseManualRippler = null;
|
||||
|
||||
public JFXButtonSkin(JFXButton button) {
|
||||
super(button);
|
||||
this.getSkinnable().armedProperty().addListener((o, oldVal, newVal) -> {
|
||||
if (newVal) {
|
||||
this.releaseManualRippler = this.buttonRippler.createManualRipple();
|
||||
if (this.clickedAnimation != null) {
|
||||
this.clickedAnimation.setRate(1.0);
|
||||
this.clickedAnimation.play();
|
||||
}
|
||||
} else {
|
||||
if (this.releaseManualRippler != null) {
|
||||
this.releaseManualRippler.run();
|
||||
}
|
||||
|
||||
if (this.clickedAnimation != null) {
|
||||
this.clickedAnimation.setRate(-1.0);
|
||||
this.clickedAnimation.play();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
this.buttonContainer.getChildren().add(this.buttonRippler);
|
||||
button.buttonTypeProperty().addListener((o, oldVal, newVal) -> this.updateButtonType(newVal));
|
||||
button.setOnMousePressed((e) -> {
|
||||
if (this.clickedAnimation != null) {
|
||||
this.clickedAnimation.setRate(1.0F);
|
||||
this.clickedAnimation.play();
|
||||
}
|
||||
});
|
||||
button.setOnMouseReleased((e) -> {
|
||||
if (this.clickedAnimation != null) {
|
||||
this.clickedAnimation.setRate(-1.0F);
|
||||
this.clickedAnimation.play();
|
||||
}
|
||||
});
|
||||
|
||||
ReadOnlyBooleanProperty focusVisibleProperty = FXUtils.focusVisibleProperty(button);
|
||||
if (focusVisibleProperty == null) {
|
||||
focusVisibleProperty = button.focusedProperty();
|
||||
}
|
||||
focusVisibleProperty.addListener((o, oldVal, newVal) -> {
|
||||
if (newVal) {
|
||||
if (!this.getSkinnable().isPressed()) {
|
||||
this.buttonRippler.showOverlay();
|
||||
}
|
||||
} else {
|
||||
this.buttonRippler.hideOverlay();
|
||||
}
|
||||
});
|
||||
button.pressedProperty().addListener((o, oldVal, newVal) -> this.buttonRippler.hideOverlay());
|
||||
button.setPickOnBounds(false);
|
||||
this.buttonContainer.setPickOnBounds(false);
|
||||
this.buttonContainer.shapeProperty().bind(this.getSkinnable().shapeProperty());
|
||||
this.buttonContainer.borderProperty().bind(this.getSkinnable().borderProperty());
|
||||
this.buttonContainer.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
if (button.getBackground() == null || this.isJavaDefaultBackground(button.getBackground()) || this.isJavaDefaultClickedBackground(button.getBackground())) {
|
||||
button.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, this.defaultRadii, null)));
|
||||
}
|
||||
|
||||
try {
|
||||
return new Background(new BackgroundFill(this.getSkinnable().getBackground() != null ? this.getSkinnable().getBackground().getFills().get(0).getFill() : Color.TRANSPARENT, this.getSkinnable().getBackground() != null ? this.getSkinnable().getBackground().getFills().get(0).getRadii() : this.defaultRadii, Insets.EMPTY));
|
||||
} catch (Exception var3) {
|
||||
return this.getSkinnable().getBackground();
|
||||
}
|
||||
}, this.getSkinnable().backgroundProperty()));
|
||||
button.ripplerFillProperty().addListener((o, oldVal, newVal) -> this.buttonRippler.setRipplerFill(newVal));
|
||||
if (button.getBackground() == null || this.isJavaDefaultBackground(button.getBackground())) {
|
||||
button.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, this.defaultRadii, null)));
|
||||
}
|
||||
|
||||
this.updateButtonType(button.getButtonType());
|
||||
this.updateChildren();
|
||||
}
|
||||
|
||||
protected void updateChildren() {
|
||||
super.updateChildren();
|
||||
if (this.buttonContainer != null) {
|
||||
this.getChildren().add(0, this.buttonContainer);
|
||||
}
|
||||
|
||||
for (int i = 1; i < this.getChildren().size(); ++i) {
|
||||
this.getChildren().get(i).setMouseTransparent(true);
|
||||
}
|
||||
}
|
||||
|
||||
protected void layoutChildren(double x, double y, double w, double h) {
|
||||
if (this.invalid) {
|
||||
if (((JFXButton) this.getSkinnable()).getRipplerFill() == null) {
|
||||
for (int i = this.getChildren().size() - 1; i >= 1; --i) {
|
||||
if (this.getChildren().get(i) instanceof Shape shape) {
|
||||
this.buttonRippler.setRipplerFill(shape.getFill());
|
||||
shape.fillProperty().addListener((o, oldVal, newVal) -> this.buttonRippler.setRipplerFill(newVal));
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.getChildren().get(i) instanceof Label label) {
|
||||
this.buttonRippler.setRipplerFill(label.getTextFill());
|
||||
label.textFillProperty().addListener((o, oldVal, newVal) -> this.buttonRippler.setRipplerFill(newVal));
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.buttonRippler.setRipplerFill(((JFXButton) this.getSkinnable()).getRipplerFill());
|
||||
}
|
||||
|
||||
this.invalid = false;
|
||||
}
|
||||
|
||||
double shift = 1.0F;
|
||||
this.buttonContainer.resizeRelocate(this.getSkinnable().getLayoutBounds().getMinX() - shift, this.getSkinnable().getLayoutBounds().getMinY() - shift, this.getSkinnable().getWidth() + (double) 2.0F * shift, this.getSkinnable().getHeight() + (double) 2.0F * shift);
|
||||
this.layoutLabelInArea(x, y, w, h);
|
||||
}
|
||||
|
||||
private boolean isJavaDefaultBackground(Background background) {
|
||||
try {
|
||||
String firstFill = background.getFills().get(0).getFill().toString();
|
||||
return "0xffffffba".equals(firstFill) || "0xffffffbf".equals(firstFill) || "0xffffff12".equals(firstFill) || "0xffffffbd".equals(firstFill);
|
||||
} catch (Exception var3) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isJavaDefaultClickedBackground(Background background) {
|
||||
try {
|
||||
return "0x039ed3ff".equals(background.getFills().get(0).getFill().toString());
|
||||
} catch (Exception var3) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateButtonType(JFXButton.ButtonType type) {
|
||||
switch (type) {
|
||||
case RAISED -> {
|
||||
JFXDepthManager.setDepth(this.buttonContainer, 2);
|
||||
this.clickedAnimation = new ButtonClickTransition();
|
||||
}
|
||||
case FLAT -> this.buttonContainer.setEffect(null);
|
||||
}
|
||||
}
|
||||
|
||||
private class ButtonClickTransition extends CachedTransition {
|
||||
public ButtonClickTransition() {
|
||||
super(JFXButtonSkin.this.buttonContainer, new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).radiusProperty(), JFXDepthManager.getShadowAt(2).radiusProperty().get(), Interpolator.EASE_BOTH),
|
||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).spreadProperty(), JFXDepthManager.getShadowAt(2).spreadProperty().get(), Interpolator.EASE_BOTH),
|
||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).offsetXProperty(), JFXDepthManager.getShadowAt(2).offsetXProperty().get(), Interpolator.EASE_BOTH),
|
||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).offsetYProperty(), JFXDepthManager.getShadowAt(2).offsetYProperty().get(), Interpolator.EASE_BOTH)),
|
||||
new KeyFrame(Duration.millis(1000.0F),
|
||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).radiusProperty(), JFXDepthManager.getShadowAt(5).radiusProperty().get(), Interpolator.EASE_BOTH),
|
||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).spreadProperty(), JFXDepthManager.getShadowAt(5).spreadProperty().get(), Interpolator.EASE_BOTH),
|
||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).offsetXProperty(), JFXDepthManager.getShadowAt(5).offsetXProperty().get(), Interpolator.EASE_BOTH),
|
||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).offsetYProperty(), JFXDepthManager.getShadowAt(5).offsetYProperty().get(), Interpolator.EASE_BOTH))));
|
||||
this.setCycleDuration(Duration.seconds(0.2));
|
||||
this.setDelay(Duration.seconds(0.0F));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
//
|
||||
// Source code recreated from a .class file by IntelliJ IDEA
|
||||
// (powered by FernFlower decompiler)
|
||||
//
|
||||
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXRippler;
|
||||
import com.jfoenix.controls.JFXRippler.RipplerMask;
|
||||
import com.jfoenix.controls.JFXRippler.RipplerPos;
|
||||
import com.jfoenix.transitions.CachedTransition;
|
||||
import com.jfoenix.transitions.JFXFillTransition;
|
||||
import com.jfoenix.utils.JFXNodeUtils;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.animation.Transition;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.VPos;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.skin.CheckBoxSkin;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.Border;
|
||||
import javafx.scene.layout.BorderStroke;
|
||||
import javafx.scene.layout.BorderStrokeStyle;
|
||||
import javafx.scene.layout.BorderWidths;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public class JFXCheckBoxSkin extends CheckBoxSkin {
|
||||
private final StackPane box = new StackPane();
|
||||
private final StackPane mark = new StackPane();
|
||||
private final double lineThick = 2.0;
|
||||
private final double padding = 10.0;
|
||||
private final JFXRippler rippler;
|
||||
private final AnchorPane container = new AnchorPane();
|
||||
private final double labelOffset = -8.0;
|
||||
private final Transition transition;
|
||||
private boolean invalid = true;
|
||||
private JFXFillTransition select;
|
||||
|
||||
public JFXCheckBoxSkin(JFXCheckBox control) {
|
||||
super(control);
|
||||
this.box.setMinSize(18.0, 18.0);
|
||||
this.box.setPrefSize(18.0, 18.0);
|
||||
this.box.setMaxSize(18.0, 18.0);
|
||||
this.box.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, new CornerRadii(2.0), Insets.EMPTY)));
|
||||
this.box.setBorder(new Border(new BorderStroke(Themes.getColorScheme().getOnSurfaceVariant(), BorderStrokeStyle.SOLID, new CornerRadii(2.0), new BorderWidths(this.lineThick))));
|
||||
StackPane boxContainer = new StackPane();
|
||||
boxContainer.getChildren().add(this.box);
|
||||
boxContainer.setPadding(new Insets(this.padding));
|
||||
this.rippler = new JFXRippler(boxContainer, RipplerMask.CIRCLE, RipplerPos.BACK);
|
||||
this.updateRippleColor();
|
||||
SVGPath shape = new SVGPath();
|
||||
shape.setContent("M384 690l452-452 60 60-512 512-238-238 60-60z");
|
||||
this.mark.setShape(shape);
|
||||
this.mark.setMaxSize(15.0, 12.0);
|
||||
this.mark.setStyle("-fx-background-color:-monet-on-primary; -fx-border-color:-monet-on-primary; -fx-border-width:2px;");
|
||||
this.mark.setVisible(false);
|
||||
this.mark.setScaleX(0.0);
|
||||
this.mark.setScaleY(0.0);
|
||||
boxContainer.getChildren().add(this.mark);
|
||||
this.container.getChildren().add(this.rippler);
|
||||
AnchorPane.setRightAnchor(this.rippler, this.labelOffset);
|
||||
control.selectedProperty().addListener((o, oldVal, newVal) -> {
|
||||
this.updateRippleColor();
|
||||
this.playSelectAnimation(newVal);
|
||||
});
|
||||
|
||||
ReadOnlyBooleanProperty focusVisibleProperty = FXUtils.focusVisibleProperty(control);
|
||||
if (focusVisibleProperty == null)
|
||||
focusVisibleProperty = control.focusedProperty();
|
||||
focusVisibleProperty.addListener((o, oldVal, newVal) -> {
|
||||
if (newVal) {
|
||||
if (!this.getSkinnable().isPressed()) {
|
||||
this.rippler.showOverlay();
|
||||
}
|
||||
} else {
|
||||
this.rippler.hideOverlay();
|
||||
}
|
||||
});
|
||||
control.pressedProperty().addListener((o, oldVal, newVal) -> this.rippler.hideOverlay());
|
||||
this.updateChildren();
|
||||
this.registerChangeListener(control.checkedColorProperty(), ignored -> {
|
||||
if (select != null) {
|
||||
select.stop();
|
||||
}
|
||||
this.createFillTransition();
|
||||
updateColors();
|
||||
});
|
||||
this.registerChangeListener(control.unCheckedColorProperty(), ignored -> updateColors());
|
||||
this.transition = new CheckBoxTransition();
|
||||
this.createFillTransition();
|
||||
}
|
||||
|
||||
private void updateRippleColor() {
|
||||
var control = (JFXCheckBox) this.getSkinnable();
|
||||
this.rippler.setRipplerFill(control.isSelected()
|
||||
? control.getCheckedColor()
|
||||
: control.getUnCheckedColor());
|
||||
}
|
||||
|
||||
private void updateColors() {
|
||||
var control = (JFXCheckBox) getSkinnable();
|
||||
boolean isSelected = control.isSelected();
|
||||
JFXNodeUtils.updateBackground(box.getBackground(), box, isSelected ? control.getCheckedColor() : Color.TRANSPARENT);
|
||||
rippler.setRipplerFill(isSelected ? control.getCheckedColor() : control.getUnCheckedColor());
|
||||
final BorderStroke borderStroke = box.getBorder().getStrokes().get(0);
|
||||
box.setBorder(new Border(new BorderStroke(
|
||||
isSelected ? control.getCheckedColor() : Themes.getColorScheme().getOnSurfaceVariant(),
|
||||
borderStroke.getTopStyle(),
|
||||
borderStroke.getRadii(),
|
||||
borderStroke.getWidths())));
|
||||
}
|
||||
|
||||
protected void updateChildren() {
|
||||
super.updateChildren();
|
||||
if (this.container != null) {
|
||||
this.getChildren().remove(1);
|
||||
this.getChildren().add(this.container);
|
||||
}
|
||||
}
|
||||
|
||||
protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
return super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset) + this.snapSizeX(this.box.minWidth(-1.0)) + this.labelOffset + 2.0 * this.padding;
|
||||
}
|
||||
|
||||
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
return super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset) + this.snapSizeY(this.box.prefWidth(-1.0)) + this.labelOffset + 2.0 * this.padding;
|
||||
}
|
||||
|
||||
protected void layoutChildren(double x, double y, double w, double h) {
|
||||
CheckBox checkBox = this.getSkinnable();
|
||||
double boxWidth = this.snapSizeX(this.container.prefWidth(-1.0));
|
||||
double boxHeight = this.snapSizeY(this.container.prefHeight(-1.0));
|
||||
double computeWidth = Math.min(checkBox.prefWidth(-1.0), checkBox.minWidth(-1.0)) + this.labelOffset + 2.0 * this.padding;
|
||||
double labelWidth = Math.min(computeWidth - boxWidth, w - this.snapSizeX(boxWidth)) + this.labelOffset + 2.0 * this.padding;
|
||||
double labelHeight = Math.min(checkBox.prefHeight(labelWidth), h);
|
||||
double maxHeight = Math.max(boxHeight, labelHeight);
|
||||
double xOffset = computeXOffset(w, labelWidth + boxWidth, checkBox.getAlignment().getHpos()) + x;
|
||||
double yOffset = computeYOffset(h, maxHeight, checkBox.getAlignment().getVpos()) + x;
|
||||
if (this.invalid) {
|
||||
if (this.getSkinnable().isSelected()) {
|
||||
this.playSelectAnimation(true);
|
||||
}
|
||||
|
||||
this.invalid = false;
|
||||
}
|
||||
|
||||
this.layoutLabelInArea(xOffset + boxWidth, yOffset, labelWidth, maxHeight, checkBox.getAlignment());
|
||||
this.container.resize(boxWidth, boxHeight);
|
||||
this.positionInArea(this.container, xOffset, yOffset, boxWidth, maxHeight, 0.0, checkBox.getAlignment().getHpos(), checkBox.getAlignment().getVpos());
|
||||
}
|
||||
|
||||
static double computeXOffset(double width, double contentWidth, HPos hpos) {
|
||||
return switch (hpos) {
|
||||
case LEFT -> 0.0;
|
||||
case CENTER -> (width - contentWidth) / 2.0;
|
||||
case RIGHT -> width - contentWidth;
|
||||
};
|
||||
}
|
||||
|
||||
static double computeYOffset(double height, double contentHeight, VPos vpos) {
|
||||
return switch (vpos) {
|
||||
case TOP -> 0.0;
|
||||
case CENTER -> (height - contentHeight) / 2.0;
|
||||
case BOTTOM -> height - contentHeight;
|
||||
default -> 0.0;
|
||||
};
|
||||
}
|
||||
|
||||
private void playSelectAnimation(Boolean selection) {
|
||||
if (selection == null) {
|
||||
selection = false;
|
||||
}
|
||||
|
||||
JFXCheckBox control = (JFXCheckBox) this.getSkinnable();
|
||||
this.transition.setRate(selection ? 1.0 : -1.0);
|
||||
this.select.setRate(selection ? 1.0 : -1.0);
|
||||
this.transition.play();
|
||||
this.select.play();
|
||||
this.box.setBorder(new Border(new BorderStroke(
|
||||
selection ? control.getCheckedColor() : Themes.getColorScheme().getOnSurfaceVariant(),
|
||||
BorderStrokeStyle.SOLID,
|
||||
new CornerRadii(2.0),
|
||||
new BorderWidths(this.lineThick))));
|
||||
}
|
||||
|
||||
private void createFillTransition() {
|
||||
this.select = new JFXFillTransition(Duration.millis(120.0), this.box, Color.TRANSPARENT,
|
||||
(Color) ((JFXCheckBox) this.getSkinnable()).getCheckedColor());
|
||||
this.select.setInterpolator(Interpolator.EASE_OUT);
|
||||
}
|
||||
|
||||
private final class CheckBoxTransition extends CachedTransition {
|
||||
CheckBoxTransition() {
|
||||
super(JFXCheckBoxSkin.this.mark,
|
||||
new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(JFXCheckBoxSkin.this.mark.visibleProperty(), false, Interpolator.EASE_OUT),
|
||||
new KeyValue(JFXCheckBoxSkin.this.mark.scaleXProperty(), (double) 0.5F, Interpolator.EASE_OUT),
|
||||
new KeyValue(JFXCheckBoxSkin.this.mark.scaleYProperty(), (double) 0.5F, Interpolator.EASE_OUT)),
|
||||
new KeyFrame(Duration.millis(400.0),
|
||||
new KeyValue(JFXCheckBoxSkin.this.mark.visibleProperty(), true, Interpolator.EASE_OUT),
|
||||
new KeyValue(JFXCheckBoxSkin.this.mark.scaleXProperty(), (double) 0.5F, Interpolator.EASE_OUT),
|
||||
new KeyValue(JFXCheckBoxSkin.this.mark.scaleYProperty(), (double) 0.5F, Interpolator.EASE_OUT)),
|
||||
new KeyFrame(Duration.millis(1000.0),
|
||||
new KeyValue(JFXCheckBoxSkin.this.mark.visibleProperty(), true, Interpolator.EASE_OUT),
|
||||
new KeyValue(JFXCheckBoxSkin.this.mark.scaleXProperty(), 1, Interpolator.EASE_OUT),
|
||||
new KeyValue(JFXCheckBoxSkin.this.mark.scaleYProperty(), 1, Interpolator.EASE_OUT))
|
||||
)
|
||||
);
|
||||
this.setCycleDuration(Duration.seconds(0.12));
|
||||
this.setDelay(Duration.seconds(0.05));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,623 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXColorPicker;
|
||||
import com.jfoenix.utils.JFXNodeUtils;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener.Change;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.Event;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.geometry.NodeOrientation;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.PopupControl;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.shape.StrokeType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
/**
|
||||
* @author Shadi Shaheen FUTURE WORK: this UI will get re-designed to match material design guidlines
|
||||
*/
|
||||
final class JFXColorPalette extends Region {
|
||||
|
||||
private static final int SQUARE_SIZE = 15;
|
||||
|
||||
// package protected for testing purposes
|
||||
JFXColorGrid colorPickerGrid;
|
||||
final JFXButton customColorLink = new JFXButton(i18n("color.custom"));
|
||||
JFXCustomColorPickerDialog customColorDialog = null;
|
||||
|
||||
private final JFXColorPicker colorPicker;
|
||||
private final GridPane customColorGrid = new GridPane();
|
||||
private final Label customColorLabel = new Label(i18n("color.recent"));
|
||||
|
||||
private PopupControl popupControl;
|
||||
private ColorSquare focusedSquare;
|
||||
|
||||
private Color mouseDragColor = null;
|
||||
private boolean dragDetected = false;
|
||||
|
||||
private final ColorSquare hoverSquare = new ColorSquare();
|
||||
|
||||
public JFXColorPalette(final JFXColorPicker colorPicker) {
|
||||
getStyleClass().add("color-palette-region");
|
||||
this.colorPicker = colorPicker;
|
||||
colorPickerGrid = new JFXColorGrid();
|
||||
colorPickerGrid.getChildren().get(0).requestFocus();
|
||||
customColorLabel.setAlignment(Pos.CENTER_LEFT);
|
||||
customColorLink.setPrefWidth(colorPickerGrid.prefWidth(-1));
|
||||
customColorLink.setAlignment(Pos.CENTER);
|
||||
customColorLink.setFocusTraversable(true);
|
||||
customColorLink.setOnAction(ev -> {
|
||||
if (customColorDialog == null) {
|
||||
customColorDialog = new JFXCustomColorPickerDialog(popupControl);
|
||||
customColorDialog.customColorProperty().addListener((ov, t1, t2) -> {
|
||||
colorPicker.setValue(customColorDialog.customColorProperty().get());
|
||||
});
|
||||
customColorDialog.setOnSave(() -> {
|
||||
Color customColor = customColorDialog.customColorProperty().get();
|
||||
buildCustomColors();
|
||||
colorPicker.getCustomColors().add(customColor);
|
||||
updateSelection(customColor);
|
||||
Event.fireEvent(colorPicker, new ActionEvent());
|
||||
colorPicker.hide();
|
||||
});
|
||||
}
|
||||
customColorDialog.setCurrentColor(colorPicker.valueProperty().get());
|
||||
if (popupControl != null) {
|
||||
popupControl.setAutoHide(false);
|
||||
}
|
||||
customColorDialog.show();
|
||||
customColorDialog.setOnHidden(event -> {
|
||||
if (popupControl != null) {
|
||||
popupControl.setAutoHide(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
initNavigation();
|
||||
customColorGrid.getStyleClass().add("color-picker-grid");
|
||||
customColorGrid.setVisible(false);
|
||||
|
||||
buildCustomColors();
|
||||
|
||||
colorPicker.getCustomColors().addListener((Change<? extends Color> change) -> buildCustomColors());
|
||||
VBox paletteBox = new VBox();
|
||||
paletteBox.getStyleClass().add("color-palette");
|
||||
paletteBox.getChildren().addAll(colorPickerGrid);
|
||||
if (colorPicker.getPreDefinedColors() == null) {
|
||||
paletteBox.getChildren().addAll(customColorLabel, customColorGrid, customColorLink);
|
||||
}
|
||||
|
||||
hoverSquare.setMouseTransparent(true);
|
||||
hoverSquare.getStyleClass().addAll("hover-square");
|
||||
setFocusedSquare(null);
|
||||
|
||||
getChildren().addAll(paletteBox, hoverSquare);
|
||||
}
|
||||
|
||||
private void setFocusedSquare(ColorSquare square) {
|
||||
hoverSquare.setVisible(square != null);
|
||||
|
||||
if (square == focusedSquare) {
|
||||
return;
|
||||
}
|
||||
focusedSquare = square;
|
||||
|
||||
hoverSquare.setVisible(focusedSquare != null);
|
||||
if (focusedSquare == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!focusedSquare.isFocused()) {
|
||||
focusedSquare.requestFocus();
|
||||
}
|
||||
|
||||
hoverSquare.rectangle.setFill(focusedSquare.rectangle.getFill());
|
||||
|
||||
Bounds b = square.localToScene(square.getLayoutBounds());
|
||||
|
||||
double x = b.getMinX();
|
||||
double y = b.getMinY();
|
||||
|
||||
double xAdjust;
|
||||
double scaleAdjust = hoverSquare.getScaleX() == 1.0 ? 0 : hoverSquare.getWidth() / 4.0;
|
||||
|
||||
if (colorPicker.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) {
|
||||
x = focusedSquare.getLayoutX();
|
||||
xAdjust = -focusedSquare.getWidth() + scaleAdjust;
|
||||
} else {
|
||||
xAdjust = focusedSquare.getWidth() / 2.0 + scaleAdjust;
|
||||
}
|
||||
|
||||
hoverSquare.setLayoutX(snapPositionX(x) - xAdjust);
|
||||
hoverSquare.setLayoutY(snapPositionY(y) - focusedSquare.getHeight() / 2.0 + (hoverSquare.getScaleY() == 1.0 ? 0 : focusedSquare.getHeight() / 4.0));
|
||||
}
|
||||
|
||||
private void buildCustomColors() {
|
||||
final ObservableList<Color> customColors = colorPicker.getCustomColors();
|
||||
customColorGrid.getChildren().clear();
|
||||
if (customColors.isEmpty()) {
|
||||
customColorLabel.setVisible(false);
|
||||
customColorLabel.setManaged(false);
|
||||
customColorGrid.setVisible(false);
|
||||
customColorGrid.setManaged(false);
|
||||
return;
|
||||
} else {
|
||||
customColorLabel.setVisible(true);
|
||||
customColorLabel.setManaged(true);
|
||||
customColorGrid.setVisible(true);
|
||||
customColorGrid.setManaged(true);
|
||||
}
|
||||
|
||||
int customColumnIndex = 0;
|
||||
int customRowIndex = 0;
|
||||
int remainingSquares = customColors.size() % NUM_OF_COLUMNS;
|
||||
int numEmpty = (remainingSquares == 0) ? 0 : NUM_OF_COLUMNS - remainingSquares;
|
||||
|
||||
for (int i = 0; i < customColors.size(); i++) {
|
||||
Color c = customColors.get(i);
|
||||
ColorSquare square = new ColorSquare(c, i, true);
|
||||
customColorGrid.add(square, customColumnIndex, customRowIndex);
|
||||
customColumnIndex++;
|
||||
if (customColumnIndex == NUM_OF_COLUMNS) {
|
||||
customColumnIndex = 0;
|
||||
customRowIndex++;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < numEmpty; i++) {
|
||||
ColorSquare emptySquare = new ColorSquare();
|
||||
customColorGrid.add(emptySquare, customColumnIndex, customRowIndex);
|
||||
customColumnIndex++;
|
||||
}
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
private void initNavigation() {
|
||||
setOnKeyPressed(ke -> {
|
||||
switch (ke.getCode()) {
|
||||
case SPACE:
|
||||
case ENTER:
|
||||
// select the focused color
|
||||
if (focusedSquare != null) {
|
||||
focusedSquare.selectColor(ke);
|
||||
}
|
||||
ke.consume();
|
||||
break;
|
||||
default: // no-op
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setPopupControl(PopupControl pc) {
|
||||
this.popupControl = pc;
|
||||
}
|
||||
|
||||
public JFXColorGrid getColorGrid() {
|
||||
return colorPickerGrid;
|
||||
}
|
||||
|
||||
public boolean isCustomColorDialogShowing() {
|
||||
return customColorDialog != null && customColorDialog.isVisible();
|
||||
}
|
||||
|
||||
class ColorSquare extends StackPane {
|
||||
Rectangle rectangle;
|
||||
boolean isEmpty;
|
||||
|
||||
public ColorSquare() {
|
||||
this(null, -1, false);
|
||||
}
|
||||
|
||||
public ColorSquare(Color color, int index) {
|
||||
this(color, index, false);
|
||||
}
|
||||
|
||||
public ColorSquare(Color color, int index, boolean isCustom) {
|
||||
// Add style class to handle selected color square
|
||||
getStyleClass().add("color-square");
|
||||
if (color != null) {
|
||||
setFocusTraversable(true);
|
||||
focusedProperty().addListener((s, ov, nv) -> setFocusedSquare(nv ? this : null));
|
||||
addEventHandler(MouseEvent.MOUSE_ENTERED, event -> setFocusedSquare(ColorSquare.this));
|
||||
addEventHandler(MouseEvent.MOUSE_EXITED, event -> setFocusedSquare(null));
|
||||
addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
|
||||
if (!dragDetected && event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 1) {
|
||||
if (!isEmpty) {
|
||||
Color fill = (Color) rectangle.getFill();
|
||||
colorPicker.setValue(fill);
|
||||
colorPicker.fireEvent(new ActionEvent());
|
||||
updateSelection(fill);
|
||||
event.consume();
|
||||
}
|
||||
colorPicker.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
rectangle = new Rectangle(SQUARE_SIZE, SQUARE_SIZE);
|
||||
if (color == null) {
|
||||
rectangle.setFill(Color.WHITE);
|
||||
isEmpty = true;
|
||||
} else {
|
||||
rectangle.setFill(color);
|
||||
}
|
||||
|
||||
rectangle.setStrokeType(StrokeType.INSIDE);
|
||||
|
||||
String tooltipStr = JFXNodeUtils.colorToHex(color);
|
||||
Tooltip.install(this, new Tooltip((tooltipStr == null) ? "" : tooltipStr));
|
||||
|
||||
rectangle.getStyleClass().add("color-rect");
|
||||
getChildren().add(rectangle);
|
||||
}
|
||||
|
||||
public void selectColor(KeyEvent event) {
|
||||
if (rectangle.getFill() != null) {
|
||||
if (rectangle.getFill() instanceof Color) {
|
||||
colorPicker.setValue((Color) rectangle.getFill());
|
||||
colorPicker.fireEvent(new ActionEvent());
|
||||
}
|
||||
event.consume();
|
||||
}
|
||||
colorPicker.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// The skin can update selection if colorpicker value changes..
|
||||
public void updateSelection(Color color) {
|
||||
setFocusedSquare(null);
|
||||
|
||||
for (ColorSquare c : colorPickerGrid.getSquares()) {
|
||||
if (c.rectangle.getFill().equals(color)) {
|
||||
setFocusedSquare(c);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// check custom colors
|
||||
for (Node n : customColorGrid.getChildren()) {
|
||||
ColorSquare c = (ColorSquare) n;
|
||||
if (c.rectangle.getFill().equals(color)) {
|
||||
setFocusedSquare(c);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class JFXColorGrid extends GridPane {
|
||||
|
||||
private final List<ColorSquare> squares;
|
||||
final int NUM_OF_COLORS;
|
||||
final int NUM_OF_ROWS;
|
||||
|
||||
public JFXColorGrid() {
|
||||
getStyleClass().add("color-picker-grid");
|
||||
setId("ColorCustomizerColorGrid");
|
||||
int columnIndex = 0;
|
||||
int rowIndex = 0;
|
||||
squares = FXCollections.observableArrayList();
|
||||
double[] limitedColors = colorPicker.getPreDefinedColors();
|
||||
limitedColors = limitedColors == null ? RAW_VALUES : limitedColors;
|
||||
NUM_OF_COLORS = limitedColors.length / 3;
|
||||
NUM_OF_ROWS = (int) Math.ceil((double) NUM_OF_COLORS / (double) NUM_OF_COLUMNS);
|
||||
final int numColors = limitedColors.length / 3;
|
||||
Color[] colors = new Color[numColors];
|
||||
for (int i = 0; i < numColors; i++) {
|
||||
colors[i] = new Color(limitedColors[i * 3] / 255,
|
||||
limitedColors[(i * 3) + 1] / 255, limitedColors[(i * 3) + 2] / 255,
|
||||
1.0);
|
||||
ColorSquare cs = new ColorSquare(colors[i], i);
|
||||
squares.add(cs);
|
||||
}
|
||||
|
||||
for (ColorSquare square : squares) {
|
||||
add(square, columnIndex, rowIndex);
|
||||
columnIndex++;
|
||||
if (columnIndex == NUM_OF_COLUMNS) {
|
||||
columnIndex = 0;
|
||||
rowIndex++;
|
||||
}
|
||||
}
|
||||
setOnMouseDragged(t -> {
|
||||
if (!dragDetected) {
|
||||
dragDetected = true;
|
||||
mouseDragColor = colorPicker.getValue();
|
||||
}
|
||||
int xIndex = clamp(0,
|
||||
(int) t.getX() / (SQUARE_SIZE + 1), NUM_OF_COLUMNS - 1);
|
||||
int yIndex = clamp(0,
|
||||
(int) t.getY() / (SQUARE_SIZE + 1), NUM_OF_ROWS - 1);
|
||||
int index = xIndex + yIndex * NUM_OF_COLUMNS;
|
||||
colorPicker.setValue((Color) squares.get(index).rectangle.getFill());
|
||||
updateSelection(colorPicker.getValue());
|
||||
});
|
||||
addEventHandler(MouseEvent.MOUSE_RELEASED, t -> {
|
||||
if (colorPickerGrid.getBoundsInLocal().contains(t.getX(), t.getY())) {
|
||||
updateSelection(colorPicker.getValue());
|
||||
colorPicker.fireEvent(new ActionEvent());
|
||||
colorPicker.hide();
|
||||
} else {
|
||||
// restore color as mouse release happened outside the grid.
|
||||
if (mouseDragColor != null) {
|
||||
colorPicker.setValue(mouseDragColor);
|
||||
updateSelection(mouseDragColor);
|
||||
}
|
||||
}
|
||||
dragDetected = false;
|
||||
});
|
||||
}
|
||||
|
||||
public List<ColorSquare> getSquares() {
|
||||
return squares;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefWidth(double height) {
|
||||
return (SQUARE_SIZE + 1) * NUM_OF_COLUMNS;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefHeight(double width) {
|
||||
return (SQUARE_SIZE + 1) * NUM_OF_ROWS;
|
||||
}
|
||||
}
|
||||
|
||||
private static final int NUM_OF_COLUMNS = 10;
|
||||
private static final double[] RAW_VALUES = {
|
||||
// WARNING: always make sure the number of colors is a divisable by NUM_OF_COLUMNS
|
||||
250, 250, 250, // first row
|
||||
245, 245, 245,
|
||||
238, 238, 238,
|
||||
224, 224, 224,
|
||||
189, 189, 189,
|
||||
158, 158, 158,
|
||||
117, 117, 117,
|
||||
97, 97, 97,
|
||||
66, 66, 66,
|
||||
33, 33, 33,
|
||||
// second row
|
||||
236, 239, 241,
|
||||
207, 216, 220,
|
||||
176, 190, 197,
|
||||
144, 164, 174,
|
||||
120, 144, 156,
|
||||
96, 125, 139,
|
||||
84, 110, 122,
|
||||
69, 90, 100,
|
||||
55, 71, 79,
|
||||
38, 50, 56,
|
||||
// third row
|
||||
255, 235, 238,
|
||||
255, 205, 210,
|
||||
239, 154, 154,
|
||||
229, 115, 115,
|
||||
239, 83, 80,
|
||||
244, 67, 54,
|
||||
229, 57, 53,
|
||||
211, 47, 47,
|
||||
198, 40, 40,
|
||||
183, 28, 28,
|
||||
// forth row
|
||||
252, 228, 236,
|
||||
248, 187, 208,
|
||||
244, 143, 177,
|
||||
240, 98, 146,
|
||||
236, 64, 122,
|
||||
233, 30, 99,
|
||||
216, 27, 96,
|
||||
194, 24, 91,
|
||||
173, 20, 87,
|
||||
136, 14, 79,
|
||||
// fifth row
|
||||
243, 229, 245,
|
||||
225, 190, 231,
|
||||
206, 147, 216,
|
||||
186, 104, 200,
|
||||
171, 71, 188,
|
||||
156, 39, 176,
|
||||
142, 36, 170,
|
||||
123, 31, 162,
|
||||
106, 27, 154,
|
||||
74, 20, 140,
|
||||
// sixth row
|
||||
237, 231, 246,
|
||||
209, 196, 233,
|
||||
179, 157, 219,
|
||||
149, 117, 205,
|
||||
126, 87, 194,
|
||||
103, 58, 183,
|
||||
94, 53, 177,
|
||||
81, 45, 168,
|
||||
69, 39, 160,
|
||||
49, 27, 146,
|
||||
// seventh row
|
||||
232, 234, 246,
|
||||
197, 202, 233,
|
||||
159, 168, 218,
|
||||
121, 134, 203,
|
||||
92, 107, 192,
|
||||
63, 81, 181,
|
||||
57, 73, 171,
|
||||
48, 63, 159,
|
||||
40, 53, 147,
|
||||
26, 35, 126,
|
||||
// eigth row
|
||||
227, 242, 253,
|
||||
187, 222, 251,
|
||||
144, 202, 249,
|
||||
100, 181, 246,
|
||||
66, 165, 245,
|
||||
33, 150, 243,
|
||||
30, 136, 229,
|
||||
25, 118, 210,
|
||||
21, 101, 192,
|
||||
13, 71, 161,
|
||||
// ninth row
|
||||
225, 245, 254,
|
||||
179, 229, 252,
|
||||
129, 212, 250,
|
||||
79, 195, 247,
|
||||
41, 182, 246,
|
||||
3, 169, 244,
|
||||
3, 155, 229,
|
||||
2, 136, 209,
|
||||
2, 119, 189,
|
||||
1, 87, 155,
|
||||
// tenth row
|
||||
224, 247, 250,
|
||||
178, 235, 242,
|
||||
128, 222, 234,
|
||||
77, 208, 225,
|
||||
38, 198, 218,
|
||||
0, 188, 212,
|
||||
0, 172, 193,
|
||||
0, 151, 167,
|
||||
0, 131, 143,
|
||||
0, 96, 100,
|
||||
// eleventh row
|
||||
224, 242, 241,
|
||||
178, 223, 219,
|
||||
128, 203, 196,
|
||||
77, 182, 172,
|
||||
38, 166, 154,
|
||||
0, 150, 136,
|
||||
0, 137, 123,
|
||||
0, 121, 107,
|
||||
0, 105, 92,
|
||||
0, 77, 64,
|
||||
// twelfth row
|
||||
232, 245, 233,
|
||||
200, 230, 201,
|
||||
165, 214, 167,
|
||||
129, 199, 132,
|
||||
102, 187, 106,
|
||||
76, 175, 80,
|
||||
67, 160, 71,
|
||||
56, 142, 60,
|
||||
46, 125, 50,
|
||||
27, 94, 32,
|
||||
|
||||
// thirteenth row
|
||||
241, 248, 233,
|
||||
220, 237, 200,
|
||||
197, 225, 165,
|
||||
174, 213, 129,
|
||||
156, 204, 101,
|
||||
139, 195, 74,
|
||||
124, 179, 66,
|
||||
104, 159, 56,
|
||||
85, 139, 47,
|
||||
51, 105, 30,
|
||||
// fourteenth row
|
||||
249, 251, 231,
|
||||
240, 244, 195,
|
||||
230, 238, 156,
|
||||
220, 231, 117,
|
||||
212, 225, 87,
|
||||
205, 220, 57,
|
||||
192, 202, 51,
|
||||
175, 180, 43,
|
||||
158, 157, 36,
|
||||
130, 119, 23,
|
||||
|
||||
// fifteenth row
|
||||
255, 253, 231,
|
||||
255, 249, 196,
|
||||
255, 245, 157,
|
||||
255, 241, 118,
|
||||
255, 238, 88,
|
||||
255, 235, 59,
|
||||
253, 216, 53,
|
||||
251, 192, 45,
|
||||
249, 168, 37,
|
||||
245, 127, 23,
|
||||
|
||||
// sixteenth row
|
||||
255, 248, 225,
|
||||
255, 236, 179,
|
||||
255, 224, 130,
|
||||
255, 213, 79,
|
||||
255, 202, 40,
|
||||
255, 193, 7,
|
||||
255, 179, 0,
|
||||
255, 160, 0,
|
||||
255, 143, 0,
|
||||
255, 111, 0,
|
||||
|
||||
// seventeenth row
|
||||
255, 243, 224,
|
||||
255, 224, 178,
|
||||
255, 204, 128,
|
||||
255, 183, 77,
|
||||
255, 167, 38,
|
||||
255, 152, 0,
|
||||
251, 140, 0,
|
||||
245, 124, 0,
|
||||
239, 108, 0,
|
||||
230, 81, 0,
|
||||
|
||||
// eighteenth row
|
||||
251, 233, 231,
|
||||
255, 204, 188,
|
||||
255, 171, 145,
|
||||
255, 138, 101,
|
||||
255, 112, 67,
|
||||
255, 87, 34,
|
||||
244, 81, 30,
|
||||
230, 74, 25,
|
||||
216, 67, 21,
|
||||
191, 54, 12,
|
||||
|
||||
// nineteenth row
|
||||
239, 235, 233,
|
||||
215, 204, 200,
|
||||
188, 170, 164,
|
||||
161, 136, 127,
|
||||
141, 110, 99,
|
||||
121, 85, 72,
|
||||
109, 76, 65,
|
||||
93, 64, 55,
|
||||
78, 52, 46,
|
||||
62, 39, 35,
|
||||
};
|
||||
|
||||
private static int clamp(int min, int value, int max) {
|
||||
if (value < min) {
|
||||
return min;
|
||||
}
|
||||
if (value > max) {
|
||||
return max;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.controls.JFXClippedPane;
|
||||
import com.jfoenix.controls.JFXColorPicker;
|
||||
import com.jfoenix.controls.JFXRippler;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import com.jfoenix.utils.JFXNodeUtils;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.css.*;
|
||||
import javafx.css.converter.BooleanConverter;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ColorPicker;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.skin.ComboBoxPopupControl;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Circle;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Shadi Shaheen
|
||||
*/
|
||||
public final class JFXColorPickerSkin extends JFXGenericPickerSkin<Color> {
|
||||
|
||||
private final Label displayNode;
|
||||
private final JFXClippedPane colorBox;
|
||||
private JFXColorPalette popupContent;
|
||||
StyleableBooleanProperty colorLabelVisible = new SimpleStyleableBooleanProperty(StyleableProperties.COLOR_LABEL_VISIBLE,
|
||||
JFXColorPickerSkin.this,
|
||||
"colorLabelVisible",
|
||||
true);
|
||||
|
||||
private final ObjectProperty<Color> textFill = new SimpleObjectProperty<>(Color.BLACK);
|
||||
private final ObjectProperty<Background> colorBoxBackground = new SimpleObjectProperty<>(null);
|
||||
|
||||
public JFXColorPickerSkin(final ColorPicker colorPicker) {
|
||||
super(colorPicker);
|
||||
|
||||
// create displayNode
|
||||
displayNode = new Label("");
|
||||
displayNode.getStyleClass().add("color-label");
|
||||
displayNode.setMouseTransparent(true);
|
||||
displayNode.textFillProperty().bind(textFill);
|
||||
|
||||
// label graphic
|
||||
colorBox = new JFXClippedPane(displayNode);
|
||||
colorBox.getStyleClass().add("color-box");
|
||||
colorBox.setManaged(false);
|
||||
colorBox.backgroundProperty().bind(colorBoxBackground);
|
||||
initColor();
|
||||
final JFXRippler rippler = new JFXRippler(colorBox, JFXRippler.RipplerMask.FIT);
|
||||
rippler.ripplerFillProperty().bind(textFill);
|
||||
getChildren().setAll(rippler);
|
||||
JFXDepthManager.setDepth(getSkinnable(), 1);
|
||||
getSkinnable().setPickOnBounds(false);
|
||||
|
||||
colorPicker.focusedProperty().addListener(observable -> {
|
||||
if (colorPicker.isFocused()) {
|
||||
if (!getSkinnable().isPressed()) {
|
||||
rippler.setOverlayVisible(true);
|
||||
}
|
||||
} else {
|
||||
rippler.setOverlayVisible(false);
|
||||
}
|
||||
});
|
||||
|
||||
// add listeners
|
||||
registerChangeListener(colorPicker.valueProperty(), obs -> updateColor());
|
||||
|
||||
colorLabelVisible.addListener(invalidate -> {
|
||||
if (colorLabelVisible.get()) {
|
||||
displayNode.setText(JFXNodeUtils.colorToHex(getSkinnable().getValue()));
|
||||
} else {
|
||||
displayNode.setText("");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
double width = 100;
|
||||
String displayNodeText = displayNode.getText();
|
||||
displayNode.setText("#DDDDDD");
|
||||
width = Math.max(width, super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset));
|
||||
displayNode.setText(displayNodeText);
|
||||
return width + rightInset + leftInset;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
if (colorBox == null) {
|
||||
reflectUpdateDisplayArea();
|
||||
}
|
||||
return topInset + colorBox.prefHeight(width) + bottomInset;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren(double x, double y, double w, double h) {
|
||||
super.layoutChildren(x, y, w, h);
|
||||
double hInsets = snappedLeftInset() + snappedRightInset();
|
||||
double vInsets = snappedTopInset() + snappedBottomInset();
|
||||
double width = w + hInsets;
|
||||
double height = h + vInsets;
|
||||
colorBox.resizeRelocate(0, 0, width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Node getPopupContent() {
|
||||
if (popupContent == null) {
|
||||
popupContent = new JFXColorPalette((JFXColorPicker) getSkinnable());
|
||||
}
|
||||
return popupContent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show() {
|
||||
super.show();
|
||||
final ColorPicker colorPicker = (ColorPicker) getSkinnable();
|
||||
popupContent.updateSelection(colorPicker.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node getDisplayNode() {
|
||||
return displayNode;
|
||||
}
|
||||
|
||||
private void updateColor() {
|
||||
final ColorPicker colorPicker = (ColorPicker) getSkinnable();
|
||||
Color color = colorPicker.getValue();
|
||||
Color circleColor = color == null ? Color.WHITE : color;
|
||||
// update picker box color
|
||||
if (((JFXColorPicker) getSkinnable()).isDisableAnimation()) {
|
||||
JFXNodeUtils.updateBackground(colorBox.getBackground(), colorBox, circleColor);
|
||||
} else {
|
||||
Circle colorCircle = new Circle();
|
||||
colorCircle.setFill(circleColor);
|
||||
colorCircle.setManaged(false);
|
||||
colorCircle.setLayoutX(colorBox.getWidth() / 4);
|
||||
colorCircle.setLayoutY(colorBox.getHeight() / 2);
|
||||
colorBox.getChildren().add(colorCircle);
|
||||
Timeline animateColor = new Timeline(new KeyFrame(Duration.millis(240),
|
||||
new KeyValue(colorCircle.radiusProperty(),
|
||||
200,
|
||||
Interpolator.EASE_BOTH)));
|
||||
animateColor.setOnFinished((finish) -> {
|
||||
colorBoxBackground.set(new Background(new BackgroundFill(colorCircle.getFill(), new CornerRadii(3), Insets.EMPTY)));
|
||||
colorBox.getChildren().remove(colorCircle);
|
||||
});
|
||||
animateColor.play();
|
||||
}
|
||||
// update label color
|
||||
textFill.set(circleColor.grayscale().getRed() < 0.5 ? Color.valueOf(
|
||||
"rgba(255, 255, 255, 0.87)") : Color.valueOf("rgba(0, 0, 0, 0.87)"));
|
||||
if (colorLabelVisible.get()) {
|
||||
displayNode.setText(JFXNodeUtils.colorToHex(circleColor));
|
||||
} else {
|
||||
displayNode.setText("");
|
||||
}
|
||||
}
|
||||
|
||||
private void initColor() {
|
||||
final ColorPicker colorPicker = (ColorPicker) getSkinnable();
|
||||
Color color = colorPicker.getValue();
|
||||
Color circleColor = color == null ? Color.WHITE : color;
|
||||
// update picker box color
|
||||
colorBoxBackground.set(new Background(new BackgroundFill(circleColor, new CornerRadii(3), Insets.EMPTY)));
|
||||
// update label color
|
||||
textFill.set(circleColor.grayscale().getRed() < 0.5 ? Color.valueOf(
|
||||
"rgba(255, 255, 255, 0.87)") : Color.valueOf("rgba(0, 0, 0, 0.87)"));
|
||||
if (colorLabelVisible.get()) {
|
||||
displayNode.setText(JFXNodeUtils.colorToHex(circleColor));
|
||||
} else {
|
||||
displayNode.setText("");
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Stylesheet Handling *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
private static final class StyleableProperties {
|
||||
private static final CssMetaData<ColorPicker, Boolean> COLOR_LABEL_VISIBLE =
|
||||
new CssMetaData<ColorPicker, Boolean>("-fx-color-label-visible",
|
||||
BooleanConverter.getInstance(), Boolean.TRUE) {
|
||||
|
||||
@Override
|
||||
public boolean isSettable(ColorPicker n) {
|
||||
final JFXColorPickerSkin skin = (JFXColorPickerSkin) n.getSkin();
|
||||
return skin.colorLabelVisible == null || !skin.colorLabelVisible.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Boolean> getStyleableProperty(ColorPicker n) {
|
||||
final JFXColorPickerSkin skin = (JFXColorPickerSkin) n.getSkin();
|
||||
return skin.colorLabelVisible;
|
||||
}
|
||||
};
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
||||
|
||||
static {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables =
|
||||
new ArrayList<>(ComboBoxPopupControl.getClassCssMetaData());
|
||||
styleables.add(COLOR_LABEL_VISIBLE);
|
||||
STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.STYLEABLES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
|
||||
return getClassCssMetaData();
|
||||
}
|
||||
|
||||
protected TextField getEditor() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected javafx.util.StringConverter<Color> getConverter() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,629 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import com.jfoenix.transitions.CachedTransition;
|
||||
import javafx.animation.Animation.Status;
|
||||
import javafx.animation.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.effect.ColorAdjust;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Circle;
|
||||
import javafx.scene.shape.Path;
|
||||
import javafx.util.Duration;
|
||||
|
||||
/**
|
||||
* @author Shadi Shaheen & Bassel El Mabsout this UI allows the user to pick a color using HSL color system
|
||||
*/
|
||||
final class JFXColorPickerUI extends Pane {
|
||||
|
||||
private CachedTransition selectorTransition;
|
||||
private int pickerSize = 400;
|
||||
// sl circle selector size
|
||||
private final int selectorSize = 20;
|
||||
private final double centerX;
|
||||
private final double centerY;
|
||||
private final double huesRadius;
|
||||
private final double slRadius;
|
||||
private double currentHue = 0;
|
||||
|
||||
private final ImageView huesCircleView;
|
||||
private final ImageView slCircleView;
|
||||
private final Pane colorSelector;
|
||||
private final Pane selector;
|
||||
private CurveTransition colorsTransition;
|
||||
|
||||
public JFXColorPickerUI(int pickerSize) {
|
||||
JFXDepthManager.setDepth(this, 1);
|
||||
|
||||
this.pickerSize = pickerSize;
|
||||
this.centerX = (double) pickerSize / 2;
|
||||
this.centerY = (double) pickerSize / 2;
|
||||
final double pickerRadius = (double) pickerSize / 2;
|
||||
this.huesRadius = pickerRadius * 0.9;
|
||||
final double huesSmallR = pickerRadius * 0.8;
|
||||
final double huesLargeR = pickerRadius;
|
||||
this.slRadius = pickerRadius * 0.7;
|
||||
|
||||
// Create Hues Circle
|
||||
huesCircleView = new ImageView(getHuesCircle(pickerSize, pickerSize));
|
||||
// clip to smooth the edges
|
||||
Circle outterCircle = new Circle(centerX, centerY, huesLargeR - 2);
|
||||
Circle innterCircle = new Circle(centerX, centerY, huesSmallR + 2);
|
||||
huesCircleView.setClip(Path.subtract(outterCircle, innterCircle));
|
||||
this.getChildren().add(huesCircleView);
|
||||
|
||||
// create Hues Circle Selector
|
||||
Circle r1 = new Circle(pickerRadius - huesSmallR);
|
||||
Circle r2 = new Circle(pickerRadius - huesRadius);
|
||||
colorSelector = new Pane();
|
||||
colorSelector.setStyle("-fx-border-color:#424242; -fx-border-width:1px; -fx-background-color:rgba(255, 255, 255, 0.87);");
|
||||
colorSelector.setPrefSize(pickerRadius - huesSmallR, pickerRadius - huesSmallR);
|
||||
colorSelector.setShape(Path.subtract(r1, r2));
|
||||
colorSelector.setCache(true);
|
||||
colorSelector.setMouseTransparent(true);
|
||||
colorSelector.setPickOnBounds(false);
|
||||
this.getChildren().add(colorSelector);
|
||||
|
||||
// add Hues Selection Listeners
|
||||
huesCircleView.addEventHandler(MouseEvent.MOUSE_DRAGGED, (event) -> {
|
||||
if (colorsTransition != null) {
|
||||
colorsTransition.stop();
|
||||
}
|
||||
double dx = event.getX() - centerX;
|
||||
double dy = event.getY() - centerY;
|
||||
double theta = Math.atan2(dy, dx);
|
||||
double x = centerX + huesRadius * Math.cos(theta);
|
||||
double y = centerY + huesRadius * Math.sin(theta);
|
||||
colorSelector.setRotate(90 + Math.toDegrees(Math.atan2(dy, dx)));
|
||||
colorSelector.setTranslateX(x - colorSelector.getPrefWidth() / 2);
|
||||
colorSelector.setTranslateY(y - colorSelector.getPrefHeight() / 2);
|
||||
});
|
||||
huesCircleView.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> {
|
||||
double dx = event.getX() - centerX;
|
||||
double dy = event.getY() - centerY;
|
||||
double theta = Math.atan2(dy, dx);
|
||||
double x = centerX + huesRadius * Math.cos(theta);
|
||||
double y = centerY + huesRadius * Math.sin(theta);
|
||||
colorsTransition = new CurveTransition(new Point2D(colorSelector.getTranslateX() + colorSelector.getPrefWidth() / 2,
|
||||
colorSelector.getTranslateY() + colorSelector.getPrefHeight() / 2),
|
||||
new Point2D(x, y));
|
||||
colorsTransition.play();
|
||||
});
|
||||
colorSelector.translateXProperty()
|
||||
.addListener((o, oldVal, newVal) -> updateHSLCircleColor((int) (newVal.intValue() + colorSelector.getPrefWidth() / 2),
|
||||
(int) (colorSelector.getTranslateY() + colorSelector
|
||||
.getPrefHeight() / 2)));
|
||||
colorSelector.translateYProperty()
|
||||
.addListener((o, oldVal, newVal) -> updateHSLCircleColor((int) (colorSelector.getTranslateX() + colorSelector
|
||||
.getPrefWidth() / 2), (int) (newVal.intValue() + colorSelector.getPrefHeight() / 2)));
|
||||
|
||||
|
||||
// Create SL Circle
|
||||
slCircleView = new ImageView(getSLCricle(pickerSize, pickerSize));
|
||||
slCircleView.setClip(new Circle(centerX, centerY, slRadius - 2));
|
||||
slCircleView.setPickOnBounds(false);
|
||||
this.getChildren().add(slCircleView);
|
||||
|
||||
// create SL Circle Selector
|
||||
selector = new Pane();
|
||||
Circle c1 = new Circle(selectorSize / 2);
|
||||
Circle c2 = new Circle((selectorSize / 2) * 0.5);
|
||||
selector.setShape(Path.subtract(c1, c2));
|
||||
selector.setStyle(
|
||||
"-fx-border-color:#424242; -fx-border-width:1px;-fx-background-color:rgba(255, 255, 255, 0.87);");
|
||||
selector.setPrefSize(selectorSize, selectorSize);
|
||||
selector.setMinSize(selectorSize, selectorSize);
|
||||
selector.setMaxSize(selectorSize, selectorSize);
|
||||
selector.setCache(true);
|
||||
selector.setMouseTransparent(true);
|
||||
this.getChildren().add(selector);
|
||||
|
||||
|
||||
// add SL selection Listeners
|
||||
slCircleView.addEventHandler(MouseEvent.MOUSE_DRAGGED, (event) -> {
|
||||
if (selectorTransition != null) {
|
||||
selectorTransition.stop();
|
||||
}
|
||||
if (Math.pow(event.getX() - centerX, 2) + Math.pow(event.getY() - centerY, 2) < Math.pow(slRadius - 2, 2)) {
|
||||
selector.setTranslateX(event.getX() - selector.getPrefWidth() / 2);
|
||||
selector.setTranslateY(event.getY() - selector.getPrefHeight() / 2);
|
||||
} else {
|
||||
double dx = event.getX() - centerX;
|
||||
double dy = event.getY() - centerY;
|
||||
double theta = Math.atan2(dy, dx);
|
||||
double x = centerX + (slRadius - 2) * Math.cos(theta);
|
||||
double y = centerY + (slRadius - 2) * Math.sin(theta);
|
||||
selector.setTranslateX(x - selector.getPrefWidth() / 2);
|
||||
selector.setTranslateY(y - selector.getPrefHeight() / 2);
|
||||
}
|
||||
});
|
||||
slCircleView.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> {
|
||||
selectorTransition = new CachedTransition(selector, new Timeline(new KeyFrame(Duration.millis(1000),
|
||||
new KeyValue(selector.translateXProperty(),
|
||||
event.getX() - selector.getPrefWidth() / 2,
|
||||
Interpolator.EASE_BOTH),
|
||||
new KeyValue(selector.translateYProperty(),
|
||||
event.getY() - selector.getPrefHeight() / 2,
|
||||
Interpolator.EASE_BOTH)))) {
|
||||
{
|
||||
setCycleDuration(Duration.millis(160));
|
||||
setDelay(Duration.seconds(0));
|
||||
}
|
||||
};
|
||||
selectorTransition.play();
|
||||
});
|
||||
// add slCircleView listener
|
||||
selector.translateXProperty()
|
||||
.addListener((o, oldVal, newVal) -> setColorAtLocation(newVal.intValue() + selectorSize / 2,
|
||||
(int) selector.getTranslateY() + selectorSize / 2));
|
||||
selector.translateYProperty()
|
||||
.addListener((o, oldVal, newVal) -> setColorAtLocation((int) selector.getTranslateX() + selectorSize / 2,
|
||||
newVal.intValue() + selectorSize / 2));
|
||||
|
||||
|
||||
// initial color selection
|
||||
double dx = 20 - centerX;
|
||||
double dy = 20 - centerY;
|
||||
double theta = Math.atan2(dy, dx);
|
||||
double x = centerX + huesRadius * Math.cos(theta);
|
||||
double y = centerY + huesRadius * Math.sin(theta);
|
||||
colorSelector.setRotate(90 + Math.toDegrees(Math.atan2(dy, dx)));
|
||||
colorSelector.setTranslateX(x - colorSelector.getPrefWidth() / 2);
|
||||
colorSelector.setTranslateY(y - colorSelector.getPrefHeight() / 2);
|
||||
selector.setTranslateX(centerX - selector.getPrefWidth() / 2);
|
||||
selector.setTranslateY(centerY - selector.getPrefHeight() / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* List of Color Nodes that needs to be updated when picking a color
|
||||
*/
|
||||
private final ObservableList<Node> colorNodes = FXCollections.observableArrayList();
|
||||
|
||||
public void addColorSelectionNode(Node... nodes) {
|
||||
colorNodes.addAll(nodes);
|
||||
}
|
||||
|
||||
public void removeColorSelectionNode(Node... nodes) {
|
||||
colorNodes.removeAll(nodes);
|
||||
}
|
||||
|
||||
private void updateHSLCircleColor(int x, int y) {
|
||||
// transform color to HSL space
|
||||
Color color = huesCircleView.getImage().getPixelReader().getColor(x, y);
|
||||
double max = Math.max(color.getRed(), Math.max(color.getGreen(), color.getBlue()));
|
||||
double min = Math.min(color.getRed(), Math.min(color.getGreen(), color.getBlue()));
|
||||
double hue = 0;
|
||||
if (max != min) {
|
||||
double d = max - min;
|
||||
if (max == color.getRed()) {
|
||||
hue = (color.getGreen() - color.getBlue()) / d + (color.getGreen() < color.getBlue() ? 6 : 0);
|
||||
} else if (max == color.getGreen()) {
|
||||
hue = (color.getBlue() - color.getRed()) / d + 2;
|
||||
} else if (max == color.getBlue()) {
|
||||
hue = (color.getRed() - color.getGreen()) / d + 4;
|
||||
}
|
||||
hue /= 6;
|
||||
}
|
||||
currentHue = map(hue, 0, 1, 0, 255);
|
||||
|
||||
// refresh the HSL circle
|
||||
refreshHSLCircle();
|
||||
}
|
||||
|
||||
private void refreshHSLCircle() {
|
||||
ColorAdjust colorAdjust = new ColorAdjust();
|
||||
colorAdjust.setHue(map(currentHue + (currentHue < 127.5 ? 1 : -1) * 127.5, 0, 255, -1, 1));
|
||||
slCircleView.setEffect(colorAdjust);
|
||||
setColorAtLocation((int) selector.getTranslateX() + selectorSize / 2,
|
||||
(int) selector.getTranslateY() + selectorSize / 2);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* this method is used to move selectors to a certain color
|
||||
*/
|
||||
private boolean allowColorChange = true;
|
||||
private ParallelTransition pTrans;
|
||||
|
||||
public void moveToColor(Color color) {
|
||||
allowColorChange = false;
|
||||
double max = Math.max(color.getRed(),
|
||||
Math.max(color.getGreen(), color.getBlue())), min = Math.min(color.getRed(),
|
||||
Math.min(color.getGreen(),
|
||||
color.getBlue()));
|
||||
double hue = 0;
|
||||
double l = (max + min) / 2;
|
||||
double s = 0;
|
||||
if (max == min) {
|
||||
hue = s = 0; // achromatic
|
||||
} else {
|
||||
double d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
if (max == color.getRed()) {
|
||||
hue = (color.getGreen() - color.getBlue()) / d + (color.getGreen() < color.getBlue() ? 6 : 0);
|
||||
} else if (max == color.getGreen()) {
|
||||
hue = (color.getBlue() - color.getRed()) / d + 2;
|
||||
} else if (max == color.getBlue()) {
|
||||
hue = (color.getRed() - color.getGreen()) / d + 4;
|
||||
}
|
||||
hue /= 6;
|
||||
}
|
||||
currentHue = map(hue, 0, 1, 0, 255);
|
||||
|
||||
// Animate Hue
|
||||
double theta = map(currentHue, 0, 255, -Math.PI, Math.PI);
|
||||
double x = centerX + huesRadius * Math.cos(theta);
|
||||
double y = centerY + huesRadius * Math.sin(theta);
|
||||
colorsTransition = new CurveTransition(
|
||||
new Point2D(
|
||||
colorSelector.getTranslateX() + colorSelector.getPrefWidth() / 2,
|
||||
colorSelector.getTranslateY() + colorSelector.getPrefHeight() / 2
|
||||
),
|
||||
new Point2D(x, y));
|
||||
|
||||
// Animate SL
|
||||
s = map(s, 0, 1, 0, 255);
|
||||
l = map(l, 0, 1, 0, 255);
|
||||
Point2D point = getPointFromSL((int) s, (int) l, slRadius);
|
||||
double pX = centerX - point.getX();
|
||||
double pY = centerY - point.getY();
|
||||
|
||||
double endPointX;
|
||||
double endPointY;
|
||||
if (Math.pow(pX - centerX, 2) + Math.pow(pY - centerY, 2) < Math.pow(slRadius - 2, 2)) {
|
||||
endPointX = pX - selector.getPrefWidth() / 2;
|
||||
endPointY = pY - selector.getPrefHeight() / 2;
|
||||
} else {
|
||||
double dx = pX - centerX;
|
||||
double dy = pY - centerY;
|
||||
theta = Math.atan2(dy, dx);
|
||||
x = centerX + (slRadius - 2) * Math.cos(theta);
|
||||
y = centerY + (slRadius - 2) * Math.sin(theta);
|
||||
endPointX = x - selector.getPrefWidth() / 2;
|
||||
endPointY = y - selector.getPrefHeight() / 2;
|
||||
}
|
||||
selectorTransition = new CachedTransition(selector, new Timeline(new KeyFrame(Duration.millis(1000),
|
||||
new KeyValue(selector.translateXProperty(),
|
||||
endPointX,
|
||||
Interpolator.EASE_BOTH),
|
||||
new KeyValue(selector.translateYProperty(),
|
||||
endPointY,
|
||||
Interpolator.EASE_BOTH)))) {
|
||||
{
|
||||
setCycleDuration(Duration.millis(160));
|
||||
setDelay(Duration.seconds(0));
|
||||
}
|
||||
};
|
||||
|
||||
if (pTrans != null) {
|
||||
pTrans.stop();
|
||||
}
|
||||
pTrans = new ParallelTransition(colorsTransition, selectorTransition);
|
||||
pTrans.setOnFinished((finish) -> {
|
||||
if (pTrans.getStatus() == Status.STOPPED) {
|
||||
allowColorChange = true;
|
||||
}
|
||||
});
|
||||
pTrans.play();
|
||||
|
||||
refreshHSLCircle();
|
||||
}
|
||||
|
||||
private void setColorAtLocation(int x, int y) {
|
||||
if (allowColorChange) {
|
||||
Color color = getColorAtLocation(x, y);
|
||||
String colorString = "rgb(" + color.getRed() * 255 + "," + color.getGreen() * 255 + "," + color.getBlue() * 255 + ");";
|
||||
for (Node node : colorNodes)
|
||||
node.setStyle("-fx-background-color:" + colorString + "; -fx-fill:" + colorString + ";");
|
||||
}
|
||||
}
|
||||
|
||||
private Color getColorAtLocation(double x, double y) {
|
||||
double dy = x - centerX;
|
||||
double dx = y - centerY;
|
||||
return getColor(dx, dy);
|
||||
}
|
||||
|
||||
private Image getHuesCircle(int width, int height) {
|
||||
WritableImage raster = new WritableImage(width, height);
|
||||
PixelWriter pixelWriter = raster.getPixelWriter();
|
||||
Point2D center = new Point2D((double) width / 2, (double) height / 2);
|
||||
double rsmall = 0.8 * width / 2;
|
||||
double rbig = (double) width / 2;
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
double dx = x - center.getX();
|
||||
double dy = y - center.getY();
|
||||
double distance = Math.sqrt((dx * dx) + (dy * dy));
|
||||
double o = Math.atan2(dy, dx);
|
||||
if (distance > rsmall && distance < rbig) {
|
||||
double H = map(o, -Math.PI, Math.PI, 0, 255);
|
||||
double S = 255;
|
||||
double L = 152;
|
||||
pixelWriter.setColor(x, y, HSL2RGB(H, S, L));
|
||||
}
|
||||
}
|
||||
}
|
||||
return raster;
|
||||
}
|
||||
|
||||
private Image getSLCricle(int width, int height) {
|
||||
WritableImage raster = new WritableImage(width, height);
|
||||
PixelWriter pixelWriter = raster.getPixelWriter();
|
||||
Point2D center = new Point2D((double) width / 2, (double) height / 2);
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
double dy = x - center.getX();
|
||||
double dx = y - center.getY();
|
||||
pixelWriter.setColor(x, y, getColor(dx, dy));
|
||||
}
|
||||
}
|
||||
return raster;
|
||||
}
|
||||
|
||||
private double clamp(double from, double small, double big) {
|
||||
return Math.min(Math.max(from, small), big);
|
||||
}
|
||||
|
||||
private Color getColor(double dx, double dy) {
|
||||
double distance = Math.sqrt((dx * dx) + (dy * dy));
|
||||
double rverysmall = 0.65 * ((double) pickerSize / 2);
|
||||
Color pixelColor = Color.BLUE;
|
||||
|
||||
if (distance <= rverysmall * 1.1) {
|
||||
double angle = -Math.PI / 2.;
|
||||
double angle1 = angle + 2 * Math.PI / 3.;
|
||||
double angle2 = angle1 + 2 * Math.PI / 3.;
|
||||
double x1 = rverysmall * Math.sin(angle1);
|
||||
double y1 = rverysmall * Math.cos(angle1);
|
||||
double x2 = rverysmall * Math.sin(angle2);
|
||||
double y2 = rverysmall * Math.cos(angle2);
|
||||
dx += 0.01;
|
||||
double[] circle = circleFrom3Points(new Point2D(x1, y1), new Point2D(x2, y2), new Point2D(dx, dy));
|
||||
double xArc = circle[0];
|
||||
double yArc = 0;
|
||||
double arcR = circle[2];
|
||||
double Arco = Math.atan2(dx - xArc, dy - yArc);
|
||||
double Arco1 = Math.atan2(x1 - xArc, y1 - yArc);
|
||||
double Arco2 = Math.atan2(x2 - xArc, y2 - yArc);
|
||||
|
||||
double finalX = xArc > 0 ? xArc - arcR : xArc + arcR;
|
||||
|
||||
double saturation = map(finalX, -rverysmall, rverysmall, 255, 0);
|
||||
|
||||
double lightness = 255;
|
||||
double diffAngle = Arco2 - Arco1;
|
||||
double diffArco = Arco - Arco1;
|
||||
if (dx < x1) {
|
||||
diffAngle = diffAngle < 0 ? 2 * Math.PI + diffAngle : diffAngle;
|
||||
diffAngle = Math.abs(2 * Math.PI - diffAngle);
|
||||
diffArco = diffArco < 0 ? 2 * Math.PI + diffArco : diffArco;
|
||||
diffArco = Math.abs(2 * Math.PI - diffArco);
|
||||
}
|
||||
lightness = map(diffArco, 0, diffAngle, 0, 255);
|
||||
|
||||
|
||||
if (distance > rverysmall) {
|
||||
saturation = 255 - saturation;
|
||||
if (lightness < 0 && dy < 0) {
|
||||
lightness = 255;
|
||||
}
|
||||
}
|
||||
lightness = clamp(lightness, 0, 255);
|
||||
if ((saturation < 10 && dx < x1) || (saturation > 240 && dx > x1)) {
|
||||
saturation = 255 - saturation;
|
||||
}
|
||||
saturation = clamp(saturation, 0, 255);
|
||||
pixelColor = HSL2RGB(currentHue, saturation, lightness);
|
||||
}
|
||||
return pixelColor;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Hues Animation *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
private final class CurveTransition extends Transition {
|
||||
Point2D from;
|
||||
double fromTheta;
|
||||
double toTheta;
|
||||
|
||||
public CurveTransition(Point2D from, Point2D to) {
|
||||
this.from = from;
|
||||
double fromDx = from.getX() - centerX;
|
||||
double fromDy = from.getY() - centerY;
|
||||
fromTheta = Math.atan2(fromDy, fromDx);
|
||||
double toDx = to.getX() - centerX;
|
||||
double toDy = to.getY() - centerY;
|
||||
toTheta = Math.atan2(toDy, toDx);
|
||||
setInterpolator(Interpolator.EASE_BOTH);
|
||||
setDelay(Duration.millis(0));
|
||||
setCycleDuration(Duration.millis(240));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void interpolate(double frac) {
|
||||
double dif = Math.min(Math.abs(toTheta - fromTheta), 2 * Math.PI - Math.abs(toTheta - fromTheta));
|
||||
if (dif == 2 * Math.PI - Math.abs(toTheta - fromTheta)) {
|
||||
int dir = -1;
|
||||
if (toTheta < fromTheta) {
|
||||
dir = 1;
|
||||
}
|
||||
dif = dir * dif;
|
||||
} else {
|
||||
dif = toTheta - fromTheta;
|
||||
}
|
||||
|
||||
Point2D newP = rotate(from, new Point2D(centerX, centerY), frac * dif);
|
||||
colorSelector.setRotate(90 + Math.toDegrees(Math.atan2(newP.getY() - centerY, newP.getX() - centerX)));
|
||||
colorSelector.setTranslateX(newP.getX() - colorSelector.getPrefWidth() / 2);
|
||||
colorSelector.setTranslateY(newP.getY() - colorSelector.getPrefHeight() / 2);
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Util methods *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
private double map(double val, double min1, double max1, double min2, double max2) {
|
||||
return min2 + (max2 - min2) * ((val - min1) / (max1 - min1));
|
||||
}
|
||||
|
||||
private Color HSL2RGB(double hue, double sat, double lum) {
|
||||
hue = map(hue, 0, 255, 0, 359);
|
||||
sat = map(sat, 0, 255, 0, 1);
|
||||
lum = map(lum, 0, 255, 0, 1);
|
||||
double v;
|
||||
double red, green, blue;
|
||||
double m;
|
||||
double sv;
|
||||
int sextant;
|
||||
double fract, vsf, mid1, mid2;
|
||||
|
||||
red = lum; // default to gray
|
||||
green = lum;
|
||||
blue = lum;
|
||||
v = (lum <= 0.5) ? (lum * (1.0 + sat)) : (lum + sat - lum * sat);
|
||||
m = lum + lum - v;
|
||||
sv = (v - m) / v;
|
||||
hue /= 60.0; //get into range 0..6
|
||||
sextant = (int) Math.floor(hue); // int32 rounds up or down.
|
||||
fract = hue - sextant;
|
||||
vsf = v * sv * fract;
|
||||
mid1 = m + vsf;
|
||||
mid2 = v - vsf;
|
||||
|
||||
if (v > 0) {
|
||||
switch (sextant) {
|
||||
case 0:
|
||||
red = v;
|
||||
green = mid1;
|
||||
blue = m;
|
||||
break;
|
||||
case 1:
|
||||
red = mid2;
|
||||
green = v;
|
||||
blue = m;
|
||||
break;
|
||||
case 2:
|
||||
red = m;
|
||||
green = v;
|
||||
blue = mid1;
|
||||
break;
|
||||
case 3:
|
||||
red = m;
|
||||
green = mid2;
|
||||
blue = v;
|
||||
break;
|
||||
case 4:
|
||||
red = mid1;
|
||||
green = m;
|
||||
blue = v;
|
||||
break;
|
||||
case 5:
|
||||
red = v;
|
||||
green = m;
|
||||
blue = mid2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new Color(red, green, blue, 1);
|
||||
}
|
||||
|
||||
private double[] circleFrom3Points(Point2D a, Point2D b, Point2D c) {
|
||||
double ax, ay, bx, by, cx, cy, x1, y11, dx1, dy1, x2, y2, dx2, dy2, ox, oy, dx, dy, radius; // Variables Used and to Declared
|
||||
ax = a.getX();
|
||||
ay = a.getY(); //first Point X and Y
|
||||
bx = b.getX();
|
||||
by = b.getY(); // Second Point X and Y
|
||||
cx = c.getX();
|
||||
cy = c.getY(); // Third Point X and Y
|
||||
|
||||
////****************Following are Basic Procedure**********************///
|
||||
x1 = (bx + ax) / 2;
|
||||
y11 = (by + ay) / 2;
|
||||
dy1 = bx - ax;
|
||||
dx1 = -(by - ay);
|
||||
|
||||
x2 = (cx + bx) / 2;
|
||||
y2 = (cy + by) / 2;
|
||||
dy2 = cx - bx;
|
||||
dx2 = -(cy - by);
|
||||
|
||||
ox = (y11 * dx1 * dx2 + x2 * dx1 * dy2 - x1 * dy1 * dx2 - y2 * dx1 * dx2) / (dx1 * dy2 - dy1 * dx2);
|
||||
oy = (ox - x1) * dy1 / dx1 + y11;
|
||||
|
||||
dx = ox - ax;
|
||||
dy = oy - ay;
|
||||
radius = Math.sqrt(dx * dx + dy * dy);
|
||||
return new double[]{ox, oy, radius};
|
||||
}
|
||||
|
||||
private Point2D getPointFromSL(int saturation, int lightness, double radius) {
|
||||
double dy = map(saturation, 0, 255, -radius, radius);
|
||||
double angle = 0.;
|
||||
double angle1 = angle + 2 * Math.PI / 3.;
|
||||
double angle2 = angle1 + 2 * Math.PI / 3.;
|
||||
double x1 = radius * Math.sin(angle1);
|
||||
double y1 = radius * Math.cos(angle1);
|
||||
double x2 = radius * Math.sin(angle2);
|
||||
double y2 = radius * Math.cos(angle2);
|
||||
double dx = 0;
|
||||
double[] circle = circleFrom3Points(new Point2D(x1, y1), new Point2D(dx, dy), new Point2D(x2, y2));
|
||||
double xArc = circle[0];
|
||||
double yArc = circle[1];
|
||||
double arcR = circle[2];
|
||||
double Arco1 = Math.atan2(x1 - xArc, y1 - yArc);
|
||||
double Arco2 = Math.atan2(x2 - xArc, y2 - yArc);
|
||||
double ArcoFinal = map(lightness, 0, 255, Arco2, Arco1);
|
||||
double finalX = xArc + arcR * Math.sin(ArcoFinal);
|
||||
double finalY = yArc + arcR * Math.cos(ArcoFinal);
|
||||
if (dy < y1) {
|
||||
ArcoFinal = map(lightness, 0, 255, Arco1, Arco2 + 2 * Math.PI);
|
||||
finalX = -xArc - arcR * Math.sin(ArcoFinal);
|
||||
finalY = yArc + arcR * Math.cos(ArcoFinal);
|
||||
}
|
||||
return new Point2D(finalX, finalY);
|
||||
}
|
||||
|
||||
private Point2D rotate(Point2D a, Point2D center, double angle) {
|
||||
double resultX = center.getX() + (a.getX() - center.getX()) * Math.cos(angle) - (a.getY() - center.getY()) * Math
|
||||
.sin(angle);
|
||||
double resultY = center.getY() + (a.getX() - center.getX()) * Math.sin(angle) + (a.getY() - center.getY()) * Math
|
||||
.cos(angle);
|
||||
return new Point2D(resultX, resultY);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,520 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import com.jfoenix.transitions.CachedTransition;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.effect.DropShadow;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.*;
|
||||
import javafx.scene.transform.Rotate;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import static javafx.animation.Interpolator.EASE_BOTH;
|
||||
|
||||
/// @author Shadi Shaheen
|
||||
final class JFXCustomColorPicker extends Pane {
|
||||
|
||||
ObjectProperty<RecentColorPath> selectedPath = new SimpleObjectProperty<>();
|
||||
private MoveTo startPoint;
|
||||
private CubicCurveTo curve0To;
|
||||
private CubicCurveTo outerCircleCurveTo;
|
||||
private CubicCurveTo curve1To;
|
||||
private CubicCurveTo innerCircleCurveTo;
|
||||
private final ArrayList<CubicCurve> curves = new ArrayList<>();
|
||||
|
||||
private final double distance = 200;
|
||||
private final double centerX = distance;
|
||||
private final double centerY = distance;
|
||||
private final double radius = 110;
|
||||
|
||||
private static final int shapesNumber = 13;
|
||||
private final ArrayList<RecentColorPath> shapes = new ArrayList<>();
|
||||
private CachedTransition showAnimation;
|
||||
private final JFXColorPickerUI hslColorPicker;
|
||||
|
||||
public JFXCustomColorPicker() {
|
||||
this.setPickOnBounds(false);
|
||||
this.setMinSize(distance * 2, distance * 2);
|
||||
|
||||
final DoubleProperty rotationAngle = new SimpleDoubleProperty(2.1);
|
||||
|
||||
// draw recent colors shape using cubic curves
|
||||
init(rotationAngle, centerX + 53, centerY + 162);
|
||||
|
||||
hslColorPicker = new JFXColorPickerUI((int) distance);
|
||||
hslColorPicker.setLayoutX(centerX - distance / 2);
|
||||
hslColorPicker.setLayoutY(centerY - distance / 2);
|
||||
this.getChildren().add(hslColorPicker);
|
||||
// add recent colors shapes
|
||||
final int shapesStartIndex = this.getChildren().size();
|
||||
final int shapesEndIndex = shapesStartIndex + shapesNumber;
|
||||
for (int i = 0; i < shapesNumber; i++) {
|
||||
final double angle = 2 * i * Math.PI / shapesNumber;
|
||||
final RecentColorPath path = new RecentColorPath(startPoint,
|
||||
curve0To,
|
||||
outerCircleCurveTo,
|
||||
curve1To,
|
||||
innerCircleCurveTo);
|
||||
shapes.add(path);
|
||||
path.setPickOnBounds(false);
|
||||
final Rotate rotate = new Rotate(Math.toDegrees(angle), centerX, centerY);
|
||||
path.getTransforms().add(rotate);
|
||||
this.getChildren().add(shapesStartIndex, path);
|
||||
path.setFill(Color.valueOf(getDefaultColor(i)));
|
||||
path.setFocusTraversable(true);
|
||||
path.addEventHandler(MouseEvent.MOUSE_CLICKED, (event) -> {
|
||||
path.requestFocus();
|
||||
selectedPath.set(path);
|
||||
});
|
||||
}
|
||||
|
||||
// add selection listeners
|
||||
selectedPath.addListener((o, oldVal, newVal) -> {
|
||||
if (oldVal != null) {
|
||||
hslColorPicker.removeColorSelectionNode(oldVal);
|
||||
oldVal.playTransition(-1);
|
||||
}
|
||||
// re-arrange children
|
||||
while (this.getChildren().indexOf(newVal) != shapesEndIndex - 1) {
|
||||
final Node temp = this.getChildren().get(shapesEndIndex - 1);
|
||||
this.getChildren().remove(shapesEndIndex - 1);
|
||||
this.getChildren().add(shapesStartIndex, temp);
|
||||
}
|
||||
// update path fill according to the color picker
|
||||
newVal.setStroke(Color.rgb(255, 255, 255, 0.87));
|
||||
newVal.playTransition(1);
|
||||
hslColorPicker.moveToColor((Color) newVal.getFill());
|
||||
hslColorPicker.addColorSelectionNode(newVal);
|
||||
});
|
||||
// init selection
|
||||
selectedPath.set((RecentColorPath) this.getChildren().get(shapesStartIndex));
|
||||
}
|
||||
|
||||
public int getShapesNumber() {
|
||||
return shapesNumber;
|
||||
}
|
||||
|
||||
public int getSelectedIndex() {
|
||||
if (selectedPath.get() != null) {
|
||||
return shapes.indexOf(selectedPath.get());
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void setColor(final Color color) {
|
||||
shapes.get(getSelectedIndex()).setFill(color);
|
||||
hslColorPicker.moveToColor(color);
|
||||
}
|
||||
|
||||
public Color getColor(final int index) {
|
||||
if (index >= 0 && index < shapes.size()) {
|
||||
return (Color) shapes.get(index).getFill();
|
||||
} else {
|
||||
return Color.WHITE;
|
||||
}
|
||||
}
|
||||
|
||||
public void preAnimate() {
|
||||
final CubicCurve firstCurve = curves.get(0);
|
||||
final double x = firstCurve.getStartX();
|
||||
final double y = firstCurve.getStartY();
|
||||
firstCurve.setStartX(centerX);
|
||||
firstCurve.setStartY(centerY);
|
||||
|
||||
final CubicCurve secondCurve = curves.get(1);
|
||||
final double x1 = secondCurve.getStartX();
|
||||
final double y1 = secondCurve.getStartY();
|
||||
secondCurve.setStartX(centerX);
|
||||
secondCurve.setStartY(centerY);
|
||||
|
||||
final double cx1 = firstCurve.getControlX1();
|
||||
final double cy1 = firstCurve.getControlY1();
|
||||
firstCurve.setControlX1(centerX + radius);
|
||||
firstCurve.setControlY1(centerY + radius / 2);
|
||||
|
||||
final KeyFrame keyFrame = new KeyFrame(Duration.millis(1000),
|
||||
new KeyValue(firstCurve.startXProperty(), x, EASE_BOTH),
|
||||
new KeyValue(firstCurve.startYProperty(), y, EASE_BOTH),
|
||||
new KeyValue(secondCurve.startXProperty(), x1, EASE_BOTH),
|
||||
new KeyValue(secondCurve.startYProperty(), y1, EASE_BOTH),
|
||||
new KeyValue(firstCurve.controlX1Property(), cx1, EASE_BOTH),
|
||||
new KeyValue(firstCurve.controlY1Property(), cy1, EASE_BOTH)
|
||||
);
|
||||
final Timeline timeline = new Timeline(keyFrame);
|
||||
showAnimation = new CachedTransition(this, timeline) {
|
||||
{
|
||||
setCycleDuration(Duration.millis(240));
|
||||
setDelay(Duration.millis(0));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void animate() {
|
||||
showAnimation.play();
|
||||
}
|
||||
|
||||
private void init(final DoubleProperty rotationAngle, final double initControlX1, final double initControlY1) {
|
||||
|
||||
final Circle innerCircle = new Circle(centerX, centerY, radius, Color.TRANSPARENT);
|
||||
final Circle outerCircle = new Circle(centerX, centerY, radius * 2, Color.web("blue", 0.5));
|
||||
|
||||
// Create a composite shape of 4 cubic curves
|
||||
// create 2 cubic curves of the shape
|
||||
createQuadraticCurve(rotationAngle, initControlX1, initControlY1);
|
||||
|
||||
// inner circle curve
|
||||
final CubicCurve innerCircleCurve = new CubicCurve();
|
||||
innerCircleCurve.startXProperty().bind(curves.get(0).startXProperty());
|
||||
innerCircleCurve.startYProperty().bind(curves.get(0).startYProperty());
|
||||
innerCircleCurve.endXProperty().bind(curves.get(1).startXProperty());
|
||||
innerCircleCurve.endYProperty().bind(curves.get(1).startYProperty());
|
||||
curves.get(0).startXProperty().addListener((o, oldVal, newVal) -> {
|
||||
final Point2D controlPoint = makeControlPoint(newVal.doubleValue(),
|
||||
curves.get(0).getStartY(),
|
||||
innerCircle,
|
||||
shapesNumber,
|
||||
-1);
|
||||
innerCircleCurve.setControlX1(controlPoint.getX());
|
||||
innerCircleCurve.setControlY1(controlPoint.getY());
|
||||
});
|
||||
curves.get(0).startYProperty().addListener((o, oldVal, newVal) -> {
|
||||
final Point2D controlPoint = makeControlPoint(curves.get(0).getStartX(),
|
||||
newVal.doubleValue(),
|
||||
innerCircle,
|
||||
shapesNumber,
|
||||
-1);
|
||||
innerCircleCurve.setControlX1(controlPoint.getX());
|
||||
innerCircleCurve.setControlY1(controlPoint.getY());
|
||||
});
|
||||
curves.get(1).startXProperty().addListener((o, oldVal, newVal) -> {
|
||||
final Point2D controlPoint = makeControlPoint(newVal.doubleValue(),
|
||||
curves.get(1).getStartY(),
|
||||
innerCircle,
|
||||
shapesNumber,
|
||||
1);
|
||||
innerCircleCurve.setControlX2(controlPoint.getX());
|
||||
innerCircleCurve.setControlY2(controlPoint.getY());
|
||||
});
|
||||
curves.get(1).startYProperty().addListener((o, oldVal, newVal) -> {
|
||||
final Point2D controlPoint = makeControlPoint(curves.get(1).getStartX(),
|
||||
newVal.doubleValue(),
|
||||
innerCircle,
|
||||
shapesNumber,
|
||||
1);
|
||||
innerCircleCurve.setControlX2(controlPoint.getX());
|
||||
innerCircleCurve.setControlY2(controlPoint.getY());
|
||||
});
|
||||
Point2D controlPoint = makeControlPoint(curves.get(0).getStartX(),
|
||||
curves.get(0).getStartY(),
|
||||
innerCircle,
|
||||
shapesNumber,
|
||||
-1);
|
||||
innerCircleCurve.setControlX1(controlPoint.getX());
|
||||
innerCircleCurve.setControlY1(controlPoint.getY());
|
||||
controlPoint = makeControlPoint(curves.get(1).getStartX(),
|
||||
curves.get(1).getStartY(),
|
||||
innerCircle,
|
||||
shapesNumber,
|
||||
1);
|
||||
innerCircleCurve.setControlX2(controlPoint.getX());
|
||||
innerCircleCurve.setControlY2(controlPoint.getY());
|
||||
|
||||
// outer circle curve
|
||||
final CubicCurve outerCircleCurve = new CubicCurve();
|
||||
outerCircleCurve.startXProperty().bind(curves.get(0).endXProperty());
|
||||
outerCircleCurve.startYProperty().bind(curves.get(0).endYProperty());
|
||||
outerCircleCurve.endXProperty().bind(curves.get(1).endXProperty());
|
||||
outerCircleCurve.endYProperty().bind(curves.get(1).endYProperty());
|
||||
controlPoint = makeControlPoint(curves.get(0).getEndX(),
|
||||
curves.get(0).getEndY(),
|
||||
outerCircle,
|
||||
shapesNumber,
|
||||
-1);
|
||||
outerCircleCurve.setControlX1(controlPoint.getX());
|
||||
outerCircleCurve.setControlY1(controlPoint.getY());
|
||||
controlPoint = makeControlPoint(curves.get(1).getEndX(), curves.get(1).getEndY(), outerCircle, shapesNumber, 1);
|
||||
outerCircleCurve.setControlX2(controlPoint.getX());
|
||||
outerCircleCurve.setControlY2(controlPoint.getY());
|
||||
|
||||
startPoint = new MoveTo();
|
||||
startPoint.xProperty().bind(curves.get(0).startXProperty());
|
||||
startPoint.yProperty().bind(curves.get(0).startYProperty());
|
||||
|
||||
curve0To = new CubicCurveTo();
|
||||
curve0To.controlX1Property().bind(curves.get(0).controlX1Property());
|
||||
curve0To.controlY1Property().bind(curves.get(0).controlY1Property());
|
||||
curve0To.controlX2Property().bind(curves.get(0).controlX2Property());
|
||||
curve0To.controlY2Property().bind(curves.get(0).controlY2Property());
|
||||
curve0To.xProperty().bind(curves.get(0).endXProperty());
|
||||
curve0To.yProperty().bind(curves.get(0).endYProperty());
|
||||
|
||||
outerCircleCurveTo = new CubicCurveTo();
|
||||
outerCircleCurveTo.controlX1Property().bind(outerCircleCurve.controlX1Property());
|
||||
outerCircleCurveTo.controlY1Property().bind(outerCircleCurve.controlY1Property());
|
||||
outerCircleCurveTo.controlX2Property().bind(outerCircleCurve.controlX2Property());
|
||||
outerCircleCurveTo.controlY2Property().bind(outerCircleCurve.controlY2Property());
|
||||
outerCircleCurveTo.xProperty().bind(outerCircleCurve.endXProperty());
|
||||
outerCircleCurveTo.yProperty().bind(outerCircleCurve.endYProperty());
|
||||
|
||||
curve1To = new CubicCurveTo();
|
||||
curve1To.controlX1Property().bind(curves.get(1).controlX2Property());
|
||||
curve1To.controlY1Property().bind(curves.get(1).controlY2Property());
|
||||
curve1To.controlX2Property().bind(curves.get(1).controlX1Property());
|
||||
curve1To.controlY2Property().bind(curves.get(1).controlY1Property());
|
||||
curve1To.xProperty().bind(curves.get(1).startXProperty());
|
||||
curve1To.yProperty().bind(curves.get(1).startYProperty());
|
||||
|
||||
innerCircleCurveTo = new CubicCurveTo();
|
||||
innerCircleCurveTo.controlX1Property().bind(innerCircleCurve.controlX2Property());
|
||||
innerCircleCurveTo.controlY1Property().bind(innerCircleCurve.controlY2Property());
|
||||
innerCircleCurveTo.controlX2Property().bind(innerCircleCurve.controlX1Property());
|
||||
innerCircleCurveTo.controlY2Property().bind(innerCircleCurve.controlY1Property());
|
||||
innerCircleCurveTo.xProperty().bind(innerCircleCurve.startXProperty());
|
||||
innerCircleCurveTo.yProperty().bind(innerCircleCurve.startYProperty());
|
||||
}
|
||||
|
||||
private void createQuadraticCurve(final DoubleProperty rotationAngle, final double initControlX1, final double initControlY1) {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
|
||||
double angle = 2 * i * Math.PI / shapesNumber;
|
||||
double xOffset = radius * Math.cos(angle);
|
||||
double yOffset = radius * Math.sin(angle);
|
||||
final double startx = centerX + xOffset;
|
||||
final double starty = centerY + yOffset;
|
||||
|
||||
final double diffStartCenterX = startx - centerX;
|
||||
final double diffStartCenterY = starty - centerY;
|
||||
final double sinRotAngle = Math.sin(rotationAngle.get());
|
||||
final double cosRotAngle = Math.cos(rotationAngle.get());
|
||||
final double startXR = cosRotAngle * diffStartCenterX - sinRotAngle * diffStartCenterY + centerX;
|
||||
final double startYR = sinRotAngle * diffStartCenterX + cosRotAngle * diffStartCenterY + centerY;
|
||||
|
||||
angle = 2 * i * Math.PI / shapesNumber;
|
||||
xOffset = distance * Math.cos(angle);
|
||||
yOffset = distance * Math.sin(angle);
|
||||
|
||||
final double endx = centerX + xOffset;
|
||||
final double endy = centerY + yOffset;
|
||||
|
||||
final CubicCurve curvedLine = new CubicCurve();
|
||||
curvedLine.setStartX(startXR);
|
||||
curvedLine.setStartY(startYR);
|
||||
curvedLine.setControlX1(startXR);
|
||||
curvedLine.setControlY1(startYR);
|
||||
curvedLine.setControlX2(endx);
|
||||
curvedLine.setControlY2(endy);
|
||||
curvedLine.setEndX(endx);
|
||||
curvedLine.setEndY(endy);
|
||||
curvedLine.setStroke(Color.FORESTGREEN);
|
||||
curvedLine.setStrokeWidth(1);
|
||||
curvedLine.setStrokeLineCap(StrokeLineCap.ROUND);
|
||||
curvedLine.setFill(Color.TRANSPARENT);
|
||||
curvedLine.setMouseTransparent(true);
|
||||
rotationAngle.addListener((o, oldVal, newVal) -> {
|
||||
final double newstartXR = ((cosRotAngle * diffStartCenterX) - (sinRotAngle * diffStartCenterY)) + centerX;
|
||||
final double newstartYR = (sinRotAngle * diffStartCenterX) + (cosRotAngle * diffStartCenterY) + centerY;
|
||||
curvedLine.setStartX(newstartXR);
|
||||
curvedLine.setStartY(newstartYR);
|
||||
});
|
||||
|
||||
curves.add(curvedLine);
|
||||
|
||||
if (i == 0) {
|
||||
curvedLine.setControlX1(initControlX1);
|
||||
curvedLine.setControlY1(initControlY1);
|
||||
} else {
|
||||
final CubicCurve firstCurve = curves.get(0);
|
||||
final double curveTheta = 2 * curves.indexOf(curvedLine) * Math.PI / shapesNumber;
|
||||
|
||||
curvedLine.controlX1Property().bind(Bindings.createDoubleBinding(() -> {
|
||||
final double a = firstCurve.getControlX1() - centerX;
|
||||
final double b = Math.sin(curveTheta) * (firstCurve.getControlY1() - centerY);
|
||||
return ((Math.cos(curveTheta) * a) - b) + centerX;
|
||||
}, firstCurve.controlX1Property(), firstCurve.controlY1Property()));
|
||||
|
||||
curvedLine.controlY1Property().bind(Bindings.createDoubleBinding(() -> {
|
||||
final double a = Math.sin(curveTheta) * (firstCurve.getControlX1() - centerX);
|
||||
final double b = Math.cos(curveTheta) * (firstCurve.getControlY1() - centerY);
|
||||
return a + b + centerY;
|
||||
}, firstCurve.controlX1Property(), firstCurve.controlY1Property()));
|
||||
|
||||
|
||||
curvedLine.controlX2Property().bind(Bindings.createDoubleBinding(() -> {
|
||||
final double a = firstCurve.getControlX2() - centerX;
|
||||
final double b = firstCurve.getControlY2() - centerY;
|
||||
return ((Math.cos(curveTheta) * a) - (Math.sin(curveTheta) * b)) + centerX;
|
||||
}, firstCurve.controlX2Property(), firstCurve.controlY2Property()));
|
||||
|
||||
curvedLine.controlY2Property().bind(Bindings.createDoubleBinding(() -> {
|
||||
final double a = Math.sin(curveTheta) * (firstCurve.getControlX2() - centerX);
|
||||
final double b = Math.cos(curveTheta) * (firstCurve.getControlY2() - centerY);
|
||||
return a + b + centerY;
|
||||
}, firstCurve.controlX2Property(), firstCurve.controlY2Property()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getDefaultColor(final int i) {
|
||||
String color = "#FFFFFF";
|
||||
switch (i) {
|
||||
case 0:
|
||||
color = "#8F3F7E";
|
||||
break;
|
||||
case 1:
|
||||
color = "#B5305F";
|
||||
break;
|
||||
case 2:
|
||||
color = "#CE584A";
|
||||
break;
|
||||
case 3:
|
||||
color = "#DB8D5C";
|
||||
break;
|
||||
case 4:
|
||||
color = "#DA854E";
|
||||
break;
|
||||
case 5:
|
||||
color = "#E9AB44";
|
||||
break;
|
||||
case 6:
|
||||
color = "#FEE435";
|
||||
break;
|
||||
case 7:
|
||||
color = "#99C286";
|
||||
break;
|
||||
case 8:
|
||||
color = "#01A05E";
|
||||
break;
|
||||
case 9:
|
||||
color = "#4A8895";
|
||||
break;
|
||||
case 10:
|
||||
color = "#16669B";
|
||||
break;
|
||||
case 11:
|
||||
color = "#2F65A5";
|
||||
break;
|
||||
case 12:
|
||||
color = "#4E6A9C";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
private Point2D rotate(final Point2D a, final Point2D center, final double angle) {
|
||||
final double resultX = center.getX() + (a.getX() - center.getX()) * Math.cos(angle) - (a.getY() - center.getY()) * Math
|
||||
.sin(angle);
|
||||
final double resultY = center.getY() + (a.getX() - center.getX()) * Math.sin(angle) + (a.getY() - center.getY()) * Math
|
||||
.cos(angle);
|
||||
return new Point2D(resultX, resultY);
|
||||
}
|
||||
|
||||
private Point2D makeControlPoint(final double endX, final double endY, final Circle circle, final int numSegments, int direction) {
|
||||
final double controlPointDistance = (4.0 / 3.0) * Math.tan(Math.PI / (2 * numSegments)) * circle.getRadius();
|
||||
final Point2D center = new Point2D(circle.getCenterX(), circle.getCenterY());
|
||||
final Point2D end = new Point2D(endX, endY);
|
||||
Point2D perp = rotate(center, end, direction * Math.PI / 2.);
|
||||
Point2D diff = perp.subtract(end);
|
||||
diff = diff.normalize();
|
||||
diff = scale(diff, controlPointDistance);
|
||||
return end.add(diff);
|
||||
}
|
||||
|
||||
private Point2D scale(final Point2D a, final double scale) {
|
||||
return new Point2D(a.getX() * scale, a.getY() * scale);
|
||||
}
|
||||
|
||||
final class RecentColorPath extends Path {
|
||||
PathClickTransition transition;
|
||||
|
||||
RecentColorPath(final PathElement... elements) {
|
||||
super(elements);
|
||||
this.setStrokeLineCap(StrokeLineCap.ROUND);
|
||||
this.setStrokeWidth(0);
|
||||
this.setStrokeType(StrokeType.CENTERED);
|
||||
this.setCache(true);
|
||||
JFXDepthManager.setDepth(this, 2);
|
||||
this.transition = new PathClickTransition(this);
|
||||
}
|
||||
|
||||
void playTransition(final double rate) {
|
||||
transition.setRate(rate);
|
||||
transition.play();
|
||||
}
|
||||
}
|
||||
|
||||
private final class PathClickTransition extends CachedTransition {
|
||||
PathClickTransition(final Path path) {
|
||||
super(JFXCustomColorPicker.this, new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(((DropShadow) path.getEffect()).radiusProperty(),
|
||||
JFXDepthManager.getShadowAt(2).radiusProperty().get(),
|
||||
EASE_BOTH),
|
||||
new KeyValue(((DropShadow) path.getEffect()).spreadProperty(),
|
||||
JFXDepthManager.getShadowAt(2).spreadProperty().get(),
|
||||
EASE_BOTH),
|
||||
new KeyValue(((DropShadow) path.getEffect()).offsetXProperty(),
|
||||
JFXDepthManager.getShadowAt(2).offsetXProperty().get(),
|
||||
EASE_BOTH),
|
||||
new KeyValue(((DropShadow) path.getEffect()).offsetYProperty(),
|
||||
JFXDepthManager.getShadowAt(2).offsetYProperty().get(),
|
||||
EASE_BOTH),
|
||||
new KeyValue(path.strokeWidthProperty(), 0, EASE_BOTH)
|
||||
),
|
||||
new KeyFrame(Duration.millis(1000),
|
||||
new KeyValue(((DropShadow) path.getEffect()).radiusProperty(),
|
||||
JFXDepthManager.getShadowAt(5).radiusProperty().get(),
|
||||
EASE_BOTH),
|
||||
new KeyValue(((DropShadow) path.getEffect()).spreadProperty(),
|
||||
JFXDepthManager.getShadowAt(5).spreadProperty().get(),
|
||||
EASE_BOTH),
|
||||
new KeyValue(((DropShadow) path.getEffect()).offsetXProperty(),
|
||||
JFXDepthManager.getShadowAt(5).offsetXProperty().get(),
|
||||
EASE_BOTH),
|
||||
new KeyValue(((DropShadow) path.getEffect()).offsetYProperty(),
|
||||
JFXDepthManager.getShadowAt(5).offsetYProperty().get(),
|
||||
EASE_BOTH),
|
||||
new KeyValue(path.strokeWidthProperty(), 2, EASE_BOTH)
|
||||
)
|
||||
)
|
||||
);
|
||||
// reduce the number to increase the shifting , increase number to reduce shifting
|
||||
setCycleDuration(Duration.millis(120));
|
||||
setDelay(Duration.seconds(0));
|
||||
setAutoReverse(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import com.jfoenix.svg.SVGGlyph;
|
||||
import com.jfoenix.transitions.JFXFillTransition;
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TextFormatter;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.*;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author Shadi Shaheen
|
||||
*/
|
||||
public class JFXCustomColorPickerDialog extends StackPane {
|
||||
private final Stage dialog = new Stage();
|
||||
// used for concurrency control and preventing FX-thread over use
|
||||
private final AtomicInteger concurrencyController = new AtomicInteger(-1);
|
||||
|
||||
private final ObjectProperty<Color> currentColorProperty = new SimpleObjectProperty<>(Color.WHITE);
|
||||
private final ObjectProperty<Color> customColorProperty = new SimpleObjectProperty<>(Color.TRANSPARENT);
|
||||
private Runnable onSave;
|
||||
|
||||
private final Scene customScene;
|
||||
private final JFXCustomColorPicker curvedColorPicker;
|
||||
private ParallelTransition paraTransition;
|
||||
private final JFXDecorator pickerDecorator;
|
||||
private boolean systemChange = false;
|
||||
private boolean userChange = false;
|
||||
private boolean initOnce = true;
|
||||
private final Runnable initRun;
|
||||
|
||||
public JFXCustomColorPickerDialog(Window owner) {
|
||||
getStyleClass().add("custom-color-dialog");
|
||||
if (owner != null) {
|
||||
dialog.initOwner(owner);
|
||||
}
|
||||
dialog.initModality(Modality.APPLICATION_MODAL);
|
||||
dialog.initStyle(StageStyle.TRANSPARENT);
|
||||
dialog.setResizable(false);
|
||||
|
||||
// create JFX Decorator
|
||||
pickerDecorator = new JFXDecorator(dialog, this, false, false, false);
|
||||
pickerDecorator.setOnCloseButtonAction(this::updateColor);
|
||||
pickerDecorator.setPickOnBounds(false);
|
||||
customScene = new Scene(pickerDecorator, Color.TRANSPARENT);
|
||||
StyleSheets.init(customScene);
|
||||
curvedColorPicker = new JFXCustomColorPicker();
|
||||
|
||||
StackPane pane = new StackPane(curvedColorPicker);
|
||||
pane.setPadding(new Insets(18));
|
||||
|
||||
VBox container = new VBox();
|
||||
container.getChildren().add(pane);
|
||||
|
||||
JFXTabPane tabs = new JFXTabPane();
|
||||
|
||||
JFXTextField rgbField = new JFXTextField();
|
||||
JFXTextField hsbField = new JFXTextField();
|
||||
JFXTextField hexField = new JFXTextField();
|
||||
|
||||
rgbField.getStyleClass().add("custom-color-field");
|
||||
rgbField.setPromptText("RGB Color");
|
||||
rgbField.setTextFormatter(colorCharFormatter());
|
||||
rgbField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
|
||||
|
||||
hsbField.getStyleClass().add("custom-color-field");
|
||||
hsbField.setPromptText("HSB Color");
|
||||
hsbField.setTextFormatter(colorCharFormatter());
|
||||
hsbField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
|
||||
|
||||
hexField.getStyleClass().add("custom-color-field");
|
||||
hexField.setPromptText("#HEX Color");
|
||||
hexField.setTextFormatter(colorCharFormatter());
|
||||
hexField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
|
||||
|
||||
StackPane tabContent = new StackPane();
|
||||
tabContent.getChildren().add(rgbField);
|
||||
tabContent.setMinHeight(100);
|
||||
|
||||
Tab rgbTab = new Tab("RGB");
|
||||
rgbTab.setContent(tabContent);
|
||||
Tab hsbTab = new Tab("HSB");
|
||||
hsbTab.setContent(hsbField);
|
||||
Tab hexTab = new Tab("HEX");
|
||||
hexTab.setContent(hexField);
|
||||
|
||||
tabs.getTabs().add(hexTab);
|
||||
tabs.getTabs().add(rgbTab);
|
||||
tabs.getTabs().add(hsbTab);
|
||||
|
||||
curvedColorPicker.selectedPath.addListener((o, oldVal, newVal) -> {
|
||||
if (paraTransition != null) {
|
||||
paraTransition.stop();
|
||||
}
|
||||
Region tabsHeader = (Region) tabs.lookup(".tab-header-background");
|
||||
pane.backgroundProperty().unbind();
|
||||
tabsHeader.backgroundProperty().unbind();
|
||||
JFXFillTransition fillTransition = new JFXFillTransition(Duration.millis(240),
|
||||
pane,
|
||||
(Color) oldVal.getFill(),
|
||||
(Color) newVal.getFill());
|
||||
JFXFillTransition tabsFillTransition = new JFXFillTransition(Duration.millis(240),
|
||||
tabsHeader,
|
||||
(Color) oldVal.getFill(),
|
||||
(Color) newVal.getFill());
|
||||
paraTransition = new ParallelTransition(fillTransition, tabsFillTransition);
|
||||
paraTransition.setOnFinished((finish) -> {
|
||||
tabsHeader.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
return new Background(new BackgroundFill(newVal.getFill(), CornerRadii.EMPTY, Insets.EMPTY));
|
||||
}, newVal.fillProperty()));
|
||||
pane.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
return new Background(new BackgroundFill(newVal.getFill(), CornerRadii.EMPTY, Insets.EMPTY));
|
||||
}, newVal.fillProperty()));
|
||||
});
|
||||
paraTransition.play();
|
||||
});
|
||||
|
||||
initRun = () -> {
|
||||
// change tabs labels font color according to the selected color
|
||||
pane.backgroundProperty().addListener((o, oldVal, newVal) -> {
|
||||
if (concurrencyController.getAndSet(1) == -1) {
|
||||
Color fontColor = ((Color) newVal.getFills().get(0).getFill()).grayscale()
|
||||
.getRed() > 0.5 ? Color.valueOf(
|
||||
"rgba(40, 40, 40, 0.87)") : Color.valueOf("rgba(255, 255, 255, 0.87)");
|
||||
// for (Node tabNode : tabs.lookupAll(".tab")) {
|
||||
// for (Node node : tabNode.lookupAll(".tab-label")) {
|
||||
// ((Label) node).setTextFill(fontColor);
|
||||
// }
|
||||
// for (Node node : tabNode.lookupAll(".jfx-rippler")) {
|
||||
// ((JFXRippler) node).setRipplerFill(fontColor);
|
||||
// }
|
||||
// }
|
||||
// ((Pane) tabs.lookup(".tab-selected-line")).setBackground(new Background(new BackgroundFill(fontColor, CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
pickerDecorator.lookupAll(".jfx-decorator-button").forEach(button -> {
|
||||
((JFXButton) button).setRipplerFill(fontColor);
|
||||
((SVGGlyph) ((JFXButton) button).getGraphic()).setFill(fontColor);
|
||||
});
|
||||
|
||||
Color newColor = (Color) newVal.getFills().get(0).getFill();
|
||||
String hex = String.format("#%02X%02X%02X",
|
||||
(int) (newColor.getRed() * 255),
|
||||
(int) (newColor.getGreen() * 255),
|
||||
(int) (newColor.getBlue() * 255));
|
||||
String rgb = String.format("rgba(%d, %d, %d, 1)",
|
||||
(int) (newColor.getRed() * 255),
|
||||
(int) (newColor.getGreen() * 255),
|
||||
(int) (newColor.getBlue() * 255));
|
||||
String hsb = String.format("hsl(%d, %d%%, %d%%)",
|
||||
(int) (newColor.getHue()),
|
||||
(int) (newColor.getSaturation() * 100),
|
||||
(int) (newColor.getBrightness() * 100));
|
||||
|
||||
if (!userChange) {
|
||||
systemChange = true;
|
||||
rgbField.setText(rgb);
|
||||
hsbField.setText(hsb);
|
||||
hexField.setText(hex);
|
||||
systemChange = false;
|
||||
}
|
||||
concurrencyController.getAndSet(-1);
|
||||
}
|
||||
});
|
||||
|
||||
// initial selected colors
|
||||
Platform.runLater(() -> {
|
||||
pane.setBackground(new Background(new BackgroundFill(curvedColorPicker.getColor(curvedColorPicker.getSelectedIndex()),
|
||||
CornerRadii.EMPTY,
|
||||
Insets.EMPTY)));
|
||||
((Region) tabs.lookup(".tab-header-background")).setBackground(new Background(new BackgroundFill(
|
||||
curvedColorPicker.getColor(curvedColorPicker.getSelectedIndex()),
|
||||
CornerRadii.EMPTY,
|
||||
Insets.EMPTY)));
|
||||
Region tabsHeader = (Region) tabs.lookup(".tab-header-background");
|
||||
pane.backgroundProperty().unbind();
|
||||
tabsHeader.backgroundProperty().unbind();
|
||||
tabsHeader.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
return new Background(new BackgroundFill(curvedColorPicker.selectedPath.get().getFill(),
|
||||
CornerRadii.EMPTY,
|
||||
Insets.EMPTY));
|
||||
}, curvedColorPicker.selectedPath.get().fillProperty()));
|
||||
pane.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
return new Background(new BackgroundFill(curvedColorPicker.selectedPath.get().getFill(),
|
||||
CornerRadii.EMPTY,
|
||||
Insets.EMPTY));
|
||||
}, curvedColorPicker.selectedPath.get().fillProperty()));
|
||||
|
||||
// bind text field line color
|
||||
rgbField.focusColorProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
return pane.getBackground().getFills().get(0).getFill();
|
||||
}, pane.backgroundProperty()));
|
||||
hsbField.focusColorProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
return pane.getBackground().getFills().get(0).getFill();
|
||||
}, pane.backgroundProperty()));
|
||||
hexField.focusColorProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
return pane.getBackground().getFills().get(0).getFill();
|
||||
}, pane.backgroundProperty()));
|
||||
|
||||
|
||||
((Pane) pickerDecorator.lookup(".jfx-decorator-buttons-container")).backgroundProperty()
|
||||
.bind(Bindings.createObjectBinding(() -> {
|
||||
return new Background(new BackgroundFill(
|
||||
pane.getBackground()
|
||||
.getFills()
|
||||
.get(0)
|
||||
.getFill(),
|
||||
CornerRadii.EMPTY,
|
||||
Insets.EMPTY));
|
||||
}, pane.backgroundProperty()));
|
||||
|
||||
((Pane) pickerDecorator.lookup(".jfx-decorator-content-container")).borderProperty()
|
||||
.bind(Bindings.createObjectBinding(() -> {
|
||||
return new Border(new BorderStroke(
|
||||
pane.getBackground()
|
||||
.getFills()
|
||||
.get(0)
|
||||
.getFill(),
|
||||
BorderStrokeStyle.SOLID,
|
||||
CornerRadii.EMPTY,
|
||||
new BorderWidths(0,
|
||||
4,
|
||||
4,
|
||||
4)));
|
||||
}, pane.backgroundProperty()));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
container.getChildren().add(tabs);
|
||||
|
||||
this.getChildren().add(container);
|
||||
this.setPadding(new Insets(0));
|
||||
|
||||
dialog.setScene(customScene);
|
||||
final EventHandler<KeyEvent> keyEventListener = key -> {
|
||||
switch (key.getCode()) {
|
||||
case ESCAPE:
|
||||
close();
|
||||
break;
|
||||
case ENTER:
|
||||
updateColor();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
dialog.addEventHandler(KeyEvent.ANY, keyEventListener);
|
||||
}
|
||||
|
||||
private void updateColor() {
|
||||
close();
|
||||
this.customColorProperty.set(curvedColorPicker.getColor(curvedColorPicker.getSelectedIndex()));
|
||||
this.onSave.run();
|
||||
}
|
||||
|
||||
private void updateColorFromUserInput(String colorWebString) {
|
||||
if (!systemChange) {
|
||||
userChange = true;
|
||||
try {
|
||||
curvedColorPicker.setColor(Color.valueOf(colorWebString));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// if color is not valid then do nothing
|
||||
}
|
||||
userChange = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void close() {
|
||||
dialog.setScene(null);
|
||||
dialog.close();
|
||||
}
|
||||
|
||||
public void setCurrentColor(Color currentColor) {
|
||||
this.currentColorProperty.set(currentColor);
|
||||
if (curvedColorPicker != null && currentColor != null) {
|
||||
curvedColorPicker.setColor(currentColor);
|
||||
}
|
||||
}
|
||||
|
||||
Color getCurrentColor() {
|
||||
return currentColorProperty.get();
|
||||
}
|
||||
|
||||
ObjectProperty<Color> customColorProperty() {
|
||||
return customColorProperty;
|
||||
}
|
||||
|
||||
void setCustomColor(Color color) {
|
||||
customColorProperty.set(color);
|
||||
}
|
||||
|
||||
Color getCustomColor() {
|
||||
return customColorProperty.get();
|
||||
}
|
||||
|
||||
public Runnable getOnSave() {
|
||||
return onSave;
|
||||
}
|
||||
|
||||
public void setOnSave(Runnable onSave) {
|
||||
this.onSave = onSave;
|
||||
}
|
||||
|
||||
public void setOnHidden(EventHandler<WindowEvent> onHidden) {
|
||||
dialog.setOnHidden(onHidden);
|
||||
}
|
||||
|
||||
public void show() {
|
||||
dialog.setOpacity(0);
|
||||
// pickerDecorator.setOpacity(0);
|
||||
if (dialog.getOwner() != null) {
|
||||
dialog.widthProperty().addListener(positionAdjuster);
|
||||
dialog.heightProperty().addListener(positionAdjuster);
|
||||
positionAdjuster.invalidated(null);
|
||||
}
|
||||
if (dialog.getScene() == null) {
|
||||
dialog.setScene(customScene);
|
||||
}
|
||||
curvedColorPicker.preAnimate();
|
||||
dialog.show();
|
||||
if (initOnce) {
|
||||
initRun.run();
|
||||
initOnce = false;
|
||||
}
|
||||
|
||||
Timeline timeline = new Timeline(new KeyFrame(Duration.millis(120),
|
||||
new KeyValue(dialog.opacityProperty(),
|
||||
1,
|
||||
Interpolator.EASE_BOTH)));
|
||||
timeline.setOnFinished((finish) -> curvedColorPicker.animate());
|
||||
timeline.play();
|
||||
}
|
||||
|
||||
|
||||
// add option to show color picker using JFX Dialog
|
||||
private InvalidationListener positionAdjuster = new InvalidationListener() {
|
||||
@Override
|
||||
public void invalidated(Observable ignored) {
|
||||
if (Double.isNaN(dialog.getWidth()) || Double.isNaN(dialog.getHeight())) {
|
||||
return;
|
||||
}
|
||||
dialog.widthProperty().removeListener(positionAdjuster);
|
||||
dialog.heightProperty().removeListener(positionAdjuster);
|
||||
fixPosition();
|
||||
}
|
||||
};
|
||||
|
||||
private void fixPosition() {
|
||||
Window w = dialog.getOwner();
|
||||
Screen s = com.sun.javafx.util.Utils.getScreen(w);
|
||||
Rectangle2D sb = s.getBounds();
|
||||
double xR = w.getX() + w.getWidth();
|
||||
double xL = w.getX() - dialog.getWidth();
|
||||
double x;
|
||||
double y;
|
||||
if (sb.getMaxX() >= xR + dialog.getWidth()) {
|
||||
x = xR;
|
||||
} else if (sb.getMinX() <= xL) {
|
||||
x = xL;
|
||||
} else {
|
||||
x = Math.max(sb.getMinX(), sb.getMaxX() - dialog.getWidth());
|
||||
}
|
||||
y = Math.max(sb.getMinY(), Math.min(sb.getMaxY() - dialog.getHeight(), w.getY()));
|
||||
dialog.setX(x);
|
||||
dialog.setY(y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void layoutChildren() {
|
||||
super.layoutChildren();
|
||||
if (dialog.getMinWidth() > 0 && dialog.getMinHeight() > 0) {
|
||||
return;
|
||||
}
|
||||
double minWidth = Math.max(0, computeMinWidth(getHeight()) + (dialog.getWidth() - customScene.getWidth()));
|
||||
double minHeight = Math.max(0, computeMinHeight(getWidth()) + (dialog.getHeight() - customScene.getHeight()));
|
||||
dialog.setMinWidth(minWidth);
|
||||
dialog.setMinHeight(minHeight);
|
||||
}
|
||||
|
||||
private static final Pattern COLOR_CHAR_PATTERN = Pattern.compile("[0-9a-zA-Z#(),%.\\s]*");
|
||||
|
||||
private static TextFormatter<String> colorCharFormatter() {
|
||||
return new TextFormatter<>(change -> {
|
||||
if (!change.isContentChange()) return change;
|
||||
|
||||
String ins = StringUtils.toHalfWidth(change.getText());
|
||||
if (!COLOR_CHAR_PATTERN.matcher(ins).matches()) return null;
|
||||
String full = StringUtils.toHalfWidth(change.getControlNewText());
|
||||
long h = full.chars().filter(c -> c == '#').count();
|
||||
if (h > 1 || (h == 1 && full.indexOf('#') != 0)) return null;
|
||||
change.setText(ins);
|
||||
return change;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.adapters.ReflectionHelper;
|
||||
import com.jfoenix.controls.behavior.JFXGenericPickerBehavior;
|
||||
import com.sun.javafx.binding.ExpressionHelper;
|
||||
import com.sun.javafx.event.EventHandlerManager;
|
||||
import com.sun.javafx.stage.WindowEventDispatcher;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanPropertyBase;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.event.EventType;
|
||||
import javafx.scene.control.ComboBoxBase;
|
||||
import javafx.scene.control.PopupControl;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.skin.ComboBoxBaseSkin;
|
||||
import javafx.scene.control.skin.ComboBoxPopupControl;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.stage.Window;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.VarHandle;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public abstract class JFXGenericPickerSkin<T> extends ComboBoxPopupControl<T> {
|
||||
|
||||
private final EventHandler<MouseEvent> mouseEnteredEventHandler;
|
||||
private final EventHandler<MouseEvent> mousePressedEventHandler;
|
||||
private final EventHandler<MouseEvent> mouseReleasedEventHandler;
|
||||
private final EventHandler<MouseEvent> mouseExitedEventHandler;
|
||||
|
||||
protected JFXGenericPickerBehavior<T> behavior;
|
||||
|
||||
// reference of the arrow button node in getChildren (not the actual field)
|
||||
protected Pane arrowButton;
|
||||
protected PopupControl popup;
|
||||
|
||||
public JFXGenericPickerSkin(ComboBoxBase<T> comboBoxBase) {
|
||||
super(comboBoxBase);
|
||||
behavior = new JFXGenericPickerBehavior<T>(comboBoxBase);
|
||||
|
||||
removeParentFakeFocusListener(comboBoxBase);
|
||||
|
||||
this.mouseEnteredEventHandler = event -> behavior.mouseEntered(event);
|
||||
this.mousePressedEventHandler = event -> {
|
||||
behavior.mousePressed(event);
|
||||
event.consume();
|
||||
};
|
||||
this.mouseReleasedEventHandler = event -> {
|
||||
behavior.mouseReleased(event);
|
||||
event.consume();
|
||||
};
|
||||
this.mouseExitedEventHandler = event -> behavior.mouseExited(event);
|
||||
|
||||
arrowButton = (Pane) getChildren().get(0);
|
||||
|
||||
parentArrowEventHandlerTerminator.accept("mouseEnteredEventHandler", MouseEvent.MOUSE_ENTERED);
|
||||
parentArrowEventHandlerTerminator.accept("mousePressedEventHandler", MouseEvent.MOUSE_PRESSED);
|
||||
parentArrowEventHandlerTerminator.accept("mouseReleasedEventHandler", MouseEvent.MOUSE_RELEASED);
|
||||
parentArrowEventHandlerTerminator.accept("mouseExitedEventHandler", MouseEvent.MOUSE_EXITED);
|
||||
this.unregisterChangeListeners(comboBoxBase.editableProperty());
|
||||
|
||||
updateArrowButtonListeners();
|
||||
registerChangeListener(comboBoxBase.editableProperty(), obs -> {
|
||||
updateArrowButtonListeners();
|
||||
reflectUpdateDisplayArea();
|
||||
});
|
||||
|
||||
removeParentPopupHandlers();
|
||||
|
||||
popup = ReflectionHelper.getFieldContent(ComboBoxPopupControl.class, this, "popup");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
if (this.behavior != null) {
|
||||
this.behavior.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Reflections internal API *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
private final BiConsumer<String, EventType<?>> parentArrowEventHandlerTerminator = (handlerName, eventType) -> {
|
||||
try {
|
||||
EventHandler handler = ReflectionHelper.getFieldContent(ComboBoxBaseSkin.class, this, handlerName);
|
||||
arrowButton.removeEventHandler(eventType, handler);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
};
|
||||
|
||||
private static final VarHandle READ_ONLY_BOOLEAN_PROPERTY_BASE_HELPER =
|
||||
findVarHandle(ReadOnlyBooleanPropertyBase.class, "helper", ExpressionHelper.class);
|
||||
|
||||
/// @author Glavo
|
||||
private static VarHandle findVarHandle(Class<?> targetClass, String fieldName, Class<?> type) {
|
||||
try {
|
||||
return MethodHandles.privateLookupIn(targetClass, MethodHandles.lookup()).findVarHandle(targetClass, fieldName, type);
|
||||
} catch (NoSuchFieldException | IllegalAccessException e) {
|
||||
LOG.warning("Failed to get var handle", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void removeParentFakeFocusListener(ComboBoxBase<T> comboBoxBase) {
|
||||
// handle FakeFocusField cast exception
|
||||
try {
|
||||
final ReadOnlyBooleanProperty focusedProperty = comboBoxBase.focusedProperty();
|
||||
//noinspection unchecked
|
||||
ExpressionHelper<Boolean> value = (ExpressionHelper<Boolean>) READ_ONLY_BOOLEAN_PROPERTY_BASE_HELPER.get(focusedProperty);
|
||||
ChangeListener<? super Boolean>[] changeListeners = ReflectionHelper.getFieldContent(value.getClass(), value, "changeListeners");
|
||||
// remove parent focus listener to prevent editor class cast exception
|
||||
for (int i = changeListeners.length - 1; i > 0; i--) {
|
||||
if (changeListeners[i] != null && changeListeners[i].getClass().getName().contains("ComboBoxPopupControl")) {
|
||||
focusedProperty.removeListener(changeListeners[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void removeParentPopupHandlers() {
|
||||
try {
|
||||
PopupControl popup = ReflectionHelper.invoke(ComboBoxPopupControl.class, this, "getPopup");
|
||||
popup.setOnAutoHide(event -> behavior.onAutoHide(popup));
|
||||
WindowEventDispatcher dispatcher = ReflectionHelper.invoke(Window.class, popup, "getInternalEventDispatcher");
|
||||
Map compositeEventHandlersMap = ReflectionHelper.getFieldContent(EventHandlerManager.class, dispatcher.getEventHandlerManager(), "eventHandlerMap");
|
||||
compositeEventHandlersMap.remove(MouseEvent.MOUSE_CLICKED);
|
||||
// CompositeEventHandler compositeEventHandler = (CompositeEventHandler) compositeEventHandlersMap.get(MouseEvent.MOUSE_CLICKED);
|
||||
// Object obj = fieldConsumer.apply(()->CompositeEventHandler.class.getDeclaredField("firstRecord"),compositeEventHandler);
|
||||
// EventHandler handler = (EventHandler) fieldConsumer.apply(() -> obj.getClass().getDeclaredField("eventHandler"), obj);
|
||||
// popup.removeEventHandler(MouseEvent.MOUSE_CLICKED, handler);
|
||||
popup.addEventHandler(MouseEvent.MOUSE_CLICKED, click -> behavior.onAutoHide(popup));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateArrowButtonListeners() {
|
||||
if (getSkinnable().isEditable()) {
|
||||
arrowButton.addEventHandler(MouseEvent.MOUSE_ENTERED, mouseEnteredEventHandler);
|
||||
arrowButton.addEventHandler(MouseEvent.MOUSE_PRESSED, mousePressedEventHandler);
|
||||
arrowButton.addEventHandler(MouseEvent.MOUSE_RELEASED, mouseReleasedEventHandler);
|
||||
arrowButton.addEventHandler(MouseEvent.MOUSE_EXITED, mouseExitedEventHandler);
|
||||
} else {
|
||||
arrowButton.removeEventHandler(MouseEvent.MOUSE_ENTERED, mouseEnteredEventHandler);
|
||||
arrowButton.removeEventHandler(MouseEvent.MOUSE_PRESSED, mousePressedEventHandler);
|
||||
arrowButton.removeEventHandler(MouseEvent.MOUSE_RELEASED, mouseReleasedEventHandler);
|
||||
arrowButton.removeEventHandler(MouseEvent.MOUSE_EXITED, mouseExitedEventHandler);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Reflections internal API for ComboBoxPopupControl *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
private final HashMap<String, Method> parentCachedMethods = new HashMap<>();
|
||||
|
||||
Function<String, Method> methodSupplier = name -> {
|
||||
if (!parentCachedMethods.containsKey(name)) {
|
||||
try {
|
||||
Method method = ComboBoxPopupControl.class.getDeclaredMethod(name);
|
||||
method.setAccessible(true);
|
||||
parentCachedMethods.put(name, method);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return parentCachedMethods.get(name);
|
||||
};
|
||||
|
||||
final Consumer<Method> methodInvoker = method -> {
|
||||
try {
|
||||
method.invoke(this);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
};
|
||||
|
||||
final Function<Method, Object> methodReturnInvoker = method -> {
|
||||
try {
|
||||
return method.invoke(this);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
protected void reflectUpdateDisplayArea() {
|
||||
methodInvoker.accept(methodSupplier.apply("updateDisplayArea"));
|
||||
}
|
||||
|
||||
protected void reflectSetTextFromTextFieldIntoComboBoxValue() {
|
||||
methodInvoker.accept(methodSupplier.apply("setTextFromTextFieldIntoComboBoxValue"));
|
||||
}
|
||||
|
||||
protected TextField reflectGetEditableInputNode() {
|
||||
return (TextField) methodReturnInvoker.apply(methodSupplier.apply("getEditableInputNode"));
|
||||
}
|
||||
|
||||
protected void reflectUpdateDisplayNode() {
|
||||
methodInvoker.accept(methodSupplier.apply("updateDisplayNode"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copy from https://github.com/sshahine/JFoenix/blob/d427fd801a338f934307ba41ce604eb5c79f0b20/jfoenix/src/main/java/com/jfoenix/skins/JFXListViewSkin.java
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.skin.ListViewSkin;
|
||||
import javafx.scene.control.skin.VirtualFlow;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public class JFXListViewSkin<T> extends ListViewSkin<T> {
|
||||
|
||||
public JFXListViewSkin(final JFXListView<T> listView) {
|
||||
super(listView);
|
||||
VirtualFlow<ListCell<T>> flow = getVirtualFlow();
|
||||
FXUtils.onChangeAndOperate(listView.depthProperty(), depth -> JFXDepthManager.setDepth(flow, depth.intValue()));
|
||||
|
||||
if (!Boolean.TRUE.equals(listView.getProperties().get("no-smooth-scrolling"))) {
|
||||
FXUtils.smoothScrolling(flow);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
return 200;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (c) 2016 JFoenix
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import com.jfoenix.controls.JFXPopup.PopupHPosition;
|
||||
import com.jfoenix.controls.JFXPopup.PopupVPosition;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.animation.*;
|
||||
import javafx.animation.Animation.Status;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.transform.Scale;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
|
||||
/// # Material Design Popup Skin
|
||||
/// TODO: REWORK
|
||||
///
|
||||
/// @author Shadi Shaheen
|
||||
/// @version 2.0
|
||||
/// @since 2017-03-01
|
||||
public class JFXPopupSkin implements Skin<JFXPopup> {
|
||||
|
||||
protected JFXPopup control;
|
||||
protected StackPane container = new StackPane();
|
||||
protected Region popupContent;
|
||||
protected Node root;
|
||||
|
||||
private Animation animation;
|
||||
protected Scale scale;
|
||||
|
||||
public JFXPopupSkin(JFXPopup control) {
|
||||
this.control = control;
|
||||
// set scale y to 0.01 instead of 0 to allow layout of the content,
|
||||
// otherwise it will cause exception in traverse engine, when focusing the 1st node
|
||||
scale = new Scale(1.0, 0.01, 0, 0);
|
||||
popupContent = control.getPopupContent();
|
||||
container.getStyleClass().add("jfx-popup-container");
|
||||
container.getChildren().add(popupContent);
|
||||
container.getTransforms().add(scale);
|
||||
container.setOpacity(0);
|
||||
root = JFXDepthManager.createMaterialNode(container, 4);
|
||||
animation = AnimationUtils.isAnimationEnabled() ? getAnimation() : null;
|
||||
}
|
||||
|
||||
public void reset(PopupVPosition vAlign, PopupHPosition hAlign, double offsetX, double offsetY) {
|
||||
// postion the popup according to its animation
|
||||
scale.setPivotX(hAlign == PopupHPosition.RIGHT ? container.getWidth() : 0);
|
||||
scale.setPivotY(vAlign == PopupVPosition.BOTTOM ? container.getHeight() : 0);
|
||||
control.setX(control.getX() + (hAlign == PopupHPosition.RIGHT ? -container.getWidth() + offsetX : offsetX));
|
||||
control.setY(control.getY() + (vAlign == PopupVPosition.BOTTOM ? -container.getHeight() + offsetY : offsetY));
|
||||
}
|
||||
|
||||
public final void animate() {
|
||||
if (animation != null) {
|
||||
if (animation.getStatus() == Status.STOPPED) {
|
||||
container.setOpacity(1);
|
||||
animation.playFromStart();
|
||||
}
|
||||
} else {
|
||||
container.setOpacity(1);
|
||||
popupContent.setOpacity(1);
|
||||
scale.setX(1.0);
|
||||
scale.setY(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JFXPopup getSkinnable() {
|
||||
return control;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node getNode() {
|
||||
return root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
if (animation != null) {
|
||||
animation.stop();
|
||||
animation = null;
|
||||
}
|
||||
container = null;
|
||||
control = null;
|
||||
popupContent = null;
|
||||
root = null;
|
||||
}
|
||||
|
||||
protected Animation getAnimation() {
|
||||
Interpolator interpolator = Motion.EASE;
|
||||
return new Timeline(
|
||||
new KeyFrame(
|
||||
Duration.ZERO,
|
||||
new KeyValue(popupContent.opacityProperty(), 0, interpolator),
|
||||
new KeyValue(scale.xProperty(), 0, interpolator),
|
||||
new KeyValue(scale.yProperty(), 0, interpolator)
|
||||
),
|
||||
new KeyFrame(Motion.SHORT4,
|
||||
new KeyValue(popupContent.opacityProperty(), 0, interpolator),
|
||||
new KeyValue(scale.xProperty(), 1, interpolator)
|
||||
),
|
||||
new KeyFrame(Motion.MEDIUM2,
|
||||
new KeyValue(popupContent.opacityProperty(), 1, interpolator),
|
||||
new KeyValue(scale.yProperty(), 1, interpolator)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public void init() {
|
||||
if (animation != null)
|
||||
animation.stop();
|
||||
container.setOpacity(0);
|
||||
scale.setX(1.0);
|
||||
scale.setY(0.01);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.controls.JFXProgressBar;
|
||||
import com.jfoenix.utils.TreeShowingProperty;
|
||||
import javafx.animation.*;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
|
||||
/// # Material Design ProgressBar Skin
|
||||
///
|
||||
/// @author Shadi Shaheen
|
||||
/// @version 2.0
|
||||
/// @since 2017-10-06
|
||||
public class JFXProgressBarSkin extends SkinBase<JFXProgressBar> {
|
||||
|
||||
private static final double DEFAULT_HEIGHT = 4;
|
||||
|
||||
private final StackPane track;
|
||||
private final StackPane bar;
|
||||
private final Rectangle clip;
|
||||
private Animation transition;
|
||||
private final TreeShowingProperty treeShowingProperty;
|
||||
private double fullWidth;
|
||||
|
||||
public JFXProgressBarSkin(JFXProgressBar control) {
|
||||
super(control);
|
||||
|
||||
this.treeShowingProperty = new TreeShowingProperty(control);
|
||||
|
||||
registerChangeListener(treeShowingProperty, obs -> updateProgress(false));
|
||||
registerChangeListener(control.progressProperty(), obs -> updateProgress(true));
|
||||
|
||||
track = new StackPane();
|
||||
track.getStyleClass().setAll("track");
|
||||
|
||||
bar = new StackPane();
|
||||
bar.getStyleClass().setAll("bar");
|
||||
|
||||
clip = new Rectangle();
|
||||
clip.setArcWidth(DEFAULT_HEIGHT);
|
||||
clip.setArcHeight(DEFAULT_HEIGHT);
|
||||
bar.setClip(clip);
|
||||
|
||||
getChildren().setAll(track, bar);
|
||||
|
||||
getSkinnable().requestLayout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
return Node.BASELINE_OFFSET_SAME_AS_HEIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
return Math.max(100, leftInset + bar.prefWidth(getSkinnable().getWidth()) + rightInset);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
return topInset + DEFAULT_HEIGHT + bottomInset;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
return getSkinnable().prefWidth(height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
return getSkinnable().prefHeight(width);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren(double x, double y, double w, double h) {
|
||||
track.resizeRelocate(x, y, w, h);
|
||||
bar.resizeRelocate(x, y, w, h);
|
||||
|
||||
clip.relocate(0, 0);
|
||||
clip.setWidth(0);
|
||||
clip.setHeight(h);
|
||||
clip.setTranslateX(0);
|
||||
|
||||
fullWidth = w;
|
||||
|
||||
clearAnimation();
|
||||
updateProgress(false);
|
||||
}
|
||||
|
||||
private boolean wasIndeterminate = false;
|
||||
|
||||
private void updateProgress(boolean playProgressAnimation) {
|
||||
double progress = Math.min(getSkinnable().getProgress(), 1.0);
|
||||
boolean isIndeterminate = progress < 0.0;
|
||||
boolean isTreeShowing = treeShowingProperty.get();
|
||||
|
||||
if (isIndeterminate != wasIndeterminate) {
|
||||
wasIndeterminate = isIndeterminate;
|
||||
clearAnimation();
|
||||
clip.setTranslateX(0);
|
||||
}
|
||||
|
||||
if (isIndeterminate) { // indeterminate
|
||||
if (isTreeShowing) {
|
||||
if (transition == null) {
|
||||
transition = createIndeterminateTransition();
|
||||
transition.playFromStart();
|
||||
} else {
|
||||
transition.play();
|
||||
}
|
||||
} else if (transition != null) {
|
||||
transition.pause();
|
||||
}
|
||||
} else { // determinate
|
||||
clearAnimation();
|
||||
if (isTreeShowing && playProgressAnimation
|
||||
&& AnimationUtils.isAnimationEnabled()
|
||||
&& getSkinnable().isSmoothProgress()) {
|
||||
transition = createDeterminateTransition(progress);
|
||||
transition.playFromStart();
|
||||
} else {
|
||||
clip.setWidth(computeBarWidth(progress));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final Duration INDETERMINATE_DURATION = Duration.seconds(1);
|
||||
|
||||
private Transition createIndeterminateTransition() {
|
||||
double minWidth = 0;
|
||||
double maxWidth = fullWidth * 0.4;
|
||||
Transition transition = new Transition() {
|
||||
{
|
||||
setInterpolator(Interpolator.LINEAR);
|
||||
setCycleDuration(INDETERMINATE_DURATION);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void interpolate(double frac) {
|
||||
double currentWidth;
|
||||
|
||||
if (frac <= 0.5) {
|
||||
currentWidth = Interpolator.EASE_IN.interpolate(minWidth, maxWidth, frac / 0.5);
|
||||
} else {
|
||||
currentWidth = Interpolator.EASE_OUT.interpolate(maxWidth, minWidth, (frac - 0.5) / 0.5);
|
||||
}
|
||||
|
||||
double targetCenter;
|
||||
if (frac <= 0.1) {
|
||||
targetCenter = 0.0;
|
||||
} else if (frac >= 0.9) {
|
||||
targetCenter = fullWidth;
|
||||
} else {
|
||||
targetCenter = ((frac - 0.1) / 0.8) * fullWidth;
|
||||
}
|
||||
|
||||
clip.setWidth(currentWidth);
|
||||
clip.setTranslateX(targetCenter - currentWidth / 2.0);
|
||||
}
|
||||
};
|
||||
|
||||
transition.setCycleCount(Timeline.INDEFINITE);
|
||||
return transition;
|
||||
}
|
||||
|
||||
private static final Duration DETERMINATE_DURATION = Duration.seconds(0.2);
|
||||
|
||||
private Timeline createDeterminateTransition(double targetProgress) {
|
||||
Timeline timeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(clip.widthProperty(), clip.getWidth(), Interpolator.EASE_OUT)),
|
||||
new KeyFrame(DETERMINATE_DURATION,
|
||||
new KeyValue(clip.widthProperty(), computeBarWidth(targetProgress), Interpolator.EASE_OUT))
|
||||
);
|
||||
timeline.setOnFinished(e -> {
|
||||
if (transition == timeline) {
|
||||
transition = null;
|
||||
}
|
||||
});
|
||||
return timeline;
|
||||
}
|
||||
|
||||
private void clearAnimation() {
|
||||
if (transition != null) {
|
||||
transition.stop();
|
||||
transition = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
treeShowingProperty.dispose();
|
||||
clearAnimation();
|
||||
}
|
||||
|
||||
private double computeBarWidth(double progress) {
|
||||
assert progress >= 0 && progress <= 1;
|
||||
double barWidth = ((int) fullWidth * 2 * progress) / 2.0;
|
||||
return progress > 0 ? Math.max(barWidth, DEFAULT_HEIGHT) : barWidth;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
//
|
||||
// Source code recreated from a .class file by IntelliJ IDEA
|
||||
// (powered by FernFlower decompiler)
|
||||
//
|
||||
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.controls.JFXRadioButton;
|
||||
import com.jfoenix.controls.JFXRippler;
|
||||
import com.jfoenix.controls.JFXRippler.RipplerMask;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.VPos;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.skin.RadioButtonSkin;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Circle;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
|
||||
public class JFXRadioButtonSkin extends RadioButtonSkin {
|
||||
private static final double PADDING = 15.0;
|
||||
|
||||
private boolean invalid = true;
|
||||
private final JFXRippler rippler;
|
||||
private final Circle radio;
|
||||
private final Circle dot;
|
||||
private Timeline timeline;
|
||||
private final AnchorPane container = new AnchorPane();
|
||||
private final double labelOffset = -10.0;
|
||||
|
||||
public JFXRadioButtonSkin(JFXRadioButton control) {
|
||||
super(control);
|
||||
double radioRadius = 7.0;
|
||||
this.radio = new Circle(radioRadius);
|
||||
this.radio.getStyleClass().setAll("radio");
|
||||
this.radio.setStrokeWidth(2.0);
|
||||
this.radio.setFill(Color.TRANSPARENT);
|
||||
|
||||
this.dot = new Circle(4);
|
||||
this.dot.getStyleClass().setAll("dot");
|
||||
this.dot.fillProperty().bind(control.selectedColorProperty());
|
||||
this.dot.setScaleX(0.0);
|
||||
this.dot.setScaleY(0.0);
|
||||
|
||||
StackPane boxContainer = new StackPane();
|
||||
boxContainer.getChildren().addAll(this.radio, this.dot);
|
||||
boxContainer.setPadding(new Insets(PADDING));
|
||||
this.rippler = new JFXRippler(boxContainer, RipplerMask.CIRCLE);
|
||||
this.container.getChildren().add(this.rippler);
|
||||
AnchorPane.setRightAnchor(this.rippler, this.labelOffset);
|
||||
|
||||
this.updateChildren();
|
||||
ReadOnlyBooleanProperty focusVisibleProperty = FXUtils.focusVisibleProperty(control);
|
||||
if (focusVisibleProperty == null) {
|
||||
focusVisibleProperty = control.focusedProperty();
|
||||
}
|
||||
|
||||
focusVisibleProperty.addListener((o, oldVal, newVal) -> {
|
||||
if (newVal) {
|
||||
if (!this.getSkinnable().isPressed()) {
|
||||
this.rippler.showOverlay();
|
||||
}
|
||||
} else {
|
||||
this.rippler.hideOverlay();
|
||||
}
|
||||
});
|
||||
control.pressedProperty().addListener((o, oldVal, newVal) -> this.rippler.hideOverlay());
|
||||
this.registerChangeListener(control.selectedColorProperty(), ignored -> updateColors());
|
||||
this.registerChangeListener(control.unSelectedColorProperty(), ignored -> updateColors());
|
||||
this.registerChangeListener(control.selectedProperty(), ignored -> {
|
||||
updateColors();
|
||||
this.playAnimation();
|
||||
});
|
||||
}
|
||||
|
||||
protected void updateChildren() {
|
||||
super.updateChildren();
|
||||
if (this.radio != null) {
|
||||
this.removeRadio();
|
||||
this.getChildren().add(this.container);
|
||||
}
|
||||
}
|
||||
|
||||
protected void layoutChildren(double x, double y, double w, double h) {
|
||||
RadioButton radioButton = this.getSkinnable();
|
||||
double contWidth = this.snapSizeX(this.container.prefWidth(-1.0)) + (double) (this.invalid ? 2 : 0);
|
||||
double contHeight = this.snapSizeY(this.container.prefHeight(-1.0)) + (double) (this.invalid ? 2 : 0);
|
||||
double computeWidth = Math.min(radioButton.prefWidth(-1.0), radioButton.minWidth(-1.0)) + this.labelOffset + 2.0 * this.PADDING;
|
||||
double labelWidth = Math.min(computeWidth - contWidth, w - this.snapSizeX(contWidth)) + this.labelOffset + 2.0 * PADDING;
|
||||
double labelHeight = Math.min(radioButton.prefHeight(labelWidth), h);
|
||||
double maxHeight = Math.max(contHeight, labelHeight);
|
||||
double xOffset = computeXOffset(w, labelWidth + contWidth, radioButton.getAlignment().getHpos()) + x;
|
||||
double yOffset = computeYOffset(h, maxHeight, radioButton.getAlignment().getVpos()) + x;
|
||||
if (this.invalid) {
|
||||
this.initializeComponents();
|
||||
this.invalid = false;
|
||||
}
|
||||
|
||||
this.layoutLabelInArea(xOffset + contWidth, yOffset, labelWidth, maxHeight, radioButton.getAlignment());
|
||||
((Text) this.getChildren().get(this.getChildren().get(0) instanceof Text ? 0 : 1)).textProperty().set(this.getSkinnable().textProperty().get());
|
||||
this.container.resize(this.snapSizeX(contWidth), this.snapSizeY(contHeight));
|
||||
this.positionInArea(this.container, xOffset, yOffset, contWidth, maxHeight, 0.0, radioButton.getAlignment().getHpos(), radioButton.getAlignment().getVpos());
|
||||
}
|
||||
|
||||
private void initializeComponents() {
|
||||
this.updateColors();
|
||||
this.playAnimation();
|
||||
}
|
||||
|
||||
private void playAnimation() {
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
if (this.timeline == null) {
|
||||
this.timeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(this.dot.scaleXProperty(), 0, Interpolator.EASE_BOTH),
|
||||
new KeyValue(this.dot.scaleYProperty(), 0, Interpolator.EASE_BOTH)),
|
||||
new KeyFrame(Duration.millis(200.0),
|
||||
new KeyValue(this.dot.scaleXProperty(), 1, Interpolator.EASE_BOTH),
|
||||
new KeyValue(this.dot.scaleYProperty(), 1, Interpolator.EASE_BOTH))
|
||||
);
|
||||
} else {
|
||||
this.timeline.stop();
|
||||
}
|
||||
this.timeline.setRate(this.getSkinnable().isSelected() ? 1.0 : -1.0);
|
||||
this.timeline.play();
|
||||
} else {
|
||||
double endScale = this.getSkinnable().isSelected() ? 1.0 : 0.0;
|
||||
this.dot.setScaleX(endScale);
|
||||
this.dot.setScaleY(endScale);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeRadio() {
|
||||
this.getChildren().removeIf(node -> "radio".equals(node.getStyleClass().get(0)));
|
||||
}
|
||||
|
||||
private void updateColors() {
|
||||
var control = (JFXRadioButton) getSkinnable();
|
||||
boolean isSelected = control.isSelected();
|
||||
Color unSelectedColor = control.getUnSelectedColor();
|
||||
Color selectedColor = control.getSelectedColor();
|
||||
rippler.setRipplerFill(isSelected ? selectedColor : unSelectedColor);
|
||||
radio.setStroke(isSelected ? selectedColor : unSelectedColor);
|
||||
}
|
||||
|
||||
protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
return super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset) + this.snapSizeX(this.radio.minWidth(-1.0)) + this.labelOffset + 2.0 * PADDING;
|
||||
}
|
||||
|
||||
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
return super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset) + this.snapSizeX(this.radio.prefWidth(-1.0)) + this.labelOffset + 2.0 * PADDING;
|
||||
}
|
||||
|
||||
static double computeXOffset(double width, double contentWidth, HPos hpos) {
|
||||
return switch (hpos) {
|
||||
case LEFT -> 0.0;
|
||||
case CENTER -> (width - contentWidth) / 2.0;
|
||||
case RIGHT -> width - contentWidth;
|
||||
};
|
||||
}
|
||||
|
||||
static double computeYOffset(double height, double contentHeight, VPos vpos) {
|
||||
return switch (vpos) {
|
||||
case TOP, BASELINE -> 0.0;
|
||||
case CENTER -> (height - contentHeight) / 2.0;
|
||||
case BOTTOM -> height - contentHeight;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.controls.JFXSlider;
|
||||
import com.jfoenix.controls.JFXSlider.IndicatorPosition;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.chart.NumberAxis;
|
||||
import javafx.scene.control.Slider;
|
||||
import javafx.scene.control.skin.SliderSkin;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.util.Duration;
|
||||
|
||||
/// # Material Design Slider Skin
|
||||
///
|
||||
/// rework of JFXSliderSkin by extending Java SliderSkin
|
||||
/// this solves padding and resizing issues
|
||||
///
|
||||
/// @author Shadi Shaheen
|
||||
/// @version 1.0
|
||||
/// @since 2016-03-09
|
||||
public class JFXSliderSkin extends SliderSkin {
|
||||
|
||||
private static final PseudoClass MIN_VALUE = PseudoClass.getPseudoClass("min");
|
||||
private static final PseudoClass MAX_VALUE = PseudoClass.getPseudoClass("max");
|
||||
|
||||
private final Pane mouseHandlerPane = new Pane();
|
||||
private final Text sliderValue;
|
||||
private final StackPane coloredTrack;
|
||||
private final StackPane thumb;
|
||||
private final StackPane track;
|
||||
private final StackPane animatedThumb;
|
||||
private NumberAxis tickLine;
|
||||
|
||||
private Timeline timeline;
|
||||
|
||||
private double indicatorRotation;
|
||||
private double horizontalRotation;
|
||||
private double shifting;
|
||||
|
||||
public JFXSliderSkin(JFXSlider slider) {
|
||||
super(slider);
|
||||
|
||||
track = (StackPane) getSkinnable().lookup(".track");
|
||||
thumb = (StackPane) getSkinnable().lookup(".thumb");
|
||||
tickLine = (NumberAxis) getSkinnable().lookup(".axis");
|
||||
if (tickLine != null) tickLine.setAnimated(false);
|
||||
|
||||
coloredTrack = new StackPane();
|
||||
coloredTrack.getStyleClass().add("colored-track");
|
||||
coloredTrack.setMouseTransparent(true);
|
||||
|
||||
sliderValue = new Text();
|
||||
sliderValue.getStyleClass().setAll("slider-value");
|
||||
|
||||
animatedThumb = new StackPane();
|
||||
animatedThumb.getStyleClass().add("animated-thumb");
|
||||
animatedThumb.getChildren().add(sliderValue);
|
||||
animatedThumb.setMouseTransparent(true);
|
||||
animatedThumb.setScaleX(0);
|
||||
animatedThumb.setScaleY(0);
|
||||
|
||||
thumb.layoutXProperty().addListener(x -> {
|
||||
if (slider.getOrientation() == Orientation.VERTICAL) initAnimation(Orientation.VERTICAL);
|
||||
});
|
||||
thumb.layoutYProperty().addListener(y -> {
|
||||
if (slider.getOrientation() == Orientation.HORIZONTAL) initAnimation(Orientation.HORIZONTAL);
|
||||
});
|
||||
|
||||
addJFXChildren();
|
||||
getChildren().addListener((ListChangeListener<Node>) c -> {
|
||||
while (c.next()) {
|
||||
if (c.wasAdded()) {
|
||||
c.getAddedSubList().forEach(added -> {
|
||||
if (added instanceof NumberAxis) {
|
||||
tickLine = (NumberAxis) added;
|
||||
tickLine.setAnimated(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
registerChangeListener(slider.showTickMarksProperty(), e -> addJFXChildren());
|
||||
registerChangeListener(slider.showTickLabelsProperty(), e -> addJFXChildren());
|
||||
registerChangeListener(slider.valueFactoryProperty(), obs -> refreshSliderValueBinding());
|
||||
|
||||
initListeners();
|
||||
}
|
||||
|
||||
private void addJFXChildren() {
|
||||
ObservableList<Node> children = getChildren();
|
||||
Slider slider = getSkinnable();
|
||||
if ((slider.isShowTickMarks() || slider.isShowTickLabels()) && tickLine != null && !children.contains(tickLine)) {
|
||||
children.add(0, tickLine);
|
||||
}
|
||||
if (children.contains(coloredTrack)) return;
|
||||
children.add(children.indexOf(thumb), coloredTrack);
|
||||
children.add(children.indexOf(thumb), animatedThumb);
|
||||
children.add(0, mouseHandlerPane);
|
||||
}
|
||||
|
||||
private void refreshSliderValueBinding() {
|
||||
sliderValue.textProperty().unbind();
|
||||
if (((JFXSlider) getSkinnable()).getValueFactory() != null) {
|
||||
sliderValue.textProperty()
|
||||
.bind(((JFXSlider) getSkinnable()).getValueFactory().call((JFXSlider) getSkinnable()));
|
||||
} else {
|
||||
sliderValue.textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
if (getSkinnable().getLabelFormatter() != null) {
|
||||
return getSkinnable().getLabelFormatter().toString(getSkinnable().getValue());
|
||||
} else {
|
||||
return String.valueOf(Math.round(getSkinnable().getValue()));
|
||||
}
|
||||
}, getSkinnable().valueProperty()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren(double x, double y, double w, double h) {
|
||||
super.layoutChildren(x, y, w, h);
|
||||
|
||||
if (timeline == null) {
|
||||
initAnimation(getSkinnable().getOrientation());
|
||||
}
|
||||
|
||||
double prefWidth = animatedThumb.prefWidth(-1);
|
||||
animatedThumb.resize(prefWidth, animatedThumb.prefHeight(prefWidth));
|
||||
|
||||
boolean horizontal = getSkinnable().getOrientation() == Orientation.HORIZONTAL;
|
||||
double width, height, layoutX, layoutY;
|
||||
if (horizontal) {
|
||||
width = thumb.getLayoutX() - snappedLeftInset();
|
||||
height = track.getHeight();
|
||||
layoutX = track.getLayoutX();
|
||||
layoutY = track.getLayoutY();
|
||||
animatedThumb.setLayoutX(thumb.getLayoutX() + thumb.getWidth() / 2 - animatedThumb.getWidth() / 2);
|
||||
} else {
|
||||
height = track.getLayoutBounds().getMaxY() + track.getLayoutY() - thumb.getLayoutY() - snappedBottomInset();
|
||||
width = track.getWidth();
|
||||
layoutX = track.getLayoutX();
|
||||
layoutY = thumb.getLayoutY();
|
||||
animatedThumb.setLayoutY(thumb.getLayoutY() + thumb.getHeight() / 2 - animatedThumb.getHeight() / 2);
|
||||
}
|
||||
|
||||
coloredTrack.resizeRelocate(layoutX, layoutY, width, height);
|
||||
mouseHandlerPane.resizeRelocate(x, y, w, h);
|
||||
}
|
||||
|
||||
private void initializeVariables() {
|
||||
shifting = 30 + thumb.getWidth();
|
||||
if (getSkinnable().getOrientation() != Orientation.HORIZONTAL) {
|
||||
horizontalRotation = -90;
|
||||
}
|
||||
if (((JFXSlider) getSkinnable()).getIndicatorPosition() != IndicatorPosition.LEFT) {
|
||||
indicatorRotation = 180;
|
||||
shifting = -shifting;
|
||||
}
|
||||
final double rotationAngle = 45;
|
||||
sliderValue.setRotate(rotationAngle + indicatorRotation + 3 * horizontalRotation);
|
||||
animatedThumb.setRotate(-rotationAngle + indicatorRotation + horizontalRotation);
|
||||
}
|
||||
|
||||
private void initListeners() {
|
||||
// delegate slider mouse events to track node
|
||||
mouseHandlerPane.setOnMousePressed(this::delegateToTrack);
|
||||
mouseHandlerPane.setOnMouseReleased(this::delegateToTrack);
|
||||
mouseHandlerPane.setOnMouseDragged(this::delegateToTrack);
|
||||
|
||||
// animate value node
|
||||
track.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> {
|
||||
timeline.setRate(1);
|
||||
timeline.play();
|
||||
});
|
||||
track.addEventHandler(MouseEvent.MOUSE_RELEASED, (event) -> {
|
||||
timeline.setRate(-1);
|
||||
timeline.play();
|
||||
});
|
||||
thumb.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> {
|
||||
timeline.setRate(1);
|
||||
timeline.play();
|
||||
});
|
||||
thumb.addEventHandler(MouseEvent.MOUSE_RELEASED, (event) -> {
|
||||
timeline.setRate(-1);
|
||||
timeline.play();
|
||||
});
|
||||
|
||||
refreshSliderValueBinding();
|
||||
updateValueStyleClass();
|
||||
|
||||
getSkinnable().valueProperty().addListener(observable -> updateValueStyleClass());
|
||||
}
|
||||
|
||||
private void delegateToTrack(MouseEvent event) {
|
||||
if (!event.isConsumed()) {
|
||||
event.consume();
|
||||
track.fireEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateValueStyleClass() {
|
||||
getSkinnable().pseudoClassStateChanged(MIN_VALUE, getSkinnable().getMin() == getSkinnable().getValue());
|
||||
getSkinnable().pseudoClassStateChanged(MAX_VALUE, getSkinnable().getMax() == getSkinnable().getValue());
|
||||
}
|
||||
|
||||
private void initAnimation(Orientation orientation) {
|
||||
initializeVariables();
|
||||
|
||||
double thumbPos, thumbNewPos;
|
||||
DoubleProperty layoutProperty;
|
||||
|
||||
if (orientation == Orientation.HORIZONTAL) {
|
||||
if (((JFXSlider) getSkinnable()).getIndicatorPosition() == IndicatorPosition.RIGHT) {
|
||||
thumbPos = thumb.getLayoutY() - thumb.getHeight();
|
||||
thumbNewPos = thumbPos - shifting;
|
||||
} else {
|
||||
double height = animatedThumb.prefHeight(animatedThumb.prefWidth(-1));
|
||||
thumbPos = thumb.getLayoutY() - height / 2;
|
||||
thumbNewPos = thumb.getLayoutY() - height - thumb.getHeight();
|
||||
}
|
||||
layoutProperty = animatedThumb.translateYProperty();
|
||||
} else {
|
||||
if (((JFXSlider) getSkinnable()).getIndicatorPosition() == IndicatorPosition.RIGHT) {
|
||||
thumbPos = thumb.getLayoutX() - thumb.getWidth();
|
||||
thumbNewPos = thumbPos - shifting;
|
||||
} else {
|
||||
double width = animatedThumb.prefWidth(-1);
|
||||
thumbPos = thumb.getLayoutX() - width / 2;
|
||||
thumbNewPos = thumb.getLayoutX() - width - thumb.getWidth();
|
||||
}
|
||||
layoutProperty = animatedThumb.translateXProperty();
|
||||
}
|
||||
|
||||
clearAnimation();
|
||||
|
||||
timeline = new Timeline(
|
||||
new KeyFrame(
|
||||
Duration.ZERO,
|
||||
new KeyValue(animatedThumb.scaleXProperty(), 0, Interpolator.EASE_BOTH),
|
||||
new KeyValue(animatedThumb.scaleYProperty(), 0, Interpolator.EASE_BOTH),
|
||||
new KeyValue(layoutProperty, thumbPos, Interpolator.EASE_BOTH)),
|
||||
new KeyFrame(
|
||||
Duration.seconds(0.2),
|
||||
new KeyValue(animatedThumb.scaleXProperty(), 1, Interpolator.EASE_BOTH),
|
||||
new KeyValue(animatedThumb.scaleYProperty(), 1, Interpolator.EASE_BOTH),
|
||||
new KeyValue(layoutProperty, thumbNewPos, Interpolator.EASE_BOTH)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
clearAnimation();
|
||||
}
|
||||
|
||||
private void clearAnimation() {
|
||||
if (timeline != null) {
|
||||
timeline.stop();
|
||||
timeline.getKeyFrames().clear();
|
||||
timeline = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.controls.JFXSpinner;
|
||||
import com.jfoenix.utils.TreeShowingProperty;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Arc;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.shape.StrokeLineCap;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/// JFXSpinner material design skin
|
||||
///
|
||||
/// @author Shadi Shaheen & Gerard Moubarak
|
||||
/// @version 1.0
|
||||
/// @since 2017-09-25
|
||||
public class JFXSpinnerSkin extends SkinBase<JFXSpinner> {
|
||||
|
||||
private static final double DEFAULT_STROKE_WIDTH = 4;
|
||||
|
||||
private JFXSpinner control;
|
||||
private final TreeShowingProperty treeShowingProperty;
|
||||
private boolean isValid = false;
|
||||
|
||||
private Timeline timeline;
|
||||
private Arc arc;
|
||||
private Arc track;
|
||||
private final StackPane arcPane;
|
||||
private final Rectangle fillRect;
|
||||
|
||||
private final double startingAngle;
|
||||
|
||||
public JFXSpinnerSkin(JFXSpinner control) {
|
||||
super(control);
|
||||
|
||||
this.control = control;
|
||||
this.treeShowingProperty = new TreeShowingProperty(control);
|
||||
this.startingAngle = control.getStartingAngle();
|
||||
|
||||
arc = new Arc();
|
||||
arc.setManaged(false);
|
||||
arc.setLength(180);
|
||||
arc.getStyleClass().setAll("arc");
|
||||
arc.setFill(Color.TRANSPARENT);
|
||||
arc.setStrokeWidth(DEFAULT_STROKE_WIDTH);
|
||||
arc.setStrokeLineCap(StrokeLineCap.ROUND);
|
||||
|
||||
track = new Arc();
|
||||
track.setManaged(false);
|
||||
track.setLength(360);
|
||||
track.setStrokeWidth(DEFAULT_STROKE_WIDTH);
|
||||
track.getStyleClass().setAll("track");
|
||||
track.setFill(Color.TRANSPARENT);
|
||||
|
||||
fillRect = new Rectangle();
|
||||
fillRect.setFill(Color.TRANSPARENT);
|
||||
final Group group = new Group(fillRect, track, arc);
|
||||
group.setManaged(false);
|
||||
arcPane = new StackPane(group);
|
||||
arcPane.setPrefSize(50, 50);
|
||||
getChildren().setAll(arcPane);
|
||||
|
||||
// register listeners
|
||||
registerChangeListener(control.progressProperty(), obs -> updateProgress());
|
||||
registerChangeListener(treeShowingProperty, obs -> updateProgress());
|
||||
}
|
||||
|
||||
private void updateProgress() {
|
||||
double progress = Double.min(getSkinnable().getProgress(), 1.0);
|
||||
if (progress < 0) { // indeterminate
|
||||
boolean treeShowing = treeShowingProperty.get();
|
||||
if (treeShowing) {
|
||||
if (timeline == null) {
|
||||
timeline = createTransition();
|
||||
timeline.playFromStart();
|
||||
} else {
|
||||
timeline.play();
|
||||
}
|
||||
} else if (timeline != null) {
|
||||
timeline.pause();
|
||||
}
|
||||
} else { // determinate
|
||||
clearAnimation();
|
||||
arc.setStartAngle(90);
|
||||
arc.setLength(-360 * progress);
|
||||
}
|
||||
}
|
||||
|
||||
private double computeSize() {
|
||||
return control.getRadius() * 2 + arc.getStrokeWidth() * 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
if (Region.USE_COMPUTED_SIZE == control.getRadius()) {
|
||||
return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset);
|
||||
} else {
|
||||
return computeSize();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
if (Region.USE_COMPUTED_SIZE == control.getRadius()) {
|
||||
return super.computeMaxWidth(height, topInset, rightInset, bottomInset, leftInset);
|
||||
} else {
|
||||
return computeSize();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
if (Region.USE_COMPUTED_SIZE == control.getRadius()) {
|
||||
return arcPane.prefWidth(-1);
|
||||
} else {
|
||||
return computeSize();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
if (Region.USE_COMPUTED_SIZE == control.getRadius()) {
|
||||
return arcPane.prefHeight(-1);
|
||||
} else {
|
||||
return computeSize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
|
||||
final double strokeWidth = arc.getStrokeWidth();
|
||||
double radius = control.getRadius();
|
||||
final double arcSize = snapSizeX(radius * 2 + strokeWidth);
|
||||
|
||||
arcPane.resizeRelocate((contentWidth - arcSize) / 2 + 1, (contentHeight - arcSize) / 2 + 1, arcSize, arcSize);
|
||||
updateArcLayout(radius, arcSize);
|
||||
|
||||
fillRect.setWidth(arcSize);
|
||||
fillRect.setHeight(arcSize);
|
||||
|
||||
if (!isValid) {
|
||||
updateProgress();
|
||||
isValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateArcLayout(double radius, double arcSize) {
|
||||
arc.setRadiusX(radius);
|
||||
arc.setRadiusY(radius);
|
||||
arc.setCenterX(arcSize / 2);
|
||||
arc.setCenterY(arcSize / 2);
|
||||
|
||||
track.setRadiusX(radius);
|
||||
track.setRadiusY(radius);
|
||||
track.setCenterX(arcSize / 2);
|
||||
track.setCenterY(arcSize / 2);
|
||||
track.setStrokeWidth(arc.getStrokeWidth());
|
||||
}
|
||||
|
||||
private void addKeyFrames(List<KeyFrame> frames, double angle, double duration) {
|
||||
frames.add(new KeyFrame(Duration.seconds(duration),
|
||||
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
|
||||
new KeyValue(arc.startAngleProperty(),
|
||||
angle + 45 + startingAngle,
|
||||
Interpolator.LINEAR)));
|
||||
frames.add(new KeyFrame(Duration.seconds(duration + 0.4),
|
||||
new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR),
|
||||
new KeyValue(arc.startAngleProperty(),
|
||||
angle + 90 + startingAngle,
|
||||
Interpolator.LINEAR)));
|
||||
frames.add(new KeyFrame(Duration.seconds(duration + 0.7),
|
||||
new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR),
|
||||
new KeyValue(arc.startAngleProperty(),
|
||||
angle + 135 + startingAngle,
|
||||
Interpolator.LINEAR)));
|
||||
frames.add(new KeyFrame(Duration.seconds(duration + 1.1),
|
||||
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
|
||||
new KeyValue(arc.startAngleProperty(),
|
||||
angle + 435 + startingAngle,
|
||||
Interpolator.LINEAR)));
|
||||
}
|
||||
|
||||
private Timeline createTransition() {
|
||||
Timeline timeline;
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
var keyFrames = new ArrayList<KeyFrame>(17);
|
||||
addKeyFrames(keyFrames, 0, 0);
|
||||
addKeyFrames(keyFrames, 450, 1.4);
|
||||
addKeyFrames(keyFrames, 900, 2.8);
|
||||
addKeyFrames(keyFrames, 1350, 4.2);
|
||||
keyFrames.add(new KeyFrame(Duration.seconds(5.6),
|
||||
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
|
||||
new KeyValue(arc.startAngleProperty(),
|
||||
1845 + startingAngle,
|
||||
Interpolator.LINEAR)));
|
||||
|
||||
timeline = new Timeline();
|
||||
timeline.getKeyFrames().setAll(keyFrames);
|
||||
} else {
|
||||
final double arcLength = 250;
|
||||
timeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(arc.startAngleProperty(), 45 + startingAngle, Interpolator.LINEAR),
|
||||
new KeyValue(arc.lengthProperty(), arcLength, Interpolator.DISCRETE)),
|
||||
new KeyFrame(Duration.seconds(1.2),
|
||||
new KeyValue(arc.startAngleProperty(), 45 + 360 + startingAngle, Interpolator.LINEAR),
|
||||
new KeyValue(arc.lengthProperty(), arcLength, Interpolator.DISCRETE))
|
||||
);
|
||||
}
|
||||
|
||||
timeline.setCycleCount(Timeline.INDEFINITE);
|
||||
return timeline;
|
||||
}
|
||||
|
||||
private void clearAnimation() {
|
||||
if (timeline != null) {
|
||||
timeline.stop();
|
||||
timeline = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
treeShowingProperty.dispose();
|
||||
clearAnimation();
|
||||
arc = null;
|
||||
track = null;
|
||||
control = null;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.controls.JFXRippler;
|
||||
import com.jfoenix.controls.JFXRippler.RipplerMask;
|
||||
import com.jfoenix.controls.JFXRippler.RipplerPos;
|
||||
import com.jfoenix.controls.JFXToggleButton;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import com.jfoenix.transitions.JFXAnimationTimer;
|
||||
import com.jfoenix.transitions.JFXKeyFrame;
|
||||
import com.jfoenix.transitions.JFXKeyValue;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.skin.ToggleButtonSkin;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.shape.Circle;
|
||||
import javafx.scene.shape.Line;
|
||||
import javafx.scene.shape.StrokeLineCap;
|
||||
import javafx.util.Duration;
|
||||
|
||||
/**
|
||||
* <h1>Material Design ToggleButton Skin</h1>
|
||||
*
|
||||
* @author Shadi Shaheen
|
||||
* @version 1.0
|
||||
* @since 2016-03-09
|
||||
*/
|
||||
public class JFXToggleButtonSkin extends ToggleButtonSkin {
|
||||
|
||||
|
||||
private Runnable releaseManualRippler = null;
|
||||
|
||||
private JFXAnimationTimer timer;
|
||||
private final Circle circle;
|
||||
private final Line line;
|
||||
|
||||
public JFXToggleButtonSkin(JFXToggleButton toggleButton) {
|
||||
super(toggleButton);
|
||||
|
||||
double circleRadius = toggleButton.getSize();
|
||||
|
||||
line = new Line();
|
||||
line.setStroke(getSkinnable().isSelected() ? toggleButton.getToggleLineColor() : toggleButton.getUnToggleLineColor());
|
||||
line.setStartX(0);
|
||||
line.setStartY(0);
|
||||
line.setEndX(circleRadius * 2 + 2);
|
||||
line.setEndY(0);
|
||||
line.setStrokeWidth(circleRadius * 1.5);
|
||||
line.setStrokeLineCap(StrokeLineCap.ROUND);
|
||||
line.setSmooth(true);
|
||||
|
||||
circle = new Circle();
|
||||
circle.setFill(getSkinnable().isSelected() ? toggleButton.getToggleColor() : toggleButton.getUnToggleColor());
|
||||
circle.setCenterX(-circleRadius);
|
||||
circle.setCenterY(0);
|
||||
circle.setRadius(circleRadius);
|
||||
circle.setSmooth(true);
|
||||
JFXDepthManager.setDepth(circle, 1);
|
||||
|
||||
StackPane circlePane = new StackPane();
|
||||
circlePane.getChildren().add(circle);
|
||||
circlePane.setPadding(new Insets(circleRadius * 1.5));
|
||||
|
||||
JFXRippler rippler = new JFXRippler(circlePane, RipplerMask.CIRCLE, RipplerPos.BACK);
|
||||
rippler.setRipplerFill(getSkinnable().isSelected() ? toggleButton.getToggleLineColor() : toggleButton.getUnToggleLineColor());
|
||||
rippler.setTranslateX(computeTranslation(circleRadius, line));
|
||||
|
||||
final StackPane main = new StackPane();
|
||||
main.getChildren().setAll(line, rippler);
|
||||
main.setCursor(Cursor.HAND);
|
||||
|
||||
// show focus traversal effect
|
||||
getSkinnable().armedProperty().addListener((o, oldVal, newVal) -> {
|
||||
if (newVal) {
|
||||
releaseManualRippler = rippler.createManualRipple();
|
||||
} else if (releaseManualRippler != null) {
|
||||
releaseManualRippler.run();
|
||||
}
|
||||
});
|
||||
toggleButton.focusedProperty().addListener((o, oldVal, newVal) -> {
|
||||
if (!toggleButton.isDisableVisualFocus()) {
|
||||
if (newVal) {
|
||||
if (!getSkinnable().isPressed()) {
|
||||
rippler.setOverlayVisible(true);
|
||||
}
|
||||
} else {
|
||||
rippler.setOverlayVisible(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
toggleButton.pressedProperty().addListener(observable -> rippler.setOverlayVisible(false));
|
||||
|
||||
// add change listener to selected property
|
||||
getSkinnable().selectedProperty().addListener(observable -> {
|
||||
rippler.setRipplerFill(toggleButton.isSelected() ? toggleButton.getToggleLineColor() : toggleButton.getUnToggleLineColor());
|
||||
if (!toggleButton.isDisableAnimation()) {
|
||||
timer.reverseAndContinue();
|
||||
} else {
|
||||
rippler.setTranslateX(computeTranslation(circleRadius, line));
|
||||
}
|
||||
});
|
||||
|
||||
getSkinnable().setGraphic(main);
|
||||
|
||||
timer = new JFXAnimationTimer(
|
||||
new JFXKeyFrame(Duration.millis(100),
|
||||
JFXKeyValue.builder()
|
||||
.setTarget(rippler.translateXProperty())
|
||||
.setEndValueSupplier(() -> computeTranslation(circleRadius, line))
|
||||
.setInterpolator(Interpolator.EASE_BOTH)
|
||||
.setAnimateCondition(() -> !((JFXToggleButton) getSkinnable()).isDisableAnimation())
|
||||
.build(),
|
||||
|
||||
JFXKeyValue.builder()
|
||||
.setTarget(line.strokeProperty())
|
||||
.setEndValueSupplier(() -> getSkinnable().isSelected() ?
|
||||
((JFXToggleButton) getSkinnable()).getToggleLineColor()
|
||||
: ((JFXToggleButton) getSkinnable()).getUnToggleLineColor())
|
||||
.setInterpolator(Interpolator.EASE_BOTH)
|
||||
.setAnimateCondition(() -> !((JFXToggleButton) getSkinnable()).isDisableAnimation())
|
||||
.build(),
|
||||
|
||||
JFXKeyValue.builder()
|
||||
.setTarget(circle.fillProperty())
|
||||
.setEndValueSupplier(() -> getSkinnable().isSelected() ?
|
||||
((JFXToggleButton) getSkinnable()).getToggleColor()
|
||||
: ((JFXToggleButton) getSkinnable()).getUnToggleColor())
|
||||
.setInterpolator(Interpolator.EASE_BOTH)
|
||||
.setAnimateCondition(() -> !((JFXToggleButton) getSkinnable()).isDisableAnimation())
|
||||
.build()
|
||||
)
|
||||
);
|
||||
timer.setCacheNodes(circle, line);
|
||||
|
||||
registerChangeListener(toggleButton.toggleColorProperty(), observableValue -> {
|
||||
if (getSkinnable().isSelected()) {
|
||||
circle.setFill(((JFXToggleButton) getSkinnable()).getToggleColor());
|
||||
}
|
||||
});
|
||||
registerChangeListener(toggleButton.unToggleColorProperty(), observableValue -> {
|
||||
if (!getSkinnable().isSelected()) {
|
||||
circle.setFill(((JFXToggleButton) getSkinnable()).getUnToggleColor());
|
||||
}
|
||||
});
|
||||
registerChangeListener(toggleButton.toggleLineColorProperty(), observableValue -> {
|
||||
if (getSkinnable().isSelected()) {
|
||||
line.setStroke(((JFXToggleButton) getSkinnable()).getToggleLineColor());
|
||||
}
|
||||
});
|
||||
registerChangeListener(toggleButton.unToggleColorProperty(), observableValue -> {
|
||||
if (!getSkinnable().isSelected()) {
|
||||
line.setStroke(((JFXToggleButton) getSkinnable()).getUnToggleLineColor());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private double computeTranslation(double circleRadius, Line line) {
|
||||
return (getSkinnable().isSelected() ? 1 : -1) * ((line.getLayoutBounds().getWidth() / 2) - circleRadius + 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
timer.dispose();
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.transitions;
|
||||
|
||||
import javafx.scene.CacheHint;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class CacheMemento {
|
||||
private boolean cache;
|
||||
private boolean cacheShape;
|
||||
private boolean snapToPixel;
|
||||
private CacheHint cacheHint = CacheHint.DEFAULT;
|
||||
private final Node node;
|
||||
private final AtomicBoolean isCached = new AtomicBoolean(false);
|
||||
|
||||
public CacheMemento(Node node) {
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
/**
|
||||
* this method will cache the node only if it wasn't cached before
|
||||
*/
|
||||
public void cache() {
|
||||
if (!isCached.getAndSet(true)) {
|
||||
this.cache = node.isCache();
|
||||
this.cacheHint = node.getCacheHint();
|
||||
node.setCache(true);
|
||||
node.setCacheHint(CacheHint.SPEED);
|
||||
if (node instanceof Region region) {
|
||||
this.cacheShape = region.isCacheShape();
|
||||
this.snapToPixel = region.isSnapToPixel();
|
||||
region.setCacheShape(true);
|
||||
region.setSnapToPixel(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void restore() {
|
||||
if (isCached.getAndSet(false)) {
|
||||
node.setCache(cache);
|
||||
node.setCacheHint(cacheHint);
|
||||
if (node instanceof Region region) {
|
||||
region.setCacheShape(cacheShape);
|
||||
region.setSnapToPixel(snapToPixel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.transitions;
|
||||
|
||||
import javafx.animation.AnimationTimer;
|
||||
import javafx.beans.value.WritableValue;
|
||||
import javafx.scene.Node;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Custom AnimationTimer that can be created the same way as a timeline,
|
||||
* however it doesn't behave the same yet. it only animates in one direction,
|
||||
* it doesn't support animation 0 -> 1 -> 0.5
|
||||
*
|
||||
* @author Shadi Shaheen
|
||||
* @version 1.0
|
||||
* @since 2017-09-21
|
||||
*/
|
||||
|
||||
public class JFXAnimationTimer extends AnimationTimer {
|
||||
|
||||
private Set<AnimationHandler> animationHandlers = new HashSet<>();
|
||||
private long startTime = -1;
|
||||
private boolean running = false;
|
||||
private List<CacheMemento> caches = new ArrayList<>();
|
||||
private double totalElapsedMilliseconds;
|
||||
|
||||
public JFXAnimationTimer(JFXKeyFrame... keyFrames) {
|
||||
for (JFXKeyFrame keyFrame : keyFrames) {
|
||||
Duration duration = keyFrame.getDuration();
|
||||
final Set<JFXKeyValue<?>> keyValuesSet = keyFrame.getValues();
|
||||
if (!keyValuesSet.isEmpty()) {
|
||||
animationHandlers.add(new AnimationHandler(duration, keyFrame.getAnimateCondition(), keyFrame.getValues()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final HashMap<JFXKeyFrame, AnimationHandler> mutableFrames = new HashMap<>();
|
||||
|
||||
public void addKeyFrame(JFXKeyFrame keyFrame) throws Exception {
|
||||
if (isRunning()) {
|
||||
throw new Exception("Can't update animation timer while running");
|
||||
}
|
||||
Duration duration = keyFrame.getDuration();
|
||||
final Set<JFXKeyValue<?>> keyValuesSet = keyFrame.getValues();
|
||||
if (!keyValuesSet.isEmpty()) {
|
||||
final AnimationHandler handler = new AnimationHandler(duration, keyFrame.getAnimateCondition(), keyFrame.getValues());
|
||||
animationHandlers.add(handler);
|
||||
mutableFrames.put(keyFrame, handler);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeKeyFrame(JFXKeyFrame keyFrame) throws Exception {
|
||||
if (isRunning()) {
|
||||
throw new Exception("Can't update animation timer while running");
|
||||
}
|
||||
AnimationHandler handler = mutableFrames.get(keyFrame);
|
||||
animationHandlers.remove(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
super.start();
|
||||
running = true;
|
||||
startTime = -1;
|
||||
for (AnimationHandler animationHandler : animationHandlers) {
|
||||
animationHandler.init();
|
||||
}
|
||||
for (CacheMemento cache : caches) {
|
||||
cache.cache();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(long now) {
|
||||
startTime = startTime == -1 ? now : startTime;
|
||||
totalElapsedMilliseconds = (now - startTime) / 1000000.0;
|
||||
boolean stop = true;
|
||||
for (AnimationHandler handler : animationHandlers) {
|
||||
handler.animate(totalElapsedMilliseconds);
|
||||
if (!handler.finished) {
|
||||
stop = false;
|
||||
}
|
||||
}
|
||||
if (stop) {
|
||||
this.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* this method will pause the timer and reverse the animation if the timer already
|
||||
* started otherwise it will start the animation.
|
||||
*/
|
||||
public void reverseAndContinue() {
|
||||
if (isRunning()) {
|
||||
super.stop();
|
||||
for (AnimationHandler handler : animationHandlers) {
|
||||
handler.reverse(totalElapsedMilliseconds);
|
||||
}
|
||||
startTime = -1;
|
||||
super.start();
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
super.stop();
|
||||
running = false;
|
||||
for (AnimationHandler handler : animationHandlers) {
|
||||
handler.clear();
|
||||
}
|
||||
for (CacheMemento cache : caches) {
|
||||
cache.restore();
|
||||
}
|
||||
if (onFinished != null) {
|
||||
onFinished.run();
|
||||
}
|
||||
}
|
||||
|
||||
public void applyEndValues() {
|
||||
if (isRunning()) {
|
||||
super.stop();
|
||||
}
|
||||
for (AnimationHandler handler : animationHandlers) {
|
||||
handler.applyEndValues();
|
||||
}
|
||||
startTime = -1;
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
private Runnable onFinished = null;
|
||||
|
||||
public void setOnFinished(Runnable onFinished) {
|
||||
this.onFinished = onFinished;
|
||||
}
|
||||
|
||||
public void setCacheNodes(Node... nodesToCache) {
|
||||
caches.clear();
|
||||
if (nodesToCache != null) {
|
||||
for (Node node : nodesToCache) {
|
||||
caches.add(new CacheMemento(node));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
caches.clear();
|
||||
for (AnimationHandler handler : animationHandlers) {
|
||||
handler.dispose();
|
||||
}
|
||||
animationHandlers.clear();
|
||||
}
|
||||
|
||||
static class AnimationHandler {
|
||||
private final double duration;
|
||||
private double currentDuration;
|
||||
private final Set<JFXKeyValue<?>> keyValues;
|
||||
private Supplier<Boolean> animationCondition = null;
|
||||
private boolean finished = false;
|
||||
|
||||
private final HashMap<WritableValue<?>, Object> initialValuesMap = new HashMap<>();
|
||||
private final HashMap<WritableValue<?>, Object> endValuesMap = new HashMap<>();
|
||||
|
||||
AnimationHandler(Duration duration, Supplier<Boolean> animationCondition, Set<JFXKeyValue<?>> keyValues) {
|
||||
this.duration = duration.toMillis();
|
||||
currentDuration = this.duration;
|
||||
this.keyValues = keyValues;
|
||||
this.animationCondition = animationCondition;
|
||||
}
|
||||
|
||||
public void init() {
|
||||
finished = animationCondition != null && !animationCondition.get();
|
||||
for (JFXKeyValue<?> keyValue : keyValues) {
|
||||
if (keyValue.getTarget() != null) {
|
||||
// replaced putIfAbsent for mobile compatibility
|
||||
if (!initialValuesMap.containsKey(keyValue.getTarget())) {
|
||||
initialValuesMap.put(keyValue.getTarget(), keyValue.getTarget().getValue());
|
||||
}
|
||||
if (!endValuesMap.containsKey(keyValue.getTarget())) {
|
||||
endValuesMap.put(keyValue.getTarget(), keyValue.getEndValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void reverse(double now) {
|
||||
finished = animationCondition != null && !animationCondition.get();
|
||||
currentDuration = duration - (currentDuration - now);
|
||||
// update initial values
|
||||
for (JFXKeyValue<?> keyValue : keyValues) {
|
||||
final WritableValue<?> target = keyValue.getTarget();
|
||||
if (target != null) {
|
||||
initialValuesMap.put(target, target.getValue());
|
||||
endValuesMap.put(target, keyValue.getEndValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// now in milliseconds
|
||||
@SuppressWarnings({"unchecked"})
|
||||
public void animate(double now) {
|
||||
// if animate condition for the key frame is not met then do nothing
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
if (now <= currentDuration) {
|
||||
for (JFXKeyValue<?> keyValue : keyValues) {
|
||||
if (keyValue.isValid()) {
|
||||
@SuppressWarnings("rawtypes") final WritableValue target = keyValue.getTarget();
|
||||
final Object endValue = endValuesMap.get(target);
|
||||
if (endValue != null && target != null && !target.getValue().equals(endValue)) {
|
||||
target.setValue(keyValue.getInterpolator().interpolate(initialValuesMap.get(target), endValue, now / currentDuration));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!finished) {
|
||||
finished = true;
|
||||
for (JFXKeyValue<?> keyValue : keyValues) {
|
||||
if (keyValue.isValid()) {
|
||||
@SuppressWarnings("rawtypes") final WritableValue target = keyValue.getTarget();
|
||||
if (target != null) {
|
||||
// set updated end value instead of cached
|
||||
final Object endValue = keyValue.getEndValue();
|
||||
if (endValue != null) {
|
||||
target.setValue(endValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
currentDuration = duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void applyEndValues() {
|
||||
for (JFXKeyValue<?> keyValue : keyValues) {
|
||||
if (keyValue.isValid()) {
|
||||
@SuppressWarnings("rawtypes") final WritableValue target = keyValue.getTarget();
|
||||
if (target != null) {
|
||||
final Object endValue = keyValue.getEndValue();
|
||||
if (endValue != null && !target.getValue().equals(endValue)) {
|
||||
target.setValue(endValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
initialValuesMap.clear();
|
||||
endValuesMap.clear();
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
clear();
|
||||
keyValues.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.transitions;
|
||||
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* @author Shadi Shaheen
|
||||
* @version 1.0
|
||||
* @since 2017-09-21
|
||||
*/
|
||||
|
||||
public class JFXKeyFrame {
|
||||
|
||||
private Duration duration;
|
||||
private Set<JFXKeyValue<?>> keyValues = new CopyOnWriteArraySet<>();
|
||||
private Supplier<Boolean> animateCondition = null;
|
||||
|
||||
public JFXKeyFrame(Duration duration, JFXKeyValue<?>... keyValues) {
|
||||
this.duration = duration;
|
||||
for (final JFXKeyValue<?> keyValue : keyValues) {
|
||||
if (keyValue != null) {
|
||||
this.keyValues.add(keyValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private JFXKeyFrame() {
|
||||
|
||||
}
|
||||
|
||||
public final Duration getDuration() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
public final Set<JFXKeyValue<?>> getValues() {
|
||||
return keyValues;
|
||||
}
|
||||
|
||||
public Supplier<Boolean> getAnimateCondition() {
|
||||
return animateCondition;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private Duration duration;
|
||||
private final Set<JFXKeyValue<?>> keyValues = new CopyOnWriteArraySet<>();
|
||||
private Supplier<Boolean> animateCondition = null;
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
public Builder setDuration(Duration duration) {
|
||||
this.duration = duration;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setKeyValues(JFXKeyValue<?>... keyValues) {
|
||||
for (final JFXKeyValue<?> keyValue : keyValues) {
|
||||
if (keyValue != null) {
|
||||
this.keyValues.add(keyValue);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setAnimateCondition(Supplier<Boolean> animateCondition) {
|
||||
this.animateCondition = animateCondition;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JFXKeyFrame build() {
|
||||
JFXKeyFrame jFXKeyFrame = new JFXKeyFrame();
|
||||
jFXKeyFrame.duration = this.duration;
|
||||
jFXKeyFrame.keyValues = this.keyValues;
|
||||
jFXKeyFrame.animateCondition = this.animateCondition;
|
||||
return jFXKeyFrame;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.transitions;
|
||||
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.beans.value.WritableValue;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* @author Shadi Shaheen
|
||||
* @version 1.0
|
||||
* @since 2017-09-21
|
||||
*/
|
||||
|
||||
public final class JFXKeyValue<T> {
|
||||
|
||||
private WritableValue<T> target;
|
||||
private Supplier<WritableValue<T>> targetSupplier;
|
||||
private Supplier<T> endValueSupplier;
|
||||
private T endValue;
|
||||
private Supplier<Boolean> animateCondition = () -> true;
|
||||
private Interpolator interpolator;
|
||||
|
||||
private JFXKeyValue() {
|
||||
}
|
||||
|
||||
// this builder is created to ensure type inference from method arguments
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public T getEndValue() {
|
||||
return endValue == null ? endValueSupplier.get() : endValue;
|
||||
}
|
||||
|
||||
public WritableValue<T> getTarget() {
|
||||
return target == null ? targetSupplier.get() : target;
|
||||
}
|
||||
|
||||
public Interpolator getInterpolator() {
|
||||
return interpolator;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return animateCondition == null || animateCondition.get();
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
public <T> JFXKeyValueBuilder<T> setTarget(WritableValue<T> target) {
|
||||
JFXKeyValueBuilder<T> builder = new JFXKeyValueBuilder<>();
|
||||
builder.setTarget(target);
|
||||
return builder;
|
||||
}
|
||||
|
||||
public <T> JFXKeyValueBuilder<T> setTargetSupplier(Supplier<WritableValue<T>> targetSupplier) {
|
||||
JFXKeyValueBuilder<T> builder = new JFXKeyValueBuilder<>();
|
||||
builder.setTargetSupplier(targetSupplier);
|
||||
return builder;
|
||||
}
|
||||
|
||||
public <T> JFXKeyValueBuilder<T> setEndValueSupplier(Supplier<T> endValueSupplier) {
|
||||
JFXKeyValueBuilder<T> builder = new JFXKeyValueBuilder<>();
|
||||
builder.setEndValueSupplier(endValueSupplier);
|
||||
return builder;
|
||||
}
|
||||
|
||||
public <T> JFXKeyValueBuilder<T> setEndValue(T endValue) {
|
||||
JFXKeyValueBuilder<T> builder = new JFXKeyValueBuilder<>();
|
||||
builder.setEndValue(endValue);
|
||||
return builder;
|
||||
}
|
||||
|
||||
public <T> JFXKeyValueBuilder<T> setAnimateCondition(Supplier<Boolean> animateCondition) {
|
||||
JFXKeyValueBuilder<T> builder = new JFXKeyValueBuilder<>();
|
||||
builder.setAnimateCondition(animateCondition);
|
||||
return builder;
|
||||
}
|
||||
|
||||
public <T> JFXKeyValueBuilder<T> setInterpolator(Interpolator interpolator) {
|
||||
JFXKeyValueBuilder<T> builder = new JFXKeyValueBuilder<>();
|
||||
builder.setInterpolator(interpolator);
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class JFXKeyValueBuilder<T> {
|
||||
|
||||
private WritableValue<T> target;
|
||||
private Supplier<WritableValue<T>> targetSupplier;
|
||||
private Supplier<T> endValueSupplier;
|
||||
private T endValue;
|
||||
private Supplier<Boolean> animateCondition = () -> true;
|
||||
private Interpolator interpolator = Interpolator.EASE_BOTH;
|
||||
|
||||
private JFXKeyValueBuilder() {
|
||||
}
|
||||
|
||||
public JFXKeyValueBuilder<T> setTarget(WritableValue<T> target) {
|
||||
this.target = target;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JFXKeyValueBuilder<T> setTargetSupplier(Supplier<WritableValue<T>> targetSupplier) {
|
||||
this.targetSupplier = targetSupplier;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JFXKeyValueBuilder<T> setEndValueSupplier(Supplier<T> endValueSupplier) {
|
||||
this.endValueSupplier = endValueSupplier;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JFXKeyValueBuilder<T> setEndValue(T endValue) {
|
||||
this.endValue = endValue;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JFXKeyValueBuilder<T> setAnimateCondition(Supplier<Boolean> animateCondition) {
|
||||
this.animateCondition = animateCondition;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JFXKeyValueBuilder<T> setInterpolator(Interpolator interpolator) {
|
||||
this.interpolator = interpolator;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JFXKeyValue<T> build() {
|
||||
JFXKeyValue<T> jFXKeyValue = new JFXKeyValue<>();
|
||||
jFXKeyValue.target = this.target;
|
||||
jFXKeyValue.interpolator = this.interpolator;
|
||||
jFXKeyValue.targetSupplier = this.targetSupplier;
|
||||
jFXKeyValue.endValue = this.endValue;
|
||||
jFXKeyValue.endValueSupplier = this.endValueSupplier;
|
||||
jFXKeyValue.animateCondition = this.animateCondition;
|
||||
return jFXKeyValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.utils;
|
||||
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.stage.Window;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// @author Shadi Shaheen
|
||||
/// @version 1.0
|
||||
/// @since 2017-02-11
|
||||
public final class JFXNodeUtils {
|
||||
|
||||
public static void updateBackground(Background newBackground, Region nodeToUpdate) {
|
||||
updateBackground(newBackground, nodeToUpdate, Color.BLACK);
|
||||
}
|
||||
|
||||
public static void updateBackground(Background newBackground, Region nodeToUpdate, Paint fill) {
|
||||
if (newBackground != null && !newBackground.getFills().isEmpty()) {
|
||||
final BackgroundFill[] fills = new BackgroundFill[newBackground.getFills().size()];
|
||||
for (int i = 0; i < newBackground.getFills().size(); i++) {
|
||||
BackgroundFill bf = newBackground.getFills().get(i);
|
||||
fills[i] = new BackgroundFill(fill, bf.getRadii(), bf.getInsets());
|
||||
}
|
||||
nodeToUpdate.setBackground(new Background(fills));
|
||||
}
|
||||
}
|
||||
|
||||
public static String colorToHex(Color c) {
|
||||
if (c != null) {
|
||||
return String.format((Locale) null, "#%02X%02X%02X",
|
||||
Math.round(c.getRed() * 255),
|
||||
Math.round(c.getGreen() * 255),
|
||||
Math.round(c.getBlue() * 255));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final @NotNull Function<Node, ObservableBooleanValue> treeVisiblePropertyGetter = initTreeVisiblePropertyGetter();
|
||||
|
||||
private static @NotNull Function<Node, ObservableBooleanValue> initTreeVisiblePropertyGetter() {
|
||||
|
||||
MethodHandles.Lookup lookup;
|
||||
try {
|
||||
lookup = MethodHandles.privateLookupIn(Node.class, MethodHandles.lookup());
|
||||
} catch (IllegalAccessException e) {
|
||||
LOG.warning("Failed to get private lookup for Node", e);
|
||||
return JFXNodeUtils::defaultTreeVisibleProperty;
|
||||
}
|
||||
|
||||
try {
|
||||
Method treeVisiblePropertyMethod = Node.class.getDeclaredMethod("treeVisibleProperty");
|
||||
if (!ObservableBooleanValue.class.isAssignableFrom(treeVisiblePropertyMethod.getReturnType())) {
|
||||
LOG.warning("Node.treeVisibleProperty() does not return ObservableBooleanValue: " + treeVisiblePropertyMethod.getReturnType());
|
||||
return JFXNodeUtils::defaultTreeVisibleProperty;
|
||||
}
|
||||
|
||||
MethodHandle handle = lookup.unreflect(treeVisiblePropertyMethod)
|
||||
.asType(MethodType.methodType(ObservableBooleanValue.class, Node.class));
|
||||
return item -> {
|
||||
try {
|
||||
return (ObservableBooleanValue) handle.invokeExact((Node) item);
|
||||
} catch (RuntimeException | Error e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
throw new AssertionError("Unreachable", e);
|
||||
}
|
||||
};
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to get method handle for Node.treeVisibleProperty()", e);
|
||||
return JFXNodeUtils::defaultTreeVisibleProperty;
|
||||
}
|
||||
}
|
||||
|
||||
/// If `Node.treeVisibleProperty()` does not exist, use `Node.visibleProperty()` as a fallback
|
||||
private static @NotNull ObservableBooleanValue defaultTreeVisibleProperty(Node item) {
|
||||
return item.visibleProperty();
|
||||
}
|
||||
|
||||
public static @NotNull ObservableBooleanValue treeVisibleProperty(Node item) {
|
||||
return treeVisiblePropertyGetter.apply(item);
|
||||
}
|
||||
|
||||
public static boolean isTreeVisible(Node item) {
|
||||
return treeVisibleProperty(item).getValue();
|
||||
}
|
||||
|
||||
public static boolean isTreeShowing(Node node) {
|
||||
if (node == null)
|
||||
return false;
|
||||
|
||||
Scene scene = node.getScene();
|
||||
if (scene == null)
|
||||
return false;
|
||||
|
||||
Window window = scene.getWindow();
|
||||
if (window == null || !window.isShowing())
|
||||
return false;
|
||||
|
||||
return isTreeVisible(node);
|
||||
}
|
||||
|
||||
private JFXNodeUtils() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package com.jfoenix.utils;
|
||||
|
||||
import javafx.beans.property.ReadOnlyBooleanPropertyBase;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Window;
|
||||
|
||||
/**
|
||||
* Used to observe changes in tree showing status for a {@link Node}. For a Node's tree to be showing
|
||||
* it must be visible, its ancestors must be visible, the node must be part of a {@link Scene} and
|
||||
* the scene must have a {@link Window} which is currently showing.<p>
|
||||
*
|
||||
* This class provides the exact same functionality as {@link JFXNodeUtils#isTreeShowing(Node)} in
|
||||
* an observable form.
|
||||
*/
|
||||
public class TreeShowingProperty extends ReadOnlyBooleanPropertyBase {
|
||||
private final ChangeListener<Boolean> windowShowingChangedListener = (obs, old, current) -> updateTreeShowing();
|
||||
private final ChangeListener<Window> sceneWindowChangedListener = (obs, old, current) -> windowChanged(old, current);
|
||||
private final ChangeListener<Scene> nodeSceneChangedListener = (obs, old, current) -> sceneChanged(old, current);
|
||||
|
||||
private final Node node;
|
||||
private final ObservableBooleanValue treeVisibleProperty;
|
||||
|
||||
private boolean valid;
|
||||
private boolean treeShowing;
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param node a {@link Node} for which the tree showing status should be observed, cannot be null
|
||||
*/
|
||||
public TreeShowingProperty(Node node) {
|
||||
this.node = node;
|
||||
this.treeVisibleProperty = JFXNodeUtils.treeVisibleProperty(node);
|
||||
|
||||
this.node.sceneProperty().addListener(nodeSceneChangedListener);
|
||||
this.treeVisibleProperty.addListener(windowShowingChangedListener);
|
||||
|
||||
sceneChanged(null, node.getScene());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return node;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "treeShowing";
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up any listeners that this class may have registered on the {@link Node}
|
||||
* that was supplied at construction.
|
||||
*/
|
||||
public void dispose() {
|
||||
node.sceneProperty().removeListener(nodeSceneChangedListener);
|
||||
|
||||
if (treeVisibleProperty != null)
|
||||
treeVisibleProperty.removeListener(windowShowingChangedListener);
|
||||
|
||||
valid = false; // prevents unregistration from triggering an invalidation notification
|
||||
sceneChanged(node.getScene(), null);
|
||||
}
|
||||
|
||||
protected void invalidate() {
|
||||
if (valid) {
|
||||
valid = false;
|
||||
fireValueChangedEvent();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean get() {
|
||||
if (!valid) {
|
||||
updateTreeShowing();
|
||||
valid = true;
|
||||
}
|
||||
|
||||
return treeShowing;
|
||||
}
|
||||
|
||||
private void sceneChanged(Scene oldScene, Scene newScene) {
|
||||
if (oldScene != null) {
|
||||
oldScene.windowProperty().removeListener(sceneWindowChangedListener);
|
||||
}
|
||||
if (newScene != null) {
|
||||
newScene.windowProperty().addListener(sceneWindowChangedListener);
|
||||
}
|
||||
|
||||
windowChanged(
|
||||
oldScene == null ? null : oldScene.getWindow(),
|
||||
newScene == null ? null : newScene.getWindow()
|
||||
);
|
||||
}
|
||||
|
||||
private void windowChanged(Window oldWindow, Window newWindow) {
|
||||
if (oldWindow != null) {
|
||||
oldWindow.showingProperty().removeListener(windowShowingChangedListener);
|
||||
}
|
||||
if (newWindow != null) {
|
||||
newWindow.showingProperty().addListener(windowShowingChangedListener);
|
||||
}
|
||||
|
||||
updateTreeShowing();
|
||||
}
|
||||
|
||||
private void updateTreeShowing() {
|
||||
boolean newValue = JFXNodeUtils.isTreeShowing(node);
|
||||
|
||||
if (newValue != treeShowing) {
|
||||
treeShowing = newValue;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@ import org.jackhuang.hmcl.util.FileSaver;
|
||||
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
|
||||
import org.jackhuang.hmcl.util.SwingUtils;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -28,6 +30,7 @@ import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -47,10 +50,12 @@ public final class EntryPoint {
|
||||
LOG.start(Metadata.HMCL_CURRENT_DIRECTORY.resolve("logs"));
|
||||
|
||||
setupJavaFXVMOptions();
|
||||
checkDirectoryPath();
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
|
||||
initIcon();
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
System.getProperties().putIfAbsent("apple.awt.application.appearance", "system");
|
||||
if (!isInsideMacAppBundle())
|
||||
initIcon();
|
||||
}
|
||||
|
||||
checkJavaFX();
|
||||
verifyJavaFX();
|
||||
@@ -159,6 +164,25 @@ public final class EntryPoint {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isInsideMacAppBundle() {
|
||||
Path thisJar = JarUtils.thisJarPath();
|
||||
if (thisJar == null)
|
||||
return false;
|
||||
|
||||
for (Path current = thisJar.getParent();
|
||||
current != null && current.getParent() != null;
|
||||
current = current.getParent()
|
||||
) {
|
||||
if ("Contents".equals(FileUtils.getName(current))
|
||||
&& FileUtils.getName(current.getParent()).endsWith(".app")
|
||||
&& Files.exists(current.resolve("Info.plist"))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void initIcon() {
|
||||
try {
|
||||
if (java.awt.Taskbar.isTaskbarSupported()) {
|
||||
@@ -170,25 +194,12 @@ public final class EntryPoint {
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkDirectoryPath() {
|
||||
String currentDir = System.getProperty("user.dir", "");
|
||||
if (currentDir.contains("!")) {
|
||||
LOG.error("The current working path contains an exclamation mark: " + currentDir);
|
||||
// No Chinese translation because both Swing and JavaFX cannot render Chinese character properly when exclamation mark exists in the path.
|
||||
showErrorAndExit("Exclamation mark(!) is not allowed in the path where HMCL is in.\n"
|
||||
+ "The path is " + currentDir);
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkJavaFX() {
|
||||
try {
|
||||
SelfDependencyPatcher.patch();
|
||||
} catch (SelfDependencyPatcher.PatchException e) {
|
||||
LOG.error("Unable to patch JVM", e);
|
||||
showErrorAndExit(i18n("fatal.javafx.missing"));
|
||||
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
|
||||
LOG.error("Unable to patch JVM", e);
|
||||
showErrorAndExit(i18n("fatal.javafx.incompatible"));
|
||||
} catch (CancellationException e) {
|
||||
LOG.error("User cancels downloading JavaFX", e);
|
||||
exit(0);
|
||||
|
||||
@@ -20,11 +20,13 @@ package org.jackhuang.hmcl;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.DataFormat;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.setting.SambaException;
|
||||
@@ -54,6 +56,7 @@ import java.net.CookieManager;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
@@ -79,6 +82,18 @@ public final class Launcher extends Application {
|
||||
LOG.info("Dark Mode: " + Optional.ofNullable(FXUtils.DARK_MODE).map(ObservableBooleanValue::get).orElse(false));
|
||||
LOG.info("Reduced Motion: " + Objects.requireNonNullElse(FXUtils.REDUCED_MOTION, false));
|
||||
|
||||
if (Screen.getScreens().isEmpty()) {
|
||||
LOG.info("No screen");
|
||||
} else {
|
||||
StringBuilder builder = new StringBuilder("Screens:");
|
||||
int count = 0;
|
||||
for (Screen screen : Screen.getScreens()) {
|
||||
builder.append("\n - Screen ").append(++count).append(": ");
|
||||
appendScreen(builder, screen);
|
||||
}
|
||||
LOG.info(builder.toString());
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
ConfigHolder.init();
|
||||
@@ -131,6 +146,38 @@ public final class Launcher extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
private static void appendScreen(StringBuilder builder, Screen screen) {
|
||||
Rectangle2D bounds = screen.getBounds();
|
||||
double scale = screen.getOutputScaleX();
|
||||
|
||||
builder.append(Math.round(bounds.getWidth() * scale));
|
||||
builder.append('x');
|
||||
builder.append(Math.round(bounds.getHeight() * scale));
|
||||
|
||||
DecimalFormat decimalFormat = new DecimalFormat("#.##");
|
||||
|
||||
if (scale != 1.0) {
|
||||
builder.append(" @ ");
|
||||
builder.append(decimalFormat.format(scale));
|
||||
builder.append('x');
|
||||
}
|
||||
|
||||
double dpi = screen.getDpi();
|
||||
builder.append(' ');
|
||||
builder.append(decimalFormat.format(dpi));
|
||||
builder.append("dpi");
|
||||
|
||||
builder.append(" in ")
|
||||
.append(Math.round(Math.sqrt(bounds.getWidth() * bounds.getWidth() + bounds.getHeight() * bounds.getHeight()) / dpi))
|
||||
.append('"');
|
||||
|
||||
builder.append(" (").append(decimalFormat.format(bounds.getMinX()))
|
||||
.append(", ").append(decimalFormat.format(bounds.getMinY()))
|
||||
.append(", ").append(decimalFormat.format(bounds.getMaxX()))
|
||||
.append(", ").append(decimalFormat.format(bounds.getMaxY()))
|
||||
.append(")");
|
||||
}
|
||||
|
||||
private static ButtonType showAlert(AlertType alertType, String contentText, ButtonType... buttons) {
|
||||
return new Alert(alertType, contentText, buttons).showAndWait().orElse(null);
|
||||
}
|
||||
@@ -206,7 +253,7 @@ public final class Launcher extends Application {
|
||||
if (Files.exists(mcDir))
|
||||
files.add(mcDir.toString());
|
||||
|
||||
String command = new CommandBuilder().add("sudo", "chown", "-R", userName).addAll(files).toString();
|
||||
String command = new CommandBuilder().addAll("sudo", "chown", "-R", userName).addAll(files).toString();
|
||||
ButtonType copyAndExit = new ButtonType(i18n("button.copy_and_exit"));
|
||||
|
||||
if (showAlert(AlertType.ERROR,
|
||||
|
||||
@@ -111,6 +111,7 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
String region = locale.getCountry();
|
||||
|
||||
return switch (LocaleUtils.getRootLanguage(locale)) {
|
||||
case "ar" -> "ar_SA";
|
||||
case "es" -> "es_ES";
|
||||
case "ja" -> "ja_JP";
|
||||
case "ru" -> "ru_RU";
|
||||
|
||||
@@ -25,26 +25,28 @@ import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.mod.ModAdviser;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.mod.ModpackProvider;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.util.FileSaver;
|
||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.FileSaver;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemInfo;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Proxy;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
@@ -54,8 +56,8 @@ import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
private final Profile profile;
|
||||
@@ -173,7 +175,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
Files.copy(fromJson, toJson);
|
||||
|
||||
JsonUtils.writeToJsonFile(toJson, fromVersion.setId(dstId));
|
||||
JsonUtils.writeToJsonFile(toJson, fromVersion.setId(dstId).setJar(dstId));
|
||||
|
||||
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
|
||||
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
|
||||
@@ -318,23 +320,34 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version, null);
|
||||
if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FABRIC))
|
||||
return VersionIconType.FABRIC.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.QUILT))
|
||||
return VersionIconType.QUILT.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LEGACY_FABRIC))
|
||||
return VersionIconType.LEGACY_FABRIC.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE))
|
||||
return VersionIconType.NEO_FORGE.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FORGE))
|
||||
return VersionIconType.FORGE.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.CLEANROOM))
|
||||
return VersionIconType.CLEANROOM.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE))
|
||||
return VersionIconType.NEO_FORGE.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.QUILT))
|
||||
return VersionIconType.QUILT.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.OPTIFINE))
|
||||
return VersionIconType.OPTIFINE.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LITELOADER))
|
||||
return VersionIconType.CHICKEN.getIcon();
|
||||
else
|
||||
return VersionIconType.FURNACE.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.OPTIFINE))
|
||||
return VersionIconType.OPTIFINE.getIcon();
|
||||
}
|
||||
|
||||
return VersionIconType.DEFAULT.getIcon();
|
||||
String gameVersion = getGameVersion(version).orElse(null);
|
||||
if (gameVersion != null) {
|
||||
GameVersionNumber versionNumber = GameVersionNumber.asGameVersion(gameVersion);
|
||||
if (versionNumber.isAprilFools()) {
|
||||
return VersionIconType.APRIL_FOOLS.getIcon();
|
||||
} else if (versionNumber instanceof GameVersionNumber.LegacySnapshot) {
|
||||
return VersionIconType.COMMAND.getIcon();
|
||||
} else if (versionNumber instanceof GameVersionNumber.Old) {
|
||||
return VersionIconType.CRAFT_TABLE.getIcon();
|
||||
}
|
||||
}
|
||||
return VersionIconType.GRASS.getIcon();
|
||||
} else {
|
||||
return iconType.getIcon();
|
||||
}
|
||||
@@ -377,7 +390,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
vs.setUsesGlobal(true);
|
||||
}
|
||||
|
||||
public LaunchOptions getLaunchOptions(String version, JavaRuntime javaVersion, Path gameDir, List<String> javaAgents, List<String> javaArguments, boolean makeLaunchScript) {
|
||||
public LaunchOptions.Builder getLaunchOptions(String version, JavaRuntime javaVersion, Path gameDir, List<String> javaAgents, List<String> javaArguments, boolean makeLaunchScript) {
|
||||
VersionSetting vs = getVersionSetting(version);
|
||||
|
||||
LaunchOptions.Builder builder = new LaunchOptions.Builder()
|
||||
@@ -408,8 +421,8 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
.setWidth(vs.getWidth())
|
||||
.setHeight(vs.getHeight())
|
||||
.setFullscreen(vs.isFullscreen())
|
||||
.setServerIp(vs.getServerIp())
|
||||
.setWrapper(vs.getWrapper())
|
||||
.setProxyOption(getProxyOption())
|
||||
.setPreLaunchCommand(vs.getPreLaunchCommand())
|
||||
.setPostExitCommand(vs.getPostExitCommand())
|
||||
.setNoGeneratedJVMArgs(vs.isNoJVMArgs())
|
||||
@@ -418,21 +431,15 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
.setNativesDir(vs.getNativesDir())
|
||||
.setProcessPriority(vs.getProcessPriority())
|
||||
.setRenderer(vs.getRenderer())
|
||||
.setEnableDebugLogOutput(vs.isEnableDebugLogOutput())
|
||||
.setUseNativeGLFW(vs.isUseNativeGLFW())
|
||||
.setUseNativeOpenAL(vs.isUseNativeOpenAL())
|
||||
.setDaemon(!makeLaunchScript && vs.getLauncherVisibility().isDaemon())
|
||||
.setJavaAgents(javaAgents)
|
||||
.setJavaArguments(javaArguments);
|
||||
|
||||
if (config().hasProxy()) {
|
||||
builder.setProxyType(config().getProxyType());
|
||||
builder.setProxyHost(config().getProxyHost());
|
||||
builder.setProxyPort(config().getProxyPort());
|
||||
|
||||
if (config().hasProxyAuth()) {
|
||||
builder.setProxyUser(config().getProxyUser());
|
||||
builder.setProxyPass(config().getProxyPass());
|
||||
}
|
||||
if (StringUtils.isNotBlank(vs.getServerIp())) {
|
||||
builder.setQuickPlayOption(new QuickPlayOption.MultiPlayer(vs.getServerIp()));
|
||||
}
|
||||
|
||||
Path json = getModpackConfiguration(version);
|
||||
@@ -450,7 +457,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
if (vs.isAutoMemory() && builder.getJavaArguments().stream().anyMatch(it -> it.startsWith("-Xmx")))
|
||||
builder.setMaxMemory(null);
|
||||
|
||||
return builder.create();
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -544,4 +551,39 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
return minimum;
|
||||
}
|
||||
}
|
||||
|
||||
public static ProxyOption getProxyOption() {
|
||||
if (!config().hasProxy() || config().getProxyType() == null) {
|
||||
return ProxyOption.Default.INSTANCE;
|
||||
}
|
||||
|
||||
return switch (config().getProxyType()) {
|
||||
case DIRECT -> ProxyOption.Direct.INSTANCE;
|
||||
case HTTP, SOCKS -> {
|
||||
String proxyHost = config().getProxyHost();
|
||||
int proxyPort = config().getProxyPort();
|
||||
|
||||
if (StringUtils.isBlank(proxyHost) || proxyPort < 0 || proxyPort > 0xFFFF) {
|
||||
yield ProxyOption.Default.INSTANCE;
|
||||
}
|
||||
|
||||
String proxyUser = config().getProxyUser();
|
||||
String proxyPass = config().getProxyPass();
|
||||
|
||||
if (StringUtils.isBlank(proxyUser)) {
|
||||
proxyUser = null;
|
||||
proxyPass = null;
|
||||
} else if (proxyPass == null) {
|
||||
proxyPass = "";
|
||||
}
|
||||
|
||||
if (config().getProxyType() == Proxy.Type.HTTP) {
|
||||
yield new ProxyOption.Http(proxyHost, proxyPort, proxyUser, proxyPass);
|
||||
} else {
|
||||
yield new ProxyOption.Socks(proxyHost, proxyPort, proxyUser, proxyPass);
|
||||
}
|
||||
}
|
||||
default -> ProxyOption.Default.INSTANCE;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ public final class HMCLModpackProvider implements ModpackProvider {
|
||||
|
||||
private final static class HMCLModpack extends Modpack {
|
||||
@Override
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name) {
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl) {
|
||||
return new HMCLModpackInstallTask(((HMCLGameRepository) dependencyManager.getGameRepository()).getProfile(), zipFile, this, name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
@@ -51,6 +52,7 @@ import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
@@ -60,7 +62,6 @@ import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import static javafx.application.Platform.runLater;
|
||||
import static javafx.application.Platform.setImplicitExit;
|
||||
@@ -81,6 +82,8 @@ public final class LauncherHelper {
|
||||
private final VersionSetting setting;
|
||||
private LauncherVisibility launcherVisibility;
|
||||
private boolean showLogs;
|
||||
private QuickPlayOption quickPlayOption;
|
||||
private boolean disableOfflineSkin = false;
|
||||
|
||||
public LauncherHelper(Profile profile, Account account, String selectedVersion) {
|
||||
this.profile = Objects.requireNonNull(profile);
|
||||
@@ -111,6 +114,14 @@ public final class LauncherHelper {
|
||||
launcherVisibility = LauncherVisibility.KEEP;
|
||||
}
|
||||
|
||||
public void setQuickPlayOption(QuickPlayOption quickPlayOption) {
|
||||
this.quickPlayOption = quickPlayOption;
|
||||
}
|
||||
|
||||
public void setDisableOfflineSkin() {
|
||||
disableOfflineSkin = true;
|
||||
}
|
||||
|
||||
public void launch() {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
@@ -188,8 +199,15 @@ public final class LauncherHelper {
|
||||
.thenComposeAsync(() -> gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version.get())).orElse(null))
|
||||
.thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in"))
|
||||
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
|
||||
LaunchOptions launchOptions = repository.getLaunchOptions(
|
||||
LaunchOptions.Builder launchOptionsBuilder = repository.getLaunchOptions(
|
||||
selectedVersion, javaVersionRef.get(), profile.getGameDir(), javaAgents, javaArguments, scriptFile != null);
|
||||
if (disableOfflineSkin) {
|
||||
launchOptionsBuilder.setDaemon(false);
|
||||
}
|
||||
if (quickPlayOption != null) {
|
||||
launchOptionsBuilder.setQuickPlayOption(quickPlayOption);
|
||||
}
|
||||
LaunchOptions launchOptions = launchOptionsBuilder.create();
|
||||
|
||||
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModsDirectory(selectedVersion), 10));
|
||||
|
||||
@@ -231,11 +249,11 @@ public final class LauncherHelper {
|
||||
i18n("message.doing"),
|
||||
() -> launchingLatch.getCount() == 0, 6.95
|
||||
).withStage("launch.state.waiting_launching"))
|
||||
.withStagesHint(Lang.immutableListOf(
|
||||
"launch.state.java",
|
||||
"launch.state.dependencies",
|
||||
"launch.state.logging_in",
|
||||
"launch.state.waiting_launching"))
|
||||
.withStagesHints(
|
||||
new Task.StagesHint("launch.state.java"),
|
||||
new Task.StagesHint("launch.state.dependencies", List.of("hmcl.install.assets", "hmcl.install.libraries", "hmcl.modpack.download")),
|
||||
new Task.StagesHint("launch.state.logging_in"),
|
||||
new Task.StagesHint("launch.state.waiting_launching"))
|
||||
.executor();
|
||||
launchingStepsPane.setExecutor(executor, false);
|
||||
executor.addTaskListener(new TaskListener() {
|
||||
@@ -307,13 +325,13 @@ public final class LauncherHelper {
|
||||
message = i18n("launch.failed.command_too_long");
|
||||
} else if (ex instanceof ExecutionPolicyLimitException) {
|
||||
Controllers.prompt(new PromptDialogPane.Builder(i18n("launch.failed.execution_policy"),
|
||||
(result, resolve, reject) -> {
|
||||
(result, handler) -> {
|
||||
if (CommandBuilder.setExecutionPolicy()) {
|
||||
LOG.info("Set the ExecutionPolicy for the scope 'CurrentUser' to 'RemoteSigned'");
|
||||
resolve.run();
|
||||
handler.resolve();
|
||||
} else {
|
||||
LOG.warning("Failed to set ExecutionPolicy");
|
||||
reject.accept(i18n("launch.failed.execution_policy.failed_to_set"));
|
||||
handler.reject(i18n("launch.failed.execution_policy.failed_to_set"));
|
||||
}
|
||||
})
|
||||
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("launch.failed.execution_policy.hint")))
|
||||
@@ -369,9 +387,18 @@ public final class LauncherHelper {
|
||||
if (setting.getJavaVersionType() == JavaVersionType.VERSION) {
|
||||
try {
|
||||
int targetJavaVersionMajor = Integer.parseInt(setting.getJavaVersion());
|
||||
GameJavaVersion minimumJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
|
||||
GameJavaVersion minimumJavaVersion = null;
|
||||
if (gameVersion.compareTo("1.12.2") == 0) {
|
||||
Optional<String> cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM);
|
||||
if (cleanroomVersion.isPresent()) {
|
||||
minimumJavaVersion = GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion.get());
|
||||
}
|
||||
}
|
||||
|
||||
if (minimumJavaVersion != null && targetJavaVersionMajor < minimumJavaVersion.getMajorVersion()) {
|
||||
if (minimumJavaVersion == null)
|
||||
minimumJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
|
||||
|
||||
if (minimumJavaVersion != null && targetJavaVersionMajor < minimumJavaVersion.majorVersion()) {
|
||||
Controllers.dialog(
|
||||
i18n("launch.failed.java_version_too_low"),
|
||||
i18n("message.error"),
|
||||
@@ -384,8 +411,17 @@ public final class LauncherHelper {
|
||||
targetJavaVersion = GameJavaVersion.get(targetJavaVersionMajor);
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
} else
|
||||
targetJavaVersion = version.getJavaVersion();
|
||||
} else {
|
||||
if (gameVersion.compareTo("1.12.2") == 0) {
|
||||
Optional<String> cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM);
|
||||
if (cleanroomVersion.isPresent()) {
|
||||
targetJavaVersion = GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion.get());
|
||||
}
|
||||
}
|
||||
|
||||
if (targetJavaVersion == null)
|
||||
targetJavaVersion = version.getJavaVersion();
|
||||
}
|
||||
|
||||
if (targetJavaVersion != null && supportedVersions.contains(targetJavaVersion)) {
|
||||
downloadJava(targetJavaVersion, profile)
|
||||
@@ -415,7 +451,7 @@ public final class LauncherHelper {
|
||||
if (java != null) {
|
||||
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
|
||||
if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) {
|
||||
if (!constraint.checkJava(gameVersion, version, java)) {
|
||||
if (!constraint.checkJava(gameVersion, version, java, analyzer)) {
|
||||
if (constraint.isMandatory()) {
|
||||
violatedMandatoryConstraints.add(constraint);
|
||||
} else {
|
||||
@@ -450,9 +486,14 @@ public final class LauncherHelper {
|
||||
return result;
|
||||
} else {
|
||||
GameJavaVersion gameJavaVersion;
|
||||
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.CLEANROOM_JAVA_21))
|
||||
gameJavaVersion = GameJavaVersion.JAVA_21;
|
||||
else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
|
||||
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.CLEANROOM)) {
|
||||
String cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM)
|
||||
.orElse("");
|
||||
|
||||
gameJavaVersion = !cleanroomVersion.isEmpty()
|
||||
? GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion)
|
||||
: GameJavaVersion.JAVA_21;
|
||||
} else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
|
||||
gameJavaVersion = version.getJavaVersion();
|
||||
else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA))
|
||||
gameJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
|
||||
@@ -542,9 +583,14 @@ public final class LauncherHelper {
|
||||
case MODDED_JAVA_21:
|
||||
suggestions.add(i18n("launch.advice.modded_java", 21, gameVersion));
|
||||
break;
|
||||
case CLEANROOM_JAVA_21:
|
||||
suggestions.add(i18n("launch.advice.cleanroom"));
|
||||
case CLEANROOM: {
|
||||
String cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM).orElse("");
|
||||
if (!cleanroomVersion.isEmpty())
|
||||
suggestions.add(i18n("launch.advice.cleanroom", GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion).majorVersion(), cleanroomVersion));
|
||||
else
|
||||
suggestions.add(i18n("launch.advice.cleanroom", 21, ""));
|
||||
break;
|
||||
}
|
||||
case VANILLA_JAVA_8_51:
|
||||
suggestions.add(i18n("launch.advice.java8_51_1_13"));
|
||||
break;
|
||||
@@ -616,7 +662,7 @@ public final class LauncherHelper {
|
||||
private static CompletableFuture<JavaRuntime> downloadJava(GameJavaVersion javaVersion, Profile profile) {
|
||||
CompletableFuture<JavaRuntime> future = new CompletableFuture<>();
|
||||
Controllers.dialog(new MessageDialogPane.Builder(
|
||||
i18n("launch.advice.require_newer_java_version", javaVersion.getMajorVersion()),
|
||||
i18n("launch.advice.require_newer_java_version", javaVersion.majorVersion()),
|
||||
i18n("message.warning"),
|
||||
MessageType.QUESTION)
|
||||
.yesOrNo(() -> {
|
||||
@@ -639,10 +685,13 @@ public final class LauncherHelper {
|
||||
return future;
|
||||
}
|
||||
|
||||
private static Task<AuthInfo> logIn(Account account) {
|
||||
private Task<AuthInfo> logIn(Account account) {
|
||||
return Task.composeAsync(() -> {
|
||||
try {
|
||||
return Task.completed(account.logIn());
|
||||
if (disableOfflineSkin && account instanceof OfflineAccount offlineAccount)
|
||||
return Task.completed(offlineAccount.logInWithoutSkin());
|
||||
else
|
||||
return Task.completed(account.logIn());
|
||||
} catch (CredentialExpiredException e) {
|
||||
LOG.info("Credential has expired", e);
|
||||
|
||||
|
||||
@@ -115,8 +115,8 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteMod getModById(String id) throws IOException {
|
||||
return getBackedRemoteModRepository().getModById(id);
|
||||
public RemoteMod getModById(DownloadProvider downloadProvider, String id) throws IOException {
|
||||
return getBackedRemoteModRepository().getModById(downloadProvider, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -125,7 +125,7 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
|
||||
return getBackedRemoteModRepository().getRemoteVersionsById(id);
|
||||
public Stream<RemoteMod.Version> getRemoteVersionsById(DownloadProvider downloadProvider, String id) throws IOException {
|
||||
return getBackedRemoteModRepository().getRemoteVersionsById(downloadProvider, id);
|
||||
}
|
||||
}
|
||||
@@ -17,21 +17,19 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.io.Zipper;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.nio.file.PathMatcher;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -43,7 +41,9 @@ public final class LogExporter {
|
||||
private LogExporter() {
|
||||
}
|
||||
|
||||
public static CompletableFuture<Void> exportLogs(Path zipFile, DefaultGameRepository gameRepository, String versionId, String logs, String launchScript) {
|
||||
public static CompletableFuture<Void> exportLogs(
|
||||
Path zipFile, DefaultGameRepository gameRepository, String versionId, String logs, String launchScript,
|
||||
PathMatcher logMatcher) {
|
||||
Path runDirectory = gameRepository.getRunDirectory(versionId);
|
||||
Path baseDirectory = gameRepository.getBaseDirectory();
|
||||
List<String> versions = new ArrayList<>();
|
||||
@@ -64,11 +64,11 @@ public final class LogExporter {
|
||||
}
|
||||
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try (Zipper zipper = new Zipper(zipFile)) {
|
||||
processLogs(runDirectory.resolve("liteconfig"), "*.log", "liteconfig", zipper);
|
||||
processLogs(runDirectory.resolve("logs"), "*.log", "logs", zipper);
|
||||
processLogs(runDirectory, "*.log", "runDirectory", zipper);
|
||||
processLogs(runDirectory.resolve("crash-reports"), "*.txt", "crash-reports", zipper);
|
||||
try (Zipper zipper = new Zipper(zipFile, true)) {
|
||||
processLogs(runDirectory.resolve("liteconfig"), "*.log", "liteconfig", zipper, logMatcher);
|
||||
processLogs(runDirectory.resolve("logs"), "*.log", "logs", zipper, logMatcher);
|
||||
processLogs(runDirectory, "*.log", "runDirectory", zipper, logMatcher);
|
||||
processLogs(runDirectory.resolve("crash-reports"), "*.txt", "crash-reports", zipper, logMatcher);
|
||||
|
||||
zipper.putTextFile(LOG.getLogs(), "hmcl.log");
|
||||
zipper.putTextFile(logs, "minecraft.log");
|
||||
@@ -86,14 +86,11 @@ public final class LogExporter {
|
||||
});
|
||||
}
|
||||
|
||||
private static void processLogs(Path directory, String fileExtension, String logDirectory, Zipper zipper) {
|
||||
private static void processLogs(Path directory, String fileExtension, String logDirectory, Zipper zipper, PathMatcher logMatcher) {
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory, fileExtension)) {
|
||||
long processStartTime = ManagementFactory.getRuntimeMXBean().getStartTime();
|
||||
|
||||
for (Path file : stream) {
|
||||
if (Files.isRegularFile(file)) {
|
||||
FileTime time = Files.readAttributes(file, BasicFileAttributes.class).lastModifiedTime();
|
||||
if (time.toMillis() >= processStartTime) {
|
||||
if (logMatcher == null || logMatcher.matches(file)) {
|
||||
try (BufferedReader reader = IOUtils.newBufferedReaderMaybeNativeEncoding(file)) {
|
||||
zipper.putLines(reader.lines().map(Logger::filterForbiddenToken), file.getFileName().toString());
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -167,7 +167,7 @@ public final class ModpackHelper {
|
||||
|
||||
return new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.withStagesHint(Arrays.asList("hmcl.modpack", "hmcl.modpack.download"));
|
||||
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
|
||||
}
|
||||
|
||||
public static boolean isExternalGameNameConflicts(String name) {
|
||||
@@ -188,7 +188,7 @@ public final class ModpackHelper {
|
||||
});
|
||||
}
|
||||
|
||||
public static Task<?> getInstallTask(Profile profile, Path zipFile, String name, Modpack modpack) {
|
||||
public static Task<?> getInstallTask(Profile profile, Path zipFile, String name, Modpack modpack, String iconUrl) {
|
||||
profile.getRepository().markVersionAsModpack(name);
|
||||
|
||||
ExceptionalRunnable<?> success = () -> {
|
||||
@@ -208,26 +208,27 @@ public final class ModpackHelper {
|
||||
};
|
||||
|
||||
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
|
||||
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
|
||||
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
|
||||
else if (modpack.getManifest() instanceof McbbsModpackManifest)
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name))
|
||||
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
|
||||
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
|
||||
else
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
|
||||
.whenComplete(Schedulers.javafx(), success, failure)
|
||||
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
|
||||
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
|
||||
}
|
||||
|
||||
public static Task<Void> getUpdateTask(Profile profile, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
|
||||
switch (configuration.getType()) {
|
||||
case ServerModpackRemoteInstallTask.MODPACK_TYPE:
|
||||
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name))
|
||||
.withStagesHint(Arrays.asList("hmcl.modpack", "hmcl.modpack.download"));
|
||||
.thenComposeAsync(profile.getRepository().refreshVersionsAsync())
|
||||
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
|
||||
default:
|
||||
throw new UnsupportedModpackException();
|
||||
}
|
||||
@@ -241,9 +242,11 @@ public final class ModpackHelper {
|
||||
}
|
||||
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack)
|
||||
.thenComposeAsync(() -> createMultiMCPostUpdateTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
|
||||
.thenComposeAsync(() -> createMultiMCPostUpdateTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
|
||||
.thenComposeAsync(profile.getRepository().refreshVersionsAsync());
|
||||
else
|
||||
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack);
|
||||
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack)
|
||||
.thenComposeAsync(profile.getRepository().refreshVersionsAsync());
|
||||
}
|
||||
|
||||
public static void toVersionSetting(MultiMCInstanceConfiguration c, VersionSetting vs) {
|
||||
|
||||
@@ -22,27 +22,28 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.OAuth;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
private final int port;
|
||||
private final CompletableFuture<String> future = new CompletableFuture<>();
|
||||
private final String codeVerifier;
|
||||
private final String state;
|
||||
|
||||
public static String lastlyOpenedURL;
|
||||
|
||||
@@ -52,6 +53,34 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
super(port);
|
||||
|
||||
this.port = port;
|
||||
|
||||
var encoder = Base64.getUrlEncoder().withoutPadding();
|
||||
var random = new SecureRandom();
|
||||
|
||||
{
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-10.12
|
||||
byte[] bytes = new byte[32];
|
||||
random.nextBytes(bytes);
|
||||
this.state = encoder.encodeToString(bytes);
|
||||
}
|
||||
|
||||
{
|
||||
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
|
||||
byte[] bytes = new byte[64];
|
||||
random.nextBytes(bytes);
|
||||
this.codeVerifier = encoder.encodeToString(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCodeVerifier() {
|
||||
return codeVerifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -93,19 +122,32 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
String parameters = session.getQueryParameterString();
|
||||
|
||||
Map<String, String> query = mapOf(NetworkUtils.parseQuery(parameters));
|
||||
if (query.containsKey("code")) {
|
||||
idToken = query.get("id_token");
|
||||
future.complete(query.get("code"));
|
||||
|
||||
String code = query.get("code");
|
||||
if (code != null) {
|
||||
if (this.state.equals(query.get("state"))) {
|
||||
idToken = query.get("id_token");
|
||||
future.complete(code);
|
||||
} else if (query.containsKey("state")) {
|
||||
LOG.warning("Failed to authenticate: invalid state in parameters");
|
||||
future.completeExceptionally(new AuthenticationException("Failed to authenticate: invalid state"));
|
||||
} else {
|
||||
LOG.warning("Failed to authenticate: missing state in parameters");
|
||||
future.completeExceptionally(new AuthenticationException("Failed to authenticate: missing state"));
|
||||
}
|
||||
} else {
|
||||
LOG.warning("Error: " + parameters);
|
||||
future.completeExceptionally(new AuthenticationException("failed to authenticate"));
|
||||
LOG.warning("Failed to authenticate: missing authorization code in parameters");
|
||||
future.completeExceptionally(new AuthenticationException("Failed to authenticate: missing authorization code"));
|
||||
}
|
||||
|
||||
String html;
|
||||
try {
|
||||
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
|
||||
.replace("%style%", Themes.getTheme().toColorScheme().toStyleSheet().replace("-monet", "--monet"))
|
||||
.replace("%lang%", Locale.getDefault().toLanguageTag())
|
||||
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
|
||||
.replace("%success%", i18n("message.success"))
|
||||
.replace("%ok%", i18n("button.ok"))
|
||||
.replace("%close_page%", i18n("account.methods.microsoft.close_page"));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to load html", e);
|
||||
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
|
||||
@@ -123,7 +165,8 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
|
||||
public static class Factory implements OAuth.Callback {
|
||||
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
|
||||
public final EventManager<OpenBrowserEvent> onOpenBrowser = new EventManager<>();
|
||||
public final EventManager<OpenBrowserEvent> onOpenBrowserAuthorizationCode = new EventManager<>();
|
||||
public final EventManager<OpenBrowserEvent> onOpenBrowserDevice = new EventManager<>();
|
||||
|
||||
@Override
|
||||
public OAuth.Session startServer() throws IOException, AuthenticationException {
|
||||
@@ -150,11 +193,13 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openBrowser(String url) throws IOException {
|
||||
public void openBrowser(OAuth.GrantFlow grantFlow, String url) throws IOException {
|
||||
lastlyOpenedURL = url;
|
||||
FXUtils.openLink(url);
|
||||
|
||||
onOpenBrowser.fireEvent(new OpenBrowserEvent(this, url));
|
||||
switch (grantFlow) {
|
||||
case AUTHORIZATION_CODE -> onOpenBrowserAuthorizationCode.fireEvent(new OpenBrowserEvent(this, url));
|
||||
case DEVICE -> onOpenBrowserDevice.fireEvent(new OpenBrowserEvent(this, url));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -162,17 +207,6 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
return System.getProperty("hmcl.microsoft.auth.id",
|
||||
JarUtils.getAttribute("hmcl.microsoft.auth.id", ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientSecret() {
|
||||
return System.getProperty("hmcl.microsoft.auth.secret",
|
||||
JarUtils.getAttribute("hmcl.microsoft.auth.secret", ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPublicClient() {
|
||||
return true; // We have turned on the device auth flow.
|
||||
}
|
||||
}
|
||||
|
||||
public static class GrantDeviceCodeEvent extends Event {
|
||||
|
||||
@@ -30,7 +30,9 @@ import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -57,7 +59,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
}
|
||||
|
||||
public Path getJavaDir(Platform platform, GameJavaVersion gameJavaVersion) {
|
||||
return getJavaDir(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
|
||||
return getJavaDir(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.component());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -66,7 +68,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
}
|
||||
|
||||
public Path getManifestFile(Platform platform, GameJavaVersion gameJavaVersion) {
|
||||
return getManifestFile(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
|
||||
return getManifestFile(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.component());
|
||||
}
|
||||
|
||||
public boolean isInstalled(Platform platform, String name) {
|
||||
@@ -74,7 +76,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
}
|
||||
|
||||
public boolean isInstalled(Platform platform, GameJavaVersion gameJavaVersion) {
|
||||
return isInstalled(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
|
||||
return isInstalled(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.component());
|
||||
}
|
||||
|
||||
public @Nullable Path getJavaExecutable(Platform platform, String name) {
|
||||
@@ -94,7 +96,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
}
|
||||
|
||||
public @Nullable Path getJavaExecutable(Platform platform, GameJavaVersion gameJavaVersion) {
|
||||
return getJavaExecutable(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
|
||||
return getJavaExecutable(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.component());
|
||||
}
|
||||
|
||||
private static void getAllJava(List<JavaRuntime> list, Platform platform, Path platformRoot, boolean isManaged) {
|
||||
@@ -169,7 +171,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
|
||||
Map<String, Object> update = new LinkedHashMap<>();
|
||||
update.put("provider", "mojang");
|
||||
update.put("component", gameJavaVersion.getComponent());
|
||||
update.put("component", gameJavaVersion.component());
|
||||
|
||||
Map<String, JavaLocalFiles.Local> files = new LinkedHashMap<>();
|
||||
result.remoteFiles.getFiles().forEach((path, file) -> {
|
||||
|
||||
@@ -304,7 +304,7 @@ public final class JavaManager {
|
||||
|
||||
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
|
||||
if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) {
|
||||
if (!constraint.checkJava(gameVersion, version, java)) {
|
||||
if (!constraint.checkJava(gameVersion, version, java, analyzer)) {
|
||||
if (constraint.isMandatory()) {
|
||||
violationMandatory = true;
|
||||
} else {
|
||||
|
||||
@@ -414,6 +414,12 @@ public final class Accounts {
|
||||
return i18n("account.failed.no_character");
|
||||
} else if (exception instanceof ServerDisconnectException) {
|
||||
if (exception.getCause() instanceof SSLException) {
|
||||
if (exception.getCause().getMessage() != null && exception.getCause().getMessage().contains("Remote host terminated")) {
|
||||
return i18n("account.failed.connect_authentication_server");
|
||||
}
|
||||
if (exception.getCause().getMessage() != null && (exception.getCause().getMessage().contains("No name matching") || exception.getCause().getMessage().contains("No subject alternative DNS name matching"))) {
|
||||
return i18n("account.failed.dns");
|
||||
}
|
||||
return i18n("account.failed.ssl");
|
||||
} else {
|
||||
return i18n("account.failed.connect_authentication_server");
|
||||
@@ -465,7 +471,7 @@ public final class Accounts {
|
||||
} else if (exception instanceof MicrosoftService.NoMinecraftJavaEditionProfileException) {
|
||||
return i18n("account.methods.microsoft.error.no_character");
|
||||
} else if (exception instanceof MicrosoftService.NoXuiException) {
|
||||
return i18n("account.methods.microsoft.error.add_family_probably");
|
||||
return i18n("account.methods.microsoft.error.add_family");
|
||||
} else if (exception instanceof OAuthServer.MicrosoftAuthenticationNotSupportedException) {
|
||||
return i18n("account.methods.microsoft.snapshot");
|
||||
} else if (exception instanceof OAuthAccount.WrongAccountException) {
|
||||
|
||||
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.setting;
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -35,21 +34,18 @@ import org.hildan.fxgson.factories.JavaFxPropertyTypeAdapterFactory;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.theme.ThemeColor;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.gson.*;
|
||||
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
|
||||
import org.jackhuang.hmcl.util.javafx.DirtyTracker;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.Proxy;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
@JsonAdapter(value = Config.Adapter.class)
|
||||
public final class Config implements Observable {
|
||||
public final class Config extends ObservableSetting {
|
||||
|
||||
public static final int CURRENT_VERSION = 2;
|
||||
public static final int CURRENT_UI_VERSION = 0;
|
||||
@@ -67,54 +63,15 @@ public final class Config implements Observable {
|
||||
.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
|
||||
.create();
|
||||
|
||||
private static final List<ObservableField<Config>> FIELDS;
|
||||
|
||||
static {
|
||||
final MethodHandles.Lookup lookup = MethodHandles.lookup();
|
||||
Field[] fields = Config.class.getDeclaredFields();
|
||||
|
||||
var configFields = new ArrayList<ObservableField<Config>>(fields.length);
|
||||
for (Field field : fields) {
|
||||
int modifiers = field.getModifiers();
|
||||
if (Modifier.isTransient(modifiers) || Modifier.isStatic(modifiers))
|
||||
continue;
|
||||
|
||||
configFields.add(ObservableField.of(lookup, field));
|
||||
}
|
||||
FIELDS = List.copyOf(configFields);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Config fromJson(String json) throws JsonParseException {
|
||||
return CONFIG_GSON.fromJson(json, Config.class);
|
||||
}
|
||||
|
||||
private transient final ObservableHelper helper = new ObservableHelper(this);
|
||||
private transient final DirtyTracker tracker = new DirtyTracker();
|
||||
private transient final Map<String, JsonElement> unknownFields = new HashMap<>();
|
||||
|
||||
public Config() {
|
||||
var shouldBeWrite = Collections.<Observable>newSetFromMap(new IdentityHashMap<>());
|
||||
Collections.addAll(shouldBeWrite, configVersion, uiVersion);
|
||||
|
||||
for (var field : FIELDS) {
|
||||
Observable observable = field.get(this);
|
||||
if (shouldBeWrite.contains(observable))
|
||||
tracker.markDirty(observable);
|
||||
else
|
||||
tracker.track(observable);
|
||||
observable.addListener(helper);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(InvalidationListener listener) {
|
||||
helper.addListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(InvalidationListener listener) {
|
||||
helper.removeListener(listener);
|
||||
tracker.markDirty(configVersion);
|
||||
tracker.markDirty(uiVersion);
|
||||
register();
|
||||
}
|
||||
|
||||
public String toJson() {
|
||||
@@ -265,6 +222,36 @@ public final class Config implements Observable {
|
||||
this.acceptPreviewUpdate.set(acceptPreviewUpdate);
|
||||
}
|
||||
|
||||
@SerializedName("disableAutoShowUpdateDialog")
|
||||
private final BooleanProperty disableAutoShowUpdateDialog = new SimpleBooleanProperty(false);
|
||||
|
||||
public BooleanProperty disableAutoShowUpdateDialogProperty() {
|
||||
return disableAutoShowUpdateDialog;
|
||||
}
|
||||
|
||||
public boolean isDisableAutoShowUpdateDialog() {
|
||||
return disableAutoShowUpdateDialog.get();
|
||||
}
|
||||
|
||||
public void setDisableAutoShowUpdateDialog(boolean disableAutoShowUpdateDialog) {
|
||||
this.disableAutoShowUpdateDialog.set(disableAutoShowUpdateDialog);
|
||||
}
|
||||
|
||||
@SerializedName("disableAprilFools")
|
||||
private final BooleanProperty disableAprilFools = new SimpleBooleanProperty(false);
|
||||
|
||||
public BooleanProperty disableAprilFoolsProperty() {
|
||||
return disableAprilFools;
|
||||
}
|
||||
|
||||
public boolean isDisableAprilFools() {
|
||||
return disableAprilFools.get();
|
||||
}
|
||||
|
||||
public void setDisableAprilFools(boolean disableAprilFools) {
|
||||
this.disableAprilFools.set(disableAprilFools);
|
||||
}
|
||||
|
||||
@SerializedName("shownTips")
|
||||
private final ObservableMap<String, Object> shownTips = FXCollections.observableHashMap();
|
||||
|
||||
@@ -319,19 +306,34 @@ public final class Config implements Observable {
|
||||
|
||||
// UI
|
||||
|
||||
@SerializedName("themeBrightness")
|
||||
private final StringProperty themeBrightness = new SimpleStringProperty("light");
|
||||
|
||||
public StringProperty themeBrightnessProperty() {
|
||||
return themeBrightness;
|
||||
}
|
||||
|
||||
public String getThemeBrightness() {
|
||||
return themeBrightness.get();
|
||||
}
|
||||
|
||||
public void setThemeBrightness(String themeBrightness) {
|
||||
this.themeBrightness.set(themeBrightness);
|
||||
}
|
||||
|
||||
@SerializedName("theme")
|
||||
private final ObjectProperty<Theme> theme = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<ThemeColor> themeColor = new SimpleObjectProperty<>(ThemeColor.DEFAULT);
|
||||
|
||||
public ObjectProperty<Theme> themeProperty() {
|
||||
return theme;
|
||||
public ObjectProperty<ThemeColor> themeColorProperty() {
|
||||
return themeColor;
|
||||
}
|
||||
|
||||
public Theme getTheme() {
|
||||
return theme.get();
|
||||
public ThemeColor getThemeColor() {
|
||||
return themeColor.get();
|
||||
}
|
||||
|
||||
public void setTheme(Theme theme) {
|
||||
this.theme.set(theme);
|
||||
public void setThemeColor(ThemeColor themeColor) {
|
||||
this.themeColor.set(themeColor);
|
||||
}
|
||||
|
||||
@SerializedName("fontFamily")
|
||||
@@ -521,7 +523,7 @@ public final class Config implements Observable {
|
||||
}
|
||||
|
||||
@SerializedName("downloadType")
|
||||
private final StringProperty downloadType = new SimpleStringProperty(DownloadProviders.DEFAULT_RAW_PROVIDER_ID);
|
||||
private final StringProperty downloadType = new SimpleStringProperty(DownloadProviders.DEFAULT_DIRECT_PROVIDER_ID);
|
||||
|
||||
public StringProperty downloadTypeProperty() {
|
||||
return downloadType;
|
||||
@@ -551,7 +553,7 @@ public final class Config implements Observable {
|
||||
}
|
||||
|
||||
@SerializedName("versionListSource")
|
||||
private final StringProperty versionListSource = new SimpleStringProperty("balanced");
|
||||
private final StringProperty versionListSource = new SimpleStringProperty(DownloadProviders.DEFAULT_AUTO_PROVIDER_ID);
|
||||
|
||||
public StringProperty versionListSourceProperty() {
|
||||
return versionListSource;
|
||||
@@ -775,53 +777,10 @@ public final class Config implements Observable {
|
||||
return configurations;
|
||||
}
|
||||
|
||||
public static final class Adapter implements JsonSerializer<Config>, JsonDeserializer<Config> {
|
||||
|
||||
public static final class Adapter extends ObservableSetting.Adapter<Config> {
|
||||
@Override
|
||||
public JsonElement serialize(Config config, Type typeOfSrc, JsonSerializationContext context) {
|
||||
if (config == null)
|
||||
return JsonNull.INSTANCE;
|
||||
|
||||
JsonObject result = new JsonObject();
|
||||
for (var field : FIELDS) {
|
||||
Observable observable = field.get(config);
|
||||
if (config.tracker.isDirty(observable)) {
|
||||
field.serialize(result, config, context);
|
||||
}
|
||||
}
|
||||
config.unknownFields.forEach(result::add);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Config deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (json == null || json.isJsonNull())
|
||||
return null;
|
||||
|
||||
if (!json.isJsonObject())
|
||||
throw new JsonParseException("Config is not an object: " + json);
|
||||
|
||||
Config config = new Config();
|
||||
|
||||
var values = new LinkedHashMap<>(json.getAsJsonObject().asMap());
|
||||
for (ObservableField<Config> field : FIELDS) {
|
||||
JsonElement value = values.remove(field.getSerializedName());
|
||||
if (value == null) {
|
||||
for (String alternateName : field.getAlternateNames()) {
|
||||
value = values.remove(alternateName);
|
||||
if (value != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
config.tracker.markDirty(field.get(config));
|
||||
field.deserialize(config, value, context);
|
||||
}
|
||||
}
|
||||
|
||||
config.unknownFields.putAll(values);
|
||||
return config;
|
||||
protected Config createInstance() {
|
||||
return new Config();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,9 +21,10 @@ import javafx.beans.InvalidationListener;
|
||||
import org.jackhuang.hmcl.download.*;
|
||||
import org.jackhuang.hmcl.task.DownloadException;
|
||||
import org.jackhuang.hmcl.task.FetchTask;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
@@ -31,104 +32,88 @@ import java.io.FileNotFoundException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.task.FetchTask.DEFAULT_CONCURRENCY;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class DownloadProviders {
|
||||
private DownloadProviders() {
|
||||
}
|
||||
|
||||
private static final DownloadProviderWrapper provider;
|
||||
public static final String DEFAULT_AUTO_PROVIDER_ID = "balanced";
|
||||
public static final String DEFAULT_DIRECT_PROVIDER_ID = "mojang";
|
||||
|
||||
public static final Map<String, DownloadProvider> providersById;
|
||||
public static final Map<String, DownloadProvider> rawProviders;
|
||||
private static final AdaptedDownloadProvider fileDownloadProvider = new AdaptedDownloadProvider();
|
||||
private static final DownloadProviderWrapper PROVIDER_WRAPPER;
|
||||
|
||||
private static final MojangDownloadProvider MOJANG;
|
||||
private static final BMCLAPIDownloadProvider BMCLAPI;
|
||||
|
||||
public static final String DEFAULT_PROVIDER_ID = "balanced";
|
||||
public static final String DEFAULT_RAW_PROVIDER_ID = "bmclapi";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final InvalidationListener observer;
|
||||
private static final DownloadProvider DEFAULT_PROVIDER;
|
||||
public static final Map<String, DownloadProvider> DIRECT_PROVIDERS;
|
||||
public static final Map<String, DownloadProvider> AUTO_PROVIDERS;
|
||||
|
||||
static {
|
||||
String bmclapiRoot = "https://bmclapi2.bangbang93.com";
|
||||
String bmclapiRootOverride = System.getProperty("hmcl.bmclapi.override");
|
||||
if (bmclapiRootOverride != null) bmclapiRoot = bmclapiRootOverride;
|
||||
String bmclapiRoot = System.getProperty("hmcl.bmclapi.override", "https://bmclapi2.bangbang93.com");
|
||||
BMCLAPIDownloadProvider bmclapiRaw = new BMCLAPIDownloadProvider(bmclapiRoot);
|
||||
|
||||
MOJANG = new MojangDownloadProvider();
|
||||
BMCLAPI = new BMCLAPIDownloadProvider(bmclapiRoot);
|
||||
rawProviders = Map.of(
|
||||
"mojang", MOJANG,
|
||||
"bmclapi", BMCLAPI
|
||||
DownloadProvider mojang = new MojangDownloadProvider();
|
||||
DownloadProvider bmclapi = new AutoDownloadProvider(bmclapiRaw, mojang);
|
||||
|
||||
DEFAULT_PROVIDER = mojang;
|
||||
DIRECT_PROVIDERS = Lang.mapOf(
|
||||
pair("mojang", mojang),
|
||||
pair("bmclapi", bmclapi)
|
||||
);
|
||||
|
||||
AdaptedDownloadProvider fileProvider = new AdaptedDownloadProvider();
|
||||
fileProvider.setDownloadProviderCandidates(List.of(BMCLAPI, MOJANG));
|
||||
BalancedDownloadProvider balanced = new BalancedDownloadProvider(MOJANG, BMCLAPI);
|
||||
AUTO_PROVIDERS = Lang.mapOf(
|
||||
pair("balanced", LocaleUtils.IS_CHINA_MAINLAND ? bmclapi : mojang),
|
||||
pair("official", LocaleUtils.IS_CHINA_MAINLAND ? new AutoDownloadProvider(
|
||||
List.of(mojang, bmclapiRaw),
|
||||
List.of(bmclapiRaw, mojang)
|
||||
) : mojang),
|
||||
pair("mirror", bmclapi)
|
||||
);
|
||||
|
||||
providersById = Map.of(
|
||||
"official", new AutoDownloadProvider(MOJANG, fileProvider),
|
||||
"balanced", new AutoDownloadProvider(balanced, fileProvider),
|
||||
"mirror", new AutoDownloadProvider(BMCLAPI, fileProvider));
|
||||
|
||||
observer = FXUtils.observeWeak(() -> {
|
||||
FetchTask.setDownloadExecutorConcurrency(
|
||||
config().getAutoDownloadThreads() ? DEFAULT_CONCURRENCY : config().getDownloadThreads());
|
||||
}, config().autoDownloadThreadsProperty(), config().downloadThreadsProperty());
|
||||
|
||||
provider = new DownloadProviderWrapper(MOJANG);
|
||||
PROVIDER_WRAPPER = new DownloadProviderWrapper(DEFAULT_PROVIDER);
|
||||
}
|
||||
|
||||
static void init() {
|
||||
InvalidationListener onChangeDownloadSource = observable -> {
|
||||
String versionListSource = Objects.requireNonNullElse(config().getVersionListSource(), "");
|
||||
if (config().isAutoChooseDownloadType()) {
|
||||
DownloadProvider currentDownloadProvider = providersById.get(versionListSource);
|
||||
if (currentDownloadProvider == null)
|
||||
currentDownloadProvider = Objects.requireNonNull(providersById.get(DEFAULT_PROVIDER_ID),
|
||||
"default provider is null");
|
||||
InvalidationListener onChangeDownloadThreads = observable -> {
|
||||
FetchTask.setDownloadExecutorConcurrency(config().getAutoDownloadThreads()
|
||||
? DEFAULT_CONCURRENCY
|
||||
: config().getDownloadThreads());
|
||||
};
|
||||
config().autoDownloadThreadsProperty().addListener(onChangeDownloadThreads);
|
||||
config().downloadThreadsProperty().addListener(onChangeDownloadThreads);
|
||||
onChangeDownloadThreads.invalidated(null);
|
||||
|
||||
provider.setProvider(currentDownloadProvider);
|
||||
InvalidationListener onChangeDownloadSource = observable -> {
|
||||
if (config().isAutoChooseDownloadType()) {
|
||||
String versionListSource = config().getVersionListSource();
|
||||
DownloadProvider downloadProvider = versionListSource != null
|
||||
? AUTO_PROVIDERS.getOrDefault(versionListSource, DEFAULT_PROVIDER)
|
||||
: DEFAULT_PROVIDER;
|
||||
PROVIDER_WRAPPER.setProvider(downloadProvider);
|
||||
} else {
|
||||
provider.setProvider(fileDownloadProvider);
|
||||
String downloadType = config().getDownloadType();
|
||||
PROVIDER_WRAPPER.setProvider(downloadType != null
|
||||
? DIRECT_PROVIDERS.getOrDefault(downloadType, DEFAULT_PROVIDER)
|
||||
: DEFAULT_PROVIDER);
|
||||
}
|
||||
};
|
||||
config().versionListSourceProperty().addListener(onChangeDownloadSource);
|
||||
config().autoChooseDownloadTypeProperty().addListener(onChangeDownloadSource);
|
||||
|
||||
config().downloadTypeProperty().addListener(onChangeDownloadSource);
|
||||
onChangeDownloadSource.invalidated(null);
|
||||
|
||||
FXUtils.onChangeAndOperate(config().downloadTypeProperty(), downloadType -> {
|
||||
DownloadProvider primary = Objects.requireNonNullElseGet(
|
||||
rawProviders.get(Objects.requireNonNullElse(downloadType, "")),
|
||||
() -> rawProviders.get(DEFAULT_RAW_PROVIDER_ID));
|
||||
|
||||
List<DownloadProvider> providers = new ArrayList<>(rawProviders.size());
|
||||
providers.add(primary);
|
||||
for (DownloadProvider provider : rawProviders.values()) {
|
||||
if (provider != primary)
|
||||
providers.add(provider);
|
||||
}
|
||||
|
||||
fileDownloadProvider.setDownloadProviderCandidates(providers);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current primary preferred download provider
|
||||
*/
|
||||
public static DownloadProvider getDownloadProvider() {
|
||||
return provider;
|
||||
return PROVIDER_WRAPPER;
|
||||
}
|
||||
|
||||
public static String localizeErrorMessage(Throwable exception) {
|
||||
@@ -149,7 +134,10 @@ public final class DownloadProviders {
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
|
||||
} else if (exception.getCause() instanceof ArtifactMalformedException) {
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.artifact_malformed");
|
||||
} else if (exception.getCause() instanceof SSLHandshakeException) {
|
||||
} else if (exception.getCause() instanceof SSLHandshakeException && !(exception.getCause().getMessage() != null && exception.getCause().getMessage().contains("Remote host terminated"))) {
|
||||
if (exception.getCause().getMessage() != null && (exception.getCause().getMessage().contains("No name matching") || exception.getCause().getMessage().contains("No subject alternative DNS name matching"))) {
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.dns.pollution");
|
||||
}
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.ssl_handshake");
|
||||
} else {
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.text.Font;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
@@ -94,31 +95,26 @@ public final class FontManager {
|
||||
return null;
|
||||
});
|
||||
|
||||
private static final ObjectProperty<FontReference> fontProperty;
|
||||
private static final ObjectProperty<FontReference> font = new SimpleObjectProperty<>();
|
||||
|
||||
static {
|
||||
updateFont();
|
||||
LOG.info("Font: " + (font.get() != null ? font.get().family() : "System"));
|
||||
}
|
||||
|
||||
private static void updateFont() {
|
||||
String fontFamily = config().getLauncherFontFamily();
|
||||
if (fontFamily == null)
|
||||
fontFamily = System.getProperty("hmcl.font.override");
|
||||
if (fontFamily == null)
|
||||
fontFamily = System.getenv("HMCL_FONT");
|
||||
|
||||
FontReference fontReference;
|
||||
if (fontFamily == null) {
|
||||
Font defaultFont = DEFAULT_FONT.get();
|
||||
fontReference = defaultFont != null ? new FontReference(defaultFont) : null;
|
||||
} else
|
||||
fontReference = new FontReference(fontFamily);
|
||||
|
||||
fontProperty = new SimpleObjectProperty<>(fontReference);
|
||||
|
||||
LOG.info("Font: " + (fontReference != null ? fontReference.getFamily() : "System"));
|
||||
fontProperty.addListener((obs, oldValue, newValue) -> {
|
||||
if (newValue != null)
|
||||
config().setLauncherFontFamily(newValue.getFamily());
|
||||
else
|
||||
config().setLauncherFontFamily(null);
|
||||
});
|
||||
font.set(defaultFont != null ? new FontReference(defaultFont) : null);
|
||||
} else {
|
||||
font.set(new FontReference(fontFamily));
|
||||
}
|
||||
}
|
||||
|
||||
private static Font tryLoadDefaultFont(Path dir) {
|
||||
@@ -231,64 +227,35 @@ public final class FontManager {
|
||||
}
|
||||
}
|
||||
|
||||
public static ObjectProperty<FontReference> fontProperty() {
|
||||
return fontProperty;
|
||||
public static ReadOnlyObjectProperty<FontReference> fontProperty() {
|
||||
return font;
|
||||
}
|
||||
|
||||
public static FontReference getFont() {
|
||||
return fontProperty.get();
|
||||
}
|
||||
|
||||
public static void setFont(FontReference font) {
|
||||
fontProperty.set(font);
|
||||
return font.get();
|
||||
}
|
||||
|
||||
public static void setFontFamily(String fontFamily) {
|
||||
setFont(fontFamily != null ? new FontReference(fontFamily) : null);
|
||||
config().setLauncherFontFamily(fontFamily);
|
||||
updateFont();
|
||||
}
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4072
|
||||
public static final class FontReference {
|
||||
private final @NotNull String family;
|
||||
private final @Nullable String style;
|
||||
public record FontReference(@NotNull String family, @Nullable String style) {
|
||||
public FontReference {
|
||||
Objects.requireNonNull(family);
|
||||
}
|
||||
|
||||
public FontReference(@NotNull String family) {
|
||||
this.family = Objects.requireNonNull(family);
|
||||
this.style = null;
|
||||
this(family, null);
|
||||
}
|
||||
|
||||
public FontReference(@NotNull Font font) {
|
||||
this.family = font.getFamily();
|
||||
this.style = font.getStyle();
|
||||
}
|
||||
|
||||
public @NotNull String getFamily() {
|
||||
return family;
|
||||
}
|
||||
|
||||
public @Nullable String getStyle() {
|
||||
return style;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof FontReference))
|
||||
return false;
|
||||
FontReference that = (FontReference) o;
|
||||
return Objects.equals(family, that.family) && Objects.equals(style, that.style);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(family, style);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("FontReference[family='%s', style='%s']", family, style);
|
||||
this(font.getFamily(), font.getStyle());
|
||||
}
|
||||
}
|
||||
|
||||
private FontManager() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,131 +19,109 @@ package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableSet;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
|
||||
import org.jackhuang.hmcl.util.gson.ObservableSetting;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.*;
|
||||
|
||||
@JsonAdapter(GlobalConfig.Serializer.class)
|
||||
public final class GlobalConfig implements Observable {
|
||||
@JsonAdapter(GlobalConfig.Adapter.class)
|
||||
public final class GlobalConfig extends ObservableSetting {
|
||||
|
||||
@Nullable
|
||||
public static GlobalConfig fromJson(String json) throws JsonParseException {
|
||||
GlobalConfig loaded = Config.CONFIG_GSON.fromJson(json, GlobalConfig.class);
|
||||
if (loaded == null) {
|
||||
return null;
|
||||
}
|
||||
GlobalConfig instance = new GlobalConfig();
|
||||
PropertyUtils.copyProperties(loaded, instance);
|
||||
instance.unknownFields.putAll(loaded.unknownFields);
|
||||
return instance;
|
||||
return Config.CONFIG_GSON.fromJson(json, GlobalConfig.class);
|
||||
}
|
||||
|
||||
private final IntegerProperty agreementVersion = new SimpleIntegerProperty();
|
||||
|
||||
private final IntegerProperty terracottaAgreementVersion = new SimpleIntegerProperty();
|
||||
|
||||
private final IntegerProperty platformPromptVersion = new SimpleIntegerProperty();
|
||||
|
||||
private final IntegerProperty logRetention = new SimpleIntegerProperty();
|
||||
|
||||
private final BooleanProperty enableOfflineAccount = new SimpleBooleanProperty(false);
|
||||
|
||||
private final StringProperty fontAntiAliasing = new SimpleStringProperty();
|
||||
|
||||
private final ObservableSet<String> userJava = FXCollections.observableSet(new LinkedHashSet<>());
|
||||
|
||||
private final ObservableSet<String> disabledJava = FXCollections.observableSet(new LinkedHashSet<>());
|
||||
|
||||
private final Map<String, Object> unknownFields = new HashMap<>();
|
||||
|
||||
private final transient ObservableHelper helper = new ObservableHelper(this);
|
||||
|
||||
public GlobalConfig() {
|
||||
PropertyUtils.attachListener(this, helper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(InvalidationListener listener) {
|
||||
helper.addListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(InvalidationListener listener) {
|
||||
helper.removeListener(listener);
|
||||
register();
|
||||
}
|
||||
|
||||
public String toJson() {
|
||||
return Config.CONFIG_GSON.toJson(this);
|
||||
}
|
||||
|
||||
public int getAgreementVersion() {
|
||||
return agreementVersion.get();
|
||||
}
|
||||
@SerializedName("agreementVersion")
|
||||
private final IntegerProperty agreementVersion = new SimpleIntegerProperty();
|
||||
|
||||
public IntegerProperty agreementVersionProperty() {
|
||||
return agreementVersion;
|
||||
}
|
||||
|
||||
public int getAgreementVersion() {
|
||||
return agreementVersion.get();
|
||||
}
|
||||
|
||||
public void setAgreementVersion(int agreementVersion) {
|
||||
this.agreementVersion.set(agreementVersion);
|
||||
}
|
||||
|
||||
public int getTerracottaAgreementVersion() {
|
||||
return terracottaAgreementVersion.get();
|
||||
}
|
||||
@SerializedName("terracottaAgreementVersion")
|
||||
private final IntegerProperty terracottaAgreementVersion = new SimpleIntegerProperty();
|
||||
|
||||
public IntegerProperty terracottaAgreementVersionProperty() {
|
||||
return terracottaAgreementVersion;
|
||||
}
|
||||
|
||||
public int getTerracottaAgreementVersion() {
|
||||
return terracottaAgreementVersion.get();
|
||||
}
|
||||
|
||||
public void setTerracottaAgreementVersion(int terracottaAgreementVersion) {
|
||||
this.terracottaAgreementVersion.set(terracottaAgreementVersion);
|
||||
}
|
||||
|
||||
public int getPlatformPromptVersion() {
|
||||
return platformPromptVersion.get();
|
||||
}
|
||||
@SerializedName("platformPromptVersion")
|
||||
private final IntegerProperty platformPromptVersion = new SimpleIntegerProperty();
|
||||
|
||||
public IntegerProperty platformPromptVersionProperty() {
|
||||
return platformPromptVersion;
|
||||
}
|
||||
|
||||
public int getPlatformPromptVersion() {
|
||||
return platformPromptVersion.get();
|
||||
}
|
||||
|
||||
public void setPlatformPromptVersion(int platformPromptVersion) {
|
||||
this.platformPromptVersion.set(platformPromptVersion);
|
||||
}
|
||||
|
||||
public int getLogRetention() {
|
||||
return logRetention.get();
|
||||
}
|
||||
@SerializedName("logRetention")
|
||||
private final IntegerProperty logRetention = new SimpleIntegerProperty(20);
|
||||
|
||||
public IntegerProperty logRetentionProperty() {
|
||||
return logRetention;
|
||||
}
|
||||
|
||||
public int getLogRetention() {
|
||||
return logRetention.get();
|
||||
}
|
||||
|
||||
public void setLogRetention(int logRetention) {
|
||||
this.logRetention.set(logRetention);
|
||||
}
|
||||
|
||||
public boolean isEnableOfflineAccount() {
|
||||
return enableOfflineAccount.get();
|
||||
}
|
||||
@SerializedName("enableOfflineAccount")
|
||||
private final BooleanProperty enableOfflineAccount = new SimpleBooleanProperty(false);
|
||||
|
||||
public BooleanProperty enableOfflineAccountProperty() {
|
||||
return enableOfflineAccount;
|
||||
}
|
||||
|
||||
public boolean isEnableOfflineAccount() {
|
||||
return enableOfflineAccount.get();
|
||||
}
|
||||
|
||||
public void setEnableOfflineAccount(boolean value) {
|
||||
enableOfflineAccount.set(value);
|
||||
}
|
||||
|
||||
@SerializedName("fontAntiAliasing")
|
||||
private final StringProperty fontAntiAliasing = new SimpleStringProperty();
|
||||
|
||||
public StringProperty fontAntiAliasingProperty() {
|
||||
return fontAntiAliasing;
|
||||
}
|
||||
@@ -156,89 +134,24 @@ public final class GlobalConfig implements Observable {
|
||||
this.fontAntiAliasing.set(value);
|
||||
}
|
||||
|
||||
@SerializedName("userJava")
|
||||
private final ObservableSet<String> userJava = FXCollections.observableSet(new LinkedHashSet<>());
|
||||
|
||||
public ObservableSet<String> getUserJava() {
|
||||
return userJava;
|
||||
}
|
||||
|
||||
@SerializedName("disabledJava")
|
||||
private final ObservableSet<String> disabledJava = FXCollections.observableSet(new LinkedHashSet<>());
|
||||
|
||||
public ObservableSet<String> getDisabledJava() {
|
||||
return disabledJava;
|
||||
}
|
||||
|
||||
public static final class Serializer implements JsonSerializer<GlobalConfig>, JsonDeserializer<GlobalConfig> {
|
||||
private static final Set<String> knownFields = new HashSet<>(Arrays.asList(
|
||||
"agreementVersion",
|
||||
"terracottaAgreementVersion",
|
||||
"platformPromptVersion",
|
||||
"logRetention",
|
||||
"userJava",
|
||||
"disabledJava",
|
||||
"enableOfflineAccount",
|
||||
"fontAntiAliasing"
|
||||
));
|
||||
|
||||
static final class Adapter extends ObservableSetting.Adapter<GlobalConfig> {
|
||||
@Override
|
||||
public JsonElement serialize(GlobalConfig src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
if (src == null) {
|
||||
return JsonNull.INSTANCE;
|
||||
}
|
||||
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
|
||||
jsonObject.add("terracottaAgreementVersion", context.serialize(src.getTerracottaAgreementVersion()));
|
||||
jsonObject.add("platformPromptVersion", context.serialize(src.getPlatformPromptVersion()));
|
||||
jsonObject.add("logRetention", context.serialize(src.getLogRetention()));
|
||||
jsonObject.add("fontAntiAliasing", context.serialize(src.getFontAntiAliasing()));
|
||||
if (src.enableOfflineAccount.get())
|
||||
jsonObject.addProperty("enableOfflineAccount", true);
|
||||
|
||||
if (!src.getUserJava().isEmpty())
|
||||
jsonObject.add("userJava", context.serialize(src.getUserJava()));
|
||||
|
||||
if (!src.getDisabledJava().isEmpty())
|
||||
jsonObject.add("disabledJava", context.serialize(src.getDisabledJava()));
|
||||
|
||||
for (Map.Entry<String, Object> entry : src.unknownFields.entrySet()) {
|
||||
jsonObject.add(entry.getKey(), context.serialize(entry.getValue()));
|
||||
}
|
||||
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GlobalConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (!(json instanceof JsonObject)) return null;
|
||||
|
||||
JsonObject obj = (JsonObject) json;
|
||||
|
||||
GlobalConfig config = new GlobalConfig();
|
||||
config.setAgreementVersion(Optional.ofNullable(obj.get("agreementVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
config.setTerracottaAgreementVersion(Optional.ofNullable(obj.get("terracottaAgreementVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
config.setPlatformPromptVersion(Optional.ofNullable(obj.get("platformPromptVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
config.setLogRetention(Optional.ofNullable(obj.get("logRetention")).map(JsonElement::getAsInt).orElse(20));
|
||||
config.setEnableOfflineAccount(Optional.ofNullable(obj.get("enableOfflineAccount")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
config.setFontAntiAliasing(Optional.ofNullable(obj.get("fontAntiAliasing")).map(JsonElement::getAsString).orElse(null));
|
||||
|
||||
JsonElement userJava = obj.get("userJava");
|
||||
if (userJava != null && userJava.isJsonArray()) {
|
||||
for (JsonElement element : userJava.getAsJsonArray()) {
|
||||
config.userJava.add(element.getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
JsonElement disabledJava = obj.get("disabledJava");
|
||||
if (disabledJava != null && disabledJava.isJsonArray()) {
|
||||
for (JsonElement element : disabledJava.getAsJsonArray()) {
|
||||
config.disabledJava.add(element.getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
for (Map.Entry<String, JsonElement> entry : obj.entrySet()) {
|
||||
if (!knownFields.contains(entry.getKey())) {
|
||||
config.unknownFields.put(entry.getKey(), context.deserialize(entry.getValue(), Object.class));
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
protected GlobalConfig createInstance() {
|
||||
return new GlobalConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.beans.InvalidationListener;
|
||||
import org.jackhuang.hmcl.task.FetchTask;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -114,6 +115,8 @@ public final class ProxyManager {
|
||||
config().hasProxyAuthProperty().addListener(updateAuthenticator);
|
||||
config().proxyUserProperty().addListener(updateAuthenticator);
|
||||
config().proxyPassProperty().addListener(updateAuthenticator);
|
||||
|
||||
FetchTask.notifyInitialized();
|
||||
}
|
||||
|
||||
private static abstract class AbstractProxySelector extends ProxySelector {
|
||||
|
||||
@@ -22,13 +22,24 @@ import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.glavo.monetfx.Brightness;
|
||||
import org.glavo.monetfx.ColorRole;
|
||||
import org.glavo.monetfx.ColorScheme;
|
||||
import org.jackhuang.hmcl.theme.Theme;
|
||||
import org.jackhuang.hmcl.theme.ThemeColor;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
@@ -36,6 +47,7 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
public final class StyleSheets {
|
||||
private static final int FONT_STYLE_SHEET_INDEX = 0;
|
||||
private static final int THEME_STYLE_SHEET_INDEX = 1;
|
||||
private static final int BRIGHTNESS_SHEET_INDEX = 2;
|
||||
|
||||
private static final ObservableList<String> stylesheets;
|
||||
|
||||
@@ -43,27 +55,46 @@ public final class StyleSheets {
|
||||
String[] array = new String[]{
|
||||
getFontStyleSheet(),
|
||||
getThemeStyleSheet(),
|
||||
getBrightnessStyleSheet(),
|
||||
"/assets/css/root.css"
|
||||
};
|
||||
stylesheets = FXCollections.observableList(Arrays.asList(array));
|
||||
|
||||
FontManager.fontProperty().addListener(o -> stylesheets.set(FONT_STYLE_SHEET_INDEX, getFontStyleSheet()));
|
||||
config().themeProperty().addListener(o -> stylesheets.set(THEME_STYLE_SHEET_INDEX, getThemeStyleSheet()));
|
||||
Themes.colorSchemeProperty().addListener(o -> {
|
||||
stylesheets.set(THEME_STYLE_SHEET_INDEX, getThemeStyleSheet());
|
||||
stylesheets.set(BRIGHTNESS_SHEET_INDEX, getBrightnessStyleSheet());
|
||||
});
|
||||
}
|
||||
|
||||
private static String toStyleSheetUri(String styleSheet) {
|
||||
return "data:text/css;charset=UTF-8;base64," + Base64.getEncoder().encodeToString(styleSheet.getBytes(StandardCharsets.UTF_8));
|
||||
private static String toStyleSheetUri(String styleSheet, String fallback) {
|
||||
if (FXUtils.JAVAFX_MAJOR_VERSION >= 17)
|
||||
// JavaFX 17+ support loading stylesheets from data URIs
|
||||
// https://bugs.openjdk.org/browse/JDK-8267554
|
||||
return "data:text/css;charset=UTF-8;base64," + Base64.getEncoder().encodeToString(styleSheet.getBytes(StandardCharsets.UTF_8));
|
||||
else
|
||||
try {
|
||||
Path temp = Files.createTempFile("hmcl", ".css");
|
||||
// For JavaFX 17 or earlier, CssParser uses the default charset
|
||||
// https://bugs.openjdk.org/browse/JDK-8279328
|
||||
Files.writeString(temp, styleSheet, Charset.defaultCharset());
|
||||
temp.toFile().deleteOnExit();
|
||||
return temp.toUri().toString();
|
||||
} catch (IOException | NullPointerException e) {
|
||||
LOG.error("Unable to create stylesheet, fallback to " + fallback, e);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getFontStyleSheet() {
|
||||
final String defaultCss = "/assets/css/font.css";
|
||||
final FontManager.FontReference font = FontManager.getFont();
|
||||
|
||||
if (font == null || "System".equals(font.getFamily()))
|
||||
if (font == null || "System".equals(font.family()))
|
||||
return defaultCss;
|
||||
|
||||
String fontFamily = font.getFamily();
|
||||
String style = font.getStyle();
|
||||
String fontFamily = font.family();
|
||||
String style = font.style();
|
||||
String weight = null;
|
||||
String posture = null;
|
||||
|
||||
@@ -102,34 +133,58 @@ public final class StyleSheets {
|
||||
|
||||
builder.append('}');
|
||||
|
||||
return toStyleSheetUri(builder.toString());
|
||||
return toStyleSheetUri(builder.toString(), defaultCss);
|
||||
}
|
||||
|
||||
private static String rgba(Color color, double opacity) {
|
||||
return String.format("rgba(%d, %d, %d, %.1f)",
|
||||
(int) Math.ceil(color.getRed() * 256),
|
||||
(int) Math.ceil(color.getGreen() * 256),
|
||||
(int) Math.ceil(color.getBlue() * 256),
|
||||
opacity);
|
||||
private static String getBrightnessStyleSheet() {
|
||||
return Themes.getColorScheme().getBrightness() == Brightness.LIGHT
|
||||
? "/assets/css/brightness-light.css"
|
||||
: "/assets/css/brightness-dark.css";
|
||||
}
|
||||
|
||||
private static void addColor(StringBuilder builder, String name, Color color) {
|
||||
builder.append(" ").append(name)
|
||||
.append(": ").append(ThemeColor.getColorDisplayName(color)).append(";\n");
|
||||
}
|
||||
|
||||
private static void addColor(StringBuilder builder, String name, Color color, double opacity) {
|
||||
builder.append(" ").append(name)
|
||||
.append(": ").append(ThemeColor.getColorDisplayNameWithOpacity(color, opacity)).append(";\n");
|
||||
}
|
||||
|
||||
private static void addColor(StringBuilder builder, ColorScheme scheme, ColorRole role, double opacity) {
|
||||
builder.append(" ").append(role.getVariableName()).append("-transparent-%02d".formatted((int) (100 * opacity)))
|
||||
.append(": ").append(ThemeColor.getColorDisplayNameWithOpacity(scheme.getColor(role), opacity))
|
||||
.append(";\n");
|
||||
}
|
||||
|
||||
private static String getThemeStyleSheet() {
|
||||
final String blueCss = "/assets/css/blue.css";
|
||||
|
||||
Theme theme = config().getTheme();
|
||||
if (theme == null || theme.getPaint().equals(Theme.BLUE.getPaint()))
|
||||
if (Theme.DEFAULT.equals(Themes.getTheme()))
|
||||
return blueCss;
|
||||
|
||||
return toStyleSheetUri(".root {" +
|
||||
"-fx-base-color:" + theme.getColor() + ';' +
|
||||
"-fx-base-darker-color: derive(-fx-base-color, -10%);" +
|
||||
"-fx-base-check-color: derive(-fx-base-color, 30%);" +
|
||||
"-fx-rippler-color:" + rgba(theme.getPaint(), 0.3) + ';' +
|
||||
"-fx-base-rippler-color: derive(" + rgba(theme.getPaint(), 0.3) + ", 100%);" +
|
||||
"-fx-base-disabled-text-fill:" + rgba(theme.getForegroundColor(), 0.7) + ";" +
|
||||
"-fx-base-text-fill:" + Theme.getColorDisplayName(theme.getForegroundColor()) + ";" +
|
||||
"-theme-thumb:" + rgba(theme.getPaint(), 0.7) + ";" +
|
||||
'}');
|
||||
ColorScheme scheme = Themes.getColorScheme();
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("* {\n");
|
||||
for (ColorRole colorRole : ColorRole.ALL) {
|
||||
addColor(builder, colorRole.getVariableName(), scheme.getColor(colorRole));
|
||||
}
|
||||
|
||||
addColor(builder, "-monet-primary-seed", scheme.getPrimaryColorSeed());
|
||||
|
||||
addColor(builder, scheme, ColorRole.PRIMARY, 0.5);
|
||||
addColor(builder, scheme, ColorRole.SECONDARY_CONTAINER, 0.5);
|
||||
addColor(builder, scheme, ColorRole.SURFACE, 0.5);
|
||||
addColor(builder, scheme, ColorRole.SURFACE, 0.8);
|
||||
addColor(builder, scheme, ColorRole.ON_SURFACE_VARIANT, 0.38);
|
||||
addColor(builder, scheme, ColorRole.SURFACE_CONTAINER_LOW, 0.8);
|
||||
addColor(builder, scheme, ColorRole.SECONDARY_CONTAINER, 0.8);
|
||||
addColor(builder, scheme, ColorRole.INVERSE_SURFACE, 0.8);
|
||||
|
||||
builder.append("}\n");
|
||||
return toStyleSheetUri(builder.toString(), blueCss);
|
||||
}
|
||||
|
||||
public static void init(Scene scene) {
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
|
||||
@JsonAdapter(Theme.TypeAdapter.class)
|
||||
public final class Theme {
|
||||
public static final Theme BLUE = new Theme("blue", "#5C6BC0");
|
||||
public static final Color BLACK = Color.web("#292929");
|
||||
public static final Color[] SUGGESTED_COLORS = new Color[]{
|
||||
Color.web("#3D6DA3"), // blue
|
||||
Color.web("#283593"), // dark blue
|
||||
Color.web("#43A047"), // green
|
||||
Color.web("#E67E22"), // orange
|
||||
Color.web("#9C27B0"), // purple
|
||||
Color.web("#B71C1C") // red
|
||||
};
|
||||
|
||||
public static Theme getTheme() {
|
||||
Theme theme = config().getTheme();
|
||||
return theme == null ? BLUE : theme;
|
||||
}
|
||||
|
||||
private final Color paint;
|
||||
private final String color;
|
||||
private final String name;
|
||||
|
||||
Theme(String name, String color) {
|
||||
this.name = name;
|
||||
this.color = Objects.requireNonNull(color);
|
||||
this.paint = Color.web(color);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public Color getPaint() {
|
||||
return paint;
|
||||
}
|
||||
|
||||
public boolean isCustom() {
|
||||
return name.startsWith("#");
|
||||
}
|
||||
|
||||
public boolean isLight() {
|
||||
return paint.grayscale().getRed() >= 0.5;
|
||||
}
|
||||
|
||||
public Color getForegroundColor() {
|
||||
return isLight() ? Color.BLACK : Color.WHITE;
|
||||
}
|
||||
|
||||
public static Theme custom(String color) {
|
||||
if (!color.startsWith("#"))
|
||||
throw new IllegalArgumentException();
|
||||
return new Theme(color, color);
|
||||
}
|
||||
|
||||
public static Optional<Theme> getTheme(String name) {
|
||||
if (name == null)
|
||||
return Optional.empty();
|
||||
else if (name.startsWith("#"))
|
||||
try {
|
||||
Color.web(name);
|
||||
return Optional.of(custom(name));
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
}
|
||||
else {
|
||||
String color = null;
|
||||
switch (name.toLowerCase(Locale.ROOT)) {
|
||||
case "blue":
|
||||
return Optional.of(BLUE);
|
||||
case "darker_blue":
|
||||
color = "#283593";
|
||||
break;
|
||||
case "green":
|
||||
color = "#43A047";
|
||||
break;
|
||||
case "orange":
|
||||
color = "#E67E22";
|
||||
break;
|
||||
case "purple":
|
||||
color = "#9C27B0";
|
||||
break;
|
||||
case "red":
|
||||
color = "#F44336";
|
||||
}
|
||||
if (color != null)
|
||||
return Optional.of(new Theme(name, color));
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public static String getColorDisplayName(Color c) {
|
||||
return c != null ? String.format("#%02X%02X%02X", Math.round(c.getRed() * 255.0D), Math.round(c.getGreen() * 255.0D), Math.round(c.getBlue() * 255.0D)) : null;
|
||||
}
|
||||
|
||||
private static ObjectBinding<Color> FOREGROUND_FILL;
|
||||
|
||||
public static ObjectBinding<Color> foregroundFillBinding() {
|
||||
if (FOREGROUND_FILL == null)
|
||||
FOREGROUND_FILL = Bindings.createObjectBinding(
|
||||
() -> Theme.getTheme().getForegroundColor(),
|
||||
config().themeProperty()
|
||||
);
|
||||
|
||||
return FOREGROUND_FILL;
|
||||
}
|
||||
|
||||
public static Color blackFill() {
|
||||
return BLACK;
|
||||
}
|
||||
|
||||
public static Color whiteFill() {
|
||||
return Color.WHITE;
|
||||
}
|
||||
|
||||
public static class TypeAdapter extends com.google.gson.TypeAdapter<Theme> {
|
||||
@Override
|
||||
public void write(JsonWriter out, Theme value) throws IOException {
|
||||
out.value(value.getName().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Theme read(JsonReader in) throws IOException {
|
||||
return getTheme(in.nextString()).orElse(Theme.BLUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,9 @@ public enum VersionIconType {
|
||||
FURNACE("/assets/img/furnace.png"),
|
||||
QUILT("/assets/img/quilt.png"),
|
||||
APRIL_FOOLS("/assets/img/april_fools.png"),
|
||||
CLEANROOM("/assets/img/cleanroom.png");
|
||||
CLEANROOM("/assets/img/cleanroom.png"),
|
||||
LEGACY_FABRIC("/assets/img/legacyfabric.png")
|
||||
;
|
||||
|
||||
// Please append new items at last
|
||||
|
||||
|
||||
@@ -459,6 +459,20 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
showLogsProperty.set(showLogs);
|
||||
}
|
||||
|
||||
private final BooleanProperty enableDebugLogOutputProperty = new SimpleBooleanProperty(this, "enableDebugLogOutput", false);
|
||||
|
||||
public BooleanProperty enableDebugLogOutputProperty() {
|
||||
return enableDebugLogOutputProperty;
|
||||
}
|
||||
|
||||
public boolean isEnableDebugLogOutput() {
|
||||
return enableDebugLogOutputProperty.get();
|
||||
}
|
||||
|
||||
public void setEnableDebugLogOutput(boolean u) {
|
||||
this.enableDebugLogOutputProperty.set(u);
|
||||
}
|
||||
|
||||
// Minecraft settings.
|
||||
|
||||
private final StringProperty serverIpProperty = new SimpleStringProperty(this, "serverIp", "");
|
||||
@@ -776,6 +790,7 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
obj.addProperty("notCheckJVM", src.isNotCheckJVM());
|
||||
obj.addProperty("notPatchNatives", src.isNotPatchNatives());
|
||||
obj.addProperty("showLogs", src.isShowLogs());
|
||||
obj.addProperty("enableDebugLogOutput", src.isEnableDebugLogOutput());
|
||||
obj.addProperty("gameDir", src.getGameDir());
|
||||
obj.addProperty("launcherVisibility", src.getLauncherVisibility().ordinal());
|
||||
obj.addProperty("processPriority", src.getProcessPriority().ordinal());
|
||||
@@ -847,6 +862,7 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
vs.setNotCheckJVM(Optional.ofNullable(obj.get("notCheckJVM")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNotPatchNatives(Optional.ofNullable(obj.get("notPatchNatives")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setShowLogs(Optional.ofNullable(obj.get("showLogs")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setEnableDebugLogOutput(Optional.ofNullable(obj.get("enableDebugLogOutput")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setLauncherVisibility(parseJsonPrimitive(obj.getAsJsonPrimitive("launcherVisibility"), LauncherVisibility.class, LauncherVisibility.HIDE));
|
||||
vs.setProcessPriority(parseJsonPrimitive(obj.getAsJsonPrimitive("processPriority"), ProcessPriority.class, ProcessPriority.NORMAL));
|
||||
vs.setUseNativeGLFW(Optional.ofNullable(obj.get("useNativeGLFW")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.terracotta;
|
||||
|
||||
import kala.compress.archivers.tar.TarArchiveEntry;
|
||||
import org.jackhuang.hmcl.download.ArtifactMalformedException;
|
||||
import org.jackhuang.hmcl.task.FetchTask;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.terracotta.provider.AbstractTerracottaProvider;
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.tree.TarFileTree;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.DigestInputStream;
|
||||
import java.security.DigestOutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class TerracottaBundle {
|
||||
private final Path root;
|
||||
|
||||
private final List<URI> links;
|
||||
|
||||
private final FileDownloadTask.IntegrityCheck hash;
|
||||
|
||||
private final Map<String, FileDownloadTask.IntegrityCheck> files;
|
||||
|
||||
public TerracottaBundle(Path root, List<URI> links, FileDownloadTask.IntegrityCheck hash, Map<String, FileDownloadTask.IntegrityCheck> files) {
|
||||
this.root = root;
|
||||
this.links = links;
|
||||
this.hash = hash;
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
public Task<Path> download(AbstractTerracottaProvider.DownloadContext context) {
|
||||
return Task.supplyAsync(() -> Files.createTempFile("terracotta-", ".tar.gz"))
|
||||
.thenComposeAsync(Schedulers.javafx(), pkg -> {
|
||||
FileDownloadTask download = new FileDownloadTask(links, pkg, hash) {
|
||||
@Override
|
||||
protected Context getContext(HttpResponse<?> response, boolean checkETag, String bmclapiHash) throws IOException {
|
||||
FetchTask.Context delegate = super.getContext(response, checkETag, bmclapiHash);
|
||||
return new Context() {
|
||||
@Override
|
||||
public void withResult(boolean success) {
|
||||
delegate.withResult(success);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer, int offset, int len) throws IOException {
|
||||
context.checkCancellation();
|
||||
delegate.write(buffer, offset, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (isSuccess()) {
|
||||
context.checkCancellation();
|
||||
}
|
||||
|
||||
delegate.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
context.bindProgress(download.progressProperty());
|
||||
return download.thenSupplyAsync(() -> pkg);
|
||||
});
|
||||
}
|
||||
|
||||
public Task<?> install(Path pkg) {
|
||||
return Task.runAsync(() -> {
|
||||
Files.createDirectories(root);
|
||||
FileUtils.cleanDirectory(root);
|
||||
|
||||
try (TarFileTree tree = TarFileTree.open(pkg)) {
|
||||
for (Map.Entry<String, FileDownloadTask.IntegrityCheck> entry : files.entrySet()) {
|
||||
String file = entry.getKey();
|
||||
FileDownloadTask.IntegrityCheck check = entry.getValue();
|
||||
|
||||
Path path = root.resolve(file);
|
||||
TarArchiveEntry archive = tree.getEntry("/" + file);
|
||||
if (archive == null) {
|
||||
throw new ArtifactMalformedException(String.format("Expecting %s file in terracotta bundle.", file));
|
||||
}
|
||||
|
||||
MessageDigest digest = DigestUtils.getDigest(check.getAlgorithm());
|
||||
try (
|
||||
InputStream is = tree.getInputStream(archive);
|
||||
OutputStream os = new DigestOutputStream(Files.newOutputStream(path), digest)
|
||||
) {
|
||||
is.transferTo(os);
|
||||
}
|
||||
|
||||
String hash = HexFormat.of().formatHex(digest.digest());
|
||||
if (!check.getChecksum().equalsIgnoreCase(hash)) {
|
||||
throw new ChecksumMismatchException(check.getAlgorithm(), check.getChecksum(), hash);
|
||||
}
|
||||
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case LINUX, MACOS, FREEBSD -> Files.setPosixFilePermissions(path, FileUtils.parsePosixFilePermission(archive.getMode()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}).whenComplete(exception -> {
|
||||
if (exception != null) {
|
||||
FileUtils.deleteDirectory(root);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Path locate(String file) {
|
||||
FileDownloadTask.IntegrityCheck check = files.get(file);
|
||||
if (check == null) {
|
||||
throw new AssertionError(String.format("Expecting %s file in terracotta bundle.", file));
|
||||
}
|
||||
return root.resolve(file).toAbsolutePath();
|
||||
}
|
||||
|
||||
public AbstractTerracottaProvider.Status status() throws IOException {
|
||||
if (Files.exists(root) && isLocalBundleValid()) {
|
||||
return AbstractTerracottaProvider.Status.READY;
|
||||
}
|
||||
|
||||
try {
|
||||
if (TerracottaMetadata.hasLegacyVersionFiles()) {
|
||||
return AbstractTerracottaProvider.Status.LEGACY_VERSION;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Logger.LOG.warning("Cannot determine whether legacy versions exist.", e);
|
||||
}
|
||||
return AbstractTerracottaProvider.Status.NOT_EXIST;
|
||||
}
|
||||
|
||||
private boolean isLocalBundleValid() throws IOException { // FIXME: Make control flow clearer.
|
||||
long total = 0;
|
||||
byte[] buffer = new byte[8192];
|
||||
|
||||
for (Map.Entry<String, FileDownloadTask.IntegrityCheck> entry : files.entrySet()) {
|
||||
Path path = root.resolve(entry.getKey());
|
||||
FileDownloadTask.IntegrityCheck check = entry.getValue();
|
||||
if (!Files.isReadable(path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MessageDigest digest = DigestUtils.getDigest(check.getAlgorithm());
|
||||
try (InputStream is = new DigestInputStream(Files.newInputStream(path), digest)) {
|
||||
int n;
|
||||
while ((n = is.read(buffer)) >= 0) {
|
||||
total += n;
|
||||
if (total >= 50 * 1024 * 1024) { // >=50MB
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!HexFormat.of().formatHex(digest.digest()).equalsIgnoreCase(check.getChecksum())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.terracotta;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ReadOnlyDoubleWrapper;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
@@ -29,26 +28,32 @@ import org.jackhuang.hmcl.task.DownloadException;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.terracotta.provider.ITerracottaProvider;
|
||||
import org.jackhuang.hmcl.terracotta.provider.AbstractTerracottaProvider;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
import org.jackhuang.hmcl.util.tree.TarFileTree;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -61,140 +66,166 @@ public final class TerracottaManager {
|
||||
private static final InvocationDispatcher<TerracottaState> STATE_D = InvocationDispatcher.runOn(Platform::runLater, STATE::set);
|
||||
|
||||
static {
|
||||
Task.runAsync(() -> {
|
||||
if (TerracottaMetadata.PROVIDER == null) {
|
||||
setState(new TerracottaState.Fatal(TerracottaState.Fatal.Type.OS));
|
||||
LOG.warning("Terracotta hasn't support your OS: " + org.jackhuang.hmcl.util.platform.Platform.SYSTEM_PLATFORM);
|
||||
} else {
|
||||
Schedulers.io().execute(() -> {
|
||||
try {
|
||||
if (TerracottaMetadata.PROVIDER == null)
|
||||
throw new IOException("Unsupported platform: " + org.jackhuang.hmcl.util.platform.Platform.CURRENT_PLATFORM);
|
||||
|
||||
switch (TerracottaMetadata.PROVIDER.status()) {
|
||||
case NOT_EXIST -> setState(new TerracottaState.Uninitialized(false));
|
||||
case LEGACY_VERSION -> setState(new TerracottaState.Uninitialized(true));
|
||||
case READY -> launch(setState(new TerracottaState.Launching()));
|
||||
case READY -> launch(setState(new TerracottaState.Launching()), false);
|
||||
}
|
||||
}
|
||||
}).whenComplete(exception -> {
|
||||
if (exception != null) {
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Cannot initialize Terracotta.", e);
|
||||
compareAndSet(TerracottaState.Bootstrap.INSTANCE, new TerracottaState.Fatal(TerracottaState.Fatal.Type.UNKNOWN));
|
||||
}
|
||||
}).start();
|
||||
});
|
||||
}
|
||||
|
||||
public static ReadOnlyObjectProperty<TerracottaState> stateProperty() {
|
||||
return STATE.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
static {
|
||||
Lang.thread(() -> {
|
||||
while (true) {
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (!(state instanceof TerracottaState.PortSpecific portSpecific)) {
|
||||
LockSupport.parkNanos(500_000);
|
||||
private static final Thread DAEMON = Lang.thread(TerracottaManager::runBackground, "Terracotta Background Daemon", true);
|
||||
|
||||
@FXThread // Written in FXThread, read-only on background daemon
|
||||
private static volatile boolean daemonRunning = false;
|
||||
|
||||
private static void runBackground() {
|
||||
final long ACTIVE = TimeUnit.MILLISECONDS.toNanos(500);
|
||||
final long BACKGROUND = TimeUnit.SECONDS.toMillis(15);
|
||||
|
||||
while (true) {
|
||||
if (daemonRunning) {
|
||||
LockSupport.parkNanos(ACTIVE);
|
||||
} else {
|
||||
long deadline = System.currentTimeMillis() + BACKGROUND;
|
||||
do {
|
||||
LockSupport.parkUntil(deadline);
|
||||
} while (!daemonRunning && System.currentTimeMillis() < deadline - 100);
|
||||
}
|
||||
|
||||
if (!(STATE_V.get() instanceof TerracottaState.PortSpecific state)) {
|
||||
continue;
|
||||
}
|
||||
int port = state.port;
|
||||
int index = state instanceof TerracottaState.Ready ready ? ready.index : Integer.MIN_VALUE;
|
||||
|
||||
TerracottaState next;
|
||||
try {
|
||||
TerracottaState.Ready object = HttpRequest.GET(String.format("http://127.0.0.1:%d/state", port))
|
||||
.retry(5)
|
||||
.getJson(TerracottaState.Ready.class);
|
||||
if (object.index <= index) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int port = portSpecific.port;
|
||||
int index = state instanceof TerracottaState.Ready ready ? ready.index : Integer.MIN_VALUE;
|
||||
|
||||
TerracottaState next;
|
||||
try {
|
||||
next = new GetTask(URI.create(String.format("http://127.0.0.1:%d/state", port)))
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenApplyAsync(jsonString -> {
|
||||
TerracottaState.Ready object = JsonUtils.fromNonNullJson(jsonString, TypeToken.get(TerracottaState.Ready.class));
|
||||
if (object.index <= index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
object.port = port;
|
||||
return object;
|
||||
})
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.run();
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Cannot fetch state from Terracotta.", e);
|
||||
next = new TerracottaState.Fatal(TerracottaState.Fatal.Type.TERRACOTTA);
|
||||
}
|
||||
|
||||
if (next != null) {
|
||||
compareAndSet(state, next);
|
||||
}
|
||||
|
||||
LockSupport.parkNanos(500_000);
|
||||
object.port = port;
|
||||
next = object;
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Cannot fetch state from Terracotta.", e);
|
||||
next = new TerracottaState.Fatal(TerracottaState.Fatal.Type.TERRACOTTA);
|
||||
}
|
||||
}, "Terracotta Background Daemon", true);
|
||||
|
||||
compareAndSet(state, next);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean validate(Path file) {
|
||||
return FileUtils.getName(file).equalsIgnoreCase(TerracottaMetadata.PACKAGE_NAME);
|
||||
}
|
||||
|
||||
public static TerracottaState.Preparing install(@Nullable Path file) {
|
||||
@FXThread
|
||||
public static void switchDaemon(boolean active) {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (!(state instanceof TerracottaState.Uninitialized ||
|
||||
state instanceof TerracottaState.Preparing preparing && preparing.hasInstallFence() ||
|
||||
state instanceof TerracottaState.Fatal && ((TerracottaState.Fatal) state).isRecoverable())
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (file != null && !FileUtils.getName(file).equalsIgnoreCase(TerracottaMetadata.PACKAGE_NAME)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TerracottaState.Preparing preparing;
|
||||
if (state instanceof TerracottaState.Preparing it) {
|
||||
preparing = it;
|
||||
} else {
|
||||
preparing = new TerracottaState.Preparing(new ReadOnlyDoubleWrapper(-1));
|
||||
}
|
||||
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
return file != null ? TarFileTree.open(file) : null;
|
||||
}).thenComposeAsync(Schedulers.javafx(), tree -> {
|
||||
return getProvider().install(preparing, tree).whenComplete(exception -> {
|
||||
if (tree != null) {
|
||||
tree.close();
|
||||
}
|
||||
if (exception != null) {
|
||||
throw exception;
|
||||
}
|
||||
});
|
||||
}).whenComplete(exception -> {
|
||||
if (exception == null) {
|
||||
try {
|
||||
TerracottaMetadata.removeLegacyVersionFiles();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Unable to remove legacy terracotta files.", e);
|
||||
}
|
||||
|
||||
TerracottaState.Launching launching = new TerracottaState.Launching();
|
||||
if (compareAndSet(preparing, launching)) {
|
||||
launch(launching);
|
||||
}
|
||||
} else if (exception instanceof ITerracottaProvider.ArchiveFileMissingException) {
|
||||
LOG.warning("Cannot install terracotta from local package.", exception);
|
||||
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.INSTALL));
|
||||
} else if (exception instanceof DownloadException) {
|
||||
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.NETWORK));
|
||||
} else {
|
||||
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.INSTALL));
|
||||
boolean dr = daemonRunning;
|
||||
if (dr != active) {
|
||||
daemonRunning = active;
|
||||
if (active) {
|
||||
LockSupport.unpark(DAEMON);
|
||||
}
|
||||
}).start();
|
||||
|
||||
return setState(preparing);
|
||||
}
|
||||
}
|
||||
|
||||
private static ITerracottaProvider getProvider() {
|
||||
ITerracottaProvider provider = TerracottaMetadata.PROVIDER;
|
||||
private static AbstractTerracottaProvider getProvider() {
|
||||
AbstractTerracottaProvider provider = TerracottaMetadata.PROVIDER;
|
||||
if (provider == null) {
|
||||
throw new AssertionError("Terracotta Provider must NOT be null.");
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
public static TerracottaState recover(@Nullable Path file) {
|
||||
public static boolean isInvalidBundle(Path file) {
|
||||
return !FileUtils.getName(file).equalsIgnoreCase(TerracottaMetadata.PACKAGE_NAME);
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public static TerracottaState.Preparing download() {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (!(state instanceof TerracottaState.Uninitialized || state instanceof TerracottaState.Fatal && ((TerracottaState.Fatal) state).isRecoverable())
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TerracottaState.Preparing preparing = new TerracottaState.Preparing(new ReadOnlyDoubleWrapper(-1), true);
|
||||
|
||||
Task.composeAsync(() -> getProvider().download(preparing))
|
||||
.thenComposeAsync(pkg -> {
|
||||
if (!preparing.requestInstallFence()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getProvider().install(pkg).thenRunAsync(() -> {
|
||||
TerracottaState.Launching launching = new TerracottaState.Launching();
|
||||
if (compareAndSet(preparing, launching)) {
|
||||
launch(launching, true);
|
||||
}
|
||||
});
|
||||
}).whenComplete(exception -> {
|
||||
if (exception instanceof CancellationException) {
|
||||
// no-op
|
||||
} else if (exception instanceof DownloadException) {
|
||||
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.NETWORK));
|
||||
} else {
|
||||
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.INSTALL));
|
||||
}
|
||||
}).start();
|
||||
|
||||
return compareAndSet(state, preparing) ? preparing : null;
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public static TerracottaState.Preparing install(Path bundle) {
|
||||
FXUtils.checkFxUserThread();
|
||||
if (isInvalidBundle(bundle)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TerracottaState state = STATE_V.get();
|
||||
TerracottaState.Preparing preparing;
|
||||
if (state instanceof TerracottaState.Preparing previousPreparing && previousPreparing.requestInstallFence()) {
|
||||
preparing = previousPreparing;
|
||||
} else if (state instanceof TerracottaState.Uninitialized || state instanceof TerracottaState.Fatal && ((TerracottaState.Fatal) state).isRecoverable()) {
|
||||
preparing = new TerracottaState.Preparing(new ReadOnlyDoubleWrapper(-1), false);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
Task.composeAsync(() -> getProvider().install(bundle))
|
||||
.thenRunAsync(() -> {
|
||||
TerracottaState.Launching launching = new TerracottaState.Launching();
|
||||
if (compareAndSet(preparing, launching)) {
|
||||
launch(launching, true);
|
||||
}
|
||||
})
|
||||
.whenComplete(exception -> {
|
||||
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.INSTALL));
|
||||
}).start();
|
||||
|
||||
return state != preparing && compareAndSet(state, preparing) ? preparing : null;
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public static TerracottaState recover() {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
TerracottaState state = STATE_V.get();
|
||||
@@ -203,21 +234,23 @@ public final class TerracottaManager {
|
||||
}
|
||||
|
||||
try {
|
||||
// FIXME: A temporary limit has been employed in TerracottaBundle#checkExisting, making
|
||||
// hash check accept 50MB at most. Calling it on JavaFX should be safe.
|
||||
return switch (getProvider().status()) {
|
||||
case NOT_EXIST, LEGACY_VERSION -> install(file);
|
||||
case NOT_EXIST, LEGACY_VERSION -> download();
|
||||
case READY -> {
|
||||
TerracottaState.Launching launching = setState(new TerracottaState.Launching());
|
||||
launch(launching);
|
||||
launch(launching, false);
|
||||
yield launching;
|
||||
}
|
||||
};
|
||||
} catch (NullPointerException | IOException e) {
|
||||
} catch (RuntimeException | IOException e) {
|
||||
LOG.warning("Cannot determine Terracotta state.", e);
|
||||
return setState(new TerracottaState.Fatal(TerracottaState.Fatal.Type.UNKNOWN));
|
||||
}
|
||||
}
|
||||
|
||||
private static void launch(TerracottaState.Launching state) {
|
||||
private static void launch(TerracottaState.Launching state, boolean removeLegacy) {
|
||||
Task.supplyAsync(() -> {
|
||||
Path path = Files.createTempDirectory(String.format("hmcl-terracotta-%d", ThreadLocalRandom.current().nextLong())).resolve("http").toAbsolutePath();
|
||||
ManagedProcess process = new ManagedProcess(new ProcessBuilder(getProvider().ofCommandLine(path)));
|
||||
@@ -235,7 +268,7 @@ public final class TerracottaManager {
|
||||
if (exitTime == -1) {
|
||||
exitTime = System.currentTimeMillis();
|
||||
} else if (System.currentTimeMillis() - exitTime >= 10000) {
|
||||
throw new IllegalStateException("Process has exited for 10s.");
|
||||
throw new IllegalStateException(String.format("Process has exited for 10s, code = %s", process.getExitCode()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,6 +276,9 @@ public final class TerracottaManager {
|
||||
TerracottaState next;
|
||||
if (exception == null) {
|
||||
next = new TerracottaState.Unknown(port);
|
||||
if (removeLegacy) {
|
||||
TerracottaMetadata.removeLegacyVersionFiles();
|
||||
}
|
||||
} else {
|
||||
next = new TerracottaState.Fatal(TerracottaState.Fatal.Type.TERRACOTTA);
|
||||
}
|
||||
@@ -277,24 +313,39 @@ public final class TerracottaManager {
|
||||
public static TerracottaState.HostScanning setScanning() {
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (state instanceof TerracottaState.PortSpecific portSpecific) {
|
||||
new GetTask(NetworkUtils.toURI(String.format(
|
||||
"http://127.0.0.1:%d/state/scanning?player=%s", portSpecific.port, getPlayerName()))
|
||||
).setSignificance(Task.TaskSignificance.MINOR).start();
|
||||
Task.supplyAsync(Schedulers.io(), TerracottaNodeList::fetch)
|
||||
.thenComposeAsync(nodes -> {
|
||||
List<Pair<String, String>> query = new ArrayList<>(nodes.size() + 1);
|
||||
query.add(pair("player", getPlayerName()));
|
||||
for (URI node : nodes) {
|
||||
query.add(pair("public_nodes", node.toString()));
|
||||
}
|
||||
return new GetTask(NetworkUtils.withQuery(
|
||||
"http://127.0.0.1:%d/state/scanning".formatted(portSpecific.port), query
|
||||
)).setSignificance(Task.TaskSignificance.MINOR);
|
||||
}).start();
|
||||
|
||||
return new TerracottaState.HostScanning(-1, -1, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Task<TerracottaState.GuestStarting> setGuesting(String room) {
|
||||
public static Task<TerracottaState.GuestConnecting> setGuesting(String room) {
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (state instanceof TerracottaState.PortSpecific portSpecific) {
|
||||
return new GetTask(NetworkUtils.toURI(String.format(
|
||||
"http://127.0.0.1:%d/state/guesting?room=%s&player=%s", portSpecific.port, room, getPlayerName()
|
||||
)))
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenSupplyAsync(() -> new TerracottaState.GuestStarting(-1, -1, null))
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
return Task.supplyAsync(Schedulers.io(), TerracottaNodeList::fetch)
|
||||
.thenComposeAsync(nodes -> {
|
||||
ArrayList<Pair<String, String>> query = new ArrayList<>(nodes.size() + 2);
|
||||
query.add(pair("room", room));
|
||||
query.add(pair("player", getPlayerName()));
|
||||
for (URI node : nodes) {
|
||||
query.add(pair("public_nodes", node.toString()));
|
||||
}
|
||||
return new GetTask(NetworkUtils.withQuery("http://127.0.0.1:%d/state/guesting".formatted(portSpecific.port), query))
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenSupplyAsync(() -> new TerracottaState.GuestConnecting(-1, -1, null))
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
});
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -20,16 +20,20 @@ package org.jackhuang.hmcl.terracotta;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.terracotta.provider.AbstractTerracottaProvider;
|
||||
import org.jackhuang.hmcl.terracotta.provider.GeneralProvider;
|
||||
import org.jackhuang.hmcl.terracotta.provider.ITerracottaProvider;
|
||||
import org.jackhuang.hmcl.terracotta.provider.MacOSProvider;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.LocalizedText;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OSVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionRange;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -38,12 +42,11 @@ import java.net.URI;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -51,41 +54,60 @@ public final class TerracottaMetadata {
|
||||
private TerracottaMetadata() {
|
||||
}
|
||||
|
||||
public record Link(@SerializedName("desc") LocalizedText description, String link) {
|
||||
private record Options(String version, String classifier) {
|
||||
public String replace(String value) {
|
||||
return value.replace("${version}", version).replace("${classifier}", classifier);
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable
|
||||
public record Link(
|
||||
@SerializedName("desc") LocalizedText description,
|
||||
@SerializedName("link") String link
|
||||
) {
|
||||
}
|
||||
|
||||
@JsonSerializable
|
||||
private record Package(
|
||||
@SerializedName("hash") String hash,
|
||||
@SerializedName("files") Map<String, String> files
|
||||
) {
|
||||
}
|
||||
|
||||
@JsonSerializable
|
||||
private record Config(
|
||||
@SerializedName("version_legacy") String legacy,
|
||||
@SerializedName("version_recent") List<String> recent,
|
||||
@SerializedName("version_latest") String latest,
|
||||
|
||||
@SerializedName("classifiers") Map<String, String> classifiers,
|
||||
@SerializedName("packages") Map<String, Package> pkgs,
|
||||
@SerializedName("downloads") List<String> downloads,
|
||||
@SerializedName("downloads_CN") List<String> downloadsCN,
|
||||
@SerializedName("links") List<Link> links
|
||||
) {
|
||||
private TerracottaNative of(String classifier) {
|
||||
List<URI> links = new ArrayList<>(this.downloads.size());
|
||||
for (String download : this.downloads) {
|
||||
links.add(URI.create(download.replace("${version}", this.latest).replace("${classifier}", classifier)));
|
||||
private @Nullable TerracottaBundle resolve(Options options) {
|
||||
Package pkg = pkgs.get(options.classifier);
|
||||
if (pkg == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String hash = Objects.requireNonNull(this.classifiers.get(classifier), String.format("Classifier %s doesn't exist.", classifier));
|
||||
if (!hash.startsWith("sha256:")) {
|
||||
throw new IllegalArgumentException(String.format("Invalid hash value %s for classifier %s.", hash, classifier));
|
||||
}
|
||||
hash = hash.substring("sha256:".length());
|
||||
Stream<String> stream = downloads.stream(), streamCN = downloadsCN.stream();
|
||||
List<URI> links = (LocaleUtils.IS_CHINA_MAINLAND ? Stream.concat(streamCN, stream) : Stream.concat(stream, streamCN))
|
||||
.map(link -> URI.create(options.replace(link)))
|
||||
.toList();
|
||||
|
||||
return new TerracottaNative(
|
||||
Collections.unmodifiableList(links),
|
||||
Metadata.DEPENDENCIES_DIRECTORY.resolve(
|
||||
String.format("terracotta/%s/terracotta-%s-%s", this.latest, this.latest, classifier)
|
||||
).toAbsolutePath(),
|
||||
new FileDownloadTask.IntegrityCheck("SHA-256", hash)
|
||||
Map<String, FileDownloadTask.IntegrityCheck> files = pkg.files.entrySet().stream().collect(Collectors.toUnmodifiableMap(
|
||||
Map.Entry::getKey,
|
||||
entry -> new FileDownloadTask.IntegrityCheck("SHA-512", entry.getValue())
|
||||
));
|
||||
|
||||
return new TerracottaBundle(
|
||||
Metadata.DEPENDENCIES_DIRECTORY.resolve(options.replace("terracotta/${version}")).toAbsolutePath(),
|
||||
links, new FileDownloadTask.IntegrityCheck("SHA-512", pkg.hash),
|
||||
files
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static final ITerracottaProvider PROVIDER;
|
||||
public static final AbstractTerracottaProvider PROVIDER;
|
||||
public static final String PACKAGE_NAME;
|
||||
public static final List<Link> PACKAGE_LINKS;
|
||||
public static final String FEEDBACK_LINK = NetworkUtils.withQuery("https://docs.hmcl.net/multiplayer/feedback.html", Map.of(
|
||||
@@ -93,8 +115,6 @@ public final class TerracottaMetadata {
|
||||
"launcher_version", Metadata.VERSION
|
||||
));
|
||||
|
||||
private static final Pattern LEGACY;
|
||||
private static final List<String> RECENT;
|
||||
private static final String LATEST;
|
||||
|
||||
static {
|
||||
@@ -105,100 +125,80 @@ public final class TerracottaMetadata {
|
||||
throw new ExceptionInInitializerError(e);
|
||||
}
|
||||
|
||||
LEGACY = Pattern.compile(config.legacy);
|
||||
RECENT = config.recent;
|
||||
LATEST = config.latest;
|
||||
|
||||
ProviderContext context = locateProvider(config);
|
||||
PROVIDER = context != null ? context.provider() : null;
|
||||
PACKAGE_NAME = context != null ? String.format("terracotta-%s-%s-pkg.tar.gz", config.latest, context.branch) : null;
|
||||
|
||||
if (context != null) {
|
||||
List<Link> packageLinks = new ArrayList<>(config.links.size());
|
||||
for (Link link : config.links) {
|
||||
packageLinks.add(new Link(
|
||||
link.description,
|
||||
link.link.replace("${version}", LATEST)
|
||||
.replace("${classifier}", context.branch)
|
||||
));
|
||||
}
|
||||
Options options = new Options(config.latest, OperatingSystem.CURRENT_OS.getCheckedName() + "-" + Architecture.SYSTEM_ARCH.getCheckedName());
|
||||
TerracottaBundle bundle = config.resolve(options);
|
||||
AbstractTerracottaProvider provider;
|
||||
if (bundle == null || (provider = locateProvider(bundle, options)) == null) {
|
||||
PROVIDER = null;
|
||||
PACKAGE_NAME = null;
|
||||
PACKAGE_LINKS = null;
|
||||
} else {
|
||||
PROVIDER = provider;
|
||||
PACKAGE_NAME = options.replace("terracotta-${version}-${classifier}-pkg.tar.gz");
|
||||
|
||||
List<Link> packageLinks = config.links.stream()
|
||||
.map(link -> new Link(link.description, options.replace(link.link)))
|
||||
.collect(Collectors.toList());
|
||||
Collections.shuffle(packageLinks);
|
||||
PACKAGE_LINKS = Collections.unmodifiableList(packageLinks);
|
||||
} else {
|
||||
PACKAGE_LINKS = null;
|
||||
}
|
||||
}
|
||||
|
||||
private record ProviderContext(ITerracottaProvider provider, String branch) {
|
||||
ProviderContext(ITerracottaProvider provider, String system, String arch) {
|
||||
this(provider, system + "-" + arch);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static ProviderContext locateProvider(Config config) {
|
||||
String architecture = switch (Architecture.SYSTEM_ARCH) {
|
||||
case X86_64 -> "x86_64";
|
||||
case ARM64 -> "arm64";
|
||||
default -> null;
|
||||
};
|
||||
if (architecture == null) {
|
||||
return null;
|
||||
}
|
||||
private static AbstractTerracottaProvider locateProvider(TerracottaBundle bundle, Options options) {
|
||||
String prefix = options.replace("terracotta-${version}-${classifier}");
|
||||
|
||||
// FIXME: As HMCL is a cross-platform application, developers may mistakenly locate
|
||||
// non-existent files in non-native platform logic without assertion errors during debugging.
|
||||
return switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS -> {
|
||||
if (OperatingSystem.SYSTEM_VERSION.isAtLeast(OSVersion.WINDOWS_8_1)) {
|
||||
yield new ProviderContext(
|
||||
new GeneralProvider(config.of(String.format("windows-%s.exe", architecture))),
|
||||
"windows", architecture
|
||||
);
|
||||
}
|
||||
yield null;
|
||||
if (!OperatingSystem.SYSTEM_VERSION.isAtLeast(OSVersion.WINDOWS_10))
|
||||
yield null;
|
||||
|
||||
yield new GeneralProvider(bundle, bundle.locate(prefix + ".exe"));
|
||||
}
|
||||
case LINUX -> new ProviderContext(
|
||||
new GeneralProvider(config.of(String.format("linux-%s", architecture))),
|
||||
"linux", architecture
|
||||
);
|
||||
case MACOS -> new ProviderContext(
|
||||
new MacOSProvider(
|
||||
config.of(String.format("macos-%s.pkg", architecture)),
|
||||
config.of(String.format("macos-%s", architecture))
|
||||
),
|
||||
"macos", architecture
|
||||
case LINUX, FREEBSD -> new GeneralProvider(bundle, bundle.locate(prefix));
|
||||
case MACOS -> new MacOSProvider(
|
||||
bundle, bundle.locate(prefix), bundle.locate(prefix + ".pkg")
|
||||
);
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public static void removeLegacyVersionFiles() throws IOException {
|
||||
try (DirectoryStream<Path> terracotta = Files.newDirectoryStream(Metadata.DEPENDENCIES_DIRECTORY.resolve("terracotta").toAbsolutePath())) {
|
||||
for (Path path : terracotta) {
|
||||
String name = FileUtils.getName(path);
|
||||
if (LATEST.equals(name) || RECENT.contains(name) || !LEGACY.matcher(name).matches()) {
|
||||
continue;
|
||||
}
|
||||
public static void removeLegacyVersionFiles() {
|
||||
try (DirectoryStream<Path> terracotta = collectLegacyVersionFiles()) {
|
||||
if (terracotta == null)
|
||||
return;
|
||||
|
||||
for (Path path : terracotta) {
|
||||
try {
|
||||
FileUtils.deleteDirectory(path);
|
||||
} catch (IOException e) {
|
||||
LOG.warning(String.format("Unable to remove legacy terracotta files: %s", path), e);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Unable to remove legacy terracotta files.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hasLegacyVersionFiles() throws IOException {
|
||||
try (DirectoryStream<Path> terracotta = Files.newDirectoryStream(Metadata.DEPENDENCIES_DIRECTORY.resolve("terracotta").toAbsolutePath())) {
|
||||
for (Path path : terracotta) {
|
||||
String name = FileUtils.getName(path);
|
||||
if (!LATEST.equals(name) && (RECENT.contains(name) || LEGACY.matcher(name).matches())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
try (DirectoryStream<Path> terracotta = collectLegacyVersionFiles()) {
|
||||
return terracotta != null && terracotta.iterator().hasNext();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
private static @Nullable DirectoryStream<Path> collectLegacyVersionFiles() throws IOException {
|
||||
Path terracottaDir = Metadata.DEPENDENCIES_DIRECTORY.resolve("terracotta");
|
||||
if (Files.notExists(terracottaDir))
|
||||
return null;
|
||||
|
||||
VersionRange<VersionNumber> range = VersionNumber.atMost(LATEST);
|
||||
return Files.newDirectoryStream(terracottaDir, path -> {
|
||||
String name = FileUtils.getName(path);
|
||||
return !LATEST.equals(name) && range.contains(VersionNumber.asVersion(name));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.terracotta;
|
||||
|
||||
import kala.compress.archivers.tar.TarArchiveEntry;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.terracotta.provider.ITerracottaProvider;
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.tree.TarFileTree;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
public final class TerracottaNative {
|
||||
private final List<URI> links;
|
||||
private final FileDownloadTask.IntegrityCheck checking;
|
||||
private final Path path;
|
||||
|
||||
public TerracottaNative(List<URI> links, Path path, FileDownloadTask.IntegrityCheck checking) {
|
||||
this.links = links;
|
||||
this.path = path;
|
||||
this.checking = checking;
|
||||
}
|
||||
|
||||
public Path getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public Task<?> install(ITerracottaProvider.Context context, @Nullable TarFileTree tree) {
|
||||
if (tree == null) {
|
||||
return new FileDownloadTask(links, path, checking) {
|
||||
@Override
|
||||
protected Context getContext(HttpResponse<?> response, boolean checkETag, String bmclapiHash) throws IOException {
|
||||
Context delegate = super.getContext(response, checkETag, bmclapiHash);
|
||||
return new Context() {
|
||||
@Override
|
||||
public void withResult(boolean success) {
|
||||
delegate.withResult(success);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer, int offset, int len) throws IOException {
|
||||
if (!context.hasInstallFence()) {
|
||||
throw new CancellationException("User has installed terracotta from local archives.");
|
||||
}
|
||||
delegate.write(buffer, offset, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (isSuccess() && !context.requestInstallFence()) {
|
||||
throw new CancellationException();
|
||||
}
|
||||
|
||||
delegate.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return Task.runAsync(() -> {
|
||||
String name = FileUtils.getName(path);
|
||||
TarArchiveEntry entry = tree.getRoot().getFiles().get(name);
|
||||
if (entry == null) {
|
||||
throw new ITerracottaProvider.ArchiveFileMissingException("Cannot exact entry: " + name);
|
||||
}
|
||||
|
||||
if (!context.requestInstallFence()) {
|
||||
throw new CancellationException();
|
||||
}
|
||||
|
||||
Files.createDirectories(path.toAbsolutePath().getParent());
|
||||
|
||||
MessageDigest digest = DigestUtils.getDigest(checking.getAlgorithm());
|
||||
try (
|
||||
InputStream stream = tree.getInputStream(entry);
|
||||
OutputStream os = Files.newOutputStream(path)
|
||||
) {
|
||||
stream.transferTo(new OutputStream() {
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
os.write(b);
|
||||
digest.update((byte) b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte @NotNull [] buffer, int offset, int len) throws IOException {
|
||||
os.write(buffer, offset, len);
|
||||
digest.update(buffer, offset, len);
|
||||
}
|
||||
});
|
||||
}
|
||||
String checksum = HexFormat.of().formatHex(digest.digest());
|
||||
if (!checksum.equalsIgnoreCase(checking.getChecksum())) {
|
||||
Files.delete(path);
|
||||
throw new ITerracottaProvider.ArchiveFileMissingException("Incorrect checksum (" + checking.getAlgorithm() + "), expected: " + checking.getChecksum() + ", actual: " + checksum);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ITerracottaProvider.Status status() throws IOException {
|
||||
if (Files.exists(path)) {
|
||||
if (DigestUtils.digestToString(checking.getAlgorithm(), path).equalsIgnoreCase(checking.getChecksum())) {
|
||||
return ITerracottaProvider.Status.READY;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (TerracottaMetadata.hasLegacyVersionFiles()) {
|
||||
return ITerracottaProvider.Status.LEGACY_VERSION;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Logger.LOG.warning("Cannot determine whether legacy versions exist.");
|
||||
}
|
||||
return ITerracottaProvider.Status.NOT_EXIST;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.terracotta;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// @author Glavo
|
||||
public final class TerracottaNodeList {
|
||||
private static final String NODE_LIST_URL = "https://terracotta.glavo.site/nodes";
|
||||
|
||||
@JsonSerializable
|
||||
private record TerracottaNode(String url, @Nullable String region) implements Validation {
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
Validation.requireNonNull(url, "TerracottaNode.url cannot be null");
|
||||
try {
|
||||
new URI(url);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new JsonParseException("Invalid URL: " + url, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static volatile List<URI> list;
|
||||
|
||||
public static List<URI> fetch() {
|
||||
List<URI> list = TerracottaNodeList.list;
|
||||
if (list != null)
|
||||
return list;
|
||||
|
||||
synchronized (TerracottaNodeList.class) {
|
||||
list = TerracottaNodeList.list;
|
||||
if (list != null)
|
||||
return list;
|
||||
|
||||
try {
|
||||
List<TerracottaNode> nodes = HttpRequest.GET(NODE_LIST_URL)
|
||||
.getJson(JsonUtils.listTypeOf(TerracottaNode.class));
|
||||
|
||||
if (nodes == null) {
|
||||
list = List.of();
|
||||
LOG.info("No available Terracotta nodes found");
|
||||
} else {
|
||||
list = nodes.stream()
|
||||
.filter(node -> {
|
||||
if (node == null)
|
||||
return false;
|
||||
|
||||
try {
|
||||
node.validate();
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Invalid terracotta node: " + node, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return StringUtils.isBlank(node.region) || LocaleUtils.IS_CHINA_MAINLAND == "CN".equalsIgnoreCase(node.region);
|
||||
})
|
||||
.map(it -> URI.create(it.url()))
|
||||
.toList();
|
||||
LOG.info("Terracotta node list: " + list);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to fetch terracotta node list", e);
|
||||
list = List.of();
|
||||
}
|
||||
|
||||
TerracottaNodeList.list = list;
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
private TerracottaNodeList() {
|
||||
}
|
||||
}
|
||||
@@ -21,15 +21,16 @@ import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleWrapper;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.beans.value.ObservableDoubleValue;
|
||||
import org.jackhuang.hmcl.terracotta.profile.TerracottaProfile;
|
||||
import org.jackhuang.hmcl.terracotta.provider.ITerracottaProvider;
|
||||
import org.jackhuang.hmcl.terracotta.provider.AbstractTerracottaProvider;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSubtype;
|
||||
import org.jackhuang.hmcl.util.gson.JsonType;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public abstract sealed class TerracottaState {
|
||||
@@ -63,32 +64,38 @@ public abstract sealed class TerracottaState {
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Preparing extends TerracottaState implements ITerracottaProvider.Context {
|
||||
public static final class Preparing extends TerracottaState implements AbstractTerracottaProvider.DownloadContext {
|
||||
private final ReadOnlyDoubleWrapper progress;
|
||||
|
||||
private final AtomicBoolean installFence = new AtomicBoolean(false);
|
||||
private final AtomicBoolean hasInstallFence;
|
||||
|
||||
Preparing(ReadOnlyDoubleWrapper progress) {
|
||||
Preparing(ReadOnlyDoubleWrapper progress, boolean hasInstallFence) {
|
||||
this.progress = progress;
|
||||
this.hasInstallFence = new AtomicBoolean(hasInstallFence);
|
||||
}
|
||||
|
||||
public ReadOnlyDoubleProperty progressProperty() {
|
||||
return progress.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindProgress(ObservableValue<? extends Number> value) {
|
||||
progress.bind(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requestInstallFence() {
|
||||
return installFence.compareAndSet(false, true);
|
||||
return hasInstallFence.compareAndSet(true, false);
|
||||
}
|
||||
|
||||
public boolean hasInstallFence() {
|
||||
return hasInstallFence.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasInstallFence() {
|
||||
return !installFence.get();
|
||||
public void bindProgress(ObservableDoubleValue progress) {
|
||||
this.progress.bind(progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkCancellation() {
|
||||
if (!hasInstallFence()) {
|
||||
throw new CancellationException("User has installed terracotta from local archives.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +119,7 @@ public abstract sealed class TerracottaState {
|
||||
@JsonSubtype(clazz = HostScanning.class, name = "host-scanning"),
|
||||
@JsonSubtype(clazz = HostStarting.class, name = "host-starting"),
|
||||
@JsonSubtype(clazz = HostOK.class, name = "host-ok"),
|
||||
@JsonSubtype(clazz = GuestStarting.class, name = "guest-connecting"),
|
||||
@JsonSubtype(clazz = GuestConnecting.class, name = "guest-connecting"),
|
||||
@JsonSubtype(clazz = GuestStarting.class, name = "guest-starting"),
|
||||
@JsonSubtype(clazz = GuestOK.class, name = "guest-ok"),
|
||||
@JsonSubtype(clazz = Exception.class, name = "exception"),
|
||||
@@ -202,12 +209,34 @@ public abstract sealed class TerracottaState {
|
||||
}
|
||||
}
|
||||
|
||||
public static final class GuestStarting extends Ready {
|
||||
GuestStarting(int port, int index, String state) {
|
||||
public static final class GuestConnecting extends Ready {
|
||||
GuestConnecting(int port, int index, String state) {
|
||||
super(port, index, state);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class GuestStarting extends Ready {
|
||||
public enum Difficulty {
|
||||
UNKNOWN,
|
||||
EASIEST,
|
||||
SIMPLE,
|
||||
MEDIUM,
|
||||
TOUGH
|
||||
}
|
||||
|
||||
@SerializedName("difficulty")
|
||||
private final Difficulty difficulty;
|
||||
|
||||
GuestStarting(int port, int index, String state, Difficulty difficulty) {
|
||||
super(port, index, state);
|
||||
this.difficulty = difficulty;
|
||||
}
|
||||
|
||||
public Difficulty getDifficulty() {
|
||||
return difficulty;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class GuestOK extends Ready implements Validation {
|
||||
@SerializedName("url")
|
||||
private final String url;
|
||||
@@ -298,7 +327,7 @@ public abstract sealed class TerracottaState {
|
||||
}
|
||||
|
||||
public boolean isRecoverable() {
|
||||
return this.type != Type.OS && this.type != Type.UNKNOWN;
|
||||
return this.type != Type.UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.terracotta.provider;
|
||||
|
||||
import javafx.beans.value.ObservableDoubleValue;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.terracotta.TerracottaBundle;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
public abstract class AbstractTerracottaProvider {
|
||||
public enum Status {
|
||||
NOT_EXIST,
|
||||
LEGACY_VERSION,
|
||||
READY
|
||||
}
|
||||
|
||||
public interface DownloadContext {
|
||||
@FXThread
|
||||
void bindProgress(ObservableDoubleValue progress);
|
||||
|
||||
void checkCancellation() throws CancellationException;
|
||||
}
|
||||
|
||||
protected final TerracottaBundle bundle;
|
||||
|
||||
protected AbstractTerracottaProvider(TerracottaBundle bundle) {
|
||||
this.bundle = bundle;
|
||||
}
|
||||
|
||||
public Status status() throws IOException {
|
||||
return bundle.status();
|
||||
}
|
||||
|
||||
public final Task<Path> download(DownloadContext progress) {
|
||||
return bundle.download(progress);
|
||||
}
|
||||
|
||||
public Task<?> install(Path pkg) throws IOException {
|
||||
return bundle.install(pkg);
|
||||
}
|
||||
|
||||
public abstract List<String> ofCommandLine(Path portTransfer);
|
||||
}
|
||||
@@ -17,51 +17,21 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.terracotta.provider;
|
||||
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.terracotta.TerracottaNative;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.tree.TarFileTree;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jackhuang.hmcl.terracotta.TerracottaBundle;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class GeneralProvider implements ITerracottaProvider {
|
||||
private final TerracottaNative target;
|
||||
public final class GeneralProvider extends AbstractTerracottaProvider {
|
||||
private final Path executable;
|
||||
|
||||
public GeneralProvider(TerracottaNative target) {
|
||||
this.target = target;
|
||||
public GeneralProvider(TerracottaBundle bundle, Path executable) {
|
||||
super(bundle);
|
||||
this.executable = executable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Status status() throws IOException {
|
||||
return target.status();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> install(Context context, @Nullable TarFileTree tree) throws IOException {
|
||||
Task<?> task = target.install(context, tree);
|
||||
context.bindProgress(task.progressProperty());
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
||||
task = task.thenRunAsync(() -> Files.setPosixFilePermissions(target.getPath(), Set.of(
|
||||
PosixFilePermission.OWNER_READ,
|
||||
PosixFilePermission.OWNER_WRITE,
|
||||
PosixFilePermission.OWNER_EXECUTE,
|
||||
PosixFilePermission.GROUP_READ,
|
||||
PosixFilePermission.GROUP_EXECUTE,
|
||||
PosixFilePermission.OTHERS_READ,
|
||||
PosixFilePermission.OTHERS_EXECUTE
|
||||
)));
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> ofCommandLine(Path path) {
|
||||
return List.of(target.getPath().toString(), "--hmcl", path.toString());
|
||||
public List<String> ofCommandLine(Path portTransfer) {
|
||||
return List.of(executable.toString(), "--hmcl", portTransfer.toString());
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.terracotta.provider;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.tree.TarFileTree;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
public interface ITerracottaProvider {
|
||||
enum Status {
|
||||
NOT_EXIST,
|
||||
LEGACY_VERSION,
|
||||
READY
|
||||
}
|
||||
|
||||
interface Context {
|
||||
void bindProgress(ObservableValue<? extends Number> value);
|
||||
|
||||
boolean requestInstallFence();
|
||||
|
||||
boolean hasInstallFence();
|
||||
}
|
||||
|
||||
abstract class ProviderException extends IOException {
|
||||
public ProviderException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ProviderException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public ProviderException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
final class ArchiveFileMissingException extends ProviderException {
|
||||
public ArchiveFileMissingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ArchiveFileMissingException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public ArchiveFileMissingException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
Status status() throws IOException;
|
||||
|
||||
Task<?> install(Context context, @Nullable TarFileTree tree) throws IOException;
|
||||
|
||||
List<String> ofCommandLine(Path path);
|
||||
}
|
||||
@@ -17,81 +17,79 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.terracotta.provider;
|
||||
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.terracotta.TerracottaNative;
|
||||
import org.jackhuang.hmcl.terracotta.TerracottaBundle;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
import org.jackhuang.hmcl.util.tree.TarFileTree;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class MacOSProvider implements ITerracottaProvider {
|
||||
public final TerracottaNative installer, binary;
|
||||
public final class MacOSProvider extends AbstractTerracottaProvider {
|
||||
private final Path executable, installer;
|
||||
|
||||
public MacOSProvider(TerracottaNative installer, TerracottaNative binary) {
|
||||
public MacOSProvider(TerracottaBundle bundle, Path executable, Path installer) {
|
||||
super(bundle);
|
||||
this.executable = executable;
|
||||
this.installer = installer;
|
||||
this.binary = binary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Status status() throws IOException {
|
||||
assert binary != null;
|
||||
|
||||
if (!Files.exists(Path.of("/Applications/terracotta.app"))) {
|
||||
return Status.NOT_EXIST;
|
||||
}
|
||||
|
||||
return binary.status();
|
||||
return bundle.status();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> install(Context context, @Nullable TarFileTree tree) throws IOException {
|
||||
assert installer != null && binary != null;
|
||||
public Task<?> install(Path pkg) throws IOException {
|
||||
return super.install(pkg).thenComposeAsync(() -> {
|
||||
Path osascript = SystemUtils.which("osascript");
|
||||
if (osascript == null) {
|
||||
throw new IllegalStateException("Cannot locate 'osascript' system executable on MacOS for installing Terracotta.");
|
||||
}
|
||||
|
||||
Task<?> installerTask = installer.install(context, tree);
|
||||
Task<?> binaryTask = binary.install(context, tree);
|
||||
context.bindProgress(installerTask.progressProperty().add(binaryTask.progressProperty()).multiply(0.4)); // (1 + 1) * 0.4 = 0.8
|
||||
Path movedInstaller = Files.createTempDirectory(Metadata.HMCL_GLOBAL_DIRECTORY, "terracotta-pkg")
|
||||
.toRealPath()
|
||||
.resolve(FileUtils.getName(installer));
|
||||
Files.copy(installer, movedInstaller, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
return Task.allOf(
|
||||
installerTask.thenComposeAsync(() -> {
|
||||
ManagedProcess process = new ManagedProcess(new ProcessBuilder(
|
||||
"osascript",
|
||||
"-e",
|
||||
String.format(
|
||||
"do shell script \"installer -pkg %s -target /\" with prompt \"%s\" with administrator privileges",
|
||||
installer.getPath(),
|
||||
i18n("terracotta.sudo_installing")
|
||||
)
|
||||
ManagedProcess process = new ManagedProcess(new ProcessBuilder(
|
||||
osascript.toString(), "-e", String.format(
|
||||
"do shell script \"installer -pkg '%s' -target /\" with prompt \"%s\" with administrator privileges",
|
||||
movedInstaller, i18n("terracotta.sudo_installing")
|
||||
)));
|
||||
process.pumpInputStream(SystemUtils::onLogLine);
|
||||
process.pumpErrorStream(SystemUtils::onLogLine);
|
||||
|
||||
return Task.fromCompletableFuture(process.getProcess().onExit()).thenRunAsync(() -> {
|
||||
try {
|
||||
FileUtils.cleanDirectory(movedInstaller.getParent());
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Cannot remove temporary Terracotta package file.", e);
|
||||
}
|
||||
|
||||
if (process.getExitCode() != 0) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"Cannot install Terracotta %s: system installer exited with code %d", movedInstaller, process.getExitCode()
|
||||
));
|
||||
process.pumpInputStream(SystemUtils::onLogLine);
|
||||
process.pumpErrorStream(SystemUtils::onLogLine);
|
||||
|
||||
return Task.fromCompletableFuture(process.getProcess().onExit());
|
||||
}),
|
||||
binaryTask.thenRunAsync(() -> Files.setPosixFilePermissions(binary.getPath(), Set.of(
|
||||
PosixFilePermission.OWNER_READ,
|
||||
PosixFilePermission.OWNER_WRITE,
|
||||
PosixFilePermission.OWNER_EXECUTE,
|
||||
PosixFilePermission.GROUP_READ,
|
||||
PosixFilePermission.GROUP_EXECUTE,
|
||||
PosixFilePermission.OTHERS_READ,
|
||||
PosixFilePermission.OTHERS_EXECUTE
|
||||
)))
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> ofCommandLine(Path path) {
|
||||
assert binary != null;
|
||||
|
||||
return List.of(binary.getPath().toString(), "--hmcl", path.toString());
|
||||
return List.of(executable.toString(), "--hmcl", path.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.theme;
|
||||
|
||||
import org.glavo.monetfx.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/// @author Glavo
|
||||
public final class Theme {
|
||||
|
||||
public static final Theme DEFAULT = new Theme(ThemeColor.DEFAULT, Brightness.DEFAULT, ColorStyle.FIDELITY, Contrast.DEFAULT);
|
||||
|
||||
private final ThemeColor primaryColorSeed;
|
||||
private final Brightness brightness;
|
||||
private final ColorStyle colorStyle;
|
||||
private final Contrast contrast;
|
||||
|
||||
public Theme(ThemeColor primaryColorSeed,
|
||||
Brightness brightness,
|
||||
ColorStyle colorStyle,
|
||||
Contrast contrast
|
||||
) {
|
||||
this.primaryColorSeed = primaryColorSeed;
|
||||
this.brightness = brightness;
|
||||
this.colorStyle = colorStyle;
|
||||
this.contrast = contrast;
|
||||
}
|
||||
|
||||
public ColorScheme toColorScheme() {
|
||||
return ColorScheme.newBuilder()
|
||||
.setPrimaryColorSeed(primaryColorSeed.color())
|
||||
.setColorStyle(colorStyle)
|
||||
.setBrightness(brightness)
|
||||
.setSpecVersion(ColorSpecVersion.SPEC_2025)
|
||||
.setContrast(contrast)
|
||||
.build();
|
||||
}
|
||||
|
||||
public ThemeColor primaryColorSeed() {
|
||||
return primaryColorSeed;
|
||||
}
|
||||
|
||||
public Brightness brightness() {
|
||||
return brightness;
|
||||
}
|
||||
|
||||
public ColorStyle colorStyle() {
|
||||
return colorStyle;
|
||||
}
|
||||
|
||||
public Contrast contrast() {
|
||||
return contrast;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return obj == this || obj instanceof Theme that
|
||||
&& this.primaryColorSeed.color().equals(that.primaryColorSeed.color())
|
||||
&& this.brightness.equals(that.brightness)
|
||||
&& this.colorStyle.equals(that.colorStyle)
|
||||
&& this.contrast.equals(that.contrast);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(primaryColorSeed, brightness, colorStyle, contrast);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Theme[" +
|
||||
"primaryColorSeed=" + primaryColorSeed + ", " +
|
||||
"brightness=" + brightness + ", " +
|
||||
"colorStyle=" + colorStyle + ", " +
|
||||
"contrast=" + contrast + ']';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.theme;
|
||||
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.WeakListener;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.scene.control.ColorPicker;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/// @author Glavo
|
||||
@JsonAdapter(ThemeColor.TypeAdapter.class)
|
||||
@JsonSerializable
|
||||
public record ThemeColor(@NotNull String name, @NotNull Color color) {
|
||||
|
||||
public static final ThemeColor DEFAULT = new ThemeColor("blue", Color.web("#5C6BC0"));
|
||||
|
||||
public static final List<ThemeColor> STANDARD_COLORS = List.of(
|
||||
DEFAULT,
|
||||
new ThemeColor("darker_blue", Color.web("#283593")),
|
||||
new ThemeColor("green", Color.web("#43A047")),
|
||||
new ThemeColor("orange", Color.web("#E67E22")),
|
||||
new ThemeColor("purple", Color.web("#9C27B0")),
|
||||
new ThemeColor("red", Color.web("#B71C1C"))
|
||||
);
|
||||
|
||||
public static String getColorDisplayName(Color c) {
|
||||
return c != null ? String.format("#%02X%02X%02X",
|
||||
Math.round(c.getRed() * 255.0D),
|
||||
Math.round(c.getGreen() * 255.0D),
|
||||
Math.round(c.getBlue() * 255.0D))
|
||||
: null;
|
||||
}
|
||||
|
||||
public static String getColorDisplayNameWithOpacity(Color c, double opacity) {
|
||||
return c != null ? String.format("#%02X%02X%02X%02X",
|
||||
Math.round(c.getRed() * 255.0D),
|
||||
Math.round(c.getGreen() * 255.0D),
|
||||
Math.round(c.getBlue() * 255.0D),
|
||||
Math.round(opacity * 255.0))
|
||||
: null;
|
||||
}
|
||||
|
||||
public static @Nullable ThemeColor of(String name) {
|
||||
if (name == null)
|
||||
return null;
|
||||
|
||||
if (!name.startsWith("#")) {
|
||||
for (ThemeColor color : STANDARD_COLORS) {
|
||||
if (name.equalsIgnoreCase(color.name()))
|
||||
return color;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return new ThemeColor(name, Color.web(name));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Contract("null -> null; !null -> !null")
|
||||
public static ThemeColor of(Color color) {
|
||||
return color != null ? new ThemeColor(getColorDisplayName(color), color) : null;
|
||||
}
|
||||
|
||||
private static final class BidirectionalBinding implements InvalidationListener, WeakListener {
|
||||
private final WeakReference<ColorPicker> colorPickerRef;
|
||||
private final WeakReference<Property<ThemeColor>> propertyRef;
|
||||
private final int hashCode;
|
||||
|
||||
private boolean updating = false;
|
||||
|
||||
private BidirectionalBinding(ColorPicker colorPicker, Property<ThemeColor> property) {
|
||||
this.colorPickerRef = new WeakReference<>(colorPicker);
|
||||
this.propertyRef = new WeakReference<>(property);
|
||||
this.hashCode = System.identityHashCode(colorPicker) ^ System.identityHashCode(property);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidated(Observable sourceProperty) {
|
||||
if (!updating) {
|
||||
final ColorPicker colorPicker = colorPickerRef.get();
|
||||
final Property<ThemeColor> property = propertyRef.get();
|
||||
|
||||
if (colorPicker == null || property == null) {
|
||||
if (colorPicker != null) {
|
||||
colorPicker.valueProperty().removeListener(this);
|
||||
}
|
||||
|
||||
if (property != null) {
|
||||
property.removeListener(this);
|
||||
}
|
||||
} else {
|
||||
updating = true;
|
||||
try {
|
||||
if (property == sourceProperty) {
|
||||
ThemeColor newValue = property.getValue();
|
||||
colorPicker.setValue(newValue != null ? newValue.color() : null);
|
||||
} else {
|
||||
Color newValue = colorPicker.getValue();
|
||||
property.setValue(newValue != null ? ThemeColor.of(newValue) : null);
|
||||
}
|
||||
} finally {
|
||||
updating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wasGarbageCollected() {
|
||||
return colorPickerRef.get() == null || propertyRef.get() == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (!(o instanceof BidirectionalBinding that))
|
||||
return false;
|
||||
|
||||
final ColorPicker colorPicker = this.colorPickerRef.get();
|
||||
final Property<ThemeColor> property = this.propertyRef.get();
|
||||
|
||||
final ColorPicker thatColorPicker = that.colorPickerRef.get();
|
||||
final Property<?> thatProperty = that.propertyRef.get();
|
||||
|
||||
if (colorPicker == null || property == null || thatColorPicker == null || thatProperty == null)
|
||||
return false;
|
||||
|
||||
return colorPicker == thatColorPicker && property == thatProperty;
|
||||
}
|
||||
}
|
||||
|
||||
public static void bindBidirectional(ColorPicker colorPicker, Property<ThemeColor> property) {
|
||||
var binding = new BidirectionalBinding(colorPicker, property);
|
||||
|
||||
colorPicker.valueProperty().removeListener(binding);
|
||||
property.removeListener(binding);
|
||||
|
||||
ThemeColor themeColor = property.getValue();
|
||||
colorPicker.setValue(themeColor != null ? themeColor.color() : null);
|
||||
|
||||
colorPicker.valueProperty().addListener(binding);
|
||||
property.addListener(binding);
|
||||
}
|
||||
|
||||
static final class TypeAdapter extends com.google.gson.TypeAdapter<ThemeColor> {
|
||||
@Override
|
||||
public void write(JsonWriter out, ThemeColor value) throws IOException {
|
||||
out.value(value.name());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThemeColor read(JsonReader in) throws IOException {
|
||||
return Objects.requireNonNullElse(of(in.nextString()), ThemeColor.DEFAULT);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.theme;
|
||||
|
||||
import com.sun.jna.Pointer;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.binding.ObjectExpression;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.WindowEvent;
|
||||
import org.glavo.monetfx.Brightness;
|
||||
import org.glavo.monetfx.ColorScheme;
|
||||
import org.glavo.monetfx.Contrast;
|
||||
import org.glavo.monetfx.beans.property.ColorSchemeProperty;
|
||||
import org.glavo.monetfx.beans.property.ReadOnlyColorSchemeProperty;
|
||||
import org.glavo.monetfx.beans.property.SimpleColorSchemeProperty;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.ui.WindowsNativeUtils;
|
||||
import org.jackhuang.hmcl.util.platform.NativeUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OSVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
import org.jackhuang.hmcl.util.platform.windows.Dwmapi;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinConstants;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinReg;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinTypes;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// @author Glavo
|
||||
public final class Themes {
|
||||
|
||||
private static final ObjectExpression<Theme> theme = new ObjectBinding<>() {
|
||||
{
|
||||
List<Observable> observables = new ArrayList<>();
|
||||
|
||||
observables.add(config().themeBrightnessProperty());
|
||||
observables.add(config().themeColorProperty());
|
||||
if (FXUtils.DARK_MODE != null) {
|
||||
observables.add(FXUtils.DARK_MODE);
|
||||
}
|
||||
bind(observables.toArray(new Observable[0]));
|
||||
}
|
||||
|
||||
private Brightness getBrightness() {
|
||||
String themeBrightness = config().getThemeBrightness();
|
||||
if (themeBrightness == null)
|
||||
return Brightness.DEFAULT;
|
||||
|
||||
return switch (themeBrightness.toLowerCase(Locale.ROOT).trim()) {
|
||||
case "auto" -> {
|
||||
if (FXUtils.DARK_MODE != null) {
|
||||
yield FXUtils.DARK_MODE.get() ? Brightness.DARK : Brightness.LIGHT;
|
||||
} else {
|
||||
yield getDefaultBrightness();
|
||||
}
|
||||
}
|
||||
case "dark" -> Brightness.DARK;
|
||||
case "light" -> Brightness.LIGHT;
|
||||
default -> Brightness.DEFAULT;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Theme computeValue() {
|
||||
ThemeColor themeColor = Objects.requireNonNullElse(config().getThemeColor(), ThemeColor.DEFAULT);
|
||||
|
||||
return new Theme(themeColor, getBrightness(), Theme.DEFAULT.colorStyle(), Contrast.DEFAULT);
|
||||
}
|
||||
};
|
||||
private static final ColorSchemeProperty colorScheme = new SimpleColorSchemeProperty();
|
||||
private static final BooleanBinding darkMode = Bindings.createBooleanBinding(
|
||||
() -> colorScheme.get().getBrightness() == Brightness.DARK,
|
||||
colorScheme
|
||||
);
|
||||
|
||||
static {
|
||||
ChangeListener<Theme> listener = (observable, oldValue, newValue) -> {
|
||||
if (!Objects.equals(oldValue, newValue)) {
|
||||
colorScheme.set(newValue != null ? newValue.toColorScheme() : Theme.DEFAULT.toColorScheme());
|
||||
}
|
||||
};
|
||||
listener.changed(theme, null, theme.get());
|
||||
theme.addListener(listener);
|
||||
}
|
||||
|
||||
private static Brightness defaultBrightness;
|
||||
|
||||
private static Brightness getDefaultBrightness() {
|
||||
if (defaultBrightness != null)
|
||||
return defaultBrightness;
|
||||
|
||||
LOG.info("Detecting system theme brightness");
|
||||
Brightness brightness = Brightness.DEFAULT;
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
WinReg reg = WinReg.INSTANCE;
|
||||
if (reg != null) {
|
||||
Object appsUseLightTheme = reg.queryValue(WinReg.HKEY.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", "AppsUseLightTheme");
|
||||
if (appsUseLightTheme instanceof Integer value) {
|
||||
brightness = value == 0 ? Brightness.DARK : Brightness.LIGHT;
|
||||
}
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
try {
|
||||
String result = SystemUtils.run("/usr/bin/defaults", "read", "-g", "AppleInterfaceStyle").trim();
|
||||
brightness = "Dark".equalsIgnoreCase(result) ? Brightness.DARK : Brightness.LIGHT;
|
||||
} catch (Exception e) {
|
||||
// If the key does not exist, it means Light mode is used
|
||||
brightness = Brightness.LIGHT;
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
Path dbusSend = SystemUtils.which("dbus-send");
|
||||
if (dbusSend != null) {
|
||||
try {
|
||||
String[] result = SystemUtils.run(List.of(
|
||||
FileUtils.getAbsolutePath(dbusSend),
|
||||
"--session",
|
||||
"--print-reply=literal",
|
||||
"--reply-timeout=1000",
|
||||
"--dest=org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.Settings.Read",
|
||||
"string:org.freedesktop.appearance",
|
||||
"string:color-scheme"
|
||||
), Duration.ofSeconds(2)).trim().split(" ");
|
||||
|
||||
if (result.length > 0) {
|
||||
String value = result[result.length - 1];
|
||||
// 1: prefer dark
|
||||
// 2: prefer light
|
||||
if ("1".equals(value)) {
|
||||
brightness = Brightness.DARK;
|
||||
} else if ("2".equals(value)) {
|
||||
brightness = Brightness.LIGHT;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to get system theme from D-Bus", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG.info("Detected system theme brightness: " + brightness);
|
||||
|
||||
return defaultBrightness = brightness;
|
||||
}
|
||||
|
||||
public static ObjectExpression<Theme> themeProperty() {
|
||||
return theme;
|
||||
}
|
||||
|
||||
public static Theme getTheme() {
|
||||
return themeProperty().get();
|
||||
}
|
||||
|
||||
public static ReadOnlyColorSchemeProperty colorSchemeProperty() {
|
||||
return colorScheme;
|
||||
}
|
||||
|
||||
public static ColorScheme getColorScheme() {
|
||||
return colorScheme.get();
|
||||
}
|
||||
|
||||
private static final ObjectBinding<Color> titleFill = Bindings.createObjectBinding(
|
||||
() -> config().isTitleTransparent()
|
||||
? getColorScheme().getOnSurface()
|
||||
: getColorScheme().getOnPrimaryContainer(),
|
||||
colorSchemeProperty(),
|
||||
config().titleTransparentProperty()
|
||||
);
|
||||
|
||||
public static ObservableValue<Color> titleFillProperty() {
|
||||
return titleFill;
|
||||
}
|
||||
|
||||
public static BooleanBinding darkModeProperty() {
|
||||
return darkMode;
|
||||
}
|
||||
|
||||
public static void applyNativeDarkMode(Stage stage) {
|
||||
if (OperatingSystem.SYSTEM_VERSION.isAtLeast(OSVersion.WINDOWS_11) && NativeUtils.USE_JNA && Dwmapi.INSTANCE != null) {
|
||||
ChangeListener<Boolean> listener = FXUtils.onWeakChange(Themes.darkModeProperty(), darkMode -> {
|
||||
if (stage.isShowing()) {
|
||||
WindowsNativeUtils.getWindowHandle(stage).ifPresent(handle -> {
|
||||
if (handle == WinTypes.HANDLE.INVALID_VALUE)
|
||||
return;
|
||||
|
||||
Dwmapi.INSTANCE.DwmSetWindowAttribute(
|
||||
new WinTypes.HANDLE(Pointer.createConstant(handle)),
|
||||
WinConstants.DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||
new WinTypes.BOOLByReference(new WinTypes.BOOL(darkMode)),
|
||||
WinTypes.BOOL.SIZE
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
stage.getProperties().put("Themes.applyNativeDarkMode.listener", listener);
|
||||
|
||||
if (stage.isShowing()) {
|
||||
listener.changed(null, false, Themes.darkModeProperty().get());
|
||||
} else {
|
||||
stage.addEventFilter(WindowEvent.WINDOW_SHOWN, new EventHandler<>() {
|
||||
@Override
|
||||
public void handle(WindowEvent event) {
|
||||
stage.removeEventFilter(WindowEvent.WINDOW_SHOWN, this);
|
||||
listener.changed(null, false, Themes.darkModeProperty().get());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Themes() {
|
||||
}
|
||||
}
|
||||
@@ -19,9 +19,11 @@ package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
@@ -49,6 +51,8 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.account.AccountListPage;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorController;
|
||||
@@ -61,22 +65,29 @@ import org.jackhuang.hmcl.ui.versions.GameListPage;
|
||||
import org.jackhuang.hmcl.ui.versions.VersionPage;
|
||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.*;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class Controllers {
|
||||
public static final String JAVA_VERSION_TIP = "javaVersion";
|
||||
public static final String JAVA_INTERPRETED_MODE_TIP = "javaInterpretedMode";
|
||||
public static final String SOFTWARE_RENDERING = "softwareRendering";
|
||||
public static final String APRIL_FOOLS = "aprilFools";
|
||||
|
||||
public static final int MIN_WIDTH = 800 + 2 + 16; // bg width + border width*2 + shadow width*2
|
||||
public static final int MIN_HEIGHT = 450 + 2 + 40 + 16; // bg height + border width*2 + toolbar height + shadow width*2
|
||||
@@ -89,7 +100,7 @@ public final class Controllers {
|
||||
|
||||
private static Scene scene;
|
||||
private static Stage stage;
|
||||
private static Lazy<VersionPage> versionPage = new Lazy<>(VersionPage::new);
|
||||
private static VersionPage versionPage;
|
||||
private static Lazy<GameListPage> gameListPage = new Lazy<>(() -> {
|
||||
GameListPage gameListPage = new GameListPage();
|
||||
gameListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
|
||||
@@ -102,7 +113,7 @@ public final class Controllers {
|
||||
});
|
||||
private static Lazy<RootPage> rootPage = new Lazy<>(RootPage::new);
|
||||
private static DecoratorController decorator;
|
||||
private static Lazy<DownloadPage> downloadPage = new Lazy<>(DownloadPage::new);
|
||||
private static DownloadPage downloadPage;
|
||||
private static Lazy<AccountListPage> accountListPage = new Lazy<>(() -> {
|
||||
AccountListPage accountListPage = new AccountListPage();
|
||||
accountListPage.selectedAccountProperty().bindBidirectional(Accounts.selectedAccountProperty());
|
||||
@@ -110,7 +121,7 @@ public final class Controllers {
|
||||
accountListPage.authServersProperty().bindContentBidirectional(config().getAuthlibInjectorServers());
|
||||
return accountListPage;
|
||||
});
|
||||
private static Lazy<LauncherSettingsPage> settingsPage = new Lazy<>(LauncherSettingsPage::new);
|
||||
private static LauncherSettingsPage settingsPage;
|
||||
private static Lazy<TerracottaPage> terracottaPage = new Lazy<>(TerracottaPage::new);
|
||||
|
||||
private Controllers() {
|
||||
@@ -126,7 +137,18 @@ public final class Controllers {
|
||||
|
||||
// FXThread
|
||||
public static VersionPage getVersionPage() {
|
||||
return versionPage.get();
|
||||
if (versionPage == null) {
|
||||
versionPage = new VersionPage();
|
||||
}
|
||||
return versionPage;
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public static void prepareVersionPage() {
|
||||
if (versionPage == null) {
|
||||
LOG.info("Prepare the version page");
|
||||
versionPage = FXUtils.prepareNode(new VersionPage());
|
||||
}
|
||||
}
|
||||
|
||||
// FXThread
|
||||
@@ -141,7 +163,18 @@ public final class Controllers {
|
||||
|
||||
// FXThread
|
||||
public static LauncherSettingsPage getSettingsPage() {
|
||||
return settingsPage.get();
|
||||
if (settingsPage == null) {
|
||||
settingsPage = new LauncherSettingsPage();
|
||||
}
|
||||
return settingsPage;
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public static void prepareSettingsPage() {
|
||||
if (settingsPage == null) {
|
||||
LOG.info("Prepare the settings page");
|
||||
settingsPage = FXUtils.prepareNode(new LauncherSettingsPage());
|
||||
}
|
||||
}
|
||||
|
||||
// FXThread
|
||||
@@ -151,7 +184,18 @@ public final class Controllers {
|
||||
|
||||
// FXThread
|
||||
public static DownloadPage getDownloadPage() {
|
||||
return downloadPage.get();
|
||||
if (downloadPage == null) {
|
||||
downloadPage = new DownloadPage();
|
||||
}
|
||||
return downloadPage;
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public static void prepareDownloadPage() {
|
||||
if (downloadPage == null) {
|
||||
LOG.info("Prepare the download page");
|
||||
downloadPage = FXUtils.prepareNode(new DownloadPage());
|
||||
}
|
||||
}
|
||||
|
||||
// FXThread
|
||||
@@ -187,6 +231,8 @@ public final class Controllers {
|
||||
public static void initialize(Stage stage) {
|
||||
LOG.info("Start initializing application");
|
||||
|
||||
LOG.info("April Fools: " + AprilFools.isEnabled());
|
||||
|
||||
if (System.getProperty("prism.lcdtext") == null) {
|
||||
String fontAntiAliasing = globalConfig().getFontAntiAliasing();
|
||||
if ("lcd".equalsIgnoreCase(fontAntiAliasing)) {
|
||||
@@ -305,16 +351,16 @@ public final class Controllers {
|
||||
if (AnimationUtils.playWindowAnimation()) {
|
||||
Timeline timeline = new Timeline(
|
||||
new KeyFrame(Duration.millis(0),
|
||||
new KeyValue(decorator.getDecorator().opacityProperty(), 0, FXUtils.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleXProperty(), 0.8, FXUtils.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleYProperty(), 0.8, FXUtils.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleZProperty(), 0.8, FXUtils.EASE)
|
||||
new KeyValue(decorator.getDecorator().opacityProperty(), 0, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleXProperty(), 0.8, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleYProperty(), 0.8, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleZProperty(), 0.8, Motion.EASE)
|
||||
),
|
||||
new KeyFrame(Duration.millis(600),
|
||||
new KeyValue(decorator.getDecorator().opacityProperty(), 1, FXUtils.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleXProperty(), 1, FXUtils.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleYProperty(), 1, FXUtils.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleZProperty(), 1, FXUtils.EASE)
|
||||
new KeyValue(decorator.getDecorator().opacityProperty(), 1, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleXProperty(), 1, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleYProperty(), 1, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleZProperty(), 1, Motion.EASE)
|
||||
)
|
||||
);
|
||||
timeline.play();
|
||||
@@ -394,6 +440,53 @@ public final class Controllers {
|
||||
agreementPane.setActions(agreementLink, yesButton, noButton);
|
||||
Controllers.dialog(agreementPane);
|
||||
}
|
||||
|
||||
aprilFools:
|
||||
if (AprilFools.isEnabled()) {
|
||||
int currentYear = LocalDate.now().getYear();
|
||||
if (config().getShownTips().get(APRIL_FOOLS) instanceof Number year && year.intValue() >= currentYear)
|
||||
break aprilFools;
|
||||
|
||||
if (!I18n.getLocale().getLocale().getLanguage().equals("zh"))
|
||||
break aprilFools;
|
||||
|
||||
SupportedLocale lzh = SupportedLocale.getSupportedLocales().stream()
|
||||
.filter(locale -> "lzh".equals(locale.getName()))
|
||||
.findFirst().orElse(null);
|
||||
|
||||
if (lzh == null) {
|
||||
LOG.warning("No supported locale found for lzh");
|
||||
break aprilFools;
|
||||
}
|
||||
|
||||
Runnable updateShowTips = () -> config().getShownTips().put(APRIL_FOOLS, currentYear);
|
||||
|
||||
Controllers.confirmWithCountdown(i18n("launcher.april_fools.switch_lzh"), null, 10,
|
||||
MessageType.QUESTION, () -> {
|
||||
Controllers.confirm(i18n("launcher.april_fools.switch_lzh.confirm"), null, MessageType.QUESTION, () -> {
|
||||
LOG.info("Switching locale to " + lzh);
|
||||
|
||||
updateShowTips.run();
|
||||
config().setLocalization(lzh);
|
||||
|
||||
Controllers.onApplicationStop();
|
||||
|
||||
try {
|
||||
FileSaver.waitForAllSaves();
|
||||
} catch (InterruptedException ignored) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
try {
|
||||
Restarter.restartSelf();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to restart self", e);
|
||||
}
|
||||
|
||||
Platform.exit();
|
||||
}, updateShowTips);
|
||||
}, updateShowTips);
|
||||
}
|
||||
}
|
||||
|
||||
public static void dialog(Region content) {
|
||||
@@ -433,26 +526,34 @@ public final class Controllers {
|
||||
dialog(new MessageDialogPane.Builder(text, title, type).actionOrCancel(actionButton, cancel).build());
|
||||
}
|
||||
|
||||
public static void confirmActionDanger(String text, String title, Runnable resolve, Runnable cancel) {
|
||||
JFXButton btnYes = new JFXButton(i18n("button.ok"));
|
||||
btnYes.getStyleClass().add("dialog-error");
|
||||
btnYes.setOnAction(e -> resolve.run());
|
||||
btnYes.setDisable(true);
|
||||
public static void confirmWithCountdown(String text, String title, int seconds, MessageType messageType,
|
||||
@Nullable Runnable ok, @Nullable Runnable cancel) {
|
||||
if (seconds <= 0)
|
||||
throw new IllegalArgumentException("Seconds must be greater than 0");
|
||||
|
||||
int countdown = 10;
|
||||
KeyFrame[] keyFrames = new KeyFrame[countdown + 1];
|
||||
for (int i = 0; i < countdown; i++) {
|
||||
JFXButton btnOk = new JFXButton(i18n("button.ok"));
|
||||
btnOk.getStyleClass().add(messageType == MessageType.WARNING || messageType == MessageType.ERROR
|
||||
? "dialog-error"
|
||||
: "dialog-accept");
|
||||
|
||||
if (ok != null)
|
||||
btnOk.setOnAction(e -> ok.run());
|
||||
btnOk.setDisable(true);
|
||||
|
||||
KeyFrame[] keyFrames = new KeyFrame[seconds + 1];
|
||||
for (int i = 0; i < seconds; i++) {
|
||||
keyFrames[i] = new KeyFrame(Duration.seconds(i),
|
||||
new KeyValue(btnYes.textProperty(), i18n("button.ok.countdown", countdown - i)));
|
||||
new KeyValue(btnOk.textProperty(), i18n("button.ok.countdown", seconds - i)));
|
||||
}
|
||||
keyFrames[countdown] = new KeyFrame(Duration.seconds(countdown),
|
||||
new KeyValue(btnYes.textProperty(), i18n("button.ok")),
|
||||
new KeyValue(btnYes.disableProperty(), false));
|
||||
keyFrames[seconds] = new KeyFrame(Duration.seconds(seconds),
|
||||
new KeyValue(btnOk.textProperty(), i18n("button.ok")),
|
||||
new KeyValue(btnOk.disableProperty(), false));
|
||||
|
||||
Timeline timeline = new Timeline(keyFrames);
|
||||
confirmAction(text, title, MessageType.WARNING, btnYes, () -> {
|
||||
confirmAction(text, title, messageType, btnOk, () -> {
|
||||
timeline.stop();
|
||||
cancel.run();
|
||||
if (cancel != null)
|
||||
cancel.run();
|
||||
});
|
||||
timeline.play();
|
||||
}
|
||||
@@ -461,8 +562,8 @@ public final class Controllers {
|
||||
return prompt(title, onResult, "");
|
||||
}
|
||||
|
||||
public static CompletableFuture<String> prompt(String title, FutureCallback<String> onResult, String initialValue) {
|
||||
InputDialogPane pane = new InputDialogPane(title, initialValue, onResult);
|
||||
public static CompletableFuture<String> prompt(String title, FutureCallback<String> onResult, String initialValue, ValidatorBase... validators) {
|
||||
InputDialogPane pane = new InputDialogPane(title, initialValue, onResult, validators);
|
||||
dialog(pane);
|
||||
return pane.getCompletableFuture();
|
||||
}
|
||||
@@ -489,7 +590,11 @@ public final class Controllers {
|
||||
}
|
||||
|
||||
public static void navigate(Node node) {
|
||||
decorator.navigate(node);
|
||||
decorator.navigate(node, ContainerAnimations.NAVIGATION, Motion.SHORT4, Motion.EASE);
|
||||
}
|
||||
|
||||
public static void navigateForward(Node node) {
|
||||
decorator.navigate(node, ContainerAnimations.FORWARD, Motion.SHORT4, Motion.EASE);
|
||||
}
|
||||
|
||||
public static void showToast(String content) {
|
||||
@@ -524,6 +629,7 @@ public final class Controllers {
|
||||
downloadPage = null;
|
||||
accountListPage = null;
|
||||
settingsPage = null;
|
||||
terracottaPage = null;
|
||||
decorator = null;
|
||||
stage = null;
|
||||
scene = null;
|
||||
|
||||
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.ui;
|
||||
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.ui.account.ClassicAccountLoginDialog;
|
||||
import org.jackhuang.hmcl.ui.account.OAuthAccountLoginDialog;
|
||||
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginPane;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CancellationException;
|
||||
@@ -49,10 +49,10 @@ public final class DialogController {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
|
||||
runInFX(() -> {
|
||||
OAuthAccountLoginDialog pane = new OAuthAccountLoginDialog((OAuthAccount) account, it -> {
|
||||
MicrosoftAccountLoginPane pane = new MicrosoftAccountLoginPane(account, it -> {
|
||||
res.set(it);
|
||||
latch.countDown();
|
||||
}, latch::countDown);
|
||||
}, latch::countDown, false);
|
||||
Controllers.dialog(pane);
|
||||
});
|
||||
latch.await();
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXDialog;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogAware;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXDialogPane;
|
||||
import org.jackhuang.hmcl.ui.decorator.Decorator;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public final class DialogUtils {
|
||||
private DialogUtils() {
|
||||
}
|
||||
|
||||
public static final String PROPERTY_DIALOG_INSTANCE = DialogUtils.class.getName() + ".dialog.instance";
|
||||
public static final String PROPERTY_DIALOG_PANE_INSTANCE = DialogUtils.class.getName() + ".dialog.pane.instance";
|
||||
public static final String PROPERTY_DIALOG_CLOSE_HANDLER = DialogUtils.class.getName() + ".dialog.closeListener";
|
||||
|
||||
public static final String PROPERTY_PARENT_PANE_REF = DialogUtils.class.getName() + ".dialog.parentPaneRef";
|
||||
public static final String PROPERTY_PARENT_DIALOG_REF = DialogUtils.class.getName() + ".dialog.parentDialogRef";
|
||||
|
||||
public static void show(Decorator decorator, Node content) {
|
||||
if (decorator.getDrawerWrapper() == null) {
|
||||
Platform.runLater(() -> show(decorator, content));
|
||||
return;
|
||||
}
|
||||
|
||||
show(decorator.getDrawerWrapper(), content, (dialog) -> {
|
||||
JFXDialogPane pane = (JFXDialogPane) dialog.getContent();
|
||||
decorator.capableDraggingWindow(dialog);
|
||||
decorator.forbidDraggingWindow(pane);
|
||||
dialog.setDialogContainer(decorator.getDrawerWrapper());
|
||||
});
|
||||
}
|
||||
|
||||
public static void show(StackPane container, Node content) {
|
||||
show(container, content, null);
|
||||
}
|
||||
|
||||
public static void show(StackPane container, Node content, @Nullable Consumer<JFXDialog> onDialogCreated) {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
JFXDialog dialog = (JFXDialog) container.getProperties().get(PROPERTY_DIALOG_INSTANCE);
|
||||
JFXDialogPane dialogPane = (JFXDialogPane) container.getProperties().get(PROPERTY_DIALOG_PANE_INSTANCE);
|
||||
|
||||
if (dialog == null) {
|
||||
dialog = new JFXDialog(AnimationUtils.isAnimationEnabled()
|
||||
? JFXDialog.DialogTransition.CENTER
|
||||
: JFXDialog.DialogTransition.NONE);
|
||||
dialogPane = new JFXDialogPane();
|
||||
|
||||
dialog.setContent(dialogPane);
|
||||
dialog.setDialogContainer(container);
|
||||
dialog.setOverlayClose(false);
|
||||
|
||||
container.getProperties().put(PROPERTY_DIALOG_INSTANCE, dialog);
|
||||
container.getProperties().put(PROPERTY_DIALOG_PANE_INSTANCE, dialogPane);
|
||||
|
||||
if (onDialogCreated != null) {
|
||||
onDialogCreated.accept(dialog);
|
||||
}
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
content.getProperties().put(PROPERTY_PARENT_PANE_REF, dialogPane);
|
||||
content.getProperties().put(PROPERTY_PARENT_DIALOG_REF, dialog);
|
||||
|
||||
dialogPane.push(content);
|
||||
|
||||
EventHandler<DialogCloseEvent> handler = event -> close(content);
|
||||
content.getProperties().put(PROPERTY_DIALOG_CLOSE_HANDLER, handler);
|
||||
content.addEventHandler(DialogCloseEvent.CLOSE, handler);
|
||||
|
||||
handleDialogShown(dialog, content);
|
||||
}
|
||||
|
||||
private static void handleDialogShown(JFXDialog dialog, Node node) {
|
||||
if (dialog.isVisible()) {
|
||||
dialog.requestFocus();
|
||||
if (node instanceof DialogAware dialogAware)
|
||||
dialogAware.onDialogShown();
|
||||
} else {
|
||||
dialog.visibleProperty().addListener(new ChangeListener<>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
|
||||
if (newValue) {
|
||||
dialog.requestFocus();
|
||||
if (node instanceof DialogAware dialogAware)
|
||||
dialogAware.onDialogShown();
|
||||
observable.removeListener(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static void close(Node content) {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
Optional.ofNullable(content.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER))
|
||||
.ifPresent(handler -> content.removeEventHandler(DialogCloseEvent.CLOSE, (EventHandler<DialogCloseEvent>) handler));
|
||||
|
||||
JFXDialogPane pane = (JFXDialogPane) content.getProperties().get(PROPERTY_PARENT_PANE_REF);
|
||||
JFXDialog dialog = (JFXDialog) content.getProperties().get(PROPERTY_PARENT_DIALOG_REF);
|
||||
|
||||
if (dialog != null && pane != null) {
|
||||
if (pane.size() == 1 && pane.peek().orElse(null) == content) {
|
||||
dialog.setOnDialogClosed(e -> pane.pop(content));
|
||||
dialog.close();
|
||||
|
||||
StackPane container = dialog.getDialogContainer();
|
||||
if (container != null) {
|
||||
container.getProperties().remove(PROPERTY_DIALOG_INSTANCE);
|
||||
container.getProperties().remove(PROPERTY_DIALOG_PANE_INSTANCE);
|
||||
container.getProperties().remove(PROPERTY_PARENT_DIALOG_REF);
|
||||
container.getProperties().remove(PROPERTY_PARENT_PANE_REF);
|
||||
}
|
||||
} else {
|
||||
pane.pop(content);
|
||||
}
|
||||
|
||||
if (content instanceof DialogAware dialogAware) {
|
||||
dialogAware.onDialogClosed();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,9 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.animation.*;
|
||||
import javafx.animation.Animation;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
@@ -26,42 +28,53 @@ import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.WeakListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.*;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.collections.ObservableMap;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventDispatcher;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.event.EventType;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.skin.VirtualFlow;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import javafx.stage.*;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
import org.jackhuang.hmcl.task.CacheFileTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedMenuItem;
|
||||
import org.jackhuang.hmcl.ui.construct.MenuSeparator;
|
||||
import org.jackhuang.hmcl.ui.construct.PopupMenu;
|
||||
import org.jackhuang.hmcl.ui.image.ImageLoader;
|
||||
import org.jackhuang.hmcl.ui.image.ImageUtils;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.ResourceNotFoundError;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
|
||||
@@ -86,12 +99,14 @@ import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.PathMatcher;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.*;
|
||||
@@ -100,8 +115,8 @@ import java.util.regex.Pattern;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class FXUtils {
|
||||
private FXUtils() {
|
||||
@@ -132,8 +147,10 @@ public final class FXUtils {
|
||||
public static final @Nullable ObservableMap<String, Object> PREFERENCES;
|
||||
public static final @Nullable ObservableBooleanValue DARK_MODE;
|
||||
public static final @Nullable Boolean REDUCED_MOTION;
|
||||
public static final @Nullable ReadOnlyObjectProperty<Color> ACCENT_COLOR;
|
||||
|
||||
public static final @Nullable MethodHandle TEXT_TRUNCATED_PROPERTY;
|
||||
public static final @Nullable MethodHandle FOCUS_VISIBLE_PROPERTY;
|
||||
|
||||
static {
|
||||
String jfxVersion = System.getProperty("javafx.version");
|
||||
@@ -148,6 +165,7 @@ public final class FXUtils {
|
||||
|
||||
ObservableMap<String, Object> preferences = null;
|
||||
ObservableBooleanValue darkMode = null;
|
||||
ReadOnlyObjectProperty<Color> accentColorProperty = null;
|
||||
Boolean reducedMotion = null;
|
||||
if (JAVAFX_MAJOR_VERSION >= 22) {
|
||||
try {
|
||||
@@ -159,14 +177,19 @@ public final class FXUtils {
|
||||
preferences = preferences0;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
var colorSchemeProperty =
|
||||
(ReadOnlyObjectProperty<? extends Enum<?>>)
|
||||
lookup.findVirtual(preferencesClass, "colorSchemeProperty", MethodType.methodType(ReadOnlyObjectProperty.class))
|
||||
.invoke(preferences);
|
||||
var colorSchemeProperty = (ReadOnlyObjectProperty<? extends Enum<?>>)
|
||||
lookup.findVirtual(preferencesClass, "colorSchemeProperty", MethodType.methodType(ReadOnlyObjectProperty.class))
|
||||
.invoke(preferences);
|
||||
|
||||
darkMode = Bindings.createBooleanBinding(() ->
|
||||
"DARK".equals(colorSchemeProperty.get().name()), colorSchemeProperty);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
var accentColorProperty0 = (ReadOnlyObjectProperty<Color>)
|
||||
lookup.findVirtual(preferencesClass, "accentColorProperty", MethodType.methodType(ReadOnlyObjectProperty.class))
|
||||
.invoke(preferences);
|
||||
accentColorProperty = accentColorProperty0;
|
||||
|
||||
if (JAVAFX_MAJOR_VERSION >= 24) {
|
||||
reducedMotion = (boolean)
|
||||
lookup.findVirtual(preferencesClass, "isReducedMotion", MethodType.methodType(boolean.class))
|
||||
@@ -179,6 +202,7 @@ public final class FXUtils {
|
||||
PREFERENCES = preferences;
|
||||
DARK_MODE = darkMode;
|
||||
REDUCED_MOTION = reducedMotion;
|
||||
ACCENT_COLOR = accentColorProperty;
|
||||
|
||||
MethodHandle textTruncatedProperty = null;
|
||||
if (JAVAFX_MAJOR_VERSION >= 23) {
|
||||
@@ -193,6 +217,20 @@ public final class FXUtils {
|
||||
}
|
||||
}
|
||||
TEXT_TRUNCATED_PROPERTY = textTruncatedProperty;
|
||||
|
||||
MethodHandle focusVisibleProperty = null;
|
||||
if (JAVAFX_MAJOR_VERSION >= 19) {
|
||||
try {
|
||||
focusVisibleProperty = MethodHandles.publicLookup().findVirtual(
|
||||
Node.class,
|
||||
"focusVisibleProperty",
|
||||
MethodType.methodType(ReadOnlyBooleanProperty.class)
|
||||
);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to lookup focusVisibleProperty", e);
|
||||
}
|
||||
}
|
||||
FOCUS_VISIBLE_PROPERTY = focusVisibleProperty;
|
||||
}
|
||||
|
||||
public static final String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace";
|
||||
@@ -379,12 +417,18 @@ public final class FXUtils {
|
||||
return region.getMaxHeight();
|
||||
}
|
||||
|
||||
public static Node limitingSize(Node node, double width, double height) {
|
||||
StackPane pane = new StackPane(node);
|
||||
pane.setAlignment(Pos.CENTER);
|
||||
FXUtils.setLimitWidth(pane, width);
|
||||
FXUtils.setLimitHeight(pane, height);
|
||||
return pane;
|
||||
public static void limitCellWidth(ListView<?> listView, ListCell<?> cell) {
|
||||
ReadOnlyDoubleProperty widthProperty;
|
||||
|
||||
if (listView.lookup(".clipped-container") instanceof Region clippedContainer) {
|
||||
widthProperty = clippedContainer.widthProperty();
|
||||
} else {
|
||||
widthProperty = listView.widthProperty();
|
||||
}
|
||||
|
||||
cell.maxWidthProperty().bind(widthProperty);
|
||||
cell.prefWidthProperty().bind(widthProperty);
|
||||
cell.minWidthProperty().bind(widthProperty);
|
||||
}
|
||||
|
||||
public static void smoothScrolling(ScrollPane scrollPane) {
|
||||
@@ -392,6 +436,11 @@ public final class FXUtils {
|
||||
ScrollUtils.addSmoothScrolling(scrollPane);
|
||||
}
|
||||
|
||||
public static void smoothScrolling(VirtualFlow<?> virtualFlow) {
|
||||
if (AnimationUtils.isAnimationEnabled())
|
||||
ScrollUtils.addSmoothScrolling(virtualFlow);
|
||||
}
|
||||
|
||||
/// If the current environment is JavaFX 23 or higher, this method returns [Labeled#textTruncatedProperty()];
|
||||
/// Otherwise, it returns `null`.
|
||||
public static @Nullable ReadOnlyBooleanProperty textTruncatedProperty(Labeled labeled) {
|
||||
@@ -408,6 +457,20 @@ public final class FXUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable ReadOnlyBooleanProperty focusVisibleProperty(Node node) {
|
||||
if (FOCUS_VISIBLE_PROPERTY != null) {
|
||||
try {
|
||||
return (ReadOnlyBooleanProperty) FOCUS_VISIBLE_PROPERTY.invokeExact(node);
|
||||
} catch (RuntimeException | Error e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final Duration TOOLTIP_FAST_SHOW_DELAY = Duration.millis(50);
|
||||
private static final Duration TOOLTIP_SLOW_SHOW_DELAY = Duration.millis(500);
|
||||
private static final Duration TOOLTIP_SHOW_DURATION = Duration.millis(5000);
|
||||
@@ -435,28 +498,12 @@ public final class FXUtils {
|
||||
installSlowTooltip(node, new Tooltip(tooltip));
|
||||
}
|
||||
|
||||
public static void playAnimation(Node node, String animationKey, Timeline timeline) {
|
||||
animationKey = "FXUTILS.ANIMATION." + animationKey;
|
||||
Object oldTimeline = node.getProperties().get(animationKey);
|
||||
// if (oldTimeline instanceof Timeline) ((Timeline) oldTimeline).stop();
|
||||
if (timeline != null) timeline.play();
|
||||
node.getProperties().put(animationKey, timeline);
|
||||
}
|
||||
|
||||
public static <T> Animation playAnimation(Node node, String animationKey, Duration duration, WritableValue<T> property, T from, T to, Interpolator interpolator) {
|
||||
if (from == null) from = property.getValue();
|
||||
if (duration == null || Objects.equals(duration, Duration.ZERO) || Objects.equals(from, to)) {
|
||||
playAnimation(node, animationKey, null);
|
||||
property.setValue(to);
|
||||
return null;
|
||||
} else {
|
||||
Timeline timeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO, new KeyValue(property, from, interpolator)),
|
||||
new KeyFrame(duration, new KeyValue(property, to, interpolator))
|
||||
);
|
||||
playAnimation(node, animationKey, timeline);
|
||||
return timeline;
|
||||
}
|
||||
public static void playAnimation(Node node, String animationKey, Animation animation) {
|
||||
animationKey = "hmcl.animations." + animationKey;
|
||||
if (node.getProperties().get(animationKey) instanceof Animation oldAnimation)
|
||||
oldAnimation.stop();
|
||||
animation.play();
|
||||
node.getProperties().put(animationKey, animation);
|
||||
}
|
||||
|
||||
public static void openFolder(Path file) {
|
||||
@@ -1189,7 +1236,16 @@ public final class FXUtils {
|
||||
|
||||
public static Task<Image> getRemoteImageTask(String url, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
|
||||
return new CacheFileTask(url)
|
||||
.thenApplyAsync(file -> loadImage(file, requestedWidth, requestedHeight, preserveRatio, smooth));
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenApplyAsync(file -> loadImage(file, requestedWidth, requestedHeight, preserveRatio, smooth))
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
}
|
||||
|
||||
public static Task<Image> getRemoteImageTask(List<URI> uris, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
|
||||
return new CacheFileTask(uris)
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenApplyAsync(file -> loadImage(file, requestedWidth, requestedHeight, preserveRatio, smooth))
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
}
|
||||
|
||||
public static ObservableValue<Image> newRemoteImage(String url, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
|
||||
@@ -1202,6 +1258,7 @@ public final class FXUtils {
|
||||
LOG.warning("An exception encountered while loading remote image: " + url, exception);
|
||||
}
|
||||
})
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.start();
|
||||
return image;
|
||||
}
|
||||
@@ -1222,12 +1279,36 @@ public final class FXUtils {
|
||||
public static JFXButton newToggleButton4(SVG icon) {
|
||||
JFXButton button = new JFXButton();
|
||||
button.getStyleClass().add("toggle-icon4");
|
||||
button.setGraphic(icon.createIcon(Theme.blackFill(), -1));
|
||||
button.setGraphic(icon.createIcon());
|
||||
return button;
|
||||
}
|
||||
|
||||
public static Label newSafeTruncatedLabel(String text) {
|
||||
Label label = new Label(text);
|
||||
public static JFXButton newToggleButton4(SVG icon, int size) {
|
||||
JFXButton button = new JFXButton();
|
||||
button.getStyleClass().add("toggle-icon4");
|
||||
button.setGraphic(icon.createIcon(size));
|
||||
return button;
|
||||
}
|
||||
|
||||
public static void setOnActionWithCooldown(ButtonBase button, Runnable action) {
|
||||
setOnActionWithCooldown(button, action, Motion.SHORT4);
|
||||
}
|
||||
|
||||
public static void setOnActionWithCooldown(ButtonBase button, Runnable action, Duration cooldown) {
|
||||
button.setOnAction(e -> {
|
||||
button.setDisable(true);
|
||||
|
||||
var pause = new PauseTransition(cooldown);
|
||||
pause.setOnFinished(event -> button.setDisable(false));
|
||||
pause.play();
|
||||
|
||||
action.run();
|
||||
e.consume();
|
||||
});
|
||||
}
|
||||
|
||||
public static Label newSafeTruncatedLabel() {
|
||||
Label label = new Label();
|
||||
label.setTextOverrun(OverrunStyle.CENTER_WORD_ELLIPSIS);
|
||||
showTooltipWhenTruncated(label);
|
||||
return label;
|
||||
@@ -1303,17 +1384,11 @@ public final class FXUtils {
|
||||
}
|
||||
|
||||
public static <T> Callback<ListView<T>, ListCell<T>> jfxListCellFactory(Function<T, Node> graphicBuilder) {
|
||||
Holder<Object> lastCell = new Holder<>();
|
||||
return view -> new JFXListCell<T>() {
|
||||
@Override
|
||||
public void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (this == lastCell.value && !isVisible())
|
||||
return;
|
||||
lastCell.value = this;
|
||||
|
||||
if (!empty) {
|
||||
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
|
||||
setGraphic(graphicBuilder.apply(item));
|
||||
@@ -1347,8 +1422,6 @@ public final class FXUtils {
|
||||
}
|
||||
};
|
||||
|
||||
public static final Interpolator EASE = Interpolator.SPLINE(0.25, 0.1, 0.25, 1);
|
||||
|
||||
public static void onEscPressed(Node node, Runnable action) {
|
||||
node.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
|
||||
if (e.getCode() == KeyCode.ESCAPE) {
|
||||
@@ -1360,13 +1433,40 @@ public final class FXUtils {
|
||||
|
||||
public static void onClicked(Node node, Runnable action) {
|
||||
node.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
|
||||
if (e.getButton() == MouseButton.PRIMARY && e.getClickCount() == 1) {
|
||||
if (e.getButton() == MouseButton.PRIMARY) {
|
||||
action.run();
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void onSecondaryButtonClicked(Node node, Runnable action) {
|
||||
node.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
|
||||
if (e.getButton() == MouseButton.SECONDARY) {
|
||||
action.run();
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static <N extends Parent> N prepareNode(N node) {
|
||||
Scene dummyScene = new Scene(node);
|
||||
StyleSheets.init(dummyScene);
|
||||
node.applyCss();
|
||||
node.layout();
|
||||
return node;
|
||||
}
|
||||
|
||||
public static void prepareOnMouseEnter(Node node, Runnable action) {
|
||||
node.addEventFilter(MouseEvent.MOUSE_ENTERED, new EventHandler<>() {
|
||||
@Override
|
||||
public void handle(MouseEvent e) {
|
||||
node.removeEventFilter(MouseEvent.MOUSE_ENTERED, this);
|
||||
action.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static <T> void onScroll(Node node, List<T> list,
|
||||
ToIntFunction<List<T>> finder,
|
||||
Consumer<T> updater
|
||||
@@ -1388,16 +1488,6 @@ public final class FXUtils {
|
||||
});
|
||||
}
|
||||
|
||||
public static void clearFocus(Node node) {
|
||||
Scene scene = node.getScene();
|
||||
if (scene != null) {
|
||||
Parent root = scene.getRoot();
|
||||
if (root != null) {
|
||||
root.requestFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void copyOnDoubleClick(Labeled label) {
|
||||
label.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
|
||||
if (e.getButton() == MouseButton.PRIMARY && e.getClickCount() == 2) {
|
||||
@@ -1411,12 +1501,16 @@ public final class FXUtils {
|
||||
}
|
||||
|
||||
public static void copyText(String text) {
|
||||
copyText(text, i18n("message.copied"));
|
||||
}
|
||||
|
||||
public static void copyText(String text, @Nullable String toastMessage) {
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(text);
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
|
||||
if (!Controllers.isStopped()) {
|
||||
Controllers.showToast(i18n("message.copied"));
|
||||
if (toastMessage != null && !Controllers.isStopped()) {
|
||||
Controllers.showToast(toastMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1435,11 +1529,11 @@ public final class FXUtils {
|
||||
for (int i = 0; i < children.getLength(); i++) {
|
||||
org.w3c.dom.Node node = children.item(i);
|
||||
|
||||
if (node instanceof Element) {
|
||||
Element element = (Element) node;
|
||||
if (node instanceof Element element) {
|
||||
if ("a".equals(element.getTagName())) {
|
||||
String href = element.getAttribute("href");
|
||||
Text text = new Text(element.getTextContent());
|
||||
text.getStyleClass().add("hyperlink");
|
||||
onClicked(text, () -> {
|
||||
String link = href;
|
||||
try {
|
||||
@@ -1449,7 +1543,6 @@ public final class FXUtils {
|
||||
hyperlinkAction.accept(link);
|
||||
});
|
||||
text.setCursor(Cursor.HAND);
|
||||
text.setFill(Color.web("#0070E0"));
|
||||
text.setUnderline(true);
|
||||
texts.add(text);
|
||||
} else if ("b".equals(element.getTagName())) {
|
||||
@@ -1551,4 +1644,39 @@ public final class FXUtils {
|
||||
? JFXPopup.PopupVPosition.BOTTOM // Show menu below the button, expanding downward
|
||||
: JFXPopup.PopupVPosition.TOP; // Show menu above the button, expanding upward
|
||||
}
|
||||
|
||||
public static void useJFXContextMenu(TextInputControl control) {
|
||||
control.setContextMenu(null);
|
||||
|
||||
PopupMenu menu = new PopupMenu();
|
||||
JFXPopup popup = new JFXPopup(menu);
|
||||
popup.setAutoHide(true);
|
||||
|
||||
control.setOnContextMenuRequested(e -> {
|
||||
boolean hasNoSelection = control.getSelectedText().isEmpty();
|
||||
|
||||
IconedMenuItem undo = new IconedMenuItem(SVG.UNDO, i18n("menu.undo"), control::undo, popup);
|
||||
IconedMenuItem redo = new IconedMenuItem(SVG.REDO, i18n("menu.redo"), control::redo, popup);
|
||||
IconedMenuItem cut = new IconedMenuItem(SVG.CONTENT_CUT, i18n("menu.cut"), control::cut, popup);
|
||||
IconedMenuItem copy = new IconedMenuItem(SVG.CONTENT_COPY, i18n("menu.copy"), control::copy, popup);
|
||||
IconedMenuItem paste = new IconedMenuItem(SVG.CONTENT_PASTE, i18n("menu.paste"), control::paste, popup);
|
||||
IconedMenuItem delete = new IconedMenuItem(SVG.DELETE, i18n("menu.deleteselection"), () -> control.replaceSelection(""), popup);
|
||||
IconedMenuItem selectall = new IconedMenuItem(SVG.SELECT_ALL, i18n("menu.selectall"), control::selectAll, popup);
|
||||
|
||||
menu.getContent().setAll(undo, redo, new MenuSeparator(), cut, copy, paste, delete, new MenuSeparator(), selectall);
|
||||
|
||||
undo.setDisable(!control.isUndoable());
|
||||
redo.setDisable(!control.isRedoable());
|
||||
cut.setDisable(hasNoSelection);
|
||||
delete.setDisable(hasNoSelection);
|
||||
copy.setDisable(hasNoSelection);
|
||||
paste.setDisable(!Clipboard.getSystemClipboard().hasString());
|
||||
selectall.setDisable(control.getText() == null || control.getText().isEmpty());
|
||||
|
||||
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(control, popup);
|
||||
popup.show(control, vPosition, JFXPopup.PopupHPosition.LEFT, e.getX(), vPosition == JFXPopup.PopupVPosition.TOP ? e.getY() : e.getY() - control.getHeight());
|
||||
|
||||
e.consume();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -24,11 +24,11 @@ import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
@@ -40,6 +40,8 @@ import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
@@ -50,9 +52,12 @@ import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
@@ -81,10 +86,13 @@ public class GameCrashWindow extends Stage {
|
||||
private final ProcessListener.ExitType exitType;
|
||||
private final LaunchOptions launchOptions;
|
||||
private final View view;
|
||||
private final StackPane stackPane;
|
||||
|
||||
private final List<Log> logs;
|
||||
|
||||
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, List<Log> logs) {
|
||||
Themes.applyNativeDarkMode(this);
|
||||
|
||||
this.managedProcess = managedProcess;
|
||||
this.exitType = exitType;
|
||||
this.repository = repository;
|
||||
@@ -93,7 +101,7 @@ public class GameCrashWindow extends Stage {
|
||||
this.logs = logs;
|
||||
this.analyzer = LibraryAnalyzer.analyze(version, repository.getGameVersion(version).orElse(null));
|
||||
|
||||
memory = Optional.ofNullable(launchOptions.getMaxMemory()).map(i -> i + " MB").orElse("-");
|
||||
memory = Optional.ofNullable(launchOptions.getMaxMemory()).map(i -> i + " " + i18n("settings.memory.unit.mib")).orElse("-");
|
||||
|
||||
total_memory = MEGABYTES.formatBytes(SystemInfo.getTotalMemorySize());
|
||||
|
||||
@@ -103,9 +111,10 @@ public class GameCrashWindow extends Stage {
|
||||
|
||||
this.view = new View();
|
||||
|
||||
this.stackPane = new StackPane(view);
|
||||
this.feedbackTextFlow.getChildren().addAll(FXUtils.parseSegment(i18n("game.crash.feedback"), Controllers::onHyperlinkAction));
|
||||
|
||||
setScene(new Scene(view, 800, 480));
|
||||
setScene(new Scene(stackPane, 800, 480));
|
||||
StyleSheets.init(getScene());
|
||||
setTitle(i18n("game.crash.title"));
|
||||
FXUtils.setIcon(this);
|
||||
@@ -269,22 +278,41 @@ public class GameCrashWindow extends Stage {
|
||||
|
||||
CompletableFuture.supplyAsync(() ->
|
||||
logs.stream().map(Log::getLog).collect(Collectors.joining("\n")))
|
||||
.thenComposeAsync(logs ->
|
||||
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
|
||||
.handleAsync((result, exception) -> {
|
||||
Alert alert;
|
||||
.thenComposeAsync(logs -> {
|
||||
long processStartTime = managedProcess.getProcess().info()
|
||||
.startInstant()
|
||||
.map(Instant::toEpochMilli).orElseGet(() -> {
|
||||
try {
|
||||
return ManagementFactory.getRuntimeMXBean().getStartTime();
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to get process start time", e);
|
||||
return 0L;
|
||||
}
|
||||
});
|
||||
|
||||
return LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs,
|
||||
new CommandBuilder().addAll(managedProcess.getCommands()).toString(),
|
||||
path -> {
|
||||
try {
|
||||
FileTime lastModifiedTime = Files.getLastModifiedTime(path);
|
||||
return lastModifiedTime.toMillis() >= processStartTime;
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to read file attributes", e);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
})
|
||||
.handleAsync((result, exception) -> {
|
||||
if (exception == null) {
|
||||
FXUtils.showFileInExplorer(logFile);
|
||||
alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile));
|
||||
var dialog = new MessageDialogPane.Builder(i18n("settings.launcher.launcher_log.export.success", logFile), i18n("message.success"), MessageDialogPane.MessageType.SUCCESS).ok(null).build();
|
||||
DialogUtils.show(stackPane, dialog);
|
||||
} else {
|
||||
LOG.warning("Failed to export game crash info", exception);
|
||||
alert = new Alert(Alert.AlertType.WARNING, i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception));
|
||||
var dialog = new MessageDialogPane.Builder(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception), i18n("message.error"), MessageDialogPane.MessageType.ERROR).ok(null).build();
|
||||
DialogUtils.show(stackPane, dialog);
|
||||
}
|
||||
|
||||
alert.setTitle(i18n("settings.launcher.launcher_log.export"));
|
||||
alert.showAndWait();
|
||||
|
||||
return null;
|
||||
}, Schedulers.javafx());
|
||||
}
|
||||
@@ -292,7 +320,7 @@ public class GameCrashWindow extends Stage {
|
||||
private final class View extends VBox {
|
||||
|
||||
View() {
|
||||
setStyle("-fx-background-color: white");
|
||||
this.getStyleClass().add("game-crash-window");
|
||||
|
||||
HBox titlePane = new HBox();
|
||||
{
|
||||
@@ -395,10 +423,13 @@ public class GameCrashWindow extends Stage {
|
||||
reasonTitle.getStyleClass().add("two-line-item-second-large-title");
|
||||
|
||||
ScrollPane reasonPane = new ScrollPane(reasonTextFlow);
|
||||
reasonTextFlow.getStyleClass().add("crash-reason-text-flow");
|
||||
reasonPane.setFitToWidth(true);
|
||||
reasonPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
reasonPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
||||
|
||||
feedbackTextFlow.getStyleClass().add("crash-reason-text-flow");
|
||||
|
||||
gameDirPane.setPadding(new Insets(8));
|
||||
VBox.setVgrow(gameDirPane, Priority.ALWAYS);
|
||||
FXUtils.onChangeAndOperate(feedbackTextFlow.visibleProperty(), visible -> {
|
||||
@@ -411,6 +442,7 @@ public class GameCrashWindow extends Stage {
|
||||
}
|
||||
|
||||
HBox toolBar = new HBox();
|
||||
VBox.setMargin(toolBar, new Insets(0, 0, 4, 0));
|
||||
{
|
||||
JFXButton exportGameCrashInfoButton = FXUtils.newRaisedButton(i18n("logwindow.export_game_crash_logs"));
|
||||
exportGameCrashInfoButton.setOnAction(e -> exportGameCrashInfo());
|
||||
@@ -422,7 +454,6 @@ public class GameCrashWindow extends Stage {
|
||||
helpButton.setOnAction(e -> FXUtils.openLink(Metadata.CONTACT_URL));
|
||||
FXUtils.installFastTooltip(helpButton, i18n("logwindow.help"));
|
||||
|
||||
|
||||
toolBar.setPadding(new Insets(8));
|
||||
toolBar.setSpacing(8);
|
||||
toolBar.getStyleClass().add("jfx-tool-bar");
|
||||
|
||||
Loaded 100 of 511 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user