Tooltip reflection for advanced options
This commit is contained in:
@@ -48,8 +48,12 @@ import java.util.Arrays
|
|||||||
*/
|
*/
|
||||||
@Throws(IOException::class, JsonParseException::class)
|
@Throws(IOException::class, JsonParseException::class)
|
||||||
fun readHMCLModpackManifest(f: File): Modpack {
|
fun readHMCLModpackManifest(f: File): Modpack {
|
||||||
val json = readTextFromZipFile(f, "modpack.json")
|
val manifestJson = readTextFromZipFile(f, "modpack.json")
|
||||||
return GSON.fromJson<Modpack>(json)?.copy(manifest = HMCLModpackManifest) ?: throw JsonParseException("`modpack.json` not found. Not a valid CurseForge modpack.")
|
val manifest = GSON.fromJson<Modpack>(manifestJson) ?: throw JsonParseException("`modpack.json` not found. Not a valid HMCL modpack.")
|
||||||
|
val gameJson = readTextFromZipFile(f, "minecraft/pack.json")
|
||||||
|
val game = GSON.fromJson<Version>(gameJson) ?: throw JsonParseException("`minecraft/pack.json` not found. Not a valid HMCL modpack.")
|
||||||
|
return if (game.jar == null) manifest.copy(manifest = HMCLModpackManifest)
|
||||||
|
else manifest.copy(manifest = HMCLModpackManifest, gameVersion = game.jar!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
object HMCLModpackManifest
|
object HMCLModpackManifest
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ fun readModpackManifest(f: File): Modpack {
|
|||||||
name = manifest.name,
|
name = manifest.name,
|
||||||
version = manifest.version,
|
version = manifest.version,
|
||||||
author = manifest.author,
|
author = manifest.author,
|
||||||
|
gameVersion = manifest.minecraft.gameVersion,
|
||||||
description = readTextFromZipFileQuietly(f, "modlist.html") ?: "No description",
|
description = readTextFromZipFileQuietly(f, "modlist.html") ?: "No description",
|
||||||
manifest = manifest)
|
manifest = manifest)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -40,8 +40,7 @@ import javafx.scene.layout.Region
|
|||||||
import javafx.scene.shape.Rectangle
|
import javafx.scene.shape.Rectangle
|
||||||
import javafx.util.Duration
|
import javafx.util.Duration
|
||||||
import org.jackhuang.hmcl.Main
|
import org.jackhuang.hmcl.Main
|
||||||
import org.jackhuang.hmcl.util.LOG
|
import org.jackhuang.hmcl.util.*
|
||||||
import org.jackhuang.hmcl.util.OS
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
@@ -228,4 +227,15 @@ fun inputDialog(title: String, contentText: String, headerText: String? = null,
|
|||||||
this.title = title
|
this.title = title
|
||||||
this.headerText = headerText
|
this.headerText = headerText
|
||||||
this.contentText = contentText
|
this.contentText = contentText
|
||||||
}.showAndWait()
|
}.showAndWait()
|
||||||
|
|
||||||
|
fun Node.installTooltip(openDelay: Double = 1000.0, visibleDelay: Double = 5000.0, closeDelay: Double = 200.0, tooltip: Tooltip) {
|
||||||
|
try {
|
||||||
|
Class.forName("javafx.scene.control.Tooltip\$TooltipBehavior")
|
||||||
|
.construct(Duration(openDelay), Duration(visibleDelay), Duration(closeDelay), false)!!
|
||||||
|
.call("install", this, tooltip)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
LOG.log(Level.SEVERE, "Cannot install tooltip by reflection", e)
|
||||||
|
Tooltip.install(this, tooltip)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import javafx.beans.property.SimpleStringProperty
|
|||||||
import javafx.beans.property.StringProperty
|
import javafx.beans.property.StringProperty
|
||||||
import javafx.fxml.FXML
|
import javafx.fxml.FXML
|
||||||
import javafx.scene.control.Alert
|
import javafx.scene.control.Alert
|
||||||
|
import javafx.scene.control.Tooltip
|
||||||
import javafx.scene.layout.StackPane
|
import javafx.scene.layout.StackPane
|
||||||
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask
|
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask
|
||||||
import org.jackhuang.hmcl.i18n
|
import org.jackhuang.hmcl.i18n
|
||||||
@@ -52,6 +53,9 @@ class VersionPage : StackPane(), DecoratorPage {
|
|||||||
|
|
||||||
browsePopup = JFXPopup(browseList)
|
browsePopup = JFXPopup(browseList)
|
||||||
managementPopup = JFXPopup(managementList)
|
managementPopup = JFXPopup(managementList)
|
||||||
|
|
||||||
|
btnBrowseMenu.installTooltip(openDelay = 0.0, closeDelay = 0.0, tooltip = Tooltip(i18n("settings.explore")))
|
||||||
|
btnManagementMenu.installTooltip(openDelay = 0.0, closeDelay = 0.0, tooltip = Tooltip(i18n("settings.manage")))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun load(id: String, profile: Profile) {
|
fun load(id: String, profile: Profile) {
|
||||||
|
|||||||
2
HMCL/src/main/resources/assets/svg/export.fxml
Normal file
2
HMCL/src/main/resources/assets/svg/export.fxml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<javafx.scene.shape.SVGPath fill="black" content="M23,12L19,8V11H10V13H19V16M1,18V6C1,4.89 1.9,4 3,4H15A2,2 0 0,1 17,6V9H15V6H3V18H15V15H17V18A2,2 0 0,1 15,20H3A2,2 0 0,1 1,18Z" />
|
||||||
@@ -53,8 +53,6 @@ class CurseForgeModpackManifest @JvmOverloads constructor(
|
|||||||
check(manifestType == MINECRAFT_MODPACK, { "Only support Minecraft modpack" })
|
check(manifestType == MINECRAFT_MODPACK, { "Only support Minecraft modpack" })
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toModpack() = Modpack(name = name, author = author, version = version, description = "No description")
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val MINECRAFT_MODPACK = "minecraftModpack"
|
val MINECRAFT_MODPACK = "minecraftModpack"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ enum class OS {
|
|||||||
* The total memory/MB this computer have.
|
* The total memory/MB this computer have.
|
||||||
*/
|
*/
|
||||||
val TOTAL_MEMORY: Int by lazy {
|
val TOTAL_MEMORY: Int by lazy {
|
||||||
val bytes = ReflectionHelper.invoke<Long>(ManagementFactory.getOperatingSystemMXBean(), "getTotalPhysicalMemorySize")
|
val bytes = ManagementFactory.getOperatingSystemMXBean().call("getTotalPhysicalMemorySize") as? Long?
|
||||||
if (bytes == null) 1024
|
if (bytes == null) 1024
|
||||||
else (bytes / 1024 / 1024).toInt()
|
else (bytes / 1024 / 1024).toInt()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,61 +19,125 @@ package org.jackhuang.hmcl.util
|
|||||||
|
|
||||||
import sun.misc.Unsafe
|
import sun.misc.Unsafe
|
||||||
import java.lang.reflect.AccessibleObject
|
import java.lang.reflect.AccessibleObject
|
||||||
|
import java.lang.reflect.Executable
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
import java.security.AccessController
|
import java.security.AccessController
|
||||||
import java.security.PrivilegedExceptionAction
|
import java.security.PrivilegedExceptionAction
|
||||||
|
|
||||||
object ReflectionHelper {
|
private val unsafe: Unsafe = AccessController.doPrivileged(PrivilegedExceptionAction {
|
||||||
|
val theUnsafe = Unsafe::class.java.getDeclaredField("theUnsafe")
|
||||||
|
theUnsafe.isAccessible = true
|
||||||
|
theUnsafe.get(null) as Unsafe
|
||||||
|
})
|
||||||
|
private val objectFieldOffset = unsafe.objectFieldOffset(
|
||||||
|
AccessibleObject::class.java.getDeclaredField("override"))
|
||||||
|
|
||||||
private lateinit var unsafe: Unsafe
|
private fun AccessibleObject.setAccessibleForcibly() =
|
||||||
private var objectFieldOffset = 0L
|
unsafe.putBoolean(this, objectFieldOffset, true)
|
||||||
|
|
||||||
init {
|
fun getMethod(obj: Any, methodName: String): Method? =
|
||||||
|
getMethod(obj.javaClass, methodName)
|
||||||
|
|
||||||
|
fun getMethod(cls: Class<*>, methodName: String): Method? =
|
||||||
try {
|
try {
|
||||||
unsafe = AccessController.doPrivileged(PrivilegedExceptionAction {
|
cls.getDeclaredMethod(methodName).apply { setAccessibleForcibly() }
|
||||||
val theUnsafe = Unsafe::class.java.getDeclaredField("theUnsafe")
|
|
||||||
theUnsafe.isAccessible = true
|
|
||||||
theUnsafe.get(null) as Unsafe
|
|
||||||
})
|
|
||||||
val overrideField = AccessibleObject::class.java.getDeclaredField("override")
|
|
||||||
objectFieldOffset = unsafe.objectFieldOffset(overrideField)
|
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call a field, method or constructor by reflection.
|
||||||
|
*
|
||||||
|
* @param name the field or method name of [clazz], "new" if you are looking for a constructor.
|
||||||
|
* @param args the arguments of the method, empty if you are looking for a field or non-argument method.
|
||||||
|
*/
|
||||||
|
fun Any.call(name: String, vararg args: Any?): Any? {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return javaClass.call(name, this, *args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call a constructor by reflection.
|
||||||
|
*
|
||||||
|
* @param args the arguments of the method, empty if you are looking for a field or non-argument method.
|
||||||
|
*/
|
||||||
|
fun Class<*>.construct(vararg args: Any?) = call("new", null, *args)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call a field, method or constructor by reflection.
|
||||||
|
*
|
||||||
|
* @param name the field or method name of [clazz], "new" if you are looking for a constructor.
|
||||||
|
* @param obj null for constructors or static/object methods/fields.
|
||||||
|
* @param args the arguments of the method, empty if you are looking for a field or non-argument method.
|
||||||
|
*/
|
||||||
|
fun Class<*>.call(name: String, obj: Any? = null, vararg args: Any?): Any? {
|
||||||
|
try {
|
||||||
|
if (args.isEmpty())
|
||||||
|
try {
|
||||||
|
return getDeclaredField(name).get(obj)
|
||||||
|
} catch(ignored: NoSuchFieldException) {
|
||||||
|
}
|
||||||
|
if (name == "new")
|
||||||
|
declaredConstructors.forEach {
|
||||||
|
if (checkParameter(it, *args)) return it.newInstance(*args)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return forMethod(name, *args)!!.invoke(obj, *args)
|
||||||
|
throw RuntimeException()
|
||||||
|
} catch(e: Exception) {
|
||||||
|
throw IllegalArgumentException("Cannot find `$name` in Class `${this.name}`, please check your code.", e)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setAccessible(obj: AccessibleObject) =
|
fun Class<*>.forMethod(name: String, vararg args: Any?): Method? =
|
||||||
unsafe.putBoolean(obj, objectFieldOffset, true)
|
declaredMethods.filter { it.name == name }.filter { checkParameter(it, *args) }.firstOrNull()
|
||||||
|
|
||||||
fun <T> get(obj: Any, fieldName: String): T? =
|
fun checkParameter(exec: Executable, vararg args: Any?): Boolean {
|
||||||
get(obj.javaClass, obj, fieldName)
|
val cArgs = exec.parameterTypes
|
||||||
|
if (args.size == cArgs.size) {
|
||||||
|
for (i in 0 until args.size) {
|
||||||
|
val arg = args[i]
|
||||||
|
// primitive variable cannot be null
|
||||||
|
if (if (arg != null) !isInstance(cArgs[i], arg) else cArgs[i].isPrimitive)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
exec.setAccessibleForcibly()
|
||||||
|
return true
|
||||||
|
} else
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
fun isInstance(superClass: Class<*>, obj: Any): Boolean {
|
||||||
fun <T, S> get(cls: Class<out S>, obj: S, fieldName: String): T? =
|
if (superClass.isInstance(obj)) return true
|
||||||
try {
|
else if (PRIMITIVES[superClass.name] == obj.javaClass) return true
|
||||||
val method = cls.getDeclaredField(fieldName)
|
return false
|
||||||
setAccessible(method)
|
}
|
||||||
method.get(obj) as T
|
|
||||||
} catch (ex: Throwable) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
fun isInstance(superClass: Class<*>, clazz: Class<*>): Boolean {
|
||||||
fun <T> invoke(obj: Any, methodName: String): T? =
|
for (i in clazz.interfaces)
|
||||||
try {
|
if (isInstance(superClass, i) || PRIMITIVES[superClass.name] == clazz)
|
||||||
val method = obj.javaClass.getDeclaredMethod(methodName)
|
return true
|
||||||
setAccessible(method)
|
return isSubClass(superClass, clazz)
|
||||||
method.invoke(obj) as T?
|
}
|
||||||
} catch (ex: Throwable) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getMethod(obj: Any, methodName: String): Method? =
|
fun isSubClass(superClass: Class<*>, clazz: Class<*>): Boolean {
|
||||||
getMethod(obj.javaClass, methodName)
|
var clz: Class<*>? = clazz
|
||||||
|
do {
|
||||||
|
if (superClass == clz) return true
|
||||||
|
clz = clz?.superclass
|
||||||
|
} while (clz != null)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
fun getMethod(cls: Class<*>, methodName: String): Method? =
|
fun <T> Class<T>.objectInstance() = call("INSTANCE")
|
||||||
try {
|
|
||||||
cls.getDeclaredMethod(methodName).apply { ReflectionHelper.setAccessible(this) }
|
val PRIMITIVES = mapOf(
|
||||||
} catch (ex: Throwable) {
|
"byte" to java.lang.Byte::class.java,
|
||||||
null
|
"short" to java.lang.Short::class.java,
|
||||||
}
|
"int" to java.lang.Integer::class.java,
|
||||||
}
|
"long" to java.lang.Long::class.java,
|
||||||
|
"char" to java.lang.Character::class.java,
|
||||||
|
"float" to java.lang.Float::class.java,
|
||||||
|
"double" to java.lang.Double::class.java,
|
||||||
|
"boolean" to java.lang.Boolean::class.java
|
||||||
|
)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ group 'org.jackhuang'
|
|||||||
version '3.0'
|
version '3.0'
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.1.4'
|
ext.kotlin_version = '1.1.4-2'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|||||||
Reference in New Issue
Block a user