From 75bdca05f210c0eb2d699c057b3a410f7c85a8bf Mon Sep 17 00:00:00 2001 From: tzdwindows 7 <3076584115@qq.com> Date: Sat, 22 Nov 2025 08:45:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(browser):=20=E6=B7=BB=E5=8A=A0=20BrowserWi?= =?UTF-8?q?ndow=20=E5=92=8C=20BrowserWindowJDialog=20=E7=B1=BB=E4=BB=A5?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=B5=8C=E5=85=A5=E5=BC=8F=E6=B5=8F=E8=A7=88?= =?UTF-8?q?=E5=99=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 BrowserWindow 类,支持通过 Builder 模式创建可定制的浏览器窗口 - 新增 BrowserWindowJDialog 类,继承自 JDialog,用于创建模态或非模态浏览器对话框 - 实现基于 CEF 的浏览器组件加载与生命周期管理 - 支持自定义上下文菜单、键盘事件(如 F12 开发者工具)、JS 对话框拦截 - 提供链接打开方式配置(在当前窗口或外部浏览器中打开) - 集成消息路由机制,支持前端与后端通信 - 支持主题与字体信息注入至网页端 - 添加资源自动释放逻辑,防止内存泄漏 - 增加对粘贴板操作的支持(复制/粘贴文本) --- .../vivid2D/browser/BrowserWindow.java | 821 +++++++ .../vivid2D/browser/BrowserWindowJDialog.java | 833 +++++++ .../vivid2D/browser/CefAppManager.java | 280 +++ .../vivid2D/browser/MainApplication.java | 2002 +++++++++++++++++ .../vivid2D/browser/WindowOperation.java | 9 + .../browser/WindowOperationHandler.java | 94 + .../vivid2D/browser/WindowRegistry.java | 153 ++ .../vivid2D/browser/util/CodeExecutor.java | 346 +++ .../util/DatabaseConnectionManager.java | 399 ++++ .../vivid2D/render/awt/EventPanel.java | 6 + .../awt/manager/LayerOperationManager.java | 1 - .../awt/manager/ParametersManagement.java | 140 +- .../data/ParametersManagementData.java | 161 +- .../render/awt/tools/SelectionTool.java | 11 +- .../awt/tools/VertexDeformationTool.java | 10 +- .../render/awt/util/FrameInterpolator.java | 15 +- .../vivid2D/render/model/Mesh2D.java | 13 + .../vivid2D/render/model/Model2D.java | 28 +- .../vivid2D/render/model/ModelPart.java | 10 +- .../vivid2D/render/model/data/MeshData.java | 163 +- .../vivid2D/render/model/data/VertexData.java | 84 + .../render/model/data/VertexListData.java | 57 + .../vivid2D/render/model/util/Vertex.java | 28 +- .../chuangzhou/vivid2D/window/MainWindow.java | 1 + 24 files changed, 5462 insertions(+), 203 deletions(-) create mode 100644 src/main/java/com/chuangzhou/vivid2D/browser/BrowserWindow.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/browser/BrowserWindowJDialog.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/browser/CefAppManager.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/browser/MainApplication.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/browser/WindowOperation.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/browser/WindowOperationHandler.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/browser/WindowRegistry.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/browser/util/CodeExecutor.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/browser/util/DatabaseConnectionManager.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/awt/EventPanel.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/model/data/VertexData.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/model/data/VertexListData.java diff --git a/src/main/java/com/chuangzhou/vivid2D/browser/BrowserWindow.java b/src/main/java/com/chuangzhou/vivid2D/browser/BrowserWindow.java new file mode 100644 index 0000000..cdb3e59 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/browser/BrowserWindow.java @@ -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 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); + }); + } +} + diff --git a/src/main/java/com/chuangzhou/vivid2D/browser/BrowserWindowJDialog.java b/src/main/java/com/chuangzhou/vivid2D/browser/BrowserWindowJDialog.java new file mode 100644 index 0000000..2a4f4e6 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/browser/BrowserWindowJDialog.java @@ -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 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); + }); + } +} + diff --git a/src/main/java/com/chuangzhou/vivid2D/browser/CefAppManager.java b/src/main/java/com/chuangzhou/vivid2D/browser/CefAppManager.java new file mode 100644 index 0000000..f858675 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/browser/CefAppManager.java @@ -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()); + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/browser/MainApplication.java b/src/main/java/com/chuangzhou/vivid2D/browser/MainApplication.java new file mode 100644 index 0000000..810f6c4 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/browser/MainApplication.java @@ -0,0 +1,2002 @@ +package com.chuangzhou.vivid2D.browser; + +import com.axis.innovators.box.AxisInnovatorsBox; +import com.axis.innovators.box.tools.FolderCreator; +import com.chuangzhou.vivid2D.browser.util.CodeExecutor; +import com.chuangzhou.vivid2D.browser.util.DatabaseConnectionManager; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.cef.browser.CefBrowser; +import org.cef.browser.CefFrame; +import org.cef.browser.CefMessageRouter; +import org.cef.callback.CefQueryCallback; +import org.cef.handler.CefMessageRouterHandlerAdapter; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Value; +import org.json.JSONArray; +import org.json.JSONObject; +import org.tzd.lm.LM; + +import javax.swing.*; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.sql.*; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; + +/** + * 这是一个简单的示例程序,用于展示如何使用JCEF来创建一个简单的浏览器窗口。 + * @author tzdwindows 7 + */ + +public class MainApplication { + private static final ExecutorService executor = Executors.newCachedThreadPool(); + private static long modelHandle; + private static long ctxHandle; + private static boolean isSystem = true; + public static void main(String[] args) { + AtomicReference window = new AtomicReference<>(); + WindowRegistry.getInstance().createNewWindow("main", builder -> + window.set(builder.title("Axis Innovators Box AI 工具箱") + .size(1280, 720) + .htmlUrl("https://www.bilibili.com/") + .openLinksInBrowser(true) + .operationHandler(createOperationHandler()) + .build()) + ); + } + + + /** + * 弹出AI窗口 + * @param parent 父窗口 + */ + public static void popupAIWindow(JFrame parent) { + LM.loadLibrary(LM.CUDA); + modelHandle = LM.llamaLoadModelFromFile(LM.DEEP_SEEK); + ctxHandle = LM.createContext(modelHandle); + + AtomicReference window = new AtomicReference<>(); + SwingUtilities.invokeLater(() -> { + WindowRegistry.getInstance().createNewChildWindow("main", builder -> + window.set(builder.title("Axis Innovators Box AI 工具箱") + .parentFrame(parent) + .icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage()) + .size(1280, 720) + .htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "AIaToolbox_dark.html") + .operationHandler(createOperationHandler()) + .build()) + ); + + CefMessageRouter msgRouter = window.get().getMsgRouter(); + if (msgRouter != null) { + msgRouter.addHandler(new CefMessageRouterHandlerAdapter() { + @Override + public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, + String request, boolean persistent, CefQueryCallback callback) { + // 处理浏览器请求 + handleBrowserQuery(browser, request, callback); + return true; + } + + @Override + public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) { + // 处理请求取消 + } + }, true); + } + + }); + } + + public static void popupCCodeEditorWindow() { + AtomicReference window = new AtomicReference<>(); + SwingUtilities.invokeLater(() -> { + WindowRegistry.getInstance().createNewWindow("main", builder -> + window.set(builder.title("TzdC 代码编辑器") + .icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage()) + .size(1487, 836) + .htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "CCodeEditor.html") + .operationHandler(createOperationHandler()) + .build()) + ); + + CefMessageRouter msgRouter = window.get().getMsgRouter(); + if (msgRouter != null) { + msgRouter.addHandler(new CefMessageRouterHandlerAdapter() { + @Override + public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, + String request, boolean persistent, CefQueryCallback callback) { + try { + JSONObject requestJson = new JSONObject(request); + if ("executeCode".equals(requestJson.optString("type"))) { + String code = requestJson.optString("code"); + String language = requestJson.optString("language"); + + // 调用代码执行逻辑 + String result = CodeExecutor.executeCode(code, language,null); + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("output", result); + callback.success(response.toString()); + return true; + } + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", e.getMessage()); + callback.failure(500, error.toString()); + } + return false; + } + + @Override + public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) { + // 处理请求取消 + } + }, true); + } + }); + } + + public static void popupCodeEditorWindow() { + AtomicReference window = new AtomicReference<>(); + SwingUtilities.invokeLater(() -> { + WindowRegistry.getInstance().createNewWindow("main", builder -> + window.set(builder.title("代码编辑器") + .icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage()) + .size(1487, 836) + .htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "CodeEditor.html") + .operationHandler(createOperationHandler()) + .build()) + ); + + CefMessageRouter msgRouter = window.get().getMsgRouter(); + if (msgRouter != null) { + msgRouter.addHandler(new CefMessageRouterHandlerAdapter() { + @Override + public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, + String request, boolean persistent, CefQueryCallback callback) { + try { + JSONObject requestJson = new JSONObject(request); + if ("executeCode".equals(requestJson.optString("type"))) { + String code = requestJson.optString("code"); + String language = requestJson.optString("language"); + + // 调用代码执行逻辑 + String result = executeCode(code, language); + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("output", result); + callback.success(response.toString()); + return true; + } + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", e.getMessage()); + callback.failure(500, error.toString()); + } + return false; + } + + @Override + public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) { + // 处理请求取消 + } + }, true); + } + }); + } + + public static String executeCode(String code, String language) { + return CodeExecutor.executeCode(code, language,new CodeExecutor.OutputListener() { + @Override + public void onOutput(String newOutput) {} + }); + } + + private static Value executeC(Context context, String code) { + return context.eval("c", code); + } + + /** + * 弹出html预览窗口 + */ + public static void popupHTMLWindow(String path) { + AtomicReference window = new AtomicReference<>(); + SwingUtilities.invokeLater(() -> { + WindowRegistry.getInstance().createNewWindow("main", builder -> + window.set(builder.title("Axis Innovators Box HTML查看器") + .icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage()) + .size(1487, 836) + .htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "HtmlViewer.html") + .operationHandler(createOperationHandler()) + .build()) + ); + + CefMessageRouter msgRouter = window.get().getMsgRouter(); + if (msgRouter != null) { + msgRouter.addHandler(new CefMessageRouterHandlerAdapter() { + @Override + public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, + String request, boolean persistent, CefQueryCallback callback) { + try { + // 解析JSON请求 + JsonObject requestJson = JsonParser.parseString(request).getAsJsonObject(); + if (requestJson.has("type") && "loadInitialContent".equals(requestJson.get("type").getAsString())) { + + Path filePath = Paths.get(path); + + // 验证文件存在性 + if (!Files.exists(filePath)) { + callback.failure(404, "{\"code\":404,\"message\":\"文件未找到\"}"); + return true; + } + + // 读取文件内容 + String content = Files.readString(filePath, StandardCharsets.UTF_8); + callback.success(content); + return true; + } + } catch (Exception e) { + callback.failure(500, "{\"code\":500,\"message\":\"" + e.getMessage() + "\"}"); + } + return false; + } + + @Override + public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) { + // 处理请求取消 + } + }, true); + } + }); + } + + private static void handleBrowserQuery(CefBrowser browser, String request, CefQueryCallback callback) { + try { + String[] parts = request.split(":", 3); + if (parts.length < 3) { + callback.failure(400, "请求格式错误"); + return; + } + + String operation = parts[0]; + String requestId = parts[1]; + String prompt = parts[2]; + + if ("ai-inference".equals(operation)) { + executor.execute(() -> { + if (isSystem) { + isSystem = false; + } + List messageList = new ArrayList<>(List.of()); + // 修改后的推理回调处理 + String jsCode = String.format( + "if (typeof updateResponse === 'function') {" + + " updateResponse('%s', '%s');" + + "}", + requestId, "
" + + "\\n" + + "推理内容" + ); + browser.executeJavaScript(jsCode, null, 0); + LM.inference(modelHandle, ctxHandle, 0.6f, prompt + "\n","", + new LM.MessageCallback() { + private boolean thinkingClosed = false; +// + @Override + public void onMessage(String message) { + messageList.add(message); + SwingUtilities.invokeLater(() -> { + // 统一转义处理 + String escaped = message + .replace("\\", "\\\\") + .replace("'", "\\'") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r"); +// + if (messageList.contains("") && !thinkingClosed) { + String endJs = String.format( + "if (typeof updateResponse === 'function') {" + + " updateResponse('%s', '%s');" + + "}", + requestId, "
" + ); + browser.executeJavaScript(endJs, null, 0); + thinkingClosed = true; + } +// + // 实时更新内容 + System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8)); +// + String jsCode = String.format( + "if (typeof updateResponse === 'function') {" + + " updateResponse('%s', '%s');" + + "}", + requestId, escaped + ); + browser.executeJavaScript(jsCode, null, 0); + }); + } + },isSystem); + messageList.clear(); + //jsCode = String.format( + // "if (typeof updateResponse === 'function') {" + + // " updateResponse('%s', '%s');" + + // "}", + // requestId, "嗯,用户问的是“请直接告诉我傅里叶变换公式”。首先,我需要回忆一下傅里叶变换的基本知识。傅里叶变换是将一个时间域的信号转换为频率域的信号,它在工程和科学研究中有着广泛的应用。\n\n接下来,我要确定傅里叶变换的数学表达式。标准形式应该是$$F(\\omega) = \\int_{-\\infty}^{\\infty} f(t) e^{-i\\omega t} dt$$。这里,$f(t)$是原函数,$e^{-i\\omega t}$是指数函数,$\\omega$是频率变量。\n\n然后,我需要考虑是否有其他形式的傅里叶变换,比如离散形式或逆变换。通常,离散傅里叶变换(DFT)使用$$X[k] = \\sum_{n=0}^{N-1} x[n] e^{-i2\\pi kn/N}$$来表示,而逆变换则是$$x[n] = \\frac{1}{N} \\sum_{k=0}^{N-1} X[k] e^{i2\\pi kn/N}$$。不过,用户的问题比较直接,可能只关注基本的连续形式。\n\n最后,我要确保回答准确无误,并且按照用户的格式要求使用标准的 LaTeX æ式来呈现。\n\n\n傅里叶变换的基本公式是:$$F(\\omega) = \\int_{-\\infty}^{\\infty} f(t) e^{-i\\omega t} dt$$".replace("\\", "\\\\") + // .replace("'", "\\'") + // .replace("\"", "\\\"") + // .replace("\n", "\\n") + // .replace("\r", "\\r") + //); + //browser.executeJavaScript(jsCode, null, 0); + callback.success("COMPLETED:" + requestId); + }); + } + } catch (Exception e) { + callback.failure(500, "服务器错误: " + e.getMessage()); + } + } + + private static WindowOperationHandler createOperationHandler() { + return new WindowOperationHandler.Builder() + .withDefaultOperations() + .build(); + } + + public static void popupDataBaseWindow() { + // 预加载常用 JDBC 驱动(警告级别,不阻塞 UI) + try { + try { Class.forName("org.h2.Driver"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: org.h2.Driver 未找到"); } + try { Class.forName("org.sqlite.JDBC"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: org.sqlite.JDBC 未找到"); } + try { Class.forName("org.postgresql.Driver"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: org.postgresql.Driver 未找到"); } + try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: com.mysql.cj.jdbc.Driver 未找到"); } + try { Class.forName("oracle.jdbc.OracleDriver"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: oracle.jdbc.OracleDriver 未找到"); } + } catch (Throwable t) { + System.err.println("预加载 JDBC 驱动时发生异常: " + t.getMessage()); + } + + AtomicReference window = new AtomicReference<>(); + SwingUtilities.invokeLater(() -> { + WindowRegistry.getInstance().createNewWindow("main", builder -> + window.set(builder.title("Axis Innovators Box 数据库管理工具") + .icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage()) + .size(1487, 836) + .htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "DatabaseTool.html") + .operationHandler(createOperationHandler()) + .build()) + ); + + if (window.get() == null) { + System.err.println("popupDataBaseWindow: window 创建失败,window.get() == null"); + return; + } + + CefMessageRouter msgRouter = window.get().getMsgRouter(); + if (msgRouter != null) { + msgRouter.addHandler(new CefMessageRouterHandlerAdapter() { + @Override + public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, + String request, boolean persistent, CefQueryCallback callback) { + try { + JSONObject requestJson = new JSONObject(request); + String type = requestJson.optString("type", ""); + + // 补默认端口与标准化 driver 名称 + String drv = requestJson.optString("driver", "").toLowerCase(); + if (!drv.isEmpty()) { + String port = requestJson.optString("port", "").trim(); + if (port.isEmpty()) { + switch (drv) { + case "mysql": + requestJson.put("port", "3306"); + break; + case "postgresql": + case "postgres": + requestJson.put("port", "5432"); + break; + case "oracle": + requestJson.put("port", "1521"); + break; + } + } + if ("postgres".equals(drv)) requestJson.put("driver", "postgresql"); + } + + // 安全标识符校验 pattern(表名/列名等仅允许常见字符) + final java.util.regex.Pattern SAFE_IDENT = java.util.regex.Pattern.compile("^[A-Za-z0-9_\\.\\$]+$"); + + switch (type) { + // 已有功能:继续使用现有处理函数(假定这些方法在类中定义) + case "connectDatabase": + handleDatabaseConnect(requestJson, callback); + break; + case "createLocalDatabase": + handleCreateLocalDatabase(requestJson, callback); + break; + case "disconnectDatabase": + handleDisconnectDatabase(requestJson, callback); + break; + case "executeQuery": + handleExecuteQuery(requestJson, callback); + break; + case "getTables": + handleGetTables(requestJson, callback); + break; + case "getTableData": + handleGetTableData(requestJson, callback); + break; + case "getTableStructure": + handleGetTableStructure(requestJson, callback); + break; + case "updateTheme": + handleUpdateTheme(requestJson, callback); + break; + case "getFonts": + handleGetFonts(requestJson, callback); + break; + + // 新增后端支持:analyzeQuery(EXPLAIN / EXPLAIN ANALYZE) + case "analyzeQuery": { + String connectionId = requestJson.optString("connectionId", ""); + String query = requestJson.optString("query", "").trim(); + if (connectionId.isEmpty() || query.isEmpty()) { + callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 或 query 为空").toString()); + break; + } + Connection conn = DatabaseConnectionManager.getConnection(connectionId); + if (conn == null || conn.isClosed()) { + callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); + break; + } + // 依据驱动选择 EXPLAIN 语法 + DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); + String drvName = info == null ? "" : (info.driver == null ? "" : info.driver.toLowerCase()); + String explainSql = "EXPLAIN " + query; + if ("postgresql".equals(drvName)) { + explainSql = "EXPLAIN ANALYZE " + query; + } + try (Statement st = conn.createStatement(); + ResultSet rs = st.executeQuery(explainSql)) { + JSONArray out = new JSONArray(); + while (rs.next()) { + // EXPLAIN 输出常为单列文本 + out.put(rs.getString(1)); + } + JSONObject resp = new JSONObject(); + resp.put("status","success"); + resp.put("explain", out); + callback.success(resp.toString()); + } catch (SQLException ex) { + JSONObject err = new JSONObject(); + err.put("status","error"); + err.put("message","分析失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } + + // exportData -> 导出为 CSV 或 JSON,写入用户目录下 .axis_innovators_box/exports/ + case "exportData": { + String connectionId = requestJson.optString("connectionId", ""); + String table = requestJson.optString("table", ""); + String format = requestJson.optString("format", "csv").toLowerCase(); + if (connectionId.isEmpty() || table.isEmpty()) { + callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 或 table 为空").toString()); + break; + } + if (!SAFE_IDENT.matcher(table).matches()) { + callback.failure(400, new JSONObject().put("status","error").put("message","非法表名").toString()); + break; + } + Connection conn = DatabaseConnectionManager.getConnection(connectionId); + if (conn == null || conn.isClosed()) { + callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); + break; + } + + Path exportDir = Paths.get(System.getProperty("user.home"), ".axis_innovators_box", "exports"); + try { Files.createDirectories(exportDir); } catch (Exception e) { /* ignore */ } + + String filenameBase = table + "_" + System.currentTimeMillis(); + Path outPath = exportDir.resolve(filenameBase + (format.equals("json") ? ".json" : ".csv")); + + String query = "SELECT * FROM " + table; + try (Statement st = conn.createStatement(); + ResultSet rs = st.executeQuery(query)) { + + ResultSetMetaData md = rs.getMetaData(); + int cols = md.getColumnCount(); + + if ("json".equals(format)) { + JSONArray arr = new JSONArray(); + while (rs.next()) { + JSONObject obj = new JSONObject(); + for (int i = 1; i <= cols; i++) { + Object val = rs.getObject(i); + obj.put(md.getColumnLabel(i), val == null ? JSONObject.NULL : val); + } + arr.put(obj); + } + Files.write(outPath, arr.toString(2).getBytes(StandardCharsets.UTF_8)); + } else { + try (java.io.BufferedWriter writer = Files.newBufferedWriter( + outPath, + StandardCharsets.UTF_8 + )) { + // 写入 UTF-8 BOM + writer.write('\uFEFF'); + + for (int i = 1; i <= cols; i++) { + if (i > 1) writer.write(","); + writer.write("\"" + md.getColumnLabel(i).replace("\"", "\"\"") + "\""); + } + writer.write("\n"); + + while (rs.next()) { + for (int i = 1; i <= cols; i++) { + if (i > 1) writer.write(","); + Object val = rs.getObject(i); + String cell = val == null ? "" : String.valueOf(val); + writer.write("\"" + cell.replace("\"", "\"\"") + "\""); + } + writer.write("\n"); + } + } + + } + + try { + String pathStr = outPath.toAbsolutePath().toString(); + String os = System.getProperty("os.name").toLowerCase(); + if (os.contains("win")) { + new ProcessBuilder("explorer.exe", "/select," + pathStr).start(); + } + } catch (Exception ignore) { + } + + JSONObject resp = new JSONObject(); + resp.put("status","success"); + resp.put("path", outPath.toAbsolutePath().toString()); + resp.put("message","导出成功"); + callback.success(resp.toString()); + } catch (SQLException | java.io.IOException ex) { + JSONObject err = new JSONObject(); + err.put("status","error"); + err.put("message","导出失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } + + // importCsv -> 从给定 path 导入 CSV,要求首行为列名且列名匹配表字段 + case "importCsv": { + String connectionId = requestJson.optString("connectionId", ""); + String table = requestJson.optString("table", ""); + String path = requestJson.optString("path", ""); + if (connectionId.isEmpty() || table.isEmpty() || path.isEmpty()) { + callback.failure(400, new JSONObject().put("status","error").put("message","参数不完整").toString()); + break; + } + if (!SAFE_IDENT.matcher(table).matches()) { + callback.failure(400, new JSONObject().put("status","error").put("message","非法表名").toString()); + break; + } + Path csvPath = Paths.get(path); + if (!Files.exists(csvPath)) { + callback.failure(400, new JSONObject().put("status","error").put("message","CSV 文件不存在").toString()); + break; + } + Connection conn = DatabaseConnectionManager.getConnection(connectionId); + if (conn == null || conn.isClosed()) { + callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); + break; + } + + // 读取首行作为列头 + try (java.io.BufferedReader br = Files.newBufferedReader(csvPath, StandardCharsets.UTF_8)) { + String headerLine = br.readLine(); + if (headerLine == null) { + callback.failure(400, new JSONObject().put("status","error").put("message","CSV 为空").toString()); + break; + } + // 简单 CSV 解析(支持双引号),但要求列名没有逗号内部双引号结构复杂情形 + String[] columns = headerLine.split(","); + for (int i = 0; i < columns.length; i++) { + columns[i] = columns[i].trim().replaceAll("^\"|\"$", ""); // 去掉可能的两端引号 + if (!SAFE_IDENT.matcher(columns[i]).matches()) { + callback.failure(400, new JSONObject().put("status","error").put("message","非法列名: " + columns[i]).toString()); + return true; + } + } + // 构建 INSERT SQL + StringBuilder placeholders = new StringBuilder(); + for (int i = 0; i < columns.length; i++) { + if (i > 0) placeholders.append(","); + placeholders.append("?"); + } + String insertSql = "INSERT INTO " + table + " (" + String.join(",", columns) + ") VALUES (" + placeholders.toString() + ")"; + conn.setAutoCommit(false); + int imported = 0; + try (PreparedStatement pstmt = conn.prepareStatement(insertSql)) { + String line; + while ((line = br.readLine()) != null) { + // 简单分割(不处理复杂引号内部逗号) + String[] parts = line.split(",", -1); + for (int i = 0; i < columns.length; i++) { + String cell = i < parts.length ? parts[i].trim().replaceAll("^\"|\"$", "") : ""; + pstmt.setString(i + 1, cell.isEmpty() ? null : cell); + } + pstmt.addBatch(); + if (++imported % 500 == 0) pstmt.executeBatch(); + } + pstmt.executeBatch(); + conn.commit(); + } catch (SQLException ex) { + conn.rollback(); + throw ex; + } finally { + conn.setAutoCommit(true); + } + JSONObject resp = new JSONObject(); + resp.put("status","success"); + resp.put("imported", imported); + resp.put("message", "导入完成"); + callback.success(resp.toString()); + } catch (Exception ex) { + JSONObject err = new JSONObject(); + err.put("status","error"); + err.put("message","导入失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } + + // generateEr -> 收集表、列信息并返回 JSON(前端可据此生成 ER 图) + case "generateEr": { + String connectionId = requestJson.optString("connectionId", ""); + if (connectionId.isEmpty()) { + callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 为空").toString()); + break; + } + Connection conn = DatabaseConnectionManager.getConnection(connectionId); + if (conn == null || conn.isClosed()) { + callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); + break; + } + try { + DatabaseMetaData meta = conn.getMetaData(); + JSONArray tablesArr = new JSONArray(); + + try (ResultSet rsTables = meta.getTables(conn.getCatalog(), null, null, new String[]{"TABLE"})) { + while (rsTables.next()) { + String tbl = rsTables.getString("TABLE_NAME"); + JSONObject tObj = new JSONObject(); + tObj.put("name", tbl); + JSONArray cols = new JSONArray(); + try (ResultSet rsCols = meta.getColumns(conn.getCatalog(), null, tbl, null)) { + while (rsCols.next()) { + JSONObject c = new JSONObject(); + c.put("name", rsCols.getString("COLUMN_NAME")); + c.put("type", rsCols.getString("TYPE_NAME")); + c.put("size", rsCols.getInt("COLUMN_SIZE")); + c.put("nullable", rsCols.getInt("NULLABLE") == DatabaseMetaData.columnNullable); + cols.put(c); + } + } + tObj.put("columns", cols); + tablesArr.put(tObj); + } + } + + JSONObject resp = new JSONObject(); + resp.put("status","success"); + resp.put("er", new JSONObject().put("tables", new JSONArray(tablesArr.toString()))); + callback.success(resp.toString()); + } catch (SQLException ex) { + JSONObject err = new JSONObject(); + err.put("status","error"); + err.put("message","生成 ER 失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } + + // analyzePerformance -> 尝试返回当前数据库会话/进程信息(简单实现,依据数据库类型) + case "analyzePerformance": { + String connectionId = requestJson.optString("connectionId", ""); + if (connectionId.isEmpty()) { + callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 为空").toString()); + break; + } + Connection conn = DatabaseConnectionManager.getConnection(connectionId); + if (conn == null || conn.isClosed()) { + callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); + break; + } + DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); + String drvName = info == null ? "" : (info.driver == null ? "" : info.driver.toLowerCase()); + try { + JSONArray out = new JSONArray(); + if ("postgresql".equals(drvName)) { + try (Statement st = conn.createStatement(); + ResultSet rs = st.executeQuery("SELECT pid, usename, state, query FROM pg_stat_activity LIMIT 50")) { + while (rs.next()) { + JSONObject r = new JSONObject(); + r.put("pid", rs.getObject("pid")); + r.put("user", rs.getString("usename")); + r.put("state", rs.getString("state")); + r.put("query", rs.getString("query")); + out.put(r); + } + } + } else if ("mysql".equals(drvName)) { + try (Statement st = conn.createStatement(); + ResultSet rs = st.executeQuery("SHOW PROCESSLIST")) { + while (rs.next()) { + JSONObject r = new JSONObject(); + r.put("Id", rs.getObject("Id")); + r.put("User", rs.getString("User")); + r.put("Host", rs.getString("Host")); + r.put("db", rs.getString("db")); + r.put("Command", rs.getString("Command")); + r.put("Time", rs.getString("Time")); + r.put("State", rs.getString("State")); + r.put("Info", rs.getString("Info")); + out.put(r); + } + } + } else { + // 通用替代:返回当前时间与简单连接信息 + JSONObject r = new JSONObject(); + r.put("now", java.time.Instant.now().toString()); + r.put("message","未实现针对该数据库的详细性能查询,返回通用信息"); + out.put(r); + } + JSONObject resp = new JSONObject(); + resp.put("status","success"); + resp.put("data", new JSONArray(out.toString())); + callback.success(resp.toString()); + } catch (SQLException ex) { + JSONObject err = new JSONObject(); + err.put("status","error"); + err.put("message","性能分析失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } + + // listUsers -> 列出数据库用户(尝试常用查询) + case "listUsers": { + String connectionId = requestJson.optString("connectionId", ""); + if (connectionId.isEmpty()) { + callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 为空").toString()); + break; + } + Connection conn = DatabaseConnectionManager.getConnection(connectionId); + if (conn == null || conn.isClosed()) { + callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); + break; + } + DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); + String drvName = info == null ? "" : (info.driver == null ? "" : info.driver.toLowerCase()); + try { + JSONArray out = new JSONArray(); + if ("postgresql".equals(drvName)) { + try (Statement st = conn.createStatement(); + ResultSet rs = st.executeQuery("SELECT usename FROM pg_user")) { + while (rs.next()) { + out.put(rs.getString(1)); + } + } + } else if ("mysql".equals(drvName)) { + try (Statement st = conn.createStatement(); + ResultSet rs = st.executeQuery("SELECT User, Host FROM mysql.user")) { + while (rs.next()) { + JSONObject u = new JSONObject(); + u.put("user", rs.getString("User")); + u.put("host", rs.getString("Host")); + out.put(u); + } + } + } else { + // H2 / SQLite: 列出连接用户或简单返回空 + out.put("not_supported_for_db"); + } + JSONObject resp = new JSONObject(); + resp.put("status","success"); + resp.put("users", new JSONArray(out.toString())); + callback.success(resp.toString()); + } catch (SQLException ex) { + JSONObject err = new JSONObject(); + err.put("status","error"); + err.put("message","列出用户失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } + case "insertRow": { + String connectionId = requestJson.optString("connectionId", ""); + String tableName = requestJson.optString("tableName", ""); + JSONObject rowData = requestJson.optJSONObject("rowData"); + + if (connectionId.isEmpty() || tableName.isEmpty() || rowData == null) { + callback.failure(400, new JSONObject().put("status","error").put("message","参数不完整").toString()); + break; + } + + Connection conn = DatabaseConnectionManager.getConnection(connectionId); + if (conn == null || conn.isClosed()) { + callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); + break; + } + + try { + // 构建INSERT语句 + StringBuilder columns = new StringBuilder(); + StringBuilder placeholders = new StringBuilder(); + List values = new ArrayList<>(); + + Iterator keys = rowData.keys(); + while (keys.hasNext()) { + String key = keys.next(); + if (columns.length() > 0) { + columns.append(", "); + placeholders.append(", "); + } + columns.append(key); + placeholders.append("?"); + values.add(rowData.get(key)); + } + + String sql = "INSERT INTO " + tableName + " (" + columns.toString() + ") VALUES (" + placeholders.toString() + ")"; + + try (PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + for (int i = 0; i < values.size(); i++) { + pstmt.setObject(i + 1, values.get(i)); + } + + int affectedRows = pstmt.executeUpdate(); + + // 获取自增ID(如果有) + int newId = -1; + try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) { + if (generatedKeys.next()) { + newId = generatedKeys.getInt(1); + } + } + + JSONObject resp = new JSONObject(); + resp.put("status", "success"); + resp.put("affectedRows", affectedRows); + if (newId != -1) { + resp.put("newId", newId); + } + resp.put("message", "插入成功"); + callback.success(resp.toString()); + } + } catch (SQLException ex) { + JSONObject err = new JSONObject(); + err.put("status", "error"); + err.put("message", "插入失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } + + case "updateRow": { + String connectionId = requestJson.optString("connectionId", ""); + String tableName = requestJson.optString("tableName", ""); + JSONObject originalData = requestJson.optJSONObject("originalData"); + JSONObject updatedData = requestJson.optJSONObject("updatedData"); + + if (connectionId.isEmpty() || tableName.isEmpty() || originalData == null || updatedData == null) { + callback.failure(400, new JSONObject().put("status","error").put("message","参数不完整").toString()); + break; + } + + Connection conn = DatabaseConnectionManager.getConnection(connectionId); + if (conn == null || conn.isClosed()) { + callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); + break; + } + + try { + // 构建UPDATE语句 + StringBuilder setClause = new StringBuilder(); + StringBuilder whereClause = new StringBuilder(); + List values = new ArrayList<>(); + + // SET部分 + Iterator updateKeys = updatedData.keys(); + while (updateKeys.hasNext()) { + String key = updateKeys.next(); + if (setClause.length() > 0) { + setClause.append(", "); + } + setClause.append(key).append(" = ?"); + values.add(updatedData.get(key)); + } + + // WHERE部分(使用原始数据识别要更新的行) + Iterator originalKeys = originalData.keys(); + while (originalKeys.hasNext()) { + String key = originalKeys.next(); + if (whereClause.length() > 0) { + whereClause.append(" AND "); + } + whereClause.append(key).append(" = ?"); + values.add(originalData.get(key)); + } + + String sql = "UPDATE " + tableName + " SET " + setClause.toString() + " WHERE " + whereClause.toString(); + + try (PreparedStatement pstmt = conn.prepareStatement(sql)) { + for (int i = 0; i < values.size(); i++) { + pstmt.setObject(i + 1, values.get(i)); + } + + int affectedRows = pstmt.executeUpdate(); + + JSONObject resp = new JSONObject(); + resp.put("status", "success"); + resp.put("affectedRows", affectedRows); + resp.put("message", "更新成功"); + callback.success(resp.toString()); + } + } catch (SQLException ex) { + JSONObject err = new JSONObject(); + err.put("status", "error"); + err.put("message", "更新失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } + + case "deleteRow": { + String connectionId = requestJson.optString("connectionId", ""); + String tableName = requestJson.optString("tableName", ""); + JSONObject rowData = requestJson.optJSONObject("rowData"); + + if (connectionId.isEmpty() || tableName.isEmpty() || rowData == null) { + callback.failure(400, new JSONObject().put("status","error").put("message","参数不完整").toString()); + break; + } + + Connection conn = DatabaseConnectionManager.getConnection(connectionId); + if (conn == null || conn.isClosed()) { + callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); + break; + } + + try { + // 构建DELETE语句 + StringBuilder whereClause = new StringBuilder(); + List values = new ArrayList<>(); + + Iterator keys = rowData.keys(); + while (keys.hasNext()) { + String key = keys.next(); + if (whereClause.length() > 0) { + whereClause.append(" AND "); + } + whereClause.append(key).append(" = ?"); + values.add(rowData.get(key)); + } + + String sql = "DELETE FROM " + tableName + " WHERE " + whereClause.toString(); + + try (PreparedStatement pstmt = conn.prepareStatement(sql)) { + for (int i = 0; i < values.size(); i++) { + pstmt.setObject(i + 1, values.get(i)); + } + + int affectedRows = pstmt.executeUpdate(); + + JSONObject resp = new JSONObject(); + resp.put("status", "success"); + resp.put("affectedRows", affectedRows); + resp.put("message", "删除成功"); + callback.success(resp.toString()); + } + } catch (SQLException ex) { + JSONObject err = new JSONObject(); + err.put("status", "error"); + err.put("message", "删除失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } + + case "createTable": { + String connectionId = requestJson.optString("connectionId", ""); + String tableName = requestJson.optString("tableName", ""); + JSONArray columns = requestJson.optJSONArray("columns"); + JSONObject tableOptions = requestJson.optJSONObject("tableOptions"); + JSONObject fileSettings = requestJson.optJSONObject("fileSettings"); + + if (connectionId.isEmpty() || tableName.isEmpty() || columns == null || columns.length() == 0) { + callback.failure(400, new JSONObject().put("status","error").put("message","参数不完整").toString()); + break; + } + + Connection conn = DatabaseConnectionManager.getConnection(connectionId); + if (conn == null || conn.isClosed()) { + callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); + break; + } + + try { + // 构建CREATE TABLE语句 + StringBuilder sql = new StringBuilder("CREATE TABLE "); + sql.append(tableName).append(" ("); + + // 添加列定义 + for (int i = 0; i < columns.length(); i++) { + JSONObject column = columns.getJSONObject(i); + String colName = column.optString("name", ""); + String colType = column.optString("type", "VARCHAR(255)"); + String colAttributes = column.optString("attributes", ""); + + if (colName.isEmpty()) { + callback.failure(400, new JSONObject().put("status","error").put("message","列名不能为空").toString()); + return true; + } + + if (i > 0) sql.append(", "); + sql.append(colName).append(" ").append(colType); + + if (!colAttributes.isEmpty()) { + sql.append(" ").append(colAttributes); + } + } + + sql.append(")"); + + // 添加表选项 + if (tableOptions != null) { + String engine = tableOptions.optString("engine", "InnoDB"); + String charset = tableOptions.optString("charset", "utf8mb4"); + String collation = tableOptions.optString("collation", "utf8mb4_unicode_ci"); + String comment = tableOptions.optString("comment", ""); + + sql.append(" ENGINE=").append(engine); + sql.append(" DEFAULT CHARSET=").append(charset); + sql.append(" COLLATE=").append(collation); + + if (!comment.isEmpty()) { + sql.append(" COMMENT='").append(comment.replace("'", "''")).append("'"); + } + } + + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate(sql.toString()); + + // 保存文件设置(如果提供了) + if (fileSettings != null) { + //saveFileSettings(connectionId, tableName, fileSettings); + } + + JSONObject resp = new JSONObject(); + resp.put("status", "success"); + resp.put("message", "表创建成功"); + callback.success(resp.toString()); + } + } catch (SQLException ex) { + JSONObject err = new JSONObject(); + err.put("status", "error"); + err.put("message", "创建表失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } + + case "alterTable": { + String connectionId = requestJson.optString("connectionId", ""); + String tableName = requestJson.optString("tableName", ""); + String newTableName = requestJson.optString("newTableName", ""); + JSONArray columns = requestJson.optJSONArray("columns"); + JSONObject tableOptions = requestJson.optJSONObject("tableOptions"); + JSONObject fileSettings = requestJson.optJSONObject("fileSettings"); + + if (connectionId.isEmpty() || tableName.isEmpty() || columns == null || columns.length() == 0) { + callback.failure(400, new JSONObject().put("status","error").put("message","参数不完整").toString()); + break; + } + + Connection conn = DatabaseConnectionManager.getConnection(connectionId); + if (conn == null || conn.isClosed()) { + callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); + break; + } + + try { + // 这里简化处理:在实际应用中,需要更复杂的ALTER TABLE逻辑 + // 包括检测哪些列需要添加、修改或删除 + + // 首先获取当前表结构 + DatabaseMetaData meta = conn.getMetaData(); + List existingColumns = new ArrayList<>(); + try (ResultSet rs = meta.getColumns(conn.getCatalog(), null, tableName, null)) { + while (rs.next()) { + existingColumns.add(rs.getString("COLUMN_NAME")); + } + } + + // 构建ALTER TABLE语句(简化版) + // 注意:实际应用中需要更复杂的逻辑来处理不同的ALTER操作 + StringBuilder sql = new StringBuilder(); + + // 重命名表(如果需要) + if (!newTableName.isEmpty() && !newTableName.equals(tableName)) { + sql.append("ALTER TABLE ").append(tableName).append(" RENAME TO ").append(newTableName).append("; "); + tableName = newTableName; // 更新表名用于后续操作 + } + + // 这里简化处理:实际应用中需要更复杂的列变更逻辑 + // 可以添加新列,但不能删除或修改现有列(简化版) + for (int i = 0; i < columns.length(); i++) { + JSONObject column = columns.getJSONObject(i); + String colName = column.optString("name", ""); + String colType = column.optString("type", "VARCHAR(255)"); + String colAttributes = column.optString("attributes", ""); + + if (colName.isEmpty()) continue; + + if (!existingColumns.contains(colName)) { + // 添加新列 + if (sql.length() > 0 && !sql.toString().endsWith("; ")) { + sql.append("; "); + } + sql.append("ALTER TABLE ").append(tableName).append(" ADD COLUMN ") + .append(colName).append(" ").append(colType); + + if (!colAttributes.isEmpty()) { + sql.append(" ").append(colAttributes); + } + } + // 注意:在实际应用中,还需要处理修改和删除列的情况 + } + + // 执行ALTER语句 + if (sql.length() > 0) { + try (Statement stmt = conn.createStatement()) { + // 处理多条SQL语句 + String[] sqlStatements = sql.toString().split(";"); + for (String sqlStmt : sqlStatements) { + if (!sqlStmt.trim().isEmpty()) { + stmt.executeUpdate(sqlStmt.trim()); + } + } + } + } + + // 更新文件设置(如果提供了) + if (fileSettings != null) { + //saveFileSettings(connectionId, tableName, fileSettings); + } + + JSONObject resp = new JSONObject(); + resp.put("status", "success"); + resp.put("message", "表结构修改成功"); + callback.success(resp.toString()); + + } catch (SQLException ex) { + JSONObject err = new JSONObject(); + err.put("status", "error"); + err.put("message", "修改表结构失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } +/* + case "uploadFile": { + String connectionId = requestJson.optString("connectionId", ""); + String tableName = requestJson.optString("tableName", ""); + String columnName = requestJson.optString("columnName", ""); + String fileName = requestJson.optString("fileName", ""); + long fileSize = requestJson.optLong("fileSize", 0); + + if (connectionId.isEmpty() || tableName.isEmpty() || columnName.isEmpty() || fileName.isEmpty()) { + callback.failure(400, new JSONObject().put("status","error").put("message","参数不完整").toString()); + break; + } + + // 在实际应用中,这里应该处理文件上传 + // 由于CEF的限制,文件上传可能需要通过其他方式处理 + // 这里提供一个模拟实现 + + try { + // 获取文件存储设置 + JSONObject fileSettings = getFileSettings(connectionId, tableName); + String storageType = fileSettings.optString("storageType", "filesystem"); + String storagePath = fileSettings.optString("storagePath", "./uploads"); + long maxFileSize = fileSettings.optLong("maxFileSize", 10 * 1024 * 1024); // 默认10MB + JSONArray allowedTypes = fileSettings.optJSONArray("allowedTypes"); + + // 检查文件大小 + if (fileSize > maxFileSize) { + callback.failure(400, new JSONObject().put("status","error") + .put("message","文件大小超过限制: " + (maxFileSize/1024/1024) + "MB").toString()); + break; + } + + // 检查文件类型 + if (allowedTypes != null && allowedTypes.length() > 0) { + String fileExt = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase(); + boolean allowed = false; + + // 简化文件类型检查 + for (int i = 0; i < allowedTypes.length(); i++) { + String type = allowedTypes.getString(i); + if (("image".equals(type) && isImageFile(fileExt)) || + ("document".equals(type) && isDocumentFile(fileExt)) || + ("audio".equals(type) && isAudioFile(fileExt)) || + ("video".equals(type) && isVideoFile(fileExt)) || + ("archive".equals(type) && isArchiveFile(fileExt))) { + allowed = true; + break; + } + } + + if (!allowed) { + callback.failure(400, new JSONObject().put("status","error") + .put("message","不允许的文件类型").toString()); + break; + } + } + + // 生成文件路径 + String fileId = "file_" + System.currentTimeMillis() + "_" + (int)(Math.random() * 1000); + String fileExtension = fileName.substring(fileName.lastIndexOf('.')); + String filePath; + + if ("database".equals(storageType)) { + // 存储在数据库中 - 返回文件ID + filePath = fileId; + } else { + // 存储在文件系统中 + java.nio.file.Path uploadDir = java.nio.file.Paths.get(storagePath); + try { + java.nio.file.Files.createDirectories(uploadDir); + } catch (Exception e) { + // 忽略创建目录失败 + } + + filePath = uploadDir.resolve(fileId + fileExtension).toString(); + + // 在实际应用中,这里应该保存文件到指定路径 + // 由于CEF限制,这里只是模拟 + } + + JSONObject resp = new JSONObject(); + resp.put("status", "success"); + resp.put("fileId", fileId); + resp.put("path", filePath); + resp.put("message", "文件上传成功"); + callback.success(resp.toString()); + + } catch (Exception ex) { + JSONObject err = new JSONObject(); + err.put("status", "error"); + err.put("message", "文件上传失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } +*/ + default: { + JSONObject err = new JSONObject(); + err.put("status", "error"); + err.put("message", "未知的操作类型: " + type); + callback.failure(400, err.toString()); + } + } + return true; + } catch (org.json.JSONException je) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "请求解析失败: " + je.getMessage()); + callback.failure(400, error.toString()); + return true; + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", e.getMessage() == null ? e.toString() : e.getMessage()); + callback.failure(500, error.toString()); + return true; + } + } + + @Override + public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) { + // 查询取消,可选地记录日志 + } + }, true); + } else { + System.err.println("popupDataBaseWindow: msgRouter 为 null,消息路由无法注册"); + } + }); + } + + // 处理数据库连接 + private static void handleDatabaseConnect(JSONObject request, CefQueryCallback callback) { + try { + String driver = request.optString("driver", "mysql"); + String host = request.optString("host", "localhost"); + String port = request.optString("port", "3306"); + String database = request.optString("database", ""); + String username = request.optString("username", ""); + String password = request.optString("password", ""); + + // 验证必要参数 + if (database.isEmpty()) { + throw new IllegalArgumentException("数据库名不能为空"); + } + + // 建立真实数据库连接 + String connectionId = DatabaseConnectionManager.connect(driver, host, port, database, username, password); + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("message", "数据库连接成功"); + response.put("connectionId", connectionId); + + DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); + response.put("database", info.database); + response.put("driver", info.driver); + + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "连接失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } + } + + // 处理创建本地数据库 + private static void handleCreateLocalDatabase(JSONObject request, CefQueryCallback callback) { + try { + String driver = request.optString("driver", "sqlite"); + String dbName = request.optString("dbName", "my_database"); + + if (dbName.isEmpty()) { + throw new IllegalArgumentException("数据库名称不能为空"); + } + + // 创建本地数据库 + String connectionId = DatabaseConnectionManager.createLocalDatabase(driver, dbName); + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("message", "本地数据库创建成功"); + response.put("connectionId", connectionId); + response.put("database", dbName); + response.put("driver", driver); + + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "创建数据库失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } + } + + // 处理断开数据库连接 + private static void handleDisconnectDatabase(JSONObject request, CefQueryCallback callback) { + try { + String connectionId = request.optString("connectionId", ""); + + if (connectionId.isEmpty()) { + throw new IllegalArgumentException("连接ID不能为空"); + } + + DatabaseConnectionManager.disconnect(connectionId); + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("message", "数据库连接已断开"); + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "断开连接失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } + } + + // 处理SQL查询执行 + private static void handleExecuteQuery(JSONObject request, CefQueryCallback callback) { + Connection connection = null; + Statement statement = null; + ResultSet resultSet = null; + + try { + String query = request.optString("query", "").trim(); + String connectionId = request.optString("connectionId", ""); + + if (connectionId.isEmpty()) { + throw new IllegalArgumentException("连接ID不能为空"); + } + if (query.isEmpty()) { + throw new IllegalArgumentException("SQL查询不能为空"); + } + + connection = DatabaseConnectionManager.getConnection(connectionId); + if (connection == null || connection.isClosed()) { + throw new SQLException("数据库连接已断开或不存在"); + } + + long startTime = System.currentTimeMillis(); + + JSONObject response = new JSONObject(); + + // 判断查询类型 + boolean isSelect = query.toLowerCase().startsWith("select"); + boolean isUpdate = query.toLowerCase().startsWith("update") || + query.toLowerCase().startsWith("insert") || + query.toLowerCase().startsWith("delete"); + + if (isSelect) { + statement = connection.createStatement(); + resultSet = statement.executeQuery(query); + + // 获取元数据 + ResultSetMetaData metaData = resultSet.getMetaData(); + int columnCount = metaData.getColumnCount(); + + // 构建列信息 + JSONArray columns = new JSONArray(); + for (int i = 1; i <= columnCount; i++) { + columns.put(metaData.getColumnName(i)); + } + + // 构建数据 + JSONArray data = new JSONArray(); + int rowCount = 0; + while (resultSet.next()) { + JSONObject row = new JSONObject(); + for (int i = 1; i <= columnCount; i++) { + String columnName = metaData.getColumnName(i); + Object value = resultSet.getObject(i); + row.put(columnName, value != null ? value.toString() : null); + } + data.put(row); + rowCount++; + } + + long endTime = System.currentTimeMillis(); + double executionTime = (endTime - startTime) / 1000.0; + + response.put("status", "success"); + response.put("executionTime", String.format("%.3fs", executionTime)); + response.put("rowCount", rowCount); + response.put("columns", columns); + response.put("data", data); + + } else if (isUpdate) { + statement = connection.createStatement(); + int affectedRows = statement.executeUpdate(query); + + long endTime = System.currentTimeMillis(); + double executionTime = (endTime - startTime) / 1000.0; + + response.put("status", "success"); + response.put("executionTime", String.format("%.3fs", executionTime)); + response.put("affectedRows", affectedRows); + response.put("message", "操作成功,影响 " + affectedRows + " 行"); + + } else { + // 其他类型的查询(CREATE, DROP, ALTER等) + statement = connection.createStatement(); + boolean hasResults = statement.execute(query); + + long endTime = System.currentTimeMillis(); + double executionTime = (endTime - startTime) / 1000.0; + + response.put("status", "success"); + response.put("executionTime", String.format("%.3fs", executionTime)); + response.put("message", "查询执行成功"); + + if (hasResults) { + resultSet = statement.getResultSet(); + ResultSetMetaData metaData = resultSet.getMetaData(); + int columnCount = metaData.getColumnCount(); + + JSONArray columns = new JSONArray(); + for (int i = 1; i <= columnCount; i++) { + columns.put(metaData.getColumnName(i)); + } + + JSONArray data = new JSONArray(); + int rowCount = 0; + while (resultSet.next()) { + JSONObject row = new JSONObject(); + for (int i = 1; i <= columnCount; i++) { + String columnName = metaData.getColumnName(i); + Object value = resultSet.getObject(i); + row.put(columnName, value != null ? value.toString() : null); + } + data.put(row); + rowCount++; + } + + response.put("rowCount", rowCount); + response.put("columns", columns); + response.put("data", data); + } + } + + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "查询执行失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } finally { + // 关闭资源 + try { + if (resultSet != null) resultSet.close(); + if (statement != null) statement.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + + // 处理获取表列表 + private static void handleGetTables(JSONObject request, CefQueryCallback callback) { + Connection connection = null; + ResultSet resultSet = null; + + try { + String connectionId = request.optString("connectionId", ""); + + if (connectionId.isEmpty()) { + throw new IllegalArgumentException("连接ID不能为空"); + } + + connection = DatabaseConnectionManager.getConnection(connectionId); + if (connection == null || connection.isClosed()) { + throw new SQLException("数据库连接已断开或不存在"); + } + + DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); + JSONArray tables = new JSONArray(); + + // 根据数据库类型获取表信息 + String catalog = null; + String schema = null; + + switch (info.driver.toLowerCase()) { + case "mysql": + catalog = info.database; + break; + case "postgresql": + schema = "public"; + break; + case "sqlite": + case "h2": + // SQLite和H2不需要特定的catalog或schema + break; + } + + DatabaseMetaData metaData = connection.getMetaData(); + resultSet = metaData.getTables(catalog, schema, null, new String[]{"TABLE"}); + + while (resultSet.next()) { + String tableName = resultSet.getString("TABLE_NAME"); + String tableType = resultSet.getString("TABLE_TYPE"); + + // 获取表的行数 + int rowCount = 0; + try (Statement countStmt = connection.createStatement(); + ResultSet countRs = countStmt.executeQuery("SELECT COUNT(*) FROM " + tableName)) { + if (countRs.next()) { + rowCount = countRs.getInt(1); + } + } catch (SQLException e) { + // 如果无法获取行数,忽略错误 + } + + JSONObject table = new JSONObject(); + table.put("name", tableName); + table.put("type", tableType); + table.put("rows", rowCount); + tables.put(table); + } + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("tables", tables); + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "获取表列表失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } finally { + try { + if (resultSet != null) resultSet.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + + // 处理获取表数据 + private static void handleGetTableData(JSONObject request, CefQueryCallback callback) { + Connection connection = null; + Statement statement = null; + ResultSet resultSet = null; + + try { + String tableName = request.optString("tableName", ""); + String connectionId = request.optString("connectionId", ""); + int limit = request.optInt("limit", 50); + int offset = request.optInt("offset", 0); + + if (connectionId.isEmpty()) { + throw new IllegalArgumentException("连接ID不能为空"); + } + if (tableName.isEmpty()) { + throw new IllegalArgumentException("表名不能为空"); + } + + connection = DatabaseConnectionManager.getConnection(connectionId); + if (connection == null || connection.isClosed()) { + throw new SQLException("数据库连接已断开或不存在"); + } + + // 构建分页查询 + DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); + String query; + + switch (info.driver.toLowerCase()) { + case "mysql": + query = String.format("SELECT * FROM `%s` LIMIT %d OFFSET %d", tableName, limit, offset); + break; + case "postgresql": + query = String.format("SELECT * FROM \"%s\" LIMIT %d OFFSET %d", tableName, limit, offset); + break; + default: + query = String.format("SELECT * FROM %s LIMIT %d OFFSET %d", tableName, limit, offset); + } + + statement = connection.createStatement(); + resultSet = statement.executeQuery(query); + + // 获取元数据 + ResultSetMetaData metaData = resultSet.getMetaData(); + int columnCount = metaData.getColumnCount(); + + // 构建列信息 + JSONArray columns = new JSONArray(); + for (int i = 1; i <= columnCount; i++) { + columns.put(metaData.getColumnName(i)); + } + + // 构建数据 + JSONArray data = new JSONArray(); + while (resultSet.next()) { + JSONObject row = new JSONObject(); + for (int i = 1; i <= columnCount; i++) { + String columnName = metaData.getColumnName(i); + Object value = resultSet.getObject(i); + row.put(columnName, value != null ? value.toString() : null); + } + data.put(row); + } + + // 获取总行数 + int total = 0; + try (Statement countStmt = connection.createStatement(); + ResultSet countRs = countStmt.executeQuery("SELECT COUNT(*) FROM " + tableName)) { + if (countRs.next()) { + total = countRs.getInt(1); + } + } catch (SQLException e) { + // 如果无法获取总行数,使用当前数据行数 + total = data.length(); + } + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("tableName", tableName); + response.put("columns", columns); + response.put("data", data); + response.put("total", total); + response.put("offset", offset); + response.put("limit", limit); + + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "获取表数据失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } finally { + try { + if (resultSet != null) resultSet.close(); + if (statement != null) statement.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + + // 处理获取表结构 + private static void handleGetTableStructure(JSONObject request, CefQueryCallback callback) { + Connection connection = null; + ResultSet rs = null; + ResultSet idxRs = null; + ResultSet pkRs = null; + ResultSet fkRs = null; + PreparedStatement ps = null; + + try { + String tableName = request.optString("tableName", ""); + String connectionId = request.optString("connectionId", ""); + + if (connectionId.isEmpty()) { + throw new IllegalArgumentException("连接ID不能为空"); + } + if (tableName.isEmpty()) { + throw new IllegalArgumentException("表名不能为空"); + } + + connection = DatabaseConnectionManager.getConnection(connectionId); + if (connection == null || connection.isClosed()) { + throw new SQLException("数据库连接已断开或不存在"); + } + + DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); + DatabaseMetaData metaData = connection.getMetaData(); + + String catalog = null; + String schema = null; + + if (info != null && info.driver != null) { + switch (info.driver.toLowerCase()) { + case "mysql": + catalog = info.database; + break; + case "postgresql": + schema = "public"; + break; + default: + // leave null + } + } + + // 读取列信息 + rs = metaData.getColumns(catalog, schema, tableName, null); + JSONArray columns = new JSONArray(); + while (rs.next()) { + JSONObject column = new JSONObject(); + + String rawType = rs.getString("TYPE_NAME"); // eg. VARCHAR, INT, DECIMAL + int colSize = rs.getInt("COLUMN_SIZE"); + int decimalDigits = 0; + try { decimalDigits = rs.getInt("DECIMAL_DIGITS"); } catch (Exception ignored) {} + + // 生成更友好的类型展示(匹配前端类型选项) + String displayType = rawType != null ? rawType : ""; + if (rawType != null) { + String rt = rawType.toUpperCase(); + if (rt.equals("VARCHAR") || rt.equals("CHAR")) { + displayType = rt + "(" + colSize + ")"; + } else if (rt.equals("DECIMAL") || rt.equals("NUMERIC")) { + displayType = "DECIMAL(" + colSize + "," + decimalDigits + ")"; + } else if (rt.equals("DOUBLE") || rt.equals("FLOAT")) { + // 保持为原始类型 + displayType = rt; + } else { + // 有时候 TYPE_NAME 已经包含长度(如 VARCHAR(255)),保留原样 + displayType = rawType; + } + } + + column.put("name", rs.getString("COLUMN_NAME")); + column.put("type", displayType); + column.put("rawType", rawType); + column.put("size", colSize); + column.put("decimalDigits", decimalDigits); + int nullableFlag = rs.getInt("NULLABLE"); + column.put("nullable", nullableFlag == DatabaseMetaData.columnNullable); + column.put("defaultValue", rs.getString("COLUMN_DEF")); + // 尝试获取是否自增(部分驱动返回 IS_AUTOINCREMENT) + try { + String isAuto = rs.getString("IS_AUTOINCREMENT"); + column.put("autoIncrement", "YES".equalsIgnoreCase(isAuto)); + } catch (Exception ignored) { + // ignore + } + + columns.put(column); + } + if (rs != null) { rs.close(); rs = null; } + + // 读取主键(用于在 constraints 中显示) + JSONArray constraints = new JSONArray(); + pkRs = metaData.getPrimaryKeys(catalog, schema, tableName); + List pkColumns = new ArrayList<>(); + while (pkRs != null && pkRs.next()) { + String pkName = pkRs.getString("PK_NAME"); + String pkCol = pkRs.getString("COLUMN_NAME"); + if (pkCol != null) pkColumns.add(pkCol); + } + if (!pkColumns.isEmpty()) { + JSONObject pkObj = new JSONObject(); + pkObj.put("name", "PRIMARY"); + pkObj.put("type", "PRIMARY KEY"); + pkObj.put("definition", "PRIMARY KEY (" + String.join(",", pkColumns) + ")"); + constraints.put(pkObj); + } + if (pkRs != null) { pkRs.close(); pkRs = null; } + + // 读取外键 + fkRs = metaData.getImportedKeys(catalog, schema, tableName); + while (fkRs != null && fkRs.next()) { + String fkName = fkRs.getString("FK_NAME"); + String fkCol = fkRs.getString("FKCOLUMN_NAME"); + String pkTable = fkRs.getString("PKTABLE_NAME"); + String pkCol = fkRs.getString("PKCOLUMN_NAME"); + JSONObject fkObj = new JSONObject(); + fkObj.put("name", fkName == null ? ("fk_" + fkCol) : fkName); + fkObj.put("type", "FOREIGN KEY"); + fkObj.put("definition", String.format("FOREIGN KEY (%s) REFERENCES %s(%s)", fkCol, pkTable, pkCol)); + constraints.put(fkObj); + } + if (fkRs != null) { fkRs.close(); fkRs = null; } + + // 读取索引信息(不重复合并同名索引的列) + idxRs = metaData.getIndexInfo(catalog, schema, tableName, false, false); + Map idxMap = new LinkedHashMap<>(); + while (idxRs != null && idxRs.next()) { + String idxName = idxRs.getString("INDEX_NAME"); + if (idxName == null) continue; // driver-specific + boolean nonUnique = idxRs.getBoolean("NON_UNIQUE"); + String colName = idxRs.getString("COLUMN_NAME"); + if (!idxMap.containsKey(idxName)) { + JSONObject idxObj = new JSONObject(); + idxObj.put("name", idxName); + idxObj.put("type", nonUnique ? "INDEX" : "UNIQUE"); + idxObj.put("columns", colName == null ? "" : colName); + idxMap.put(idxName, idxObj); + } else { + JSONObject idxObj = idxMap.get(idxName); + String prevCols = idxObj.optString("columns", ""); + if (colName != null && !prevCols.contains(colName)) { + if (prevCols.isEmpty()) idxObj.put("columns", colName); + else idxObj.put("columns", prevCols + "," + colName); + } + } + } + JSONArray indexes = new JSONArray(); + for (JSONObject v : idxMap.values()) indexes.put(v); + if (idxRs != null) { idxRs.close(); idxRs = null; } + + // 表级信息(MySQL: ENGINE, COLLATION, COMMENT) + String engine = null; + String collation = null; + String charset = null; + String tableComment = null; + if (info != null && info.driver != null && "mysql".equalsIgnoreCase(info.driver) && info.database != null) { + String sql = "SELECT ENGINE, TABLE_COLLATION, TABLE_COMMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?"; + try { + ps = connection.prepareStatement(sql); + ps.setString(1, info.database); + ps.setString(2, tableName); + ResultSet tRs = ps.executeQuery(); + if (tRs.next()) { + engine = tRs.getString("ENGINE"); + collation = tRs.getString("TABLE_COLLATION"); + tableComment = tRs.getString("TABLE_COMMENT"); + if (collation != null && collation.contains("_")) { + charset = collation.split("_")[0]; + } + } + if (tRs != null) { tRs.close(); tRs = null; } + } catch (Exception ignored) { + // 忽略信息 schema 查询失败的情况(可能权限或 driver 不支持) + } finally { + if (ps != null) { try { ps.close(); } catch (SQLException ignored) {} ps = null; } + } + } + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("tableName", tableName); + response.put("engine", engine == null ? JSONObject.NULL : engine); + response.put("charset", charset == null ? JSONObject.NULL : charset); + response.put("collation", collation == null ? JSONObject.NULL : collation); + response.put("comment", tableComment == null ? JSONObject.NULL : tableComment); + response.put("columns", columns); + response.put("indexes", indexes); + response.put("constraints", constraints); + + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "获取表结构失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } finally { + try { if (rs != null) rs.close(); } catch (SQLException ignored) {} + try { if (idxRs != null) idxRs.close(); } catch (SQLException ignored) {} + try { if (pkRs != null) pkRs.close(); } catch (SQLException ignored) {} + try { if (fkRs != null) fkRs.close(); } catch (SQLException ignored) {} + try { if (ps != null) ps.close(); } catch (SQLException ignored) {} + } + } + + + // 处理主题更新(保持不变) + // 这里自己实现了一个一般使用事件实现 + private static void handleUpdateTheme(JSONObject request, CefQueryCallback callback) { + try { + // 通过 AxisInnovatorsBox 获取当前主题状态 + boolean isDarkMode = AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode(); + String theme = isDarkMode ? "dark" : "light"; + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("theme", theme); + response.put("message", "当前主题: " + theme); + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "获取主题失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } + } + + // 处理字体获取(保持不变) + private static void handleGetFonts(JSONObject request, CefQueryCallback callback) { + try { + JSONArray fonts = new JSONArray(); + String[] fontList = { + "Segoe UI", "Microsoft YaHei", "SimSun", "Arial", + "Helvetica", "Times New Roman", "Courier New", "Verdana" + }; + + for (String font : fontList) { + fonts.put(font); + } + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("fonts", fonts); + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "获取字体失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } + } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/browser/WindowOperation.java b/src/main/java/com/chuangzhou/vivid2D/browser/WindowOperation.java new file mode 100644 index 0000000..d68af1b --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/browser/WindowOperation.java @@ -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) { +} diff --git a/src/main/java/com/chuangzhou/vivid2D/browser/WindowOperationHandler.java b/src/main/java/com/chuangzhou/vivid2D/browser/WindowOperationHandler.java new file mode 100644 index 0000000..c55a1b0 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/browser/WindowOperationHandler.java @@ -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> operations; + private final Component attachedComponent; + + public static class Builder { + private WindowRegistry registry = WindowRegistry.getInstance(); + private final Map> 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 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 handler = operations.get(operation.type()); + if (handler != null) { + handler.accept(operation.targetWindow()); + operation.callback().success("操作成功: " + operation.type()); + } else { + operation.callback().failure(-1, "未定义的操作: " + operation.type()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/browser/WindowRegistry.java b/src/main/java/com/chuangzhou/vivid2D/browser/WindowRegistry.java new file mode 100644 index 0000000..f0a6ae5 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/browser/WindowRegistry.java @@ -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 windows = + new ConcurrentHashMap<>(); + private final ConcurrentMap childWindows = + new ConcurrentHashMap<>(); + private final Map> 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 handler) { + callbacks.put(requestId, handler); + } + + public Consumer 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 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 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(); + } + } + } + }); + } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/browser/util/CodeExecutor.java b/src/main/java/com/chuangzhou/vivid2D/browser/util/CodeExecutor.java new file mode 100644 index 0000000..60232f7 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/browser/util/CodeExecutor.java @@ -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 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 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 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 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 \n" + + "\n" + + "int main() {\n" + + " while (1){\n" + + " printf(\"Hello World\\n\");\n" + + "}\n" + + " return 0;\n" + + "}"; + executeCode(pythonCode, "c", null); + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/browser/util/DatabaseConnectionManager.java b/src/main/java/com/chuangzhou/vivid2D/browser/util/DatabaseConnectionManager.java new file mode 100644 index 0000000..57796ac --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/browser/util/DatabaseConnectionManager.java @@ -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 connections = new java.util.concurrent.ConcurrentHashMap<>(); + private static final Map 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(); + } + } + } + } + } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/EventPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/EventPanel.java new file mode 100644 index 0000000..9218326 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/EventPanel.java @@ -0,0 +1,6 @@ +package com.chuangzhou.vivid2D.render.awt; + +import javax.swing.*; + +public class EventPanel extends JPanel { +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/LayerOperationManager.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/LayerOperationManager.java index 159e896..0f8809d 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/LayerOperationManager.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/LayerOperationManager.java @@ -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; diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ParametersManagement.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ParametersManagement.java index c46b6f5..816a9be 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ParametersManagement.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ParametersManagement.java @@ -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 eventPayload = (Map) 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 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 payload = (Map) 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 newVertexUpdatePayload = Map.of( - "id", newVertexObj.getName(), - "Vertex", new float[]{newVertexObj.position.x, newVertexObj.position.y} - ); - List 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 originalValues = existingParameter.value(); + List 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 payload = (Map) 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 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"; diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/data/ParametersManagementData.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/data/ParametersManagementData.java index e2efbaa..061ea33 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/data/ParametersManagementData.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/data/ParametersManagementData.java @@ -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 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 animationParameters; public List paramIds; - public List values; + public List values; // 这里将存储 SerializableVertex 而不是 Vertex public List keyframes; public List 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 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 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 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 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 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 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 - ); - } } } \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/SelectionTool.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/SelectionTool.java index 94149a6..8f88370 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/SelectionTool.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/SelectionTool.java @@ -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() {} /** * 获取鼠标悬停的网格 diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/VertexDeformationTool.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/VertexDeformationTool.java index 3278e88..5daea78 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/VertexDeformationTool.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/VertexDeformationTool.java @@ -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 parameters = Map.of("id", primaryVertex.getName(), - "Vertex", new float[]{modelX, modelY}); + "Vertex", primaryVertex); renderPanel.getParametersManagement().broadcast( targetMesh.getModelPart(), "meshVertices", diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/FrameInterpolator.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/FrameInterpolator.java index afbe652..5532128 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/FrameInterpolator.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/FrameInterpolator.java @@ -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; diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/Mesh2D.java b/src/main/java/com/chuangzhou/vivid2D/render/model/Mesh2D.java index b37df68..6329e23 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/Mesh2D.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/Mesh2D.java @@ -434,6 +434,19 @@ public class Mesh2D { markDirty(); } + /** + * 直接设置此网格的活动顶点列表。 + * 这是从序列化数据恢复网格状态的首选方法,因为它可以保留每个顶点的完整信息。 + * + * @param vertexList 包含完整顶点信息的新顶点列表。 + */ + public void setActiveVertexList(VertexList vertexList) { + if (vertexList != null) { + this.activeVertexList = vertexList; + markDirty(); // 标记网格需要更新 + } + } + /** * 设置是否为渲染顶点 */ diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java b/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java index 6ed1046..71395e9 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java @@ -70,10 +70,20 @@ public class Model2D { // ==================== 光源系统 ==================== private final List lights; + private final List 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) { diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java index 84a3ae4..c0adb42 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java @@ -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 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(); } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/MeshData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/MeshData.java index 3ca54b1..25c009c 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/data/MeshData.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/data/MeshData.java @@ -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 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 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 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; } -} +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/VertexData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/VertexData.java new file mode 100644 index 0000000..8e8dfa1 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/data/VertexData.java @@ -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 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; + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/VertexListData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/VertexListData.java new file mode 100644 index 0000000..d55abcb --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/data/VertexListData.java @@ -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 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 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; + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Vertex.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Vertex.java index 388bbc9..404ee2a 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Vertex.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Vertex.java @@ -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 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); diff --git a/src/main/java/com/chuangzhou/vivid2D/window/MainWindow.java b/src/main/java/com/chuangzhou/vivid2D/window/MainWindow.java index b951fbd..012c5ff 100644 --- a/src/main/java/com/chuangzhou/vivid2D/window/MainWindow.java +++ b/src/main/java/com/chuangzhou/vivid2D/window/MainWindow.java @@ -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,