Able to launch game now

This commit is contained in:
huangyuhui
2017-08-08 23:57:17 +08:00
parent f1ffa5ca03
commit da66102bc0
62 changed files with 1790 additions and 744 deletions

View File

@@ -19,6 +19,7 @@ package org.jackhuang.hmcl
import javafx.application.Application import javafx.application.Application
import javafx.stage.Stage import javafx.stage.Stage
import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.ui.Controllers import org.jackhuang.hmcl.ui.Controllers
import org.jackhuang.hmcl.util.DEFAULT_USER_AGENT import org.jackhuang.hmcl.util.DEFAULT_USER_AGENT
import org.jackhuang.hmcl.util.OS import org.jackhuang.hmcl.util.OS
@@ -27,6 +28,7 @@ import java.io.File
class MainApplication : Application() { class MainApplication : Application() {
override fun start(stage: Stage) { override fun start(stage: Stage) {
PRIMARY_STAGE = stage
Controllers.initialize(stage) Controllers.initialize(stage)
stage.isResizable = false stage.isResizable = false
@@ -36,6 +38,10 @@ class MainApplication : Application() {
companion object { companion object {
val VERSION = "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@"
val TITLE = "HMCL $VERSION"
lateinit var PRIMARY_STAGE: Stage
@JvmStatic @JvmStatic
fun main(args: Array<String>) { fun main(args: Array<String>) {
DEFAULT_USER_AGENT = "Hello Minecraft! Launcher" DEFAULT_USER_AGENT = "Hello Minecraft! Launcher"
@@ -57,5 +63,10 @@ class MainApplication : Application() {
} }
fun getMinecraftDirectory(): File = getWorkingDirectory("minecraft") fun getMinecraftDirectory(): File = getWorkingDirectory("minecraft")
fun stop() {
PRIMARY_STAGE.close()
Scheduler.shutdown()
}
} }
} }

View File

@@ -0,0 +1,41 @@
/*
* 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.game
import org.jackhuang.hmcl.launch.DefaultLauncher
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.Scheduler
object LauncherHelper {
fun launch() {
val profile = Settings.selectedProfile
val repository = profile.repository
val account = Settings.selectedAccount ?: throw IllegalStateException("No account here")
val version = repository.getVersion(profile.selectedVersion)
val launcher = DefaultLauncher(
repository = repository,
versionId = profile.selectedVersion,
options = profile.getVersionSetting(profile.selectedVersion).toLaunchOptions(profile.gameDir),
account = account.logIn(Settings.PROXY)
)
profile.dependency.checkGameCompletionAsync(version)
.then(launcher.launchAsync())
.subscribe(Scheduler.JAVAFX) { println("lalala") }
}
}

View File

@@ -25,7 +25,7 @@ import java.util.TreeMap
class Config { class Config {
@SerializedName("last") @SerializedName("last")
var last: String = "" var selectedProfile: String = ""
set(value) { set(value) {
field = value field = value
Settings.save() Settings.save()
@@ -97,7 +97,13 @@ class Config {
Settings.save() Settings.save()
} }
@SerializedName("accounts") @SerializedName("accounts")
var accounts: MutableMap<String, MutableMap<*, *>> = TreeMap() var accounts: MutableMap<String, MutableMap<Any, Any>> = TreeMap()
set(value) {
field = value
Settings.save()
}
@SerializedName("selectedAccount")
var selectedAccount: String = ""
set(value) { set(value) {
field = value field = value
Settings.save() Settings.save()

View File

@@ -20,6 +20,9 @@ package org.jackhuang.hmcl.setting
import com.google.gson.* import com.google.gson.*
import javafx.beans.InvalidationListener import javafx.beans.InvalidationListener
import javafx.beans.property.* 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.game.HMCLGameRepository
import org.jackhuang.hmcl.util.* import org.jackhuang.hmcl.util.*
import java.io.File import java.io.File
@@ -39,6 +42,7 @@ class Profile(var name: String = "Default", gameDir: File = File(".minecraft"))
var noCommon: Boolean by noCommonProperty var noCommon: Boolean by noCommonProperty
var repository = HMCLGameRepository(gameDir) var repository = HMCLGameRepository(gameDir)
var dependency = DefaultDependencyManager(repository, BMCLAPIDownloadProvider)
init { init {
gameDirProperty.addListener { _, _, newValue -> gameDirProperty.addListener { _, _, newValue ->

View File

@@ -30,8 +30,15 @@ import java.io.File
import java.util.logging.Level import java.util.logging.Level
import org.jackhuang.hmcl.ProfileLoadingEvent import org.jackhuang.hmcl.ProfileLoadingEvent
import org.jackhuang.hmcl.ProfileChangedEvent import org.jackhuang.hmcl.ProfileChangedEvent
import org.jackhuang.hmcl.auth.Account
import org.jackhuang.hmcl.auth.Accounts
import org.jackhuang.hmcl.auth.OfflineAccount
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount
import org.jackhuang.hmcl.event.EVENT_BUS import org.jackhuang.hmcl.event.EVENT_BUS
import org.jackhuang.hmcl.util.FileTypeAdapter import org.jackhuang.hmcl.util.FileTypeAdapter
import org.jackhuang.hmcl.util.ignoreException
import java.net.Proxy
import java.util.*
object Settings { object Settings {
@@ -48,8 +55,31 @@ object Settings {
val SETTINGS: Config val SETTINGS: Config
private val ACCOUNTS = mutableMapOf<String, Account>()
init { init {
SETTINGS = initSettings(); SETTINGS = initSettings();
loop@for ((name, settings) in SETTINGS.accounts) {
val factory = when(settings["type"]) {
"yggdrasil" -> YggdrasilAccount
"offline" -> OfflineAccount
else -> {
SETTINGS.accounts.remove(name)
continue@loop
}
}
val account = factory.fromStorage(settings)
if (account.username != name) {
SETTINGS.accounts.remove(name)
continue
}
ACCOUNTS[name] = account
}
save() save()
if (!getProfiles().containsKey(DEFAULT_PROFILE)) if (!getProfiles().containsKey(DEFAULT_PROFILE))
@@ -59,6 +89,10 @@ object Settings {
profile.name = name profile.name = name
profile.addPropertyChangedListener(InvalidationListener { save() }) profile.addPropertyChangedListener(InvalidationListener { save() })
} }
ignoreException {
Runtime.getRuntime().addShutdownHook(Thread(this::save))
}
} }
fun getDownloadProvider(): DownloadProvider = when (SETTINGS.downloadtype) { fun getDownloadProvider(): DownloadProvider = when (SETTINGS.downloadtype) {
@@ -93,16 +127,62 @@ object Settings {
fun save() { fun save() {
try { try {
SETTINGS.accounts.clear()
for ((name, account) in ACCOUNTS) {
val storage = account.toStorage()
storage["type"] = when(account) {
is OfflineAccount -> "offline"
is YggdrasilAccount -> "yggdrasil"
else -> ""
}
SETTINGS.accounts[name] = storage
}
SETTINGS_FILE.writeText(GSON.toJson(SETTINGS)) SETTINGS_FILE.writeText(GSON.toJson(SETTINGS))
} catch (ex: IOException) { } catch (ex: IOException) {
LOG.log(Level.SEVERE, "Failed to save config", ex) LOG.log(Level.SEVERE, "Failed to save config", ex)
} }
} }
fun getLastProfile(): Profile { val selectedProfile: Profile
if (!hasProfile(SETTINGS.last)) get() {
SETTINGS.last = DEFAULT_PROFILE if (!hasProfile(SETTINGS.selectedProfile))
return getProfile(SETTINGS.last) SETTINGS.selectedProfile = DEFAULT_PROFILE
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
return acc
}
return a
}
fun setSelectedAccount(name: String) {
if (ACCOUNTS.containsKey(name))
SETTINGS.selectedAccount = name
}
val PROXY: Proxy = Proxy.NO_PROXY
fun addAccount(account: Account) {
ACCOUNTS[account.username] = account
}
fun getAccount(name: String): Account? {
return ACCOUNTS[name]
}
fun getAccounts(): Map<String, Account> {
return Collections.unmodifiableMap(ACCOUNTS)
}
fun deleteAccount(name: String) {
ACCOUNTS.remove(name)
} }
fun getProfile(name: String?): Profile { fun getProfile(name: String?): Profile {
@@ -136,16 +216,16 @@ object Settings {
return true return true
} }
fun delProfile(ver: Profile): Boolean { fun deleteProfile(ver: Profile): Boolean {
return delProfile(ver.name) return deleteProfile(ver.name)
} }
fun delProfile(ver: String): Boolean { fun deleteProfile(ver: String): Boolean {
if (DEFAULT_PROFILE == ver) { if (DEFAULT_PROFILE == ver) {
return false return false
} }
var notify = false var notify = false
if (getLastProfile().name == ver) if (selectedProfile.name == ver)
notify = true notify = true
val flag = getProfiles().remove(ver) != null val flag = getProfiles().remove(ver) != null
if (notify && flag) if (notify && flag)
@@ -154,9 +234,8 @@ object Settings {
} }
internal fun onProfileChanged() { internal fun onProfileChanged() {
val p = getLastProfile() EVENT_BUS.fireEvent(ProfileChangedEvent(SETTINGS, selectedProfile))
EVENT_BUS.fireEvent(ProfileChangedEvent(SETTINGS, p)) selectedProfile.repository.refreshVersions()
p.repository.refreshVersions()
} }
/** /**

View File

@@ -20,7 +20,11 @@ package org.jackhuang.hmcl.setting
import com.google.gson.* import com.google.gson.*
import javafx.beans.InvalidationListener import javafx.beans.InvalidationListener
import javafx.beans.property.* 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.*
import java.io.File
import java.io.IOException
import java.lang.reflect.Type import java.lang.reflect.Type
class VersionSetting() { class VersionSetting() {
@@ -61,14 +65,14 @@ class VersionSetting() {
/** /**
* The permanent generation size of JVM garbage collection. * The permanent generation size of JVM garbage collection.
*/ */
val permSizeProperty = SimpleIntegerProperty(this, "permSize", 0) val permSizeProperty = SimpleStringProperty(this, "permSize", "")
var permSize: Int by permSizeProperty var permSize: String by permSizeProperty
/** /**
* The maximum memory that JVM can allocate. * The maximum memory that JVM can allocate.
* The size of JVM heap. * The size of JVM heap.
*/ */
val maxMemoryProperty = SimpleIntegerProperty(this, "maxMemory", 0) val maxMemoryProperty = SimpleIntegerProperty(this, "maxMemory", OS.SUGGESTED_MEMORY)
var maxMemory: Int by maxMemoryProperty var maxMemory: Int by maxMemoryProperty
/** /**
@@ -127,7 +131,7 @@ class VersionSetting() {
* String type prevents unexpected value from causing JsonSyntaxException. * String type prevents unexpected value from causing JsonSyntaxException.
* We can only reset this field instead of recreating the whole setting file. * We can only reset this field instead of recreating the whole setting file.
*/ */
val widthProperty = SimpleIntegerProperty(this, "width", 0) val widthProperty = SimpleIntegerProperty(this, "width", 854)
var width: Int by widthProperty var width: Int by widthProperty
@@ -138,7 +142,7 @@ class VersionSetting() {
* String type prevents unexpected value from causing JsonSyntaxException. * String type prevents unexpected value from causing JsonSyntaxException.
* We can only reset this field instead of recreating the whole setting file. * We can only reset this field instead of recreating the whole setting file.
*/ */
val heightProperty = SimpleIntegerProperty(this, "height", 0) val heightProperty = SimpleIntegerProperty(this, "height", 480)
var height: Int by heightProperty var height: Int by heightProperty
@@ -179,6 +183,32 @@ class VersionSetting() {
launcherVisibilityProperty.addListener(listener) launcherVisibilityProperty.addListener(listener)
} }
@Throws(IOException::class)
fun toLaunchOptions(gameDir: File): LaunchOptions {
return LaunchOptions(
gameDir = gameDir,
java = if (java == null) JavaVersion.fromCurrentEnvironment()
else JavaVersion.fromExecutable(File(java)),
versionName = MainApplication.TITLE,
profileName = MainApplication.TITLE,
minecraftArgs = minecraftArgs,
javaArgs = javaArgs,
maxMemory = maxMemory,
metaspace = permSize.toIntOrNull(),
width = width,
height = height,
fullscreen = fullscreen,
serverIp = serverIp,
wrapper = wrapper,
proxyHost = Settings.SETTINGS.proxyHost,
proxyPort = Settings.SETTINGS.proxyPort,
proxyUser = Settings.SETTINGS.proxyUserName,
proxyPass = Settings.SETTINGS.proxyPassword,
precalledCommand = precalledCommand,
noGeneratedJVMArgs = noJVMArgs
)
}
companion object Serializer: JsonSerializer<VersionSetting>, JsonDeserializer<VersionSetting> { companion object Serializer: JsonSerializer<VersionSetting>, JsonDeserializer<VersionSetting> {
override fun serialize(src: VersionSetting?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { override fun serialize(src: VersionSetting?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
if (src == null) return JsonNull.INSTANCE if (src == null) return JsonNull.INSTANCE
@@ -214,7 +244,7 @@ class VersionSetting() {
javaArgs = json["javaArgs"]?.asString ?: "" javaArgs = json["javaArgs"]?.asString ?: ""
minecraftArgs = json["minecraftArgs"]?.asString ?: "" minecraftArgs = json["minecraftArgs"]?.asString ?: ""
maxMemory = parseJsonPrimitive(json["maxMemory"]?.asJsonPrimitive) maxMemory = parseJsonPrimitive(json["maxMemory"]?.asJsonPrimitive)
permSize = parseJsonPrimitive(json["permSize"]?.asJsonPrimitive) permSize = json["permSize"]?.asString ?: ""
width = parseJsonPrimitive(json["width"]?.asJsonPrimitive) width = parseJsonPrimitive(json["width"]?.asJsonPrimitive)
height = parseJsonPrimitive(json["height"]?.asJsonPrimitive) height = parseJsonPrimitive(json["height"]?.asJsonPrimitive)
javaDir = json["javaDir"]?.asString ?: "" javaDir = json["javaDir"]?.asString ?: ""

View File

@@ -0,0 +1,83 @@
/*
* 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.controls.JFXButton
import com.jfoenix.effects.JFXDepthManager
import javafx.beans.binding.Bindings
import javafx.fxml.FXML
import javafx.scene.control.Label
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() {
@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 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)
// create content
val headerColor = getDefaultColor(i % 12)
header.style = "-fx-background-radius: 5 5 0 0; -fx-background-color: " + headerColor
body.minHeight = Math.random() * 20 + 50
// create image view
icon.translateYProperty().bind(Bindings.createDoubleBinding(Callable { header.boundsInParent.height - icon.height / 2 }, header.boundsInParentProperty(), icon.heightProperty()))
}
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
}
}

View File

@@ -0,0 +1,152 @@
/*
* 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.controls.*
import javafx.application.Platform
import javafx.fxml.FXML
import javafx.scene.Node
import javafx.scene.control.ScrollPane
import javafx.scene.layout.StackPane
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
import javafx.scene.control.Label
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 java.util.concurrent.Callable
class AccountsPage : StackPane(), HasTitle {
override val titleProperty: StringProperty = SimpleStringProperty(this, "title", "Accounts")
@FXML lateinit var scrollPane: ScrollPane
@FXML lateinit var masonryPane: JFXMasonryPane
@FXML lateinit var dialog: JFXDialog
@FXML lateinit var txtUsername: JFXTextField
@FXML lateinit var txtPassword: JFXPasswordField
@FXML lateinit var lblPassword: Label
@FXML lateinit var lblCreationWarning: Label
@FXML lateinit var cboType: JFXComboBox<String>
init {
loadFXML("/assets/fxml/account.fxml")
children.remove(dialog)
dialog.dialogContainer = this
JFXScrollPane.smoothScrolling(scrollPane)
cboType.selectionModel.selectedIndexProperty().addListener { _, _, newValue ->
val visible = newValue != 0
lblPassword.isVisible = visible
txtPassword.isVisible = visible
}
cboType.selectionModel.select(0)
loadAccounts()
}
fun loadAccounts() {
val children = mutableListOf<Node>()
var i = 0
for ((_, account) in Settings.getAccounts()) {
children += buildNode(++i, account)
}
masonryPane.children.setAll(children)
Platform.runLater { scrollPane.requestLayout() }
}
private fun buildNode(i: Int, account: Account): Node {
return AccountItem(i, Math.random() * 100 + 100, Math.random() * 100 + 100).apply {
lblUser.text = account.username
lblType.text = when(account) {
is OfflineAccount -> "Offline Account"
is YggdrasilAccount -> "Yggdrasil Account"
else -> throw Error("Unsupported account: $account")
}
btnDelete.setOnMouseClicked {
Settings.deleteAccount(account.username)
Platform.runLater(this@AccountsPage::loadAccounts)
}
}
}
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()
}
fun onCreationAccept() {
val type = cboType.selectionModel.selectedIndex
val username = txtUsername.text
val password = txtPassword.text
val task = Task.of(Callable {
try {
val account = when (type) {
0 -> OfflineAccount.fromUsername(username)
1 -> YggdrasilAccount.fromUsername(username, password)
else -> throw UnsupportedOperationException()
}
account.logIn(Settings.PROXY)
account
} catch (e: Exception) {
e
}
})
task.subscribe(Scheduler.JAVAFX) {
val account = task.result
if (account is Account) {
Settings.addAccount(account)
dialog.close()
loadAccounts()
} else if (account is Exception) {
lblCreationWarning.text = account.localizedMessage
}
}
}
fun onCreationCancel() {
dialog.close()
}
}

View File

@@ -0,0 +1,40 @@
/*
* 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 javafx.scene.layout.StackPane
import javafx.scene.layout.VBox
import javafx.scene.paint.Color
import javafx.scene.shape.Rectangle
import javafx.scene.text.Text
class ClassTitle(val text: String) : StackPane() {
init {
val vbox = VBox()
vbox.children += Text(text).apply {
}
vbox.children += Rectangle().apply {
widthProperty().bind(vbox.widthProperty())
height = 1.0
fill = Color.GRAY
}
children.setAll(vbox)
styleClass += "class-title"
}
}

View File

@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.ui
import javafx.fxml.FXMLLoader import javafx.fxml.FXMLLoader
import javafx.scene.Node import javafx.scene.Node
import javafx.scene.Scene import javafx.scene.Scene
import javafx.scene.layout.Pane
import javafx.scene.layout.StackPane import javafx.scene.layout.StackPane
import javafx.stage.Stage import javafx.stage.Stage
@@ -28,18 +27,22 @@ object Controllers {
lateinit var scene: Scene private set lateinit var scene: Scene private set
lateinit var stage: Stage private set lateinit var stage: Stage private set
lateinit var mainController: MainController val mainPane = MainPage()
private val mainPane: Pane = loadPane("main")
lateinit var versionController: VersionController val versionPane = VersionPage()
val versionPane: Pane = loadPane("version")
lateinit var leftPaneController: LeftPaneController
lateinit var decorator: Decorator lateinit var decorator: Decorator
fun initialize(stage: Stage) { fun initialize(stage: Stage) {
this.stage = stage this.stage = stage
val decorator = Decorator(stage, mainPane, max = false) decorator = Decorator(stage, max = false)
decorator.mainPage = mainPane
decorator.showPage(null)
leftPaneController = LeftPaneController(decorator.leftPane)
// Let root pane fix window size. // Let root pane fix window size.
with(mainPane.parent as StackPane) { with(mainPane.parent as StackPane) {
mainPane.prefWidthProperty().bind(widthProperty()) mainPane.prefWidthProperty().bind(widthProperty())
@@ -56,7 +59,7 @@ object Controllers {
} }
fun navigate(node: Node?) { fun navigate(node: Node?) {
//mainController.setContentPage(node) decorator.showPage(node)
} }
private fun <T> loadPane(s: String): T = FXMLLoader(Controllers::class.java.getResource("/assets/fxml/$s.fxml")).load() private fun <T> loadPane(s: String): T = FXMLLoader(Controllers::class.java.getResource("/assets/fxml/$s.fxml")).load()

View File

@@ -43,9 +43,18 @@ import javafx.stage.Stage
import javafx.stage.StageStyle import javafx.stage.StageStyle
import javafx.scene.layout.BorderStrokeStyle import javafx.scene.layout.BorderStrokeStyle
import javafx.scene.layout.BorderStroke import javafx.scene.layout.BorderStroke
import org.jackhuang.hmcl.MainApplication
import org.jackhuang.hmcl.ui.animation.AnimationProducer
import org.jackhuang.hmcl.ui.animation.ContainerAnimations
import org.jackhuang.hmcl.ui.animation.TransitionHandler
import org.jackhuang.hmcl.ui.wizard.*
import org.jackhuang.hmcl.util.* 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 {
override val wizardController: WizardController = WizardController(this)
class Decorator @JvmOverloads constructor(private val primaryStage: Stage, node: Node, private val max: Boolean = true, min: Boolean = true) : GridPane() {
private var xOffset: Double = 0.0 private var xOffset: Double = 0.0
private var yOffset: Double = 0.0 private var yOffset: Double = 0.0
private var newX: Double = 0.0 private var newX: Double = 0.0
@@ -66,7 +75,7 @@ class Decorator @JvmOverloads constructor(private val primaryStage: Stage, node:
@FXML lateinit var titleLabel: Label @FXML lateinit var titleLabel: Label
@FXML lateinit var leftPane: VBox @FXML lateinit var leftPane: VBox
private val onCloseButtonActionProperty: ObjectProperty<Runnable> = SimpleObjectProperty(Runnable { this.primaryStage.close() }) private val onCloseButtonActionProperty: ObjectProperty<Runnable> = SimpleObjectProperty(Runnable { MainApplication.stop() })
@JvmName("onCloseButtonActionProperty") get @JvmName("onCloseButtonActionProperty") get
var onCloseButtonAction: Runnable by onCloseButtonActionProperty var onCloseButtonAction: Runnable by onCloseButtonActionProperty
@@ -89,6 +98,9 @@ class Decorator @JvmOverloads constructor(private val primaryStage: Stage, node:
private val close = SVGGlyph(0, "CLOSE", "M810 274l-238 238 238 238-60 60-238-238-238 238-60-60 238-238-238-238 60-60 238 238 238-238z", Color.WHITE) private val close = SVGGlyph(0, "CLOSE", "M810 274l-238 238 238 238-60 60-238-238-238 238-60-60 238-238-238-238 60-60 238 238 238-238z", Color.WHITE)
.apply { setPrefSize(12.0, 12.0); setSize(12.0, 12.0) } .apply { setPrefSize(12.0, 12.0); setSize(12.0, 12.0) }
val animationHandler: TransitionHandler
override val cancelQueue: Queue<Any> = ConcurrentLinkedQueue<Any>()
init { init {
loadFXML("/assets/fxml/decorator.fxml") loadFXML("/assets/fxml/decorator.fxml")
@@ -111,13 +123,9 @@ class Decorator @JvmOverloads constructor(private val primaryStage: Stage, node:
titleContainer.addEventHandler(MouseEvent.MOUSE_ENTERED) { this.allowMove = true } titleContainer.addEventHandler(MouseEvent.MOUSE_ENTERED) { this.allowMove = true }
titleContainer.addEventHandler(MouseEvent.MOUSE_EXITED) { if (!this.isDragging) this.allowMove = false } titleContainer.addEventHandler(MouseEvent.MOUSE_EXITED) { if (!this.isDragging) this.allowMove = false }
this.contentPlaceHolder.children.add(node) animationHandler = TransitionHandler(contentPlaceHolder)
(node as Region).setMinSize(0.0, 0.0)
this.border = Border(BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths(0.0, 4.0, 4.0, 4.0))) setOverflowHidden(lookup("#contentPlaceHolderRoot") as Pane)
val clip = Rectangle()
clip.widthProperty().bind(node.widthProperty())
clip.heightProperty().bind(node.heightProperty())
node.setClip(clip)
} }
fun onMouseMoved(mouseEvent: MouseEvent) { fun onMouseMoved(mouseEvent: MouseEvent) {
@@ -285,18 +293,22 @@ class Decorator @JvmOverloads constructor(private val primaryStage: Stage, node:
this.yOffset = mouseEvent.sceneY this.yOffset = mouseEvent.sceneY
} }
@Suppress("UNUSED_PARAMETER")
private fun isRightEdge(x: Double, y: Double, boundsInParent: Bounds): Boolean { private fun isRightEdge(x: Double, y: Double, boundsInParent: Bounds): Boolean {
return x < this.width && x > this.width - this.contentPlaceHolder.snappedLeftInset() return x < this.width && x > this.width - this.contentPlaceHolder.snappedLeftInset()
} }
@Suppress("UNUSED_PARAMETER")
private fun isTopEdge(x: Double, y: Double, boundsInParent: Bounds): Boolean { private fun isTopEdge(x: Double, y: Double, boundsInParent: Bounds): Boolean {
return y >= 0.0 && y < this.contentPlaceHolder.snappedLeftInset() return y >= 0.0 && y < this.contentPlaceHolder.snappedLeftInset()
} }
@Suppress("UNUSED_PARAMETER")
private fun isBottomEdge(x: Double, y: Double, boundsInParent: Bounds): Boolean { private fun isBottomEdge(x: Double, y: Double, boundsInParent: Bounds): Boolean {
return y < this.height && y > this.height - this.contentPlaceHolder.snappedLeftInset() return y < this.height && y > this.height - this.contentPlaceHolder.snappedLeftInset()
} }
@Suppress("UNUSED_PARAMETER")
private fun isLeftEdge(x: Double, y: Double, boundsInParent: Bounds): Boolean { private fun isLeftEdge(x: Double, y: Double, boundsInParent: Bounds): Boolean {
return x >= 0.0 && x < this.contentPlaceHolder.snappedLeftInset() return x >= 0.0 && x < this.contentPlaceHolder.snappedLeftInset()
} }
@@ -335,7 +347,74 @@ class Decorator @JvmOverloads constructor(private val primaryStage: Stage, node:
} }
} }
fun setContent(content: Node) { private fun setContent(content: Node, animation: AnimationProducer) {
this.contentPlaceHolder.children.setAll(content) animationHandler.setContent(content, animation)
if (content is Region) {
content.setMinSize(0.0, 0.0)
setOverflowHidden(content)
}
backNavButton.isDisable = !wizardController.canPrev()
if (content is Refreshable)
refreshNavButton.isVisible = true
if (content != mainPage)
closeNavButton.isVisible = true
val prefix = if (category == null) "" else category + " - "
titleLabel.textProperty().unbind()
if (content is WizardPage)
titleLabel.text = prefix + content.title
if (content is HasTitle)
titleLabel.textProperty().bind(content.titleProperty)
}
lateinit var mainPage: Node
var category: String? = null
fun showPage(content: Node?) {
onEnd()
setContent(content ?: mainPage, ContainerAnimations.FADE.animationProducer)
}
fun startWizard(wizardProvider: WizardProvider, category: String? = null) {
this.category = category
wizardController.provider = wizardProvider
wizardController.onStart()
}
override fun onStart() {
backNavButton.isVisible = true
backNavButton.isDisable = false
closeNavButton.isVisible = true
refreshNavButton.isVisible = false
}
override fun onEnd() {
backNavButton.isVisible = false
closeNavButton.isVisible = false
refreshNavButton.isVisible = false
}
override fun navigateTo(page: Node, nav: Navigation.NavigationDirection) {
setContent(page, nav.animation.animationProducer)
}
fun onRefresh() {
(contentPlaceHolder.children.single() as Refreshable).refresh()
}
fun onCloseNav() {
wizardController.onCancel()
showPage(null)
}
fun onBack() {
wizardController.onPrev(true)
} }
} }

View File

@@ -33,6 +33,7 @@ import javafx.scene.image.WritableImage
import javafx.scene.input.MouseEvent import javafx.scene.input.MouseEvent
import javafx.scene.input.ScrollEvent import javafx.scene.input.ScrollEvent
import javafx.scene.layout.Pane import javafx.scene.layout.Pane
import javafx.scene.layout.Region
import javafx.scene.shape.Rectangle import javafx.scene.shape.Rectangle
import javafx.util.Duration import javafx.util.Duration
@@ -99,7 +100,7 @@ fun takeSnapshot(node: Parent, width: Double, height: Double): WritableImage {
return scene.snapshot(null) return scene.snapshot(null)
} }
fun setOverflowHidden(node: Pane) { fun setOverflowHidden(node: Region) {
val rectangle = Rectangle() val rectangle = Rectangle()
rectangle.widthProperty().bind(node.widthProperty()) rectangle.widthProperty().bind(node.widthProperty())
rectangle.heightProperty().bind(node.heightProperty()) rectangle.heightProperty().bind(node.heightProperty())
@@ -109,5 +110,5 @@ fun setOverflowHidden(node: Pane) {
val stylesheets = arrayOf( val stylesheets = arrayOf(
Controllers::class.java.getResource("/css/jfoenix-fonts.css").toExternalForm(), Controllers::class.java.getResource("/css/jfoenix-fonts.css").toExternalForm(),
Controllers::class.java.getResource("/css/jfoenix-design.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-components.css").toExternalForm(),
Controllers::class.java.getResource("/assets/css/jfoenix-main-demo.css").toExternalForm()) Controllers::class.java.getResource("/assets/css/jfoenix-main-demo.css").toExternalForm())

View File

@@ -0,0 +1,29 @@
/*
* 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 javafx.geometry.Pos
import javafx.scene.Node
import javafx.scene.control.Label
import javafx.scene.layout.HBox
class IconedItem(val icon: Node, val text: String)
: RipplerContainer(HBox().apply {
children += icon.apply { isMouseTransparent = true }
children += Label(text).apply { alignment = Pos.CENTER; isMouseTransparent = true }
})

View File

@@ -0,0 +1,120 @@
/*
* 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.controls.JFXComboBox
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.event.EVENT_BUS
import org.jackhuang.hmcl.event.RefreshedVersionsEvent
import org.jackhuang.hmcl.game.LauncherHelper
import org.jackhuang.hmcl.game.minecraftVersion
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.ui.download.DownloadWizardProvider
class LeftPaneController(val leftPane: VBox) {
val versionsPane = VBox()
val cboProfiles = JFXComboBox<String>().apply { items.add("Default"); prefWidthProperty().bind(leftPane.widthProperty()) }
val accountItem = VersionListItem("mojang@mojang.com", "Yggdrasil")
init {
addChildren(ClassTitle("ACCOUNTS"))
addChildren(RipplerContainer(accountItem).apply {
accountItem.onSettingsButtonClicked {
Controllers.navigate(AccountsPage())
}
})
addChildren(ClassTitle("LAUNCHER"))
addChildren(IconedItem(SVG.gear("black"), "Settings").apply { prefWidthProperty().bind(leftPane.widthProperty()) })
addChildren(ClassTitle("PROFILES"))
addChildren(cboProfiles)
addChildren(ClassTitle("VERSIONS"))
addChildren(versionsPane)
EVENT_BUS.channel<RefreshedVersionsEvent>() += this::loadVersions
EVENT_BUS.channel<ProfileLoadingEvent>() += this::onProfilesLoading
EVENT_BUS.channel<ProfileChangedEvent>() += this::onProfileChanged
Settings.onProfileLoading()
Controllers.decorator.addMenuButton.setOnMouseClicked {
Controllers.decorator.startWizard(DownloadWizardProvider(), "Install New Game")
}
Controllers.decorator.refreshMenuButton.setOnMouseClicked {
Settings.selectedProfile.repository.refreshVersions()
}
Controllers.mainPane.buttonLaunch.setOnMouseClicked { LauncherHelper.launch() }
}
private fun addChildren(content: Node) {
if (content is Pane) {
leftPane.children += content
} else {
val pane = StackPane()
pane.styleClass += "left-pane-item"
pane.children.setAll(content)
leftPane.children += pane
}
}
fun onProfilesLoading() {
// TODO: Profiles
}
fun onProfileChanged(event: ProfileChangedEvent) {
val profile = event.value
profile.selectedVersionProperty.addListener { _, _, newValue ->
versionChanged(newValue)
}
}
private fun loadAccounts() {
}
private fun loadVersions() {
val profile = Settings.selectedProfile
versionsPane.children.clear()
profile.repository.getVersions().forEach { version ->
val item = VersionListItem(version.id, minecraftVersion(profile.repository.getVersionJar(version.id)) ?: "Unknown")
val ripplerContainer = RipplerContainer(item)
item.onSettingsButtonClicked {
Controllers.decorator.showPage(Controllers.versionPane)
Controllers.versionPane.loadVersionSetting(item.versionName, profile.getVersionSetting(item.versionName))
}
ripplerContainer.ripplerFill = Paint.valueOf("#89E1F9")
ripplerContainer.setOnMouseClicked {
// clean selected property
versionsPane.children.forEach { if (it is RipplerContainer) it.selected = false }
ripplerContainer.selected = true
profile.selectedVersion = version.id
}
ripplerContainer.userData = version.id to item
versionsPane.children += ripplerContainer
}
}
fun versionChanged(selectedVersion: String) {
versionsPane.children
.filter { it is RipplerContainer && it.userData is Pair<*, *> }
.forEach { (it as RipplerContainer).selected = (it.userData as Pair<String, VersionListItem>).first == selectedVersion }
}
}

View File

@@ -21,9 +21,12 @@ import com.jfoenix.controls.JFXButton
import com.jfoenix.controls.JFXComboBox import com.jfoenix.controls.JFXComboBox
import com.jfoenix.controls.JFXListCell import com.jfoenix.controls.JFXListCell
import com.jfoenix.controls.JFXListView import com.jfoenix.controls.JFXListView
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
import javafx.collections.FXCollections import javafx.collections.FXCollections
import javafx.fxml.FXML import javafx.fxml.FXML
import javafx.scene.Node import javafx.scene.Node
import javafx.scene.layout.BorderPane
import javafx.scene.layout.Pane import javafx.scene.layout.Pane
import javafx.scene.layout.StackPane import javafx.scene.layout.StackPane
import org.jackhuang.hmcl.ProfileChangedEvent import org.jackhuang.hmcl.ProfileChangedEvent
@@ -36,11 +39,19 @@ import org.jackhuang.hmcl.setting.VersionSetting
import org.jackhuang.hmcl.ui.animation.ContainerAnimations import org.jackhuang.hmcl.ui.animation.ContainerAnimations
import org.jackhuang.hmcl.ui.download.DownloadWizardProvider import org.jackhuang.hmcl.ui.download.DownloadWizardProvider
import org.jackhuang.hmcl.ui.animation.TransitionHandler 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.Wizard
/** /**
* @see /assets/fxml/main.fxml * @see /assets/fxml/main.fxml
*/ */
class MainController { class MainPage : BorderPane(), HasTitle {
override val titleProperty: StringProperty = SimpleStringProperty(this, "title", "Main Page")
@FXML lateinit var buttonLaunch: JFXButton
init {
loadFXML("/assets/fxml/main.fxml")
}
} }

View File

@@ -0,0 +1,46 @@
/*
* 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.validation.base.ValidatorBase
import javafx.scene.control.TextInputControl
class NumberValidator @JvmOverloads constructor(val nullable: Boolean = false) : ValidatorBase() {
override fun eval() {
if (this.srcControl.get() is TextInputControl) {
this.evalTextInputField()
}
}
private fun evalTextInputField() {
val textField = this.srcControl.get() as TextInputControl
if (textField.text.isBlank())
hasErrors.set(false)
else
try {
Integer.parseInt(textField.text)
this.hasErrors.set(false)
} catch (var3: Exception) {
this.hasErrors.set(true)
}
}
}

View File

@@ -0,0 +1,230 @@
/*
* 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.controls.JFXRippler
import javafx.animation.Transition
import javafx.beans.DefaultProperty
import javafx.beans.NamedArg
import javafx.beans.Observable
import javafx.beans.binding.Bindings
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleObjectProperty
import javafx.geometry.Insets
import javafx.geometry.Pos
import javafx.scene.Node
import javafx.scene.control.Label
import javafx.scene.layout.*
import javafx.scene.paint.Color
import javafx.scene.paint.Paint
import javafx.scene.shape.Rectangle
import javafx.scene.shape.Shape
import org.jackhuang.hmcl.util.getValue
import org.jackhuang.hmcl.util.setValue
import java.util.concurrent.Callable
@DefaultProperty("container")
open class RipplerContainer(@NamedArg("container") container: Node): StackPane() {
val containerProperty = SimpleObjectProperty<Node>(this, "container", null)
@JvmName("containerProperty") get
var container: Node by containerProperty
val ripplerFillProperty = SimpleObjectProperty<Paint>(this, "ripplerFill", null)
@JvmName("ripplerFillProperty") get
var ripplerFill: Paint? by ripplerFillProperty
val selectedProperty = SimpleBooleanProperty(this, "selected", false)
@JvmName("selectedProperty") get
var selected: Boolean by selectedProperty
private val buttonContainer = StackPane()
private val buttonRippler = object : JFXRippler(StackPane()) {
override fun getMask(): Node {
val mask = StackPane()
mask.shapeProperty().bind(buttonContainer.shapeProperty())
mask.backgroundProperty().bind(Bindings.createObjectBinding<Background>(Callable<Background> { Background(BackgroundFill(Color.WHITE, if (buttonContainer.backgroundProperty().get() != null && buttonContainer.getBackground().getFills().size > 0) buttonContainer.background.fills[0].radii else defaultRadii, if (buttonContainer.backgroundProperty().get() != null && buttonContainer.background.fills.size > 0) buttonContainer.background.fills[0].insets else Insets.EMPTY)) }, buttonContainer.backgroundProperty()))
mask.resize(buttonContainer.width - buttonContainer.snappedRightInset() - buttonContainer.snappedLeftInset(), buttonContainer.height - buttonContainer.snappedBottomInset() - buttonContainer.snappedTopInset())
return mask
}
override fun initListeners() {
this.ripplerPane.setOnMousePressed { event ->
if (releaseManualRippler != null) {
releaseManualRippler!!.run()
}
releaseManualRippler = null
this.createRipple(event.x, event.y)
}
}
}
private var clickedAnimation: Transition? = null
private val defaultRadii = CornerRadii(3.0)
private var invalid = true
private var releaseManualRippler: Runnable? = null
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 ->
if (this.clickedAnimation != null) {
this.clickedAnimation!!.rate = 1.0
this.clickedAnimation!!.play()
}
}
setOnMouseReleased { e ->
if (this.clickedAnimation != null) {
this.clickedAnimation!!.rate = -1.0
this.clickedAnimation!!.play()
}
}
focusedProperty().addListener { o, oldVal, newVal ->
if (newVal) {
if (!isPressed) {
this.buttonRippler.showOverlay()
}
} else {
this.buttonRippler.hideOverlay()
}
}
pressedProperty().addListener { _, _, _ -> this.buttonRippler.hideOverlay() }
isPickOnBounds = false
this.buttonContainer.isPickOnBounds = false
this.buttonContainer.shapeProperty().bind(shapeProperty())
this.buttonContainer.borderProperty().bind(borderProperty())
this.buttonContainer.backgroundProperty().bind(Bindings.createObjectBinding<Background>(Callable<Background> {
if (background == null || this.isJavaDefaultBackground(background) || this.isJavaDefaultClickedBackground(background)) {
background = Background(BackgroundFill(Color.TRANSPARENT, defaultRadii, null))
}
try {
return@Callable(
if (background != null && (background.fills[0] as BackgroundFill).insets == Insets(-0.2, -0.2, -0.2, -0.2))
Background(BackgroundFill((if (background != null) (background.fills[0] as BackgroundFill).fill else Color.TRANSPARENT) as Paint,
if (backgroundProperty().get() != null) background.fills[0].radii else defaultRadii, Insets.EMPTY))
else
Background(BackgroundFill((if (background != null) background.fills[0].fill else Color.TRANSPARENT) as Paint,
if (background != null) background.fills[0].radii else defaultRadii, Insets.EMPTY))
)
} catch (var3: Exception) {
return@Callable background
}
}, backgroundProperty()))
ripplerFillProperty.addListener { o, oldVal, newVal -> this.buttonRippler.ripplerFill = newVal }
if (background == null || this.isJavaDefaultBackground(background)) {
background = Background(BackgroundFill(Color.TRANSPARENT, this.defaultRadii, null))
}
this.updateChildren()
containerProperty.addListener { _ -> updateChildren() }
selectedProperty.addListener { _, _, newValue ->
if (newValue) background = Background(BackgroundFill(ripplerFill, defaultRadii, null))
else background = Background(BackgroundFill(Color.TRANSPARENT, defaultRadii, null))
}
shape = Rectangle().apply {
widthProperty().bind(this@RipplerContainer.widthProperty())
heightProperty().bind(this@RipplerContainer.heightProperty())
}
}
protected fun updateChildren() {
children.add(container)
if (this.buttonContainer != null) {
this.children.add(0, this.buttonContainer)
}
for (i in 1..this.children.size - 1) {
this.children[i].isPickOnBounds = false
}
}
fun layoutChildren(x: Double, y: Double, w: Double, h: Double) {
if (this.invalid) {
if (ripplerFill == null) {
for (i in this.children.size - 1 downTo 1) {
if (this.children[i] is Shape) {
this.buttonRippler.ripplerFill = (this.children[i] as Shape).fill
(this.children[i] as Shape).fillProperty().addListener { o, oldVal, newVal -> this.buttonRippler.ripplerFill = newVal }
break
}
if (this.children[i] is Label) {
this.buttonRippler.ripplerFill = (this.children[i] as Label).textFill
(this.children[i] as Label).textFillProperty().addListener { o, oldVal, newVal -> this.buttonRippler.ripplerFill = newVal }
break
}
}
} else {
this.buttonRippler.ripplerFill = ripplerFill
}
this.invalid = false
}
val shift = 1.0
this.buttonContainer.resizeRelocate(layoutBounds.minX - shift, layoutBounds.minY - shift, width + 2.0 * shift, height + 2.0 * shift)
//this.layoutLabelInArea(x, y, w, h)
}
private fun isJavaDefaultBackground(background: Background): Boolean {
try {
val firstFill = (background.fills[0] as BackgroundFill).fill.toString()
return "0xffffffba" == firstFill || "0xffffffbf" == firstFill || "0xffffffbd" == firstFill
} catch (var3: Exception) {
return false
}
}
private fun isJavaDefaultClickedBackground(background: Background): Boolean {
try {
return "0x039ed3ff" == (background.fills[0] as BackgroundFill).fill.toString()
} catch (var3: Exception) {
return false
}
}
}

View File

@@ -50,8 +50,10 @@ object SVG {
return svg return svg
} }
fun gear(): Node = createSVGPath("M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z") fun gear(fill: String = "white", width: Double = 20.0, height: Double = 20.0): Node = createSVGPath("M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z", fill, width, height)
fun back(fill: String = "white"): Node = createSVGPath("M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z", fill) fun back(fill: String = "white", width: Double = 20.0, height: Double = 20.0): Node = createSVGPath("M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z", fill, width, height)
fun close(fill: String = "white"): Node = createSVGPath("M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z", fill) fun close(fill: String = "white", width: Double = 20.0, height: Double = 20.0): Node = createSVGPath("M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z", fill, width, height)
fun dotsVertical(fill: String = "white"): Node = createSVGPath("M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z", fill) fun dotsVertical(fill: String = "white", width: Double = 20.0, height: Double = 20.0): Node = createSVGPath("M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z", fill, width, height)
fun delete(fill: String = "white", width: Double = 20.0, height: Double = 20.0): Node = createSVGPath("M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z", fill, width, height)
fun accountEdit(fill: String = "white", width: Double = 20.0, height: Double = 20.0) = createSVGPath("M21.7,13.35L20.7,14.35L18.65,12.3L19.65,11.3C19.86,11.09 20.21,11.09 20.42,11.3L21.7,12.58C21.91,12.79 21.91,13.14 21.7,13.35M12,18.94L18.06,12.88L20.11,14.93L14.06,21H12V18.94M12,14C7.58,14 4,15.79 4,18V20H10V18.11L14,14.11C13.34,14.03 12.67,14 12,14M12,4A4,4 0 0,0 8,8A4,4 0 0,0 12,12A4,4 0 0,0 16,8A4,4 0 0,0 12,4Z", fill, width, height)
} }

View File

@@ -0,0 +1,28 @@
/*
* 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 javafx.util.StringConverter
class SafeIntStringConverter : StringConverter<Int>() {
/** {@inheritDoc} */
override fun fromString(value: String?) = value?.toIntOrNull()
/** {@inheritDoc} */
override fun toString(value: Int?) = value?.toString() ?: ""
}

View File

@@ -1,73 +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.controls.JFXButton
import com.jfoenix.controls.JFXScrollPane
import com.jfoenix.controls.JFXTextField
import javafx.fxml.FXML
import javafx.scene.control.Label
import javafx.scene.control.ScrollPane
import javafx.scene.layout.ColumnConstraints
import javafx.scene.layout.GridPane
import javafx.scene.layout.Priority
import javafx.scene.paint.Color
import javafx.stage.DirectoryChooser
import org.jackhuang.hmcl.setting.VersionSetting
class VersionController {
@FXML
lateinit var titleLabel: Label
@FXML
lateinit var backButton: JFXButton
@FXML
lateinit var scroll: ScrollPane
@FXML
lateinit var settingsPane: GridPane
@FXML
lateinit var txtGameDir: JFXTextField
fun initialize() {
Controllers.versionController = this
settingsPane.columnConstraints.addAll(
ColumnConstraints(),
ColumnConstraints().apply { hgrow = Priority.ALWAYS },
ColumnConstraints()
)
backButton.ripplerFill = Color.WHITE
backButton.setOnMouseClicked {
Controllers.navigate(null)
}
JFXScrollPane.smoothScrolling(scroll)
}
fun loadVersionSetting(id: String, version: VersionSetting) {
titleLabel.text = id
}
fun onExploreJavaDir() {
val chooser = DirectoryChooser()
chooser.title = "Selecting Java Directory"
val selectedDir = chooser.showDialog(Controllers.stage)
if (selectedDir != null)
txtGameDir.text = selectedDir.absolutePath
}
}

View File

@@ -39,10 +39,6 @@ class VersionListItem(val versionName: String, val gameVersion: String) : Border
handler() handler()
} }
fun onLaunch() {
}
fun onSettingsButtonClicked(handler: () -> Unit) { fun onSettingsButtonClicked(handler: () -> Unit) {
this.handler = handler this.handler = handler
} }

View File

@@ -0,0 +1,123 @@
/*
* 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.controls.JFXScrollPane
import com.jfoenix.controls.JFXTextField
import javafx.beans.InvalidationListener
import javafx.beans.property.Property
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
import javafx.fxml.FXML
import javafx.scene.control.ScrollPane
import javafx.scene.layout.*
import javafx.stage.DirectoryChooser
import org.jackhuang.hmcl.setting.VersionSetting
import org.jackhuang.hmcl.ui.wizard.HasTitle
class VersionPage : StackPane(), HasTitle {
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
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) {
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<Int>, SafeIntStringConverter())
}
private fun bindString(textField: JFXTextField, property: Property<String>) {
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
}
}

View File

@@ -22,7 +22,9 @@ import javafx.animation.KeyFrame
import javafx.animation.KeyValue import javafx.animation.KeyValue
import javafx.util.Duration import javafx.util.Duration
enum class ContainerAnimations(val animationProducer: (AnimationHandler) -> List<KeyFrame>) { typealias AnimationProducer = (AnimationHandler) -> List<KeyFrame>
enum class ContainerAnimations(val animationProducer: AnimationProducer) {
/** /**
* None * None
*/ */

View File

@@ -26,7 +26,9 @@ import javafx.scene.SnapshotParameters
import javafx.scene.image.ImageView import javafx.scene.image.ImageView
import javafx.scene.image.WritableImage import javafx.scene.image.WritableImage
import javafx.scene.layout.StackPane import javafx.scene.layout.StackPane
import javafx.scene.shape.Rectangle
import javafx.util.Duration import javafx.util.Duration
import org.jackhuang.hmcl.ui.setOverflowHidden
import org.jackhuang.hmcl.ui.takeSnapshot import org.jackhuang.hmcl.ui.takeSnapshot
/** /**
@@ -43,7 +45,7 @@ class TransitionHandler(override val view: StackPane): AnimationHandler {
override lateinit var duration: Duration override lateinit var duration: Duration
private set private set
fun setContent(newView: Node, transition: (TransitionHandler) -> List<KeyFrame>, duration: Duration = Duration.millis(320.0)) { fun setContent(newView: Node, transition: AnimationProducer, duration: Duration = Duration.millis(320.0)) {
this.duration = duration this.duration = duration
val prevAnimation = animation val prevAnimation = animation
@@ -82,8 +84,7 @@ class TransitionHandler(override val view: StackPane): AnimationHandler {
snapshot.isVisible = true snapshot.isVisible = true
snapshot.opacity = 1.0 snapshot.opacity = 1.0
view.children.setAll(snapshot) view.children.setAll(snapshot, newView)
view.children.add(newView)
snapshot.toFront() snapshot.toFront()
} }
} }

View File

@@ -20,14 +20,28 @@ package org.jackhuang.hmcl.ui.download
import javafx.scene.Node import javafx.scene.Node
import javafx.scene.layout.Pane import javafx.scene.layout.Pane
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.ui.wizard.WizardController import org.jackhuang.hmcl.ui.wizard.WizardController
import org.jackhuang.hmcl.ui.wizard.WizardProvider import org.jackhuang.hmcl.ui.wizard.WizardProvider
class DownloadWizardProvider(): WizardProvider() { class DownloadWizardProvider(): WizardProvider() {
override fun finish(settings: Map<String, Any>): Any? { override fun finish(settings: Map<String, Any>): Any? {
println(settings) val builder = Settings.selectedProfile.dependency.gameBuilder()
return null
builder.name(settings["name"] as String)
builder.gameVersion(settings["game"] as String)
if (settings.containsKey("forge"))
builder.version("forge", settings["forge"] as String)
if (settings.containsKey("liteloader"))
builder.version("liteloader", settings["liteloader"] as String)
if (settings.containsKey("optifine"))
builder.version("optifine", settings["optifine"] as String)
return builder.buildAsync()
} }
override fun createPage(controller: WizardController, step: Int, settings: Map<String, Any>): Node { override fun createPage(controller: WizardController, step: Int, settings: Map<String, Any>): Node {

View File

@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui.download package org.jackhuang.hmcl.ui.download
import com.jfoenix.controls.JFXListView import com.jfoenix.controls.JFXListView
import com.jfoenix.controls.JFXTextField
import javafx.fxml.FXML import javafx.fxml.FXML
import javafx.scene.control.Label import javafx.scene.control.Label
import javafx.scene.layout.StackPane import javafx.scene.layout.StackPane
@@ -33,15 +34,20 @@ class InstallersPage(private val controller: WizardController, private val downl
@FXML lateinit var lblForge: Label @FXML lateinit var lblForge: Label
@FXML lateinit var lblLiteLoader: Label @FXML lateinit var lblLiteLoader: Label
@FXML lateinit var lblOptiFine: Label @FXML lateinit var lblOptiFine: Label
@FXML lateinit var txtName: JFXTextField
init { init {
loadFXML("/assets/fxml/download/installers.fxml") loadFXML("/assets/fxml/download/installers.fxml")
val gameVersion = controller.settings["game"] as String
txtName.text = gameVersion
list.selectionModel.selectedIndexProperty().addListener { _, _, newValue -> list.selectionModel.selectedIndexProperty().addListener { _, _, newValue ->
controller.settings[INSTALLER_TYPE] = newValue controller.settings[INSTALLER_TYPE] = newValue
controller.onNext(when (newValue){ controller.onNext(when (newValue){
0 -> VersionsPage(controller, controller.settings["game"] as String, downloadProvider, "forge") { controller.onPrev(false) } 0 -> VersionsPage(controller, gameVersion, downloadProvider, "forge") { controller.onPrev(false) }
1 -> VersionsPage(controller, controller.settings["game"] as String, downloadProvider, "liteloader") { controller.onPrev(false) } 1 -> VersionsPage(controller, gameVersion, downloadProvider, "liteloader") { controller.onPrev(false) }
2 -> VersionsPage(controller, controller.settings["game"] as String, downloadProvider, "optifine") { controller.onPrev(false) } 2 -> VersionsPage(controller, gameVersion, downloadProvider, "optifine") { controller.onPrev(false) }
else -> throw IllegalStateException() else -> throw IllegalStateException()
}) })
} }
@@ -72,6 +78,11 @@ class InstallersPage(private val controller: WizardController, private val downl
settings.remove(INSTALLER_TYPE) settings.remove(INSTALLER_TYPE)
} }
fun onInstall() {
controller.settings["name"] = txtName.text
controller.onFinish()
}
companion object { companion object {
val INSTALLER_TYPE = "INSTALLER_TYPE" val INSTALLER_TYPE = "INSTALLER_TYPE"
} }

View File

@@ -24,6 +24,7 @@ import javafx.scene.layout.StackPane
import org.jackhuang.hmcl.download.RemoteVersion import org.jackhuang.hmcl.download.RemoteVersion
import org.jackhuang.hmcl.download.DownloadProvider import org.jackhuang.hmcl.download.DownloadProvider
import org.jackhuang.hmcl.task.Scheduler import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.task.TaskExecutor
import org.jackhuang.hmcl.ui.animation.ContainerAnimations import org.jackhuang.hmcl.ui.animation.ContainerAnimations
import org.jackhuang.hmcl.ui.animation.TransitionHandler import org.jackhuang.hmcl.ui.animation.TransitionHandler
import org.jackhuang.hmcl.ui.loadFXML import org.jackhuang.hmcl.ui.loadFXML
@@ -37,6 +38,7 @@ class VersionsPage(private val controller: WizardController, private val gameVer
val transitionHandler = TransitionHandler(this) val transitionHandler = TransitionHandler(this)
private val versionList = downloadProvider.getVersionListById(libraryId) private val versionList = downloadProvider.getVersionListById(libraryId)
private var executor: TaskExecutor? = null
init { init {
loadFXML("/assets/fxml/download/versions.fxml") loadFXML("/assets/fxml/download/versions.fxml")
@@ -45,7 +47,7 @@ class VersionsPage(private val controller: WizardController, private val gameVer
controller.settings[libraryId] = newValue.remoteVersion.selfVersion controller.settings[libraryId] = newValue.remoteVersion.selfVersion
callback() callback()
} }
versionList.refreshAsync(downloadProvider).subscribe(Scheduler.JAVAFX) { executor = versionList.refreshAsync(downloadProvider).subscribe(Scheduler.JAVAFX) {
val versions = ArrayList(versionList.getVersions(gameVersion)) val versions = ArrayList(versionList.getVersions(gameVersion))
versions.sortWith(RemoteVersion) versions.sortWith(RemoteVersion)
for (version in versions) { for (version in versions) {
@@ -61,5 +63,6 @@ class VersionsPage(private val controller: WizardController, private val gameVer
override fun cleanup(settings: MutableMap<String, Any>) { override fun cleanup(settings: MutableMap<String, Any>) {
settings.remove(libraryId) settings.remove(libraryId)
executor?.cancel()
} }
} }

View File

@@ -20,10 +20,11 @@ package org.jackhuang.hmcl.ui.download
import javafx.fxml.FXML import javafx.fxml.FXML
import javafx.scene.control.Label import javafx.scene.control.Label
import javafx.scene.layout.BorderPane import javafx.scene.layout.BorderPane
import javafx.scene.layout.StackPane
import org.jackhuang.hmcl.download.RemoteVersion import org.jackhuang.hmcl.download.RemoteVersion
import org.jackhuang.hmcl.ui.loadFXML import org.jackhuang.hmcl.ui.loadFXML
class VersionsPageItem(val remoteVersion: RemoteVersion<*>) : BorderPane() { class VersionsPageItem(val remoteVersion: RemoteVersion<*>) : StackPane() {
@FXML lateinit var lblSelfVersion: Label @FXML lateinit var lblSelfVersion: Label
@FXML lateinit var lblGameVersion: Label @FXML lateinit var lblGameVersion: Label

View File

@@ -0,0 +1,143 @@
/*
* 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.wizard
import com.jfoenix.concurrency.JFXUtilities
import com.jfoenix.controls.JFXProgressBar
import javafx.application.Platform
import javafx.scene.Node
import javafx.scene.control.Label
import javafx.scene.layout.StackPane
import javafx.scene.layout.VBox
import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.task.TaskExecutor
import org.jackhuang.hmcl.task.TaskListener
import java.util.*
import kotlin.concurrent.thread
interface AbstractWizardDisplayer : WizardDisplayer {
val wizardController: WizardController
val cancelQueue: Queue<Any>
override fun handleDeferredWizardResult(settings: Map<String, Any>, deferredResult: DeferredWizardResult) {
val vbox = VBox()
val progressBar = JFXProgressBar()
val label = Label()
progressBar.maxHeight = 10.0
vbox.children += progressBar
vbox.children += label
navigateTo(StackPane().apply { children += vbox }, Navigation.NavigationDirection.FINISH)
cancelQueue.add(thread {
deferredResult.start(settings, object : ResultProgressHandle {
private var running = true
override fun setProgress(currentStep: Int, totalSteps: Int) {
progressBar.progress = 1.0 * currentStep / totalSteps
}
override fun setProgress(description: String, currentStep: Int, totalSteps: Int) {
label.text = description
progressBar.progress = 1.0 * currentStep / totalSteps
}
override fun setBusy(description: String) {
progressBar.progress = JFXProgressBar.INDETERMINATE_PROGRESS
}
override fun finished(result: Any) {
running = false
}
override fun failed(message: String, canNavigateBack: Boolean) {
label.text = message
running = false
}
override val isRunning: Boolean
get() = running
})
if (!Thread.currentThread().isInterrupted)
JFXUtilities.runInFX {
navigateTo(Label("Successful"), Navigation.NavigationDirection.FINISH)
}
})
}
override fun handleTask(settings: Map<String, Any>, task: Task) {
val vbox = VBox()
val tasksBar = JFXProgressBar()
val label = Label()
tasksBar.maxHeight = 10.0
vbox.children += tasksBar
vbox.children += label
var finishedTasks = 0
navigateTo(StackPane().apply { children += vbox }, Navigation.NavigationDirection.FINISH)
task.executor().let { executor ->
executor.taskListener = object : TaskListener {
override fun onReady(task: Task) {
Platform.runLater { tasksBar.progressProperty().set(finishedTasks * 1.0 / executor.totTask.get()) }
}
override fun onFinished(task: Task) {
Platform.runLater {
label.text = task.title
++finishedTasks
tasksBar.progressProperty().set(finishedTasks * 1.0 / executor.totTask.get())
}
}
override fun onFailed(task: Task) {
Platform.runLater {
label.text = task.title
++finishedTasks
tasksBar.progressProperty().set(finishedTasks * 1.0 / executor.totTask.get())
}
}
override fun onTerminate() {
Platform.runLater { navigateTo(Label("Successful"), Navigation.NavigationDirection.FINISH) }
}
}
cancelQueue.add(executor)
executor.submit(Task.of(Scheduler.JAVAFX) {
navigateTo(Label("Successful"), Navigation.NavigationDirection.FINISH)
})
}.start()
}
override fun onCancel() {
while (cancelQueue.isNotEmpty()) {
val x = cancelQueue.poll()
when (x) {
is TaskExecutor -> x.cancel()
is Thread -> x.interrupt()
}
}
}
}

View File

@@ -17,29 +17,26 @@
*/ */
package org.jackhuang.hmcl.ui.wizard package org.jackhuang.hmcl.ui.wizard
import com.jfoenix.concurrency.JFXUtilities
import com.jfoenix.controls.JFXButton import com.jfoenix.controls.JFXButton
import com.jfoenix.controls.JFXProgressBar
import com.jfoenix.controls.JFXToolbar import com.jfoenix.controls.JFXToolbar
import com.jfoenix.effects.JFXDepthManager
import javafx.fxml.FXML import javafx.fxml.FXML
import javafx.scene.Node import javafx.scene.Node
import javafx.scene.control.Label import javafx.scene.control.Label
import javafx.scene.layout.StackPane import javafx.scene.layout.StackPane
import javafx.scene.layout.VBox
import org.jackhuang.hmcl.ui.Controllers import org.jackhuang.hmcl.ui.Controllers
import org.jackhuang.hmcl.ui.animation.TransitionHandler import org.jackhuang.hmcl.ui.animation.TransitionHandler
import org.jackhuang.hmcl.ui.loadFXML import org.jackhuang.hmcl.ui.loadFXML
import kotlin.concurrent.thread import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
internal class DefaultWizardDisplayer(private val prefix: String, wizardProvider: WizardProvider) : StackPane(), WizardDisplayer { internal class DefaultWizardDisplayer(private val prefix: String, wizardProvider: WizardProvider) : StackPane(), AbstractWizardDisplayer {
val wizardController = WizardController(this, wizardProvider) override val wizardController = WizardController(this).apply { provider = wizardProvider }
override val cancelQueue: Queue<Any> = ConcurrentLinkedQueue<Any>()
lateinit var transitionHandler: TransitionHandler lateinit var transitionHandler: TransitionHandler
@FXML lateinit var root: StackPane @FXML lateinit var root: StackPane
@FXML lateinit var closeButton: JFXButton
@FXML lateinit var backButton: JFXButton @FXML lateinit var backButton: JFXButton
@FXML lateinit var toolbar: JFXToolbar @FXML lateinit var toolbar: JFXToolbar
/** /**
@@ -48,6 +45,8 @@ internal class DefaultWizardDisplayer(private val prefix: String, wizardProvider
@FXML lateinit var refreshButton: JFXButton @FXML lateinit var refreshButton: JFXButton
@FXML lateinit var titleLabel: Label @FXML lateinit var titleLabel: Label
lateinit var nowPage: Node
init { init {
loadFXML("/assets/fxml/wizard.fxml") loadFXML("/assets/fxml/wizard.fxml")
toolbar.effect = null toolbar.effect = null
@@ -59,6 +58,16 @@ internal class DefaultWizardDisplayer(private val prefix: String, wizardProvider
wizardController.onStart() wizardController.onStart()
} }
override fun onStart() {
}
override fun onEnd() {
}
override fun onCancel() {
}
fun back() { fun back() {
wizardController.onPrev(true) wizardController.onPrev(true)
} }
@@ -68,58 +77,17 @@ internal class DefaultWizardDisplayer(private val prefix: String, wizardProvider
Controllers.navigate(null) Controllers.navigate(null)
} }
fun refresh() {
(nowPage as Refreshable).refresh()
}
override fun navigateTo(page: Node, nav: Navigation.NavigationDirection) { override fun navigateTo(page: Node, nav: Navigation.NavigationDirection) {
backButton.isDisable = !wizardController.canPrev() backButton.isDisable = !wizardController.canPrev()
transitionHandler.setContent(page, nav.animation.animationProducer) transitionHandler.setContent(page, nav.animation.animationProducer)
val title = if (prefix.isEmpty()) "" else "$prefix - " val title = if (prefix.isEmpty()) "" else "$prefix - "
if (page is WizardPage) if (page is WizardPage)
titleLabel.text = title + page.title titleLabel.text = title + page.title
} refreshButton.isVisible = page is Refreshable
nowPage = page
override fun handleDeferredWizardResult(settings: Map<String, Any>, deferredResult: DeferredWizardResult) {
val vbox = VBox()
val progressBar = JFXProgressBar()
val label = Label()
progressBar.maxHeight = 10.0
vbox.children += progressBar
vbox.children += label
root.children.setAll(progressBar)
thread {
deferredResult.start(settings, object : ResultProgressHandle {
private var running = true
override fun setProgress(currentStep: Int, totalSteps: Int) {
progressBar.progress = 1.0 * currentStep / totalSteps
}
override fun setProgress(description: String, currentStep: Int, totalSteps: Int) {
label.text = description
progressBar.progress = 1.0 * currentStep / totalSteps
}
override fun setBusy(description: String) {
progressBar.progress = JFXProgressBar.INDETERMINATE_PROGRESS
}
override fun finished(result: Any) {
running = false
}
override fun failed(message: String, canNavigateBack: Boolean) {
label.text = message
running = false
}
override val isRunning: Boolean
get() = running
})
JFXUtilities.runInFX {
navigateTo(Label("Successful"), Navigation.NavigationDirection.FINISH)
}
}
} }
} }

View File

@@ -0,0 +1,24 @@
/*
* 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.wizard
import javafx.beans.property.StringProperty
interface HasTitle {
val titleProperty: StringProperty
}

View File

@@ -25,6 +25,7 @@ interface Navigation {
fun onPrev(cleanUp: Boolean) fun onPrev(cleanUp: Boolean)
fun canPrev(): Boolean fun canPrev(): Boolean
fun onFinish() fun onFinish()
fun onEnd()
fun onCancel() fun onCancel()
enum class NavigationDirection(val animation: ContainerAnimations) { enum class NavigationDirection(val animation: ContainerAnimations) {

View File

@@ -17,7 +17,6 @@
*/ */
package org.jackhuang.hmcl.ui.wizard package org.jackhuang.hmcl.ui.wizard
interface WizardObserver { interface Refreshable {
fun refresh()
fun stepsChanged(wizard: Wizard)
} }

View File

@@ -18,15 +18,20 @@
package org.jackhuang.hmcl.ui.wizard package org.jackhuang.hmcl.ui.wizard
import javafx.scene.Node import javafx.scene.Node
import org.jackhuang.hmcl.task.Task
import java.util.* import java.util.*
class WizardController(protected val displayer: WizardDisplayer, protected val provider: WizardProvider) : Navigation { class WizardController(protected val displayer: WizardDisplayer) : Navigation {
lateinit var provider: WizardProvider
val settings = mutableMapOf<String, Any>() val settings = mutableMapOf<String, Any>()
val pages = Stack<Node>() val pages = Stack<Node>()
override fun onStart() { override fun onStart() {
settings.clear()
pages.clear()
val page = navigatingTo(0) val page = navigatingTo(0)
pages.push(page) pages.push(page)
displayer.onStart()
displayer.navigateTo(page, Navigation.NavigationDirection.START) displayer.navigateTo(page, Navigation.NavigationDirection.START)
} }
@@ -62,11 +67,19 @@ class WizardController(protected val displayer: WizardDisplayer, protected val p
when (result) { when (result) {
is DeferredWizardResult -> displayer.handleDeferredWizardResult(settings, result) is DeferredWizardResult -> displayer.handleDeferredWizardResult(settings, result)
is Summary -> displayer.navigateTo(result.component, Navigation.NavigationDirection.NEXT) is Summary -> displayer.navigateTo(result.component, Navigation.NavigationDirection.NEXT)
is Task -> displayer.handleTask(settings, result)
} }
} }
override fun onEnd() {
settings.clear()
pages.clear()
displayer.onEnd()
}
override fun onCancel() { override fun onCancel() {
displayer.onCancel()
onEnd()
} }
fun navigatingTo(step: Int): Node { fun navigatingTo(step: Int): Node {

View File

@@ -18,9 +18,16 @@
package org.jackhuang.hmcl.ui.wizard package org.jackhuang.hmcl.ui.wizard
import javafx.scene.Node import javafx.scene.Node
import org.jackhuang.hmcl.task.Task
interface WizardDisplayer { interface WizardDisplayer {
fun onStart()
fun onEnd()
fun onCancel()
fun navigateTo(page: Node, nav: Navigation.NavigationDirection) fun navigateTo(page: Node, nav: Navigation.NavigationDirection)
fun handleDeferredWizardResult(settings: Map<String, Any>, deferredResult: DeferredWizardResult) fun handleDeferredWizardResult(settings: Map<String, Any>, deferredResult: DeferredWizardResult)
fun handleTask(settings: Map<String, Any>, task: Task)
} }

View File

@@ -1,394 +0,0 @@
.root {
-fx-font-family: Roboto;
src: "/resources/roboto/Roboto-Regular.ttf";
}
/* Burgers Demo */
.jfx-hamburger {
-fx-spacing: 5;
-fx-cursor: hand;
}
.jfx-hamburger StackPane {
-fx-pref-width: 40px;
-fx-pref-height: 7px;
-fx-background-color: #D63333;
-fx-background-radius: 5px;
}
/* Input Demo */
.text-field {
-fx-max-width: 300;
}
.jfx-text-field, .jfx-password-field {
-fx-background-color: WHITE;
-fx-font-weight: BOLD;
-fx-prompt-text-fill: #808080;
-fx-alignment: top-left;
-jfx-focus-color: #4059A9;
-jfx-unfocus-color: #4d4d4d;
-fx-max-width: 300;
}
.jfx-decorator {
-fx-decorator-color: RED;
}
.jfx-decorator .jfx-decorator-buttons-container {
-fx-background-color: -fx-decorator-color;
}
.jfx-decorator .resize-border {
-fx-border-color: -fx-decorator-color;
-fx-border-width: 0 1 1 1;
}
.jfx-text-area, .text-area {
-fx-font-weight: BOLD;
}
.jfx-text-field:error, .jfx-password-field:error, .jfx-text-area:error {
-jfx-focus-color: #D34336;
-jfx-unfocus-color: #D34336;
}
.jfx-text-field .error-label, .jfx-password-field .error-label, .jfx-text-area .error-label {
-fx-text-fill: #D34336;
-fx-font-size: 0.75em;
}
.jfx-text-field .error-icon, .jfx-password-field .error-icon, .jfx-text-area .error-icon {
-fx-text-fill: #D34336;
-fx-font-size: 1em;
}
/* Progress Bar Demo */
.progress-bar > .bar {
-fx-min-width: 500;
}
.jfx-progress-bar > .bar {
-fx-min-width: 500;
}
.jfx-progress-bar {
-fx-progress-color: #0F9D58;
-fx-stroke-width: 3;
}
/* Icons Demo */
.icon {
-fx-text-fill: #FE774D;
-fx-padding: 10;
-fx-cursor: hand;
}
.icons-rippler {
-jfx-rippler-fill: BLUE;
-jfx-mask-type: CIRCLE;
}
.icons-rippler:hover {
-fx-cursor: hand;
}
.jfx-check-box {
-fx-font-weight: BOLD;
}
.custom-jfx-check-box {
-jfx-checked-color: RED;
-jfx-unchecked-color: BLACK;
}
/* Button */
.button {
-fx-padding: 0.7em 0.57em;
-fx-font-size: 14px;
}
.button-raised {
-fx-padding: 0.7em 0.57em;
-fx-font-size: 14px;
-jfx-button-type: RAISED;
-fx-background-color: rgb(77, 102, 204);
-fx-pref-width: 200;
-fx-text-fill: WHITE;
}
/* The main scrollbar **track** CSS class */
.mylistview .scroll-bar:horizontal .track,
.mylistview .scroll-bar:vertical .track {
-fx-background-color: transparent;
-fx-border-color: transparent;
-fx-background-radius: 0em;
-fx-border-radius: 2em;
}
/* The increment and decrement button CSS class of scrollbar */
.mylistview .scroll-bar:horizontal .increment-button,
.mylistview .scroll-bar:horizontal .decrement-button {
-fx-background-color: transparent;
-fx-background-radius: 0em;
-fx-padding: 0 0 10 0;
}
/* The increment and decrement button CSS class of scrollbar */
.mylistview .scroll-bar:vertical .increment-button,
.mylistview .scroll-bar:vertical .decrement-button {
-fx-background-color: transparent;
-fx-background-radius: 0em;
-fx-padding: 0 10 0 0;
}
.mylistview .scroll-bar .increment-arrow,
.mylistview .scroll-bar .decrement-arrow {
-fx-shape: " ";
-fx-padding: 0;
}
/* The main scrollbar **thumb** CSS class which we drag every time (movable) */
.mylistview .scroll-bar:horizontal .thumb,
.mylistview .scroll-bar:vertical .thumb {
-fx-background-color: derive(black, 90%);
-fx-background-insets: 2, 0, 0;
-fx-background-radius: 2em;
}
.jfx-list-cell-container {
-fx-alignment: center-left;
}
.jfx-list-cell-container > .label {
-fx-text-fill: BLACK;
}
.jfx-list-cell:odd:selected > .jfx-rippler > StackPane, .jfx-list-cell:even:selected > .jfx-rippler > StackPane {
-fx-background-color: rgba(0, 0, 255, 0.2);
}
.jfx-list-cell {
-fx-background-insets: 0.0;
-fx-text-fill: BLACK;
}
.jfx-list-cell:odd, .jfx-list-cell:even {
-fx-background-color: WHITE;
}
.jfx-list-cell:filled:hover {
-fx-text-fill: black;
}
.jfx-list-cell .jfx-rippler {
-jfx-rippler-fill: BLUE;
}
.jfx-list-view {
-fx-background-insets: 0;
-jfx-cell-horizontal-margin: 0.0;
-jfx-cell-vertical-margin: 5.0;
-jfx-vertical-gap: 10;
-jfx-expanded: false;
-fx-pref-width: 200;
}
.jfx-toggle-button {
-jfx-toggle-color: RED;
}
.jfx-tool-bar {
-fx-font-size: 15;
-fx-background-color: #5264AE;
-fx-pref-width: 100%;
-fx-pref-height: 64px;
}
.jfx-tool-bar HBox {
-fx-alignment: center;
/* -fx-spacing: 25;*/
-fx-padding: 0 10;
}
.jfx-tool-bar Label {
-fx-text-fill: WHITE;
}
.jfx-popup-container {
-fx-background-color: WHITE;
}
.jfx-snackbar-content {
-fx-background-color: #323232;
-fx-padding: 5;
-fx-spacing: 5;
}
.jfx-snackbar-toast {
-fx-text-fill: WHITE;
}
.jfx-snackbar-action {
-fx-text-fill: #ff4081;
}
.jfx-list-cell-content-container {
-fx-alignment: center-left;
}
.jfx-list-cell-container .label {
-fx-text-fill: BLACK;
}
.combo-box-popup .list-view .jfx-list-cell:odd:selected .jfx-list-cell-container,
.combo-box-popup .list-view .jfx-list-cell:even:selected .jfx-list-cell-container {
-fx-background-color: rgba(0.0, 0.0, 255.0, 0.2);
}
.combo-box-popup .list-view .jfx-list-cell {
-fx-background-insets: 0.0;
-fx-text-fill: BLACK;
}
.combo-box-popup .list-view .jfx-list-cell:odd,
.combo-box-popup .list-view .jfx-list-cell:even {
-fx-background-color: WHITE;
}
.combo-box-popup .list-view .jfx-list-cell:filled:hover {
-fx-text-fill: black;
}
.combo-box-popup .list-view .jfx-list-cell .jfx-rippler {
-fx-rippler-fill: #5264AE;
}
/*.combo-box .combo-box-button-container{
-fx-border-color:BLACK;-fx-border-width: 0 0 1 0;
}
.combo-box .combo-box-selected-value-container{
-fx-border-color:BLACK;
} */
/*
* TREE TABLE CSS
*/
.tree-table-view {
-fx-tree-table-color: rgba(255, 0, 0, 0.2);
-fx-tree-table-rippler-color: rgba(255, 0, 0, 0.4);
}
.tree-table-view:focused .tree-table-row-cell:selected {
-fx-background-color: -fx-tree-table-color;
-fx-table-cell-border-color: -fx-tree-table-color;
-fx-text-fill: BLACK;
}
.tree-table-view:focused .tree-table-row-cell:selected .tree-table-cell {
-fx-text-fill: BLACK;
}
.tree-table-view .jfx-rippler {
-jfx-rippler-fill: -fx-tree-table-rippler-color;
}
.tree-table-view .column-header,
.tree-table-view .column-header-background,
.tree-table-view .column-header-background .filler {
-fx-background-color: TRANSPARENT;
}
.tree-table-view .column-header {
-fx-border-width: 0 1 0 1;
-fx-border-color: #F3F3F3;
}
.tree-table-view .column-header .label {
-fx-text-fill: #949494;
-fx-padding: 16 0 16 0;
}
.tree-table-view .column-header .arrow, .tree-table-view .column-header .sort-order-dot {
-fx-background-color: #949494;
}
.tree-table-view .column-header:last-visible {
-fx-border-width: 0 2 0 1;
}
.tree-table-view .column-header-background {
-fx-border-width: 0 0.0 1 0;
-fx-border-color: #F3F3F3;
}
.tree-table-view .tree-table-cell {
-fx-border-width: 0 0 0 0;
-fx-padding: 16 0 16 0;
}
.tree-table-view .column-overlay {
-fx-background-color: -fx-tree-table-color;
}
.tree-table-view .column-resize-line, .tree-table-view .column-drag-header {
-fx-background-color: -fx-tree-table-rippler-color;
}
.tree-table-view:focused {
-fx-background-color: -fx-tree-table-color, -fx-box-border, -fx-control-inner-background;
-fx-background-insets: -1.4, 0, 1;
-fx-background-radius: 1.4, 0, 0;
/*....*/
-fx-padding: 1; /* 0.083333em; */
}
.tree-table-row-cell > .tree-disclosure-node > .arrow {
-fx-background-color: -fx-text-fill;
-fx-padding: 0.333333em 0.229em 0.333333em 0.229em; /* 4 */
-fx-shape: "M 0 -3.5 L 4 0 L 0 3.5 z";
}
.tree-table-row-cell .jfx-text-field {
-fx-focus-color: rgba(240, 40, 40);
}
.tree-table-row-group {
-fx-background-color: rgba(230, 230, 230);
}
.animated-option-button {
-fx-pref-width: 50px;
-fx-background-color: #44B449;
-fx-background-radius: 50px;
-fx-pref-height: 50px;
-fx-text-fill: white;
-fx-border-color: WHITE;
-fx-border-radius: 50px;
-fx-border-width: 4px;
}
.animated-option-sub-button {
-fx-background-color: #43609C;
}
.animated-option-sub-button2 {
-fx-background-color: rgb(203, 104, 96);
}
.tree-table-view .menu-item:focused {
-fx-background-color: -fx-tree-table-color;
}
.tree-table-view .menu-item .label {
-fx-padding: 5 0 5 0;
}

View File

@@ -25,6 +25,25 @@
-fx-text-fill: rgba(0.0, 0.0, 0.0, 0.87); -fx-text-fill: rgba(0.0, 0.0, 0.0, 0.87);
} }
.class-title {
-fx-font-size: 12px;
-fx-padding: 0 16 0 16;
}
.rippler-container HBox {
-fx-font-size: 14px;
-fx-padding: 10 16 10 16;
-fx-spacing: 10;
}
.left-pane-item {
-fx-padding: 10 16 10 16;
}
.jfx-decorator-left-pane {
-fx-padding: 20 0 20 0;
}
/******************************************************************************* /*******************************************************************************
* * * *
* JFX Drawer * * JFX Drawer *
@@ -84,6 +103,11 @@
-fx-padding: 0.7em 0.8em; -fx-padding: 0.7em 0.8em;
} }
.dialog-cancel {
-fx-font-weight: BOLD;
-fx-padding: 0.7em 0.8em;
}
/******************************************************************************* /*******************************************************************************
* * * *
* JFX Pop Up * * JFX Pop Up *
@@ -453,7 +477,6 @@
-fx-font-weight: BOLD; -fx-font-weight: BOLD;
-fx-prompt-text-fill: #808080; -fx-prompt-text-fill: #808080;
-fx-alignment: top-left; -fx-alignment: top-left;
-fx-max-width: 300.0;
-fx-pref-width: 300.0; -fx-pref-width: 300.0;
-jfx-focus-color: #4059A9; -jfx-focus-color: #4059A9;
-jfx-unfocus-color: #4d4d4d; -jfx-unfocus-color: #4d4d4d;
@@ -658,9 +681,9 @@
} }
.toggle-icon3 { .toggle-icon3 {
-fx-pref-width: 40px; -fx-pref-width: 5px;
-fx-background-radius: 50px; -fx-background-radius: 50px;
-fx-pref-height: 30px; -fx-pref-height: 5px;
-fx-background-color: transparent; -fx-background-color: transparent;
-jfx-toggle-color: rgba(128, 128, 255, 0.2); -jfx-toggle-color: rgba(128, 128, 255, 0.2);
-jfx-untoggle-color: transparent; -jfx-untoggle-color: transparent;
@@ -672,10 +695,33 @@
} }
.toggle-icon3 .jfx-rippler { .toggle-icon3 .jfx-rippler {
-jfx-rippler-fill: white;
-jfx-mask-type: CIRCLE;
}
.toggle-icon4 {
-fx-pref-width: 5px;
-fx-background-radius: 50px;
-fx-pref-height: 5px;
-fx-background-color: transparent;
-jfx-toggle-color: rgba(128, 128, 255, 0.2);
-jfx-untoggle-color: transparent;
}
.toggle-icon4 .icon {
-fx-fill: rgb(204.0, 204.0, 51.0);
-fx-padding: 10.0;
}
.toggle-icon4 .jfx-rippler {
-jfx-rippler-fill: #0F9D58; -jfx-rippler-fill: #0F9D58;
-jfx-mask-type: CIRCLE; -jfx-mask-type: CIRCLE;
} }
.transparent {
-fx-background-color: null;
}
/******************************************************************************* /*******************************************************************************
* * * *
* JFX Spinner * * JFX Spinner *

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.*?>
<?import com.jfoenix.controls.JFXButton?>
<?import javafx.scene.shape.SVGPath?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.image.Image?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
<VBox fx:id="content">
<StackPane fx:id="header" VBox.vgrow="ALWAYS">
<BorderPane>
<top>
<HBox alignment="CENTER_RIGHT">
<JFXButton fx:id="btnDelete" styleClass="toggle-icon3" />
</HBox>
</top>
<center>
<VBox style="-fx-padding: 0 0 0 20;">
<Label fx:id="lblUser" style="-fx-font-size: 20;" />
<Label fx:id="lblType" style="-fx-font-size: 10;" />
</VBox>
</center>
</BorderPane>
</StackPane>
<StackPane fx:id="body" style="-fx-background-radius: 0 0 5 5; -fx-background-color: rgb(255,255,255,0.87);" />
</VBox>
<StackPane fx:id="icon" StackPane.alignment="TOP_RIGHT">
<ImageView StackPane.alignment="CENTER_RIGHT">
<StackPane.margin>
<Insets right="12" />
</StackPane.margin>
<Image url="/assets/img/icon.png" />
</ImageView>
</StackPane>
</fx:root>

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import com.jfoenix.controls.JFXMasonryPane?>
<?import com.jfoenix.controls.JFXButton?>
<?import com.jfoenix.controls.JFXDialogLayout?>
<?import com.jfoenix.controls.JFXDialog?>
<?import com.jfoenix.controls.JFXPasswordField?>
<?import com.jfoenix.controls.JFXTextField?>
<?import com.jfoenix.controls.JFXComboBox?>
<?import javafx.collections.FXCollections?>
<?import java.lang.String?>
<?import com.jfoenix.validation.RequiredFieldValidator?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
<ScrollPane fitToHeight="true" fitToWidth="true" fx:id="scrollPane" hbarPolicy="NEVER">
<JFXMasonryPane fx:id="masonryPane">
</JFXMasonryPane>
</ScrollPane>
<AnchorPane pickOnBounds="false">
<JFXButton onMouseClicked="#addNewAccount" AnchorPane.bottomAnchor="16" AnchorPane.rightAnchor="16" buttonType="RAISED" prefWidth="40" prefHeight="40" style="-fx-text-fill:WHITE;-fx-background-color:#5264AE;-fx-font-size:14px;-fx-background-radius: 80px;">
<graphic>
<fx:include source="/assets/svg/plus.fxml" />
</graphic>
</JFXButton>
</AnchorPane>
<JFXDialog fx:id="dialog" transitionType="CENTER">
<JFXDialogLayout>
<heading>
<Label>Create a new account</Label>
</heading>
<body>
<GridPane vgap="30" hgap="30">
<columnConstraints>
<ColumnConstraints />
<ColumnConstraints hgrow="ALWAYS" />
</columnConstraints>
<Label text="Type" GridPane.halignment="RIGHT" GridPane.columnIndex="0" GridPane.rowIndex="0" />
<Label text="Username" GridPane.halignment="RIGHT" GridPane.columnIndex="0" GridPane.rowIndex="1" />
<Label fx:id="lblPassword" text="Password" GridPane.halignment="RIGHT" GridPane.columnIndex="0" GridPane.rowIndex="2" />
<JFXComboBox fx:id="cboType" GridPane.columnIndex="1" GridPane.rowIndex="0">
<items>
<FXCollections fx:factory="observableArrayList">
<String fx:value="Offline Account"/>
<String fx:value="Online Account" />
</FXCollections>
</items>
</JFXComboBox>
<JFXTextField fx:id="txtUsername" promptText="Username" labelFloat="true" GridPane.columnIndex="1" GridPane.rowIndex="1">
<validators>
<RequiredFieldValidator message="Input Required!">
</RequiredFieldValidator>
</validators>
</JFXTextField>
<JFXPasswordField fx:id="txtPassword" promptText="Password" labelFloat="true" GridPane.columnIndex="1" GridPane.rowIndex="2">
<validators>
<RequiredFieldValidator message="Input Required!">
</RequiredFieldValidator>
</validators>
</JFXPasswordField>
</GridPane>
</body>
<actions>
<Label fx:id="lblCreationWarning" />
<JFXButton onMouseClicked="#onCreationAccept" text="Accept" styleClass="dialog-accept" />
<JFXButton onMouseClicked="#onCreationCancel" text="Cancel" styleClass="dialog-cancel" />
</actions>
</JFXDialogLayout>
</JFXDialog>
</fx:root>

View File

@@ -7,9 +7,6 @@
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import java.lang.String?> <?import java.lang.String?>
<?import javafx.scene.shape.Rectangle?> <?import javafx.scene.shape.Rectangle?>
<?import com.jfoenix.controls.JFXComboBox?>
<?import com.jfoenix.controls.JFXListView?>
<?import com.jfoenix.controls.JFXListCell?>
<fx:root xmlns="http://javafx.com/javafx" <fx:root xmlns="http://javafx.com/javafx"
type="GridPane" type="GridPane"
xmlns:fx="http://javafx.com/fxml" xmlns:fx="http://javafx.com/fxml"
@@ -22,7 +19,7 @@
<String fx:value="resize-border" /> <String fx:value="resize-border" />
</styleClass> </styleClass>
<columnConstraints> <columnConstraints>
<ColumnConstraints prefWidth="200" /> <ColumnConstraints minWidth="200" maxWidth="200" />
<ColumnConstraints hgrow="ALWAYS" /> <ColumnConstraints hgrow="ALWAYS" />
</columnConstraints> </columnConstraints>
<rowConstraints> <rowConstraints>
@@ -30,10 +27,12 @@
<RowConstraints vgrow="ALWAYS" /> <RowConstraints vgrow="ALWAYS" />
</rowConstraints> </rowConstraints>
<StackPane GridPane.rowIndex="1" GridPane.columnIndex="0" VBox.vgrow="ALWAYS" styleClass="jfx-decorator-content-container"> <StackPane GridPane.rowIndex="1" GridPane.columnIndex="0" VBox.vgrow="ALWAYS" styleClass="jfx-decorator-content-container">
<BorderPane prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER"> <BorderPane fx:id="leftRootPane">
<center>
<BorderPane>
<center> <center>
<ScrollPane fitToHeight="true" fitToWidth="true"> <ScrollPane fitToHeight="true" fitToWidth="true">
<VBox fx:id="leftPane"> <VBox fx:id="leftPane" styleClass="jfx-decorator-left-pane" spacing="5">
</VBox> </VBox>
</ScrollPane> </ScrollPane>
</center> </center>
@@ -54,25 +53,36 @@
</BorderPane> </BorderPane>
</bottom> </bottom>
</BorderPane> </BorderPane>
</center>
<right>
<Rectangle height="${leftRootPane.height}" width="1" fill="gray" />
</right>
</BorderPane>
</StackPane> </StackPane>
<StackPane GridPane.rowIndex="1" GridPane.columnIndex="1" fx:id="contentPlaceHolder" styleClass="jfx-decorator-content-container" minWidth="0" minHeight="0" VBox.vgrow="ALWAYS"> <StackPane fx:id="contentPlaceHolderRoot" GridPane.rowIndex="1" GridPane.columnIndex="1" styleClass="jfx-decorator-content-container" VBox.vgrow="ALWAYS">
<StackPane fx:id="contentPlaceHolder" styleClass="jfx-decorator-content-container">
<styleClass> <styleClass>
<String fx:value="jfx-decorator-content-container" /> <String fx:value="jfx-decorator-content-container" />
</styleClass> </styleClass>
<!-- Node --> <!-- Node -->
</StackPane> </StackPane>
<BorderPane GridPane.rowIndex="0" GridPane.columnIndex="0" GridPane.columnSpan="2" fx:id="titleContainer" styleClass="jfx-tool-bar" pickOnBounds="false"> </StackPane>
<BorderPane GridPane.rowIndex="0" GridPane.columnIndex="0" GridPane.columnSpan="2" fx:id="titleContainer" minHeight="30" styleClass="jfx-tool-bar" pickOnBounds="false">
<left> <left>
<HBox minWidth="200" fx:id="titleWrapper" alignment="CENTER_LEFT"> <BorderPane minWidth="200" maxWidth="200" fx:id="titleWrapper">
<Label text="Hello Minecraft! Launcher" mouseTransparent="false" style="-fx-background-color: transparent; -fx-text-fill: white; -fx-font-size: 15px;" /> <center>
</HBox> <Label text="Hello Minecraft! Launcher" BorderPane.alignment="CENTER" mouseTransparent="true" style="-fx-background-color: transparent; -fx-text-fill: white; -fx-font-size: 15px;" />
</center>
<right>
<Rectangle height="${navBar.height}" width="1" fill="gray" />
</right>
</BorderPane>
</left> </left>
<center> <center>
<BorderPane fx:id="navBar"> <BorderPane fx:id="navBar">
<left> <left>
<HBox fx:id="navLeft" alignment="CENTER_LEFT" style="-fx-padding: 0;"> <HBox fx:id="navLeft" alignment="CENTER_LEFT" style="-fx-padding: 0;">
<Rectangle height="${navBar.height}" width="1" fill="gray" /> <JFXButton fx:id="backNavButton" onMouseClicked="#onBack" maxHeight="20" styleClass="toggle-icon3">
<JFXButton fx:id="backNavButton" maxHeight="20" styleClass="toggle-icon3">
<graphic> <graphic>
<fx:include source="/assets/svg/arrow-left.fxml"/> <fx:include source="/assets/svg/arrow-left.fxml"/>
</graphic> </graphic>
@@ -82,7 +92,7 @@
</left> </left>
<right> <right>
<HBox fx:id="navRight" alignment="CENTER_LEFT"> <HBox fx:id="navRight" alignment="CENTER_LEFT">
<JFXButton fx:id="refreshNavButton" maxHeight="20" styleClass="toggle-icon3" disable="true"> <JFXButton fx:id="refreshNavButton" onMouseClicked="#onRefresh" maxHeight="20" styleClass="toggle-icon3" disable="true">
<graphic> <graphic>
<fx:include source="/assets/svg/refresh.fxml"/> <fx:include source="/assets/svg/refresh.fxml"/>
</graphic> </graphic>
@@ -90,7 +100,7 @@
<Insets left="20"/> <Insets left="20"/>
</StackPane.margin> </StackPane.margin>
</JFXButton> </JFXButton>
<JFXButton fx:id="closeNavButton" maxHeight="20" styleClass="toggle-icon3"> <JFXButton fx:id="closeNavButton" onMouseClicked="#onCloseNav" maxHeight="20" styleClass="toggle-icon3">
<graphic> <graphic>
<fx:include source="/assets/svg/close.fxml"/> <fx:include source="/assets/svg/close.fxml"/>
</graphic> </graphic>

View File

@@ -7,14 +7,16 @@
<?import javafx.scene.layout.BorderPane?> <?import javafx.scene.layout.BorderPane?>
<?import com.jfoenix.controls.JFXButton?> <?import com.jfoenix.controls.JFXButton?>
<?import javafx.scene.layout.HBox?> <?import javafx.scene.layout.HBox?>
<?import com.jfoenix.controls.JFXTextField?>
<fx:root xmlns="http://javafx.com/javafx" <fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml" xmlns:fx="http://javafx.com/fxml"
type="StackPane"> type="StackPane">
<BorderPane> <BorderPane>
<top> <top>
<HBox alignment="CENTER" style="-fx-padding: 40px;"> <VBox alignment="CENTER" style="-fx-padding: 40px;" spacing="20">
<Label fx:id="lblGameVersion" alignment="CENTER" /> <Label fx:id="lblGameVersion" alignment="CENTER" />
</HBox> <JFXTextField fx:id="txtName" labelFloat="true" promptText="Enter the name of this new version" maxWidth="300" />
</VBox>
</top> </top>
<center> <center>
<JFXListView fx:id="list" styleClass="jfx-list-view" maxHeight="150" maxWidth="300"> <JFXListView fx:id="list" styleClass="jfx-list-view" maxHeight="150" maxWidth="300">
@@ -46,7 +48,7 @@
</center> </center>
<bottom> <bottom>
<HBox alignment="CENTER"> <HBox alignment="CENTER">
<JFXButton fx:id="buttonLaunch" prefWidth="100" prefHeight="40" buttonType="RAISED" text="Install" <JFXButton onMouseClicked="#onInstall" prefWidth="100" prefHeight="40" buttonType="RAISED" text="Install"
style="-fx-text-fill:WHITE;-fx-background-color:#5264AE;-fx-font-size:14px;"/> style="-fx-text-fill:WHITE;-fx-background-color:#5264AE;-fx-font-size:14px;"/>
</HBox> </HBox>
</bottom> </bottom>

View File

@@ -5,18 +5,18 @@
<fx:root xmlns="http://javafx.com/javafx" <fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml" xmlns:fx="http://javafx.com/fxml"
type="BorderPane" pickOnBounds="false"> type="StackPane" mouseTransparent="true">
<VBox alignment="CENTER">
<Label alignment="CENTER" fx:id="lblGameVersion" style="-fx-color: gray;" />
</VBox>
<BorderPane>
<left> <left>
<VBox alignment="CENTER_LEFT" mouseTransparent="true"> <VBox alignment="CENTER_LEFT">
<Label fx:id="lblSelfVersion" style="-fx-font-size: 15;" /> <Label fx:id="lblSelfVersion" style="-fx-font-size: 15;" />
</VBox> </VBox>
</left> </left>
<center>
<VBox alignment="CENTER" mouseTransparent="true">
<Label alignment="CENTER" fx:id="lblGameVersion" style="-fx-color: gray;" />
</VBox>
</center>
<right> <right>
<fx:include source="/assets/svg/arrow-right.fxml" /> <fx:include source="/assets/svg/arrow-right.fxml" />
</right> </right>
</BorderPane>
</fx:root> </fx:root>

View File

@@ -2,10 +2,9 @@
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<?import com.jfoenix.controls.*?> <?import com.jfoenix.controls.*?>
<BorderPane <fx:root
maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity"
fx:controller="org.jackhuang.hmcl.ui.MainController" style="-fx-background-color: white;" type="BorderPane"
style="-fx-background-color: white;"
xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1"> xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1">
<center> <center>
<StackPane fx:id="page" prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER" /> <StackPane fx:id="page" prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER" />
@@ -23,4 +22,4 @@
</right> </right>
</BorderPane> </BorderPane>
</bottom> </bottom>
</BorderPane> </fx:root>

View File

@@ -3,29 +3,31 @@
<?import javafx.scene.control.*?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.image.Image?>
<?import com.jfoenix.controls.JFXButton?> <?import com.jfoenix.controls.JFXButton?>
<fx:root xmlns="http://javafx.com/javafx" <fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml" xmlns:fx="http://javafx.com/fxml"
style="-fx-background-color: transparent; " styleClass="transparent"
type="BorderPane" pickOnBounds="false"> type="BorderPane" pickOnBounds="false">
<left> <left>
<VBox alignment="CENTER_LEFT" mouseTransparent="true"> <HBox alignment="CENTER" mouseTransparent="true">
<ImageView>
<Image url="/assets/img/icon.png" requestedWidth="25" requestedHeight="25" />
</ImageView>
<VBox alignment="CENTER_LEFT">
<Label fx:id="lblVersionName" style="-fx-font-size: 15;" /> <Label fx:id="lblVersionName" style="-fx-font-size: 15;" />
<Label fx:id="lblGameVersion" /> <Label fx:id="lblGameVersion" style="-fx-font-size: 10;" />
</VBox> </VBox>
</HBox>
</left> </left>
<right> <right>
<HBox> <HBox alignment="CENTER" pickOnBounds="false">
<JFXButton onMouseClicked="#onSettings" styleClass="toggle-icon3"> <JFXButton styleClass="toggle-icon4" onMouseClicked="#onSettings">
<graphic> <graphic>
<fx:include source="/assets/svg/gear.fxml"/> <fx:include source="/assets/svg/gear.fxml"/>
</graphic> </graphic>
</JFXButton> </JFXButton>
<JFXButton onMouseClicked="#onLaunch" styleClass="toggle-icon3">
<graphic>
<fx:include source="/assets/svg/rocket.fxml"/>
</graphic>
</JFXButton>
</HBox> </HBox>
</right> </right>
</fx:root> </fx:root>

View File

@@ -5,16 +5,19 @@
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<StackPane xmlns="http://javafx.com/javafx" <fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml" xmlns:fx="http://javafx.com/fxml"
fx:controller="org.jackhuang.hmcl.ui.VersionController"> type="StackPane">
<BorderPane>
<center>
<ScrollPane fx:id="scroll" <ScrollPane fx:id="scroll"
style="-fx-background-color: white; -fx-font-size: 14; -fx-pref-width: 100%; " style="-fx-font-size: 14; -fx-pref-width: 100%; "
fitToHeight="true" fitToWidth="true"> fitToHeight="true" fitToWidth="true">
<VBox> <VBox fx:id="rootPane" style="-fx-padding: 20;">
<GridPane fx:id="settingsPane" style="-fx-margin-left: 10; -fx-background-color: white; " hgap="5" vgap="10"> <GridPane fx:id="settingsPane" hgap="5" vgap="10">
<columnConstraints>
<ColumnConstraints />
<ColumnConstraints hgrow="ALWAYS" />
<ColumnConstraints />
</columnConstraints>
<Label GridPane.rowIndex="0" GridPane.columnIndex="0">Java Directory</Label> <Label GridPane.rowIndex="0" GridPane.columnIndex="0">Java Directory</Label>
<Label GridPane.rowIndex="1" GridPane.columnIndex="0">Max Memory</Label> <Label GridPane.rowIndex="1" GridPane.columnIndex="0">Max Memory</Label>
<Label GridPane.rowIndex="2" GridPane.columnIndex="0">Launcher Visibility</Label> <Label GridPane.rowIndex="2" GridPane.columnIndex="0">Launcher Visibility</Label>
@@ -59,9 +62,9 @@
<BorderPane GridPane.rowIndex="4" GridPane.columnIndex="1" GridPane.columnSpan="2"> <BorderPane GridPane.rowIndex="4" GridPane.columnIndex="1" GridPane.columnSpan="2">
<left> <left>
<HBox prefWidth="210"> <HBox prefWidth="210">
<JFXTextField fx:id="txtWidth" promptText="800" prefWidth="100"/> <JFXTextField fx:id="txtWidth" promptText="800" prefWidth="100" />
<Label>x</Label> <Label>x</Label>
<JFXTextField fx:id="txtHeight" promptText="480" prefWidth="100"/> <JFXTextField fx:id="txtHeight" promptText="480" prefWidth="100" />
</HBox> </HBox>
</left> </left>
<right> <right>
@@ -75,36 +78,17 @@
<JFXButton GridPane.rowIndex="0" GridPane.columnIndex="2" text="Explore" onMouseClicked="#onExploreJavaDir" /> <JFXButton GridPane.rowIndex="0" GridPane.columnIndex="2" text="Explore" onMouseClicked="#onExploreJavaDir" />
</GridPane> </GridPane>
<VBox fx:id="advancedSettingsPane" style="-fx-padding-bottom: 10; -fx-background-color: white; " spacing="30"> <HBox alignment="CENTER">
<JFXTextField labelFloat="true" promptText="JVM Args" styleClass="fit-width" fx:id="txtJVMArgs" maxWidth="Infinity"/> <JFXButton text="Show advanced settings" onMouseClicked="#onShowAdvanced" />
<JFXTextField labelFloat="true" promptText="Game Args" styleClass="fit-width" fx:id="txtGameArgs" maxWidth="Infinity"/> </HBox>
<JFXTextField labelFloat="true" promptText="Metaspace" styleClass="fit-width" fx:id="txtMetaspace" maxWidth="Infinity"/> <VBox fx:id="advancedSettingsPane" spacing="30">
<JFXTextField labelFloat="true" promptText="Wrapper Launcher(like optirun)" styleClass="fit-width" fx:id="txtWrapper" maxWidth="Infinity"/> <JFXTextField labelFloat="true" promptText="JVM Args" styleClass="fit-width" fx:id="txtJVMArgs" prefWidth="${advancedSettingsPane.width}" />
<JFXTextField labelFloat="true" promptText="Pre-calling command" styleClass="fit-width" fx:id="txtPrecallingCommand" maxWidth="Infinity"/> <JFXTextField labelFloat="true" promptText="Game Args" styleClass="fit-width" fx:id="txtGameArgs" prefWidth="${advancedSettingsPane.width}" />
<JFXTextField labelFloat="true" promptText="Server IP" styleClass="fit-width" fx:id="txtServerIP" maxWidth="Infinity"/> <JFXTextField labelFloat="true" promptText="Metaspace" styleClass="fit-width" fx:id="txtMetaspace" prefWidth="${advancedSettingsPane.width}" />
<JFXTextField labelFloat="true" promptText="Wrapper Launcher(like optirun)" styleClass="fit-width" fx:id="txtWrapper" prefWidth="${advancedSettingsPane.width}" />
<JFXTextField labelFloat="true" promptText="Pre-calling command" styleClass="fit-width" fx:id="txtPrecallingCommand" prefWidth="${advancedSettingsPane.width}" />
<JFXTextField labelFloat="true" promptText="Server IP" styleClass="fit-width" fx:id="txtServerIP" prefWidth="${advancedSettingsPane.width}" />
</VBox> </VBox>
</VBox> </VBox>
</ScrollPane> </ScrollPane>
</center> </fx:root>
<top>
<JFXToolbar maxHeight="20" styleClass="jfx-tool-bar">
<leftItems>
<JFXButton fx:id="backButton" maxHeight="20" styleClass="toggle-icon3"
StackPane.alignment="CENTER_LEFT">
<graphic>
<fx:include source="/assets/svg/arrow-left.fxml" />
</graphic>
<StackPane.margin>
<Insets left="20" />
</StackPane.margin>
</JFXButton>
<Label fx:id="titleLabel" style="-fx-text-fill:WHITE; -fx-font-size: 20;"
StackPane.alignment="CENTER_LEFT"/>
</leftItems>
<rightItems>
</rightItems>
</JFXToolbar>
</top>
</BorderPane>
</StackPane>

View File

@@ -11,7 +11,7 @@
<VBox> <VBox>
<JFXToolbar fx:id="toolbar" styleClass="jfx-tool-bar"> <JFXToolbar fx:id="toolbar" styleClass="jfx-tool-bar">
<leftItems> <leftItems>
<JFXButton fx:id="closeButton" maxHeight="20" styleClass="toggle-icon3" <JFXButton maxHeight="20" styleClass="toggle-icon3"
StackPane.alignment="CENTER_RIGHT" onMouseClicked="#close"> StackPane.alignment="CENTER_RIGHT" onMouseClicked="#close">
<graphic> <graphic>
<fx:include source="/assets/svg/close.fxml"/> <fx:include source="/assets/svg/close.fxml"/>
@@ -31,7 +31,7 @@
</leftItems> </leftItems>
<rightItems> <rightItems>
<JFXButton fx:id="refreshButton" maxHeight="20" styleClass="toggle-icon3" disable="true" <JFXButton fx:id="refreshButton" maxHeight="20" styleClass="toggle-icon3" disable="true"
StackPane.alignment="CENTER_RIGHT"> StackPane.alignment="CENTER_RIGHT" onMouseClicked="#refrseh">
<graphic> <graphic>
<fx:include source="/assets/svg/refresh.fxml"/> <fx:include source="/assets/svg/refresh.fxml"/>
</graphic> </graphic>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -24,5 +24,5 @@ abstract class Account() {
@Throws(AuthenticationException::class) @Throws(AuthenticationException::class)
abstract fun logIn(proxy: Proxy = Proxy.NO_PROXY): AuthInfo abstract fun logIn(proxy: Proxy = Proxy.NO_PROXY): AuthInfo
abstract fun logOut() abstract fun logOut()
abstract fun toStorage(): Map<out Any, Any> abstract fun toStorage(): MutableMap<Any, Any>
} }

View File

@@ -0,0 +1,27 @@
/*
* 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.auth
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount
object Accounts {
val ACCOUNTS = mapOf(
"offline" to OfflineAccount,
"yggdrasil" to YggdrasilAccount
)
}

View File

@@ -35,13 +35,15 @@ class OfflineAccount private constructor(val uuid: String, override val username
// Offline account need not log out. // Offline account need not log out.
} }
override fun toStorage(): Map<Any, Any> { override fun toStorage(): MutableMap<Any, Any> {
return mapOf( return mutableMapOf(
"uuid" to uuid, "uuid" to uuid,
"username" to username "username" to username
) )
} }
override fun toString() = "OfflineAccount[username=$username,uuid=$uuid]"
companion object OfflineAccountFactory : AccountFactory<OfflineAccount> { companion object OfflineAccountFactory : AccountFactory<OfflineAccount> {
override fun fromUsername(username: String, password: String): OfflineAccount { override fun fromUsername(username: String, password: String): OfflineAccount {

View File

@@ -135,8 +135,8 @@ class YggdrasilAccount private constructor(override val username: String): Accou
selectedProfile = null selectedProfile = null
} }
override fun toStorage(): Map<out Any, Any> { override fun toStorage(): MutableMap<Any, Any> {
val result = HashMap<String, Any>() val result = HashMap<Any, Any>()
result[STORAGE_KEY_USER_NAME] = username result[STORAGE_KEY_USER_NAME] = username
if (userId != null) if (userId != null)
@@ -189,6 +189,8 @@ class YggdrasilAccount private constructor(override val username: String): Accou
} }
} }
override fun toString() = "YggdrasilAccount[username=$username]"
companion object YggdrasilAccountFactory : AccountFactory<YggdrasilAccount> { companion object YggdrasilAccountFactory : AccountFactory<YggdrasilAccount> {
private val GSON = GsonBuilder() private val GSON = GsonBuilder()
.registerTypeAdapter(GameProfile::class.java, GameProfile) .registerTypeAdapter(GameProfile::class.java, GameProfile)

View File

@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.launch
import org.jackhuang.hmcl.auth.AuthInfo import org.jackhuang.hmcl.auth.AuthInfo
import org.jackhuang.hmcl.game.* import org.jackhuang.hmcl.game.*
import org.jackhuang.hmcl.task.TaskResult
import org.jackhuang.hmcl.util.* import org.jackhuang.hmcl.util.*
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@@ -26,12 +27,12 @@ import java.util.*
import kotlin.concurrent.thread import kotlin.concurrent.thread
/** /**
* @param version A resolved version(calling [Version.resolve]) * @param versionId The version to be launched.
* @param account The user account * @param account The user account
* @param options The launching configuration * @param options The launching configuration
*/ */
open class DefaultLauncher(repository: GameRepository, version: Version, account: AuthInfo, options: LaunchOptions, listener: ProcessListener? = null, isDaemon: Boolean = true) open class DefaultLauncher(repository: GameRepository, versionId: String, account: AuthInfo, options: LaunchOptions, listener: ProcessListener? = null, isDaemon: Boolean = true)
: Launcher(repository, version, account, options, listener, isDaemon) { : Launcher(repository, versionId, account, options, listener, isDaemon) {
protected val native: File by lazy { repository.getNativeDirectory(version.id) } protected val native: File by lazy { repository.getNativeDirectory(version.id) }
@@ -234,7 +235,7 @@ open class DefaultLauncher(repository: GameRepository, version: Version, account
} }
builder.directory(repository.getRunDirectory(version.id)) builder.directory(repository.getRunDirectory(version.id))
.environment().put("APPDATA", options.gameDir.parent) .environment().put("APPDATA", options.gameDir.absoluteFile.parent)
val p = JavaProcess(builder.start(), rawCommandLine) val p = JavaProcess(builder.start(), rawCommandLine)
if (listener == null) if (listener == null)
startMonitors(p) startMonitors(p)
@@ -243,6 +244,14 @@ open class DefaultLauncher(repository: GameRepository, version: Version, account
return p return p
} }
fun launchAsync(): TaskResult<JavaProcess> {
return object : TaskResult<JavaProcess>() {
override fun execute() {
result = launch()
}
}
}
override fun makeLaunchScript(file: String): File { override fun makeLaunchScript(file: String): File {
val isWindows = OS.WINDOWS == OS.CURRENT_OS val isWindows = OS.WINDOWS == OS.CURRENT_OS
val scriptFile = File(file + (if (isWindows) ".bat" else ".sh")) val scriptFile = File(file + (if (isWindows) ".bat" else ".sh"))

View File

@@ -26,12 +26,13 @@ import java.io.File
abstract class Launcher( abstract class Launcher(
protected val repository: GameRepository, protected val repository: GameRepository,
protected val version: Version, protected val versionId: String,
protected val account: AuthInfo, protected val account: AuthInfo,
protected val options: LaunchOptions, protected val options: LaunchOptions,
protected val listener: ProcessListener? = null, protected val listener: ProcessListener? = null,
protected val isDaemon: Boolean = true) { protected val isDaemon: Boolean = true) {
val version: Version = repository.getVersion(versionId).resolve(repository)
abstract val rawCommandLine: List<String> abstract val rawCommandLine: List<String>
abstract fun launch(): JavaProcess abstract fun launch(): JavaProcess

View File

@@ -17,7 +17,7 @@
*/ */
package org.jackhuang.hmcl.task package org.jackhuang.hmcl.task
private class CoupleTask<P: Task>(private val pred: P, private val succ: Task.(P) -> Task?, override val reliant: Boolean) : Task() { internal class CoupleTask<P: Task>(private val pred: P, private val succ: Task.(P) -> Task?, override val reliant: Boolean) : Task() {
override val hidden: Boolean = true override val hidden: Boolean = true
override val dependents: Collection<Task> = listOf(pred) override val dependents: Collection<Task> = listOf(pred)
@@ -30,11 +30,12 @@ private class CoupleTask<P: Task>(private val pred: P, private val succ: Task.(P
} }
} }
infix fun Task.then(b: Task): Task = CoupleTask(this, { b }, true)
/** /**
* @param b A runnable that decides what to do next, You can also do something here. * @param b A runnable that decides what to do next, You can also do something here.
*/ */
infix fun <T: Task> T.then(b: Task.(T) -> Task?): Task = CoupleTask(this, b, true) infix fun <T: Task> T.then(b: Task.(T) -> Task?): Task = CoupleTask(this, b, true)
infix fun Task.with(b: Task): Task = CoupleTask(this, { b }, false) /**
* @param b A runnable that decides what to do next, You can also do something here.
*/
infix fun <T: Task> T.with(b: Task.(T) -> Task?): Task = CoupleTask(this, b, false)

View File

@@ -23,16 +23,17 @@ import java.util.concurrent.atomic.AtomicReference
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
interface Scheduler { interface Scheduler {
fun schedule(block: () -> Unit): Future<*>? = schedule(Callable { block() })
fun schedule(block: Callable<Unit>): Future<*>? fun schedule(block: Callable<Unit>): Future<*>?
companion object Schedulers { companion object Schedulers {
val JAVAFX: Scheduler = SchedulerImpl(Platform::runLater) val JAVAFX: Scheduler = SchedulerImpl(Platform::runLater)
val SWING: Scheduler = SchedulerImpl(SwingUtilities::invokeLater) val SWING: Scheduler = SchedulerImpl(SwingUtilities::invokeLater)
private class SchedulerImpl(val executor: (() -> Unit) -> Unit) : Scheduler { private class SchedulerImpl(val executor: (Runnable) -> Unit) : Scheduler {
override fun schedule(block: Callable<Unit>): Future<*>? { override fun schedule(block: Callable<Unit>): Future<*>? {
val latch = CountDownLatch(1) val latch = CountDownLatch(1)
val wrapper = AtomicReference<Exception>() val wrapper = AtomicReference<Exception>()
executor { executor.invoke(Runnable {
try { try {
block.call() block.call()
} catch (e: Exception) { } catch (e: Exception) {
@@ -40,7 +41,7 @@ interface Scheduler {
} finally { } finally {
latch.countDown() latch.countDown()
} }
} })
return object : Future<Unit> { return object : Future<Unit> {
override fun get(timeout: Long, unit: TimeUnit) { override fun get(timeout: Long, unit: TimeUnit) {
latch.await(timeout, unit) latch.await(timeout, unit)
@@ -66,7 +67,9 @@ interface Scheduler {
} }
val DEFAULT = NEW_THREAD val DEFAULT = NEW_THREAD
private val CACHED_EXECUTOR: ExecutorService by lazy { private val CACHED_EXECUTOR: ExecutorService by lazy {
Executors.newCachedThreadPool() ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
SynchronousQueue<Runnable>());
} }
private val IO_EXECUTOR: ExecutorService by lazy { private val IO_EXECUTOR: ExecutorService by lazy {

View File

@@ -57,6 +57,8 @@ abstract class Task {
abstract fun execute() abstract fun execute()
infix fun parallel(couple: Task): Task = ParallelTask(this, couple) infix fun parallel(couple: Task): Task = ParallelTask(this, couple)
infix fun then(b: Task): Task = CoupleTask(this, { b }, true)
infix fun with(b: Task): Task = CoupleTask(this, { b }, false)
/** /**
* The collection of sub-tasks that should execute before this task running. * The collection of sub-tasks that should execute before this task running.
@@ -112,18 +114,18 @@ abstract class Task {
fun executor() = TaskExecutor().submit(this) fun executor() = TaskExecutor().submit(this)
fun subscribe(subscriber: Task) { fun subscribe(subscriber: Task) = executor().apply {
executor().submit(subscriber).start() submit(subscriber).start()
} }
fun subscribe(scheduler: Scheduler = Scheduler.DEFAULT, closure: () -> Unit) = subscribe(Task.of(closure, scheduler)) fun subscribe(scheduler: Scheduler = Scheduler.DEFAULT, closure: () -> Unit) = subscribe(Task.of(scheduler, closure))
override fun toString(): String { override fun toString(): String {
return title return title
} }
companion object { companion object {
fun of(closure: () -> Unit, scheduler: Scheduler = Scheduler.DEFAULT): Task = SimpleTask(closure, scheduler) fun of(scheduler: Scheduler = Scheduler.DEFAULT, closure: () -> Unit): Task = SimpleTask(closure, scheduler)
fun <V> of(callable: Callable<V>): TaskResult<V> = TaskCallable(callable) fun <V> of(callable: Callable<V>): TaskResult<V> = TaskCallable(callable)
} }
} }

View File

@@ -29,7 +29,7 @@ class TaskExecutor() {
var canceled = false var canceled = false
private set private set
private val totTask = AtomicInteger(0) val totTask = AtomicInteger(0)
private val taskQueue = ConcurrentLinkedQueue<Task>() private val taskQueue = ConcurrentLinkedQueue<Task>()
private val workerQueue = ConcurrentLinkedQueue<Future<*>>() private val workerQueue = ConcurrentLinkedQueue<Future<*>>()
@@ -47,9 +47,10 @@ class TaskExecutor() {
* Start the subscription and run all registered tasks asynchronously. * Start the subscription and run all registered tasks asynchronously.
*/ */
fun start() { fun start() {
thread { workerQueue.add(Scheduler.Schedulers.NEW_THREAD.schedule(Callable {
totTask.addAndGet(taskQueue.size) totTask.addAndGet(taskQueue.size)
while (!taskQueue.isEmpty() && !canceled) { while (!taskQueue.isEmpty()) {
if (canceled) break
val task = taskQueue.poll() val task = taskQueue.poll()
if (task != null) { if (task != null) {
val future = task.scheduler.schedule(Callable { executeTask(task); Unit }) val future = task.scheduler.schedule(Callable { executeTask(task); Unit })
@@ -61,9 +62,9 @@ class TaskExecutor() {
} }
} }
} }
if (!canceled) if (canceled || Thread.interrupted())
taskListener?.onTerminate() taskListener?.onTerminate()
} }))
} }
/** /**
@@ -91,9 +92,11 @@ class TaskExecutor() {
if (future != null) if (future != null)
workerQueue.add(future) workerQueue.add(future)
} }
if (canceled)
return false
try { try {
counter.await() counter.await()
return success.get() return success.get() && !canceled
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
Thread.currentThread().interrupt() Thread.currentThread().interrupt()
// Once interrupted, we are aborting the subscription. // Once interrupted, we are aborting the subscription.
@@ -110,9 +113,10 @@ class TaskExecutor() {
LOG.fine("Executing task: ${t.title}") LOG.fine("Executing task: ${t.title}")
taskListener?.onReady(t) taskListener?.onReady(t)
val doDependentsSucceeded = executeTasks(t.dependents) val doDependentsSucceeded = executeTasks(t.dependents)
var flag = false var flag = false
try { try {
if (!doDependentsSucceeded && t.reliant) if (!doDependentsSucceeded && t.reliant || canceled)
throw SilentException() throw SilentException()
t.execute() t.execute()

View File

@@ -52,6 +52,7 @@ data class JavaVersion internal constructor(
} }
} }
@Throws(IOException::class)
fun fromExecutable(file: File): JavaVersion { fun fromExecutable(file: File): JavaVersion {
var platform = Platform.BIT_32 var platform = Platform.BIT_32
var version: String? = null var version: String? = null
@@ -66,6 +67,7 @@ data class JavaVersion internal constructor(
platform = Platform.BIT_64 platform = Platform.BIT_64
} }
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
Thread.currentThread().interrupt()
throw IOException("Java process is interrupted", e) throw IOException("Java process is interrupted", e)
} }
val thisVersion = version ?: throw IOException("Java version not matched") val thisVersion = version ?: throw IOException("Java version not matched")

View File

@@ -49,9 +49,9 @@ enum class OS {
ReflectionHelper.get<Long>(ManagementFactory.getOperatingSystemMXBean(), "getTotalPhysicalMemorySize") ?: 1024L ReflectionHelper.get<Long>(ManagementFactory.getOperatingSystemMXBean(), "getTotalPhysicalMemorySize") ?: 1024L
} }
val SUGGESTED_MEMORY: Long by lazy { val SUGGESTED_MEMORY: Int by lazy {
val memory = TOTAL_MEMORY / 1024 / 1024 / 4 val memory = TOTAL_MEMORY / 1024 / 1024 / 4
Math.round(1.0 * memory / 128.0) * 128 (Math.round(1.0 * memory / 128.0) * 128).toInt()
} }
val PATH_SEPARATOR: String = File.pathSeparator val PATH_SEPARATOR: String = File.pathSeparator

View File

@@ -19,7 +19,7 @@ package org.jackhuang.hmcl
import org.jackhuang.hmcl.auth.OfflineAccount import org.jackhuang.hmcl.auth.OfflineAccount
import org.jackhuang.hmcl.download.DefaultDependencyManager import org.jackhuang.hmcl.download.DefaultDependencyManager
import org.jackhuang.hmcl.download.liteloader.LiteLoaderVersionList import org.jackhuang.hmcl.download.LiteLoaderVersionList
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider
import org.jackhuang.hmcl.download.MojangDownloadProvider import org.jackhuang.hmcl.download.MojangDownloadProvider
import org.jackhuang.hmcl.game.DefaultGameRepository import org.jackhuang.hmcl.game.DefaultGameRepository
@@ -50,7 +50,7 @@ class Test {
fun launch() { fun launch() {
val launcher = DefaultLauncher( val launcher = DefaultLauncher(
repository = repository, repository = repository,
version = repository.getVersion("test"), versionId = "test",
account = OfflineAccount.fromUsername("player007").logIn(), account = OfflineAccount.fromUsername("player007").logIn(),
options = LaunchOptions(gameDir = repository.baseDirectory), options = LaunchOptions(gameDir = repository.baseDirectory),
listener = object : ProcessListener { listener = object : ProcessListener {

View File

@@ -576,8 +576,8 @@ Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published GNU General Public License, you may choose any version ever published
by the Free Software Foundation. by the Free Software Foundation.
If the Program specifies that a proxy can decide which future If the Program specifies that a PROXY can decide which future
versions of the GNU General Public License can be used, that proxy's versions of the GNU General Public License can be used, that PROXY's
public statement of acceptance of a version permanently authorizes you public statement of acceptance of a version permanently authorizes you
to choose that version for the Program. to choose that version for the Program.

Binary file not shown.