Tooltip reflection for advanced options

This commit is contained in:
huangyuhui
2017-08-23 12:07:08 +08:00
parent 86cef86fc9
commit 2e3d9c22e3
9 changed files with 134 additions and 51 deletions

View File

@@ -48,8 +48,12 @@ import java.util.Arrays
*/
@Throws(IOException::class, JsonParseException::class)
fun readHMCLModpackManifest(f: File): Modpack {
val json = readTextFromZipFile(f, "modpack.json")
return GSON.fromJson<Modpack>(json)?.copy(manifest = HMCLModpackManifest) ?: throw JsonParseException("`modpack.json` not found. Not a valid CurseForge modpack.")
val manifestJson = readTextFromZipFile(f, "modpack.json")
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

View File

@@ -29,6 +29,7 @@ fun readModpackManifest(f: File): Modpack {
name = manifest.name,
version = manifest.version,
author = manifest.author,
gameVersion = manifest.minecraft.gameVersion,
description = readTextFromZipFileQuietly(f, "modlist.html") ?: "No description",
manifest = manifest)
} catch (e: Exception) {

View File

@@ -40,8 +40,7 @@ import javafx.scene.layout.Region
import javafx.scene.shape.Rectangle
import javafx.util.Duration
import org.jackhuang.hmcl.Main
import org.jackhuang.hmcl.util.LOG
import org.jackhuang.hmcl.util.OS
import org.jackhuang.hmcl.util.*
import java.io.File
import java.io.IOException
import java.util.logging.Level
@@ -229,3 +228,14 @@ fun inputDialog(title: String, contentText: String, headerText: String? = null,
this.headerText = headerText
this.contentText = contentText
}.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)
}
}

View File

@@ -24,6 +24,7 @@ import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
import javafx.fxml.FXML
import javafx.scene.control.Alert
import javafx.scene.control.Tooltip
import javafx.scene.layout.StackPane
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask
import org.jackhuang.hmcl.i18n
@@ -52,6 +53,9 @@ class VersionPage : StackPane(), DecoratorPage {
browsePopup = JFXPopup(browseList)
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) {

View 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" />

View File

@@ -53,8 +53,6 @@ class CurseForgeModpackManifest @JvmOverloads constructor(
check(manifestType == MINECRAFT_MODPACK, { "Only support Minecraft modpack" })
}
fun toModpack() = Modpack(name = name, author = author, version = version, description = "No description")
companion object {
val MINECRAFT_MODPACK = "minecraftModpack"
}

View File

@@ -65,7 +65,7 @@ enum class OS {
* The total memory/MB this computer have.
*/
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
else (bytes / 1024 / 1024).toInt()
}

View File

@@ -19,61 +19,125 @@ package org.jackhuang.hmcl.util
import sun.misc.Unsafe
import java.lang.reflect.AccessibleObject
import java.lang.reflect.Executable
import java.lang.reflect.Method
import java.security.AccessController
import java.security.PrivilegedExceptionAction
object ReflectionHelper {
private lateinit var unsafe: Unsafe
private var objectFieldOffset = 0L
init {
try {
unsafe = AccessController.doPrivileged(PrivilegedExceptionAction {
private val unsafe: Unsafe = AccessController.doPrivileged(PrivilegedExceptionAction {
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) {
}
}
})
private val objectFieldOffset = unsafe.objectFieldOffset(
AccessibleObject::class.java.getDeclaredField("override"))
private fun setAccessible(obj: AccessibleObject) =
unsafe.putBoolean(obj, objectFieldOffset, true)
private fun AccessibleObject.setAccessibleForcibly() =
unsafe.putBoolean(this, objectFieldOffset, true)
fun <T> get(obj: Any, fieldName: String): T? =
get(obj.javaClass, obj, fieldName)
@Suppress("UNCHECKED_CAST")
fun <T, S> get(cls: Class<out S>, obj: S, fieldName: String): T? =
try {
val method = cls.getDeclaredField(fieldName)
setAccessible(method)
method.get(obj) as T
} catch (ex: Throwable) {
null
}
@Suppress("UNCHECKED_CAST")
fun <T> invoke(obj: Any, methodName: String): T? =
try {
val method = obj.javaClass.getDeclaredMethod(methodName)
setAccessible(method)
method.invoke(obj) as T?
} catch (ex: Throwable) {
null
}
fun getMethod(obj: Any, methodName: String): Method? =
fun getMethod(obj: Any, methodName: String): Method? =
getMethod(obj.javaClass, methodName)
fun getMethod(cls: Class<*>, methodName: String): Method? =
fun getMethod(cls: Class<*>, methodName: String): Method? =
try {
cls.getDeclaredMethod(methodName).apply { ReflectionHelper.setAccessible(this) }
cls.getDeclaredMethod(methodName).apply { setAccessibleForcibly() }
} 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)
}
}
fun Class<*>.forMethod(name: String, vararg args: Any?): Method? =
declaredMethods.filter { it.name == name }.filter { checkParameter(it, *args) }.firstOrNull()
fun checkParameter(exec: Executable, vararg args: Any?): Boolean {
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
}
fun isInstance(superClass: Class<*>, obj: Any): Boolean {
if (superClass.isInstance(obj)) return true
else if (PRIMITIVES[superClass.name] == obj.javaClass) return true
return false
}
fun isInstance(superClass: Class<*>, clazz: Class<*>): Boolean {
for (i in clazz.interfaces)
if (isInstance(superClass, i) || PRIMITIVES[superClass.name] == clazz)
return true
return isSubClass(superClass, clazz)
}
fun isSubClass(superClass: Class<*>, clazz: Class<*>): Boolean {
var clz: Class<*>? = clazz
do {
if (superClass == clz) return true
clz = clz?.superclass
} while (clz != null)
return false
}
fun <T> Class<T>.objectInstance() = call("INSTANCE")
val PRIMITIVES = mapOf(
"byte" to java.lang.Byte::class.java,
"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
)

View File

@@ -20,7 +20,7 @@ group 'org.jackhuang'
version '3.0'
buildscript {
ext.kotlin_version = '1.1.4'
ext.kotlin_version = '1.1.4-2'
repositories {
mavenCentral()