docs: 添加 Axis Innovators Box 框架完整 API 文档

- 新增浏览器模块技术文档,涵盖 BrowserCore、BrowserWindow 等核心组件
- 添加事件系统文档,包括 EventBus、GlobalEventBus 及各类事件定义
- 创建 LanguageManager 国际化管理器详细说明文档
- 新增 Log4j2OutputStream 标准输出重定向类文档
- 添加 Main 入口类启动流程与路由机制说明
- 创建 BrowserCreationCallback 回调接口使用指南
- 完善 AxisInnovatorsBox 主类架构与崩溃诊断系统文档
This commit is contained in:
2026-01-03 08:46:19 +08:00
parent 06f36fc3f0
commit 7badbb0d8e
29 changed files with 3011 additions and 2275 deletions

View File

@@ -0,0 +1,41 @@
package com.axis.innovators.box.browser;
import com.axis.innovators.box.browser.bridge.JsBridgeController;
import com.axis.innovators.box.events.BrowserCreationCallback;
import javax.swing.*;
import java.awt.*;
@SuppressWarnings("unchecked")
public abstract class BaseBrowserBuilder<T extends BaseBrowserBuilder<T>> {
protected String windowId;
protected String title = "JCEF Window";
protected Dimension size = new Dimension(800, 600);
protected WindowOperationHandler operationHandler;
protected String htmlPath;
protected String htmlUrl = "";
protected Image icon;
protected boolean resizable = true;
protected boolean maximizable = true;
protected boolean minimizable = true;
protected boolean openLinksInExternalBrowser = true;
protected BrowserCreationCallback browserCreationCallback;
protected JsBridgeController controller;
public BaseBrowserBuilder(String windowId) {
this.windowId = windowId;
}
public T title(String title) { this.title = title; return (T) this; }
public T size(int width, int height) { this.size = new Dimension(width, height); return (T) this; }
public T operationHandler(WindowOperationHandler handler) { this.operationHandler = handler; return (T) this; }
public T htmlPath(String path) { this.htmlPath = path; return (T) this; }
public T htmlUrl(String url) { this.htmlUrl = url; return (T) this; }
public T icon(Image icon) { this.icon = icon; return (T) this; }
public T resizable(boolean resizable) { this.resizable = resizable; return (T) this; }
public T controller(JsBridgeController controller) {this.controller = controller;return (T) this;}
public T openLinksInBrowser(boolean openInBrowser) { this.openLinksInExternalBrowser = !openInBrowser; return (T) this; }
public T setBrowserCreationCallback(BrowserCreationCallback callback) { this.browserCreationCallback = callback; return (T) this; }
// 抽象构建方法
public abstract Window build();
}

View File

@@ -0,0 +1,57 @@
package com.axis.innovators.box.browser;
import com.axis.innovators.box.browser.bridge.JsBridgeController;
import org.cef.browser.CefBrowser;
import org.cef.browser.CefMessageRouter;
/**
* 浏览器容器接口
* 统一 JFrame 和 JDialog 的对外 API
*/
public interface BrowserContainer {
/**
* 获取窗口唯一ID
*/
String getWindowId();
/**
* 获取 CEF 消息路由 (兼容旧 API)
*/
CefMessageRouter getMsgRouter();
/**
* 获取 CEF 浏览器实例
*/
CefBrowser getBrowser();
/**
* 在当前浏览器上下文中执行 JavaScript 代码
* 支持从任意线程调用,内部会自动处理线程调度
*
* @param script 要执行的 JavaScript 代码字符串
*/
void executingJsCode(String script);
/**
* 关闭窗口
*/
void closeWindow();
/**
* 更新主题
*/
void updateTheme();
/**
* 设置窗口可见性
* (JFrame 和 JDialog 原生支持此方法,接口中声明即可)
* @param b true 显示false 隐藏
*/
void setVisible(boolean b);
/**
* 用于动态设置Java与JavaScript之间的通信桥梁
* @param controller JsBridgeController 实例
*/
void setController(JsBridgeController controller);
}

View File

@@ -0,0 +1,621 @@
package com.axis.innovators.box.browser;
import com.axis.innovators.box.AxisInnovatorsBox;
import com.axis.innovators.box.browser.bridge.JsBridgeController;
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.*;
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.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.util.Vector;
import java.util.function.Consumer;
import static org.cef.callback.CefMenuModel.MenuId.MENU_ID_USER_FIRST;
/**
* 浏览器核心逻辑代理类
* 负责封装 CefClient, CefBrowser 以及所有 Handler 的通用逻辑
*/
public class BrowserCore {
private final Window parentWindow; // 宿主窗口JFrame 或 JDialog
private final String windowId;
private CefApp cefApp;
private CefClient client;
private CefBrowser browser;
private CefMessageRouter msgRouter;
private final BaseBrowserBuilder<?> config;
private final java.util.Queue<String> pendingScripts = new java.util.concurrent.ConcurrentLinkedQueue<>();
private volatile boolean isPageLoaded = false;
private JsBridgeController jsController;
public BrowserCore(Window parentWindow, String windowId, BaseBrowserBuilder<?> config) {
this.parentWindow = parentWindow;
this.windowId = windowId;
this.config = config;
}
public Component initialize() throws MalformedURLException {
this.cefApp = CefAppManager.getInstance();
this.client = cefApp.createClient();
// 1. 设置通用 Handler
setupDisplayHandler();
setupKeyboardHandler();
setupRequestHandler();
setupContextMenuHandler();
setupDialogHandler();
setupDownloadHandler();
setupLifeSpanHandler();
setupFileDialogHandler();
setupScriptQueueHandler();
// 2. 消息路由
//if (config.operationHandler != null || this.jsController != null) {
setupMessageHandlers(config.operationHandler);
//}
// 3. 创建浏览器
String targetUrl;
if (config.htmlUrl != null && !config.htmlUrl.isEmpty()) {
targetUrl = config.htmlUrl;
System.out.println("Loading URL: " + targetUrl);
} else if (config.htmlPath != null && !config.htmlPath.isEmpty()) {
targetUrl = new File(config.htmlPath).toURI().toURL().toString();
System.out.println("Loading File: " + targetUrl);
} else {
throw new IllegalArgumentException("URL or HTML path must be provided");
}
this.browser = client.createBrowser(targetUrl, false, false);
Component browserUI = browser.getUIComponent();
// 4. 加载结束后的主题注入
updateTheme();
return browserUI;
}
/**
* 设置或更新 JSBridge 控制器
* 支持运行时动态切换
*/
public void setJsBridgeController(JsBridgeController controller) {
this.jsController = controller;
if (controller != null) {
controller.attach(this);
if (browser != null && isPageLoaded) {
System.out.println("🔄 [BrowserCore] 动态更新 JSBridge 控制器,正在重新注入 JS...");
String bridgeScript = controller.generateInjectionJs();
browser.executeJavaScript(bridgeScript, browser.getURL(), 0);
}
}
}
/**
* 核心逻辑:设置加载监听器,处理脚本队列
*/
private void setupScriptQueueHandler() {
client.addLoadHandler(new CefLoadHandlerAdapter() {
@Override
public void onLoadingStateChange(CefBrowser browser, boolean isLoading, boolean canGoBack, boolean canGoForward) {
// 可选:在这里也可以监控加载状态
}
@Override
public void onLoadStart(CefBrowser browser, CefFrame frame, CefRequest.TransitionType transitionType) {
if (frame.isMain()) {
// 开始加载新页面时,重置状态
isPageLoaded = false;
}
}
@Override
public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
// 只有主框架加载完毕,且没有发生严重错误,才执行脚本
// 只有主框架加载完毕才注入
if (frame.isMain()) {
System.out.println("✅ [BrowserCore] 页面加载完成 (Code: " + httpStatusCode + ")");
isPageLoaded = true;
if (jsController != null) {
System.out.println("💉 [BrowserCore] 正在注入 JSBridge 代码...");
String bridgeScript = jsController.generateInjectionJs();
frame.executeJavaScript(bridgeScript, frame.getURL(), 0);
} else {
System.err.println("⚠️ [BrowserCore] jsController 为空,未注入 JS 对象");
}
while (!pendingScripts.isEmpty()) {
String script = pendingScripts.poll();
if (script != null) {
try {
frame.executeJavaScript(script, frame.getURL(), 0);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
@Override
public void onLoadError(CefBrowser browser, CefFrame frame, ErrorCode errorCode, String errorText, String failedUrl) {
if (frame.isMain()) {
System.err.println("❌ [BrowserCore] 页面加载失败: " + errorText);
// 加载失败通常不应该执行脚本,或者你可以选择清空队列
// pendingScripts.clear();
}
}
});
}
// --- Handlers ---
private void setupDisplayHandler() {
client.addDisplayHandler(new CefDisplayHandlerAdapter() {
@Override
public boolean onConsoleMessage(CefBrowser browser, CefSettings.LogSeverity level, String message, String source, int line) {
String symbol = switch (level) {
case LOGSEVERITY_ERROR -> "";
case LOGSEVERITY_WARNING -> "⚠️";
case LOGSEVERITY_DEFAULT -> "🐞";
default -> "";
};
System.out.printf("[Browser Console] %s %s (Line %d) -> %s%n", symbol, source, line, message);
return false;
}
@Override
public void onTitleChange(CefBrowser browser, String title) {
SwingUtilities.invokeLater(() -> {
if (parentWindow instanceof JFrame) {
((JFrame) parentWindow).setTitle(title);
} else if (parentWindow instanceof JDialog) {
((JDialog) parentWindow).setTitle(title);
}
});
}
});
}
private void setupKeyboardHandler() {
if (AxisInnovatorsBox.getMain() != null && AxisInnovatorsBox.getMain().isDebugEnvironment()) {
client.addKeyboardHandler(new CefKeyboardHandlerAdapter() {
@Override
public boolean onKeyEvent(CefBrowser browser, CefKeyEvent event) {
if (event.windows_key_code == 123) { // F12
browser.getDevTools().createImmediately();
return true;
}
return false;
}
});
}
}
private void setupRequestHandler() {
client.addRequestHandler(new CefRequestHandlerAdapter() {
@Override
public boolean onBeforeBrowse(CefBrowser browser, CefFrame frame, CefRequest request, boolean user_gesture, boolean is_redirect) {
String url = request.getURL();
if (url != null && url.toLowerCase().startsWith("data:")) return false;
// 处理外部浏览器打开
if (user_gesture && config.openLinksInExternalBrowser) {
openInExternalBrowser(url);
return true;
}
return false;
}
/**
* 严格匹配你提供的源码签名
*/
@Override
public boolean onCertificateError(CefBrowser browser,
CefLoadHandler.ErrorCode cert_error,
String request_url,
CefCallback callback) {
SwingUtilities.invokeLater(() -> {
int option = JOptionPane.showConfirmDialog(parentWindow,
"证书错误: " + cert_error + "\n是否继续访问",
"安全警告",
JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE);
if (option == JOptionPane.YES_OPTION) {
// JCEF 122 中 CefCallback.Continue() 无参数,表示"继续/忽略错误"
callback.Continue();
} else {
callback.cancel();
}
});
return true; // 表示我们处理了这个事件
}
/**
* 严格匹配你提供的源码签名
*/
@Override
public boolean getAuthCredentials(CefBrowser browser,
String origin_url,
boolean isProxy,
String host,
int port,
String realm,
String scheme,
CefAuthCallback callback) {
SwingUtilities.invokeLater(() -> {
JTextField username = new JTextField();
JPasswordField password = new JPasswordField();
Object[] message = {
"Host: " + host,
"Realm: " + realm,
"Username:", username,
"Password:", password
};
int option = JOptionPane.showConfirmDialog(parentWindow, message, "登录认证", JOptionPane.OK_CANCEL_OPTION);
if (option == JOptionPane.OK_OPTION) {
callback.Continue(username.getText(), new String(password.getPassword()));
} else {
callback.cancel();
}
});
return true;
}
});
}
private void setupFileDialogHandler() {
client.addDialogHandler((browser, mode, title, defaultFilePath, acceptFilters, callback) -> {
SwingUtilities.invokeLater(() -> {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setDialogTitle(title != null ? title : "选择文件");
// 设置默认路径
if (defaultFilePath != null && !defaultFilePath.isEmpty()) {
fileChooser.setCurrentDirectory(new File(defaultFilePath));
}
// 处理模式:是否多选
boolean isMulti = (mode == CefDialogHandler.FileDialogMode.FILE_DIALOG_OPEN_MULTIPLE);
fileChooser.setMultiSelectionEnabled(isMulti);
// 打开对话框
int result = fileChooser.showOpenDialog(parentWindow);
if (result == JFileChooser.APPROVE_OPTION) {
Vector<String> filePaths = new Vector<>();
if (isMulti) {
for (File file : fileChooser.getSelectedFiles()) {
filePaths.add(file.getAbsolutePath());
}
} else {
filePaths.add(fileChooser.getSelectedFile().getAbsolutePath());
}
callback.Continue(filePaths);
} else {
callback.Cancel();
}
});
return true;
});
}
private void setupContextMenuHandler() {
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) {
browser.executeJavaScript("document.execCommand('copy');", 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;
}
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");
browser.executeJavaScript("if (document.activeElement) { document.activeElement.value += '" + escapedText + "'; document.dispatchEvent(new Event('input', { bubbles: true })); }", browser.getURL(), 0);
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
private void setupDialogHandler() {
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) {
SwingUtilities.invokeLater(() -> {
if (dialog_type == JSDialogType.JSDIALOGTYPE_ALERT) {
JOptionPane.showMessageDialog(parentWindow, message_text, "警告", JOptionPane.INFORMATION_MESSAGE);
callback.Continue(true, "");
} else if (dialog_type == JSDialogType.JSDIALOGTYPE_CONFIRM) {
int result = JOptionPane.showConfirmDialog(parentWindow, message_text, "确认", JOptionPane.YES_NO_OPTION);
callback.Continue(result == JOptionPane.YES_OPTION, "");
} else if (dialog_type == JSDialogType.JSDIALOGTYPE_PROMPT) {
String result = JOptionPane.showInputDialog(parentWindow, message_text, default_prompt_text);
if (result != null) callback.Continue(true, result);
else callback.Continue(false, "");
}
});
return true;
}
});
}
private void setupDownloadHandler() {
client.addDownloadHandler(new CefDownloadHandlerAdapter() {
@Override
public void onBeforeDownload(CefBrowser browser, CefDownloadItem downloadItem, String suggestedName, CefBeforeDownloadCallback callback) {
callback.Continue(suggestedName, true);
}
@Override
public void onDownloadUpdated(CefBrowser browser,
CefDownloadItem downloadItem,
CefDownloadItemCallback callback) {
if (downloadItem.isComplete()) {
System.out.println("下载完成: " + downloadItem.getFullPath());
} else if (downloadItem.isCanceled()) {
System.out.println("下载取消");
} else {
// 获取进度百分比
int percent = downloadItem.getPercentComplete();
if (percent % 10 == 0) { // 减少日志输出频率
System.out.println("下载中: " + percent + "%");
}
}
}
});
}
private void setupLifeSpanHandler() {
client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
@Override
public boolean onBeforePopup(CefBrowser browser, CefFrame frame, String targetUrl, String targetFrameName) {
boolean isDataProtocol = targetUrl != null && targetUrl.toLowerCase().startsWith("data:");
// 策略1使用外部浏览器打开
if (config.openLinksInExternalBrowser && !isDataProtocol) {
openInExternalBrowser(targetUrl);
return true;
}
// 策略2创建新的 Swing 窗口 (WindowRegistry)
SwingUtilities.invokeLater(() -> {
String popupWindowId = windowId + "_popup_" + System.currentTimeMillis();
WindowRegistry.getInstance().createNewWindow(popupWindowId, popupBuilder -> {
popupBuilder.title("Popup") // 可以优化为获取页面标题
.size(parentWindow.getWidth(), parentWindow.getHeight())
.htmlUrl(targetUrl)
.icon(config.icon)
.openLinksInBrowser(true);
if (config.operationHandler != null) {
popupBuilder.operationHandler(config.operationHandler);
}
});
});
return true;
}
});
}
private void setupMessageHandlers(WindowOperationHandler handler) {
// 1. 配置 (保持上一轮修复的代码)
CefMessageRouter.CefMessageRouterConfig routerConfig = new CefMessageRouter.CefMessageRouterConfig();
routerConfig.jsQueryFunction = "javaQuery";
routerConfig.jsCancelFunction = "javaQueryCancel";
// 2. 创建 (保持上一轮修复的代码)
msgRouter = CefMessageRouter.create(routerConfig);
// 3. 添加处理器
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
@Override
public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, String request, boolean persistent, CefQueryCallback callback) {
// A. 处理 JS Bridge 请求 (Controller)
if (jsController != null && request.startsWith("jsbridge:")) {
return jsController.handleQuery(browser, frame, queryId, request, persistent, callback);
}
// B. 处理 System 请求 (OperationHandler)
if (request.startsWith("system:")) {
// ★★★ [增加判空] 防止 handler 为空导致空指针异常 ★★★
if (handler != null) {
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));
return true;
} else {
// 如果没有 handler但这又是一个 system 请求,则报错或忽略
System.err.println("收到 system 请求但 handler 为空: " + request);
callback.failure(404, "No handler for system operation");
return true;
}
}
// C. 处理 Java Response
if (request.startsWith("java-response:")) {
String[] parts = request.split(":");
String requestId = parts[1];
String responseData = parts.length > 2 ? parts[2] : "";
Consumer<String> cb = WindowRegistry.getInstance().getCallback(requestId);
if (cb != null) {
cb.accept(responseData);
callback.success("");
} else {
callback.failure(-1, "无效的请求ID");
}
return true;
}
return false;
}
}, true);
client.addMessageRouter(msgRouter);
}
private void injectJsBridge() {
if (jsController != null && browser != null) {
String script = jsController.generateInjectionJs();
System.out.println("💉 [Injecting JS Bridge]");
browser.executeJavaScript(script, browser.getURL(), 0);
}
}
private void openInExternalBrowser(String url) {
try {
Desktop.getDesktop().browse(new URI(url));
} catch (Exception e) {
System.err.println("外部浏览器打开失败: " + e.getMessage());
}
}
// --- Theme & Font Injection ---
public void updateTheme() {
if (browser == null) return;
// 字体信息
String fontInfo = getSystemFontsInfo();
// 主题信息
boolean isDarkTheme = false;
if (AxisInnovatorsBox.getMain() != null) {
isDarkTheme = AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode();
}
injectResources(fontInfo, isDarkTheme);
}
private void injectResources(String fontInfo, boolean isDarkTheme) {
if (client == null) return;
client.addLoadHandler(new CefLoadHandlerAdapter() {
@Override
public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
// 注入字体
String fontScript = String.format(
"if(typeof window.javaFontInfo === 'undefined'){ window.javaFontInfo=%s; document.dispatchEvent(new CustomEvent('javaFontsLoaded',{detail:window.javaFontInfo})); }",
fontInfo
);
browser.executeJavaScript(fontScript, browser.getURL(), 0);
if (jsController != null) {
String bridgeScript = jsController.generateInjectionJs();
frame.executeJavaScript(bridgeScript, frame.getURL(), 0);
}
// 注入主题
String themeInfo = String.format("{\"isDarkTheme\": %s, \"timestamp\": %d}", isDarkTheme, System.currentTimeMillis());
String themeScript = String.format(
"window.javaThemeInfo=%s; document.dispatchEvent(new CustomEvent('javaThemeChanged',{detail:window.javaThemeInfo}));",
themeInfo
);
browser.executeJavaScript(themeScript, browser.getURL(), 0);
}
});
}
private String getSystemFontsInfo() {
try {
JsonObject fontInfo = new JsonObject();
JsonObject uiFonts = new JsonObject();
String[] fontKeys = { "Label.font", "Button.font", "TextField.font", "Panel.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("size", font.getSize());
uiFonts.add(key, fontObj);
}
}
fontInfo.add("uiFonts", uiFonts);
return new Gson().toJson(fontInfo);
} catch (Exception e) {
return "{}";
}
}
public CefMessageRouter getMsgRouter() {
return msgRouter;
}
public void executingJsCode(String script) {
// 无论哪个线程调用,都放入队列检查逻辑
SwingUtilities.invokeLater(() -> {
if (browser == null) return;
if (isPageLoaded) {
// 场景A页面已经加载好了直接执行
try {
browser.getMainFrame().executeJavaScript(script, browser.getMainFrame().getURL(), 0);
System.out.println("⚡ [直接执行] " + script);
} catch (Exception e) {
System.err.println("执行JS异常: " + e.getMessage());
}
} else {
// 场景B页面还没好加入队列等待 onLoadEnd 自动处理
System.out.println("⏳ [加入队列] 页面未就绪: " + (script.length() > 20 ? script.substring(0, 20) + "..." : script));
pendingScripts.add(script);
}
});
}
public void dispose() {
if (browser != null) browser.close(true);
if (client != null) client.dispose();
// CefApp don't dispose here, it's global
}
public CefBrowser getBrowser() { return browser; }
public CefClient getClient() { return client; }
}

View File

@@ -1,880 +1,168 @@
package com.axis.innovators.box.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 com.axis.innovators.box.browser.bridge.JsBridgeController;
import org.cef.browser.CefBrowser;
import org.cef.browser.CefFrame;
import org.cef.browser.CefMessageRouter;
import org.cef.callback.*;
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 BrowserWindow extends JFrame {
public class BrowserWindow extends JFrame implements BrowserContainer {
private final BrowserCore browserCore;
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 static boolean isInitialized = false;
private WindowOperationHandler operationHandler;
private static Thread cefThread;
private CefMessageRouter msgRouter;
public static class Builder {
private BrowserCreationCallback browserCreationCallback;
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 boolean resizable = true; // 默认允许调整大小
private boolean maximizable = true; // 默认允许最大化
private boolean minimizable = true; // 默认允许最小化
private String htmlUrl = "";
private boolean openLinksInExternalBrowser = true; // 默认使用外部浏览器
public static class Builder extends BaseBrowserBuilder<Builder> {
public Builder(String windowId) { super(windowId); }
public Builder resizable(boolean resizable) {
this.resizable = resizable;
return this;
}
public Builder maximizable(boolean maximizable) {
this.maximizable = maximizable;
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;
}
public Builder minimizable(boolean minimizable) {
this.minimizable = minimizable;
return this;
}
public Builder(String windowId) {
this.windowId = windowId;
}
/**
* 设置浏览器创建回调
* @param callback 回调
*/
public Builder setBrowserCreationCallback(BrowserCreationCallback callback){
this.browserCreationCallback = callback;
return this;
}
/**
* 设置浏览器窗口标题
* @param title 标题
*/
public Builder title(String title) {
this.title = title;
return this;
}
/**
* 设置浏览器窗口大小
* @param width 宽度
* @param height 高度
*/
public Builder size(int width, int height) {
this.size = new Dimension(width, height);
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路径
*/
@Override
public BrowserWindow 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());
}
}
validatePaths();
return new BrowserWindow(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 void validatePaths() {
if ((htmlUrl == null || htmlUrl.isEmpty()) && (htmlPath == null || htmlPath.isEmpty())) {
throw new IllegalArgumentException("HTML path or URL cannot be empty");
}
}
}
private BrowserWindow(Builder builder) {
this.windowId = builder.windowId;
this.htmlPath = builder.htmlPath;
this.operationHandler = builder.operationHandler;
this.htmlUrl = builder.htmlUrl;
if (builder.icon != null) setIconImage(builder.icon);
// 设置图标(如果存在)
if (builder.icon != null) {
setIconImage(builder.icon);
// 初始化核心代理
this.browserCore = new BrowserCore(this, windowId, builder);
if (builder.controller != null) {
browserCore.setJsBridgeController(builder.controller);
}
// 初始化浏览器组件
try {
this.browserComponent = initializeCef(builder);
if (operationHandler != null) {
setupMessageHandlers(operationHandler);
Component browserComponent = browserCore.initialize();
// 布局自定义回调
if (builder.browserCreationCallback != null) {
boolean handled = builder.browserCreationCallback.onLayoutCustomization(
this, getContentPane(), browserComponent, builder
);
if (handled) {
configureWindow(builder);
return;
}
}
// 默认布局
initDefaultLayout(builder, browserComponent);
configureWindow(builder);
} catch (Exception e) {
JOptionPane.showMessageDialog(this, "初始化失败: " + e.getMessage());
throw new RuntimeException(e);
}
}
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) {
private void initDefaultLayout(Builder builder, Component browserComponent) {
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));
@Override
public void onTitleChange(CefBrowser browser, String title) {
WindowDragListener dragListener = new WindowDragListener(this);
titleBar.addMouseListener(dragListener);
titleBar.addMouseMotionListener(dragListener);
}
@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.addRequestHandler(new CefRequestHandlerAdapter() {
@Override
public boolean onBeforeBrowse(CefBrowser browser, CefFrame frame,
CefRequest request, boolean userGesture, boolean isRedirect) {
String url = request.getURL();
// 【关键判断】如果是 data: 协议,绝对禁止调用 Desktop.browse
// 返回 false 让 CEF 内核自己渲染这个 Base64 内容
if (url != null && url.toLowerCase().startsWith("data:")) {
return false;
}
// 处理其他普通链接 (http/https/file)
if (userGesture) {
if (builder.openLinksInExternalBrowser) {
try {
Desktop.getDesktop().browse(new URI(url));
return true; // 拦截,交给系统
} catch (Exception e) {
System.out.println("外部浏览器打开失败: " + 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();
}
}
});
client.addDownloadHandler(new org.cef.handler.CefDownloadHandler() {
@Override
public void onBeforeDownload(org.cef.browser.CefBrowser browser,
org.cef.callback.CefDownloadItem downloadItem,
String suggestedName,
org.cef.callback.CefBeforeDownloadCallback callback) {
callback.Continue(suggestedName, false);
}
@Override
public void onDownloadUpdated(org.cef.browser.CefBrowser browser,
org.cef.callback.CefDownloadItem downloadItem,
org.cef.callback.CefDownloadItemCallback callback) {
}
});
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(
BrowserWindow.this,
message_text,
"警告",
JOptionPane.INFORMATION_MESSAGE
);
callback.Continue(true, "");
});
return true;
} else if (dialog_type == JSDialogType.JSDIALOGTYPE_CONFIRM) { // 处理 confirm()
SwingUtilities.invokeLater(() -> {
int result = JOptionPane.showConfirmDialog(
BrowserWindow.this,
message_text,
"确认",
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE
);
// 如果用户点击 YES (确定),则传回 true
boolean confirmed = (result == JOptionPane.YES_OPTION);
callback.Continue(confirmed, "");
});
return true;
} else if (dialog_type == JSDialogType.JSDIALOGTYPE_PROMPT) {
SwingUtilities.invokeLater(() -> {
Object result = JOptionPane.showInputDialog(
BrowserWindow.this,
message_text,
"输入",
JOptionPane.QUESTION_MESSAGE,
null,
null,
default_prompt_text
);
String input = (result != null) ? result.toString() : null;
if (input != null) {
callback.Continue(true, input);
} else {
callback.Continue(false, "");
}
});
return true;
}
// 默认行为:如果不是以上三种类型,交给 CEF 默认处理
return false;
}
});
// 3. 拦截所有新窗口(关键修复点!)
client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
@Override
public boolean onBeforePopup(CefBrowser browser, CefFrame frame,
String targetUrl, String targetFrameName) {
boolean isDataProtocol = targetUrl != null && targetUrl.toLowerCase().startsWith("data:");
if (builder.openLinksInExternalBrowser && !isDataProtocol) {
try {
Desktop.getDesktop().browse(new URI(targetUrl));
} catch (Exception e) {
System.out.println("外部浏览器打开失败: " + e.getMessage());
}
return true; // 拦截默认行为
}
SwingUtilities.invokeLater(() -> {
String popupWindowId = windowId + "_popup_" + System.currentTimeMillis();
WindowRegistry.getInstance().createNewWindow(popupWindowId, popupBuilder -> {
popupBuilder.title(getTitle()) // 继承标题
.size(getWidth(), getHeight()) // 继承大小
.htmlUrl(targetUrl) // 传入 data: URL
.icon(builder.icon) // 继承图标
.openLinksInBrowser(true); // 新窗口内链接强制内部打开
if (builder.operationHandler != null) {
popupBuilder.operationHandler(builder.operationHandler);
}
});
});
return true; // 拦截 CEF 默认弹窗,由 Java Swing 窗口接管
}
});
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 Url 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);
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
// 添加资源释放监听器
addWindowListener(new WindowAdapter() {
@Override
public void windowClosed(WindowEvent e) {
browser.close(true);
client.dispose();
}
});
setVisible(true);
return browserComponent; // 直接返回,跳过默认布局
}
}
CefMessageRouter.CefMessageRouterConfig config = new CefMessageRouter.CefMessageRouterConfig();
config.jsQueryFunction = "javaQuery";// 定义方法
config.jsCancelFunction = "javaQueryCancel";// 定义取消方法
updateTheme();
// 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(null);
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;
dragPanel.add(titleBar, BorderLayout.NORTH);
getContentPane().add(dragPanel, BorderLayout.CENTER);
getContentPane().add(browserComponent, BorderLayout.CENTER);
}
/**
* 更新主题
*/
public void updateTheme() {
// 1. 获取Java字体信息
String fontInfo = getSystemFontsInfo();
if (AxisInnovatorsBox.getMain() != null) {
boolean isDarkTheme = AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode();
injectFontInfoToPage(browser, fontInfo, isDarkTheme);
}
private void configureWindow(Builder builder) {
setTitle(builder.title);
setSize(builder.size);
setLocationRelativeTo(null);
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
setResizable(builder.resizable);
// 2. 注入主题信息
//injectThemeInfoToPage(browser, isDarkTheme);
//// 3. 刷新浏览器
//SwingUtilities.invokeLater(() -> {
// browser.reload();
//});
}
/**
* 获取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 injectThemeInfoToPage(CefBrowser browser, boolean isDarkTheme) {
if (client == null) {
return;
}
String themeInfo = String.format(
"{\"isDarkTheme\": %s, \"timestamp\": %d}",
isDarkTheme,
System.currentTimeMillis()
);
// 最简单的脚本 - 直接设置和分发事件
String script = String.format(
"window.javaThemeInfo = %s;" +
"console.log('主题信息已设置:', window.javaThemeInfo);" +
"" +
"var event = new CustomEvent('javaThemeChanged', {" +
" detail: window.javaThemeInfo" +
"});" +
"document.dispatchEvent(event);" +
"console.log('javaThemeChanged事件已分发');",
themeInfo);
browser.executeJavaScript(script, browser.getURL(), 0);
}
/**
* 注入字体信息到页面并设置字体
*/
private void injectFontInfoToPage(CefBrowser browser, String fontInfo,boolean isDarkTheme) {
if (client == null) {
return;
}
client.addLoadHandler(new CefLoadHandlerAdapter() {
addWindowListener(new WindowAdapter() {
@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');" +
"}";
browser.executeJavaScript(script, browser.getURL(), 0);
// 添加调试信息
browser.executeJavaScript(
"console.log('Font information injection is completewindow.javaFontInfo:', typeof window.javaFontInfo);" +
"console.log('Number of event listeners:', document.eventListeners ? document.eventListeners('javaFontsLoaded') : '无法获取');",
browser.getURL(), 0
);
String themeInfo = String.format(
"{\"isDarkTheme\": %s, \"timestamp\": %d}",
isDarkTheme,
System.currentTimeMillis()
);
script = String.format(
"window.javaThemeInfo = %s;" +
"console.log('主题信息已设置:', window.javaThemeInfo);" +
"" +
"var event = new CustomEvent('javaThemeChanged', {" +
" detail: window.javaThemeInfo" +
"});" +
"document.dispatchEvent(event);" +
"console.log('javaThemeChanged事件已分发');",
themeInfo);
browser.executeJavaScript(script, browser.getURL(), 0);
public void windowClosed(WindowEvent e) {
closeWindow();
}
});
}
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() + ")");
}
setVisible(true);
}
@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<String> 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;
}
@Override
public void executingJsCode(String script) {
if (browserCore != null) {
browserCore.executingJsCode(script);
}
}
/**
* 获取消息路由器
* @return 消息路由器
* 保持 API 兼容性:将调用转发给 Core
*/
@Override
public CefMessageRouter getMsgRouter() {
return msgRouter;
if (browserCore == null) return null;
return browserCore.getMsgRouter();
}
/**
* 获取浏览器对象
* @return 浏览器对象
*/
@Override
public CefBrowser getBrowser() {
return browser;
if (browserCore == null) return null;
return browserCore.getBrowser();
}
@Override
public void closeWindow() {
SwingUtilities.invokeLater(() -> {
if (browser != null) {
browser.close(true);
}
dispose();
cefApp.dispose();
WindowRegistry.getInstance().unregisterWindow(windowId);
});
if (browserCore != null) {
browserCore.dispose();
}
WindowRegistry.getInstance().unregisterWindow(windowId);
dispose();
}
}
@Override
public void updateTheme() {
if (browserCore != null) {
browserCore.updateTheme();
}
}
@Override
public void setController(JsBridgeController controller) {
browserCore.setJsBridgeController(controller);
}
// 提取拖拽逻辑内部类
private static class WindowDragListener extends MouseAdapter {
private final Window window;
private Point dragStart;
public WindowDragListener(Window window) { this.window = window; }
@Override
public void mousePressed(MouseEvent e) { dragStart = e.getPoint(); }
@Override
public void mouseReleased(MouseEvent e) { dragStart = null; }
@Override
public void mouseDragged(MouseEvent e) {
if (dragStart != null) {
Point curr = e.getLocationOnScreen();
window.setLocation(curr.x - dragStart.x, curr.y - dragStart.y);
}
}
}
}

View File

@@ -1,834 +1,164 @@
package com.axis.innovators.box.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 com.axis.innovators.box.browser.bridge.JsBridgeController;
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 org.json.JSONObject;
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 {
public class BrowserWindowJDialog extends JDialog implements BrowserContainer {
private final BrowserCore browserCore;
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;
public static class Builder extends BaseBrowserBuilder<Builder> {
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(String windowId) { super(windowId); }
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路径
*/
@Override
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());
}
if ((htmlUrl == null || htmlUrl.isEmpty()) && (htmlPath == null || htmlPath.isEmpty())) {
throw new IllegalArgumentException("HTML path or URL cannot be empty");
}
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);
// 设置图标(如果存在)
if (builder.icon != null) {
setIconImage(builder.icon);
this.browserCore = new BrowserCore(this, windowId, builder);
if (builder.controller != null) {
browserCore.setJsBridgeController(builder.controller);
}
// 初始化浏览器组件
try {
this.browserComponent = initializeCef(builder);
if (operationHandler != null) {
setupMessageHandlers(operationHandler);
Component browserComponent = browserCore.initialize();
if (builder.browserCreationCallback != null) {
boolean handled = builder.browserCreationCallback.onLayoutCustomization(
this, getContentPane(), browserComponent, builder
);
if (handled) {
configureWindow(builder);
return;
}
}
initDefaultLayout(builder, browserComponent);
configureWindow(builder);
} 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) {}
private void initDefaultLayout(Builder builder, Component browserComponent) {
getContentPane().setLayout(new BorderLayout());
@Override
public void OnFullscreenModeChange(CefBrowser browser, boolean fullscreen) {}
JPanel dragPanel = new JPanel(new BorderLayout());
dragPanel.setOpaque(false);
JPanel titleBar = new JPanel();
titleBar.setOpaque(false);
titleBar.setPreferredSize(new Dimension(builder.size.width, 20));
@Override
public boolean onTooltip(CefBrowser browser, String text) {
return false;
}
WindowDragListener dragListener = new WindowDragListener(this);
titleBar.addMouseListener(dragListener);
titleBar.addMouseMotionListener(dragListener);
@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, CefJSDialogHandler.JSDialogType dialog_type, String message_text, String default_prompt_text, CefJSDialogCallback callback, BoolRef suppress_message) {
if (dialog_type == CefJSDialogHandler.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;
dragPanel.add(titleBar, BorderLayout.NORTH);
getContentPane().add(dragPanel, BorderLayout.CENTER);
getContentPane().add(browserComponent, BorderLayout.CENTER);
}
private void configureWindow(Builder builder) {
setTitle(builder.title);
setSize(builder.size);
setLocationRelativeTo(builder.parentFrame);
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
setResizable(builder.resizable);
/**
* 更新主题
*/
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() {
addWindowListener(new WindowAdapter() {
@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 completewindow.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 completewindow.javaFontInfo:', typeof window.javaFontInfo);" +
"console.log('Number of event listeners:', document.eventListeners ? document.eventListeners('javaFontsLoaded') : '无法获取');",
browser.getURL(), 0
);
public void windowClosed(WindowEvent e) {
closeWindow();
}
});
}
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() + ")");
}
setVisible(true);
}
@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<String> 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 消息路由器
*/
@Override
public CefMessageRouter getMsgRouter() {
return msgRouter;
if (browserCore == null) return null;
return browserCore.getMsgRouter();
}
/**
* 获取浏览器对象
* @return 浏览器对象
*/
@Override
public void executingJsCode(String script) {
if (browserCore != null) {
browserCore.executingJsCode(script);
}
}
@Override
public CefBrowser getBrowser() {
return browser;
if (browserCore == null) return null;
return browserCore.getBrowser();
}
@Override
public void closeWindow() {
SwingUtilities.invokeLater(() -> {
if (browser != null) {
browser.close(true);
}
dispose();
cefApp.dispose();
WindowRegistry.getInstance().unregisterWindow(windowId);
});
if (browserCore != null) {
browserCore.dispose();
}
WindowRegistry.getInstance().unregisterWindow(windowId);
dispose();
}
}
@Override
public void updateTheme() {
if (browserCore != null) {
browserCore.updateTheme();
}
}
@Override
public void setController(JsBridgeController controller) {
browserCore.setJsBridgeController(controller);
}
// 复用拖拽逻辑(实际项目中可以将 WindowDragListener 提取为单独的公共类)
private static class WindowDragListener extends MouseAdapter {
private final Window window;
private Point dragStart;
public WindowDragListener(Window window) { this.window = window; }
@Override
public void mousePressed(MouseEvent e) { dragStart = e.getPoint(); }
@Override
public void mouseReleased(MouseEvent e) { dragStart = null; }
@Override
public void mouseDragged(MouseEvent e) {
if (dragStart != null) {
Point curr = e.getLocationOnScreen();
window.setLocation(curr.x - dragStart.x, curr.y - dragStart.y);
}
}
}
}

View File

@@ -42,12 +42,10 @@ public class MainApplication {
private static long ctxHandle;
private static boolean isSystem = true;
public static void main(String[] args) {
System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8));
TerminalManager.popupRealLinuxWindow();
}
public static void popupSimulatingLinuxWindow(JFrame parent){
}
/**
* 初始化数据库连接
*/
@@ -234,20 +232,17 @@ public class MainApplication {
* @param parent 父窗口
*/
public static void popupSimulatingSQLWindow(JFrame parent) {
AtomicReference<BrowserWindowJDialog> window = new AtomicReference<>();
initDatabase();
SwingUtilities.invokeLater(() -> {
WindowRegistry.getInstance().createNewChildWindow("main", builder ->
window.set(builder.title("SQL Command Line Client")
BrowserWindowJDialog window = WindowRegistry.getInstance().createNewChildWindow("main",builder ->
builder.title("SQL Command Line Client")
.parentFrame(parent)
.icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage())
.size(900, 600)
.htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "SQLTerminal.html")
.operationHandler(createOperationHandler())
.build())
);
.operationHandler(createOperationHandler()));
CefMessageRouter msgRouter = window.get().getMsgRouter();
CefMessageRouter msgRouter = window.getMsgRouter();
if (msgRouter != null) {
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
// 在 popupSimulatingSQLWindow 方法的 msgRouter.addHandler 内部:
@@ -297,19 +292,17 @@ public class MainApplication {
modelHandle = LM.llamaLoadModelFromFile(LM.DEEP_SEEK);
ctxHandle = LM.createContext(modelHandle);
AtomicReference<BrowserWindowJDialog> window = new AtomicReference<>();
SwingUtilities.invokeLater(() -> {
WindowRegistry.getInstance().createNewChildWindow("main", builder ->
window.set(builder.title("Axis Innovators Box AI 工具箱")
BrowserWindowJDialog window = WindowRegistry.getInstance().createNewChildWindow("main", builder ->
builder.title("Axis Innovators Box AI 工具箱")
.parentFrame(parent)
.icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage())
.size(1280, 720)
.htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "AIaToolbox_dark.html")
.operationHandler(createOperationHandler())
.build())
);
CefMessageRouter msgRouter = window.get().getMsgRouter();
CefMessageRouter msgRouter = window.getMsgRouter();
if (msgRouter != null) {
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
@Override
@@ -331,18 +324,16 @@ public class MainApplication {
}
public static void popupCCodeEditorWindow() {
AtomicReference<BrowserWindow> window = new AtomicReference<>();
SwingUtilities.invokeLater(() -> {
WindowRegistry.getInstance().createNewWindow("main", builder ->
window.set(builder.title("TzdC 代码编辑器")
BrowserWindow window = WindowRegistry.getInstance().createNewWindow("main", builder ->
builder.title("TzdC 代码编辑器")
.icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage())
.size(1487, 836)
.htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "CCodeEditor.html")
.operationHandler(createOperationHandler())
.build())
);
CefMessageRouter msgRouter = window.get().getMsgRouter();
CefMessageRouter msgRouter = window.getMsgRouter();
if (msgRouter != null) {
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
@Override
@@ -382,18 +373,15 @@ public class MainApplication {
}
public static void popupCodeEditorWindow() {
AtomicReference<BrowserWindow> window = new AtomicReference<>();
SwingUtilities.invokeLater(() -> {
WindowRegistry.getInstance().createNewWindow("main", builder ->
window.set(builder.title("代码编辑器")
BrowserWindow window = WindowRegistry.getInstance().createNewWindow("main", builder ->builder.title("代码编辑器")
.icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage())
.size(1487, 836)
.htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "CodeEditor.html")
.operationHandler(createOperationHandler())
.build())
);
CefMessageRouter msgRouter = window.get().getMsgRouter();
CefMessageRouter msgRouter = window.getMsgRouter();
if (msgRouter != null) {
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
@Override
@@ -447,18 +435,16 @@ public class MainApplication {
* 弹出html预览窗口
*/
public static void popupHTMLWindow(String path) {
AtomicReference<BrowserWindow> window = new AtomicReference<>();
SwingUtilities.invokeLater(() -> {
WindowRegistry.getInstance().createNewWindow("main", builder ->
window.set(builder.title("Axis Innovators Box HTML查看器")
BrowserWindow window = WindowRegistry.getInstance().createNewWindow("main", builder ->
builder.title("Axis Innovators Box HTML查看器")
.icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage())
.size(1487, 836)
.htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "HtmlViewer.html")
.operationHandler(createOperationHandler())
.build())
);
CefMessageRouter msgRouter = window.get().getMsgRouter();
CefMessageRouter msgRouter = window.getMsgRouter();
if (msgRouter != null) {
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
@Override
@@ -603,23 +589,21 @@ public class MainApplication {
System.err.println("预加载 JDBC 驱动时发生异常: " + t.getMessage());
}
AtomicReference<BrowserWindow> window = new AtomicReference<>();
SwingUtilities.invokeLater(() -> {
WindowRegistry.getInstance().createNewWindow("main", builder ->
window.set(builder.title("Axis Innovators Box 数据库管理工具")
BrowserWindow window = WindowRegistry.getInstance().createNewWindow("main", builder ->
builder.title("Axis Innovators Box 数据库管理工具")
.icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage())
.size(1487, 836)
.htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "DatabaseTool.html")
.operationHandler(createOperationHandler())
.build())
);
if (window.get() == null) {
if (window == null) {
System.err.println("popupDataBaseWindow: window 创建失败window.get() == null");
return;
}
CefMessageRouter msgRouter = window.get().getMsgRouter();
CefMessageRouter msgRouter = window.getMsgRouter();
if (msgRouter != null) {
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
@Override

View File

@@ -15,10 +15,11 @@ import java.util.function.Consumer;
public class WindowRegistry {
private static WindowRegistry instance;
private final ConcurrentMap<String, BrowserWindow> windows =
new ConcurrentHashMap<>();
private final ConcurrentMap<String, BrowserWindowJDialog> childWindows =
new ConcurrentHashMap<>();
// 建议:统一使用 BrowserContainer 接口存储,这样可以合并两个 Map
// 这里为了兼容你现有的代码,暂时保留分开的 Map但逻辑上可以合并
private final ConcurrentMap<String, BrowserWindow> windows = new ConcurrentHashMap<>();
private final ConcurrentMap<String, BrowserWindowJDialog> childWindows = new ConcurrentHashMap<>();
private final Map<String, Consumer<String>> callbacks = new ConcurrentHashMap<>();
private WindowRegistry() {}
@@ -47,75 +48,91 @@ public class WindowRegistry {
}
public void unregisterWindow(String windowId) {
// 先尝试从主窗口移除
BrowserWindow window = windows.remove(windowId);
if (window != null) {
window.closeWindow();
return;
}
// 如果不是主窗口,尝试从子窗口移除
BrowserWindowJDialog child = childWindows.remove(windowId);
if (child != null) {
child.closeWindow();
}
}
public BrowserWindow getWindow(String windowId) {
return windows.get(windowId);
// 泛型获取方法,尝试查找两种类型的窗口
public BrowserContainer getWindow(String windowId) {
if (windows.containsKey(windowId)) return windows.get(windowId);
if (childWindows.containsKey(windowId)) return childWindows.get(windowId);
return null;
}
public void update() {
for (BrowserWindow window : windows.values()) {
if (window != null) {
window.updateTheme();
}
}
for (BrowserWindowJDialog window : childWindows.values()) {
if (window != null) {
window.updateTheme();
}
}
windows.values().forEach(BrowserWindow::updateTheme);
childWindows.values().forEach(BrowserWindowJDialog::updateTheme);
}
/**
* 创建一个新的窗口
* @param windowId 窗口ID
* @param config 窗口配置
* @return 返回创建的 BrowserWindow 实例
*/
public void createNewWindow(String windowId, Consumer<BrowserWindow.Builder> config) {
public BrowserWindow createNewWindow(String windowId, Consumer<BrowserWindow.Builder> config) {
BrowserWindow.Builder builder = new BrowserWindow.Builder(windowId);
config.accept(builder);
BrowserWindow window = builder.build();
registerWindow(window);
loadExtLibsPath(window);
// 1. 这里只运行配置逻辑,不要在 config 中调用 build()
config.accept(builder);
// 2. 这里统一进行 build只创建一次实例
BrowserWindow window = builder.build();
registerWindow(window);
loadExtLibsPath(window); // 使用统一的接口方法
return window; // 返回实例
}
/**
* 创建一个新的子窗口
* @param windowId 窗口ID
* @param config 窗口配置
* @return 返回创建的 BrowserWindowJDialog 实例
*/
public void createNewChildWindow(String windowId, Consumer<BrowserWindowJDialog.Builder> config) {
public BrowserWindowJDialog createNewChildWindow(String windowId, Consumer<BrowserWindowJDialog.Builder> config) {
BrowserWindowJDialog.Builder builder = new BrowserWindowJDialog.Builder(windowId);
config.accept(builder);
BrowserWindowJDialog window = builder.build();
registerChildWindow(window);
loadExtLibsPath(window);
return window;
}
private void loadExtLibsPath(BrowserWindow window) {
/**
* 统一的注入逻辑,接收 BrowserContainer 接口
* 这样 BrowserWindow 和 BrowserWindowJDialog 都可以复用此方法
*/
private void loadExtLibsPath(BrowserContainer window) {
CefBrowser cefBrowser = window.getBrowser();
if (cefBrowser == null) return;
if (cefBrowser != null)
// 使用 CefClient 的调度方法(如果可用)或直接添加 LoadHandler
// 使用 client 注入或者直接 browser 注入
// 这里的逻辑只需要写一遍
cefBrowser.getClient().addLoadHandler(new CefLoadHandlerAdapter() {
@Override
public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
if (frame.isMain()) {
try {
String extLibsPath = FolderCreator.getJavaScriptFolder() + "\\" + "extLibs";
String extLibsPath = FolderCreator.getJavaScriptFolder() + File.separator + "extLibs";
File extLibsDir = new File(extLibsPath);
// 注意:这里可能需要转义反斜杠,或者直接使用 /
String safePath = extLibsPath.replace("\\", "\\\\");
if (!extLibsDir.exists() || !extLibsDir.isDirectory()) {
throw new IOException("extLibs目录无效: " + extLibsPath);
// 仅打印日志,不抛出导致程序崩溃的异常,或者视需求而定
System.err.println("extLibs目录不存在: " + extLibsPath);
}
String script = "window.extLibsPath = " + JSONObject.valueToString(extLibsPath) + ";";
// 使用 JSON 库处理字符串转义是最安全的
String jsonPath = JSONObject.valueToString(extLibsPath);
String script = "window.extLibsPath = " + jsonPath + "; console.log('ExtLibs path set:', window.extLibsPath);";
browser.executeJavaScript(script, frame.getURL(), 0);
} catch (Exception e) {
System.err.println("注入extLibsPath失败: " + e.getMessage());
@@ -125,29 +142,4 @@ public class WindowRegistry {
}
});
}
private void loadExtLibsPath(BrowserWindowJDialog window) {
CefBrowser cefBrowser = window.getBrowser();
if (cefBrowser != null)
// 使用 CefClient 的调度方法(如果可用)或直接添加 LoadHandler
cefBrowser.getClient().addLoadHandler(new CefLoadHandlerAdapter() {
@Override
public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
if (frame.isMain()) {
try {
String extLibsPath = FolderCreator.getJavaScriptFolder() + "\\" + "extLibs";
File extLibsDir = new File(extLibsPath);
if (!extLibsDir.exists() || !extLibsDir.isDirectory()) {
throw new IOException("extLibs目录无效: " + extLibsPath);
}
String script = "window.extLibsPath = " + JSONObject.valueToString(extLibsPath) + ";";
browser.executeJavaScript(script, frame.getURL(), 0);
} catch (Exception e) {
System.err.println("注入extLibsPath失败: " + e.getMessage());
e.printStackTrace();
}
}
}
});
}
}
}

View File

@@ -0,0 +1,169 @@
package com.axis.innovators.box.browser.bridge;
import com.axis.innovators.box.browser.BrowserCore;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonParser;
import org.cef.browser.CefBrowser;
import org.cef.browser.CefFrame;
import org.cef.callback.CefQueryCallback;
import org.cef.handler.CefMessageRouterHandlerAdapter;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* JS 桥接控制器基类
* 用户继承此类并在其中编写带有 @JsMapping 的方法
*/
public abstract class JsBridgeController {
private final Map<String, Method> methodRegistry = new HashMap<>();
private final Gson gson = new Gson();
private BrowserCore browserCore;
public JsBridgeController() {
scanMethods();
}
/**
* 绑定 BrowserCore用于执行回调或JS注入
*/
public void attach(BrowserCore core) {
this.browserCore = core;
}
/**
* 扫描子类中所有带 @JsMapping 的方法
*/
private void scanMethods() {
Method[] methods = this.getClass().getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(JsMapping.class)) {
JsMapping annotation = method.getAnnotation(JsMapping.class);
String path = annotation.value();
// 默认规则:如果注解为空,使用 tzd.<方法名>
if (path.isEmpty()) {
path = "tzd." + method.getName();
}
// 如果没有包含点且不为空,默认为 tzd.<value>
else if (!path.contains(".")) {
path = "tzd." + path;
}
// 否则直接使用定义的全路径 (如 "ggg.ddd")
methodRegistry.put(path, method);
}
}
}
/**
* 处理来自 CEF 的路由消息
* 格式协议: "jsbridge:methodPath:[argsJson]"
*/
public boolean handleQuery(CefBrowser browser, CefFrame frame, long queryId, String request, boolean persistent, CefQueryCallback callback) {
if (!request.startsWith("jsbridge:")) {
return false;
}
// 解析请求jsbridge:tzd.test:[1, "abc"]
try {
String content = request.substring("jsbridge:".length());
int firstColon = content.indexOf(":");
if (firstColon == -1) return false;
String methodPath = content.substring(0, firstColon);
String argsJson = content.substring(firstColon + 1);
Method method = methodRegistry.get(methodPath);
if (method == null) {
callback.failure(404, "Method not found: " + methodPath);
return true;
}
// 参数反序列化与调用
Object[] args = parseArgs(method, argsJson);
Object result = method.invoke(this, args);
// 返回结果
String response = result != null ? gson.toJson(result) : "null";
callback.success(response);
} catch (Exception e) {
e.printStackTrace();
callback.failure(500, "Java execution error: " + e.getMessage());
}
return true;
}
/**
* 根据方法签名将 JSON 数组转为 Java 对象数组
*/
private Object[] parseArgs(Method method, String jsonArrayStr) {
if (jsonArrayStr == null || jsonArrayStr.isEmpty()) return new Object[0];
JsonArray jsonArray = JsonParser.parseString(jsonArrayStr).getAsJsonArray();
Class<?>[] paramTypes = method.getParameterTypes();
if (jsonArray.size() != paramTypes.length) {
throw new IllegalArgumentException("Argument count mismatch");
}
Object[] args = new Object[paramTypes.length];
for (int i = 0; i < paramTypes.length; i++) {
args[i] = gson.fromJson(jsonArray.get(i), paramTypes[i]);
}
return args;
}
/**
* 生成注入到前端的 JavaScript 代码
* 自动构建对象层级(如 var tzd = {}; tzd.ggg = {}; ...
*/
public String generateInjectionJs() {
StringBuilder js = new StringBuilder();
js.append("(function() {");
js.append(" function _javaCall(path, args) {");
js.append(" return new Promise(function(resolve, reject) {");
js.append(" window.javaQuery({");
js.append(" request: 'jsbridge:' + path + ':' + JSON.stringify(args),");
js.append(" onSuccess: function(res) { resolve(JSON.parse(res)); },");
js.append(" onFailure: function(code, msg) { reject(msg); }");
js.append(" });");
js.append(" });");
js.append(" };");
js.append(" function _ensureNs(ns) {");
js.append(" var parts = ns.split('.'); var root = window;");
js.append(" for(var i=0; i<parts.length; i++) {");
js.append(" if(!root[parts[i]]) root[parts[i]] = {};");
js.append(" root = root[parts[i]];");
js.append(" }");
js.append(" return root;");
js.append(" };");
for (String path : methodRegistry.keySet()) {
int lastDot = path.lastIndexOf('.');
String ns = (lastDot > -1) ? path.substring(0, lastDot) : "window";
String funcName = (lastDot > -1) ? path.substring(lastDot + 1) : path;
// 生成代码:构建命名空间并挂载函数
if (!"window".equals(ns)) {
js.append(String.format(" _ensureNs('%s');", ns));
}
// 挂载函数代理
js.append(String.format(" window.%s = window.%s || {};", ns, ns)); // 安全检查
js.append(String.format(" %s.%s = function() {", (ns.equals("window") ? "window" : ns), funcName));
js.append(String.format(" return _javaCall('%s', Array.prototype.slice.call(arguments));", path));
js.append(" };");
}
js.append("})();");
return js.toString();
}
protected BrowserCore getBrowserCore() {
return browserCore;
}
}

View File

@@ -0,0 +1,20 @@
package com.axis.innovators.box.browser.bridge;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标记该方法可以被 JavaScript 调用
* <p>
* 用法示例:
* 1. @JsMapping -> JS: tzd.methodName()
* 2. @JsMapping("myFunc") -> JS: tzd.myFunc()
* 3. @JsMapping("app.utils.calc") -> JS: app.utils.calc()
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface JsMapping {
String value() default "";
}

View File

@@ -0,0 +1,79 @@
package com.axis.innovators.box.browser.test;
import com.axis.innovators.box.browser.BrowserWindow;
import com.axis.innovators.box.browser.bridge.JsBridgeController;
import com.axis.innovators.box.browser.bridge.JsMapping;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.Date;
/**
* 测试启动入口
*/
public class BrowserTestMain {
public static void main(String[] args) {
String htmlPath = new File("C:\\Users\\Administrator\\MCreatorWorkspaces\\AxisInnovatorsBox\\src\\main\\java\\com\\axis\\innovators\\box\\browser\\test\\test_bridge.html").getAbsolutePath();
System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8));
SwingUtilities.invokeLater(() -> {
new BrowserWindow.Builder("test_window_001")
.title("JSBridge 功能测试")
.size(1024, 768)
.htmlPath(htmlPath)
.build().setController(new MyTestController());
});
}
/**
* 自定义业务控制器
*/
public static class MyTestController extends JsBridgeController {
/**
* 1. 默认映射
* JS 调用: tzd.getJavaTime()
*/
@JsMapping
public String getJavaTime() {
return "Java Time: " + new Date().toString();
}
/**
* 2. 自定义方法名
* JS 调用: tzd.hello("World")
*/
@JsMapping("hello")
public String sayHello(String name) {
System.out.println("Java 收到名字: " + name);
return "你好, " + name + "! (来自 Java)";
}
/**
* 3. 自定义深层路径
* JS 调用: sys.math.add(10, 20)
*/
@JsMapping("sys.math.add")
public int add(int a, int b) {
System.out.println("Java 计算: " + a + " + " + b);
return a + b;
}
/**
* 4. 操作浏览器窗口
* JS 调用: win.ctrl.setTitle("新标题")
*/
@JsMapping("win.ctrl.setTitle")
public void setWindowTitle(String title) {
SwingUtilities.invokeLater(() -> {
Window window = SwingUtilities.getWindowAncestor(getBrowserCore().getBrowser().getUIComponent());
if (window instanceof JFrame) {
((JFrame) window).setTitle(title);
}
});
}
}
}

View File

@@ -0,0 +1,106 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>JSBridge 测试页面</title>
<style>
body { font-family: 'Segoe UI', sans-serif; padding: 20px; background-color: #f4f4f9; }
h2 { color: #333; }
.card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); margin-bottom: 20px; }
button {
padding: 10px 20px; margin: 5px; cursor: pointer;
background-color: #007bff; color: white; border: none; border-radius: 4px; font-size: 14px;
}
button:hover { background-color: #0056b3; }
button.action { background-color: #28a745; }
#console {
background: #2d2d2d; color: #00ff00; padding: 15px;
height: 300px; overflow-y: auto; border-radius: 4px; font-family: monospace;
}
.log-line { margin: 5px 0; border-bottom: 1px solid #444; }
</style>
</head>
<body>
<h2>Java <-> JavaScript 交互测试</h2>
<div class="card">
<h3>1. 基础调用 (tzd 命名空间)</h3>
<button onclick="testGetTime()">获取 Java 时间 (tzd.getJavaTime)</button>
<button onclick="testHello()">发送问候 (tzd.hello)</button>
</div>
<div class="card">
<h3>2. 自定义路径与计算</h3>
<button onclick="testMath()">计算 15 + 25 (sys.math.add)</button>
</div>
<div class="card">
<h3>3. 控制宿主窗口</h3>
<button class="action" onclick="changeTitle()">修改窗口标题 (win.ctrl.setTitle)</button>
</div>
<div class="card">
<h3>运行日志</h3>
<div id="console"></div>
</div>
<script>
// 日志辅助函数
function log(msg) {
const consoleDiv = document.getElementById('console');
const line = document.createElement('div');
line.className = 'log-line';
line.innerText = `[${new Date().toLocaleTimeString()}] ${msg}`;
consoleDiv.appendChild(line);
consoleDiv.scrollTop = consoleDiv.scrollHeight;
}
// --- 测试用例 ---
async function testGetTime() {
try {
// 等待 Java 返回结果
log("调用 tzd.getJavaTime()...");
let time = await tzd.getJavaTime();
log("✅ Java 返回: " + time);
} catch (e) {
log("❌ 错误: " + e);
}
}
async function testHello() {
try {
log("调用 tzd.hello('Web User')...");
let response = await tzd.hello("Web User");
log("✅ Java 返回: " + response);
} catch (e) {
log("❌ 错误: " + e);
}
}
async function testMath() {
try {
let a = 15, b = 25;
log(`调用 sys.math.add(${a}, ${b})...`);
// 注意Controller 定义了 @JsMapping("sys.math.add")
let result = await sys.math.add(a, b);
log("✅ 计算结果: " + result);
} catch (e) {
log("❌ 错误: " + e);
}
}
async function changeTitle() {
try {
let newTitle = "标题被 JS 修改了 - " + Math.floor(Math.random() * 100);
log("调用 win.ctrl.setTitle...");
await win.ctrl.setTitle(newTitle);
log("✅ 标题已修改为: " + newTitle);
} catch (e) {
log("❌ 错误: " + e);
}
}
</script>
</body>
</html>

View File

@@ -11,6 +11,7 @@ import org.cef.browser.CefBrowser;
import org.cef.browser.CefFrame;
import org.cef.browser.CefMessageRouter;
import org.cef.callback.CefQueryCallback;
import org.cef.handler.CefLoadHandlerAdapter;
import org.cef.handler.CefMessageRouterHandlerAdapter;
import org.json.JSONObject;
@@ -37,23 +38,20 @@ public class TerminalManager {
* 启动真实的终端窗口
*/
public static void popupRealLinuxWindow() {
AtomicReference<BrowserWindow> window = new AtomicReference<>();
SwingUtilities.invokeLater(() -> {
// 1. 创建窗口
WindowRegistry.getInstance().createNewWindow("real_terminal", builder ->
window.set(builder.title("Terminal Linux")
.size(900, 600)
.htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "LinuxTerminal.html")
.operationHandler(createOperationHandler())
.build())
);
BrowserWindow window = WindowRegistry.getInstance().createNewWindow("real_terminal", builder -> {
builder.title("Terminal Linux")
.size(900, 600)
.htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "LinuxTerminal.html")
.operationHandler(createOperationHandler());
});
// 2. 初始化 PTY 进程
startPtyProcess(window.get().getBrowser());
startPtyProcess(window.getBrowser());
// 3. 注册 JCEF 消息处理器
CefMessageRouter msgRouter = window.get().getMsgRouter();
CefMessageRouter msgRouter = window.getMsgRouter();
if (msgRouter != null) {
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
@Override
@@ -62,10 +60,8 @@ public class TerminalManager {
try {
JSONObject json = new JSONObject(request);
if ("terminalInput".equals(json.optString("type"))) {
// 接收前端的按键数据
String data = json.getString("data");
if (ptyInput != null) {
// 写入到 Shell 进程的标准输入
ptyInput.write(data.getBytes(StandardCharsets.UTF_8));
ptyInput.flush();
}
@@ -77,14 +73,20 @@ public class TerminalManager {
}
return false;
}
@Override
public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) {}
}, true);
}
// 窗口关闭时杀死进程
window.get().addWindowListener(new java.awt.event.WindowAdapter() {
window.getBrowser().getClient().addLoadHandler(new CefLoadHandlerAdapter() {
@Override
public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
if (frame.isMain()) {
System.out.println("页面加载完成,开始初始化终端逻辑...");
startPtyProcess(browser);
}
}
});
window.addWindowListener(new java.awt.event.WindowAdapter() {
@Override
public void windowClosed(java.awt.event.WindowEvent windowEvent) {
stopPtyProcess();

View File

@@ -45,21 +45,15 @@ public class ModernJarViewer {
// --- CEF Window Initialization ---
// 修改:增加 jarPath 参数
public static void popupSimulatingWindow(JFrame parent, String jarPath) {
AtomicReference<BrowserWindow> windowRef = new AtomicReference<>();
SwingUtilities.invokeLater(() -> {
// 1. 创建浏览器窗口
WindowRegistry.getInstance().createNewWindow("main", builder -> {
BrowserWindow window = builder.title("Java Decompiler Pro")
BrowserWindow window = WindowRegistry.getInstance().createNewWindow("main", builder -> {
builder.title("Java Decompiler Pro")
.size(1400, 900)
.htmlPath(FolderCreator.getJavaScriptFolder() + File.separator + "HtmlJarViewer.html")
.operationHandler(new WindowOperationHandler.Builder().withDefaultOperations().build())
.build();
windowRef.set(window);
.operationHandler(new WindowOperationHandler.Builder().withDefaultOperations().build());
});
// 2. 配置消息路由 (JS -> Java Bridge)
BrowserWindow window = windowRef.get();
CefMessageRouter msgRouter = window.getMsgRouter();
if (msgRouter != null) {

View File

@@ -1,4 +1,4 @@
# Auto-generated build information
version=0.0.1
buildTimestamp=2026-01-02T18:36:04.5818254
buildTimestamp=2026-01-03T08:33:49.8039508
buildSystem=WINDOWS