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 -1
View File
@@ -6,7 +6,7 @@ plugins {
} }
group = "com.prepeep" group = "com.prepeep"
version = "1.0.0-SNAPSHOT" version = "1.0.0"
repositories { repositories {
mavenCentral() mavenCentral()
@@ -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.FlowLayout
import java.awt.event.ActionEvent import java.awt.event.ActionEvent
import javax.swing.AbstractAction import javax.swing.AbstractAction
import javax.swing.BorderFactory
import javax.swing.JButton import javax.swing.JButton
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.JPanel import javax.swing.JPanel
import javax.swing.JTextField import javax.swing.JTextField
import javax.swing.SwingUtilities
/** /**
* 嵌入浏览器面板。 * 嵌入浏览器面板。
* *
* 职责 * 两种视图模式,通过右上角「⚙」图标切换
* - 使用 JBCefBrowser 渲染用户配置的起始地址(例如本地 Harness 地址 * - [Mode.BROWSER]:显示配置好的网页(JCEF
* - 工具栏:地址栏、前进/后退、刷新、设置入口(快捷跳转到 IDE Settings 的插件配置页) * - [Mode.SETTINGS]:表单,可修改「起始地址」,点保存后自动切回浏览器并加载新地址
* - 通过 [JBCefJSQuery] 提供 JS ↔ 插件代码双向桥
* - 拦截并处理 _blank 弹窗(默认吞掉,可选交给系统浏览器)
*
* 由于 IntelliJ Platform 官方插件模型目前不允许在 JCEF 浏览器内部直接调用
* "打开当前项目文件" 之类的 API(无 DOM 访问权限),插件通过注入到页面中的
* 桥接函数 [JS_BRIDGE_NAME] 让网页(如 Harness 前端)发起请求,
* 再由本类把结果返回给页面。
*/ */
class HarmnessBrowserPanel( class HarmnessBrowserPanel(
private val project: com.intellij.openapi.project.Project, private val project: com.intellij.openapi.project.Project,
@@ -49,6 +44,10 @@ class HarmnessBrowserPanel(
private lateinit var query: JBCefJSQuery private lateinit var query: JBCefJSQuery
private val loadEnded = java.util.concurrent.atomic.AtomicBoolean(false) private val loadEnded = java.util.concurrent.atomic.AtomicBoolean(false)
private var mode = Mode.BROWSER
private enum class Mode { BROWSER, SETTINGS }
init { init {
Disposer.register(parentDisposable, this) Disposer.register(parentDisposable, this)
@@ -59,21 +58,39 @@ class HarmnessBrowserPanel(
BorderLayout.CENTER) BorderLayout.CENTER)
} else { } else {
browser = JBCefBrowser() browser = JBCefBrowser()
// 布局:顶部工具栏 + 浏览器
add(createToolbar(), BorderLayout.NORTH)
add(browser.component, BorderLayout.CENTER)
// 设置弹窗处理
setupPopupHandling() setupPopupHandling()
// 设置 JS 桥
setupBridge() 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("") { val reload = JButton(object : AbstractAction("") {
override fun actionPerformed(e: ActionEvent) { cefBrowser.reload() } override fun actionPerformed(e: ActionEvent) { cefBrowser.reload() }
}) })
// 右上角:设置图标(⚙),点击在两种模式间切换
val settingsBtn = JButton(object : AbstractAction("") { val settingsBtn = JButton(object : AbstractAction("") {
override fun actionPerformed(e: ActionEvent) { openSettings() } override fun actionPerformed(e: ActionEvent) { toggleSettings() }
})
val externalBtn = JButton(object : AbstractAction("") {
override fun actionPerformed(e: ActionEvent) { openInExternalBrowser() }
}) })
settingsBtn.toolTipText = if (mode == Mode.BROWSER) "设置" else "返回主界面"
settingsBtn.border = BorderFactory.createEmptyBorder(2, 6, 2, 6)
addressField = JTextField(settings.startUrl) addressField = JTextField(settings.startUrl)
addressField.toolTipText = "当前页面地址(可手动输入后回车跳转)" addressField.toolTipText = "当前页面地址(可手动输入后回车跳转)"
@@ -111,35 +129,81 @@ class HarmnessBrowserPanel(
toolbar.add(reload) toolbar.add(reload)
toolbar.add(addressField) toolbar.add(addressField)
toolbar.add(settingsBtn) toolbar.add(settingsBtn)
toolbar.add(externalBtn)
return toolbar return toolbar
} }
private fun toggleSettings() {
if (mode == Mode.BROWSER) {
showSettingsMode()
} else {
// 从设置页切回主界面时重新加载
showBrowserMode()
}
}
private fun navigate(url: String) { private fun navigate(url: String) {
val fixedUrl = if (url.startsWith("http://") || url.startsWith("https://")) url else "http://$url" val fixedUrl = if (url.startsWith("http://") || url.startsWith("https://")) url else "http://$url"
browser.loadURL(fixedUrl) 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() { private fun createSettingsForm(): JComponent {
val url = addressField.text.ifBlank { settings.startUrl } val panel = JBPanel<JBPanel<*>>()
if (Desktop.isDesktopSupported()) { panel.layout = java.awt.GridBagLayout()
try { val gbc = java.awt.GridBagConstraints()
Desktop.getDesktop().browse(java.net.URI(url)) gbc.insets = java.awt.Insets(6, 8, 6, 8)
} catch (e: Exception) { gbc.fill = java.awt.GridBagConstraints.HORIZONTAL
BrowserUtil.browse(url) 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 { showBrowserMode()
BrowserUtil.browse(url)
} }
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) loadEnded.set(false)
} }
fun reloadFromSettings() {
loadCurrentUrl()
}
override fun dispose() { override fun dispose() {
loadEnded.set(false) loadEnded.set(false)
if (::query.isInitialized) { 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 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")]) @State(name = "HarmnessToolwindowSettings", storages = [Storage("harmness-toolwindow.xml")])
@Service(Service.Level.APP) @Service(Service.Level.APP)
class SettingsState : PersistentStateComponent<SettingsState.State> { class SettingsState : PersistentStateComponent<SettingsState.State> {
data class State( data class State(
var startUrl: String = "http://127.0.0.1:8888", var startUrl: String = DEFAULT_URL,
) )
private var myState = State() private var myState = State()
@@ -39,7 +35,8 @@ class SettingsState : PersistentStateComponent<SettingsState.State> {
} }
companion object { 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 = fun getInstance(): SettingsState =
ServiceManager.getService(SettingsState::class.java) ServiceManager.getService(SettingsState::class.java)
+1 -7
View File
@@ -9,7 +9,7 @@
IDEA without switching to a separate browser window. IDEA without switching to a separate browser window.
]]></description> ]]></description>
<idea-version since-build="242" until-build="251.*"/> <idea-version since-build="242" until-build="263.*"/>
<depends>com.intellij.modules.platform</depends> <depends>com.intellij.modules.platform</depends>
@@ -21,12 +21,6 @@
anchor="right" anchor="right"
/> />
<applicationConfigurable
parent="tools"
instance="com.harmness.toolwindow.HarmnessSettingsConfigurable"
id="com.harmness.toolwindow.HarmnessSettingsConfigurable"
displayName="Harmness Browser"/>
<applicationService <applicationService
serviceImplementation="com.harmness.toolwindow.SettingsState"/> serviceImplementation="com.harmness.toolwindow.SettingsState"/>