Reflection-based JS bridge; copyable error report card
This commit is contained in:
@@ -9,16 +9,16 @@ import com.intellij.ui.components.JBPanel
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.ui.jcef.JBCefApp
|
||||
import com.intellij.ui.jcef.JBCefBrowser
|
||||
import com.intellij.ui.jcef.JBCefJSQuery
|
||||
import org.cef.handler.CefLifeSpanHandlerAdapter
|
||||
import org.cef.browser.CefBrowser
|
||||
import org.cef.handler.CefLoadHandlerAdapter
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.CardLayout
|
||||
import java.awt.Desktop
|
||||
import java.awt.FlowLayout
|
||||
import java.awt.GridBagConstraints
|
||||
import java.awt.GridBagLayout
|
||||
import java.awt.Insets
|
||||
import java.awt.Toolkit
|
||||
import java.awt.datatransfer.StringSelection
|
||||
import java.awt.event.ActionEvent
|
||||
import javax.swing.AbstractAction
|
||||
import javax.swing.BorderFactory
|
||||
@@ -32,15 +32,13 @@ import javax.swing.SwingUtilities
|
||||
/**
|
||||
* Harmness 嵌入浏览器面板。
|
||||
*
|
||||
* 布局:
|
||||
* - NORTH:工具栏(← → ⟳ 地址栏 ⚙)
|
||||
* - CENTER:CardLayout,包含三张卡:
|
||||
* - "settings":设置表单(起始地址 + 保存/取消)
|
||||
* - "browser":JCEF 浏览器(懒初始化,失败时显示错误)
|
||||
* - "error":初始化失败时的错误信息
|
||||
* 四个视图(CardLayout):
|
||||
* - standby → 初始化中
|
||||
* - browser → 网页(JCEF,惰性初始化)
|
||||
* - settings → 起始地址设置表单
|
||||
* - error → 错误信息 + 一键复制按钮
|
||||
*
|
||||
* 关键设计:工具栏和设置表单**不依赖** JCEF。即使 JCEF 初始化失败,
|
||||
* 界面仍然可用,错误信息会显示在中心区域,方便排错。
|
||||
* 工具栏和设置表单不依赖 JCEF,始终可用。
|
||||
*/
|
||||
class HarmnessBrowserPanel(
|
||||
private val project: com.intellij.openapi.project.Project,
|
||||
@@ -50,70 +48,65 @@ class HarmnessBrowserPanel(
|
||||
private val settings = SettingsState.getInstance()
|
||||
private val logger = Logger.getInstance(HarmnessBrowserPanel::class.java)
|
||||
|
||||
// JCEF 浏览器(懒初始化)
|
||||
// JCEF 对象(惰性初始化,可能为 null)
|
||||
private var browser: JBCefBrowser? = null
|
||||
private var query: JBCefJSQuery? = null
|
||||
private var bridgeAvailable = false // 反射创建 JBCefJSQuery 是否成功
|
||||
|
||||
private var addressField: JTextField = JTextField()
|
||||
private var settingsUrlField: JTextField = JTextField(30)
|
||||
|
||||
private val cardLayout = CardLayout()
|
||||
private val center = JBPanel<JBPanel<*>>(cardLayout)
|
||||
private var currentCard = CARD_STANDBY
|
||||
|
||||
// 反射持有的 JSQuery 对象(类型为 Any,避免编译期依赖)
|
||||
private var jsQuery: Any? = null
|
||||
|
||||
init {
|
||||
Disposer.register(parentDisposable, this)
|
||||
|
||||
// 工具栏(不依赖 JCEF,总是先建立)
|
||||
add(createToolbar(), BorderLayout.NORTH)
|
||||
add(center, BorderLayout.CENTER)
|
||||
|
||||
// 中心卡片
|
||||
center.add(createStandbyCard(), CARD_STANDBY)
|
||||
center.add(createSettingsCard(), CARD_SETTINGS)
|
||||
center.add(createErrorCard(), CARD_ERROR)
|
||||
cardLayout.show(center, CARD_STANDBY)
|
||||
|
||||
// 在窗口系统就绪后异步初始化浏览器,避免阻塞 EDT 或初始化失败拖垮整个面板
|
||||
// 延迟初始化浏览器,不阻塞 EDT
|
||||
SwingUtilities.invokeLater { initBrowserSafely() }
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 工具栏
|
||||
// 工具栏(纯 Swing,不依赖 JCEF)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private fun createToolbar(): JComponent {
|
||||
val back = JButton(object : AbstractAction("←") {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val cef = browser?.cefBrowser
|
||||
if (cef != null && cef.canGoBack()) cef.goBack()
|
||||
runCatching { browser?.cefBrowser?.goBack() }
|
||||
}
|
||||
})
|
||||
val forward = JButton(object : AbstractAction("→") {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val cef = browser?.cefBrowser
|
||||
if (cef != null && cef.canGoForward()) cef.goForward()
|
||||
runCatching { browser?.cefBrowser?.goForward() }
|
||||
}
|
||||
})
|
||||
val reload = JButton(object : AbstractAction("⟳") {
|
||||
override fun actionPerformed(e: ActionEvent) { browser?.cefBrowser?.reload() }
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
runCatching { browser?.cefBrowser?.reload() }
|
||||
}
|
||||
})
|
||||
|
||||
// 右上角设置图标:在浏览器视图与设置视图间切换
|
||||
val settingsBtn = JButton(object : AbstractAction("⚙") {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
if (cardLayout == null) return
|
||||
if (currentCard == CARD_SETTINGS) {
|
||||
showBrowser()
|
||||
} else {
|
||||
showSettings()
|
||||
}
|
||||
if (currentCard == CARD_SETTINGS) showBrowser() else showSettings()
|
||||
}
|
||||
})
|
||||
settingsBtn.toolTipText = "设置"
|
||||
settingsBtn.border = BorderFactory.createEmptyBorder(2, 8, 2, 8)
|
||||
|
||||
addressField = JTextField(settings.startUrl)
|
||||
addressField.toolTipText = "当前页面地址(可手动输入后回车跳转)"
|
||||
addressField.toolTipText = "当前页面地址(回车跳转)"
|
||||
addressField.addActionListener { navigate(addressField.text) }
|
||||
|
||||
val toolbar = JPanel(FlowLayout(FlowLayout.LEFT, 4, 2))
|
||||
@@ -126,11 +119,9 @@ class HarmnessBrowserPanel(
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 卡片管理与视图切换
|
||||
// 视图切换
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private var currentCard: String = CARD_STANDBY
|
||||
|
||||
private fun showCard(name: String) {
|
||||
currentCard = name
|
||||
cardLayout.show(center, name)
|
||||
@@ -143,7 +134,6 @@ class HarmnessBrowserPanel(
|
||||
|
||||
private fun showBrowser() {
|
||||
if (browser == null) {
|
||||
// 浏览器还没建好:尝试初始化;失败显示错误
|
||||
initBrowserSafely()
|
||||
} else {
|
||||
showCard(CARD_BROWSER)
|
||||
@@ -152,213 +142,258 @@ class HarmnessBrowserPanel(
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// JCEF 浏览器初始化(安全包装)
|
||||
// JCEF 浏览器初始化(安全)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private fun initBrowserSafely() {
|
||||
if (browser != null) {
|
||||
showCard(CARD_BROWSER)
|
||||
loadCurrentUrl()
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (browser != null) return
|
||||
if (!JBCefApp.isSupported()) {
|
||||
showError("JCEF (Java Chromium Embedded Framework) 在当前 IDE 中不受支持。\n" +
|
||||
"请使用官方发行版 IDE(内置 JBR),或检查 IDE 是否禁用了 JCEF。")
|
||||
showError("JCEF 不受支持",
|
||||
"当前 IDE 构建未启用 JCEF。\n请使用 JetBrains 官方发行版(内置 JBR 运行时)。")
|
||||
return
|
||||
}
|
||||
val b = JBCefBrowser()
|
||||
browser = b
|
||||
|
||||
// 通过反射安全初始化 JS↔Java 桥
|
||||
setupBridgeViaReflection(b)
|
||||
|
||||
// 注入浏览器组件
|
||||
center.add(b.component, CARD_BROWSER)
|
||||
showCard(CARD_BROWSER)
|
||||
loadCurrentUrl()
|
||||
} catch (t: Throwable) {
|
||||
logger.error("JCEF init failed", t)
|
||||
showError("JCEF 初始化失败", buildErrorText(t))
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// JS ↔ Java 桥(全反射,编译期零 JBCefJSQuery 依赖)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private fun setupBridgeViaReflection(b: JBCefBrowser) {
|
||||
try {
|
||||
// 1) 检查 JBCefJSQuery 类是否可用
|
||||
val queryClass = try {
|
||||
Class.forName("com.intellij.ui.jcef.JBCefJSQuery")
|
||||
} catch (_: Throwable) {
|
||||
logger.info("JBCefJSQuery class not available; bridge disabled")
|
||||
return
|
||||
}
|
||||
|
||||
val b = JBCefBrowser()
|
||||
browser = b
|
||||
setupPopupHandling(b)
|
||||
setupBridge(b)
|
||||
// 2) 反射创建 JBCefJSQuery.create(JBCefBrowser)
|
||||
val createMethod = queryClass.getMethod("create", JBCefBrowser::class.java)
|
||||
val queryInstance = createMethod.invoke(null, b) ?: return
|
||||
jsQuery = queryInstance
|
||||
|
||||
// 把浏览器组件放进 "browser" 卡片
|
||||
center.add(b.component, CARD_BROWSER)
|
||||
if (currentCard != CARD_SETTINGS) {
|
||||
showCard(CARD_BROWSER)
|
||||
// 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
|
||||
}
|
||||
}
|
||||
loadCurrentUrl()
|
||||
|
||||
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 {
|
||||
val js = """
|
||||
window.harmnessBridge = function(payload) {
|
||||
$injectedJs
|
||||
};
|
||||
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.error("Failed to init JCEF browser", t)
|
||||
showError(buildErrorMessage(t))
|
||||
logger.warn("Bridge init failed (browser still works without bridge): ${t.message}")
|
||||
bridgeAvailable = false
|
||||
// 不中断:浏览器本身仍可用,只是网页无法调用插件 API
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildErrorMessage(t: Throwable): String {
|
||||
val sb = StringBuilder()
|
||||
sb.append("JCEF 浏览器初始化失败:\n")
|
||||
appendEx(sb, t)
|
||||
return sb.toString()
|
||||
}
|
||||
// ------------------------------------------------------------------
|
||||
// 错误展示(可复制)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private fun appendEx(sb: StringBuilder, t: Throwable, depth: Int = 0) {
|
||||
if (depth > 4) return
|
||||
sb.append(t.javaClass.simpleName).append(": ").append(t.message).append('\n')
|
||||
for (el in t.stackTrace.take(6)) {
|
||||
sb.append(" at ").append(el).append('\n')
|
||||
private fun showError(title: String, detail: String) {
|
||||
val appInfo = try {
|
||||
com.intellij.openapi.application.ApplicationInfo.getInstance().build?.asString()
|
||||
} catch (_: Throwable) { "unknown" }
|
||||
val fullText = buildString {
|
||||
appendLine("=== Harmness Browser 错误报告 ===")
|
||||
appendLine("错误: $title")
|
||||
appendLine()
|
||||
appendLine(detail)
|
||||
appendLine()
|
||||
appendLine("--- 环境信息 ---")
|
||||
appendLine("插件版本: 1.0.0")
|
||||
appendLine("IDE Build: $appInfo")
|
||||
appendLine("OS: ${System.getProperty("os.name")} ${System.getProperty("os.version")}")
|
||||
appendLine("Java: ${System.getProperty("java.version")}")
|
||||
appendLine("JCEF 支持: ${try { JBCefApp.isSupported() } catch (_: Throwable) { "检查失败" }}")
|
||||
}
|
||||
val area = center.getClientProperty("errorArea") as? JTextArea
|
||||
if (area != null) {
|
||||
area.text = fullText
|
||||
area.caretPosition = 0
|
||||
}
|
||||
t.cause?.let { appendEx(sb, it, depth + 1) }
|
||||
}
|
||||
|
||||
private fun showError(message: String) {
|
||||
val area = center.getClientProperty(ERROR_AREA_KEY) as? JTextArea
|
||||
?: run {
|
||||
val a = JTextArea()
|
||||
center.putClientProperty(ERROR_AREA_KEY, a)
|
||||
a
|
||||
}
|
||||
area.text = message
|
||||
showCard(CARD_ERROR)
|
||||
}
|
||||
|
||||
private fun buildErrorText(t: Throwable): String = buildString {
|
||||
append(t.javaClass.name).append(": ").appendLine(t.message ?: "no message")
|
||||
appendLine()
|
||||
t.stackTrace.take(15).forEach { appendLine(" at $it") }
|
||||
t.cause?.let { c ->
|
||||
appendLine()
|
||||
append("Caused by: ").append(c.javaClass.name).append(": ").appendLine(c.message ?: "")
|
||||
c.stackTrace.take(8).forEach { appendLine(" at $it") }
|
||||
}
|
||||
}
|
||||
|
||||
private fun createErrorCard(): JComponent {
|
||||
val area = JTextArea().apply {
|
||||
isEditable = false
|
||||
font = font.deriveFont(12f)
|
||||
lineWrap = true
|
||||
wrapStyleWord = true
|
||||
}
|
||||
center.putClientProperty("errorArea", area)
|
||||
|
||||
val copyBtn = JButton("📋 复制错误信息")
|
||||
copyBtn.addActionListener {
|
||||
val clip = Toolkit.getDefaultToolkit().systemClipboard
|
||||
clip.setContents(StringSelection(area.text), null)
|
||||
copyBtn.text = "✓ 已复制"
|
||||
SwingUtilities.invokeLater {
|
||||
try { Thread.sleep(1500) } catch (_: InterruptedException) {}
|
||||
copyBtn.text = "📋 复制错误信息"
|
||||
}
|
||||
}
|
||||
|
||||
val wrapper = JBPanel<JBPanel<*>>(BorderLayout())
|
||||
wrapper.add(JBScrollPane(area), BorderLayout.CENTER)
|
||||
wrapper.add(copyBtn, BorderLayout.SOUTH)
|
||||
return wrapper
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 卡片内容
|
||||
// 其他卡片
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private fun createStandbyCard(): JComponent {
|
||||
val p = JBPanel<JBPanel<*>>(BorderLayout())
|
||||
val label = JBLabel("正在初始化浏览器…", 0)
|
||||
p.add(label, BorderLayout.CENTER)
|
||||
return p
|
||||
return JBPanel<JBPanel<*>>(BorderLayout()).apply {
|
||||
add(JBLabel("正在初始化浏览器…", 0), BorderLayout.CENTER)
|
||||
}
|
||||
}
|
||||
|
||||
/** 设置表单:起始地址 + 保存/取消 */
|
||||
private fun createSettingsCard(): JComponent {
|
||||
val panel = JBPanel<JBPanel<*>>()
|
||||
panel.layout = GridBagLayout()
|
||||
val gbc = GridBagConstraints()
|
||||
gbc.insets = Insets(8, 10, 8, 10)
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL
|
||||
gbc.anchor = GridBagConstraints.NORTHWEST
|
||||
val panel = JBPanel<JBPanel<*>>().apply { layout = GridBagLayout() }
|
||||
val gbc = GridBagConstraints().apply {
|
||||
insets = Insets(8, 10, 8, 10)
|
||||
fill = GridBagConstraints.HORIZONTAL
|
||||
anchor = GridBagConstraints.NORTHWEST
|
||||
}
|
||||
|
||||
val title = JBLabel("<html><h2 style='margin:0'>设置</h2></html>")
|
||||
gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 2
|
||||
panel.add(title, gbc)
|
||||
// 标题
|
||||
panel.add(JBLabel("<html><h2 style='margin:0'>⚙ 设置</h2></html>"), gbc.also {
|
||||
it.gridx = 0; it.gridy = 0; it.gridwidth = 2
|
||||
})
|
||||
gbc.gridwidth = 1
|
||||
|
||||
val label = JBLabel("起始地址 (Start URL):")
|
||||
gbc.gridx = 0; gbc.gridy = 1
|
||||
panel.add(label, gbc)
|
||||
|
||||
// 起始地址
|
||||
panel.add(JBLabel("起始地址:"), gbc.also { it.gridx = 0; it.gridy = 1 })
|
||||
settingsUrlField = JTextField(settings.startUrl, 28)
|
||||
gbc.gridx = 1; gbc.gridy = 1
|
||||
panel.add(settingsUrlField, gbc)
|
||||
panel.add(settingsUrlField, gbc.also { it.gridx = 1; it.gridy = 1 })
|
||||
|
||||
val hint = JBLabel("<html><small>例如:http://127.0.0.1:8899 (本地 Harmness 服务地址)<br>" +
|
||||
"目标站点需允许被嵌入(不要有 X-Frame-Options / CSP frame-ancestors 限制)</small></html>")
|
||||
gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 2
|
||||
panel.add(hint, gbc)
|
||||
// 提示
|
||||
panel.add(JBLabel("<html><small>例如:http://127.0.0.1:8899<br>" +
|
||||
"目标站点须允许被 iframe 嵌入(无 X-Frame-Options / CSP 限制)</small></html>"),
|
||||
gbc.also { it.gridx = 0; it.gridy = 2; it.gridwidth = 2 })
|
||||
gbc.gridwidth = 1
|
||||
|
||||
// 按钮
|
||||
val saveBtn = JButton("保存")
|
||||
saveBtn.addActionListener {
|
||||
settings.startUrl = settingsUrlField.text.trim()
|
||||
saveBtn.text = "已保存 ✓"
|
||||
saveBtn.text = "✓ 已保存"
|
||||
SwingUtilities.invokeLater {
|
||||
try { Thread.sleep(500) } catch (_: InterruptedException) {}
|
||||
try { Thread.sleep(400) } catch (_: InterruptedException) {}
|
||||
saveBtn.text = "保存"
|
||||
}
|
||||
// 保存后切回浏览器视图并加载新地址
|
||||
showBrowser()
|
||||
}
|
||||
|
||||
val cancelBtn = JButton("取消")
|
||||
cancelBtn.addActionListener { showBrowser() }
|
||||
|
||||
val btnPanel = JPanel(FlowLayout(FlowLayout.LEFT, 8, 0))
|
||||
btnPanel.add(saveBtn)
|
||||
btnPanel.add(cancelBtn)
|
||||
gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 2
|
||||
panel.add(btnPanel, gbc)
|
||||
val btnPanel = JPanel(FlowLayout(FlowLayout.LEFT, 8, 0)).apply {
|
||||
add(saveBtn); add(cancelBtn)
|
||||
}
|
||||
panel.add(btnPanel, gbc.also { it.gridx = 0; it.gridy = 3; it.gridwidth = 2 })
|
||||
|
||||
gbc.gridy = 4
|
||||
gbc.weighty = 1.0
|
||||
panel.add(JBPanel<JBPanel<*>>(), gbc)
|
||||
// 弹性填充
|
||||
panel.add(JBPanel<JBPanel<*>>(), gbc.also {
|
||||
it.gridy = 4; it.weighty = 1.0
|
||||
})
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
private fun createErrorCard(): JComponent {
|
||||
val area = JTextArea()
|
||||
area.isEditable = false
|
||||
area.font = area.font.deriveFont(12f)
|
||||
area.lineWrap = true
|
||||
area.wrapStyleWord = true
|
||||
center.putClientProperty(ERROR_AREA_KEY, area)
|
||||
return JBScrollPane(area)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 浏览器行为
|
||||
// 浏览器操作
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private fun navigate(url: String) {
|
||||
val b = browser ?: return
|
||||
val fixedUrl = if (url.startsWith("http://") || url.startsWith("https://")) url else "http://$url"
|
||||
b.loadURL(fixedUrl)
|
||||
val fixed = if (url.startsWith("http://") || url.startsWith("https://")) url else "http://$url"
|
||||
b.loadURL(fixed)
|
||||
}
|
||||
|
||||
private fun setupPopupHandling(b: JBCefBrowser) {
|
||||
b.jbCefClient.addLifeSpanHandler(object : CefLifeSpanHandlerAdapter() {
|
||||
override fun onBeforePopup(
|
||||
cefBrowser: org.cef.browser.CefBrowser,
|
||||
frame: org.cef.browser.CefFrame,
|
||||
targetUrl: String,
|
||||
targetFrameName: String
|
||||
): Boolean {
|
||||
if (targetUrl.isNotBlank()) {
|
||||
BrowserUtil.browse(targetUrl)
|
||||
}
|
||||
return true // 取消弹窗
|
||||
}
|
||||
}, b.cefBrowser)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// JS ↔ Java 桥
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private fun setupBridge(b: JBCefBrowser) {
|
||||
val q = JBCefJSQuery.create(b)
|
||||
query = q
|
||||
q.addHandler { request ->
|
||||
val r = request ?: ""
|
||||
logger.debug("JS bridge request: $r")
|
||||
when {
|
||||
r == "ping" -> JBCefJSQuery.Response("pong")
|
||||
r == "getUrl" -> JBCefJSQuery.Response(b.cefBrowser.url)
|
||||
r.startsWith("openExternal:") -> {
|
||||
val url = r.removePrefix("openExternal:")
|
||||
BrowserUtil.browse(url)
|
||||
JBCefJSQuery.Response("ok")
|
||||
}
|
||||
else -> JBCefJSQuery.Response("unknown request: $r", 1, "unknown")
|
||||
}
|
||||
}
|
||||
|
||||
// 注入桥接函数:主 frame 加载完成后
|
||||
b.jbCefClient.addLoadHandler(object : CefLoadHandlerAdapter() {
|
||||
override fun onLoadEnd(cefBrowser: org.cef.browser.CefBrowser, frame: org.cef.browser.CefFrame, httpStatusCode: Int) {
|
||||
if (frame.isMain) {
|
||||
val jsCode = """
|
||||
window.$JS_BRIDGE_NAME = function(payload) {
|
||||
var callbacks = payload && payload.callbacks;
|
||||
${q.inject(
|
||||
"payload.query",
|
||||
"function(response) { if (callbacks && callbacks.onSuccess) callbacks.onSuccess(response); }",
|
||||
"function(error_code, error_message) { if (callbacks && callbacks.onFailure) callbacks.onFailure(error_code, error_message); }"
|
||||
)};
|
||||
};
|
||||
if (window.__HARMNESS_READY__) window.__HARMNESS_READY__();
|
||||
""".trimIndent()
|
||||
b.cefBrowser.executeJavaScript(jsCode, cefBrowser.url, 0)
|
||||
}
|
||||
}
|
||||
}, b.cefBrowser)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 对外方法
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** 加载当前设置中的起始地址(初次打开 / 设置变更后调用) */
|
||||
fun loadCurrentUrl() {
|
||||
val b = browser ?: return
|
||||
val url = settings.startUrl
|
||||
@@ -366,32 +401,29 @@ class HarmnessBrowserPanel(
|
||||
b.loadURL(url)
|
||||
}
|
||||
|
||||
/** 工具窗口被选中时由工厂回调:若浏览器存在则刷新,否则尝试初始化 */
|
||||
fun onToolWindowSelected() {
|
||||
if (browser == null) {
|
||||
initBrowserSafely()
|
||||
} else {
|
||||
loadCurrentUrl()
|
||||
}
|
||||
if (browser == null) initBrowserSafely() else loadCurrentUrl()
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 生命周期
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
override fun dispose() {
|
||||
query?.dispose()
|
||||
// 反射释放 jsQuery
|
||||
try {
|
||||
browser?.let { Disposer.dispose(it) }
|
||||
} catch (t: Throwable) {
|
||||
logger.warn("dispose browser failed", t)
|
||||
}
|
||||
jsQuery?.javaClass?.getMethod("dispose")?.invoke(jsQuery)
|
||||
} catch (_: Throwable) {}
|
||||
try { browser?.let { Disposer.dispose(it) } } catch (_: Throwable) {}
|
||||
browser = null
|
||||
query = null
|
||||
jsQuery = null
|
||||
bridgeAvailable = false
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CARD_STANDBY = "standby"
|
||||
private const val CARD_BROWSER = "browser"
|
||||
private const val CARD_SETTINGS = "settings"
|
||||
private const val CARD_ERROR = "error"
|
||||
private const val CARD_STANDBY = "standby"
|
||||
private const val ERROR_AREA_KEY = "harmness.errorArea"
|
||||
const val JS_BRIDGE_NAME = "harmnessBridge"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user