v1.0.1: fully reflection-based browser (EmbeddedBrowser/EmbeddedJsBridge), zero compile-time JCEF deps; panel never blank
This commit is contained in:
+1
-1
@@ -6,7 +6,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "com.prepeep"
|
group = "com.prepeep"
|
||||||
version = "1.0.0"
|
version = "1.0.1"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package com.harmness.toolwindow
|
||||||
|
|
||||||
|
import com.intellij.openapi.Disposable
|
||||||
|
import com.intellij.openapi.diagnostic.Logger
|
||||||
|
import java.lang.reflect.InvocationTargetException
|
||||||
|
import javax.swing.JComponent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JCEF 浏览器的纯反射封装。
|
||||||
|
*
|
||||||
|
* 目标:让插件代码在**编译期完全不引用** com.intellij.ui.jcef.*
|
||||||
|
* (以及 org.cef.*),避免 IDE 缺少 JCEF 类时出现
|
||||||
|
* NoClassDefFoundError 把整个面板拖垮。
|
||||||
|
*
|
||||||
|
* 所有 JCEF 调用都走反射;类不存在或调用失败时返回 null / 静默降级。
|
||||||
|
*/
|
||||||
|
class EmbeddedBrowser internal constructor(val instance: Any) {
|
||||||
|
|
||||||
|
/** 浏览器 Swing 组件(可加入面板);失败返回 null */
|
||||||
|
val component: JComponent?
|
||||||
|
get() = try {
|
||||||
|
instance.invoke("getComponent") as? JComponent
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
log.warn("getComponent failed", t)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun load(url: String): Boolean = try {
|
||||||
|
instance.invoke("loadURL", url)
|
||||||
|
true
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
log.warn("loadURL failed for $url", t)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun canGoBack(): Boolean = cefCall("canGoBack") as? Boolean ?: false
|
||||||
|
|
||||||
|
fun goBack(): Boolean = cefCall("goBack") != null
|
||||||
|
|
||||||
|
fun canGoForward(): Boolean = cefCall("canGoForward") as? Boolean ?: false
|
||||||
|
|
||||||
|
fun goForward(): Boolean = cefCall("goForward") != null
|
||||||
|
|
||||||
|
fun reload(): Boolean = cefCall("reload") != null
|
||||||
|
|
||||||
|
fun dispose() {
|
||||||
|
try {
|
||||||
|
instance.invoke("dispose")
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
log.warn("browser dispose failed", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// org.cef.browser.CefBrowser 上的方法(getCefBrowser() 返回它)
|
||||||
|
private fun cefCall(name: String, vararg args: Any?): Any? = try {
|
||||||
|
val cef = instance.invoke("getCefBrowser") ?: return null
|
||||||
|
cef.invoke(name, *args)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
log.warn("cefCall $name failed", t)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Any.invoke(method: String, vararg args: Any?): Any? {
|
||||||
|
val clazz = this::class.java
|
||||||
|
return try {
|
||||||
|
for (m in clazz.methods) {
|
||||||
|
if (m.name == method && m.parameterCount == args.size) {
|
||||||
|
m.isAccessible = true
|
||||||
|
return m.invoke(this, *args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 按名字+精确参数类型再找一次
|
||||||
|
val types = args.map { it?.javaClass ?: String::class.java }.toTypedArray()
|
||||||
|
val m = clazz.getMethod(method, *types)
|
||||||
|
m.isAccessible = true
|
||||||
|
m.invoke(this, *args)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
log.warn("reflect invoke $method failed", t)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = Logger.getInstance(EmbeddedBrowser::class.java)
|
||||||
|
|
||||||
|
/** IDE 是否提供可用 JCEF(JBCefApp.isSupported 的反射版本) */
|
||||||
|
fun isJcefSupported(): Boolean = try {
|
||||||
|
val app = Class.forName("com.intellij.ui.jcef.JBCefApp")
|
||||||
|
val m = app.getMethod("isSupported")
|
||||||
|
m.invoke(null) as? Boolean ?: false
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
log.info("JCEF not supported in this IDE: ${t.javaClass.simpleName}")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尝试创建嵌入式浏览器。
|
||||||
|
* @return 成功返回封装对象;失败(类缺失 / 不支持 / 构造异常)返回 null
|
||||||
|
*/
|
||||||
|
fun create(): EmbeddedBrowser? {
|
||||||
|
try {
|
||||||
|
val cls = Class.forName("com.intellij.ui.jcef.JBCefBrowser")
|
||||||
|
val ctor = cls.getConstructor()
|
||||||
|
val inst = ctor.newInstance()
|
||||||
|
// 验证 getComponent() 可用,避免构造成功但组件拿不到
|
||||||
|
inst.javaClass.getMethod("getComponent").invoke(inst)
|
||||||
|
return EmbeddedBrowser(inst)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
val real = when (t) {
|
||||||
|
is InvocationTargetException -> t.targetException ?: t
|
||||||
|
else -> t
|
||||||
|
}
|
||||||
|
log.warn("EmbeddedBrowser.create failed: ${real.javaClass.simpleName}: ${real.message}", real)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.harmness.toolwindow
|
||||||
|
|
||||||
|
import com.intellij.openapi.Disposable
|
||||||
|
import com.intellij.openapi.diagnostic.Logger
|
||||||
|
import java.lang.reflect.InvocationTargetException
|
||||||
|
import java.util.function.Function
|
||||||
|
import javax.swing.JComponent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JCEF JS↔Java 桥的反射封装。
|
||||||
|
*
|
||||||
|
* 负责两件事:
|
||||||
|
* 1. 通过反射创建 JBCefJSQuery 并注册 handler(把网页发来的字符串请求转发给 [onRequest])
|
||||||
|
* 2. 通过反射调用 JBCefJSQuery.inject() 生成注入到页面的 JS 代码
|
||||||
|
*
|
||||||
|
* 与 [EmbeddedBrowser] 一样,编译期零 JCEF 依赖。类缺失/调用失败时
|
||||||
|
* 仅返回 null / 空串,不影响浏览器主体功能。
|
||||||
|
*/
|
||||||
|
class EmbeddedJsBridge private constructor(
|
||||||
|
private val queryInstance: Any,
|
||||||
|
private val onRequest: (String) -> Any?
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回可注入到页面 JS 的代码片段(调用插件 handler),
|
||||||
|
* 失败返回空串。
|
||||||
|
*/
|
||||||
|
fun injectJs(): String = try {
|
||||||
|
val m = queryInstance.javaClass.getMethod(
|
||||||
|
"inject", String::class.java, String::class.java, String::class.java
|
||||||
|
)
|
||||||
|
(m.invoke(queryInstance, "payload.query",
|
||||||
|
"function(response){ if (payload.callbacks && payload.callbacks.onSuccess) payload.callbacks.onSuccess(response); }",
|
||||||
|
"function(code,msg){ if (payload.callbacks && payload.callbacks.onFailure) payload.callbacks.onFailure(code,msg); }"
|
||||||
|
) as? String) ?: ""
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
log.warn("injectJs failed", t)
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dispose() {
|
||||||
|
try {
|
||||||
|
queryInstance.javaClass.getMethod("dispose").invoke(queryInstance)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
log.warn("bridge dispose failed", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = Logger.getInstance(EmbeddedJsBridge::class.java)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为已创建的 [browser] 建立 JS 桥。
|
||||||
|
* @return 成功返回桥对象;失败返回 null(浏览器仍可用,仅网页无法调用插件 API)
|
||||||
|
*/
|
||||||
|
fun create(browser: EmbeddedBrowser, onRequest: (String) -> Any?): EmbeddedJsBridge? {
|
||||||
|
try {
|
||||||
|
val queryClass = Class.forName("com.intellij.ui.jcef.JBCefJSQuery")
|
||||||
|
// create(JBCefBrowserBase) — 用可赋值参数类型查找
|
||||||
|
val browserInst = browser.instance
|
||||||
|
val createMethod = queryClass.methods.firstOrNull { m ->
|
||||||
|
m.name == "create" && m.parameterCount == 1 &&
|
||||||
|
m.parameterTypes[0].isAssignableFrom(browserInst.javaClass)
|
||||||
|
} ?: return null
|
||||||
|
val queryInstance = createMethod.invoke(null, browserInst)
|
||||||
|
|
||||||
|
// addHandler(java.util.function.Function<String, Response>)
|
||||||
|
val handlerFn = Function<String, Any?> { req -> onRequest(req ?: "") }
|
||||||
|
val addMethod = queryClass.methods.firstOrNull { m ->
|
||||||
|
m.name == "addHandler" && m.parameterCount == 1 &&
|
||||||
|
m.parameterTypes[0] == Function::class.java
|
||||||
|
} ?: return null
|
||||||
|
addMethod.invoke(queryInstance, handlerFn)
|
||||||
|
|
||||||
|
return EmbeddedJsBridge(queryInstance, onRequest)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
val real = when (t) {
|
||||||
|
is InvocationTargetException -> t.targetException ?: t
|
||||||
|
else -> t
|
||||||
|
}
|
||||||
|
log.warn("EmbeddedJsBridge.create failed: ${real.javaClass.simpleName}: ${real.message}", real)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,11 @@
|
|||||||
package com.harmness.toolwindow
|
package com.harmness.toolwindow
|
||||||
|
|
||||||
import com.intellij.ide.BrowserUtil
|
|
||||||
import com.intellij.openapi.Disposable
|
import com.intellij.openapi.Disposable
|
||||||
import com.intellij.openapi.diagnostic.Logger
|
import com.intellij.openapi.diagnostic.Logger
|
||||||
import com.intellij.openapi.util.Disposer
|
import com.intellij.openapi.util.Disposer
|
||||||
import com.intellij.ui.components.JBLabel
|
import com.intellij.ui.components.JBLabel
|
||||||
import com.intellij.ui.components.JBPanel
|
import com.intellij.ui.components.JBPanel
|
||||||
import com.intellij.ui.components.JBScrollPane
|
import com.intellij.ui.components.JBScrollPane
|
||||||
import com.intellij.ui.jcef.JBCefApp
|
|
||||||
import com.intellij.ui.jcef.JBCefBrowser
|
|
||||||
import org.cef.browser.CefBrowser
|
|
||||||
import org.cef.handler.CefLoadHandlerAdapter
|
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.CardLayout
|
import java.awt.CardLayout
|
||||||
import java.awt.FlowLayout
|
import java.awt.FlowLayout
|
||||||
@@ -34,11 +29,12 @@ import javax.swing.SwingUtilities
|
|||||||
*
|
*
|
||||||
* 四个视图(CardLayout):
|
* 四个视图(CardLayout):
|
||||||
* - standby → 初始化中
|
* - standby → 初始化中
|
||||||
* - browser → 网页(JCEF,惰性初始化)
|
* - browser → 网页(通过 [EmbeddedBrowser] 反射包装,惰性初始化)
|
||||||
* - settings → 起始地址设置表单
|
* - settings → 起始地址设置表单
|
||||||
* - error → 错误信息 + 一键复制按钮
|
* - error → 错误信息 + 一键复制
|
||||||
*
|
*
|
||||||
* 工具栏和设置表单不依赖 JCEF,始终可用。
|
* 本类**不引用任何 JCEF 类**。浏览器能力全部走反射封装,
|
||||||
|
* 即使 IDE 没有 JCEF,工具栏 / 设置表单 / 错误卡片也始终可用。
|
||||||
*/
|
*/
|
||||||
class HarmnessBrowserPanel(
|
class HarmnessBrowserPanel(
|
||||||
private val project: com.intellij.openapi.project.Project,
|
private val project: com.intellij.openapi.project.Project,
|
||||||
@@ -48,9 +44,9 @@ class HarmnessBrowserPanel(
|
|||||||
private val settings = SettingsState.getInstance()
|
private val settings = SettingsState.getInstance()
|
||||||
private val logger = Logger.getInstance(HarmnessBrowserPanel::class.java)
|
private val logger = Logger.getInstance(HarmnessBrowserPanel::class.java)
|
||||||
|
|
||||||
// JCEF 对象(惰性初始化,可能为 null)
|
// 反射浏览器(可能为 null)
|
||||||
private var browser: JBCefBrowser? = null
|
private var browser: EmbeddedBrowser? = null
|
||||||
private var bridgeAvailable = false // 反射创建 JBCefJSQuery 是否成功
|
private var bridge: EmbeddedJsBridge? = null
|
||||||
|
|
||||||
private var addressField: JTextField = JTextField()
|
private var addressField: JTextField = JTextField()
|
||||||
private var settingsUrlField: JTextField = JTextField(30)
|
private var settingsUrlField: JTextField = JTextField(30)
|
||||||
@@ -59,9 +55,6 @@ class HarmnessBrowserPanel(
|
|||||||
private val center = JBPanel<JBPanel<*>>(cardLayout)
|
private val center = JBPanel<JBPanel<*>>(cardLayout)
|
||||||
private var currentCard = CARD_STANDBY
|
private var currentCard = CARD_STANDBY
|
||||||
|
|
||||||
// 反射持有的 JSQuery 对象(类型为 Any,避免编译期依赖)
|
|
||||||
private var jsQuery: Any? = null
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Disposer.register(parentDisposable, this)
|
Disposer.register(parentDisposable, this)
|
||||||
|
|
||||||
@@ -73,7 +66,7 @@ class HarmnessBrowserPanel(
|
|||||||
center.add(createErrorCard(), CARD_ERROR)
|
center.add(createErrorCard(), CARD_ERROR)
|
||||||
cardLayout.show(center, CARD_STANDBY)
|
cardLayout.show(center, CARD_STANDBY)
|
||||||
|
|
||||||
// 延迟初始化浏览器,不阻塞 EDT
|
// 窗口就绪后惰性初始化浏览器,避免阻塞 EDT
|
||||||
SwingUtilities.invokeLater { initBrowserSafely() }
|
SwingUtilities.invokeLater { initBrowserSafely() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,19 +76,13 @@ class HarmnessBrowserPanel(
|
|||||||
|
|
||||||
private fun createToolbar(): JComponent {
|
private fun createToolbar(): JComponent {
|
||||||
val back = JButton(object : AbstractAction("←") {
|
val back = JButton(object : AbstractAction("←") {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) { runCatching { browser?.goBack() } }
|
||||||
runCatching { browser?.cefBrowser?.goBack() }
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
val forward = JButton(object : AbstractAction("→") {
|
val forward = JButton(object : AbstractAction("→") {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) { runCatching { browser?.goForward() } }
|
||||||
runCatching { browser?.cefBrowser?.goForward() }
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
val reload = JButton(object : AbstractAction("⟳") {
|
val reload = JButton(object : AbstractAction("⟳") {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) { runCatching { browser?.reload() } }
|
||||||
runCatching { browser?.cefBrowser?.reload() }
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
val settingsBtn = JButton(object : AbstractAction("⚙") {
|
val settingsBtn = JButton(object : AbstractAction("⚙") {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
@@ -142,7 +129,7 @@ class HarmnessBrowserPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// JCEF 浏览器初始化(安全)
|
// 浏览器初始化(反射,安全)
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
private fun initBrowserSafely() {
|
private fun initBrowserSafely() {
|
||||||
@@ -152,108 +139,64 @@ class HarmnessBrowserPanel(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (!JBCefApp.isSupported()) {
|
if (!EmbeddedBrowser.isJcefSupported()) {
|
||||||
showError("JCEF 不受支持",
|
showError("JCEF 不受支持",
|
||||||
"当前 IDE 构建未启用 JCEF。\n请使用 JetBrains 官方发行版(内置 JBR 运行时)。")
|
"当前 IDE 构建未启用 JCEF(找不到 com.intellij.ui.jcef.JBCefBrowser)。\n" +
|
||||||
|
"请使用 JetBrains 官方发行版 IDE,或更换为内置 JCEF 的构建。")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val b = EmbeddedBrowser.create()
|
||||||
|
?: run {
|
||||||
|
showError("无法创建浏览器",
|
||||||
|
"反射创建 JBCefBrowser 失败。请检查 IDE 是否启用 JCEF。")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val b = JBCefBrowser()
|
|
||||||
browser = b
|
browser = b
|
||||||
|
|
||||||
// 通过反射安全初始化 JS↔Java 桥
|
// JS 桥(失败不影响浏览器)
|
||||||
setupBridgeViaReflection(b)
|
bridge = EmbeddedJsBridge.create(b) { request ->
|
||||||
|
logger.debug("bridge request: $request")
|
||||||
|
try {
|
||||||
|
val responseClass = Class.forName("com.intellij.ui.jcef.JBCefJSQuery\$Response")
|
||||||
|
val ctor = responseClass.getConstructor(String::class.java)
|
||||||
|
ctor.newInstance(handleBridgeRequest(request))
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
// 无 Response 类:直接返回字符串
|
||||||
|
handleBridgeRequest(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 注入浏览器组件
|
// 浏览器组件加入 "browser" 卡片
|
||||||
center.add(b.component, CARD_BROWSER)
|
val comp = b.component
|
||||||
|
if (comp != null) {
|
||||||
|
center.add(comp, CARD_BROWSER)
|
||||||
showCard(CARD_BROWSER)
|
showCard(CARD_BROWSER)
|
||||||
loadCurrentUrl()
|
loadCurrentUrl()
|
||||||
|
} else {
|
||||||
|
showError("浏览器组件不可用", "JBCefBrowser.getComponent() 返回空。")
|
||||||
|
}
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
logger.error("JCEF init failed", t)
|
logger.error("JCEF init failed", t)
|
||||||
showError("JCEF 初始化失败", buildErrorText(t))
|
showError("JCEF 初始化失败", buildErrorText(t))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
private fun handleBridgeRequest(request: String): String = when {
|
||||||
// JS ↔ Java 桥(全反射,编译期零 JBCefJSQuery 依赖)
|
request == "ping" -> "pong"
|
||||||
// ------------------------------------------------------------------
|
request == "getUrl" -> browser?.let {
|
||||||
|
|
||||||
private fun setupBridgeViaReflection(b: JBCefBrowser) {
|
|
||||||
try {
|
try {
|
||||||
// 1) 检查 JBCefJSQuery 类是否可用
|
val cef = it.instance.javaClass.getMethod("getCefBrowser").invoke(it.instance)
|
||||||
val queryClass = try {
|
cef.javaClass.getMethod("getUrl").invoke(cef) as? String ?: ""
|
||||||
Class.forName("com.intellij.ui.jcef.JBCefJSQuery")
|
} catch (_: Throwable) { "" }
|
||||||
} catch (_: Throwable) {
|
} ?: ""
|
||||||
logger.info("JBCefJSQuery class not available; bridge disabled")
|
request.startsWith("openExternal:") -> {
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) 反射创建 JBCefJSQuery.create(JBCefBrowser)
|
|
||||||
val createMethod = queryClass.getMethod("create", JBCefBrowser::class.java)
|
|
||||||
val queryInstance = createMethod.invoke(null, b) ?: return
|
|
||||||
jsQuery = queryInstance
|
|
||||||
|
|
||||||
// 3) 用反射构建 Handler(避免编译期引用 Response 内部类)
|
|
||||||
val handlerInterface = Class.forName("com.intellij.ui.jcef.JBCefJSQuery\$Handler")
|
|
||||||
val handlerProxy = java.lang.reflect.Proxy.newProxyInstance(
|
|
||||||
handlerInterface.classLoader,
|
|
||||||
arrayOf(handlerInterface)
|
|
||||||
) { _, method, args ->
|
|
||||||
when (method.name) {
|
|
||||||
"success" -> {
|
|
||||||
val response = args?.getOrNull(0)?.toString() ?: ""
|
|
||||||
logger.debug("Bridge success: $response")
|
|
||||||
}
|
|
||||||
"error" -> {
|
|
||||||
logger.warn("Bridge error: code=${args?.getOrNull(0)}, msg=${args?.getOrNull(1)}")
|
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val addHandlerMethod = queryClass.getMethod("addHandler", handlerInterface)
|
|
||||||
addHandlerMethod.invoke(queryInstance, handlerProxy)
|
|
||||||
|
|
||||||
// 4) 反射调用 inject() 获取注入到页面的 JS 代码
|
|
||||||
val injectMethod = queryClass.getMethod(
|
|
||||||
"inject", String::class.java, String::class.java, String::class.java
|
|
||||||
)
|
|
||||||
val injectedJs = injectMethod.invoke(
|
|
||||||
queryInstance,
|
|
||||||
"payload.query",
|
|
||||||
"function(r){}",
|
|
||||||
"function(c,m){}"
|
|
||||||
) as? String ?: ""
|
|
||||||
|
|
||||||
// 5) 在页面加载完成后注入桥接函数
|
|
||||||
b.jbCefClient.addLoadHandler(object : CefLoadHandlerAdapter() {
|
|
||||||
override fun onLoadEnd(
|
|
||||||
cBrowser: CefBrowser,
|
|
||||||
frame: org.cef.browser.CefFrame,
|
|
||||||
httpStatusCode: Int
|
|
||||||
) {
|
|
||||||
if (!frame.isMain) return
|
|
||||||
try {
|
try {
|
||||||
val js = """
|
com.intellij.ide.BrowserUtil.browse(request.removePrefix("openExternal:"))
|
||||||
window.harmnessBridge = function(payload) {
|
} catch (_: Throwable) {}
|
||||||
$injectedJs
|
"ok"
|
||||||
};
|
|
||||||
if (window.__HARMNESS_READY__) window.__HARMNESS_READY__();
|
|
||||||
""".trimIndent()
|
|
||||||
cBrowser.executeJavaScript(js, cBrowser.url ?: "", 0)
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
logger.warn("Bridge inject failed", t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, b.cefBrowser)
|
|
||||||
|
|
||||||
bridgeAvailable = true
|
|
||||||
logger.info("JS↔Java bridge initialized successfully via reflection")
|
|
||||||
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
logger.warn("Bridge init failed (browser still works without bridge): ${t.message}")
|
|
||||||
bridgeAvailable = false
|
|
||||||
// 不中断:浏览器本身仍可用,只是网页无法调用插件 API
|
|
||||||
}
|
}
|
||||||
|
else -> "unknown request: $request"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
@@ -271,11 +214,11 @@ class HarmnessBrowserPanel(
|
|||||||
appendLine(detail)
|
appendLine(detail)
|
||||||
appendLine()
|
appendLine()
|
||||||
appendLine("--- 环境信息 ---")
|
appendLine("--- 环境信息 ---")
|
||||||
appendLine("插件版本: 1.0.0")
|
appendLine("插件版本: 1.0.1")
|
||||||
appendLine("IDE Build: $appInfo")
|
appendLine("IDE Build: $appInfo")
|
||||||
appendLine("OS: ${System.getProperty("os.name")} ${System.getProperty("os.version")}")
|
appendLine("OS: ${System.getProperty("os.name")} ${System.getProperty("os.version")}")
|
||||||
appendLine("Java: ${System.getProperty("java.version")}")
|
appendLine("Java: ${System.getProperty("java.version")}")
|
||||||
appendLine("JCEF 支持: ${try { JBCefApp.isSupported() } catch (_: Throwable) { "检查失败" }}")
|
appendLine("JCEF 支持: ${EmbeddedBrowser.isJcefSupported()}")
|
||||||
}
|
}
|
||||||
val area = center.getClientProperty("errorArea") as? JTextArea
|
val area = center.getClientProperty("errorArea") as? JTextArea
|
||||||
if (area != null) {
|
if (area != null) {
|
||||||
@@ -340,24 +283,20 @@ class HarmnessBrowserPanel(
|
|||||||
anchor = GridBagConstraints.NORTHWEST
|
anchor = GridBagConstraints.NORTHWEST
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标题
|
|
||||||
panel.add(JBLabel("<html><h2 style='margin:0'>⚙ 设置</h2></html>"), gbc.also {
|
panel.add(JBLabel("<html><h2 style='margin:0'>⚙ 设置</h2></html>"), gbc.also {
|
||||||
it.gridx = 0; it.gridy = 0; it.gridwidth = 2
|
it.gridx = 0; it.gridy = 0; it.gridwidth = 2
|
||||||
})
|
})
|
||||||
gbc.gridwidth = 1
|
gbc.gridwidth = 1
|
||||||
|
|
||||||
// 起始地址
|
|
||||||
panel.add(JBLabel("起始地址:"), gbc.also { it.gridx = 0; it.gridy = 1 })
|
panel.add(JBLabel("起始地址:"), gbc.also { it.gridx = 0; it.gridy = 1 })
|
||||||
settingsUrlField = JTextField(settings.startUrl, 28)
|
settingsUrlField = JTextField(settings.startUrl, 28)
|
||||||
panel.add(settingsUrlField, gbc.also { it.gridx = 1; it.gridy = 1 })
|
panel.add(settingsUrlField, gbc.also { it.gridx = 1; it.gridy = 1 })
|
||||||
|
|
||||||
// 提示
|
|
||||||
panel.add(JBLabel("<html><small>例如:http://127.0.0.1:8899<br>" +
|
panel.add(JBLabel("<html><small>例如:http://127.0.0.1:8899<br>" +
|
||||||
"目标站点须允许被 iframe 嵌入(无 X-Frame-Options / CSP 限制)</small></html>"),
|
"目标站点须允许被 iframe 嵌入(无 X-Frame-Options / CSP 限制)</small></html>"),
|
||||||
gbc.also { it.gridx = 0; it.gridy = 2; it.gridwidth = 2 })
|
gbc.also { it.gridx = 0; it.gridy = 2; it.gridwidth = 2 })
|
||||||
gbc.gridwidth = 1
|
gbc.gridwidth = 1
|
||||||
|
|
||||||
// 按钮
|
|
||||||
val saveBtn = JButton("保存")
|
val saveBtn = JButton("保存")
|
||||||
saveBtn.addActionListener {
|
saveBtn.addActionListener {
|
||||||
settings.startUrl = settingsUrlField.text.trim()
|
settings.startUrl = settingsUrlField.text.trim()
|
||||||
@@ -376,7 +315,6 @@ class HarmnessBrowserPanel(
|
|||||||
}
|
}
|
||||||
panel.add(btnPanel, gbc.also { it.gridx = 0; it.gridy = 3; it.gridwidth = 2 })
|
panel.add(btnPanel, gbc.also { it.gridx = 0; it.gridy = 3; it.gridwidth = 2 })
|
||||||
|
|
||||||
// 弹性填充
|
|
||||||
panel.add(JBPanel<JBPanel<*>>(), gbc.also {
|
panel.add(JBPanel<JBPanel<*>>(), gbc.also {
|
||||||
it.gridy = 4; it.weighty = 1.0
|
it.gridy = 4; it.weighty = 1.0
|
||||||
})
|
})
|
||||||
@@ -391,14 +329,14 @@ class HarmnessBrowserPanel(
|
|||||||
private fun navigate(url: String) {
|
private fun navigate(url: String) {
|
||||||
val b = browser ?: return
|
val b = browser ?: return
|
||||||
val fixed = if (url.startsWith("http://") || url.startsWith("https://")) url else "http://$url"
|
val fixed = if (url.startsWith("http://") || url.startsWith("https://")) url else "http://$url"
|
||||||
b.loadURL(fixed)
|
b.load(fixed)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadCurrentUrl() {
|
fun loadCurrentUrl() {
|
||||||
val b = browser ?: return
|
val b = browser ?: return
|
||||||
val url = settings.startUrl
|
val url = settings.startUrl
|
||||||
addressField.text = url
|
addressField.text = url
|
||||||
b.loadURL(url)
|
b.load(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToolWindowSelected() {
|
fun onToolWindowSelected() {
|
||||||
@@ -410,14 +348,10 @@ class HarmnessBrowserPanel(
|
|||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
// 反射释放 jsQuery
|
try { bridge?.dispose() } catch (_: Throwable) {}
|
||||||
try {
|
try { browser?.dispose() } catch (_: Throwable) {}
|
||||||
jsQuery?.javaClass?.getMethod("dispose")?.invoke(jsQuery)
|
bridge = null
|
||||||
} catch (_: Throwable) {}
|
|
||||||
try { browser?.let { Disposer.dispose(it) } } catch (_: Throwable) {}
|
|
||||||
browser = null
|
browser = null
|
||||||
jsQuery = null
|
|
||||||
bridgeAvailable = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
Reference in New Issue
Block a user