Release v1.0.0: in-panel settings (gear icon), default URL 127.0.0.1:8899

This commit is contained in:
harmness
2026-09-10 19:58:04 +08:00
parent dfda6042e7
commit b3fee364e0
6 changed files with 111 additions and 141 deletions
@@ -1,45 +0,0 @@
package com.harmness.toolwindow
import com.intellij.openapi.diagnostic.Logger
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBTextField
import com.intellij.util.ui.FormBuilder
import com.intellij.util.ui.UIUtil
import javax.swing.JComponent
import javax.swing.JPanel
/**
* 设置面板 UI:编辑「起始地址」并即时生效。
*/
class HarmnessBrowserConfigurablePanel {
private val urlField = JBTextField()
val panel: JPanel = FormBuilder.createFormBuilder()
.addLabeledComponent("Start URL", urlField, 1, false)
.addComponentFillVertically(JBLabel(
"<html>Embedded browser will open this URL when the tool window is shown.<br>" +
"The target site must allow embedding (no strict <code>X-Frame-Options</code> / <code>CSP frame-ancestors</code>).<br>" +
"e.g. <b>http://127.0.0.1:8888</b></html>", UIUtil.ComponentStyle.SMALL, UIUtil.FontColor.NORMAL), 1)
.panel
init {
urlField.columns = 40
urlField.emptyText.text = "http://127.0.0.1:8888"
}
fun isModified(settings: SettingsState): Boolean =
urlField.text != settings.startUrl
fun apply(settings: SettingsState) {
settings.startUrl = urlField.text.trim()
}
fun reset(settings: SettingsState) {
urlField.text = settings.startUrl
}
companion object {
private val LOG = Logger.getInstance(HarmnessBrowserConfigurablePanel::class.java)
}
}
@@ -18,24 +18,19 @@ import java.awt.Desktop
import java.awt.FlowLayout
import java.awt.event.ActionEvent
import javax.swing.AbstractAction
import javax.swing.BorderFactory
import javax.swing.JButton
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.JTextField
import javax.swing.SwingUtilities
/**
* 嵌入浏览器面板。
*
* 职责
* - 使用 JBCefBrowser 渲染用户配置的起始地址(例如本地 Harness 地址
* - 工具栏:地址栏、前进/后退、刷新、设置入口(快捷跳转到 IDE Settings 的插件配置页)
* - 通过 [JBCefJSQuery] 提供 JS ↔ 插件代码双向桥
* - 拦截并处理 _blank 弹窗(默认吞掉,可选交给系统浏览器)
*
* 由于 IntelliJ Platform 官方插件模型目前不允许在 JCEF 浏览器内部直接调用
* "打开当前项目文件" 之类的 API(无 DOM 访问权限),插件通过注入到页面中的
* 桥接函数 [JS_BRIDGE_NAME] 让网页(如 Harness 前端)发起请求,
* 再由本类把结果返回给页面。
* 两种视图模式,通过右上角「⚙」图标切换
* - [Mode.BROWSER]:显示配置好的网页(JCEF
* - [Mode.SETTINGS]:表单,可修改「起始地址」,点保存后自动切回浏览器并加载新地址
*/
class HarmnessBrowserPanel(
private val project: com.intellij.openapi.project.Project,
@@ -49,6 +44,10 @@ class HarmnessBrowserPanel(
private lateinit var query: JBCefJSQuery
private val loadEnded = java.util.concurrent.atomic.AtomicBoolean(false)
private var mode = Mode.BROWSER
private enum class Mode { BROWSER, SETTINGS }
init {
Disposer.register(parentDisposable, this)
@@ -59,21 +58,39 @@ class HarmnessBrowserPanel(
BorderLayout.CENTER)
} else {
browser = JBCefBrowser()
// 布局:顶部工具栏 + 浏览器
add(createToolbar(), BorderLayout.NORTH)
add(browser.component, BorderLayout.CENTER)
// 设置弹窗处理
setupPopupHandling()
// 设置 JS 桥
setupBridge()
// 初始加载
loadCurrentUrl()
// 顶部工具栏(右上角 ⚙)
add(createToolbar(), BorderLayout.NORTH)
// 初始为浏览器视图
showBrowserMode()
}
}
// ------------------------------------------------------------------
// 视图切换
// ------------------------------------------------------------------
private fun showBrowserMode() {
mode = Mode.BROWSER
removeAll()
add(createToolbar(), BorderLayout.NORTH)
add(browser.component, BorderLayout.CENTER)
revalidate()
repaint()
loadCurrentUrl()
}
private fun showSettingsMode() {
mode = Mode.SETTINGS
removeAll()
add(createToolbar(), BorderLayout.NORTH)
add(createSettingsForm(), BorderLayout.CENTER)
revalidate()
repaint()
}
// ------------------------------------------------------------------
// 工具栏
// ------------------------------------------------------------------
@@ -94,12 +111,13 @@ class HarmnessBrowserPanel(
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() }
override fun actionPerformed(e: ActionEvent) { toggleSettings() }
})
settingsBtn.toolTipText = if (mode == Mode.BROWSER) "设置" else "返回主界面"
settingsBtn.border = BorderFactory.createEmptyBorder(2, 6, 2, 6)
addressField = JTextField(settings.startUrl)
addressField.toolTipText = "当前页面地址(可手动输入后回车跳转)"
@@ -111,35 +129,81 @@ class HarmnessBrowserPanel(
toolbar.add(reload)
toolbar.add(addressField)
toolbar.add(settingsBtn)
toolbar.add(externalBtn)
return toolbar
}
private fun toggleSettings() {
if (mode == Mode.BROWSER) {
showSettingsMode()
} else {
// 从设置页切回主界面时重新加载
showBrowserMode()
}
}
private fun navigate(url: String) {
val fixedUrl = if (url.startsWith("http://") || url.startsWith("https://")) url else "http://$url"
browser.loadURL(fixedUrl)
loadEnded.set(false)
}
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)
private fun createSettingsForm(): JComponent {
val panel = JBPanel<JBPanel<*>>()
panel.layout = java.awt.GridBagLayout()
val gbc = java.awt.GridBagConstraints()
gbc.insets = java.awt.Insets(6, 8, 6, 8)
gbc.fill = java.awt.GridBagConstraints.HORIZONTAL
gbc.anchor = java.awt.GridBagConstraints.NORTH
val title = JBLabel("<html><h2>Settings</h2></html>")
gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 2
panel.add(title, gbc)
gbc.gridwidth = 1
val label = JBLabel("起始地址 (Start URL):")
gbc.gridx = 0; gbc.gridy = 1
panel.add(label, gbc)
val urlField = JTextField(settings.startUrl, 30)
gbc.gridx = 1; gbc.gridy = 1
panel.add(urlField, gbc)
val hint = JBLabel("<html><small>例如:http://127.0.0.1:8899 (本地 Harness 服务地址)</small></html>")
gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 2
panel.add(hint, gbc)
gbc.gridwidth = 1
val saveBtn = JButton("保存")
saveBtn.addActionListener {
settings.startUrl = urlField.text.trim()
saveBtn.text = "已保存 ✓"
SwingUtilities.invokeLater {
Thread.sleep(400)
saveBtn.text = "保存"
}
} else {
BrowserUtil.browse(url)
showBrowserMode()
}
val cancelBtn = JButton("取消")
cancelBtn.addActionListener {
showBrowserMode()
}
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)
gbc.gridy = 4
gbc.weighty = 1.0
panel.add(JBPanel<JBPanel<*>>(), gbc)
return panel
}
// ------------------------------------------------------------------
@@ -224,10 +288,6 @@ class HarmnessBrowserPanel(
loadEnded.set(false)
}
fun reloadFromSettings() {
loadCurrentUrl()
}
override fun dispose() {
loadEnded.set(false)
if (::query.isInitialized) {
@@ -1,36 +0,0 @@
package com.harmness.toolwindow
import com.intellij.openapi.options.Configurable
import javax.swing.JComponent
/**
* Settings configurable registered in plugin.xml.
* Shows the HarmnessBrowserConfigurablePanel under Tools → Harmness Browser.
*/
class HarmnessSettingsConfigurable : Configurable {
private var panel: HarmnessBrowserConfigurablePanel? = null
override fun getDisplayName(): String = "Harmness Browser"
override fun getHelpTopic(): String? = null
override fun createComponent(): JComponent {
panel = HarmnessBrowserConfigurablePanel()
return panel!!.panel
}
override fun isModified(): Boolean = panel?.isModified(SettingsState.getInstance()) ?: false
override fun apply() {
panel?.apply(SettingsState.getInstance())
}
override fun reset() {
panel?.reset(SettingsState.getInstance())
}
override fun disposeUIResources() {
panel = null
}
}
@@ -8,18 +8,14 @@ import com.intellij.openapi.components.Storage
import com.intellij.openapi.diagnostic.Logger
/**
* 插件设置:目前仅保存一个「浏览器起始地址」。
*
* 该地址必须是一个允许被嵌入的 Web 应用地址(例如本地 Harness 服务地址),
* 并且需要在页面上开启允许被 iframe/嵌入浏览(X-Frame-Options / CSP frame-ancestors
* 或者由 Harness 提供专门的无 frame 限制入口。
* 插件设置:仅保存「浏览器起始地址」。
*/
@State(name = "HarmnessToolwindowSettings", storages = [Storage("harmness-toolwindow.xml")])
@Service(Service.Level.APP)
class SettingsState : PersistentStateComponent<SettingsState.State> {
data class State(
var startUrl: String = "http://127.0.0.1:8888",
var startUrl: String = DEFAULT_URL,
)
private var myState = State()
@@ -39,7 +35,8 @@ class SettingsState : PersistentStateComponent<SettingsState.State> {
}
companion object {
const val DEFAULT_URL = "http://127.0.0.1:8888"
// 与 DSH Web GUI 默认端口一致
const val DEFAULT_URL = "http://127.0.0.1:8899"
fun getInstance(): SettingsState =
ServiceManager.getService(SettingsState::class.java)