feat(browser): 添加 BrowserWindow 和 BrowserWindowJDialog 类以支持嵌入式浏览器功能
- 新增 BrowserWindow 类,支持通过 Builder 模式创建可定制的浏览器窗口 - 新增 BrowserWindowJDialog 类,继承自 JDialog,用于创建模态或非模态浏览器对话框 - 实现基于 CEF 的浏览器组件加载与生命周期管理 - 支持自定义上下文菜单、键盘事件(如 F12 开发者工具)、JS 对话框拦截 - 提供链接打开方式配置(在当前窗口或外部浏览器中打开) - 集成消息路由机制,支持前端与后端通信 - 支持主题与字体信息注入至网页端 - 添加资源自动释放逻辑,防止内存泄漏 - 增加对粘贴板操作的支持(复制/粘贴文本)
This commit is contained in:
821
src/main/java/com/chuangzhou/vivid2D/browser/BrowserWindow.java
Normal file
821
src/main/java/com/chuangzhou/vivid2D/browser/BrowserWindow.java
Normal file
@@ -0,0 +1,821 @@
|
||||
package com.chuangzhou.vivid2D.browser;
|
||||
|
||||
import com.axis.innovators.box.AxisInnovatorsBox;
|
||||
import com.axis.innovators.box.events.BrowserCreationCallback;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import org.cef.CefApp;
|
||||
import org.cef.CefClient;
|
||||
import org.cef.CefSettings;
|
||||
import org.cef.browser.CefBrowser;
|
||||
import org.cef.browser.CefFrame;
|
||||
import org.cef.browser.CefMessageRouter;
|
||||
import org.cef.callback.CefContextMenuParams;
|
||||
import org.cef.callback.CefJSDialogCallback;
|
||||
import org.cef.callback.CefMenuModel;
|
||||
import org.cef.callback.CefQueryCallback;
|
||||
import org.cef.handler.*;
|
||||
import org.cef.misc.BoolRef;
|
||||
import org.cef.network.CefRequest;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.Clipboard;
|
||||
import java.awt.datatransfer.DataFlavor;
|
||||
import java.awt.datatransfer.UnsupportedFlavorException;
|
||||
import java.awt.event.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.cef.callback.CefMenuModel.MenuId.MENU_ID_USER_FIRST;
|
||||
|
||||
/**
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class BrowserWindow extends JFrame {
|
||||
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 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路径
|
||||
*/
|
||||
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());
|
||||
}
|
||||
}
|
||||
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 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);
|
||||
}
|
||||
|
||||
// 初始化浏览器组件
|
||||
try {
|
||||
this.browserComponent = initializeCef(builder);
|
||||
if (operationHandler != null) {
|
||||
setupMessageHandlers(operationHandler);
|
||||
}
|
||||
} 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) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTitleChange(CefBrowser browser, String title) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void OnFullscreenModeChange(CefBrowser browser, boolean fullscreen) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTooltip(CefBrowser browser, String text) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusMessage(CefBrowser browser, String value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onConsoleMessage(
|
||||
CefBrowser browser,
|
||||
CefSettings.LogSeverity level,
|
||||
String message,
|
||||
String source,
|
||||
int line
|
||||
) {
|
||||
// 格式化输出到 Java 控制台
|
||||
//if (level != CefSettings.LogSeverity.LOGSEVERITY_WARNING) {
|
||||
String log = String.format(
|
||||
"[Browser Console] %s %s (Line %d) -> %s",
|
||||
getLogLevelSymbol(level),
|
||||
source,
|
||||
line,
|
||||
message
|
||||
);
|
||||
System.out.println(log);
|
||||
//}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCursorChange(CefBrowser browser, int cursorType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
private String getLogLevelSymbol(CefSettings.LogSeverity level) {
|
||||
switch (level) {
|
||||
case LOGSEVERITY_ERROR: return "⛔";
|
||||
case LOGSEVERITY_WARNING: return "⚠️";
|
||||
case LOGSEVERITY_DEFAULT: return "🐞";
|
||||
default: return "ℹ️";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (AxisInnovatorsBox.getMain() != null && AxisInnovatorsBox.getMain().isDebugEnvironment()) {
|
||||
client.addKeyboardHandler(new CefKeyboardHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onKeyEvent(CefBrowser browser, CefKeyEvent event) {
|
||||
// 检测 F12
|
||||
if (event.windows_key_code == 123) {
|
||||
browser.getDevTools().createImmediately();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforePopup(CefBrowser browser, CefFrame frame,
|
||||
String targetUrl, String targetFrameName) {
|
||||
// 处理弹出窗口:根据配置决定打开方式
|
||||
if (builder.openLinksInExternalBrowser) {
|
||||
// 使用默认浏览器打开
|
||||
try {
|
||||
Desktop.getDesktop().browse(new URI(targetUrl));
|
||||
} catch (Exception e) {
|
||||
System.out.println("Failed to open external browser: " + e.getMessage());
|
||||
}
|
||||
return true; // 拦截弹窗
|
||||
} else {
|
||||
// 在当前浏览器中打开
|
||||
browser.loadURL(targetUrl);
|
||||
return true; // 拦截弹窗并在当前窗口打开
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
client.addRequestHandler(new CefRequestHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforeBrowse(CefBrowser browser, CefFrame frame,
|
||||
CefRequest request, boolean userGesture, boolean isRedirect) {
|
||||
// 处理主窗口导航
|
||||
if (userGesture) {
|
||||
if (builder.openLinksInExternalBrowser) {
|
||||
// 使用默认浏览器打开
|
||||
try {
|
||||
Desktop.getDesktop().browse(new URI(request.getURL()));
|
||||
return true; // 取消内置浏览器导航
|
||||
} catch (Exception e) {
|
||||
System.out.println("Failed to open external browser: " + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
// 允许在当前浏览器中打开
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
client.addContextMenuHandler(new CefContextMenuHandlerAdapter() {
|
||||
@Override
|
||||
public void onBeforeContextMenu(CefBrowser browser, CefFrame frame,
|
||||
CefContextMenuParams params, CefMenuModel model) {
|
||||
model.clear();
|
||||
if (!params.getSelectionText().isEmpty() || params.isEditable()) {
|
||||
model.addItem(MENU_ID_USER_FIRST, "复制");
|
||||
}
|
||||
|
||||
if (params.isEditable()) {
|
||||
model.addItem(MENU_ID_USER_FIRST + 1, "粘贴");
|
||||
model.addItem(MENU_ID_USER_FIRST + 2, "粘贴纯文本");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextMenuCommand(CefBrowser browser, CefFrame frame,
|
||||
CefContextMenuParams params, int commandId, int eventFlags) {
|
||||
if (commandId == MENU_ID_USER_FIRST) {
|
||||
if (params.isEditable()) {
|
||||
browser.executeJavaScript("document.execCommand('copy');", browser.getURL(), 0);
|
||||
} else {
|
||||
browser.executeJavaScript(
|
||||
"window.getSelection().toString();",
|
||||
browser.getURL(),
|
||||
0
|
||||
);
|
||||
}
|
||||
return true;
|
||||
} else if (commandId == MENU_ID_USER_FIRST + 1) {
|
||||
pasteContent(browser, false);
|
||||
return true;
|
||||
} else if (commandId == MENU_ID_USER_FIRST + 2) {
|
||||
pasteContent(browser, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理粘贴操作
|
||||
* @param plainText 是否去除格式(纯文本模式)
|
||||
*/
|
||||
private void pasteContent(CefBrowser browser, boolean plainText) {
|
||||
try {
|
||||
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
|
||||
if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
|
||||
String text = (String) clipboard.getData(DataFlavor.stringFlavor);
|
||||
|
||||
if (plainText) {
|
||||
text = text.replaceAll("<[^>]+>", "");
|
||||
}
|
||||
|
||||
String escapedText = text
|
||||
.replace("\\", "\\\\")
|
||||
.replace("'", "\\'")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r");
|
||||
|
||||
String script = String.format(
|
||||
"if (document.activeElement) {\n" +
|
||||
" document.activeElement.value += '%s';\n" + // 简单追加文本
|
||||
" document.dispatchEvent(new Event('input', { bubbles: true }));\n" + // 触发输入事件
|
||||
"}",
|
||||
escapedText
|
||||
);
|
||||
browser.executeJavaScript(script, browser.getURL(), 0);
|
||||
}
|
||||
} catch (UnsupportedFlavorException | IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
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 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新主题
|
||||
*/
|
||||
public void updateTheme() {
|
||||
// 1. 获取Java字体信息
|
||||
String fontInfo = getSystemFontsInfo();
|
||||
boolean isDarkTheme = AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode();
|
||||
injectFontInfoToPage(browser, fontInfo, isDarkTheme);
|
||||
|
||||
// 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() {
|
||||
@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 static void printStackTrace() {
|
||||
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||||
for (int i = 2; i < stackTrace.length; i++) {
|
||||
StackTraceElement element = stackTrace[i];
|
||||
System.out.println(element.getClassName() + "." + element.getMethodName() +
|
||||
"(" + (element.getFileName() != null ? element.getFileName() : "Unknown Source") +
|
||||
":" + element.getLineNumber() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisible(boolean b) {
|
||||
if (b) {
|
||||
if (browser != null) {
|
||||
updateTheme();
|
||||
}
|
||||
}
|
||||
super.setVisible(b);
|
||||
}
|
||||
|
||||
public Component getBrowserComponent() {
|
||||
return browserComponent;
|
||||
}
|
||||
|
||||
private void setupMessageHandlers(WindowOperationHandler handler) {
|
||||
if (client != null) {
|
||||
msgRouter = CefMessageRouter.create();
|
||||
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onQuery(CefBrowser browser,
|
||||
CefFrame frame,
|
||||
long queryId,
|
||||
String request,
|
||||
boolean persistent,
|
||||
CefQueryCallback callback) {
|
||||
if (request.startsWith("system:")) {
|
||||
String[] parts = request.split(":");
|
||||
String operation = parts.length >= 2 ? parts[1] : null;
|
||||
String targetWindow = parts.length > 2 ? parts[2] : null;
|
||||
handler.handleOperation(
|
||||
new WindowOperation(operation, targetWindow, callback) // [!code ++]
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if (request.startsWith("java-response:")) {
|
||||
String[] parts = request.split(":");
|
||||
String requestId = parts[1];
|
||||
String responseData = parts.length > 2 ? parts[2] : "";
|
||||
Consumer<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 消息路由器
|
||||
*/
|
||||
public CefMessageRouter getMsgRouter() {
|
||||
return msgRouter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取浏览器对象
|
||||
* @return 浏览器对象
|
||||
*/
|
||||
public CefBrowser getBrowser() {
|
||||
return browser;
|
||||
}
|
||||
|
||||
public void closeWindow() {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (browser != null) {
|
||||
browser.close(true);
|
||||
}
|
||||
dispose();
|
||||
cefApp.dispose();
|
||||
WindowRegistry.getInstance().unregisterWindow(windowId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,833 @@
|
||||
package com.chuangzhou.vivid2D.browser;
|
||||
|
||||
import com.axis.innovators.box.AxisInnovatorsBox;
|
||||
import com.axis.innovators.box.events.BrowserCreationCallback;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import org.cef.CefApp;
|
||||
import org.cef.CefClient;
|
||||
import org.cef.CefSettings;
|
||||
import org.cef.browser.CefBrowser;
|
||||
import org.cef.browser.CefFrame;
|
||||
import org.cef.browser.CefMessageRouter;
|
||||
import org.cef.callback.CefContextMenuParams;
|
||||
import org.cef.callback.CefJSDialogCallback;
|
||||
import org.cef.callback.CefMenuModel;
|
||||
import org.cef.callback.CefQueryCallback;
|
||||
import org.cef.handler.*;
|
||||
import org.cef.misc.BoolRef;
|
||||
import org.cef.network.CefRequest;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.Clipboard;
|
||||
import java.awt.datatransfer.DataFlavor;
|
||||
import java.awt.datatransfer.UnsupportedFlavorException;
|
||||
import java.awt.event.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.cef.callback.CefMenuModel.MenuId.MENU_ID_USER_FIRST;
|
||||
|
||||
/**
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class BrowserWindowJDialog extends JDialog {
|
||||
private final String windowId;
|
||||
private final String htmlUrl;
|
||||
private CefApp cefApp;
|
||||
private CefClient client;
|
||||
private CefBrowser browser;
|
||||
private final Component browserComponent;
|
||||
private final String htmlPath;
|
||||
//private boolean isInitialized = false;
|
||||
private WindowOperationHandler operationHandler;
|
||||
private static Thread cefThread;
|
||||
private CefMessageRouter msgRouter;
|
||||
|
||||
public static class Builder {
|
||||
private String windowId;
|
||||
private String title = "JCEF Window";
|
||||
private Dimension size = new Dimension(800, 600);
|
||||
private WindowOperationHandler operationHandler;
|
||||
private String htmlPath;
|
||||
private Image icon;
|
||||
private JFrame parentFrame;
|
||||
private boolean resizable = true; // 默认允许调整大小
|
||||
private boolean maximizable = true; // 默认允许最大化
|
||||
private boolean minimizable = true; // 默认允许最小化
|
||||
private String htmlUrl = "";
|
||||
private BrowserCreationCallback browserCreationCallback;
|
||||
private boolean openLinksInExternalBrowser = true; // 默认使用外部浏览器
|
||||
|
||||
public Builder resizable(boolean resizable) {
|
||||
this.resizable = resizable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder maximizable(boolean maximizable) {
|
||||
this.maximizable = maximizable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder minimizable(boolean minimizable) {
|
||||
this.minimizable = minimizable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder(String windowId) {
|
||||
this.windowId = windowId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器窗口标题
|
||||
* @param title 标题
|
||||
*/
|
||||
public Builder title(String title) {
|
||||
this.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置链接打开方式
|
||||
*
|
||||
* @param openInBrowser 是否在当前浏览器窗口中打开链接
|
||||
* true - 在当前浏览器窗口中打开链接(本地跳转)
|
||||
* false - 使用系统默认浏览器打开链接(外部跳转)
|
||||
* @return Builder实例,支持链式调用
|
||||
*
|
||||
* @apiNote 此方法控制两种不同的链接打开行为:
|
||||
* 1. 当设置为true时:
|
||||
* - 所有链接将在当前CEF浏览器窗口内打开
|
||||
*
|
||||
* 2. 当设置为false时(默认值):
|
||||
* - 所有链接将在系统默认浏览器中打开
|
||||
* - 更安全,避免潜在的安全风险
|
||||
* - 适用于简单的信息展示场景
|
||||
*
|
||||
* @implNote 内部实现说明:
|
||||
* - 实际存储的是反向值(openLinksInExternalBrowser)
|
||||
* - 这样设置是为了保持与历史版本的兼容性
|
||||
* - 方法名使用"openInBrowser"更符合用户直觉
|
||||
*
|
||||
* @example 使用示例:
|
||||
* // 在当前窗口打开链接
|
||||
* new Builder().openLinksInBrowser(true).build();
|
||||
*
|
||||
* // 使用系统浏览器打开链接(默认)
|
||||
* new Builder().openLinksInBrowser(false).build();
|
||||
*
|
||||
* @see #openLinksInExternalBrowser 内部存储字段
|
||||
* @see CefLifeSpanHandler#onBeforePopup 弹窗处理实现
|
||||
* @see CefRequestHandler#onBeforeBrowse 导航处理实现
|
||||
*/
|
||||
public Builder openLinksInBrowser(boolean openInBrowser) {
|
||||
this.openLinksInExternalBrowser = !openInBrowser;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置浏览器创建回调
|
||||
* @param callback 回调
|
||||
*/
|
||||
public Builder setBrowserCreationCallback(BrowserCreationCallback callback){
|
||||
this.browserCreationCallback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器窗口大小
|
||||
* @param width 宽度
|
||||
* @param height 高度
|
||||
*/
|
||||
public Builder size(int width, int height) {
|
||||
this.size = new Dimension(width, height);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器窗口父窗口
|
||||
* @param parent 父窗口
|
||||
*/
|
||||
public Builder parentFrame(JFrame parent) {
|
||||
this.parentFrame = parent;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置浏览器触发事件
|
||||
* @param handler 事件处理器
|
||||
*/
|
||||
public Builder operationHandler(WindowOperationHandler handler) {
|
||||
this.operationHandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器图标
|
||||
* @param icon 图标
|
||||
*/
|
||||
public Builder icon(Image icon) {
|
||||
this.icon = icon;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置HTML路径
|
||||
*/
|
||||
public BrowserWindowJDialog build() {
|
||||
if (htmlUrl.isEmpty()) {
|
||||
if (this.htmlPath == null || this.htmlPath.isEmpty()) {
|
||||
throw new IllegalArgumentException("HTML paths cannot be empty");
|
||||
}
|
||||
File htmlFile = new File(this.htmlPath);
|
||||
if (!htmlFile.exists()) {
|
||||
throw new RuntimeException("The HTML file does not exist: " + htmlFile.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
return new BrowserWindowJDialog(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置HTML路径
|
||||
* @param path HTML路径
|
||||
*/
|
||||
public Builder htmlPath(String path) {
|
||||
this.htmlPath = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用Url
|
||||
* @param htmlUrl Url路径
|
||||
*/
|
||||
public Builder htmlUrl(String htmlUrl) {
|
||||
this.htmlUrl = htmlUrl;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private BrowserWindowJDialog(Builder builder) {
|
||||
// 根据父窗口是否存在,设置是否为模态对话框
|
||||
super(builder.parentFrame, builder.title, builder.parentFrame != null);
|
||||
this.windowId = builder.windowId;
|
||||
this.htmlPath = builder.htmlPath;
|
||||
this.htmlUrl = builder.htmlUrl;
|
||||
this.operationHandler = builder.operationHandler;
|
||||
|
||||
// 设置图标(如果存在)
|
||||
if (builder.icon != null) {
|
||||
setIconImage(builder.icon);
|
||||
}
|
||||
|
||||
// 初始化浏览器组件
|
||||
try {
|
||||
this.browserComponent = initializeCef(builder);
|
||||
if (operationHandler != null) {
|
||||
setupMessageHandlers(operationHandler);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
JOptionPane.showMessageDialog(this, "初始化失败: " + e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
private static boolean isInitialized = false;
|
||||
private Component initializeCef(Builder builder) throws MalformedURLException {
|
||||
if (!isInitialized) {
|
||||
isInitialized = true;
|
||||
try {
|
||||
this.cefApp = CefAppManager.getInstance();
|
||||
//CefAppManager.incrementBrowserCount();
|
||||
client = cefApp.createClient();
|
||||
client.addDisplayHandler(new CefDisplayHandler (){
|
||||
@Override
|
||||
public void onAddressChange(CefBrowser browser, CefFrame frame, String url) {}
|
||||
|
||||
@Override
|
||||
public void onTitleChange(CefBrowser browser, String title) {}
|
||||
|
||||
@Override
|
||||
public void OnFullscreenModeChange(CefBrowser browser, boolean fullscreen) {}
|
||||
|
||||
@Override
|
||||
public boolean onTooltip(CefBrowser browser, String text) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusMessage(CefBrowser browser, String value) {}
|
||||
|
||||
@Override
|
||||
public boolean onConsoleMessage(
|
||||
CefBrowser browser,
|
||||
CefSettings.LogSeverity level,
|
||||
String message,
|
||||
String source,
|
||||
int line
|
||||
) {
|
||||
// 格式化输出到 Java 控制台
|
||||
//if (level != CefSettings.LogSeverity.LOGSEVERITY_WARNING) {
|
||||
String log = String.format(
|
||||
"[Browser Console] %s %s (Line %d) -> %s",
|
||||
getLogLevelSymbol(level),
|
||||
source,
|
||||
line,
|
||||
message
|
||||
);
|
||||
System.out.println(log);
|
||||
//}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCursorChange(CefBrowser browser, int cursorType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
private String getLogLevelSymbol(CefSettings.LogSeverity level) {
|
||||
switch (level) {
|
||||
case LOGSEVERITY_ERROR: return "⛔";
|
||||
case LOGSEVERITY_WARNING: return "⚠️";
|
||||
case LOGSEVERITY_DEFAULT: return "🐞";
|
||||
default: return "ℹ️";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (AxisInnovatorsBox.getMain() != null && AxisInnovatorsBox.getMain().isDebugEnvironment()) {
|
||||
client.addKeyboardHandler(new CefKeyboardHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onKeyEvent(CefBrowser browser, CefKeyEvent event) {
|
||||
// 检测 F12
|
||||
if (event.windows_key_code == 123) {
|
||||
browser.getDevTools().createImmediately();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforePopup(CefBrowser browser, CefFrame frame,
|
||||
String targetUrl, String targetFrameName) {
|
||||
// 处理弹出窗口:根据配置决定打开方式
|
||||
if (builder.openLinksInExternalBrowser) {
|
||||
// 使用默认浏览器打开
|
||||
try {
|
||||
Desktop.getDesktop().browse(new URI(targetUrl));
|
||||
} catch (Exception e) {
|
||||
System.out.println("Failed to open external browser: " + e.getMessage());
|
||||
}
|
||||
return true; // 拦截弹窗
|
||||
} else {
|
||||
// 在当前浏览器中打开
|
||||
browser.loadURL(targetUrl);
|
||||
return true; // 拦截弹窗并在当前窗口打开
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
client.addRequestHandler(new CefRequestHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforeBrowse(CefBrowser browser, CefFrame frame,
|
||||
CefRequest request, boolean userGesture, boolean isRedirect) {
|
||||
// 处理主窗口导航
|
||||
if (userGesture) {
|
||||
if (builder.openLinksInExternalBrowser) {
|
||||
// 使用默认浏览器打开
|
||||
try {
|
||||
Desktop.getDesktop().browse(new URI(request.getURL()));
|
||||
return true; // 取消内置浏览器导航
|
||||
} catch (Exception e) {
|
||||
System.out.println("Failed to open external browser: " + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
// 允许在当前浏览器中打开
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
client.addContextMenuHandler(new CefContextMenuHandlerAdapter() {
|
||||
@Override
|
||||
public void onBeforeContextMenu(CefBrowser browser, CefFrame frame,
|
||||
CefContextMenuParams params, CefMenuModel model) {
|
||||
model.clear();
|
||||
if (!params.getSelectionText().isEmpty() || params.isEditable()) {
|
||||
model.addItem(MENU_ID_USER_FIRST, "复制");
|
||||
}
|
||||
|
||||
if (params.isEditable()) {
|
||||
model.addItem(MENU_ID_USER_FIRST + 1, "粘贴");
|
||||
model.addItem(MENU_ID_USER_FIRST + 2, "粘贴纯文本");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextMenuCommand(CefBrowser browser, CefFrame frame,
|
||||
CefContextMenuParams params, int commandId, int eventFlags) {
|
||||
if (commandId == MENU_ID_USER_FIRST) {
|
||||
if (params.isEditable()) {
|
||||
browser.executeJavaScript("document.execCommand('copy');", browser.getURL(), 0);
|
||||
} else {
|
||||
browser.executeJavaScript(
|
||||
"window.getSelection().toString();",
|
||||
browser.getURL(),
|
||||
0
|
||||
);
|
||||
}
|
||||
return true;
|
||||
} else if (commandId == MENU_ID_USER_FIRST + 1) {
|
||||
pasteContent(browser, false);
|
||||
return true;
|
||||
} else if (commandId == MENU_ID_USER_FIRST + 2) {
|
||||
pasteContent(browser, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理粘贴操作
|
||||
* @param plainText 是否去除格式(纯文本模式)
|
||||
*/
|
||||
private void pasteContent(CefBrowser browser, boolean plainText) {
|
||||
try {
|
||||
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
|
||||
if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
|
||||
String text = (String) clipboard.getData(DataFlavor.stringFlavor);
|
||||
|
||||
if (plainText) {
|
||||
text = text.replaceAll("<[^>]+>", "");
|
||||
}
|
||||
|
||||
String escapedText = text
|
||||
.replace("\\", "\\\\")
|
||||
.replace("'", "\\'")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r");
|
||||
|
||||
String script = String.format(
|
||||
"if (document.activeElement) {\n" +
|
||||
" document.activeElement.value += '%s';\n" + // 简单追加文本
|
||||
" document.dispatchEvent(new Event('input', { bubbles: true }));\n" + // 触发输入事件
|
||||
"}",
|
||||
escapedText
|
||||
);
|
||||
browser.executeJavaScript(script, browser.getURL(), 0);
|
||||
}
|
||||
} catch (UnsupportedFlavorException | IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 添加 alert 弹窗监控处理
|
||||
client.addJSDialogHandler(new CefJSDialogHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onJSDialog(CefBrowser browser, String origin_url, JSDialogType dialog_type, String message_text, String default_prompt_text, CefJSDialogCallback callback, BoolRef suppress_message) {
|
||||
if (dialog_type == JSDialogType.JSDIALOGTYPE_ALERT) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
JOptionPane.showMessageDialog(
|
||||
BrowserWindowJDialog.this,
|
||||
message_text,
|
||||
"警告",
|
||||
JOptionPane.INFORMATION_MESSAGE
|
||||
);
|
||||
});
|
||||
callback.Continue(true, "");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 3. 拦截所有新窗口(关键修复点!)
|
||||
//client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
|
||||
// @Override
|
||||
// public boolean onBeforePopup(CefBrowser browser,
|
||||
// CefFrame frame, String target_url, String target_frame_name) {
|
||||
// return true; // 返回true表示拦截弹窗
|
||||
// }
|
||||
//});
|
||||
|
||||
|
||||
Thread.currentThread().setName("BrowserRenderThread");
|
||||
|
||||
|
||||
// 4. 加载HTML
|
||||
if (htmlUrl.isEmpty()) {
|
||||
String fileUrl = new File(htmlPath).toURI().toURL().toString();
|
||||
System.out.println("Loading HTML from: " + fileUrl);
|
||||
|
||||
// 5. 创建浏览器组件(直接添加到内容面板)
|
||||
browser = client.createBrowser(fileUrl, false, false);
|
||||
} else {
|
||||
System.out.println("Loading HTML from: " + htmlUrl);
|
||||
browser = client.createBrowser(htmlUrl, false, false);
|
||||
}
|
||||
|
||||
Component browserComponent = browser.getUIComponent();
|
||||
|
||||
if (builder.browserCreationCallback != null) {
|
||||
boolean handled = builder.browserCreationCallback.onLayoutCustomization(
|
||||
this, // 当前窗口
|
||||
getContentPane(), // 内容面板
|
||||
browserComponent, // 浏览器组件
|
||||
builder // 构建器对象
|
||||
);
|
||||
|
||||
// 如果回调返回true,跳过默认布局
|
||||
if (handled) {
|
||||
// 设置窗口基本属性
|
||||
setTitle(builder.title);
|
||||
setSize(builder.size);
|
||||
setLocationRelativeTo(builder.parentFrame);
|
||||
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
||||
|
||||
// 添加资源释放监听器
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent e) {
|
||||
browser.close(true);
|
||||
client.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
setVisible(true);
|
||||
return browserComponent; // 直接返回,跳过默认布局
|
||||
}
|
||||
}
|
||||
|
||||
updateTheme();
|
||||
|
||||
CefMessageRouter.CefMessageRouterConfig config = new CefMessageRouter.CefMessageRouterConfig();
|
||||
config.jsQueryFunction = "javaQuery";// 定义方法
|
||||
config.jsCancelFunction = "javaQueryCancel";// 定义取消方法
|
||||
|
||||
|
||||
// 6. 配置窗口布局(确保只添加一次)
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
getContentPane().removeAll();
|
||||
getContentPane().setLayout(new BorderLayout());
|
||||
|
||||
// 透明拖拽层(仅顶部可拖拽)
|
||||
JPanel dragPanel = new JPanel(new BorderLayout());
|
||||
dragPanel.setOpaque(false);
|
||||
|
||||
JPanel titleBar = new JPanel();
|
||||
titleBar.setOpaque(false);
|
||||
titleBar.setPreferredSize(new Dimension(builder.size.width, 20));
|
||||
final Point[] dragStart = new Point[1];
|
||||
titleBar.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
dragStart[0] = e.getPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
dragStart[0] = null;
|
||||
}
|
||||
});
|
||||
titleBar.addMouseMotionListener(new MouseMotionAdapter() {
|
||||
@Override
|
||||
public void mouseDragged(MouseEvent e) {
|
||||
if (dragStart[0] != null) {
|
||||
Point curr = e.getLocationOnScreen();
|
||||
setLocation(curr.x - dragStart[0].x, curr.y - dragStart[0].y);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
dragPanel.add(titleBar, BorderLayout.NORTH);
|
||||
getContentPane().add(dragPanel, BorderLayout.CENTER);
|
||||
getContentPane().add(browserComponent, BorderLayout.CENTER);
|
||||
|
||||
// 7. 窗口属性设置
|
||||
setTitle(builder.title);
|
||||
setSize(builder.size);
|
||||
setLocationRelativeTo(builder.parentFrame);
|
||||
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
||||
|
||||
// 8. 资源释放
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent e) {
|
||||
browser.close(true);
|
||||
client.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
setVisible(true);
|
||||
|
||||
});
|
||||
return browserComponent;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
JOptionPane.showMessageDialog(null, "初始化失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
} else {
|
||||
isInitialized = false;
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
dispose();
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 更新主题
|
||||
*/
|
||||
public void updateTheme() {
|
||||
// 1. 获取Java字体信息
|
||||
String fontInfo = getSystemFontsInfo();
|
||||
injectFontInfoToPage(browser, fontInfo);
|
||||
|
||||
// 2. 注入主题信息
|
||||
if (AxisInnovatorsBox.getMain() != null) {
|
||||
boolean isDarkTheme = AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode();
|
||||
injectThemeInfoToPage(browser, isDarkTheme);
|
||||
}
|
||||
|
||||
//// 3. 刷新浏览器
|
||||
//SwingUtilities.invokeLater(() -> {
|
||||
// browser.reload();
|
||||
//});
|
||||
|
||||
}
|
||||
private void injectThemeInfoToPage(CefBrowser browser, boolean isDarkTheme) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.addLoadHandler(new CefLoadHandlerAdapter() {
|
||||
@Override
|
||||
public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
|
||||
String themeInfo = String.format(
|
||||
"{\"isDarkTheme\": %s, \"timestamp\": %d}",
|
||||
isDarkTheme,
|
||||
System.currentTimeMillis()
|
||||
);
|
||||
|
||||
String script =
|
||||
"window.javaThemeInfo = " + themeInfo + ";\n" +
|
||||
"console.log('Java theme information has been loaded:', window.javaThemeInfo);\n" +
|
||||
"\n" +
|
||||
"if (typeof applyJavaTheme === 'function') {\n" +
|
||||
" applyJavaTheme(window.javaThemeInfo);\n" +
|
||||
"}\n" +
|
||||
"\n" +
|
||||
"var event = new CustomEvent('javaThemeChanged', {\n" +
|
||||
" detail: window.javaThemeInfo\n" +
|
||||
"});\n" +
|
||||
"document.dispatchEvent(event);\n" +
|
||||
"console.log('The javaThemeChanged event is dispatched');";
|
||||
|
||||
browser.executeJavaScript(script, browser.getURL(), 0);
|
||||
|
||||
browser.executeJavaScript(
|
||||
"console.log('Theme information injection is complete,window.javaThemeInfo:', typeof window.javaThemeInfo);" +
|
||||
"console.log('Number of theme event listeners:', document.eventListeners ? document.eventListeners('javaThemeChanged') : '无法获取');",
|
||||
browser.getURL(), 0
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取Java字体信息(从UIManager获取)
|
||||
*/
|
||||
private String getSystemFontsInfo() {
|
||||
try {
|
||||
Gson gson = new Gson();
|
||||
JsonObject fontInfo = new JsonObject();
|
||||
JsonObject uiFonts = new JsonObject();
|
||||
|
||||
String[] fontKeys = {
|
||||
"Label.font", "Button.font", "ToggleButton.font", "RadioButton.font",
|
||||
"CheckBox.font", "ColorChooser.font", "ComboBox.font", "EditorPane.font",
|
||||
"TextArea.font", "TextField.font", "PasswordField.font", "TextPane.font",
|
||||
"FormattedTextField.font", "Table.font", "TableHeader.font", "List.font",
|
||||
"Tree.font", "TabbedPane.font", "MenuBar.font", "Menu.font", "MenuItem.font",
|
||||
"PopupMenu.font", "CheckBoxMenuItem.font", "RadioButtonMenuItem.font",
|
||||
"Spinner.font", "ToolBar.font", "TitledBorder.font", "OptionPane.messageFont",
|
||||
"OptionPane.buttonFont", "Panel.font", "Viewport.font", "ToolTip.font"
|
||||
};
|
||||
|
||||
for (String key : fontKeys) {
|
||||
Font font = UIManager.getFont(key);
|
||||
if (font != null) {
|
||||
JsonObject fontObj = new JsonObject();
|
||||
fontObj.addProperty("name", font.getFontName());
|
||||
fontObj.addProperty("family", font.getFamily());
|
||||
fontObj.addProperty("size", font.getSize());
|
||||
fontObj.addProperty("style", font.getStyle());
|
||||
fontObj.addProperty("bold", font.isBold());
|
||||
fontObj.addProperty("italic", font.isItalic());
|
||||
fontObj.addProperty("plain", font.isPlain());
|
||||
uiFonts.add(key, fontObj);
|
||||
}
|
||||
}
|
||||
|
||||
fontInfo.add("uiFonts", uiFonts);
|
||||
fontInfo.addProperty("timestamp", System.currentTimeMillis());
|
||||
fontInfo.addProperty("lookAndFeel", UIManager.getLookAndFeel().getName());
|
||||
|
||||
return gson.toJson(fontInfo);
|
||||
} catch (Exception e) {
|
||||
return "{\"error\": \"无法获取UIManager字体信息: " + e.getMessage() + "\"}";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入字体信息到页面并设置字体
|
||||
*/
|
||||
private void injectFontInfoToPage(CefBrowser browser, String fontInfo) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
client.addLoadHandler(new CefLoadHandlerAdapter() {
|
||||
@Override
|
||||
public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
|
||||
// 使用更简单的脚本来注入字体信息
|
||||
String script =
|
||||
"if (typeof window.javaFontInfo === 'undefined') {" +
|
||||
" window.javaFontInfo = " + fontInfo + ";" +
|
||||
" console.log('Java font information has been loaded:', window.javaFontInfo);" +
|
||||
" " +
|
||||
" var event = new CustomEvent('javaFontsLoaded', {" +
|
||||
" detail: window.javaFontInfo" +
|
||||
" });" +
|
||||
" document.dispatchEvent(event);" +
|
||||
" console.log('The javaFontsLoaded event is dispatched');" +
|
||||
"}";
|
||||
|
||||
System.out.println("正在注入字体信息到页面...");
|
||||
browser.executeJavaScript(script, browser.getURL(), 0);
|
||||
|
||||
// 添加调试信息
|
||||
browser.executeJavaScript(
|
||||
"console.log('Font information injection is complete,window.javaFontInfo:', typeof window.javaFontInfo);" +
|
||||
"console.log('Number of event listeners:', document.eventListeners ? document.eventListeners('javaFontsLoaded') : '无法获取');",
|
||||
browser.getURL(), 0
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public static void printStackTrace() {
|
||||
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||||
for (int i = 2; i < stackTrace.length; i++) {
|
||||
StackTraceElement element = stackTrace[i];
|
||||
System.out.println(element.getClassName() + "." + element.getMethodName() +
|
||||
"(" + (element.getFileName() != null ? element.getFileName() : "Unknown Source") +
|
||||
":" + element.getLineNumber() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisible(boolean b) {
|
||||
if (b) {
|
||||
if (browser != null) {
|
||||
updateTheme();
|
||||
}
|
||||
}
|
||||
super.setVisible(b);
|
||||
}
|
||||
|
||||
public Component getBrowserComponent() {
|
||||
return browserComponent;
|
||||
}
|
||||
|
||||
private void setupMessageHandlers(WindowOperationHandler handler) {
|
||||
if (client != null) {
|
||||
msgRouter = CefMessageRouter.create();
|
||||
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onQuery(CefBrowser browser,
|
||||
CefFrame frame,
|
||||
long queryId,
|
||||
String request,
|
||||
boolean persistent,
|
||||
CefQueryCallback callback) {
|
||||
if (request.startsWith("system:")) {
|
||||
String[] parts = request.split(":");
|
||||
String operation = parts.length >= 2 ? parts[1] : null;
|
||||
String targetWindow = parts.length > 2 ? parts[2] : null;
|
||||
handler.handleOperation(
|
||||
new WindowOperation(operation, targetWindow, callback) // [!code ++]
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if (request.startsWith("java-response:")) {
|
||||
String[] parts = request.split(":");
|
||||
String requestId = parts[1];
|
||||
String responseData = parts.length > 2 ? parts[2] : "";
|
||||
Consumer<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 消息路由器
|
||||
*/
|
||||
public CefMessageRouter getMsgRouter() {
|
||||
return msgRouter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取浏览器对象
|
||||
* @return 浏览器对象
|
||||
*/
|
||||
public CefBrowser getBrowser() {
|
||||
return browser;
|
||||
}
|
||||
|
||||
public void closeWindow() {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (browser != null) {
|
||||
browser.close(true);
|
||||
}
|
||||
dispose();
|
||||
cefApp.dispose();
|
||||
WindowRegistry.getInstance().unregisterWindow(windowId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
280
src/main/java/com/chuangzhou/vivid2D/browser/CefAppManager.java
Normal file
280
src/main/java/com/chuangzhou/vivid2D/browser/CefAppManager.java
Normal file
@@ -0,0 +1,280 @@
|
||||
package com.chuangzhou.vivid2D.browser;
|
||||
|
||||
import com.axis.innovators.box.AxisInnovatorsBox;
|
||||
import com.axis.innovators.box.register.LanguageManager;
|
||||
import com.axis.innovators.box.tools.FolderCreator;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.cef.CefApp;
|
||||
import org.cef.CefSettings;
|
||||
import org.cef.callback.CefCommandLine;
|
||||
import org.cef.handler.CefAppHandlerAdapter;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* 增强版CEF应用管理器
|
||||
* 特性:
|
||||
* 1. 多级锁并发控制
|
||||
* 2. 设置冲突自动恢复
|
||||
* 3. 状态跟踪和验证
|
||||
* 4. 增强的异常处理
|
||||
*
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class CefAppManager {
|
||||
private static final Logger logger = LogManager.getLogger(CefAppManager.class);
|
||||
private static volatile CefApp cefApp;
|
||||
private static final CefSettings settings = new CefSettings();
|
||||
|
||||
// 状态跟踪
|
||||
private static final AtomicBoolean isInitialized = new AtomicBoolean(false);
|
||||
private static final AtomicBoolean settingsApplied = new AtomicBoolean(false);
|
||||
private static final AtomicBoolean isDisposing = new AtomicBoolean(false);
|
||||
|
||||
// 并发控制
|
||||
private static final Lock initLock = new ReentrantLock();
|
||||
private static final Lock disposeLock = new ReentrantLock();
|
||||
private static final AtomicBoolean shutdownHookRegistered = new AtomicBoolean(false);
|
||||
|
||||
static {
|
||||
initializeDefaultSettings();
|
||||
registerShutdownHook();
|
||||
}
|
||||
|
||||
private static void registerShutdownHook() {
|
||||
if (shutdownHookRegistered.compareAndSet(false, true)) {
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
logger.info("JVM shutdown hook triggered");
|
||||
dispose(true);
|
||||
}));
|
||||
logger.debug("Shutdown hook registered successfully");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化Cef
|
||||
*/
|
||||
private static void initializeDefaultSettings() {
|
||||
initLock.lock();
|
||||
try {
|
||||
settings.windowless_rendering_enabled = false;
|
||||
settings.javascript_flags = "";
|
||||
settings.cache_path = FolderCreator.getLibraryFolder() + "/jcef/cache";
|
||||
settings.root_cache_path = FolderCreator.getLibraryFolder() + "/jcef/cache";
|
||||
settings.persist_session_cookies = false;
|
||||
settings.log_severity = CefSettings.LogSeverity.LOGSEVERITY_WARNING;
|
||||
|
||||
String subprocessPath = FolderCreator.getLibraryFolder() + "/jcef/lib/win64/jcef_helper.exe";
|
||||
validateSubprocessPath(subprocessPath);
|
||||
settings.browser_subprocess_path = subprocessPath;
|
||||
|
||||
//settings.background_color = new Color(255, 255, 255, 0);
|
||||
settings.command_line_args_disabled = false;
|
||||
|
||||
// 转换语言标识格式:system:zh_CN -> zh-CN
|
||||
|
||||
CefApp.addAppHandler(new CefAppHandlerAdapter(null) {
|
||||
@Override
|
||||
public void onBeforeCommandLineProcessing(
|
||||
String processType,
|
||||
CefCommandLine commandLine
|
||||
) {
|
||||
//commandLine.appendSwitch("disable-dev-tools");
|
||||
//commandLine.appendSwitch("disable-view-source");
|
||||
|
||||
LanguageManager.loadSavedLanguage();
|
||||
LanguageManager.Language currentLang = LanguageManager.getLoadedLanguages();
|
||||
if (currentLang != null){
|
||||
String langCode = currentLang.getRegisteredName()
|
||||
.replace("system:", "")
|
||||
.replace("_", "-")
|
||||
.toLowerCase();
|
||||
settings.locale = langCode;
|
||||
commandLine.appendSwitchWithValue("--lang", langCode);
|
||||
commandLine.appendSwitchWithValue("--accept-language", langCode);
|
||||
}
|
||||
|
||||
boolean isDarkTheme = isDarkTheme();
|
||||
if (isDarkTheme) {
|
||||
commandLine.appendSwitch("force-dark-mode");
|
||||
commandLine.appendSwitchWithValue("enable-features", "WebContentsForceDark");
|
||||
}
|
||||
|
||||
logger.info("CEF commandLine: {}", commandLine.getSwitches());
|
||||
}
|
||||
});
|
||||
|
||||
logger.info("Optimized CEF settings initialized");
|
||||
} finally {
|
||||
initLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断地区主题是否为黑色主题
|
||||
* @return 是否
|
||||
*/
|
||||
private static boolean isDarkTheme() {
|
||||
if (AxisInnovatorsBox.getMain() == null){
|
||||
return false;
|
||||
}
|
||||
return AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode();
|
||||
}
|
||||
|
||||
public static CefApp getInstance() {
|
||||
if (cefApp == null) {
|
||||
if (initLock.tryLock()) {
|
||||
try {
|
||||
performSafeInitialization();
|
||||
} finally {
|
||||
initLock.unlock();
|
||||
}
|
||||
} else {
|
||||
handleConcurrentInitialization();
|
||||
}
|
||||
}
|
||||
return cefApp;
|
||||
}
|
||||
|
||||
private static void performSafeInitialization() {
|
||||
if (cefApp != null) return;
|
||||
if (isDisposing.get()) {
|
||||
throw new IllegalStateException("CEF is during disposal process");
|
||||
}
|
||||
|
||||
try {
|
||||
// 阶段1:启动CEF运行时
|
||||
if (!CefApp.startup(new String[0])) {
|
||||
throw new IllegalStateException("CEF native startup failed");
|
||||
}
|
||||
|
||||
// 阶段2:应用设置(仅首次)
|
||||
if (settingsApplied.compareAndSet(false, true)) {
|
||||
cefApp = CefApp.getInstance(settings);
|
||||
logger.info("CEF initialized with custom settings");
|
||||
} else {
|
||||
cefApp = CefApp.getInstance();
|
||||
logger.info("CEF reused existing instance");
|
||||
}
|
||||
|
||||
isInitialized.set(true);
|
||||
} catch (IllegalStateException ex) {
|
||||
handleInitializationError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleConcurrentInitialization() {
|
||||
try {
|
||||
if (initLock.tryLock(3, TimeUnit.SECONDS)) {
|
||||
try {
|
||||
if (cefApp == null) {
|
||||
performSafeInitialization();
|
||||
}
|
||||
} finally {
|
||||
initLock.unlock();
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
logger.warn("CEF initialization interrupted");
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleInitializationError(IllegalStateException ex) {
|
||||
if (ex.getMessage().contains("Settings can only be passed")) {
|
||||
logger.warn("Settings conflict detected, recovering...");
|
||||
recoverFromSettingsConflict();
|
||||
} else if (ex.getMessage().contains("was terminated")) {
|
||||
handleTerminatedState(ex);
|
||||
} else {
|
||||
logger.error("Critical CEF error", ex);
|
||||
throw new RuntimeException("CEF initialization failed", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void recoverFromSettingsConflict() {
|
||||
disposeLock.lock();
|
||||
try {
|
||||
if (cefApp == null) {
|
||||
cefApp = CefApp.getInstance();
|
||||
settingsApplied.set(true);
|
||||
isInitialized.set(true);
|
||||
logger.info("Recovered from settings conflict");
|
||||
}
|
||||
} finally {
|
||||
disposeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleTerminatedState(IllegalStateException ex) {
|
||||
disposeLock.lock();
|
||||
try {
|
||||
logger.warn("CEF terminated state detected");
|
||||
dispose(false);
|
||||
performEmergencyRecovery();
|
||||
} finally {
|
||||
disposeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private static void performEmergencyRecovery() {
|
||||
try {
|
||||
logger.info("Attempting emergency recovery...");
|
||||
CefApp.startup(new String[0]);
|
||||
cefApp = CefApp.getInstance();
|
||||
isInitialized.set(true);
|
||||
settingsApplied.set(true);
|
||||
logger.info("Emergency recovery successful");
|
||||
} catch (Exception e) {
|
||||
logger.error("Emergency recovery failed", e);
|
||||
throw new RuntimeException("Unrecoverable CEF state", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void dispose(boolean isShutdownHook) {
|
||||
disposeLock.lock();
|
||||
try {
|
||||
if (cefApp == null || isDisposing.get()) return;
|
||||
|
||||
isDisposing.set(true);
|
||||
try {
|
||||
logger.info("Disposing CEF resources...");
|
||||
cefApp.dispose();
|
||||
|
||||
if (!isShutdownHook) {
|
||||
cefApp = null;
|
||||
isInitialized.set(false);
|
||||
settingsApplied.set(false);
|
||||
}
|
||||
logger.info("CEF resources released");
|
||||
} catch (Exception e) {
|
||||
logger.error("Disposal error", e);
|
||||
} finally {
|
||||
isDisposing.set(false);
|
||||
}
|
||||
} finally {
|
||||
disposeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateSubprocessPath(String path) {
|
||||
File exeFile = new File(path);
|
||||
if (!exeFile.exists()) {
|
||||
String errorMsg = "JCEF helper executable missing: " + path;
|
||||
logger.error(errorMsg);
|
||||
throw new IllegalStateException(errorMsg);
|
||||
}
|
||||
logger.debug("Validated JCEF helper at: {}", path);
|
||||
}
|
||||
|
||||
// 状态查询接口
|
||||
public static String getInitStatus() {
|
||||
return String.format("Initialized: %s, SettingsApplied: %s, Disposing: %s",
|
||||
isInitialized.get(), settingsApplied.get(), isDisposing.get());
|
||||
}
|
||||
}
|
||||
2002
src/main/java/com/chuangzhou/vivid2D/browser/MainApplication.java
Normal file
2002
src/main/java/com/chuangzhou/vivid2D/browser/MainApplication.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
package com.chuangzhou.vivid2D.browser;
|
||||
|
||||
import org.cef.callback.CefQueryCallback;
|
||||
|
||||
/**
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public record WindowOperation(String type, String targetWindow, CefQueryCallback callback) {
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.chuangzhou.vivid2D.browser;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class WindowOperationHandler {
|
||||
private final WindowRegistry registry;
|
||||
private final Map<String, Consumer<String>> operations;
|
||||
private final Component attachedComponent;
|
||||
|
||||
public static class Builder {
|
||||
private WindowRegistry registry = WindowRegistry.getInstance();
|
||||
private final Map<String, Consumer<String>> operations = new ConcurrentHashMap<>();
|
||||
private Component attachedComponent;
|
||||
|
||||
public Builder attachTo(Component component) {
|
||||
this.attachedComponent = component;
|
||||
return this;
|
||||
}
|
||||
public Builder withDefaultOperations() {
|
||||
this.operations.put("open", target -> {
|
||||
registry.getWindow(target).setVisible(true);
|
||||
});
|
||||
this.operations.put("close", target -> {
|
||||
if (target != null) {
|
||||
System.out.println("Close window: " + target);
|
||||
registry.unregisterWindow(target);
|
||||
} else if (attachedComponent != null) {
|
||||
Window window = SwingUtilities.getWindowAncestor(attachedComponent);
|
||||
if (window instanceof BrowserWindow) {
|
||||
((BrowserWindow) window).closeWindow();
|
||||
}
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder onOperation(String operation, Consumer<String> handler) {
|
||||
this.operations.put(operation, handler);
|
||||
return this;
|
||||
}
|
||||
|
||||
public WindowOperationHandler build() {
|
||||
return new WindowOperationHandler(this);
|
||||
}
|
||||
|
||||
private void handleOpen(String targetWindow) {
|
||||
registry.createNewWindow(targetWindow, builder ->
|
||||
builder.title("New Window")
|
||||
);
|
||||
}
|
||||
|
||||
private void handleClose(String targetWindow) {
|
||||
if (targetWindow != null) {
|
||||
registry.unregisterWindow(targetWindow);
|
||||
} else {
|
||||
handleCurrentWindowClose();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCurrentWindowClose() {
|
||||
if (attachedComponent != null) {
|
||||
BrowserWindow currentWindow = (BrowserWindow)
|
||||
SwingUtilities.getWindowAncestor(attachedComponent);
|
||||
if (currentWindow != null) {
|
||||
registry.unregisterWindow(currentWindow.getWindowId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private WindowOperationHandler(Builder builder) {
|
||||
this.registry = builder.registry;
|
||||
this.attachedComponent = builder.attachedComponent;
|
||||
this.operations = new ConcurrentHashMap<>(builder.operations);
|
||||
}
|
||||
|
||||
public void handleOperation(WindowOperation operation) {
|
||||
Consumer<String> handler = operations.get(operation.type());
|
||||
if (handler != null) {
|
||||
handler.accept(operation.targetWindow());
|
||||
operation.callback().success("操作成功: " + operation.type());
|
||||
} else {
|
||||
operation.callback().failure(-1, "未定义的操作: " + operation.type());
|
||||
}
|
||||
}
|
||||
}
|
||||
153
src/main/java/com/chuangzhou/vivid2D/browser/WindowRegistry.java
Normal file
153
src/main/java/com/chuangzhou/vivid2D/browser/WindowRegistry.java
Normal file
@@ -0,0 +1,153 @@
|
||||
package com.chuangzhou.vivid2D.browser;
|
||||
|
||||
import com.axis.innovators.box.tools.FolderCreator;
|
||||
import org.cef.browser.CefBrowser;
|
||||
import org.cef.browser.CefFrame;
|
||||
import org.cef.handler.CefLoadHandlerAdapter;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
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<>();
|
||||
private final Map<String, Consumer<String>> callbacks = new ConcurrentHashMap<>();
|
||||
|
||||
private WindowRegistry() {}
|
||||
|
||||
public static synchronized WindowRegistry getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new WindowRegistry();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void registerWindow(BrowserWindow window) {
|
||||
windows.put(window.getWindowId(), window);
|
||||
}
|
||||
|
||||
public void registerChildWindow(BrowserWindowJDialog window) {
|
||||
childWindows.put(window.getWindowId(), window);
|
||||
}
|
||||
|
||||
public void registerCallback(String requestId, Consumer<String> handler) {
|
||||
callbacks.put(requestId, handler);
|
||||
}
|
||||
|
||||
public Consumer<String> getCallback(String requestId) {
|
||||
return callbacks.remove(requestId);
|
||||
}
|
||||
|
||||
public void unregisterWindow(String windowId) {
|
||||
BrowserWindow window = windows.remove(windowId);
|
||||
if (window != null) {
|
||||
window.closeWindow();
|
||||
}
|
||||
}
|
||||
|
||||
public BrowserWindow getWindow(String windowId) {
|
||||
return windows.get(windowId);
|
||||
}
|
||||
|
||||
public void update() {
|
||||
for (BrowserWindow window : windows.values()) {
|
||||
if (window != null) {
|
||||
window.updateTheme();
|
||||
}
|
||||
}
|
||||
|
||||
for (BrowserWindowJDialog window : childWindows.values()) {
|
||||
if (window != null) {
|
||||
window.updateTheme();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个新的窗口
|
||||
* @param windowId 窗口ID
|
||||
* @param config 窗口配置
|
||||
*/
|
||||
public void 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个新的子窗口
|
||||
* @param windowId 窗口ID
|
||||
* @param config 窗口配置
|
||||
*/
|
||||
public void 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);
|
||||
}
|
||||
|
||||
private void loadExtLibsPath(BrowserWindow 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
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,346 @@
|
||||
package com.chuangzhou.vivid2D.browser.util;
|
||||
|
||||
import javax.tools.JavaCompiler;
|
||||
import javax.tools.StandardJavaFileManager;
|
||||
import javax.tools.ToolProvider;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public class CodeExecutor {
|
||||
// 用于捕获输出的回调接口
|
||||
public interface OutputListener {
|
||||
void onOutput(String newOutput);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行代码
|
||||
* @param code 代码字符串
|
||||
* @param language 代码类型
|
||||
* @param listener 回调,代码的输出回调
|
||||
* @return 返回i执行结果
|
||||
*/
|
||||
public static String executeCode(String code, String language, OutputListener listener) {
|
||||
switch (language.toLowerCase()) {
|
||||
case "python":
|
||||
return executePythonNative(code, listener);
|
||||
case "c":
|
||||
case "cpp":
|
||||
return executeC(code, listener);
|
||||
case "java":
|
||||
return executeJavaCode(code, listener);
|
||||
default:
|
||||
return "不支持的语言类型: " + language;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行Java代码
|
||||
* @return 返回执行结果
|
||||
*/
|
||||
public static String executeJavaCode(String code, OutputListener listener) {
|
||||
Path tempDir = null;
|
||||
try {
|
||||
// ===== 1. 创建临时目录 =====
|
||||
tempDir = Files.createTempDirectory("javaCode");
|
||||
|
||||
// ===== 2. 写入Java源文件(强制UTF-8)=====
|
||||
Path javaFile = tempDir.resolve("Main.java");
|
||||
Files.writeString(javaFile,
|
||||
code,
|
||||
StandardCharsets.UTF_8
|
||||
);
|
||||
|
||||
// ===== 3. 编译时指定编码 =====
|
||||
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
|
||||
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8);
|
||||
|
||||
List<String> options = new ArrayList<>();
|
||||
options.add("-encoding");
|
||||
options.add("UTF-8");
|
||||
options.add("-d");
|
||||
options.add(tempDir.toString());
|
||||
|
||||
JavaCompiler.CompilationTask task = compiler.getTask(
|
||||
null,
|
||||
fileManager,
|
||||
null,
|
||||
options,
|
||||
null,
|
||||
fileManager.getJavaFileObjects(javaFile)
|
||||
);
|
||||
|
||||
if (!task.call()) {
|
||||
return "编译失败";
|
||||
}
|
||||
|
||||
// ===== 4. 执行配置 =====
|
||||
String javaExe = Path.of(System.getProperty("java.home"), "bin", "java").toString();
|
||||
ProcessBuilder pb = new ProcessBuilder(
|
||||
javaExe,
|
||||
"-Dfile.encoding=UTF-8",
|
||||
"-Dsun.stdout.encoding=UTF-8", // 针对OpenJDK的特殊设置
|
||||
"-Dsun.stderr.encoding=UTF-8",
|
||||
"-cp",
|
||||
tempDir.toString(),
|
||||
"Main"
|
||||
);
|
||||
|
||||
// ===== 5. 设置环境变量 =====
|
||||
Map<String, String> env = pb.environment();
|
||||
env.put("JAVA_TOOL_OPTIONS", "-Dfile.encoding=UTF-8");
|
||||
env.put("LANG", "en_US.UTF-8"); // Linux/macOS
|
||||
env.put("LC_ALL", "en_US.UTF-8");
|
||||
|
||||
// ===== 6. 输出处理 =====
|
||||
pb.redirectErrorStream(true);
|
||||
Process process = pb.start();
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
|
||||
StringBuilder output = new StringBuilder();
|
||||
char[] buffer = new char[4096];
|
||||
int charsRead;
|
||||
|
||||
while ((charsRead = reader.read(buffer)) != -1) {
|
||||
String chunk = new String(buffer, 0, charsRead);
|
||||
output.append(chunk);
|
||||
if (listener != null) {
|
||||
// 处理控制台编码转换(Windows专用)
|
||||
if (System.getProperty("os.name").startsWith("Windows")) {
|
||||
chunk = new String(chunk.getBytes(StandardCharsets.UTF_8), "GBK");
|
||||
}
|
||||
listener.onOutput(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
int exitCode = process.waitFor();
|
||||
return output.toString() + "\n退出码: " + exitCode;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
return "执行错误: " + e.getMessage();
|
||||
} finally {
|
||||
// 清理代码...
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 需要用户安装python环境
|
||||
*/
|
||||
private static String executePythonNative(String code, OutputListener listener) {
|
||||
try {
|
||||
Path pythonFile = Files.createTempFile("script_", ".py");
|
||||
Files.writeString(pythonFile,
|
||||
"# -*- coding: utf-8 -*-\n" + code,
|
||||
StandardCharsets.UTF_8
|
||||
);
|
||||
|
||||
ProcessBuilder pb = new ProcessBuilder("python", pythonFile.toString())
|
||||
.redirectErrorStream(true);
|
||||
|
||||
Map<String, String> env = pb.environment();
|
||||
env.put("PYTHONIOENCODING", "UTF-8");
|
||||
env.put("PYTHONUTF8", "1");
|
||||
|
||||
Process process = pb.start();
|
||||
|
||||
StringBuilder output = new StringBuilder();
|
||||
Thread outputThread = new Thread(() -> {
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
String finalLine = line + "\n";
|
||||
output.append(finalLine);
|
||||
if (listener != null) {
|
||||
listener.onOutput(finalLine);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
outputThread.start();
|
||||
|
||||
// 等待执行完成
|
||||
int exitCode = process.waitFor();
|
||||
outputThread.join();
|
||||
|
||||
// 清理文件
|
||||
Files.deleteIfExists(pythonFile);
|
||||
|
||||
return String.format("退出码: %d\n输出内容:\n%s", exitCode, output);
|
||||
|
||||
} catch (Exception e) {
|
||||
return "执行错误: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行C代码
|
||||
*/
|
||||
private static String executeC(String code, OutputListener listener) {
|
||||
Path tempDir = null;
|
||||
Path cFile = null;
|
||||
Path exeFile = null;
|
||||
try {
|
||||
// 创建临时工作目录
|
||||
tempDir = Files.createTempDirectory("c_compile_");
|
||||
|
||||
// 生成C源代码文件
|
||||
cFile = tempDir.resolve("program.c");
|
||||
Files.writeString(cFile, code, StandardCharsets.UTF_8);
|
||||
|
||||
// 生成可执行文件路径
|
||||
exeFile = tempDir.resolve("program.exe");
|
||||
|
||||
// 1. 编译代码 -------------------------------------------------
|
||||
String tccPath =System.getProperty("user.dir") + "/library/tcc/tcc.exe";
|
||||
|
||||
Process compileProcess;
|
||||
|
||||
if (listener != null) {
|
||||
compileProcess = new ProcessBuilder(
|
||||
tccPath,
|
||||
"-o", exeFile.toString(),
|
||||
cFile.toString()
|
||||
)
|
||||
.directory(tempDir.toFile())
|
||||
.redirectErrorStream(true)
|
||||
.start();
|
||||
|
||||
// 捕获编译输出
|
||||
StringBuilder compileOutput = new StringBuilder();
|
||||
Thread compileOutputThread = new Thread(() -> {
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(compileProcess.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
compileOutput.append(line).append("\n");
|
||||
if (listener != null) {
|
||||
listener.onOutput("[编译输出] " + line + "\n");
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
compileOutputThread.start();
|
||||
|
||||
// 等待编译完成
|
||||
int compileExitCode = compileProcess.waitFor();
|
||||
compileOutputThread.join(1000);
|
||||
|
||||
if (compileExitCode != 0) {
|
||||
return "编译失败:\n" + compileOutput;
|
||||
}
|
||||
|
||||
// 2. 执行程序 -------------------------------------------------
|
||||
Process executeProcess = new ProcessBuilder(exeFile.toString())
|
||||
.directory(tempDir.toFile())
|
||||
.redirectErrorStream(true)
|
||||
.start();
|
||||
|
||||
// 实时输出处理
|
||||
AtomicReference<StringBuilder> execOutput = new AtomicReference<>(new StringBuilder());
|
||||
Thread executeOutputThread = new Thread(() -> {
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(executeProcess.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
char[] buffer = new char[1024];
|
||||
int charsRead;
|
||||
while ((charsRead = reader.read(buffer)) != -1) {
|
||||
String outputChunk = new String(buffer, 0, charsRead);
|
||||
execOutput.get().append(outputChunk);
|
||||
if (listener != null) {
|
||||
listener.onOutput(outputChunk);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
executeOutputThread.start();
|
||||
|
||||
// 等待执行完成(最多10秒)
|
||||
boolean finished = executeProcess.waitFor(10, TimeUnit.SECONDS);
|
||||
executeOutputThread.join(1000);
|
||||
|
||||
if (!finished) {
|
||||
executeProcess.destroyForcibly();
|
||||
return "执行超时\n部分输出:\n" + execOutput.get();
|
||||
}
|
||||
|
||||
// 获取最终输出
|
||||
String finalOutput = execOutput.get().toString();
|
||||
int exitCode = executeProcess.exitValue();
|
||||
return String.format("执行结果: %s\n退出码: %d\n输出内容:\n%s",
|
||||
exitCode == 0 ? "成功" : "失败",
|
||||
exitCode,
|
||||
finalOutput);
|
||||
} else {
|
||||
new ProcessBuilder(
|
||||
tccPath,
|
||||
"-o", exeFile.toString(),
|
||||
cFile.toString()
|
||||
)
|
||||
.directory(tempDir.toFile())
|
||||
.redirectErrorStream(true)
|
||||
.start();
|
||||
|
||||
new ProcessBuilder(
|
||||
"cmd.exe",
|
||||
"/c",
|
||||
"start",
|
||||
"\"Tzd输出窗口\"",
|
||||
"cmd.exe",
|
||||
"/K",
|
||||
"chcp 65001 & ",
|
||||
exeFile.toString()
|
||||
).start();
|
||||
return String.format("执行结果: %s\n退出码: %d\n输出内容:\n%s",
|
||||
"成功",
|
||||
0,
|
||||
"");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
return "执行错误: " + e.getMessage();
|
||||
} finally {
|
||||
// 清理临时文件
|
||||
try {
|
||||
if (listener != null){
|
||||
if (cFile != null) Files.deleteIfExists(cFile);
|
||||
if (exeFile != null) Files.deleteIfExists(exeFile);
|
||||
if (tempDir != null) Files.deleteIfExists(tempDir);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("临时文件清理失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 使用方法
|
||||
public static void main(String[] args) {
|
||||
String pythonCode = "#include <stdio.h>\n" +
|
||||
"\n" +
|
||||
"int main() {\n" +
|
||||
" while (1){\n" +
|
||||
" printf(\"Hello World\\n\");\n" +
|
||||
"}\n" +
|
||||
" return 0;\n" +
|
||||
"}";
|
||||
executeCode(pythonCode, "c", null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
package com.chuangzhou.vivid2D.browser.util;
|
||||
|
||||
import java.sql.*;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* 数据库连接管理器
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class DatabaseConnectionManager {
|
||||
private static final Map<String, Connection> connections = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
private static final Map<String, DatabaseInfo> connectionInfo = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
public static class DatabaseInfo {
|
||||
public String driver;
|
||||
public String url;
|
||||
public String host;
|
||||
public String port;
|
||||
public String database;
|
||||
public String username;
|
||||
|
||||
public DatabaseInfo(String driver, String url, String host, String port, String database, String username) {
|
||||
this.driver = driver;
|
||||
this.url = url;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.database = database;
|
||||
this.username = username;
|
||||
}
|
||||
}
|
||||
|
||||
public static String connect(String driver, String host, String port,
|
||||
String database, String username, String password) throws SQLException {
|
||||
String connectionId = "conn_" + System.currentTimeMillis();
|
||||
|
||||
String drv = driver == null ? "" : driver.toLowerCase();
|
||||
|
||||
// 规范化 database 路径(特别是 Windows 反斜杠问题)
|
||||
if (database != null) {
|
||||
database = database.replace("\\", "/");
|
||||
} else {
|
||||
database = "";
|
||||
}
|
||||
|
||||
// 先显式加载驱动,避免因为 classloader 问题找不到驱动
|
||||
try {
|
||||
switch (drv) {
|
||||
case "mysql":
|
||||
Class.forName("com.mysql.cj.jdbc.Driver");
|
||||
break;
|
||||
case "postgresql":
|
||||
Class.forName("org.postgresql.Driver");
|
||||
break;
|
||||
case "sqlite":
|
||||
Class.forName("org.sqlite.JDBC");
|
||||
break;
|
||||
case "oracle":
|
||||
Class.forName("oracle.jdbc.OracleDriver");
|
||||
break;
|
||||
case "h2":
|
||||
Class.forName("org.h2.Driver");
|
||||
break;
|
||||
default:
|
||||
// 不抛出,使后续 URL 构造仍可检查类型
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new SQLException("JDBC 驱动未找到,请确认对应驱动已加入 classpath: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
String url = buildConnectionUrl(driver, host, port, database);
|
||||
|
||||
Connection connection;
|
||||
Properties props = new Properties();
|
||||
if (username != null && !username.isEmpty()) props.setProperty("user", username);
|
||||
if (password != null && !password.isEmpty()) props.setProperty("password", password);
|
||||
|
||||
switch (drv) {
|
||||
case "mysql":
|
||||
props.setProperty("useSSL", "false");
|
||||
props.setProperty("serverTimezone", "UTC");
|
||||
props.setProperty("allowPublicKeyRetrieval", "true");
|
||||
props.setProperty("useUnicode", "true");
|
||||
props.setProperty("characterEncoding", "UTF-8");
|
||||
connection = DriverManager.getConnection(url, props);
|
||||
break;
|
||||
case "postgresql":
|
||||
connection = DriverManager.getConnection(url, props);
|
||||
break;
|
||||
case "sqlite":
|
||||
// sqlite 不需要 props,URL 已经是文件路径(已做过替换)
|
||||
connection = DriverManager.getConnection(url);
|
||||
break;
|
||||
case "oracle":
|
||||
connection = DriverManager.getConnection(url, props);
|
||||
break;
|
||||
case "h2":
|
||||
// H2 使用默认用户 sa / 空密码(如果需要可调整)
|
||||
connection = DriverManager.getConnection(url, "sa", "");
|
||||
break;
|
||||
default:
|
||||
throw new SQLException("不支持的数据库类型: " + driver);
|
||||
}
|
||||
|
||||
connections.put(connectionId, connection);
|
||||
connectionInfo.put(connectionId, new DatabaseInfo(driver, url, host, port, database, username));
|
||||
return connectionId;
|
||||
}
|
||||
|
||||
public static void disconnect(String connectionId) throws SQLException {
|
||||
Connection connection = connections.get(connectionId);
|
||||
if (connection != null && !connection.isClosed()) {
|
||||
connection.close();
|
||||
}
|
||||
connections.remove(connectionId);
|
||||
connectionInfo.remove(connectionId);
|
||||
}
|
||||
|
||||
public static Connection getConnection(String connectionId) {
|
||||
return connections.get(connectionId);
|
||||
}
|
||||
|
||||
public static DatabaseInfo getConnectionInfo(String connectionId) {
|
||||
return connectionInfo.get(connectionId);
|
||||
}
|
||||
|
||||
private static String buildConnectionUrl(String driver, String host, String port, String database) {
|
||||
String drv = driver == null ? "" : driver.toLowerCase();
|
||||
switch (drv) {
|
||||
case "mysql":
|
||||
return "jdbc:mysql://" + host + ":" + port + "/" + database;
|
||||
case "postgresql":
|
||||
return "jdbc:postgresql://" + host + ":" + port + "/" + database;
|
||||
case "sqlite":
|
||||
// 对于 SQLite,database 可能是绝对路径或相对文件名,先把反斜杠替成正斜杠
|
||||
if (database == null || database.isEmpty()) {
|
||||
return "jdbc:sqlite::memory:";
|
||||
}
|
||||
String normalized = database.replace("\\", "/");
|
||||
// 如果看起来像相对文件名(不含冒号也不以 / 开头),则当作相对于用户目录的路径
|
||||
if (!normalized.contains(":") && !normalized.startsWith("/")) {
|
||||
String userHome = System.getProperty("user.home").replace("\\", "/");
|
||||
normalized = userHome + "/" + normalized;
|
||||
}
|
||||
return "jdbc:sqlite:" + normalized;
|
||||
case "oracle":
|
||||
return "jdbc:oracle:thin:@" + host + ":" + port + ":" + database;
|
||||
case "h2":
|
||||
// H2 文件路径同样做反斜杠处理
|
||||
if (database == null || database.isEmpty()) {
|
||||
String userHome = System.getProperty("user.home").replace("\\", "/");
|
||||
return "jdbc:h2:file:" + userHome + "/.axis_innovators_box/databases/h2db";
|
||||
} else {
|
||||
String norm = database.replace("\\", "/");
|
||||
// 如果传入仅是名字(无斜杠或冒号),则存到用户目录下
|
||||
if (!norm.contains("/") && !norm.contains(":")) {
|
||||
String userHome = System.getProperty("user.home").replace("\\", "/");
|
||||
norm = userHome + "/.axis_innovators_box/databases/" + norm;
|
||||
}
|
||||
return "jdbc:h2:file:" + norm;
|
||||
}
|
||||
default:
|
||||
throw new IllegalArgumentException("不支持的数据库类型: " + driver);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 在服务器上创建数据库(MySQL / PostgreSQL / Oracle(示例))
|
||||
* @param driver mysql | postgresql | oracle
|
||||
* @param host 数据库主机
|
||||
* @param port 端口
|
||||
* @param dbName 要创建的数据库名(或 schema 名)
|
||||
* @param adminUser 管理员用户名(用于创建数据库)
|
||||
* @param adminPassword 管理员密码
|
||||
* @return 如果创建成功返回一个简短消息,否则抛出 SQLException
|
||||
* @throws SQLException
|
||||
*/
|
||||
public static String createDatabaseOnServer(String driver, String host, String port,
|
||||
String dbName, String adminUser, String adminPassword) throws SQLException {
|
||||
if (driver == null) throw new SQLException("driver 不能为空");
|
||||
String drv = driver.toLowerCase().trim();
|
||||
|
||||
// 简单校验 dbName(避免注入)——只允许字母数字下划线
|
||||
if (dbName == null || !dbName.matches("[A-Za-z0-9_]+")) {
|
||||
throw new SQLException("不合法的数据库名: " + dbName);
|
||||
}
|
||||
|
||||
try {
|
||||
switch (drv) {
|
||||
case "mysql":
|
||||
// 加载驱动(如果尚未加载)
|
||||
try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) {
|
||||
throw new SQLException("MySQL 驱动未找到,请加入 mysql-connector-java 到 classpath", e);
|
||||
}
|
||||
// 连接到服务器的默认库(不指定数据库)以执行 CREATE DATABASE
|
||||
String mysqlUrl = "jdbc:mysql://" + host + ":" + port + "/?useSSL=false&serverTimezone=UTC";
|
||||
try (Connection conn = DriverManager.getConnection(mysqlUrl, adminUser, adminPassword);
|
||||
Statement st = conn.createStatement()) {
|
||||
String sql = "CREATE DATABASE `" + dbName + "` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci";
|
||||
st.executeUpdate(sql);
|
||||
}
|
||||
return "MySQL 数据库创建成功: " + dbName;
|
||||
|
||||
case "postgresql":
|
||||
case "postgres":
|
||||
try { Class.forName("org.postgresql.Driver"); } catch (ClassNotFoundException e) {
|
||||
throw new SQLException("Postgres 驱动未找到,请加入 postgresql 到 classpath", e);
|
||||
}
|
||||
// 连接到默认 postgres 数据库以创建新数据库
|
||||
String pgUrl = "jdbc:postgresql://" + host + ":" + port + "/postgres";
|
||||
try (Connection conn = DriverManager.getConnection(pgUrl, adminUser, adminPassword);
|
||||
Statement st = conn.createStatement()) {
|
||||
String sql = "CREATE DATABASE " + dbName + " WITH ENCODING 'UTF8'";
|
||||
st.executeUpdate(sql);
|
||||
}
|
||||
return "PostgreSQL 数据库创建成功: " + dbName;
|
||||
|
||||
case "oracle":
|
||||
// Oracle 数据库“创建数据库”通常由 DBA 完成(复杂),这里示例创建用户/模式(更常见)
|
||||
try { Class.forName("oracle.jdbc.OracleDriver"); } catch (ClassNotFoundException e) {
|
||||
throw new SQLException("Oracle 驱动未找到,请把 ojdbc.jar 加入 classpath", e);
|
||||
}
|
||||
// 需使用具有足够权限的账户(通常为 sys or system),并且 URL 需要正确(SID / ServiceName)
|
||||
// 下面示例假设通过 SID 连接: jdbc:oracle:thin:@host:port:SID
|
||||
String oracleUrl = "jdbc:oracle:thin:@" + host + ":" + port + ":" + "ORCL"; // 把 ORCL 换成实际 SID
|
||||
try (Connection conn = DriverManager.getConnection(oracleUrl, adminUser, adminPassword);
|
||||
Statement st = conn.createStatement()) {
|
||||
// 创建 user(schema)示例
|
||||
String pwd = adminPassword; // 实际应使用独立密码,不推荐用 adminPassword
|
||||
String createUser = "CREATE USER " + dbName + " IDENTIFIED BY \"" + pwd + "\"";
|
||||
String grant = "GRANT CONNECT, RESOURCE TO " + dbName;
|
||||
st.executeUpdate(createUser);
|
||||
st.executeUpdate(grant);
|
||||
} catch (SQLException ex) {
|
||||
// Oracle 操作更容易失败,给出提示
|
||||
throw new SQLException("Oracle: 无法创建用户/模式,请检查权限和 URL(通常需由 DBA 操作): " + ex.getMessage(), ex);
|
||||
}
|
||||
return "Oracle 用户/模式创建成功(注意:真正的 DB 实例通常由 DBA 管理): " + dbName;
|
||||
|
||||
default:
|
||||
throw new SQLException("不支持的数据库类型: " + driver);
|
||||
}
|
||||
} catch (SQLException se) {
|
||||
// 透传 SQLException,调用方会拿到 message 并反馈给前端
|
||||
throw se;
|
||||
} catch (Exception e) {
|
||||
throw new SQLException("创建数据库时发生异常: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
public static String createLocalDatabase(String driver, String dbName) throws SQLException {
|
||||
switch (driver.toLowerCase()) {
|
||||
case "sqlite":
|
||||
// 创建目录并构造规范化路径(确保路径使用正斜杠)
|
||||
String dbFileName = dbName.endsWith(".db") ? dbName : (dbName + ".db");
|
||||
java.nio.file.Path dbDir = java.nio.file.Paths.get(System.getProperty("user.home"), ".axis_innovators_box", "databases");
|
||||
try {
|
||||
java.nio.file.Files.createDirectories(dbDir);
|
||||
} catch (Exception e) {
|
||||
throw new SQLException("无法创建数据库目录: " + e.getMessage(), e);
|
||||
}
|
||||
String dbPath = dbDir.resolve(dbFileName).toAbsolutePath().toString().replace("\\", "/");
|
||||
|
||||
// 显式加载 sqlite 驱动(避免 No suitable driver)
|
||||
try {
|
||||
Class.forName("org.sqlite.JDBC");
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new SQLException("未找到 sqlite 驱动,请确认 sqlite-jdbc 已加入 classpath", e);
|
||||
}
|
||||
|
||||
// 直接使用 connect 构建连接(connect 中会通过 buildConnectionUrl 处理 path)
|
||||
String connectionId = connect("sqlite", "", "", dbPath, "", "");
|
||||
|
||||
// 创建示例表
|
||||
createSampleTables(connectionId);
|
||||
|
||||
return connectionId;
|
||||
|
||||
case "h2":
|
||||
java.nio.file.Path h2Dir = java.nio.file.Paths.get(System.getProperty("user.home"), ".axis_innovators_box", "databases");
|
||||
try {
|
||||
java.nio.file.Files.createDirectories(h2Dir);
|
||||
} catch (Exception e) {
|
||||
throw new SQLException("无法创建数据库目录: " + e.getMessage(), e);
|
||||
}
|
||||
String h2Path = h2Dir.resolve(dbName).toAbsolutePath().toString().replace("\\", "/");
|
||||
String h2Url = "jdbc:h2:file:" + h2Path;
|
||||
|
||||
try {
|
||||
Class.forName("org.h2.Driver");
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new SQLException("未找到 H2 驱动,请确认 h2.jar 已加入 classpath", e);
|
||||
}
|
||||
|
||||
Connection h2Conn = DriverManager.getConnection(h2Url, "sa", "");
|
||||
String h2ConnectionId = "conn_" + System.currentTimeMillis();
|
||||
connections.put(h2ConnectionId, h2Conn);
|
||||
connectionInfo.put(h2ConnectionId, new DatabaseInfo("h2", h2Url, "localhost", "", dbName, "sa"));
|
||||
|
||||
createSampleTables(h2ConnectionId);
|
||||
|
||||
return h2ConnectionId;
|
||||
|
||||
default:
|
||||
throw new SQLException("不支持创建本地数据库类型: " + driver);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createSampleTables(String connectionId) throws SQLException {
|
||||
Connection conn = getConnection(connectionId);
|
||||
DatabaseInfo info = getConnectionInfo(connectionId);
|
||||
|
||||
if ("sqlite".equals(info.driver) || "h2".equals(info.driver)) {
|
||||
try (Statement stmt = conn.createStatement()) {
|
||||
// 创建用户表
|
||||
stmt.execute("CREATE TABLE IF NOT EXISTS users (" +
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
"username VARCHAR(50) NOT NULL UNIQUE, " +
|
||||
"email VARCHAR(100) NOT NULL, " +
|
||||
"password VARCHAR(100) NOT NULL, " +
|
||||
"status VARCHAR(20) DEFAULT 'active', " +
|
||||
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
|
||||
")");
|
||||
|
||||
// 创建产品表
|
||||
stmt.execute("CREATE TABLE IF NOT EXISTS products (" +
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
"name VARCHAR(100) NOT NULL, " +
|
||||
"description TEXT, " +
|
||||
"price DECIMAL(10,2) NOT NULL, " +
|
||||
"stock INTEGER DEFAULT 0, " +
|
||||
"category VARCHAR(50), " +
|
||||
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
|
||||
")");
|
||||
|
||||
// 创建订单表
|
||||
stmt.execute("CREATE TABLE IF NOT EXISTS orders (" +
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
"user_id INTEGER, " +
|
||||
"product_id INTEGER, " +
|
||||
"quantity INTEGER NOT NULL, " +
|
||||
"total_price DECIMAL(10,2) NOT NULL, " +
|
||||
"status VARCHAR(20) DEFAULT 'pending', " +
|
||||
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
|
||||
"FOREIGN KEY (user_id) REFERENCES users(id), " +
|
||||
"FOREIGN KEY (product_id) REFERENCES products(id)" +
|
||||
")");
|
||||
|
||||
// 插入示例数据
|
||||
insertSampleData(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void insertSampleData(Connection conn) throws SQLException {
|
||||
// 检查是否已有数据
|
||||
try (Statement checkStmt = conn.createStatement();
|
||||
ResultSet rs = checkStmt.executeQuery("SELECT COUNT(*) FROM users")) {
|
||||
if (rs.next() && rs.getInt(1) == 0) {
|
||||
// 插入用户数据
|
||||
try (PreparedStatement pstmt = conn.prepareStatement(
|
||||
"INSERT INTO users (username, email, password) VALUES (?, ?, ?)")) {
|
||||
String[][] users = {
|
||||
{"admin", "admin@example.com", "password123"},
|
||||
{"user1", "user1@example.com", "password123"},
|
||||
{"user2", "user2@example.com", "password123"}
|
||||
};
|
||||
|
||||
for (String[] user : users) {
|
||||
pstmt.setString(1, user[0]);
|
||||
pstmt.setString(2, user[1]);
|
||||
pstmt.setString(3, user[2]);
|
||||
pstmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
// 插入产品数据
|
||||
try (PreparedStatement pstmt = conn.prepareStatement(
|
||||
"INSERT INTO products (name, description, price, stock, category) VALUES (?, ?, ?, ?, ?)")) {
|
||||
Object[][] products = {
|
||||
{"笔记本电脑", "高性能笔记本电脑", 5999.99, 50, "电子"},
|
||||
{"智能手机", "最新款智能手机", 3999.99, 100, "电子"},
|
||||
{"办公椅", "舒适办公椅", 299.99, 30, "家居"},
|
||||
{"咖啡机", "全自动咖啡机", 899.99, 20, "家电"}
|
||||
};
|
||||
|
||||
for (Object[] product : products) {
|
||||
pstmt.setString(1, (String) product[0]);
|
||||
pstmt.setString(2, (String) product[1]);
|
||||
pstmt.setDouble(3, (Double) product[2]);
|
||||
pstmt.setInt(4, (Integer) product[3]);
|
||||
pstmt.setString(5, (String) product[4]);
|
||||
pstmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.chuangzhou.vivid2D.render.awt;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
public class EventPanel extends JPanel {
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package com.chuangzhou.vivid2D.render.awt.manager;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
@@ -8,6 +8,9 @@ import com.chuangzhou.vivid2D.render.model.AnimationParameter;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelEvent;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Vertex;
|
||||
import com.chuangzhou.vivid2D.render.systems.Matrix3fUtils;
|
||||
import org.joml.Matrix3f;
|
||||
import org.joml.Vector2f;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -23,21 +26,26 @@ public class ParametersManagement {
|
||||
|
||||
public ParametersManagement(ParametersPanel parametersPanel) {
|
||||
this.parametersPanel = parametersPanel;
|
||||
ModelRenderPanel renderPanel = parametersPanel.getRenderPanel();
|
||||
installingCallbacks();
|
||||
renderPanel.getModel().addEvent((eventName, eventBus) -> {
|
||||
if (eventName.equals("model_part_added")) {
|
||||
installingCallbacks();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装参数管理回调
|
||||
*/
|
||||
public void installingCallbacks() {
|
||||
ModelRenderPanel renderPanel = parametersPanel.getRenderPanel();
|
||||
for (int i = 0; i < renderPanel.getModel().getParts().size(); i++) {
|
||||
ModelPart modelPart = renderPanel.getModel().getParts().get(i);
|
||||
modelPart.addEvent((eventName, eventBus) -> {
|
||||
if (eventName.equals("vertex")){
|
||||
if (!(eventBus instanceof Map)) {
|
||||
logger.error("Error: eventBus is not a Map for vertex event.");
|
||||
return;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> eventPayload = (Map<String, Object>) eventBus;
|
||||
ModelPart caller = (ModelPart) eventPayload.get("caller");
|
||||
Vertex oldVertexObj = (Vertex) eventPayload.get("oldVertex");
|
||||
Vertex newVertexObj = (Vertex) eventPayload.get("newVertex");
|
||||
updateVertex(caller, oldVertexObj, newVertexObj);
|
||||
if (eventName.equals("vertex_position")){
|
||||
//logger.info("顶点位置已更新: {}", eventBus);
|
||||
updateVertex((ModelPart) eventBus);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -50,68 +58,72 @@ public class ParametersManagement {
|
||||
* - 如果没有找到,则调用 broadcast 创建一个新的关键帧记录。
|
||||
*
|
||||
* @param caller 触发事件的 ModelPart
|
||||
* @param oldVertexObj 变化前的 Vertex 状态,用于查找要替换的目标
|
||||
* @param newVertexObj 变化后的 Vertex 状态,用于提供新的数据
|
||||
*/
|
||||
public void updateVertex(ModelPart caller, Vertex oldVertexObj, Vertex newVertexObj) {
|
||||
if (newVertexObj == null || newVertexObj.getName() == null || oldVertexObj == null || oldVertexObj.getName() == null) {
|
||||
return;
|
||||
}
|
||||
boolean updatedExisting = false;
|
||||
public void updateVertex(ModelPart caller) {
|
||||
for (int i = 0; i < oldValues.size(); i++) {
|
||||
Parameter existingParameter = oldValues.get(i);
|
||||
if (existingParameter.modelPart().equals(caller)) {
|
||||
List<Object> values = existingParameter.value();
|
||||
for (int j = 0; j < values.size(); j++) {
|
||||
if (!"meshVertices".equals(existingParameter.paramId().get(j))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Object value = values.get(j);
|
||||
if (!(value instanceof Map)) {
|
||||
continue;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> payload = (Map<String, Object>) value;
|
||||
String storedVertexId = (String) payload.get("id");
|
||||
float[] storedPosition = (float[]) payload.get("Vertex");
|
||||
if (!Objects.equals(storedVertexId, oldVertexObj.getName())) {
|
||||
continue;
|
||||
}
|
||||
if (storedPosition == null || storedPosition.length != 2) {
|
||||
continue;
|
||||
}
|
||||
final float epsilon = 1e-5f;
|
||||
boolean positionMatches = Math.abs(storedPosition[0] - oldVertexObj.position.x) < epsilon &&
|
||||
Math.abs(storedPosition[1] - oldVertexObj.position.y) < epsilon;
|
||||
if (positionMatches) {
|
||||
logger.debug("在{}关键帧中找到原来匹配的顶点(ID:{})并执行原地更新。", existingParameter.keyframe().get(j), oldVertexObj.getName());
|
||||
Map<String, Object> newVertexUpdatePayload = Map.of(
|
||||
"id", newVertexObj.getName(),
|
||||
"Vertex", new float[]{newVertexObj.position.x, newVertexObj.position.y}
|
||||
);
|
||||
List<Object> newValues = new ArrayList<>(values);
|
||||
newValues.set(j, newVertexUpdatePayload);
|
||||
Parameter updatedParameter = new Parameter(
|
||||
existingParameter.modelPart(),
|
||||
new ArrayList<>(existingParameter.animationParameter()),
|
||||
new ArrayList<>(existingParameter.paramId()),
|
||||
newValues,
|
||||
new ArrayList<>(existingParameter.keyframe()),
|
||||
new ArrayList<>(existingParameter.isKeyframe())
|
||||
);
|
||||
oldValues.set(i, updatedParameter);
|
||||
updatedExisting = true;
|
||||
// break;
|
||||
}
|
||||
}
|
||||
if (!existingParameter.modelPart().equals(caller)) {
|
||||
continue;
|
||||
}
|
||||
if (updatedExisting) {
|
||||
break;
|
||||
|
||||
List<Object> originalValues = existingParameter.value();
|
||||
List<Object> updatedValues = new ArrayList<>(originalValues);
|
||||
boolean vertexDataChanged = false;
|
||||
|
||||
for (int j = 0; j < updatedValues.size(); j++) {
|
||||
if (!"meshVertices".equals(existingParameter.paramId().get(j))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Object value = originalValues.get(j);
|
||||
if (!(value instanceof Map)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> payload = (Map<String, Object>) value;
|
||||
String storedVertexId = (String) payload.get("id");
|
||||
Vertex sourceVertex = (Vertex) payload.get("Vertex");
|
||||
|
||||
if (sourceVertex == null) continue;
|
||||
//Vertex newVertex = findLiveVertex(caller,sourceVertex).copy();
|
||||
Vertex newVertex = sourceVertex.copy();
|
||||
Vector2f worldPoint = Matrix3fUtils.transformPoint(caller.getWorldTransform(), newVertex.originalPosition);
|
||||
newVertex.position.set(worldPoint);
|
||||
Map<String, Object> newVertexUpdatePayload = new HashMap<>();
|
||||
newVertexUpdatePayload.put("id", storedVertexId);
|
||||
newVertexUpdatePayload.put("Vertex", newVertex);
|
||||
updatedValues.set(j, newVertexUpdatePayload);
|
||||
vertexDataChanged = true;
|
||||
//logger.info("已更新顶点: {} -> {}", storedVertexId, worldPoint);
|
||||
}
|
||||
|
||||
if (vertexDataChanged) {
|
||||
Parameter updatedParameter = new Parameter(
|
||||
existingParameter.modelPart(),
|
||||
new ArrayList<>(existingParameter.animationParameter()),
|
||||
new ArrayList<>(existingParameter.paramId()),
|
||||
updatedValues,
|
||||
new ArrayList<>(existingParameter.keyframe()),
|
||||
new ArrayList<>(existingParameter.isKeyframe())
|
||||
);
|
||||
oldValues.set(i, updatedParameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Vertex findLiveVertex(ModelPart part, Vertex vertex) {
|
||||
for (com.chuangzhou.vivid2D.render.model.Mesh2D mesh : part.getMeshes()) {
|
||||
for (Vertex v : mesh.getActiveVertexList()) {
|
||||
if (vertex._equals(v)) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.warn("未找到匹配的顶点: {}", vertex);
|
||||
return vertex;
|
||||
}
|
||||
|
||||
public static ParametersManagement getInstance(ParametersPanel parametersPanel) {
|
||||
String managementFilePath = parametersPanel.getRenderPanel().getGlContextManager().getModelPath() + ".data";
|
||||
|
||||
@@ -6,14 +6,20 @@ import com.chuangzhou.vivid2D.render.model.AnimationParameter;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.data.ParameterData;
|
||||
import com.chuangzhou.vivid2D.render.model.data.PartData;
|
||||
import com.chuangzhou.vivid2D.render.model.data.VertexData;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Vertex; // 导入 Vertex
|
||||
import org.joml.Vector2f; // 导入 Vector2f
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* ParametersManagement 的序列化数据类
|
||||
* 修复了直接序列化 Vertex 运行时对象导致的问题
|
||||
*/
|
||||
public class ParametersManagementData implements Serializable {
|
||||
@Serial
|
||||
@@ -43,21 +49,9 @@ public class ParametersManagementData implements Serializable {
|
||||
}
|
||||
|
||||
public ParametersManagement toParametersManagement(ParametersPanel parametersPanel) {
|
||||
ParametersManagement management = new ParametersManagement(parametersPanel);
|
||||
|
||||
if (this.oldValues != null) {
|
||||
for (ManagementParameterRecord paramRecord : this.oldValues) {
|
||||
ParametersManagement.Parameter param = paramRecord.toParameter();
|
||||
management.oldValues.add(param);
|
||||
}
|
||||
}
|
||||
|
||||
return management;
|
||||
return toParametersManagement(parametersPanel, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 ModelPart 列表来重新关联 ModelPart 引用
|
||||
*/
|
||||
public ParametersManagement toParametersManagement(ParametersPanel parametersPanel, List<ModelPart> modelParts) {
|
||||
ParametersManagement management = new ParametersManagement(parametersPanel);
|
||||
|
||||
@@ -74,6 +68,7 @@ public class ParametersManagementData implements Serializable {
|
||||
public ParametersManagementData copy() {
|
||||
ParametersManagementData copy = new ParametersManagementData();
|
||||
copy.oldValues = new ArrayList<>();
|
||||
copy.isBreakage = this.isBreakage;
|
||||
if (this.oldValues != null) {
|
||||
for (ManagementParameterRecord paramRecord : this.oldValues) {
|
||||
copy.oldValues.add(paramRecord.copy());
|
||||
@@ -82,32 +77,17 @@ public class ParametersManagementData implements Serializable {
|
||||
return copy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("ParametersManagementData:\n");
|
||||
|
||||
if (oldValues == null || oldValues.isEmpty()) {
|
||||
sb.append(" No parameter records\n");
|
||||
} else {
|
||||
for (int i = 0; i < oldValues.size(); i++) {
|
||||
ManagementParameterRecord paramRecord = oldValues.get(i);
|
||||
sb.append(String.format(" Record %d: %s\n", i, paramRecord.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
// ==================== 内部类 ====================
|
||||
|
||||
public static class ManagementParameterRecord implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public String modelPartName; // 通过名称引用 ModelPart
|
||||
public PartData modelPartData; // 新增:完整的 ModelPart 数据
|
||||
public String modelPartName;
|
||||
public PartData modelPartData;
|
||||
public List<ParameterData> animationParameters;
|
||||
public List<String> paramIds;
|
||||
public List<Object> values;
|
||||
public List<Object> values; // 这里将存储 SerializableVertex 而不是 Vertex
|
||||
public List<Float> keyframes;
|
||||
public List<Boolean> isKeyframes;
|
||||
|
||||
@@ -123,25 +103,26 @@ public class ParametersManagementData implements Serializable {
|
||||
this();
|
||||
if (parameter.modelPart() != null) {
|
||||
this.modelPartName = parameter.modelPart().getName();
|
||||
// 序列化完整的 ModelPart 数据
|
||||
this.modelPartData = new PartData(parameter.modelPart());
|
||||
}
|
||||
|
||||
// 序列化 AnimationParameter 列表
|
||||
if (parameter.animationParameter() != null) {
|
||||
for (AnimationParameter animParam : parameter.animationParameter()) {
|
||||
ParameterData animParamData = new ParameterData(animParam);
|
||||
this.animationParameters.add(animParamData);
|
||||
this.animationParameters.add(new ParameterData(animParam));
|
||||
}
|
||||
}
|
||||
|
||||
// 序列化其他列表
|
||||
if (parameter.paramId() != null) {
|
||||
this.paramIds.addAll(parameter.paramId());
|
||||
}
|
||||
|
||||
// [核心修复]:深拷贝 values,并检查是否包含 Vertex 对象
|
||||
if (parameter.value() != null) {
|
||||
this.values.addAll(parameter.value());
|
||||
for (Object val : parameter.value()) {
|
||||
this.values.add(convertValueForSerialization(val));
|
||||
}
|
||||
}
|
||||
|
||||
if (parameter.keyframe() != null) {
|
||||
this.keyframes.addAll(parameter.keyframe());
|
||||
}
|
||||
@@ -150,33 +131,58 @@ public class ParametersManagementData implements Serializable {
|
||||
}
|
||||
}
|
||||
|
||||
public ParametersManagement.Parameter toParameter() {
|
||||
// 注意:ModelPart 需要通过名称在反序列化时重新关联
|
||||
List<AnimationParameter> animParams = new ArrayList<>();
|
||||
if (this.animationParameters != null) {
|
||||
for (ParameterData animParamData : this.animationParameters) {
|
||||
AnimationParameter animParam = animParamData.toAnimationParameter();
|
||||
animParams.add(animParam);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 将运行时对象转换为可序列化对象
|
||||
*/
|
||||
private Object convertValueForSerialization(Object val) {
|
||||
// 检查是否是包含 Vertex 的 Map (对应 "meshVertices" 参数)
|
||||
if (val instanceof Map) {
|
||||
Map<?, ?> originalMap = (Map<?, ?>) val;
|
||||
// 浅拷贝 Map 结构
|
||||
Map<String, Object> newMap = new HashMap<>();
|
||||
for (Map.Entry<?, ?> entry : originalMap.entrySet()) {
|
||||
String key = String.valueOf(entry.getKey());
|
||||
Object value = entry.getValue();
|
||||
|
||||
return new ParametersManagement.Parameter(
|
||||
null, // ModelPart 需要在外部重新设置
|
||||
animParams,
|
||||
this.paramIds != null ? new ArrayList<>(this.paramIds) : new ArrayList<>(),
|
||||
this.values != null ? new ArrayList<>(this.values) : new ArrayList<>(),
|
||||
this.keyframes != null ? new ArrayList<>(this.keyframes) : new ArrayList<>(),
|
||||
this.isKeyframes != null ? new ArrayList<>(this.isKeyframes) : new ArrayList<>()
|
||||
);
|
||||
if ("Vertex".equals(key) && value instanceof Vertex) {
|
||||
// 将 Vertex 转换为 DTO
|
||||
newMap.put(key, new VertexData((Vertex) value));
|
||||
} else {
|
||||
// 递归处理还是直接存放? 目前直接存放,假设其他都是基本类型或String
|
||||
newMap.put(key, value);
|
||||
}
|
||||
}
|
||||
return newMap;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 ModelPart 列表重新关联 ModelPart 引用
|
||||
* 将序列化对象还原为运行时对象
|
||||
*/
|
||||
private Object convertValueFromSerialization(Object val) {
|
||||
if (val instanceof Map) {
|
||||
Map<?, ?> storedMap = (Map<?, ?>) val;
|
||||
Map<String, Object> runtimeMap = new HashMap<>();
|
||||
for (Map.Entry<?, ?> entry : storedMap.entrySet()) {
|
||||
String key = String.valueOf(entry.getKey());
|
||||
Object value = entry.getValue();
|
||||
|
||||
if ("Vertex".equals(key) && value instanceof VertexData) {
|
||||
// 将 DTO 还原为 Vertex
|
||||
runtimeMap.put(key, ((VertexData) value).toVertex());
|
||||
} else {
|
||||
runtimeMap.put(key, value);
|
||||
}
|
||||
}
|
||||
return runtimeMap;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
public ParametersManagement.Parameter toParameter(List<ModelPart> modelParts) {
|
||||
ModelPart modelPart = null;
|
||||
|
||||
// 通过名称查找对应的 ModelPart
|
||||
if (this.modelPartName != null && modelParts != null) {
|
||||
for (ModelPart part : modelParts) {
|
||||
if (this.modelPartName.equals(part.getName())) {
|
||||
@@ -185,22 +191,24 @@ public class ParametersManagementData implements Serializable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没找到,尝试使用 modelPartData 重建
|
||||
if (modelPart == null && this.modelPartData != null) {
|
||||
try {
|
||||
// 创建一个空的 meshMap,因为这里可能没有完整的网格上下文
|
||||
modelPart = this.modelPartData.toModelPart(new java.util.HashMap<>());
|
||||
} catch (Exception e) {
|
||||
System.err.println("重建 ModelPart 失败: " + e.getMessage());
|
||||
}
|
||||
modelPart = this.modelPartData.toModelPart(new HashMap<>());
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
List<AnimationParameter> animParams = new ArrayList<>();
|
||||
if (this.animationParameters != null) {
|
||||
for (ParameterData animParamData : this.animationParameters) {
|
||||
AnimationParameter animParam = animParamData.toAnimationParameter();
|
||||
animParams.add(animParam);
|
||||
for (ParameterData p : this.animationParameters) {
|
||||
animParams.add(p.toAnimationParameter());
|
||||
}
|
||||
}
|
||||
|
||||
// [核心修复]:还原 values 中的 Vertex 对象
|
||||
List<Object> runtimeValues = new ArrayList<>();
|
||||
if (this.values != null) {
|
||||
for (Object val : this.values) {
|
||||
runtimeValues.add(convertValueFromSerialization(val));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +216,7 @@ public class ParametersManagementData implements Serializable {
|
||||
modelPart,
|
||||
animParams,
|
||||
this.paramIds != null ? new ArrayList<>(this.paramIds) : new ArrayList<>(),
|
||||
this.values != null ? new ArrayList<>(this.values) : new ArrayList<>(),
|
||||
runtimeValues,
|
||||
this.keyframes != null ? new ArrayList<>(this.keyframes) : new ArrayList<>(),
|
||||
this.isKeyframes != null ? new ArrayList<>(this.isKeyframes) : new ArrayList<>()
|
||||
);
|
||||
@@ -219,29 +227,20 @@ public class ParametersManagementData implements Serializable {
|
||||
copy.modelPartName = this.modelPartName;
|
||||
copy.modelPartData = this.modelPartData != null ? this.modelPartData.copy() : null;
|
||||
|
||||
// 深拷贝 animationParameters
|
||||
copy.animationParameters = new ArrayList<>();
|
||||
if (this.animationParameters != null) {
|
||||
for (ParameterData animParam : this.animationParameters) {
|
||||
copy.animationParameters.add(animParam.copy());
|
||||
}
|
||||
for (ParameterData p : this.animationParameters) copy.animationParameters.add(p.copy());
|
||||
}
|
||||
|
||||
// 深拷贝其他列表
|
||||
copy.paramIds = this.paramIds != null ? new ArrayList<>(this.paramIds) : new ArrayList<>();
|
||||
// 注意:values 里的 SerializableVertex 也应该 copy,或者依赖其不可变性/序列化特性
|
||||
// 这里做简单的列表拷贝,因为 SerializableVertex 通常是纯数据
|
||||
copy.values = this.values != null ? new ArrayList<>(this.values) : new ArrayList<>();
|
||||
|
||||
copy.keyframes = this.keyframes != null ? new ArrayList<>(this.keyframes) : new ArrayList<>();
|
||||
copy.isKeyframes = this.isKeyframes != null ? new ArrayList<>(this.isKeyframes) : new ArrayList<>();
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"ManagementParameterRecord[Part=%s, Params=%s, Values=%s, Keyframes=%s]",
|
||||
modelPartName, paramIds, values, keyframes
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1247,16 +1247,7 @@ public class SelectionTool extends Tool {
|
||||
logger.info("选择工具请求进入液化模式: {}", targetMesh.getName());
|
||||
}
|
||||
|
||||
public void updateMeshVertices() {
|
||||
for (Mesh2D mesh : selectedMeshes) {
|
||||
updateMeshVertices(mesh);
|
||||
}
|
||||
updateMeshVertices(hoveredMesh);
|
||||
}
|
||||
|
||||
public void updateMeshVertices(Mesh2D mesh) {
|
||||
//
|
||||
}
|
||||
public void updateMeshVertices() {}
|
||||
|
||||
/**
|
||||
* 获取鼠标悬停的网格
|
||||
|
||||
@@ -97,17 +97,13 @@ public class VertexDeformationTool extends Tool {
|
||||
|
||||
// [核心修正] 检查 Alt 键是否被按下
|
||||
if (e.isAltDown()) {
|
||||
// --- 进入“推/拉”模式 ---
|
||||
isPushPullMode = true;
|
||||
dragStartPoint = new Vector2f(modelX, modelY);
|
||||
|
||||
// 创建当前网格顶点状态的快照,这是计算位移的基准
|
||||
dragBaseState = new ArrayList<>(targetMesh.getActiveVertexList().size());
|
||||
for(Vertex v : targetMesh.getActiveVertexList()){
|
||||
dragBaseState.add(v.copy()); // 必须是深拷贝
|
||||
dragBaseState.add(v.copy());
|
||||
}
|
||||
|
||||
currentDragMode = ModelRenderPanel.DragMode.NONE; // 确保不触发控制点拖动
|
||||
currentDragMode = ModelRenderPanel.DragMode.NONE;
|
||||
logger.debug("进入推/拉模式,起点: ({}, {})", modelX, modelY);
|
||||
|
||||
} else {
|
||||
@@ -182,7 +178,7 @@ public class VertexDeformationTool extends Tool {
|
||||
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||
try {
|
||||
Map<String, Object> parameters = Map.of("id", primaryVertex.getName(),
|
||||
"Vertex", new float[]{modelX, modelY});
|
||||
"Vertex", primaryVertex);
|
||||
renderPanel.getParametersManagement().broadcast(
|
||||
targetMesh.getModelPart(),
|
||||
"meshVertices",
|
||||
|
||||
@@ -44,16 +44,21 @@ public class FrameInterpolator {
|
||||
} else if (o != null && o.getClass().isArray()) {
|
||||
try {
|
||||
// 处理 float[] 的情况
|
||||
if (o instanceof float[]) {
|
||||
float[] arr = (float[]) o;
|
||||
if (o instanceof float[] arr) {
|
||||
if (arr.length > 0) out[0] = arr[0];
|
||||
if (arr.length > 1) out[1] = arr[1];
|
||||
} else {
|
||||
Object[] arr = (Object[]) o;
|
||||
if (arr.length > 0) out[0] = toFloat(arr[0]);
|
||||
if (arr.length > 1) out[1] = toFloat(arr[1]);
|
||||
Object[] arr = null;
|
||||
if (o instanceof Object[]) {
|
||||
arr = (Object[]) o;
|
||||
}
|
||||
if (arr != null && arr.length > 0) out[0] = toFloat(arr[0]);
|
||||
if (arr != null && arr.length > 1) out[1] = toFloat(arr[1]);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
} if (o instanceof Vertex vertex) {
|
||||
out[0] = vertex.position.x;
|
||||
out[1] = vertex.position.y;
|
||||
} else if (o instanceof Number || o instanceof String) {
|
||||
float v = toFloat(o);
|
||||
out[0] = out[1] = v;
|
||||
|
||||
@@ -434,6 +434,19 @@ public class Mesh2D {
|
||||
markDirty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接设置此网格的活动顶点列表。
|
||||
* 这是从序列化数据恢复网格状态的首选方法,因为它可以保留每个顶点的完整信息。
|
||||
*
|
||||
* @param vertexList 包含完整顶点信息的新顶点列表。
|
||||
*/
|
||||
public void setActiveVertexList(VertexList vertexList) {
|
||||
if (vertexList != null) {
|
||||
this.activeVertexList = vertexList;
|
||||
markDirty(); // 标记网格需要更新
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否为渲染顶点
|
||||
*/
|
||||
|
||||
@@ -70,10 +70,20 @@ public class Model2D {
|
||||
// ==================== 光源系统 ====================
|
||||
private final List<LightSource> lights;
|
||||
|
||||
private final List<ModelEvent> events = new java.util.concurrent.CopyOnWriteArrayList<>();
|
||||
// ==================== 构造器 ====================
|
||||
public Model2D() {
|
||||
this.uuid = UUID.randomUUID();
|
||||
this.parts = new ArrayList<>();
|
||||
this.parts = new ArrayList<>() {
|
||||
@Override
|
||||
public boolean add(ModelPart modelPart) {triggerEvent("model_part_added");return super.add(modelPart);}
|
||||
|
||||
@Override
|
||||
public void add(int index, ModelPart element) {triggerEvent("model_part_added");super.add(index, element);}
|
||||
|
||||
@Override
|
||||
public ModelPart set(int index, ModelPart element) {triggerEvent("model_part_added");return super.set(index, element);}
|
||||
};
|
||||
this.partMap = new HashMap<>();
|
||||
this.meshes = new ArrayList<>();
|
||||
this.textures = new HashMap<>();
|
||||
@@ -193,6 +203,20 @@ public class Model2D {
|
||||
return part;
|
||||
}
|
||||
|
||||
private void triggerEvent(String eventName) {
|
||||
for (ModelEvent event : events) {
|
||||
if (event != null)
|
||||
event.trigger(eventName,this);
|
||||
}
|
||||
}
|
||||
|
||||
public void addEvent(ModelEvent event) {
|
||||
events.add(event);
|
||||
}
|
||||
|
||||
public void removeEvent(ModelEvent event) {
|
||||
events.remove(event);
|
||||
}
|
||||
public void addPart(ModelPart part) {
|
||||
if (partMap.containsKey(part.getName())) {
|
||||
throw new IllegalArgumentException("Part already exists: " + part.getName());
|
||||
@@ -204,6 +228,8 @@ public class Model2D {
|
||||
if (rootPart == null) {
|
||||
rootPart = part;
|
||||
}
|
||||
|
||||
triggerEvent("model_part_added");
|
||||
}
|
||||
|
||||
public ModelPart getPart(String name) {
|
||||
|
||||
@@ -1608,18 +1608,10 @@ public class ModelPart {
|
||||
for (Mesh2D mesh : meshes) {
|
||||
if (mesh == null) continue;
|
||||
for (Vertex vertex : mesh.getActiveVertexList()) {
|
||||
Vertex oldVertexCopy = vertex.copy();
|
||||
Vector2f localPoint = vertex.originalPosition;
|
||||
Vector2f worldPoint = Matrix3fUtils.transformPoint(this.worldTransform, localPoint);
|
||||
vertex.position.set(worldPoint);
|
||||
Map<String, Object> eventPayload = new HashMap<>();
|
||||
eventPayload.put("caller", this);
|
||||
eventPayload.put("oldVertex", oldVertexCopy);
|
||||
eventPayload.put("newVertex", vertex);
|
||||
for (ModelEvent event : events) {
|
||||
if (event != null)
|
||||
event.trigger("vertex", eventPayload);
|
||||
}
|
||||
triggerEvent("vertex_position");
|
||||
}
|
||||
mesh.markDirty();
|
||||
}
|
||||
|
||||
@@ -1,21 +1,34 @@
|
||||
package com.chuangzhou.vivid2D.render.model.data;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Vertex;
|
||||
import com.chuangzhou.vivid2D.render.model.util.VertexList;
|
||||
import com.chuangzhou.vivid2D.render.systems.Matrix3fUtils;
|
||||
import org.joml.Matrix3f;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Mesh2D的数据传输对象(DTO),用于序列化和反序列化网格数据。
|
||||
* 负责处理从世界坐标到局部坐标的转换,以便正确保存模型数据。
|
||||
*/
|
||||
public class MeshData implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final long serialVersionUID = 2L;
|
||||
|
||||
public VertexListData vertexListData;
|
||||
public VertexListData deformationControlVerticesData;
|
||||
public String name;
|
||||
public float[] vertices;
|
||||
public float[] uvs;
|
||||
public int[] indices;
|
||||
public String textureName;
|
||||
public boolean visible;
|
||||
public int drawMode;
|
||||
public float pivotX, pivotY;
|
||||
public float originalPivotX, originalPivotY;
|
||||
|
||||
public MeshData() {
|
||||
this.visible = true;
|
||||
@@ -23,35 +36,141 @@ public class MeshData implements Serializable {
|
||||
}
|
||||
|
||||
public MeshData(Mesh2D mesh) {
|
||||
this();
|
||||
this.name = mesh.getName();
|
||||
this.vertices = mesh.getVertices();
|
||||
this.uvs = mesh.getUVs();
|
||||
this.indices = mesh.getIndices();
|
||||
this.visible = mesh.isVisible();
|
||||
this.drawMode = mesh.getDrawMode();
|
||||
|
||||
if (mesh.getTexture() != null) {
|
||||
this.textureName = mesh.getTexture().getName();
|
||||
}
|
||||
|
||||
// ==================== 核心修复开始 ====================
|
||||
// 1. 获取逆变换矩阵:用于将 世界坐标 -> 局部坐标
|
||||
// 这是为了确保保存的是模型相对于父级的原始形状,而不是经过变换后的世界状态
|
||||
Matrix3f inverseTransform = new Matrix3f();
|
||||
ModelPart parentPart = mesh.getModelPart();
|
||||
|
||||
if (parentPart != null) {
|
||||
parentPart.getWorldTransform().invert(inverseTransform);
|
||||
}
|
||||
|
||||
// 2. 处理顶点数据 (Active Vertex List)
|
||||
// 我们必须创建一个顶点的深拷贝,以免修改运行时正在渲染的 Mesh2D 对象
|
||||
VertexList originalVertexList = mesh.getActiveVertexList();
|
||||
List<Vertex> localSpaceVertices = new ArrayList<>();
|
||||
|
||||
if (originalVertexList != null) {
|
||||
for (int i = 0; i < originalVertexList.size(); i++) {
|
||||
// 深拷贝顶点
|
||||
Vertex vCopy = originalVertexList.get(i).copy();
|
||||
|
||||
// 变换位置:pos = inverse * pos (将世界坐标还原为局部坐标)
|
||||
Matrix3fUtils.transformPoint(inverseTransform, vCopy.position);
|
||||
|
||||
// 重要:同时重置 originalPosition 为局部坐标
|
||||
// 这样加载后,deformation 算法才能基于正确的局部 bind-pose 进行计算
|
||||
vCopy.originalPosition.set(vCopy.position);
|
||||
|
||||
localSpaceVertices.add(vCopy);
|
||||
}
|
||||
}
|
||||
|
||||
// 重建一个临时的 VertexList 用于封装数据(保留原有的索引)
|
||||
VertexList tempLocalVertexList = new VertexList(
|
||||
originalVertexList != null ? originalVertexList.getName() : "temp",
|
||||
localSpaceVertices,
|
||||
originalVertexList != null ? originalVertexList.getIndices() : new int[0]
|
||||
);
|
||||
this.vertexListData = new VertexListData(tempLocalVertexList);
|
||||
|
||||
// 3. 处理变形控制点 (Deformation Control Vertices)
|
||||
// 控制点同样存在于世界空间,必须还原回局部
|
||||
if (mesh.getDeformationControlVertices() != null) {
|
||||
List<Vertex> tempControlVertices = new ArrayList<>();
|
||||
for (Vertex v : mesh.getDeformationControlVertices()) {
|
||||
// 深拷贝
|
||||
Vertex vCopy = v.copy();
|
||||
|
||||
// 还原坐标
|
||||
Matrix3fUtils.transformPoint(inverseTransform, vCopy.position);
|
||||
vCopy.originalPosition.set(vCopy.position); // 同步 original
|
||||
|
||||
tempControlVertices.add(vCopy);
|
||||
}
|
||||
// 重建 VertexList 结构用于存储
|
||||
VertexList controlListWrapper = new VertexList("control_points", tempControlVertices, new int[0]);
|
||||
this.deformationControlVerticesData = new VertexListData(controlListWrapper);
|
||||
}
|
||||
|
||||
// 4. 处理 Pivot (中心点)
|
||||
// Pivot 在 Mesh2D 中通常随着 ModelPart 变换被推到了世界坐标
|
||||
Vector2f currentPivot = new Vector2f(mesh.getPivot());
|
||||
Matrix3fUtils.transformPoint(inverseTransform, currentPivot); // 还原回局部
|
||||
this.pivotX = currentPivot.x;
|
||||
this.pivotY = currentPivot.y;
|
||||
|
||||
// OriginalPivot 通常本身就是局部的,但为了保险,我们使用刚刚逆变换计算出来的 currentPivot
|
||||
// 因为在未变形状态下,OriginalPivot 应该等于 Pivot
|
||||
this.originalPivotX = currentPivot.x;
|
||||
this.originalPivotY = currentPivot.y;
|
||||
// ==================== 核心修复结束 ====================
|
||||
}
|
||||
|
||||
public Mesh2D toMesh2D() {
|
||||
Mesh2D mesh = new Mesh2D(name, vertices, uvs, indices);
|
||||
mesh.setVisible(visible);
|
||||
mesh.setDrawMode(drawMode);
|
||||
Mesh2D mesh = new Mesh2D(this.name);
|
||||
|
||||
// 恢复顶点 (此时它们是局部坐标)
|
||||
if (this.vertexListData != null) {
|
||||
VertexList restoredVertexList = this.vertexListData.toVertexList();
|
||||
mesh.setActiveVertexList(restoredVertexList);
|
||||
|
||||
// 恢复控制点 (此时它们是局部坐标)
|
||||
if (this.deformationControlVerticesData != null) {
|
||||
VertexList restoredControlListData = this.deformationControlVerticesData.toVertexList();
|
||||
List<Vertex> controlVertices = new ArrayList<>();
|
||||
|
||||
// 尝试将控制点链接回主网格的顶点引用(如果索引匹配)
|
||||
// 这样拖动控制点时,主网格的顶点也会被标记为被控制
|
||||
for (Vertex restoredControlInfoVertex : restoredControlListData) {
|
||||
int originalIndex = restoredControlInfoVertex.getIndex();
|
||||
if (originalIndex >= 0 && originalIndex < restoredVertexList.size()) {
|
||||
Vertex actualVertexInMainList = restoredVertexList.get(originalIndex);
|
||||
// 确保位置一致(理论上现在都是局部坐标,应该是一致的)
|
||||
controlVertices.add(actualVertexInMainList);
|
||||
} else {
|
||||
// 如果是独立的控制点(不在网格上,或者 cage 顶点),则直接添加
|
||||
controlVertices.add(restoredControlInfoVertex);
|
||||
}
|
||||
}
|
||||
mesh.setDeformationControlVertices(controlVertices);
|
||||
}
|
||||
}
|
||||
|
||||
mesh.setVisible(this.visible);
|
||||
mesh.setDrawMode(this.drawMode);
|
||||
|
||||
// 恢复 Pivot (局部坐标)
|
||||
// 注意:这里设置的是局部 Pivot。
|
||||
// 当 mesh 被 addMesh 加入 ModelPart 时,ModelPart 会根据自身的变换
|
||||
// 将这个局部 Pivot 再次推算到新的世界 Pivot 位置。
|
||||
mesh.setPivot(this.pivotX, this.pivotY);
|
||||
mesh.setOriginalPivot(new Vector2f(this.originalPivotX, this.originalPivotY));
|
||||
|
||||
mesh.markDirty();
|
||||
return mesh;
|
||||
}
|
||||
|
||||
public MeshData copy() {
|
||||
MeshData copy = new MeshData();
|
||||
copy.name = this.name;
|
||||
copy.vertices = this.vertices != null ? this.vertices.clone() : null;
|
||||
copy.uvs = this.uvs != null ? this.uvs.clone() : null;
|
||||
copy.indices = this.indices != null ? this.indices.clone() : null;
|
||||
copy.textureName = this.textureName;
|
||||
copy.visible = this.visible;
|
||||
copy.drawMode = this.drawMode;
|
||||
return copy;
|
||||
MeshData c = new MeshData();
|
||||
c.vertexListData = this.vertexListData != null ? this.vertexListData.copy() : null;
|
||||
c.deformationControlVerticesData = this.deformationControlVerticesData != null ? this.deformationControlVerticesData.copy() : null;
|
||||
c.name = this.name;
|
||||
c.textureName = this.textureName;
|
||||
c.visible = this.visible;
|
||||
c.drawMode = this.drawMode;
|
||||
c.pivotX = this.pivotX;
|
||||
c.pivotY = this.pivotY;
|
||||
c.originalPivotX = this.originalPivotX;
|
||||
c.originalPivotY = this.originalPivotY;
|
||||
return c;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.chuangzhou.vivid2D.render.model.data;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.util.Vertex;
|
||||
import com.chuangzhou.vivid2D.render.model.util.VertexTag;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class VertexData implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public float posX, posY;
|
||||
public float uvX, uvY;
|
||||
public float originalPosX, originalPosY;
|
||||
public VertexTag tag;
|
||||
public boolean selected;
|
||||
public String name;
|
||||
public boolean isDelete;
|
||||
public int index;
|
||||
public List<Integer> controlledTriangles;
|
||||
|
||||
public VertexData() {
|
||||
this.controlledTriangles = new ArrayList<>();
|
||||
}
|
||||
|
||||
public VertexData(Vertex vertex) {
|
||||
this.posX = vertex.position.x;
|
||||
this.posY = vertex.position.y;
|
||||
this.uvX = vertex.uv.x;
|
||||
this.uvY = vertex.uv.y;
|
||||
this.originalPosX = vertex.originalPosition.x;
|
||||
this.originalPosY = vertex.originalPosition.y;
|
||||
this.tag = vertex.getTag();
|
||||
this.selected = vertex.isSelected();
|
||||
this.name = vertex.getName();
|
||||
this.isDelete = vertex.isDelete();
|
||||
this.index = vertex.getIndex();
|
||||
this.controlledTriangles = new ArrayList<>(vertex.getControlledTriangles());
|
||||
}
|
||||
|
||||
public Vertex toVertex() {
|
||||
Vertex vertex = new Vertex(
|
||||
new Vector2f(posX, posY),
|
||||
new Vector2f(uvX, uvY),
|
||||
new Vector2f(originalPosX, originalPosY)
|
||||
);
|
||||
vertex.setTag(this.tag);
|
||||
vertex.setSelected(this.selected);
|
||||
vertex.setName(this.name);
|
||||
vertex.setIndex(this.index);
|
||||
vertex.setControlledTriangles(this.controlledTriangles);
|
||||
if (this.isDelete) {
|
||||
vertex.delete();
|
||||
}
|
||||
return vertex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建此对象的深拷贝。
|
||||
* Creates a deep copy of this object.
|
||||
* @return a new, independent VertexData instance.
|
||||
*/
|
||||
public VertexData copy() {
|
||||
VertexData c = new VertexData();
|
||||
c.posX = this.posX;
|
||||
c.posY = this.posY;
|
||||
c.uvX = this.uvX;
|
||||
c.uvY = this.uvY;
|
||||
c.originalPosX = this.originalPosX;
|
||||
c.originalPosY = this.originalPosY;
|
||||
c.tag = this.tag; // Enums are immutable, direct copy is fine
|
||||
c.selected = this.selected;
|
||||
c.name = this.name; // Strings are immutable, direct copy is fine
|
||||
c.isDelete = this.isDelete;
|
||||
c.index = this.index;
|
||||
// Create a new list instance to ensure the copy is independent
|
||||
c.controlledTriangles = new ArrayList<>(this.controlledTriangles);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.chuangzhou.vivid2D.render.model.data;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.util.Vertex;
|
||||
import com.chuangzhou.vivid2D.render.model.util.VertexList;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class VertexListData implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public String name;
|
||||
public List<VertexData> vertices;
|
||||
public int[] indices;
|
||||
|
||||
public VertexListData() {
|
||||
this.vertices = new ArrayList<>();
|
||||
}
|
||||
|
||||
public VertexListData(VertexList vertexList) {
|
||||
this.name = vertexList.getName();
|
||||
this.indices = vertexList.getIndices();
|
||||
this.vertices = vertexList.vertices.stream()
|
||||
.map(VertexData::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public VertexList toVertexList() {
|
||||
List<Vertex> restoredVertices = this.vertices.stream()
|
||||
.map(VertexData::toVertex)
|
||||
.collect(Collectors.toList());
|
||||
return new VertexList(this.name, restoredVertices, this.indices);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建此对象的深拷贝。
|
||||
* Creates a deep copy of this object.
|
||||
* @return a new, independent VertexListData instance.
|
||||
*/
|
||||
public VertexListData copy() {
|
||||
VertexListData c = new VertexListData();
|
||||
c.name = this.name;
|
||||
// The indices array must be cloned to be independent
|
||||
c.indices = this.indices != null ? this.indices.clone() : null;
|
||||
// Create a new list and fill it with copies of each VertexData
|
||||
if (this.vertices != null) {
|
||||
c.vertices = this.vertices.stream()
|
||||
.map(VertexData::copy) // Call copy on each element
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
return c;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package com.chuangzhou.vivid2D.render.model.util;
|
||||
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@@ -9,9 +11,12 @@ import java.util.Objects;
|
||||
/**
|
||||
* 封装一个2D顶点,包含位置、UV坐标和原始位置。
|
||||
*
|
||||
* @author Gemini
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class Vertex {
|
||||
public class Vertex implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private List<Integer> controlledTriangles = new ArrayList<>();
|
||||
public Vector2f position; // 当前顶点位置 (x, y)
|
||||
public Vector2f uv; // UV坐标 (u, v)
|
||||
@@ -147,8 +152,15 @@ public class Vertex {
|
||||
copy.setTag(this.tag);
|
||||
copy.setSelected(this.selected);
|
||||
copy.setName(this.name);
|
||||
copy.setControlledTriangles(this.controlledTriangles);
|
||||
copy.setIndex(this.index);
|
||||
if (this.controlledTriangles != null) {
|
||||
copy.setControlledTriangles(new ArrayList<>(this.controlledTriangles));
|
||||
} else {
|
||||
copy.setControlledTriangles(new ArrayList<>());
|
||||
}
|
||||
if (this.isDelete) {
|
||||
copy.delete();
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
@@ -166,6 +178,16 @@ public class Vertex {
|
||||
Objects.equals(name, vertex.name);
|
||||
}
|
||||
|
||||
public boolean _equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Vertex vertex = (Vertex) o;
|
||||
return isDelete == vertex.isDelete &&
|
||||
Objects.equals(position, vertex.position) &&
|
||||
tag == vertex.tag &&
|
||||
Objects.equals(name, vertex.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(position, uv, originalPosition, tag, selected, name, isDelete);
|
||||
|
||||
@@ -497,6 +497,7 @@ public class MainWindow extends JFrame {
|
||||
} catch (ExecutionException e) {
|
||||
// 实际异常被包装在 ExecutionException 中
|
||||
Throwable cause = e.getCause();
|
||||
e.printStackTrace();
|
||||
System.err.println("保存失败: " + cause.getMessage());
|
||||
statusBarLabel.setText("保存失败!错误: " + cause.getMessage());
|
||||
JOptionPane.showMessageDialog(MainWindow.this,
|
||||
|
||||
Reference in New Issue
Block a user