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.util.*;
|
||||
import com.chuangzhou.vivid2D.render.awt.util.renderer.LayerCellRenderer;
|
||||
// 确保 LayerReorderTransferHandler 被正确导入,它处理内部重排
|
||||
import com.chuangzhou.vivid2D.render.awt.util.renderer.LayerReorderTransferHandler;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Texture;
|
||||
import com.chuangzhou.vivid2D.window.MainWindow;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
@@ -18,6 +20,8 @@ import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.border.TitledBorder;
|
||||
import javax.swing.plaf.basic.BasicSliderUI;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
@@ -34,10 +38,13 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
// 引入 JnaFileChooser
|
||||
import jnafilechooser.api.JnaFileChooser;
|
||||
// 引入拖放相关的类
|
||||
// 引入拖放和快捷键相关的类
|
||||
import java.awt.datatransfer.DataFlavor;
|
||||
import java.awt.datatransfer.Transferable;
|
||||
import javax.swing.TransferHandler;
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.Action;
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
|
||||
public class ModelLayerPanel extends JPanel {
|
||||
@@ -99,9 +106,8 @@ public class ModelLayerPanel extends JPanel {
|
||||
setupModernLookAndFeel();
|
||||
this.thumbnailManager = new ThumbnailManager(renderPanel);
|
||||
|
||||
// --- 新增:设置外部文件拖放处理器 ---
|
||||
this.setTransferHandler(new FileDropTransferHandler());
|
||||
// ---------------------------------
|
||||
// FIX: 移除在 this 上的 TransferHandler,因为 JList 将会覆盖它
|
||||
// this.setTransferHandler(new FileDropTransferHandler());
|
||||
|
||||
if (this.model != null) {
|
||||
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() {
|
||||
String modelDataPath = renderPanel.getGlContextManager().getModelPath() + ".data";
|
||||
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(modelDataPath))) {
|
||||
@@ -185,6 +204,10 @@ public class ModelLayerPanel extends JPanel {
|
||||
gbc.fill = GridBagConstraints.BOTH;
|
||||
add(centerScrollPane, gbc);
|
||||
|
||||
setupWindowFileDropHandler();
|
||||
// 绑定快捷键
|
||||
setupKeyBindings(layerList);
|
||||
|
||||
JPanel controlPanel = createControlPanel();
|
||||
gbc.gridy = 2;
|
||||
gbc.weighty = 0.0;
|
||||
@@ -193,9 +216,37 @@ public class ModelLayerPanel extends JPanel {
|
||||
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() {
|
||||
JList<ModelPart> list = new JList<>(listModel);
|
||||
// 【修正 1:启用多选】
|
||||
// 启用多选
|
||||
list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
|
||||
list.setBackground(SURFACE_COLOR);
|
||||
list.setForeground(TEXT_COLOR);
|
||||
@@ -205,8 +256,10 @@ public class ModelLayerPanel extends JPanel {
|
||||
cellRenderer.attachMouseListener(list, listModel);
|
||||
list.setCellRenderer(cellRenderer);
|
||||
list.setDragEnabled(true);
|
||||
// 【修正 2:使用多选 TransferHandler】
|
||||
list.setTransferHandler(new LayerReorderTransferHandler(this));
|
||||
|
||||
// FIX: 使用新的复合 TransferHandler 统一处理文件拖放和内部重排
|
||||
list.setTransferHandler(new CompositeLayerTransferHandler(this));
|
||||
|
||||
list.setDropMode(DropMode.INSERT);
|
||||
list.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
@@ -436,7 +489,7 @@ public class ModelLayerPanel extends JPanel {
|
||||
|
||||
/**
|
||||
* 【新增方法】执行多选拖拽后的图层块重排序操作。
|
||||
* 供 LayerReorderTransferHandler 调用。
|
||||
* 供 CompositeLayerTransferHandler (原 LayerReorderTransferHandler) 调用。
|
||||
* @param srcIndices 列表中的视觉源索引数组(从上到下,已排序)。
|
||||
* @param dropIndex 列表中的视觉目标插入索引。
|
||||
*/
|
||||
@@ -486,7 +539,7 @@ public class ModelLayerPanel extends JPanel {
|
||||
}
|
||||
|
||||
removeButton.setEnabled(hasSelection);
|
||||
// 【修正 3:多选时启用上下移动按钮】
|
||||
// 多选时启用上下移动按钮
|
||||
upButton.setEnabled(hasSelection);
|
||||
downButton.setEnabled(hasSelection);
|
||||
// 绑定贴图仍然只在单选时有意义
|
||||
@@ -608,9 +661,10 @@ public class ModelLayerPanel extends JPanel {
|
||||
}
|
||||
|
||||
private void createEmptyPart() {
|
||||
String name = JOptionPane.showInputDialog(this, "新图层名称:", "新图层");
|
||||
String name = JOptionPane.showInputDialog(SwingUtilities.getWindowAncestor(this), "新图层名称:", "新图层");
|
||||
if (name == null || name.trim().isEmpty()) return;
|
||||
|
||||
// 传入名称
|
||||
operationManager.addLayer(name);
|
||||
reloadFromModel();
|
||||
|
||||
@@ -634,7 +688,7 @@ public class ModelLayerPanel extends JPanel {
|
||||
|
||||
private void showRenameDialog(ModelPart part) {
|
||||
String newName = (String) JOptionPane.showInputDialog(
|
||||
this,
|
||||
SwingUtilities.getWindowAncestor(this),
|
||||
"输入新名称:",
|
||||
"重命名图层",
|
||||
JOptionPane.PLAIN_MESSAGE,
|
||||
@@ -793,7 +847,19 @@ public class ModelLayerPanel extends JPanel {
|
||||
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||
try {
|
||||
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.markNeedsUpdate();
|
||||
} catch (Throwable ex) {
|
||||
@@ -822,27 +888,32 @@ public class ModelLayerPanel extends JPanel {
|
||||
}
|
||||
|
||||
private void onRemoveLayer() {
|
||||
// 修正:支持删除多个选中的图层
|
||||
List<ModelPart> selectedParts = layerList.getSelectedValuesList();
|
||||
if (selectedParts.isEmpty()) return;
|
||||
|
||||
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;
|
||||
|
||||
try {
|
||||
for(ModelPart part : selectedParts) {
|
||||
operationManager.removeLayer(part);
|
||||
thumbnailManager.removeThumbnail(part);
|
||||
// 仅移除第一个选中项的参数管理(这是一个简化,实际应用中可能需要遍历移除)
|
||||
if (part == selectedParts.get(0)) {
|
||||
renderPanel.getParametersManagement().removeParameter(part, "all");
|
||||
renderPanel.getGlContextManager().executeInGLContext(() -> renderPanel.getParametersManagement().removeParameter(part, "all"));
|
||||
if (renderPanel != null && renderPanel.getParametersManagement() != null) {
|
||||
renderPanel.getParametersManagement().removeParameter(part, "all");
|
||||
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||
if (renderPanel != null && renderPanel.getParametersManagement() != null) {
|
||||
renderPanel.getParametersManagement().removeParameter(part, "all");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
reloadFromModel();
|
||||
} 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 {
|
||||
BufferedImage img = ImageIO.read(f);
|
||||
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();
|
||||
|
||||
// --- 修复:确保图层名称唯一性 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);
|
||||
Mesh2D mesh = MeshTextureUtil.createQuadForImage(img, name + "_mesh");
|
||||
mesh.createDefaultSecondaryVertices();
|
||||
// 修复上一个问题中 GL Context lambda 无法解析 'mesh' 的编译错误
|
||||
final Mesh2D mesh = MeshTextureUtil.createQuadForImage(img, name + "_mesh");
|
||||
part.addMesh(mesh);
|
||||
|
||||
if (renderPanel != null) {
|
||||
@@ -970,11 +1086,11 @@ public class ModelLayerPanel extends JPanel {
|
||||
}
|
||||
|
||||
private void createPartWithTransparentTexture() {
|
||||
String name = JOptionPane.showInputDialog(this, "新图层名称(透明):", "透明图层");
|
||||
String name = JOptionPane.showInputDialog(SwingUtilities.getWindowAncestor(this), "新图层名称(透明):", "透明图层");
|
||||
if (name == null || name.trim().isEmpty()) return;
|
||||
int w = 128, h = 128;
|
||||
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")) {
|
||||
String[] sp = wh.split("x");
|
||||
w = Math.max(1, Integer.parseInt(sp[0].trim()));
|
||||
@@ -995,6 +1111,8 @@ public class ModelLayerPanel extends JPanel {
|
||||
}
|
||||
|
||||
model.markNeedsUpdate();
|
||||
// 传入名称
|
||||
operationManager.addLayer(part.getName());
|
||||
reloadFromModel();
|
||||
selectPart(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 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
|
||||
public boolean canImport(TransferSupport support) {
|
||||
// 检查是否支持文件列表数据格式
|
||||
return support.isDataFlavorSupported(DataFlavor.javaFileListFlavor);
|
||||
// 1. 检查是否为外部文件拖放 (文件列表 DataFlavor) - 优先处理
|
||||
if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. 否则,委托给内部处理器处理 (图层重排)
|
||||
return internalReorderHandler.canImport(support);
|
||||
}
|
||||
|
||||
@Override
|
||||
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 {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<File> files = (List<File>) support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
|
||||
if (files.size() != 1) {
|
||||
JOptionPane.showMessageDialog(ModelLayerPanel.this, "仅支持拖放单个文件。", "导入失败", JOptionPane.WARNING_MESSAGE);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (files.size() != 1) {
|
||||
// 仅支持拖放单个文件
|
||||
JOptionPane.showMessageDialog(ModelLayerPanel.this, "仅支持拖放单个文件。", "导入失败", JOptionPane.WARNING_MESSAGE);
|
||||
File file = files.get(0);
|
||||
String fileName = file.getName().toLowerCase();
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 【只处理外部文件拖放】的处理器。
|
||||
* 将其设置给顶层窗口的内容面板。
|
||||
*/
|
||||
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())};
|
||||
|
||||
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) {
|
||||
glContextManager.executeInGLContext(() -> toolManagement.handleMouseDragged(e, modelCoords[0][0], modelCoords[0][1]));
|
||||
|
||||
@@ -94,11 +94,7 @@ public class KeyboardManager {
|
||||
logger.info("{}摄像机", newState ? "启用" : "禁用");
|
||||
}
|
||||
});
|
||||
|
||||
// 注册工具快捷键
|
||||
registerToolShortcuts();
|
||||
|
||||
// 设置键盘监听器
|
||||
setupKeyListeners();
|
||||
}
|
||||
|
||||
@@ -106,17 +102,6 @@ public class KeyboardManager {
|
||||
* 注册工具快捷键
|
||||
*/
|
||||
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),
|
||||
new AbstractAction() {
|
||||
@Override
|
||||
@@ -125,16 +110,6 @@ public class KeyboardManager {
|
||||
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 java.util.*;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* 2D模型部件,支持层级变换和变形器
|
||||
@@ -50,7 +51,7 @@ public class ModelPart {
|
||||
private boolean boundsDirty;
|
||||
private boolean pivotInitialized;
|
||||
|
||||
private final List<ModelEvent> events = new ArrayList<>();
|
||||
private final List<ModelEvent> events = new CopyOnWriteArrayList<>();
|
||||
private boolean inMultiSelectionOperation = false;
|
||||
|
||||
// ====== 液化模式枚举 ======
|
||||
|
||||
@@ -678,7 +678,7 @@ public class Mesh2D {
|
||||
|
||||
public boolean setOriginalPivot(Vector2f p) {
|
||||
if (p != null) {
|
||||
BoundingBox bounds = getBounds();
|
||||
BoundingBox bounds = calculateOriginalBounds();
|
||||
if (bounds != null &&
|
||||
p.x >= bounds.getMinX() && p.x <= bounds.getMaxX() &&
|
||||
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.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.formdev.flatlaf.themes.FlatMacDarkLaf;
|
||||
import jnafilechooser.api.JnaFileChooser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||
import java.awt.*;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
@@ -37,9 +35,11 @@ public class MainWindow extends JFrame {
|
||||
private final TransformPanel transformPanel;
|
||||
private final ParametersPanel parametersPanel;
|
||||
private final ModelPartInfoPanel partInfoPanel;
|
||||
private String currentModelPath = null;
|
||||
private final KeyBindingManager keyBindingManager;
|
||||
public String currentModelPath = null;
|
||||
private JLabel statusBarLabel;
|
||||
private JMenuBar menuBar;
|
||||
private boolean isModelModified = false;
|
||||
|
||||
/**
|
||||
* 构造主窗口。
|
||||
@@ -61,6 +61,7 @@ public class MainWindow extends JFrame {
|
||||
setupInitialListeners();
|
||||
setSize(1600, 900);
|
||||
setLocationRelativeTo(null);
|
||||
keyBindingManager = new KeyBindingManager(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,6 +119,7 @@ public class MainWindow extends JFrame {
|
||||
statusBarLabel.setText("新模型 " + finalModelName + " 创建并加载完毕。");
|
||||
setEditComponentsEnabled(true);
|
||||
layerPanel.setModel(newModel);
|
||||
setModelModified(false);
|
||||
} catch (Exception e) {
|
||||
System.err.println("新建模型加载失败: " + e.getMessage());
|
||||
currentModelPath = null;
|
||||
@@ -215,21 +217,21 @@ public class MainWindow extends JFrame {
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
if (currentModelPath == null) {
|
||||
shutdown();
|
||||
return;
|
||||
}
|
||||
int confirm = JOptionPane.showConfirmDialog(
|
||||
MainWindow.this,
|
||||
"是否在退出前保存更改?",
|
||||
"退出确认",
|
||||
JOptionPane.YES_NO_CANCEL_OPTION
|
||||
);
|
||||
if (confirm == JOptionPane.CANCEL_OPTION) {
|
||||
return;
|
||||
}
|
||||
if (confirm == JOptionPane.YES_OPTION) {
|
||||
saveData(true);
|
||||
if (shouldAskUserToSave()) {
|
||||
int confirm = JOptionPane.showConfirmDialog(
|
||||
MainWindow.this,
|
||||
"模型已修改。是否在退出前保存更改?",
|
||||
"退出确认",
|
||||
JOptionPane.YES_NO_CANCEL_OPTION
|
||||
);
|
||||
if (confirm == JOptionPane.CANCEL_OPTION) {
|
||||
return;
|
||||
}
|
||||
if (confirm == JOptionPane.YES_OPTION) {
|
||||
saveData(true);
|
||||
} else {
|
||||
shutdown();
|
||||
}
|
||||
} else {
|
||||
shutdown();
|
||||
}
|
||||
@@ -244,6 +246,7 @@ public class MainWindow extends JFrame {
|
||||
layerPanel.setSelectedLayers(selectedPart);
|
||||
transformPanel.setSelectedParts(selectedPart);
|
||||
if (!selectedPart.isEmpty()) {
|
||||
setModelModified(true);
|
||||
partInfoPanel.updatePanel(selectedPart.get(0));
|
||||
} else {
|
||||
partInfoPanel.updatePanel(null);
|
||||
@@ -308,7 +311,7 @@ public class MainWindow extends JFrame {
|
||||
/**
|
||||
* 加载模型并更新 UI 状态。
|
||||
*/
|
||||
private void loadModel(String modelPath) {
|
||||
public void loadModel(String modelPath) {
|
||||
setEditComponentsEnabled(false);
|
||||
statusBarLabel.setText("正在加载模型: " + modelPath);
|
||||
CompletableFuture.runAsync(() -> {
|
||||
@@ -337,6 +340,7 @@ public class MainWindow extends JFrame {
|
||||
statusBarLabel.setText("模型加载完毕。");
|
||||
setEditComponentsEnabled(true);
|
||||
layerPanel.setModel(finalModel);
|
||||
setModelModified(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -346,28 +350,16 @@ public class MainWindow extends JFrame {
|
||||
* 保存模型和参数数据。
|
||||
* @param exitOnComplete 如果为 true,则在保存后调用 shutdown()。
|
||||
*/
|
||||
private void saveData(boolean exitOnComplete) {
|
||||
public void saveData(boolean exitOnComplete) {
|
||||
if (currentModelPath == null) {
|
||||
JnaFileChooser jnaFileChooser = new JnaFileChooser();
|
||||
jnaFileChooser.setTitle("另存为 Vivid2D 模型文件 (*.model)");
|
||||
|
||||
// JnaFileChooser 使用 addFilter() 来添加过滤器
|
||||
jnaFileChooser.addFilter("Vivid2D 模型文件 (*.model)", "model");
|
||||
jnaFileChooser.setMultiSelectionEnabled(false);
|
||||
jnaFileChooser.setOpenButtonText("保存");
|
||||
jnaFileChooser.setMode(JnaFileChooser.Mode.Files);
|
||||
|
||||
// 弹出保存对话框
|
||||
JnaFileChooser jnaFileChooser = getJnaFileChooser();
|
||||
if (jnaFileChooser.showSaveDialog(this)) {
|
||||
File fileToSave = jnaFileChooser.getSelectedFile();
|
||||
String path = fileToSave.getAbsolutePath();
|
||||
|
||||
// 确保文件以 .model 结尾 (原生对话框可能已经处理,但 Swing 风格代码保留以防万一)
|
||||
if (!path.toLowerCase().endsWith(".model")) {
|
||||
path += ".model";
|
||||
fileToSave = new File(path);
|
||||
}
|
||||
|
||||
this.currentModelPath = path;
|
||||
setTitle("Vivid2D Editor - " + fileToSave.getName());
|
||||
} else {
|
||||
@@ -393,11 +385,57 @@ public class MainWindow extends JFrame {
|
||||
statusBarLabel.setText("保存参数失败!");
|
||||
}
|
||||
statusBarLabel.setText("保存成功。");
|
||||
setModelModified(false);
|
||||
if (exitOnComplete) {
|
||||
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