diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.kt b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.kt index 3a92a045b..bd9b58b91 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.kt +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.kt @@ -19,41 +19,44 @@ package org.jackhuang.hmcl.setting import com.google.gson.* import javafx.beans.InvalidationListener -import javafx.beans.property.* import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider import org.jackhuang.hmcl.download.DefaultDependencyManager -import org.jackhuang.hmcl.download.DependencyManager import org.jackhuang.hmcl.game.HMCLGameRepository +import org.jackhuang.hmcl.mod.ModManager import org.jackhuang.hmcl.util.* +import org.jackhuang.hmcl.util.property.ImmediateBooleanProperty +import org.jackhuang.hmcl.util.property.ImmediateObjectProperty +import org.jackhuang.hmcl.util.property.ImmediateStringProperty import java.io.File import java.lang.reflect.Type -class Profile(var name: String = "Default", gameDir: File = File(".minecraft")) { - val globalProperty = SimpleObjectProperty(this, "global", VersionSetting()) +class Profile(var name: String = "Default", initialGameDir: File = File(".minecraft"), initialSelectedVersion: String = "") { + val globalProperty = ImmediateObjectProperty(this, "global", VersionSetting()) var global: VersionSetting by globalProperty - val selectedVersionProperty = SimpleStringProperty(this, "selectedVersion", "") + val selectedVersionProperty = ImmediateStringProperty(this, "selectedVersion", initialSelectedVersion) var selectedVersion: String by selectedVersionProperty - val gameDirProperty = SimpleObjectProperty(this, "gameDir", gameDir) + val gameDirProperty = ImmediateObjectProperty(this, "gameDir", initialGameDir) var gameDir: File by gameDirProperty - val noCommonProperty = SimpleBooleanProperty(this, "noCommon", false) + val noCommonProperty = ImmediateBooleanProperty(this, "noCommon", false) var noCommon: Boolean by noCommonProperty - var repository = HMCLGameRepository(gameDir) + var repository = HMCLGameRepository(initialGameDir) var dependency = DefaultDependencyManager(repository, BMCLAPIDownloadProvider) + var modManager = ModManager(repository) init { - gameDirProperty.addListener { _, _, newValue -> - repository.baseDirectory = newValue + gameDirProperty.addListener { _ -> + repository.baseDirectory = gameDir repository.refreshVersions() } - selectedVersionProperty.addListener { _, _, newValue -> - if (newValue.isNotBlank() && !repository.hasVersion(newValue)) { + selectedVersionProperty.addListener { _ -> + if (selectedVersion.isNotBlank() && !repository.hasVersion(selectedVersion)) { val newVersion = repository.getVersions().firstOrNull() - // will cause anthor change event, we must insure that there will not be dead recursion. + // will cause anthor change event, we must ensure that there will not be dead recursion. selectedVersion = newVersion?.id ?: "" } } @@ -110,9 +113,8 @@ class Profile(var name: String = "Default", gameDir: File = File(".minecraft")) override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext): Profile? { if (json == null || json == JsonNull.INSTANCE || json !is JsonObject) return null - return Profile(gameDir = File(json["gameDir"]?.asString ?: "")).apply { + return Profile(initialGameDir = File(json["gameDir"]?.asString ?: ""), initialSelectedVersion = json["selectedVersion"]?.asString ?: "").apply { global = context.deserialize(json["global"], VersionSetting::class.java) - selectedVersion = json["selectedVersion"]?.asString ?: "" noCommon = json["noCommon"]?.asBoolean ?: false } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.kt b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.kt index 2cc07e93d..2b05398b6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.kt +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.kt @@ -24,23 +24,20 @@ import org.jackhuang.hmcl.MainApplication import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider import org.jackhuang.hmcl.download.DownloadProvider import org.jackhuang.hmcl.download.MojangDownloadProvider -import org.jackhuang.hmcl.util.GSON import org.jackhuang.hmcl.util.LOG import java.io.File import java.util.logging.Level import org.jackhuang.hmcl.ProfileLoadingEvent import org.jackhuang.hmcl.ProfileChangedEvent import org.jackhuang.hmcl.auth.Account -import org.jackhuang.hmcl.auth.Accounts +import org.jackhuang.hmcl.util.* import org.jackhuang.hmcl.auth.OfflineAccount import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount import org.jackhuang.hmcl.event.EVENT_BUS -import org.jackhuang.hmcl.util.FileTypeAdapter -import org.jackhuang.hmcl.util.ignoreException +import org.jackhuang.hmcl.util.property.ImmediateObjectProperty import java.net.Proxy import java.util.* - object Settings { val GSON = GsonBuilder() .registerTypeAdapter(VersionSetting::class.java, VersionSetting) @@ -53,7 +50,7 @@ object Settings { val SETTINGS_FILE = File("hmcl.json").absoluteFile - val SETTINGS: Config + private val SETTINGS: Config private val ACCOUNTS = mutableMapOf() @@ -82,10 +79,10 @@ object Settings { save() - if (!getProfiles().containsKey(DEFAULT_PROFILE)) - getProfiles().put(DEFAULT_PROFILE, Profile()); + if (!getProfileMap().containsKey(DEFAULT_PROFILE)) + getProfileMap().put(DEFAULT_PROFILE, Profile()); - for ((name, profile) in getProfiles().entries) { + for ((name, profile) in getProfileMap().entries) { profile.name = name profile.addPropertyChangedListener(InvalidationListener { save() }) } @@ -151,23 +148,35 @@ object Settings { return getProfile(SETTINGS.selectedProfile) } - val selectedAccount: Account? - get() { - val a = getAccount(SETTINGS.selectedAccount) - if (a == null && ACCOUNTS.isNotEmpty()) { - val (key, acc) = ACCOUNTS.entries.first() - SETTINGS.selectedAccount = key + val selectedAccountProperty = object : ImmediateObjectProperty(this, "selectedAccount", getAccount(SETTINGS.selectedAccount)) { + override fun get(): Account? { + val a = super.get() + if (a == null || !ACCOUNTS.containsKey(a.username)) { + val acc = if (ACCOUNTS.isEmpty()) null else ACCOUNTS.values.first() + set(acc) return acc - } - return a + } else return a } - fun setSelectedAccount(name: String) { - if (ACCOUNTS.containsKey(name)) - SETTINGS.selectedAccount = name + override fun set(newValue: Account?) { + if (newValue == null || ACCOUNTS.containsKey(newValue.username)) { + super.set(newValue) + } + } + + override fun invalidated() { + super.invalidated() + + SETTINGS.selectedAccount = value?.username ?: "" + } } + var selectedAccount: Account? by selectedAccountProperty val PROXY: Proxy = Proxy.NO_PROXY + val PROXY_HOST: String? get() = SETTINGS.proxyHost + val PROXY_PORT: String? get() = SETTINGS.proxyPort + val PROXY_USER: String? get() = SETTINGS.proxyUserName + val PROXY_PASS: String? get() = SETTINGS.proxyPassword fun addAccount(account: Account) { ACCOUNTS[account.username] = account @@ -183,36 +192,38 @@ object Settings { fun deleteAccount(name: String) { ACCOUNTS.remove(name) + + selectedAccountProperty.get() } fun getProfile(name: String?): Profile { - var p: Profile? = getProfiles()[name ?: DEFAULT_PROFILE] + var p: Profile? = getProfileMap()[name ?: DEFAULT_PROFILE] if (p == null) - if (getProfiles().containsKey(DEFAULT_PROFILE)) - p = getProfiles()[DEFAULT_PROFILE]!! + if (getProfileMap().containsKey(DEFAULT_PROFILE)) + p = getProfileMap()[DEFAULT_PROFILE]!! else { p = Profile() - getProfiles().put(DEFAULT_PROFILE, p) + getProfileMap().put(DEFAULT_PROFILE, p) } return p } fun hasProfile(name: String?): Boolean { - return getProfiles().containsKey(name ?: DEFAULT_PROFILE) + return getProfileMap().containsKey(name ?: DEFAULT_PROFILE) } - fun getProfiles(): MutableMap { + fun getProfileMap(): MutableMap { return SETTINGS.configurations } - fun getProfilesFiltered(): Collection { - return getProfiles().values.filter { t -> t.name.isNotBlank() } + fun getProfiles(): Collection { + return getProfileMap().values.filter { t -> t.name.isNotBlank() } } fun putProfile(ver: Profile?): Boolean { - if (ver == null || ver.name.isBlank() || getProfiles().containsKey(ver.name)) + if (ver == null || ver.name.isBlank() || getProfileMap().containsKey(ver.name)) return false - getProfiles().put(ver.name, ver) + getProfileMap().put(ver.name, ver) return true } @@ -227,15 +238,15 @@ object Settings { var notify = false if (selectedProfile.name == ver) notify = true - val flag = getProfiles().remove(ver) != null + val flag = getProfileMap().remove(ver) != null if (notify && flag) onProfileChanged() return flag } internal fun onProfileChanged() { - EVENT_BUS.fireEvent(ProfileChangedEvent(SETTINGS, selectedProfile)) selectedProfile.repository.refreshVersions() + EVENT_BUS.fireEvent(ProfileChangedEvent(SETTINGS, selectedProfile)) } /** diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.kt b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.kt index 80cb61124..d97be135a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.kt +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.kt @@ -19,10 +19,10 @@ package org.jackhuang.hmcl.setting import com.google.gson.* import javafx.beans.InvalidationListener -import javafx.beans.property.* import org.jackhuang.hmcl.MainApplication import org.jackhuang.hmcl.game.LaunchOptions import org.jackhuang.hmcl.util.* +import org.jackhuang.hmcl.util.property.* import java.io.File import java.io.IOException import java.lang.reflect.Type @@ -39,7 +39,7 @@ class VersionSetting() { * * Defaults false because if one version uses global first, custom version file will not be generated. */ - val usesGlobalProperty = SimpleBooleanProperty(this, "usesGlobal", false) + val usesGlobalProperty = ImmediateBooleanProperty(this, "usesGlobal", false) var usesGlobal: Boolean by usesGlobalProperty // java @@ -47,39 +47,39 @@ class VersionSetting() { /** * Java version or null if user customizes java directory. */ - val javaProperty = SimpleStringProperty(this, "java", null) + val javaProperty = ImmediateNullableStringProperty(this, "java", null) var java: String? by javaProperty /** * User customized java directory or null if user uses system Java. */ - val javaDirProperty = SimpleStringProperty(this, "javaDir", "") + val javaDirProperty = ImmediateStringProperty(this, "javaDir", "") var javaDir: String by javaDirProperty /** * The command to launch java, i.e. optirun. */ - val wrapperProperty = SimpleStringProperty(this, "wrapper", "") + val wrapperProperty = ImmediateStringProperty(this, "wrapper", "") var wrapper: String by wrapperProperty /** * The permanent generation size of JVM garbage collection. */ - val permSizeProperty = SimpleStringProperty(this, "permSize", "") + val permSizeProperty = ImmediateStringProperty(this, "permSize", "") var permSize: String by permSizeProperty /** * The maximum memory that JVM can allocate. * The size of JVM heap. */ - val maxMemoryProperty = SimpleIntegerProperty(this, "maxMemory", OS.SUGGESTED_MEMORY) + val maxMemoryProperty = ImmediateIntegerProperty(this, "maxMemory", OS.SUGGESTED_MEMORY) var maxMemory: Int by maxMemoryProperty /** * The command that will be executed before launching the Minecraft. * Operating system relevant. */ - val precalledCommandProperty = SimpleStringProperty(this, "precalledCommand", "") + val precalledCommandProperty = ImmediateStringProperty(this, "precalledCommand", "") var precalledCommand: String by precalledCommandProperty // options @@ -87,25 +87,25 @@ class VersionSetting() { /** * The user customized arguments passed to JVM. */ - val javaArgsProperty = SimpleStringProperty(this, "javaArgs", "") + val javaArgsProperty = ImmediateStringProperty(this, "javaArgs", "") var javaArgs: String by javaArgsProperty /** * The user customized arguments passed to Minecraft. */ - val minecraftArgsProperty = SimpleStringProperty(this, "minecraftArgs", "") + val minecraftArgsProperty = ImmediateStringProperty(this, "minecraftArgs", "") var minecraftArgs: String by minecraftArgsProperty /** * True if disallow HMCL use default JVM arguments. */ - val noJVMArgsProperty = SimpleBooleanProperty(this, "noJVMArgs", false) + val noJVMArgsProperty = ImmediateBooleanProperty(this, "noJVMArgs", false) var noJVMArgs: Boolean by noJVMArgsProperty /** * True if HMCL does not check game's completeness. */ - val notCheckGameProperty = SimpleBooleanProperty(this, "notCheckGame", false) + val notCheckGameProperty = ImmediateBooleanProperty(this, "notCheckGame", false) var notCheckGame: Boolean by notCheckGameProperty // Minecraft settings. @@ -115,13 +115,13 @@ class VersionSetting() { * * Format: ip:port or without port. */ - val serverIpProperty = SimpleStringProperty(this, "serverIp", "") + val serverIpProperty = ImmediateStringProperty(this, "serverIp", "") var serverIp: String by serverIpProperty /** * True if Minecraft started in fullscreen mode. */ - val fullscreenProperty = SimpleBooleanProperty(this, "fullscreen", false) + val fullscreenProperty = ImmediateBooleanProperty(this, "fullscreen", false) var fullscreen: Boolean by fullscreenProperty /** @@ -131,7 +131,7 @@ class VersionSetting() { * String type prevents unexpected value from causing JsonSyntaxException. * We can only reset this field instead of recreating the whole setting file. */ - val widthProperty = SimpleIntegerProperty(this, "width", 854) + val widthProperty = ImmediateIntegerProperty(this, "width", 854) var width: Int by widthProperty @@ -142,7 +142,7 @@ class VersionSetting() { * String type prevents unexpected value from causing JsonSyntaxException. * We can only reset this field instead of recreating the whole setting file. */ - val heightProperty = SimpleIntegerProperty(this, "height", 480) + val heightProperty = ImmediateIntegerProperty(this, "height", 480) var height: Int by heightProperty @@ -150,7 +150,7 @@ class VersionSetting() { * 0 - .minecraft
* 1 - .minecraft/versions/<version>/
*/ - val gameDirTypeProperty = SimpleObjectProperty(this, "gameDirTypeProperty", EnumGameDirectory.ROOT_FOLDER) + val gameDirTypeProperty = ImmediateObjectProperty(this, "gameDirTypeProperty", EnumGameDirectory.ROOT_FOLDER) var gameDirType: EnumGameDirectory by gameDirTypeProperty // launcher settings @@ -160,7 +160,7 @@ class VersionSetting() { * 1 - Hide the launcher when the game starts.
* 2 - Keep the launcher open.
*/ - val launcherVisibilityProperty = SimpleObjectProperty(this, "launcherVisibility", LauncherVisibility.HIDE) + val launcherVisibilityProperty = ImmediateObjectProperty(this, "launcherVisibility", LauncherVisibility.HIDE) var launcherVisibility: LauncherVisibility by launcherVisibilityProperty fun addPropertyChangedListener(listener: InvalidationListener) { @@ -200,10 +200,10 @@ class VersionSetting() { fullscreen = fullscreen, serverIp = serverIp, wrapper = wrapper, - proxyHost = Settings.SETTINGS.proxyHost, - proxyPort = Settings.SETTINGS.proxyPort, - proxyUser = Settings.SETTINGS.proxyUserName, - proxyPass = Settings.SETTINGS.proxyPassword, + proxyHost = Settings.PROXY_HOST, + proxyPort = Settings.PROXY_PORT, + proxyUser = Settings.PROXY_USER, + proxyPass = Settings.PROXY_PASS, precalledCommand = precalledCommand, noGeneratedJVMArgs = noJVMArgs ) @@ -217,7 +217,7 @@ class VersionSetting() { addProperty("usesGlobal", src.usesGlobal) addProperty("javaArgs", src.javaArgs) addProperty("minecraftArgs", src.minecraftArgs) - addProperty("maxMemory", src.maxMemory) + addProperty("maxMemory", if (src.maxMemory <= 0) OS.SUGGESTED_MEMORY else src.maxMemory) addProperty("permSize", src.permSize) addProperty("width", src.width) addProperty("height", src.height) @@ -239,11 +239,14 @@ class VersionSetting() { override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): VersionSetting? { if (json == null || json == JsonNull.INSTANCE || json !is JsonObject) return null + var maxMemoryN = parseJsonPrimitive(json["maxMemory"]?.asJsonPrimitive, OS.SUGGESTED_MEMORY) + if (maxMemoryN <= 0) maxMemoryN = OS.SUGGESTED_MEMORY + return VersionSetting().apply { usesGlobal = json["usesGlobal"]?.asBoolean ?: false javaArgs = json["javaArgs"]?.asString ?: "" minecraftArgs = json["minecraftArgs"]?.asString ?: "" - maxMemory = parseJsonPrimitive(json["maxMemory"]?.asJsonPrimitive) + maxMemory = maxMemoryN permSize = json["permSize"]?.asString ?: "" width = parseJsonPrimitive(json["width"]?.asJsonPrimitive) height = parseJsonPrimitive(json["height"]?.asJsonPrimitive) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountItem.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountItem.kt index d5d0a56c1..9b7d5db81 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountItem.kt +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountItem.kt @@ -18,38 +18,36 @@ package org.jackhuang.hmcl.ui import com.jfoenix.controls.JFXButton +import com.jfoenix.controls.JFXCheckBox +import com.jfoenix.controls.JFXRadioButton import com.jfoenix.effects.JFXDepthManager import javafx.beans.binding.Bindings import javafx.fxml.FXML import javafx.scene.control.Label +import javafx.scene.control.ToggleGroup import javafx.scene.layout.Pane import javafx.scene.layout.StackPane import javafx.scene.layout.VBox import java.util.concurrent.Callable -class AccountItem(i: Int, width: Double, height: Double) : StackPane() { +class AccountItem(i: Int, width: Double, height: Double, group: ToggleGroup) : StackPane() { @FXML lateinit var icon: Pane @FXML lateinit var content: VBox @FXML lateinit var header: StackPane @FXML lateinit var body: StackPane @FXML lateinit var btnDelete: JFXButton - @FXML lateinit var btnEdit: JFXButton @FXML lateinit var lblUser: Label + @FXML lateinit var chkSelected: JFXRadioButton @FXML lateinit var lblType: Label init { loadFXML("/assets/fxml/account-item.fxml") - minWidth = width - maxWidth = width - prefWidth = width - - minHeight = height - maxHeight = height - prefHeight = height - JFXDepthManager.setDepth(this, 1) + chkSelected.toggleGroup = group + btnDelete.graphic = SVG.delete("white", 15.0, 15.0) + // create content val headerColor = getDefaultColor(i % 12) header.style = "-fx-background-radius: 5 5 0 0; -fx-background-color: " + headerColor diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountsPage.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountsPage.kt index 8a3f35270..9bacc35f7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountsPage.kt +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountsPage.kt @@ -25,18 +25,19 @@ import javafx.scene.control.ScrollPane import javafx.scene.layout.StackPane import javafx.beans.property.SimpleStringProperty import javafx.beans.property.StringProperty +import javafx.beans.value.ChangeListener import javafx.scene.control.Label +import javafx.scene.control.ToggleGroup import org.jackhuang.hmcl.auth.Account import org.jackhuang.hmcl.auth.OfflineAccount import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount import org.jackhuang.hmcl.setting.Settings import org.jackhuang.hmcl.task.Scheduler -import org.jackhuang.hmcl.task.with import org.jackhuang.hmcl.task.Task -import org.jackhuang.hmcl.ui.wizard.HasTitle +import org.jackhuang.hmcl.ui.wizard.DecoratorPage import java.util.concurrent.Callable -class AccountsPage : StackPane(), HasTitle { +class AccountsPage() : StackPane(), DecoratorPage { override val titleProperty: StringProperty = SimpleStringProperty(this, "title", "Accounts") @FXML lateinit var scrollPane: ScrollPane @@ -48,12 +49,30 @@ class AccountsPage : StackPane(), HasTitle { @FXML lateinit var lblCreationWarning: Label @FXML lateinit var cboType: JFXComboBox + val listener = ChangeListener { _, _, newValue -> + masonryPane.children.forEach { + if (it is AccountItem) { + it.chkSelected.isSelected = newValue?.username == it.lblUser.text + } + } + } + init { loadFXML("/assets/fxml/account.fxml") children.remove(dialog) dialog.dialogContainer = this - JFXScrollPane.smoothScrolling(scrollPane) + scrollPane.smoothScrolling() + + txtUsername.textProperty().addListener { _ -> + txtUsername.validate() + } + txtUsername.validate() + + txtPassword.textProperty().addListener { _ -> + txtPassword.validate() + } + txtPassword.validate() cboType.selectionModel.selectedIndexProperty().addListener { _, _, newValue -> val visible = newValue != 0 @@ -62,27 +81,42 @@ class AccountsPage : StackPane(), HasTitle { } cboType.selectionModel.select(0) + Settings.selectedAccountProperty.addListener(listener) + loadAccounts() + + if (Settings.getAccounts().isEmpty()) + addNewAccount() + } + + override fun onClose() { + Settings.selectedAccountProperty.removeListener(listener) } fun loadAccounts() { val children = mutableListOf() var i = 0 + val group = ToggleGroup() for ((_, account) in Settings.getAccounts()) { - children += buildNode(++i, account) + children += buildNode(++i, account, group) + } + group.selectedToggleProperty().addListener { _, _, newValue -> + if (newValue != null) + Settings.selectedAccount = newValue.properties["account"] as Account } masonryPane.children.setAll(children) - Platform.runLater { scrollPane.requestLayout() } + Platform.runLater { + masonryPane.requestLayout() + scrollPane.requestLayout() + } } - private fun buildNode(i: Int, account: Account): Node { - return AccountItem(i, Math.random() * 100 + 100, Math.random() * 100 + 100).apply { + private fun buildNode(i: Int, account: Account, group: ToggleGroup): Node { + return AccountItem(i, Math.random() * 100 + 100, Math.random() * 100 + 100, group).apply { + chkSelected.properties["account"] = account + chkSelected.isSelected = Settings.selectedAccount == account lblUser.text = account.username - lblType.text = when(account) { - is OfflineAccount -> "Offline Account" - is YggdrasilAccount -> "Yggdrasil Account" - else -> throw Error("Unsupported account: $account") - } + lblType.text = accountType(account) btnDelete.setOnMouseClicked { Settings.deleteAccount(account.username) Platform.runLater(this@AccountsPage::loadAccounts) @@ -90,28 +124,6 @@ class AccountsPage : StackPane(), HasTitle { } } - private fun getDefaultColor(i: Int): String { - var color = "#FFFFFF" - when (i) { - 0 -> color = "#8F3F7E" - 1 -> color = "#B5305F" - 2 -> color = "#CE584A" - 3 -> color = "#DB8D5C" - 4 -> color = "#DA854E" - 5 -> color = "#E9AB44" - 6 -> color = "#FEE435" - 7 -> color = "#99C286" - 8 -> color = "#01A05E" - 9 -> color = "#4A8895" - 10 -> color = "#16669B" - 11 -> color = "#2F65A5" - 12 -> color = "#4E6A9C" - else -> { - } - } - return color - } - fun addNewAccount() { dialog.show() } @@ -149,4 +161,11 @@ class AccountsPage : StackPane(), HasTitle { fun onCreationCancel() { dialog.close() } -} \ No newline at end of file +} + +fun accountType(account: Account) = + when(account) { + is OfflineAccount -> "Offline Account" + is YggdrasilAccount -> "Yggdrasil Account" + else -> throw Error("Unsupported account: $account") + } \ No newline at end of file diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.kt index 277ebbf03..c84acdee4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.kt +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.kt @@ -38,16 +38,10 @@ object Controllers { fun initialize(stage: Stage) { this.stage = stage - decorator = Decorator(stage, max = false) - decorator.mainPage = mainPane + decorator = Decorator(stage, mainPane, max = false) decorator.showPage(null) leftPaneController = LeftPaneController(decorator.leftPane) - // Let root pane fix window size. - with(mainPane.parent as StackPane) { - mainPane.prefWidthProperty().bind(widthProperty()) - mainPane.prefHeightProperty().bind(heightProperty()) - } decorator.isCustomMaximize = false scene = Scene(decorator, 800.0, 480.0) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Decorator.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Decorator.kt index b8b52f708..fbb2d47d6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Decorator.kt +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Decorator.kt @@ -37,12 +37,9 @@ import javafx.scene.control.Tooltip import javafx.scene.input.MouseEvent import javafx.scene.layout.* import javafx.scene.paint.Color -import javafx.scene.shape.Rectangle import javafx.stage.Screen import javafx.stage.Stage import javafx.stage.StageStyle -import javafx.scene.layout.BorderStrokeStyle -import javafx.scene.layout.BorderStroke import org.jackhuang.hmcl.MainApplication import org.jackhuang.hmcl.ui.animation.AnimationProducer import org.jackhuang.hmcl.ui.animation.ContainerAnimations @@ -52,7 +49,7 @@ import org.jackhuang.hmcl.util.* import java.util.* import java.util.concurrent.ConcurrentLinkedQueue -class Decorator @JvmOverloads constructor(private val primaryStage: Stage, private val max: Boolean = true, min: Boolean = true) : GridPane(), AbstractWizardDisplayer { +class Decorator @JvmOverloads constructor(private val primaryStage: Stage, private val mainPage: Node, private val max: Boolean = true, min: Boolean = true) : GridPane(), AbstractWizardDisplayer { override val wizardController: WizardController = WizardController(this) private var xOffset: Double = 0.0 @@ -370,16 +367,28 @@ class Decorator @JvmOverloads constructor(private val primaryStage: Stage, priva if (content is WizardPage) titleLabel.text = prefix + content.title - if (content is HasTitle) + if (content is DecoratorPage) titleLabel.textProperty().bind(content.titleProperty) } - lateinit var mainPage: Node var category: String? = null + var nowPage: Node? = null fun showPage(content: Node?) { + val c = content ?: mainPage onEnd() - setContent(content ?: mainPage, ContainerAnimations.FADE.animationProducer) + val nowPage = nowPage + if (nowPage is DecoratorPage) + nowPage.onClose() + this.nowPage = content + setContent(c, ContainerAnimations.FADE.animationProducer) + + if (c is Region) + // Let root pane fix window size. + with(c.parent as StackPane) { + c.prefWidthProperty().bind(widthProperty()) + c.prefHeightProperty().bind(heightProperty()) + } } fun startWizard(wizardProvider: WizardProvider, category: String? = null) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.kt index a83d2d9e5..d675542b3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.kt +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.kt @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.ui import com.jfoenix.concurrency.JFXUtilities +import com.jfoenix.controls.JFXScrollPane import javafx.animation.Animation import javafx.animation.KeyFrame import javafx.animation.Timeline @@ -29,10 +30,10 @@ import javafx.scene.Parent import javafx.scene.Scene import javafx.scene.control.ListView import javafx.scene.control.ScrollBar +import javafx.scene.control.ScrollPane import javafx.scene.image.WritableImage import javafx.scene.input.MouseEvent import javafx.scene.input.ScrollEvent -import javafx.scene.layout.Pane import javafx.scene.layout.Region import javafx.scene.shape.Rectangle import javafx.util.Duration @@ -90,6 +91,8 @@ fun ListView<*>.smoothScrolling() { } } +fun ScrollPane.smoothScrolling() = JFXScrollPane.smoothScrolling(this) + fun runOnUiThread(runnable: () -> Unit) = { JFXUtilities.runInFX(runnable) } @@ -109,6 +112,5 @@ fun setOverflowHidden(node: Region) { val stylesheets = arrayOf( Controllers::class.java.getResource("/css/jfoenix-fonts.css").toExternalForm(), -Controllers::class.java.getResource("/css/jfoenix-design.css").toExternalForm(), -//Controllers::class.java.getResource("/assets/css/jfoenix-components.css").toExternalForm(), - Controllers::class.java.getResource("/assets/css/jfoenix-main-demo.css").toExternalForm()) \ No newline at end of file + Controllers::class.java.getResource("/css/jfoenix-design.css").toExternalForm(), + Controllers::class.java.getResource("/assets/css/jfoenix-main-demo.css").toExternalForm()) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.kt index e4117b1df..ef5f3a59b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.kt +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.kt @@ -18,11 +18,14 @@ package org.jackhuang.hmcl.ui import com.jfoenix.controls.JFXComboBox +import javafx.beans.property.StringProperty +import javafx.beans.value.ChangeListener import javafx.scene.Node import javafx.scene.layout.* import javafx.scene.paint.Paint import org.jackhuang.hmcl.ProfileChangedEvent import org.jackhuang.hmcl.ProfileLoadingEvent +import org.jackhuang.hmcl.auth.Account import org.jackhuang.hmcl.event.EVENT_BUS import org.jackhuang.hmcl.event.RefreshedVersionsEvent import org.jackhuang.hmcl.game.LauncherHelper @@ -62,6 +65,21 @@ class LeftPaneController(val leftPane: VBox) { Settings.selectedProfile.repository.refreshVersions() } Controllers.mainPane.buttonLaunch.setOnMouseClicked { LauncherHelper.launch() } + + val listener = ChangeListener { _, _, newValue -> + if (newValue == null) { + accountItem.lblVersionName.text = "mojang@mojang.com" + accountItem.lblGameVersion.text = "Yggdrasil" + } else { + accountItem.lblVersionName.text = newValue.username + accountItem.lblGameVersion.text = accountType(newValue) + } + } + Settings.selectedAccountProperty.addListener(listener) + listener.changed(null, null, Settings.selectedAccount) + + if (Settings.getAccounts().isEmpty()) + Controllers.navigate(AccountsPage()) } private fun addChildren(content: Node) { @@ -81,13 +99,10 @@ class LeftPaneController(val leftPane: VBox) { fun onProfileChanged(event: ProfileChangedEvent) { val profile = event.value - profile.selectedVersionProperty.addListener { _, _, newValue -> - versionChanged(newValue) + profile.selectedVersionProperty.addListener { observable -> + versionChanged(profile.selectedVersion) } - } - - private fun loadAccounts() { - + profile.selectedVersionProperty.fireValueChangedEvent() } private fun loadVersions() { @@ -98,7 +113,7 @@ class LeftPaneController(val leftPane: VBox) { val ripplerContainer = RipplerContainer(item) item.onSettingsButtonClicked { Controllers.decorator.showPage(Controllers.versionPane) - Controllers.versionPane.loadVersionSetting(item.versionName, profile.getVersionSetting(item.versionName)) + Controllers.versionPane.load(item.versionName, profile) } ripplerContainer.ripplerFill = Paint.valueOf("#89E1F9") ripplerContainer.setOnMouseClicked { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.kt index d4b2c11fb..a91148f63 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.kt +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.kt @@ -18,34 +18,16 @@ package org.jackhuang.hmcl.ui import com.jfoenix.controls.JFXButton -import com.jfoenix.controls.JFXComboBox -import com.jfoenix.controls.JFXListCell -import com.jfoenix.controls.JFXListView import javafx.beans.property.SimpleStringProperty import javafx.beans.property.StringProperty -import javafx.collections.FXCollections import javafx.fxml.FXML -import javafx.scene.Node -import javafx.scene.layout.BorderPane -import javafx.scene.layout.Pane import javafx.scene.layout.StackPane -import org.jackhuang.hmcl.ProfileChangedEvent -import org.jackhuang.hmcl.ProfileLoadingEvent -import org.jackhuang.hmcl.event.EVENT_BUS -import org.jackhuang.hmcl.event.RefreshedVersionsEvent -import org.jackhuang.hmcl.game.minecraftVersion -import org.jackhuang.hmcl.setting.Settings -import org.jackhuang.hmcl.setting.VersionSetting -import org.jackhuang.hmcl.ui.animation.ContainerAnimations -import org.jackhuang.hmcl.ui.download.DownloadWizardProvider -import org.jackhuang.hmcl.ui.animation.TransitionHandler -import org.jackhuang.hmcl.ui.wizard.HasTitle -import org.jackhuang.hmcl.ui.wizard.Wizard +import org.jackhuang.hmcl.ui.wizard.DecoratorPage /** * @see /assets/fxml/main.fxml */ -class MainPage : BorderPane(), HasTitle { +class MainPage : StackPane(), DecoratorPage { override val titleProperty: StringProperty = SimpleStringProperty(this, "title", "Main Page") @FXML lateinit var buttonLaunch: JFXButton diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ModController.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ModController.kt new file mode 100644 index 000000000..012c037f8 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ModController.kt @@ -0,0 +1,51 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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.fxml.FXML +import javafx.scene.Node +import javafx.scene.control.ScrollPane +import javafx.scene.layout.VBox +import org.jackhuang.hmcl.mod.ModManager +import org.jackhuang.hmcl.task.Scheduler +import org.jackhuang.hmcl.task.Task +import java.util.concurrent.Callable + +class ModController { + @FXML lateinit var scrollPane: ScrollPane + @FXML lateinit var rootPane: VBox + lateinit var modManager: ModManager + lateinit var versionId: String + + fun initialize() { + scrollPane.smoothScrolling() + } + + fun loadMods() { + Task.of(Callable { + modManager.refreshMods(versionId) + }).subscribe(Scheduler.JAVAFX) { + rootPane.children.clear() + for (modInfo in modManager.getMods(versionId)) { + rootPane.children += ModItem(modInfo) { + modManager.removeMods(versionId, modInfo) + } + } + } + } +} \ No newline at end of file diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ModItem.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ModItem.kt new file mode 100644 index 000000000..073b61012 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ModItem.kt @@ -0,0 +1,45 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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.controls.JFXCheckBox +import javafx.fxml.FXML +import javafx.scene.control.Label +import javafx.scene.layout.BorderPane +import org.jackhuang.hmcl.mod.ModInfo + +class ModItem(info: ModInfo, private val deleteCallback: () -> Unit) : BorderPane() { + @FXML lateinit var lblModFileName: Label + @FXML lateinit var lblModAuthor: Label + @FXML lateinit var chkEnabled: JFXCheckBox + + init { + loadFXML("/assets/fxml/mod-item.fxml") + + lblModFileName.text = info.fileName + lblModAuthor.text = "${info.name}, Version: ${info.version}, Game Version: ${info.mcversion}, Authors: ${info.authors}" + chkEnabled.isSelected = info.isActive + chkEnabled.selectedProperty().addListener { _, _, newValue -> + info.activeProperty.set(newValue) + } + } + + fun onDelete() { + deleteCallback() + } +} \ No newline at end of file diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/RipplerContainer.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/RipplerContainer.kt index c3018b7af..97f22620c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/RipplerContainer.kt +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/RipplerContainer.kt @@ -81,42 +81,22 @@ open class RipplerContainer(@NamedArg("container") container: Node): StackPane() init { styleClass += "rippler-container" this.container = container - /* - armedProperty().addListener { o, oldVal, newVal -> - if (newVal!!.booleanValue()) { - this.releaseManualRippler = this.buttonRippler.createManualRipple() - if (this.clickedAnimation != null) { - this.clickedAnimation!!.rate = 1.0 - this.clickedAnimation!!.play() - } - } else { - if (this.releaseManualRippler != null) { - this.releaseManualRippler!!.run() - } - - if (this.clickedAnimation != null) { - this.clickedAnimation!!.rate = -1.0 - this.clickedAnimation!!.play() - } - } - - }*/ this.buttonContainer.children.add(this.buttonRippler) - setOnMousePressed { e -> + setOnMousePressed { if (this.clickedAnimation != null) { this.clickedAnimation!!.rate = 1.0 this.clickedAnimation!!.play() } } - setOnMouseReleased { e -> + setOnMouseReleased { if (this.clickedAnimation != null) { this.clickedAnimation!!.rate = -1.0 this.clickedAnimation!!.play() } } - focusedProperty().addListener { o, oldVal, newVal -> + focusedProperty().addListener { _, _, newVal -> if (newVal) { if (!isPressed) { this.buttonRippler.showOverlay() @@ -126,7 +106,7 @@ open class RipplerContainer(@NamedArg("container") container: Node): StackPane() } } - pressedProperty().addListener { _, _, _ -> this.buttonRippler.hideOverlay() } + pressedProperty().addListener { _ -> this.buttonRippler.hideOverlay() } isPickOnBounds = false this.buttonContainer.isPickOnBounds = false this.buttonContainer.shapeProperty().bind(shapeProperty()) @@ -157,8 +137,8 @@ open class RipplerContainer(@NamedArg("container") container: Node): StackPane() this.updateChildren() containerProperty.addListener { _ -> updateChildren() } - selectedProperty.addListener { _, _, newValue -> - if (newValue) background = Background(BackgroundFill(ripplerFill, defaultRadii, null)) + selectedProperty.addListener { _ -> + if (selected) background = Background(BackgroundFill(ripplerFill, defaultRadii, null)) else background = Background(BackgroundFill(Color.TRANSPARENT, defaultRadii, null)) } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionPage.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionPage.kt index d61771070..55b6745ad 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionPage.kt +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionPage.kt @@ -17,107 +17,38 @@ */ package org.jackhuang.hmcl.ui -import com.jfoenix.controls.JFXScrollPane -import com.jfoenix.controls.JFXTextField +import com.jfoenix.controls.* import javafx.beans.InvalidationListener import javafx.beans.property.Property import javafx.beans.property.SimpleStringProperty import javafx.beans.property.StringProperty +import javafx.beans.value.ChangeListener import javafx.fxml.FXML +import javafx.scene.control.Label import javafx.scene.control.ScrollPane import javafx.scene.layout.* import javafx.stage.DirectoryChooser +import org.jackhuang.hmcl.setting.Profile import org.jackhuang.hmcl.setting.VersionSetting -import org.jackhuang.hmcl.ui.wizard.HasTitle +import org.jackhuang.hmcl.ui.wizard.DecoratorPage +import org.jackhuang.hmcl.util.OS -class VersionPage : StackPane(), HasTitle { +class VersionPage : StackPane(), DecoratorPage { override val titleProperty: StringProperty = SimpleStringProperty(this, "title", null) - var lastVersionSetting: VersionSetting? = null - @FXML lateinit var rootPane: VBox - @FXML lateinit var scroll: ScrollPane - @FXML lateinit var settingsPane: GridPane - @FXML lateinit var txtWidth: JFXTextField - @FXML lateinit var txtHeight: JFXTextField - @FXML lateinit var txtMaxMemory: JFXTextField - @FXML lateinit var txtJVMArgs: JFXTextField - @FXML lateinit var txtGameArgs: JFXTextField - @FXML lateinit var txtMetaspace: JFXTextField - @FXML lateinit var txtWrapper: JFXTextField - @FXML lateinit var txtPrecallingCommand: JFXTextField - @FXML lateinit var txtServerIP: JFXTextField - @FXML lateinit var txtGameDir: JFXTextField - @FXML lateinit var advancedSettingsPane: VBox + @FXML lateinit var versionSettingsController: VersionSettingsController + @FXML lateinit var modController: ModController init { loadFXML("/assets/fxml/version.fxml") } - fun initialize() { - JFXScrollPane.smoothScrolling(scroll) - - fun validation(field: JFXTextField) = InvalidationListener { field.validate() } - fun validator(nullable: Boolean = false) = NumberValidator(nullable).apply { message = "Must be a number." } - - txtWidth.setValidators(validator()) - txtWidth.textProperty().addListener(validation(txtWidth)) - txtHeight.setValidators(validator()) - txtHeight.textProperty().addListener(validation(txtHeight)) - txtMaxMemory.setValidators(validator()) - txtMaxMemory.textProperty().addListener(validation(txtMaxMemory)) - txtMetaspace.setValidators(validator(true)) - txtMetaspace.textProperty().addListener(validation(txtMetaspace)) - } - - fun loadVersionSetting(id: String, version: VersionSetting) { + fun load(id: String, profile: Profile) { titleProperty.set("Version settings - " + id) - rootPane.children -= advancedSettingsPane - lastVersionSetting?.apply { - widthProperty.unbind() - heightProperty.unbind() - maxMemoryProperty.unbind() - javaArgsProperty.unbind() - minecraftArgsProperty.unbind() - permSizeProperty.unbind() - wrapperProperty.unbind() - precalledCommandProperty.unbind() - serverIpProperty.unbind() - } - - bindInt(txtWidth, version.widthProperty) - bindInt(txtHeight, version.heightProperty) - bindInt(txtMaxMemory, version.maxMemoryProperty) - bindString(txtJVMArgs, version.javaArgsProperty) - bindString(txtGameArgs, version.minecraftArgsProperty) - bindString(txtMetaspace, version.permSizeProperty) - bindString(txtWrapper, version.wrapperProperty) - bindString(txtPrecallingCommand, version.precalledCommandProperty) - bindString(txtServerIP, version.serverIpProperty) - - lastVersionSetting = version - } - - private fun bindInt(textField: JFXTextField, property: Property<*>) { - textField.textProperty().unbind() - textField.textProperty().bindBidirectional(property as Property, SafeIntStringConverter()) - } - - private fun bindString(textField: JFXTextField, property: Property) { - textField.textProperty().unbind() - textField.textProperty().bindBidirectional(property) - } - - fun onShowAdvanced() { - if (!rootPane.children.contains(advancedSettingsPane)) - rootPane.children += advancedSettingsPane - } - - fun onExploreJavaDir() { - val chooser = DirectoryChooser() - chooser.title = "Selecting Java Directory" - val selectedDir = chooser.showDialog(Controllers.stage) - if (selectedDir != null) - txtGameDir.text = selectedDir.absolutePath + versionSettingsController.loadVersionSetting(id, profile.getVersionSetting(id)) + modController.modManager = profile.modManager + modController.versionId = id + modController.loadMods() } } \ No newline at end of file diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionSettingsController.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionSettingsController.kt new file mode 100644 index 000000000..8f00daf5d --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionSettingsController.kt @@ -0,0 +1,151 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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.controls.JFXCheckBox +import com.jfoenix.controls.JFXComboBox +import com.jfoenix.controls.JFXTextField +import javafx.beans.InvalidationListener +import javafx.beans.property.Property +import javafx.beans.value.ChangeListener +import javafx.fxml.FXML +import javafx.scene.control.Label +import javafx.scene.control.ScrollPane +import javafx.scene.layout.GridPane +import javafx.scene.layout.VBox +import javafx.stage.DirectoryChooser +import org.jackhuang.hmcl.setting.VersionSetting +import org.jackhuang.hmcl.util.OS + +class VersionSettingsController { + var lastVersionSetting: VersionSetting? = null + @FXML lateinit var rootPane: VBox + @FXML lateinit var scroll: ScrollPane + @FXML lateinit var settingsPane: GridPane + @FXML lateinit var txtWidth: JFXTextField + @FXML lateinit var txtHeight: JFXTextField + @FXML lateinit var txtMaxMemory: JFXTextField + @FXML lateinit var txtJVMArgs: JFXTextField + @FXML lateinit var txtGameArgs: JFXTextField + @FXML lateinit var txtMetaspace: JFXTextField + @FXML lateinit var txtWrapper: JFXTextField + @FXML lateinit var txtPrecallingCommand: JFXTextField + @FXML lateinit var txtServerIP: JFXTextField + @FXML lateinit var txtGameDir: JFXTextField + @FXML lateinit var advancedSettingsPane: VBox + @FXML lateinit var cboLauncherVisibility: JFXComboBox<*> + @FXML lateinit var cboRunDirectory: JFXComboBox<*> + @FXML lateinit var chkFullscreen: JFXCheckBox + @FXML lateinit var lblPhysicalMemory: Label + + fun initialize() { + lblPhysicalMemory.text = "Physical Memory: ${OS.TOTAL_MEMORY}MB" + + scroll.smoothScrolling() + + fun validation(field: JFXTextField) = InvalidationListener { field.validate() } + fun validator(nullable: Boolean = false) = NumberValidator(nullable).apply { message = "Must be a number." } + + txtWidth.setValidators(validator()) + txtWidth.textProperty().addListener(validation(txtWidth)) + txtHeight.setValidators(validator()) + txtHeight.textProperty().addListener(validation(txtHeight)) + txtMaxMemory.setValidators(validator()) + txtMaxMemory.textProperty().addListener(validation(txtMaxMemory)) + txtMetaspace.setValidators(validator(true)) + txtMetaspace.textProperty().addListener(validation(txtMetaspace)) + } + + fun loadVersionSetting(id: String, version: VersionSetting) { + rootPane.children -= advancedSettingsPane + + lastVersionSetting?.apply { + widthProperty.unbind() + heightProperty.unbind() + maxMemoryProperty.unbind() + javaArgsProperty.unbind() + minecraftArgsProperty.unbind() + permSizeProperty.unbind() + wrapperProperty.unbind() + precalledCommandProperty.unbind() + serverIpProperty.unbind() + fullscreenProperty.unbind() + unbindEnum(cboLauncherVisibility) + unbindEnum(cboRunDirectory) + } + + bindInt(txtWidth, version.widthProperty) + bindInt(txtHeight, version.heightProperty) + bindInt(txtMaxMemory, version.maxMemoryProperty) + bindString(txtJVMArgs, version.javaArgsProperty) + bindString(txtGameArgs, version.minecraftArgsProperty) + bindString(txtMetaspace, version.permSizeProperty) + bindString(txtWrapper, version.wrapperProperty) + bindString(txtPrecallingCommand, version.precalledCommandProperty) + bindString(txtServerIP, version.serverIpProperty) + bindEnum(cboLauncherVisibility, version.launcherVisibilityProperty) + bindEnum(cboRunDirectory, version.gameDirTypeProperty) + + chkFullscreen.selectedProperty().unbind() + chkFullscreen.selectedProperty().bindBidirectional(version.fullscreenProperty) + + lastVersionSetting = version + } + + private fun bindInt(textField: JFXTextField, property: Property<*>) { + textField.textProperty().unbind() + @Suppress("UNCHECKED_CAST") + textField.textProperty().bindBidirectional(property as Property, SafeIntStringConverter()) + } + + private fun bindString(textField: JFXTextField, property: Property) { + textField.textProperty().unbind() + textField.textProperty().bindBidirectional(property) + } + + private fun bindEnum(comboBox: JFXComboBox<*>, property: Property>) { + unbindEnum(comboBox) + val listener = ChangeListener { _, _, newValue -> + property.value = property.value.javaClass.enumConstants[newValue.toInt()] + } + comboBox.selectionModel.select(property.value.ordinal) + comboBox.properties["listener"] = listener + comboBox.selectionModel.selectedIndexProperty().addListener(listener) + } + + private fun unbindEnum(comboBox: JFXComboBox<*>) { + @Suppress("UNCHECKED_CAST") + val listener = comboBox.properties["listener"] as? ChangeListener ?: return + comboBox.selectionModel.selectedIndexProperty().removeListener(listener) + } + + fun onShowAdvanced() { + if (!rootPane.children.contains(advancedSettingsPane)) + rootPane.children += advancedSettingsPane + else + rootPane.children.remove(advancedSettingsPane) + } + + fun onExploreJavaDir() { + val chooser = DirectoryChooser() + chooser.title = "Selecting Java Directory" + val selectedDir = chooser.showDialog(Controllers.stage) + if (selectedDir != null) + txtGameDir.text = selectedDir.absolutePath + } +} \ No newline at end of file diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/HasTitle.kt b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/DecoratorPage.kt similarity index 94% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/HasTitle.kt rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/DecoratorPage.kt index 92dde71a2..5b06ea433 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/HasTitle.kt +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/DecoratorPage.kt @@ -19,6 +19,8 @@ package org.jackhuang.hmcl.ui.wizard import javafx.beans.property.StringProperty -interface HasTitle { +interface DecoratorPage { val titleProperty: StringProperty + + fun onClose() {} } \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/fxml/account-item.fxml b/HMCL/src/main/resources/assets/fxml/account-item.fxml index a2875c6b5..4987018cb 100644 --- a/HMCL/src/main/resources/assets/fxml/account-item.fxml +++ b/HMCL/src/main/resources/assets/fxml/account-item.fxml @@ -3,11 +3,11 @@ - + @@ -27,9 +27,11 @@ - + + + - + diff --git a/HMCL/src/main/resources/assets/fxml/account.fxml b/HMCL/src/main/resources/assets/fxml/account.fxml index 8c26e5720..e6fe0ade3 100644 --- a/HMCL/src/main/resources/assets/fxml/account.fxml +++ b/HMCL/src/main/resources/assets/fxml/account.fxml @@ -13,6 +13,7 @@ + @@ -34,10 +35,10 @@ - + + -