Initialize Harmness Browser JetBrains plugin skeleton
- Gradle + Kotlin + IntelliJ Platform SDK build - JCEF embedded browser tool window with configurable start URL - Settings page (Tools > Harmness Browser) - JS<->Java bridge scaffold (window.harmnessBridge) - Vite + React + TypeScript web scaffold for embedded page - README: build, register, upload to Marketplace, install guide
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
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 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<HarmnessBrowserPanel>(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)
|
||||
return
|
||||
}
|
||||
|
||||
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(AbstractAction("←") {
|
||||
if (cefBrowser.canGoBack()) cefBrowser.goBack()
|
||||
})
|
||||
val forward = JButton(AbstractAction("→") {
|
||||
if (cefBrowser.canGoForward()) cefBrowser.goForward()
|
||||
})
|
||||
val reload = JButton(AbstractAction("⟳") { cefBrowser.reload() })
|
||||
val settingsBtn = JButton(AbstractAction("⚙") { openSettings() })
|
||||
val externalBtn = JButton(AbstractAction("⧉") { 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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user