Compare commits

...
Author SHA1 Message Date
huanghongxun 32d0d6a7ee Add some task test 2019-02-26 15:28:40 +08:00
huanghongxun d9a336e50b Remove redundant codes 2019-02-26 13:29:09 +08:00
huanghongxun 3c958d06c8 Replace scheduler to executor 2019-02-26 11:39:38 +08:00
huanghongxun 15a75a69b3 Fix exception handling 2019-02-26 08:17:46 +08:00
huanghongxun cae32cff4c remove isDependentSucceeded in whenComplete 2019-02-25 23:59:21 +08:00
huanghongxun eeef76594f Rename task.lastException -> task.exception 2019-02-25 22:17:30 +08:00
huanghongxun 8f2a1030bc Refactor Task 2019-02-24 23:27:53 +08:00
huanghongxun dc1df98277 Fix compilation error 2019-02-24 23:13:31 +08:00
huanghongxun def45cc3e4 Fix NumberFormatException when parsing forge version list json 2019-02-24 23:11:09 +08:00
huanghongxun 8007f1958e Rename isDependentsSucceeded to isDependentSucceeded 2019-02-24 17:00:17 +08:00
huanghongxun b22e724a3f Remove SilentException 2019-02-24 12:39:40 +08:00
huanghongxun ed66b016db Rename then-series functions 2019-02-24 12:39:39 +08:00
yushijinhun 34890f21b3 Update issue template 2019-02-24 12:29:58 +08:00
yushijinhun 9f85e4a63b Do not run GameVersion.minecraftVersion on UI thread 2019-02-24 12:25:19 +08:00
yushijinhun 7fcd4d94a6 Merge branch 'javafx' of github.com:huanghongxun/HMCL into javafx 2019-02-23 23:52:26 +08:00
yushijinhun 84fbca7905 Remove unused code 2019-02-23 23:48:23 +08:00
huanghongxun 9add2387b9 Add comments to FileUtils and IOUtils 2019-02-23 23:42:05 +08:00
huanghongxun a388450f94 Remove AutoTypingMap 2019-02-23 22:50:01 +08:00
huanghongxun fff7abc2ac finalized no longer passes AutoTypingMap as callback parameter 2019-02-23 22:50:00 +08:00
yushijinhun e5587418c7 Remove SafeIntStringConverter 2019-02-23 22:36:38 +08:00
yushijinhun 7c89fac1e9 Rename MultiStepBinding->BindingMapping 2019-02-23 22:14:29 +08:00
yushijinhun bc9a9ffb72 rewrite MultiStepBinding.asyncMap 2019-02-23 21:58:44 +08:00
yushijinhun 46d27a04a2 Fix auth server list in add account pane 2019-02-23 21:49:38 +08:00
huanghongxun 0f33981c96 Fix #534 2019-02-23 19:13:24 +08:00
yushijinhun b396e0b208 Fix compilation error 2019-02-23 18:41:39 +08:00
yushijinhun 1bf2f17538 Fix #533 2019-02-23 18:38:43 +08:00
huanghongxun 608a531655 Remove AtomicReference 2019-02-20 20:18:34 +08:00
huanghongxun e57180eb15 remove lastException in task.variables 2019-02-20 20:07:43 +08:00
huanghongxun 3ae826d9ca Fix task.getLastException does not receive subtasks' value 2019-02-20 20:07:11 +08:00
huanghongxun e940845c60 Fix not correct mod installation 2019-02-20 16:40:26 +08:00
huanghongxun d8aa989cc4 Fix #532 2019-02-19 23:15:45 +08:00
huanghongxun 31ed4268b6 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2019-02-19 19:39:23 +08:00
huanghongxun c698eaac6f Add sponsor information 2019-02-19 19:38:37 +08:00
huanghongxun d0dd23fa14 Fix compilation error along with openjfx 2019-02-19 19:36:23 +08:00
yushijinhun c6f1aa21c5 Log exception in login pane 2019-02-19 18:56:36 +08:00
huanghongxun a3dc7798e7 fetch Forge remote version list only from BMCLAPI 2019-02-17 14:01:05 +08:00
huanghongxun 88f6fd1965 Set cursor in button to hand by default 2019-02-17 13:57:49 +08:00
huanghongxun 9062c2e447 Display more friendly error message when fail to install library 2019-02-17 13:45:10 +08:00
huanghongxun e3d2a4bf9d Update JFoenix to fix #523 2019-02-16 01:52:37 +08:00
huanghongxun c2d609cdb4 Use TableView in ModListPage to allow multiple selection 2019-02-15 19:39:43 +08:00
huanghongxun b3721e29c6 Update copyright statement 2019-02-14 16:20:14 +08:00
huanghongxun eab4463c2d HMCLauncher: verify if JVM version is between 8 and 10 2019-02-14 15:34:52 +08:00
yushijinhun 394873864f Fix #526 2019-02-13 20:09:51 +08:00
yushijinhun 758216245f Add missing license headers 2019-02-13 20:05:01 +08:00
huanghongxun 6edfd112a2 Replace "close" icon to "delete" icon on ModItem 2019-02-13 17:51:01 +08:00
huanghongxun 625215b068 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2019-02-13 17:46:18 +08:00
huanghongxun 4759e4957c refactor 2019-02-13 17:46:11 +08:00
huanghongxun b62f9494ae Have a placeholder in the bottom of LIstPage 2019-02-12 21:39:01 +08:00
huanghongxun 12b47e7d51 Better prompt of LibraryDownloadException 2019-02-12 21:30:45 +08:00
huanghongxun 9ced29aa93 Display furnace image as default for modded 1.13 game 2019-02-11 19:53:55 +08:00
huanghongxun 5095640ba2 Report memory usage when OutOfMemoryError occurred. 2019-02-11 19:47:17 +08:00
huanghongxun c889d7cb3f Download missing game jar when launching 2019-02-11 19:36:19 +08:00
huanghongxun 84636f3f50 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2019-02-10 10:51:39 +08:00
huanghongxun 9a6f07d7c9 make DefaultGameRepositoy.readVersionJson nonnull. 2019-02-10 10:51:04 +08:00
huanghongxun e8f5088049 Support automatic installation of Forge 1.13. 2019-02-10 10:49:48 +08:00
yushijinhun 0bee306d67 css clean up 2019-02-09 21:44:14 +08:00
yushijinhun 8d75b2d413 Fix #519 2019-02-09 21:37:26 +08:00
huanghongxun 426ea607e8 HMCLauncher: try bundled JRE 2019-02-08 22:05:16 +08:00
yushijinhun 507cade50d polish authlib-injector 2019-02-08 17:08:23 +08:00
huanghongxun f0c7f54cc6 Redownload asset index when install new game 2019-02-08 15:54:28 +08:00
yushijinhun 62c38482f6 polish 639bc58e84
see https://github.com/huanghongxun/HMCL/commit/639bc58e84b68a7b508ed9ba0a701a13b2fac1b4#r32215378
2019-02-08 14:55:58 +08:00
yushijinhun 0ff6ca18ed Use https for bmclapi 2019-02-08 14:35:16 +08:00
huanghongxun bbe6c388a3 Merge branch 'skin-refactor' into javafx 2019-02-06 22:53:38 +08:00
huanghongxun 639bc58e84 Support version identification of Minecraft 1.14 2019-02-06 21:54:51 +08:00
huanghongxun c0bcafd747 Revert "Use JFXListView in place of custom components to accelerate rendering"
This reverts commit 0813211
2019-02-06 17:14:49 +08:00
huanghongxun bcaa019551 Only maintain game version json with launchwrapper installed.
Forge modlauncher is not supported right now.
2019-02-06 15:14:08 +08:00
huanghongxun 38725abc48 DownloadInfo.url can be null in forge 1.13.2 version json 2019-02-06 13:35:16 +08:00
yushijinhun 00f1173bac Add translation for CharacterDeletedException 2019-02-05 14:56:11 +08:00
yushijinhun 68a02966d0 Skip parsing json file if it does not exist 2019-02-05 14:31:33 +08:00
yushijinhun 4721fcea66 allow characterName/authserverName to be changed at runtime 2019-02-05 02:09:39 +08:00
yushijinhun 5ea63273be check validity of YggdrasilSession response 2019-02-05 01:46:48 +08:00
yushijinhun 7cee25aab2 Fix concurrent problems in TexturesLoader.Selector 2019-02-05 00:15:17 +08:00
yushijinhun 9298f5e030 Account Refactor 2019-02-04 23:12:40 +08:00
yushijinhun 93f943d40c Fix crash in version settings
introduced in d75c3bbf96
2019-02-04 17:06:20 +08:00
Feng Chang f20c11a4e2 Ensure sequential execution of configuration writer threads (#516)
* Ensure sequential execution of configuration writer threads

Signed-off-by: Chang Feng <chang_196700@hotmail.com>

* Change method to add synchronized to action

Signed-off-by: Chang Feng <chang_196700@hotmail.com>
2019-02-04 02:18:24 +08:00
yushijinhun 0c25f31493 fix npe 2019-01-26 22:50:51 +08:00
yushijinhun 8d774e267f Show register link in login dialog
https://github.com/yushijinhun/authlib-injector/issues/25
2019-01-26 15:19:17 +08:00
yushijinhun 6c1de5d485 Improve tooltip UX 2019-01-26 13:33:59 +08:00
yushijinhun 77c86ed3bc Add tooltip for authlib-injector accounts 2019-01-26 13:17:45 +08:00
yushijinhun d75c3bbf96 Refactor TwoLineListItem 2019-01-26 13:06:56 +08:00
huanghongxun 8bb6cb714c use result of prev task instead of AutoTypingMap 2019-01-22 17:15:03 +08:00
huanghongxun 28d002f6d3 Do not schedule task if preExecute and postExecute are not overrided. 2019-01-22 16:46:11 +08:00
huanghongxun 78fcc69d29 Fixed not displaying "Home" icon. Mentioned in #513 2019-01-22 16:28:41 +08:00
huanghongxun cdfae54ab9 Fixed costly time consumption reading level.dat. Mentioned in #512 2019-01-22 16:11:31 +08:00
huanghongxun 0813211c20 Use JFXListView in place of custom components to accelerate rendering 2019-01-22 16:01:00 +08:00
huanghongxun 056a4eead2 Add comments to LiteLoaderInstallTask and OptiFineInstallTask 2019-01-22 15:54:59 +08:00
huanghongxun 2635d31425 Fix not correctly adding "--tweakClass" for MultiMC modpack 2019-01-22 15:52:57 +08:00
huanghongxun 76d35af5b9 MultiMC modpack patch json file may have "+libraries" or "libraries" 2019-01-22 15:51:52 +08:00
huanghongxun bcff1695d3 Refactor MaintainTask 2019-01-22 15:50:49 +08:00
huanghongxun 3d04447c2f Load world asynchronously 2019-01-22 14:59:01 +08:00
huanghongxun 2736b88a10 Add some promise-like method to TaskResult 2019-01-22 14:58:00 +08:00
huanghongxun 0804a2fbf8 Merge TaskCallable and TaskCallable2 2019-01-22 14:56:48 +08:00
huanghongxun 7224ed6a7d Fix missorting optifine versions 2019-01-16 12:34:13 +08:00
huanghongxun c41737364c Prevent from potential conflict that a MultiMC modpack has ".minecraft" and "minecraft" at the same time 2019-01-13 17:10:23 +08:00
huanghongxun 320e579844 Remove game deletion and renaming action in game settings page 2019-01-13 16:55:07 +08:00
huanghongxun fc3f2e045b Retry fetching remote text file when downloading had been terminated 2019-01-13 16:50:54 +08:00
huanghongxun 481b47bf10 Fix #508. MultiMC stores files in ".minecraft" instead 2019-01-13 16:50:21 +08:00
huanghongxun 0fb87f0b1d Accept modpack drag 2019-01-13 11:55:55 +08:00
huanghongxun 1252d7700f Update alert prompt of Forge 2760 2019-01-11 21:23:26 +08:00
huanghongxun 30c2d88f62 Use spinnerPane in ModListPage 2019-01-11 21:18:26 +08:00
huanghongxun 718a98d7ec Fix alerting Forge conflicts with LiteLoader but actually not 2019-01-11 21:17:02 +08:00
huanghongxun fb3ba220b6 Fix redownloading disabled mods for curse modpack 2019-01-09 13:18:04 +08:00
yushijinhun d655c9ec6b Use /* instead of /** for license header
see: https://github.com/mycila/license-maven-plugin/issues/118
2019-01-05 19:04:24 +08:00
yushijinhun 8a67f33cf6 Update license year to 2019 2019-01-05 18:50:16 +08:00
yushijinhun 2e53688c52 Add gradle default tasks: clean, build 2019-01-05 18:49:28 +08:00
yushijinhun 11ce04fba4 Add dependency org.jetbrains.annotations 2019-01-05 18:49:02 +08:00
yushijinhun 8b191e4a3b Fix grey auth server list cell 2018-12-31 19:43:36 +08:00
huanghongxun 6405263616 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-12-30 19:29:27 +08:00
huanghongxun a2119ab008 Remove "MCBBS" in introduction to the functinoality of modpack install 2018-12-30 19:28:53 +08:00
Haowei Wen b21d8f094a Merge pull request #504 from xfl03/xfl03-jfxpatch-1
Fixed #503
2018-12-23 02:53:10 +08:00
Alexander Xia 90c41a83b1 Fixed #503 2018-12-23 02:49:18 +08:00
huanghongxun a1ec774089 Remind user to move files when switch game working directory 2018-12-22 00:56:05 +08:00
huanghongxun 631056064a Add tooltip about what would be cleaned in game directory 2018-12-22 00:25:19 +08:00
huanghongxun 97e90de71e Add refresh button in mod list page 2018-12-21 01:12:41 +08:00
huanghongxun bbaed35160 Add button to reveal world directory in explorer 2018-12-21 00:40:50 +08:00
huanghongxun 778f26d55f Support manually created world pack 2018-12-21 00:34:46 +08:00
huanghongxun fab5bdc2e1 Auto detect world and datapack's encoding 2018-12-20 20:27:32 +08:00
huanghongxun b7723a88c3 Add dragging world pack to GUI for installation 2018-12-20 20:24:23 +08:00
huanghongxun 1d2b59d5a2 Fix not showing spinner when clicked accept button 2018-12-20 20:07:00 +08:00
huanghongxun 7d42c0f581 Change "redownload asset index" to "Update game asset files" 2018-12-20 19:30:35 +08:00
huanghongxun bfe7398e17 Fixed #501 2018-12-18 13:44:30 +08:00
huanghongxun c4f0a6a197 Suppress IllegalStateException. Closes #500 2018-12-18 13:32:38 +08:00
huanghongxun 1ca9d0fa2c Backup when updating modpack 2018-12-18 13:08:02 +08:00
huanghongxun a015585bd1 Let TaskExecutorDialogPane close itself when finished 2018-12-18 12:35:03 +08:00
huanghongxun bc34427793 Add doDependenciesSucceeded in Task 2018-12-18 00:26:31 +08:00
huanghongxun 0fe867cd93 Fixed #499 2018-12-17 23:31:47 +08:00
yushijinhun 70f0393bde Fix proxy options are passed to game even it's disabled 2018-12-16 16:03:29 +08:00
yushijinhun 9f060be3fd Fix #502
* Check if port is valid before setting it
* Validate user input
* Change proxyPort to int
2018-12-16 15:49:57 +08:00
yushijinhun 176968d883 Fix #498 2018-12-16 00:31:37 +08:00
huanghongxun e6bd55aa69 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-12-13 00:43:15 +08:00
huanghongxun f5ffa875b8 Auto detect encoding of compressed packs 2018-12-13 00:42:52 +08:00
huanghongxun 2cb23cbfcb Log messages at finer and finest level 2018-12-11 15:47:12 +08:00
huanghongxun 31dc9b0925 Delete game whose installation fails 2018-12-11 15:44:17 +08:00
huanghongxun af7cd9a821 Merge pull request #496 from yushijinhun/gpl-additional-terms
Add additional terms under GPLv3 section 7
2018-12-09 00:14:58 +08:00
yushijinhun b34a64f425 Add additional terms under GPLv3 section 7 2018-12-08 23:18:31 +08:00
huanghongxun 11820e31a8 Merge pull request #495 from yushijinhun/license
Update license header
2018-12-08 21:27:10 +08:00
yushijinhun 672b6054a6 Format license headers 2018-12-08 21:13:03 +08:00
yushijinhun d2486732d2 Prevent translation contributors being removed by license-gradle-plugin 2018-12-08 21:11:10 +08:00
yushijinhun 924067e4dd Specify GPL version in the readme link
in case FSF updates it
2018-12-08 21:11:10 +08:00
yushijinhun 7820f748a0 Restore original GPLv3 license
From: https://www.gnu.org/licenses/gpl-3.0.txt
2018-12-08 21:11:10 +08:00
yushijinhun e8757deb0d Use license-gradle-plugin 2018-12-08 21:11:08 +08:00
yushijinhun c371924dc0 Remove Fonts class as it has never been used 2018-12-08 19:39:09 +08:00
yushijinhun cf28b27d50 Remove HMCLCore usage instructions 2018-12-08 19:14:56 +08:00
huanghongxun b78f9c8cd5 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-12-02 20:03:15 +08:00
huanghongxun 91bd67dc26 Restore default cache directory if needed. Closes #489 2018-12-02 20:02:22 +08:00
huanghongxun 0671b49c98 Fix bubble ui 2018-12-02 18:48:22 +08:00
yushijinhun 900f483a03 Fix #493 2018-12-02 14:57:37 +08:00
yushijinhun 4d7af2e68b Fix #492
Related: 4d73187376
2018-12-02 14:54:58 +08:00
yushijinhun db59dcea65 Log exception when failing to load version list
see https://github.com/huanghongxun/HMCL/issues/489#issuecomment-443427026
2018-12-02 13:55:25 +08:00
huanghongxun ef6cfa785f Query cursemeta for removed mods 2018-11-26 00:40:53 +08:00
huanghongxun 2b792e73c9 Fix Java 9 requirement 2018-11-26 00:40:52 +08:00
huanghongxun 0f2922dab4 Update gradle 2018-11-26 00:40:51 +08:00
huanghongxun 45b55e2853 Fix not displaying crash report window when errored in Application.start 2018-11-26 00:40:50 +08:00
huanghongxun 9c7795687f Show installation succeeded when Curse modpack not finished completion 2018-11-26 00:40:49 +08:00
yushijinhun b273c80a02 Add AuthlibInjectorAccount.toString() 2018-11-24 20:43:41 +08:00
yushijinhun 8cdab4710b Add response text to exception message when authlib-injector meta is malformed 2018-11-24 20:33:46 +08:00
yushijinhun ea5b31d537 Allow specifying authlib-injector path 2018-11-24 20:32:22 +08:00
yushijinhun bb0c11fa12 Log authentication exceptions 2018-11-23 23:44:45 +08:00
yushijinhun 37186439cb Fetch authlib-injector metadata on startup 2018-11-23 23:29:41 +08:00
yushijinhun ab9f93d68e Make update checker thread daemon 2018-11-23 23:12:38 +08:00
yushijinhun ca6920e82a Fix preferredLoginType is not exported in modpack 2018-11-23 23:05:24 +08:00
yushijinhun d9db108a31 Use flag instead of accountId to store selected account
Mentioned in #488
2018-11-23 22:57:21 +08:00
yushijinhun 34b7e1d582 Support use authlib-injector offline 2018-11-23 22:01:52 +08:00
yushijinhun 4d73187376 Use @JsonAdapter 2018-11-23 21:49:50 +08:00
yushijinhun dcd0a42705 Refactor AuthlibInjectorServer 2018-11-23 21:34:54 +08:00
yushijinhun 1b40916046 Rewrite config properties 2018-11-23 21:16:05 +08:00
yushijinhun a4e4782f2f Change type from ExceptionalSupplier to AuthlibInjectorDownloader 2018-11-23 17:47:51 +08:00
yushijinhun 875b0c27ef Delete css if bss is generated 2018-11-23 17:08:17 +08:00
huanghongxun a38a1775a0 Update Forge crash detection. Closes #486 2018-11-18 22:09:48 +08:00
huanghongxun fd6ae986c3 Fixed #485 2018-11-13 21:20:14 +08:00
huanghongxun 74a87e65c8 Compile css to bss to speed up launching 2018-11-11 21:12:03 +08:00
yushijinhun a285d2c27d Parallel ca cert check 2018-11-11 11:04:19 +08:00
huanghongxun 44ac3bc900 Fix messy name of installed modpack game 2018-11-06 22:03:43 +08:00
huanghongxun 1c346c97a8 Add an option to skip JVM compatibility 2018-10-31 22:36:35 +08:00
yushijinhun d890b41380 Refactor Accounts to support login type name 2018-10-28 01:08:36 +08:00
yushijinhun cbcc89b48f Refactor RipplerContainer.StyleableProperties 2018-10-27 20:38:55 +08:00
yushijinhun 521bd00a70 Refactor TwoLineListItem.StyleableProperties 2018-10-27 20:16:05 +08:00
yushijinhun b4693423ef Logs the exception if auth server failed to be added 2018-10-27 19:38:30 +08:00
yushijinhun 594a2741d7 Remember the last used login type 2018-10-27 19:30:56 +08:00
yushijinhun a1d9b52b9d Refactor uiVersion 2018-10-27 19:10:02 +08:00
huanghongxun c9d825b6c1 Add a button for cleaning crash-reports and logs manually 2018-10-23 14:08:51 +08:00
huanghongxun 7dcd0eb616 Only display installer when game repository finished loading 2018-10-23 13:52:47 +08:00
huanghongxun e79a95b885 Fix NPE when version json missing "libraries" 2018-10-23 13:42:16 +08:00
huanghongxun 5d232c3f5e Fix NPE when app stopped 2018-10-23 13:36:45 +08:00
huanghongxun ee1c0d362d Fix NPE 2018-10-23 13:34:37 +08:00
huanghongxun aafcd57c82 Attempt to solve #481 2018-10-23 13:32:25 +08:00
huanghongxun 66a4566dc5 Update JFoenix.jar 2018-10-21 11:26:07 +08:00
huanghongxun 4c317f5b03 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-10-21 11:05:33 +08:00
huanghongxun 12d706a812 Don't show AuthenticationException class name 2018-10-21 11:05:27 +08:00
yushijinhun c006068580 Keep 'Next' button and 'Cancel' button on the same line 2018-10-20 21:33:34 +08:00
yushijinhun f8a838506d Replace 'Open logs folder' with 'Export logs'
Related: 8905f2e855
2018-10-20 17:52:06 +08:00
yushijinhun 270844bbb1 Start logging earlier 2018-10-20 17:44:43 +08:00
yushijinhun c9466cf8ef Move MINECRAFT_DIRECTORY & HMCL_DIRECTORY to Metadata 2018-10-20 17:43:35 +08:00
yushijinhun 7d6f1f98f1 Fix verison comparing
Related: fb9418dead
2018-10-20 15:56:14 +08:00
huanghongxun fb9418dead Fixed NPE 2018-10-17 13:51:49 +08:00
huanghongxun 4a1b06b80c Update I18N 2018-10-16 20:32:52 +08:00
huanghongxun 8905f2e855 Add a button to open log directory 2018-10-16 00:20:56 +08:00
huanghongxun 6c74364fa5 Fix writeText 2018-10-15 22:27:26 +08:00
huanghongxun f0720c1755 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-10-15 22:08:31 +08:00
huanghongxun f1acc1d2c1 Fix NPE 2018-10-15 13:05:32 +08:00
Haowei Wen 82bddbfa6e Merge pull request #474 from outloudvi/patch-1
Fix opening resourcepacks/ while opening saves/
2018-10-15 00:26:53 +08:00
Outvi V 841755fe56 Fix opening resourcepacks/ while opening saves/ 2018-10-14 22:39:22 +08:00
huanghongxun 340ee751e8 Suppress VersionNotFoundException 2018-10-14 19:10:04 +08:00
huanghongxun cf70f72f9c Fix #471 2018-10-14 19:04:27 +08:00
huanghongxun 16ea88a659 Fixed failing to download file when cached entry was removed by user 2018-10-14 19:04:21 +08:00
huanghongxun eaa6790013 Update welcome.png 2018-10-14 18:50:47 +08:00
huanghongxun af571d533d Maintain statement when no account 2018-10-14 12:45:52 +08:00
huanghongxun 004c3c588a Log navigation to catch bugs 2018-10-14 12:39:22 +08:00
huanghongxun 2a44167c22 Suppress NPE 2018-10-14 12:39:03 +08:00
huanghongxun cf6b735026 Removed JFXDrawer and JFXHamburger in JFoenix.jar 2018-10-14 12:20:03 +08:00
huanghongxun 5918b04b48 Prevent from calling register and fireEvent at the same time 2018-10-13 11:17:16 +08:00
huanghongxun 8004cb3a64 Change default red to darker one 2018-10-11 22:26:50 +08:00
huanghongxun 9981423927 Change separator fill in title bar and launch button to reduce color difference 2018-10-11 22:20:41 +08:00
huanghongxun 0e5250dfb3 Satisfy requirements metioned in #469 2018-10-11 22:06:34 +08:00
huanghongxun 2dc956b357 Fix request mojang server for skin info every time 2018-10-11 22:05:24 +08:00
huanghongxun b8bd0a08e0 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-10-11 14:05:35 +08:00
huanghongxun fb31eebcf2 Add a warning that forge 2760 is not compatible with liteloader 2018-10-11 14:04:52 +08:00
huanghongxun 3c8689a3f1 Close launching progress dialog when game crashed before opening window 2018-10-11 13:51:37 +08:00
huanghongxun 2957d66dd6 Add test game button in versions page 2018-10-11 13:41:11 +08:00
huanghongxun 6f6efe0874 Rename DecoratorControl to Decorator 2018-10-11 13:32:39 +08:00
huanghongxun 355fc04069 Drop modpack file to install 2018-10-11 13:32:11 +08:00
huanghongxun b4738f383b Fix not on FX thread 2018-10-11 12:59:06 +08:00
huanghongxun b12711969c Rewrite encode location 2018-10-10 16:37:04 +08:00
huanghongxun 94727353c4 Fix response code 400 when downloading mods from curse 2018-10-10 15:27:27 +08:00
huanghongxun 5eb7449b74 Delete old files if etag was updated 2018-10-10 01:36:31 +08:00
huanghongxun dae41dfb30 Add an entrance directly to add authlib-injector server 2018-10-09 22:15:46 +08:00
huanghongxun 07a701e572 Cache asset indexes by ETag cache 2018-10-09 20:22:54 +08:00
huanghongxun 5323aaed6d ETag caching 2018-10-09 20:16:16 +08:00
huanghongxun daf659373f Removed static login method 2018-10-08 21:23:01 +08:00
huanghongxun 0eb81c29df Update I18N 2018-10-08 01:31:02 +08:00
huanghongxun 8aea89c103 Login when launcher opened to reduce launching time 2018-10-08 01:27:48 +08:00
huanghongxun f1d97454c2 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-10-06 12:42:50 +08:00
huanghongxun ffb6a45e28 Remove toLowerCase 2018-10-06 12:42:30 +08:00
huanghongxun f0d1c9b361 Merge pull request #468 from yushijinhun/authlib-injector-ali
支持 authlib-injector API 地址指示
2018-10-06 12:38:04 +08:00
huanghongxun aedd4c290e Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-10-06 12:20:48 +08:00
huanghongxun 749ecfe37f Fix #467 2018-10-06 12:20:42 +08:00
yushijinhun c51da28e31 Add error message for authlib-injector download failure 2018-10-06 12:06:31 +08:00
yushijinhun f3dca75a10 Do not check for update of authlib-injector each time 2018-10-06 11:56:02 +08:00
yushijinhun c3dcb0292c Support -Dauthlibinjector.side=client argument
See yushijinhun/authlib-injector#16
2018-10-06 11:38:55 +08:00
yushijinhun 2a42ecda78 Support authlib-injector ALI
See yushijinhun/authlib-injector#18
2018-10-06 00:47:36 +08:00
huanghongxun 08be1896ee Forbid adding profiles with already existent name 2018-10-05 23:04:56 +08:00
huanghongxun 6b3152c4b5 Try to fix #462. 2018-10-05 22:27:19 +08:00
yushijinhun 19ea27dece emmm... Forgot to add DialogAware.java
Related: b9e26a4112
2018-10-04 12:48:29 +08:00
yushijinhun b9e26a4112 Add DialogAware & Improve add auth server dialog 2018-10-04 12:45:04 +08:00
yushijinhun a71aea8e43 Update translation: account.failed.server_response_malformed 2018-10-04 11:56:12 +08:00
yushijinhun 1b1f22b9fa Fix Java list display error on JDK10
如何重现:
1. 关闭当前选中版本的版本特定设置
2. 重启HMCL
3. 从主界面进入版本设置
4. 打开版本特定设置
5. 展开Java列表

问题:Java列表中的项目除了自定义项外,在外观上仍保持被禁用的状态(但是可以点击)。

原因未知。

此问题只在Java10上出现。
若在进入版本设置界面前,版本特定设置已经被打开,则不会出现此问题。

已知Java项目的styleClass、pseudoClass、disable、disabled属性均正常。

试过但没用的解决方法:
1. 删除HMCL全部CSS
2. 将Java列表默认展开
3. 使用JavaFX原生组件替代JFoenix组件

本提交中的代码由于玄学原因,可以解决此问题。
2018-10-03 22:40:17 +08:00
yushijinhun 705a5659b6 Fix cursor doesn't change when hanging on proxy settings 2018-10-03 21:41:22 +08:00
yushijinhun 4121921aca Fix compile errors 2018-10-03 19:20:47 +08:00
yushijinhun 8cb4ca0e8b Remove Settings.font 2018-10-03 17:17:16 +08:00
yushijinhun 6205937a10 Refactor update channel selecting 2018-10-03 17:15:42 +08:00
yushijinhun 2f522b744f Add MappedProperty & reservedSelectedPropertyFor(CheckBox) 2018-10-03 17:09:12 +08:00
yushijinhun b8a84f831a Refactor proxy settings 2018-10-03 16:43:48 +08:00
yushijinhun 5f9490d780 Add SafeStringConverter & Refactor font settings 2018-10-03 16:38:21 +08:00
huanghongxun a1c5b87b55 Fix crash when no game installed 2018-10-03 15:55:38 +08:00
yushijinhun 641e308c53 Remove right graphic of Game List&Launcher Settings 2018-10-03 13:40:09 +08:00
yushijinhun 64bc70a15d Fix Account/VersionListItem can be unselected 2018-10-03 13:01:30 +08:00
yushijinhun acadbdb5c9 Fix potential NPE
Related: 3323ea2be0
2018-10-03 12:10:19 +08:00
yushijinhun 3323ea2be0 Remove JFXButton in AdvancedListItem 2018-10-03 12:02:17 +08:00
huanghongxun 45f55b2075 Adjust looking of LeftPaneController 2018-10-03 01:32:55 +08:00
yushijinhun 372005dae1 Partially revert 223c2bc7c7 2018-10-02 22:07:57 +08:00
yushijinhun 2c7ca6d354 Remove some methods in Lang 2018-10-02 22:01:42 +08:00
yushijinhun e21f315239 Fix VersionNumber.equals() and hashCode() may not be consistent 2018-10-02 21:25:29 +08:00
yushijinhun 223c2bc7c7 Change update.bubble.subtitle to 'Click here to upgrade' 2018-10-02 21:15:14 +08:00
yushijinhun 3facbba4ee Use databind for update bubble 2018-10-02 21:15:14 +08:00
yushijinhun b8bf56c8c3 Add pseudo class 'first' for 'options-list-item'
Related: caf85d2a92
2018-10-02 20:38:41 +08:00
huanghongxun 2fa08e0a70 Change default profile directory to ".minecraft" 2018-10-02 18:34:09 +08:00
huanghongxun 8e1e13058b Fix #463 2018-10-02 18:33:48 +08:00
huanghongxun 92520e46a3 Remove settings entrance in main page 2018-10-02 18:11:52 +08:00
huanghongxun 6ae6c9c971 Name changes 2018-10-02 18:11:13 +08:00
huanghongxun caf85d2a92 Fix showing a gray horizontal line in component list cell 2018-10-02 18:06:24 +08:00
huanghongxun 030b577dee Add an explicit entry to game list page 2018-10-02 12:44:36 +08:00
huanghongxun 83516ba679 Fix #461 2018-10-02 12:15:52 +08:00
huanghongxun c8f74ee8e3 Add test game in game list page 2018-10-02 11:47:53 +08:00
yushijinhun c50e41ab91 Use setItems in DatapackListPage 2018-10-01 21:01:43 +08:00
yushijinhun c03a0feb75 Refactor ProfileList 2018-10-01 20:59:11 +08:00
yushijinhun 7da5b8fbc8 Add AutomatedToggleGroup & Refactor AccountList 2018-10-01 20:39:00 +08:00
yushijinhun 83f7e61d37 Add selectedItemPropertyFor(ToggleGroup) 2018-10-01 20:03:57 +08:00
yushijinhun 86765275c8 Make SelectionModelSelectedItemProperty a util class 2018-10-01 15:38:12 +08:00
yushijinhun a01e15d841 Add MultiStepBinding&ReadWriteComposedProperty 2018-10-01 15:35:39 +08:00
yushijinhun 5e5db332d0 Remove 'Downloads' paragraph in readme, as GitHub Releases is available 2018-10-01 11:29:12 +08:00
huanghongxun 2de2f45124 Fix always showing horizontal scrollbar of game list popup in main page 2018-09-30 20:07:57 +08:00
huanghongxun b1b3eb9369 Fix always using custom common directory 2018-09-30 20:07:57 +08:00
yushijinhun 507665cd91 Fix #460 2018-09-30 19:45:39 +08:00
yushijinhun c779172cc2 Fix custom toggle is not selected when using customized java 2018-09-30 19:30:22 +08:00
yushijinhun 684813e131 Refactor Java lookup 2018-09-30 19:18:29 +08:00
huanghongxun 76ef47efc4 Suppress exceptions thrown when checking Java installation 2018-09-30 13:22:42 +08:00
huanghongxun 71a8fa0e66 Display platform of java installations 2018-09-28 19:30:41 +08:00
huanghongxun 48d98b2424 Conceal NPE 2018-09-26 19:07:09 +08:00
huanghongxun 5a0e17b121 Fix crash when user set common directory to an invalid path 2018-09-26 19:00:31 +08:00
huanghongxun 5f686f7205 Fix crash when modpack to be installed is malformed. 2018-09-26 13:13:44 +08:00
huanghongxun 518969d2f2 Fix JsonParseException 2018-09-26 13:13:21 +08:00
huanghongxun 01ff1327f7 Make head displayed more precisely 2018-09-25 12:30:21 +08:00
huanghongxun 4d366397a8 Merge pull request #457 from andylizi/add-helmet-support
支持在头像中显示头盔层
2018-09-24 17:28:17 +08:00
huanghongxun 80f79d20a5 Update message UI 2018-09-24 17:26:02 +08:00
huanghongxun 1611d11544 Fix crash when yggdrasil server gives response with malformed UUID 2018-09-24 17:24:13 +08:00
andylizi c8e8b25c85 Adjust faceOffset in skin preview 2018-09-24 15:34:48 +08:00
andylizi 92cfc3c1d3 Make skin preview more accurate 2018-09-24 13:47:59 +08:00
huanghongxun c4cebe72b4 Show bubble when updates available 2018-09-24 01:51:14 +08:00
andylizi 6d346a3ad6 Render helmet layer in skin preview. Fixed #456 2018-09-23 22:53:23 +08:00
huanghongxun 6e2696dea3 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-09-22 22:26:14 +08:00
huanghongxun ba67c968cf Change version selection list look 2018-09-22 22:25:53 +08:00
huanghongxun 332719f49a Fix closing page 2018-09-22 21:40:16 +08:00
yushijinhun fb18975c5c Fix param table in readme 2018-09-22 21:08:03 +08:00
yushijinhun 08a7c27353 Fix config file is checked before created 2018-09-22 21:05:50 +08:00
yushijinhun 4c9cb99a94 Add hmcl.self_integrity_check.disable option 2018-09-22 21:05:50 +08:00
huanghongxun cbecca5f55 Merge pull request #455 from yushijinhun/javafx
重构
2018-09-22 18:12:18 +08:00
yushijinhun 4a0831a155 Replace Properties with InvocationDispatcher & Make config saving async 2018-09-22 14:21:17 +08:00
yushijinhun fa38c53915 Remove Constants.SYSTEM_CHARSET 2018-09-22 12:54:15 +08:00
yushijinhun e7c2170ce3 Fix getSteve/AlexSkin ignores scaleRatio param 2018-09-22 12:53:02 +08:00
yushijinhun 9b69fe8c56 Remove Constants.DEFAULT_ICON 2018-09-22 12:52:30 +08:00
yushijinhun ed91771594 Remove truePredicate/falsePredicate/emptyConsumer 2018-09-22 12:39:57 +08:00
yushijinhun b33e014f71 Move GSON const to JsonUtils 2018-09-22 12:37:03 +08:00
yushijinhun 19632ac162 Move JsonUtils->gson, JarUtils->io, Properties->javafx 2018-09-22 12:33:18 +08:00
yushijinhun 74e3a9e2d1 Use http.agent to set useragent 2018-09-22 12:30:33 +08:00
yushijinhun b15291dc57 Refactor util package structure 2018-09-22 12:12:06 +08:00
huanghongxun 1b5d4c1b42 Improve UI details 2018-09-22 00:18:55 +08:00
huanghongxun 923924638e Add a close button to close all pages 2018-09-20 22:54:41 +08:00
huanghongxun 8b52c46664 Merge resources.zip to allow multiple datapack zip containing resources.zip 2018-09-20 00:12:41 +08:00
huanghongxun aa8c386cdd Support zip file in datapacks folder 2018-09-19 23:02:27 +08:00
huanghongxun bafe17107c Fix loading empty repository 2018-09-19 16:31:17 +08:00
huanghongxun 9c169fb560 Add a separator to separate launch button and popup button 2018-09-19 10:46:14 +08:00
huanghongxun 61ab8a6769 Fix NPE when first start 2018-09-19 00:20:32 +08:00
huanghongxun b1cd7057db Add a convenient entrance to launch other version 2018-09-19 00:16:21 +08:00
huanghongxun d426952d47 Fix Minecraft jar does not exist when renaming a version 2018-09-19 00:12:44 +08:00
huanghongxun 67b6a60382 Fix bugs with world. 2018-09-17 16:30:27 +08:00
huanghongxun 9d5da5a39d Try again when decorator is not initialized. 2018-09-17 16:28:52 +08:00
huanghongxun efb14572d6 Fix selected profile automatically changes to Home 2018-09-17 16:28:01 +08:00
481 changed files with 13253 additions and 8680 deletions

No files matched your search

+28
View File
@@ -0,0 +1,28 @@
# 公告
提交前请确保你的启动器版本是**最新的开发版**,可以在启动器设置中更换更新通道至开发版获取更新。
**如果你在阅读完本公告后仍然希望在此处发布 issue,请将公告内容删去,并按照下面的模板填入相关信息。**
我们发现,对于不经常检查邮箱的人来说,GitHub Issues 的反馈效率太低,时隔几个小时甚至一天的情况很多。为了改善反馈效率,并将积极为 HMCL 提供问题反馈和建议的人聚集起来,我们希望在这里反馈的人可以在 HMCL 3 用户 QQ 群:219177735 中反馈信息,**而不在 GitHub Issues 中反馈问题**。
如果你希望在 QQ 群中反馈问题,也请将下面的信息表填好直接发在群中,加快我们的沟通速度。
# 问题提交
*完整地填下面的问题提交表对我们很重要,这可以加快我们分析问题原因的速度。*
* 启动器版本:
* 操作系统:
* Java 版本:
* 错误截图(最好请将整个电脑屏幕的截图发上来):
* 游戏版本(如果是启动通过启动器自带的自动安装功能安装的游戏):
* 对游戏做的修改(是否自行通过安装器安装 Rift 等 API,以及 mod):
* 游戏崩溃报告(如果有):
* 启动器崩溃报告(如果有):
* 启动器日志文件(出现问题后点击启动器设置中的【导出启动器日志】按钮):
* 问题描述(如何触发问题):
+5 -1
View File
@@ -25,8 +25,12 @@ NVIDIA
/HMCLCore/out/
# eclipse
/bin/
/HMCL/bin/
/HMCLCore/bin/
.classpath
.project
.settings
# netbeans
.nb-gradle
.nb-gradle
+23 -2
View File
@@ -1,5 +1,6 @@
plugins {
id 'com.github.johnrengelman.shadow' version '2.0.4'
id 'com.github.johnrengelman.shadow' version '4.0.0'
id 'application'
}
import java.nio.file.FileSystems
@@ -23,6 +24,8 @@ if (System.getenv("BUILD_NUMBER") != null && System.getenv("BUILD_NUMBER_OFFSET"
def versionroot = System.getenv("VERSION_ROOT") ?: "3.2"
version = versionroot + '.' + buildnumber
mainClassName = 'org.jackhuang.hmcl.Main'
dependencies {
compile project(":HMCLCore")
compile rootProject.files("lib/JFoenix.jar")
@@ -75,7 +78,7 @@ def repack(File file) {
jar {
manifest {
attributes 'Created-By': 'Copyright(c) 2013-2018 huangyuhui.',
'Main-Class': 'org.jackhuang.hmcl.Main',
'Main-Class': mainClassName,
'Multi-Release': 'true',
'Implementation-Version': version
}
@@ -107,6 +110,24 @@ def createExecutable(String suffix, String header) {
createChecksum(output)
}
processResources {
ext.convertToBSS = { String resource ->
exclude resource
doFirst {
def cssFile = new File(this.projectDir, "src/main/resources/" + resource)
def bssFile = new File(this.projectDir, "build/compiled-resources/" + resource[0..-4] + "bss")
bssFile.parentFile.mkdirs()
exec {
commandLine 'javapackager', '-createbss', '-outdir', bssFile.parent, '-srcfiles', cssFile.path
}
}
}
from "build/compiled-resources"
convertToBSS "assets/css/root.css"
convertToBSS "assets/css/blue.css"
}
task makePack(dependsOn: jar) {
ext.outputPath = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + "pack")
doLast {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,11 +13,11 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.utils.JFXUtilities;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
@@ -26,6 +26,7 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.io.IOException;
@@ -48,22 +49,25 @@ public final class Launcher extends Application {
Thread.currentThread().setUncaughtExceptionHandler(CRASH_REPORTER);
try {
ConfigHolder.init();
} catch (IOException e) {
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
}
try {
ConfigHolder.init();
} catch (IOException e) {
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
}
try {
// When launcher visibility is set to "hide and reopen" without Platform.implicitExit = false,
// Stage.show() cannot work again because JavaFX Toolkit have already shut down.
Platform.setImplicitExit(false);
Controllers.initialize(primaryStage);
primaryStage.setResizable(false);
primaryStage.setScene(Controllers.getScene());
// runLater to ensure ConfigHolder.init() finished initialization
Platform.runLater(() -> {
// When launcher visibility is set to "hide and reopen" without Platform.implicitExit = false,
// Stage.show() cannot work again because JavaFX Toolkit have already shut down.
Platform.setImplicitExit(false);
Controllers.initialize(primaryStage);
primaryStage.setResizable(false);
primaryStage.setScene(Controllers.getScene());
UpdateChecker.init();
UpdateChecker.init();
primaryStage.show();
primaryStage.show();
});
} catch (Throwable e) {
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
}
@@ -72,22 +76,14 @@ public final class Launcher extends Application {
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER);
if (!FileUtils.makeDirectory(LOG_DIRECTORY))
System.out.println("Unable to create log directory " + LOG_DIRECTORY + ", log files cannot be generated.");
try {
Logging.start(LOG_DIRECTORY);
// NetworkUtils.setUserAgentSupplier(() -> "Hello Minecraft! Launcher");
Constants.UI_THREAD_SCHEDULER = Constants.JAVAFX_UI_THREAD_SCHEDULER;
LOG.info("*** " + Metadata.TITLE + " ***");
LOG.info("Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION);
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: " + Paths.get("").toAbsolutePath());
LOG.info("HMCL Directory: " + HMCL_DIRECTORY);
LOG.info("HMCL Directory: " + Metadata.HMCL_DIRECTORY);
LOG.info("Memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB");
ManagementFactory.getMemoryPoolMXBeans().stream().filter(bean -> bean.getName().equals("Metaspace")).findAny()
.ifPresent(bean -> LOG.info("Metaspace: " + bean.getUsage().getUsed() / 1024 / 1024 + "MB"));
@@ -144,9 +140,5 @@ public final class Launcher extends Application {
return result;
}
public static final File MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
public static final File HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
public static final File LOG_DIRECTORY = new File(Launcher.HMCL_DIRECTORY, "logs");
public static final CrashReporter CRASH_REPORTER = new CrashReporter();
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,26 +13,25 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.Logging;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -40,10 +39,16 @@ public final class Main {
public static void main(String[] args) {
System.setProperty("java.net.useSystemProxies", "true");
System.setProperty("http.agent", "HMCL/" + Metadata.VERSION);
System.setProperty("javafx.autoproxy.disable", "true");
checkJavaFX();
checkDirectoryPath();
checkDSTRootCAX3();
// This environment check will take ~300ms
thread(() -> checkDSTRootCAX3(), "CA Certificate Check", true);
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
if (UpdateHandler.processArguments(args)) {
return;
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,11 +13,14 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl;
import org.jackhuang.hmcl.util.JarUtils;
import java.nio.file.Path;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
/**
* Stores metadata about this application.
@@ -33,4 +36,7 @@ public final class Metadata {
public static final String CONTACT_URL = "https://hmcl.huangyuhui.net/contact";
public static final String HELP_URL = "https://hmcl.huangyuhui.net/help";
public static final String PUBLISH_URL = "http://www.mcbbs.net/thread-142335-1-1.html";
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
public static final Path HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
}
@@ -1,176 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.game;
import javafx.geometry.Rectangle2D;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Scheduler;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.util.NetworkUtils;
import java.io.File;
import java.util.*;
public final class AccountHelper {
private AccountHelper() {}
public static final File SKIN_DIR = new File(Launcher.HMCL_DIRECTORY, "skins");
public static void loadSkins() {
for (Account account : Accounts.getAccounts()) {
if (account instanceof YggdrasilAccount) {
new SkinLoadTask((YggdrasilAccount) account, false).start();
}
}
}
public static Task loadSkinAsync(YggdrasilAccount account) {
return new SkinLoadTask(account, false);
}
public static Task refreshSkinAsync(YggdrasilAccount account) {
return new SkinLoadTask(account, true);
}
private static File getSkinFile(UUID uuid) {
return new File(SKIN_DIR, uuid + ".png");
}
public static Image getSkin(YggdrasilAccount account) {
return getSkin(account, 1);
}
public static Image getSkin(YggdrasilAccount account, double scaleRatio) {
UUID uuid = account.getUUID();
if (uuid == null)
return getSteveSkin(scaleRatio);
File file = getSkinFile(uuid);
if (file.exists()) {
Image original = new Image("file:" + file.getAbsolutePath());
return new Image("file:" + file.getAbsolutePath(),
original.getWidth() * scaleRatio,
original.getHeight() * scaleRatio,
false, false);
}
return getDefaultSkin(uuid, scaleRatio);
}
public static Image getSkinImmediately(YggdrasilAccount account, GameProfile profile, double scaleRatio) throws Exception {
File file = getSkinFile(profile.getId());
downloadSkin(account, profile, true);
if (!file.exists())
return getDefaultSkin(profile.getId(), scaleRatio);
String url = "file:" + file.getAbsolutePath();
return scale(url, scaleRatio);
}
public static Rectangle2D getViewport(double scaleRatio) {
double size = 8.0 * scaleRatio;
return new Rectangle2D(size, size, size, size);
}
private static class SkinLoadTask extends Task {
private final YggdrasilAccount account;
private final boolean refresh;
private final List<Task> dependencies = new LinkedList<>();
public SkinLoadTask(YggdrasilAccount account, boolean refresh) {
this.account = account;
this.refresh = refresh;
}
@Override
public Scheduler getScheduler() {
return Schedulers.io();
}
@Override
public Collection<Task> getDependencies() {
return dependencies;
}
@Override
public void execute() throws Exception {
if (!account.isLoggedIn() && (account.getCharacter() == null || refresh))
DialogController.logIn(account);
downloadSkin(account, refresh);
}
}
private static void downloadSkin(YggdrasilAccount account, GameProfile profile, boolean refresh) throws Exception {
account.clearCache();
Optional<Texture> texture = account.getSkin(profile);
if (!texture.isPresent()) return;
String url = texture.get().getUrl();
File file = getSkinFile(profile.getId());
if (!refresh && file.exists())
return;
new FileDownloadTask(NetworkUtils.toURL(url), file).run();
}
private static void downloadSkin(YggdrasilAccount account, boolean refresh) throws Exception {
account.clearCache();
if (account.getCharacter() == null) return;
Optional<Texture> texture = account.getSkin();
if (!texture.isPresent()) return;
String url = texture.get().getUrl();
File file = getSkinFile(account.getUUID());
if (!refresh && file.exists())
return;
new FileDownloadTask(NetworkUtils.toURL(url), file).run();
}
public static Image scale(String url, double scaleRatio) {
Image origin = new Image(url);
return new Image(url,
origin.getWidth() * scaleRatio,
origin.getHeight() * scaleRatio,
false, false);
}
public static Image getSteveSkin(double scaleRatio) {
return scale("/assets/img/steve.png", 4);
}
public static Image getAlexSkin(double scaleRatio) {
return scale("/assets/img/alex.png", 4);
}
public static Image getDefaultSkin(UUID uuid, double scaleRatio) {
int type = uuid.hashCode() & 1;
if (type == 1)
return getAlexSkin(scaleRatio);
else
return getSteveSkin(scaleRatio);
}
}
@@ -1,3 +1,20 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import javafx.beans.property.SimpleStringProperty;
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,21 +13,18 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
@@ -80,18 +77,21 @@ public class HMCLGameRepository extends DefaultGameRepository {
}
}
@Override
public void refreshVersions() {
EventBus.EVENT_BUS.fireEvent(new RefreshingVersionsEvent(this));
refreshVersionsImpl();
EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this));
}
public void changeDirectory(File 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"));
}
public void clean(String id) throws IOException {
clean(getBaseDirectory());
clean(getRunDirectory(id));
}
private File getVersionSettingFile(String id) {
return new File(getVersionRoot(id), "hmclversion.cfg");
}
@@ -144,10 +144,26 @@ public class HMCLGameRepository extends DefaultGameRepository {
return setting;
}
public File getVersionIcon(String id) {
public File getVersionIconFile(String id) {
return new File(getVersionRoot(id), "icon.png");
}
public Image getVersionIconImage(String id) {
if (id == null || !isLoaded())
return new Image("/assets/img/grass.png");
Version version = getVersion(id);
File iconFile = getVersionIconFile(id);
if (iconFile.exists())
return new Image("file:" + iconFile.getAbsolutePath());
else if ("net.minecraft.launchwrapper.Launch".equals(version.getMainClass()))
return new Image("/assets/img/furnace.png");
else if ("cpw.mods.modlauncher.Launcher".equals(version.getMainClass()))
return new Image("/assets/img/furnace.png");
else
return new Image("/assets/img/grass.png");
}
public boolean saveVersionSetting(String id) {
if (!versionSettings.containsKey(id))
return false;
@@ -202,8 +218,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
beingModpackVersions.remove(id);
}
private static final Gson GSON = new GsonBuilder().setPrettyPrinting()
.registerTypeAdapter(VersionSetting.class, VersionSetting.Serializer.INSTANCE)
private static final Gson GSON = new GsonBuilder()
.setPrettyPrinting()
.create();
private static final HashSet<String> FORBIDDEN = new HashSet<>(Arrays.asList("modpack", "minecraftinstance", "manifest"));
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,15 +13,15 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.Zipper;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.Zipper;
import java.io.File;
import java.util.ArrayList;
@@ -30,7 +30,7 @@ import java.util.List;
/**
* Export the game to a mod pack file.
*/
public class HMCLModpackExportTask extends Task {
public class HMCLModpackExportTask extends Task<Void> {
private final DefaultGameRepository repository;
private final String version;
private final List<String> whitelist;
@@ -61,6 +61,8 @@ public class HMCLModpackExportTask extends Task {
Logging.LOG.info("Compressing game files without some files in blacklist, including files or directories: usernamecache.json, asm, logs, backups, versions, assets, usercache.json, libraries, crash-reports, launcher_profiles.json, NVIDIA, TCNodeTracker");
try (Zipper zip = new Zipper(output.toPath())) {
zip.putDirectory(repository.getRunDirectory(version).toPath(), "minecraft", path -> {
if (path.isEmpty())
return true;
for (String s : blackList)
if (path.equals(s))
return false;
@@ -73,8 +75,8 @@ public class HMCLModpackExportTask extends Task {
Version mv = repository.getResolvedVersion(version);
String gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version))
.orElseThrow(() -> new IllegalStateException("Cannot parse the version of " + version));
zip.putTextFile(Constants.GSON.toJson(mv.setJar(gameVersion)), "minecraft/pack.json"); // Making "jar" to gameVersion is to be compatible with old HMCL.
zip.putTextFile(Constants.GSON.toJson(modpack.setGameVersion(gameVersion)), "modpack.json"); // Newer HMCL only reads 'gameVersion' field.
zip.putTextFile(JsonUtils.GSON.toJson(mv.setJar(gameVersion)), "minecraft/pack.json"); // Making "jar" to gameVersion is to be compatible with old HMCL.
zip.putTextFile(JsonUtils.GSON.toJson(modpack.setGameVersion(gameVersion)), "modpack.json"); // Newer HMCL only reads 'gameVersion' field.
}
}
}
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
@@ -27,22 +27,22 @@ import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.ModpackInstallTask;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.CompressingUtils;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.FileUtils;
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.util.LinkedList;
import java.util.List;
public final class HMCLModpackInstallTask extends Task {
public final class HMCLModpackInstallTask extends Task<Void> {
private final File zipFile;
private final String name;
private final HMCLGameRepository repository;
private final Modpack modpack;
private final List<Task> dependencies = new LinkedList<>();
private final List<Task> dependents = new LinkedList<>();
private final List<Task<?>> dependencies = new LinkedList<>();
private final List<Task<?>> dependents = new LinkedList<>();
public HMCLModpackInstallTask(Profile profile, File zipFile, Modpack modpack, String name) {
DependencyManager dependency = profile.getDependency();
@@ -65,7 +65,7 @@ public final class HMCLModpackInstallTask extends Task {
ModpackConfiguration<Modpack> config = null;
try {
if (json.exists()) {
config = Constants.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<Modpack>>() {
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<Modpack>>() {
}.getType());
if (!MODPACK_TYPE.equals(config.getType()))
@@ -73,25 +73,25 @@ public final class HMCLModpackInstallTask extends Task {
}
} catch (JsonParseException | IOException ignore) {
}
dependents.add(new ModpackInstallTask<>(zipFile, run, "/minecraft", it -> !"pack.json".equals(it), config));
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), "/minecraft", it -> !"pack.json".equals(it), config));
}
@Override
public List<Task> getDependencies() {
public List<Task<?>> getDependencies() {
return dependencies;
}
@Override
public List<Task> getDependents() {
public List<Task<?>> getDependents() {
return dependents;
}
@Override
public void execute() throws Exception {
String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json");
Version version = Constants.GSON.fromJson(json, Version.class).setId(name).setJar(null);
Version version = JsonUtils.GSON.fromJson(json, Version.class).setId(name).setJar(null);
dependencies.add(new VersionJsonSaveTask(repository, version));
dependencies.add(new MinecraftInstanceTask<>(zipFile, "/minecraft", modpack, MODPACK_TYPE, repository.getModpackConfiguration(name)));
dependencies.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), "/minecraft", modpack, MODPACK_TYPE, repository.getModpackConfiguration(name)));
}
public static final String MODPACK_TYPE = "HMCL";
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,19 +13,20 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.util.CompressingUtils;
import org.jackhuang.hmcl.util.JsonUtils;
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.CompressingUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.List;
/**
@@ -36,7 +37,7 @@ public final class HMCLModpackManager {
public static final List<String> MODPACK_BLACK_LIST = Lang.immutableListOf(
"usernamecache.json", "usercache.json", // Minecraft
"launcher_profiles.json", "launcher.pack.lzma", // Minecraft Launcher
"pack.json", "launcher.jar", "hmclmc.log", // HMCL
"pack.json", "launcher.jar", "hmclmc.log", "cache", // HMCL
"manifest.json", "minecraftinstance.json", ".curseclient", // Curse
"minetweaker.log", // Mods
"jars", "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft
@@ -75,14 +76,15 @@ public final class HMCLModpackManager {
* Read the manifest in a HMCL modpack.
*
* @param file a HMCL modpack file.
* @param encoding encoding of modpack zip file.
* @throws IOException if the file is not a valid zip file.
* @throws JsonParseException if the manifest.json is missing or malformed.
* @return the manifest of HMCL modpack.
*/
public static Modpack readHMCLModpackManifest(File file) throws IOException, JsonParseException {
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json");
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, Modpack.class);
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json");
public static Modpack readHMCLModpackManifest(Path file, Charset encoding) throws IOException, JsonParseException {
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json", encoding);
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, Modpack.class).setEncoding(encoding);
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json", encoding);
Version game = JsonUtils.fromNonNullJson(gameJson, Version.class);
if (game.getJar() == null)
if (StringUtils.isBlank(manifest.getVersion()))
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
@@ -38,11 +38,22 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.ui.LogWindow;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
@@ -51,54 +62,74 @@ import java.util.concurrent.atomic.AtomicInteger;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class LauncherHelper {
public static final LauncherHelper INSTANCE = new LauncherHelper();
private LauncherHelper(){}
public static final Queue<ManagedProcess> PROCESSES = new ConcurrentLinkedQueue<>();
private final Profile profile;
private final Account account;
private final String selectedVersion;
private File scriptFile;
private final VersionSetting setting;
private LauncherVisibility launcherVisibility;
private boolean showLogs;
public LauncherHelper(Profile profile, Account account, String selectedVersion) {
this.profile = Objects.requireNonNull(profile);
this.account = Objects.requireNonNull(account);
this.selectedVersion = Objects.requireNonNull(selectedVersion);
this.setting = profile.getVersionSetting(selectedVersion);
this.launcherVisibility = setting.getLauncherVisibility();
this.showLogs = setting.isShowLogs();
}
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(it -> {});
public void launch(Profile profile, Account account, String selectedVersion, File scriptFile) {
if (account == null)
throw new IllegalArgumentException("No account");
public void setTestMode() {
launcherVisibility = LauncherVisibility.KEEP;
showLogs = true;
}
public void launch() {
Logging.LOG.info("Launching game version: " + selectedVersion);
GameRepository repository = profile.getRepository();
Version version = repository.getResolvedVersion(selectedVersion);
VersionSetting setting = profile.getVersionSetting(selectedVersion);
Platform.runLater(() -> {
try {
checkGameState(profile, setting, version, () -> {
Controllers.dialog(launchingStepsPane);
Schedulers.newThread().schedule(() -> launch0(profile, account, selectedVersion, scriptFile));
Schedulers.newThread().execute(this::launch0);
});
} catch (InterruptedException ignore) {
}
});
}
private void launch0(Profile profile, Account account, String selectedVersion, File scriptFile) {
public void makeLaunchScript(File scriptFile) {
this.scriptFile = Objects.requireNonNull(scriptFile);
launch();
}
private void launch0() {
HMCLGameRepository repository = profile.getRepository();
DefaultDependencyManager dependencyManager = profile.getDependency();
Version version = MaintainTask.maintain(repository.getResolvedVersion(selectedVersion));
VersionSetting setting = profile.getVersionSetting(selectedVersion);
Optional<String> gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version));
TaskExecutor executor = Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.DEPENDENCIES))
.then(variables -> {
TaskExecutor executor = Task.runAsync(Schedulers.javafx(), () -> emitStatus(LoadingState.DEPENDENCIES))
.thenCompose(() -> {
if (setting.isNotCheckGame())
return null;
else
return dependencyManager.checkGameCompletionAsync(version);
})
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.MODS)))
.then(var -> {
.thenRun(Schedulers.javafx(), () -> emitStatus(LoadingState.MODS))
.thenCompose(() -> {
try {
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
if ("Curse".equals(configuration.getType()))
@@ -109,66 +140,66 @@ public final class LauncherHelper {
return null;
}
})
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.LOGGING_IN)))
.then(Task.of(i18n("account.methods"), variables -> {
.thenRun(Schedulers.javafx(), () -> emitStatus(LoadingState.LOGGING_IN))
.thenSupply(i18n("account.methods"), () -> {
try {
variables.set("account", account.logIn());
return account.logIn();
} catch (CredentialExpiredException e) {
variables.set("account", DialogController.logIn(account));
LOG.info("Credential has expired: " + e);
return DialogController.logIn(account);
} catch (AuthenticationException e) {
variables.set("account",
account.playOffline().orElseThrow(() -> e));
LOG.warning("Authentication failed, try playing offline: " + e);
return account.playOffline().orElseThrow(() -> e);
}
}))
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.LAUNCHING)))
.then(Task.of(variables -> {
variables.set("launcher", new HMCLGameLauncher(
repository,
selectedVersion,
variables.get("account"),
setting.toLaunchOptions(profile.getGameDir()),
setting.getLauncherVisibility() == LauncherVisibility.CLOSE
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
: new HMCLProcessListener(variables.get("account"), setting, gameVersion.isPresent())
));
}))
.then(variables -> {
DefaultLauncher launcher = variables.get("launcher");
})
.thenApply(Schedulers.javafx(), authInfo -> {
emitStatus(LoadingState.LAUNCHING);
return authInfo;
})
.thenApply(authInfo -> new HMCLGameLauncher(
repository,
selectedVersion,
authInfo,
setting.toLaunchOptions(profile.getGameDir()),
launcherVisibility == LauncherVisibility.CLOSE
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
: new HMCLProcessListener(authInfo, gameVersion.isPresent())
))
.thenCompose(launcher -> { // launcher is prev task's result
if (scriptFile == null) {
return new LaunchTask<>(launcher::launch).setName(i18n("version.launch"));
} else {
return new LaunchTask<>(() -> {
return new LaunchTask<ManagedProcess>(() -> {
launcher.makeLaunchScript(scriptFile);
return null;
}).setName(i18n("version.launch_script"));
}
})
.then(Task.of(variables -> {
.thenAccept(process -> { // process is LaunchTask's result
if (scriptFile == null) {
ManagedProcess process = variables.get(LaunchTask.LAUNCH_ID);
PROCESSES.add(process);
if (setting.getLauncherVisibility() == LauncherVisibility.CLOSE)
if (launcherVisibility == LauncherVisibility.CLOSE)
Launcher.stopApplication();
else
launchingStepsPane.setCancel(it -> {
process.stop();
it.fireEvent(new DialogCloseEvent());
});
} else
} else {
Platform.runLater(() -> {
launchingStepsPane.fireEvent(new DialogCloseEvent());
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
});
}))
}
})
.executor();
launchingStepsPane.setExecutor(executor);
launchingStepsPane.setExecutor(executor, false);
executor.addTaskListener(new TaskListener() {
final AtomicInteger finished = new AtomicInteger(0);
@Override
public void onFinished(Task task) {
public void onFinished(Task<?> task) {
finished.incrementAndGet();
int runningTasks = executor.getRunningTasks();
Platform.runLater(() -> launchingStepsPane.setProgress(1.0 * finished.get() / runningTasks));
@@ -182,11 +213,14 @@ public final class LauncherHelper {
// because onStop will be invoked if tasks fail when the executor service shut down.
if (!Controllers.isStopped()) {
launchingStepsPane.fireEvent(new DialogCloseEvent());
Exception ex = executor.getLastException();
Exception ex = executor.getException();
if (ex != null) {
String message;
if (ex instanceof CurseCompletionException) {
message = i18n("modpack.type.curse.error");
if (ex.getCause() instanceof FileNotFoundException)
message = i18n("modpack.type.curse.not_found");
else
message = i18n("modpack.type.curse.error");
} else if (ex instanceof PermissionException) {
message = i18n("launch.failed.executable_permission");
} else if (ex instanceof ProcessCreationException) {
@@ -200,7 +234,7 @@ public final class LauncherHelper {
}
Controllers.dialog(message,
scriptFile == null ? i18n("launch.failed") : i18n("version.launch_script.failed"),
MessageBox.ERROR_MESSAGE);
MessageType.ERROR);
}
}
});
@@ -213,6 +247,11 @@ public final class LauncherHelper {
}
private static void checkGameState(Profile profile, VersionSetting setting, Version version, Runnable onAccept) throws InterruptedException {
if (setting.isNotCheckJVM()) {
onAccept.run();
return;
}
boolean flag = false;
boolean java8required = false;
boolean newJavaRequired = false;
@@ -222,7 +261,7 @@ public final class LauncherHelper {
VersionNumber gameVersion = VersionNumber.asVersion(GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version)).orElse("Unknown"));
JavaVersion java = setting.getJavaVersion();
if (java == null) {
Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.warning"), MessageBox.WARNING_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.warning"), MessageType.WARNING, onAccept);
setting.setJava(null);
setting.setDefaultJavaPath(null);
java = JavaVersion.fromCurrentEnvironment();
@@ -231,7 +270,7 @@ public final class LauncherHelper {
// Game later than 1.7.2 accepts Java 8.
if (!flag && java.getParsedVersion() < JavaVersion.JAVA_8 && gameVersion.compareTo(VersionNumber.asVersion("1.7.2")) > 0) {
Optional<JavaVersion> java8 = JavaVersion.getJREs().stream()
Optional<JavaVersion> java8 = JavaVersion.getJavas().stream()
.filter(javaVersion -> javaVersion.getParsedVersion() >= JavaVersion.JAVA_8)
.max(Comparator.comparing(JavaVersion::getVersionNumber));
if (java8.isPresent()) {
@@ -241,10 +280,10 @@ public final class LauncherHelper {
if (gameVersion.compareTo(VersionNumber.asVersion("1.13")) >= 0) {
// Minecraft 1.13 and later versions only support Java 8 or later.
// Terminate launching operation.
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageType.ERROR, null);
} else {
// Most mods require Java 8 or later version.
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.warning"), MessageBox.WARNING_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.warning"), MessageType.WARNING, onAccept);
}
flag = true;
}
@@ -256,38 +295,40 @@ public final class LauncherHelper {
&& version.getLibraries().stream()
.filter(library -> "launchwrapper".equals(library.getArtifactId()))
.anyMatch(library -> VersionNumber.asVersion(library.getVersion()).compareTo(VersionNumber.asVersion("1.13")) < 0)) {
Optional<JavaVersion> java8 = JavaVersion.getJREs().stream().filter(javaVersion -> javaVersion.getParsedVersion() == JavaVersion.JAVA_8).findAny();
Optional<JavaVersion> java8 = JavaVersion.getJavas().stream().filter(javaVersion -> javaVersion.getParsedVersion() == JavaVersion.JAVA_8).findAny();
if (java8.isPresent()) {
java8required = true;
setting.setJavaVersion(java8.get());
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.corrected"), i18n("message.info"), MessageType.INFORMATION, onAccept);
flag = true;
} else {
Controllers.dialog(i18n("launch.advice.java9"), i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"), i18n("message.error"), MessageType.ERROR, null);
flag = true;
}
}
// Minecraft 1.13 may crash when generating world on Java 8 earlier than 1.8.0_51
VersionNumber JAVA_8 = VersionNumber.asVersion("1.8.0.51");
VersionNumber JAVA_8 = VersionNumber.asVersion("1.8.0_51");
if (!flag && gameVersion.compareTo(VersionNumber.asVersion("1.13")) >= 0 && java.getParsedVersion() == JavaVersion.JAVA_8 && java.getVersionNumber().compareTo(JAVA_8) < 0) {
Optional<JavaVersion> java8 = JavaVersion.getJREs().stream()
Optional<JavaVersion> java8 = JavaVersion.getJavas().stream()
.filter(javaVersion -> javaVersion.getVersionNumber().compareTo(JAVA_8) >= 0)
.max(Comparator.comparing(JavaVersion::getVersionNumber));
if (java8.isPresent()) {
newJavaRequired = true;
setting.setJavaVersion(java8.get());
} else {
Controllers.dialog(i18n("launch.advice.java8_51_1_13"), i18n("message.warning"), MessageBox.WARNING_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.advice.java8_51_1_13"), i18n("message.warning"), MessageType.WARNING, onAccept);
flag = true;
}
}
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.Platform.BIT_32 &&
org.jackhuang.hmcl.util.Platform.IS_64_BIT) {
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_32 &&
org.jackhuang.hmcl.util.platform.Platform.IS_64_BIT) {
final JavaVersion java32 = java;
// First find if same java version but whose platform is 64-bit installed.
Optional<JavaVersion> java64 = JavaVersion.getJREs().stream()
.filter(javaVersion -> javaVersion.getPlatform() == org.jackhuang.hmcl.util.Platform.PLATFORM)
Optional<JavaVersion> java64 = JavaVersion.getJavas().stream()
.filter(javaVersion -> javaVersion.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.PLATFORM)
.filter(javaVersion -> javaVersion.getParsedVersion() == java32.getParsedVersion())
.max(Comparator.comparing(JavaVersion::getVersionNumber));
@@ -295,8 +336,8 @@ public final class LauncherHelper {
final boolean java8requiredFinal = java8required, newJavaRequiredFinal = newJavaRequired;
// Then find if other java version which satisfies requirements installed.
java64 = JavaVersion.getJREs().stream()
.filter(javaVersion -> javaVersion.getPlatform() == org.jackhuang.hmcl.util.Platform.PLATFORM)
java64 = JavaVersion.getJavas().stream()
.filter(javaVersion -> javaVersion.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.PLATFORM)
.filter(javaVersion -> {
if (java8requiredFinal) return javaVersion.getParsedVersion() == JavaVersion.JAVA_8;
if (newJavaRequiredFinal) return javaVersion.getParsedVersion() >= JavaVersion.JAVA_8;
@@ -308,33 +349,43 @@ public final class LauncherHelper {
if (java64.isPresent()) {
setting.setJavaVersion(java64.get());
} else {
Controllers.dialog(i18n("launch.advice.different_platform"), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.advice.different_platform"), i18n("message.error"), MessageType.ERROR, onAccept);
flag = true;
}
}
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.Platform.BIT_32 &&
// 32-bit JVM cannot make use of too much memory.
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_32 &&
setting.getMaxMemory() > 1.5 * 1024) {
// 1.5 * 1024 is an inaccurate number.
// Actual memory limit depends on operating system and memory.
Controllers.dialog(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), MessageType.ERROR, onAccept);
flag = true;
}
// Cannot allocate too much memory exceeding free space.
if (!flag && OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
Controllers.dialog(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), MessageType.ERROR, onAccept);
flag = true;
}
// Forge 2760~2773 will crash game with LiteLoader.
if (!flag) {
boolean hasForge2760 = version.getLibraries().stream().filter(it -> it.is("net.minecraftforge", "forge"))
.anyMatch(it ->
VersionNumber.VERSION_COMPARATOR.compare("1.12.2-14.23.5.2760", it.getVersion()) <= 0 &&
VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "1.12.2-14.23.5.2773") < 0);
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo(VersionNumber.asVersion("1.12.2")) == 0) {
Controllers.dialog(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), MessageType.ERROR, onAccept);
flag = true;
}
}
if (!flag)
onAccept.run();
}
public static void stopManagedProcesses() {
while (!PROCESSES.isEmpty())
Optional.ofNullable(PROCESSES.poll()).ifPresent(ManagedProcess::stop);
}
public void emitStatus(LoadingState state) {
if (state == LoadingState.DONE) {
launchingStepsPane.fireEvent(new DialogCloseEvent());
@@ -344,8 +395,8 @@ public final class LauncherHelper {
launchingStepsPane.setSubtitle((state.ordinal() + 1) + " / " + LoadingState.values().length);
}
private void checkExit(LauncherVisibility v) {
switch (v) {
private void checkExit() {
switch (launcherVisibility) {
case HIDE_AND_REOPEN:
Platform.runLater(Controllers.getStage()::show);
break;
@@ -365,7 +416,7 @@ public final class LauncherHelper {
}
}
private static class LaunchTask<T> extends TaskResult<T> {
private static class LaunchTask<T> extends Task<T> {
private final ExceptionalSupplier<T, Exception> supplier;
public LaunchTask(ExceptionalSupplier<T, Exception> supplier) {
@@ -376,13 +427,6 @@ public final class LauncherHelper {
public void execute() throws Exception {
setResult(supplier.get());
}
@Override
public String getId() {
return LAUNCH_ID;
}
static final String LAUNCH_ID = "launch";
}
/**
@@ -392,9 +436,7 @@ public final class LauncherHelper {
*/
class HMCLProcessListener implements ProcessListener {
private final VersionSetting setting;
private final Map<String, String> forbiddenTokens;
private final LauncherVisibility visibility;
private ManagedProcess process;
private boolean lwjgl;
private LogWindow logWindow;
@@ -402,8 +444,7 @@ public final class LauncherHelper {
private final LinkedList<Pair<String, Log4jLevel>> logs;
private final CountDownLatch latch = new CountDownLatch(1);
public HMCLProcessListener(AuthInfo authInfo, VersionSetting setting, boolean detectWindow) {
this.setting = setting;
public HMCLProcessListener(AuthInfo authInfo, boolean detectWindow) {
this.detectWindow = detectWindow;
if (authInfo == null)
@@ -415,7 +456,6 @@ public final class LauncherHelper {
pair(authInfo.getUsername(), "<player>")
);
visibility = setting.getLauncherVisibility();
logs = new LinkedList<>();
}
@@ -423,7 +463,7 @@ public final class LauncherHelper {
public void setProcess(ManagedProcess process) {
this.process = process;
if (setting.isShowLogs())
if (showLogs)
Platform.runLater(() -> {
logWindow = new LogWindow();
logWindow.show();
@@ -431,6 +471,39 @@ public final class LauncherHelper {
});
}
private void finishLaunch() {
switch (launcherVisibility) {
case HIDE_AND_REOPEN:
Platform.runLater(() -> {
// If application was stopped and execution services did not finish termination,
// these codes will be executed.
if (Controllers.getStage() != null) {
Controllers.getStage().hide();
emitStatus(LoadingState.DONE);
}
});
break;
case CLOSE:
// Never come to here.
break;
case KEEP:
Platform.runLater(() -> {
emitStatus(LoadingState.DONE);
});
break;
case HIDE:
Platform.runLater(() -> {
// If application was stopped and execution services did not finish termination,
// these codes will be executed.
if (Controllers.getStage() != null) {
Controllers.getStage().close();
emitStatus(LoadingState.DONE);
}
});
break;
}
}
@Override
public synchronized void onLog(String log, Log4jLevel level) {
String newLog = log;
@@ -446,7 +519,7 @@ public final class LauncherHelper {
if (logs.size() > config().getLogLines())
logs.removeFirst();
if (setting.isShowLogs()) {
if (showLogs) {
try {
latch.await();
logWindow.waitForLoaded();
@@ -460,28 +533,7 @@ public final class LauncherHelper {
if (!lwjgl && (log.contains("LWJGL Version: ") || !detectWindow)) {
lwjgl = true;
switch (visibility) {
case HIDE_AND_REOPEN:
Platform.runLater(() -> {
Controllers.getStage().hide();
emitStatus(LoadingState.DONE);
});
break;
case CLOSE:
// Never come to here.
break;
case KEEP:
Platform.runLater(() -> {
emitStatus(LoadingState.DONE);
});
break;
case HIDE:
Platform.runLater(() -> {
Controllers.getStage().close();
emitStatus(LoadingState.DONE);
});
break;
}
finishLaunch();
}
}
@@ -490,6 +542,9 @@ public final class LauncherHelper {
if (exitType == ExitType.INTERRUPTED)
return;
// Game crashed before opening the game window.
if (!lwjgl) finishLaunch();
if (exitType != ExitType.NORMAL && logWindow == null)
Platform.runLater(() -> {
logWindow = new LogWindow();
@@ -511,8 +566,14 @@ public final class LauncherHelper {
});
});
checkExit(visibility);
checkExit();
}
}
public static final Queue<ManagedProcess> PROCESSES = new ConcurrentLinkedQueue<>();
public static void stopManagedProcesses() {
while (!PROCESSES.isEmpty())
Optional.ofNullable(PROCESSES.poll()).ifPresent(ManagedProcess::stop);
}
}
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
@@ -23,35 +23,39 @@ import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.FinalizedCallback;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.Optional;
public final class ModpackHelper {
private ModpackHelper() {}
public static Modpack readModpackManifest(File file) throws UnsupportedModpackException {
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException {
try {
return CurseManifest.readCurseForgeModpackManifest(file);
return CurseManifest.readCurseForgeModpackManifest(file, charset);
} catch (Exception e) {
// ignore it, not a valid CurseForge modpack.
}
try {
return HMCLModpackManager.readHMCLModpackManifest(file);
return HMCLModpackManager.readHMCLModpackManifest(file, charset);
} catch (Exception e) {
// ignore it, not a valid HMCL modpack.
}
try {
return MultiMCInstanceConfiguration.readMultiMCModpackManifest(file);
return MultiMCInstanceConfiguration.readMultiMCModpackManifest(file, charset);
} catch (Exception e) {
// ignore it, not a valid MultiMC modpack.
}
@@ -64,7 +68,7 @@ public final class ModpackHelper {
throw new FileNotFoundException(file.getPath());
else
try {
return Constants.GSON.fromJson(FileUtils.readText(file), new TypeToken<ModpackConfiguration<?>>() {
return JsonUtils.GSON.fromJson(FileUtils.readText(file), new TypeToken<ModpackConfiguration<?>>() {
}.getType());
} catch (JsonParseException e) {
throw new IOException("Malformed modpack configuration");
@@ -82,52 +86,60 @@ public final class ModpackHelper {
throw new UnsupportedModpackException();
}
public static Task getInstallTask(Profile profile, File zipFile, String name, Modpack modpack) {
public static Task<Void> getInstallTask(Profile profile, File zipFile, String name, Modpack modpack) {
profile.getRepository().markVersionAsModpack(name);
FinalizedCallback finalizeTask = (variables, isDependentsSucceeded) -> {
if (isDependentsSucceeded) {
ExceptionalRunnable<?> success = () -> {
HMCLGameRepository repository = profile.getRepository();
repository.refreshVersions();
VersionSetting vs = repository.specializeVersionSetting(name);
repository.undoMark(name);
if (vs != null)
vs.setGameDirType(EnumGameDirectory.VERSION_FOLDER);
};
ExceptionalConsumer<Exception, ?> failure = ex -> {
if (ex instanceof CurseCompletionException && !(ex.getCause() instanceof FileNotFoundException)) {
success.run();
// This is tolerable and we will not delete the game
} else {
HMCLGameRepository repository = profile.getRepository();
repository.refreshVersions();
VersionSetting vs = repository.specializeVersionSetting(name);
repository.undoMark(name);
if (vs != null)
vs.setGameDirType(EnumGameDirectory.VERSION_FOLDER);
repository.removeVersionFromDisk(name);
}
};
if (modpack.getManifest() instanceof CurseManifest)
return new CurseInstallTask(profile.getDependency(), zipFile, ((CurseManifest) modpack.getManifest()), name)
.finalized(finalizeTask);
return new CurseInstallTask(profile.getDependency(), zipFile, modpack, ((CurseManifest) modpack.getManifest()), name)
.whenComplete(Schedulers.defaultScheduler(), success, failure);
else if (modpack.getManifest() instanceof HMCLModpackManifest)
return new HMCLModpackInstallTask(profile, zipFile, modpack, name)
.finalized(finalizeTask);
.whenComplete(Schedulers.defaultScheduler(), success, failure);
else if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name)
.finalized(finalizeTask)
.then(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name));
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, ((MultiMCInstanceConfiguration) modpack.getManifest()), name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenCompose(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name));
else throw new IllegalStateException("Unrecognized modpack: " + modpack);
}
public static Task getUpdateTask(Profile profile, File zipFile, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, MismatchedModpackTypeException {
Modpack modpack = ModpackHelper.readModpackManifest(zipFile);
public static Task<Void> getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, MismatchedModpackTypeException {
Modpack modpack = ModpackHelper.readModpackManifest(zipFile.toPath(), charset);
switch (configuration.getType()) {
case CurseInstallTask.MODPACK_TYPE:
if (!(modpack.getManifest() instanceof CurseManifest))
throw new MismatchedModpackTypeException(CurseInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
return new CurseInstallTask(profile.getDependency(), zipFile, (CurseManifest) modpack.getManifest(), name);
return new ModpackUpdateTask(profile.getRepository(), name, new CurseInstallTask(profile.getDependency(), zipFile, modpack, (CurseManifest) modpack.getManifest(), name));
case MultiMCModpackInstallTask.MODPACK_TYPE:
if (!(modpack.getManifest() instanceof MultiMCInstanceConfiguration))
throw new MismatchedModpackTypeException(MultiMCModpackInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, (MultiMCInstanceConfiguration) modpack.getManifest(), name);
return new ModpackUpdateTask(profile.getRepository(), name, new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
case HMCLModpackInstallTask.MODPACK_TYPE:
if (!(modpack.getManifest() instanceof HMCLModpackManifest))
throw new MismatchedModpackTypeException(HMCLModpackInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
return new HMCLModpackInstallTask(profile, zipFile, modpack, name);
return new ModpackUpdateTask(profile.getRepository(), name, new HMCLModpackInstallTask(profile, zipFile, modpack, name));
default:
throw new UnsupportedModpackException();
}
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,20 +13,19 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.mod.MultiMCInstanceConfiguration;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.Scheduler;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import java.util.Objects;
public final class MultiMCInstallVersionSettingTask extends Task {
public final class MultiMCInstallVersionSettingTask extends Task<Void> {
private final Profile profile;
private final MultiMCInstanceConfiguration manifest;
private final String version;
@@ -35,11 +34,8 @@ public final class MultiMCInstallVersionSettingTask extends Task {
this.profile = profile;
this.manifest = manifest;
this.version = version;
}
@Override
public Scheduler getScheduler() {
return Schedulers.javafx();
setExecutor(Schedulers.javafx());
}
@Override
@@ -0,0 +1,221 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.util.Lang.threadPool;
import static org.jackhuang.hmcl.util.Logging.LOG;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import javax.imageio.ImageIO;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image;
/**
* @author yushijinhun
*/
public final class TexturesLoader {
private TexturesLoader() {
}
// ==== Texture Loading ====
public static class LoadedTexture {
private final BufferedImage image;
private final Map<String, String> metadata;
public LoadedTexture(BufferedImage image, Map<String, String> metadata) {
this.image = requireNonNull(image);
this.metadata = requireNonNull(metadata);
}
public BufferedImage getImage() {
return image;
}
public Map<String, String> getMetadata() {
return metadata;
}
}
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 Path getTexturePath(Texture texture) {
String url = texture.getUrl();
int slash = url.lastIndexOf('/');
int dot = url.lastIndexOf('.');
if (dot < slash) {
dot = url.length();
}
String hash = url.substring(slash + 1, dot);
String prefix = hash.length() > 2 ? hash.substring(0, 2) : "xx";
return TEXTURES_DIR.resolve(prefix).resolve(hash);
}
public static LoadedTexture loadTexture(Texture texture) throws IOException {
if (StringUtils.isBlank(texture.getUrl())) {
throw new IOException("Texture url is empty");
}
Path file = getTexturePath(texture);
if (!Files.isRegularFile(file)) {
// download it
try {
new FileDownloadTask(new URL(texture.getUrl()), file.toFile()).run();
LOG.info("Texture downloaded: " + texture.getUrl());
} catch (Exception e) {
if (Files.isRegularFile(file)) {
// concurrency conflict?
LOG.log(Level.WARNING, "Failed to download texture " + texture.getUrl() + ", but the file is available", e);
} else {
throw new IOException("Failed to download texture " + texture.getUrl());
}
}
}
BufferedImage img;
try (InputStream in = Files.newInputStream(file)) {
img = ImageIO.read(in);
}
Map<String, String> metadata = texture.getMetadata();
if (metadata == null) {
metadata = emptyMap();
}
return new LoadedTexture(img, metadata);
}
// ====
// ==== Skins ====
private final static Map<TextureModel, LoadedTexture> DEFAULT_SKINS = new EnumMap<>(TextureModel.class);
static {
loadDefaultSkin("/assets/img/steve.png", TextureModel.STEVE);
loadDefaultSkin("/assets/img/alex.png", TextureModel.ALEX);
}
private static void loadDefaultSkin(String path, TextureModel model) {
try (InputStream in = TexturesLoader.class.getResourceAsStream(path)) {
DEFAULT_SKINS.put(model, new LoadedTexture(ImageIO.read(in), singletonMap("model", model.modelName)));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
public static LoadedTexture getDefaultSkin(TextureModel model) {
return DEFAULT_SKINS.get(model);
}
public static ObjectBinding<LoadedTexture> skinBinding(YggdrasilService service, UUID uuid) {
LoadedTexture uuidFallback = getDefaultSkin(TextureModel.detectUUID(uuid));
return BindingMapping.of(service.getProfileRepository().binding(uuid))
.map(profile -> profile
.flatMap(it -> {
try {
return YggdrasilService.getTextures(it);
} catch (ServerResponseMalformedException e) {
LOG.log(Level.WARNING, "Failed to parse texture payload", e);
return Optional.empty();
}
})
.flatMap(it -> Optional.ofNullable(it.get(TextureType.SKIN)))
.filter(it -> StringUtils.isNotBlank(it.getUrl())))
.asyncMap(it -> {
if (it.isPresent()) {
Texture texture = it.get();
return CompletableFuture.supplyAsync(() -> {
try {
return loadTexture(texture);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
return uuidFallback;
}
}, POOL);
} else {
return CompletableFuture.completedFuture(uuidFallback);
}
}, uuidFallback);
}
// ====
// ==== Avatar ====
public static BufferedImage toAvatar(BufferedImage skin, int size) {
BufferedImage avatar = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = avatar.createGraphics();
int scale = skin.getWidth() / 64;
int faceOffset = (int) Math.round(size / 18.0);
g.drawImage(skin,
faceOffset, faceOffset, size - faceOffset, size - faceOffset,
8 * scale, 8 * scale, 16 * scale, 16 * scale,
null);
g.drawImage(skin,
0, 0, size, size,
40 * scale, 8 * scale, 48 * scale, 16 * scale, null);
g.dispose();
return avatar;
}
public static ObjectBinding<Image> fxAvatarBinding(YggdrasilService service, UUID uuid, int size) {
return BindingMapping.of(skinBinding(service, uuid))
.map(it -> toAvatar(it.image, size))
.map(img -> SwingFXUtils.toFXImage(img, null));
}
public static ObjectBinding<Image> fxAvatarBinding(Account account, int size) {
if (account instanceof YggdrasilAccount) {
return fxAvatarBinding(((YggdrasilAccount) account).getYggdrasilService(), account.getUUID(), size);
} else {
return Bindings.createObjectBinding(
() -> SwingFXUtils.toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size), null));
}
}
// ====
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
@@ -23,20 +23,25 @@ import javafx.beans.property.ReadOnlyListProperty;
import javafx.beans.property.ReadOnlyListWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ObservableList;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccountFactory;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactProvider;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloader;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.authlibinjector.SimpleAuthlibInjectorArtifactProvider;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.MojangYggdrasilProvider;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
import org.jackhuang.hmcl.task.Schedulers;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
@@ -57,43 +62,46 @@ public final class Accounts {
private Accounts() {}
public static final OfflineAccountFactory FACTORY_OFFLINE = OfflineAccountFactory.INSTANCE;
public static final YggdrasilAccountFactory FACTORY_YGGDRASIL = new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE);
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(
new AuthlibInjectorDownloader(Launcher.HMCL_DIRECTORY.toPath(), DownloadProviders::getDownloadProvider)::getArtifactInfo,
Accounts::getOrCreateAuthlibInjectorServer);
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(createAuthlibInjectorArtifactProvider(), Accounts::getOrCreateAuthlibInjectorServer);
private static final String TYPE_OFFLINE = "offline";
private static final String TYPE_YGGDRASIL_ACCOUNT = "yggdrasil";
private static final String TYPE_AUTHLIB_INJECTOR = "authlibInjector";
// ==== login type / account factory mapping ====
private static final Map<String, AccountFactory<?>> type2factory = new HashMap<>();
private static final Map<AccountFactory<?>, String> factory2type = new HashMap<>();
static {
type2factory.put("offline", FACTORY_OFFLINE);
type2factory.put("yggdrasil", FACTORY_MOJANG);
type2factory.put("authlibInjector", FACTORY_AUTHLIB_INJECTOR);
private static Map<String, AccountFactory<?>> type2factory = mapOf(
pair(TYPE_OFFLINE, FACTORY_OFFLINE),
pair(TYPE_YGGDRASIL_ACCOUNT, FACTORY_YGGDRASIL),
pair(TYPE_AUTHLIB_INJECTOR, FACTORY_AUTHLIB_INJECTOR));
type2factory.forEach((type, factory) -> factory2type.put(factory, type));
}
private static String accountType(Account account) {
public static String getLoginType(AccountFactory<?> factory) {
return Optional.ofNullable(factory2type.get(factory))
.orElseThrow(() -> new IllegalArgumentException("Unrecognized account factory"));
}
public static AccountFactory<?> getAccountFactory(String loginType) {
return Optional.ofNullable(type2factory.get(loginType))
.orElseThrow(() -> new IllegalArgumentException("Unrecognized login type"));
}
// ====
public static AccountFactory<?> getAccountFactory(Account account) {
if (account instanceof OfflineAccount)
return TYPE_OFFLINE;
return FACTORY_OFFLINE;
else if (account instanceof AuthlibInjectorAccount)
return TYPE_AUTHLIB_INJECTOR;
return FACTORY_AUTHLIB_INJECTOR;
else if (account instanceof YggdrasilAccount)
return TYPE_YGGDRASIL_ACCOUNT;
return FACTORY_MOJANG;
else
throw new IllegalArgumentException("Failed to determine account type: " + account);
}
public static AccountFactory<?> getAccountFactory(Account account) {
return type2factory.get(accountType(account));
}
static String accountId(Account account) {
return account.getUsername() + ":" + account.getCharacter();
}
private static ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
private static ReadOnlyListWrapper<Account> accountsWrapper = new ReadOnlyListWrapper<>(accounts);
private static ReadOnlyListWrapper<Account> accountsWrapper = new ReadOnlyListWrapper<>(Accounts.class, "accounts", accounts);
private static ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>() {
private static ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>(Accounts.class, "selectedAccount") {
{
accounts.addListener(onInvalidating(this::invalidated));
}
@@ -124,7 +132,7 @@ public final class Accounts {
// selection is valid, store it
if (!initialized)
return;
config().setSelectedAccount(selected == null ? "" : accountId(selected));
updateAccountStorages();
}
};
@@ -137,9 +145,12 @@ public final class Accounts {
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
}
static Map<Object, Object> getAccountStorage(Account account) {
private static Map<Object, Object> getAccountStorage(Account account) {
Map<Object, Object> storage = account.toStorage();
storage.put("type", accountType(account));
storage.put("type", getLoginType(getAccountFactory(account)));
if (account == selectedAccount.get()) {
storage.put("selected", true);
}
return storage;
}
@@ -174,18 +185,38 @@ public final class Accounts {
return;
}
accounts.add(account);
if (Boolean.TRUE.equals(storage.get("selected"))) {
selectedAccount.set(account);
}
});
initialized = true;
config().getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts));
// load selected account
selectedAccount.set(
accounts.stream()
.filter(it -> accountId(it).equals(config().getSelectedAccount()))
.findFirst()
.orElse(null));
Account selected = selectedAccount.get();
if (selected != null) {
Schedulers.io().execute(() -> {
try {
selected.logIn();
} catch (AuthenticationException e) {
LOG.log(Level.WARNING, "Failed to log " + selected + " in", e);
}
});
}
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
continue;
Schedulers.io().execute(() -> {
try {
server.fetchMetadataResponse();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to fetch authlib-injector server metdata: " + server, e);
}
});
}
}
public static ObservableList<Account> getAccounts() {
@@ -209,21 +240,22 @@ 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, DownloadProviders::getDownloadProvider);
} else {
LOG.info("Using specified authlib-injector: " + authlibinjectorLocation);
return new SimpleAuthlibInjectorArtifactProvider(Paths.get(authlibinjectorLocation));
}
}
private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) {
return config().getAuthlibInjectorServers().stream()
.filter(server -> url.equals(server.getUrl()))
.findFirst()
.orElseGet(() -> {
// this usually happens when migrating data from an older version
AuthlibInjectorServer server;
try {
server = AuthlibInjectorServer.fetchServerInfo(url);
LOG.info("Migrated authlib injector server " + server);
} catch (IOException e) {
server = new AuthlibInjectorServer(url, url);
LOG.log(Level.WARNING, "Failed to migrate authlib injector server " + url, e);
}
AuthlibInjectorServer server = new AuthlibInjectorServer(url);
config().getAuthlibInjectorServers().add(server);
return server;
});
@@ -244,18 +276,14 @@ public final class Accounts {
// ====
// ==== Login type name i18n ===
private static Map<AccountFactory<?>, String> loginType2name = mapOf(
pair(Accounts.FACTORY_OFFLINE, i18n("account.methods.offline")),
pair(Accounts.FACTORY_YGGDRASIL, i18n("account.methods.yggdrasil")),
pair(Accounts.FACTORY_AUTHLIB_INJECTOR, i18n("account.methods.authlib_injector")));
private static Map<AccountFactory<?>, String> unlocalizedLoginTypeNames = mapOf(
pair(Accounts.FACTORY_OFFLINE, "account.methods.offline"),
pair(Accounts.FACTORY_MOJANG, "account.methods.yggdrasil"),
pair(Accounts.FACTORY_AUTHLIB_INJECTOR, "account.methods.authlib_injector"));
public static String getAccountTypeName(AccountFactory<?> factory) {
return Optional.ofNullable(loginType2name.get(factory))
.orElseThrow(() -> new IllegalArgumentException("No corresponding login type name"));
}
public static String getAccountTypeName(Account account) {
return getAccountTypeName(getAccountFactory(account));
public static String getLocalizedLoginTypeName(AccountFactory<?> factory) {
return i18n(Optional.ofNullable(unlocalizedLoginTypeNames.get(factory))
.orElseThrow(() -> new IllegalArgumentException("Unrecognized account factory")));
}
// ====
}
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
@@ -32,44 +32,45 @@ import org.hildan.fxgson.creators.ObservableListCreator;
import org.hildan.fxgson.creators.ObservableMapCreator;
import org.hildan.fxgson.creators.ObservableSetCreator;
import org.hildan.fxgson.factories.JavaFxPropertyTypeAdapterFactory;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.upgrade.UpdateChannel;
import org.jackhuang.hmcl.util.EnumOrdinalDeserializer;
import org.jackhuang.hmcl.util.FileTypeAdapter;
import org.jackhuang.hmcl.util.ObservableHelper;
import org.jackhuang.hmcl.util.gson.EnumOrdinalDeserializer;
import org.jackhuang.hmcl.util.gson.FileTypeAdapter;
import org.jackhuang.hmcl.util.i18n.Locales;
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.lang.reflect.Modifier;
import java.net.Proxy;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Stream;
public final class Config implements Cloneable, Observable {
public static final int CURRENT_UI_VERSION = 0;
private static final Gson CONFIG_GSON = new GsonBuilder()
.registerTypeAdapter(VersionSetting.class, VersionSetting.Serializer.INSTANCE)
.registerTypeAdapter(Profile.class, Profile.Serializer.INSTANCE)
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
.registerTypeAdapter(ObservableMap.class, new ObservableMapCreator())
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
.registerTypeAdapter(Theme.class, new Theme.TypeAdapter())
.registerTypeAdapter(SupportedLocale.class, new SupportedLocale.TypeAdapter())
.registerTypeAdapter(EnumBackgroundImage.class, new EnumOrdinalDeserializer<>(EnumBackgroundImage.class)) // backward compatibility for backgroundType
.registerTypeAdapter(Proxy.Type.class, new EnumOrdinalDeserializer<>(Proxy.Type.class)) // backward compatibility for hasProxy
.setPrettyPrinting()
.create();
@Nullable
public static Config fromJson(String json) throws JsonParseException {
Config instance = CONFIG_GSON.fromJson(json, Config.class);
// Gson will replace the property fields (even they are final!)
// So we have to add the listeners again after deserialization
instance.addListenerToProperties();
Config loaded = CONFIG_GSON.fromJson(json, Config.class);
if (loaded == null) {
return null;
}
Config instance = new Config();
PropertyUtils.copyProperties(loaded, instance);
return instance;
}
@@ -86,7 +87,7 @@ public final class Config implements Cloneable, Observable {
private ObjectProperty<EnumCommonDirectory> commonDirType = new SimpleObjectProperty<>(EnumCommonDirectory.DEFAULT);
@SerializedName("commonpath")
private StringProperty commonDirectory = new SimpleStringProperty(Launcher.MINECRAFT_DIRECTORY.getAbsolutePath());
private StringProperty commonDirectory = new SimpleStringProperty(Metadata.MINECRAFT_DIRECTORY.toString());
@SerializedName("hasProxy")
private BooleanProperty hasProxy = new SimpleBooleanProperty();
@@ -95,13 +96,13 @@ public final class Config implements Cloneable, Observable {
private BooleanProperty hasProxyAuth = new SimpleBooleanProperty();
@SerializedName("proxyType")
private ObjectProperty<Proxy.Type> proxyType = new SimpleObjectProperty<>(Proxy.Type.DIRECT);
private ObjectProperty<Proxy.Type> proxyType = new SimpleObjectProperty<>(Proxy.Type.HTTP);
@SerializedName("proxyHost")
private StringProperty proxyHost = new SimpleStringProperty();
@SerializedName("proxyPort")
private StringProperty proxyPort = new SimpleStringProperty();
private IntegerProperty proxyPort = new SimpleIntegerProperty();
@SerializedName("proxyUserName")
private StringProperty proxyUser = new SimpleStringProperty();
@@ -124,9 +125,6 @@ public final class Config implements Cloneable, Observable {
@SerializedName("accounts")
private ObservableList<Map<Object, Object>> accountStorages = FXCollections.observableArrayList();
@SerializedName("selectedAccount")
private StringProperty selectedAccount = new SimpleStringProperty("");
@SerializedName("fontFamily")
private StringProperty fontFamily = new SimpleStringProperty("Consolas");
@@ -137,38 +135,34 @@ public final class Config implements Cloneable, Observable {
private IntegerProperty logLines = new SimpleIntegerProperty(100);
@SerializedName("authlibInjectorServers")
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList();
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[] { server });
@SerializedName("updateChannel")
private ObjectProperty<UpdateChannel> updateChannel = new SimpleObjectProperty<>(UpdateChannel.STABLE);
@SerializedName("enableMainPageGameList")
private BooleanProperty enableMainPageGameList = new SimpleBooleanProperty(false);
@SerializedName("_version")
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
/**
* The version of UI that the user have last used.
* If there is a major change in UI, {@link Config#CURRENT_UI_VERSION} should be increased.
* When {@link #CURRENT_UI_VERSION} is higher than the property, the user guide should be shown,
* then this property is set to the same value as {@link #CURRENT_UI_VERSION}.
* In particular, the property is default to 0, so that whoever open the application for the first time will see the guide.
*/
@SerializedName("uiVersion")
private IntegerProperty uiVersion = new SimpleIntegerProperty(0);
/**
* The preferred login type to use when the user wants to add an account.
*/
@SerializedName("preferredLoginType")
private StringProperty preferredLoginType = new SimpleStringProperty();
private transient ObservableHelper helper = new ObservableHelper(this);
public Config() {
addListenerToProperties();
}
private void addListenerToProperties() {
Stream.of(getClass().getDeclaredFields())
.filter(it -> {
int modifiers = it.getModifiers();
return !Modifier.isTransient(modifiers) && !Modifier.isStatic(modifiers);
})
.filter(it -> Observable.class.isAssignableFrom(it.getType()))
.map(it -> {
try {
return (Observable) it.get(this);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Failed to get my own properties");
}
})
.forEach(helper::receiveUpdatesFrom);
PropertyUtils.attachListener(this, helper);
}
@Override
@@ -299,15 +293,15 @@ public final class Config implements Cloneable, Observable {
return proxyHost;
}
public String getProxyPort() {
public int getProxyPort() {
return proxyPort.get();
}
public void setProxyPort(String proxyPort) {
public void setProxyPort(int proxyPort) {
this.proxyPort.set(proxyPort);
}
public StringProperty proxyPortProperty() {
public IntegerProperty proxyPortProperty() {
return proxyPort;
}
@@ -379,18 +373,6 @@ public final class Config implements Cloneable, Observable {
return accountStorages;
}
public String getSelectedAccount() {
return selectedAccount.get();
}
public void setSelectedAccount(String selectedAccount) {
this.selectedAccount.set(selectedAccount);
}
public StringProperty selectedAccountProperty() {
return selectedAccount;
}
public String getFontFamily() {
return fontFamily.get();
}
@@ -443,15 +425,39 @@ public final class Config implements Cloneable, Observable {
this.updateChannel.set(updateChannel);
}
public boolean isEnableMainPageGameList() {
return enableMainPageGameList.get();
public int getConfigVersion() {
return configVersion.get();
}
public BooleanProperty enableMainPageGameListProperty() {
return enableMainPageGameList;
public IntegerProperty configVersionProperty() {
return configVersion;
}
public void setEnableMainPageGameList(boolean enableMainPageGameList) {
this.enableMainPageGameList.set(enableMainPageGameList);
public void setConfigVersion(int configVersion) {
this.configVersion.set(configVersion);
}
public int getUiVersion() {
return uiVersion.get();
}
public IntegerProperty uiVersionProperty() {
return uiVersion;
}
public void setUiVersion(int uiVersion) {
this.uiVersion.set(uiVersion);
}
public String getPreferredLoginType() {
return preferredLoginType.get();
}
public void setPreferredLoginType(String preferredLoginType) {
this.preferredLoginType.set(preferredLoginType);
}
public StringProperty preferredLoginTypeProperty() {
return preferredLoginType;
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,13 +13,16 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.util.OperatingSystem;
import org.jackhuang.hmcl.util.InvocationDispatcher;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.IOException;
import java.nio.file.Files;
@@ -49,13 +52,6 @@ public final class ConfigHolder {
return configInstance;
}
public static boolean isNewlyCreated() {
if (configInstance == null) {
throw new IllegalStateException("Configuration hasn't been loaded");
}
return newlyCreated;
}
public synchronized static void init() throws IOException {
if (configInstance != null) {
throw new IllegalStateException("Configuration is already loaded");
@@ -68,7 +64,7 @@ public final class ConfigHolder {
Settings.init();
if (newlyCreated) {
saveConfig();
saveConfigSync();
// hide the config file on windows
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
@@ -103,7 +99,7 @@ public final class ConfigHolder {
private static Config loadConfig() throws IOException {
if (Files.exists(configLocation)) {
try {
String content = new String(Files.readAllBytes(configLocation), UTF_8);
String content = FileUtils.readText(configLocation);
Config deserialized = Config.fromJson(content);
if (deserialized == null) {
LOG.info("Config is empty");
@@ -122,22 +118,26 @@ public final class ConfigHolder {
return new Config();
}
private static void saveConfig() throws IOException {
LOG.info("Saving config");
private static InvocationDispatcher<String> configWriter = InvocationDispatcher.runOn(Lang::thread, content -> {
try {
Files.write(configLocation, configInstance.toJson().getBytes(UTF_8));
writeToConfig(content);
} catch (IOException e) {
LOG.log(Level.SEVERE, "Failed to save config", e);
throw e;
}
});
private static void writeToConfig(String content) throws IOException {
LOG.info("Saving config");
synchronized (configLocation) {
Files.write(configLocation, content.getBytes(UTF_8));
}
}
static void markConfigDirty() {
// TODO: change this to async
try {
saveConfig();
} catch (IOException ignored) {
// ignore it as it has been logged
}
configWriter.accept(configInstance.toJson());
}
private static void saveConfigSync() throws IOException {
writeToConfig(configInstance.toJson());
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,22 +13,19 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
import java.util.LinkedList;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import static org.jackhuang.hmcl.util.Lang.tryCast;
final class ConfigUpgrader {
private static final int VERSION = 0;
private ConfigUpgrader() {
}
@@ -37,10 +34,22 @@ final class ConfigUpgrader {
*
* @param deserialized deserialized config settings
* @param rawJson raw json structure of the config settings without modification
* @return true if config version is upgraded
*/
static void upgradeConfig(Config deserialized, Map<?, ?> rawJson) {
static boolean upgradeConfig(Config deserialized, Map<?, ?> rawJson) {
boolean upgraded;
if (deserialized.getConfigVersion() < VERSION) {
deserialized.setConfigVersion(VERSION);
// TODO: Add upgrade code here.
upgraded = true;
} else {
upgraded = false;
}
upgradeV2(deserialized, rawJson);
upgradeV3(deserialized, rawJson);
return upgraded;
}
/**
@@ -53,20 +62,21 @@ final class ConfigUpgrader {
// Convert OfflineAccounts whose stored uuid is important.
tryCast(rawJson.get("auth"), Map.class).ifPresent(auth -> {
tryCast(auth.get("offline"), Map.class).ifPresent(offline -> {
List<OfflineAccount> accounts = new LinkedList<>();
String selected = rawJson.containsKey("selectedAccount") ? null
: tryCast(offline.get("IAuthenticator_UserName"), String.class).orElse(null);
tryCast(offline.get("uuidMap"), Map.class).ifPresent(uuidMap -> {
((Map<?, ?>) uuidMap).forEach((key, value) -> {
OfflineAccount account = OfflineAccountFactory.INSTANCE.create((String) key, UUIDTypeAdapter.fromString((String) value));
accounts.add(account);
deserialized.getAccountStorages().add(Accounts.getAccountStorage(account));
Map<Object, Object> storage = new HashMap<>();
storage.put("type", "offline");
storage.put("username", key);
storage.put("uuid", value);
if (key.equals(selected)) {
storage.put("selected", true);
}
deserialized.getAccountStorages().add(storage);
});
});
tryCast(offline.get("IAuthenticator_UserName"), String.class).ifPresent(selected -> {
if (!rawJson.containsKey("selectedAccount"))
accounts.stream().filter(account -> selected.equals(account.getUsername())).findAny().ifPresent(account ->
deserialized.setSelectedAccount(Accounts.accountId(account)));
});
});
});
}
@@ -98,5 +108,22 @@ final class ConfigUpgrader {
}
});
}
tryCast(rawJson.get("selectedAccount"), String.class)
.ifPresent(selected -> {
deserialized.getAccountStorages().stream()
.filter(storage -> {
Object type = storage.get("type");
if ("offline".equals(type)) {
return selected.equals(storage.get("username") + ":" + storage.get("username"));
} else if ("yggdrasil".equals(type) || "authlibInjector".equals(type)) {
return selected.equals(storage.get("username") + ":" + storage.get("displayName"));
} else {
return false;
}
})
.findFirst()
.ifPresent(storage -> storage.put("selected", true));
});
}
}
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,24 +13,27 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import com.jfoenix.utils.JFXUtilities;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.*;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.EventPriority;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import java.io.File;
import java.lang.reflect.Type;
@@ -42,10 +45,10 @@ import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
*
* @author huangyuhui
*/
@JsonAdapter(Profile.Serializer.class)
public final class Profile implements Observable {
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
private final HMCLGameRepository repository;
private final ModManager modManager;
private final StringProperty selectedVersion = new SimpleStringProperty();
@@ -85,9 +88,9 @@ public final class Profile implements Observable {
return global.get();
}
private final ImmediateStringProperty name;
private final SimpleStringProperty name;
public ImmediateStringProperty nameProperty() {
public StringProperty nameProperty() {
return name;
}
@@ -126,41 +129,38 @@ public final class Profile implements Observable {
}
public Profile(String name, File initialGameDir, VersionSetting global, String selectedVersion, boolean useRelativePath) {
this.name = new ImmediateStringProperty(this, "name", name);
gameDir = new ImmediateObjectProperty<>(this, "gameDir", initialGameDir);
this.name = new SimpleStringProperty(this, "name", name);
gameDir = new SimpleObjectProperty<>(this, "gameDir", initialGameDir);
repository = new HMCLGameRepository(this, initialGameDir);
modManager = new ModManager(repository);
this.global.set(global == null ? new VersionSetting() : global);
this.selectedVersion.set(selectedVersion);
this.useRelativePath.set(useRelativePath);
gameDir.addListener((a, b, newValue) -> repository.changeDirectory(newValue));
this.selectedVersion.addListener(o -> checkSelectedVersion());
listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> checkSelectedVersion()));
listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> checkSelectedVersion(), EventPriority.HIGHEST));
addPropertyChangedListener(onInvalidating(this::invalidate));
}
private void checkSelectedVersion() {
if (!repository.isLoaded()) return;
String newValue = selectedVersion.get();
if (!repository.hasVersion(newValue)) {
Optional<String> version = repository.getVersions().stream().findFirst().map(Version::getId);
if (version.isPresent())
selectedVersion.setValue(version.get());
else if (newValue != null)
selectedVersion.setValue(null);
}
JFXUtilities.runInFX(() -> {
if (!repository.isLoaded()) return;
String newValue = selectedVersion.get();
if (!repository.hasVersion(newValue)) {
Optional<String> version = repository.getVersions().stream().findFirst().map(Version::getId);
if (version.isPresent())
selectedVersion.setValue(version.get());
else if (newValue != null)
selectedVersion.setValue(null);
}
});
}
public HMCLGameRepository getRepository() {
return repository;
}
public ModManager getModManager() {
return modManager;
}
public DefaultDependencyManager getDependency() {
return new DefaultDependencyManager(repository, DownloadProviders.getDownloadProvider(), HMCLCacheRepository.REPOSITORY);
}
@@ -210,11 +210,6 @@ public final class Profile implements Observable {
}
public static final class Serializer implements JsonSerializer<Profile>, JsonDeserializer<Profile> {
public static final Serializer INSTANCE = new Serializer();
private Serializer() {
}
@Override
public JsonElement serialize(Profile src, Type typeOfSrc, JsonSerializationContext context) {
if (src == null)
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,24 +13,26 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
import com.jfoenix.utils.JFXUtilities;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyListProperty;
import javafx.beans.property.ReadOnlyListWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.*;
import javafx.collections.ObservableList;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import java.io.File;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
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.ui.FXUtils.onInvalidating;
@@ -65,7 +67,11 @@ public final class Profiles {
@Override
protected void invalidated() {
if (!initialized)
return;
Profile profile = get();
if (profiles.isEmpty()) {
if (profile != null) {
set(null);
@@ -78,16 +84,27 @@ public final class Profiles {
}
}
if (!initialized)
return;
config().setSelectedProfile(profile == null ? "" : profile.getName());
if (profile != null) {
if (profile.getRepository().isLoaded())
selectedVersion.bind(profile.selectedVersionProperty());
else {
selectedVersion.unbind();
selectedVersion.set(null);
// bind when repository was reloaded.
profile.getRepository().refreshVersionsAsync().start();
}
} else {
selectedVersion.unbind();
selectedVersion.set(null);
}
}
};
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, Launcher.MINECRAFT_DIRECTORY);
Profile home = new Profile(Profiles.HOME_PROFILE, Metadata.MINECRAFT_DIRECTORY.toFile());
Platform.runLater(() -> profiles.addAll(current, home));
}
}
@@ -127,18 +144,33 @@ public final class Profiles {
HashSet<String> names = new HashSet<>();
config().getConfigurations().forEach((name, profile) -> {
if (!names.add(name)) return;
profile.setName(name);
profiles.add(profile);
profile.setName(name);
});
checkProfiles();
initialized = true;
// Platform.runLater is necessary or profiles will be empty
// since checkProfiles adds 2 base profile later.
Platform.runLater(() -> {
initialized = true;
selectedProfile.set(
profiles.stream()
.filter(it -> it.getName().equals(config().getSelectedProfile()))
.findFirst()
.orElse(profiles.get(0)));
selectedProfile.set(
profiles.stream()
.filter(it -> it.getName().equals(config().getSelectedProfile()))
.findFirst()
.orElse(profiles.get(0)));
});
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> {
JFXUtilities.runInFX(() -> {
Profile profile = selectedProfile.get();
if (profile != null && profile.getRepository() == event.getSource()) {
selectedVersion.bind(profile.selectedVersionProperty());
for (Consumer<Profile> listener : versionsListeners)
listener.accept(profile);
}
});
});
}
public static ObservableList<Profile> getProfiles() {
@@ -160,4 +192,24 @@ public final class Profiles {
public static ObjectProperty<Profile> selectedProfileProperty() {
return selectedProfile;
}
private static final ReadOnlyStringWrapper selectedVersion = new ReadOnlyStringWrapper();
public static ReadOnlyStringProperty selectedVersionProperty() {
return selectedVersion.getReadOnlyProperty();
}
// Guaranteed that the repository is loaded.
public static String getSelectedVersion() {
return selectedVersion.get();
}
private static final List<Consumer<Profile>> versionsListeners = new LinkedList<>();
public static void registerVersionsListener(Consumer<Profile> listener) {
Profile profile = getSelectedProfile();
if (profile != null && profile.getRepository().isLoaded())
listener.accept(profile);
versionsListeners.add(listener);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,14 +13,13 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.value.ObservableObjectValue;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import java.net.Authenticator;
@@ -30,6 +29,7 @@ import java.net.Proxy;
import java.net.Proxy.Type;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Logging.LOG;
public final class ProxyManager {
private ProxyManager() {
@@ -49,10 +49,14 @@ public final class ProxyManager {
proxyProperty = Bindings.createObjectBinding(
() -> {
String host = config().getProxyHost();
Integer port = Lang.toIntOrNull(config().getProxyPort());
if (!config().hasProxy() || StringUtils.isBlank(host) || port == null || config().getProxyType() == Proxy.Type.DIRECT) {
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));
}
},
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,14 +13,15 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.Launcher;
import javafx.beans.binding.Bindings;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.io.FileUtils;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
@@ -49,20 +50,17 @@ public class Settings {
Profiles.init();
CacheRepository.setInstance(HMCLCacheRepository.REPOSITORY);
HMCLCacheRepository.REPOSITORY.directoryProperty().bind(config().commonDirectoryProperty());
}
public Font getFont() {
return Font.font(config().getFontFamily(), config().getFontSize());
}
public void setFont(Font font) {
config().setFontFamily(font.getFamily());
config().setFontSize(font.getSize());
HMCLCacheRepository.REPOSITORY.directoryProperty().bind(Bindings.createStringBinding(() -> {
if (FileUtils.canCreateDirectory(getCommonDirectory())) {
return getCommonDirectory();
} else {
return getDefaultCommonDirectory();
}
}, config().commonDirectoryProperty(), config().commonDirTypeProperty()));
}
public static String getDefaultCommonDirectory() {
return Launcher.MINECRAFT_DIRECTORY.getAbsolutePath();
return Metadata.MINECRAFT_DIRECTORY.toString();
}
public String getCommonDirectory() {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,18 +13,21 @@
* 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 {http://www.gnu.org/licenses/}.
* 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 org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.IOUtils;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.io.File;
import java.io.IOException;
@@ -33,6 +36,7 @@ import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
@JsonAdapter(Theme.TypeAdapter.class)
public class Theme {
public static final Theme BLUE = new Theme("blue", "#5C6BC0");
@@ -42,7 +46,7 @@ public class Theme {
Color.web("#43A047"), // green
Color.web("#E67E22"), // orange
Color.web("#9C27B0"), // purple
Color.web("#F44336") // red
Color.web("#B71C1C") // red
};
private final String color;
@@ -81,14 +85,14 @@ public class Theme {
.replace("%base-color%", color)
.replace("%font-color%", getColorDisplayName(getForegroundColor())));
css = temp.toURI().toString();
} catch (IOException e) {
} catch (IOException | NullPointerException e) {
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet. Fallback to blue theme.", e);
css = Theme.class.getResource("/assets/css/blue.css").toExternalForm();
css = "/assets/css/blue.css";
}
return new String[]{
css,
Theme.class.getResource("/assets/css/root.css").toExternalForm()
"/assets/css/root.css"
};
}
@@ -129,7 +133,8 @@ public class Theme {
}
public static ObjectBinding<Color> foregroundFillBinding() {
return Bindings.createObjectBinding(() -> config().getTheme().getForegroundColor(), config().themeProperty());
return BindingMapping.of(config().themeProperty())
.map(Theme::getForegroundColor);
}
public static ObjectBinding<Color> blackFillBinding() {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,19 +13,33 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -36,6 +50,7 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config;
*
* @author huangyuhui
*/
@JsonAdapter(VersionSetting.Serializer.class)
public final class VersionSetting {
public transient String id;
@@ -50,9 +65,9 @@ public final class VersionSetting {
this.global = global;
}
private final ImmediateBooleanProperty usesGlobalProperty = new ImmediateBooleanProperty(this, "usesGlobal", false);
private final BooleanProperty usesGlobalProperty = new SimpleBooleanProperty(this, "usesGlobal", false);
public ImmediateBooleanProperty usesGlobalProperty() {
public BooleanProperty usesGlobalProperty() {
return usesGlobalProperty;
}
@@ -74,9 +89,9 @@ public final class VersionSetting {
// java
private final ImmediateStringProperty javaProperty = new ImmediateStringProperty(this, "java", "");
private final StringProperty javaProperty = new SimpleStringProperty(this, "java", "");
public ImmediateStringProperty javaProperty() {
public StringProperty javaProperty() {
return javaProperty;
}
@@ -100,7 +115,7 @@ public final class VersionSetting {
setDefaultJavaPath(null);
}
private final ImmediateStringProperty defaultJavaPathProperty = new ImmediateStringProperty(this, "defaultJavaPath", "");
private final StringProperty defaultJavaPathProperty = new SimpleStringProperty(this, "defaultJavaPath", "");
/**
* Path to Java executable, or null if user customizes java directory.
@@ -114,9 +129,9 @@ public final class VersionSetting {
defaultJavaPathProperty.set(defaultJavaPath);
}
private final ImmediateStringProperty javaDirProperty = new ImmediateStringProperty(this, "javaDir", "");
private final StringProperty javaDirProperty = new SimpleStringProperty(this, "javaDir", "");
public ImmediateStringProperty javaDirProperty() {
public StringProperty javaDirProperty() {
return javaDirProperty;
}
@@ -131,9 +146,9 @@ public final class VersionSetting {
javaDirProperty.set(javaDir);
}
private final ImmediateStringProperty wrapperProperty = new ImmediateStringProperty(this, "wrapper", "");
private final StringProperty wrapperProperty = new SimpleStringProperty(this, "wrapper", "");
public ImmediateStringProperty wrapperProperty() {
public StringProperty wrapperProperty() {
return wrapperProperty;
}
@@ -148,9 +163,9 @@ public final class VersionSetting {
wrapperProperty.set(wrapper);
}
private final ImmediateStringProperty permSizeProperty = new ImmediateStringProperty(this, "permSize", "");
private final StringProperty permSizeProperty = new SimpleStringProperty(this, "permSize", "");
public ImmediateStringProperty permSizeProperty() {
public StringProperty permSizeProperty() {
return permSizeProperty;
}
@@ -165,9 +180,9 @@ public final class VersionSetting {
permSizeProperty.set(permSize);
}
private final ImmediateIntegerProperty maxMemoryProperty = new ImmediateIntegerProperty(this, "maxMemory", OperatingSystem.SUGGESTED_MEMORY);
private final IntegerProperty maxMemoryProperty = new SimpleIntegerProperty(this, "maxMemory", OperatingSystem.SUGGESTED_MEMORY);
public ImmediateIntegerProperty maxMemoryProperty() {
public IntegerProperty maxMemoryProperty() {
return maxMemoryProperty;
}
@@ -185,9 +200,9 @@ public final class VersionSetting {
/**
* The minimum memory that JVM can allocate for heap.
*/
private final ImmediateObjectProperty<Integer> minMemoryProperty = new ImmediateObjectProperty<>(this, "minMemory", null);
private final ObjectProperty<Integer> minMemoryProperty = new SimpleObjectProperty<>(this, "minMemory", null);
public ImmediateObjectProperty<Integer> minMemoryProperty() {
public ObjectProperty<Integer> minMemoryProperty() {
return minMemoryProperty;
}
@@ -199,9 +214,9 @@ public final class VersionSetting {
minMemoryProperty.set(minMemory);
}
private final ImmediateStringProperty preLaunchCommandProperty = new ImmediateStringProperty(this, "precalledCommand", "");
private final StringProperty preLaunchCommandProperty = new SimpleStringProperty(this, "precalledCommand", "");
public ImmediateStringProperty preLaunchCommandProperty() {
public StringProperty preLaunchCommandProperty() {
return preLaunchCommandProperty;
}
@@ -219,9 +234,9 @@ public final class VersionSetting {
// options
private final ImmediateStringProperty javaArgsProperty = new ImmediateStringProperty(this, "javaArgs", "");
private final StringProperty javaArgsProperty = new SimpleStringProperty(this, "javaArgs", "");
public ImmediateStringProperty javaArgsProperty() {
public StringProperty javaArgsProperty() {
return javaArgsProperty;
}
@@ -236,9 +251,9 @@ public final class VersionSetting {
javaArgsProperty.set(javaArgs);
}
private final ImmediateStringProperty minecraftArgsProperty = new ImmediateStringProperty(this, "minecraftArgs", "");
private final StringProperty minecraftArgsProperty = new SimpleStringProperty(this, "minecraftArgs", "");
public ImmediateStringProperty minecraftArgsProperty() {
public StringProperty minecraftArgsProperty() {
return minecraftArgsProperty;
}
@@ -253,9 +268,9 @@ public final class VersionSetting {
minecraftArgsProperty.set(minecraftArgs);
}
private final ImmediateBooleanProperty noJVMArgsProperty = new ImmediateBooleanProperty(this, "noJVMArgs", false);
private final BooleanProperty noJVMArgsProperty = new SimpleBooleanProperty(this, "noJVMArgs", false);
public ImmediateBooleanProperty noJVMArgsProperty() {
public BooleanProperty noJVMArgsProperty() {
return noJVMArgsProperty;
}
@@ -270,9 +285,26 @@ public final class VersionSetting {
noJVMArgsProperty.set(noJVMArgs);
}
private final ImmediateBooleanProperty notCheckGameProperty = new ImmediateBooleanProperty(this, "notCheckGame", false);
private final BooleanProperty notCheckJVMProperty = new SimpleBooleanProperty(this, "notCheckJVM", false);
public ImmediateBooleanProperty notCheckGameProperty() {
public BooleanProperty notCheckJVMProperty() {
return notCheckJVMProperty;
}
/**
* True if HMCL does not check JVM validity.
*/
public boolean isNotCheckJVM() {
return notCheckJVMProperty.get();
}
public void setNotCheckJVM(boolean notCheckJVM) {
notCheckJVMProperty.set(notCheckJVM);
}
private final BooleanProperty notCheckGameProperty = new SimpleBooleanProperty(this, "notCheckGame", false);
public BooleanProperty notCheckGameProperty() {
return notCheckGameProperty;
}
@@ -287,9 +319,9 @@ public final class VersionSetting {
notCheckGameProperty.set(notCheckGame);
}
private final ImmediateBooleanProperty showLogsProperty = new ImmediateBooleanProperty(this, "showLogs", false);
private final BooleanProperty showLogsProperty = new SimpleBooleanProperty(this, "showLogs", false);
public ImmediateBooleanProperty showLogsProperty() {
public BooleanProperty showLogsProperty() {
return showLogsProperty;
}
@@ -306,14 +338,14 @@ public final class VersionSetting {
// Minecraft settings.
private final ImmediateStringProperty serverIpProperty = new ImmediateStringProperty(this, "serverIp", "");
private final StringProperty serverIpProperty = new SimpleStringProperty(this, "serverIp", "");
public ImmediateStringProperty serverIpProperty() {
public StringProperty serverIpProperty() {
return serverIpProperty;
}
/**
* The server ip that will be entered after Minecraft successfully loaded immediately.
* The server ip that will be entered after Minecraft successfully loaded ly.
*
* Format: ip:port or without port.
*/
@@ -326,9 +358,9 @@ public final class VersionSetting {
}
private final ImmediateBooleanProperty fullscreenProperty = new ImmediateBooleanProperty(this, "fullscreen", false);
private final BooleanProperty fullscreenProperty = new SimpleBooleanProperty(this, "fullscreen", false);
public ImmediateBooleanProperty fullscreenProperty() {
public BooleanProperty fullscreenProperty() {
return fullscreenProperty;
}
@@ -343,9 +375,9 @@ public final class VersionSetting {
fullscreenProperty.set(fullscreen);
}
private final ImmediateIntegerProperty widthProperty = new ImmediateIntegerProperty(this, "width", 854);
private final IntegerProperty widthProperty = new SimpleIntegerProperty(this, "width", 854);
public ImmediateIntegerProperty widthProperty() {
public IntegerProperty widthProperty() {
return widthProperty;
}
@@ -365,9 +397,9 @@ public final class VersionSetting {
}
private final ImmediateIntegerProperty heightProperty = new ImmediateIntegerProperty(this, "height", 480);
private final IntegerProperty heightProperty = new SimpleIntegerProperty(this, "height", 480);
public ImmediateIntegerProperty heightProperty() {
public IntegerProperty heightProperty() {
return heightProperty;
}
@@ -390,9 +422,9 @@ public final class VersionSetting {
* 0 - .minecraft<br/>
* 1 - .minecraft/versions/&lt;version&gt;/<br/>
*/
private final ImmediateObjectProperty<EnumGameDirectory> gameDirTypeProperty = new ImmediateObjectProperty<>(this, "gameDirType", EnumGameDirectory.ROOT_FOLDER);
private final ObjectProperty<EnumGameDirectory> gameDirTypeProperty = new SimpleObjectProperty<>(this, "gameDirType", EnumGameDirectory.ROOT_FOLDER);
public ImmediateObjectProperty<EnumGameDirectory> gameDirTypeProperty() {
public ObjectProperty<EnumGameDirectory> gameDirTypeProperty() {
return gameDirTypeProperty;
}
@@ -407,9 +439,9 @@ public final class VersionSetting {
/**
* Your custom gameDir
*/
private final ImmediateStringProperty gameDirProperty = new ImmediateStringProperty(this, "gameDir", "");
private final StringProperty gameDirProperty = new SimpleStringProperty(this, "gameDir", "");
public ImmediateStringProperty gameDirProperty() {
public StringProperty gameDirProperty() {
return gameDirProperty;
}
@@ -428,9 +460,9 @@ public final class VersionSetting {
* 1 - Hide the launcher when the game starts.<br/>
* 2 - Keep the launcher open.<br/>
*/
private final ImmediateObjectProperty<LauncherVisibility> launcherVisibilityProperty = new ImmediateObjectProperty<>(this, "launcherVisibility", LauncherVisibility.HIDE);
private final ObjectProperty<LauncherVisibility> launcherVisibilityProperty = new SimpleObjectProperty<>(this, "launcherVisibility", LauncherVisibility.HIDE);
public ImmediateObjectProperty<LauncherVisibility> launcherVisibilityProperty() {
public ObjectProperty<LauncherVisibility> launcherVisibilityProperty() {
return launcherVisibilityProperty;
}
@@ -449,12 +481,12 @@ public final class VersionSetting {
if ("Default".equals(getJava())) return JavaVersion.fromCurrentEnvironment();
else if (isUsesCustomJavaDir()) {
try {
return JavaVersion.fromExecutable(new File(getJavaDir()));
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
} catch (IOException e) {
return null; // Custom Java Directory not found,
}
} else if (StringUtils.isNotBlank(getJava())) {
List<JavaVersion> matchedJava = JavaVersion.getJREs().stream()
List<JavaVersion> matchedJava = JavaVersion.getJavas().stream()
.filter(java -> java.getVersion().equals(getJava()))
.collect(Collectors.toList());
if (matchedJava.isEmpty()) {
@@ -487,6 +519,7 @@ public final class VersionSetting {
minecraftArgsProperty.addListener(listener);
noJVMArgsProperty.addListener(listener);
notCheckGameProperty.addListener(listener);
notCheckJVMProperty.addListener(listener);
showLogsProperty.addListener(listener);
serverIpProperty.addListener(listener);
fullscreenProperty.addListener(listener);
@@ -500,7 +533,7 @@ public final class VersionSetting {
public LaunchOptions toLaunchOptions(File gameDir) throws InterruptedException {
JavaVersion javaVersion = Optional.ofNullable(getJavaVersion()).orElse(JavaVersion.fromCurrentEnvironment());
return new LaunchOptions.Builder()
LaunchOptions.Builder builder = new LaunchOptions.Builder()
.setGameDir(gameDir)
.setJava(javaVersion)
.setVersionName(Metadata.TITLE)
@@ -515,21 +548,20 @@ public final class VersionSetting {
.setFullscreen(isFullscreen())
.setServerIp(getServerIp())
.setWrapper(getWrapper())
.setProxyHost(config().getProxyHost())
.setProxyPort(config().getProxyPort())
.setProxyUser(config().getProxyUser())
.setProxyPass(config().getProxyPass())
.setPrecalledCommand(getPreLaunchCommand())
.setNoGeneratedJVMArgs(isNoJVMArgs())
.create();
.setNoGeneratedJVMArgs(isNoJVMArgs());
if (config().hasProxy()) {
builder.setProxyHost(config().getProxyHost());
builder.setProxyPort(config().getProxyPort());
if (config().hasProxyAuth()) {
builder.setProxyUser(config().getProxyUser());
builder.setProxyPass(config().getProxyPass());
}
}
return builder.create();
}
public static class Serializer implements JsonSerializer<VersionSetting>, JsonDeserializer<VersionSetting> {
public static final Serializer INSTANCE = new Serializer();
private Serializer() {
}
@Override
public JsonElement serialize(VersionSetting src, Type typeOfSrc, JsonSerializationContext context) {
if (src == null) return JsonNull.INSTANCE;
@@ -551,6 +583,7 @@ public final class VersionSetting {
obj.addProperty("fullscreen", src.isFullscreen());
obj.addProperty("noJVMArgs", src.isNoJVMArgs());
obj.addProperty("notCheckGame", src.isNotCheckGame());
obj.addProperty("notCheckJVM", src.isNotCheckJVM());
obj.addProperty("showLogs", src.isShowLogs());
obj.addProperty("gameDir", src.getGameDir());
obj.addProperty("launcherVisibility", src.getLauncherVisibility().ordinal());
@@ -588,6 +621,7 @@ public final class VersionSetting {
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.setNotCheckGame(Optional.ofNullable(obj.get("notCheckGame")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotCheckJVM(Optional.ofNullable(obj.get("notCheckJVM")).map(JsonElement::getAsBoolean).orElse(false));
vs.setShowLogs(Optional.ofNullable(obj.get("showLogs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setLauncherVisibility(LauncherVisibility.values()[Optional.ofNullable(obj.get("launcherVisibility")).map(JsonElement::getAsInt).orElse(1)]);
vs.setGameDirType(EnumGameDirectory.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(0)]);
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,10 +13,11 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.utils.JFXUtilities;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.image.Image;
@@ -24,26 +25,40 @@ import javafx.scene.layout.Region;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.account.AccountList;
import org.jackhuang.hmcl.ui.account.AuthlibInjectorServersPage;
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.decorator.DecoratorController;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.profile.ProfileList;
import org.jackhuang.hmcl.ui.versions.GameItem;
import org.jackhuang.hmcl.ui.versions.GameList;
import org.jackhuang.hmcl.ui.versions.VersionPage;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.JavaVersion;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.File;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Controllers {
@@ -76,17 +91,23 @@ public final class Controllers {
// FXThread
public static GameList getGameListPage() {
if (gameListPage == null)
if (gameListPage == null) {
gameListPage = new GameList();
FXUtils.applyDragListener(gameListPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
File modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
});
}
return gameListPage;
}
// FXThread
public static AccountList getAccountListPage() {
if (accountListPage == null) {
accountListPage = new AccountList();
AccountList accountListPage = new AccountList();
accountListPage.selectedAccountProperty().bindBidirectional(Accounts.selectedAccountProperty());
accountListPage.accountsProperty().bindContent(Accounts.accountsProperty());
Controllers.accountListPage = accountListPage;
}
return accountListPage;
}
@@ -94,9 +115,10 @@ public final class Controllers {
// FXThread
public static ProfileList getProfileListPage() {
if (profileListPage == null) {
profileListPage = new ProfileList();
ProfileList profileListPage = new ProfileList();
profileListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
profileListPage.profilesProperty().bindContent(Profiles.profilesProperty());
Controllers.profileListPage = profileListPage;
}
return profileListPage;
}
@@ -121,8 +143,44 @@ public final class Controllers {
}
public static MainPage getMainPage() {
if (mainPage == null)
mainPage = new MainPage();
if (mainPage == null) {
MainPage mainPage = new MainPage();
FXUtils.applyDragListener(mainPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
File modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
});
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> {
if (version != null) {
mainPage.setCurrentGame(version);
} else {
mainPage.setCurrentGame(i18n("version.empty"));
}
});
mainPage.showUpdateProperty().bind(UpdateChecker.outdatedProperty());
mainPage.latestVersionProperty().bind(
BindingMapping.of(UpdateChecker.latestVersionProperty())
.map(version -> version == null ? "" : i18n("update.bubble.title", version.getVersion())));
Profiles.registerVersionsListener(profile -> {
HMCLGameRepository repository = profile.getRepository();
List<Node> children = repository.getVersions().parallelStream()
.filter(version -> !version.isHidden())
.sorted(Comparator.comparing((Version version) -> version.getReleaseTime() == null ? new Date(0L) : version.getReleaseTime())
.thenComparing(a -> VersionNumber.asVersion(a.getId())))
.map(version -> {
Node node = PopupMenu.wrapPopupMenuItem(new GameItem(profile, version.getId()));
node.setOnMouseClicked(e -> profile.setSelectedVersion(version.getId()));
return node;
})
.collect(Collectors.toList());
JFXUtilities.runInFX(() -> {
if (profile == Profiles.getSelectedProfile())
mainPage.getVersions().setAll(children);
});
});
Controllers.mainPage = mainPage;
}
return mainPage;
}
@@ -131,6 +189,8 @@ public final class Controllers {
}
public static void initialize(Stage stage) {
Logging.LOG.info("Start initializing application");
Controllers.stage = stage;
stage.setOnCloseRequest(e -> Launcher.stopApplication());
@@ -139,7 +199,13 @@ public final class Controllers {
leftPaneController = new LeftPaneController();
decorator.getDecorator().drawerProperty().setAll(leftPaneController);
Task.of(JavaVersion::initialize).start();
if (config().getCommonDirType() == EnumCommonDirectory.CUSTOM &&
!FileUtils.canCreateDirectory(config().getCommonDirectory())) {
config().setCommonDirType(EnumCommonDirectory.DEFAULT);
dialog(i18n("launcher.cache_directory.invalid"));
}
Task.runAsync(JavaVersion::initialize).start();
scene = new Scene(decorator.getDecorator(), 800, 519);
scene.getStylesheets().setAll(config().getTheme().getStylesheets());
@@ -158,14 +224,14 @@ public final class Controllers {
}
public static void dialog(String text, String title) {
dialog(text, title, MessageBox.INFORMATION_MESSAGE);
dialog(text, title, MessageType.INFORMATION);
}
public static void dialog(String text, String title, int type) {
public static void dialog(String text, String title, MessageType type) {
dialog(text, title, type, null);
}
public static void dialog(String text, String title, int type, Runnable onAccept) {
public static void dialog(String text, String title, MessageType type, Runnable onAccept) {
dialog(new MessageDialogPane(text, title, type, onAccept));
}
@@ -179,6 +245,10 @@ public final class Controllers {
return pane;
}
public static Region taskDialog(TaskExecutor executor, String title) {
return taskDialog(executor, title, "");
}
public static Region taskDialog(TaskExecutor executor, String title, String subtitle) {
return taskDialog(executor, title, subtitle, null);
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,24 +13,25 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.utils.JFXUtilities;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.task.SilentException;
import org.jackhuang.hmcl.ui.account.AccountLoginPane;
import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
public final class DialogController {
public static AuthInfo logIn(Account account) throws Exception {
public static AuthInfo logIn(Account account) throws CancellationException, AuthenticationException, InterruptedException {
if (account instanceof YggdrasilAccount) {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
@@ -42,7 +43,7 @@ public final class DialogController {
Controllers.dialog(pane);
});
latch.await();
return Optional.ofNullable(res.get()).orElseThrow(SilentException::new);
return Optional.ofNullable(res.get()).orElseThrow(CancellationException::new);
}
return account.logIn();
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,11 +13,11 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.utils.JFXUtilities;
import com.jfoenix.controls.*;
import javafx.animation.Animation;
import javafx.animation.Interpolator;
@@ -31,30 +31,42 @@ import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Lang.tryCast;
@@ -83,13 +95,18 @@ public final class FXUtils {
}
public static <T> void onChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
onChange(value, consumer);
consumer.accept(value.getValue());
onChange(value, consumer);
}
public static <T> void onWeakChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
onWeakChange(value, consumer);
consumer.accept(value.getValue());
onWeakChange(value, consumer);
}
public static void runLaterIf(BooleanSupplier condition, Runnable runnable) {
if (condition.getAsBoolean()) Platform.runLater(() -> runLaterIf(condition, runnable));
else runnable.run();
}
public static void limitSize(ImageView imageView, double maxWidth, double maxHeight) {
@@ -195,6 +212,14 @@ public final class FXUtils {
return region.getMaxHeight();
}
public static Node limitingSize(Node node, double width, double height) {
StackPane pane = new StackPane(node);
pane.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(pane, width);
FXUtils.setLimitHeight(pane, height);
return pane;
}
public static void smoothScrolling(ScrollPane scrollPane) {
JFXScrollPane.smoothScrolling(scrollPane);
}
@@ -203,11 +228,27 @@ public final class FXUtils {
FXMLLoader loader = new FXMLLoader(node.getClass().getResource(absolutePath), I18n.getResourceBundle());
loader.setRoot(node);
loader.setController(node);
Lang.invoke((ExceptionalSupplier<Object, IOException>) loader::load);
try {
loader.load();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
public static void installTooltip(Node node, String tooltip) {
installTooltip(node, 0, 5000, 0, new Tooltip(tooltip));
public static void installFastTooltip(Node node, Tooltip tooltip) {
installTooltip(node, 50, 5000, 0, tooltip);
}
public static void installFastTooltip(Node node, String tooltip) {
installFastTooltip(node, new Tooltip(tooltip));
}
public static void installSlowTooltip(Node node, Tooltip tooltip) {
installTooltip(node, 500, 5000, 0, tooltip);
}
public static void installSlowTooltip(Node node, String tooltip) {
installSlowTooltip(node, new Tooltip(tooltip));
}
public static void installTooltip(Node node, double openDelay, double visibleDelay, double closeDelay, Tooltip tooltip) {
@@ -291,7 +332,7 @@ public final class FXUtils {
}
public static void bindInt(JFXTextField textField, Property<Number> property) {
textField.textProperty().bindBidirectional(property, SafeIntStringConverter.INSTANCE);
textField.textProperty().bindBidirectional(property, SafeStringConverter.fromInteger());
}
public static void unbindInt(JFXTextField textField, Property<Number> property) {
@@ -328,11 +369,11 @@ public final class FXUtils {
* @param comboBox the combo box being bound with {@code property}.
* @param property the property being bound with {@code combo box}.
* @see #unbindEnum(JFXComboBox)
* @deprecated Use {@link SelectionModelSelectedItemProperty#selectedItemPropertyFor(ComboBox)}
* @deprecated Use {@link ExtendedProperties#selectedItemPropertyFor(ComboBox)}
*/
@SuppressWarnings("unchecked")
@Deprecated
public static void bindEnum(JFXComboBox<?> comboBox, Property<? extends Enum> property) {
public static void bindEnum(JFXComboBox<?> comboBox, Property<? extends Enum<?>> property) {
unbindEnum(comboBox);
ChangeListener<Number> listener = (a, b, newValue) ->
((Property) property).setValue(property.getValue().getClass().getEnumConstants()[newValue.intValue()]);
@@ -421,6 +462,34 @@ public final class FXUtils {
}
}
public static void applyDragListener(Node node, FileFilter filter, Consumer<List<File>> callback) {
applyDragListener(node, filter, callback, null);
}
public static void applyDragListener(Node node, FileFilter filter, Consumer<List<File>> callback, Runnable dragDropped) {
node.setOnDragOver(event -> {
if (event.getGestureSource() != node && event.getDragboard().hasFiles()) {
if (event.getDragboard().getFiles().stream().anyMatch(filter::accept))
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
}
event.consume();
});
node.setOnDragDropped(event -> {
List<File> files = event.getDragboard().getFiles();
if (files != null) {
List<File> acceptFiles = files.stream().filter(filter::accept).collect(Collectors.toList());
if (!acceptFiles.isEmpty()) {
callback.accept(acceptFiles);
event.setDropCompleted(true);
}
}
if (dragDropped != null)
dragDropped.run();
event.consume();
});
}
public static <T> StringConverter<T> stringConverter(Function<T, String> func) {
return new StringConverter<T>() {
@@ -442,6 +511,7 @@ public final class FXUtils {
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
setGraphic(graphicBuilder.apply(item));
}
}
@@ -459,4 +529,11 @@ public final class FXUtils {
return "Interpolator.SINE";
}
};
public static Runnable withJFXPopupClosing(Runnable runnable, JFXPopup popup) {
return () -> {
runnable.run();
popup.hide();
};
}
}
@@ -1,241 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.*;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.VersionNumber;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.StringUtils.removePrefix;
import static org.jackhuang.hmcl.util.StringUtils.removeSuffix;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameVersionListPage extends StackPane {
private final JFXSpinner spinner;
private final StackPane contentPane;
private final JFXMasonryPane masonryPane;
private Profile profile;
public GameVersionListPage() {
spinner = new JFXSpinner();
spinner.getStyleClass().setAll("first-spinner");
contentPane = new StackPane();
ScrollPane scrollPane = new ScrollPane();
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setFitToWidth(true);
masonryPane = new JFXMasonryPane();
masonryPane.setHSpacing(3);
masonryPane.setVSpacing(3);
masonryPane.setCellWidth(182);
masonryPane.setCellHeight(153);
scrollPane.setContent(masonryPane);
VBox vBox = new VBox();
vBox.setPadding(new Insets(15));
vBox.setPickOnBounds(false);
vBox.setAlignment(Pos.BOTTOM_RIGHT);
vBox.setSpacing(15);
JFXButton btnRefresh = new JFXButton();
btnRefresh.setPrefWidth(40);
btnRefresh.setPrefHeight(40);
btnRefresh.setButtonType(JFXButton.ButtonType.RAISED);
btnRefresh.getStyleClass().setAll("jfx-button-raised-round");
btnRefresh.setGraphic(SVG.refresh(Theme.foregroundFillBinding(), -1, -1));
btnRefresh.setOnMouseClicked(e -> profile.getRepository().refreshVersionsAsync().start());
FXUtils.installTooltip(btnRefresh, i18n("button.refresh"));
vBox.getChildren().setAll(btnRefresh);
contentPane.getChildren().setAll(scrollPane, vBox);
getChildren().setAll(spinner);
Profiles.selectedProfileProperty().addListener((o, a, b) -> this.profile = b);
profile = Profiles.getSelectedProfile();
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(event -> {
if (event.getSource() == profile.getRepository())
loadVersions((HMCLGameRepository) event.getSource());
});
EventBus.EVENT_BUS.channel(RefreshingVersionsEvent.class).register(event -> {
if (event.getSource() == profile.getRepository())
// This will occupy 0.5s. Too slow!
JFXUtilities.runInFX(this::loadingVersions);
});
if (profile.getRepository().isLoaded())
loadVersions(profile.getRepository());
else
profile.getRepository().refreshVersionsAsync().start();
}
private String modifyVersion(String gameVersion, String version) {
return removeSuffix(removePrefix(removeSuffix(removePrefix(version.replace(gameVersion, "").trim(), "-"), "-"), "_"), "_");
}
private Node buildNode(HMCLGameRepository repository, Version version, Callable<String> gameCallable) {
Profile profile = repository.getProfile();
String id = version.getId();
VersionItem item = new VersionItem();
item.setUpdate(repository.isModpack(id));
Task.ofResult("game", gameCallable).subscribe(Schedulers.javafx(), vars -> {
String game = vars.get("game");
item.setGameVersion(game);
StringBuilder libraries = new StringBuilder();
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version);
analyzer.getForge().ifPresent(library -> libraries.append(i18n("install.installer.forge")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)forge", ""))).append("\n"));
analyzer.getLiteLoader().ifPresent(library -> libraries.append(i18n("install.installer.liteloader")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)liteloader", ""))).append("\n"));
analyzer.getOptiFine().ifPresent(library -> libraries.append(i18n("install.installer.optifine")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)optifine", ""))).append("\n"));
item.setLibraries(libraries.toString());
});
item.setVersionName(id);
item.setOnLaunchButtonClicked(e -> Versions.launch(profile, id));
item.setOnScriptButtonClicked(e -> Versions.generateLaunchScript(profile, id));
item.setOnSettingsButtonClicked(e -> {
Controllers.getVersionPage().load(id, profile);
Controllers.navigate(Controllers.getVersionPage());
});
item.setOnUpdateButtonClicked(event -> {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
if (selectedFile != null) {
AtomicReference<Region> region = new AtomicReference<>();
try {
TaskExecutor executor = ModpackHelper.getUpdateTask(profile, selectedFile, id, ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(id)))
.then(Task.of(Schedulers.javafx(), () -> region.get().fireEvent(new DialogCloseEvent()))).executor();
region.set(Controllers.taskDialog(executor, i18n("modpack.update"), ""));
executor.start();
} catch (UnsupportedModpackException e) {
region.get().fireEvent(new DialogCloseEvent());
Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageBox.ERROR_MESSAGE);
} catch (MismatchedModpackTypeException e) {
region.get().fireEvent(new DialogCloseEvent());
Controllers.dialog(i18n("modpack.mismatched_type"), i18n("message.error"), MessageBox.ERROR_MESSAGE);
} catch (IOException e) {
region.get().fireEvent(new DialogCloseEvent());
Controllers.dialog(i18n("modpack.invalid"), i18n("message.error"), MessageBox.ERROR_MESSAGE);
}
}
});
item.setOnMouseClicked(event -> {
if (event.getButton() == MouseButton.SECONDARY) {
JFXListView<String> versionList = new JFXListView<>();
JFXPopup versionPopup = new JFXPopup(versionList);
versionList.getStyleClass().add("option-list-view");
FXUtils.setLimitWidth(versionList, 150);
versionList.getItems().setAll(Lang.immutableListOf(
i18n("version.manage.rename"),
i18n("version.manage.remove"),
i18n("modpack.export"),
i18n("folder.game")
));
versionList.setOnMouseClicked(e -> {
versionPopup.hide();
switch (versionList.getSelectionModel().getSelectedIndex()) {
case 0:
Versions.renameVersion(profile, id);
break;
case 1:
Versions.deleteVersion(profile, id);
break;
case 2:
Versions.exportVersion(profile, id);
break;
case 3:
FXUtils.openFolder(repository.getRunDirectory(id));
break;
default:
break;
}
});
versionPopup.show(item, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
} else if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
Versions.launch(profile, id);
}
});
File iconFile = repository.getVersionIcon(id);
if (iconFile.exists())
item.setImage(new Image("file:" + iconFile.getAbsolutePath()));
return item;
}
private void loadingVersions() {
getChildren().setAll(spinner);
masonryPane.getChildren().clear();
}
private void loadVersions(HMCLGameRepository repository) {
List<Node> children = repository.getVersions().parallelStream()
.filter(version -> !version.isHidden())
.sorted((a, b) -> VersionNumber.COMPARATOR.compare(VersionNumber.asVersion(a.getId()), VersionNumber.asVersion(b.getId())))
.map(version -> buildNode(repository, version, () -> GameVersion.minecraftVersion(repository.getVersionJar(version.getId())).orElse("Unknown")))
.collect(Collectors.toList());
JFXUtilities.runInFX(() -> {
if (profile == repository.getProfile()) {
masonryPane.getChildren().setAll(children);
getChildren().setAll(contentPane);
}
});
}
}
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,35 +13,32 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.utils.JFXUtilities;
import javafx.application.Platform;
import javafx.beans.binding.When;
import javafx.scene.image.Image;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem;
import org.jackhuang.hmcl.ui.account.AddAccountPane;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.IconedItem;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.profile.ProfileAdvancedListItem;
import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.util.concurrent.atomic.AtomicReference;
@@ -55,38 +52,48 @@ public final class LeftPaneController extends AdvancedListBox {
AccountAdvancedListItem accountListItem = new AccountAdvancedListItem();
accountListItem.setOnAction(e -> Controllers.navigate(Controllers.getAccountListPage()));
accountListItem.accountProperty().bind(Accounts.selectedAccountProperty());
GameAdvancedListItem gameListItem = new GameAdvancedListItem();
gameListItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage()));
gameListItem.actionButtonVisibleProperty().bind(Profiles.selectedVersionProperty().isNotNull());
gameListItem.setOnAction(e -> {
Profile profile = Profiles.getSelectedProfile();
String version = Profiles.getSelectedVersion();
if (version == null) {
Controllers.navigate(Controllers.getGameListPage());
} else {
Versions.modifyGameSettings(profile, version);
}
});
ProfileAdvancedListItem profileListItem = new ProfileAdvancedListItem();
profileListItem.setOnAction(e -> Controllers.navigate(Controllers.getProfileListPage()));
profileListItem.profileProperty().bind(Profiles.selectedProfileProperty());
IconedItem launcherSettingsItem = new IconedItem(SVG.gear(Theme.blackFillBinding(), 20, 20), "iconed-item");
AdvancedListItem gameItem = new AdvancedListItem();
gameItem.setImage(new Image("/assets/img/bookshelf.png"));
gameItem.setTitle(i18n("version.manage"));
gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage()));
launcherSettingsItem.getLabel().textProperty().bind(
new When(UpdateChecker.outdatedProperty())
.then(i18n("update.found"))
.otherwise(i18n("settings.launcher")));
launcherSettingsItem.getLabel().textFillProperty().bind(
new When(UpdateChecker.outdatedProperty())
.then(Color.RED)
.otherwise(Color.BLACK));
launcherSettingsItem.setOnMouseClicked(e -> Controllers.navigate(Controllers.getSettingsPage()));
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
launcherSettingsItem.setImage(new Image("/assets/img/command.png"));
launcherSettingsItem.setTitle(i18n("settings.launcher"));
launcherSettingsItem.setOnAction(e -> Controllers.navigate(Controllers.getSettingsPage()));
this
.startCategory(i18n("account").toUpperCase())
.add(accountListItem)
.startCategory(i18n("version").toUpperCase())
.add(gameListItem)
.add(gameItem)
.startCategory(i18n("profile.title").toUpperCase())
.add(profileListItem)
.startCategory(i18n("launcher").toUpperCase())
.add(launcherSettingsItem);
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(event -> onRefreshedVersions((HMCLGameRepository) event.getSource()));
if (Profiles.selectedProfileProperty().get().getRepository().isLoaded())
Profile profile = Profiles.getSelectedProfile();
if (profile != null && profile.getRepository().isLoaded())
onRefreshedVersions(Profiles.selectedProfileProperty().get().getRepository());
}
@@ -102,7 +109,6 @@ public final class LeftPaneController extends AdvancedListBox {
// ====
private boolean checkedModpack = false;
private static boolean showNewAccount = true;
private void onRefreshedVersions(HMCLGameRepository repository) {
JFXUtilities.runInFX(() -> {
@@ -112,27 +118,20 @@ public final class LeftPaneController extends AdvancedListBox {
if (repository.getVersionCount() == 0) {
File modpackFile = new File("modpack.zip").getAbsoluteFile();
if (modpackFile.exists()) {
try {
AtomicReference<Region> region = new AtomicReference<>();
Modpack modpack = ModpackHelper.readModpackManifest(modpackFile);
TaskExecutor executor = ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
.with(Task.of(Schedulers.javafx(), () -> {
region.get().fireEvent(new DialogCloseEvent());
checkAccount();
})).executor();
region.set(Controllers.taskDialog(executor, i18n("modpack.installing"), ""));
executor.start();
showNewAccount = false;
} catch (UnsupportedModpackException ignore) {
}
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(modpackFile.toPath()))
.thenApply(encoding -> ModpackHelper.readModpackManifest(modpackFile.toPath(), encoding))
.thenAccept(modpack -> {
AtomicReference<Region> region = new AtomicReference<>();
TaskExecutor executor = ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
.withRun(Schedulers.javafx(), this::checkAccount).executor();
region.set(Controllers.taskDialog(executor, i18n("modpack.installing")));
executor.start();
}).start();
}
}
}
if (showNewAccount) {
showNewAccount = false;
checkAccount();
}
checkAccount();
});
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
@@ -22,26 +22,59 @@ import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
public abstract class ListPage<T extends Node> extends Control {
private final ListProperty<T> items = new SimpleListProperty<>(FXCollections.observableArrayList());
private final BooleanProperty loading = new SimpleBooleanProperty(false);
private final ListProperty<T> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading", false);
private final BooleanProperty refreshable = new SimpleBooleanProperty(this, "refreshable", false);
public abstract void add();
public ListProperty<T> itemsProperty() {
return items;
}
public BooleanProperty loadingProperty() {
return loading;
public void refresh() {
}
@Override
protected Skin<?> createDefaultSkin() {
return new ListPageSkin(this);
}
public ObservableList<T> getItems() {
return items.get();
}
public void setItems(ObservableList<T> items) {
this.items.set(items);
}
public ListProperty<T> itemsProperty() {
return items;
}
public boolean isLoading() {
return loading.get();
}
public void setLoading(boolean loading) {
this.loading.set(loading);
}
public BooleanProperty loadingProperty() {
return loading;
}
public boolean isRefreshable() {
return refreshable.get();
}
public BooleanProperty refreshableProperty() {
return refreshable;
}
public void setRefreshable(boolean refreshable) {
this.refreshable.set(refreshable);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,32 +13,32 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXScrollPane;
import com.jfoenix.controls.JFXSpinner;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SkinBase;
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> {
public class ListPageSkin extends SkinBase<ListPage<?>> {
public ListPageSkin(ListPage<?> skinnable) {
super(skinnable);
StackPane rootPane = new StackPane();
JFXSpinner spinner = new JFXSpinner();
spinner.setRadius(16);
spinner.getStyleClass().setAll("materialDesign-purple", "first-spinner");
SpinnerPane spinnerPane = new SpinnerPane();
spinnerPane.getStyleClass().add("large-spinner-pane");
Pane placeholder = new Pane();
StackPane contentPane = new StackPane();
{
@@ -46,17 +46,20 @@ public class ListPageSkin extends SkinBase<ListPage> {
{
scrollPane.setFitToWidth(true);
VBox accountList = new VBox();
accountList.maxWidthProperty().bind(scrollPane.widthProperty());
accountList.setSpacing(10);
accountList.setStyle("-fx-padding: 10 10 10 10;");
VBox list = new VBox();
list.maxWidthProperty().bind(scrollPane.widthProperty());
list.setSpacing(10);
list.setPadding(new Insets(10));
Bindings.bindContent(accountList.getChildren(), skinnable.itemsProperty());
VBox content = new VBox();
content.getChildren().setAll(list, placeholder);
scrollPane.setContent(accountList);
Bindings.bindContent(list.getChildren(), skinnable.itemsProperty());
scrollPane.setContent(content);
JFXScrollPane.smoothScrolling(scrollPane);
}
VBox vBox = new VBox();
{
vBox.setAlignment(Pos.BOTTOM_RIGHT);
@@ -67,24 +70,40 @@ public class ListPageSkin extends SkinBase<ListPage> {
JFXButton btnAdd = new JFXButton();
FXUtils.setLimitWidth(btnAdd, 40);
FXUtils.setLimitHeight(btnAdd, 40);
btnAdd.getStyleClass().setAll("jfx-button-raised-round");
btnAdd.getStyleClass().add("jfx-button-raised-round");
btnAdd.setButtonType(JFXButton.ButtonType.RAISED);
btnAdd.setGraphic(SVG.plus(Theme.whiteFillBinding(), -1, -1));
btnAdd.setOnMouseClicked(e -> skinnable.add());
JFXButton btnRefresh = new JFXButton();
FXUtils.setLimitWidth(btnRefresh, 40);
FXUtils.setLimitHeight(btnRefresh, 40);
btnRefresh.getStyleClass().add("jfx-button-raised-round");
btnRefresh.setButtonType(JFXButton.ButtonType.RAISED);
btnRefresh.setGraphic(SVG.refresh(Theme.whiteFillBinding(), -1, -1));
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
vBox.getChildren().setAll(btnAdd);
FXUtils.onChangeAndOperate(skinnable.refreshableProperty(),
refreshable -> {
if (refreshable) vBox.getChildren().setAll(btnRefresh, btnAdd);
else vBox.getChildren().setAll(btnAdd);
});
}
contentPane.getChildren().setAll(scrollPane, vBox);
// Keep a blank space to prevent buttons from blocking up mod items.
BorderPane group = new BorderPane();
group.setPickOnBounds(false);
group.setBottom(vBox);
placeholder.minHeightProperty().bind(vBox.heightProperty());
contentPane.getChildren().setAll(scrollPane, group);
}
rootPane.getChildren().setAll(contentPane);
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
spinnerPane.setContent(contentPane);
skinnable.loadingProperty().addListener((a, b, newValue) -> {
if (newValue) rootPane.getChildren().setAll(spinner);
else rootPane.getChildren().setAll(contentPane);
});
getChildren().setAll(rootPane);
getChildren().setAll(spinnerPane);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
@@ -33,11 +33,10 @@ import javafx.stage.Stage;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.util.IOUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
@@ -176,7 +175,7 @@ public final class LogWindow extends Stage {
engine = webView.getEngine();
engine.loadContent(Lang.ignoringException(() -> IOUtils.readFullyAsString(getClass().getResourceAsStream("/assets/log-window-content.html")))
.replace("${FONT}", Settings.instance().getFont().getSize() + "px \"" + Settings.instance().getFont().getFamily() + "\""));
.replace("${FONT}", config().getFontSize() + "px \"" + config().getFontFamily() + "\""));
engine.getLoadWorker().stateProperty().addListener((a, b, newValue) -> {
if (newValue == Worker.State.SUCCEEDED) {
document = engine.getDocument();
@@ -202,11 +201,11 @@ public final class LogWindow extends Stage {
if (!flag)
cboLines.getSelectionModel().select(0);
btnFatals.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(fatal.get()) + " fatals", fatal));
btnErrors.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(error.get()) + " errors", error));
btnWarns.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(warn.get()) + " warns", warn));
btnInfos.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(info.get()) + " infos", info));
btnDebugs.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(debug.get()) + " debugs", debug));
btnFatals.textProperty().bind(Bindings.concat(fatal, " fatals"));
btnErrors.textProperty().bind(Bindings.concat(error, " errors"));
btnWarns.textProperty().bind(Bindings.concat(warn, " warns"));
btnInfos.textProperty().bind(Bindings.concat(info, " infos"));
btnDebugs.textProperty().bind(Bindings.concat(debug, " debugs"));
btnFatals.selectedProperty().addListener(o -> specificChanged());
btnErrors.selectedProperty().addListener(o -> specificChanged());
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,46 +13,194 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.fxml.FXML;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.setting.ConfigHolder;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.construct.PopupMenu;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import static org.jackhuang.hmcl.ui.FXUtils.SINE;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MainPage extends StackPane implements DecoratorPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", i18n("main_page"));
@FXML
private StackPane main;
private final PopupMenu menu = new PopupMenu();
private final JFXPopup popup = new JFXPopup(menu);
private final StringProperty currentGame = new SimpleStringProperty(this, "currentGame");
private final BooleanProperty showUpdate = new SimpleBooleanProperty(this, "showUpdate");
private final StringProperty latestVersion = new SimpleStringProperty(this, "latestVersion");
private final ObservableList<Node> versions = FXCollections.observableArrayList();
private StackPane updatePane;
private JFXButton menuButton;
{
FXUtils.loadFXML(this, "/assets/fxml/main.fxml");
setPadding(new Insets(25));
FXUtils.onChangeAndOperate(ConfigHolder.config().enableMainPageGameListProperty(), newValue -> {
if (newValue)
getChildren().setAll(new GameVersionListPage());
else
getChildren().setAll(main);
});
updatePane = new StackPane();
updatePane.setVisible(false);
updatePane.getStyleClass().add("bubble");
FXUtils.setLimitWidth(updatePane, 230);
FXUtils.setLimitHeight(updatePane, 55);
StackPane.setAlignment(updatePane, Pos.TOP_RIGHT);
updatePane.setOnMouseClicked(e -> onUpgrade());
FXUtils.onChange(showUpdateProperty(), this::doAnimation);
{
HBox hBox = new HBox();
hBox.setSpacing(12);
hBox.setAlignment(Pos.CENTER_LEFT);
StackPane.setAlignment(hBox, Pos.CENTER_LEFT);
StackPane.setMargin(hBox, new Insets(9, 12, 9, 16));
{
Label lblIcon = new Label();
lblIcon.setGraphic(SVG.update(Theme.whiteFillBinding(), 20, 20));
TwoLineListItem prompt = new TwoLineListItem();
prompt.setSubtitle(i18n("update.bubble.subtitle"));
prompt.setPickOnBounds(false);
prompt.titleProperty().bind(latestVersionProperty());
hBox.getChildren().setAll(lblIcon, prompt);
}
JFXButton closeUpdateButton = new JFXButton();
closeUpdateButton.setGraphic(SVG.close(Theme.whiteFillBinding(), 10, 10));
StackPane.setAlignment(closeUpdateButton, Pos.TOP_RIGHT);
closeUpdateButton.getStyleClass().add("toggle-icon-tiny");
StackPane.setMargin(closeUpdateButton, new Insets(5));
closeUpdateButton.setOnMouseClicked(e -> closeUpdateBubble());
updatePane.getChildren().setAll(hBox, closeUpdateButton);
}
StackPane launchPane = new StackPane();
launchPane.setMaxWidth(230);
launchPane.setMaxHeight(55);
StackPane.setAlignment(launchPane, Pos.BOTTOM_RIGHT);
{
JFXButton launchButton = new JFXButton();
launchButton.setPrefWidth(230);
launchButton.setPrefHeight(55);
launchButton.setButtonType(JFXButton.ButtonType.RAISED);
launchButton.getStyleClass().add("jfx-button-raised");
launchButton.setOnMouseClicked(e -> launch());
launchButton.setClip(new Rectangle(-100, -100, 310, 200));
{
VBox graphic = new VBox();
graphic.setAlignment(Pos.CENTER);
graphic.setTranslateX(-7);
graphic.setMaxWidth(200);
Label launchLabel = new Label(i18n("version.launch"));
launchLabel.setStyle("-fx-font-size: 16px;");
Label currentLabel = new Label();
currentLabel.setStyle("-fx-font-size: 12px;");
currentLabel.textProperty().bind(currentGameProperty());
graphic.getChildren().setAll(launchLabel, currentLabel);
launchButton.setGraphic(graphic);
}
Rectangle separator = new Rectangle();
separator.getStyleClass().add("darker-fill");
separator.setWidth(1);
separator.setHeight(57);
separator.setTranslateX(95);
separator.setMouseTransparent(true);
menuButton = new JFXButton();
menuButton.setPrefHeight(55);
menuButton.setPrefWidth(230);
menuButton.setButtonType(JFXButton.ButtonType.RAISED);
menuButton.getStyleClass().add("jfx-button-raised");
menuButton.setStyle("-fx-font-size: 15px;");
menuButton.setOnMouseClicked(e -> onMenu());
menuButton.setClip(new Rectangle(211, -100, 100, 200));
StackPane graphic = new StackPane();
Node svg = SVG.triangle(Theme.whiteFillBinding(), 10, 10);
StackPane.setAlignment(svg, Pos.CENTER_RIGHT);
graphic.getChildren().setAll(svg);
graphic.setTranslateX(12);
menuButton.setGraphic(graphic);
launchPane.getChildren().setAll(launchButton, separator, menuButton);
}
getChildren().setAll(updatePane, launchPane);
menu.setMaxHeight(365);
menu.setMaxWidth(545);
menu.setAlwaysShowingVBar(true);
menu.setOnMouseClicked(e -> popup.hide());
Bindings.bindContent(menu.getContent(), versions);
}
private void doAnimation(boolean show) {
Duration duration = Duration.millis(320);
Timeline nowAnimation = new Timeline();
nowAnimation.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO,
new KeyValue(updatePane.translateXProperty(), show ? 260 : 0, SINE)),
new KeyFrame(duration,
new KeyValue(updatePane.translateXProperty(), show ? 0 : 260, SINE)));
if (show) nowAnimation.getKeyFrames().add(
new KeyFrame(Duration.ZERO, e -> updatePane.setVisible(true)));
else nowAnimation.getKeyFrames().add(
new KeyFrame(duration, e -> updatePane.setVisible(false)));
nowAnimation.play();
}
@FXML
private void launch() {
Profile profile = Profiles.getSelectedProfile();
Versions.launch(profile, profile.getSelectedVersion());
}
private void onMenu() {
popup.show(menuButton, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.RIGHT, 0, -menuButton.getHeight());
}
private void onUpgrade() {
RemoteVersion target = UpdateChecker.getLatestVersion();
if (target == null) {
return;
}
UpdateHandler.updateFrom(target);
}
private void closeUpdateBubble() {
showUpdate.unbind();
showUpdate.set(false);
}
public String getTitle() {
return title.get();
}
@@ -65,4 +213,44 @@ public final class MainPage extends StackPane implements DecoratorPage {
public void setTitle(String title) {
this.title.set(title);
}
public String getCurrentGame() {
return currentGame.get();
}
public StringProperty currentGameProperty() {
return currentGame;
}
public void setCurrentGame(String currentGame) {
this.currentGame.set(currentGame);
}
public boolean isShowUpdate() {
return showUpdate.get();
}
public BooleanProperty showUpdateProperty() {
return showUpdate;
}
public void setShowUpdate(boolean showUpdate) {
this.showUpdate.set(showUpdate);
}
public String getLatestVersion() {
return latestVersion.get();
}
public StringProperty latestVersionProperty() {
return latestVersion;
}
public void setLatestVersion(String latestVersion) {
this.latestVersion.set(latestVersion);
}
public ObservableList<Node> getVersions() {
return versions;
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
@@ -150,4 +150,20 @@ public final class SVG {
public static Node openInNew(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("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", fill, width, height);
}
public static Node triangle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M1,21H23L12,2", fill, width, height);
}
public static Node home(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z", fill, width, height);
}
public static Node viewList(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("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", fill, width, height);
}
public static Node check(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z", fill, width, height);
}
}
@@ -1,43 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
import javafx.util.StringConverter;
import org.jackhuang.hmcl.util.Lang;
import java.util.Optional;
/**
* @author huangyuhui
*/
public final class SafeIntStringConverter extends StringConverter<Number> {
public static final SafeIntStringConverter INSTANCE = new SafeIntStringConverter();
private SafeIntStringConverter() {
}
@Override
public Number fromString(String string) {
return Optional.ofNullable(string).map(Lang::toIntOrNull).orElse(null);
}
@Override
public String toString(Number object) {
return Optional.ofNullable(object).map(Object::toString).orElse("");
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
@@ -24,36 +24,47 @@ import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.When;
import javafx.beans.property.*;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.scene.control.ToggleGroup;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jackhuang.hmcl.upgrade.UpdateChannel;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.i18n.Locales;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import java.awt.*;
import java.io.IOException;
import java.net.Proxy;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.SelectionModelSelectedItemProperty.selectedItemPropertyFor;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.reservedSelectedPropertyFor;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
public final class SettingsPage extends SettingsView implements DecoratorPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", i18n("settings.launcher"));
private ObjectProperty<Proxy.Type> selectedProxyType;
private InvalidationListener updateListener;
public SettingsPage() {
@@ -64,26 +75,19 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
selectedItemPropertyFor(cboDownloadSource).bindBidirectional(config().downloadTypeProperty());
// ====
chkEnableGameList.selectedProperty().bindBidirectional(config().enableMainPageGameListProperty());
// ==== Font ====
cboFont.valueProperty().bindBidirectional(config().fontFamilyProperty());
cboFont.initValue(Settings.instance().getFont());
cboFont.valueProperty().addListener((a, b, newValue) -> {
Font font = Font.font(newValue, Settings.instance().getFont().getSize());
Settings.instance().setFont(font);
lblDisplay.setStyle("-fx-font: " + font.getSize() + " \"" + font.getFamily() + "\";");
});
txtFontSize.textProperty().bindBidirectional(config().fontSizeProperty(),
SafeStringConverter.fromFiniteDouble()
.restrict(it -> it > 0)
.fallbackTo(12.0)
.asPredicate(Validator.addTo(txtFontSize)));
txtFontSize.setText(Double.toString(Settings.instance().getFont().getSize()));
txtFontSize.getValidators().add(new Validator(it -> Lang.toDoubleOrNull(it) != null));
txtFontSize.textProperty().addListener((a, b, newValue) -> {
if (txtFontSize.validate()) {
Font font = Font.font(Settings.instance().getFont().getFamily(), Double.parseDouble(newValue));
Settings.instance().setFont(font);
lblDisplay.setStyle("-fx-font: " + font.getSize() + " \"" + font.getFamily() + "\";");
}
});
lblDisplay.setStyle("-fx-font: " + Settings.instance().getFont().getSize() + " \"" + Settings.instance().getFont().getFamily() + "\";");
lblDisplay.fontProperty().bind(Bindings.createObjectBinding(
() -> Font.font(config().getFontFamily(), config().getFontSize()),
config().fontFamilyProperty(), config().fontSizeProperty()));
// ====
// ==== Languages ====
cboLanguage.getItems().setAll(Locales.LOCALES);
@@ -92,61 +96,40 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
// ==== Proxy ====
txtProxyHost.textProperty().bindBidirectional(config().proxyHostProperty());
txtProxyPort.textProperty().bindBidirectional(config().proxyPortProperty());
txtProxyPort.textProperty().bindBidirectional(config().proxyPortProperty(),
SafeStringConverter.fromInteger()
.restrict(it -> it >= 0 && it <= 0xFFFF)
.fallbackTo(0)
.asPredicate(Validator.addTo(txtProxyPort)));
txtProxyUsername.textProperty().bindBidirectional(config().proxyUserProperty());
txtProxyPassword.textProperty().bindBidirectional(config().proxyPassProperty());
proxyPane.disableProperty().bind(chkDisableProxy.selectedProperty());
authPane.disableProperty().bind(chkProxyAuthentication.selectedProperty().not());
chkDisableProxy.setSelected(!config().hasProxy());
chkDisableProxy.selectedProperty().addListener(o -> config().setHasProxy(!chkDisableProxy.isSelected()));
config().hasProxyProperty().addListener(o -> chkDisableProxy.setSelected(!config().hasProxy()));
reservedSelectedPropertyFor(chkDisableProxy).bindBidirectional(config().hasProxyProperty());
chkProxyAuthentication.selectedProperty().bindBidirectional(config().hasProxyAuthProperty());
selectedProxyType = new SimpleObjectProperty<Proxy.Type>(Proxy.Type.HTTP) {
{
invalidated();
}
@Override
protected void invalidated() {
Proxy.Type type = Objects.requireNonNull(get());
if (type == Proxy.Type.DIRECT) {
set(Proxy.Type.HTTP); // HTTP by default
} else {
chkProxyHttp.setSelected(type == Proxy.Type.HTTP);
chkProxySocks.setSelected(type == Proxy.Type.SOCKS);
}
}
};
selectedProxyType.bindBidirectional(config().proxyTypeProperty());
ToggleGroup proxyConfigurationGroup = new ToggleGroup();
chkProxyHttp.setUserData(Proxy.Type.HTTP);
chkProxyHttp.setToggleGroup(proxyConfigurationGroup);
chkProxySocks.setUserData(Proxy.Type.SOCKS);
chkProxySocks.setToggleGroup(proxyConfigurationGroup);
proxyConfigurationGroup.getToggles().forEach(
toggle -> toggle.selectedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
selectedProxyType.set((Proxy.Type) toggle.getUserData());
}
}));
selectedItemPropertyFor(proxyConfigurationGroup, Proxy.Type.class).bindBidirectional(config().proxyTypeProperty());
// ====
fileCommonLocation.loadChildren(Arrays.asList(
fileCommonLocation.createChildren(i18n("launcher.common_directory.default"), EnumCommonDirectory.DEFAULT)
fileCommonLocation.createChildren(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT)
), EnumCommonDirectory.CUSTOM);
fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty());
fileCommonLocation.customTextProperty().bindBidirectional(config().commonDirectoryProperty());
fileCommonLocation.subtitleProperty().bind(
Bindings.createObjectBinding(() -> Optional.ofNullable(Settings.instance().getCommonDirectory())
.orElse(i18n("launcher.common_directory.disabled")),
.orElse(i18n("launcher.cache_directory.disabled")),
config().commonDirectoryProperty(), config().commonDirTypeProperty()));
// ==== Update ====
FXUtils.installTooltip(btnUpdate, i18n("update.tooltip"));
FXUtils.installFastTooltip(btnUpdate, i18n("update.tooltip"));
updateListener = any -> {
btnUpdate.setVisible(UpdateChecker.isOutdated());
@@ -177,27 +160,12 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
lblUpdateNote.setWrappingWidth(470);
ObjectProperty<UpdateChannel> updateChannel = new SimpleObjectProperty<UpdateChannel>() {
@Override
protected void invalidated() {
UpdateChannel updateChannel = Objects.requireNonNull(get());
chkUpdateDev.setSelected(updateChannel == UpdateChannel.DEVELOPMENT);
chkUpdateStable.setSelected(updateChannel == UpdateChannel.STABLE);
}
};
ToggleGroup updateChannelGroup = new ToggleGroup();
chkUpdateDev.setToggleGroup(updateChannelGroup);
chkUpdateDev.setUserData(UpdateChannel.DEVELOPMENT);
chkUpdateStable.setToggleGroup(updateChannelGroup);
chkUpdateStable.setUserData(UpdateChannel.STABLE);
updateChannelGroup.getToggles().forEach(
toggle -> toggle.selectedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
updateChannel.set((UpdateChannel) toggle.getUserData());
}
}));
updateChannel.bindBidirectional(ConfigHolder.config().updateChannelProperty());
selectedItemPropertyFor(updateChannelGroup, UpdateChannel.class).bindBidirectional(config().updateChannelProperty());
// ====
// ==== Background ====
@@ -249,8 +217,39 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
UpdateHandler.updateFrom(target);
}
@Override
protected void onExportLogs() {
// We cannot determine which file is JUL using.
// So we write all the logs to a new file.
thread(() -> {
Path logFile = Paths.get("hmcl-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
LOG.info("Exporting logs to " + logFile);
try {
Files.write(logFile, Logging.getRawLogs());
} catch (IOException e) {
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + e, null, MessageType.ERROR));
LOG.log(Level.WARNING, "Failed to export logs", e);
return;
}
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", logFile)));
if (Desktop.isDesktopSupported()) {
try {
Desktop.getDesktop().open(logFile.toFile());
} catch (IOException ignored) {
}
}
});
}
@Override
protected void onHelp() {
FXUtils.openLink(Metadata.HELP_URL);
}
@Override
protected void onSponsor() {
FXUtils.openLink("https://hmcl.huangyuhui.net/api/redirect/sponsor");
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
@@ -21,23 +21,22 @@ import com.jfoenix.controls.*;
import javafx.geometry.HPos;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.*;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public abstract class SettingsView extends StackPane {
protected final JFXTextField txtProxyHost;
protected final JFXTextField txtProxyPort;
@@ -64,7 +63,6 @@ public abstract class SettingsView extends StackPane {
protected final JFXCheckBox chkProxyAuthentication;
protected final GridPane authPane;
protected final Pane proxyPane;
protected final JFXToggleButton chkEnableGameList;
public SettingsView() {
scroll = new ScrollPane();
@@ -78,6 +76,19 @@ public abstract class SettingsView extends StackPane {
{
ComponentList settingsPane = new ComponentList();
{
{
StackPane sponsorPane = new StackPane();
sponsorPane.setCursor(Cursor.HAND);
sponsorPane.setOnMouseClicked(e -> onSponsor());
{
Label label = new Label(i18n("sponsor.hmcl"));
label.setWrapText(true);
label.prefWidthProperty().bind(sponsorPane.widthProperty());
sponsorPane.getChildren().add(label);
}
settingsPane.getContent().add(sponsorPane);
}
ComponentSublist updatePane = new ComponentSublist();
updatePane.setTitle(i18n("update"));
updatePane.setHasSubtitle(true);
@@ -86,7 +97,7 @@ public abstract class SettingsView extends StackPane {
lblUpdate = new Label(i18n("update"));
lblUpdateSub = new Label();
lblUpdateSub.getStyleClass().setAll("subtitle-label");
lblUpdateSub.getStyleClass().add("subtitle-label");
headerLeft.getChildren().setAll(lblUpdate, lblUpdateSub);
updatePane.setHeaderLeft(headerLeft);
@@ -95,7 +106,7 @@ public abstract class SettingsView extends StackPane {
{
btnUpdate = new JFXButton();
btnUpdate.setOnMouseClicked(e -> onUpdate());
btnUpdate.getStyleClass().setAll("toggle-icon4");
btnUpdate.getStyleClass().add("toggle-icon4");
btnUpdate.setGraphic(SVG.update(Theme.blackFillBinding(), 20, 20));
updatePane.setHeaderRight(btnUpdate);
@@ -127,7 +138,7 @@ public abstract class SettingsView extends StackPane {
Label help = new Label(i18n("help"));
Label helpSubtitle = new Label(i18n("help.detail"));
helpSubtitle.getStyleClass().setAll("subtitle-label");
helpSubtitle.getStyleClass().add("subtitle-label");
headerLeft.getChildren().setAll(help, helpSubtitle);
updatePane.setLeft(headerLeft);
@@ -136,7 +147,7 @@ public abstract class SettingsView extends StackPane {
{
JFXButton btnExternal = new JFXButton();
btnExternal.setOnMouseClicked(e -> onHelp());
btnExternal.getStyleClass().setAll("toggle-icon4");
btnExternal.getStyleClass().add("toggle-icon4");
btnExternal.setGraphic(SVG.openInNew(Theme.blackFillBinding(), -1, -1));
updatePane.setRight(btnExternal);
@@ -146,9 +157,9 @@ public abstract class SettingsView extends StackPane {
{
fileCommonLocation = new MultiFileItem<>(true);
fileCommonLocation.setTitle(i18n("launcher.common_directory"));
fileCommonLocation.setTitle(i18n("launcher.cache_directory"));
fileCommonLocation.setDirectory(true);
fileCommonLocation.setChooserTitle(i18n("launcher.common_directory.choose"));
fileCommonLocation.setChooserTitle(i18n("launcher.cache_directory.choose"));
fileCommonLocation.setHasSubtitle(true);
fileCommonLocation.setCustomText("settings.custom");
@@ -165,23 +176,6 @@ public abstract class SettingsView extends StackPane {
settingsPane.getContent().add(backgroundItem);
}
{
BorderPane borderPane = new BorderPane();
Label left = new Label(i18n("settings.launcher.enable_game_list"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
borderPane.setLeft(left);
chkEnableGameList = new JFXToggleButton();
chkEnableGameList.setSize(8);
FXUtils.setLimitHeight(chkEnableGameList, 20);
borderPane.setRight(chkEnableGameList);
// Do not uncomment this line,
// since option for user to enable game list in main page is unnecessary
// settingsPane.addChildren(borderPane);
}
{
BorderPane downloadSourcePane = new BorderPane();
{
@@ -258,7 +252,6 @@ public abstract class SettingsView extends StackPane {
{
txtProxyHost = new JFXTextField();
txtProxyHost.getStyleClass().setAll("fit-width");
GridPane.setRowIndex(txtProxyHost, 1);
GridPane.setColumnIndex(txtProxyHost, 1);
gridPane.getChildren().add(txtProxyHost);
@@ -274,7 +267,6 @@ public abstract class SettingsView extends StackPane {
{
txtProxyPort = new JFXTextField();
txtProxyPort.getStyleClass().setAll("fit-width");
GridPane.setRowIndex(txtProxyPort, 2);
GridPane.setColumnIndex(txtProxyPort, 1);
FXUtils.setValidateWhileTextChanged(txtProxyPort, true);
@@ -311,7 +303,6 @@ public abstract class SettingsView extends StackPane {
{
txtProxyUsername = new JFXTextField();
txtProxyUsername.getStyleClass().setAll("fit-width");
GridPane.setRowIndex(txtProxyUsername, 0);
GridPane.setColumnIndex(txtProxyUsername, 1);
authPane.getChildren().add(txtProxyUsername);
@@ -326,7 +317,6 @@ public abstract class SettingsView extends StackPane {
{
txtProxyPassword = new JFXPasswordField();
txtProxyPassword.getStyleClass().setAll("fit-width");
GridPane.setRowIndex(txtProxyPassword, 1);
GridPane.setColumnIndex(txtProxyPassword, 1);
authPane.getChildren().add(txtProxyPassword);
@@ -355,34 +345,49 @@ public abstract class SettingsView extends StackPane {
}
{
VBox fontPane = new VBox();
fontPane.setSpacing(5);
settingsPane.getContent().add(fontPane);
ComponentSublist logPane = new ComponentSublist();
logPane.setTitle(i18n("settings.launcher.log"));
{
BorderPane borderPane = new BorderPane();
fontPane.getChildren().add(borderPane);
{
Label left = new Label(i18n("settings.launcher.log_font"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
borderPane.setLeft(left);
}
JFXButton logButton = new JFXButton(i18n("settings.launcher.launcher_log.export"));
logButton.setOnMouseClicked(e -> onExportLogs());
logButton.getStyleClass().add("jfx-button-border");
{
HBox hBox = new HBox();
hBox.setSpacing(3);
cboFont = new FontComboBox(12, false);
txtFontSize = new JFXTextField();
FXUtils.setLimitWidth(txtFontSize, 50);
hBox.getChildren().setAll(cboFont, txtFontSize);
borderPane.setRight(hBox);
}
logPane.setHeaderRight(logButton);
}
lblDisplay = new Label("[23:33:33] [Client Thread/INFO] [WaterPower]: Loaded mod WaterPower.");
fontPane.getChildren().add(lblDisplay);
{
VBox fontPane = new VBox();
fontPane.setSpacing(5);
{
BorderPane borderPane = new BorderPane();
fontPane.getChildren().add(borderPane);
{
Label left = new Label(i18n("settings.launcher.log.font"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
borderPane.setLeft(left);
}
{
HBox hBox = new HBox();
hBox.setSpacing(3);
cboFont = new FontComboBox(12);
txtFontSize = new JFXTextField();
FXUtils.setLimitWidth(txtFontSize, 50);
hBox.getChildren().setAll(cboFont, txtFontSize);
borderPane.setRight(hBox);
}
}
lblDisplay = new Label("[23:33:33] [Client Thread/INFO] [WaterPower]: Loaded mod WaterPower.");
fontPane.getChildren().add(lblDisplay);
logPane.getContent().add(fontPane);
}
settingsPane.getContent().add(logPane);
}
{
@@ -499,4 +504,6 @@ public abstract class SettingsView extends StackPane {
protected abstract void onUpdate();
protected abstract void onHelp();
protected abstract void onExportLogs();
protected abstract void onSponsor();
}
@@ -1,231 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import javafx.beans.binding.Bindings;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.effect.BlurType;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.text.TextAlignment;
import org.jackhuang.hmcl.setting.Theme;
import java.util.Optional;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class VersionItem extends StackPane {
@FXML
private Pane icon;
@FXML
private VBox content;
@FXML
private StackPane header;
@FXML
private StackPane body;
@FXML
private JFXButton btnSettings;
@FXML
private JFXButton btnUpdate;
@FXML
private JFXButton btnLaunch;
@FXML
private JFXButton btnScript;
@FXML
private Label lblVersionName;
@FXML
private Label lblGameVersion;
@FXML
private Label lblLibraries;
@FXML
private ImageView iconView;
private EventHandler<? super MouseEvent> launchClickedHandler = null;
private void initializeComponents() {
setPickOnBounds(false);
FXUtils.setLimitWidth(this, 160);
FXUtils.setLimitHeight(this, 149);
content = new VBox();
{
header = new StackPane();
VBox.setVgrow(header, Priority.ALWAYS);
header.setPickOnBounds(false);
header.setPadding(new Insets(8));
header.setStyle("-fx-background-radius: 2 2 0 0; -fx-background-color: rgb(255,255,255,0.87);");
{
VBox headerContent = new VBox();
headerContent.setPadding(new Insets(8, 8, 0, 8));
{
lblVersionName = new Label();
lblVersionName.setStyle("-fx-font-size: 15;");
lblVersionName.setTextAlignment(TextAlignment.JUSTIFY);
lblVersionName.setWrapText(true);
headerContent.getChildren().add(lblVersionName);
lblGameVersion = new Label();
lblGameVersion.setStyle("-fx-font-size: 11;");
lblGameVersion.setTextAlignment(TextAlignment.JUSTIFY);
lblGameVersion.setWrapText(true);
headerContent.getChildren().add(lblGameVersion);
lblLibraries = new Label();
lblLibraries.setStyle("-fx-font-size: 10; -fx-text-fill: gray;");
lblLibraries.setTextAlignment(TextAlignment.JUSTIFY);
lblLibraries.setWrapText(true);
headerContent.getChildren().add(lblLibraries);
}
header.getChildren().add(headerContent);
}
content.getChildren().add(header);
body = new StackPane();
body.setStyle("-fx-background-radius: 0 0 2 2; -fx-background-color: rgb(255,255,255,0.87); -fx-padding: 8;");
body.setMinHeight(40);
body.setPickOnBounds(false);
{
BorderPane bodyContent = new BorderPane();
{
HBox hbox = new HBox();
hbox.setSpacing(8);
{
btnSettings = new JFXButton();
btnSettings.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(btnSettings, 30);
FXUtils.setLimitHeight(btnSettings, 30);
hbox.getChildren().add(btnSettings);
btnUpdate = new JFXButton();
btnUpdate.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(btnUpdate, 30);
FXUtils.setLimitHeight(btnUpdate, 30);
hbox.getChildren().add(btnUpdate);
}
bodyContent.setLeft(hbox);
}
{
HBox hbox = new HBox();
hbox.setSpacing(8);
{
btnScript = new JFXButton();
btnScript.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(btnScript, 30);
FXUtils.setLimitHeight(btnScript, 30);
hbox.getChildren().add(btnScript);
btnLaunch = new JFXButton();
btnLaunch.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(btnLaunch, 30);
FXUtils.setLimitHeight(btnLaunch, 30);
hbox.getChildren().add(btnLaunch);
}
bodyContent.setRight(hbox);
}
body.getChildren().setAll(bodyContent);
}
content.getChildren().add(body);
}
getChildren().add(content);
icon = new StackPane();
StackPane.setAlignment(icon, Pos.TOP_RIGHT);
icon.setPickOnBounds(false);
{
iconView = new ImageView();
StackPane.setAlignment(iconView, Pos.CENTER_RIGHT);
StackPane.setMargin(iconView, new Insets(0, 12, 0, 0));
iconView.setImage(new Image("/assets/img/icon.png"));
icon.getChildren().add(iconView);
}
getChildren().add(icon);
}
public VersionItem() {
initializeComponents();
setEffect(new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.26), 5.0, 0.12, -1.0, 1.0));
btnSettings.setGraphic(SVG.gear(Theme.blackFillBinding(), 15, 15));
btnUpdate.setGraphic(SVG.update(Theme.blackFillBinding(), 15, 15));
btnLaunch.setGraphic(SVG.launch(Theme.blackFillBinding(), 15, 15));
btnScript.setGraphic(SVG.script(Theme.blackFillBinding(), 15, 15));
JFXUtilities.runInFX(() -> {
FXUtils.installTooltip(btnSettings, i18n("version.settings"));
FXUtils.installTooltip(btnUpdate, i18n("version.update"));
FXUtils.installTooltip(btnLaunch, i18n("version.launch"));
FXUtils.installTooltip(btnScript, i18n("version.launch_script"));
});
icon.translateYProperty().bind(Bindings.createDoubleBinding(() -> header.getBoundsInParent().getHeight() - icon.getHeight() / 2 - 16, header.boundsInParentProperty(), icon.heightProperty()));
FXUtils.limitSize(iconView, 32, 32);
setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY)
if (e.getClickCount() == 2)
Optional.ofNullable(launchClickedHandler).ifPresent(h -> h.handle(e));
});
}
public void setUpdate(boolean update) {
btnUpdate.setVisible(update);
}
public void setVersionName(String versionName) {
lblVersionName.setText(versionName);
}
public void setGameVersion(String gameVersion) {
lblGameVersion.setText(gameVersion);
}
public void setLibraries(String libraries) {
lblLibraries.setText(libraries);
}
public void setImage(Image image) {
iconView.setImage(image);
}
public void setOnSettingsButtonClicked(EventHandler<? super MouseEvent> handler) {
btnSettings.setOnMouseClicked(handler);
}
public void setOnScriptButtonClicked(EventHandler<? super MouseEvent> handler) {
btnScript.setOnMouseClicked(handler);
}
public void setOnLaunchButtonClicked(EventHandler<? super MouseEvent> handler) {
launchClickedHandler = handler;
btnLaunch.setOnMouseClicked(handler);
}
public void setOnUpdateButtonClicked(EventHandler<? super MouseEvent> handler) {
btnUpdate.setOnMouseClicked(handler);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,18 +13,20 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.account;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -36,28 +38,21 @@ public class AccountAdvancedListItem extends AdvancedListItem {
protected void invalidated() {
Account account = get();
if (account == null) {
titleProperty().set(i18n("account.missing"));
subtitleProperty().set(i18n("account.missing.add"));
imageProperty().set(new Image("/assets/img/craft_table.png"));
viewportProperty().set(null);
titleProperty().unbind();
setTitle(i18n("account.missing"));
setSubtitle(i18n("account.missing.add"));
imageProperty().unbind();
setImage(new Image("/assets/img/craft_table.png"));
} else {
titleProperty().set(account.getCharacter());
subtitleProperty().set(accountSubtitle(account));
imageProperty().set(AccountHelper.getDefaultSkin(account.getUUID(), 4));
viewportProperty().set(AccountHelper.getViewport(4));
if (account instanceof YggdrasilAccount) {
AccountHelper.loadSkinAsync((YggdrasilAccount) account).subscribe(Schedulers.javafx(), () -> {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
imageProperty().set(image);
});
}
titleProperty().bind(Bindings.createStringBinding(account::getCharacter, account));
setSubtitle(accountSubtitle(account));
imageProperty().bind(TexturesLoader.fxAvatarBinding(account, 32));
}
}
};
public AccountAdvancedListItem() {
setRightGraphic(SVG.viewList(Theme.blackFillBinding(), -1, -1));
}
public ObjectProperty<Account> accountProperty() {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,54 +13,28 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.account;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.ToggleGroup;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.ListPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.MappedObservableList;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
public class AccountList extends ListPage<AccountListItem> implements DecoratorPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(i18n("account.manage"));
private ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>() {
{
itemsProperty().addListener(onInvalidating(this::invalidated));
}
@Override
protected void invalidated() {
Account selected = get();
itemsProperty().forEach(item -> item.selectedProperty().set(item.getAccount() == selected));
}
};
private final ListProperty<Account> accounts = new SimpleListProperty<>(FXCollections.observableArrayList());
private ToggleGroup toggleGroup;
private final ObservableList<AccountListItem> accountItems;
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", i18n("account.manage"));
private final ListProperty<Account> accounts = new SimpleListProperty<>(this, "accounts", FXCollections.observableArrayList());
private final ObjectProperty<Account> selectedAccount;
public AccountList() {
toggleGroup = new ToggleGroup();
accountItems = MappedObservableList.create(
accountsProperty(),
account -> new AccountListItem(toggleGroup, account));
itemsProperty().bindContent(accountItems);
toggleGroup.selectedToggleProperty().addListener((o, a, toggle) -> {
if (toggle == null || toggle.getUserData() == null) return;
selectedAccount.set(((AccountListItem) toggle.getUserData()).getAccount());
});
setItems(MappedObservableList.create(accounts, AccountListItem::new));
selectedAccount = createSelectedItemPropertyFor(getItems(), Account.class);
}
public ObjectProperty<Account> selectedAccountProperty() {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,60 +13,63 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.account;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.*;
import javafx.geometry.Rectangle2D;
import javafx.scene.control.Control;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Skin;
import javafx.scene.control.ToggleGroup;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CredentialExpiredException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.DialogController;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AccountListItem extends Control {
import java.util.concurrent.CancellationException;
import java.util.logging.Level;
public class AccountListItem extends RadioButton {
private final Account account;
private final ToggleGroup toggleGroup;
private final StringProperty title = new SimpleStringProperty();
private final StringProperty subtitle = new SimpleStringProperty();
private final BooleanProperty selected = new SimpleBooleanProperty();
private final ObjectProperty<Image> image = new SimpleObjectProperty<>();
private final ObjectProperty<Rectangle2D> viewport = new SimpleObjectProperty<>();
public AccountListItem(ToggleGroup toggleGroup, Account account) {
public AccountListItem(Account account) {
this.account = account;
this.toggleGroup = toggleGroup;
getStyleClass().clear();
setUserData(account);
StringBuilder subtitleString = new StringBuilder(Accounts.getAccountTypeName(account));
String loginTypeName = Accounts.getLocalizedLoginTypeName(Accounts.getAccountFactory(account));
if (account instanceof AuthlibInjectorAccount) {
AuthlibInjectorServer server = ((AuthlibInjectorAccount) account).getServer();
subtitleString.append(", ").append(i18n("account.injector.server")).append(": ").append(server.getName());
}
if (account instanceof OfflineAccount)
title.set(account.getCharacter());
else
title.set(account.getUsername() + " - " + account.getCharacter());
subtitle.set(subtitleString.toString());
selected.set(Accounts.selectedAccountProperty().get() == account);
viewport.set(AccountHelper.getViewport(4));
if (account instanceof YggdrasilAccount) {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
this.image.set(image);
subtitle.bind(Bindings.concat(
loginTypeName, ", ", i18n("account.injector.server"), ": ",
Bindings.createStringBinding(server::getName, server)));
} else {
this.image.set(AccountHelper.getDefaultSkin(account.getUUID(), 4));
subtitle.set(loginTypeName);
}
StringBinding characterName = Bindings.createStringBinding(account::getCharacter, account);
if (account instanceof OfflineAccount) {
title.bind(characterName);
} else {
title.bind(Bindings.concat(account.getUsername(), " - ", characterName));
}
image.bind(TexturesLoader.fxAvatarBinding(account, 32));
}
@Override
@@ -74,50 +77,66 @@ public class AccountListItem extends Control {
return new AccountListItemSkin(this);
}
public ToggleGroup getToggleGroup() {
return toggleGroup;
public void refresh() {
account.clearCache();
thread(() -> {
try {
account.logIn();
} catch (CredentialExpiredException e) {
try {
DialogController.logIn(account);
} catch (CancellationException e1) {
// ignore cancellation
} catch (Exception e1) {
LOG.log(Level.WARNING, "Failed to refresh " + account + " with password", e1);
}
} catch (AuthenticationException e) {
LOG.log(Level.WARNING, "Failed to refresh " + account + " with token", e);
}
});
}
public void remove() {
Accounts.getAccounts().remove(account);
}
public Account getAccount() {
return account;
}
public String getTitle() {
return title.get();
}
public void setTitle(String title) {
this.title.set(title);
}
public StringProperty titleProperty() {
return title;
}
public String getSubtitle() {
return subtitle.get();
}
public void setSubtitle(String subtitle) {
this.subtitle.set(subtitle);
}
public StringProperty subtitleProperty() {
return subtitle;
}
public BooleanProperty selectedProperty() {
return selected;
public Image getImage() {
return image.get();
}
public void setImage(Image image) {
this.image.set(image);
}
public ObjectProperty<Image> imageProperty() {
return image;
}
public ObjectProperty<Rectangle2D> viewportProperty() {
return viewport;
}
public void refresh() {
if (account instanceof YggdrasilAccount) {
// progressBar.setVisible(true);
AccountHelper.refreshSkinAsync((YggdrasilAccount) account)
.finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
// progressBar.setVisible(false);
if (isDependentsSucceeded) {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
this.image.set(image);
}
}).start();
}
}
public void remove() {
Accounts.getAccounts().remove(account);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,27 +13,34 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.account;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.utils.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
public class AccountListItemSkin extends SkinBase<AccountListItem> {
public AccountListItemSkin(AccountListItem skinnable) {
@@ -41,29 +48,40 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
BorderPane root = new BorderPane();
JFXRadioButton chkSelected = new JFXRadioButton();
JFXRadioButton chkSelected = new JFXRadioButton() {
@Override
public void fire() {
skinnable.fire();
}
};
BorderPane.setAlignment(chkSelected, Pos.CENTER);
chkSelected.setUserData(skinnable);
chkSelected.selectedProperty().bindBidirectional(skinnable.selectedProperty());
chkSelected.setToggleGroup(skinnable.getToggleGroup());
chkSelected.selectedProperty().bind(skinnable.selectedProperty());
root.setLeft(chkSelected);
HBox center = new HBox();
center.setSpacing(8);
center.setAlignment(Pos.CENTER_LEFT);
StackPane imageViewContainer = new StackPane();
FXUtils.setLimitWidth(imageViewContainer, 32);
FXUtils.setLimitHeight(imageViewContainer, 32);
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, 32, 32);
imageView.imageProperty().bind(skinnable.imageProperty());
imageView.viewportProperty().bind(skinnable.viewportProperty());
imageViewContainer.getChildren().setAll(imageView);
TwoLineListItem item = new TwoLineListItem();
Label title = new Label();
title.getStyleClass().add("title");
title.textProperty().bind(skinnable.titleProperty());
Label subtitle = new Label();
subtitle.getStyleClass().add("subtitle");
subtitle.textProperty().bind(skinnable.subtitleProperty());
if (skinnable.getAccount() instanceof AuthlibInjectorAccount) {
Tooltip tooltip = new Tooltip();
AuthlibInjectorServer server = ((AuthlibInjectorAccount) skinnable.getAccount()).getServer();
tooltip.textProperty().bind(BindingMapping.of(server, AuthlibInjectorServer::toString));
FXUtils.installSlowTooltip(subtitle, tooltip);
}
VBox item = new VBox(title, subtitle);
item.getStyleClass().add("two-line-list-item");
BorderPane.setAlignment(item, Pos.CENTER);
center.getChildren().setAll(imageView, item);
root.setCenter(center);
@@ -73,7 +91,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
btnRefresh.getStyleClass().add("toggle-icon4");
btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), -1, -1));
JFXUtilities.runInFX(() -> FXUtils.installTooltip(btnRefresh, i18n("button.refresh")));
JFXUtilities.runInFX(() -> FXUtils.installFastTooltip(btnRefresh, i18n("button.refresh")));
right.getChildren().add(btnRefresh);
JFXButton btnRemove = new JFXButton();
@@ -81,14 +99,12 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
btnRemove.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnRemove, Pos.CENTER);
btnRemove.setGraphic(SVG.delete(Theme.blackFillBinding(), -1, -1));
JFXUtilities.runInFX(() -> FXUtils.installTooltip(btnRemove, i18n("button.delete")));
JFXUtilities.runInFX(() -> FXUtils.installFastTooltip(btnRemove, i18n("button.delete")));
right.getChildren().add(btnRemove);
root.setRight(right);
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;");
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 12;");
JFXDepthManager.setDepth(root, 1);
item.titleProperty().bind(skinnable.titleProperty());
item.subtitleProperty().bind(skinnable.subtitleProperty());
getChildren().setAll(root);
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.account;
@@ -31,14 +31,16 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import java.util.function.Consumer;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
public class AccountLoginPane extends StackPane {
private final Account oldAccount;
private final Consumer<AuthInfo> success;
private final Runnable failed;
@FXML
private Label lblUsername;
@FXML private Label lblUsername;
@FXML private JFXPasswordField txtPassword;
@FXML private Label lblCreationWarning;
@FXML private JFXProgressBar progressBar;
@@ -59,25 +61,20 @@ public class AccountLoginPane extends StackPane {
String password = txtPassword.getText();
progressBar.setVisible(true);
lblCreationWarning.setText("");
Task.ofResult("login", () -> {
try {
return oldAccount.logInWithPassword(password);
} catch (Exception e) {
return e;
}
}).subscribe(Schedulers.javafx(), variable -> {
Object account = variable.get("login");
if (account instanceof AuthInfo) {
success.accept(((AuthInfo) account));
fireEvent(new DialogCloseEvent());
} else if (account instanceof NoSelectedCharacterException) {
fireEvent(new DialogCloseEvent());
} else if (account instanceof Exception) {
lblCreationWarning.setText(AddAccountPane.accountException((Exception) account));
}
progressBar.setVisible(false);
});
Task.supplyAsync(() -> oldAccount.logInWithPassword(password))
.whenComplete(Schedulers.javafx(), authInfo -> {
success.accept(authInfo);
fireEvent(new DialogCloseEvent());
progressBar.setVisible(false);
}, e -> {
LOG.log(Level.INFO, "Failed to login with password: " + oldAccount, e);
if (e instanceof NoSelectedCharacterException) {
fireEvent(new DialogCloseEvent());
} else {
lblCreationWarning.setText(AddAccountPane.accountException(e));
}
progressBar.setVisible(false);
}).start();
}
@FXML
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,42 +13,48 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.account;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.utils.JFXUtilities;
import com.jfoenix.controls.*;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ListProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Level;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableList;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.*;
@@ -63,10 +69,13 @@ public class AddAccountPane extends StackPane {
@FXML private JFXComboBox<AccountFactory<?>> cboType;
@FXML private JFXComboBox<AuthlibInjectorServer> cboServers;
@FXML private Label lblInjectorServer;
@FXML private Hyperlink linkManageInjectorServers;
@FXML private JFXDialogLayout layout;
@FXML private JFXButton btnAccept;
@FXML private JFXButton btnAddServer;
@FXML private JFXButton btnManageServer;
@FXML private SpinnerPane acceptPane;
@FXML private HBox linksContainer;
private ListProperty<Hyperlink> links = new SimpleListProperty<>();;
public AddAccountPane() {
FXUtils.loadFXML(this, "/assets/fxml/account-add.fxml");
@@ -77,21 +86,31 @@ public class AddAccountPane extends StackPane {
cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer));
selectDefaultServer();
cboType.getItems().setAll(Accounts.FACTORY_OFFLINE, Accounts.FACTORY_YGGDRASIL, Accounts.FACTORY_AUTHLIB_INJECTOR);
cboType.setConverter(stringConverter(Accounts::getAccountTypeName));
cboType.getSelectionModel().select(0);
cboType.getItems().setAll(Accounts.FACTORY_OFFLINE, Accounts.FACTORY_MOJANG, Accounts.FACTORY_AUTHLIB_INJECTOR);
cboType.setConverter(stringConverter(Accounts::getLocalizedLoginTypeName));
// try selecting the preferred login type
cboType.getSelectionModel().select(
cboType.getItems().stream()
.filter(type -> Accounts.getLoginType(type).equals(config().getPreferredLoginType()))
.findFirst()
.orElse(Accounts.FACTORY_OFFLINE));
btnAddServer.visibleProperty().bind(cboServers.visibleProperty());
btnManageServer.visibleProperty().bind(cboServers.visibleProperty());
cboServers.getItems().addListener(onInvalidating(this::checkIfNoServer));
checkIfNoServer();
ReadOnlyObjectProperty<AccountFactory<?>> loginType = cboType.getSelectionModel().selectedItemProperty();
// remember the last used login type
loginType.addListener((observable, oldValue, newValue) -> config().setPreferredLoginType(Accounts.getLoginType(newValue)));
txtPassword.visibleProperty().bind(loginType.isNotEqualTo(Accounts.FACTORY_OFFLINE));
lblPassword.visibleProperty().bind(txtPassword.visibleProperty());
cboServers.visibleProperty().bind(loginType.isEqualTo(Accounts.FACTORY_AUTHLIB_INJECTOR));
lblInjectorServer.visibleProperty().bind(cboServers.visibleProperty());
linkManageInjectorServers.visibleProperty().bind(cboServers.visibleProperty());
txtUsername.getValidators().add(new Validator(i18n("input.email"), str -> !txtPassword.isVisible() || str.contains("@")));
@@ -105,6 +124,34 @@ public class AddAccountPane extends StackPane {
txtUsername.textProperty(),
txtPassword.textProperty(), txtPassword.visibleProperty(),
cboServers.getSelectionModel().selectedItemProperty(), cboServers.visibleProperty()));
// authlib-injector links
links.bind(BindingMapping.of(cboServers.getSelectionModel().selectedItemProperty())
.map(AddAccountPane::createHyperlinks)
.map(FXCollections::observableList));
Bindings.bindContent(linksContainer.getChildren(), links);
linksContainer.visibleProperty().bind(cboServers.visibleProperty());
}
private static final String[] ALLOWED_LINKS = { "register" };
public static List<Hyperlink> createHyperlinks(AuthlibInjectorServer server) {
if (server == null) {
return emptyList();
}
Map<String, String> links = server.getLinks();
List<Hyperlink> result = new ArrayList<>();
for (String key : ALLOWED_LINKS) {
String value = links.get(key);
if (value != null) {
Hyperlink link = new Hyperlink(i18n("account.injector.link." + key));
FXUtils.installSlowTooltip(link, value);
link.setOnAction(e -> FXUtils.openLink(value));
result.add(link);
}
}
return unmodifiableList(result);
}
/**
@@ -148,10 +195,8 @@ public class AddAccountPane extends StackPane {
AccountFactory<?> factory = cboType.getSelectionModel().getSelectedItem();
Object additionalData = getAuthAdditionalData();
Task.ofResult("create_account", () -> factory.create(new Selector(), username, password, additionalData))
.finalized(Schedulers.javafx(), variables -> {
Account account = variables.get("create_account");
Task.supplyAsync(() -> factory.create(new Selector(), username, password, additionalData))
.whenComplete(Schedulers.javafx(), account -> {
int oldIndex = Accounts.getAccounts().indexOf(account);
if (oldIndex == -1) {
Accounts.getAccounts().add(account);
@@ -189,14 +234,20 @@ public class AddAccountPane extends StackPane {
Controllers.navigate(Controllers.getServersPage());
}
@FXML
private void onAddInjecterServer() {
Controllers.dialog(new AddAuthlibInjectorServerPane());
}
private class Selector extends BorderPane implements CharacterSelector {
private final AdvancedListBox listBox = new AdvancedListBox();
private final JFXButton cancel = new JFXButton();
private final CountDownLatch latch = new CountDownLatch(1);
private GameProfile selectedProfile = null;
{
public Selector() {
setStyle("-fx-padding: 8px;");
cancel.setText(i18n("button.cancel"));
@@ -214,48 +265,33 @@ public class AddAccountPane extends StackPane {
}
@Override
public GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException {
if (!(account instanceof YggdrasilAccount))
return CharacterSelector.DEFAULT.select(account, names);
YggdrasilAccount yggdrasilAccount = (YggdrasilAccount) account;
public GameProfile select(YggdrasilService service, List<GameProfile> profiles) throws NoSelectedCharacterException {
Platform.runLater(() -> {
for (GameProfile profile : profiles) {
ImageView portraitView = new ImageView();
portraitView.setSmooth(false);
portraitView.imageProperty().bind(TexturesLoader.fxAvatarBinding(service, profile.getId(), 32));
FXUtils.limitSize(portraitView, 32, 32);
for (GameProfile profile : names) {
Image image;
try {
image = AccountHelper.getSkinImmediately(yggdrasilAccount, profile, 4);
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Failed to get skin for " + profile.getName(), e);
image = null;
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
accountItem.setOnMouseClicked(e -> {
selectedProfile = profile;
latch.countDown();
});
listBox.add(accountItem);
}
ImageView portraitView = new ImageView();
portraitView.setSmooth(false);
if (image == null) {
portraitView.setImage(Constants.DEFAULT_ICON.get());
} else {
portraitView.setImage(image);
portraitView.setViewport(AccountHelper.getViewport(4));
}
FXUtils.limitSize(portraitView, 32, 32);
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
accountItem.setOnMouseClicked(e -> {
selectedProfile = profile;
latch.countDown();
});
listBox.add(accountItem);
}
JFXUtilities.runInFX(() -> Controllers.dialog(this));
Controllers.dialog(this);
});
try {
latch.await();
if (selectedProfile == null)
throw new NoSelectedCharacterException(account);
throw new NoSelectedCharacterException();
return selectedProfile;
} catch (InterruptedException ignore) {
throw new NoSelectedCharacterException(account);
throw new NoSelectedCharacterException();
} finally {
JFXUtilities.runInFX(() -> Selector.this.fireEvent(new DialogCloseEvent()));
}
@@ -281,6 +317,12 @@ public class AddAccountPane extends StackPane {
return i18n("account.failed.invalid_password");
}
return exception.getMessage();
} else if (exception instanceof AuthlibInjectorDownloadException) {
return i18n("account.failed.injector_download_failure");
} else if (exception instanceof CharacterDeletedException) {
return i18n("account.failed.character_deleted");
} else if (exception.getClass() == AuthenticationException.class) {
return exception.getLocalizedMessage();
} else {
return exception.getClass().getName() + ": " + exception.getLocalizedMessage();
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,14 +13,14 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.account;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
@@ -29,17 +29,20 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
import org.jackhuang.hmcl.ui.construct.DialogAware;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.loadFXML;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AddAuthlibInjectorServerPane extends StackPane {
public class AddAuthlibInjectorServerPane extends StackPane implements DialogAware {
@FXML private StackPane addServerContainer;
@FXML private Label lblServerUrl;
@@ -67,16 +70,13 @@ public class AddAuthlibInjectorServerPane extends StackPane {
transitionHandler = new TransitionHandler(addServerContainer);
transitionHandler.setContent(addServerPane, ContainerAnimations.NONE.getAnimationProducer());
btnAddNext.disableProperty().bind(
Bindings.createBooleanBinding(txtServerUrl::validate, txtServerUrl.textProperty()).not());
btnAddNext.disableProperty().bind(txtServerUrl.textProperty().isEmpty());
nextPane.hideSpinner();
}
private String fixInputUrl(String url) {
if (!url.endsWith("/")) {
url += "/";
}
return url;
@Override
public void onDialogShown() {
txtServerUrl.requestFocus();
}
private String resolveFetchExceptionMessage(Throwable exception) {
@@ -99,18 +99,18 @@ public class AddAuthlibInjectorServerPane extends StackPane {
lblCreationWarning.setText("");
String url = fixInputUrl(txtServerUrl.getText());
String url = txtServerUrl.getText();
nextPane.showSpinner();
addServerPane.setDisable(true);
Task.of(() -> {
serverBeingAdded = AuthlibInjectorServer.fetchServerInfo(url);
}).finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
Task.runAsync(() -> {
serverBeingAdded = AuthlibInjectorServer.locateServer(url);
}).whenComplete(Schedulers.javafx(), exception -> {
addServerPane.setDisable(false);
nextPane.hideSpinner();
if (isDependentsSucceeded) {
if (exception == null) {
lblServerName.setText(serverBeingAdded.getName());
lblServerUrl.setText(serverBeingAdded.getUrl());
@@ -118,7 +118,8 @@ public class AddAuthlibInjectorServerPane extends StackPane {
transitionHandler.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT.getAnimationProducer());
} else {
lblCreationWarning.setText(resolveFetchExceptionMessage(variables.<Exception>get("lastException")));
LOG.log(Level.WARNING, "Failed to resolve auth server: " + url, exception);
lblCreationWarning.setText(resolveFetchExceptionMessage(exception));
}
}).start();
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,12 +13,14 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.account;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.binding.Bindings;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
@@ -33,17 +35,17 @@ public final class AuthlibInjectorServerItem extends BorderPane {
private final AuthlibInjectorServer server;
private final Label lblServerName = new Label();
private final Label lblServerIp = new Label();
private final Label lblServerUrl = new Label();
public AuthlibInjectorServerItem(AuthlibInjectorServer server, Consumer<AuthlibInjectorServerItem> deleteCallback) {
this.server = server;
lblServerName.setStyle("-fx-font-size: 15;");
lblServerIp.setStyle("-fx-font-size: 10;");
lblServerUrl.setStyle("-fx-font-size: 10;");
VBox center = new VBox();
BorderPane.setAlignment(center, Pos.CENTER);
center.getChildren().addAll(lblServerName, lblServerIp);
center.getChildren().addAll(lblServerName, lblServerUrl);
setCenter(center);
JFXButton right = new JFXButton();
@@ -55,8 +57,8 @@ public final class AuthlibInjectorServerItem extends BorderPane {
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
lblServerName.setText(server.getName());
lblServerIp.setText(server.getUrl());
lblServerName.textProperty().bind(Bindings.createStringBinding(server::getName, server));
lblServerUrl.setText(server.getUrl());
}
public AuthlibInjectorServer getServer() {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.account;
@@ -28,7 +28,7 @@ import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.MappedObservableList;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.loadFXML;
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.animation;
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.animation;
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright c 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.animation;
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.animation;
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,43 +13,90 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.*;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import javafx.scene.input.MouseEvent;
public class AdvancedListItem extends Control {
private final ObjectProperty<Image> image = new SimpleObjectProperty<>();
private final ObjectProperty<Rectangle2D> viewport = new SimpleObjectProperty<>();
private final StringProperty title = new SimpleStringProperty();
private final StringProperty subtitle = new SimpleStringProperty();
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
private final ObjectProperty<Node> rightGraphic = new SimpleObjectProperty<>(this, "rightGraphic");
private final StringProperty title = new SimpleStringProperty(this, "title");
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
private final BooleanProperty actionButtonVisible = new SimpleBooleanProperty(this, "actionButtonVisible", true);
public AdvancedListItem() {
addEventHandler(MouseEvent.MOUSE_CLICKED, e -> fireEvent(new ActionEvent()));
}
public Image getImage() {
return image.get();
}
public ObjectProperty<Image> imageProperty() {
return image;
}
public ObjectProperty<Rectangle2D> viewportProperty() {
return viewport;
public void setImage(Image image) {
this.image.set(image);
}
public Node getRightGraphic() {
return rightGraphic.get();
}
public ObjectProperty<Node> rightGraphicProperty() {
return rightGraphic;
}
public void setRightGraphic(Node rightGraphic) {
this.rightGraphic.set(rightGraphic);
}
public String getTitle() {
return title.get();
}
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public String getSubtitle() {
return subtitle.get();
}
public StringProperty subtitleProperty() {
return subtitle;
}
public void setSubtitle(String subtitle) {
this.subtitle.set(subtitle);
}
public boolean isActionButtonVisible() {
return actionButtonVisible.get();
}
public BooleanProperty actionButtonVisibleProperty() {
return actionButtonVisible;
}
public void setActionButtonVisible(boolean actionButtonVisible) {
this.actionButtonVisible.set(actionButtonVisible);
}
public final ObjectProperty<EventHandler<ActionEvent>> onActionProperty() {
return onAction;
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,11 +13,10 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
@@ -28,9 +27,7 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextAlignment;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
@@ -38,6 +35,7 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
super(skinnable);
StackPane stackPane = new StackPane();
RipplerContainer container = new RipplerContainer(stackPane);
BorderPane root = new BorderPane();
root.setPickOnBounds(false);
@@ -54,7 +52,6 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
FXUtils.limitSize(imageView, 32, 32);
imageView.setPreserveRatio(true);
imageView.imageProperty().bind(skinnable.imageProperty());
imageView.viewportProperty().bind(skinnable.viewportProperty());
imageViewContainer.getChildren().setAll(imageView);
VBox vbox = new VBox();
@@ -85,20 +82,27 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
HBox right = new HBox();
right.setAlignment(Pos.CENTER);
right.setPickOnBounds(false);
JFXButton settings = new JFXButton();
FXUtils.setLimitWidth(settings, 40);
settings.getStyleClass().setAll("toggle-icon4");
settings.setGraphic(SVG.dotsVertical(Theme.blackFillBinding(), -1, -1));
right.getChildren().setAll(settings);
right.setMouseTransparent(true);
right.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(right, 40);
FXUtils.onChangeAndOperate(skinnable.rightGraphicProperty(),
newGraphic -> {
if (newGraphic == null) {
right.getChildren().clear();
} else {
right.getChildren().setAll(newGraphic);
}
});
root.setRight(right);
FXUtils.onChangeAndOperate(skinnable.actionButtonVisibleProperty(),
visible -> root.setRight(visible ? right : null));
stackPane.setStyle("-fx-padding: 10 16 10 16;");
stackPane.getStyleClass().setAll("transparent");
stackPane.getStyleClass().add("transparent");
stackPane.setPickOnBounds(false);
stackPane.getChildren().setAll(root);
getChildren().setAll(stackPane);
getChildren().setAll(container);
}
}
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,26 +13,26 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import javafx.beans.DefaultProperty;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.util.MappedObservableList;
@DefaultProperty("content")
public class ComponentList extends Control {
@@ -100,23 +100,33 @@ public class ComponentList extends Control {
}
protected static class Skin extends SkinBase<ComponentList> {
private static final PseudoClass PSEUDO_CLASS_FIRST = PseudoClass.getPseudoClass("first");
private final ObservableList<Node> list;
private final ObjectBinding<Node> firstItem;
protected Skin(ComponentList control) {
super(control);
list = MappedObservableList.create(control.getContent(), this::mapper);
list = MappedObservableList.create(control.getContent(), node -> {
ComponentListCell cell = new ComponentListCell(node);
cell.getStyleClass().add("options-list-item");
return cell;
});
firstItem = Bindings.valueAt(list, 0);
firstItem.addListener((observable, oldValue, newValue) -> {
if (newValue != null)
newValue.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, true);
if (oldValue != null)
oldValue.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, false);
});
if (!list.isEmpty())
list.get(0).pseudoClassStateChanged(PSEUDO_CLASS_FIRST, true);
VBox vbox = new VBox();
Bindings.bindContent(vbox.getChildren(), list);
getChildren().setAll(vbox);
}
private Node mapper(Node node) {
StackPane child = new StackPane();
child.getChildren().add(new ComponentListCell(node));
child.getStyleClass().add("options-list-item");
return child;
}
}
}
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
@@ -24,6 +24,7 @@ import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
@@ -78,7 +79,6 @@ class ComponentListCell extends StackPane {
content.getStyleClass().add("options-sublist");
BorderPane groupNode = new BorderPane();
groupNode.getStyleClass().add("options-list-item-header");
Node expandIcon = SVG.expand(Theme.blackFillBinding(), 10, 10);
JFXButton expandButton = new JFXButton();
@@ -86,21 +86,25 @@ class ComponentListCell extends StackPane {
expandButton.getStyleClass().add("options-list-item-expand-button");
VBox labelVBox = new VBox();
labelVBox.setAlignment(Pos.CENTER_LEFT);
boolean overrideHeaderLeft = false;
if (list instanceof ComponentSublist) {
Node leftNode = ((ComponentSublist) list).getHeaderLeft();
if (leftNode != null)
if (leftNode != null) {
labelVBox.getChildren().setAll(leftNode);
} else {
overrideHeaderLeft = true;
}
}
if (!overrideHeaderLeft) {
Label label = new Label();
label.textProperty().bind(list.titleProperty());
label.setMouseTransparent(true);
labelVBox.getChildren().add(label);
if (list.isHasSubtitle()) {
Label subtitleLabel = new Label();
subtitleLabel.textProperty().bind(list.subtitleProperty());
subtitleLabel.setMouseTransparent(true);
subtitleLabel.getStyleClass().add("subtitle-label");
labelVBox.getChildren().add(subtitleLabel);
}
@@ -109,6 +113,8 @@ class ComponentListCell extends StackPane {
groupNode.setLeft(labelVBox);
HBox right = new HBox();
right.setSpacing(16);
right.setAlignment(Pos.CENTER_RIGHT);
if (list instanceof ComponentSublist) {
Node rightNode = ((ComponentSublist) list).getHeaderRight();
if (rightNode != null)
@@ -116,21 +122,13 @@ class ComponentListCell extends StackPane {
}
right.getChildren().add(expandButton);
groupNode.setRight(right);
labelVBox.setAlignment(Pos.CENTER_LEFT);
right.setAlignment(Pos.CENTER_RIGHT);
VBox container = new VBox();
container.setStyle("-fx-padding: 8 0 0 0;");
container.setPadding(new Insets(8, 0, 0, 0));
FXUtils.setLimitHeight(container, 0);
Rectangle clipRect = new Rectangle();
clipRect.widthProperty().bind(container.widthProperty());
clipRect.heightProperty().bind(container.heightProperty());
container.setClip(clipRect);
FXUtils.setOverflowHidden(container, true);
container.getChildren().setAll(content);
VBox holder = new VBox();
holder.getChildren().setAll(groupNode, container);
holder.getStyleClass().add("options-list-item-container");
groupNode.setBottom(container);
expandButton.setOnMouseClicked(e -> {
if (expandAnimation != null && expandAnimation.getStatus() == Animation.Status.RUNNING) {
@@ -162,7 +160,7 @@ class ComponentListCell extends StackPane {
expandedProperty().addListener((a, b, newValue) ->
expandIcon.setRotate(newValue ? 180 : 0));
getChildren().setAll(holder);
getChildren().setAll(groupNode);
} else
getChildren().setAll(content);
}
@@ -1,3 +1,20 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.DefaultProperty;
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,12 +13,19 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.task;
package org.jackhuang.hmcl.ui.construct;
import org.jackhuang.hmcl.util.AutoTypingMap;
import org.jackhuang.hmcl.ui.Controllers;
/**
* @author yushijinhun
* @see Controllers#dialog(javafx.scene.layout.Region)
*/
public interface DialogAware {
default void onDialogShown() {
}
public interface FinalizedCallback {
void execute(AutoTypingMap<String> variables, boolean isDependentsSucceeded) throws Exception;
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
@@ -61,7 +61,7 @@ public class FileItem extends BorderPane {
right.setGraphic(SVG.pencil(Theme.blackFillBinding(), 15, 15));
right.getStyleClass().add("toggle-icon4");
right.setOnMouseClicked(e -> onExplore());
FXUtils.installTooltip(right, i18n("button.edit"));
FXUtils.installFastTooltip(right, i18n("button.edit"));
setRight(right);
Tooltip tip = new Tooltip();
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,45 +13,51 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import static javafx.collections.FXCollections.emptyObservableList;
import static javafx.collections.FXCollections.observableList;
import static javafx.collections.FXCollections.singletonObservableList;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXListCell;
import javafx.beans.NamedArg;
import javafx.scene.control.ListCell;
import javafx.beans.binding.Bindings;
import javafx.scene.text.Font;
public class FontComboBox extends JFXComboBox<String> {
private boolean loaded = false;
public FontComboBox(@NamedArg(value = "fontSize", defaultValue = "12.0") double fontSize,
@NamedArg(value = "enableStyle", defaultValue = "false") boolean enableStyle) {
valueProperty().addListener((a, b, newValue) -> {
if (enableStyle)
setStyle("-fx-font-family: \"" + newValue + "\";");
});
public FontComboBox(@NamedArg(value = "fontSize", defaultValue = "12.0") double fontSize) {
styleProperty().bind(Bindings.concat("-fx-font-family: \"", valueProperty(), "\""));
setCellFactory(listView -> new ListCell<String>() {
setCellFactory(listView -> new JFXListCell<String>() {
@Override
protected void updateItem(String item, boolean empty) {
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (item != null) {
if (!empty) {
setText(item);
setFont(new Font(item, fontSize));
setGraphic(null);
setStyle("-fx-font-family: \"" + item + "\"");
}
}
});
itemsProperty().bind(BindingMapping.of(valueProperty())
.map(value -> value == null ? emptyObservableList() : singletonObservableList(value)));
setOnMouseClicked(e -> {
if (loaded) return;
getItems().setAll(Font.getFamilies());
if (loaded)
return;
itemsProperty().unbind();
setItems(observableList(Font.getFamilies()));
loaded = true;
});
}
public void initValue(Font font) {
getItems().setAll(font.getFamily());
getSelectionModel().select(font.getFamily());
}
}
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,11 +13,10 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
@@ -39,12 +38,17 @@ public class IconedItem extends RipplerContainer {
private static HBox createHBox(Node icon) {
HBox hBox = new HBox();
hBox.getStyleClass().setAll("iconed-item-container");
icon.setMouseTransparent(true);
if (icon != null) {
icon.setMouseTransparent(true);
hBox.getChildren().add(icon);
}
hBox.getStyleClass().add("iconed-item-container");
Label textLabel = new Label();
textLabel.setId("label");
textLabel.setMouseTransparent(true);
hBox.getChildren().addAll(icon, textLabel);
hBox.getChildren().addAll(textLabel);
return hBox;
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,11 +13,12 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.scene.Node;
import org.jackhuang.hmcl.ui.FXUtils;
public class IconedMenuItem extends IconedItem {
@@ -27,4 +28,9 @@ public class IconedMenuItem extends IconedItem {
getStyleClass().setAll("iconed-menu-item");
setOnMouseClicked(e -> action.run());
}
public IconedMenuItem addTooltip(String tooltip) {
FXUtils.installFastTooltip(this, tooltip);
return this;
}
}
@@ -1,3 +1,20 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
@@ -46,7 +63,7 @@ public final class ImagePickerItem extends BorderPane {
deleteButton.onMouseClickedProperty().bind(onDeleteButtonClicked);
deleteButton.getStyleClass().add("toggle-icon4");
FXUtils.installTooltip(selectButton, i18n("button.edit"));
FXUtils.installFastTooltip(selectButton, i18n("button.edit"));
HBox hBox = new HBox();
hBox.getChildren().setAll(imageView, selectButton, deleteButton);
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
@@ -38,15 +38,20 @@ public class InputDialogPane extends StackPane {
private Label content;
@FXML
private Label lblCreationWarning;
@FXML
private SpinnerPane acceptPane;
public InputDialogPane(String text, FutureCallback<String> onResult) {
FXUtils.loadFXML(this, "/assets/fxml/input-dialog.fxml");
content.setText(text);
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
acceptButton.setOnMouseClicked(e -> {
acceptPane.showSpinner();
onResult.call(textField.getText(), () -> {
acceptPane.hideSpinner();
fireEvent(new DialogCloseEvent());
}, msg -> {
acceptPane.hideSpinner();
lblCreationWarning.setText(msg);
});
});
@@ -0,0 +1,133 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXCheckBox;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TreeTableCell;
import javafx.util.Callback;
import javafx.util.StringConverter;
public class JFXCheckBoxTreeTableCell<S,T> extends TreeTableCell<S,T> {
private final CheckBox checkBox;
private boolean showLabel;
private ObservableValue<Boolean> booleanProperty;
public JFXCheckBoxTreeTableCell() {
this(null, null);
}
public JFXCheckBoxTreeTableCell(
final Callback<Integer, ObservableValue<Boolean>> getSelectedProperty) {
this(getSelectedProperty, null);
}
public JFXCheckBoxTreeTableCell(
final Callback<Integer, ObservableValue<Boolean>> getSelectedProperty,
final StringConverter<T> converter) {
this.getStyleClass().add("check-box-tree-table-cell");
this.checkBox = new JFXCheckBox();
setGraphic(null);
setSelectedStateCallback(getSelectedProperty);
setConverter(converter);
}
private ObjectProperty<StringConverter<T>> converter =
new SimpleObjectProperty<StringConverter<T>>(this, "converter") {
protected void invalidated() {
updateShowLabel();
}
};
public final ObjectProperty<StringConverter<T>> converterProperty() {
return converter;
}
public final void setConverter(StringConverter<T> value) {
converterProperty().set(value);
}
public final StringConverter<T> getConverter() {
return converterProperty().get();
}
private ObjectProperty<Callback<Integer, ObservableValue<Boolean>>>
selectedStateCallback =
new SimpleObjectProperty<Callback<Integer, ObservableValue<Boolean>>>(
this, "selectedStateCallback");
public final ObjectProperty<Callback<Integer, ObservableValue<Boolean>>> selectedStateCallbackProperty() {
return selectedStateCallback;
}
public final void setSelectedStateCallback(Callback<Integer, ObservableValue<Boolean>> value) {
selectedStateCallbackProperty().set(value);
}
public final Callback<Integer, ObservableValue<Boolean>> getSelectedStateCallback() {
return selectedStateCallbackProperty().get();
}
@SuppressWarnings("unchecked")
@Override public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
StringConverter<T> c = getConverter();
if (showLabel) {
setText(c.toString(item));
}
setGraphic(checkBox);
if (booleanProperty instanceof BooleanProperty) {
checkBox.selectedProperty().unbindBidirectional((BooleanProperty)booleanProperty);
}
ObservableValue<?> obsValue = getSelectedProperty();
if (obsValue instanceof BooleanProperty) {
booleanProperty = (ObservableValue<Boolean>) obsValue;
checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);
}
checkBox.disableProperty().bind(Bindings.not(
getTreeTableView().editableProperty().and(
getTableColumn().editableProperty()).and(
editableProperty())
));
}
}
private void updateShowLabel() {
this.showLabel = converter != null;
this.checkBox.setAlignment(showLabel ? Pos.CENTER_LEFT : Pos.CENTER);
}
private ObservableValue<?> getSelectedProperty() {
return getSelectedStateCallback() != null ?
getSelectedStateCallback().call(getIndex()) :
getTableColumn().getCellObservableValue(getIndex());
}
}
@@ -0,0 +1,36 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.geometry.Insets;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
public class MenuSeparator extends StackPane {
public MenuSeparator() {
Rectangle rect = new Rectangle();
rect.widthProperty().bind(widthProperty().add(-14));
rect.setHeight(1);
rect.setFill(Color.GRAY);
maxHeightProperty().set(10);
setPadding(new Insets(3));
getChildren().setAll(rect);
}
}
@@ -1,172 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.TextInputDialog;
import org.jackhuang.hmcl.ui.FXUtils;
import javax.swing.*;
import java.util.Optional;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MessageBox {
private MessageBox() {
}
private static final String TITLE = i18n("message.info");
/**
* User Operation: Yes
*/
public static final int YES_OPTION = 0;
/**
* User Operation: No
*/
public static final int NO_OPTION = 1;
/**
* User Operation: Cancel
*/
public static final int CANCEL_OPTION = 2;
/**
* User Operation: OK
*/
public static final int OK_OPTION = 0;
/**
* User Operation: Closed Message Box
*/
public static final int CLOSED_OPTION = -1;
/**
* Buttons: Yes No
*/
public static final int YES_NO_OPTION = 10;
/**
* Buttons: Yes No Cancel
*/
public static final int YES_NO_CANCEL_OPTION = 11;
/**
* Buttons: OK Cancel
*/
public static final int OK_CANCEL_OPTION = 12;
/**
* Message Box Type: Error
*/
public static final int ERROR_MESSAGE = 0;
/**
* Message Box Type: Info
*/
public static final int INFORMATION_MESSAGE = 1;
/**
* Message Box Type: Warning
*/
public static final int WARNING_MESSAGE = 2;
/**
* Message Box Type: Question
*/
public static final int QUESTION_MESSAGE = 3;
/**
* Message Box Type: Fine
*/
public static final int FINE_MESSAGE = 4;
/**
* Message Box Type: Plain
*/
public static final int PLAIN_MESSAGE = -1;
public static void show(String message) {
show(message, TITLE);
}
/**
* Show MsgBox with title and options
*
* @param message The Message
* @param title The title of MsgBox.
* @return user operation.
*/
public static Optional<ButtonType> show(String message, String title) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle(title);
alert.setHeaderText(title);
alert.setContentText(message);
return alert.showAndWait();
}
public static int confirm(String message, String title) {
return confirm(message, title, -1);
}
public static int confirm(String message, int option) {
return confirm(message, TITLE, option);
}
public static int confirm(String message, String title, int option) {
return FXUtils.runInUIThread(() -> {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle(title);
alert.setHeaderText(title);
alert.setContentText(message);
switch (option) {
case YES_NO_OPTION:
alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO);
break;
case YES_NO_CANCEL_OPTION:
alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO, ButtonType.CANCEL);
break;
case OK_CANCEL_OPTION:
alert.getButtonTypes().setAll(ButtonType.OK, ButtonType.CANCEL);
break;
default:
throw new IllegalArgumentException("Unrecognized message box option " + option);
}
Optional<ButtonType> buttonType = alert.showAndWait();
if (!buttonType.isPresent()) return CLOSED_OPTION;
else if (buttonType.get() == ButtonType.OK) return OK_OPTION;
else if (buttonType.get() == ButtonType.YES) return YES_OPTION;
else if (buttonType.get() == ButtonType.NO) return NO_OPTION;
else if (buttonType.get() == ButtonType.CANCEL) return CANCEL_OPTION;
else throw new IllegalStateException("Unexpected button type:" + buttonType.get());
});
}
public static Optional<String> input(String message) {
return input(message, UIManager.getString("OptionPane.inputDialogTitle"));
}
public static Optional<String> input(String message, String title) {
return input(message, title, "");
}
public static Optional<String> input(String message, String title, String initialValue) {
TextInputDialog dialog = new TextInputDialog(initialValue);
dialog.setTitle(title);
dialog.setHeaderText(message);
dialog.setContentText(message);
return dialog.showAndWait();
}
}
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun4008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
@@ -32,6 +32,15 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MessageDialogPane extends StackPane {
public static enum MessageType {
ERROR,
INFORMATION,
WARNING,
QUESTION,
FINE,
PLAIN,
}
@FXML
private JFXButton acceptButton;
@FXML
@@ -45,7 +54,7 @@ public final class MessageDialogPane extends StackPane {
@FXML
private HBox actions;
public MessageDialogPane(String text, String title, int type, Runnable onAccept) {
public MessageDialogPane(String text, String title, MessageType type, Runnable onAccept) {
FXUtils.loadFXML(this, "/assets/fxml/message-dialog.fxml");
if (title != null)
@@ -60,19 +69,19 @@ public final class MessageDialogPane extends StackPane {
actions.getChildren().remove(cancelButton);
switch (type) {
case MessageBox.INFORMATION_MESSAGE:
case INFORMATION:
graphic.setGraphic(SVG.infoCircle(Theme.blackFillBinding(), 40, 40));
break;
case MessageBox.ERROR_MESSAGE:
case ERROR:
graphic.setGraphic(SVG.closeCircle(Theme.blackFillBinding(), 40, 40));
break;
case MessageBox.FINE_MESSAGE:
case FINE:
graphic.setGraphic(SVG.checkCircle(Theme.blackFillBinding(), 40, 40));
break;
case MessageBox.WARNING_MESSAGE:
case WARNING:
graphic.setGraphic(SVG.alert(Theme.blackFillBinding(), 40, 40));
break;
case MessageBox.QUESTION_MESSAGE:
case QUESTION:
graphic.setGraphic(SVG.helpCircle(Theme.blackFillBinding(), 40, 40));
break;
default:
@@ -81,7 +90,7 @@ public final class MessageDialogPane extends StackPane {
}
public MessageDialogPane(String text, String title, Runnable onAccept, Runnable onCancel) {
this(text, title, MessageBox.QUESTION_MESSAGE, onAccept);
this(text, title, MessageType.QUESTION, onAccept);
cancelButton.setVisible(true);
cancelButton.setOnMouseClicked(e -> {
@@ -1,3 +1,20 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXColorPicker;
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
@@ -24,6 +24,7 @@ import javafx.beans.NamedArg;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
@@ -138,7 +139,7 @@ public class MultiFileItem<T> extends ComponentList {
public Node createChildren(String title, String subtitle, T userData) {
BorderPane pane = new BorderPane();
pane.setStyle("-fx-padding: 3;");
pane.setPadding(new Insets(3));
FXUtils.setLimitHeight(pane, 20);
JFXRadioButton left = new JFXRadioButton(title);
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,12 +13,11 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.Event;
import javafx.event.EventHandler;
@@ -29,33 +28,51 @@ import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
import org.jackhuang.hmcl.util.Logging;
import java.util.Optional;
import java.util.Stack;
import java.util.logging.Level;
public class Navigator extends StackPane {
private static final String PROPERTY_DIALOG_CLOSE_HANDLER = Navigator.class.getName() + ".closeListener";
private final Stack<Node> stack = new Stack<>();
private final TransitionHandler animationHandler = new TransitionHandler(this);
private final ReadOnlyBooleanWrapper canGoBack = new ReadOnlyBooleanWrapper();
private boolean initialized = false;
public Navigator(Node init) {
public void init(Node init) {
stack.push(init);
getChildren().setAll(init);
fireEvent(new NavigationEvent(this, init, NavigationEvent.NAVIGATED));
initialized = true;
}
public void navigate(Node node) {
FXUtils.checkFxUserThread();
if (!initialized)
throw new IllegalStateException("Navigator must have a root page");
Node from = stack.peek();
if (from == node)
return;
Logging.LOG.info("Navigate to " + node);
stack.push(node);
fireEvent(new NavigationEvent(this, from, NavigationEvent.NAVIGATING));
NavigationEvent navigating = new NavigationEvent(this, from, NavigationEvent.NAVIGATING);
fireEvent(navigating);
node.fireEvent(navigating);
setContent(node);
fireEvent(new NavigationEvent(this, node, NavigationEvent.NAVIGATED));
NavigationEvent navigated = new NavigationEvent(this, node, NavigationEvent.NAVIGATED);
fireEvent(navigated);
node.fireEvent(navigated);
EventHandler<PageCloseEvent> handler = event -> close(node);
node.getProperties().put(PROPERTY_DIALOG_CLOSE_HANDLER, handler);
@@ -66,15 +83,38 @@ public class Navigator extends StackPane {
close(stack.peek());
}
public void clear() {
while (stack.size() > 1)
close(stack.peek());
}
@SuppressWarnings("unchecked")
public void close(Node from) {
FXUtils.checkFxUserThread();
stack.remove(from);
if (!initialized)
throw new IllegalStateException("Navigator must have a root page");
if (stack.peek() != from) {
// Allow page to be closed multiple times.
Logging.LOG.log(Level.INFO, "Closing already closed page: " + from, new Throwable());
return;
}
Logging.LOG.info("Closed page " + from);
stack.pop();
Node node = stack.peek();
fireEvent(new NavigationEvent(this, from, NavigationEvent.NAVIGATING));
NavigationEvent navigating = new NavigationEvent(this, from, NavigationEvent.NAVIGATING);
fireEvent(navigating);
node.fireEvent(navigating);
setContent(node);
fireEvent(new NavigationEvent(this, node, NavigationEvent.NAVIGATED));
NavigationEvent navigated = new NavigationEvent(this, node, NavigationEvent.NAVIGATED);
fireEvent(navigated);
node.fireEvent(navigated);
Optional.ofNullable(from.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER))
.ifPresent(handler -> from.removeEventHandler(PageCloseEvent.CLOSE, (EventHandler<PageCloseEvent>) handler));
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
@@ -0,0 +1,94 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.When;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
public class PopupMenu extends Control {
private final ObservableList<Node> content = FXCollections.observableArrayList();
private final BooleanProperty alwaysShowingVBar = new SimpleBooleanProperty();
public PopupMenu() {
}
public ObservableList<Node> getContent() {
return content;
}
public boolean isAlwaysShowingVBar() {
return alwaysShowingVBar.get();
}
public BooleanProperty alwaysShowingVBarProperty() {
return alwaysShowingVBar;
}
public void setAlwaysShowingVBar(boolean alwaysShowingVBar) {
this.alwaysShowingVBar.set(alwaysShowingVBar);
}
@Override
protected Skin<?> createDefaultSkin() {
return new PopupMenuSkin();
}
public static Node wrapPopupMenuItem(Node node) {
StackPane pane = new StackPane();
pane.getChildren().setAll(node);
pane.getStyleClass().add("menu-container");
node.setMouseTransparent(true);
return new RipplerContainer(pane);
}
private class PopupMenuSkin extends SkinBase<PopupMenu> {
protected PopupMenuSkin() {
super(PopupMenu.this);
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToHeight(true);
scrollPane.setFitToWidth(true);
scrollPane.vbarPolicyProperty().bind(new When(alwaysShowingVBar)
.then(ScrollPane.ScrollBarPolicy.ALWAYS)
.otherwise(ScrollPane.ScrollBarPolicy.AS_NEEDED));
VBox content = new VBox();
content.getStyleClass().add("menu");
Bindings.bindContent(content.getChildren(), PopupMenu.this.getContent());
scrollPane.setContent(content);
FXUtils.smoothScrolling(scrollPane);
getChildren().setAll(scrollPane);
}
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,12 +13,11 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXRippler;
import javafx.animation.Transition;
import javafx.beans.DefaultProperty;
import javafx.beans.InvalidationListener;
import javafx.beans.NamedArg;
@@ -27,7 +26,12 @@ import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.*;
import javafx.css.CssMetaData;
import javafx.css.PseudoClass;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.Styleable;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleablePropertyFactory;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.layout.Background;
@@ -39,8 +43,6 @@ import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle;
import org.jackhuang.hmcl.util.Lang;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@DefaultProperty("container")
@@ -61,68 +63,28 @@ public class RipplerContainer extends StackPane {
mask.resize(buttonContainer.getWidth() - buttonContainer.snappedRightInset() - buttonContainer.snappedLeftInset(), buttonContainer.getHeight() - buttonContainer.snappedBottomInset() - buttonContainer.snappedTopInset());
return mask;
}
@Override
protected void initListeners() {
this.ripplerPane.setOnMousePressed(event -> {
if (releaseManualRippler != null)
releaseManualRippler.run();
releaseManualRippler = null;
createRipple(event.getX(), event.getY());
});
}
};
private Transition clickedAnimation;
private final CornerRadii defaultRadii = new CornerRadii(3);
private Runnable releaseManualRippler;
public RipplerContainer(@NamedArg("container") Node container) {
setContainer(container);
getStyleClass().add(DEFAULT_STYLE_CLASS);
buttonContainer.getChildren().add(buttonRippler);
setOnMousePressed(event -> {
if (clickedAnimation != null) {
clickedAnimation.setRate(1);
clickedAnimation.play();
}
});
setOnMouseReleased(event -> {
if (clickedAnimation != null) {
clickedAnimation.setRate(-1);
clickedAnimation.play();
}
});
focusedProperty().addListener((a, b, newValue) -> {
if (newValue) {
if (!isPressed())
buttonRippler.showOverlay();
buttonRippler.setOverlayVisible(true);
} else {
buttonRippler.hideOverlay();
buttonRippler.setOverlayVisible(false);
}
});
pressedProperty().addListener(o -> buttonRippler.hideOverlay());
pressedProperty().addListener(o -> buttonRippler.setOverlayVisible(false));
setPickOnBounds(false);
buttonContainer.setPickOnBounds(false);
buttonContainer.shapeProperty().bind(shapeProperty());
buttonContainer.borderProperty().bind(borderProperty());
buttonContainer.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
if (getBackground() == null || isJavaDefaultBackground(getBackground()) || isJavaDefaultClickedBackground(getBackground()))
setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, defaultRadii, null)));
try {
return new Background(new BackgroundFill(getBackground() != null ? getBackground().getFills().get(0).getFill() : Color.TRANSPARENT,
getBackground() != null ? getBackground().getFills().get(0).getRadii() : defaultRadii, Insets.EMPTY));
} catch (Exception e) {
return getBackground();
}
}, backgroundProperty()));
ripplerFillProperty().addListener((a, b, newValue) -> buttonRippler.setRipplerFill(newValue));
if (getBackground() == null || isJavaDefaultBackground(getBackground()))
setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, defaultRadii, null)));
buttonRippler.ripplerFillProperty().bind(ripplerFillProperty());
containerProperty().addListener(o -> updateChildren());
updateChildren();
@@ -150,24 +112,6 @@ public class RipplerContainer extends StackPane {
getChildren().get(i).setPickOnBounds(false);
}
private boolean isJavaDefaultBackground(Background background) {
try {
String firstFill = background.getFills().get(0).getFill().toString();
return "0xffffffba".equals(firstFill) || "0xffffffbf".equals(firstFill) || "0xffffffbd".equals(firstFill);
} catch (Exception e) {
return false;
}
}
private boolean isJavaDefaultClickedBackground(Background background) {
try {
String firstFill = background.getFills().get(0).getFill().toString();
return "0x039ed3ff".equals(firstFill);
} catch (Exception e) {
return false;
}
}
public Node getContainer() {
return container.get();
}
@@ -210,32 +154,12 @@ public class RipplerContainer extends StackPane {
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
return StyleableProperties.FACTORY.getCssMetaData();
}
private static class StyleableProperties {
private static final StyleablePropertyFactory<RipplerContainer> FACTORY = new StyleablePropertyFactory<>(StackPane.getClassCssMetaData());
private static final CssMetaData<RipplerContainer, Paint> RIPPLER_FILL = new CssMetaData<RipplerContainer, Paint>("-jfx-rippler-fill", StyleConverter.getPaintConverter(), Color.rgb(0, 200, 255)) {
@Override
public boolean isSettable(RipplerContainer control) {
return control.ripplerFill == null || !control.ripplerFill.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(RipplerContainer control) {
return control.ripplerFillProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
private StyleableProperties() {
}
static {
List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Node.getClassCssMetaData());
Collections.addAll(styleables, RIPPLER_FILL);
STYLEABLES = Collections.unmodifiableList(styleables);
}
private static final CssMetaData<RipplerContainer, Paint> RIPPLER_FILL = FACTORY.createPaintCssMetaData("-jfx-rippler-fill", s -> s.ripplerFill, Color.rgb(0, 200, 255));
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,13 +13,15 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXSpinner;
import javafx.beans.DefaultProperty;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
@@ -32,19 +34,29 @@ public class SpinnerPane extends StackPane {
private final JFXSpinner spinner = new JFXSpinner();
private final StackPane contentPane = new StackPane();
private final ObjectProperty<Node> content = new SimpleObjectProperty<>(this, "content");
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading") {
protected void invalidated() {
if (get())
transitionHandler.setContent(spinner, ContainerAnimations.FADE.getAnimationProducer());
else
transitionHandler.setContent(contentPane, ContainerAnimations.FADE.getAnimationProducer());
}
};
public SpinnerPane() {
getStyleClass().add("spinner-pane");
getChildren().setAll(contentPane);
content.addListener((a, b, newValue) -> contentPane.getChildren().setAll(newValue));
}
public void showSpinner() {
transitionHandler.setContent(spinner, ContainerAnimations.FADE.getAnimationProducer());
setLoading(true);
}
public void hideSpinner() {
transitionHandler.setContent(contentPane, ContainerAnimations.FADE.getAnimationProducer());
setLoading(false);
}
public Node getContent() {
@@ -58,4 +70,16 @@ public class SpinnerPane extends StackPane {
public void setContent(Node content) {
this.content.set(content);
}
public boolean isLoading() {
return loading.get();
}
public BooleanProperty loadingProperty() {
return loading;
}
public void setLoading(boolean loading) {
this.loading.set(loading);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,13 +13,14 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.utils.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXProgressBar;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
@@ -27,6 +28,7 @@ import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.ui.FXUtils;
import java.util.Optional;
@@ -66,10 +68,23 @@ public class TaskExecutorDialogPane extends StackPane {
}
public void setExecutor(TaskExecutor executor) {
setExecutor(executor, true);
}
public void setExecutor(TaskExecutor executor, boolean autoClose) {
this.executor = executor;
if (executor != null)
if (executor != null) {
taskListPane.setExecutor(executor);
if (autoClose)
executor.addTaskListener(new TaskListener() {
@Override
public void onStop(boolean success, TaskExecutor executor) {
Platform.runLater(() -> fireEvent(new DialogCloseEvent()));
}
});
}
}
public StringProperty titleProperty() {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,7 +13,7 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
@@ -24,7 +24,6 @@ import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.download.forge.ForgeInstallTask;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
@@ -43,7 +42,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class TaskListPane extends StackPane {
private final AdvancedListBox listBox = new AdvancedListBox();
private final Map<Task, ProgressListNode> nodes = new HashMap<>();
private final Map<Task<?>, ProgressListNode> nodes = new HashMap<>();
private final ReadOnlyIntegerWrapper finishedTasks = new ReadOnlyIntegerWrapper();
private final ReadOnlyIntegerWrapper totTasks = new ReadOnlyIntegerWrapper();
@@ -73,12 +72,12 @@ public final class TaskListPane extends StackPane {
}
@Override
public void onReady(Task task) {
public void onReady(Task<?> task) {
Platform.runLater(() -> totTasks.set(totTasks.getValue() + 1));
}
@Override
public void onRunning(Task task) {
public void onRunning(Task<?> task) {
if (!task.getSignificance().shouldShow())
return;
@@ -94,6 +93,8 @@ public final class TaskListPane extends StackPane {
task.setName(i18n("modpack.type.curse.completion"));
} else if (task instanceof ModpackInstallTask) {
task.setName(i18n("modpack.installing"));
} else if (task instanceof ModpackUpdateTask) {
task.setName(i18n("modpack.update"));
} else if (task instanceof CurseInstallTask) {
task.setName(i18n("modpack.install", i18n("modpack.type.curse")));
} else if (task instanceof MultiMCModpackInstallTask) {
@@ -109,11 +110,10 @@ public final class TaskListPane extends StackPane {
ProgressListNode node = new ProgressListNode(task);
nodes.put(task, node);
Platform.runLater(() -> listBox.add(node));
}
@Override
public void onFinished(Task task) {
public void onFinished(Task<?> task) {
ProgressListNode node = nodes.remove(task);
if (node == null)
return;
@@ -125,7 +125,7 @@ public final class TaskListPane extends StackPane {
}
@Override
public void onFailed(Task task, Throwable throwable) {
public void onFailed(Task<?> task, Throwable throwable) {
ProgressListNode node = nodes.remove(task);
if (node == null)
return;
@@ -137,20 +137,19 @@ public final class TaskListPane extends StackPane {
});
}
private static class ProgressListNode extends VBox {
private static class ProgressListNode extends BorderPane {
private final JFXProgressBar bar = new JFXProgressBar();
private final Label title = new Label();
private final Label state = new Label();
public ProgressListNode(Task task) {
public ProgressListNode(Task<?> task) {
bar.progressProperty().bind(task.progressProperty());
title.setText(task.getName());
state.textProperty().bind(task.messageProperty());
BorderPane borderPane = new BorderPane();
borderPane.setLeft(title);
borderPane.setRight(state);
getChildren().addAll(borderPane, bar);
setLeft(title);
setRight(state);
setBottom(bar);
bar.minWidthProperty().bind(widthProperty());
bar.prefWidthProperty().bind(widthProperty());
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,36 +13,21 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.css.*;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.text.Font;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class TwoLineListItem extends StackPane {
public class TwoLineListItem extends VBox {
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
private final StringProperty title = new SimpleStringProperty(this, "title");
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
private final StyleableObjectProperty<Font> titleFont = new SimpleStyleableObjectProperty<>(StyleableProperties.TITLE_FONT, this, "title-font", Font.font(15));
private final StyleableObjectProperty<Font> subtitleFont = new SimpleStyleableObjectProperty<>(StyleableProperties.SUBTITLE_FONT, this, "subtitle-font", Font.getDefault());
private final StyleableObjectProperty<Paint> titleFill = new SimpleStyleableObjectProperty<>(StyleableProperties.TITLE_FILL, this, "title-fill", Color.BLACK);
private final StyleableObjectProperty<Paint> subtitleFill = new SimpleStyleableObjectProperty<>(StyleableProperties.SUBTITLE_FILL, this, "subtitle-fill", Color.GRAY);
public TwoLineListItem(String titleString, String subtitleString) {
this();
@@ -51,19 +36,17 @@ public class TwoLineListItem extends StackPane {
}
public TwoLineListItem() {
setMouseTransparent(true);
Label lblTitle = new Label();
lblTitle.textFillProperty().bind(titleFill);
lblTitle.fontProperty().bind(titleFont);
lblTitle.getStyleClass().add("title");
lblTitle.textProperty().bind(title);
Label lblSubtitle = new Label();
lblSubtitle.textFillProperty().bind(subtitleFill);
lblSubtitle.fontProperty().bind(subtitleFont);
lblSubtitle.getStyleClass().add("subtitle");
lblSubtitle.textProperty().bind(subtitle);
VBox vbox = new VBox();
vbox.getChildren().setAll(lblTitle, lblSubtitle);
getChildren().setAll(vbox);
getChildren().setAll(lblTitle, lblSubtitle);
getStyleClass().add(DEFAULT_STYLE_CLASS);
}
public String getTitle() {
@@ -90,128 +73,8 @@ public class TwoLineListItem extends StackPane {
this.subtitle.set(subtitle);
}
public Font getTitleFont() {
return titleFont.get();
}
public StyleableObjectProperty<Font> titleFontProperty() {
return titleFont;
}
public void setTitleFont(Font titleFont) {
this.titleFont.set(titleFont);
}
public Font getSubtitleFont() {
return subtitleFont.get();
}
public StyleableObjectProperty<Font> subtitleFontProperty() {
return subtitleFont;
}
public void setSubtitleFont(Font subtitleFont) {
this.subtitleFont.set(subtitleFont);
}
public Paint getTitleFill() {
return titleFill.get();
}
public StyleableObjectProperty<Paint> titleFillProperty() {
return titleFill;
}
public void setTitleFill(Paint titleFill) {
this.titleFill.set(titleFill);
}
public Paint getSubtitleFill() {
return subtitleFill.get();
}
public StyleableObjectProperty<Paint> subtitleFillProperty() {
return subtitleFill;
}
public void setSubtitleFill(Paint subtitleFill) {
this.subtitleFill.set(subtitleFill);
}
@Override
public String toString() {
return getTitle();
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}
private static class StyleableProperties {
private static final FontCssMetaData<TwoLineListItem> TITLE_FONT = new FontCssMetaData<TwoLineListItem>("-jfx-title-font", Font.font(15)) {
@Override
public boolean isSettable(TwoLineListItem control) {
return control.title == null || !control.title.isBound();
}
@Override
public StyleableProperty<Font> getStyleableProperty(TwoLineListItem control) {
return control.titleFontProperty();
}
};
private static final FontCssMetaData<TwoLineListItem> SUBTITLE_FONT = new FontCssMetaData<TwoLineListItem>("-jfx-subtitle-font", Font.getDefault()) {
@Override
public boolean isSettable(TwoLineListItem control) {
return control.subtitle == null || !control.subtitle.isBound();
}
@Override
public StyleableProperty<Font> getStyleableProperty(TwoLineListItem control) {
return control.subtitleFontProperty();
}
};
private static final CssMetaData<TwoLineListItem, Paint> TITLE_FILL = new CssMetaData<TwoLineListItem, Paint>("-jfx-title-fill", StyleConverter.getPaintConverter(), Color.BLACK) {
@Override
public boolean isSettable(TwoLineListItem control) {
return control.titleFill == null || !control.titleFill.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(TwoLineListItem control) {
return control.titleFillProperty();
}
};
private static final CssMetaData<TwoLineListItem, Paint> SUBTITLE_FILL = new CssMetaData<TwoLineListItem, Paint>("-jfx-subtitle-fill", StyleConverter.getPaintConverter(), Color.GRAY) {
@Override
public boolean isSettable(TwoLineListItem control) {
return control.subtitleFill == null || !control.subtitleFill.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(TwoLineListItem control) {
return control.subtitleFillProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
private StyleableProperties() {
}
static {
List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Node.getClassCssMetaData());
Collections.addAll(styleables, TITLE_FONT);
Collections.addAll(styleables, SUBTITLE_FONT);
STYLEABLES = Collections.unmodifiableList(styleables);
}
}
}
@@ -1,37 +0,0 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.TextInputControl;
import java.net.MalformedURLException;
import java.net.URL;
public class URLValidator extends ValidatorBase {
private final ObservableList<String> protocols = FXCollections.observableArrayList();
public URLValidator() {
super();
}
public ObservableList<String> getProtocols() {
return protocols;
}
@Override
protected void eval() {
if (srcControl.get() instanceof TextInputControl) {
try {
URL url = new URL(((TextInputControl) srcControl.get()).getText());
if (protocols.isEmpty())
hasErrors.set(false);
else
hasErrors.set(!protocols.contains(url.getProtocol()));
} catch (MalformedURLException e) {
hasErrors.set(true);
}
}
}
}
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* Hello Minecraft! Launcher
* Copyright (C) 2019 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
@@ -13,16 +13,41 @@
* 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 {http://www.gnu.org/licenses/}.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.scene.control.TextInputControl;
import java.util.function.Consumer;
import java.util.function.Predicate;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
public final class Validator extends ValidatorBase {
public static Consumer<Predicate<String>> addTo(JFXTextField control) {
return addTo(control, null);
}
/**
* @see SafeStringConverter#asPredicate(Consumer)
*/
public static Consumer<Predicate<String>> addTo(JFXTextField control, String message) {
return predicate -> {
Validator validator = new Validator(message, predicate);
InvalidationListener listener = any -> control.validate();
validator.getProperties().put(validator, listener);
control.textProperty().addListener(new WeakInvalidationListener(listener));
control.getValidators().add(validator);
};
}
private final Predicate<String> validator;
/**
Loaded 100 of 481 files, more files were not shown because too many files have changed in this diff. Show more