Compare commits

..
Author SHA1 Message Date
8d876fbff5 [release/3.6] 为不同类型的 Dialog 指定相应的默认标题 (#5671)
https://github.com/HMCL-dev/HMCL/pull/5079
https://github.com/HMCL-dev/HMCL/pull/5152
https://github.com/HMCL-dev/HMCL/pull/5633

Co-authored-by: neveler <55753029+neveler@users.noreply.github.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2026-03-23 22:07:21 +08:00
辞庐andGlavo 4c1f15c98c [release/3.6] 修复 CurseForge 源无法搜索到内容的问题 (#5733)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-03-08 22:13:54 +08:00
3gf8jv4dv e8b9d98c99 [release/3.6] chore: update copyright year and attribution (#5670)
https://github.com/HMCL-dev/HMCL/pull/5622
2026-03-07 21:50:58 +08:00
3gf8jv4dv 61b7eb34f6 [release/3.6] fix(l10n): replace mcedit with minecraft wiki's tutorial page (#5672)
https://github.com/HMCL-dev/HMCL/pull/5524
2026-03-07 21:39:05 +08:00
3gf8jv4dv a2df345b3c [release/3.6] style(l10n): simplify update button text in mod management header (#5673)
https://github.com/HMCL-dev/HMCL/pull/4712
2026-03-07 21:37:56 +08:00
3gf8jv4dv 70030a9558 [release/3.6] fix(i18n): update i18n for open dir button on instance mods tab (#5674)
https://github.com/HMCL-dev/HMCL/pull/4734
2026-03-03 21:45:00 +08:00
Glavoand3gf8jv4dv f03a4ff807 [release/3.6] 添加“提前预览 HMCL 版本”选项 (#4668)
https://github.com/HMCL-dev/HMCL/pull/4223
https://github.com/HMCL-dev/HMCL/pull/4598
https://github.com/HMCL-dev/HMCL/pull/4599

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-10-13 15:38:29 +08:00
Glavo de9a105f2f [release/3.6] Update versions.txt to 1.21.10 (#4667)
https://github.com/HMCL-dev/HMCL/pull/4626
2025-10-13 15:27:48 +08:00
Glavo b5d3653c9a [release/3.6] 移除 Pack200 支持 (#4666)
https://github.com/HMCL-dev/HMCL/pull/4267
2025-10-13 15:23:53 +08:00
Glavo 554503fff0 [release/3.6] 在 CacheRepository.ETagItem 中添加 expires 字段 (#4662) 2025-10-13 15:12:51 +08:00
Glavo 785f334241 [release/3.6] 更新 Jenkins CI 服务器的发布模型 (#4582)
https://github.com/HMCL-dev/HMCL/pull/4559
2025-10-02 20:38:58 +08:00
GlavoandShulkerSakura 0b87959716 [release/3.6] Added 16 characters length detection of username (#4580)
https://github.com/HMCL-dev/HMCL/pull/4088

Co-authored-by: ShulkerSakura <50770360+ShulkerSakura@users.noreply.github.com>
2025-10-01 20:44:11 +08:00
Glavoand8Mi_Yile ae18ca7567 [release/3.6] 更新 unlisted-versions-of-minecraft 镜像源地址 (#4579)
https://github.com/HMCL-dev/HMCL/pull/4120

Co-authored-by: 8Mi_Yile <admin@8mi.tech>
2025-10-01 20:34:56 +08:00
Glavoand3gf8jv4dv 7ebdc783c6 [release/3.6] chore: update versions.txt to 1.21.9 (#4578)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-10-01 20:22:18 +08:00
Glavo 39abdf1ecf [release/3.6] 修复启用代理身份认证且未设置密码时启动器崩溃的问题 (#4536)
https://github.com/HMCL-dev/HMCL/pull/4465
2025-09-23 11:11:19 +08:00
Glavo 8df6563d56 [release/3.6] 修复滚动切换游戏实例/账户功能 (#4535)
https://github.com/HMCL-dev/HMCL/pull/4466
2025-09-22 22:39:38 +08:00
Glavo f62c12fd3c [release/3.6] 优化 Java 自动选择规则 (#4534)
https://github.com/HMCL-dev/HMCL/pull/4515
2025-09-22 22:32:11 +08:00
Glavo 1205212e52 [release/3.6] 修复使用 JavaFX 25 时游戏下载界面布局错误的问题 (#4533)
https://github.com/HMCL-dev/HMCL/pull/4508
2025-09-22 22:25:32 +08:00
Glavo 0d9a275fb3 [release/3.6] 检测到配置文件由高版本启动器创建时禁止保存配置文件 (#4449)
https://github.com/HMCL-dev/HMCL/pull/4414
2025-09-11 01:19:56 +08:00
Glavoand3gf8jv4dv ab72ac3d10 [release/3.6] chore(l10n): change translation from 版本 to 实例 in zh-cn for term INSTANCE (#4448)
https://github.com/HMCL-dev/HMCL/pull/4376

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-09-11 00:24:53 +08:00
Glavoand辞庐 9d5f8a9f0e [release/3.6] 修复 LWJGL 路径显示错误的问题 (#4447)
https://github.com/HMCL-dev/HMCL/pull/4192

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2025-09-11 00:15:13 +08:00
GlavoandWulian233 fe2e96345b [release/3.6] 修复世界管理页面不支持显示极限模式的问题 (#4446)
https://github.com/HMCL-dev/HMCL/pull/4321

Co-authored-by: Wulian233 <1055917385@qq.com>
2025-09-11 00:05:23 +08:00
Glavoand辞庐 881fa2858f [release/3.6] 修复整合包格式不匹配提示未被正确格式化的问题 (#4445)
https://github.com/HMCL-dev/HMCL/pull/4176

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2025-09-11 00:01:11 +08:00
Glavo a0b95866be [release/3.6] Update microsoft_auth.html (#4444)
https://github.com/HMCL-dev/HMCL/pull/4388
2025-09-10 23:53:35 +08:00
Glavo 93bbef385d [release/3.6] 修复 RemoteMod::getIntegrityCheck 未选择正确的算法的问题 (#4443)
https://github.com/HMCL-dev/HMCL/pull/4238
2025-09-10 23:44:09 +08:00
Glavoand神奇的赵二 9dcc2727aa [release/3.6] 新增内存显示条禁用样式 (#4442)
https://github.com/HMCL-dev/HMCL/pull/4185

Co-authored-by: 神奇的赵二 (OrzMiku) <miku@ecy.pink>
2025-09-10 23:39:12 +08:00
Glavoand辞庐 865c8588d8 [release/3.6] 修复“下载线程数”的指示器被提示覆盖的问题 (#4441)
https://github.com/HMCL-dev/HMCL/pull/4153

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2025-09-10 23:34:52 +08:00
3gf8jv4dvand辞庐 419612bd3e [release/3.6] backport language file changes to the 3.6 branch (#4319)
https://github.com/HMCL-dev/HMCL/pull/3690
https://github.com/HMCL-dev/HMCL/pull/4149
https://github.com/HMCL-dev/HMCL/pull/4188
https://github.com/HMCL-dev/HMCL/pull/4189
https://github.com/HMCL-dev/HMCL/pull/4199
https://github.com/HMCL-dev/HMCL/pull/4200
https://github.com/HMCL-dev/HMCL/pull/4303

---------

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2025-09-10 23:34:13 +08:00
Glavo d16eded1ad [release/3.6] 弃用 Java 11 支持 (#4422) 2025-09-10 23:24:51 +08:00
Glavo c3a47655f0 [release/3.6] 更新模组和整合包数据 (#4440)
https://github.com/HMCL-dev/HMCL/pull/4439
2025-09-10 23:21:30 +08:00
Glavo 34842a5f26 [release/3.6] 修复无法加载 WebP 图标的问题 (#4436)
https://github.com/HMCL-dev/HMCL/pull/4171
2025-09-10 23:20:27 +08:00
Glavo 6dd7d53c20 [release/3.6] 修复 backgroundType 为 null 时启动器崩溃的问题 (#4438)
https://github.com/HMCL-dev/HMCL/pull/4437
2025-09-10 22:58:18 +08:00
Glavo 1654c652e0 [release/3.6] 在 ./.hmcl/java 中搜索 Java (#4434)
https://github.com/HMCL-dev/HMCL/pull/4179
2025-09-10 21:37:45 +08:00
Glavoand辞庐 881f5e7874 [release/3.6] 模组下载列表翻页后自动回到顶部 (#4433)
https://github.com/HMCL-dev/HMCL/pull/4170

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2025-09-10 21:28:03 +08:00
Glavo 73f0eb472a [release/3.6] 修复首次打开子页面时切换动画丢失的问题 (#4431)
https://github.com/HMCL-dev/HMCL/pull/4365
2025-09-10 21:17:00 +08:00
Glavo e13b8385e0 [release/3.6] 在 Windows 7/8/8.1 上使用 llvmpipe-loader 加载 mesa (#4430)
https://github.com/HMCL-dev/HMCL/pull/4224
2025-09-10 21:01:36 +08:00
Glavo a6e4651307 [release/3.6] 修复非 Windows 平台上游戏进程的 user.home 可能被设置为 null 的问题 (#4427)
https://github.com/HMCL-dev/HMCL/pull/4241
2025-09-10 20:55:33 +08:00
Glavo 554e573d7f [release/3.6] 支持通过环境变量强制使用 GPU 渲染和控制动画帧率 (#4426)
https://github.com/HMCL-dev/HMCL/pull/4164
2025-09-10 20:46:51 +08:00
Glavo 3096f933af [release/3.6] 修复导入异常格式的数据包时启动器崩溃的问题 (#4425)
https://github.com/HMCL-dev/HMCL/pull/4152
2025-09-10 20:41:29 +08:00
GlavoandWulian233 ff3b486f58 [release/3.6] 修复游戏版本列表错误地过滤掉部分版本的问题 (#4424)
https://github.com/HMCL-dev/HMCL/pull/4066
https://github.com/HMCL-dev/HMCL/pull/4244

---------

Co-authored-by: Wulian233 <1055917385@qq.com>
2025-09-10 20:36:01 +08:00
GlavoandRoj234 708e0326a7 [release/3.6] 修复多次启动游戏时内存泄露的问题 (#4423)
https://github.com/HMCL-dev/HMCL/pull/4121

Co-authored-by: Roj234 <82699138+roj234@users.noreply.github.com>
2025-09-10 20:27:12 +08:00
GlavoandWulian233 f7a5c7a310 [release/3.6] 修复 1.21.5 以上的地图跳转种子地图错误 (#4317)
https://github.com/HMCL-dev/HMCL/pull/4274

Co-authored-by: Wulian233 <1055917385@qq.com>
2025-08-22 20:48:56 +08:00
Glavo 633e8f8fe2 [release/3.6] Update amdgpu.ids (#4316) 2025-08-22 20:44:36 +08:00
Glavoand辞庐 440ea1b21d [release/3.6] fix: 无法从 BMCLAPI 下载 NeoForge 所有所需文件 (#4313)
https://github.com/HMCL-dev/HMCL/pull/4174

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2025-08-22 20:36:18 +08:00
850 changed files with 20572 additions and 77963 deletions

No files matched your search

-8
View File
@@ -1,8 +0,0 @@
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 服务器。
-1
View File
@@ -1071,7 +1071,6 @@ 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
-3
View File
@@ -1,3 +0,0 @@
code_review:
pull_request_opened:
code_review: false
-65
View File
@@ -1,65 +0,0 @@
# Default: normalize all text files to LF in the repository and on checkout.
# Git auto-detects binary files and leaves them untouched.
* text=auto eol=lf
# ─── Explicitly text – source & build ────────────────────────────────────────
*.java text eol=lf
*.kt text eol=lf
*.kts text eol=lf
*.groovy text eol=lf
*.gradle text eol=lf
# ─── Configuration & data ─────────────────────────────────────────────────────
*.properties text eol=lf
*.toml text eol=lf
*.xml text eol=lf
*.json text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
*.csv text eol=lf
*.html text eol=lf
*.css text eol=lf
*.txt text eol=lf
*.md text eol=lf
*.svg text eol=lf
*.MF text eol=lf
# Gradle dependency-lock files
*.lockfile text eol=lf
# ─── Scripts ──────────────────────────────────────────────────────────────────
# Unix shell scripts must always use LF so they stay executable on Linux/macOS.
*.sh text eol=lf
gradlew text eol=lf
# CI pipeline scripts
Jenkinsfile text eol=lf
# Windows batch files must keep CRLF so cmd.exe can parse them.
*.bat text eol=crlf
# ─── Binary files ─────────────────────────────────────────────────────────────
# Raster / animated images
*.png binary
*.apng binary
*.jpg binary
*.jpeg binary
*.webp binary
*.gif binary
*.ico binary
# Archives & compiled artifacts
*.jar binary
*.zip binary
*.class binary
# Cryptographic / certificate files
*.der binary
# Minecraft schematic
*.litematic binary
# Compiled / intermediate build artefacts
*.bin binary
*.probe binary
-5
View File
@@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: QQ 群
url: https://docs.hmcl.net/groups.html
about: Hello Minecraft! Launcher 的官方 QQ 交流群。
+4 -4
View File
@@ -9,15 +9,15 @@ body:
value: |
提交前请确认:
* 该问题确实是 **HMCL 的错误**,而**不是 Minecraft 非正常退出**。如果你的 Minecraft 非正常退出,请前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 服务器](https://discord.gg/jVvC7HfM6U) 获取帮助。
* 你的启动器版本是**最新的预览版本**。你可以从 [GitHub Actions](https://github.com/HMCL-dev/HMCL/actions/workflows/gradle.yml?query=branch%3Amain+event%3Apush) 或 [nightly.link](https://nightly.link/HMCL-dev/HMCL/workflows/gradle/main) 下载最新预览版本。
* 该问题确实是 **HMCL 的错误**,而**不是 Minecraft 非正常退出**,如果你的 Minecraft 非正常退出,请前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 服务器](https://discord.gg/jVvC7HfM6U) 获取帮助。
* 你的启动器版本是**最新的预览版本**,可以点击 [此处](https://zkitefly.github.io/HMCL-Snapshot-Update/) 下载最新预览版本。
如果你的问题并不属于上述两类,你可以选取另一种 Issue 类型,或者直接前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 服务器](https://discord.gg/jVvC7HfM6U) 获取帮助。
Before submitting, please confirm:
* The issue is indeed **a bug of HMCL**, not **Minecraft abnormal exit**. If your Minecraft exits abnormally, please go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord server](https://discord.gg/jVvC7HfM6U) for help.
* Your launcher is the **latest nightly build**. You can download the latest nightly build from [GitHub Actions](https://github.com/HMCL-dev/HMCL/actions/workflows/gradle.yml?query=branch%3Amain+event%3Apush) or [nightly.link](https://nightly.link/HMCL-dev/HMCL/workflows/gradle/main).
* Your launcher is the **latest nightly build**. You can click [here](https://zkitefly.github.io/HMCL-Snapshot-Update/en) to download the latest nightly build.
If your issue does not fall into the above two categories, you can choose another type of issue or directly go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord server](https://discord.gg/jVvC7HfM6U) for help.
- type: textarea
@@ -38,7 +38,7 @@ body:
attributes:
label: 启动器崩溃报告 / 启动器日志文件 | Launcher Crash Report / Launcher Log File
description: |
如果你的启动器崩溃了,请将崩溃报告填入 (或将文件拖入) 下方。
如果你的启动器崩溃了,请将崩溃报告填入(或将文件拖入)下方。
如果你的启动器没有崩溃,请在遇到问题后**不要退出启动器**,在启动器的 “设置 → 通用 → 调试” 一栏中点击 “导出启动器日志”,并将导出的日志拖到下方的输入栏中。
**请注意:启动器崩溃报告或日志文件是诊断问题的重要依据,请务必上传!**
-29
View File
@@ -1,29 +0,0 @@
name: Check Codes
on:
push:
paths:
- '**.java'
- '**.properties'
pull_request:
paths:
- '**.java'
- '**.properties'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: '17'
java-package: 'jdk+fx'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
cache-cleanup: never
- name: Check Codes
run: ./gradlew checkstyle checkTranslations --no-daemon --parallel --stacktrace
+25
View File
@@ -0,0 +1,25 @@
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
+23
View File
@@ -0,0 +1,23 @@
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
+141
View File
@@ -0,0 +1,141 @@
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 }}
+21
View File
@@ -0,0 +1,21 @@
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
+10 -32
View File
@@ -12,45 +12,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up JDK 17
uses: actions/setup-java@v5
- uses: actions/checkout@v4
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
java-version: '11'
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 --parallel
run: ./gradlew build --no-daemon
env:
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
MICROSOFT_AUTH_SECRET: ${{ secrets.MICROSOFT_AUTH_SECRET }}
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
- name: Get short SHA
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
- name: Upload JAR
uses: actions/upload-artifact@v7
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: HMCL-${{ env.SHORT_SHA }}-jar
path: HMCL/build/libs/HMCL-*.jar
archive: false
- name: Upload EXE
uses: actions/upload-artifact@v7
with:
name: HMCL-${{ env.SHORT_SHA }}-exe
path: HMCL/build/libs/HMCL-*.exe
archive: false
- name: Upload SH
uses: actions/upload-artifact@v7
with:
name: HMCL-${{ env.SHORT_SHA }}-sh
path: HMCL/build/libs/HMCL-*.sh
archive: false
- name: Upload DEB
uses: actions/upload-artifact@v7
with:
name: HMCL-${{ env.SHORT_SHA }}-deb
path: HMCL/build/libs/HMCL-*.deb
archive: false
name: HMCL-${{ env.SHORT_SHA }}
path: HMCL/build/libs
-33
View File
@@ -1,33 +0,0 @@
name: Mirror Repository
on:
workflow_dispatch:
push:
concurrency:
group: mirror-repository
cancel-in-progress: true
jobs:
mirror:
strategy:
fail-fast: false
matrix:
include:
- name: Gitee
repo: gitee.com/huanghongxun/HMCL
user: 'hmcl-sync'
token: 'GITEE_SYNC_TOKEN'
- name: CNB
repo: cnb.cool/HMCL-dev/HMCL
user: 'cnb'
token: 'CNB_SYNC_TOKEN'
name: Mirror to ${{ matrix.name }}
if: ${{ github.repository == 'HMCL-dev/HMCL' }}
runs-on: ubuntu-latest
steps:
- name: Mirror GitHub to ${{ matrix.name }}
run: |
git clone --mirror "https://github.com/${{ github.repository }}.git" -- repo
cd repo
git push -f --prune "https://${{ matrix.user }}:${{ secrets[matrix.token] }}@${{ matrix.repo }}.git" "refs/heads/*:refs/heads/*" "refs/tags/*:refs/tags/*"
-136
View File
@@ -1,136 +0,0 @@
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"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.deb"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.deb.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')
export DEB_SHA256=$(cat HMCL-$HMCL_VERSION.deb.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
echo "$DEB_SHA256 HMCL-$HMCL_VERSION.deb" | sha256sum -c
- name: Generate release note
if: ${{ env.continue == 'true' }}
run: |
# GitHub Release Note
echo "&nbsp;**[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
echo "| [HMCL-$HMCL_VERSION.deb]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.deb) | \`$(cat HMCL-$HMCL_VERSION.deb.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
echo "| [HMCL-$HMCL_VERSION.deb]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.deb) | \`$(cat HMCL-$HMCL_VERSION.deb.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" \
"HMCL-${{ env.HMCL_VERSION }}.deb" \
--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"
echo "Uploading HMCL-$HMCL_VERSION.deb"
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "$HMCL_TAG_NAME" --file-name "HMCL-$HMCL_VERSION.deb"
env:
CNB_TOKEN: ${{ secrets.CNB_SYNC_TOKEN }}
CNB_REPO: HMCL-dev/HMCL
+2 -21
View File
@@ -2,10 +2,8 @@
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
*.hprof
.gradle
gradle.properties
*.lck
*.1
@@ -15,22 +13,13 @@ gradle.properties
/externalgames
NVIDIA
minecraft-exported-crash-info*
hmcl-exported-logs-*
terracotta-log-*
/.java/
/.local/
/.cache/
# IANA Language Subtag Registry
language-subtag-registry
# gradle build
/build/
/HMCL/build/
/HMCLCore/build/
/HMCLBoot/build/
/HMCLTransformerDiscoveryService/build/
/minecraft/libraries/HMCLTransformerDiscoveryService/build/
/minecraft/libraries/HMCLMultiMCBootstrap/build/
/buildSrc/build/
# idea
@@ -39,14 +28,12 @@ language-subtag-registry
/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
@@ -65,10 +52,4 @@ language-subtag-registry
# test
/hmcl.json
/.hmcl.json
/.hmcl/
# AI
/.gemini/
!/.gemini/config.yaml
.codex
/.gradle-*/
/.hmcl/
-6
View File
@@ -1,6 +0,0 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Hello Minecraft! Launcher&#10;Copyright (C) &amp;#36;today.year huangyuhui &lt;huanghongxun2008@126.com&gt; and contributors&#10;&#10;This program is free software: you can redistribute it and/or modify&#10;it under the terms of the GNU General Public License as published by&#10;the Free Software Foundation, either version 3 of the License, or&#10;(at your option) any later version.&#10;&#10;This program is distributed in the hope that it will be useful,&#10;but WITHOUT ANY WARRANTY; without even the implied warranty of&#10;MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&#10;GNU General Public License for more details.&#10;&#10;You should have received a copy of the GNU General Public License&#10;along with this program. If not, see &lt;https://www.gnu.org/licenses/&gt;." />
<option name="myName" value="HMCL" />
</copyright>
</component>
-8
View File
@@ -1,8 +0,0 @@
<component name="CopyrightManager">
<settings default="HMCL">
<LanguageOptions name="JAVA">
<option name="fileTypeOverride" value="3" />
<option name="addBlankAfter" value="false" />
</LanguageOptions>
</settings>
</component>
-21
View File
@@ -1,21 +0,0 @@
# Java Code Style Requirements
These rules apply to all Java code written or modified in this repository.
## Nullability
- Annotate every class with JetBrains Annotations `@NotNullByDefault`.
- Any type, field, parameter, return value, local variable, or generic type argument that may be `null` must be explicitly annotated with `@Nullable`.
- Nullability must never be implicit.
## Immutability
- Immutable arrays and collections must be explicitly annotated with JetBrains Annotations `@Unmodifiable` or `@UnmodifiableView` as appropriate.
- For arrays, use type-use syntax such as `String @Unmodifiable []`.
## Documentation
- Every class, field, and method must have documentation.
- Documentation must use `///` Markdown-style Javadoc comments.
- Keep documentation accurate and specific to the actual behavior, constraints, and side effects.
- Add concise implementation comments inside complex logic whenever they materially improve readability or explain non-obvious behavior.
+48 -203
View File
@@ -1,13 +1,6 @@
import org.jackhuang.hmcl.gradle.TerracottaConfigUpgradeTask
import org.jackhuang.hmcl.gradle.ci.GitHubActionUtils
import org.jackhuang.hmcl.gradle.ci.JenkinsUtils
import org.jackhuang.hmcl.gradle.l10n.CheckTranslations
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.pack.CreateDeb
import org.jackhuang.hmcl.gradle.pack.ReleaseType
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
import java.net.URI
import java.nio.file.FileSystems
@@ -30,6 +23,7 @@ val versionType = System.getenv("VERSION_TYPE") ?: if (isOfficial) "nightly" els
val versionRoot = System.getenv("VERSION_ROOT") ?: projectConfig.getProperty("versionRoot") ?: "3"
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE") ?: ""
@@ -52,24 +46,15 @@ if (buildNumber != null) {
}
}
val embedResources by configurations.registering
dependencies {
implementation(project(":HMCLCore"))
implementation(project(":HMCLBoot"))
implementation("libs:JFoenix")
implementation(libs.jwebp)
implementation(libs.fxsvgimage)
implementation(libs.twelvemonkeys.imageio.webp)
implementation(libs.java.info)
implementation(libs.monet.fx)
implementation(libs.nayuki.qrcodegen)
if (launcherExe.isBlank()) {
implementation(libs.hmclauncher)
}
embedResources(libs.authlib.injector)
embedResources(libs.lwjgl.unsafe.agent)
}
fun digest(algorithm: String, bytes: ByteArray): ByteArray = MessageDigest.getInstance(algorithm).digest(bytes)
@@ -113,69 +98,16 @@ fun attachSignature(jar: File) {
}
}
tasks.withType<JavaCompile> {
sourceCompatibility = "17"
targetCompatibility = "17"
}
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/javafx.scene",
"javafx.graphics/com.sun.glass.ui",
"javafx.graphics/com.sun.javafx.stage",
"javafx.graphics/com.sun.javafx.util",
"javafx.graphics/com.sun.prism",
"javafx.controls/com.sun.javafx.scene.control",
"javafx.controls/com.sun.javafx.scene.control.behavior",
"javafx.graphics/com.sun.javafx.tk.quantum",
"javafx.controls/javafx.scene.control.skin",
"jdk.attach/sun.tools.attach",
)
tasks.compileJava {
options.compilerArgs.addAll(addOpens.map { "--add-exports=$it=ALL-UNNAMED" })
}
val hmclProperties = buildList {
add("hmcl.version" to project.version.toString())
add("hmcl.add-opens" to addOpens.joinToString(" "))
System.getenv("GITHUB_SHA")?.let {
add("hmcl.version.hash" to it)
val java11 = sourceSets.create("java11") {
java {
srcDir("src/main/java11")
}
add("hmcl.version.type" to versionType)
add("hmcl.microsoft.auth.id" to microsoftAuthId)
add("hmcl.curseforge.apikey" to curseForgeApiKey)
add("hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!)
add("hmcl.lwjgl-unsafe-agent.version" to libs.lwjgl.unsafe.agent.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.getByName<JavaCompile>(java11.compileJavaTaskName) {
options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
sourceCompatibility = "11"
targetCompatibility = "11"
}
tasks.jar {
@@ -186,8 +118,6 @@ tasks.jar {
val jarPath = tasks.jar.get().archiveFile.get().asFile
tasks.shadowJar {
dependsOn(createPropertiesFile)
archiveClassifier.set(null as String?)
exclude("**/package-info.class")
@@ -197,7 +127,7 @@ tasks.shadowJar {
exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi")
listOf(
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "linux-*",
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*","freebsd-*", "linux-*", "darwin-*",
"*-ppc", "*-ppc64le", "*-s390x", "*-armel",
).forEach { exclude("com/sun/jna/$it/**") }
@@ -205,18 +135,40 @@ 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.",
"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",
)
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)
}
}
if (launcherExe.isNotBlank()) {
into("assets") {
@@ -231,44 +183,26 @@ tasks.shadowJar {
}
tasks.processResources {
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 })
}
inputs.property("terracotta_version", libs.versions.terracotta)
doLast {
upgradeTerracottaConfig.get().checkValid()
into("META-INF/versions/11") {
from(sourceSets["java11"].output)
}
dependsOn(tasks["java11Classes"])
}
fun artifactFile(ext: String) = jarPath.resolveSibling(jarPath.nameWithoutExtension + '.' + ext)
val makeExecutables by tasks.registering {
val extensions = listOf("exe", "sh")
dependsOn(tasks.jar)
inputs.file(jarPath)
outputs.files(extensions.map { artifactFile(it) })
outputs.files(extensions.map { File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + it) })
doLast {
val jarContent = jarPath.readBytes()
ZipFile(jarPath).use { zipFile ->
for (extension in extensions) {
val output = artifactFile(extension)
val output = File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + extension)
val entry = zipFile.getEntry("assets/HMCLauncher.$extension")
?: throw GradleException("HMCLauncher.$extension not found")
@@ -283,31 +217,8 @@ val makeExecutables by tasks.registering {
}
}
val makeDeb by tasks.registering(CreateDeb::class) {
dependsOn(makeExecutables)
val debFile = layout.file(provider { artifactFile("deb") })
val debChannel = when (versionType) {
"stable" -> ReleaseType.STABLE
"dev" -> ReleaseType.DEVELOPMENT
else -> ReleaseType.NIGHTLY
}
version.set(project.version.toString())
releaseType.set(debChannel)
appShFile.set(layout.file(provider { artifactFile("sh") }))
iconFile.set(layout.projectDirectory.file("image/hmcl.png"))
outputFile.set(debFile)
doLast {
createChecksum(debFile.get().asFile)
}
}
tasks.build {
dependsOn(makeExecutables)
dependsOn(makeDeb)
}
fun parseToolOptions(options: String?): MutableList<String> {
@@ -360,16 +271,6 @@ 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)
@@ -378,7 +279,7 @@ tasks.register<JavaExec>("run") {
classpath = files(jarPath)
workingDir = rootProject.rootDir
val vmOptions = parseToolOptions(System.getenv("HMCL_JAVA_OPTS") ?: "-Xmx1g")
val vmOptions = parseToolOptions(System.getenv("HMCL_JAVA_OPTS"))
if (vmOptions.none { it.startsWith("-Dhmcl.offline.auth.restricted=") })
vmOptions += "-Dhmcl.offline.auth.restricted=false"
@@ -398,62 +299,6 @@ tasks.register<JavaExec>("run") {
}
}
// terracotta
val upgradeTerracottaConfig = tasks.register<TerracottaConfigUpgradeTask>("upgradeTerracottaConfig") {
val destination = layout.projectDirectory.file("src/main/resources/assets/terracotta.json")
val source = layout.projectDirectory.file("terracotta-template.json");
classifiers.set(
listOf(
"windows-x86_64", "windows-arm64",
"macos-x86_64", "macos-arm64",
"linux-x86_64", "linux-arm64", "linux-loongarch64", "linux-riscv64",
"freebsd-x86_64"
)
)
version.set(libs.versions.terracotta)
downloadURL.set($$"https://github.com/burningtnt/Terracotta/releases/download/v${version}/terracotta-${version}-${classifier}-pkg.tar.gz")
templateFile.set(source)
outputFile.set(destination)
}
// Check Translations
tasks.register<CheckTranslations>("checkTranslations") {
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.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

@@ -1,129 +0,0 @@
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.jfoenix.controls;
import com.jfoenix.converters.ButtonTypeConverter;
import com.jfoenix.skins.JFXButtonSkin;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.Styleable;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Control;
import javafx.scene.control.Labeled;
import javafx.scene.control.Skin;
import javafx.scene.paint.Paint;
public class JFXButton extends Button {
private static final String DEFAULT_STYLE_CLASS = "jfx-button";
private List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
public JFXButton() {
this.initialize();
}
public JFXButton(String text) {
super(text);
this.initialize();
}
public JFXButton(String text, Node graphic) {
super(text, graphic);
this.initialize();
}
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
protected Skin<?> createDefaultSkin() {
return new JFXButtonSkin(this);
}
private final ObjectProperty<Paint> ripplerFill = new SimpleObjectProperty<>(this, "ripplerFill", null);
public final ObjectProperty<Paint> ripplerFillProperty() {
return this.ripplerFill;
}
public final Paint getRipplerFill() {
return this.ripplerFillProperty().get();
}
public final void setRipplerFill(Paint ripplerFill) {
this.ripplerFillProperty().set(ripplerFill);
}
private final StyleableObjectProperty<ButtonType> buttonType = new SimpleStyleableObjectProperty<>(
JFXButton.StyleableProperties.BUTTON_TYPE, this, "buttonType", JFXButton.ButtonType.FLAT);
public ButtonType getButtonType() {
return this.buttonType == null ? JFXButton.ButtonType.FLAT : this.buttonType.get();
}
public StyleableObjectProperty<ButtonType> buttonTypeProperty() {
return this.buttonType;
}
public void setButtonType(ButtonType type) {
this.buttonType.set(type);
}
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
if (this.STYLEABLES == null) {
List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
styleables.addAll(getClassCssMetaData());
styleables.addAll(Labeled.getClassCssMetaData());
this.STYLEABLES = List.copyOf(styleables);
}
return this.STYLEABLES;
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return JFXButton.StyleableProperties.CHILD_STYLEABLES;
}
protected void layoutChildren() {
super.layoutChildren();
this.setNeedsLayout(false);
}
public enum ButtonType {
FLAT,
RAISED;
}
private static final class StyleableProperties {
private static final CssMetaData<JFXButton, ButtonType> BUTTON_TYPE;
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
BUTTON_TYPE = new CssMetaData<>("-jfx-button-type", ButtonTypeConverter.getInstance(), JFXButton.ButtonType.FLAT) {
public boolean isSettable(JFXButton control) {
return control.buttonType == null || !control.buttonType.isBound();
}
public StyleableProperty<ButtonType> getStyleableProperty(JFXButton control) {
return control.buttonTypeProperty();
}
};
List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
Collections.addAll(styleables, BUTTON_TYPE);
CHILD_STYLEABLES = List.copyOf(styleables);
}
}
}
@@ -1,169 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.skins.JFXCheckBoxSkin;
import javafx.css.*;
import javafx.css.converter.PaintConverter;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Skin;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/// JFXCheckBox is the material design implementation of a checkbox.
/// it shows ripple effect and a custom selection animation.
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXCheckBox extends CheckBox {
public JFXCheckBox(String text) {
super(text);
initialize();
}
public JFXCheckBox() {
initialize();
}
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
@Override
protected Skin<?> createDefaultSkin() {
return new JFXCheckBoxSkin(this);
}
/***************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
/// Initialize the style class to 'jfx-check-box'.
///
/// This is the selector class from which CSS can be used to style
/// this control.
private static final String DEFAULT_STYLE_CLASS = "jfx-check-box";
/// checkbox color property when selected
private StyleableObjectProperty<Paint> checkedColor;
private static final Color DEFAULT_CHECKED_COLOR = Color.valueOf("#0F9D58");
public StyleableObjectProperty<Paint> checkedColorProperty() {
if (checkedColor == null) {
checkedColor = new SimpleStyleableObjectProperty<>(StyleableProperties.CHECKED_COLOR,
JFXCheckBox.this,
"checkedColor",
DEFAULT_CHECKED_COLOR);
}
return this.checkedColor;
}
public Paint getCheckedColor() {
return checkedColor == null ? DEFAULT_CHECKED_COLOR : checkedColor.get();
}
public void setCheckedColor(Paint color) {
this.checkedColor.set(color);
}
/**
* checkbox color property when not selected
*/
private StyleableObjectProperty<Paint> unCheckedColor;
private static final Color DEFAULT_UNCHECKED_COLOR = Color.valueOf("#5A5A5A");
public StyleableObjectProperty<Paint> unCheckedColorProperty() {
if (unCheckedColor == null) {
unCheckedColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNCHECKED_COLOR,
JFXCheckBox.this,
"unCheckedColor",
DEFAULT_UNCHECKED_COLOR);
}
return this.unCheckedColor;
}
public Paint getUnCheckedColor() {
return unCheckedColor == null ? DEFAULT_UNCHECKED_COLOR : unCheckedColor.get();
}
public void setUnCheckedColor(Paint color) {
this.unCheckedColor.set(color);
}
private static final class StyleableProperties {
private static final CssMetaData<JFXCheckBox, Paint> CHECKED_COLOR =
new CssMetaData<>("-jfx-checked-color",
PaintConverter.getInstance(), DEFAULT_CHECKED_COLOR) {
@Override
public boolean isSettable(JFXCheckBox control) {
return control.checkedColor == null || !control.checkedColor.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXCheckBox control) {
return control.checkedColorProperty();
}
};
private static final CssMetaData<JFXCheckBox, Paint> UNCHECKED_COLOR =
new CssMetaData<>("-jfx-unchecked-color",
PaintConverter.getInstance(), DEFAULT_UNCHECKED_COLOR) {
@Override
public boolean isSettable(JFXCheckBox control) {
return control.unCheckedColor == null || !control.unCheckedColor.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXCheckBox control) {
return control.unCheckedColorProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables =
new ArrayList<>(CheckBox.getClassCssMetaData());
Collections.addAll(styleables,
CHECKED_COLOR,
UNCHECKED_COLOR
);
CHILD_STYLEABLES = List.copyOf(styleables);
}
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
}
@@ -1,60 +0,0 @@
/*
* 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());
}
}
@@ -1,145 +0,0 @@
/*
* 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;
}
}
@@ -1,357 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.converters.base.NodeConverter;
import com.jfoenix.skins.JFXComboBoxListViewSkin;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ObservableList;
import javafx.css.*;
import javafx.css.converter.BooleanConverter;
import javafx.css.converter.PaintConverter;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.util.StringConverter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu;
/**
* JFXComboBox is the material design implementation of a combobox.
*
* @author Shadi Shaheen
* @version 1.0
* @since 2016-03-09
*/
public class JFXComboBox<T> extends ComboBox<T> {
/**
* {@inheritDoc}
*/
public JFXComboBox() {
initialize();
}
/**
* {@inheritDoc}
*/
public JFXComboBox(ObservableList<T> items) {
super(items);
initialize();
}
private void initialize() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
this.setCellFactory(listView -> new JFXListCell<T>() {
@Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
updateDisplayText(this, item, empty);
}
});
// had to refactor the code out of the skin class to allow
// customization of the button cell
this.setButtonCell(new ListCell<T>() {
{
// fixed clearing the combo box value is causing
// java prompt text to be shown because the button cell is not updated
JFXComboBox.this.valueProperty().addListener(observable -> {
if (JFXComboBox.this.getValue() == null) {
updateItem(null, true);
}
});
}
@Override
protected void updateItem(T item, boolean empty) {
updateDisplayText(this, item, empty);
this.setVisible(item != null || !empty);
}
});
useJFXContextMenu(editorProperty().get());
}
/**
* {@inheritDoc}
*/
@Override
protected Skin<?> createDefaultSkin() {
return new JFXComboBoxListViewSkin<T>(this);
}
/**
* Initialize the style class to 'jfx-combo-box'.
* <p>
* This is the selector class from which CSS can be used to style
* this control.
*/
private static final String DEFAULT_STYLE_CLASS = "jfx-combo-box";
/***************************************************************************
* *
* Node Converter Property *
* *
**************************************************************************/
/**
* Converts the user-typed input (when the ComboBox is
* {@link #editableProperty() editable}) to an object of type T, such that
* the input may be retrieved via the {@link #valueProperty() value} property.
*/
public ObjectProperty<NodeConverter<T>> nodeConverterProperty() {
return nodeConverter;
}
private ObjectProperty<NodeConverter<T>> nodeConverter = new SimpleObjectProperty<>(this, "nodeConverter",
JFXComboBox.<T>defaultNodeConverter());
public final void setNodeConverter(NodeConverter<T> value) {
nodeConverterProperty().set(value);
}
public final NodeConverter<T> getNodeConverter() {
return nodeConverterProperty().get();
}
private static <T> NodeConverter<T> defaultNodeConverter() {
return new NodeConverter<T>() {
@Override
public Node toNode(T object) {
if (object == null) {
return null;
}
StackPane selectedValueContainer = new StackPane();
selectedValueContainer.getStyleClass().add("combo-box-selected-value-container");
selectedValueContainer.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null)));
Label selectedValueLabel = object instanceof Label ? new Label(((Label) object).getText()) : new Label(
object.toString());
selectedValueLabel.setTextFill(Color.BLACK);
selectedValueContainer.getChildren().add(selectedValueLabel);
StackPane.setAlignment(selectedValueLabel, Pos.CENTER_LEFT);
StackPane.setMargin(selectedValueLabel, new Insets(0, 0, 0, 5));
return selectedValueContainer;
}
@SuppressWarnings("unchecked")
@Override
public T fromNode(Node node) {
return (T) node;
}
@Override
public String toString(T object) {
if (object == null) {
return null;
}
if (object instanceof Label) {
return ((Label) object).getText();
}
return object.toString();
}
};
}
private boolean updateDisplayText(ListCell<T> cell, T item, boolean empty) {
if (empty) {
// create empty cell
if (cell == null) {
return true;
}
cell.setGraphic(null);
cell.setText(null);
return true;
} else if (item instanceof Node) {
Node currentNode = cell.getGraphic();
Node newNode = (Node) item;
// create a node from the selected node of the listview
// using JFXComboBox {@link #nodeConverterProperty() NodeConverter})
NodeConverter<T> nc = this.getNodeConverter();
Node node = nc == null ? null : nc.toNode(item);
if (currentNode == null || !currentNode.equals(newNode)) {
cell.setText(null);
cell.setGraphic(node == null ? newNode : node);
}
return node == null;
} else {
// run item through StringConverter if it isn't null
StringConverter<T> c = this.getConverter();
String s = item == null ? this.getPromptText() : (c == null ? item.toString() : c.toString(item));
cell.setText(s);
cell.setGraphic(null);
return s == null || s.isEmpty();
}
}
/***************************************************************************
* *
* styleable Properties *
* *
**************************************************************************/
/**
* set true to show a float the prompt text when focusing the field
*/
private StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT,
JFXComboBox.this,
"lableFloat",
false);
public final StyleableBooleanProperty labelFloatProperty() {
return this.labelFloat;
}
public final boolean isLabelFloat() {
return this.labelFloatProperty().get();
}
public final void setLabelFloat(final boolean labelFloat) {
this.labelFloatProperty().set(labelFloat);
}
/**
* default color used when the field is unfocused
*/
private StyleableObjectProperty<Paint> unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR,
JFXComboBox.this,
"unFocusColor",
Color.rgb(77,
77,
77));
public Paint getUnFocusColor() {
return unFocusColor == null ? Color.rgb(77, 77, 77) : unFocusColor.get();
}
public StyleableObjectProperty<Paint> unFocusColorProperty() {
return this.unFocusColor;
}
public void setUnFocusColor(Paint color) {
this.unFocusColor.set(color);
}
/**
* default color used when the field is focused
*/
private StyleableObjectProperty<Paint> focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR,
JFXComboBox.this,
"focusColor",
Color.valueOf("#4059A9"));
public Paint getFocusColor() {
return focusColor == null ? Color.valueOf("#4059A9") : focusColor.get();
}
public StyleableObjectProperty<Paint> focusColorProperty() {
return this.focusColor;
}
public void setFocusColor(Paint color) {
this.focusColor.set(color);
}
private final static class StyleableProperties {
private static final CssMetaData<JFXComboBox<?>, Paint> UNFOCUS_COLOR = new CssMetaData<JFXComboBox<?>, Paint>(
"-jfx-unfocus-color",
PaintConverter.getInstance(),
Color.valueOf("#A6A6A6")) {
@Override
public boolean isSettable(JFXComboBox<?> control) {
return control.unFocusColor == null || !control.unFocusColor.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXComboBox<?> control) {
return control.unFocusColorProperty();
}
};
private static final CssMetaData<JFXComboBox<?>, Paint> FOCUS_COLOR = new CssMetaData<JFXComboBox<?>, Paint>(
"-jfx-focus-color",
PaintConverter.getInstance(),
Color.valueOf("#3f51b5")) {
@Override
public boolean isSettable(JFXComboBox<?> control) {
return control.focusColor == null || !control.focusColor.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXComboBox<?> control) {
return control.focusColorProperty();
}
};
private static final CssMetaData<JFXComboBox<?>, Boolean> LABEL_FLOAT = new CssMetaData<JFXComboBox<?>, Boolean>(
"-jfx-label-float",
BooleanConverter.getInstance(),
false) {
@Override
public boolean isSettable(JFXComboBox<?> control) {
return control.labelFloat == null || !control.labelFloat.isBound();
}
@Override
public StyleableBooleanProperty getStyleableProperty(JFXComboBox<?> control) {
return control.labelFloatProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(
Control.getClassCssMetaData());
Collections.addAll(styleables, UNFOCUS_COLOR, FOCUS_COLOR, LABEL_FLOAT);
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
}
}
// inherit the styleable properties from parent
private List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
if (STYLEABLES == null) {
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(
Control.getClassCssMetaData());
styleables.addAll(getClassCssMetaData());
styleables.addAll(Control.getClassCssMetaData());
STYLEABLES = Collections.unmodifiableList(styleables);
}
return STYLEABLES;
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
}
@@ -1,531 +0,0 @@
/*
* 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.*;
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.*;
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.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();
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);
}
@Override
protected void starting() {
}
@Override
protected void stopping() {
}
}
/***************************************************************************
* *
* 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();
}
}
@@ -1,104 +0,0 @@
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.jfoenix.controls;
import java.util.List;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
public class JFXDialogLayout extends StackPane {
private final StackPane heading = new StackPane();
private final StackPane body = new StackPane();
private final FlowPane actions = new FlowPane() {
protected double computeMinWidth(double height) {
if (this.getContentBias() == Orientation.HORIZONTAL) {
double maxPref = 0.0;
for (Node child : this.getChildren()) {
if (child.isManaged()) {
maxPref = Math.max(maxPref, child.minWidth(-1.0));
}
}
Insets insets = this.getInsets();
return insets.getLeft() + this.snapSizeX(maxPref) + insets.getRight();
} else {
return this.computePrefWidth(height);
}
}
protected double computeMinHeight(double width) {
if (this.getContentBias() == Orientation.VERTICAL) {
double maxPref = 0.0;
for (Node child : this.getChildren()) {
if (child.isManaged()) {
maxPref = Math.max(maxPref, child.minHeight(-1.0));
}
}
Insets insets = this.getInsets();
return insets.getTop() + this.snapSizeY(maxPref) + insets.getBottom();
} else {
return this.computePrefHeight(width);
}
}
};
private static final String DEFAULT_STYLE_CLASS = "jfx-dialog-layout";
public JFXDialogLayout() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
VBox layout = new VBox();
this.heading.getStyleClass().add("jfx-layout-heading");
this.heading.getStyleClass().add("title");
this.body.getStyleClass().add("jfx-layout-body");
this.body.prefHeightProperty().bind(this.prefHeightProperty());
this.body.prefWidthProperty().bind(this.prefWidthProperty());
this.actions.getStyleClass().add("jfx-layout-actions");
layout.getChildren().setAll(this.heading, this.body, this.actions);
this.getChildren().add(layout);
}
public ObservableList<Node> getHeading() {
return this.heading.getChildren();
}
public void setHeading(Node... titleContent) {
this.heading.getChildren().setAll(titleContent);
}
public ObservableList<Node> getBody() {
return this.body.getChildren();
}
public void setBody(Node... body) {
this.body.getChildren().setAll(body);
}
public ObservableList<Node> getActions() {
return this.actions.getChildren();
}
public void setActions(Node... actions) {
this.actions.getChildren().setAll(actions);
}
public void setActions(List<? extends Node> actions) {
this.actions.getChildren().setAll(actions);
}
}
@@ -1,271 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.utils.JFXNodeUtils;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.util.Duration;
import java.util.Set;
/// material design implementation of ListCell
///
/// By default, JFXListCell will try to create a graphic node for the cell,
/// to override it you need to set graphic to null in [#updateItem(Object, boolean)] method.
///
/// NOTE: passive nodes (Labels and Shapes) will be set to mouse transparent in order to
/// show the ripple effect upon clicking , to change this behavior you can override the
/// method {[#makeChildrenTransparent()]
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXListCell<T> extends ListCell<T> {
protected JFXRippler cellRippler = new JFXRippler(this) {
@Override
protected Node getMask() {
Region clip = new Region();
JFXNodeUtils.updateBackground(JFXListCell.this.getBackground(), clip);
double width = control.getLayoutBounds().getWidth();
double height = control.getLayoutBounds().getHeight();
clip.resize(width, height);
return clip;
}
@Override
protected void positionControl(Node control) {
// do nothing
}
};
protected Node cellContent;
private Rectangle clip;
private Timeline gapAnimation;
private boolean playExpandAnimation = false;
private boolean selectionChanged = false;
/**
* {@inheritDoc}
*/
public JFXListCell() {
initialize();
initListeners();
}
/**
* init listeners to update the vertical gap / selection animation
*/
private void initListeners() {
listViewProperty().addListener((listObj, oldList, newList) -> {
if (newList instanceof JFXListView<?> listView) {
listView.currentVerticalGapProperty().addListener((o, oldVal, newVal) -> {
cellRippler.rippler.setClip(null);
if (newVal.doubleValue() != 0) {
playExpandAnimation = true;
getListView().requestLayout();
} else {
// fake expand state
double gap = clip.getY() * 2;
gapAnimation = new Timeline(
new KeyFrame(Duration.millis(240),
new KeyValue(this.translateYProperty(),
-gap / 2 - (gap * (getIndex())),
Interpolator.EASE_BOTH)
));
gapAnimation.play();
gapAnimation.setOnFinished((finish) -> {
requestLayout();
Platform.runLater(() -> getListView().requestLayout());
});
}
});
selectedProperty().addListener((o, oldVal, newVal) -> {
if (newVal) {
selectionChanged = true;
}
});
}
});
}
@Override
protected void layoutChildren() {
super.layoutChildren();
cellRippler.resizeRelocate(0, 0, getWidth(), getHeight());
double gap = getGap();
if (clip == null) {
clip = new Rectangle(0, gap / 2, getWidth(), getHeight() - gap);
setClip(clip);
} else {
if (gap != 0) {
if (playExpandAnimation || selectionChanged) {
// fake list collapse state
if (playExpandAnimation) {
this.setTranslateY(-gap / 2 + (-gap * (getIndex())));
clip.setY(gap / 2);
clip.setHeight(getHeight() - gap);
gapAnimation = new Timeline(new KeyFrame(Duration.millis(240),
new KeyValue(this.translateYProperty(),
0,
Interpolator.EASE_BOTH)));
playExpandAnimation = false;
} else if (selectionChanged) {
clip.setY(0);
clip.setHeight(getHeight());
gapAnimation = new Timeline(
new KeyFrame(Duration.millis(240),
new KeyValue(clip.yProperty(), gap / 2, Interpolator.EASE_BOTH),
new KeyValue(clip.heightProperty(), getHeight() - gap, Interpolator.EASE_BOTH)
));
}
playExpandAnimation = false;
selectionChanged = false;
gapAnimation.play();
} else {
if (gapAnimation != null) {
gapAnimation.stop();
}
this.setTranslateY(0);
clip.setY(gap / 2);
clip.setHeight(getHeight() - gap);
}
} else {
this.setTranslateY(0);
clip.setY(0);
clip.setHeight(getHeight());
}
clip.setX(0);
clip.setWidth(getWidth());
}
if (!getChildren().contains(cellRippler)) {
makeChildrenTransparent();
getChildren().add(0, cellRippler);
cellRippler.rippler.clear();
}
}
/**
* this method is used to set some nodes in cell content as mouse transparent nodes
* so clicking on them will trigger the ripple effect.
*/
protected void makeChildrenTransparent() {
for (Node child : getChildren()) {
if (child instanceof Label) {
Set<Node> texts = child.lookupAll("Text");
for (Node text : texts) {
text.setMouseTransparent(true);
}
} else if (child instanceof Shape) {
child.setMouseTransparent(true);
}
}
}
/**
* {@inheritDoc}
*/
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
// remove empty (Trailing cells)
setMouseTransparent(true);
setStyle("-fx-background-color:TRANSPARENT;");
} else {
setMouseTransparent(false);
setStyle(null);
if (item instanceof Node newNode) {
setText(null);
Node currentNode = getGraphic();
if (currentNode == null || !currentNode.equals(newNode)) {
cellContent = newNode;
cellRippler.rippler.cacheRippleClip(false);
// build the Cell node
// RIPPLER ITEM : in case if the list item has its own rippler bind the list rippler and item rippler properties
if (newNode instanceof JFXRippler newRippler) {
// build cell container from exisiting rippler
cellRippler.ripplerFillProperty().bind(newRippler.ripplerFillProperty());
cellRippler.maskTypeProperty().bind(newRippler.maskTypeProperty());
cellRippler.positionProperty().bind(newRippler.positionProperty());
cellContent = newRippler.getControl();
}
((Region) cellContent).setMaxHeight(cellContent.prefHeight(-1));
setGraphic(cellContent);
}
} else {
setText(item == null ? "null" : item.toString());
setGraphic(null);
}
// show cell tooltip if it's toggled in JFXListView
if (getListView() instanceof JFXListView<?> listView && listView.isShowTooltip()) {
if (item instanceof Label label) {
setTooltip(new Tooltip(label.getText()));
} else if (getText() != null) {
setTooltip(new Tooltip(getText()));
}
}
}
}
// Stylesheet Handling *
/**
* Initialize the style class to 'jfx-list-cell'.
* <p>
* This is the selector class from which CSS can be used to style
* this control.
*/
private static final String DEFAULT_STYLE_CLASS = "jfx-list-cell";
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
this.setPadding(new Insets(8, 12, 8, 12));
}
@Override
protected double computePrefHeight(double width) {
double gap = getGap();
return super.computePrefHeight(width) + gap;
}
private double getGap() {
return (getListView() instanceof JFXListView<?> listView)
? (listView.isExpanded() ? listView.currentVerticalGapProperty().get() : 0)
: 0;
}
}
@@ -1,258 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.skins.JFXListViewSkin;
import javafx.beans.property.*;
import javafx.css.*;
import javafx.css.converter.BooleanConverter;
import javafx.css.converter.SizeConverter;
import javafx.scene.control.ListView;
import javafx.scene.control.Skin;
import javafx.scene.input.MouseEvent;
import java.util.*;
/// Material design implementation of List View
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXListView<T> extends ListView<T> {
/**
* {@inheritDoc}
*/
public JFXListView() {
this.setCellFactory(listView -> new JFXListCell<>());
initialize();
}
/**
* {@inheritDoc}
*/
@Override
protected Skin<?> createDefaultSkin() {
return new JFXListViewSkin<>(this);
}
private IntegerProperty depth;
public IntegerProperty depthProperty() {
if (depth == null) {
depth = new SimpleIntegerProperty(this, "depth", 0);
}
return depth;
}
public int getDepth() {
return depth != null ? depth.get() : 0;
}
public void setDepth(int depth) {
depthProperty().set(depth);
}
private DoubleProperty currentVerticalGap;
DoubleProperty currentVerticalGapProperty() {
if (currentVerticalGap == null) {
currentVerticalGap = new SimpleDoubleProperty(this, "currentVerticalGap");
}
return currentVerticalGap;
}
private void updateVerticalGap() {
if (isExpanded()) {
currentVerticalGapProperty().set(verticalGap.get());
} else {
currentVerticalGapProperty().set(0);
}
}
/*
* this only works if the items were labels / strings
*/
private BooleanProperty showTooltip;
public final BooleanProperty showTooltipProperty() {
if (showTooltip == null) {
showTooltip = new SimpleBooleanProperty(this, "showTooltip", false);
}
return this.showTooltip;
}
public final boolean isShowTooltip() {
return showTooltip != null && showTooltip.get();
}
public final void setShowTooltip(final boolean showTooltip) {
this.showTooltipProperty().set(showTooltip);
}
/***************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
/// Initialize the style class to 'jfx-list-view'.
///
/// This is the selector class from which CSS can be used to style
/// this control.
private static final String DEFAULT_STYLE_CLASS = "jfx-list-view";
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
/**
* propagate mouse events to the parent node ( e.g. to allow dragging while clicking on the list)
*/
public void propagateMouseEventsToParent() {
this.addEventHandler(MouseEvent.ANY, e -> {
e.consume();
this.getParent().fireEvent(e);
});
}
private StyleableDoubleProperty verticalGap;
public StyleableDoubleProperty verticalGapProperty() {
if (this.verticalGap == null) {
this.verticalGap = new StyleableDoubleProperty(0.0) {
@Override
public Object getBean() {
return JFXListView.this;
}
@Override
public String getName() {
return "verticalGap";
}
@Override
public CssMetaData<? extends Styleable, Number> getCssMetaData() {
return StyleableProperties.VERTICAL_GAP;
}
@Override
protected void invalidated() {
updateVerticalGap();
}
};
}
return this.verticalGap;
}
public Double getVerticalGap() {
return verticalGap == null ? 0.0 : verticalGap.get();
}
public void setVerticalGap(Double gap) {
verticalGapProperty().set(gap);
}
private StyleableBooleanProperty expanded;
public StyleableBooleanProperty expandedProperty() {
if (expanded == null) {
expanded = new StyleableBooleanProperty(false) {
@Override
public Object getBean() {
return JFXListView.this;
}
@Override
public String getName() {
return "expanded";
}
@Override
public CssMetaData<? extends Styleable, Boolean> getCssMetaData() {
return StyleableProperties.EXPANDED;
}
@Override
protected void invalidated() {
updateVerticalGap();
}
};
}
return this.expanded;
}
public Boolean isExpanded() {
return expanded != null && expanded.get();
}
public void setExpanded(Boolean expanded) {
expandedProperty().set(expanded);
}
private static final class StyleableProperties {
private static final CssMetaData<JFXListView<?>, Number> VERTICAL_GAP =
new CssMetaData<>("-jfx-vertical-gap",
SizeConverter.getInstance(), 0) {
@Override
public boolean isSettable(JFXListView<?> control) {
return control.verticalGap == null || !control.verticalGap.isBound();
}
@Override
public StyleableDoubleProperty getStyleableProperty(JFXListView<?> control) {
return control.verticalGapProperty();
}
};
private static final CssMetaData<JFXListView<?>, Boolean> EXPANDED =
new CssMetaData<>("-jfx-expanded",
BooleanConverter.getInstance(), false) {
@Override
public boolean isSettable(JFXListView<?> control) {
// it's only settable if the List is not shown yet
return control.getHeight() == 0 && (control.expanded == null || !control.expanded.isBound());
}
@Override
public StyleableBooleanProperty getStyleableProperty(JFXListView<?> control) {
return control.expandedProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables =
new ArrayList<>(ListView.getClassCssMetaData());
Collections.addAll(styleables, VERTICAL_GAP, EXPANDED);
CHILD_STYLEABLES = List.copyOf(styleables);
}
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
}
@@ -1,297 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.skins.JFXPasswordFieldSkin;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.*;
import javafx.css.converter.BooleanConverter;
import javafx.css.converter.PaintConverter;
import javafx.scene.control.Control;
import javafx.scene.control.PasswordField;
import javafx.scene.control.Skin;
import javafx.scene.control.TextField;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu;
/**
* JFXPasswordField is the material design implementation of a password Field.
*
* @author Shadi Shaheen
* @version 1.0
* @since 2016-03-09
*/
public class JFXPasswordField extends PasswordField {
/**
* {@inheritDoc}
*/
public JFXPasswordField() {
initialize();
}
/**
* {@inheritDoc}
*/
@Override
protected Skin<?> createDefaultSkin() {
return new JFXPasswordFieldSkin(this);
}
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
if ("dalvik".equals(System.getProperty("java.vm.name").toLowerCase(Locale.ROOT))) {
this.setStyle("-fx-skin: \"com.jfoenix.android.skins.JFXPasswordFieldSkinAndroid\";");
}
useJFXContextMenu(this);
}
/**
* Initialize the style class to 'jfx-password-field'.
* <p>
* This is the selector class from which CSS can be used to style
* this control.
*/
private static final String DEFAULT_STYLE_CLASS = "jfx-password-field";
/***************************************************************************
* *
* Properties *
* *
**************************************************************************/
/**
* holds the current active validator on the password field in case of validation error
*/
private ReadOnlyObjectWrapper<ValidatorBase> activeValidator = new ReadOnlyObjectWrapper<>();
public ValidatorBase getActiveValidator() {
return activeValidator == null ? null : activeValidator.get();
}
public ReadOnlyObjectProperty<ValidatorBase> activeValidatorProperty() {
return this.activeValidator.getReadOnlyProperty();
}
/**
* list of validators that will validate the password value upon calling
* {{@link #validate()}
*/
private ObservableList<ValidatorBase> validators = FXCollections.observableArrayList();
public ObservableList<ValidatorBase> getValidators() {
return validators;
}
public void setValidators(ValidatorBase... validators) {
this.validators.addAll(validators);
}
/**
* validates the password value using the list of validators provided by the user
* {{@link #setValidators(ValidatorBase...)}
*
* @return true if the value is valid else false
*/
public boolean validate() {
for (ValidatorBase validator : validators) {
if (validator.getSrcControl() == null) {
validator.setSrcControl(this);
}
validator.validate();
if (validator.getHasErrors()) {
activeValidator.set(validator);
return false;
}
}
activeValidator.set(null);
return true;
}
public void resetValidation() {
pseudoClassStateChanged(ValidatorBase.PSEUDO_CLASS_ERROR, false);
activeValidator.set(null);
}
/***************************************************************************
* *
* styleable Properties *
* *
**************************************************************************/
/**
* set true to show a float the prompt text when focusing the field
*/
private StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT, JFXPasswordField.this, "lableFloat", false);
public final StyleableBooleanProperty labelFloatProperty() {
return this.labelFloat;
}
public final boolean isLabelFloat() {
return this.labelFloatProperty().get();
}
public final void setLabelFloat(final boolean labelFloat) {
this.labelFloatProperty().set(labelFloat);
}
/**
* default color used when the field is unfocused
*/
private StyleableObjectProperty<Paint> unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR, JFXPasswordField.this, "unFocusColor", Color.rgb(77, 77, 77));
public Paint getUnFocusColor() {
return unFocusColor == null ? Color.rgb(77, 77, 77) : unFocusColor.get();
}
public StyleableObjectProperty<Paint> unFocusColorProperty() {
return this.unFocusColor;
}
public void setUnFocusColor(Paint color) {
this.unFocusColor.set(color);
}
/**
* default color used when the field is focused
*/
private StyleableObjectProperty<Paint> focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR, JFXPasswordField.this, "focusColor", Color.valueOf("#4059A9"));
public Paint getFocusColor() {
return focusColor == null ? Color.valueOf("#4059A9") : focusColor.get();
}
public StyleableObjectProperty<Paint> focusColorProperty() {
return this.focusColor;
}
public void setFocusColor(Paint color) {
this.focusColor.set(color);
}
/**
* disable animation on validation
*/
private StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION, JFXPasswordField.this, "disableAnimation", false);
public final StyleableBooleanProperty disableAnimationProperty() {
return this.disableAnimation;
}
public final Boolean isDisableAnimation() {
return disableAnimation != null && this.disableAnimationProperty().get();
}
public final void setDisableAnimation(final Boolean disabled) {
this.disableAnimationProperty().set(disabled);
}
private final static class StyleableProperties {
private static final CssMetaData<JFXPasswordField, Paint> UNFOCUS_COLOR = new CssMetaData<JFXPasswordField, Paint>("-jfx-unfocus-color", PaintConverter.getInstance(), Color.valueOf("#A6A6A6")) {
@Override
public boolean isSettable(JFXPasswordField control) {
return control.unFocusColor == null || !control.unFocusColor.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXPasswordField control) {
return control.unFocusColorProperty();
}
};
private static final CssMetaData<JFXPasswordField, Paint> FOCUS_COLOR = new CssMetaData<JFXPasswordField, Paint>("-jfx-focus-color", PaintConverter.getInstance(), Color.valueOf("#3f51b5")) {
@Override
public boolean isSettable(JFXPasswordField control) {
return control.focusColor == null || !control.focusColor.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXPasswordField control) {
return control.focusColorProperty();
}
};
private static final CssMetaData<JFXPasswordField, Boolean> LABEL_FLOAT = new CssMetaData<JFXPasswordField, Boolean>("-jfx-label-float", BooleanConverter.getInstance(), false) {
@Override
public boolean isSettable(JFXPasswordField control) {
return control.labelFloat == null || !control.labelFloat.isBound();
}
@Override
public StyleableBooleanProperty getStyleableProperty(JFXPasswordField control) {
return control.labelFloatProperty();
}
};
private static final CssMetaData<JFXPasswordField, Boolean> DISABLE_ANIMATION = new CssMetaData<JFXPasswordField, Boolean>("-fx-disable-animation", BooleanConverter.getInstance(), false) {
@Override
public boolean isSettable(JFXPasswordField control) {
return control.disableAnimation == null || !control.disableAnimation.isBound();
}
@Override
public StyleableBooleanProperty getStyleableProperty(JFXPasswordField control) {
return control.disableAnimationProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
Collections.addAll(styleables, UNFOCUS_COLOR, FOCUS_COLOR, LABEL_FLOAT, DISABLE_ANIMATION);
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
}
}
// inherit the styleable properties from parent
private List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
if (STYLEABLES == null) {
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
styleables.addAll(getClassCssMetaData());
styleables.addAll(TextField.getClassCssMetaData());
STYLEABLES = Collections.unmodifiableList(styleables);
}
return STYLEABLES;
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
}
@@ -1,188 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.skins.JFXPopupSkin;
import javafx.application.Platform;
import javafx.beans.DefaultProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.NodeOrientation;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.PopupControl;
import javafx.scene.control.Skin;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.stage.Window;
/// JFXPopup is the material design implementation of a popup.
///
/// @author Shadi Shaheen
/// @version 2.0
/// @since 2017-03-01
@DefaultProperty(value = "popupContent")
public class JFXPopup extends PopupControl {
public enum PopupHPosition {
RIGHT,
LEFT;
public PopupHPosition getOpposite() {
return (this == RIGHT) ? LEFT : RIGHT;
}
}
public enum PopupVPosition {
TOP, BOTTOM
}
/// Creates empty popup.
public JFXPopup() {
this(null);
}
/// creates popup with a specified container and content
///
/// @param content the node that will be shown in the popup
public JFXPopup(Region content) {
setPopupContent(content);
initialize();
}
private void initialize() {
this.setAutoFix(false);
this.setAutoHide(true);
this.setHideOnEscape(true);
this.setConsumeAutoHidingEvents(false);
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
@Override
protected Skin<?> createDefaultSkin() {
return new JFXPopupSkin(this);
}
/***************************************************************************
* *
* Setters / Getters *
* *
**************************************************************************/
private final ObjectProperty<Region> popupContent = new SimpleObjectProperty<>(new Pane());
public final ObjectProperty<Region> popupContentProperty() {
return this.popupContent;
}
public final Region getPopupContent() {
return this.popupContentProperty().get();
}
public final void setPopupContent(final Region popupContent) {
this.popupContentProperty().set(popupContent);
}
/***************************************************************************
* *
* Public API *
* *
**************************************************************************/
/// show the popup using the default position
public void show(Node node) {
this.show(node, PopupVPosition.TOP, PopupHPosition.LEFT, 0, 0);
}
/// show the popup according to the specified position
///
/// @param vAlign can be TOP/BOTTOM
/// @param hAlign can be LEFT/RIGHT
public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign) {
this.show(node, vAlign, hAlign, 0, 0);
}
/// show the popup according to the specified position with a certain offset
///
/// @param vAlign can be TOP/BOTTOM
/// @param hAlign can be LEFT/RIGHT
/// @param initOffsetX on the x-axis
/// @param initOffsetY on the y-axis
public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign, double initOffsetX, double initOffsetY) {
show(node, vAlign, hAlign, initOffsetX, initOffsetY, false);
}
public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign, double initOffsetX, double initOffsetY, boolean attachToNode) {
if (!isShowing()) {
Scene scene = node.getScene();
if (scene == null || scene.getWindow() == null) {
throw new IllegalStateException("Can not show popup. The node must be attached to a scene/window.");
}
Window parent = scene.getWindow();
final Point2D origin = node.localToScene(0, 0);
boolean isRTL = node.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT;
double anchorX = parent.getX() + scene.getX() + origin.getX() + (hAlign == PopupHPosition.RIGHT ? ((Region) node).getWidth() : 0);
double anchorY = parent.getY() + origin.getY() + scene.getY() + (vAlign == PopupVPosition.BOTTOM ? ((Region) node).getHeight() : 0);
if (attachToNode)
this.show(node, anchorX, anchorY);
else
this.show(parent, anchorX, anchorY);
((JFXPopupSkin) getSkin()).reset(vAlign, isRTL ? hAlign.getOpposite() : hAlign, isRTL ? -initOffsetX : initOffsetX, initOffsetY);
Platform.runLater(() -> ((JFXPopupSkin) getSkin()).animate());
}
}
public void show(Window window, double x, double y, PopupVPosition vAlign, PopupHPosition hAlign, double initOffsetX, double initOffsetY) {
if (!isShowing()) {
if (window == null) {
throw new IllegalStateException("Can not show popup. The node must be attached to a scene/window.");
}
Window parent = window;
final double anchorX = parent.getX() + x + initOffsetX;
final double anchorY = parent.getY() + y + initOffsetY;
this.show(parent, anchorX, anchorY);
((JFXPopupSkin) getSkin()).reset(vAlign, hAlign, initOffsetX, initOffsetY);
Platform.runLater(() -> ((JFXPopupSkin) getSkin()).animate());
}
}
@Override
public void hide() {
super.hide();
((JFXPopupSkin) getSkin()).init();
}
/***************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
/// Initialize the style class to 'jfx-popup'.
///
/// This is the selector class from which CSS can be used to style
/// this control.
private static final String DEFAULT_STYLE_CLASS = "jfx-popup";
}
@@ -1,66 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.skins.JFXProgressBarSkin;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.Skin;
/// JFXProgressBar is the material design implementation of a progress bar.
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXProgressBar extends ProgressBar {
/// Initialize the style class to 'jfx-progress-bar'.
///
/// This is the selector class from which CSS can be used to style
/// this control.
private static final String DEFAULT_STYLE_CLASS = "jfx-progress-bar";
public JFXProgressBar() {
initialize();
}
public JFXProgressBar(double progress) {
super(progress);
initialize();
}
@Override
protected Skin<?> createDefaultSkin() {
return new JFXProgressBarSkin(this);
}
private boolean smoothProgress = true;
public boolean isSmoothProgress() {
return smoothProgress;
}
public void setSmoothProgress(boolean smoothProgress) {
this.smoothProgress = smoothProgress;
}
private void initialize() {
setPrefWidth(200);
getStyleClass().add(DEFAULT_STYLE_CLASS);
}
}
@@ -1,160 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.skins.JFXRadioButtonSkin;
import javafx.css.*;
import javafx.css.converter.ColorConverter;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Skin;
import javafx.scene.paint.Color;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/// JFXRadioButton is the material design implementation of a radio button.
///
/// @author Bashir Elias & Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXRadioButton extends RadioButton {
public JFXRadioButton(String text) {
super(text);
initialize();
}
public JFXRadioButton() {
initialize();
}
@Override
protected Skin<?> createDefaultSkin() {
return new JFXRadioButtonSkin(this);
}
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
/// Initialize the style class to 'jfx-radio-button'.
///
/// This is the selector class from which CSS can be used to style
/// this control.
private static final String DEFAULT_STYLE_CLASS = "jfx-radio-button";
/// default color used when the radio button is selected
private StyleableObjectProperty<Color> selectedColor;
private static final Color DEFAULT_SELECTED_COLOR = Color.valueOf("#0F9D58");
public final StyleableObjectProperty<Color> selectedColorProperty() {
if (selectedColor == null) {
selectedColor = new SimpleStyleableObjectProperty<>(StyleableProperties.SELECTED_COLOR,
JFXRadioButton.this,
"selectedColor",
DEFAULT_SELECTED_COLOR);
}
return this.selectedColor;
}
public final Color getSelectedColor() {
return selectedColor == null ? DEFAULT_SELECTED_COLOR : this.selectedColorProperty().get();
}
public final void setSelectedColor(final Color selectedColor) {
this.selectedColorProperty().set(selectedColor);
}
/// default color used when the radio button is not selected
private StyleableObjectProperty<Color> unSelectedColor;
private static final Color DEFAULT_UNSELECTED_COLOR = Color.valueOf("#5A5A5A");
public final StyleableObjectProperty<Color> unSelectedColorProperty() {
if (unSelectedColor == null) {
unSelectedColor = new SimpleStyleableObjectProperty<>(
StyleableProperties.UNSELECTED_COLOR,
JFXRadioButton.this,
"unSelectedColor",
DEFAULT_UNSELECTED_COLOR);
}
return this.unSelectedColor;
}
public final Color getUnSelectedColor() {
return unSelectedColor == null ? DEFAULT_UNSELECTED_COLOR : this.unSelectedColorProperty().get();
}
public final void setUnSelectedColor(final Color unSelectedColor) {
this.unSelectedColorProperty().set(unSelectedColor);
}
private static final class StyleableProperties {
private static final CssMetaData<JFXRadioButton, Color> SELECTED_COLOR =
new CssMetaData<JFXRadioButton, Color>("-jfx-selected-color",
ColorConverter.getInstance(), DEFAULT_SELECTED_COLOR) {
@Override
public boolean isSettable(JFXRadioButton control) {
return control.selectedColor == null || !control.selectedColor.isBound();
}
@Override
public StyleableProperty<Color> getStyleableProperty(JFXRadioButton control) {
return control.selectedColorProperty();
}
};
private static final CssMetaData<JFXRadioButton, Color> UNSELECTED_COLOR =
new CssMetaData<JFXRadioButton, Color>("-jfx-unselected-color",
ColorConverter.getInstance(), DEFAULT_UNSELECTED_COLOR) {
@Override
public boolean isSettable(JFXRadioButton control) {
return control.unSelectedColor == null || !control.unSelectedColor.isBound();
}
@Override
public StyleableProperty<Color> getStyleableProperty(JFXRadioButton control) {
return control.unSelectedColorProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables =
new ArrayList<>(RadioButton.getClassCssMetaData());
Collections.addAll(styleables,
SELECTED_COLOR,
UNSELECTED_COLOR
);
CHILD_STYLEABLES = List.copyOf(styleables);
}
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
}
@@ -1,815 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.converters.RipplerMaskTypeConverter;
import com.jfoenix.utils.JFXNodeUtils;
import javafx.animation.*;
import javafx.beans.DefaultProperty;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.*;
import javafx.css.converter.BooleanConverter;
import javafx.css.converter.PaintConverter;
import javafx.css.converter.SizeConverter;
import javafx.geometry.Bounds;
import javafx.scene.CacheHint;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.util.Duration;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* JFXRippler is the material design implementation of a ripple effect.
* the ripple effect can be applied to any node in the scene. JFXRippler is
* a {@link StackPane} container that holds a specified node (control node) and a ripple generator.
* <p>
* UPDATE NOTES:
* - fireEventProgrammatically(Event) method has been removed as the ripple controller is
* the control itself, so you can trigger manual ripple by firing mouse event on the control
* instead of JFXRippler
*
* @author Shadi Shaheen
* @version 1.0
* @since 2016-03-09
*/
@DefaultProperty(value = "control")
public class JFXRippler extends StackPane {
public enum RipplerPos {
FRONT, BACK
}
public enum RipplerMask {
CIRCLE, RECT, FIT
}
protected RippleGenerator rippler;
protected Pane ripplerPane;
protected Node control;
protected static final double RIPPLE_MAX_RADIUS = 300;
private static final Interpolator RIPPLE_INTERPOLATOR = Interpolator.SPLINE(0.0825,
0.3025,
0.0875,
0.9975); //0.1, 0.54, 0.28, 0.95);
private boolean forceOverlay = false;
/// creates empty rippler node
public JFXRippler() {
this(null, RipplerMask.RECT, RipplerPos.FRONT);
}
/// creates a rippler for the specified control
public JFXRippler(Node control) {
this(control, RipplerMask.RECT, RipplerPos.FRONT);
}
/// creates a rippler for the specified control
///
/// @param pos can be either FRONT/BACK (position the ripple effect infront of or behind the control)
public JFXRippler(Node control, RipplerPos pos) {
this(control, RipplerMask.RECT, pos);
}
/// creates a rippler for the specified control and apply the specified mask to it
///
/// @param mask can be either rectangle/cricle
public JFXRippler(Node control, RipplerMask mask) {
this(control, mask, RipplerPos.FRONT);
}
/// creates a rippler for the specified control, mask and position.
///
/// @param mask can be either rectangle/cricle
/// @param pos can be either FRONT/BACK (position the ripple effect infront of or behind the control)
public JFXRippler(Node control, RipplerMask mask, RipplerPos pos) {
initialize();
setMaskType(mask);
setPosition(pos);
createRippleUI();
setControl(control);
// listen to control position changed
position.addListener(observable -> updateControlPosition());
setPickOnBounds(false);
setCache(true);
setCacheHint(CacheHint.SPEED);
setCacheShape(true);
}
protected final void createRippleUI() {
// create rippler panels
rippler = new RippleGenerator();
ripplerPane = new StackPane();
ripplerPane.setMouseTransparent(true);
ripplerPane.getChildren().add(rippler);
getChildren().add(ripplerPane);
}
/***************************************************************************
* *
* Setters / Getters *
* *
**************************************************************************/
public void setControl(Node control) {
if (control != null) {
this.control = control;
// position control
positionControl(control);
// add control listeners to generate / release ripples
initControlListeners();
}
}
// Override this method to create JFXRippler for a control outside the ripple
protected void positionControl(Node control) {
if (this.position.get() == RipplerPos.BACK) {
getChildren().add(control);
} else {
getChildren().add(0, control);
}
}
protected void updateControlPosition() {
if (this.position.get() == RipplerPos.BACK) {
ripplerPane.toBack();
} else {
ripplerPane.toFront();
}
}
public Node getControl() {
return control;
}
// methods that can be changed by extending the rippler class
/// generate the clipping mask
///
/// @return the mask node
protected Node getMask() {
double borderWidth = ripplerPane.getBorder() != null ? ripplerPane.getBorder().getInsets().getTop() : 0;
Bounds bounds = control.getBoundsInParent();
double width = control.getLayoutBounds().getWidth();
double height = control.getLayoutBounds().getHeight();
double diffMinX = Math.abs(control.getBoundsInLocal().getMinX() - control.getLayoutBounds().getMinX());
double diffMinY = Math.abs(control.getBoundsInLocal().getMinY() - control.getLayoutBounds().getMinY());
double diffMaxX = Math.abs(control.getBoundsInLocal().getMaxX() - control.getLayoutBounds().getMaxX());
double diffMaxY = Math.abs(control.getBoundsInLocal().getMaxY() - control.getLayoutBounds().getMaxY());
Node mask;
switch (getMaskType()) {
case RECT:
mask = new Rectangle(bounds.getMinX() + diffMinX - snappedLeftInset(),
bounds.getMinY() + diffMinY - snappedTopInset(),
width - 2 * borderWidth,
height - 2 * borderWidth); // -0.1 to prevent resizing the anchor pane
break;
case CIRCLE:
double radius = Math.min((width / 2) - 2 * borderWidth, (height / 2) - 2 * borderWidth);
mask = new Circle((bounds.getMinX() + diffMinX + bounds.getMaxX() - diffMaxX) / 2 - snappedLeftInset(),
(bounds.getMinY() + diffMinY + bounds.getMaxY() - diffMaxY) / 2 - snappedTopInset(),
radius,
Color.BLUE);
break;
case FIT:
mask = new Region();
if (control instanceof Shape) {
((Region) mask).setShape((Shape) control);
} else if (control instanceof Region) {
((Region) mask).setShape(((Region) control).getShape());
JFXNodeUtils.updateBackground(((Region) control).getBackground(), (Region) mask);
}
mask.resize(width, height);
mask.relocate(bounds.getMinX() + diffMinX, bounds.getMinY() + diffMinY);
break;
default:
mask = new Rectangle(bounds.getMinX() + diffMinX - snappedLeftInset(),
bounds.getMinY() + diffMinY - snappedTopInset(),
width - 2 * borderWidth,
height - 2 * borderWidth); // -0.1 to prevent resizing the anchor pane
break;
}
return mask;
}
/**
* compute the ripple radius
*
* @return the ripple radius size
*/
protected double computeRippleRadius(double centerX, double centerY) {
double dx = Math.max(centerX, control.getLayoutBounds().getWidth() - centerX);
double dy = Math.max(centerY, control.getLayoutBounds().getHeight() - centerY);
return (Math.sqrt(dx * dx + dy * dy) / 0.9) + 2;
}
protected void setOverLayBounds(Rectangle overlay) {
overlay.setWidth(control.getLayoutBounds().getWidth());
overlay.setHeight(control.getLayoutBounds().getHeight());
}
/**
* init mouse listeners on the control
*/
protected void initControlListeners() {
// if the control got resized the overlay rect must be rest
control.layoutBoundsProperty().addListener(observable -> resetRippler());
if (getChildren().contains(control)) {
control.boundsInParentProperty().addListener(observable -> resetRippler());
}
control.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
if (event.getButton() == MouseButton.PRIMARY)
createRipple(event.getX(), event.getY());
});
// create fade out transition for the ripple
control.addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
if (event.getButton() == MouseButton.PRIMARY)
releaseRipple();
});
}
/**
* creates Ripple effect
*/
protected void createRipple(double x, double y) {
if (!isRipplerDisabled()) {
rippler.setGeneratorCenterX(x);
rippler.setGeneratorCenterY(y);
rippler.createRipple();
}
}
protected void releaseRipple() {
rippler.releaseRipple();
}
/**
* creates Ripple effect in the center of the control
*
* @return a runnable to release the ripple when needed
*/
public Runnable createManualRipple() {
if (!isRipplerDisabled()) {
rippler.setGeneratorCenterX(control.getLayoutBounds().getWidth() / 2);
rippler.setGeneratorCenterY(control.getLayoutBounds().getHeight() / 2);
rippler.createRipple();
return () -> {
// create fade out transition for the ripple
releaseRipple();
};
}
return () -> {
};
}
/// show/hide the ripple overlay
///
/// @param forceOverlay used to hold the overlay after ripple action
public void setOverlayVisible(boolean visible, boolean forceOverlay) {
this.forceOverlay = forceOverlay;
setOverlayVisible(visible);
}
/// show/hide the ripple overlay
/// NOTE: setting overlay visibility to false will reset forceOverlay to false
public void setOverlayVisible(boolean visible) {
if (visible) {
showOverlay();
} else {
forceOverlay = false;
hideOverlay();
}
}
/**
* this method will be set to private in future versions of JFoenix,
* user the method {@link #setOverlayVisible(boolean)}
*/
public void showOverlay() {
if (rippler.overlayRect != null) {
rippler.overlayRect.outAnimation.stop();
}
rippler.createOverlay();
rippler.overlayRect.inAnimation.play();
}
public void hideOverlay() {
if (!forceOverlay) {
if (rippler.overlayRect != null) {
rippler.overlayRect.inAnimation.stop();
}
if (rippler.overlayRect != null) {
rippler.overlayRect.outAnimation.play();
}
} else {
System.err.println("Ripple Overlay is forced!");
}
}
/**
* Generates ripples on the screen every 0.3 seconds or whenever
* the createRipple method is called. Ripples grow and fade out
* over 0.6 seconds
*/
protected final class RippleGenerator extends Group {
private double generatorCenterX = 0;
private double generatorCenterY = 0;
private OverLayRipple overlayRect;
private final AtomicBoolean generating = new AtomicBoolean(false);
private boolean cacheRipplerClip = false;
private boolean resetClip = false;
private final Queue<Ripple> ripplesQueue = new LinkedList<>();
RippleGenerator() {
// improve in performance, by preventing
// redrawing the parent when the ripple effect is triggered
this.setManaged(false);
this.setCache(true);
this.setCacheHint(CacheHint.SPEED);
}
void createRipple() {
if (!generating.getAndSet(true)) {
// create overlay once then change its color later
createOverlay();
if (this.getClip() == null || (getChildren().size() == 1 && !cacheRipplerClip) || resetClip) {
this.setClip(getMask());
}
this.resetClip = false;
// create the ripple effect
final Ripple ripple = new Ripple(generatorCenterX, generatorCenterY);
getChildren().add(ripple);
ripplesQueue.add(ripple);
// animate the ripple
overlayRect.outAnimation.stop();
overlayRect.inAnimation.play();
ripple.inAnimation.play();
}
}
private void releaseRipple() {
Ripple ripple = ripplesQueue.poll();
if (ripple != null) {
ripple.inAnimation.stop();
ripple.outAnimation = new Timeline(
new KeyFrame(Duration.millis(Math.min(800, (0.9 * 500) / ripple.getScaleX()))
, ripple.outKeyValues));
ripple.outAnimation.setOnFinished((event) -> getChildren().remove(ripple));
ripple.outAnimation.play();
if (generating.getAndSet(false)) {
if (overlayRect != null) {
overlayRect.inAnimation.stop();
if (!forceOverlay) {
overlayRect.outAnimation.play();
}
}
}
}
}
void cacheRippleClip(boolean cached) {
cacheRipplerClip = cached;
}
void createOverlay() {
if (overlayRect == null) {
overlayRect = new OverLayRipple();
overlayRect.setClip(getMask());
getChildren().add(0, overlayRect);
overlayRect.fillProperty().bind(Bindings.createObjectBinding(() -> {
if (getRipplerFill() instanceof Color fill) {
return new Color(fill.getRed(),
fill.getGreen(),
fill.getBlue(),
0.2);
} else {
return Color.TRANSPARENT;
}
}, ripplerFillProperty()));
}
}
void setGeneratorCenterX(double generatorCenterX) {
this.generatorCenterX = generatorCenterX;
}
void setGeneratorCenterY(double generatorCenterY) {
this.generatorCenterY = generatorCenterY;
}
private final class OverLayRipple extends Rectangle {
// Overlay ripple animations
Animation inAnimation = new Timeline(new KeyFrame(Duration.millis(300),
new KeyValue(opacityProperty(), 1, Interpolator.EASE_IN)));
Animation outAnimation = new Timeline(new KeyFrame(Duration.millis(300),
new KeyValue(opacityProperty(), 0, Interpolator.EASE_OUT)));
OverLayRipple() {
super();
setOverLayBounds(this);
this.getStyleClass().add("jfx-rippler-overlay");
// update initial position
if (JFXRippler.this.getChildrenUnmodifiable().contains(control)) {
double diffMinX = Math.abs(control.getBoundsInLocal().getMinX() - control.getLayoutBounds().getMinX());
double diffMinY = Math.abs(control.getBoundsInLocal().getMinY() - control.getLayoutBounds().getMinY());
Bounds bounds = control.getBoundsInParent();
this.setX(bounds.getMinX() + diffMinX - snappedLeftInset());
this.setY(bounds.getMinY() + diffMinY - snappedTopInset());
}
// set initial attributes
setOpacity(0);
setCache(true);
setCacheHint(CacheHint.SPEED);
setCacheShape(true);
setManaged(false);
}
}
private final class Ripple extends Circle {
KeyValue[] outKeyValues;
Animation outAnimation = null;
Animation inAnimation = null;
private Ripple(double centerX, double centerY) {
super(centerX,
centerY,
getRipplerRadius() == Region.USE_COMPUTED_SIZE ?
computeRippleRadius(centerX, centerY) : getRipplerRadius(), null);
setCache(true);
setCacheHint(CacheHint.SPEED);
setCacheShape(true);
setManaged(false);
setSmooth(true);
KeyValue[] inKeyValues = new KeyValue[isRipplerRecenter() ? 4 : 2];
outKeyValues = new KeyValue[isRipplerRecenter() ? 5 : 3];
inKeyValues[0] = new KeyValue(scaleXProperty(), 0.9, RIPPLE_INTERPOLATOR);
inKeyValues[1] = new KeyValue(scaleYProperty(), 0.9, RIPPLE_INTERPOLATOR);
outKeyValues[0] = new KeyValue(this.scaleXProperty(), 1, RIPPLE_INTERPOLATOR);
outKeyValues[1] = new KeyValue(this.scaleYProperty(), 1, RIPPLE_INTERPOLATOR);
outKeyValues[2] = new KeyValue(this.opacityProperty(), 0, RIPPLE_INTERPOLATOR);
if (isRipplerRecenter()) {
double dx = (control.getLayoutBounds().getWidth() / 2 - centerX) / 1.55;
double dy = (control.getLayoutBounds().getHeight() / 2 - centerY) / 1.55;
inKeyValues[2] = outKeyValues[3] = new KeyValue(translateXProperty(),
Math.signum(dx) * Math.min(Math.abs(dx),
this.getRadius() / 2),
RIPPLE_INTERPOLATOR);
inKeyValues[3] = outKeyValues[4] = new KeyValue(translateYProperty(),
Math.signum(dy) * Math.min(Math.abs(dy),
this.getRadius() / 2),
RIPPLE_INTERPOLATOR);
}
inAnimation = new Timeline(new KeyFrame(Duration.ZERO,
new KeyValue(scaleXProperty(),
0,
RIPPLE_INTERPOLATOR),
new KeyValue(scaleYProperty(),
0,
RIPPLE_INTERPOLATOR),
new KeyValue(translateXProperty(),
0,
RIPPLE_INTERPOLATOR),
new KeyValue(translateYProperty(),
0,
RIPPLE_INTERPOLATOR),
new KeyValue(opacityProperty(),
1,
RIPPLE_INTERPOLATOR)
), new KeyFrame(Duration.millis(600), inKeyValues));
setScaleX(0);
setScaleY(0);
if (getRipplerFill() instanceof Color fill) {
Color circleColor = new Color(fill.getRed(),
fill.getGreen(),
fill.getBlue(),
0.3);
setStroke(circleColor);
setFill(circleColor);
} else {
setStroke(getRipplerFill());
setFill(getRipplerFill());
}
}
}
public void clear() {
getChildren().clear();
rippler.overlayRect = null;
generating.set(false);
}
}
private void resetOverLay() {
if (rippler.overlayRect != null) {
rippler.overlayRect.inAnimation.stop();
final RippleGenerator.OverLayRipple oldOverlay = rippler.overlayRect;
rippler.overlayRect.outAnimation.setOnFinished((finish) -> rippler.getChildren().remove(oldOverlay));
rippler.overlayRect.outAnimation.play();
rippler.overlayRect = null;
}
}
private void resetClip() {
this.rippler.resetClip = true;
}
protected void resetRippler() {
resetOverLay();
resetClip();
}
/***************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
/**
* Initialize the style class to 'jfx-rippler'.
* <p>
* This is the selector class from which CSS can be used to style
* this control.
*/
private static final String DEFAULT_STYLE_CLASS = "jfx-rippler";
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
/**
* the ripple recenter property, by default it's false.
* if true the ripple effect will show gravitational pull to the center of its control
*/
private StyleableBooleanProperty ripplerRecenter;
public boolean isRipplerRecenter() {
return ripplerRecenter != null && ripplerRecenter.get();
}
public StyleableBooleanProperty ripplerRecenterProperty() {
if (this.ripplerRecenter == null) {
this.ripplerRecenter = new SimpleStyleableBooleanProperty(
StyleableProperties.RIPPLER_RECENTER,
JFXRippler.this,
"ripplerRecenter",
false);
}
return this.ripplerRecenter;
}
public void setRipplerRecenter(boolean recenter) {
ripplerRecenterProperty().set(recenter);
}
/**
* the ripple radius size, by default it will be automatically computed.
*/
private StyleableDoubleProperty ripplerRadius;
public double getRipplerRadius() {
return ripplerRadius == null ? Region.USE_COMPUTED_SIZE : ripplerRadius.get();
}
public StyleableDoubleProperty ripplerRadiusProperty() {
if (this.ripplerRadius == null) {
this.ripplerRadius = new SimpleStyleableDoubleProperty(
StyleableProperties.RIPPLER_RADIUS,
JFXRippler.this,
"ripplerRadius",
Region.USE_COMPUTED_SIZE);
}
return this.ripplerRadius;
}
public void setRipplerRadius(double radius) {
ripplerRadiusProperty().set(radius);
}
private static final Color DEFAULT_RIPPLER_FILL = Color.rgb(0, 200, 255);
/**
* the default color of the ripple effect
*/
private StyleableObjectProperty<Paint> ripplerFill;
public Paint getRipplerFill() {
return ripplerFill == null ? DEFAULT_RIPPLER_FILL : ripplerFill.get();
}
public StyleableObjectProperty<Paint> ripplerFillProperty() {
if (this.ripplerFill == null) {
this.ripplerFill = new SimpleStyleableObjectProperty<>(StyleableProperties.RIPPLER_FILL,
JFXRippler.this,
"ripplerFill",
DEFAULT_RIPPLER_FILL);
}
return this.ripplerFill;
}
public void setRipplerFill(Paint color) {
ripplerFillProperty().set(color);
}
/// mask property used for clipping the rippler.
/// can be either CIRCLE/RECT
private StyleableObjectProperty<RipplerMask> maskType;
public RipplerMask getMaskType() {
return maskType == null ? RipplerMask.RECT : maskType.get();
}
public StyleableObjectProperty<RipplerMask> maskTypeProperty() {
if (this.maskType == null) {
this.maskType = new SimpleStyleableObjectProperty<>(
StyleableProperties.MASK_TYPE,
JFXRippler.this,
"maskType",
RipplerMask.RECT);
}
return this.maskType;
}
public void setMaskType(RipplerMask type) {
if (this.maskType != null || type != RipplerMask.RECT)
maskTypeProperty().set(type);
}
/**
* the ripple disable, by default it's false.
* if true the ripple effect will be hidden
*/
private StyleableBooleanProperty ripplerDisabled;
public boolean isRipplerDisabled() {
return ripplerDisabled != null && ripplerDisabled.get();
}
public StyleableBooleanProperty ripplerDisabledProperty() {
if (this.ripplerDisabled == null) {
this.ripplerDisabled = new SimpleStyleableBooleanProperty(
StyleableProperties.RIPPLER_DISABLED,
JFXRippler.this,
"ripplerDisabled",
false);
}
return this.ripplerDisabled;
}
public void setRipplerDisabled(boolean disabled) {
ripplerDisabledProperty().set(disabled);
}
/**
* indicates whether the ripple effect is infront of or behind the node
*/
protected ObjectProperty<RipplerPos> position = new SimpleObjectProperty<>();
public void setPosition(RipplerPos pos) {
this.position.set(pos);
}
public RipplerPos getPosition() {
return position == null ? RipplerPos.FRONT : position.get();
}
public ObjectProperty<RipplerPos> positionProperty() {
return this.position;
}
private static final class StyleableProperties {
private static final CssMetaData<JFXRippler, Boolean> RIPPLER_RECENTER =
new CssMetaData<>("-jfx-rippler-recenter",
BooleanConverter.getInstance(), false) {
@Override
public boolean isSettable(JFXRippler control) {
return control.ripplerRecenter == null || !control.ripplerRecenter.isBound();
}
@Override
public StyleableProperty<Boolean> getStyleableProperty(JFXRippler control) {
return control.ripplerRecenterProperty();
}
};
private static final CssMetaData<JFXRippler, Boolean> RIPPLER_DISABLED =
new CssMetaData<>("-jfx-rippler-disabled",
BooleanConverter.getInstance(), false) {
@Override
public boolean isSettable(JFXRippler control) {
return control.ripplerDisabled == null || !control.ripplerDisabled.isBound();
}
@Override
public StyleableProperty<Boolean> getStyleableProperty(JFXRippler control) {
return control.ripplerDisabledProperty();
}
};
private static final CssMetaData<JFXRippler, Paint> RIPPLER_FILL =
new CssMetaData<>("-jfx-rippler-fill",
PaintConverter.getInstance(), DEFAULT_RIPPLER_FILL) {
@Override
public boolean isSettable(JFXRippler control) {
return control.ripplerFill == null || !control.ripplerFill.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXRippler control) {
return control.ripplerFillProperty();
}
};
private static final CssMetaData<JFXRippler, Number> RIPPLER_RADIUS =
new CssMetaData<>("-jfx-rippler-radius",
SizeConverter.getInstance(), Region.USE_COMPUTED_SIZE) {
@Override
public boolean isSettable(JFXRippler control) {
return control.ripplerRadius == null || !control.ripplerRadius.isBound();
}
@Override
public StyleableProperty<Number> getStyleableProperty(JFXRippler control) {
return control.ripplerRadiusProperty();
}
};
private static final CssMetaData<JFXRippler, RipplerMask> MASK_TYPE =
new CssMetaData<>("-jfx-mask-type",
RipplerMaskTypeConverter.getInstance(), RipplerMask.RECT) {
@Override
public boolean isSettable(JFXRippler control) {
return control.maskType == null || !control.maskType.isBound();
}
@Override
public StyleableProperty<RipplerMask> getStyleableProperty(JFXRippler control) {
return control.maskTypeProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables =
new ArrayList<>(StackPane.getClassCssMetaData());
Collections.addAll(styleables,
RIPPLER_RECENTER,
RIPPLER_RADIUS,
RIPPLER_FILL,
MASK_TYPE,
RIPPLER_DISABLED
);
STYLEABLES = Collections.unmodifiableList(styleables);
}
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}
}
@@ -1,180 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.converters.IndicatorPositionConverter;
import com.jfoenix.skins.JFXSliderSkin;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.*;
import javafx.scene.control.Skin;
import javafx.scene.control.Slider;
import javafx.util.Callback;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/// JFXSlider is the material design implementation of a slider.
///
/// @author Bashir Elias & Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXSlider extends Slider {
public JFXSlider() {
super(0, 100, 50);
initialize();
}
public JFXSlider(double min, double max, double value) {
super(min, max, value);
initialize();
}
@Override
protected Skin<?> createDefaultSkin() {
return new JFXSliderSkin(this);
}
private void initialize() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
}
public enum IndicatorPosition {
LEFT, RIGHT
}
/***************************************************************************
* *
* Properties *
* *
**************************************************************************/
/// String binding factory for the slider value.
/// Sets a custom string for the value text (by default, it shows the value rounded to the nearest whole number).
///
///
/// For example, to have the value displayed as a percentage (assuming the slider has a range of (0, 100)):
/// ```java
/// JFXSlider mySlider = ...
/// mySlider.setValueFactory(slider ->
/// Bindings.createStringBinding(
/// () -> ((int) slider.getValue()) + "%",
/// slider.valueProperty()
/// )
/// );
/// ```
///
/// NOTE: might be replaced later with a call back to create the animated thumb node
///
/// @param callback a callback to create the string value binding
private ObjectProperty<Callback<JFXSlider, StringBinding>> valueFactory;
public final ObjectProperty<Callback<JFXSlider, StringBinding>> valueFactoryProperty() {
if (valueFactory == null) {
valueFactory = new SimpleObjectProperty<>(this, "valueFactory");
}
return valueFactory;
}
/// @return the current slider value factory
public final Callback<JFXSlider, StringBinding> getValueFactory() {
return valueFactory == null ? null : valueFactory.get();
}
/// sets custom string binding for the slider text value
///
/// @param valueFactory a callback to create the string value binding
public final void setValueFactory(final Callback<JFXSlider, StringBinding> valueFactory) {
this.valueFactoryProperty().set(valueFactory);
}
/***************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
/// Initialize the style class to 'jfx-slider'.
///
/// This is the selector class from which CSS can be used to style
/// this control.
private static final String DEFAULT_STYLE_CLASS = "jfx-slider";
/// indicates the position of the slider indicator, can be
/// either LEFT or RIGHT
private StyleableObjectProperty<IndicatorPosition> indicatorPosition;
public StyleableObjectProperty<IndicatorPosition> indicatorPositionProperty() {
if (indicatorPosition == null) {
indicatorPosition = new SimpleStyleableObjectProperty<>(
StyleableProperties.INDICATOR_POSITION,
JFXSlider.this,
"indicatorPosition",
IndicatorPosition.LEFT);
}
return this.indicatorPosition;
}
public IndicatorPosition getIndicatorPosition() {
return indicatorPosition == null ? IndicatorPosition.LEFT : indicatorPosition.get();
}
public void setIndicatorPosition(IndicatorPosition pos) {
indicatorPositionProperty().set(pos);
}
private static final class StyleableProperties {
private static final CssMetaData<JFXSlider, IndicatorPosition> INDICATOR_POSITION = new CssMetaData<>(
"-jfx-indicator-position",
IndicatorPositionConverter.getInstance(),
IndicatorPosition.LEFT) {
@Override
public boolean isSettable(JFXSlider control) {
return control.indicatorPosition == null || !control.indicatorPosition.isBound();
}
@Override
public StyleableProperty<IndicatorPosition> getStyleableProperty(JFXSlider control) {
return control.indicatorPositionProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(
Slider.getClassCssMetaData());
Collections.addAll(styleables, INDICATOR_POSITION);
CHILD_STYLEABLES = List.copyOf(styleables);
}
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
}
@@ -1,421 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.PauseTransition;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.WeakChangeListener;
import javafx.css.PseudoClass;
import javafx.event.Event;
import javafx.event.EventType;
import javafx.geometry.Bounds;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.animation.Motion;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
/// "Snackbars provide brief messages about app processes at the bottom of the screen"
/// (<a href="https://material.io/design/components/snackbars.html#">Material Design Guidelines</a>).
///
/// To show a snackbar you need to
/// <ol>
/// - Have a [Pane] (snackbarContainer) to show the snackbar on top of. Register it in
/// [the JFXSnackbar constructor][#JFXSnackbar(Pane)] or using the [#registerSnackbarContainer(Pane)] method.
/// - Have or create a [JFXSnackbar]. - Having one snackbar where you pass all your
/// [SnackbarEvents][JFXSnackbar.SnackbarEvent] will ensure that the [enqueuemethod][JFXSnackbar#enqueue(SnackbarEvent)] works as intended.
///
/// - Have something to show in the snackbar. A [JFXSnackbarLayout] is nice and pretty,
/// but any arbitrary [Node] will do.
/// - Create a [SnackbarEvent][JFXSnackbar.SnackbarEvent] specifying the contents and the
/// duration.
/// </ol>
///
/// Finally, with all those things prepared, show your snackbar using
/// [snackbar.enqueue(snackbarEvent);][JFXSnackbar#enqueue(SnackbarEvent)].
///
/// It's most convenient to create functions to do most of this (creating the layout and event) with the default
/// settings; that way all you need to do to show a snackbar is specify the message or just the message and the duration.
///
/// @see <a href="https://material.io/design/components/snackbars.html#"> The Material Design Snackbar</a>
public class JFXSnackbar extends Group {
private static final String DEFAULT_STYLE_CLASS = "jfx-snackbar";
private Pane snackbarContainer;
private final ChangeListener<? super Number> sizeListener = (o, oldVal, newVal) -> refreshPopup();
private final WeakChangeListener<? super Number> weakSizeListener = new WeakChangeListener<>(sizeListener);
private final AtomicBoolean processingQueue = new AtomicBoolean(false);
private final ConcurrentLinkedQueue<SnackbarEvent> eventQueue = new ConcurrentLinkedQueue<>();
private final ConcurrentHashMap.KeySetView<Object, Boolean> eventsSet = ConcurrentHashMap.newKeySet();
private final Pane content;
private PseudoClass activePseudoClass = null;
private PauseTransition pauseTransition;
/// This constructor assumes that you will eventually call the [#registerSnackbarContainer(Pane)] method before
/// calling the [#enqueue(SnackbarEvent)] method. Otherwise, how will the snackbar know where to show itself?
///
///
/// "Snackbars provide brief messages about app processes at the bottom of the screen"
/// (<a href="https://material.io/design/components/snackbars.html#">Material Design Guidelines</a>).
///
/// To show a snackbar you need to
///
/// - Have a [Pane] (snackbarContainer) to show the snackbar on top of. Register it in
/// [the JFXSnackbar constructor][#JFXSnackbar(Pane)] or using the [#registerSnackbarContainer(Pane)] method.
/// - Have or create a [JFXSnackbar]. - Having one snackbar where you pass all your
/// [SnackbarEvents][JFXSnackbar.SnackbarEvent] will ensure that the [enqueuemethod][JFXSnackbar#enqueue(SnackbarEvent)] works as intended.
/// - Have something to show in the snackbar. A [JFXSnackbarLayout] is nice and pretty,
/// but any arbitrary [Node] will do.
/// - Create a [SnackbarEvent][JFXSnackbar.SnackbarEvent] specifying the contents and the
/// duration.
///
/// Finally, with all those things prepared, show your snackbar using
/// [snackbar.enqueue(snackbarEvent);][JFXSnackbar#enqueue(SnackbarEvent)].
///
public JFXSnackbar() {
this(null);
}
/// "Snackbars provide brief messages about app processes at the bottom of the screen"
/// (<a href="https://material.io/design/components/snackbars.html#">Material Design Guidelines</a>).
///
/// To show a snackbar you need to
///
/// - Have a [Pane] (snackbarContainer) to show the snackbar on top of. Register it in
/// [the JFXSnackbar constructor][#JFXSnackbar(Pane)] or using the [#registerSnackbarContainer(Pane)] method.
/// - Have or create a [JFXSnackbar]. - Having one snackbar where you pass all your
/// [SnackbarEvents][JFXSnackbar.SnackbarEvent] will ensure that the [enqueuemethod][JFXSnackbar#enqueue(SnackbarEvent)] works as intended.
/// - Have something to show in the snackbar. A [JFXSnackbarLayout] is nice and pretty,
/// but any arbitrary [Node] will do.
/// - Create a [SnackbarEvent][JFXSnackbar.SnackbarEvent] specifying the contents and the
/// duration.
///
/// Finally, with all those things prepared, show your snackbar using
/// [snackbar.enqueue(snackbarEvent);][JFXSnackbar#enqueue(SnackbarEvent)].
///
/// @param snackbarContainer where the snackbar will appear. Using a single snackbar instead of many, will ensure that
/// the [#enqueue(SnackbarEvent)] method works correctly.
public JFXSnackbar(Pane snackbarContainer) {
initialize();
content = new StackPane();
content.getStyleClass().add("jfx-snackbar-content");
//wrap the content in a group so that the content is managed inside its own container
//but the group is not managed in the snackbarContainer so it does not affect any layout calculations
getChildren().add(content);
setManaged(false);
setVisible(false);
// register the container before resizing it
registerSnackbarContainer(snackbarContainer);
// resize the popup if its layout has been changed
layoutBoundsProperty().addListener((o, oldVal, newVal) -> refreshPopup());
addEventHandler(SnackbarEvent.SNACKBAR, this::enqueue);
}
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
// Setters / Getters
public Pane getPopupContainer() {
return snackbarContainer;
}
public void setPrefWidth(double width) {
content.setPrefWidth(width);
}
public double getPrefWidth() {
return content.getPrefWidth();
}
// Public API
public void registerSnackbarContainer(Pane snackbarContainer) {
if (snackbarContainer != null) {
if (this.snackbarContainer != null) {
//since listeners are added the container should be properly registered/unregistered
throw new IllegalArgumentException("Snackbar Container already set");
}
this.snackbarContainer = snackbarContainer;
this.snackbarContainer.getChildren().add(this);
this.snackbarContainer.heightProperty().addListener(weakSizeListener);
this.snackbarContainer.widthProperty().addListener(weakSizeListener);
}
}
public void unregisterSnackbarContainer(Pane snackbarContainer) {
if (snackbarContainer != null) {
if (this.snackbarContainer == null) {
throw new IllegalArgumentException("Snackbar Container not set");
}
this.snackbarContainer.getChildren().remove(this);
this.snackbarContainer.heightProperty().removeListener(weakSizeListener);
this.snackbarContainer.widthProperty().removeListener(weakSizeListener);
this.snackbarContainer = null;
}
}
private void show(SnackbarEvent event) {
content.getChildren().setAll(event.getContent());
openAnimation = getTimeline(event.getTimeout());
if (event.getPseudoClass() != null) {
activePseudoClass = event.getPseudoClass();
content.pseudoClassStateChanged(activePseudoClass, true);
}
openAnimation.play();
}
private Timeline openAnimation = null;
private Timeline getTimeline(Duration timeout) {
Timeline animation;
animation = new Timeline(
new KeyFrame(
Duration.ZERO,
e -> {
this.toBack();
this.setVisible(false);
},
new KeyValue(this.translateYProperty(), this.getLayoutBounds().getHeight(), Motion.EASE),
new KeyValue(this.opacityProperty(), 0, Motion.EASE)
),
new KeyFrame(
Duration.millis(10),
e -> {
this.toFront();
this.setVisible(true);
}
),
new KeyFrame(Duration.millis(300),
new KeyValue(this.opacityProperty(), 1, Motion.EASE),
new KeyValue(this.translateYProperty(), 0, Motion.EASE)
)
);
animation.setCycleCount(1);
pauseTransition = Duration.INDEFINITE.equals(timeout) ? null : new PauseTransition(timeout);
if (pauseTransition != null) {
animation.setOnFinished(finish -> {
pauseTransition.setOnFinished(done -> {
pauseTransition = null;
eventsSet.remove(currentEvent);
currentEvent = eventQueue.peek();
close();
});
pauseTransition.play();
});
}
return animation;
}
public void close() {
if (openAnimation != null) {
openAnimation.stop();
}
if (this.isVisible()) {
Timeline closeAnimation = new Timeline(
new KeyFrame(
Duration.ZERO,
e -> this.toFront(),
new KeyValue(this.opacityProperty(), 1, Motion.EASE),
new KeyValue(this.translateYProperty(), 0, Motion.EASE)
),
new KeyFrame(
Duration.millis(290),
e -> this.setVisible(true)
),
new KeyFrame(Duration.millis(300),
e -> {
this.toBack();
this.setVisible(false);
},
new KeyValue(this.translateYProperty(), this.getLayoutBounds().getHeight(), Motion.EASE),
new KeyValue(this.opacityProperty(), 0, Motion.EASE)
)
);
closeAnimation.setCycleCount(1);
closeAnimation.setOnFinished(e -> {
resetPseudoClass();
processSnackbar();
});
closeAnimation.play();
}
}
private SnackbarEvent currentEvent = null;
public SnackbarEvent getCurrentEvent() {
return currentEvent;
}
/**
* Shows {@link SnackbarEvent SnackbarEvents} one by one. The next event will be shown after the current event's duration.
*
* @param event the {@link SnackbarEvent event} to put in the queue.
*/
public void enqueue(SnackbarEvent event) {
synchronized (this) {
if (!eventsSet.contains(event)) {
eventsSet.add(event);
eventQueue.offer(event);
} else if (currentEvent == event && pauseTransition != null) {
pauseTransition.playFromStart();
}
}
if (processingQueue.compareAndSet(false, true)) {
Platform.runLater(() -> {
currentEvent = eventQueue.poll();
if (currentEvent != null) {
show(currentEvent);
}
});
}
}
private void resetPseudoClass() {
if (activePseudoClass != null) {
content.pseudoClassStateChanged(activePseudoClass, false);
activePseudoClass = null;
}
}
private void processSnackbar() {
currentEvent = eventQueue.poll();
if (currentEvent != null) {
eventsSet.remove(currentEvent);
show(currentEvent);
} else {
//The enqueue method and this listener should be executed sequentially on the FX Thread so there
//should not be a race condition
processingQueue.getAndSet(false);
}
}
private void refreshPopup() {
if (snackbarContainer == null) {
return;
}
Bounds contentBound = this.getLayoutBounds();
double offsetX = Math.ceil(snackbarContainer.getWidth() / 2) - Math.ceil(contentBound.getWidth() / 2);
double offsetY = snackbarContainer.getHeight() - contentBound.getHeight();
this.setLayoutX(offsetX);
this.setLayoutY(offsetY);
}
///////////////////////////////////////////////////////////////////////////
// Event API
///////////////////////////////////////////////////////////////////////////
/// Specifies _what_ and _how long_ to show a [JFXSnackbar].
///
/// The _what_ can be any arbitrary [Node]; the [JFXSnackbarLayout] is a great choice.
///
/// The _how long_ is specified in the form of a [javafx.util.Duration][javafx.util.Duration], not to be
/// confused with the [java.time.Duration].
public static class SnackbarEvent extends Event {
public static final EventType<SnackbarEvent> SNACKBAR = new EventType<>(Event.ANY, "SNACKBAR");
/// The amount of time the snackbar will show for, if not otherwise specified.
///
/// It's 1.5 seconds.
public static Duration DEFAULT_DURATION = Duration.seconds(1.5);
private final Node content;
private final PseudoClass pseudoClass;
private final Duration timeout;
/// Creates a [SnackbarEvent] with the [default duration][#DEFAULT_DURATION] and no pseudoClass.
///
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
public SnackbarEvent(Node content) {
this(content, DEFAULT_DURATION, null);
}
/// Creates a [SnackbarEvent] with the [default duration][#DEFAULT_DURATION]; you specify the contents and
/// pseudoClass.
///
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
public SnackbarEvent(Node content, PseudoClass pseudoClass) {
this(content, DEFAULT_DURATION, pseudoClass);
}
/// Creates a SnackbarEvent with no pseudoClass; you specify the contents and duration.
/// pseudoClass.
///
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
/// @param timeout the amount of time you want the snackbar to show for.
public SnackbarEvent(Node content, Duration timeout) {
this(content, timeout, null);
}
/// Creates a SnackbarEvent; you specify the contents, duration and pseudoClass.
///
/// If you don't need so much customization, try one of the other constructors.
///
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
/// @param timeout the amount of time you want the snackbar to show for.
public SnackbarEvent(Node content, Duration timeout, PseudoClass pseudoClass) {
super(SNACKBAR);
this.content = content;
this.pseudoClass = pseudoClass;
this.timeout = timeout;
}
public Node getContent() {
return content;
}
public PseudoClass getPseudoClass() {
return pseudoClass;
}
public Duration getTimeout() {
return timeout;
}
@Override
@SuppressWarnings("unchecked")
public EventType<? extends SnackbarEvent> getEventType() {
return (EventType<? extends SnackbarEvent>) super.getEventType();
}
public boolean isPersistent() {
return Duration.INDEFINITE.equals(getTimeout());
}
}
}
@@ -1,110 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import javafx.beans.binding.Bindings;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
/// JFXSnackbarLayout default layout for snackbar content
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2018-11-16
public class JFXSnackbarLayout extends BorderPane {
private final Label toast;
private JFXButton action;
private final StackPane actionContainer;
public JFXSnackbarLayout(String message) {
this(message, null, null);
}
public JFXSnackbarLayout(String message, String actionText, EventHandler<ActionEvent> actionHandler) {
initialize();
toast = new Label();
toast.setMinWidth(Control.USE_PREF_SIZE);
toast.getStyleClass().add("jfx-snackbar-toast");
toast.setWrapText(true);
toast.setText(message);
StackPane toastContainer = new StackPane(toast);
toastContainer.setPadding(new Insets(20));
actionContainer = new StackPane();
actionContainer.setPadding(new Insets(0, 10, 0, 0));
toast.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> {
if (getPrefWidth() == -1) {
return getPrefWidth();
}
double actionWidth = actionContainer.isVisible() ? actionContainer.getWidth() : 0.0;
return prefWidthProperty().get() - actionWidth;
}, prefWidthProperty(), actionContainer.widthProperty(), actionContainer.visibleProperty()));
setLeft(toastContainer);
setRight(actionContainer);
if (actionText != null) {
action = new JFXButton();
action.setText(actionText);
action.setOnAction(actionHandler);
action.setMinWidth(Control.USE_PREF_SIZE);
action.setButtonType(JFXButton.ButtonType.FLAT);
action.getStyleClass().add("jfx-snackbar-action");
// actions will be added upon showing the snackbar if needed
actionContainer.getChildren().add(action);
if (!actionText.isEmpty()) {
action.setVisible(true);
actionContainer.setVisible(true);
actionContainer.setManaged(true);
// to force updating the layout bounds
action.setText("");
action.setText(actionText);
action.setOnAction(actionHandler);
} else {
actionContainer.setVisible(false);
actionContainer.setManaged(false);
action.setVisible(false);
}
}
}
private static final String DEFAULT_STYLE_CLASS = "jfx-snackbar-layout";
public String getToast() {
return toast.getText();
}
public void setToast(String toast) {
this.toast.setText(toast);
}
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
}
@@ -1,138 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.skins.JFXSpinnerSkin;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableDoubleProperty;
import javafx.css.Styleable;
import javafx.css.StyleableDoubleProperty;
import javafx.css.converter.SizeConverter;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.Skin;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
/// JFXSpinner is the material design implementation of a loading spinner.
///
/// @author Bashir Elias & Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXSpinner extends ProgressIndicator {
public static final double INDETERMINATE_PROGRESS = ProgressIndicator.INDETERMINATE_PROGRESS;
public JFXSpinner() {
this(INDETERMINATE_PROGRESS);
}
public JFXSpinner(double progress) {
super(progress);
init();
}
private void init() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
}
@Override
protected Skin<?> createDefaultSkin() {
return new JFXSpinnerSkin(this);
}
/***************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
/// Initialize the style class to 'jfx-spinner'.
///
/// This is the selector class from which CSS can be used to style
/// this control.
private static final String DEFAULT_STYLE_CLASS = "jfx-spinner";
private static final double DEFAULT_RADIUS = 16.0;
/**
* specifies the radius of the spinner node, by default it's set to `16.0`
*/
private StyleableDoubleProperty radius;
public final StyleableDoubleProperty radiusProperty() {
if (this.radius == null) {
this.radius = new SimpleStyleableDoubleProperty(StyleableProperties.RADIUS,
JFXSpinner.this,
"radius",
DEFAULT_RADIUS);
}
return this.radius;
}
public final double getRadius() {
return radius != null ? radius.get() : DEFAULT_RADIUS;
}
public final void setRadius(final double radius) {
this.radiusProperty().set(radius);
}
public double getStartingAngle() {
return 360 - ThreadLocalRandom.current().nextDouble() * 720;
}
private static final class StyleableProperties {
private static final CssMetaData<JFXSpinner, Number> RADIUS =
new CssMetaData<>("-jfx-radius",
SizeConverter.getInstance(), DEFAULT_RADIUS) {
@Override
public boolean isSettable(JFXSpinner control) {
return control.radius == null || !control.radius.isBound();
}
@Override
public StyleableDoubleProperty getStyleableProperty(JFXSpinner control) {
return control.radiusProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables =
new ArrayList<>(ProgressIndicator.getClassCssMetaData());
Collections.addAll(styleables, RADIUS);
CHILD_STYLEABLES = List.copyOf(styleables);
}
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
}
@@ -1,297 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.skins.JFXTextAreaSkin;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.*;
import javafx.css.converter.BooleanConverter;
import javafx.css.converter.PaintConverter;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.control.TextArea;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu;
/**
* JFXTextArea is the material design implementation of a text area.
*
* @author Shadi Shaheen
* @version 1.0
* @since 2016-03-09
*/
public class JFXTextArea extends TextArea {
/**
* Initialize the style class to 'jfx-text-field'.
* <p>
* This is the selector class from which CSS can be used to style
* this control.
*/
private static final String DEFAULT_STYLE_CLASS = "jfx-text-area";
/**
* {@inheritDoc}
*/
public JFXTextArea() {
initialize();
}
/**
* {@inheritDoc}
*/
public JFXTextArea(String text) {
super(text);
initialize();
}
/**
* {@inheritDoc}
*/
@Override
protected Skin<?> createDefaultSkin() {
return new JFXTextAreaSkin(this);
}
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
if ("dalvik".equalsIgnoreCase(System.getProperty("java.vm.name"))) {
this.setStyle("-fx-skin: \"com.jfoenix.android.skins.JFXTextAreaSkinAndroid\";");
}
useJFXContextMenu(this);
}
/***************************************************************************
* *
* Properties *
* *
**************************************************************************/
/**
* holds the current active validator on the text area in case of validation error
*/
private ReadOnlyObjectWrapper<ValidatorBase> activeValidator = new ReadOnlyObjectWrapper<>();
public ValidatorBase getActiveValidator() {
return activeValidator == null ? null : activeValidator.get();
}
public ReadOnlyObjectProperty<ValidatorBase> activeValidatorProperty() {
return this.activeValidator.getReadOnlyProperty();
}
/**
* list of validators that will validate the text value upon calling
* {{@link #validate()}
*/
private ObservableList<ValidatorBase> validators = FXCollections.observableArrayList();
public ObservableList<ValidatorBase> getValidators() {
return validators;
}
public void setValidators(ValidatorBase... validators) {
this.validators.addAll(validators);
}
/**
* validates the text value using the list of validators provided by the user
* {{@link #setValidators(ValidatorBase...)}
*
* @return true if the value is valid else false
*/
public boolean validate() {
for (ValidatorBase validator : validators) {
if (validator.getSrcControl() == null) {
validator.setSrcControl(this);
}
validator.validate();
if (validator.getHasErrors()) {
activeValidator.set(validator);
return false;
}
}
activeValidator.set(null);
return true;
}
public void resetValidation() {
pseudoClassStateChanged(ValidatorBase.PSEUDO_CLASS_ERROR, false);
activeValidator.set(null);
}
/***************************************************************************
* *
* styleable Properties *
* *
**************************************************************************/
/**
* set true to show a float the prompt text when focusing the field
*/
private StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT, JFXTextArea.this, "lableFloat", false);
public final StyleableBooleanProperty labelFloatProperty() {
return this.labelFloat;
}
public final boolean isLabelFloat() {
return this.labelFloatProperty().get();
}
public final void setLabelFloat(final boolean labelFloat) {
this.labelFloatProperty().set(labelFloat);
}
/**
* default color used when the text area is unfocused
*/
private StyleableObjectProperty<Paint> unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR, JFXTextArea.this, "unFocusColor", Color.rgb(77, 77, 77));
public Paint getUnFocusColor() {
return unFocusColor == null ? Color.rgb(77, 77, 77) : unFocusColor.get();
}
public StyleableObjectProperty<Paint> unFocusColorProperty() {
return this.unFocusColor;
}
public void setUnFocusColor(Paint color) {
this.unFocusColor.set(color);
}
/**
* default color used when the text area is focused
*/
private StyleableObjectProperty<Paint> focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR, JFXTextArea.this, "focusColor", Color.valueOf("#4059A9"));
public Paint getFocusColor() {
return focusColor == null ? Color.valueOf("#4059A9") : focusColor.get();
}
public StyleableObjectProperty<Paint> focusColorProperty() {
return this.focusColor;
}
public void setFocusColor(Paint color) {
this.focusColor.set(color);
}
/**
* disable animation on validation
*/
private StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION, JFXTextArea.this, "disableAnimation", false);
public final StyleableBooleanProperty disableAnimationProperty() {
return this.disableAnimation;
}
public final Boolean isDisableAnimation() {
return disableAnimation != null && this.disableAnimationProperty().get();
}
public final void setDisableAnimation(final Boolean disabled) {
this.disableAnimationProperty().set(disabled);
}
private final static class StyleableProperties {
private static final CssMetaData<JFXTextArea, Paint> UNFOCUS_COLOR = new CssMetaData<JFXTextArea, Paint>("-jfx-unfocus-color", PaintConverter.getInstance(), Color.rgb(77, 77, 77)) {
@Override
public boolean isSettable(JFXTextArea control) {
return control.unFocusColor == null || !control.unFocusColor.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXTextArea control) {
return control.unFocusColorProperty();
}
};
private static final CssMetaData<JFXTextArea, Paint> FOCUS_COLOR = new CssMetaData<JFXTextArea, Paint>("-jfx-focus-color", PaintConverter.getInstance(), Color.valueOf("#4059A9")) {
@Override
public boolean isSettable(JFXTextArea control) {
return control.focusColor == null || !control.focusColor.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXTextArea control) {
return control.focusColorProperty();
}
};
private static final CssMetaData<JFXTextArea, Boolean> LABEL_FLOAT = new CssMetaData<JFXTextArea, Boolean>("-jfx-label-float", BooleanConverter.getInstance(), false) {
@Override
public boolean isSettable(JFXTextArea control) {
return control.labelFloat == null || !control.labelFloat.isBound();
}
@Override
public StyleableBooleanProperty getStyleableProperty(JFXTextArea control) {
return control.labelFloatProperty();
}
};
private static final CssMetaData<JFXTextArea, Boolean> DISABLE_ANIMATION = new CssMetaData<JFXTextArea, Boolean>("-jfx-disable-animation", BooleanConverter.getInstance(), false) {
@Override
public boolean isSettable(JFXTextArea control) {
return control.disableAnimation == null || !control.disableAnimation.isBound();
}
@Override
public StyleableBooleanProperty getStyleableProperty(JFXTextArea control) {
return control.disableAnimationProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
Collections.addAll(styleables, UNFOCUS_COLOR, FOCUS_COLOR, LABEL_FLOAT, DISABLE_ANIMATION);
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
}
}
// inherit the styleable properties from parent
private List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
if (STYLEABLES == null) {
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
styleables.addAll(getClassCssMetaData());
styleables.addAll(TextArea.getClassCssMetaData());
STYLEABLES = Collections.unmodifiableList(styleables);
}
return STYLEABLES;
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
}
@@ -1,307 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.skins.JFXTextFieldSkin;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.*;
import javafx.css.converter.BooleanConverter;
import javafx.css.converter.PaintConverter;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.control.TextField;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.jackhuang.hmcl.ui.FXUtils.useJFXContextMenu;
/**
* JFXTextField is the material design implementation of a text Field.
*
* @author Shadi Shaheen
* @version 1.0
* @since 2016-03-09
*/
public class JFXTextField extends TextField {
/**
* Initialize the style class to 'jfx-text-field'.
* <p>
* This is the selector class from which CSS can be used to style
* this control.
*/
private static final String DEFAULT_STYLE_CLASS = "jfx-text-field";
/**
* {@inheritDoc}
*/
public JFXTextField() {
initialize();
}
/**
* {@inheritDoc}
*/
public JFXTextField(String text) {
super(text);
initialize();
}
/**
* {@inheritDoc}
*/
@Override
protected Skin<?> createDefaultSkin() {
return new JFXTextFieldSkin(this);
}
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
if ("dalvik".equalsIgnoreCase(System.getProperty("java.vm.name"))) {
this.setStyle("-fx-skin: \"com.jfoenix.android.skins.JFXTextFieldSkinAndroid\";");
}
useJFXContextMenu(this);
}
// https://github.com/HMCL-dev/HMCL/issues/5822
// TODO: This method may no longer be needed after we update JFXTextFieldSkin
@Override
protected void layoutChildren() {
super.layoutChildren();
this.setNeedsLayout(false);
}
/***************************************************************************
* *
* Properties *
* *
**************************************************************************/
/**
* holds the current active validator on the text field in case of validation error
*/
private ReadOnlyObjectWrapper<ValidatorBase> activeValidator = new ReadOnlyObjectWrapper<>();
public ValidatorBase getActiveValidator() {
return activeValidator == null ? null : activeValidator.get();
}
public ReadOnlyObjectProperty<ValidatorBase> activeValidatorProperty() {
return this.activeValidator.getReadOnlyProperty();
}
/**
* list of validators that will validate the text value upon calling
* {{@link #validate()}
*/
private ObservableList<ValidatorBase> validators = FXCollections.observableArrayList();
public ObservableList<ValidatorBase> getValidators() {
return validators;
}
public void setValidators(ValidatorBase... validators) {
this.validators.addAll(validators);
}
/**
* validates the text value using the list of validators provided by the user
* {{@link #setValidators(ValidatorBase...)}
*
* @return true if the value is valid else false
*/
public boolean validate() {
for (ValidatorBase validator : validators) {
if (validator.getSrcControl() == null) {
validator.setSrcControl(this);
}
validator.validate();
if (validator.getHasErrors()) {
activeValidator.set(validator);
return false;
}
}
activeValidator.set(null);
return true;
}
public void resetValidation() {
pseudoClassStateChanged(ValidatorBase.PSEUDO_CLASS_ERROR, false);
activeValidator.set(null);
}
/***************************************************************************
* *
* styleable Properties *
* *
**************************************************************************/
/**
* set true to show a float the prompt text when focusing the field
*/
private StyleableBooleanProperty labelFloat = new SimpleStyleableBooleanProperty(StyleableProperties.LABEL_FLOAT, JFXTextField.this, "lableFloat", false);
public final StyleableBooleanProperty labelFloatProperty() {
return this.labelFloat;
}
public final boolean isLabelFloat() {
return this.labelFloatProperty().get();
}
public final void setLabelFloat(final boolean labelFloat) {
this.labelFloatProperty().set(labelFloat);
}
/**
* default color used when the field is unfocused
*/
private StyleableObjectProperty<Paint> unFocusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNFOCUS_COLOR, JFXTextField.this, "unFocusColor", Color.rgb(77, 77, 77));
public Paint getUnFocusColor() {
return unFocusColor == null ? Color.rgb(77, 77, 77) : unFocusColor.get();
}
public StyleableObjectProperty<Paint> unFocusColorProperty() {
return this.unFocusColor;
}
public void setUnFocusColor(Paint color) {
this.unFocusColor.set(color);
}
/**
* default color used when the field is focused
*/
private StyleableObjectProperty<Paint> focusColor = new SimpleStyleableObjectProperty<>(StyleableProperties.FOCUS_COLOR, JFXTextField.this, "focusColor", Color.valueOf("#4059A9"));
public Paint getFocusColor() {
return focusColor == null ? Color.valueOf("#4059A9") : focusColor.get();
}
public StyleableObjectProperty<Paint> focusColorProperty() {
return this.focusColor;
}
public void setFocusColor(Paint color) {
this.focusColor.set(color);
}
/**
* disable animation on validation
*/
private StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION, JFXTextField.this, "disableAnimation", false);
public final StyleableBooleanProperty disableAnimationProperty() {
return this.disableAnimation;
}
public final Boolean isDisableAnimation() {
return disableAnimation != null && this.disableAnimationProperty().get();
}
public final void setDisableAnimation(final Boolean disabled) {
this.disableAnimationProperty().set(disabled);
}
private final static class StyleableProperties {
private static final CssMetaData<JFXTextField, Paint> UNFOCUS_COLOR = new CssMetaData<JFXTextField, Paint>("-jfx-unfocus-color", PaintConverter.getInstance(), Color.valueOf("#A6A6A6")) {
@Override
public boolean isSettable(JFXTextField control) {
return control.unFocusColor == null || !control.unFocusColor.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXTextField control) {
return control.unFocusColorProperty();
}
};
private static final CssMetaData<JFXTextField, Paint> FOCUS_COLOR = new CssMetaData<JFXTextField, Paint>("-jfx-focus-color", PaintConverter.getInstance(), Color.valueOf("#3f51b5")) {
@Override
public boolean isSettable(JFXTextField control) {
return control.focusColor == null || !control.focusColor.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXTextField control) {
return control.focusColorProperty();
}
};
private static final CssMetaData<JFXTextField, Boolean> LABEL_FLOAT = new CssMetaData<JFXTextField, Boolean>("-jfx-label-float", BooleanConverter.getInstance(), false) {
@Override
public boolean isSettable(JFXTextField control) {
return control.labelFloat == null || !control.labelFloat.isBound();
}
@Override
public StyleableBooleanProperty getStyleableProperty(JFXTextField control) {
return control.labelFloatProperty();
}
};
private static final CssMetaData<JFXTextField, Boolean> DISABLE_ANIMATION = new CssMetaData<JFXTextField, Boolean>("-jfx-disable-animation", BooleanConverter.getInstance(), false) {
@Override
public boolean isSettable(JFXTextField control) {
return control.disableAnimation == null || !control.disableAnimation.isBound();
}
@Override
public StyleableBooleanProperty getStyleableProperty(JFXTextField control) {
return control.disableAnimationProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
Collections.addAll(styleables, UNFOCUS_COLOR, FOCUS_COLOR, LABEL_FLOAT, DISABLE_ANIMATION);
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
}
}
// inherit the styleable properties from parent
private List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
if (STYLEABLES == null) {
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
styleables.addAll(getClassCssMetaData());
styleables.addAll(TextField.getClassCssMetaData());
STYLEABLES = Collections.unmodifiableList(styleables);
}
return STYLEABLES;
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
}
@@ -1,373 +0,0 @@
/*
* 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;
}
}
@@ -1,146 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.utils.JFXNodeUtils;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import java.lang.ref.WeakReference;
/// JFXTreeCell is simple material design implementation of a tree cell.
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2017-02-15
public class JFXTreeCell<T> extends TreeCell<T> {
protected JFXRippler cellRippler = new JFXRippler(this) {
@Override
protected Node getMask() {
Region clip = new Region();
JFXNodeUtils.updateBackground(JFXTreeCell.this.getBackground(), clip);
double width = control.getLayoutBounds().getWidth();
double height = control.getLayoutBounds().getHeight();
clip.resize(width, height);
return clip;
}
@Override
protected void positionControl(Node control) {
// do nothing
}
};
private HBox hbox;
private final StackPane selectedPane = new StackPane();
private final InvalidationListener treeItemGraphicInvalidationListener = observable -> updateDisplay(getItem(),
isEmpty());
private final WeakInvalidationListener weakTreeItemGraphicListener = new WeakInvalidationListener(
treeItemGraphicInvalidationListener);
private WeakReference<TreeItem<T>> treeItemRef;
public JFXTreeCell() {
selectedPane.getStyleClass().add("selection-bar");
selectedPane.setBackground(new Background(new BackgroundFill(Color.RED, CornerRadii.EMPTY, Insets.EMPTY)));
selectedPane.setPrefWidth(3);
selectedPane.setMouseTransparent(true);
selectedProperty().addListener((o, oldVal, newVal) -> selectedPane.setVisible(newVal ? true : false));
final InvalidationListener treeItemInvalidationListener = observable -> {
TreeItem<T> oldTreeItem = treeItemRef == null ? null : treeItemRef.get();
if (oldTreeItem != null) {
oldTreeItem.graphicProperty().removeListener(weakTreeItemGraphicListener);
}
TreeItem<T> newTreeItem = getTreeItem();
if (newTreeItem != null) {
newTreeItem.graphicProperty().addListener(weakTreeItemGraphicListener);
treeItemRef = new WeakReference<>(newTreeItem);
}
};
final WeakInvalidationListener weakTreeItemListener = new WeakInvalidationListener(treeItemInvalidationListener);
treeItemProperty().addListener(weakTreeItemListener);
if (getTreeItem() != null) {
getTreeItem().graphicProperty().addListener(weakTreeItemGraphicListener);
}
}
@Override
protected void layoutChildren() {
super.layoutChildren();
if (!getChildren().contains(selectedPane)) {
getChildren().add(0, cellRippler);
cellRippler.rippler.clear();
getChildren().add(0, selectedPane);
}
cellRippler.resizeRelocate(0, 0, getWidth(), getHeight());
cellRippler.releaseRipple();
selectedPane.resizeRelocate(0, 0, selectedPane.prefWidth(-1), getHeight());
selectedPane.setVisible(isSelected());
}
private void updateDisplay(T item, boolean empty) {
if (item == null || empty) {
hbox = null;
setText(null);
setGraphic(null);
} else {
TreeItem<T> treeItem = getTreeItem();
if (treeItem != null && treeItem.getGraphic() != null) {
if (item instanceof Node) {
setText(null);
if (hbox == null) {
hbox = new HBox(3);
}
hbox.getChildren().setAll(treeItem.getGraphic(), (Node) item);
setGraphic(hbox);
} else {
hbox = null;
setText(item.toString());
setGraphic(treeItem.getGraphic());
}
} else {
hbox = null;
if (item instanceof Node) {
setText(null);
setGraphic((Node) item);
} else {
setText(item.toString());
setGraphic(null);
}
}
}
}
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
updateDisplay(item, empty);
setMouseTransparent(item == null || empty);
}
}
@@ -1,48 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
/// JFXTreeView is the material design implementation of a TreeView
/// with expand/collapse animation and selection indicator.
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2017-02-15
public class JFXTreeView<T> extends TreeView<T> {
private static final String DEFAULT_STYLE_CLASS = "jfx-tree-view";
public JFXTreeView() {
init();
}
public JFXTreeView(TreeItem<T> root) {
super(root);
init();
}
private void init() {
this.setCellFactory((view) -> new JFXTreeCell<>());
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
}
@@ -1,46 +0,0 @@
/*
* 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);
}
}
}
@@ -1,80 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls.datamodels.treetable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.TreeTableColumn;
/// data model that is used in JFXTreeTableView, it's used to implement
/// the grouping feature.
///
/// **Note:** the data object used in JFXTreeTableView **must** extends this class
///
/// @param <T> is the concrete object of the Tree table
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class RecursiveTreeObject<T> {
/// grouped children objects
private ObservableList<T> children = FXCollections.observableArrayList();
public ObservableList<T> getChildren() {
return children;
}
public void setChildren(ObservableList<T> children) {
this.children = children;
}
/// Whether or not the object is grouped by a specified tree table column
ObjectProperty<TreeTableColumn<T, ?>> groupedColumn = new SimpleObjectProperty<>();
public final ObjectProperty<TreeTableColumn<T, ?>> groupedColumnProperty() {
return this.groupedColumn;
}
public final TreeTableColumn<T, ?> getGroupedColumn() {
return this.groupedColumnProperty().get();
}
public final void setGroupedColumn(final TreeTableColumn<T, ?> groupedColumn) {
this.groupedColumnProperty().set(groupedColumn);
}
/// the value that must be shown when grouped
ObjectProperty<Object> groupedValue = new SimpleObjectProperty<>();
public final ObjectProperty<Object> groupedValueProperty() {
return this.groupedValue;
}
public final Object getGroupedValue() {
return this.groupedValueProperty().get();
}
public final void setGroupedValue(final Object groupedValue) {
this.groupedValueProperty().set(groupedValue);
}
}
@@ -1,55 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls.events;
import javafx.event.Event;
import javafx.event.EventType;
import java.io.Serial;
/// JFXDialog events, used exclusively by the following methods:
///
/// - [com.jfoenix.controls.JFXDialog#show()]
/// - [com.jfoenix.controls.JFXDialog#close()]
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXDialogEvent extends Event {
@Serial
private static final long serialVersionUID = 1L;
/// This event occurs when a JFXDialog is closed, no longer visible to the user
/// ( after the exit animation ends )
public static final EventType<JFXDialogEvent> CLOSED = new EventType<>(Event.ANY, "JFX_DIALOG_CLOSED");
/// This event occurs when a JFXDialog is opened, visible to the user
/// ( after the entrance animation ends )
public static final EventType<JFXDialogEvent> OPENED = new EventType<>(Event.ANY, "JFX_DIALOG_OPENED");
/// Construct a new JFXDialog `Event` with the specified event type
///
/// @param eventType the event type
public JFXDialogEvent(EventType<? extends Event> eventType) {
super(eventType);
}
}
@@ -1,122 +0,0 @@
/*
* Copyright (c) 2016 JFoenix
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.jfoenix.effects;
import javafx.scene.Node;
import javafx.scene.effect.BlurType;
import javafx.scene.effect.DropShadow;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
/**
* it will create a shadow effect for a given node and a specified depth level.
* depth levels are {0,1,2,3,4,5}
*
* @author Shadi Shaheen
* @version 1.0
* @since 2016-03-09
*/
public final class JFXDepthManager {
private JFXDepthManager() {
throw new AssertionError();
}
private static final DropShadow[] depth = new DropShadow[] {
new DropShadow(BlurType.GAUSSIAN, Color.TRANSPARENT, 0, 0, 0, 0),
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.12), 4, 0, 0, 0),
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.16), 8, 0, 0, 4),
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.20), 12, 0, 0, 4),
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.24), 16, 0, 0, 4),
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.28), 20, 0, 0, 8)
};
/**
* this method is used to add shadow effect to the node,
* however the shadow is not real ( gets affected with node transformations)
* <p>
* use {@link #createMaterialNode(Node, int)} instead to generate a real shadow
*/
public static void setDepth(Node control, int level) {
level = level < 0 ? 0 : level;
level = level > 5 ? 5 : level;
control.setEffect(new DropShadow(BlurType.GAUSSIAN,
depth[level].getColor(),
depth[level].getRadius(),
depth[level].getSpread(),
depth[level].getOffsetX(),
depth[level].getOffsetY()));
}
public static int getLevels() {
return depth.length;
}
public static DropShadow getShadowAt(int level) {
return depth[level];
}
/**
* this method will generate a new container node that prevent
* control transformation to be applied to the shadow effect
* (which makes it looks as a real shadow)
*/
public static Node createMaterialNode(Node control, int level) {
Node container = new Pane(control) {
@Override
protected double computeMaxWidth(double height) {
return computePrefWidth(height);
}
@Override
protected double computeMaxHeight(double width) {
return computePrefHeight(width);
}
@Override
protected double computePrefWidth(double height) {
return control.prefWidth(height);
}
@Override
protected double computePrefHeight(double width) {
return control.prefHeight(width);
}
};
container.getStyleClass().add("depth-container");
container.setPickOnBounds(false);
level = level < 0 ? 0 : level;
level = level > 5 ? 5 : level;
container.setEffect(new DropShadow(BlurType.GAUSSIAN,
depth[level].getColor(),
depth[level].getRadius(),
depth[level].getSpread(),
depth[level].getOffsetX(),
depth[level].getOffsetY()));
return container;
}
public static void pop(Node control) {
control.setEffect(new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.26), 5, 0.05, 0, 1));
}
}
@@ -1,222 +0,0 @@
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.jfoenix.skins;
import com.jfoenix.adapters.skins.ButtonSkin;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRippler;
import com.jfoenix.effects.JFXDepthManager;
import com.jfoenix.transitions.CachedTransition;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.animation.Transition;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.effect.DropShadow;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Shape;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils;
public class JFXButtonSkin extends ButtonSkin {
private final StackPane buttonContainer = new StackPane();
private final JFXRippler buttonRippler = new JFXRippler(new StackPane()) {
protected Node getMask() {
StackPane mask = new StackPane();
mask.shapeProperty().bind(JFXButtonSkin.this.buttonContainer.shapeProperty());
mask.backgroundProperty().bind(Bindings.createObjectBinding(() -> new Background(
new BackgroundFill(Color.WHITE,
JFXButtonSkin.this.buttonContainer.backgroundProperty().get() != null && !JFXButtonSkin.this.buttonContainer.getBackground().getFills().isEmpty()
? JFXButtonSkin.this.buttonContainer.getBackground().getFills().get(0).getRadii()
: JFXButtonSkin.this.defaultRadii, JFXButtonSkin.this.buttonContainer.backgroundProperty().get() != null && !JFXButtonSkin.this.buttonContainer.getBackground().getFills().isEmpty() ? JFXButtonSkin.this.buttonContainer.getBackground().getFills().get(0).getInsets() : Insets.EMPTY)),
JFXButtonSkin.this.buttonContainer.backgroundProperty()));
mask.resize(JFXButtonSkin.this.buttonContainer.getWidth() - JFXButtonSkin.this.buttonContainer.snappedRightInset() - JFXButtonSkin.this.buttonContainer.snappedLeftInset(), JFXButtonSkin.this.buttonContainer.getHeight() - JFXButtonSkin.this.buttonContainer.snappedBottomInset() - JFXButtonSkin.this.buttonContainer.snappedTopInset());
return mask;
}
private void initListeners() {
this.ripplerPane.setOnMousePressed((event) -> {
if (JFXButtonSkin.this.releaseManualRippler != null) {
JFXButtonSkin.this.releaseManualRippler.run();
}
JFXButtonSkin.this.releaseManualRippler = null;
this.createRipple(event.getX(), event.getY());
});
}
};
private Transition clickedAnimation;
private final CornerRadii defaultRadii = new CornerRadii(3.0);
private boolean invalid = true;
private Runnable releaseManualRippler = null;
public JFXButtonSkin(JFXButton button) {
super(button);
this.getSkinnable().armedProperty().addListener((o, oldVal, newVal) -> {
if (newVal) {
this.releaseManualRippler = this.buttonRippler.createManualRipple();
if (this.clickedAnimation != null) {
this.clickedAnimation.setRate(1.0);
this.clickedAnimation.play();
}
} else {
if (this.releaseManualRippler != null) {
this.releaseManualRippler.run();
}
if (this.clickedAnimation != null) {
this.clickedAnimation.setRate(-1.0);
this.clickedAnimation.play();
}
}
});
this.buttonContainer.getChildren().add(this.buttonRippler);
button.buttonTypeProperty().addListener((o, oldVal, newVal) -> this.updateButtonType(newVal));
button.setOnMousePressed((e) -> {
if (this.clickedAnimation != null) {
this.clickedAnimation.setRate(1.0F);
this.clickedAnimation.play();
}
});
button.setOnMouseReleased((e) -> {
if (this.clickedAnimation != null) {
this.clickedAnimation.setRate(-1.0F);
this.clickedAnimation.play();
}
});
ReadOnlyBooleanProperty focusVisibleProperty = FXUtils.focusVisibleProperty(button);
if (focusVisibleProperty == null) {
focusVisibleProperty = button.focusedProperty();
}
focusVisibleProperty.addListener((o, oldVal, newVal) -> {
if (newVal) {
if (!this.getSkinnable().isPressed()) {
this.buttonRippler.showOverlay();
}
} else {
this.buttonRippler.hideOverlay();
}
});
button.pressedProperty().addListener((o, oldVal, newVal) -> this.buttonRippler.hideOverlay());
button.setPickOnBounds(false);
this.buttonContainer.setPickOnBounds(false);
this.buttonContainer.shapeProperty().bind(this.getSkinnable().shapeProperty());
this.buttonContainer.borderProperty().bind(this.getSkinnable().borderProperty());
this.buttonContainer.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
if (button.getBackground() == null || this.isJavaDefaultBackground(button.getBackground()) || this.isJavaDefaultClickedBackground(button.getBackground())) {
button.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, this.defaultRadii, null)));
}
try {
return new Background(new BackgroundFill(this.getSkinnable().getBackground() != null ? this.getSkinnable().getBackground().getFills().get(0).getFill() : Color.TRANSPARENT, this.getSkinnable().getBackground() != null ? this.getSkinnable().getBackground().getFills().get(0).getRadii() : this.defaultRadii, Insets.EMPTY));
} catch (Exception var3) {
return this.getSkinnable().getBackground();
}
}, this.getSkinnable().backgroundProperty()));
button.ripplerFillProperty().addListener((o, oldVal, newVal) -> this.buttonRippler.setRipplerFill(newVal));
if (button.getBackground() == null || this.isJavaDefaultBackground(button.getBackground())) {
button.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, this.defaultRadii, null)));
}
this.updateButtonType(button.getButtonType());
this.updateChildren();
}
protected void updateChildren() {
super.updateChildren();
if (this.buttonContainer != null) {
this.getChildren().add(0, this.buttonContainer);
}
for (int i = 1; i < this.getChildren().size(); ++i) {
this.getChildren().get(i).setMouseTransparent(true);
}
}
protected void layoutChildren(double x, double y, double w, double h) {
if (this.invalid) {
if (((JFXButton) this.getSkinnable()).getRipplerFill() == null) {
for (int i = this.getChildren().size() - 1; i >= 1; --i) {
if (this.getChildren().get(i) instanceof Shape shape) {
this.buttonRippler.setRipplerFill(shape.getFill());
shape.fillProperty().addListener((o, oldVal, newVal) -> this.buttonRippler.setRipplerFill(newVal));
break;
}
if (this.getChildren().get(i) instanceof Label label) {
this.buttonRippler.setRipplerFill(label.getTextFill());
label.textFillProperty().addListener((o, oldVal, newVal) -> this.buttonRippler.setRipplerFill(newVal));
break;
}
}
} else {
this.buttonRippler.setRipplerFill(((JFXButton) this.getSkinnable()).getRipplerFill());
}
this.invalid = false;
}
double shift = 1.0F;
this.buttonContainer.resizeRelocate(this.getSkinnable().getLayoutBounds().getMinX() - shift, this.getSkinnable().getLayoutBounds().getMinY() - shift, this.getSkinnable().getWidth() + (double) 2.0F * shift, this.getSkinnable().getHeight() + (double) 2.0F * shift);
this.layoutLabelInArea(x, y, w, h);
}
private boolean isJavaDefaultBackground(Background background) {
try {
String firstFill = background.getFills().get(0).getFill().toString();
return "0xffffffba".equals(firstFill) || "0xffffffbf".equals(firstFill) || "0xffffff12".equals(firstFill) || "0xffffffbd".equals(firstFill);
} catch (Exception var3) {
return false;
}
}
private boolean isJavaDefaultClickedBackground(Background background) {
try {
return "0x039ed3ff".equals(background.getFills().get(0).getFill().toString());
} catch (Exception var3) {
return false;
}
}
private void updateButtonType(JFXButton.ButtonType type) {
switch (type) {
case RAISED -> {
JFXDepthManager.setDepth(this.buttonContainer, 2);
this.clickedAnimation = new ButtonClickTransition();
}
case FLAT -> this.buttonContainer.setEffect(null);
}
}
private class ButtonClickTransition extends CachedTransition {
public ButtonClickTransition() {
super(JFXButtonSkin.this.buttonContainer, new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).radiusProperty(), JFXDepthManager.getShadowAt(2).radiusProperty().get(), Interpolator.EASE_BOTH),
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).spreadProperty(), JFXDepthManager.getShadowAt(2).spreadProperty().get(), Interpolator.EASE_BOTH),
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).offsetXProperty(), JFXDepthManager.getShadowAt(2).offsetXProperty().get(), Interpolator.EASE_BOTH),
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).offsetYProperty(), JFXDepthManager.getShadowAt(2).offsetYProperty().get(), Interpolator.EASE_BOTH)),
new KeyFrame(Duration.millis(1000.0F),
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).radiusProperty(), JFXDepthManager.getShadowAt(5).radiusProperty().get(), Interpolator.EASE_BOTH),
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).spreadProperty(), JFXDepthManager.getShadowAt(5).spreadProperty().get(), Interpolator.EASE_BOTH),
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).offsetXProperty(), JFXDepthManager.getShadowAt(5).offsetXProperty().get(), Interpolator.EASE_BOTH),
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).offsetYProperty(), JFXDepthManager.getShadowAt(5).offsetYProperty().get(), Interpolator.EASE_BOTH))));
this.setCycleDuration(Duration.seconds(0.2));
this.setDelay(Duration.seconds(0.0F));
}
}
}
@@ -1,228 +0,0 @@
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.jfoenix.skins;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXRippler;
import com.jfoenix.controls.JFXRippler.RipplerMask;
import com.jfoenix.controls.JFXRippler.RipplerPos;
import com.jfoenix.transitions.CachedTransition;
import com.jfoenix.transitions.JFXFillTransition;
import com.jfoenix.utils.JFXNodeUtils;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.animation.Transition;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.VPos;
import javafx.scene.control.CheckBox;
import javafx.scene.control.skin.CheckBoxSkin;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.SVGPath;
import javafx.util.Duration;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.FXUtils;
public class JFXCheckBoxSkin extends CheckBoxSkin {
private final StackPane box = new StackPane();
private final StackPane mark = new StackPane();
private final double lineThick = 2.0;
private final double padding = 10.0;
private final JFXRippler rippler;
private final AnchorPane container = new AnchorPane();
private final double labelOffset = -8.0;
private final Transition transition;
private boolean invalid = true;
private JFXFillTransition select;
public JFXCheckBoxSkin(JFXCheckBox control) {
super(control);
this.box.setMinSize(18.0, 18.0);
this.box.setPrefSize(18.0, 18.0);
this.box.setMaxSize(18.0, 18.0);
this.box.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, new CornerRadii(2.0), Insets.EMPTY)));
this.box.setBorder(new Border(new BorderStroke(Themes.getColorScheme().getOnSurfaceVariant(), BorderStrokeStyle.SOLID, new CornerRadii(2.0), new BorderWidths(this.lineThick))));
StackPane boxContainer = new StackPane();
boxContainer.getChildren().add(this.box);
boxContainer.setPadding(new Insets(this.padding));
this.rippler = new JFXRippler(boxContainer, RipplerMask.CIRCLE, RipplerPos.BACK);
this.updateRippleColor();
SVGPath shape = new SVGPath();
shape.setContent("M384 690l452-452 60 60-512 512-238-238 60-60z");
this.mark.setShape(shape);
this.mark.setMaxSize(15.0, 12.0);
this.mark.setStyle("-fx-background-color:-monet-on-primary; -fx-border-color:-monet-on-primary; -fx-border-width:2px;");
this.mark.setVisible(false);
this.mark.setScaleX(0.0);
this.mark.setScaleY(0.0);
boxContainer.getChildren().add(this.mark);
this.container.getChildren().add(this.rippler);
AnchorPane.setRightAnchor(this.rippler, this.labelOffset);
control.selectedProperty().addListener((o, oldVal, newVal) -> {
this.updateRippleColor();
this.playSelectAnimation(newVal);
});
ReadOnlyBooleanProperty focusVisibleProperty = FXUtils.focusVisibleProperty(control);
if (focusVisibleProperty == null)
focusVisibleProperty = control.focusedProperty();
focusVisibleProperty.addListener((o, oldVal, newVal) -> {
if (newVal) {
if (!this.getSkinnable().isPressed()) {
this.rippler.showOverlay();
}
} else {
this.rippler.hideOverlay();
}
});
control.pressedProperty().addListener((o, oldVal, newVal) -> this.rippler.hideOverlay());
this.updateChildren();
this.registerChangeListener(control.checkedColorProperty(), ignored -> {
if (select != null) {
select.stop();
}
this.createFillTransition();
updateColors();
});
this.registerChangeListener(control.unCheckedColorProperty(), ignored -> updateColors());
this.transition = new CheckBoxTransition();
this.createFillTransition();
}
private void updateRippleColor() {
var control = (JFXCheckBox) this.getSkinnable();
this.rippler.setRipplerFill(control.isSelected()
? control.getCheckedColor()
: control.getUnCheckedColor());
}
private void updateColors() {
var control = (JFXCheckBox) getSkinnable();
boolean isSelected = control.isSelected();
JFXNodeUtils.updateBackground(box.getBackground(), box, isSelected ? control.getCheckedColor() : Color.TRANSPARENT);
rippler.setRipplerFill(isSelected ? control.getCheckedColor() : control.getUnCheckedColor());
final BorderStroke borderStroke = box.getBorder().getStrokes().get(0);
box.setBorder(new Border(new BorderStroke(
isSelected ? control.getCheckedColor() : Themes.getColorScheme().getOnSurfaceVariant(),
borderStroke.getTopStyle(),
borderStroke.getRadii(),
borderStroke.getWidths())));
}
protected void updateChildren() {
super.updateChildren();
if (this.container != null) {
this.getChildren().remove(1);
this.getChildren().add(this.container);
}
}
protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset) + this.snapSizeX(this.box.minWidth(-1.0)) + this.labelOffset + 2.0 * this.padding;
}
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset) + this.snapSizeY(this.box.prefWidth(-1.0)) + this.labelOffset + 2.0 * this.padding;
}
protected void layoutChildren(double x, double y, double w, double h) {
CheckBox checkBox = this.getSkinnable();
double boxWidth = this.snapSizeX(this.container.prefWidth(-1.0));
double boxHeight = this.snapSizeY(this.container.prefHeight(-1.0));
double computeWidth = Math.min(checkBox.prefWidth(-1.0), checkBox.minWidth(-1.0)) + this.labelOffset + 2.0 * this.padding;
double labelWidth = Math.min(computeWidth - boxWidth, w - this.snapSizeX(boxWidth)) + this.labelOffset + 2.0 * this.padding;
double labelHeight = Math.min(checkBox.prefHeight(labelWidth), h);
double maxHeight = Math.max(boxHeight, labelHeight);
double xOffset = computeXOffset(w, labelWidth + boxWidth, checkBox.getAlignment().getHpos()) + x;
double yOffset = computeYOffset(h, maxHeight, checkBox.getAlignment().getVpos()) + x;
if (this.invalid) {
if (this.getSkinnable().isSelected()) {
this.playSelectAnimation(true);
}
this.invalid = false;
}
this.layoutLabelInArea(xOffset + boxWidth, yOffset, labelWidth, maxHeight, checkBox.getAlignment());
this.container.resize(boxWidth, boxHeight);
this.positionInArea(this.container, xOffset, yOffset, boxWidth, maxHeight, 0.0, checkBox.getAlignment().getHpos(), checkBox.getAlignment().getVpos());
}
static double computeXOffset(double width, double contentWidth, HPos hpos) {
return switch (hpos) {
case LEFT -> 0.0;
case CENTER -> (width - contentWidth) / 2.0;
case RIGHT -> width - contentWidth;
};
}
static double computeYOffset(double height, double contentHeight, VPos vpos) {
return switch (vpos) {
case TOP -> 0.0;
case CENTER -> (height - contentHeight) / 2.0;
case BOTTOM -> height - contentHeight;
default -> 0.0;
};
}
private void playSelectAnimation(Boolean selection) {
if (selection == null) {
selection = false;
}
JFXCheckBox control = (JFXCheckBox) this.getSkinnable();
this.transition.setRate(selection ? 1.0 : -1.0);
this.select.setRate(selection ? 1.0 : -1.0);
this.transition.play();
this.select.play();
this.box.setBorder(new Border(new BorderStroke(
selection ? control.getCheckedColor() : Themes.getColorScheme().getOnSurfaceVariant(),
BorderStrokeStyle.SOLID,
new CornerRadii(2.0),
new BorderWidths(this.lineThick))));
}
private void createFillTransition() {
this.select = new JFXFillTransition(Duration.millis(120.0), this.box, Color.TRANSPARENT,
(Color) ((JFXCheckBox) this.getSkinnable()).getCheckedColor());
this.select.setInterpolator(Interpolator.EASE_OUT);
}
private final class CheckBoxTransition extends CachedTransition {
CheckBoxTransition() {
super(JFXCheckBoxSkin.this.mark,
new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(JFXCheckBoxSkin.this.mark.visibleProperty(), false, Interpolator.EASE_OUT),
new KeyValue(JFXCheckBoxSkin.this.mark.scaleXProperty(), (double) 0.5F, Interpolator.EASE_OUT),
new KeyValue(JFXCheckBoxSkin.this.mark.scaleYProperty(), (double) 0.5F, Interpolator.EASE_OUT)),
new KeyFrame(Duration.millis(400.0),
new KeyValue(JFXCheckBoxSkin.this.mark.visibleProperty(), true, Interpolator.EASE_OUT),
new KeyValue(JFXCheckBoxSkin.this.mark.scaleXProperty(), (double) 0.5F, Interpolator.EASE_OUT),
new KeyValue(JFXCheckBoxSkin.this.mark.scaleYProperty(), (double) 0.5F, Interpolator.EASE_OUT)),
new KeyFrame(Duration.millis(1000.0),
new KeyValue(JFXCheckBoxSkin.this.mark.visibleProperty(), true, Interpolator.EASE_OUT),
new KeyValue(JFXCheckBoxSkin.this.mark.scaleXProperty(), 1, Interpolator.EASE_OUT),
new KeyValue(JFXCheckBoxSkin.this.mark.scaleYProperty(), 1, Interpolator.EASE_OUT))
)
);
this.setCycleDuration(Duration.seconds(0.12));
this.setDelay(Duration.seconds(0.05));
}
}
}
@@ -1,623 +0,0 @@
/*
* 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;
}
}
@@ -1,258 +0,0 @@
/*
* 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;
}
}
@@ -1,629 +0,0 @@
/*
* 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);
}
}
@@ -1,277 +0,0 @@
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.jfoenix.skins;
import com.jfoenix.adapters.skins.ComboBoxListViewSkin;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.transitions.CachedTransition;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ComboBoxBase;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.text.Text;
import javafx.scene.transform.Scale;
import javafx.util.Duration;
public class JFXComboBoxListViewSkin<T> extends ComboBoxListViewSkin<T> {
protected final ObjectProperty<Paint> promptTextFill;
private boolean invalid = true;
private final StackPane customPane;
private final StackPane line = new StackPane();
private final StackPane focusedLine = new StackPane();
private final Text promptText = new Text();
private final double initScale = 0.05;
private final Scale scale;
private final Timeline linesAnimation;
private ParallelTransition transition;
private CachedTransition promptTextUpTransition;
private CachedTransition promptTextDownTransition;
private CachedTransition promptTextColorTransition;
private final Scale promptTextScale;
private Paint oldPromptTextFill;
private final BooleanBinding usePromptText;
public JFXComboBoxListViewSkin(JFXComboBox<T> comboBox) {
super(comboBox);
this.scale = new Scale(this.initScale, 1.0F);
this.linesAnimation = new Timeline(new KeyFrame(Duration.ZERO, new KeyValue(this.scale.xProperty(), this.initScale, Interpolator.EASE_BOTH), new KeyValue(this.focusedLine.opacityProperty(), 0, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(1.0F), new KeyValue(this.focusedLine.opacityProperty(), 1, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(160.0F), new KeyValue(this.scale.xProperty(), 1, Interpolator.EASE_BOTH)));
this.promptTextScale = new Scale(1.0F, 1.0F, 0.0F, 0.0F);
this.promptTextFill = new SimpleObjectProperty(Color.valueOf("#B2B2B2"));
this.usePromptText = Bindings.createBooleanBinding(this::usePromptText, ((JFXComboBox) this.getSkinnable()).valueProperty(), this.getSkinnable().promptTextProperty());
this.getArrowButton().setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null)));
this.promptText.textProperty().bind(comboBox.promptTextProperty());
this.promptText.fillProperty().bind(this.promptTextFill);
this.promptText.getStyleClass().addAll("text", "prompt-text");
this.promptText.getTransforms().add(this.promptTextScale);
if (!comboBox.isLabelFloat()) {
this.promptText.visibleProperty().bind(this.usePromptText);
}
this.customPane = new StackPane();
this.customPane.setMouseTransparent(true);
this.customPane.getStyleClass().add("combo-box-button-container");
this.customPane.backgroundProperty().bindBidirectional(this.getSkinnable().backgroundProperty());
this.customPane.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null)));
this.customPane.getChildren().add(this.promptText);
this.getChildren().add(0, this.customPane);
StackPane.setAlignment(this.promptText, Pos.CENTER_LEFT);
this.line.getStyleClass().add("input-line");
this.focusedLine.getStyleClass().add("input-focused-line");
this.getChildren().add(this.line);
this.getChildren().add(this.focusedLine);
this.line.setPrefHeight(1.0F);
this.line.setTranslateY(1.0F);
this.line.setManaged(false);
this.line.setBackground(new Background(new BackgroundFill(((JFXComboBox) this.getSkinnable()).getUnFocusColor(), CornerRadii.EMPTY, Insets.EMPTY)));
if (this.getSkinnable().isDisabled()) {
this.line.setBorder(new Border(new BorderStroke(((JFXComboBox) this.getSkinnable()).getUnFocusColor(), BorderStrokeStyle.DASHED, CornerRadii.EMPTY, new BorderWidths(1.0F))));
this.line.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)));
}
this.focusedLine.setPrefHeight(2.0F);
this.focusedLine.setTranslateY(0.0F);
this.focusedLine.setBackground(new Background(new BackgroundFill(((JFXComboBox) this.getSkinnable()).getFocusColor(), CornerRadii.EMPTY, Insets.EMPTY)));
this.focusedLine.setOpacity(0.0F);
this.focusedLine.getTransforms().add(this.scale);
this.focusedLine.setManaged(false);
if (comboBox.isEditable()) {
comboBox.getEditor().setStyle("-fx-background-color:TRANSPARENT;-fx-padding: 4 8 4 8");
comboBox.getEditor().promptTextProperty().unbind();
comboBox.getEditor().setPromptText(null);
comboBox.getEditor().textProperty().addListener((o, oldVal, newVal) -> {
this.usePromptText.invalidate();
comboBox.setValue(this.getConverter().fromString(newVal));
});
}
comboBox.labelFloatProperty().addListener((o, oldVal, newVal) -> {
if (newVal) {
this.promptText.visibleProperty().unbind();
JFXUtilities.runInFX(() -> this.createFloatingAnimation());
} else {
this.promptText.visibleProperty().bind(this.usePromptText);
}
this.createFocusTransition();
});
comboBox.focusColorProperty().addListener((o, oldVal, newVal) -> {
if (newVal != null) {
this.focusedLine.setBackground(new Background(new BackgroundFill(newVal, CornerRadii.EMPTY, Insets.EMPTY)));
if (((JFXComboBox) this.getSkinnable()).isLabelFloat()) {
this.promptTextColorTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptTextFill, newVal, Interpolator.EASE_BOTH)))) {
{
this.setDelay(Duration.millis(0.0F));
this.setCycleDuration(Duration.millis(160.0F));
}
protected void starting() {
super.starting();
JFXComboBoxListViewSkin.this.oldPromptTextFill = JFXComboBoxListViewSkin.this.promptTextFill.get();
}
};
this.transition = null;
}
}
});
comboBox.unFocusColorProperty().addListener((o, oldVal, newVal) -> {
if (newVal != null) {
this.line.setBackground(new Background(new BackgroundFill(newVal, CornerRadii.EMPTY, Insets.EMPTY)));
}
});
comboBox.disabledProperty().addListener((o, oldVal, newVal) -> {
this.line.setBorder(newVal ? new Border(new BorderStroke(((JFXComboBox) this.getSkinnable()).getUnFocusColor(), BorderStrokeStyle.DASHED, CornerRadii.EMPTY, new BorderWidths(this.line.getHeight()))) : Border.EMPTY);
this.line.setBackground(new Background(new BackgroundFill(newVal ? Color.TRANSPARENT : ((JFXComboBox) this.getSkinnable()).getUnFocusColor(), CornerRadii.EMPTY, Insets.EMPTY)));
});
comboBox.focusedProperty().addListener((o, oldVal, newVal) -> {
if (newVal) {
this.focus();
} else {
this.unFocus();
}
});
comboBox.valueProperty().addListener((o, oldVal, newVal) -> {
if (((JFXComboBox) this.getSkinnable()).isLabelFloat()) {
this.animateFloatingLabel(newVal != null && !newVal.toString().isEmpty());
}
});
}
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
this.customPane.resizeRelocate(x, y, w, h);
if (this.invalid) {
this.invalid = false;
if (!this.getSkinnable().isEditable()) {
Text javaPromptText = (Text) super.getDisplayNode().lookup(".text");
if (javaPromptText != null) {
this.promptTextFill.set(javaPromptText.getFill());
}
}
this.createFloatingAnimation();
if (((ComboBoxBase) this.getSkinnable()).getValue() != null) {
this.animateFloatingLabel(true);
}
}
this.focusedLine.resizeRelocate(x, this.getSkinnable().getHeight(), w, this.focusedLine.prefHeight(-1.0F));
this.line.resizeRelocate(x, this.getSkinnable().getHeight(), w, this.line.prefHeight(-1.0F));
this.scale.setPivotX(w / (double) 2.0F);
}
private void createFloatingAnimation() {
this.promptTextUpTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptText.translateYProperty(), -this.customPane.getHeight() + 6.05, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.xProperty(), 0.85, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.yProperty(), 0.85, Interpolator.EASE_BOTH)))) {
{
this.setDelay(Duration.millis(0.0F));
this.setCycleDuration(Duration.millis(240.0F));
}
};
this.promptTextColorTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptTextFill, ((JFXComboBox) this.getSkinnable()).getFocusColor(), Interpolator.EASE_BOTH)))) {
{
this.setDelay(Duration.millis(0.0F));
this.setCycleDuration(Duration.millis(160.0F));
}
protected void starting() {
super.starting();
JFXComboBoxListViewSkin.this.oldPromptTextFill = JFXComboBoxListViewSkin.this.promptTextFill.get();
}
};
this.promptTextDownTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptText.translateYProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.xProperty(), 1, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.yProperty(), 1, Interpolator.EASE_BOTH)))) {
{
this.setDelay(Duration.millis(0.0F));
this.setCycleDuration(Duration.millis(240.0F));
}
};
this.promptTextDownTransition.setOnFinished((finish) -> {
this.promptText.setTranslateY(0.0F);
this.promptTextScale.setX(1.0F);
this.promptTextScale.setY(1.0F);
});
}
private void focus() {
if (this.transition == null) {
this.createFocusTransition();
}
this.transition.play();
}
private void animateFloatingLabel(boolean up) {
if (this.promptText == null) {
Platform.runLater(() -> this.animateFloatingLabel(up));
} else {
if (this.transition != null) {
this.transition.stop();
this.transition.getChildren().remove(this.promptTextUpTransition);
this.transition.getChildren().remove(this.promptTextColorTransition);
this.transition = null;
}
if (up && this.promptText.getTranslateY() == (double) 0.0F) {
this.promptTextDownTransition.stop();
this.promptTextUpTransition.play();
if (this.getSkinnable().isFocused()) {
this.promptTextColorTransition.play();
}
} else if (!up) {
this.promptTextUpTransition.stop();
if (this.getSkinnable().isFocused()) {
this.promptTextFill.set(this.oldPromptTextFill);
}
this.promptTextDownTransition.play();
}
}
}
private void createFocusTransition() {
this.transition = new ParallelTransition();
if (((JFXComboBox) this.getSkinnable()).isLabelFloat()) {
this.transition.getChildren().add(this.promptTextUpTransition);
this.transition.getChildren().add(this.promptTextColorTransition);
}
this.transition.getChildren().add(this.linesAnimation);
}
private void unFocus() {
if (this.transition != null) {
this.transition.stop();
}
this.scale.setX(this.initScale);
this.focusedLine.setOpacity(0.0F);
if (((JFXComboBox) this.getSkinnable()).isLabelFloat() && this.oldPromptTextFill != null) {
this.promptTextFill.set(this.oldPromptTextFill);
if (this.usePromptText()) {
this.promptTextDownTransition.play();
}
}
}
private boolean usePromptText() {
Object txt = ((JFXComboBox) this.getSkinnable()).getValue();
String promptTxt = this.getSkinnable().getPromptText();
return (txt == null || txt.toString().isEmpty()) && promptTxt != null && !promptTxt.isEmpty() && !this.promptTextFill.get().equals(Color.TRANSPARENT);
}
}
@@ -1,520 +0,0 @@
/*
* 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);
}
}
}
@@ -1,459 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.skins;
import com.jfoenix.controls.*;
import com.jfoenix.svg.SVGGlyph;
import com.jfoenix.transitions.JFXFillTransition;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.control.Tab;
import javafx.scene.control.TextFormatter;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.*;
import javafx.util.Duration;
import org.glavo.monetfx.ColorScheme;
import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
* @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::close);
pickerDecorator.setPickOnBounds(false);
customScene = new Scene(pickerDecorator, Color.TRANSPARENT);
StyleSheets.init(customScene);
curvedColorPicker = new JFXCustomColorPicker();
StackPane pane = new StackPane(curvedColorPicker);
pane.setPadding(new Insets(18));
VBox container = new VBox();
container.getChildren().add(pane);
JFXTabPane tabs = new JFXTabPane();
JFXTextField rgbField = new JFXTextField();
JFXTextField hsbField = new JFXTextField();
JFXTextField hexField = new JFXTextField();
rgbField.getStyleClass().add("custom-color-field");
rgbField.setPromptText("RGB Color");
rgbField.setTextFormatter(colorCharFormatter());
rgbField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
hsbField.getStyleClass().add("custom-color-field");
hsbField.setPromptText("HSB Color");
hsbField.setTextFormatter(colorCharFormatter());
hsbField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
hexField.getStyleClass().add("custom-color-field");
hexField.setPromptText("#HEX Color");
hexField.setTextFormatter(colorCharFormatter());
hexField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
StackPane tabContent = new StackPane();
tabContent.getChildren().add(rgbField);
tabContent.setMinHeight(100);
Tab rgbTab = new Tab("RGB");
rgbTab.setContent(tabContent);
Tab hsbTab = new Tab("HSB");
hsbTab.setContent(hsbField);
Tab hexTab = new Tab("HEX");
hexTab.setContent(hexField);
tabs.getTabs().add(hexTab);
tabs.getTabs().add(rgbTab);
tabs.getTabs().add(hsbTab);
curvedColorPicker.selectedPath.addListener((o, oldVal, newVal) -> {
if (paraTransition != null) {
paraTransition.stop();
}
Region tabsHeader = (Region) tabs.lookup(".tab-header-background");
pane.backgroundProperty().unbind();
tabsHeader.backgroundProperty().unbind();
JFXFillTransition fillTransition = new JFXFillTransition(Duration.millis(240),
pane,
(Color) oldVal.getFill(),
(Color) newVal.getFill());
JFXFillTransition tabsFillTransition = new JFXFillTransition(Duration.millis(240),
tabsHeader,
(Color) oldVal.getFill(),
(Color) newVal.getFill());
paraTransition = new ParallelTransition(fillTransition, tabsFillTransition);
paraTransition.setOnFinished((finish) -> {
tabsHeader.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
return new Background(new BackgroundFill(newVal.getFill(), CornerRadii.EMPTY, Insets.EMPTY));
}, newVal.fillProperty()));
pane.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
return new Background(new BackgroundFill(newVal.getFill(), CornerRadii.EMPTY, Insets.EMPTY));
}, newVal.fillProperty()));
});
paraTransition.play();
});
container.getChildren().add(tabs);
HBox actionsHBox = new HBox();
actionsHBox.getStyleClass().add("jfx-color-dialog-actions");
JFXButton acceptButton = new JFXButton(i18n("button.ok"));
acceptButton.setOnAction(event -> updateColor());
acceptButton.getStyleClass().add("jfx-color-dialog-accept");
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.setOnAction(event -> close());
cancelButton.getStyleClass().add("jfx-color-dialog-cancel");
actionsHBox.getChildren().addAll(acceptButton, cancelButton);
container.getChildren().add(actionsHBox);
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("rgb(%d, %d, %d)",
(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()));
acceptButton.textFillProperty().bind(Bindings.createObjectBinding(() -> {
Color fill = (Color) pane.getBackground().getFills().get(0).getFill();
return ColorScheme.newBuilder(Themes.getColorScheme())
.setPrimaryColorSeed(fill)
.build()
.getPrimary();
}, 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()));
});
};
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 {
Color color = Color.valueOf(colorWebString);
if (color.getOpacity() != 1.0) {
color = Color.color(color.getRed(), color.getGreen(), color.getBlue());
}
curvedColorPicker.setColor(color);
} catch (IllegalArgumentException ignored) {
// if color is not valid then do nothing
}
userChange = false;
}
}
private void close() {
dialog.setScene(null);
dialog.close();
}
public void setCurrentColor(Color currentColor) {
this.currentColorProperty.set(currentColor);
if (curvedColorPicker != null && currentColor != null) {
curvedColorPicker.setColor(currentColor);
}
}
Color getCurrentColor() {
return currentColorProperty.get();
}
ObjectProperty<Color> customColorProperty() {
return customColorProperty;
}
void setCustomColor(Color color) {
customColorProperty.set(color);
}
Color getCustomColor() {
return customColorProperty.get();
}
public Runnable getOnSave() {
return onSave;
}
public void setOnSave(Runnable onSave) {
this.onSave = onSave;
}
public void setOnHidden(EventHandler<WindowEvent> onHidden) {
dialog.setOnHidden(onHidden);
}
public void show() {
dialog.setOpacity(0);
// pickerDecorator.setOpacity(0);
if (dialog.getOwner() != null) {
dialog.widthProperty().addListener(positionAdjuster);
dialog.heightProperty().addListener(positionAdjuster);
positionAdjuster.invalidated(null);
}
if (dialog.getScene() == null) {
dialog.setScene(customScene);
}
curvedColorPicker.preAnimate();
dialog.show();
if (initOnce) {
initRun.run();
initOnce = false;
}
Timeline timeline = new Timeline(new KeyFrame(Duration.millis(120),
new KeyValue(dialog.opacityProperty(),
1,
Interpolator.EASE_BOTH)));
timeline.setOnFinished((finish) -> curvedColorPicker.animate());
timeline.play();
}
// add option to show color picker using JFX Dialog
private InvalidationListener positionAdjuster = new InvalidationListener() {
@Override
public void invalidated(Observable ignored) {
if (Double.isNaN(dialog.getWidth()) || Double.isNaN(dialog.getHeight())) {
return;
}
dialog.widthProperty().removeListener(positionAdjuster);
dialog.heightProperty().removeListener(positionAdjuster);
fixPosition();
}
};
private void fixPosition() {
Window w = dialog.getOwner();
Screen s = com.sun.javafx.util.Utils.getScreen(w);
Rectangle2D sb = s.getBounds();
double xR = w.getX() + w.getWidth();
double xL = w.getX() - dialog.getWidth();
double x;
double y;
if (sb.getMaxX() >= xR + dialog.getWidth()) {
x = xR;
} else if (sb.getMinX() <= xL) {
x = xL;
} else {
x = Math.max(sb.getMinX(), sb.getMaxX() - dialog.getWidth());
}
y = Math.max(sb.getMinY(), Math.min(sb.getMaxY() - dialog.getHeight(), w.getY()));
dialog.setX(x);
dialog.setY(y);
}
@Override
public void layoutChildren() {
super.layoutChildren();
if (dialog.getMinWidth() > 0 && dialog.getMinHeight() > 0) {
return;
}
double minWidth = Math.max(0, computeMinWidth(getHeight()) + (dialog.getWidth() - customScene.getWidth()));
double minHeight = Math.max(0, computeMinHeight(getWidth()) + (dialog.getHeight() - customScene.getHeight()));
dialog.setMinWidth(minWidth);
dialog.setMinHeight(minHeight);
}
private static final Pattern COLOR_CHAR_PATTERN = Pattern.compile("[0-9a-zA-Z#(),%.\\s]*");
private static TextFormatter<String> colorCharFormatter() {
return new TextFormatter<>(change -> {
if (!change.isContentChange()) return change;
String ins = StringUtils.toHalfWidth(change.getText());
if (!COLOR_CHAR_PATTERN.matcher(ins).matches()) return null;
String full = StringUtils.toHalfWidth(change.getControlNewText());
long h = full.chars().filter(c -> c == '#').count();
if (h > 1 || (h == 1 && full.indexOf('#') != 0)) return null;
change.setText(ins);
return change;
});
}
}
@@ -1,242 +0,0 @@
/*
* 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"));
}
}
@@ -1,47 +0,0 @@
// Copy from https://github.com/sshahine/JFoenix/blob/d427fd801a338f934307ba41ce604eb5c79f0b20/jfoenix/src/main/java/com/jfoenix/skins/JFXListViewSkin.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.skins;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.effects.JFXDepthManager;
import javafx.scene.control.ListCell;
import javafx.scene.control.skin.ListViewSkin;
import javafx.scene.control.skin.VirtualFlow;
import org.jackhuang.hmcl.ui.FXUtils;
public class JFXListViewSkin<T> extends ListViewSkin<T> {
public JFXListViewSkin(final JFXListView<T> listView) {
super(listView);
VirtualFlow<ListCell<T>> flow = getVirtualFlow();
FXUtils.onChangeAndOperate(listView.depthProperty(), depth -> JFXDepthManager.setDepth(flow, depth.intValue()));
if (!Boolean.TRUE.equals(listView.getProperties().get("no-smooth-scrolling"))) {
FXUtils.smoothScrolling(flow);
}
}
@Override
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return 200;
}
}
@@ -1,139 +0,0 @@
/*
* Copyright (c) 2016 JFoenix
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.jfoenix.skins;
import com.jfoenix.controls.JFXPopup;
import com.jfoenix.controls.JFXPopup.PopupHPosition;
import com.jfoenix.controls.JFXPopup.PopupVPosition;
import com.jfoenix.effects.JFXDepthManager;
import javafx.animation.*;
import javafx.animation.Animation.Status;
import javafx.scene.Node;
import javafx.scene.control.Skin;
import javafx.scene.layout.*;
import javafx.scene.transform.Scale;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.animation.Motion;
/// # Material Design Popup Skin
/// TODO: REWORK
///
/// @author Shadi Shaheen
/// @version 2.0
/// @since 2017-03-01
public class JFXPopupSkin implements Skin<JFXPopup> {
protected JFXPopup control;
protected StackPane container = new StackPane();
protected Region popupContent;
protected Node root;
private Animation animation;
protected Scale scale;
public JFXPopupSkin(JFXPopup control) {
this.control = control;
// set scale y to 0.01 instead of 0 to allow layout of the content,
// otherwise it will cause exception in traverse engine, when focusing the 1st node
scale = new Scale(1.0, 0.01, 0, 0);
popupContent = control.getPopupContent();
container.getStyleClass().add("jfx-popup-container");
container.getChildren().add(popupContent);
container.getTransforms().add(scale);
container.setOpacity(0);
root = JFXDepthManager.createMaterialNode(container, 4);
animation = AnimationUtils.isAnimationEnabled() ? getAnimation() : null;
}
public void reset(PopupVPosition vAlign, PopupHPosition hAlign, double offsetX, double offsetY) {
// postion the popup according to its animation
scale.setPivotX(hAlign == PopupHPosition.RIGHT ? container.getWidth() : 0);
scale.setPivotY(vAlign == PopupVPosition.BOTTOM ? container.getHeight() : 0);
control.setX(control.getX() + (hAlign == PopupHPosition.RIGHT ? -container.getWidth() + offsetX : offsetX));
control.setY(control.getY() + (vAlign == PopupVPosition.BOTTOM ? -container.getHeight() + offsetY : offsetY));
}
public final void animate() {
if (animation != null) {
if (animation.getStatus() == Status.STOPPED) {
container.setOpacity(1);
animation.playFromStart();
}
} else {
container.setOpacity(1);
popupContent.setOpacity(1);
scale.setX(1.0);
scale.setY(1.0);
}
}
@Override
public JFXPopup getSkinnable() {
return control;
}
@Override
public Node getNode() {
return root;
}
@Override
public void dispose() {
if (animation != null) {
animation.stop();
animation = null;
}
container = null;
control = null;
popupContent = null;
root = null;
}
protected Animation getAnimation() {
Interpolator interpolator = Motion.EASE;
return new Timeline(
new KeyFrame(
Duration.ZERO,
new KeyValue(popupContent.opacityProperty(), 0, interpolator),
new KeyValue(scale.xProperty(), 0, interpolator),
new KeyValue(scale.yProperty(), 0, interpolator)
),
new KeyFrame(Motion.SHORT4,
new KeyValue(popupContent.opacityProperty(), 0, interpolator),
new KeyValue(scale.xProperty(), 1, interpolator)
),
new KeyFrame(Motion.MEDIUM2,
new KeyValue(popupContent.opacityProperty(), 1, interpolator),
new KeyValue(scale.yProperty(), 1, interpolator)
)
);
}
public void init() {
if (animation != null)
animation.stop();
container.setOpacity(0);
scale.setX(1.0);
scale.setY(0.01);
}
}
@@ -1,225 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.skins;
import com.jfoenix.controls.JFXProgressBar;
import com.jfoenix.utils.TreeShowingProperty;
import javafx.animation.*;
import javafx.scene.Node;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.*;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
/// # Material Design ProgressBar Skin
///
/// @author Shadi Shaheen
/// @version 2.0
/// @since 2017-10-06
public class JFXProgressBarSkin extends SkinBase<JFXProgressBar> {
private static final double DEFAULT_HEIGHT = 4;
private final StackPane track;
private final StackPane bar;
private final Rectangle clip;
private Animation transition;
private final TreeShowingProperty treeShowingProperty;
private double fullWidth;
public JFXProgressBarSkin(JFXProgressBar control) {
super(control);
this.treeShowingProperty = new TreeShowingProperty(control);
registerChangeListener(treeShowingProperty, obs -> updateProgress(false));
registerChangeListener(control.progressProperty(), obs -> updateProgress(true));
track = new StackPane();
track.getStyleClass().setAll("track");
bar = new StackPane();
bar.getStyleClass().setAll("bar");
clip = new Rectangle();
clip.setArcWidth(DEFAULT_HEIGHT);
clip.setArcHeight(DEFAULT_HEIGHT);
bar.setClip(clip);
getChildren().setAll(track, bar);
getSkinnable().requestLayout();
}
@Override
public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
return Node.BASELINE_OFFSET_SAME_AS_HEIGHT;
}
@Override
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return Math.max(100, leftInset + bar.prefWidth(getSkinnable().getWidth()) + rightInset);
}
@Override
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
return topInset + DEFAULT_HEIGHT + bottomInset;
}
@Override
protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return getSkinnable().prefWidth(height);
}
@Override
protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
return getSkinnable().prefHeight(width);
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
track.resizeRelocate(x, y, w, h);
bar.resizeRelocate(x, y, w, h);
clip.relocate(0, 0);
clip.setWidth(0);
clip.setHeight(h);
clip.setTranslateX(0);
fullWidth = w;
clearAnimation();
updateProgress(false);
}
private boolean wasIndeterminate = false;
private void updateProgress(boolean playProgressAnimation) {
double progress = Math.min(getSkinnable().getProgress(), 1.0);
boolean isIndeterminate = progress < 0.0;
boolean isTreeShowing = treeShowingProperty.get();
if (isIndeterminate != wasIndeterminate) {
wasIndeterminate = isIndeterminate;
clearAnimation();
clip.setTranslateX(0);
}
if (isIndeterminate) { // indeterminate
if (isTreeShowing) {
if (transition == null) {
transition = createIndeterminateTransition();
transition.playFromStart();
} else {
transition.play();
}
} else if (transition != null) {
transition.pause();
}
} else { // determinate
clearAnimation();
if (isTreeShowing && playProgressAnimation
&& AnimationUtils.isAnimationEnabled()
&& getSkinnable().isSmoothProgress()) {
transition = createDeterminateTransition(progress);
transition.playFromStart();
} else {
clip.setWidth(computeBarWidth(progress));
}
}
}
private static final Duration INDETERMINATE_DURATION = Duration.seconds(1);
private Transition createIndeterminateTransition() {
double minWidth = 0;
double maxWidth = fullWidth * 0.4;
Transition transition = new Transition() {
{
setInterpolator(Interpolator.LINEAR);
setCycleDuration(INDETERMINATE_DURATION);
}
@Override
protected void interpolate(double frac) {
double currentWidth;
if (frac <= 0.5) {
currentWidth = Interpolator.EASE_IN.interpolate(minWidth, maxWidth, frac / 0.5);
} else {
currentWidth = Interpolator.EASE_OUT.interpolate(maxWidth, minWidth, (frac - 0.5) / 0.5);
}
double targetCenter;
if (frac <= 0.1) {
targetCenter = 0.0;
} else if (frac >= 0.9) {
targetCenter = fullWidth;
} else {
targetCenter = ((frac - 0.1) / 0.8) * fullWidth;
}
clip.setWidth(currentWidth);
clip.setTranslateX(targetCenter - currentWidth / 2.0);
}
};
transition.setCycleCount(Timeline.INDEFINITE);
return transition;
}
private static final Duration DETERMINATE_DURATION = Duration.seconds(0.2);
private Timeline createDeterminateTransition(double targetProgress) {
Timeline timeline = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(clip.widthProperty(), clip.getWidth(), Interpolator.EASE_OUT)),
new KeyFrame(DETERMINATE_DURATION,
new KeyValue(clip.widthProperty(), computeBarWidth(targetProgress), Interpolator.EASE_OUT))
);
timeline.setOnFinished(e -> {
if (transition == timeline) {
transition = null;
}
});
return timeline;
}
private void clearAnimation() {
if (transition != null) {
transition.stop();
transition = null;
}
}
@Override
public void dispose() {
super.dispose();
treeShowingProperty.dispose();
clearAnimation();
}
private double computeBarWidth(double progress) {
assert progress >= 0 && progress <= 1;
double barWidth = ((int) fullWidth * 2 * progress) / 2.0;
return progress > 0 ? Math.max(barWidth, DEFAULT_HEIGHT) : barWidth;
}
}
@@ -1,179 +0,0 @@
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.jfoenix.skins;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.controls.JFXRippler;
import com.jfoenix.controls.JFXRippler.RipplerMask;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.VPos;
import javafx.scene.control.RadioButton;
import javafx.scene.control.skin.RadioButtonSkin;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.text.Text;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
public class JFXRadioButtonSkin extends RadioButtonSkin {
private static final double PADDING = 15.0;
private boolean invalid = true;
private final JFXRippler rippler;
private final Circle radio;
private final Circle dot;
private Timeline timeline;
private final AnchorPane container = new AnchorPane();
private final double labelOffset = -10.0;
public JFXRadioButtonSkin(JFXRadioButton control) {
super(control);
double radioRadius = 7.0;
this.radio = new Circle(radioRadius);
this.radio.getStyleClass().setAll("radio");
this.radio.setStrokeWidth(2.0);
this.radio.setFill(Color.TRANSPARENT);
this.dot = new Circle(4);
this.dot.getStyleClass().setAll("dot");
this.dot.fillProperty().bind(control.selectedColorProperty());
this.dot.setScaleX(0.0);
this.dot.setScaleY(0.0);
StackPane boxContainer = new StackPane();
boxContainer.getChildren().addAll(this.radio, this.dot);
boxContainer.setPadding(new Insets(PADDING));
this.rippler = new JFXRippler(boxContainer, RipplerMask.CIRCLE);
this.container.getChildren().add(this.rippler);
AnchorPane.setRightAnchor(this.rippler, this.labelOffset);
this.updateChildren();
ReadOnlyBooleanProperty focusVisibleProperty = FXUtils.focusVisibleProperty(control);
if (focusVisibleProperty == null) {
focusVisibleProperty = control.focusedProperty();
}
focusVisibleProperty.addListener((o, oldVal, newVal) -> {
if (newVal) {
if (!this.getSkinnable().isPressed()) {
this.rippler.showOverlay();
}
} else {
this.rippler.hideOverlay();
}
});
control.pressedProperty().addListener((o, oldVal, newVal) -> this.rippler.hideOverlay());
this.registerChangeListener(control.selectedColorProperty(), ignored -> updateColors());
this.registerChangeListener(control.unSelectedColorProperty(), ignored -> updateColors());
this.registerChangeListener(control.selectedProperty(), ignored -> {
updateColors();
this.playAnimation();
});
}
protected void updateChildren() {
super.updateChildren();
if (this.radio != null) {
this.removeRadio();
this.getChildren().add(this.container);
}
}
protected void layoutChildren(double x, double y, double w, double h) {
RadioButton radioButton = this.getSkinnable();
double contWidth = this.snapSizeX(this.container.prefWidth(-1.0)) + (double) (this.invalid ? 2 : 0);
double contHeight = this.snapSizeY(this.container.prefHeight(-1.0)) + (double) (this.invalid ? 2 : 0);
double computeWidth = Math.min(radioButton.prefWidth(-1.0), radioButton.minWidth(-1.0)) + this.labelOffset + 2.0 * this.PADDING;
double labelWidth = Math.min(computeWidth - contWidth, w - this.snapSizeX(contWidth)) + this.labelOffset + 2.0 * PADDING;
double labelHeight = Math.min(radioButton.prefHeight(labelWidth), h);
double maxHeight = Math.max(contHeight, labelHeight);
double xOffset = computeXOffset(w, labelWidth + contWidth, radioButton.getAlignment().getHpos()) + x;
double yOffset = computeYOffset(h, maxHeight, radioButton.getAlignment().getVpos()) + x;
if (this.invalid) {
this.initializeComponents();
this.invalid = false;
}
this.layoutLabelInArea(xOffset + contWidth, yOffset, labelWidth, maxHeight, radioButton.getAlignment());
((Text) this.getChildren().get(this.getChildren().get(0) instanceof Text ? 0 : 1)).textProperty().set(this.getSkinnable().textProperty().get());
this.container.resize(this.snapSizeX(contWidth), this.snapSizeY(contHeight));
this.positionInArea(this.container, xOffset, yOffset, contWidth, maxHeight, 0.0, radioButton.getAlignment().getHpos(), radioButton.getAlignment().getVpos());
}
private void initializeComponents() {
this.updateColors();
this.playAnimation();
}
private void playAnimation() {
if (AnimationUtils.isAnimationEnabled()) {
if (this.timeline == null) {
this.timeline = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(this.dot.scaleXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(this.dot.scaleYProperty(), 0, Interpolator.EASE_BOTH)),
new KeyFrame(Duration.millis(200.0),
new KeyValue(this.dot.scaleXProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(this.dot.scaleYProperty(), 1, Interpolator.EASE_BOTH))
);
} else {
this.timeline.stop();
}
this.timeline.setRate(this.getSkinnable().isSelected() ? 1.0 : -1.0);
this.timeline.play();
} else {
double endScale = this.getSkinnable().isSelected() ? 1.0 : 0.0;
this.dot.setScaleX(endScale);
this.dot.setScaleY(endScale);
}
}
private void removeRadio() {
this.getChildren().removeIf(node -> "radio".equals(node.getStyleClass().get(0)));
}
private void updateColors() {
var control = (JFXRadioButton) getSkinnable();
boolean isSelected = control.isSelected();
Color unSelectedColor = control.getUnSelectedColor();
Color selectedColor = control.getSelectedColor();
rippler.setRipplerFill(isSelected ? selectedColor : unSelectedColor);
radio.setStroke(isSelected ? selectedColor : unSelectedColor);
}
protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset) + this.snapSizeX(this.radio.minWidth(-1.0)) + this.labelOffset + 2.0 * PADDING;
}
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset) + this.snapSizeX(this.radio.prefWidth(-1.0)) + this.labelOffset + 2.0 * PADDING;
}
static double computeXOffset(double width, double contentWidth, HPos hpos) {
return switch (hpos) {
case LEFT -> 0.0;
case CENTER -> (width - contentWidth) / 2.0;
case RIGHT -> width - contentWidth;
};
}
static double computeYOffset(double height, double contentHeight, VPos vpos) {
return switch (vpos) {
case TOP, BASELINE -> 0.0;
case CENTER -> (height - contentHeight) / 2.0;
case BOTTOM -> height - contentHeight;
};
}
}
@@ -1,291 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.skins;
import com.jfoenix.controls.JFXSlider;
import com.jfoenix.controls.JFXSlider.IndicatorPosition;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.chart.NumberAxis;
import javafx.scene.control.Slider;
import javafx.scene.control.skin.SliderSkin;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.util.Duration;
/// # Material Design Slider Skin
///
/// rework of JFXSliderSkin by extending Java SliderSkin
/// this solves padding and resizing issues
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
public class JFXSliderSkin extends SliderSkin {
private static final PseudoClass MIN_VALUE = PseudoClass.getPseudoClass("min");
private static final PseudoClass MAX_VALUE = PseudoClass.getPseudoClass("max");
private final Pane mouseHandlerPane = new Pane();
private final Text sliderValue;
private final StackPane coloredTrack;
private final StackPane thumb;
private final StackPane track;
private final StackPane animatedThumb;
private NumberAxis tickLine;
private Timeline timeline;
private double indicatorRotation;
private double horizontalRotation;
private double shifting;
public JFXSliderSkin(JFXSlider slider) {
super(slider);
track = (StackPane) getSkinnable().lookup(".track");
thumb = (StackPane) getSkinnable().lookup(".thumb");
tickLine = (NumberAxis) getSkinnable().lookup(".axis");
if (tickLine != null) tickLine.setAnimated(false);
coloredTrack = new StackPane();
coloredTrack.getStyleClass().add("colored-track");
coloredTrack.setMouseTransparent(true);
sliderValue = new Text();
sliderValue.getStyleClass().setAll("slider-value");
animatedThumb = new StackPane();
animatedThumb.getStyleClass().add("animated-thumb");
animatedThumb.getChildren().add(sliderValue);
animatedThumb.setMouseTransparent(true);
animatedThumb.setScaleX(0);
animatedThumb.setScaleY(0);
thumb.layoutXProperty().addListener(x -> {
if (slider.getOrientation() == Orientation.VERTICAL) initAnimation(Orientation.VERTICAL);
});
thumb.layoutYProperty().addListener(y -> {
if (slider.getOrientation() == Orientation.HORIZONTAL) initAnimation(Orientation.HORIZONTAL);
});
addJFXChildren();
getChildren().addListener((ListChangeListener<Node>) c -> {
while (c.next()) {
if (c.wasAdded()) {
c.getAddedSubList().forEach(added -> {
if (added instanceof NumberAxis) {
tickLine = (NumberAxis) added;
tickLine.setAnimated(false);
}
});
}
}
});
registerChangeListener(slider.showTickMarksProperty(), e -> addJFXChildren());
registerChangeListener(slider.showTickLabelsProperty(), e -> addJFXChildren());
registerChangeListener(slider.valueFactoryProperty(), obs -> refreshSliderValueBinding());
initListeners();
}
private void addJFXChildren() {
ObservableList<Node> children = getChildren();
Slider slider = getSkinnable();
if ((slider.isShowTickMarks() || slider.isShowTickLabels()) && tickLine != null && !children.contains(tickLine)) {
children.add(0, tickLine);
}
if (children.contains(coloredTrack)) return;
children.add(children.indexOf(thumb), coloredTrack);
children.add(children.indexOf(thumb), animatedThumb);
children.add(0, mouseHandlerPane);
}
private void refreshSliderValueBinding() {
sliderValue.textProperty().unbind();
if (((JFXSlider) getSkinnable()).getValueFactory() != null) {
sliderValue.textProperty()
.bind(((JFXSlider) getSkinnable()).getValueFactory().call((JFXSlider) getSkinnable()));
} else {
sliderValue.textProperty().bind(Bindings.createStringBinding(() -> {
if (getSkinnable().getLabelFormatter() != null) {
return getSkinnable().getLabelFormatter().toString(getSkinnable().getValue());
} else {
return String.valueOf(Math.round(getSkinnable().getValue()));
}
}, getSkinnable().valueProperty()));
}
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
if (timeline == null) {
initAnimation(getSkinnable().getOrientation());
}
double prefWidth = animatedThumb.prefWidth(-1);
animatedThumb.resize(prefWidth, animatedThumb.prefHeight(prefWidth));
boolean horizontal = getSkinnable().getOrientation() == Orientation.HORIZONTAL;
double width, height, layoutX, layoutY;
if (horizontal) {
width = thumb.getLayoutX() - snappedLeftInset();
height = track.getHeight();
layoutX = track.getLayoutX();
layoutY = track.getLayoutY();
animatedThumb.setLayoutX(thumb.getLayoutX() + thumb.getWidth() / 2 - animatedThumb.getWidth() / 2);
} else {
height = track.getLayoutBounds().getMaxY() + track.getLayoutY() - thumb.getLayoutY() - snappedBottomInset();
width = track.getWidth();
layoutX = track.getLayoutX();
layoutY = thumb.getLayoutY();
animatedThumb.setLayoutY(thumb.getLayoutY() + thumb.getHeight() / 2 - animatedThumb.getHeight() / 2);
}
coloredTrack.resizeRelocate(layoutX, layoutY, width, height);
mouseHandlerPane.resizeRelocate(x, y, w, h);
}
private void initializeVariables() {
shifting = 30 + thumb.getWidth();
if (getSkinnable().getOrientation() != Orientation.HORIZONTAL) {
horizontalRotation = -90;
}
if (((JFXSlider) getSkinnable()).getIndicatorPosition() != IndicatorPosition.LEFT) {
indicatorRotation = 180;
shifting = -shifting;
}
final double rotationAngle = 45;
sliderValue.setRotate(rotationAngle + indicatorRotation + 3 * horizontalRotation);
animatedThumb.setRotate(-rotationAngle + indicatorRotation + horizontalRotation);
}
private void initListeners() {
// delegate slider mouse events to track node
mouseHandlerPane.setOnMousePressed(this::delegateToTrack);
mouseHandlerPane.setOnMouseReleased(this::delegateToTrack);
mouseHandlerPane.setOnMouseDragged(this::delegateToTrack);
// animate value node
track.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> {
timeline.setRate(1);
timeline.play();
});
track.addEventHandler(MouseEvent.MOUSE_RELEASED, (event) -> {
timeline.setRate(-1);
timeline.play();
});
thumb.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> {
timeline.setRate(1);
timeline.play();
});
thumb.addEventHandler(MouseEvent.MOUSE_RELEASED, (event) -> {
timeline.setRate(-1);
timeline.play();
});
refreshSliderValueBinding();
updateValueStyleClass();
getSkinnable().valueProperty().addListener(observable -> updateValueStyleClass());
}
private void delegateToTrack(MouseEvent event) {
if (!event.isConsumed()) {
event.consume();
track.fireEvent(event);
}
}
private void updateValueStyleClass() {
getSkinnable().pseudoClassStateChanged(MIN_VALUE, getSkinnable().getMin() == getSkinnable().getValue());
getSkinnable().pseudoClassStateChanged(MAX_VALUE, getSkinnable().getMax() == getSkinnable().getValue());
}
private void initAnimation(Orientation orientation) {
initializeVariables();
double thumbPos, thumbNewPos;
DoubleProperty layoutProperty;
if (orientation == Orientation.HORIZONTAL) {
if (((JFXSlider) getSkinnable()).getIndicatorPosition() == IndicatorPosition.RIGHT) {
thumbPos = thumb.getLayoutY() - thumb.getHeight();
thumbNewPos = thumbPos - shifting;
} else {
double height = animatedThumb.prefHeight(animatedThumb.prefWidth(-1));
thumbPos = thumb.getLayoutY() - height / 2;
thumbNewPos = thumb.getLayoutY() - height - thumb.getHeight();
}
layoutProperty = animatedThumb.translateYProperty();
} else {
if (((JFXSlider) getSkinnable()).getIndicatorPosition() == IndicatorPosition.RIGHT) {
thumbPos = thumb.getLayoutX() - thumb.getWidth();
thumbNewPos = thumbPos - shifting;
} else {
double width = animatedThumb.prefWidth(-1);
thumbPos = thumb.getLayoutX() - width / 2;
thumbNewPos = thumb.getLayoutX() - width - thumb.getWidth();
}
layoutProperty = animatedThumb.translateXProperty();
}
clearAnimation();
timeline = new Timeline(
new KeyFrame(
Duration.ZERO,
new KeyValue(animatedThumb.scaleXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(animatedThumb.scaleYProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(layoutProperty, thumbPos, Interpolator.EASE_BOTH)),
new KeyFrame(
Duration.seconds(0.2),
new KeyValue(animatedThumb.scaleXProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(animatedThumb.scaleYProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(layoutProperty, thumbNewPos, Interpolator.EASE_BOTH)));
}
@Override
public void dispose() {
super.dispose();
clearAnimation();
}
private void clearAnimation() {
if (timeline != null) {
timeline.stop();
timeline.getKeyFrames().clear();
timeline = null;
}
}
}
@@ -1,264 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.skins;
import com.jfoenix.controls.JFXSpinner;
import com.jfoenix.utils.TreeShowingProperty;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.scene.Group;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Arc;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeLineCap;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import java.util.ArrayList;
import java.util.List;
/// JFXSpinner material design skin
///
/// @author Shadi Shaheen & Gerard Moubarak
/// @version 1.0
/// @since 2017-09-25
public class JFXSpinnerSkin extends SkinBase<JFXSpinner> {
private static final double DEFAULT_STROKE_WIDTH = 4;
private JFXSpinner control;
private final TreeShowingProperty treeShowingProperty;
private boolean isValid = false;
private Timeline timeline;
private Arc arc;
private Arc track;
private final StackPane arcPane;
private final Rectangle fillRect;
private final double startingAngle;
public JFXSpinnerSkin(JFXSpinner control) {
super(control);
this.control = control;
this.treeShowingProperty = new TreeShowingProperty(control);
this.startingAngle = control.getStartingAngle();
arc = new Arc();
arc.setManaged(false);
arc.setLength(180);
arc.getStyleClass().setAll("arc");
arc.setFill(Color.TRANSPARENT);
arc.setStrokeWidth(DEFAULT_STROKE_WIDTH);
arc.setStrokeLineCap(StrokeLineCap.ROUND);
track = new Arc();
track.setManaged(false);
track.setLength(360);
track.setStrokeWidth(DEFAULT_STROKE_WIDTH);
track.getStyleClass().setAll("track");
track.setFill(Color.TRANSPARENT);
fillRect = new Rectangle();
fillRect.setFill(Color.TRANSPARENT);
final Group group = new Group(fillRect, track, arc);
group.setManaged(false);
arcPane = new StackPane(group);
arcPane.setPrefSize(50, 50);
getChildren().setAll(arcPane);
// register listeners
registerChangeListener(control.progressProperty(), obs -> updateProgress());
registerChangeListener(treeShowingProperty, obs -> updateProgress());
}
private void updateProgress() {
double progress = Double.min(getSkinnable().getProgress(), 1.0);
if (progress < 0) { // indeterminate
boolean treeShowing = treeShowingProperty.get();
if (treeShowing) {
if (timeline == null) {
timeline = createTransition();
timeline.playFromStart();
} else {
timeline.play();
}
} else if (timeline != null) {
timeline.pause();
}
} else { // determinate
clearAnimation();
arc.setStartAngle(90);
arc.setLength(-360 * progress);
}
}
private double computeSize() {
return control.getRadius() * 2 + arc.getStrokeWidth() * 2;
}
@Override
protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
if (Region.USE_COMPUTED_SIZE == control.getRadius()) {
return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset);
} else {
return computeSize();
}
}
@Override
protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
if (Region.USE_COMPUTED_SIZE == control.getRadius()) {
return super.computeMaxWidth(height, topInset, rightInset, bottomInset, leftInset);
} else {
return computeSize();
}
}
@Override
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
if (Region.USE_COMPUTED_SIZE == control.getRadius()) {
return arcPane.prefWidth(-1);
} else {
return computeSize();
}
}
@Override
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
if (Region.USE_COMPUTED_SIZE == control.getRadius()) {
return arcPane.prefHeight(-1);
} else {
return computeSize();
}
}
/**
* {@inheritDoc}
*/
@Override
protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
final double strokeWidth = arc.getStrokeWidth();
double radius = control.getRadius();
final double arcSize = snapSizeX(radius * 2 + strokeWidth);
arcPane.resizeRelocate((contentWidth - arcSize) / 2 + 1, (contentHeight - arcSize) / 2 + 1, arcSize, arcSize);
updateArcLayout(radius, arcSize);
fillRect.setWidth(arcSize);
fillRect.setHeight(arcSize);
if (!isValid) {
updateProgress();
isValid = true;
}
}
private void updateArcLayout(double radius, double arcSize) {
arc.setRadiusX(radius);
arc.setRadiusY(radius);
arc.setCenterX(arcSize / 2);
arc.setCenterY(arcSize / 2);
track.setRadiusX(radius);
track.setRadiusY(radius);
track.setCenterX(arcSize / 2);
track.setCenterY(arcSize / 2);
track.setStrokeWidth(arc.getStrokeWidth());
}
private void addKeyFrames(List<KeyFrame> frames, double angle, double duration) {
frames.add(new KeyFrame(Duration.seconds(duration),
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
new KeyValue(arc.startAngleProperty(),
angle + 45 + startingAngle,
Interpolator.LINEAR)));
frames.add(new KeyFrame(Duration.seconds(duration + 0.4),
new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR),
new KeyValue(arc.startAngleProperty(),
angle + 90 + startingAngle,
Interpolator.LINEAR)));
frames.add(new KeyFrame(Duration.seconds(duration + 0.7),
new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR),
new KeyValue(arc.startAngleProperty(),
angle + 135 + startingAngle,
Interpolator.LINEAR)));
frames.add(new KeyFrame(Duration.seconds(duration + 1.1),
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
new KeyValue(arc.startAngleProperty(),
angle + 435 + startingAngle,
Interpolator.LINEAR)));
}
private Timeline createTransition() {
Timeline timeline;
if (AnimationUtils.isAnimationEnabled()) {
var keyFrames = new ArrayList<KeyFrame>(17);
addKeyFrames(keyFrames, 0, 0);
addKeyFrames(keyFrames, 450, 1.4);
addKeyFrames(keyFrames, 900, 2.8);
addKeyFrames(keyFrames, 1350, 4.2);
keyFrames.add(new KeyFrame(Duration.seconds(5.6),
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
new KeyValue(arc.startAngleProperty(),
1845 + startingAngle,
Interpolator.LINEAR)));
timeline = new Timeline();
timeline.getKeyFrames().setAll(keyFrames);
} else {
final double arcLength = 250;
timeline = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(arc.startAngleProperty(), 45 + startingAngle, Interpolator.LINEAR),
new KeyValue(arc.lengthProperty(), arcLength, Interpolator.DISCRETE)),
new KeyFrame(Duration.seconds(1.2),
new KeyValue(arc.startAngleProperty(), 45 + 360 + startingAngle, Interpolator.LINEAR),
new KeyValue(arc.lengthProperty(), arcLength, Interpolator.DISCRETE))
);
}
timeline.setCycleCount(Timeline.INDEFINITE);
return timeline;
}
private void clearAnimation() {
if (timeline != null) {
timeline.stop();
timeline = null;
}
}
@Override
public void dispose() {
super.dispose();
treeShowingProperty.dispose();
clearAnimation();
arc = null;
track = null;
control = null;
}
}
File diff suppressed because it is too large. Load diff
@@ -1,186 +0,0 @@
/*
* 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;
}
}
@@ -1,68 +0,0 @@
/*
* 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);
}
}
}
}
@@ -1,286 +0,0 @@
/*
* 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();
}
}
}
@@ -1,104 +0,0 @@
/*
* 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;
}
}
}
@@ -1,157 +0,0 @@
/*
* 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;
}
}
}
@@ -1,140 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.utils;
import javafx.beans.value.ObservableBooleanValue;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.stage.Window;
import org.jetbrains.annotations.NotNull;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.util.Locale;
import java.util.function.Function;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2017-02-11
public final class JFXNodeUtils {
public static void updateBackground(Background newBackground, Region nodeToUpdate) {
updateBackground(newBackground, nodeToUpdate, Color.BLACK);
}
public static void updateBackground(Background newBackground, Region nodeToUpdate, Paint fill) {
if (newBackground != null && !newBackground.getFills().isEmpty()) {
final BackgroundFill[] fills = new BackgroundFill[newBackground.getFills().size()];
for (int i = 0; i < newBackground.getFills().size(); i++) {
BackgroundFill bf = newBackground.getFills().get(i);
fills[i] = new BackgroundFill(fill, bf.getRadii(), bf.getInsets());
}
nodeToUpdate.setBackground(new Background(fills));
}
}
public static String colorToHex(Color c) {
if (c != null) {
return String.format((Locale) null, "#%02X%02X%02X",
Math.round(c.getRed() * 255),
Math.round(c.getGreen() * 255),
Math.round(c.getBlue() * 255));
} else {
return null;
}
}
private static final @NotNull Function<Node, ObservableBooleanValue> treeVisiblePropertyGetter = initTreeVisiblePropertyGetter();
private static @NotNull Function<Node, ObservableBooleanValue> initTreeVisiblePropertyGetter() {
MethodHandles.Lookup lookup;
try {
lookup = MethodHandles.privateLookupIn(Node.class, MethodHandles.lookup());
} catch (IllegalAccessException e) {
LOG.warning("Failed to get private lookup for Node", e);
return JFXNodeUtils::defaultTreeVisibleProperty;
}
try {
Method treeVisiblePropertyMethod = Node.class.getDeclaredMethod("treeVisibleProperty");
if (!ObservableBooleanValue.class.isAssignableFrom(treeVisiblePropertyMethod.getReturnType())) {
LOG.warning("Node.treeVisibleProperty() does not return ObservableBooleanValue: " + treeVisiblePropertyMethod.getReturnType());
return JFXNodeUtils::defaultTreeVisibleProperty;
}
MethodHandle handle = lookup.unreflect(treeVisiblePropertyMethod)
.asType(MethodType.methodType(ObservableBooleanValue.class, Node.class));
return item -> {
try {
return (ObservableBooleanValue) handle.invokeExact((Node) item);
} catch (RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new AssertionError("Unreachable", e);
}
};
} catch (Exception e) {
LOG.warning("Failed to get method handle for Node.treeVisibleProperty()", e);
return JFXNodeUtils::defaultTreeVisibleProperty;
}
}
/// If `Node.treeVisibleProperty()` does not exist, use `Node.visibleProperty()` as a fallback
private static @NotNull ObservableBooleanValue defaultTreeVisibleProperty(Node item) {
return item.visibleProperty();
}
public static @NotNull ObservableBooleanValue treeVisibleProperty(Node item) {
return treeVisiblePropertyGetter.apply(item);
}
public static boolean isTreeVisible(Node item) {
return treeVisibleProperty(item).getValue();
}
public static boolean isTreeShowing(Node node) {
if (node == null)
return false;
Scene scene = node.getScene();
if (scene == null)
return false;
Window window = scene.getWindow();
if (window == null || !window.isShowing())
return false;
return isTreeVisible(node);
}
private JFXNodeUtils() {
}
}
@@ -1,143 +0,0 @@
/*
* Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.jfoenix.utils;
import javafx.beans.property.ReadOnlyBooleanPropertyBase;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableBooleanValue;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.stage.Window;
/**
* Used to observe changes in tree showing status for a {@link Node}. For a Node's tree to be showing
* it must be visible, its ancestors must be visible, the node must be part of a {@link Scene} and
* the scene must have a {@link Window} which is currently showing.<p>
*
* This class provides the exact same functionality as {@link JFXNodeUtils#isTreeShowing(Node)} in
* an observable form.
*/
public class TreeShowingProperty extends ReadOnlyBooleanPropertyBase {
private final ChangeListener<Boolean> windowShowingChangedListener = (obs, old, current) -> updateTreeShowing();
private final ChangeListener<Window> sceneWindowChangedListener = (obs, old, current) -> windowChanged(old, current);
private final ChangeListener<Scene> nodeSceneChangedListener = (obs, old, current) -> sceneChanged(old, current);
private final Node node;
private final ObservableBooleanValue treeVisibleProperty;
private boolean valid;
private boolean treeShowing;
/**
* Constructs a new instance.
*
* @param node a {@link Node} for which the tree showing status should be observed, cannot be null
*/
public TreeShowingProperty(Node node) {
this.node = node;
this.treeVisibleProperty = JFXNodeUtils.treeVisibleProperty(node);
this.node.sceneProperty().addListener(nodeSceneChangedListener);
this.treeVisibleProperty.addListener(windowShowingChangedListener);
sceneChanged(null, node.getScene());
}
@Override
public Object getBean() {
return node;
}
@Override
public String getName() {
return "treeShowing";
}
/**
* Cleans up any listeners that this class may have registered on the {@link Node}
* that was supplied at construction.
*/
public void dispose() {
node.sceneProperty().removeListener(nodeSceneChangedListener);
if (treeVisibleProperty != null)
treeVisibleProperty.removeListener(windowShowingChangedListener);
valid = false; // prevents unregistration from triggering an invalidation notification
sceneChanged(node.getScene(), null);
}
protected void invalidate() {
if (valid) {
valid = false;
fireValueChangedEvent();
}
}
@Override
public boolean get() {
if (!valid) {
updateTreeShowing();
valid = true;
}
return treeShowing;
}
private void sceneChanged(Scene oldScene, Scene newScene) {
if (oldScene != null) {
oldScene.windowProperty().removeListener(sceneWindowChangedListener);
}
if (newScene != null) {
newScene.windowProperty().addListener(sceneWindowChangedListener);
}
windowChanged(
oldScene == null ? null : oldScene.getWindow(),
newScene == null ? null : newScene.getWindow()
);
}
private void windowChanged(Window oldWindow, Window newWindow) {
if (oldWindow != null) {
oldWindow.showingProperty().removeListener(windowShowingChangedListener);
}
if (newWindow != null) {
newWindow.showingProperty().addListener(windowShowingChangedListener);
}
updateTreeShowing();
}
private void updateTreeShowing() {
boolean newValue = JFXNodeUtils.isTreeShowing(node);
if (newValue != treeShowing) {
treeShowing = newValue;
invalidate();
}
}
}
@@ -1,283 +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;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
import org.jackhuang.hmcl.util.SwingUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import javax.swing.JOptionPane;
import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CancellationException;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class EntryPoint {
private EntryPoint() {
}
public static void main(String[] args) {
System.getProperties().putIfAbsent("java.net.useSystemProxies", "true");
System.getProperties().putIfAbsent("javafx.autoproxy.disable", "true");
System.getProperties().putIfAbsent("http.agent", "HMCL/" + Metadata.VERSION);
createHMCLDirectories();
LOG.start(Metadata.HMCL_CURRENT_DIRECTORY.resolve("logs"));
checkWine();
setupJavaFXVMOptions();
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
System.getProperties().putIfAbsent("apple.awt.application.appearance", "system");
if (!isInsideMacAppBundle())
initIcon();
}
checkJavaFX();
verifyJavaFX();
addEnableNativeAccess();
enableUnsafeMemoryAccess();
Launcher.main(args);
}
public static void exit(int exitCode) {
FileSaver.shutdown();
LOG.shutdown();
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 {
Files.createDirectories(Metadata.HMCL_CURRENT_DIRECTORY);
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
try {
Files.setAttribute(Metadata.HMCL_CURRENT_DIRECTORY, "dos:hidden", true);
} catch (IOException e) {
LOG.warning("Failed to set hidden attribute of " + Metadata.HMCL_CURRENT_DIRECTORY, e);
}
}
} 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));
}
}
if (!Files.isDirectory(Metadata.HMCL_GLOBAL_DIRECTORY)) {
try {
Files.createDirectories(Metadata.HMCL_GLOBAL_DIRECTORY);
} catch (IOException e) {
LOG.warning("Failed to create HMCL global directory " + Metadata.HMCL_GLOBAL_DIRECTORY, e);
}
}
}
private static boolean isInsideMacAppBundle() {
Path thisJar = JarUtils.thisJarPath();
if (thisJar == null)
return false;
for (Path current = thisJar.getParent();
current != null && current.getParent() != null;
current = current.getParent()
) {
if ("Contents".equals(FileUtils.getName(current))
&& FileUtils.getName(current.getParent()).endsWith(".app")
&& Files.exists(current.resolve("Info.plist"))
) {
return true;
}
}
return false;
}
private static void initIcon() {
try {
if (java.awt.Taskbar.isTaskbarSupported()) {
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 checkJavaFX() {
try {
SelfDependencyPatcher.patch();
} catch (SelfDependencyPatcher.PatchException e) {
LOG.error("Unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.missing"));
} catch (CancellationException e) {
LOG.error("User cancels downloading JavaFX", e);
exit(0);
}
}
/**
* Check if JavaFX exists but is incomplete
*/
private static void verifyJavaFX() {
try {
Class.forName("javafx.beans.binding.Binding"); // javafx.base
Class.forName("javafx.stage.Stage"); // javafx.graphics
Class.forName("javafx.scene.control.Skin"); // javafx.controls
} catch (Exception e) {
LOG.warning("JavaFX is incomplete or not found", e);
showErrorAndExit(i18n("fatal.javafx.incomplete"));
}
}
private static void checkWine() {
if (OperatingSystem.isRunningUnderWine()) {
SwingUtils.initLookAndFeel();
LOG.warning("HMCL is running under Wine or its distributions!");
int result = JOptionPane.showOptionDialog(null, i18n("fatal.wine_warning"), i18n("message.warning"), JOptionPane.OK_CANCEL_OPTION,
JOptionPane.WARNING_MESSAGE, null, null, null);
if (result == JOptionPane.CANCEL_OPTION || result == JOptionPane.CLOSED_OPTION) {
exit(1);
}
}
}
private static void addEnableNativeAccess() {
if (JavaRuntime.CURRENT_VERSION > 21) {
try {
// 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) {
LOG.error("Failed to add enable native access for JavaFX", e);
showErrorAndExit(i18n("fatal.javafx.incomplete"));
}
}
}
private static void enableUnsafeMemoryAccess() {
// https://openjdk.org/jeps/498
if (JavaRuntime.CURRENT_VERSION == 24 || JavaRuntime.CURRENT_VERSION == 25) {
try {
Class<?> clazz = Class.forName("sun.misc.Unsafe");
boolean ignored = (boolean) MethodHandles.privateLookupIn(clazz, MethodHandles.lookup())
.findStatic(clazz, "trySetMemoryAccessWarned", MethodType.methodType(boolean.class))
.invokeExact();
} catch (Throwable e) {
LOG.warning("Failed to enable unsafe memory access", e);
}
}
}
/**
* Indicates that a fatal error has occurred, and that the application cannot start.
*/
private static void showErrorAndExit(String message) {
SwingUtils.showErrorDialog(message);
exit(1);
}
}
@@ -17,34 +17,31 @@
*/
package org.jackhuang.hmcl;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
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.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.input.Clipboard;
import javafx.scene.input.DataFormat;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.SambaException;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.CrashReporter;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.NativeUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemInfo;
import java.io.File;
import java.io.IOException;
@@ -55,17 +52,14 @@ 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;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Launcher extends Application {
public static final CookieManager COOKIE_MANAGER = new CookieManager();
@@ -77,40 +71,30 @@ public final class Launcher extends Application {
CookieHandler.setDefault(COOKIE_MANAGER);
LOG.info("JavaFX Version: " + System.getProperty("javafx.runtime.version"));
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 {
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);
}
try {
try {
ConfigHolder.init();
} catch (SambaException e) {
showAlert(AlertType.WARNING, i18n("fatal.samba"));
} catch (SambaException ignored) {
Main.showWarningAndContinue(i18n("fatal.samba"));
} catch (IOException e) {
LOG.error("Failed to load config", e);
checkConfigInTempDir();
checkConfigOwner();
showAlert(AlertType.ERROR, i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
EntryPoint.exit(1);
Main.showErrorAndExit(i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
}
// https://lapcatsoftware.com/articles/app-translocation.html
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
&& ConfigHolder.isNewlyCreated()
&& System.getProperty("user.dir").startsWith("/private/var/folders/")) {
if (!confirmWithCountdown(AlertType.WARNING, i18n("fatal.mac_app_translocation"), 5))
if (showAlert(AlertType.WARNING, i18n("fatal.mac_app_translocation"), ButtonType.YES, ButtonType.NO) == ButtonType.NO)
return;
} else {
checkConfigInTempDir();
@@ -126,7 +110,7 @@ public final class Launcher extends Application {
}
if (Metadata.HMCL_CURRENT_DIRECTORY.toString().indexOf('=') >= 0) {
showAlert(AlertType.WARNING, i18n("fatal.illegal_char"));
Main.showWarningAndContinue(i18n("fatal.illegal_char"));
}
// runLater to ensure ConfigHolder.init() finished initialization
@@ -136,9 +120,6 @@ public final class Launcher extends Application {
Platform.setImplicitExit(false);
Controllers.initialize(primaryStage);
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
Themes.applyNativeDarkMode(primaryStage);
UpdateChecker.init();
primaryStage.show();
@@ -148,63 +129,10 @@ 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);
}
private static boolean confirmWithCountdown(Alert.AlertType alertType, String contentText, int seconds) {
Alert alert = new Alert(alertType, contentText, ButtonType.YES, ButtonType.NO);
Button okButton = (Button) alert.getDialogPane().lookupButton(ButtonType.YES);
okButton.setDisable(true);
KeyFrame[] keyFrames = new KeyFrame[seconds + 1];
for (int i = 0; i < seconds; i++) {
keyFrames[i] = new KeyFrame(Duration.seconds(i),
new KeyValue(okButton.textProperty(), i18n("button.ok.countdown", seconds - i)));
}
keyFrames[seconds] = new KeyFrame(Duration.seconds(seconds),
new KeyValue(okButton.textProperty(), i18n("button.ok")),
new KeyValue(okButton.disableProperty(), false));
Timeline timeline = new Timeline(keyFrames);
alert.setOnShown(e -> timeline.play());
alert.setOnCloseRequest(e -> timeline.stop());
return alert.showAndWait().orElse(null) == ButtonType.YES;
}
private static boolean isConfigInTempDir() {
String configPath = ConfigHolder.configLocation().toString();
@@ -244,8 +172,8 @@ public final class Launcher extends Application {
private static void checkConfigInTempDir() {
if (ConfigHolder.isNewlyCreated() && isConfigInTempDir()
&& !confirmWithCountdown(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), 5)) {
EntryPoint.exit(0);
&& showAlert(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), ButtonType.YES, ButtonType.NO) == ButtonType.NO) {
Main.exit(0);
}
}
@@ -276,7 +204,7 @@ public final class Launcher extends Application {
if (Files.exists(mcDir))
files.add(mcDir.toString());
String command = new CommandBuilder().addAll("sudo", "chown", "-R", userName).addAll(files).toString();
String command = new CommandBuilder().add("sudo", "chown", "-R", userName).addAll(files).toString();
ButtonType copyAndExit = new ButtonType(i18n("button.copy_and_exit"));
if (showAlert(AlertType.ERROR,
@@ -285,7 +213,7 @@ public final class Launcher extends Application {
Clipboard.getSystemClipboard()
.setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, command));
}
EntryPoint.exit(1);
Main.exit(1);
}
@Override
@@ -307,8 +235,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.getVersion()
: OperatingSystem.OS_RELEASE_PRETTY_NAME + " (" + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION.getVersion() + ')'));
? OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION
: OperatingSystem.OS_RELEASE_PRETTY_NAME + " (" + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + ')'));
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
LOG.info("Processor Identifier: " + System.getenv("PROCESSOR_IDENTIFIER"));
}
@@ -344,8 +272,6 @@ public final class Launcher extends Application {
LOG.info("XDG Current Desktop: " + System.getenv("XDG_CURRENT_DESKTOP"));
}
LOG.info("Zlib Compatible: " + ZlibUtils.IS_ZLIB_COMPATIBLE);
Lang.thread(SystemInfo::initialize, "Detection System Information", true);
launch(Launcher.class, args);
@@ -0,0 +1,266 @@
/*
* 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;
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.java.JavaRuntime;
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.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 {
private Main() {
}
public static void main(String[] args) {
System.getProperties().putIfAbsent("java.net.useSystemProxies", "true");
System.getProperties().putIfAbsent("javafx.autoproxy.disable", "true");
System.getProperties().putIfAbsent("http.agent", "HMCL/" + Metadata.VERSION);
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);
}
}
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)
initIcon();
checkJavaFX();
verifyJavaFX();
addEnableNativeAccess();
enableUnsafeMemoryAccess();
Launcher.main(args);
}
public static void exit(int exitCode) {
FileSaver.shutdown();
LOG.shutdown();
System.exit(exitCode);
}
private static void createHMCLDirectories() {
if (!Files.isDirectory(Metadata.HMCL_CURRENT_DIRECTORY)) {
try {
Files.createDirectories(Metadata.HMCL_CURRENT_DIRECTORY);
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
try {
Files.setAttribute(Metadata.HMCL_CURRENT_DIRECTORY, "dos:hidden", true);
} catch (IOException e) {
LOG.warning("Failed to set hidden attribute of " + Metadata.HMCL_CURRENT_DIRECTORY, e);
}
}
} catch (IOException e) {
e.printStackTrace(System.err);
showErrorAndExit(i18n("fatal.create_hmcl_current_directory_failure", Metadata.HMCL_CURRENT_DIRECTORY));
}
}
if (!Files.isDirectory(Metadata.HMCL_GLOBAL_DIRECTORY)) {
try {
Files.createDirectories(Metadata.HMCL_GLOBAL_DIRECTORY);
} catch (IOException e) {
LOG.warning("Failed to create HMCL global directory " + Metadata.HMCL_GLOBAL_DIRECTORY, e);
}
}
}
private static void initIcon() {
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(Main.class.getResource("/assets/img/icon-mac.png"));
AwtUtils.setAppleIcon(image);
}
private static void checkDirectoryPath() {
String currentDirectory = new File("").getAbsolutePath();
if (currentDirectory.contains("!")) {
// 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);
}
}
private static void checkJavaFX() {
try {
SelfDependencyPatcher.patch();
} catch (SelfDependencyPatcher.PatchException e) {
LOG.error("unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.missing"));
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
LOG.error("unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.incompatible"));
} catch (CancellationException e) {
LOG.error("User cancels downloading JavaFX", e);
exit(0);
}
}
/**
* Check if JavaFX exists but is incomplete
*/
private static void verifyJavaFX() {
try {
Class.forName("javafx.beans.binding.Binding"); // javafx.base
Class.forName("javafx.stage.Stage"); // javafx.graphics
Class.forName("javafx.scene.control.Skin"); // javafx.controls
} catch (Exception e) {
e.printStackTrace(System.err);
showErrorAndExit(i18n("fatal.javafx.incomplete"));
}
}
private static void addEnableNativeAccess() {
if (JavaRuntime.CURRENT_VERSION > 21) {
try {
ModuleHelper.addEnableNativeAccess(Class.forName("javafx.stage.Stage")); // javafx.graphics
} catch (ClassNotFoundException e) {
e.printStackTrace(System.err);
showErrorAndExit(i18n("fatal.javafx.incomplete"));
}
}
}
private static void enableUnsafeMemoryAccess() {
// https://openjdk.org/jeps/498
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);
} catch (Throwable e) {
e.printStackTrace(System.err);
}
}
}
/**
* 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) {
}
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);
}
}
}
@@ -24,6 +24,7 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.EnumSet;
/**
@@ -35,19 +36,18 @@ 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.getAttribute("hmcl.version", "@develop@"));
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 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";
public static final String HMCL_UPDATE_URL = System.getProperty("hmcl.update_source.override", PUBLISH_URL + "/api/update_link");
public static final String MANUAL_UPDATE_URL = "https://github.com/HMCL-dev/HMCL/releases";
public static final String DOCS_URL = "https://docs.hmcl.net";
public static final String CONTACT_URL = DOCS_URL + "/help.html";
@@ -55,41 +55,37 @@ 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.getAttribute("hmcl.version.type", "nightly");
public static final String GITHUB_SHA = JarUtils.getAttribute("hmcl.version.hash", null);
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 Path CURRENT_DIRECTORY = Path.of(System.getProperty("user.dir")).toAbsolutePath().normalize();
public static final Path CURRENT_DIRECTORY = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize();
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
public static final Path HMCL_GLOBAL_DIRECTORY;
public static final Path HMCL_CURRENT_DIRECTORY;
public static final Path DEPENDENCIES_DIRECTORY;
static {
String hmclHome = System.getProperty("hmcl.home", System.getenv("HMCL_USER_HOME"));
if (StringUtils.isBlank(hmclHome)) {
String hmclHome = System.getProperty("hmcl.home");
if (hmclHome == null) {
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
String xdgData = System.getenv("XDG_DATA_HOME");
if (StringUtils.isNotBlank(xdgData)) {
HMCL_GLOBAL_DIRECTORY = Path.of(xdgData, "hmcl").toAbsolutePath().normalize();
HMCL_GLOBAL_DIRECTORY = Paths.get(xdgData, "hmcl").toAbsolutePath().normalize();
} else {
HMCL_GLOBAL_DIRECTORY = Path.of(System.getProperty("user.home"), ".local", "share", "hmcl").toAbsolutePath().normalize();
HMCL_GLOBAL_DIRECTORY = Paths.get(System.getProperty("user.home"), ".local", "share", "hmcl").toAbsolutePath().normalize();
}
} else {
HMCL_GLOBAL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
}
} else {
HMCL_GLOBAL_DIRECTORY = Path.of(hmclHome).toAbsolutePath().normalize();
HMCL_GLOBAL_DIRECTORY = Paths.get(hmclHome).toAbsolutePath().normalize();
}
String hmclCurrentDir = System.getProperty("hmcl.dir", System.getenv("HMCL_LOCAL_HOME"));
HMCL_CURRENT_DIRECTORY = StringUtils.isNotBlank(hmclCurrentDir)
? Path.of(hmclCurrentDir).toAbsolutePath().normalize()
String hmclCurrentDir = System.getProperty("hmcl.dir");
HMCL_CURRENT_DIRECTORY = hmclCurrentDir != null
? Paths.get(hmclCurrentDir).toAbsolutePath().normalize()
: CURRENT_DIRECTORY.resolve(".hmcl");
String hmclDependencies = System.getProperty("hmcl.dependencies.dir", System.getenv("HMCL_DEPENDENCIES_DIR"));
DEPENDENCIES_DIRECTORY = StringUtils.isNotBlank(hmclDependencies)
? Path.of(hmclDependencies).toAbsolutePath().normalize()
: HMCL_CURRENT_DIRECTORY.resolve("dependencies");
DEPENDENCIES_DIRECTORY = HMCL_CURRENT_DIRECTORY.resolve("dependencies");
}
public static boolean isStable() {
@@ -121,6 +117,7 @@ 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(),
@@ -1,20 +1,3 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.countly;
import org.jackhuang.hmcl.Metadata;
@@ -59,7 +42,7 @@ public class CrashReport {
"\n Content: \n " +
stackTrace + "\n\n" +
"-- System Details --\n" +
" Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION.getVersion() + "\n" +
" Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + "\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,21 +21,15 @@ 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.NativePatcher;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.*;
import java.util.stream.Stream;
import java.util.Map;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
@@ -64,83 +58,61 @@ public final class HMCLGameLauncher extends DefaultLauncher {
}
private void generateOptionsTxt() {
if (config().isDisableAutoGameOptions())
File optionsFile = new File(repository.getRunDirectory(version.getId()), "options.txt");
File configFolder = new File(repository.getRunDirectory(version.getId()), "config");
if (optionsFile.exists()) {
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 (configFolder.isDirectory()) {
if (findFiles(configFolder, "options.txt")) {
return;
}
}
Locale locale = Locale.getDefault();
if (!I18n.isUseChinese()) {
return;
}
String lang;
/*
* 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
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 时自动切换为英文
*/
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(repository.getGameVersion(version));
if (gameVersion.compareTo("1.1") < 0)
return;
if (gameVersion.compareTo("1.1") < 0) {
lang = null;
} else if (gameVersion.compareTo("1.11") < 0) {
lang = "zh_CN";
} else {
lang = "zh_cn";
}
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);
if (lang != null) {
try {
FileUtils.writeText(optionsFile, String.format("lang:%s\n", lang));
} catch (IOException e) {
LOG.warning("Unable to generate options.txt", e);
}
}
}
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";
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;
}
case "en" -> {
if ("Qabs".equals(LocaleUtils.getScript(locale)) && gameVersion.compareTo("1.16") >= 0) {
yield "en_UD";
}
yield "";
}
default -> "";
};
}
return false;
}
@Override
@@ -150,64 +122,8 @@ public final class HMCLGameLauncher extends DefaultLauncher {
}
@Override
public void makeLaunchScript(Path scriptFile) throws IOException {
public void makeLaunchScript(File scriptFile) throws IOException {
generateOptionsTxt();
super.makeLaunchScript(scriptFile);
}
@Override
protected void appendJvmArgs(CommandBuilder result) {
super.appendJvmArgs(result);
if (config().getAllowAutoAgent()
&& !options.isNoGeneratedJVMArgs()
&& !options.isNoGeneratedOptimizingJVMArgs()
&& NativePatcher.needPatchMemoryUtil(version, options.getJava().getParsedVersion())) {
LOG.info("Attempting to patch game with lwjgl-unsafe-agent");
try {
result.add("-javaagent:" + extractLwjglUnsafeAgent());
} catch (Exception e) {
LOG.warning("Failed to extract lwjgl-unsafe-agent", e);
}
}
}
private Path extractLwjglUnsafeAgent() throws IOException {
String agentVersion = JarUtils.getAttribute("hmcl.lwjgl-unsafe-agent.version", null);
if (agentVersion == null) {
throw new IOException("Missing hmcl.lwjgl-unsafe-agent.version attribute");
}
Library library = new Library(new Artifact("org.glavo", "lwjgl-unsafe-agent", agentVersion));
String fileName = library.getArtifact().getFileName();
Path agentPath = repository.getLibraryFile(version, library).toAbsolutePath().normalize();
if (agentPath.toString().contains("=")) {
throw new IOException("Invalid library path: " + agentPath);
}
byte[] bytes;
try (InputStream input = DefaultLauncher.class.getResourceAsStream("/assets/" + fileName)) {
if (input == null) {
throw new IOException("/assets/" + fileName + " not found");
}
bytes = input.readAllBytes();
}
if (Files.isRegularFile(agentPath)) {
try {
if (Files.size(agentPath) == bytes.length) {
return agentPath;
}
} catch (IOException e) {
LOG.warning("Failed to check size of " + agentPath, e);
}
}
Files.createDirectories(agentPath.getParent());
FileUtils.saveSafely(agentPath, output -> output.write(bytes));
return agentPath;
}
}
@@ -25,30 +25,28 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.mod.ModAdviser;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemInfo;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.net.Proxy;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.time.Instant;
import java.util.*;
@@ -56,10 +54,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.Pair.pair;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
public final class HMCLGameRepository extends DefaultGameRepository {
public class HMCLGameRepository extends DefaultGameRepository {
private final Profile profile;
// local version settings
@@ -68,7 +66,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
public final EventManager<Event> onVersionIconChanged = new EventManager<>();
public HMCLGameRepository(Profile profile, Path baseDirectory) {
public HMCLGameRepository(Profile profile, File baseDirectory) {
super(baseDirectory);
this.profile = profile;
}
@@ -87,20 +85,18 @@ public final class HMCLGameRepository extends DefaultGameRepository {
}
@Override
public Path getRunDirectory(String id) {
public File getRunDirectory(String id) {
switch (getGameDirectoryType(id)) {
case VERSION_FOLDER:
return getVersionRoot(id);
case ROOT_FOLDER:
return super.getRunDirectory(id);
case CUSTOM:
try {
return Path.of(getVersionSetting(id).getGameDir());
} catch (InvalidPathException ignored) {
return getVersionRoot(id);
}
File dir = new File(getVersionSetting(id).getGameDir());
if (!FileUtils.isValidPath(dir)) return getVersionRoot(id);
return dir;
default:
throw new AssertionError("Unreachable");
throw new Error();
}
}
@@ -123,24 +119,25 @@ public final class HMCLGameRepository extends DefaultGameRepository {
});
try {
Path file = getBaseDirectory().resolve("launcher_profiles.json");
if (!Files.exists(file) && !versions.isEmpty()) {
Files.createDirectories(file.getParent());
Files.writeString(file, PROFILE);
}
File file = new File(getBaseDirectory(), "launcher_profiles.json");
if (!file.exists() && !versions.isEmpty())
FileUtils.writeText(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(Path newDirectory) {
public void changeDirectory(File newDirectory) {
setBaseDirectory(newDirectory);
refreshVersionsAsync().start();
}
private void clean(Path directory) throws IOException {
FileUtils.deleteDirectory(directory.resolve("crash-reports"));
FileUtils.deleteDirectory(directory.resolve("logs"));
private void clean(File directory) throws IOException {
FileUtils.deleteDirectory(new File(directory, "crash-reports"));
FileUtils.deleteDirectory(new File(directory, "logs"));
}
public void clean(String id) throws IOException {
@@ -149,8 +146,8 @@ public final class HMCLGameRepository extends DefaultGameRepository {
}
public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException {
Path srcDir = getVersionRoot(srcId);
Path dstDir = getVersionRoot(dstId);
Path srcDir = getVersionRoot(srcId).toPath();
Path dstDir = getVersionRoot(dstId).toPath();
Version fromVersion = getVersion(srcId);
@@ -161,8 +158,6 @@ public final 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");
@@ -175,7 +170,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
}
Files.copy(fromJson, toJson);
JsonUtils.writeToJsonFile(toJson, fromVersion.setId(dstId).setJar(dstId));
FileUtils.writeText(toJson.toFile(), JsonUtils.GSON.toJson(fromVersion.setId(dstId)));
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
@@ -184,22 +179,22 @@ public final class HMCLGameRepository extends DefaultGameRepository {
VersionSetting newVersionSetting = initLocalVersionSetting(dstId, oldVersionSetting);
saveVersionSetting(dstId);
Path srcGameDir = getRunDirectory(srcId);
Path dstGameDir = getRunDirectory(dstId);
File srcGameDir = getRunDirectory(srcId);
File dstGameDir = getRunDirectory(dstId);
if (originalGameDirType != GameDirectoryType.VERSION_FOLDER)
FileUtils.copyDirectory(srcGameDir, dstGameDir, path -> Modpack.acceptFile(path, blackList, null));
FileUtils.copyDirectory(srcGameDir.toPath(), dstGameDir.toPath(), path -> Modpack.acceptFile(path, blackList, null));
}
private Path getLocalVersionSettingFile(String id) {
return getVersionRoot(id).resolve("hmclversion.cfg");
private File getLocalVersionSettingFile(String id) {
return new File(getVersionRoot(id), "hmclversion.cfg");
}
private void loadLocalVersionSetting(String id) {
Path file = getLocalVersionSettingFile(id);
if (Files.exists(file))
File file = getLocalVersionSettingFile(id);
if (file.exists())
try {
VersionSetting versionSetting = JsonUtils.fromJsonFile(file, VersionSetting.class);
VersionSetting versionSetting = GSON.fromJson(FileUtils.readText(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.
@@ -262,12 +257,12 @@ public final class HMCLGameRepository extends DefaultGameRepository {
return vs;
}
public Optional<Path> getVersionIconFile(String id) {
Path root = getVersionRoot(id);
public Optional<File> getVersionIconFile(String id) {
File root = getVersionRoot(id);
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
Path file = root.resolve("icon." + extension);
if (Files.exists(file)) {
File file = new File(root, "icon." + extension);
if (file.exists()) {
return Optional.of(file);
}
}
@@ -275,7 +270,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
return Optional.empty();
}
public void setVersionIconFile(String id, Path iconFile) throws IOException {
public void setVersionIconFile(String id, File iconFile) throws IOException {
String ext = FileUtils.getExtension(iconFile).toLowerCase(Locale.ROOT);
if (!FXUtils.IMAGE_EXTENSIONS.contains(ext)) {
throw new IllegalArgumentException("Unsupported icon file: " + ext);
@@ -283,18 +278,13 @@ public final class HMCLGameRepository extends DefaultGameRepository {
deleteIconFile(id);
FileUtils.copyFile(iconFile, getVersionRoot(id).resolve("icon." + ext));
FileUtils.copyFile(iconFile, new File(getVersionRoot(id), "icon." + ext));
}
public void deleteIconFile(String id) {
Path root = getVersionRoot(id);
File root = getVersionRoot(id);
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
Path file = root.resolve("icon." + extension);
try {
Files.deleteIfExists(file);
} catch (IOException e) {
LOG.warning("Failed to delete icon file: " + file, e);
}
new File(root, "icon." + extension).delete();
}
}
@@ -307,10 +297,10 @@ public final class HMCLGameRepository extends DefaultGameRepository {
if (iconType == VersionIconType.DEFAULT) {
Version version = getVersion(id).resolve(this);
Optional<Path> iconFile = getVersionIconFile(id);
Optional<File> iconFile = getVersionIconFile(id);
if (iconFile.isPresent()) {
try {
return FXUtils.loadImage(iconFile.get(), 64, 64, true, true);
return FXUtils.loadImage(iconFile.get().toPath());
} catch (Exception e) {
LOG.warning("Failed to load version icon of " + id, e);
}
@@ -320,34 +310,21 @@ public final class HMCLGameRepository extends DefaultGameRepository {
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version, null);
if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FABRIC))
return VersionIconType.FABRIC.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.QUILT))
return VersionIconType.QUILT.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LEGACY_FABRIC))
return VersionIconType.LEGACY_FABRIC.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE))
return VersionIconType.NEO_FORGE.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FORGE))
return VersionIconType.FORGE.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.CLEANROOM))
return VersionIconType.CLEANROOM.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LITELOADER))
return VersionIconType.CHICKEN.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE))
return VersionIconType.NEO_FORGE.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.QUILT))
return VersionIconType.QUILT.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.OPTIFINE))
return VersionIconType.OPTIFINE.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LITELOADER))
return VersionIconType.CHICKEN.getIcon();
else
return VersionIconType.FURNACE.getIcon();
}
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();
return VersionIconType.DEFAULT.getIcon();
} else {
return iconType.getIcon();
}
@@ -356,7 +333,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
public void saveVersionSetting(String id) {
if (!localVersionSettings.containsKey(id))
return;
Path file = getLocalVersionSettingFile(id).toAbsolutePath().normalize();
Path file = getLocalVersionSettingFile(id).toPath().toAbsolutePath().normalize();
try {
Files.createDirectories(file.getParent());
} catch (IOException e) {
@@ -390,7 +367,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
vs.setUsesGlobal(true);
}
public LaunchOptions.Builder getLaunchOptions(String version, JavaRuntime javaVersion, Path gameDir, List<String> javaAgents, List<String> javaArguments, boolean makeLaunchScript) {
public LaunchOptions getLaunchOptions(String version, JavaRuntime javaVersion, File gameDir, List<String> javaAgents, List<String> javaArguments, boolean makeLaunchScript) {
VersionSetting vs = getVersionSetting(version);
LaunchOptions.Builder builder = new LaunchOptions.Builder()
@@ -401,7 +378,7 @@ public final 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()
@@ -421,49 +398,53 @@ public final class HMCLGameRepository extends DefaultGameRepository {
.setWidth(vs.getWidth())
.setHeight(vs.getHeight())
.setFullscreen(vs.isFullscreen())
.setServerIp(vs.getServerIp())
.setWrapper(vs.getWrapper())
.setProxyOption(getProxyOption())
.setPreLaunchCommand(vs.getPreLaunchCommand())
.setPostExitCommand(vs.getPostExitCommand())
.setNoGeneratedJVMArgs(vs.isNoJVMArgs())
.setNoGeneratedOptimizingJVMArgs(vs.isNoOptimizingJVMArgs())
.setNativesDirType(vs.getNativesDirType())
.setNativesDir(vs.getNativesDir())
.setProcessPriority(vs.getProcessPriority())
.setGraphicsBackend(vs.getGraphicsBackend())
.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());
builder.setProxyPort(config().getProxyPort());
if (config().hasProxyAuth()) {
builder.setProxyUser(config().getProxyUser());
builder.setProxyPass(config().getProxyPass());
}
}
Path json = getModpackConfiguration(version);
if (Files.exists(json)) {
File json = getModpackConfiguration(version);
if (json.exists()) {
try {
String jsonText = Files.readString(json);
String jsonText = FileUtils.readText(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) {
LOG.warning("Failed to parse modpack configuration file " + json, e);
e.printStackTrace();
}
}
if (vs.isAutoMemory() && builder.getJavaArguments().stream().anyMatch(it -> it.startsWith("-Xmx")))
builder.setMaxMemory(null);
return builder;
return builder.create();
}
@Override
public Path getModpackConfiguration(String version) {
return getVersionRoot(version).resolve("modpack.cfg");
public File getModpackConfiguration(String version) {
return new File(getVersionRoot(version), "modpack.cfg");
}
public void markVersionAsModpack(String id) {
@@ -476,24 +457,16 @@ public final class HMCLGameRepository extends DefaultGameRepository {
public void markVersionLaunchedAbnormally(String id) {
try {
Files.createFile(getVersionRoot(id).resolve(".abnormal"));
Files.createFile(getVersionRoot(id).toPath().resolve(".abnormal"));
} catch (IOException ignored) {
}
}
public boolean unmarkVersionLaunchedAbnormally(String id) {
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;
}
File file = new File(getVersionRoot(id), ".abnormal");
boolean result = file.isFile();
file.delete();
return result;
}
private static final Gson GSON = new GsonBuilder()
@@ -515,7 +488,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
FORBIDDEN_VERSION_IDS.contains(id.toLowerCase(Locale.ROOT)))
return false;
return FileUtils.isNameValidForJar(id);
return OperatingSystem.isNameValid(id);
}
/**
@@ -552,39 +525,4 @@ public final class HMCLGameRepository extends DefaultGameRepository {
return minimum;
}
}
public static ProxyOption getProxyOption() {
if (!config().hasProxy() || config().getProxyType() == null) {
return ProxyOption.Default.INSTANCE;
}
return switch (config().getProxyType()) {
case DIRECT -> ProxyOption.Direct.INSTANCE;
case HTTP, SOCKS -> {
String proxyHost = config().getProxyHost();
int proxyPort = config().getProxyPort();
if (StringUtils.isBlank(proxyHost) || proxyPort < 0 || proxyPort > 0xFFFF) {
yield ProxyOption.Default.INSTANCE;
}
String proxyUser = config().getProxyUser();
String proxyPass = config().getProxyPass();
if (StringUtils.isBlank(proxyUser)) {
proxyUser = null;
proxyPass = null;
} else if (proxyPass == null) {
proxyPass = "";
}
if (config().getProxyType() == Proxy.Type.HTTP) {
yield new ProxyOption.Http(proxyHost, proxyPort, proxyUser, proxyPass);
} else {
yield new ProxyOption.Socks(proxyHost, proxyPort, proxyUser, proxyPass);
}
}
default -> ProxyOption.Default.INSTANCE;
};
}
}
@@ -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 Path zipFile;
private final File 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, Path zipFile, Modpack modpack, String name) {
public HMCLModpackInstallTask(Profile profile, File zipFile, Modpack modpack, String name) {
dependency = profile.getDependency();
repository = profile.getRepository();
this.zipFile = zipFile;
this.name = name;
this.modpack = modpack;
Path run = repository.getRunDirectory(name);
Path json = repository.getModpackConfiguration(name);
if (repository.hasVersion(name) && Files.notExists(json))
File run = repository.getRunDirectory(name);
File json = repository.getModpackConfiguration(name);
if (repository.hasVersion(name) && !json.exists())
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 (Files.exists(json)) {
config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(Modpack.class));
if (json.exists()) {
config = JsonUtils.GSON.fromJson(FileUtils.readText(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,6 +30,7 @@ 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;
@@ -48,14 +49,15 @@ public final class HMCLModpackProvider implements ModpackProvider {
}
@Override
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, File zipFile, Modpack modpack) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof HMCLModpackManifest))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
if (!(dependencyManager.getGameRepository() instanceof HMCLGameRepository repository)) {
if (!(dependencyManager.getGameRepository() instanceof HMCLGameRepository)) {
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));
@@ -77,9 +79,9 @@ public final class HMCLModpackProvider implements ModpackProvider {
return manifest;
}
private final static class HMCLModpack extends Modpack {
private static class HMCLModpack extends Modpack {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl) {
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
return new HMCLModpackInstallTask(((HMCLGameRepository) dependencyManager.getGameRepository()).getProfile(), zipFile, this, name);
}
}
@@ -22,7 +22,6 @@ 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;
@@ -50,42 +49,37 @@ 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.URI;
import java.net.URL;
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;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.Lang.resolveException;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.platform.Platform.SYSTEM_PLATFORM;
import static org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java;
public final class LauncherHelper {
private static final String LWJGL_3_4_1_TIP = "lwjgl3.4.1-ffm";
private final Profile profile;
private Account account;
private final String selectedVersion;
private Path scriptFile;
private File 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);
@@ -116,14 +110,6 @@ 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();
@@ -133,8 +119,9 @@ public final class LauncherHelper {
launch0();
}
public void makeLaunchScript(Path scriptFile) {
public void makeLaunchScript(File scriptFile) {
this.scriptFile = Objects.requireNonNull(scriptFile);
launch();
}
@@ -172,69 +159,41 @@ public final class LauncherHelper {
}
}),
Task.composeAsync(() -> {
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS
|| !(setting.getRenderer() instanceof Renderer.Driver renderer)
|| renderer.mesaDriverName() == null)
return null;
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 (lib == null)
return null;
File file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
if (file.getAbsolutePath().indexOf('=') >= 0) {
LOG.warning("Invalid character '=' in the libraries directory path, unable to attach software renderer loader");
return null;
}
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.SYSTEM_VERSION);
if (lib == null)
return null;
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.mesaDriverName();
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
return new LibraryDownloadTask(dependencyManager, file, lib)
.thenRunAsync(() -> javaAgents.add(agent));
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
return new LibraryDownloadTask(dependencyManager, file, lib)
.thenRunAsync(() -> javaAgents.add(agent));
} else {
javaAgents.add(agent);
return null;
}
} else {
javaAgents.add(agent);
return null;
}
})
);
}).withStage("launch.state.dependencies")
.thenComposeAsync(() -> gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version.get())).orElse(null))
.thenComposeAsync(() -> {
if (config().getAllowAutoAgent()
|| setting.isNoJVMArgs()
|| setting.isNoOptimizingJVMArgs()
|| Boolean.TRUE.equals(config().getShownTips().get(LWJGL_3_4_1_TIP))
|| !NativePatcher.needPatchMemoryUtil(version.get(), javaVersionRef.get().getParsedVersion())) {
return Task.completed(null);
} else {
CompletableFuture<Void> future = new CompletableFuture<>();
runInFX(() -> {
Controllers.confirm(i18n("launch.advice.lwjgl_3_4_1"), i18n("launch.advice.lwjgl_3_4_1.title"), MessageType.QUESTION, () -> {
config().getShownTips().put(LWJGL_3_4_1_TIP, true);
config().setAllowAutoAgent(true);
future.complete(null);
}, () -> {
config().getShownTips().put(LWJGL_3_4_1_TIP, true);
future.complete(null);
});
});
return Task.fromCompletableFuture(future);
}
})
.thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in"))
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
LaunchOptions.Builder launchOptionsBuilder = repository.getLaunchOptions(
LaunchOptions launchOptions = repository.getLaunchOptions(
selectedVersion, javaVersionRef.get(), profile.getGameDir(), javaAgents, javaArguments, scriptFile != null);
if (disableOfflineSkin) {
launchOptionsBuilder.setDaemon(false);
}
if (quickPlayOption != null) {
launchOptionsBuilder.setQuickPlayOption(quickPlayOption);
}
LaunchOptions launchOptions = launchOptionsBuilder.create();
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModsDirectory(selectedVersion), 10));
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModManager(selectedVersion).getModsDirectory(), 10));
return new HMCLGameLauncher(
repository,
@@ -267,18 +226,18 @@ public final class LauncherHelper {
} else {
runLater(() -> {
launchingStepsPane.fireEvent(new DialogCloseEvent());
Controllers.dialog(i18n("version.launch_script.success", FileUtils.getAbsolutePath(scriptFile)));
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
});
}
}).withFakeProgress(
i18n("message.doing"),
() -> launchingLatch.getCount() == 0, 6.95
).withStage("launch.state.waiting_launching"))
.withStagesHints(
new Task.StagesHint("launch.state.java"),
new Task.StagesHint("launch.state.dependencies", List.of("hmcl.install.assets", "hmcl.install.libraries", "hmcl.modpack.download")),
new Task.StagesHint("launch.state.logging_in"),
new Task.StagesHint("launch.state.waiting_launching"))
.withStagesHint(Lang.immutableListOf(
"launch.state.java",
"launch.state.dependencies",
"launch.state.logging_in",
"launch.state.waiting_launching"))
.executor();
launchingStepsPane.setExecutor(executor, false);
executor.addTaskListener(new TaskListener() {
@@ -310,27 +269,27 @@ public final class LauncherHelper {
if (ex.getCause() instanceof ResponseCodeException) {
ResponseCodeException rce = (ResponseCodeException) ex.getCause();
int responseCode = rce.getResponseCode();
String uri = rce.getUri();
URL url = rce.getUrl();
if (responseCode == 404)
message += i18n("download.code.404", uri);
message += i18n("download.code.404", url);
else
message += i18n("download.failed", uri, responseCode);
message += i18n("download.failed", url, responseCode);
} else {
message += StringUtils.getStackTrace(ex.getCause());
}
} else if (ex instanceof DownloadException) {
URI uri = ((DownloadException) ex).getUri();
URL url = ((DownloadException) ex).getUrl();
if (ex.getCause() instanceof SocketTimeoutException) {
message = i18n("install.failed.downloading.timeout", uri);
message = i18n("install.failed.downloading.timeout", url);
} else if (ex.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) ex.getCause();
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
message = i18n("download.code." + responseCodeException.getResponseCode(), uri);
message = i18n("download.code." + responseCodeException.getResponseCode(), url);
} else {
message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
}
} else {
message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
}
} else if (ex instanceof GameAssetIndexDownloadTask.GameAssetIndexMalformedException) {
message = i18n("assets.index.malformed");
@@ -341,22 +300,22 @@ public final class LauncherHelper {
} else if (ex instanceof ResponseCodeException) {
ResponseCodeException rce = (ResponseCodeException) ex;
int responseCode = rce.getResponseCode();
String uri = rce.getUri();
URL url = rce.getUrl();
if (responseCode == 404)
message = i18n("download.code.404", uri);
message = i18n("download.code.404", url);
else
message = i18n("download.failed", uri, responseCode);
message = i18n("download.failed", url, responseCode);
} else if (ex instanceof CommandTooLongException) {
message = i18n("launch.failed.command_too_long");
} else if (ex instanceof ExecutionPolicyLimitException) {
Controllers.prompt(new PromptDialogPane.Builder(i18n("launch.failed.execution_policy"),
(result, handler) -> {
(result, resolve, reject) -> {
if (CommandBuilder.setExecutionPolicy()) {
LOG.info("Set the ExecutionPolicy for the scope 'CurrentUser' to 'RemoteSigned'");
handler.resolve();
resolve.run();
} else {
LOG.warning("Failed to set ExecutionPolicy");
handler.reject(i18n("launch.failed.execution_policy.failed_to_set"));
reject.accept(i18n("launch.failed.execution_policy.failed_to_set"));
}
})
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("launch.failed.execution_policy.hint")))
@@ -412,18 +371,9 @@ public final class LauncherHelper {
if (setting.getJavaVersionType() == JavaVersionType.VERSION) {
try {
int targetJavaVersionMajor = Integer.parseInt(setting.getJavaVersion());
GameJavaVersion minimumJavaVersion = null;
if (gameVersion.compareTo("1.12.2") == 0) {
Optional<String> cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM);
if (cleanroomVersion.isPresent()) {
minimumJavaVersion = GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion.get());
}
}
GameJavaVersion minimumJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
if (minimumJavaVersion == null)
minimumJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
if (minimumJavaVersion != null && targetJavaVersionMajor < minimumJavaVersion.majorVersion()) {
if (minimumJavaVersion != null && targetJavaVersionMajor < minimumJavaVersion.getMajorVersion()) {
Controllers.dialog(
i18n("launch.failed.java_version_too_low"),
i18n("message.error"),
@@ -436,17 +386,8 @@ public final class LauncherHelper {
targetJavaVersion = GameJavaVersion.get(targetJavaVersionMajor);
} catch (NumberFormatException ignored) {
}
} else {
if (gameVersion.compareTo("1.12.2") == 0) {
Optional<String> cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM);
if (cleanroomVersion.isPresent()) {
targetJavaVersion = GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion.get());
}
}
if (targetJavaVersion == null)
targetJavaVersion = version.getJavaVersion();
}
} else
targetJavaVersion = version.getJavaVersion();
if (targetJavaVersion != null && supportedVersions.contains(targetJavaVersion)) {
downloadJava(targetJavaVersion, profile)
@@ -476,7 +417,7 @@ public final class LauncherHelper {
if (java != null) {
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) {
if (!constraint.checkJava(gameVersion, version, java, analyzer)) {
if (!constraint.checkJava(gameVersion, version, java)) {
if (constraint.isMandatory()) {
violatedMandatoryConstraints.add(constraint);
} else {
@@ -511,14 +452,7 @@ public final class LauncherHelper {
return result;
} else {
GameJavaVersion gameJavaVersion;
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.CLEANROOM)) {
String cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM)
.orElse("");
gameJavaVersion = !cleanroomVersion.isEmpty()
? GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion)
: GameJavaVersion.JAVA_21;
} else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
gameJavaVersion = version.getJavaVersion();
else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA))
gameJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
@@ -608,14 +542,6 @@ public final class LauncherHelper {
case MODDED_JAVA_21:
suggestions.add(i18n("launch.advice.modded_java", 21, gameVersion));
break;
case CLEANROOM: {
String cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM).orElse("");
if (!cleanroomVersion.isEmpty())
suggestions.add(i18n("launch.advice.cleanroom", GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion).majorVersion(), cleanroomVersion));
else
suggestions.add(i18n("launch.advice.cleanroom", 21, ""));
break;
}
case VANILLA_JAVA_8_51:
suggestions.add(i18n("launch.advice.java8_51_1_13"));
break;
@@ -624,7 +550,7 @@ public final class LauncherHelper {
break;
case VANILLA_X86:
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER
&& Platform.isSupportedTranslationX86_64()) {
&& isCompatibleWithX86Java()) {
suggestions.add(i18n("launch.advice.vanilla_x86.translation"));
}
break;
@@ -687,7 +613,7 @@ public final class LauncherHelper {
private static CompletableFuture<JavaRuntime> downloadJava(GameJavaVersion javaVersion, Profile profile) {
CompletableFuture<JavaRuntime> future = new CompletableFuture<>();
Controllers.dialog(new MessageDialogPane.Builder(
i18n("launch.advice.require_newer_java_version", javaVersion.majorVersion()),
i18n("launch.advice.require_newer_java_version", javaVersion.getMajorVersion()),
i18n("message.warning"),
MessageType.QUESTION)
.yesOrNo(() -> {
@@ -710,13 +636,10 @@ public final class LauncherHelper {
return future;
}
private Task<AuthInfo> logIn(Account account) {
private static Task<AuthInfo> logIn(Account account) {
return Task.composeAsync(() -> {
try {
if (disableOfflineSkin && account instanceof OfflineAccount offlineAccount)
return Task.completed(offlineAccount.logInWithoutSkin());
else
return Task.completed(account.logIn());
return Task.completed(account.logIn());
} catch (CredentialExpiredException e) {
LOG.info("Credential has expired", e);
@@ -781,7 +704,6 @@ 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;
@@ -925,27 +847,21 @@ public final class LauncherHelper {
level = Lang.requireNonNullElse(Log4jLevel.guessLevel(log), Log4jLevel.INFO);
logBuffer.add(new Log(log, level));
} else {
lock.lock();
try {
synchronized (this) {
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")) {
lock.lock();
try {
synchronized (this) {
if (!lwjgl) {
lwjgl = true;
finishLaunch();
}
} finally {
lock.unlock();
}
}
}
@@ -970,12 +886,9 @@ public final class LauncherHelper {
// Game crashed before opening the game window.
if (!lwjgl) {
lock.lock();
try {
synchronized (this) {
if (!lwjgl)
finishLaunch();
} finally {
lock.unlock();
}
}
@@ -989,15 +902,7 @@ public final class LauncherHelper {
}
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 final Queue<WeakReference<ManagedProcess>> PROCESSES = new ConcurrentLinkedQueue<>();
public static void stopManagedProcesses() {
while (!PROCESSES.isEmpty())
@@ -17,7 +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;
import org.jackhuang.hmcl.ui.versions.ModTranslations;
@@ -41,47 +41,38 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
protected abstract SortType getBackedRemoteModRepositorySortOrder();
@Override
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
public SearchResult search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
if (!StringUtils.containsChinese(searchFilter)) {
return getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
}
Set<String> englishSearchFiltersSet = new LinkedHashSet<>(INITIAL_CAPACITY);
Set<String> englishSearchFiltersSet = new HashSet<>(INITIAL_CAPACITY);
int count = 0;
for (ModTranslations.Mod mod : ModTranslations.getTranslationsByRepositoryType(getType()).searchMod(searchFilter)) {
String englishSearchFilter = String.join(" ", StringUtils.tokenize(StringUtils.isNotBlank(mod.getSubname()) ? mod.getSubname() : mod.getName()));
if (StringUtils.isNotBlank(englishSearchFilter)) {
englishSearchFiltersSet.add(englishSearchFilter);
for (String englishWord : StringUtils.tokenize(StringUtils.isNotBlank(mod.getSubname()) ? mod.getSubname() : mod.getName())) {
if (englishSearchFiltersSet.contains(englishWord)) {
continue;
}
englishSearchFiltersSet.add(englishWord);
}
count++;
if (count >= 3) break;
}
if (englishSearchFiltersSet.isEmpty()) {
return getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
}
RemoteMod[] searchResultArray = new RemoteMod[pageSize];
int totalPages, chineseIndex = 0, englishIndex = pageSize - 1;
{
SearchResult searchResult = null;
List<RemoteMod> remoteMods = List.of();
for (String englishSearchFilter : englishSearchFiltersSet) {
searchResult = getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, englishSearchFilter, getBackedRemoteModRepositorySortOrder(), sortOrder);
remoteMods = searchResult.getUnsortedResults().toList();
if (!remoteMods.isEmpty()) {
break;
}
}
for (RemoteMod remoteMod : remoteMods) {
SearchResult searchResult = getBackedRemoteModRepository().search(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?");
continue;
}
RemoteMod remoteMod = iterator.next();
ModTranslations.Mod chineseTranslation = ModTranslations.getTranslationsByRepositoryType(getType()).getModByCurseForgeId(remoteMod.getSlug());
if (chineseTranslation != null && !StringUtils.isBlank(chineseTranslation.getName()) && StringUtils.containsChinese(chineseTranslation.getName())) {
searchResultArray[chineseIndex++] = remoteMod;
@@ -118,13 +109,13 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
}
@Override
public Optional<RemoteMod.Version> getRemoteVersionByLocalFile(Path file) throws IOException {
return getBackedRemoteModRepository().getRemoteVersionByLocalFile(file);
public Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException {
return getBackedRemoteModRepository().getRemoteVersionByLocalFile(localModFile, file);
}
@Override
public RemoteMod getModById(DownloadProvider downloadProvider, String id) throws IOException {
return getBackedRemoteModRepository().getModById(downloadProvider, id);
public RemoteMod getModById(String id) throws IOException {
return getBackedRemoteModRepository().getModById(id);
}
@Override
@@ -133,7 +124,7 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
}
@Override
public Stream<RemoteMod.Version> getRemoteVersionsById(DownloadProvider downloadProvider, String id) throws IOException {
return getBackedRemoteModRepository().getRemoteVersionsById(downloadProvider, id);
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
return getBackedRemoteModRepository().getRemoteVersionsById(id);
}
}
@@ -31,6 +31,7 @@ public final class Log {
private final String log;
private Log4jLevel level;
private boolean selected = false;
public Log(String log) {
this.log = log;
@@ -56,6 +57,14 @@ public final class Log {
return level;
}
public boolean isSelected() {
return selected;
}
public void setSelected(boolean selected) {
this.selected = selected;
}
@Override
public String toString() {
return log;
@@ -17,19 +17,21 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.Zipper;
import org.jackhuang.hmcl.util.logging.Logger;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.Zipper;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.management.ManagementFactory;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -41,11 +43,9 @@ public final class LogExporter {
private LogExporter() {
}
public static CompletableFuture<Void> exportLogs(
Path zipFile, DefaultGameRepository gameRepository, String versionId, String logs, String launchScript,
PathMatcher logMatcher) {
Path runDirectory = gameRepository.getRunDirectory(versionId);
Path baseDirectory = gameRepository.getBaseDirectory();
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();
List<String> versions = new ArrayList<>();
String currentVersionId = versionId;
@@ -64,11 +64,11 @@ public final class LogExporter {
}
return CompletableFuture.runAsync(() -> {
try (Zipper zipper = new Zipper(zipFile, true)) {
processLogs(runDirectory.resolve("liteconfig"), "*.log", "liteconfig", zipper, logMatcher);
processLogs(runDirectory.resolve("logs"), "*.log", "logs", zipper, logMatcher);
processLogs(runDirectory, "*.log", "runDirectory", zipper, logMatcher);
processLogs(runDirectory.resolve("crash-reports"), "*.txt", "crash-reports", zipper, logMatcher);
try (Zipper zipper = new Zipper(zipFile)) {
processLogs(runDirectory.resolve("liteconfig"), "*.log", "liteconfig", zipper);
processLogs(runDirectory.resolve("logs"), "*.log", "logs", zipper);
processLogs(runDirectory, "*.log", "runDirectory", zipper);
processLogs(runDirectory.resolve("crash-reports"), "*.txt", "crash-reports", zipper);
zipper.putTextFile(LOG.getLogs(), "hmcl.log");
zipper.putTextFile(logs, "minecraft.log");
@@ -86,11 +86,14 @@ public final class LogExporter {
});
}
private static void processLogs(Path directory, String fileExtension, String logDirectory, Zipper zipper, PathMatcher logMatcher) {
private static void processLogs(Path directory, String fileExtension, String logDirectory, Zipper zipper) {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory, fileExtension)) {
long processStartTime = ManagementFactory.getRuntimeMXBean().getStartTime();
for (Path file : stream) {
if (Files.isRegularFile(file)) {
if (logMatcher == null || logMatcher.matches(file)) {
FileTime time = Files.readAttributes(file, BasicFileAttributes.class).lastModifiedTime();
if (time.toMillis() >= processStartTime) {
try (BufferedReader reader = IOUtils.newBufferedReaderMaybeNativeEncoding(file)) {
zipper.putLines(reader.lines().map(Logger::filterForbiddenToken), file.getFileName().toString());
} catch (IOException e) {
@@ -19,13 +19,11 @@ 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;
@@ -44,6 +42,7 @@ 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;
@@ -51,7 +50,7 @@ import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -62,8 +61,7 @@ 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),
@@ -74,16 +72,12 @@ 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(Path file) {
public static boolean isFileModpackByExtension(File file) {
String ext = FileUtils.getExtension(file);
return "zip".equals(ext) || "mrpack".equals(ext);
}
@@ -141,12 +135,15 @@ public final class ModpackHelper {
(path.getFileName() == null || ".minecraft".equals(FileUtils.getName(path)));
}
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 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 Task<?> getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) {
@@ -170,28 +167,28 @@ public final class ModpackHelper {
return new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
.withStagesHint(Arrays.asList("hmcl.modpack", "hmcl.modpack.download"));
}
public static boolean isExternalGameNameConflicts(String name) {
return Files.exists(Paths.get("externalgames").resolve(name));
}
public static Task<?> getInstallManuallyCreatedModpackTask(Profile profile, Path zipFile, String name, Charset charset) {
public static Task<?> getInstallManuallyCreatedModpackTask(Profile profile, File zipFile, String name, Charset charset) {
if (isExternalGameNameConflicts(name)) {
throw new IllegalArgumentException("name existing");
}
return new ManuallyCreatedModpackInstallTask(profile, zipFile, charset, name)
return new ManuallyCreatedModpackInstallTask(profile, zipFile.toPath(), charset, name)
.thenAcceptAsync(Schedulers.javafx(), location -> {
Profile newProfile = new Profile(name, location);
Profile newProfile = new Profile(name, location.toFile());
newProfile.setUseRelativePath(true);
Profiles.getProfiles().add(newProfile);
Profiles.setSelectedProfile(newProfile);
});
}
public static Task<?> getInstallTask(Profile profile, Path zipFile, String name, Modpack modpack, String iconUrl) {
public static Task<?> getInstallTask(Profile profile, File zipFile, String name, Modpack modpack) {
profile.getRepository().markVersionAsModpack(name);
ExceptionalRunnable<?> success = () -> {
@@ -211,45 +208,35 @@ public final class ModpackHelper {
};
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
else if (modpack.getManifest() instanceof McbbsModpackManifest)
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name))
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name));
else
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
.whenComplete(Schedulers.javafx(), success, failure)
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.javafx(), success, failure);
}
public static Task<Void> getUpdateTask(Profile profile, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
switch (configuration.getType()) {
case ServerModpackRemoteInstallTask.MODPACK_TYPE:
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name))
.thenComposeAsync(profile.getRepository().refreshVersionsAsync())
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
.withStagesHint(Arrays.asList("hmcl.modpack", "hmcl.modpack.download"));
default:
throw new UnsupportedModpackException();
}
}
public static Task<?> getUpdateTask(Profile profile, Path zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
Modpack modpack = ModpackHelper.readModpackManifest(zipFile, charset);
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);
ModpackProvider provider = getProviderByType(configuration.getType());
if (provider == null) {
throw new UnsupportedModpackException();
}
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack)
.thenComposeAsync(() -> createMultiMCPostUpdateTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
.thenComposeAsync(profile.getRepository().refreshVersionsAsync());
else
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack)
.thenComposeAsync(profile.getRepository().refreshVersionsAsync());
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack);
}
public static void toVersionSetting(MultiMCInstanceConfiguration c, VersionSetting vs) {
@@ -289,24 +276,6 @@ 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));
@@ -22,15 +22,13 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.OAuth;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
@@ -39,14 +37,12 @@ import java.util.concurrent.ExecutionException;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
private final int port;
private final CompletableFuture<String> future = new CompletableFuture<>();
private final String codeVerifier;
private final String state;
public static String lastlyOpenedURL;
@@ -56,34 +52,6 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
super(port);
this.port = port;
var encoder = Base64.getUrlEncoder().withoutPadding();
var random = new SecureRandom();
{
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
// https://datatracker.ietf.org/doc/html/rfc6749#section-10.12
byte[] bytes = new byte[32];
random.nextBytes(bytes);
this.state = encoder.encodeToString(bytes);
}
{
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
byte[] bytes = new byte[64];
random.nextBytes(bytes);
this.codeVerifier = encoder.encodeToString(bytes);
}
}
@Override
public String getCodeVerifier() {
return codeVerifier;
}
@Override
public String getState() {
return state;
}
@Override
@@ -125,32 +93,19 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
String parameters = session.getQueryParameterString();
Map<String, String> query = mapOf(NetworkUtils.parseQuery(parameters));
String code = query.get("code");
if (code != null) {
if (this.state.equals(query.get("state"))) {
idToken = query.get("id_token");
future.complete(code);
} else if (query.containsKey("state")) {
LOG.warning("Failed to authenticate: invalid state in parameters");
future.completeExceptionally(new AuthenticationException("Failed to authenticate: invalid state"));
} else {
LOG.warning("Failed to authenticate: missing state in parameters");
future.completeExceptionally(new AuthenticationException("Failed to authenticate: missing state"));
}
if (query.containsKey("code")) {
idToken = query.get("id_token");
future.complete(query.get("code"));
} else {
LOG.warning("Failed to authenticate: missing authorization code in parameters");
future.completeExceptionally(new AuthenticationException("Failed to authenticate: missing authorization code"));
LOG.warning("Error: " + parameters);
future.completeExceptionally(new AuthenticationException("failed to authenticate"));
}
String html;
try {
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
.replace("%style%", Themes.getTheme().toColorScheme().toStyleSheet().replace("-monet", "--monet"))
.replace("%lang%", Locale.getDefault().toLanguageTag())
.replace("%success%", i18n("message.success"))
.replace("%ok%", i18n("button.ok"))
.replace("%close_page%", i18n("account.methods.microsoft.close_page"));
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
} catch (IOException e) {
LOG.error("Failed to load html", e);
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
@@ -166,18 +121,9 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
return newFixedLengthResponse(Response.Status.OK, "text/html; charset=UTF-8", html);
}
@Override
public void close() {
if (!future.isDone())
future.completeExceptionally(new AuthenticationException("OAuth server is closing"));
stop();
}
public static class Factory implements OAuth.Callback {
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
public final EventManager<LoginCompletedDeviceCodeEvent> onLoginCompletedDeviceCode = new EventManager<>();
public final EventManager<OpenBrowserEvent> onOpenBrowserAuthorizationCode = new EventManager<>();
public final EventManager<OpenBrowserEvent> onOpenBrowserDevice = new EventManager<>();
public final EventManager<OpenBrowserEvent> onOpenBrowser = new EventManager<>();
@Override
public OAuth.Session startServer() throws IOException, AuthenticationException {
@@ -204,24 +150,28 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
}
@Override
public void loginCompletedDeviceCode() {
onLoginCompletedDeviceCode.fireEvent(new LoginCompletedDeviceCodeEvent(this));
}
@Override
public void openBrowser(OAuth.GrantFlow grantFlow, String url) throws IOException {
public void openBrowser(String url) throws IOException {
lastlyOpenedURL = url;
FXUtils.openLink(url);
switch (grantFlow) {
case AUTHORIZATION_CODE -> onOpenBrowserAuthorizationCode.fireEvent(new OpenBrowserEvent(this, url));
case DEVICE -> onOpenBrowserDevice.fireEvent(new OpenBrowserEvent(this, url));
}
onOpenBrowser.fireEvent(new OpenBrowserEvent(this, url));
}
@Override
public String getClientId() {
return System.getProperty("hmcl.microsoft.auth.id",
JarUtils.getAttribute("hmcl.microsoft.auth.id", ""));
JarUtils.getManifestAttribute("Microsoft-Auth-Id", ""));
}
@Override
public String getClientSecret() {
return System.getProperty("hmcl.microsoft.auth.secret",
JarUtils.getManifestAttribute("Microsoft-Auth-Secret", ""));
}
@Override
public boolean isPublicClient() {
return true; // We have turned on the device auth flow.
}
}
@@ -244,12 +194,6 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
}
}
public static class LoginCompletedDeviceCodeEvent extends Event {
public LoginCompletedDeviceCodeEvent(Object source) {
super(source);
}
}
public static class OpenBrowserEvent extends Event {
private final String url;
@@ -26,6 +26,8 @@ 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;
@@ -43,6 +45,7 @@ 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.*;
@@ -107,7 +110,7 @@ public final class TexturesLoader {
if (!Files.isRegularFile(file)) {
// download it
try {
new FileDownloadTask(texture.getUrl(), file).run();
new FileDownloadTask(new URL(texture.getUrl()), file.toFile()).run();
LOG.info("Texture downloaded: " + texture.getUrl());
} catch (Exception e) {
if (Files.isRegularFile(file)) {
@@ -262,8 +265,15 @@ public final class TexturesLoader {
int scale = (int) skin.getWidth() / 64;
int faceOffset = (int) Math.round(size / 18.0);
g.setImageSmoothing(false);
drawAvatar(g, skin, size, scale, faceOffset);
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);
}
}
private static void drawAvatar(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
@@ -275,6 +285,37 @@ 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,9 +30,8 @@ import org.jackhuang.hmcl.util.platform.Platform;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.io.InputStream;
import java.nio.file.*;
import java.util.*;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -59,7 +58,7 @@ public final class HMCLJavaRepository implements JavaRepository {
}
public Path getJavaDir(Platform platform, GameJavaVersion gameJavaVersion) {
return getJavaDir(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.component());
return getJavaDir(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
}
@Override
@@ -68,7 +67,7 @@ public final class HMCLJavaRepository implements JavaRepository {
}
public Path getManifestFile(Platform platform, GameJavaVersion gameJavaVersion) {
return getManifestFile(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.component());
return getManifestFile(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
}
public boolean isInstalled(Platform platform, String name) {
@@ -76,7 +75,7 @@ public final class HMCLJavaRepository implements JavaRepository {
}
public boolean isInstalled(Platform platform, GameJavaVersion gameJavaVersion) {
return isInstalled(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.component());
return isInstalled(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
}
public @Nullable Path getJavaExecutable(Platform platform, String name) {
@@ -96,10 +95,10 @@ public final class HMCLJavaRepository implements JavaRepository {
}
public @Nullable Path getJavaExecutable(Platform platform, GameJavaVersion gameJavaVersion) {
return getJavaExecutable(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.component());
return getJavaExecutable(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
}
private static void getAllJava(List<Path> list, Platform platform, Path platformRoot) {
private static void getAllJava(List<JavaRuntime> list, Platform platform, Path platformRoot, boolean isManaged) {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(platformRoot)) {
for (Path file : stream) {
try {
@@ -117,7 +116,12 @@ public final class HMCLJavaRepository implements JavaRepository {
}
if (Files.isDirectory(javaDir)) {
list.add(executable);
JavaManifest manifest;
try (InputStream input = Files.newInputStream(file)) {
manifest = JsonUtils.fromJsonFully(input, JavaManifest.class);
}
list.add(JavaRuntime.of(executable, manifest.getInfo(), isManaged));
}
}
} catch (Throwable e) {
@@ -130,23 +134,28 @@ public final class HMCLJavaRepository implements JavaRepository {
}
@Override
public Collection<Path> getAllJava(Platform platform) {
public Collection<JavaRuntime> getAllJava(Platform platform) {
Path platformRoot = getPlatformRoot(platform);
if (!Files.isDirectory(platformRoot))
return Collections.emptyList();
ArrayList<Path> list = new ArrayList<>();
ArrayList<JavaRuntime> list = new ArrayList<>();
getAllJava(list, platform, platformRoot, true);
if (platform.getOperatingSystem() == OperatingSystem.MACOS) {
platformRoot = root.resolve(platform.getOperatingSystem().getMojangName() + "-" + platform.getArchitecture().getCheckedName());
if (Files.isDirectory(platformRoot))
getAllJava(list, platform, platformRoot, false);
}
getAllJava(list, platform, platformRoot);
return list;
}
@Override
public Task<JavaRuntime> getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion) {
Path javaDir = getJavaDir(platform, gameJavaVersion);
Path tempDir = getPlatformRoot(platform).resolve(".tmp").resolve(javaDir.getFileName());
return new MojangJavaDownloadTask(downloadProvider, javaDir, tempDir, gameJavaVersion, JavaManager.getMojangJavaPlatform(platform)).thenApplyAsync(result -> {
return new MojangJavaDownloadTask(downloadProvider, javaDir, gameJavaVersion, JavaManager.getMojangJavaPlatform(platform)).thenApplyAsync(result -> {
Path executable;
try {
executable = JavaManager.getExecutable(javaDir).toRealPath();
@@ -159,16 +168,16 @@ public final class HMCLJavaRepository implements JavaRepository {
JavaInfo info;
if (JavaManager.isCompatible(platform))
info = JavaInfoUtils.fromExecutable(executable);
info = JavaInfoUtils.fromExecutable(executable, false);
else
info = new JavaInfo(platform, result.download().version().name(), null);
info = new JavaInfo(platform, result.download.getVersion().getName(), null);
Map<String, Object> update = new LinkedHashMap<>();
update.put("provider", "mojang");
update.put("component", gameJavaVersion.component());
update.put("component", gameJavaVersion.getComponent());
Map<String, JavaLocalFiles.Local> files = new LinkedHashMap<>();
result.remoteFiles().getFiles().forEach((path, file) -> {
result.remoteFiles.getFiles().forEach((path, file) -> {
if (file instanceof MojangJavaRemoteFiles.RemoteFile) {
DownloadInfo downloadInfo = ((MojangJavaRemoteFiles.RemoteFile) file).getDownloads().get("raw");
if (downloadInfo != null) {
@@ -182,7 +191,7 @@ public final class HMCLJavaRepository implements JavaRepository {
});
JavaManifest manifest = new JavaManifest(info, update, files);
JsonUtils.writeToJsonFile(getManifestFile(platform, gameJavaVersion), manifest);
FileUtils.writeText(getManifestFile(platform, gameJavaVersion), JsonUtils.GSON.toJson(manifest));
return JavaRuntime.of(executable, info, true);
});
}
@@ -194,7 +203,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();
JsonUtils.writeToJsonFile(getManifestFile(platform, name), result);
FileUtils.writeText(getManifestFile(platform, name), JsonUtils.GSON.toJson(result));
return JavaRuntime.of(executable, result.getInfo(), true);
});
}
@@ -203,7 +212,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));
FileUtils.deleteDirectory(getJavaDir(platform, name).toFile());
});
}
@@ -216,7 +225,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));
FileUtils.deleteDirectory(getJavaDir(java.getPlatform(), name).toFile());
}
});
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.java;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
@@ -28,6 +27,7 @@ import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
/**
@@ -39,10 +39,35 @@ public final class JavaInfoUtils {
private JavaInfoUtils() {
}
public static @NotNull JavaInfo fromExecutable(Path executable) throws IOException {
private static Path tryFindReleaseFile(Path executable) {
Path parent = executable.getParent();
if (parent != null && parent.getFileName() != null && parent.getFileName().toString().equals("bin")) {
Path javaHome = parent.getParent();
if (javaHome != null && javaHome.getFileName() != null) {
Path releaseFile = javaHome.resolve("release");
String javaHomeName = javaHome.getFileName().toString();
if ((javaHomeName.contains("jre") || javaHomeName.contains("jdk") || javaHomeName.contains("openj9"))
&& Files.isRegularFile(releaseFile)) {
return releaseFile;
}
}
}
return null;
}
public static @NotNull JavaInfo fromExecutable(Path executable, boolean tryFindReleaseFile) throws IOException {
assert executable.isAbsolute();
Path releaseFile;
if (tryFindReleaseFile && (releaseFile = tryFindReleaseFile(executable)) != null) {
try {
return JavaInfo.fromReleaseFile(releaseFile);
} catch (IOException ignored) {
}
}
Path thisPath = JarUtils.thisJarPath();
if (thisPath == null) {
throw new IOException("Failed to find current HMCL location");
}
@@ -69,6 +94,7 @@ public final class JavaInfoUtils {
? architecture
: Architecture.SYSTEM_ARCH);
return new JavaInfo(platform, result.javaVersion, result.javaVendor);
} catch (IOException e) {
throw e;
@@ -77,9 +103,14 @@ public final class JavaInfoUtils {
}
}
@JsonSerializable
private record Result(@SerializedName("os.name") String osName, @SerializedName("os.arch") String osArch,
@SerializedName("java.version") String javaVersion,
@SerializedName("java.vendor") String javaVendor) {
private static final class Result {
@SerializedName("os.name")
public String osName;
@SerializedName("os.arch")
public String osArch;
@SerializedName("java.version")
public String javaVersion;
@SerializedName("java.vendor")
public String javaVendor;
}
}
@@ -20,20 +20,17 @@ 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.io.FileUtils;
import org.jackhuang.hmcl.util.Hex;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.*;
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;
@@ -101,9 +98,10 @@ public final class JavaInstallTask extends Task<JavaManifest> {
}
if (tree.isExecutable(entry))
FileUtils.setExecutable(path);
//noinspection ResultOfMethodCallIgnored
path.toFile().setExecutable(true);
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalFile(HexFormat.of().formatHex(messageDigest.digest()), size));
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalFile(Hex.encodeHex(messageDigest.digest()), size));
}
nameStack.remove(nameStack.size() - 1);
}
@@ -17,8 +17,6 @@
*/
package org.jackhuang.hmcl.java;
import com.google.gson.*;
import com.google.gson.stream.JsonWriter;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.jackhuang.hmcl.Metadata;
@@ -32,9 +30,7 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.DigestUtils;
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.platform.*;
import org.jackhuang.hmcl.util.platform.windows.WinReg;
@@ -44,14 +40,9 @@ import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -64,16 +55,6 @@ public final class JavaManager {
private JavaManager() {
}
private static final String[] KNOWN_VENDOR_DIRECTORIES = {
"Java",
"BellSoft",
"AdoptOpenJDK",
"Zulu",
"Microsoft",
"Eclipse Foundation",
"Semeru"
};
public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_GLOBAL_DIRECTORY.resolve("java"));
public static final HMCLJavaRepository LOCAL_REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_CURRENT_DIRECTORY.resolve("java"));
@@ -122,27 +103,16 @@ public final class JavaManager {
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
// Windows x86-64 platform is compatible with x86 programs
return architecture == Architecture.X86;
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
// Since Windows 10 Build 21277, Windows Arm64 has been compatible with x86-64 programs via translation
if (architecture == Architecture.X86_64 && Platform.isSupportedTranslationX86_64())
return true;
// Windows Arm64 is compatible with x86 programs via translation
if (architecture == Architecture.X86)
return true;
return false;
}
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
return OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277 && architecture == Architecture.X86_64 || architecture == Architecture.X86;
break;
case LINUX:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
return architecture == Architecture.X86;
break;
case MACOS:
// macOS Arm64 compatible with x86-64 programs via Rosetta 2.
if (Architecture.SYSTEM_ARCH == Architecture.ARM64 && Platform.isSupportedTranslationX86_64())
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
return architecture == Architecture.X86_64;
break;
}
@@ -190,12 +160,12 @@ public final class JavaManager {
return javaRuntime;
}
JavaInfo info = JavaInfoUtils.fromExecutable(executable);
JavaInfo info = JavaInfoUtils.fromExecutable(executable, true);
return JavaRuntime.of(executable, info, false);
}
public static void refresh() {
Task.supplyAsync(() -> searchPotentialJavaExecutables(false)).whenComplete(Schedulers.javafx(), (result, exception) -> {
Task.supplyAsync(JavaManager::searchPotentialJavaExecutables).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (result != null) {
LATCH.await();
allJava = result;
@@ -239,18 +209,9 @@ 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 {
@@ -334,7 +295,7 @@ public final class JavaManager {
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) {
if (!constraint.checkJava(gameVersion, version, java, analyzer)) {
if (!constraint.checkJava(gameVersion, version, java)) {
if (constraint.isMandatory()) {
violationMandatory = true;
} else {
@@ -356,7 +317,7 @@ public final class JavaManager {
}
public static void initialize() {
Map<Path, JavaRuntime> allJava = searchPotentialJavaExecutables(true);
Map<Path, JavaRuntime> allJava = searchPotentialJavaExecutables();
JavaManager.allJava = allJava;
LATCH.countDown();
FXUtils.runInFX(() -> updateAllJavaProperty(allJava));
@@ -364,56 +325,56 @@ public final class JavaManager {
// search java
private static Map<Path, JavaRuntime> searchPotentialJavaExecutables(boolean useCache) {
Searcher searcher = new Searcher(Metadata.HMCL_GLOBAL_DIRECTORY.resolve("javaCache.json"));
if (useCache)
searcher.loadCache();
searcher.searchAllJavaInRepository(Platform.SYSTEM_PLATFORM);
private static Map<Path, JavaRuntime> searchPotentialJavaExecutables() {
Map<Path, JavaRuntime> javaRuntimes = new HashMap<>();
searchAllJavaInRepository(javaRuntimes, Platform.SYSTEM_PLATFORM);
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86);
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86);
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
if (Platform.isSupportedTranslationX86_64())
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86_64);
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86);
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277)
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86_64);
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86);
}
break;
case MACOS:
if (Architecture.SYSTEM_ARCH == Architecture.ARM64 && Platform.isSupportedTranslationX86_64())
searcher.searchAllJavaInRepository(Platform.MACOS_X86_64);
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
searchAllJavaInRepository(javaRuntimes, Platform.MACOS_X86_64);
break;
}
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
searcher.queryJavaInRegistryKey(WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Runtime Environment");
searcher.queryJavaInRegistryKey(WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Development Kit");
searcher.queryJavaInRegistryKey(WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JRE");
searcher.queryJavaInRegistryKey(WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JDK");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Runtime Environment");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Development Kit");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JRE");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JDK");
searcher.searchJavaInProgramFiles("ProgramFiles", "C:\\Program Files");
searcher.searchJavaInProgramFiles("ProgramFiles(x86)", "C:\\Program Files (x86)");
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles", "C:\\Program Files");
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles(x86)", "C:\\Program Files (x86)");
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles(ARM)", "C:\\Program Files (ARM)");
}
break;
case LINUX:
searcher.searchAllJavaInDirectory(Path.of("/usr/java")); // Oracle RPMs
searcher.searchAllJavaInDirectory(Path.of("/usr/lib/jvm")); // General locations
searcher.searchAllJavaInDirectory(Path.of("/usr/lib32/jvm")); // General locations
searcher.searchAllJavaInDirectory(Path.of("/usr/lib64/jvm")); // General locations
searcher.searchAllJavaInDirectory(Path.of(System.getProperty("user.home"), "/.sdkman/candidates/java")); // SDKMAN!
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/java")); // Oracle RPMs
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib/jvm")); // General locations
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib32/jvm")); // General locations
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib64/jvm")); // General locations
searchAllJavaInDirectory(javaRuntimes, Paths.get(System.getProperty("user.home"), "/.sdkman/candidates/java")); // SDKMAN!
break;
case MACOS:
searcher.searchJavaInMacJavaVirtualMachines(Path.of("/Library/Java/JavaVirtualMachines"));
searcher.searchJavaInMacJavaVirtualMachines(Path.of(System.getProperty("user.home"), "/Library/Java/JavaVirtualMachines"));
searcher.tryAddJavaExecutable(Path.of("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"));
searcher.tryAddJavaExecutable(Path.of("/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java"));
searchJavaInMacJavaVirtualMachines(javaRuntimes, Paths.get("/Library/Java/JavaVirtualMachines"));
searchJavaInMacJavaVirtualMachines(javaRuntimes, Paths.get(System.getProperty("user.home"), "/Library/Java/JavaVirtualMachines"));
tryAddJavaExecutable(javaRuntimes, Paths.get("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"));
tryAddJavaExecutable(javaRuntimes, Paths.get("/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java"));
// Homebrew
searcher.tryAddJavaExecutable(Path.of("/opt/homebrew/opt/java/bin/java"));
searcher.searchAllJavaInDirectory(Path.of("/opt/homebrew/Cellar/openjdk"));
try (DirectoryStream<Path> dirs = Files.newDirectoryStream(Path.of("/opt/homebrew/Cellar"), "openjdk@*")) {
tryAddJavaExecutable(javaRuntimes, Paths.get("/opt/homebrew/opt/java/bin/java"));
searchAllJavaInDirectory(javaRuntimes, Paths.get("/opt/homebrew/Cellar/openjdk"));
try (DirectoryStream<Path> dirs = Files.newDirectoryStream(Paths.get("/opt/homebrew/Cellar"), "openjdk@*")) {
for (Path dir : dirs) {
searcher.searchAllJavaInDirectory(dir);
searchAllJavaInDirectory(javaRuntimes, dir);
}
} catch (IOException e) {
LOG.warning("Failed to get subdirectories of /opt/homebrew/Cellar");
@@ -427,30 +388,23 @@ public final class JavaManager {
// Search Minecraft bundled runtimes
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && Architecture.SYSTEM_ARCH.isX86()) {
FileUtils.tryGetPath(System.getenv("localappdata"), "Packages\\Microsoft.4297127D64EC6_8wekyb3d8bbwe\\LocalCache\\Local\\runtime")
.ifPresent(it -> searcher.searchAllOfficialJava(it, false));
.ifPresent(it -> searchAllOfficialJava(javaRuntimes, it, false));
FileUtils.tryGetPath(Lang.requireNonNullElse(System.getenv("ProgramFiles(x86)"), "C:\\Program Files (x86)"), "Minecraft Launcher\\runtime")
.ifPresent(it -> searcher.searchAllOfficialJava(it, false));
.ifPresent(it -> searchAllOfficialJava(javaRuntimes, it, false));
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.X86_64) {
searcher.searchAllOfficialJava(Path.of(System.getProperty("user.home"), ".minecraft/runtime"), false);
searchAllOfficialJava(javaRuntimes, Paths.get(System.getProperty("user.home"), ".minecraft/runtime"), false);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
searcher.searchAllOfficialJava(Path.of(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime"), false);
searchAllOfficialJava(javaRuntimes, Paths.get(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime"), false);
}
searcher.searchAllOfficialJava(CacheRepository.getInstance().getCacheDirectory().resolve("java"), true);
searchAllOfficialJava(javaRuntimes, CacheRepository.getInstance().getCacheDirectory().resolve("java"), true);
// Search in PATH.
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 {
searcher.tryAddJavaExecutable(Path.of(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
tryAddJavaExecutable(javaRuntimes, Paths.get(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
} catch (InvalidPathException ignored) {
}
}
@@ -460,17 +414,16 @@ public final class JavaManager {
String[] paths = System.getenv("HMCL_JRES").split(File.pathSeparator);
for (String path : paths) {
try {
searcher.tryAddJavaHome(Path.of(path));
tryAddJavaHome(javaRuntimes, Paths.get(path));
} catch (InvalidPathException ignored) {
}
}
}
searcher.searchAllJavaInDirectory(Path.of(System.getProperty("user.home"), ".jdks"));
searchAllJavaInDirectory(javaRuntimes, Paths.get(System.getProperty("user.home"), ".jdks"));
for (String javaPath : ConfigHolder.globalConfig().getUserJava()) {
try {
searcher.tryAddJavaExecutable(Path.of(javaPath));
tryAddJavaExecutable(javaRuntimes, Paths.get(javaPath));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path: " + javaPath);
}
@@ -478,406 +431,230 @@ public final class JavaManager {
JavaRuntime currentJava = JavaRuntime.CURRENT_JAVA;
if (currentJava != null
&& !searcher.javaRuntimes.containsKey(currentJava.getBinary())
&& !javaRuntimes.containsKey(currentJava.getBinary())
&& !ConfigHolder.globalConfig().getDisabledJava().contains(currentJava.getBinary().toString())) {
searcher.addResult(currentJava.getBinary(), currentJava);
javaRuntimes.put(currentJava.getBinary(), currentJava);
}
searcher.saveCache();
LOG.trace(searcher.javaRuntimes.values().stream().sorted()
LOG.trace(javaRuntimes.values().stream().sorted()
.map(it -> String.format(" - %s %s (%s, %s): %s",
it.isJDK() ? "JDK" : "JRE",
it.getVersion(),
it.getPlatform().getArchitecture().getDisplayName(),
Lang.requireNonNullElse(it.getVendor(), "Unknown"),
it.getBinary()))
.collect(Collectors.joining("\n", "Finished Java lookup, found " + searcher.javaRuntimes.size() + "\n", "")));
return searcher.javaRuntimes;
.collect(Collectors.joining("\n", "Finished Java lookup, found " + javaRuntimes.size() + "\n", "")));
return javaRuntimes;
}
private static final class Searcher {
private final Path cacheFile;
final Map<Path, JavaRuntime> javaRuntimes = new HashMap<>();
private final LinkedHashMap<Path, JavaInfoCache> caches = new LinkedHashMap<>();
private final Set<Path> failed = new HashSet<>();
private boolean needRefreshCache = false;
Searcher(Path cacheFile) {
this.cacheFile = cacheFile;
private static void tryAddJavaHome(Map<Path, JavaRuntime> javaRuntimes, Path javaHome) {
Path executable = getExecutable(javaHome);
if (!Files.isRegularFile(executable)) {
return;
}
private static final Pattern CACHE_VERSION_PATTERN = Pattern.compile("(?<major>\\d+)(?:\\.(?<minor>\\d+))?");
private static final int CACHE_MAJOR_VERSION = 0;
private static final int CACHE_MINOR_VERSION = 0;
private record JavaInfoCache(String key, JavaInfo info) {
try {
executable = executable.toRealPath();
} catch (IOException e) {
LOG.warning("Failed to resolve path " + executable, e);
return;
}
void loadCache() {
if (Files.notExists(cacheFile))
return;
if (javaRuntimes.containsKey(executable) || ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
return;
}
JavaInfo info = null;
Path releaseFile = javaHome.resolve("release");
if (Files.exists(releaseFile)) {
try {
JsonObject jsonFile = JsonUtils.fromJsonFile(cacheFile, JsonObject.class);
JsonElement fileVersion = jsonFile.get("version");
Matcher matcher;
if (fileVersion instanceof JsonPrimitive version
&& (matcher = CACHE_VERSION_PATTERN.matcher(version.getAsString())).matches()) {
int major = Integer.parseInt(matcher.group("major"));
String minorString = matcher.group("minor");
int minor = minorString != null ? Integer.parseInt(minorString) : 0;
if (major != CACHE_MAJOR_VERSION || minor < CACHE_MINOR_VERSION)
throw new IOException("Unsupported cache file, version: %s".formatted(version.getAsString()));
} else
throw new IOException("Invalid version JSON: " + fileVersion);
JsonArray cachesArray = jsonFile.getAsJsonArray("caches");
for (JsonElement element : cachesArray) {
try {
var obj = (JsonObject) element;
Path realPath = Path.of(obj.getAsJsonPrimitive("path").getAsString()).toRealPath();
String key = obj.getAsJsonPrimitive("key").getAsString();
OperatingSystem osName = OperatingSystem.parseOSName(obj.getAsJsonPrimitive("os.name").getAsString());
Architecture osArch = Architecture.parseArchName(obj.getAsJsonPrimitive("os.arch").getAsString());
String javaVersion = obj.getAsJsonPrimitive("java.version").getAsString();
JavaInfo.Builder infoBuilder = JavaInfo.newBuilder(Platform.getPlatform(osName, osArch), javaVersion);
if (obj.get("java.vendor") instanceof JsonPrimitive vendor)
infoBuilder.setVendor(vendor.getAsString());
caches.put(realPath, new JavaInfoCache(key, infoBuilder.build()));
} catch (Exception e) {
LOG.warning("Invalid cache: " + element);
needRefreshCache = true;
}
}
} catch (Exception ex) {
LOG.warning("Failed to load cache file: " + cacheFile);
needRefreshCache = true;
}
}
void saveCache() {
if (!needRefreshCache)
return;
needRefreshCache = false;
try {
FileUtils.saveSafely(cacheFile, output -> {
try (var writer = new JsonWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8))) {
writer.beginObject();
writer.name("version").value("%d.%d".formatted(CACHE_MAJOR_VERSION, CACHE_MINOR_VERSION));
writer.name("caches");
writer.beginArray();
for (Map.Entry<Path, JavaInfoCache> entry : caches.entrySet()) {
Path path = entry.getKey();
JavaInfoCache cache = entry.getValue();
JavaInfo info = cache.info();
writer.beginObject();
writer.name("path").value(path.toString());
writer.name("key").value(cache.key());
writer.name("os.name").value(info.getPlatform().os().getCheckedName());
writer.name("os.arch").value(info.getPlatform().arch().getCheckedName());
writer.name("java.version").value(info.getVersion());
if (info.getVendor() != null)
writer.name("java.vendor").value(info.getVendor());
writer.endObject();
}
writer.endArray();
writer.endObject();
}
});
} catch (Exception e) {
LOG.warning("Failed to save cache file: " + cacheFile);
}
}
private static @Nullable String createCacheKey(Path realPath) {
Path binDir = realPath.getParent();
if (binDir == null || !FileUtils.getName(binDir).equals("bin"))
return null;
if (Files.isRegularFile(realPath.resolveSibling("ikvm.properties")))
return null;
Path javaHome = binDir.getParent();
if (javaHome == null)
return null;
Path libDir = javaHome.resolve("lib");
if (!Files.isDirectory(libDir))
return null;
BasicFileAttributes launcherAttributes;
String releaseHash = null;
BasicFileAttributes coreLibsAttributes = null;
try {
launcherAttributes = Files.readAttributes(realPath, BasicFileAttributes.class);
Path releaseFile = javaHome.resolve("release");
if (Files.exists(releaseFile)) {
releaseHash = DigestUtils.digestToString("SHA-1", releaseFile);
} else {
Path coreLibsFile = libDir.resolve("rt.jar");
if (!Files.isRegularFile(coreLibsFile)) {
coreLibsFile = javaHome.resolve("jre/lib/rt.jar");
if (!Files.isRegularFile(coreLibsFile))
return null;
coreLibsAttributes = Files.readAttributes(coreLibsFile, BasicFileAttributes.class);
}
}
} catch (Exception e) {
LOG.warning("Failed to create cache key for " + realPath, e);
return null;
}
StringJoiner joiner = new StringJoiner(",");
joiner.add("sz:" + launcherAttributes.size());
joiner.add("lm:" + launcherAttributes.lastModifiedTime().toMillis());
if (releaseHash != null)
joiner.add(releaseHash);
if (coreLibsAttributes != null) {
joiner.add("rsz:" + coreLibsAttributes.size());
joiner.add("rlm:" + coreLibsAttributes.lastModifiedTime().toMillis());
}
return joiner.toString();
}
void addResult(Path realPath, JavaRuntime javaRuntime) {
javaRuntimes.put(realPath, javaRuntime);
}
void tryAddJavaHome(Path javaHome) {
tryAddJavaExecutable(getExecutable(javaHome));
}
void tryAddJavaExecutable(Path executable) {
tryAddJavaExecutable(executable, false);
}
void tryAddJavaExecutable(Path executable, boolean isManaged) {
try {
executable = executable.toRealPath();
info = JavaInfo.fromReleaseFile(releaseFile);
} catch (IOException e) {
return;
}
if (javaRuntimes.containsKey(executable)
|| failed.contains(executable)
|| ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
return;
}
String cacheKey = createCacheKey(executable);
if (cacheKey != null) {
JavaInfoCache cache = caches.get(executable);
if (cache != null) {
if (isCompatible(cache.info().getPlatform()) && cacheKey.equals(cache.key())) {
javaRuntimes.put(executable, JavaRuntime.of(executable, cache.info(), isManaged));
return;
} else {
caches.remove(executable);
needRefreshCache = true;
}
}
} else if (caches.remove(executable) != null) {
needRefreshCache = true;
}
JavaInfo info;
try {
info = JavaInfoUtils.fromExecutable(executable);
} catch (IOException e) {
LOG.warning("Failed to lookup Java executable at " + executable, e);
failed.add(executable);
return;
}
if (cacheKey != null) {
caches.put(executable, new JavaInfoCache(cacheKey, info));
needRefreshCache = true;
}
javaRuntimes.put(executable, JavaRuntime.of(executable, info, isManaged));
}
void tryAddJavaInComponentDir(String platform, Path component, boolean verify) {
Path sha1File = component.resolve(platform).resolve(component.getFileName() + ".sha1");
if (!Files.isRegularFile(sha1File))
return;
Path dir = component.resolve(platform).resolve(component.getFileName());
if (verify) {
try (BufferedReader reader = Files.newBufferedReader(sha1File)) {
String line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) continue;
int idx = line.indexOf(" /#//");
if (idx <= 0)
throw new IOException("Illegal line: " + line);
Path file = dir.resolve(line.substring(0, idx));
// Should we check the sha1 of files? This will take a lot of time.
if (Files.notExists(file))
throw new NoSuchFileException(file.toAbsolutePath().toString());
}
} catch (IOException e) {
LOG.warning("Failed to verify Java in " + component, e);
return;
}
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
Path macPath = dir.resolve("jre.bundle/Contents/Home");
if (Files.exists(macPath)) {
tryAddJavaHome(macPath);
return;
} else
LOG.warning("The Java is not in 'jre.bundle/Contents/Home'");
}
tryAddJavaHome(dir);
}
void searchAllJavaInRepository(Platform platform) {
for (Path java : REPOSITORY.getAllJava(platform)) {
tryAddJavaExecutable(java, true);
}
for (Path java : LOCAL_REPOSITORY.getAllJava(platform)) {
tryAddJavaExecutable(java, true);
}
if (platform.os() == OperatingSystem.MACOS) {
// In the past, we used 'osx' as the checked name for macOS
Path platformRoot = REPOSITORY.getPlatformRoot(platform).resolveSibling("osx-" + platform.getArchitecture().getCheckedName());
searchAllJavaInDirectory(platformRoot);
}
}
void searchAllOfficialJava(Path directory, boolean verify) {
if (!Files.isDirectory(directory))
return;
// Examples:
// $HOME/Library/Application Support/minecraft/runtime/java-runtime-beta/mac-os/java-runtime-beta/jre.bundle/Contents/Home
// $HOME/.minecraft/runtime/java-runtime-beta/linux/java-runtime-beta
String javaPlatform = getMojangJavaPlatform(Platform.SYSTEM_PLATFORM);
if (javaPlatform != null) {
searchAllOfficialJava(directory, javaPlatform, verify);
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
if (Platform.isSupportedTranslationX86_64()) {
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.WINDOWS_X86_64), verify);
}
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
if (Architecture.SYSTEM_ARCH == Architecture.ARM64 && Platform.isSupportedTranslationX86_64()) {
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.MACOS_X86_64), verify);
try {
info = JavaInfoUtils.fromExecutable(executable, false);
} catch (IOException e2) {
e2.addSuppressed(e);
LOG.warning("Failed to lookup Java executable at " + executable, e2);
}
}
}
void searchAllOfficialJava(Path directory, String platform, boolean verify) {
try (DirectoryStream<Path> dir = Files.newDirectoryStream(directory)) {
// component can be jre-legacy, java-runtime-alpha, java-runtime-beta, java-runtime-gamma or any other being added in the future.
for (Path component : dir) {
tryAddJavaInComponentDir(platform, component, verify);
if (info != null && isCompatible(info.getPlatform()))
javaRuntimes.put(executable, JavaRuntime.of(executable, info, false));
}
private static void tryAddJavaExecutable(Map<Path, JavaRuntime> javaRuntimes, Path executable) {
try {
executable = executable.toRealPath();
} catch (IOException e) {
return;
}
if (javaRuntimes.containsKey(executable) || ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
return;
}
JavaInfo info = null;
try {
info = JavaInfoUtils.fromExecutable(executable, true);
} catch (IOException e) {
LOG.warning("Failed to lookup Java executable at " + executable, e);
}
if (info != null && isCompatible(info.getPlatform())) {
javaRuntimes.put(executable, JavaRuntime.of(executable, info, false));
}
}
private static void tryAddJavaInComponentDir(Map<Path, JavaRuntime> javaRuntimes, String platform, Path component, boolean verify) {
Path sha1File = component.resolve(platform).resolve(component.getFileName() + ".sha1");
if (!Files.isRegularFile(sha1File))
return;
Path dir = component.resolve(platform).resolve(component.getFileName());
if (verify) {
try (BufferedReader reader = Files.newBufferedReader(sha1File)) {
String line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) continue;
int idx = line.indexOf(" /#//");
if (idx <= 0)
throw new IOException("Illegal line: " + line);
Path file = dir.resolve(line.substring(0, idx));
// Should we check the sha1 of files? This will take a lot of time.
if (Files.notExists(file))
throw new NoSuchFileException(file.toAbsolutePath().toString());
}
} catch (IOException e) {
LOG.warning("Failed to list java-runtime directory " + directory, e);
LOG.warning("Failed to verify Java in " + component, e);
return;
}
}
void searchAllJavaInDirectory(Path directory) {
if (!Files.isDirectory(directory)) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
Path macPath = dir.resolve("jre.bundle/Contents/Home");
if (Files.exists(macPath)) {
tryAddJavaHome(javaRuntimes, macPath);
return;
}
} else
LOG.warning("The Java is not in 'jre.bundle/Contents/Home'");
}
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
for (Path subDir : stream) {
tryAddJavaHome(subDir);
tryAddJavaHome(javaRuntimes, dir);
}
private static void searchAllJavaInRepository(Map<Path, JavaRuntime> javaRuntimes, Platform platform) {
for (JavaRuntime java : REPOSITORY.getAllJava(platform)) {
javaRuntimes.put(java.getBinary(), java);
}
for (JavaRuntime java : LOCAL_REPOSITORY.getAllJava(platform)) {
javaRuntimes.put(java.getBinary(), java);
}
}
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, boolean verify) {
if (!Files.isDirectory(directory))
return;
// Examples:
// $HOME/Library/Application Support/minecraft/runtime/java-runtime-beta/mac-os/java-runtime-beta/jre.bundle/Contents/Home
// $HOME/.minecraft/runtime/java-runtime-beta/linux/java-runtime-beta
String javaPlatform = getMojangJavaPlatform(Platform.SYSTEM_PLATFORM);
if (javaPlatform != null) {
searchAllOfficialJava(javaRuntimes, directory, javaPlatform, verify);
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277) {
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86_64), verify);
}
} catch (IOException e) {
LOG.warning("Failed to find Java in " + directory, e);
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && Architecture.CURRENT_ARCH == Architecture.ARM64) {
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.MACOS_X86_64), verify);
}
}
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, String platform, boolean verify) {
try (DirectoryStream<Path> dir = Files.newDirectoryStream(directory)) {
// component can be jre-legacy, java-runtime-alpha, java-runtime-beta, java-runtime-gamma or any other being added in the future.
for (Path component : dir) {
tryAddJavaInComponentDir(javaRuntimes, platform, component, verify);
}
} catch (IOException e) {
LOG.warning("Failed to list java-runtime directory " + directory, e);
}
}
private static void searchAllJavaInDirectory(Map<Path, JavaRuntime> javaRuntimes, Path directory) {
if (!Files.isDirectory(directory)) {
return;
}
void searchJavaInProgramFiles(String env, String defaultValue) {
String programFiles = Lang.requireNonNullElse(System.getenv(env), defaultValue);
Path path;
try {
path = Path.of(programFiles);
} catch (InvalidPathException ignored) {
return;
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
for (Path subDir : stream) {
tryAddJavaHome(javaRuntimes, subDir);
}
} catch (IOException e) {
LOG.warning("Failed to find Java in " + directory, e);
}
}
for (String vendor : KNOWN_VENDOR_DIRECTORIES) {
searchAllJavaInDirectory(path.resolve(vendor));
}
private static void searchJavaInProgramFiles(Map<Path, JavaRuntime> javaRuntimes, String env, String defaultValue) {
String programFiles = Lang.requireNonNullElse(System.getenv(env), defaultValue);
Path path;
try {
path = Paths.get(programFiles);
} catch (InvalidPathException ignored) {
return;
}
void searchJavaInMacJavaVirtualMachines(Path directory) {
if (!Files.isDirectory(directory)) {
return;
}
for (String vendor : new String[]{"Java", "BellSoft", "AdoptOpenJDK", "Zulu", "Microsoft", "Eclipse Foundation", "Semeru"}) {
searchAllJavaInDirectory(javaRuntimes, path.resolve(vendor));
}
}
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
for (Path subDir : stream) {
tryAddJavaHome(subDir.resolve("Contents/Home"));
}
} catch (IOException e) {
LOG.warning("Failed to find Java in " + directory, e);
}
private static void searchJavaInMacJavaVirtualMachines(Map<Path, JavaRuntime> javaRuntimes, Path directory) {
if (!Files.isDirectory(directory)) {
return;
}
// ==== Windows Registry Support ====
void queryJavaInRegistryKey(WinReg.HKEY hkey, String location) {
WinReg reg = WinReg.INSTANCE;
if (reg == null)
return;
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
for (Path subDir : stream) {
tryAddJavaHome(javaRuntimes, subDir.resolve("Contents/Home"));
}
} catch (IOException e) {
LOG.warning("Failed to find Java in " + directory, e);
}
}
for (String java : reg.querySubKeys(hkey, location)) {
if (!reg.querySubKeys(hkey, java).contains(java + "\\MSI"))
continue;
if (reg.queryValue(hkey, java, "JavaHome") instanceof String home) {
try {
tryAddJavaHome(Path.of(home));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path in system registry: " + home);
}
// ==== Windows Registry Support ====
private static void queryJavaInRegistryKey(Map<Path, JavaRuntime> javaRuntimes, WinReg.HKEY hkey, String location) {
WinReg reg = WinReg.INSTANCE;
if (reg == null)
return;
for (String java : reg.querySubKeys(hkey, location)) {
if (!reg.querySubKeys(hkey, java).contains(java + "\\MSI"))
continue;
Object home = reg.queryValue(hkey, java, "JavaHome");
if (home instanceof String) {
try {
tryAddJavaHome(javaRuntimes, Paths.get((String) home));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path in system registry: " + home);
}
}
}
}
}
@@ -36,7 +36,6 @@ 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;
@@ -69,6 +68,18 @@ 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);
@@ -326,6 +337,8 @@ public final class Accounts {
});
}
triggerAuthlibInjectorUpdateCheck();
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
continue;
@@ -333,7 +346,7 @@ public final class Accounts {
try {
server.fetchMetadataResponse();
} catch (IOException e) {
LOG.warning("Failed to fetch authlib-injector server metadata: " + server, e);
LOG.warning("Failed to fetch authlib-injector server metdata: " + server, e);
}
});
}
@@ -358,18 +371,24 @@ public final class Accounts {
// ==== authlib-injector ====
private static AuthlibInjectorArtifactProvider createAuthlibInjectorArtifactProvider() {
String authlibinjectorLocation = System.getProperty("hmcl.authlibinjector.location");
if (authlibinjectorLocation != null) {
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 {
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) {
@@ -414,12 +433,6 @@ public final class Accounts {
return i18n("account.failed.no_character");
} else if (exception instanceof ServerDisconnectException) {
if (exception.getCause() instanceof SSLException) {
if (exception.getCause().getMessage() != null && exception.getCause().getMessage().contains("Remote host terminated")) {
return i18n("account.failed.connect_authentication_server");
}
if (exception.getCause().getMessage() != null && (exception.getCause().getMessage().contains("No name matching") || exception.getCause().getMessage().contains("No subject alternative DNS name matching"))) {
return i18n("account.failed.dns");
}
return i18n("account.failed.ssl");
} else {
return i18n("account.failed.connect_authentication_server");
@@ -468,12 +481,10 @@ public final class Accounts {
}
} else if (exception instanceof MicrosoftService.XBox400Exception) {
return i18n("account.methods.microsoft.error.wrong_verify_method");
} else if (exception instanceof MicrosoftService.MinecraftJavaEditionLicenseNotFoundException) {
return i18n("account.methods.microsoft.error.no_license");
} else if (exception instanceof MicrosoftService.MinecraftJavaEditionProfileNotFoundException) {
} else if (exception instanceof MicrosoftService.NoMinecraftJavaEditionProfileException) {
return i18n("account.methods.microsoft.error.no_character");
} else if (exception instanceof MicrosoftService.NoXuiException) {
return i18n("account.methods.microsoft.error.add_family");
return i18n("account.methods.microsoft.error.add_family_probably");
} else if (exception instanceof OAuthServer.MicrosoftAuthenticationNotSupportedException) {
return i18n("account.methods.microsoft.snapshot");
} else if (exception instanceof OAuthAccount.WrongAccountException) {
@@ -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,7 +38,6 @@ 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";
@@ -74,7 +73,8 @@ public final class AuthlibInjectorServers implements Validation {
if (ConfigHolder.isNewlyCreated() && Files.exists(configLocation)) {
AuthlibInjectorServers configInstance;
try {
configInstance = JsonUtils.fromJsonFile(configLocation, AuthlibInjectorServers.class);
String content = FileUtils.readText(configLocation);
configInstance = JsonUtils.GSON.fromJson(content, 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,7 +107,6 @@ 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 {
@@ -164,7 +163,7 @@ public final class ConfigHolder {
LOG.warning("Failed to get owner");
}
try {
String content = Files.readString(configLocation);
String content = FileUtils.readText(configLocation);
Config deserialized = Config.fromJson(content);
if (deserialized == null) {
LOG.info("Config is empty");
@@ -193,7 +192,7 @@ public final class ConfigHolder {
private static GlobalConfig loadGlobalConfig() throws IOException {
if (Files.exists(GLOBAL_CONFIG_PATH)) {
try {
String content = Files.readString(GLOBAL_CONFIG_PATH);
String content = FileUtils.readText(GLOBAL_CONFIG_PATH);
GlobalConfig deserialized = GlobalConfig.fromJson(content);
if (deserialized == null) {
LOG.info("Config is empty");
@@ -21,126 +21,142 @@ 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.util.Lang;
import org.jackhuang.hmcl.ui.FXUtils;
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.URI;
import java.net.URL;
import java.nio.file.AccessDeniedException;
import java.util.List;
import java.util.Arrays;
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() {}
public static final String DEFAULT_AUTO_PROVIDER_ID = "balanced";
public static final String DEFAULT_DIRECT_PROVIDER_ID = "mojang";
private static DownloadProvider currentDownloadProvider;
private static final DownloadProviderWrapper PROVIDER_WRAPPER;
public static final Map<String, DownloadProvider> providersById;
public static final Map<String, DownloadProvider> rawProviders;
private static final AdaptedDownloadProvider fileDownloadProvider = new AdaptedDownloadProvider();
private static final DownloadProvider DEFAULT_PROVIDER;
public static final Map<String, DownloadProvider> DIRECT_PROVIDERS;
public static final Map<String, DownloadProvider> AUTO_PROVIDERS;
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;
static {
String bmclapiRoot = System.getProperty("hmcl.bmclapi.override", "https://bmclapi2.bangbang93.com");
BMCLAPIDownloadProvider bmclapiRaw = new BMCLAPIDownloadProvider(bmclapiRoot);
String bmclapiRoot = "https://bmclapi2.bangbang93.com";
String bmclapiRootOverride = System.getProperty("hmcl.bmclapi.override");
if (bmclapiRootOverride != null) bmclapiRoot = bmclapiRootOverride;
DownloadProvider mojang = new MojangDownloadProvider();
DownloadProvider bmclapi = new AutoDownloadProvider(bmclapiRaw, mojang);
DEFAULT_PROVIDER = mojang;
DIRECT_PROVIDERS = Lang.mapOf(
pair("mojang", mojang),
pair("bmclapi", bmclapi)
MOJANG = new MojangDownloadProvider();
BMCLAPI = new BMCLAPIDownloadProvider(bmclapiRoot);
rawProviders = mapOf(
pair("mojang", MOJANG),
pair("bmclapi", 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)
);
AdaptedDownloadProvider fileProvider = new AdaptedDownloadProvider();
fileProvider.setDownloadProviderCandidates(Arrays.asList(BMCLAPI, MOJANG));
BalancedDownloadProvider balanced = new BalancedDownloadProvider(MOJANG, BMCLAPI);
PROVIDER_WRAPPER = new DownloadProviderWrapper(DEFAULT_PROVIDER);
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());
}
static void init() {
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);
FXUtils.onChangeAndOperate(config().versionListSourceProperty(), versionListSource -> {
if (!providersById.containsKey(versionListSource)) {
config().setVersionListSource(DEFAULT_PROVIDER_ID);
return;
}
};
config().versionListSourceProperty().addListener(onChangeDownloadSource);
config().autoChooseDownloadTypeProperty().addListener(onChangeDownloadSource);
config().downloadTypeProperty().addListener(onChangeDownloadSource);
onChangeDownloadSource.invalidated(null);
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));
}
/**
* Get current primary preferred download provider
*/
public static DownloadProvider getDownloadProvider() {
return PROVIDER_WRAPPER;
return config().isAutoChooseDownloadType() ? currentDownloadProvider : fileDownloadProvider;
}
public static String localizeErrorMessage(Throwable exception) {
if (exception instanceof DownloadException) {
URI uri = ((DownloadException) exception).getUri();
URL url = ((DownloadException) exception).getUrl();
if (exception.getCause() instanceof SocketTimeoutException) {
return i18n("install.failed.downloading.timeout", uri);
return i18n("install.failed.downloading.timeout", url);
} else if (exception.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
return i18n("download.code." + responseCodeException.getResponseCode(), uri);
return i18n("download.code." + responseCodeException.getResponseCode(), url);
} else {
return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
}
} else if (exception.getCause() instanceof FileNotFoundException) {
return i18n("download.code.404", uri);
return i18n("download.code.404", url);
} else if (exception.getCause() instanceof AccessDeniedException) {
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
} else if (exception.getCause() instanceof ArtifactMalformedException) {
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.artifact_malformed");
} else if (exception.getCause() instanceof SSLHandshakeException && !(exception.getCause().getMessage() != null && exception.getCause().getMessage().contains("Remote host terminated"))) {
if (exception.getCause().getMessage() != null && (exception.getCause().getMessage().contains("No name matching") || exception.getCause().getMessage().contains("No subject alternative DNS name matching"))) {
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.dns.pollution");
}
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.ssl_handshake");
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.artifact_malformed");
} else if (exception.getCause() instanceof SSLHandshakeException) {
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.ssl_handshake");
} else {
return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
}
} else if (exception instanceof ArtifactMalformedException) {
return i18n("exception.artifact_malformed");
@@ -22,6 +22,5 @@ public enum EnumBackgroundImage {
CUSTOM,
CLASSIC,
NETWORK,
TRANSLUCENT, // Deprecated
PAINT
TRANSLUCENT
}
@@ -18,24 +18,26 @@
package org.jackhuang.hmcl.setting;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.Metadata;
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.*;
import java.util.Locale;
import java.util.Objects;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -52,21 +54,7 @@ public final class FontManager {
public static final double DEFAULT_FONT_SIZE = 12.0f;
private static final Lazy<Font> DEFAULT_FONT = new Lazy<>(() -> {
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);
Font font = tryLoadDefaultFont(Metadata.HMCL_CURRENT_DIRECTORY);
if (font != null)
return font;
@@ -85,43 +73,45 @@ public final class FontManager {
return font;
}
// Default
String fcMatchPattern;
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()
&& !(fcMatchPattern = I18n.getLocale().getFcMatchPattern()).isEmpty())
return findByFcMatch(fcMatchPattern);
&& Locale.getDefault() != Locale.ROOT
&& !"en".equals(Locale.getDefault().getLanguage()))
return findByFcMatch();
else
return null;
});
private static final ObjectProperty<FontReference> font = new SimpleObjectProperty<>();
private static final ObjectProperty<FontReference> fontProperty;
static {
updateFont();
LOG.info("Font: " + (font.get() != null ? font.get().family() : "System"));
}
private static void updateFont() {
String fontFamily = config().getLauncherFontFamily();
if (fontFamily == null)
fontFamily = System.getProperty("hmcl.font.override");
if (fontFamily == null)
fontFamily = System.getenv("HMCL_FONT");
FontReference fontReference;
if (fontFamily == null) {
Font defaultFont = DEFAULT_FONT.get();
font.set(defaultFont != null ? new FontReference(defaultFont) : null);
} else {
font.set(new FontReference(fontFamily));
}
fontReference = defaultFont != null ? new FontReference(defaultFont) : null;
} else
fontReference = new FontReference(fontFamily);
fontProperty = new SimpleObjectProperty<>(fontReference);
LOG.info("Font: " + (fontReference != null ? fontReference.getFamily() : "System"));
fontProperty.addListener((obs, oldValue, newValue) -> {
if (newValue != null)
config().setLauncherFontFamily(newValue.getFamily());
else
config().setLauncherFontFamily(null);
});
}
private static Font tryLoadDefaultFont(Path dir) {
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) {
@@ -137,47 +127,16 @@ public final class FontManager {
return null;
}
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) {
public static Font findByFcMatch() {
Path fcMatch = SystemUtils.which("fc-match");
if (fcMatch == null)
return null;
try {
String result = SystemUtils.run(fcMatch.toString(),
pattern,
":lang=" + Locale.getDefault().toLanguageTag(),
"--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);
@@ -193,69 +152,110 @@ public final class FontManager {
return null;
}
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;
}
if (JavaRuntime.CURRENT_VERSION >= 9) {
try {
MethodHandle methodHandle = MethodHandles.publicLookup().findStatic(Font.class, "loadFonts",
MethodType.methodType(Font[].class, String.class, double.class));
for (Font font : fonts) {
if (font.getFamily().equalsIgnoreCase(family)) {
return font;
}
}
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;
}
if (family.indexOf(',') >= 0) {
for (String candidateFamily : family.split(",")) {
for (Font font : fonts) {
if (font.getFamily().equalsIgnoreCase(candidateFamily)) {
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) {
}
}
LOG.warning(String.format("Family '%s' not found in font file '%s'", family, path));
return fonts[0];
Font font = Font.loadFont(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
if (font == null)
LOG.warning("Failed to load font from " + path);
return font;
} catch (Throwable e) {
LOG.warning("Failed to get default font with fc-match", e);
return null;
}
}
public static ReadOnlyObjectProperty<FontReference> fontProperty() {
return font;
public static ObjectProperty<FontReference> fontProperty() {
return fontProperty;
}
public static FontReference getFont() {
return font.get();
return fontProperty.get();
}
public static void setFont(FontReference font) {
fontProperty.set(font);
}
public static void setFontFamily(String fontFamily) {
config().setLauncherFontFamily(fontFamily);
updateFont();
setFont(fontFamily != null ? new FontReference(fontFamily) : null);
}
// https://github.com/HMCL-dev/HMCL/issues/4072
public record FontReference(@NotNull String family, @Nullable String style) {
public FontReference {
Objects.requireNonNull(family);
}
public static final class FontReference {
private final @NotNull String family;
private final @Nullable String style;
public FontReference(@NotNull String family) {
this(family, null);
this.family = Objects.requireNonNull(family);
this.style = null;
}
public FontReference(@NotNull Font font) {
this(font.getFamily(), font.getStyle());
this.family = font.getFamily();
this.style = font.getStyle();
}
public @NotNull String getFamily() {
return family;
}
public @Nullable String getStyle() {
return style;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof FontReference))
return false;
FontReference that = (FontReference) o;
return Objects.equals(family, that.family) && Objects.equals(style, that.style);
}
@Override
public int hashCode() {
return Objects.hash(family, style);
}
@Override
public String toString() {
return String.format("FontReference[family='%s', style='%s']", family, style);
}
}
private FontManager() {
}
}
@@ -19,139 +19,195 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
import javafx.beans.property.*;
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.collections.FXCollections;
import javafx.collections.ObservableSet;
import org.jackhuang.hmcl.util.gson.ObservableSetting;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Type;
import java.util.*;
@JsonAdapter(GlobalConfig.Adapter.class)
public final class GlobalConfig extends ObservableSetting {
@JsonAdapter(GlobalConfig.Serializer.class)
public final class GlobalConfig implements Observable {
@Nullable
public static GlobalConfig fromJson(String json) throws JsonParseException {
return Config.CONFIG_GSON.fromJson(json, GlobalConfig.class);
GlobalConfig loaded = Config.CONFIG_GSON.fromJson(json, GlobalConfig.class);
if (loaded == null) {
return null;
}
GlobalConfig instance = new GlobalConfig();
PropertyUtils.copyProperties(loaded, instance);
instance.unknownFields.putAll(loaded.unknownFields);
return instance;
}
private final IntegerProperty agreementVersion = new SimpleIntegerProperty();
private final IntegerProperty platformPromptVersion = new SimpleIntegerProperty();
private final IntegerProperty logRetention = new SimpleIntegerProperty();
private final BooleanProperty enableOfflineAccount = new SimpleBooleanProperty(false);
private final ObservableSet<String> userJava = FXCollections.observableSet(new LinkedHashSet<>());
private final ObservableSet<String> disabledJava = FXCollections.observableSet(new LinkedHashSet<>());
private final Map<String, Object> unknownFields = new HashMap<>();
private final transient ObservableHelper helper = new ObservableHelper(this);
public GlobalConfig() {
register();
PropertyUtils.attachListener(this, helper);
}
@Override
public void addListener(InvalidationListener listener) {
helper.addListener(listener);
}
@Override
public void removeListener(InvalidationListener listener) {
helper.removeListener(listener);
}
public String toJson() {
return Config.CONFIG_GSON.toJson(this);
}
@SerializedName("agreementVersion")
private final IntegerProperty agreementVersion = new SimpleIntegerProperty();
public int getAgreementVersion() {
return agreementVersion.get();
}
public IntegerProperty agreementVersionProperty() {
return agreementVersion;
}
public int getAgreementVersion() {
return agreementVersion.get();
}
public void setAgreementVersion(int agreementVersion) {
this.agreementVersion.set(agreementVersion);
}
@SerializedName("terracottaAgreementVersion")
private final IntegerProperty terracottaAgreementVersion = new SimpleIntegerProperty();
public IntegerProperty terracottaAgreementVersionProperty() {
return terracottaAgreementVersion;
public int getPlatformPromptVersion() {
return platformPromptVersion.get();
}
public int getTerracottaAgreementVersion() {
return terracottaAgreementVersion.get();
}
public void setTerracottaAgreementVersion(int terracottaAgreementVersion) {
this.terracottaAgreementVersion.set(terracottaAgreementVersion);
}
@SerializedName("platformPromptVersion")
private final IntegerProperty platformPromptVersion = new SimpleIntegerProperty();
public IntegerProperty platformPromptVersionProperty() {
return platformPromptVersion;
}
public int getPlatformPromptVersion() {
return platformPromptVersion.get();
}
public void setPlatformPromptVersion(int platformPromptVersion) {
this.platformPromptVersion.set(platformPromptVersion);
}
@SerializedName("logRetention")
private final IntegerProperty logRetention = new SimpleIntegerProperty(20);
public int getLogRetention() {
return logRetention.get();
}
public IntegerProperty logRetentionProperty() {
return logRetention;
}
public int getLogRetention() {
return logRetention.get();
}
public void setLogRetention(int logRetention) {
this.logRetention.set(logRetention);
}
@SerializedName("enableOfflineAccount")
private final BooleanProperty enableOfflineAccount = new SimpleBooleanProperty(false);
public boolean isEnableOfflineAccount() {
return enableOfflineAccount.get();
}
public BooleanProperty enableOfflineAccountProperty() {
return enableOfflineAccount;
}
public boolean isEnableOfflineAccount() {
return enableOfflineAccount.get();
}
public void setEnableOfflineAccount(boolean value) {
enableOfflineAccount.set(value);
}
@SerializedName("fontAntiAliasing")
private final StringProperty fontAntiAliasing = new SimpleStringProperty();
public StringProperty fontAntiAliasingProperty() {
return fontAntiAliasing;
}
public String getFontAntiAliasing() {
return fontAntiAliasing.get();
}
public void setFontAntiAliasing(String value) {
this.fontAntiAliasing.set(value);
}
@SerializedName("userJava")
private final ObservableSet<String> userJava = FXCollections.observableSet(new LinkedHashSet<>());
public ObservableSet<String> getUserJava() {
return userJava;
}
@SerializedName("disabledJava")
private final ObservableSet<String> disabledJava = FXCollections.observableSet(new LinkedHashSet<>());
public ObservableSet<String> getDisabledJava() {
return disabledJava;
}
static final class Adapter extends ObservableSetting.Adapter<GlobalConfig> {
public static final class Serializer implements JsonSerializer<GlobalConfig>, JsonDeserializer<GlobalConfig> {
private static final Set<String> knownFields = new HashSet<>(Arrays.asList(
"agreementVersion",
"platformPromptVersion",
"logRetention",
"userJava",
"disabledJava",
"enableOfflineAccount"
));
@Override
protected GlobalConfig createInstance() {
return new GlobalConfig();
public JsonElement serialize(GlobalConfig src, Type typeOfSrc, JsonSerializationContext context) {
if (src == null) {
return JsonNull.INSTANCE;
}
JsonObject jsonObject = new JsonObject();
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
jsonObject.add("platformPromptVersion", context.serialize(src.getPlatformPromptVersion()));
jsonObject.add("logRetention", context.serialize(src.getLogRetention()));
if (src.enableOfflineAccount.get())
jsonObject.addProperty("enableOfflineAccount", true);
if (!src.getUserJava().isEmpty())
jsonObject.add("userJava", context.serialize(src.getUserJava()));
if (!src.getDisabledJava().isEmpty())
jsonObject.add("disabledJava", context.serialize(src.getDisabledJava()));
for (Map.Entry<String, Object> entry : src.unknownFields.entrySet()) {
jsonObject.add(entry.getKey(), context.serialize(entry.getValue()));
}
return jsonObject;
}
@Override
public GlobalConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (!(json instanceof JsonObject)) return null;
JsonObject obj = (JsonObject) json;
GlobalConfig config = new GlobalConfig();
config.setAgreementVersion(Optional.ofNullable(obj.get("agreementVersion")).map(JsonElement::getAsInt).orElse(0));
config.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));
JsonElement userJava = obj.get("userJava");
if (userJava != null && userJava.isJsonArray()) {
for (JsonElement element : userJava.getAsJsonArray()) {
config.userJava.add(element.getAsString());
}
}
JsonElement disabledJava = obj.get("disabledJava");
if (disabledJava != null && disabledJava.isJsonArray()) {
for (JsonElement element : disabledJava.getAsJsonArray()) {
config.disabledJava.add(element.getAsString());
}
}
for (Map.Entry<String, JsonElement> entry : obj.entrySet()) {
if (!knownFields.contains(entry.getKey())) {
config.unknownFields.put(entry.getKey(), context.deserialize(entry.getValue(), Object.class));
}
}
return config;
}
}
}
Loaded 100 of 850 files, more files were not shown because too many files have changed in this diff. Show more