Compare commits

...
Author SHA1 Message Date
Glavo 0a230ef6b5 优化 Release Note (#3472) 2024-11-23 19:48:46 +08:00
Burning_TNT c12ae9fdf8 Fix #3460 无法识别奇异的 Forge (Like) 模组 Metadata (#3470) 2024-11-21 21:22:20 +08:00
Glavo 773ef4282c Bump action-gh-release to v2 (#3469) 2024-11-17 22:09:29 +08:00
Burning_TNTandYELANDAOKONG 9edfe29659 实现 #2969: 启动前日志列出mods目录的jar文件 (#3125)
* Support #2969: 启动前日志列出mods目录的jar文件

* Support #2969: 启动前日志列出mods目录jar文件

* Support #2969: 启动前日志中使用字符画列出mods目录模组文件

* Support #2969: 启动前日志中使用字符画列出mods目录模组文件

* Support #2969: 启动前日志中使用字符画列出mods目录模组文件

* Support #2969: 日志列出mods目录时不再过滤文件后缀

* Support #2969: 启动时日志列出mods目录(代码优化)

* Support #2969: 启动时日志列出mods目录(不再列出Zip内容)

* Support #2969

* Fix: checkstyle

---------

Co-authored-by: YELANDAOKONG <yelandaokong@yldk.xyz>
2024-11-16 23:29:53 +08:00
Burning_TNT 5591d92e25 Fix #2991: 导入 PrismLauncher 格式整合包时 $.jvmArgs 参数未正确移除两侧引号 (#3030) 2024-11-15 22:49:30 +08:00
Glavo 65bdb29e71 在 macOS 上使用 JavaFX 19.0.2.1 (#3461) 2024-11-15 18:02:59 +08:00
Glavo 355df211db 在游戏下载列表中添加 Minecraft Wiki 链接 (#3435) 2024-11-14 17:49:27 +08:00
3gf8jv4dv fa66549f53 Revise English (#3453)
- Adjust some translation styles again
- Fix some missed mistakes
2024-11-14 09:59:15 +08:00
3gf8jv4dv 6e48dbd6c6 Fix dialog title when modpack download fails (#3451)
* Fix dialog title when modpack download fails

* Update
2024-11-14 09:58:14 +08:00
Zkitefly e3c7278759 Update check-update.yml (#3452) 2024-11-14 09:42:43 +08:00
3gf8jv4dv dd1829d8ca Check word 其它 → 其他 when checking translation (#3455)
There is some controversy over the use of this word.
In order to ensure consistency in formal texts, we
should unify them to more tried-and-true and commonly
used usages.

By checking at build time, it lets the translator
or proofreader correct the word on his/her own without
any special attention from the reviewer.
2024-11-14 09:27:45 +08:00
3gf8jv4dv 60ac16a3ea Revise Chinese (#3454)
* Revise Chinese

- Simplified Chinese only
- Minor revision

* oops...
2024-11-13 22:14:55 +08:00
Glavo b400a9a3ef 使最小化按钮居中 (#3450)
* 修复最小化按钮未居中的问题

* update
2024-11-12 14:24:15 +08:00
3gf8jv4dv c7cc2d0733 Fix missing i18n when checking for updates fails (#3444)
* Fix missing i18n when checking for updates fails

* Revise translation
2024-11-11 19:20:28 +08:00
Glavo 861223e909 清理 GameJavaVersion (#3441)
* 清理 GameJavaVersion

* update
2024-11-11 02:30:41 +08:00
Glavo 70ecbc734e Fix #3438: 修复 Minecraft 1.20.2+Forge 默认图标错误的问题 (#3439) 2024-11-10 23:19:14 +08:00
ZkiteflyandGlavo 2ca7e7c830 Fix #3200: 启动时不应补全 Modrinth 整合包中已禁用的 mod (#3308)
* Update ModrinthCompletionTask.java

* Update ModrinthCompletionTask.java

* Update ModrinthCompletionTask.java

* Update ModrinthCompletionTask.java

* Update ModrinthCompletionTask.java

* update

* update

* update

* update

* update

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-11-10 21:27:36 +08:00
Wulian 9a0db56ac3 修复 Copyright © 显示为“漏”的bug (#3437)
Fixes #3436

Specify UTF-8 charset when reading `deps.json` file in `AboutPage.java`.

---

For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/HMCL-dev/HMCL/issues/3436?shareId=XXXX-XXXX-XXXX-XXXX).
2024-11-10 19:02:59 +08:00
Glavo c1ae5875ba 将 VERSION_ROOT 更新至 3.6 (#3431) 2024-11-10 13:01:50 +08:00
Glavo 1d0ae427bf Update JavaVersionConstraint: 1.20.5 requires Java 21 (#3434) 2024-11-09 19:39:09 +08:00
3gf8jv4dv 3dde2d19e2 Fix extra "=" in zh_CN launch.advice translation (#3430)
Closes #3429
2024-11-08 05:59:02 +08:00
3gf8jv4dv d5ec6d7b85 Revise i18n translation before releases (#3428)
- Fix some English grammar and text style
- Revise `Allow Cheats` to `Allow Commands/Cheats`
  - Due to the change from `Allow Cheats` to `Allow Commands` by Mojang in 24w04a.
  - https://minecraft.wiki/w/Java_Edition_24w04a
2024-11-07 21:37:59 +08:00
Glavo 13856b71a0 更新 HMCLauncher.exe (#3427) 2024-11-05 03:43:18 +08:00
Glavo c6658771ca 修复 HMCLauncher 无法正常识别 Windows on Arm 平台的问题 (#3425)
* update

* update

* update
2024-11-05 00:20:42 +08:00
Glavo 406f36584d 设置 FontComboBox 的最小宽度 (#3424) 2024-11-04 18:55:04 +08:00
Glavo 898750d058 修复对话框按钮自行移动的问题 (#3423)
* 修复对话框按钮自行移动的问题

* update

* update

* update
2024-11-04 18:41:14 +08:00
Glavo 09e9e934d3 Fix #3412: 修复点击前置模组时,模组下载对话框不会关闭的问题 (#3422) 2024-11-04 06:00:45 +08:00
Glavo 3d170267b7 Fix #3416: 修复打开游戏设置页时崩溃的问题 (#3421) 2024-11-04 05:34:28 +08:00
3gf8jv4dv 3ca7e3cf88 Fix incorrect i18n key and line breaks in es & ja (#3404)
* Fix incorrect i18n key and line breaks in es & ja

* fix
2024-11-03 17:03:41 +08:00
3gf8jv4dv 1378d1445a README & PLATFORM docs post-fixes (#3417) 2024-11-02 17:36:41 +08:00
3gf8jv4dv 16d304dbb4 en_US Post-fix (#3413)
* en_US Post-fix

Some minor fixes.

* fix `game.crash.feedback`

* oops...

* oops...

* Bunch of grammar fixes :/

* Another bunch fixes

* Minor fixes

* fix

* fix

* fix
2024-11-02 17:36:20 +08:00
3gf8jv4dv aff5db7759 Change the terminology %AppData% to %APPDATA% (#3420) 2024-11-02 02:27:54 +08:00
Glavo 0697aa9755 重命名包 moe.mickey.minecraft.skin.fx (#3419) 2024-11-02 01:05:52 +08:00
Glavo 995d3bd4b9 不再生成 bss 文件 (#3418) 2024-11-01 20:59:33 +08:00
Glavo 3bf5204e79 优化 Theme (#3414) 2024-11-01 02:12:16 +08:00
3gf8jv4dv 0b1ce6e94a Adjust language selection order (#3411)
Sort alphabetically by language code.
2024-10-31 23:30:44 +08:00
3gf8jv4dv a66ccd7385 Adjust Microsoft login related i18n (#3393)
* Adjust Microsoft login related i18n

- Adjust English and Chinese
- Fix typos and grammatical errors

* fix
2024-10-30 02:26:55 +08:00
3gf8jv4dv 7a3f125802 Adjust custom command hint (zh_CN only) (#3394)
I forgot to change the zh_CN part.
A half-width colon is better.
2024-10-30 02:22:34 +08:00
3gf8jv4dv 4a8dcce3d4 Adjust about page i18n (#3398)
* Adjust about page i18n

- Adjust all languages
- Add missing translations

* fix

* fix
2024-10-30 02:21:54 +08:00
3gf8jv4dv adcc3eb43f Adjust buttons, chat & crash i18n (#3401)
* Adjust buttons, chat & crash i18n

- Add missing translations

* fix

* fix

* fix

* fix ja

* fix es & ja
2024-10-30 02:20:39 +08:00
3gf8jv4dv 8bbd1a7cf4 Remove unused i18n keys in es, ja & ru (#3405) 2024-10-30 02:19:45 +08:00
Zkitefly 467ff26278 模组管理添加右键查看模组详情 (#3232)
* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java
2024-10-30 02:19:16 +08:00
Glavo d6a267afa4 优化 FXUtils.installTooltip (#3407)
* update

* Fix checkstyle

* update

* update

* update

* update

* update
2024-10-28 01:52:38 +08:00
Glavo 743e934436 优化“复制”图标 (#3406) 2024-10-28 00:29:29 +08:00
3gf8jv4dv f89ac2231a Adjust Java management i18n (#3402)
- Add missing languages.
2024-10-27 22:07:25 +08:00
3gf8jv4dv 1877a18ebb Fix en language missing a line break (#3403) 2024-10-27 22:07:03 +08:00
Glavo c99f85d9d5 缩小复制图标 (#3397)
* update

* update
2024-10-27 00:21:31 +08:00
Glavo 228942c60a 优化删除账号按钮图标 (#3396) 2024-10-26 23:49:43 +08:00
Glavo fecd567eab 简化 ContainerAnimations (#3395) 2024-10-26 23:34:41 +08:00
3gf8jv4dv 602840519b Change forgot password link (#3392) 2024-10-26 23:32:12 +08:00
959adf1075 更改主页 UI (#3129)
* Change root page UI

* Update HMCL/src/main/resources/assets/lang/I18N.properties

Co-authored-by: 3gf8jv4dv <158490944+3gf8jv4dv@users.noreply.github.com>

* Shorten I18N

* update

---------

Co-authored-by: 3gf8jv4dv <158490944+3gf8jv4dv@users.noreply.github.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-26 21:02:34 +08:00
Glavo 422f681fe6 Bump XZ to 1.10 (#3391) 2024-10-26 20:54:48 +08:00
Glavo 474d298fd3 Bump Gradle to 8.10.2 (#3390) 2024-10-26 13:52:49 +08:00
3gf8jv4dv 4052136217 Adjust sponsor hint i18n (#3387)
- Adjust for all languages.
- For BMCLAPI: Make it clear that this is a service
  for users in Chinese Mainland.
- For HMCL: Since the page that was opened was not
  Afdian but the GitHub repo page, I made adjustments
  based on the actual situation.
- Fix some typos.
2024-10-26 02:51:19 +08:00
3gf8jv4dv 3193e02867 Adjust feedback page i18n (#3389)
- Adjust for all languages.
- Add missing translations to some languages.
2024-10-26 02:49:58 +08:00
Glavo c7122b3fff 优化首页公告 (#3378)
* update

* 添加公告隐藏动画

* update

* update
2024-10-26 02:45:58 +08:00
3gf8jv4dv 8775194d26 Adjust i18n for Linux/FreeBSD-specific settings in Advanced Settings (#3388)
I checked the code and it looked like this was a
Linux/FreeBSD-specific feature, so I adjusted the
translation for that.
2024-10-26 01:14:28 +08:00
3gf8jv4dv b3f27cc96e Adjust homepage hint typesetting (#3382)
- Adjustments made for all languages.
- Remove outdated hidden hint links.
2024-10-25 22:30:12 +08:00
3gf8jv4dv 3313a1ed27 For language selection, remove all region descriptions (#3383)
Currently, HMCL does not provide variants for
languages ​​(e.g. US, UK, etc.). For Chinese, there
are already descriptions of Simplified (简体) and
Traditional (繁體) to distinguish them, so keep them.
2024-10-25 22:05:19 +08:00
3gf8jv4dv da66622994 Adjust Microsoft login hint (zh_CN only) (#3384) 2024-10-25 22:04:48 +08:00
3gf8jv4dv 446cdc32d7 Adjust language file header comments (#3385)
- Update copyright year
- Unify typesetting style
2024-10-25 22:03:52 +08:00
3gf8jv4dv b08d2b0dc0 Remove some unnecessary tooltips (#3386)
* Remove some unnecessary tooltips

These texts are basically fixed, and the problem of
too long texts can be solved by revising the translation
for different languages.

Currently, for both Chinese and English, the i18n
keys involved in this PR will not be partially obscured
due to long text, so their tooltips are removed.

* fix checkstyle
2024-10-25 16:11:16 +08:00
Glavo e051c212d1 支持使用鼠标返回键返回上级 (#3381)
* 支持使用鼠标返回键返回上级

* fix checkstyle
2024-10-25 12:36:07 +08:00
Glavo 7645a600ef 修复部分控件响应任意鼠标按键点击事件的问题 (#3380)
* update

* update

* update

* update

* update

* update

* update

* update

* update
2024-10-25 12:24:57 +08:00
Glavo 9e56b5cd15 清理对 setOnMouseClicked 的误用 (#3379) 2024-10-25 04:08:40 +08:00
3gf8jv4dvandGlavo dcfcd1d8b2 Revise zh_TW part 3 (#3358)
* Revise zh_TW part 3

* Update

* Update

* fix typo

* fix

* fix

* oops...

* fix

* fix main page hint

* Update HMCL/src/main/resources/assets/lang/I18N_zh.properties

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-25 02:25:26 +08:00
3gf8jv4dv b2dc4b45c2 Revise zh_TW part 2 (#3357)
* Revise zh_TW part 2

* Update

* fix typo

* fix

* fix

* fix

* fix

When choosing a download source, the commas in the BMCLAPI item are changed to half-width.
2024-10-25 01:47:22 +08:00
0314921728 Revise en_US (#3367)
* Revise en_US

* fix

* fix

* Revise

Co-authored-by: Zkitefly <2573874409@qq.com>

* fix some English grammar

* another English grammar fixes

* fix fix fix...

* Revise

Co-authored-by: Glavo <zjx001202@gmail.com>

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: Zkitefly <2573874409@qq.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-25 01:28:02 +08:00
Glavo 7d339677e4 调整公告中超链接的风格 (#3377)
* 调整 MessageDialogPane 中超链接的风格

* Fix checkstyle

* fix
2024-10-24 21:50:05 +08:00
3gf8jv4dv c9d259d7f1 zh_CN post-fix (#3376)
- When choosing a download source, the commas in the BMCLAPI item are changed to half-width.
- Remove extra line breaks in the home page hint.
2024-10-24 21:32:37 +08:00
Glavo eae2670855 支持在启动器内上传微软账号皮肤 (#3375)
* update

* Fix clearCache
2024-10-24 20:03:21 +08:00
3gf8jv4dvandGlavo e857f19b18 Revise zh_CN part 4 (#3349)
* Revise zh_CN part 4

* Update

* fix some typo

* Revise

Co-authored-by: Glavo <zjx001202@gmail.com>

* Revise

Co-authored-by: Glavo <zjx001202@gmail.com>

* Revise

Co-authored-by: Glavo <zjx001202@gmail.com>

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-24 14:25:37 +08:00
Glavo a11816884c 创建 @FXThread 注解 (#3374) 2024-10-24 01:01:31 +08:00
3gf8jv4dv f686cb0ceb Update versions.txt (#3373) 2024-10-23 23:00:02 +08:00
Glavo 5aac4b82fc 优化 Java 下载功能 (#3371)
* update

* update

* update

* update
2024-10-23 21:49:35 +08:00
Glavo 99c3ef8a57 优化 Java 下载功能 (#3369)
* update

* update

* update
2024-10-20 22:26:38 +08:00
3gf8jv4dv 0a17aad9d6 Revise readme (#3362) 2024-10-20 21:36:01 +08:00
lijiaxuanand3gf8jv4dv 965995ad20 略微修改了一下英文版的readme.md (#3354)
* Updated English version of readme.md

修改了英文版的readme.md,并未增加或删除内容,只是让一些句子更加自然了一些

* Update README.md

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2024-10-20 21:35:27 +08:00
Glavo a5067cb1dd Bump JavaFX to 17.0.13 (#3364) 2024-10-20 21:33:55 +08:00
3gf8jv4dv 1f9c0736aa Capitalize choose player dialog title text (#3368) 2024-10-20 21:33:35 +08:00
Glavo a27671560d 支持下载 OpenJ9 (#3363)
* 支持下载 OpenJ9

* update
2024-10-19 22:59:40 +08:00
Glavo 5c767deaf3 优化 WorldInfoPage (#3361)
* update

* update

* update
2024-10-19 22:25:47 +08:00
3gf8jv4dv c157d4e0d1 Revise zh_TW part 4 (#3359) 2024-10-19 21:48:30 +08:00
3gf8jv4dv 655f42734a Revise zh_TW part 1 (#3356)
* Revise zh_TW part 1

* Update

* Update

* Update
2024-10-19 21:41:54 +08:00
3gf8jv4dv 55b01ab101 Revise zh_CN part 3 (#3348)
* Revise zh_CN part 3

* Update
2024-10-19 21:35:57 +08:00
3gf8jv4dv c6ed42d3e6 Revise zh_CN part 2 (#3346)
* Revise zh_CN part 2

* Update

* Oops...
2024-10-19 21:35:22 +08:00
0562051989 Fix mod update channel i18n (#3331)
* Fix mod update channel i18n

* Update

Co-authored-by: Burning_TNT <pangyl08@163.com>

* update

---------

Co-authored-by: Burning_TNT <pangyl08@163.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-19 21:34:11 +08:00
3gf8jv4dvandBurning_TNT f68e056e47 Revise zh_CN part 1 (#3343)
* Revise zh_CN part 1

* Update

Co-authored-by: Burning_TNT <pangyl08@163.com>

---------

Co-authored-by: Burning_TNT <pangyl08@163.com>
2024-10-19 21:28:03 +08:00
Burning_TNTandGlavo 457490276c Update CallerFinder (#3350)
* Update CallerFinder

* Revert "Fix #2687 (#2688)"

This reverts commit 207b4d0033.

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-18 02:17:47 +08:00
Glavo 49ea9e5da0 fix checkstyle (#3353) 2024-10-17 12:48:17 +08:00
Glavo 9f3fb7aff6 修复无法安装 zip 格式的 Java 压缩包的问题 (#3352) 2024-10-17 04:20:09 +08:00
Glavo 1033389c38 Revert "Bump commons-compress to 1.27.1 (#3344)" (#3351)
This reverts commit fab32a6d51.
2024-10-16 22:15:13 +08:00
Glavo 58e32c4459 当整合包没有描述时,隐藏查看整合包描述按钮 (#3347) 2024-10-15 19:56:32 +08:00
Glavo 936285847c Bump Gson to 2.11.0 (#3345) 2024-10-15 19:10:02 +08:00
Glavo fab32a6d51 Bump commons-compress to 1.27.1 (#3344) 2024-10-15 19:05:22 +08:00
Glavo 5b345affec 优化 Java 下载功能 (#3342)
* update

* update
2024-10-15 18:47:57 +08:00
3gf8jv4dv 32c3b694b3 Revise readme and platform docs [skip ci] (#3054)
* Revise [skip ci]

Squash previous commits.

* Revise [skip ci]

Unify badge style.

* Revise [skip ci]

Change the badge style of QQ group

* Revise [skip ci]

- Add Chinese variants (Traditional, Taiwan)
- Compilation requirements for Java bump to 11+
- Various revisions

* Revise [skip ci]

Fix a link

* Revise [skip ci]

Revert a change

* Revise [skip ci]

* Revise [skip ci]

* Revise [skip ci]
2024-10-15 18:47:38 +08:00
3gf8jv4dv 4dce13855a Fix some i18n key mistake for es, ja & ru (#3341) 2024-10-15 18:26:12 +08:00
Burning_TNTandGlavo 1ce9858893 Support #2992: 检查模组更新界面加入全选/取消全选复选框 (#3032)
Support #2992

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-15 04:06:40 +08:00
Glavo 71c9632d61 GitHub Action 使用 Java 8 构建 HMCL (#3340)
* update

* update
2024-10-15 03:13:35 +08:00
3gf8jv4dv cce17edaa6 Capitalize text above the sidebar line (#3336) 2024-10-15 01:24:44 +08:00
3gf8jv4dvandGlavo deeef8f67a Download source and thread setting titles i18n key (#3338)
* Change the download source and thread setting titles to the appropriate i18n key

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-15 01:19:11 +08:00
3gf8jv4dv 25c76a748f Fix alignment of host and port in proxy settings (#3332) 2024-10-15 01:06:21 +08:00
3gf8jv4dv 2109df8145 Fix some link (#3334)
* Fix some link

* Revise
2024-10-15 00:58:40 +08:00
3gf8jv4dv ae01a49cc4 Fix some error message wrap (#3333)
* Fix some error message wrap

* Fix checkstyle
2024-10-15 00:57:23 +08:00
Glavo a4f5e013cb 修复使用 Java 8 构建失败的问题 (#3326) 2024-10-14 01:24:14 +08:00
Glavo d4579852bb 优化 AboutPage (#3325)
* 优化关于页面

* update
2024-10-14 01:08:06 +08:00
Zkitefly edd95ad626 模组管理搜索操作更改为文本更新后触发 (#3221)
* 更改为文本更新后触发

* 添加防抖
2024-10-13 21:57:32 +08:00
ZkiteflyandGlavo dec307b531 安装新版本的版本名输入框显示已选择加载器名称 (#3234)
* Update InstallersPage.java

* 优化代码

* Update InstallersPage.java

* Update InstallersPage.java

* fix: checkstyle

* 给 LibraryAnalyzer.LibraryType.fromPatchId 稍微优化一下,把她里面的实现改为一个预先生成的 map,

相信你知道怎么把这个函数改成一个 lookupMap.get(patchId) 的形式

* Update InstallersPage.java

* Update InstallersPage.java

* Fix

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-13 21:46:48 +08:00
Glavo 00b7085370 更新 Linux LoongArch64 支持 (#3264) 2024-10-13 20:54:15 +08:00
Glavo 17c1998119 修复对话框标题和按钮未使用自定义字体的问题 (#3324) 2024-10-13 20:39:55 +08:00
Glavo 1c05cd981f 使用 ./gradlew run 启动 HMCL 时解析 HMCL_JAVA_OPTS 环境变量 (#3323) 2024-10-13 20:20:05 +08:00
Glavo e9c3795ffc 预生成 openjfx-dependencies.json (#3322) 2024-10-13 19:52:00 +08:00
Zkiteflyand3gf8jv4dv c3917082ee 添加打开启动器日志文件夹按钮 (#3237)
* 添加打开启动器日志文件夹

* Update HMCL/src/main/resources/assets/lang/I18N.properties

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>

* Update HMCL/src/main/resources/assets/lang/I18N_zh.properties

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>

* Update HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsView.java

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>

* Update HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2024-10-13 19:37:35 +08:00
3gf8jv4dv 6265580d75 Adjust the order of sidebars in instance settings (#3278) 2024-10-13 19:35:24 +08:00
YLDK 65fc3d0c4f Support #3087: 隐藏整合包文件位置显示 (#3088)
* Support #3087: 隐藏整合包文件位置显示

* Support #3087: 移除整合包文件位置显示代码

* Support #3087: 移除整合包文件位置显示代码
2024-10-13 19:29:02 +08:00
Glavo 1c864ba072 使用 Jsoup 解析并渲染 HTML 页面 (#3321) 2024-10-13 19:19:52 +08:00
Glavo ef9159666c 使用 TypeToken 替代 Type (#3320) 2024-10-11 20:50:58 +08:00
Glavo 541f2e2619 清理不再使用的 i18n 条目 (#3318) 2024-10-09 02:00:53 +08:00
Glavo c7f248a092 优化更新窗口 (#3317)
* 优化更新窗口

* fix NPE
2024-10-09 01:40:26 +08:00
Zkitefly 7937f8ddf5 发现更新弹窗添加取消按钮 (#3282) 2024-10-09 01:03:56 +08:00
Glavo 74926704bd Close #3209: 搜索 IntelliJ IDEA 安装的 Java (#3316) 2024-10-09 00:35:07 +08:00
Glavo 9196bda537 优化 FXUtils.testLinuxCommand (#3315) 2024-10-09 00:19:06 +08:00
Glavo 4c1e607b8e 在 Linux/FreeBSD 平台读取 os-release (#3314)
* 简化 OperatingSystem.getPhysicalMemoryStatus

* Read os-release

* update

* update
2024-10-07 00:57:35 +08:00
Zkitefly 26224ae366 删除账户和删除认证服务器时弹出提示确认删除 (#3280) 2024-10-06 23:38:48 +08:00
Glavo d3e9511aca 删除 FractureiserDetector (#3313) 2024-10-06 23:31:19 +08:00
ShulkerSakura b624cebc08 修改下载页面按钮名称 (#3262) 2024-10-06 23:21:12 +08:00
Glavo 7e4d437a1d 重构 Java 管理 (#2988)
* update

* update

* Update task name

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* Update logo
2024-10-05 23:50:14 +08:00
Zkitefly 37d6857b82 根据版本类型调整下载项图标 (#3306) 2024-10-03 00:31:06 +08:00
Zkitefly f8d7c92d51 Close #3208: 隐藏版本更新界面的Fabric/Quilt API选项 (#3271) 2024-09-24 03:32:02 +08:00
YSXX1013andGlavo ea9999a1f9 支持使用 CMake 与 MinGW 构建 HMCLauncher (#3011)
* 尝试性的为HMCLauncher添加cmake及gcc支持

* 完善cmake及gcc支持

* 尝试支持Github Actions

* 尝试支持Github Actions-2

* 尝试支持Github Actions-3

* 尝试支持Github Actions-4

* 尝试支持Github Actions-5

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-09-24 02:51:44 +08:00
3gf8jv4dv d969fab284 Fix checkstyle (#3292) 2024-09-19 17:42:58 +08:00
vochant 97fe52c2a7 feat: Supported JRE from SDKMAN! on Linux (#3062) 2024-09-19 03:09:05 +08:00
Glavo 8a816f7f35 优化 LogWindow (#3274)
* update

* update

* update

* update

* update
2024-09-02 20:54:33 +08:00
6409841cca Fix #3138: HMCL 错误地将 Authlib 日志识别为 ERROR (#3142)
* Close#3138

* 恢复gradle-wrapper.properties

* 恢复gradle-wrapper.properties

* 添加空格

* 修复控制台输出的日志

* Merge if statement

* update

* update

---------

Co-authored-by: CiLuQwQ <a@>
Co-authored-by: burningtnt <pangyl08@163.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2024-09-02 01:51:53 +08:00
ZkiteflyandGlavo 6348166dac 支持在 Linux/BSD 平台使用 showFileInExplorer (#3233)
* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update FXUtils.java

* Update ModListPageSkin.java

* 使用 dbus-send

* Update FXUtils.java

* Update FXUtils.java

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-09-01 00:27:06 +08:00
ZkiteflyandGlavo 48417b9b7b 显示 Forge 发版时间 (#3256)
* 显示时间

* Fix

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-08-31 00:44:43 +08:00
237 changed files with 10386 additions and 4760 deletions

No files matched your search

+13 -8
View File
@@ -54,12 +54,14 @@ jobs:
echo "" >> RELEASE_NOTE
echo "| File Name | SHA-256 Checksum |" >> RELEASE_NOTE
echo "| --- | --- |" >> RELEASE_NOTE
echo "| HMCL-$HMCL_VERSION.exe | $(cat HMCL-$HMCL_VERSION.exe.sha256) |" >> RELEASE_NOTE
echo "| HMCL-$HMCL_VERSION.jar | $(cat HMCL-$HMCL_VERSION.jar.sha256) |" >> RELEASE_NOTE
echo "| HMCL-$HMCL_VERSION.sh | $(cat HMCL-$HMCL_VERSION.sh.sha256) |" >> 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@v1
uses: softprops/action-gh-release@v2
with:
body_path: RELEASE_NOTE
files: |
@@ -69,6 +71,7 @@ jobs:
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
name: ${{ env.HMCL_TAG_NAME }}
tag_name: ${{ env.HMCL_TAG_NAME }}
prerelease: ture
stable-check-update:
if: ${{ github.repository_owner == 'HMCL-dev' }}
needs: dev-check-update
@@ -118,12 +121,14 @@ jobs:
echo "" >> RELEASE_NOTE
echo "| File Name | SHA-256 Checksum |" >> RELEASE_NOTE
echo "| --- | --- |" >> RELEASE_NOTE
echo "| HMCL-$HMCL_VERSION.exe | $(cat HMCL-$HMCL_VERSION.exe.sha256) |" >> RELEASE_NOTE
echo "| HMCL-$HMCL_VERSION.jar | $(cat HMCL-$HMCL_VERSION.jar.sha256) |" >> RELEASE_NOTE
echo "| HMCL-$HMCL_VERSION.sh | $(cat HMCL-$HMCL_VERSION.sh.sha256) |" >> 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@v1
uses: softprops/action-gh-release@v2
with:
body_path: RELEASE_NOTE
files: |
+2 -2
View File
@@ -14,11 +14,11 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up JDK 11
- name: Set up JDK 8
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '11'
java-version: 8
java-package: 'jdk+fx'
- name: Build with Gradle
run: ./gradlew build --no-daemon
+58 -24
View File
@@ -25,7 +25,7 @@ val buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number ->
if (!shortCommit.isNullOrEmpty()) "$prefix-$shortCommit" else "SNAPSHOT"
}
}
val versionRoot = System.getenv("VERSION_ROOT") ?: "3.5"
val versionRoot = System.getenv("VERSION_ROOT") ?: "3.6"
val versionType = System.getenv("VERSION_TYPE") ?: if (isOfficial) "nightly" else "unofficial"
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
@@ -167,33 +167,10 @@ fun createExecutable(suffix: String, header: String) {
}
tasks.processResources {
fun convertToBSS(resource: String) {
doFirst {
val cssFile = File(projectDir, "src/main/resources/$resource")
val bssFile = File(projectDir, "build/compiled-resources/${resource.substring(0, resource.length - 4)}.bss")
bssFile.parentFile.mkdirs()
javaexec {
classpath = sourceSets["main"].compileClasspath
mainClass.set("com.sun.javafx.css.parser.Css2Bin")
args(cssFile, bssFile)
}
}
}
from("build/compiled-resources")
convertToBSS("assets/css/root.css")
convertToBSS("assets/css/blue.css")
into("META-INF/versions/11") {
from(sourceSets["java11"].output)
}
dependsOn(tasks["java11Classes"])
into("assets") {
from(project.layout.buildDirectory.file("openjfx-dependencies.json"))
}
dependsOn(rootProject.tasks["generateOpenJFXDependencies"])
}
val makeExecutables = tasks.create("makeExecutables") {
@@ -208,6 +185,56 @@ tasks.build {
dependsOn(makeExecutables)
}
fun parseToolOptions(options: String?): List<String> {
if (options == null)
return listOf()
val builder = StringBuilder()
val result = mutableListOf<String>()
var offset = 0
loop@ while (offset < options.length) {
val ch = options[offset]
if (Character.isWhitespace(ch)) {
if (builder.isNotEmpty()) {
result += builder.toString()
builder.clear()
}
while (offset < options.length && Character.isWhitespace(options[offset])) {
offset++
}
continue@loop
}
if (ch == '\'' || ch == '"') {
offset++
while (offset < options.length) {
val ch2 = options[offset++]
if (ch2 != ch) {
builder.append(ch2)
} else {
continue@loop
}
}
throw GradleException("Unmatched quote in $options")
}
builder.append(ch)
offset++
}
if (builder.isNotEmpty()) {
result += builder.toString()
}
return result
}
tasks.create<JavaExec>("run") {
dependsOn(tasks.jar)
@@ -215,4 +242,11 @@ tasks.create<JavaExec>("run") {
classpath = files(jarPath)
workingDir = rootProject.rootDir
val vmOptions = parseToolOptions(System.getenv("HMCL_JAVA_OPTS"))
jvmArgs(vmOptions)
doFirst {
logger.quiet("HMCL_JAVA_OPTS: $vmOptions")
}
}
@@ -222,7 +222,9 @@ public final class Launcher extends Application {
try {
LOG.info("*** " + Metadata.TITLE + " ***");
LOG.info("Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION);
LOG.info("Operating System: " + (OperatingSystem.OS_RELEASE_PRETTY_NAME == null
? OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION
: OperatingSystem.OS_RELEASE_PRETTY_NAME + " (" + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + ')'));
LOG.info("System Architecture: " + Architecture.SYSTEM_ARCH_NAME);
LOG.info("Java Architecture: " + Architecture.CURRENT_ARCH_NAME);
LOG.info("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor"));
@@ -20,10 +20,9 @@ package org.jackhuang.hmcl;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import org.jackhuang.hmcl.ui.AwtUtils;
import org.jackhuang.hmcl.util.FractureiserDetector;
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
import org.jackhuang.hmcl.ui.SwingUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import javax.net.ssl.HttpsURLConnection;
@@ -61,7 +60,7 @@ public final class Main {
checkDirectoryPath();
if (JavaVersion.CURRENT_JAVA.getParsedVersion() < 9)
if (JavaRuntime.CURRENT_VERSION < 9)
// This environment check will take ~300ms
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
@@ -70,7 +69,6 @@ public final class Main {
checkJavaFX();
verifyJavaFX();
detectFractureiser();
Launcher.main(args);
}
@@ -94,13 +92,6 @@ public final class Main {
}
}
private static void detectFractureiser() {
if (FractureiserDetector.detect()) {
LOG.error("Detected that this computer is infected by fractureiser");
showErrorAndExit(i18n("fatal.fractureiser"));
}
}
private static void checkJavaFX() {
try {
SelfDependencyPatcher.patch();
@@ -37,7 +37,7 @@ 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.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jetbrains.annotations.Nullable;
@@ -383,7 +383,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
vs.setUsesGlobal(true);
}
public LaunchOptions getLaunchOptions(String version, JavaVersion javaVersion, File gameDir, List<String> javaAgents, boolean makeLaunchScript) {
public LaunchOptions getLaunchOptions(String version, JavaRuntime javaVersion, File gameDir, List<String> javaAgents, boolean makeLaunchScript) {
VersionSetting vs = getVersionSetting(version);
LaunchOptions.Builder builder = new LaunchOptions.Builder()
@@ -396,7 +396,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
.setOverrideJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
.setMaxMemory(vs.isNoJVMArgs() && vs.isAutoMemory() ? null : (int)(getAllocatedMemory(
vs.getMaxMemory() * 1024L * 1024L,
OperatingSystem.getPhysicalMemoryStatus().orElse(OperatingSystem.PhysicalMemoryStatus.INVALID).getAvailable(),
OperatingSystem.getPhysicalMemoryStatus().getAvailable(),
vs.isAutoMemory()
) / 1024 / 1024))
.setMinMemory(vs.getMinMemory())
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
@@ -67,8 +66,7 @@ public final class HMCLModpackInstallTask extends Task<Void> {
ModpackConfiguration<Modpack> config = null;
try {
if (json.exists()) {
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<Modpack>>() {
}.getType());
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.");
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.game;
import com.jfoenix.controls.JFXButton;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.*;
@@ -28,21 +27,23 @@ import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.game.*;
import org.jackhuang.hmcl.download.java.JavaRepository;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.launch.*;
import org.jackhuang.hmcl.mod.ModpackCompletionException;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
@@ -55,19 +56,18 @@ import java.net.SocketTimeoutException;
import java.net.URL;
import java.nio.file.AccessDeniedException;
import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static javafx.application.Platform.runLater;
import static javafx.application.Platform.setImplicitExit;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.Lang.resolveException;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.platform.Platform.SYSTEM_PLATFORM;
import static org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java;
public final class LauncherHelper {
@@ -132,12 +132,12 @@ public final class LauncherHelper {
CountDownLatch launchingLatch = new CountDownLatch(1);
List<String> javaAgents = new ArrayList<>(0);
AtomicReference<JavaVersion> javaVersionRef = new AtomicReference<>();
AtomicReference<JavaRuntime> javaVersionRef = new AtomicReference<>();
TaskExecutor executor = checkGameState(profile, setting, version.get())
.thenComposeAsync(javaVersion -> {
javaVersionRef.set(Objects.requireNonNull(javaVersion));
version.set(NativePatcher.patchNative(version.get(), gameVersion.orElse(null), javaVersion, setting));
.thenComposeAsync(java -> {
javaVersionRef.set(Objects.requireNonNull(java));
version.set(NativePatcher.patchNative(version.get(), gameVersion.orElse(null), java, setting));
if (setting.isNotCheckGame())
return null;
return Task.allOf(
@@ -155,7 +155,7 @@ public final class LauncherHelper {
Task.composeAsync(() -> {
Renderer renderer = setting.getRenderer();
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
Library lib = NativePatcher.getMesaLoader(javaVersion, renderer);
Library lib = NativePatcher.getMesaLoader(java, renderer);
if (lib == null)
return null;
File file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
@@ -179,12 +179,13 @@ public final class LauncherHelper {
})
);
}).withStage("launch.state.dependencies")
.thenComposeAsync(() -> {
return gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version.get())).orElse(null);
})
.thenComposeAsync(() -> gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version.get())).orElse(null))
.thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in"))
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
LaunchOptions launchOptions = repository.getLaunchOptions(selectedVersion, javaVersionRef.get(), profile.getGameDir(), javaAgents, scriptFile != null);
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModManager(selectedVersion).getModsDirectory(), 10));
return new HMCLGameLauncher(
repository,
version.get(),
@@ -214,7 +215,7 @@ public final class LauncherHelper {
it.fireEvent(new DialogCloseEvent());
}));
} else {
Platform.runLater(() -> {
runLater(() -> {
launchingStepsPane.fireEvent(new DialogCloseEvent());
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
});
@@ -234,7 +235,7 @@ public final class LauncherHelper {
@Override
public void onStop(boolean success, TaskExecutor executor) {
Platform.runLater(() -> {
runLater(() -> {
// Check if the application has stopped
// because onStop will be invoked if tasks fail when the executor service shut down.
if (!Controllers.isStopped()) {
@@ -251,9 +252,9 @@ public final class LauncherHelper {
} else if (ex instanceof PermissionException) {
message = i18n("launch.failed.executable_permission");
} else if (ex instanceof ProcessCreationException) {
message = i18n("launch.failed.creating_process") + ex.getLocalizedMessage();
message = i18n("launch.failed.creating_process") + "\n" + ex.getLocalizedMessage();
} else if (ex instanceof NotDecompressingNativesException) {
message = i18n("launch.failed.decompressing_natives") + ex.getLocalizedMessage();
message = i18n("launch.failed.decompressing_natives") + "\n" + ex.getLocalizedMessage();
} else if (ex instanceof LibraryDownloadException) {
message = i18n("launch.failed.download_library", ((LibraryDownloadException) ex).getLibrary().getName()) + "\n";
if (ex.getCause() instanceof ResponseCodeException) {
@@ -331,178 +332,181 @@ public final class LauncherHelper {
executor.start();
}
private static Task<JavaVersion> checkGameState(Profile profile, VersionSetting setting, Version version) {
private static Task<JavaRuntime> checkGameState(Profile profile, VersionSetting setting, Version version) {
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version, profile.getRepository().getGameVersion(version).orElse(null));
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(analyzer.getVersion(LibraryAnalyzer.LibraryType.MINECRAFT));
Task<JavaRuntime> getJavaTask = Task.supplyAsync(() -> {
try {
return setting.getJava(gameVersion, version);
} catch (InterruptedException e) {
throw new CancellationException();
}
});
Task<JavaRuntime> task;
if (setting.isNotCheckJVM()) {
return Task.composeAsync(() -> setting.getJavaVersion(gameVersion, version))
.thenApplyAsync(javaVersion -> Optional.ofNullable(javaVersion).orElseGet(JavaVersion::fromCurrentEnvironment))
.withStage("launch.state.java");
}
task = getJavaTask.thenApplyAsync(java -> Lang.requireNonNullElse(java, JavaRuntime.getDefault()));
} else if (setting.getJavaVersionType() == JavaVersionType.AUTO || setting.getJavaVersionType() == JavaVersionType.VERSION) {
task = getJavaTask.thenComposeAsync(Schedulers.javafx(), java -> {
if (java != null) {
return Task.completed(java);
}
return Task.composeAsync(() -> {
return setting.getJavaVersion(gameVersion, version);
}).thenComposeAsync(Schedulers.javafx(), javaVersion -> {
// Reset invalid java version
if (javaVersion == null) {
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
Runnable continueAction = () -> future.complete(JavaVersion.fromCurrentEnvironment());
// Reset invalid java version
CompletableFuture<JavaRuntime> future = new CompletableFuture<>();
Task<JavaRuntime> result = Task.fromCompletableFuture(future);
Runnable breakAction = () -> future.completeExceptionally(new CancellationException("No accepted java"));
List<GameJavaVersion> supportedVersions = GameJavaVersion.getSupportedVersions(SYSTEM_PLATFORM);
if (setting.isJavaAutoSelected()) {
GameJavaVersion targetJavaVersion = null;
GameJavaVersion targetJavaVersion = null;
if (setting.getJavaVersionType() == JavaVersionType.VERSION) {
try {
int targetJavaVersionMajor = Integer.parseInt(setting.getJavaVersion());
GameJavaVersion minimumJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
if (org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java()) {
JavaVersionConstraint.VersionRanges range = JavaVersionConstraint.findSuitableJavaVersionRange(gameVersion, version);
if (range.getMandatory().contains(VersionNumber.asVersion("21.0.3"))) {
targetJavaVersion = GameJavaVersion.JAVA_21;
} else if (range.getMandatory().contains(VersionNumber.asVersion("17.0.1"))) {
targetJavaVersion = GameJavaVersion.JAVA_17;
} else if (range.getMandatory().contains(VersionNumber.asVersion("16.0.1"))) {
targetJavaVersion = GameJavaVersion.JAVA_16;
} else {
String java8Version;
if (minimumJavaVersion != null && targetJavaVersionMajor < minimumJavaVersion.getMajorVersion()) {
Controllers.dialog(
i18n("launch.failed.java_version_too_low"),
i18n("message.error"),
MessageType.ERROR,
breakAction
);
return result;
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
java8Version = "1.8.0_51";
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
java8Version = "1.8.0_202";
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
java8Version = "1.8.0_74";
} else {
java8Version = null;
targetJavaVersion = GameJavaVersion.get(targetJavaVersionMajor);
} catch (NumberFormatException ignored) {
}
} else
targetJavaVersion = version.getJavaVersion();
if (targetJavaVersion != null && supportedVersions.contains(targetJavaVersion)) {
downloadJava(targetJavaVersion, profile)
.whenCompleteAsync((downloadedJava, exception) -> {
if (exception == null) {
future.complete(downloadedJava);
} else {
LOG.warning("Failed to download java", exception);
Controllers.confirm(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING,
() -> future.complete(JavaRuntime.getDefault()),
breakAction);
}
}, Schedulers.javafx());
} else {
Controllers.confirm(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING,
() -> future.complete(JavaRuntime.getDefault()),
breakAction);
}
return result;
});
} else {
task = getJavaTask.thenComposeAsync(java -> {
Set<JavaVersionConstraint> violatedMandatoryConstraints = EnumSet.noneOf(JavaVersionConstraint.class);
Set<JavaVersionConstraint> violatedSuggestedConstraints = EnumSet.noneOf(JavaVersionConstraint.class);
if (java != null) {
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) {
if (!constraint.checkJava(gameVersion, version, java)) {
if (constraint.isMandatory()) {
violatedMandatoryConstraints.add(constraint);
} else {
violatedSuggestedConstraints.add(constraint);
}
}
if (java8Version != null && range.getMandatory().contains(VersionNumber.asVersion(java8Version)))
targetJavaVersion = GameJavaVersion.JAVA_8;
else
targetJavaVersion = null;
}
}
}
if (targetJavaVersion == null) {
Controllers.confirm(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING, continueAction, () -> {
future.completeExceptionally(new CancellationException("No accepted java"));
CompletableFuture<JavaRuntime> future = new CompletableFuture<>();
Task<JavaRuntime> result = Task.fromCompletableFuture(future);
Runnable breakAction = () -> future.completeExceptionally(new CancellationException("Launch operation was cancelled by user"));
if (java == null || !violatedMandatoryConstraints.isEmpty()) {
JavaRuntime suggestedJava = JavaManager.findSuitableJava(gameVersion, version);
if (suggestedJava != null) {
FXUtils.runInFX(() -> {
Controllers.confirm(i18n("launch.advice.java.auto"), i18n("message.warning"), () -> {
setting.setJavaAutoSelected();
future.complete(suggestedJava);
}, breakAction);
});
return result;
} else if (java == null) {
FXUtils.runInFX(() -> Controllers.dialog(
i18n("launch.invalid_java"),
i18n("message.error"),
MessageType.ERROR,
breakAction
));
return result;
} else {
downloadJava(gameVersion.toString(), targetJavaVersion, profile)
.thenAcceptAsync(downloadedJavaVersion -> {
future.complete(downloadedJavaVersion);
})
.exceptionally(throwable -> {
GameJavaVersion gameJavaVersion;
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
gameJavaVersion = version.getJavaVersion();
else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA))
gameJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
else
gameJavaVersion = null;
if (gameJavaVersion != null) {
FXUtils.runInFX(() -> downloadJava(gameJavaVersion, profile).whenCompleteAsync((downloadedJava, throwable) -> {
if (throwable == null) {
setting.setJavaAutoSelected();
future.complete(downloadedJava);
} else {
LOG.warning("Failed to download java", throwable);
Controllers.confirm(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING, continueAction, () -> {
future.completeExceptionally(new CancellationException("No accepted java"));
});
return null;
});
}
} else {
Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.warning"), MessageType.WARNING, continueAction);
breakAction.run();
}
}, Schedulers.javafx()));
return result;
}
setting.setJava(null);
setting.setDefaultJavaPath(null);
setting.setJavaVersion(JavaVersion.fromCurrentEnvironment());
}
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA_LINUX_JAVA_8)) {
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
FXUtils.runInFX(() -> Controllers.dialog(i18n("launch.advice.vanilla_linux_java_8"), i18n("message.error"), MessageType.ERROR, breakAction));
return result;
} else {
violatedMandatoryConstraints.remove(JavaVersionConstraint.VANILLA_LINUX_JAVA_8);
}
}
return Task.fromCompletableFuture(future);
} else {
return Task.completed(javaVersion);
}
}).thenComposeAsync(javaVersion -> {
return Task.allOf(Task.completed(javaVersion), Task.supplyAsync(() -> JavaVersionConstraint.findSuitableJavaVersion(gameVersion, version)));
}).thenComposeAsync(Schedulers.javafx(), javaVersions -> {
JavaVersion javaVersion = (JavaVersion) javaVersions.get(0);
JavaVersion suggestedJavaVersion = (JavaVersion) javaVersions.get(1);
if (setting.isJavaAutoSelected()) return Task.completed(javaVersion);
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.LAUNCH_WRAPPER)) {
FXUtils.runInFX(() -> Controllers.dialog(
i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"),
i18n("message.error"),
MessageType.ERROR,
breakAction
));
return result;
}
JavaVersionConstraint violatedMandatoryConstraint = null;
List<JavaVersionConstraint> violatedSuggestedConstraints = null;
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
if (constraint.appliesToVersion(gameVersion, version, javaVersion, analyzer)) {
if (!constraint.checkJava(gameVersion, version, javaVersion)) {
if (constraint.getType() == JavaVersionConstraint.RULE_MANDATORY) {
violatedMandatoryConstraint = constraint;
} else if (constraint.getType() == JavaVersionConstraint.RULE_SUGGESTED) {
if (violatedSuggestedConstraints == null)
violatedSuggestedConstraints = new ArrayList<>(1);
violatedSuggestedConstraints.add(constraint);
if (!violatedMandatoryConstraints.isEmpty()) {
FXUtils.runInFX(() -> Controllers.dialog(
i18n("launch.advice.unknown") + "\n" + violatedMandatoryConstraints,
i18n("message.error"),
MessageType.ERROR,
breakAction
));
return result;
}
}
}
}
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
Runnable breakAction = () -> future.completeExceptionally(new CancellationException("Launch operation was cancelled by user"));
List<String> suggestions = new ArrayList<>();
if (violatedMandatoryConstraint != null) {
if (suggestedJavaVersion != null) {
Controllers.confirm(i18n("launch.advice.java.auto"), i18n("message.warning"), () -> {
setting.setJavaAutoSelected();
future.complete(suggestedJavaVersion);
}, breakAction);
return Task.fromCompletableFuture(future);
} else {
switch (violatedMandatoryConstraint) {
case GAME_JSON:
downloadJava(gameVersion.toString(), version.getJavaVersion(), profile)
.thenAcceptAsync(downloadedJavaVersion -> {
setting.setJavaVersion(downloadedJavaVersion);
future.complete(downloadedJavaVersion);
}, Schedulers.javafx())
.whenCompleteAsync((result, throwable) -> {
LOG.warning("Failed to download java", throwable);
breakAction.run();
}, Schedulers.javafx());
return Task.fromCompletableFuture(future);
case VANILLA_JAVA_16:
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 16), i18n("message.warning"),
() -> FXUtils.openLink(OPENJDK_DOWNLOAD_LINK), null);
breakAction.run();
return Task.fromCompletableFuture(future);
case VANILLA_JAVA_17:
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 17), i18n("message.warning"),
() -> FXUtils.openLink(OPENJDK_DOWNLOAD_LINK), null);
breakAction.run();
return Task.fromCompletableFuture(future);
case VANILLA_JAVA_21:
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 21), i18n("message.warning"),
() -> FXUtils.openLink(OPENJDK_DOWNLOAD_LINK), null);
breakAction.run();
return Task.fromCompletableFuture(future);
case VANILLA_JAVA_8:
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageType.ERROR, breakAction);
return Task.fromCompletableFuture(future);
case VANILLA_LINUX_JAVA_8:
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
Controllers.dialog(i18n("launch.advice.vanilla_linux_java_8"), i18n("message.error"), MessageType.ERROR, breakAction);
return Task.fromCompletableFuture(future);
} else {
break;
}
case LAUNCH_WRAPPER:
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"), i18n("message.error"), MessageType.ERROR, breakAction);
return Task.fromCompletableFuture(future);
}
if (Architecture.SYSTEM_ARCH == Architecture.X86_64 && java.getPlatform().getArchitecture() == Architecture.X86) {
suggestions.add(i18n("launch.advice.different_platform"));
}
}
List<String> suggestions = new ArrayList<>(0);
// 32-bit JVM cannot make use of too much memory.
if (java.getBits() == Bits.BIT_32 && setting.getMaxMemory() > 1.5 * 1024) {
// 1.5 * 1024 is an inaccurate number.
// Actual memory limit depends on operating system and memory.
suggestions.add(i18n("launch.advice.too_large_memory_for_32bit"));
}
if (Architecture.SYSTEM_ARCH == Architecture.X86_64 && javaVersion.getPlatform().getArchitecture() == Architecture.X86) {
suggestions.add(i18n("launch.advice.different_platform"));
}
// 32-bit JVM cannot make use of too much memory.
if (javaVersion.getBits() == Bits.BIT_32 && setting.getMaxMemory() > 1.5 * 1024) {
// 1.5 * 1024 is an inaccurate number.
// Actual memory limit depends on operating system and memory.
suggestions.add(i18n("launch.advice.too_large_memory_for_32bit"));
}
if (violatedSuggestedConstraints != null) {
for (JavaVersionConstraint violatedSuggestedConstraint : violatedSuggestedConstraints) {
switch (violatedSuggestedConstraint) {
case MODDED_JAVA_7:
@@ -510,7 +514,7 @@ public final class LauncherHelper {
break;
case MODDED_JAVA_8:
// Minecraft>=1.7.10+Forge accepts Java 8
if (javaVersion.getParsedVersion() < 8)
if (java.getParsedVersion() < 8)
suggestions.add(i18n("launch.advice.newer_java"));
else
suggestions.add(i18n("launch.advice.modded_java", 8, gameVersion));
@@ -526,6 +530,9 @@ public final class LauncherHelper {
case MODDED_JAVA_17:
suggestions.add(i18n("launch.advice.modded_java", 17, gameVersion));
break;
case MODDED_JAVA_21:
suggestions.add(i18n("launch.advice.modded_java", 21, gameVersion));
break;
case VANILLA_JAVA_8_51:
suggestions.add(i18n("launch.advice.java8_51_1_13"));
break;
@@ -534,117 +541,91 @@ public final class LauncherHelper {
break;
case VANILLA_X86:
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER
&& org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java()) {
&& isCompatibleWithX86Java()) {
suggestions.add(i18n("launch.advice.vanilla_x86.translation"));
}
break;
default:
suggestions.add(violatedSuggestedConstraint.name());
}
}
}
// Cannot allocate too much memory exceeding free space.
if (OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
suggestions.add(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY));
}
VersionNumber forgeVersion = version.getLibraries().stream()
.filter(it -> it.is("net.minecraftforge", "forge"))
.findFirst()
.map(library -> VersionNumber.asVersion(library.getVersion()))
.orElse(null);
// Forge 2760~2773 will crash game with LiteLoader.
boolean hasForge2760 = forgeVersion != null && (forgeVersion.compareTo("1.12.2-14.23.5.2760") >= 0) && (forgeVersion.compareTo("1.12.2-14.23.5.2773") < 0);
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo("1.12.2") == 0) {
suggestions.add(i18n("launch.advice.forge2760_liteloader"));
}
// OptiFine 1.14.4 is not compatible with Forge 28.2.2 and later versions.
boolean hasForge28_2_2 = forgeVersion != null && (forgeVersion.compareTo("1.14.4-28.2.2") >= 0);
boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine"));
if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo("1.14.4") == 0) {
suggestions.add(i18n("launch.advice.forge28_2_2_optifine"));
}
if (suggestions.isEmpty()) {
if (!future.isDone()) {
future.complete(javaVersion);
// Cannot allocate too much memory exceeding free space.
if (OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
suggestions.add(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY));
}
} else {
String message;
if (suggestions.size() == 1) {
message = i18n("launch.advice", suggestions.get(0));
VersionNumber forgeVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.FORGE)
.map(VersionNumber::asVersion)
.orElse(null);
// Forge 2760~2773 will crash game with LiteLoader.
boolean hasForge2760 = forgeVersion != null && (forgeVersion.compareTo("1.12.2-14.23.5.2760") >= 0) && (forgeVersion.compareTo("1.12.2-14.23.5.2773") < 0);
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo("1.12.2") == 0) {
suggestions.add(i18n("launch.advice.forge2760_liteloader"));
}
// OptiFine 1.14.4 is not compatible with Forge 28.2.2 and later versions.
boolean hasForge28_2_2 = forgeVersion != null && (forgeVersion.compareTo("1.14.4-28.2.2") >= 0);
boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine"));
if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo("1.14.4") == 0) {
suggestions.add(i18n("launch.advice.forge28_2_2_optifine"));
}
if (suggestions.isEmpty()) {
if (!future.isDone()) {
future.complete(java);
}
} else {
message = i18n("launch.advice.multi", suggestions.stream().map(it -> "→ " + it).collect(Collectors.joining("\n")));
String message;
if (suggestions.size() == 1) {
message = i18n("launch.advice", suggestions.get(0));
} else {
message = i18n("launch.advice.multi", suggestions.stream().map(it -> "→ " + it).collect(Collectors.joining("\n")));
}
FXUtils.runInFX(() -> Controllers.confirm(
message,
i18n("message.warning"),
MessageType.WARNING,
() -> future.complete(java),
breakAction));
}
Controllers.confirm(message, i18n("message.warning"), MessageType.WARNING, () -> future.complete(javaVersion), breakAction);
}
return result;
});
}
return Task.fromCompletableFuture(future);
}).withStage("launch.state.java");
return task.withStage("launch.state.java");
}
private static CompletableFuture<JavaVersion> downloadJava(String gameVersion, GameJavaVersion javaVersion, Profile profile) {
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
JFXHyperlink link = new JFXHyperlink(i18n("download.external_link"));
link.setOnAction(e -> {
if (javaVersion.getMajorVersion() == JavaVersion.JAVA_8) {
FXUtils.openLink(ORACLEJDK_DOWNLOAD_LINK);
} else {
FXUtils.openLink(OPENJDK_DOWNLOAD_LINK);
}
future.completeExceptionally(new CancellationException());
});
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",
gameVersion,
javaVersion.getMajorVersion()),
i18n("launch.advice.require_newer_java_version", javaVersion.getMajorVersion()),
i18n("message.warning"),
MessageType.QUESTION)
.addAction(link)
.yesOrNo(() -> {
downloadJavaImpl(javaVersion, profile.getDependency().getDownloadProvider())
.thenAcceptAsync(future::complete)
.exceptionally(throwable -> {
Throwable resolvedException = resolveException(throwable);
LOG.warning("Failed to download java", throwable);
if (!(resolvedException instanceof CancellationException)) {
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
DownloadProvider downloadProvider = profile.getDependency().getDownloadProvider();
Controllers.taskDialog(JavaManager.getDownloadJavaTask(downloadProvider, SYSTEM_PLATFORM, javaVersion)
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
future.complete(result);
} else {
Throwable resolvedException = resolveException(exception);
LOG.warning("Failed to download java", exception);
if (!(resolvedException instanceof CancellationException)) {
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
}
future.completeExceptionally(new CancellationException());
}
future.completeExceptionally(new CancellationException());
return null;
});
}), i18n("download.java"), new TaskCancellationAction(() -> future.completeExceptionally(new CancellationException())));
}, () -> future.completeExceptionally(new CancellationException())).build());
return future;
}
/**
* Directly start java downloading.
*
* @param javaVersion target Java version
* @param downloadProvider download provider
* @return JavaVersion, null if we failed to download java, failed if an error occurred when downloading.
*/
private static CompletableFuture<JavaVersion> downloadJavaImpl(GameJavaVersion javaVersion, DownloadProvider downloadProvider) {
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
Controllers.taskDialog(JavaRepository.downloadJava(javaVersion, downloadProvider)
.whenComplete(Schedulers.javafx(), (downloadedJava, exception) -> {
if (exception != null) {
future.completeExceptionally(exception);
} else {
future.complete(downloadedJava);
}
}), i18n("download.java"), TaskCancellationAction.NORMAL);
return future;
}
private static Task<AuthInfo> logIn(Account account) {
return Task.composeAsync(() -> {
try {
@@ -685,7 +666,7 @@ public final class LauncherHelper {
private void checkExit() {
switch (launcherVisibility) {
case HIDE_AND_REOPEN:
Platform.runLater(() -> {
runLater(() -> {
Optional.ofNullable(Controllers.getStage())
.ifPresent(Stage::show);
});
@@ -696,9 +677,9 @@ public final class LauncherHelper {
case CLOSE:
throw new Error("Never get to here");
case HIDE:
Platform.runLater(() -> {
runLater(() -> {
// Shut down the platform when user closed log window.
Platform.setImplicitExit(true);
setImplicitExit(true);
// If we use Launcher.stop(), log window will be halt immediately.
Launcher.stopWithoutPlatform();
});
@@ -717,14 +698,14 @@ public final class LauncherHelper {
private final Version version;
private final LaunchOptions launchOptions;
private ManagedProcess process;
private boolean lwjgl;
private volatile boolean lwjgl;
private LogWindow logWindow;
private final boolean detectWindow;
private final ArrayDeque<String> logs;
private final ArrayDeque</*Log4jLevel*/Object> levels;
private final CountDownLatch logWindowLatch = new CountDownLatch(1);
private final CircularArrayList<Log> logs;
private final CountDownLatch launchingLatch;
private final String forbiddenAccessToken;
private Thread submitLogThread;
private LinkedBlockingQueue<Log> logBuffer;
public HMCLProcessListener(HMCLGameRepository repository, Version version, AuthInfo authInfo, LaunchOptions launchOptions, CountDownLatch launchingLatch, boolean detectWindow) {
this.repository = repository;
@@ -733,10 +714,7 @@ public final class LauncherHelper {
this.launchingLatch = launchingLatch;
this.detectWindow = detectWindow;
this.forbiddenAccessToken = authInfo != null ? authInfo.getAccessToken() : null;
final int numLogs = config().getLogLines() + 1;
this.logs = new ArrayDeque<>(numLogs);
this.levels = new ArrayDeque<>(numLogs);
this.logs = new CircularArrayList<>(Log.getLogLines() + 1);
}
@Override
@@ -752,18 +730,66 @@ public final class LauncherHelper {
LOG.info("Process ClassPath: " + classpath);
}
if (showLogs)
Platform.runLater(() -> {
logWindow = new LogWindow(process);
logWindow.showNormal();
if (showLogs) {
CountDownLatch logWindowLatch = new CountDownLatch(1);
runLater(() -> {
logWindow = new LogWindow(process, logs);
logWindow.show();
logWindowLatch.countDown();
});
logBuffer = new LinkedBlockingQueue<>();
submitLogThread = Lang.thread(new Runnable() {
private final ArrayList<Log> currentLogs = new ArrayList<>();
private final Semaphore semaphore = new Semaphore(0);
private void submitLogs() {
if (currentLogs.size() == 1) {
Log log = currentLogs.get(0);
runLater(() -> logWindow.logLine(log));
} else {
runLater(() -> {
logWindow.logLines(currentLogs);
semaphore.release();
});
semaphore.acquireUninterruptibly();
}
currentLogs.clear();
}
@Override
public void run() {
while (true) {
try {
currentLogs.add(logBuffer.take());
//noinspection BusyWait
Thread.sleep(200); // Wait for more logs
} catch (InterruptedException e) {
break;
}
logBuffer.drainTo(currentLogs);
submitLogs();
}
do {
submitLogs();
} while (logBuffer.drainTo(currentLogs) > 0);
}
}, "Game Log Submitter", true);
try {
logWindowLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
private void finishLaunch() {
switch (launcherVisibility) {
case HIDE_AND_REOPEN:
Platform.runLater(() -> {
runLater(() -> {
// If application was stopped and execution services did not finish termination,
// these codes will be executed.
if (Controllers.getStage() != null) {
@@ -776,11 +802,11 @@ public final class LauncherHelper {
// Never come to here.
break;
case KEEP:
Platform.runLater(launchingLatch::countDown);
runLater(launchingLatch::countDown);
break;
case HIDE:
launchingLatch.countDown();
Platform.runLater(() -> {
runLater(() -> {
// If application was stopped and execution services did not finish termination,
// these codes will be executed.
if (Controllers.getStage() != null) {
@@ -796,44 +822,37 @@ public final class LauncherHelper {
@Override
public void onLog(String log, boolean isErrorStream) {
String filteredLog = forbiddenAccessToken == null ? log : log.replace(forbiddenAccessToken, "<access token>");
if (isErrorStream)
System.err.println(filteredLog);
System.err.println(log);
else
System.out.println(filteredLog);
System.out.println(log);
Log4jLevel level;
if (isErrorStream)
level = Log4jLevel.ERROR;
else
level = showLogs ? Optional.ofNullable(Log4jLevel.guessLevel(filteredLog)).orElse(Log4jLevel.INFO) : null;
synchronized (this) {
logs.add(filteredLog);
levels.add(level != null ? level : Optional.empty()); // Use 'Optional.empty()' as hole
if (logs.size() > config().getLogLines()) {
logs.removeFirst();
levels.removeFirst();
}
}
log = StringUtils.parseEscapeSequence(log);
if (forbiddenAccessToken != null)
log = log.replace(forbiddenAccessToken, "<access token>");
Log4jLevel level = isErrorStream && !log.startsWith("[authlib-injector]") ? Log4jLevel.ERROR : null;
if (showLogs) {
try {
logWindowLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
if (level == null)
level = Lang.requireNonNullElse(Log4jLevel.guessLevel(log), Log4jLevel.INFO);
logBuffer.add(new Log(log, level));
} else {
synchronized (this) {
logs.addLast(new Log(log, level));
if (logs.size() > Log.getLogLines())
logs.removeFirst();
}
Platform.runLater(() -> logWindow.logLine(filteredLog, level));
}
if (!lwjgl) {
String lowerCaseLog = filteredLog.toLowerCase(Locale.ROOT);
String lowerCaseLog = log.toLowerCase(Locale.ROOT);
if (!detectWindow || lowerCaseLog.contains("lwjgl version") || lowerCaseLog.contains("lwjgl openal")) {
lwjgl = true;
finishLaunch();
synchronized (this) {
if (!lwjgl) {
lwjgl = true;
finishLaunch();
}
}
}
}
}
@@ -841,7 +860,13 @@ public final class LauncherHelper {
@Override
public void onExit(int exitCode, ExitType exitType) {
if (showLogs) {
Platform.runLater(() -> logWindow.logLine(String.format("[HMCL ProcessListener] Minecraft exit with code %d(0x%x), type is %s.", exitCode, exitCode, exitType), Log4jLevel.INFO));
logBuffer.add(new Log(String.format("[HMCL ProcessListener] Minecraft exit with code %d(0x%x), type is %s.", exitCode, exitCode, exitType), Log4jLevel.INFO));
submitLogThread.interrupt();
try {
submitLogThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
launchingLatch.countDown();
@@ -850,14 +875,16 @@ public final class LauncherHelper {
return;
// Game crashed before opening the game window.
if (!lwjgl) finishLaunch();
if (!lwjgl) {
synchronized (this) {
if (!lwjgl)
finishLaunch();
}
}
if (exitType != ExitType.NORMAL) {
ArrayList<Pair<String, Log4jLevel>> pairs = new ArrayList<>(logs.size());
Lang.forEachZipped(logs, levels,
(log, l) -> pairs.add(pair(log, l instanceof Log4jLevel ? ((Log4jLevel) l) : Optional.ofNullable(Log4jLevel.guessLevel(log)).orElse(Log4jLevel.INFO))));
repository.markVersionLaunchedAbnormally(version.getId());
Platform.runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, pairs).show());
runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, logs).show());
}
checkExit();
@@ -865,8 +892,8 @@ public final class LauncherHelper {
}
private static final String ORACLEJDK_DOWNLOAD_LINK = "https://www.java.com/download";
private static final String OPENJDK_DOWNLOAD_LINK = "https://docs.microsoft.com/java/openjdk/download";
private static final String ORACLEJDK_DOWNLOAD_LINK = "https://www.java.com/download/";
private static final String OPENJDK_DOWNLOAD_LINK = "https://learn.microsoft.com/java/openjdk/download";
public static final Queue<ManagedProcess> PROCESSES = new ConcurrentLinkedQueue<>();
@@ -0,0 +1,72 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.util.Log4jLevel;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public final class Log {
public static final int DEFAULT_LOG_LINES = 2000;
public static int getLogLines() {
Integer lines = config().getLogLines();
return lines != null && lines > 0 ? lines : DEFAULT_LOG_LINES;
}
private final String log;
private Log4jLevel level;
private boolean selected = false;
public Log(String log) {
this.log = log;
}
public Log(String log, Log4jLevel level) {
this.log = log;
this.level = level;
}
public String getLog() {
return log;
}
public Log4jLevel getLevel() {
Log4jLevel level = this.level;
if (level == null) {
level = Log4jLevel.guessLevel(log);
if (level == null)
level = Log4jLevel.INFO;
this.level = level;
}
return level;
}
public boolean isSelected() {
return selected;
}
public void setSelected(boolean selected) {
this.selected = selected;
}
@Override
public String toString() {
return log;
}
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.mod.curse.CurseModpackProvider;
@@ -141,8 +140,7 @@ public final class ModpackHelper {
throw new FileNotFoundException(file.getPath());
else
try {
return JsonUtils.GSON.fromJson(FileUtils.readText(file), new TypeToken<ModpackConfiguration<?>>() {
}.getType());
return JsonUtils.GSON.fromJson(FileUtils.readText(file), ModpackConfiguration.class);
} catch (JsonParseException e) {
throw new IOException("Malformed modpack configuration");
}
@@ -0,0 +1,221 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.java;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.java.mojang.MojangJavaDownloadTask;
import org.jackhuang.hmcl.download.java.mojang.MojangJavaRemoteFiles;
import org.jackhuang.hmcl.game.DownloadInfo;
import org.jackhuang.hmcl.game.GameJavaVersion;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.*;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class HMCLJavaRepository implements JavaRepository {
public static final String MOJANG_JAVA_PREFIX = "mojang-";
private final Path root;
public HMCLJavaRepository(Path root) {
this.root = root;
}
public Path getPlatformRoot(Platform platform) {
return root.resolve(platform.toString());
}
@Override
public Path getJavaDir(Platform platform, String name) {
return getPlatformRoot(platform).resolve(name);
}
public Path getJavaDir(Platform platform, GameJavaVersion gameJavaVersion) {
return getJavaDir(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
}
@Override
public Path getManifestFile(Platform platform, String name) {
return getPlatformRoot(platform).resolve(name + ".json");
}
public Path getManifestFile(Platform platform, GameJavaVersion gameJavaVersion) {
return getManifestFile(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
}
public boolean isInstalled(Platform platform, String name) {
return Files.exists(getManifestFile(platform, name));
}
public boolean isInstalled(Platform platform, GameJavaVersion gameJavaVersion) {
return isInstalled(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
}
public @Nullable Path getJavaExecutable(Platform platform, String name) {
Path javaDir = getJavaDir(platform, name);
try {
return JavaManager.getExecutable(javaDir).toRealPath();
} catch (IOException ignored) {
if (platform.getOperatingSystem() == OperatingSystem.OSX) {
try {
return JavaManager.getMacExecutable(javaDir).toRealPath();
} catch (IOException ignored1) {
}
}
}
return null;
}
public @Nullable Path getJavaExecutable(Platform platform, GameJavaVersion gameJavaVersion) {
return getJavaExecutable(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
}
@Override
public Collection<JavaRuntime> getAllJava(Platform platform) {
Path root = getPlatformRoot(platform);
if (!Files.isDirectory(root))
return Collections.emptyList();
ArrayList<JavaRuntime> list = new ArrayList<>();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(root)) {
for (Path file : stream) {
try {
String name = file.getFileName().toString();
if (name.endsWith(".json") && Files.isRegularFile(file)) {
Path javaDir = file.resolveSibling(name.substring(0, name.length() - ".json".length()));
Path executable;
try {
executable = JavaManager.getExecutable(javaDir).toRealPath();
} catch (IOException e) {
if (platform.getOperatingSystem() == OperatingSystem.OSX)
executable = JavaManager.getMacExecutable(javaDir).toRealPath();
else
throw e;
}
if (Files.isDirectory(javaDir)) {
JavaManifest manifest;
try (InputStream input = Files.newInputStream(file)) {
manifest = JsonUtils.fromJsonFully(input, JavaManifest.class);
}
list.add(JavaRuntime.of(executable, manifest.getInfo(), true));
}
}
} catch (Throwable e) {
LOG.warning("Failed to parse " + file, e);
}
}
} catch (IOException ignored) {
}
return list;
}
@Override
public Task<JavaRuntime> getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion) {
Path javaDir = getJavaDir(platform, gameJavaVersion);
return new MojangJavaDownloadTask(downloadProvider, javaDir, gameJavaVersion, JavaManager.getMojangJavaPlatform(platform)).thenApplyAsync(result -> {
Path executable;
try {
executable = JavaManager.getExecutable(javaDir).toRealPath();
} catch (IOException e) {
if (platform.getOperatingSystem() == OperatingSystem.OSX)
executable = JavaManager.getMacExecutable(javaDir).toRealPath();
else
throw e;
}
JavaInfo info;
if (JavaManager.isCompatible(platform))
info = JavaInfo.fromExecutable(executable, false);
else
info = new JavaInfo(platform, result.download.getVersion().getName(), null);
Map<String, Object> update = new LinkedHashMap<>();
update.put("provider", "mojang");
update.put("component", gameJavaVersion.getComponent());
Map<String, JavaLocalFiles.Local> files = new LinkedHashMap<>();
result.remoteFiles.getFiles().forEach((path, file) -> {
if (file instanceof MojangJavaRemoteFiles.RemoteFile) {
DownloadInfo downloadInfo = ((MojangJavaRemoteFiles.RemoteFile) file).getDownloads().get("raw");
if (downloadInfo != null) {
files.put(path, new JavaLocalFiles.LocalFile(downloadInfo.getSha1(), downloadInfo.getSize()));
}
} else if (file instanceof MojangJavaRemoteFiles.RemoteDirectory) {
files.put(path, new JavaLocalFiles.LocalDirectory());
} else if (file instanceof MojangJavaRemoteFiles.RemoteLink) {
files.put(path, new JavaLocalFiles.LocalLink(((MojangJavaRemoteFiles.RemoteLink) file).getTarget()));
}
});
JavaManifest manifest = new JavaManifest(info, update, files);
FileUtils.writeText(getManifestFile(platform, gameJavaVersion), JsonUtils.GSON.toJson(manifest));
return JavaRuntime.of(executable, info, true);
});
}
public Task<JavaRuntime> getInstallJavaTask(Platform platform, String name, Map<String, Object> update, Path archiveFile) {
Path javaDir = getJavaDir(platform, name);
return new JavaInstallTask(javaDir, update, archiveFile).thenApplyAsync(result -> {
if (!result.getInfo().getPlatform().equals(platform))
throw new IOException("Platform is mismatch: expected " + platform + " but got " + result.getInfo().getPlatform());
Path executable = javaDir.resolve("bin").resolve(platform.getOperatingSystem().getJavaExecutable()).toRealPath();
FileUtils.writeText(getManifestFile(platform, name), JsonUtils.GSON.toJson(result));
return JavaRuntime.of(executable, result.getInfo(), true);
});
}
@Override
public Task<Void> getUninstallJavaTask(Platform platform, String name) {
return Task.runAsync(() -> {
Files.deleteIfExists(getManifestFile(platform, name));
FileUtils.deleteDirectory(getJavaDir(platform, name).toFile());
});
}
@Override
public Task<Void> getUninstallJavaTask(JavaRuntime java) {
return Task.runAsync(() -> {
Path root = getPlatformRoot(java.getPlatform());
Path relativized = root.relativize(java.getBinary());
if (relativized.getNameCount() > 1) {
String name = relativized.getName(0).toString();
Files.deleteIfExists(getManifestFile(java.getPlatform(), name));
FileUtils.deleteDirectory(getJavaDir(java.getPlatform(), name).toFile());
}
});
}
}
@@ -0,0 +1,116 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.java;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.Hex;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;
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.LinkedHashMap;
import java.util.Map;
/**
* @author Glavo
*/
public final class JavaInstallTask extends Task<JavaManifest> {
private final Path targetDir;
private final Map<String, Object> update;
private final Path archiveFile;
private final Map<String, JavaLocalFiles.Local> files = new LinkedHashMap<>();
private final ArrayList<String> nameStack = new ArrayList<>();
private final byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
private final MessageDigest messageDigest = DigestUtils.getDigest("SHA-1");
public JavaInstallTask(Path targetDir, Map<String, Object> update, Path archiveFile) {
this.targetDir = targetDir;
this.update = update;
this.archiveFile = archiveFile;
}
@Override
public void execute() throws Exception {
JavaInfo info;
try (ArchiveFileTree<?, ?> tree = ArchiveFileTree.open(archiveFile)) {
info = JavaInfo.fromArchive(tree);
copyDirContent(tree, targetDir);
}
setResult(new JavaManifest(info, update, files));
}
private <F, E extends ArchiveEntry> void copyDirContent(ArchiveFileTree<F, E> tree, Path targetDir) throws IOException {
copyDirContent(tree, tree.getRoot().getSubDirs().values().iterator().next(), targetDir);
}
private <F, E extends ArchiveEntry> void copyDirContent(ArchiveFileTree<F, E> tree, ArchiveFileTree.Dir<E> dir, Path targetDir) throws IOException {
Files.createDirectories(targetDir);
for (Map.Entry<String, E> pair : dir.getFiles().entrySet()) {
Path path = targetDir.resolve(pair.getKey());
E entry = pair.getValue();
nameStack.add(pair.getKey());
if (tree.isLink(entry)) {
String linkTarget = tree.getLink(entry);
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalLink(linkTarget));
Files.createSymbolicLink(path, Paths.get(linkTarget));
} else {
long size = 0L;
try (InputStream input = tree.getInputStream(entry);
OutputStream output = Files.newOutputStream(path, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
messageDigest.reset();
int c;
while ((c = input.read(buffer)) > 0) {
size += c;
output.write(buffer, 0, c);
messageDigest.update(buffer, 0, c);
}
}
if (tree.isExecutable(entry))
//noinspection ResultOfMethodCallIgnored
path.toFile().setExecutable(true);
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalFile(Hex.encodeHex(messageDigest.digest()), size));
}
nameStack.remove(nameStack.size() - 1);
}
for (Map.Entry<String, ArchiveFileTree.Dir<E>> pair : dir.getSubDirs().entrySet()) {
nameStack.add(pair.getKey());
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalDirectory());
copyDirContent(tree, pair.getValue(), targetDir.resolve(pair.getKey()));
nameStack.remove(nameStack.size() - 1);
}
}
}
@@ -0,0 +1,128 @@
/*
* 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.java;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import java.lang.reflect.Type;
/**
* @author Glavo
*/
public final class JavaLocalFiles {
@JsonAdapter(Serializer.class)
public abstract static class Local {
private final String type;
Local(String type) {
this.type = type;
}
public String getType() {
return type;
}
}
public static final class LocalFile extends Local {
private final String sha1;
private final long size;
public LocalFile(String sha1, long size) {
super("file");
this.sha1 = sha1;
this.size = size;
}
public String getSha1() {
return sha1;
}
public long getSize() {
return size;
}
}
public static final class LocalDirectory extends Local {
public LocalDirectory() {
super("directory");
}
}
public static final class LocalLink extends Local {
private final String target;
public LocalLink(String target) {
super("link");
this.target = target;
}
public String getTarget() {
return target;
}
}
public static class Serializer implements JsonSerializer<Local>, JsonDeserializer<Local> {
@Override
public JsonElement serialize(Local src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject obj = new JsonObject();
obj.addProperty("type", src.getType());
if (src instanceof LocalFile) {
obj.addProperty("sha1", ((LocalFile) src).getSha1());
obj.addProperty("size", ((LocalFile) src).getSize());
} else if (src instanceof LocalLink) {
obj.addProperty("target", ((LocalLink) src).getTarget());
}
return obj;
}
@Override
public Local deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (!json.isJsonObject())
throw new JsonParseException(json.toString());
JsonObject obj = json.getAsJsonObject();
if (!obj.has("type"))
throw new JsonParseException(json.toString());
String type = obj.getAsJsonPrimitive("type").getAsString();
try {
switch (type) {
case "file": {
String sha1 = obj.getAsJsonPrimitive("sha1").getAsString();
long size = obj.getAsJsonPrimitive("size").getAsLong();
return new LocalFile(sha1, size);
}
case "directory": {
return new LocalDirectory();
}
case "link": {
String target = obj.getAsJsonPrimitive("target").getAsString();
return new LocalLink(target);
}
default:
throw new AssertionError("unknown type: " + type);
}
} catch (Throwable e) {
throw new JsonParseException(json.toString());
}
}
}
}
@@ -0,0 +1,698 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.java;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.GameJavaVersion;
import org.jackhuang.hmcl.game.JavaVersionConstraint;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.ConfigHolder;
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.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class JavaManager {
private JavaManager() {
}
public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_DIRECTORY.resolve("java"));
public static String getMojangJavaPlatform(Platform platform) {
if (platform.getOperatingSystem() == OperatingSystem.WINDOWS) {
if (Architecture.SYSTEM_ARCH == Architecture.X86) {
return "windows-x86";
} else if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
return "windows-x64";
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
return "windows-arm64";
}
} else if (platform.getOperatingSystem() == OperatingSystem.LINUX) {
if (Architecture.SYSTEM_ARCH == Architecture.X86) {
return "linux-i386";
} else if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
return "linux";
}
} else if (platform.getOperatingSystem() == OperatingSystem.OSX) {
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
return "mac-os";
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
return "mac-os-arm64";
}
}
return null;
}
public static Path getExecutable(Path javaHome) {
return javaHome.resolve("bin").resolve(OperatingSystem.CURRENT_OS.getJavaExecutable());
}
public static Path getMacExecutable(Path javaHome) {
return javaHome.resolve("jre.bundle/Contents/Home/bin/java");
}
public static boolean isCompatible(Platform platform) {
if (platform.getOperatingSystem() != OperatingSystem.CURRENT_OS)
return false;
Architecture architecture = platform.getArchitecture();
if (architecture == Architecture.SYSTEM_ARCH || architecture == Architecture.CURRENT_ARCH)
return true;
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
return architecture == Architecture.X86;
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 OSX:
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
return architecture == Architecture.X86_64;
break;
}
return false;
}
private static volatile Map<Path, JavaRuntime> allJava;
private static final CountDownLatch LATCH = new CountDownLatch(1);
private static final ObjectProperty<Collection<JavaRuntime>> allJavaProperty = new SimpleObjectProperty<>();
private static Map<Path, JavaRuntime> getAllJavaMap() throws InterruptedException {
Map<Path, JavaRuntime> map = allJava;
if (map == null) {
LATCH.await();
map = allJava;
}
return map;
}
private static void updateAllJavaProperty(Map<Path, JavaRuntime> javaRuntimes) {
JavaRuntime[] array = javaRuntimes.values().toArray(new JavaRuntime[0]);
Arrays.sort(array);
allJavaProperty.set(Arrays.asList(array));
}
public static boolean isInitialized() {
return allJava != null;
}
public static Collection<JavaRuntime> getAllJava() throws InterruptedException {
return getAllJavaMap().values();
}
public static ObjectProperty<Collection<JavaRuntime>> getAllJavaProperty() {
return allJavaProperty;
}
public static JavaRuntime getJava(Path executable) throws IOException, InterruptedException {
executable = executable.toRealPath();
JavaRuntime javaRuntime = getAllJavaMap().get(executable);
if (javaRuntime != null) {
return javaRuntime;
}
JavaInfo info = JavaInfo.fromExecutable(executable);
return JavaRuntime.of(executable, info, false);
}
public static void refresh() {
Task.supplyAsync(JavaManager::searchPotentialJavaExecutables).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (result != null) {
LATCH.await();
allJava = result;
updateAllJavaProperty(result);
}
}).start();
}
public static Task<JavaRuntime> getAddJavaTask(Path binary) {
return Task.supplyAsync("Get Java", () -> JavaManager.getJava(binary))
.thenApplyAsync(Schedulers.javafx(), javaRuntime -> {
if (!JavaManager.isCompatible(javaRuntime.getPlatform())) {
throw new UnsupportedPlatformException("Incompatible platform: " + javaRuntime.getPlatform());
}
String pathString = javaRuntime.getBinary().toString();
ConfigHolder.globalConfig().getDisabledJava().remove(pathString);
if (ConfigHolder.globalConfig().getUserJava().add(pathString)) {
addJava(javaRuntime);
}
return javaRuntime;
});
}
public static Task<JavaRuntime> getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion) {
return REPOSITORY.getDownloadJavaTask(downloadProvider, platform, gameJavaVersion)
.thenApplyAsync(Schedulers.javafx(), java -> {
addJava(java);
return java;
});
}
public static Task<JavaRuntime> getInstallJavaTask(Platform platform, String name, Map<String, Object> update, Path archiveFile) {
return REPOSITORY.getInstallJavaTask(platform, name, update, archiveFile)
.thenApplyAsync(Schedulers.javafx(), java -> {
addJava(java);
return java;
});
}
public static Task<Void> getUninstallJavaTask(JavaRuntime java) {
assert java.isManaged();
Path root = REPOSITORY.getPlatformRoot(java.getPlatform());
Path relativized = root.relativize(java.getBinary());
if (relativized.getNameCount() > 1) {
FXUtils.runInFX(() -> {
try {
removeJava(java);
} catch (InterruptedException e) {
throw new AssertionError("Unreachable code", e);
}
});
String name = relativized.getName(0).toString();
return REPOSITORY.getUninstallJavaTask(java.getPlatform(), name);
} else {
return Task.completed(null);
}
}
// FXThread
public static void addJava(JavaRuntime java) throws InterruptedException {
Map<Path, JavaRuntime> oldMap = getAllJavaMap();
if (!oldMap.containsKey(java.getBinary())) {
HashMap<Path, JavaRuntime> newMap = new HashMap<>(oldMap);
newMap.put(java.getBinary(), java);
allJava = newMap;
updateAllJavaProperty(newMap);
}
}
// FXThread
public static void removeJava(JavaRuntime java) throws InterruptedException {
removeJava(java.getBinary());
}
// FXThread
public static void removeJava(Path realPath) throws InterruptedException {
Map<Path, JavaRuntime> oldMap = getAllJavaMap();
if (oldMap.containsKey(realPath)) {
HashMap<Path, JavaRuntime> newMap = new HashMap<>(oldMap);
newMap.remove(realPath);
allJava = newMap;
updateAllJavaProperty(newMap);
}
}
private static int compareJavaVersion(JavaRuntime java1, JavaRuntime java2, GameJavaVersion suggestedJavaVersion) {
if (suggestedJavaVersion != null) {
boolean b1 = java1.getParsedVersion() == suggestedJavaVersion.getMajorVersion();
boolean b2 = java2.getParsedVersion() == suggestedJavaVersion.getMajorVersion();
if (b1 != b2)
return b1 ? 1 : -1;
}
return java1.getVersionNumber().compareTo(java2.getVersionNumber());
}
@Nullable
public static JavaRuntime findSuitableJava(GameVersionNumber gameVersion, Version version) throws InterruptedException {
return findSuitableJava(getAllJava(), gameVersion, version);
}
@Nullable
public static JavaRuntime findSuitableJava(Collection<JavaRuntime> javaRuntimes, GameVersionNumber gameVersion, Version version) {
LibraryAnalyzer analyzer = version != null ? LibraryAnalyzer.analyze(version, gameVersion != null ? gameVersion.toString() : null) : null;
boolean forceX86 = Architecture.SYSTEM_ARCH == Architecture.ARM64
&& (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS || OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
&& (gameVersion == null || gameVersion.compareTo("1.6") < 0);
GameJavaVersion suggestedJavaVersion =
(version != null && gameVersion != null && gameVersion.compareTo("1.7.10") >= 0) ? version.getJavaVersion() : null;
JavaRuntime mandatory = null;
JavaRuntime suggested = null;
for (JavaRuntime java : javaRuntimes) {
if (forceX86) {
if (!java.getArchitecture().isX86())
continue;
} else {
if (java.getArchitecture() != Architecture.SYSTEM_ARCH)
continue;
}
boolean violationMandatory = false;
boolean violationSuggested = false;
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) {
if (!constraint.checkJava(gameVersion, version, java)) {
if (constraint.isMandatory()) {
violationMandatory = true;
} else {
violationSuggested = true;
}
}
}
}
if (!violationMandatory) {
if (mandatory == null) mandatory = java;
else if (compareJavaVersion(java, mandatory, suggestedJavaVersion) > 0)
mandatory = java;
if (!violationSuggested) {
if (suggested == null) suggested = java;
else if (compareJavaVersion(java, suggested, suggestedJavaVersion) > 0)
suggested = java;
}
}
}
return suggested != null ? suggested : mandatory;
}
public static void initialize() {
Map<Path, JavaRuntime> allJava = searchPotentialJavaExecutables();
JavaManager.allJava = allJava;
LATCH.countDown();
FXUtils.runInFX(() -> updateAllJavaProperty(allJava));
}
// search java
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)
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86);
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277)
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86_64);
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86);
}
break;
case OSX:
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
searchAllJavaInRepository(javaRuntimes, Platform.OSX_X86_64);
break;
}
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Runtime Environment\\");
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Development Kit\\");
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JRE\\");
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK\\");
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:
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 OSX:
tryAddJavaHome(javaRuntimes, Paths.get("/Library/Java/JavaVirtualMachines/Contents/Home"));
tryAddJavaHome(javaRuntimes, Paths.get(System.getProperty("user.home"), "/Library/Java/JavaVirtualMachines/Contents/Home"));
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
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) {
searchAllJavaInDirectory(javaRuntimes, dir);
}
} catch (IOException e) {
LOG.warning("Failed to get subdirectories of /opt/homebrew/Cellar");
}
break;
default:
break;
}
// 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 -> searchAllOfficialJava(javaRuntimes, it, false));
FileUtils.tryGetPath(Lang.requireNonNullElse(System.getenv("ProgramFiles(x86)"), "C:\\Program Files (x86)"), "Minecraft Launcher\\runtime")
.ifPresent(it -> searchAllOfficialJava(javaRuntimes, it, false));
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.X86_64) {
searchAllOfficialJava(javaRuntimes, Paths.get(System.getProperty("user.home"), ".minecraft/runtime"), false);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
searchAllOfficialJava(javaRuntimes, Paths.get(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime"), false);
}
searchAllOfficialJava(javaRuntimes, CacheRepository.getInstance().getCacheDirectory().resolve("java"), true);
// Search in PATH.
if (System.getenv("PATH") != null) {
String[] paths = System.getenv("PATH").split(OperatingSystem.PATH_SEPARATOR);
for (String path : paths) {
try {
tryAddJavaExecutable(javaRuntimes, Paths.get(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
} catch (InvalidPathException ignored) {
}
}
}
if (System.getenv("HMCL_JRES") != null) {
String[] paths = System.getenv("HMCL_JRES").split(OperatingSystem.PATH_SEPARATOR);
for (String path : paths) {
try {
tryAddJavaHome(javaRuntimes, Paths.get(path));
} catch (InvalidPathException ignored) {
}
}
}
searchAllJavaInDirectory(javaRuntimes, Paths.get(System.getProperty("user.home"), ".jdks"));
for (String javaPath : ConfigHolder.globalConfig().getUserJava()) {
try {
tryAddJavaExecutable(javaRuntimes, Paths.get(javaPath));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path: " + javaPath);
}
}
JavaRuntime currentJava = JavaRuntime.CURRENT_JAVA;
if (currentJava != null
&& !javaRuntimes.containsKey(currentJava.getBinary())
&& !ConfigHolder.globalConfig().getDisabledJava().contains(currentJava.getBinary().toString())) {
javaRuntimes.put(currentJava.getBinary(), currentJava);
}
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 " + javaRuntimes.size() + "\n", "")));
return javaRuntimes;
}
private static void tryAddJavaHome(Map<Path, JavaRuntime> javaRuntimes, Path javaHome) {
Path executable = getExecutable(javaHome);
if (!Files.isRegularFile(executable)) {
return;
}
try {
executable = executable.toRealPath();
} catch (IOException e) {
LOG.warning("Failed to resolve path " + executable, e);
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 {
info = JavaInfo.fromReleaseFile(releaseFile);
} catch (IOException e) {
try {
info = JavaInfo.fromExecutable(executable, false);
} catch (IOException e2) {
e2.addSuppressed(e);
LOG.warning("Failed to lookup Java executable at " + executable, e2);
}
}
}
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 = JavaInfo.fromExecutable(executable);
} 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 verify Java in " + component, e);
return;
}
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
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'");
}
tryAddJavaHome(javaRuntimes, dir);
}
private static void searchAllJavaInRepository(Map<Path, JavaRuntime> javaRuntimes, Platform platform) {
for (JavaRuntime java : 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);
}
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX && Architecture.CURRENT_ARCH == Architecture.ARM64) {
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.OSX_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;
}
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);
}
}
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;
}
for (String vendor : new String[]{"Java", "BellSoft", "AdoptOpenJDK", "Zulu", "Microsoft", "Eclipse Foundation", "Semeru"}) {
searchAllJavaInDirectory(javaRuntimes, path.resolve(vendor));
}
}
// ==== Windows Registry Support ====
private static void queryJavaInRegistryKey(Map<Path, JavaRuntime> javaRuntimes, String location) {
for (String java : querySubFolders(location)) {
if (!querySubFolders(java).contains(java + "\\MSI"))
continue;
String home = queryRegisterValue(java, "JavaHome");
if (home != null) {
try {
tryAddJavaHome(javaRuntimes, Paths.get(home));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path in system registry: " + home);
}
}
}
}
private static List<String> querySubFolders(String location) {
List<String> res = new ArrayList<>();
try {
Process process = Runtime.getRuntime().exec(new String[]{"cmd", "/c", "reg", "query", location});
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) {
for (String line; (line = reader.readLine()) != null; ) {
if (line.startsWith(location) && !line.equals(location)) {
res.add(line);
}
}
}
} catch (IOException e) {
LOG.warning("Failed to query sub folders of " + location, e);
}
return res;
}
private static String queryRegisterValue(String location, String name) {
boolean last = false;
try {
Process process = Runtime.getRuntime().exec(new String[]{"cmd", "/c", "reg", "query", location, "/v", name});
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) {
for (String line; (line = reader.readLine()) != null; ) {
if (StringUtils.isNotBlank(line)) {
if (last && line.trim().startsWith(name)) {
int begins = line.indexOf(name);
if (begins > 0) {
String s2 = line.substring(begins + name.length());
begins = s2.indexOf("REG_SZ");
if (begins > 0) {
return s2.substring(begins + "REG_SZ".length()).trim();
}
}
}
if (location.equals(line.trim())) {
last = true;
}
}
}
}
} catch (IOException e) {
LOG.warning("Failed to query register value of " + location, e);
}
return null;
}
}
@@ -0,0 +1,112 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.java;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.Optional;
import static org.jackhuang.hmcl.util.gson.JsonUtils.mapTypeOf;
/**
* @author Glavo
*/
@JsonAdapter(JavaManifest.Serializer.class)
public final class JavaManifest {
private final JavaInfo info;
@Nullable
private final Map<String, Object> update;
@Nullable
private final Map<String, JavaLocalFiles.Local> files;
public JavaManifest(JavaInfo info, @Nullable Map<String, Object> update, @Nullable Map<String, JavaLocalFiles.Local> files) {
this.info = info;
this.update = update;
this.files = files;
}
public JavaInfo getInfo() {
return info;
}
public Map<String, Object> getUpdate() {
return update;
}
public Map<String, JavaLocalFiles.Local> getFiles() {
return files;
}
public static final class Serializer implements JsonSerializer<JavaManifest>, JsonDeserializer<JavaManifest> {
private static final Type LOCAL_FILES_TYPE = mapTypeOf(String.class, JavaLocalFiles.Local.class).getType();
@Override
public JsonElement serialize(JavaManifest src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject res = new JsonObject();
res.addProperty("os.name", src.getInfo().getPlatform().getOperatingSystem().getCheckedName());
res.addProperty("os.arch", src.getInfo().getPlatform().getArchitecture().getCheckedName());
res.addProperty("java.version", src.getInfo().getVersion());
res.addProperty("java.vendor", src.getInfo().getVendor());
if (src.getUpdate() != null)
res.add("update", context.serialize(src.getUpdate()));
if (src.getFiles() != null)
res.add("files", context.serialize(src.getFiles(), LOCAL_FILES_TYPE));
return res;
}
@Override
public JavaManifest deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (!json.isJsonObject())
throw new JsonParseException(json.toString());
try {
JsonObject jsonObject = json.getAsJsonObject();
OperatingSystem osName = OperatingSystem.parseOSName(jsonObject.getAsJsonPrimitive("os.name").getAsString());
Architecture osArch = Architecture.parseArchName(jsonObject.getAsJsonPrimitive("os.arch").getAsString());
String javaVersion = jsonObject.getAsJsonPrimitive("java.version").getAsString();
String javaVendor = Optional.ofNullable(jsonObject.get("java.vendor")).map(JsonElement::getAsString).orElse(null);
Map<String, Object> update = jsonObject.has("update") ? context.deserialize(jsonObject.get("update"), Map.class) : null;
Map<String, JavaLocalFiles.Local> files = jsonObject.has("files") ? context.deserialize(jsonObject.get("files"), LOCAL_FILES_TYPE) : null;
if (osName == null || osArch == null || javaVersion == null)
throw new JsonParseException(json.toString());
return new JavaManifest(new JavaInfo(Platform.getPlatform(osName, osArch), javaVersion, javaVendor), update, files);
} catch (JsonParseException e) {
throw e;
} catch (Throwable e) {
throw new JsonParseException(e);
}
}
}
}
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.reflect.TypeToken;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
@@ -55,6 +54,8 @@ import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.Lang.immutableListOf;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
import static org.jackhuang.hmcl.util.gson.JsonUtils.mapTypeOf;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -171,14 +172,11 @@ public final class Accounts {
config().getAccountStorages().setAll(portable);
}
@SuppressWarnings("unchecked")
private static void loadGlobalAccountStorages() {
Path globalAccountsFile = Metadata.HMCL_DIRECTORY.resolve("accounts.json");
if (Files.exists(globalAccountsFile)) {
try (Reader reader = Files.newBufferedReader(globalAccountsFile)) {
globalAccountStorages.setAll((List<Map<Object, Object>>)
Config.CONFIG_GSON.fromJson(reader, new TypeToken<List<Map<Object, Object>>>() {
}.getType()));
globalAccountStorages.setAll(Config.CONFIG_GSON.fromJson(reader, listTypeOf(mapTypeOf(Object.class, Object.class))));
} catch (Throwable e) {
LOG.warning("Failed to load global accounts", e);
}
@@ -164,7 +164,7 @@ public final class Config implements Cloneable, Observable {
private StringProperty launcherFontFamily = new SimpleStringProperty();
@SerializedName("logLines")
private IntegerProperty logLines = new SimpleIntegerProperty(1000);
private ObjectProperty<Integer> logLines = new SimpleObjectProperty<>();
@SerializedName("titleTransparent")
private BooleanProperty titleTransparent = new SimpleBooleanProperty(false);
@@ -573,15 +573,15 @@ public final class Config implements Cloneable, Observable {
this.launcherFontFamily.set(launcherFontFamily);
}
public int getLogLines() {
public Integer getLogLines() {
return logLines.get();
}
public void setLogLines(int logLines) {
public void setLogLines(Integer logLines) {
this.logLines.set(logLines);
}
public IntegerProperty logLinesProperty() {
public ObjectProperty<Integer> logLinesProperty() {
return logLines;
}
@@ -23,6 +23,8 @@ import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
import org.jetbrains.annotations.Nullable;
@@ -51,6 +53,10 @@ public final class GlobalConfig implements Cloneable, Observable {
private final IntegerProperty logRetention = new SimpleIntegerProperty();
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);
@@ -114,11 +120,21 @@ public final class GlobalConfig implements Cloneable, Observable {
this.logRetention.set(logRetention);
}
public ObservableSet<String> getUserJava() {
return userJava;
}
public ObservableSet<String> getDisabledJava() {
return disabledJava;
}
public static final class Serializer implements JsonSerializer<GlobalConfig>, JsonDeserializer<GlobalConfig> {
private static final Set<String> knownFields = new HashSet<>(Arrays.asList(
"agreementVersion",
"platformPromptVersion",
"logRetention"
"logRetention",
"userJava",
"disabledJava"
));
@Override
@@ -131,6 +147,12 @@ public final class GlobalConfig implements Cloneable, Observable {
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
jsonObject.add("platformPromptVersion", context.serialize(src.getPlatformPromptVersion()));
jsonObject.add("logRetention", context.serialize(src.getLogRetention()));
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()));
}
@@ -149,6 +171,20 @@ public final class GlobalConfig implements Cloneable, Observable {
config.setPlatformPromptVersion(Optional.ofNullable(obj.get("platformPromptVersion")).map(JsonElement::getAsInt).orElse(0));
config.setLogRetention(Optional.ofNullable(obj.get("logRetention")).map(JsonElement::getAsInt).orElse(20));
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));
@@ -0,0 +1,25 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
/**
* @author Glavo
*/
public enum JavaVersionType {
DEFAULT, AUTO, VERSION, DETECTED, CUSTOM
}
@@ -24,9 +24,8 @@ import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import java.io.File;
import java.io.IOException;
@@ -36,11 +35,10 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -58,34 +56,6 @@ public class Theme {
Color.web("#B71C1C") // red
};
private static Charset cssCharset;
private static Charset getCssCharset() {
if (cssCharset != null)
return cssCharset;
Charset defaultCharset = Charset.defaultCharset();
if (defaultCharset != StandardCharsets.UTF_8) {
// https://bugs.openjdk.org/browse/JDK-8279328
// For JavaFX 17 or earlier, native encoding should be used
String jfxVersion = System.getProperty("javafx.version");
if (jfxVersion != null) {
Matcher matcher = Pattern.compile("^(?<version>[0-9]+)").matcher(jfxVersion);
if (matcher.find()) {
int v = Lang.parseInt(matcher.group(), -1);
if (v >= 18) {
cssCharset = StandardCharsets.UTF_8;
}
}
}
}
if (cssCharset == null)
cssCharset = defaultCharset;
return cssCharset;
}
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private static Optional<Font> font;
@@ -145,6 +115,14 @@ public class Theme {
return isLight() ? Color.BLACK : Color.WHITE;
}
private static String rgba(Color color, double opacity) {
return String.format("rgba(%d, %d, %d, %.1f)",
(int) Math.ceil(color.getRed() * 256),
(int) Math.ceil(color.getGreen() * 256),
(int) Math.ceil(color.getBlue() * 256),
opacity);
}
public String[] getStylesheets(String overrideFontFamily) {
String css = "/assets/css/blue.css";
@@ -163,30 +141,44 @@ public class Theme {
if (fontFamily != null || !this.color.equalsIgnoreCase(BLUE.color)) {
Color textFill = getForegroundColor();
String fontCss = "";
if (fontFamily != null) {
fontCss = "-fx-font-family: \"" + fontFamily + "\";";
if (fontStyle != null && !fontStyle.isEmpty())
fontCss += " -fx-font-style: \"" + fontStyle + "\";";
}
try {
File temp = File.createTempFile("hmcl", ".css");
String themeText = IOUtils.readFullyAsString(Theme.class.getResourceAsStream("/assets/css/custom.css"))
.replace("%base-color%", color)
.replace("%base-red%", Integer.toString((int) Math.ceil(paint.getRed() * 256)))
.replace("%base-green%", Integer.toString((int) Math.ceil(paint.getGreen() * 256)))
.replace("%base-blue%", Integer.toString((int) Math.ceil(paint.getBlue() * 256)))
.replace("%base-rippler-color%", String.format("rgba(%d, %d, %d, 0.3)", (int) Math.ceil(paint.getRed() * 256), (int) Math.ceil(paint.getGreen() * 256), (int) Math.ceil(paint.getBlue() * 256)))
.replace("%disabled-font-color%", String.format("rgba(%d, %d, %d, 0.7)", (int) Math.ceil(textFill.getRed() * 256), (int) Math.ceil(textFill.getGreen() * 256), (int) Math.ceil(textFill.getBlue() * 256)))
.replace("%font-color%", getColorDisplayName(getForegroundColor()))
.replace("%font%", fontCss);
FileUtils.writeText(temp, themeText, getCssCharset());
temp.deleteOnExit();
css = temp.toURI().toString();
} catch (IOException | NullPointerException e) {
LOG.error("Unable to create theme stylesheet. Fallback to blue theme.", e);
}
StringBuilder themeBuilder = new StringBuilder(512);
themeBuilder.append(".root {")
.append("-fx-base-color:").append(color).append(';')
.append("-fx-base-darker-color: derive(-fx-base-color, -10%);")
.append("-fx-base-check-color: derive(-fx-base-color, 30%);")
.append("-fx-rippler-color:").append(rgba(paint, 0.3)).append(';')
.append("-fx-base-rippler-color: derive(").append(rgba(paint, 0.3)).append(", 100%);")
.append("-fx-base-disabled-text-fill:").append(rgba(textFill, 0.7)).append(";")
.append("-fx-base-text-fill:").append(getColorDisplayName(getForegroundColor())).append(";")
.append("-theme-thumb:").append(rgba(paint, 0.7)).append(";");
if (fontFamily == null)
// https://github.com/HMCL-dev/HMCL/pull/3423
themeBuilder.append("-fx-font-family: -fx-base-font-family;");
else
themeBuilder.append("-fx-font-family:\"").append(fontFamily).append("\";");
if (fontStyle != null && !fontStyle.isEmpty())
themeBuilder.append("-fx-font-style:\"").append(fontStyle).append("\";");
themeBuilder.append('}');
if (FXUtils.JAVAFX_MAJOR_VERSION >= 17)
// JavaFX 17+ support loading stylesheets from data URIs
// https://bugs.openjdk.org/browse/JDK-8267554
css = "data:text/css;charset=UTF-8;base64," + Base64.getEncoder().encodeToString(themeBuilder.toString().getBytes(StandardCharsets.UTF_8));
else
try {
File temp = File.createTempFile("hmcl", ".css");
// For JavaFX 17 or earlier, CssParser uses the default charset
// https://bugs.openjdk.org/browse/JDK-8279328
FileUtils.writeText(temp, themeBuilder.toString(), Charset.defaultCharset());
temp.deleteOnExit();
css = temp.toURI().toString();
} catch (IOException | NullPointerException e) {
LOG.error("Unable to create theme stylesheet. Fallback to blue theme.", e);
}
}
return new String[]{css, "/assets/css/root.css"};
@@ -235,7 +227,7 @@ public class Theme {
}
public static String getColorDisplayName(Color c) {
return c != null ? String.format("#%02x%02x%02x", Math.round(c.getRed() * 255.0D), Math.round(c.getGreen() * 255.0D), Math.round(c.getBlue() * 255.0D)).toUpperCase(Locale.ROOT) : null;
return c != null ? String.format("#%02X%02X%02X", Math.round(c.getRed() * 255.0D), Math.round(c.getGreen() * 255.0D), Math.round(c.getBlue() * 255.0D)) : null;
}
private static ObjectBinding<Color> FOREGROUND_FILL;
@@ -22,29 +22,23 @@ import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.property.*;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.*;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
*
* @author huangyuhui
*/
@JsonAdapter(VersionSetting.Serializer.class)
@@ -84,38 +78,43 @@ public final class VersionSetting implements Cloneable {
// java
private final StringProperty javaProperty = new SimpleStringProperty(this, "java", "");
private final ObjectProperty<JavaVersionType> javaVersionTypeProperty = new SimpleObjectProperty<>(this, "javaVersionType", JavaVersionType.AUTO);
public StringProperty javaProperty() {
return javaProperty;
public ObjectProperty<JavaVersionType> javaVersionTypeProperty() {
return javaVersionTypeProperty;
}
/**
* Java version or "Custom" if user customizes java directory, "Default" if the jvm that this app relies on.
*/
public String getJava() {
return javaProperty.get();
public JavaVersionType getJavaVersionType() {
return javaVersionTypeProperty.get();
}
public void setJava(String java) {
javaProperty.set(java);
public void setJavaVersionType(JavaVersionType javaVersionType) {
javaVersionTypeProperty.set(javaVersionType);
}
public boolean isUsesCustomJavaDir() {
return "Custom".equals(getJava());
private final StringProperty javaVersionProperty = new SimpleStringProperty(this, "javaVersion", "");
public StringProperty javaVersionProperty() {
return javaVersionProperty;
}
public String getJavaVersion() {
return javaVersionProperty.get();
}
public void setJavaVersion(String java) {
javaVersionProperty.set(java);
}
public void setUsesCustomJavaDir() {
setJava("Custom");
setJavaVersionType(JavaVersionType.CUSTOM);
setJavaVersion("");
setDefaultJavaPath(null);
}
public boolean isJavaAutoSelected() {
return "Auto".equals(getJava());
}
public void setJavaAutoSelected() {
setJava("Auto");
setJavaVersionType(JavaVersionType.AUTO);
setJavaVersion("");
setDefaultJavaPath(null);
}
@@ -644,58 +643,74 @@ public final class VersionSetting implements Cloneable {
launcherVisibilityProperty.set(launcherVisibility);
}
public Task<JavaVersion> getJavaVersion(GameVersionNumber gameVersion, Version version) {
return getJavaVersion(gameVersion, version, true);
}
public JavaRuntime getJava(GameVersionNumber gameVersion, Version version) throws InterruptedException {
switch (getJavaVersionType()) {
case DEFAULT:
return JavaRuntime.getDefault();
case AUTO:
return JavaManager.findSuitableJava(gameVersion, version);
case CUSTOM:
try {
return JavaManager.getJava(Paths.get(getJavaDir()));
} catch (IOException | InvalidPathException e) {
return null; // Custom Java not found
}
case VERSION: {
String javaVersion = getJavaVersion();
if (StringUtils.isBlank(javaVersion)) {
return JavaManager.findSuitableJava(gameVersion, version);
}
public Task<JavaVersion> getJavaVersion(GameVersionNumber gameVersion, Version version, boolean checkJava) {
return Task.runAsync(Schedulers.javafx(), () -> {
if (StringUtils.isBlank(getJava())) {
setJava(StringUtils.isBlank(getJavaDir()) ? "Auto" : "Custom");
}
}).thenSupplyAsync(() -> {
try {
if ("Default".equals(getJava())) {
return JavaVersion.fromCurrentEnvironment();
} else if (isJavaAutoSelected()) {
return JavaVersionConstraint.findSuitableJavaVersion(gameVersion, version);
} else if (isUsesCustomJavaDir()) {
try {
if (checkJava)
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
else
return new JavaVersion(Paths.get(getJavaDir()), "", Platform.getPlatform(OperatingSystem.CURRENT_OS, Architecture.UNKNOWN));
} catch (IOException | InvalidPathException e) {
return null; // Custom Java Directory not found,
}
} else if (StringUtils.isNotBlank(getJava())) {
List<JavaVersion> matchedJava = JavaVersion.getJavas().stream()
.filter(java -> java.getVersion().equals(getJava()))
.collect(Collectors.toList());
if (matchedJava.isEmpty()) {
FXUtils.runInFX(() -> setJava("Auto"));
return JavaVersion.fromCurrentEnvironment();
} else {
return matchedJava.stream()
.filter(java -> java.getBinary().toString().equals(getDefaultJavaPath()))
.findFirst()
.orElse(matchedJava.get(0));
}
} else throw new Error();
} catch (InterruptedException e) {
throw new CancellationException();
}
});
}
int majorVersion = -1;
try {
majorVersion = Integer.parseInt(javaVersion);
} catch (NumberFormatException ignored) {
}
public void setJavaVersion(JavaVersion java) {
setJava(java.getVersion());
setDefaultJavaPath(java.getBinary().toString());
if (majorVersion < 0) {
LOG.warning("Invalid Java version: " + javaVersion);
return null;
}
final int finalMajorVersion = majorVersion;
Collection<JavaRuntime> allJava = JavaManager.getAllJava().stream()
.filter(it -> it.getParsedVersion() == finalMajorVersion)
.collect(Collectors.toList());
return JavaManager.findSuitableJava(allJava, gameVersion, version);
}
case DETECTED: {
String javaVersion = getJavaVersion();
if (StringUtils.isBlank(javaVersion)) {
return JavaManager.findSuitableJava(gameVersion, version);
}
try {
String defaultJavaPath = getDefaultJavaPath();
if (StringUtils.isNotBlank(defaultJavaPath)) {
JavaRuntime java = JavaManager.getJava(Paths.get(defaultJavaPath).toRealPath());
if (java != null && java.getVersion().equals(javaVersion)) {
return java;
}
}
} catch (IOException | InvalidPathException ignored) {
}
for (JavaRuntime java : JavaManager.getAllJava()) {
if (java.getVersion().equals(javaVersion)) {
return java;
}
}
return null;
}
default:
throw new AssertionError("JavaVersionType: " + getJavaVersionType());
}
}
public void addPropertyChangedListener(InvalidationListener listener) {
usesGlobalProperty.addListener(listener);
javaProperty.addListener(listener);
javaVersionProperty.addListener(listener);
javaDirProperty.addListener(listener);
wrapperProperty.addListener(listener);
permSizeProperty.addListener(listener);
@@ -733,7 +748,8 @@ public final class VersionSetting implements Cloneable {
public VersionSetting clone() {
VersionSetting versionSetting = new VersionSetting();
versionSetting.setUsesGlobal(isUsesGlobal());
versionSetting.setJava(getJava());
versionSetting.setJavaVersionType(getJavaVersionType());
versionSetting.setJavaVersion(getJavaVersion());
versionSetting.setDefaultJavaPath(getDefaultJavaPath());
versionSetting.setJavaDir(getJavaDir());
versionSetting.setWrapper(getWrapper());
@@ -787,7 +803,6 @@ public final class VersionSetting implements Cloneable {
obj.addProperty("precalledCommand", src.getPreLaunchCommand());
obj.addProperty("postExitCommand", src.getPostExitCommand());
obj.addProperty("serverIp", src.getServerIp());
obj.addProperty("java", src.getJava());
obj.addProperty("wrapper", src.getWrapper());
obj.addProperty("fullscreen", src.isFullscreen());
obj.addProperty("noJVMArgs", src.isNoJVMArgs());
@@ -806,6 +821,24 @@ public final class VersionSetting implements Cloneable {
obj.addProperty("nativesDirType", src.getNativesDirType().ordinal());
obj.addProperty("versionIcon", src.getVersionIcon().ordinal());
obj.addProperty("javaVersionType", src.getJavaVersionType().name());
String java;
switch (src.getJavaVersionType()) {
case DEFAULT:
java = "Default";
break;
case AUTO:
java = "Auto";
break;
case CUSTOM:
java = "Custom";
break;
default:
java = src.getJavaVersion();
break;
}
obj.addProperty("java", java);
obj.addProperty("renderer", src.getRenderer().name());
if (src.getRenderer() == Renderer.LLVMPIPE)
obj.addProperty("useSoftwareRenderer", true);
@@ -846,7 +879,6 @@ public final class VersionSetting implements Cloneable {
vs.setPreLaunchCommand(Optional.ofNullable(obj.get("precalledCommand")).map(JsonElement::getAsString).orElse(""));
vs.setPostExitCommand(Optional.ofNullable(obj.get("postExitCommand")).map(JsonElement::getAsString).orElse(""));
vs.setServerIp(Optional.ofNullable(obj.get("serverIp")).map(JsonElement::getAsString).orElse(""));
vs.setJava(Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse(""));
vs.setWrapper(Optional.ofNullable(obj.get("wrapper")).map(JsonElement::getAsString).orElse(""));
vs.setGameDir(Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse(""));
vs.setNativesDir(Optional.ofNullable(obj.get("nativesDir")).map(JsonElement::getAsString).orElse(""));
@@ -865,6 +897,27 @@ public final class VersionSetting implements Cloneable {
vs.setNativesDirType(getOrDefault(NativesDirectoryType.values(), obj.get("nativesDirType"), NativesDirectoryType.VERSION_FOLDER));
vs.setVersionIcon(getOrDefault(VersionIconType.values(), obj.get("versionIcon"), VersionIconType.DEFAULT));
if (obj.get("javaVersionType") != null) {
JavaVersionType javaVersionType = parseJsonPrimitive(obj.getAsJsonPrimitive("javaVersionType"), JavaVersionType.class, JavaVersionType.AUTO);
vs.setJavaVersionType(javaVersionType);
vs.setJavaVersion(Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse(null));
} else {
String java = Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse("");
switch (java) {
case "Default":
vs.setJavaVersionType(JavaVersionType.DEFAULT);
break;
case "Auto":
vs.setJavaVersionType(JavaVersionType.AUTO);
break;
case "Custom":
vs.setJavaVersionType(JavaVersionType.CUSTOM);
break;
default:
vs.setJavaVersion(java);
}
}
vs.setRenderer(Optional.ofNullable(obj.get("renderer")).map(JsonElement::getAsString)
.flatMap(name -> {
try {
@@ -892,5 +945,25 @@ public final class VersionSetting implements Cloneable {
else
return Lang.parseInt(primitive.getAsString(), defaultValue);
}
private <E extends Enum<E>> E parseJsonPrimitive(JsonPrimitive primitive, Class<E> clazz, E defaultValue) {
if (primitive == null)
return defaultValue;
else {
E[] enumConstants = clazz.getEnumConstants();
if (primitive.isNumber()) {
int index = primitive.getAsInt();
return index >= 0 && index < enumConstants.length ? enumConstants[index] : defaultValue;
} else {
String name = primitive.getAsString();
for (E enumConstant : enumConstants) {
if (enumConstant.name().equalsIgnoreCase(name)) {
return enumConstant;
}
}
return defaultValue;
}
}
}
}
}
@@ -37,6 +37,7 @@ import javafx.stage.StageStyle;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
@@ -53,7 +54,6 @@ import org.jackhuang.hmcl.ui.versions.VersionPage;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
@@ -252,7 +252,7 @@ public final class Controllers {
dialog(i18n("launcher.cache_directory.invalid"));
}
Task.runAsync(JavaVersion::initialize).start();
Lang.thread(JavaManager::initialize, "Search Java", true);
scene = new Scene(decorator.getDecorator());
scene.setFill(Color.TRANSPARENT);
@@ -391,9 +391,6 @@ public final class Controllers {
Controllers.getSettingsPage().showFeedback();
Controllers.navigate(Controllers.getSettingsPage());
break;
case "hmcl://hide-announcement":
Controllers.getRootPage().getMainPage().hideAnnouncementPane();
break;
}
} else {
FXUtils.openLink(href);
@@ -53,7 +53,7 @@ public class CrashWindow extends Stage {
Button btnContact = new Button();
btnContact.setText(i18n("launcher.contact"));
btnContact.setOnMouseClicked(event -> FXUtils.openLink(Metadata.CONTACT_URL));
btnContact.setOnAction(event -> FXUtils.openLink(Metadata.CONTACT_URL));
HBox box = new HBox();
box.setStyle("-fx-padding: 8px;");
box.getChildren().add(btnContact);
@@ -23,14 +23,14 @@ import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.beans.value.WritableValue;
import javafx.beans.value.*;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.*;
import javafx.scene.image.Image;
@@ -40,6 +40,7 @@ import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
@@ -50,12 +51,12 @@ import javafx.util.StringConverter;
import org.glavo.png.PNGType;
import org.glavo.png.PNGWriter;
import org.glavo.png.javafx.PNGJavaFXUtils;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
@@ -67,26 +68,24 @@ import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import javax.swing.*;
import javax.swing.event.HyperlinkEvent;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.awt.*;
import java.io.*;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.Lang.thread;
@@ -98,6 +97,20 @@ public final class FXUtils {
private FXUtils() {
}
public static final int JAVAFX_MAJOR_VERSION;
static {
String jfxVersion = System.getProperty("javafx.version");
int majorVersion = -1;
if (jfxVersion != null) {
Matcher matcher = Pattern.compile("^(?<version>[0-9]+)").matcher(jfxVersion);
if (matcher.find()) {
majorVersion = Lang.parseInt(matcher.group(), -1);
}
}
JAVAFX_MAJOR_VERSION = majorVersion;
}
public static final String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace";
private static final Map<String, Image> builtinImageCache = new ConcurrentHashMap<>();
@@ -300,8 +313,12 @@ public final class FXUtils {
ScrollUtils.addSmoothScrolling(scrollPane);
}
private static final Duration TOOLTIP_FAST_SHOW_DELAY = Duration.millis(50);
private static final Duration TOOLTIP_SLOW_SHOW_DELAY = Duration.millis(500);
private static final Duration TOOLTIP_SHOW_DURATION = Duration.millis(5000);
public static void installFastTooltip(Node node, Tooltip tooltip) {
installTooltip(node, 50, 5000, 0, tooltip);
runInFX(() -> TooltipInstaller.INSTALLER.installTooltip(node, TOOLTIP_FAST_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
}
public static void installFastTooltip(Node node, String tooltip) {
@@ -309,39 +326,13 @@ public final class FXUtils {
}
public static void installSlowTooltip(Node node, Tooltip tooltip) {
installTooltip(node, 500, 5000, 0, tooltip);
runInFX(() -> TooltipInstaller.INSTALLER.installTooltip(node, TOOLTIP_SLOW_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
}
public static void installSlowTooltip(Node node, String tooltip) {
installSlowTooltip(node, new Tooltip(tooltip));
}
public static void installTooltip(Node node, double openDelay, double visibleDelay, double closeDelay, Tooltip tooltip) {
runInFX(() -> {
try {
// Java 8
Class<?> behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior");
Constructor<?> behaviorConstructor = behaviorClass.getDeclaredConstructor(Duration.class, Duration.class, Duration.class, boolean.class);
behaviorConstructor.setAccessible(true);
Object behavior = behaviorConstructor.newInstance(new Duration(openDelay), new Duration(visibleDelay), new Duration(closeDelay), false);
Method installMethod = behaviorClass.getDeclaredMethod("install", Node.class, Tooltip.class);
installMethod.setAccessible(true);
installMethod.invoke(behavior, node, tooltip);
} catch (ReflectiveOperationException e) {
try {
// Java 9
Tooltip.class.getMethod("setShowDelay", Duration.class).invoke(tooltip, new Duration(openDelay));
Tooltip.class.getMethod("setShowDuration", Duration.class).invoke(tooltip, new Duration(visibleDelay));
Tooltip.class.getMethod("setHideDelay", Duration.class).invoke(tooltip, new Duration(closeDelay));
} catch (ReflectiveOperationException e2) {
e.addSuppressed(e2);
LOG.error("Cannot install tooltip", e);
}
Tooltip.install(node, tooltip);
}
});
}
public static void playAnimation(Node node, String animationKey, Timeline timeline) {
animationKey = "FXUTILS.ANIMATION." + animationKey;
Object oldTimeline = node.getProperties().get(animationKey);
@@ -408,6 +399,22 @@ public final class FXUtils {
});
}
private static String which(String command) {
String path = System.getenv("PATH");
if (path == null)
return null;
for (String item : path.split(OperatingSystem.PATH_SEPARATOR)) {
try {
Path program = Paths.get(item, command);
if (Files.isExecutable(program))
return program.toRealPath().toString();
} catch (Throwable ignored) {
}
}
return null;
}
public static void showFileInExplorer(Path file) {
String path = file.toAbsolutePath().toString();
@@ -416,6 +423,16 @@ public final class FXUtils {
openCommands = new String[]{"explorer.exe", "/select,", path};
else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
openCommands = new String[]{"/usr/bin/open", "-R", path};
else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && which("dbus-send") != null)
openCommands = new String[]{
"dbus-send",
"--print-reply",
"--dest=org.freedesktop.FileManager1",
"/org/freedesktop/FileManager1",
"org.freedesktop.FileManager1.ShowItems",
"array:string:" + file.toAbsolutePath().toUri(),
"string:"
};
else
openCommands = null;
@@ -472,12 +489,13 @@ public final class FXUtils {
}
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
for (String browser : linuxBrowsers) {
try (final InputStream is = Runtime.getRuntime().exec(new String[]{"which", browser}).getInputStream()) {
if (is.read() != -1) {
String path = which(browser);
if (path != null) {
try {
Runtime.getRuntime().exec(new String[]{browser, link});
return;
} catch (Throwable ignored) {
}
} catch (Throwable ignored) {
}
LOG.warning("No known browser found");
}
@@ -496,52 +514,6 @@ public final class FXUtils {
});
}
public static void showWebDialog(String title, String content) {
showWebDialog(title, content, 800, 480);
}
public static void showWebDialog(String title, String content, int width, int height) {
try {
WebStage stage = new WebStage(width, height);
stage.getWebView().getEngine().loadContent(content);
stage.setTitle(title);
stage.showAndWait();
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
LOG.warning("WebView is missing or initialization failed, use JEditorPane replaced", e);
SwingUtils.initLookAndFeel();
SwingUtilities.invokeLater(() -> {
final JFrame frame = new JFrame(title);
frame.setSize(width, height);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setLocationByPlatform(true);
frame.setIconImage(new ImageIcon(FXUtils.class.getResource("/assets/img/icon.png")).getImage());
frame.setLayout(new BorderLayout());
final JProgressBar progressBar = new JProgressBar();
progressBar.setIndeterminate(true);
frame.add(progressBar, BorderLayout.PAGE_START);
Schedulers.defaultScheduler().execute(() -> {
final JEditorPane pane = new JEditorPane("text/html", content);
pane.setEditable(false);
pane.addHyperlinkListener(event -> {
if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
openLink(event.getURL().toExternalForm());
}
});
SwingUtilities.invokeLater(() -> {
progressBar.setVisible(false);
frame.add(new JScrollPane(pane), BorderLayout.CENTER);
});
});
frame.setVisible(true);
frame.toFront();
});
}
}
public static <T> void bind(JFXTextField textField, Property<T> property, StringConverter<T> converter) {
textField.setText(converter == null ? (String) property.getValue() : converter.toString(property.getValue()));
TextFieldBindingListener<T> listener = new TextFieldBindingListener<>(textField, property, converter);
@@ -676,6 +648,61 @@ public final class FXUtils {
comboBox.getSelectionModel().selectedIndexProperty().removeListener(listener);
}
public static void bindAllEnabled(BooleanProperty allEnabled, BooleanProperty... children) {
int itemCount = children.length;
int childSelectedCount = 0;
for (BooleanProperty child : children) {
if (child.get())
childSelectedCount++;
}
allEnabled.set(childSelectedCount == itemCount);
class Listener implements InvalidationListener {
private int childSelectedCount;
private boolean updating = false;
public Listener(int childSelectedCount) {
this.childSelectedCount = childSelectedCount;
}
@Override
public void invalidated(Observable observable) {
if (updating)
return;
updating = true;
try {
boolean value = ((BooleanProperty) observable).get();
if (observable == allEnabled) {
for (BooleanProperty child : children) {
child.setValue(value);
}
childSelectedCount = value ? itemCount : 0;
} else {
if (value)
childSelectedCount++;
else
childSelectedCount--;
allEnabled.set(childSelectedCount == itemCount);
}
} finally {
updating = false;
}
}
}
InvalidationListener listener = new Listener(childSelectedCount);
WeakInvalidationListener weakListener = new WeakInvalidationListener(listener);
allEnabled.addListener(listener);
for (BooleanProperty child : children) {
child.addListener(weakListener);
}
}
public static void setIcon(Stage stage) {
String icon;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
@@ -815,6 +842,17 @@ public final class FXUtils {
return button;
}
public static Label truncatedLabel(String text, int limit) {
Label label = new Label();
if (text.length() <= limit) {
label.setText(text);
} else {
label.setText(StringUtils.truncate(text, limit));
installFastTooltip(label, text);
}
return label;
}
public static void applyDragListener(Node node, FileFilter filter, Consumer<List<File>> callback) {
applyDragListener(node, filter, callback, null);
}
@@ -912,6 +950,15 @@ public final class FXUtils {
});
}
public static void onClicked(Node node, Runnable action) {
node.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
if (e.getButton() == MouseButton.PRIMARY && e.getClickCount() == 1) {
action.run();
e.consume();
}
});
}
public static void copyText(String text) {
ClipboardContent content = new ClipboardContent();
content.putString(text);
@@ -933,7 +980,7 @@ public final class FXUtils {
Element r = doc.getDocumentElement();
NodeList children = r.getChildNodes();
List<javafx.scene.Node> texts = new ArrayList<>();
List<Node> texts = new ArrayList<>();
for (int i = 0; i < children.getLength(); i++) {
org.w3c.dom.Node node = children.item(i);
@@ -941,8 +988,8 @@ public final class FXUtils {
Element element = (Element) node;
if ("a".equals(element.getTagName())) {
String href = element.getAttribute("href");
JFXHyperlink hyperlink = new JFXHyperlink(element.getTextContent());
hyperlink.setOnAction(e -> {
Text text = new Text(element.getTextContent());
onClicked(text, () -> {
String link = href;
try {
link = new URI(href).toASCIIString();
@@ -950,7 +997,10 @@ public final class FXUtils {
}
hyperlinkAction.accept(link);
});
texts.add(hyperlink);
text.setCursor(Cursor.HAND);
text.setFill(Color.web("#0070E0"));
text.setUnderline(true);
texts.add(text);
} else if ("b".equals(element.getTagName())) {
Text text = new Text(element.getTextContent());
text.getStyleClass().add("bold");
@@ -977,4 +1027,11 @@ public final class FXUtils {
return tf;
}
public static String toWeb(Color color) {
int r = (int) Math.round(color.getRed() * 255.0);
int g = (int) Math.round(color.getGreen() * 255.0);
int b = (int) Math.round(color.getBlue() * 255.0);
return String.format("#%02x%02x%02x", r, g, b);
}
}
@@ -20,8 +20,6 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
@@ -43,6 +41,7 @@ import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.logging.Logger;
import org.jackhuang.hmcl.util.Pair;
@@ -66,7 +65,6 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -77,8 +75,6 @@ public class GameCrashWindow extends Stage {
private final String total_memory;
private final String java;
private final LibraryAnalyzer analyzer;
private final StringProperty os = new SimpleStringProperty(OperatingSystem.SYSTEM_NAME);
private final StringProperty arch = new SimpleStringProperty(Architecture.SYSTEM_ARCH.getDisplayName());
private final TextFlow reasonTextFlow = new TextFlow(new Text(i18n("game.crash.reason.unknown")));
private final BooleanProperty loading = new SimpleBooleanProperty();
private final TextFlow feedbackTextFlow = new TextFlow();
@@ -89,9 +85,9 @@ public class GameCrashWindow extends Stage {
private final LaunchOptions launchOptions;
private final View view;
private final Collection<Pair<String, Log4jLevel>> logs;
private final List<Log> logs;
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, Collection<Pair<String, Log4jLevel>> logs) {
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, List<Log> logs) {
this.managedProcess = managedProcess;
this.exitType = exitType;
this.repository = repository;
@@ -124,7 +120,7 @@ public class GameCrashWindow extends Stage {
private void analyzeCrashReport() {
loading.set(true);
Task.allOf(Task.supplyAsync(() -> {
String rawLog = logs.stream().map(Pair::getKey).collect(Collectors.joining("\n"));
String rawLog = logs.stream().map(Log::getLog).collect(Collectors.joining("\n"));
// Get the crash-report from the crash-reports/xxx, or the output of console.
String crashReport = null;
@@ -264,20 +260,18 @@ public class GameCrashWindow extends Stage {
private void showLogWindow() {
LogWindow logWindow = new LogWindow(managedProcess);
logWindow.logLine(Logger.filterForbiddenToken("Command: " + new CommandBuilder().addAll(managedProcess.getCommands())), Log4jLevel.INFO);
logWindow.logLine(new Log(Logger.filterForbiddenToken("Command: " + new CommandBuilder().addAll(managedProcess.getCommands())), Log4jLevel.INFO));
if (managedProcess.getClasspath() != null)
logWindow.logLine("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO);
for (Map.Entry<String, Log4jLevel> entry : logs)
logWindow.logLine(entry.getKey(), entry.getValue());
logWindow.showNormal();
logWindow.logLine(new Log("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO));
logWindow.logLines(logs);
logWindow.show();
}
private void exportGameCrashInfo() {
Path logFile = Paths.get("minecraft-exported-crash-info-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".zip").toAbsolutePath();
CompletableFuture.supplyAsync(() ->
logs.stream().map(Pair::getKey).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)))
logs.stream().map(Log::getLog).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)))
.thenComposeAsync(logs ->
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
.handleAsync((result, exception) -> {
@@ -358,12 +352,12 @@ public class GameCrashWindow extends Stage {
TwoLineListItem os = new TwoLineListItem();
os.getStyleClass().setAll("two-line-item-second-large");
os.setTitle(i18n("system.operating_system"));
os.subtitleProperty().bind(GameCrashWindow.this.os);
os.setSubtitle(Lang.requireNonNullElse(OperatingSystem.OS_RELEASE_NAME, OperatingSystem.SYSTEM_NAME));
TwoLineListItem arch = new TwoLineListItem();
arch.getStyleClass().setAll("two-line-item-second-large");
arch.setTitle(i18n("system.architecture"));
arch.subtitleProperty().bind(GameCrashWindow.this.arch);
arch.setSubtitle(Architecture.SYSTEM_ARCH.getDisplayName());
infoPane.getChildren().setAll(launcher, version, total_memory, memory, java, os, arch);
}
@@ -392,13 +386,13 @@ public class GameCrashWindow extends Stage {
gameDir.getStyleClass().setAll("two-line-item-second-large");
gameDir.setTitle(i18n("game.directory"));
gameDir.setSubtitle(launchOptions.getGameDir().getAbsolutePath());
runInFX(() -> FXUtils.installFastTooltip(gameDir, i18n("game.directory")));
FXUtils.installFastTooltip(gameDir, i18n("game.directory"));
TwoLineListItem javaDir = new TwoLineListItem();
javaDir.getStyleClass().setAll("two-line-item-second-large");
javaDir.setTitle(i18n("settings.game.java_directory"));
javaDir.setSubtitle(launchOptions.getJava().getBinary().toAbsolutePath().toString());
runInFX(() -> FXUtils.installFastTooltip(javaDir, i18n("settings.game.java_directory")));
FXUtils.installFastTooltip(javaDir, i18n("settings.game.java_directory"));
Label reasonTitle = new Label(i18n("game.crash.reason"));
reasonTitle.getStyleClass().add("two-line-item-second-large-title");
@@ -422,14 +416,14 @@ public class GameCrashWindow extends Stage {
HBox toolBar = new HBox();
{
JFXButton exportGameCrashInfoButton = FXUtils.newRaisedButton(i18n("logwindow.export_game_crash_logs"));
exportGameCrashInfoButton.setOnMouseClicked(e -> exportGameCrashInfo());
exportGameCrashInfoButton.setOnAction(e -> exportGameCrashInfo());
JFXButton logButton = FXUtils.newRaisedButton(i18n("logwindow.title"));
logButton.setOnMouseClicked(e -> showLogWindow());
logButton.setOnAction(e -> showLogWindow());
JFXButton helpButton = FXUtils.newRaisedButton(i18n("help"));
helpButton.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/help.html"));
runInFX(() -> FXUtils.installFastTooltip(helpButton, i18n("logwindow.help")));
FXUtils.installFastTooltip(helpButton, i18n("logwindow.help"));
toolBar.setPadding(new Insets(8));
@@ -0,0 +1,266 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.scene.Cursor;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class HTMLRenderer {
private static URI resolveLink(Node linkNode) {
String href = linkNode.absUrl("href");
if (href.isEmpty())
return null;
try {
return new URI(href);
} catch (Throwable e) {
return null;
}
}
private final List<javafx.scene.Node> children = new ArrayList<>();
private final List<Node> stack = new ArrayList<>();
private boolean bold;
private boolean italic;
private boolean underline;
private boolean strike;
private boolean highlight;
private String headerLevel;
private Node hyperlink;
private final Consumer<URI> onClickHyperlink;
public HTMLRenderer(Consumer<URI> onClickHyperlink) {
this.onClickHyperlink = onClickHyperlink;
}
private void updateStyle() {
bold = false;
italic = false;
underline = false;
strike = false;
highlight = false;
headerLevel = null;
hyperlink = null;
for (Node node : stack) {
String nodeName = node.nodeName();
switch (nodeName) {
case "b":
case "strong":
bold = true;
break;
case "i":
case "em":
italic = true;
break;
case "ins":
underline = true;
break;
case "del":
strike = true;
break;
case "mark":
highlight = true;
break;
case "a":
hyperlink = node;
break;
case "h1":
case "h2":
case "h3":
case "h4":
case "h5":
case "h6":
headerLevel = nodeName;
break;
}
}
}
private void pushNode(Node node) {
stack.add(node);
updateStyle();
}
private void popNode() {
stack.remove(stack.size() - 1);
updateStyle();
}
private void applyStyle(Text text) {
if (hyperlink != null) {
URI target = resolveLink(hyperlink);
if (target != null) {
FXUtils.onClicked(text, () -> onClickHyperlink.accept(target));
text.setCursor(Cursor.HAND);
}
text.getStyleClass().add("html-hyperlink");
}
if (hyperlink != null || underline)
text.setUnderline(true);
if (strike)
text.setStrikethrough(true);
if (bold || highlight)
text.getStyleClass().add("html-bold");
if (italic)
text.getStyleClass().add("html-italic");
if (headerLevel != null)
text.getStyleClass().add("html-" + headerLevel);
}
private void appendText(String text) {
Text textNode = new Text(text);
applyStyle(textNode);
children.add(textNode);
}
private void appendImage(Node node) {
String src = node.absUrl("src");
URI imageUri = null;
try {
if (!src.isEmpty())
imageUri = URI.create(src);
} catch (Exception ignored) {
}
String alt = node.attr("alt");
if (imageUri != null) {
URI uri = URI.create(src);
String widthAttr = node.attr("width");
String heightAttr = node.attr("height");
double width = 0;
double height = 0;
if (!widthAttr.isEmpty() && !heightAttr.isEmpty()) {
try {
width = Double.parseDouble(widthAttr);
height = Double.parseDouble(heightAttr);
} catch (NumberFormatException ignored) {
}
if (width <= 0 || height <= 0) {
width = 0;
height = 0;
}
}
Image image = FXUtils.newRemoteImage(uri.toString(), width, height, true, true, false);
if (image.isError()) {
LOG.warning("Failed to load image: " + uri, image.getException());
} else {
ImageView imageView = new ImageView(image);
if (hyperlink != null) {
URI target = resolveLink(hyperlink);
if (target != null) {
FXUtils.onClicked(imageView, () -> onClickHyperlink.accept(target));
imageView.setCursor(Cursor.HAND);
}
}
children.add(imageView);
return;
}
}
if (!alt.isEmpty())
appendText(alt);
}
public void appendNode(Node node) {
if (node instanceof TextNode) {
appendText(((TextNode) node).text());
}
String name = node.nodeName();
switch (name) {
case "img":
appendImage(node);
break;
case "li":
appendText("\n \u2022 ");
break;
case "dt":
appendText(" ");
break;
case "p":
case "h1":
case "h2":
case "h3":
case "h4":
case "h5":
case "h6":
case "tr":
if (!children.isEmpty())
appendText("\n\n");
break;
}
if (node.childNodeSize() > 0) {
pushNode(node);
for (Node childNode : node.childNodes()) {
appendNode(childNode);
}
popNode();
}
switch (name) {
case "br":
case "dd":
case "p":
case "h1":
case "h2":
case "h3":
case "h4":
case "h5":
case "h6":
appendText("\n");
break;
}
}
public TextFlow render() {
TextFlow textFlow = new TextFlow();
textFlow.getStyleClass().add("html");
textFlow.getChildren().setAll(children);
return textFlow;
}
}
@@ -23,7 +23,6 @@ import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.PseudoClass;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
@@ -33,7 +32,7 @@ import javafx.scene.control.Label;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.setting.Theme;
@@ -60,8 +59,8 @@ public class InstallerItem extends Control {
private final ObjectProperty<InstalledState> versionProperty = new SimpleObjectProperty<>(this, "version", null);
private final ObjectProperty<State> resolvedStateProperty = new SimpleObjectProperty<>(this, "resolvedState", InstallableState.INSTANCE);
private final ObjectProperty<EventHandler<? super MouseEvent>> installActionProperty = new SimpleObjectProperty<>(this, "installAction");
private final ObjectProperty<EventHandler<? super MouseEvent>> removeActionProperty = new SimpleObjectProperty<>(this, "removeAction");
private final ObjectProperty<Runnable> onInstall = new SimpleObjectProperty<>(this, "onInstall");
private final ObjectProperty<Runnable> onRemove = new SimpleObjectProperty<>(this, "onRemove");
public interface State {
}
@@ -170,12 +169,28 @@ public class InstallerItem extends Control {
return resolvedStateProperty;
}
public ObjectProperty<EventHandler<? super MouseEvent>> installActionProperty() {
return installActionProperty;
public ObjectProperty<Runnable> onInstallProperty() {
return onInstall;
}
public ObjectProperty<EventHandler<? super MouseEvent>> removeActionProperty() {
return removeActionProperty;
public Runnable getOnInstall() {
return onInstall.get();
}
public void setOnInstall(Runnable onInstall) {
this.onInstall.set(onInstall);
}
public ObjectProperty<Runnable> onRemoveProperty() {
return onRemove;
}
public Runnable getOnRemove() {
return onRemove.get();
}
public void setOnRemove(Runnable onRemove) {
this.onRemove.set(onRemove);
}
@Override
@@ -371,7 +386,11 @@ public class InstallerItem extends Control {
removeButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> control.resolvedStateProperty.get() instanceof InstalledState, control.resolvedStateProperty));
}
removeButton.managedProperty().bind(removeButton.visibleProperty());
removeButton.onMouseClickedProperty().bind(control.removeActionProperty);
removeButton.setOnAction(e -> {
Runnable onRemove = control.getOnRemove();
if (onRemove != null)
onRemove.run();
});
buttonsContainer.getChildren().add(removeButton);
JFXButton installButton = new JFXButton();
@@ -383,7 +402,7 @@ public class InstallerItem extends Control {
));
installButton.getStyleClass().add("toggle-icon4");
installButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> {
if (control.installActionProperty.get() == null) {
if (control.getOnInstall() == null) {
return false;
}
@@ -396,18 +415,27 @@ public class InstallerItem extends Control {
}
return false;
}, control.resolvedStateProperty, control.installActionProperty));
}, control.resolvedStateProperty, control.onInstall));
installButton.managedProperty().bind(installButton.visibleProperty());
installButton.onMouseClickedProperty().bind(control.installActionProperty);
installButton.setOnAction(e -> {
Runnable onInstall = control.getOnInstall();
if (onInstall != null)
onInstall.run();
});
buttonsContainer.getChildren().add(installButton);
FXUtils.onChangeAndOperate(installButton.visibleProperty(), clickable -> {
if (clickable) {
container.onMouseClickedProperty().bind(control.installActionProperty);
container.setOnMouseClicked(event -> {
Runnable onInstall = control.getOnInstall();
if (onInstall != null && event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 1) {
onInstall.run();
event.consume();
}
});
pane.setCursor(Cursor.HAND);
} else {
container.onMouseClickedProperty().unbind();
container.onMouseClickedProperty().set(null);
container.setOnMouseClicked(null);
pane.setCursor(Cursor.DEFAULT);
}
});
@@ -70,7 +70,7 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
btnAdd.getStyleClass().add("jfx-button-raised-round");
btnAdd.setButtonType(JFXButton.ButtonType.RAISED);
btnAdd.setGraphic(SVG.PLUS.createIcon(Theme.whiteFill(), -1, -1));
btnAdd.setOnMouseClicked(e -> skinnable.add());
btnAdd.setOnAction(e -> skinnable.add());
JFXButton btnRefresh = new JFXButton();
FXUtils.setLimitWidth(btnRefresh, 40);
@@ -78,7 +78,7 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
btnRefresh.getStyleClass().add("jfx-button-raised-round");
btnRefresh.setButtonType(JFXButton.ButtonType.RAISED);
btnRefresh.setGraphic(SVG.REFRESH.createIcon(Theme.whiteFill(), -1, -1));
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
btnRefresh.setOnAction(e -> skinnable.refresh());
vBox.getChildren().setAll(btnAdd);
@@ -25,8 +25,8 @@ import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -34,17 +34,19 @@ import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import org.jackhuang.hmcl.game.GameDumpGenerator;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.game.Log;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.CircularArrayList;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import java.io.IOException;
@@ -54,14 +56,11 @@ import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.StringUtils.parseEscapeSequence;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
@@ -69,128 +68,120 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
*/
public final class LogWindow extends Stage {
private final ArrayDeque<Log> logs = new ArrayDeque<>();
private final Map<Log4jLevel, SimpleIntegerProperty> levelCountMap = new EnumMap<Log4jLevel, SimpleIntegerProperty>(Log4jLevel.class) {
{
for (Log4jLevel level : Log4jLevel.values()) put(level, new SimpleIntegerProperty());
private static final Log4jLevel[] LEVELS = {Log4jLevel.FATAL, Log4jLevel.ERROR, Log4jLevel.WARN, Log4jLevel.INFO, Log4jLevel.DEBUG};
private final CircularArrayList<Log> logs;
private final Map<Log4jLevel, SimpleIntegerProperty> levelCountMap = new EnumMap<>(Log4jLevel.class);
private final Map<Log4jLevel, SimpleBooleanProperty> levelShownMap = new EnumMap<>(Log4jLevel.class);
{
for (Log4jLevel level : Log4jLevel.values()) {
levelCountMap.put(level, new SimpleIntegerProperty());
levelShownMap.put(level, new SimpleBooleanProperty(true));
}
};
private final Map<Log4jLevel, SimpleBooleanProperty> levelShownMap = new EnumMap<Log4jLevel, SimpleBooleanProperty>(Log4jLevel.class) {
{
for (Log4jLevel level : Log4jLevel.values()) {
SimpleBooleanProperty property = new SimpleBooleanProperty(true);
put(level, property);
}
}
};
private final LogWindowImpl impl = new LogWindowImpl();
private final ChangeListener<Number> logLinesListener = FXUtils.onWeakChange(config().logLinesProperty(), logLines -> checkLogCount());
private Consumer<String> exportGameCrashInfoCallback;
private boolean stopCheckLogCount = false;
}
private final LogWindowImpl impl;
private final ManagedProcess gameProcess;
public LogWindow(ManagedProcess gameProcess) {
this(gameProcess, new CircularArrayList<>());
}
public LogWindow(ManagedProcess gameProcess, CircularArrayList<Log> logs) {
this.logs = logs;
this.impl = new LogWindowImpl();
setScene(new Scene(impl, 800, 480));
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
setTitle(i18n("logwindow.title"));
FXUtils.setIcon(this);
levelShownMap.values().forEach(property -> property.addListener((a, b, newValue) -> shakeLogs()));
for (SimpleBooleanProperty property : levelShownMap.values()) {
property.addListener(o -> shakeLogs());
}
this.gameProcess = gameProcess;
}
public void logLine(String filteredLine, Log4jLevel level) {
Log log = new Log(parseEscapeSequence(filteredLine), level);
public void logLine(Log log) {
Log4jLevel level = log.getLevel();
logs.add(log);
if (levelShownMap.get(level).get())
impl.listView.getItems().add(log);
levelCountMap.get(level).setValue(levelCountMap.get(level).getValue() + 1);
if (!stopCheckLogCount) checkLogCount();
SimpleIntegerProperty property = levelCountMap.get(log.getLevel());
property.set(property.get() + 1);
checkLogCount();
autoScroll();
}
public void showGameCrashReport(Consumer<String> exportGameCrashInfoCallback) {
this.exportGameCrashInfoCallback = exportGameCrashInfoCallback;
this.impl.showCrashReport.set(true);
stopCheckLogCount = true;
for (Log log : impl.listView.getItems()) {
if (log.log.contains("Minecraft Crash Report")) {
Platform.runLater(() -> {
impl.listView.scrollTo(log);
});
break;
}
public void logLines(List<Log> logs) {
for (Log log : logs) {
Log4jLevel level = log.getLevel();
this.logs.add(log);
if (levelShownMap.get(level).get())
impl.listView.getItems().add(log);
SimpleIntegerProperty property = levelCountMap.get(log.getLevel());
property.set(property.get() + 1);
}
show();
}
public void showNormal() {
this.impl.showCrashReport.set(false);
show();
checkLogCount();
autoScroll();
}
private void shakeLogs() {
impl.listView.getItems().setAll(logs.stream().filter(log -> levelShownMap.get(log.level).get()).collect(Collectors.toList()));
impl.listView.getItems().setAll(logs.stream().filter(log -> levelShownMap.get(log.getLevel()).get()).collect(Collectors.toList()));
autoScroll();
}
private void checkLogCount() {
while (logs.size() > config().getLogLines()) {
int nRemove = logs.size() - Log.getLogLines();
if (nRemove <= 0)
return;
ObservableList<Log> items = impl.listView.getItems();
int itemsSize = items.size();
int count = 0;
for (int i = 0; i < nRemove; i++) {
Log removedLog = logs.removeFirst();
if (!impl.listView.getItems().isEmpty() && impl.listView.getItems().get(0) == removedLog) {
impl.listView.getItems().remove(0);
}
if (itemsSize > count && items.get(count) == removedLog)
count++;
}
items.remove(0, count);
}
private static class Log {
private final String log;
private final Log4jLevel level;
private boolean selected = false;
public Log(String log, Log4jLevel level) {
this.log = log;
this.level = level;
}
private void autoScroll() {
if (!impl.listView.getItems().isEmpty() && impl.autoScroll.get())
impl.listView.scrollTo(impl.listView.getItems().size() - 1);
}
public class LogWindowImpl extends Control {
private final class LogWindowImpl extends Control {
private final ListView<Log> listView = new JFXListView<>();
private final BooleanProperty autoScroll = new SimpleBooleanProperty();
private final List<StringProperty> buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList());
private final List<BooleanProperty> showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList());
private final JFXComboBox<String> cboLines = new JFXComboBox<>();
private final BooleanProperty showCrashReport = new SimpleBooleanProperty();
private final StringProperty[] buttonText = new StringProperty[LEVELS.length];
private final BooleanProperty[] showLevel = new BooleanProperty[LEVELS.length];
private final JFXComboBox<Integer> cboLines = new JFXComboBox<>();
LogWindowImpl() {
getStyleClass().add("log-window");
listView.setItems(FXCollections.observableList(new CircularArrayList<>(config().getLogLines() + 1)));
listView.setItems(FXCollections.observableList(new CircularArrayList<>(logs.size())));
boolean flag = false;
cboLines.getItems().setAll("500", "2000", "5000", "10000");
for (String i : cboLines.getItems())
if (Integer.toString(config().getLogLines()).equals(i)) {
cboLines.getSelectionModel().select(i);
flag = true;
}
for (int i = 0; i < LEVELS.length; i++) {
buttonText[i] = new SimpleStringProperty();
showLevel[i] = new SimpleBooleanProperty(true);
}
if (!flag)
cboLines.getSelectionModel().select(2);
cboLines.getItems().setAll(500, 2000, 5000, 10000);
cboLines.setValue(Log.getLogLines());
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> config().setLogLines(newValue));
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
config().setLogLines(newValue == null ? 1000 : Integer.parseInt(newValue));
});
Log4jLevel[] levels = new Log4jLevel[]{Log4jLevel.FATAL, Log4jLevel.ERROR, Log4jLevel.WARN, Log4jLevel.INFO, Log4jLevel.DEBUG};
String[] suffix = new String[]{"fatals", "errors", "warns", "infos", "debugs"};
for (int i = 0; i < 5; ++i) {
buttonText.get(i).bind(Bindings.concat(levelCountMap.get(levels[i]), " " + suffix[i]));
levelShownMap.get(levels[i]).bind(showLevel.get(i));
for (int i = 0; i < LEVELS.length; ++i) {
buttonText[i].bind(Bindings.concat(levelCountMap.get(LEVELS[i]), " " + LEVELS[i].name().toLowerCase(Locale.ROOT) + "s"));
levelShownMap.get(LEVELS[i]).bind(showLevel[i]);
}
}
@@ -207,7 +198,7 @@ public final class LogWindow extends Stage {
thread(() -> {
Path logFile = Paths.get("minecraft-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
try {
Files.write(logFile, logs.stream().map(x -> x.log).collect(Collectors.toList()));
Files.write(logFile, logs.stream().map(Log::getLog).collect(Collectors.toList()));
} catch (IOException e) {
LOG.warning("Failed to export logs", e);
return;
@@ -223,38 +214,31 @@ public final class LogWindow extends Stage {
});
}
private void onExportDump(JFXButton button) {
private void onExportDump(SpinnerPane pane) {
assert SystemUtils.supportJVMAttachment();
pane.setLoading(true);
thread(() -> {
if (button.getText().equals(i18n("logwindow.export_dump.dependency_ok.button"))) {
if (SystemUtils.supportJVMAttachment()) {
Platform.runLater(() -> button.setText(i18n("logwindow.export_dump.dependency_ok.doing_button")));
Path dumpFile = Paths.get("minecraft-exported-jstack-dump-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
Path dumpFile = Paths.get("minecraft-exported-jstack-dump-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
try {
if (gameProcess.isRunning()) {
GameDumpGenerator.writeDumpTo(gameProcess.getPID(), dumpFile);
FXUtils.showFileInExplorer(dumpFile);
}
} catch (Throwable e) {
LOG.warning("Failed to create minecraft jstack dump", e);
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.ERROR, i18n("logwindow.export_dump.dependency_ok.button"));
alert.setTitle(i18n("message.error"));
alert.showAndWait();
});
}
Platform.runLater(() -> button.setText(i18n("logwindow.export_dump.dependency_ok.button")));
try {
if (gameProcess.isRunning()) {
GameDumpGenerator.writeDumpTo(gameProcess.getPID(), dumpFile);
FXUtils.showFileInExplorer(dumpFile);
}
}
});
}
} catch (Throwable e) {
LOG.warning("Failed to create minecraft jstack dump", e);
private void onExportGameCrashInfo() {
if (exportGameCrashInfoCallback == null) return;
exportGameCrashInfoCallback.accept(logs.stream().map(x -> x.log).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)));
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.ERROR, i18n("logwindow.export_dump"));
alert.setTitle(i18n("message.error"));
alert.showAndWait();
});
}
Platform.runLater(() -> pane.setLoading(false));
});
}
@Override
@@ -263,7 +247,7 @@ public final class LogWindow extends Stage {
}
}
private static class LogWindowSkin extends SkinBase<LogWindowImpl> {
private static final class LogWindowSkin extends SkinBase<LogWindowImpl> {
private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
private static final PseudoClass FATAL = PseudoClass.getPseudoClass("fatal");
private static final PseudoClass ERROR = PseudoClass.getPseudoClass("error");
@@ -275,17 +259,7 @@ public final class LogWindow extends Stage {
private final Set<ListCell<Log>> selected = new HashSet<>();
private static ToggleButton createToggleButton(String backgroundColor, StringProperty buttonText, BooleanProperty showLevel) {
ToggleButton button = new ToggleButton();
button.setStyle("-fx-background-color: " + backgroundColor + ";");
button.getStyleClass().add("log-toggle");
button.textProperty().bind(buttonText);
button.setSelected(true);
showLevel.bind(button.selectedProperty());
return button;
}
protected LogWindowSkin(LogWindowImpl control) {
LogWindowSkin(LogWindowImpl control) {
super(control);
VBox vbox = new VBox(3);
@@ -310,13 +284,16 @@ public final class LogWindow extends Stage {
{
HBox hBox = new HBox(3);
hBox.getChildren().setAll(
createToggleButton("#F7A699", control.buttonText.get(0), control.showLevel.get(0)),
createToggleButton("#FFCCBB", control.buttonText.get(1), control.showLevel.get(1)),
createToggleButton("#FFEECC", control.buttonText.get(2), control.showLevel.get(2)),
createToggleButton("#FBFBFB", control.buttonText.get(3), control.showLevel.get(3)),
createToggleButton("#EEE9E0", control.buttonText.get(4), control.showLevel.get(4))
);
for (int i = 0; i < LEVELS.length; i++) {
ToggleButton button = new ToggleButton();
button.setStyle("-fx-background-color: " + FXUtils.toWeb(LEVELS[i].getColor()) + ";");
button.getStyleClass().add("log-toggle");
button.textProperty().bind(control.buttonText[i]);
button.setSelected(true);
control.showLevel[i].bind(button.selectedProperty());
hBox.getChildren().add(button);
}
borderPane.setRight(hBox);
}
@@ -346,12 +323,15 @@ public final class LogWindow extends Stage {
setGraphic(null);
setOnMouseClicked(event -> {
if (event.getButton() != MouseButton.PRIMARY)
return;
if (!event.isControlDown()) {
for (ListCell<Log> logListCell: selected) {
for (ListCell<Log> logListCell : selected) {
if (logListCell != this) {
logListCell.pseudoClassStateChanged(SELECTED, false);
if (logListCell.getItem() != null) {
logListCell.getItem().selected = false;
logListCell.getItem().setSelected(false);
}
}
}
@@ -362,8 +342,10 @@ public final class LogWindow extends Stage {
selected.add(this);
pseudoClassStateChanged(SELECTED, true);
if (getItem() != null) {
getItem().selected = true;
getItem().setSelected(true);
}
event.consume();
});
}
@@ -377,18 +359,18 @@ public final class LogWindow extends Stage {
lastCell.value = this;
pseudoClassStateChanged(EMPTY, empty);
pseudoClassStateChanged(FATAL, !empty && item.level == Log4jLevel.FATAL);
pseudoClassStateChanged(ERROR, !empty && item.level == Log4jLevel.ERROR);
pseudoClassStateChanged(WARN, !empty && item.level == Log4jLevel.WARN);
pseudoClassStateChanged(INFO, !empty && item.level == Log4jLevel.INFO);
pseudoClassStateChanged(DEBUG, !empty && item.level == Log4jLevel.DEBUG);
pseudoClassStateChanged(TRACE, !empty && item.level == Log4jLevel.TRACE);
pseudoClassStateChanged(SELECTED, !empty && item.selected);
pseudoClassStateChanged(FATAL, !empty && item.getLevel() == Log4jLevel.FATAL);
pseudoClassStateChanged(ERROR, !empty && item.getLevel() == Log4jLevel.ERROR);
pseudoClassStateChanged(WARN, !empty && item.getLevel() == Log4jLevel.WARN);
pseudoClassStateChanged(INFO, !empty && item.getLevel() == Log4jLevel.INFO);
pseudoClassStateChanged(DEBUG, !empty && item.getLevel() == Log4jLevel.DEBUG);
pseudoClassStateChanged(TRACE, !empty && item.getLevel() == Log4jLevel.TRACE);
pseudoClassStateChanged(SELECTED, !empty && item.isSelected());
if (empty) {
setText(null);
} else {
setText(item.log);
setText(item.getLog());
}
}
});
@@ -398,10 +380,9 @@ public final class LogWindow extends Stage {
StringBuilder stringBuilder = new StringBuilder();
for (Log item : listView.getItems()) {
if (item != null && item.selected) {
if (item.log != null) {
stringBuilder.append(item.log);
}
if (item != null && item.isSelected()) {
if (item.getLog() != null)
stringBuilder.append(item.getLog());
stringBuilder.append('\n');
}
}
@@ -417,11 +398,6 @@ public final class LogWindow extends Stage {
{
BorderPane bottom = new BorderPane();
JFXButton exportGameCrashInfoButton = new JFXButton(i18n("logwindow.export_game_crash_logs"));
exportGameCrashInfoButton.setOnMouseClicked(e -> getSkinnable().onExportGameCrashInfo());
exportGameCrashInfoButton.visibleProperty().bind(getSkinnable().showCrashReport);
bottom.setLeft(exportGameCrashInfoButton);
HBox hBox = new HBox(3);
bottom.setRight(hBox);
hBox.setAlignment(Pos.CENTER_RIGHT);
@@ -432,24 +408,24 @@ public final class LogWindow extends Stage {
control.autoScroll.bind(autoScrollCheckBox.selectedProperty());
JFXButton exportLogsButton = new JFXButton(i18n("button.export"));
exportLogsButton.setOnMouseClicked(e -> getSkinnable().onExportLogs());
exportLogsButton.setOnAction(e -> getSkinnable().onExportLogs());
JFXButton terminateButton = new JFXButton(i18n("logwindow.terminate_game"));
terminateButton.setOnMouseClicked(e -> getSkinnable().onTerminateGame());
terminateButton.setOnAction(e -> getSkinnable().onTerminateGame());
JFXButton exportDumpButton = new JFXButton();
SpinnerPane exportDumpPane = new SpinnerPane();
JFXButton exportDumpButton = new JFXButton(i18n("logwindow.export_dump"));
if (SystemUtils.supportJVMAttachment()) {
exportDumpButton.setText(i18n("logwindow.export_dump.dependency_ok.button"));
exportDumpButton.setOnAction(e -> getSkinnable().onExportDump(exportDumpButton));
exportDumpButton.setOnAction(e -> getSkinnable().onExportDump(exportDumpPane));
} else {
exportDumpButton.setText(i18n("logwindow.export_dump.no_dependency.button"));
exportDumpButton.setTooltip(new Tooltip(i18n("logwindow.export_dump.no_dependency.tooltip")));
exportDumpButton.setTooltip(new Tooltip(i18n("logwindow.export_dump.no_dependency")));
exportDumpButton.setDisable(true);
}
exportDumpPane.setContent(exportDumpButton);
JFXButton clearButton = new JFXButton(i18n("button.clear"));
clearButton.setOnMouseClicked(e -> getSkinnable().onClear());
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpButton, clearButton);
clearButton.setOnAction(e -> getSkinnable().onClear());
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton);
vbox.getChildren().add(bottom);
}
@@ -32,7 +32,7 @@ public enum SVG {
CANCEL("M12 2C17.5 2 22 6.5 22 12S17.5 22 12 22 2 17.5 2 12 6.5 2 12 2M12 4C10.1 4 8.4 4.6 7.1 5.7L18.3 16.9C19.3 15.5 20 13.8 20 12C20 7.6 16.4 4 12 4M16.9 18.3L5.7 7.1C4.6 8.4 4 10.1 4 12C4 16.4 7.6 20 12 20C13.9 20 15.6 19.4 16.9 18.3Z"),
CHAT("M20,2A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H6L2,22V4C2,2.89 2.9,2 4,2H20M4,4V17.17L5.17,16H20V4H4M6,7H18V9H6V7M6,11H15V13H6V11Z"),
CLOSE("M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"),
COPY("M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"),
COPY("M15 16H7V6h8m.4-2H6.6A1.6 1.6 90 005 5.6V16.4a1.6 1.6 90 001.6 1.6h8.8A1.6 1.6 90 0017 16.4V5.6A1.6 1.6 90 0015.4 4M13 1H3.6A1.6 1.6 90 002 2.6V13h2V3H13V1Z"),
DOTS_VERTICAL("M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z"),
DOTS_HORIZONTAL("M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z"),
DELETE("M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"),
@@ -59,6 +59,7 @@ public enum SVG {
ALERT_OUTLINE("M12,2L1,21H23M12,6L19.53,19H4.47M11,10V14H13V10M11,16V18H13V16"),
PLUS("M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z"),
PLUS_CIRCLE_OUTLINE("M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M13,7H11V11H7V13H11V17H13V13H17V11H13V7Z"),
MINUS("M19 13H5V11H19V13Z"),
IMPORT_ICON("M14,12L10,8V11H2V13H10V16M20,18V6C20,4.89 19.1,4 18,4H6A2,2 0 0,0 4,6V9H6V6H18V18H6V15H4V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18Z"),
EXPORT("M23,12L19,8V11H10V13H19V16M1,18V6C1,4.89 1.9,4 3,4H15A2,2 0 0,1 17,6V9H15V6H3V18H15V15H17V18A2,2 0 0,1 15,20H3A2,2 0 0,1 1,18Z"),
OPEN_IN_NEW("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"),
@@ -110,7 +111,8 @@ public enum SVG {
LAN("M10,2C8.89,2 8,2.89 8,4V7C8,8.11 8.89,9 10,9H11V11H2V13H6V15H5C3.89,15 3,15.89 3,17V20C3,21.11 3.89,22 5,22H9C10.11,22 11,21.11 11,20V17C11,15.89 10.11,15 9,15H8V13H16V15H15C13.89,15 13,15.89 13,17V20C13,21.11 13.89,22 15,22H19C20.11,22 21,21.11 21,20V17C21,15.89 20.11,15 19,15H18V13H22V11H13V9H14C15.11,9 16,8.11 16,7V4C16,2.89 15.11,2 14,2H10M10,4H14V7H10V4M5,17H9V20H5V17M15,17H19V20H15V17Z"),
THUMB_UP_OUTLINE("M5,9V21H1V9H5M9,21A2,2 0 0,1 7,19V9C7,8.45 7.22,7.95 7.59,7.59L14.17,1L15.23,2.06C15.5,2.33 15.67,2.7 15.67,3.11L15.64,3.43L14.69,8H21C22.11,8 23,8.9 23,10V12C23,12.26 22.95,12.5 22.86,12.73L19.84,19.78C19.54,20.5 18.83,21 18,21H9M9,19H18.03L21,12V10H12.21L13.34,4.68L9,9.03V19Z"),
THUMB_DOWN_OUTLINE("M19,15V3H23V15H19M15,3A2,2 0 0,1 17,5V15C17,15.55 16.78,16.05 16.41,16.41L9.83,23L8.77,21.94C8.5,21.67 8.33,21.3 8.33,20.88L8.36,20.57L9.31,16H3C1.89,16 1,15.1 1,14V12C1,11.74 1.05,11.5 1.14,11.27L4.16,4.22C4.46,3.5 5.17,3 6,3H15M15,5H5.97L3,12V14H11.78L10.65,19.32L15,14.97V5Z"),
SELECT_ALL("M9,9H15V15H9M7,17H17V7H7M15,5H17V3H15M15,21H17V19H15M19,17H21V15H19M19,9H21V7H19M19,21A2,2 0 0,0 21,19H19M19,13H21V11H19M11,21H13V19H11M9,3H7V5H9M3,17H5V15H3M5,21V19H3A2,2 0 0,0 5,21M19,3V5H21A2,2 0 0,0 19,3M13,3H11V5H13M3,9H5V7H3M7,21H9V19H7M3,13H5V11H3M3,5H5V3A2,2 0 0,0 3,5Z");
SELECT_ALL("M9,9H15V15H9M7,17H17V7H7M15,5H17V3H15M15,21H17V19H15M19,17H21V15H19M19,9H21V7H19M19,21A2,2 0 0,0 21,19H19M19,13H21V11H19M11,21H13V19H11M9,3H7V5H9M3,17H5V15H3M5,21V19H3A2,2 0 0,0 5,21M19,3V5H21A2,2 0 0,0 19,3M13,3H11V5H13M3,9H5V7H3M7,21H9V19H7M3,13H5V11H3M3,5H5V3A2,2 0 0,0 3,5Z"),
JAVA("m 12.400746,23.498132 c 0,0 -1.047682,0.609185 0.745753,0.816159 2.173248,0.247515 3.284943,0.212301 5.680103,-0.241112 0,0 0.629462,0.394742 1.509642,0.737207 -5.369636,2.301245 -12.1518319,-0.133351 -7.934431,-1.312254 z m -0.656135,-3.003246 c 0,0 -1.175708,0.870569 0.619861,1.056206 2.321545,0.238977 4.155521,0.25925 7.32844,-0.35207 0,0 0.43849,0.444887 1.128765,0.688135 -6.492002,1.897965 -13.7233551,0.149351 -9.077066,-1.392271 z m 5.531806,-5.094318 c 1.322937,1.523496 -0.347807,2.894427 -0.347807,2.894427 0,0 3.359626,-1.733668 1.816908,-3.905821 -1.441364,-2.024926 -2.545591,-3.030987 3.43644,-6.5004569 0,0 -9.389665,2.3449859 -4.905541,7.5129179 z m 7.101193,10.318794 c 0,0 0.775626,0.639057 -0.854578,1.133019 C 20.42373,27.79123 10.623314,28.075018 7.9006204,26.88973 6.9222859,26.464048 8.75733,25.873001 9.3345153,25.749244 9.9362392,25.619093 10.280844,25.642568 10.280844,25.642568 9.1926195,24.875486 3.2457961,27.147927 7.2604887,27.798718 18.208875,29.573994 27.21766,26.999632 24.37761,25.718316 Z M 12.904316,17.383886 c 0,0 -4.9855587,1.184229 -1.765696,1.614178 1.359213,0.182426 4.069103,0.140825 6.594422,-0.07042 2.063359,-0.173899 4.135252,-0.544104 4.135252,-0.544104 0,0 -0.727616,0.311527 -1.253592,0.671064 -5.062375,1.331456 -14.8414526,0.711604 -12.02594,-0.649727 2.38129,-1.151156 4.316621,-1.019929 4.316621,-1.019929 z m 8.943706,4.998298 c 5.146658,-2.673583 2.766435,-5.24368 1.106361,-4.898013 -0.407551,0.08428 -0.58892,0.1579 -0.58892,0.1579 0,0 0.151501,-0.236845 0.439557,-0.339265 3.284942,-1.155424 5.812393,3.406524 -1.060485,5.213807 0,0 0.08003,-0.07147 0.103478,-0.134425 z M 18.744451,2.2855 c 0,0 2.849653,2.8506848 -2.703489,7.2344656 -4.453183,3.5164124 -1.015676,5.5221324 -0.0021,7.8127104 C 13.439893,14.987688 11.532301,12.92329 12.811497,11.001852 14.690284,8.1810394 19.893487,6.8143757 18.743384,2.2855 Z M 13.41002,29.628384 c 4.939684,0.315794 12.525242,-0.174974 12.70448,-2.512485 0,0 -0.345672,0.886571 -4.081906,1.589641 -4.216334,0.793754 -9.416337,0.700936 -12.4996379,0.192025 0,0 0.6315969,0.522768 3.8781309,0.730807 z");
private final String path;
@@ -91,7 +91,7 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
ret.textFillProperty().bind(Theme.foregroundFillBinding());
ret.setGraphic(wrap(svg.createIcon(Theme.foregroundFillBinding(), -1, -1)));
ret.setText(text);
ret.setOnMouseClicked(e -> onClick.run());
ret.setOnAction(e -> onClick.run());
return ret;
}
@@ -100,7 +100,7 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
ret.getStyleClass().add("jfx-tool-bar-button");
ret.setGraphic(wrap(svg.createIcon(Theme.blackFill(), -1, -1)));
ret.setText(text);
ret.setOnMouseClicked(e -> onClick.run());
ret.setOnAction(e -> onClick.run());
return ret;
}
@@ -110,7 +110,7 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
ret.textFillProperty().bind(Theme.foregroundFillBinding());
ret.setGraphic(wrap(svg.createIcon(Theme.foregroundFillBinding(), -1, -1)));
FXUtils.installFastTooltip(ret, tooltip);
ret.setOnMouseClicked(e -> onClick.run());
ret.setOnAction(e -> onClick.run());
return ret;
}
@@ -0,0 +1,123 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.util.Duration;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public class TooltipInstaller {
public static final TooltipInstaller INSTALLER;
static {
TooltipInstaller installer = null;
try {
installer = new NewInstaller();
} catch (Throwable e) {
try {
installer = new OldInstaller();
} catch (Throwable e2) {
e2.addSuppressed(e);
LOG.warning("Failed to initialize TooltipInstaller", e2);
}
}
INSTALLER = installer != null ? installer : new TooltipInstaller();
}
TooltipInstaller() {
}
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
Tooltip.install(node, tooltip);
}
// For Java 8
private static final class OldInstaller extends TooltipInstaller {
private static final Constructor<?> createTooltipBehavior;
private static final Method installTooltipBehavior;
static {
try {
Class<?> behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior");
createTooltipBehavior = behaviorClass.getDeclaredConstructor(Duration.class, Duration.class, Duration.class, boolean.class);
createTooltipBehavior.setAccessible(true);
installTooltipBehavior = behaviorClass.getDeclaredMethod("install", Node.class, Tooltip.class);
installTooltipBehavior.setAccessible(true);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
@Override
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
try {
Object behavior = createTooltipBehavior.newInstance(showDelay, showDuration, hideDelay, false);
installTooltipBehavior.invoke(behavior, node, tooltip);
} catch (ReflectiveOperationException e) {
LOG.warning("Failed to set tooltip show delay", e);
Tooltip.install(node, tooltip);
}
}
}
// For Java 9+
private static final class NewInstaller extends TooltipInstaller {
private static final MethodHandle setTooltipShowDelay;
private static final MethodHandle setTooltipShowDuration;
private static final MethodHandle setTooltipHideDelay;
static {
try {
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
MethodType methodType = MethodType.methodType(void.class, Duration.class);
setTooltipShowDelay = lookup.findVirtual(Tooltip.class, "setShowDelay", methodType);
setTooltipShowDuration = lookup.findVirtual(Tooltip.class, "setShowDuration", methodType);
setTooltipHideDelay = lookup.findVirtual(Tooltip.class, "setHideDelay", methodType);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
@Override
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
try {
setTooltipShowDelay.invokeExact(tooltip, showDelay);
setTooltipShowDuration.invokeExact(tooltip, showDuration);
setTooltipHideDelay.invokeExact(tooltip, hideDelay);
} catch (Throwable e) {
LOG.warning("Failed to set tooltip show delay", e);
}
Tooltip.install(node, tooltip);
}
}
}
@@ -19,58 +19,73 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import javafx.concurrent.Worker;
import javafx.scene.control.Label;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import org.jackhuang.hmcl.Metadata;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.ScrollPane;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Node;
import java.io.IOException;
import java.net.URL;
import static org.jackhuang.hmcl.Metadata.CHANGELOG_URL;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class UpgradeDialog extends JFXDialogLayout {
public final class UpgradeDialog extends JFXDialogLayout {
public UpgradeDialog(RemoteVersion remoteVersion, Runnable updateRunnable) {
{
setHeading(new Label(i18n("update.changelog")));
}
setHeading(new Label(i18n("update.changelog")));
setBody(new ProgressIndicator());
{
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html#nowchange";
try {
WebView webView = new WebView();
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
WebEngine engine = webView.getEngine();
engine.load(url);
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == Worker.State.FAILED) {
LOG.warning("Failed to load update log, trying to open it in browser");
FXUtils.openLink(url);
setBody();
}
});
setBody(webView);
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
LOG.warning("WebView is missing or initialization failed", e);
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
Task.supplyAsync(Schedulers.io(), () -> {
Document document = Jsoup.parse(new URL(url), 30 * 1000);
Node node = document.selectFirst("#nowchange");
if (node == null)
throw new IOException("Cannot find #nowchange in document");
HTMLRenderer renderer = new HTMLRenderer(uri -> {
LOG.info("Open link: " + uri);
FXUtils.openLink(uri.toString());
});
do {
renderer.appendNode(node);
node = node.nextSibling();
} while (node != null);
return renderer.render();
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
ScrollPane scrollPane = new ScrollPane(result);
scrollPane.setFitToWidth(true);
setBody(scrollPane);
} else {
LOG.warning("Failed to load update log, trying to open it in browser");
FXUtils.openLink(url);
setBody();
}
}
}).start();
{
JFXButton updateButton = new JFXButton(i18n("update.accept"));
updateButton.getStyleClass().add("dialog-accept");
updateButton.setOnMouseClicked(e -> updateRunnable.run());
JFXHyperlink openInBrowser = new JFXHyperlink(i18n("web.view_in_browser"));
openInBrowser.setExternalLink(url);
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.getStyleClass().add("dialog-cancel");
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
JFXButton updateButton = new JFXButton(i18n("update.accept"));
updateButton.getStyleClass().add("dialog-accept");
updateButton.setOnAction(e -> updateRunnable.run());
setActions(updateButton, cancelButton);
onEscPressed(this, cancelButton::fire);
}
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.getStyleClass().add("dialog-cancel");
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
setActions(openInBrowser, updateButton, cancelButton);
onEscPressed(this, cancelButton::fire);
}
}
@@ -0,0 +1,75 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class WebPage extends SpinnerPane implements DecoratorPage {
private final ObjectProperty<DecoratorPage.State> stateProperty;
public WebPage(String title, String content) {
this.stateProperty = new SimpleObjectProperty<>(DecoratorPage.State.fromTitle(title));
this.setBackground(new Background(new BackgroundFill(Color.WHITE, null, null)));
Task.supplyAsync(() -> {
Document document = Jsoup.parseBodyFragment(content);
HTMLRenderer renderer = new HTMLRenderer(uri -> {
Controllers.confirm(i18n("web.open_in_browser", uri), i18n("message.confirm"), () -> {
FXUtils.openLink(uri.toString());
}, null);
});
renderer.appendNode(document);
return renderer.render();
}).whenComplete(Schedulers.javafx(), ((result, exception) -> {
if (exception == null) {
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToWidth(true);
scrollPane.setContent(result);
setContent(scrollPane);
setFailedReason(null);
} else {
LOG.warning("Failed to load content", exception);
setFailedReason(i18n("web.failed"));
}
})).start();
}
@Override
public ReadOnlyObjectProperty<DecoratorPage.State> stateProperty() {
return stateProperty;
}
}
@@ -1,72 +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.ui;
import com.jfoenix.controls.JFXProgressBar;
import javafx.beans.binding.Bindings;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.Theme;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public class WebStage extends Stage {
protected final StackPane pane = new StackPane();
protected final JFXProgressBar progressBar = new JFXProgressBar();
protected final WebView webView = new WebView();
protected final WebEngine webEngine = webView.getEngine();
public WebStage() {
this(800, 480);
}
public WebStage(int width, int height) {
setScene(new Scene(pane, width, height));
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
FXUtils.setIcon(this);
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
webView.setContextMenuEnabled(false);
progressBar.progressProperty().bind(webView.getEngine().getLoadWorker().progressProperty());
progressBar.visibleProperty().bind(Bindings.createBooleanBinding(() -> {
switch (webView.getEngine().getLoadWorker().getState()) {
case SUCCEEDED:
case FAILED:
case CANCELLED:
return false;
default:
return true;
}
}, webEngine.getLoadWorker().stateProperty()));
BorderPane borderPane = new BorderPane();
borderPane.setPickOnBounds(false);
borderPane.setTop(progressBar);
progressBar.prefWidthProperty().bind(borderPane.widthProperty());
pane.getChildren().setAll(webView, borderPane);
}
public WebView getWebView() {
return webView;
}
}
@@ -32,17 +32,14 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CredentialExpiredException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
@@ -128,7 +125,7 @@ public class AccountListItem extends RadioButton {
.orElse(emptySet());
return uploadableTextures.contains(TextureType.SKIN);
}, profile);
} else if (account instanceof OfflineAccount || account instanceof MicrosoftAccount) {
} else if (account instanceof OfflineAccount || account.canUploadSkin()) {
return createBooleanBinding(() -> true);
} else {
return createBooleanBinding(() -> false);
@@ -144,11 +141,7 @@ public class AccountListItem extends RadioButton {
Controllers.dialog(new OfflineAccountSkinPane((OfflineAccount) account));
return null;
}
if (account instanceof MicrosoftAccount) {
FXUtils.openLink("https://www.minecraft.net/msaprofile/mygames/editskin");
return null;
}
if (!(account instanceof YggdrasilAccount)) {
if (!account.canUploadSkin()) {
return null;
}
@@ -174,7 +167,7 @@ public class AccountListItem extends RadioButton {
NormalizedSkin skin = new NormalizedSkin(skinImg);
String model = skin.isSlim() ? "slim" : "";
LOG.info("Uploading skin [" + selectedFile + "], model [" + model + "]");
((YggdrasilAccount) account).uploadSkin(model, selectedFile.toPath());
account.uploadSkin(skin.isSlim(), selectedFile.toPath());
})
.thenComposeAsync(refreshAsync())
.whenComplete(Schedulers.javafx(), e -> {
@@ -42,10 +42,9 @@ import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AccountListItemSkin extends SkinBase<AccountListItem> {
public final class AccountListItemSkin extends SkinBase<AccountListItem> {
public AccountListItemSkin(AccountListItem skinnable) {
super(skinnable);
@@ -94,7 +93,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
JFXButton btnMove = new JFXButton();
SpinnerPane spinnerMove = new SpinnerPane();
spinnerMove.getStyleClass().add("small-spinner-pane");
btnMove.setOnMouseClicked(e -> {
btnMove.setOnAction(e -> {
Account account = skinnable.getAccount();
Accounts.getAccounts().remove(account);
if (account.isPortable()) {
@@ -118,10 +117,10 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
btnMove.getStyleClass().add("toggle-icon4");
if (skinnable.getAccount().isPortable()) {
btnMove.setGraphic(SVG.EARTH.createIcon(Theme.blackFill(), -1, -1));
runInFX(() -> FXUtils.installFastTooltip(btnMove, i18n("account.move_to_global")));
FXUtils.installFastTooltip(btnMove, i18n("account.move_to_global"));
} else {
btnMove.setGraphic(SVG.EXPORT.createIcon(Theme.blackFill(), -1, -1));
runInFX(() -> FXUtils.installFastTooltip(btnMove, i18n("account.move_to_portable")));
FXUtils.installFastTooltip(btnMove, i18n("account.move_to_portable"));
}
spinnerMove.setContent(btnMove);
right.getChildren().add(spinnerMove);
@@ -129,7 +128,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
JFXButton btnRefresh = new JFXButton();
SpinnerPane spinnerRefresh = new SpinnerPane();
spinnerRefresh.getStyleClass().setAll("small-spinner-pane");
btnRefresh.setOnMouseClicked(e -> {
btnRefresh.setOnAction(e -> {
spinnerRefresh.showSpinner();
skinnable.refreshAsync()
.whenComplete(Schedulers.javafx(), ex -> {
@@ -143,13 +142,13 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
});
btnRefresh.getStyleClass().add("toggle-icon4");
btnRefresh.setGraphic(SVG.REFRESH.createIcon(Theme.blackFill(), -1, -1));
runInFX(() -> FXUtils.installFastTooltip(btnRefresh, i18n("button.refresh")));
FXUtils.installFastTooltip(btnRefresh, i18n("button.refresh"));
spinnerRefresh.setContent(btnRefresh);
right.getChildren().add(spinnerRefresh);
JFXButton btnUpload = new JFXButton();
SpinnerPane spinnerUpload = new SpinnerPane();
btnUpload.setOnMouseClicked(e -> {
btnUpload.setOnAction(e -> {
Task<?> uploadTask = skinnable.uploadSkin();
if (uploadTask != null) {
spinnerUpload.showSpinner();
@@ -160,7 +159,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
});
btnUpload.getStyleClass().add("toggle-icon4");
btnUpload.setGraphic(SVG.HANGER.createIcon(Theme.blackFill(), -1, -1));
runInFX(() -> FXUtils.installFastTooltip(btnUpload, i18n("account.skin.upload")));
FXUtils.installFastTooltip(btnUpload, i18n("account.skin.upload"));
spinnerUpload.managedProperty().bind(spinnerUpload.visibleProperty());
spinnerUpload.visibleProperty().bind(skinnable.canUploadSkin());
spinnerUpload.setContent(btnUpload);
@@ -170,18 +169,19 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
JFXButton btnCopyUUID = new JFXButton();
SpinnerPane spinnerCopyUUID = new SpinnerPane();
spinnerCopyUUID.getStyleClass().add("small-spinner-pane");
btnCopyUUID.setOnMouseClicked(e -> FXUtils.copyText(skinnable.getAccount().getUUID().toString()));
btnUpload.getStyleClass().add("toggle-icon4");
btnCopyUUID.setOnAction(e -> FXUtils.copyText(skinnable.getAccount().getUUID().toString()));
btnCopyUUID.setGraphic(SVG.COPY.createIcon(Theme.blackFill(), -1, -1));
runInFX(() -> FXUtils.installFastTooltip(btnCopyUUID, i18n("account.copy_uuid")));
FXUtils.installFastTooltip(btnCopyUUID, i18n("account.copy_uuid"));
spinnerCopyUUID.setContent(btnCopyUUID);
right.getChildren().add(spinnerCopyUUID);
JFXButton btnRemove = new JFXButton();
btnRemove.setOnMouseClicked(e -> skinnable.remove());
btnRemove.setOnAction(e -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), skinnable::remove, null));
btnRemove.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnRemove, Pos.CENTER);
btnRemove.setGraphic(SVG.DELETE.createIcon(Theme.blackFill(), -1, -1));
runInFX(() -> FXUtils.installFastTooltip(btnRemove, i18n("button.delete")));
btnRemove.setGraphic(SVG.DELETE_OUTLINE.createIcon(Theme.blackFill(), -1, -1));
FXUtils.installFastTooltip(btnRemove, i18n("button.delete"));
right.getChildren().add(btnRemove);
root.setRight(right);
@@ -44,6 +44,7 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import java.net.URI;
import java.util.Locale;
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -95,7 +96,7 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
VBox boxMethods = new VBox();
{
boxMethods.getStyleClass().add("advanced-list-box-content");
boxMethods.getChildren().add(new ClassTitle(i18n("account.create")));
boxMethods.getChildren().add(new ClassTitle(i18n("account.create").toUpperCase(Locale.ROOT)));
FXUtils.setLimitWidth(boxMethods, 200);
AdvancedListItem offlineItem = new AdvancedListItem();
@@ -123,7 +124,9 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
JFXButton btnRemove = new JFXButton();
btnRemove.setOnAction(e -> {
skinnable.authServersProperty().remove(server);
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
skinnable.authServersProperty().remove(server);
}, null);
e.consume();
});
btnRemove.getStyleClass().add("toggle-icon4");
@@ -147,7 +147,7 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
confirmServerPane.setActions(prevButton, cancelButton, finishButton);
}
this.setContent(addServerPane, ContainerAnimations.NONE.getAnimationProducer());
this.setContent(addServerPane, ContainerAnimations.NONE);
lblCreationWarning.maxWidthProperty().bind(((FlowPane) lblCreationWarning.getParent()).widthProperty());
btnAddNext.disableProperty().bind(txtServerUrl.textProperty().isEmpty());
@@ -198,7 +198,7 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
lblServerWarning.setVisible("http".equals(NetworkUtils.toURL(serverBeingAdded.getUrl()).getProtocol()));
this.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT.getAnimationProducer());
this.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT);
} else {
LOG.warning("Failed to resolve auth server: " + url, exception);
lblCreationWarning.setText(resolveFetchExceptionMessage(exception));
@@ -208,7 +208,7 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
}
private void onAddPrev() {
this.setContent(addServerPane, ContainerAnimations.SWIPE_RIGHT.getAnimationProducer());
this.setContent(addServerPane, ContainerAnimations.SWIPE_RIGHT);
}
private void onAddFinish() {
@@ -65,6 +65,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
@@ -296,7 +297,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
}
});
hintPane.setOnMouseClicked(e -> {
FXUtils.onClicked(hintPane, () -> {
if (deviceCode.get() != null) {
FXUtils.copyText(deviceCode.get().getUserCode());
}
@@ -316,7 +317,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
JFXHyperlink deauthorizeLink = new JFXHyperlink(i18n("account.methods.microsoft.deauthorize"));
deauthorizeLink.setExternalLink("https://account.live.com/consent/Edit?client_id=000000004C794E0A");
JFXHyperlink forgotpasswordLink = new JFXHyperlink(i18n("account.methods.forgot_password"));
forgotpasswordLink.setExternalLink("https://www.minecraft.net/password/forgot");
forgotpasswordLink.setExternalLink("https://account.live.com/ResetPassword.aspx");
JFXHyperlink createProfileLink = new JFXHyperlink(i18n("account.methods.microsoft.makegameidsettings"));
createProfileLink.setExternalLink("https://www.minecraft.net/msaprofile/mygames/editprofile");
box.getChildren().setAll(profileLink, birthLink, purchaseLink, deauthorizeLink, forgotpasswordLink, createProfileLink);
@@ -509,7 +510,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
if (factory instanceof OfflineAccountFactory) {
txtUsername.setPromptText(i18n("account.methods.offline.name.special_characters"));
runInFX(() -> FXUtils.installFastTooltip(txtUsername, i18n("account.methods.offline.name.special_characters")));
FXUtils.installFastTooltip(txtUsername, i18n("account.methods.offline.name.special_characters"));
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
@@ -637,7 +638,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
StackPane.setAlignment(cancel, Pos.BOTTOM_RIGHT);
cancel.setOnAction(e -> latch.countDown());
listBox.startCategory(i18n("account.choose"));
listBox.startCategory(i18n("account.choose").toUpperCase(Locale.ROOT));
setCenter(listBox);
@@ -657,7 +658,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
TexturesLoader.bindAvatar(portraitCanvas, service, profile.getId());
IconedItem accountItem = new IconedItem(portraitCanvas, profile.getName());
accountItem.setOnMouseClicked(e -> {
FXUtils.onClicked(accountItem, () -> {
selectedProfile = profile;
latch.countDown();
});
@@ -58,7 +58,7 @@ public class OAuthAccountLoginDialog extends DialogPane {
);
}
});
hintPane.setOnMouseClicked(e -> {
FXUtils.onClicked(hintPane, () -> {
if (deviceCode.get() != null) {
FXUtils.copyText(deviceCode.get().getUserCode());
}
@@ -28,9 +28,9 @@ import javafx.scene.control.Label;
import javafx.scene.input.DragEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.*;
import moe.mickey.minecraft.skin.fx.SkinCanvas;
import moe.mickey.minecraft.skin.fx.animation.SkinAniRunning;
import moe.mickey.minecraft.skin.fx.animation.SkinAniWavingArms;
import org.jackhuang.hmcl.ui.skin.SkinCanvas;
import org.jackhuang.hmcl.ui.skin.animation.SkinAniRunning;
import org.jackhuang.hmcl.ui.skin.animation.SkinAniWavingArms;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.Skin;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -27,165 +27,225 @@ import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
public enum ContainerAnimations {
NONE(c -> {
c.getPreviousNode().setTranslateX(0);
c.getPreviousNode().setTranslateY(0);
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(1);
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
}, c -> Collections.emptyList()),
public enum ContainerAnimations implements AnimationProducer {
NONE {
@Override
public void init(AnimationHandler c) {
c.getPreviousNode().setTranslateX(0);
c.getPreviousNode().setTranslateY(0);
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(1);
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
}
@Override
public List<KeyFrame> animate(AnimationHandler c) {
return Collections.emptyList();
}
},
/**
* A fade between the old and new view
*/
FADE(c -> {
c.getPreviousNode().setTranslateX(0);
c.getPreviousNode().setTranslateY(0);
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(1);
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(0);
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
FADE {
@Override
public void init(AnimationHandler c) {
c.getPreviousNode().setTranslateX(0);
c.getPreviousNode().setTranslateY(0);
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(1);
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(0);
}
@Override
public List<KeyFrame> animate(AnimationHandler c) {
return Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 0, Interpolator.EASE_BOTH)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH)))),
new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH)));
}
},
/**
* A fade between the old and new view
*/
FADE_IN(c -> {
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(0);
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
FADE_IN {
@Override
public void init(AnimationHandler c) {
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(0);
}
@Override
public List<KeyFrame> animate(AnimationHandler c) {
return Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getCurrentNode().opacityProperty(), 0, FXUtils.SINE)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getCurrentNode().opacityProperty(), 1, FXUtils.SINE)))),
new KeyValue(c.getCurrentNode().opacityProperty(), 1, FXUtils.SINE)));
}
},
/**
* A fade between the old and new view
*/
FADE_OUT(c -> {
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
FADE_OUT {
@Override
public void init(AnimationHandler c) {
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
}
@Override
public List<KeyFrame> animate(AnimationHandler c) {
return Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getCurrentNode().opacityProperty(), 1, FXUtils.SINE)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getCurrentNode().opacityProperty(), 0, FXUtils.SINE)))),
new KeyValue(c.getCurrentNode().opacityProperty(), 0, FXUtils.SINE)));
}
},
/**
* A zoom effect
*/
ZOOM_IN(c -> {
c.getPreviousNode().setTranslateX(0);
c.getPreviousNode().setTranslateY(0);
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(1);
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
ZOOM_IN {
@Override
public void init(AnimationHandler c) {
c.getPreviousNode().setTranslateX(0);
c.getPreviousNode().setTranslateY(0);
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(1);
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
}
@Override
public List<KeyFrame> animate(AnimationHandler c) {
return Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getPreviousNode().scaleXProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().scaleYProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getPreviousNode().scaleXProperty(), 4, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().scaleYProperty(), 4, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH)))),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH)));
}
},
/**
* A zoom effect
*/
ZOOM_OUT(c -> {
c.getPreviousNode().setTranslateX(0);
c.getPreviousNode().setTranslateY(0);
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(1);
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
}, c ->
(Arrays.asList(new KeyFrame(Duration.ZERO,
ZOOM_OUT {
@Override
public void init(AnimationHandler c) {
c.getPreviousNode().setTranslateX(0);
c.getPreviousNode().setTranslateY(0);
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(1);
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
}
@Override
public List<KeyFrame> animate(AnimationHandler c) {
return Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getPreviousNode().scaleXProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().scaleYProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getPreviousNode().scaleXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().scaleYProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH))))),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH)));
}
},
/**
* A swipe effect
*/
SWIPE_LEFT(c -> {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth());
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
SWIPE_LEFT {
@Override
public void init(AnimationHandler c) {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth());
}
@Override
public List<KeyFrame> animate(AnimationHandler c) {
return Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getCurrentNode().translateXProperty(), c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), -c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH)))),
new KeyValue(c.getPreviousNode().translateXProperty(), -c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH)));
}
},
/**
* A swipe effect
*/
SWIPE_RIGHT(c -> {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(-c.getCurrentRoot().getWidth());
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
SWIPE_RIGHT {
@Override
public void init(AnimationHandler c) {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(-c.getCurrentRoot().getWidth());
}
@Override
public List<KeyFrame> animate(AnimationHandler c) {
return Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getCurrentNode().translateXProperty(), -c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH)))),
new KeyValue(c.getPreviousNode().translateXProperty(), c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH)));
}
},
SWIPE_LEFT_FADE_SHORT(c -> {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth());
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
SWIPE_LEFT_FADE_SHORT {
@Override
public void init(AnimationHandler c) {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth());
}
@Override
public List<KeyFrame> animate(AnimationHandler c) {
return Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getCurrentNode().translateXProperty(), 50, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 0, Interpolator.EASE_BOTH),
@@ -194,19 +254,26 @@ public enum ContainerAnimations {
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), -50, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH)))),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH)));
}
},
SWIPE_RIGHT_FADE_SHORT(c -> {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth());
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
SWIPE_RIGHT_FADE_SHORT {
@Override
public void init(AnimationHandler c) {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth());
}
@Override
public List<KeyFrame> animate(AnimationHandler c) {
return Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getCurrentNode().translateXProperty(), -50, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 0, Interpolator.EASE_BOTH),
@@ -215,38 +282,12 @@ public enum ContainerAnimations {
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), 50, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH))));
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH)));
}
};
private final AnimationProducer animationProducer;
private ContainerAnimations opposite;
ContainerAnimations(Consumer<AnimationHandler> init, Function<AnimationHandler, List<KeyFrame>> animationProducer) {
this.animationProducer = new AnimationProducer() {
@Override
public void init(AnimationHandler handler) {
init.accept(handler);
}
@Override
public List<KeyFrame> animate(AnimationHandler handler) {
return animationProducer.apply(handler);
}
@Override
public @Nullable AnimationProducer opposite() {
return opposite != null ? opposite.getAnimationProducer() : null;
}
};
}
public AnimationProducer getAnimationProducer() {
return animationProducer;
}
public ContainerAnimations getOpposite() {
return opposite;
}
static {
NONE.opposite = NONE;
FADE.opposite = FADE;
@@ -257,4 +298,15 @@ public enum ContainerAnimations {
ZOOM_IN.opposite = ZOOM_OUT;
ZOOM_OUT.opposite = ZOOM_IN;
}
@Override
public abstract void init(AnimationHandler handler);
@Override
public abstract List<KeyFrame> animate(AnimationHandler handler);
@Override
public @Nullable ContainerAnimations opposite() {
return opposite;
}
}
@@ -25,7 +25,6 @@ import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.Pair;
@@ -42,7 +41,7 @@ public class AdvancedListItem extends Control {
public AdvancedListItem() {
getStyleClass().add("advanced-list-item");
addEventHandler(MouseEvent.MOUSE_CLICKED, e -> fireEvent(new ActionEvent()));
FXUtils.onClicked(this, () -> fireEvent(new ActionEvent()));
}
public Node getLeftGraphic() {
@@ -17,21 +17,36 @@
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextFlow;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
public class AnnouncementCard extends VBox {
public final class AnnouncementCard extends VBox {
public AnnouncementCard(String title, String content) {
TextFlow tf = FXUtils.segmentToTextFlow(content, Controllers::onHyperlinkAction);
public AnnouncementCard(String title, String content, Runnable onClose) {
TextFlow body = FXUtils.segmentToTextFlow(content, Controllers::onHyperlinkAction);
body.setLineSpacing(4);
Label label = new Label(title);
label.getStyleClass().add("title");
getChildren().setAll(label, tf);
setSpacing(14);
BorderPane titleBar = new BorderPane();
titleBar.getStyleClass().add("title");
titleBar.setLeft(new Label(title));
if (onClose != null) {
Node hideNode = SVG.CLOSE.createIcon(Theme.blackFill(), 20, 20);
hideNode.setOnMouseClicked(e -> onClose.run());
hideNode.setCursor(Cursor.HAND);
titleBar.setRight(hideNode);
}
getChildren().setAll(titleBar, body);
setSpacing(16);
getStyleClass().addAll("card", "announcement");
}
}
@@ -25,9 +25,6 @@ import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
@@ -139,7 +136,7 @@ final class ComponentListCell extends StackPane {
container.getChildren().setAll(content);
groupNode.getChildren().add(container);
EventHandler<Event> onExpand = e -> {
Runnable onExpand = () -> {
if (expandAnimation != null && expandAnimation.getStatus() == Animation.Status.RUNNING) {
expandAnimation.stop();
}
@@ -182,8 +179,8 @@ final class ComponentListCell extends StackPane {
});
};
headerRippler.setOnMouseClicked(onExpand);
expandButton.setOnAction((EventHandler<ActionEvent>) (Object) onExpand);
FXUtils.onClicked(headerRippler, onExpand);
expandButton.setOnAction(e -> onExpand.run());
expandedProperty().addListener((a, b, newValue) -> expandIcon.setRotate(newValue ? 180 : 0));
@@ -60,7 +60,7 @@ public class FileItem extends BorderPane {
JFXButton right = new JFXButton();
right.setGraphic(SVG.PENCIL.createIcon(Theme.blackFill(), 15, 15));
right.getStyleClass().add("toggle-icon4");
right.setOnMouseClicked(e -> onExplore());
right.setOnAction(e -> onExplore());
FXUtils.installFastTooltip(right, i18n("button.edit"));
setRight(right);
@@ -21,6 +21,7 @@ import static javafx.collections.FXCollections.emptyObservableList;
import static javafx.collections.FXCollections.observableList;
import static javafx.collections.FXCollections.singletonObservableList;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import com.jfoenix.controls.JFXComboBox;
@@ -34,6 +35,8 @@ public class FontComboBox extends JFXComboBox<String> {
private boolean loaded = false;
public FontComboBox() {
setMinWidth(260);
styleProperty().bind(Bindings.concat("-fx-font-family: \"", valueProperty(), "\""));
setCellFactory(listView -> new JFXListCell<String>() {
@@ -51,7 +54,7 @@ public class FontComboBox extends JFXComboBox<String> {
itemsProperty().bind(BindingMapping.of(valueProperty())
.map(value -> value == null ? emptyObservableList() : singletonObservableList(value)));
setOnMouseClicked(e -> {
FXUtils.onClicked(this, () -> {
if (loaded)
return;
itemsProperty().unbind();
@@ -30,9 +30,9 @@ public class IconedMenuItem extends IconedItem {
getStyleClass().setAll("iconed-menu-item");
if (popup == null) {
setOnMouseClicked(e -> action.run());
FXUtils.onClicked(this, action);
} else {
setOnMouseClicked(e -> {
FXUtils.onClicked(this, () -> {
action.run();
popup.hide();
});
@@ -17,7 +17,6 @@ import javafx.scene.layout.Priority;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.StringUtils;
public class IconedTwoLineListItem extends HBox {
@@ -29,13 +28,8 @@ public class IconedTwoLineListItem extends HBox {
private final ImageView imageView = new ImageView();
private final TwoLineListItem twoLineListItem = new TwoLineListItem();
private final Lazy<JFXButton> externalLinkButton = new Lazy<>(() -> {
JFXButton button = new JFXButton();
button.getStyleClass().add("toggle-icon4");
button.setGraphic(SVG.OPEN_IN_NEW.createIcon(Theme.blackFill(), -1, -1));
button.setOnAction(e -> FXUtils.openLink(externalLink.get()));
return button;
});
private JFXButton externalLinkButton;
@SuppressWarnings("FieldCanBeLocal")
private final InvalidationListener observer;
@@ -52,7 +46,7 @@ public class IconedTwoLineListItem extends HBox {
getChildren().clear();
if (image.get() != null) getChildren().add(imageView);
getChildren().add(twoLineListItem);
if (StringUtils.isNotBlank(externalLink.get())) getChildren().add(externalLinkButton.get());
if (StringUtils.isNotBlank(externalLink.get())) getChildren().add(getExternalLinkButton());
}, image, externalLink);
}
@@ -111,4 +105,14 @@ public class IconedTwoLineListItem extends HBox {
public ImageView getImageView() {
return imageView;
}
public JFXButton getExternalLinkButton() {
if (externalLinkButton == null) {
externalLinkButton = new JFXButton();
externalLinkButton.getStyleClass().add("toggle-icon4");
externalLinkButton.setGraphic(SVG.OPEN_IN_NEW.createIcon(Theme.blackFill(), -1, -1));
externalLinkButton.setOnAction(e -> FXUtils.openLink(externalLink.get()));
}
return externalLinkButton;
}
}
@@ -23,12 +23,12 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
@@ -44,8 +44,8 @@ public final class ImagePickerItem extends BorderPane {
private final ImageView imageView;
private final StringProperty title = new SimpleStringProperty(this, "title");
private final ObjectProperty<EventHandler<? super MouseEvent>> onSelectButtonClicked = new SimpleObjectProperty<>(this, "onSelectButtonClicked");
private final ObjectProperty<EventHandler<? super MouseEvent>> onDeleteButtonClicked = new SimpleObjectProperty<>(this, "onDeleteButtonClicked");
private final ObjectProperty<EventHandler<ActionEvent>> onSelectButtonClicked = new SimpleObjectProperty<>(this, "onSelectButtonClicked");
private final ObjectProperty<EventHandler<ActionEvent>> onDeleteButtonClicked = new SimpleObjectProperty<>(this, "onDeleteButtonClicked");
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
public ImagePickerItem() {
@@ -55,12 +55,12 @@ public final class ImagePickerItem extends BorderPane {
JFXButton selectButton = new JFXButton();
selectButton.setGraphic(SVG.PENCIL.createIcon(Theme.blackFill(), 20, 20));
selectButton.onMouseClickedProperty().bind(onSelectButtonClicked);
selectButton.onActionProperty().bind(onSelectButtonClicked);
selectButton.getStyleClass().add("toggle-icon4");
JFXButton deleteButton = new JFXButton();
deleteButton.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), 20, 20));
deleteButton.onMouseClickedProperty().bind(onDeleteButtonClicked);
deleteButton.onActionProperty().bind(onDeleteButtonClicked);
deleteButton.getStyleClass().add("toggle-icon4");
FXUtils.installFastTooltip(selectButton, i18n("button.edit"));
@@ -93,27 +93,27 @@ public final class ImagePickerItem extends BorderPane {
this.title.set(title);
}
public EventHandler<? super MouseEvent> getOnSelectButtonClicked() {
public EventHandler<ActionEvent> getOnSelectButtonClicked() {
return onSelectButtonClicked.get();
}
public ObjectProperty<EventHandler<? super MouseEvent>> onSelectButtonClickedProperty() {
public ObjectProperty<EventHandler<ActionEvent>> onSelectButtonClickedProperty() {
return onSelectButtonClicked;
}
public void setOnSelectButtonClicked(EventHandler<? super MouseEvent> onSelectButtonClicked) {
public void setOnSelectButtonClicked(EventHandler<ActionEvent> onSelectButtonClicked) {
this.onSelectButtonClicked.set(onSelectButtonClicked);
}
public EventHandler<? super MouseEvent> getOnDeleteButtonClicked() {
public EventHandler<ActionEvent> getOnDeleteButtonClicked() {
return onDeleteButtonClicked.get();
}
public ObjectProperty<EventHandler<? super MouseEvent>> onDeleteButtonClickedProperty() {
public ObjectProperty<EventHandler<ActionEvent>> onDeleteButtonClickedProperty() {
return onDeleteButtonClicked;
}
public void setOnDeleteButtonClicked(EventHandler<? super MouseEvent> onDeleteButtonClicked) {
public void setOnDeleteButtonClicked(EventHandler<ActionEvent> onDeleteButtonClicked) {
this.onDeleteButtonClicked.set(onDeleteButtonClicked);
}
@@ -166,6 +166,16 @@ public final class MessageDialogPane extends HBox {
return this;
}
public Builder addAction(String text, @Nullable Runnable action) {
JFXButton btnAction = new JFXButton(text);
btnAction.getStyleClass().add("dialog-accept");
if (action != null) {
btnAction.setOnAction(e -> action.run());
}
dialog.addButton(btnAction);
return this;
}
public Builder ok(@Nullable Runnable ok) {
JFXButton btnOk = new JFXButton(i18n("button.ok"));
btnOk.getStyleClass().add("dialog-accept");
@@ -57,7 +57,7 @@ public final class MultiFileItem<T> extends VBox {
if (toggleSelectedListener != null)
toggleSelectedListener.accept(newValue);
selectedData.set((T) newValue.getUserData());
selectedData.set(newValue != null ? (T) newValue.getUserData() : null);
});
selectedData.addListener((a, b, newValue) -> {
Optional<Toggle> selecting = group.getToggles().stream()
@@ -183,6 +183,10 @@ public final class MultiFileItem<T> extends VBox {
super(title, data);
}
public JFXTextField getCustomField() {
return customField;
}
public String getValue() {
return customField.getText();
}
@@ -126,7 +126,7 @@ public class Navigator extends TransitionPane {
if (obj instanceof AnimationProducer) {
setContent(node, (AnimationProducer) obj);
} else {
setContent(node, ContainerAnimations.NONE.getAnimationProducer());
setContent(node, ContainerAnimations.NONE);
}
NavigationEvent navigated = new NavigationEvent(this, node, Navigation.NavigationDirection.PREVIOUS, NavigationEvent.NAVIGATED);
@@ -61,9 +61,7 @@ public class OptionToggleButton extends StackPane {
toggleButton.setSize(8);
FXUtils.setLimitHeight(toggleButton, 30);
container.setOnMouseClicked(e -> {
toggleButton.setSelected(!toggleButton.isSelected());
});
FXUtils.onClicked(container, () -> toggleButton.setSelected(!toggleButton.isSelected()));
FXUtils.onChangeAndOperate(subtitleProperty(), subtitle -> {
if (StringUtils.isNotBlank(subtitle)) {
@@ -32,7 +32,6 @@ import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
@DefaultProperty("content")
public class SpinnerPane extends Control {
@@ -101,18 +100,19 @@ public class SpinnerPane extends Control {
return onFailedActionProperty().get();
}
private ObjectProperty<EventHandler<Event>> onFailedAction = new SimpleObjectProperty<EventHandler<Event>>(this, "onFailedAction") {
private final ObjectProperty<EventHandler<Event>> onFailedAction = new SimpleObjectProperty<EventHandler<Event>>(this, "onFailedAction") {
@Override
protected void invalidated() {
setEventHandler(FAILED_ACTION, get());
}
};
@Override
protected Skin createDefaultSkin() {
protected SkinBase<SpinnerPane> createDefaultSkin() {
return new Skin(this);
}
private static class Skin extends SkinBase<SpinnerPane> {
private static final class Skin extends SkinBase<SpinnerPane> {
private final JFXSpinner spinner = new JFXSpinner();
private final StackPane contentPane = new StackPane();
private final StackPane topPane = new StackPane();
@@ -122,20 +122,18 @@ public class SpinnerPane extends Control {
@SuppressWarnings("FieldCanBeLocal") // prevent from gc.
private final InvalidationListener observer;
protected Skin(SpinnerPane control) {
Skin(SpinnerPane control) {
super(control);
topPane.getChildren().setAll(spinner);
topPane.getStyleClass().add("notice-pane");
failedPane.getStyleClass().add("notice-pane");
failedPane.getChildren().setAll(failedReasonLabel);
failedPane.onMouseClickedProperty().bind(
BindingMapping.of(control.onFailedAction)
.map(actionHandler -> (e -> {
if (actionHandler != null) {
actionHandler.handle(new Event(FAILED_ACTION));
}
})));
FXUtils.onClicked(failedPane, () -> {
EventHandler<Event> action = control.getOnFailedAction();
if (action != null)
action.handle(new Event(FAILED_ACTION));
});
FXUtils.onChangeAndOperate(getSkinnable().content, newValue -> {
if (newValue == null) {
@@ -148,12 +146,12 @@ public class SpinnerPane extends Control {
observer = FXUtils.observeWeak(() -> {
if (getSkinnable().getFailedReason() != null) {
root.setContent(failedPane, ContainerAnimations.FADE.getAnimationProducer());
root.setContent(failedPane, ContainerAnimations.FADE);
failedReasonLabel.setText(getSkinnable().getFailedReason());
} else if (getSkinnable().isLoading()) {
root.setContent(topPane, ContainerAnimations.FADE.getAnimationProducer());
root.setContent(topPane, ContainerAnimations.FADE);
} else {
root.setContent(contentPane, ContainerAnimations.FADE.getAnimationProducer());
root.setContent(contentPane, ContainerAnimations.FADE);
}
}, getSkinnable().loadingProperty(), getSkinnable().failedReasonProperty());
}
@@ -30,7 +30,6 @@ import javafx.geometry.Insets;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.transform.Rotate;
@@ -594,11 +593,9 @@ public class TabHeader extends Control implements TabControl, PageAware {
FXUtils.onChangeAndOperate(tab.selectedProperty(), selected -> inner.pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, selected));
this.setOnMouseClicked(event -> {
if (event.getButton() == MouseButton.PRIMARY) {
this.setOpacity(1);
getSkinnable().getSelectionModel().select(tab);
}
FXUtils.onClicked(this, () -> {
this.setOpacity(1);
getSkinnable().getSelectionModel().select(tab);
});
}
}
@@ -34,7 +34,7 @@ import org.jackhuang.hmcl.download.forge.ForgeNewInstallTask;
import org.jackhuang.hmcl.download.forge.ForgeOldInstallTask;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameInstallTask;
import org.jackhuang.hmcl.download.java.JavaDownloadTask;
import org.jackhuang.hmcl.download.java.mojang.MojangJavaDownloadTask;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
import org.jackhuang.hmcl.download.neoforge.NeoForgeInstallTask;
import org.jackhuang.hmcl.download.neoforge.NeoForgeOldInstallTask;
@@ -42,6 +42,7 @@ import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.download.quilt.QuiltAPIInstallTask;
import org.jackhuang.hmcl.download.quilt.QuiltInstallTask;
import org.jackhuang.hmcl.game.HMCLModpackInstallTask;
import org.jackhuang.hmcl.java.JavaInstallTask;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
import org.jackhuang.hmcl.mod.ModpackInstallTask;
import org.jackhuang.hmcl.mod.ModpackUpdateTask;
@@ -159,8 +160,10 @@ public final class TaskListPane extends StackPane {
task.setName(i18n("modpack.export"));
} else if (task instanceof MinecraftInstanceTask) {
task.setName(i18n("modpack.scan"));
} else if (task instanceof JavaDownloadTask) {
} else if (task instanceof MojangJavaDownloadTask) {
task.setName(i18n("download.java"));
} else if (task instanceof JavaInstallTask) {
task.setName(i18n("java.install"));
}
Platform.runLater(() -> {
@@ -30,6 +30,8 @@ import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.input.DragEvent;
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.stage.Stage;
@@ -144,6 +146,18 @@ public class DecoratorController {
// press ESC to go back
onEscPressed(navigator, this::back);
try {
// For JavaFX 12+
MouseButton button = MouseButton.valueOf("BACK");
navigator.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> {
if (e.getButton() == button) {
back();
e.consume();
}
});
} catch (IllegalArgumentException ignored) {
}
}
public Decorator getDecorator() {
@@ -443,7 +457,7 @@ public class DecoratorController {
public void startWizard(WizardProvider wizardProvider, String category) {
FXUtils.checkFxUserThread();
navigator.navigate(new DecoratorWizardDisplayer(wizardProvider, category), ContainerAnimations.FADE.getAnimationProducer());
navigator.navigate(new DecoratorWizardDisplayer(wizardProvider, category), ContainerAnimations.FADE);
}
// ==== Authlib Injector DnD ====
@@ -18,10 +18,8 @@
package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.svg.SVGGlyph;
import javafx.beans.binding.Bindings;
import javafx.collections.ListChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -44,13 +42,8 @@ import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.util.Lang;
public class DecoratorSkin extends SkinBase<Decorator> {
private static final PseudoClass TRANSPARENT = PseudoClass.getPseudoClass("transparent");
private static final SVGGlyph minus = Lang.apply(new SVGGlyph(0, "MINUS", "M804.571 420.571v109.714q0 22.857-16 38.857t-38.857 16h-694.857q-22.857 0-38.857-16t-16-38.857v-109.714q0-22.857 16-38.857t38.857-16h694.857q22.857 0 38.857 16t16 38.857z", Color.WHITE),
glyph -> { glyph.setSize(12, 2); glyph.setTranslateY(4); });
private final StackPane root, parent;
private final StackPane titleContainer;
private final Stage primaryStage;
@@ -69,8 +62,6 @@ public class DecoratorSkin extends SkinBase<Decorator> {
primaryStage = control.getPrimaryStage();
minus.fillProperty().bind(Theme.foregroundFillBinding());
Decorator skinnable = getSkinnable();
root = new StackPane();
root.getStyleClass().add("window");
@@ -174,11 +165,11 @@ public class DecoratorSkin extends SkinBase<Decorator> {
if (s.isAnimate()) {
AnimationProducer animation;
if (skinnable.getNavigationDirection() == Navigation.NavigationDirection.NEXT) {
animation = ContainerAnimations.SWIPE_LEFT_FADE_SHORT.getAnimationProducer();
animation = ContainerAnimations.SWIPE_LEFT_FADE_SHORT;
} else if (skinnable.getNavigationDirection() == Navigation.NavigationDirection.PREVIOUS) {
animation = ContainerAnimations.SWIPE_RIGHT_FADE_SHORT.getAnimationProducer();
animation = ContainerAnimations.SWIPE_RIGHT_FADE_SHORT;
} else {
animation = ContainerAnimations.FADE.getAnimationProducer();
animation = ContainerAnimations.FADE;
}
skinnable.setNavigationDirection(Navigation.NavigationDirection.START);
navBarPane.setContent(node, animation);
@@ -202,9 +193,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
btnHelp.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/help.html"));
JFXButton btnMin = new JFXButton();
StackPane pane = new StackPane(minus);
pane.setAlignment(Pos.CENTER);
btnMin.setGraphic(pane);
btnMin.setGraphic(SVG.MINUS.createIcon(Theme.foregroundFillBinding(), -1, -1));
btnMin.getStyleClass().add("jfx-decorator-button");
btnMin.setOnAction(e -> skinnable.minimize());
@@ -36,7 +36,7 @@ public abstract class DecoratorTabPage extends DecoratorTransitionPage implement
if (newValue.getNode() != null) {
onNavigating(getCurrentPage());
if (getCurrentPage() != null) getCurrentPage().fireEvent(new Navigator.NavigationEvent(null, getCurrentPage(), Navigation.NavigationDirection.NEXT, Navigator.NavigationEvent.NAVIGATING));
navigate(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer());
navigate(newValue.getNode(), ContainerAnimations.FADE);
onNavigated(getCurrentPage());
if (getCurrentPage() != null) getCurrentPage().fireEvent(new Navigator.NavigationEvent(null, getCurrentPage(), Navigation.NavigationDirection.NEXT, Navigator.NavigationEvent.NAVIGATED));
}
@@ -74,7 +74,7 @@ public class DecoratorWizardDisplayer extends DecoratorTransitionPage implements
@Override
public void navigateTo(Node page, Navigation.NavigationDirection nav) {
displayer.navigateTo(page, nav);
navigate(page, nav.getAnimation().getAnimationProducer());
navigate(page, nav.getAnimation());
String prefix = category == null ? "" : category + " - ";
@@ -59,7 +59,7 @@ class AdditionalInstallersPage extends InstallersPage {
for (InstallerItem library : group.getLibraries()) {
String libraryId = library.getLibraryId();
if (libraryId.equals("game")) continue;
library.removeActionProperty().set(e -> {
library.setOnRemove(() -> {
controller.getSettings().put(libraryId, new UpdateInstallerWizardProvider.RemoveVersionAction(libraryId));
reload();
});
@@ -55,6 +55,7 @@ import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Locale;
import java.util.concurrent.CancellationException;
import java.util.function.Supplier;
@@ -96,12 +97,12 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
tab.select(newGameTab);
FXUtils.onChangeAndOperate(tab.getSelectionModel().selectedItemProperty(), newValue -> {
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer());
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE);
});
{
AdvancedListBox sideBar = new AdvancedListBox()
.startCategory(i18n("download.game"))
.startCategory(i18n("download.game").toUpperCase(Locale.ROOT))
.addNavigationDrawerItem(item -> {
item.setTitle(i18n("game"));
item.setLeftGraphic(wrap(SVG.GAMEPAD));
@@ -114,7 +115,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
settingsItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(modpackTab));
settingsItem.setOnAction(e -> tab.select(modpackTab));
})
.startCategory(i18n("download.content"))
.startCategory(i18n("download.content").toUpperCase(Locale.ROOT))
.addNavigationDrawerItem(item -> {
item.setTitle(i18n("mods"));
item.setLeftGraphic(wrap(SVG.PUZZLE));
@@ -52,6 +52,8 @@ public class InstallersPage extends Control implements WizardPage {
protected JFXTextField txtName = new JFXTextField();
protected BooleanProperty installable = new SimpleBooleanProperty();
private boolean isNameModifiedByUser = false;
public InstallersPage(WizardController controller, HMCLGameRepository repository, String gameVersion, DownloadProvider downloadProvider) {
this.controller = controller;
this.group = new InstallerItem.InstallerItemGroup(gameVersion, getInstallerItemStyle());
@@ -61,12 +63,13 @@ public class InstallersPage extends Control implements WizardPage {
new Validator(i18n("install.new_game.already_exists"), str -> !repository.versionIdConflicts(str)),
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
installable.bind(createBooleanBinding(txtName::validate, txtName.textProperty()));
txtName.setText(gameVersion);
txtName.textProperty().addListener((obs, oldText, newText) -> isNameModifiedByUser = true);
for (InstallerItem library : group.getLibraries()) {
String libraryId = library.getLibraryId();
if (libraryId.equals(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId())) continue;
library.installActionProperty().set(e -> {
library.setOnInstall(() -> {
if (LibraryAnalyzer.LibraryType.FABRIC_API.getPatchId().equals(libraryId)) {
Controllers.dialog(i18n("install.installer.fabric-api.warning"), i18n("message.warning"), MessageDialogPane.MessageType.WARNING);
}
@@ -74,7 +77,7 @@ public class InstallersPage extends Control implements WizardPage {
if (!(library.resolvedStateProperty().get() instanceof InstallerItem.IncompatibleState))
controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), gameVersion, downloadProvider, libraryId, () -> controller.onPrev(false)));
});
library.removeActionProperty().set(e -> {
library.setOnRemove(() -> {
controller.getSettings().remove(libraryId);
reload();
});
@@ -103,6 +106,9 @@ public class InstallersPage extends Control implements WizardPage {
library.versionProperty().set(null);
}
}
if (!isNameModifiedByUser) {
setTxtNameWithLoaders();
}
}
@Override
@@ -124,6 +130,49 @@ public class InstallersPage extends Control implements WizardPage {
return new InstallersPageSkin(this);
}
private void setTxtNameWithLoaders() {
StringBuilder nameBuilder = new StringBuilder(group.getGame().versionProperty().get().getVersion());
for (InstallerItem library : group.getLibraries()) {
String libraryId = library.getLibraryId().replace(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), "");
if (!controller.getSettings().containsKey(libraryId)) {
continue;
}
LibraryAnalyzer.LibraryType libraryType = LibraryAnalyzer.LibraryType.fromPatchId(libraryId);
if (libraryType != null) {
String loaderName;
switch (libraryType) {
case FORGE:
loaderName = i18n("install.installer.forge");
break;
case NEO_FORGE:
loaderName = i18n("install.installer.neoforge");
break;
case FABRIC:
loaderName = i18n("install.installer.fabric");
break;
case LITELOADER:
loaderName = i18n("install.installer.liteloader");
break;
case QUILT:
loaderName = i18n("install.installer.quilt");
break;
case OPTIFINE:
loaderName = i18n("install.installer.optifine");
break;
default:
continue;
}
nameBuilder.append("-").append(loaderName);
}
}
txtName.setText(nameBuilder.toString());
isNameModifiedByUser = false;
}
protected static class InstallersPageSkin extends SkinBase<InstallersPage> {
/**
@@ -173,7 +222,7 @@ public class InstallersPage extends Control implements WizardPage {
installButton.disableProperty().bind(control.installable.not());
installButton.setPrefWidth(100);
installButton.setPrefHeight(40);
installButton.setOnMouseClicked(e -> control.onInstall());
installButton.setOnAction(e -> control.onInstall());
BorderPane.setAlignment(installButton, Pos.CENTER_RIGHT);
root.setBottom(installButton);
}
@@ -31,10 +31,12 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WebPage;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -78,6 +80,8 @@ public final class LocalModpackPage extends ModpackPage {
});
}
btnDescription.setVisible(false);
File selectedFile;
Optional<File> filePath = tryCast(controller.getSettings().get(MODPACK_FILE), File.class);
if (filePath.isPresent()) {
@@ -107,7 +111,6 @@ public final class LocalModpackPage extends ModpackPage {
hideSpinner();
lblName.setText(selectedFile.getName());
installAsVersion.set(false);
lblModpackLocation.setText(selectedFile.getAbsolutePath());
if (!name.isPresent()) {
// trim: https://github.com/HMCL-dev/HMCL/issues/962
@@ -130,12 +133,12 @@ public final class LocalModpackPage extends ModpackPage {
lblVersion.setText(manifest.getVersion());
lblAuthor.setText(manifest.getAuthor());
lblModpackLocation.setText(selectedFile.getAbsolutePath());
if (!name.isPresent()) {
// trim: https://github.com/HMCL-dev/HMCL/issues/962
txtModpackName.setText(manifest.getName().trim());
}
btnDescription.setVisible(StringUtils.isNotBlank(manifest.getDescription()));
}
}).start();
}
@@ -153,9 +156,8 @@ public final class LocalModpackPage extends ModpackPage {
}
protected void onDescribe() {
if (manifest != null) {
FXUtils.showWebDialog(i18n("modpack.description"), manifest.getDescription());
}
if (manifest != null)
Controllers.navigate(new WebPage(i18n("modpack.description"), manifest.getDescription()));
}
public static final String MODPACK_FILE = "MODPACK_FILE";
@@ -22,9 +22,9 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
protected final Label lblName;
protected final Label lblVersion;
protected final Label lblAuthor;
protected final Label lblModpackLocation;
protected final JFXTextField txtModpackName;
protected final JFXButton btnInstall;
protected final JFXButton btnDescription;
protected ModpackPage(WizardController controller) {
this.controller = controller;
@@ -37,15 +37,6 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
ComponentList componentList = new ComponentList();
{
BorderPane locationPane = new BorderPane();
{
locationPane.setLeft(new Label(i18n("modpack.task.install.will")));
lblModpackLocation = new Label();
BorderPane.setAlignment(lblModpackLocation, Pos.CENTER_RIGHT);
locationPane.setCenter(lblModpackLocation);
}
BorderPane archiveNamePane = new BorderPane();
{
Label label = new Label(i18n("archive.file.name"));
@@ -87,7 +78,7 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
BorderPane descriptionPane = new BorderPane();
{
JFXButton btnDescription = new JFXButton(i18n("modpack.description"));
btnDescription = new JFXButton(i18n("modpack.description"));
btnDescription.getStyleClass().add("jfx-button-border");
btnDescription.setOnAction(e -> onDescribe());
descriptionPane.setLeft(btnDescription);
@@ -99,7 +90,7 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
}
componentList.getContent().setAll(
locationPane, archiveNamePane, modpackNamePane, versionPane, authorPane, descriptionPane);
archiveNamePane, modpackNamePane, versionPane, authorPane, descriptionPane);
}
borderPane.getChildren().setAll(componentList);
@@ -22,11 +22,12 @@ import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WebPage;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.IOException;
import java.util.Map;
@@ -43,7 +44,6 @@ public final class RemoteModpackPage extends ModpackPage {
manifest = tryCast(controller.getSettings().get(MODPACK_SERVER_MANIFEST), ServerModpackManifest.class)
.orElseThrow(() -> new IllegalStateException("MODPACK_SERVER_MANIFEST should exist"));
lblModpackLocation.setText(manifest.getFileApi());
try {
controller.getSettings().put(MODPACK_MANIFEST, manifest.toModpack(null));
@@ -70,6 +70,8 @@ public final class RemoteModpackPage extends ModpackPage {
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().versionIdConflicts(str)),
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
}
btnDescription.setVisible(StringUtils.isNotBlank(manifest.getDescription()));
}
@Override
@@ -84,7 +86,7 @@ public final class RemoteModpackPage extends ModpackPage {
}
protected void onDescribe() {
FXUtils.showWebDialog(i18n("modpack.description"), manifest.getDescription());
Controllers.navigate(new WebPage(i18n("modpack.description"), manifest.getDescription()));
}
public static final String MODPACK_SERVER_MANIFEST = "MODPACK_SERVER_MANIFEST";
@@ -95,7 +95,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
HintPane hintPane = new HintPane();
hintPane.setText(i18n("sponsor.bmclapi"));
hintPane.getStyleClass().add("sponsor-pane");
hintPane.setOnMouseClicked(e -> onSponsor());
FXUtils.onClicked(hintPane, this::onSponsor);
BorderPane.setMargin(hintPane, new Insets(10, 10, 0, 10));
this.setTop(hintPane);
@@ -146,7 +146,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
failedPane.getStyleClass().add("notice-pane");
{
Label label = new Label(i18n("download.failed.refresh"));
label.setOnMouseClicked(e -> onRefresh());
FXUtils.onClicked(label, this::onRefresh);
failedPane.getChildren().setAll(label);
}
@@ -155,7 +155,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
emptyPane.getStyleClass().add("notice-pane");
{
Label label = new Label(i18n("download.failed.empty"));
label.setOnMouseClicked(e -> onBack());
FXUtils.onClicked(label, this::onBack);
emptyPane.getChildren().setAll(label);
}
@@ -178,9 +178,9 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
btnRefresh.setGraphic(wrap(SVG.REFRESH.createIcon(Theme.blackFill(), -1, -1)));
Holder<RemoteVersionListCell> lastCell = new Holder<>();
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell));
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell, libraryId));
list.setOnMouseClicked(e -> {
FXUtils.onClicked(list, () -> {
if (list.getSelectionModel().getSelectedIndex() < 0)
return;
navigation.getSettings().put(libraryId, list.getSelectionModel().getSelectedItem());
@@ -210,7 +210,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
@Override
public void refresh() {
VersionList<?> currentVersionList = versionList;
root.setContent(spinner, ContainerAnimations.FADE.getAnimationProducer());
root.setContent(spinner, ContainerAnimations.FADE);
executor = currentVersionList.refreshAsync(gameVersion).whenComplete((result, exception) -> {
if (exception == null) {
List<RemoteVersion> items = loadVersions();
@@ -218,7 +218,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
Platform.runLater(() -> {
if (versionList != currentVersionList) return;
if (currentVersionList.getVersions(gameVersion).isEmpty()) {
root.setContent(emptyPane, ContainerAnimations.FADE.getAnimationProducer());
root.setContent(emptyPane, ContainerAnimations.FADE);
} else {
if (items.isEmpty()) {
chkRelease.setSelected(true);
@@ -227,14 +227,14 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
} else {
list.getItems().setAll(items);
}
root.setContent(center, ContainerAnimations.FADE.getAnimationProducer());
root.setContent(center, ContainerAnimations.FADE);
}
});
} else {
LOG.warning("Failed to fetch versions list", exception);
Platform.runLater(() -> {
if (versionList != currentVersionList) return;
root.setContent(failedPane, ContainerAnimations.FADE.getAnimationProducer());
root.setContent(failedPane, ContainerAnimations.FADE);
});
}
@@ -272,8 +272,12 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
private final Holder<RemoteVersionListCell> lastCell;
RemoteVersionListCell(Holder<RemoteVersionListCell> lastCell) {
RemoteVersionListCell(Holder<RemoteVersionListCell> lastCell, String libraryId) {
this.lastCell = lastCell;
if ("game".equals(libraryId)) {
content.getExternalLinkButton().setGraphic(SVG.EARTH.createIcon(Theme.blackFill(), -1, -1));
FXUtils.installFastTooltip(content.getExternalLinkButton(), i18n("wiki.tooltip"));
}
pane.getStyleClass().add("md-list-cell");
StackPane.setMargin(content, new Insets(10, 16, 10, 16));
@@ -307,14 +311,19 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
case RELEASE:
content.getTags().setAll(i18n("version.game.release"));
content.setImage(VersionIconType.GRASS.getIcon());
content.setExternalLink(i18n("wiki.version.game.release", remoteVersion.getGameVersion()));
break;
case SNAPSHOT:
content.getTags().setAll(i18n("version.game.snapshot"));
content.setImage(VersionIconType.COMMAND.getIcon());
content.setExternalLink(i18n("wiki.version.game.snapshot", remoteVersion.getGameVersion()));
break;
default:
content.getTags().setAll(i18n("version.game.old"));
content.setImage(VersionIconType.CRAFT_TABLE.getIcon());
content.setExternalLink(null);
break;
}
} else {
@@ -339,6 +348,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
content.setSubtitle(remoteVersion.getGameVersion());
else
content.getTags().setAll(remoteVersion.getGameVersion());
content.setExternalLink(null);
}
}
}
@@ -21,12 +21,10 @@ import com.jfoenix.controls.*;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.auth.Account;
@@ -78,7 +76,6 @@ public final class ModpackInfoPage extends Control implements WizardPage {
private final SimpleStringProperty authlibInjectorServer = new SimpleStringProperty();
private final SimpleStringProperty launchArguments = new SimpleStringProperty("");
private final SimpleStringProperty javaArguments = new SimpleStringProperty("");
private final ObjectProperty<EventHandler<? super MouseEvent>> next = new SimpleObjectProperty<>();
private final SimpleStringProperty mcbbsThreadId = new SimpleStringProperty("");
public ModpackInfoPage(WizardController controller, HMCLGameRepository gameRepository, String version) {
@@ -97,8 +94,6 @@ public final class ModpackInfoPage extends Control implements WizardPage {
javaArguments.set(versionSetting.getJavaArgs());
canIncludeLauncher = JarUtils.thisJarPath() != null;
next.set(e -> onNext());
}
private void onNext() {
@@ -178,9 +173,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
if (skinnable.controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_SERVER) {
Hyperlink hyperlink = new Hyperlink(i18n("modpack.wizard.step.initialization.server"));
hyperlink.setOnMouseClicked(e -> {
FXUtils.openLink("https://docs.hmcl.net/modpack/serverpack.html");
});
hyperlink.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/modpack/serverpack.html"));
borderPane.setTop(hyperlink);
} else {
HintPane pane = new HintPane(MessageDialogPane.MessageType.INFO);
@@ -378,7 +371,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
borderPane.setBottom(hbox);
JFXButton nextButton = FXUtils.newRaisedButton(i18n("wizard.next"));
nextButton.onMouseClickedProperty().bind(skinnable.next);
nextButton.setOnAction(e -> skinnable.onNext());
nextButton.setPrefWidth(100);
nextButton.setPrefHeight(40);
nextButton.disableProperty().bind(
@@ -17,18 +17,31 @@
*/
package org.jackhuang.hmcl.ui.main;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class AboutPage extends StackPane {
public final class AboutPage extends StackPane {
public AboutPage() {
ComponentList about = new ComponentList();
@@ -48,131 +61,9 @@ public class AboutPage extends StackPane {
about.getContent().setAll(launcher, author);
}
ComponentList thanks = new ComponentList();
{
IconedTwoLineListItem yushijinhun = new IconedTwoLineListItem();
yushijinhun.setImage(FXUtils.newBuiltinImage("/assets/img/yushijinhun.png"));
yushijinhun.setTitle("yushijinhun");
yushijinhun.setSubtitle(i18n("about.thanks_to.yushijinhun.statement"));
yushijinhun.setExternalLink("https://yushi.moe/");
ComponentList thanks = loadIconedTwoLineList("/assets/about/thanks.json");
IconedTwoLineListItem bangbang93 = new IconedTwoLineListItem();
bangbang93.setImage(FXUtils.newBuiltinImage("/assets/img/bangbang93.png"));
bangbang93.setTitle("bangbang93");
bangbang93.setSubtitle(i18n("about.thanks_to.bangbang93.statement"));
bangbang93.setExternalLink("https://bmclapi2.bangbang93.com/");
IconedTwoLineListItem glavo = new IconedTwoLineListItem();
glavo.setImage(FXUtils.newBuiltinImage("/assets/img/glavo.png"));
glavo.setTitle("Glavo");
glavo.setSubtitle(i18n("about.thanks_to.glavo.statement"));
glavo.setExternalLink("https://github.com/Glavo");
IconedTwoLineListItem zekerzhayard = new IconedTwoLineListItem();
zekerzhayard.setImage(FXUtils.newBuiltinImage("/assets/img/zekerzhayard.png"));
zekerzhayard.setTitle("ZekerZhayard");
zekerzhayard.setSubtitle(i18n("about.thanks_to.zekerzhayard.statement"));
zekerzhayard.setExternalLink("https://github.com/ZekerZhayard");
IconedTwoLineListItem zkitefly = new IconedTwoLineListItem();
zkitefly.setImage(FXUtils.newBuiltinImage("/assets/img/zkitefly.png"));
zkitefly.setTitle("Zkitefly");
zkitefly.setSubtitle(i18n("about.thanks_to.zkitefly.statement"));
zkitefly.setExternalLink("https://github.com/zkitefly");
IconedTwoLineListItem burningtnt = new IconedTwoLineListItem();
burningtnt.setImage(FXUtils.newBuiltinImage("/assets/img/burningtnt.png"));
burningtnt.setTitle("Burning_TNT");
burningtnt.setSubtitle(i18n("about.thanks_to.burningtnt.statement"));
burningtnt.setExternalLink("https://github.com/burningtnt");
IconedTwoLineListItem shulkerSakura = new IconedTwoLineListItem();
shulkerSakura.setTitle("ShulkerSakura");
shulkerSakura.setImage(FXUtils.newBuiltinImage("/assets/img/ShulkerSakura.png"));
shulkerSakura.setSubtitle(i18n("about.thanks_to.shulkersakura.statement"));
shulkerSakura.setExternalLink("https://github.com/ShulkerSakura");
IconedTwoLineListItem gamerteam = new IconedTwoLineListItem();
gamerteam.setTitle("gamerteam");
gamerteam.setImage(FXUtils.newBuiltinImage("/assets/img/gamerteam.png"));
gamerteam.setSubtitle(i18n("about.thanks_to.gamerteam.statement"));
gamerteam.setExternalLink("http://www.zhaisoul.com/");
IconedTwoLineListItem redLnn = new IconedTwoLineListItem();
redLnn.setTitle("Red_lnn");
redLnn.setImage(FXUtils.newBuiltinImage("/assets/img/red_lnn.png"));
redLnn.setSubtitle(i18n("about.thanks_to.red_lnn.statement"));
IconedTwoLineListItem mcmod = new IconedTwoLineListItem();
mcmod.setImage(FXUtils.newBuiltinImage("/assets/img/mcmod.png"));
mcmod.setTitle(i18n("about.thanks_to.mcmod"));
mcmod.setSubtitle(i18n("about.thanks_to.mcmod.statement"));
mcmod.setExternalLink("https://www.mcmod.cn/");
IconedTwoLineListItem mcbbs = new IconedTwoLineListItem();
mcbbs.setImage(FXUtils.newBuiltinImage("/assets/img/chest.png"));
mcbbs.setTitle(i18n("about.thanks_to.mcbbs"));
mcbbs.setSubtitle(i18n("about.thanks_to.mcbbs.statement"));
IconedTwoLineListItem contributors = new IconedTwoLineListItem();
contributors.setImage(FXUtils.newBuiltinImage("/assets/img/github.png"));
contributors.setTitle(i18n("about.thanks_to.contributors"));
contributors.setSubtitle(i18n("about.thanks_to.contributors.statement"));
contributors.setExternalLink("https://github.com/HMCL-dev/HMCL/graphs/contributors");
IconedTwoLineListItem users = new IconedTwoLineListItem();
users.setImage(FXUtils.newBuiltinImage("/assets/img/icon.png"));
users.setTitle(i18n("about.thanks_to.users"));
users.setSubtitle(i18n("about.thanks_to.users.statement"));
users.setExternalLink("https://docs.hmcl.net/groups.html");
thanks.getContent().setAll(yushijinhun, bangbang93, glavo, zekerzhayard, zkitefly, burningtnt, mcmod, mcbbs, shulkerSakura, gamerteam, redLnn, contributors, users);
}
ComponentList dep = new ComponentList();
{
IconedTwoLineListItem javafx = new IconedTwoLineListItem();
javafx.setTitle("JavaFX");
javafx.setSubtitle("Copyright © 2013, 2024, Oracle and/or its affiliates.\nLicensed under the GPL 2 with Classpath Exception.");
javafx.setExternalLink("https://openjfx.io/");
IconedTwoLineListItem jfoenix = new IconedTwoLineListItem();
jfoenix.setTitle("JFoenix");
jfoenix.setSubtitle("Copyright © 2016 JFoenix.\nLicensed under the MIT License.");
jfoenix.setExternalLink("https://github.com/sshahine/JFoenix");
IconedTwoLineListItem gson = new IconedTwoLineListItem();
gson.setTitle("Gson");
gson.setSubtitle("Copyright © 2008 Google Inc.\nLicensed under the Apache 2.0 License.");
gson.setExternalLink("https://github.com/google/gson");
IconedTwoLineListItem xz = new IconedTwoLineListItem();
xz.setTitle("XZ for Java");
xz.setSubtitle("Lasse Collin, Igor Pavlov, and/or Brett Okken.\nPublic Domain.");
xz.setExternalLink("https://tukaani.org/xz/java.html");
IconedTwoLineListItem fxgson = new IconedTwoLineListItem();
fxgson.setTitle("fx-gson");
fxgson.setSubtitle("Copyright © 2016 Joffrey Bion.\nLicensed under the MIT License.");
fxgson.setExternalLink("https://github.com/joffrey-bion/fx-gson");
IconedTwoLineListItem constantPoolScanner = new IconedTwoLineListItem();
constantPoolScanner.setTitle("Constant Pool Scanner");
constantPoolScanner.setSubtitle("Copyright © 1997-2010 Oracle and/or its affiliates.\nLicensed under the GPL 2 or the CDDL.");
constantPoolScanner.setExternalLink("https://github.com/jenkinsci/constant-pool-scanner");
IconedTwoLineListItem openNBT = new IconedTwoLineListItem();
openNBT.setTitle("OpenNBT");
openNBT.setSubtitle("Copyright © 2013-2021 Steveice10.\nLicensed under the MIT License.");
openNBT.setExternalLink("https://github.com/GeyserMC/OpenNBT");
IconedTwoLineListItem minecraftJFXSkin = new IconedTwoLineListItem();
minecraftJFXSkin.setTitle("minecraft-jfx-skin");
minecraftJFXSkin.setSubtitle("Copyright © 2016 InfinityStudio.\nLicensed under the GPL 3.");
minecraftJFXSkin.setExternalLink("https://github.com/InfinityStudio/minecraft-jfx-skin");
dep.getContent().setAll(javafx, jfoenix, gson, xz, fxgson, constantPoolScanner, openNBT, minecraftJFXSkin);
}
ComponentList deps = loadIconedTwoLineList("/assets/about/deps.json");
ComponentList legal = new ComponentList();
{
@@ -204,7 +95,7 @@ public class AboutPage extends StackPane {
thanks,
ComponentList.createComponentListTitle(i18n("about.dependency")),
dep,
deps,
ComponentList.createComponentListTitle(i18n("about.legal")),
legal
@@ -216,4 +107,49 @@ public class AboutPage extends StackPane {
FXUtils.smoothScrolling(scrollPane);
getChildren().setAll(scrollPane);
}
private static ComponentList loadIconedTwoLineList(String path) {
ComponentList componentList = new ComponentList();
InputStream input = FXUtils.class.getResourceAsStream(path);
if (input == null) {
LOG.warning("Resources not found: " + path);
return componentList;
}
try (Reader reader = new InputStreamReader(input, StandardCharsets.UTF_8)) {
JsonArray array = JsonUtils.GSON.fromJson(reader, JsonArray.class);
for (JsonElement element : array) {
JsonObject obj = element.getAsJsonObject();
IconedTwoLineListItem item = new IconedTwoLineListItem();
if (obj.has("image")) {
String image = obj.get("image").getAsString();
item.setImage(image.startsWith("/")
? FXUtils.newBuiltinImage(image)
: new Image(image));
}
if (obj.has("title"))
item.setTitle(obj.get("title").getAsString());
else if (obj.has("titleLocalized"))
item.setTitle(i18n(obj.get("titleLocalized").getAsString()));
if (obj.has("subtitle"))
item.setSubtitle(obj.get("subtitle").getAsString());
else if (obj.has("subtitleLocalized"))
item.setSubtitle(i18n(obj.get("subtitleLocalized").getAsString()));
if (obj.has("externalLink"))
item.setExternalLink(obj.get("externalLink").getAsString());
componentList.getContent().add(item);
}
} catch (IOException | JsonParseException e) {
LOG.warning("Failed to load list: " + path, e);
}
return componentList;
}
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.*;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
@@ -100,7 +99,7 @@ public class DownloadSettingsPage extends StackPane {
downloadSource.getChildren().setAll(chooseWrapper, versionListSourcePane, downloadSourcePane);
}
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.version_list_source")), downloadSource);
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.download_source")), downloadSource);
}
{
@@ -204,7 +203,6 @@ public class DownloadSettingsPage extends StackPane {
Label host = new Label(i18n("settings.launcher.proxy.host"));
GridPane.setRowIndex(host, 1);
GridPane.setColumnIndex(host, 0);
GridPane.setHalignment(host, HPos.RIGHT);
gridPane.getChildren().add(host);
}
@@ -220,7 +218,6 @@ public class DownloadSettingsPage extends StackPane {
Label port = new Label(i18n("settings.launcher.proxy.port"));
GridPane.setRowIndex(port, 2);
GridPane.setColumnIndex(port, 0);
GridPane.setHalignment(port, HPos.RIGHT);
gridPane.getChildren().add(port);
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui.main;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
@@ -34,6 +33,7 @@ import org.jackhuang.hmcl.util.io.HttpRequest;
import java.util.Collections;
import java.util.List;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class HelpPage extends SpinnerPane {
@@ -63,8 +63,7 @@ public class HelpPage extends SpinnerPane {
private void loadHelp() {
showSpinner();
Task.<List<HelpCategory>>supplyAsync(() -> HttpRequest.GET("https://docs.hmcl.net/index.json").getJson(new TypeToken<List<HelpCategory>>() {
}.getType()))
Task.supplyAsync(() -> HttpRequest.GET("https://docs.hmcl.net/index.json").getJson(listTypeOf(HelpCategory.class)))
.thenAcceptAsync(Schedulers.javafx(), helpCategories -> {
for (HelpCategory category : helpCategories) {
ComponentList categoryPane = new ComponentList();
@@ -0,0 +1,426 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.*;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.java.JavaDistribution;
import org.jackhuang.hmcl.download.java.JavaPackageType;
import org.jackhuang.hmcl.download.java.JavaRemoteVersion;
import org.jackhuang.hmcl.download.java.disco.*;
import org.jackhuang.hmcl.game.GameJavaVersion;
import org.jackhuang.hmcl.java.JavaInfo;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.DialogPane;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.platform.Platform;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
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;
/**
* @author Glavo
*/
public final class JavaDownloadDialog extends StackPane {
public static Runnable showDialogAction(DownloadProvider downloadProvider) {
Platform platform = Platform.SYSTEM_PLATFORM;
List<GameJavaVersion> supportedVersions = GameJavaVersion.getSupportedVersions(platform);
EnumSet<DiscoJavaDistribution> distributions = EnumSet.noneOf(DiscoJavaDistribution.class);
for (DiscoJavaDistribution distribution : DiscoJavaDistribution.values()) {
if (distribution.isSupport(platform)) {
distributions.add(distribution);
}
}
return supportedVersions.isEmpty() && distributions.isEmpty()
? null
: () -> Controllers.dialog(new JavaDownloadDialog(downloadProvider, platform, supportedVersions, distributions));
}
private final DownloadProvider downloadProvider;
private final Platform platform;
private final List<GameJavaVersion> supportedGameJavaVersions;
private final EnumSet<DiscoJavaDistribution> distributions;
private JavaDownloadDialog(DownloadProvider downloadProvider, Platform platform, List<GameJavaVersion> supportedGameJavaVersions, EnumSet<DiscoJavaDistribution> distributions) {
this.downloadProvider = downloadProvider;
this.platform = platform;
this.supportedGameJavaVersions = supportedGameJavaVersions;
this.distributions = distributions;
if (!supportedGameJavaVersions.isEmpty()) {
this.getChildren().add(new DownloadMojangJava());
} else {
this.getChildren().add(new DownloadDiscoJava());
}
}
private final class DownloadMojangJava extends DialogPane {
private final ToggleGroup toggleGroup = new ToggleGroup();
DownloadMojangJava() {
setTitle(i18n("java.download"));
validProperty().bind(toggleGroup.selectedToggleProperty().isNotNull());
VBox vbox = new VBox(16);
Label prompt = new Label(i18n("java.download.prompt"));
vbox.getChildren().add(prompt);
for (GameJavaVersion version : supportedGameJavaVersions) {
JFXRadioButton button = new JFXRadioButton("Java " + version.getMajorVersion());
button.setUserData(version);
vbox.getChildren().add(button);
toggleGroup.getToggles().add(button);
if (JavaManager.REPOSITORY.isInstalled(platform, version))
button.setDisable(true);
}
setBody(vbox);
if (!distributions.isEmpty()) {
JFXHyperlink more = new JFXHyperlink(i18n("java.download.more"));
more.setOnAction(event -> JavaDownloadDialog.this.getChildren().setAll(new DownloadDiscoJava()));
setActions(warningLabel, more, acceptPane, cancelButton);
} else
setActions(warningLabel, acceptPane, cancelButton);
}
private Task<Void> downloadTask(GameJavaVersion javaVersion) {
return JavaManager.getDownloadJavaTask(downloadProvider, platform, javaVersion).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception != null) {
Throwable resolvedException = resolveException(exception);
LOG.warning("Failed to download java", exception);
if (!(resolvedException instanceof CancellationException)) {
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
}
}
});
}
@Override
protected void onAccept() {
fireEvent(new DialogCloseEvent());
GameJavaVersion javaVersion = (GameJavaVersion) toggleGroup.getSelectedToggle().getUserData();
if (javaVersion == null)
return;
if (JavaManager.REPOSITORY.isInstalled(platform, javaVersion))
Controllers.confirm(i18n("download.java.override"), null, () -> {
Controllers.taskDialog(Task.supplyAsync(() -> JavaManager.REPOSITORY.getJavaExecutable(platform, javaVersion))
.thenComposeAsync(Schedulers.javafx(), realPath -> {
if (realPath != null) {
JavaManager.removeJava(realPath);
}
return downloadTask(javaVersion);
}), i18n("download.java"), TaskCancellationAction.NORMAL);
}, null);
else
Controllers.taskDialog(downloadTask(javaVersion), i18n("download.java"), TaskCancellationAction.NORMAL);
}
}
private final class DownloadDiscoJava extends JFXDialogLayout {
private final JFXComboBox<DiscoJavaDistribution> distributionBox;
private final JFXComboBox<DiscoJavaRemoteVersion> remoteVersionBox;
private final JFXComboBox<JavaPackageType> packageTypeBox;
private final Label warningLabel = new Label();
private final JFXButton downloadButton;
private final StackPane downloadButtonPane = new StackPane();
private final DownloadProvider downloadProvider = DownloadProviders.getDownloadProvider();
private final Map<DiscoJavaDistribution, DiscoJavaVersionList> javaVersionLists = new HashMap<>();
private final ObjectProperty<DiscoJavaVersionList> currentJavaVersionList = new SimpleObjectProperty<>();
DownloadDiscoJava() {
assert !distributions.isEmpty();
this.distributionBox = new JFXComboBox<>();
this.distributionBox.setConverter(FXUtils.stringConverter(JavaDistribution::getDisplayName));
this.remoteVersionBox = new JFXComboBox<>();
this.remoteVersionBox.setConverter(FXUtils.stringConverter(JavaRemoteVersion::getDistributionVersion));
this.packageTypeBox = new JFXComboBox<>(FXCollections.observableArrayList());
this.downloadButton = new JFXButton(i18n("download"));
downloadButton.setOnAction(e -> onDownload());
downloadButton.getStyleClass().add("dialog-accept");
downloadButton.disableProperty().bind(Bindings.isNull(remoteVersionBox.getSelectionModel().selectedItemProperty()));
downloadButtonPane.getChildren().setAll(downloadButton);
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
cancelButton.getStyleClass().add("dialog-cancel");
onEscPressed(this, cancelButton::fire);
GridPane body = new GridPane();
body.getColumnConstraints().setAll(new ColumnConstraints(), FXUtils.getColumnHgrowing());
body.setVgap(8);
body.setHgap(16);
body.addRow(0, new Label(i18n("java.download.distribution")), distributionBox);
body.addRow(1, new Label(i18n("java.download.version")), remoteVersionBox);
body.addRow(2, new Label(i18n("java.download.packageType")), packageTypeBox);
distributionBox.setItems(FXCollections.observableList(new ArrayList<>(distributions)));
FXUtils.onChange(packageTypeBox.getSelectionModel().selectedItemProperty(), packageType -> {
ObservableList<DiscoJavaRemoteVersion> versions;
if (packageType == null
|| currentJavaVersionList.get() == null
|| (versions = currentJavaVersionList.get().versions.get(packageType)) == null) {
remoteVersionBox.setItems(null);
return;
}
remoteVersionBox.setItems(versions);
for (int i = 0; i < versions.size(); i++) {
DiscoJavaRemoteVersion version = versions.get(i);
if (version.getJdkVersion() == GameJavaVersion.LATEST.getMajorVersion()) {
remoteVersionBox.getSelectionModel().select(i);
return;
}
}
for (int i = 0; i < versions.size(); i++) {
DiscoJavaRemoteVersion version = versions.get(i);
if (version.isLTS()) {
remoteVersionBox.getSelectionModel().select(i);
return;
}
}
remoteVersionBox.getSelectionModel().selectFirst();
});
Consumer<DiscoJavaVersionList> updateListStatus = list -> {
remoteVersionBox.setItems(null);
packageTypeBox.getItems().clear();
remoteVersionBox.setDisable(true);
packageTypeBox.setDisable(true);
warningLabel.setText(null);
if (list == null || (list.versions != null && list.versions.isEmpty()))
downloadButtonPane.getChildren().setAll(downloadButton);
else if (list.status == DiscoJavaVersionList.Status.LOADING)
downloadButtonPane.getChildren().setAll(new JFXSpinner());
else {
downloadButtonPane.getChildren().setAll(downloadButton);
if (list.status == DiscoJavaVersionList.Status.SUCCESS) {
packageTypeBox.getItems().setAll(list.versions.keySet());
packageTypeBox.getSelectionModel().selectFirst();
remoteVersionBox.setDisable(false);
packageTypeBox.setDisable(false);
} else
warningLabel.setText(i18n("java.download.load_list.failed"));
}
};
currentJavaVersionList.addListener((observable, oldValue, newValue) -> {
if (oldValue != null)
oldValue.listener = null;
if (newValue != null) {
updateListStatus.accept(newValue);
if (newValue.status == DiscoJavaVersionList.Status.LOADING)
newValue.listener = updateListStatus;
} else {
currentJavaVersionList.set(null);
updateListStatus.accept(null);
}
});
FXUtils.onChange(distributionBox.getSelectionModel().selectedItemProperty(),
it -> currentJavaVersionList.set(getJavaVersionList(it)));
setHeading(new Label(i18n("java.download")));
setBody(body);
setActions(warningLabel, downloadButtonPane, cancelButton);
}
private void onDownload() {
fireEvent(new DialogCloseEvent());
DiscoJavaDistribution distribution = distributionBox.getSelectionModel().getSelectedItem();
DiscoJavaRemoteVersion version = remoteVersionBox.getSelectionModel().getSelectedItem();
JavaPackageType packageType = packageTypeBox.getSelectionModel().getSelectedItem();
if (version == null)
return;
Controllers.taskDialog(new GetTask(downloadProvider.injectURLWithCandidates(version.getLinks().getPkgInfoUri()))
.setExecutor(Schedulers.io())
.thenComposeAsync(json -> {
DiscoResult<DiscoRemoteFileInfo> result = JsonUtils.fromNonNullJson(json, DiscoResult.typeOf(DiscoRemoteFileInfo.class));
if (result.getResult().size() != 1)
throw new IOException("Illegal result: " + json);
DiscoRemoteFileInfo fileInfo = result.getResult().get(0);
if (!fileInfo.getChecksumType().equals("sha1") && !fileInfo.getChecksumType().equals("sha256"))
throw new IOException("Unsupported checksum type: " + fileInfo.getChecksumType());
if (StringUtils.isBlank(fileInfo.getDirectDownloadUri()))
throw new IOException("Missing download URI: " + json);
File targetFile = File.createTempFile("hmcl-java-", "." + version.getArchiveType());
targetFile.deleteOnExit();
Task<FileDownloadTask.IntegrityCheck> getIntegrityCheck;
if (StringUtils.isNotBlank(fileInfo.getChecksum()))
getIntegrityCheck = Task.completed(new FileDownloadTask.IntegrityCheck(fileInfo.getChecksumType(), fileInfo.getChecksum()));
else if (StringUtils.isNotBlank(fileInfo.getChecksumUri()))
getIntegrityCheck = new GetTask(downloadProvider.injectURLWithCandidates(fileInfo.getChecksumUri()))
.thenApplyAsync(checksum -> {
checksum = checksum.trim();
int idx = checksum.indexOf(' ');
if (idx > 0)
checksum = checksum.substring(0, idx);
return new FileDownloadTask.IntegrityCheck(fileInfo.getChecksumType(), checksum);
});
else
throw new IOException("Unable to get checksum for file");
return getIntegrityCheck
.thenComposeAsync(integrityCheck ->
new FileDownloadTask(downloadProvider.injectURLWithCandidates(fileInfo.getDirectDownloadUri()),
targetFile, integrityCheck).setName(fileInfo.getFileName()))
.thenSupplyAsync(targetFile::toPath);
})
.whenComplete(Schedulers.javafx(), ((result, exception) -> {
if (exception == null) {
String javaVersion = version.getJavaVersion();
JavaInfo info = new JavaInfo(platform, javaVersion, distribution.getVendor());
Map<String, Object> updateInfo = new LinkedHashMap<>();
updateInfo.put("type", "disco");
updateInfo.put("info", version);
int idx = javaVersion.indexOf('+');
if (idx > 0) {
javaVersion = javaVersion.substring(0, idx);
}
String defaultName = distribution.getApiParameter() + "-" + javaVersion + "-" + packageType.name().toLowerCase(Locale.ROOT);
Controllers.getDecorator().startWizard(new SinglePageWizardProvider(controller ->
new JavaInstallPage(controller::onFinish, info, version, updateInfo, defaultName, result)));
} else {
LOG.warning("Failed to download java", exception);
Throwable resolvedException = resolveException(exception);
if (!(resolvedException instanceof CancellationException)) {
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
}
}
})), i18n("java.download"), TaskCancellationAction.NORMAL);
}
private DiscoJavaVersionList getJavaVersionList(DiscoJavaDistribution distribution) {
if (distribution == null)
return null;
return javaVersionLists.computeIfAbsent(distribution, it -> {
DiscoJavaVersionList versionList = new DiscoJavaVersionList(it);
new DiscoFetchJavaListTask(downloadProvider, it, platform).setExecutor(Schedulers.io()).thenApplyAsync(versions -> {
EnumMap<JavaPackageType, ObservableList<DiscoJavaRemoteVersion>> result = new EnumMap<>(JavaPackageType.class);
if (versions.isEmpty())
return result;
for (Map.Entry<JavaPackageType, TreeMap<Integer, DiscoJavaRemoteVersion>> entry : versions.entrySet())
for (DiscoJavaRemoteVersion version : entry.getValue().values())
if (version.isLTS()
|| version.getJdkVersion() == entry.getValue().lastKey() // latest version
|| version.getJdkVersion() == 16)
result.computeIfAbsent(entry.getKey(), ignored -> FXCollections.observableArrayList())
.add(version);
for (List<DiscoJavaRemoteVersion> l : result.values())
Collections.reverse(l);
return result;
}).whenComplete(Schedulers.javafx(), ((result, exception) -> {
if (exception == null) {
versionList.status = DiscoJavaVersionList.Status.SUCCESS;
versionList.versions = result;
} else {
LOG.warning("Failed to load java list", exception);
versionList.status = DiscoJavaVersionList.Status.FAILED;
}
versionList.invalidate();
})).start();
return versionList;
});
}
}
private static final class DiscoJavaVersionList {
enum Status {
LOADING, SUCCESS, FAILED
}
final DiscoJavaDistribution distribution;
Status status = Status.LOADING;
EnumMap<JavaPackageType, ObservableList<DiscoJavaRemoteVersion>> versions;
Consumer<DiscoJavaVersionList> listener;
DiscoJavaVersionList(DiscoJavaDistribution distribution) {
this.distribution = distribution;
}
void invalidate() {
if (listener != null)
listener.accept(this);
}
}
}
@@ -0,0 +1,190 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.download.java.JavaRemoteVersion;
import org.jackhuang.hmcl.download.java.disco.DiscoJavaDistribution;
import org.jackhuang.hmcl.download.java.disco.DiscoJavaRemoteVersion;
import org.jackhuang.hmcl.java.HMCLJavaRepository;
import org.jackhuang.hmcl.java.JavaInfo;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.wizard.WizardSinglePage;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class JavaInstallPage extends WizardSinglePage {
private static final Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z0-9.\\-_]+");
private final Path file;
private final JavaInfo info;
private final JavaRemoteVersion remoteVersion;
private final Map<String, Object> update;
private final StringProperty nameProperty = new SimpleStringProperty();
public JavaInstallPage(Runnable onFinish, JavaInfo info, JavaRemoteVersion remoteVersion, Map<String, Object> update, String defaultName, Path file) {
super(onFinish);
this.info = info;
this.remoteVersion = remoteVersion;
this.update = update;
this.file = file;
this.nameProperty.set(defaultName);
}
@Override
protected SkinBase<?> createDefaultSkin() {
return new Skin(this);
}
@Override
protected Object finish() {
Task<JavaRuntime> installTask = JavaManager.getInstallJavaTask(info.getPlatform(), nameProperty.get(), update, file);
return remoteVersion == null ? installTask : installTask.whenComplete(exception -> {
try {
Files.delete(file);
} catch (IOException e) {
LOG.warning("Failed to delete file: " + file, e);
}
});
}
@Override
public String getTitle() {
return i18n("java.install");
}
private static final class Skin extends SkinBase<JavaInstallPage> {
private final ComponentList componentList = new ComponentList();
private final JFXTextField nameField;
private final Set<String> usedNames = new HashSet<>();
Skin(JavaInstallPage control) {
super(control);
VBox borderPane = new VBox();
borderPane.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(borderPane, 500);
{
BorderPane namePane = new BorderPane();
{
Label label = new Label(i18n("java.install.name"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
namePane.setLeft(label);
nameField = new JFXTextField();
nameField.textProperty().bindBidirectional(control.nameProperty);
FXUtils.setLimitWidth(nameField, 200);
BorderPane.setAlignment(nameField, Pos.CENTER_RIGHT);
BorderPane.setMargin(nameField, new Insets(0, 0, 12, 0));
namePane.setRight(nameField);
nameField.setValidators(
new RequiredValidator(),
new Validator(i18n("java.install.warning.invalid_character"),
text -> !text.startsWith(HMCLJavaRepository.MOJANG_JAVA_PREFIX) && NAME_PATTERN.matcher(text).matches()),
new Validator(i18n("java.install.failed.exists"), text -> !usedNames.contains(text))
);
String defaultName = control.nameProperty.get();
if (JavaManager.REPOSITORY.isInstalled(control.info.getPlatform(), defaultName)) {
usedNames.add(defaultName);
}
nameField.textProperty().addListener(o -> nameField.validate());
nameField.validate();
componentList.getContent().add(namePane);
}
String vendor = JavaInfo.normalizeVendor(control.info.getVendor());
if (vendor != null)
addInfo(i18n("java.info.vendor"), vendor);
if (control.remoteVersion instanceof DiscoJavaRemoteVersion) {
String distributionName = ((DiscoJavaRemoteVersion) control.remoteVersion).getDistribution();
DiscoJavaDistribution distribution = DiscoJavaDistribution.of(distributionName);
addInfo(i18n("java.info.disco.distribution"), distribution != null ? distribution.getDisplayName() : distributionName);
} else
addInfo(i18n("java.install.archive"), control.file.toAbsolutePath().toString());
addInfo(i18n("java.info.version"), control.info.getVersion());
addInfo(i18n("java.info.architecture"), control.info.getPlatform().getArchitecture().getDisplayName());
BorderPane installPane = new BorderPane();
{
JFXButton installButton = FXUtils.newRaisedButton(i18n("button.install"));
installButton.setOnAction(e -> {
String name = control.nameProperty.get();
if (JavaManager.REPOSITORY.isInstalled(control.info.getPlatform(), name)) {
Controllers.dialog(i18n("java.install.failed.exists"), null, MessageDialogPane.MessageType.WARNING);
usedNames.add(name);
nameField.validate();
} else
control.onFinish.run();
});
installButton.disableProperty().bind(nameField.activeValidatorProperty().isNotNull());
installPane.setRight(installButton);
componentList.getContent().add(installPane);
}
}
borderPane.getChildren().setAll(componentList);
this.getChildren().setAll(borderPane);
}
private void addInfo(String name, String value) {
BorderPane pane = new BorderPane();
pane.setLeft(new Label(name));
Label valueLabel = FXUtils.truncatedLabel(value, 60);
BorderPane.setAlignment(valueLabel, Pos.CENTER_RIGHT);
pane.setCenter(valueLabel);
this.componentList.getContent().add(pane);
}
}
}
@@ -0,0 +1,321 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXButton;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.java.JavaInfo;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException;
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class JavaManagementPage extends ListPageBase<JavaManagementPage.JavaItem> {
@SuppressWarnings("FieldCanBeLocal")
private final ChangeListener<Collection<JavaRuntime>> listener;
private final Runnable onInstallJava;
public JavaManagementPage() {
this.listener = FXUtils.onWeakChangeAndOperate(JavaManager.getAllJavaProperty(), this::loadJava);
if (Platform.SYSTEM_PLATFORM.equals(OperatingSystem.LINUX, Architecture.LOONGARCH64_OW)) {
onInstallJava = () -> FXUtils.openLink("https://www.loongnix.cn/zh/api/java/");
} else {
onInstallJava = JavaDownloadDialog.showDialogAction(DownloadProviders.getDownloadProvider());
}
FXUtils.applyDragListener(this, it -> {
String name = it.getName();
return it.isDirectory() || name.endsWith(".zip") || name.endsWith(".tar.gz") || name.equals(OperatingSystem.CURRENT_OS.getJavaExecutable());
}, files -> {
for (File file : files) {
if (file.isDirectory()) {
onAddJavaHome(file.toPath());
} else {
String fileName = file.getName();
if (fileName.equals(OperatingSystem.CURRENT_OS.getJavaExecutable())) {
onAddJavaBinary(file.toPath());
} else if (fileName.endsWith(".zip") || fileName.endsWith(".tar.gz")) {
onInstallArchive(file.toPath());
} else {
throw new AssertionError("Unreachable code");
}
}
}
});
}
@Override
protected Skin<?> createDefaultSkin() {
return new JavaPageSkin(this);
}
void onAddJava() {
FileChooser chooser = new FileChooser();
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe"));
chooser.setTitle(i18n("settings.game.java_directory.choose"));
File file = chooser.showOpenDialog(Controllers.getStage());
if (file != null) {
JavaManager.getAddJavaTask(file.toPath()).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
LOG.warning("Failed to add java", exception);
Controllers.dialog(i18n("java.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
}).start();
}
}
void onShowRestoreJavaPage() {
Controllers.navigate(new JavaRestorePage(ConfigHolder.globalConfig().getDisabledJava()));
}
private void onAddJavaBinary(Path file) {
JavaManager.getAddJavaTask(file).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
LOG.warning("Failed to add java", exception);
Controllers.dialog(i18n("java.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
}).start();
}
private void onAddJavaHome(Path file) {
Task.composeAsync(() -> {
Path releaseFile = file.resolve("release");
if (Files.notExists(releaseFile))
throw new IOException("Missing release file " + releaseFile);
return JavaManager.getAddJavaTask(file.resolve("bin").resolve(OperatingSystem.CURRENT_OS.getJavaExecutable()));
}).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
LOG.warning("Failed to add java", exception);
Controllers.dialog(i18n("java.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
}).start();
}
private void onInstallArchive(Path file) {
Task.supplyAsync(() -> {
try (ArchiveFileTree<?, ?> tree = ArchiveFileTree.open(file)) {
JavaInfo info = JavaInfo.fromArchive(tree);
if (!JavaManager.isCompatible(info.getPlatform()))
throw new UnsupportedPlatformException(info.getPlatform().toString());
return Pair.pair(tree.getRoot().getSubDirs().keySet().iterator().next(), info);
}
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
Controllers.getDecorator().startWizard(new SinglePageWizardProvider(controller ->
new JavaInstallPage(controller::onFinish, result.getValue(), null, null, result.getKey(), file)));
} else {
if (exception instanceof UnsupportedPlatformException) {
Controllers.dialog(i18n("java.install.failed.unsupported_platform"), null, MessageDialogPane.MessageType.WARNING);
} else {
Controllers.dialog(i18n("java.install.failed.invalid"), null, MessageDialogPane.MessageType.WARNING);
}
}
}).start();
}
// FXThread
private void loadJava(Collection<JavaRuntime> javaRuntimes) {
if (javaRuntimes != null) {
List<JavaItem> items = new ArrayList<>();
for (JavaRuntime java : javaRuntimes) {
items.add(new JavaItem(java));
}
this.setItems(FXCollections.observableList(items));
this.setLoading(false);
} else
this.setLoading(true);
}
static final class JavaItem extends Control {
private final JavaRuntime java;
public JavaItem(JavaRuntime java) {
this.java = java;
}
public JavaRuntime getJava() {
return java;
}
public void onReveal() {
Path target;
Path parent = java.getBinary().getParent();
if (parent != null
&& parent.getParent() != null
&& parent.getFileName() != null
&& parent.getFileName().toString().equals("bin")
&& Files.exists(parent.getParent().resolve("release"))) {
target = parent.getParent();
} else {
target = java.getBinary();
}
FXUtils.showFileInExplorer(target);
}
public void onRemove() {
if (java.isManaged()) {
Controllers.taskDialog(JavaManager.getUninstallJavaTask(java), i18n("java.uninstall"), TaskCancellationAction.NORMAL);
} else {
String path = java.getBinary().toString();
ConfigHolder.globalConfig().getUserJava().remove(path);
ConfigHolder.globalConfig().getDisabledJava().add(path);
try {
JavaManager.removeJava(java);
} catch (InterruptedException ignored) {
}
}
}
@Override
protected Skin<?> createDefaultSkin() {
return new JavaRuntimeItemSkin(this);
}
}
private static final class JavaRuntimeItemSkin extends SkinBase<JavaItem> {
JavaRuntimeItemSkin(JavaItem control) {
super(control);
JavaRuntime java = control.getJava();
String vendor = JavaInfo.normalizeVendor(java.getVendor());
BorderPane root = new BorderPane();
HBox center = new HBox();
center.setMouseTransparent(true);
center.setSpacing(8);
center.setAlignment(Pos.CENTER_LEFT);
TwoLineListItem item = new TwoLineListItem();
item.setTitle((java.isJDK() ? "JDK" : "JRE") + " " + java.getVersion());
item.setSubtitle(java.getBinary().toString());
item.getTags().add(i18n("java.info.architecture") + ": " + java.getArchitecture().getDisplayName());
if (vendor != null)
item.getTags().add(i18n("java.info.vendor") + ": " + vendor);
BorderPane.setAlignment(item, Pos.CENTER);
center.getChildren().setAll(item);
root.setCenter(center);
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
{
JFXButton revealButton = new JFXButton();
revealButton.getStyleClass().add("toggle-icon4");
revealButton.setGraphic(FXUtils.limitingSize(SVG.FOLDER_OUTLINE.createIcon(Theme.blackFill(), 24, 24), 24, 24));
revealButton.setOnAction(e -> control.onReveal());
FXUtils.installFastTooltip(revealButton, i18n("java.reveal"));
JFXButton removeButton = new JFXButton();
removeButton.getStyleClass().add("toggle-icon4");
removeButton.setOnAction(e -> Controllers.confirm(
java.isManaged() ? i18n("java.uninstall.confirm") : i18n("java.disable.confirm"),
i18n("message.warning"),
control::onRemove,
null
));
if (java.isManaged()) {
removeButton.setGraphic(FXUtils.limitingSize(SVG.DELETE_OUTLINE.createIcon(Theme.blackFill(), 24, 24), 24, 24));
FXUtils.installFastTooltip(removeButton, i18n("java.uninstall"));
if (JavaRuntime.CURRENT_JAVA != null && java.getBinary().equals(JavaRuntime.CURRENT_JAVA.getBinary()))
removeButton.setDisable(true);
} else {
removeButton.setGraphic(FXUtils.limitingSize(SVG.CLOSE.createIcon(Theme.blackFill(), 24, 24), 24, 24));
FXUtils.installFastTooltip(removeButton, i18n("java.disable"));
}
right.getChildren().setAll(revealButton, removeButton);
}
root.setRight(right);
root.getStyleClass().add("md-list-cell");
root.setPadding(new Insets(8));
getChildren().setAll(new RipplerContainer(root));
}
}
private static final class JavaPageSkin extends ToolbarListPageSkin<JavaManagementPage> {
JavaPageSkin(JavaManagementPage skinnable) {
super(skinnable);
}
@Override
protected List<Node> initializeToolbar(JavaManagementPage skinnable) {
ArrayList<Node> res = new ArrayList<>(4);
res.add(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, JavaManager::refresh));
if (skinnable.onInstallJava != null) {
res.add(createToolbarButton2(i18n("java.download"), SVG.DOWNLOAD_OUTLINE, skinnable.onInstallJava));
}
res.add(createToolbarButton2(i18n("java.add"), SVG.PLUS, skinnable::onAddJava));
JFXButton disableJava = createToolbarButton2(i18n("java.disabled.management"), SVG.VIEW_LIST, skinnable::onShowRestoreJavaPage);
disableJava.disableProperty().bind(Bindings.isEmpty(ConfigHolder.globalConfig().getDisabledJava()));
res.add(disableJava);
return res;
}
}
}
@@ -0,0 +1,206 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXButton;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class JavaRestorePage extends ListPageBase<JavaRestorePage.DisabledJavaItem> implements DecoratorPage {
private final ObjectProperty<State> state = new SimpleObjectProperty<>(State.fromTitle(i18n("java.disabled.management")));
@SuppressWarnings("FieldCanBeLocal")
private final InvalidationListener listener;
public JavaRestorePage(ObservableSet<String> disabledJava) {
this.listener = o -> {
ArrayList<DisabledJavaItem> result = new ArrayList<>(disabledJava.size());
for (String path : disabledJava) {
Path realPath = null;
try {
realPath = Paths.get(path).toRealPath();
} catch (IOException ignored) {
}
result.add(new DisabledJavaItem(disabledJava, path, realPath));
}
result.sort((a, b) -> {
if (a.realPath == null && b.realPath != null)
return -1;
if (a.realPath != null && b.realPath == null)
return 1;
return a.path.compareTo(b.path);
});
this.setItems(FXCollections.observableList(result));
};
disabledJava.addListener(new WeakInvalidationListener(listener));
listener.invalidated(disabledJava);
}
@Override
protected Skin<?> createDefaultSkin() {
return new JavaRestorePageSkin(this);
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state;
}
static final class DisabledJavaItem extends Control {
final ObservableSet<String> disabledJava;
final String path;
final Path realPath;
DisabledJavaItem(ObservableSet<String> disabledJava, String path, Path realPath) {
this.disabledJava = disabledJava;
this.path = path;
this.realPath = realPath;
}
@Override
protected Skin<?> createDefaultSkin() {
return new DisabledJavaItemSkin(this);
}
void onReveal() {
if (realPath != null) {
Path target;
Path parent = realPath.getParent();
if (parent != null
&& parent.getParent() != null
&& parent.getFileName() != null
&& parent.getFileName().toString().equals("bin")
&& Files.exists(parent.getParent().resolve("release"))) {
target = parent.getParent();
} else {
target = realPath;
}
FXUtils.showFileInExplorer(target);
}
}
void onRestore() {
disabledJava.remove(path);
JavaManager.getAddJavaTask(realPath).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
LOG.warning("Failed to add java", exception);
Controllers.dialog(i18n("java.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
}).start();
}
void onRemove() {
disabledJava.remove(path);
}
}
private static final class DisabledJavaItemSkin extends SkinBase<DisabledJavaItem> {
DisabledJavaItemSkin(DisabledJavaItem skinnable) {
super(skinnable);
BorderPane root = new BorderPane();
Label label = new Label(skinnable.path);
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
root.setCenter(label);
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
{
JFXButton revealButton = new JFXButton();
revealButton.getStyleClass().add("toggle-icon4");
revealButton.setGraphic(FXUtils.limitingSize(SVG.FOLDER_OUTLINE.createIcon(Theme.blackFill(), 24, 24), 24, 24));
revealButton.setOnAction(e -> skinnable.onReveal());
FXUtils.installFastTooltip(revealButton, i18n("java.reveal"));
if (skinnable.realPath == null) {
revealButton.setDisable(true);
JFXButton removeButton = new JFXButton();
removeButton.getStyleClass().add("toggle-icon4");
removeButton.setGraphic(FXUtils.limitingSize(SVG.DELETE_OUTLINE.createIcon(Theme.blackFill(), 24, 24), 24, 24));
removeButton.setOnAction(e -> skinnable.onRemove());
FXUtils.installFastTooltip(removeButton, i18n("java.disabled.management.remove"));
right.getChildren().setAll(revealButton, removeButton);
} else {
JFXButton restoreButton = new JFXButton();
restoreButton.getStyleClass().add("toggle-icon4");
restoreButton.setGraphic(FXUtils.limitingSize(SVG.RESTORE.createIcon(Theme.blackFill(), 24, 24), 24, 24));
restoreButton.setOnAction(e -> skinnable.onRestore());
FXUtils.installFastTooltip(restoreButton, i18n("java.disabled.management.restore"));
right.getChildren().setAll(revealButton, restoreButton);
}
}
root.setRight(right);
root.getStyleClass().add("md-list-cell");
root.setPadding(new Insets(8));
this.getChildren().setAll(new RipplerContainer(root));
}
}
private static final class JavaRestorePageSkin extends ToolbarListPageSkin<JavaRestorePage> {
JavaRestorePageSkin(JavaRestorePage skinnable) {
super(skinnable);
}
@Override
protected List<Node> initializeToolbar(JavaRestorePage skinnable) {
return Collections.emptyList();
}
}
}
@@ -27,19 +27,22 @@ import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.PageAware;
import org.jackhuang.hmcl.ui.construct.TabControl;
import org.jackhuang.hmcl.ui.construct.TabHeader;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.versions.VersionSettingsPage;
import java.util.Locale;
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class LauncherSettingsPage extends DecoratorAnimatedPage implements DecoratorPage, PageAware {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("settings")));
private final TabHeader tab;
private final TabHeader.Tab<VersionSettingsPage> gameTab = new TabHeader.Tab<>("versionSettingsPage");
private final TabControl.Tab<JavaManagementPage> javaManagementTab = new TabControl.Tab<>("javaManagementPage");
private final TabHeader.Tab<SettingsPage> settingsTab = new TabHeader.Tab<>("settingsPage");
private final TabHeader.Tab<PersonalizationPage> personalizationTab = new TabHeader.Tab<>("personalizationPage");
private final TabHeader.Tab<DownloadSettingsPage> downloadTab = new TabHeader.Tab<>("downloadSettingsPage");
@@ -50,19 +53,20 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor
public LauncherSettingsPage() {
gameTab.setNodeSupplier(() -> new VersionSettingsPage(true));
javaManagementTab.setNodeSupplier(JavaManagementPage::new);
settingsTab.setNodeSupplier(SettingsPage::new);
personalizationTab.setNodeSupplier(PersonalizationPage::new);
downloadTab.setNodeSupplier(DownloadSettingsPage::new);
helpTab.setNodeSupplier(HelpPage::new);
feedbackTab.setNodeSupplier(FeedbackPage::new);
aboutTab.setNodeSupplier(AboutPage::new);
tab = new TabHeader(gameTab, settingsTab, personalizationTab, downloadTab, helpTab, feedbackTab, aboutTab);
tab = new TabHeader(gameTab, javaManagementTab, settingsTab, personalizationTab, downloadTab, helpTab, feedbackTab, aboutTab);
tab.select(gameTab);
gameTab.initializeIfNeeded();
gameTab.getNode().loadVersion(Profiles.getSelectedProfile(), null);
FXUtils.onChangeAndOperate(tab.getSelectionModel().selectedItemProperty(), newValue -> {
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer());
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE);
});
{
@@ -71,10 +75,15 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor
settingsItem.setTitle(i18n("settings.type.global.manage"));
settingsItem.setLeftGraphic(wrap(SVG.GAMEPAD));
settingsItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(gameTab));
runInFX(() -> FXUtils.installFastTooltip(settingsItem, i18n("settings.type.global.manage")));
settingsItem.setOnAction(e -> tab.select(gameTab));
})
.startCategory(i18n("launcher"))
.addNavigationDrawerItem(javaItem -> {
javaItem.setTitle(i18n("java.management"));
javaItem.setLeftGraphic(wrap(SVG.WRENCH_OUTLINE));
javaItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(javaManagementTab));
javaItem.setOnAction(e -> tab.select(javaManagementTab));
})
.startCategory(i18n("launcher").toUpperCase(Locale.ROOT))
.addNavigationDrawerItem(settingsItem -> {
settingsItem.setTitle(i18n("settings.launcher.general"));
settingsItem.setLeftGraphic(wrap(SVG.APPLICATION_OUTLINE));
@@ -93,7 +102,7 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor
downloadItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(downloadTab));
downloadItem.setOnAction(e -> tab.select(downloadTab));
})
.startCategory(i18n("help"))
.startCategory(i18n("help").toUpperCase(Locale.ROOT))
.addNavigationDrawerItem(helpItem -> {
helpItem.setTitle(i18n("help"));
helpItem.setLeftGraphic(wrap(SVG.HELP_CIRCLE_OUTLINE));
@@ -32,7 +32,6 @@ import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
@@ -46,6 +45,8 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.AnnouncementCard;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.PopupMenu;
@@ -65,7 +66,6 @@ import java.util.stream.IntStream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.SINE;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MainPage extends StackPane implements DecoratorPage {
@@ -83,7 +83,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
private final ObservableList<Node> versionNodes;
private Profile profile;
private VBox announcementPane;
private TransitionPane announcementPane;
private final StackPane updatePane;
private final JFXButton menuButton;
@@ -102,13 +102,23 @@ public final class MainPage extends StackPane implements DecoratorPage {
setPadding(new Insets(20));
if (Metadata.isNightly() || (Metadata.isDev() && !Objects.equals(Metadata.VERSION, config().getShownTips().get(ANNOUNCEMENT)))) {
announcementPane = new VBox(16);
AnnouncementCard announcementCard = null;
if (Metadata.isNightly()) {
announcementPane.getChildren().add(new AnnouncementCard(i18n("update.channel.nightly.title"), i18n("update.channel.nightly.hint")));
announcementCard = new AnnouncementCard(i18n("update.channel.nightly.title"), i18n("update.channel.nightly.hint"), null);
} else if (Metadata.isDev()) {
announcementPane.getChildren().add(new AnnouncementCard(i18n("update.channel.dev.title"), i18n("update.channel.dev.hint")));
announcementCard = new AnnouncementCard(i18n("update.channel.dev.title"), i18n("update.channel.dev.hint"), this::hideAnnouncementPane);
}
if (announcementCard != null) {
VBox announcementBox = new VBox(16);
announcementBox.getChildren().add(announcementCard);
announcementPane = new TransitionPane();
announcementPane.setContent(announcementBox, ContainerAnimations.NONE);
getChildren().add(announcementPane);
}
getChildren().add(announcementPane);
}
updatePane = new StackPane();
@@ -117,7 +127,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
FXUtils.setLimitWidth(updatePane, 230);
FXUtils.setLimitHeight(updatePane, 55);
StackPane.setAlignment(updatePane, Pos.TOP_RIGHT);
updatePane.setOnMouseClicked(e -> onUpgrade());
FXUtils.onClicked(updatePane, this::onUpgrade);
FXUtils.onChange(showUpdateProperty(), this::showUpdate);
{
@@ -144,7 +154,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
StackPane.setAlignment(closeUpdateButton, Pos.TOP_RIGHT);
closeUpdateButton.getStyleClass().add("toggle-icon-tiny");
StackPane.setMargin(closeUpdateButton, new Insets(5));
closeUpdateButton.setOnMouseClicked(e -> closeUpdateBubble());
closeUpdateButton.setOnAction(e -> closeUpdateBubble());
updatePane.getChildren().setAll(hBox, closeUpdateButton);
}
@@ -206,14 +216,14 @@ public final class MainPage extends StackPane implements DecoratorPage {
menuButton.setPrefWidth(230);
//menuButton.setButtonType(JFXButton.ButtonType.RAISED);
menuButton.setStyle("-fx-font-size: 15px;");
menuButton.setOnMouseClicked(e -> onMenu());
menuButton.setOnAction(e -> onMenu());
menuButton.setClip(new Rectangle(211, -100, 100, 200));
StackPane graphic = new StackPane();
Node svg = SVG.TRIANGLE.createIcon(Theme.foregroundFillBinding(), 10, 10);
StackPane.setAlignment(svg, Pos.CENTER_RIGHT);
graphic.getChildren().setAll(svg);
graphic.setTranslateX(12);
runInFX(() -> FXUtils.installFastTooltip(menuButton, i18n("version.switch")));
FXUtils.installFastTooltip(menuButton, i18n("version.switch"));
menuButton.setGraphic(graphic);
launchPane.getChildren().setAll(launchButton, separator, menuButton);
@@ -224,10 +234,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
menu.setMaxHeight(365);
menu.setMaxWidth(545);
menu.setAlwaysShowingVBar(true);
menu.setOnMouseClicked(e -> popup.hide());
FXUtils.onClicked(menu, popup::hide);
versionNodes = MappedObservableList.create(versions, version -> {
Node node = PopupMenu.wrapPopupMenuItem(new GameItem(profile, version.getId()));
node.setOnMouseClicked(e -> profile.setSelectedVersion(version.getId()));
FXUtils.onClicked(node, () -> profile.setSelectedVersion(version.getId()));
return node;
});
Bindings.bindContent(menu.getContent(), versionNodes);
@@ -237,10 +247,13 @@ public final class MainPage extends StackPane implements DecoratorPage {
doAnimation(show);
if (show && getLatestVersion() != null && !Objects.equals(config().getPromptedVersion(), getLatestVersion().getVersion())) {
Controllers.dialog("", i18n("update.bubble.title", getLatestVersion().getVersion()), MessageDialogPane.MessageType.INFO, () -> {
config().setPromptedVersion(getLatestVersion().getVersion());
onUpgrade();
});
Controllers.dialog(new MessageDialogPane.Builder("", i18n("update.bubble.title", getLatestVersion().getVersion()), MessageDialogPane.MessageType.INFO)
.addAction(i18n("button.view"), () -> {
config().setPromptedVersion(getLatestVersion().getVersion());
onUpgrade();
})
.addCancel(null)
.build());
}
}
@@ -287,10 +300,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
public void hideAnnouncementPane() {
if (announcementPane != null) {
config().getShownTips().put(ANNOUNCEMENT, Metadata.VERSION);
Pane parent = (Pane) announcementPane.getParent();
if (parent != null)
parent.getChildren().remove(announcementPane);
announcementPane = null;
announcementPane.setContent(new StackPane(), ContainerAnimations.FADE);
}
}
@@ -165,7 +165,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
downloadItem.setActionButtonVisible(false);
downloadItem.setTitle(i18n("download"));
downloadItem.setOnAction(e -> Controllers.navigate(Controllers.getDownloadPage()));
runInFX(() -> FXUtils.installFastTooltip(downloadItem, i18n("download.hint")));
FXUtils.installFastTooltip(downloadItem, i18n("download.hint"));
// fifth item in left sidebar
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
@@ -30,6 +30,7 @@ import javafx.scene.layout.*;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.scene.text.TextFlow;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -72,7 +73,7 @@ public abstract class SettingsView extends StackPane {
{
StackPane sponsorPane = new StackPane();
sponsorPane.setCursor(Cursor.HAND);
sponsorPane.setOnMouseClicked(e -> onSponsor());
FXUtils.onClicked(sponsorPane, this::onSponsor);
sponsorPane.setPadding(new Insets(8, 0, 8, 0));
GridPane gridPane = new GridPane();
@@ -120,7 +121,7 @@ public abstract class SettingsView extends StackPane {
{
btnUpdate = new JFXButton();
btnUpdate.setOnMouseClicked(e -> onUpdate());
btnUpdate.setOnAction(e -> onUpdate());
btnUpdate.getStyleClass().add("toggle-icon4");
btnUpdate.setGraphic(SVG.UPDATE.createIcon(Theme.blackFill(), 20, 20));
@@ -160,7 +161,7 @@ public abstract class SettingsView extends StackPane {
{
JFXButton cleanButton = new JFXButton(i18n("launcher.cache_directory.clean"));
cleanButton.setOnMouseClicked(e -> clearCacheDirectory());
cleanButton.setOnAction(e -> clearCacheDirectory());
cleanButton.getStyleClass().add("jfx-button-border");
fileCommonLocationSublist.setHeaderRight(cleanButton);
@@ -191,10 +192,19 @@ public abstract class SettingsView extends StackPane {
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
debugPane.setLeft(left);
JFXButton openLogFolderButton = new JFXButton(i18n("settings.launcher.launcher_log.reveal"));
openLogFolderButton.setOnAction(e -> openLogFolder());
openLogFolderButton.getStyleClass().add("jfx-button-border");
JFXButton logButton = new JFXButton(i18n("settings.launcher.launcher_log.export"));
logButton.setOnMouseClicked(e -> onExportLogs());
logButton.setOnAction(e -> onExportLogs());
logButton.getStyleClass().add("jfx-button-border");
debugPane.setRight(logButton);
HBox buttonBox = new HBox();
buttonBox.setSpacing(10);
buttonBox.getChildren().addAll(openLogFolderButton, logButton);
BorderPane.setAlignment(buttonBox, Pos.CENTER_RIGHT);
debugPane.setRight(buttonBox);
settingsPane.getContent().add(debugPane);
}
@@ -205,6 +215,10 @@ public abstract class SettingsView extends StackPane {
}
}
public void openLogFolder() {
FXUtils.openFolder(Metadata.HMCL_DIRECTORY.resolve("logs").toFile());
}
protected abstract void onUpdate();
protected abstract void onExportLogs();
@@ -22,7 +22,6 @@ import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.SkinBase;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.setting.Theme;
@@ -47,9 +46,7 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
skinnable.pseudoClassStateChanged(SELECTED, active);
});
getSkinnable().addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
getSkinnable().setSelected(true);
});
FXUtils.onClicked(getSkinnable(), () -> getSkinnable().setSelected(true));
Node left = VersionPage.wrap(SVG.FOLDER_OUTLINE);
root.setLeft(left);
@@ -64,7 +61,7 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
right.setAlignment(Pos.CENTER_RIGHT);
JFXButton btnRemove = new JFXButton();
btnRemove.setOnMouseClicked(e -> skinnable.remove());
btnRemove.setOnAction(e -> skinnable.remove());
btnRemove.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnRemove, Pos.CENTER);
btnRemove.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), 14, 14));
@@ -1,4 +1,4 @@
package moe.mickey.minecraft.skin.fx;
package org.jackhuang.hmcl.ui.skin;
import javafx.event.Event;
import javafx.event.EventHandler;
@@ -1,4 +1,4 @@
package moe.mickey.minecraft.skin.fx;
package org.jackhuang.hmcl.ui.skin;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
@@ -1,4 +1,4 @@
package moe.mickey.minecraft.skin.fx;
package org.jackhuang.hmcl.ui.skin;
import javafx.animation.AnimationTimer;
@@ -1,4 +1,4 @@
package moe.mickey.minecraft.skin.fx;
package org.jackhuang.hmcl.ui.skin;
import javafx.scene.*;
import javafx.scene.image.Image;
@@ -1,4 +1,4 @@
package moe.mickey.minecraft.skin.fx;
package org.jackhuang.hmcl.ui.skin;
import javafx.scene.shape.Mesh;
import javafx.scene.shape.MeshView;
@@ -1,4 +1,4 @@
package moe.mickey.minecraft.skin.fx;
package org.jackhuang.hmcl.ui.skin;
import javafx.scene.Group;
import javafx.scene.Node;
@@ -1,4 +1,4 @@
package moe.mickey.minecraft.skin.fx;
package org.jackhuang.hmcl.ui.skin;
import javafx.scene.image.Image;
import javafx.scene.image.PixelReader;
@@ -1,4 +1,4 @@
package moe.mickey.minecraft.skin.fx;
package org.jackhuang.hmcl.ui.skin;
import javafx.geometry.Point2D;
import javafx.scene.Group;
@@ -1,4 +1,4 @@
package moe.mickey.minecraft.skin.fx;
package org.jackhuang.hmcl.ui.skin;
import javafx.animation.Transition;
import javafx.beans.value.WritableValue;
@@ -1,10 +1,10 @@
package moe.mickey.minecraft.skin.fx.animation;
package org.jackhuang.hmcl.ui.skin.animation;
import javafx.util.Duration;
import moe.mickey.minecraft.skin.fx.FunctionHelper;
import moe.mickey.minecraft.skin.fx.SkinAnimation;
import moe.mickey.minecraft.skin.fx.SkinCanvas;
import moe.mickey.minecraft.skin.fx.SkinTransition;
import org.jackhuang.hmcl.ui.skin.FunctionHelper;
import org.jackhuang.hmcl.ui.skin.SkinAnimation;
import org.jackhuang.hmcl.ui.skin.SkinCanvas;
import org.jackhuang.hmcl.ui.skin.SkinTransition;
public final class SkinAniRunning extends SkinAnimation {
@@ -1,10 +1,10 @@
package moe.mickey.minecraft.skin.fx.animation;
package org.jackhuang.hmcl.ui.skin.animation;
import javafx.util.Duration;
import moe.mickey.minecraft.skin.fx.FunctionHelper;
import moe.mickey.minecraft.skin.fx.SkinAnimation;
import moe.mickey.minecraft.skin.fx.SkinCanvas;
import moe.mickey.minecraft.skin.fx.SkinTransition;
import org.jackhuang.hmcl.ui.skin.FunctionHelper;
import org.jackhuang.hmcl.ui.skin.SkinAnimation;
import org.jackhuang.hmcl.ui.skin.SkinCanvas;
import org.jackhuang.hmcl.ui.skin.SkinTransition;
public final class SkinAniWavingArms extends SkinAnimation {
@@ -64,7 +64,6 @@ import java.util.Locale;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
@@ -302,7 +301,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
lblGameVersion.visibleProperty().bind(hasVersion);
gameVersionField.managedProperty().bind(hasVersion);
gameVersionField.visibleProperty().bind(hasVersion);
runInFX(() -> FXUtils.installFastTooltip(gameVersionField, i18n("search.enter")));
FXUtils.installFastTooltip(gameVersionField, i18n("search.enter"));
FXUtils.onChangeAndOperate(getSkinnable().version, version -> {
if (StringUtils.isNotBlank(version.getVersion())) {
@@ -488,7 +487,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
JFXListView<RemoteMod> listView = new JFXListView<>();
spinnerPane.setContent(listView);
Bindings.bindContent(listView.getItems(), getSkinnable().items);
listView.setOnMouseClicked(e -> {
FXUtils.onClicked(listView, () -> {
if (listView.getSelectionModel().getSelectedIndex() < 0)
return;
RemoteMod selectedItem = listView.getSelectionModel().getSelectedItem();
@@ -62,7 +62,6 @@ import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class DownloadPage extends Control implements DecoratorPage {
@@ -241,14 +240,14 @@ public class DownloadPage extends Control implements DecoratorPage {
openMcmodButton.setExternalLink(getSkinnable().translations.getMcmodUrl(getSkinnable().mod));
descriptionPane.getChildren().add(openMcmodButton);
openMcmodButton.setMinWidth(Region.USE_PREF_SIZE);
runInFX(() -> FXUtils.installFastTooltip(openMcmodButton, i18n("mods.mcmod")));
FXUtils.installFastTooltip(openMcmodButton, i18n("mods.mcmod"));
}
JFXHyperlink openUrlButton = new JFXHyperlink(control.page.getLocalizedOfficialPage());
openUrlButton.setExternalLink(getSkinnable().addon.getPageUrl());
descriptionPane.getChildren().add(openUrlButton);
openUrlButton.setMinWidth(Region.USE_PREF_SIZE);
runInFX(() -> FXUtils.installFastTooltip(openUrlButton, control.page.getLocalizedOfficialPage()));
FXUtils.installFastTooltip(openUrlButton, control.page.getLocalizedOfficialPage());
}
SpinnerPane spinnerPane = new SpinnerPane();
@@ -346,7 +345,10 @@ public class DownloadPage extends Control implements DecoratorPage {
pane.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content);
RipplerContainer container = new RipplerContainer(pane);
container.setOnMouseClicked(e -> Controllers.navigate(new DownloadPage(page, addon, version, callback)));
FXUtils.onClicked(container, () -> {
fireEvent(new DialogCloseEvent());
Controllers.navigate(new DownloadPage(page, addon, version, callback));
});
getChildren().setAll(container);
if (addon != RemoteMod.BROKEN) {
@@ -381,8 +383,6 @@ public class DownloadPage extends Control implements DecoratorPage {
{
StackPane graphicPane = new StackPane();
graphicPane.getChildren().setAll(SVG.RELEASE_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24));
TwoLineListItem content = new TwoLineListItem();
HBox.setHgrow(content, Priority.ALWAYS);
content.setTitle(dataItem.getName());
@@ -390,11 +390,16 @@ public class DownloadPage extends Control implements DecoratorPage {
switch (dataItem.getVersionType()) {
case Alpha:
content.getTags().add(i18n("mods.channel.alpha"));
graphicPane.getChildren().setAll(SVG.ALPHA_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24));
break;
case Beta:
content.getTags().add(i18n("version.game.snapshot"));
content.getTags().add(i18n("mods.channel.beta"));
graphicPane.getChildren().setAll(SVG.BETA_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24));
break;
case Release:
content.getTags().add(i18n("version.game.release"));
content.getTags().add(i18n("mods.channel.release"));
graphicPane.getChildren().setAll(SVG.RELEASE_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24));
break;
}
@@ -425,7 +430,7 @@ public class DownloadPage extends Control implements DecoratorPage {
}
RipplerContainer container = new RipplerContainer(pane);
container.setOnMouseClicked(e -> Controllers.dialog(new ModVersion(dataItem, selfPage)));
FXUtils.onClicked(container, () -> Controllers.dialog(new ModVersion(dataItem, selfPage)));
getChildren().setAll(container);
// Workaround for https://github.com/HMCL-dev/HMCL/issues/2129
@@ -440,7 +445,7 @@ public class DownloadPage extends Control implements DecoratorPage {
VBox box = new VBox(8);
box.setPadding(new Insets(8));
ModItem modItem = new ModItem(version, selfPage);
modItem.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
FXUtils.onClicked(modItem, () -> fireEvent(new DialogCloseEvent()));
box.getChildren().setAll(modItem);
SpinnerPane spinnerPane = new SpinnerPane();
ScrollPane scrollPane = new ScrollPane();
@@ -457,7 +462,7 @@ public class DownloadPage extends Control implements DecoratorPage {
this.setBody(box);
JFXButton downloadButton = new JFXButton(i18n("download"));
JFXButton downloadButton = new JFXButton(i18n("mods.install"));
downloadButton.getStyleClass().add("dialog-accept");
downloadButton.setOnAction(e -> {
if (!spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) {
@@ -466,7 +471,7 @@ public class DownloadPage extends Control implements DecoratorPage {
selfPage.download(version);
});
JFXButton saveAsButton = new JFXButton(i18n("button.save_as"));
JFXButton saveAsButton = new JFXButton(i18n("mods.save_as"));
saveAsButton.getStyleClass().add("dialog-accept");
saveAsButton.setOnAction(e -> {
if (!spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) {
@@ -502,7 +507,6 @@ public class DownloadPage extends Control implements DecoratorPage {
dependencies.put(dependency.getType(), list);
}
DependencyModItem dependencyModItem = new DependencyModItem(selfPage.page, dependency.load(), selfPage.version, selfPage.callback);
dependencyModItem.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
dependencies.get(dependency.getType()).add(dependencyModItem);
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui.versions;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.setting.Profile;
@@ -34,15 +33,13 @@ import java.util.function.Consumer;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameAdvancedListItem extends AdvancedListItem {
private final Tooltip tooltip;
private final ImageView imageView;
private final WeakListenerHolder holder = new WeakListenerHolder();
private Profile profile;
@SuppressWarnings("unused")
private Consumer<Event> onVersionIconChangedListener;
public GameAdvancedListItem() {
tooltip = new Tooltip();
Pair<Node, ImageView> view = createImageView(null);
setLeftGraphic(view.getKey());
imageView = view.getValue();
@@ -63,17 +60,13 @@ public class GameAdvancedListItem extends AdvancedListItem {
}
if (version != null && Profiles.getSelectedProfile() != null &&
Profiles.getSelectedProfile().getRepository().hasVersion(version)) {
FXUtils.installFastTooltip(this, tooltip);
setTitle(version);
setSubtitle(null);
setTitle(i18n("version.manage.manage"));
setSubtitle(version);
imageView.setImage(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version));
tooltip.setText(version);
} else {
Tooltip.uninstall(this,tooltip);
setTitle(i18n("version.empty"));
setSubtitle(i18n("version.empty.add"));
imageView.setImage(VersionIconType.DEFAULT.getIcon());
tooltip.setText("");
}
}
}
@@ -35,7 +35,6 @@ import org.jackhuang.hmcl.ui.construct.PopupMenu;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.Lazy;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameListItemSkin extends SkinBase<GameListItem> {
@@ -79,33 +78,33 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
right.setAlignment(Pos.CENTER_RIGHT);
if (skinnable.canUpdate()) {
JFXButton btnUpgrade = new JFXButton();
btnUpgrade.setOnMouseClicked(e -> skinnable.update());
btnUpgrade.setOnAction(e -> skinnable.update());
btnUpgrade.getStyleClass().add("toggle-icon4");
btnUpgrade.setGraphic(FXUtils.limitingSize(SVG.UPDATE.createIcon(Theme.blackFill(), 24, 24), 24, 24));
runInFX(() -> FXUtils.installFastTooltip(btnUpgrade, i18n("version.update")));
FXUtils.installFastTooltip(btnUpgrade, i18n("version.update"));
right.getChildren().add(btnUpgrade);
}
{
JFXButton btnLaunch = new JFXButton();
btnLaunch.setOnMouseClicked(e -> skinnable.launch());
btnLaunch.setOnAction(e -> skinnable.launch());
btnLaunch.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnLaunch, Pos.CENTER);
btnLaunch.setGraphic(FXUtils.limitingSize(SVG.ROCKET_LAUNCH_OUTLINE.createIcon(Theme.blackFill(), 24, 24), 24, 24));
runInFX(() -> FXUtils.installFastTooltip(btnLaunch, i18n("version.launch.test")));
FXUtils.installFastTooltip(btnLaunch, i18n("version.launch.test"));
right.getChildren().add(btnLaunch);
}
{
JFXButton btnManage = new JFXButton();
btnManage.setOnMouseClicked(e -> {
btnManage.setOnAction(e -> {
currentSkinnable = skinnable;
popup.get().show(root, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight());
});
btnManage.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnManage, Pos.CENTER);
btnManage.setGraphic(FXUtils.limitingSize(SVG.DOTS_VERTICAL.createIcon(Theme.blackFill(), 24, 24), 24, 24));
runInFX(() -> FXUtils.installFastTooltip(btnManage, i18n("settings.game.management")));
FXUtils.installFastTooltip(btnManage, i18n("settings.game.management"));
right.getChildren().add(btnManage);
}
@@ -91,13 +91,11 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
installNewGameItem.setTitle(i18n("install.new_game"));
installNewGameItem.setLeftGraphic(VersionPage.wrap(SVG.PLUS_CIRCLE_OUTLINE));
installNewGameItem.setOnAction(e -> Versions.addNewGame());
runInFX(() -> FXUtils.installFastTooltip(installNewGameItem, i18n("install.new_game")));
})
.addNavigationDrawerItem(installModpackItem -> {
installModpackItem.setTitle(i18n("install.modpack"));
installModpackItem.setLeftGraphic(VersionPage.wrap(SVG.PACK));
installModpackItem.setOnAction(e -> Versions.importModpack());
runInFX(() -> FXUtils.installFastTooltip(installModpackItem, i18n("install.modpack")));
})
.addNavigationDrawerItem(refreshItem -> {
refreshItem.setTitle(i18n("button.refresh"));
@@ -108,7 +106,6 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
globalManageItem.setTitle(i18n("settings.type.global.manage"));
globalManageItem.setLeftGraphic(VersionPage.wrap(SVG.GEAR_OUTLINE));
globalManageItem.setOnAction(e -> modifyGlobalGameSettings());
runInFX(() -> FXUtils.installFastTooltip(globalManageItem, i18n("settings.type.global.manage")));
});
FXUtils.setLimitHeight(bottomLeftCornerList, 40 * 4 + 12 * 2);
setLeft(pane, bottomLeftCornerList);
@@ -79,6 +79,12 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
// Conventional libraries: game, fabric, forge, neoforge, liteloader, optifine
for (InstallerItem item : group.getLibraries()) {
String libraryId = item.getLibraryId();
// Skip fabric-api and quilt-api
if (libraryId.contains("fabric-api") || libraryId.contains("quilt-api")) {
continue;
}
String libraryVersion = analyzer.getVersion(libraryId).orElse(null);
if (libraryVersion != null) {
@@ -91,11 +97,11 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
item.versionProperty().set(null);
}
item.installActionProperty().set(e -> {
item.setOnInstall(() -> {
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, libraryId, libraryVersion));
});
item.removeActionProperty().set(e -> profile.getDependency().removeLibraryAsync(version, libraryId)
item.setOnRemove(() -> profile.getDependency().removeLibraryAsync(version, libraryId)
.thenComposeAsync(profile.getRepository()::saveAsync)
.withComposeAsync(profile.getRepository().refreshVersionsAsync())
.withRunAsync(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId))
@@ -115,7 +121,7 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
InstallerItem installerItem = new InstallerItem(libraryId, InstallerItem.Style.LIST_ITEM);
installerItem.versionProperty().set(new InstallerItem.InstalledState(libraryVersion, false, false));
installerItem.removeActionProperty().set(e -> profile.getDependency().removeLibraryAsync(version, libraryId)
installerItem.setOnRemove(() -> profile.getDependency().removeLibraryAsync(version, libraryId)
.thenComposeAsync(profile.getRepository()::saveAsync)
.withComposeAsync(profile.getRepository().refreshVersionsAsync())
.withRunAsync(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId))
@@ -192,7 +192,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
})
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception != null || result == null) {
Controllers.dialog("Failed to check updates", "failed", MessageDialogPane.MessageType.ERROR);
Controllers.dialog(i18n("mods.check_updates.failed_check"), i18n("message.failed"), MessageDialogPane.MessageType.ERROR);
} else if (result.isEmpty()) {
Controllers.dialog(i18n("mods.check_updates.empty"));
} else {
Loaded 100 of 237 files, more files were not shown because too many files have changed in this diff. Show more