feat(window): 实现全局快捷键管理和模型修改状态跟踪- 新增 KeyBindingManager 类,负责注册和管理全局快捷键- 在 MainWindow 中集成快捷键管理器,并暴露 saveData 方法
- 实现模型修改状态跟踪机制,支持退出前保存提示 -重构图层面板的文件拖放逻辑,支持窗口级和列表级拖放处理 -修复图层名称重复问题,确保新建图层名称唯一性 - 优化图层删除逻辑,支持多选删除和参数清理 - 改进贴图绑定逻辑,确保正确设置网格纹理 - 更新 Mesh2D 中原始轴心点计算方法,使用原始边界而非当前边界
This commit is contained in:
@@ -5,11 +5,13 @@ import com.chuangzhou.vivid2D.render.awt.manager.ThumbnailManager;
|
|||||||
import com.chuangzhou.vivid2D.render.awt.manager.data.LayerOperationManagerData;
|
import com.chuangzhou.vivid2D.render.awt.manager.data.LayerOperationManagerData;
|
||||||
import com.chuangzhou.vivid2D.render.awt.util.*;
|
import com.chuangzhou.vivid2D.render.awt.util.*;
|
||||||
import com.chuangzhou.vivid2D.render.awt.util.renderer.LayerCellRenderer;
|
import com.chuangzhou.vivid2D.render.awt.util.renderer.LayerCellRenderer;
|
||||||
|
// 确保 LayerReorderTransferHandler 被正确导入,它处理内部重排
|
||||||
import com.chuangzhou.vivid2D.render.awt.util.renderer.LayerReorderTransferHandler;
|
import com.chuangzhou.vivid2D.render.awt.util.renderer.LayerReorderTransferHandler;
|
||||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||||
import com.chuangzhou.vivid2D.render.model.util.Texture;
|
import com.chuangzhou.vivid2D.render.model.util.Texture;
|
||||||
|
import com.chuangzhou.vivid2D.window.MainWindow;
|
||||||
import org.joml.Vector2f;
|
import org.joml.Vector2f;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
@@ -18,6 +20,8 @@ import javax.swing.border.EmptyBorder;
|
|||||||
import javax.swing.border.TitledBorder;
|
import javax.swing.border.TitledBorder;
|
||||||
import javax.swing.plaf.basic.BasicSliderUI;
|
import javax.swing.plaf.basic.BasicSliderUI;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
|
import java.awt.event.ActionEvent;
|
||||||
|
import java.awt.event.KeyEvent;
|
||||||
import java.awt.event.MouseAdapter;
|
import java.awt.event.MouseAdapter;
|
||||||
import java.awt.event.MouseEvent;
|
import java.awt.event.MouseEvent;
|
||||||
import java.awt.geom.RoundRectangle2D;
|
import java.awt.geom.RoundRectangle2D;
|
||||||
@@ -34,10 +38,13 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
// 引入 JnaFileChooser
|
// 引入 JnaFileChooser
|
||||||
import jnafilechooser.api.JnaFileChooser;
|
import jnafilechooser.api.JnaFileChooser;
|
||||||
// 引入拖放相关的类
|
// 引入拖放和快捷键相关的类
|
||||||
import java.awt.datatransfer.DataFlavor;
|
import java.awt.datatransfer.DataFlavor;
|
||||||
import java.awt.datatransfer.Transferable;
|
import java.awt.datatransfer.Transferable;
|
||||||
import javax.swing.TransferHandler;
|
import javax.swing.TransferHandler;
|
||||||
|
import javax.swing.AbstractAction;
|
||||||
|
import javax.swing.Action;
|
||||||
|
import javax.swing.KeyStroke;
|
||||||
|
|
||||||
|
|
||||||
public class ModelLayerPanel extends JPanel {
|
public class ModelLayerPanel extends JPanel {
|
||||||
@@ -99,9 +106,8 @@ public class ModelLayerPanel extends JPanel {
|
|||||||
setupModernLookAndFeel();
|
setupModernLookAndFeel();
|
||||||
this.thumbnailManager = new ThumbnailManager(renderPanel);
|
this.thumbnailManager = new ThumbnailManager(renderPanel);
|
||||||
|
|
||||||
// --- 新增:设置外部文件拖放处理器 ---
|
// FIX: 移除在 this 上的 TransferHandler,因为 JList 将会覆盖它
|
||||||
this.setTransferHandler(new FileDropTransferHandler());
|
// this.setTransferHandler(new FileDropTransferHandler());
|
||||||
// ---------------------------------
|
|
||||||
|
|
||||||
if (this.model != null) {
|
if (this.model != null) {
|
||||||
this.psdImporter = new PSDImporter(model, renderPanel, this);
|
this.psdImporter = new PSDImporter(model, renderPanel, this);
|
||||||
@@ -125,6 +131,19 @@ public class ModelLayerPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupWindowFileDropHandler() {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
Window window = SwingUtilities.getWindowAncestor(this);
|
||||||
|
// 确保找到的 window 是 RootPaneContainer (如 JFrame, JDialog)
|
||||||
|
if (window instanceof MainWindow mainWindow) {
|
||||||
|
JComponent contentPane = (JComponent) mainWindow.getContentPane();
|
||||||
|
if (contentPane != null) {
|
||||||
|
contentPane.setTransferHandler(new FileDropOnlyTransferHandler());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public void loadMetadata() {
|
public void loadMetadata() {
|
||||||
String modelDataPath = renderPanel.getGlContextManager().getModelPath() + ".data";
|
String modelDataPath = renderPanel.getGlContextManager().getModelPath() + ".data";
|
||||||
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(modelDataPath))) {
|
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(modelDataPath))) {
|
||||||
@@ -185,6 +204,10 @@ public class ModelLayerPanel extends JPanel {
|
|||||||
gbc.fill = GridBagConstraints.BOTH;
|
gbc.fill = GridBagConstraints.BOTH;
|
||||||
add(centerScrollPane, gbc);
|
add(centerScrollPane, gbc);
|
||||||
|
|
||||||
|
setupWindowFileDropHandler();
|
||||||
|
// 绑定快捷键
|
||||||
|
setupKeyBindings(layerList);
|
||||||
|
|
||||||
JPanel controlPanel = createControlPanel();
|
JPanel controlPanel = createControlPanel();
|
||||||
gbc.gridy = 2;
|
gbc.gridy = 2;
|
||||||
gbc.weighty = 0.0;
|
gbc.weighty = 0.0;
|
||||||
@@ -193,9 +216,37 @@ public class ModelLayerPanel extends JPanel {
|
|||||||
add(controlPanel, gbc);
|
add(controlPanel, gbc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置快捷键绑定。
|
||||||
|
* @param list JList 组件
|
||||||
|
*/
|
||||||
|
private void setupKeyBindings(JList<ModelPart> list) {
|
||||||
|
// 关键更改:从 JList 获取 InputMap,但使用 WHEN_IN_FOCUSED_WINDOW 模式
|
||||||
|
// 这样,只要包含这个 ModelLayerPanel 的窗口处于焦点状态,快捷键就会生效。
|
||||||
|
InputMap inputMap = list.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
|
||||||
|
|
||||||
|
// 获取组件的 ActionMap
|
||||||
|
ActionMap actionMap = list.getActionMap();
|
||||||
|
|
||||||
|
// 绑定 Delete/Backspace 键到删除操作
|
||||||
|
KeyStroke deleteKey = KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0);
|
||||||
|
KeyStroke backspaceKey = KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0);
|
||||||
|
|
||||||
|
inputMap.put(deleteKey, "deleteLayer");
|
||||||
|
inputMap.put(backspaceKey, "deleteLayer");
|
||||||
|
Action deleteAction = new AbstractAction() {
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent e) {
|
||||||
|
onRemoveLayer();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
actionMap.put("deleteLayer", deleteAction);
|
||||||
|
}
|
||||||
|
|
||||||
private JList<ModelPart> createModernList() {
|
private JList<ModelPart> createModernList() {
|
||||||
JList<ModelPart> list = new JList<>(listModel);
|
JList<ModelPart> list = new JList<>(listModel);
|
||||||
// 【修正 1:启用多选】
|
// 启用多选
|
||||||
list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
|
list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
|
||||||
list.setBackground(SURFACE_COLOR);
|
list.setBackground(SURFACE_COLOR);
|
||||||
list.setForeground(TEXT_COLOR);
|
list.setForeground(TEXT_COLOR);
|
||||||
@@ -205,8 +256,10 @@ public class ModelLayerPanel extends JPanel {
|
|||||||
cellRenderer.attachMouseListener(list, listModel);
|
cellRenderer.attachMouseListener(list, listModel);
|
||||||
list.setCellRenderer(cellRenderer);
|
list.setCellRenderer(cellRenderer);
|
||||||
list.setDragEnabled(true);
|
list.setDragEnabled(true);
|
||||||
// 【修正 2:使用多选 TransferHandler】
|
|
||||||
list.setTransferHandler(new LayerReorderTransferHandler(this));
|
// FIX: 使用新的复合 TransferHandler 统一处理文件拖放和内部重排
|
||||||
|
list.setTransferHandler(new CompositeLayerTransferHandler(this));
|
||||||
|
|
||||||
list.setDropMode(DropMode.INSERT);
|
list.setDropMode(DropMode.INSERT);
|
||||||
list.addMouseListener(new MouseAdapter() {
|
list.addMouseListener(new MouseAdapter() {
|
||||||
@Override
|
@Override
|
||||||
@@ -436,7 +489,7 @@ public class ModelLayerPanel extends JPanel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 【新增方法】执行多选拖拽后的图层块重排序操作。
|
* 【新增方法】执行多选拖拽后的图层块重排序操作。
|
||||||
* 供 LayerReorderTransferHandler 调用。
|
* 供 CompositeLayerTransferHandler (原 LayerReorderTransferHandler) 调用。
|
||||||
* @param srcIndices 列表中的视觉源索引数组(从上到下,已排序)。
|
* @param srcIndices 列表中的视觉源索引数组(从上到下,已排序)。
|
||||||
* @param dropIndex 列表中的视觉目标插入索引。
|
* @param dropIndex 列表中的视觉目标插入索引。
|
||||||
*/
|
*/
|
||||||
@@ -486,7 +539,7 @@ public class ModelLayerPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeButton.setEnabled(hasSelection);
|
removeButton.setEnabled(hasSelection);
|
||||||
// 【修正 3:多选时启用上下移动按钮】
|
// 多选时启用上下移动按钮
|
||||||
upButton.setEnabled(hasSelection);
|
upButton.setEnabled(hasSelection);
|
||||||
downButton.setEnabled(hasSelection);
|
downButton.setEnabled(hasSelection);
|
||||||
// 绑定贴图仍然只在单选时有意义
|
// 绑定贴图仍然只在单选时有意义
|
||||||
@@ -608,9 +661,10 @@ public class ModelLayerPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void createEmptyPart() {
|
private void createEmptyPart() {
|
||||||
String name = JOptionPane.showInputDialog(this, "新图层名称:", "新图层");
|
String name = JOptionPane.showInputDialog(SwingUtilities.getWindowAncestor(this), "新图层名称:", "新图层");
|
||||||
if (name == null || name.trim().isEmpty()) return;
|
if (name == null || name.trim().isEmpty()) return;
|
||||||
|
|
||||||
|
// 传入名称
|
||||||
operationManager.addLayer(name);
|
operationManager.addLayer(name);
|
||||||
reloadFromModel();
|
reloadFromModel();
|
||||||
|
|
||||||
@@ -634,7 +688,7 @@ public class ModelLayerPanel extends JPanel {
|
|||||||
|
|
||||||
private void showRenameDialog(ModelPart part) {
|
private void showRenameDialog(ModelPart part) {
|
||||||
String newName = (String) JOptionPane.showInputDialog(
|
String newName = (String) JOptionPane.showInputDialog(
|
||||||
this,
|
SwingUtilities.getWindowAncestor(this),
|
||||||
"输入新名称:",
|
"输入新名称:",
|
||||||
"重命名图层",
|
"重命名图层",
|
||||||
JOptionPane.PLAIN_MESSAGE,
|
JOptionPane.PLAIN_MESSAGE,
|
||||||
@@ -793,7 +847,19 @@ public class ModelLayerPanel extends JPanel {
|
|||||||
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||||
try {
|
try {
|
||||||
Texture texture = Texture.createFromFile(texName, filePath);
|
Texture texture = Texture.createFromFile(texName, filePath);
|
||||||
meshToBind.setTexture(texture);
|
List<Mesh2D> partMeshes = sel.getMeshes();
|
||||||
|
Mesh2D actualMesh = null;
|
||||||
|
if (partMeshes != null && !partMeshes.isEmpty()) {
|
||||||
|
actualMesh = partMeshes.get(partMeshes.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actualMesh != null) {
|
||||||
|
actualMesh.setTexture(texture);
|
||||||
|
} else {
|
||||||
|
// 修复:将无法解析的 'mesh' 替换为正确的局部变量 'meshToBind'
|
||||||
|
meshToBind.setTexture(texture);
|
||||||
|
}
|
||||||
|
|
||||||
model.addTexture(texture);
|
model.addTexture(texture);
|
||||||
model.markNeedsUpdate();
|
model.markNeedsUpdate();
|
||||||
} catch (Throwable ex) {
|
} catch (Throwable ex) {
|
||||||
@@ -822,27 +888,32 @@ public class ModelLayerPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void onRemoveLayer() {
|
private void onRemoveLayer() {
|
||||||
// 修正:支持删除多个选中的图层
|
|
||||||
List<ModelPart> selectedParts = layerList.getSelectedValuesList();
|
List<ModelPart> selectedParts = layerList.getSelectedValuesList();
|
||||||
if (selectedParts.isEmpty()) return;
|
if (selectedParts.isEmpty()) return;
|
||||||
|
|
||||||
String names = selectedParts.stream().map(ModelPart::getName).collect(java.util.stream.Collectors.joining("、"));
|
String names = selectedParts.stream().map(ModelPart::getName).collect(java.util.stream.Collectors.joining("、"));
|
||||||
int r = JOptionPane.showConfirmDialog(this, "确认删除图层:" + names + " ?", "确认删除", JOptionPane.YES_NO_OPTION);
|
int r = JOptionPane.showConfirmDialog(SwingUtilities.getWindowAncestor(this), "确认删除图层:" + names + " ?", "确认删除", JOptionPane.YES_NO_OPTION);
|
||||||
if (r != JOptionPane.YES_OPTION) return;
|
if (r != JOptionPane.YES_OPTION) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for(ModelPart part : selectedParts) {
|
for(ModelPart part : selectedParts) {
|
||||||
operationManager.removeLayer(part);
|
operationManager.removeLayer(part);
|
||||||
thumbnailManager.removeThumbnail(part);
|
thumbnailManager.removeThumbnail(part);
|
||||||
// 仅移除第一个选中项的参数管理(这是一个简化,实际应用中可能需要遍历移除)
|
|
||||||
if (part == selectedParts.get(0)) {
|
if (part == selectedParts.get(0)) {
|
||||||
renderPanel.getParametersManagement().removeParameter(part, "all");
|
if (renderPanel != null && renderPanel.getParametersManagement() != null) {
|
||||||
renderPanel.getGlContextManager().executeInGLContext(() -> renderPanel.getParametersManagement().removeParameter(part, "all"));
|
renderPanel.getParametersManagement().removeParameter(part, "all");
|
||||||
|
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||||
|
if (renderPanel != null && renderPanel.getParametersManagement() != null) {
|
||||||
|
renderPanel.getParametersManagement().removeParameter(part, "all");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reloadFromModel();
|
reloadFromModel();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
JOptionPane.showMessageDialog(this, "删除失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
|
// 修复:将父组件改为顶层窗口
|
||||||
|
JOptionPane.showMessageDialog(SwingUtilities.getWindowAncestor(this), "删除失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -900,12 +971,57 @@ public class ModelLayerPanel extends JPanel {
|
|||||||
try {
|
try {
|
||||||
BufferedImage img = ImageIO.read(f);
|
BufferedImage img = ImageIO.read(f);
|
||||||
if (img == null) throw new IOException("无法读取图片:" + f.getAbsolutePath());
|
if (img == null) throw new IOException("无法读取图片:" + f.getAbsolutePath());
|
||||||
String name = JOptionPane.showInputDialog(this, "新图层名称:", f.getName());
|
|
||||||
|
// 1. 获取用户输入的名称(或默认文件名)
|
||||||
|
String name = JOptionPane.showInputDialog(SwingUtilities.getWindowAncestor(this), "新图层名称:", f.getName());
|
||||||
if (name == null || name.trim().isEmpty()) name = f.getName();
|
if (name == null || name.trim().isEmpty()) name = f.getName();
|
||||||
|
|
||||||
|
// --- 修复:确保图层名称唯一性 start ---
|
||||||
|
// 解决 "Part already exists" 错误,确保 ModelPart 名称唯一
|
||||||
|
Map<String, ModelPart> partMap = getModelPartMap();
|
||||||
|
if (partMap != null) {
|
||||||
|
String uniqueName = name;
|
||||||
|
int counter = 1;
|
||||||
|
|
||||||
|
// 分离文件名和扩展名
|
||||||
|
String nameWithoutExt = name;
|
||||||
|
String extension = "";
|
||||||
|
int dotIndex = name.lastIndexOf('.');
|
||||||
|
if (dotIndex > 0) { // 确保点不在开头
|
||||||
|
nameWithoutExt = name.substring(0, dotIndex);
|
||||||
|
extension = name.substring(dotIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 剥离已有的 (数字) 部分,从干净的基础名开始计数
|
||||||
|
String baseNameForCounter = nameWithoutExt;
|
||||||
|
int bracketStart = baseNameForCounter.lastIndexOf('(');
|
||||||
|
int bracketEnd = baseNameForCounter.lastIndexOf(')');
|
||||||
|
|
||||||
|
if (bracketStart > 0 && bracketEnd == baseNameForCounter.length() - 1) {
|
||||||
|
try {
|
||||||
|
// 检查括号中的内容是否为数字
|
||||||
|
Integer.parseInt(baseNameForCounter.substring(bracketStart + 1, bracketEnd).trim());
|
||||||
|
baseNameForCounter = baseNameForCounter.substring(0, bracketStart).trim();
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// 不是数字,则保持不变
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查并生成唯一名称
|
||||||
|
while (partMap.containsKey(uniqueName)) {
|
||||||
|
uniqueName = baseNameForCounter + " (" + counter + ")" + extension;
|
||||||
|
counter++;
|
||||||
|
if (counter > 100) { // 避免无限循环
|
||||||
|
throw new IllegalStateException("无法生成唯一图层名称。");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
name = uniqueName; // 使用最终的唯一名称
|
||||||
|
}
|
||||||
|
// --- 修复:确保图层名称唯一性 end ---
|
||||||
|
|
||||||
ModelPart part = model.createPart(name);
|
ModelPart part = model.createPart(name);
|
||||||
Mesh2D mesh = MeshTextureUtil.createQuadForImage(img, name + "_mesh");
|
// 修复上一个问题中 GL Context lambda 无法解析 'mesh' 的编译错误
|
||||||
mesh.createDefaultSecondaryVertices();
|
final Mesh2D mesh = MeshTextureUtil.createQuadForImage(img, name + "_mesh");
|
||||||
part.addMesh(mesh);
|
part.addMesh(mesh);
|
||||||
|
|
||||||
if (renderPanel != null) {
|
if (renderPanel != null) {
|
||||||
@@ -970,11 +1086,11 @@ public class ModelLayerPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void createPartWithTransparentTexture() {
|
private void createPartWithTransparentTexture() {
|
||||||
String name = JOptionPane.showInputDialog(this, "新图层名称(透明):", "透明图层");
|
String name = JOptionPane.showInputDialog(SwingUtilities.getWindowAncestor(this), "新图层名称(透明):", "透明图层");
|
||||||
if (name == null || name.trim().isEmpty()) return;
|
if (name == null || name.trim().isEmpty()) return;
|
||||||
int w = 128, h = 128;
|
int w = 128, h = 128;
|
||||||
try {
|
try {
|
||||||
String wh = JOptionPane.showInputDialog(this, "输入尺寸(宽x高,例如 128x128)或留空使用 128x128:", "128x128");
|
String wh = JOptionPane.showInputDialog(SwingUtilities.getWindowAncestor(this), "输入尺寸(宽x高,例如 128x128)或留空使用 128x128:", "128x128");
|
||||||
if (wh != null && wh.contains("x")) {
|
if (wh != null && wh.contains("x")) {
|
||||||
String[] sp = wh.split("x");
|
String[] sp = wh.split("x");
|
||||||
w = Math.max(1, Integer.parseInt(sp[0].trim()));
|
w = Math.max(1, Integer.parseInt(sp[0].trim()));
|
||||||
@@ -995,6 +1111,8 @@ public class ModelLayerPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model.markNeedsUpdate();
|
model.markNeedsUpdate();
|
||||||
|
// 传入名称
|
||||||
|
operationManager.addLayer(part.getName());
|
||||||
reloadFromModel();
|
reloadFromModel();
|
||||||
selectPart(part);
|
selectPart(part);
|
||||||
thumbnailManager.generateThumbnail(part);
|
thumbnailManager.generateThumbnail(part);
|
||||||
@@ -1010,55 +1128,84 @@ public class ModelLayerPanel extends JPanel {
|
|||||||
// ====================================================================
|
// ====================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 【新增】处理外部文件拖放的 TransferHandler。
|
* 【新类】复合拖放处理器:统一处理外部文件拖放和内部图层重排。
|
||||||
* 支持拖放单个 .psd 或图片文件来创建图层。
|
* 将其设置给 JList (layerList),以确保它优先于 JScrollPane 捕获事件。
|
||||||
*/
|
*/
|
||||||
private class FileDropTransferHandler extends TransferHandler {
|
private class CompositeLayerTransferHandler extends TransferHandler {
|
||||||
|
private final LayerReorderTransferHandler internalReorderHandler;
|
||||||
private final List<String> IMAGE_EXTENSIONS = List.of("png", "jpg", "jpeg");
|
private final List<String> IMAGE_EXTENSIONS = List.of("png", "jpg", "jpeg");
|
||||||
private static final String PSD_EXTENSION = "psd";
|
private static final String PSD_EXTENSION = "psd";
|
||||||
|
|
||||||
|
public CompositeLayerTransferHandler(ModelLayerPanel panel) {
|
||||||
|
// 内部图层重排处理器实例
|
||||||
|
this.internalReorderHandler = new LayerReorderTransferHandler(panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSourceActions(JComponent c) {
|
||||||
|
// 委托给内部处理器处理拖出操作 (内部重排)
|
||||||
|
return internalReorderHandler.getSourceActions(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Transferable createTransferable(JComponent c) {
|
||||||
|
// 委托给内部处理器处理拖出数据 (内部重排)
|
||||||
|
return internalReorderHandler.createTransferable(c);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canImport(TransferSupport support) {
|
public boolean canImport(TransferSupport support) {
|
||||||
// 检查是否支持文件列表数据格式
|
// 1. 检查是否为外部文件拖放 (文件列表 DataFlavor) - 优先处理
|
||||||
return support.isDataFlavorSupported(DataFlavor.javaFileListFlavor);
|
if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 否则,委托给内部处理器处理 (图层重排)
|
||||||
|
return internalReorderHandler.canImport(support);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean importData(TransferSupport support) {
|
public boolean importData(TransferSupport support) {
|
||||||
if (!canImport(support)) return false;
|
// 1. 处理外部文件拖放
|
||||||
|
if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
||||||
|
try {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Transferable t = support.getTransferable();
|
||||||
|
List<File> files = (List<File>) t.getTransferData(DataFlavor.javaFileListFlavor);
|
||||||
|
|
||||||
try {
|
if (files.size() != 1) {
|
||||||
@SuppressWarnings("unchecked")
|
JOptionPane.showMessageDialog(ModelLayerPanel.this, "仅支持拖放单个文件。", "导入失败", JOptionPane.WARNING_MESSAGE);
|
||||||
List<File> files = (List<File>) support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (files.size() != 1) {
|
File file = files.get(0);
|
||||||
// 仅支持拖放单个文件
|
String fileName = file.getName().toLowerCase();
|
||||||
JOptionPane.showMessageDialog(ModelLayerPanel.this, "仅支持拖放单个文件。", "导入失败", JOptionPane.WARNING_MESSAGE);
|
String extension = "";
|
||||||
|
int dotIndex = fileName.lastIndexOf('.');
|
||||||
|
if (dotIndex != -1) {
|
||||||
|
extension = fileName.substring(dotIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PSD_EXTENSION.equals(extension)) {
|
||||||
|
psdImporter.importPSDFile(file);
|
||||||
|
return true;
|
||||||
|
} else if (IMAGE_EXTENSIONS.contains(extension)) {
|
||||||
|
createPartWithTextureFromFile(file);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
JOptionPane.showMessageDialog(ModelLayerPanel.this, "不支持的文件类型: ." + extension, "导入失败", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception ex) {
|
||||||
|
JOptionPane.showMessageDialog(ModelLayerPanel.this, "文件导入失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
|
||||||
|
ex.printStackTrace();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
File file = files.get(0);
|
|
||||||
String fileName = file.getName().toLowerCase();
|
|
||||||
String extension = fileName.substring(fileName.lastIndexOf('.') + 1);
|
|
||||||
|
|
||||||
if (PSD_EXTENSION.equals(extension)) {
|
|
||||||
// 导入 PSD 文件
|
|
||||||
psdImporter.importPSDFile(file);
|
|
||||||
return true;
|
|
||||||
} else if (IMAGE_EXTENSIONS.contains(extension)) {
|
|
||||||
// 创建带贴图的图层
|
|
||||||
createPartWithTextureFromFile(file);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
JOptionPane.showMessageDialog(ModelLayerPanel.this, "不支持的文件类型: ." + extension, "导入失败", JOptionPane.WARNING_MESSAGE);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception ex) {
|
|
||||||
JOptionPane.showMessageDialog(ModelLayerPanel.this, "文件导入失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
|
|
||||||
ex.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. 否则,委托给内部处理器处理 (图层重排)
|
||||||
|
return internalReorderHandler.importData(support);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1218,4 +1365,78 @@ public class ModelLayerPanel extends JPanel {
|
|||||||
return new Dimension(14, 14);
|
return new Dimension(14, 14);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 【只处理外部文件拖放】的处理器。
|
||||||
|
* 将其设置给顶层窗口的内容面板。
|
||||||
|
*/
|
||||||
|
private class FileDropOnlyTransferHandler extends TransferHandler {
|
||||||
|
private final List<String> IMAGE_EXTENSIONS = List.of("png", "jpg", "jpeg");
|
||||||
|
private static final String PSD_EXTENSION = "psd";
|
||||||
|
private static final String MODEL_EXTENSION = "model";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canImport(TransferSupport support) {
|
||||||
|
return support.isDataFlavorSupported(DataFlavor.javaFileListFlavor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean importData(TransferSupport support) {
|
||||||
|
if (!canImport(support)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Transferable t = support.getTransferable();
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<File> files = (List<File>) t.getTransferData(DataFlavor.javaFileListFlavor);
|
||||||
|
if (files.size() != 1) {
|
||||||
|
JOptionPane.showMessageDialog(SwingUtilities.getWindowAncestor(ModelLayerPanel.this), "仅支持拖放单个文件。", "导入失败", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final File file = files.get(0);
|
||||||
|
String fileName = file.getName().toLowerCase();
|
||||||
|
String extension = "";
|
||||||
|
int dotIndex = fileName.lastIndexOf('.');
|
||||||
|
if (dotIndex != -1) {
|
||||||
|
extension = fileName.substring(dotIndex + 1);
|
||||||
|
}
|
||||||
|
final String finalExtension = extension;
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
if (MODEL_EXTENSION.equals(finalExtension)) {
|
||||||
|
Window window = SwingUtilities.getWindowAncestor(ModelLayerPanel.this);
|
||||||
|
if (window instanceof MainWindow mainWindow) {
|
||||||
|
if (mainWindow.shouldAskUserToSave()) {
|
||||||
|
int confirm = JOptionPane.showConfirmDialog(
|
||||||
|
mainWindow,
|
||||||
|
"当前模型已修改。加载新模型 " + file.getName() + " 前是否保存更改?",
|
||||||
|
"加载模型确认",
|
||||||
|
JOptionPane.YES_NO_CANCEL_OPTION
|
||||||
|
);
|
||||||
|
if (confirm == JOptionPane.CANCEL_OPTION) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (confirm == JOptionPane.YES_OPTION) {
|
||||||
|
mainWindow.saveData(false);
|
||||||
|
mainWindow.loadModel(file.getAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
JOptionPane.showMessageDialog(SwingUtilities.getWindowAncestor(ModelLayerPanel.this), "无法获取主窗口引用,无法加载模型文件。", "导入失败", JOptionPane.ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
} else if (PSD_EXTENSION.equals(finalExtension)) {
|
||||||
|
psdImporter.importPSDFile(file);
|
||||||
|
} else if (IMAGE_EXTENSIONS.contains(finalExtension)) {
|
||||||
|
createPartWithTextureFromFile(file);
|
||||||
|
} else {
|
||||||
|
JOptionPane.showMessageDialog(SwingUtilities.getWindowAncestor(ModelLayerPanel.this), "不支持的文件类型: ." + finalExtension, "导入失败", JOptionPane.WARNING_MESSAGE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
JOptionPane.showMessageDialog(SwingUtilities.getWindowAncestor(ModelLayerPanel.this), "文件导入失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
|
||||||
|
ex.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -387,6 +387,16 @@ public class ModelRenderPanel extends JPanel {
|
|||||||
|
|
||||||
final float[][] modelCoords = {worldManagement.screenToModelCoordinates(e.getX(), e.getY())};
|
final float[][] modelCoords = {worldManagement.screenToModelCoordinates(e.getX(), e.getY())};
|
||||||
|
|
||||||
|
float modelX = modelCoords[0][0];
|
||||||
|
float modelY = modelCoords[0][1];
|
||||||
|
for (ModelClickListener listener : clickListeners) {
|
||||||
|
try {
|
||||||
|
listener.onModelHover(getSelectedMesh(), modelX, modelY, e.getX(), e.getY());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
logger.error("点击事件监听器执行出错", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 如果有激活的工具,优先交给工具处理
|
// 如果有激活的工具,优先交给工具处理
|
||||||
if (toolManagement.hasActiveTool() && modelCoords[0] != null) {
|
if (toolManagement.hasActiveTool() && modelCoords[0] != null) {
|
||||||
glContextManager.executeInGLContext(() -> toolManagement.handleMouseDragged(e, modelCoords[0][0], modelCoords[0][1]));
|
glContextManager.executeInGLContext(() -> toolManagement.handleMouseDragged(e, modelCoords[0][0], modelCoords[0][1]));
|
||||||
|
|||||||
@@ -94,11 +94,7 @@ public class KeyboardManager {
|
|||||||
logger.info("{}摄像机", newState ? "启用" : "禁用");
|
logger.info("{}摄像机", newState ? "启用" : "禁用");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 注册工具快捷键
|
|
||||||
registerToolShortcuts();
|
registerToolShortcuts();
|
||||||
|
|
||||||
// 设置键盘监听器
|
|
||||||
setupKeyListeners();
|
setupKeyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,17 +102,6 @@ public class KeyboardManager {
|
|||||||
* 注册工具快捷键
|
* 注册工具快捷键
|
||||||
*/
|
*/
|
||||||
private void registerToolShortcuts() {
|
private void registerToolShortcuts() {
|
||||||
// 木偶变形工具快捷键:Ctrl+P
|
|
||||||
registerShortcut("puppetTool", KeyStroke.getKeyStroke(KeyEvent.VK_P, KeyEvent.CTRL_DOWN_MASK),
|
|
||||||
new AbstractAction() {
|
|
||||||
@Override
|
|
||||||
public void actionPerformed(ActionEvent e) {
|
|
||||||
panel.switchTool("木偶变形工具");
|
|
||||||
logger.info("切换到木偶变形工具");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 顶点变形工具快捷键:Ctrl+T
|
|
||||||
registerShortcut("vertexTool", KeyStroke.getKeyStroke(KeyEvent.VK_T, KeyEvent.CTRL_DOWN_MASK),
|
registerShortcut("vertexTool", KeyStroke.getKeyStroke(KeyEvent.VK_T, KeyEvent.CTRL_DOWN_MASK),
|
||||||
new AbstractAction() {
|
new AbstractAction() {
|
||||||
@Override
|
@Override
|
||||||
@@ -125,16 +110,6 @@ public class KeyboardManager {
|
|||||||
logger.info("切换到顶点变形工具");
|
logger.info("切换到顶点变形工具");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 选择工具快捷键:Ctrl+S
|
|
||||||
registerShortcut("selectionTool", KeyStroke.getKeyStroke(KeyEvent.VK_S, KeyEvent.CTRL_DOWN_MASK),
|
|
||||||
new AbstractAction() {
|
|
||||||
@Override
|
|
||||||
public void actionPerformed(ActionEvent e) {
|
|
||||||
panel.switchTool("选择工具");
|
|
||||||
logger.info("切换到选择工具");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import javax.swing.tree.DefaultMutableTreeNode;
|
import javax.swing.tree.DefaultMutableTreeNode;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 2D模型部件,支持层级变换和变形器
|
* 2D模型部件,支持层级变换和变形器
|
||||||
@@ -50,7 +51,7 @@ public class ModelPart {
|
|||||||
private boolean boundsDirty;
|
private boolean boundsDirty;
|
||||||
private boolean pivotInitialized;
|
private boolean pivotInitialized;
|
||||||
|
|
||||||
private final List<ModelEvent> events = new ArrayList<>();
|
private final List<ModelEvent> events = new CopyOnWriteArrayList<>();
|
||||||
private boolean inMultiSelectionOperation = false;
|
private boolean inMultiSelectionOperation = false;
|
||||||
|
|
||||||
// ====== 液化模式枚举 ======
|
// ====== 液化模式枚举 ======
|
||||||
|
|||||||
@@ -678,7 +678,7 @@ public class Mesh2D {
|
|||||||
|
|
||||||
public boolean setOriginalPivot(Vector2f p) {
|
public boolean setOriginalPivot(Vector2f p) {
|
||||||
if (p != null) {
|
if (p != null) {
|
||||||
BoundingBox bounds = getBounds();
|
BoundingBox bounds = calculateOriginalBounds();
|
||||||
if (bounds != null &&
|
if (bounds != null &&
|
||||||
p.x >= bounds.getMinX() && p.x <= bounds.getMaxX() &&
|
p.x >= bounds.getMinX() && p.x <= bounds.getMaxX() &&
|
||||||
p.y >= bounds.getMinY() && p.y <= bounds.getMaxY()) {
|
p.y >= bounds.getMinY() && p.y <= bounds.getMaxY()) {
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.chuangzhou.vivid2D.window;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.event.ActionEvent;
|
||||||
|
import java.awt.event.InputEvent;
|
||||||
|
import java.awt.event.KeyEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 负责管理主窗口的全局快捷键。
|
||||||
|
*/
|
||||||
|
public class KeyBindingManager {
|
||||||
|
|
||||||
|
// Action 名称常量
|
||||||
|
private static final String ACTION_SAVE = "saveAction";
|
||||||
|
private static final String ACTION_SAVE_AS = "saveAsAction";
|
||||||
|
|
||||||
|
private final JRootPane rootPane;
|
||||||
|
private final MainWindow mainWindow;
|
||||||
|
|
||||||
|
public KeyBindingManager(MainWindow mainWindow) {
|
||||||
|
this.mainWindow = mainWindow;
|
||||||
|
this.rootPane = mainWindow.getRootPane();
|
||||||
|
setupGlobalKeyBindings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置全局快捷键绑定到 RootPane。
|
||||||
|
* 使用 JComponent.WHEN_IN_FOCUSED_WINDOW 确保在窗口获得焦点时生效。
|
||||||
|
*/
|
||||||
|
private void setupGlobalKeyBindings() {
|
||||||
|
// 获取 InputMap 和 ActionMap
|
||||||
|
InputMap inputMap = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
|
||||||
|
ActionMap actionMap = rootPane.getActionMap();
|
||||||
|
|
||||||
|
// 绑定动作
|
||||||
|
actionMap.put(ACTION_SAVE, new SaveAction());
|
||||||
|
actionMap.put(ACTION_SAVE_AS, new SaveAsAction());
|
||||||
|
|
||||||
|
// 绑定快捷键 KeyStroke
|
||||||
|
bindKey(inputMap, KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK, ACTION_SAVE);
|
||||||
|
bindKey(inputMap, KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK, ACTION_SAVE_AS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 辅助方法:简化 KeyStroke 和 Action 的绑定。
|
||||||
|
*/
|
||||||
|
public void bindKey(InputMap inputMap, int keyCode, int modifiers, String actionName) {
|
||||||
|
KeyStroke key = KeyStroke.getKeyStroke(keyCode, modifiers);
|
||||||
|
inputMap.put(key, actionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部类:Ctrl + S 保存动作。
|
||||||
|
*/
|
||||||
|
private class SaveAction extends AbstractAction {
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent e) {
|
||||||
|
// 调用 MainWindow 的保存方法 (不退出)
|
||||||
|
mainWindow.saveData(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部类:Ctrl + Shift + S 另存为动作。
|
||||||
|
*/
|
||||||
|
private class SaveAsAction extends AbstractAction {
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent e) {
|
||||||
|
// 另存为操作:强制进入 "另存为" 逻辑
|
||||||
|
String originalPath = mainWindow.currentModelPath;
|
||||||
|
|
||||||
|
// 临时将路径设为 null
|
||||||
|
mainWindow.currentModelPath = null;
|
||||||
|
mainWindow.saveData(false);
|
||||||
|
|
||||||
|
// 如果用户取消另存为 (saveData 中 currentModelPath 仍为 null),恢复原来的路径
|
||||||
|
if (mainWindow.currentModelPath == null) {
|
||||||
|
mainWindow.currentModelPath = originalPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,12 +8,10 @@ import com.chuangzhou.vivid2D.render.awt.manager.data.ParametersManagementData;
|
|||||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||||
import com.formdev.flatlaf.themes.FlatMacDarkLaf;
|
|
||||||
import jnafilechooser.api.JnaFileChooser;
|
import jnafilechooser.api.JnaFileChooser;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.event.WindowAdapter;
|
import java.awt.event.WindowAdapter;
|
||||||
import java.awt.event.WindowEvent;
|
import java.awt.event.WindowEvent;
|
||||||
@@ -37,9 +35,11 @@ public class MainWindow extends JFrame {
|
|||||||
private final TransformPanel transformPanel;
|
private final TransformPanel transformPanel;
|
||||||
private final ParametersPanel parametersPanel;
|
private final ParametersPanel parametersPanel;
|
||||||
private final ModelPartInfoPanel partInfoPanel;
|
private final ModelPartInfoPanel partInfoPanel;
|
||||||
private String currentModelPath = null;
|
private final KeyBindingManager keyBindingManager;
|
||||||
|
public String currentModelPath = null;
|
||||||
private JLabel statusBarLabel;
|
private JLabel statusBarLabel;
|
||||||
private JMenuBar menuBar;
|
private JMenuBar menuBar;
|
||||||
|
private boolean isModelModified = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构造主窗口。
|
* 构造主窗口。
|
||||||
@@ -61,6 +61,7 @@ public class MainWindow extends JFrame {
|
|||||||
setupInitialListeners();
|
setupInitialListeners();
|
||||||
setSize(1600, 900);
|
setSize(1600, 900);
|
||||||
setLocationRelativeTo(null);
|
setLocationRelativeTo(null);
|
||||||
|
keyBindingManager = new KeyBindingManager(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,6 +119,7 @@ public class MainWindow extends JFrame {
|
|||||||
statusBarLabel.setText("新模型 " + finalModelName + " 创建并加载完毕。");
|
statusBarLabel.setText("新模型 " + finalModelName + " 创建并加载完毕。");
|
||||||
setEditComponentsEnabled(true);
|
setEditComponentsEnabled(true);
|
||||||
layerPanel.setModel(newModel);
|
layerPanel.setModel(newModel);
|
||||||
|
setModelModified(false);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.err.println("新建模型加载失败: " + e.getMessage());
|
System.err.println("新建模型加载失败: " + e.getMessage());
|
||||||
currentModelPath = null;
|
currentModelPath = null;
|
||||||
@@ -215,21 +217,21 @@ public class MainWindow extends JFrame {
|
|||||||
addWindowListener(new WindowAdapter() {
|
addWindowListener(new WindowAdapter() {
|
||||||
@Override
|
@Override
|
||||||
public void windowClosing(WindowEvent e) {
|
public void windowClosing(WindowEvent e) {
|
||||||
if (currentModelPath == null) {
|
if (shouldAskUserToSave()) {
|
||||||
shutdown();
|
int confirm = JOptionPane.showConfirmDialog(
|
||||||
return;
|
MainWindow.this,
|
||||||
}
|
"模型已修改。是否在退出前保存更改?",
|
||||||
int confirm = JOptionPane.showConfirmDialog(
|
"退出确认",
|
||||||
MainWindow.this,
|
JOptionPane.YES_NO_CANCEL_OPTION
|
||||||
"是否在退出前保存更改?",
|
);
|
||||||
"退出确认",
|
if (confirm == JOptionPane.CANCEL_OPTION) {
|
||||||
JOptionPane.YES_NO_CANCEL_OPTION
|
return;
|
||||||
);
|
}
|
||||||
if (confirm == JOptionPane.CANCEL_OPTION) {
|
if (confirm == JOptionPane.YES_OPTION) {
|
||||||
return;
|
saveData(true);
|
||||||
}
|
} else {
|
||||||
if (confirm == JOptionPane.YES_OPTION) {
|
shutdown();
|
||||||
saveData(true);
|
}
|
||||||
} else {
|
} else {
|
||||||
shutdown();
|
shutdown();
|
||||||
}
|
}
|
||||||
@@ -244,6 +246,7 @@ public class MainWindow extends JFrame {
|
|||||||
layerPanel.setSelectedLayers(selectedPart);
|
layerPanel.setSelectedLayers(selectedPart);
|
||||||
transformPanel.setSelectedParts(selectedPart);
|
transformPanel.setSelectedParts(selectedPart);
|
||||||
if (!selectedPart.isEmpty()) {
|
if (!selectedPart.isEmpty()) {
|
||||||
|
setModelModified(true);
|
||||||
partInfoPanel.updatePanel(selectedPart.get(0));
|
partInfoPanel.updatePanel(selectedPart.get(0));
|
||||||
} else {
|
} else {
|
||||||
partInfoPanel.updatePanel(null);
|
partInfoPanel.updatePanel(null);
|
||||||
@@ -308,7 +311,7 @@ public class MainWindow extends JFrame {
|
|||||||
/**
|
/**
|
||||||
* 加载模型并更新 UI 状态。
|
* 加载模型并更新 UI 状态。
|
||||||
*/
|
*/
|
||||||
private void loadModel(String modelPath) {
|
public void loadModel(String modelPath) {
|
||||||
setEditComponentsEnabled(false);
|
setEditComponentsEnabled(false);
|
||||||
statusBarLabel.setText("正在加载模型: " + modelPath);
|
statusBarLabel.setText("正在加载模型: " + modelPath);
|
||||||
CompletableFuture.runAsync(() -> {
|
CompletableFuture.runAsync(() -> {
|
||||||
@@ -337,6 +340,7 @@ public class MainWindow extends JFrame {
|
|||||||
statusBarLabel.setText("模型加载完毕。");
|
statusBarLabel.setText("模型加载完毕。");
|
||||||
setEditComponentsEnabled(true);
|
setEditComponentsEnabled(true);
|
||||||
layerPanel.setModel(finalModel);
|
layerPanel.setModel(finalModel);
|
||||||
|
setModelModified(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -346,28 +350,16 @@ public class MainWindow extends JFrame {
|
|||||||
* 保存模型和参数数据。
|
* 保存模型和参数数据。
|
||||||
* @param exitOnComplete 如果为 true,则在保存后调用 shutdown()。
|
* @param exitOnComplete 如果为 true,则在保存后调用 shutdown()。
|
||||||
*/
|
*/
|
||||||
private void saveData(boolean exitOnComplete) {
|
public void saveData(boolean exitOnComplete) {
|
||||||
if (currentModelPath == null) {
|
if (currentModelPath == null) {
|
||||||
JnaFileChooser jnaFileChooser = new JnaFileChooser();
|
JnaFileChooser jnaFileChooser = getJnaFileChooser();
|
||||||
jnaFileChooser.setTitle("另存为 Vivid2D 模型文件 (*.model)");
|
|
||||||
|
|
||||||
// JnaFileChooser 使用 addFilter() 来添加过滤器
|
|
||||||
jnaFileChooser.addFilter("Vivid2D 模型文件 (*.model)", "model");
|
|
||||||
jnaFileChooser.setMultiSelectionEnabled(false);
|
|
||||||
jnaFileChooser.setOpenButtonText("保存");
|
|
||||||
jnaFileChooser.setMode(JnaFileChooser.Mode.Files);
|
|
||||||
|
|
||||||
// 弹出保存对话框
|
|
||||||
if (jnaFileChooser.showSaveDialog(this)) {
|
if (jnaFileChooser.showSaveDialog(this)) {
|
||||||
File fileToSave = jnaFileChooser.getSelectedFile();
|
File fileToSave = jnaFileChooser.getSelectedFile();
|
||||||
String path = fileToSave.getAbsolutePath();
|
String path = fileToSave.getAbsolutePath();
|
||||||
|
|
||||||
// 确保文件以 .model 结尾 (原生对话框可能已经处理,但 Swing 风格代码保留以防万一)
|
|
||||||
if (!path.toLowerCase().endsWith(".model")) {
|
if (!path.toLowerCase().endsWith(".model")) {
|
||||||
path += ".model";
|
path += ".model";
|
||||||
fileToSave = new File(path);
|
fileToSave = new File(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentModelPath = path;
|
this.currentModelPath = path;
|
||||||
setTitle("Vivid2D Editor - " + fileToSave.getName());
|
setTitle("Vivid2D Editor - " + fileToSave.getName());
|
||||||
} else {
|
} else {
|
||||||
@@ -393,11 +385,57 @@ public class MainWindow extends JFrame {
|
|||||||
statusBarLabel.setText("保存参数失败!");
|
statusBarLabel.setText("保存参数失败!");
|
||||||
}
|
}
|
||||||
statusBarLabel.setText("保存成功。");
|
statusBarLabel.setText("保存成功。");
|
||||||
|
setModelModified(false);
|
||||||
if (exitOnComplete) {
|
if (exitOnComplete) {
|
||||||
shutdown();
|
shutdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private @NotNull JnaFileChooser getJnaFileChooser() {
|
||||||
|
JnaFileChooser jnaFileChooser = new JnaFileChooser();
|
||||||
|
jnaFileChooser.setTitle("另存为 Vivid2D 模型文件 (*.model)");
|
||||||
|
jnaFileChooser.addFilter("Vivid2D 模型文件 (*.model)", "model");
|
||||||
|
jnaFileChooser.setMultiSelectionEnabled(false);
|
||||||
|
jnaFileChooser.setOpenButtonText("保存");
|
||||||
|
jnaFileChooser.setMode(JnaFileChooser.Mode.Files);
|
||||||
|
String defaultFileName;
|
||||||
|
Model2D currentModel = renderPanel.getModel();
|
||||||
|
if (currentModel != null && currentModel.getName() != null && !currentModel.getName().trim().isEmpty()) {
|
||||||
|
defaultFileName = currentModel.getName() + ".model";
|
||||||
|
} else {
|
||||||
|
defaultFileName = "model.model";
|
||||||
|
}
|
||||||
|
jnaFileChooser.setDefaultFileName(defaultFileName);
|
||||||
|
return jnaFileChooser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取模型是否已修改的状态。
|
||||||
|
*/
|
||||||
|
public boolean isModelModified() {
|
||||||
|
return isModelModified;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑面板在进行任何修改操作时应调用 setModelModified(true)。
|
||||||
|
* 保存或加载成功后应调用 setModelModified(false)。
|
||||||
|
*/
|
||||||
|
public void setModelModified(boolean modified) {
|
||||||
|
this.isModelModified = modified;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前是否需要询问用户保存模型。
|
||||||
|
* 只要模型被修改过,就应该询问。
|
||||||
|
*/
|
||||||
|
public boolean shouldAskUserToSave() {
|
||||||
|
return isModelModified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyBindingManager getKeyBindingManager() {
|
||||||
|
return keyBindingManager;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清理资源并退出应用程序。
|
* 清理资源并退出应用程序。
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user