Compare commits

..
761 Commits
Author SHA1 Message Date
Glavo 0d9e2d92e0 [release/3.9] 修复 CurseForge 源无法搜索到内容的问题 (#5232)
https://github.com/HMCL-dev/HMCL/pull/5220
2026-01-14 20:49:09 +08:00
Glavoand辞庐 9f754e78e8 [release/3.9] Fix #5121 : NeoForge alpha 版本未正确归类的问题 (#5231)
https://github.com/HMCL-dev/HMCL/pull/5125

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2026-01-14 20:48:26 +08:00
GlavoandWulian233 cecd28791b [release/3.9] Fix #5126: BMCLAPI 选择 Liteloader 不支持的版本时报错 (#5230)
https://github.com/HMCL-dev/HMCL/pull/5128

Co-authored-by: Wulian233 <1055917385@qq.com>
2026-01-14 20:47:09 +08:00
Glavoand辞庐 5e42652e75 [release/3.9] 更新 NeoForge 版本号规则 (#5229)
https://github.com/HMCL-dev/HMCL/pull/5110

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2026-01-14 20:46:32 +08:00
Glavo 9f682e0c39 [release/3.9] 优化 JFXListView 的平滑滚动效果 (#5228)
https://github.com/HMCL-dev/HMCL/pull/5052
2026-01-14 20:36:57 +08:00
Glavo eac12efafb [release/3.9] 禁用 LogWindow 的平滑滚动效果 (#5227)
https://github.com/HMCL-dev/HMCL/pull/5046
2026-01-14 20:36:11 +08:00
Glavo d3d46f6a29 Fix #5039: 修复陶瓦联机页面部分文本在深色模式下颜色异常的问题 (#5040) 2025-12-21 15:13:58 +08:00
Glavo 6f1db67e9a 优化反馈页面 (#5008)
1. 将主页左下角“官方群组”按钮调整为跳转到“反馈”页面;
2. 将反馈页面分为“官方群组”和“提交反馈”两部分
2025-12-20 20:35:41 +08:00
mineDiamond 0d7ece819d feat: 添加快速进入世界功能 (#4872)
close #2563
2025-12-20 20:33:52 +08:00
Damon Lu 7f852080cd 在 macOS 上添加启动脚本的后缀 .command (#4927) 2025-12-20 20:32:34 +08:00
neveler e0360c2b38 使用 subtitle 显示预览更新提示 (#5019) 2025-12-20 16:04:20 +08:00
Glavo 3bf0eb2a16 Fix #5014: 修复 GameCrashWindow 中 feedbackTextFlow 样式错误的问题 (#5020) 2025-12-20 16:00:42 +08:00
Glavo 84b428b57c 在 JFXCustomColorPickerDialog 中使用 HEX 作为默认 Tab (#5011) 2025-12-20 15:33:03 +08:00
neveler 41895e97e1 修复 subtitle 颜色丢失的问题 (#5018)
fix #5017
2025-12-20 15:32:34 +08:00
zimzaza4 f839dc192c 修改AccountAdvancedListItem 控件, 在 RootPage 中添加快速选择账户 (#4932) 2025-12-19 22:18:10 +08:00
Glavo 24250a6239 更新 CNB Issue 模板 (#5010) 2025-12-19 22:03:43 +08:00
Glavo a3ab2b52b6 添加 CNB Issue 模板 (#5009) 2025-12-19 21:39:00 +08:00
Glavo c4d4c6fcf3 优化镜像仓库同步功能 (#5006) 2025-12-19 16:47:12 +08:00
Glavo f77f005f88 mirror.yml 中直接传递用户名 (#5005) 2025-12-19 16:10:02 +08:00
Glavo 9ec494664c 简化镜像仓库同步 workflow (#5004) 2025-12-19 16:02:02 +08:00
Glavo dc7c883962 在低于 JavaFX 23 的情况下隐藏主题模式跟随系统选项 (#4985) 2025-12-18 21:25:07 +08:00
Glavo 567bd15997 在陶瓦联机界面启动游戏时禁用离线皮肤功能 (#4916) 2025-12-18 21:18:03 +08:00
Glavo 25c9ef0f27 修复无法解析类似 "26.1 Snapshot 1" 的版本号的问题 (#5002) 2025-12-18 21:09:07 +08:00
Glavo d45d994307 将 Release 同步至 CNB (#5000) 2025-12-18 21:08:30 +08:00
Glavo 01b7a6b95a Revert "修复 TransitionPane 动画被打断时节点状态异常的问题" (#4986)
Reverts HMCL-dev/HMCL#4957
2025-12-18 20:47:48 +08:00
神奇的赵二 (OrzMiku) a0593c03c6 支持下载 Java25 (#4990) 2025-12-18 20:46:52 +08:00
Glavo 91f05315aa Add CNB badge (#4999) 2025-12-18 20:11:06 +08:00
Burning_TNT 84b8fff37b Bump authlib-injector to 1.2.7: fix compatibility with 26.1-snapshot-1 (#4992)
Fix #4989
2025-12-17 17:05:07 -05:00
Glavo 3a9cc2b9bb 修复 CNB 同步功能 (#4995) 2025-12-18 05:57:14 +08:00
Glavo 056f30f6f5 将代码同步至 CNB (#4994) 2025-12-18 05:47:29 +08:00
辞庐 2d6e01fcf9 添加输出游戏调试日志选项 (#4144) 2025-12-14 20:49:23 +08:00
Calboot b6d95c3ebb [Enhancement] 给 TableView 添加样式 (#4854) 2025-12-12 22:00:55 +08:00
Glavo 29e40ec1a5 在 DownloadListPage 中添加图标缓存 (#4866) 2025-12-12 21:12:51 +08:00
Glavo 497789603a 在日志中记录显示器信息 (#4581) 2025-12-11 20:47:10 +08:00
Glavo 8d3cb66d17 将 TableView 中的 CheckBox 替换为 JFXCheckBox (#4964) 2025-12-11 20:41:26 +08:00
mineDiamond 0613b342f0 feat: 添加1.21.11_unobfuscated支持 (#4963) 2025-12-11 20:29:55 +08:00
Glavo 18000fae30 修复 TransitionPane 动画被打断时节点状态异常的问题 (#4957) 2025-12-09 15:32:17 +08:00
Glavo d2c985491a 合并 SettingsPage 与 SettingsView (#4955) 2025-12-08 16:45:29 +08:00
Glavo f7f0d55504 更新 RipplerContainer 的涟漪颜色 (#4953) 2025-12-08 15:17:10 +08:00
Glavo 210e127ac7 Fix #4950: 修复部分按钮波纹颜色错误的问题 (#4952) 2025-12-07 21:46:42 +08:00
Glavo 36748a3a07 Fix #4941: 更新 JFXComboBox 下拉菜单滚动条样式 (#4951) 2025-12-07 21:43:47 +08:00
Burning_TNT cf667b2b8d Fix #4759: 部分 LF/NF 模组无法正确识别 (#4945) 2025-12-07 21:13:49 +08:00
Burning_TNT a3f7a230e7 Fix #4597: 绕过 OptiFine HD U G6 pre1 与其他模组 Fingerprint 冲突的问题 (#4946) 2025-12-07 21:12:08 +08:00
辞庐 9dd8084be2 修复本地安装新版本 Forge 时未移除版本号中 forge- 前缀的问题 (#4935) 2025-12-07 21:04:18 +08:00
Glavo 9cf5a5e5db 修复从 BMCLAPI 下载部分旧文件时校验失败的问题 (#4949) 2025-12-07 20:45:02 +08:00
Glavo 8efa54dd11 更新超链接被聚焦时的边框样式 (#4948) 2025-12-07 20:44:32 +08:00
Wulian233 7902020300 Fix #4939: 修复深色模式超链接的已访问样式配色问题 (#4943) 2025-12-07 16:35:46 +08:00
Glavo e72c0bbb50 修复部分控件 Tooltip 文字颜色异常的问题 (#4947) 2025-12-07 16:15:22 +08:00
ef3df92592 feat: 资源包管理 (#4475)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-12-06 22:17:04 +08:00
Glavo 04b300f453 简化 MessageDialogPane (#4918) 2025-12-06 21:18:58 +08:00
mineDiamondandGlavo e455ce4c84 feat: 支持1.21.11-pre4/5/rc1/2_unobfuscated (#4911)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-12-06 21:03:27 +08:00
Glavo 456d13959e 更新 GameVersionNumber 解析规则 (#4917)
1. 支持 [Minecraft
新版本号方案](https://www.minecraft.net/en-us/article/minecraft-new-version-numbering-system)
2. 支持对版本号进行归一化处理。
2025-12-06 20:55:11 +08:00
Glavo 7ac22ad567 Windows 11 上日志窗口标题栏颜色应当跟随主题模式设置 (#4910) 2025-12-04 01:24:36 +08:00
neveler ed4fec6c42 在 HintPane 提示框中为图标和文本添加间距 (#4915) 2025-12-03 16:45:44 +08:00
Glavo c321453399 Fix #4900: 修复 ManagedProcess#getLines 完成后不会解除锁定的问题 (#4913) 2025-12-03 16:19:17 +08:00
Calboot aae02bef50 [Bugfix] 修复模组更新列表界面按 esc 无法正确退出的问题 (#4895)
Fixes #4894
2025-12-03 16:17:46 +08:00
Glavo 89c9a93aab 优化深色模式下日志窗口筛选按钮的颜色 (#4912) 2025-12-03 16:04:10 +08:00
neveler 74ec4aaae8 优化检查更新工作流 (#4879) 2025-12-02 21:15:43 +08:00
Glavo 669a9dfc57 优化更新弹窗的颜色 (#4903) 2025-12-02 21:14:10 +08:00
Glavo a1e75e48da 修复部分控件在深色模式下颜色异常的问题 (#4899) 2025-12-02 20:29:46 +08:00
Glavo 75292d2a52 在陶瓦联机页面向非中国内地用户添加提示框 (#4869) 2025-11-30 21:47:26 +08:00
mineDiamond 6b6dee6cfa feat: 添加对 1.21.11-pre3_unobfuscated 的支持 (#4870) 2025-11-30 21:41:36 +08:00
Calboot 6558b3b2f9 [Bugfix] #4863: macOS 上错误的 Modrinth 整合包默认文件名称 (#4864)
Fixes #4863
2025-11-30 21:27:59 +08:00
Glavo 68fc2d7cd9 修复部分控件颜色错误的问题 (#4887) 2025-11-30 21:23:39 +08:00
Glavo cda45db222 优化对 Linux MIPS64el 平台的支持 (#4886) 2025-11-30 21:23:23 +08:00
3gf8jv4dv 7325b36e8c fix(i18n): improve mod update dialog text (#4788)
fix #4773
2025-11-30 16:13:19 +08:00
Glavo 6fa6f23ad4 在 Manifest 文件中添加 Enable-Final-Field-Mutation 条目 (#4561) 2025-11-30 15:40:06 +08:00
Glavo 8351ee4094 重构下载源选择逻辑 (#4856) 2025-11-30 15:27:33 +08:00
Glavo 310a344f96 更新至 Material Design 3 颜色系统 (#4835) 2025-11-30 15:25:27 +08:00
Glavo 3b78ec9664 修复 JFXRadioButton 在 Windows 平台上中心圆点偏移的问题 (#4874) 2025-11-28 21:09:58 +08:00
Glavo cfcbe6e962 修复修改主题色后 JFXCheckBox 颜色不会及时变化的问题 (#4871) 2025-11-27 21:25:27 +08:00
Glavo fe809ad397 Fix #4860: 播放动画时将 ListPageBase 缓存为位图 (#4862) 2025-11-26 15:53:39 +08:00
Glavo 78a78f4fe7 使用 JFXColorPicker 代替 ColorPicker (#4865) 2025-11-25 21:39:39 +08:00
Glavo 6ca6429114 修复惯性滚动可能导致弹出菜单错位的问题 (#4857) 2025-11-25 15:55:12 +08:00
Glavo 4a3d330f12 Bump JUnit to 6.0.1 (#4858) 2025-11-25 15:36:31 +08:00
Glavo ac71e66fe1 修复 FetchTask 未设置 User-Agent 的问题 (#4850) 2025-11-24 20:33:24 +08:00
Glavo 3c5e4f15d9 支持自动切换游戏语言至阿拉伯语 (#4853) 2025-11-23 21:51:30 +08:00
Hommam-Prom 1c37fbc97a Arabic translation file (#4837) 2025-11-23 21:36:33 +08:00
Glavo 8823ae8ee4 将阿拉伯语列为实验性支持语言 (#4851) 2025-11-23 21:33:01 +08:00
ZZZank a7b952fc4d add @PropertyKey to i18n methods for easier translation<->key navigation (#4831) 2025-11-23 20:32:32 +08:00
Glavo 63ceb3c6c8 Bump version root to 3.9 (#4849) 2025-11-23 15:44:54 +08:00
Glavo ab6e8692ad 修复缓存异常时 FetchTask 无限循环的问题 (#4844) 2025-11-23 15:20:14 +08:00
辞庐 97ada51489 更新 amdgpu.ids (#4845) 2025-11-23 15:15:37 +08:00
Burning_TNT 8e9c2f27a1 Bump Terracotta to 0.3.14 (#4838)
* 更新 MacOS 元数据以增强本地化支持 by @burningtnt in
https://github.com/burningtnt/Terracotta/pull/69
* 重构 EasyTier 参数拼接逻辑 by @burningtnt in
https://github.com/burningtnt/Terracotta/pull/81
* 为 ET 添加 tcp://0.0.0.0:0 监听器 by @burningtnt in
https://github.com/burningtnt/Terracotta/pull/90
* 强制 ET 以 p2p_only 模式运行 by @burningtnt in
https://github.com/burningtnt/Terracotta/pull/97
* 支持联机房间码持久化 by @burningtnt in
https://github.com/burningtnt/Terracotta/pull/94
* 修复 'API c:player_profiles_list invocation failed: Host Profile is
consumed or invalid, machine_id may have conflict.' 的问题 by @burningtnt
in https://github.com/burningtnt/Terracotta/pull/98
2025-11-22 15:28:36 +08:00
mineDiamondandGlavo 8563db3654 添加对 1.21.11-pre1/2_unobfuscated 的支持 (#4829)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-11-21 22:06:51 +08:00
Glavo 2695b5c42f 使用 EdgeOne 分发 HMCL 元数据 (#4830) 2025-11-21 22:02:04 +08:00
Glavo bed87425e7 从首页打开子页面时应当打开默认标签页 (#4826) 2025-11-20 17:40:51 +08:00
Glavo d6ad1cbdf7 修复 TabHeader 相关动画问题 (#4822) 2025-11-19 15:32:18 +08:00
Glavo da5bbd2b22 播放过渡动画时缓存节点至位图 (#4815) 2025-11-18 16:35:02 +08:00
Glavo 5c726913e4 更新陶瓦联机用户须知 (#4816) 2025-11-18 16:27:00 +08:00
Glavo 02bd9e9d2d 使用低版本 Java 启动且带有 --apply-to 参数时提醒用户手动下载最新版本 HMCL (#4793) 2025-11-17 20:23:40 +08:00
辞庐 458793f0c0 fix: 修复 Windows 下全局设置中本地库路径路径分隔符显示错误的问题 (#4808) 2025-11-17 16:37:34 +08:00
Haowei Wen 0543da31cc Fix #4802: 修复复制实例时使用非法实例 ID 启动器行为异常的问题 (#4805) 2025-11-17 16:27:04 +08:00
Glavo a74dc98ce6 修复下载安装整合包时启动器崩溃的问题 (#4813) 2025-11-17 15:43:20 +08:00
Glavo 9dfc6155c4 在 JFXListView 上启用平滑滚动 (#4809) 2025-11-17 15:42:55 +08:00
Glavo e4f10d7cd8 添加更多动画效果 (#4803) 2025-11-16 21:26:38 +08:00
Glavo a21a8d5045 Bump JNA to 5.18.1 (#4799) 2025-11-15 23:22:02 +08:00
Glavo 59994bdeed Bump jsoup to 1.21.2 (#4798) 2025-11-15 23:09:33 +08:00
Glavo a95e4c65ee Bump Gradle to 9.2.0 (#4796) 2025-11-15 23:05:56 +08:00
Glavo df7783c568 修复进入世界管理页面时动画错误的问题 (#4797) 2025-11-15 23:00:09 +08:00
Glavo e5df2034fd 修复初次打开部分页面时页面闪烁的问题 (#4795) 2025-11-15 22:48:30 +08:00
mineDiamond f13d53d6da feat: 增强对数据包/资源包mcmeta的解析能力 (#4612)
close #3952
2025-11-15 21:25:48 +08:00
Glavo 75c4fb6550 优化更多界面的动画 (#4790) 2025-11-15 21:22:18 +08:00
Glavo 44869da20e 更新界面动画效果 (#4780) 2025-11-14 17:42:28 +08:00
Glavo 756dce0d6f 添加对 25w46a_unobfuscated 的支持 (#4782) 2025-11-14 17:42:20 +08:00
mineDiamond 4a4fa07db4 fix: 修复模组列表点击搜索按钮无法正确聚焦到搜索栏的问题 (#4783) 2025-11-13 21:44:00 +08:00
mineDiamondand3gf8jv4dv 17bc003698 feat: 优化数据包管理功能 (#4672)
close #4661

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-11-13 16:43:17 +08:00
Damon Lu 80b2242e4d 在 macOS 下禁用最小化动画 (#4762) 2025-11-13 16:32:31 +08:00
Glavo 47ccda048c 优化游戏实例默认图标 (#4776) 2025-11-13 15:40:36 +08:00
Glavo ae144aa7b8 预加载页面以减少动画卡顿 (#4779) 2025-11-12 21:21:02 +08:00
Glavo 343c09b6ff 修复无法获取部分远古版本版本号的问题 (#4775) 2025-11-11 20:58:56 +08:00
Glavo 1bbc6ef777 更新模组数据 (2025-11-07) (#4770) 2025-11-09 20:57:10 +08:00
辞庐 f4c6a4f891 优化 Github Actions 构建速度 (#4745) 2025-11-08 21:54:15 +08:00
Glavo 733b2c90c4 Fix #4538: 删除实例失败时弹出对话框 (#4540) 2025-11-08 21:52:01 +08:00
Glavo 81b61523df 使 NetworkUtils.doGet 支持更多 URI scheme (#4768) 2025-11-08 21:51:28 +08:00
Glavo d93f4d6bbb 添加对 25w45a_unobfuscated 的支持 (#4767) 2025-11-08 21:50:16 +08:00
neveler f0a16b0cd9 使用矩阵简化更新工作流 (#4725) 2025-11-08 21:20:01 +08:00
Glavo 783cadcf77 从注册表中读取 Windows 版本信息 (#4760) 2025-11-08 20:13:38 +08:00
Glavo cbb4b0c51b 创建 ObservableSetting 辅助类 (#4719) 2025-11-02 21:33:54 +08:00
Glavo c4d9803177 更新 PLATFORM.md (#4750) 2025-11-02 20:52:47 +08:00
Glavo 2ce4fcabcb Revert #4453: 恢复对 JavaFX 14 的兼容性 (#4749) 2025-11-02 20:52:12 +08:00
3a30e5b048 修复 Terracotta 在 macOS 上的兼容性问题 (#4723)
Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-02 20:29:41 +08:00
Glavo 49b0a99ced 优化检查更新日志的方式 (#4743) 2025-11-02 20:27:25 +08:00
Wulian233 8f2c860242 更新 Chunkbase 种子版本 (#4742) 2025-11-01 22:32:50 +08:00
Glavo 8c10775de9 在 PLATFORM.md 中记录启动器对平台的支持状态 (#4737) 2025-11-01 16:46:38 +08:00
3gf8jv4dv b5bf7ce4d0 fix(i18n): update i18n for open dir button on instance mods tab (#4734) 2025-11-01 16:46:13 +08:00
Glavo bd6a6fa9b6 在不支持的平台上打开陶瓦联机页面时弹出提示 (#4735) 2025-10-31 21:07:25 +08:00
Glavo 1c0ae3918d 修复 Platform.FREEBSD_X86_64 架构错误的问题 (#4736) 2025-10-31 21:05:37 +08:00
Glavo c6ce2df731 将官方群组入口重新移动到主界面 (#4727) 2025-10-28 20:05:59 +08:00
Burning_TNT 18985c0b63 Bump Terracotta to 0.3.12 (#4724) 2025-10-27 22:09:28 +08:00
Minecraft-温迪andGlavo 322e29642d 只在未被打包为 App 时设置 macOS Dock 栏图标 (#4698)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-10-27 21:45:13 +08:00
Glavo a86e5d7bf5 Update PLATFORM.md (#4717) 2025-10-27 21:23:29 +08:00
Glavo c90eaa2937 Fix #4720: 修复 JFXListViewSkin 创建大量平台线程的问题 (#4721) 2025-10-27 21:20:32 +08:00
Glavo defacb3554 限制 ./gradlew run 运行时的默认内存占用 (#4722) 2025-10-27 20:51:51 +08:00
Glavo 0a66863272 重构 TerracottaMetadata (#4718) 2025-10-26 20:21:29 +08:00
Glavo d90df221bc 添加详细构建指南 (#4524) 2025-10-26 15:53:15 +08:00
Glavo fd7f909561 将 Localization.md 翻译至英语 (#4669) 2025-10-24 11:55:14 +08:00
neveler a921a11be6 Add tooltip to external link button (#4696) 2025-10-22 21:53:17 +08:00
Glavoand3gf8jv4dv 7fbc99f3eb 优化没有可启动的游戏时的提示 (#4697)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-10-22 20:45:56 +08:00
3gf8jv4dv 7e40c799e5 style(l10n): simplify update button text in mod management header (#4712) 2025-10-22 20:40:59 +08:00
Glavo f2032ab7a8 使用 Translator 格式化下载速度 (#4709) 2025-10-22 20:35:42 +08:00
GlavoandCopilot d7c6a23dbe 通过解析 IANA 语言子标签注册表增强本地化功能 (#4675)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-21 15:37:32 +08:00
Glavo 27e1e021d7 优化更新日志展示功能 (#4703)
1. 如果检测到是降级更新,则不展示更新日志;
2. 如果检测到最新更新日志和最新版本不匹配,则不显示更新日志;
3. 展示当前版本与 `nowchange`/`nowpreview` 之间的所有更新日志,而不只展示一个版本的日志。
2025-10-20 20:21:13 +08:00
Glavo 2516fb5d69 在陶瓦联机界面的启动游戏按钮中显示实例名称 (#4705) 2025-10-20 20:08:31 +08:00
Burning_TNTandGlavo d66c080dea Feature: Download Terracotta binary from Gitee, CNB, alist (sponsored by 8mi) first in China mainland. (#4700)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-10-19 21:03:32 +08:00
Glavo 94ca81035b 优化陶瓦联机的复制邀请码功能 (#4687) 2025-10-18 22:18:14 +08:00
Glavo 1ab414e702 优化 Task 的资源占用 (#4694) 2025-10-18 21:50:23 +08:00
Glavo 16f6e89e45 删除 CompletableFutureTask.resolveException (#4690) 2025-10-18 21:47:39 +08:00
Glavo 22f6bfd845 删除 VersionPage.Skin 中未使用的 BorderPane (#4695) 2025-10-18 21:21:41 +08:00
Glavo c4823b3e55 优化陶瓦联机用户须知对话框 (#4686) 2025-10-18 20:25:00 +08:00
Glavo e34ec9f801 调整陶瓦联机界面左侧按钮 (#4688) 2025-10-18 20:20:17 +08:00
Burning_TNT d2e87ba024 Bump Terracotta to 0.3.10. (#4682) 2025-10-17 20:59:10 +08:00
Glavo ffb50423af Bump version root to 3.8 (#4676) 2025-10-16 20:43:16 +08:00
Glavo 69f19115ea Bump HMCLauncher to 3.7.0.1 (#4681) 2025-10-16 15:29:50 +08:00
Glavo ecb4eedd12 指定使用 UTF-8 编码编译 buildSrc 子项目 (#4677) 2025-10-15 21:11:00 +08:00
Glavo df6f35bc8e Fix #4670: 修复不在场景中的按钮尝试取消自身焦点时触发 NPE 的问题 (#4674) 2025-10-15 15:05:21 +08:00
Glavo 58d3780553 Fix #4663: 排序模组时应忽略大小写 (#4665) 2025-10-13 15:13:46 +08:00
Glavo 58b357d4b8 修复模组下载页面图标模糊的问题 (#4660) 2025-10-12 21:14:18 +08:00
辞庐andGlavo d69d27af57 修复导出游戏崩溃信息后弹窗没有正常弹出的问题 (#4624)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-10-12 21:08:42 +08:00
Glavo f9a39f1c90 优化对无法识别的模组的提示 (#4659) 2025-10-12 20:57:47 +08:00
3gf8jv4dv f8a4261480 chore(l10n): add preview update option translation to spanish (#4599) 2025-10-12 20:54:53 +08:00
Glavo 2c428faa95 修复无法识别部分旧版本 Forge 模组的问题 (#4658) 2025-10-12 20:42:55 +08:00
Glavo da91fe15df 更新陶瓦联机页面文本 (#4607) 2025-10-12 20:05:14 +08:00
辞庐 1df962884c Fix #4613 修复本地安装升级 Cleanroom 相关问题 (#4625) 2025-10-11 21:37:33 +08:00
Glavo 8ad5d83946 修复无法识别部分 Forge 模组信息的问题 (#4654) 2025-10-11 21:27:48 +08:00
Glavo 74878bda0c 对于安装了信雅互联的实例抑制对 Fabric 模组的警告 (#4653) 2025-10-11 20:57:53 +08:00
Glavo b32d3304b0 支持读取 META-INF/jarjar/metadata.json (#4652) 2025-10-11 20:45:27 +08:00
Glavo 60375c9632 Add net.neoforged.fml.startup.Client to FORGE_OPTIFINE_MAIN (#4649) 2025-10-11 20:06:14 +08:00
Glavo 33877691f8 使用 Gradle Task 检查 I18N 中文件的全角括号 (#4650) 2025-10-10 22:02:23 +08:00
3gf8jv4dv 109d6874ab Fix #4645: mc 1.21.9+ instances with neoforge do not show their icons (#4646) 2025-10-10 21:43:26 +08:00
Burning_TNT a225e7fac4 更改陶瓦联机反馈表入口展示方式 (#4623) 2025-10-10 21:43:08 +08:00
Glavo 7dad43570e Fix #4647: 修复总是将同时兼容 Forge 和 NeoForge 的模组识别为 Forge 模组的问题 (#4648) 2025-10-10 21:35:11 +08:00
Glavo 8a166266d8 添加 SupportedLocale#getTextDirection() (#4644) 2025-10-10 21:34:14 +08:00
Glavo c53fbd0efb Close #4640: 支持读取 Embedded-Dependencies-Mod (#4641) 2025-10-10 20:32:34 +08:00
Glavo 1e74567a22 在游戏下载界面提示版本支持状态 (#4471) 2025-10-10 16:45:18 +08:00
3gf8jv4dv 0b9616f460 chore(docs&l10n): adjust preview update option text (#4598) 2025-10-10 15:55:54 +08:00
3gf8jv4dv 0b89612ec5 chore(l10n): fix loader mismatch warning on chinese (traditional) (#4642) 2025-10-10 15:54:22 +08:00
3gf8jv4dv 642893d2e9 chore(docs&l10n): highlight support for cleanroom! (#4615) 2025-10-10 15:53:21 +08:00
Glavo 2e878e43cb 修复模组列表页面加载模组图标相关问题 (#4638)
1. 修复高分屏上模组图标模糊的问题;
2. 修复快速滚动时可能会显示其他模组的图标的问题;
3. 修复快速滚动时可能重复加载图标的问题。
2025-10-09 21:33:35 +08:00
Glavo c8edf4be62 优化模组中文译名展示方式 (#4636)
1. 隐藏不包含中文字符的译名
2. 过滤译名中的 Emoji
2025-10-09 16:34:33 +08:00
Glavo 96f16382c0 将模组中文译名中的小括号统一为半角括号 (#4635) 2025-10-09 15:49:27 +08:00
Glavo 11c45e0ab8 优化模组管理页面模组信息的展示方式 (#4621) 2025-10-09 15:21:28 +08:00
Glavo 08f6f973c0 修复颠倒英语中括号显示错误的问题 (#4631) 2025-10-09 15:19:12 +08:00
Glavo 35e63fc8c8 重构本地化支持 (#4632) 2025-10-09 15:18:45 +08:00
Zkitefly 23037b8afc 使用 subname 匹配模组翻译 (#3231) 2025-10-08 22:36:19 +08:00
Glavo 25323dc385 清理 ModTranslations (#4634) 2025-10-08 21:09:19 +08:00
Glavo 496b403eeb 使用 checktyle 检查对 toLowerCase()/toUpperCase() 的误用 (#4633) 2025-10-08 16:27:57 +08:00
Burning_TNT 7be00f2408 修复 Terracotta 和部分 MacOS 版本的兼容性 (#4619) 2025-10-07 23:07:53 +08:00
Glavo 08d7974e27 Update versions.txt to 1.21.10 (#4626) 2025-10-07 22:50:53 +08:00
辞庐 f16cbed5b4 Bump authlib-injector to 1.2.6 (#4617) 2025-10-07 22:46:03 +08:00
Glavo 0988dc53cd 优化模组加载器检测机制 (#4622) 2025-10-07 16:26:26 +08:00
Wulian233 f1c8791961 优化模组信息对话框 UI 宽高 (#4616) 2025-10-06 16:20:06 +08:00
Glavo 07e5ac9e0e 添加 ReleaseSchedule.md (#4606) 2025-10-05 20:36:17 +08:00
Burning_TNT c98dbe6694 Feature: Display feedback form when updating Terracotta. (#4611) 2025-10-05 20:35:49 +08:00
Burning_TNT 460134cc1e 隐藏空的 Terracotta 玩家列表 (#4609) 2025-10-05 20:12:51 +08:00
Burning_TNT 9dd117c454 Bump Terracotta to 0.3.10 (#4608)
https://github.com/burningtnt/Terracotta/compare/v0.3.9-rc.10...v0.3.10
2025-10-04 23:24:43 +08:00
Wulian233 c60af8675f 修复部分按钮点击后始终保持焦点状态的问题 (#4585) 2025-10-04 21:16:35 +08:00
Burning_TNT 411efbe774 在全局设置中储存是否曾向用户展示过陶瓦联机用户须知 (#4601) 2025-10-04 21:15:22 +08:00
Glavo 2e09a40b69 修复 FetchTask 不遵循代理设置的问题 (#4596) 2025-10-04 21:11:44 +08:00
Glavo ba47c2d10d Update badges (#4595) 2025-10-03 21:46:09 +08:00
Glavo fd90ca8afe Add README_en_Qabs.md (#4594) 2025-10-03 21:41:18 +08:00
ZkiteflyandGlavo dda30274a4 优化模组管理布局 (#3229)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-10-03 21:20:52 +08:00
Glavo 2b43ee24a1 Fix Debug_zh.md (#4593) 2025-10-03 21:02:49 +08:00
3gf8jv4dv 66f1e60433 fix(i18n): correct the language name in literary chinese (#4592)
Fix #4591
2025-10-03 20:02:14 +08:00
Burning_TNTand3gf8jv4dv 3c3e2b8cfc Terracotta | 陶瓦联机 (#4215)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-10-03 20:01:08 +08:00
Burning_TNTandGlavo 365bb089c5 Fix #4572: Use String::lines to handle line separators on various OS (#4575)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-10-03 19:59:43 +08:00
Glavo 87af44328b 添加“提前预览 HMCL 版本”选项 (#4223) 2025-10-03 19:56:59 +08:00
Glavo 8731faaac7 更新 Jenkins CI 链接 (#4584) 2025-10-02 21:57:44 +08:00
Glavo 5abc1f1f9d 修复“不自动添加 Java 虚拟机优化参数”选项未保存至配置文件的问题 (#4583) 2025-10-02 20:47:34 +08:00
Glavo 40b4ecd42a 从 synchronized 切换至 ReentrantLock (#4564) 2025-10-02 15:50:56 +08:00
Glavo e0f5c4d91e 优化 EventBus (#4565) 2025-10-02 15:38:03 +08:00
辞庐 35ecd58460 Fix #4556 : 修复在短时间内连续点击 OptionToggleButton 只会更改一次状态的问题 (#4568) 2025-10-02 15:31:20 +08:00
辞庐 f29c2bc202 Fix 4555: 修复外观 - 不透明度滑块 刻度线在不同滑块位置时排布不统一的问题 (#4571) 2025-10-02 15:25:52 +08:00
Glavo 361e928e73 支持从 WorkflowMultiBranchProject 中检查最新版本 (#4576) 2025-10-02 15:22:15 +08:00
Glavo 484b65b513 优化对颠倒的英语的支持 (#4577) 2025-10-02 15:21:03 +08:00
3gf8jv4dv 19f02e3967 chore: update versions.txt to 1.21.9 (#4566) 2025-10-01 13:08:43 +08:00
Glavo 495da6b6b0 更新 Jenkins CI 服务器的发布模型 (#4559) 2025-10-01 13:04:19 +08:00
Glavo 168c4ac708 支持通过环境变量控制 UI 缩放比例 (#4521) 2025-09-29 16:51:57 +08:00
Glavo 7e8a6679cc 将 EventManager 中的 synchronized 替换为 ReentrantLock (#4562) 2025-09-29 15:49:18 +08:00
Glavo e90d5185f6 增强 ArchiveFileTree (#4547) 2025-09-29 15:19:37 +08:00
Glavo 8a89baee09 使用 Gradle 获取更新信息 (#4560) 2025-09-28 20:29:07 +08:00
辞庐 7f4c5fc4a0 清理代码 (#4519) 2025-09-28 00:01:00 +08:00
Glavo 75789c774d 修复切换下载源后需要重启启动器才会生效的问题 (#4296) 2025-09-27 16:26:38 +08:00
Glavo f0a16b3d14 GitHub Action 按文件类型分别上传构建产物 (#4537) 2025-09-27 15:56:28 +08:00
Glavo 710e69751f 重构 FetchTask 以使用 HttpClient 和虚拟线程 (#4546) 2025-09-26 20:54:22 +08:00
Glavo 938d8647ce 删除 InvocationDispatcher 中的 synchronized 块 (#4550) 2025-09-26 20:44:34 +08:00
Glavo 4b4dd592c3 将 Schedulers.io() 切换至虚拟线程 (#4548) 2025-09-26 20:12:01 +08:00
Glavo 225dba7448 Bump JNA to 5.18.0 (#4549) 2025-09-26 17:06:01 +08:00
Glavo 5b1414428e 添加 Debug_zh.md (#4542) 2025-09-24 15:22:50 +08:00
Glavo cb467ae240 优化本地化支持 (#4539)
- 简化代码
- 修复错误用词
- 优化语言名称显示
2025-09-23 15:47:46 +08:00
rootwhois 9416c0cfb3 修复日志页面选中条目时 hover 背景闪烁两次的问题 (#4517)
fix #4245
2025-09-23 11:46:18 +08:00
3gf8jv4dv 16ea98c95a chore(l10n): revise chinese (traditional) and english (#4530) 2025-09-23 11:30:54 +08:00
3gf8jv4dv a8552a92d1 chore(l10n): add new translations to spanish (#4531) 2025-09-23 11:30:03 +08:00
Glavo 36d71bd14e 支持颠倒的英语 (#4527) 2025-09-22 22:07:06 +08:00
3gf8jv4dv cbe6554390 feat(l10n): add boot_es.properties (#4532) 2025-09-22 21:48:09 +08:00
Glavo 1b65e0b6d7 Fix #4526: 修复将空字符串注册为 Access Token 后日志输出异常的问题 (#4528) 2025-09-22 16:07:16 +08:00
Glavo bd9ae189f8 优化本地化支持 (#4525) 2025-09-21 15:15:22 +08:00
Glavo 867a04dfba 支持在“实例管理”按钮上通过滚动滚轮切换游戏实例 (#4509) 2025-09-20 20:15:11 +08:00
Wulian233 dd7703c7a9 修复删除世界时无法删除 zip 格式世界的问题 (#4500) 2025-09-20 17:14:19 +08:00
Glavo 756b95ca8c 添加本地化说明文档 (#4516) 2025-09-20 16:41:58 +08:00
rootwhoisandGlavo ac0a0c7604 修复 AdvancedListBox 在非必要的情况下显示滚动条的问题 (#4514)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-09-20 16:28:59 +08:00
Glavo 9ee611c0f6 Bump Gradle to 9.1.0 (#4520) 2025-09-20 15:55:20 +08:00
Glavo c38371f454 优化 Java 自动选择规则 (#4515) 2025-09-19 21:35:44 +08:00
Glavo eb3be8c465 在未启用 JIT 或硬件渲染加速的环境中默认禁用动画 (#4510) 2025-09-19 16:01:39 +08:00
Glavo e6ac07f62b 修复使用 JavaFX 25 时游戏下载界面布局错误的问题 (#4508) 2025-09-18 21:11:20 +08:00
Glavo a24fea4a95 支持禁止生成 JVM 优化参数 (#4507) 2025-09-18 20:04:25 +08:00
Glavo 8bd104c207 使用 Java 25 启动游戏时默认开启 Compact Object Headers (#4506) 2025-09-18 17:24:59 +08:00
Glavo a14a265986 Bump JavaFX to 21.0.8/25 (#4454) 2025-09-18 17:09:48 +08:00
Glavo 66524613b4 支持以类型安全的方式读写 Navigation#getSettings() (#4504) 2025-09-18 16:58:07 +08:00
Glavo a0568e34a8 清理残留的 java.io.File 用例 (#4503) 2025-09-18 15:26:03 +08:00
Glavo 45f7719096 Close #2987: 从 java.io.File 迁移至 NIO (#4502) 2025-09-17 21:41:17 +08:00
Glavo 77fadf5d28 将 org.jackhuang.hmcl.ui 从 java.io.File 迁移至 NIO (#4501) 2025-09-17 21:14:16 +08:00
Glavo e9d7d0a33c 将 org.jackhuang.hmcl.mod 从 java.io.File 迁移至 NIO (#4499) 2025-09-17 19:29:59 +08:00
Glavo 94994ffdb6 将 org.jackhuang.hmcl.setting 从 java.io.File 迁移至 NIO (#4498) 2025-09-17 18:39:34 +08:00
Glavo 26e274a89a 将 GameRepository 从 java.io.File 迁移至 NIO (#4496)
https://github.com/HMCL-dev/HMCL/issues/2987
2025-09-17 16:07:59 +08:00
Glavo 6a497df0d1 清理 org.jackhuang.hmcl.download.game (#4495)
1. 将 `java.io.File` 迁移至 NIO;
2. 将部分类迁移至 record。
2025-09-16 21:22:02 +08:00
Glavo fc1cce5e5a 删除 FileUtils 中基于 java.io.File 的工具方法 (#4493) 2025-09-16 20:52:44 +08:00
Glavo b328ed2cc9 Fix #4481: 修复 .hmcl 目录为跨驱动器符号链接时无法卸载 Java 的问题 (#4492) 2025-09-16 19:46:59 +08:00
Glavo 5a8d567bd7 将 isNameValid 方法移动至 FileUtils (#4491) 2025-09-16 16:11:38 +08:00
Glavo 5005343d00 缓存文件时遵循 Cache-Control 设置 (#4462) 2025-09-16 15:15:30 +08:00
Glavo e4bc8f48c5 Bump Gson to 2.13.2 (#4490) 2025-09-16 14:55:59 +08:00
ZkiteflyandGlavo 831a8a9bb8 fix(fmllib): Forge 1.5.2 安装时下载 fmllib,修复无法启动的问题 (#3976)
Fix #3975

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-09-16 14:38:44 +08:00
7c024473d6 世界管理新增删除世界功能 (#4263)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-09-15 23:06:30 +08:00
Glavo f594abc37d 更新 HMCLBoot 以使用地区中立的文件命名 (#4488) 2025-09-15 21:28:35 +08:00
Glavo b66e249591 在未启用 JIT 或硬件渲染加速的环境中启动时弹出提示 (#4487) 2025-09-15 21:26:47 +08:00
Glavo 15e490f313 Convert Pair to record (#4485) 2025-09-15 15:31:17 +08:00
Glavo 7ce59cac2c 清理 org.jackhuang.hmcl.util.logging (#4484) 2025-09-15 15:21:14 +08:00
Glavo a7178802f8 Create FileUtils#deleteDirectory(Path) (#4482) 2025-09-14 21:08:05 +08:00
Glavo 51954163d4 创建 RawPreservingProperty 接口 (#4464) 2025-09-14 21:06:32 +08:00
Wulian233 19072bd617 修复download.hint翻译条目未添加光影的说明的问题 (#4474) 2025-09-14 20:54:28 +08:00
Burning_TNT a39a23f938 Fix: 无法安装带有 Fabric 的非标准 MMC 整合包 (#4034)
- 修复无法安装带有 Fabric 的非标准 MMC 整合包的漏洞
- 在安装时写入当前安装器实现信息和启动器信息,以在后续调试时获得更多信息

Fix #4049
2025-09-14 20:50:53 +08:00
Glavo a10e9a04b1 创建 OSVersion 工具类 (#4480) 2025-09-14 20:13:43 +08:00
Wulian233 c2d5a07053 修复数据包管理在低于1.13的版本意外出现的问题 (#4428) 2025-09-13 20:33:22 +08:00
Glavo e7526e39bf 将 IconedTwoLineListItem#tags 的类型修改为 ObservableList<Label> (#4473) 2025-09-13 20:17:05 +08:00
Glavo 811b1fb5f4 创建 JsonSerializable 接口 (#4472) 2025-09-13 19:53:56 +08:00
Glavo 72ae46c93d 优化 FXUtils#openLink (#4470) 2025-09-13 14:37:53 +08:00
Glavo 6b6276e6e2 在 Label 文本溢出时安装 Tooltip (#4467) 2025-09-13 14:30:27 +08:00
Glavo 60ff34a587 修复滚动切换游戏实例/账户功能 (#4466) 2025-09-13 14:16:24 +08:00
Glavo 356960cc82 新增辅助方法 GameVersionNumber#isAtLeast(String, String) (#4458) 2025-09-12 22:12:39 +08:00
Glavo 8708e7f8b1 Fix #4463: 修复启用代理身份认证且未设置密码时启动器崩溃的问题 (#4465) 2025-09-12 21:31:59 +08:00
Glavo 962ee0d108 使用 java.util.HexFormat 替代 Hex (#4457) 2025-09-12 19:07:43 +08:00
Glavo cf5957ffd1 删除 AwtUtils (#4460) 2025-09-12 18:47:03 +08:00
Glavo f41b008b42 不再使用 OperatingSystemMXBean 中被弃用的方法 (#4459) 2025-09-12 18:46:08 +08:00
ENC_Euphony 228a1e4887 修复 popup 第一次无法计算高度的问题 & 应用此方法到世界管理界面 (#4412)
Close #4287
2025-09-12 00:01:22 +08:00
Glavo 913ce55d43 简化调用 JavaFX 12+ API 的代码 (#4456) 2025-09-11 19:25:49 +08:00
Glavo 9ded2e489d 优化对于 ISO 639-3 语言代码的支持 (#4455) 2025-09-11 19:13:17 +08:00
Glavo c65e4d83a2 简化 StyleSheets (#4453)
现在我们总是使用 JavaFX 17+,所以不再需要为不支持 `data` URI 的 JavaFX 提供 workaround。
2025-09-11 16:01:59 +08:00
Glavo 3184f91227 使用 Java 17 构建 HMCL (#4452) 2025-09-11 15:51:04 +08:00
Glavo 8eb17b6664 检测到配置文件由高版本启动器创建时禁止保存配置文件 (#4414) 2025-09-11 00:51:41 +08:00
Glavo 36ea1dac94 更新模组和整合包数据 (#4439) 2025-09-10 23:16:20 +08:00
Glavo 3fbe199aab Fix #4435: 修复 backgroundType 为 null 时启动器崩溃的问题 (#4437) 2025-09-10 22:50:39 +08:00
Glavo 8db8ade9c7 文言时间使用古代时辰格式 (#4413) 2025-09-09 20:23:13 +08:00
Glavo 41e004f4d5 删除不必要的 System.gc() 调用 (#4416) 2025-09-09 20:21:47 +08:00
Glavo 37ad445620 序列化不透明的 Color 时省略 Alpha 通道的值 (#4415) 2025-09-09 20:21:18 +08:00
Glavo 480f8b6890 使用宏自动从英文文档复制部分内容 (#4406) 2025-09-08 20:40:23 +08:00
Glavo 24fd2cf521 Fix #4409: 修复导出的历史日志文件为空的问题 (#4411) 2025-09-07 22:31:44 +08:00
Glavo 68dbe6945b 优化语言设置中简体/繁体中文选项的显示方式 (#4410) 2025-09-07 21:06:26 +08:00
Glavo c3352d2b73 优化游戏下载界面 (#4405) 2025-09-07 16:33:49 +08:00
小宇 55a7169e21 Fix MultiMC modpack update not applying configuration overrides (#4382) 2025-09-07 15:53:47 +08:00
Glavo 50080ab875 修复地支中的「丑」被误写为「醜」的问题 (#4404) 2025-09-07 15:51:21 +08:00
GlavoandCopilot 5697cbd251 使用 Gradle 自动更新文档内容 (#4403)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-07 15:47:17 +08:00
Glavo 86ad055f8f 修复 Config 的序列化问题 (#4402) 2025-09-06 17:46:43 +08:00
+1 4883c3897b 文言翻译优化 (#4387)
Co-authored-by: 0xis-cn <mat-ling@139.com>
Co-authored-by: BeiDou114514 <201355137+BeiDou114514@users.noreply.github.com>
Co-authored-by: Furina-de-zh-Hant <229449402+Furina-de-zh-Hant@users.noreply.github.com>
Co-authored-by: Kaohaaa <178006033+Kaohaaa@users.noreply.github.com>
Co-authored-by: 365KW <132805104+365KW@users.noreply.github.com>
Co-authored-by: Andypsl8 <115411501+Andypsl8@users.noreply.github.com>
Co-authored-by: Au0727 <125126631+Au0727@users.noreply.github.com>
Co-authored-by: threwd <88961950+threwd@users.noreply.github.com>
Co-authored-by: vinds476283 <222576327+vinds476283@users.noreply.github.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-09-06 17:38:11 +08:00
Glavo 09c17fc85b 优化 checkTranslations Task (#4396) 2025-09-06 15:41:38 +08:00
Glavo fe59f495f6 优化地区判断逻辑 (#4401) 2025-09-06 15:40:37 +08:00
Glavo 9a475c2e92 更新 PLATFORM.md (#4399) 2025-09-06 02:13:24 +08:00
Glavo 8cd8a389b9 将 README.md 移动至 docs 文件夹中 (#4398) 2025-09-06 02:06:30 +08:00
Glavo 9cc45bc66a 将 README.md 翻译至更多语言 (#4397) 2025-09-06 01:41:36 +08:00
Glavo ea0b65be2e 在 README 中声明对 FreeBSD 的支持 (#4395) 2025-09-05 21:13:38 +08:00
GlavoandCopilot f4a8e3d1a6 重构 Config (#4394)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-05 20:24:01 +08:00
Glavo 7576bf6a01 在 LocaleUtils 中添加更多工具方法 (#4390) 2025-09-04 15:51:17 +08:00
3gf8jv4dv 3d63523f25 chore(l10n): change translation from 版本 to 实例 in zh-cn for term INSTANCE (#4376) 2025-09-04 15:35:31 +08:00
Glavo dbfd6c7599 更新文档结构 (#4389) 2025-09-04 10:59:59 +08:00
Glavo ed6c7387bc Update microsoft_auth.html (#4388) 2025-09-03 23:48:52 +08:00
Glavo 6ab216dcb6 增强本地化支持 (#4379) 2025-09-03 21:55:20 +08:00
GlavoandCopilot b38076f847 Fix #4377: 修复离线账户换肤功能使用无效 CSL API 导致启动失败的问题 (#4386)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-03 20:16:10 +08:00
Glavo 16816d8490 修复 Linux 平台上无法正确为安装了 Cleanroom 的游戏版本选择 Java 的问题 (#4385) 2025-09-03 16:56:47 +08:00
e8813fe153 feat: Cleanroom 自动安装 (#4272)
Co-authored-by: Zkitefly <64117916+zkitefly@users.noreply.github.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-09-03 15:50:57 +08:00
Glavo ce24e4e622 修复连续点击两次返回按钮时会最大化窗口的问题 (#4384) 2025-09-03 15:34:59 +08:00
Glavo 10e5e7bc9c 优化 Minecraft Wiki 跳转功能 (#4372) 2025-09-01 23:13:20 +08:00
Glavo 28b222e3dd 优化中文适配 (#4370) 2025-09-01 21:01:59 +08:00
Glavo 86085031f6 优化自动切换游戏语言功能 (#4353) 2025-08-31 14:57:23 +08:00
Glavo 30f47704f1 更新文言文翻译 (#4364) 2025-08-31 12:18:34 +08:00
Glavo 82e0cd1ca1 Fix #3669: 修复首次打开子页面时切换动画丢失的问题 (#4365) 2025-08-31 12:11:13 +08:00
Wulian233andGlavo 5374127241 修改部分文言条目 (#4359)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-31 00:59:14 +08:00
Glavo df489c0cfe 添加乌克兰语支持 (#4363) 2025-08-31 00:48:22 +08:00
BANERandGlavo d83f6106f5 Add I18N_uk.properties (#4346)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-31 00:33:16 +08:00
Glavo b7361c8da4 优化语言设置 (#4362) 2025-08-31 00:18:25 +08:00
Glavo 73531dbf60 优化文言文翻译 (#4361) 2025-08-30 22:21:45 +08:00
Andypsl8 5af6858d4c 更新文言文翻译 (#4356) 2025-08-30 02:06:33 +08:00
Glavo 1a3b473356 Fix #3076: 修复无法识别部分简体中文环境的问题 (#4350) 2025-08-29 20:31:05 +08:00
Glavo 67e4dddda1 添加文言文支持 (#4348) 2025-08-29 14:42:10 +08:00
Glavo 55816032b7 启动游戏时隐藏 GameAssetDownloadTask (#4349) 2025-08-29 12:25:49 +08:00
Glavo b9fd0e50a7 为迁移至 Java 17 做准备 (#4311) 2025-08-28 20:43:03 +08:00
Glavo 71d54955ad 修复 TaskListPane.StatusChangeListener 持有 ListCell 的强引用的问题 (#4347) 2025-08-28 20:21:55 +08:00
Burning_TNTandGlavo d34dc82db6 Feature: Decompress legacy HMCL log files while exporting. (#4342)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-27 21:44:33 +08:00
Glavo 73214ad599 不对回环地址应用代理 (#4338) 2025-08-27 21:43:54 +08:00
Wulian233 1ddb73595d 修复世界管理页面不支持显示极限模式的问题 (#4321) 2025-08-27 21:41:52 +08:00
Glavo f9ee9c8592 更严格地检测文件名合法性 (#4341) 2025-08-27 20:10:52 +08:00
Burning_TNT 0fdfc945c3 Fix: IJ support (#4343) 2025-08-26 22:43:26 +08:00
YuChuXi 97219c951e 游戏崩溃窗口中增加对拍照的提醒 (#4294) 2025-08-26 21:54:42 +08:00
Glavo 64cc64095d 增加 TaskListPane 中任务条目之间的间距 (#4340) 2025-08-26 21:51:59 +08:00
辞庐 91a613d966 HintPane 使用新版 Bootstrap 调色盘 (#4323) 2025-08-24 22:30:14 +08:00
Glavo 06b7b1e574 Fix #4325: 修复未将 15w14a 识别为愚人节版本的问题 (#4327) 2025-08-24 22:27:49 +08:00
Glavo 933b7cab2f 将 URI 中的错误代理对编码为 U+FFFD (#4329) 2025-08-24 22:27:18 +08:00
Glavo 885e25d07a 更新文档 (#4328) 2025-08-24 20:33:13 +08:00
Glavo 421f9a3b2b 支持以字符串形式序列化 VersionSetting 中的枚举成员 (#4299) 2025-08-21 21:56:49 +08:00
Glavo e52cfb036e 优化 Task.getCaller() (#4308) 2025-08-21 20:53:19 +08:00
3gf8jv4dv 87a3a4ece3 chore(locale): add new translations for russian and spanish (#4304) 2025-08-21 20:45:40 +08:00
3gf8jv4dv b152e2bfab fix(locale): add missing i18n when installing an instance with quilt api (#4303) 2025-08-21 20:45:08 +08:00
Glavo c9c97a45b7 修复 TaskListPane 中任务总数显示错误的问题 (#4310) 2025-08-21 20:43:19 +08:00
Glavo cba4ba2799 删除无用的 debug 日志 (#4309) 2025-08-21 20:42:51 +08:00
3gf8jv4dv ab9eea3620 fix(locale): revise mcmod-info-mirror description on thanks list (#4305) 2025-08-21 20:42:22 +08:00
Glavoand3gf8jv4dv 96f2d011ca 优化 TaskListPane 的显示效果 (#4306)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-08-21 16:45:07 +08:00
Glavo 9ee55cd17d 修复 ModrinthModpackExportTask 名称未被翻译的问题 (#4301) 2025-08-20 21:47:56 +08:00
Glavo 07ee7867da 修复 VersionIconType 成员的排列顺序 (#4298) 2025-08-20 21:20:31 +08:00
Glavo 940d4f722c 修复 TaskListPane 可能出现横向滚动条的问题 (#4297) 2025-08-20 20:34:12 +08:00
Glavo af56096162 优化对于 IntelliJ IDEA 的支持 (#4270) 2025-08-20 20:33:03 +08:00
Glavo 0e12c1a132 Fix #4290: 在 macOS 上禁止最大化和全屏模式 (#4293) 2025-08-20 15:40:48 +08:00
Glavo ca20509cb1 优化 TaskListPane (#4292) 2025-08-20 00:32:40 +08:00
HenryxjhandGlavo 58c93ff349 修复 Windows 平台 GPU 信息可能显示为 "[B@xxx" 的问题 (#4288)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-19 21:33:10 +08:00
Glavo aab3b22290 修复 TaskListPane 未正确处理 StagesHintTask 的问题 (#4285) 2025-08-19 16:22:51 +08:00
Wulian233 6cf933741f 基于实际屏幕分辨率添加游戏窗口分辨率选项 (#4275) 2025-08-18 20:05:34 +08:00
Glavo 9d17834227 Fix #4281: 支持解析 Indev 版本号 (#4282) 2025-08-18 20:04:22 +08:00
Glavo 7805508d4c Fix #2676: 优化版本号排序规则 (#4276) 2025-08-18 16:24:03 +08:00
Wulian233 72f6c2faf3 修复 1.21.5 以上的地图跳转种子地图错误 (#4274) 2025-08-18 16:21:07 +08:00
Glavo 7035767828 修复 pending 类型的游戏版本被错误识别为愚人节版的问题 (#4273) 2025-08-16 16:08:58 +08:00
Wulian233andGlavo 13a2777c87 为 NeoForge 下载添加正式版/快照分类 (#4268)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-16 15:58:31 +08:00
Glavo a4f9438d9e 移除 Pack200 支持 (#4267) 2025-08-16 15:45:12 +08:00
Glavo 4445021d66 Fix #4061: 在 Windows 7/8/8.1 上使用 llvmpipe-loader 加载 mesa (#4224) 2025-08-14 23:07:23 +08:00
Glavo f6a3b76a0b 暂时禁用 MCIM 加速 (#4265) 2025-08-14 20:20:46 +08:00
Glavo 42bb602ff5 优化游戏下载页 (#4264) 2025-08-14 20:14:40 +08:00
Glavo 4547448718 优化游戏窗口大小选择功能 (#4234) 2025-08-14 16:32:14 +08:00
Glavo 10bda7e6f4 转义 URI 中的中括号和大括号 (#4261) 2025-08-14 16:16:43 +08:00
Glavo f239140055 Update amdgpu.ids (#4262) 2025-08-14 15:54:30 +08:00
Glavo 6f4b557f63 支持最大化和全屏模式 (#4260) 2025-08-14 15:10:02 +08:00
Glavo 5118ce4e62 Fix #4256: 修复游戏下载页面可能崩溃的问题 (#4257) 2025-08-13 16:14:26 +08:00
ZkiteflyandGlavo a705b7cf95 Fix 3955: 启动时不应补全服务端自动更新整合包中已禁用的 mod (#3956)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-13 16:03:53 +08:00
GlavoandCopilot 0d5d1ae4e6 将 authlib-injector 嵌入启动器 (#4246)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-13 15:37:09 +08:00
Glavo b57547415b 在未安装游戏版本时支持一键下载启动游戏 (#4248) 2025-08-13 15:36:53 +08:00
Wulian233andGlavo b60d889bf7 为愚人节分类添加图标 (#4249)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-13 15:17:47 +08:00
Glavo 2f463c1eda 在 ./.hmcl/java 中搜索 Java (#4179) 2025-08-11 20:59:30 +08:00
Wulian233 d1e006ed29 修复版本列表排序错误的问题 (#4244) 2025-08-11 20:57:05 +08:00
GlavoandCopilot ba58905c9c 优化游戏下载页版本分类功能 (#4141)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-11 16:52:03 +08:00
Glavo de221b9dae 修复非 Windows 平台上游戏进程的 user.home 可能被设置为 null 的问题 (#4241) 2025-08-11 16:08:27 +08:00
Glavo 0913c5e710 支持通过 MCIM 加速模组搜索 (#4225) 2025-08-11 16:06:59 +08:00
ZkiteflyandGlavo 41fd38d8d1 导出日志时附带最近日志 (#4051)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-10 21:55:01 +08:00
ENC_EuphonyandGlavo 7ec880852f 修复部分版本 wiki 链接不正确的问题 (#4239)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-10 21:41:44 +08:00
Glavo 9213b7fb2e Fix 4232: 修复初始化 CacheRepository 导致启动器崩溃的问题 (#4235) 2025-08-10 21:41:10 +08:00
Glavo 80f1367d9a Fix #4233: 修复 RemoteMod::getIntegrityCheck 未选择正确的算法的问题 (#4238) 2025-08-10 15:39:13 +08:00
Glavo f521a041a9 Bump Shadow Plugin to 9.0.1 (#4220) 2025-08-10 15:23:44 +08:00
be9efbe6ee 修复版本 Wiki 映射 (#4211)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-08-10 15:06:31 +08:00
辞庐and3gf8jv4dv 8f3c716ca7 Fix #4127: 修改整合包下载页面描述 (#4200)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-08-10 15:05:57 +08:00
Glavo 3cdddfa86d 在实例名称中包含非字母、数字、减号和下划线字符时弹出警告 (#4222) 2025-08-09 21:29:41 +08:00
Glavo b1d8edc72a FetchTask 启用透明 GZip 压缩 (#4219) 2025-08-08 16:24:48 +08:00
Glavo bbc546e2a0 创建 ByteArray 辅助类 (#4214) 2025-08-07 16:14:10 +08:00
Liu Wenyuan f621776cdd 修复 APNG Truecolour16Alpha 处理器 clone 方法返回类型错误的问题 (#4210) 2025-08-07 15:04:57 +08:00
ENC_Euphony 7ba9d9eadd 智能选择 popup 弹出位置 (#4203) 2025-08-07 15:01:18 +08:00
ZkiteflyandGlavo ac4c3a7501 模组管理支持搜索根据更多数据搜索内容 (#3230)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-06 21:02:36 +08:00
Glavo f95ebe2224 优化 GetTask 中 ByteArrayOutputStream 的初始大小 (#4209) 2025-08-06 20:43:51 +08:00
辞庐and3gf8jv4dv 7ee2c51b81 将 Apple M1 平台 改为 Apple Silicon 平台 (#4199)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-08-06 20:40:24 +08:00
Glavo 192c90816b 修复 #3716 与 main 的冲突 (#4208) 2025-08-06 20:24:46 +08:00
1a772e8c65 添加 Modrinth 整合包导出 (#3716)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-06 20:11:39 +08:00
3gf8jv4dv 95bed4712c chore(locale): port and fix translation in native directory path preview (#4197) 2025-08-06 16:51:29 +08:00
Glavo 2c6ceb3cb0 优化 data URI 支持 (#4206) 2025-08-06 16:48:37 +08:00
Glavo 3118c87c65 实验性支持 APNG 图片 (#4205) 2025-08-06 16:10:45 +08:00
ENC_Euphony f945a85441 Fix: 修复一些 typo (#4202) 2025-08-05 22:51:06 +08:00
辞庐 72ffbb4176 为弹窗文本添加平滑滚动 (#4201) 2025-08-05 22:50:47 +08:00
辞庐 77dc5dbe06 修复整合包格式不匹配提示未被正确格式化的问题 (#4176) 2025-08-04 20:32:42 +08:00
Glavo 0a1d6be7b7 优化 ModTranslations::loadFromResource (#4178) 2025-08-04 20:31:29 +08:00
Wulian233 bb3d03fc72 修复一些代码中的 typo (#4190) 2025-08-04 20:30:36 +08:00
Glavo a0ddaf6766 GetTask 应当遵循 Content-Type 中的字符集设置 (#4183) 2025-08-04 20:28:46 +08:00
辞庐andGlavo 0bd2adc3c6 Fix #4187: 修复 LWJGL 路径显示错误的问题 (#4192)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-04 20:24:19 +08:00
Glavo 77c3ef6ced 修复未正确处理 EnumBackgroundImage.TRANSLUCENT 的问题 (#4195) 2025-08-04 20:18:29 +08:00
3gf8jv4dv a66ddb9a20 chore(locale): port character length limit translation to es and ru (#4191) 2025-08-04 20:17:23 +08:00
3gf8jv4dv eef365b37e chore(locale): port background option translation to es and ru (#4194) 2025-08-04 17:10:19 +08:00
3gf8jv4dv 5cab603b6e chore(locale): port font anti-aliasing option translation to es and ru (#4193) 2025-08-04 17:09:54 +08:00
神奇的赵二 (OrzMiku) 6c5d07adad fix(#4166): 新增内存显示条禁用样式 (#4185) 2025-08-04 17:08:19 +08:00
3gf8jv4dv 1935a7021d fix(locale): corrent some typos in zh-tw (#4189) 2025-08-04 16:46:11 +08:00
3gf8jv4dv 31c46c963d fix(locale): corrent residual jvm terms in chinese (#4188) 2025-08-04 16:09:21 +08:00
吞云(TuYw)andGlavo 4bc6ffa0f0 支持修改主页背景的不透明度 (#3205)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-04 16:05:47 +08:00
Glavo b62659de62 支持纯色背景 (#4184) 2025-08-04 15:17:46 +08:00
Glavo 9a27c27b9a 支持使用 data URI 作为背景图片源 (#4182) 2025-08-03 20:37:02 +08:00
Glavo 74c647cc97 统一转义 URI 中的特殊字符 (#4181) 2025-08-03 19:34:34 +08:00
Glavo e2df8e25b1 修复无法找到不包含 release 文件的 Java 的问题 (#4180) 2025-08-03 19:20:11 +08:00
Glavo 9969dc60c5 优化 ArchiveFileTree (#4177) 2025-08-02 19:33:44 +08:00
辞庐andGlavo 6230b436ad Fix #4158 : 缺少 Microsoft OAuth App ID 时禁用微软账户的刷新选项 (#4160)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-08-02 18:15:03 +08:00
Glavo 3873459aaa 通过 x-bmclapi-hash 缓存资源 (#4169) 2025-08-02 16:04:26 +08:00
3gf8jv4dv 4ee3857427 fix(locale): add missing modrinth category i18n keys (#4149) 2025-08-02 15:43:12 +08:00
Glavo 2617dd3630 修复无法加载 WebP 图标的问题 (#4171) 2025-08-02 15:37:48 +08:00
辞庐 e0425102c8 fix: 无法从 BMCLAPI 下载 NeoForge 所有所需文件 (#4174) 2025-08-02 15:33:42 +08:00
辞庐 a470475c21 模组下载列表翻页后自动回到顶部 (#4170) 2025-08-02 15:22:47 +08:00
Glavo bcd73e6d15 修复下载缓存不生效的问题 (#4167) 2025-08-01 20:53:32 +08:00
Glavo 7d6f21dc3a Bump Gradle to 9.0.0 (#4168) 2025-08-01 19:26:11 +08:00
GlavoandCopilot 3eddfa23b7 优化 JsonUtils (#4161)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-31 20:41:27 +08:00
Glavo d5fa232704 忽略 %ProgramFiles%\Common Files\Oracle\Java 中的 Java (#4142) 2025-07-31 20:17:27 +08:00
Glavo c84afb3155 通过主页右下角弹出菜单切换版本后隐藏菜单 (#4163) 2025-07-31 19:41:43 +08:00
Glavo 401fef5774 创建 checkstyle task (#4162) 2025-07-31 19:41:02 +08:00
Glavo 9298fe48e1 支持通过环境变量强制使用 GPU 渲染和控制动画帧率 (#4164) 2025-07-31 19:39:45 +08:00
Glavo 7f51e0395c 遵循系统动画开关设置 (#4148) 2025-07-31 15:37:40 +08:00
辞庐 4f410fb8ad 修复“下载线程数”的指示器被提示覆盖的问题 (#4153) 2025-07-31 15:05:23 +08:00
Glavo 9bcf95287a Fix #4146: 修复导入异常格式的数据包时启动器崩溃的问题 (#4152) 2025-07-31 15:04:56 +08:00
3gf8jv4dv 5703377626 fix(locale): restore accidentally removed translations (#4155) 2025-07-31 15:04:41 +08:00
Glavo 3adb3a67e9 将代码中的 URL 替换为 URI (#4131) 2025-07-30 18:19:46 +08:00
辞庐 7918b333e6 README 添加 CurseForge API Key 覆写参数 (#4147) 2025-07-30 15:34:29 +08:00
Glavo 329b0e8f3a 在 Windows 平台优先使用注册表探测 GPU (#4145) 2025-07-30 15:32:42 +08:00
辞庐 3b17e0a897 优化主页公告关闭按钮 (#4064) 2025-07-29 23:13:06 +08:00
辞庐 e10f1dac20 修复日志窗口内结束游戏进程会结束启动的所有游戏的问题 (#4130) 2025-07-29 17:37:42 +08:00
27Onion NebellandGlavo 86c171545e Update version check logic to support version like "26-ea" (#4128)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-07-29 17:28:24 +08:00
ShulkerSakuraandGlavo c3a3881cfc Added 16 characters length detection of username. (#4088)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-07-28 16:09:46 +08:00
Glavo 7f04cdd6b1 在日志中记录更多系统信息 (#4126) 2025-07-28 15:58:13 +08:00
Wulian233 27361c65fe Fix #4063: 修复游戏版本列表错误地过滤掉部分版本的问题 (#4066) 2025-07-27 03:01:39 +08:00
Burning_TNT ffa7afcbab 优化 GitHub Action 工作流 (#4037) 2025-07-27 00:46:11 +08:00
8Mi_Yile 9aa1367823 更新 unlisted-versions-of-minecraft 镜像源地址 (#4120) 2025-07-27 00:43:00 +08:00
Roj234andGlavo acc96b95c8 修复多次启动游戏时内存泄露的问题 (#4121)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-07-27 00:40:22 +08:00
Glavo 46502875c5 简化 FileUtils (#4119) 2025-07-26 20:10:38 +08:00
Glavo 43b769923b 使用 readAllBytes 和 transferTo 简化代码 (#4115) 2025-07-26 19:49:05 +08:00
Zkitefly fe3fc0413c 更换 unlisted-versions-of-minecraft 镜像源 (#4095) 2025-07-26 01:29:12 +08:00
Glavo fd13088726 添加字体抗锯齿算法设置选项 (#4118) 2025-07-26 01:28:35 +08:00
3gf8jv4dv 195db2ee6d fix(locale): unify terminology usage of 'JVM' and 'Java' (#3690) 2025-07-25 17:05:05 +08:00
Glavo 96f68e1017 不再使用反射调用 Java 9~11 中引入的 API (#4114) 2025-07-25 17:02:37 +08:00
Glavo 24d3bbf4fe Bump HMCLauncher to 3.7.0.0 (#4113) 2025-07-25 16:09:20 +08:00
Glavoand3gf8jv4dv d60a923841 使用 Java 11 构建 HMCL (#4078)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-07-25 16:05:09 +08:00
Glavo f9dd7a1e64 修复 InstallersPage 意外地显示滚动条的问题 (#4097)
This reverts commit c669026312.
2025-07-19 22:36:30 +08:00
Glavo cbb83d4f15 修复无法使用部分日志字体的问题 (#4096) 2025-07-19 14:56:31 +08:00
Glavo bab863287d Update versions.txt to 1.21.8 (#4085) 2025-07-18 00:08:44 +08:00
Glavo e4c479754b Bump HMCLauncher to 3.6.0.6 (#4086) 2025-07-16 18:50:57 +08:00
Glavo 8ebf58e8fa Fix #4072: 修复无法使用部分中文字体的问题 (#4074) 2025-07-09 19:39:28 +08:00
辞庐 064b39d753 修复添加游戏文件夹页面缺少灰色背景的问题 (#4065) 2025-07-06 15:46:08 +08:00
Glavo c669026312 Fix #4067: 修复使用 JavaFX 25 启动时下载界面布局错位的问题 (#4069) 2025-07-05 22:29:07 +08:00
Glavo d9b95b28b2 Update PLATFORM.md (#4062) 2025-07-04 17:06:41 +08:00
Glavo d492884927 使用 Java 8 运行时禁止更新 (#4032) 2025-07-02 23:44:27 +08:00
3gf8jv4dv cf4fde5592 chore(locale): add shader category translation for russian and spanish (#4042) 2025-07-01 07:58:40 +08:00
Glavo fd959c41c3 Update versions.txt to 1.21.7 (#4035) 2025-07-01 07:52:54 +08:00
3gf8jv4dv e34037c055 chore(locale): add world management translation for russian and spanish (#4041)
Some terminology is consistent with Minecraft Wiki.
2025-06-29 07:13:44 +08:00
3gf8jv4dv 417c804f48 chore(locale): add tip translation of deprecated java ver for ru and es (#4043)
and fix a previous typo.
2025-06-29 07:12:34 +08:00
Glavo ea6eced136 更新 mod_data.txt (#4040) 2025-06-29 04:07:48 +08:00
Glavo 432465ab46 修复部分 Linux 环境中未正确选择默认字体的问题 (#4036) 2025-06-27 17:23:27 +08:00
Burning_TNT 03c95cd726 Fix: 压制 DiscoJavaDistribution 编译警告 (#4038)
* Fix: Suppress compile-time warning.

* Fix: typo

* Adjust F*** -> Fucking.
2025-06-27 17:16:40 +08:00
Burning_TNT 730febca8c Fix: MultiMC allows libraries with the same artifact descriptor string. (#4016) 2025-06-21 21:18:53 +08:00
Kieran Moy bc41499d1c fix typo (#4015) 2025-06-19 17:19:09 +08:00
3gf8jv4dv 60fae5cc88 chore: update versions.txt to 1.21.6 (#4011) 2025-06-18 23:58:12 +08:00
Glavo c11837baa4 修复光影下载页面中标签名称未翻译的问题 (#4007) 2025-06-17 09:07:47 +08:00
Glavo aaf1218536 修复 macOS 上无法读取 CPU 信息的问题 (#4009) 2025-06-16 11:48:05 +08:00
Glavo 88637b33bc 更新 GROUPS_URL (#4004) 2025-06-15 09:34:24 +08:00
Burning_TNT 462eea3539 Fix #3997: 模组搜索返回结果为空时下载界面异常 (#3998) 2025-06-14 21:23:39 +08:00
Burning_TNT d624086594 解析 MultiMC 整合包中未声明但在依赖链上的组件 (#4003) 2025-06-14 21:23:21 +08:00
Burning_TNT fa8b3a952b Fix #3999: 自动安装更改 MC 版本后无法点击安装键 (#4001) 2025-06-14 21:12:59 +08:00
Burning_TNT c70a6c3372 移除漏洞反馈 Issue 模板中的系统信息 (#4000) 2025-06-14 21:12:33 +08:00
Glavo 0616c37de7 创建世界管理页面 (#3991) 2025-06-13 14:28:51 +08:00
Burning_TNT b72056f7fe Fix #3994: 修复 Controller 未能读取 JAVA_VERSION_TIP 的问题 (#3995) 2025-06-12 20:49:55 +08:00
Glavo 3ebe8dce44 修复在 schematics 文件夹不存在的情况下,打开投影管理页面会抛出异常的问题 (#3996) 2025-06-12 20:46:04 +08:00
Glavo a9c28573b8 Bump HMCLauncher to 3.6.0.4 (#3992) 2025-06-12 12:38:45 +08:00
9bead7e34e 支持下载光影 (#2794)
* Clean up duplicated codes in DownloadPage.

* Remove useless codes. Provide support for downloading shaderpacks from Modrinth.

* Fix.

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

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

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

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

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

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

* Update Icon

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-06-11 19:30:47 +08:00
3gf8jv4dvandGlavo d0b36ffa15 fix(i18n): localize MiB in VersionSettingsPage (#3984)
For Slavic languages (Russian, Ukrainian, etc.) they need translations.

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-06-11 01:43:05 +08:00
辞庐andGlavo ea081333e7 自动安装页面隐藏 mcbbs 项 (#3836)
* update

* CheckStyle

* Update InstallerListPage.java

CheckStyle

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-06-11 01:25:59 +08:00
3gf8jv4dv 2b99da430f chore(locale): add folder.schematics to spanish and russian (#3983) 2025-06-11 01:16:20 +08:00
Wulian233andGlavo f1ea316bad feat: 更新日志中仅显示本次更新内容 (#3974)
* 更新日志中仅显示本次更新内容

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-06-11 00:57:30 +08:00
3gf8jv4dv 9565c3bdfe fix(locale): fix units of information in russian (#3985) 2025-06-11 00:32:28 +08:00
3gf8jv4dv 710fd0f075 fix(locale): fix units of information in japanese (#3986) 2025-06-11 00:29:23 +08:00
Burning_TNT 7d7dbb9eb5 重构 MultiMC 整合包兼容功能 (#3547)
Fix #3540
Fix #3706
2025-06-08 20:59:44 +08:00
3gf8jv4dv 88a7243b32 chore(locale): clean up chinese text style (#3811)
- Clean up most of the punctuation to bring it up to standard.
- Fine-tune some of the text wording.
2025-06-08 20:29:43 +08:00
Glavo a0e9a20782 优化 JavaVersionConstraint (#3981) 2025-06-08 20:21:05 +08:00
Glavo 8889cca878 使用独立线程保存设置 (#3929) 2025-06-08 12:33:13 +08:00
Glavo bc911b95a0 Bump HMCLauncher to 3.6.0.3 (#3979) 2025-06-08 10:01:54 +08:00
3gf8jv4dv a46991bd95 chore(i18n): unify reveal button i18n (#3965)
- Unify i18n for related buttons in other pages
to `reveal.in_file_manager`.
- Add translation of `reveal.in_file_manager` for
Spanish, Japanese, and Russian.
2025-06-08 09:49:32 +08:00
3gf8jv4dv 395823c2b5 chore(locale): revise settings.launcher.launcher_log.reveal (#3966)
For various languages. Add translation for Japanese.
2025-06-08 09:48:19 +08:00
3gf8jv4dv 9769cf3996 chore(locale): add translation of "ban query" to spanish and russian (#3970)
and revise english.
2025-06-08 09:46:41 +08:00
Glavo ee652f6f31 优化原理图管理功能 (#3978) 2025-06-08 09:45:50 +08:00
Glavo d317844b28 修复 Java 管理页面无法添加 IKVM 的问题 (#3968) 2025-06-08 09:45:23 +08:00
3gf8jv4dv d9d32eab1d fix(ui): refine PromptDialogPane (#3693)
- Limit the dialog width to avoid too long a width
- Avoid accidental blank line in the information box (That blue bg box)
2025-06-06 21:56:40 +08:00
3gf8jv4dv 24bb630afa fix(locale): correct zh-TW translation of "add" (#3964)
Another one of the previous mistakes.

Glossary:
- en-US: add
- zh-CN: 添加
- zh-TW: 新增
2025-06-06 21:54:34 +08:00
3gf8jv4dv 2359ad9ed7 chore(locale): add schematic translation to spanish and russian (#3972)
and revise english.

The relevant terminology is consistent with the Litematica mod.
2025-06-06 21:05:58 +08:00
辞庐andGlavo 17abfc1a55 添加Xbox封禁相关提示 (#3860)
---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-05-31 22:53:17 +08:00
Glavo caa98c8956 新增“原理图管理”界面 (#3951) 2025-05-31 21:53:16 +08:00
General_K1ng 6124b6711f 在切换目录时确保主页版本列表切换菜单正确更新 (#3949) 2025-05-31 21:48:24 +08:00
Glavo 3b3cc54cb5 Bump HMCLauncher to 3.6.0.2 (#3953) 2025-05-31 16:36:23 +08:00
Glavo 45a40ee675 优化 Linux 平台硬件检测 (#3946) 2025-05-31 11:38:28 +08:00
Glavo 4a75f1d462 修复通过 fc-match 获取字体时未正确处理 TTC 文件的问题 (#3950) 2025-05-30 22:33:19 +08:00
Glavo e0805fc25f Fix #3940: 修复未正确解析 IPv6 服务器地址的问题 (#3942) 2025-05-29 19:34:53 +08:00
General_K1ng f223d2bc41 fix(ui): 修复版本列表为空时显示的问题 (#3944) 2025-05-29 19:26:42 +08:00
3gf8jv4dv 5ab75b6a84 chore(locale): add translations of new text for spanish and russian (#3941) 2025-05-26 21:47:44 +08:00
Glavoand3gf8jv4dv 362969db95 使用 Java 8 启动启动器时弹出弃用警告 (#3938)
* 使用 Java 8 启动启动器时弹出弃用警告

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

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

* 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>

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-05-26 09:48:55 +08:00
Glavo fb37e3b0df 修复导出启动器时未正确从 .hmcl/background 中查找壁纸的问题 (#3936) 2025-05-25 13:16:40 +08:00
Glavo ad312a61fe 清理高通骁龙 SoC 名称 (#3935) 2025-05-25 13:05:54 +08:00
Glavo 58a4700a46 创建 WindowsVersion 工具类 (#3932) 2025-05-23 21:11:35 +08:00
Glavo 68c8a30bd7 导出整合包时将启动器配置集中存放在 .hmcl 文件夹中 (#3930) 2025-05-23 18:08:57 +08:00
Glavo bb37e3fafa Use StringUtils.isASCII to check if a string contains non-ASCII characters (#3931) 2025-05-23 00:25:28 +08:00
Glavo 942f7b703e 清理 CPU 名称 (#3928) 2025-05-21 14:22:27 +08:00
Burning_TNTandGlavo 267da46050 将日志 caller 信息中的 'org.jackhuang.hmcl.' 缩短为 '@.' (#3919)
* Shorten 'org.jackhuang.hmcl.' in logs to @.

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-05-21 00:08:41 +08:00
Glavo 7c40dca3ea Bump JavaFX to 17.0.15/24.0.1 (#3927) 2025-05-20 23:55:24 +08:00
Glavo 48e3f79965 将 OS X 重命名为 macOS (#3878) 2025-05-20 19:34:35 +08:00
Glavo c5f56eb5c4 清理 OperatingSystem (#3925) 2025-05-20 19:03:48 +08:00
Burning_TNT 0900ad3e5a Fix #3757: 切换游戏文件夹后,全局游戏设置应重新读取设置 (#3818) 2025-05-20 13:41:25 +08:00
Glavo d347b0c6cb 优化系统版本检测 (#3924) 2025-05-20 13:33:57 +08:00
Glavo 2f3bf61d94 调用 PowerShell 时添加 NoProfile 参数 (#3923) 2025-05-20 12:54:04 +08:00
3gf8jv4dv 5479607597 fix(i18n): refine tasklistpane i18n (#3600)
The current TaskListPane may not look different for Chinese users, but
the grammar is not quite right for English users.

My previous series of PRs did not address this aspect due to the
associated hard coding of the i18n key.

I believe that revisions in this aspect can further improve the
experience of Indo-European speakers.

All languages except Japanese are ready for this PR.
2025-05-20 11:53:01 +08:00
Glavo 4291ae5f16 在 Linux 平台通过 fc-match 查询默认字体 (#3922) 2025-05-20 00:25:56 +08:00
e74yp8andGlavo 1ac677365b Fix #3865 游戏版本名称在游戏版本更换时报红 (#3874)
---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-05-19 12:00:16 +08:00
Glavo b079e50916 修复启动器在部分 Linux 发行版上无法正确识别用户地区的问题 (#3921) 2025-05-19 11:06:45 +08:00
e74yp8 f986df9654 Fix #3896: 下载界面对于多行信息显示错误 (#3901) 2025-05-18 23:11:34 +08:00
RainChanandGlavo c576c48f9d Fix #3852: 修复无法一键更新整合包的问题 (#3888)
* Fix #3852 修复无法一键更新整合包

* 删除未使用的import

* fix checkstyle

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-05-17 07:54:13 +08:00
Glavo 5bb035c884 在非 x86 平台检测 CPU 物理核心数 (#3917) 2025-05-17 00:20:17 +08:00
Glavo 973b7a9716 启动时在日志中记录 CPU 信息 (#3914) 2025-05-16 14:50:43 +08:00
Glavo 6e05b5ee58 使用 JNA 查询注册表 (#3913) 2025-05-14 22:18:02 +08:00
Glavo 76ed9353bd 重构 Theme (#3910)
* update

* update

* update

* update

* update

* update
2025-05-14 14:18:50 +08:00
Glavo 9c0f823705 优化代理设置 (#3876) 2025-05-13 16:39:43 +08:00
e74yp8 b69602b735 Fix #3218: 修复复制游戏版本里的“复制存档”选项失效问题 (#3868) 2025-05-12 23:21:20 +08:00
3gf8jv4dv 7cdf4504d1 fix(locale): add i18n key curse.category.7669 (#3906) 2025-05-11 23:07:29 +08:00
Burning_TNT d95efc4cdc Use org.intellij.lang.annotations.Language (#3907) 2025-05-11 23:07:06 +08:00
Glavo 48ca2d6ee6 在 SystemInfo 中采集内存信息 (#3903) 2025-05-10 12:06:11 +08:00
Glavo 1da98d6d67 启动时在日志中记录当前平台的所有 GPU (#3893) 2025-05-09 10:08:08 +08:00
Glavo 06a89dd6a9 尝试从 HMCL.jar 所在文件夹加载默认字体 (#3899) 2025-05-08 22:36:33 +08:00
3gf8jv4dv 4eee79da17 chore(locale): revise invalid username prompt for spanish and russian (#3897) 2025-05-06 21:48:41 +08:00
Glavo 9788338b65 更新“关于”页面 (#3895) 2025-05-06 21:48:05 +08:00
3gf8jv4dv fb8da249b6 chore(locale): add/revise prompt about create hmcl dir failure (#3879) 2025-05-04 23:14:53 +08:00
Glavo 2d8b1df465 在 Windows 平台通过 GeoID 检测用户所处国家/区域 (#3891) 2025-05-04 23:13:40 +08:00
Glavo 1ab7ab0750 使用 JNA 调用本机 API (#3890) 2025-05-04 22:31:50 +08:00
3gf8jv4dv f283254ca0 fix(locale): revise version.empty translation (#3812)
Revise `.launch` and `.hint` to make text match the actual situation.
2025-05-04 09:31:35 +08:00
Wulian233andGlavo 2ea96fdaa7 Fix #3864 游戏内容下载的分页按钮不重置 (#3866)
* Fix #3864

* Update DownloadListPage.java

* Update DownloadListPage.java

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-05-03 04:45:47 +08:00
Glavo b32d3c354a Move authlib-injector.jar to .hmcl/dependencies/universal (#3886) 2025-05-03 04:45:25 +08:00
e74yp8 cd57b163a3 Fix #3851: 当用户名为中文名时,离线账户 - LittleSkin 换肤 功能失效 (#3883) 2025-05-02 22:21:55 +08:00
Glavo cc5484f9ed Fix #3881: 打开启动器时应当创建 HMCL_GLOBAL_DIRECTORY (#3882) 2025-05-01 11:59:34 +08:00
Glavo bfa9c1a697 修复启动器未压缩日志的问题 (#3877) 2025-04-30 14:56:03 +08:00
Glavo 9a37d412a3 使用工作路径下的 .hmcl 文件夹存放所有配置文件 (#3875) 2025-04-30 14:50:51 +08:00
Glavo dc5ebcf64d 使用 libs.versions.toml 管理依赖版本 (#3873) 2025-04-29 13:41:45 +08:00
Glavo 038f56b048 Bump Shadow Plugin to 8.3.6 (#3871) 2025-04-29 10:13:02 +08:00
Glavo 528f79e008 使用 Java 11 构建 buildSrc (#3870) 2025-04-29 10:04:29 +08:00
Glavo 75d0ec34f5 将构建项目所需的最低 JDK 版本升级至 11 (#3805) 2025-04-29 09:52:06 +08:00
Glavo d70712a18f Fix #3546: 修复游戏崩溃窗口乱码的问题 (#3867) 2025-04-27 22:15:30 +08:00
Glavo 05c5b53c52 自动探测日志文件编码 (#3863) 2025-04-27 20:39:49 +08:00
Glavo e98c7dfef1 Fix #3400: 逐行过滤日志中的 token (#3856) 2025-04-27 15:41:09 +08:00
3gf8jv4dv 8ba0e16dd9 chore(locale): add new translations for russian and spanish (#3838)
For relevant text of offline account restrictions.
2025-04-26 16:40:12 +08:00
Glavo 77397f6722 优化离线账户中包含非 ASCII 字符时的提示 (#3853) 2025-04-26 13:19:38 +08:00
Glavo c9ba789b7c Bump Gradle to 8.14 (#3857) 2025-04-26 00:05:20 +08:00
Glavo 91135b961e 右键主页“启动游戏”按钮时展开版本列表菜单 (#3854) 2025-04-25 00:31:52 +08:00
3gf8jv4dvandGlavo 435790aa03 fix(i18n): show installable translations according to different styles (#3624)
* fix(i18n): show installable translations according to different styles

when install a new instance (安装新游戏): Do not install (不安装)
on Edit Instance → Loaders (自动安装页面): Not installed (未安装)

resolves https://github.com/HMCL-dev/HMCL/issues/3297

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-25 00:14:45 +08:00
Glavo c252d7027e 更新 Issue 模板 (#3847) 2025-04-22 23:59:24 +08:00
Glavo 9d0c8da310 拆分 HMCLauncher 至独立项目 (#3840) 2025-04-20 11:52:29 +08:00
Burning_TNTandGlavo 84b3312ebb Fix #3825: 修复 NeoForge 愚人节版本无法正常解析的问题 (#3826)
* Fix #2825.

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-19 11:49:42 +08:00
Burning_TNTandGlavo f351d1d133 Close #3829: 将模组/整合包/资源包的默认下载源调整为 Modrinth (#3830)
* Close #3829: Use Modrinth as default download source.

* Update HMCLLocalizedDownloadListPage.java

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-18 22:49:02 +08:00
Glavo 5ca1120223 Bump jsoup to 1.19.1 (#3835) 2025-04-18 22:42:10 +08:00
Glavo 6aa4019221 Bump Gson to 2.13.0 (#3834) 2025-04-18 22:35:05 +08:00
Glavo 73fb8bda10 优化默认 JVM 参数 (#3824) 2025-04-15 03:46:33 +08:00
Glavo cc16f84992 默认限制离线账户功能 (#3823) 2025-04-14 20:34:01 +08:00
辞庐 b4945a150e 统一导出和导入整合包页面样式 (#3822) 2025-04-13 22:34:22 +08:00
Wulian233andGlavo 8bcbe74d82 优化整合包导出类型选择页面 (#3800)
* 优化整合包导出类型页面

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-12 22:10:28 +08:00
Glavo 69c2429e92 Bump mesa-loader-windows to 25.0.3 (#3819) 2025-04-12 20:36:05 +08:00
Glavo 88e83dc44a 修复自定义背景图片路径包含无效字符时启动器崩溃的问题 (#3817) 2025-04-12 17:51:28 +08:00
Burning_TNT 87c2126e5a Fix #3471: 模组管理内“安装到当前版本”与“下载到本地文件夹”相同 (#3772) 2025-04-12 17:39:58 +08:00
Liyan Zhao abf1ec3eb8 反序列化账户信息时优先将数字解析为 Long (#3080) 2025-04-12 13:01:49 +08:00
Burning_TNT 139fb41149 Fix #3147: 修复 LiteLoader 下载相关问题 (#3776)
* Fix #3147

* Fix: Use 'release' classifier on snapshot versions. Speed up loading time.

* Use https.
2025-04-12 12:07:26 +08:00
Glavo 6c986921d6 Delete unused constants (#3816) 2025-04-12 12:05:24 +08:00
3gf8jv4dv b8e6d6fc96 fix(locale): adjust the wording of the unofficial hmcl build prompts (#3813)
The display condition for `download.curseforge.unavailable` is whether
HMCL has a CurseForge API secret built in, not its update channel.
2025-04-12 11:39:40 +08:00
3gf8jv4dv 75e4aee8aa fix(locale): improve world backup ui i18n (#3815) 2025-04-12 11:38:50 +08:00
Glavo abda9f50e1 Fix #3807: 修复 Fabric Metadata 无效时 FabricInstallTask 抛出 NPE 的问题 (#3814) 2025-04-12 02:13:14 +08:00
Burning_TNT c33ef5170b Fix #3644: 修复下载 - 模组界面无法正常翻页、Category 会在切换下载源时出错的问题 (#3768) 2025-04-09 11:35:56 +08:00
Glavo a3cb871928 修复 RISC-V 平台游戏识别 CPU 名称出错的问题 (#3806) 2025-04-09 01:27:38 +08:00
Glavo 62e965c6fe 使用 Kala Compress 替代 commons-compress (#3481) 2025-04-08 17:36:36 +08:00
Glavo 99a031cea4 Gradle run task 应使用 HMCL_JAVA_HOME 所指向的 Java (#3804) 2025-04-08 12:29:14 +08:00
Zkitefly f97b9b9382 Update unlisted-versions.json (#3799) 2025-04-06 11:29:17 +08:00
Zkitefly a8929560df Update PURCHASE_URL (#3797) 2025-04-05 21:04:55 +08:00
Glavo 22c8c59b5d 优化 CacheRepository (#3794) 2025-04-05 06:08:02 +08:00
ZkiteflyandGlavo 78b043c19c 添加 unlisted-versions-of-minecraft (#3247)
* 添加 unlisted-versions-of-minecraft

* Update BMCLAPIDownloadProvider.java

* Update VersionsPage.java

* Update VersionsPage.java

* Update BMCLAPIDownloadProvider.java

* update name

* update

* update

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-04 22:49:31 +08:00
3gf8jv4dv fd51fb7d26 chore(locale): adjust the use of quotation marks in spanish (#3685)
Replace the quotation marks with guillemets.
2025-04-04 22:38:05 +08:00
Burning_TNT 43c59dad9c Fix: There are two progress bars when updating HMCL (#3793) 2025-04-04 20:54:08 +08:00
Zkitefly 81b0547610 更新链接为 Metadata 中的常量,优化代码可维护性 (#3674)
* 更新链接为 Metadata 中的常量,优化代码可维护性

* update
2025-04-04 18:47:42 +08:00
997a16486b 优化下载窗口标题 (#3771)
* update

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

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

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

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

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

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

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

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

* Apply suggestions from code review

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

* update

* update

* update

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-04 00:47:37 +08:00
Wulian233andGlavo 36ebdab698 折叠高级设置中基于特定系统不可用的选项 (#3734)
* 隐藏高级设置中基于特定系统不可用的选项

* 不支持的系统使用折叠选项

* 完成~待审

* 修改文本描述

* 修正翻译

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-03 23:08:34 +08:00
Glavo a6471bca09 Add 25w14craftmine to versions.txt (#3781) 2025-04-02 00:35:05 +08:00
Burning_TNT cb6320ffae Fix #3627: 修复离线账户切换模型时左侧 3D 预览不会即时更新的问题 (#3769) 2025-04-01 15:43:01 +08:00
Burning_TNT 7cf520cb48 Fix #3589: 修复 Windows 平台部分语言编码下无法打开 Minecraft 中文 Wiki 的问题 (#3770) 2025-04-01 15:40:05 +08:00
Minecraft-温迪andGlavo 7dc922d4bd Fix #3777: 修复使用 Rosetta 2 转译运行时无法正确识别系统架构的问题 (#3778)
* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-01 15:31:09 +08:00
Glavo 8e57729349 Update versions.txt (#3744) 2025-03-26 22:55:44 +08:00
Glavo 66ed87eb1e 修复 cslApi 为 null 时启动器崩溃的问题 (#3762) 2025-03-24 02:52:17 +08:00
ShulkerSakuraandGlavo 27ffc8eec5 Add MacOS Style Icon (#3755)
* Add MacOS Style Icon

* MacOS Style Alt Icon

* Scaled Mac Style Icon

* Delete unused icons

* Rename icon

* update

* Optimize icon file

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-03-22 19:13:32 +08:00
Glavo e2f50c92e9 修复无法解析部分 Java 版本号的问题 (#3759)
* 修复无法解析部分 Java 版本号的问题

* update

* update
2025-03-22 12:17:13 +08:00
Glavo e8ff42c4b2 修复取消导出整合包时弹出报错对话框的问题 (#3752)
* 修复取消导出整合包时弹出报错对话框的问题

* Update
2025-03-21 11:01:37 +08:00
Glavo 44ca2cdb55 更新主页公告 (#3751)
* 更新主页公告

* update

* update
2025-03-20 21:25:30 +08:00
Glavo 77bbee02f9 消除 JEP 498 引入的弃用警告 (#3750)
* 消除 JEP 498 引入的弃用警告

* Fix checkstyle
2025-03-20 16:59:46 +08:00
Glavo e4c7b137f1 Add --enable-native-access for javafx.graphics (#3749)
* Add --enable-native-access for javafx.graphics

* fix checkstyle

* update
2025-03-20 16:21:17 +08:00
Glavo efd088e014 在日志中记录编码信息 (#3747)
* 在日志中记录 Code Page

* update

* update
2025-03-20 10:02:16 +08:00
Glavo 2ae266491c 更新关于页面头像 (#3745) 2025-03-18 20:11:07 +08:00
Glavo 0912613cfc 优化 ScrollUtils (#3432)
* 优化 ScrollUtils

* update
2025-03-18 13:31:47 +08:00
Glavo d9e3816b8e 优化账户列表页面 (#3736) 2025-03-13 22:18:23 +08:00
GlavoandZkitefly bdaf72fe2f 修复 ModrinthRemoteModRepository::getRemoteVersionByLocalFile 未捕获 NoSuchFileException 的问题 (#3735)
---------

Co-authored-by: Zkitefly <2573874409@qq.com>
2025-03-11 21:58:18 +08:00
Glavo 7efbbefdcf HTMLRenderer 应当避免插入过多换行符 (#3733) 2025-03-10 22:14:46 +08:00
Glavo ad71216bfc 为部分页面添加半透明背景遮罩 (#3730) 2025-03-09 13:02:00 +08:00
Wulian233 57903da09b Close #3728: 为更新日志对话框添加平滑滚动 (#3729) 2025-03-08 22:35:51 +08:00
3gf8jv4dv cf56c8035b fix: add scrollbar for GameCrashWindow (#3723)
Shows a scroll bar when needed to prevent user misinterpretation
when there is too much information.
2025-03-08 16:32:54 +08:00
Glavo dbf519b435 Fix #3720: 修复备份文件路径被错误地设置为源世界的问题 (#3722) 2025-03-08 16:18:49 +08:00
Glavo 696b9bd7ef Fix #3694: 显示对话框时隐藏焦点 (#3717) 2025-03-08 08:47:19 +08:00
3gf8jv4dv ec5236368a chore(locale): add new translations of world parts for es and ru (#3714) 2025-03-07 21:39:39 +08:00
3gf8jv4dv 8d3794d828 fix(i18n): split game.version to make different scenarios appropriate (#3709)
- `game.version` indicates the currently used game instance
in the game crash window, not the game version.

- Change the i18n key previously used in ModListPageSkin to
`mods.game.version`, to be consistent with the world section.
2025-03-07 21:06:34 +08:00
3gf8jv4dv d1b59ef553 fix(locale): revise spanish (#3713)
Fix some mistakes. :\
2025-03-07 20:20:04 +08:00
3gf8jv4dv 79f29e143e fix(locale): clean up language typos (#3707) 2025-03-07 19:35:05 +08:00
Glavo e7ffb0b271 优化世界管理页面 (#3711)
* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update
2025-03-07 19:19:04 +08:00
3gf8jv4dv cda2f6fb82 chore(locale): improve spanish translation (#3712)
- Add translation of the `game.crash` part.
- Improve further the translation of the remaining parts.
2025-03-07 17:34:40 +08:00
3gf8jv4dv 114aa4ce94 chore(locale): add missing translations for spanish (#3710)
Several i18n keys were added during the 3.6.12 semester.
This patch adds those translations.
2025-03-07 17:12:34 +08:00
Glavo 0ff194b391 Fix #3485: 修复游戏 Java 路径显示错误的问题 (#3708) 2025-03-06 12:30:13 +08:00
3gf8jv4dvandGlavo 025bde66d6 chore(locale): improve russian translation (game crash part) (#3704)
* chore(locale): improve russian translation (game crash part)

- Re-translate something from English
- Fix and unify the style of text left by different contributors
- Fix some inconsistent/incorrect terminology
- Add missing translations

* Update I18N_ru.properties

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-03-06 12:00:37 +08:00
Glavo 872c1943b7 修复 Javadoc 中的无效链接 (#3703) 2025-03-05 19:00:22 +08:00
Glavo d268d923d7 更新全部 SVG 图标至 Material Symbols (#3687) 2025-03-05 16:39:21 +08:00
3gf8jv4dv d523ccd312 fix(ui&locale): optimize the create offline account dialog (#3695)
- Adjust lblErrorMessage for invalid username to avoid ui truncation
- Improve the wording of warnings
2025-03-05 16:05:02 +08:00
3gf8jv4dv 78a5ade35a fix(locale): fix english translation of game.crash.reason.file_changed (#3699)
My mistake in https://github.com/HMCL-dev/HMCL/pull/3367
2025-03-05 16:02:56 +08:00
Glavo f011a14798 在 DDE 上禁用自绘窗口动画 (#3701) 2025-03-05 16:01:48 +08:00
Glavo f4701c678e Revert "启用高帧率动画 (#3655)" (#3700)
This reverts commit a45b636b1c.
2025-03-05 14:45:02 +08:00
Zkitefly e61fb27c61 更新 PURCHASE_URL (#3675)
* Update YggdrasilService.java

* Update YggdrasilService.java
2025-03-05 02:54:39 +08:00
3gf8jv4dv d2f58a89a2 fix(locale): remove duplicate extensions (#3692)
Oops... Didn't notice this before.

The issue first appeared in https://github.com/HMCL-dev/HMCL/pull/2468
2025-03-03 15:58:53 +08:00
3gf8jv4dvandGlavo 5174e0a67a chore(locale): revise english (#3688)
* chore(locale): revise english

`settings.launcher.launcher_log.reveal`
Considering that HMCL is a cross-platform launcher and not all systems
use the term `Explorer`, this should be changed to a more generic term.

`update.channel.dev.hint` and `update.channel.nightly.hint`
Revise text style.

Misc
Resolve rendering issues on some text editors.

* Update I18N.properties

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-03-03 10:41:03 +08:00
3gf8jv4dv 4225e98d6b chore(locale): improve russian translation (curseforge category) (#3686)
- Add translations
- Some proper nouns (mod name) keep the original name
- Some proper nouns (game content) follow the game and wiki
2025-03-03 02:12:46 +08:00
Glavo 0540f3c9cd 在世界管理界面显示世界图标 (#3689)
* 在世界管理界面显示世界图标

* update

* update
2025-03-03 02:12:07 +08:00
Glavo 4292d47fe8 Bump Gson to 2.12.1 (#3691) 2025-03-02 23:58:07 +08:00
3gf8jv4dv 4e98d80855 chore(locale): improve russian translation (setting, launch, menu, etc) (#3684)
Fully revision, except for the `curse` and `game.crash.reason` parts.

- Re-translate something from English
- Fix some inconsistent terminology
- Change some terminology to make sure to keep up with the game and wiki
- Add some missing translations
- Revise some incorrect/outdated translations
2025-03-02 23:56:55 +08:00
Glavo 578aaafb0a 使用 DDE 桌面时启用窗口动画 (#3682) 2025-03-02 00:28:44 +08:00
3gf8jv4dv 60deea64c8 chore(locale): improve russian translation (mods & nbt) (#3678)
- add `mods.check_updates.empty`
- add nbt translations
2025-03-01 21:05:08 +08:00
3gf8jv4dv 7e1cd4319b chore(locale): improve russian translation (modpack) (#3679)
- Revise some translations based on English
- Change some terminology to make sure to keep up with the game and wiki
- Add some missing translations
2025-03-01 21:04:54 +08:00
Glavo 318da840f9 修复对话框关闭动画缺失的问题 (#3676) 2025-03-01 21:01:51 +08:00
Glavo 0c7046d764 优化 TransitionPane (#3680)
* update

* update

* update

* update

* update

* update

* update

* update

* update

* Fix checkstyle
2025-03-01 20:50:31 +08:00
Glavo bdf4886445 禁止标题栏中返回键获取焦点 (#3677) 2025-03-01 10:20:39 +08:00
Glavo 4a758d045d 使更多标签支持双击复制 (#3673) 2025-03-01 01:47:56 +08:00
Glavo 1059a4b3d4 支持关闭鼠标进入 RipplerContainer 时的渐变动画 (#3670)
* update

* fix checkstyle
2025-02-28 23:56:42 +08:00
Glavo a25eb14da4 优化 TransitionPane (#3668) 2025-02-28 23:41:40 +08:00
Glavo 349b663c83 禁止标题栏按钮获取焦点 (#3666) 2025-02-28 01:54:53 +08:00
Glavo 04781da3a7 清理 FXUtils (#3667) 2025-02-28 01:53:31 +08:00
Glavo 96e1ddff79 Close #3664: 支持双击复制世界信息 (#3665)
* Close #3664: 支持双击复制世界信息

* update

* update
2025-02-27 23:58:20 +08:00
3gf8jv4dv e8502890a4 chore(locale): improve russian translation (game setting / management) (#3661)
- Re-translate something from English
- Fix some inconsistent terminology
- Revise text to fit UI
- Change some terminology to make sure to keep up with the game and wiki
- Add some missing translations
2025-02-27 21:19:31 +08:00
3gf8jv4dv 4d0619a9df chore(locale): improve russian translation (javafx dialog / search) (#3662) 2025-02-27 21:18:57 +08:00
3gf8jv4dv fba14737f9 chore(locale): revise unofficial hint for russian (#3660)
- add `unofficial.hint`
- fix typo in `account.methods.microsoft.snapshot`
2025-02-27 20:56:59 +08:00
3gf8jv4dv 5de98399c5 chore(locale): add wiki translation for russian (#3659) 2025-02-27 20:56:41 +08:00
Glavo 7adfa0e3d1 启动时尝试从 HMCL_DIRECTORY 中加载字体 (#3663)
* 启动时尝试从 HMCL_DIRECTORY 中加载字体

* update

* update
2025-02-27 15:05:51 +08:00
Glavo a45b636b1c 启用高帧率动画 (#3655)
* 启用高帧率动画

* update
2025-02-27 09:51:06 +08:00
Glavo 50c22e1f43 添加最小化和还原动画 (#3657)
* update

* update

* update

* update

* update

* update

* update

* update

* update

* fix checkstyle
2025-02-26 20:39:49 +08:00
Glavo 3b38c22398 修复标题过长时窗口异常的问题 (#3654) 2025-02-26 02:00:27 +08:00
Glavo 618243350e 优化 NBT 查看器 (#3653)
* update

* update

* update
2025-02-26 01:40:36 +08:00
Glavo ff4940e79f 添加关闭启动器时的淡出动画 (#3652)
* 添加关闭启动器时的淡出动画

* update
2025-02-26 01:04:12 +08:00
Glavo 1c163934ee Fix #2048: 当微软账户 Token 过期时应当刷新账户 (#3651) 2025-02-25 20:50:49 +08:00
Glavo c513d00c50 添加启动动画 (#3442)
* update

* update

* update
2025-02-25 18:20:44 +08:00
3gf8jv4dv 2c9549b36b fix(ui): limit width of FileSelector in offline skin pane (#3623)
make the text around the selector not be covered.

resolves https://github.com/HMCL-dev/HMCL/issues/3296
2025-02-25 17:58:31 +08:00
Glavo 3389e0a934 Close #3649: 点击账户卡片空白处时切换账户 (#3650)
* Close #3649: 点击账户卡片空白处时切换账户

* update
2025-02-25 17:36:51 +08:00
3gf8jv4dv 78149449a9 chore(locale): improve russian translation (modrinth category) (#3647)
- Add missing translations
- Fix existing translations
2025-02-25 17:33:07 +08:00
3gf8jv4dv b792b23ad4 chore(locale): improve russian translation (web) (#3648) 2025-02-25 17:32:27 +08:00
3gf8jv4dv 881b0e9127 chore(locale): improve russian translation (account part) (#3642)
- Re-translate some translations based on English
- Fix some inconsistent terminology
  - Such as Account, учетная запись > аккаунт
- Add some missing translations
- Reorder some keys to ensure they match with English
- Adjust terminology related to LittleSkin to ensure consistency with
the terminology used on the site
2025-02-23 10:48:45 +08:00
3gf8jv4dv 77e06c8820 chore(locale): adjust english translation of account.portable (#3643)
Match the style of the account type shown for the account item
on account list page
2025-02-23 10:48:03 +08:00
Glavo bc2405cf12 修复在 macOS 平台无法找到通过 DMG 安装的 Java 的问题 (#3645) 2025-02-23 10:47:25 +08:00
Glavo 72b5428f8b 修复 NeoForge 版本识别错误的问题 (#3638) 2025-02-22 17:28:25 +08:00
Glavo e7e8d45497 禁止整合包复制全局游戏设置 (#3639)
* 禁止整合包复制全局游戏设置

* update
2025-02-22 14:35:54 +08:00
3gf8jv4dv 82dbe18d78 fix(i18n): fix a missing i18n for LittleSkin term (#3637)
The word "LittleSkin" appears twice in OfflineAccountSkinPane, but one
of them is hardcoded. Add i18n support for the hardcoded one.
2025-02-22 08:36:34 +08:00
3gf8jv4dv ecb7ef0867 fix(locale): remove accidental japanese characters from russian (#3636)
This bug first appeared in https://github.com/HMCL-dev/HMCL/pull/2412
2025-02-22 00:50:59 +08:00
Glavo d40742a624 Bump jsoup to 1.18.3 (#3632) 2025-02-19 16:28:46 +08:00
Glavo 6f8002a0cf Fix typo in FXUtils (#3631) 2025-02-19 15:34:08 +08:00
3gf8jv4dv 3d3896c5c4 fix(i18n): add i18n support for steve & alex option in offline skin pane (#3630)
Some Slavic Minecraft communities have translated Steve and Alex,
such as Russian and Ukrainian. They need a translation.

ref:
- https://ru.minecraft.wiki/w/%D0%A1%D0%BA%D0%B8%D0%BD
- https://uk.minecraft.wiki/w/%D0%A1%D0%BA%D1%96%D0%BD
2025-02-19 15:30:34 +08:00
Glavoand3gf8jv4dv a24f8e2ec0 在游戏设置中添加“复制全局游戏设置”选项 (#3628)
* update

* update

* Fix checkstyle

* update

* fix checkstyle

* 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>

* Fix FXUtils.bindEnum

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-02-19 15:29:50 +08:00
Glavo fce08a6923 删除全局游戏设置页面的离线账户更换皮肤提示 (#3629) 2025-02-19 13:38:51 +08:00
Glavo 5fa01c8838 修复在非官方支持的平台上 Minecraft 1.21 与 Sodium 不兼容的问题 (#3622)
* update

* update

* update
2025-02-18 23:58:16 +08:00
Glavo 15a3fb5f59 优化 VersionsPage 搜索框 (#3625)
* update

* update

* update

* update
2025-02-18 23:51:28 +08:00
Glavo d5ab14f476 Fix #3295: 修复离线账户选择 Steve/Alex 皮肤时模型错误的问题 (#3626)
* Fix #3295: 修复离线账户选择 Steve/Alex 皮肤时模型错误的问题

* update
2025-02-18 22:46:52 +08:00
Glavo 554ecfd6d5 Fix #3445: 点击安装整合包时无条件关闭整合包下载对话框 (#3621) 2025-02-17 23:35:58 +08:00
Glavoand3gf8jv4dv c2abe755f4 优化整合包下载对话框 (#3620)
* update

* 优化整合包下载对话框

* update

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

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

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-02-17 22:44:25 +08:00
Glavo c0410c46ff 在日志中记录 PROCESSOR_IDENTIFIER (#3619) 2025-02-16 21:10:58 +08:00
Glavo da93b4c12d 修复无法识别 Intel64 架构的问题 (#3618) 2025-02-16 17:23:14 +08:00
Zkitefly c16d6e8d5f 添加 NeoForge 的 BMCLAPI 代理支持 (#3605) 2025-02-15 16:38:30 +08:00
Glavo 6df73aeb68 修复语言设置中英语选项名称错误的问题 (#3612) 2025-02-15 12:19:09 +08:00
3gf8jv4dv 23d11eca81 fix(locale): adjust time formats for various languages (#3613)
- Change to a standard and commonly used format.
- Rename i18n key as it is used in more than one place.
2025-02-15 12:16:42 +08:00
Glavo 0e307f3b71 简化 AdvancedListBox (#3611) 2025-02-12 21:53:10 +08:00
Glavo 33b791f2b0 更新日期时间格式 (#3610)
* Revert "improv(i18n): refine time formats on ui (#3608)"

This reverts commit 741f65d37c.

* update
2025-02-12 19:37:05 +08:00
3gf8jv4dv de01946ab2 fix(locale): fix a typo in zh-TW language (#3606) 2025-02-12 17:35:03 +08:00
3gf8jv4dv 5526bac70e fix(locale): change Socks to SOCKS (#3607)
ref: https://en.wikipedia.org/wiki/SOCKS
2025-02-12 17:33:31 +08:00
3gf8jv4dv 741f65d37c improv(i18n): refine time formats on ui (#3608)
- unify time formats using DateTimeFormatter, deprecate `world.time`
- change FormatStyle to make the time detail appropriate
- fix incorrect time format in English locale
2025-02-12 17:32:36 +08:00
Haowei Wen ecafc9d6f0 Fix height computation in ComponentListCell 2025-02-11 23:51:50 +08:00
Glavo 8795d522b0 支持临时隐藏预览版提示 (#3603) 2025-02-11 19:00:03 +08:00
Glavo d8e7e0e97e 更新 mod 数据 (#3601)
* 更新 mod 数据

* update

* update

* update
2025-02-10 22:04:48 +08:00
Glavo 8236dfc7e5 Fix #3595: patchNative may throw NPE when gameVersion is null (#3602) 2025-02-10 19:59:25 +08:00
Glavo 8a15b7d274 修复使用自定义字体时未正确设置字重的问题 (#3598) 2025-02-10 00:41:31 +08:00
3gf8jv4dv fce4c13515 fix(locale): fix zh-tw locale for game crash window buttons (#3596) 2025-02-09 09:28:00 +08:00
Glavo 2f36506099 Fix #3508: Java 下载对话框中切换包类型时不应重置版本字段 (#3594)
* Fix #3508: Java 下载对话框中切换包类型时不应重置版本字段

* fix checkstyle
2025-02-08 18:02:46 +08:00
Glavo 5e60eaf595 修复 macOS 平台 HMCLauncher.sh 不会显示 Java 下载页面的问题 (#3549) 2025-01-29 07:35:19 +08:00
Glavo f70fb4ae08 Update README (#3548) 2025-01-29 07:27:17 +08:00
3gf8jv4dv 38adb48051 Fix qq badge in readme (#3544)
ref: https://shields.io/blog/simple-icons-14
2025-01-25 16:38:55 +08:00
Glavo f972c49762 Fix #3539: 安装 MultiMC 整合包后应当设置图标 (#3541) 2025-01-24 09:11:22 +08:00
Glavo 5afeb3d019 优化 HMCLauncher.sh (#3538) 2025-01-23 18:21:29 +08:00
Burning_TNT 38897fbd2a Fix #3063 修复本地模组管理界面水平滚动条显示异常的问题 (#3463) 2025-01-23 13:51:55 +08:00
ZkiteflyandGlavo 0562042218 Update HMCLauncher.sh (#3265)
* Update HMCLauncher.sh

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-01-23 13:17:33 +08:00
Glavo c5e7ee8aef 优化对 Linux RISC-V 64 平台的支持 (#3536) 2025-01-23 10:23:27 +08:00
Glavo 35b288b800 Fix #3529: 修复在 FreeBSD x86-64 平台未正确替换 LWJGL 本地库的问题 (#3531)
* Fix #3529: 修复在 FreeBSD x86-64 平台未正确替换 LWJGL 本地库的问题

* update

* update

* update
2025-01-20 14:03:11 +08:00
Zkitefly a609d32081 更新版权年份至2025 (#3527) 2025-01-16 11:26:31 +08:00
Haowei Wen 72d8b75643 模组列表页中按下 Esc 时取消选中 (#3523)
* 模组列表页中按下 Esc 时取消选中

目前在模组列表页中选中模组后,按下 Esc 是不会有任何反应的,因为 [ListView](https://github.com/openjdk/jfx/blob/5b074c4c2b4f539301813322a6589cec28121553/modules/javafx.controls/src/main/java/com/sun/javafx/scene/control/behavior/ListViewBehavior.java#L108) 会处理这个事件,认为是取消编辑,而不论是否处于编辑模式中。

这个 PR 添加了 `FXUtils.ignoreEvent()` 函数,用于忽略掉对特定事件的处理。然后,我们就可以让 ListView 不处理 Esc 按下事件,然后在模组页添加 Esc 处理逻辑(必须让 ListView 忽略掉这个事件,不然就会被它优先处理)。

理论上所有用了 ListView / JFXListView 的地方都会有这个问题,即当焦点在 ListView 中时,按下 Esc 不会触发正常的事件处理(如返回上一页、关闭对话框)。比如在游戏下载页面中,对某个版本按下右键后,焦点就会转移到其上,此时再按 Esc 就毫无反应。

* 修复 Esc 在版本下载页/模组下载页/数据包页不工作
2025-01-16 11:15:29 +08:00
3gf8jv4dvandyushijinhun 01f9a452f1 Update ISSUE_TEMPLATE [skip ci] (#3446)
* Update ISSUE_TEMPLATE [skip ci]

* Apply suggestions from yushijinhun

Co-authored-by: yushijinhun <yushijinhun@gmail.com>

---------

Co-authored-by: yushijinhun <yushijinhun@gmail.com>
2025-01-06 03:40:49 +08:00
3gf8jv4dvandyushijinhun f2edc8001c Revise Spanish & English (#3465)
* Revise Spanish & English

Overhaul for Spanish. I cannot guarantee it is 100% correct, but I
promise to proofread it. During the overhaul some more mistakes in the
English language were discovered, so let's fix it.

All translations have been revised except for the `game.crash.reason`
series.

* Apply suggestions from yushijinhun

Co-authored-by: yushijinhun <yushijinhun@gmail.com>

---------

Co-authored-by: yushijinhun <yushijinhun@gmail.com>
2025-01-06 03:37:19 +08:00
3gf8jv4dvandyushijinhun d2df515325 Revise README [skip ci] (#3447)
* Revise README [skip ci]

* Apply suggestions from yushijinhun

Co-authored-by: yushijinhun <yushijinhun@gmail.com>

---------

Co-authored-by: yushijinhun <yushijinhun@gmail.com>
2025-01-06 03:32:42 +08:00
3gf8jv4dv 95e39fe87a gha: revise release description [skip ci] (#3474)
unify text style
2025-01-06 03:12:13 +08:00
lijiaxuanand3gf8jv4dv 662bf1a9d4 Update I18N.properties English Translations (#3477)
* Update I18N.properties English Translations

更新了英文语言翻译文件,修复了一些语法错误并且让一些句子更加通顺

* Update I18N.properties

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

* Update I18N.properties

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

* Update I18N.properties

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

* Update I18N.properties

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

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

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

* Update I18N.properties

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

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

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

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

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-01-06 02:15:58 +08:00
Zkiteflyand3gf8jv4dv bfda297838 检查安装时的非ASCII字符名称 (#3244)
* 检查安装时的非ASCII字符名称

* i18n

* 添加导出模组列表信息功能

* Revert "添加导出模组列表信息功能"

This reverts commit 6505d1c284.

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

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

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

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

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

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

---------

Co-authored-by: 3gf8jv4dv <158490944+3gf8jv4dv@users.noreply.github.com>
2025-01-05 01:31:44 +08:00
ZkiteflyandGlavo d80f61e2e7 update getAssetIndex() (#3252)
* update getAssetIndex()

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-01-04 21:33:48 +08:00
Zkitefly 1f7b156423 Press ESC in search box to exit search (#3519) 2025-01-04 01:38:11 +08:00
Haowei Wen 56d20a5d08 Fix problems with window resizing in Linux 2025-01-04 01:35:36 +08:00
Haowei Wen b5cea83a8a Workaround for JDK-8344372
https://bugs.openjdk.org/browse/JDK-8344372
2025-01-03 20:15:03 +08:00
Haowei Wen 397edaec5d Press ESC in search box to exit search 2025-01-03 17:08:19 +08:00
Zkitefly 39b8de4824 fix 2025-01-03 17:08:19 +08:00
Zkitefly 5197157a97 添加搜索功能 2025-01-03 17:08:19 +08:00
Glavo d61826ba4c 优化 Release Note (#3516) 2025-01-01 05:37:30 +08:00
Glavo 85886da770 Bump Mesa to 24.3.2 (#3515)
* Bump Mesa to 24.3.2

* update
2025-01-01 05:14:14 +08:00
Haowei Wen 6f53da159a Refine DownloadPage
* Fix cursor is not hand when hovering on ModItem
* Fix clicking on the ModItem in ModVersion dialog would open a new dialog again
* Fix ESC doesn't work in ModVersion dialog
2024-12-31 19:56:38 +08:00
Haowei Wen 78e15d130b Update memory status every 1s, fix #3440
This commit also fixes Lang.getTimer() returns a non-daemon Timer,
preventing Java from exiting after closing the launcher.
2024-12-30 20:14:04 +08:00
Haowei Wen 7d12ef6b09 Fix memory slider indicator is covered by progress bar 2024-12-30 19:39:42 +08:00
Haowei Wen 260f4b6c5d Fix ESC not working in input dialog 2024-12-30 00:17:00 +08:00
Haowei Wen 104534d12f Fix style 2024-12-30 00:07:43 +08:00
Haowei Wen 81b3911dc9 Fix value in text field might not be saved when pressing ESC
https://github.com/HMCL-dev/HMCL/pull/1869#issuecomment-2335027530
2024-12-30 00:02:32 +08:00
Haowei Wen c5b8aabc57 Refactor VersionSetting.clone() & .addListener(), fix javaVersionType is not saved 2024-12-29 22:36:40 +08:00
Haowei Wen f0a064afdd Fix sometimes version specific setting is lost 2024-12-29 22:18:15 +08:00
Haowei Wen 8a4f6af193 Fix NPE due to the order of attaching specificSettingsListener 2024-12-29 21:40:09 +08:00
Haowei Wen e63ee49eaa Remove VersionSetting.global property as it's unused 2024-12-29 20:59:47 +08:00
Haowei Wen d5408c38b3 Copy global settings when specialize version setting 2024-12-29 20:18:58 +08:00
Haowei Wen 5fd52bb808 Move skin cache directory to hmcl dir, close #2970 2024-12-29 18:28:23 +08:00
白铭骢 (Mingcong Bai) cb2b3525b8 fix(DefaultLauncher): also set LIBGL_KOPPER_DRI2=1 for Zink mode (#3503)
The amdgpu DDX is missing support for modifiers, causing Zink to fail.
Disable DRI3 to workaround this issue.

Link: https://gitlab.freedesktop.org/mesa/mesa/-/issues/10093

Signed-off-by: Mingcong Bai <jeffbai@aosc.io>
2024-12-25 19:55:42 +08:00
Glavo 4c6fdf49de Bump JetBrains Annotations to 26.0.1 (#3493) 2024-12-13 00:29:34 +08:00
Burning_TNT 981b55a10e Fix #3456 在更新预览界面没法拖动启动器窗口 (#3464) 2024-12-10 16:20:02 +08:00
Glavo 8460df7d5d 修复 OAuth 登录时轮询过于频繁的问题 (#3490)
* 修复 OAuth 登录时轮询过于频繁的问题

* update
2024-12-09 17:47:42 +08:00
Glavo 727e6746d2 Update versions.txt (#3484)
* Update versions.txt

* update
2024-12-04 12:29:17 +08:00
Glavo 8e0f0e2a38 使用 ONE_PASS_BOX 阴影代替高斯模糊 (#3458)
* 使用 ONE_PASS_BOX 代替高斯模糊

* update

* update

* update
2024-12-02 21:24:58 +08:00
Glavo 3df52b0e8f Fix typo in check-update.yml (#3479) 2024-11-29 20:06:08 +08:00
741 changed files with 71986 additions and 17443 deletions

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: QQ 群
url: https://docs.hmcl.net/groups.html
about: Hello Minecraft! Launcher 的官方 QQ 交流群。
- name: Discord 服务器
url: https://discord.gg/jVvC7HfM6U
about: Hello Minecraft! Launcher 的官方 Discord 服务器。
+1
View File
@@ -1071,6 +1071,7 @@ ij_jsp_keep_indents_on_empty_lines = false
ij_jspx_keep_indents_on_empty_lines = false
[{*.markdown,*.md}]
max_line_length = 200
ij_markdown_force_one_space_after_blockquote_symbol = true
ij_markdown_force_one_space_after_header_symbol = true
ij_markdown_force_one_space_after_list_bullet = true
+23 -27
View File
@@ -1,6 +1,6 @@
name: Bug 反馈 | Bug Feedback
name: Bug 反馈 | Bug Report
description:
反馈一个 HMCL 错误。| Bug Feedback.
反馈一个 HMCL 错误。| File a bug report for HMCL.
title: "[Bug] "
labels: bug
body:
@@ -8,29 +8,25 @@ body:
attributes:
value: |
提交前请确认:
* 该问题确实是 **HMCL 的错误**,而**不是 Minecraft 非正常退出**,如果你的 Minecraft 非正常退出,请前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 频道](https://discord.gg/jVvC7HfM6U) 中获取帮助。
* 你的启动器版本是**最新的快照版本**,可以点击 [此处](https://zkitefly.github.io/HMCL-Snapshot-Update/) 下载最新快照版本。
如果你的问题并不属于上述两类,你可以选取另一种 Issue 类型,或者直接前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 频道](https://discord.gg/jVvC7HfM6U) 中获取帮助。
* 该问题确实是 **HMCL 的错误**,而**不是 Minecraft 非正常退出**,如果你的 Minecraft 非正常退出,请前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 服务器](https://discord.gg/jVvC7HfM6U) 获取帮助。
* 你的启动器版本是**最新的预览版本**,可以点击 [此处](https://zkitefly.github.io/HMCL-Snapshot-Update/) 下载最新预览版本。
如果你的问题并不属于上述两类,你可以选取另一种 Issue 类型,或者直接前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 服务器](https://discord.gg/jVvC7HfM6U) 获取帮助。
Before submitting, please confirm:
* The issue is indeed **an error of HMCL**, not **Minecraft abnormal exit**. If your Minecraft exits abnormally, please go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord channel](https://discord.gg/jVvC7HfM6U) for help.
* Your launcher version is **the latest snapshot version**. You can click [here](https://github.com/burningtnt/HMCL-Snapshot-Update/raw/master/datas/HMCL-dev.jar) to download the latest snapshot version.
If your issue does not fall into the above two categories, you can choose another type of issue or directly go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord channel](https://discord.gg/jVvC7HfM6U) for help.
- type: input
id: platform
attributes:
label: 平台 | Platform
description: 请输入您遇到 BUG 的平台。 Please enter the platform on which you encountered the bug.
placeholder: e.g. Windows 11
validations:
required: true
* The issue is indeed **a bug of HMCL**, not **Minecraft abnormal exit**. If your Minecraft exits abnormally, please go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord server](https://discord.gg/jVvC7HfM6U) for help.
* Your launcher is the **latest nightly build**. You can click [here](https://zkitefly.github.io/HMCL-Snapshot-Update/en) to download the latest nightly build.
If your issue does not fall into the above two categories, you can choose another type of issue or directly go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord server](https://discord.gg/jVvC7HfM6U) for help.
- type: textarea
id: bug-report
attributes:
label: 问题描述 | Problem Description
description: 请尽可能地详细描述你所遇到的问题,并描述如何重新触发这个问题。 Please describe the problem you met in as much detail as possible. Besides, please describe how to trigger this problem again.
label: 问题描述 | Bug Description
description: |
请尽可能地详细描述你所遇到的问题,并描述如何重新触发这个问题。
Please describe the bug you met in as much detail as possible. Additionally, describe the steps to reproduce this bug.
placeholder: |
1. 点击 HMCL 上的某个按钮 | Click a button named ...
2. 向下翻页 | Scroll down
@@ -42,12 +38,12 @@ body:
attributes:
label: 启动器崩溃报告 / 启动器日志文件 | Launcher Crash Report / Launcher Log File
description: |
如果您的启动器崩溃了,请将崩溃报告填入(或将文件拖入)下方。
如果您的启动器没有崩溃,请在遇到问题后不要退出启动器,在启动器的【设置】>【通用】>【调试】一栏中点击“导出启动器日志”,并将导出的日志拖到下方的输入栏中。
如果你的启动器崩溃了,请将崩溃报告填入(或将文件拖入)下方。
如果你的启动器没有崩溃,请在遇到问题后**不要退出启动器**,在启动器的 “设置 → 通用 → 调试” 一栏中点击 “导出启动器日志”,并将导出的日志拖到下方的输入栏中。
**请注意:启动器崩溃报告或日志文件是诊断问题的重要依据,请务必上传!**
If your launcher crashes, please fill it in (or drag the file in) below.
If your launcher doesn't crash, please DO NOT EXIT your launcher, click "Export Launcher Log" in the [Settings] > [General] > [Debug] column of the launcher, and drag the exported log to the input field below.
**Attention: The crash report or the log file is the key to solve the problem. Please update it!**
If your launcher crashes, please fill in (or drag the file in) the following input field with the crash report.
If your launcher does not crash, please DO NOT EXIT your launcher, click "Export Launcher Logs" in the "Settings → General → Debug" of the launcher, and drag the exported log to the following input field.
**ATTENTION: The crash report or log file is the key to resolving the bug. Please upload them!**
validations:
required: true
required: true
+4 -4
View File
@@ -1,11 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: QQ 群 | QQ Groups
- name: QQ 群 | QQ Group
url: https://docs.hmcl.net/groups.html
about: Hello Minecraft! Launcher 的官方 QQ 交流群。| The official QQ groups of Hello Minecraft! Laucher.
- name: Discord 频道 | Discord Channel
about: Hello Minecraft! Launcher 的官方 QQ 交流群。| The official QQ group of Hello Minecraft! Launcher.
- name: Discord 服务器 | Discord Server
url: https://discord.gg/jVvC7HfM6U
about: Hello Minecraft! Launcher 的官方 Discord 频道。| The official Discord channel of Hello Minecraft! Launcher.
about: Hello Minecraft! Launcher 的官方 Discord 服务器。| The official Discord server of Hello Minecraft! Launcher.
- name: 其他反馈 | Others
url: https://github.com/HMCL-dev/HMCL/discussions/new/choose
about: 通过 Discussions 反馈其他问题。| Report other problems in Discussions.
+19 -6
View File
@@ -1,22 +1,35 @@
name: 新功能 | Feature Request
description: 为 HMCL 提出新功能 | A new feature for HMCL
description: 为 HMCL 提出新功能。| Suggest a new feature or enhancement for HMCL.
title: "[Feature] "
labels: enhancement
body:
- type: markdown
attributes:
value: 请确认 Issues 列表无重复的项目。| Please make sure that no duplicated issues has already been delivered.
value: |
请确认 Issues 列表无重复的项目。
Please make sure that no duplicate issues have already been submitted.
- type: textarea
id: description
id: summary
attributes:
label: 描述 | Description
description: 请详细描述你想加入的新功能。| Please describe the new feature detaily.
label: 概述 | Summary
description: |
请介绍你想加入的新功能。
Please describe the new feature.
validations:
required: true
- type: textarea
id: reason
attributes:
label: 原因 | Reason
description: 请描述该功能带来的好处及原因。| Please describe why you want to add the feature into HMCL.
description: |
请描述该功能带来的好处及原因。
Please describe why you want to add the feature or enhancement to HMCL.
validations:
required: true
- type: textarea
id: description
attributes:
label: 详情 | Description
description: |
在这里可以补充描述该功能的具体实现方式或建议。(可选)
Describe implementation details or suggestions here. (Optional)
-46
View File
@@ -1,46 +0,0 @@
name: Build HMCLauncher
on:
push:
paths:
- 'HMCLauncher/**'
- '.github/workflows/build-launcher.yml'
pull_request:
paths:
- 'HMCLauncher/**'
- '.github/workflows/build-launcher.yml'
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v2
with:
msbuild-architecture: x86
- name: Build HMCLauncher
run: msbuild /p:Configuration=Release /t:Rebuild /verbosity:detailed .\HMCLauncher\
- name: Copy HMCLauncher to assets
run: Copy-Item .\HMCLauncher\Release\HMCLauncher.exe -Destination .\HMCL\src\main\resources\assets\HMCLauncher.exe
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '11'
java-package: 'jdk+fx'
- name: Build with Gradle
run: .\gradlew makeExecutables --no-daemon
env:
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
MICROSOFT_AUTH_SECRET: ${{ secrets.MICROSOFT_AUTH_SECRET }}
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
- name: Get short SHA
run: echo "SHORT_SHA=$("${{ github.sha }}".SubString(0, 7))" >> $env:GITHUB_ENV
- name: Copy HMCLauncher to libs
run: Copy-Item .\HMCLauncher\Release\HMCLauncher.exe -Destination .\HMCL\build\libs\HMCLauncher.exe
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: HMCLauncher-${{ env.SHORT_SHA }}
path: HMCL/build/libs/*.exe
+29
View File
@@ -0,0 +1,29 @@
name: Check Codes
on:
push:
paths:
- '**.java'
- '**.properties'
pull_request:
paths:
- '**.java'
- '**.properties'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
java-package: 'jdk+fx'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
cache-cleanup: never
- name: Check Codes
run: ./gradlew checkstyle checkTranslations --no-daemon --parallel --stacktrace
-25
View File
@@ -1,25 +0,0 @@
name: Check Style
on:
push:
paths:
- '**.java'
pull_request:
paths:
- '**.java'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '11'
java-package: 'jdk+fx'
- name: Check style main
run: ./gradlew checkstyleMain --no-daemon
- name: Check style test
run: ./gradlew checkstyleTest --no-daemon
-23
View File
@@ -1,23 +0,0 @@
name: Check Translations
on:
push:
paths:
- '**.properties'
pull_request:
paths:
- '**.properties'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '11'
java-package: 'jdk+fx'
- name: Check style test
run: ./gradlew checkTranslations --no-daemon
-140
View File
@@ -1,140 +0,0 @@
name: Check Update
on:
workflow_dispatch:
schedule:
- cron: '30 * * * *'
permissions:
contents: write
jobs:
dev-check-update:
if: ${{ github.repository_owner == 'HMCL-dev' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Fetch tags
run: git fetch --all --tags
- name: Install tools
run: sudo apt-get install -y jq
- name: Fetch last version
run: |
wget -O ci.json https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/api/json
export HMCL_EXE_FILE_NAME=`cat ci.json | jq -M -r '.artifacts[] | select(.fileName | endswith(".exe")) | .fileName'`
if [ -z `echo $HMCL_EXE_FILE_NAME | grep -E "^HMCL-[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?\.exe\$"` ]; then exit 1; fi
export HMCL_VERSION=`echo "${HMCL_EXE_FILE_NAME%.exe}" | tail -c +6`
export HMCL_COMMIT_SHA=`cat ci.json | jq -M -r '.actions[] | select(._class == "hudson.plugins.git.util.BuildData") | .lastBuiltRevision.SHA1'`
if [ "${#HMCL_COMMIT_SHA}" != 40 ]; then exit 1; fi
echo "HMCL_VERSION=$HMCL_VERSION" >> $GITHUB_ENV
echo "HMCL_COMMIT_SHA=$HMCL_COMMIT_SHA" >> $GITHUB_ENV
echo "HMCL_TAG_NAME=v$HMCL_VERSION" >> $GITHUB_ENV
- name: Check for existing tags
run: if [ -z "$(git tag -l "$HMCL_TAG_NAME")" ]; then echo "continue=true" >> $GITHUB_ENV; fi
- name: Download artifacts
if: ${{ env.continue == 'true' }}
run: |
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe.sha256"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha256"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh.sha256"
env:
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/artifact/HMCL/build/libs
- name: Generate release note
if: ${{ env.continue == 'true' }}
run: |
echo "The full changelogs can be found on the website: https://docs.hmcl.net/changelog/dev.html" >> RELEASE_NOTE
echo "Notice: changelogs are written in Chinese." >> RELEASE_NOTE
echo "" >> RELEASE_NOTE
echo "| File Name | SHA-256 Checksum |" >> RELEASE_NOTE
echo "| --- | --- |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.exe]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | $(cat HMCL-$HMCL_VERSION.exe.sha256) |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.jar]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | $(cat HMCL-$HMCL_VERSION.jar.sha256) |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.sh]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | $(cat HMCL-$HMCL_VERSION.sh.sha256) |" >> RELEASE_NOTE
env:
GH_DOWNLOAD_BASE_URL: https://github.com/HMCL-dev/HMCL/releases/download
- name: Create release
if: ${{ env.continue == 'true' }}
uses: softprops/action-gh-release@v2
with:
body_path: RELEASE_NOTE
files: |
HMCL-${{ env.HMCL_VERSION }}.exe
HMCL-${{ env.HMCL_VERSION }}.jar
HMCL-${{ env.HMCL_VERSION }}.sh
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
name: ${{ env.HMCL_TAG_NAME }}
tag_name: ${{ env.HMCL_TAG_NAME }}
prerelease: ture
stable-check-update:
if: ${{ github.repository_owner == 'HMCL-dev' }}
needs: dev-check-update
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Fetch tags
run: git fetch --all --tags
- name: Install tools
run: sudo apt-get install -y jq
- name: Fetch last version
run: |
wget -O ci.json https://ci.huangyuhui.net/job/HMCL-stable/lastSuccessfulBuild/api/json
export HMCL_EXE_FILE_NAME=`cat ci.json | jq -M -r '.artifacts[] | select(.fileName | endswith(".exe")) | .fileName'`
if [ -z `echo $HMCL_EXE_FILE_NAME | grep -E "^HMCL-[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?\.exe\$"` ]; then exit 1; fi
export HMCL_VERSION=`echo "${HMCL_EXE_FILE_NAME%.exe}" | tail -c +6`
export HMCL_COMMIT_SHA=`cat ci.json | jq -M -r '.actions[] | select(._class == "hudson.plugins.git.util.BuildData") | .lastBuiltRevision.SHA1'`
if [ "${#HMCL_COMMIT_SHA}" != 40 ]; then exit 1; fi
echo "HMCL_VERSION=$HMCL_VERSION" >> $GITHUB_ENV
echo "HMCL_COMMIT_SHA=$HMCL_COMMIT_SHA" >> $GITHUB_ENV
echo "HMCL_TAG_NAME=release-$HMCL_VERSION" >> $GITHUB_ENV
- name: Check for existing tags
run: if ! git tag -l | grep -q "$HMCL_TAG_NAME"; then echo "continue=true" >> $GITHUB_ENV; fi
- name: Download artifacts
if: ${{ env.continue == 'true' }}
run: |
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe.sha256"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha256"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh.sha256"
env:
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL-stable/lastSuccessfulBuild/artifact/HMCL/build/libs
- name: Generate release note
if: ${{ env.continue == 'true' }}
run: |
echo "**This version is a stable version.**" >> RELEASE_NOTE
echo "" >> RELEASE_NOTE
echo "The full changelogs can be found on the website: https://docs.hmcl.net/changelog/stable.html" >> RELEASE_NOTE
echo "" >> RELEASE_NOTE
echo "Notice: changelogs are written in Chinese." >> RELEASE_NOTE
echo "" >> RELEASE_NOTE
echo "| File Name | SHA-256 Checksum |" >> RELEASE_NOTE
echo "| --- | --- |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.exe]($GH_DOWNLOAD_BASE_URL/release-$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | $(cat HMCL-$HMCL_VERSION.exe.sha256) |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.jar]($GH_DOWNLOAD_BASE_URL/release-$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | $(cat HMCL-$HMCL_VERSION.jar.sha256) |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.sh]($GH_DOWNLOAD_BASE_URL/release-$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | $(cat HMCL-$HMCL_VERSION.sh.sha256) |" >> RELEASE_NOTE
env:
GH_DOWNLOAD_BASE_URL: https://github.com/HMCL-dev/HMCL/releases/download
- name: Create release
if: ${{ env.continue == 'true' }}
uses: softprops/action-gh-release@v2
with:
body_path: RELEASE_NOTE
files: |
HMCL-${{ env.HMCL_VERSION }}.exe
HMCL-${{ env.HMCL_VERSION }}.jar
HMCL-${{ env.HMCL_VERSION }}.sh
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
name: ${{ env.HMCL_TAG_NAME }}
tag_name: ${{ env.HMCL_TAG_NAME }}
-21
View File
@@ -1,21 +0,0 @@
name: Sync to Gitee
on:
push
jobs:
run:
if: ${{ github.repository_owner == 'HMCL-dev' }}
runs-on: ubuntu-latest
steps:
- name: Mirror GitHub to Gitee
uses: Yikun/hub-mirror-action@v1.4
with:
src: github/HMCL-dev
dst: gitee/huanghongxun
static_list: 'HMCL'
force_update: true
debug: true
dst_key: ${{ secrets.GITEE_SYNC_BOT_PRIVATE_KEY }}
dst_token: ${{ secrets.GITEE_SYNC_BOT_TOKEN }}
cache_path: /github/workspace/hub-mirror-cache
+26 -7
View File
@@ -5,7 +5,6 @@ on:
pull_request:
paths-ignore:
- '**.md'
- 'HMCLauncher/**'
jobs:
build:
@@ -14,22 +13,42 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up JDK 8
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: 8
java-version: '17'
java-package: 'jdk+fx'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
cache-cleanup: never
- name: Build with Gradle
run: ./gradlew build --no-daemon
run: ./gradlew build --no-daemon --parallel
env:
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
MICROSOFT_AUTH_SECRET: ${{ secrets.MICROSOFT_AUTH_SECRET }}
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
- name: Get short SHA
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
- name: Upload Artifacts
- name: Upload JAR
uses: actions/upload-artifact@v4
with:
name: HMCL-${{ env.SHORT_SHA }}
path: HMCL/build/libs
name: HMCL-${{ env.SHORT_SHA }}-jar
path: |
HMCL/build/libs/HMCL-*.jar
HMCL/build/libs/HMCL-*.jar.sha256
- name: Upload EXE
uses: actions/upload-artifact@v4
with:
name: HMCL-${{ env.SHORT_SHA }}-exe
path: |
HMCL/build/libs/HMCL-*.exe
HMCL/build/libs/HMCL-*.exe.sha256
- name: Upload SH
uses: actions/upload-artifact@v4
with:
name: HMCL-${{ env.SHORT_SHA }}-sh
path: |
HMCL/build/libs/HMCL-*.sh
HMCL/build/libs/HMCL-*.sh.sha256
+32
View File
@@ -0,0 +1,32 @@
name: Mirror Repository
on:
workflow_dispatch:
push:
concurrency:
group: mirror-repository
cancel-in-progress: true
jobs:
mirror:
strategy:
matrix:
target:
- name: Gitee
repo: gitee.com/huanghongxun/HMCL
user: 'hmcl-sync'
token: 'GITEE_SYNC_TOKEN'
- name: CNB
repo: cnb.cool/HMCL-dev/HMCL
user: 'cnb'
token: 'CNB_SYNC_TOKEN'
name: Mirror to ${{ matrix.target.name }}
if: ${{ github.repository == 'HMCL-dev/HMCL' }}
runs-on: ubuntu-latest
steps:
- name: Mirror GitHub to ${{ matrix.target.name }}
run: |
git clone --mirror "https://github.com/$GITHUB_REPOSITORY.git" -- repo
cd repo
git push -f --prune "https://${{ matrix.target.user }}:${{ secrets[matrix.target.token] }}@${{ matrix.target.repo }}.git" "refs/heads/*:refs/heads/*" "refs/tags/*:refs/tags/*"
+125
View File
@@ -0,0 +1,125 @@
name: Create Release
on:
workflow_dispatch:
# schedule:
# - cron: '30 * * * *'
permissions:
contents: write
jobs:
check-update:
if: ${{ github.repository_owner == 'HMCL-dev' }}
strategy:
fail-fast: false
max-parallel: 1
matrix:
include:
- channel: dev
task: checkUpdateDev
- channel: stable
task: checkUpdateStable
runs-on: ubuntu-latest
name: check-update-${{ matrix.channel }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-tags: true
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '25'
- name: Fetch last version
run: ./gradlew ${{ matrix.task }} --no-daemon --info --stacktrace
- name: Check for existing tags
run: if [ -z "$(git tag -l "$HMCL_TAG_NAME")" ]; then echo "continue=true" >> $GITHUB_ENV; fi
- name: Download artifacts
if: ${{ env.continue == 'true' }}
run: |
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.exe"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.exe.sha256"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar.sha256"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh.sha256"
- name: Verify artifacts
if: ${{ env.continue == 'true' }}
run: |
export JAR_SHA256=$(cat HMCL-$HMCL_VERSION.jar.sha256 | tr -d '\n')
export EXE_SHA256=$(cat HMCL-$HMCL_VERSION.exe.sha256 | tr -d '\n')
export SH_SHA256=$(cat HMCL-$HMCL_VERSION.sh.sha256 | tr -d '\n')
echo "$JAR_SHA256 HMCL-$HMCL_VERSION.jar" | sha256sum -c
echo "$EXE_SHA256 HMCL-$HMCL_VERSION.exe" | sha256sum -c
echo "$SH_SHA256 HMCL-$HMCL_VERSION.sh" | sha256sum -c
- name: Generate release note
if: ${{ env.continue == 'true' }}
run: |
# GitHub Release Note
echo "&nbsp;**[Changelog](https://docs.hmcl.net/changelog/${{ matrix.channel }}.html#HMCL-$HMCL_VERSION)** (Chinese)" >> RELEASE_NOTE
echo "" >> RELEASE_NOTE
echo "| File | SHA-256 Checksum |" >> RELEASE_NOTE
echo "| --- | --- |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.exe]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.jar]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.sh]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> RELEASE_NOTE
# CNB Release Note
echo "[更新日志](https://docs.hmcl.net/changelog/${{ matrix.channel }}.html#HMCL-$HMCL_VERSION)" >> CNB_RELEASE_NOTE
echo "" >> CNB_RELEASE_NOTE
echo "| 文件 | SHA-256 校验码 |" >> CNB_RELEASE_NOTE
echo "| :--- | --- |" >> CNB_RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.exe]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> CNB_RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.jar]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> CNB_RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.sh]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> CNB_RELEASE_NOTE
env:
GH_DOWNLOAD_BASE_URL: https://github.com/${{ github.repository }}/releases/download
CNB_DOWNLOAD_BASE_URL: https://cnb.cool/HMCL-dev/HMCL/-/releases/download
- name: Create GitHub release
if: ${{ env.continue == 'true' }}
run: |
gh release create "${{ env.HMCL_TAG_NAME }}" \
"HMCL-${{ env.HMCL_VERSION }}.exe" \
"HMCL-${{ env.HMCL_VERSION }}.jar" \
"HMCL-${{ env.HMCL_VERSION }}.sh" \
--target "${{ env.HMCL_COMMIT_SHA }}" \
--title "${{ env.HMCL_TAG_NAME }}" \
--notes-file RELEASE_NOTE \
--prerelease
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install git-cnb
if: ${{ env.continue == 'true' }}
run: go install "cnb.cool/looc/git-cnb@$GIT_CNB_VERSION"
env:
GIT_CNB_VERSION: '1.1.2'
- name: Create CNB release
if: ${{ env.continue == 'true' }}
run: |
echo "Uploading tags to CNB"
git fetch --tags
git push "https://cnb:${{ secrets.CNB_SYNC_TOKEN }}@cnb.cool/$CNB_REPO.git" "$HMCL_TAG_NAME"
echo "Creating CNB release"
~/go/bin/git-cnb release create \
--repo "$CNB_REPO" \
--tag "$HMCL_TAG_NAME" \
--name "HMCL $HMCL_VERSION" \
--body "$(cat CNB_RELEASE_NOTE)" \
--prerelease true
echo "Uploading HMCL-$HMCL_VERSION.jar"
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "$HMCL_TAG_NAME" --file-name "HMCL-$HMCL_VERSION.jar"
echo "Uploading HMCL-$HMCL_VERSION.exe"
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "$HMCL_TAG_NAME" --file-name "HMCL-$HMCL_VERSION.exe"
echo "Uploading HMCL-$HMCL_VERSION.sh"
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "$HMCL_TAG_NAME" --file-name "HMCL-$HMCL_VERSION.sh"
env:
CNB_TOKEN: ${{ secrets.CNB_SYNC_TOKEN }}
CNB_REPO: HMCL-dev/HMCL
+14 -2
View File
@@ -2,6 +2,7 @@
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
*.hprof
.gradle
@@ -13,13 +14,22 @@ hs_err_pid*
/externalgames
NVIDIA
minecraft-exported-crash-info*
hmcl-exported-logs-*
/.java/
/.local/
/.cache/
# IANA Language Subtag Registry
language-subtag-registry
# gradle build
/build/
/HMCL/build/
/HMCLCore/build/
/HMCLTransformerDiscoveryService/build/
/HMCLBoot/build/
/minecraft/libraries/HMCLTransformerDiscoveryService/build/
/minecraft/libraries/HMCLMultiMCBootstrap/build/
/buildSrc/build/
# idea
.idea
@@ -27,12 +37,14 @@ minecraft-exported-crash-info*
/HMCL/out/
/HMCLCore/out/
/minecraft/libraries/HMCLTransformerDiscoveryService/out/
/minecraft/libraries/HMCLMultiMCBootstrap/out/
# eclipse
/bin/
/HMCL/bin/
/HMCLCore/bin/
/minecraft/libraries/HMCLTransformerDiscoveryService/bin/
/minecraft/libraries/HMCLMultiMCBootstrap/bin/
.classpath
.project
.settings
@@ -41,7 +53,6 @@ minecraft-exported-crash-info*
.nb-gradle
*.exe
!/HMCL/src/main/resources/assets/HMCLauncher.exe
# macos
.DS_Store
@@ -52,3 +63,4 @@ minecraft-exported-crash-info*
# test
/hmcl.json
/.hmcl.json
/.hmcl/
+3 -1
View File
@@ -1,2 +1,4 @@
/data.csv
/data.json
/data.json
/mod.json
/modpack.json
+235 -81
View File
@@ -1,3 +1,11 @@
import org.jackhuang.hmcl.gradle.ci.GitHubActionUtils
import org.jackhuang.hmcl.gradle.ci.JenkinsUtils
import org.jackhuang.hmcl.gradle.l10n.CheckTranslations
import org.jackhuang.hmcl.gradle.l10n.CreateLanguageList
import org.jackhuang.hmcl.gradle.l10n.CreateLocaleNamesResourceBundle
import org.jackhuang.hmcl.gradle.l10n.UpsideDownTranslate
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
import java.net.URI
import java.nio.file.FileSystems
import java.nio.file.Files
@@ -8,36 +16,55 @@ import java.security.spec.PKCS8EncodedKeySpec
import java.util.zip.ZipFile
plugins {
id("com.github.johnrengelman.shadow") version "7.1.2"
alias(libs.plugins.shadow)
}
val isOfficial = System.getenv("HMCL_SIGNATURE_KEY") != null
|| (System.getenv("GITHUB_REPOSITORY_OWNER") == "HMCL-dev" && System.getenv("GITHUB_BASE_REF")
.isNullOrEmpty())
val projectConfig = PropertiesUtils.load(rootProject.file("config/project.properties").toPath())
val isOfficial = JenkinsUtils.IS_ON_CI || GitHubActionUtils.IS_ON_OFFICIAL_REPO
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number ->
val offset = System.getenv("BUILD_NUMBER_OFFSET")?.toInt() ?: 0
if (number != null) {
(number - offset).toString()
} else {
val shortCommit = System.getenv("GITHUB_SHA")?.lowercase()?.substring(0, 7)
val prefix = if (isOfficial) "dev" else "unofficial"
if (!shortCommit.isNullOrEmpty()) "$prefix-$shortCommit" else "SNAPSHOT"
}
}
val versionRoot = System.getenv("VERSION_ROOT") ?: "3.6"
val versionType = System.getenv("VERSION_TYPE") ?: if (isOfficial) "nightly" else "unofficial"
val versionRoot = System.getenv("VERSION_ROOT") ?: projectConfig.getProperty("versionRoot") ?: "3"
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
version = "$versionRoot.$buildNumber"
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE") ?: ""
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt()
if (buildNumber != null) {
version = if (JenkinsUtils.IS_ON_CI && versionType == "dev") {
"$versionRoot.0.$buildNumber"
} else {
"$versionRoot.$buildNumber"
}
} else {
val shortCommit = System.getenv("GITHUB_SHA")?.lowercase()?.substring(0, 7)
version = if (shortCommit.isNullOrBlank()) {
"$versionRoot.SNAPSHOT"
} else if (isOfficial) {
"$versionRoot.dev-$shortCommit"
} else {
"$versionRoot.unofficial-$shortCommit"
}
}
val embedResources by configurations.registering
dependencies {
implementation(project(":HMCLCore"))
implementation(project(":HMCLBoot"))
implementation("libs:JFoenix")
implementation("com.twelvemonkeys.imageio:imageio-webp:3.12.0")
implementation(libs.twelvemonkeys.imageio.webp)
implementation(libs.java.info)
implementation(libs.monet.fx)
if (launcherExe.isBlank()) {
implementation(libs.hmclauncher)
}
embedResources(libs.authlib.injector)
}
fun digest(algorithm: String, bytes: ByteArray): ByteArray = MessageDigest.getInstance(algorithm).digest(bytes)
@@ -81,21 +108,68 @@ fun attachSignature(jar: File) {
}
}
val java11 = sourceSets.create("java11") {
java {
srcDir("src/main/java11")
}
tasks.withType<JavaCompile> {
sourceCompatibility = "17"
targetCompatibility = "17"
}
tasks.getByName<JavaCompile>(java11.compileJavaTaskName) {
if (JavaVersion.current() < JavaVersion.VERSION_11) {
javaCompiler.set(javaToolchains.compilerFor {
languageVersion.set(JavaLanguageVersion.of(11))
})
tasks.checkstyleMain {
// Third-party code is not checked
exclude("**/org/jackhuang/hmcl/ui/image/apng/**")
}
val addOpens = listOf(
"java.base/java.lang",
"java.base/java.lang.reflect",
"java.base/jdk.internal.loader",
"javafx.base/com.sun.javafx.binding",
"javafx.base/com.sun.javafx.event",
"javafx.base/com.sun.javafx.runtime",
"javafx.base/javafx.beans.property",
"javafx.graphics/javafx.css",
"javafx.graphics/javafx.stage",
"javafx.graphics/com.sun.glass.ui",
"javafx.graphics/com.sun.javafx.stage",
"javafx.graphics/com.sun.javafx.util",
"javafx.graphics/com.sun.prism",
"javafx.controls/com.sun.javafx.scene.control",
"javafx.controls/com.sun.javafx.scene.control.behavior",
"javafx.graphics/com.sun.javafx.tk.quantum",
"javafx.controls/javafx.scene.control.skin",
"jdk.attach/sun.tools.attach",
)
tasks.compileJava {
options.compilerArgs.addAll(addOpens.map { "--add-exports=$it=ALL-UNNAMED" })
}
val hmclProperties = buildList {
add("hmcl.version" to project.version.toString())
add("hmcl.add-opens" to addOpens.joinToString(" "))
System.getenv("GITHUB_SHA")?.let {
add("hmcl.version.hash" to it)
}
add("hmcl.version.type" to versionType)
add("hmcl.microsoft.auth.id" to microsoftAuthId)
add("hmcl.microsoft.auth.secret" to microsoftAuthSecret)
add("hmcl.curseforge.apikey" to curseForgeApiKey)
add("hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!)
}
val hmclPropertiesFile = layout.buildDirectory.file("hmcl.properties")
val createPropertiesFile by tasks.registering {
outputs.file(hmclPropertiesFile)
hmclProperties.forEach { (k, v) -> inputs.property(k, v) }
doLast {
val targetFile = hmclPropertiesFile.get().asFile
targetFile.parentFile.mkdir()
targetFile.bufferedWriter().use {
for ((k, v) in hmclProperties) {
it.write("$k=$v\n")
}
}
}
options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
sourceCompatibility = "11"
targetCompatibility = "11"
}
tasks.jar {
@@ -105,7 +179,9 @@ tasks.jar {
val jarPath = tasks.jar.get().archiveFile.get().asFile
tasks.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJar") {
tasks.shadowJar {
dependsOn(createPropertiesFile)
archiveClassifier.set(null as String?)
exclude("**/package-info.class")
@@ -114,41 +190,31 @@ tasks.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("sha
exclude("META-INF/services/javax.imageio.spi.ImageReaderSpi")
exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi")
listOf(
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "linux-*", "darwin-*",
"*-ppc", "*-ppc64le", "*-s390x", "*-armel",
).forEach { exclude("com/sun/jna/$it/**") }
minimize {
exclude(dependency("com.google.code.gson:.*:.*"))
exclude(dependency("net.java.dev.jna:jna:.*"))
exclude(dependency("libs:JFoenix:.*"))
exclude(project(":HMCLBoot"))
}
manifest {
attributes(
"Created-By" to "Copyright(c) 2013-2024 huangyuhui.",
"Main-Class" to "org.jackhuang.hmcl.Main",
"Multi-Release" to "true",
"Implementation-Version" to project.version,
"Microsoft-Auth-Id" to microsoftAuthId,
"Microsoft-Auth-Secret" to microsoftAuthSecret,
"CurseForge-Api-Key" to curseForgeApiKey,
"Build-Channel" to versionType,
"Class-Path" to "pack200.jar",
"Add-Opens" to listOf(
"java.base/java.lang",
"java.base/java.lang.reflect",
"java.base/jdk.internal.loader",
"javafx.base/com.sun.javafx.binding",
"javafx.base/com.sun.javafx.event",
"javafx.base/com.sun.javafx.runtime",
"javafx.graphics/javafx.css",
"javafx.graphics/com.sun.javafx.stage",
"javafx.graphics/com.sun.prism",
"javafx.controls/com.sun.javafx.scene.control",
"javafx.controls/com.sun.javafx.scene.control.behavior",
"javafx.controls/javafx.scene.control.skin",
"jdk.attach/sun.tools.attach"
).joinToString(" ")
)
manifest.attributes(
"Created-By" to "Copyright(c) 2013-2025 huangyuhui.",
"Implementation-Version" to project.version.toString(),
"Main-Class" to "org.jackhuang.hmcl.Main",
"Multi-Release" to "true",
"Add-Opens" to addOpens.joinToString(" "),
"Enable-Native-Access" to "ALL-UNNAMED",
"Enable-Final-Field-Mutation" to "ALL-UNNAMED",
)
System.getenv("GITHUB_SHA")?.also {
attributes("GitHub-SHA" to it)
if (launcherExe.isNotBlank()) {
into("assets") {
from(file(launcherExe))
}
}
@@ -158,29 +224,49 @@ tasks.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("sha
}
}
fun createExecutable(suffix: String, header: String) {
val output = File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + suffix)
output.outputStream().use {
it.write(File(project.projectDir, header).readBytes())
it.write(jarPath.readBytes())
}
createChecksum(output)
}
tasks.processResources {
into("META-INF/versions/11") {
from(sourceSets["java11"].output)
dependsOn(createPropertiesFile)
dependsOn(upsideDownTranslate)
dependsOn(createLocaleNamesResourceBundle)
dependsOn(createLanguageList)
into("assets/") {
from(hmclPropertiesFile)
from(embedResources)
}
into("assets/lang") {
from(createLanguageList.map { it.outputFile })
from(upsideDownTranslate.map { it.outputFile })
from(createLocaleNamesResourceBundle.map { it.outputDirectory })
}
dependsOn(tasks["java11Classes"])
}
val makeExecutables = tasks.create("makeExecutables") {
val makeExecutables by tasks.registering {
val extensions = listOf("exe", "sh")
dependsOn(tasks.jar)
inputs.file(jarPath)
outputs.files(extensions.map { File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + it) })
doLast {
createExecutable("exe", "src/main/resources/assets/HMCLauncher.exe")
createExecutable("sh", "src/main/resources/assets/HMCLauncher.sh")
val jarContent = jarPath.readBytes()
ZipFile(jarPath).use { zipFile ->
for (extension in extensions) {
val output = File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + extension)
val entry = zipFile.getEntry("assets/HMCLauncher.$extension")
?: throw GradleException("HMCLauncher.$extension not found")
output.outputStream().use { outputStream ->
zipFile.getInputStream(entry).use { it.copyTo(outputStream) }
outputStream.write(jarContent)
}
createChecksum(output)
}
}
}
}
@@ -188,9 +274,9 @@ tasks.build {
dependsOn(makeExecutables)
}
fun parseToolOptions(options: String?): List<String> {
fun parseToolOptions(options: String?): MutableList<String> {
if (options == null)
return listOf()
return mutableListOf()
val builder = StringBuilder()
val result = mutableListOf<String>()
@@ -238,7 +324,17 @@ fun parseToolOptions(options: String?): List<String> {
return result
}
tasks.create<JavaExec>("run") {
// For IntelliJ IDEA
tasks.withType<JavaExec> {
if (name != "run") {
jvmArgs(addOpens.map { "--add-opens=$it=ALL-UNNAMED" })
// if (javaVersion >= JavaVersion.VERSION_24) {
// jvmArgs("--enable-native-access=ALL-UNNAMED")
// }
}
}
tasks.register<JavaExec>("run") {
dependsOn(tasks.jar)
group = "application"
@@ -246,10 +342,68 @@ tasks.create<JavaExec>("run") {
classpath = files(jarPath)
workingDir = rootProject.rootDir
val vmOptions = parseToolOptions(System.getenv("HMCL_JAVA_OPTS"))
val vmOptions = parseToolOptions(System.getenv("HMCL_JAVA_OPTS") ?: "-Xmx1g")
if (vmOptions.none { it.startsWith("-Dhmcl.offline.auth.restricted=") })
vmOptions += "-Dhmcl.offline.auth.restricted=false"
jvmArgs(vmOptions)
val hmclJavaHome = System.getenv("HMCL_JAVA_HOME")
if (hmclJavaHome != null) {
this.executable(
file(hmclJavaHome).resolve("bin")
.resolve(if (System.getProperty("os.name").lowercase().startsWith("windows")) "java.exe" else "java")
)
}
doFirst {
logger.quiet("HMCL_JAVA_OPTS: $vmOptions")
logger.quiet("HMCL_JAVA_OPTS: {}", vmOptions)
logger.quiet("HMCL_JAVA_HOME: {}", hmclJavaHome ?: System.getProperty("java.home"))
}
}
// Check Translations
tasks.register<CheckTranslations>("checkTranslations") {
val dir = layout.projectDirectory.dir("src/main/resources/assets/lang")
englishFile.set(dir.file("I18N.properties"))
simplifiedChineseFile.set(dir.file("I18N_zh_CN.properties"))
traditionalChineseFile.set(dir.file("I18N_zh.properties"))
classicalChineseFile.set(dir.file("I18N_lzh.properties"))
}
// l10n
val generatedDir = layout.buildDirectory.dir("generated")
val upsideDownTranslate by tasks.registering(UpsideDownTranslate::class) {
inputFile.set(layout.projectDirectory.file("src/main/resources/assets/lang/I18N.properties"))
outputFile.set(generatedDir.map { it.file("generated/i18n/I18N_en_Qabs.properties") })
}
val createLanguageList by tasks.registering(CreateLanguageList::class) {
resourceBundleDir.set(layout.projectDirectory.dir("src/main/resources/assets/lang"))
resourceBundleBaseName.set("I18N")
additionalLanguages.set(listOf("en-Qabs"))
outputFile.set(generatedDir.map { it.file("languages.json") })
}
val createLocaleNamesResourceBundle by tasks.registering(CreateLocaleNamesResourceBundle::class) {
dependsOn(createLanguageList)
languagesFile.set(createLanguageList.flatMap { it.outputFile })
outputDirectory.set(generatedDir.map { it.dir("generated/LocaleNames") })
}
// mcmod data
tasks.register<ParseModDataTask>("parseModData") {
inputFile.set(layout.projectDirectory.file("mod.json"))
outputFile.set(layout.projectDirectory.file("src/main/resources/assets/mod_data.txt"))
}
tasks.register<ParseModDataTask>("parseModPackData") {
inputFile.set(layout.projectDirectory.file("modpack.json"))
outputFile.set(layout.projectDirectory.file("src/main/resources/assets/modpack_data.txt"))
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

-123
View File
@@ -1,123 +0,0 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2021 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/>.
#
import json
import codecs
import re
import sys
from urllib.parse import urlparse, parse_qs
S = ';'
MOD_SEPARATOR = ','
CURSEFORGE_PATTERN1 = re.compile(
r'^/(minecraft|Minecraft|minecraft-bedrock)/(mc-mods|modpacks|customization|mc-addons|texture-packs|customization/configuration|addons)/+(?P<modid>[\w-]+)(/(.*?))?$')
CURSEFORGE_PATTERN2 = re.compile(
r'^/projects/(?P<modid>[\w-]+)(/(.*?))?$')
CURSEFORGE_PATTERN3 = re.compile(
r'^/mc-mods/minecraft/(?P<modid>[\w-]+)(/(.*?))?$')
CURSEFORGE_PATTERN4 = re.compile(
r'^/legacy/mc-mods/minecraft/(\d+)-(?P<modid>[\w-]+)'
)
def parseCurseforge(url):
res = urlparse(url)
if res.scheme not in ['http', 'https']:
return ''
for pattern in [CURSEFORGE_PATTERN1, CURSEFORGE_PATTERN2, CURSEFORGE_PATTERN3, CURSEFORGE_PATTERN4]:
match = pattern.match(res.path)
if match:
return match.group('modid')
return ''
MCMOD_PATTERN = re.compile(
r'^http(s)?://www\.mcmod\.cn/(class|modpack)/(?P<modid>\d+)\.html$')
def parseMcmod(url):
match = MCMOD_PATTERN.match(url)
if match:
return match.group('modid')
return ''
skip = [
'Minecraft',
'The Building Game'
]
if __name__ == '__main__':
json_name = sys.argv[1] or 'data.json'
with codecs.open(json_name, mode='r', encoding='utf-8-sig') as jsonfile, codecs.open('data.csv', mode='w', encoding='utf-8') as outfile:
data = json.load(jsonfile)
for mod in data:
chinese_name = mod['name']['main']
sub_name = mod['name']['sub']
abbr = mod['name']['abbr']
if sub_name in skip:
continue
if S in chinese_name:
print('Error! ' + chinese_name)
exit(1)
if S in sub_name:
print('Error! ' + chinese_name)
exit(1)
black_lists = [
'Wood Converter'
]
curseforge_id = ''
mcmod_id = ''
links = mod['links']['list']
if 'curseforge' in links and links['curseforge'] and sub_name not in black_lists and chinese_name not in black_lists:
for link in links['curseforge']:
curseforge_id = parseCurseforge(link['url'])
if curseforge_id != '':
break
if curseforge_id == '':
print('Error curseforge ' + chinese_name)
exit(1)
if 'mcmod' in links and links['mcmod']:
mcmod_id = parseMcmod(links['mcmod'][0]['url'])
if mcmod_id == '':
print('Error mcmod ' + chinese_name)
exit(1)
mod_id = []
if 'modid' in mod and 'list' in mod['modid']:
for id in mod['modid']['list']:
if MOD_SEPARATOR in id:
print('Error mod id!' + id)
exit(1)
mod_id.append(id)
mod_ids = MOD_SEPARATOR.join([str(id) for id in mod_id])
outfile.write(
f'{curseforge_id}{S}{mcmod_id}{S}{mod_ids}{S}{chinese_name}{S}{sub_name}{S}{abbr}\n')
print('Success!')
@@ -0,0 +1,60 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.utils.JFXNodeUtils;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
/**
* JFXClippedPane is a StackPane that clips its content if exceeding the pane bounds.
*
* @author Shadi Shaheen
* @version 1.0
* @since 2018-06-02
*/
public class JFXClippedPane extends StackPane {
private final Region clip = new Region();
public JFXClippedPane() {
super();
init();
}
public JFXClippedPane(Node... children) {
super(children);
init();
}
private void init() {
setClip(clip);
clip.setBackground(new Background(new BackgroundFill(Color.BLACK, new CornerRadii(2), Insets.EMPTY)));
backgroundProperty().addListener(observable -> JFXNodeUtils.updateBackground(getBackground(), clip));
}
@Override
protected void layoutChildren() {
super.layoutChildren();
clip.resizeRelocate(0, 0, getWidth(), getHeight());
}
}
@@ -0,0 +1,145 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.skins.JFXColorPickerSkin;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableBooleanProperty;
import javafx.css.Styleable;
import javafx.css.StyleableBooleanProperty;
import javafx.css.converter.BooleanConverter;
import javafx.scene.control.ColorPicker;
import javafx.scene.control.Skin;
import javafx.scene.paint.Color;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* JFXColorPicker is the metrial design implementation of color picker.
*
* @author Shadi Shaheen
* @version 1.0
* @since 2016-03-09
*/
public class JFXColorPicker extends ColorPicker {
/**
* {@inheritDoc}
*/
public JFXColorPicker() {
initialize();
}
/**
* {@inheritDoc}
*/
public JFXColorPicker(Color color) {
super(color);
initialize();
}
/**
* {@inheritDoc}
*/
@Override
protected Skin<?> createDefaultSkin() {
return new JFXColorPickerSkin(this);
}
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
/**
* Initialize the style class to 'jfx-color-picker'.
* <p>
* This is the selector class from which CSS can be used to style
* this control.
*/
private static final String DEFAULT_STYLE_CLASS = "jfx-color-picker";
private double[] preDefinedColors = null;
public double[] getPreDefinedColors() {
return preDefinedColors;
}
public void setPreDefinedColors(double[] preDefinedColors) {
this.preDefinedColors = preDefinedColors;
}
/**
* disable animation on button action
*/
private final StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION,
JFXColorPicker.this,
"disableAnimation",
false);
public final StyleableBooleanProperty disableAnimationProperty() {
return this.disableAnimation;
}
public final Boolean isDisableAnimation() {
return disableAnimation != null && this.disableAnimationProperty().get();
}
public final void setDisableAnimation(final Boolean disabled) {
this.disableAnimationProperty().set(disabled);
}
private static final class StyleableProperties {
private static final CssMetaData<JFXColorPicker, Boolean> DISABLE_ANIMATION =
new CssMetaData<JFXColorPicker, Boolean>("-jfx-disable-animation",
BooleanConverter.getInstance(), false) {
@Override
public boolean isSettable(JFXColorPicker control) {
return control.disableAnimation == null || !control.disableAnimation.isBound();
}
@Override
public StyleableBooleanProperty getStyleableProperty(JFXColorPicker control) {
return control.disableAnimationProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables =
new ArrayList<>(ColorPicker.getClassCssMetaData());
Collections.addAll(styleables, DISABLE_ANIMATION);
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
}
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
}
@@ -0,0 +1,533 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.controls.events.JFXDialogEvent;
import com.jfoenix.converters.DialogTransitionConverter;
import com.jfoenix.effects.JFXDepthManager;
import com.jfoenix.transitions.CachedTransition;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.animation.Transition;
import javafx.beans.DefaultProperty;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.Styleable;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.CacheHint;
import javafx.scene.Node;
import javafx.scene.SnapshotParameters;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.animation.Motion;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/// Note: for JFXDialog to work properly, the root node **MUST**
/// be of type [StackPane]
///
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2016-03-09
@DefaultProperty(value = "content")
public class JFXDialog extends StackPane {
private static final double INITIAL_SCALE = 0.8;
// public static enum JFXDialogLayout{PLAIN, HEADING, ACTIONS, BACKDROP};
public enum DialogTransition {
CENTER, NONE
}
private StackPane contentHolder;
private double offsetX = 0;
private double offsetY = 0;
private StackPane dialogContainer;
private Region content;
private Transition animation;
private final EventHandler<? super MouseEvent> closeHandler = e -> close();
/// creates empty JFXDialog control with CENTER animation type
public JFXDialog() {
this(null, null, DialogTransition.CENTER);
}
/// creates empty JFXDialog control with a specified animation type
public JFXDialog(DialogTransition transition) {
this(null, null, transition);
}
/// creates JFXDialog control with a specified animation type, the animation type
/// can be one of the following:
///
/// - CENTER
/// - TOP
/// - RIGHT
/// - BOTTOM
/// - LEFT
///
/// @param dialogContainer is the parent of the dialog, it
/// @param content the content of dialog
/// @param transitionType the animation type
public JFXDialog(StackPane dialogContainer, Region content, DialogTransition transitionType) {
initialize();
setContent(content);
setDialogContainer(dialogContainer);
this.transitionType.set(transitionType);
// init change listeners
initChangeListeners();
}
/// creates JFXDialog control with a specified animation type that
/// is closed when clicking on the overlay, the animation type
/// can be one of the following:
///
/// - CENTER
/// - TOP
/// - RIGHT
/// - BOTTOM
/// - LEFT
public JFXDialog(StackPane dialogContainer, Region content, DialogTransition transitionType, boolean overlayClose) {
setOverlayClose(overlayClose);
initialize();
setContent(content);
setDialogContainer(dialogContainer);
this.transitionType.set(transitionType);
// init change listeners
initChangeListeners();
}
private void initChangeListeners() {
overlayCloseProperty().addListener((o, oldVal, newVal) -> {
if (newVal) {
this.addEventHandler(MouseEvent.MOUSE_PRESSED, closeHandler);
} else {
this.removeEventHandler(MouseEvent.MOUSE_PRESSED, closeHandler);
}
});
}
private void initialize() {
this.setVisible(false);
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
this.transitionType.addListener((o, oldVal, newVal) -> {
animation = getShowAnimation(transitionType.get());
});
contentHolder = new StackPane();
contentHolder.setBackground(new Background(new BackgroundFill(Color.WHITE, new CornerRadii(2), null)));
JFXDepthManager.setDepth(contentHolder, 4);
contentHolder.setPickOnBounds(false);
// ensure stackpane is never resized beyond it's preferred size
contentHolder.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
this.getChildren().add(contentHolder);
this.getStyleClass().add("jfx-dialog-overlay-pane");
StackPane.setAlignment(contentHolder, Pos.CENTER);
this.setBackground(new Background(new BackgroundFill(Color.rgb(0, 0, 0, 0.1), null, null)));
// close the dialog if clicked on the overlay pane
if (overlayClose.get()) {
this.addEventHandler(MouseEvent.MOUSE_PRESSED, closeHandler);
}
// prevent propagating the events to overlay pane
contentHolder.addEventHandler(MouseEvent.ANY, Event::consume);
}
/***************************************************************************
* *
* Setters / Getters *
* *
**************************************************************************/
/// @return the dialog container
public StackPane getDialogContainer() {
return dialogContainer;
}
/// set the dialog container
/// Note: the dialog container must be StackPane, its the container for the dialog to be shown in.
public void setDialogContainer(StackPane dialogContainer) {
if (dialogContainer != null) {
this.dialogContainer = dialogContainer;
// FIXME: need to be improved to consider only the parent boundary
offsetX = dialogContainer.getBoundsInLocal().getWidth();
offsetY = dialogContainer.getBoundsInLocal().getHeight();
animation = getShowAnimation(transitionType.get());
}
}
/// @return dialog content node
public Region getContent() {
return content;
}
/// set the content of the dialog
public void setContent(Region content) {
if (content != null) {
this.content = content;
this.content.setPickOnBounds(false);
contentHolder.getChildren().setAll(content);
}
}
/// indicates whether the dialog will close when clicking on the overlay or not
private final BooleanProperty overlayClose = new SimpleBooleanProperty(true);
public final BooleanProperty overlayCloseProperty() {
return this.overlayClose;
}
public final boolean isOverlayClose() {
return this.overlayCloseProperty().get();
}
public final void setOverlayClose(final boolean overlayClose) {
this.overlayCloseProperty().set(overlayClose);
}
/// if sets to true, the content of dialog container will be cached and replaced with an image
/// when displaying the dialog (better performance).
/// this is recommended if the content behind the dialog will not change during the showing
/// period
private final BooleanProperty cacheContainer = new SimpleBooleanProperty(false);
public boolean isCacheContainer() {
return cacheContainer.get();
}
public BooleanProperty cacheContainerProperty() {
return cacheContainer;
}
public void setCacheContainer(boolean cacheContainer) {
this.cacheContainer.set(cacheContainer);
}
/// it will show the dialog in the specified container
public void show(StackPane dialogContainer) {
this.setDialogContainer(dialogContainer);
showDialog();
}
private ArrayList<Node> tempContent;
/**
* show the dialog inside its parent container
*/
public void show() {
this.setDialogContainer(dialogContainer);
showDialog();
}
private void showDialog() {
if (dialogContainer == null) {
throw new RuntimeException("ERROR: JFXDialog container is not set!");
}
if (isCacheContainer()) {
tempContent = new ArrayList<>(dialogContainer.getChildren());
SnapshotParameters snapShotparams = new SnapshotParameters();
snapShotparams.setFill(Color.TRANSPARENT);
WritableImage temp = dialogContainer.snapshot(snapShotparams,
new WritableImage((int) dialogContainer.getWidth(),
(int) dialogContainer.getHeight()));
ImageView tempImage = new ImageView(temp);
tempImage.setCache(true);
tempImage.setCacheHint(CacheHint.SPEED);
dialogContainer.getChildren().setAll(tempImage, this);
} else {
//prevent error if opening an already opened dialog
dialogContainer.getChildren().remove(this);
tempContent = null;
dialogContainer.getChildren().add(this);
}
if (animation != null) {
animation.play();
} else {
setVisible(true);
setOpacity(1);
Event.fireEvent(JFXDialog.this, new JFXDialogEvent(JFXDialogEvent.OPENED));
}
}
/**
* close the dialog
*/
public void close() {
if (animation != null) {
animation.setRate(-2);
animation.play();
animation.setOnFinished(e -> {
closeDialog();
});
} else {
setOpacity(0);
setVisible(false);
closeDialog();
}
}
private void closeDialog() {
resetProperties();
Event.fireEvent(JFXDialog.this, new JFXDialogEvent(JFXDialogEvent.CLOSED));
if (tempContent == null) {
dialogContainer.getChildren().remove(this);
} else {
dialogContainer.getChildren().setAll(tempContent);
}
}
/***************************************************************************
* *
* Transitions *
* *
**************************************************************************/
private Transition getShowAnimation(DialogTransition transitionType) {
Transition animation = null;
if (contentHolder != null) {
animation = switch (transitionType) {
case CENTER -> {
contentHolder.setScaleX(INITIAL_SCALE);
contentHolder.setScaleY(INITIAL_SCALE);
yield new CenterTransition();
}
case NONE -> {
contentHolder.setScaleX(1);
contentHolder.setScaleY(1);
contentHolder.setTranslateX(0);
contentHolder.setTranslateY(0);
yield null;
}
};
}
if (animation != null) {
animation.setOnFinished(finish ->
Event.fireEvent(JFXDialog.this, new JFXDialogEvent(JFXDialogEvent.OPENED)));
}
return animation;
}
private void resetProperties() {
this.setVisible(false);
contentHolder.setTranslateX(0);
contentHolder.setTranslateY(0);
contentHolder.setScaleX(1);
contentHolder.setScaleY(1);
}
private final class CenterTransition extends CachedTransition {
private static final Interpolator INTERPOLATOR = Motion.EMPHASIZED_DECELERATE;
CenterTransition() {
super(contentHolder, new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(contentHolder.scaleXProperty(), INITIAL_SCALE, INTERPOLATOR),
new KeyValue(contentHolder.scaleYProperty(), INITIAL_SCALE, INTERPOLATOR),
new KeyValue(JFXDialog.this.visibleProperty(), false, Motion.LINEAR)
),
new KeyFrame(Duration.millis(10),
new KeyValue(JFXDialog.this.visibleProperty(), true, Motion.LINEAR),
new KeyValue(JFXDialog.this.opacityProperty(), 0, INTERPOLATOR)
),
new KeyFrame(Motion.EXTRA_LONG4,
new KeyValue(contentHolder.scaleXProperty(), 1, INTERPOLATOR),
new KeyValue(contentHolder.scaleYProperty(), 1, INTERPOLATOR),
new KeyValue(JFXDialog.this.visibleProperty(), true, Motion.LINEAR),
new KeyValue(JFXDialog.this.opacityProperty(), 1, INTERPOLATOR)
))
);
// reduce the number to increase the shifting , increase number to reduce shifting
setCycleDuration(Duration.seconds(0.4));
setDelay(Duration.ZERO);
}
}
/***************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
/// Initialize the style class to 'jfx-dialog'.
///
/// This is the selector class from which CSS can be used to style
/// this control.
private static final String DEFAULT_STYLE_CLASS = "jfx-dialog";
/// dialog transition type property, it can be one of the following:
///
/// - CENTER
/// - TOP
/// - RIGHT
/// - BOTTOM
/// - LEFT
/// - NONE
private final StyleableObjectProperty<DialogTransition> transitionType = new SimpleStyleableObjectProperty<>(
StyleableProperties.DIALOG_TRANSITION,
JFXDialog.this,
"dialogTransition",
DialogTransition.CENTER);
public DialogTransition getTransitionType() {
return transitionType == null ? DialogTransition.CENTER : transitionType.get();
}
public StyleableObjectProperty<DialogTransition> transitionTypeProperty() {
return this.transitionType;
}
public void setTransitionType(DialogTransition transition) {
this.transitionType.set(transition);
}
private static final class StyleableProperties {
private static final CssMetaData<JFXDialog, DialogTransition> DIALOG_TRANSITION =
new CssMetaData<JFXDialog, DialogTransition>("-jfx-dialog-transition",
DialogTransitionConverter.getInstance(),
DialogTransition.CENTER) {
@Override
public boolean isSettable(JFXDialog control) {
return control.transitionType == null || !control.transitionType.isBound();
}
@Override
public StyleableProperty<DialogTransition> getStyleableProperty(JFXDialog control) {
return control.transitionTypeProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables =
new ArrayList<>(StackPane.getClassCssMetaData());
Collections.addAll(styleables,
DIALOG_TRANSITION
);
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
}
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
/***************************************************************************
* *
* Custom Events *
* *
**************************************************************************/
private final ObjectProperty<EventHandler<? super JFXDialogEvent>> onDialogClosedProperty = new ObjectPropertyBase<EventHandler<? super JFXDialogEvent>>() {
@Override
protected void invalidated() {
setEventHandler(JFXDialogEvent.CLOSED, get());
}
@Override
public Object getBean() {
return JFXDialog.this;
}
@Override
public String getName() {
return "onClosed";
}
};
/**
* Defines a function to be called when the dialog is closed.
* Note: it will be triggered after the close animation is finished.
*/
public ObjectProperty<EventHandler<? super JFXDialogEvent>> onDialogClosedProperty() {
return onDialogClosedProperty;
}
public void setOnDialogClosed(EventHandler<? super JFXDialogEvent> handler) {
onDialogClosedProperty().set(handler);
}
public EventHandler<? super JFXDialogEvent> getOnDialogClosed() {
return onDialogClosedProperty().get();
}
private final ObjectProperty<EventHandler<? super JFXDialogEvent>> onDialogOpenedProperty = new ObjectPropertyBase<>() {
@Override
protected void invalidated() {
setEventHandler(JFXDialogEvent.OPENED, get());
}
@Override
public Object getBean() {
return JFXDialog.this;
}
@Override
public String getName() {
return "onOpened";
}
};
/**
* Defines a function to be called when the dialog is opened.
* Note: it will be triggered after the show animation is finished.
*/
public ObjectProperty<EventHandler<? super JFXDialogEvent>> onDialogOpenedProperty() {
return onDialogOpenedProperty;
}
public void setOnDialogOpened(EventHandler<? super JFXDialogEvent> handler) {
onDialogOpenedProperty().set(handler);
}
public EventHandler<? super JFXDialogEvent> getOnDialogOpened() {
return onDialogOpenedProperty().get();
}
}
@@ -0,0 +1,800 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.converters.RipplerMaskTypeConverter;
import com.jfoenix.utils.JFXNodeUtils;
import javafx.animation.*;
import javafx.beans.DefaultProperty;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.*;
import javafx.css.converter.BooleanConverter;
import javafx.css.converter.PaintConverter;
import javafx.css.converter.SizeConverter;
import javafx.geometry.Bounds;
import javafx.scene.CacheHint;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.util.Duration;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* JFXRippler is the material design implementation of a ripple effect.
* the ripple effect can be applied to any node in the scene. JFXRippler is
* a {@link StackPane} container that holds a specified node (control node) and a ripple generator.
* <p>
* UPDATE NOTES:
* - fireEventProgrammatically(Event) method has been removed as the ripple controller is
* the control itself, so you can trigger manual ripple by firing mouse event on the control
* instead of JFXRippler
*
* @author Shadi Shaheen
* @version 1.0
* @since 2016-03-09
*/
@DefaultProperty(value = "control")
public class JFXRippler extends StackPane {
public enum RipplerPos {
FRONT, BACK
}
public enum RipplerMask {
CIRCLE, RECT, FIT
}
protected RippleGenerator rippler;
protected Pane ripplerPane;
protected Node control;
protected static final double RIPPLE_MAX_RADIUS = 300;
private boolean enabled = true;
private boolean forceOverlay = false;
private final Interpolator rippleInterpolator = Interpolator.SPLINE(0.0825,
0.3025,
0.0875,
0.9975); //0.1, 0.54, 0.28, 0.95);
/// creates empty rippler node
public JFXRippler() {
this(null, RipplerMask.RECT, RipplerPos.FRONT);
}
/// creates a rippler for the specified control
public JFXRippler(Node control) {
this(control, RipplerMask.RECT, RipplerPos.FRONT);
}
/// creates a rippler for the specified control
///
/// @param pos can be either FRONT/BACK (position the ripple effect infront of or behind the control)
public JFXRippler(Node control, RipplerPos pos) {
this(control, RipplerMask.RECT, pos);
}
/// creates a rippler for the specified control and apply the specified mask to it
///
/// @param mask can be either rectangle/cricle
public JFXRippler(Node control, RipplerMask mask) {
this(control, mask, RipplerPos.FRONT);
}
/// creates a rippler for the specified control, mask and position.
///
/// @param mask can be either rectangle/cricle
/// @param pos can be either FRONT/BACK (position the ripple effect infront of or behind the control)
public JFXRippler(Node control, RipplerMask mask, RipplerPos pos) {
initialize();
setMaskType(mask);
setPosition(pos);
createRippleUI();
setControl(control);
// listen to control position changed
position.addListener(observable -> updateControlPosition());
setPickOnBounds(false);
setCache(true);
setCacheHint(CacheHint.SPEED);
setCacheShape(true);
}
protected final void createRippleUI() {
// create rippler panels
rippler = new RippleGenerator();
ripplerPane = new StackPane();
ripplerPane.setMouseTransparent(true);
ripplerPane.getChildren().add(rippler);
getChildren().add(ripplerPane);
}
/***************************************************************************
* *
* Setters / Getters *
* *
**************************************************************************/
public void setControl(Node control) {
if (control != null) {
this.control = control;
// position control
positionControl(control);
// add control listeners to generate / release ripples
initControlListeners();
}
}
// Override this method to create JFXRippler for a control outside the ripple
protected void positionControl(Node control) {
if (this.position.get() == RipplerPos.BACK) {
getChildren().add(control);
} else {
getChildren().add(0, control);
}
}
protected void updateControlPosition() {
if (this.position.get() == RipplerPos.BACK) {
ripplerPane.toBack();
} else {
ripplerPane.toFront();
}
}
public Node getControl() {
return control;
}
public void setEnabled(boolean enable) {
this.enabled = enable;
}
// methods that can be changed by extending the rippler class
/// generate the clipping mask
///
/// @return the mask node
protected Node getMask() {
double borderWidth = ripplerPane.getBorder() != null ? ripplerPane.getBorder().getInsets().getTop() : 0;
Bounds bounds = control.getBoundsInParent();
double width = control.getLayoutBounds().getWidth();
double height = control.getLayoutBounds().getHeight();
double diffMinX = Math.abs(control.getBoundsInLocal().getMinX() - control.getLayoutBounds().getMinX());
double diffMinY = Math.abs(control.getBoundsInLocal().getMinY() - control.getLayoutBounds().getMinY());
double diffMaxX = Math.abs(control.getBoundsInLocal().getMaxX() - control.getLayoutBounds().getMaxX());
double diffMaxY = Math.abs(control.getBoundsInLocal().getMaxY() - control.getLayoutBounds().getMaxY());
Node mask;
switch (getMaskType()) {
case RECT:
mask = new Rectangle(bounds.getMinX() + diffMinX - snappedLeftInset(),
bounds.getMinY() + diffMinY - snappedTopInset(),
width - 2 * borderWidth,
height - 2 * borderWidth); // -0.1 to prevent resizing the anchor pane
break;
case CIRCLE:
double radius = Math.min((width / 2) - 2 * borderWidth, (height / 2) - 2 * borderWidth);
mask = new Circle((bounds.getMinX() + diffMinX + bounds.getMaxX() - diffMaxX) / 2 - snappedLeftInset(),
(bounds.getMinY() + diffMinY + bounds.getMaxY() - diffMaxY) / 2 - snappedTopInset(),
radius,
Color.BLUE);
break;
case FIT:
mask = new Region();
if (control instanceof Shape) {
((Region) mask).setShape((Shape) control);
} else if (control instanceof Region) {
((Region) mask).setShape(((Region) control).getShape());
JFXNodeUtils.updateBackground(((Region) control).getBackground(), (Region) mask);
}
mask.resize(width, height);
mask.relocate(bounds.getMinX() + diffMinX, bounds.getMinY() + diffMinY);
break;
default:
mask = new Rectangle(bounds.getMinX() + diffMinX - snappedLeftInset(),
bounds.getMinY() + diffMinY - snappedTopInset(),
width - 2 * borderWidth,
height - 2 * borderWidth); // -0.1 to prevent resizing the anchor pane
break;
}
return mask;
}
/**
* compute the ripple radius
*
* @return the ripple radius size
*/
protected double computeRippleRadius() {
double width2 = control.getLayoutBounds().getWidth() * control.getLayoutBounds().getWidth();
double height2 = control.getLayoutBounds().getHeight() * control.getLayoutBounds().getHeight();
return Math.min(Math.sqrt(width2 + height2), RIPPLE_MAX_RADIUS) * 1.1 + 5;
}
protected void setOverLayBounds(Rectangle overlay) {
overlay.setWidth(control.getLayoutBounds().getWidth());
overlay.setHeight(control.getLayoutBounds().getHeight());
}
/**
* init mouse listeners on the control
*/
protected void initControlListeners() {
// if the control got resized the overlay rect must be rest
control.layoutBoundsProperty().addListener(observable -> resetRippler());
if (getChildren().contains(control)) {
control.boundsInParentProperty().addListener(observable -> resetRippler());
}
control.addEventHandler(MouseEvent.MOUSE_PRESSED,
(event) -> createRipple(event.getX(), event.getY()));
// create fade out transition for the ripple
control.addEventHandler(MouseEvent.MOUSE_RELEASED, e -> releaseRipple());
}
/**
* creates Ripple effect
*/
protected void createRipple(double x, double y) {
if (!isRipplerDisabled()) {
rippler.setGeneratorCenterX(x);
rippler.setGeneratorCenterY(y);
rippler.createRipple();
}
}
protected void releaseRipple() {
rippler.releaseRipple();
}
/**
* creates Ripple effect in the center of the control
*
* @return a runnable to release the ripple when needed
*/
public Runnable createManualRipple() {
if (!isRipplerDisabled()) {
rippler.setGeneratorCenterX(control.getLayoutBounds().getWidth() / 2);
rippler.setGeneratorCenterY(control.getLayoutBounds().getHeight() / 2);
rippler.createRipple();
return () -> {
// create fade out transition for the ripple
releaseRipple();
};
}
return () -> {
};
}
/// show/hide the ripple overlay
///
/// @param forceOverlay used to hold the overlay after ripple action
public void setOverlayVisible(boolean visible, boolean forceOverlay) {
this.forceOverlay = forceOverlay;
setOverlayVisible(visible);
}
/// show/hide the ripple overlay
/// NOTE: setting overlay visibility to false will reset forceOverlay to false
public void setOverlayVisible(boolean visible) {
if (visible) {
showOverlay();
} else {
forceOverlay = false;
hideOverlay();
}
}
/**
* this method will be set to private in future versions of JFoenix,
* user the method {@link #setOverlayVisible(boolean)}
*/
public void showOverlay() {
if (rippler.overlayRect != null) {
rippler.overlayRect.outAnimation.stop();
}
rippler.createOverlay();
rippler.overlayRect.inAnimation.play();
}
public void hideOverlay() {
if (!forceOverlay) {
if (rippler.overlayRect != null) {
rippler.overlayRect.inAnimation.stop();
}
if (rippler.overlayRect != null) {
rippler.overlayRect.outAnimation.play();
}
} else {
System.err.println("Ripple Overlay is forced!");
}
}
/**
* Generates ripples on the screen every 0.3 seconds or whenever
* the createRipple method is called. Ripples grow and fade out
* over 0.6 seconds
*/
protected final class RippleGenerator extends Group {
private double generatorCenterX = 0;
private double generatorCenterY = 0;
private OverLayRipple overlayRect;
private final AtomicBoolean generating = new AtomicBoolean(false);
private boolean cacheRipplerClip = false;
private boolean resetClip = false;
private final Queue<Ripple> ripplesQueue = new LinkedList<>();
RippleGenerator() {
// improve in performance, by preventing
// redrawing the parent when the ripple effect is triggered
this.setManaged(false);
this.setCache(true);
this.setCacheHint(CacheHint.SPEED);
}
void createRipple() {
if (enabled) {
if (!generating.getAndSet(true)) {
// create overlay once then change its color later
createOverlay();
if (this.getClip() == null || (getChildren().size() == 1 && !cacheRipplerClip) || resetClip) {
this.setClip(getMask());
}
this.resetClip = false;
// create the ripple effect
final Ripple ripple = new Ripple(generatorCenterX, generatorCenterY);
getChildren().add(ripple);
ripplesQueue.add(ripple);
// animate the ripple
overlayRect.outAnimation.stop();
overlayRect.inAnimation.play();
ripple.inAnimation.play();
}
}
}
private void releaseRipple() {
Ripple ripple = ripplesQueue.poll();
if (ripple != null) {
ripple.inAnimation.stop();
ripple.outAnimation = new Timeline(
new KeyFrame(Duration.millis(Math.min(800, (0.9 * 500) / ripple.getScaleX()))
, ripple.outKeyValues));
ripple.outAnimation.setOnFinished((event) -> getChildren().remove(ripple));
ripple.outAnimation.play();
if (generating.getAndSet(false)) {
if (overlayRect != null) {
overlayRect.inAnimation.stop();
if (!forceOverlay) {
overlayRect.outAnimation.play();
}
}
}
}
}
void cacheRippleClip(boolean cached) {
cacheRipplerClip = cached;
}
void createOverlay() {
if (overlayRect == null) {
overlayRect = new OverLayRipple();
overlayRect.setClip(getMask());
getChildren().add(0, overlayRect);
overlayRect.fillProperty().bind(Bindings.createObjectBinding(() -> {
if (ripplerFill.get() instanceof Color) {
return new Color(((Color) ripplerFill.get()).getRed(),
((Color) ripplerFill.get()).getGreen(),
((Color) ripplerFill.get()).getBlue(),
0.2);
} else {
return Color.TRANSPARENT;
}
}, ripplerFill));
}
}
void setGeneratorCenterX(double generatorCenterX) {
this.generatorCenterX = generatorCenterX;
}
void setGeneratorCenterY(double generatorCenterY) {
this.generatorCenterY = generatorCenterY;
}
private final class OverLayRipple extends Rectangle {
// Overlay ripple animations
Animation inAnimation = new Timeline(new KeyFrame(Duration.millis(300),
new KeyValue(opacityProperty(), 1, Interpolator.EASE_IN)));
Animation outAnimation = new Timeline(new KeyFrame(Duration.millis(300),
new KeyValue(opacityProperty(), 0, Interpolator.EASE_OUT)));
OverLayRipple() {
super();
setOverLayBounds(this);
this.getStyleClass().add("jfx-rippler-overlay");
// update initial position
if (JFXRippler.this.getChildrenUnmodifiable().contains(control)) {
double diffMinX = Math.abs(control.getBoundsInLocal().getMinX() - control.getLayoutBounds().getMinX());
double diffMinY = Math.abs(control.getBoundsInLocal().getMinY() - control.getLayoutBounds().getMinY());
Bounds bounds = control.getBoundsInParent();
this.setX(bounds.getMinX() + diffMinX - snappedLeftInset());
this.setY(bounds.getMinY() + diffMinY - snappedTopInset());
}
// set initial attributes
setOpacity(0);
setCache(true);
setCacheHint(CacheHint.SPEED);
setCacheShape(true);
setManaged(false);
}
}
private final class Ripple extends Circle {
KeyValue[] outKeyValues;
Animation outAnimation = null;
Animation inAnimation = null;
private Ripple(double centerX, double centerY) {
super(centerX,
centerY,
ripplerRadius.get().doubleValue() == Region.USE_COMPUTED_SIZE ?
computeRippleRadius() : ripplerRadius.get().doubleValue(), null);
setCache(true);
setCacheHint(CacheHint.SPEED);
setCacheShape(true);
setManaged(false);
setSmooth(true);
KeyValue[] inKeyValues = new KeyValue[isRipplerRecenter() ? 4 : 2];
outKeyValues = new KeyValue[isRipplerRecenter() ? 5 : 3];
inKeyValues[0] = new KeyValue(scaleXProperty(), 0.9, rippleInterpolator);
inKeyValues[1] = new KeyValue(scaleYProperty(), 0.9, rippleInterpolator);
outKeyValues[0] = new KeyValue(this.scaleXProperty(), 1, rippleInterpolator);
outKeyValues[1] = new KeyValue(this.scaleYProperty(), 1, rippleInterpolator);
outKeyValues[2] = new KeyValue(this.opacityProperty(), 0, rippleInterpolator);
if (isRipplerRecenter()) {
double dx = (control.getLayoutBounds().getWidth() / 2 - centerX) / 1.55;
double dy = (control.getLayoutBounds().getHeight() / 2 - centerY) / 1.55;
inKeyValues[2] = outKeyValues[3] = new KeyValue(translateXProperty(),
Math.signum(dx) * Math.min(Math.abs(dx),
this.getRadius() / 2),
rippleInterpolator);
inKeyValues[3] = outKeyValues[4] = new KeyValue(translateYProperty(),
Math.signum(dy) * Math.min(Math.abs(dy),
this.getRadius() / 2),
rippleInterpolator);
}
inAnimation = new Timeline(new KeyFrame(Duration.ZERO,
new KeyValue(scaleXProperty(),
0,
rippleInterpolator),
new KeyValue(scaleYProperty(),
0,
rippleInterpolator),
new KeyValue(translateXProperty(),
0,
rippleInterpolator),
new KeyValue(translateYProperty(),
0,
rippleInterpolator),
new KeyValue(opacityProperty(),
1,
rippleInterpolator)
), new KeyFrame(Duration.millis(900), inKeyValues));
setScaleX(0);
setScaleY(0);
if (ripplerFill.get() instanceof Color) {
Color circleColor = new Color(((Color) ripplerFill.get()).getRed(),
((Color) ripplerFill.get()).getGreen(),
((Color) ripplerFill.get()).getBlue(),
0.3);
setStroke(circleColor);
setFill(circleColor);
} else {
setStroke(ripplerFill.get());
setFill(ripplerFill.get());
}
}
}
public void clear() {
getChildren().clear();
rippler.overlayRect = null;
generating.set(false);
}
}
private void resetOverLay() {
if (rippler.overlayRect != null) {
rippler.overlayRect.inAnimation.stop();
final RippleGenerator.OverLayRipple oldOverlay = rippler.overlayRect;
rippler.overlayRect.outAnimation.setOnFinished((finish) -> rippler.getChildren().remove(oldOverlay));
rippler.overlayRect.outAnimation.play();
rippler.overlayRect = null;
}
}
private void resetClip() {
this.rippler.resetClip = true;
}
protected void resetRippler() {
resetOverLay();
resetClip();
}
/***************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
/**
* Initialize the style class to 'jfx-rippler'.
* <p>
* This is the selector class from which CSS can be used to style
* this control.
*/
private static final String DEFAULT_STYLE_CLASS = "jfx-rippler";
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
/**
* the ripple recenter property, by default it's false.
* if true the ripple effect will show gravitational pull to the center of its control
*/
private final StyleableObjectProperty<Boolean> ripplerRecenter = new SimpleStyleableObjectProperty<>(
StyleableProperties.RIPPLER_RECENTER,
JFXRippler.this,
"ripplerRecenter",
false);
public Boolean isRipplerRecenter() {
return ripplerRecenter != null && ripplerRecenter.get();
}
public StyleableObjectProperty<Boolean> ripplerRecenterProperty() {
return this.ripplerRecenter;
}
public void setRipplerRecenter(Boolean radius) {
this.ripplerRecenter.set(radius);
}
/**
* the ripple radius size, by default it will be automatically computed.
*/
private final StyleableObjectProperty<Number> ripplerRadius = new SimpleStyleableObjectProperty<>(
StyleableProperties.RIPPLER_RADIUS,
JFXRippler.this,
"ripplerRadius",
Region.USE_COMPUTED_SIZE);
public Number getRipplerRadius() {
return ripplerRadius == null ? Region.USE_COMPUTED_SIZE : ripplerRadius.get();
}
public StyleableObjectProperty<Number> ripplerRadiusProperty() {
return this.ripplerRadius;
}
public void setRipplerRadius(Number radius) {
this.ripplerRadius.set(radius);
}
/**
* the default color of the ripple effect
*/
private final StyleableObjectProperty<Paint> ripplerFill = new SimpleStyleableObjectProperty<>(StyleableProperties.RIPPLER_FILL,
JFXRippler.this,
"ripplerFill",
Color.rgb(0,
200,
255));
public Paint getRipplerFill() {
return ripplerFill == null ? Color.rgb(0, 200, 255) : ripplerFill.get();
}
public StyleableObjectProperty<Paint> ripplerFillProperty() {
return this.ripplerFill;
}
public void setRipplerFill(Paint color) {
this.ripplerFill.set(color);
}
/// mask property used for clipping the rippler.
/// can be either CIRCLE/RECT
private final StyleableObjectProperty<RipplerMask> maskType = new SimpleStyleableObjectProperty<>(
StyleableProperties.MASK_TYPE,
JFXRippler.this,
"maskType",
RipplerMask.RECT);
public RipplerMask getMaskType() {
return maskType == null ? RipplerMask.RECT : maskType.get();
}
public StyleableObjectProperty<RipplerMask> maskTypeProperty() {
return this.maskType;
}
public void setMaskType(RipplerMask type) {
this.maskType.set(type);
}
/**
* the ripple disable, by default it's false.
* if true the ripple effect will be hidden
*/
private final StyleableBooleanProperty ripplerDisabled = new SimpleStyleableBooleanProperty(
StyleableProperties.RIPPLER_DISABLED,
JFXRippler.this,
"ripplerDisabled",
false);
public Boolean isRipplerDisabled() {
return ripplerDisabled != null && ripplerDisabled.get();
}
public StyleableBooleanProperty ripplerDisabledProperty() {
return this.ripplerDisabled;
}
public void setRipplerDisabled(Boolean disabled) {
this.ripplerDisabled.set(disabled);
}
/**
* indicates whether the ripple effect is infront of or behind the node
*/
protected ObjectProperty<RipplerPos> position = new SimpleObjectProperty<>();
public void setPosition(RipplerPos pos) {
this.position.set(pos);
}
public RipplerPos getPosition() {
return position == null ? RipplerPos.FRONT : position.get();
}
public ObjectProperty<RipplerPos> positionProperty() {
return this.position;
}
private static final class StyleableProperties {
private static final CssMetaData<JFXRippler, Boolean> RIPPLER_RECENTER =
new CssMetaData<>("-jfx-rippler-recenter",
BooleanConverter.getInstance(), false) {
@Override
public boolean isSettable(JFXRippler control) {
return control.ripplerRecenter == null || !control.ripplerRecenter.isBound();
}
@Override
public StyleableProperty<Boolean> getStyleableProperty(JFXRippler control) {
return control.ripplerRecenterProperty();
}
};
private static final CssMetaData<JFXRippler, Boolean> RIPPLER_DISABLED =
new CssMetaData<>("-jfx-rippler-disabled",
BooleanConverter.getInstance(), false) {
@Override
public boolean isSettable(JFXRippler control) {
return control.ripplerDisabled == null || !control.ripplerDisabled.isBound();
}
@Override
public StyleableProperty<Boolean> getStyleableProperty(JFXRippler control) {
return control.ripplerDisabledProperty();
}
};
private static final CssMetaData<JFXRippler, Paint> RIPPLER_FILL =
new CssMetaData<>("-jfx-rippler-fill",
PaintConverter.getInstance(), Color.rgb(0, 200, 255)) {
@Override
public boolean isSettable(JFXRippler control) {
return control.ripplerFill == null || !control.ripplerFill.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXRippler control) {
return control.ripplerFillProperty();
}
};
private static final CssMetaData<JFXRippler, Number> RIPPLER_RADIUS =
new CssMetaData<>("-jfx-rippler-radius",
SizeConverter.getInstance(), Region.USE_COMPUTED_SIZE) {
@Override
public boolean isSettable(JFXRippler control) {
return control.ripplerRadius == null || !control.ripplerRadius.isBound();
}
@Override
public StyleableProperty<Number> getStyleableProperty(JFXRippler control) {
return control.ripplerRadiusProperty();
}
};
private static final CssMetaData<JFXRippler, RipplerMask> MASK_TYPE =
new CssMetaData<>("-jfx-mask-type",
RipplerMaskTypeConverter.getInstance(), RipplerMask.RECT) {
@Override
public boolean isSettable(JFXRippler control) {
return control.maskType == null || !control.maskType.isBound();
}
@Override
public StyleableProperty<RipplerMask> getStyleableProperty(JFXRippler control) {
return control.maskTypeProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables =
new ArrayList<>(StackPane.getClassCssMetaData());
Collections.addAll(styleables,
RIPPLER_RECENTER,
RIPPLER_RADIUS,
RIPPLER_FILL,
MASK_TYPE,
RIPPLER_DISABLED
);
STYLEABLES = Collections.unmodifiableList(styleables);
}
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}
}
@@ -0,0 +1,373 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.skins.JFXToggleButtonSkin;
import javafx.css.*;
import javafx.css.converter.BooleanConverter;
import javafx.css.converter.PaintConverter;
import javafx.scene.control.Skin;
import javafx.scene.control.ToggleButton;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* JFXToggleButton is the material design implementation of a toggle button.
* important CSS Selectors:
* <p>
* .jfx-toggle-button{
* -fx-toggle-color: color-value;
* -fx-untoggle-color: color-value;
* -fx-toggle-line-color: color-value;
* -fx-untoggle-line-color: color-value;
* }
* <p>
* To change the rippler color when toggled:
* <p>
* .jfx-toggle-button .jfx-rippler{
* -fx-rippler-fill: color-value;
* }
* <p>
* .jfx-toggle-button:selected .jfx-rippler{
* -fx-rippler-fill: color-value;
* }
*
* @author Shadi Shaheen
* @version 1.0
* @since 2016-03-09
*/
public class JFXToggleButton extends ToggleButton {
/**
* {@inheritDoc}
*/
public JFXToggleButton() {
initialize();
}
/**
* {@inheritDoc}
*/
@Override
protected Skin<?> createDefaultSkin() {
return new JFXToggleButtonSkin(this);
}
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
// it's up for the user to add this behavior
// toggleColor.addListener((o, oldVal, newVal) -> {
// // update line color in case not set by the user
// if(newVal instanceof Color)
// toggleLineColor.set(((Color)newVal).desaturate().desaturate().brighter());
// });
}
/***************************************************************************
* *
* styleable Properties *
* *
**************************************************************************/
/**
* Initialize the style class to 'jfx-toggle-button'.
* <p>
* This is the selector class from which CSS can be used to style
* this control.
*/
private static final String DEFAULT_STYLE_CLASS = "jfx-toggle-button";
/**
* default color used when the button is toggled
*/
private final StyleableObjectProperty<Paint> toggleColor = new SimpleStyleableObjectProperty<>(StyleableProperties.TOGGLE_COLOR,
JFXToggleButton.this,
"toggleColor",
Color.valueOf(
"#009688"));
public Paint getToggleColor() {
return toggleColor == null ? Color.valueOf("#009688") : toggleColor.get();
}
public StyleableObjectProperty<Paint> toggleColorProperty() {
return this.toggleColor;
}
public void setToggleColor(Paint color) {
this.toggleColor.set(color);
}
/**
* default color used when the button is not toggled
*/
private StyleableObjectProperty<Paint> untoggleColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNTOGGLE_COLOR,
JFXToggleButton.this,
"unToggleColor",
Color.valueOf(
"#FAFAFA"));
public Paint getUnToggleColor() {
return untoggleColor == null ? Color.valueOf("#FAFAFA") : untoggleColor.get();
}
public StyleableObjectProperty<Paint> unToggleColorProperty() {
return this.untoggleColor;
}
public void setUnToggleColor(Paint color) {
this.untoggleColor.set(color);
}
/**
* default line color used when the button is toggled
*/
private final StyleableObjectProperty<Paint> toggleLineColor = new SimpleStyleableObjectProperty<>(
StyleableProperties.TOGGLE_LINE_COLOR,
JFXToggleButton.this,
"toggleLineColor",
Color.valueOf("#77C2BB"));
public Paint getToggleLineColor() {
return toggleLineColor == null ? Color.valueOf("#77C2BB") : toggleLineColor.get();
}
public StyleableObjectProperty<Paint> toggleLineColorProperty() {
return this.toggleLineColor;
}
public void setToggleLineColor(Paint color) {
this.toggleLineColor.set(color);
}
/**
* default line color used when the button is not toggled
*/
private final StyleableObjectProperty<Paint> untoggleLineColor = new SimpleStyleableObjectProperty<>(
StyleableProperties.UNTOGGLE_LINE_COLOR,
JFXToggleButton.this,
"unToggleLineColor",
Color.valueOf("#999999"));
public Paint getUnToggleLineColor() {
return untoggleLineColor == null ? Color.valueOf("#999999") : untoggleLineColor.get();
}
public StyleableObjectProperty<Paint> unToggleLineColorProperty() {
return this.untoggleLineColor;
}
public void setUnToggleLineColor(Paint color) {
this.untoggleLineColor.set(color);
}
/**
* Default size of the toggle button.
*/
private final StyleableDoubleProperty size = new SimpleStyleableDoubleProperty(
StyleableProperties.SIZE,
JFXToggleButton.this,
"size",
10.0);
public double getSize() {
return size.get();
}
public StyleableDoubleProperty sizeProperty() {
return this.size;
}
public void setSize(double size) {
this.size.set(size);
}
/**
* Disable the visual indicator for focus
*/
private final StyleableBooleanProperty disableVisualFocus = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_VISUAL_FOCUS,
JFXToggleButton.this,
"disableVisualFocus",
false);
public final StyleableBooleanProperty disableVisualFocusProperty() {
return this.disableVisualFocus;
}
public final Boolean isDisableVisualFocus() {
return disableVisualFocus != null && this.disableVisualFocusProperty().get();
}
public final void setDisableVisualFocus(final Boolean disabled) {
this.disableVisualFocusProperty().set(disabled);
}
/**
* disable animation on button action
*/
private final StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION,
JFXToggleButton.this,
"disableAnimation",
!AnimationUtils.isAnimationEnabled());
public final StyleableBooleanProperty disableAnimationProperty() {
return this.disableAnimation;
}
public final Boolean isDisableAnimation() {
return disableAnimation != null && this.disableAnimationProperty().get();
}
public final void setDisableAnimation(final Boolean disabled) {
this.disableAnimationProperty().set(disabled);
}
private static final class StyleableProperties {
private static final CssMetaData<JFXToggleButton, Paint> TOGGLE_COLOR =
new CssMetaData<>("-jfx-toggle-color",
PaintConverter.getInstance(), Color.valueOf("#009688")) {
@Override
public boolean isSettable(JFXToggleButton control) {
return control.toggleColor == null || !control.toggleColor.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXToggleButton control) {
return control.toggleColorProperty();
}
};
private static final CssMetaData<JFXToggleButton, Paint> UNTOGGLE_COLOR =
new CssMetaData<>("-jfx-untoggle-color",
PaintConverter.getInstance(), Color.valueOf("#FAFAFA")) {
@Override
public boolean isSettable(JFXToggleButton control) {
return control.untoggleColor == null || !control.untoggleColor.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXToggleButton control) {
return control.unToggleColorProperty();
}
};
private static final CssMetaData<JFXToggleButton, Paint> TOGGLE_LINE_COLOR =
new CssMetaData<>("-jfx-toggle-line-color",
PaintConverter.getInstance(), Color.valueOf("#77C2BB")) {
@Override
public boolean isSettable(JFXToggleButton control) {
return control.toggleLineColor == null || !control.toggleLineColor.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXToggleButton control) {
return control.toggleLineColorProperty();
}
};
private static final CssMetaData<JFXToggleButton, Paint> UNTOGGLE_LINE_COLOR =
new CssMetaData<>("-jfx-untoggle-line-color",
PaintConverter.getInstance(), Color.valueOf("#999999")) {
@Override
public boolean isSettable(JFXToggleButton control) {
return control.untoggleLineColor == null || !control.untoggleLineColor.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXToggleButton control) {
return control.unToggleLineColorProperty();
}
};
private static final CssMetaData<JFXToggleButton, Number> SIZE =
new CssMetaData<>("-jfx-size",
StyleConverter.getSizeConverter(), 10.0) {
@Override
public boolean isSettable(JFXToggleButton control) {
return !control.size.isBound();
}
@Override
public StyleableProperty<Number> getStyleableProperty(JFXToggleButton control) {
return control.sizeProperty();
}
};
private static final CssMetaData<JFXToggleButton, Boolean> DISABLE_VISUAL_FOCUS =
new CssMetaData<>("-jfx-disable-visual-focus",
BooleanConverter.getInstance(), false) {
@Override
public boolean isSettable(JFXToggleButton control) {
return control.disableVisualFocus == null || !control.disableVisualFocus.isBound();
}
@Override
public StyleableBooleanProperty getStyleableProperty(JFXToggleButton control) {
return control.disableVisualFocusProperty();
}
};
private static final CssMetaData<JFXToggleButton, Boolean> DISABLE_ANIMATION =
new CssMetaData<>("-jfx-disable-animation",
BooleanConverter.getInstance(), false) {
@Override
public boolean isSettable(JFXToggleButton control) {
return control.disableAnimation == null || !control.disableAnimation.isBound();
}
@Override
public StyleableBooleanProperty getStyleableProperty(JFXToggleButton control) {
return control.disableAnimationProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables =
new ArrayList<>(ToggleButton.getClassCssMetaData());
Collections.addAll(styleables,
SIZE,
TOGGLE_COLOR,
UNTOGGLE_COLOR,
TOGGLE_LINE_COLOR,
UNTOGGLE_LINE_COLOR,
DISABLE_VISUAL_FOCUS,
DISABLE_ANIMATION
);
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
}
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
}
@@ -0,0 +1,46 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls.behavior;
import com.sun.javafx.scene.control.behavior.ComboBoxBaseBehavior;
import javafx.scene.control.ComboBoxBase;
import javafx.scene.control.PopupControl;
/**
* @author Shadi Shaheen
* @version 2.0
* @since 2017-10-05
*/
public class JFXGenericPickerBehavior<T> extends ComboBoxBaseBehavior<T> {
public JFXGenericPickerBehavior(ComboBoxBase<T> var1) {
super(var1);
}
public void onAutoHide(PopupControl var1) {
if (!var1.isShowing() && this.getNode().isShowing()) {
this.getNode().hide();
}
if (!this.getNode().isShowing()) {
super.onAutoHide(var1);
}
}
}
@@ -0,0 +1,222 @@
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.jfoenix.skins;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXRippler;
import com.jfoenix.controls.JFXRippler.RipplerMask;
import com.jfoenix.controls.JFXRippler.RipplerPos;
import com.jfoenix.transitions.CachedTransition;
import com.jfoenix.transitions.JFXFillTransition;
import com.jfoenix.utils.JFXNodeUtils;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.animation.Transition;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.VPos;
import javafx.scene.control.CheckBox;
import javafx.scene.control.skin.CheckBoxSkin;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.SVGPath;
import javafx.util.Duration;
import org.jackhuang.hmcl.theme.Themes;
public class JFXCheckBoxSkin extends CheckBoxSkin {
private final StackPane box = new StackPane();
private final StackPane mark = new StackPane();
private final double lineThick = 2.0;
private final double padding = 10.0;
private final JFXRippler rippler;
private final AnchorPane container = new AnchorPane();
private final double labelOffset = -8.0;
private final Transition transition;
private boolean invalid = true;
private JFXFillTransition select;
public JFXCheckBoxSkin(JFXCheckBox control) {
super(control);
this.box.setMinSize(18.0, 18.0);
this.box.setPrefSize(18.0, 18.0);
this.box.setMaxSize(18.0, 18.0);
this.box.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, new CornerRadii(2.0), Insets.EMPTY)));
this.box.setBorder(new Border(new BorderStroke(Themes.getColorScheme().getOnSurfaceVariant(), BorderStrokeStyle.SOLID, new CornerRadii(2.0), new BorderWidths(this.lineThick))));
StackPane boxContainer = new StackPane();
boxContainer.getChildren().add(this.box);
boxContainer.setPadding(new Insets(this.padding));
this.rippler = new JFXRippler(boxContainer, RipplerMask.CIRCLE, RipplerPos.BACK);
this.updateRippleColor();
SVGPath shape = new SVGPath();
shape.setContent("M384 690l452-452 60 60-512 512-238-238 60-60z");
this.mark.setShape(shape);
this.mark.setMaxSize(15.0, 12.0);
this.mark.setStyle("-fx-background-color:-monet-on-primary; -fx-border-color:-monet-on-primary; -fx-border-width:2px;");
this.mark.setVisible(false);
this.mark.setScaleX(0.0);
this.mark.setScaleY(0.0);
boxContainer.getChildren().add(this.mark);
this.container.getChildren().add(this.rippler);
AnchorPane.setRightAnchor(this.rippler, this.labelOffset);
control.selectedProperty().addListener((o, oldVal, newVal) -> {
this.updateRippleColor();
this.playSelectAnimation(newVal);
});
control.focusedProperty().addListener((o, oldVal, newVal) -> {
if (newVal) {
if (!this.getSkinnable().isPressed()) {
this.rippler.showOverlay();
}
} else {
this.rippler.hideOverlay();
}
});
control.pressedProperty().addListener((o, oldVal, newVal) -> this.rippler.hideOverlay());
this.updateChildren();
this.registerChangeListener(control.checkedColorProperty(), ignored -> {
if (select != null) {
select.stop();
}
this.createFillTransition();
updateColors();
});
this.registerChangeListener(control.unCheckedColorProperty(), ignored -> updateColors());
this.transition = new CheckBoxTransition();
this.createFillTransition();
}
private void updateRippleColor() {
var control = (JFXCheckBox) this.getSkinnable();
this.rippler.setRipplerFill(control.isSelected()
? control.getCheckedColor()
: control.getUnCheckedColor());
}
private void updateColors() {
var control = (JFXCheckBox) getSkinnable();
boolean isSelected = control.isSelected();
JFXNodeUtils.updateBackground(box.getBackground(), box, isSelected ? control.getCheckedColor() : Color.TRANSPARENT);
rippler.setRipplerFill(isSelected ? control.getCheckedColor() : control.getUnCheckedColor());
final BorderStroke borderStroke = box.getBorder().getStrokes().get(0);
box.setBorder(new Border(new BorderStroke(
isSelected ? control.getCheckedColor() : Themes.getColorScheme().getOnSurfaceVariant(),
borderStroke.getTopStyle(),
borderStroke.getRadii(),
borderStroke.getWidths())));
}
protected void updateChildren() {
super.updateChildren();
if (this.container != null) {
this.getChildren().remove(1);
this.getChildren().add(this.container);
}
}
protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset) + this.snapSizeX(this.box.minWidth(-1.0)) + this.labelOffset + 2.0 * this.padding;
}
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset) + this.snapSizeY(this.box.prefWidth(-1.0)) + this.labelOffset + 2.0 * this.padding;
}
protected void layoutChildren(double x, double y, double w, double h) {
CheckBox checkBox = this.getSkinnable();
double boxWidth = this.snapSizeX(this.container.prefWidth(-1.0));
double boxHeight = this.snapSizeY(this.container.prefHeight(-1.0));
double computeWidth = Math.min(checkBox.prefWidth(-1.0), checkBox.minWidth(-1.0)) + this.labelOffset + 2.0 * this.padding;
double labelWidth = Math.min(computeWidth - boxWidth, w - this.snapSizeX(boxWidth)) + this.labelOffset + 2.0 * this.padding;
double labelHeight = Math.min(checkBox.prefHeight(labelWidth), h);
double maxHeight = Math.max(boxHeight, labelHeight);
double xOffset = computeXOffset(w, labelWidth + boxWidth, checkBox.getAlignment().getHpos()) + x;
double yOffset = computeYOffset(h, maxHeight, checkBox.getAlignment().getVpos()) + x;
if (this.invalid) {
if (this.getSkinnable().isSelected()) {
this.playSelectAnimation(true);
}
this.invalid = false;
}
this.layoutLabelInArea(xOffset + boxWidth, yOffset, labelWidth, maxHeight, checkBox.getAlignment());
this.container.resize(boxWidth, boxHeight);
this.positionInArea(this.container, xOffset, yOffset, boxWidth, maxHeight, 0.0, checkBox.getAlignment().getHpos(), checkBox.getAlignment().getVpos());
}
static double computeXOffset(double width, double contentWidth, HPos hpos) {
return switch (hpos) {
case LEFT -> 0.0;
case CENTER -> (width - contentWidth) / 2.0;
case RIGHT -> width - contentWidth;
};
}
static double computeYOffset(double height, double contentHeight, VPos vpos) {
return switch (vpos) {
case TOP -> 0.0;
case CENTER -> (height - contentHeight) / 2.0;
case BOTTOM -> height - contentHeight;
default -> 0.0;
};
}
private void playSelectAnimation(Boolean selection) {
if (selection == null) {
selection = false;
}
JFXCheckBox control = (JFXCheckBox) this.getSkinnable();
this.transition.setRate(selection ? 1.0 : -1.0);
this.select.setRate(selection ? 1.0 : -1.0);
this.transition.play();
this.select.play();
this.box.setBorder(new Border(new BorderStroke(
selection ? control.getCheckedColor() : Themes.getColorScheme().getOnSurfaceVariant(),
BorderStrokeStyle.SOLID,
new CornerRadii(2.0),
new BorderWidths(this.lineThick))));
}
private void createFillTransition() {
this.select = new JFXFillTransition(Duration.millis(120.0), this.box, Color.TRANSPARENT,
(Color) ((JFXCheckBox) this.getSkinnable()).getCheckedColor());
this.select.setInterpolator(Interpolator.EASE_OUT);
}
private final class CheckBoxTransition extends CachedTransition {
CheckBoxTransition() {
super(JFXCheckBoxSkin.this.mark,
new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(JFXCheckBoxSkin.this.mark.visibleProperty(), false, Interpolator.EASE_OUT),
new KeyValue(JFXCheckBoxSkin.this.mark.scaleXProperty(), (double) 0.5F, Interpolator.EASE_OUT),
new KeyValue(JFXCheckBoxSkin.this.mark.scaleYProperty(), (double) 0.5F, Interpolator.EASE_OUT)),
new KeyFrame(Duration.millis(400.0),
new KeyValue(JFXCheckBoxSkin.this.mark.visibleProperty(), true, Interpolator.EASE_OUT),
new KeyValue(JFXCheckBoxSkin.this.mark.scaleXProperty(), (double) 0.5F, Interpolator.EASE_OUT),
new KeyValue(JFXCheckBoxSkin.this.mark.scaleYProperty(), (double) 0.5F, Interpolator.EASE_OUT)),
new KeyFrame(Duration.millis(1000.0),
new KeyValue(JFXCheckBoxSkin.this.mark.visibleProperty(), true, Interpolator.EASE_OUT),
new KeyValue(JFXCheckBoxSkin.this.mark.scaleXProperty(), 1, Interpolator.EASE_OUT),
new KeyValue(JFXCheckBoxSkin.this.mark.scaleYProperty(), 1, Interpolator.EASE_OUT))
)
);
this.setCycleDuration(Duration.seconds(0.12));
this.setDelay(Duration.seconds(0.05));
}
}
}
@@ -0,0 +1,623 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.skins;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXColorPicker;
import com.jfoenix.utils.JFXNodeUtils;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.geometry.Bounds;
import javafx.geometry.NodeOrientation;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.PopupControl;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeType;
import java.util.List;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
* @author Shadi Shaheen FUTURE WORK: this UI will get re-designed to match material design guidlines
*/
final class JFXColorPalette extends Region {
private static final int SQUARE_SIZE = 15;
// package protected for testing purposes
JFXColorGrid colorPickerGrid;
final JFXButton customColorLink = new JFXButton(i18n("color.custom"));
JFXCustomColorPickerDialog customColorDialog = null;
private final JFXColorPicker colorPicker;
private final GridPane customColorGrid = new GridPane();
private final Label customColorLabel = new Label(i18n("color.recent"));
private PopupControl popupControl;
private ColorSquare focusedSquare;
private Color mouseDragColor = null;
private boolean dragDetected = false;
private final ColorSquare hoverSquare = new ColorSquare();
public JFXColorPalette(final JFXColorPicker colorPicker) {
getStyleClass().add("color-palette-region");
this.colorPicker = colorPicker;
colorPickerGrid = new JFXColorGrid();
colorPickerGrid.getChildren().get(0).requestFocus();
customColorLabel.setAlignment(Pos.CENTER_LEFT);
customColorLink.setPrefWidth(colorPickerGrid.prefWidth(-1));
customColorLink.setAlignment(Pos.CENTER);
customColorLink.setFocusTraversable(true);
customColorLink.setOnAction(ev -> {
if (customColorDialog == null) {
customColorDialog = new JFXCustomColorPickerDialog(popupControl);
customColorDialog.customColorProperty().addListener((ov, t1, t2) -> {
colorPicker.setValue(customColorDialog.customColorProperty().get());
});
customColorDialog.setOnSave(() -> {
Color customColor = customColorDialog.customColorProperty().get();
buildCustomColors();
colorPicker.getCustomColors().add(customColor);
updateSelection(customColor);
Event.fireEvent(colorPicker, new ActionEvent());
colorPicker.hide();
});
}
customColorDialog.setCurrentColor(colorPicker.valueProperty().get());
if (popupControl != null) {
popupControl.setAutoHide(false);
}
customColorDialog.show();
customColorDialog.setOnHidden(event -> {
if (popupControl != null) {
popupControl.setAutoHide(true);
}
});
});
initNavigation();
customColorGrid.getStyleClass().add("color-picker-grid");
customColorGrid.setVisible(false);
buildCustomColors();
colorPicker.getCustomColors().addListener((Change<? extends Color> change) -> buildCustomColors());
VBox paletteBox = new VBox();
paletteBox.getStyleClass().add("color-palette");
paletteBox.getChildren().addAll(colorPickerGrid);
if (colorPicker.getPreDefinedColors() == null) {
paletteBox.getChildren().addAll(customColorLabel, customColorGrid, customColorLink);
}
hoverSquare.setMouseTransparent(true);
hoverSquare.getStyleClass().addAll("hover-square");
setFocusedSquare(null);
getChildren().addAll(paletteBox, hoverSquare);
}
private void setFocusedSquare(ColorSquare square) {
hoverSquare.setVisible(square != null);
if (square == focusedSquare) {
return;
}
focusedSquare = square;
hoverSquare.setVisible(focusedSquare != null);
if (focusedSquare == null) {
return;
}
if (!focusedSquare.isFocused()) {
focusedSquare.requestFocus();
}
hoverSquare.rectangle.setFill(focusedSquare.rectangle.getFill());
Bounds b = square.localToScene(square.getLayoutBounds());
double x = b.getMinX();
double y = b.getMinY();
double xAdjust;
double scaleAdjust = hoverSquare.getScaleX() == 1.0 ? 0 : hoverSquare.getWidth() / 4.0;
if (colorPicker.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) {
x = focusedSquare.getLayoutX();
xAdjust = -focusedSquare.getWidth() + scaleAdjust;
} else {
xAdjust = focusedSquare.getWidth() / 2.0 + scaleAdjust;
}
hoverSquare.setLayoutX(snapPositionX(x) - xAdjust);
hoverSquare.setLayoutY(snapPositionY(y) - focusedSquare.getHeight() / 2.0 + (hoverSquare.getScaleY() == 1.0 ? 0 : focusedSquare.getHeight() / 4.0));
}
private void buildCustomColors() {
final ObservableList<Color> customColors = colorPicker.getCustomColors();
customColorGrid.getChildren().clear();
if (customColors.isEmpty()) {
customColorLabel.setVisible(false);
customColorLabel.setManaged(false);
customColorGrid.setVisible(false);
customColorGrid.setManaged(false);
return;
} else {
customColorLabel.setVisible(true);
customColorLabel.setManaged(true);
customColorGrid.setVisible(true);
customColorGrid.setManaged(true);
}
int customColumnIndex = 0;
int customRowIndex = 0;
int remainingSquares = customColors.size() % NUM_OF_COLUMNS;
int numEmpty = (remainingSquares == 0) ? 0 : NUM_OF_COLUMNS - remainingSquares;
for (int i = 0; i < customColors.size(); i++) {
Color c = customColors.get(i);
ColorSquare square = new ColorSquare(c, i, true);
customColorGrid.add(square, customColumnIndex, customRowIndex);
customColumnIndex++;
if (customColumnIndex == NUM_OF_COLUMNS) {
customColumnIndex = 0;
customRowIndex++;
}
}
for (int i = 0; i < numEmpty; i++) {
ColorSquare emptySquare = new ColorSquare();
customColorGrid.add(emptySquare, customColumnIndex, customRowIndex);
customColumnIndex++;
}
requestLayout();
}
private void initNavigation() {
setOnKeyPressed(ke -> {
switch (ke.getCode()) {
case SPACE:
case ENTER:
// select the focused color
if (focusedSquare != null) {
focusedSquare.selectColor(ke);
}
ke.consume();
break;
default: // no-op
}
});
}
public void setPopupControl(PopupControl pc) {
this.popupControl = pc;
}
public JFXColorGrid getColorGrid() {
return colorPickerGrid;
}
public boolean isCustomColorDialogShowing() {
return customColorDialog != null && customColorDialog.isVisible();
}
class ColorSquare extends StackPane {
Rectangle rectangle;
boolean isEmpty;
public ColorSquare() {
this(null, -1, false);
}
public ColorSquare(Color color, int index) {
this(color, index, false);
}
public ColorSquare(Color color, int index, boolean isCustom) {
// Add style class to handle selected color square
getStyleClass().add("color-square");
if (color != null) {
setFocusTraversable(true);
focusedProperty().addListener((s, ov, nv) -> setFocusedSquare(nv ? this : null));
addEventHandler(MouseEvent.MOUSE_ENTERED, event -> setFocusedSquare(ColorSquare.this));
addEventHandler(MouseEvent.MOUSE_EXITED, event -> setFocusedSquare(null));
addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
if (!dragDetected && event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 1) {
if (!isEmpty) {
Color fill = (Color) rectangle.getFill();
colorPicker.setValue(fill);
colorPicker.fireEvent(new ActionEvent());
updateSelection(fill);
event.consume();
}
colorPicker.hide();
}
});
}
rectangle = new Rectangle(SQUARE_SIZE, SQUARE_SIZE);
if (color == null) {
rectangle.setFill(Color.WHITE);
isEmpty = true;
} else {
rectangle.setFill(color);
}
rectangle.setStrokeType(StrokeType.INSIDE);
String tooltipStr = JFXNodeUtils.colorToHex(color);
Tooltip.install(this, new Tooltip((tooltipStr == null) ? "" : tooltipStr));
rectangle.getStyleClass().add("color-rect");
getChildren().add(rectangle);
}
public void selectColor(KeyEvent event) {
if (rectangle.getFill() != null) {
if (rectangle.getFill() instanceof Color) {
colorPicker.setValue((Color) rectangle.getFill());
colorPicker.fireEvent(new ActionEvent());
}
event.consume();
}
colorPicker.hide();
}
}
// The skin can update selection if colorpicker value changes..
public void updateSelection(Color color) {
setFocusedSquare(null);
for (ColorSquare c : colorPickerGrid.getSquares()) {
if (c.rectangle.getFill().equals(color)) {
setFocusedSquare(c);
return;
}
}
// check custom colors
for (Node n : customColorGrid.getChildren()) {
ColorSquare c = (ColorSquare) n;
if (c.rectangle.getFill().equals(color)) {
setFocusedSquare(c);
return;
}
}
}
class JFXColorGrid extends GridPane {
private final List<ColorSquare> squares;
final int NUM_OF_COLORS;
final int NUM_OF_ROWS;
public JFXColorGrid() {
getStyleClass().add("color-picker-grid");
setId("ColorCustomizerColorGrid");
int columnIndex = 0;
int rowIndex = 0;
squares = FXCollections.observableArrayList();
double[] limitedColors = colorPicker.getPreDefinedColors();
limitedColors = limitedColors == null ? RAW_VALUES : limitedColors;
NUM_OF_COLORS = limitedColors.length / 3;
NUM_OF_ROWS = (int) Math.ceil((double) NUM_OF_COLORS / (double) NUM_OF_COLUMNS);
final int numColors = limitedColors.length / 3;
Color[] colors = new Color[numColors];
for (int i = 0; i < numColors; i++) {
colors[i] = new Color(limitedColors[i * 3] / 255,
limitedColors[(i * 3) + 1] / 255, limitedColors[(i * 3) + 2] / 255,
1.0);
ColorSquare cs = new ColorSquare(colors[i], i);
squares.add(cs);
}
for (ColorSquare square : squares) {
add(square, columnIndex, rowIndex);
columnIndex++;
if (columnIndex == NUM_OF_COLUMNS) {
columnIndex = 0;
rowIndex++;
}
}
setOnMouseDragged(t -> {
if (!dragDetected) {
dragDetected = true;
mouseDragColor = colorPicker.getValue();
}
int xIndex = clamp(0,
(int) t.getX() / (SQUARE_SIZE + 1), NUM_OF_COLUMNS - 1);
int yIndex = clamp(0,
(int) t.getY() / (SQUARE_SIZE + 1), NUM_OF_ROWS - 1);
int index = xIndex + yIndex * NUM_OF_COLUMNS;
colorPicker.setValue((Color) squares.get(index).rectangle.getFill());
updateSelection(colorPicker.getValue());
});
addEventHandler(MouseEvent.MOUSE_RELEASED, t -> {
if (colorPickerGrid.getBoundsInLocal().contains(t.getX(), t.getY())) {
updateSelection(colorPicker.getValue());
colorPicker.fireEvent(new ActionEvent());
colorPicker.hide();
} else {
// restore color as mouse release happened outside the grid.
if (mouseDragColor != null) {
colorPicker.setValue(mouseDragColor);
updateSelection(mouseDragColor);
}
}
dragDetected = false;
});
}
public List<ColorSquare> getSquares() {
return squares;
}
@Override
protected double computePrefWidth(double height) {
return (SQUARE_SIZE + 1) * NUM_OF_COLUMNS;
}
@Override
protected double computePrefHeight(double width) {
return (SQUARE_SIZE + 1) * NUM_OF_ROWS;
}
}
private static final int NUM_OF_COLUMNS = 10;
private static final double[] RAW_VALUES = {
// WARNING: always make sure the number of colors is a divisable by NUM_OF_COLUMNS
250, 250, 250, // first row
245, 245, 245,
238, 238, 238,
224, 224, 224,
189, 189, 189,
158, 158, 158,
117, 117, 117,
97, 97, 97,
66, 66, 66,
33, 33, 33,
// second row
236, 239, 241,
207, 216, 220,
176, 190, 197,
144, 164, 174,
120, 144, 156,
96, 125, 139,
84, 110, 122,
69, 90, 100,
55, 71, 79,
38, 50, 56,
// third row
255, 235, 238,
255, 205, 210,
239, 154, 154,
229, 115, 115,
239, 83, 80,
244, 67, 54,
229, 57, 53,
211, 47, 47,
198, 40, 40,
183, 28, 28,
// forth row
252, 228, 236,
248, 187, 208,
244, 143, 177,
240, 98, 146,
236, 64, 122,
233, 30, 99,
216, 27, 96,
194, 24, 91,
173, 20, 87,
136, 14, 79,
// fifth row
243, 229, 245,
225, 190, 231,
206, 147, 216,
186, 104, 200,
171, 71, 188,
156, 39, 176,
142, 36, 170,
123, 31, 162,
106, 27, 154,
74, 20, 140,
// sixth row
237, 231, 246,
209, 196, 233,
179, 157, 219,
149, 117, 205,
126, 87, 194,
103, 58, 183,
94, 53, 177,
81, 45, 168,
69, 39, 160,
49, 27, 146,
// seventh row
232, 234, 246,
197, 202, 233,
159, 168, 218,
121, 134, 203,
92, 107, 192,
63, 81, 181,
57, 73, 171,
48, 63, 159,
40, 53, 147,
26, 35, 126,
// eigth row
227, 242, 253,
187, 222, 251,
144, 202, 249,
100, 181, 246,
66, 165, 245,
33, 150, 243,
30, 136, 229,
25, 118, 210,
21, 101, 192,
13, 71, 161,
// ninth row
225, 245, 254,
179, 229, 252,
129, 212, 250,
79, 195, 247,
41, 182, 246,
3, 169, 244,
3, 155, 229,
2, 136, 209,
2, 119, 189,
1, 87, 155,
// tenth row
224, 247, 250,
178, 235, 242,
128, 222, 234,
77, 208, 225,
38, 198, 218,
0, 188, 212,
0, 172, 193,
0, 151, 167,
0, 131, 143,
0, 96, 100,
// eleventh row
224, 242, 241,
178, 223, 219,
128, 203, 196,
77, 182, 172,
38, 166, 154,
0, 150, 136,
0, 137, 123,
0, 121, 107,
0, 105, 92,
0, 77, 64,
// twelfth row
232, 245, 233,
200, 230, 201,
165, 214, 167,
129, 199, 132,
102, 187, 106,
76, 175, 80,
67, 160, 71,
56, 142, 60,
46, 125, 50,
27, 94, 32,
// thirteenth row
241, 248, 233,
220, 237, 200,
197, 225, 165,
174, 213, 129,
156, 204, 101,
139, 195, 74,
124, 179, 66,
104, 159, 56,
85, 139, 47,
51, 105, 30,
// fourteenth row
249, 251, 231,
240, 244, 195,
230, 238, 156,
220, 231, 117,
212, 225, 87,
205, 220, 57,
192, 202, 51,
175, 180, 43,
158, 157, 36,
130, 119, 23,
// fifteenth row
255, 253, 231,
255, 249, 196,
255, 245, 157,
255, 241, 118,
255, 238, 88,
255, 235, 59,
253, 216, 53,
251, 192, 45,
249, 168, 37,
245, 127, 23,
// sixteenth row
255, 248, 225,
255, 236, 179,
255, 224, 130,
255, 213, 79,
255, 202, 40,
255, 193, 7,
255, 179, 0,
255, 160, 0,
255, 143, 0,
255, 111, 0,
// seventeenth row
255, 243, 224,
255, 224, 178,
255, 204, 128,
255, 183, 77,
255, 167, 38,
255, 152, 0,
251, 140, 0,
245, 124, 0,
239, 108, 0,
230, 81, 0,
// eighteenth row
251, 233, 231,
255, 204, 188,
255, 171, 145,
255, 138, 101,
255, 112, 67,
255, 87, 34,
244, 81, 30,
230, 74, 25,
216, 67, 21,
191, 54, 12,
// nineteenth row
239, 235, 233,
215, 204, 200,
188, 170, 164,
161, 136, 127,
141, 110, 99,
121, 85, 72,
109, 76, 65,
93, 64, 55,
78, 52, 46,
62, 39, 35,
};
private static int clamp(int min, int value, int max) {
if (value < min) {
return min;
}
if (value > max) {
return max;
}
return value;
}
}
@@ -0,0 +1,258 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.skins;
import com.jfoenix.controls.JFXClippedPane;
import com.jfoenix.controls.JFXColorPicker;
import com.jfoenix.controls.JFXRippler;
import com.jfoenix.effects.JFXDepthManager;
import com.jfoenix.utils.JFXNodeUtils;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.*;
import javafx.css.converter.BooleanConverter;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.ColorPicker;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.skin.ComboBoxPopupControl;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.util.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @author Shadi Shaheen
*/
public final class JFXColorPickerSkin extends JFXGenericPickerSkin<Color> {
private final Label displayNode;
private final JFXClippedPane colorBox;
private JFXColorPalette popupContent;
StyleableBooleanProperty colorLabelVisible = new SimpleStyleableBooleanProperty(StyleableProperties.COLOR_LABEL_VISIBLE,
JFXColorPickerSkin.this,
"colorLabelVisible",
true);
private final ObjectProperty<Color> textFill = new SimpleObjectProperty<>(Color.BLACK);
private final ObjectProperty<Background> colorBoxBackground = new SimpleObjectProperty<>(null);
public JFXColorPickerSkin(final ColorPicker colorPicker) {
super(colorPicker);
// create displayNode
displayNode = new Label("");
displayNode.getStyleClass().add("color-label");
displayNode.setMouseTransparent(true);
displayNode.textFillProperty().bind(textFill);
// label graphic
colorBox = new JFXClippedPane(displayNode);
colorBox.getStyleClass().add("color-box");
colorBox.setManaged(false);
colorBox.backgroundProperty().bind(colorBoxBackground);
initColor();
final JFXRippler rippler = new JFXRippler(colorBox, JFXRippler.RipplerMask.FIT);
rippler.ripplerFillProperty().bind(textFill);
getChildren().setAll(rippler);
JFXDepthManager.setDepth(getSkinnable(), 1);
getSkinnable().setPickOnBounds(false);
colorPicker.focusedProperty().addListener(observable -> {
if (colorPicker.isFocused()) {
if (!getSkinnable().isPressed()) {
rippler.setOverlayVisible(true);
}
} else {
rippler.setOverlayVisible(false);
}
});
// add listeners
registerChangeListener(colorPicker.valueProperty(), obs -> updateColor());
colorLabelVisible.addListener(invalidate -> {
if (colorLabelVisible.get()) {
displayNode.setText(JFXNodeUtils.colorToHex(getSkinnable().getValue()));
} else {
displayNode.setText("");
}
});
}
@Override
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
double width = 100;
String displayNodeText = displayNode.getText();
displayNode.setText("#DDDDDD");
width = Math.max(width, super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset));
displayNode.setText(displayNodeText);
return width + rightInset + leftInset;
}
@Override
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
if (colorBox == null) {
reflectUpdateDisplayArea();
}
return topInset + colorBox.prefHeight(width) + bottomInset;
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
double hInsets = snappedLeftInset() + snappedRightInset();
double vInsets = snappedTopInset() + snappedBottomInset();
double width = w + hInsets;
double height = h + vInsets;
colorBox.resizeRelocate(0, 0, width, height);
}
@Override
protected Node getPopupContent() {
if (popupContent == null) {
popupContent = new JFXColorPalette((JFXColorPicker) getSkinnable());
}
return popupContent;
}
@Override
public void show() {
super.show();
final ColorPicker colorPicker = (ColorPicker) getSkinnable();
popupContent.updateSelection(colorPicker.getValue());
}
@Override
public Node getDisplayNode() {
return displayNode;
}
private void updateColor() {
final ColorPicker colorPicker = (ColorPicker) getSkinnable();
Color color = colorPicker.getValue();
Color circleColor = color == null ? Color.WHITE : color;
// update picker box color
if (((JFXColorPicker) getSkinnable()).isDisableAnimation()) {
JFXNodeUtils.updateBackground(colorBox.getBackground(), colorBox, circleColor);
} else {
Circle colorCircle = new Circle();
colorCircle.setFill(circleColor);
colorCircle.setManaged(false);
colorCircle.setLayoutX(colorBox.getWidth() / 4);
colorCircle.setLayoutY(colorBox.getHeight() / 2);
colorBox.getChildren().add(colorCircle);
Timeline animateColor = new Timeline(new KeyFrame(Duration.millis(240),
new KeyValue(colorCircle.radiusProperty(),
200,
Interpolator.EASE_BOTH)));
animateColor.setOnFinished((finish) -> {
colorBoxBackground.set(new Background(new BackgroundFill(colorCircle.getFill(), new CornerRadii(3), Insets.EMPTY)));
colorBox.getChildren().remove(colorCircle);
});
animateColor.play();
}
// update label color
textFill.set(circleColor.grayscale().getRed() < 0.5 ? Color.valueOf(
"rgba(255, 255, 255, 0.87)") : Color.valueOf("rgba(0, 0, 0, 0.87)"));
if (colorLabelVisible.get()) {
displayNode.setText(JFXNodeUtils.colorToHex(circleColor));
} else {
displayNode.setText("");
}
}
private void initColor() {
final ColorPicker colorPicker = (ColorPicker) getSkinnable();
Color color = colorPicker.getValue();
Color circleColor = color == null ? Color.WHITE : color;
// update picker box color
colorBoxBackground.set(new Background(new BackgroundFill(circleColor, new CornerRadii(3), Insets.EMPTY)));
// update label color
textFill.set(circleColor.grayscale().getRed() < 0.5 ? Color.valueOf(
"rgba(255, 255, 255, 0.87)") : Color.valueOf("rgba(0, 0, 0, 0.87)"));
if (colorLabelVisible.get()) {
displayNode.setText(JFXNodeUtils.colorToHex(circleColor));
} else {
displayNode.setText("");
}
}
/***************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
private static final class StyleableProperties {
private static final CssMetaData<ColorPicker, Boolean> COLOR_LABEL_VISIBLE =
new CssMetaData<ColorPicker, Boolean>("-fx-color-label-visible",
BooleanConverter.getInstance(), Boolean.TRUE) {
@Override
public boolean isSettable(ColorPicker n) {
final JFXColorPickerSkin skin = (JFXColorPickerSkin) n.getSkin();
return skin.colorLabelVisible == null || !skin.colorLabelVisible.isBound();
}
@Override
public StyleableProperty<Boolean> getStyleableProperty(ColorPicker n) {
final JFXColorPickerSkin skin = (JFXColorPickerSkin) n.getSkin();
return skin.colorLabelVisible;
}
};
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables =
new ArrayList<>(ComboBoxPopupControl.getClassCssMetaData());
styleables.add(COLOR_LABEL_VISIBLE);
STYLEABLES = Collections.unmodifiableList(styleables);
}
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return getClassCssMetaData();
}
protected TextField getEditor() {
return null;
}
protected javafx.util.StringConverter<Color> getConverter() {
return null;
}
}
@@ -0,0 +1,629 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.skins;
import com.jfoenix.effects.JFXDepthManager;
import com.jfoenix.transitions.CachedTransition;
import javafx.animation.Animation.Status;
import javafx.animation.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.effect.ColorAdjust;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Path;
import javafx.util.Duration;
/**
* @author Shadi Shaheen & Bassel El Mabsout this UI allows the user to pick a color using HSL color system
*/
final class JFXColorPickerUI extends Pane {
private CachedTransition selectorTransition;
private int pickerSize = 400;
// sl circle selector size
private final int selectorSize = 20;
private final double centerX;
private final double centerY;
private final double huesRadius;
private final double slRadius;
private double currentHue = 0;
private final ImageView huesCircleView;
private final ImageView slCircleView;
private final Pane colorSelector;
private final Pane selector;
private CurveTransition colorsTransition;
public JFXColorPickerUI(int pickerSize) {
JFXDepthManager.setDepth(this, 1);
this.pickerSize = pickerSize;
this.centerX = (double) pickerSize / 2;
this.centerY = (double) pickerSize / 2;
final double pickerRadius = (double) pickerSize / 2;
this.huesRadius = pickerRadius * 0.9;
final double huesSmallR = pickerRadius * 0.8;
final double huesLargeR = pickerRadius;
this.slRadius = pickerRadius * 0.7;
// Create Hues Circle
huesCircleView = new ImageView(getHuesCircle(pickerSize, pickerSize));
// clip to smooth the edges
Circle outterCircle = new Circle(centerX, centerY, huesLargeR - 2);
Circle innterCircle = new Circle(centerX, centerY, huesSmallR + 2);
huesCircleView.setClip(Path.subtract(outterCircle, innterCircle));
this.getChildren().add(huesCircleView);
// create Hues Circle Selector
Circle r1 = new Circle(pickerRadius - huesSmallR);
Circle r2 = new Circle(pickerRadius - huesRadius);
colorSelector = new Pane();
colorSelector.setStyle("-fx-border-color:#424242; -fx-border-width:1px; -fx-background-color:rgba(255, 255, 255, 0.87);");
colorSelector.setPrefSize(pickerRadius - huesSmallR, pickerRadius - huesSmallR);
colorSelector.setShape(Path.subtract(r1, r2));
colorSelector.setCache(true);
colorSelector.setMouseTransparent(true);
colorSelector.setPickOnBounds(false);
this.getChildren().add(colorSelector);
// add Hues Selection Listeners
huesCircleView.addEventHandler(MouseEvent.MOUSE_DRAGGED, (event) -> {
if (colorsTransition != null) {
colorsTransition.stop();
}
double dx = event.getX() - centerX;
double dy = event.getY() - centerY;
double theta = Math.atan2(dy, dx);
double x = centerX + huesRadius * Math.cos(theta);
double y = centerY + huesRadius * Math.sin(theta);
colorSelector.setRotate(90 + Math.toDegrees(Math.atan2(dy, dx)));
colorSelector.setTranslateX(x - colorSelector.getPrefWidth() / 2);
colorSelector.setTranslateY(y - colorSelector.getPrefHeight() / 2);
});
huesCircleView.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> {
double dx = event.getX() - centerX;
double dy = event.getY() - centerY;
double theta = Math.atan2(dy, dx);
double x = centerX + huesRadius * Math.cos(theta);
double y = centerY + huesRadius * Math.sin(theta);
colorsTransition = new CurveTransition(new Point2D(colorSelector.getTranslateX() + colorSelector.getPrefWidth() / 2,
colorSelector.getTranslateY() + colorSelector.getPrefHeight() / 2),
new Point2D(x, y));
colorsTransition.play();
});
colorSelector.translateXProperty()
.addListener((o, oldVal, newVal) -> updateHSLCircleColor((int) (newVal.intValue() + colorSelector.getPrefWidth() / 2),
(int) (colorSelector.getTranslateY() + colorSelector
.getPrefHeight() / 2)));
colorSelector.translateYProperty()
.addListener((o, oldVal, newVal) -> updateHSLCircleColor((int) (colorSelector.getTranslateX() + colorSelector
.getPrefWidth() / 2), (int) (newVal.intValue() + colorSelector.getPrefHeight() / 2)));
// Create SL Circle
slCircleView = new ImageView(getSLCricle(pickerSize, pickerSize));
slCircleView.setClip(new Circle(centerX, centerY, slRadius - 2));
slCircleView.setPickOnBounds(false);
this.getChildren().add(slCircleView);
// create SL Circle Selector
selector = new Pane();
Circle c1 = new Circle(selectorSize / 2);
Circle c2 = new Circle((selectorSize / 2) * 0.5);
selector.setShape(Path.subtract(c1, c2));
selector.setStyle(
"-fx-border-color:#424242; -fx-border-width:1px;-fx-background-color:rgba(255, 255, 255, 0.87);");
selector.setPrefSize(selectorSize, selectorSize);
selector.setMinSize(selectorSize, selectorSize);
selector.setMaxSize(selectorSize, selectorSize);
selector.setCache(true);
selector.setMouseTransparent(true);
this.getChildren().add(selector);
// add SL selection Listeners
slCircleView.addEventHandler(MouseEvent.MOUSE_DRAGGED, (event) -> {
if (selectorTransition != null) {
selectorTransition.stop();
}
if (Math.pow(event.getX() - centerX, 2) + Math.pow(event.getY() - centerY, 2) < Math.pow(slRadius - 2, 2)) {
selector.setTranslateX(event.getX() - selector.getPrefWidth() / 2);
selector.setTranslateY(event.getY() - selector.getPrefHeight() / 2);
} else {
double dx = event.getX() - centerX;
double dy = event.getY() - centerY;
double theta = Math.atan2(dy, dx);
double x = centerX + (slRadius - 2) * Math.cos(theta);
double y = centerY + (slRadius - 2) * Math.sin(theta);
selector.setTranslateX(x - selector.getPrefWidth() / 2);
selector.setTranslateY(y - selector.getPrefHeight() / 2);
}
});
slCircleView.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> {
selectorTransition = new CachedTransition(selector, new Timeline(new KeyFrame(Duration.millis(1000),
new KeyValue(selector.translateXProperty(),
event.getX() - selector.getPrefWidth() / 2,
Interpolator.EASE_BOTH),
new KeyValue(selector.translateYProperty(),
event.getY() - selector.getPrefHeight() / 2,
Interpolator.EASE_BOTH)))) {
{
setCycleDuration(Duration.millis(160));
setDelay(Duration.seconds(0));
}
};
selectorTransition.play();
});
// add slCircleView listener
selector.translateXProperty()
.addListener((o, oldVal, newVal) -> setColorAtLocation(newVal.intValue() + selectorSize / 2,
(int) selector.getTranslateY() + selectorSize / 2));
selector.translateYProperty()
.addListener((o, oldVal, newVal) -> setColorAtLocation((int) selector.getTranslateX() + selectorSize / 2,
newVal.intValue() + selectorSize / 2));
// initial color selection
double dx = 20 - centerX;
double dy = 20 - centerY;
double theta = Math.atan2(dy, dx);
double x = centerX + huesRadius * Math.cos(theta);
double y = centerY + huesRadius * Math.sin(theta);
colorSelector.setRotate(90 + Math.toDegrees(Math.atan2(dy, dx)));
colorSelector.setTranslateX(x - colorSelector.getPrefWidth() / 2);
colorSelector.setTranslateY(y - colorSelector.getPrefHeight() / 2);
selector.setTranslateX(centerX - selector.getPrefWidth() / 2);
selector.setTranslateY(centerY - selector.getPrefHeight() / 2);
}
/**
* List of Color Nodes that needs to be updated when picking a color
*/
private final ObservableList<Node> colorNodes = FXCollections.observableArrayList();
public void addColorSelectionNode(Node... nodes) {
colorNodes.addAll(nodes);
}
public void removeColorSelectionNode(Node... nodes) {
colorNodes.removeAll(nodes);
}
private void updateHSLCircleColor(int x, int y) {
// transform color to HSL space
Color color = huesCircleView.getImage().getPixelReader().getColor(x, y);
double max = Math.max(color.getRed(), Math.max(color.getGreen(), color.getBlue()));
double min = Math.min(color.getRed(), Math.min(color.getGreen(), color.getBlue()));
double hue = 0;
if (max != min) {
double d = max - min;
if (max == color.getRed()) {
hue = (color.getGreen() - color.getBlue()) / d + (color.getGreen() < color.getBlue() ? 6 : 0);
} else if (max == color.getGreen()) {
hue = (color.getBlue() - color.getRed()) / d + 2;
} else if (max == color.getBlue()) {
hue = (color.getRed() - color.getGreen()) / d + 4;
}
hue /= 6;
}
currentHue = map(hue, 0, 1, 0, 255);
// refresh the HSL circle
refreshHSLCircle();
}
private void refreshHSLCircle() {
ColorAdjust colorAdjust = new ColorAdjust();
colorAdjust.setHue(map(currentHue + (currentHue < 127.5 ? 1 : -1) * 127.5, 0, 255, -1, 1));
slCircleView.setEffect(colorAdjust);
setColorAtLocation((int) selector.getTranslateX() + selectorSize / 2,
(int) selector.getTranslateY() + selectorSize / 2);
}
/**
* this method is used to move selectors to a certain color
*/
private boolean allowColorChange = true;
private ParallelTransition pTrans;
public void moveToColor(Color color) {
allowColorChange = false;
double max = Math.max(color.getRed(),
Math.max(color.getGreen(), color.getBlue())), min = Math.min(color.getRed(),
Math.min(color.getGreen(),
color.getBlue()));
double hue = 0;
double l = (max + min) / 2;
double s = 0;
if (max == min) {
hue = s = 0; // achromatic
} else {
double d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
if (max == color.getRed()) {
hue = (color.getGreen() - color.getBlue()) / d + (color.getGreen() < color.getBlue() ? 6 : 0);
} else if (max == color.getGreen()) {
hue = (color.getBlue() - color.getRed()) / d + 2;
} else if (max == color.getBlue()) {
hue = (color.getRed() - color.getGreen()) / d + 4;
}
hue /= 6;
}
currentHue = map(hue, 0, 1, 0, 255);
// Animate Hue
double theta = map(currentHue, 0, 255, -Math.PI, Math.PI);
double x = centerX + huesRadius * Math.cos(theta);
double y = centerY + huesRadius * Math.sin(theta);
colorsTransition = new CurveTransition(
new Point2D(
colorSelector.getTranslateX() + colorSelector.getPrefWidth() / 2,
colorSelector.getTranslateY() + colorSelector.getPrefHeight() / 2
),
new Point2D(x, y));
// Animate SL
s = map(s, 0, 1, 0, 255);
l = map(l, 0, 1, 0, 255);
Point2D point = getPointFromSL((int) s, (int) l, slRadius);
double pX = centerX - point.getX();
double pY = centerY - point.getY();
double endPointX;
double endPointY;
if (Math.pow(pX - centerX, 2) + Math.pow(pY - centerY, 2) < Math.pow(slRadius - 2, 2)) {
endPointX = pX - selector.getPrefWidth() / 2;
endPointY = pY - selector.getPrefHeight() / 2;
} else {
double dx = pX - centerX;
double dy = pY - centerY;
theta = Math.atan2(dy, dx);
x = centerX + (slRadius - 2) * Math.cos(theta);
y = centerY + (slRadius - 2) * Math.sin(theta);
endPointX = x - selector.getPrefWidth() / 2;
endPointY = y - selector.getPrefHeight() / 2;
}
selectorTransition = new CachedTransition(selector, new Timeline(new KeyFrame(Duration.millis(1000),
new KeyValue(selector.translateXProperty(),
endPointX,
Interpolator.EASE_BOTH),
new KeyValue(selector.translateYProperty(),
endPointY,
Interpolator.EASE_BOTH)))) {
{
setCycleDuration(Duration.millis(160));
setDelay(Duration.seconds(0));
}
};
if (pTrans != null) {
pTrans.stop();
}
pTrans = new ParallelTransition(colorsTransition, selectorTransition);
pTrans.setOnFinished((finish) -> {
if (pTrans.getStatus() == Status.STOPPED) {
allowColorChange = true;
}
});
pTrans.play();
refreshHSLCircle();
}
private void setColorAtLocation(int x, int y) {
if (allowColorChange) {
Color color = getColorAtLocation(x, y);
String colorString = "rgb(" + color.getRed() * 255 + "," + color.getGreen() * 255 + "," + color.getBlue() * 255 + ");";
for (Node node : colorNodes)
node.setStyle("-fx-background-color:" + colorString + "; -fx-fill:" + colorString + ";");
}
}
private Color getColorAtLocation(double x, double y) {
double dy = x - centerX;
double dx = y - centerY;
return getColor(dx, dy);
}
private Image getHuesCircle(int width, int height) {
WritableImage raster = new WritableImage(width, height);
PixelWriter pixelWriter = raster.getPixelWriter();
Point2D center = new Point2D((double) width / 2, (double) height / 2);
double rsmall = 0.8 * width / 2;
double rbig = (double) width / 2;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
double dx = x - center.getX();
double dy = y - center.getY();
double distance = Math.sqrt((dx * dx) + (dy * dy));
double o = Math.atan2(dy, dx);
if (distance > rsmall && distance < rbig) {
double H = map(o, -Math.PI, Math.PI, 0, 255);
double S = 255;
double L = 152;
pixelWriter.setColor(x, y, HSL2RGB(H, S, L));
}
}
}
return raster;
}
private Image getSLCricle(int width, int height) {
WritableImage raster = new WritableImage(width, height);
PixelWriter pixelWriter = raster.getPixelWriter();
Point2D center = new Point2D((double) width / 2, (double) height / 2);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
double dy = x - center.getX();
double dx = y - center.getY();
pixelWriter.setColor(x, y, getColor(dx, dy));
}
}
return raster;
}
private double clamp(double from, double small, double big) {
return Math.min(Math.max(from, small), big);
}
private Color getColor(double dx, double dy) {
double distance = Math.sqrt((dx * dx) + (dy * dy));
double rverysmall = 0.65 * ((double) pickerSize / 2);
Color pixelColor = Color.BLUE;
if (distance <= rverysmall * 1.1) {
double angle = -Math.PI / 2.;
double angle1 = angle + 2 * Math.PI / 3.;
double angle2 = angle1 + 2 * Math.PI / 3.;
double x1 = rverysmall * Math.sin(angle1);
double y1 = rverysmall * Math.cos(angle1);
double x2 = rverysmall * Math.sin(angle2);
double y2 = rverysmall * Math.cos(angle2);
dx += 0.01;
double[] circle = circleFrom3Points(new Point2D(x1, y1), new Point2D(x2, y2), new Point2D(dx, dy));
double xArc = circle[0];
double yArc = 0;
double arcR = circle[2];
double Arco = Math.atan2(dx - xArc, dy - yArc);
double Arco1 = Math.atan2(x1 - xArc, y1 - yArc);
double Arco2 = Math.atan2(x2 - xArc, y2 - yArc);
double finalX = xArc > 0 ? xArc - arcR : xArc + arcR;
double saturation = map(finalX, -rverysmall, rverysmall, 255, 0);
double lightness = 255;
double diffAngle = Arco2 - Arco1;
double diffArco = Arco - Arco1;
if (dx < x1) {
diffAngle = diffAngle < 0 ? 2 * Math.PI + diffAngle : diffAngle;
diffAngle = Math.abs(2 * Math.PI - diffAngle);
diffArco = diffArco < 0 ? 2 * Math.PI + diffArco : diffArco;
diffArco = Math.abs(2 * Math.PI - diffArco);
}
lightness = map(diffArco, 0, diffAngle, 0, 255);
if (distance > rverysmall) {
saturation = 255 - saturation;
if (lightness < 0 && dy < 0) {
lightness = 255;
}
}
lightness = clamp(lightness, 0, 255);
if ((saturation < 10 && dx < x1) || (saturation > 240 && dx > x1)) {
saturation = 255 - saturation;
}
saturation = clamp(saturation, 0, 255);
pixelColor = HSL2RGB(currentHue, saturation, lightness);
}
return pixelColor;
}
/***************************************************************************
* *
* Hues Animation *
* *
**************************************************************************/
private final class CurveTransition extends Transition {
Point2D from;
double fromTheta;
double toTheta;
public CurveTransition(Point2D from, Point2D to) {
this.from = from;
double fromDx = from.getX() - centerX;
double fromDy = from.getY() - centerY;
fromTheta = Math.atan2(fromDy, fromDx);
double toDx = to.getX() - centerX;
double toDy = to.getY() - centerY;
toTheta = Math.atan2(toDy, toDx);
setInterpolator(Interpolator.EASE_BOTH);
setDelay(Duration.millis(0));
setCycleDuration(Duration.millis(240));
}
@Override
protected void interpolate(double frac) {
double dif = Math.min(Math.abs(toTheta - fromTheta), 2 * Math.PI - Math.abs(toTheta - fromTheta));
if (dif == 2 * Math.PI - Math.abs(toTheta - fromTheta)) {
int dir = -1;
if (toTheta < fromTheta) {
dir = 1;
}
dif = dir * dif;
} else {
dif = toTheta - fromTheta;
}
Point2D newP = rotate(from, new Point2D(centerX, centerY), frac * dif);
colorSelector.setRotate(90 + Math.toDegrees(Math.atan2(newP.getY() - centerY, newP.getX() - centerX)));
colorSelector.setTranslateX(newP.getX() - colorSelector.getPrefWidth() / 2);
colorSelector.setTranslateY(newP.getY() - colorSelector.getPrefHeight() / 2);
}
}
/***************************************************************************
* *
* Util methods *
* *
**************************************************************************/
private double map(double val, double min1, double max1, double min2, double max2) {
return min2 + (max2 - min2) * ((val - min1) / (max1 - min1));
}
private Color HSL2RGB(double hue, double sat, double lum) {
hue = map(hue, 0, 255, 0, 359);
sat = map(sat, 0, 255, 0, 1);
lum = map(lum, 0, 255, 0, 1);
double v;
double red, green, blue;
double m;
double sv;
int sextant;
double fract, vsf, mid1, mid2;
red = lum; // default to gray
green = lum;
blue = lum;
v = (lum <= 0.5) ? (lum * (1.0 + sat)) : (lum + sat - lum * sat);
m = lum + lum - v;
sv = (v - m) / v;
hue /= 60.0; //get into range 0..6
sextant = (int) Math.floor(hue); // int32 rounds up or down.
fract = hue - sextant;
vsf = v * sv * fract;
mid1 = m + vsf;
mid2 = v - vsf;
if (v > 0) {
switch (sextant) {
case 0:
red = v;
green = mid1;
blue = m;
break;
case 1:
red = mid2;
green = v;
blue = m;
break;
case 2:
red = m;
green = v;
blue = mid1;
break;
case 3:
red = m;
green = mid2;
blue = v;
break;
case 4:
red = mid1;
green = m;
blue = v;
break;
case 5:
red = v;
green = m;
blue = mid2;
break;
}
}
return new Color(red, green, blue, 1);
}
private double[] circleFrom3Points(Point2D a, Point2D b, Point2D c) {
double ax, ay, bx, by, cx, cy, x1, y11, dx1, dy1, x2, y2, dx2, dy2, ox, oy, dx, dy, radius; // Variables Used and to Declared
ax = a.getX();
ay = a.getY(); //first Point X and Y
bx = b.getX();
by = b.getY(); // Second Point X and Y
cx = c.getX();
cy = c.getY(); // Third Point X and Y
////****************Following are Basic Procedure**********************///
x1 = (bx + ax) / 2;
y11 = (by + ay) / 2;
dy1 = bx - ax;
dx1 = -(by - ay);
x2 = (cx + bx) / 2;
y2 = (cy + by) / 2;
dy2 = cx - bx;
dx2 = -(cy - by);
ox = (y11 * dx1 * dx2 + x2 * dx1 * dy2 - x1 * dy1 * dx2 - y2 * dx1 * dx2) / (dx1 * dy2 - dy1 * dx2);
oy = (ox - x1) * dy1 / dx1 + y11;
dx = ox - ax;
dy = oy - ay;
radius = Math.sqrt(dx * dx + dy * dy);
return new double[]{ox, oy, radius};
}
private Point2D getPointFromSL(int saturation, int lightness, double radius) {
double dy = map(saturation, 0, 255, -radius, radius);
double angle = 0.;
double angle1 = angle + 2 * Math.PI / 3.;
double angle2 = angle1 + 2 * Math.PI / 3.;
double x1 = radius * Math.sin(angle1);
double y1 = radius * Math.cos(angle1);
double x2 = radius * Math.sin(angle2);
double y2 = radius * Math.cos(angle2);
double dx = 0;
double[] circle = circleFrom3Points(new Point2D(x1, y1), new Point2D(dx, dy), new Point2D(x2, y2));
double xArc = circle[0];
double yArc = circle[1];
double arcR = circle[2];
double Arco1 = Math.atan2(x1 - xArc, y1 - yArc);
double Arco2 = Math.atan2(x2 - xArc, y2 - yArc);
double ArcoFinal = map(lightness, 0, 255, Arco2, Arco1);
double finalX = xArc + arcR * Math.sin(ArcoFinal);
double finalY = yArc + arcR * Math.cos(ArcoFinal);
if (dy < y1) {
ArcoFinal = map(lightness, 0, 255, Arco1, Arco2 + 2 * Math.PI);
finalX = -xArc - arcR * Math.sin(ArcoFinal);
finalY = yArc + arcR * Math.cos(ArcoFinal);
}
return new Point2D(finalX, finalY);
}
private Point2D rotate(Point2D a, Point2D center, double angle) {
double resultX = center.getX() + (a.getX() - center.getX()) * Math.cos(angle) - (a.getY() - center.getY()) * Math
.sin(angle);
double resultY = center.getY() + (a.getX() - center.getX()) * Math.sin(angle) + (a.getY() - center.getY()) * Math
.cos(angle);
return new Point2D(resultX, resultY);
}
}
@@ -0,0 +1,520 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.skins;
import com.jfoenix.effects.JFXDepthManager;
import com.jfoenix.transitions.CachedTransition;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.effect.DropShadow;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.scene.transform.Rotate;
import javafx.util.Duration;
import java.util.ArrayList;
import static javafx.animation.Interpolator.EASE_BOTH;
/// @author Shadi Shaheen
final class JFXCustomColorPicker extends Pane {
ObjectProperty<RecentColorPath> selectedPath = new SimpleObjectProperty<>();
private MoveTo startPoint;
private CubicCurveTo curve0To;
private CubicCurveTo outerCircleCurveTo;
private CubicCurveTo curve1To;
private CubicCurveTo innerCircleCurveTo;
private final ArrayList<CubicCurve> curves = new ArrayList<>();
private final double distance = 200;
private final double centerX = distance;
private final double centerY = distance;
private final double radius = 110;
private static final int shapesNumber = 13;
private final ArrayList<RecentColorPath> shapes = new ArrayList<>();
private CachedTransition showAnimation;
private final JFXColorPickerUI hslColorPicker;
public JFXCustomColorPicker() {
this.setPickOnBounds(false);
this.setMinSize(distance * 2, distance * 2);
final DoubleProperty rotationAngle = new SimpleDoubleProperty(2.1);
// draw recent colors shape using cubic curves
init(rotationAngle, centerX + 53, centerY + 162);
hslColorPicker = new JFXColorPickerUI((int) distance);
hslColorPicker.setLayoutX(centerX - distance / 2);
hslColorPicker.setLayoutY(centerY - distance / 2);
this.getChildren().add(hslColorPicker);
// add recent colors shapes
final int shapesStartIndex = this.getChildren().size();
final int shapesEndIndex = shapesStartIndex + shapesNumber;
for (int i = 0; i < shapesNumber; i++) {
final double angle = 2 * i * Math.PI / shapesNumber;
final RecentColorPath path = new RecentColorPath(startPoint,
curve0To,
outerCircleCurveTo,
curve1To,
innerCircleCurveTo);
shapes.add(path);
path.setPickOnBounds(false);
final Rotate rotate = new Rotate(Math.toDegrees(angle), centerX, centerY);
path.getTransforms().add(rotate);
this.getChildren().add(shapesStartIndex, path);
path.setFill(Color.valueOf(getDefaultColor(i)));
path.setFocusTraversable(true);
path.addEventHandler(MouseEvent.MOUSE_CLICKED, (event) -> {
path.requestFocus();
selectedPath.set(path);
});
}
// add selection listeners
selectedPath.addListener((o, oldVal, newVal) -> {
if (oldVal != null) {
hslColorPicker.removeColorSelectionNode(oldVal);
oldVal.playTransition(-1);
}
// re-arrange children
while (this.getChildren().indexOf(newVal) != shapesEndIndex - 1) {
final Node temp = this.getChildren().get(shapesEndIndex - 1);
this.getChildren().remove(shapesEndIndex - 1);
this.getChildren().add(shapesStartIndex, temp);
}
// update path fill according to the color picker
newVal.setStroke(Color.rgb(255, 255, 255, 0.87));
newVal.playTransition(1);
hslColorPicker.moveToColor((Color) newVal.getFill());
hslColorPicker.addColorSelectionNode(newVal);
});
// init selection
selectedPath.set((RecentColorPath) this.getChildren().get(shapesStartIndex));
}
public int getShapesNumber() {
return shapesNumber;
}
public int getSelectedIndex() {
if (selectedPath.get() != null) {
return shapes.indexOf(selectedPath.get());
}
return -1;
}
public void setColor(final Color color) {
shapes.get(getSelectedIndex()).setFill(color);
hslColorPicker.moveToColor(color);
}
public Color getColor(final int index) {
if (index >= 0 && index < shapes.size()) {
return (Color) shapes.get(index).getFill();
} else {
return Color.WHITE;
}
}
public void preAnimate() {
final CubicCurve firstCurve = curves.get(0);
final double x = firstCurve.getStartX();
final double y = firstCurve.getStartY();
firstCurve.setStartX(centerX);
firstCurve.setStartY(centerY);
final CubicCurve secondCurve = curves.get(1);
final double x1 = secondCurve.getStartX();
final double y1 = secondCurve.getStartY();
secondCurve.setStartX(centerX);
secondCurve.setStartY(centerY);
final double cx1 = firstCurve.getControlX1();
final double cy1 = firstCurve.getControlY1();
firstCurve.setControlX1(centerX + radius);
firstCurve.setControlY1(centerY + radius / 2);
final KeyFrame keyFrame = new KeyFrame(Duration.millis(1000),
new KeyValue(firstCurve.startXProperty(), x, EASE_BOTH),
new KeyValue(firstCurve.startYProperty(), y, EASE_BOTH),
new KeyValue(secondCurve.startXProperty(), x1, EASE_BOTH),
new KeyValue(secondCurve.startYProperty(), y1, EASE_BOTH),
new KeyValue(firstCurve.controlX1Property(), cx1, EASE_BOTH),
new KeyValue(firstCurve.controlY1Property(), cy1, EASE_BOTH)
);
final Timeline timeline = new Timeline(keyFrame);
showAnimation = new CachedTransition(this, timeline) {
{
setCycleDuration(Duration.millis(240));
setDelay(Duration.millis(0));
}
};
}
public void animate() {
showAnimation.play();
}
private void init(final DoubleProperty rotationAngle, final double initControlX1, final double initControlY1) {
final Circle innerCircle = new Circle(centerX, centerY, radius, Color.TRANSPARENT);
final Circle outerCircle = new Circle(centerX, centerY, radius * 2, Color.web("blue", 0.5));
// Create a composite shape of 4 cubic curves
// create 2 cubic curves of the shape
createQuadraticCurve(rotationAngle, initControlX1, initControlY1);
// inner circle curve
final CubicCurve innerCircleCurve = new CubicCurve();
innerCircleCurve.startXProperty().bind(curves.get(0).startXProperty());
innerCircleCurve.startYProperty().bind(curves.get(0).startYProperty());
innerCircleCurve.endXProperty().bind(curves.get(1).startXProperty());
innerCircleCurve.endYProperty().bind(curves.get(1).startYProperty());
curves.get(0).startXProperty().addListener((o, oldVal, newVal) -> {
final Point2D controlPoint = makeControlPoint(newVal.doubleValue(),
curves.get(0).getStartY(),
innerCircle,
shapesNumber,
-1);
innerCircleCurve.setControlX1(controlPoint.getX());
innerCircleCurve.setControlY1(controlPoint.getY());
});
curves.get(0).startYProperty().addListener((o, oldVal, newVal) -> {
final Point2D controlPoint = makeControlPoint(curves.get(0).getStartX(),
newVal.doubleValue(),
innerCircle,
shapesNumber,
-1);
innerCircleCurve.setControlX1(controlPoint.getX());
innerCircleCurve.setControlY1(controlPoint.getY());
});
curves.get(1).startXProperty().addListener((o, oldVal, newVal) -> {
final Point2D controlPoint = makeControlPoint(newVal.doubleValue(),
curves.get(1).getStartY(),
innerCircle,
shapesNumber,
1);
innerCircleCurve.setControlX2(controlPoint.getX());
innerCircleCurve.setControlY2(controlPoint.getY());
});
curves.get(1).startYProperty().addListener((o, oldVal, newVal) -> {
final Point2D controlPoint = makeControlPoint(curves.get(1).getStartX(),
newVal.doubleValue(),
innerCircle,
shapesNumber,
1);
innerCircleCurve.setControlX2(controlPoint.getX());
innerCircleCurve.setControlY2(controlPoint.getY());
});
Point2D controlPoint = makeControlPoint(curves.get(0).getStartX(),
curves.get(0).getStartY(),
innerCircle,
shapesNumber,
-1);
innerCircleCurve.setControlX1(controlPoint.getX());
innerCircleCurve.setControlY1(controlPoint.getY());
controlPoint = makeControlPoint(curves.get(1).getStartX(),
curves.get(1).getStartY(),
innerCircle,
shapesNumber,
1);
innerCircleCurve.setControlX2(controlPoint.getX());
innerCircleCurve.setControlY2(controlPoint.getY());
// outer circle curve
final CubicCurve outerCircleCurve = new CubicCurve();
outerCircleCurve.startXProperty().bind(curves.get(0).endXProperty());
outerCircleCurve.startYProperty().bind(curves.get(0).endYProperty());
outerCircleCurve.endXProperty().bind(curves.get(1).endXProperty());
outerCircleCurve.endYProperty().bind(curves.get(1).endYProperty());
controlPoint = makeControlPoint(curves.get(0).getEndX(),
curves.get(0).getEndY(),
outerCircle,
shapesNumber,
-1);
outerCircleCurve.setControlX1(controlPoint.getX());
outerCircleCurve.setControlY1(controlPoint.getY());
controlPoint = makeControlPoint(curves.get(1).getEndX(), curves.get(1).getEndY(), outerCircle, shapesNumber, 1);
outerCircleCurve.setControlX2(controlPoint.getX());
outerCircleCurve.setControlY2(controlPoint.getY());
startPoint = new MoveTo();
startPoint.xProperty().bind(curves.get(0).startXProperty());
startPoint.yProperty().bind(curves.get(0).startYProperty());
curve0To = new CubicCurveTo();
curve0To.controlX1Property().bind(curves.get(0).controlX1Property());
curve0To.controlY1Property().bind(curves.get(0).controlY1Property());
curve0To.controlX2Property().bind(curves.get(0).controlX2Property());
curve0To.controlY2Property().bind(curves.get(0).controlY2Property());
curve0To.xProperty().bind(curves.get(0).endXProperty());
curve0To.yProperty().bind(curves.get(0).endYProperty());
outerCircleCurveTo = new CubicCurveTo();
outerCircleCurveTo.controlX1Property().bind(outerCircleCurve.controlX1Property());
outerCircleCurveTo.controlY1Property().bind(outerCircleCurve.controlY1Property());
outerCircleCurveTo.controlX2Property().bind(outerCircleCurve.controlX2Property());
outerCircleCurveTo.controlY2Property().bind(outerCircleCurve.controlY2Property());
outerCircleCurveTo.xProperty().bind(outerCircleCurve.endXProperty());
outerCircleCurveTo.yProperty().bind(outerCircleCurve.endYProperty());
curve1To = new CubicCurveTo();
curve1To.controlX1Property().bind(curves.get(1).controlX2Property());
curve1To.controlY1Property().bind(curves.get(1).controlY2Property());
curve1To.controlX2Property().bind(curves.get(1).controlX1Property());
curve1To.controlY2Property().bind(curves.get(1).controlY1Property());
curve1To.xProperty().bind(curves.get(1).startXProperty());
curve1To.yProperty().bind(curves.get(1).startYProperty());
innerCircleCurveTo = new CubicCurveTo();
innerCircleCurveTo.controlX1Property().bind(innerCircleCurve.controlX2Property());
innerCircleCurveTo.controlY1Property().bind(innerCircleCurve.controlY2Property());
innerCircleCurveTo.controlX2Property().bind(innerCircleCurve.controlX1Property());
innerCircleCurveTo.controlY2Property().bind(innerCircleCurve.controlY1Property());
innerCircleCurveTo.xProperty().bind(innerCircleCurve.startXProperty());
innerCircleCurveTo.yProperty().bind(innerCircleCurve.startYProperty());
}
private void createQuadraticCurve(final DoubleProperty rotationAngle, final double initControlX1, final double initControlY1) {
for (int i = 0; i < 2; i++) {
double angle = 2 * i * Math.PI / shapesNumber;
double xOffset = radius * Math.cos(angle);
double yOffset = radius * Math.sin(angle);
final double startx = centerX + xOffset;
final double starty = centerY + yOffset;
final double diffStartCenterX = startx - centerX;
final double diffStartCenterY = starty - centerY;
final double sinRotAngle = Math.sin(rotationAngle.get());
final double cosRotAngle = Math.cos(rotationAngle.get());
final double startXR = cosRotAngle * diffStartCenterX - sinRotAngle * diffStartCenterY + centerX;
final double startYR = sinRotAngle * diffStartCenterX + cosRotAngle * diffStartCenterY + centerY;
angle = 2 * i * Math.PI / shapesNumber;
xOffset = distance * Math.cos(angle);
yOffset = distance * Math.sin(angle);
final double endx = centerX + xOffset;
final double endy = centerY + yOffset;
final CubicCurve curvedLine = new CubicCurve();
curvedLine.setStartX(startXR);
curvedLine.setStartY(startYR);
curvedLine.setControlX1(startXR);
curvedLine.setControlY1(startYR);
curvedLine.setControlX2(endx);
curvedLine.setControlY2(endy);
curvedLine.setEndX(endx);
curvedLine.setEndY(endy);
curvedLine.setStroke(Color.FORESTGREEN);
curvedLine.setStrokeWidth(1);
curvedLine.setStrokeLineCap(StrokeLineCap.ROUND);
curvedLine.setFill(Color.TRANSPARENT);
curvedLine.setMouseTransparent(true);
rotationAngle.addListener((o, oldVal, newVal) -> {
final double newstartXR = ((cosRotAngle * diffStartCenterX) - (sinRotAngle * diffStartCenterY)) + centerX;
final double newstartYR = (sinRotAngle * diffStartCenterX) + (cosRotAngle * diffStartCenterY) + centerY;
curvedLine.setStartX(newstartXR);
curvedLine.setStartY(newstartYR);
});
curves.add(curvedLine);
if (i == 0) {
curvedLine.setControlX1(initControlX1);
curvedLine.setControlY1(initControlY1);
} else {
final CubicCurve firstCurve = curves.get(0);
final double curveTheta = 2 * curves.indexOf(curvedLine) * Math.PI / shapesNumber;
curvedLine.controlX1Property().bind(Bindings.createDoubleBinding(() -> {
final double a = firstCurve.getControlX1() - centerX;
final double b = Math.sin(curveTheta) * (firstCurve.getControlY1() - centerY);
return ((Math.cos(curveTheta) * a) - b) + centerX;
}, firstCurve.controlX1Property(), firstCurve.controlY1Property()));
curvedLine.controlY1Property().bind(Bindings.createDoubleBinding(() -> {
final double a = Math.sin(curveTheta) * (firstCurve.getControlX1() - centerX);
final double b = Math.cos(curveTheta) * (firstCurve.getControlY1() - centerY);
return a + b + centerY;
}, firstCurve.controlX1Property(), firstCurve.controlY1Property()));
curvedLine.controlX2Property().bind(Bindings.createDoubleBinding(() -> {
final double a = firstCurve.getControlX2() - centerX;
final double b = firstCurve.getControlY2() - centerY;
return ((Math.cos(curveTheta) * a) - (Math.sin(curveTheta) * b)) + centerX;
}, firstCurve.controlX2Property(), firstCurve.controlY2Property()));
curvedLine.controlY2Property().bind(Bindings.createDoubleBinding(() -> {
final double a = Math.sin(curveTheta) * (firstCurve.getControlX2() - centerX);
final double b = Math.cos(curveTheta) * (firstCurve.getControlY2() - centerY);
return a + b + centerY;
}, firstCurve.controlX2Property(), firstCurve.controlY2Property()));
}
}
}
private String getDefaultColor(final int i) {
String color = "#FFFFFF";
switch (i) {
case 0:
color = "#8F3F7E";
break;
case 1:
color = "#B5305F";
break;
case 2:
color = "#CE584A";
break;
case 3:
color = "#DB8D5C";
break;
case 4:
color = "#DA854E";
break;
case 5:
color = "#E9AB44";
break;
case 6:
color = "#FEE435";
break;
case 7:
color = "#99C286";
break;
case 8:
color = "#01A05E";
break;
case 9:
color = "#4A8895";
break;
case 10:
color = "#16669B";
break;
case 11:
color = "#2F65A5";
break;
case 12:
color = "#4E6A9C";
break;
default:
break;
}
return color;
}
private Point2D rotate(final Point2D a, final Point2D center, final double angle) {
final double resultX = center.getX() + (a.getX() - center.getX()) * Math.cos(angle) - (a.getY() - center.getY()) * Math
.sin(angle);
final double resultY = center.getY() + (a.getX() - center.getX()) * Math.sin(angle) + (a.getY() - center.getY()) * Math
.cos(angle);
return new Point2D(resultX, resultY);
}
private Point2D makeControlPoint(final double endX, final double endY, final Circle circle, final int numSegments, int direction) {
final double controlPointDistance = (4.0 / 3.0) * Math.tan(Math.PI / (2 * numSegments)) * circle.getRadius();
final Point2D center = new Point2D(circle.getCenterX(), circle.getCenterY());
final Point2D end = new Point2D(endX, endY);
Point2D perp = rotate(center, end, direction * Math.PI / 2.);
Point2D diff = perp.subtract(end);
diff = diff.normalize();
diff = scale(diff, controlPointDistance);
return end.add(diff);
}
private Point2D scale(final Point2D a, final double scale) {
return new Point2D(a.getX() * scale, a.getY() * scale);
}
final class RecentColorPath extends Path {
PathClickTransition transition;
RecentColorPath(final PathElement... elements) {
super(elements);
this.setStrokeLineCap(StrokeLineCap.ROUND);
this.setStrokeWidth(0);
this.setStrokeType(StrokeType.CENTERED);
this.setCache(true);
JFXDepthManager.setDepth(this, 2);
this.transition = new PathClickTransition(this);
}
void playTransition(final double rate) {
transition.setRate(rate);
transition.play();
}
}
private final class PathClickTransition extends CachedTransition {
PathClickTransition(final Path path) {
super(JFXCustomColorPicker.this, new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(((DropShadow) path.getEffect()).radiusProperty(),
JFXDepthManager.getShadowAt(2).radiusProperty().get(),
EASE_BOTH),
new KeyValue(((DropShadow) path.getEffect()).spreadProperty(),
JFXDepthManager.getShadowAt(2).spreadProperty().get(),
EASE_BOTH),
new KeyValue(((DropShadow) path.getEffect()).offsetXProperty(),
JFXDepthManager.getShadowAt(2).offsetXProperty().get(),
EASE_BOTH),
new KeyValue(((DropShadow) path.getEffect()).offsetYProperty(),
JFXDepthManager.getShadowAt(2).offsetYProperty().get(),
EASE_BOTH),
new KeyValue(path.strokeWidthProperty(), 0, EASE_BOTH)
),
new KeyFrame(Duration.millis(1000),
new KeyValue(((DropShadow) path.getEffect()).radiusProperty(),
JFXDepthManager.getShadowAt(5).radiusProperty().get(),
EASE_BOTH),
new KeyValue(((DropShadow) path.getEffect()).spreadProperty(),
JFXDepthManager.getShadowAt(5).spreadProperty().get(),
EASE_BOTH),
new KeyValue(((DropShadow) path.getEffect()).offsetXProperty(),
JFXDepthManager.getShadowAt(5).offsetXProperty().get(),
EASE_BOTH),
new KeyValue(((DropShadow) path.getEffect()).offsetYProperty(),
JFXDepthManager.getShadowAt(5).offsetYProperty().get(),
EASE_BOTH),
new KeyValue(path.strokeWidthProperty(), 2, EASE_BOTH)
)
)
);
// reduce the number to increase the shifting , increase number to reduce shifting
setCycleDuration(Duration.millis(120));
setDelay(Duration.seconds(0));
setAutoReverse(false);
}
}
}
@@ -0,0 +1,407 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.skins;
import com.jfoenix.controls.*;
import com.jfoenix.svg.SVGGlyph;
import com.jfoenix.transitions.JFXFillTransition;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.control.Tab;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.*;
import javafx.util.Duration;
import org.jackhuang.hmcl.setting.StyleSheets;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author Shadi Shaheen
*/
public class JFXCustomColorPickerDialog extends StackPane {
private final Stage dialog = new Stage();
// used for concurrency control and preventing FX-thread over use
private final AtomicInteger concurrencyController = new AtomicInteger(-1);
private final ObjectProperty<Color> currentColorProperty = new SimpleObjectProperty<>(Color.WHITE);
private final ObjectProperty<Color> customColorProperty = new SimpleObjectProperty<>(Color.TRANSPARENT);
private Runnable onSave;
private final Scene customScene;
private final JFXCustomColorPicker curvedColorPicker;
private ParallelTransition paraTransition;
private final JFXDecorator pickerDecorator;
private boolean systemChange = false;
private boolean userChange = false;
private boolean initOnce = true;
private final Runnable initRun;
public JFXCustomColorPickerDialog(Window owner) {
getStyleClass().add("custom-color-dialog");
if (owner != null) {
dialog.initOwner(owner);
}
dialog.initModality(Modality.APPLICATION_MODAL);
dialog.initStyle(StageStyle.TRANSPARENT);
dialog.setResizable(false);
// create JFX Decorator
pickerDecorator = new JFXDecorator(dialog, this, false, false, false);
pickerDecorator.setOnCloseButtonAction(this::updateColor);
pickerDecorator.setPickOnBounds(false);
customScene = new Scene(pickerDecorator, Color.TRANSPARENT);
StyleSheets.init(customScene);
curvedColorPicker = new JFXCustomColorPicker();
StackPane pane = new StackPane(curvedColorPicker);
pane.setPadding(new Insets(18));
VBox container = new VBox();
container.getChildren().add(pane);
JFXTabPane tabs = new JFXTabPane();
JFXTextField rgbField = new JFXTextField();
JFXTextField hsbField = new JFXTextField();
JFXTextField hexField = new JFXTextField();
rgbField.getStyleClass().add("custom-color-field");
rgbField.setPromptText("RGB Color");
rgbField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
hsbField.getStyleClass().add("custom-color-field");
hsbField.setPromptText("HSB Color");
hsbField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
hexField.getStyleClass().add("custom-color-field");
hexField.setPromptText("#HEX Color");
hexField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
StackPane tabContent = new StackPane();
tabContent.getChildren().add(rgbField);
tabContent.setMinHeight(100);
Tab rgbTab = new Tab("RGB");
rgbTab.setContent(tabContent);
Tab hsbTab = new Tab("HSB");
hsbTab.setContent(hsbField);
Tab hexTab = new Tab("HEX");
hexTab.setContent(hexField);
tabs.getTabs().add(hexTab);
tabs.getTabs().add(rgbTab);
tabs.getTabs().add(hsbTab);
curvedColorPicker.selectedPath.addListener((o, oldVal, newVal) -> {
if (paraTransition != null) {
paraTransition.stop();
}
Region tabsHeader = (Region) tabs.lookup(".tab-header-background");
pane.backgroundProperty().unbind();
tabsHeader.backgroundProperty().unbind();
JFXFillTransition fillTransition = new JFXFillTransition(Duration.millis(240),
pane,
(Color) oldVal.getFill(),
(Color) newVal.getFill());
JFXFillTransition tabsFillTransition = new JFXFillTransition(Duration.millis(240),
tabsHeader,
(Color) oldVal.getFill(),
(Color) newVal.getFill());
paraTransition = new ParallelTransition(fillTransition, tabsFillTransition);
paraTransition.setOnFinished((finish) -> {
tabsHeader.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
return new Background(new BackgroundFill(newVal.getFill(), CornerRadii.EMPTY, Insets.EMPTY));
}, newVal.fillProperty()));
pane.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
return new Background(new BackgroundFill(newVal.getFill(), CornerRadii.EMPTY, Insets.EMPTY));
}, newVal.fillProperty()));
});
paraTransition.play();
});
initRun = () -> {
// change tabs labels font color according to the selected color
pane.backgroundProperty().addListener((o, oldVal, newVal) -> {
if (concurrencyController.getAndSet(1) == -1) {
Color fontColor = ((Color) newVal.getFills().get(0).getFill()).grayscale()
.getRed() > 0.5 ? Color.valueOf(
"rgba(40, 40, 40, 0.87)") : Color.valueOf("rgba(255, 255, 255, 0.87)");
// for (Node tabNode : tabs.lookupAll(".tab")) {
// for (Node node : tabNode.lookupAll(".tab-label")) {
// ((Label) node).setTextFill(fontColor);
// }
// for (Node node : tabNode.lookupAll(".jfx-rippler")) {
// ((JFXRippler) node).setRipplerFill(fontColor);
// }
// }
// ((Pane) tabs.lookup(".tab-selected-line")).setBackground(new Background(new BackgroundFill(fontColor, CornerRadii.EMPTY, Insets.EMPTY)));
pickerDecorator.lookupAll(".jfx-decorator-button").forEach(button -> {
((JFXButton) button).setRipplerFill(fontColor);
((SVGGlyph) ((JFXButton) button).getGraphic()).setFill(fontColor);
});
Color newColor = (Color) newVal.getFills().get(0).getFill();
String hex = String.format("#%02X%02X%02X",
(int) (newColor.getRed() * 255),
(int) (newColor.getGreen() * 255),
(int) (newColor.getBlue() * 255));
String rgb = String.format("rgba(%d, %d, %d, 1)",
(int) (newColor.getRed() * 255),
(int) (newColor.getGreen() * 255),
(int) (newColor.getBlue() * 255));
String hsb = String.format("hsl(%d, %d%%, %d%%)",
(int) (newColor.getHue()),
(int) (newColor.getSaturation() * 100),
(int) (newColor.getBrightness() * 100));
if (!userChange) {
systemChange = true;
rgbField.setText(rgb);
hsbField.setText(hsb);
hexField.setText(hex);
systemChange = false;
}
concurrencyController.getAndSet(-1);
}
});
// initial selected colors
Platform.runLater(() -> {
pane.setBackground(new Background(new BackgroundFill(curvedColorPicker.getColor(curvedColorPicker.getSelectedIndex()),
CornerRadii.EMPTY,
Insets.EMPTY)));
((Region) tabs.lookup(".tab-header-background")).setBackground(new Background(new BackgroundFill(
curvedColorPicker.getColor(curvedColorPicker.getSelectedIndex()),
CornerRadii.EMPTY,
Insets.EMPTY)));
Region tabsHeader = (Region) tabs.lookup(".tab-header-background");
pane.backgroundProperty().unbind();
tabsHeader.backgroundProperty().unbind();
tabsHeader.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
return new Background(new BackgroundFill(curvedColorPicker.selectedPath.get().getFill(),
CornerRadii.EMPTY,
Insets.EMPTY));
}, curvedColorPicker.selectedPath.get().fillProperty()));
pane.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
return new Background(new BackgroundFill(curvedColorPicker.selectedPath.get().getFill(),
CornerRadii.EMPTY,
Insets.EMPTY));
}, curvedColorPicker.selectedPath.get().fillProperty()));
// bind text field line color
rgbField.focusColorProperty().bind(Bindings.createObjectBinding(() -> {
return pane.getBackground().getFills().get(0).getFill();
}, pane.backgroundProperty()));
hsbField.focusColorProperty().bind(Bindings.createObjectBinding(() -> {
return pane.getBackground().getFills().get(0).getFill();
}, pane.backgroundProperty()));
hexField.focusColorProperty().bind(Bindings.createObjectBinding(() -> {
return pane.getBackground().getFills().get(0).getFill();
}, pane.backgroundProperty()));
((Pane) pickerDecorator.lookup(".jfx-decorator-buttons-container")).backgroundProperty()
.bind(Bindings.createObjectBinding(() -> {
return new Background(new BackgroundFill(
pane.getBackground()
.getFills()
.get(0)
.getFill(),
CornerRadii.EMPTY,
Insets.EMPTY));
}, pane.backgroundProperty()));
((Pane) pickerDecorator.lookup(".jfx-decorator-content-container")).borderProperty()
.bind(Bindings.createObjectBinding(() -> {
return new Border(new BorderStroke(
pane.getBackground()
.getFills()
.get(0)
.getFill(),
BorderStrokeStyle.SOLID,
CornerRadii.EMPTY,
new BorderWidths(0,
4,
4,
4)));
}, pane.backgroundProperty()));
});
};
container.getChildren().add(tabs);
this.getChildren().add(container);
this.setPadding(new Insets(0));
dialog.setScene(customScene);
final EventHandler<KeyEvent> keyEventListener = key -> {
switch (key.getCode()) {
case ESCAPE:
close();
break;
case ENTER:
updateColor();
break;
default:
break;
}
};
dialog.addEventHandler(KeyEvent.ANY, keyEventListener);
}
private void updateColor() {
close();
this.customColorProperty.set(curvedColorPicker.getColor(curvedColorPicker.getSelectedIndex()));
this.onSave.run();
}
private void updateColorFromUserInput(String colorWebString) {
if (!systemChange) {
userChange = true;
try {
curvedColorPicker.setColor(Color.valueOf(colorWebString));
} catch (IllegalArgumentException ignored) {
// if color is not valid then do nothing
}
userChange = false;
}
}
private void close() {
dialog.setScene(null);
dialog.close();
}
public void setCurrentColor(Color currentColor) {
this.currentColorProperty.set(currentColor);
}
Color getCurrentColor() {
return currentColorProperty.get();
}
ObjectProperty<Color> customColorProperty() {
return customColorProperty;
}
void setCustomColor(Color color) {
customColorProperty.set(color);
}
Color getCustomColor() {
return customColorProperty.get();
}
public Runnable getOnSave() {
return onSave;
}
public void setOnSave(Runnable onSave) {
this.onSave = onSave;
}
public void setOnHidden(EventHandler<WindowEvent> onHidden) {
dialog.setOnHidden(onHidden);
}
public void show() {
dialog.setOpacity(0);
// pickerDecorator.setOpacity(0);
if (dialog.getOwner() != null) {
dialog.widthProperty().addListener(positionAdjuster);
dialog.heightProperty().addListener(positionAdjuster);
positionAdjuster.invalidated(null);
}
if (dialog.getScene() == null) {
dialog.setScene(customScene);
}
curvedColorPicker.preAnimate();
dialog.show();
if (initOnce) {
initRun.run();
initOnce = false;
}
Timeline timeline = new Timeline(new KeyFrame(Duration.millis(120),
new KeyValue(dialog.opacityProperty(),
1,
Interpolator.EASE_BOTH)));
timeline.setOnFinished((finish) -> curvedColorPicker.animate());
timeline.play();
}
// add option to show color picker using JFX Dialog
private InvalidationListener positionAdjuster = new InvalidationListener() {
@Override
public void invalidated(Observable ignored) {
if (Double.isNaN(dialog.getWidth()) || Double.isNaN(dialog.getHeight())) {
return;
}
dialog.widthProperty().removeListener(positionAdjuster);
dialog.heightProperty().removeListener(positionAdjuster);
fixPosition();
}
};
private void fixPosition() {
Window w = dialog.getOwner();
Screen s = com.sun.javafx.util.Utils.getScreen(w);
Rectangle2D sb = s.getBounds();
double xR = w.getX() + w.getWidth();
double xL = w.getX() - dialog.getWidth();
double x;
double y;
if (sb.getMaxX() >= xR + dialog.getWidth()) {
x = xR;
} else if (sb.getMinX() <= xL) {
x = xL;
} else {
x = Math.max(sb.getMinX(), sb.getMaxX() - dialog.getWidth());
}
y = Math.max(sb.getMinY(), Math.min(sb.getMaxY() - dialog.getHeight(), w.getY()));
dialog.setX(x);
dialog.setY(y);
}
@Override
public void layoutChildren() {
super.layoutChildren();
if (dialog.getMinWidth() > 0 && dialog.getMinHeight() > 0) {
return;
}
double minWidth = Math.max(0, computeMinWidth(getHeight()) + (dialog.getWidth() - customScene.getWidth()));
double minHeight = Math.max(0, computeMinHeight(getWidth()) + (dialog.getHeight() - customScene.getHeight()));
dialog.setMinWidth(minWidth);
dialog.setMinHeight(minHeight);
}
}
@@ -0,0 +1,242 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.skins;
import com.jfoenix.adapters.ReflectionHelper;
import com.jfoenix.controls.behavior.JFXGenericPickerBehavior;
import com.sun.javafx.binding.ExpressionHelper;
import com.sun.javafx.event.EventHandlerManager;
import com.sun.javafx.stage.WindowEventDispatcher;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanPropertyBase;
import javafx.beans.value.ChangeListener;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.control.ComboBoxBase;
import javafx.scene.control.PopupControl;
import javafx.scene.control.TextField;
import javafx.scene.control.skin.ComboBoxBaseSkin;
import javafx.scene.control.skin.ComboBoxPopupControl;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.stage.Window;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public abstract class JFXGenericPickerSkin<T> extends ComboBoxPopupControl<T> {
private final EventHandler<MouseEvent> mouseEnteredEventHandler;
private final EventHandler<MouseEvent> mousePressedEventHandler;
private final EventHandler<MouseEvent> mouseReleasedEventHandler;
private final EventHandler<MouseEvent> mouseExitedEventHandler;
protected JFXGenericPickerBehavior<T> behavior;
// reference of the arrow button node in getChildren (not the actual field)
protected Pane arrowButton;
protected PopupControl popup;
public JFXGenericPickerSkin(ComboBoxBase<T> comboBoxBase) {
super(comboBoxBase);
behavior = new JFXGenericPickerBehavior<T>(comboBoxBase);
removeParentFakeFocusListener(comboBoxBase);
this.mouseEnteredEventHandler = event -> behavior.mouseEntered(event);
this.mousePressedEventHandler = event -> {
behavior.mousePressed(event);
event.consume();
};
this.mouseReleasedEventHandler = event -> {
behavior.mouseReleased(event);
event.consume();
};
this.mouseExitedEventHandler = event -> behavior.mouseExited(event);
arrowButton = (Pane) getChildren().get(0);
parentArrowEventHandlerTerminator.accept("mouseEnteredEventHandler", MouseEvent.MOUSE_ENTERED);
parentArrowEventHandlerTerminator.accept("mousePressedEventHandler", MouseEvent.MOUSE_PRESSED);
parentArrowEventHandlerTerminator.accept("mouseReleasedEventHandler", MouseEvent.MOUSE_RELEASED);
parentArrowEventHandlerTerminator.accept("mouseExitedEventHandler", MouseEvent.MOUSE_EXITED);
this.unregisterChangeListeners(comboBoxBase.editableProperty());
updateArrowButtonListeners();
registerChangeListener(comboBoxBase.editableProperty(), obs -> {
updateArrowButtonListeners();
reflectUpdateDisplayArea();
});
removeParentPopupHandlers();
popup = ReflectionHelper.getFieldContent(ComboBoxPopupControl.class, this, "popup");
}
@Override
public void dispose() {
super.dispose();
if (this.behavior != null) {
this.behavior.dispose();
}
}
/***************************************************************************
* *
* Reflections internal API *
* *
**************************************************************************/
private final BiConsumer<String, EventType<?>> parentArrowEventHandlerTerminator = (handlerName, eventType) -> {
try {
EventHandler handler = ReflectionHelper.getFieldContent(ComboBoxBaseSkin.class, this, handlerName);
arrowButton.removeEventHandler(eventType, handler);
} catch (Exception e) {
e.printStackTrace();
}
};
private static final VarHandle READ_ONLY_BOOLEAN_PROPERTY_BASE_HELPER =
findVarHandle(ReadOnlyBooleanPropertyBase.class, "helper", ExpressionHelper.class);
/// @author Glavo
private static VarHandle findVarHandle(Class<?> targetClass, String fieldName, Class<?> type) {
try {
return MethodHandles.privateLookupIn(targetClass, MethodHandles.lookup()).findVarHandle(targetClass, fieldName, type);
} catch (NoSuchFieldException | IllegalAccessException e) {
LOG.warning("Failed to get var handle", e);
return null;
}
}
private void removeParentFakeFocusListener(ComboBoxBase<T> comboBoxBase) {
// handle FakeFocusField cast exception
try {
final ReadOnlyBooleanProperty focusedProperty = comboBoxBase.focusedProperty();
//noinspection unchecked
ExpressionHelper<Boolean> value = (ExpressionHelper<Boolean>) READ_ONLY_BOOLEAN_PROPERTY_BASE_HELPER.get(focusedProperty);
ChangeListener<? super Boolean>[] changeListeners = ReflectionHelper.getFieldContent(value.getClass(), value, "changeListeners");
// remove parent focus listener to prevent editor class cast exception
for (int i = changeListeners.length - 1; i > 0; i--) {
if (changeListeners[i] != null && changeListeners[i].getClass().getName().contains("ComboBoxPopupControl")) {
focusedProperty.removeListener(changeListeners[i]);
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void removeParentPopupHandlers() {
try {
PopupControl popup = ReflectionHelper.invoke(ComboBoxPopupControl.class, this, "getPopup");
popup.setOnAutoHide(event -> behavior.onAutoHide(popup));
WindowEventDispatcher dispatcher = ReflectionHelper.invoke(Window.class, popup, "getInternalEventDispatcher");
Map compositeEventHandlersMap = ReflectionHelper.getFieldContent(EventHandlerManager.class, dispatcher.getEventHandlerManager(), "eventHandlerMap");
compositeEventHandlersMap.remove(MouseEvent.MOUSE_CLICKED);
// CompositeEventHandler compositeEventHandler = (CompositeEventHandler) compositeEventHandlersMap.get(MouseEvent.MOUSE_CLICKED);
// Object obj = fieldConsumer.apply(()->CompositeEventHandler.class.getDeclaredField("firstRecord"),compositeEventHandler);
// EventHandler handler = (EventHandler) fieldConsumer.apply(() -> obj.getClass().getDeclaredField("eventHandler"), obj);
// popup.removeEventHandler(MouseEvent.MOUSE_CLICKED, handler);
popup.addEventHandler(MouseEvent.MOUSE_CLICKED, click -> behavior.onAutoHide(popup));
} catch (Exception e) {
e.printStackTrace();
}
}
private void updateArrowButtonListeners() {
if (getSkinnable().isEditable()) {
arrowButton.addEventHandler(MouseEvent.MOUSE_ENTERED, mouseEnteredEventHandler);
arrowButton.addEventHandler(MouseEvent.MOUSE_PRESSED, mousePressedEventHandler);
arrowButton.addEventHandler(MouseEvent.MOUSE_RELEASED, mouseReleasedEventHandler);
arrowButton.addEventHandler(MouseEvent.MOUSE_EXITED, mouseExitedEventHandler);
} else {
arrowButton.removeEventHandler(MouseEvent.MOUSE_ENTERED, mouseEnteredEventHandler);
arrowButton.removeEventHandler(MouseEvent.MOUSE_PRESSED, mousePressedEventHandler);
arrowButton.removeEventHandler(MouseEvent.MOUSE_RELEASED, mouseReleasedEventHandler);
arrowButton.removeEventHandler(MouseEvent.MOUSE_EXITED, mouseExitedEventHandler);
}
}
/***************************************************************************
* *
* Reflections internal API for ComboBoxPopupControl *
* *
**************************************************************************/
private final HashMap<String, Method> parentCachedMethods = new HashMap<>();
Function<String, Method> methodSupplier = name -> {
if (!parentCachedMethods.containsKey(name)) {
try {
Method method = ComboBoxPopupControl.class.getDeclaredMethod(name);
method.setAccessible(true);
parentCachedMethods.put(name, method);
} catch (Exception e) {
e.printStackTrace();
}
}
return parentCachedMethods.get(name);
};
final Consumer<Method> methodInvoker = method -> {
try {
method.invoke(this);
} catch (Exception e) {
e.printStackTrace();
}
};
final Function<Method, Object> methodReturnInvoker = method -> {
try {
return method.invoke(this);
} catch (Exception e) {
e.printStackTrace();
}
return null;
};
protected void reflectUpdateDisplayArea() {
methodInvoker.accept(methodSupplier.apply("updateDisplayArea"));
}
protected void reflectSetTextFromTextFieldIntoComboBoxValue() {
methodInvoker.accept(methodSupplier.apply("setTextFromTextFieldIntoComboBoxValue"));
}
protected TextField reflectGetEditableInputNode() {
return (TextField) methodReturnInvoker.apply(methodSupplier.apply("getEditableInputNode"));
}
protected void reflectUpdateDisplayNode() {
methodInvoker.accept(methodSupplier.apply("updateDisplayNode"));
}
}
@@ -0,0 +1,96 @@
// Copy from https://github.com/sshahine/JFoenix/blob/d427fd801a338f934307ba41ce604eb5c79f0b20/jfoenix/src/main/java/com/jfoenix/skins/JFXListViewSkin.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.skins;
import com.jfoenix.adapters.skins.ListViewSkin;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.effects.JFXDepthManager;
import javafx.scene.control.ListCell;
import javafx.scene.control.skin.VirtualFlow;
import javafx.scene.layout.Region;
import org.jackhuang.hmcl.ui.FXUtils;
// https://github.com/HMCL-dev/HMCL/issues/4720
public class JFXListViewSkin<T> extends ListViewSkin<T> {
private final VirtualFlow<ListCell<T>> flow;
@SuppressWarnings("unchecked")
public JFXListViewSkin(final JFXListView<T> listView) {
super(listView);
flow = (VirtualFlow<ListCell<T>>) getChildren().get(0);
JFXDepthManager.setDepth(flow, listView.depthProperty().get());
listView.depthProperty().addListener((o, oldVal, newVal) -> JFXDepthManager.setDepth(flow, newVal));
if (!Boolean.TRUE.equals(listView.getProperties().get("no-smooth-scrolling"))) {
FXUtils.smoothScrolling(flow);
}
}
@Override
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return 200;
}
@Override
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
final int itemsCount = getSkinnable().getItems().size();
if (getSkinnable().maxHeightProperty().isBound() || itemsCount <= 0) {
return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
}
final double fixedCellSize = getSkinnable().getFixedCellSize();
double computedHeight = fixedCellSize != Region.USE_COMPUTED_SIZE ?
fixedCellSize * itemsCount + snapVerticalInsets() : estimateHeight();
double height = super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
if (height > computedHeight) {
height = computedHeight;
}
if (getSkinnable().getMaxHeight() > 0 && computedHeight > getSkinnable().getMaxHeight()) {
return getSkinnable().getMaxHeight();
}
return height;
}
private double estimateHeight() {
// compute the border/padding for the list
double borderWidth = snapVerticalInsets();
// compute the gap between list cells
JFXListView<T> listview = (JFXListView<T>) getSkinnable();
double gap = listview.isExpanded() ? ((JFXListView<T>) getSkinnable()).getVerticalGap() * (getSkinnable().getItems()
.size()) : 0;
// compute the height of each list cell
double cellsHeight = 0;
for (int i = 0; i < flow.getCellCount(); i++) {
ListCell<T> cell = flow.getCell(i);
cellsHeight += cell.getHeight();
}
return cellsHeight + gap + borderWidth;
}
private double snapVerticalInsets() {
return getSkinnable().snappedBottomInset() + getSkinnable().snappedTopInset();
}
}
@@ -0,0 +1,173 @@
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.jfoenix.skins;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.controls.JFXRippler;
import com.jfoenix.controls.JFXRippler.RipplerMask;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.VPos;
import javafx.scene.control.RadioButton;
import javafx.scene.control.skin.RadioButtonSkin;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.text.Text;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
public class JFXRadioButtonSkin extends RadioButtonSkin {
private static final double PADDING = 15.0;
private boolean invalid = true;
private final JFXRippler rippler;
private final Circle radio;
private final Circle dot;
private Timeline timeline;
private final AnchorPane container = new AnchorPane();
private final double labelOffset = -10.0;
public JFXRadioButtonSkin(JFXRadioButton control) {
super(control);
double radioRadius = 7.0;
this.radio = new Circle(radioRadius);
this.radio.getStyleClass().setAll("radio");
this.radio.setStrokeWidth(2.0);
this.radio.setFill(Color.TRANSPARENT);
this.dot = new Circle(4);
this.dot.getStyleClass().setAll("dot");
this.dot.fillProperty().bind(control.selectedColorProperty());
this.dot.setScaleX(0.0);
this.dot.setScaleY(0.0);
StackPane boxContainer = new StackPane();
boxContainer.getChildren().addAll(this.radio, this.dot);
boxContainer.setPadding(new Insets(PADDING));
this.rippler = new JFXRippler(boxContainer, RipplerMask.CIRCLE);
this.container.getChildren().add(this.rippler);
AnchorPane.setRightAnchor(this.rippler, this.labelOffset);
this.updateChildren();
control.focusedProperty().addListener((o, oldVal, newVal) -> {
if (newVal) {
if (!this.getSkinnable().isPressed()) {
this.rippler.showOverlay();
}
} else {
this.rippler.hideOverlay();
}
});
control.pressedProperty().addListener((o, oldVal, newVal) -> this.rippler.hideOverlay());
this.registerChangeListener(control.selectedColorProperty(), ignored -> updateColors());
this.registerChangeListener(control.unSelectedColorProperty(), ignored -> updateColors());
this.registerChangeListener(control.selectedProperty(), ignored -> {
updateColors();
this.playAnimation();
});
}
protected void updateChildren() {
super.updateChildren();
if (this.radio != null) {
this.removeRadio();
this.getChildren().add(this.container);
}
}
protected void layoutChildren(double x, double y, double w, double h) {
RadioButton radioButton = this.getSkinnable();
double contWidth = this.snapSizeX(this.container.prefWidth(-1.0)) + (double) (this.invalid ? 2 : 0);
double contHeight = this.snapSizeY(this.container.prefHeight(-1.0)) + (double) (this.invalid ? 2 : 0);
double computeWidth = Math.min(radioButton.prefWidth(-1.0), radioButton.minWidth(-1.0)) + this.labelOffset + 2.0 * this.PADDING;
double labelWidth = Math.min(computeWidth - contWidth, w - this.snapSizeX(contWidth)) + this.labelOffset + 2.0 * PADDING;
double labelHeight = Math.min(radioButton.prefHeight(labelWidth), h);
double maxHeight = Math.max(contHeight, labelHeight);
double xOffset = computeXOffset(w, labelWidth + contWidth, radioButton.getAlignment().getHpos()) + x;
double yOffset = computeYOffset(h, maxHeight, radioButton.getAlignment().getVpos()) + x;
if (this.invalid) {
this.initializeComponents();
this.invalid = false;
}
this.layoutLabelInArea(xOffset + contWidth, yOffset, labelWidth, maxHeight, radioButton.getAlignment());
((Text) this.getChildren().get(this.getChildren().get(0) instanceof Text ? 0 : 1)).textProperty().set(this.getSkinnable().textProperty().get());
this.container.resize(this.snapSizeX(contWidth), this.snapSizeY(contHeight));
this.positionInArea(this.container, xOffset, yOffset, contWidth, maxHeight, 0.0, radioButton.getAlignment().getHpos(), radioButton.getAlignment().getVpos());
}
private void initializeComponents() {
this.updateColors();
this.playAnimation();
}
private void playAnimation() {
if (AnimationUtils.isAnimationEnabled()) {
if (this.timeline == null) {
this.timeline = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(this.dot.scaleXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(this.dot.scaleYProperty(), 0, Interpolator.EASE_BOTH)),
new KeyFrame(Duration.millis(200.0),
new KeyValue(this.dot.scaleXProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(this.dot.scaleYProperty(), 1, Interpolator.EASE_BOTH))
);
} else {
this.timeline.stop();
}
this.timeline.setRate(this.getSkinnable().isSelected() ? 1.0 : -1.0);
this.timeline.play();
} else {
double endScale = this.getSkinnable().isSelected() ? 1.0 : 0.0;
this.dot.setScaleX(endScale);
this.dot.setScaleY(endScale);
}
}
private void removeRadio() {
this.getChildren().removeIf(node -> "radio".equals(node.getStyleClass().get(0)));
}
private void updateColors() {
var control = (JFXRadioButton) getSkinnable();
boolean isSelected = control.isSelected();
Color unSelectedColor = control.getUnSelectedColor();
Color selectedColor = control.getSelectedColor();
rippler.setRipplerFill(isSelected ? selectedColor : unSelectedColor);
radio.setStroke(isSelected ? selectedColor : unSelectedColor);
}
protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset) + this.snapSizeX(this.radio.minWidth(-1.0)) + this.labelOffset + 2.0 * PADDING;
}
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset) + this.snapSizeX(this.radio.prefWidth(-1.0)) + this.labelOffset + 2.0 * PADDING;
}
static double computeXOffset(double width, double contentWidth, HPos hpos) {
return switch (hpos) {
case LEFT -> 0.0;
case CENTER -> (width - contentWidth) / 2.0;
case RIGHT -> width - contentWidth;
};
}
static double computeYOffset(double height, double contentHeight, VPos vpos) {
return switch (vpos) {
case TOP, BASELINE -> 0.0;
case CENTER -> (height - contentHeight) / 2.0;
case BOTTOM -> height - contentHeight;
};
}
}
@@ -0,0 +1,186 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.skins;
import com.jfoenix.controls.JFXRippler;
import com.jfoenix.controls.JFXRippler.RipplerMask;
import com.jfoenix.controls.JFXRippler.RipplerPos;
import com.jfoenix.controls.JFXToggleButton;
import com.jfoenix.effects.JFXDepthManager;
import com.jfoenix.transitions.JFXAnimationTimer;
import com.jfoenix.transitions.JFXKeyFrame;
import com.jfoenix.transitions.JFXKeyValue;
import javafx.animation.Interpolator;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.control.skin.ToggleButtonSkin;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.shape.StrokeLineCap;
import javafx.util.Duration;
/**
* <h1>Material Design ToggleButton Skin</h1>
*
* @author Shadi Shaheen
* @version 1.0
* @since 2016-03-09
*/
public class JFXToggleButtonSkin extends ToggleButtonSkin {
private Runnable releaseManualRippler = null;
private JFXAnimationTimer timer;
private final Circle circle;
private final Line line;
public JFXToggleButtonSkin(JFXToggleButton toggleButton) {
super(toggleButton);
double circleRadius = toggleButton.getSize();
line = new Line();
line.setStroke(getSkinnable().isSelected() ? toggleButton.getToggleLineColor() : toggleButton.getUnToggleLineColor());
line.setStartX(0);
line.setStartY(0);
line.setEndX(circleRadius * 2 + 2);
line.setEndY(0);
line.setStrokeWidth(circleRadius * 1.5);
line.setStrokeLineCap(StrokeLineCap.ROUND);
line.setSmooth(true);
circle = new Circle();
circle.setFill(getSkinnable().isSelected() ? toggleButton.getToggleColor() : toggleButton.getUnToggleColor());
circle.setCenterX(-circleRadius);
circle.setCenterY(0);
circle.setRadius(circleRadius);
circle.setSmooth(true);
JFXDepthManager.setDepth(circle, 1);
StackPane circlePane = new StackPane();
circlePane.getChildren().add(circle);
circlePane.setPadding(new Insets(circleRadius * 1.5));
JFXRippler rippler = new JFXRippler(circlePane, RipplerMask.CIRCLE, RipplerPos.BACK);
rippler.setRipplerFill(getSkinnable().isSelected() ? toggleButton.getToggleLineColor() : toggleButton.getUnToggleLineColor());
rippler.setTranslateX(computeTranslation(circleRadius, line));
final StackPane main = new StackPane();
main.getChildren().setAll(line, rippler);
main.setCursor(Cursor.HAND);
// show focus traversal effect
getSkinnable().armedProperty().addListener((o, oldVal, newVal) -> {
if (newVal) {
releaseManualRippler = rippler.createManualRipple();
} else if (releaseManualRippler != null) {
releaseManualRippler.run();
}
});
toggleButton.focusedProperty().addListener((o, oldVal, newVal) -> {
if (!toggleButton.isDisableVisualFocus()) {
if (newVal) {
if (!getSkinnable().isPressed()) {
rippler.setOverlayVisible(true);
}
} else {
rippler.setOverlayVisible(false);
}
}
});
toggleButton.pressedProperty().addListener(observable -> rippler.setOverlayVisible(false));
// add change listener to selected property
getSkinnable().selectedProperty().addListener(observable -> {
rippler.setRipplerFill(toggleButton.isSelected() ? toggleButton.getToggleLineColor() : toggleButton.getUnToggleLineColor());
if (!toggleButton.isDisableAnimation()) {
timer.reverseAndContinue();
} else {
rippler.setTranslateX(computeTranslation(circleRadius, line));
}
});
getSkinnable().setGraphic(main);
timer = new JFXAnimationTimer(
new JFXKeyFrame(Duration.millis(100),
JFXKeyValue.builder()
.setTarget(rippler.translateXProperty())
.setEndValueSupplier(() -> computeTranslation(circleRadius, line))
.setInterpolator(Interpolator.EASE_BOTH)
.setAnimateCondition(() -> !((JFXToggleButton) getSkinnable()).isDisableAnimation())
.build(),
JFXKeyValue.builder()
.setTarget(line.strokeProperty())
.setEndValueSupplier(() -> getSkinnable().isSelected() ?
((JFXToggleButton) getSkinnable()).getToggleLineColor()
: ((JFXToggleButton) getSkinnable()).getUnToggleLineColor())
.setInterpolator(Interpolator.EASE_BOTH)
.setAnimateCondition(() -> !((JFXToggleButton) getSkinnable()).isDisableAnimation())
.build(),
JFXKeyValue.builder()
.setTarget(circle.fillProperty())
.setEndValueSupplier(() -> getSkinnable().isSelected() ?
((JFXToggleButton) getSkinnable()).getToggleColor()
: ((JFXToggleButton) getSkinnable()).getUnToggleColor())
.setInterpolator(Interpolator.EASE_BOTH)
.setAnimateCondition(() -> !((JFXToggleButton) getSkinnable()).isDisableAnimation())
.build()
)
);
timer.setCacheNodes(circle, line);
registerChangeListener(toggleButton.toggleColorProperty(), observableValue -> {
if (getSkinnable().isSelected()) {
circle.setFill(((JFXToggleButton) getSkinnable()).getToggleColor());
}
});
registerChangeListener(toggleButton.unToggleColorProperty(), observableValue -> {
if (!getSkinnable().isSelected()) {
circle.setFill(((JFXToggleButton) getSkinnable()).getUnToggleColor());
}
});
registerChangeListener(toggleButton.toggleLineColorProperty(), observableValue -> {
if (getSkinnable().isSelected()) {
line.setStroke(((JFXToggleButton) getSkinnable()).getToggleLineColor());
}
});
registerChangeListener(toggleButton.unToggleColorProperty(), observableValue -> {
if (!getSkinnable().isSelected()) {
line.setStroke(((JFXToggleButton) getSkinnable()).getUnToggleLineColor());
}
});
}
private double computeTranslation(double circleRadius, Line line) {
return (getSkinnable().isSelected() ? 1 : -1) * ((line.getLayoutBounds().getWidth() / 2) - circleRadius + 2);
}
@Override
public void dispose() {
super.dispose();
timer.dispose();
timer = null;
}
}
@@ -0,0 +1,68 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.transitions;
import javafx.scene.CacheHint;
import javafx.scene.Node;
import javafx.scene.layout.Region;
import java.util.concurrent.atomic.AtomicBoolean;
public final class CacheMemento {
private boolean cache;
private boolean cacheShape;
private boolean snapToPixel;
private CacheHint cacheHint = CacheHint.DEFAULT;
private final Node node;
private final AtomicBoolean isCached = new AtomicBoolean(false);
public CacheMemento(Node node) {
this.node = node;
}
/**
* this method will cache the node only if it wasn't cached before
*/
public void cache() {
if (!isCached.getAndSet(true)) {
this.cache = node.isCache();
this.cacheHint = node.getCacheHint();
node.setCache(true);
node.setCacheHint(CacheHint.SPEED);
if (node instanceof Region region) {
this.cacheShape = region.isCacheShape();
this.snapToPixel = region.isSnapToPixel();
region.setCacheShape(true);
region.setSnapToPixel(true);
}
}
}
public void restore() {
if (isCached.getAndSet(false)) {
node.setCache(cache);
node.setCacheHint(cacheHint);
if (node instanceof Region region) {
region.setCacheShape(cacheShape);
region.setSnapToPixel(snapToPixel);
}
}
}
}
@@ -0,0 +1,286 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.transitions;
import javafx.animation.AnimationTimer;
import javafx.beans.value.WritableValue;
import javafx.scene.Node;
import javafx.util.Duration;
import java.util.*;
import java.util.function.Supplier;
/**
* Custom AnimationTimer that can be created the same way as a timeline,
* however it doesn't behave the same yet. it only animates in one direction,
* it doesn't support animation 0 -> 1 -> 0.5
*
* @author Shadi Shaheen
* @version 1.0
* @since 2017-09-21
*/
public class JFXAnimationTimer extends AnimationTimer {
private Set<AnimationHandler> animationHandlers = new HashSet<>();
private long startTime = -1;
private boolean running = false;
private List<CacheMemento> caches = new ArrayList<>();
private double totalElapsedMilliseconds;
public JFXAnimationTimer(JFXKeyFrame... keyFrames) {
for (JFXKeyFrame keyFrame : keyFrames) {
Duration duration = keyFrame.getDuration();
final Set<JFXKeyValue<?>> keyValuesSet = keyFrame.getValues();
if (!keyValuesSet.isEmpty()) {
animationHandlers.add(new AnimationHandler(duration, keyFrame.getAnimateCondition(), keyFrame.getValues()));
}
}
}
private final HashMap<JFXKeyFrame, AnimationHandler> mutableFrames = new HashMap<>();
public void addKeyFrame(JFXKeyFrame keyFrame) throws Exception {
if (isRunning()) {
throw new Exception("Can't update animation timer while running");
}
Duration duration = keyFrame.getDuration();
final Set<JFXKeyValue<?>> keyValuesSet = keyFrame.getValues();
if (!keyValuesSet.isEmpty()) {
final AnimationHandler handler = new AnimationHandler(duration, keyFrame.getAnimateCondition(), keyFrame.getValues());
animationHandlers.add(handler);
mutableFrames.put(keyFrame, handler);
}
}
public void removeKeyFrame(JFXKeyFrame keyFrame) throws Exception {
if (isRunning()) {
throw new Exception("Can't update animation timer while running");
}
AnimationHandler handler = mutableFrames.get(keyFrame);
animationHandlers.remove(handler);
}
@Override
public void start() {
super.start();
running = true;
startTime = -1;
for (AnimationHandler animationHandler : animationHandlers) {
animationHandler.init();
}
for (CacheMemento cache : caches) {
cache.cache();
}
}
@Override
public void handle(long now) {
startTime = startTime == -1 ? now : startTime;
totalElapsedMilliseconds = (now - startTime) / 1000000.0;
boolean stop = true;
for (AnimationHandler handler : animationHandlers) {
handler.animate(totalElapsedMilliseconds);
if (!handler.finished) {
stop = false;
}
}
if (stop) {
this.stop();
}
}
/**
* this method will pause the timer and reverse the animation if the timer already
* started otherwise it will start the animation.
*/
public void reverseAndContinue() {
if (isRunning()) {
super.stop();
for (AnimationHandler handler : animationHandlers) {
handler.reverse(totalElapsedMilliseconds);
}
startTime = -1;
super.start();
} else {
start();
}
}
@Override
public void stop() {
super.stop();
running = false;
for (AnimationHandler handler : animationHandlers) {
handler.clear();
}
for (CacheMemento cache : caches) {
cache.restore();
}
if (onFinished != null) {
onFinished.run();
}
}
public void applyEndValues() {
if (isRunning()) {
super.stop();
}
for (AnimationHandler handler : animationHandlers) {
handler.applyEndValues();
}
startTime = -1;
}
public boolean isRunning() {
return running;
}
private Runnable onFinished = null;
public void setOnFinished(Runnable onFinished) {
this.onFinished = onFinished;
}
public void setCacheNodes(Node... nodesToCache) {
caches.clear();
if (nodesToCache != null) {
for (Node node : nodesToCache) {
caches.add(new CacheMemento(node));
}
}
}
public void dispose() {
caches.clear();
for (AnimationHandler handler : animationHandlers) {
handler.dispose();
}
animationHandlers.clear();
}
static class AnimationHandler {
private final double duration;
private double currentDuration;
private final Set<JFXKeyValue<?>> keyValues;
private Supplier<Boolean> animationCondition = null;
private boolean finished = false;
private final HashMap<WritableValue<?>, Object> initialValuesMap = new HashMap<>();
private final HashMap<WritableValue<?>, Object> endValuesMap = new HashMap<>();
AnimationHandler(Duration duration, Supplier<Boolean> animationCondition, Set<JFXKeyValue<?>> keyValues) {
this.duration = duration.toMillis();
currentDuration = this.duration;
this.keyValues = keyValues;
this.animationCondition = animationCondition;
}
public void init() {
finished = animationCondition != null && !animationCondition.get();
for (JFXKeyValue<?> keyValue : keyValues) {
if (keyValue.getTarget() != null) {
// replaced putIfAbsent for mobile compatibility
if (!initialValuesMap.containsKey(keyValue.getTarget())) {
initialValuesMap.put(keyValue.getTarget(), keyValue.getTarget().getValue());
}
if (!endValuesMap.containsKey(keyValue.getTarget())) {
endValuesMap.put(keyValue.getTarget(), keyValue.getEndValue());
}
}
}
}
void reverse(double now) {
finished = animationCondition != null && !animationCondition.get();
currentDuration = duration - (currentDuration - now);
// update initial values
for (JFXKeyValue<?> keyValue : keyValues) {
final WritableValue<?> target = keyValue.getTarget();
if (target != null) {
initialValuesMap.put(target, target.getValue());
endValuesMap.put(target, keyValue.getEndValue());
}
}
}
// now in milliseconds
@SuppressWarnings({"unchecked"})
public void animate(double now) {
// if animate condition for the key frame is not met then do nothing
if (finished) {
return;
}
if (now <= currentDuration) {
for (JFXKeyValue<?> keyValue : keyValues) {
if (keyValue.isValid()) {
@SuppressWarnings("rawtypes") final WritableValue target = keyValue.getTarget();
final Object endValue = endValuesMap.get(target);
if (endValue != null && target != null && !target.getValue().equals(endValue)) {
target.setValue(keyValue.getInterpolator().interpolate(initialValuesMap.get(target), endValue, now / currentDuration));
}
}
}
} else {
if (!finished) {
finished = true;
for (JFXKeyValue<?> keyValue : keyValues) {
if (keyValue.isValid()) {
@SuppressWarnings("rawtypes") final WritableValue target = keyValue.getTarget();
if (target != null) {
// set updated end value instead of cached
final Object endValue = keyValue.getEndValue();
if (endValue != null) {
target.setValue(endValue);
}
}
}
}
currentDuration = duration;
}
}
}
@SuppressWarnings("unchecked")
public void applyEndValues() {
for (JFXKeyValue<?> keyValue : keyValues) {
if (keyValue.isValid()) {
@SuppressWarnings("rawtypes") final WritableValue target = keyValue.getTarget();
if (target != null) {
final Object endValue = keyValue.getEndValue();
if (endValue != null && !target.getValue().equals(endValue)) {
target.setValue(endValue);
}
}
}
}
}
public void clear() {
initialValuesMap.clear();
endValuesMap.clear();
}
void dispose() {
clear();
keyValues.clear();
}
}
}
@@ -0,0 +1,104 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.transitions;
import javafx.util.Duration;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Supplier;
/**
* @author Shadi Shaheen
* @version 1.0
* @since 2017-09-21
*/
public class JFXKeyFrame {
private Duration duration;
private Set<JFXKeyValue<?>> keyValues = new CopyOnWriteArraySet<>();
private Supplier<Boolean> animateCondition = null;
public JFXKeyFrame(Duration duration, JFXKeyValue<?>... keyValues) {
this.duration = duration;
for (final JFXKeyValue<?> keyValue : keyValues) {
if (keyValue != null) {
this.keyValues.add(keyValue);
}
}
}
private JFXKeyFrame() {
}
public final Duration getDuration() {
return duration;
}
public final Set<JFXKeyValue<?>> getValues() {
return keyValues;
}
public Supplier<Boolean> getAnimateCondition() {
return animateCondition;
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private Duration duration;
private final Set<JFXKeyValue<?>> keyValues = new CopyOnWriteArraySet<>();
private Supplier<Boolean> animateCondition = null;
private Builder() {
}
public Builder setDuration(Duration duration) {
this.duration = duration;
return this;
}
public Builder setKeyValues(JFXKeyValue<?>... keyValues) {
for (final JFXKeyValue<?> keyValue : keyValues) {
if (keyValue != null) {
this.keyValues.add(keyValue);
}
}
return this;
}
public Builder setAnimateCondition(Supplier<Boolean> animateCondition) {
this.animateCondition = animateCondition;
return this;
}
public JFXKeyFrame build() {
JFXKeyFrame jFXKeyFrame = new JFXKeyFrame();
jFXKeyFrame.duration = this.duration;
jFXKeyFrame.keyValues = this.keyValues;
jFXKeyFrame.animateCondition = this.animateCondition;
return jFXKeyFrame;
}
}
}
@@ -0,0 +1,157 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.transitions;
import javafx.animation.Interpolator;
import javafx.beans.value.WritableValue;
import java.util.function.Supplier;
/**
* @author Shadi Shaheen
* @version 1.0
* @since 2017-09-21
*/
public final class JFXKeyValue<T> {
private WritableValue<T> target;
private Supplier<WritableValue<T>> targetSupplier;
private Supplier<T> endValueSupplier;
private T endValue;
private Supplier<Boolean> animateCondition = () -> true;
private Interpolator interpolator;
private JFXKeyValue() {
}
// this builder is created to ensure type inference from method arguments
public static Builder builder() {
return new Builder();
}
public T getEndValue() {
return endValue == null ? endValueSupplier.get() : endValue;
}
public WritableValue<T> getTarget() {
return target == null ? targetSupplier.get() : target;
}
public Interpolator getInterpolator() {
return interpolator;
}
public boolean isValid() {
return animateCondition == null || animateCondition.get();
}
public static final class Builder {
public <T> JFXKeyValueBuilder<T> setTarget(WritableValue<T> target) {
JFXKeyValueBuilder<T> builder = new JFXKeyValueBuilder<>();
builder.setTarget(target);
return builder;
}
public <T> JFXKeyValueBuilder<T> setTargetSupplier(Supplier<WritableValue<T>> targetSupplier) {
JFXKeyValueBuilder<T> builder = new JFXKeyValueBuilder<>();
builder.setTargetSupplier(targetSupplier);
return builder;
}
public <T> JFXKeyValueBuilder<T> setEndValueSupplier(Supplier<T> endValueSupplier) {
JFXKeyValueBuilder<T> builder = new JFXKeyValueBuilder<>();
builder.setEndValueSupplier(endValueSupplier);
return builder;
}
public <T> JFXKeyValueBuilder<T> setEndValue(T endValue) {
JFXKeyValueBuilder<T> builder = new JFXKeyValueBuilder<>();
builder.setEndValue(endValue);
return builder;
}
public <T> JFXKeyValueBuilder<T> setAnimateCondition(Supplier<Boolean> animateCondition) {
JFXKeyValueBuilder<T> builder = new JFXKeyValueBuilder<>();
builder.setAnimateCondition(animateCondition);
return builder;
}
public <T> JFXKeyValueBuilder<T> setInterpolator(Interpolator interpolator) {
JFXKeyValueBuilder<T> builder = new JFXKeyValueBuilder<>();
builder.setInterpolator(interpolator);
return builder;
}
}
public static final class JFXKeyValueBuilder<T> {
private WritableValue<T> target;
private Supplier<WritableValue<T>> targetSupplier;
private Supplier<T> endValueSupplier;
private T endValue;
private Supplier<Boolean> animateCondition = () -> true;
private Interpolator interpolator = Interpolator.EASE_BOTH;
private JFXKeyValueBuilder() {
}
public JFXKeyValueBuilder<T> setTarget(WritableValue<T> target) {
this.target = target;
return this;
}
public JFXKeyValueBuilder<T> setTargetSupplier(Supplier<WritableValue<T>> targetSupplier) {
this.targetSupplier = targetSupplier;
return this;
}
public JFXKeyValueBuilder<T> setEndValueSupplier(Supplier<T> endValueSupplier) {
this.endValueSupplier = endValueSupplier;
return this;
}
public JFXKeyValueBuilder<T> setEndValue(T endValue) {
this.endValue = endValue;
return this;
}
public JFXKeyValueBuilder<T> setAnimateCondition(Supplier<Boolean> animateCondition) {
this.animateCondition = animateCondition;
return this;
}
public JFXKeyValueBuilder<T> setInterpolator(Interpolator interpolator) {
this.interpolator = interpolator;
return this;
}
public JFXKeyValue<T> build() {
JFXKeyValue<T> jFXKeyValue = new JFXKeyValue<>();
jFXKeyValue.target = this.target;
jFXKeyValue.interpolator = this.interpolator;
jFXKeyValue.targetSupplier = this.targetSupplier;
jFXKeyValue.endValue = this.endValue;
jFXKeyValue.endValueSupplier = this.endValueSupplier;
jFXKeyValue.animateCondition = this.animateCondition;
return jFXKeyValue;
}
}
}
@@ -0,0 +1,64 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.utils;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import java.util.Locale;
/// @author Shadi Shaheen
/// @version 1.0
/// @since 2017-02-11
public final class JFXNodeUtils {
public static void updateBackground(Background newBackground, Region nodeToUpdate) {
updateBackground(newBackground, nodeToUpdate, Color.BLACK);
}
public static void updateBackground(Background newBackground, Region nodeToUpdate, Paint fill) {
if (newBackground != null && !newBackground.getFills().isEmpty()) {
final BackgroundFill[] fills = new BackgroundFill[newBackground.getFills().size()];
for (int i = 0; i < newBackground.getFills().size(); i++) {
BackgroundFill bf = newBackground.getFills().get(i);
fills[i] = new BackgroundFill(fill, bf.getRadii(), bf.getInsets());
}
nodeToUpdate.setBackground(new Background(fills));
}
}
public static String colorToHex(Color c) {
if (c != null) {
return String.format((Locale) null, "#%02X%02X%02X",
Math.round(c.getRed() * 255),
Math.round(c.getGreen() * 255),
Math.round(c.getBlue() * 255));
} else {
return null;
}
}
private JFXNodeUtils() {
}
}
@@ -0,0 +1,277 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
import org.jackhuang.hmcl.util.SwingUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CancellationException;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class EntryPoint {
private EntryPoint() {
}
public static void main(String[] args) {
System.getProperties().putIfAbsent("java.net.useSystemProxies", "true");
System.getProperties().putIfAbsent("javafx.autoproxy.disable", "true");
System.getProperties().putIfAbsent("http.agent", "HMCL/" + Metadata.VERSION);
createHMCLDirectories();
LOG.start(Metadata.HMCL_CURRENT_DIRECTORY.resolve("logs"));
setupJavaFXVMOptions();
checkDirectoryPath();
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && !isInsideMacAppBundle())
initIcon();
checkJavaFX();
verifyJavaFX();
addEnableNativeAccess();
enableUnsafeMemoryAccess();
Launcher.main(args);
}
public static void exit(int exitCode) {
FileSaver.shutdown();
LOG.shutdown();
System.exit(exitCode);
}
private static void setupJavaFXVMOptions() {
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU"))) {
LOG.info("HMCL_FORCE_GPU: true");
System.getProperties().putIfAbsent("prism.forceGPU", "true");
}
String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE");
if (animationFrameRate != null) {
LOG.info("HMCL_ANIMATION_FRAME_RATE: " + animationFrameRate);
try {
if (Integer.parseInt(animationFrameRate) <= 0)
throw new NumberFormatException(animationFrameRate);
System.getProperties().putIfAbsent("javafx.animation.pulse", animationFrameRate);
} catch (NumberFormatException e) {
LOG.warning("Invalid animation frame rate: " + animationFrameRate);
}
}
String uiScale = System.getProperty("hmcl.uiScale", System.getenv("HMCL_UI_SCALE"));
if (uiScale != null) {
uiScale = uiScale.trim();
LOG.info("HMCL_UI_SCALE: " + uiScale);
try {
float scaleValue;
if (uiScale.endsWith("%")) {
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 1)) / 100.0f;
} else if (uiScale.endsWith("dpi") || uiScale.endsWith("DPI")) {
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 3)) / 96.0f;
} else {
scaleValue = Float.parseFloat(uiScale);
}
float lowerBound;
float upperBound;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
// JavaFX behavior may be abnormal when the DPI scaling factor is too high
lowerBound = 0.25f;
upperBound = 4f;
} else {
lowerBound = 0.01f;
upperBound = 10f;
}
if (scaleValue >= lowerBound && scaleValue <= upperBound) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
System.getProperties().putIfAbsent("glass.win.uiScale", uiScale);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
LOG.warning("macOS does not support setting UI scale, so it will be ignored");
} else {
System.getProperties().putIfAbsent("glass.gtk.uiScale", uiScale);
}
} else {
LOG.warning("UI scale out of range: " + uiScale);
}
} catch (Throwable e) {
LOG.warning("Invalid UI scale: " + uiScale);
}
}
}
private static void createHMCLDirectories() {
if (!Files.isDirectory(Metadata.HMCL_CURRENT_DIRECTORY)) {
try {
Files.createDirectories(Metadata.HMCL_CURRENT_DIRECTORY);
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
try {
Files.setAttribute(Metadata.HMCL_CURRENT_DIRECTORY, "dos:hidden", true);
} catch (IOException e) {
LOG.warning("Failed to set hidden attribute of " + Metadata.HMCL_CURRENT_DIRECTORY, e);
}
}
} catch (IOException e) {
// Logger has not been started yet, so print directly to System.err
System.err.println("Failed to create HMCL directory: " + Metadata.HMCL_CURRENT_DIRECTORY);
e.printStackTrace(System.err);
showErrorAndExit(i18n("fatal.create_hmcl_current_directory_failure", Metadata.HMCL_CURRENT_DIRECTORY));
}
}
if (!Files.isDirectory(Metadata.HMCL_GLOBAL_DIRECTORY)) {
try {
Files.createDirectories(Metadata.HMCL_GLOBAL_DIRECTORY);
} catch (IOException e) {
LOG.warning("Failed to create HMCL global directory " + Metadata.HMCL_GLOBAL_DIRECTORY, e);
}
}
}
private static boolean isInsideMacAppBundle() {
Path thisJar = JarUtils.thisJarPath();
if (thisJar == null)
return false;
for (Path current = thisJar.getParent();
current != null && current.getParent() != null;
current = current.getParent()
) {
if ("Contents".equals(FileUtils.getName(current))
&& FileUtils.getName(current.getParent()).endsWith(".app")
&& Files.exists(current.resolve("Info.plist"))
) {
return true;
}
}
return false;
}
private static void initIcon() {
try {
if (java.awt.Taskbar.isTaskbarSupported()) {
var image = java.awt.Toolkit.getDefaultToolkit().getImage(EntryPoint.class.getResource("/assets/img/icon-mac.png"));
java.awt.Taskbar.getTaskbar().setIconImage(image);
}
} catch (Throwable e) {
LOG.warning("Failed to set application icon", e);
}
}
private static void checkDirectoryPath() {
String currentDir = System.getProperty("user.dir", "");
if (currentDir.contains("!")) {
LOG.error("The current working path contains an exclamation mark: " + currentDir);
// No Chinese translation because both Swing and JavaFX cannot render Chinese character properly when exclamation mark exists in the path.
showErrorAndExit("Exclamation mark(!) is not allowed in the path where HMCL is in.\n"
+ "The path is " + currentDir);
}
}
private static void checkJavaFX() {
try {
SelfDependencyPatcher.patch();
} catch (SelfDependencyPatcher.PatchException e) {
LOG.error("Unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.missing"));
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
LOG.error("Unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.incompatible"));
} catch (CancellationException e) {
LOG.error("User cancels downloading JavaFX", e);
exit(0);
}
}
/**
* Check if JavaFX exists but is incomplete
*/
private static void verifyJavaFX() {
try {
Class.forName("javafx.beans.binding.Binding"); // javafx.base
Class.forName("javafx.stage.Stage"); // javafx.graphics
Class.forName("javafx.scene.control.Skin"); // javafx.controls
} catch (Exception e) {
LOG.warning("JavaFX is incomplete or not found", e);
showErrorAndExit(i18n("fatal.javafx.incomplete"));
}
}
private static void addEnableNativeAccess() {
if (JavaRuntime.CURRENT_VERSION > 21) {
try {
// javafx.graphics
Module module = Class.forName("javafx.stage.Stage").getModule();
if (module.isNamed()) {
try {
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Module.class, MethodHandles.lookup());
MethodHandle implAddEnableNativeAccess = lookup.findVirtual(Module.class,
"implAddEnableNativeAccess", MethodType.methodType(Module.class));
Module ignored = (Module) implAddEnableNativeAccess.invokeExact(module);
} catch (Throwable e) {
e.printStackTrace(System.err);
}
}
} catch (ClassNotFoundException e) {
LOG.error("Failed to add enable native access for JavaFX", e);
showErrorAndExit(i18n("fatal.javafx.incomplete"));
}
}
}
private static void enableUnsafeMemoryAccess() {
// https://openjdk.org/jeps/498
if (JavaRuntime.CURRENT_VERSION == 24 || JavaRuntime.CURRENT_VERSION == 25) {
try {
Class<?> clazz = Class.forName("sun.misc.Unsafe");
boolean ignored = (boolean) MethodHandles.privateLookupIn(clazz, MethodHandles.lookup())
.findStatic(clazz, "trySetMemoryAccessWarned", MethodType.methodType(boolean.class))
.invokeExact();
} catch (Throwable e) {
LOG.warning("Failed to enable unsafe memory access", e);
}
}
}
/**
* Indicates that a fatal error has occurred, and that the application cannot start.
*/
private static void showErrorAndExit(String message) {
SwingUtils.showErrorDialog(message);
exit(1);
}
}
@@ -19,14 +19,19 @@ package org.jackhuang.hmcl;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ObservableBooleanValue;
import javafx.geometry.Rectangle2D;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
import javafx.scene.input.Clipboard;
import javafx.scene.input.DataFormat;
import javafx.stage.Screen;
import javafx.stage.Stage;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.SambaException;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
@@ -38,21 +43,28 @@ import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.NativeUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemInfo;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryPoolMXBean;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -66,27 +78,37 @@ public final class Launcher extends Application {
CookieHandler.setDefault(COOKIE_MANAGER);
LOG.info("JavaFX Version: " + System.getProperty("javafx.runtime.version"));
try {
Object pipeline = Class.forName("com.sun.prism.GraphicsPipeline").getMethod("getPipeline").invoke(null);
LOG.info("Prism pipeline: " + (pipeline == null ? "null" : pipeline.getClass().getName()));
} catch (Throwable e) {
LOG.warning("Failed to get prism pipeline", e);
LOG.info("Prism Pipeline: " + FXUtils.GRAPHICS_PIPELINE);
LOG.info("Dark Mode: " + Optional.ofNullable(FXUtils.DARK_MODE).map(ObservableBooleanValue::get).orElse(false));
LOG.info("Reduced Motion: " + Objects.requireNonNullElse(FXUtils.REDUCED_MOTION, false));
if (Screen.getScreens().isEmpty()) {
LOG.info("No screen");
} else {
StringBuilder builder = new StringBuilder("Screens:");
int count = 0;
for (Screen screen : Screen.getScreens()) {
builder.append("\n - Screen ").append(++count).append(": ");
appendScreen(builder, screen);
}
LOG.info(builder.toString());
}
try {
try {
ConfigHolder.init();
} catch (SambaException ignored) {
Main.showWarningAndContinue(i18n("fatal.samba"));
} catch (SambaException e) {
showAlert(AlertType.WARNING, i18n("fatal.samba"));
} catch (IOException e) {
LOG.error("Failed to load config", e);
checkConfigInTempDir();
checkConfigOwner();
Main.showErrorAndExit(i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
showAlert(AlertType.ERROR, i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
EntryPoint.exit(1);
}
// https://lapcatsoftware.com/articles/app-translocation.html
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
&& ConfigHolder.isNewlyCreated()
&& System.getProperty("user.dir").startsWith("/private/var/folders/")) {
if (showAlert(AlertType.WARNING, i18n("fatal.mac_app_translocation"), ButtonType.YES, ButtonType.NO) == ButtonType.NO)
@@ -100,8 +122,12 @@ public final class Launcher extends Application {
return;
}
if (Metadata.HMCL_DIRECTORY.toString().indexOf('=') >= 0) {
Main.showWarningAndContinue(i18n("fatal.illegal_char"));
if (ConfigHolder.isUnsupportedVersion()) {
showAlert(AlertType.WARNING, i18n("fatal.config_unsupported_version"));
}
if (Metadata.HMCL_CURRENT_DIRECTORY.toString().indexOf('=') >= 0) {
showAlert(AlertType.WARNING, i18n("fatal.illegal_char"));
}
// runLater to ensure ConfigHolder.init() finished initialization
@@ -120,6 +146,38 @@ public final class Launcher extends Application {
}
}
private static void appendScreen(StringBuilder builder, Screen screen) {
Rectangle2D bounds = screen.getBounds();
double scale = screen.getOutputScaleX();
builder.append(Math.round(bounds.getWidth() * scale));
builder.append('x');
builder.append(Math.round(bounds.getHeight() * scale));
DecimalFormat decimalFormat = new DecimalFormat("#.##");
if (scale != 1.0) {
builder.append(" @ ");
builder.append(decimalFormat.format(scale));
builder.append('x');
}
double dpi = screen.getDpi();
builder.append(' ');
builder.append(decimalFormat.format(dpi));
builder.append("dpi");
builder.append(" in ")
.append(Math.round(Math.sqrt(bounds.getWidth() * bounds.getWidth() + bounds.getHeight() * bounds.getHeight()) / dpi))
.append('"');
builder.append(" (").append(decimalFormat.format(bounds.getMinX()))
.append(", ").append(decimalFormat.format(bounds.getMinY()))
.append(", ").append(decimalFormat.format(bounds.getMaxX()))
.append(", ").append(decimalFormat.format(bounds.getMaxY()))
.append(")");
}
private static ButtonType showAlert(AlertType alertType, String contentText, ButtonType... buttons) {
return new Alert(alertType, contentText, buttons).showAndWait().orElse(null);
}
@@ -148,7 +206,7 @@ public final class Launcher extends Application {
|| configPath.startsWith("/var/cache/")
|| configPath.startsWith("/dev/shm/")
|| configPath.contains("/Trash/");
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
return configPath.startsWith("/var/folders/")
|| configPath.startsWith("/private/var/folders/")
|| configPath.startsWith("/tmp/")
@@ -164,7 +222,7 @@ public final class Launcher extends Application {
private static void checkConfigInTempDir() {
if (ConfigHolder.isNewlyCreated() && isConfigInTempDir()
&& showAlert(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), ButtonType.YES, ButtonType.NO) == ButtonType.NO) {
Main.exit(0);
EntryPoint.exit(0);
}
}
@@ -186,8 +244,10 @@ public final class Launcher extends Application {
ArrayList<String> files = new ArrayList<>();
files.add(ConfigHolder.configLocation().toString());
if (Files.exists(Metadata.HMCL_DIRECTORY))
files.add(Metadata.HMCL_DIRECTORY.toString());
if (Files.exists(Metadata.HMCL_GLOBAL_DIRECTORY))
files.add(Metadata.HMCL_GLOBAL_DIRECTORY.toString());
if (Files.exists(Metadata.HMCL_CURRENT_DIRECTORY))
files.add(Metadata.HMCL_CURRENT_DIRECTORY.toString());
Path mcDir = Paths.get(".minecraft").toAbsolutePath().normalize();
if (Files.exists(mcDir))
@@ -202,12 +262,13 @@ public final class Launcher extends Application {
Clipboard.getSystemClipboard()
.setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, command));
}
Main.exit(1);
EntryPoint.exit(1);
}
@Override
public void stop() throws Exception {
Controllers.onApplicationStop();
FileSaver.shutdown();
LOG.shutdown();
}
@@ -223,29 +284,45 @@ public final class Launcher extends Application {
try {
LOG.info("*** " + Metadata.TITLE + " ***");
LOG.info("Operating System: " + (OperatingSystem.OS_RELEASE_PRETTY_NAME == null
? OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION
: OperatingSystem.OS_RELEASE_PRETTY_NAME + " (" + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + ')'));
LOG.info("System Architecture: " + Architecture.SYSTEM_ARCH_NAME);
LOG.info("Java Architecture: " + Architecture.CURRENT_ARCH_NAME);
? OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION.getVersion()
: OperatingSystem.OS_RELEASE_PRETTY_NAME + " (" + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION.getVersion() + ')'));
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
LOG.info("Processor Identifier: " + System.getenv("PROCESSOR_IDENTIFIER"));
}
LOG.info("System Architecture: " + Architecture.SYSTEM_ARCH.getDisplayName());
LOG.info("Native Encoding: " + OperatingSystem.NATIVE_CHARSET);
LOG.info("JNU Encoding: " + System.getProperty("sun.jnu.encoding"));
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
LOG.info("Code Page: " + OperatingSystem.CODE_PAGE);
}
LOG.info("Java Architecture: " + Architecture.CURRENT_ARCH.getDisplayName());
LOG.info("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor"));
LOG.info("Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor"));
LOG.info("Java Home: " + System.getProperty("java.home"));
LOG.info("Current Directory: " + System.getProperty("user.dir"));
LOG.info("HMCL Directory: " + Metadata.HMCL_DIRECTORY);
LOG.info("Current Directory: " + Metadata.CURRENT_DIRECTORY);
LOG.info("HMCL Global Directory: " + Metadata.HMCL_GLOBAL_DIRECTORY);
LOG.info("HMCL Current Directory: " + Metadata.HMCL_CURRENT_DIRECTORY);
LOG.info("HMCL Jar Path: " + Lang.requireNonNullElse(JarUtils.thisJarPath(), "Not Found"));
LOG.info("HMCL Log File: " + Lang.requireNonNullElse(LOG.getLogFile(), "In Memory"));
LOG.info("Memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB");
LOG.info("Physical memory: " + OperatingSystem.TOTAL_MEMORY + " MB");
LOG.info("Metaspace: " + ManagementFactory.getMemoryPoolMXBeans().stream()
.filter(bean -> bean.getName().equals("Metaspace"))
.findAny()
.map(bean -> bean.getUsage().getUsed() / 1024 / 1024 + "MB")
.orElse("Unknown"));
LOG.info("JVM Max Memory: " + MEGABYTES.formatBytes(Runtime.getRuntime().maxMemory()));
try {
for (MemoryPoolMXBean bean : ManagementFactory.getMemoryPoolMXBeans()) {
if ("Metaspace".equals(bean.getName())) {
long bytes = bean.getUsage().getUsed();
LOG.info("Metaspace: " + MEGABYTES.formatBytes(bytes));
break;
}
}
} catch (NoClassDefFoundError ignored) {
}
LOG.info("Native Backend: " + (NativeUtils.USE_JNA ? "JNA" : "None"));
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
LOG.info("XDG Session Type: " + System.getenv("XDG_SESSION_TYPE"));
LOG.info("XDG Current Desktop: " + System.getenv("XDG_CURRENT_DESKTOP"));
}
Lang.thread(SystemInfo::initialize, "Detection System Information", true);
launch(Launcher.class, args);
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
@@ -274,7 +351,7 @@ public final class Launcher extends Application {
Controllers.getStage().close();
Schedulers.shutdown();
Controllers.shutdown();
Lang.executeDelayed(OperatingSystem::forceGC, TimeUnit.SECONDS, 5, true);
Lang.executeDelayed(System::gc, TimeUnit.SECONDS, 5, true);
});
}
@@ -1,192 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import org.jackhuang.hmcl.ui.AwtUtils;
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
import org.jackhuang.hmcl.ui.SwingUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.concurrent.CancellationException;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Main {
private Main() {
}
public static void main(String[] args) {
System.getProperties().putIfAbsent("java.net.useSystemProxies", "true");
System.getProperties().putIfAbsent("javafx.autoproxy.disable", "true");
System.getProperties().putIfAbsent("http.agent", "HMCL/" + Metadata.VERSION);
LOG.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
checkDirectoryPath();
if (JavaRuntime.CURRENT_VERSION < 9)
// This environment check will take ~300ms
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
initIcon();
checkJavaFX();
verifyJavaFX();
Launcher.main(args);
}
public static void exit(int exitCode) {
LOG.shutdown();
System.exit(exitCode);
}
private static void initIcon() {
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(Main.class.getResource("/assets/img/icon@8x.png"));
AwtUtils.setAppleIcon(image);
}
private static void checkDirectoryPath() {
String currentDirectory = new File("").getAbsolutePath();
if (currentDirectory.contains("!")) {
// No Chinese translation because both Swing and JavaFX cannot render Chinese character properly when exclamation mark exists in the path.
showErrorAndExit("Exclamation mark(!) is not allowed in the path where HMCL is in.\n"
+ "The path is " + currentDirectory);
}
}
private static void checkJavaFX() {
try {
SelfDependencyPatcher.patch();
} catch (SelfDependencyPatcher.PatchException e) {
LOG.error("unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.missing"));
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
LOG.error("unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.incompatible"));
} catch (CancellationException e) {
LOG.error("User cancels downloading JavaFX", e);
exit(0);
}
}
/**
* Check if JavaFX exists but is incomplete
*/
private static void verifyJavaFX() {
try {
Class.forName("javafx.beans.binding.Binding"); // javafx.base
Class.forName("javafx.stage.Stage"); // javafx.graphics
Class.forName("javafx.scene.control.Skin"); // javafx.controls
} catch (Exception e) {
showErrorAndExit(i18n("fatal.javafx.incomplete"));
}
}
/**
* Indicates that a fatal error has occurred, and that the application cannot start.
*/
static void showErrorAndExit(String message) {
System.err.println(message);
System.err.println("A fatal error has occurred, forcibly exiting.");
try {
if (Platform.isFxApplicationThread()) {
new Alert(Alert.AlertType.ERROR, message).showAndWait();
exit(1);
}
} catch (Throwable ignored) {
}
SwingUtils.showErrorDialog(message);
exit(1);
}
/**
* Indicates that potential issues have been detected, and that the application may not function properly (but it can still run).
*/
static void showWarningAndContinue(String message) {
System.err.println(message);
System.err.println("Potential issues have been detected.");
try {
if (Platform.isFxApplicationThread()) {
new Alert(Alert.AlertType.WARNING, message).showAndWait();
return;
}
} catch (Throwable ignored) {
}
SwingUtils.showWarningDialog(message);
}
private static void fixLetsEncrypt() {
try {
KeyStore defaultKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
Path ksPath = Paths.get(System.getProperty("java.home"), "lib", "security", "cacerts");
try (InputStream ksStream = Files.newInputStream(ksPath)) {
defaultKeyStore.load(ksStream, "changeit".toCharArray());
}
KeyStore letsEncryptKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
try (InputStream letsEncryptFile = Main.class.getResourceAsStream("/assets/lekeystore.jks")) {
letsEncryptKeyStore.load(letsEncryptFile, "supersecretpassword".toCharArray());
}
KeyStore merged = KeyStore.getInstance(KeyStore.getDefaultType());
merged.load(null, new char[0]);
for (String alias : Collections.list(letsEncryptKeyStore.aliases()))
merged.setCertificateEntry(alias, letsEncryptKeyStore.getCertificate(alias));
for (String alias : Collections.list(defaultKeyStore.aliases()))
merged.setCertificateEntry(alias, defaultKeyStore.getCertificate(alias));
TrustManagerFactory instance = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
instance.init(merged);
SSLContext tls = SSLContext.getInstance("TLS");
tls.init(null, instance.getTrustManagers(), null);
HttpsURLConnection.setDefaultSSLSocketFactory(tls.getSocketFactory());
LOG.info("Added Lets Encrypt root certificates as additional trust");
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException |
KeyManagementException e) {
LOG.error("Failed to load lets encrypt certificate. Expect problems", e);
}
}
}
@@ -19,36 +19,51 @@ package org.jackhuang.hmcl;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.EnumSet;
/**
* Stores metadata about this application.
*/
public final class Metadata {
private Metadata() {}
private Metadata() {
}
public static final String NAME = "HMCL";
public static final String FULL_NAME = "Hello Minecraft! Launcher";
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getManifestAttribute("Implementation-Version", "@develop@"));
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getAttribute("hmcl.version", "@develop@"));
public static final String TITLE = NAME + " " + VERSION;
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
public static final String HMCL_UPDATE_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link");
public static final String CONTACT_URL = "https://docs.hmcl.net/help.html";
public static final String HELP_URL = "https://docs.hmcl.net";
public static final String CHANGELOG_URL = "https://docs.hmcl.net/changelog/";
public static final int MINIMUM_REQUIRED_JAVA_VERSION = 17;
public static final int MINIMUM_SUPPORTED_JAVA_VERSION = 17;
public static final int RECOMMENDED_JAVA_VERSION = 21;
public static final String PUBLISH_URL = "https://hmcl.huangyuhui.net";
public static final String EULA_URL = "https://docs.hmcl.net/eula/hmcl.html";
public static final String ABOUT_URL = PUBLISH_URL + "/about";
public static final String DOWNLOAD_URL = PUBLISH_URL + "/download";
public static final String HMCL_UPDATE_URL = System.getProperty("hmcl.update_source.override", PUBLISH_URL + "/api/update_link");
public static final String BUILD_CHANNEL = JarUtils.getManifestAttribute("Build-Channel", "nightly");
public static final String GITHUB_SHA = JarUtils.getManifestAttribute("GitHub-SHA", null);
public static final String DOCS_URL = "https://docs.hmcl.net";
public static final String CONTACT_URL = DOCS_URL + "/help.html";
public static final String CHANGELOG_URL = DOCS_URL + "/changelog/";
public static final String EULA_URL = DOCS_URL + "/eula/hmcl.html";
public static final String GROUPS_URL = "https://www.bilibili.com/opus/905435541874409529";
public static final String BUILD_CHANNEL = JarUtils.getAttribute("hmcl.version.type", "nightly");
public static final String GITHUB_SHA = JarUtils.getAttribute("hmcl.version.hash", null);
public static final Path CURRENT_DIRECTORY = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize();
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
public static final Path HMCL_DIRECTORY;
public static final Path HMCL_GLOBAL_DIRECTORY;
public static final Path HMCL_CURRENT_DIRECTORY;
public static final Path DEPENDENCIES_DIRECTORY;
static {
String hmclHome = System.getProperty("hmcl.home");
@@ -56,16 +71,22 @@ public final class Metadata {
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
String xdgData = System.getenv("XDG_DATA_HOME");
if (StringUtils.isNotBlank(xdgData)) {
HMCL_DIRECTORY = Paths.get(xdgData, "hmcl").toAbsolutePath();
HMCL_GLOBAL_DIRECTORY = Paths.get(xdgData, "hmcl").toAbsolutePath().normalize();
} else {
HMCL_DIRECTORY = Paths.get(System.getProperty("user.home", "."), ".local", "share", "hmcl").toAbsolutePath();
HMCL_GLOBAL_DIRECTORY = Paths.get(System.getProperty("user.home"), ".local", "share", "hmcl").toAbsolutePath().normalize();
}
} else {
HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
HMCL_GLOBAL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
}
} else {
HMCL_DIRECTORY = Paths.get(hmclHome).toAbsolutePath().normalize();
HMCL_GLOBAL_DIRECTORY = Paths.get(hmclHome).toAbsolutePath().normalize();
}
String hmclCurrentDir = System.getProperty("hmcl.dir");
HMCL_CURRENT_DIRECTORY = hmclCurrentDir != null
? Paths.get(hmclCurrentDir).toAbsolutePath().normalize()
: CURRENT_DIRECTORY.resolve(".hmcl");
DEPENDENCIES_DIRECTORY = HMCL_CURRENT_DIRECTORY.resolve("dependencies");
}
public static boolean isStable() {
@@ -79,4 +100,31 @@ public final class Metadata {
public static boolean isNightly() {
return !isStable() && !isDev();
}
public static @Nullable String getSuggestedJavaDownloadLink() {
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.LOONGARCH64_OW)
return "https://www.loongnix.cn/zh/api/java/downloads-jdk21/index.html";
else {
EnumSet<Architecture> supportedArchitectures;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
supportedArchitectures = EnumSet.of(Architecture.X86_64, Architecture.X86, Architecture.ARM64);
else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX)
supportedArchitectures = EnumSet.of(
Architecture.X86_64, Architecture.X86,
Architecture.ARM64, Architecture.ARM32,
Architecture.RISCV64, Architecture.LOONGARCH64
);
else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
supportedArchitectures = EnumSet.of(Architecture.X86_64, Architecture.ARM64);
else
supportedArchitectures = EnumSet.noneOf(Architecture.class);
if (supportedArchitectures.contains(Architecture.SYSTEM_ARCH))
return String.format("https://docs.hmcl.net/downloads/%s/%s.html",
OperatingSystem.CURRENT_OS.getCheckedName(),
Architecture.SYSTEM_ARCH.getCheckedName()
);
else
return null;
}
}
}
@@ -42,9 +42,9 @@ public class CrashReport {
"\n Content: \n " +
stackTrace + "\n\n" +
"-- System Details --\n" +
" Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + "\n" +
" System Architecture: " + Architecture.SYSTEM_ARCH_NAME + "\n" +
" Java Architecture: " + Architecture.CURRENT_ARCH_NAME + "\n" +
" Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION.getVersion() + "\n" +
" System Architecture: " + Architecture.SYSTEM_ARCH.getDisplayName() + "\n" +
" Java Architecture: " + Architecture.CURRENT_ARCH.getDisplayName() + "\n" +
" Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor") + "\n" +
" Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor") + "\n" +
" JVM Max Memory: " + Runtime.getRuntime().maxMemory() + "\n" +
@@ -21,15 +21,17 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.launch.DefaultLauncher;
import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.nio.file.*;
import java.util.*;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
@@ -58,61 +60,83 @@ public final class HMCLGameLauncher extends DefaultLauncher {
}
private void generateOptionsTxt() {
File optionsFile = new File(repository.getRunDirectory(version.getId()), "options.txt");
File configFolder = new File(repository.getRunDirectory(version.getId()), "config");
if (optionsFile.exists()) {
if (config().isDisableAutoGameOptions())
return;
}
if (configFolder.isDirectory()) {
if (findFiles(configFolder, "options.txt")) {
return;
Path runDir = repository.getRunDirectory(version.getId());
Path optionsFile = runDir.resolve("options.txt");
Path configFolder = runDir.resolve("config");
if (Files.exists(optionsFile))
return;
if (Files.isDirectory(configFolder)) {
try (Stream<Path> stream = Files.walk(configFolder, 2, FileVisitOption.FOLLOW_LINKS)) {
if (stream.anyMatch(file -> "options.txt".equals(FileUtils.getName(file))))
return;
} catch (IOException e) {
LOG.warning("Failed to visit config folder", e);
}
}
if (!I18n.isUseChinese()) {
return;
}
Locale locale = Locale.getDefault();
String lang;
/*
1.0- :没有语言选项,遇到这些版本时不设置
1.1 ~ 5 :zh_CN 时正常,zh_cn 时崩溃(最后两位字母必须大写,否则将会 NPE 崩溃)
1.6 ~ 10 :zh_CN 时正常,zh_cn 时自动切换为英文
1.11 ~ 12:zh_cn 时正常,zh_CN 时虽然显示了中文但语言设置会错误地显示选择英文
1.13+ :zh_cn 时正常,zh_CN 时自动切换为英文
* 1.0 : No language option, do not set for these versions
* 1.1 ~ 1.5 : zh_CN works fine, zh_cn will crash (the last two letters must be uppercase, otherwise it will cause an NPE crash)
* 1.6 ~ 1.10 : zh_CN works fine, zh_cn will automatically switch to English
* 1.11 ~ 1.12 : zh_cn works fine, zh_CN will display Chinese but the language setting will incorrectly show English as selected
* 1.13+ : zh_cn works fine, zh_CN will automatically switch to English
*/
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(repository.getGameVersion(version));
if (gameVersion.compareTo("1.1") < 0) {
lang = null;
} else if (gameVersion.compareTo("1.11") < 0) {
lang = "zh_CN";
} else {
lang = "zh_cn";
}
if (gameVersion.compareTo("1.1") < 0)
return;
if (lang != null) {
try {
FileUtils.writeText(optionsFile, String.format("lang:%s\n", lang));
} catch (IOException e) {
LOG.warning("Unable to generate options.txt", e);
}
String lang = normalizedLanguageTag(locale, gameVersion);
if (lang.isEmpty())
return;
if (gameVersion.compareTo("1.11") >= 0)
lang = lang.toLowerCase(Locale.ROOT);
try {
Files.createDirectories(optionsFile.getParent());
Files.writeString(optionsFile, String.format("lang:%s\n", lang));
} catch (IOException e) {
LOG.warning("Unable to generate options.txt", e);
}
}
private boolean findFiles(File folder, String fileName) {
File[] fs = folder.listFiles();
if (fs != null) {
for (File f : fs) {
if (f.isDirectory())
if (f.listFiles((dir, name) -> name.equals(fileName)) != null)
return true;
if (f.getName().equals(fileName))
return true;
private static String normalizedLanguageTag(Locale locale, GameVersionNumber gameVersion) {
String region = locale.getCountry();
return switch (LocaleUtils.getRootLanguage(locale)) {
case "ar" -> "ar_SA";
case "es" -> "es_ES";
case "ja" -> "ja_JP";
case "ru" -> "ru_RU";
case "uk" -> "uk_UA";
case "zh" -> {
if ("lzh".equals(locale.getLanguage()) && gameVersion.compareTo("1.16") >= 0)
yield "lzh";
String script = LocaleUtils.getScript(locale);
if ("Hant".equals(script)) {
if ((region.equals("HK") || region.equals("MO") && gameVersion.compareTo("1.16") >= 0))
yield "zh_HK";
yield "zh_TW";
}
yield "zh_CN";
}
}
return false;
case "en" -> {
if ("Qabs".equals(LocaleUtils.getScript(locale)) && gameVersion.compareTo("1.16") >= 0) {
yield "en_UD";
}
yield "";
}
default -> "";
};
}
@Override
@@ -122,7 +146,7 @@ public final class HMCLGameLauncher extends DefaultLauncher {
}
@Override
public void makeLaunchScript(File scriptFile) throws IOException {
public void makeLaunchScript(Path scriptFile) throws IOException {
generateOptionsTxt();
super.makeLaunchScript(scriptFile);
}
@@ -25,27 +25,29 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.mod.ModAdviser;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.ProxyManager;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemInfo;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.time.Instant;
import java.util.*;
@@ -53,10 +55,10 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class HMCLGameRepository extends DefaultGameRepository {
public final class HMCLGameRepository extends DefaultGameRepository {
private final Profile profile;
// local version settings
@@ -65,7 +67,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
public final EventManager<Event> onVersionIconChanged = new EventManager<>();
public HMCLGameRepository(Profile profile, File baseDirectory) {
public HMCLGameRepository(Profile profile, Path baseDirectory) {
super(baseDirectory);
this.profile = profile;
}
@@ -84,18 +86,20 @@ public class HMCLGameRepository extends DefaultGameRepository {
}
@Override
public File getRunDirectory(String id) {
public Path getRunDirectory(String id) {
switch (getGameDirectoryType(id)) {
case VERSION_FOLDER:
return getVersionRoot(id);
case ROOT_FOLDER:
return super.getRunDirectory(id);
case CUSTOM:
File dir = new File(getVersionSetting(id).getGameDir());
if (!FileUtils.isValidPath(dir)) return getVersionRoot(id);
return dir;
try {
return Path.of(getVersionSetting(id).getGameDir());
} catch (InvalidPathException ignored) {
return getVersionRoot(id);
}
default:
throw new Error();
throw new AssertionError("Unreachable");
}
}
@@ -118,25 +122,24 @@ public class HMCLGameRepository extends DefaultGameRepository {
});
try {
File file = new File(getBaseDirectory(), "launcher_profiles.json");
if (!file.exists() && !versions.isEmpty())
FileUtils.writeText(file, PROFILE);
Path file = getBaseDirectory().resolve("launcher_profiles.json");
if (!Files.exists(file) && !versions.isEmpty()) {
Files.createDirectories(file.getParent());
Files.writeString(file, PROFILE);
}
} catch (IOException ex) {
LOG.warning("Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex);
}
// https://github.com/HMCL-dev/HMCL/issues/938
System.gc();
}
public void changeDirectory(File newDirectory) {
public void changeDirectory(Path newDirectory) {
setBaseDirectory(newDirectory);
refreshVersionsAsync().start();
}
private void clean(File directory) throws IOException {
FileUtils.deleteDirectory(new File(directory, "crash-reports"));
FileUtils.deleteDirectory(new File(directory, "logs"));
private void clean(Path directory) throws IOException {
FileUtils.deleteDirectory(directory.resolve("crash-reports"));
FileUtils.deleteDirectory(directory.resolve("logs"));
}
public void clean(String id) throws IOException {
@@ -145,25 +148,33 @@ public class HMCLGameRepository extends DefaultGameRepository {
}
public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException {
Path srcDir = getVersionRoot(srcId).toPath();
Path dstDir = getVersionRoot(dstId).toPath();
Path srcDir = getVersionRoot(srcId);
Path dstDir = getVersionRoot(dstId);
Version fromVersion = getVersion(srcId);
if (Files.exists(dstDir)) throw new IOException("Version exists");
FileUtils.copyDirectory(srcDir, dstDir);
List<String> blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST);
blackList.add(srcId + ".jar");
blackList.add(srcId + ".json");
if (!copySaves)
blackList.add("saves");
Path fromJson = dstDir.resolve(srcId + ".json");
Path fromJar = dstDir.resolve(srcId + ".jar");
if (Files.exists(dstDir)) throw new IOException("Version exists");
Files.createDirectories(dstDir);
FileUtils.copyDirectory(srcDir, dstDir, path -> Modpack.acceptFile(path, blackList, null));
Path fromJson = srcDir.resolve(srcId + ".json");
Path fromJar = srcDir.resolve(srcId + ".jar");
Path toJson = dstDir.resolve(dstId + ".json");
Path toJar = dstDir.resolve(dstId + ".jar");
if (Files.exists(fromJar)) {
Files.move(fromJar, toJar);
Files.copy(fromJar, toJar);
}
Files.move(fromJson, toJson);
Files.copy(fromJson, toJson);
FileUtils.writeText(toJson.toFile(), JsonUtils.GSON.toJson(fromVersion.setId(dstId)));
JsonUtils.writeToJsonFile(toJson, fromVersion.setId(dstId));
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
@@ -172,28 +183,22 @@ public class HMCLGameRepository extends DefaultGameRepository {
VersionSetting newVersionSetting = initLocalVersionSetting(dstId, oldVersionSetting);
saveVersionSetting(dstId);
File srcGameDir = getRunDirectory(srcId);
File dstGameDir = getRunDirectory(dstId);
List<String> blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST);
blackList.add(srcId + ".jar");
blackList.add(srcId + ".json");
if (!copySaves)
blackList.add("saves");
Path srcGameDir = getRunDirectory(srcId);
Path dstGameDir = getRunDirectory(dstId);
if (originalGameDirType != GameDirectoryType.VERSION_FOLDER)
FileUtils.copyDirectory(srcGameDir.toPath(), dstGameDir.toPath(), path -> Modpack.acceptFile(path, blackList, null));
FileUtils.copyDirectory(srcGameDir, dstGameDir, path -> Modpack.acceptFile(path, blackList, null));
}
private File getLocalVersionSettingFile(String id) {
return new File(getVersionRoot(id), "hmclversion.cfg");
private Path getLocalVersionSettingFile(String id) {
return getVersionRoot(id).resolve("hmclversion.cfg");
}
private void loadLocalVersionSetting(String id) {
File file = getLocalVersionSettingFile(id);
if (file.exists())
Path file = getLocalVersionSettingFile(id);
if (Files.exists(file))
try {
VersionSetting versionSetting = GSON.fromJson(FileUtils.readText(file), VersionSetting.class);
VersionSetting versionSetting = JsonUtils.fromJsonFile(file, VersionSetting.class);
initLocalVersionSetting(id, versionSetting);
} catch (Exception ex) {
// If [JsonParseException], [IOException] or [NullPointerException] happens, the json file is malformed and needed to be recreated.
@@ -203,6 +208,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
/**
* Create new version setting if version id has no version setting.
*
* @param id the version id.
* @return new version setting, null if given version does not exist.
*/
@@ -217,7 +223,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
private VersionSetting initLocalVersionSetting(String id, VersionSetting vs) {
localVersionSettings.put(id, vs);
vs.addPropertyChangedListener(a -> saveVersionSetting(id));
vs.addListener(a -> saveVersionSetting(id));
return vs;
}
@@ -225,7 +231,6 @@ public class HMCLGameRepository extends DefaultGameRepository {
* Get the version setting for version id.
*
* @param id version id
*
* @return corresponding version setting, null if the version has no its own version setting.
*/
@Nullable
@@ -250,19 +255,18 @@ public class HMCLGameRepository extends DefaultGameRepository {
public VersionSetting getVersionSetting(String id) {
VersionSetting vs = getLocalVersionSetting(id);
if (vs == null || vs.isUsesGlobal()) {
profile.getGlobal().setGlobal(true); // always keep global.isGlobal = true
profile.getGlobal().setUsesGlobal(true);
return profile.getGlobal();
} else
return vs;
}
public Optional<File> getVersionIconFile(String id) {
File root = getVersionRoot(id);
public Optional<Path> getVersionIconFile(String id) {
Path root = getVersionRoot(id);
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
File file = new File(root, "icon." + extension);
if (file.exists()) {
Path file = root.resolve("icon." + extension);
if (Files.exists(file)) {
return Optional.of(file);
}
}
@@ -270,7 +274,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
return Optional.empty();
}
public void setVersionIconFile(String id, File iconFile) throws IOException {
public void setVersionIconFile(String id, Path iconFile) throws IOException {
String ext = FileUtils.getExtension(iconFile).toLowerCase(Locale.ROOT);
if (!FXUtils.IMAGE_EXTENSIONS.contains(ext)) {
throw new IllegalArgumentException("Unsupported icon file: " + ext);
@@ -278,13 +282,18 @@ public class HMCLGameRepository extends DefaultGameRepository {
deleteIconFile(id);
FileUtils.copyFile(iconFile, new File(getVersionRoot(id), "icon." + ext));
FileUtils.copyFile(iconFile, getVersionRoot(id).resolve("icon." + ext));
}
public void deleteIconFile(String id) {
File root = getVersionRoot(id);
Path root = getVersionRoot(id);
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
new File(root, "icon." + extension).delete();
Path file = root.resolve("icon." + extension);
try {
Files.deleteIfExists(file);
} catch (IOException e) {
LOG.warning("Failed to delete icon file: " + file, e);
}
}
}
@@ -297,10 +306,10 @@ public class HMCLGameRepository extends DefaultGameRepository {
if (iconType == VersionIconType.DEFAULT) {
Version version = getVersion(id).resolve(this);
Optional<File> iconFile = getVersionIconFile(id);
Optional<Path> iconFile = getVersionIconFile(id);
if (iconFile.isPresent()) {
try {
return FXUtils.loadImage(iconFile.get().toPath());
return FXUtils.loadImage(iconFile.get());
} catch (Exception e) {
LOG.warning("Failed to load version icon of " + id, e);
}
@@ -310,44 +319,53 @@ public class HMCLGameRepository extends DefaultGameRepository {
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version, null);
if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FABRIC))
return VersionIconType.FABRIC.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FORGE))
return VersionIconType.FORGE.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE))
return VersionIconType.NEO_FORGE.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.QUILT))
return VersionIconType.QUILT.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.OPTIFINE))
return VersionIconType.OPTIFINE.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE))
return VersionIconType.NEO_FORGE.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FORGE))
return VersionIconType.FORGE.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.CLEANROOM))
return VersionIconType.CLEANROOM.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LITELOADER))
return VersionIconType.CHICKEN.getIcon();
else
return VersionIconType.FURNACE.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.OPTIFINE))
return VersionIconType.OPTIFINE.getIcon();
}
return VersionIconType.DEFAULT.getIcon();
String gameVersion = getGameVersion(version).orElse(null);
if (gameVersion != null) {
GameVersionNumber versionNumber = GameVersionNumber.asGameVersion(gameVersion);
if (versionNumber.isAprilFools()) {
return VersionIconType.APRIL_FOOLS.getIcon();
} else if (versionNumber instanceof GameVersionNumber.LegacySnapshot) {
return VersionIconType.COMMAND.getIcon();
} else if (versionNumber instanceof GameVersionNumber.Old) {
return VersionIconType.CRAFT_TABLE.getIcon();
}
}
return VersionIconType.GRASS.getIcon();
} else {
return iconType.getIcon();
}
}
public boolean saveVersionSetting(String id) {
public void saveVersionSetting(String id) {
if (!localVersionSettings.containsKey(id))
return false;
File file = getLocalVersionSettingFile(id);
if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile()))
return false;
return;
Path file = getLocalVersionSettingFile(id).toAbsolutePath().normalize();
try {
FileUtils.writeText(file, GSON.toJson(localVersionSettings.get(id)));
return true;
Files.createDirectories(file.getParent());
} catch (IOException e) {
LOG.error("Unable to save version setting of " + id, e);
return false;
LOG.warning("Failed to create directory: " + file.getParent(), e);
}
FileSaver.save(file, GSON.toJson(localVersionSettings.get(id)));
}
/**
* Make version use self version settings instead of the global one.
*
* @param id the version id.
* @return specialized version setting, null if given version does not exist.
*/
@@ -357,7 +375,9 @@ public class HMCLGameRepository extends DefaultGameRepository {
vs = createLocalVersionSetting(id);
if (vs == null)
return null;
vs.setUsesGlobal(false);
if (vs.isUsesGlobal()) {
vs.setUsesGlobal(false);
}
return vs;
}
@@ -367,7 +387,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
vs.setUsesGlobal(true);
}
public LaunchOptions getLaunchOptions(String version, JavaRuntime javaVersion, File gameDir, List<String> javaAgents, boolean makeLaunchScript) {
public LaunchOptions.Builder getLaunchOptions(String version, JavaRuntime javaVersion, Path gameDir, List<String> javaAgents, List<String> javaArguments, boolean makeLaunchScript) {
VersionSetting vs = getVersionSetting(version);
LaunchOptions.Builder builder = new LaunchOptions.Builder()
@@ -378,9 +398,9 @@ public class HMCLGameRepository extends DefaultGameRepository {
.setProfileName(Metadata.TITLE)
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
.setOverrideJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
.setMaxMemory(vs.isNoJVMArgs() && vs.isAutoMemory() ? null : (int)(getAllocatedMemory(
.setMaxMemory(vs.isNoJVMArgs() && vs.isAutoMemory() ? null : (int) (getAllocatedMemory(
vs.getMaxMemory() * 1024L * 1024L,
OperatingSystem.getPhysicalMemoryStatus().getAvailable(),
SystemInfo.getPhysicalMemoryStatus().getAvailable(),
vs.isAutoMemory()
) / 1024 / 1024))
.setMinMemory(vs.getMinMemory())
@@ -398,48 +418,58 @@ public class HMCLGameRepository extends DefaultGameRepository {
.setWidth(vs.getWidth())
.setHeight(vs.getHeight())
.setFullscreen(vs.isFullscreen())
.setServerIp(vs.getServerIp())
.setWrapper(vs.getWrapper())
.setPreLaunchCommand(vs.getPreLaunchCommand())
.setPostExitCommand(vs.getPostExitCommand())
.setNoGeneratedJVMArgs(vs.isNoJVMArgs())
.setNoGeneratedOptimizingJVMArgs(vs.isNoOptimizingJVMArgs())
.setNativesDirType(vs.getNativesDirType())
.setNativesDir(vs.getNativesDir())
.setProcessPriority(vs.getProcessPriority())
.setRenderer(vs.getRenderer())
.setEnableDebugLogOutput(vs.isEnableDebugLogOutput())
.setUseNativeGLFW(vs.isUseNativeGLFW())
.setUseNativeOpenAL(vs.isUseNativeOpenAL())
.setDaemon(!makeLaunchScript && vs.getLauncherVisibility().isDaemon())
.setJavaAgents(javaAgents);
.setJavaAgents(javaAgents)
.setJavaArguments(javaArguments);
if (StringUtils.isNotBlank(vs.getServerIp())) {
builder.setQuickPlayOption(new QuickPlayOption.MultiPlayer(vs.getServerIp()));
}
if (config().hasProxy()) {
builder.setProxy(ProxyManager.getProxy());
builder.setProxyType(config().getProxyType());
builder.setProxyHost(config().getProxyHost());
builder.setProxyPort(config().getProxyPort());
if (config().hasProxyAuth()) {
builder.setProxyUser(config().getProxyUser());
builder.setProxyPass(config().getProxyPass());
}
}
File json = getModpackConfiguration(version);
if (json.exists()) {
Path json = getModpackConfiguration(version);
if (Files.exists(json)) {
try {
String jsonText = FileUtils.readText(json);
String jsonText = Files.readString(json);
ModpackConfiguration<?> modpackConfiguration = JsonUtils.GSON.fromJson(jsonText, ModpackConfiguration.class);
ModpackProvider provider = ModpackHelper.getProviderByType(modpackConfiguration.getType());
if (provider != null) provider.injectLaunchOptions(jsonText, builder);
} catch (IOException | JsonParseException e) {
e.printStackTrace();
LOG.warning("Failed to parse modpack configuration file " + json, e);
}
}
if (vs.isAutoMemory() && builder.getJavaArguments().stream().anyMatch(it -> it.startsWith("-Xmx")))
builder.setMaxMemory(null);
return builder.create();
return builder;
}
@Override
public File getModpackConfiguration(String version) {
return new File(getVersionRoot(version), "modpack.cfg");
public Path getModpackConfiguration(String version) {
return getVersionRoot(version).resolve("modpack.cfg");
}
public void markVersionAsModpack(String id) {
@@ -452,16 +482,24 @@ public class HMCLGameRepository extends DefaultGameRepository {
public void markVersionLaunchedAbnormally(String id) {
try {
Files.createFile(getVersionRoot(id).toPath().resolve(".abnormal"));
Files.createFile(getVersionRoot(id).resolve(".abnormal"));
} catch (IOException ignored) {
}
}
public boolean unmarkVersionLaunchedAbnormally(String id) {
File file = new File(getVersionRoot(id), ".abnormal");
boolean result = file.isFile();
file.delete();
return result;
Path file = getVersionRoot(id).resolve(".abnormal");
if (Files.isRegularFile(file)) {
try {
Files.delete(file);
} catch (IOException e) {
LOG.warning("Failed to delete abnormal mark file: " + file, e);
}
return true;
} else {
return false;
}
}
private static final Gson GSON = new GsonBuilder()
@@ -483,7 +521,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
FORBIDDEN_VERSION_IDS.contains(id.toLowerCase(Locale.ROOT)))
return false;
return OperatingSystem.isNameValid(id);
return FileUtils.isNameValid(id);
}
/**
@@ -505,16 +543,16 @@ public class HMCLGameRepository extends DefaultGameRepository {
public static long getAllocatedMemory(long minimum, long available, boolean auto) {
if (auto) {
available -= 384 * 1024 * 1024; // Reserve 384MiB memory for off-heap memory and HMCL itself
available -= 512 * 1024 * 1024; // Reserve 512 MiB memory for off-heap memory and HMCL itself
if (available <= 0) {
return minimum;
}
final long threshold = 8L * 1024 * 1024 * 1024;
final long threshold = 8L * 1024 * 1024 * 1024; // 8 GiB
final long suggested = Math.min(available <= threshold
? (long) (available * 0.8)
: (long) (threshold * 0.8 + (available - threshold) * 0.2),
16384L * 1024 * 1024);
16L * 1024 * 1024 * 1024);
return Math.max(minimum, suggested);
} else {
return minimum;
@@ -28,16 +28,16 @@ import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class HMCLModpackInstallTask extends Task<Void> {
private final File zipFile;
private final Path zipFile;
private final String name;
private final HMCLGameRepository repository;
private final DefaultDependencyManager dependency;
@@ -45,16 +45,16 @@ public final class HMCLModpackInstallTask extends Task<Void> {
private final List<Task<?>> dependencies = new ArrayList<>(1);
private final List<Task<?>> dependents = new ArrayList<>(4);
public HMCLModpackInstallTask(Profile profile, File zipFile, Modpack modpack, String name) {
public HMCLModpackInstallTask(Profile profile, Path zipFile, Modpack modpack, String name) {
dependency = profile.getDependency();
repository = profile.getRepository();
this.zipFile = zipFile;
this.name = name;
this.modpack = modpack;
File run = repository.getRunDirectory(name);
File json = repository.getModpackConfiguration(name);
if (repository.hasVersion(name) && !json.exists())
Path run = repository.getRunDirectory(name);
Path json = repository.getModpackConfiguration(name);
if (repository.hasVersion(name) && Files.notExists(json))
throw new IllegalArgumentException("Version " + name + " already exists");
dependents.add(dependency.gameBuilder().name(name).gameVersion(modpack.getGameVersion()).buildAsync());
@@ -65,8 +65,8 @@ public final class HMCLModpackInstallTask extends Task<Void> {
ModpackConfiguration<Modpack> config = null;
try {
if (json.exists()) {
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), ModpackConfiguration.typeOf(Modpack.class));
if (Files.exists(json)) {
config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(Modpack.class));
if (!HMCLModpackProvider.INSTANCE.getName().equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a HMCL modpack. Cannot update this version.");
@@ -18,7 +18,7 @@
package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import org.apache.commons.compress.archivers.zip.ZipFile;
import kala.compress.archivers.zip.ZipArchiveReader;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.Modpack;
@@ -30,7 +30,6 @@ import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
@@ -49,22 +48,21 @@ public final class HMCLModpackProvider implements ModpackProvider {
}
@Override
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, File zipFile, Modpack modpack) throws MismatchedModpackTypeException {
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof HMCLModpackManifest))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
if (!(dependencyManager.getGameRepository() instanceof HMCLGameRepository)) {
if (!(dependencyManager.getGameRepository() instanceof HMCLGameRepository repository)) {
throw new IllegalArgumentException("HMCLModpackProvider requires HMCLGameRepository");
}
HMCLGameRepository repository = (HMCLGameRepository) dependencyManager.getGameRepository();
Profile profile = repository.getProfile();
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new HMCLModpackInstallTask(profile, zipFile, modpack, name));
}
@Override
public Modpack readManifest(ZipFile file, Path path, Charset encoding) throws IOException, JsonParseException {
public Modpack readManifest(ZipArchiveReader file, Path path, Charset encoding) throws IOException, JsonParseException {
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json");
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class).setEncoding(encoding);
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json");
@@ -79,9 +77,9 @@ public final class HMCLModpackProvider implements ModpackProvider {
return manifest;
}
private static class HMCLModpack extends Modpack {
private final static class HMCLModpack extends Modpack {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name) {
return new HMCLModpackInstallTask(((HMCLGameRepository) dependencyManager.getGameRepository()).getProfile(), zipFile, this, name);
}
}
@@ -22,6 +22,7 @@ import javafx.stage.Stage;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
@@ -49,20 +50,23 @@ import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URI;
import java.nio.file.AccessDeniedException;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import 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.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.Lang.resolveException;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -74,10 +78,12 @@ public final class LauncherHelper {
private final Profile profile;
private Account account;
private final String selectedVersion;
private File scriptFile;
private Path scriptFile;
private final VersionSetting setting;
private LauncherVisibility launcherVisibility;
private boolean showLogs;
private QuickPlayOption quickPlayOption;
private boolean disableOfflineSkin = false;
public LauncherHelper(Profile profile, Account account, String selectedVersion) {
this.profile = Objects.requireNonNull(profile);
@@ -108,6 +114,14 @@ public final class LauncherHelper {
launcherVisibility = LauncherVisibility.KEEP;
}
public void setQuickPlayOption(QuickPlayOption quickPlayOption) {
this.quickPlayOption = quickPlayOption;
}
public void setDisableOfflineSkin() {
disableOfflineSkin = true;
}
public void launch() {
FXUtils.checkFxUserThread();
@@ -117,13 +131,15 @@ public final class LauncherHelper {
launch0();
}
public void makeLaunchScript(File scriptFile) {
public void makeLaunchScript(Path scriptFile) {
this.scriptFile = Objects.requireNonNull(scriptFile);
launch();
}
private void launch0() {
// https://github.com/HMCL-dev/HMCL/pull/4121
PROCESSES.removeIf(it -> it.get() == null);
HMCLGameRepository repository = profile.getRepository();
DefaultDependencyManager dependencyManager = profile.getDependency();
AtomicReference<Version> version = new AtomicReference<>(MaintainTask.maintain(repository, repository.getResolvedVersion(selectedVersion)));
@@ -131,13 +147,14 @@ public final class LauncherHelper {
boolean integrityCheck = repository.unmarkVersionLaunchedAbnormally(selectedVersion);
CountDownLatch launchingLatch = new CountDownLatch(1);
List<String> javaAgents = new ArrayList<>(0);
List<String> javaArguments = new ArrayList<>(0);
AtomicReference<JavaRuntime> javaVersionRef = new AtomicReference<>();
TaskExecutor executor = checkGameState(profile, setting, version.get())
.thenComposeAsync(java -> {
javaVersionRef.set(Objects.requireNonNull(java));
version.set(NativePatcher.patchNative(version.get(), gameVersion.orElse(null), java, setting));
version.set(NativePatcher.patchNative(repository, version.get(), gameVersion.orElse(null), java, setting, javaArguments));
if (setting.isNotCheckGame())
return null;
return Task.allOf(
@@ -155,16 +172,16 @@ public final class LauncherHelper {
Task.composeAsync(() -> {
Renderer renderer = setting.getRenderer();
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
Library lib = NativePatcher.getMesaLoader(java, renderer);
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.SYSTEM_VERSION);
if (lib == null)
return null;
File file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
if (file.getAbsolutePath().indexOf('=') >= 0) {
Path file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
if (file.toAbsolutePath().toString().indexOf('=') >= 0) {
LOG.warning("Invalid character '=' in the libraries directory path, unable to attach software renderer loader");
return null;
}
String agent = file.getAbsolutePath() + "=" + renderer.name().toLowerCase(Locale.ROOT);
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.name().toLowerCase(Locale.ROOT);
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
return new LibraryDownloadTask(dependencyManager, file, lib)
@@ -182,9 +199,17 @@ public final class LauncherHelper {
.thenComposeAsync(() -> gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version.get())).orElse(null))
.thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in"))
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
LaunchOptions launchOptions = repository.getLaunchOptions(selectedVersion, javaVersionRef.get(), profile.getGameDir(), javaAgents, scriptFile != null);
LaunchOptions.Builder launchOptionsBuilder = repository.getLaunchOptions(
selectedVersion, javaVersionRef.get(), profile.getGameDir(), javaAgents, javaArguments, scriptFile != null);
if (disableOfflineSkin) {
launchOptionsBuilder.setDaemon(false);
}
if (quickPlayOption != null) {
launchOptionsBuilder.setQuickPlayOption(quickPlayOption);
}
LaunchOptions launchOptions = launchOptionsBuilder.create();
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModManager(selectedVersion).getModsDirectory(), 10));
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModsDirectory(selectedVersion), 10));
return new HMCLGameLauncher(
repository,
@@ -206,7 +231,7 @@ public final class LauncherHelper {
}
}).thenAcceptAsync(process -> { // process is LaunchTask's result
if (scriptFile == null) {
PROCESSES.add(process);
PROCESSES.add(new WeakReference<>(process));
if (launcherVisibility == LauncherVisibility.CLOSE)
Launcher.stopApplication();
else
@@ -217,7 +242,7 @@ public final class LauncherHelper {
} else {
runLater(() -> {
launchingStepsPane.fireEvent(new DialogCloseEvent());
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
Controllers.dialog(i18n("version.launch_script.success", FileUtils.getAbsolutePath(scriptFile)));
});
}
}).withFakeProgress(
@@ -260,27 +285,27 @@ public final class LauncherHelper {
if (ex.getCause() instanceof ResponseCodeException) {
ResponseCodeException rce = (ResponseCodeException) ex.getCause();
int responseCode = rce.getResponseCode();
URL url = rce.getUrl();
String uri = rce.getUri();
if (responseCode == 404)
message += i18n("download.code.404", url);
message += i18n("download.code.404", uri);
else
message += i18n("download.failed", url, responseCode);
message += i18n("download.failed", uri, responseCode);
} else {
message += StringUtils.getStackTrace(ex.getCause());
}
} else if (ex instanceof DownloadException) {
URL url = ((DownloadException) ex).getUrl();
URI uri = ((DownloadException) ex).getUri();
if (ex.getCause() instanceof SocketTimeoutException) {
message = i18n("install.failed.downloading.timeout", url);
message = i18n("install.failed.downloading.timeout", uri);
} else if (ex.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) ex.getCause();
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
message = i18n("download.code." + responseCodeException.getResponseCode(), url);
message = i18n("download.code." + responseCodeException.getResponseCode(), uri);
} else {
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
}
} else {
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
}
} else if (ex instanceof GameAssetIndexDownloadTask.GameAssetIndexMalformedException) {
message = i18n("assets.index.malformed");
@@ -291,11 +316,11 @@ public final class LauncherHelper {
} else if (ex instanceof ResponseCodeException) {
ResponseCodeException rce = (ResponseCodeException) ex;
int responseCode = rce.getResponseCode();
URL url = rce.getUrl();
String uri = rce.getUri();
if (responseCode == 404)
message = i18n("download.code.404", url);
message = i18n("download.code.404", uri);
else
message = i18n("download.failed", url, responseCode);
message = i18n("download.failed", uri, responseCode);
} else if (ex instanceof CommandTooLongException) {
message = i18n("launch.failed.command_too_long");
} else if (ex instanceof ExecutionPolicyLimitException) {
@@ -443,7 +468,9 @@ public final class LauncherHelper {
return result;
} else {
GameJavaVersion gameJavaVersion;
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.CLEANROOM_JAVA_21))
gameJavaVersion = GameJavaVersion.JAVA_21;
else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
gameJavaVersion = version.getJavaVersion();
else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA))
gameJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
@@ -533,6 +560,9 @@ public final class LauncherHelper {
case MODDED_JAVA_21:
suggestions.add(i18n("launch.advice.modded_java", 21, gameVersion));
break;
case CLEANROOM_JAVA_21:
suggestions.add(i18n("launch.advice.cleanroom"));
break;
case VANILLA_JAVA_8_51:
suggestions.add(i18n("launch.advice.java8_51_1_13"));
break;
@@ -551,8 +581,9 @@ public final class LauncherHelper {
}
// 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));
long totalMemorySizeMB = (long) MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize());
if (totalMemorySizeMB > 0 && totalMemorySizeMB < setting.getMaxMemory()) {
suggestions.add(i18n("launch.advice.not_enough_space", totalMemorySizeMB));
}
VersionNumber forgeVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.FORGE)
@@ -626,10 +657,13 @@ public final class LauncherHelper {
return future;
}
private static Task<AuthInfo> logIn(Account account) {
private Task<AuthInfo> logIn(Account account) {
return Task.composeAsync(() -> {
try {
return Task.completed(account.logIn());
if (disableOfflineSkin && account instanceof OfflineAccount offlineAccount)
return Task.completed(offlineAccount.logInWithoutSkin());
else
return Task.completed(account.logIn());
} catch (CredentialExpiredException e) {
LOG.info("Credential has expired", e);
@@ -694,6 +728,7 @@ public final class LauncherHelper {
*/
private final class HMCLProcessListener implements ProcessListener {
private final ReentrantLock lock = new ReentrantLock();
private final HMCLGameRepository repository;
private final Version version;
private final LaunchOptions launchOptions;
@@ -837,21 +872,27 @@ public final class LauncherHelper {
level = Lang.requireNonNullElse(Log4jLevel.guessLevel(log), Log4jLevel.INFO);
logBuffer.add(new Log(log, level));
} else {
synchronized (this) {
lock.lock();
try {
logs.addLast(new Log(log, level));
if (logs.size() > Log.getLogLines())
logs.removeFirst();
} finally {
lock.unlock();
}
}
if (!lwjgl) {
String lowerCaseLog = log.toLowerCase(Locale.ROOT);
if (!detectWindow || lowerCaseLog.contains("lwjgl version") || lowerCaseLog.contains("lwjgl openal")) {
synchronized (this) {
lock.lock();
try {
if (!lwjgl) {
lwjgl = true;
finishLaunch();
}
} finally {
lock.unlock();
}
}
}
@@ -876,9 +917,12 @@ public final class LauncherHelper {
// Game crashed before opening the game window.
if (!lwjgl) {
synchronized (this) {
lock.lock();
try {
if (!lwjgl)
finishLaunch();
} finally {
lock.unlock();
}
}
@@ -892,13 +936,18 @@ public final class LauncherHelper {
}
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";
private static final Queue<WeakReference<ManagedProcess>> PROCESSES = new ConcurrentLinkedQueue<>();
public static final Queue<ManagedProcess> PROCESSES = new ConcurrentLinkedQueue<>();
public static int countMangedProcesses() {
PROCESSES.removeIf(it -> {
ManagedProcess process = it.get();
return process == null || !process.isRunning();
});
return PROCESSES.size();
}
public static void stopManagedProcesses() {
while (!PROCESSES.isEmpty())
Optional.ofNullable(PROCESSES.poll()).ifPresent(ManagedProcess::stop);
Optional.ofNullable(PROCESSES.poll()).map(WeakReference::get).ifPresent(ManagedProcess::stop);
}
}
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.RemoteModRepository;
@@ -41,9 +42,9 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
protected abstract SortType getBackedRemoteModRepositorySortOrder();
@Override
public SearchResult search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
if (!StringUtils.containsChinese(searchFilter)) {
return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
return getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
}
Set<String> englishSearchFiltersSet = new HashSet<>(INITIAL_CAPACITY);
@@ -65,7 +66,7 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
RemoteMod[] searchResultArray = new RemoteMod[pageSize];
int totalPages, chineseIndex = 0, englishIndex = pageSize - 1;
{
SearchResult searchResult = getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, String.join(" ", englishSearchFiltersSet), getBackedRemoteModRepositorySortOrder(), sortOrder);
SearchResult searchResult = getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, String.join(" ", englishSearchFiltersSet), getBackedRemoteModRepositorySortOrder(), sortOrder);
for (Iterator<RemoteMod> iterator = searchResult.getUnsortedResults().iterator(); iterator.hasNext(); ) {
if (chineseIndex > englishIndex) {
LOG.warning("Too many search results! Are the backed remote mod repository broken? Or are the API broken?");
@@ -17,12 +17,13 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.logging.Logger;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.Zipper;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.management.ManagementFactory;
@@ -43,8 +44,8 @@ public final class LogExporter {
}
public static CompletableFuture<Void> exportLogs(Path zipFile, DefaultGameRepository gameRepository, String versionId, String logs, String launchScript) {
Path runDirectory = gameRepository.getRunDirectory(versionId).toPath();
Path baseDirectory = gameRepository.getBaseDirectory().toPath();
Path runDirectory = gameRepository.getRunDirectory(versionId);
Path baseDirectory = gameRepository.getBaseDirectory();
List<String> versions = new ArrayList<>();
String currentVersionId = versionId;
@@ -93,9 +94,8 @@ public final class LogExporter {
if (Files.isRegularFile(file)) {
FileTime time = Files.readAttributes(file, BasicFileAttributes.class).lastModifiedTime();
if (time.toMillis() >= processStartTime) {
try {
String crashLog = Logger.filterForbiddenToken(FileUtils.readText(file, OperatingSystem.NATIVE_CHARSET));
zipper.putTextFile(crashLog, file.getFileName().toString());
try (BufferedReader reader = IOUtils.newBufferedReaderMaybeNativeEncoding(file)) {
zipper.putLines(reader.lines().map(Logger::filterForbiddenToken), file.getFileName().toString());
} catch (IOException e) {
LOG.warning("Failed to read log file: " + file, e);
}
@@ -18,12 +18,14 @@
package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import org.apache.commons.compress.archivers.zip.ZipFile;
import kala.compress.archivers.zip.ZipArchiveReader;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.mod.curse.CurseModpackProvider;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackProvider;
import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackProvider;
import org.jackhuang.hmcl.mod.multimc.MultiMCComponents;
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackProvider;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
@@ -42,7 +44,6 @@ import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
@@ -50,10 +51,7 @@ import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.*;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.Lang.mapOf;
@@ -61,7 +59,8 @@ import static org.jackhuang.hmcl.util.Lang.toIterable;
import static org.jackhuang.hmcl.util.Pair.pair;
public final class ModpackHelper {
private ModpackHelper() {}
private ModpackHelper() {
}
private static final Map<String, ModpackProvider> providers = mapOf(
pair(CurseModpackProvider.INSTANCE.getName(), CurseModpackProvider.INSTANCE),
@@ -72,18 +71,22 @@ public final class ModpackHelper {
pair(HMCLModpackProvider.INSTANCE.getName(), HMCLModpackProvider.INSTANCE)
);
static {
MultiMCComponents.setImplementation(Metadata.FULL_TITLE);
}
@Nullable
public static ModpackProvider getProviderByType(String type) {
return providers.get(type);
}
public static boolean isFileModpackByExtension(File file) {
public static boolean isFileModpackByExtension(Path file) {
String ext = FileUtils.getExtension(file);
return "zip".equals(ext) || "mrpack".equals(ext);
}
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException, ManuallyCreatedModpackException {
try (ZipFile zipFile = CompressingUtils.openZipFile(file, charset)) {
try (ZipArchiveReader zipFile = CompressingUtils.openZipFile(file, charset)) {
// Order for trying detecting manifest is necessary here.
// Do not change to iterating providers.
for (ModpackProvider provider : new ModpackProvider[]{
@@ -135,15 +138,12 @@ public final class ModpackHelper {
(path.getFileName() == null || ".minecraft".equals(FileUtils.getName(path)));
}
public static ModpackConfiguration<?> readModpackConfiguration(File file) throws IOException {
if (!file.exists())
throw new FileNotFoundException(file.getPath());
else
try {
return JsonUtils.GSON.fromJson(FileUtils.readText(file), ModpackConfiguration.class);
} catch (JsonParseException e) {
throw new IOException("Malformed modpack configuration");
}
public static ModpackConfiguration<?> readModpackConfiguration(Path file) throws IOException {
try {
return JsonUtils.fromJsonFile(file, ModpackConfiguration.class);
} catch (JsonParseException e) {
throw new IOException("Malformed modpack configuration");
}
}
public static Task<?> getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) {
@@ -174,21 +174,21 @@ public final class ModpackHelper {
return Files.exists(Paths.get("externalgames").resolve(name));
}
public static Task<?> getInstallManuallyCreatedModpackTask(Profile profile, File zipFile, String name, Charset charset) {
public static Task<?> getInstallManuallyCreatedModpackTask(Profile profile, Path zipFile, String name, Charset charset) {
if (isExternalGameNameConflicts(name)) {
throw new IllegalArgumentException("name existing");
}
return new ManuallyCreatedModpackInstallTask(profile, zipFile.toPath(), charset, name)
return new ManuallyCreatedModpackInstallTask(profile, zipFile, charset, name)
.thenAcceptAsync(Schedulers.javafx(), location -> {
Profile newProfile = new Profile(name, location.toFile());
Profile newProfile = new Profile(name, location);
newProfile.setUseRelativePath(true);
Profiles.getProfiles().add(newProfile);
Profiles.setSelectedProfile(newProfile);
});
}
public static Task<?> getInstallTask(Profile profile, File zipFile, String name, Modpack modpack) {
public static Task<?> getInstallTask(Profile profile, Path zipFile, String name, Modpack modpack) {
profile.getRepository().markVersionAsModpack(name);
ExceptionalRunnable<?> success = () -> {
@@ -210,14 +210,17 @@ public final class ModpackHelper {
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
else if (modpack.getManifest() instanceof McbbsModpackManifest)
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name));
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name))
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
else
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.javafx(), success, failure);
.whenComplete(Schedulers.javafx(), success, failure)
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
}
public static Task<Void> getUpdateTask(Profile profile, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
@@ -230,13 +233,17 @@ public final class ModpackHelper {
}
}
public static Task<?> getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
Modpack modpack = ModpackHelper.readModpackManifest(zipFile.toPath(), charset);
public static Task<?> getUpdateTask(Profile profile, Path zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
Modpack modpack = ModpackHelper.readModpackManifest(zipFile, charset);
ModpackProvider provider = getProviderByType(configuration.getType());
if (provider == null) {
throw new UnsupportedModpackException();
}
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack);
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack)
.thenComposeAsync(() -> createMultiMCPostUpdateTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
else
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack);
}
public static void toVersionSetting(MultiMCInstanceConfiguration c, VersionSetting vs) {
@@ -276,6 +283,24 @@ public final class ModpackHelper {
}
}
private static void applyCommandAndJvmSettings(MultiMCInstanceConfiguration c, VersionSetting vs) {
if (c.isOverrideCommands()) {
vs.setWrapper(Lang.nonNull(c.getWrapperCommand(), ""));
vs.setPreLaunchCommand(Lang.nonNull(c.getPreLaunchCommand(), ""));
}
if (c.isOverrideJavaArgs()) {
vs.setJavaArgs(Lang.nonNull(c.getJvmArgs(), ""));
}
}
private static Task<Void> createMultiMCPostUpdateTask(Profile profile, MultiMCInstanceConfiguration manifest, String version) {
return Task.runAsync(Schedulers.javafx(), () -> {
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
ModpackHelper.applyCommandAndJvmSettings(manifest, vs);
});
}
private static Task<Void> createMultiMCPostInstallTask(Profile profile, MultiMCInstanceConfiguration manifest, String version) {
return Task.runAsync(Schedulers.javafx(), () -> {
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@@ -103,6 +104,7 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
String html;
try {
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
.replace("%lang%", Locale.getDefault().toLanguageTag())
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
} catch (IOException e) {
LOG.error("Failed to load html", e);
@@ -158,13 +160,13 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
@Override
public String getClientId() {
return System.getProperty("hmcl.microsoft.auth.id",
JarUtils.getManifestAttribute("Microsoft-Auth-Id", ""));
JarUtils.getAttribute("hmcl.microsoft.auth.id", ""));
}
@Override
public String getClientSecret() {
return System.getProperty("hmcl.microsoft.auth.secret",
JarUtils.getManifestAttribute("Microsoft-Auth-Secret", ""));
JarUtils.getAttribute("hmcl.microsoft.auth.secret", ""));
}
@Override
@@ -26,8 +26,6 @@ import javafx.beans.value.ObservableValue;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
@@ -45,7 +43,6 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
@@ -87,7 +84,7 @@ public final class TexturesLoader {
}
private static final ThreadPoolExecutor POOL = threadPool("TexturesDownload", true, 2, 10, TimeUnit.SECONDS);
private static final Path TEXTURES_DIR = Metadata.MINECRAFT_DIRECTORY.resolve("assets").resolve("skins");
private static final Path TEXTURES_DIR = Metadata.HMCL_GLOBAL_DIRECTORY.resolve("skins");
private static Path getTexturePath(Texture texture) {
String url = texture.getUrl();
@@ -110,7 +107,7 @@ public final class TexturesLoader {
if (!Files.isRegularFile(file)) {
// download it
try {
new FileDownloadTask(new URL(texture.getUrl()), file.toFile()).run();
new FileDownloadTask(texture.getUrl(), file).run();
LOG.info("Texture downloaded: " + texture.getUrl());
} catch (Exception e) {
if (Files.isRegularFile(file)) {
@@ -265,15 +262,8 @@ public final class TexturesLoader {
int scale = (int) skin.getWidth() / 64;
int faceOffset = (int) Math.round(size / 18.0);
try {
g.setImageSmoothing(false);
drawAvatar(g, skin, size, scale, faceOffset);
} catch (NoSuchMethodError ignored) {
// Earlier JavaFX did not support GraphicsContext::setImageSmoothing
// In order to prevent the blurring caused by bilinear interpolation,
// we use the self-implemented nearest neighbor interpolation to scale
drawAvatarSlow(g, skin, size, scale, faceOffset);
}
g.setImageSmoothing(false);
drawAvatar(g, skin, size, scale, faceOffset);
}
private static void drawAvatar(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
@@ -285,37 +275,6 @@ public final class TexturesLoader {
0, 0, size, size);
}
private static void drawAvatarSlow(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
PixelReader reader = skin.getPixelReader();
PixelWriter writer = g.getPixelWriter();
drawImage(writer, reader,
8 * scale, 8 * scale, 8 * scale, 8 * scale,
faceOffset, faceOffset, size - 2 * faceOffset, size - 2 * faceOffset);
drawImage(writer, reader,
40 * scale, 8 * scale, 8 * scale, 8 * scale,
0, 0, size, size);
}
/*
* Scale and draw image using nearest-neighbor interpolation
*/
private static void drawImage(PixelWriter writer, PixelReader reader,
int sx, int sy, int sw, int sh,
int dx, int dy, int dw, int dh) {
double xScale = ((double) sw) / dw;
double yScale = ((double) sh) / dh;
for (int xOffset = 0; xOffset < dw; xOffset++) {
for (int yOffset = 0; yOffset < dh; yOffset++) {
int color = reader.getArgb(sx + (int) (xOffset * xScale), sy + (int) (yOffset * yScale));
// Draw only non-transparent pixels
if ((color >>> 24) != 0)
writer.setArgb(dx + xOffset, dy + yOffset, color);
}
}
}
private static final class SkinBindingChangeListener implements ChangeListener<LoadedTexture> {
static final WeakHashMap<Canvas, SkinBindingChangeListener> hole = new WeakHashMap<>();
@@ -30,7 +30,6 @@ import org.jackhuang.hmcl.util.platform.Platform;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.*;
@@ -83,7 +82,7 @@ public final class HMCLJavaRepository implements JavaRepository {
try {
return JavaManager.getExecutable(javaDir).toRealPath();
} catch (IOException ignored) {
if (platform.getOperatingSystem() == OperatingSystem.OSX) {
if (platform.getOperatingSystem() == OperatingSystem.MACOS) {
try {
return JavaManager.getMacExecutable(javaDir).toRealPath();
} catch (IOException ignored1) {
@@ -98,14 +97,8 @@ public final class HMCLJavaRepository implements JavaRepository {
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)) {
private static void getAllJava(List<JavaRuntime> list, Platform platform, Path platformRoot, boolean isManaged) {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(platformRoot)) {
for (Path file : stream) {
try {
String name = file.getFileName().toString();
@@ -115,19 +108,15 @@ public final class HMCLJavaRepository implements JavaRepository {
try {
executable = JavaManager.getExecutable(javaDir).toRealPath();
} catch (IOException e) {
if (platform.getOperatingSystem() == OperatingSystem.OSX)
if (platform.getOperatingSystem() == OperatingSystem.MACOS)
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));
JavaManifest manifest = JsonUtils.fromJsonFile(file, JavaManifest.class);
list.add(JavaRuntime.of(executable, manifest.getInfo(), isManaged));
}
}
} catch (Throwable e) {
@@ -137,6 +126,23 @@ public final class HMCLJavaRepository implements JavaRepository {
} catch (IOException ignored) {
}
}
@Override
public Collection<JavaRuntime> getAllJava(Platform platform) {
Path platformRoot = getPlatformRoot(platform);
if (!Files.isDirectory(platformRoot))
return Collections.emptyList();
ArrayList<JavaRuntime> list = new ArrayList<>();
getAllJava(list, platform, platformRoot, true);
if (platform.getOperatingSystem() == OperatingSystem.MACOS) {
platformRoot = root.resolve(platform.getOperatingSystem().getMojangName() + "-" + platform.getArchitecture().getCheckedName());
if (Files.isDirectory(platformRoot))
getAllJava(list, platform, platformRoot, false);
}
return list;
}
@@ -149,7 +155,7 @@ public final class HMCLJavaRepository implements JavaRepository {
try {
executable = JavaManager.getExecutable(javaDir).toRealPath();
} catch (IOException e) {
if (platform.getOperatingSystem() == OperatingSystem.OSX)
if (platform.getOperatingSystem() == OperatingSystem.MACOS)
executable = JavaManager.getMacExecutable(javaDir).toRealPath();
else
throw e;
@@ -157,7 +163,7 @@ public final class HMCLJavaRepository implements JavaRepository {
JavaInfo info;
if (JavaManager.isCompatible(platform))
info = JavaInfo.fromExecutable(executable, false);
info = JavaInfoUtils.fromExecutable(executable, false);
else
info = new JavaInfo(platform, result.download.getVersion().getName(), null);
@@ -180,7 +186,7 @@ public final class HMCLJavaRepository implements JavaRepository {
});
JavaManifest manifest = new JavaManifest(info, update, files);
FileUtils.writeText(getManifestFile(platform, gameJavaVersion), JsonUtils.GSON.toJson(manifest));
JsonUtils.writeToJsonFile(getManifestFile(platform, gameJavaVersion), manifest);
return JavaRuntime.of(executable, info, true);
});
}
@@ -192,7 +198,7 @@ public final class HMCLJavaRepository implements JavaRepository {
throw new IOException("Platform is mismatch: expected " + platform + " but got " + result.getInfo().getPlatform());
Path executable = javaDir.resolve("bin").resolve(platform.getOperatingSystem().getJavaExecutable()).toRealPath();
FileUtils.writeText(getManifestFile(platform, name), JsonUtils.GSON.toJson(result));
JsonUtils.writeToJsonFile(getManifestFile(platform, name), result);
return JavaRuntime.of(executable, result.getInfo(), true);
});
}
@@ -201,7 +207,7 @@ public final class HMCLJavaRepository implements JavaRepository {
public Task<Void> getUninstallJavaTask(Platform platform, String name) {
return Task.runAsync(() -> {
Files.deleteIfExists(getManifestFile(platform, name));
FileUtils.deleteDirectory(getJavaDir(platform, name).toFile());
FileUtils.deleteDirectory(getJavaDir(platform, name));
});
}
@@ -214,7 +220,7 @@ public final class HMCLJavaRepository implements JavaRepository {
if (relativized.getNameCount() > 1) {
String name = relativized.getName(0).toString();
Files.deleteIfExists(getManifestFile(java.getPlatform(), name));
FileUtils.deleteDirectory(getJavaDir(java.getPlatform(), name).toFile());
FileUtils.deleteDirectory(getJavaDir(java.getPlatform(), name));
}
});
}
@@ -0,0 +1,116 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.java;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
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.SystemUtils;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* @author Glavo
* @see <a href="https://github.com/Glavo/java-info">Glavo/java-info</a>
*/
public final class JavaInfoUtils {
private JavaInfoUtils() {
}
private static Path tryFindReleaseFile(Path executable) {
Path parent = executable.getParent();
if (parent != null && parent.getFileName() != null && parent.getFileName().toString().equals("bin")) {
Path javaHome = parent.getParent();
if (javaHome != null && javaHome.getFileName() != null) {
Path releaseFile = javaHome.resolve("release");
String javaHomeName = javaHome.getFileName().toString();
if ((javaHomeName.contains("jre") || javaHomeName.contains("jdk") || javaHomeName.contains("openj9"))
&& Files.isRegularFile(releaseFile)) {
return releaseFile;
}
}
}
return null;
}
public static @NotNull JavaInfo fromExecutable(Path executable, boolean tryFindReleaseFile) throws IOException {
assert executable.isAbsolute();
Path releaseFile;
if (tryFindReleaseFile && (releaseFile = tryFindReleaseFile(executable)) != null) {
try {
return JavaInfo.fromReleaseFile(releaseFile);
} catch (IOException ignored) {
}
}
Path thisPath = JarUtils.thisJarPath();
if (thisPath == null) {
throw new IOException("Failed to find current HMCL location");
}
try {
Result result = JsonUtils.GSON.fromJson(SystemUtils.run(
executable.toString(),
"-classpath",
thisPath.toString(),
org.glavo.info.Main.class.getName()
), Result.class);
if (result == null) {
throw new IOException("Failed to get Java info from " + executable);
}
if (result.javaVersion == null) {
throw new IOException("Failed to get Java version from " + executable);
}
Architecture architecture = Architecture.parseArchName(result.osArch);
Platform platform = Platform.getPlatform(OperatingSystem.CURRENT_OS,
architecture != Architecture.UNKNOWN
? architecture
: Architecture.SYSTEM_ARCH);
return new JavaInfo(platform, result.javaVersion, result.javaVendor);
} catch (IOException e) {
throw e;
} catch (Throwable e) {
throw new IOException(e);
}
}
private static final class Result {
@SerializedName("os.name")
public String osName;
@SerializedName("os.arch")
public String osArch;
@SerializedName("java.version")
public String javaVersion;
@SerializedName("java.vendor")
public String javaVendor;
}
}
@@ -17,20 +17,23 @@
*/
package org.jackhuang.hmcl.java;
import org.apache.commons.compress.archivers.ArchiveEntry;
import kala.compress.archivers.ArchiveEntry;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.Hex;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;
import java.io.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -98,10 +101,9 @@ public final class JavaInstallTask extends Task<JavaManifest> {
}
if (tree.isExecutable(entry))
//noinspection ResultOfMethodCallIgnored
path.toFile().setExecutable(true);
FileUtils.setExecutable(path);
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalFile(Hex.encodeHex(messageDigest.digest()), size));
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalFile(HexFormat.of().formatHex(messageDigest.digest()), size));
}
nameStack.remove(nameStack.size() - 1);
}
@@ -31,18 +31,15 @@ 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.platform.*;
import org.jackhuang.hmcl.util.platform.windows.WinReg;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.CountDownLatch;
@@ -58,7 +55,8 @@ public final class JavaManager {
private JavaManager() {
}
public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_DIRECTORY.resolve("java"));
public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_GLOBAL_DIRECTORY.resolve("java"));
public static final HMCLJavaRepository LOCAL_REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_CURRENT_DIRECTORY.resolve("java"));
public static String getMojangJavaPlatform(Platform platform) {
if (platform.getOperatingSystem() == OperatingSystem.WINDOWS) {
@@ -75,7 +73,7 @@ public final class JavaManager {
} else if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
return "linux";
}
} else if (platform.getOperatingSystem() == OperatingSystem.OSX) {
} else if (platform.getOperatingSystem() == OperatingSystem.MACOS) {
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
return "mac-os";
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
@@ -113,7 +111,7 @@ public final class JavaManager {
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
return architecture == Architecture.X86;
break;
case OSX:
case MACOS:
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
return architecture == Architecture.X86_64;
break;
@@ -162,7 +160,7 @@ public final class JavaManager {
return javaRuntime;
}
JavaInfo info = JavaInfo.fromExecutable(executable);
JavaInfo info = JavaInfoUtils.fromExecutable(executable, true);
return JavaRuntime.of(executable, info, false);
}
@@ -211,9 +209,18 @@ public final class JavaManager {
public static Task<Void> getUninstallJavaTask(JavaRuntime java) {
assert java.isManaged();
Path root = REPOSITORY.getPlatformRoot(java.getPlatform());
Path relativized = root.relativize(java.getBinary());
Path platformRoot;
try {
platformRoot = REPOSITORY.getPlatformRoot(java.getPlatform()).toRealPath();
} catch (Throwable ignored) {
return Task.completed(null);
}
if (!java.getBinary().startsWith(platformRoot))
return Task.completed(null);
Path relativized = platformRoot.relativize(java.getBinary());
if (relativized.getNameCount() > 1) {
FXUtils.runInFX(() -> {
try {
@@ -257,16 +264,15 @@ public final class JavaManager {
}
}
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();
private static JavaRuntime chooseJava(@Nullable JavaRuntime java1, JavaRuntime java2) {
if (java1 == null)
return java2;
if (b1 != b2)
return b1 ? 1 : -1;
}
return java1.getVersionNumber().compareTo(java2.getVersionNumber());
if (java1.getParsedVersion() != java2.getParsedVersion())
// Prefer the Java version that is closer to the game's recommended Java version
return java1.getParsedVersion() < java2.getParsedVersion() ? java1 : java2;
else
return java1.getVersionNumber().compareTo(java2.getVersionNumber()) >= 0 ? java1 : java2;
}
@Nullable
@@ -279,12 +285,9 @@ public final class JavaManager {
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)
&& (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS || OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
&& (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) {
@@ -312,15 +315,10 @@ public final class JavaManager {
}
if (!violationMandatory) {
if (mandatory == null) mandatory = java;
else if (compareJavaVersion(java, mandatory, suggestedJavaVersion) > 0)
mandatory = java;
mandatory = chooseJava(mandatory, java);
if (!violationSuggested) {
if (suggested == null) suggested = java;
else if (compareJavaVersion(java, suggested, suggestedJavaVersion) > 0)
suggested = java;
}
if (!violationSuggested)
suggested = chooseJava(suggested, java);
}
}
@@ -349,18 +347,18 @@ public final class JavaManager {
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86);
}
break;
case OSX:
case MACOS:
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
searchAllJavaInRepository(javaRuntimes, Platform.OSX_X86_64);
searchAllJavaInRepository(javaRuntimes, Platform.MACOS_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\\");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Runtime Environment");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Development Kit");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JRE");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JDK");
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles", "C:\\Program Files");
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles(x86)", "C:\\Program Files (x86)");
@@ -375,9 +373,9 @@ public final class JavaManager {
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"));
case MACOS:
searchJavaInMacJavaVirtualMachines(javaRuntimes, Paths.get("/Library/Java/JavaVirtualMachines"));
searchJavaInMacJavaVirtualMachines(javaRuntimes, Paths.get(System.getProperty("user.home"), "/Library/Java/JavaVirtualMachines"));
tryAddJavaExecutable(javaRuntimes, Paths.get("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"));
tryAddJavaExecutable(javaRuntimes, Paths.get("/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java"));
// Homebrew
@@ -405,24 +403,31 @@ public final class JavaManager {
.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) {
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
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);
String[] paths = System.getenv("PATH").split(File.pathSeparator);
for (String path : paths) {
// https://github.com/HMCL-dev/HMCL/issues/4079
// https://github.com/Meloong-Git/PCL/issues/4261
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && path.toLowerCase(Locale.ROOT)
.contains("\\common files\\oracle\\java\\")) {
continue;
}
try {
tryAddJavaExecutable(javaRuntimes, Paths.get(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
tryAddJavaExecutable(javaRuntimes, Path.of(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
} catch (InvalidPathException ignored) {
}
}
}
if (System.getenv("HMCL_JRES") != null) {
String[] paths = System.getenv("HMCL_JRES").split(OperatingSystem.PATH_SEPARATOR);
String[] paths = System.getenv("HMCL_JRES").split(File.pathSeparator);
for (String path : paths) {
try {
tryAddJavaHome(javaRuntimes, Paths.get(path));
@@ -483,12 +488,15 @@ public final class JavaManager {
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);
}
LOG.warning("Failed to read release file " + releaseFile, e);
}
}
if (info == null) {
try {
info = JavaInfoUtils.fromExecutable(executable, false);
} catch (IOException e) {
LOG.warning("Failed to lookup Java executable at " + executable, e);
}
}
@@ -509,7 +517,7 @@ public final class JavaManager {
JavaInfo info = null;
try {
info = JavaInfo.fromExecutable(executable);
info = JavaInfoUtils.fromExecutable(executable, true);
} catch (IOException e) {
LOG.warning("Failed to lookup Java executable at " + executable, e);
}
@@ -548,7 +556,7 @@ public final class JavaManager {
}
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
Path macPath = dir.resolve("jre.bundle/Contents/Home");
if (Files.exists(macPath)) {
tryAddJavaHome(javaRuntimes, macPath);
@@ -564,6 +572,10 @@ public final class JavaManager {
for (JavaRuntime java : REPOSITORY.getAllJava(platform)) {
javaRuntimes.put(java.getBinary(), java);
}
for (JavaRuntime java : LOCAL_REPOSITORY.getAllJava(platform)) {
javaRuntimes.put(java.getBinary(), java);
}
}
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, boolean verify) {
@@ -587,8 +599,8 @@ public final class JavaManager {
}
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);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && Architecture.CURRENT_ARCH == Architecture.ARM64) {
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.MACOS_X86_64), verify);
}
}
@@ -631,68 +643,37 @@ public final class JavaManager {
}
}
private static void searchJavaInMacJavaVirtualMachines(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.resolve("Contents/Home"));
}
} catch (IOException e) {
LOG.warning("Failed to find Java in " + directory, e);
}
}
// ==== Windows Registry Support ====
private static void queryJavaInRegistryKey(Map<Path, JavaRuntime> javaRuntimes, String location) {
for (String java : querySubFolders(location)) {
if (!querySubFolders(java).contains(java + "\\MSI"))
private static void queryJavaInRegistryKey(Map<Path, JavaRuntime> javaRuntimes, WinReg.HKEY hkey, String location) {
WinReg reg = WinReg.INSTANCE;
if (reg == null)
return;
for (String java : reg.querySubKeys(hkey, location)) {
if (!reg.querySubKeys(hkey, java).contains(java + "\\MSI"))
continue;
String home = queryRegisterValue(java, "JavaHome");
if (home != null) {
Object home = reg.queryValue(hkey, java, "JavaHome");
if (home instanceof String) {
try {
tryAddJavaHome(javaRuntimes, Paths.get(home));
tryAddJavaHome(javaRuntimes, Paths.get((String) 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;
}
}
@@ -22,6 +22,7 @@ import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.*;
@@ -34,9 +35,8 @@ import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
import org.jackhuang.hmcl.game.OAuthServer;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.InvocationDispatcher;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
import javax.net.ssl.SSLException;
@@ -50,6 +50,7 @@ import java.util.*;
import static java.util.stream.Collectors.toList;
import static javafx.collections.FXCollections.observableArrayList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.Lang.immutableListOf;
import static org.jackhuang.hmcl.util.Lang.mapOf;
@@ -68,18 +69,6 @@ public final class Accounts {
private static final AuthlibInjectorArtifactProvider AUTHLIB_INJECTOR_DOWNLOADER = createAuthlibInjectorArtifactProvider();
private static void triggerAuthlibInjectorUpdateCheck() {
if (AUTHLIB_INJECTOR_DOWNLOADER instanceof AuthlibInjectorDownloader) {
Schedulers.io().execute(() -> {
try {
((AuthlibInjectorDownloader) AUTHLIB_INJECTOR_DOWNLOADER).checkUpdate();
} catch (IOException e) {
LOG.warning("Failed to check update for authlib-injector", e);
}
});
}
}
public static final OAuthServer.Factory OAUTH_CALLBACK = new OAuthServer.Factory();
public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
@@ -173,7 +162,7 @@ public final class Accounts {
}
private static void loadGlobalAccountStorages() {
Path globalAccountsFile = Metadata.HMCL_DIRECTORY.resolve("accounts.json");
Path globalAccountsFile = Metadata.HMCL_GLOBAL_DIRECTORY.resolve("accounts.json");
if (Files.exists(globalAccountsFile)) {
try (Reader reader = Files.newBufferedReader(globalAccountsFile)) {
globalAccountStorages.setAll(Config.CONFIG_GSON.fromJson(reader, listTypeOf(mapTypeOf(Object.class, Object.class))));
@@ -182,21 +171,8 @@ public final class Accounts {
}
}
InvocationDispatcher<String> dispatcher = InvocationDispatcher.runOn(Lang::thread, json -> {
LOG.info("Saving global accounts");
synchronized (globalAccountsFile) {
try {
synchronized (globalAccountsFile) {
FileUtils.saveSafely(globalAccountsFile, json);
}
} catch (IOException e) {
LOG.error("Failed to save global accounts", e);
}
}
});
globalAccountStorages.addListener(onInvalidating(() ->
dispatcher.accept(Config.CONFIG_GSON.toJson(globalAccountStorages))));
FileSaver.save(globalAccountsFile, Config.CONFIG_GSON.toJson(globalAccountStorages))));
}
private static Account parseAccount(Map<Object, Object> storage) {
@@ -277,6 +253,30 @@ public final class Accounts {
selected = accounts.get(0);
}
if (!globalConfig().isEnableOfflineAccount())
for (Account account : accounts) {
if (account instanceof MicrosoftAccount) {
globalConfig().setEnableOfflineAccount(true);
break;
}
}
if (!globalConfig().isEnableOfflineAccount())
accounts.addListener(new ListChangeListener<Account>() {
@Override
public void onChanged(Change<? extends Account> change) {
while (change.next()) {
for (Account account : change.getAddedSubList()) {
if (account instanceof MicrosoftAccount) {
accounts.removeListener(this);
globalConfig().setEnableOfflineAccount(true);
return;
}
}
}
}
});
selectedAccount.set(selected);
InvalidationListener listener = o -> {
@@ -326,8 +326,6 @@ public final class Accounts {
});
}
triggerAuthlibInjectorUpdateCheck();
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
continue;
@@ -335,7 +333,7 @@ public final class Accounts {
try {
server.fetchMetadataResponse();
} catch (IOException e) {
LOG.warning("Failed to fetch authlib-injector server metdata: " + server, e);
LOG.warning("Failed to fetch authlib-injector server metadata: " + server, e);
}
});
}
@@ -360,24 +358,18 @@ public final class Accounts {
// ==== authlib-injector ====
private static AuthlibInjectorArtifactProvider createAuthlibInjectorArtifactProvider() {
String authlibinjectorLocation = System.getProperty("hmcl.authlibinjector.location");
if (authlibinjectorLocation == null) {
return new AuthlibInjectorDownloader(
Metadata.HMCL_DIRECTORY.resolve("authlib-injector.jar"),
DownloadProviders::getDownloadProvider) {
@Override
public Optional<AuthlibInjectorArtifactInfo> getArtifactInfoImmediately() {
Optional<AuthlibInjectorArtifactInfo> local = super.getArtifactInfoImmediately();
if (local.isPresent()) {
return local;
}
// search authlib-injector.jar in current directory, it's used as a fallback
return parseArtifact(Paths.get("authlib-injector.jar"));
}
};
} else {
if (authlibinjectorLocation != null) {
LOG.info("Using specified authlib-injector: " + authlibinjectorLocation);
return new SimpleAuthlibInjectorArtifactProvider(Paths.get(authlibinjectorLocation));
}
String authlibInjectorVersion = JarUtils.getAttribute("hmcl.authlib-injector.version", null);
if (authlibInjectorVersion == null)
throw new AssertionError("Missing hmcl.authlib-injector.version");
String authlibInjectorFileName = "authlib-injector-" + authlibInjectorVersion + ".jar";
return new AuthlibInjectorExtractor(Accounts.class.getResource("/assets/" + authlibInjectorFileName),
Metadata.DEPENDENCIES_DIRECTORY.resolve("universal").resolve(authlibInjectorFileName));
}
private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) {
@@ -463,6 +455,8 @@ public final class Accounts {
return i18n("account.methods.microsoft.error.country_unavailable");
} else if (errorCode == MicrosoftService.XboxAuthorizationException.MISSING_XBOX_ACCOUNT) {
return i18n("account.methods.microsoft.error.missing_xbox_account");
} else if (errorCode == MicrosoftService.XboxAuthorizationException.BANNED) {
return i18n("account.methods.microsoft.error.banned");
} else {
return i18n("account.methods.microsoft.error.unknown", errorCode);
}
@@ -21,10 +21,10 @@ import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import java.io.IOException;
@@ -38,6 +38,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@JsonSerializable
public final class AuthlibInjectorServers implements Validation {
public static final String CONFIG_FILENAME = "authlib-injectors.json";
@@ -73,8 +74,7 @@ public final class AuthlibInjectorServers implements Validation {
if (ConfigHolder.isNewlyCreated() && Files.exists(configLocation)) {
AuthlibInjectorServers configInstance;
try {
String content = FileUtils.readText(configLocation);
configInstance = JsonUtils.GSON.fromJson(content, AuthlibInjectorServers.class);
configInstance = JsonUtils.fromJsonFile(configLocation, AuthlibInjectorServers.class);
} catch (IOException | JsonParseException e) {
LOG.warning("Malformed authlib-injectors.json", e);
return;
File diff suppressed because it is too large. Load diff
@@ -19,8 +19,7 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.util.InvocationDispatcher;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
@@ -39,13 +38,14 @@ public final class ConfigHolder {
public static final String CONFIG_FILENAME = "hmcl.json";
public static final String CONFIG_FILENAME_LINUX = ".hmcl.json";
public static final Path GLOBAL_CONFIG_PATH = Metadata.HMCL_DIRECTORY.resolve("config.json");
public static final Path GLOBAL_CONFIG_PATH = Metadata.HMCL_GLOBAL_DIRECTORY.resolve("config.json");
private static Path configLocation;
private static Config configInstance;
private static GlobalConfig globalConfigInstance;
private static boolean newlyCreated;
private static boolean ownerChanged = false;
private static boolean unsupportedVersion = false;
public static Config config() {
if (configInstance == null) {
@@ -73,6 +73,10 @@ public final class ConfigHolder {
return ownerChanged;
}
public static boolean isUnsupportedVersion() {
return unsupportedVersion;
}
public static void init() throws IOException {
if (configInstance != null) {
throw new IllegalStateException("Configuration is already loaded");
@@ -83,10 +87,11 @@ public final class ConfigHolder {
LOG.info("Config location: " + configLocation);
configInstance = loadConfig();
configInstance.addListener(source -> markConfigDirty());
if (!unsupportedVersion)
configInstance.addListener(source -> FileSaver.save(configLocation, configInstance.toJson()));
globalConfigInstance = loadGlobalConfig();
globalConfigInstance.addListener(source -> markGlobalConfigDirty());
globalConfigInstance.addListener(source -> FileSaver.save(GLOBAL_CONFIG_PATH, globalConfigInstance.toJson()));
Locale.setDefault(config().getLocalization().getLocale());
I18n.setLocale(configInstance.getLocalization());
@@ -94,22 +99,15 @@ public final class ConfigHolder {
Settings.init();
if (newlyCreated) {
saveConfigSync();
// hide the config file on windows
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
try {
Files.setAttribute(configLocation, "dos:hidden", true);
} catch (IOException e) {
LOG.warning("Failed to set hidden attribute of " + configLocation, e);
}
}
LOG.info("Creating config file " + configLocation);
FileUtils.saveSafely(configLocation, configInstance.toJson());
}
if (!Files.isWritable(configLocation)) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
&& configLocation.getFileSystem() == FileSystems.getDefault()
&& configLocation.toFile().canWrite()) {
LOG.warning("Config at " + configLocation + " is not writable, but it seems to be a Samba share or OpenJDK bug");
// There are some serious problems with the implementation of Samba or OpenJDK
throw new SambaException();
} else {
@@ -121,12 +119,14 @@ public final class ConfigHolder {
}
private static Path locateConfig() {
Path exePath = Paths.get("").toAbsolutePath();
Path defaultConfigFile = Metadata.HMCL_CURRENT_DIRECTORY.resolve(CONFIG_FILENAME);
if (Files.isRegularFile(defaultConfigFile))
return defaultConfigFile;
try {
Path jarPath = JarUtils.thisJarPath();
if (jarPath != null && Files.isRegularFile(jarPath) && Files.isWritable(jarPath)) {
jarPath = jarPath.getParent();
exePath = jarPath;
Path config = jarPath.resolve(CONFIG_FILENAME);
if (Files.isRegularFile(config))
@@ -149,7 +149,7 @@ public final class ConfigHolder {
return dotConfig;
// create new
return exePath.resolve(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? CONFIG_FILENAME : CONFIG_FILENAME_LINUX);
return defaultConfigFile;
}
private static Config loadConfig() throws IOException {
@@ -164,12 +164,19 @@ public final class ConfigHolder {
LOG.warning("Failed to get owner");
}
try {
String content = FileUtils.readText(configLocation);
String content = Files.readString(configLocation);
Config deserialized = Config.fromJson(content);
if (deserialized == null) {
LOG.info("Config is empty");
} else {
ConfigUpgrader.upgradeConfig(deserialized, content);
int configVersion = deserialized.getConfigVersion();
if (configVersion < Config.CURRENT_VERSION) {
ConfigUpgrader.upgradeConfig(deserialized, content);
} else if (configVersion > Config.CURRENT_VERSION) {
unsupportedVersion = true;
LOG.warning(String.format("Current HMCL only support the configuration version up to %d. However, the version now is %d.", Config.CURRENT_VERSION, configVersion));
}
return deserialized;
}
} catch (JsonParseException e) {
@@ -177,40 +184,16 @@ public final class ConfigHolder {
}
}
LOG.info("Creating an empty config");
newlyCreated = true;
return new Config();
}
private static final InvocationDispatcher<String> configWriter = InvocationDispatcher.runOn(Lang::thread, content -> {
try {
writeToConfig(content);
} catch (IOException e) {
LOG.error("Failed to save config", e);
}
});
private static void writeToConfig(String content) throws IOException {
LOG.info("Saving config");
synchronized (configLocation) {
FileUtils.saveSafely(configLocation, content);
}
}
static void markConfigDirty() {
configWriter.accept(configInstance.toJson());
}
private static void saveConfigSync() throws IOException {
writeToConfig(configInstance.toJson());
}
// Global Config
private static GlobalConfig loadGlobalConfig() throws IOException {
if (Files.exists(GLOBAL_CONFIG_PATH)) {
try {
String content = FileUtils.readText(GLOBAL_CONFIG_PATH);
String content = Files.readString(GLOBAL_CONFIG_PATH);
GlobalConfig deserialized = GlobalConfig.fromJson(content);
if (deserialized == null) {
LOG.info("Config is empty");
@@ -226,26 +209,4 @@ public final class ConfigHolder {
return new GlobalConfig();
}
private static final InvocationDispatcher<String> globalConfigWriter = InvocationDispatcher.runOn(Lang::thread, content -> {
try {
writeToGlobalConfig(content);
} catch (IOException e) {
LOG.error("Failed to save config", e);
}
});
private static void writeToGlobalConfig(String content) throws IOException {
LOG.info("Saving global config");
synchronized (GLOBAL_CONFIG_PATH) {
FileUtils.saveSafely(GLOBAL_CONFIG_PATH, content);
}
}
static void markGlobalConfigDirty() {
globalConfigWriter.accept(globalConfigInstance.toJson());
}
private static void saveGlobalConfigSync() throws IOException {
writeToConfig(globalConfigInstance.toJson());
}
}
@@ -31,8 +31,6 @@ final class ConfigUpgrader {
private ConfigUpgrader() {
}
private static final int CURRENT_VERSION = 2;
/**
* This method is for the compatibility with old HMCL versions.
*
@@ -42,17 +40,10 @@ final class ConfigUpgrader {
static void upgradeConfig(Config deserialized, String rawContent) {
int configVersion = deserialized.getConfigVersion();
if (configVersion == CURRENT_VERSION) {
if (configVersion >= Config.CURRENT_VERSION)
return;
}
if (configVersion > CURRENT_VERSION) {
LOG.warning(String.format("Current HMCL only support the configuration version up to %d. However, the version now is %d.", CURRENT_VERSION, configVersion));
deserialized.setConfigVersion(CURRENT_VERSION);
return;
}
LOG.info(String.format("Updating configuration from %d to %d.", configVersion, CURRENT_VERSION));
LOG.info(String.format("Updating configuration from %d to %d.", configVersion, Config.CURRENT_VERSION));
Map<?, ?> rawJson = Collections.unmodifiableMap(new Gson().<Map<?, ?>>fromJson(rawContent, Map.class));
if (configVersion < 1) {
@@ -100,6 +91,6 @@ final class ConfigUpgrader {
}
}
deserialized.setConfigVersion(CURRENT_VERSION);
deserialized.setConfigVersion(Config.CURRENT_VERSION);
}
}
@@ -21,142 +21,123 @@ import javafx.beans.InvalidationListener;
import org.jackhuang.hmcl.download.*;
import org.jackhuang.hmcl.task.DownloadException;
import org.jackhuang.hmcl.task.FetchTask;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import javax.net.ssl.SSLHandshakeException;
import java.io.FileNotFoundException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URI;
import java.nio.file.AccessDeniedException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.task.FetchTask.DEFAULT_CONCURRENCY;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class DownloadProviders {
private DownloadProviders() {}
private DownloadProviders() {
}
private static DownloadProvider currentDownloadProvider;
public static final String DEFAULT_AUTO_PROVIDER_ID = "balanced";
public static final String DEFAULT_DIRECT_PROVIDER_ID = "mojang";
public static final Map<String, DownloadProvider> providersById;
public static final Map<String, DownloadProvider> rawProviders;
private static final AdaptedDownloadProvider fileDownloadProvider = new AdaptedDownloadProvider();
private static final DownloadProviderWrapper PROVIDER_WRAPPER;
private static final MojangDownloadProvider MOJANG;
private static final BMCLAPIDownloadProvider BMCLAPI;
public static final String DEFAULT_PROVIDER_ID = "balanced";
public static final String DEFAULT_RAW_PROVIDER_ID = "bmclapi";
private static final InvalidationListener observer;
private static final DownloadProvider DEFAULT_PROVIDER;
public static final Map<String, DownloadProvider> DIRECT_PROVIDERS;
public static final Map<String, DownloadProvider> AUTO_PROVIDERS;
static {
String bmclapiRoot = "https://bmclapi2.bangbang93.com";
String bmclapiRootOverride = System.getProperty("hmcl.bmclapi.override");
if (bmclapiRootOverride != null) bmclapiRoot = bmclapiRootOverride;
String bmclapiRoot = System.getProperty("hmcl.bmclapi.override", "https://bmclapi2.bangbang93.com");
BMCLAPIDownloadProvider bmclapiRaw = new BMCLAPIDownloadProvider(bmclapiRoot);
MOJANG = new MojangDownloadProvider();
BMCLAPI = new BMCLAPIDownloadProvider(bmclapiRoot);
rawProviders = mapOf(
pair("mojang", MOJANG),
pair("bmclapi", BMCLAPI)
DownloadProvider mojang = new MojangDownloadProvider();
DownloadProvider bmclapi = new AutoDownloadProvider(bmclapiRaw, mojang);
DEFAULT_PROVIDER = mojang;
DIRECT_PROVIDERS = Lang.mapOf(
pair("mojang", mojang),
pair("bmclapi", bmclapi)
);
AdaptedDownloadProvider fileProvider = new AdaptedDownloadProvider();
fileProvider.setDownloadProviderCandidates(Arrays.asList(BMCLAPI, MOJANG));
BalancedDownloadProvider balanced = new BalancedDownloadProvider(MOJANG, BMCLAPI);
AUTO_PROVIDERS = Lang.mapOf(
pair("balanced", LocaleUtils.IS_CHINA_MAINLAND ? bmclapi : mojang),
pair("official", LocaleUtils.IS_CHINA_MAINLAND ? new AutoDownloadProvider(
List.of(mojang, bmclapiRaw),
List.of(bmclapiRaw, mojang)
) : mojang),
pair("mirror", bmclapi)
);
providersById = mapOf(
pair("official", new AutoDownloadProvider(MOJANG, fileProvider)),
pair("balanced", new AutoDownloadProvider(balanced, fileProvider)),
pair("mirror", new AutoDownloadProvider(BMCLAPI, fileProvider)));
observer = FXUtils.observeWeak(() -> {
FetchTask.setDownloadExecutorConcurrency(
config().getAutoDownloadThreads() ? DEFAULT_CONCURRENCY : config().getDownloadThreads());
}, config().autoDownloadThreadsProperty(), config().downloadThreadsProperty());
PROVIDER_WRAPPER = new DownloadProviderWrapper(DEFAULT_PROVIDER);
}
static void init() {
FXUtils.onChangeAndOperate(config().versionListSourceProperty(), versionListSource -> {
if (!providersById.containsKey(versionListSource)) {
config().setVersionListSource(DEFAULT_PROVIDER_ID);
return;
InvalidationListener onChangeDownloadThreads = observable -> {
FetchTask.setDownloadExecutorConcurrency(config().getAutoDownloadThreads()
? DEFAULT_CONCURRENCY
: config().getDownloadThreads());
};
config().autoDownloadThreadsProperty().addListener(onChangeDownloadThreads);
config().downloadThreadsProperty().addListener(onChangeDownloadThreads);
onChangeDownloadThreads.invalidated(null);
InvalidationListener onChangeDownloadSource = observable -> {
if (config().isAutoChooseDownloadType()) {
String versionListSource = config().getVersionListSource();
DownloadProvider downloadProvider = versionListSource != null
? AUTO_PROVIDERS.getOrDefault(versionListSource, DEFAULT_PROVIDER)
: DEFAULT_PROVIDER;
PROVIDER_WRAPPER.setProvider(downloadProvider);
} else {
String downloadType = config().getDownloadType();
PROVIDER_WRAPPER.setProvider(downloadType != null
? DIRECT_PROVIDERS.getOrDefault(downloadType, DEFAULT_PROVIDER)
: DEFAULT_PROVIDER);
}
currentDownloadProvider = Optional.ofNullable(providersById.get(versionListSource))
.orElse(providersById.get(DEFAULT_PROVIDER_ID));
});
if (!rawProviders.containsKey(config().getDownloadType())) {
config().setDownloadType(DEFAULT_RAW_PROVIDER_ID);
}
FXUtils.onChangeAndOperate(config().downloadTypeProperty(), downloadType -> {
DownloadProvider primary = Optional.ofNullable(rawProviders.get(downloadType))
.orElse(rawProviders.get(DEFAULT_RAW_PROVIDER_ID));
fileDownloadProvider.setDownloadProviderCandidates(
Stream.concat(
Stream.of(primary),
rawProviders.values().stream().filter(x -> x != primary)
).collect(Collectors.toList())
);
});
}
public static String getPrimaryDownloadProviderId() {
String downloadType = config().getDownloadType();
if (providersById.containsKey(downloadType))
return downloadType;
else
return DEFAULT_PROVIDER_ID;
}
public static DownloadProvider getDownloadProviderByPrimaryId(String primaryId) {
return Optional.ofNullable(providersById.get(primaryId))
.orElse(providersById.get(DEFAULT_PROVIDER_ID));
};
config().versionListSourceProperty().addListener(onChangeDownloadSource);
config().autoChooseDownloadTypeProperty().addListener(onChangeDownloadSource);
config().downloadTypeProperty().addListener(onChangeDownloadSource);
onChangeDownloadSource.invalidated(null);
}
/**
* Get current primary preferred download provider
*/
public static DownloadProvider getDownloadProvider() {
return config().isAutoChooseDownloadType() ? currentDownloadProvider : fileDownloadProvider;
return PROVIDER_WRAPPER;
}
public static String localizeErrorMessage(Throwable exception) {
if (exception instanceof DownloadException) {
URL url = ((DownloadException) exception).getUrl();
URI uri = ((DownloadException) exception).getUri();
if (exception.getCause() instanceof SocketTimeoutException) {
return i18n("install.failed.downloading.timeout", url);
return i18n("install.failed.downloading.timeout", uri);
} else if (exception.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
return i18n("download.code." + responseCodeException.getResponseCode(), url);
return i18n("download.code." + responseCodeException.getResponseCode(), uri);
} else {
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
}
} else if (exception.getCause() instanceof FileNotFoundException) {
return i18n("download.code.404", url);
return i18n("download.code.404", uri);
} else if (exception.getCause() instanceof AccessDeniedException) {
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
} else if (exception.getCause() instanceof ArtifactMalformedException) {
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.artifact_malformed");
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.artifact_malformed");
} else if (exception.getCause() instanceof SSLHandshakeException) {
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.ssl_handshake");
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.ssl_handshake");
} else {
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
}
} else if (exception instanceof ArtifactMalformedException) {
return i18n("exception.artifact_malformed");
@@ -22,5 +22,6 @@ public enum EnumBackgroundImage {
CUSTOM,
CLASSIC,
NETWORK,
TRANSLUCENT
TRANSLUCENT, // Deprecated
PAINT
}
@@ -0,0 +1,294 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class FontManager {
public static final String[] FONT_EXTENSIONS = {
"ttf", "otf", "woff"
};
public static final double DEFAULT_FONT_SIZE = 12.0f;
private static final Lazy<Font> DEFAULT_FONT = new Lazy<>(() -> {
Font font;
// Recommended
font = tryLoadLocalizedFont(Metadata.HMCL_CURRENT_DIRECTORY.resolve("font"));
if (font != null)
return font;
font = tryLoadLocalizedFont(Metadata.HMCL_GLOBAL_DIRECTORY.resolve("font"));
if (font != null)
return font;
// Legacy
font = tryLoadDefaultFont(Metadata.HMCL_CURRENT_DIRECTORY);
if (font != null)
return font;
font = tryLoadDefaultFont(Metadata.CURRENT_DIRECTORY);
if (font != null)
return font;
font = tryLoadDefaultFont(Metadata.HMCL_GLOBAL_DIRECTORY);
if (font != null)
return font;
Path thisJar = JarUtils.thisJarPath();
if (thisJar != null && thisJar.getParent() != null) {
font = tryLoadDefaultFont(thisJar.getParent());
if (font != null)
return font;
}
// Default
String fcMatchPattern;
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()
&& !(fcMatchPattern = I18n.getLocale().getFcMatchPattern()).isEmpty())
return findByFcMatch(fcMatchPattern);
else
return null;
});
private static final ObjectProperty<FontReference> fontProperty;
static {
String fontFamily = config().getLauncherFontFamily();
if (fontFamily == null)
fontFamily = System.getProperty("hmcl.font.override");
if (fontFamily == null)
fontFamily = System.getenv("HMCL_FONT");
FontReference fontReference;
if (fontFamily == null) {
Font defaultFont = DEFAULT_FONT.get();
fontReference = defaultFont != null ? new FontReference(defaultFont) : null;
} else
fontReference = new FontReference(fontFamily);
fontProperty = new SimpleObjectProperty<>(fontReference);
LOG.info("Font: " + (fontReference != null ? fontReference.getFamily() : "System"));
fontProperty.addListener((obs, oldValue, newValue) -> {
if (newValue != null)
config().setLauncherFontFamily(newValue.getFamily());
else
config().setLauncherFontFamily(null);
});
}
private static Font tryLoadDefaultFont(Path dir) {
for (String extension : FONT_EXTENSIONS) {
Path path = dir.resolve("font." + extension);
if (Files.isRegularFile(path)) {
LOG.info("Load font file: " + path);
try {
Font font = Font.loadFont(path.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
if (font != null) {
return font;
}
} catch (MalformedURLException ignored) {
}
LOG.warning("Failed to load font " + path);
}
}
return null;
}
private static Font tryLoadLocalizedFont(Path dir) {
Map<String, Map<String, Path>> fontFiles = LocaleUtils.findAllLocalizedFiles(dir, "font", Set.of(FONT_EXTENSIONS));
if (fontFiles.isEmpty())
return null;
List<Locale> candidateLocales = I18n.getLocale().getCandidateLocales();
for (Locale locale : candidateLocales) {
Map<String, Path> extToFiles = fontFiles.get(LocaleUtils.toLanguageKey(locale));
if (extToFiles != null) {
for (String ext : FONT_EXTENSIONS) {
Path fontFile = extToFiles.get(ext);
if (fontFile != null) {
LOG.info("Load font file: " + fontFile);
try {
Font font = Font.loadFont(
fontFile.toAbsolutePath().normalize().toUri().toURL().toExternalForm(),
DEFAULT_FONT_SIZE);
if (font != null)
return font;
} catch (MalformedURLException ignored) {
}
LOG.warning("Failed to load font " + fontFile);
}
}
}
}
return null;
}
public static Font findByFcMatch(String pattern) {
Path fcMatch = SystemUtils.which("fc-match");
if (fcMatch == null)
return null;
try {
String result = SystemUtils.run(fcMatch.toString(),
pattern,
"--format", "%{family}\\n%{file}").trim();
String[] results = result.split("\\n");
if (results.length != 2 || results[0].isEmpty() || results[1].isEmpty()) {
LOG.warning("Unexpected output from fc-match: " + result);
return null;
}
String family = results[0].trim();
String path = results[1];
Path file = Paths.get(path).toAbsolutePath().normalize();
if (!Files.isRegularFile(file)) {
LOG.warning("Font file does not exist: " + path);
return null;
}
LOG.info("Load font file: " + path);
Font[] fonts = Font.loadFonts(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
if (fonts == null) {
LOG.warning("Failed to load font from " + path);
return null;
} else if (fonts.length == 0) {
LOG.warning("No fonts loaded from " + path);
return null;
}
for (Font font : fonts) {
if (font.getFamily().equalsIgnoreCase(family)) {
return font;
}
}
if (family.indexOf(',') >= 0) {
for (String candidateFamily : family.split(",")) {
for (Font font : fonts) {
if (font.getFamily().equalsIgnoreCase(candidateFamily)) {
return font;
}
}
}
}
LOG.warning(String.format("Family '%s' not found in font file '%s'", family, path));
return fonts[0];
} catch (Throwable e) {
LOG.warning("Failed to get default font with fc-match", e);
return null;
}
}
public static ObjectProperty<FontReference> fontProperty() {
return fontProperty;
}
public static FontReference getFont() {
return fontProperty.get();
}
public static void setFont(FontReference font) {
fontProperty.set(font);
}
public static void setFontFamily(String fontFamily) {
setFont(fontFamily != null ? new FontReference(fontFamily) : null);
}
// https://github.com/HMCL-dev/HMCL/issues/4072
public static final class FontReference {
private final @NotNull String family;
private final @Nullable String style;
public FontReference(@NotNull String family) {
this.family = Objects.requireNonNull(family);
this.style = null;
}
public FontReference(@NotNull Font font) {
this.family = font.getFamily();
this.style = font.getStyle();
}
public @NotNull String getFamily() {
return family;
}
public @Nullable String getStyle() {
return style;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof FontReference))
return false;
FontReference that = (FontReference) o;
return Objects.equals(family, that.family) && Objects.equals(style, that.style);
}
@Override
public int hashCode() {
return Objects.hash(family, style);
}
@Override
public String toString() {
return String.format("FontReference[family='%s', style='%s']", family, style);
}
}
private FontManager() {
}
}
@@ -21,8 +21,7 @@ import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
@@ -33,7 +32,7 @@ import java.lang.reflect.Type;
import java.util.*;
@JsonAdapter(GlobalConfig.Serializer.class)
public final class GlobalConfig implements Cloneable, Observable {
public final class GlobalConfig implements Observable {
@Nullable
public static GlobalConfig fromJson(String json) throws JsonParseException {
@@ -49,10 +48,16 @@ public final class GlobalConfig implements Cloneable, Observable {
private final IntegerProperty agreementVersion = new SimpleIntegerProperty();
private final IntegerProperty terracottaAgreementVersion = new SimpleIntegerProperty();
private final IntegerProperty platformPromptVersion = new SimpleIntegerProperty();
private final IntegerProperty logRetention = new SimpleIntegerProperty();
private final BooleanProperty enableOfflineAccount = new SimpleBooleanProperty(false);
private final StringProperty fontAntiAliasing = new SimpleStringProperty();
private final ObservableSet<String> userJava = FXCollections.observableSet(new LinkedHashSet<>());
private final ObservableSet<String> disabledJava = FXCollections.observableSet(new LinkedHashSet<>());
@@ -79,11 +84,6 @@ public final class GlobalConfig implements Cloneable, Observable {
return Config.CONFIG_GSON.toJson(this);
}
@Override
public GlobalConfig clone() {
return fromJson(this.toJson());
}
public int getAgreementVersion() {
return agreementVersion.get();
}
@@ -96,6 +96,18 @@ public final class GlobalConfig implements Cloneable, Observable {
this.agreementVersion.set(agreementVersion);
}
public int getTerracottaAgreementVersion() {
return terracottaAgreementVersion.get();
}
public IntegerProperty terracottaAgreementVersionProperty() {
return terracottaAgreementVersion;
}
public void setTerracottaAgreementVersion(int terracottaAgreementVersion) {
this.terracottaAgreementVersion.set(terracottaAgreementVersion);
}
public int getPlatformPromptVersion() {
return platformPromptVersion.get();
}
@@ -120,6 +132,30 @@ public final class GlobalConfig implements Cloneable, Observable {
this.logRetention.set(logRetention);
}
public boolean isEnableOfflineAccount() {
return enableOfflineAccount.get();
}
public BooleanProperty enableOfflineAccountProperty() {
return enableOfflineAccount;
}
public void setEnableOfflineAccount(boolean value) {
enableOfflineAccount.set(value);
}
public StringProperty fontAntiAliasingProperty() {
return fontAntiAliasing;
}
public String getFontAntiAliasing() {
return fontAntiAliasing.get();
}
public void setFontAntiAliasing(String value) {
this.fontAntiAliasing.set(value);
}
public ObservableSet<String> getUserJava() {
return userJava;
}
@@ -131,10 +167,13 @@ public final class GlobalConfig implements Cloneable, Observable {
public static final class Serializer implements JsonSerializer<GlobalConfig>, JsonDeserializer<GlobalConfig> {
private static final Set<String> knownFields = new HashSet<>(Arrays.asList(
"agreementVersion",
"terracottaAgreementVersion",
"platformPromptVersion",
"logRetention",
"userJava",
"disabledJava"
"disabledJava",
"enableOfflineAccount",
"fontAntiAliasing"
));
@Override
@@ -145,8 +184,13 @@ public final class GlobalConfig implements Cloneable, Observable {
JsonObject jsonObject = new JsonObject();
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
jsonObject.add("terracottaAgreementVersion", context.serialize(src.getTerracottaAgreementVersion()));
jsonObject.add("platformPromptVersion", context.serialize(src.getPlatformPromptVersion()));
jsonObject.add("logRetention", context.serialize(src.getLogRetention()));
jsonObject.add("fontAntiAliasing", context.serialize(src.getFontAntiAliasing()));
if (src.enableOfflineAccount.get())
jsonObject.addProperty("enableOfflineAccount", true);
if (!src.getUserJava().isEmpty())
jsonObject.add("userJava", context.serialize(src.getUserJava()));
@@ -168,8 +212,11 @@ public final class GlobalConfig implements Cloneable, Observable {
GlobalConfig config = new GlobalConfig();
config.setAgreementVersion(Optional.ofNullable(obj.get("agreementVersion")).map(JsonElement::getAsInt).orElse(0));
config.setTerracottaAgreementVersion(Optional.ofNullable(obj.get("terracottaAgreementVersion")).map(JsonElement::getAsInt).orElse(0));
config.setPlatformPromptVersion(Optional.ofNullable(obj.get("platformPromptVersion")).map(JsonElement::getAsInt).orElse(0));
config.setLogRetention(Optional.ofNullable(obj.get("logRetention")).map(JsonElement::getAsInt).orElse(20));
config.setEnableOfflineAccount(Optional.ofNullable(obj.get("enableOfflineAccount")).map(JsonElement::getAsBoolean).orElse(false));
config.setFontAntiAliasing(Optional.ofNullable(obj.get("fontAntiAliasing")).map(JsonElement::getAsString).orElse(null));
JsonElement userJava = obj.get("userJava");
if (userJava != null && userJava.isJsonArray()) {
@@ -35,8 +35,8 @@ import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import java.io.File;
import java.lang.reflect.Type;
import java.nio.file.Path;
import java.util.Optional;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
@@ -65,17 +65,17 @@ public final class Profile implements Observable {
this.selectedVersion.set(selectedVersion);
}
private final ObjectProperty<File> gameDir;
private final ObjectProperty<Path> gameDir;
public ObjectProperty<File> gameDirProperty() {
public ObjectProperty<Path> gameDirProperty() {
return gameDir;
}
public File getGameDir() {
public Path getGameDir() {
return gameDir.get();
}
public void setGameDir(File gameDir) {
public void setGameDir(Path gameDir) {
this.gameDir.set(gameDir);
}
@@ -103,7 +103,7 @@ public final class Profile implements Observable {
this.name.set(name);
}
private BooleanProperty useRelativePath = new SimpleBooleanProperty(this, "useRelativePath", false);
private final BooleanProperty useRelativePath = new SimpleBooleanProperty(this, "useRelativePath", false);
public BooleanProperty useRelativePathProperty() {
return useRelativePath;
@@ -118,18 +118,18 @@ public final class Profile implements Observable {
}
public Profile(String name) {
this(name, new File(".minecraft"));
this(name, Path.of(".minecraft"));
}
public Profile(String name, File initialGameDir) {
public Profile(String name, Path initialGameDir) {
this(name, initialGameDir, new VersionSetting());
}
public Profile(String name, File initialGameDir, VersionSetting global) {
public Profile(String name, Path initialGameDir, VersionSetting global) {
this(name, initialGameDir, global, null, false);
}
public Profile(String name, File initialGameDir, VersionSetting global, String selectedVersion, boolean useRelativePath) {
public Profile(String name, Path initialGameDir, VersionSetting global, String selectedVersion, boolean useRelativePath) {
this.name = new SimpleStringProperty(this, "name", name);
gameDir = new SimpleObjectProperty<>(this, "gameDir", initialGameDir);
repository = new HMCLGameRepository(this, initialGameDir);
@@ -188,7 +188,7 @@ public final class Profile implements Observable {
global.addListener(listener);
gameDir.addListener(listener);
useRelativePath.addListener(listener);
global.get().addPropertyChangedListener(listener);
global.get().addListener(listener);
selectedVersion.addListener(listener);
}
@@ -234,7 +234,7 @@ public final class Profile implements Observable {
JsonObject jsonObject = new JsonObject();
jsonObject.add("global", context.serialize(src.getGlobal()));
jsonObject.addProperty("gameDir", src.getGameDir().getPath());
jsonObject.addProperty("gameDir", src.getGameDir().toString());
jsonObject.addProperty("useRelativePath", src.isUseRelativePath());
jsonObject.addProperty("selectedMinecraftVersion", src.getSelectedVersion());
@@ -243,12 +243,11 @@ public final class Profile implements Observable {
@Override
public Profile deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (json == JsonNull.INSTANCE || !(json instanceof JsonObject)) return null;
JsonObject obj = (JsonObject) json;
if (!(json instanceof JsonObject obj)) return null;
String gameDir = Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse("");
return new Profile("Default",
new File(gameDir),
Path.of(gameDir),
context.deserialize(obj.get("global"), VersionSetting.class),
Optional.ofNullable(obj.get("selectedMinecraftVersion")).map(JsonElement::getAsString).orElse(""),
Optional.ofNullable(obj.get("useRelativePath")).map(JsonElement::getAsBoolean).orElse(false));
@@ -26,7 +26,7 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -48,20 +48,17 @@ public final class Profiles {
}
public static String getProfileDisplayName(Profile profile) {
switch (profile.getName()) {
case Profiles.DEFAULT_PROFILE:
return i18n("profile.default");
case Profiles.HOME_PROFILE:
return i18n("profile.home");
default:
return profile.getName();
}
return switch (profile.getName()) {
case Profiles.DEFAULT_PROFILE -> i18n("profile.default");
case Profiles.HOME_PROFILE -> i18n("profile.home");
default -> profile.getName();
};
}
private static final ObservableList<Profile> profiles = observableArrayList(profile -> new Observable[] { profile });
private static final ReadOnlyListWrapper<Profile> profilesWrapper = new ReadOnlyListWrapper<>(profiles);
private static ObjectProperty<Profile> selectedProfile = new SimpleObjectProperty<Profile>() {
private static final ObjectProperty<Profile> selectedProfile = new SimpleObjectProperty<Profile>() {
{
profiles.addListener(onInvalidating(this::invalidated));
}
@@ -104,8 +101,8 @@ public final class Profiles {
private static void checkProfiles() {
if (profiles.isEmpty()) {
Profile current = new Profile(Profiles.DEFAULT_PROFILE, new File(".minecraft"), new VersionSetting(), null, true);
Profile home = new Profile(Profiles.HOME_PROFILE, Metadata.MINECRAFT_DIRECTORY.toFile());
Profile current = new Profile(Profiles.DEFAULT_PROFILE, Path.of(".minecraft"), new VersionSetting(), null, true);
Profile home = new Profile(Profiles.HOME_PROFILE, Metadata.MINECRAFT_DIRECTORY);
Platform.runLater(() -> profiles.addAll(current, home));
}
}
@@ -17,100 +17,169 @@
*/
package org.jackhuang.hmcl.setting;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.value.ObservableObjectValue;
import javafx.beans.InvalidationListener;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.net.Authenticator;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.Proxy.Type;
import java.io.IOException;
import java.net.*;
import java.util.List;
import java.util.Objects;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ProxyManager {
private ProxyManager() {
private static final SimpleProxySelector NO_PROXY = new SimpleProxySelector(Proxy.NO_PROXY);
private static final ProxySelector SYSTEM_DEFAULT;
static {
ProxySelector systemProxySelector = ProxySelector.getDefault();
SYSTEM_DEFAULT = systemProxySelector != null
? new ProxySelectorWrapper(systemProxySelector)
: NO_PROXY;
}
private static ObjectBinding<Proxy> proxyProperty;
private static volatile @NotNull ProxySelector defaultProxySelector = SYSTEM_DEFAULT;
private static volatile @Nullable SimpleAuthenticator defaultAuthenticator = null;
public static Proxy getProxy() {
return proxyProperty.get();
private static ProxySelector getProxySelector() {
if (config().hasProxy()) {
Proxy.Type proxyType = config().getProxyType();
String host = config().getProxyHost();
int port = config().getProxyPort();
if (proxyType == Proxy.Type.DIRECT || StringUtils.isBlank(host)) {
return NO_PROXY;
} else if (port < 0 || port > 0xFFFF) {
LOG.warning("Illegal proxy port: " + port);
return NO_PROXY;
} else {
return new ProxySelectorWrapper(new SimpleProxySelector(new Proxy(proxyType, new InetSocketAddress(host, port))));
}
} else {
return ProxyManager.SYSTEM_DEFAULT;
}
}
public static ObservableObjectValue<Proxy> proxyProperty() {
return proxyProperty;
private static SimpleAuthenticator getAuthenticator() {
if (config().hasProxy() && config().hasProxyAuth()) {
String username = config().getProxyUser();
String password = config().getProxyPass();
if (username != null || password != null)
return new SimpleAuthenticator(
Objects.requireNonNullElse(username, ""),
Objects.requireNonNullElse(password, "").toCharArray()
);
else
return null;
} else
return null;
}
static void init() {
proxyProperty = Bindings.createObjectBinding(
() -> {
String host = config().getProxyHost();
int port = config().getProxyPort();
if (!config().hasProxy() || StringUtils.isBlank(host) || config().getProxyType() == Proxy.Type.DIRECT) {
return Proxy.NO_PROXY;
} else {
if (port < 0 || port > 0xFFFF) {
LOG.warning("Illegal proxy port: " + port);
return Proxy.NO_PROXY;
}
return new Proxy(config().getProxyType(), new InetSocketAddress(host, port));
}
},
config().proxyTypeProperty(),
config().proxyHostProperty(),
config().proxyPortProperty(),
config().hasProxyProperty());
proxyProperty.addListener(any -> updateSystemProxy());
updateSystemProxy();
ProxySelector.setDefault(new ProxySelector() {
@Override
public List<Proxy> select(URI uri) {
return defaultProxySelector.select(uri);
}
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
defaultProxySelector.connectFailed(uri, sa, ioe);
}
});
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
if (config().hasProxyAuth()) {
String username = config().getProxyUser();
String password = config().getProxyPass();
if (username != null && password != null) {
return new PasswordAuthentication(username, password.toCharArray());
}
}
return null;
var defaultAuthenticator = ProxyManager.defaultAuthenticator;
return defaultAuthenticator != null ? defaultAuthenticator.getPasswordAuthentication() : null;
}
});
defaultProxySelector = getProxySelector();
InvalidationListener updateProxySelector = observable -> defaultProxySelector = getProxySelector();
config().proxyTypeProperty().addListener(updateProxySelector);
config().proxyHostProperty().addListener(updateProxySelector);
config().proxyPortProperty().addListener(updateProxySelector);
config().hasProxyProperty().addListener(updateProxySelector);
defaultAuthenticator = getAuthenticator();
InvalidationListener updateAuthenticator = observable -> defaultAuthenticator = getAuthenticator();
config().hasProxyProperty().addListener(updateAuthenticator);
config().hasProxyAuthProperty().addListener(updateAuthenticator);
config().proxyUserProperty().addListener(updateAuthenticator);
config().proxyPassProperty().addListener(updateAuthenticator);
}
private static void updateSystemProxy() {
Proxy proxy = proxyProperty.get();
if (proxy.type() == Proxy.Type.DIRECT) {
System.clearProperty("http.proxyHost");
System.clearProperty("http.proxyPort");
System.clearProperty("https.proxyHost");
System.clearProperty("https.proxyPort");
System.clearProperty("socksProxyHost");
System.clearProperty("socksProxyPort");
} else {
InetSocketAddress address = (InetSocketAddress) proxy.address();
String host = address.getHostString();
String port = String.valueOf(address.getPort());
if (proxy.type() == Type.HTTP) {
System.clearProperty("socksProxyHost");
System.clearProperty("socksProxyPort");
System.setProperty("http.proxyHost", host);
System.setProperty("http.proxyPort", port);
System.setProperty("https.proxyHost", host);
System.setProperty("https.proxyPort", port);
} else if (proxy.type() == Type.SOCKS) {
System.clearProperty("http.proxyHost");
System.clearProperty("http.proxyPort");
System.clearProperty("https.proxyHost");
System.clearProperty("https.proxyPort");
System.setProperty("socksProxyHost", host);
System.setProperty("socksProxyPort", port);
private static abstract class AbstractProxySelector extends ProxySelector {
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
if (uri == null || sa == null || ioe == null) {
throw new IllegalArgumentException("Arguments can't be null.");
}
}
}
private static final class SimpleProxySelector extends AbstractProxySelector {
private final List<Proxy> proxies;
SimpleProxySelector(Proxy proxy) {
this.proxies = List.of(proxy);
}
@Override
public List<Proxy> select(URI uri) {
if (uri == null)
throw new IllegalArgumentException("URI can't be null.");
return proxies;
}
@Override
public String toString() {
return "SimpleProxySelector" + proxies;
}
}
/// Wraps another ProxySelector to avoid using proxy for loopback addresses.
private static final class ProxySelectorWrapper extends AbstractProxySelector {
private final ProxySelector source;
ProxySelectorWrapper(ProxySelector source) {
this.source = source;
}
@Override
public List<Proxy> select(URI uri) {
if (uri == null)
throw new IllegalArgumentException("URI can't be null.");
if (NetworkUtils.isLoopbackAddress(uri))
return NO_PROXY.proxies;
return source.select(uri);
}
}
private static final class SimpleAuthenticator extends Authenticator {
private final String username;
private final char[] password;
private SimpleAuthenticator(String username, char[] password) {
this.username = username;
this.password = password;
}
@Override
public PasswordAuthentication getPasswordAuthentication() {
return getRequestorType() == RequestorType.PROXY ? new PasswordAuthentication(username, password) : null;
}
}
private ProxyManager() {
}
}
@@ -0,0 +1,196 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import org.glavo.monetfx.Brightness;
import org.glavo.monetfx.ColorRole;
import org.glavo.monetfx.ColorScheme;
import org.jackhuang.hmcl.theme.Theme;
import org.jackhuang.hmcl.theme.ThemeColor;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.FXUtils;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Base64;
import java.util.Locale;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class StyleSheets {
private static final int FONT_STYLE_SHEET_INDEX = 0;
private static final int THEME_STYLE_SHEET_INDEX = 1;
private static final int BRIGHTNESS_SHEET_INDEX = 2;
private static final ObservableList<String> stylesheets;
static {
String[] array = new String[]{
getFontStyleSheet(),
getThemeStyleSheet(),
getBrightnessStyleSheet(),
"/assets/css/root.css"
};
stylesheets = FXCollections.observableList(Arrays.asList(array));
FontManager.fontProperty().addListener(o -> stylesheets.set(FONT_STYLE_SHEET_INDEX, getFontStyleSheet()));
Themes.colorSchemeProperty().addListener(o -> {
stylesheets.set(THEME_STYLE_SHEET_INDEX, getThemeStyleSheet());
stylesheets.set(BRIGHTNESS_SHEET_INDEX, getBrightnessStyleSheet());
});
}
private static String toStyleSheetUri(String styleSheet, String fallback) {
if (FXUtils.JAVAFX_MAJOR_VERSION >= 17)
// JavaFX 17+ support loading stylesheets from data URIs
// https://bugs.openjdk.org/browse/JDK-8267554
return "data:text/css;charset=UTF-8;base64," + Base64.getEncoder().encodeToString(styleSheet.getBytes(StandardCharsets.UTF_8));
else
try {
Path temp = Files.createTempFile("hmcl", ".css");
// For JavaFX 17 or earlier, CssParser uses the default charset
// https://bugs.openjdk.org/browse/JDK-8279328
Files.writeString(temp, styleSheet, Charset.defaultCharset());
temp.toFile().deleteOnExit();
return temp.toUri().toString();
} catch (IOException | NullPointerException e) {
LOG.error("Unable to create stylesheet, fallback to " + fallback, e);
return fallback;
}
}
private static String getFontStyleSheet() {
final String defaultCss = "/assets/css/font.css";
final FontManager.FontReference font = FontManager.getFont();
if (font == null || "System".equals(font.getFamily()))
return defaultCss;
String fontFamily = font.getFamily();
String style = font.getStyle();
String weight = null;
String posture = null;
if (style != null) {
style = style.toLowerCase(Locale.ROOT);
if (style.contains("thin"))
weight = "100";
else if (style.contains("extralight") || style.contains("extra light") || style.contains("ultralight") | style.contains("ultra light"))
weight = "200";
else if (style.contains("medium"))
weight = "500";
else if (style.contains("semibold") || style.contains("semi bold") || style.contains("demibold") || style.contains("demi bold"))
weight = "600";
else if (style.contains("extrabold") || style.contains("extra bold") || style.contains("ultrabold") || style.contains("ultra bold"))
weight = "800";
else if (style.contains("black") || style.contains("heavy"))
weight = "900";
else if (style.contains("light"))
weight = "lighter";
else if (style.contains("bold"))
weight = "bold";
posture = style.contains("italic") || style.contains("oblique") ? "italic" : null;
}
StringBuilder builder = new StringBuilder();
builder.append(".root {");
builder.append("-fx-font-family:\"").append(fontFamily).append("\";");
if (weight != null)
builder.append("-fx-font-weight:").append(weight).append(";");
if (posture != null)
builder.append("-fx-font-style:").append(posture).append(";");
builder.append('}');
return toStyleSheetUri(builder.toString(), defaultCss);
}
private static String getBrightnessStyleSheet() {
return Themes.getColorScheme().getBrightness() == Brightness.LIGHT
? "/assets/css/brightness-light.css"
: "/assets/css/brightness-dark.css";
}
private static void addColor(StringBuilder builder, String name, Color color) {
builder.append(" ").append(name)
.append(": ").append(ThemeColor.getColorDisplayName(color)).append(";\n");
}
private static void addColor(StringBuilder builder, String name, Color color, double opacity) {
builder.append(" ").append(name)
.append(": ").append(ThemeColor.getColorDisplayNameWithOpacity(color, opacity)).append(";\n");
}
private static void addColor(StringBuilder builder, ColorScheme scheme, ColorRole role, double opacity) {
builder.append(" ").append(role.getVariableName()).append("-transparent-%02d".formatted((int) (100 * opacity)))
.append(": ").append(ThemeColor.getColorDisplayNameWithOpacity(scheme.getColor(role), opacity))
.append(";\n");
}
private static String getThemeStyleSheet() {
final String blueCss = "/assets/css/blue.css";
if (Theme.DEFAULT.equals(Themes.getTheme()))
return blueCss;
ColorScheme scheme = Themes.getColorScheme();
StringBuilder builder = new StringBuilder();
builder.append("* {\n");
for (ColorRole colorRole : ColorRole.ALL) {
addColor(builder, colorRole.getVariableName(), scheme.getColor(colorRole));
}
addColor(builder, "-monet-primary-seed", scheme.getPrimaryColorSeed());
addColor(builder, scheme, ColorRole.PRIMARY, 0.5);
addColor(builder, scheme, ColorRole.SECONDARY_CONTAINER, 0.5);
addColor(builder, scheme, ColorRole.SURFACE, 0.5);
addColor(builder, scheme, ColorRole.SURFACE, 0.8);
addColor(builder, scheme, ColorRole.ON_SURFACE_VARIANT, 0.38);
addColor(builder, scheme, ColorRole.SURFACE_CONTAINER_LOW, 0.8);
addColor(builder, scheme, ColorRole.SECONDARY_CONTAINER, 0.8);
addColor(builder, scheme, ColorRole.INVERSE_SURFACE, 0.8);
builder.append("}\n");
return toStyleSheetUri(builder.toString(), blueCss);
}
public static void init(Scene scene) {
Bindings.bindContent(scene.getStylesheets(), stylesheets);
}
private StyleSheets() {
}
}
@@ -1,264 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.charset.Charset;
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 static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@JsonAdapter(Theme.TypeAdapter.class)
public class Theme {
public static final Theme BLUE = new Theme("blue", "#5C6BC0");
public static final Color BLACK = Color.web("#292929");
public static final Color[] SUGGESTED_COLORS = new Color[]{
Color.web("#3D6DA3"), // blue
Color.web("#283593"), // dark blue
Color.web("#43A047"), // green
Color.web("#E67E22"), // orange
Color.web("#9C27B0"), // purple
Color.web("#B71C1C") // red
};
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private static Optional<Font> font;
private static Optional<Font> tryLoadFont() {
//noinspection OptionalAssignedToNull
if (font != null) {
return font;
}
Path path = Paths.get("font.ttf");
if (!Files.isRegularFile(path)) {
path = Paths.get("font.otf");
}
if (Files.isRegularFile(path)) {
try {
return font = Optional.ofNullable(Font.loadFont(path.toAbsolutePath().toUri().toURL().toExternalForm(), 0));
} catch (MalformedURLException ignored) {
}
}
return font = Optional.empty();
}
public static Theme getTheme() {
Theme theme = config().getTheme();
return theme == null ? BLUE : theme;
}
private final Color paint;
private final String color;
private final String name;
Theme(String name, String color) {
this.name = name;
this.color = Objects.requireNonNull(color);
this.paint = Color.web(color);
}
public String getName() {
return name;
}
public String getColor() {
return color;
}
public boolean isCustom() {
return name.startsWith("#");
}
public boolean isLight() {
return paint.grayscale().getRed() >= 0.5;
}
public Color getForegroundColor() {
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";
String fontFamily = overrideFontFamily == null
? System.getProperty("hmcl.font.override", System.getenv("HMCL_FONT"))
: overrideFontFamily;
String fontStyle = null;
if (fontFamily == null) {
Optional<Font> font = tryLoadFont();
if (font.isPresent()) {
fontFamily = font.get().getFamily();
fontStyle = font.get().getStyle();
}
}
if (fontFamily != null || !this.color.equalsIgnoreCase(BLUE.color)) {
Color textFill = getForegroundColor();
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"};
}
public static Theme custom(String color) {
if (!color.startsWith("#"))
throw new IllegalArgumentException();
return new Theme(color, color);
}
public static Optional<Theme> getTheme(String name) {
if (name == null)
return Optional.empty();
else if (name.startsWith("#"))
try {
Color.web(name);
return Optional.of(custom(name));
} catch (IllegalArgumentException ignore) {
}
else {
String color = null;
switch (name.toLowerCase(Locale.ROOT)) {
case "blue":
return Optional.of(BLUE);
case "darker_blue":
color = "#283593";
break;
case "green":
color = "#43A047";
break;
case "orange":
color = "#E67E22";
break;
case "purple":
color = "#9C27B0";
break;
case "red":
color = "#F44336";
}
if (color != null)
return Optional.of(new Theme(name, color));
}
return Optional.empty();
}
public static String getColorDisplayName(Color c) {
return c != null ? String.format("#%02X%02X%02X", Math.round(c.getRed() * 255.0D), Math.round(c.getGreen() * 255.0D), Math.round(c.getBlue() * 255.0D)) : null;
}
private static ObjectBinding<Color> FOREGROUND_FILL;
public static ObjectBinding<Color> foregroundFillBinding() {
if (FOREGROUND_FILL == null)
FOREGROUND_FILL = Bindings.createObjectBinding(
() -> Theme.getTheme().getForegroundColor(),
config().themeProperty()
);
return FOREGROUND_FILL;
}
public static Color blackFill() {
return BLACK;
}
public static Color whiteFill() {
return Color.WHITE;
}
public static class TypeAdapter extends com.google.gson.TypeAdapter<Theme> {
@Override
public void write(JsonWriter out, Theme value) throws IOException {
out.value(value.getName().toLowerCase(Locale.ROOT));
}
@Override
public Theme read(JsonReader in) throws IOException {
return getTheme(in.nextString()).orElse(Theme.BLUE);
}
}
}
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.setting;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.mod.ModLoaderType;
import org.jackhuang.hmcl.ui.FXUtils;
public enum VersionIconType {
@@ -33,10 +34,24 @@ public enum VersionIconType {
FORGE("/assets/img/forge.png"),
NEO_FORGE("/assets/img/neoforge.png"),
FURNACE("/assets/img/furnace.png"),
QUILT("/assets/img/quilt.png");
QUILT("/assets/img/quilt.png"),
APRIL_FOOLS("/assets/img/april_fools.png"),
CLEANROOM("/assets/img/cleanroom.png");
// Please append new items at last
public static VersionIconType getIconType(ModLoaderType modLoaderType) {
return switch (modLoaderType) {
case FORGE -> VersionIconType.FORGE;
case NEO_FORGED -> VersionIconType.NEO_FORGE;
case FABRIC -> VersionIconType.FABRIC;
case QUILT -> VersionIconType.QUILT;
case LITE_LOADER -> VersionIconType.CHICKEN;
case CLEANROOM -> VersionIconType.CLEANROOM;
default -> VersionIconType.COMMAND;
};
}
private final String resourceUrl;
VersionIconType(String resourceUrl) {
@@ -20,13 +20,16 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.*;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemInfo;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.IOException;
@@ -36,22 +39,28 @@ import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author huangyuhui
*/
@JsonAdapter(VersionSetting.Serializer.class)
public final class VersionSetting implements Cloneable {
public final class VersionSetting implements Cloneable, Observable {
private boolean global = false;
private static final int SUGGESTED_MEMORY;
public boolean isGlobal() {
return global;
static {
double totalMemoryMB = MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize());
SUGGESTED_MEMORY = totalMemoryMB >= 32768
? 8192
: Integer.max((int) (Math.round(totalMemoryMB / 4.0 / 128.0) * 128), 256);
}
public void setGlobal(boolean global) {
this.global = global;
private final transient ObservableHelper helper = new ObservableHelper(this);
public VersionSetting() {
PropertyUtils.attachListener(this, helper);
}
private final BooleanProperty usesGlobalProperty = new SimpleBooleanProperty(this, "usesGlobal", true);
@@ -220,7 +229,7 @@ public final class VersionSetting implements Cloneable {
permSizeProperty.set(permSize);
}
private final IntegerProperty maxMemoryProperty = new SimpleIntegerProperty(this, "maxMemory", OperatingSystem.SUGGESTED_MEMORY);
private final IntegerProperty maxMemoryProperty = new SimpleIntegerProperty(this, "maxMemory", SUGGESTED_MEMORY);
public IntegerProperty maxMemoryProperty() {
return maxMemoryProperty;
@@ -371,6 +380,20 @@ public final class VersionSetting implements Cloneable {
noJVMArgsProperty.set(noJVMArgs);
}
private final BooleanProperty noOptimizingJVMArgsProperty = new SimpleBooleanProperty(this, "noOptimizingJVMArgs", false);
public BooleanProperty noOptimizingJVMArgsProperty() {
return noOptimizingJVMArgsProperty;
}
public boolean isNoOptimizingJVMArgs() {
return noOptimizingJVMArgsProperty.get();
}
public void setNoOptimizingJVMArgs(boolean noOptimizingJVMArgs) {
noOptimizingJVMArgsProperty.set(noOptimizingJVMArgs);
}
private final BooleanProperty notCheckJVMProperty = new SimpleBooleanProperty(this, "notCheckJVM", false);
public BooleanProperty notCheckJVMProperty() {
@@ -436,6 +459,20 @@ public final class VersionSetting implements Cloneable {
showLogsProperty.set(showLogs);
}
private final BooleanProperty enableDebugLogOutputProperty = new SimpleBooleanProperty(this, "enableDebugLogOutput", false);
public BooleanProperty enableDebugLogOutputProperty() {
return enableDebugLogOutputProperty;
}
public boolean isEnableDebugLogOutput() {
return enableDebugLogOutputProperty.get();
}
public void setEnableDebugLogOutput(boolean u) {
this.enableDebugLogOutputProperty.set(u);
}
// Minecraft settings.
private final StringProperty serverIpProperty = new SimpleStringProperty(this, "serverIp", "");
@@ -708,79 +745,21 @@ public final class VersionSetting implements Cloneable {
}
}
public void addPropertyChangedListener(InvalidationListener listener) {
usesGlobalProperty.addListener(listener);
javaVersionProperty.addListener(listener);
javaDirProperty.addListener(listener);
wrapperProperty.addListener(listener);
permSizeProperty.addListener(listener);
maxMemoryProperty.addListener(listener);
minMemoryProperty.addListener(listener);
autoMemory.addListener(listener);
preLaunchCommandProperty.addListener(listener);
postExitCommand.addListener(listener);
javaArgsProperty.addListener(listener);
minecraftArgsProperty.addListener(listener);
environmentVariablesProperty.addListener(listener);
noJVMArgsProperty.addListener(listener);
notCheckGameProperty.addListener(listener);
notCheckJVMProperty.addListener(listener);
notPatchNativesProperty.addListener(listener);
showLogsProperty.addListener(listener);
serverIpProperty.addListener(listener);
fullscreenProperty.addListener(listener);
widthProperty.addListener(listener);
heightProperty.addListener(listener);
gameDirTypeProperty.addListener(listener);
gameDirProperty.addListener(listener);
processPriorityProperty.addListener(listener);
rendererProperty.addListener(listener);
useNativeGLFW.addListener(listener);
useNativeOpenAL.addListener(listener);
launcherVisibilityProperty.addListener(listener);
defaultJavaPathProperty.addListener(listener);
nativesDirProperty.addListener(listener);
nativesDirTypeProperty.addListener(listener);
versionIcon.addListener(listener);
@Override
public void addListener(InvalidationListener listener) {
helper.addListener(listener);
}
@Override
public void removeListener(InvalidationListener listener) {
helper.removeListener(listener);
}
@Override
public VersionSetting clone() {
VersionSetting versionSetting = new VersionSetting();
versionSetting.setUsesGlobal(isUsesGlobal());
versionSetting.setJavaVersionType(getJavaVersionType());
versionSetting.setJavaVersion(getJavaVersion());
versionSetting.setDefaultJavaPath(getDefaultJavaPath());
versionSetting.setJavaDir(getJavaDir());
versionSetting.setWrapper(getWrapper());
versionSetting.setPermSize(getPermSize());
versionSetting.setMaxMemory(getMaxMemory());
versionSetting.setMinMemory(getMinMemory());
versionSetting.setAutoMemory(isAutoMemory());
versionSetting.setPreLaunchCommand(getPreLaunchCommand());
versionSetting.setPostExitCommand(getPostExitCommand());
versionSetting.setJavaArgs(getJavaArgs());
versionSetting.setMinecraftArgs(getMinecraftArgs());
versionSetting.setEnvironmentVariables(getEnvironmentVariables());
versionSetting.setNoJVMArgs(isNoJVMArgs());
versionSetting.setNotCheckGame(isNotCheckGame());
versionSetting.setNotCheckJVM(isNotCheckJVM());
versionSetting.setNotPatchNatives(isNotPatchNatives());
versionSetting.setShowLogs(isShowLogs());
versionSetting.setServerIp(getServerIp());
versionSetting.setFullscreen(isFullscreen());
versionSetting.setWidth(getWidth());
versionSetting.setHeight(getHeight());
versionSetting.setGameDirType(getGameDirType());
versionSetting.setGameDir(getGameDir());
versionSetting.setProcessPriority(getProcessPriority());
versionSetting.setRenderer(getRenderer());
versionSetting.setUseNativeGLFW(isUseNativeGLFW());
versionSetting.setUseNativeOpenAL(isUseNativeOpenAL());
versionSetting.setLauncherVisibility(getLauncherVisibility());
versionSetting.setNativesDir(getNativesDir());
versionSetting.setVersionIcon(getVersionIcon());
return versionSetting;
VersionSetting cloned = new VersionSetting();
PropertyUtils.copyProperties(this, cloned);
return cloned;
}
public static class Serializer implements JsonSerializer<VersionSetting>, JsonDeserializer<VersionSetting> {
@@ -793,7 +772,7 @@ public final class VersionSetting implements Cloneable {
obj.addProperty("javaArgs", src.getJavaArgs());
obj.addProperty("minecraftArgs", src.getMinecraftArgs());
obj.addProperty("environmentVariables", src.getEnvironmentVariables());
obj.addProperty("maxMemory", src.getMaxMemory() <= 0 ? OperatingSystem.SUGGESTED_MEMORY : src.getMaxMemory());
obj.addProperty("maxMemory", src.getMaxMemory() <= 0 ? SUGGESTED_MEMORY : src.getMaxMemory());
obj.addProperty("minMemory", src.getMinMemory());
obj.addProperty("autoMemory", src.isAutoMemory());
obj.addProperty("permSize", src.getPermSize());
@@ -806,10 +785,12 @@ public final class VersionSetting implements Cloneable {
obj.addProperty("wrapper", src.getWrapper());
obj.addProperty("fullscreen", src.isFullscreen());
obj.addProperty("noJVMArgs", src.isNoJVMArgs());
obj.addProperty("noOptimizingJVMArgs", src.isNoOptimizingJVMArgs());
obj.addProperty("notCheckGame", src.isNotCheckGame());
obj.addProperty("notCheckJVM", src.isNotCheckJVM());
obj.addProperty("notPatchNatives", src.isNotPatchNatives());
obj.addProperty("showLogs", src.isShowLogs());
obj.addProperty("enableDebugLogOutput", src.isEnableDebugLogOutput());
obj.addProperty("gameDir", src.getGameDir());
obj.addProperty("launcherVisibility", src.getLauncherVisibility().ordinal());
obj.addProperty("processPriority", src.getProcessPriority().ordinal());
@@ -846,22 +827,14 @@ public final class VersionSetting implements Cloneable {
return obj;
}
private static <T> T getOrDefault(T[] values, JsonElement index, T defaultValue) {
if (index == null)
return defaultValue;
int idx = index.getAsInt();
return idx >= 0 && idx < values.length ? values[idx] : defaultValue;
}
@Override
public VersionSetting deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (!(json instanceof JsonObject))
return null;
JsonObject obj = (JsonObject) json;
int maxMemoryN = parseJsonPrimitive(Optional.ofNullable(obj.get("maxMemory")).map(JsonElement::getAsJsonPrimitive).orElse(null), OperatingSystem.SUGGESTED_MEMORY);
if (maxMemoryN <= 0) maxMemoryN = OperatingSystem.SUGGESTED_MEMORY;
int maxMemoryN = parseJsonPrimitive(Optional.ofNullable(obj.get("maxMemory")).map(JsonElement::getAsJsonPrimitive).orElse(null), SUGGESTED_MEMORY);
if (maxMemoryN <= 0) maxMemoryN = SUGGESTED_MEMORY;
VersionSetting vs = new VersionSetting();
@@ -884,18 +857,20 @@ public final class VersionSetting implements Cloneable {
vs.setNativesDir(Optional.ofNullable(obj.get("nativesDir")).map(JsonElement::getAsString).orElse(""));
vs.setFullscreen(Optional.ofNullable(obj.get("fullscreen")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNoJVMArgs(Optional.ofNullable(obj.get("noJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNoOptimizingJVMArgs(Optional.ofNullable(obj.get("noOptimizingJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotCheckGame(Optional.ofNullable(obj.get("notCheckGame")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotCheckJVM(Optional.ofNullable(obj.get("notCheckJVM")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotPatchNatives(Optional.ofNullable(obj.get("notPatchNatives")).map(JsonElement::getAsBoolean).orElse(false));
vs.setShowLogs(Optional.ofNullable(obj.get("showLogs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setLauncherVisibility(getOrDefault(LauncherVisibility.values(), obj.get("launcherVisibility"), LauncherVisibility.HIDE));
vs.setProcessPriority(getOrDefault(ProcessPriority.values(), obj.get("processPriority"), ProcessPriority.NORMAL));
vs.setEnableDebugLogOutput(Optional.ofNullable(obj.get("enableDebugLogOutput")).map(JsonElement::getAsBoolean).orElse(false));
vs.setLauncherVisibility(parseJsonPrimitive(obj.getAsJsonPrimitive("launcherVisibility"), LauncherVisibility.class, LauncherVisibility.HIDE));
vs.setProcessPriority(parseJsonPrimitive(obj.getAsJsonPrimitive("processPriority"), ProcessPriority.class, ProcessPriority.NORMAL));
vs.setUseNativeGLFW(Optional.ofNullable(obj.get("useNativeGLFW")).map(JsonElement::getAsBoolean).orElse(false));
vs.setUseNativeOpenAL(Optional.ofNullable(obj.get("useNativeOpenAL")).map(JsonElement::getAsBoolean).orElse(false));
vs.setGameDirType(getOrDefault(GameDirectoryType.values(), obj.get("gameDirType"), GameDirectoryType.ROOT_FOLDER));
vs.setGameDirType(parseJsonPrimitive(obj.getAsJsonPrimitive("gameDirType"), GameDirectoryType.class, GameDirectoryType.ROOT_FOLDER));
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
vs.setNativesDirType(getOrDefault(NativesDirectoryType.values(), obj.get("nativesDirType"), NativesDirectoryType.VERSION_FOLDER));
vs.setVersionIcon(getOrDefault(VersionIconType.values(), obj.get("versionIcon"), VersionIconType.DEFAULT));
vs.setNativesDirType(parseJsonPrimitive(obj.getAsJsonPrimitive("nativesDirType"), NativesDirectoryType.class, NativesDirectoryType.VERSION_FOLDER));
vs.setVersionIcon(parseJsonPrimitive(obj.getAsJsonPrimitive("versionIcon"), VersionIconType.class, VersionIconType.DEFAULT));
if (obj.get("javaVersionType") != null) {
JavaVersionType javaVersionType = parseJsonPrimitive(obj.getAsJsonPrimitive("javaVersionType"), JavaVersionType.class, JavaVersionType.AUTO);
@@ -0,0 +1,320 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.terracotta;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.DownloadException;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.terracotta.provider.ITerracottaProvider;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.InvocationDispatcher;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jackhuang.hmcl.util.tree.TarFileTree;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class TerracottaManager {
private TerracottaManager() {
}
private static final AtomicReference<TerracottaState> STATE_V = new AtomicReference<>(TerracottaState.Bootstrap.INSTANCE);
private static final ReadOnlyObjectWrapper<TerracottaState> STATE = new ReadOnlyObjectWrapper<>(STATE_V.getPlain());
private static final InvocationDispatcher<TerracottaState> STATE_D = InvocationDispatcher.runOn(Platform::runLater, STATE::set);
static {
Schedulers.io().execute(() -> {
try {
if (TerracottaMetadata.PROVIDER == null)
throw new IOException("Unsupported platform: " + org.jackhuang.hmcl.util.platform.Platform.CURRENT_PLATFORM);
switch (TerracottaMetadata.PROVIDER.status()) {
case NOT_EXIST -> setState(new TerracottaState.Uninitialized(false));
case LEGACY_VERSION -> setState(new TerracottaState.Uninitialized(true));
case READY -> launch(setState(new TerracottaState.Launching()));
}
} catch (Exception e) {
compareAndSet(TerracottaState.Bootstrap.INSTANCE, new TerracottaState.Fatal(TerracottaState.Fatal.Type.UNKNOWN));
}
});
}
public static ReadOnlyObjectProperty<TerracottaState> stateProperty() {
return STATE.getReadOnlyProperty();
}
static {
Lang.thread(() -> {
while (true) {
TerracottaState state = STATE_V.get();
if (!(state instanceof TerracottaState.PortSpecific portSpecific)) {
LockSupport.parkNanos(500_000);
continue;
}
int port = portSpecific.port;
int index = state instanceof TerracottaState.Ready ready ? ready.index : Integer.MIN_VALUE;
TerracottaState next;
try {
next = new GetTask(URI.create(String.format("http://127.0.0.1:%d/state", port)))
.setSignificance(Task.TaskSignificance.MINOR)
.thenApplyAsync(jsonString -> {
TerracottaState.Ready object = JsonUtils.fromNonNullJson(jsonString, TypeToken.get(TerracottaState.Ready.class));
if (object.index <= index) {
return null;
}
object.port = port;
return object;
})
.setSignificance(Task.TaskSignificance.MINOR)
.run();
} catch (Exception e) {
LOG.warning("Cannot fetch state from Terracotta.", e);
next = new TerracottaState.Fatal(TerracottaState.Fatal.Type.TERRACOTTA);
}
if (next != null) {
compareAndSet(state, next);
}
LockSupport.parkNanos(500_000);
}
}, "Terracotta Background Daemon", true);
}
public static boolean validate(Path file) {
return FileUtils.getName(file).equalsIgnoreCase(TerracottaMetadata.PACKAGE_NAME);
}
public static TerracottaState.Preparing install(@Nullable Path file) {
FXUtils.checkFxUserThread();
TerracottaState state = STATE_V.get();
if (!(state instanceof TerracottaState.Uninitialized ||
state instanceof TerracottaState.Preparing preparing && preparing.hasInstallFence() ||
state instanceof TerracottaState.Fatal && ((TerracottaState.Fatal) state).isRecoverable())
) {
return null;
}
if (file != null && !FileUtils.getName(file).equalsIgnoreCase(TerracottaMetadata.PACKAGE_NAME)) {
return null;
}
TerracottaState.Preparing preparing = new TerracottaState.Preparing(new ReadOnlyDoubleWrapper(-1));
Task.supplyAsync(Schedulers.io(), () -> {
return file != null ? TarFileTree.open(file) : null;
}).thenComposeAsync(Schedulers.javafx(), tree -> {
return getProvider().install(preparing, tree).whenComplete(exception -> {
if (tree != null) {
tree.close();
}
if (exception != null) {
throw exception;
}
});
}).whenComplete(exception -> {
if (exception == null) {
try {
TerracottaMetadata.removeLegacyVersionFiles();
} catch (IOException e) {
LOG.warning("Unable to remove legacy terracotta files.", e);
}
TerracottaState.Launching launching = new TerracottaState.Launching();
if (compareAndSet(preparing, launching)) {
launch(launching);
}
} else if (exception instanceof CancellationException) {
} else if (exception instanceof ITerracottaProvider.ArchiveFileMissingException) {
LOG.warning("Cannot install terracotta from local package.", exception);
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.INSTALL));
} else if (exception instanceof DownloadException) {
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.NETWORK));
} else {
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.INSTALL));
}
}).start();
return compareAndSet(state, preparing) ? preparing : null;
}
private static ITerracottaProvider getProvider() {
ITerracottaProvider provider = TerracottaMetadata.PROVIDER;
if (provider == null) {
throw new AssertionError("Terracotta Provider must NOT be null.");
}
return provider;
}
public static TerracottaState recover(@Nullable Path file) {
FXUtils.checkFxUserThread();
TerracottaState state = STATE_V.get();
if (!(state instanceof TerracottaState.Fatal && ((TerracottaState.Fatal) state).isRecoverable())) {
return null;
}
try {
return switch (getProvider().status()) {
case NOT_EXIST, LEGACY_VERSION -> install(file);
case READY -> {
TerracottaState.Launching launching = setState(new TerracottaState.Launching());
launch(launching);
yield launching;
}
};
} catch (NullPointerException | IOException e) {
LOG.warning("Cannot determine Terracotta state.", e);
return setState(new TerracottaState.Fatal(TerracottaState.Fatal.Type.UNKNOWN));
}
}
private static void launch(TerracottaState.Launching state) {
Task.supplyAsync(() -> {
Path path = Files.createTempDirectory(String.format("hmcl-terracotta-%d", ThreadLocalRandom.current().nextLong())).resolve("http").toAbsolutePath();
ManagedProcess process = new ManagedProcess(new ProcessBuilder(getProvider().ofCommandLine(path)));
process.pumpInputStream(SystemUtils::onLogLine);
process.pumpErrorStream(SystemUtils::onLogLine);
long exitTime = -1;
while (true) {
if (Files.exists(path)) {
JsonObject object = JsonUtils.fromNonNullJson(Files.readString(path), JsonObject.class);
return object.get("port").getAsInt();
}
if (!process.isRunning()) {
if (exitTime == -1) {
exitTime = System.currentTimeMillis();
} else if (System.currentTimeMillis() - exitTime >= 10000) {
throw new IllegalStateException("Process has exited for 10s.");
}
}
}
}).whenComplete(Schedulers.javafx(), (port, exception) -> {
TerracottaState next;
if (exception == null) {
next = new TerracottaState.Unknown(port);
} else {
next = new TerracottaState.Fatal(TerracottaState.Fatal.Type.TERRACOTTA);
}
compareAndSet(state, next);
}).start();
}
public static Task<String> exportLogs() {
if (STATE_V.get() instanceof TerracottaState.PortSpecific portSpecific) {
return new GetTask(URI.create(String.format("http://127.0.0.1:%d/log?fetch=true", portSpecific.port)))
.setSignificance(Task.TaskSignificance.MINOR);
}
return Task.completed(null);
}
public static TerracottaState.Waiting setWaiting() {
TerracottaState state = STATE_V.get();
if (state instanceof TerracottaState.PortSpecific portSpecific) {
new GetTask(URI.create(String.format("http://127.0.0.1:%d/state/ide", portSpecific.port)))
.setSignificance(Task.TaskSignificance.MINOR)
.start();
return new TerracottaState.Waiting(-1, -1, null);
}
return null;
}
private static String getPlayerName() {
Account account = Accounts.getSelectedAccount();
return account != null ? account.getCharacter() : i18n("terracotta.player_anonymous");
}
public static TerracottaState.HostScanning setScanning() {
TerracottaState state = STATE_V.get();
if (state instanceof TerracottaState.PortSpecific portSpecific) {
new GetTask(NetworkUtils.toURI(String.format(
"http://127.0.0.1:%d/state/scanning?player=%s", portSpecific.port, getPlayerName()))
).setSignificance(Task.TaskSignificance.MINOR).start();
return new TerracottaState.HostScanning(-1, -1, null);
}
return null;
}
public static Task<TerracottaState.GuestStarting> setGuesting(String room) {
TerracottaState state = STATE_V.get();
if (state instanceof TerracottaState.PortSpecific portSpecific) {
return new GetTask(NetworkUtils.toURI(String.format(
"http://127.0.0.1:%d/state/guesting?room=%s&player=%s", portSpecific.port, room, getPlayerName()
)))
.setSignificance(Task.TaskSignificance.MINOR)
.thenSupplyAsync(() -> new TerracottaState.GuestStarting(-1, -1, null))
.setSignificance(Task.TaskSignificance.MINOR);
} else {
return null;
}
}
private static <T extends TerracottaState> T setState(T value) {
if (value == null) {
throw new AssertionError();
}
STATE_V.set(value);
STATE_D.accept(value);
return value;
}
private static boolean compareAndSet(TerracottaState previous, TerracottaState next) {
if (next == null) {
throw new AssertionError();
}
if (STATE_V.compareAndSet(previous, next)) {
STATE_D.accept(next);
return true;
} else {
return false;
}
}
}
@@ -0,0 +1,205 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.terracotta;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.terracotta.provider.GeneralProvider;
import org.jackhuang.hmcl.terracotta.provider.ITerracottaProvider;
import org.jackhuang.hmcl.terracotta.provider.MacOSProvider;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
import org.jackhuang.hmcl.util.i18n.LocalizedText;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OSVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class TerracottaMetadata {
private TerracottaMetadata() {
}
public record Link(@SerializedName("desc") LocalizedText description, String link) {
}
private record Config(
@SerializedName("version_legacy") String legacy,
@SerializedName("version_recent") List<String> recent,
@SerializedName("version_latest") String latest,
@SerializedName("classifiers") Map<String, String> classifiers,
@SerializedName("downloads") List<String> downloads,
@SerializedName("downloads_CN") List<String> downloadsCN,
@SerializedName("links") List<Link> links
) {
private @Nullable TerracottaNative of(String classifier) {
String hash = this.classifiers.get(classifier);
if (hash == null)
return null;
if (!hash.startsWith("sha256:"))
throw new IllegalArgumentException(String.format("Invalid hash value %s for classifier %s.", hash, classifier));
hash = hash.substring("sha256:".length());
List<URI> links = new ArrayList<>(this.downloads.size() + this.downloadsCN.size());
for (String download : LocaleUtils.IS_CHINA_MAINLAND
? Lang.merge(this.downloadsCN, this.downloads)
: Lang.merge(this.downloads, this.downloadsCN)) {
links.add(URI.create(download.replace("${version}", this.latest).replace("${classifier}", classifier)));
}
return new TerracottaNative(
Collections.unmodifiableList(links),
Metadata.DEPENDENCIES_DIRECTORY.resolve(
String.format("terracotta/%s/terracotta-%s-%s", this.latest, this.latest, classifier)
).toAbsolutePath(),
new FileDownloadTask.IntegrityCheck("SHA-256", hash)
);
}
}
public static final ITerracottaProvider PROVIDER;
public static final String PACKAGE_NAME;
public static final List<Link> PACKAGE_LINKS;
public static final String FEEDBACK_LINK = NetworkUtils.withQuery("https://docs.hmcl.net/multiplayer/feedback.html", Map.of(
"v", "v1",
"launcher_version", Metadata.VERSION
));
private static final Pattern LEGACY;
private static final List<String> RECENT;
private static final String LATEST;
static {
Config config;
try (InputStream is = TerracottaMetadata.class.getResourceAsStream("/assets/terracotta.json")) {
config = JsonUtils.fromNonNullJsonFully(is, Config.class);
} catch (IOException e) {
throw new ExceptionInInitializerError(e);
}
LEGACY = Pattern.compile(config.legacy);
RECENT = config.recent;
LATEST = config.latest;
ProviderContext context = locateProvider(config);
PROVIDER = context != null ? context.provider() : null;
PACKAGE_NAME = context != null ? String.format("terracotta-%s-%s-pkg.tar.gz", config.latest, context.branch) : null;
if (context != null) {
List<Link> packageLinks = new ArrayList<>(config.links.size());
for (Link link : config.links) {
packageLinks.add(new Link(
link.description,
link.link.replace("${version}", LATEST)
.replace("${classifier}", context.branch)
));
}
Collections.shuffle(packageLinks);
PACKAGE_LINKS = Collections.unmodifiableList(packageLinks);
} else {
PACKAGE_LINKS = null;
}
}
private record ProviderContext(ITerracottaProvider provider, String branch) {
ProviderContext(ITerracottaProvider provider, String system, String arch) {
this(provider, system + "-" + arch);
}
}
@Nullable
private static ProviderContext locateProvider(Config config) {
String arch = Architecture.SYSTEM_ARCH.getCheckedName();
return switch (OperatingSystem.CURRENT_OS) {
case WINDOWS -> {
if (!OperatingSystem.SYSTEM_VERSION.isAtLeast(OSVersion.WINDOWS_10))
yield null;
TerracottaNative target = config.of("windows-%s.exe".formatted(arch));
yield target != null
? new ProviderContext(new GeneralProvider(target), "windows", arch)
: null;
}
case LINUX -> {
TerracottaNative target = config.of("linux-%s".formatted(arch));
yield target != null
? new ProviderContext(new GeneralProvider(target), "linux", arch)
: null;
}
case MACOS -> {
TerracottaNative installer = config.of("macos-%s.pkg".formatted(arch));
TerracottaNative binary = config.of("macos-%s".formatted(arch));
yield installer != null && binary != null
? new ProviderContext(new MacOSProvider(installer, binary), "macos", arch)
: null;
}
default -> null;
};
}
public static void removeLegacyVersionFiles() throws IOException {
try (DirectoryStream<Path> terracotta = Files.newDirectoryStream(Metadata.DEPENDENCIES_DIRECTORY.resolve("terracotta").toAbsolutePath())) {
for (Path path : terracotta) {
String name = FileUtils.getName(path);
if (LATEST.equals(name) || RECENT.contains(name) || !LEGACY.matcher(name).matches()) {
continue;
}
try {
FileUtils.deleteDirectory(path);
} catch (IOException e) {
LOG.warning(String.format("Unable to remove legacy terracotta files: %s", path), e);
}
}
}
}
public static boolean hasLegacyVersionFiles() throws IOException {
try (DirectoryStream<Path> terracotta = Files.newDirectoryStream(Metadata.DEPENDENCIES_DIRECTORY.resolve("terracotta").toAbsolutePath())) {
for (Path path : terracotta) {
String name = FileUtils.getName(path);
if (!LATEST.equals(name) && (RECENT.contains(name) || LEGACY.matcher(name).matches())) {
return true;
}
}
}
return false;
}
}
@@ -0,0 +1,147 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.terracotta;
import kala.compress.archivers.tar.TarArchiveEntry;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.terracotta.provider.ITerracottaProvider;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.logging.Logger;
import org.jackhuang.hmcl.util.tree.TarFileTree;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.HexFormat;
import java.util.List;
import java.util.concurrent.CancellationException;
public final class TerracottaNative {
private final List<URI> links;
private final FileDownloadTask.IntegrityCheck checking;
private final Path path;
public TerracottaNative(List<URI> links, Path path, FileDownloadTask.IntegrityCheck checking) {
this.links = links;
this.path = path;
this.checking = checking;
}
public Path getPath() {
return path;
}
public Task<?> install(ITerracottaProvider.Context context, @Nullable TarFileTree tree) {
if (tree == null) {
return new FileDownloadTask(links, path, checking) {
@Override
protected Context getContext(HttpResponse<?> response, boolean checkETag, String bmclapiHash) throws IOException {
Context delegate = super.getContext(response, checkETag, bmclapiHash);
return new Context() {
@Override
public void withResult(boolean success) {
delegate.withResult(success);
}
@Override
public void write(byte[] buffer, int offset, int len) throws IOException {
if (!context.hasInstallFence()) {
throw new CancellationException("User has installed terracotta from local archives.");
}
delegate.write(buffer, offset, len);
}
@Override
public void close() throws IOException {
if (isSuccess() && !context.requestInstallFence()) {
throw new CancellationException();
}
delegate.close();
}
};
}
};
}
return Task.runAsync(() -> {
String name = FileUtils.getName(path);
TarArchiveEntry entry = tree.getRoot().getFiles().get(name);
if (entry == null) {
throw new ITerracottaProvider.ArchiveFileMissingException("Cannot exact entry: " + name);
}
if (!context.requestInstallFence()) {
throw new CancellationException();
}
Files.createDirectories(path.toAbsolutePath().getParent());
MessageDigest digest = DigestUtils.getDigest(checking.getAlgorithm());
try (
InputStream stream = tree.getInputStream(entry);
OutputStream os = Files.newOutputStream(path)
) {
stream.transferTo(new OutputStream() {
@Override
public void write(int b) throws IOException {
os.write(b);
digest.update((byte) b);
}
@Override
public void write(byte @NotNull [] buffer, int offset, int len) throws IOException {
os.write(buffer, offset, len);
digest.update(buffer, offset, len);
}
});
}
String checksum = HexFormat.of().formatHex(digest.digest());
if (!checksum.equalsIgnoreCase(checking.getChecksum())) {
Files.delete(path);
throw new ITerracottaProvider.ArchiveFileMissingException("Incorrect checksum (" + checking.getAlgorithm() + "), expected: " + checking.getChecksum() + ", actual: " + checksum);
}
});
}
public ITerracottaProvider.Status status() throws IOException {
if (Files.exists(path)) {
if (DigestUtils.digestToString(checking.getAlgorithm(), path).equalsIgnoreCase(checking.getChecksum())) {
return ITerracottaProvider.Status.READY;
}
}
try {
if (TerracottaMetadata.hasLegacyVersionFiles()) {
return ITerracottaProvider.Status.LEGACY_VERSION;
}
} catch (IOException e) {
Logger.LOG.warning("Cannot determine whether legacy versions exist.");
}
return ITerracottaProvider.Status.NOT_EXIST;
}
}
@@ -0,0 +1,304 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.terracotta;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.value.ObservableValue;
import org.jackhuang.hmcl.terracotta.profile.TerracottaProfile;
import org.jackhuang.hmcl.terracotta.provider.ITerracottaProvider;
import org.jackhuang.hmcl.util.gson.JsonSubtype;
import org.jackhuang.hmcl.util.gson.JsonType;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public abstract sealed class TerracottaState {
protected TerracottaState() {
}
public boolean isUIFakeState() {
return false;
}
public boolean isForkOf(TerracottaState state) {
return false;
}
public static final class Bootstrap extends TerracottaState {
static final Bootstrap INSTANCE = new Bootstrap();
private Bootstrap() {
}
}
public static final class Uninitialized extends TerracottaState {
private final boolean hasLegacy;
Uninitialized(boolean hasLegacy) {
this.hasLegacy = hasLegacy;
}
public boolean hasLegacy() {
return hasLegacy;
}
}
public static final class Preparing extends TerracottaState implements ITerracottaProvider.Context {
private final ReadOnlyDoubleWrapper progress;
private final AtomicBoolean installFence = new AtomicBoolean(false);
Preparing(ReadOnlyDoubleWrapper progress) {
this.progress = progress;
}
public ReadOnlyDoubleProperty progressProperty() {
return progress.getReadOnlyProperty();
}
@Override
public void bindProgress(ObservableValue<? extends Number> value) {
progress.bind(value);
}
@Override
public boolean requestInstallFence() {
return installFence.compareAndSet(false, true);
}
@Override
public boolean hasInstallFence() {
return !installFence.get();
}
}
public static final class Launching extends TerracottaState {
Launching() {
}
}
static abstract sealed class PortSpecific extends TerracottaState {
transient int port;
protected PortSpecific(int port) {
this.port = port;
}
}
@JsonType(
property = "state",
subtypes = {
@JsonSubtype(clazz = Waiting.class, name = "waiting"),
@JsonSubtype(clazz = HostScanning.class, name = "host-scanning"),
@JsonSubtype(clazz = HostStarting.class, name = "host-starting"),
@JsonSubtype(clazz = HostOK.class, name = "host-ok"),
@JsonSubtype(clazz = GuestStarting.class, name = "guest-connecting"),
@JsonSubtype(clazz = GuestStarting.class, name = "guest-starting"),
@JsonSubtype(clazz = GuestOK.class, name = "guest-ok"),
@JsonSubtype(clazz = Exception.class, name = "exception"),
}
)
static abstract sealed class Ready extends PortSpecific {
@SerializedName("index")
final int index;
@SerializedName("state")
private final String state;
Ready(int port, int index, String state) {
super(port);
this.index = index;
this.state = state;
}
@Override
public boolean isUIFakeState() {
return this.index == -1;
}
}
public static final class Unknown extends PortSpecific {
Unknown(int port) {
super(port);
}
}
public static final class Waiting extends Ready {
Waiting(int port, int index, String state) {
super(port, index, state);
}
}
public static final class HostScanning extends Ready {
HostScanning(int port, int index, String state) {
super(port, index, state);
}
}
public static final class HostStarting extends Ready {
HostStarting(int port, int index, String state) {
super(port, index, state);
}
}
public static final class HostOK extends Ready implements Validation {
@SerializedName("room")
private final String code;
@SerializedName("profile_index")
private final int profileIndex;
@SerializedName("profiles")
private final List<TerracottaProfile> profiles;
HostOK(int port, int index, String state, String code, int profileIndex, List<TerracottaProfile> profiles) {
super(port, index, state);
this.code = code;
this.profileIndex = profileIndex;
this.profiles = profiles;
}
@Override
public void validate() throws JsonParseException, TolerableValidationException {
if (code == null) {
throw new JsonParseException("code is null");
}
if (profiles == null) {
throw new JsonParseException("profiles is null");
}
}
public String getCode() {
return code;
}
public List<TerracottaProfile> getProfiles() {
return profiles;
}
@Override
public boolean isForkOf(TerracottaState state) {
return state instanceof HostOK hostOK && this.index - hostOK.index <= profileIndex;
}
}
public static final class GuestStarting extends Ready {
GuestStarting(int port, int index, String state) {
super(port, index, state);
}
}
public static final class GuestOK extends Ready implements Validation {
@SerializedName("url")
private final String url;
@SerializedName("profile_index")
private final int profileIndex;
@SerializedName("profiles")
private final List<TerracottaProfile> profiles;
GuestOK(int port, int index, String state, String url, int profileIndex, List<TerracottaProfile> profiles) {
super(port, index, state);
this.url = url;
this.profileIndex = profileIndex;
this.profiles = profiles;
}
@Override
public void validate() throws JsonParseException, TolerableValidationException {
if (profiles == null) {
throw new JsonParseException("profiles is null");
}
}
public String getUrl() {
return url;
}
public List<TerracottaProfile> getProfiles() {
return profiles;
}
@Override
public boolean isForkOf(TerracottaState state) {
return state instanceof GuestOK guestOK && this.index - guestOK.index <= profileIndex;
}
}
public static final class Exception extends Ready implements Validation {
public enum Type {
PING_HOST_FAIL,
PING_HOST_RST,
GUEST_ET_CRASH,
HOST_ET_CRASH,
PING_SERVER_RST,
SCAFFOLDING_INVALID_RESPONSE
}
private static final TerracottaState.Exception.Type[] LOOKUP = Type.values();
@SerializedName("type")
private final int type;
Exception(int port, int index, String state, int type) {
super(port, index, state);
this.type = type;
}
@Override
public void validate() throws JsonParseException, TolerableValidationException {
if (type < 0 || type >= LOOKUP.length) {
throw new JsonParseException(String.format("Type must between [0, %s)", LOOKUP.length));
}
}
public Type getType() {
return LOOKUP[type];
}
}
public static final class Fatal extends TerracottaState {
public enum Type {
OS,
NETWORK,
INSTALL,
TERRACOTTA,
UNKNOWN;
}
private final Type type;
public Fatal(Type type) {
this.type = type;
}
public Type getType() {
return type;
}
public boolean isRecoverable() {
return this.type != Type.UNKNOWN;
}
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,18 +15,15 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.animation;
package org.jackhuang.hmcl.terracotta.profile;
import javafx.scene.Node;
import javafx.scene.layout.Pane;
import javafx.util.Duration;
import com.google.gson.annotations.SerializedName;
public interface AnimationHandler {
Duration getDuration();
Pane getCurrentRoot();
Node getPreviousNode();
Node getCurrentNode();
public enum ProfileKind {
@SerializedName("HOST")
HOST,
@SerializedName("LOCAL")
LOCAL,
@SerializedName("GUEST")
GUEST
}
@@ -0,0 +1,57 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.terracotta.profile;
import com.google.gson.annotations.SerializedName;
public final class TerracottaProfile {
@SerializedName("machine_id")
private final String machineID;
@SerializedName("name")
private final String name;
@SerializedName("vendor")
private final String vendor;
@SerializedName("kind")
private final ProfileKind type;
private TerracottaProfile(String machineID, String name, String vendor, ProfileKind type) {
this.machineID = machineID;
this.name = name;
this.vendor = vendor;
this.type = type;
}
public String getMachineID() {
return machineID;
}
public String getName() {
return name;
}
public String getVendor() {
return vendor;
}
public ProfileKind getType() {
return type;
}
}
@@ -0,0 +1,67 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.terracotta.provider;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.terracotta.TerracottaNative;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.tree.TarFileTree;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.util.List;
import java.util.Set;
public final class GeneralProvider implements ITerracottaProvider {
private final TerracottaNative target;
public GeneralProvider(TerracottaNative target) {
this.target = target;
}
@Override
public Status status() throws IOException {
return target.status();
}
@Override
public Task<?> install(Context context, @Nullable TarFileTree tree) throws IOException {
Task<?> task = target.install(context, tree);
context.bindProgress(task.progressProperty());
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
task = task.thenRunAsync(() -> Files.setPosixFilePermissions(target.getPath(), Set.of(
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE,
PosixFilePermission.OWNER_EXECUTE,
PosixFilePermission.GROUP_READ,
PosixFilePermission.GROUP_EXECUTE,
PosixFilePermission.OTHERS_READ,
PosixFilePermission.OTHERS_EXECUTE
)));
}
return task;
}
@Override
public List<String> ofCommandLine(Path path) {
return List.of(target.getPath().toString(), "--hmcl", path.toString());
}
}
@@ -0,0 +1,77 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.terracotta.provider;
import javafx.beans.value.ObservableValue;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.tree.TarFileTree;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
public interface ITerracottaProvider {
enum Status {
NOT_EXIST,
LEGACY_VERSION,
READY
}
interface Context {
void bindProgress(ObservableValue<? extends Number> value);
boolean requestInstallFence();
boolean hasInstallFence();
}
abstract class ProviderException extends IOException {
public ProviderException(String message) {
super(message);
}
public ProviderException(String message, Throwable cause) {
super(message, cause);
}
public ProviderException(Throwable cause) {
super(cause);
}
}
final class ArchiveFileMissingException extends ProviderException {
public ArchiveFileMissingException(String message) {
super(message);
}
public ArchiveFileMissingException(String message, Throwable cause) {
super(message, cause);
}
public ArchiveFileMissingException(Throwable cause) {
super(cause);
}
}
Status status() throws IOException;
Task<?> install(Context context, @Nullable TarFileTree tree) throws IOException;
List<String> ofCommandLine(Path path);
}
@@ -0,0 +1,121 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.terracotta.provider;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.terracotta.TerracottaNative;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jackhuang.hmcl.util.tree.TarFileTree;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.PosixFilePermission;
import java.util.List;
import java.util.Set;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class MacOSProvider implements ITerracottaProvider {
public final TerracottaNative installer, binary;
public MacOSProvider(TerracottaNative installer, TerracottaNative binary) {
this.installer = installer;
this.binary = binary;
}
@Override
public Status status() throws IOException {
assert binary != null;
if (!Files.exists(Path.of("/Applications/terracotta.app"))) {
return Status.NOT_EXIST;
}
return binary.status();
}
@Override
public Task<?> install(Context context, @Nullable TarFileTree tree) throws IOException {
assert installer != null && binary != null;
Task<?> installerTask = installer.install(context, tree);
Task<?> binaryTask = binary.install(context, tree);
context.bindProgress(installerTask.progressProperty().add(binaryTask.progressProperty()).multiply(0.4)); // (1 + 1) * 0.4 = 0.8
return Task.allOf(
installerTask.thenComposeAsync(() -> {
Path osascript = SystemUtils.which("osascript");
if (osascript == null) {
throw new IllegalStateException("Cannot locate 'osascript' system executable on MacOS for installing Terracotta.");
}
Path pkg = Files.createTempDirectory(Metadata.HMCL_GLOBAL_DIRECTORY, "terracotta-pkg")
.toRealPath()
.resolve(FileUtils.getName(installer.getPath()));
Files.copy(installer.getPath(), pkg, StandardCopyOption.REPLACE_EXISTING);
ManagedProcess process = new ManagedProcess(new ProcessBuilder(
osascript.toString(), "-e", String.format(
"do shell script \"installer -pkg '%s' -target /\" with prompt \"%s\" with administrator privileges",
pkg, i18n("terracotta.sudo_installing")
)));
process.pumpInputStream(SystemUtils::onLogLine);
process.pumpErrorStream(SystemUtils::onLogLine);
return Task.fromCompletableFuture(process.getProcess().onExit()).thenRunAsync(() -> {
try {
FileUtils.cleanDirectory(pkg.getParent());
} catch (IOException e) {
LOG.warning("Cannot remove temporary Terracotta package file.", e);
}
if (process.getExitCode() != 0) {
throw new IllegalStateException(String.format(
"Cannot install Terracotta %s: system installer exited with code %d",
pkg,
process.getExitCode()
));
}
});
}),
binaryTask.thenRunAsync(() -> Files.setPosixFilePermissions(binary.getPath(), Set.of(
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE,
PosixFilePermission.OWNER_EXECUTE,
PosixFilePermission.GROUP_READ,
PosixFilePermission.GROUP_EXECUTE,
PosixFilePermission.OTHERS_READ,
PosixFilePermission.OTHERS_EXECUTE
)))
);
}
@Override
public List<String> ofCommandLine(Path path) {
assert binary != null;
return List.of(binary.getPath().toString(), "--hmcl", path.toString());
}
}
@@ -0,0 +1,94 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.theme;
import org.glavo.monetfx.*;
import java.util.*;
/// @author Glavo
public final class Theme {
public static final Theme DEFAULT = new Theme(ThemeColor.DEFAULT, Brightness.DEFAULT, ColorStyle.FIDELITY, Contrast.DEFAULT);
private final ThemeColor primaryColorSeed;
private final Brightness brightness;
private final ColorStyle colorStyle;
private final Contrast contrast;
public Theme(ThemeColor primaryColorSeed,
Brightness brightness,
ColorStyle colorStyle,
Contrast contrast
) {
this.primaryColorSeed = primaryColorSeed;
this.brightness = brightness;
this.colorStyle = colorStyle;
this.contrast = contrast;
}
public ColorScheme toColorScheme() {
return ColorScheme.newBuilder()
.setPrimaryColorSeed(primaryColorSeed.color())
.setColorStyle(colorStyle)
.setBrightness(brightness)
.setSpecVersion(ColorSpecVersion.SPEC_2025)
.setContrast(contrast)
.build();
}
public ThemeColor primaryColorSeed() {
return primaryColorSeed;
}
public Brightness brightness() {
return brightness;
}
public ColorStyle colorStyle() {
return colorStyle;
}
public Contrast contrast() {
return contrast;
}
@Override
public boolean equals(Object obj) {
return obj == this || obj instanceof Theme that
&& this.primaryColorSeed.color().equals(that.primaryColorSeed.color())
&& this.brightness.equals(that.brightness)
&& this.colorStyle.equals(that.colorStyle)
&& this.contrast.equals(that.contrast);
}
@Override
public int hashCode() {
return Objects.hash(primaryColorSeed, brightness, colorStyle, contrast);
}
@Override
public String toString() {
return "Theme[" +
"primaryColorSeed=" + primaryColorSeed + ", " +
"brightness=" + brightness + ", " +
"colorStyle=" + colorStyle + ", " +
"contrast=" + contrast + ']';
}
}
@@ -0,0 +1,193 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.theme;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakListener;
import javafx.beans.property.Property;
import javafx.scene.control.ColorPicker;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.List;
import java.util.Objects;
/// @author Glavo
@JsonAdapter(ThemeColor.TypeAdapter.class)
@JsonSerializable
public record ThemeColor(@NotNull String name, @NotNull Color color) {
public static final ThemeColor DEFAULT = new ThemeColor("blue", Color.web("#5C6BC0"));
public static final List<ThemeColor> STANDARD_COLORS = List.of(
DEFAULT,
new ThemeColor("darker_blue", Color.web("#283593")),
new ThemeColor("green", Color.web("#43A047")),
new ThemeColor("orange", Color.web("#E67E22")),
new ThemeColor("purple", Color.web("#9C27B0")),
new ThemeColor("red", Color.web("#B71C1C"))
);
public static String getColorDisplayName(Color c) {
return c != null ? String.format("#%02X%02X%02X",
Math.round(c.getRed() * 255.0D),
Math.round(c.getGreen() * 255.0D),
Math.round(c.getBlue() * 255.0D))
: null;
}
public static String getColorDisplayNameWithOpacity(Color c, double opacity) {
return c != null ? String.format("#%02X%02X%02X%02X",
Math.round(c.getRed() * 255.0D),
Math.round(c.getGreen() * 255.0D),
Math.round(c.getBlue() * 255.0D),
Math.round(opacity * 255.0))
: null;
}
public static @Nullable ThemeColor of(String name) {
if (name == null)
return null;
if (!name.startsWith("#")) {
for (ThemeColor color : STANDARD_COLORS) {
if (name.equalsIgnoreCase(color.name()))
return color;
}
}
try {
return new ThemeColor(name, Color.web(name));
} catch (IllegalArgumentException e) {
return null;
}
}
@Contract("null -> null; !null -> !null")
public static ThemeColor of(Color color) {
return color != null ? new ThemeColor(getColorDisplayName(color), color) : null;
}
private static final class BidirectionalBinding implements InvalidationListener, WeakListener {
private final WeakReference<ColorPicker> colorPickerRef;
private final WeakReference<Property<ThemeColor>> propertyRef;
private final int hashCode;
private boolean updating = false;
private BidirectionalBinding(ColorPicker colorPicker, Property<ThemeColor> property) {
this.colorPickerRef = new WeakReference<>(colorPicker);
this.propertyRef = new WeakReference<>(property);
this.hashCode = System.identityHashCode(colorPicker) ^ System.identityHashCode(property);
}
@Override
public void invalidated(Observable sourceProperty) {
if (!updating) {
final ColorPicker colorPicker = colorPickerRef.get();
final Property<ThemeColor> property = propertyRef.get();
if (colorPicker == null || property == null) {
if (colorPicker != null) {
colorPicker.valueProperty().removeListener(this);
}
if (property != null) {
property.removeListener(this);
}
} else {
updating = true;
try {
if (property == sourceProperty) {
ThemeColor newValue = property.getValue();
colorPicker.setValue(newValue != null ? newValue.color() : null);
} else {
Color newValue = colorPicker.getValue();
property.setValue(newValue != null ? ThemeColor.of(newValue) : null);
}
} finally {
updating = false;
}
}
}
}
@Override
public boolean wasGarbageCollected() {
return colorPickerRef.get() == null || propertyRef.get() == null;
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof BidirectionalBinding that))
return false;
final ColorPicker colorPicker = this.colorPickerRef.get();
final Property<ThemeColor> property = this.propertyRef.get();
final ColorPicker thatColorPicker = that.colorPickerRef.get();
final Property<?> thatProperty = that.propertyRef.get();
if (colorPicker == null || property == null || thatColorPicker == null || thatProperty == null)
return false;
return colorPicker == thatColorPicker && property == thatProperty;
}
}
public static void bindBidirectional(ColorPicker colorPicker, Property<ThemeColor> property) {
var binding = new BidirectionalBinding(colorPicker, property);
colorPicker.valueProperty().removeListener(binding);
property.removeListener(binding);
ThemeColor themeColor = property.getValue();
colorPicker.setValue(themeColor != null ? themeColor.color() : null);
colorPicker.valueProperty().addListener(binding);
property.addListener(binding);
}
static final class TypeAdapter extends com.google.gson.TypeAdapter<ThemeColor> {
@Override
public void write(JsonWriter out, ThemeColor value) throws IOException {
out.value(value.name());
}
@Override
public ThemeColor read(JsonReader in) throws IOException {
return Objects.requireNonNullElse(of(in.nextString()), ThemeColor.DEFAULT);
}
}
}
@@ -0,0 +1,134 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.theme;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.ObjectExpression;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.paint.Color;
import org.glavo.monetfx.Brightness;
import org.glavo.monetfx.ColorScheme;
import org.glavo.monetfx.Contrast;
import org.glavo.monetfx.beans.property.ColorSchemeProperty;
import org.glavo.monetfx.beans.property.ReadOnlyColorSchemeProperty;
import org.glavo.monetfx.beans.property.SimpleColorSchemeProperty;
import org.jackhuang.hmcl.ui.FXUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
/// @author Glavo
public final class Themes {
private static final ObjectExpression<Theme> theme = new ObjectBinding<>() {
{
List<Observable> observables = new ArrayList<>();
observables.add(config().themeBrightnessProperty());
observables.add(config().themeColorProperty());
if (FXUtils.DARK_MODE != null) {
observables.add(FXUtils.DARK_MODE);
}
bind(observables.toArray(new Observable[0]));
}
private Brightness getBrightness() {
String themeBrightness = config().getThemeBrightness();
if (themeBrightness == null)
return Brightness.DEFAULT;
return switch (themeBrightness.toLowerCase(Locale.ROOT).trim()) {
case "auto" -> {
if (FXUtils.DARK_MODE != null) {
yield FXUtils.DARK_MODE.get() ? Brightness.DARK : Brightness.LIGHT;
} else {
yield Brightness.DEFAULT;
}
}
case "dark" -> Brightness.DARK;
case "light" -> Brightness.LIGHT;
default -> Brightness.DEFAULT;
};
}
@Override
protected Theme computeValue() {
ThemeColor themeColor = Objects.requireNonNullElse(config().getThemeColor(), ThemeColor.DEFAULT);
return new Theme(themeColor, getBrightness(), Theme.DEFAULT.colorStyle(), Contrast.DEFAULT);
}
};
private static final ColorSchemeProperty colorScheme = new SimpleColorSchemeProperty();
private static final BooleanBinding darkMode = Bindings.createBooleanBinding(
() -> colorScheme.get().getBrightness() == Brightness.DARK,
colorScheme
);
static {
ChangeListener<Theme> listener = (observable, oldValue, newValue) -> {
if (!Objects.equals(oldValue, newValue)) {
colorScheme.set(newValue != null ? newValue.toColorScheme() : Theme.DEFAULT.toColorScheme());
}
};
listener.changed(theme, null, theme.get());
theme.addListener(listener);
}
public static ObjectExpression<Theme> themeProperty() {
return theme;
}
public static Theme getTheme() {
return themeProperty().get();
}
public static ReadOnlyColorSchemeProperty colorSchemeProperty() {
return colorScheme;
}
public static ColorScheme getColorScheme() {
return colorScheme.get();
}
private static final ObjectBinding<Color> titleFill = Bindings.createObjectBinding(
() -> config().isTitleTransparent()
? getColorScheme().getOnSurface()
: getColorScheme().getOnPrimaryContainer(),
colorSchemeProperty(),
config().titleTransparentProperty()
);
public static ObservableValue<Color> titleFillProperty() {
return titleFill;
}
public static BooleanBinding darkModeProperty() {
return darkMode;
}
private Themes() {
}
}
@@ -1,20 +0,0 @@
package org.jackhuang.hmcl.ui;
import java.awt.*;
import java.lang.reflect.Method;
public final class AwtUtils {
private AwtUtils() {
}
public static void setAppleIcon(Image image) {
try {
Class<?> taskbarClass = Class.forName("java.awt.Taskbar");
Method getTaskBarMethod = taskbarClass.getDeclaredMethod("getTaskbar");
Object taskBar = getTaskBarMethod.invoke(null);
Method setIconImageMethod = taskbarClass.getDeclaredMethod("setIconImage", Image.class);
setIconImageMethod.invoke(taskBar, image);
} catch (Throwable ignore) {
}
}
}
@@ -19,6 +19,9 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.DoubleProperty;
@@ -34,14 +37,20 @@ import javafx.scene.paint.Color;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.account.AccountListPage;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.Motion;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.decorator.DecoratorController;
@@ -49,14 +58,17 @@ import org.jackhuang.hmcl.ui.download.DownloadPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.main.LauncherSettingsPage;
import org.jackhuang.hmcl.ui.main.RootPage;
import org.jackhuang.hmcl.ui.terracotta.TerracottaPage;
import org.jackhuang.hmcl.ui.versions.GameListPage;
import org.jackhuang.hmcl.ui.versions.VersionPage;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@@ -65,6 +77,10 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Controllers {
public static final String JAVA_VERSION_TIP = "javaVersion";
public static final String JAVA_INTERPRETED_MODE_TIP = "javaInterpretedMode";
public static final String SOFTWARE_RENDERING = "softwareRendering";
public static final int MIN_WIDTH = 800 + 2 + 16; // bg width + border width*2 + shadow width*2
public static final int MIN_HEIGHT = 450 + 2 + 40 + 16; // bg height + border width*2 + toolbar height + shadow width*2
public static final Screen SCREEN = Screen.getPrimary();
@@ -76,20 +92,20 @@ public final class Controllers {
private static Scene scene;
private static Stage stage;
private static Lazy<VersionPage> versionPage = new Lazy<>(VersionPage::new);
private static VersionPage versionPage;
private static Lazy<GameListPage> gameListPage = new Lazy<>(() -> {
GameListPage gameListPage = new GameListPage();
gameListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
gameListPage.profilesProperty().bindContent(Profiles.profilesProperty());
FXUtils.applyDragListener(gameListPage, ModpackHelper::isFileModpackByExtension, modpacks -> {
File modpack = modpacks.get(0);
Path modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
});
return gameListPage;
});
private static Lazy<RootPage> rootPage = new Lazy<>(RootPage::new);
private static DecoratorController decorator;
private static Lazy<DownloadPage> downloadPage = new Lazy<>(DownloadPage::new);
private static DownloadPage downloadPage;
private static Lazy<AccountListPage> accountListPage = new Lazy<>(() -> {
AccountListPage accountListPage = new AccountListPage();
accountListPage.selectedAccountProperty().bindBidirectional(Accounts.selectedAccountProperty());
@@ -97,7 +113,8 @@ public final class Controllers {
accountListPage.authServersProperty().bindContentBidirectional(config().getAuthlibInjectorServers());
return accountListPage;
});
private static Lazy<LauncherSettingsPage> settingsPage = new Lazy<>(LauncherSettingsPage::new);
private static LauncherSettingsPage settingsPage;
private static Lazy<TerracottaPage> terracottaPage = new Lazy<>(TerracottaPage::new);
private Controllers() {
}
@@ -112,7 +129,18 @@ public final class Controllers {
// FXThread
public static VersionPage getVersionPage() {
return versionPage.get();
if (versionPage == null) {
versionPage = new VersionPage();
}
return versionPage;
}
@FXThread
public static void prepareVersionPage() {
if (versionPage == null) {
LOG.info("Prepare the version page");
versionPage = FXUtils.prepareNode(new VersionPage());
}
}
// FXThread
@@ -127,7 +155,18 @@ public final class Controllers {
// FXThread
public static LauncherSettingsPage getSettingsPage() {
return settingsPage.get();
if (settingsPage == null) {
settingsPage = new LauncherSettingsPage();
}
return settingsPage;
}
@FXThread
public static void prepareSettingsPage() {
if (settingsPage == null) {
LOG.info("Prepare the settings page");
settingsPage = FXUtils.prepareNode(new LauncherSettingsPage());
}
}
// FXThread
@@ -137,7 +176,23 @@ public final class Controllers {
// FXThread
public static DownloadPage getDownloadPage() {
return downloadPage.get();
if (downloadPage == null) {
downloadPage = new DownloadPage();
}
return downloadPage;
}
@FXThread
public static void prepareDownloadPage() {
if (downloadPage == null) {
LOG.info("Prepare the download page");
downloadPage = FXUtils.prepareNode(new DownloadPage());
}
}
// FXThread
public static Node getTerracottaPage() {
return terracottaPage.get();
}
// FXThread
@@ -168,6 +223,18 @@ public final class Controllers {
public static void initialize(Stage stage) {
LOG.info("Start initializing application");
if (System.getProperty("prism.lcdtext") == null) {
String fontAntiAliasing = globalConfig().getFontAntiAliasing();
if ("lcd".equalsIgnoreCase(fontAntiAliasing)) {
LOG.info("Enable sub-pixel antialiasing");
System.getProperties().put("prism.lcdtext", "true");
} else if ("gray".equalsIgnoreCase(fontAntiAliasing)
|| OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && SCREEN.getOutputScaleX() > 1) {
LOG.info("Disable sub-pixel antialiasing");
System.getProperties().put("prism.lcdtext", "false");
}
}
Controllers.stage = stage;
stageSizeChangeListener = o -> {
@@ -197,7 +264,11 @@ public final class Controllers {
if (targetProperty != null
&& Controllers.stage != null
&& !Controllers.stage.isIconified()) {
&& !Controllers.stage.isIconified()
// https://github.com/HMCL-dev/HMCL/issues/4290
&& (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS ||
!Controllers.stage.isFullScreen() && !Controllers.stage.isMaximized())
) {
targetProperty.set(sourceProperty.get());
}
};
@@ -260,18 +331,36 @@ public final class Controllers {
stage.setMinHeight(MIN_HEIGHT);
decorator.getDecorator().prefWidthProperty().bind(scene.widthProperty());
decorator.getDecorator().prefHeightProperty().bind(scene.heightProperty());
scene.getStylesheets().setAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
StyleSheets.init(scene);
FXUtils.setIcon(stage);
stage.setTitle(Metadata.FULL_TITLE);
stage.initStyle(StageStyle.TRANSPARENT);
stage.setScene(scene);
if (AnimationUtils.playWindowAnimation()) {
Timeline timeline = new Timeline(
new KeyFrame(Duration.millis(0),
new KeyValue(decorator.getDecorator().opacityProperty(), 0, Motion.EASE),
new KeyValue(decorator.getDecorator().scaleXProperty(), 0.8, Motion.EASE),
new KeyValue(decorator.getDecorator().scaleYProperty(), 0.8, Motion.EASE),
new KeyValue(decorator.getDecorator().scaleZProperty(), 0.8, Motion.EASE)
),
new KeyFrame(Duration.millis(600),
new KeyValue(decorator.getDecorator().opacityProperty(), 1, Motion.EASE),
new KeyValue(decorator.getDecorator().scaleXProperty(), 1, Motion.EASE),
new KeyValue(decorator.getDecorator().scaleYProperty(), 1, Motion.EASE),
new KeyValue(decorator.getDecorator().scaleZProperty(), 1, Motion.EASE)
)
);
timeline.play();
}
if (!Architecture.SYSTEM_ARCH.isX86() && globalConfig().getPlatformPromptVersion() < 1) {
Runnable continueAction = () -> globalConfig().setPlatformPromptVersion(1);
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX && Architecture.SYSTEM_ARCH == Architecture.ARM64) {
Controllers.dialog(i18n("fatal.unsupported_platform.osx_arm64"), null, MessageType.INFO, continueAction);
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && Architecture.SYSTEM_ARCH == Architecture.ARM64) {
Controllers.dialog(i18n("fatal.unsupported_platform.macos_arm64"), null, MessageType.INFO, continueAction);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && Architecture.SYSTEM_ARCH == Architecture.ARM64) {
Controllers.dialog(i18n("fatal.unsupported_platform.windows_arm64"), null, MessageType.INFO, continueAction);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX &&
@@ -284,6 +373,44 @@ public final class Controllers {
}
}
if (JavaRuntime.CURRENT_VERSION < Metadata.MINIMUM_SUPPORTED_JAVA_VERSION) {
Number shownTipVersion = null;
try {
shownTipVersion = (Number) config().getShownTips().get(JAVA_VERSION_TIP);
} catch (ClassCastException e) {
LOG.warning("Invalid type for shown tips key: " + JAVA_VERSION_TIP, e);
}
if (shownTipVersion == null || shownTipVersion.intValue() < Metadata.MINIMUM_SUPPORTED_JAVA_VERSION) {
MessageDialogPane.Builder builder = new MessageDialogPane.Builder(i18n("fatal.deprecated_java_version"), null, MessageType.WARNING);
String downloadLink = Metadata.getSuggestedJavaDownloadLink();
if (downloadLink != null)
builder.addHyperLink(
i18n("fatal.deprecated_java_version.download_link", Metadata.RECOMMENDED_JAVA_VERSION),
downloadLink
);
Controllers.dialog(builder
.ok(() -> config().getShownTips().put(JAVA_VERSION_TIP, Metadata.MINIMUM_SUPPORTED_JAVA_VERSION))
.build());
}
}
// Check whether JIT is enabled in the current environment
if (!JavaRuntime.CURRENT_JIT_ENABLED && !Boolean.TRUE.equals(config().getShownTips().get(JAVA_INTERPRETED_MODE_TIP))) {
Controllers.dialog(new MessageDialogPane.Builder(i18n("warning.java_interpreted_mode"), i18n("message.warning"), MessageType.WARNING)
.ok(null)
.addCancel(i18n("button.do_not_show_again"), () ->
config().getShownTips().put(JAVA_INTERPRETED_MODE_TIP, true))
.build());
}
// Check whether hardware acceleration is enabled
if (!FXUtils.GPU_ACCELERATION_ENABLED && !Boolean.TRUE.equals(config().getShownTips().get(SOFTWARE_RENDERING))) {
Controllers.dialog(new MessageDialogPane.Builder(i18n("warning.software_rendering"), i18n("message.warning"), MessageType.WARNING)
.ok(null)
.addCancel(i18n("button.do_not_show_again"), () ->
config().getShownTips().put(SOFTWARE_RENDERING, true))
.build());
}
if (globalConfig().getAgreementVersion() < 1) {
JFXDialogLayout agreementPane = new JFXDialogLayout();
@@ -342,6 +469,38 @@ public final class Controllers {
dialog(new MessageDialogPane.Builder(text, title, type).actionOrCancel(actionButton, cancel).build());
}
public static void confirmWithCountdown(String text, String title, int seconds, MessageType messageType,
@Nullable Runnable ok, @Nullable Runnable cancel) {
if (seconds <= 0)
throw new IllegalArgumentException("Seconds must be greater than 0");
JFXButton btnOk = new JFXButton(i18n("button.ok"));
btnOk.getStyleClass().add(messageType == MessageType.WARNING || messageType == MessageType.ERROR
? "dialog-error"
: "dialog-accept");
if (ok != null)
btnOk.setOnAction(e -> ok.run());
btnOk.setDisable(true);
KeyFrame[] keyFrames = new KeyFrame[seconds + 1];
for (int i = 0; i < seconds; i++) {
keyFrames[i] = new KeyFrame(Duration.seconds(i),
new KeyValue(btnOk.textProperty(), i18n("button.ok.countdown", seconds - i)));
}
keyFrames[seconds] = new KeyFrame(Duration.seconds(seconds),
new KeyValue(btnOk.textProperty(), i18n("button.ok")),
new KeyValue(btnOk.disableProperty(), false));
Timeline timeline = new Timeline(keyFrames);
confirmAction(text, title, messageType, btnOk, () -> {
timeline.stop();
if (cancel != null)
cancel.run();
});
timeline.play();
}
public static CompletableFuture<String> prompt(String title, FutureCallback<String> onResult) {
return prompt(title, onResult, "");
}
@@ -368,16 +527,17 @@ public final class Controllers {
public static TaskExecutorDialogPane taskDialog(Task<?> task, String title, TaskCancellationAction onCancel) {
TaskExecutor executor = task.executor();
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(onCancel);
pane.setTitle(title);
pane.setExecutor(executor);
dialog(pane);
TaskExecutorDialogPane pane = taskDialog(executor, title, onCancel);
executor.start();
return pane;
}
public static void navigate(Node node) {
decorator.navigate(node);
decorator.navigate(node, ContainerAnimations.NAVIGATION, Motion.SHORT4, Motion.EASE);
}
public static void navigateForward(Node node) {
decorator.navigate(node, ContainerAnimations.FORWARD, Motion.SHORT4, Motion.EASE);
}
public static void showToast(String content) {
@@ -391,6 +551,10 @@ public final class Controllers {
Controllers.getSettingsPage().showFeedback();
Controllers.navigate(Controllers.getSettingsPage());
break;
case "hmcl://game/launch":
Profile profile = Profiles.getSelectedProfile();
Versions.launch(profile, profile.getSelectedVersion(), LauncherHelper::setKeep);
break;
}
} else {
FXUtils.openLink(href);
@@ -408,6 +572,7 @@ public final class Controllers {
downloadPage = null;
accountListPage = null;
settingsPage = null;
terracottaPage = null;
decorator = null;
stage = null;
scene = null;
File diff suppressed because it is too large. Load diff
@@ -37,20 +37,17 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.logging.Logger;
import org.jackhuang.hmcl.util.Pair;
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.CommandBuilder;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.logging.Logger;
import org.jackhuang.hmcl.util.platform.*;
import java.io.IOException;
import java.nio.file.Files;
@@ -64,10 +61,10 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class GameCrashWindow extends Stage {
private final Version version;
@@ -98,7 +95,7 @@ public class GameCrashWindow extends Stage {
memory = Optional.ofNullable(launchOptions.getMaxMemory()).map(i -> i + " MB").orElse("-");
total_memory = OperatingSystem.TOTAL_MEMORY + " MB";
total_memory = MEGABYTES.formatBytes(SystemInfo.getTotalMemorySize());
this.java = launchOptions.getJava().getArchitecture() == Architecture.SYSTEM_ARCH
? launchOptions.getJava().getVersion()
@@ -109,7 +106,7 @@ public class GameCrashWindow extends Stage {
this.feedbackTextFlow.getChildren().addAll(FXUtils.parseSegment(i18n("game.crash.feedback"), Controllers::onHyperlinkAction));
setScene(new Scene(view, 800, 480));
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
StyleSheets.init(getScene());
setTitle(i18n("game.crash.title"));
FXUtils.setIcon(this);
@@ -133,22 +130,22 @@ public class GameCrashWindow extends Stage {
crashReport = CrashReportAnalyzer.extractCrashReport(rawLog);
}
return pair(CrashReportAnalyzer.anaylze(rawLog), crashReport != null ? CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport) : new HashSet<>());
return pair(CrashReportAnalyzer.analyze(rawLog), crashReport != null ? CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport) : new HashSet<>());
}), Task.supplyAsync(() -> {
Path latestLog = repository.getRunDirectory(version.getId()).toPath().resolve("logs/latest.log");
Path latestLog = repository.getRunDirectory(version.getId()).resolve("logs/latest.log");
if (!Files.isReadable(latestLog)) {
return pair(new HashSet<CrashReportAnalyzer.Result>(), new HashSet<String>());
}
String log;
try {
log = FileUtils.readText(latestLog);
log = FileUtils.readTextMaybeNativeEncoding(latestLog);
} catch (IOException e) {
LOG.warning("Failed to read logs/latest.log", e);
return pair(new HashSet<CrashReportAnalyzer.Result>(), new HashSet<String>());
}
return pair(CrashReportAnalyzer.anaylze(log), CrashReportAnalyzer.findKeywordsFromCrashReport(log));
return pair(CrashReportAnalyzer.analyze(log), CrashReportAnalyzer.findKeywordsFromCrashReport(log));
})).whenComplete(Schedulers.javafx(), (taskResult, exception) -> {
loading.set(false);
@@ -271,7 +268,7 @@ public class GameCrashWindow extends Stage {
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(Log::getLog).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)))
logs.stream().map(Log::getLog).collect(Collectors.joining("\n")))
.thenComposeAsync(logs ->
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
.handleAsync((result, exception) -> {
@@ -289,13 +286,13 @@ public class GameCrashWindow extends Stage {
alert.showAndWait();
return null;
});
}, Schedulers.javafx());
}
private final class View extends VBox {
View() {
setStyle("-fx-background-color: white");
this.getStyleClass().add("game-crash-window");
HBox titlePane = new HBox();
{
@@ -385,7 +382,7 @@ public class GameCrashWindow extends Stage {
TwoLineListItem gameDir = new TwoLineListItem();
gameDir.getStyleClass().setAll("two-line-item-second-large");
gameDir.setTitle(i18n("game.directory"));
gameDir.setSubtitle(launchOptions.getGameDir().getAbsolutePath());
gameDir.setSubtitle(launchOptions.getGameDir().toAbsolutePath().toString());
FXUtils.installFastTooltip(gameDir, i18n("game.directory"));
TwoLineListItem javaDir = new TwoLineListItem();
@@ -398,9 +395,12 @@ public class GameCrashWindow extends Stage {
reasonTitle.getStyleClass().add("two-line-item-second-large-title");
ScrollPane reasonPane = new ScrollPane(reasonTextFlow);
reasonTextFlow.getStyleClass().add("crash-reason-text-flow");
reasonPane.setFitToWidth(true);
reasonPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
reasonPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
reasonPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
feedbackTextFlow.getStyleClass().add("crash-reason-text-flow");
gameDirPane.setPadding(new Insets(8));
VBox.setVgrow(gameDirPane, Priority.ALWAYS);
@@ -414,6 +414,7 @@ public class GameCrashWindow extends Stage {
}
HBox toolBar = new HBox();
VBox.setMargin(toolBar, new Insets(0, 0, 4, 0));
{
JFXButton exportGameCrashInfoButton = FXUtils.newRaisedButton(i18n("logwindow.export_game_crash_logs"));
exportGameCrashInfoButton.setOnAction(e -> exportGameCrashInfo());
@@ -422,10 +423,9 @@ public class GameCrashWindow extends Stage {
logButton.setOnAction(e -> showLogWindow());
JFXButton helpButton = FXUtils.newRaisedButton(i18n("help"));
helpButton.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/help.html"));
helpButton.setOnAction(e -> FXUtils.openLink(Metadata.CONTACT_URL));
FXUtils.installFastTooltip(helpButton, i18n("logwindow.help"));
toolBar.setPadding(new Insets(8));
toolBar.setSpacing(8);
toolBar.getStyleClass().add("jfx-tool-bar");
@@ -22,6 +22,7 @@ import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import org.jackhuang.hmcl.util.StringUtils;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
@@ -151,30 +152,27 @@ public final class HTMLRenderer {
children.add(textNode);
}
private void appendAutoLineBreak(String text) {
AutoLineBreak textNode = new AutoLineBreak(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);
if (StringUtils.isNotBlank(src)) {
String widthAttr = node.attr("width");
String heightAttr = node.attr("height");
double width = 0;
double height = 0;
int width = 0;
int height = 0;
if (!widthAttr.isEmpty() && !heightAttr.isEmpty()) {
try {
width = Double.parseDouble(widthAttr);
height = Double.parseDouble(heightAttr);
width = (int) Double.parseDouble(widthAttr);
height = (int) Double.parseDouble(heightAttr);
} catch (NumberFormatException ignored) {
}
@@ -184,10 +182,12 @@ public final class HTMLRenderer {
}
}
Image image = FXUtils.newRemoteImage(uri.toString(), width, height, true, true, false);
if (image.isError()) {
LOG.warning("Failed to load image: " + uri, image.getException());
} else {
try {
Image image = FXUtils.getRemoteImageTask(src, width, height, true, true)
.run();
if (image == null)
throw new AssertionError("Image loading task returned null");
ImageView imageView = new ImageView(image);
if (hyperlink != null) {
URI target = resolveLink(hyperlink);
@@ -198,6 +198,8 @@ public final class HTMLRenderer {
}
children.add(imageView);
return;
} catch (Throwable e) {
LOG.warning("Failed to load image: " + src, e);
}
}
@@ -230,7 +232,7 @@ public final class HTMLRenderer {
case "h6":
case "tr":
if (!children.isEmpty())
appendText("\n\n");
appendAutoLineBreak("\n\n");
break;
}
@@ -252,15 +254,62 @@ public final class HTMLRenderer {
case "h4":
case "h5":
case "h6":
appendText("\n");
appendAutoLineBreak("\n");
break;
}
}
private static boolean isSpacing(String text) {
if (text == null)
return true;
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
if (ch != ' ' && ch != '\t')
return false;
}
return true;
}
public void mergeLineBreaks() {
for (int i = 0; i < this.children.size(); i++) {
javafx.scene.Node child = this.children.get(i);
if (child instanceof AutoLineBreak) {
int lastAutoLineBreak = -1;
for (int j = i + 1; j < this.children.size(); j++) {
javafx.scene.Node otherChild = this.children.get(j);
if (otherChild instanceof AutoLineBreak) {
lastAutoLineBreak = j;
} else if (otherChild instanceof Text && isSpacing(((Text) otherChild).getText())) {
// do nothing
} else {
break;
}
}
if (lastAutoLineBreak > 0) {
this.children.subList(i + 1, lastAutoLineBreak + 1).clear();
if (((Text) child).getText().length() == 1) {
((Text) child).setText("\n\n");
}
}
}
}
}
public TextFlow render() {
TextFlow textFlow = new TextFlow();
textFlow.getStyleClass().add("html");
textFlow.getChildren().setAll(children);
return textFlow;
}
private static final class AutoLineBreak extends Text {
public AutoLineBreak(String text) {
super(text);
}
}
}
@@ -22,6 +22,7 @@ import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -35,7 +36,6 @@ import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.i18n.I18n;
@@ -138,6 +138,9 @@ public class InstallerItem extends Control {
case "forge":
iconType = VersionIconType.FORGE;
break;
case "cleanroom":
iconType = VersionIconType.CLEANROOM;
break;
case "liteloader":
iconType = VersionIconType.CHICKEN;
break;
@@ -232,6 +235,7 @@ public class InstallerItem extends Control {
InstallerItem fabric = new InstallerItem(FABRIC, style);
InstallerItem fabricApi = new InstallerItem(FABRIC_API, style);
InstallerItem forge = new InstallerItem(FORGE, style);
InstallerItem cleanroom = new InstallerItem(CLEANROOM, style);
InstallerItem neoForge = new InstallerItem(NEO_FORGE, style);
InstallerItem liteLoader = new InstallerItem(LITELOADER, style);
InstallerItem optiFine = new InstallerItem(OPTIFINE, style);
@@ -239,11 +243,11 @@ public class InstallerItem extends Control {
InstallerItem quiltApi = new InstallerItem(QUILT_API, style);
Map<InstallerItem, Set<InstallerItem>> incompatibleMap = new HashMap<>();
mutualIncompatible(incompatibleMap, forge, fabric, quilt, neoForge);
addIncompatibles(incompatibleMap, liteLoader, fabric, quilt, neoForge);
addIncompatibles(incompatibleMap, optiFine, fabric, quilt, neoForge);
addIncompatibles(incompatibleMap, fabricApi, forge, quiltApi, neoForge, liteLoader, optiFine);
addIncompatibles(incompatibleMap, quiltApi, forge, fabric, fabricApi, neoForge, liteLoader, optiFine);
mutualIncompatible(incompatibleMap, forge, fabric, quilt, neoForge, cleanroom);
addIncompatibles(incompatibleMap, liteLoader, fabric, quilt, neoForge, cleanroom);
addIncompatibles(incompatibleMap, optiFine, fabric, quilt, neoForge, cleanroom);
addIncompatibles(incompatibleMap, fabricApi, forge, quiltApi, neoForge, liteLoader, optiFine, cleanroom);
addIncompatibles(incompatibleMap, quiltApi, forge, fabric, fabricApi, neoForge, liteLoader, optiFine, cleanroom);
for (Map.Entry<InstallerItem, Set<InstallerItem>> entry : incompatibleMap.entrySet()) {
InstallerItem item = entry.getKey();
@@ -277,16 +281,24 @@ public class InstallerItem extends Control {
game.versionProperty.set(new InstalledState(gameVersion, false, false));
}
InstallerItem[] all = {game, forge, neoForge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi};
InstallerItem[] all = {game, forge, neoForge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi, cleanroom};
for (InstallerItem item : all) {
if (!item.resolvedStateProperty.isBound()) {
item.resolvedStateProperty.bind(item.versionProperty);
item.resolvedStateProperty.bind(Bindings.createObjectBinding(() -> {
InstalledState itemVersion = item.versionProperty.get();
if (itemVersion != null) {
return itemVersion;
}
return InstallableState.INSTANCE;
}, item.versionProperty));
}
}
if (gameVersion == null) {
this.libraries = all;
} else if (gameVersion.equals("1.12.2")) {
this.libraries = new InstallerItem[]{game, forge, cleanroom, liteLoader, optiFine};
} else if (GameVersionNumber.compare(gameVersion, "1.13") < 0) {
this.libraries = new InstallerItem[]{game, forge, liteLoader, optiFine};
} else {
@@ -306,7 +318,9 @@ public class InstallerItem extends Control {
private static final class InstallerItemSkin extends SkinBase<InstallerItem> {
private static final PseudoClass LIST_ITEM = PseudoClass.getPseudoClass("list-item");
private static final PseudoClass CARD = PseudoClass.getPseudoClass("card");
private static final WeakListenerHolder holder = new WeakListenerHolder();
@SuppressWarnings({"FieldCanBeLocal", "unused"})
private final ChangeListener<Number> holder;
InstallerItemSkin(InstallerItem control) {
super(control);
@@ -314,9 +328,10 @@ public class InstallerItem extends Control {
Pane pane;
if (control.style == Style.CARD) {
pane = new VBox();
holder.add(FXUtils.onWeakChange(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7)));
holder = FXUtils.onWeakChangeAndOperate(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7));
} else {
pane = new HBox();
holder = null;
}
pane.getStyleClass().add("installer-item");
RipplerContainer container = new RipplerContainer(pane);
@@ -362,7 +377,9 @@ public class InstallerItem extends Control {
}
return i18n("install.installer.version", s.version);
} else if (state instanceof InstallableState) {
return i18n("install.installer.not_installed");
return control.style == Style.CARD
? i18n("install.installer.do_not_install")
: i18n("install.installer.not_installed");
} else if (state instanceof IncompatibleState) {
return i18n("install.installer.incompatible", i18n("install.installer." + ((IncompatibleState) state).incompatibleItemName));
} else {
@@ -378,12 +395,15 @@ public class InstallerItem extends Control {
pane.getChildren().add(buttonsContainer);
JFXButton removeButton = new JFXButton();
removeButton.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), -1, -1));
removeButton.setGraphic(SVG.CLOSE.createIcon());
removeButton.getStyleClass().add("toggle-icon4");
if (control.id.equals(MINECRAFT.getPatchId())) {
removeButton.setVisible(false);
} else {
removeButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> control.resolvedStateProperty.get() instanceof InstalledState, control.resolvedStateProperty));
removeButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> {
State state = control.resolvedStateProperty.get();
return state instanceof InstalledState && !((InstalledState) state).external;
}, control.resolvedStateProperty));
}
removeButton.managedProperty().bind(removeButton.visibleProperty());
removeButton.setOnAction(e -> {
@@ -396,8 +416,8 @@ public class InstallerItem extends Control {
JFXButton installButton = new JFXButton();
installButton.graphicProperty().bind(Bindings.createObjectBinding(() ->
control.resolvedStateProperty.get() instanceof InstallableState ?
SVG.ARROW_RIGHT.createIcon(Theme.blackFill(), -1, -1) :
SVG.UPDATE.createIcon(Theme.blackFill(), -1, -1),
SVG.ARROW_FORWARD.createIcon() :
SVG.UPDATE.createIcon(),
control.resolvedStateProperty
));
installButton.getStyleClass().add("toggle-icon4");
@@ -23,10 +23,11 @@ import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.scene.control.Control;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import static org.jackhuang.hmcl.ui.construct.SpinnerPane.FAILED_ACTION;
public class ListPageBase<T> extends Control {
public class ListPageBase<T> extends Control implements TransitionPane.Cacheable {
private final ListProperty<T> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading", false);
private final StringProperty failedReason = new SimpleStringProperty(this, "failed");
@@ -27,7 +27,6 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
public class ListPageSkin extends SkinBase<ListPage<?>> {
@@ -64,12 +63,10 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
vBox.setAlignment(Pos.BOTTOM_RIGHT);
vBox.setPickOnBounds(false);
JFXButton btnAdd = new JFXButton();
JFXButton btnAdd = FXUtils.newRaisedButton("");
FXUtils.setLimitWidth(btnAdd, 40);
FXUtils.setLimitHeight(btnAdd, 40);
btnAdd.getStyleClass().add("jfx-button-raised-round");
btnAdd.setButtonType(JFXButton.ButtonType.RAISED);
btnAdd.setGraphic(SVG.PLUS.createIcon(Theme.whiteFill(), -1, -1));
btnAdd.setGraphic(SVG.ADD.createIcon());
btnAdd.setOnAction(e -> skinnable.add());
JFXButton btnRefresh = new JFXButton();
@@ -77,7 +74,7 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
FXUtils.setLimitHeight(btnRefresh, 40);
btnRefresh.getStyleClass().add("jfx-button-raised-round");
btnRefresh.setButtonType(JFXButton.ButtonType.RAISED);
btnRefresh.setGraphic(SVG.REFRESH.createIcon(Theme.whiteFill(), -1, -1));
btnRefresh.setGraphic(SVG.REFRESH.createIcon());
btnRefresh.setOnAction(e -> skinnable.refresh());
vBox.getChildren().setAll(btnAdd);
@@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXListView;
import com.sun.jna.Pointer;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
@@ -28,6 +29,7 @@ import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
@@ -37,17 +39,21 @@ import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import org.jackhuang.hmcl.game.GameDumpGenerator;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.game.Log;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.CircularArrayList;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.platform.windows.Dwmapi;
import org.jackhuang.hmcl.util.platform.windows.WinConstants;
import org.jackhuang.hmcl.util.platform.windows.WinTypes;
import java.io.IOException;
import java.nio.file.Files;
@@ -84,6 +90,36 @@ public final class LogWindow extends Stage {
private final LogWindowImpl impl;
private final ManagedProcess gameProcess;
@SuppressWarnings("unused")
private Object windowsDarkModeListenerHolder;
{
if (OperatingSystem.SYSTEM_VERSION.isAtLeast(OSVersion.WINDOWS_11) && NativeUtils.USE_JNA && Dwmapi.INSTANCE != null) {
this.addEventFilter(WindowEvent.WINDOW_SHOWN, new EventHandler<>() {
@Override
public void handle(WindowEvent event) {
LogWindow.this.removeEventFilter(WindowEvent.WINDOW_SHOWN, this);
windowsDarkModeListenerHolder = FXUtils.onWeakChangeAndOperate(Themes.darkModeProperty(), darkMode -> {
if (LogWindow.this.isShowing()) {
WindowsNativeUtils.getWindowHandle(LogWindow.this).ifPresent(handle -> {
if (handle == WinTypes.HANDLE.INVALID_VALUE)
return;
Dwmapi.INSTANCE.DwmSetWindowAttribute(
new WinTypes.HANDLE(Pointer.createConstant(handle)),
WinConstants.DWMWA_USE_IMMERSIVE_DARK_MODE,
new WinTypes.BOOLByReference(new WinTypes.BOOL(darkMode)),
WinTypes.BOOL.SIZE
);
});
}
});
}
});
}
}
public LogWindow(ManagedProcess gameProcess) {
this(gameProcess, new CircularArrayList<>());
}
@@ -92,7 +128,7 @@ public final class LogWindow extends Stage {
this.logs = logs;
this.impl = new LogWindowImpl();
setScene(new Scene(impl, 800, 480));
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
StyleSheets.init(getScene());
setTitle(i18n("logwindow.title"));
FXUtils.setIcon(this);
@@ -168,6 +204,7 @@ public final class LogWindow extends Stage {
LogWindowImpl() {
getStyleClass().add("log-window");
listView.getProperties().put("no-smooth-scrolling", true);
listView.setItems(FXCollections.observableList(new CircularArrayList<>(logs.size())));
for (int i = 0; i < LEVELS.length; i++) {
@@ -186,7 +223,7 @@ public final class LogWindow extends Stage {
}
private void onTerminateGame() {
LauncherHelper.stopManagedProcesses();
LogWindow.this.gameProcess.stop();
}
private void onClear() {
@@ -224,7 +261,7 @@ public final class LogWindow extends Stage {
try {
if (gameProcess.isRunning()) {
GameDumpGenerator.writeDumpTo(gameProcess.getPID(), dumpFile);
GameDumpGenerator.writeDumpTo(gameProcess.getProcess().pid(), dumpFile);
FXUtils.showFileInExplorer(dumpFile);
}
} catch (Throwable e) {
@@ -264,7 +301,6 @@ public final class LogWindow extends Stage {
VBox vbox = new VBox(3);
vbox.setPadding(new Insets(3, 0, 3, 0));
vbox.setStyle("-fx-background-color: white");
getChildren().setAll(vbox);
{
@@ -286,8 +322,7 @@ public final class LogWindow extends Stage {
HBox hBox = new HBox(3);
for (int i = 0; i < LEVELS.length; i++) {
ToggleButton button = new ToggleButton();
button.setStyle("-fx-background-color: " + FXUtils.toWeb(LEVELS[i].getColor()) + ";");
button.getStyleClass().add("log-toggle");
button.getStyleClass().addAll("log-toggle", LEVELS[i].name().toLowerCase(Locale.ROOT));
button.textProperty().bind(control.buttonText[i]);
button.setSelected(true);
control.showLevel[i].bind(button.selectedProperty());
@@ -307,11 +342,12 @@ public final class LogWindow extends Stage {
listView.scrollTo(listView.getItems().size() - 1);
});
listView.setStyle("-fx-font-family: " + Lang.requireNonNullElse(config().getFontFamily(), FXUtils.DEFAULT_MONOSPACE_FONT)
+ "; -fx-font-size: " + config().getFontSize() + "px;");
listView.setStyle("-fx-font-family: \"" + Lang.requireNonNullElse(config().getFontFamily(), FXUtils.DEFAULT_MONOSPACE_FONT)
+ "\"; -fx-font-size: " + config().getFontSize() + "px;");
Holder<Object> lastCell = new Holder<>();
listView.setCellFactory(x -> new ListCell<Log>() {
{
x.setSelectionModel(new NoneMultipleSelectionModel<>());
getStyleClass().add("log-window-list-cell");
Region clippedContainer = (Region) listView.lookup(".clipped-container");
if (clippedContainer != null) {
+122 -107
View File
@@ -25,94 +25,110 @@ import javafx.scene.layout.StackPane;
import javafx.scene.paint.Paint;
import javafx.scene.shape.SVGPath;
/**
* All vector icons used in the launcher.
* <p>
* Unless otherwise stated,
* these icons are from <a href="https://fonts.google.com/icons?icon.size=24&icon.color=%235f6368&icon.query=list&icon.set=Material+Symbols">Material Symbols</a>,
* with a style of outlined, a weight of 400, a grade of 0, and an optical size of 24 px.
* The view boxes of all icons are normalized to {@code 0 0 24 24}.
*/
public enum SVG {
GEAR("M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z"),
GEAR_OUTLINE("M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10M10,22C9.75,22 9.54,21.82 9.5,21.58L9.13,18.93C8.5,18.68 7.96,18.34 7.44,17.94L4.95,18.95C4.73,19.03 4.46,18.95 4.34,18.73L2.34,15.27C2.21,15.05 2.27,14.78 2.46,14.63L4.57,12.97L4.5,12L4.57,11L2.46,9.37C2.27,9.22 2.21,8.95 2.34,8.73L4.34,5.27C4.46,5.05 4.73,4.96 4.95,5.05L7.44,6.05C7.96,5.66 8.5,5.32 9.13,5.07L9.5,2.42C9.54,2.18 9.75,2 10,2H14C14.25,2 14.46,2.18 14.5,2.42L14.87,5.07C15.5,5.32 16.04,5.66 16.56,6.05L19.05,5.05C19.27,4.96 19.54,5.05 19.66,5.27L21.66,8.73C21.79,8.95 21.73,9.22 21.54,9.37L19.43,11L19.5,12L19.43,13L21.54,14.63C21.73,14.78 21.79,15.05 21.66,15.27L19.66,18.73C19.54,18.95 19.27,19.04 19.05,18.95L16.56,17.95C16.04,18.34 15.5,18.68 14.87,18.93L14.5,21.58C14.46,21.82 14.25,22 14,22H10M11.25,4L10.88,6.61C9.68,6.86 8.62,7.5 7.85,8.39L5.44,7.35L4.69,8.65L6.8,10.2C6.4,11.37 6.4,12.64 6.8,13.8L4.68,15.36L5.43,16.66L7.86,15.62C8.63,16.5 9.68,17.14 10.87,17.38L11.24,20H12.76L13.13,17.39C14.32,17.14 15.37,16.5 16.14,15.62L18.57,16.66L19.32,15.36L17.2,13.81C17.6,12.64 17.6,11.37 17.2,10.2L19.31,8.65L18.56,7.35L16.15,8.39C15.38,7.5 14.32,6.86 13.12,6.62L12.75,4H11.25Z"),
BACK("M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z"),
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("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"),
DELETE_OUTLINE("M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19M8,9H16V19H8V9M15.5,4L14.5,3H9.5L8.5,4H5V6H19V4H15.5Z"),
ACCOUNT_EDIT("M21.7,13.35L20.7,14.35L18.65,12.3L19.65,11.3C19.86,11.09 20.21,11.09 20.42,11.3L21.7,12.58C21.91,12.79 21.91,13.14 21.7,13.35M12,18.94L18.06,12.88L20.11,14.93L14.06,21H12V18.94M12,14C7.58,14 4,15.79 4,18V20H10V18.11L14,14.11C13.34,14.03 12.67,14 12,14M12,4A4,4 0 0,0 8,8A4,4 0 0,0 12,12A4,4 0 0,0 16,8A4,4 0 0,0 12,4Z"),
EXPAND("M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"),
COLLAPSE("M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z"),
NAVIGATE("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"),
ROCKET_LAUNCH_OUTLINE("M13.13 22.19L11.5 18.36C13.07 17.78 14.54 17 15.9 16.09L13.13 22.19M5.64 12.5L1.81 10.87L7.91 8.1C7 9.46 6.22 10.93 5.64 12.5M19.22 4C19.5 4 19.75 4 19.96 4.05C20.13 5.44 19.94 8.3 16.66 11.58C14.96 13.29 12.93 14.6 10.65 15.47L8.5 13.37C9.42 11.06 10.73 9.03 12.42 7.34C15.18 4.58 17.64 4 19.22 4M19.22 2C17.24 2 14.24 2.69 11 5.93C8.81 8.12 7.5 10.53 6.65 12.64C6.37 13.39 6.56 14.21 7.11 14.77L9.24 16.89C9.62 17.27 10.13 17.5 10.66 17.5C10.89 17.5 11.13 17.44 11.36 17.35C13.5 16.53 15.88 15.19 18.07 13C23.73 7.34 21.61 2.39 21.61 2.39S20.7 2 19.22 2M14.54 9.46C13.76 8.68 13.76 7.41 14.54 6.63S16.59 5.85 17.37 6.63C18.14 7.41 18.15 8.68 17.37 9.46C16.59 10.24 15.32 10.24 14.54 9.46M8.88 16.53L7.47 15.12L8.88 16.53M6.24 22L9.88 18.36C9.54 18.27 9.21 18.12 8.91 17.91L4.83 22H6.24M2 22H3.41L8.18 17.24L6.76 15.83L2 20.59V22M2 19.17L6.09 15.09C5.88 14.79 5.73 14.47 5.64 14.12L2 17.76V19.17Z"),
LAUNCH_OUTLINE("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"),
SCRIPT("M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z"),
PENCIL("M20.71,4.04C21.1,3.65 21.1,3 20.71,2.63L18.37,0.29C18,-0.1 17.35,-0.1 16.96,0.29L15,2.25L18.75,6M17.75,7L14,3.25L4,13.25V17H7.75L17.75,7Z"),
PENCIL_OUTLINE("M14.06,9L15,9.94L5.92,19H5V18.08L14.06,9M17.66,3C17.41,3 17.15,3.1 16.96,3.29L15.13,5.12L18.88,8.87L20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18.17,3.09 17.92,3 17.66,3M14.06,6.19L3,17.25V21H6.75L17.81,9.94L14.06,6.19Z"),
REFRESH("M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z"),
FOLDER_OPEN("M19,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H19A2,2 0 0,1 21,8H21L4,8V18L6.14,10H23.21L20.93,18.5C20.7,19.37 19.92,20 19,20Z"),
FOLDER_OUTLINE("M20,18H4V8H20M20,6H12L10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6Z"),
UPDATE("M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z"),
CLOSE_CIRCLE("M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z"),
CHECK_CIRCLE("M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z"),
INFO_CIRCLE("M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"),
HELP_CIRCLE("M15.07,11.25L14.17,12.17C13.45,12.89 13,13.5 13,15H11V14.5C11,13.39 11.45,12.39 12.17,11.67L13.41,10.41C13.78,10.05 14,9.55 14,9C14,7.89 13.1,7 12,7A2,2 0 0,0 10,9H8A4,4 0 0,1 12,5A4,4 0 0,1 16,9C16,9.88 15.64,10.67 15.07,11.25M13,19H11V17H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z"),
HELP_CIRCLE_OUTLINE("M11,18H13V16H11V18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,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,6A4,4 0 0,0 8,10H10A2,2 0 0,1 12,8A2,2 0 0,1 14,10C14,12 11,11.75 11,15H13C13,12.75 16,12.5 16,10A4,4 0 0,0 12,6Z"),
ALERT("M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z"),
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"),
TRIANGLE("M1,21H23L12,2"),
HOME("M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z"),
VIEW_LIST("M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z"),
CHECK("M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"),
ARROW_RIGHT("M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z"),
WRENCH("M22.7,19L13.6,9.9C14.5,7.6 14,4.9 12.1,3C10.1,1 7.1,0.6 4.7,1.7L9,6L6,9L1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1C4.8,14 7.5,14.5 9.8,13.6L18.9,22.7C19.3,23.1 19.9,23.1 20.3,22.7L22.6,20.4C23.1,20 23.1,19.3 22.7,19Z"),
WRENCH_OUTLINE("M22.61,19L13.53,9.91C14.46,7.57 14,4.81 12.09,2.91C9.79,0.61 6.21,0.4 3.66,2.26L7.5,6.11L6.08,7.5L2.25,3.69C0.39,6.23 0.6,9.82 2.9,12.11C4.76,13.97 7.47,14.46 9.79,13.59L18.9,22.7C19.29,23.09 19.92,23.09 20.31,22.7L22.61,20.4C23,20 23,19.39 22.61,19M19.61,20.59L10.15,11.13C9.54,11.58 8.86,11.85 8.15,11.95C6.79,12.15 5.36,11.74 4.32,10.7C3.37,9.76 2.93,8.5 3,7.26L6.09,10.35L10.33,6.11L7.24,3C8.5,2.95 9.73,3.39 10.68,4.33C11.76,5.41 12.17,6.9 11.92,8.29C11.8,9 11.5,9.66 11.04,10.25L20.5,19.7L19.61,20.59Z"),
UPLOAD("M9,16V10H5L12,3L19,10H15V16H9M5,20V18H19V20H5Z"),
HANGER("M12 4A3.5 3.5 0 0 0 8.5 7.5H10.5A1.5 1.5 0 0 1 12 6A1.5 1.5 0 0 1 13.5 7.5A1.5 1.5 0 0 1 12 9C11.45 9 11 9.45 11 10V11.75L2.4 18.2A1 1 0 0 0 3 20H21A1 1 0 0 0 21.6 18.2L13 11.75V10.85A3.5 3.5 0 0 0 15.5 7.5A3.5 3.5 0 0 0 12 4M12 13.5L18 18H6Z"),
PUZZLE("M22,13.5C22,15.26 20.7,16.72 19,16.96V20A2,2 0 0,1 17,22H13.2V21.7A2.7,2.7 0 0,0 10.5,19C9,19 7.8,20.21 7.8,21.7V22H4A2,2 0 0,1 2,20V16.2H2.3C3.79,16.2 5,15 5,13.5C5,12 3.79,10.8 2.3,10.8H2V7A2,2 0 0,1 4,5H7.04C7.28,3.3 8.74,2 10.5,2C12.26,2 13.72,3.3 13.96,5H17A2,2 0 0,1 19,7V10.04C20.7,10.28 22,11.74 22,13.5M17,15H18.5A1.5,1.5 0 0,0 20,13.5A1.5,1.5 0 0,0 18.5,12H17V7H12V5.5A1.5,1.5 0 0,0 10.5,4A1.5,1.5 0 0,0 9,5.5V7H4V9.12C5.76,9.8 7,11.5 7,13.5C7,15.5 5.75,17.2 4,17.88V20H6.12C6.8,18.25 8.5,17 10.5,17C12.5,17 14.2,18.25 14.88,20H17V15Z"),
CUBE("M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L6.04,7.5L12,10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z"),
PACK("M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L10.11,5.22L16,8.61L17.96,7.5L12,4.15M6.04,7.5L12,10.85L13.96,9.75L8.08,6.35L6.04,7.5M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z"),
TEXTURE_BOX("M20 2H4C2.9 2 2 2.9 2 4V20C2 21.11 2.9 22 4 22H20C21.11 22 22 21.11 22 20V4C22 2.9 21.11 2 20 2M4 6L6 4H10.9L4 10.9V6M4 13.7L13.7 4H18.6L4 18.6V13.7M20 18L18 20H13.1L20 13.1V18M20 10.3L10.3 20H5.4L20 5.4V10.3Z"),
GAMEPAD("M6,9H8V11H10V13H8V15H6V13H4V11H6V9M18.5,9A1.5,1.5 0 0,1 20,10.5A1.5,1.5 0 0,1 18.5,12A1.5,1.5 0 0,1 17,10.5A1.5,1.5 0 0,1 18.5,9M15.5,12A1.5,1.5 0 0,1 17,13.5A1.5,1.5 0 0,1 15.5,15A1.5,1.5 0 0,1 14,13.5A1.5,1.5 0 0,1 15.5,12M17,5A7,7 0 0,1 24,12A7,7 0 0,1 17,19C15.04,19 13.27,18.2 12,16.9C10.73,18.2 8.96,19 7,19A7,7 0 0,1 0,12A7,7 0 0,1 7,5H17M7,7A5,5 0 0,0 2,12A5,5 0 0,0 7,17C8.64,17 10.09,16.21 11,15H13C13.91,16.21 15.36,17 17,17A5,5 0 0,0 22,12A5,5 0 0,0 17,7H7Z"),
FIRE("M17.66 11.2C17.43 10.9 17.15 10.64 16.89 10.38C16.22 9.78 15.46 9.35 14.82 8.72C13.33 7.26 13 4.85 13.95 3C13 3.23 12.17 3.75 11.46 4.32C8.87 6.4 7.85 10.07 9.07 13.22C9.11 13.32 9.15 13.42 9.15 13.55C9.15 13.77 9 13.97 8.8 14.05C8.57 14.15 8.33 14.09 8.14 13.93C8.08 13.88 8.04 13.83 8 13.76C6.87 12.33 6.69 10.28 7.45 8.64C5.78 10 4.87 12.3 5 14.47C5.06 14.97 5.12 15.47 5.29 15.97C5.43 16.57 5.7 17.17 6 17.7C7.08 19.43 8.95 20.67 10.96 20.92C13.1 21.19 15.39 20.8 17.03 19.32C18.86 17.66 19.5 15 18.56 12.72L18.43 12.46C18.22 12 17.66 11.2 17.66 11.2M14.5 17.5C14.22 17.74 13.76 18 13.4 18.1C12.28 18.5 11.16 17.94 10.5 17.28C11.69 17 12.4 16.12 12.61 15.23C12.78 14.43 12.46 13.77 12.33 13C12.21 12.26 12.23 11.63 12.5 10.94C12.69 11.32 12.89 11.7 13.13 12C13.9 13 15.11 13.44 15.37 14.8C15.41 14.94 15.43 15.08 15.43 15.23C15.46 16.05 15.1 16.95 14.5 17.5H14.5Z"),
MONITOR_SCREENSHOT("M9,6H5V10H7V8H9M19,10H17V12H15V14H19M21,16H3V4H21M21,2H3C1.89,2 1,2.89 1,4V16A2,2 0 0,0 3,18H10V20H8V22H16V20H14V18H21A2,2 0 0,0 23,16V4C23,2.89 22.1,2 21,2"),
TEXTURE("M9.29,21H12.12L21,12.12V9.29M19,21C19.55,21 20.05,20.78 20.41,20.41C20.78,20.05 21,19.55 21,19V17L17,21M5,3A2,2 0 0,0 3,5V7L7,3M11.88,3L3,11.88V14.71L14.71,3M19.5,3.08L3.08,19.5C3.17,19.85 3.35,20.16 3.59,20.41C3.84,20.65 4.15,20.83 4.5,20.92L20.93,4.5C20.74,3.8 20.2,3.26 19.5,3.08Z"),
ALPHA_CIRCLE_OUTLINE("M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,1 11,7M11,9V11H13V9H11M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z"),
BETA_CIRCLE_OUTLINE("M15,10.5C15,11.3 14.3,12 13.5,12C14.3,12 15,12.7 15,13.5V15A2,2 0 0,1 13,17H9V7H13A2,2 0 0,1 15,9V10.5M13,15V13H11V15H13M13,11V9H11V11H13M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z"),
RELEASE_CIRCLE_OUTLINE("M9,7H13A2,2 0 0,1 15,9V11C15,11.84 14.5,12.55 13.76,12.85L15,17H13L11.8,13H11V17H9V7M11,9V11H13V9H11M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,16.41 7.58,20 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z"),
INFORMATION_OUTLINE("M11,9H13V7H11M12,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,2M11,17H13V11H11V17Z"),
HAND_HEAR_OUTLINE("M16 3.23C16.71 2.41 17.61 2 18.7 2C19.61 2 20.37 2.33 21 3C21.63 3.67 21.96 4.43 22 5.3C22 6 21.67 6.81 21 7.76S19.68 9.5 19.03 10.15C18.38 10.79 17.37 11.74 16 13C14.61 11.74 13.59 10.79 12.94 10.15S11.63 8.71 10.97 7.76C10.31 6.81 10 6 10 5.3C10 4.39 10.32 3.63 10.97 3C11.62 2.37 12.4 2.04 13.31 2C14.38 2 15.27 2.41 16 3.23M22 19V20L14 22.5L7 20.56V22H1V11H8.97L15.13 13.3C16.25 13.72 17 14.8 17 16H19C20.66 16 22 17.34 22 19M5 20V13H3V20H5M19.9 18.57C19.74 18.24 19.39 18 19 18H13.65C13.11 18 12.58 17.92 12.07 17.75L9.69 16.96L10.32 15.06L12.7 15.85C13 15.95 15 16 15 16C15 15.63 14.77 15.3 14.43 15.17L8.61 13H7V18.5L13.97 20.41L19.9 18.57Z"),
MOJANG("m 13.965847,0 c -1.010612,0.82802228 -1.197232,2.1950303 -1.265172,3.4179557 0.02123,1.0021189 1.341654,1.2994159 1.953117,0.590289 C 15.091158,2.6579315 14.369242,1.2738804 13.965847,0 Z M 10.913012,2.9296764 C 10.755901,3.6982508 10.628413,4.4668999 10.492533,5.2354744 8.9893533,3.9913178 7.1504705,3.0824837 5.1419856,3.2947971 3.4944341,3.4646478 0.94293227,2.6961479 0.14038761,4.645185 -0.12288102,8.3139606 0.07223999,12.01236 0.03402357,15.689629 c -0.12314178,1.222925 0.86170213,2.420422 2.14407513,2.280295 4.2207899,0.03397 8.4502143,0.04723 12.6710043,-0.0038 1.265389,0.135918 1.957646,-1.010748 2.13599,-2.0893 C 13.269608,16.437357 9.1760813,16.929609 5.7111265,15.129192 2.5986124,13.58355 2.246023,8.3138817 5.5581114,6.7979639 9.3203049,5.1758896 13.859607,8.0382886 14.942405,11.787743 15.613316,12.11046 16.284433,12.424684 16.95959,12.743154 16.624135,10.348258 16.653651,7.800456 15.579346,5.5881508 15.057054,4.7473897 14.097531,6.2714182 13.379911,5.6217388 12.416008,4.865903 11.749527,3.8128948 10.913012,2.9296713 Z"),
MICROSOFT("M2,3H11V12H2V3M11,22H2V13H11V22M21,3V12H12V3H21M21,22H12V13H21V22Z"),
ACCOUNT_OUTLINE("M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z"),
ACCOUNT_GROUP_OUTLINE("M12,5A3.5,3.5 0 0,0 8.5,8.5A3.5,3.5 0 0,0 12,12A3.5,3.5 0 0,0 15.5,8.5A3.5,3.5 0 0,0 12,5M12,7A1.5,1.5 0 0,1 13.5,8.5A1.5,1.5 0 0,1 12,10A1.5,1.5 0 0,1 10.5,8.5A1.5,1.5 0 0,1 12,7M5.5,8A2.5,2.5 0 0,0 3,10.5C3,11.44 3.53,12.25 4.29,12.68C4.65,12.88 5.06,13 5.5,13C5.94,13 6.35,12.88 6.71,12.68C7.08,12.47 7.39,12.17 7.62,11.81C6.89,10.86 6.5,9.7 6.5,8.5C6.5,8.41 6.5,8.31 6.5,8.22C6.2,8.08 5.86,8 5.5,8M18.5,8C18.14,8 17.8,8.08 17.5,8.22C17.5,8.31 17.5,8.41 17.5,8.5C17.5,9.7 17.11,10.86 16.38,11.81C16.5,12 16.63,12.15 16.78,12.3C16.94,12.45 17.1,12.58 17.29,12.68C17.65,12.88 18.06,13 18.5,13C18.94,13 19.35,12.88 19.71,12.68C20.47,12.25 21,11.44 21,10.5A2.5,2.5 0 0,0 18.5,8M12,14C9.66,14 5,15.17 5,17.5V19H19V17.5C19,15.17 14.34,14 12,14M4.71,14.55C2.78,14.78 0,15.76 0,17.5V19H3V17.07C3,16.06 3.69,15.22 4.71,14.55M19.29,14.55C20.31,15.22 21,16.06 21,17.07V19H24V17.5C24,15.76 21.22,14.78 19.29,14.55M12,16C13.53,16 15.24,16.5 16.23,17H7.77C8.76,16.5 10.47,16 12,16Z"),
ACCOUNT_ARROW_RIGHT_OUTLINE("M19,21V19H15V17H19V15L22,18L19,21M13,18C13,18.71 13.15,19.39 13.42,20H2V17C2,14.79 5.58,13 10,13C11,13 11.96,13.09 12.85,13.26C13.68,13.42 14.44,13.64 15.11,13.92C13.83,14.83 13,16.32 13,18M4,17V18H11C11,16.96 11.23,15.97 11.64,15.08L10,15C6.69,15 4,15.9 4,17M10,4A4,4 0 0,1 14,8A4,4 0 0,1 10,12A4,4 0 0,1 6,8A4,4 0 0,1 10,4M10,6A2,2 0 0,0 8,8A2,2 0 0,0 10,10A2,2 0 0,0 12,8A2,2 0 0,0 10,6Z"),
STYLE_OUTLINE("M2.5 19.6L3.8 20.2V11.2L1.4 17C1 18.1 1.5 19.2 2.5 19.6M15.2 4.8L20.2 16.8L12.9 19.8L7.9 7.9V7.8L15.2 4.8M15.3 2.8C15 2.8 14.8 2.8 14.5 2.9L7.1 6C6.4 6.3 5.9 7 5.9 7.8C5.9 8 5.9 8.3 6 8.6L11 20.5C11.3 21.3 12 21.7 12.8 21.7C13.1 21.7 13.3 21.7 13.6 21.6L21 18.5C22 18.1 22.5 16.9 22.1 15.9L17.1 4C16.8 3.2 16 2.8 15.3 2.8M10.5 9.9C9.9 9.9 9.5 9.5 9.5 8.9S9.9 7.9 10.5 7.9C11.1 7.9 11.5 8.4 11.5 8.9S11.1 9.9 10.5 9.9M5.9 19.8C5.9 20.9 6.8 21.8 7.9 21.8H9.3L5.9 13.5V19.8Z"),
DOWNLOAD_OUTLINE("M13,5V11H14.17L12,13.17L9.83,11H11V5H13M15,3H9V9H5L12,16L19,9H15V3M19,18H5V20H19V18Z"),
APPLICATION_OUTLINE("M21 2H3C1.9 2 1 2.9 1 4V20C1 21.1 1.9 22 3 22H21C22.1 22 23 21.1 23 20V4C23 2.9 22.1 2 21 2M21 20H3V6H21V20Z"),
EARTH("M17.9,17.39C17.64,16.59 16.89,16 16,16H15V13A1,1 0 0,0 14,12H8V10H10A1,1 0 0,0 11,9V7H13A2,2 0 0,0 15,5V4.59C17.93,5.77 20,8.64 20,12C20,14.08 19.2,15.97 17.9,17.39M11,19.93C7.05,19.44 4,16.08 4,12C4,11.38 4.08,10.78 4.21,10.21L9,15V16A2,2 0 0,0 11,18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"),
BELL("M21,19V20H3V19L5,17V11C5,7.9 7.03,5.17 10,4.29C10,4.19 10,4.1 10,4A2,2 0 0,1 12,2A2,2 0 0,1 14,4C14,4.1 14,4.19 14,4.29C16.97,5.17 19,7.9 19,11V17L21,19M14,21A2,2 0 0,1 12,23A2,2 0 0,1 10,21"),
CONTENT_SAVE_MOVE_OUTLINE("M13 17H17V14L22 18.5L17 23V20H13V17M14 12.8C13.5 12.31 12.78 12 12 12C10.34 12 9 13.34 9 15C9 16.31 9.84 17.41 11 17.82C11.07 15.67 12.27 13.8 14 12.8M11.09 19H5V5H16.17L19 7.83V12.35C19.75 12.61 20.42 13 21 13.54V7L17 3H5C3.89 3 3 3.9 3 5V19C3 20.1 3.89 21 5 21H11.81C11.46 20.39 11.21 19.72 11.09 19M6 10H15V6H6V10Z"),
ACCOUNT("M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z"),
SERVER("M13,19H14A1,1 0 0,1 15,20H22V22H15A1,1 0 0,1 14,23H10A1,1 0 0,1 9,22H2V20H9A1,1 0 0,1 10,19H11V17H4A1,1 0 0,1 3,16V12A1,1 0 0,1 4,11H20A1,1 0 0,1 21,12V16A1,1 0 0,1 20,17H13V19M4,3H20A1,1 0 0,1 21,4V8A1,1 0 0,1 20,9H4A1,1 0 0,1 3,8V4A1,1 0 0,1 4,3M9,7H10V5H9V7M9,15H10V13H9V15M5,5V7H7V5H5M5,13V15H7V13H5Z"),
MESSAGE_ALERT_OUTLINE("M13,10H11V6H13V10M13,12H11V14H13V12M22,4V16A2,2 0 0,1 20,18H6L2,22V4A2,2 0 0,1 4,2H20A2,2 0 0,1 22,4M20,4H4V17.2L5.2,16H20V4Z"),
CHECK_CIRCLE_OUTLINE("M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M12 20C7.59 20 4 16.41 4 12S7.59 4 12 4 20 7.59 20 12 16.41 20 12 20M16.59 7.58L10 14.17L7.41 11.59L6 13L10 17L18 9L16.59 7.58Z"),
CLOSE_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,2C6.47,2 2,6.47 2,12C2,17.53 6.47,22 12,22C17.53,22 22,17.53 22,12C22,6.47 17.53,2 12,2M14.59,8L12,10.59L9.41,8L8,9.41L10.59,12L8,14.59L9.41,16L12,13.41L14.59,16L16,14.59L13.41,12L16,9.41L14.59,8Z"),
CLOCK_OUTLINE("M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z"),
MAGNIFY("M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"),
MENU_UP("M7,15L12,10L17,15H7Z"),
MENU_DOWN("M7,10L12,15L17,10H7Z"),
RESTORE("M13,3A9,9 0 0,0 4,12H1L4.89,15.89L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3Z"),
BUG("M14,12H10V10H14M14,16H10V14H14M20,8H17.19C16.74,7.22 16.12,6.55 15.37,6.04L17,4.41L15.59,3L13.42,5.17C12.96,5.06 12.5,5 12,5C11.5,5 11.04,5.06 10.59,5.17L8.41,3L7,4.41L8.62,6.04C7.88,6.55 7.26,7.22 6.81,8H4V10H6.09C6.04,10.33 6,10.66 6,11V12H4V14H6V15C6,15.34 6.04,15.67 6.09,16H4V18H6.81C7.85,19.79 9.78,21 12,21C14.22,21 16.15,19.79 17.19,18H20V16H17.91C17.96,15.67 18,15.34 18,15V14H20V12H18V11C18,10.66 17.96,10.33 17.91,10H20V8Z"),
DISCORD("M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z"),
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"),
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");
ADD("M11 13H5V11H11V5H13V11H19V13H13V19H11V13Z"),
ADD_CIRCLE("M11 17H13V13H17V11H13V7H11V11H7V13H11V17ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
ALPHA_CIRCLE("M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,1 11,7M11,9V11H13V9H11M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z"), // Not Material
ARCHIVE("M12 18 16 14 14.6 12.6 13 14.2V10H11V14.2L9.4 12.6 8 14 12 18ZM5 8V19H19V8H5ZM5 21Q4.175 21 3.5875 20.4125T3 19V6.525Q3 6.175 3.1125 5.85T3.45 5.25L4.7 3.725Q4.975 3.375 5.3875 3.1875T6.25 3H17.75Q18.2 3 18.6125 3.1875T19.3 3.725L20.55 5.25Q20.775 5.525 20.8875 5.85T21 6.525V19Q21 19.825 20.4125 20.4125T19 21H5ZM5.4 6H18.6L17.75 5H6.25L5.4 6ZM12 13.5Z"),
ARCHIVE_FILL("M12 18l4-4-1.4-1.4L13 14.2V10H11v4.2L9.4 12.6 8 14l4 4ZM5 21q-.825 0-1.4125-.5875T3 19V6.525q0-.35.1125-.675t.3375-.6L4.7 3.725q.275-.35.6875-.5375T6.25 3h11.5q.45 0 .8625.1875T19.3 3.725L20.55 5.25q.225.275.3375.6T21 6.525V19q0 .825-.5875 1.4125T19 21H5ZM5.4 6H18.6l-.85-1H6.25L5.4 6Z"),
ARROW_BACK("M7.825 13 13.425 18.6 12 20 4 12 12 4 13.425 5.4 7.825 11H20V13H7.825Z"),
ARROW_DROP_DOWN("M12 15 7 10H17L12 15Z"),
ARROW_DROP_UP("M7 14 12 9 17 14H7Z"),
ARROW_FORWARD("M16.175 13H4V11H16.175L10.575 5.4 12 4 20 12 12 20 10.575 18.6 16.175 13Z"),
BETA_CIRCLE("M15,10.5C15,11.3 14.3,12 13.5,12C14.3,12 15,12.7 15,13.5V15A2,2 0 0,1 13,17H9V7H13A2,2 0 0,1 15,9V10.5M13,15V13H11V15H13M13,11V9H11V11H13M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z"), // Not Material
CANCEL("M8.4 17 12 13.4 15.6 17 17 15.6 13.4 12 17 8.4 15.6 7 12 10.6 8.4 7 7 8.4 10.6 12 7 15.6 8.4 17ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
CHAT("M6 14H14V12H6V14ZM6 11H18V9H6V11ZM6 8H18V6H6V8ZM2 22V4Q2 3.175 2.5875 2.5875T4 2H20Q20.825 2 21.4125 2.5875T22 4V16Q22 16.825 21.4125 17.4125T20 18H6L2 22ZM5.15 16H20V4H4V17.125L5.15 16ZM4 16V4 16Z"),
CHECK("M9.55 18 3.85 12.3 5.275 10.875 9.55 15.15 18.725 5.975 20.15 7.4 9.55 18Z"),
CHECKROOM("M3 20Q2.575 20 2.2875 19.7125T2 19Q2 18.75 2.1 18.5375T2.4 18.2L11 11.75V10Q11 9.575 11.3 9.2875T12.025 9Q12.65 9 13.075 8.55T13.5 7.475Q13.5 6.85 13.0625 6.425T12 6Q11.375 6 10.9375 6.4375T10.5 7.5H8.5Q8.5 6.05 9.525 5.025T12 4Q13.45 4 14.475 5.0125T15.5 7.475Q15.5 8.65 14.8125 9.575T13 10.85V11.75L21.6 18.2Q21.8 18.325 21.9 18.5375T22 19Q22 19.425 21.7125 19.7125T21 20H3ZM6 18H18L12 13.5 6 18Z"),
CHECK_CIRCLE("M10.6 16.6 17.65 9.55 16.25 8.15 10.6 13.8 7.75 10.95 6.35 12.35 10.6 16.6ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
CLOSE("M6.4 19 5 17.6 10.6 12 5 6.4 6.4 5 12 10.6 17.6 5 19 6.4 13.4 12 19 17.6 17.6 19 12 13.4 6.4 19Z"),
CONTENT_COPY("M9 18Q8.175 18 7.5875 17.4125T7 16V4Q7 3.175 7.5875 2.5875T9 2H18Q18.825 2 19.4125 2.5875T20 4V16Q20 16.825 19.4125 17.4125T18 18H9ZM9 16H18V4H9V16ZM5 22Q4.175 22 3.5875 21.4125T3 20V6H5V20H16V22H5ZM9 16V4 16Z"),
CREATE_NEW_FOLDER("M14 16h2V14h2V12H16V10H14v2H12v2h2v2ZM4 20q-.825 0-1.4125-.5875T2 18V6q0-.825.5875-1.4125T4 4h6l2 2h8q.825 0 1.4125.5875T22 8V18q0 .825-.5875 1.4125T20 20H4Zm0-2H20V8H11.175l-2-2H4V18ZV6 18Z"),
DELETE("M7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM9 17H11V8H9V17ZM13 17H15V8H13V17ZM7 6V19 6Z"),
DELETE_FOREVER("M9.4 16.5 12 13.9 14.6 16.5 16 15.1 13.4 12.5 16 9.9 14.6 8.5 12 11.1 9.4 8.5 8 9.9 10.6 12.5 8 15.1 9.4 16.5ZM7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM7 6V19 6Z"),
DEPLOYED_CODE("M11 19.425V12.575L5 9.1V15.95L11 19.425ZM13 19.425 19 15.95V9.1L13 12.575V19.425ZM12 10.85 17.925 7.425 12 4 6.075 7.425 12 10.85ZM4 17.7Q3.525 17.425 3.2625 16.975T3 15.975V8.025Q3 7.475 3.2625 7.025T4 6.3L11 2.275Q11.475 2 12 2T13 2.275L20 6.3Q20.475 6.575 20.7375 7.025T21 8.025V15.975Q21 16.525 20.7375 16.975T20 17.7L13 21.725Q12.525 22 12 22T11 21.725L4 17.7ZM12 12Z"),
DEPLOYED_CODE_FILL("M11 21.725 4 17.7q-.475-.275-.7375-.725T3 15.975V8.025q0-.55.2625-1T4 6.3l7-4.025Q11.475 2 12 2t1 .275L20 6.3q.475.275.7375.725t.2625 1v7.95q0 .55-.2625 1T20 17.7l-7 4.025Q12.525 22 12 22t-1-.275Zm0-9.15v6.85L12 20l1-.575v-6.85L19 9.1V8.05l-1.075-.625L12 10.85 6.075 7.425 5 8.05V9.1l6 3.475Z"),
DOWNLOAD("M12 16 7 11 8.4 9.55 11 12.15V4H13V12.15L15.6 9.55 17 11 12 16ZM6 20Q5.175 20 4.5875 19.4125T4 18V15H6V18H18V15H20V18Q20 18.825 19.4125 19.4125T18 20H6Z"),
DRESSER("M4 21V5Q4 4.175 4.5875 3.5875T6 3H18Q18.825 3 19.4125 3.5875T20 5V21H18V19H6V21H4ZM6 11H11V5H6V11ZM13 7H18V5H13V7ZM13 11H18V9H13V11ZM10 16H14V14H10V16ZM6 13V17H18V13H6ZM6 13V17 13Z"),
EDIT("M5 19H6.425L16.2 9.225 14.775 7.8 5 17.575V19ZM3 21V16.75L16.2 3.575Q16.5 3.3 16.8625 3.15T17.625 3Q18.025 3 18.4 3.15T19.05 3.6L20.425 5Q20.725 5.275 20.8625 5.65T21 6.4Q21 6.8 20.8625 7.1625T20.425 7.825L7.25 21H3ZM19 6.4 17.6 5 19 6.4ZM15.475 8.525 14.775 7.8 16.2 9.225 15.475 8.525Z"),
ERROR("M12 17Q12.425 17 12.7125 16.7125T13 16Q13 15.575 12.7125 15.2875T12 15Q11.575 15 11.2875 15.2875T11 16Q11 16.425 11.2875 16.7125T12 17ZM11 13H13V7H11V13ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
EXPLORE("M12 12Zm0 8q-3.325 0-5.6625-2.3375T4 12Q4 8.675 6.3375 6.3375T12 4q3.325-0 5.6625 2.3375T20 12q0 3.325-2.3375 5.6625T12 20Zm0 2q2.075-0 3.9-.7875t3.175-2.1375q1.35-1.35 2.1375-3.175T22 12q-0-2.075-.7875-3.9T19.075 4.925q-1.35-1.35-3.175-2.1375T12 2q-2.075 0-3.9.7875T4.925 4.925Q3.575 6.275 2.7875 8.1T2 12q0 2.075.7875 3.9T4.925 19.075q1.35 1.35 3.175 2.1375T12 22Zm0-8.5q.625 0 1.0625-.4375T13.5 12t-.4375-1.0625T12 10.5t-1.0625.4375T10.5 12t.4375 1.0625T12 13.5Zm-4.5 3 2-7 7-2-2 7-7 2Z"),
EXTENSION("M8.8 21H5Q4.175 21 3.5875 20.4125T3 19V15.2Q4.2 15.2 5.1 14.4375T6 12.5Q6 11.325 5.1 10.5625T3 9.8V6Q3 5.175 3.5875 4.5875T5 4H9Q9 2.95 9.725 2.225T11.5 1.5Q12.55 1.5 13.275 2.225T14 4H18Q18.825 4 19.4125 4.5875T20 6V10Q21.05 10 21.775 10.725T22.5 12.5Q22.5 13.55 21.775 14.275T20 15V19Q20 19.825 19.4125 20.4125T18 21H14.2Q14.2 19.75 13.4125 18.875T11.5 18Q10.375 18 9.5875 18.875T8.8 21ZM5 19H7.125Q7.725 17.35 9.05 16.675T11.5 16Q12.625 16 13.95 16.675T15.875 19H18V13H20Q20.2 13 20.35 12.85T20.5 12.5Q20.5 12.3 20.35 12.15T20 12H18V6H12V4Q12 3.8 11.85 3.65T11.5 3.5Q11.3 3.5 11.15 3.65T11 4V6H5V8.2Q6.35 8.7 7.175 9.875T8 12.5Q8 13.925 7.175 15.1T5 16.8V19ZM11.5 12.5Z"),
EXTENSION_FILL("M8.8 21H5q-.825 0-1.4125-.5875T3 19V15.2q1.2 0 2.1-.7625T6 12.5q0-1.175-.9-1.9375T3 9.8V6q0-.825.5875-1.4125T5 4H9q0-1.05.725-1.775T11.5 1.5q1.05 0 1.775.725T14 4h4q.825 0 1.4125.5875T20 6v4q1.05 0 1.775.725T22.5 12.5q0 1.05-.725 1.775T20 15v4q0 .825-.5875 1.4125T18 21H14.2q0-1.25-.7875-2.125T11.5 18q-1.125 0-1.9125.875T8.8 21Z"),
FEEDBACK("M12 15Q12.425 15 12.7125 14.7125T13 14Q13 13.575 12.7125 13.2875T12 13Q11.575 13 11.2875 13.2875T11 14Q11 14.425 11.2875 14.7125T12 15ZM11 11H13V5H11V11ZM2 22V4Q2 3.175 2.5875 2.5875T4 2H20Q20.825 2 21.4125 2.5875T22 4V16Q22 16.825 21.4125 17.4125T20 18H6L2 22ZM5.15 16H20V4H4V17.125L5.15 16ZM4 16V4 16Z"),
FEEDBACK_FILL("M2 22V4q0-.825.5875-1.4125T4 2H20q.825 0 1.4125.5875T22 4V16q0 .825-.5875 1.4125T20 18H6L2 22Zm10-7q.425 0 .7125-.2875T13 14t-.2875-.7125T12 13t-.7125.2875T11 14t.2875.7125T12 15Zm-1-4h2V5H11v6Z"),
FOLDER("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8V18Q22 18.825 21.4125 19.4125T20 20H4ZM4 18H20V8H11.175L9.175 6H4V18ZM4 18V6 18Z"),
FOLDER_COPY("M3 21Q2.175 21 1.5875 20.4125T1 19V6H3V19H20V21H3ZM7 17Q6.175 17 5.5875 16.4125T5 15V4Q5 3.175 5.5875 2.5875T7 2H12L14 4H21Q21.825 4 22.4125 4.5875T23 6V15Q23 15.825 22.4125 16.4125T21 17H7ZM7 15H21V6H13.175L11.175 4H7V15ZM7 15V4 15Z"),
FOLDER_OPEN("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8H11.175L9.175 6H4V18L6.4 10H23.5L20.925 18.575Q20.725 19.225 20.1875 19.6125T19 20H4ZM6.1 18H19L20.8 12H7.9L6.1 18ZM6.1 18 7.9 12 6.1 18ZM4 8V6 8Z"),
FORMAT_LIST_BULLETED("M9 19V17H21V19H9ZM9 13V11H21V13H9ZM9 7V5H21V7H9ZM5 20Q4.175 20 3.5875 19.4125T3 18Q3 17.175 3.5875 16.5875T5 16Q5.825 16 6.4125 16.5875T7 18Q7 18.825 6.4125 19.4125T5 20ZM5 14Q4.175 14 3.5875 13.4125T3 12Q3 11.175 3.5875 10.5875T5 10Q5.825 10 6.4125 10.5875T7 12Q7 12.825 6.4125 13.4125T5 14ZM5 8Q4.175 8 3.5875 7.4125T3 6Q3 5.175 3.5875 4.5875T5 4Q5.825 4 6.4125 4.5875T7 6Q7 6.825 6.4125 7.4125T5 8Z"),
FORT("M1 21V17l2-2V9L1 7V3H3V5H5V3H7V5H9V3h2V7L9 9v1h6V9L13 7V3h2V5h2V3h2V5h2V3h2V7L21 9v6l2 2v4H14V18q0-.825-.5875-1.4125T12 16q-.825 0-1.4125.5875T10 18v3H1Zm2-2H8V18q0-1.65 1.175-2.825T12 14q1.65 0 2.825 1.175T16 18v1h5V17.825l-2-2V8.175L20.175 7h-4.35L17 8.175V12H7V8.175L8.175 7H3.825L5 8.175v7.65l-2 2V19Zm9-6Z"),
FOR_YOU("M12 12Q14.025 12 16.225 11.5875T20 10.5V20.5Q18.5 21.175 16.35 21.5875T12 22Q9.8 22 7.65 21.5875T4 20.5V10.5Q5.575 11.175 7.775 11.5875T12 12ZM18 19V13.25Q16.75 13.6 15.1125 13.8T12 14Q10.525 14 8.8875 13.8T6 13.25V19Q7.25 19.45 8.875 19.725T12 20Q13.5 20 15.125 19.725T18 19ZM12 2Q13.65 2 14.825 3.175T16 6Q16 7.65 14.825 8.825T12 10Q10.35 10 9.175 8.825T8 6Q8 4.35 9.175 3.175T12 2ZM12 8Q12.825 8 13.4125 7.4125T14 6Q14 5.175 13.4125 4.5875T12 4Q11.175 4 10.5875 4.5875T10 6Q10 6.825 10.5875 7.4125T12 8ZM12 6ZM12 16.625Z"),
GAMEPAD("M12 7.65ZM16.35 12ZM7.65 12ZM12 16.35ZM12 10.5 9 7.5V2H15V7.5L12 10.5ZM16.5 15 13.5 12 16.5 9H22V15H16.5ZM2 15V9H7.5L10.5 12 7.5 15H2ZM9 22V16.5L12 13.5 15 16.5V22H9ZM12 7.65 13 6.65V4H11V6.65L12 7.65ZM4 13H6.65L7.65 12 6.65 11H4V13ZM11 20H13V17.35L12 16.35 11 17.35V20ZM17.35 13H20V11H17.35L16.35 12 17.35 13Z"),
GLOBE_BOOK("M3.075 13Q3.05 12.75 3.0375 12.5T3.025 12Q3.025 10.125 3.725 8.4875T5.65 5.6375Q6.875 4.425 8.5 3.7125T12 3Q13.875 3 15.5125 3.7125T18.3625 5.6375Q19.575 6.85 20.2875 8.4875T21 12Q21 12.25 20.9875 12.5T20.95 13H18.925Q18.975 12.75 18.9875 12.5T19 12Q19 11.75 18.9875 11.5T18.925 11H15.975Q16 11.25 16 11.5V12.5Q16 12.75 15.975 13H14V12.175Q14 11.875 13.9875 11.575T13.95 11H10.075Q10.05 11.275 10.0375 11.575T10.025 12.175V13H8.05Q8.025 12.75 8.025 12.5V11.5Q8.025 11.25 8.05 11H5.1Q5.05 11.25 5.0375 11.5T5.025 12Q5.025 12.25 5.0375 12.5T5.1 13H3.075ZM5.7 9H8.275Q8.475 7.925 8.775 7.0625T9.425 5.5Q8.225 5.95 7.25 6.8625T5.7 9ZM10.35 9H13.65Q13.4 7.925 13.025 6.9T12 5Q11.35 5.875 10.9625 6.9T10.35 9ZM15.75 9H18.325Q17.75 7.775 16.7625 6.8625T14.575 5.5Q14.925 6.25 15.2375 7.0875T15.75 9ZM11 21V20Q11 18.75 10.125 17.875T8 17H2V15H8Q9.2 15 10.2375 15.525T12 17Q12.725 16.05 13.7625 15.525T16 15H22V17H16Q14.75 17 13.875 17.875T13 20V21H11Z"),
GRAPH2("M5 22q-1.25 0-2.125-.875T2 19q0-.975.5625-1.75T4 16.175V14q0-1.25.875-2.125T7 11h4V7.825q-.875-.3-1.4375-1.075T9 5q0-1.25.875-2.125T12 2t2.125.875T15 5q0 .975-.5625 1.75T13 7.825V11h4q1.25 0 2.125.875T20 14v2.175q.875.3 1.4375 1.075T22 19q0 1.25-.875 2.125T19 22t-2.125-.875T16 19q0-.975.5625-1.75T18 16.175V14q0-.425-.2875-.7125T17 13H13v3.175q.875.3 1.4375 1.075T15 19q0 1.25-.875 2.125T12 22t-2.125-.875T9 19q0-.975.5625-1.75T11 16.175V13H7q-.425 0-.7125.2875T6 14v2.175q.875.3 1.4375 1.075T8 19q0 1.25-.875 2.125T5 22Zm0-2q.425 0 .7125-.2875T6 19q0-.425-.2875-.7125T5 18q-.425 0-.7125.2875T4 19q0 .425.2875.7125T5 20Zm7 0q.425 0 .7125-.2875T13 19q0-.425-.2875-.7125T12 18q-.425 0-.7125.2875T11 19q0 .425.2875.7125T12 20Zm7 0q.425 0 .7125-.2875T20 19q0-.425-.2875-.7125T19 18q-.425 0-.7125.2875T18 19q0 .425.2875.7125T19 20ZM12 6q.425 0 .7125-.2875T13 5q0-.425-.2875-.7125T12 4q-.425 0-.7125.2875T11 5q0 .425.2875.7125T12 6Z"),
HELP("M11.95 18Q12.475 18 12.8375 17.6375T13.2 16.75Q13.2 16.225 12.8375 15.8625T11.95 15.5Q11.425 15.5 11.0625 15.8625T10.7 16.75Q10.7 17.275 11.0625 17.6375T11.95 18ZM11.05 14.15H12.9Q12.9 13.325 13.0875 12.85T14.15 11.55Q14.8 10.9 15.175 10.3125T15.55 8.9Q15.55 7.5 14.525 6.75T12.1 6Q10.675 6 9.7875 6.75T8.55 8.55L10.2 9.2Q10.325 8.75 10.7625 8.225T12.1 7.7Q12.9 7.7 13.3 8.1375T13.7 9.1Q13.7 9.6 13.4 10.0375T12.65 10.85Q11.55 11.825 11.3 12.325T11.05 14.15ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
HELP_FILL("M11.95 18q.525 0 .8875-.3625T13.2 16.75q0-.525-.3625-.8875T11.95 15.5q-.525 0-.8875.3625T10.7 16.75q0 .525.3625.8875T11.95 18Zm-.9-3.85H12.9q0-.825.1875-1.3t1.0625-1.3q.65-.65 1.025-1.2375T15.55 8.9q0-1.4-1.025-2.15T12.1 6q-1.425 0-2.3125.75T8.55 8.55l1.65.65q.125-.45.5625-.975T12.1 7.7q.8 0 1.2.4375t.4.9625q0 .5-.3.9375t-.75.8125q-1.1.975-1.35 1.475t-.25 1.825ZM12 22q-2.075 0-3.9-.7875T4.925 19.075q-1.35-1.35-2.1375-3.175T2 12q0-2.075.7875-3.9T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2q2.075 0 3.9.7875T19.075 4.925q1.35 1.35 2.1375 3.175T22 12q0 2.075-.7875 3.9T19.075 19.075q-1.35 1.35-3.175 2.1375T12 22Z"),
HOME("M6 19H9V13H15V19H18V10L12 5.5 6 10V19ZM4 21V9L12 3 20 9V21H13V15H11V21H4ZM12 12.25Z"),
HOST("M4 21Q3.175 21 2.5875 20.4125T2 19V5Q2 4.175 2.5875 3.5875T4 3H9Q9.825 3 10.4125 3.5875T11 5V19Q11 19.825 10.4125 20.4125T9 21H4ZM15 21Q14.175 21 13.5875 20.4125T13 19V5Q13 4.175 13.5875 3.5875T15 3H20Q20.825 3 21.4125 3.5875T22 5V19Q22 19.825 21.4125 20.4125T20 21H15ZM4 19H9V5H4V19ZM15 19H20V5H15V19ZM5 15H8V13H5V15ZM16 15H19V13H16V15ZM5 12H8V10H5V12ZM16 12H19V10H16V12ZM5 9H8V7H5V9ZM16 9H19V7H16V9ZM4 19H9 4ZM15 19H20 15Z"),
INFO("M11 17H13V11H11V17ZM12 9Q12.425 9 12.7125 8.7125T13 8Q13 7.575 12.7125 7.2875T12 7Q11.575 7 11.2875 7.2875T11 8Q11 8.425 11.2875 8.7125T12 9ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
INFO_FILL("M12 22q2.075-0 3.9-.7875t3.175-2.1375q1.35-1.35 2.1375-3.175T22 12q-0-2.075-.7875-3.9T19.075 4.925q-1.35-1.35-3.175-2.1375T12 2q-2.075 0-3.9.7875T4.925 4.925Q3.575 6.275 2.7875 8.1T2 12q0 2.075.7875 3.9T4.925 19.075q1.35 1.35 3.175 2.1375T12 22ZM12 9q-.425 0-.7125-.2875T11 8t.2875-.7125T12 7q.425-0 .7125.2875T13 8t-.2875.7125T12 9Zm-1 8V11h2v6H11Z"),
KEYBOARD_ARROW_DOWN("M12 15.4 6 9.4 7.4 8 12 12.6 16.6 8 18 9.4 12 15.4Z"),
KEYBOARD_ARROW_UP("M12 10.8 7.4 15.4 6 14 12 8 18 14 16.6 15.4 12 10.8Z"),
LANDSCAPE("M1 18l6-8 4.5 6H19L14 9.35l-2.5 3.3L10.25 11 14 6l9 12H1Zm13.025-2ZM5 16H9L7 13.325 5 16ZH9 5Z"),
LIST("M7 9V7H21V9H7ZM7 13V11H21V13H7ZM7 17V15H21V17H7ZM4 9Q3.575 9 3.2875 8.7125T3 8Q3 7.575 3.2875 7.2875T4 7Q4.425 7 4.7125 7.2875T5 8Q5 8.425 4.7125 8.7125T4 9ZM4 13Q3.575 13 3.2875 12.7125T3 12Q3 11.575 3.2875 11.2875T4 11Q4.425 11 4.7125 11.2875T5 12Q5 12.425 4.7125 12.7125T4 13ZM4 17Q3.575 17 3.2875 16.7125T3 16Q3 15.575 3.2875 15.2875T4 15Q4.425 15 4.7125 15.2875T5 16Q5 16.425 4.7125 16.7125T4 17Z"),
LISTS("M2 20V16H6V20H2ZM8 20V16H22V20H8ZM2 14V10H6V14H2ZM8 14V10H22V14H8ZM2 8V4H6V8H2ZM8 8V4H22V8H8Z"),
LOCAL_CAFE("M4 21V19H20V21H4ZM8 17Q6.35 17 5.175 15.825T4 13V3H20Q20.825 3 21.4125 3.5875T22 5V8Q22 8.825 21.4125 9.4125T20 10H18V13Q18 14.65 16.825 15.825T14 17H8ZM8 15H14Q14.825 15 15.4125 14.4125T16 13V5H6V13Q6 13.825 6.5875 14.4125T8 15ZM18 8H20V5H18V8ZM8 15H6 16 8Z"),
LOCAL_CAFE_FILL("M4 21V19H20v2H4Zm4-4q-1.65 0-2.825-1.175T4 13V3H20q.825 0 1.4125.5875T22 5V8q0 .825-.5875 1.4125T20 10H18v3q0 1.65-1.175 2.825T14 17H8ZM18 8h2V5H18V8Z"),
LOCATION_CITY("M3 21V7H9V5l3-3 3 3v6h6V21H3Zm2-2H7V17H5v2Zm0-4H7V13H5v2Zm0-4H7V9H5v2Zm6 8h2V17H11v2Zm0-4h2V13H11v2Zm0-4h2V9H11v2Zm0-4h2V5H11V7Zm6 12h2V17H17v2Zm0-4h2V13H17v2Z"),
MENU("M3 18V16H21V18H3ZM3 13V11H21V13H3ZM3 8V6H21V8H3Z"),
MICROSOFT("M4 20H22v2H4V13H20v7h2V4H20v7H4V4h7V20h2V4h9V2H2V22H4"), // Not Material
MINIMIZE("M6 21V19H18V21H6Z"),
MOJANG("M13.9658 0C12.9552.828 12.7686 2.195 12.7007 3.418 12.7219 4.4201 14.0423 4.7174 14.6538 4.0082 15.0912 2.6579 14.3692 1.2739 13.9658 0ZM10.913 2.9297C10.7559 3.6983 10.6284 4.4669 10.4925 5.2355 8.9894 3.9913 7.1505 3.0825 5.142 3.2948 3.4944 3.4646.9429 2.6961.1404 4.6452-.1229 8.314.0722 12.0124.034 15.6896-.0891 16.9126.8957 18.1101 2.1781 17.9699 6.3989 18.0039 10.6283 18.0172 14.8491 17.9661 16.1145 18.102 16.8067 16.9554 16.9851 15.8768 13.2696 16.4374 9.1761 16.9296 5.7111 15.1292 2.5986 13.5836 2.246 8.3139 5.5581 6.798 9.3203 5.1759 13.8596 8.0383 14.9424 11.7877 15.6133 12.1105 16.2844 12.4247 16.9596 12.7432 16.6241 10.3483 16.6537 7.8005 15.5793 5.5882 15.0571 4.7474 14.0975 6.2714 13.3799 5.6217 12.416 4.8659 11.7495 3.8129 10.913 2.9297Z"), // Not Material
MORE_HORIZ("M6 14Q5.175 14 4.5875 13.4125T4 12Q4 11.175 4.5875 10.5875T6 10Q6.825 10 7.4125 10.5875T8 12Q8 12.825 7.4125 13.4125T6 14ZM12 14Q11.175 14 10.5875 13.4125T10 12Q10 11.175 10.5875 10.5875T12 10Q12.825 10 13.4125 10.5875T14 12Q14 12.825 13.4125 13.4125T12 14ZM18 14Q17.175 14 16.5875 13.4125T16 12Q16 11.175 16.5875 10.5875T18 10Q18.825 10 19.4125 10.5875T20 12Q20 12.825 19.4125 13.4125T18 14Z"),
MORE_VERT("M12 20Q11.175 20 10.5875 19.4125T10 18Q10 17.175 10.5875 16.5875T12 16Q12.825 16 13.4125 16.5875T14 18Q14 18.825 13.4125 19.4125T12 20ZM12 14Q11.175 14 10.5875 13.4125T10 12Q10 11.175 10.5875 10.5875T12 10Q12.825 10 13.4125 10.5875T14 12Q14 12.825 13.4125 13.4125T12 14ZM12 8Q11.175 8 10.5875 7.4125T10 6Q10 5.175 10.5875 4.5875T12 4Q12.825 4 13.4125 4.5875T14 6Q14 6.825 13.4125 7.4125T12 8Z"),
OPEN_IN_NEW("M5 21Q4.175 21 3.5875 20.4125T3 19V5Q3 4.175 3.5875 3.5875T5 3H12V5H5V19H19V12H21V19Q21 19.825 20.4125 20.4125T19 21H5ZM9.7 15.7 8.3 14.3 17.6 5H14V3H21V10H19V6.4L9.7 15.7Z"),
OUTPUT("M5 21Q4.175 21 3.5875 20.4125T3 19V5Q3 4.175 3.5875 3.5875T5 3H19Q19.825 3 20.4125 3.5875T21 5V7H19V5H5V19H19V17H21V19Q21 19.825 20.4125 20.4125T19 21H5ZM17 17 15.6 15.6 18.175 13H9V11H18.175L15.6 8.4 17 7 22 12 17 17Z"),
PACKAGE("M10 9.75 12 8.75 14 9.75V5H10V9.75ZM7 17V15H12V17H7ZM5 21Q4.175 21 3.5875 20.4125T3 19V5Q3 4.175 3.5875 3.5875T5 3H19Q19.825 3 20.4125 3.5875T21 5V19Q21 19.825 20.4125 20.4125T19 21H5ZM5 5V19 5ZM5 19H19V5H16V13L12 11 8 13V5H5V19Z"),
PACKAGE2("M11 19.425V12.575L5 9.1V15.95L11 19.425ZM13 19.425 19 15.95V9.1L13 12.575V19.425ZM11 21.725 4 17.7Q3.525 17.425 3.2625 16.975T3 15.975V8.025Q3 7.475 3.2625 7.025T4 6.3L11 2.275Q11.475 2 12 2T13 2.275L20 6.3Q20.475 6.575 20.7375 7.025T21 8.025V15.975Q21 16.525 20.7375 16.975T20 17.7L13 21.725Q12.525 22 12 22T11 21.725ZM16 8.525 17.925 7.425 12 4 10.05 5.125 16 8.525ZM12 10.85 13.95 9.725 8.025 6.3 6.075 7.425 12 10.85Z"),
PACKAGE2_FILL("M11 21.725v-9.15L3 7.95v8.025q0 .55.2625 1T4 17.7l7 4.025Zm2 0L20 17.7q.475-.275.7375-.725t.2625-1V7.95l-8 4.625v9.15Zm3.975-13.75 2.95-1.725L13 2.275Q12.525 2 12 2t-1 .275L9.025 3.4l7.95 4.575ZM12 10.85l2.975-1.7L7.05 4.55l-3 1.725L12 10.85Z"),
PERSON("M12 12Q10.35 12 9.175 10.825T8 8Q8 6.35 9.175 5.175T12 4Q13.65 4 14.825 5.175T16 8Q16 9.65 14.825 10.825T12 12ZM4 20V17.2Q4 16.35 4.4375 15.6375T5.6 14.55Q7.15 13.775 8.75 13.3875T12 13Q13.65 13 15.25 13.3875T18.4 14.55Q19.125 14.925 19.5625 15.6375T20 17.2V20H4ZM6 18H18V17.2Q18 16.925 17.8625 16.7T17.5 16.35Q16.15 15.675 14.775 15.3375T12 15Q10.6 15 9.225 15.3375T6.5 16.35Q6.275 16.475 6.1375 16.7T6 17.2V18ZM12 10Q12.825 10 13.4125 9.4125T14 8Q14 7.175 13.4125 6.5875T12 6Q11.175 6 10.5875 6.5875T10 8Q10 8.825 10.5875 9.4125T12 10ZM12 8ZM12 18Z"),
PUBLIC("M12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM11 19.95V18Q10.175 18 9.5875 17.4125T9 16V15L4.2 10.2Q4.125 10.65 4.0625 11.1T4 12Q4 15.025 5.9875 17.3T11 19.95ZM17.9 17.4Q18.925 16.275 19.4625 14.8875T20 12Q20 9.55 18.6375 7.525T15 4.6V5Q15 5.825 14.4125 6.4125T13 7H11V9Q11 9.425 10.7125 9.7125T10 10H8V12H14Q14.425 12 14.7125 12.2875T15 13V16H16Q16.65 16 17.175 16.3875T17.9 17.4Z"),
REFRESH("M12 20Q8.65 20 6.325 17.675T4 12Q4 8.65 6.325 6.325T12 4Q13.725 4 15.3 4.7125T18 6.75V4H20V11H13V9H17.2Q16.4 7.6 15.0125 6.8T12 6Q9.5 6 7.75 7.75T6 12Q6 14.5 7.75 16.25T12 18Q13.925 18 15.475 16.9T17.65 14H19.75Q19.05 16.65 16.9 18.325T12 20Z"),
RELEASE_CIRCLE("M9,7H13A2,2 0 0,1 15,9V11C15,11.84 14.5,12.55 13.76,12.85L15,17H13L11.8,13H11V17H9V7M11,9V11H13V9H11M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,16.41 7.58,20 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z"), // Not Material
RESTORE("M12 21Q8.55 21 5.9875 18.7125T3.05 13H5.1Q5.45 15.6 7.4125 17.3T12 19Q14.925 19 16.9625 16.9625T19 12Q19 9.075 16.9625 7.0375T12 5Q10.275 5 8.775 5.8T6.25 8H9V10H3V4H5V6.35Q6.275 4.75 8.1125 3.875T12 3Q13.875 3 15.5125 3.7125T18.3625 5.6375Q19.575 6.85 20.2875 8.4875T21 12Q21 13.875 20.2875 15.5125T18.3625 18.3625Q17.15 19.575 15.5125 20.2875T12 21Z"), // Not Material
ROCKET_LAUNCH("M5.65 10.025 7.6 10.85Q7.95 10.15 8.325 9.5T9.15 8.2L7.75 7.925 5.65 10.025ZM9.2 12.1 12.05 14.925Q13.1 14.525 14.3 13.7T16.55 11.825Q18.3 10.075 19.2875 7.9375T20.15 4Q18.35 3.875 16.2 4.8625T12.3 7.6Q11.25 8.65 10.425 9.85T9.2 12.1ZM13.65 10.475Q13.075 9.9 13.075 9.0625T13.65 7.65Q14.225 7.075 15.075 7.075T16.5 7.65Q17.075 8.225 17.075 9.0625T16.5 10.475Q15.925 11.05 15.075 11.05T13.65 10.475ZM14.125 18.5 16.225 16.4 15.95 15Q15.3 15.45 14.65 15.8125T13.3 16.525L14.125 18.5ZM21.95 2.175Q22.425 5.2 21.3625 8.0625T17.7 13.525L18.2 16Q18.3 16.5 18.15 16.975T17.65 17.8L13.45 22 11.35 17.075 7.075 12.8 2.15 10.7 6.325 6.5Q6.675 6.15 7.1625 6T8.15 5.95L10.625 6.45Q13.225 3.85 16.075 2.775T21.95 2.175ZM3.925 15.975Q4.8 15.1 6.0625 15.0875T8.2 15.95Q9.075 16.825 9.0625 18.0875T8.175 20.225Q7.55 20.85 6.0875 21.3T2.05 22.1Q2.4 19.525 2.85 18.0625T3.925 15.975ZM5.35 17.375Q5.1 17.625 4.85 18.2875T4.5 19.625Q5.175 19.525 5.8375 19.2875T6.75 18.8Q7.05 18.5 7.075 18.075T6.8 17.35Q6.5 17.05 6.075 17.0625T5.35 17.375Z"),
SCHEMA("M4 23V17H6.5V15H4V9H6.5V7H4V1h7V7H8.5V9H11v2h3V9h7v6H14V13H11v2H8.5v2H11v6H4Zm2-2H9V19H6v2Zm0-8H9V11H6v2Zm10 0h3V11H16v2ZM6 5H9V3H6V5ZM7.5 4Zm0 8Zm10 0Zm-10 8Z"),
SCHEMA_FILL("M4 23V17H6.5V15H4V9H6.5V7H4V1h7V7H8.5V9H11v2h3V9h7v6H14V13H11v2H8.5v2H11v6H4Z"),
SCREENSHOT_MONITOR("M15 16H19V12H17.5V14.5H15V16ZM5 10H6.5V7.5H9V6H5V10ZM8 21V19H4Q3.175 19 2.5875 18.4125T2 17V5Q2 4.175 2.5875 3.5875T4 3H20Q20.825 3 21.4125 3.5875T22 5V17Q22 17.825 21.4125 18.4125T20 19H16V21H8ZM4 17H20V5H4V17ZM4 17V5 17Z"),
SCRIPT("M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z"), // Not Material
SEARCH("M19.6 21 13.3 14.7Q12.55 15.3 11.575 15.65T9.5 16Q6.775 16 4.8875 14.1125T3 9.5Q3 6.775 4.8875 4.8875T9.5 3Q12.225 3 14.1125 4.8875T16 9.5Q16 10.6 15.65 11.575T14.7 13.3L21 19.6 19.6 21ZM9.5 14Q11.375 14 12.6875 12.6875T14 9.5Q14 7.625 12.6875 6.3125T9.5 5Q7.625 5 6.3125 6.3125T5 9.5Q5 11.375 6.3125 12.6875T9.5 14Z"),
SELECT_ALL("M7 17V7H17V17H7ZM9 15H15V9H9V15ZM5 19V21Q4.175 21 3.5875 20.4125T3 19H5ZM3 17V15H5V17H3ZM3 13V11H5V13H3ZM3 9V7H5V9H3ZM5 5H3Q3 4.175 3.5875 3.5875T5 3V5ZM7 21V19H9V21H7ZM7 5V3H9V5H7ZM11 21V19H13V21H11ZM11 5V3H13V5H11ZM15 21V19H17V21H15ZM15 5V3H17V5H15ZM19 21V19H21Q21 19.825 20.4125 20.4125T19 21ZM19 17V15H21V17H19ZM19 13V11H21V13H19ZM19 9V7H21V9H19ZM19 5V3Q19.825 3 20.4125 3.5875T21 5H19Z"),
SETTINGS("M19.43 12.98C19.47 12.66 19.5 12.34 19.5 12 19.5 11.66 19.47 11.34 19.43 11.02L21.54 9.37C21.73 9.22 21.78 8.95 21.66 8.73L19.66 5.27C19.57 5.11 19.4 5.02 19.22 5.02 19.16 5.02 19.1 5.03 19.05 5.05L16.56 6.05C16.04 5.65 15.48 5.32 14.87 5.07L14.49 2.42C14.46 2.18 14.25 2 14 2H10C9.75 2 9.54 2.18 9.51 2.42L9.13 5.07C8.52 5.32 7.96 5.66 7.44 6.05L4.95 5.05C4.89 5.03 4.83 5.02 4.77 5.02 4.6 5.02 4.43 5.11 4.34 5.27L2.34 8.73C2.21 8.95 2.27 9.22 2.46 9.37L4.57 11.02C4.53 11.34 4.5 11.67 4.5 12 4.5 12.33 4.53 12.66 4.57 12.98L2.46 14.63C2.27 14.78 2.22 15.05 2.34 15.27L4.34 18.73C4.43 18.89 4.6 18.98 4.78 18.98 4.84 18.98 4.9 18.97 4.95 18.95L7.44 17.95C7.96 18.35 8.52 18.68 9.13 18.93L9.51 21.58C9.54 21.82 9.75 22 10 22H14C14.25 22 14.46 21.82 14.49 21.58L14.87 18.93C15.48 18.68 16.04 18.34 16.56 17.95L19.05 18.95C19.11 18.97 19.17 18.98 19.23 18.98 19.4 18.98 19.57 18.89 19.66 18.73L21.66 15.27C21.78 15.05 21.73 14.78 21.54 14.63L19.43 12.98ZM17.45 11.27C17.49 11.58 17.5 11.79 17.5 12 17.5 12.21 17.48 12.43 17.45 12.73L17.31 13.86 18.2 14.56 19.28 15.4 18.58 16.61 17.31 16.1 16.27 15.68 15.37 16.36C14.94 16.68 14.53 16.92 14.12 17.09L13.06 17.52 12.9 18.65 12.7 20H11.3L11.11 18.65 10.95 17.52 9.89 17.09C9.46 16.91 9.06 16.68 8.66 16.38L7.75 15.68 6.69 16.11 5.42 16.62 4.72 15.41 5.8 14.57 6.69 13.87 6.55 12.74C6.52 12.43 6.5 12.2 6.5 12S6.52 11.57 6.55 11.27L6.69 10.14 5.8 9.44 4.72 8.6 5.42 7.39 6.69 7.9 7.73 8.32 8.63 7.64C9.06 7.32 9.47 7.08 9.88 6.91L10.94 6.48 11.1 5.35 11.3 4H12.69L12.88 5.35 13.04 6.48 14.1 6.91C14.53 7.09 14.93 7.32 15.33 7.62L16.24 8.32 17.3 7.89 18.57 7.38 19.27 8.59 18.2 9.44 17.31 10.14 17.45 11.27ZM12 8C9.79 8 8 9.79 8 12S9.79 16 12 16 16 14.21 16 12 14.21 8 12 8ZM12 14C10.9 14 10 13.1 10 12S10.9 10 12 10 14 10.9 14 12 13.1 14 12 14Z"), // Material Icons
SETTINGS_FILL("M9.25 22l-.4-3.2q-.325-.125-.6125-.3t-.5625-.375L4.7 19.375l-2.75-4.75 2.575-1.95Q4.5 12.5 4.5 12.3375v-.675q0-.1625.025-.3375L1.95 9.375 4.7 4.625l2.975 1.25q.275-.2.575-.375t.6-.3L9.25 2h5.5l.4 3.2q.325.125.6125.3t.5625.375L19.3 4.625l2.75 4.75-2.575 1.95q.025.175.025.3375v.675q0 .1625-.05.3375l2.575 1.95-2.75 4.75-2.95-1.25q-.275.2-.575.375t-.6.3l-.4 3.2H9.25Zm2.8-6.5q1.45 0 2.475-1.025T15.55 12 14.525 9.525 12.05 8.5q-1.475 0-2.4875 1.025T8.55 12q0 1.45 1.0125 2.475T12.05 15.5Z"), // Material Icons
STADIA_CONTROLLER("M4.725 20Q3.225 20 2.1625 18.925T1.05 16.325Q1.05 16.1 1.075 15.875T1.15 15.425L3.25 7.025Q3.6 5.675 4.675 4.8375T7.125 4H16.875Q18.25 4 19.325 4.8375T20.75 7.025L22.85 15.425Q22.9 15.65 22.9375 15.8875T22.975 16.35Q22.975 17.875 21.8875 18.9375T19.275 20Q18.225 20 17.325 19.45T15.975 17.95L15.275 16.5Q15.15 16.25 14.9 16.125T14.375 16H9.625Q9.35 16 9.1 16.125T8.725 16.5L8.025 17.95Q7.575 18.9 6.675 19.45T4.725 20ZM4.8 18Q5.275 18 5.6625 17.75T6.25 17.075L6.95 15.65Q7.325 14.875 8.05 14.4375T9.625 14H14.375Q15.225 14 15.95 14.45T17.075 15.65L17.775 17.075Q17.975 17.5 18.3625 17.75T19.225 18Q19.925 18 20.425 17.5375T20.95 16.375Q20.95 16.4 20.9 15.9L18.8 7.525Q18.625 6.85 18.1 6.425T16.875 6H7.125Q6.425 6 5.8875 6.425T5.2 7.525L3.1 15.9Q3.05 16.05 3.05 16.35 3.05 17.05 3.5625 17.525T4.8 18ZM13.5 11Q13.925 11 14.2125 10.7125T14.5 10Q14.5 9.575 14.2125 9.2875T13.5 9Q13.075 9 12.7875 9.2875T12.5 10Q12.5 10.425 12.7875 10.7125T13.5 11ZM15.5 9Q15.925 9 16.2125 8.7125T16.5 8Q16.5 7.575 16.2125 7.2875T15.5 7Q15.075 7 14.7875 7.2875T14.5 8Q14.5 8.425 14.7875 8.7125T15.5 9ZM15.5 13Q15.925 13 16.2125 12.7125T16.5 12Q16.5 11.575 16.2125 11.2875T15.5 11Q15.075 11 14.7875 11.2875T14.5 12Q14.5 12.425 14.7875 12.7125T15.5 13ZM17.5 11Q17.925 11 18.2125 10.7125T18.5 10Q18.5 9.575 18.2125 9.2875T17.5 9Q17.075 9 16.7875 9.2875T16.5 10Q16.5 10.425 16.7875 10.7125T17.5 11ZM8.5 12.5Q8.825 12.5 9.0375 12.2875T9.25 11.75V10.75H10.25Q10.575 10.75 10.7875 10.5375T11 10Q11 9.675 10.7875 9.4625T10.25 9.25H9.25V8.25Q9.25 7.925 9.0375 7.7125T8.5 7.5Q8.175 7.5 7.9625 7.7125T7.75 8.25V9.25H6.75Q6.425 9.25 6.2125 9.4625T6 10Q6 10.325 6.2125 10.5375T6.75 10.75H7.75V11.75Q7.75 12.075 7.9625 12.2875T8.5 12.5ZM12 12Z"),
STADIA_CONTROLLER_FILL("M4.725 20q-1.5 0-2.5625-1.075T1.05 16.325q0-.225.025-.45t.075-.45l2.1-8.4q.35-1.35 1.425-2.1875T7.125 4h9.75q1.375 0 2.45.8375T20.75 7.025l2.1 8.4q.05.225.0875.4625t.0375.4625q0 1.525-1.0875 2.5875T19.275 20q-1.05 0-1.95-.55t-1.35-1.5l-.7-1.45q-.125-.25-.375-.375T14.375 16H9.625q-.275 0-.525.125t-.375.375l-.7 1.45q-.45.95-1.35 1.5T4.725 20ZM13.5 11q.425 0 .7125-.2875T14.5 10t-.2875-.7125T13.5 9t-.7125.2875T12.5 10t.2875.7125T13.5 11Zm2-2q.425 0 .7125-.2875T16.5 8q0-.425-.2875-.7125T15.5 7q-.425 0-.7125.2875T14.5 8t.2875.7125T15.5 9Zm0 4q.425 0 .7125-.2875T16.5 12q0-.425-.2875-.7125T15.5 11q-.425 0-.7125.2875T14.5 12t.2875.7125T15.5 13Zm2-2q.425 0 .7125-.2875T18.5 10q0-.425-.2875-.7125T17.5 9q-.425 0-.7125.2875T16.5 10q0 .425.2875.7125T17.5 11Zm-9 1.5q.325 0 .5375-.2125T9.25 11.75v-1h1q.325 0 .5375-.2125T11 10t-.2125-.5375T10.25 9.25h-1v-1q0-.325-.2125-.5375T8.5 7.5q-.325 0-.5375.2125T7.75 8.25v1h-1q-.325 0-.5375.2125T6 10q0 .325.2125.5375T6.75 10.75h1v1q0 .325.2125.5375T8.5 12.5Z"),
STYLE("M3.975 19.8 3.125 19.45Q2.35 19.125 2.0875 18.325T2.175 16.75L3.975 12.85V19.8ZM7.975 22Q7.15 22 6.5625 21.4125T5.975 20V14L8.625 21.35Q8.7 21.525 8.775 21.6875T8.975 22H7.975ZM13.125 21.9Q12.325 22.2 11.575 21.825T10.525 20.65L6.075 8.45Q5.775 7.65 6.125 6.8875T7.275 5.85L14.825 3.1Q15.625 2.8 16.375 3.175T17.425 4.35L21.875 16.55Q22.175 17.35 21.825 18.1125T20.675 19.15L13.125 21.9ZM10.975 10Q11.4 10 11.6875 9.7125T11.975 9Q11.975 8.575 11.6875 8.2875T10.975 8Q10.55 8 10.2625 8.2875T9.975 9Q9.975 9.425 10.2625 9.7125T10.975 10ZM12.425 20 19.975 17.25 15.525 5 7.975 7.75 12.425 20ZM7.975 7.75 15.525 5 7.975 7.75Z"),
STYLE_FILL("M3.975 19.8l-.85-.35q-.775-.325-1.0375-1.125T2.175 16.75l1.8-3.9V19.8Zm4 2.2q-.825 0-1.4125-.5875T5.975 20V14l2.65 7.35q.075.175.15.3375t.2.3125h-1Zm5.15-.1q-.8.3-1.55-.075t-1.05-1.175L6.075 8.45q-.3-.8.05-1.5625T7.275 5.85l7.55-2.75q.8-.3 1.55.075t1.05 1.175l4.45 12.2q.3.8-.05 1.5625T20.675 19.15l-7.55 2.75ZM10.975 10q.425 0 .7125-.2875T11.975 9q0-.425-.2875-.7125T10.975 8t-.7125.2875T9.975 9t.2875.7125T10.975 10Z"),
TEXTURE("M4.4-3Q3.925-3.1 3.5125-3.5125T3-4.4L19.6-21Q20.125-20.875 20.5-20.4875T21.025-19.6L4.4-3ZM3-9.3V-12.1L11.9-21H14.7L3-9.3ZM3-17V-19Q3-19.825 3.5875-20.4125T5-21H7L3-17ZM17-3 21-7V-5Q21-4.175 20.4125-3.5875T19-3H17ZM9.3-3 21-14.7V-11.9L12.1-3H9.3Z"),
TRIP("M4 21Q3.175 21 2.5875 20.4125T2 19V8Q2 7.175 2.5875 6.5875T4 6H8V4Q8 3.175 8.5875 2.5875T10 2H14Q14.825 2 15.4125 2.5875T16 4V6H20Q20.825 6 21.4125 6.5875T22 8V19Q22 19.825 21.4125 20.4125T20 21H4ZM10 6H14V4H10V6ZM6 8H4V19H6V8ZM16 19V8H8V19H16ZM18 8V19H20V8H18ZM12 13.5Z"),
TUNE("M11 21V15H13V17H21V19H13V21H11ZM3 19V17H9V19H3ZM7 15V13H3V11H7V9H9V15H7ZM11 13V11H21V13H11ZM15 9V3H17V5H21V7H17V9H15ZM3 7V5H13V7H3Z"),
UPDATE("M12 21Q10.125 21 8.4875 20.2875T5.6375 18.3625Q4.425 17.15 3.7125 15.5125T3 12Q3 10.125 3.7125 8.4875T5.6375 5.6375Q6.85 4.425 8.4875 3.7125T12 3Q14.05 3 15.8875 3.875T19 6.35V4H21V10H15V8H17.75Q16.725 6.6 15.225 5.8T12 5Q9.075 5 7.0375 7.0375T5 12Q5 14.925 7.0375 16.9625T12 19Q14.625 19 16.5875 17.3T18.9 13H20.95Q20.575 16.425 18.0125 18.7125T12 21ZM14.8 16.2 11 12.4V7H13V11.6L16.2 14.8 14.8 16.2Z"),
VISIBILITY("M12 16q1.875 0 3.1875-1.3125T16.5 11.5 15.1875 8.3125 12 7 8.8125 8.3125 7.5 11.5t1.3125 3.1875T12 16Zm0-1.8q-1.125 0-1.9125-.7875T9.3 11.5t.7875-1.9125T12 8.8q1.125 0 1.9125.7875T14.7 11.5q0 1.125-.7875 1.9125T12 14.2ZM12 19q-3.65 0-6.65-2.0375T1 11.5Q2.35 8.075 5.35 6.0375T12 4q3.65 0 6.65 2.0375T23 11.5q-1.35 3.425-4.35 5.4625T12 19Zm0-7.5ZM12 17q2.825 0 5.1875-1.4875T20.8 11.5q-1.25-2.525-3.6125-4.0125T12 6 6.8125 7.4875 3.2 11.5q1.25 2.525 3.6125 4.0125T12 17Z"),
VISIBILITY_OFF("M16.1 13.3l-1.45-1.45q.225-1.175-.675-2.2t-2.325-.8L10.2 7.4q.425-.2.8625-.3T12 7q1.875 0 3.1875 1.3125T16.5 11.5q0 .5-.1.9375t-.3.8625Zm3.2 3.15-1.45-1.4q.95-.725 1.6875-1.5875T20.8 11.5q-1.25-2.525-3.5875-4.0125T12 6q-.725 0-1.425.1T9.2 6.4L7.65 4.85q1.025-.425 2.1-.6375T12 4q3.775 0 6.725 2.0875T23 11.5q-.575 1.475-1.5125 2.7375T19.3 16.45Zm.5 6.15-4.2-4.15q-.875.275-1.7625.4125T12 19q-3.775 0-6.725-2.0875T1 11.5q.525-1.325 1.325-2.4625T4.15 7L1.4 4.2 2.8 2.8 21.2 21.2l-1.4 1.4ZM5.55 8.4q-.725.65-1.325 1.425T3.2 11.5q1.25 2.525 3.5875 4.0125T12 17q.5 0 .975-.0625T13.95 16.8l-.9-.95q-.275.075-.525.1125T12 16q-1.875 0-3.1875-1.3125T7.5 11.5q0-.275.0375-.525T7.65 10.45L5.55 8.4Zm7.975 2.325ZM9.75 12.6Z"),
WARNING("M1 21 12 2 23 21H1ZM4.45 19H19.55L12 6 4.45 19ZM12 18Q12.425 18 12.7125 17.7125T13 17Q13 16.575 12.7125 16.2875T12 16Q11.575 16 11.2875 16.2875T11 17Q11 17.425 11.2875 17.7125T12 18ZM11 15H13V10H11V15ZM12 12.5Z"),
WB_SUNNY("M11 4V1H13V4H11ZM11 23V20H13V23H11ZM20 13V11H23V13H20ZM1 13V11H4V13H1ZM18.7 6.7 17.3 5.3 19.05 3.5 20.5 4.95 18.7 6.7ZM4.95 20.5 3.5 19.05 5.3 17.3 6.7 18.7 4.95 20.5ZM19.05 20.5 17.3 18.7 18.7 17.3 20.5 19.05 19.05 20.5ZM5.3 6.7 3.5 4.95 4.95 3.5 6.7 5.3 5.3 6.7ZM12 18Q9.5 18 7.75 16.25T6 12Q6 9.5 7.75 7.75T12 6Q14.5 6 16.25 7.75T18 12Q18 14.5 16.25 16.25T12 18ZM12 16Q13.675 16 14.8375 14.8375T16 12Q16 10.325 14.8375 9.1625T12 8Q10.325 8 9.1625 9.1625T8 12Q8 13.675 9.1625 14.8375T12 16ZM12 12Z"),
WB_SUNNY_FILL("M11 4V1h2V4H11Zm0 19V20h2v3H11Zm9-10V11h3v2H20ZM1 13V11H4v2H1ZM18.7 6.7 17.3 5.3l1.75-1.8L20.5 4.95 18.7 6.7ZM4.95 20.5 3.5 19.05 5.3 17.3l1.4 1.4-1.75 1.8Zm14.1 0-1.75-1.8 1.4-1.4 1.8 1.75-1.45 1.45ZM5.3 6.7 3.5 4.95 4.95 3.5 6.7 5.3 5.3 6.7ZM12 18q-2.5 0-4.25-1.75T6 12 7.75 7.75 12 6t4.25 1.75T18 12t-1.75 4.25T12 18Z"),
;
public static final double DEFAULT_SIZE = 24;
private final String path;
@@ -124,39 +140,38 @@ public enum SVG {
return path;
}
private static Node createIcon(SVGPath path, double width, double height) {
if (width < 0 || height < 0) {
private static Node createIcon(SVGPath path, double size) {
if (size < 0) {
StackPane pane = new StackPane(path);
pane.setAlignment(Pos.CENTER);
return pane;
}
Group svg = new Group(path);
double scale = Math.min(width / 24, height / 24);
svg.setScaleX(scale);
svg.setScaleY(scale);
return svg;
double scale = size / 24;
path.setScaleX(scale);
path.setScaleY(scale);
return new Group(path);
}
public Node createIcon(ObservableValue<? extends Paint> fill, double width, double height) {
public Node createIcon(double size) {
SVGPath p = new SVGPath();
p.getStyleClass().add("svg");
p.setContent(path);
if (fill != null)
p.fillProperty().bind(fill);
return createIcon(p, width, height);
p.getStyleClass().add("svg");
return createIcon(p, size);
}
public Node createIcon(Paint fill, double width, double height) {
public Node createIcon() {
SVGPath p = new SVGPath();
p.getStyleClass().add("svg");
p.setContent(path);
if (fill != null)
p.fillProperty().set(fill);
return createIcon(p, width, height);
p.getStyleClass().add("svg");
return createIcon(p, -1);
}
public Node createIcon(ObservableValue<? extends Paint> color) {
SVGPath p = new SVGPath();
p.setContent(path);
p.getStyleClass().add("svg");
p.fillProperty().bind(color);
return createIcon(p, -1);
}
}
@@ -25,14 +25,12 @@ import javafx.animation.Animation.Status;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.skin.VirtualFlow;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.util.Duration;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import org.jackhuang.hmcl.util.Holder;
/**
* Utility class for ScrollPanes.
@@ -53,8 +51,8 @@ final class ScrollUtils {
}
}
private ScrollUtils() {
}
private static final double DEFAULT_SPEED = 1.0;
private static final double DEFAULT_TRACK_PAD_ADJUSTMENT = 7.0;
/**
* Determines if the given ScrollEvent comes from a trackpad.
@@ -69,16 +67,10 @@ final class ScrollUtils {
* @see ScrollEvent#getDeltaY()
*/
public static boolean isTrackPad(ScrollEvent event, ScrollDirection scrollDirection) {
switch (scrollDirection) {
case UP:
case DOWN:
return Math.abs(event.getDeltaY()) < 10;
case LEFT:
case RIGHT:
return Math.abs(event.getDeltaX()) < 10;
default:
return false;
}
return switch (scrollDirection) {
case UP, DOWN -> Math.abs(event.getDeltaY()) < 10;
case LEFT, RIGHT -> Math.abs(event.getDeltaX()) < 10;
};
}
/**
@@ -118,7 +110,7 @@ final class ScrollUtils {
* default speed value of 1.
*/
public static void addSmoothScrolling(ScrollPane scrollPane) {
addSmoothScrolling(scrollPane, 1);
addSmoothScrolling(scrollPane, DEFAULT_SPEED);
}
/**
@@ -127,7 +119,7 @@ final class ScrollUtils {
* with a default trackPadAdjustment of 7.
*/
public static void addSmoothScrolling(ScrollPane scrollPane, double speed) {
addSmoothScrolling(scrollPane, speed, 7);
addSmoothScrolling(scrollPane, speed, DEFAULT_TRACK_PAD_ADJUSTMENT);
}
/**
@@ -142,23 +134,38 @@ final class ScrollUtils {
smoothScroll(scrollPane, speed, trackPadAdjustment);
}
/// @author Glavo
public static void addSmoothScrolling(VirtualFlow<?> virtualFlow) {
addSmoothScrolling(virtualFlow, DEFAULT_SPEED);
}
/// @author Glavo
public static void addSmoothScrolling(VirtualFlow<?> virtualFlow, double speed) {
addSmoothScrolling(virtualFlow, speed, DEFAULT_TRACK_PAD_ADJUSTMENT);
}
/// @author Glavo
public static void addSmoothScrolling(VirtualFlow<?> virtualFlow, double speed, double trackPadAdjustment) {
smoothScroll(virtualFlow, speed, trackPadAdjustment);
}
private static final double[] FRICTIONS = {0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, 0.00003, 0.00001};
private static final Duration DURATION = Duration.millis(3);
private static void smoothScroll(ScrollPane scrollPane, double speed, double trackPadAdjustment) {
final double[] frictions = {0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, 0.00003, 0.00001};
final double[] derivatives = new double[frictions.length];
AtomicReference<Double> atomicSpeed = new AtomicReference<>(speed);
final double[] derivatives = new double[FRICTIONS.length];
Timeline timeline = new Timeline();
AtomicReference<ScrollDirection> scrollDirection = new AtomicReference<>();
Holder<ScrollDirection> scrollDirectionHolder = new Holder<>();
final EventHandler<MouseEvent> mouseHandler = event -> timeline.stop();
final EventHandler<ScrollEvent> scrollHandler = event -> {
if (event.getEventType() == ScrollEvent.SCROLL) {
scrollDirection.set(determineScrollDirection(event));
if (isTrackPad(event, scrollDirection.get())) {
atomicSpeed.set(speed / trackPadAdjustment);
} else {
atomicSpeed.set(speed);
}
derivatives[0] += scrollDirection.get().intDirection * atomicSpeed.get();
ScrollDirection scrollDirection = determineScrollDirection(event);
scrollDirectionHolder.value = scrollDirection;
double currentSpeed = isTrackPad(event, scrollDirection) ? speed / trackPadAdjustment : speed;
derivatives[0] += scrollDirection.intDirection * currentSpeed;
if (timeline.getStatus() == Status.STOPPED) {
timeline.play();
}
@@ -166,42 +173,40 @@ final class ScrollUtils {
}
};
if (scrollPane.getContent().getParent() != null) {
scrollPane.getContent().getParent().addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
scrollPane.getContent().getParent().addEventFilter(MouseEvent.MOUSE_PRESSED, mouseHandler);
scrollPane.getContent().getParent().addEventHandler(ScrollEvent.ANY, scrollHandler);
}
scrollPane.getContent().parentProperty().addListener((observable, oldValue, newValue) -> {
if (oldValue != null) {
oldValue.removeEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
oldValue.removeEventFilter(MouseEvent.MOUSE_PRESSED, mouseHandler);
oldValue.removeEventHandler(ScrollEvent.ANY, scrollHandler);
}
if (newValue != null) {
newValue.addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
newValue.addEventFilter(MouseEvent.MOUSE_PRESSED, mouseHandler);
newValue.addEventHandler(ScrollEvent.ANY, scrollHandler);
}
});
timeline.getKeyFrames().add(new KeyFrame(Duration.millis(3), event -> {
timeline.getKeyFrames().add(new KeyFrame(DURATION, event -> {
for (int i = 0; i < derivatives.length; i++) {
derivatives[i] *= frictions[i];
derivatives[i] *= FRICTIONS[i];
}
for (int i = 1; i < derivatives.length; i++) {
derivatives[i] += derivatives[i - 1];
}
double dy = derivatives[derivatives.length - 1];
Function<Bounds, Double> sizeFunction = (scrollDirection.get() == ScrollDirection.UP || scrollDirection.get() == ScrollDirection.DOWN) ? Bounds::getHeight : Bounds::getWidth;
double size = sizeFunction.apply(scrollPane.getContent().getLayoutBounds());
double value;
switch (scrollDirection.get()) {
double size;
switch (scrollDirectionHolder.value) {
case LEFT:
case RIGHT:
value = Math.min(Math.max(scrollPane.hvalueProperty().get() + dy / size, 0), 1);
scrollPane.hvalueProperty().set(value);
size = scrollPane.getContent().getLayoutBounds().getWidth();
scrollPane.setHvalue(Math.min(Math.max(scrollPane.getHvalue() + dy / size, 0), 1));
break;
case UP:
case DOWN:
value = Math.min(Math.max(scrollPane.vvalueProperty().get() + dy / size, 0), 1);
scrollPane.vvalueProperty().set(value);
size = scrollPane.getContent().getLayoutBounds().getHeight();
scrollPane.setVvalue(Math.min(Math.max(scrollPane.getVvalue() + dy / size, 0), 1));
break;
}
@@ -212,4 +217,51 @@ final class ScrollUtils {
timeline.setCycleCount(Animation.INDEFINITE);
}
/// @author Glavo
private static void smoothScroll(VirtualFlow<?> virtualFlow, double speed, double trackPadAdjustment) {
if (!virtualFlow.isVertical())
return;
final double[] derivatives = new double[FRICTIONS.length];
Timeline timeline = new Timeline();
final EventHandler<MouseEvent> mouseHandler = event -> timeline.stop();
final EventHandler<ScrollEvent> scrollHandler = event -> {
if (event.getEventType() == ScrollEvent.SCROLL) {
ScrollDirection scrollDirection = determineScrollDirection(event);
if (scrollDirection == ScrollDirection.LEFT || scrollDirection == ScrollDirection.RIGHT) {
return;
}
double currentSpeed = isTrackPad(event, scrollDirection) ? speed / trackPadAdjustment : speed;
derivatives[0] += scrollDirection.intDirection * currentSpeed;
if (timeline.getStatus() == Status.STOPPED) {
timeline.play();
}
event.consume();
}
};
virtualFlow.addEventFilter(MouseEvent.MOUSE_PRESSED, mouseHandler);
virtualFlow.addEventFilter(ScrollEvent.ANY, scrollHandler);
timeline.getKeyFrames().add(new KeyFrame(DURATION, event -> {
for (int i = 0; i < derivatives.length; i++) {
derivatives[i] *= FRICTIONS[i];
}
for (int i = 1; i < derivatives.length; i++) {
derivatives[i] += derivatives[i - 1];
}
double dy = derivatives[derivatives.length - 1];
virtualFlow.scrollPixels(dy);
if (Math.abs(dy) < 0.001) {
timeline.stop();
}
}));
timeline.setCycleCount(Animation.INDEFINITE);
}
private ScrollUtils() {
}
}
Loaded 100 of 741 files, more files were not shown because too many files have changed in this diff. Show more