From 2e3d9c22e31b21e120eb57f3b4eabdf7d439c948 Mon Sep 17 00:00:00 2001 From: huangyuhui Date: Wed, 23 Aug 2017 12:07:08 +0800 Subject: [PATCH] Tooltip reflection for advanced options --- .../org/jackhuang/hmcl/game/HMCLModpack.kt | 8 +- .../org/jackhuang/hmcl/game/ModpackHelper.kt | 1 + .../kotlin/org/jackhuang/hmcl/ui/FXUtils.kt | 16 +- .../org/jackhuang/hmcl/ui/VersionPage.kt | 4 + .../src/main/resources/assets/svg/export.fxml | 2 + .../jackhuang/hmcl/mod/CurseForgeModpack.kt | 2 - .../main/kotlin/org/jackhuang/hmcl/util/OS.kt | 2 +- .../jackhuang/hmcl/util/ReflectionHelper.kt | 148 +++++++++++++----- build.gradle | 2 +- 9 files changed, 134 insertions(+), 51 deletions(-) create mode 100644 HMCL/src/main/resources/assets/svg/export.fxml diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/HMCLModpack.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/HMCLModpack.kt index 37f954836..28d2836a6 100644 --- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/HMCLModpack.kt +++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/HMCLModpack.kt @@ -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(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(manifestJson) ?: throw JsonParseException("`modpack.json` not found. Not a valid HMCL modpack.") + val gameJson = readTextFromZipFile(f, "minecraft/pack.json") + val game = GSON.fromJson(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 diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/ModpackHelper.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/ModpackHelper.kt index 16a23014f..8ed72d12b 100644 --- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/ModpackHelper.kt +++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/ModpackHelper.kt @@ -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) { diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/FXUtils.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/FXUtils.kt index d7e4761c4..f16aa33e8 100644 --- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/FXUtils.kt +++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/FXUtils.kt @@ -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 @@ -228,4 +227,15 @@ fun inputDialog(title: String, contentText: String, headerText: String? = null, this.title = title this.headerText = headerText this.contentText = contentText - }.showAndWait() \ No newline at end of file + }.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) + } +} \ No newline at end of file diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/VersionPage.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/VersionPage.kt index 3f0b61da8..b81a0d1e8 100644 --- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/VersionPage.kt +++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/VersionPage.kt @@ -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) { diff --git a/HMCL/src/main/resources/assets/svg/export.fxml b/HMCL/src/main/resources/assets/svg/export.fxml new file mode 100644 index 000000000..01c406ab2 --- /dev/null +++ b/HMCL/src/main/resources/assets/svg/export.fxml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/mod/CurseForgeModpack.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/mod/CurseForgeModpack.kt index 9609ea1dd..c63a544ab 100644 --- a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/mod/CurseForgeModpack.kt +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/mod/CurseForgeModpack.kt @@ -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" } diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/OS.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/OS.kt index 00d98b9ac..23cb9580a 100644 --- a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/OS.kt +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/OS.kt @@ -65,7 +65,7 @@ enum class OS { * The total memory/MB this computer have. */ val TOTAL_MEMORY: Int by lazy { - val bytes = ReflectionHelper.invoke(ManagementFactory.getOperatingSystemMXBean(), "getTotalPhysicalMemorySize") + val bytes = ManagementFactory.getOperatingSystemMXBean().call("getTotalPhysicalMemorySize") as? Long? if (bytes == null) 1024 else (bytes / 1024 / 1024).toInt() } diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/ReflectionHelper.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/ReflectionHelper.kt index 57148d25e..159b6018d 100644 --- a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/ReflectionHelper.kt +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/ReflectionHelper.kt @@ -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 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 var objectFieldOffset = 0L +private fun AccessibleObject.setAccessibleForcibly() = + 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 { - 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) + 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) } +} - private fun setAccessible(obj: AccessibleObject) = - unsafe.putBoolean(obj, objectFieldOffset, true) +fun Class<*>.forMethod(name: String, vararg args: Any?): Method? = + declaredMethods.filter { it.name == name }.filter { checkParameter(it, *args) }.firstOrNull() - fun get(obj: Any, fieldName: String): T? = - get(obj.javaClass, obj, fieldName) +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 +} - @Suppress("UNCHECKED_CAST") - fun get(cls: Class, obj: S, fieldName: String): T? = - try { - val method = cls.getDeclaredField(fieldName) - setAccessible(method) - method.get(obj) as T - } catch (ex: Throwable) { - null - } +fun isInstance(superClass: Class<*>, obj: Any): Boolean { + if (superClass.isInstance(obj)) return true + else if (PRIMITIVES[superClass.name] == obj.javaClass) return true + return false +} - @Suppress("UNCHECKED_CAST") - fun 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 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 getMethod(obj: Any, methodName: String): Method? = - getMethod(obj.javaClass, methodName) +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 getMethod(cls: Class<*>, methodName: String): Method? = - try { - cls.getDeclaredMethod(methodName).apply { ReflectionHelper.setAccessible(this) } - } catch (ex: Throwable) { - null - } -} \ No newline at end of file +fun Class.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 +) diff --git a/build.gradle b/build.gradle index e647d9554..efe04fcba 100644 --- a/build.gradle +++ b/build.gradle @@ -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()