package com.harmness.toolwindow import com.intellij.ide.BrowserUtil import com.intellij.openapi.Disposable import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.util.Disposer import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBPanel import com.intellij.ui.jcef.JBCefApp import com.intellij.ui.jcef.JBCefBrowser import com.intellij.ui.jcef.JBCefJSQuery import org.cef.browser.CefBrowser import org.cef.handler.CefDisplayHandlerAdapter import org.cef.handler.CefLifeSpanHandlerAdapter import org.cef.handler.CefLoadHandlerAdapter import java.awt.BorderLayout import java.awt.Desktop import java.awt.FlowLayout import java.awt.event.ActionEvent import javax.swing.AbstractAction import javax.swing.JButton import javax.swing.JComponent import javax.swing.JPanel import javax.swing.JTextField /** * 嵌入浏览器面板。 * * 职责: * - 使用 JBCefBrowser 渲染用户配置的起始地址(例如本地 Harness 地址) * - 工具栏:地址栏、前进/后退、刷新、设置入口(快捷跳转到 IDE Settings 的插件配置页) * - 通过 [JBCefJSQuery] 提供 JS ↔ 插件代码双向桥 * - 拦截并处理 _blank 弹窗(默认吞掉,可选交给系统浏览器) * * 由于 IntelliJ Platform 官方插件模型目前不允许在 JCEF 浏览器内部直接调用 * "打开当前项目文件" 之类的 API(无 DOM 访问权限),插件通过注入到页面中的 * 桥接函数 [JS_BRIDGE_NAME] 让网页(如 Harness 前端)发起请求, * 再由本类把结果返回给页面。 */ class HarmnessBrowserPanel( private val project: com.intellij.openapi.project.Project, parentDisposable: Disposable ) : Disposable, JBPanel(BorderLayout()) { private val settings = SettingsState.getInstance() private lateinit var browser: JBCefBrowser private lateinit var addressField: JTextField private lateinit var query: JBCefJSQuery private val loadEnded = java.util.concurrent.atomic.AtomicBoolean(false) init { Disposer.register(parentDisposable, this) if (!JBCefApp.isSupported()) { add(JBLabel( "JCEF (Java Chromium Embedded Framework) is not supported in this IDE instance.\n" + "Please use an IDE build that bundles JCEF, or a compatible JDK.", 0), BorderLayout.CENTER) } else { browser = JBCefBrowser() // 布局:顶部工具栏 + 浏览器 add(createToolbar(), BorderLayout.NORTH) add(browser.component, BorderLayout.CENTER) // 设置弹窗处理 setupPopupHandling() // 设置 JS 桥 setupBridge() // 初始加载 loadCurrentUrl() } } // ------------------------------------------------------------------ // 工具栏 // ------------------------------------------------------------------ private fun createToolbar(): JComponent { val cefBrowser = browser.cefBrowser val back = JButton(object : AbstractAction("←") { override fun actionPerformed(e: ActionEvent) { if (cefBrowser.canGoBack()) cefBrowser.goBack() } }) val forward = JButton(object : AbstractAction("→") { override fun actionPerformed(e: ActionEvent) { if (cefBrowser.canGoForward()) cefBrowser.goForward() } }) val reload = JButton(object : AbstractAction("⟳") { override fun actionPerformed(e: ActionEvent) { cefBrowser.reload() } }) val settingsBtn = JButton(object : AbstractAction("⚙") { override fun actionPerformed(e: ActionEvent) { openSettings() } }) val externalBtn = JButton(object : AbstractAction("⧉") { override fun actionPerformed(e: ActionEvent) { openInExternalBrowser() } }) addressField = JTextField(settings.startUrl) addressField.toolTipText = "当前页面地址(可手动输入后回车跳转)" addressField.addActionListener { navigate(addressField.text) } val toolbar = JPanel(FlowLayout(FlowLayout.LEFT, 4, 2)) toolbar.add(back) toolbar.add(forward) toolbar.add(reload) toolbar.add(addressField) toolbar.add(settingsBtn) toolbar.add(externalBtn) return toolbar } private fun navigate(url: String) { val fixedUrl = if (url.startsWith("http://") || url.startsWith("https://")) url else "http://$url" browser.loadURL(fixedUrl) } private fun openSettings() { val app = com.intellij.openapi.application.ApplicationManager.getApplication() val show = Runnable { com.intellij.openapi.options.ShowSettingsUtil.getInstance() .showSettingsDialog(project, "Harmness Browser") } if (app.isDispatchThread) show.run() else app.invokeLater(show) } private fun openInExternalBrowser() { val url = addressField.text.ifBlank { settings.startUrl } if (Desktop.isDesktopSupported()) { try { Desktop.getDesktop().browse(java.net.URI(url)) } catch (e: Exception) { BrowserUtil.browse(url) } } else { BrowserUtil.browse(url) } } // ------------------------------------------------------------------ // 弹窗与外部链接策略 // ------------------------------------------------------------------ private fun setupPopupHandling() { browser.jbCefClient.addLifeSpanHandler(object : CefLifeSpanHandlerAdapter() { override fun onBeforePopup( browser: CefBrowser, frame: org.cef.browser.CefFrame, targetUrl: String, targetFrameName: String ): Boolean { // 拒绝在 IDE 内打开新窗口;改由外部系统浏览器打开 if (targetUrl.isNotBlank()) { BrowserUtil.browse(targetUrl) } return true // 取消弹窗 } }, browser.cefBrowser) browser.jbCefClient.addDisplayHandler(object : CefDisplayHandlerAdapter() { override fun onTitleChange(browser: CefBrowser, title: String?) { // 可用来同步工具窗口标题,暂时留空 } }, browser.cefBrowser) } // ------------------------------------------------------------------ // JS ↔ Java 桥 // ------------------------------------------------------------------ private fun setupBridge() { query = JBCefJSQuery.create(browser) query.addHandler { request -> val r = request ?: "" LOG.debug("JS bridge request: $r") when { r == "ping" -> JBCefJSQuery.Response("pong") r == "getUrl" -> JBCefJSQuery.Response(browser.cefBrowser.url) r.startsWith("openExternal:") -> { val url = r.removePrefix("openExternal:") BrowserUtil.browse(url) JBCefJSQuery.Response("ok") } else -> JBCefJSQuery.Response("unknown request: $r", 1, "unknown") } } // 注入桥接函数:页面加载完成后注入到 window.harmnessBridge browser.jbCefClient.addLoadHandler(object : CefLoadHandlerAdapter() { override fun onLoadEnd(browser: CefBrowser, frame: org.cef.browser.CefFrame, httpStatusCode: Int) { if (frame.isMain && loadEnded.compareAndSet(false, true)) { val jsCode = """ window.$JS_BRIDGE_NAME = function(payload) { var callbacks = payload && payload.callbacks; ${query.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() browser.executeJavaScript(jsCode, browser.url, 0) } } }, browser.cefBrowser) } // ------------------------------------------------------------------ // 对外方法(供 ToolWindow 调用) // ------------------------------------------------------------------ fun loadCurrentUrl() { if (!::browser.isInitialized) return val url = settings.startUrl addressField.text = url browser.loadURL(url) loadEnded.set(false) } fun reloadFromSettings() { loadCurrentUrl() } override fun dispose() { loadEnded.set(false) if (::query.isInitialized) { query.dispose() } if (::browser.isInitialized) { Disposer.dispose(browser) } } companion object { private val LOG = Logger.getInstance(HarmnessBrowserPanel::class.java) const val JS_BRIDGE_NAME = "harmnessBridge" } }