package com.chuangzhou.vivid2D.browser; import com.axis.innovators.box.AxisInnovatorsBox; import com.axis.innovators.box.events.BrowserCreationCallback; import com.google.gson.Gson; import com.google.gson.JsonObject; import org.cef.CefApp; import org.cef.CefClient; import org.cef.CefSettings; import org.cef.browser.CefBrowser; import org.cef.browser.CefFrame; import org.cef.browser.CefMessageRouter; import org.cef.callback.CefContextMenuParams; import org.cef.callback.CefJSDialogCallback; import org.cef.callback.CefMenuModel; import org.cef.callback.CefQueryCallback; import org.cef.handler.*; import org.cef.misc.BoolRef; import org.cef.network.CefRequest; import javax.swing.*; import java.awt.*; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.event.*; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URI; import java.util.function.Consumer; import static org.cef.callback.CefMenuModel.MenuId.MENU_ID_USER_FIRST; /** * @author tzdwindows 7 */ public class BrowserWindowJDialog extends JDialog { private final String windowId; private final String htmlUrl; private CefApp cefApp; private CefClient client; private CefBrowser browser; private final Component browserComponent; private final String htmlPath; //private boolean isInitialized = false; private WindowOperationHandler operationHandler; private static Thread cefThread; private CefMessageRouter msgRouter; public static class Builder { private String windowId; private String title = "JCEF Window"; private Dimension size = new Dimension(800, 600); private WindowOperationHandler operationHandler; private String htmlPath; private Image icon; private JFrame parentFrame; private boolean resizable = true; // 默认允许调整大小 private boolean maximizable = true; // 默认允许最大化 private boolean minimizable = true; // 默认允许最小化 private String htmlUrl = ""; private BrowserCreationCallback browserCreationCallback; private boolean openLinksInExternalBrowser = true; // 默认使用外部浏览器 public Builder resizable(boolean resizable) { this.resizable = resizable; return this; } public Builder maximizable(boolean maximizable) { this.maximizable = maximizable; return this; } public Builder minimizable(boolean minimizable) { this.minimizable = minimizable; return this; } public Builder(String windowId) { this.windowId = windowId; } /** * 设置浏览器窗口标题 * @param title 标题 */ public Builder title(String title) { this.title = title; return this; } /** * 设置链接打开方式 * * @param openInBrowser 是否在当前浏览器窗口中打开链接 * true - 在当前浏览器窗口中打开链接(本地跳转) * false - 使用系统默认浏览器打开链接(外部跳转) * @return Builder实例,支持链式调用 * * @apiNote 此方法控制两种不同的链接打开行为: * 1. 当设置为true时: * - 所有链接将在当前CEF浏览器窗口内打开 * * 2. 当设置为false时(默认值): * - 所有链接将在系统默认浏览器中打开 * - 更安全,避免潜在的安全风险 * - 适用于简单的信息展示场景 * * @implNote 内部实现说明: * - 实际存储的是反向值(openLinksInExternalBrowser) * - 这样设置是为了保持与历史版本的兼容性 * - 方法名使用"openInBrowser"更符合用户直觉 * * @example 使用示例: * // 在当前窗口打开链接 * new Builder().openLinksInBrowser(true).build(); * * // 使用系统浏览器打开链接(默认) * new Builder().openLinksInBrowser(false).build(); * * @see #openLinksInExternalBrowser 内部存储字段 * @see CefLifeSpanHandler#onBeforePopup 弹窗处理实现 * @see CefRequestHandler#onBeforeBrowse 导航处理实现 */ public Builder openLinksInBrowser(boolean openInBrowser) { this.openLinksInExternalBrowser = !openInBrowser; return this; } /** * 设置浏览器创建回调 * @param callback 回调 */ public Builder setBrowserCreationCallback(BrowserCreationCallback callback){ this.browserCreationCallback = callback; return this; } /** * 设置浏览器窗口大小 * @param width 宽度 * @param height 高度 */ public Builder size(int width, int height) { this.size = new Dimension(width, height); return this; } /** * 设置浏览器窗口父窗口 * @param parent 父窗口 */ public Builder parentFrame(JFrame parent) { this.parentFrame = parent; return this; } /** * 设置浏览器触发事件 * @param handler 事件处理器 */ public Builder operationHandler(WindowOperationHandler handler) { this.operationHandler = handler; return this; } /** * 设置浏览器图标 * @param icon 图标 */ public Builder icon(Image icon) { this.icon = icon; return this; } /** * 设置HTML路径 */ public BrowserWindowJDialog build() { if (htmlUrl.isEmpty()) { if (this.htmlPath == null || this.htmlPath.isEmpty()) { throw new IllegalArgumentException("HTML paths cannot be empty"); } File htmlFile = new File(this.htmlPath); if (!htmlFile.exists()) { throw new RuntimeException("The HTML file does not exist: " + htmlFile.getAbsolutePath()); } } return new BrowserWindowJDialog(this); } /** * 设置HTML路径 * @param path HTML路径 */ public Builder htmlPath(String path) { this.htmlPath = path; return this; } /** * 使用Url * @param htmlUrl Url路径 */ public Builder htmlUrl(String htmlUrl) { this.htmlUrl = htmlUrl; return this; } } private BrowserWindowJDialog(Builder builder) { // 根据父窗口是否存在,设置是否为模态对话框 super(builder.parentFrame, builder.title, builder.parentFrame != null); this.windowId = builder.windowId; this.htmlPath = builder.htmlPath; this.htmlUrl = builder.htmlUrl; this.operationHandler = builder.operationHandler; // 设置图标(如果存在) if (builder.icon != null) { setIconImage(builder.icon); } // 初始化浏览器组件 try { this.browserComponent = initializeCef(builder); if (operationHandler != null) { setupMessageHandlers(operationHandler); } } catch (Exception e) { JOptionPane.showMessageDialog(this, "初始化失败: " + e.getMessage()); throw new RuntimeException(e); } } private static boolean isInitialized = false; private Component initializeCef(Builder builder) throws MalformedURLException { if (!isInitialized) { isInitialized = true; try { this.cefApp = CefAppManager.getInstance(); //CefAppManager.incrementBrowserCount(); client = cefApp.createClient(); client.addDisplayHandler(new CefDisplayHandler (){ @Override public void onAddressChange(CefBrowser browser, CefFrame frame, String url) {} @Override public void onTitleChange(CefBrowser browser, String title) {} @Override public void OnFullscreenModeChange(CefBrowser browser, boolean fullscreen) {} @Override public boolean onTooltip(CefBrowser browser, String text) { return false; } @Override public void onStatusMessage(CefBrowser browser, String value) {} @Override public boolean onConsoleMessage( CefBrowser browser, CefSettings.LogSeverity level, String message, String source, int line ) { // 格式化输出到 Java 控制台 //if (level != CefSettings.LogSeverity.LOGSEVERITY_WARNING) { String log = String.format( "[Browser Console] %s %s (Line %d) -> %s", getLogLevelSymbol(level), source, line, message ); System.out.println(log); //} return false; } @Override public boolean onCursorChange(CefBrowser browser, int cursorType) { return false; } private String getLogLevelSymbol(CefSettings.LogSeverity level) { switch (level) { case LOGSEVERITY_ERROR: return "⛔"; case LOGSEVERITY_WARNING: return "⚠️"; case LOGSEVERITY_DEFAULT: return "🐞"; default: return "ℹ️"; } } }); if (AxisInnovatorsBox.getMain() != null && AxisInnovatorsBox.getMain().isDebugEnvironment()) { client.addKeyboardHandler(new CefKeyboardHandlerAdapter() { @Override public boolean onKeyEvent(CefBrowser browser, CefKeyEvent event) { // 检测 F12 if (event.windows_key_code == 123) { browser.getDevTools().createImmediately(); return true; } return false; } }); } client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() { @Override public boolean onBeforePopup(CefBrowser browser, CefFrame frame, String targetUrl, String targetFrameName) { // 处理弹出窗口:根据配置决定打开方式 if (builder.openLinksInExternalBrowser) { // 使用默认浏览器打开 try { Desktop.getDesktop().browse(new URI(targetUrl)); } catch (Exception e) { System.out.println("Failed to open external browser: " + e.getMessage()); } return true; // 拦截弹窗 } else { // 在当前浏览器中打开 browser.loadURL(targetUrl); return true; // 拦截弹窗并在当前窗口打开 } } }); client.addRequestHandler(new CefRequestHandlerAdapter() { @Override public boolean onBeforeBrowse(CefBrowser browser, CefFrame frame, CefRequest request, boolean userGesture, boolean isRedirect) { // 处理主窗口导航 if (userGesture) { if (builder.openLinksInExternalBrowser) { // 使用默认浏览器打开 try { Desktop.getDesktop().browse(new URI(request.getURL())); return true; // 取消内置浏览器导航 } catch (Exception e) { System.out.println("Failed to open external browser: " + e.getMessage()); } } else { // 允许在当前浏览器中打开 return false; } } return false; } }); client.addContextMenuHandler(new CefContextMenuHandlerAdapter() { @Override public void onBeforeContextMenu(CefBrowser browser, CefFrame frame, CefContextMenuParams params, CefMenuModel model) { model.clear(); if (!params.getSelectionText().isEmpty() || params.isEditable()) { model.addItem(MENU_ID_USER_FIRST, "复制"); } if (params.isEditable()) { model.addItem(MENU_ID_USER_FIRST + 1, "粘贴"); model.addItem(MENU_ID_USER_FIRST + 2, "粘贴纯文本"); } } @Override public boolean onContextMenuCommand(CefBrowser browser, CefFrame frame, CefContextMenuParams params, int commandId, int eventFlags) { if (commandId == MENU_ID_USER_FIRST) { if (params.isEditable()) { browser.executeJavaScript("document.execCommand('copy');", browser.getURL(), 0); } else { browser.executeJavaScript( "window.getSelection().toString();", browser.getURL(), 0 ); } return true; } else if (commandId == MENU_ID_USER_FIRST + 1) { pasteContent(browser, false); return true; } else if (commandId == MENU_ID_USER_FIRST + 2) { pasteContent(browser, true); return true; } return false; } /** * 处理粘贴操作 * @param plainText 是否去除格式(纯文本模式) */ private void pasteContent(CefBrowser browser, boolean plainText) { try { Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) { String text = (String) clipboard.getData(DataFlavor.stringFlavor); if (plainText) { text = text.replaceAll("<[^>]+>", ""); } String escapedText = text .replace("\\", "\\\\") .replace("'", "\\'") .replace("\n", "\\n") .replace("\r", "\\r"); String script = String.format( "if (document.activeElement) {\n" + " document.activeElement.value += '%s';\n" + // 简单追加文本 " document.dispatchEvent(new Event('input', { bubbles: true }));\n" + // 触发输入事件 "}", escapedText ); browser.executeJavaScript(script, browser.getURL(), 0); } } catch (UnsupportedFlavorException | IOException e) { e.printStackTrace(); } } }); // 添加 alert 弹窗监控处理 client.addJSDialogHandler(new CefJSDialogHandlerAdapter() { @Override public boolean onJSDialog(CefBrowser browser, String origin_url, JSDialogType dialog_type, String message_text, String default_prompt_text, CefJSDialogCallback callback, BoolRef suppress_message) { if (dialog_type == JSDialogType.JSDIALOGTYPE_ALERT) { SwingUtilities.invokeLater(() -> { JOptionPane.showMessageDialog( BrowserWindowJDialog.this, message_text, "警告", JOptionPane.INFORMATION_MESSAGE ); }); callback.Continue(true, ""); return true; } return false; } }); // 3. 拦截所有新窗口(关键修复点!) //client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() { // @Override // public boolean onBeforePopup(CefBrowser browser, // CefFrame frame, String target_url, String target_frame_name) { // return true; // 返回true表示拦截弹窗 // } //}); Thread.currentThread().setName("BrowserRenderThread"); // 4. 加载HTML if (htmlUrl.isEmpty()) { String fileUrl = new File(htmlPath).toURI().toURL().toString(); System.out.println("Loading HTML from: " + fileUrl); // 5. 创建浏览器组件(直接添加到内容面板) browser = client.createBrowser(fileUrl, false, false); } else { System.out.println("Loading HTML from: " + htmlUrl); browser = client.createBrowser(htmlUrl, false, false); } Component browserComponent = browser.getUIComponent(); if (builder.browserCreationCallback != null) { boolean handled = builder.browserCreationCallback.onLayoutCustomization( this, // 当前窗口 getContentPane(), // 内容面板 browserComponent, // 浏览器组件 builder // 构建器对象 ); // 如果回调返回true,跳过默认布局 if (handled) { // 设置窗口基本属性 setTitle(builder.title); setSize(builder.size); setLocationRelativeTo(builder.parentFrame); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); // 添加资源释放监听器 addWindowListener(new WindowAdapter() { @Override public void windowClosed(WindowEvent e) { browser.close(true); client.dispose(); } }); setVisible(true); return browserComponent; // 直接返回,跳过默认布局 } } updateTheme(); CefMessageRouter.CefMessageRouterConfig config = new CefMessageRouter.CefMessageRouterConfig(); config.jsQueryFunction = "javaQuery";// 定义方法 config.jsCancelFunction = "javaQueryCancel";// 定义取消方法 // 6. 配置窗口布局(确保只添加一次) SwingUtilities.invokeLater(() -> { getContentPane().removeAll(); getContentPane().setLayout(new BorderLayout()); // 透明拖拽层(仅顶部可拖拽) JPanel dragPanel = new JPanel(new BorderLayout()); dragPanel.setOpaque(false); JPanel titleBar = new JPanel(); titleBar.setOpaque(false); titleBar.setPreferredSize(new Dimension(builder.size.width, 20)); final Point[] dragStart = new Point[1]; titleBar.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { dragStart[0] = e.getPoint(); } @Override public void mouseReleased(MouseEvent e) { dragStart[0] = null; } }); titleBar.addMouseMotionListener(new MouseMotionAdapter() { @Override public void mouseDragged(MouseEvent e) { if (dragStart[0] != null) { Point curr = e.getLocationOnScreen(); setLocation(curr.x - dragStart[0].x, curr.y - dragStart[0].y); } } }); dragPanel.add(titleBar, BorderLayout.NORTH); getContentPane().add(dragPanel, BorderLayout.CENTER); getContentPane().add(browserComponent, BorderLayout.CENTER); // 7. 窗口属性设置 setTitle(builder.title); setSize(builder.size); setLocationRelativeTo(builder.parentFrame); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); // 8. 资源释放 addWindowListener(new WindowAdapter() { @Override public void windowClosed(WindowEvent e) { browser.close(true); client.dispose(); } }); setVisible(true); }); return browserComponent; } catch (Exception e) { e.printStackTrace(); JOptionPane.showMessageDialog(null, "初始化失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } } else { isInitialized = false; SwingUtilities.invokeLater(() -> { dispose(); }); } return null; } /** * 更新主题 */ public void updateTheme() { // 1. 获取Java字体信息 String fontInfo = getSystemFontsInfo(); injectFontInfoToPage(browser, fontInfo); // 2. 注入主题信息 if (AxisInnovatorsBox.getMain() != null) { boolean isDarkTheme = AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode(); injectThemeInfoToPage(browser, isDarkTheme); } //// 3. 刷新浏览器 //SwingUtilities.invokeLater(() -> { // browser.reload(); //}); } private void injectThemeInfoToPage(CefBrowser browser, boolean isDarkTheme) { if (client == null) { return; } client.addLoadHandler(new CefLoadHandlerAdapter() { @Override public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) { String themeInfo = String.format( "{\"isDarkTheme\": %s, \"timestamp\": %d}", isDarkTheme, System.currentTimeMillis() ); String script = "window.javaThemeInfo = " + themeInfo + ";\n" + "console.log('Java theme information has been loaded:', window.javaThemeInfo);\n" + "\n" + "if (typeof applyJavaTheme === 'function') {\n" + " applyJavaTheme(window.javaThemeInfo);\n" + "}\n" + "\n" + "var event = new CustomEvent('javaThemeChanged', {\n" + " detail: window.javaThemeInfo\n" + "});\n" + "document.dispatchEvent(event);\n" + "console.log('The javaThemeChanged event is dispatched');"; browser.executeJavaScript(script, browser.getURL(), 0); browser.executeJavaScript( "console.log('Theme information injection is complete,window.javaThemeInfo:', typeof window.javaThemeInfo);" + "console.log('Number of theme event listeners:', document.eventListeners ? document.eventListeners('javaThemeChanged') : '无法获取');", browser.getURL(), 0 ); } }); } /** * 获取Java字体信息(从UIManager获取) */ private String getSystemFontsInfo() { try { Gson gson = new Gson(); JsonObject fontInfo = new JsonObject(); JsonObject uiFonts = new JsonObject(); String[] fontKeys = { "Label.font", "Button.font", "ToggleButton.font", "RadioButton.font", "CheckBox.font", "ColorChooser.font", "ComboBox.font", "EditorPane.font", "TextArea.font", "TextField.font", "PasswordField.font", "TextPane.font", "FormattedTextField.font", "Table.font", "TableHeader.font", "List.font", "Tree.font", "TabbedPane.font", "MenuBar.font", "Menu.font", "MenuItem.font", "PopupMenu.font", "CheckBoxMenuItem.font", "RadioButtonMenuItem.font", "Spinner.font", "ToolBar.font", "TitledBorder.font", "OptionPane.messageFont", "OptionPane.buttonFont", "Panel.font", "Viewport.font", "ToolTip.font" }; for (String key : fontKeys) { Font font = UIManager.getFont(key); if (font != null) { JsonObject fontObj = new JsonObject(); fontObj.addProperty("name", font.getFontName()); fontObj.addProperty("family", font.getFamily()); fontObj.addProperty("size", font.getSize()); fontObj.addProperty("style", font.getStyle()); fontObj.addProperty("bold", font.isBold()); fontObj.addProperty("italic", font.isItalic()); fontObj.addProperty("plain", font.isPlain()); uiFonts.add(key, fontObj); } } fontInfo.add("uiFonts", uiFonts); fontInfo.addProperty("timestamp", System.currentTimeMillis()); fontInfo.addProperty("lookAndFeel", UIManager.getLookAndFeel().getName()); return gson.toJson(fontInfo); } catch (Exception e) { return "{\"error\": \"无法获取UIManager字体信息: " + e.getMessage() + "\"}"; } } /** * 注入字体信息到页面并设置字体 */ private void injectFontInfoToPage(CefBrowser browser, String fontInfo) { if (client == null) { return; } client.addLoadHandler(new CefLoadHandlerAdapter() { @Override public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) { // 使用更简单的脚本来注入字体信息 String script = "if (typeof window.javaFontInfo === 'undefined') {" + " window.javaFontInfo = " + fontInfo + ";" + " console.log('Java font information has been loaded:', window.javaFontInfo);" + " " + " var event = new CustomEvent('javaFontsLoaded', {" + " detail: window.javaFontInfo" + " });" + " document.dispatchEvent(event);" + " console.log('The javaFontsLoaded event is dispatched');" + "}"; System.out.println("正在注入字体信息到页面..."); browser.executeJavaScript(script, browser.getURL(), 0); // 添加调试信息 browser.executeJavaScript( "console.log('Font information injection is complete,window.javaFontInfo:', typeof window.javaFontInfo);" + "console.log('Number of event listeners:', document.eventListeners ? document.eventListeners('javaFontsLoaded') : '无法获取');", browser.getURL(), 0 ); } }); } public static void printStackTrace() { StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); for (int i = 2; i < stackTrace.length; i++) { StackTraceElement element = stackTrace[i]; System.out.println(element.getClassName() + "." + element.getMethodName() + "(" + (element.getFileName() != null ? element.getFileName() : "Unknown Source") + ":" + element.getLineNumber() + ")"); } } @Override public void setVisible(boolean b) { if (b) { if (browser != null) { updateTheme(); } } super.setVisible(b); } public Component getBrowserComponent() { return browserComponent; } private void setupMessageHandlers(WindowOperationHandler handler) { if (client != null) { msgRouter = CefMessageRouter.create(); msgRouter.addHandler(new CefMessageRouterHandlerAdapter() { @Override public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, String request, boolean persistent, CefQueryCallback callback) { if (request.startsWith("system:")) { String[] parts = request.split(":"); String operation = parts.length >= 2 ? parts[1] : null; String targetWindow = parts.length > 2 ? parts[2] : null; handler.handleOperation( new WindowOperation(operation, targetWindow, callback) // [!code ++] ); return true; } if (request.startsWith("java-response:")) { String[] parts = request.split(":"); String requestId = parts[1]; String responseData = parts.length > 2 ? parts[2] : ""; Consumer handler = WindowRegistry.getInstance().getCallback(requestId); if (handler != null) { handler.accept(responseData); callback.success(""); } else { callback.failure(-1, "无效的请求ID"); } return true; } return false; } }, true); client.addMessageRouter(msgRouter); } } public String getWindowId() { return windowId; } /** * 获取消息路由器 * @return 消息路由器 */ public CefMessageRouter getMsgRouter() { return msgRouter; } /** * 获取浏览器对象 * @return 浏览器对象 */ public CefBrowser getBrowser() { return browser; } public void closeWindow() { SwingUtilities.invokeLater(() -> { if (browser != null) { browser.close(true); } dispose(); cefApp.dispose(); WindowRegistry.getInstance().unregisterWindow(windowId); }); } }