docs: 添加 Axis Innovators Box 框架完整 API 文档
- 新增浏览器模块技术文档,涵盖 BrowserCore、BrowserWindow 等核心组件 - 添加事件系统文档,包括 EventBus、GlobalEventBus 及各类事件定义 - 创建 LanguageManager 国际化管理器详细说明文档 - 新增 Log4j2OutputStream 标准输出重定向类文档 - 添加 Main 入口类启动流程与路由机制说明 - 创建 BrowserCreationCallback 回调接口使用指南 - 完善 AxisInnovatorsBox 主类架构与崩溃诊断系统文档
This commit is contained in:
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
621
src/main/java/com/axis/innovators/box/browser/BrowserCore.java
Normal file
621
src/main/java/com/axis/innovators/box/browser/BrowserCore.java
Normal 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; }
|
||||
}
|
||||
@@ -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 complete,window.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 complete,window.javaThemeInfo:', typeof window.javaThemeInfo);" +
|
||||
"console.log('Number of theme event listeners:', document.eventListeners ? document.eventListeners('javaThemeChanged') : '无法获取');",
|
||||
browser.getURL(), 0
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取Java字体信息(从UIManager获取)
|
||||
*/
|
||||
private String getSystemFontsInfo() {
|
||||
try {
|
||||
Gson gson = new Gson();
|
||||
JsonObject fontInfo = new JsonObject();
|
||||
JsonObject uiFonts = new JsonObject();
|
||||
|
||||
String[] fontKeys = {
|
||||
"Label.font", "Button.font", "ToggleButton.font", "RadioButton.font",
|
||||
"CheckBox.font", "ColorChooser.font", "ComboBox.font", "EditorPane.font",
|
||||
"TextArea.font", "TextField.font", "PasswordField.font", "TextPane.font",
|
||||
"FormattedTextField.font", "Table.font", "TableHeader.font", "List.font",
|
||||
"Tree.font", "TabbedPane.font", "MenuBar.font", "Menu.font", "MenuItem.font",
|
||||
"PopupMenu.font", "CheckBoxMenuItem.font", "RadioButtonMenuItem.font",
|
||||
"Spinner.font", "ToolBar.font", "TitledBorder.font", "OptionPane.messageFont",
|
||||
"OptionPane.buttonFont", "Panel.font", "Viewport.font", "ToolTip.font"
|
||||
};
|
||||
|
||||
for (String key : fontKeys) {
|
||||
Font font = UIManager.getFont(key);
|
||||
if (font != null) {
|
||||
JsonObject fontObj = new JsonObject();
|
||||
fontObj.addProperty("name", font.getFontName());
|
||||
fontObj.addProperty("family", font.getFamily());
|
||||
fontObj.addProperty("size", font.getSize());
|
||||
fontObj.addProperty("style", font.getStyle());
|
||||
fontObj.addProperty("bold", font.isBold());
|
||||
fontObj.addProperty("italic", font.isItalic());
|
||||
fontObj.addProperty("plain", font.isPlain());
|
||||
uiFonts.add(key, fontObj);
|
||||
}
|
||||
}
|
||||
|
||||
fontInfo.add("uiFonts", uiFonts);
|
||||
fontInfo.addProperty("timestamp", System.currentTimeMillis());
|
||||
fontInfo.addProperty("lookAndFeel", UIManager.getLookAndFeel().getName());
|
||||
|
||||
return gson.toJson(fontInfo);
|
||||
} catch (Exception e) {
|
||||
return "{\"error\": \"无法获取UIManager字体信息: " + e.getMessage() + "\"}";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入字体信息到页面并设置字体
|
||||
*/
|
||||
private void injectFontInfoToPage(CefBrowser browser, String fontInfo) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
client.addLoadHandler(new CefLoadHandlerAdapter() {
|
||||
@Override
|
||||
public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
|
||||
// 使用更简单的脚本来注入字体信息
|
||||
String script =
|
||||
"if (typeof window.javaFontInfo === 'undefined') {" +
|
||||
" window.javaFontInfo = " + fontInfo + ";" +
|
||||
" console.log('Java font information has been loaded:', window.javaFontInfo);" +
|
||||
" " +
|
||||
" var event = new CustomEvent('javaFontsLoaded', {" +
|
||||
" detail: window.javaFontInfo" +
|
||||
" });" +
|
||||
" document.dispatchEvent(event);" +
|
||||
" console.log('The javaFontsLoaded event is dispatched');" +
|
||||
"}";
|
||||
|
||||
System.out.println("正在注入字体信息到页面...");
|
||||
browser.executeJavaScript(script, browser.getURL(), 0);
|
||||
|
||||
// 添加调试信息
|
||||
browser.executeJavaScript(
|
||||
"console.log('Font information injection is complete,window.javaFontInfo:', typeof window.javaFontInfo);" +
|
||||
"console.log('Number of event listeners:', document.eventListeners ? document.eventListeners('javaFontsLoaded') : '无法获取');",
|
||||
browser.getURL(), 0
|
||||
);
|
||||
public 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user