From e51c046007d930bc1324674858289096de96bded Mon Sep 17 00:00:00 2001 From: harmness Date: Thu, 10 Sep 2026 20:17:17 +0800 Subject: [PATCH] Defensive panel: toolbar/settings independent of JCEF, lazy browser init, error card, drop WindowFocusService --- .../toolwindow/HarmnessBrowserPanel.kt | 332 +++++++++++------- .../toolwindow/HarmnessToolWindowFactory.kt | 40 ++- .../com/harmness/toolwindow/SettingsState.kt | 7 +- .../harmness/toolwindow/WindowFocusService.kt | 48 --- src/main/resources/META-INF/plugin.xml | 6 - 5 files changed, 242 insertions(+), 191 deletions(-) delete mode 100644 src/main/kotlin/com/harmness/toolwindow/WindowFocusService.kt diff --git a/src/main/kotlin/com/harmness/toolwindow/HarmnessBrowserPanel.kt b/src/main/kotlin/com/harmness/toolwindow/HarmnessBrowserPanel.kt index c41f7f2..abb1519 100644 --- a/src/main/kotlin/com/harmness/toolwindow/HarmnessBrowserPanel.kt +++ b/src/main/kotlin/com/harmness/toolwindow/HarmnessBrowserPanel.kt @@ -6,31 +6,41 @@ 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.components.JBScrollPane 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.CardLayout import java.awt.Desktop import java.awt.FlowLayout +import java.awt.GridBagConstraints +import java.awt.GridBagLayout +import java.awt.Insets 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.JTextArea import javax.swing.JTextField import javax.swing.SwingUtilities /** - * 嵌入浏览器面板。 + * Harmness 嵌入浏览器面板。 * - * 两种视图模式,通过右上角「⚙」图标切换: - * - [Mode.BROWSER]:显示配置好的网页(JCEF) - * - [Mode.SETTINGS]:表单,可修改「起始地址」,点保存后自动切回浏览器并加载新地址 + * 布局: + * - NORTH:工具栏(← → ⟳ 地址栏 ⚙) + * - CENTER:CardLayout,包含三张卡: + * - "settings":设置表单(起始地址 + 保存/取消) + * - "browser":JCEF 浏览器(懒初始化,失败时显示错误) + * - "error":初始化失败时的错误信息 + * + * 关键设计:工具栏和设置表单**不依赖** JCEF。即使 JCEF 初始化失败, + * 界面仍然可用,错误信息会显示在中心区域,方便排错。 */ class HarmnessBrowserPanel( private val project: com.intellij.openapi.project.Project, @@ -38,57 +48,33 @@ class HarmnessBrowserPanel( ) : Disposable, JBPanel(BorderLayout()) { private val settings = SettingsState.getInstance() + private val logger = Logger.getInstance(HarmnessBrowserPanel::class.java) - private lateinit var browser: JBCefBrowser - private lateinit var addressField: JTextField - private lateinit var query: JBCefJSQuery - private val loadEnded = java.util.concurrent.atomic.AtomicBoolean(false) + // JCEF 浏览器(懒初始化) + private var browser: JBCefBrowser? = null + private var query: JBCefJSQuery? = null - private var mode = Mode.BROWSER + private var addressField: JTextField = JTextField() + private var settingsUrlField: JTextField = JTextField(30) - private enum class Mode { BROWSER, SETTINGS } + private val cardLayout = CardLayout() + private val center = JBPanel>(cardLayout) 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() - setupPopupHandling() - setupBridge() - - // 顶部工具栏(右上角 ⚙) - add(createToolbar(), BorderLayout.NORTH) - // 初始为浏览器视图 - showBrowserMode() - } - } - - // ------------------------------------------------------------------ - // 视图切换 - // ------------------------------------------------------------------ - - private fun showBrowserMode() { - mode = Mode.BROWSER - removeAll() + // 工具栏(不依赖 JCEF,总是先建立) add(createToolbar(), BorderLayout.NORTH) - add(browser.component, BorderLayout.CENTER) - revalidate() - repaint() - loadCurrentUrl() - } + add(center, BorderLayout.CENTER) - private fun showSettingsMode() { - mode = Mode.SETTINGS - removeAll() - add(createToolbar(), BorderLayout.NORTH) - add(createSettingsForm(), BorderLayout.CENTER) - revalidate() - repaint() + // 中心卡片 + center.add(createStandbyCard(), CARD_STANDBY) + center.add(createSettingsCard(), CARD_SETTINGS) + center.add(createErrorCard(), CARD_ERROR) + cardLayout.show(center, CARD_STANDBY) + + // 在窗口系统就绪后异步初始化浏览器,避免阻塞 EDT 或初始化失败拖垮整个面板 + SwingUtilities.invokeLater { initBrowserSafely() } } // ------------------------------------------------------------------ @@ -96,28 +82,35 @@ class HarmnessBrowserPanel( // ------------------------------------------------------------------ private fun createToolbar(): JComponent { - val cefBrowser = browser.cefBrowser - val back = JButton(object : AbstractAction("←") { override fun actionPerformed(e: ActionEvent) { - if (cefBrowser.canGoBack()) cefBrowser.goBack() + val cef = browser?.cefBrowser + if (cef != null && cef.canGoBack()) cef.goBack() } }) val forward = JButton(object : AbstractAction("→") { override fun actionPerformed(e: ActionEvent) { - if (cefBrowser.canGoForward()) cefBrowser.goForward() + val cef = browser?.cefBrowser + if (cef != null && cef.canGoForward()) cef.goForward() } }) val reload = JButton(object : AbstractAction("⟳") { - override fun actionPerformed(e: ActionEvent) { cefBrowser.reload() } + override fun actionPerformed(e: ActionEvent) { browser?.cefBrowser?.reload() } }) - // 右上角:设置图标(⚙),点击在两种模式间切换 + // 右上角设置图标:在浏览器视图与设置视图间切换 val settingsBtn = JButton(object : AbstractAction("⚙") { - override fun actionPerformed(e: ActionEvent) { toggleSettings() } + override fun actionPerformed(e: ActionEvent) { + if (cardLayout == null) return + if (currentCard == CARD_SETTINGS) { + showBrowser() + } else { + showSettings() + } + } }) - settingsBtn.toolTipText = if (mode == Mode.BROWSER) "设置" else "返回主界面" - settingsBtn.border = BorderFactory.createEmptyBorder(2, 6, 2, 6) + settingsBtn.toolTipText = "设置" + settingsBtn.border = BorderFactory.createEmptyBorder(2, 8, 2, 8) addressField = JTextField(settings.startUrl) addressField.toolTipText = "当前页面地址(可手动输入后回车跳转)" @@ -132,34 +125,110 @@ class HarmnessBrowserPanel( return toolbar } - private fun toggleSettings() { - if (mode == Mode.BROWSER) { - showSettingsMode() + // ------------------------------------------------------------------ + // 卡片管理与视图切换 + // ------------------------------------------------------------------ + + private var currentCard: String = CARD_STANDBY + + private fun showCard(name: String) { + currentCard = name + cardLayout.show(center, name) + } + + private fun showSettings() { + settingsUrlField.text = settings.startUrl + showCard(CARD_SETTINGS) + } + + private fun showBrowser() { + if (browser == null) { + // 浏览器还没建好:尝试初始化;失败显示错误 + initBrowserSafely() } else { - // 从设置页切回主界面时重新加载 - showBrowserMode() + showCard(CARD_BROWSER) + loadCurrentUrl() } } - private fun navigate(url: String) { - val fixedUrl = if (url.startsWith("http://") || url.startsWith("https://")) url else "http://$url" - browser.loadURL(fixedUrl) - loadEnded.set(false) + // ------------------------------------------------------------------ + // JCEF 浏览器初始化(安全包装) + // ------------------------------------------------------------------ + + private fun initBrowserSafely() { + try { + if (browser != null) return + if (!JBCefApp.isSupported()) { + showError("JCEF (Java Chromium Embedded Framework) 在当前 IDE 中不受支持。\n" + + "请使用官方发行版 IDE(内置 JBR),或检查 IDE 是否禁用了 JCEF。") + return + } + + val b = JBCefBrowser() + browser = b + setupPopupHandling(b) + setupBridge(b) + + // 把浏览器组件放进 "browser" 卡片 + center.add(b.component, CARD_BROWSER) + if (currentCard != CARD_SETTINGS) { + showCard(CARD_BROWSER) + } + loadCurrentUrl() + } catch (t: Throwable) { + logger.error("Failed to init JCEF browser", t) + showError(buildErrorMessage(t)) + } + } + + 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') + } + 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 createSettingsForm(): JComponent { - val panel = 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 + private fun createStandbyCard(): JComponent { + val p = JBPanel>(BorderLayout()) + val label = JBLabel("正在初始化浏览器…", 0) + p.add(label, BorderLayout.CENTER) + return p + } - val title = JBLabel("

Settings

") + /** 设置表单:起始地址 + 保存/取消 */ + private fun createSettingsCard(): JComponent { + val panel = JBPanel>() + panel.layout = GridBagLayout() + val gbc = GridBagConstraints() + gbc.insets = Insets(8, 10, 8, 10) + gbc.fill = GridBagConstraints.HORIZONTAL + gbc.anchor = GridBagConstraints.NORTHWEST + + val title = JBLabel("

设置

") gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 2 panel.add(title, gbc) gbc.gridwidth = 1 @@ -168,30 +237,30 @@ class HarmnessBrowserPanel( gbc.gridx = 0; gbc.gridy = 1 panel.add(label, gbc) - val urlField = JTextField(settings.startUrl, 30) + settingsUrlField = JTextField(settings.startUrl, 28) gbc.gridx = 1; gbc.gridy = 1 - panel.add(urlField, gbc) + panel.add(settingsUrlField, gbc) - val hint = JBLabel("例如:http://127.0.0.1:8899 (本地 Harness 服务地址)") + val hint = JBLabel("例如:http://127.0.0.1:8899 (本地 Harmness 服务地址)
" + + "目标站点需允许被嵌入(不要有 X-Frame-Options / CSP frame-ancestors 限制)
") 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() + settings.startUrl = settingsUrlField.text.trim() saveBtn.text = "已保存 ✓" SwingUtilities.invokeLater { - Thread.sleep(400) + try { Thread.sleep(500) } catch (_: InterruptedException) {} saveBtn.text = "保存" } - showBrowserMode() + // 保存后切回浏览器视图并加载新地址 + showBrowser() } val cancelBtn = JButton("取消") - cancelBtn.addActionListener { - showBrowserMode() - } + cancelBtn.addActionListener { showBrowser() } val btnPanel = JPanel(FlowLayout(FlowLayout.LEFT, 8, 0)) btnPanel.add(saveBtn) @@ -206,45 +275,55 @@ class HarmnessBrowserPanel( 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 setupPopupHandling() { - browser.jbCefClient.addLifeSpanHandler(object : CefLifeSpanHandlerAdapter() { + 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) + } + + private fun setupPopupHandling(b: JBCefBrowser) { + b.jbCefClient.addLifeSpanHandler(object : CefLifeSpanHandlerAdapter() { override fun onBeforePopup( - browser: CefBrowser, + cefBrowser: org.cef.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) + }, b.cefBrowser) } // ------------------------------------------------------------------ // JS ↔ Java 桥 // ------------------------------------------------------------------ - private fun setupBridge() { - query = JBCefJSQuery.create(browser) - query.addHandler { request -> + private fun setupBridge(b: JBCefBrowser) { + val q = JBCefJSQuery.create(b) + query = q + q.addHandler { request -> val r = request ?: "" - LOG.debug("JS bridge request: $r") + logger.debug("JS bridge request: $r") when { r == "ping" -> JBCefJSQuery.Response("pong") - r == "getUrl" -> JBCefJSQuery.Response(browser.cefBrowser.url) + r == "getUrl" -> JBCefJSQuery.Response(b.cefBrowser.url) r.startsWith("openExternal:") -> { val url = r.removePrefix("openExternal:") BrowserUtil.browse(url) @@ -254,52 +333,65 @@ class HarmnessBrowserPanel( } } - // 注入桥接函数:页面加载完成后注入到 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)) { + // 注入桥接函数:主 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; - ${query.inject( + ${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() - browser.executeJavaScript(jsCode, browser.url, 0) + b.cefBrowser.executeJavaScript(jsCode, cefBrowser.url, 0) } } - }, browser.cefBrowser) + }, b.cefBrowser) } // ------------------------------------------------------------------ - // 对外方法(供 ToolWindow 调用) + // 对外方法 // ------------------------------------------------------------------ + /** 加载当前设置中的起始地址(初次打开 / 设置变更后调用) */ fun loadCurrentUrl() { - if (!::browser.isInitialized) return + val b = browser ?: return val url = settings.startUrl addressField.text = url - browser.loadURL(url) - loadEnded.set(false) + b.loadURL(url) + } + + /** 工具窗口被选中时由工厂回调:若浏览器存在则刷新,否则尝试初始化 */ + fun onToolWindowSelected() { + if (browser == null) { + initBrowserSafely() + } else { + loadCurrentUrl() + } } override fun dispose() { - loadEnded.set(false) - if (::query.isInitialized) { - query.dispose() - } - if (::browser.isInitialized) { - Disposer.dispose(browser) + query?.dispose() + try { + browser?.let { Disposer.dispose(it) } + } catch (t: Throwable) { + logger.warn("dispose browser failed", t) } + browser = null + query = null } companion object { - private val LOG = Logger.getInstance(HarmnessBrowserPanel::class.java) + 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" } } \ No newline at end of file diff --git a/src/main/kotlin/com/harmness/toolwindow/HarmnessToolWindowFactory.kt b/src/main/kotlin/com/harmness/toolwindow/HarmnessToolWindowFactory.kt index 33509d0..c746559 100644 --- a/src/main/kotlin/com/harmness/toolwindow/HarmnessToolWindowFactory.kt +++ b/src/main/kotlin/com/harmness/toolwindow/HarmnessToolWindowFactory.kt @@ -5,34 +5,46 @@ import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.Project import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindowFactory +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBPanel import com.intellij.ui.content.ContentFactory +import com.intellij.ui.content.ContentManagerEvent +import com.intellij.ui.content.ContentManagerListener +import java.awt.BorderLayout /** - * 本插件的工具窗口工厂。 + * 工具窗口工厂:创建侧边栏 "Harmness Browser"。 * - * 工具窗口是 IntelliJ 侧边栏的区域(如 Project、Terminal、Version Control), - * 由 IntelliJ 自动管理生命周期。我们在此创建一个自定义工具窗口 "Harmness Browser", - * 其内容就是 [HarmnessBrowserPanel]。 + * 采用防御式创建:面板构造或 JCEF 初始化失败时不会让整个工具窗口空白, + * 而是显示具体错误信息,便于排查。 */ class HarmnessToolWindowFactory : ToolWindowFactory, DumbAware { override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { LOG.info("Creating Harmness Browser tool window for project: ${project.name}") - val panel = HarmnessBrowserPanel(project, toolWindow.disposable) + // 面板构造抛异常时,用一个错误提示面板代替,避免 IDE 显示空白内容 + val panel = try { + HarmnessBrowserPanel(project, toolWindow.disposable) + } catch (t: Throwable) { + LOG.error("Failed to create HarmnessBrowserPanel", t) + val errPanel = JBPanel>(BorderLayout()) + val msg = buildString { + append("创建 Harmness 面板失败:\n") + append(t.javaClass.simpleName).append(": ").append(t.message ?: "no message") + } + errPanel.add(JBLabel(msg), BorderLayout.CENTER) + errPanel + } val content = ContentFactory.getInstance().createContent(panel, "Browser", false) toolWindow.contentManager.addContent(content) - // 当工具窗口被选中时刷新 - val contentMgr = toolWindow.contentManager - contentMgr.addContentManagerListener(object : com.intellij.ui.content.ContentManagerListener { - override fun selectionChanged(event: com.intellij.ui.content.ContentManagerEvent) { - val isActive = event.content == contentMgr.selectedContent - WindowFocusService.getInstance().setActive(isActive) - if (isActive) { - panel.loadCurrentUrl() - } + toolWindow.contentManager.addContentManagerListener(object : ContentManagerListener { + override fun selectionChanged(event: ContentManagerEvent) { + if (event.content != toolWindow.contentManager.selectedContent) return + runCatching { (panel as? HarmnessBrowserPanel)?.onToolWindowSelected() } + .onFailure { LOG.warn("selectionChanged handler failed", it) } } }) } diff --git a/src/main/kotlin/com/harmness/toolwindow/SettingsState.kt b/src/main/kotlin/com/harmness/toolwindow/SettingsState.kt index a2a15ec..cfe9ff2 100644 --- a/src/main/kotlin/com/harmness/toolwindow/SettingsState.kt +++ b/src/main/kotlin/com/harmness/toolwindow/SettingsState.kt @@ -2,13 +2,15 @@ package com.harmness.toolwindow import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.Service -import com.intellij.openapi.components.ServiceManager import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger /** * 插件设置:仅保存「浏览器起始地址」。 + * + * 通过 @Service 注解注册(无需在 plugin.xml 中重复声明)。 */ @State(name = "HarmnessToolwindowSettings", storages = [Storage("harmness-toolwindow.xml")]) @Service(Service.Level.APP) @@ -38,7 +40,6 @@ class SettingsState : PersistentStateComponent { // 与 DSH Web GUI 默认端口一致 const val DEFAULT_URL = "http://127.0.0.1:8899" - fun getInstance(): SettingsState = - ServiceManager.getService(SettingsState::class.java) + fun getInstance(): SettingsState = service() } } \ No newline at end of file diff --git a/src/main/kotlin/com/harmness/toolwindow/WindowFocusService.kt b/src/main/kotlin/com/harmness/toolwindow/WindowFocusService.kt deleted file mode 100644 index 85c9d61..0000000 --- a/src/main/kotlin/com/harmness/toolwindow/WindowFocusService.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.harmness.toolwindow - -import com.intellij.openapi.components.Service -import com.intellij.openapi.components.service -import com.intellij.openapi.diagnostic.Logger - -/** - * 全局单例服务,负责把「工具窗口是否可见/激活」的实时状态广播给浏览器面板。 - * - * 之所以需要它:JCEF 浏览器的 JavaScript 侧想知道窗口何时被聚焦/失焦, - * 以便暂停或恢复渲染(例如 Harness 界面在后台时降低 CPU 占用)。 - * 由于 ToolWindow 在内容区与焦点事件里提供的上下文有限,我们用这个轻量级 - * 事件总线在 ToolWindowManager 和 JBCefBrowser 之间传递状态。 - */ -@Service(Service.Level.APP) -class WindowFocusService { - private val logger = Logger.getInstance(WindowFocusService::class.java) - - @Volatile - var active: Boolean = false - private set - - private val listeners = mutableListOf<(Boolean) -> Unit>() - - @Synchronized - fun addListener(listener: (Boolean) -> Unit) { - listeners.add(listener) - // 注册时立即补发一次当前状态,避免面板错过初始状态 - listener(active) - } - - @Synchronized - fun removeListener(listener: (Boolean) -> Unit) { - listeners.remove(listener) - } - - @Synchronized - fun setActive(value: Boolean) { - if (active == value) return - active = value - logger.debug("Window focus changed: $value") - listeners.toList().forEach { it(value) } - } - - companion object { - fun getInstance(): WindowFocusService = service() - } -} \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index e41bb7d..4ff2775 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -20,11 +20,5 @@ factoryClass="com.harmness.toolwindow.HarmnessToolWindowFactory" anchor="right" /> - - - - \ No newline at end of file