Compare commits
463
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d9e2d92e0 | ||
|
|
9f754e78e8 | ||
|
|
cecd28791b | ||
|
|
5e42652e75 | ||
|
|
9f682e0c39 | ||
|
|
eac12efafb | ||
|
|
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 | ||
|
|
69f19115ea | ||
|
|
ecb4eedd12 | ||
|
|
df6f35bc8e | ||
|
|
58d3780553 | ||
|
|
58b357d4b8 | ||
|
|
d69d27af57 | ||
|
|
f9a39f1c90 | ||
|
|
f8a4261480 | ||
|
|
2c428faa95 | ||
|
|
da91fe15df | ||
|
|
1df962884c | ||
|
|
8ad5d83946 | ||
|
|
74878bda0c | ||
|
|
b32d3304b0 | ||
|
|
60375c9632 | ||
|
|
33877691f8 | ||
|
|
109d6874ab | ||
|
|
a225e7fac4 | ||
|
|
7dad43570e | ||
|
|
8a166266d8 | ||
|
|
c53fbd0efb | ||
|
|
1e74567a22 | ||
|
|
0b9616f460 | ||
|
|
0b89612ec5 | ||
|
|
642893d2e9 | ||
|
|
2e878e43cb | ||
|
|
c8edf4be62 | ||
|
|
96f16382c0 | ||
|
|
11c45e0ab8 | ||
|
|
08f6f973c0 | ||
|
|
35e63fc8c8 | ||
|
|
23037b8afc | ||
|
|
25323dc385 | ||
|
|
496b403eeb | ||
|
|
7be00f2408 | ||
|
|
08d7974e27 | ||
|
|
f16cbed5b4 | ||
|
|
0988dc53cd | ||
|
|
f1c8791961 | ||
|
|
07e5ac9e0e | ||
|
|
c98dbe6694 | ||
|
|
460134cc1e | ||
|
|
9dd117c454 | ||
|
|
c60af8675f | ||
|
|
411efbe774 | ||
|
|
2e09a40b69 | ||
|
|
ba47c2d10d | ||
|
|
fd90ca8afe | ||
|
|
dda30274a4 | ||
|
|
2b43ee24a1 | ||
|
|
66f1e60433 | ||
|
|
3c3e2b8cfc | ||
|
|
365bb089c5 | ||
|
|
87af44328b | ||
|
|
8731faaac7 | ||
|
|
5abc1f1f9d | ||
|
|
40b4ecd42a | ||
|
|
e0f5c4d91e | ||
|
|
35ecd58460 | ||
|
|
f29c2bc202 | ||
|
|
361e928e73 | ||
|
|
484b65b513 | ||
|
|
19f02e3967 | ||
|
|
495da6b6b0 | ||
|
|
168c4ac708 | ||
|
|
7e8a6679cc | ||
|
|
e90d5185f6 | ||
|
|
8a89baee09 | ||
|
|
7f4c5fc4a0 | ||
|
|
75789c774d | ||
|
|
f0a16b3d14 | ||
|
|
710e69751f | ||
|
|
938d8647ce | ||
|
|
4b4dd592c3 | ||
|
|
225dba7448 | ||
|
|
5b1414428e | ||
|
|
cb467ae240 | ||
|
|
9416c0cfb3 | ||
|
|
16ea98c95a | ||
|
|
a8552a92d1 | ||
|
|
36d71bd14e | ||
|
|
cbe6554390 | ||
|
|
1b65e0b6d7 | ||
|
|
bd9ae189f8 | ||
|
|
867a04dfba | ||
|
|
dd7703c7a9 | ||
|
|
756b95ca8c | ||
|
|
ac0a0c7604 | ||
|
|
9ee611c0f6 | ||
|
|
c38371f454 | ||
|
|
eb3be8c465 | ||
|
|
e6ac07f62b | ||
|
|
a24fea4a95 | ||
|
|
8bd104c207 | ||
|
|
a14a265986 | ||
|
|
66524613b4 | ||
|
|
a0568e34a8 | ||
|
|
45f7719096 | ||
|
|
77fadf5d28 | ||
|
|
e9d7d0a33c | ||
|
|
94994ffdb6 | ||
|
|
26e274a89a | ||
|
|
6a497df0d1 | ||
|
|
fc1cce5e5a | ||
|
|
b328ed2cc9 | ||
|
|
5a8d567bd7 | ||
|
|
5005343d00 | ||
|
|
e4bc8f48c5 | ||
|
|
831a8a9bb8 | ||
|
|
7c024473d6 | ||
|
|
f594abc37d | ||
|
|
b66e249591 | ||
|
|
15e490f313 | ||
|
|
7ce59cac2c | ||
|
|
a7178802f8 | ||
|
|
51954163d4 | ||
|
|
19072bd617 | ||
|
|
a39a23f938 | ||
|
|
a10e9a04b1 | ||
|
|
c2d5a07053 | ||
|
|
e7526e39bf | ||
|
|
811b1fb5f4 | ||
|
|
72ae46c93d | ||
|
|
6b6276e6e2 | ||
|
|
60ff34a587 | ||
|
|
356960cc82 | ||
|
|
8708e7f8b1 | ||
|
|
962ee0d108 | ||
|
|
cf5957ffd1 | ||
|
|
f41b008b42 | ||
|
|
228a1e4887 | ||
|
|
913ce55d43 | ||
|
|
9ded2e489d | ||
|
|
c65e4d83a2 | ||
|
|
3184f91227 | ||
|
|
8eb17b6664 | ||
|
|
36ea1dac94 | ||
|
|
3fbe199aab | ||
|
|
8db8ade9c7 | ||
|
|
41e004f4d5 | ||
|
|
37ad445620 | ||
|
|
480f8b6890 | ||
|
|
24fd2cf521 | ||
|
|
68dbe6945b | ||
|
|
c3352d2b73 | ||
|
|
55a7169e21 | ||
|
|
50080ab875 | ||
|
|
5697cbd251 | ||
|
|
86ad055f8f | ||
|
+1 |
4883c3897b | ||
|
|
09c17fc85b | ||
|
|
fe59f495f6 | ||
|
|
9a475c2e92 | ||
|
|
8cd8a389b9 | ||
|
|
9cc45bc66a | ||
|
|
ea0b65be2e | ||
|
|
f4a8e3d1a6 | ||
|
|
7576bf6a01 | ||
|
|
3d63523f25 | ||
|
|
dbfd6c7599 | ||
|
|
ed6c7387bc | ||
|
|
6ab216dcb6 | ||
|
|
b38076f847 | ||
|
|
16816d8490 | ||
|
|
e8813fe153 | ||
|
|
ce24e4e622 | ||
|
|
10e5e7bc9c | ||
|
|
28b222e3dd | ||
|
|
86085031f6 | ||
|
|
30f47704f1 | ||
|
|
82e0cd1ca1 | ||
|
|
5374127241 | ||
|
|
df489c0cfe | ||
|
|
d83f6106f5 | ||
|
|
b7361c8da4 | ||
|
|
73531dbf60 | ||
|
|
5af6858d4c | ||
|
|
1a3b473356 | ||
|
|
67e4dddda1 | ||
|
|
55816032b7 | ||
|
|
b9fd0e50a7 | ||
|
|
71d54955ad | ||
|
|
d34dc82db6 | ||
|
|
73214ad599 | ||
|
|
1ddb73595d | ||
|
|
f9ee9c8592 | ||
|
|
0fdfc945c3 | ||
|
|
97219c951e | ||
|
|
64cc64095d | ||
|
|
91a613d966 | ||
|
|
06b7b1e574 | ||
|
|
933b7cab2f | ||
|
|
885e25d07a | ||
|
|
421f9a3b2b | ||
|
|
e52cfb036e | ||
|
|
87a3a4ece3 | ||
|
|
b152e2bfab | ||
|
|
c9c97a45b7 | ||
|
|
cba4ba2799 | ||
|
|
ab9eea3620 | ||
|
|
96f2d011ca | ||
|
|
9ee55cd17d | ||
|
|
07ee7867da | ||
|
|
940d4f722c | ||
|
|
af56096162 | ||
|
|
0e12c1a132 | ||
|
|
ca20509cb1 | ||
|
|
58c93ff349 | ||
|
|
aab3b22290 | ||
|
|
6cf933741f | ||
|
|
9d17834227 | ||
|
|
7805508d4c | ||
|
|
72f6c2faf3 | ||
|
|
7035767828 | ||
|
|
13a2777c87 | ||
|
|
a4f9438d9e | ||
|
|
4445021d66 | ||
|
|
f6a3b76a0b | ||
|
|
42bb602ff5 | ||
|
|
4547448718 | ||
|
|
10bda7e6f4 | ||
|
|
f239140055 | ||
|
|
6f4b557f63 | ||
|
|
5118ce4e62 | ||
|
|
a705b7cf95 | ||
|
|
0d5d1ae4e6 | ||
|
|
b57547415b | ||
|
|
b60d889bf7 | ||
|
|
2f463c1eda | ||
|
|
d1e006ed29 | ||
|
|
ba58905c9c | ||
|
|
de221b9dae | ||
|
|
0913c5e710 | ||
|
|
41fd38d8d1 | ||
|
|
7ec880852f | ||
|
|
9213b7fb2e | ||
|
|
80f1367d9a | ||
|
|
f521a041a9 | ||
|
|
be9efbe6ee | ||
|
|
8f3c716ca7 | ||
|
|
3cdddfa86d | ||
|
|
b1d8edc72a | ||
|
|
bbc546e2a0 | ||
|
|
f621776cdd | ||
|
|
7ba9d9eadd | ||
|
|
ac4c3a7501 | ||
|
|
f95ebe2224 | ||
|
|
7ee2c51b81 | ||
|
|
192c90816b | ||
|
|
1a772e8c65 | ||
|
|
95bed4712c | ||
|
|
2c6ceb3cb0 | ||
|
|
3118c87c65 | ||
|
|
f945a85441 | ||
|
|
72ffbb4176 | ||
|
|
77dc5dbe06 | ||
|
|
0a1d6be7b7 | ||
|
|
bb3d03fc72 | ||
|
|
a0ddaf6766 | ||
|
|
0bd2adc3c6 | ||
|
|
77c3ef6ced | ||
|
|
a66ddb9a20 | ||
|
|
eef365b37e | ||
|
|
5cab603b6e | ||
|
|
6c5d07adad | ||
|
|
1935a7021d | ||
|
|
31c46c963d | ||
|
|
4bc6ffa0f0 | ||
|
|
b62659de62 | ||
|
|
9a27c27b9a | ||
|
|
74c647cc97 | ||
|
|
e2df8e25b1 | ||
|
|
9969dc60c5 | ||
|
|
6230b436ad | ||
|
|
3873459aaa | ||
|
|
4ee3857427 | ||
|
|
2617dd3630 | ||
|
|
e0425102c8 | ||
|
|
a470475c21 | ||
|
|
bcd73e6d15 | ||
|
|
7d6f21dc3a | ||
|
|
3eddfa23b7 | ||
|
|
d5fa232704 | ||
|
|
c84afb3155 | ||
|
|
401fef5774 | ||
|
|
9298fe48e1 | ||
|
|
7f51e0395c | ||
|
|
4f410fb8ad | ||
|
|
9bcf95287a | ||
|
|
5703377626 | ||
|
|
3adb3a67e9 | ||
|
|
7918b333e6 | ||
|
|
329b0e8f3a | ||
|
|
3b17e0a897 | ||
|
|
e10f1dac20 | ||
|
|
86c171545e | ||
|
|
c3a3881cfc | ||
|
|
7f04cdd6b1 | ||
|
|
27361c65fe | ||
|
|
ffa7afcbab | ||
|
|
9aa1367823 | ||
|
|
acc96b95c8 | ||
|
|
46502875c5 | ||
|
|
43b769923b | ||
|
|
fe3fc0413c | ||
|
|
fd13088726 | ||
|
|
195db2ee6d | ||
|
|
96f68e1017 | ||
|
|
24d3bbf4fe | ||
|
|
d60a923841 |
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 服务器。
|
||||
@@ -1071,6 +1071,7 @@ ij_jsp_keep_indents_on_empty_lines = false
|
||||
ij_jspx_keep_indents_on_empty_lines = false
|
||||
|
||||
[{*.markdown,*.md}]
|
||||
max_line_length = 200
|
||||
ij_markdown_force_one_space_after_blockquote_symbol = true
|
||||
ij_markdown_force_one_space_after_header_symbol = true
|
||||
ij_markdown_force_one_space_after_list_bullet = true
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
name: Check Codes
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '**.java'
|
||||
- '**.properties'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.java'
|
||||
- '**.properties'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
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,25 +0,0 @@
|
||||
name: Check Style
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '**.java'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.java'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Check style main
|
||||
run: ./gradlew checkstyleMain --no-daemon
|
||||
- name: Check style test
|
||||
run: ./gradlew checkstyleTest --no-daemon
|
||||
@@ -1,23 +0,0 @@
|
||||
name: Check Translations
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '**.properties'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.properties'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Check style test
|
||||
run: ./gradlew checkTranslations --no-daemon
|
||||
@@ -1,141 +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@v4
|
||||
- name: Fetch tags
|
||||
run: git fetch --all --tags
|
||||
- name: Install tools
|
||||
run: sudo apt-get install -y jq
|
||||
- name: Fetch last version
|
||||
run: |
|
||||
wget -O ci.json https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/api/json
|
||||
|
||||
export HMCL_EXE_FILE_NAME=`cat ci.json | jq -M -r '.artifacts[] | select(.fileName | endswith(".exe")) | .fileName'`
|
||||
if [ -z `echo $HMCL_EXE_FILE_NAME | grep -E "^HMCL-[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?\.exe\$"` ]; then exit 1; fi
|
||||
|
||||
export HMCL_VERSION=`echo "${HMCL_EXE_FILE_NAME%.exe}" | tail -c +6`
|
||||
export HMCL_COMMIT_SHA=`cat ci.json | jq -M -r '.actions[] | select(._class == "hudson.plugins.git.util.BuildData") | .lastBuiltRevision.SHA1'`
|
||||
|
||||
if [ "${#HMCL_COMMIT_SHA}" != 40 ]; then exit 1; fi
|
||||
|
||||
echo "HMCL_VERSION=$HMCL_VERSION" >> $GITHUB_ENV
|
||||
echo "HMCL_COMMIT_SHA=$HMCL_COMMIT_SHA" >> $GITHUB_ENV
|
||||
echo "HMCL_TAG_NAME=v$HMCL_VERSION" >> $GITHUB_ENV
|
||||
- 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 "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe.sha256"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha256"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh.sha256"
|
||||
env:
|
||||
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/artifact/HMCL/build/libs
|
||||
- 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@v3
|
||||
- name: Fetch tags
|
||||
run: git fetch --all --tags
|
||||
- name: Install tools
|
||||
run: sudo apt-get install -y jq
|
||||
- name: Fetch last version
|
||||
run: |
|
||||
wget -O ci.json https://ci.huangyuhui.net/job/HMCL-stable/lastSuccessfulBuild/api/json
|
||||
|
||||
export HMCL_EXE_FILE_NAME=`cat ci.json | jq -M -r '.artifacts[] | select(.fileName | endswith(".exe")) | .fileName'`
|
||||
if [ -z `echo $HMCL_EXE_FILE_NAME | grep -E "^HMCL-[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?\.exe\$"` ]; then exit 1; fi
|
||||
|
||||
export HMCL_VERSION=`echo "${HMCL_EXE_FILE_NAME%.exe}" | tail -c +6`
|
||||
export HMCL_COMMIT_SHA=`cat ci.json | jq -M -r '.actions[] | select(._class == "hudson.plugins.git.util.BuildData") | .lastBuiltRevision.SHA1'`
|
||||
|
||||
if [ "${#HMCL_COMMIT_SHA}" != 40 ]; then exit 1; fi
|
||||
|
||||
echo "HMCL_VERSION=$HMCL_VERSION" >> $GITHUB_ENV
|
||||
echo "HMCL_COMMIT_SHA=$HMCL_COMMIT_SHA" >> $GITHUB_ENV
|
||||
echo "HMCL_TAG_NAME=release-$HMCL_VERSION" >> $GITHUB_ENV
|
||||
- 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 "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe.sha256"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha256"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh.sha256"
|
||||
env:
|
||||
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL-stable/lastSuccessfulBuild/artifact/HMCL/build/libs
|
||||
- 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/release-$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.jar]($GH_DOWNLOAD_BASE_URL/release-$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.sh]($GH_DOWNLOAD_BASE_URL/release-$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 }}
|
||||
@@ -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
|
||||
@@ -13,22 +13,42 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 11
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
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 Artifacts
|
||||
- name: Upload JAR
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}
|
||||
path: HMCL/build/libs
|
||||
name: HMCL-${{ env.SHORT_SHA }}-jar
|
||||
path: |
|
||||
HMCL/build/libs/HMCL-*.jar
|
||||
HMCL/build/libs/HMCL-*.jar.sha256
|
||||
- name: Upload EXE
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}-exe
|
||||
path: |
|
||||
HMCL/build/libs/HMCL-*.exe
|
||||
HMCL/build/libs/HMCL-*.exe.sha256
|
||||
- name: Upload SH
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}-sh
|
||||
path: |
|
||||
HMCL/build/libs/HMCL-*.sh
|
||||
HMCL/build/libs/HMCL-*.sh.sha256
|
||||
@@ -0,0 +1,32 @@
|
||||
name: Mirror Repository
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
|
||||
concurrency:
|
||||
group: mirror-repository
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
mirror:
|
||||
strategy:
|
||||
matrix:
|
||||
target:
|
||||
- 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.target.name }}
|
||||
if: ${{ github.repository == 'HMCL-dev/HMCL' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Mirror GitHub to ${{ matrix.target.name }}
|
||||
run: |
|
||||
git clone --mirror "https://github.com/$GITHUB_REPOSITORY.git" -- repo
|
||||
cd repo
|
||||
git push -f --prune "https://${{ matrix.target.user }}:${{ secrets[matrix.target.token] }}@${{ matrix.target.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
|
||||
+12
-1
@@ -2,6 +2,7 @@
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
*.hprof
|
||||
|
||||
.gradle
|
||||
|
||||
@@ -13,13 +14,21 @@ hs_err_pid*
|
||||
/externalgames
|
||||
NVIDIA
|
||||
minecraft-exported-crash-info*
|
||||
hmcl-exported-logs-*
|
||||
/.java/
|
||||
/.local/
|
||||
/.cache/
|
||||
|
||||
# IANA Language Subtag Registry
|
||||
language-subtag-registry
|
||||
|
||||
# gradle build
|
||||
/build/
|
||||
/HMCL/build/
|
||||
/HMCLCore/build/
|
||||
/HMCLTransformerDiscoveryService/build/
|
||||
/HMCLBoot/build/
|
||||
/minecraft/libraries/HMCLTransformerDiscoveryService/build/
|
||||
/minecraft/libraries/HMCLMultiMCBootstrap/build/
|
||||
/buildSrc/build/
|
||||
|
||||
# idea
|
||||
@@ -28,12 +37,14 @@ minecraft-exported-crash-info*
|
||||
/HMCL/out/
|
||||
/HMCLCore/out/
|
||||
/minecraft/libraries/HMCLTransformerDiscoveryService/out/
|
||||
/minecraft/libraries/HMCLMultiMCBootstrap/out/
|
||||
|
||||
# eclipse
|
||||
/bin/
|
||||
/HMCL/bin/
|
||||
/HMCLCore/bin/
|
||||
/minecraft/libraries/HMCLTransformerDiscoveryService/bin/
|
||||
/minecraft/libraries/HMCLMultiMCBootstrap/bin/
|
||||
.classpath
|
||||
.project
|
||||
.settings
|
||||
|
||||
+142
-45
@@ -1,5 +1,9 @@
|
||||
import org.jackhuang.hmcl.gradle.ci.GitHubActionUtils
|
||||
import org.jackhuang.hmcl.gradle.ci.JenkinsUtils
|
||||
import org.jackhuang.hmcl.gradle.l10n.CheckTranslations
|
||||
import org.jackhuang.hmcl.gradle.l10n.CreateLanguageList
|
||||
import org.jackhuang.hmcl.gradle.l10n.CreateLocaleNamesResourceBundle
|
||||
import org.jackhuang.hmcl.gradle.l10n.UpsideDownTranslate
|
||||
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
|
||||
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
|
||||
import java.net.URI
|
||||
@@ -46,15 +50,21 @@ if (buildNumber != null) {
|
||||
}
|
||||
}
|
||||
|
||||
val embedResources by configurations.registering
|
||||
|
||||
dependencies {
|
||||
implementation(project(":HMCLCore"))
|
||||
implementation(project(":HMCLBoot"))
|
||||
implementation("libs:JFoenix")
|
||||
implementation(libs.twelvemonkeys.imageio.webp)
|
||||
implementation(libs.java.info)
|
||||
implementation(libs.monet.fx)
|
||||
|
||||
if (launcherExe.isBlank()) {
|
||||
implementation(libs.hmclauncher)
|
||||
}
|
||||
|
||||
embedResources(libs.authlib.injector)
|
||||
}
|
||||
|
||||
fun digest(algorithm: String, bytes: ByteArray): ByteArray = MessageDigest.getInstance(algorithm).digest(bytes)
|
||||
@@ -98,16 +108,68 @@ fun attachSignature(jar: File) {
|
||||
}
|
||||
}
|
||||
|
||||
val java11 = sourceSets.create("java11") {
|
||||
java {
|
||||
srcDir("src/main/java11")
|
||||
}
|
||||
tasks.withType<JavaCompile> {
|
||||
sourceCompatibility = "17"
|
||||
targetCompatibility = "17"
|
||||
}
|
||||
|
||||
tasks.getByName<JavaCompile>(java11.compileJavaTaskName) {
|
||||
options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
|
||||
sourceCompatibility = "11"
|
||||
targetCompatibility = "11"
|
||||
tasks.checkstyleMain {
|
||||
// Third-party code is not checked
|
||||
exclude("**/org/jackhuang/hmcl/ui/image/apng/**")
|
||||
}
|
||||
|
||||
val addOpens = listOf(
|
||||
"java.base/java.lang",
|
||||
"java.base/java.lang.reflect",
|
||||
"java.base/jdk.internal.loader",
|
||||
"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/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(" "))
|
||||
System.getenv("GITHUB_SHA")?.let {
|
||||
add("hmcl.version.hash" to it)
|
||||
}
|
||||
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!!)
|
||||
}
|
||||
|
||||
val hmclPropertiesFile = layout.buildDirectory.file("hmcl.properties")
|
||||
val createPropertiesFile by tasks.registering {
|
||||
outputs.file(hmclPropertiesFile)
|
||||
hmclProperties.forEach { (k, v) -> inputs.property(k, v) }
|
||||
|
||||
doLast {
|
||||
val targetFile = hmclPropertiesFile.get().asFile
|
||||
targetFile.parentFile.mkdir()
|
||||
targetFile.bufferedWriter().use {
|
||||
for ((k, v) in hmclProperties) {
|
||||
it.write("$k=$v\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.jar {
|
||||
@@ -118,6 +180,8 @@ tasks.jar {
|
||||
val jarPath = tasks.jar.get().archiveFile.get().asFile
|
||||
|
||||
tasks.shadowJar {
|
||||
dependsOn(createPropertiesFile)
|
||||
|
||||
archiveClassifier.set(null as String?)
|
||||
|
||||
exclude("**/package-info.class")
|
||||
@@ -127,7 +191,7 @@ tasks.shadowJar {
|
||||
exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi")
|
||||
|
||||
listOf(
|
||||
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*","freebsd-*", "linux-*", "darwin-*",
|
||||
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "linux-*", "darwin-*",
|
||||
"*-ppc", "*-ppc64le", "*-s390x", "*-armel",
|
||||
).forEach { exclude("com/sun/jna/$it/**") }
|
||||
|
||||
@@ -135,40 +199,18 @@ tasks.shadowJar {
|
||||
exclude(dependency("com.google.code.gson:.*:.*"))
|
||||
exclude(dependency("net.java.dev.jna:jna:.*"))
|
||||
exclude(dependency("libs:JFoenix:.*"))
|
||||
exclude(project(":HMCLBoot"))
|
||||
}
|
||||
|
||||
manifest {
|
||||
attributes(
|
||||
"Created-By" to "Copyright(c) 2013-2025 huangyuhui.",
|
||||
"Main-Class" to "org.jackhuang.hmcl.Main",
|
||||
"Multi-Release" to "true",
|
||||
"Implementation-Version" to project.version,
|
||||
"Microsoft-Auth-Id" to microsoftAuthId,
|
||||
"Microsoft-Auth-Secret" to microsoftAuthSecret,
|
||||
"CurseForge-Api-Key" to curseForgeApiKey,
|
||||
"Build-Channel" to versionType,
|
||||
"Add-Opens" to listOf(
|
||||
"java.base/java.lang",
|
||||
"java.base/java.lang.reflect",
|
||||
"java.base/jdk.internal.loader",
|
||||
"javafx.base/com.sun.javafx.binding",
|
||||
"javafx.base/com.sun.javafx.event",
|
||||
"javafx.base/com.sun.javafx.runtime",
|
||||
"javafx.graphics/javafx.css",
|
||||
"javafx.graphics/com.sun.javafx.stage",
|
||||
"javafx.graphics/com.sun.prism",
|
||||
"javafx.controls/com.sun.javafx.scene.control",
|
||||
"javafx.controls/com.sun.javafx.scene.control.behavior",
|
||||
"javafx.controls/javafx.scene.control.skin",
|
||||
"jdk.attach/sun.tools.attach",
|
||||
).joinToString(" "),
|
||||
"Enable-Native-Access" to "ALL-UNNAMED"
|
||||
)
|
||||
|
||||
System.getenv("GITHUB_SHA")?.also {
|
||||
attributes("GitHub-SHA" to it)
|
||||
}
|
||||
}
|
||||
manifest.attributes(
|
||||
"Created-By" to "Copyright(c) 2013-2025 huangyuhui.",
|
||||
"Implementation-Version" to project.version.toString(),
|
||||
"Main-Class" to "org.jackhuang.hmcl.Main",
|
||||
"Multi-Release" to "true",
|
||||
"Add-Opens" to addOpens.joinToString(" "),
|
||||
"Enable-Native-Access" to "ALL-UNNAMED",
|
||||
"Enable-Final-Field-Mutation" to "ALL-UNNAMED",
|
||||
)
|
||||
|
||||
if (launcherExe.isNotBlank()) {
|
||||
into("assets") {
|
||||
@@ -183,10 +225,21 @@ tasks.shadowJar {
|
||||
}
|
||||
|
||||
tasks.processResources {
|
||||
into("META-INF/versions/11") {
|
||||
from(sourceSets["java11"].output)
|
||||
dependsOn(createPropertiesFile)
|
||||
dependsOn(upsideDownTranslate)
|
||||
dependsOn(createLocaleNamesResourceBundle)
|
||||
dependsOn(createLanguageList)
|
||||
|
||||
into("assets/") {
|
||||
from(hmclPropertiesFile)
|
||||
from(embedResources)
|
||||
}
|
||||
|
||||
into("assets/lang") {
|
||||
from(createLanguageList.map { it.outputFile })
|
||||
from(upsideDownTranslate.map { it.outputFile })
|
||||
from(createLocaleNamesResourceBundle.map { it.outputDirectory })
|
||||
}
|
||||
dependsOn(tasks["java11Classes"])
|
||||
}
|
||||
|
||||
val makeExecutables by tasks.registering {
|
||||
@@ -271,6 +324,16 @@ fun parseToolOptions(options: String?): MutableList<String> {
|
||||
return result
|
||||
}
|
||||
|
||||
// For IntelliJ IDEA
|
||||
tasks.withType<JavaExec> {
|
||||
if (name != "run") {
|
||||
jvmArgs(addOpens.map { "--add-opens=$it=ALL-UNNAMED" })
|
||||
// if (javaVersion >= JavaVersion.VERSION_24) {
|
||||
// jvmArgs("--enable-native-access=ALL-UNNAMED")
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register<JavaExec>("run") {
|
||||
dependsOn(tasks.jar)
|
||||
|
||||
@@ -279,7 +342,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"
|
||||
|
||||
@@ -299,6 +362,40 @@ tasks.register<JavaExec>("run") {
|
||||
}
|
||||
}
|
||||
|
||||
// Check Translations
|
||||
|
||||
tasks.register<CheckTranslations>("checkTranslations") {
|
||||
val dir = layout.projectDirectory.dir("src/main/resources/assets/lang")
|
||||
|
||||
englishFile.set(dir.file("I18N.properties"))
|
||||
simplifiedChineseFile.set(dir.file("I18N_zh_CN.properties"))
|
||||
traditionalChineseFile.set(dir.file("I18N_zh.properties"))
|
||||
classicalChineseFile.set(dir.file("I18N_lzh.properties"))
|
||||
}
|
||||
|
||||
// l10n
|
||||
|
||||
val generatedDir = layout.buildDirectory.dir("generated")
|
||||
|
||||
val upsideDownTranslate by tasks.registering(UpsideDownTranslate::class) {
|
||||
inputFile.set(layout.projectDirectory.file("src/main/resources/assets/lang/I18N.properties"))
|
||||
outputFile.set(generatedDir.map { it.file("generated/i18n/I18N_en_Qabs.properties") })
|
||||
}
|
||||
|
||||
val createLanguageList by tasks.registering(CreateLanguageList::class) {
|
||||
resourceBundleDir.set(layout.projectDirectory.dir("src/main/resources/assets/lang"))
|
||||
resourceBundleBaseName.set("I18N")
|
||||
additionalLanguages.set(listOf("en-Qabs"))
|
||||
outputFile.set(generatedDir.map { it.file("languages.json") })
|
||||
}
|
||||
|
||||
val createLocaleNamesResourceBundle by tasks.registering(CreateLocaleNamesResourceBundle::class) {
|
||||
dependsOn(createLanguageList)
|
||||
|
||||
languagesFile.set(createLanguageList.flatMap { it.outputFile })
|
||||
outputDirectory.set(generatedDir.map { it.dir("generated/LocaleNames") })
|
||||
}
|
||||
|
||||
// mcmod data
|
||||
|
||||
tasks.register<ParseModDataTask>("parseModData") {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
@@ -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,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,800 @@
|
||||
/*
|
||||
* 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.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 boolean enabled = true;
|
||||
private boolean forceOverlay = false;
|
||||
private final Interpolator rippleInterpolator = Interpolator.SPLINE(0.0825,
|
||||
0.3025,
|
||||
0.0875,
|
||||
0.9975); //0.1, 0.54, 0.28, 0.95);
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enable) {
|
||||
this.enabled = enable;
|
||||
}
|
||||
|
||||
// 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) -> createRipple(event.getX(), event.getY()));
|
||||
// create fade out transition for the ripple
|
||||
control.addEventHandler(MouseEvent.MOUSE_RELEASED, e -> 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 (enabled) {
|
||||
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 (ripplerFill.get() instanceof Color) {
|
||||
return new Color(((Color) ripplerFill.get()).getRed(),
|
||||
((Color) ripplerFill.get()).getGreen(),
|
||||
((Color) ripplerFill.get()).getBlue(),
|
||||
0.2);
|
||||
} else {
|
||||
return Color.TRANSPARENT;
|
||||
}
|
||||
}, ripplerFill));
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
ripplerRadius.get().doubleValue() == Region.USE_COMPUTED_SIZE ?
|
||||
computeRippleRadius() : ripplerRadius.get().doubleValue(), 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, rippleInterpolator);
|
||||
inKeyValues[1] = new KeyValue(scaleYProperty(), 0.9, rippleInterpolator);
|
||||
|
||||
outKeyValues[0] = new KeyValue(this.scaleXProperty(), 1, rippleInterpolator);
|
||||
outKeyValues[1] = new KeyValue(this.scaleYProperty(), 1, rippleInterpolator);
|
||||
outKeyValues[2] = new KeyValue(this.opacityProperty(), 0, rippleInterpolator);
|
||||
|
||||
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),
|
||||
rippleInterpolator);
|
||||
inKeyValues[3] = outKeyValues[4] = new KeyValue(translateYProperty(),
|
||||
Math.signum(dy) * Math.min(Math.abs(dy),
|
||||
this.getRadius() / 2),
|
||||
rippleInterpolator);
|
||||
}
|
||||
inAnimation = new Timeline(new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(scaleXProperty(),
|
||||
0,
|
||||
rippleInterpolator),
|
||||
new KeyValue(scaleYProperty(),
|
||||
0,
|
||||
rippleInterpolator),
|
||||
new KeyValue(translateXProperty(),
|
||||
0,
|
||||
rippleInterpolator),
|
||||
new KeyValue(translateYProperty(),
|
||||
0,
|
||||
rippleInterpolator),
|
||||
new KeyValue(opacityProperty(),
|
||||
1,
|
||||
rippleInterpolator)
|
||||
), new KeyFrame(Duration.millis(900), inKeyValues));
|
||||
|
||||
setScaleX(0);
|
||||
setScaleY(0);
|
||||
if (ripplerFill.get() instanceof Color) {
|
||||
Color circleColor = new Color(((Color) ripplerFill.get()).getRed(),
|
||||
((Color) ripplerFill.get()).getGreen(),
|
||||
((Color) ripplerFill.get()).getBlue(),
|
||||
0.3);
|
||||
setStroke(circleColor);
|
||||
setFill(circleColor);
|
||||
} else {
|
||||
setStroke(ripplerFill.get());
|
||||
setFill(ripplerFill.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 final StyleableObjectProperty<Boolean> ripplerRecenter = new SimpleStyleableObjectProperty<>(
|
||||
StyleableProperties.RIPPLER_RECENTER,
|
||||
JFXRippler.this,
|
||||
"ripplerRecenter",
|
||||
false);
|
||||
|
||||
public Boolean isRipplerRecenter() {
|
||||
return ripplerRecenter != null && ripplerRecenter.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Boolean> ripplerRecenterProperty() {
|
||||
return this.ripplerRecenter;
|
||||
}
|
||||
|
||||
public void setRipplerRecenter(Boolean radius) {
|
||||
this.ripplerRecenter.set(radius);
|
||||
}
|
||||
|
||||
/**
|
||||
* the ripple radius size, by default it will be automatically computed.
|
||||
*/
|
||||
private final StyleableObjectProperty<Number> ripplerRadius = new SimpleStyleableObjectProperty<>(
|
||||
StyleableProperties.RIPPLER_RADIUS,
|
||||
JFXRippler.this,
|
||||
"ripplerRadius",
|
||||
Region.USE_COMPUTED_SIZE);
|
||||
|
||||
public Number getRipplerRadius() {
|
||||
return ripplerRadius == null ? Region.USE_COMPUTED_SIZE : ripplerRadius.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Number> ripplerRadiusProperty() {
|
||||
return this.ripplerRadius;
|
||||
}
|
||||
|
||||
public void setRipplerRadius(Number radius) {
|
||||
this.ripplerRadius.set(radius);
|
||||
}
|
||||
|
||||
/**
|
||||
* the default color of the ripple effect
|
||||
*/
|
||||
private final StyleableObjectProperty<Paint> ripplerFill = new SimpleStyleableObjectProperty<>(StyleableProperties.RIPPLER_FILL,
|
||||
JFXRippler.this,
|
||||
"ripplerFill",
|
||||
Color.rgb(0,
|
||||
200,
|
||||
255));
|
||||
|
||||
public Paint getRipplerFill() {
|
||||
return ripplerFill == null ? Color.rgb(0, 200, 255) : ripplerFill.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> ripplerFillProperty() {
|
||||
return this.ripplerFill;
|
||||
}
|
||||
|
||||
public void setRipplerFill(Paint color) {
|
||||
this.ripplerFill.set(color);
|
||||
}
|
||||
|
||||
/// mask property used for clipping the rippler.
|
||||
/// can be either CIRCLE/RECT
|
||||
private final StyleableObjectProperty<RipplerMask> maskType = new SimpleStyleableObjectProperty<>(
|
||||
StyleableProperties.MASK_TYPE,
|
||||
JFXRippler.this,
|
||||
"maskType",
|
||||
RipplerMask.RECT);
|
||||
|
||||
public RipplerMask getMaskType() {
|
||||
return maskType == null ? RipplerMask.RECT : maskType.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<RipplerMask> maskTypeProperty() {
|
||||
return this.maskType;
|
||||
}
|
||||
|
||||
public void setMaskType(RipplerMask type) {
|
||||
this.maskType.set(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* the ripple disable, by default it's false.
|
||||
* if true the ripple effect will be hidden
|
||||
*/
|
||||
private final StyleableBooleanProperty ripplerDisabled = new SimpleStyleableBooleanProperty(
|
||||
StyleableProperties.RIPPLER_DISABLED,
|
||||
JFXRippler.this,
|
||||
"ripplerDisabled",
|
||||
false);
|
||||
|
||||
public Boolean isRipplerDisabled() {
|
||||
return ripplerDisabled != null && ripplerDisabled.get();
|
||||
}
|
||||
|
||||
public StyleableBooleanProperty ripplerDisabledProperty() {
|
||||
return this.ripplerDisabled;
|
||||
}
|
||||
|
||||
public void setRipplerDisabled(Boolean disabled) {
|
||||
this.ripplerDisabled.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(), Color.rgb(0, 200, 255)) {
|
||||
@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,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,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,222 @@
|
||||
//
|
||||
// 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.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;
|
||||
|
||||
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);
|
||||
});
|
||||
control.focusedProperty().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,407 @@
|
||||
/*
|
||||
* 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.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 java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* @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.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
|
||||
|
||||
hsbField.getStyleClass().add("custom-color-field");
|
||||
hsbField.setPromptText("HSB Color");
|
||||
hsbField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
|
||||
|
||||
hexField.getStyleClass().add("custom-color-field");
|
||||
hexField.setPromptText("#HEX Color");
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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,96 @@
|
||||
// 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.adapters.skins.ListViewSkin;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.skin.VirtualFlow;
|
||||
import javafx.scene.layout.Region;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4720
|
||||
public class JFXListViewSkin<T> extends ListViewSkin<T> {
|
||||
|
||||
private final VirtualFlow<ListCell<T>> flow;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public JFXListViewSkin(final JFXListView<T> listView) {
|
||||
super(listView);
|
||||
flow = (VirtualFlow<ListCell<T>>) getChildren().get(0);
|
||||
JFXDepthManager.setDepth(flow, listView.depthProperty().get());
|
||||
listView.depthProperty().addListener((o, oldVal, newVal) -> JFXDepthManager.setDepth(flow, newVal));
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
final int itemsCount = getSkinnable().getItems().size();
|
||||
if (getSkinnable().maxHeightProperty().isBound() || itemsCount <= 0) {
|
||||
return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
|
||||
}
|
||||
|
||||
final double fixedCellSize = getSkinnable().getFixedCellSize();
|
||||
double computedHeight = fixedCellSize != Region.USE_COMPUTED_SIZE ?
|
||||
fixedCellSize * itemsCount + snapVerticalInsets() : estimateHeight();
|
||||
double height = super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
|
||||
if (height > computedHeight) {
|
||||
height = computedHeight;
|
||||
}
|
||||
|
||||
if (getSkinnable().getMaxHeight() > 0 && computedHeight > getSkinnable().getMaxHeight()) {
|
||||
return getSkinnable().getMaxHeight();
|
||||
}
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
private double estimateHeight() {
|
||||
// compute the border/padding for the list
|
||||
double borderWidth = snapVerticalInsets();
|
||||
// compute the gap between list cells
|
||||
|
||||
JFXListView<T> listview = (JFXListView<T>) getSkinnable();
|
||||
double gap = listview.isExpanded() ? ((JFXListView<T>) getSkinnable()).getVerticalGap() * (getSkinnable().getItems()
|
||||
.size()) : 0;
|
||||
// compute the height of each list cell
|
||||
double cellsHeight = 0;
|
||||
for (int i = 0; i < flow.getCellCount(); i++) {
|
||||
ListCell<T> cell = flow.getCell(i);
|
||||
cellsHeight += cell.getHeight();
|
||||
}
|
||||
return cellsHeight + gap + borderWidth;
|
||||
}
|
||||
|
||||
private double snapVerticalInsets() {
|
||||
return getSkinnable().snappedBottomInset() + getSkinnable().snappedTopInset();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
//
|
||||
// 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.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.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();
|
||||
control.focusedProperty().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,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,64 @@
|
||||
/*
|
||||
* 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.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/// @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 JFXNodeUtils() {
|
||||
}
|
||||
|
||||
}
|
||||
+129
-118
@@ -17,41 +17,28 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.Alert;
|
||||
import org.jackhuang.hmcl.util.FileSaver;
|
||||
import org.jackhuang.hmcl.ui.AwtUtils;
|
||||
import org.jackhuang.hmcl.util.ModuleHelper;
|
||||
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
|
||||
import org.jackhuang.hmcl.ui.SwingUtils;
|
||||
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 javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Method;
|
||||
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.nio.file.Paths;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
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;
|
||||
|
||||
public final class Main {
|
||||
public final class EntryPoint {
|
||||
|
||||
private Main() {
|
||||
private EntryPoint() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
@@ -62,28 +49,10 @@ public final class Main {
|
||||
createHMCLDirectories();
|
||||
LOG.start(Metadata.HMCL_CURRENT_DIRECTORY.resolve("logs"));
|
||||
|
||||
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU")))
|
||||
System.getProperties().putIfAbsent("prism.forceGPU", "true");
|
||||
|
||||
String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE");
|
||||
if (animationFrameRate != null) {
|
||||
try {
|
||||
if (Integer.parseInt(animationFrameRate) <= 0)
|
||||
throw new NumberFormatException(animationFrameRate);
|
||||
|
||||
System.getProperties().putIfAbsent("javafx.animation.pulse", animationFrameRate);
|
||||
} catch (NumberFormatException e) {
|
||||
LOG.warning("Invalid animation frame rate: " + animationFrameRate);
|
||||
}
|
||||
}
|
||||
|
||||
setupJavaFXVMOptions();
|
||||
checkDirectoryPath();
|
||||
|
||||
if (JavaRuntime.CURRENT_VERSION < 9)
|
||||
// This environment check will take ~300ms
|
||||
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && !isInsideMacAppBundle())
|
||||
initIcon();
|
||||
|
||||
checkJavaFX();
|
||||
@@ -100,6 +69,71 @@ public final class Main {
|
||||
System.exit(exitCode);
|
||||
}
|
||||
|
||||
private static void setupJavaFXVMOptions() {
|
||||
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU"))) {
|
||||
LOG.info("HMCL_FORCE_GPU: true");
|
||||
System.getProperties().putIfAbsent("prism.forceGPU", "true");
|
||||
}
|
||||
|
||||
String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE");
|
||||
if (animationFrameRate != null) {
|
||||
LOG.info("HMCL_ANIMATION_FRAME_RATE: " + animationFrameRate);
|
||||
|
||||
try {
|
||||
if (Integer.parseInt(animationFrameRate) <= 0)
|
||||
throw new NumberFormatException(animationFrameRate);
|
||||
|
||||
System.getProperties().putIfAbsent("javafx.animation.pulse", animationFrameRate);
|
||||
} catch (NumberFormatException e) {
|
||||
LOG.warning("Invalid animation frame rate: " + animationFrameRate);
|
||||
}
|
||||
}
|
||||
|
||||
String uiScale = System.getProperty("hmcl.uiScale", System.getenv("HMCL_UI_SCALE"));
|
||||
if (uiScale != null) {
|
||||
uiScale = uiScale.trim();
|
||||
|
||||
LOG.info("HMCL_UI_SCALE: " + uiScale);
|
||||
|
||||
try {
|
||||
float scaleValue;
|
||||
if (uiScale.endsWith("%")) {
|
||||
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 1)) / 100.0f;
|
||||
} else if (uiScale.endsWith("dpi") || uiScale.endsWith("DPI")) {
|
||||
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 3)) / 96.0f;
|
||||
} else {
|
||||
scaleValue = Float.parseFloat(uiScale);
|
||||
}
|
||||
|
||||
float lowerBound;
|
||||
float upperBound;
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
// JavaFX behavior may be abnormal when the DPI scaling factor is too high
|
||||
lowerBound = 0.25f;
|
||||
upperBound = 4f;
|
||||
} else {
|
||||
lowerBound = 0.01f;
|
||||
upperBound = 10f;
|
||||
}
|
||||
|
||||
if (scaleValue >= lowerBound && scaleValue <= upperBound) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
System.getProperties().putIfAbsent("glass.win.uiScale", uiScale);
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
LOG.warning("macOS does not support setting UI scale, so it will be ignored");
|
||||
} else {
|
||||
System.getProperties().putIfAbsent("glass.gtk.uiScale", uiScale);
|
||||
}
|
||||
} else {
|
||||
LOG.warning("UI scale out of range: " + uiScale);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Invalid UI scale: " + uiScale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void createHMCLDirectories() {
|
||||
if (!Files.isDirectory(Metadata.HMCL_CURRENT_DIRECTORY)) {
|
||||
try {
|
||||
@@ -112,6 +146,8 @@ public final class Main {
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Logger has not been started yet, so print directly to System.err
|
||||
System.err.println("Failed to create HMCL directory: " + Metadata.HMCL_CURRENT_DIRECTORY);
|
||||
e.printStackTrace(System.err);
|
||||
showErrorAndExit(i18n("fatal.create_hmcl_current_directory_failure", Metadata.HMCL_CURRENT_DIRECTORY));
|
||||
}
|
||||
@@ -126,17 +162,43 @@ public final class Main {
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(Main.class.getResource("/assets/img/icon-mac.png"));
|
||||
AwtUtils.setAppleIcon(image);
|
||||
try {
|
||||
if (java.awt.Taskbar.isTaskbarSupported()) {
|
||||
var image = java.awt.Toolkit.getDefaultToolkit().getImage(EntryPoint.class.getResource("/assets/img/icon-mac.png"));
|
||||
java.awt.Taskbar.getTaskbar().setIconImage(image);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to set application icon", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkDirectoryPath() {
|
||||
String currentDirectory = new File("").getAbsolutePath();
|
||||
if (currentDirectory.contains("!")) {
|
||||
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 " + currentDirectory);
|
||||
+ "The path is " + currentDir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,10 +206,10 @@ public final class Main {
|
||||
try {
|
||||
SelfDependencyPatcher.patch();
|
||||
} catch (SelfDependencyPatcher.PatchException e) {
|
||||
LOG.error("unable to patch JVM", e);
|
||||
LOG.error("Unable to patch JVM", e);
|
||||
showErrorAndExit(i18n("fatal.javafx.missing"));
|
||||
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
|
||||
LOG.error("unable to patch JVM", e);
|
||||
LOG.error("Unable to patch JVM", e);
|
||||
showErrorAndExit(i18n("fatal.javafx.incompatible"));
|
||||
} catch (CancellationException e) {
|
||||
LOG.error("User cancels downloading JavaFX", e);
|
||||
@@ -164,7 +226,7 @@ public final class Main {
|
||||
Class.forName("javafx.stage.Stage"); // javafx.graphics
|
||||
Class.forName("javafx.scene.control.Skin"); // javafx.controls
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(System.err);
|
||||
LOG.warning("JavaFX is incomplete or not found", e);
|
||||
showErrorAndExit(i18n("fatal.javafx.incomplete"));
|
||||
}
|
||||
}
|
||||
@@ -172,9 +234,20 @@ public final class Main {
|
||||
private static void addEnableNativeAccess() {
|
||||
if (JavaRuntime.CURRENT_VERSION > 21) {
|
||||
try {
|
||||
ModuleHelper.addEnableNativeAccess(Class.forName("javafx.stage.Stage")); // javafx.graphics
|
||||
// javafx.graphics
|
||||
Module module = Class.forName("javafx.stage.Stage").getModule();
|
||||
if (module.isNamed()) {
|
||||
try {
|
||||
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Module.class, MethodHandles.lookup());
|
||||
MethodHandle implAddEnableNativeAccess = lookup.findVirtual(Module.class,
|
||||
"implAddEnableNativeAccess", MethodType.methodType(Module.class));
|
||||
Module ignored = (Module) implAddEnableNativeAccess.invokeExact(module);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace(System.err);
|
||||
LOG.error("Failed to add enable native access for JavaFX", e);
|
||||
showErrorAndExit(i18n("fatal.javafx.incomplete"));
|
||||
}
|
||||
}
|
||||
@@ -185,11 +258,11 @@ public final class Main {
|
||||
if (JavaRuntime.CURRENT_VERSION == 24 || JavaRuntime.CURRENT_VERSION == 25) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName("sun.misc.Unsafe");
|
||||
Method trySetMemoryAccessWarned = clazz.getDeclaredMethod("trySetMemoryAccessWarned");
|
||||
trySetMemoryAccessWarned.setAccessible(true);
|
||||
trySetMemoryAccessWarned.invoke(null);
|
||||
boolean ignored = (boolean) MethodHandles.privateLookupIn(clazz, MethodHandles.lookup())
|
||||
.findStatic(clazz, "trySetMemoryAccessWarned", MethodType.methodType(boolean.class))
|
||||
.invokeExact();
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace(System.err);
|
||||
LOG.warning("Failed to enable unsafe memory access", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,70 +270,8 @@ public final class Main {
|
||||
/**
|
||||
* Indicates that a fatal error has occurred, and that the application cannot start.
|
||||
*/
|
||||
static void showErrorAndExit(String message) {
|
||||
System.err.println(message);
|
||||
System.err.println("A fatal error has occurred, forcibly exiting.");
|
||||
|
||||
try {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
new Alert(Alert.AlertType.ERROR, message).showAndWait();
|
||||
exit(1);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
private static void showErrorAndExit(String message) {
|
||||
SwingUtils.showErrorDialog(message);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that potential issues have been detected, and that the application may not function properly (but it can still run).
|
||||
*/
|
||||
static void showWarningAndContinue(String message) {
|
||||
System.err.println(message);
|
||||
System.err.println("Potential issues have been detected.");
|
||||
|
||||
try {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
new Alert(Alert.AlertType.WARNING, message).showAndWait();
|
||||
return;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
SwingUtils.showWarningDialog(message);
|
||||
}
|
||||
|
||||
private static void fixLetsEncrypt() {
|
||||
try {
|
||||
KeyStore defaultKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
Path ksPath = Paths.get(System.getProperty("java.home"), "lib", "security", "cacerts");
|
||||
|
||||
try (InputStream ksStream = Files.newInputStream(ksPath)) {
|
||||
defaultKeyStore.load(ksStream, "changeit".toCharArray());
|
||||
}
|
||||
|
||||
KeyStore letsEncryptKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
try (InputStream letsEncryptFile = Main.class.getResourceAsStream("/assets/lekeystore.jks")) {
|
||||
letsEncryptKeyStore.load(letsEncryptFile, "supersecretpassword".toCharArray());
|
||||
}
|
||||
|
||||
KeyStore merged = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
merged.load(null, new char[0]);
|
||||
for (String alias : Collections.list(letsEncryptKeyStore.aliases()))
|
||||
merged.setCertificateEntry(alias, letsEncryptKeyStore.getCertificate(alias));
|
||||
for (String alias : Collections.list(defaultKeyStore.aliases()))
|
||||
merged.setCertificateEntry(alias, defaultKeyStore.getCertificate(alias));
|
||||
|
||||
TrustManagerFactory instance = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
instance.init(merged);
|
||||
SSLContext tls = SSLContext.getInstance("TLS");
|
||||
tls.init(null, instance.getTrustManagers(), null);
|
||||
HttpsURLConnection.setDefaultSSLSocketFactory(tls.getSocketFactory());
|
||||
LOG.info("Added Lets Encrypt root certificates as additional trust");
|
||||
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException |
|
||||
KeyManagementException e) {
|
||||
LOG.error("Failed to load lets encrypt certificate. Expect problems", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,14 +19,18 @@ 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;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.FileSaver;
|
||||
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -52,8 +56,11 @@ 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;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
@@ -71,23 +78,33 @@ public final class Launcher extends Application {
|
||||
CookieHandler.setDefault(COOKIE_MANAGER);
|
||||
|
||||
LOG.info("JavaFX Version: " + System.getProperty("javafx.runtime.version"));
|
||||
try {
|
||||
Object pipeline = Class.forName("com.sun.prism.GraphicsPipeline").getMethod("getPipeline").invoke(null);
|
||||
LOG.info("Prism pipeline: " + (pipeline == null ? "null" : pipeline.getClass().getName()));
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to get prism pipeline", e);
|
||||
LOG.info("Prism Pipeline: " + FXUtils.GRAPHICS_PIPELINE);
|
||||
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();
|
||||
} catch (SambaException ignored) {
|
||||
Main.showWarningAndContinue(i18n("fatal.samba"));
|
||||
} catch (SambaException e) {
|
||||
showAlert(AlertType.WARNING, i18n("fatal.samba"));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to load config", e);
|
||||
checkConfigInTempDir();
|
||||
checkConfigOwner();
|
||||
Main.showErrorAndExit(i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
|
||||
showAlert(AlertType.ERROR, i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
|
||||
EntryPoint.exit(1);
|
||||
}
|
||||
|
||||
// https://lapcatsoftware.com/articles/app-translocation.html
|
||||
@@ -110,7 +127,7 @@ public final class Launcher extends Application {
|
||||
}
|
||||
|
||||
if (Metadata.HMCL_CURRENT_DIRECTORY.toString().indexOf('=') >= 0) {
|
||||
Main.showWarningAndContinue(i18n("fatal.illegal_char"));
|
||||
showAlert(AlertType.WARNING, i18n("fatal.illegal_char"));
|
||||
}
|
||||
|
||||
// runLater to ensure ConfigHolder.init() finished initialization
|
||||
@@ -129,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);
|
||||
}
|
||||
@@ -173,7 +222,7 @@ public final class Launcher extends Application {
|
||||
private static void checkConfigInTempDir() {
|
||||
if (ConfigHolder.isNewlyCreated() && isConfigInTempDir()
|
||||
&& showAlert(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), ButtonType.YES, ButtonType.NO) == ButtonType.NO) {
|
||||
Main.exit(0);
|
||||
EntryPoint.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +262,7 @@ public final class Launcher extends Application {
|
||||
Clipboard.getSystemClipboard()
|
||||
.setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, command));
|
||||
}
|
||||
Main.exit(1);
|
||||
EntryPoint.exit(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -235,8 +284,8 @@ public final class Launcher extends Application {
|
||||
try {
|
||||
LOG.info("*** " + Metadata.TITLE + " ***");
|
||||
LOG.info("Operating System: " + (OperatingSystem.OS_RELEASE_PRETTY_NAME == null
|
||||
? OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION
|
||||
: OperatingSystem.OS_RELEASE_PRETTY_NAME + " (" + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + ')'));
|
||||
? OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION.getVersion()
|
||||
: OperatingSystem.OS_RELEASE_PRETTY_NAME + " (" + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION.getVersion() + ')'));
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
LOG.info("Processor Identifier: " + System.getenv("PROCESSOR_IDENTIFIER"));
|
||||
}
|
||||
|
||||
@@ -36,14 +36,15 @@ public final class Metadata {
|
||||
|
||||
public static final String NAME = "HMCL";
|
||||
public static final String FULL_NAME = "Hello Minecraft! Launcher";
|
||||
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getManifestAttribute("Implementation-Version", "@develop@"));
|
||||
|
||||
public static final int MINIMUM_REQUIRED_JAVA_VERSION = 8;
|
||||
public static final int MINIMUM_SUPPORTED_JAVA_VERSION = 17;
|
||||
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getAttribute("hmcl.version", "@develop@"));
|
||||
|
||||
public static final String TITLE = NAME + " " + VERSION;
|
||||
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
|
||||
|
||||
public static final int MINIMUM_REQUIRED_JAVA_VERSION = 17;
|
||||
public static final int MINIMUM_SUPPORTED_JAVA_VERSION = 17;
|
||||
public static final int RECOMMENDED_JAVA_VERSION = 21;
|
||||
|
||||
public static final String PUBLISH_URL = "https://hmcl.huangyuhui.net";
|
||||
public static final String ABOUT_URL = PUBLISH_URL + "/about";
|
||||
public static final String DOWNLOAD_URL = PUBLISH_URL + "/download";
|
||||
@@ -55,8 +56,8 @@ public final class Metadata {
|
||||
public static final String EULA_URL = DOCS_URL + "/eula/hmcl.html";
|
||||
public static final String GROUPS_URL = "https://www.bilibili.com/opus/905435541874409529";
|
||||
|
||||
public static final String BUILD_CHANNEL = JarUtils.getManifestAttribute("Build-Channel", "nightly");
|
||||
public static final String GITHUB_SHA = JarUtils.getManifestAttribute("GitHub-SHA", null);
|
||||
public static final String BUILD_CHANNEL = JarUtils.getAttribute("hmcl.version.type", "nightly");
|
||||
public static final String GITHUB_SHA = JarUtils.getAttribute("hmcl.version.hash", null);
|
||||
|
||||
public static final Path CURRENT_DIRECTORY = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize();
|
||||
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
|
||||
@@ -117,7 +118,6 @@ public final class Metadata {
|
||||
supportedArchitectures = EnumSet.of(Architecture.X86_64, Architecture.ARM64);
|
||||
else
|
||||
supportedArchitectures = EnumSet.noneOf(Architecture.class);
|
||||
|
||||
if (supportedArchitectures.contains(Architecture.SYSTEM_ARCH))
|
||||
return String.format("https://docs.hmcl.net/downloads/%s/%s.html",
|
||||
OperatingSystem.CURRENT_OS.getCheckedName(),
|
||||
|
||||
@@ -42,7 +42,7 @@ public class CrashReport {
|
||||
"\n Content: \n " +
|
||||
stackTrace + "\n\n" +
|
||||
"-- System Details --\n" +
|
||||
" Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + "\n" +
|
||||
" Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION.getVersion() + "\n" +
|
||||
" System Architecture: " + Architecture.SYSTEM_ARCH.getDisplayName() + "\n" +
|
||||
" Java Architecture: " + Architecture.CURRENT_ARCH.getDisplayName() + "\n" +
|
||||
" Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor") + "\n" +
|
||||
|
||||
@@ -21,15 +21,17 @@ import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.launch.DefaultLauncher;
|
||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
@@ -58,61 +60,83 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
}
|
||||
|
||||
private void generateOptionsTxt() {
|
||||
File optionsFile = new File(repository.getRunDirectory(version.getId()), "options.txt");
|
||||
File configFolder = new File(repository.getRunDirectory(version.getId()), "config");
|
||||
|
||||
if (optionsFile.exists()) {
|
||||
if (config().isDisableAutoGameOptions())
|
||||
return;
|
||||
}
|
||||
|
||||
if (configFolder.isDirectory()) {
|
||||
if (findFiles(configFolder, "options.txt")) {
|
||||
return;
|
||||
Path runDir = repository.getRunDirectory(version.getId());
|
||||
Path optionsFile = runDir.resolve("options.txt");
|
||||
Path configFolder = runDir.resolve("config");
|
||||
|
||||
if (Files.exists(optionsFile))
|
||||
return;
|
||||
|
||||
if (Files.isDirectory(configFolder)) {
|
||||
try (Stream<Path> stream = Files.walk(configFolder, 2, FileVisitOption.FOLLOW_LINKS)) {
|
||||
if (stream.anyMatch(file -> "options.txt".equals(FileUtils.getName(file))))
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to visit config folder", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!I18n.isUseChinese()) {
|
||||
return;
|
||||
}
|
||||
Locale locale = Locale.getDefault();
|
||||
|
||||
String lang;
|
||||
/*
|
||||
1.0- :没有语言选项,遇到这些版本时不设置
|
||||
1.1 ~ 5 :zh_CN 时正常,zh_cn 时崩溃(最后两位字母必须大写,否则将会 NPE 崩溃)
|
||||
1.6 ~ 10 :zh_CN 时正常,zh_cn 时自动切换为英文
|
||||
1.11 ~ 12:zh_cn 时正常,zh_CN 时虽然显示了中文但语言设置会错误地显示选择英文
|
||||
1.13+ :zh_cn 时正常,zh_CN 时自动切换为英文
|
||||
* 1.0 : No language option, do not set for these versions
|
||||
* 1.1 ~ 1.5 : zh_CN works fine, zh_cn will crash (the last two letters must be uppercase, otherwise it will cause an NPE crash)
|
||||
* 1.6 ~ 1.10 : zh_CN works fine, zh_cn will automatically switch to English
|
||||
* 1.11 ~ 1.12 : zh_cn works fine, zh_CN will display Chinese but the language setting will incorrectly show English as selected
|
||||
* 1.13+ : zh_cn works fine, zh_CN will automatically switch to English
|
||||
*/
|
||||
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(repository.getGameVersion(version));
|
||||
if (gameVersion.compareTo("1.1") < 0) {
|
||||
lang = null;
|
||||
} else if (gameVersion.compareTo("1.11") < 0) {
|
||||
lang = "zh_CN";
|
||||
} else {
|
||||
lang = "zh_cn";
|
||||
}
|
||||
if (gameVersion.compareTo("1.1") < 0)
|
||||
return;
|
||||
|
||||
if (lang != null) {
|
||||
try {
|
||||
FileUtils.writeText(optionsFile, String.format("lang:%s\n", lang));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Unable to generate options.txt", e);
|
||||
}
|
||||
String lang = normalizedLanguageTag(locale, gameVersion);
|
||||
if (lang.isEmpty())
|
||||
return;
|
||||
|
||||
if (gameVersion.compareTo("1.11") >= 0)
|
||||
lang = lang.toLowerCase(Locale.ROOT);
|
||||
|
||||
try {
|
||||
Files.createDirectories(optionsFile.getParent());
|
||||
Files.writeString(optionsFile, String.format("lang:%s\n", lang));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Unable to generate options.txt", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean findFiles(File folder, String fileName) {
|
||||
File[] fs = folder.listFiles();
|
||||
if (fs != null) {
|
||||
for (File f : fs) {
|
||||
if (f.isDirectory())
|
||||
if (f.listFiles((dir, name) -> name.equals(fileName)) != null)
|
||||
return true;
|
||||
if (f.getName().equals(fileName))
|
||||
return true;
|
||||
private static String normalizedLanguageTag(Locale locale, GameVersionNumber gameVersion) {
|
||||
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";
|
||||
case "uk" -> "uk_UA";
|
||||
case "zh" -> {
|
||||
if ("lzh".equals(locale.getLanguage()) && gameVersion.compareTo("1.16") >= 0)
|
||||
yield "lzh";
|
||||
|
||||
String script = LocaleUtils.getScript(locale);
|
||||
if ("Hant".equals(script)) {
|
||||
if ((region.equals("HK") || region.equals("MO") && gameVersion.compareTo("1.16") >= 0))
|
||||
yield "zh_HK";
|
||||
yield "zh_TW";
|
||||
}
|
||||
yield "zh_CN";
|
||||
}
|
||||
}
|
||||
return false;
|
||||
case "en" -> {
|
||||
if ("Qabs".equals(LocaleUtils.getScript(locale)) && gameVersion.compareTo("1.16") >= 0) {
|
||||
yield "en_UD";
|
||||
}
|
||||
|
||||
yield "";
|
||||
}
|
||||
default -> "";
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -122,7 +146,7 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void makeLaunchScript(File scriptFile) throws IOException {
|
||||
public void makeLaunchScript(Path scriptFile) throws IOException {
|
||||
generateOptionsTxt();
|
||||
super.makeLaunchScript(scriptFile);
|
||||
}
|
||||
|
||||
@@ -25,28 +25,29 @@ 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.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
@@ -54,10 +55,10 @@ 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 class HMCLGameRepository extends DefaultGameRepository {
|
||||
public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
private final Profile profile;
|
||||
|
||||
// local version settings
|
||||
@@ -66,7 +67,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
|
||||
public final EventManager<Event> onVersionIconChanged = new EventManager<>();
|
||||
|
||||
public HMCLGameRepository(Profile profile, File baseDirectory) {
|
||||
public HMCLGameRepository(Profile profile, Path baseDirectory) {
|
||||
super(baseDirectory);
|
||||
this.profile = profile;
|
||||
}
|
||||
@@ -85,18 +86,20 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getRunDirectory(String id) {
|
||||
public Path getRunDirectory(String id) {
|
||||
switch (getGameDirectoryType(id)) {
|
||||
case VERSION_FOLDER:
|
||||
return getVersionRoot(id);
|
||||
case ROOT_FOLDER:
|
||||
return super.getRunDirectory(id);
|
||||
case CUSTOM:
|
||||
File dir = new File(getVersionSetting(id).getGameDir());
|
||||
if (!FileUtils.isValidPath(dir)) return getVersionRoot(id);
|
||||
return dir;
|
||||
try {
|
||||
return Path.of(getVersionSetting(id).getGameDir());
|
||||
} catch (InvalidPathException ignored) {
|
||||
return getVersionRoot(id);
|
||||
}
|
||||
default:
|
||||
throw new Error();
|
||||
throw new AssertionError("Unreachable");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,25 +122,24 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
});
|
||||
|
||||
try {
|
||||
File file = new File(getBaseDirectory(), "launcher_profiles.json");
|
||||
if (!file.exists() && !versions.isEmpty())
|
||||
FileUtils.writeText(file, PROFILE);
|
||||
Path file = getBaseDirectory().resolve("launcher_profiles.json");
|
||||
if (!Files.exists(file) && !versions.isEmpty()) {
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.writeString(file, PROFILE);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
LOG.warning("Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex);
|
||||
}
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/938
|
||||
System.gc();
|
||||
}
|
||||
|
||||
public void changeDirectory(File newDirectory) {
|
||||
public void changeDirectory(Path newDirectory) {
|
||||
setBaseDirectory(newDirectory);
|
||||
refreshVersionsAsync().start();
|
||||
}
|
||||
|
||||
private void clean(File directory) throws IOException {
|
||||
FileUtils.deleteDirectory(new File(directory, "crash-reports"));
|
||||
FileUtils.deleteDirectory(new File(directory, "logs"));
|
||||
private void clean(Path directory) throws IOException {
|
||||
FileUtils.deleteDirectory(directory.resolve("crash-reports"));
|
||||
FileUtils.deleteDirectory(directory.resolve("logs"));
|
||||
}
|
||||
|
||||
public void clean(String id) throws IOException {
|
||||
@@ -146,8 +148,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
|
||||
public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException {
|
||||
Path srcDir = getVersionRoot(srcId).toPath();
|
||||
Path dstDir = getVersionRoot(dstId).toPath();
|
||||
Path srcDir = getVersionRoot(srcId);
|
||||
Path dstDir = getVersionRoot(dstId);
|
||||
|
||||
Version fromVersion = getVersion(srcId);
|
||||
|
||||
@@ -158,6 +160,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
blackList.add("saves");
|
||||
|
||||
if (Files.exists(dstDir)) throw new IOException("Version exists");
|
||||
|
||||
Files.createDirectories(dstDir);
|
||||
FileUtils.copyDirectory(srcDir, dstDir, path -> Modpack.acceptFile(path, blackList, null));
|
||||
|
||||
Path fromJson = srcDir.resolve(srcId + ".json");
|
||||
@@ -170,7 +174,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
Files.copy(fromJson, toJson);
|
||||
|
||||
FileUtils.writeText(toJson.toFile(), JsonUtils.GSON.toJson(fromVersion.setId(dstId)));
|
||||
JsonUtils.writeToJsonFile(toJson, fromVersion.setId(dstId));
|
||||
|
||||
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
|
||||
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
|
||||
@@ -179,22 +183,22 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
VersionSetting newVersionSetting = initLocalVersionSetting(dstId, oldVersionSetting);
|
||||
saveVersionSetting(dstId);
|
||||
|
||||
File srcGameDir = getRunDirectory(srcId);
|
||||
File dstGameDir = getRunDirectory(dstId);
|
||||
Path srcGameDir = getRunDirectory(srcId);
|
||||
Path dstGameDir = getRunDirectory(dstId);
|
||||
|
||||
if (originalGameDirType != GameDirectoryType.VERSION_FOLDER)
|
||||
FileUtils.copyDirectory(srcGameDir.toPath(), dstGameDir.toPath(), path -> Modpack.acceptFile(path, blackList, null));
|
||||
FileUtils.copyDirectory(srcGameDir, dstGameDir, path -> Modpack.acceptFile(path, blackList, null));
|
||||
}
|
||||
|
||||
private File getLocalVersionSettingFile(String id) {
|
||||
return new File(getVersionRoot(id), "hmclversion.cfg");
|
||||
private Path getLocalVersionSettingFile(String id) {
|
||||
return getVersionRoot(id).resolve("hmclversion.cfg");
|
||||
}
|
||||
|
||||
private void loadLocalVersionSetting(String id) {
|
||||
File file = getLocalVersionSettingFile(id);
|
||||
if (file.exists())
|
||||
Path file = getLocalVersionSettingFile(id);
|
||||
if (Files.exists(file))
|
||||
try {
|
||||
VersionSetting versionSetting = GSON.fromJson(FileUtils.readText(file), VersionSetting.class);
|
||||
VersionSetting versionSetting = JsonUtils.fromJsonFile(file, VersionSetting.class);
|
||||
initLocalVersionSetting(id, versionSetting);
|
||||
} catch (Exception ex) {
|
||||
// If [JsonParseException], [IOException] or [NullPointerException] happens, the json file is malformed and needed to be recreated.
|
||||
@@ -257,12 +261,12 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
return vs;
|
||||
}
|
||||
|
||||
public Optional<File> getVersionIconFile(String id) {
|
||||
File root = getVersionRoot(id);
|
||||
public Optional<Path> getVersionIconFile(String id) {
|
||||
Path root = getVersionRoot(id);
|
||||
|
||||
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
|
||||
File file = new File(root, "icon." + extension);
|
||||
if (file.exists()) {
|
||||
Path file = root.resolve("icon." + extension);
|
||||
if (Files.exists(file)) {
|
||||
return Optional.of(file);
|
||||
}
|
||||
}
|
||||
@@ -270,7 +274,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public void setVersionIconFile(String id, File iconFile) throws IOException {
|
||||
public void setVersionIconFile(String id, Path iconFile) throws IOException {
|
||||
String ext = FileUtils.getExtension(iconFile).toLowerCase(Locale.ROOT);
|
||||
if (!FXUtils.IMAGE_EXTENSIONS.contains(ext)) {
|
||||
throw new IllegalArgumentException("Unsupported icon file: " + ext);
|
||||
@@ -278,13 +282,18 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
|
||||
deleteIconFile(id);
|
||||
|
||||
FileUtils.copyFile(iconFile, new File(getVersionRoot(id), "icon." + ext));
|
||||
FileUtils.copyFile(iconFile, getVersionRoot(id).resolve("icon." + ext));
|
||||
}
|
||||
|
||||
public void deleteIconFile(String id) {
|
||||
File root = getVersionRoot(id);
|
||||
Path root = getVersionRoot(id);
|
||||
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
|
||||
new File(root, "icon." + extension).delete();
|
||||
Path file = root.resolve("icon." + extension);
|
||||
try {
|
||||
Files.deleteIfExists(file);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to delete icon file: " + file, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,10 +306,10 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
|
||||
if (iconType == VersionIconType.DEFAULT) {
|
||||
Version version = getVersion(id).resolve(this);
|
||||
Optional<File> iconFile = getVersionIconFile(id);
|
||||
Optional<Path> iconFile = getVersionIconFile(id);
|
||||
if (iconFile.isPresent()) {
|
||||
try {
|
||||
return FXUtils.loadImage(iconFile.get().toPath());
|
||||
return FXUtils.loadImage(iconFile.get());
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load version icon of " + id, e);
|
||||
}
|
||||
@@ -310,21 +319,32 @@ public 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.FORGE))
|
||||
return VersionIconType.FORGE.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.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.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();
|
||||
}
|
||||
@@ -333,7 +353,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
public void saveVersionSetting(String id) {
|
||||
if (!localVersionSettings.containsKey(id))
|
||||
return;
|
||||
Path file = getLocalVersionSettingFile(id).toPath().toAbsolutePath().normalize();
|
||||
Path file = getLocalVersionSettingFile(id).toAbsolutePath().normalize();
|
||||
try {
|
||||
Files.createDirectories(file.getParent());
|
||||
} catch (IOException e) {
|
||||
@@ -367,7 +387,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
vs.setUsesGlobal(true);
|
||||
}
|
||||
|
||||
public LaunchOptions getLaunchOptions(String version, JavaRuntime javaVersion, File 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()
|
||||
@@ -378,7 +398,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
.setProfileName(Metadata.TITLE)
|
||||
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
|
||||
.setOverrideJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
|
||||
.setMaxMemory(vs.isNoJVMArgs() && vs.isAutoMemory() ? null : (int)(getAllocatedMemory(
|
||||
.setMaxMemory(vs.isNoJVMArgs() && vs.isAutoMemory() ? null : (int) (getAllocatedMemory(
|
||||
vs.getMaxMemory() * 1024L * 1024L,
|
||||
SystemInfo.getPhysicalMemoryStatus().getAvailable(),
|
||||
vs.isAutoMemory()
|
||||
@@ -398,21 +418,26 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
.setWidth(vs.getWidth())
|
||||
.setHeight(vs.getHeight())
|
||||
.setFullscreen(vs.isFullscreen())
|
||||
.setServerIp(vs.getServerIp())
|
||||
.setWrapper(vs.getWrapper())
|
||||
.setPreLaunchCommand(vs.getPreLaunchCommand())
|
||||
.setPostExitCommand(vs.getPostExitCommand())
|
||||
.setNoGeneratedJVMArgs(vs.isNoJVMArgs())
|
||||
.setNoGeneratedOptimizingJVMArgs(vs.isNoOptimizingJVMArgs())
|
||||
.setNativesDirType(vs.getNativesDirType())
|
||||
.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 (StringUtils.isNotBlank(vs.getServerIp())) {
|
||||
builder.setQuickPlayOption(new QuickPlayOption.MultiPlayer(vs.getServerIp()));
|
||||
}
|
||||
|
||||
if (config().hasProxy()) {
|
||||
builder.setProxyType(config().getProxyType());
|
||||
builder.setProxyHost(config().getProxyHost());
|
||||
@@ -424,27 +449,27 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
}
|
||||
|
||||
File json = getModpackConfiguration(version);
|
||||
if (json.exists()) {
|
||||
Path json = getModpackConfiguration(version);
|
||||
if (Files.exists(json)) {
|
||||
try {
|
||||
String jsonText = FileUtils.readText(json);
|
||||
String jsonText = Files.readString(json);
|
||||
ModpackConfiguration<?> modpackConfiguration = JsonUtils.GSON.fromJson(jsonText, ModpackConfiguration.class);
|
||||
ModpackProvider provider = ModpackHelper.getProviderByType(modpackConfiguration.getType());
|
||||
if (provider != null) provider.injectLaunchOptions(jsonText, builder);
|
||||
} catch (IOException | JsonParseException e) {
|
||||
e.printStackTrace();
|
||||
LOG.warning("Failed to parse modpack configuration file " + json, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (vs.isAutoMemory() && builder.getJavaArguments().stream().anyMatch(it -> it.startsWith("-Xmx")))
|
||||
builder.setMaxMemory(null);
|
||||
|
||||
return builder.create();
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getModpackConfiguration(String version) {
|
||||
return new File(getVersionRoot(version), "modpack.cfg");
|
||||
public Path getModpackConfiguration(String version) {
|
||||
return getVersionRoot(version).resolve("modpack.cfg");
|
||||
}
|
||||
|
||||
public void markVersionAsModpack(String id) {
|
||||
@@ -457,16 +482,24 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
|
||||
public void markVersionLaunchedAbnormally(String id) {
|
||||
try {
|
||||
Files.createFile(getVersionRoot(id).toPath().resolve(".abnormal"));
|
||||
Files.createFile(getVersionRoot(id).resolve(".abnormal"));
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean unmarkVersionLaunchedAbnormally(String id) {
|
||||
File file = new File(getVersionRoot(id), ".abnormal");
|
||||
boolean result = file.isFile();
|
||||
file.delete();
|
||||
return result;
|
||||
Path file = getVersionRoot(id).resolve(".abnormal");
|
||||
if (Files.isRegularFile(file)) {
|
||||
try {
|
||||
Files.delete(file);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to delete abnormal mark file: " + file, e);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
@@ -488,7 +521,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
FORBIDDEN_VERSION_IDS.contains(id.toLowerCase(Locale.ROOT)))
|
||||
return false;
|
||||
|
||||
return OperatingSystem.isNameValid(id);
|
||||
return FileUtils.isNameValid(id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,16 +28,16 @@ import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
private final File zipFile;
|
||||
private final Path zipFile;
|
||||
private final String name;
|
||||
private final HMCLGameRepository repository;
|
||||
private final DefaultDependencyManager dependency;
|
||||
@@ -45,16 +45,16 @@ public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
private final List<Task<?>> dependencies = new ArrayList<>(1);
|
||||
private final List<Task<?>> dependents = new ArrayList<>(4);
|
||||
|
||||
public HMCLModpackInstallTask(Profile profile, File zipFile, Modpack modpack, String name) {
|
||||
public HMCLModpackInstallTask(Profile profile, Path zipFile, Modpack modpack, String name) {
|
||||
dependency = profile.getDependency();
|
||||
repository = profile.getRepository();
|
||||
this.zipFile = zipFile;
|
||||
this.name = name;
|
||||
this.modpack = modpack;
|
||||
|
||||
File run = repository.getRunDirectory(name);
|
||||
File json = repository.getModpackConfiguration(name);
|
||||
if (repository.hasVersion(name) && !json.exists())
|
||||
Path run = repository.getRunDirectory(name);
|
||||
Path json = repository.getModpackConfiguration(name);
|
||||
if (repository.hasVersion(name) && Files.notExists(json))
|
||||
throw new IllegalArgumentException("Version " + name + " already exists");
|
||||
|
||||
dependents.add(dependency.gameBuilder().name(name).gameVersion(modpack.getGameVersion()).buildAsync());
|
||||
@@ -65,8 +65,8 @@ public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
|
||||
ModpackConfiguration<Modpack> config = null;
|
||||
try {
|
||||
if (json.exists()) {
|
||||
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), ModpackConfiguration.typeOf(Modpack.class));
|
||||
if (Files.exists(json)) {
|
||||
config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(Modpack.class));
|
||||
|
||||
if (!HMCLModpackProvider.INSTANCE.getName().equals(config.getType()))
|
||||
throw new IllegalArgumentException("Version " + name + " is not a HMCL modpack. Cannot update this version.");
|
||||
|
||||
@@ -30,7 +30,6 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
@@ -49,15 +48,14 @@ public final class HMCLModpackProvider implements ModpackProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, File zipFile, Modpack modpack) throws MismatchedModpackTypeException {
|
||||
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
|
||||
if (!(modpack.getManifest() instanceof HMCLModpackManifest))
|
||||
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
|
||||
|
||||
if (!(dependencyManager.getGameRepository() instanceof HMCLGameRepository)) {
|
||||
if (!(dependencyManager.getGameRepository() instanceof HMCLGameRepository repository)) {
|
||||
throw new IllegalArgumentException("HMCLModpackProvider requires HMCLGameRepository");
|
||||
}
|
||||
|
||||
HMCLGameRepository repository = (HMCLGameRepository) dependencyManager.getGameRepository();
|
||||
Profile profile = repository.getProfile();
|
||||
|
||||
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new HMCLModpackInstallTask(profile, zipFile, modpack, name));
|
||||
@@ -79,9 +77,9 @@ public final class HMCLModpackProvider implements ModpackProvider {
|
||||
return manifest;
|
||||
}
|
||||
|
||||
private static class HMCLModpack extends Modpack {
|
||||
private final static class HMCLModpack extends Modpack {
|
||||
@Override
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name) {
|
||||
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;
|
||||
@@ -49,17 +50,18 @@ import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.net.URI;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
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;
|
||||
@@ -76,10 +78,12 @@ public final class LauncherHelper {
|
||||
private final Profile profile;
|
||||
private Account account;
|
||||
private final String selectedVersion;
|
||||
private File scriptFile;
|
||||
private Path scriptFile;
|
||||
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);
|
||||
@@ -110,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();
|
||||
|
||||
@@ -119,9 +131,8 @@ public final class LauncherHelper {
|
||||
launch0();
|
||||
}
|
||||
|
||||
public void makeLaunchScript(File scriptFile) {
|
||||
public void makeLaunchScript(Path scriptFile) {
|
||||
this.scriptFile = Objects.requireNonNull(scriptFile);
|
||||
|
||||
launch();
|
||||
}
|
||||
|
||||
@@ -160,19 +171,17 @@ public final class LauncherHelper {
|
||||
}),
|
||||
Task.composeAsync(() -> {
|
||||
Renderer renderer = setting.getRenderer();
|
||||
if (renderer != Renderer.DEFAULT
|
||||
&& OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
|
||||
&& OperatingSystem.WINDOWS_VERSION != null) {
|
||||
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.WINDOWS_VERSION);
|
||||
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.SYSTEM_VERSION);
|
||||
if (lib == null)
|
||||
return null;
|
||||
File file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
|
||||
if (file.getAbsolutePath().indexOf('=') >= 0) {
|
||||
Path file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
|
||||
if (file.toAbsolutePath().toString().indexOf('=') >= 0) {
|
||||
LOG.warning("Invalid character '=' in the libraries directory path, unable to attach software renderer loader");
|
||||
return null;
|
||||
}
|
||||
|
||||
String agent = file.getAbsolutePath() + "=" + renderer.name().toLowerCase(Locale.ROOT);
|
||||
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.name().toLowerCase(Locale.ROOT);
|
||||
|
||||
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
|
||||
return new LibraryDownloadTask(dependencyManager, file, lib)
|
||||
@@ -190,10 +199,17 @@ 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.getModManager(selectedVersion).getModsDirectory(), 10));
|
||||
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModsDirectory(selectedVersion), 10));
|
||||
|
||||
return new HMCLGameLauncher(
|
||||
repository,
|
||||
@@ -226,7 +242,7 @@ public final class LauncherHelper {
|
||||
} else {
|
||||
runLater(() -> {
|
||||
launchingStepsPane.fireEvent(new DialogCloseEvent());
|
||||
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
|
||||
Controllers.dialog(i18n("version.launch_script.success", FileUtils.getAbsolutePath(scriptFile)));
|
||||
});
|
||||
}
|
||||
}).withFakeProgress(
|
||||
@@ -269,27 +285,27 @@ public final class LauncherHelper {
|
||||
if (ex.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException rce = (ResponseCodeException) ex.getCause();
|
||||
int responseCode = rce.getResponseCode();
|
||||
URL url = rce.getUrl();
|
||||
String uri = rce.getUri();
|
||||
if (responseCode == 404)
|
||||
message += i18n("download.code.404", url);
|
||||
message += i18n("download.code.404", uri);
|
||||
else
|
||||
message += i18n("download.failed", url, responseCode);
|
||||
message += i18n("download.failed", uri, responseCode);
|
||||
} else {
|
||||
message += StringUtils.getStackTrace(ex.getCause());
|
||||
}
|
||||
} else if (ex instanceof DownloadException) {
|
||||
URL url = ((DownloadException) ex).getUrl();
|
||||
URI uri = ((DownloadException) ex).getUri();
|
||||
if (ex.getCause() instanceof SocketTimeoutException) {
|
||||
message = i18n("install.failed.downloading.timeout", url);
|
||||
message = i18n("install.failed.downloading.timeout", uri);
|
||||
} else if (ex.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException responseCodeException = (ResponseCodeException) ex.getCause();
|
||||
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
|
||||
message = i18n("download.code." + responseCodeException.getResponseCode(), url);
|
||||
message = i18n("download.code." + responseCodeException.getResponseCode(), uri);
|
||||
} else {
|
||||
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
|
||||
message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
|
||||
}
|
||||
} else {
|
||||
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
|
||||
message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
|
||||
}
|
||||
} else if (ex instanceof GameAssetIndexDownloadTask.GameAssetIndexMalformedException) {
|
||||
message = i18n("assets.index.malformed");
|
||||
@@ -300,11 +316,11 @@ public final class LauncherHelper {
|
||||
} else if (ex instanceof ResponseCodeException) {
|
||||
ResponseCodeException rce = (ResponseCodeException) ex;
|
||||
int responseCode = rce.getResponseCode();
|
||||
URL url = rce.getUrl();
|
||||
String uri = rce.getUri();
|
||||
if (responseCode == 404)
|
||||
message = i18n("download.code.404", url);
|
||||
message = i18n("download.code.404", uri);
|
||||
else
|
||||
message = i18n("download.failed", url, responseCode);
|
||||
message = i18n("download.failed", uri, responseCode);
|
||||
} else if (ex instanceof CommandTooLongException) {
|
||||
message = i18n("launch.failed.command_too_long");
|
||||
} else if (ex instanceof ExecutionPolicyLimitException) {
|
||||
@@ -452,7 +468,9 @@ public final class LauncherHelper {
|
||||
return result;
|
||||
} else {
|
||||
GameJavaVersion gameJavaVersion;
|
||||
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
|
||||
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.CLEANROOM_JAVA_21))
|
||||
gameJavaVersion = GameJavaVersion.JAVA_21;
|
||||
else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
|
||||
gameJavaVersion = version.getJavaVersion();
|
||||
else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA))
|
||||
gameJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
|
||||
@@ -542,6 +560,9 @@ 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"));
|
||||
break;
|
||||
case VANILLA_JAVA_8_51:
|
||||
suggestions.add(i18n("launch.advice.java8_51_1_13"));
|
||||
break;
|
||||
@@ -636,10 +657,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);
|
||||
|
||||
@@ -704,6 +728,7 @@ public final class LauncherHelper {
|
||||
*/
|
||||
private final class HMCLProcessListener implements ProcessListener {
|
||||
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private final HMCLGameRepository repository;
|
||||
private final Version version;
|
||||
private final LaunchOptions launchOptions;
|
||||
@@ -847,21 +872,27 @@ public final class LauncherHelper {
|
||||
level = Lang.requireNonNullElse(Log4jLevel.guessLevel(log), Log4jLevel.INFO);
|
||||
logBuffer.add(new Log(log, level));
|
||||
} else {
|
||||
synchronized (this) {
|
||||
lock.lock();
|
||||
try {
|
||||
logs.addLast(new Log(log, level));
|
||||
if (logs.size() > Log.getLogLines())
|
||||
logs.removeFirst();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
if (!lwjgl) {
|
||||
String lowerCaseLog = log.toLowerCase(Locale.ROOT);
|
||||
if (!detectWindow || lowerCaseLog.contains("lwjgl version") || lowerCaseLog.contains("lwjgl openal")) {
|
||||
synchronized (this) {
|
||||
lock.lock();
|
||||
try {
|
||||
if (!lwjgl) {
|
||||
lwjgl = true;
|
||||
finishLaunch();
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -886,9 +917,12 @@ public final class LauncherHelper {
|
||||
|
||||
// Game crashed before opening the game window.
|
||||
if (!lwjgl) {
|
||||
synchronized (this) {
|
||||
lock.lock();
|
||||
try {
|
||||
if (!lwjgl)
|
||||
finishLaunch();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -902,7 +936,15 @@ public final class LauncherHelper {
|
||||
|
||||
}
|
||||
|
||||
public static final Queue<WeakReference<ManagedProcess>> PROCESSES = new ConcurrentLinkedQueue<>();
|
||||
private static final Queue<WeakReference<ManagedProcess>> PROCESSES = new ConcurrentLinkedQueue<>();
|
||||
|
||||
public static int countMangedProcesses() {
|
||||
PROCESSES.removeIf(it -> {
|
||||
ManagedProcess process = it.get();
|
||||
return process == null || !process.isRunning();
|
||||
});
|
||||
return PROCESSES.size();
|
||||
}
|
||||
|
||||
public static void stopManagedProcesses() {
|
||||
while (!PROCESSES.isEmpty())
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
@@ -41,9 +42,9 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
|
||||
protected abstract SortType getBackedRemoteModRepositorySortOrder();
|
||||
|
||||
@Override
|
||||
public SearchResult search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||
if (!StringUtils.containsChinese(searchFilter)) {
|
||||
return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
|
||||
return getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
|
||||
}
|
||||
|
||||
Set<String> englishSearchFiltersSet = new HashSet<>(INITIAL_CAPACITY);
|
||||
@@ -65,7 +66,7 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
|
||||
RemoteMod[] searchResultArray = new RemoteMod[pageSize];
|
||||
int totalPages, chineseIndex = 0, englishIndex = pageSize - 1;
|
||||
{
|
||||
SearchResult searchResult = getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, String.join(" ", englishSearchFiltersSet), getBackedRemoteModRepositorySortOrder(), sortOrder);
|
||||
SearchResult searchResult = getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, String.join(" ", englishSearchFiltersSet), getBackedRemoteModRepositorySortOrder(), sortOrder);
|
||||
for (Iterator<RemoteMod> iterator = searchResult.getUnsortedResults().iterator(); iterator.hasNext(); ) {
|
||||
if (chineseIndex > englishIndex) {
|
||||
LOG.warning("Too many search results! Are the backed remote mod repository broken? Or are the API broken?");
|
||||
|
||||
@@ -44,8 +44,8 @@ public final class LogExporter {
|
||||
}
|
||||
|
||||
public static CompletableFuture<Void> exportLogs(Path zipFile, DefaultGameRepository gameRepository, String versionId, String logs, String launchScript) {
|
||||
Path runDirectory = gameRepository.getRunDirectory(versionId).toPath();
|
||||
Path baseDirectory = gameRepository.getBaseDirectory().toPath();
|
||||
Path runDirectory = gameRepository.getRunDirectory(versionId);
|
||||
Path baseDirectory = gameRepository.getBaseDirectory();
|
||||
List<String> versions = new ArrayList<>();
|
||||
|
||||
String currentVersionId = versionId;
|
||||
|
||||
@@ -19,11 +19,13 @@ package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import kala.compress.archivers.zip.ZipArchiveReader;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.mod.*;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseModpackProvider;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackProvider;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackProvider;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCComponents;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackProvider;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
|
||||
@@ -42,7 +44,6 @@ import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
@@ -50,10 +51,7 @@ import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
@@ -61,7 +59,8 @@ import static org.jackhuang.hmcl.util.Lang.toIterable;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public final class ModpackHelper {
|
||||
private ModpackHelper() {}
|
||||
private ModpackHelper() {
|
||||
}
|
||||
|
||||
private static final Map<String, ModpackProvider> providers = mapOf(
|
||||
pair(CurseModpackProvider.INSTANCE.getName(), CurseModpackProvider.INSTANCE),
|
||||
@@ -72,12 +71,16 @@ public final class ModpackHelper {
|
||||
pair(HMCLModpackProvider.INSTANCE.getName(), HMCLModpackProvider.INSTANCE)
|
||||
);
|
||||
|
||||
static {
|
||||
MultiMCComponents.setImplementation(Metadata.FULL_TITLE);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static ModpackProvider getProviderByType(String type) {
|
||||
return providers.get(type);
|
||||
}
|
||||
|
||||
public static boolean isFileModpackByExtension(File file) {
|
||||
public static boolean isFileModpackByExtension(Path file) {
|
||||
String ext = FileUtils.getExtension(file);
|
||||
return "zip".equals(ext) || "mrpack".equals(ext);
|
||||
}
|
||||
@@ -135,15 +138,12 @@ public final class ModpackHelper {
|
||||
(path.getFileName() == null || ".minecraft".equals(FileUtils.getName(path)));
|
||||
}
|
||||
|
||||
public static ModpackConfiguration<?> readModpackConfiguration(File file) throws IOException {
|
||||
if (!file.exists())
|
||||
throw new FileNotFoundException(file.getPath());
|
||||
else
|
||||
try {
|
||||
return JsonUtils.GSON.fromJson(FileUtils.readText(file), ModpackConfiguration.class);
|
||||
} catch (JsonParseException e) {
|
||||
throw new IOException("Malformed modpack configuration");
|
||||
}
|
||||
public static ModpackConfiguration<?> readModpackConfiguration(Path file) throws IOException {
|
||||
try {
|
||||
return JsonUtils.fromJsonFile(file, ModpackConfiguration.class);
|
||||
} catch (JsonParseException e) {
|
||||
throw new IOException("Malformed modpack configuration");
|
||||
}
|
||||
}
|
||||
|
||||
public static Task<?> getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) {
|
||||
@@ -174,21 +174,21 @@ public final class ModpackHelper {
|
||||
return Files.exists(Paths.get("externalgames").resolve(name));
|
||||
}
|
||||
|
||||
public static Task<?> getInstallManuallyCreatedModpackTask(Profile profile, File zipFile, String name, Charset charset) {
|
||||
public static Task<?> getInstallManuallyCreatedModpackTask(Profile profile, Path zipFile, String name, Charset charset) {
|
||||
if (isExternalGameNameConflicts(name)) {
|
||||
throw new IllegalArgumentException("name existing");
|
||||
}
|
||||
|
||||
return new ManuallyCreatedModpackInstallTask(profile, zipFile.toPath(), charset, name)
|
||||
return new ManuallyCreatedModpackInstallTask(profile, zipFile, charset, name)
|
||||
.thenAcceptAsync(Schedulers.javafx(), location -> {
|
||||
Profile newProfile = new Profile(name, location.toFile());
|
||||
Profile newProfile = new Profile(name, location);
|
||||
newProfile.setUseRelativePath(true);
|
||||
Profiles.getProfiles().add(newProfile);
|
||||
Profiles.setSelectedProfile(newProfile);
|
||||
});
|
||||
}
|
||||
|
||||
public static Task<?> getInstallTask(Profile profile, File zipFile, String name, Modpack modpack) {
|
||||
public static Task<?> getInstallTask(Profile profile, Path zipFile, String name, Modpack modpack) {
|
||||
profile.getRepository().markVersionAsModpack(name);
|
||||
|
||||
ExceptionalRunnable<?> success = () -> {
|
||||
@@ -210,14 +210,17 @@ public final class ModpackHelper {
|
||||
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
|
||||
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
|
||||
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
|
||||
else if (modpack.getManifest() instanceof McbbsModpackManifest)
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name));
|
||||
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name))
|
||||
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
|
||||
else
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||
.whenComplete(Schedulers.javafx(), success, failure);
|
||||
.whenComplete(Schedulers.javafx(), success, failure)
|
||||
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
|
||||
}
|
||||
|
||||
public static Task<Void> getUpdateTask(Profile profile, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
|
||||
@@ -230,13 +233,17 @@ public final class ModpackHelper {
|
||||
}
|
||||
}
|
||||
|
||||
public static Task<?> getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(zipFile.toPath(), charset);
|
||||
public static Task<?> getUpdateTask(Profile profile, Path zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(zipFile, charset);
|
||||
ModpackProvider provider = getProviderByType(configuration.getType());
|
||||
if (provider == null) {
|
||||
throw new UnsupportedModpackException();
|
||||
}
|
||||
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack);
|
||||
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack)
|
||||
.thenComposeAsync(() -> createMultiMCPostUpdateTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
|
||||
else
|
||||
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack);
|
||||
}
|
||||
|
||||
public static void toVersionSetting(MultiMCInstanceConfiguration c, VersionSetting vs) {
|
||||
@@ -276,6 +283,24 @@ public final class ModpackHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyCommandAndJvmSettings(MultiMCInstanceConfiguration c, VersionSetting vs) {
|
||||
if (c.isOverrideCommands()) {
|
||||
vs.setWrapper(Lang.nonNull(c.getWrapperCommand(), ""));
|
||||
vs.setPreLaunchCommand(Lang.nonNull(c.getPreLaunchCommand(), ""));
|
||||
}
|
||||
|
||||
if (c.isOverrideJavaArgs()) {
|
||||
vs.setJavaArgs(Lang.nonNull(c.getJvmArgs(), ""));
|
||||
}
|
||||
}
|
||||
|
||||
private static Task<Void> createMultiMCPostUpdateTask(Profile profile, MultiMCInstanceConfiguration manifest, String version) {
|
||||
return Task.runAsync(Schedulers.javafx(), () -> {
|
||||
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
|
||||
ModpackHelper.applyCommandAndJvmSettings(manifest, vs);
|
||||
});
|
||||
}
|
||||
|
||||
private static Task<Void> createMultiMCPostInstallTask(Profile profile, MultiMCInstanceConfiguration manifest, String version) {
|
||||
return Task.runAsync(Schedulers.javafx(), () -> {
|
||||
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
|
||||
|
||||
@@ -160,13 +160,13 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
@Override
|
||||
public String getClientId() {
|
||||
return System.getProperty("hmcl.microsoft.auth.id",
|
||||
JarUtils.getManifestAttribute("Microsoft-Auth-Id", ""));
|
||||
JarUtils.getAttribute("hmcl.microsoft.auth.id", ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientSecret() {
|
||||
return System.getProperty("hmcl.microsoft.auth.secret",
|
||||
JarUtils.getManifestAttribute("Microsoft-Auth-Secret", ""));
|
||||
JarUtils.getAttribute("hmcl.microsoft.auth.secret", ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -26,8 +26,6 @@ import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.canvas.GraphicsContext;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelReader;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
@@ -45,7 +43,6 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
@@ -110,7 +107,7 @@ public final class TexturesLoader {
|
||||
if (!Files.isRegularFile(file)) {
|
||||
// download it
|
||||
try {
|
||||
new FileDownloadTask(new URL(texture.getUrl()), file.toFile()).run();
|
||||
new FileDownloadTask(texture.getUrl(), file).run();
|
||||
LOG.info("Texture downloaded: " + texture.getUrl());
|
||||
} catch (Exception e) {
|
||||
if (Files.isRegularFile(file)) {
|
||||
@@ -265,15 +262,8 @@ public final class TexturesLoader {
|
||||
int scale = (int) skin.getWidth() / 64;
|
||||
int faceOffset = (int) Math.round(size / 18.0);
|
||||
|
||||
try {
|
||||
g.setImageSmoothing(false);
|
||||
drawAvatar(g, skin, size, scale, faceOffset);
|
||||
} catch (NoSuchMethodError ignored) {
|
||||
// Earlier JavaFX did not support GraphicsContext::setImageSmoothing
|
||||
// In order to prevent the blurring caused by bilinear interpolation,
|
||||
// we use the self-implemented nearest neighbor interpolation to scale
|
||||
drawAvatarSlow(g, skin, size, scale, faceOffset);
|
||||
}
|
||||
g.setImageSmoothing(false);
|
||||
drawAvatar(g, skin, size, scale, faceOffset);
|
||||
}
|
||||
|
||||
private static void drawAvatar(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
|
||||
@@ -285,37 +275,6 @@ public final class TexturesLoader {
|
||||
0, 0, size, size);
|
||||
}
|
||||
|
||||
private static void drawAvatarSlow(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
|
||||
PixelReader reader = skin.getPixelReader();
|
||||
PixelWriter writer = g.getPixelWriter();
|
||||
drawImage(writer, reader,
|
||||
8 * scale, 8 * scale, 8 * scale, 8 * scale,
|
||||
faceOffset, faceOffset, size - 2 * faceOffset, size - 2 * faceOffset);
|
||||
drawImage(writer, reader,
|
||||
40 * scale, 8 * scale, 8 * scale, 8 * scale,
|
||||
0, 0, size, size);
|
||||
}
|
||||
|
||||
/*
|
||||
* Scale and draw image using nearest-neighbor interpolation
|
||||
*/
|
||||
private static void drawImage(PixelWriter writer, PixelReader reader,
|
||||
int sx, int sy, int sw, int sh,
|
||||
int dx, int dy, int dw, int dh) {
|
||||
double xScale = ((double) sw) / dw;
|
||||
double yScale = ((double) sh) / dh;
|
||||
|
||||
for (int xOffset = 0; xOffset < dw; xOffset++) {
|
||||
for (int yOffset = 0; yOffset < dh; yOffset++) {
|
||||
int color = reader.getArgb(sx + (int) (xOffset * xScale), sy + (int) (yOffset * yScale));
|
||||
|
||||
// Draw only non-transparent pixels
|
||||
if ((color >>> 24) != 0)
|
||||
writer.setArgb(dx + xOffset, dy + yOffset, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SkinBindingChangeListener implements ChangeListener<LoadedTexture> {
|
||||
static final WeakHashMap<Canvas, SkinBindingChangeListener> hole = new WeakHashMap<>();
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
@@ -116,11 +115,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
}
|
||||
|
||||
if (Files.isDirectory(javaDir)) {
|
||||
JavaManifest manifest;
|
||||
try (InputStream input = Files.newInputStream(file)) {
|
||||
manifest = JsonUtils.fromJsonFully(input, JavaManifest.class);
|
||||
}
|
||||
|
||||
JavaManifest manifest = JsonUtils.fromJsonFile(file, JavaManifest.class);
|
||||
list.add(JavaRuntime.of(executable, manifest.getInfo(), isManaged));
|
||||
}
|
||||
}
|
||||
@@ -191,7 +186,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
});
|
||||
|
||||
JavaManifest manifest = new JavaManifest(info, update, files);
|
||||
FileUtils.writeText(getManifestFile(platform, gameJavaVersion), JsonUtils.GSON.toJson(manifest));
|
||||
JsonUtils.writeToJsonFile(getManifestFile(platform, gameJavaVersion), manifest);
|
||||
return JavaRuntime.of(executable, info, true);
|
||||
});
|
||||
}
|
||||
@@ -203,7 +198,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
throw new IOException("Platform is mismatch: expected " + platform + " but got " + result.getInfo().getPlatform());
|
||||
|
||||
Path executable = javaDir.resolve("bin").resolve(platform.getOperatingSystem().getJavaExecutable()).toRealPath();
|
||||
FileUtils.writeText(getManifestFile(platform, name), JsonUtils.GSON.toJson(result));
|
||||
JsonUtils.writeToJsonFile(getManifestFile(platform, name), result);
|
||||
return JavaRuntime.of(executable, result.getInfo(), true);
|
||||
});
|
||||
}
|
||||
@@ -212,7 +207,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
public Task<Void> getUninstallJavaTask(Platform platform, String name) {
|
||||
return Task.runAsync(() -> {
|
||||
Files.deleteIfExists(getManifestFile(platform, name));
|
||||
FileUtils.deleteDirectory(getJavaDir(platform, name).toFile());
|
||||
FileUtils.deleteDirectory(getJavaDir(platform, name));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -225,7 +220,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
if (relativized.getNameCount() > 1) {
|
||||
String name = relativized.getName(0).toString();
|
||||
Files.deleteIfExists(getManifestFile(java.getPlatform(), name));
|
||||
FileUtils.deleteDirectory(getJavaDir(java.getPlatform(), name).toFile());
|
||||
FileUtils.deleteDirectory(getJavaDir(java.getPlatform(), name));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,17 +20,20 @@ package org.jackhuang.hmcl.java;
|
||||
import kala.compress.archivers.ArchiveEntry;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.Hex;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -98,10 +101,9 @@ public final class JavaInstallTask extends Task<JavaManifest> {
|
||||
}
|
||||
|
||||
if (tree.isExecutable(entry))
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
path.toFile().setExecutable(true);
|
||||
FileUtils.setExecutable(path);
|
||||
|
||||
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalFile(Hex.encodeHex(messageDigest.digest()), size));
|
||||
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalFile(HexFormat.of().formatHex(messageDigest.digest()), size));
|
||||
}
|
||||
nameStack.remove(nameStack.size() - 1);
|
||||
}
|
||||
|
||||
@@ -209,9 +209,18 @@ public final class JavaManager {
|
||||
|
||||
public static Task<Void> getUninstallJavaTask(JavaRuntime java) {
|
||||
assert java.isManaged();
|
||||
Path root = REPOSITORY.getPlatformRoot(java.getPlatform());
|
||||
Path relativized = root.relativize(java.getBinary());
|
||||
|
||||
Path platformRoot;
|
||||
try {
|
||||
platformRoot = REPOSITORY.getPlatformRoot(java.getPlatform()).toRealPath();
|
||||
} catch (Throwable ignored) {
|
||||
return Task.completed(null);
|
||||
}
|
||||
|
||||
if (!java.getBinary().startsWith(platformRoot))
|
||||
return Task.completed(null);
|
||||
|
||||
Path relativized = platformRoot.relativize(java.getBinary());
|
||||
if (relativized.getNameCount() > 1) {
|
||||
FXUtils.runInFX(() -> {
|
||||
try {
|
||||
@@ -403,8 +412,15 @@ public final class JavaManager {
|
||||
if (System.getenv("PATH") != null) {
|
||||
String[] paths = System.getenv("PATH").split(File.pathSeparator);
|
||||
for (String path : paths) {
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4079
|
||||
// https://github.com/Meloong-Git/PCL/issues/4261
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && path.toLowerCase(Locale.ROOT)
|
||||
.contains("\\common files\\oracle\\java\\")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
tryAddJavaExecutable(javaRuntimes, Paths.get(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
|
||||
tryAddJavaExecutable(javaRuntimes, Path.of(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
|
||||
} catch (InvalidPathException ignored) {
|
||||
}
|
||||
}
|
||||
@@ -472,12 +488,15 @@ public final class JavaManager {
|
||||
try {
|
||||
info = JavaInfo.fromReleaseFile(releaseFile);
|
||||
} catch (IOException e) {
|
||||
try {
|
||||
info = JavaInfoUtils.fromExecutable(executable, false);
|
||||
} catch (IOException e2) {
|
||||
e2.addSuppressed(e);
|
||||
LOG.warning("Failed to lookup Java executable at " + executable, e2);
|
||||
}
|
||||
LOG.warning("Failed to read release file " + releaseFile, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (info == null) {
|
||||
try {
|
||||
info = JavaInfoUtils.fromExecutable(executable, false);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to lookup Java executable at " + executable, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.util.FileSaver;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
@@ -68,18 +69,6 @@ public final class Accounts {
|
||||
|
||||
private static final AuthlibInjectorArtifactProvider AUTHLIB_INJECTOR_DOWNLOADER = createAuthlibInjectorArtifactProvider();
|
||||
|
||||
private static void triggerAuthlibInjectorUpdateCheck() {
|
||||
if (AUTHLIB_INJECTOR_DOWNLOADER instanceof AuthlibInjectorDownloader) {
|
||||
Schedulers.io().execute(() -> {
|
||||
try {
|
||||
((AuthlibInjectorDownloader) AUTHLIB_INJECTOR_DOWNLOADER).checkUpdate();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to check update for authlib-injector", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static final OAuthServer.Factory OAUTH_CALLBACK = new OAuthServer.Factory();
|
||||
|
||||
public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
|
||||
@@ -337,8 +326,6 @@ public final class Accounts {
|
||||
});
|
||||
}
|
||||
|
||||
triggerAuthlibInjectorUpdateCheck();
|
||||
|
||||
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
|
||||
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
|
||||
continue;
|
||||
@@ -346,7 +333,7 @@ public final class Accounts {
|
||||
try {
|
||||
server.fetchMetadataResponse();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to fetch authlib-injector server metdata: " + server, e);
|
||||
LOG.warning("Failed to fetch authlib-injector server metadata: " + server, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -371,24 +358,18 @@ public final class Accounts {
|
||||
// ==== authlib-injector ====
|
||||
private static AuthlibInjectorArtifactProvider createAuthlibInjectorArtifactProvider() {
|
||||
String authlibinjectorLocation = System.getProperty("hmcl.authlibinjector.location");
|
||||
if (authlibinjectorLocation == null) {
|
||||
return new AuthlibInjectorDownloader(
|
||||
Metadata.DEPENDENCIES_DIRECTORY.resolve("universal").resolve("authlib-injector.jar"),
|
||||
DownloadProviders::getDownloadProvider) {
|
||||
@Override
|
||||
public Optional<AuthlibInjectorArtifactInfo> getArtifactInfoImmediately() {
|
||||
Optional<AuthlibInjectorArtifactInfo> local = super.getArtifactInfoImmediately();
|
||||
if (local.isPresent()) {
|
||||
return local;
|
||||
}
|
||||
// search authlib-injector.jar in current directory, it's used as a fallback
|
||||
return parseArtifact(Metadata.CURRENT_DIRECTORY.resolve("authlib-injector.jar"));
|
||||
}
|
||||
};
|
||||
} else {
|
||||
if (authlibinjectorLocation != null) {
|
||||
LOG.info("Using specified authlib-injector: " + authlibinjectorLocation);
|
||||
return new SimpleAuthlibInjectorArtifactProvider(Paths.get(authlibinjectorLocation));
|
||||
}
|
||||
|
||||
String authlibInjectorVersion = JarUtils.getAttribute("hmcl.authlib-injector.version", null);
|
||||
if (authlibInjectorVersion == null)
|
||||
throw new AssertionError("Missing hmcl.authlib-injector.version");
|
||||
|
||||
String authlibInjectorFileName = "authlib-injector-" + authlibInjectorVersion + ".jar";
|
||||
return new AuthlibInjectorExtractor(Accounts.class.getResource("/assets/" + authlibInjectorFileName),
|
||||
Metadata.DEPENDENCIES_DIRECTORY.resolve("universal").resolve(authlibInjectorFileName));
|
||||
}
|
||||
|
||||
private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) {
|
||||
|
||||
@@ -21,10 +21,10 @@ import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
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.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -38,6 +38,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@JsonSerializable
|
||||
public final class AuthlibInjectorServers implements Validation {
|
||||
|
||||
public static final String CONFIG_FILENAME = "authlib-injectors.json";
|
||||
@@ -73,8 +74,7 @@ public final class AuthlibInjectorServers implements Validation {
|
||||
if (ConfigHolder.isNewlyCreated() && Files.exists(configLocation)) {
|
||||
AuthlibInjectorServers configInstance;
|
||||
try {
|
||||
String content = FileUtils.readText(configLocation);
|
||||
configInstance = JsonUtils.GSON.fromJson(content, AuthlibInjectorServers.class);
|
||||
configInstance = JsonUtils.fromJsonFile(configLocation, AuthlibInjectorServers.class);
|
||||
} catch (IOException | JsonParseException e) {
|
||||
LOG.warning("Malformed authlib-injectors.json", e);
|
||||
return;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -107,6 +107,7 @@ public final class ConfigHolder {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
|
||||
&& configLocation.getFileSystem() == FileSystems.getDefault()
|
||||
&& configLocation.toFile().canWrite()) {
|
||||
LOG.warning("Config at " + configLocation + " is not writable, but it seems to be a Samba share or OpenJDK bug");
|
||||
// There are some serious problems with the implementation of Samba or OpenJDK
|
||||
throw new SambaException();
|
||||
} else {
|
||||
@@ -163,7 +164,7 @@ public final class ConfigHolder {
|
||||
LOG.warning("Failed to get owner");
|
||||
}
|
||||
try {
|
||||
String content = FileUtils.readText(configLocation);
|
||||
String content = Files.readString(configLocation);
|
||||
Config deserialized = Config.fromJson(content);
|
||||
if (deserialized == null) {
|
||||
LOG.info("Config is empty");
|
||||
@@ -192,7 +193,7 @@ public final class ConfigHolder {
|
||||
private static GlobalConfig loadGlobalConfig() throws IOException {
|
||||
if (Files.exists(GLOBAL_CONFIG_PATH)) {
|
||||
try {
|
||||
String content = FileUtils.readText(GLOBAL_CONFIG_PATH);
|
||||
String content = Files.readString(GLOBAL_CONFIG_PATH);
|
||||
GlobalConfig deserialized = GlobalConfig.fromJson(content);
|
||||
if (deserialized == null) {
|
||||
LOG.info("Config is empty");
|
||||
|
||||
@@ -21,142 +21,123 @@ 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;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.net.URI;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.task.FetchTask.DEFAULT_CONCURRENCY;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class DownloadProviders {
|
||||
private DownloadProviders() {}
|
||||
private DownloadProviders() {
|
||||
}
|
||||
|
||||
private static DownloadProvider currentDownloadProvider;
|
||||
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";
|
||||
|
||||
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 = mapOf(
|
||||
pair("mojang", MOJANG),
|
||||
pair("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(Arrays.asList(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 = mapOf(
|
||||
pair("official", new AutoDownloadProvider(MOJANG, fileProvider)),
|
||||
pair("balanced", new AutoDownloadProvider(balanced, fileProvider)),
|
||||
pair("mirror", new AutoDownloadProvider(BMCLAPI, fileProvider)));
|
||||
|
||||
observer = FXUtils.observeWeak(() -> {
|
||||
FetchTask.setDownloadExecutorConcurrency(
|
||||
config().getAutoDownloadThreads() ? DEFAULT_CONCURRENCY : config().getDownloadThreads());
|
||||
}, config().autoDownloadThreadsProperty(), config().downloadThreadsProperty());
|
||||
PROVIDER_WRAPPER = new DownloadProviderWrapper(DEFAULT_PROVIDER);
|
||||
}
|
||||
|
||||
static void init() {
|
||||
FXUtils.onChangeAndOperate(config().versionListSourceProperty(), versionListSource -> {
|
||||
if (!providersById.containsKey(versionListSource)) {
|
||||
config().setVersionListSource(DEFAULT_PROVIDER_ID);
|
||||
return;
|
||||
InvalidationListener onChangeDownloadThreads = observable -> {
|
||||
FetchTask.setDownloadExecutorConcurrency(config().getAutoDownloadThreads()
|
||||
? DEFAULT_CONCURRENCY
|
||||
: config().getDownloadThreads());
|
||||
};
|
||||
config().autoDownloadThreadsProperty().addListener(onChangeDownloadThreads);
|
||||
config().downloadThreadsProperty().addListener(onChangeDownloadThreads);
|
||||
onChangeDownloadThreads.invalidated(null);
|
||||
|
||||
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 {
|
||||
String downloadType = config().getDownloadType();
|
||||
PROVIDER_WRAPPER.setProvider(downloadType != null
|
||||
? DIRECT_PROVIDERS.getOrDefault(downloadType, DEFAULT_PROVIDER)
|
||||
: DEFAULT_PROVIDER);
|
||||
}
|
||||
|
||||
currentDownloadProvider = Optional.ofNullable(providersById.get(versionListSource))
|
||||
.orElse(providersById.get(DEFAULT_PROVIDER_ID));
|
||||
});
|
||||
|
||||
if (!rawProviders.containsKey(config().getDownloadType())) {
|
||||
config().setDownloadType(DEFAULT_RAW_PROVIDER_ID);
|
||||
}
|
||||
|
||||
FXUtils.onChangeAndOperate(config().downloadTypeProperty(), downloadType -> {
|
||||
DownloadProvider primary = Optional.ofNullable(rawProviders.get(downloadType))
|
||||
.orElse(rawProviders.get(DEFAULT_RAW_PROVIDER_ID));
|
||||
fileDownloadProvider.setDownloadProviderCandidates(
|
||||
Stream.concat(
|
||||
Stream.of(primary),
|
||||
rawProviders.values().stream().filter(x -> x != primary)
|
||||
).collect(Collectors.toList())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public static String getPrimaryDownloadProviderId() {
|
||||
String downloadType = config().getDownloadType();
|
||||
if (providersById.containsKey(downloadType))
|
||||
return downloadType;
|
||||
else
|
||||
return DEFAULT_PROVIDER_ID;
|
||||
}
|
||||
|
||||
public static DownloadProvider getDownloadProviderByPrimaryId(String primaryId) {
|
||||
return Optional.ofNullable(providersById.get(primaryId))
|
||||
.orElse(providersById.get(DEFAULT_PROVIDER_ID));
|
||||
};
|
||||
config().versionListSourceProperty().addListener(onChangeDownloadSource);
|
||||
config().autoChooseDownloadTypeProperty().addListener(onChangeDownloadSource);
|
||||
config().downloadTypeProperty().addListener(onChangeDownloadSource);
|
||||
onChangeDownloadSource.invalidated(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current primary preferred download provider
|
||||
*/
|
||||
public static DownloadProvider getDownloadProvider() {
|
||||
return config().isAutoChooseDownloadType() ? currentDownloadProvider : fileDownloadProvider;
|
||||
return PROVIDER_WRAPPER;
|
||||
}
|
||||
|
||||
public static String localizeErrorMessage(Throwable exception) {
|
||||
if (exception instanceof DownloadException) {
|
||||
URL url = ((DownloadException) exception).getUrl();
|
||||
URI uri = ((DownloadException) exception).getUri();
|
||||
if (exception.getCause() instanceof SocketTimeoutException) {
|
||||
return i18n("install.failed.downloading.timeout", url);
|
||||
return i18n("install.failed.downloading.timeout", uri);
|
||||
} else if (exception.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
|
||||
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
|
||||
return i18n("download.code." + responseCodeException.getResponseCode(), url);
|
||||
return i18n("download.code." + responseCodeException.getResponseCode(), uri);
|
||||
} else {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
|
||||
}
|
||||
} else if (exception.getCause() instanceof FileNotFoundException) {
|
||||
return i18n("download.code.404", url);
|
||||
return i18n("download.code.404", uri);
|
||||
} else if (exception.getCause() instanceof AccessDeniedException) {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
|
||||
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", url) + "\n" + i18n("exception.artifact_malformed");
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.artifact_malformed");
|
||||
} else if (exception.getCause() instanceof SSLHandshakeException) {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.ssl_handshake");
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.ssl_handshake");
|
||||
} else {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
|
||||
}
|
||||
} else if (exception instanceof ArtifactMalformedException) {
|
||||
return i18n("exception.artifact_malformed");
|
||||
|
||||
@@ -22,5 +22,6 @@ public enum EnumBackgroundImage {
|
||||
CUSTOM,
|
||||
CLASSIC,
|
||||
NETWORK,
|
||||
TRANSLUCENT
|
||||
TRANSLUCENT, // Deprecated
|
||||
PAINT
|
||||
}
|
||||
@@ -21,23 +21,20 @@ import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.text.Font;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.util.Lazy;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.net.MalformedURLException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -54,7 +51,21 @@ public final class FontManager {
|
||||
public static final double DEFAULT_FONT_SIZE = 12.0f;
|
||||
|
||||
private static final Lazy<Font> DEFAULT_FONT = new Lazy<>(() -> {
|
||||
Font font = tryLoadDefaultFont(Metadata.HMCL_CURRENT_DIRECTORY);
|
||||
Font font;
|
||||
|
||||
// Recommended
|
||||
|
||||
font = tryLoadLocalizedFont(Metadata.HMCL_CURRENT_DIRECTORY.resolve("font"));
|
||||
if (font != null)
|
||||
return font;
|
||||
|
||||
font = tryLoadLocalizedFont(Metadata.HMCL_GLOBAL_DIRECTORY.resolve("font"));
|
||||
if (font != null)
|
||||
return font;
|
||||
|
||||
// Legacy
|
||||
|
||||
font = tryLoadDefaultFont(Metadata.HMCL_CURRENT_DIRECTORY);
|
||||
if (font != null)
|
||||
return font;
|
||||
|
||||
@@ -73,10 +84,12 @@ public final class FontManager {
|
||||
return font;
|
||||
}
|
||||
|
||||
// Default
|
||||
|
||||
String fcMatchPattern;
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()
|
||||
&& Locale.getDefault() != Locale.ROOT
|
||||
&& !"en".equals(Locale.getDefault().getLanguage()))
|
||||
return findByFcMatch();
|
||||
&& !(fcMatchPattern = I18n.getLocale().getFcMatchPattern()).isEmpty())
|
||||
return findByFcMatch(fcMatchPattern);
|
||||
else
|
||||
return null;
|
||||
});
|
||||
@@ -112,6 +125,7 @@ public final class FontManager {
|
||||
for (String extension : FONT_EXTENSIONS) {
|
||||
Path path = dir.resolve("font." + extension);
|
||||
if (Files.isRegularFile(path)) {
|
||||
LOG.info("Load font file: " + path);
|
||||
try {
|
||||
Font font = Font.loadFont(path.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
|
||||
if (font != null) {
|
||||
@@ -127,16 +141,47 @@ public final class FontManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Font findByFcMatch() {
|
||||
private static Font tryLoadLocalizedFont(Path dir) {
|
||||
Map<String, Map<String, Path>> fontFiles = LocaleUtils.findAllLocalizedFiles(dir, "font", Set.of(FONT_EXTENSIONS));
|
||||
if (fontFiles.isEmpty())
|
||||
return null;
|
||||
|
||||
List<Locale> candidateLocales = I18n.getLocale().getCandidateLocales();
|
||||
|
||||
for (Locale locale : candidateLocales) {
|
||||
Map<String, Path> extToFiles = fontFiles.get(LocaleUtils.toLanguageKey(locale));
|
||||
if (extToFiles != null) {
|
||||
for (String ext : FONT_EXTENSIONS) {
|
||||
Path fontFile = extToFiles.get(ext);
|
||||
if (fontFile != null) {
|
||||
LOG.info("Load font file: " + fontFile);
|
||||
try {
|
||||
Font font = Font.loadFont(
|
||||
fontFile.toAbsolutePath().normalize().toUri().toURL().toExternalForm(),
|
||||
DEFAULT_FONT_SIZE);
|
||||
if (font != null)
|
||||
return font;
|
||||
} catch (MalformedURLException ignored) {
|
||||
}
|
||||
|
||||
LOG.warning("Failed to load font " + fontFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Font findByFcMatch(String pattern) {
|
||||
Path fcMatch = SystemUtils.which("fc-match");
|
||||
if (fcMatch == null)
|
||||
return null;
|
||||
|
||||
try {
|
||||
String result = SystemUtils.run(fcMatch.toString(),
|
||||
":lang=" + Locale.getDefault().toLanguageTag(),
|
||||
pattern,
|
||||
"--format", "%{family}\\n%{file}").trim();
|
||||
|
||||
String[] results = result.split("\\n");
|
||||
if (results.length != 2 || results[0].isEmpty() || results[1].isEmpty()) {
|
||||
LOG.warning("Unexpected output from fc-match: " + result);
|
||||
@@ -152,46 +197,34 @@ public final class FontManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (JavaRuntime.CURRENT_VERSION >= 9) {
|
||||
try {
|
||||
MethodHandle methodHandle = MethodHandles.publicLookup().findStatic(Font.class, "loadFonts",
|
||||
MethodType.methodType(Font[].class, String.class, double.class));
|
||||
LOG.info("Load font file: " + path);
|
||||
Font[] fonts = Font.loadFonts(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
|
||||
if (fonts == null) {
|
||||
LOG.warning("Failed to load font from " + path);
|
||||
return null;
|
||||
} else if (fonts.length == 0) {
|
||||
LOG.warning("No fonts loaded from " + path);
|
||||
return null;
|
||||
}
|
||||
|
||||
Font[] fonts = (Font[]) methodHandle.invokeExact(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
|
||||
if (fonts == null) {
|
||||
LOG.warning("Failed to load font from " + path);
|
||||
return null;
|
||||
} else if (fonts.length == 0) {
|
||||
LOG.warning("No fonts loaded from " + path);
|
||||
return null;
|
||||
}
|
||||
|
||||
for (Font font : fonts) {
|
||||
if (font.getFamily().equalsIgnoreCase(family)) {
|
||||
return font;
|
||||
}
|
||||
}
|
||||
|
||||
if (family.indexOf(',') >= 0) {
|
||||
for (String candidateFamily : family.split(",")) {
|
||||
for (Font font : fonts) {
|
||||
if (font.getFamily().equalsIgnoreCase(candidateFamily)) {
|
||||
return font;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG.warning(String.format("Family '%s' not found in font file '%s'", family, path));
|
||||
return fonts[0];
|
||||
} catch (NoSuchMethodException | IllegalAccessException ignored) {
|
||||
for (Font font : fonts) {
|
||||
if (font.getFamily().equalsIgnoreCase(family)) {
|
||||
return font;
|
||||
}
|
||||
}
|
||||
|
||||
Font font = Font.loadFont(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
|
||||
if (font == null)
|
||||
LOG.warning("Failed to load font from " + path);
|
||||
return font;
|
||||
if (family.indexOf(',') >= 0) {
|
||||
for (String candidateFamily : family.split(",")) {
|
||||
for (Font font : fonts) {
|
||||
if (font.getFamily().equalsIgnoreCase(candidateFamily)) {
|
||||
return font;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG.warning(String.format("Family '%s' not found in font file '%s'", family, path));
|
||||
return fonts[0];
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to get default font with fc-match", e);
|
||||
return null;
|
||||
|
||||
@@ -21,10 +21,7 @@ import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableSet;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
@@ -51,12 +48,16 @@ public final class GlobalConfig implements Observable {
|
||||
|
||||
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<>());
|
||||
@@ -95,6 +96,18 @@ public final class GlobalConfig implements Observable {
|
||||
this.agreementVersion.set(agreementVersion);
|
||||
}
|
||||
|
||||
public int getTerracottaAgreementVersion() {
|
||||
return terracottaAgreementVersion.get();
|
||||
}
|
||||
|
||||
public IntegerProperty terracottaAgreementVersionProperty() {
|
||||
return terracottaAgreementVersion;
|
||||
}
|
||||
|
||||
public void setTerracottaAgreementVersion(int terracottaAgreementVersion) {
|
||||
this.terracottaAgreementVersion.set(terracottaAgreementVersion);
|
||||
}
|
||||
|
||||
public int getPlatformPromptVersion() {
|
||||
return platformPromptVersion.get();
|
||||
}
|
||||
@@ -131,6 +144,18 @@ public final class GlobalConfig implements Observable {
|
||||
enableOfflineAccount.set(value);
|
||||
}
|
||||
|
||||
public StringProperty fontAntiAliasingProperty() {
|
||||
return fontAntiAliasing;
|
||||
}
|
||||
|
||||
public String getFontAntiAliasing() {
|
||||
return fontAntiAliasing.get();
|
||||
}
|
||||
|
||||
public void setFontAntiAliasing(String value) {
|
||||
this.fontAntiAliasing.set(value);
|
||||
}
|
||||
|
||||
public ObservableSet<String> getUserJava() {
|
||||
return userJava;
|
||||
}
|
||||
@@ -142,11 +167,13 @@ public final class GlobalConfig implements Observable {
|
||||
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"
|
||||
"enableOfflineAccount",
|
||||
"fontAntiAliasing"
|
||||
));
|
||||
|
||||
@Override
|
||||
@@ -157,8 +184,10 @@ public final class GlobalConfig implements Observable {
|
||||
|
||||
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);
|
||||
|
||||
@@ -183,9 +212,11 @@ public final class GlobalConfig implements Observable {
|
||||
|
||||
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()) {
|
||||
|
||||
@@ -35,8 +35,8 @@ import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
|
||||
@@ -65,17 +65,17 @@ public final class Profile implements Observable {
|
||||
this.selectedVersion.set(selectedVersion);
|
||||
}
|
||||
|
||||
private final ObjectProperty<File> gameDir;
|
||||
private final ObjectProperty<Path> gameDir;
|
||||
|
||||
public ObjectProperty<File> gameDirProperty() {
|
||||
public ObjectProperty<Path> gameDirProperty() {
|
||||
return gameDir;
|
||||
}
|
||||
|
||||
public File getGameDir() {
|
||||
public Path getGameDir() {
|
||||
return gameDir.get();
|
||||
}
|
||||
|
||||
public void setGameDir(File gameDir) {
|
||||
public void setGameDir(Path gameDir) {
|
||||
this.gameDir.set(gameDir);
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ public final class Profile implements Observable {
|
||||
this.name.set(name);
|
||||
}
|
||||
|
||||
private BooleanProperty useRelativePath = new SimpleBooleanProperty(this, "useRelativePath", false);
|
||||
private final BooleanProperty useRelativePath = new SimpleBooleanProperty(this, "useRelativePath", false);
|
||||
|
||||
public BooleanProperty useRelativePathProperty() {
|
||||
return useRelativePath;
|
||||
@@ -118,18 +118,18 @@ public final class Profile implements Observable {
|
||||
}
|
||||
|
||||
public Profile(String name) {
|
||||
this(name, new File(".minecraft"));
|
||||
this(name, Path.of(".minecraft"));
|
||||
}
|
||||
|
||||
public Profile(String name, File initialGameDir) {
|
||||
public Profile(String name, Path initialGameDir) {
|
||||
this(name, initialGameDir, new VersionSetting());
|
||||
}
|
||||
|
||||
public Profile(String name, File initialGameDir, VersionSetting global) {
|
||||
public Profile(String name, Path initialGameDir, VersionSetting global) {
|
||||
this(name, initialGameDir, global, null, false);
|
||||
}
|
||||
|
||||
public Profile(String name, File initialGameDir, VersionSetting global, String selectedVersion, boolean useRelativePath) {
|
||||
public Profile(String name, Path initialGameDir, VersionSetting global, String selectedVersion, boolean useRelativePath) {
|
||||
this.name = new SimpleStringProperty(this, "name", name);
|
||||
gameDir = new SimpleObjectProperty<>(this, "gameDir", initialGameDir);
|
||||
repository = new HMCLGameRepository(this, initialGameDir);
|
||||
@@ -234,7 +234,7 @@ public final class Profile implements Observable {
|
||||
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.add("global", context.serialize(src.getGlobal()));
|
||||
jsonObject.addProperty("gameDir", src.getGameDir().getPath());
|
||||
jsonObject.addProperty("gameDir", src.getGameDir().toString());
|
||||
jsonObject.addProperty("useRelativePath", src.isUseRelativePath());
|
||||
jsonObject.addProperty("selectedMinecraftVersion", src.getSelectedVersion());
|
||||
|
||||
@@ -243,12 +243,11 @@ public final class Profile implements Observable {
|
||||
|
||||
@Override
|
||||
public Profile deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (json == JsonNull.INSTANCE || !(json instanceof JsonObject)) return null;
|
||||
JsonObject obj = (JsonObject) json;
|
||||
if (!(json instanceof JsonObject obj)) return null;
|
||||
String gameDir = Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse("");
|
||||
|
||||
return new Profile("Default",
|
||||
new File(gameDir),
|
||||
Path.of(gameDir),
|
||||
context.deserialize(obj.get("global"), VersionSetting.class),
|
||||
Optional.ofNullable(obj.get("selectedMinecraftVersion")).map(JsonElement::getAsString).orElse(""),
|
||||
Optional.ofNullable(obj.get("useRelativePath")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
|
||||
@@ -26,7 +26,7 @@ import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -48,20 +48,17 @@ public final class Profiles {
|
||||
}
|
||||
|
||||
public static String getProfileDisplayName(Profile profile) {
|
||||
switch (profile.getName()) {
|
||||
case Profiles.DEFAULT_PROFILE:
|
||||
return i18n("profile.default");
|
||||
case Profiles.HOME_PROFILE:
|
||||
return i18n("profile.home");
|
||||
default:
|
||||
return profile.getName();
|
||||
}
|
||||
return switch (profile.getName()) {
|
||||
case Profiles.DEFAULT_PROFILE -> i18n("profile.default");
|
||||
case Profiles.HOME_PROFILE -> i18n("profile.home");
|
||||
default -> profile.getName();
|
||||
};
|
||||
}
|
||||
|
||||
private static final ObservableList<Profile> profiles = observableArrayList(profile -> new Observable[] { profile });
|
||||
private static final ReadOnlyListWrapper<Profile> profilesWrapper = new ReadOnlyListWrapper<>(profiles);
|
||||
|
||||
private static ObjectProperty<Profile> selectedProfile = new SimpleObjectProperty<Profile>() {
|
||||
private static final ObjectProperty<Profile> selectedProfile = new SimpleObjectProperty<Profile>() {
|
||||
{
|
||||
profiles.addListener(onInvalidating(this::invalidated));
|
||||
}
|
||||
@@ -104,8 +101,8 @@ public final class Profiles {
|
||||
|
||||
private static void checkProfiles() {
|
||||
if (profiles.isEmpty()) {
|
||||
Profile current = new Profile(Profiles.DEFAULT_PROFILE, new File(".minecraft"), new VersionSetting(), null, true);
|
||||
Profile home = new Profile(Profiles.HOME_PROFILE, Metadata.MINECRAFT_DIRECTORY.toFile());
|
||||
Profile current = new Profile(Profiles.DEFAULT_PROFILE, Path.of(".minecraft"), new VersionSetting(), null, true);
|
||||
Profile home = new Profile(Profiles.HOME_PROFILE, Metadata.MINECRAFT_DIRECTORY);
|
||||
Platform.runLater(() -> profiles.addAll(current, home));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,21 +18,33 @@
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.beans.InvalidationListener;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class ProxyManager {
|
||||
|
||||
private static final ProxySelector NO_PROXY = new SimpleProxySelector(Proxy.NO_PROXY);
|
||||
private static final ProxySelector SYSTEM_DEFAULT = Lang.requireNonNullElse(ProxySelector.getDefault(), NO_PROXY);
|
||||
private static final SimpleProxySelector NO_PROXY = new SimpleProxySelector(Proxy.NO_PROXY);
|
||||
private static final ProxySelector SYSTEM_DEFAULT;
|
||||
|
||||
static {
|
||||
ProxySelector systemProxySelector = ProxySelector.getDefault();
|
||||
SYSTEM_DEFAULT = systemProxySelector != null
|
||||
? new ProxySelectorWrapper(systemProxySelector)
|
||||
: NO_PROXY;
|
||||
}
|
||||
|
||||
private static volatile @NotNull ProxySelector defaultProxySelector = SYSTEM_DEFAULT;
|
||||
private static volatile @Nullable SimpleAuthenticator defaultAuthenticator = null;
|
||||
|
||||
private static ProxySelector getProxySelector() {
|
||||
if (config().hasProxy()) {
|
||||
@@ -46,22 +58,22 @@ public final class ProxyManager {
|
||||
LOG.warning("Illegal proxy port: " + port);
|
||||
return NO_PROXY;
|
||||
} else {
|
||||
return new SimpleProxySelector(new Proxy(proxyType, new InetSocketAddress(host, port)));
|
||||
return new ProxySelectorWrapper(new SimpleProxySelector(new Proxy(proxyType, new InetSocketAddress(host, port))));
|
||||
}
|
||||
} else {
|
||||
return ProxyManager.SYSTEM_DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
private static Authenticator getAuthenticator() {
|
||||
private static SimpleAuthenticator getAuthenticator() {
|
||||
if (config().hasProxy() && config().hasProxyAuth()) {
|
||||
String username = config().getProxyUser();
|
||||
String password = config().getProxyPass();
|
||||
|
||||
if (username != null || password != null)
|
||||
return new SimpleAuthenticator(
|
||||
Lang.requireNonNullElse(username, ""),
|
||||
Lang.requireNonNullElse(password, "").toCharArray()
|
||||
Objects.requireNonNullElse(username, ""),
|
||||
Objects.requireNonNullElse(password, "").toCharArray()
|
||||
);
|
||||
else
|
||||
return null;
|
||||
@@ -70,30 +82,75 @@ public final class ProxyManager {
|
||||
}
|
||||
|
||||
static void init() {
|
||||
ProxySelector.setDefault(getProxySelector());
|
||||
InvalidationListener updateProxySelector = observable -> ProxySelector.setDefault(getProxySelector());
|
||||
ProxySelector.setDefault(new ProxySelector() {
|
||||
@Override
|
||||
public List<Proxy> select(URI uri) {
|
||||
return defaultProxySelector.select(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
|
||||
defaultProxySelector.connectFailed(uri, sa, ioe);
|
||||
}
|
||||
});
|
||||
Authenticator.setDefault(new Authenticator() {
|
||||
@Override
|
||||
protected PasswordAuthentication getPasswordAuthentication() {
|
||||
var defaultAuthenticator = ProxyManager.defaultAuthenticator;
|
||||
return defaultAuthenticator != null ? defaultAuthenticator.getPasswordAuthentication() : null;
|
||||
}
|
||||
});
|
||||
|
||||
defaultProxySelector = getProxySelector();
|
||||
InvalidationListener updateProxySelector = observable -> defaultProxySelector = getProxySelector();
|
||||
config().proxyTypeProperty().addListener(updateProxySelector);
|
||||
config().proxyHostProperty().addListener(updateProxySelector);
|
||||
config().proxyPortProperty().addListener(updateProxySelector);
|
||||
config().hasProxyProperty().addListener(updateProxySelector);
|
||||
|
||||
Authenticator.setDefault(getAuthenticator());
|
||||
InvalidationListener updateAuthenticator = observable -> Authenticator.setDefault(getAuthenticator());
|
||||
defaultAuthenticator = getAuthenticator();
|
||||
InvalidationListener updateAuthenticator = observable -> defaultAuthenticator = getAuthenticator();
|
||||
config().hasProxyProperty().addListener(updateAuthenticator);
|
||||
config().hasProxyAuthProperty().addListener(updateAuthenticator);
|
||||
config().proxyUserProperty().addListener(updateAuthenticator);
|
||||
config().proxyPassProperty().addListener(updateAuthenticator);
|
||||
}
|
||||
|
||||
private static final class SimpleProxySelector extends ProxySelector {
|
||||
private static abstract class AbstractProxySelector extends ProxySelector {
|
||||
@Override
|
||||
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
|
||||
if (uri == null || sa == null || ioe == null) {
|
||||
throw new IllegalArgumentException("Arguments can't be null.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SimpleProxySelector extends AbstractProxySelector {
|
||||
private final List<Proxy> proxies;
|
||||
|
||||
SimpleProxySelector(Proxy proxy) {
|
||||
this(Collections.singletonList(proxy));
|
||||
this.proxies = List.of(proxy);
|
||||
}
|
||||
|
||||
SimpleProxySelector(List<Proxy> proxies) {
|
||||
this.proxies = proxies;
|
||||
@Override
|
||||
public List<Proxy> select(URI uri) {
|
||||
if (uri == null)
|
||||
throw new IllegalArgumentException("URI can't be null.");
|
||||
return proxies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SimpleProxySelector" + proxies;
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps another ProxySelector to avoid using proxy for loopback addresses.
|
||||
private static final class ProxySelectorWrapper extends AbstractProxySelector {
|
||||
private final ProxySelector source;
|
||||
|
||||
ProxySelectorWrapper(ProxySelector source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -101,19 +158,10 @@ public final class ProxyManager {
|
||||
if (uri == null)
|
||||
throw new IllegalArgumentException("URI can't be null.");
|
||||
|
||||
return proxies;
|
||||
}
|
||||
if (NetworkUtils.isLoopbackAddress(uri))
|
||||
return NO_PROXY.proxies;
|
||||
|
||||
@Override
|
||||
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
|
||||
if (uri == null || sa == null || ioe == null) {
|
||||
throw new IllegalArgumentException("Arguments can't be null.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SimpleProxySelector" + proxies;
|
||||
return source.select(uri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +175,7 @@ public final class ProxyManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PasswordAuthentication getPasswordAuthentication() {
|
||||
public PasswordAuthentication getPasswordAuthentication() {
|
||||
return getRequestorType() == RequestorType.PROXY ? new PasswordAuthentication(username, password) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,13 @@ 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 org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
@@ -34,7 +39,6 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -43,6 +47,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
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;
|
||||
|
||||
@@ -50,12 +55,16 @@ 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, String fallback) {
|
||||
@@ -68,7 +77,7 @@ public final class StyleSheets {
|
||||
Path temp = Files.createTempFile("hmcl", ".css");
|
||||
// For JavaFX 17 or earlier, CssParser uses the default charset
|
||||
// https://bugs.openjdk.org/browse/JDK-8279328
|
||||
FileUtils.writeText(temp, styleSheet, Charset.defaultCharset());
|
||||
Files.writeString(temp, styleSheet, Charset.defaultCharset());
|
||||
temp.toFile().deleteOnExit();
|
||||
return temp.toUri().toString();
|
||||
} catch (IOException | NullPointerException e) {
|
||||
@@ -124,34 +133,58 @@ public final class StyleSheets {
|
||||
|
||||
builder.append('}');
|
||||
|
||||
return toStyleSheetUri(builder.toString(), fontFamily);
|
||||
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) + ";" +
|
||||
'}', blueCss);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public enum VersionIconType {
|
||||
@@ -33,10 +34,24 @@ public enum VersionIconType {
|
||||
FORGE("/assets/img/forge.png"),
|
||||
NEO_FORGE("/assets/img/neoforge.png"),
|
||||
FURNACE("/assets/img/furnace.png"),
|
||||
QUILT("/assets/img/quilt.png");
|
||||
QUILT("/assets/img/quilt.png"),
|
||||
APRIL_FOOLS("/assets/img/april_fools.png"),
|
||||
CLEANROOM("/assets/img/cleanroom.png");
|
||||
|
||||
// Please append new items at last
|
||||
|
||||
public static VersionIconType getIconType(ModLoaderType modLoaderType) {
|
||||
return switch (modLoaderType) {
|
||||
case FORGE -> VersionIconType.FORGE;
|
||||
case NEO_FORGED -> VersionIconType.NEO_FORGE;
|
||||
case FABRIC -> VersionIconType.FABRIC;
|
||||
case QUILT -> VersionIconType.QUILT;
|
||||
case LITE_LOADER -> VersionIconType.CHICKEN;
|
||||
case CLEANROOM -> VersionIconType.CLEANROOM;
|
||||
default -> VersionIconType.COMMAND;
|
||||
};
|
||||
}
|
||||
|
||||
private final String resourceUrl;
|
||||
|
||||
VersionIconType(String resourceUrl) {
|
||||
|
||||
@@ -380,6 +380,20 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
noJVMArgsProperty.set(noJVMArgs);
|
||||
}
|
||||
|
||||
private final BooleanProperty noOptimizingJVMArgsProperty = new SimpleBooleanProperty(this, "noOptimizingJVMArgs", false);
|
||||
|
||||
public BooleanProperty noOptimizingJVMArgsProperty() {
|
||||
return noOptimizingJVMArgsProperty;
|
||||
}
|
||||
|
||||
public boolean isNoOptimizingJVMArgs() {
|
||||
return noOptimizingJVMArgsProperty.get();
|
||||
}
|
||||
|
||||
public void setNoOptimizingJVMArgs(boolean noOptimizingJVMArgs) {
|
||||
noOptimizingJVMArgsProperty.set(noOptimizingJVMArgs);
|
||||
}
|
||||
|
||||
private final BooleanProperty notCheckJVMProperty = new SimpleBooleanProperty(this, "notCheckJVM", false);
|
||||
|
||||
public BooleanProperty notCheckJVMProperty() {
|
||||
@@ -445,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", "");
|
||||
@@ -757,10 +785,12 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
obj.addProperty("wrapper", src.getWrapper());
|
||||
obj.addProperty("fullscreen", src.isFullscreen());
|
||||
obj.addProperty("noJVMArgs", src.isNoJVMArgs());
|
||||
obj.addProperty("noOptimizingJVMArgs", src.isNoOptimizingJVMArgs());
|
||||
obj.addProperty("notCheckGame", src.isNotCheckGame());
|
||||
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());
|
||||
@@ -797,14 +827,6 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
return obj;
|
||||
}
|
||||
|
||||
private static <T> T getOrDefault(T[] values, JsonElement index, T defaultValue) {
|
||||
if (index == null)
|
||||
return defaultValue;
|
||||
|
||||
int idx = index.getAsInt();
|
||||
return idx >= 0 && idx < values.length ? values[idx] : defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VersionSetting deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (!(json instanceof JsonObject))
|
||||
@@ -835,18 +857,20 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
vs.setNativesDir(Optional.ofNullable(obj.get("nativesDir")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setFullscreen(Optional.ofNullable(obj.get("fullscreen")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNoJVMArgs(Optional.ofNullable(obj.get("noJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNoOptimizingJVMArgs(Optional.ofNullable(obj.get("noOptimizingJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNotCheckGame(Optional.ofNullable(obj.get("notCheckGame")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
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.setLauncherVisibility(getOrDefault(LauncherVisibility.values(), obj.get("launcherVisibility"), LauncherVisibility.HIDE));
|
||||
vs.setProcessPriority(getOrDefault(ProcessPriority.values(), obj.get("processPriority"), ProcessPriority.NORMAL));
|
||||
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));
|
||||
vs.setUseNativeOpenAL(Optional.ofNullable(obj.get("useNativeOpenAL")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setGameDirType(getOrDefault(GameDirectoryType.values(), obj.get("gameDirType"), GameDirectoryType.ROOT_FOLDER));
|
||||
vs.setGameDirType(parseJsonPrimitive(obj.getAsJsonPrimitive("gameDirType"), GameDirectoryType.class, GameDirectoryType.ROOT_FOLDER));
|
||||
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
|
||||
vs.setNativesDirType(getOrDefault(NativesDirectoryType.values(), obj.get("nativesDirType"), NativesDirectoryType.VERSION_FOLDER));
|
||||
vs.setVersionIcon(getOrDefault(VersionIconType.values(), obj.get("versionIcon"), VersionIconType.DEFAULT));
|
||||
vs.setNativesDirType(parseJsonPrimitive(obj.getAsJsonPrimitive("nativesDirType"), NativesDirectoryType.class, NativesDirectoryType.VERSION_FOLDER));
|
||||
vs.setVersionIcon(parseJsonPrimitive(obj.getAsJsonPrimitive("versionIcon"), VersionIconType.class, VersionIconType.DEFAULT));
|
||||
|
||||
if (obj.get("javaVersionType") != null) {
|
||||
JavaVersionType javaVersionType = parseJsonPrimitive(obj.getAsJsonPrimitive("javaVersionType"), JavaVersionType.class, JavaVersionType.AUTO);
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* 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 com.google.gson.JsonObject;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ReadOnlyDoubleWrapper;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
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.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
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.concurrent.CancellationException;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class TerracottaManager {
|
||||
private TerracottaManager() {
|
||||
}
|
||||
|
||||
private static final AtomicReference<TerracottaState> STATE_V = new AtomicReference<>(TerracottaState.Bootstrap.INSTANCE);
|
||||
private static final ReadOnlyObjectWrapper<TerracottaState> STATE = new ReadOnlyObjectWrapper<>(STATE_V.getPlain());
|
||||
private static final InvocationDispatcher<TerracottaState> STATE_D = InvocationDispatcher.runOn(Platform::runLater, STATE::set);
|
||||
|
||||
static {
|
||||
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()));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
compareAndSet(TerracottaState.Bootstrap.INSTANCE, new TerracottaState.Fatal(TerracottaState.Fatal.Type.UNKNOWN));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}, "Terracotta Background Daemon", true);
|
||||
}
|
||||
|
||||
public static boolean validate(Path file) {
|
||||
return FileUtils.getName(file).equalsIgnoreCase(TerracottaMetadata.PACKAGE_NAME);
|
||||
}
|
||||
|
||||
public static TerracottaState.Preparing install(@Nullable Path file) {
|
||||
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 = 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 CancellationException) {
|
||||
} 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));
|
||||
}
|
||||
}).start();
|
||||
|
||||
return compareAndSet(state, preparing) ? preparing : null;
|
||||
}
|
||||
|
||||
private static ITerracottaProvider getProvider() {
|
||||
ITerracottaProvider provider = TerracottaMetadata.PROVIDER;
|
||||
if (provider == null) {
|
||||
throw new AssertionError("Terracotta Provider must NOT be null.");
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
public static TerracottaState recover(@Nullable Path file) {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (!(state instanceof TerracottaState.Fatal && ((TerracottaState.Fatal) state).isRecoverable())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return switch (getProvider().status()) {
|
||||
case NOT_EXIST, LEGACY_VERSION -> install(file);
|
||||
case READY -> {
|
||||
TerracottaState.Launching launching = setState(new TerracottaState.Launching());
|
||||
launch(launching);
|
||||
yield launching;
|
||||
}
|
||||
};
|
||||
} catch (NullPointerException | 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) {
|
||||
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)));
|
||||
process.pumpInputStream(SystemUtils::onLogLine);
|
||||
process.pumpErrorStream(SystemUtils::onLogLine);
|
||||
|
||||
long exitTime = -1;
|
||||
while (true) {
|
||||
if (Files.exists(path)) {
|
||||
JsonObject object = JsonUtils.fromNonNullJson(Files.readString(path), JsonObject.class);
|
||||
return object.get("port").getAsInt();
|
||||
}
|
||||
|
||||
if (!process.isRunning()) {
|
||||
if (exitTime == -1) {
|
||||
exitTime = System.currentTimeMillis();
|
||||
} else if (System.currentTimeMillis() - exitTime >= 10000) {
|
||||
throw new IllegalStateException("Process has exited for 10s.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}).whenComplete(Schedulers.javafx(), (port, exception) -> {
|
||||
TerracottaState next;
|
||||
if (exception == null) {
|
||||
next = new TerracottaState.Unknown(port);
|
||||
} else {
|
||||
next = new TerracottaState.Fatal(TerracottaState.Fatal.Type.TERRACOTTA);
|
||||
}
|
||||
compareAndSet(state, next);
|
||||
}).start();
|
||||
}
|
||||
|
||||
public static Task<String> exportLogs() {
|
||||
if (STATE_V.get() instanceof TerracottaState.PortSpecific portSpecific) {
|
||||
return new GetTask(URI.create(String.format("http://127.0.0.1:%d/log?fetch=true", portSpecific.port)))
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
}
|
||||
return Task.completed(null);
|
||||
}
|
||||
|
||||
public static TerracottaState.Waiting setWaiting() {
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (state instanceof TerracottaState.PortSpecific portSpecific) {
|
||||
new GetTask(URI.create(String.format("http://127.0.0.1:%d/state/ide", portSpecific.port)))
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.start();
|
||||
return new TerracottaState.Waiting(-1, -1, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String getPlayerName() {
|
||||
Account account = Accounts.getSelectedAccount();
|
||||
return account != null ? account.getCharacter() : i18n("terracotta.player_anonymous");
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
return new TerracottaState.HostScanning(-1, -1, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Task<TerracottaState.GuestStarting> 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);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static <T extends TerracottaState> T setState(T value) {
|
||||
if (value == null) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
STATE_V.set(value);
|
||||
STATE_D.accept(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static boolean compareAndSet(TerracottaState previous, TerracottaState next) {
|
||||
if (next == null) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
if (STATE_V.compareAndSet(previous, next)) {
|
||||
STATE_D.accept(next);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* 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 com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
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.Lang;
|
||||
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.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
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.regex.Pattern;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class TerracottaMetadata {
|
||||
private TerracottaMetadata() {
|
||||
}
|
||||
|
||||
public record Link(@SerializedName("desc") LocalizedText description, String link) {
|
||||
}
|
||||
|
||||
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("downloads") List<String> downloads,
|
||||
@SerializedName("downloads_CN") List<String> downloadsCN,
|
||||
@SerializedName("links") List<Link> links
|
||||
) {
|
||||
private @Nullable TerracottaNative of(String classifier) {
|
||||
String hash = this.classifiers.get(classifier);
|
||||
if (hash == null)
|
||||
return null;
|
||||
|
||||
if (!hash.startsWith("sha256:"))
|
||||
throw new IllegalArgumentException(String.format("Invalid hash value %s for classifier %s.", hash, classifier));
|
||||
hash = hash.substring("sha256:".length());
|
||||
|
||||
List<URI> links = new ArrayList<>(this.downloads.size() + this.downloadsCN.size());
|
||||
for (String download : LocaleUtils.IS_CHINA_MAINLAND
|
||||
? Lang.merge(this.downloadsCN, this.downloads)
|
||||
: Lang.merge(this.downloads, this.downloadsCN)) {
|
||||
links.add(URI.create(download.replace("${version}", this.latest).replace("${classifier}", classifier)));
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static final ITerracottaProvider 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(
|
||||
"v", "v1",
|
||||
"launcher_version", Metadata.VERSION
|
||||
));
|
||||
|
||||
private static final Pattern LEGACY;
|
||||
private static final List<String> RECENT;
|
||||
private static final String LATEST;
|
||||
|
||||
static {
|
||||
Config config;
|
||||
try (InputStream is = TerracottaMetadata.class.getResourceAsStream("/assets/terracotta.json")) {
|
||||
config = JsonUtils.fromNonNullJsonFully(is, Config.class);
|
||||
} catch (IOException e) {
|
||||
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)
|
||||
));
|
||||
}
|
||||
|
||||
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 arch = Architecture.SYSTEM_ARCH.getCheckedName();
|
||||
return switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS -> {
|
||||
if (!OperatingSystem.SYSTEM_VERSION.isAtLeast(OSVersion.WINDOWS_10))
|
||||
yield null;
|
||||
|
||||
TerracottaNative target = config.of("windows-%s.exe".formatted(arch));
|
||||
yield target != null
|
||||
? new ProviderContext(new GeneralProvider(target), "windows", arch)
|
||||
: null;
|
||||
}
|
||||
case LINUX -> {
|
||||
TerracottaNative target = config.of("linux-%s".formatted(arch));
|
||||
yield target != null
|
||||
? new ProviderContext(new GeneralProvider(target), "linux", arch)
|
||||
: null;
|
||||
}
|
||||
case MACOS -> {
|
||||
TerracottaNative installer = config.of("macos-%s.pkg".formatted(arch));
|
||||
TerracottaNative binary = config.of("macos-%s".formatted(arch));
|
||||
|
||||
yield installer != null && binary != null
|
||||
? new ProviderContext(new MacOSProvider(installer, binary), "macos", arch)
|
||||
: null;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
FileUtils.deleteDirectory(path);
|
||||
} catch (IOException e) {
|
||||
LOG.warning(String.format("Unable to remove legacy terracotta files: %s", path), 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* 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,304 @@
|
||||
/*
|
||||
* 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 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 org.jackhuang.hmcl.terracotta.profile.TerracottaProfile;
|
||||
import org.jackhuang.hmcl.terracotta.provider.ITerracottaProvider;
|
||||
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.atomic.AtomicBoolean;
|
||||
|
||||
public abstract sealed class TerracottaState {
|
||||
protected TerracottaState() {
|
||||
}
|
||||
|
||||
public boolean isUIFakeState() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isForkOf(TerracottaState state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static final class Bootstrap extends TerracottaState {
|
||||
static final Bootstrap INSTANCE = new Bootstrap();
|
||||
|
||||
private Bootstrap() {
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Uninitialized extends TerracottaState {
|
||||
private final boolean hasLegacy;
|
||||
|
||||
Uninitialized(boolean hasLegacy) {
|
||||
this.hasLegacy = hasLegacy;
|
||||
}
|
||||
|
||||
public boolean hasLegacy() {
|
||||
return hasLegacy;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Preparing extends TerracottaState implements ITerracottaProvider.Context {
|
||||
private final ReadOnlyDoubleWrapper progress;
|
||||
|
||||
private final AtomicBoolean installFence = new AtomicBoolean(false);
|
||||
|
||||
Preparing(ReadOnlyDoubleWrapper progress) {
|
||||
this.progress = progress;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasInstallFence() {
|
||||
return !installFence.get();
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Launching extends TerracottaState {
|
||||
Launching() {
|
||||
}
|
||||
}
|
||||
|
||||
static abstract sealed class PortSpecific extends TerracottaState {
|
||||
transient int port;
|
||||
|
||||
protected PortSpecific(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonType(
|
||||
property = "state",
|
||||
subtypes = {
|
||||
@JsonSubtype(clazz = Waiting.class, name = "waiting"),
|
||||
@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 = GuestStarting.class, name = "guest-starting"),
|
||||
@JsonSubtype(clazz = GuestOK.class, name = "guest-ok"),
|
||||
@JsonSubtype(clazz = Exception.class, name = "exception"),
|
||||
}
|
||||
)
|
||||
static abstract sealed class Ready extends PortSpecific {
|
||||
@SerializedName("index")
|
||||
final int index;
|
||||
|
||||
@SerializedName("state")
|
||||
private final String state;
|
||||
|
||||
Ready(int port, int index, String state) {
|
||||
super(port);
|
||||
this.index = index;
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUIFakeState() {
|
||||
return this.index == -1;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Unknown extends PortSpecific {
|
||||
Unknown(int port) {
|
||||
super(port);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Waiting extends Ready {
|
||||
Waiting(int port, int index, String state) {
|
||||
super(port, index, state);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class HostScanning extends Ready {
|
||||
HostScanning(int port, int index, String state) {
|
||||
super(port, index, state);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class HostStarting extends Ready {
|
||||
HostStarting(int port, int index, String state) {
|
||||
super(port, index, state);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class HostOK extends Ready implements Validation {
|
||||
@SerializedName("room")
|
||||
private final String code;
|
||||
|
||||
@SerializedName("profile_index")
|
||||
private final int profileIndex;
|
||||
|
||||
@SerializedName("profiles")
|
||||
private final List<TerracottaProfile> profiles;
|
||||
|
||||
HostOK(int port, int index, String state, String code, int profileIndex, List<TerracottaProfile> profiles) {
|
||||
super(port, index, state);
|
||||
this.code = code;
|
||||
this.profileIndex = profileIndex;
|
||||
this.profiles = profiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
if (code == null) {
|
||||
throw new JsonParseException("code is null");
|
||||
}
|
||||
if (profiles == null) {
|
||||
throw new JsonParseException("profiles is null");
|
||||
}
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public List<TerracottaProfile> getProfiles() {
|
||||
return profiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isForkOf(TerracottaState state) {
|
||||
return state instanceof HostOK hostOK && this.index - hostOK.index <= profileIndex;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class GuestStarting extends Ready {
|
||||
GuestStarting(int port, int index, String state) {
|
||||
super(port, index, state);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class GuestOK extends Ready implements Validation {
|
||||
@SerializedName("url")
|
||||
private final String url;
|
||||
|
||||
@SerializedName("profile_index")
|
||||
private final int profileIndex;
|
||||
|
||||
@SerializedName("profiles")
|
||||
private final List<TerracottaProfile> profiles;
|
||||
|
||||
GuestOK(int port, int index, String state, String url, int profileIndex, List<TerracottaProfile> profiles) {
|
||||
super(port, index, state);
|
||||
this.url = url;
|
||||
this.profileIndex = profileIndex;
|
||||
this.profiles = profiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
if (profiles == null) {
|
||||
throw new JsonParseException("profiles is null");
|
||||
}
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public List<TerracottaProfile> getProfiles() {
|
||||
return profiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isForkOf(TerracottaState state) {
|
||||
return state instanceof GuestOK guestOK && this.index - guestOK.index <= profileIndex;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Exception extends Ready implements Validation {
|
||||
public enum Type {
|
||||
PING_HOST_FAIL,
|
||||
PING_HOST_RST,
|
||||
GUEST_ET_CRASH,
|
||||
HOST_ET_CRASH,
|
||||
PING_SERVER_RST,
|
||||
SCAFFOLDING_INVALID_RESPONSE
|
||||
}
|
||||
|
||||
private static final TerracottaState.Exception.Type[] LOOKUP = Type.values();
|
||||
|
||||
@SerializedName("type")
|
||||
private final int type;
|
||||
|
||||
Exception(int port, int index, String state, int type) {
|
||||
super(port, index, state);
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
if (type < 0 || type >= LOOKUP.length) {
|
||||
throw new JsonParseException(String.format("Type must between [0, %s)", LOOKUP.length));
|
||||
}
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return LOOKUP[type];
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Fatal extends TerracottaState {
|
||||
public enum Type {
|
||||
OS,
|
||||
NETWORK,
|
||||
INSTALL,
|
||||
TERRACOTTA,
|
||||
UNKNOWN;
|
||||
}
|
||||
|
||||
private final Type type;
|
||||
|
||||
public Fatal(Type type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public boolean isRecoverable() {
|
||||
return this.type != Type.UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-13
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -15,18 +15,15 @@
|
||||
* 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.animation;
|
||||
package org.jackhuang.hmcl.terracotta.profile;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.util.Duration;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public interface AnimationHandler {
|
||||
Duration getDuration();
|
||||
|
||||
Pane getCurrentRoot();
|
||||
|
||||
Node getPreviousNode();
|
||||
|
||||
Node getCurrentNode();
|
||||
public enum ProfileKind {
|
||||
@SerializedName("HOST")
|
||||
HOST,
|
||||
@SerializedName("LOCAL")
|
||||
LOCAL,
|
||||
@SerializedName("GUEST")
|
||||
GUEST
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.profile;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public final class TerracottaProfile {
|
||||
@SerializedName("machine_id")
|
||||
private final String machineID;
|
||||
|
||||
@SerializedName("name")
|
||||
private final String name;
|
||||
|
||||
@SerializedName("vendor")
|
||||
private final String vendor;
|
||||
|
||||
@SerializedName("kind")
|
||||
private final ProfileKind type;
|
||||
|
||||
private TerracottaProfile(String machineID, String name, String vendor, ProfileKind type) {
|
||||
this.machineID = machineID;
|
||||
this.name = name;
|
||||
this.vendor = vendor;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getMachineID() {
|
||||
return machineID;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getVendor() {
|
||||
return vendor;
|
||||
}
|
||||
|
||||
public ProfileKind getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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 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 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 GeneralProvider(TerracottaNative target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
@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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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 org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.terracotta.TerracottaNative;
|
||||
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.StandardCopyOption;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
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 MacOSProvider(TerracottaNative installer, TerracottaNative binary) {
|
||||
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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> install(Context context, @Nullable TarFileTree tree) throws IOException {
|
||||
assert installer != null && binary != null;
|
||||
|
||||
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
|
||||
|
||||
return Task.allOf(
|
||||
installerTask.thenComposeAsync(() -> {
|
||||
Path osascript = SystemUtils.which("osascript");
|
||||
if (osascript == null) {
|
||||
throw new IllegalStateException("Cannot locate 'osascript' system executable on MacOS for installing Terracotta.");
|
||||
}
|
||||
|
||||
Path pkg = Files.createTempDirectory(Metadata.HMCL_GLOBAL_DIRECTORY, "terracotta-pkg")
|
||||
.toRealPath()
|
||||
.resolve(FileUtils.getName(installer.getPath()));
|
||||
Files.copy(installer.getPath(), pkg, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
ManagedProcess process = new ManagedProcess(new ProcessBuilder(
|
||||
osascript.toString(), "-e", String.format(
|
||||
"do shell script \"installer -pkg '%s' -target /\" with prompt \"%s\" with administrator privileges",
|
||||
pkg, i18n("terracotta.sudo_installing")
|
||||
)));
|
||||
process.pumpInputStream(SystemUtils::onLogLine);
|
||||
process.pumpErrorStream(SystemUtils::onLogLine);
|
||||
|
||||
return Task.fromCompletableFuture(process.getProcess().onExit()).thenRunAsync(() -> {
|
||||
try {
|
||||
FileUtils.cleanDirectory(pkg.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",
|
||||
pkg,
|
||||
process.getExitCode()
|
||||
));
|
||||
}
|
||||
});
|
||||
}),
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -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,134 @@
|
||||
/*
|
||||
* 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 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.scene.paint.Color;
|
||||
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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
|
||||
/// @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 Brightness.DEFAULT;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private Themes() {
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import java.awt.*;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public final class AwtUtils {
|
||||
private AwtUtils() {
|
||||
}
|
||||
|
||||
public static void setAppleIcon(Image image) {
|
||||
try {
|
||||
Class<?> taskbarClass = Class.forName("java.awt.Taskbar");
|
||||
Method getTaskBarMethod = taskbarClass.getDeclaredMethod("getTaskbar");
|
||||
Object taskBar = getTaskBarMethod.invoke(null);
|
||||
Method setIconImageMethod = taskbarClass.getDeclaredMethod("setIconImage", Image.class);
|
||||
setIconImageMethod.invoke(taskBar, image);
|
||||
} catch (Throwable ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ import javafx.stage.StageStyle;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.java.JavaManager;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
@@ -48,6 +49,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;
|
||||
@@ -55,14 +58,17 @@ import org.jackhuang.hmcl.ui.download.DownloadPage;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.main.LauncherSettingsPage;
|
||||
import org.jackhuang.hmcl.ui.main.RootPage;
|
||||
import org.jackhuang.hmcl.ui.terracotta.TerracottaPage;
|
||||
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.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@@ -72,6 +78,8 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
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 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
|
||||
@@ -84,20 +92,20 @@ 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());
|
||||
gameListPage.profilesProperty().bindContent(Profiles.profilesProperty());
|
||||
FXUtils.applyDragListener(gameListPage, ModpackHelper::isFileModpackByExtension, modpacks -> {
|
||||
File modpack = modpacks.get(0);
|
||||
Path modpack = modpacks.get(0);
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
|
||||
});
|
||||
return gameListPage;
|
||||
});
|
||||
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());
|
||||
@@ -105,7 +113,8 @@ 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() {
|
||||
}
|
||||
@@ -120,7 +129,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
|
||||
@@ -135,7 +155,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
|
||||
@@ -145,7 +176,23 @@ 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
|
||||
public static Node getTerracottaPage() {
|
||||
return terracottaPage.get();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
@@ -176,6 +223,18 @@ public final class Controllers {
|
||||
public static void initialize(Stage stage) {
|
||||
LOG.info("Start initializing application");
|
||||
|
||||
if (System.getProperty("prism.lcdtext") == null) {
|
||||
String fontAntiAliasing = globalConfig().getFontAntiAliasing();
|
||||
if ("lcd".equalsIgnoreCase(fontAntiAliasing)) {
|
||||
LOG.info("Enable sub-pixel antialiasing");
|
||||
System.getProperties().put("prism.lcdtext", "true");
|
||||
} else if ("gray".equalsIgnoreCase(fontAntiAliasing)
|
||||
|| OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && SCREEN.getOutputScaleX() > 1) {
|
||||
LOG.info("Disable sub-pixel antialiasing");
|
||||
System.getProperties().put("prism.lcdtext", "false");
|
||||
}
|
||||
}
|
||||
|
||||
Controllers.stage = stage;
|
||||
|
||||
stageSizeChangeListener = o -> {
|
||||
@@ -205,7 +264,11 @@ public final class Controllers {
|
||||
|
||||
if (targetProperty != null
|
||||
&& Controllers.stage != null
|
||||
&& !Controllers.stage.isIconified()) {
|
||||
&& !Controllers.stage.isIconified()
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4290
|
||||
&& (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS ||
|
||||
!Controllers.stage.isFullScreen() && !Controllers.stage.isMaximized())
|
||||
) {
|
||||
targetProperty.set(sourceProperty.get());
|
||||
}
|
||||
};
|
||||
@@ -278,16 +341,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();
|
||||
@@ -312,24 +375,43 @@ public final class Controllers {
|
||||
|
||||
if (JavaRuntime.CURRENT_VERSION < Metadata.MINIMUM_SUPPORTED_JAVA_VERSION) {
|
||||
Number shownTipVersion = null;
|
||||
|
||||
try {
|
||||
shownTipVersion = (Number) config().getShownTips().get(JAVA_VERSION_TIP);
|
||||
} catch (ClassCastException e) {
|
||||
LOG.warning("Invalid type for shown tips key: " + JAVA_VERSION_TIP, e);
|
||||
}
|
||||
|
||||
if (shownTipVersion == null || shownTipVersion.intValue() < Metadata.MINIMUM_SUPPORTED_JAVA_VERSION) {
|
||||
MessageDialogPane.Builder builder = new MessageDialogPane.Builder(i18n("fatal.deprecated_java_version"), null, MessageType.WARNING);
|
||||
String downloadLink = Metadata.getSuggestedJavaDownloadLink();
|
||||
if (downloadLink != null)
|
||||
builder.addHyperLink(i18n("fatal.deprecated_java_version.download_link", 21), downloadLink);
|
||||
builder.addHyperLink(
|
||||
i18n("fatal.deprecated_java_version.download_link", Metadata.RECOMMENDED_JAVA_VERSION),
|
||||
downloadLink
|
||||
);
|
||||
Controllers.dialog(builder
|
||||
.ok(() -> config().getShownTips().put(JAVA_VERSION_TIP, Metadata.MINIMUM_SUPPORTED_JAVA_VERSION))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether JIT is enabled in the current environment
|
||||
if (!JavaRuntime.CURRENT_JIT_ENABLED && !Boolean.TRUE.equals(config().getShownTips().get(JAVA_INTERPRETED_MODE_TIP))) {
|
||||
Controllers.dialog(new MessageDialogPane.Builder(i18n("warning.java_interpreted_mode"), i18n("message.warning"), MessageType.WARNING)
|
||||
.ok(null)
|
||||
.addCancel(i18n("button.do_not_show_again"), () ->
|
||||
config().getShownTips().put(JAVA_INTERPRETED_MODE_TIP, true))
|
||||
.build());
|
||||
}
|
||||
|
||||
// Check whether hardware acceleration is enabled
|
||||
if (!FXUtils.GPU_ACCELERATION_ENABLED && !Boolean.TRUE.equals(config().getShownTips().get(SOFTWARE_RENDERING))) {
|
||||
Controllers.dialog(new MessageDialogPane.Builder(i18n("warning.software_rendering"), i18n("message.warning"), MessageType.WARNING)
|
||||
.ok(null)
|
||||
.addCancel(i18n("button.do_not_show_again"), () ->
|
||||
config().getShownTips().put(SOFTWARE_RENDERING, true))
|
||||
.build());
|
||||
}
|
||||
|
||||
if (globalConfig().getAgreementVersion() < 1) {
|
||||
JFXDialogLayout agreementPane = new JFXDialogLayout();
|
||||
agreementPane.setHeading(new Label(i18n("launcher.agreement")));
|
||||
@@ -387,6 +469,38 @@ public final class Controllers {
|
||||
dialog(new MessageDialogPane.Builder(text, title, type).actionOrCancel(actionButton, cancel).build());
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
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(btnOk.textProperty(), i18n("button.ok.countdown", seconds - i)));
|
||||
}
|
||||
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, btnOk, () -> {
|
||||
timeline.stop();
|
||||
if (cancel != null)
|
||||
cancel.run();
|
||||
});
|
||||
timeline.play();
|
||||
}
|
||||
|
||||
public static CompletableFuture<String> prompt(String title, FutureCallback<String> onResult) {
|
||||
return prompt(title, onResult, "");
|
||||
}
|
||||
@@ -419,7 +533,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) {
|
||||
@@ -433,6 +551,10 @@ public final class Controllers {
|
||||
Controllers.getSettingsPage().showFeedback();
|
||||
Controllers.navigate(Controllers.getSettingsPage());
|
||||
break;
|
||||
case "hmcl://game/launch":
|
||||
Profile profile = Profiles.getSelectedProfile();
|
||||
Versions.launch(profile, profile.getSelectedVersion(), LauncherHelper::setKeep);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
FXUtils.openLink(href);
|
||||
@@ -450,6 +572,7 @@ public final class Controllers {
|
||||
downloadPage = null;
|
||||
accountListPage = null;
|
||||
settingsPage = null;
|
||||
terracottaPage = null;
|
||||
decorator = null;
|
||||
stage = null;
|
||||
scene = null;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -41,9 +41,12 @@ import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -59,9 +62,9 @@ import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
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;
|
||||
|
||||
public class GameCrashWindow extends Stage {
|
||||
private final Version version;
|
||||
@@ -129,7 +132,7 @@ public class GameCrashWindow extends Stage {
|
||||
|
||||
return pair(CrashReportAnalyzer.analyze(rawLog), crashReport != null ? CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport) : new HashSet<>());
|
||||
}), Task.supplyAsync(() -> {
|
||||
Path latestLog = repository.getRunDirectory(version.getId()).toPath().resolve("logs/latest.log");
|
||||
Path latestLog = repository.getRunDirectory(version.getId()).resolve("logs/latest.log");
|
||||
if (!Files.isReadable(latestLog)) {
|
||||
return pair(new HashSet<CrashReportAnalyzer.Result>(), new HashSet<String>());
|
||||
}
|
||||
@@ -283,13 +286,13 @@ public class GameCrashWindow extends Stage {
|
||||
alert.showAndWait();
|
||||
|
||||
return null;
|
||||
});
|
||||
}, Schedulers.javafx());
|
||||
}
|
||||
|
||||
private final class View extends VBox {
|
||||
|
||||
View() {
|
||||
setStyle("-fx-background-color: white");
|
||||
this.getStyleClass().add("game-crash-window");
|
||||
|
||||
HBox titlePane = new HBox();
|
||||
{
|
||||
@@ -379,7 +382,7 @@ public class GameCrashWindow extends Stage {
|
||||
TwoLineListItem gameDir = new TwoLineListItem();
|
||||
gameDir.getStyleClass().setAll("two-line-item-second-large");
|
||||
gameDir.setTitle(i18n("game.directory"));
|
||||
gameDir.setSubtitle(launchOptions.getGameDir().getAbsolutePath());
|
||||
gameDir.setSubtitle(launchOptions.getGameDir().toAbsolutePath().toString());
|
||||
FXUtils.installFastTooltip(gameDir, i18n("game.directory"));
|
||||
|
||||
TwoLineListItem javaDir = new TwoLineListItem();
|
||||
@@ -392,10 +395,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 -> {
|
||||
@@ -408,6 +414,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());
|
||||
@@ -419,7 +426,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");
|
||||
|
||||
@@ -36,7 +36,6 @@ import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
@@ -139,6 +138,9 @@ public class InstallerItem extends Control {
|
||||
case "forge":
|
||||
iconType = VersionIconType.FORGE;
|
||||
break;
|
||||
case "cleanroom":
|
||||
iconType = VersionIconType.CLEANROOM;
|
||||
break;
|
||||
case "liteloader":
|
||||
iconType = VersionIconType.CHICKEN;
|
||||
break;
|
||||
@@ -233,6 +235,7 @@ public class InstallerItem extends Control {
|
||||
InstallerItem fabric = new InstallerItem(FABRIC, style);
|
||||
InstallerItem fabricApi = new InstallerItem(FABRIC_API, style);
|
||||
InstallerItem forge = new InstallerItem(FORGE, style);
|
||||
InstallerItem cleanroom = new InstallerItem(CLEANROOM, style);
|
||||
InstallerItem neoForge = new InstallerItem(NEO_FORGE, style);
|
||||
InstallerItem liteLoader = new InstallerItem(LITELOADER, style);
|
||||
InstallerItem optiFine = new InstallerItem(OPTIFINE, style);
|
||||
@@ -240,11 +243,11 @@ public class InstallerItem extends Control {
|
||||
InstallerItem quiltApi = new InstallerItem(QUILT_API, style);
|
||||
|
||||
Map<InstallerItem, Set<InstallerItem>> incompatibleMap = new HashMap<>();
|
||||
mutualIncompatible(incompatibleMap, forge, fabric, quilt, neoForge);
|
||||
addIncompatibles(incompatibleMap, liteLoader, fabric, quilt, neoForge);
|
||||
addIncompatibles(incompatibleMap, optiFine, fabric, quilt, neoForge);
|
||||
addIncompatibles(incompatibleMap, fabricApi, forge, quiltApi, neoForge, liteLoader, optiFine);
|
||||
addIncompatibles(incompatibleMap, quiltApi, forge, fabric, fabricApi, neoForge, liteLoader, optiFine);
|
||||
mutualIncompatible(incompatibleMap, forge, fabric, quilt, neoForge, cleanroom);
|
||||
addIncompatibles(incompatibleMap, liteLoader, fabric, quilt, neoForge, cleanroom);
|
||||
addIncompatibles(incompatibleMap, optiFine, fabric, quilt, neoForge, cleanroom);
|
||||
addIncompatibles(incompatibleMap, fabricApi, forge, quiltApi, neoForge, liteLoader, optiFine, cleanroom);
|
||||
addIncompatibles(incompatibleMap, quiltApi, forge, fabric, fabricApi, neoForge, liteLoader, optiFine, cleanroom);
|
||||
|
||||
for (Map.Entry<InstallerItem, Set<InstallerItem>> entry : incompatibleMap.entrySet()) {
|
||||
InstallerItem item = entry.getKey();
|
||||
@@ -278,7 +281,7 @@ public class InstallerItem extends Control {
|
||||
game.versionProperty.set(new InstalledState(gameVersion, false, false));
|
||||
}
|
||||
|
||||
InstallerItem[] all = {game, forge, neoForge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi};
|
||||
InstallerItem[] all = {game, forge, neoForge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi, cleanroom};
|
||||
|
||||
for (InstallerItem item : all) {
|
||||
if (!item.resolvedStateProperty.isBound()) {
|
||||
@@ -294,6 +297,8 @@ public class InstallerItem extends Control {
|
||||
|
||||
if (gameVersion == null) {
|
||||
this.libraries = all;
|
||||
} else if (gameVersion.equals("1.12.2")) {
|
||||
this.libraries = new InstallerItem[]{game, forge, cleanroom, liteLoader, optiFine};
|
||||
} else if (GameVersionNumber.compare(gameVersion, "1.13") < 0) {
|
||||
this.libraries = new InstallerItem[]{game, forge, liteLoader, optiFine};
|
||||
} else {
|
||||
@@ -323,11 +328,7 @@ public class InstallerItem extends Control {
|
||||
Pane pane;
|
||||
if (control.style == Style.CARD) {
|
||||
pane = new VBox();
|
||||
|
||||
if (FXUtils.JAVAFX_MAJOR_VERSION == 8)
|
||||
holder = FXUtils.onWeakChange(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7));
|
||||
else
|
||||
holder = FXUtils.onWeakChangeAndOperate(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7));
|
||||
holder = FXUtils.onWeakChangeAndOperate(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7));
|
||||
} else {
|
||||
pane = new HBox();
|
||||
holder = null;
|
||||
@@ -394,7 +395,7 @@ public class InstallerItem extends Control {
|
||||
pane.getChildren().add(buttonsContainer);
|
||||
|
||||
JFXButton removeButton = new JFXButton();
|
||||
removeButton.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), -1));
|
||||
removeButton.setGraphic(SVG.CLOSE.createIcon());
|
||||
removeButton.getStyleClass().add("toggle-icon4");
|
||||
if (control.id.equals(MINECRAFT.getPatchId())) {
|
||||
removeButton.setVisible(false);
|
||||
@@ -415,8 +416,8 @@ public class InstallerItem extends Control {
|
||||
JFXButton installButton = new JFXButton();
|
||||
installButton.graphicProperty().bind(Bindings.createObjectBinding(() ->
|
||||
control.resolvedStateProperty.get() instanceof InstallableState ?
|
||||
SVG.ARROW_FORWARD.createIcon(Theme.blackFill(), -1) :
|
||||
SVG.UPDATE.createIcon(Theme.blackFill(), -1),
|
||||
SVG.ARROW_FORWARD.createIcon() :
|
||||
SVG.UPDATE.createIcon(),
|
||||
control.resolvedStateProperty
|
||||
));
|
||||
installButton.getStyleClass().add("toggle-icon4");
|
||||
|
||||
@@ -23,10 +23,11 @@ import javafx.collections.ObservableList;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.control.Control;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.construct.SpinnerPane.FAILED_ACTION;
|
||||
|
||||
public class ListPageBase<T> extends Control {
|
||||
public class ListPageBase<T> extends Control implements TransitionPane.Cacheable {
|
||||
private final ListProperty<T> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
|
||||
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading", false);
|
||||
private final StringProperty failedReason = new SimpleStringProperty(this, "failed");
|
||||
|
||||
@@ -27,7 +27,6 @@ import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
|
||||
public class ListPageSkin extends SkinBase<ListPage<?>> {
|
||||
@@ -64,12 +63,10 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
|
||||
vBox.setAlignment(Pos.BOTTOM_RIGHT);
|
||||
vBox.setPickOnBounds(false);
|
||||
|
||||
JFXButton btnAdd = new JFXButton();
|
||||
JFXButton btnAdd = FXUtils.newRaisedButton("");
|
||||
FXUtils.setLimitWidth(btnAdd, 40);
|
||||
FXUtils.setLimitHeight(btnAdd, 40);
|
||||
btnAdd.getStyleClass().add("jfx-button-raised-round");
|
||||
btnAdd.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
btnAdd.setGraphic(SVG.ADD.createIcon(Theme.whiteFill(), -1));
|
||||
btnAdd.setGraphic(SVG.ADD.createIcon());
|
||||
btnAdd.setOnAction(e -> skinnable.add());
|
||||
|
||||
JFXButton btnRefresh = new JFXButton();
|
||||
@@ -77,7 +74,7 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
|
||||
FXUtils.setLimitHeight(btnRefresh, 40);
|
||||
btnRefresh.getStyleClass().add("jfx-button-raised-round");
|
||||
btnRefresh.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
btnRefresh.setGraphic(SVG.REFRESH.createIcon(Theme.whiteFill(), -1));
|
||||
btnRefresh.setGraphic(SVG.REFRESH.createIcon());
|
||||
btnRefresh.setOnAction(e -> skinnable.refresh());
|
||||
|
||||
vBox.getChildren().setAll(btnAdd);
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.sun.jna.Pointer;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
@@ -28,6 +29,7 @@ import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Scene;
|
||||
@@ -37,17 +39,21 @@ import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.WindowEvent;
|
||||
import org.jackhuang.hmcl.game.GameDumpGenerator;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.game.Log;
|
||||
import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.CircularArrayList;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.platform.windows.Dwmapi;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinConstants;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinTypes;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@@ -84,6 +90,36 @@ public final class LogWindow extends Stage {
|
||||
private final LogWindowImpl impl;
|
||||
private final ManagedProcess gameProcess;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private Object windowsDarkModeListenerHolder;
|
||||
|
||||
{
|
||||
if (OperatingSystem.SYSTEM_VERSION.isAtLeast(OSVersion.WINDOWS_11) && NativeUtils.USE_JNA && Dwmapi.INSTANCE != null) {
|
||||
this.addEventFilter(WindowEvent.WINDOW_SHOWN, new EventHandler<>() {
|
||||
@Override
|
||||
public void handle(WindowEvent event) {
|
||||
LogWindow.this.removeEventFilter(WindowEvent.WINDOW_SHOWN, this);
|
||||
|
||||
windowsDarkModeListenerHolder = FXUtils.onWeakChangeAndOperate(Themes.darkModeProperty(), darkMode -> {
|
||||
if (LogWindow.this.isShowing()) {
|
||||
WindowsNativeUtils.getWindowHandle(LogWindow.this).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
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public LogWindow(ManagedProcess gameProcess) {
|
||||
this(gameProcess, new CircularArrayList<>());
|
||||
}
|
||||
@@ -168,6 +204,7 @@ public final class LogWindow extends Stage {
|
||||
LogWindowImpl() {
|
||||
getStyleClass().add("log-window");
|
||||
|
||||
listView.getProperties().put("no-smooth-scrolling", true);
|
||||
listView.setItems(FXCollections.observableList(new CircularArrayList<>(logs.size())));
|
||||
|
||||
for (int i = 0; i < LEVELS.length; i++) {
|
||||
@@ -186,7 +223,7 @@ public final class LogWindow extends Stage {
|
||||
}
|
||||
|
||||
private void onTerminateGame() {
|
||||
LauncherHelper.stopManagedProcesses();
|
||||
LogWindow.this.gameProcess.stop();
|
||||
}
|
||||
|
||||
private void onClear() {
|
||||
@@ -224,7 +261,7 @@ public final class LogWindow extends Stage {
|
||||
|
||||
try {
|
||||
if (gameProcess.isRunning()) {
|
||||
GameDumpGenerator.writeDumpTo(gameProcess.getPID(), dumpFile);
|
||||
GameDumpGenerator.writeDumpTo(gameProcess.getProcess().pid(), dumpFile);
|
||||
FXUtils.showFileInExplorer(dumpFile);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
@@ -264,7 +301,6 @@ public final class LogWindow extends Stage {
|
||||
|
||||
VBox vbox = new VBox(3);
|
||||
vbox.setPadding(new Insets(3, 0, 3, 0));
|
||||
vbox.setStyle("-fx-background-color: white");
|
||||
getChildren().setAll(vbox);
|
||||
|
||||
{
|
||||
@@ -286,8 +322,7 @@ public final class LogWindow extends Stage {
|
||||
HBox hBox = new HBox(3);
|
||||
for (int i = 0; i < LEVELS.length; i++) {
|
||||
ToggleButton button = new ToggleButton();
|
||||
button.setStyle("-fx-background-color: " + FXUtils.toWeb(LEVELS[i].getColor()) + ";");
|
||||
button.getStyleClass().add("log-toggle");
|
||||
button.getStyleClass().addAll("log-toggle", LEVELS[i].name().toLowerCase(Locale.ROOT));
|
||||
button.textProperty().bind(control.buttonText[i]);
|
||||
button.setSelected(true);
|
||||
control.showLevel[i].bind(button.selectedProperty());
|
||||
@@ -312,6 +347,7 @@ public final class LogWindow extends Stage {
|
||||
Holder<Object> lastCell = new Holder<>();
|
||||
listView.setCellFactory(x -> new ListCell<Log>() {
|
||||
{
|
||||
x.setSelectionModel(new NoneMultipleSelectionModel<>());
|
||||
getStyleClass().add("log-window-list-cell");
|
||||
Region clippedContainer = (Region) listView.lookup(".clipped-container");
|
||||
if (clippedContainer != null) {
|
||||
|
||||
@@ -38,6 +38,7 @@ public enum SVG {
|
||||
ADD_CIRCLE("M11 17H13V13H17V11H13V7H11V11H7V13H11V17ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
|
||||
ALPHA_CIRCLE("M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,1 11,7M11,9V11H13V9H11M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z"), // Not Material
|
||||
ARCHIVE("M12 18 16 14 14.6 12.6 13 14.2V10H11V14.2L9.4 12.6 8 14 12 18ZM5 8V19H19V8H5ZM5 21Q4.175 21 3.5875 20.4125T3 19V6.525Q3 6.175 3.1125 5.85T3.45 5.25L4.7 3.725Q4.975 3.375 5.3875 3.1875T6.25 3H17.75Q18.2 3 18.6125 3.1875T19.3 3.725L20.55 5.25Q20.775 5.525 20.8875 5.85T21 6.525V19Q21 19.825 20.4125 20.4125T19 21H5ZM5.4 6H18.6L17.75 5H6.25L5.4 6ZM12 13.5Z"),
|
||||
ARCHIVE_FILL("M12 18l4-4-1.4-1.4L13 14.2V10H11v4.2L9.4 12.6 8 14l4 4ZM5 21q-.825 0-1.4125-.5875T3 19V6.525q0-.35.1125-.675t.3375-.6L4.7 3.725q.275-.35.6875-.5375T6.25 3h11.5q.45 0 .8625.1875T19.3 3.725L20.55 5.25q.225.275.3375.6T21 6.525V19q0 .825-.5875 1.4125T19 21H5ZM5.4 6H18.6l-.85-1H6.25L5.4 6Z"),
|
||||
ARROW_BACK("M7.825 13 13.425 18.6 12 20 4 12 12 4 13.425 5.4 7.825 11H20V13H7.825Z"),
|
||||
ARROW_DROP_DOWN("M12 15 7 10H17L12 15Z"),
|
||||
ARROW_DROP_UP("M7 14 12 9 17 14H7Z"),
|
||||
@@ -54,13 +55,16 @@ public enum SVG {
|
||||
DELETE("M7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM9 17H11V8H9V17ZM13 17H15V8H13V17ZM7 6V19 6Z"),
|
||||
DELETE_FOREVER("M9.4 16.5 12 13.9 14.6 16.5 16 15.1 13.4 12.5 16 9.9 14.6 8.5 12 11.1 9.4 8.5 8 9.9 10.6 12.5 8 15.1 9.4 16.5ZM7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM7 6V19 6Z"),
|
||||
DEPLOYED_CODE("M11 19.425V12.575L5 9.1V15.95L11 19.425ZM13 19.425 19 15.95V9.1L13 12.575V19.425ZM12 10.85 17.925 7.425 12 4 6.075 7.425 12 10.85ZM4 17.7Q3.525 17.425 3.2625 16.975T3 15.975V8.025Q3 7.475 3.2625 7.025T4 6.3L11 2.275Q11.475 2 12 2T13 2.275L20 6.3Q20.475 6.575 20.7375 7.025T21 8.025V15.975Q21 16.525 20.7375 16.975T20 17.7L13 21.725Q12.525 22 12 22T11 21.725L4 17.7ZM12 12Z"),
|
||||
DEPLOYED_CODE_FILL("M11 21.725 4 17.7q-.475-.275-.7375-.725T3 15.975V8.025q0-.55.2625-1T4 6.3l7-4.025Q11.475 2 12 2t1 .275L20 6.3q.475.275.7375.725t.2625 1v7.95q0 .55-.2625 1T20 17.7l-7 4.025Q12.525 22 12 22t-1-.275Zm0-9.15v6.85L12 20l1-.575v-6.85L19 9.1V8.05l-1.075-.625L12 10.85 6.075 7.425 5 8.05V9.1l6 3.475Z"),
|
||||
DOWNLOAD("M12 16 7 11 8.4 9.55 11 12.15V4H13V12.15L15.6 9.55 17 11 12 16ZM6 20Q5.175 20 4.5875 19.4125T4 18V15H6V18H18V15H20V18Q20 18.825 19.4125 19.4125T18 20H6Z"),
|
||||
DRESSER("M4 21V5Q4 4.175 4.5875 3.5875T6 3H18Q18.825 3 19.4125 3.5875T20 5V21H18V19H6V21H4ZM6 11H11V5H6V11ZM13 7H18V5H13V7ZM13 11H18V9H13V11ZM10 16H14V14H10V16ZM6 13V17H18V13H6ZM6 13V17 13Z"),
|
||||
EDIT("M5 19H6.425L16.2 9.225 14.775 7.8 5 17.575V19ZM3 21V16.75L16.2 3.575Q16.5 3.3 16.8625 3.15T17.625 3Q18.025 3 18.4 3.15T19.05 3.6L20.425 5Q20.725 5.275 20.8625 5.65T21 6.4Q21 6.8 20.8625 7.1625T20.425 7.825L7.25 21H3ZM19 6.4 17.6 5 19 6.4ZM15.475 8.525 14.775 7.8 16.2 9.225 15.475 8.525Z"),
|
||||
ERROR("M12 17Q12.425 17 12.7125 16.7125T13 16Q13 15.575 12.7125 15.2875T12 15Q11.575 15 11.2875 15.2875T11 16Q11 16.425 11.2875 16.7125T12 17ZM11 13H13V7H11V13ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
|
||||
EXPLORE("M12 12Zm0 8q-3.325 0-5.6625-2.3375T4 12Q4 8.675 6.3375 6.3375T12 4q3.325-0 5.6625 2.3375T20 12q0 3.325-2.3375 5.6625T12 20Zm0 2q2.075-0 3.9-.7875t3.175-2.1375q1.35-1.35 2.1375-3.175T22 12q-0-2.075-.7875-3.9T19.075 4.925q-1.35-1.35-3.175-2.1375T12 2q-2.075 0-3.9.7875T4.925 4.925Q3.575 6.275 2.7875 8.1T2 12q0 2.075.7875 3.9T4.925 19.075q1.35 1.35 3.175 2.1375T12 22Zm0-8.5q.625 0 1.0625-.4375T13.5 12t-.4375-1.0625T12 10.5t-1.0625.4375T10.5 12t.4375 1.0625T12 13.5Zm-4.5 3 2-7 7-2-2 7-7 2Z"),
|
||||
EXTENSION("M8.8 21H5Q4.175 21 3.5875 20.4125T3 19V15.2Q4.2 15.2 5.1 14.4375T6 12.5Q6 11.325 5.1 10.5625T3 9.8V6Q3 5.175 3.5875 4.5875T5 4H9Q9 2.95 9.725 2.225T11.5 1.5Q12.55 1.5 13.275 2.225T14 4H18Q18.825 4 19.4125 4.5875T20 6V10Q21.05 10 21.775 10.725T22.5 12.5Q22.5 13.55 21.775 14.275T20 15V19Q20 19.825 19.4125 20.4125T18 21H14.2Q14.2 19.75 13.4125 18.875T11.5 18Q10.375 18 9.5875 18.875T8.8 21ZM5 19H7.125Q7.725 17.35 9.05 16.675T11.5 16Q12.625 16 13.95 16.675T15.875 19H18V13H20Q20.2 13 20.35 12.85T20.5 12.5Q20.5 12.3 20.35 12.15T20 12H18V6H12V4Q12 3.8 11.85 3.65T11.5 3.5Q11.3 3.5 11.15 3.65T11 4V6H5V8.2Q6.35 8.7 7.175 9.875T8 12.5Q8 13.925 7.175 15.1T5 16.8V19ZM11.5 12.5Z"),
|
||||
EXTENSION_FILL("M8.8 21H5q-.825 0-1.4125-.5875T3 19V15.2q1.2 0 2.1-.7625T6 12.5q0-1.175-.9-1.9375T3 9.8V6q0-.825.5875-1.4125T5 4H9q0-1.05.725-1.775T11.5 1.5q1.05 0 1.775.725T14 4h4q.825 0 1.4125.5875T20 6v4q1.05 0 1.775.725T22.5 12.5q0 1.05-.725 1.775T20 15v4q0 .825-.5875 1.4125T18 21H14.2q0-1.25-.7875-2.125T11.5 18q-1.125 0-1.9125.875T8.8 21Z"),
|
||||
FEEDBACK("M12 15Q12.425 15 12.7125 14.7125T13 14Q13 13.575 12.7125 13.2875T12 13Q11.575 13 11.2875 13.2875T11 14Q11 14.425 11.2875 14.7125T12 15ZM11 11H13V5H11V11ZM2 22V4Q2 3.175 2.5875 2.5875T4 2H20Q20.825 2 21.4125 2.5875T22 4V16Q22 16.825 21.4125 17.4125T20 18H6L2 22ZM5.15 16H20V4H4V17.125L5.15 16ZM4 16V4 16Z"),
|
||||
FEEDBACK_FILL("M2 22V4q0-.825.5875-1.4125T4 2H20q.825 0 1.4125.5875T22 4V16q0 .825-.5875 1.4125T20 18H6L2 22Zm10-7q.425 0 .7125-.2875T13 14t-.2875-.7125T12 13t-.7125.2875T11 14t.2875.7125T12 15Zm-1-4h2V5H11v6Z"),
|
||||
FOLDER("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8V18Q22 18.825 21.4125 19.4125T20 20H4ZM4 18H20V8H11.175L9.175 6H4V18ZM4 18V6 18Z"),
|
||||
FOLDER_COPY("M3 21Q2.175 21 1.5875 20.4125T1 19V6H3V19H20V21H3ZM7 17Q6.175 17 5.5875 16.4125T5 15V4Q5 3.175 5.5875 2.5875T7 2H12L14 4H21Q21.825 4 22.4125 4.5875T23 6V15Q23 15.825 22.4125 16.4125T21 17H7ZM7 15H21V6H13.175L11.175 4H7V15ZM7 15V4 15Z"),
|
||||
FOLDER_OPEN("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8H11.175L9.175 6H4V18L6.4 10H23.5L20.925 18.575Q20.725 19.225 20.1875 19.6125T19 20H4ZM6.1 18H19L20.8 12H7.9L6.1 18ZM6.1 18 7.9 12 6.1 18ZM4 8V6 8Z"),
|
||||
@@ -69,16 +73,20 @@ public enum SVG {
|
||||
FOR_YOU("M12 12Q14.025 12 16.225 11.5875T20 10.5V20.5Q18.5 21.175 16.35 21.5875T12 22Q9.8 22 7.65 21.5875T4 20.5V10.5Q5.575 11.175 7.775 11.5875T12 12ZM18 19V13.25Q16.75 13.6 15.1125 13.8T12 14Q10.525 14 8.8875 13.8T6 13.25V19Q7.25 19.45 8.875 19.725T12 20Q13.5 20 15.125 19.725T18 19ZM12 2Q13.65 2 14.825 3.175T16 6Q16 7.65 14.825 8.825T12 10Q10.35 10 9.175 8.825T8 6Q8 4.35 9.175 3.175T12 2ZM12 8Q12.825 8 13.4125 7.4125T14 6Q14 5.175 13.4125 4.5875T12 4Q11.175 4 10.5875 4.5875T10 6Q10 6.825 10.5875 7.4125T12 8ZM12 6ZM12 16.625Z"),
|
||||
GAMEPAD("M12 7.65ZM16.35 12ZM7.65 12ZM12 16.35ZM12 10.5 9 7.5V2H15V7.5L12 10.5ZM16.5 15 13.5 12 16.5 9H22V15H16.5ZM2 15V9H7.5L10.5 12 7.5 15H2ZM9 22V16.5L12 13.5 15 16.5V22H9ZM12 7.65 13 6.65V4H11V6.65L12 7.65ZM4 13H6.65L7.65 12 6.65 11H4V13ZM11 20H13V17.35L12 16.35 11 17.35V20ZM17.35 13H20V11H17.35L16.35 12 17.35 13Z"),
|
||||
GLOBE_BOOK("M3.075 13Q3.05 12.75 3.0375 12.5T3.025 12Q3.025 10.125 3.725 8.4875T5.65 5.6375Q6.875 4.425 8.5 3.7125T12 3Q13.875 3 15.5125 3.7125T18.3625 5.6375Q19.575 6.85 20.2875 8.4875T21 12Q21 12.25 20.9875 12.5T20.95 13H18.925Q18.975 12.75 18.9875 12.5T19 12Q19 11.75 18.9875 11.5T18.925 11H15.975Q16 11.25 16 11.5V12.5Q16 12.75 15.975 13H14V12.175Q14 11.875 13.9875 11.575T13.95 11H10.075Q10.05 11.275 10.0375 11.575T10.025 12.175V13H8.05Q8.025 12.75 8.025 12.5V11.5Q8.025 11.25 8.05 11H5.1Q5.05 11.25 5.0375 11.5T5.025 12Q5.025 12.25 5.0375 12.5T5.1 13H3.075ZM5.7 9H8.275Q8.475 7.925 8.775 7.0625T9.425 5.5Q8.225 5.95 7.25 6.8625T5.7 9ZM10.35 9H13.65Q13.4 7.925 13.025 6.9T12 5Q11.35 5.875 10.9625 6.9T10.35 9ZM15.75 9H18.325Q17.75 7.775 16.7625 6.8625T14.575 5.5Q14.925 6.25 15.2375 7.0875T15.75 9ZM11 21V20Q11 18.75 10.125 17.875T8 17H2V15H8Q9.2 15 10.2375 15.525T12 17Q12.725 16.05 13.7625 15.525T16 15H22V17H16Q14.75 17 13.875 17.875T13 20V21H11Z"),
|
||||
GRAPH2("M5 22q-1.25 0-2.125-.875T2 19q0-.975.5625-1.75T4 16.175V14q0-1.25.875-2.125T7 11h4V7.825q-.875-.3-1.4375-1.075T9 5q0-1.25.875-2.125T12 2t2.125.875T15 5q0 .975-.5625 1.75T13 7.825V11h4q1.25 0 2.125.875T20 14v2.175q.875.3 1.4375 1.075T22 19q0 1.25-.875 2.125T19 22t-2.125-.875T16 19q0-.975.5625-1.75T18 16.175V14q0-.425-.2875-.7125T17 13H13v3.175q.875.3 1.4375 1.075T15 19q0 1.25-.875 2.125T12 22t-2.125-.875T9 19q0-.975.5625-1.75T11 16.175V13H7q-.425 0-.7125.2875T6 14v2.175q.875.3 1.4375 1.075T8 19q0 1.25-.875 2.125T5 22Zm0-2q.425 0 .7125-.2875T6 19q0-.425-.2875-.7125T5 18q-.425 0-.7125.2875T4 19q0 .425.2875.7125T5 20Zm7 0q.425 0 .7125-.2875T13 19q0-.425-.2875-.7125T12 18q-.425 0-.7125.2875T11 19q0 .425.2875.7125T12 20Zm7 0q.425 0 .7125-.2875T20 19q0-.425-.2875-.7125T19 18q-.425 0-.7125.2875T18 19q0 .425.2875.7125T19 20ZM12 6q.425 0 .7125-.2875T13 5q0-.425-.2875-.7125T12 4q-.425 0-.7125.2875T11 5q0 .425.2875.7125T12 6Z"),
|
||||
HELP("M11.95 18Q12.475 18 12.8375 17.6375T13.2 16.75Q13.2 16.225 12.8375 15.8625T11.95 15.5Q11.425 15.5 11.0625 15.8625T10.7 16.75Q10.7 17.275 11.0625 17.6375T11.95 18ZM11.05 14.15H12.9Q12.9 13.325 13.0875 12.85T14.15 11.55Q14.8 10.9 15.175 10.3125T15.55 8.9Q15.55 7.5 14.525 6.75T12.1 6Q10.675 6 9.7875 6.75T8.55 8.55L10.2 9.2Q10.325 8.75 10.7625 8.225T12.1 7.7Q12.9 7.7 13.3 8.1375T13.7 9.1Q13.7 9.6 13.4 10.0375T12.65 10.85Q11.55 11.825 11.3 12.325T11.05 14.15ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
|
||||
HELP_FILL("M11.95 18q.525 0 .8875-.3625T13.2 16.75q0-.525-.3625-.8875T11.95 15.5q-.525 0-.8875.3625T10.7 16.75q0 .525.3625.8875T11.95 18Zm-.9-3.85H12.9q0-.825.1875-1.3t1.0625-1.3q.65-.65 1.025-1.2375T15.55 8.9q0-1.4-1.025-2.15T12.1 6q-1.425 0-2.3125.75T8.55 8.55l1.65.65q.125-.45.5625-.975T12.1 7.7q.8 0 1.2.4375t.4.9625q0 .5-.3.9375t-.75.8125q-1.1.975-1.35 1.475t-.25 1.825ZM12 22q-2.075 0-3.9-.7875T4.925 19.075q-1.35-1.35-2.1375-3.175T2 12q0-2.075.7875-3.9T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2q2.075 0 3.9.7875T19.075 4.925q1.35 1.35 2.1375 3.175T22 12q0 2.075-.7875 3.9T19.075 19.075q-1.35 1.35-3.175 2.1375T12 22Z"),
|
||||
HOME("M6 19H9V13H15V19H18V10L12 5.5 6 10V19ZM4 21V9L12 3 20 9V21H13V15H11V21H4ZM12 12.25Z"),
|
||||
HOST("M4 21Q3.175 21 2.5875 20.4125T2 19V5Q2 4.175 2.5875 3.5875T4 3H9Q9.825 3 10.4125 3.5875T11 5V19Q11 19.825 10.4125 20.4125T9 21H4ZM15 21Q14.175 21 13.5875 20.4125T13 19V5Q13 4.175 13.5875 3.5875T15 3H20Q20.825 3 21.4125 3.5875T22 5V19Q22 19.825 21.4125 20.4125T20 21H15ZM4 19H9V5H4V19ZM15 19H20V5H15V19ZM5 15H8V13H5V15ZM16 15H19V13H16V15ZM5 12H8V10H5V12ZM16 12H19V10H16V12ZM5 9H8V7H5V9ZM16 9H19V7H16V9ZM4 19H9 4ZM15 19H20 15Z"),
|
||||
INFO("M11 17H13V11H11V17ZM12 9Q12.425 9 12.7125 8.7125T13 8Q13 7.575 12.7125 7.2875T12 7Q11.575 7 11.2875 7.2875T11 8Q11 8.425 11.2875 8.7125T12 9ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
|
||||
INFO_FILL("M12 22q2.075-0 3.9-.7875t3.175-2.1375q1.35-1.35 2.1375-3.175T22 12q-0-2.075-.7875-3.9T19.075 4.925q-1.35-1.35-3.175-2.1375T12 2q-2.075 0-3.9.7875T4.925 4.925Q3.575 6.275 2.7875 8.1T2 12q0 2.075.7875 3.9T4.925 19.075q1.35 1.35 3.175 2.1375T12 22ZM12 9q-.425 0-.7125-.2875T11 8t.2875-.7125T12 7q.425-0 .7125.2875T13 8t-.2875.7125T12 9Zm-1 8V11h2v6H11Z"),
|
||||
KEYBOARD_ARROW_DOWN("M12 15.4 6 9.4 7.4 8 12 12.6 16.6 8 18 9.4 12 15.4Z"),
|
||||
KEYBOARD_ARROW_UP("M12 10.8 7.4 15.4 6 14 12 8 18 14 16.6 15.4 12 10.8Z"),
|
||||
LANDSCAPE("M1 18l6-8 4.5 6H19L14 9.35l-2.5 3.3L10.25 11 14 6l9 12H1Zm13.025-2ZM5 16H9L7 13.325 5 16ZH9 5Z"),
|
||||
LIST("M7 9V7H21V9H7ZM7 13V11H21V13H7ZM7 17V15H21V17H7ZM4 9Q3.575 9 3.2875 8.7125T3 8Q3 7.575 3.2875 7.2875T4 7Q4.425 7 4.7125 7.2875T5 8Q5 8.425 4.7125 8.7125T4 9ZM4 13Q3.575 13 3.2875 12.7125T3 12Q3 11.575 3.2875 11.2875T4 11Q4.425 11 4.7125 11.2875T5 12Q5 12.425 4.7125 12.7125T4 13ZM4 17Q3.575 17 3.2875 16.7125T3 16Q3 15.575 3.2875 15.2875T4 15Q4.425 15 4.7125 15.2875T5 16Q5 16.425 4.7125 16.7125T4 17Z"),
|
||||
LISTS("M2 20V16H6V20H2ZM8 20V16H22V20H8ZM2 14V10H6V14H2ZM8 14V10H22V14H8ZM2 8V4H6V8H2ZM8 8V4H22V8H8Z"),
|
||||
LOCAL_CAFE("M4 21V19H20V21H4ZM8 17Q6.35 17 5.175 15.825T4 13V3H20Q20.825 3 21.4125 3.5875T22 5V8Q22 8.825 21.4125 9.4125T20 10H18V13Q18 14.65 16.825 15.825T14 17H8ZM8 15H14Q14.825 15 15.4125 14.4125T16 13V5H6V13Q6 13.825 6.5875 14.4125T8 15ZM18 8H20V5H18V8ZM8 15H6 16 8Z"),
|
||||
LOCAL_CAFE_FILL("M4 21V19H20v2H4Zm4-4q-1.65 0-2.825-1.175T4 13V3H20q.825 0 1.4125.5875T22 5V8q0 .825-.5875 1.4125T20 10H18v3q0 1.65-1.175 2.825T14 17H8ZM18 8h2V5H18V8Z"),
|
||||
LOCATION_CITY("M3 21V7H9V5l3-3 3 3v6h6V21H3Zm2-2H7V17H5v2Zm0-4H7V13H5v2Zm0-4H7V9H5v2Zm6 8h2V17H11v2Zm0-4h2V13H11v2Zm0-4h2V9H11v2Zm0-4h2V5H11V7Zm6 12h2V17H17v2Zm0-4h2V13H17v2Z"),
|
||||
MENU("M3 18V16H21V18H3ZM3 13V11H21V13H3ZM3 8V6H21V8H3Z"),
|
||||
MICROSOFT("M4 20H22v2H4V13H20v7h2V4H20v7H4V4h7V20h2V4h9V2H2V22H4"), // Not Material
|
||||
@@ -90,6 +98,7 @@ public enum SVG {
|
||||
OUTPUT("M5 21Q4.175 21 3.5875 20.4125T3 19V5Q3 4.175 3.5875 3.5875T5 3H19Q19.825 3 20.4125 3.5875T21 5V7H19V5H5V19H19V17H21V19Q21 19.825 20.4125 20.4125T19 21H5ZM17 17 15.6 15.6 18.175 13H9V11H18.175L15.6 8.4 17 7 22 12 17 17Z"),
|
||||
PACKAGE("M10 9.75 12 8.75 14 9.75V5H10V9.75ZM7 17V15H12V17H7ZM5 21Q4.175 21 3.5875 20.4125T3 19V5Q3 4.175 3.5875 3.5875T5 3H19Q19.825 3 20.4125 3.5875T21 5V19Q21 19.825 20.4125 20.4125T19 21H5ZM5 5V19 5ZM5 19H19V5H16V13L12 11 8 13V5H5V19Z"),
|
||||
PACKAGE2("M11 19.425V12.575L5 9.1V15.95L11 19.425ZM13 19.425 19 15.95V9.1L13 12.575V19.425ZM11 21.725 4 17.7Q3.525 17.425 3.2625 16.975T3 15.975V8.025Q3 7.475 3.2625 7.025T4 6.3L11 2.275Q11.475 2 12 2T13 2.275L20 6.3Q20.475 6.575 20.7375 7.025T21 8.025V15.975Q21 16.525 20.7375 16.975T20 17.7L13 21.725Q12.525 22 12 22T11 21.725ZM16 8.525 17.925 7.425 12 4 10.05 5.125 16 8.525ZM12 10.85 13.95 9.725 8.025 6.3 6.075 7.425 12 10.85Z"),
|
||||
PACKAGE2_FILL("M11 21.725v-9.15L3 7.95v8.025q0 .55.2625 1T4 17.7l7 4.025Zm2 0L20 17.7q.475-.275.7375-.725t.2625-1V7.95l-8 4.625v9.15Zm3.975-13.75 2.95-1.725L13 2.275Q12.525 2 12 2t-1 .275L9.025 3.4l7.95 4.575ZM12 10.85l2.975-1.7L7.05 4.55l-3 1.725L12 10.85Z"),
|
||||
PERSON("M12 12Q10.35 12 9.175 10.825T8 8Q8 6.35 9.175 5.175T12 4Q13.65 4 14.825 5.175T16 8Q16 9.65 14.825 10.825T12 12ZM4 20V17.2Q4 16.35 4.4375 15.6375T5.6 14.55Q7.15 13.775 8.75 13.3875T12 13Q13.65 13 15.25 13.3875T18.4 14.55Q19.125 14.925 19.5625 15.6375T20 17.2V20H4ZM6 18H18V17.2Q18 16.925 17.8625 16.7T17.5 16.35Q16.15 15.675 14.775 15.3375T12 15Q10.6 15 9.225 15.3375T6.5 16.35Q6.275 16.475 6.1375 16.7T6 17.2V18ZM12 10Q12.825 10 13.4125 9.4125T14 8Q14 7.175 13.4125 6.5875T12 6Q11.175 6 10.5875 6.5875T10 8Q10 8.825 10.5875 9.4125T12 10ZM12 8ZM12 18Z"),
|
||||
PUBLIC("M12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM11 19.95V18Q10.175 18 9.5875 17.4125T9 16V15L4.2 10.2Q4.125 10.65 4.0625 11.1T4 12Q4 15.025 5.9875 17.3T11 19.95ZM17.9 17.4Q18.925 16.275 19.4625 14.8875T20 12Q20 9.55 18.6375 7.525T15 4.6V5Q15 5.825 14.4125 6.4125T13 7H11V9Q11 9.425 10.7125 9.7125T10 10H8V12H14Q14.425 12 14.7125 12.2875T15 13V16H16Q16.65 16 17.175 16.3875T17.9 17.4Z"),
|
||||
REFRESH("M12 20Q8.65 20 6.325 17.675T4 12Q4 8.65 6.325 6.325T12 4Q13.725 4 15.3 4.7125T18 6.75V4H20V11H13V9H17.2Q16.4 7.6 15.0125 6.8T12 6Q9.5 6 7.75 7.75T6 12Q6 14.5 7.75 16.25T12 18Q13.925 18 15.475 16.9T17.65 14H19.75Q19.05 16.65 16.9 18.325T12 20Z"),
|
||||
@@ -97,13 +106,17 @@ public enum SVG {
|
||||
RESTORE("M12 21Q8.55 21 5.9875 18.7125T3.05 13H5.1Q5.45 15.6 7.4125 17.3T12 19Q14.925 19 16.9625 16.9625T19 12Q19 9.075 16.9625 7.0375T12 5Q10.275 5 8.775 5.8T6.25 8H9V10H3V4H5V6.35Q6.275 4.75 8.1125 3.875T12 3Q13.875 3 15.5125 3.7125T18.3625 5.6375Q19.575 6.85 20.2875 8.4875T21 12Q21 13.875 20.2875 15.5125T18.3625 18.3625Q17.15 19.575 15.5125 20.2875T12 21Z"), // Not Material
|
||||
ROCKET_LAUNCH("M5.65 10.025 7.6 10.85Q7.95 10.15 8.325 9.5T9.15 8.2L7.75 7.925 5.65 10.025ZM9.2 12.1 12.05 14.925Q13.1 14.525 14.3 13.7T16.55 11.825Q18.3 10.075 19.2875 7.9375T20.15 4Q18.35 3.875 16.2 4.8625T12.3 7.6Q11.25 8.65 10.425 9.85T9.2 12.1ZM13.65 10.475Q13.075 9.9 13.075 9.0625T13.65 7.65Q14.225 7.075 15.075 7.075T16.5 7.65Q17.075 8.225 17.075 9.0625T16.5 10.475Q15.925 11.05 15.075 11.05T13.65 10.475ZM14.125 18.5 16.225 16.4 15.95 15Q15.3 15.45 14.65 15.8125T13.3 16.525L14.125 18.5ZM21.95 2.175Q22.425 5.2 21.3625 8.0625T17.7 13.525L18.2 16Q18.3 16.5 18.15 16.975T17.65 17.8L13.45 22 11.35 17.075 7.075 12.8 2.15 10.7 6.325 6.5Q6.675 6.15 7.1625 6T8.15 5.95L10.625 6.45Q13.225 3.85 16.075 2.775T21.95 2.175ZM3.925 15.975Q4.8 15.1 6.0625 15.0875T8.2 15.95Q9.075 16.825 9.0625 18.0875T8.175 20.225Q7.55 20.85 6.0875 21.3T2.05 22.1Q2.4 19.525 2.85 18.0625T3.925 15.975ZM5.35 17.375Q5.1 17.625 4.85 18.2875T4.5 19.625Q5.175 19.525 5.8375 19.2875T6.75 18.8Q7.05 18.5 7.075 18.075T6.8 17.35Q6.5 17.05 6.075 17.0625T5.35 17.375Z"),
|
||||
SCHEMA("M4 23V17H6.5V15H4V9H6.5V7H4V1h7V7H8.5V9H11v2h3V9h7v6H14V13H11v2H8.5v2H11v6H4Zm2-2H9V19H6v2Zm0-8H9V11H6v2Zm10 0h3V11H16v2ZM6 5H9V3H6V5ZM7.5 4Zm0 8Zm10 0Zm-10 8Z"),
|
||||
SCHEMA_FILL("M4 23V17H6.5V15H4V9H6.5V7H4V1h7V7H8.5V9H11v2h3V9h7v6H14V13H11v2H8.5v2H11v6H4Z"),
|
||||
SCREENSHOT_MONITOR("M15 16H19V12H17.5V14.5H15V16ZM5 10H6.5V7.5H9V6H5V10ZM8 21V19H4Q3.175 19 2.5875 18.4125T2 17V5Q2 4.175 2.5875 3.5875T4 3H20Q20.825 3 21.4125 3.5875T22 5V17Q22 17.825 21.4125 18.4125T20 19H16V21H8ZM4 17H20V5H4V17ZM4 17V5 17Z"),
|
||||
SCRIPT("M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z"), // Not Material
|
||||
SEARCH("M19.6 21 13.3 14.7Q12.55 15.3 11.575 15.65T9.5 16Q6.775 16 4.8875 14.1125T3 9.5Q3 6.775 4.8875 4.8875T9.5 3Q12.225 3 14.1125 4.8875T16 9.5Q16 10.6 15.65 11.575T14.7 13.3L21 19.6 19.6 21ZM9.5 14Q11.375 14 12.6875 12.6875T14 9.5Q14 7.625 12.6875 6.3125T9.5 5Q7.625 5 6.3125 6.3125T5 9.5Q5 11.375 6.3125 12.6875T9.5 14Z"),
|
||||
SELECT_ALL("M7 17V7H17V17H7ZM9 15H15V9H9V15ZM5 19V21Q4.175 21 3.5875 20.4125T3 19H5ZM3 17V15H5V17H3ZM3 13V11H5V13H3ZM3 9V7H5V9H3ZM5 5H3Q3 4.175 3.5875 3.5875T5 3V5ZM7 21V19H9V21H7ZM7 5V3H9V5H7ZM11 21V19H13V21H11ZM11 5V3H13V5H11ZM15 21V19H17V21H15ZM15 5V3H17V5H15ZM19 21V19H21Q21 19.825 20.4125 20.4125T19 21ZM19 17V15H21V17H19ZM19 13V11H21V13H19ZM19 9V7H21V9H19ZM19 5V3Q19.825 3 20.4125 3.5875T21 5H19Z"),
|
||||
SETTINGS("M19.43 12.98C19.47 12.66 19.5 12.34 19.5 12 19.5 11.66 19.47 11.34 19.43 11.02L21.54 9.37C21.73 9.22 21.78 8.95 21.66 8.73L19.66 5.27C19.57 5.11 19.4 5.02 19.22 5.02 19.16 5.02 19.1 5.03 19.05 5.05L16.56 6.05C16.04 5.65 15.48 5.32 14.87 5.07L14.49 2.42C14.46 2.18 14.25 2 14 2H10C9.75 2 9.54 2.18 9.51 2.42L9.13 5.07C8.52 5.32 7.96 5.66 7.44 6.05L4.95 5.05C4.89 5.03 4.83 5.02 4.77 5.02 4.6 5.02 4.43 5.11 4.34 5.27L2.34 8.73C2.21 8.95 2.27 9.22 2.46 9.37L4.57 11.02C4.53 11.34 4.5 11.67 4.5 12 4.5 12.33 4.53 12.66 4.57 12.98L2.46 14.63C2.27 14.78 2.22 15.05 2.34 15.27L4.34 18.73C4.43 18.89 4.6 18.98 4.78 18.98 4.84 18.98 4.9 18.97 4.95 18.95L7.44 17.95C7.96 18.35 8.52 18.68 9.13 18.93L9.51 21.58C9.54 21.82 9.75 22 10 22H14C14.25 22 14.46 21.82 14.49 21.58L14.87 18.93C15.48 18.68 16.04 18.34 16.56 17.95L19.05 18.95C19.11 18.97 19.17 18.98 19.23 18.98 19.4 18.98 19.57 18.89 19.66 18.73L21.66 15.27C21.78 15.05 21.73 14.78 21.54 14.63L19.43 12.98ZM17.45 11.27C17.49 11.58 17.5 11.79 17.5 12 17.5 12.21 17.48 12.43 17.45 12.73L17.31 13.86 18.2 14.56 19.28 15.4 18.58 16.61 17.31 16.1 16.27 15.68 15.37 16.36C14.94 16.68 14.53 16.92 14.12 17.09L13.06 17.52 12.9 18.65 12.7 20H11.3L11.11 18.65 10.95 17.52 9.89 17.09C9.46 16.91 9.06 16.68 8.66 16.38L7.75 15.68 6.69 16.11 5.42 16.62 4.72 15.41 5.8 14.57 6.69 13.87 6.55 12.74C6.52 12.43 6.5 12.2 6.5 12S6.52 11.57 6.55 11.27L6.69 10.14 5.8 9.44 4.72 8.6 5.42 7.39 6.69 7.9 7.73 8.32 8.63 7.64C9.06 7.32 9.47 7.08 9.88 6.91L10.94 6.48 11.1 5.35 11.3 4H12.69L12.88 5.35 13.04 6.48 14.1 6.91C14.53 7.09 14.93 7.32 15.33 7.62L16.24 8.32 17.3 7.89 18.57 7.38 19.27 8.59 18.2 9.44 17.31 10.14 17.45 11.27ZM12 8C9.79 8 8 9.79 8 12S9.79 16 12 16 16 14.21 16 12 14.21 8 12 8ZM12 14C10.9 14 10 13.1 10 12S10.9 10 12 10 14 10.9 14 12 13.1 14 12 14Z"), // Material Icons
|
||||
SETTINGS_FILL("M9.25 22l-.4-3.2q-.325-.125-.6125-.3t-.5625-.375L4.7 19.375l-2.75-4.75 2.575-1.95Q4.5 12.5 4.5 12.3375v-.675q0-.1625.025-.3375L1.95 9.375 4.7 4.625l2.975 1.25q.275-.2.575-.375t.6-.3L9.25 2h5.5l.4 3.2q.325.125.6125.3t.5625.375L19.3 4.625l2.75 4.75-2.575 1.95q.025.175.025.3375v.675q0 .1625-.05.3375l2.575 1.95-2.75 4.75-2.95-1.25q-.275.2-.575.375t-.6.3l-.4 3.2H9.25Zm2.8-6.5q1.45 0 2.475-1.025T15.55 12 14.525 9.525 12.05 8.5q-1.475 0-2.4875 1.025T8.55 12q0 1.45 1.0125 2.475T12.05 15.5Z"), // Material Icons
|
||||
STADIA_CONTROLLER("M4.725 20Q3.225 20 2.1625 18.925T1.05 16.325Q1.05 16.1 1.075 15.875T1.15 15.425L3.25 7.025Q3.6 5.675 4.675 4.8375T7.125 4H16.875Q18.25 4 19.325 4.8375T20.75 7.025L22.85 15.425Q22.9 15.65 22.9375 15.8875T22.975 16.35Q22.975 17.875 21.8875 18.9375T19.275 20Q18.225 20 17.325 19.45T15.975 17.95L15.275 16.5Q15.15 16.25 14.9 16.125T14.375 16H9.625Q9.35 16 9.1 16.125T8.725 16.5L8.025 17.95Q7.575 18.9 6.675 19.45T4.725 20ZM4.8 18Q5.275 18 5.6625 17.75T6.25 17.075L6.95 15.65Q7.325 14.875 8.05 14.4375T9.625 14H14.375Q15.225 14 15.95 14.45T17.075 15.65L17.775 17.075Q17.975 17.5 18.3625 17.75T19.225 18Q19.925 18 20.425 17.5375T20.95 16.375Q20.95 16.4 20.9 15.9L18.8 7.525Q18.625 6.85 18.1 6.425T16.875 6H7.125Q6.425 6 5.8875 6.425T5.2 7.525L3.1 15.9Q3.05 16.05 3.05 16.35 3.05 17.05 3.5625 17.525T4.8 18ZM13.5 11Q13.925 11 14.2125 10.7125T14.5 10Q14.5 9.575 14.2125 9.2875T13.5 9Q13.075 9 12.7875 9.2875T12.5 10Q12.5 10.425 12.7875 10.7125T13.5 11ZM15.5 9Q15.925 9 16.2125 8.7125T16.5 8Q16.5 7.575 16.2125 7.2875T15.5 7Q15.075 7 14.7875 7.2875T14.5 8Q14.5 8.425 14.7875 8.7125T15.5 9ZM15.5 13Q15.925 13 16.2125 12.7125T16.5 12Q16.5 11.575 16.2125 11.2875T15.5 11Q15.075 11 14.7875 11.2875T14.5 12Q14.5 12.425 14.7875 12.7125T15.5 13ZM17.5 11Q17.925 11 18.2125 10.7125T18.5 10Q18.5 9.575 18.2125 9.2875T17.5 9Q17.075 9 16.7875 9.2875T16.5 10Q16.5 10.425 16.7875 10.7125T17.5 11ZM8.5 12.5Q8.825 12.5 9.0375 12.2875T9.25 11.75V10.75H10.25Q10.575 10.75 10.7875 10.5375T11 10Q11 9.675 10.7875 9.4625T10.25 9.25H9.25V8.25Q9.25 7.925 9.0375 7.7125T8.5 7.5Q8.175 7.5 7.9625 7.7125T7.75 8.25V9.25H6.75Q6.425 9.25 6.2125 9.4625T6 10Q6 10.325 6.2125 10.5375T6.75 10.75H7.75V11.75Q7.75 12.075 7.9625 12.2875T8.5 12.5ZM12 12Z"),
|
||||
STADIA_CONTROLLER_FILL("M4.725 20q-1.5 0-2.5625-1.075T1.05 16.325q0-.225.025-.45t.075-.45l2.1-8.4q.35-1.35 1.425-2.1875T7.125 4h9.75q1.375 0 2.45.8375T20.75 7.025l2.1 8.4q.05.225.0875.4625t.0375.4625q0 1.525-1.0875 2.5875T19.275 20q-1.05 0-1.95-.55t-1.35-1.5l-.7-1.45q-.125-.25-.375-.375T14.375 16H9.625q-.275 0-.525.125t-.375.375l-.7 1.45q-.45.95-1.35 1.5T4.725 20ZM13.5 11q.425 0 .7125-.2875T14.5 10t-.2875-.7125T13.5 9t-.7125.2875T12.5 10t.2875.7125T13.5 11Zm2-2q.425 0 .7125-.2875T16.5 8q0-.425-.2875-.7125T15.5 7q-.425 0-.7125.2875T14.5 8t.2875.7125T15.5 9Zm0 4q.425 0 .7125-.2875T16.5 12q0-.425-.2875-.7125T15.5 11q-.425 0-.7125.2875T14.5 12t.2875.7125T15.5 13Zm2-2q.425 0 .7125-.2875T18.5 10q0-.425-.2875-.7125T17.5 9q-.425 0-.7125.2875T16.5 10q0 .425.2875.7125T17.5 11Zm-9 1.5q.325 0 .5375-.2125T9.25 11.75v-1h1q.325 0 .5375-.2125T11 10t-.2125-.5375T10.25 9.25h-1v-1q0-.325-.2125-.5375T8.5 7.5q-.325 0-.5375.2125T7.75 8.25v1h-1q-.325 0-.5375.2125T6 10q0 .325.2125.5375T6.75 10.75h1v1q0 .325.2125.5375T8.5 12.5Z"),
|
||||
STYLE("M3.975 19.8 3.125 19.45Q2.35 19.125 2.0875 18.325T2.175 16.75L3.975 12.85V19.8ZM7.975 22Q7.15 22 6.5625 21.4125T5.975 20V14L8.625 21.35Q8.7 21.525 8.775 21.6875T8.975 22H7.975ZM13.125 21.9Q12.325 22.2 11.575 21.825T10.525 20.65L6.075 8.45Q5.775 7.65 6.125 6.8875T7.275 5.85L14.825 3.1Q15.625 2.8 16.375 3.175T17.425 4.35L21.875 16.55Q22.175 17.35 21.825 18.1125T20.675 19.15L13.125 21.9ZM10.975 10Q11.4 10 11.6875 9.7125T11.975 9Q11.975 8.575 11.6875 8.2875T10.975 8Q10.55 8 10.2625 8.2875T9.975 9Q9.975 9.425 10.2625 9.7125T10.975 10ZM12.425 20 19.975 17.25 15.525 5 7.975 7.75 12.425 20ZM7.975 7.75 15.525 5 7.975 7.75Z"),
|
||||
STYLE_FILL("M3.975 19.8l-.85-.35q-.775-.325-1.0375-1.125T2.175 16.75l1.8-3.9V19.8Zm4 2.2q-.825 0-1.4125-.5875T5.975 20V14l2.65 7.35q.075.175.15.3375t.2.3125h-1Zm5.15-.1q-.8.3-1.55-.075t-1.05-1.175L6.075 8.45q-.3-.8.05-1.5625T7.275 5.85l7.55-2.75q.8-.3 1.55.075t1.05 1.175l4.45 12.2q.3.8-.05 1.5625T20.675 19.15l-7.55 2.75ZM10.975 10q.425 0 .7125-.2875T11.975 9q0-.425-.2875-.7125T10.975 8t-.7125.2875T9.975 9t.2875.7125T10.975 10Z"),
|
||||
TEXTURE("M4.4-3Q3.925-3.1 3.5125-3.5125T3-4.4L19.6-21Q20.125-20.875 20.5-20.4875T21.025-19.6L4.4-3ZM3-9.3V-12.1L11.9-21H14.7L3-9.3ZM3-17V-19Q3-19.825 3.5875-20.4125T5-21H7L3-17ZM17-3 21-7V-5Q21-4.175 20.4125-3.5875T19-3H17ZM9.3-3 21-14.7V-11.9L12.1-3H9.3Z"),
|
||||
TRIP("M4 21Q3.175 21 2.5875 20.4125T2 19V8Q2 7.175 2.5875 6.5875T4 6H8V4Q8 3.175 8.5875 2.5875T10 2H14Q14.825 2 15.4125 2.5875T16 4V6H20Q20.825 6 21.4125 6.5875T22 8V19Q22 19.825 21.4125 20.4125T20 21H4ZM10 6H14V4H10V6ZM6 8H4V19H6V8ZM16 19V8H8V19H16ZM18 8V19H20V8H18ZM12 13.5Z"),
|
||||
TUNE("M11 21V15H13V17H21V19H13V21H11ZM3 19V17H9V19H3ZM7 15V13H3V11H7V9H9V15H7ZM11 13V11H21V13H11ZM15 9V3H17V5H21V7H17V9H15ZM3 7V5H13V7H3Z"),
|
||||
@@ -112,6 +125,7 @@ public enum SVG {
|
||||
VISIBILITY_OFF("M16.1 13.3l-1.45-1.45q.225-1.175-.675-2.2t-2.325-.8L10.2 7.4q.425-.2.8625-.3T12 7q1.875 0 3.1875 1.3125T16.5 11.5q0 .5-.1.9375t-.3.8625Zm3.2 3.15-1.45-1.4q.95-.725 1.6875-1.5875T20.8 11.5q-1.25-2.525-3.5875-4.0125T12 6q-.725 0-1.425.1T9.2 6.4L7.65 4.85q1.025-.425 2.1-.6375T12 4q3.775 0 6.725 2.0875T23 11.5q-.575 1.475-1.5125 2.7375T19.3 16.45Zm.5 6.15-4.2-4.15q-.875.275-1.7625.4125T12 19q-3.775 0-6.725-2.0875T1 11.5q.525-1.325 1.325-2.4625T4.15 7L1.4 4.2 2.8 2.8 21.2 21.2l-1.4 1.4ZM5.55 8.4q-.725.65-1.325 1.425T3.2 11.5q1.25 2.525 3.5875 4.0125T12 17q.5 0 .975-.0625T13.95 16.8l-.9-.95q-.275.075-.525.1125T12 16q-1.875 0-3.1875-1.3125T7.5 11.5q0-.275.0375-.525T7.65 10.45L5.55 8.4Zm7.975 2.325ZM9.75 12.6Z"),
|
||||
WARNING("M1 21 12 2 23 21H1ZM4.45 19H19.55L12 6 4.45 19ZM12 18Q12.425 18 12.7125 17.7125T13 17Q13 16.575 12.7125 16.2875T12 16Q11.575 16 11.2875 16.2875T11 17Q11 17.425 11.2875 17.7125T12 18ZM11 15H13V10H11V15ZM12 12.5Z"),
|
||||
WB_SUNNY("M11 4V1H13V4H11ZM11 23V20H13V23H11ZM20 13V11H23V13H20ZM1 13V11H4V13H1ZM18.7 6.7 17.3 5.3 19.05 3.5 20.5 4.95 18.7 6.7ZM4.95 20.5 3.5 19.05 5.3 17.3 6.7 18.7 4.95 20.5ZM19.05 20.5 17.3 18.7 18.7 17.3 20.5 19.05 19.05 20.5ZM5.3 6.7 3.5 4.95 4.95 3.5 6.7 5.3 5.3 6.7ZM12 18Q9.5 18 7.75 16.25T6 12Q6 9.5 7.75 7.75T12 6Q14.5 6 16.25 7.75T18 12Q18 14.5 16.25 16.25T12 18ZM12 16Q13.675 16 14.8375 14.8375T16 12Q16 10.325 14.8375 9.1625T12 8Q10.325 8 9.1625 9.1625T8 12Q8 13.675 9.1625 14.8375T12 16ZM12 12Z"),
|
||||
WB_SUNNY_FILL("M11 4V1h2V4H11Zm0 19V20h2v3H11Zm9-10V11h3v2H20ZM1 13V11H4v2H1ZM18.7 6.7 17.3 5.3l1.75-1.8L20.5 4.95 18.7 6.7ZM4.95 20.5 3.5 19.05 5.3 17.3l1.4 1.4-1.75 1.8Zm14.1 0-1.75-1.8 1.4-1.4 1.8 1.75-1.45 1.45ZM5.3 6.7 3.5 4.95 4.95 3.5 6.7 5.3 5.3 6.7ZM12 18q-2.5 0-4.25-1.75T6 12 7.75 7.75 12 6t4.25 1.75T18 12t-1.75 4.25T12 18Z"),
|
||||
;
|
||||
|
||||
public static final double DEFAULT_SIZE = 24;
|
||||
@@ -139,24 +153,25 @@ public enum SVG {
|
||||
return new Group(path);
|
||||
}
|
||||
|
||||
public Node createIcon(ObservableValue<? extends Paint> fill, double size) {
|
||||
public Node createIcon(double size) {
|
||||
SVGPath p = new SVGPath();
|
||||
p.getStyleClass().add("svg");
|
||||
p.setContent(path);
|
||||
if (fill != null)
|
||||
p.fillProperty().bind(fill);
|
||||
|
||||
p.getStyleClass().add("svg");
|
||||
return createIcon(p, size);
|
||||
}
|
||||
|
||||
public Node createIcon(Paint fill, double size) {
|
||||
public Node createIcon() {
|
||||
SVGPath p = new SVGPath();
|
||||
p.getStyleClass().add("svg");
|
||||
p.setContent(path);
|
||||
if (fill != null)
|
||||
p.fillProperty().set(fill);
|
||||
|
||||
return createIcon(p, size);
|
||||
p.getStyleClass().add("svg");
|
||||
return createIcon(p, -1);
|
||||
}
|
||||
|
||||
public Node createIcon(ObservableValue<? extends Paint> color) {
|
||||
SVGPath p = new SVGPath();
|
||||
p.setContent(path);
|
||||
p.getStyleClass().add("svg");
|
||||
p.fillProperty().bind(color);
|
||||
return createIcon(p, -1);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import javafx.animation.KeyFrame;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.skin.VirtualFlow;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.input.ScrollEvent;
|
||||
import javafx.util.Duration;
|
||||
@@ -50,8 +51,8 @@ final class ScrollUtils {
|
||||
}
|
||||
}
|
||||
|
||||
private ScrollUtils() {
|
||||
}
|
||||
private static final double DEFAULT_SPEED = 1.0;
|
||||
private static final double DEFAULT_TRACK_PAD_ADJUSTMENT = 7.0;
|
||||
|
||||
/**
|
||||
* Determines if the given ScrollEvent comes from a trackpad.
|
||||
@@ -66,16 +67,10 @@ final class ScrollUtils {
|
||||
* @see ScrollEvent#getDeltaY()
|
||||
*/
|
||||
public static boolean isTrackPad(ScrollEvent event, ScrollDirection scrollDirection) {
|
||||
switch (scrollDirection) {
|
||||
case UP:
|
||||
case DOWN:
|
||||
return Math.abs(event.getDeltaY()) < 10;
|
||||
case LEFT:
|
||||
case RIGHT:
|
||||
return Math.abs(event.getDeltaX()) < 10;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return switch (scrollDirection) {
|
||||
case UP, DOWN -> Math.abs(event.getDeltaY()) < 10;
|
||||
case LEFT, RIGHT -> Math.abs(event.getDeltaX()) < 10;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,7 +110,7 @@ final class ScrollUtils {
|
||||
* default speed value of 1.
|
||||
*/
|
||||
public static void addSmoothScrolling(ScrollPane scrollPane) {
|
||||
addSmoothScrolling(scrollPane, 1);
|
||||
addSmoothScrolling(scrollPane, DEFAULT_SPEED);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,7 +119,7 @@ final class ScrollUtils {
|
||||
* with a default trackPadAdjustment of 7.
|
||||
*/
|
||||
public static void addSmoothScrolling(ScrollPane scrollPane, double speed) {
|
||||
addSmoothScrolling(scrollPane, speed, 7);
|
||||
addSmoothScrolling(scrollPane, speed, DEFAULT_TRACK_PAD_ADJUSTMENT);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,6 +134,21 @@ final class ScrollUtils {
|
||||
smoothScroll(scrollPane, speed, trackPadAdjustment);
|
||||
}
|
||||
|
||||
/// @author Glavo
|
||||
public static void addSmoothScrolling(VirtualFlow<?> virtualFlow) {
|
||||
addSmoothScrolling(virtualFlow, DEFAULT_SPEED);
|
||||
}
|
||||
|
||||
/// @author Glavo
|
||||
public static void addSmoothScrolling(VirtualFlow<?> virtualFlow, double speed) {
|
||||
addSmoothScrolling(virtualFlow, speed, DEFAULT_TRACK_PAD_ADJUSTMENT);
|
||||
}
|
||||
|
||||
/// @author Glavo
|
||||
public static void addSmoothScrolling(VirtualFlow<?> virtualFlow, double speed, double trackPadAdjustment) {
|
||||
smoothScroll(virtualFlow, speed, trackPadAdjustment);
|
||||
}
|
||||
|
||||
private static final double[] FRICTIONS = {0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, 0.00003, 0.00001};
|
||||
private static final Duration DURATION = Duration.millis(3);
|
||||
|
||||
@@ -163,16 +173,16 @@ final class ScrollUtils {
|
||||
}
|
||||
};
|
||||
if (scrollPane.getContent().getParent() != null) {
|
||||
scrollPane.getContent().getParent().addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
|
||||
scrollPane.getContent().getParent().addEventFilter(MouseEvent.MOUSE_PRESSED, mouseHandler);
|
||||
scrollPane.getContent().getParent().addEventHandler(ScrollEvent.ANY, scrollHandler);
|
||||
}
|
||||
scrollPane.getContent().parentProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (oldValue != null) {
|
||||
oldValue.removeEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
|
||||
oldValue.removeEventFilter(MouseEvent.MOUSE_PRESSED, mouseHandler);
|
||||
oldValue.removeEventHandler(ScrollEvent.ANY, scrollHandler);
|
||||
}
|
||||
if (newValue != null) {
|
||||
newValue.addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
|
||||
newValue.addEventFilter(MouseEvent.MOUSE_PRESSED, mouseHandler);
|
||||
newValue.addEventHandler(ScrollEvent.ANY, scrollHandler);
|
||||
}
|
||||
});
|
||||
@@ -207,4 +217,51 @@ final class ScrollUtils {
|
||||
timeline.setCycleCount(Animation.INDEFINITE);
|
||||
}
|
||||
|
||||
/// @author Glavo
|
||||
private static void smoothScroll(VirtualFlow<?> virtualFlow, double speed, double trackPadAdjustment) {
|
||||
if (!virtualFlow.isVertical())
|
||||
return;
|
||||
|
||||
final double[] derivatives = new double[FRICTIONS.length];
|
||||
|
||||
Timeline timeline = new Timeline();
|
||||
final EventHandler<MouseEvent> mouseHandler = event -> timeline.stop();
|
||||
final EventHandler<ScrollEvent> scrollHandler = event -> {
|
||||
if (event.getEventType() == ScrollEvent.SCROLL) {
|
||||
ScrollDirection scrollDirection = determineScrollDirection(event);
|
||||
if (scrollDirection == ScrollDirection.LEFT || scrollDirection == ScrollDirection.RIGHT) {
|
||||
return;
|
||||
}
|
||||
double currentSpeed = isTrackPad(event, scrollDirection) ? speed / trackPadAdjustment : speed;
|
||||
|
||||
derivatives[0] += scrollDirection.intDirection * currentSpeed;
|
||||
if (timeline.getStatus() == Status.STOPPED) {
|
||||
timeline.play();
|
||||
}
|
||||
event.consume();
|
||||
}
|
||||
};
|
||||
virtualFlow.addEventFilter(MouseEvent.MOUSE_PRESSED, mouseHandler);
|
||||
virtualFlow.addEventFilter(ScrollEvent.ANY, scrollHandler);
|
||||
|
||||
timeline.getKeyFrames().add(new KeyFrame(DURATION, event -> {
|
||||
for (int i = 0; i < derivatives.length; i++) {
|
||||
derivatives[i] *= FRICTIONS[i];
|
||||
}
|
||||
for (int i = 1; i < derivatives.length; i++) {
|
||||
derivatives[i] += derivatives[i - 1];
|
||||
}
|
||||
|
||||
double dy = derivatives[derivatives.length - 1];
|
||||
virtualFlow.scrollPixels(dy);
|
||||
|
||||
if (Math.abs(dy) < 0.001) {
|
||||
timeline.stop();
|
||||
}
|
||||
}));
|
||||
timeline.setCycleCount(Animation.INDEFINITE);
|
||||
}
|
||||
|
||||
private ScrollUtils() {
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
public final class SwingUtils {
|
||||
static {
|
||||
if (System.getProperty("swing.defaultlaf") == null) {
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SwingUtils() {
|
||||
}
|
||||
|
||||
public static void initLookAndFeel() {
|
||||
// Make sure the static constructor is called
|
||||
}
|
||||
|
||||
public static void showInfoDialog(Object message) {
|
||||
JOptionPane.showMessageDialog(null, message, "Info", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
|
||||
public static void showWarningDialog(Object message) {
|
||||
JOptionPane.showMessageDialog(null, message, "Warning", JOptionPane.WARNING_MESSAGE);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Object message) {
|
||||
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
|
||||
@@ -85,30 +84,22 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
|
||||
return stackPane;
|
||||
}
|
||||
|
||||
public static JFXButton createToolbarButton(String text, SVG svg, Runnable onClick) {
|
||||
JFXButton ret = new JFXButton();
|
||||
ret.getStyleClass().add("jfx-tool-bar-button");
|
||||
ret.textFillProperty().bind(Theme.foregroundFillBinding());
|
||||
ret.setGraphic(wrap(svg.createIcon(Theme.foregroundFillBinding(), -1)));
|
||||
ret.setText(text);
|
||||
ret.setOnAction(e -> onClick.run());
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static JFXButton createToolbarButton2(String text, SVG svg, Runnable onClick) {
|
||||
JFXButton ret = new JFXButton();
|
||||
ret.getStyleClass().add("jfx-tool-bar-button");
|
||||
ret.setGraphic(wrap(svg.createIcon(Theme.blackFill(), -1)));
|
||||
ret.setGraphic(wrap(svg.createIcon()));
|
||||
ret.setText(text);
|
||||
ret.setOnAction(e -> onClick.run());
|
||||
ret.setOnAction(e -> {
|
||||
onClick.run();
|
||||
FXUtils.clearFocus(ret);
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static JFXButton createDecoratorButton(String tooltip, SVG svg, Runnable onClick) {
|
||||
JFXButton ret = new JFXButton();
|
||||
ret.getStyleClass().add("jfx-decorator-button");
|
||||
ret.textFillProperty().bind(Theme.foregroundFillBinding());
|
||||
ret.setGraphic(wrap(svg.createIcon(Theme.foregroundFillBinding(), -1)));
|
||||
ret.setGraphic(wrap(svg.createIcon()));
|
||||
FXUtils.installFastTooltip(ret, tooltip);
|
||||
ret.setOnAction(e -> onClick.run());
|
||||
return ret;
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2024 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 javafx.scene.Node;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public class TooltipInstaller {
|
||||
public static final TooltipInstaller INSTALLER;
|
||||
|
||||
static {
|
||||
TooltipInstaller installer = null;
|
||||
|
||||
try {
|
||||
installer = new NewInstaller();
|
||||
} catch (Throwable e) {
|
||||
try {
|
||||
installer = new OldInstaller();
|
||||
} catch (Throwable e2) {
|
||||
e2.addSuppressed(e);
|
||||
LOG.warning("Failed to initialize TooltipInstaller", e2);
|
||||
}
|
||||
}
|
||||
|
||||
INSTALLER = installer != null ? installer : new TooltipInstaller();
|
||||
}
|
||||
|
||||
TooltipInstaller() {
|
||||
}
|
||||
|
||||
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
|
||||
Tooltip.install(node, tooltip);
|
||||
}
|
||||
|
||||
// For Java 8
|
||||
private static final class OldInstaller extends TooltipInstaller {
|
||||
private static final Constructor<?> createTooltipBehavior;
|
||||
private static final Method installTooltipBehavior;
|
||||
|
||||
static {
|
||||
try {
|
||||
Class<?> behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior");
|
||||
createTooltipBehavior = behaviorClass.getDeclaredConstructor(Duration.class, Duration.class, Duration.class, boolean.class);
|
||||
createTooltipBehavior.setAccessible(true);
|
||||
installTooltipBehavior = behaviorClass.getDeclaredMethod("install", Node.class, Tooltip.class);
|
||||
installTooltipBehavior.setAccessible(true);
|
||||
} catch (Exception e) {
|
||||
throw new ExceptionInInitializerError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
|
||||
try {
|
||||
Object behavior = createTooltipBehavior.newInstance(showDelay, showDuration, hideDelay, false);
|
||||
installTooltipBehavior.invoke(behavior, node, tooltip);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
LOG.warning("Failed to set tooltip show delay", e);
|
||||
Tooltip.install(node, tooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For Java 9+
|
||||
private static final class NewInstaller extends TooltipInstaller {
|
||||
private static final MethodHandle setTooltipShowDelay;
|
||||
private static final MethodHandle setTooltipShowDuration;
|
||||
private static final MethodHandle setTooltipHideDelay;
|
||||
|
||||
static {
|
||||
try {
|
||||
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
|
||||
MethodType methodType = MethodType.methodType(void.class, Duration.class);
|
||||
|
||||
setTooltipShowDelay = lookup.findVirtual(Tooltip.class, "setShowDelay", methodType);
|
||||
setTooltipShowDuration = lookup.findVirtual(Tooltip.class, "setShowDuration", methodType);
|
||||
setTooltipHideDelay = lookup.findVirtual(Tooltip.class, "setHideDelay", methodType);
|
||||
} catch (Exception e) {
|
||||
throw new ExceptionInInitializerError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
|
||||
try {
|
||||
setTooltipShowDelay.invokeExact(tooltip, showDelay);
|
||||
setTooltipShowDuration.invokeExact(tooltip, showDuration);
|
||||
setTooltipHideDelay.invokeExact(tooltip, hideDelay);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to set tooltip show delay", e);
|
||||
}
|
||||
|
||||
Tooltip.install(node, tooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,19 +19,21 @@ package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXSpinner;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Node;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
|
||||
import static org.jackhuang.hmcl.Metadata.CHANGELOG_URL;
|
||||
@@ -40,30 +42,30 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class UpgradeDialog extends JFXDialogLayout {
|
||||
|
||||
public UpgradeDialog(RemoteVersion remoteVersion, Runnable updateRunnable) {
|
||||
maxWidthProperty().bind(Controllers.getScene().widthProperty().multiply(0.7));
|
||||
maxHeightProperty().bind(Controllers.getScene().heightProperty().multiply(0.7));
|
||||
|
||||
setHeading(new Label(i18n("update.changelog")));
|
||||
setBody(new ProgressIndicator());
|
||||
setBody(new JFXSpinner());
|
||||
|
||||
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
|
||||
boolean isPreview = remoteVersion.isPreview();
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
Document document = Jsoup.parse(new URL(url), 30 * 1000);
|
||||
String id = null;
|
||||
Node node = null;
|
||||
if (isPreview) {
|
||||
id = "nowpreview";
|
||||
node = document.selectFirst("#" + id);
|
||||
}
|
||||
if (node == null) {
|
||||
id = "nowchange";
|
||||
node = document.selectFirst("#" + id);
|
||||
}
|
||||
|
||||
if (node == null || !"h1".equals(node.nodeName()))
|
||||
throw new IOException("Cannot find current changelog in document");
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
VersionNumber targetVersion = VersionNumber.asVersion(remoteVersion.getVersion());
|
||||
VersionNumber currentVersion = VersionNumber.asVersion(Metadata.VERSION);
|
||||
if (targetVersion.compareTo(currentVersion) <= 0)
|
||||
// Downgrade update, no need to display changelog
|
||||
return null;
|
||||
|
||||
Document document = Jsoup.parse(new URL(url), 30 * 1000);
|
||||
Node node = document.selectFirst("h1[data-version=\"%s\"]".formatted(targetVersion));
|
||||
|
||||
if (node == null || !"h1".equals(node.nodeName())) {
|
||||
LOG.warning("Changelog not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
HTMLRenderer renderer = new HTMLRenderer(uri -> {
|
||||
LOG.info("Open link: " + uri);
|
||||
@@ -71,8 +73,11 @@ public final class UpgradeDialog extends JFXDialogLayout {
|
||||
});
|
||||
|
||||
do {
|
||||
if ("h1".equals(node.nodeName()) && !id.equals(node.attr("id"))) {
|
||||
break;
|
||||
if ("h1".equals(node.nodeName())) {
|
||||
String changelogVersion = node.attr("data-version");
|
||||
if (StringUtils.isBlank(changelogVersion) || currentVersion.compareTo(changelogVersion) >= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
renderer.appendNode(node);
|
||||
node = node.nextSibling();
|
||||
@@ -82,10 +87,14 @@ public final class UpgradeDialog extends JFXDialogLayout {
|
||||
return renderer.render();
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
ScrollPane scrollPane = new ScrollPane(result);
|
||||
scrollPane.setFitToWidth(true);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
setBody(scrollPane);
|
||||
if (result != null) {
|
||||
ScrollPane scrollPane = new ScrollPane(result);
|
||||
scrollPane.setFitToWidth(true);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
setBody(scrollPane);
|
||||
} else {
|
||||
setBody();
|
||||
}
|
||||
} else {
|
||||
LOG.warning("Failed to load update log, trying to open it in browser");
|
||||
FXUtils.openLink(url);
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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 javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// @author Glavo
|
||||
public final class WindowsNativeUtils {
|
||||
|
||||
public static OptionalLong getWindowHandle(Stage stage) {
|
||||
try {
|
||||
Class<?> windowStageClass = Class.forName("com.sun.javafx.tk.quantum.WindowStage");
|
||||
Class<?> glassWindowClass = Class.forName("com.sun.glass.ui.Window");
|
||||
Class<?> tkStageClass = Class.forName("com.sun.javafx.tk.TKStage");
|
||||
|
||||
Object tkStage = MethodHandles.privateLookupIn(Window.class, MethodHandles.lookup())
|
||||
.findVirtual(Window.class, "getPeer", MethodType.methodType(tkStageClass))
|
||||
.invoke(stage);
|
||||
|
||||
MethodHandles.Lookup windowStageLookup = MethodHandles.privateLookupIn(windowStageClass, MethodHandles.lookup());
|
||||
MethodHandle getPlatformWindow = windowStageLookup.findVirtual(windowStageClass, "getPlatformWindow", MethodType.methodType(glassWindowClass));
|
||||
Object platformWindow = getPlatformWindow.invoke(tkStage);
|
||||
|
||||
long handle = (long) MethodHandles.privateLookupIn(glassWindowClass, MethodHandles.lookup())
|
||||
.findVirtual(glassWindowClass, "getNativeWindow", MethodType.methodType(long.class))
|
||||
.invoke(platformWindow);
|
||||
|
||||
return OptionalLong.of(handle);
|
||||
} catch (Throwable ex) {
|
||||
LOG.warning("Failed to get window handle", ex);
|
||||
return OptionalLong.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private WindowsNativeUtils() {
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
@@ -69,6 +68,10 @@ public class AccountAdvancedListItem extends AdvancedListItem {
|
||||
};
|
||||
|
||||
public AccountAdvancedListItem() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public AccountAdvancedListItem(Account account) {
|
||||
tooltip = new Tooltip();
|
||||
FXUtils.installFastTooltip(this, tooltip);
|
||||
|
||||
@@ -77,25 +80,13 @@ public class AccountAdvancedListItem extends AdvancedListItem {
|
||||
|
||||
setActionButtonVisible(false);
|
||||
|
||||
setOnScroll(event -> {
|
||||
double deltaY = event.getDeltaY();
|
||||
if (deltaY == 0)
|
||||
return;
|
||||
|
||||
Account current = account.get();
|
||||
if (current == null) return;
|
||||
|
||||
ObservableList<Account> accounts = Accounts.getAccounts();
|
||||
int currentIndex = accounts.indexOf(current);
|
||||
if (currentIndex < 0) return;
|
||||
|
||||
if (deltaY > 0) // up
|
||||
currentIndex--;
|
||||
else // down
|
||||
currentIndex++;
|
||||
|
||||
Accounts.setSelectedAccount(accounts.get((currentIndex + accounts.size()) % accounts.size()));
|
||||
});
|
||||
if (account != null) {
|
||||
this.accountProperty().set(account);
|
||||
} else {
|
||||
FXUtils.onScroll(this, Accounts.getAccounts(),
|
||||
accounts -> accounts.indexOf(accountProperty().get()),
|
||||
Accounts::setSelectedAccount);
|
||||
}
|
||||
}
|
||||
|
||||
public ObjectProperty<Account> accountProperty() {
|
||||
|
||||
@@ -41,13 +41,14 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.DialogController;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CancellationException;
|
||||
@@ -116,8 +117,7 @@ public class AccountListItem extends RadioButton {
|
||||
}
|
||||
|
||||
public ObservableBooleanValue canUploadSkin() {
|
||||
if (account instanceof AuthlibInjectorAccount) {
|
||||
AuthlibInjectorAccount aiAccount = (AuthlibInjectorAccount) account;
|
||||
if (account instanceof AuthlibInjectorAccount aiAccount) {
|
||||
ObjectBinding<Optional<CompleteGameProfile>> profile = aiAccount.getYggdrasilService().getProfileRepository().binding(aiAccount.getUUID());
|
||||
return createBooleanBinding(() -> {
|
||||
Set<TextureType> uploadableTextures = profile.get()
|
||||
@@ -148,7 +148,7 @@ public class AccountListItem extends RadioButton {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("account.skin.upload"));
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("account.skin.file"), "*.png"));
|
||||
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
|
||||
Path selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
if (selectedFile == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -156,7 +156,7 @@ public class AccountListItem extends RadioButton {
|
||||
return refreshAsync()
|
||||
.thenRunAsync(() -> {
|
||||
Image skinImg;
|
||||
try (FileInputStream input = new FileInputStream(selectedFile)) {
|
||||
try (var input = Files.newInputStream(selectedFile)) {
|
||||
skinImg = new Image(input);
|
||||
} catch (IOException e) {
|
||||
throw new InvalidSkinException("Failed to read skin image", e);
|
||||
@@ -167,7 +167,7 @@ public class AccountListItem extends RadioButton {
|
||||
NormalizedSkin skin = new NormalizedSkin(skinImg);
|
||||
String model = skin.isSlim() ? "slim" : "";
|
||||
LOG.info("Uploading skin [" + selectedFile + "], model [" + model + "]");
|
||||
account.uploadSkin(skin.isSlim(), selectedFile.toPath());
|
||||
account.uploadSkin(skin.isSlim(), selectedFile);
|
||||
})
|
||||
.thenComposeAsync(refreshAsync())
|
||||
.whenComplete(Schedulers.javafx(), e -> {
|
||||
|
||||
@@ -33,9 +33,9 @@ import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
@@ -116,10 +116,10 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
});
|
||||
btnMove.getStyleClass().add("toggle-icon4");
|
||||
if (skinnable.getAccount().isPortable()) {
|
||||
btnMove.setGraphic(SVG.PUBLIC.createIcon(Theme.blackFill(), -1));
|
||||
btnMove.setGraphic(SVG.PUBLIC.createIcon());
|
||||
FXUtils.installFastTooltip(btnMove, i18n("account.move_to_global"));
|
||||
} else {
|
||||
btnMove.setGraphic(SVG.OUTPUT.createIcon(Theme.blackFill(), -1));
|
||||
btnMove.setGraphic(SVG.OUTPUT.createIcon());
|
||||
FXUtils.installFastTooltip(btnMove, i18n("account.move_to_portable"));
|
||||
}
|
||||
spinnerMove.setContent(btnMove);
|
||||
@@ -128,6 +128,10 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
JFXButton btnRefresh = new JFXButton();
|
||||
SpinnerPane spinnerRefresh = new SpinnerPane();
|
||||
spinnerRefresh.getStyleClass().setAll("small-spinner-pane");
|
||||
if (skinnable.getAccount() instanceof MicrosoftAccount && Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
|
||||
btnRefresh.setDisable(true);
|
||||
FXUtils.installFastTooltip(spinnerRefresh, i18n("account.methods.microsoft.snapshot"));
|
||||
}
|
||||
btnRefresh.setOnAction(e -> {
|
||||
spinnerRefresh.showSpinner();
|
||||
skinnable.refreshAsync()
|
||||
@@ -141,7 +145,7 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
.start();
|
||||
});
|
||||
btnRefresh.getStyleClass().add("toggle-icon4");
|
||||
btnRefresh.setGraphic(SVG.REFRESH.createIcon(Theme.blackFill(), -1));
|
||||
btnRefresh.setGraphic(SVG.REFRESH.createIcon());
|
||||
FXUtils.installFastTooltip(btnRefresh, i18n("button.refresh"));
|
||||
spinnerRefresh.setContent(btnRefresh);
|
||||
right.getChildren().add(spinnerRefresh);
|
||||
@@ -158,7 +162,7 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
}
|
||||
});
|
||||
btnUpload.getStyleClass().add("toggle-icon4");
|
||||
btnUpload.setGraphic(SVG.CHECKROOM.createIcon(Theme.blackFill(), -1));
|
||||
btnUpload.setGraphic(SVG.CHECKROOM.createIcon());
|
||||
FXUtils.installFastTooltip(btnUpload, i18n("account.skin.upload"));
|
||||
btnUpload.disableProperty().bind(Bindings.not(skinnable.canUploadSkin()));
|
||||
spinnerUpload.setContent(btnUpload);
|
||||
@@ -170,7 +174,7 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
spinnerCopyUUID.getStyleClass().add("small-spinner-pane");
|
||||
btnUpload.getStyleClass().add("toggle-icon4");
|
||||
btnCopyUUID.setOnAction(e -> FXUtils.copyText(skinnable.getAccount().getUUID().toString()));
|
||||
btnCopyUUID.setGraphic(SVG.CONTENT_COPY.createIcon(Theme.blackFill(), -1));
|
||||
btnCopyUUID.setGraphic(SVG.CONTENT_COPY.createIcon());
|
||||
FXUtils.installFastTooltip(btnCopyUUID, i18n("account.copy_uuid"));
|
||||
spinnerCopyUUID.setContent(btnCopyUUID);
|
||||
right.getChildren().add(spinnerCopyUUID);
|
||||
@@ -179,7 +183,7 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
btnRemove.setOnAction(e -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), skinnable::remove, null));
|
||||
btnRemove.getStyleClass().add("toggle-icon4");
|
||||
BorderPane.setAlignment(btnRemove, Pos.CENTER);
|
||||
btnRemove.setGraphic(SVG.DELETE.createIcon(Theme.blackFill(), -1));
|
||||
btnRemove.setGraphic(SVG.DELETE.createIcon());
|
||||
FXUtils.installFastTooltip(btnRemove, i18n("button.delete"));
|
||||
right.getChildren().add(btnRemove);
|
||||
root.setRight(right);
|
||||
|
||||
@@ -19,7 +19,13 @@ package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ListProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -33,7 +39,6 @@ import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
@@ -41,58 +46,27 @@ import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.ClassTitle;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
import org.jackhuang.hmcl.util.platform.NativeUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.windows.Kernel32;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinConstants;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
|
||||
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class AccountListPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
static final BooleanProperty RESTRICTED = new SimpleBooleanProperty(true);
|
||||
|
||||
private static boolean isExemptedRegion() {
|
||||
String zoneId = ZoneId.systemDefault().getId();
|
||||
if (Arrays.asList(
|
||||
"Asia/Shanghai",
|
||||
// Although Asia/Beijing is not a legal name, Deepin uses it
|
||||
"Asia/Beijing",
|
||||
"Asia/Chongqing",
|
||||
"Asia/Chungking",
|
||||
"Asia/Harbin"
|
||||
).contains(zoneId))
|
||||
return true;
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && NativeUtils.USE_JNA) {
|
||||
Kernel32 kernel32 = Kernel32.INSTANCE;
|
||||
|
||||
// https://learn.microsoft.com/windows/win32/intl/table-of-geographical-locations
|
||||
if (kernel32 != null && kernel32.GetUserGeoID(WinConstants.GEOCLASS_NATION) == 45) // China
|
||||
return true;
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && "GMT+08:00".equals(zoneId))
|
||||
// Some Linux distributions may use invalid time zone ids (e.g., Asia/Beijing)
|
||||
// Java may not be able to resolve this name and use GMT+08:00 instead.
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static {
|
||||
String property = System.getProperty("hmcl.offline.auth.restricted", "auto");
|
||||
|
||||
if ("false".equals(property)
|
||||
|| "auto".equals(property) && isExemptedRegion()
|
||||
|| "auto".equals(property) && LocaleUtils.IS_CHINA_MAINLAND
|
||||
|| globalConfig().isEnableOfflineAccount())
|
||||
RESTRICTED.set(false);
|
||||
else
|
||||
@@ -183,14 +157,14 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
|
||||
e.consume();
|
||||
});
|
||||
btnRemove.getStyleClass().add("toggle-icon4");
|
||||
btnRemove.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), 14));
|
||||
btnRemove.setGraphic(SVG.CLOSE.createIcon(14));
|
||||
item.setRightGraphic(btnRemove);
|
||||
|
||||
ObservableValue<String> title = BindingMapping.of(server, AuthlibInjectorServer::getName);
|
||||
item.titleProperty().bind(title);
|
||||
String host = "";
|
||||
try {
|
||||
host = URI.create(server.getUrl()).getHost();
|
||||
host = NetworkUtils.toURI(server.getUrl()).getHost();
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warning("Unparsable authlib-injector server url " + server.getUrl(), e);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import org.jackhuang.hmcl.ui.construct.DialogAware;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
import java.io.IOException;
|
||||
@@ -196,7 +195,8 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
|
||||
lblServerName.setText(serverBeingAdded.getName());
|
||||
lblServerUrl.setText(serverBeingAdded.getUrl());
|
||||
|
||||
lblServerWarning.setVisible("http".equals(NetworkUtils.toURL(serverBeingAdded.getUrl()).getProtocol()));
|
||||
//noinspection HttpUrlsUsage
|
||||
lblServerWarning.setVisible(serverBeingAdded.getUrl().startsWith("http://"));
|
||||
|
||||
this.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT);
|
||||
} else {
|
||||
|
||||
@@ -19,9 +19,6 @@ package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
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.NamedArg;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
@@ -39,7 +36,6 @@ import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextInputControl;
|
||||
import javafx.scene.layout.*;
|
||||
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||
@@ -54,7 +50,6 @@ import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
@@ -268,33 +263,12 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
};
|
||||
|
||||
if (factory instanceof OfflineAccountFactory && username != null && (!USERNAME_CHECKER_PATTERN.matcher(username).matches() || username.length() > 16)) {
|
||||
JFXButton btnYes = new JFXButton(i18n("button.ok"));
|
||||
btnYes.getStyleClass().add("dialog-error");
|
||||
btnYes.setOnAction(e -> doCreate.run());
|
||||
btnYes.setDisable(true);
|
||||
|
||||
int countdown = 10;
|
||||
KeyFrame[] keyFrames = new KeyFrame[countdown + 1];
|
||||
for (int i = 0; i < countdown; i++) {
|
||||
keyFrames[i] = new KeyFrame(Duration.seconds(i),
|
||||
new KeyValue(btnYes.textProperty(), i18n("button.ok.countdown", countdown - i)));
|
||||
}
|
||||
keyFrames[countdown] = new KeyFrame(Duration.seconds(countdown),
|
||||
new KeyValue(btnYes.textProperty(), i18n("button.ok")),
|
||||
new KeyValue(btnYes.disableProperty(), false));
|
||||
|
||||
Timeline timeline = new Timeline(keyFrames);
|
||||
Controllers.confirmAction(
|
||||
i18n("account.methods.offline.name.invalid"), i18n("message.warning"),
|
||||
Controllers.confirmWithCountdown(i18n("account.methods.offline.name.invalid"), i18n("message.warning"), 10,
|
||||
MessageDialogPane.MessageType.WARNING,
|
||||
btnYes,
|
||||
() -> {
|
||||
timeline.stop();
|
||||
doCreate, () -> {
|
||||
body.setDisable(false);
|
||||
spinner.hideSpinner();
|
||||
}
|
||||
);
|
||||
timeline.play();
|
||||
});
|
||||
} else {
|
||||
doCreate.run();
|
||||
}
|
||||
@@ -490,7 +464,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
linksContainer.setMinWidth(USE_PREF_SIZE);
|
||||
|
||||
JFXButton btnAddServer = new JFXButton();
|
||||
btnAddServer.setGraphic(SVG.ADD.createIcon(Theme.blackFill(), 20));
|
||||
btnAddServer.setGraphic(SVG.ADD.createIcon(20));
|
||||
btnAddServer.getStyleClass().add("toggle-icon4");
|
||||
btnAddServer.setOnAction(e -> {
|
||||
Controllers.dialog(new AddAuthlibInjectorServerPane());
|
||||
|
||||
Loaded 100 of 629 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user