wangxichen hace 6 días
padre
commit
3532806a5c
Se han modificado 2 ficheros con 984 adiciones y 0 borrados
  1. 4 0
      src/com/mes/ui/MesClient.java
  2. 980 0
      src/com/mes/ui/VisualUiEditorPanel.java

+ 4 - 0
src/com/mes/ui/MesClient.java

@@ -871,6 +871,8 @@ public class MesClient extends JFrame {
         modbusBtn3.setBounds(568, 241, 160, 60);
 //        indexPanelA.add(modbusBtn3);
 
+        VisualUiEditorPanel.applySavedDesign(indexPanelA);
+
         tabbedPane.addTab("工作面板", new ImageIcon(MesClient.class.getResource("/bg/a_side.png")), indexScrollPaneA, null);
         tabbedPane.setEnabledAt(0, true);
 
@@ -889,6 +891,8 @@ public class MesClient extends JFrame {
 
 		tabbedPane.addTab("工作记录", new ImageIcon(MesClient.class.getResource("/bg/menu_data_preprocess.png")), searchScrollPane, null);
 
+        tabbedPane.addTab("UI设计", null, new VisualUiEditorPanel(indexPanelA), null);
+
 
 		tabbedPane.addChangeListener(new ChangeListener() {
             @Override

+ 980 - 0
src/com/mes/ui/VisualUiEditorPanel.java

@@ -0,0 +1,980 @@
+package com.mes.ui;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+
+import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSplitPane;
+import javax.swing.JTabbedPane;
+import javax.swing.JTextField;
+import javax.swing.KeyStroke;
+import javax.swing.SwingConstants;
+import javax.swing.SwingUtilities;
+import javax.swing.TransferHandler;
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.Cursor;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.GridLayout;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.StringSelection;
+import java.awt.event.ActionEvent;
+import java.awt.event.FocusAdapter;
+import java.awt.event.FocusEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class VisualUiEditorPanel extends JPanel {
+    private static final int CANVAS_WIDTH = 990;
+    private static final int CANVAS_HEIGHT = 550;
+    private static final int GRID_SIZE = 8;
+    private static final int HANDLE_SIZE = 10;
+    private static final String DESIGN_FILE = "ui-designs/op40-main.ui.json";
+
+    private final File designFile;
+    private final Container sourceContainer;
+    private final Map<String, Component> sourceComponents = new HashMap<String, Component>();
+    private final JPanel canvas;
+    private final JPanel previewCanvas;
+    private final JLabel statusLabel;
+    private final Deque<String> undoStack = new ArrayDeque<String>();
+    private final Deque<String> redoStack = new ArrayDeque<String>();
+
+    private JTextField textField;
+    private JTextField xField;
+    private JTextField yField;
+    private JTextField widthField;
+    private JTextField heightField;
+    private JSONObject schema;
+    private String selectedId;
+    private boolean dirty;
+
+    public VisualUiEditorPanel() {
+        this(null);
+    }
+
+    public VisualUiEditorPanel(Container sourceContainer) {
+        this.sourceContainer = sourceContainer;
+        this.designFile = new File(System.getProperty("user.dir"), DESIGN_FILE);
+        this.schema = loadSchema();
+        if (sourceContainer != null) {
+            indexSourceComponents(sourceContainer, sourceComponents);
+        }
+        setLayout(new BorderLayout(8, 8));
+        setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
+
+        JPanel palette = createPalette();
+        JPanel inspector = createInspector();
+        this.canvas = createCanvas();
+        this.previewCanvas = createPreviewCanvas();
+        this.statusLabel = new JLabel("Ready");
+
+        JTabbedPane centerTabs = new JTabbedPane();
+        centerTabs.addTab("Design", new JScrollPane(canvas));
+        centerTabs.addTab("Preview", new JScrollPane(previewCanvas));
+
+        JSplitPane rightSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT, palette, inspector);
+        rightSplit.setResizeWeight(0.35);
+        rightSplit.setPreferredSize(new Dimension(250, 500));
+
+        add(centerTabs, BorderLayout.CENTER);
+        add(rightSplit, BorderLayout.EAST);
+        add(statusLabel, BorderLayout.SOUTH);
+
+        installShortcuts();
+        renderAll();
+    }
+
+    private JPanel createPalette() {
+        JPanel panel = new JPanel(new GridLayout(0, 1, 6, 6));
+        panel.setBorder(BorderFactory.createTitledBorder("Palette"));
+        addPaletteButton(panel, "Label", "label");
+        addPaletteButton(panel, "Button", "button");
+        addPaletteButton(panel, "Text Field", "textfield");
+        addPaletteButton(panel, "Panel", "panel");
+        return panel;
+    }
+
+    private void addPaletteButton(JPanel panel, String title, final String type) {
+        JButton button = new JButton(title);
+        button.setFocusable(false);
+        button.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(ActionEvent e) {
+                addComponentAt(type, 40 + schema.getJSONArray("components").size() * 16, 40 + schema.getJSONArray("components").size() * 16);
+            }
+        });
+        button.setTransferHandler(new TransferHandler("text") {
+            protected java.awt.datatransfer.Transferable createTransferable(JComponent c) {
+                return new StringSelection(type);
+            }
+
+            public int getSourceActions(JComponent c) {
+                return COPY;
+            }
+        });
+        button.addMouseMotionListener(new MouseAdapter() {
+            public void mouseDragged(MouseEvent e) {
+                JComponent component = (JComponent) e.getSource();
+                component.getTransferHandler().exportAsDrag(component, e, TransferHandler.COPY);
+            }
+        });
+        panel.add(button);
+    }
+
+    private JPanel createInspector() {
+        JPanel panel = new JPanel();
+        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
+        panel.setBorder(BorderFactory.createTitledBorder("Properties"));
+
+        textField = addProperty(panel, "Text");
+        xField = addProperty(panel, "X");
+        yField = addProperty(panel, "Y");
+        widthField = addProperty(panel, "Width");
+        heightField = addProperty(panel, "Height");
+
+        addButton(panel, "Apply", new Runnable() {
+            public void run() {
+                applyInspector();
+            }
+        });
+        addButton(panel, "Delete", new Runnable() {
+            public void run() {
+                deleteSelected();
+            }
+        });
+        addButton(panel, "Import Current", new Runnable() {
+            public void run() {
+                importCurrentLayout();
+            }
+        });
+        addButton(panel, "Apply To Live", new Runnable() {
+            public void run() {
+                applyToSourceContainer();
+            }
+        });
+        addButton(panel, "Save", new Runnable() {
+            public void run() {
+                saveDesign();
+            }
+        });
+        return panel;
+    }
+
+    private void addButton(JPanel panel, String title, final Runnable runnable) {
+        JButton button = new JButton(title);
+        button.setAlignmentX(Component.LEFT_ALIGNMENT);
+        button.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(ActionEvent e) {
+                runnable.run();
+            }
+        });
+        panel.add(button);
+    }
+
+    private JTextField addProperty(JPanel panel, String label) {
+        JLabel jLabel = new JLabel(label);
+        jLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
+        JTextField field = new JTextField();
+        field.setMaximumSize(new Dimension(Integer.MAX_VALUE, 28));
+        field.setAlignmentX(Component.LEFT_ALIGNMENT);
+        field.addFocusListener(new FocusAdapter() {
+            public void focusLost(FocusEvent e) {
+                applyInspector();
+            }
+        });
+        panel.add(jLabel);
+        panel.add(field);
+        return field;
+    }
+
+    private JPanel createCanvas() {
+        JPanel panel = new JPanel(null);
+        panel.setBackground(new Color(245, 247, 250));
+        panel.setPreferredSize(new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT));
+        panel.setBorder(BorderFactory.createLineBorder(new Color(180, 190, 200)));
+        panel.setFocusable(true);
+        panel.addMouseListener(new MouseAdapter() {
+            public void mousePressed(MouseEvent e) {
+                selectedId = null;
+                updateInspector();
+                renderAll();
+                canvas.requestFocusInWindow();
+            }
+        });
+        panel.setTransferHandler(new TransferHandler() {
+            public boolean canImport(TransferSupport support) {
+                return support.isDataFlavorSupported(DataFlavor.stringFlavor);
+            }
+
+            public boolean importData(TransferSupport support) {
+                if (!canImport(support)) {
+                    return false;
+                }
+                try {
+                    String type = (String) support.getTransferable().getTransferData(DataFlavor.stringFlavor);
+                    Point point = support.getDropLocation().getDropPoint();
+                    addComponentAt(type, point.x, point.y);
+                    return true;
+                } catch (Exception e) {
+                    return false;
+                }
+            }
+        });
+        return panel;
+    }
+
+    private JPanel createPreviewCanvas() {
+        JPanel panel = new JPanel(null);
+        panel.setBackground(Color.WHITE);
+        panel.setPreferredSize(new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT));
+        panel.setBorder(BorderFactory.createLineBorder(new Color(180, 190, 200)));
+        return panel;
+    }
+
+    private void installShortcuts() {
+        bindShortcut("save", KeyStroke.getKeyStroke("control S"), new Runnable() {
+            public void run() {
+                saveDesign();
+            }
+        });
+        bindShortcut("undo", KeyStroke.getKeyStroke("control Z"), new Runnable() {
+            public void run() {
+                undo();
+            }
+        });
+        bindShortcut("redo", KeyStroke.getKeyStroke("control Y"), new Runnable() {
+            public void run() {
+                redo();
+            }
+        });
+        bindShortcut("redoShift", KeyStroke.getKeyStroke("control shift Z"), new Runnable() {
+            public void run() {
+                redo();
+            }
+        });
+        bindShortcut("delete", KeyStroke.getKeyStroke("DELETE"), new Runnable() {
+            public void run() {
+                deleteSelected();
+            }
+        });
+    }
+
+    private void bindShortcut(String name, KeyStroke keyStroke, final Runnable action) {
+        getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(keyStroke, name);
+        getActionMap().put(name, new javax.swing.AbstractAction() {
+            public void actionPerformed(ActionEvent e) {
+                if (!(java.awt.KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() instanceof JTextField)) {
+                    action.run();
+                }
+            }
+        });
+    }
+
+    private JSONObject loadSchema() {
+        if (designFile.exists()) {
+            try {
+                String text = new String(Files.readAllBytes(designFile.toPath()), StandardCharsets.UTF_8);
+                JSONObject loaded = JSON.parseObject(text);
+                if (loaded != null && loaded.getJSONArray("components") != null) {
+                    return loaded;
+                }
+            } catch (Exception e) {
+                JOptionPane.showMessageDialog(this, "Failed to load UI design: " + e.getMessage());
+            }
+        }
+        return sourceContainer == null ? emptySchema() : schemaFromContainer(sourceContainer);
+    }
+
+    private JSONObject emptySchema() {
+        JSONObject root = new JSONObject();
+        JSONObject canvasObject = new JSONObject();
+        canvasObject.put("width", CANVAS_WIDTH);
+        canvasObject.put("height", CANVAS_HEIGHT);
+        canvasObject.put("gridSize", GRID_SIZE);
+        root.put("version", 1);
+        root.put("source", "empty");
+        root.put("canvas", canvasObject);
+        root.put("components", new JSONArray());
+        return root;
+    }
+
+    private JSONObject schemaFromContainer(Container container) {
+        JSONObject root = emptySchema();
+        root.put("source", "runtime-swing-container");
+        JSONArray components = root.getJSONArray("components");
+        sourceComponents.clear();
+        Component[] children = container.getComponents();
+        for (int i = 0; i < children.length; i++) {
+            Component child = children[i];
+            if (!(child instanceof JComponent) || !child.isVisible()) {
+                continue;
+            }
+            JSONObject item = componentToJson((JComponent) child, i);
+            components.add(item);
+            sourceComponents.put(item.getString("sourceId"), child);
+        }
+        return root;
+    }
+
+    private JSONObject componentToJson(JComponent component, int index) {
+        Rectangle bounds = component.getBounds();
+        String type = getComponentType(component);
+        String sourceId = type + "-" + index;
+        component.putClientProperty("visualUiEditor.sourceId", sourceId);
+
+        JSONObject item = new JSONObject();
+        JSONObject props = new JSONObject();
+        item.put("id", sourceId);
+        item.put("sourceId", sourceId);
+        item.put("type", type);
+        item.put("x", bounds.x);
+        item.put("y", bounds.y);
+        item.put("width", Math.max(bounds.width, 24));
+        item.put("height", Math.max(bounds.height, 24));
+        props.put("text", getComponentText(component));
+        props.put("enabled", component.isEnabled());
+        item.put("props", props);
+        item.put("style", new JSONObject());
+        return item;
+    }
+
+    public static void applySavedDesign(Container container) {
+        JSONObject savedSchema = readSavedSchema();
+        if (savedSchema == null || container == null) {
+            return;
+        }
+        Map<String, Component> componentsBySourceId = new HashMap<String, Component>();
+        indexSourceComponents(container, componentsBySourceId);
+        applySchemaToContainer(savedSchema, container, componentsBySourceId);
+    }
+
+    private static JSONObject readSavedSchema() {
+        File file = new File(System.getProperty("user.dir"), DESIGN_FILE);
+        if (!file.exists()) {
+            return null;
+        }
+        try {
+            String text = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
+            JSONObject loaded = JSON.parseObject(text);
+            if (loaded != null && loaded.getJSONArray("components") != null) {
+                return loaded;
+            }
+        } catch (Exception e) {
+            System.err.println("Failed to load saved UI design: " + e.getMessage());
+        }
+        return null;
+    }
+
+    private static void indexSourceComponents(Container container, Map<String, Component> target) {
+        target.clear();
+        Component[] children = container.getComponents();
+        for (int i = 0; i < children.length; i++) {
+            Component child = children[i];
+            if (!(child instanceof JComponent)) {
+                continue;
+            }
+            JComponent component = (JComponent) child;
+            String sourceId = (String) component.getClientProperty("visualUiEditor.sourceId");
+            if (sourceId == null || sourceId.length() == 0) {
+                sourceId = getComponentType(component) + "-" + i;
+                component.putClientProperty("visualUiEditor.sourceId", sourceId);
+            }
+            target.put(sourceId, component);
+        }
+    }
+
+    private static void applySchemaToContainer(JSONObject schema, Container container, Map<String, Component> componentsBySourceId) {
+        Set<String> appliedSourceIds = new HashSet<String>();
+        JSONArray components = schema.getJSONArray("components");
+        for (int i = 0; i < components.size(); i++) {
+            JSONObject item = components.getJSONObject(i);
+            String sourceId = item.getString("sourceId");
+            Component component = sourceId == null ? null : componentsBySourceId.get(sourceId);
+            if (component == null) {
+                String id = item.getString("id");
+                component = id == null ? null : componentsBySourceId.get(id);
+            }
+            if (component == null) {
+                component = createRuntimeComponent(item);
+                String newSourceId = item.getString("id");
+                if (newSourceId != null) {
+                    ((JComponent) component).putClientProperty("visualUiEditor.sourceId", newSourceId);
+                    componentsBySourceId.put(newSourceId, component);
+                    appliedSourceIds.add(newSourceId);
+                }
+                container.add(component);
+            } else {
+                String existingSourceId = (String) ((JComponent) component).getClientProperty("visualUiEditor.sourceId");
+                if (existingSourceId != null) {
+                    appliedSourceIds.add(existingSourceId);
+                }
+            }
+            component.setVisible(true);
+            component.setBounds(item.getIntValue("x"), item.getIntValue("y"), item.getIntValue("width"), item.getIntValue("height"));
+            applyText(component, item.getJSONObject("props").getString("text"));
+        }
+
+        for (Map.Entry<String, Component> entry : componentsBySourceId.entrySet()) {
+            if (!appliedSourceIds.contains(entry.getKey())) {
+                entry.getValue().setVisible(false);
+            }
+        }
+        container.revalidate();
+        container.repaint();
+    }
+
+    private static JComponent createRuntimeComponent(JSONObject item) {
+        String type = item.getString("type");
+        String text = item.getJSONObject("props").getString("text");
+        if ("button".equals(type)) {
+            return new JButton(text);
+        }
+        if ("textfield".equals(type)) {
+            return new JTextField(text);
+        }
+        if ("panel".equals(type)) {
+            JPanel panel = new JPanel(new BorderLayout());
+            panel.add(new JLabel(text, SwingConstants.CENTER), BorderLayout.CENTER);
+            panel.setBackground(new Color(230, 235, 240));
+            return panel;
+        }
+        JLabel label = new JLabel(text, SwingConstants.CENTER);
+        label.setOpaque(true);
+        label.setBackground(Color.WHITE);
+        return label;
+    }
+
+    private static String getComponentType(JComponent component) {
+        if (component instanceof JButton) {
+            return "button";
+        }
+        if (component instanceof JTextField) {
+            return "textfield";
+        }
+        if (component instanceof JLabel) {
+            return "label";
+        }
+        if (component instanceof JPanel) {
+            return "panel";
+        }
+        return "component";
+    }
+
+    private static String getComponentText(JComponent component) {
+        if (component instanceof JButton) {
+            return ((JButton) component).getText();
+        }
+        if (component instanceof JTextField) {
+            return ((JTextField) component).getText();
+        }
+        if (component instanceof JLabel) {
+            return ((JLabel) component).getText();
+        }
+        return component.getClass().getSimpleName();
+    }
+
+    private void saveDesign() {
+        try {
+            File parent = designFile.getParentFile();
+            if (parent != null && !parent.exists()) {
+                parent.mkdirs();
+            }
+            Files.write(designFile.toPath(), JSON.toJSONString(schema).getBytes(StandardCharsets.UTF_8));
+            dirty = false;
+            applyToSourceContainer();
+            updateStatus("Saved and applied " + designFile.getPath());
+        } catch (IOException e) {
+            JOptionPane.showMessageDialog(this, "Save failed: " + e.getMessage());
+        }
+    }
+
+    private void importCurrentLayout() {
+        if (sourceContainer == null) {
+            return;
+        }
+        pushHistory();
+        schema = schemaFromContainer(sourceContainer);
+        selectedId = null;
+        markDirty();
+        renderAll();
+    }
+
+    private void applyToSourceContainer() {
+        if (sourceContainer == null) {
+            return;
+        }
+        indexSourceComponents(sourceContainer, sourceComponents);
+        applySchemaToContainer(schema, sourceContainer, sourceComponents);
+        updateStatus("Applied to current 工作面板 instance.");
+    }
+
+    private static void applyText(Component component, String text) {
+        if (component instanceof JButton) {
+            ((JButton) component).setText(text);
+        } else if (component instanceof JTextField) {
+            ((JTextField) component).setText(text);
+        } else if (component instanceof JLabel) {
+            ((JLabel) component).setText(text);
+        }
+    }
+
+    private void addComponentAt(String type, int x, int y) {
+        pushHistory();
+        JSONObject item = new JSONObject();
+        JSONObject props = new JSONObject();
+        String id = type + "-" + System.currentTimeMillis();
+        int width = defaultWidth(type);
+        int height = defaultHeight(type);
+        item.put("id", id);
+        item.put("type", type);
+        item.put("x", clamp(snap(x), 0, CANVAS_WIDTH - width));
+        item.put("y", clamp(snap(y), 0, CANVAS_HEIGHT - height));
+        item.put("width", width);
+        item.put("height", height);
+        props.put("text", defaultText(type));
+        props.put("enabled", true);
+        item.put("props", props);
+        item.put("style", new JSONObject());
+        schema.getJSONArray("components").add(item);
+        selectedId = id;
+        markDirty();
+        renderAll();
+    }
+
+    private int defaultWidth(String type) {
+        return "panel".equals(type) || "textfield".equals(type) ? 180 : 120;
+    }
+
+    private int defaultHeight(String type) {
+        return "panel".equals(type) ? 120 : 36;
+    }
+
+    private String defaultText(String type) {
+        if ("label".equals(type)) {
+            return "Label";
+        }
+        if ("button".equals(type)) {
+            return "Button";
+        }
+        if ("textfield".equals(type)) {
+            return "Text";
+        }
+        return "Panel";
+    }
+
+    private void renderAll() {
+        renderCanvas(canvas, true);
+        renderCanvas(previewCanvas, false);
+        updateInspector();
+        updateStatus(null);
+    }
+
+    private void renderCanvas(JPanel target, boolean editable) {
+        target.removeAll();
+        JSONArray components = schema.getJSONArray("components");
+        for (int i = 0; i < components.size(); i++) {
+            JSONObject item = components.getJSONObject(i);
+            JComponent view = createView(item, editable);
+            view.setBounds(item.getIntValue("x"), item.getIntValue("y"), item.getIntValue("width"), item.getIntValue("height"));
+            target.add(view);
+        }
+        target.revalidate();
+        target.repaint();
+    }
+
+    private JComponent createView(final JSONObject item, boolean editable) {
+        String type = item.getString("type");
+        String text = item.getJSONObject("props").getString("text");
+        JComponent view;
+        if ("button".equals(type)) {
+            view = new DesignerButton(text, editable && selectedId != null && selectedId.equals(item.getString("id")));
+        } else if ("textfield".equals(type)) {
+            JTextField field = new DesignerTextField(text, editable && selectedId != null && selectedId.equals(item.getString("id")));
+            field.setEditable(false);
+            view = field;
+        } else if ("panel".equals(type)) {
+            JPanel panel = new DesignerPanel(editable && selectedId != null && selectedId.equals(item.getString("id")));
+            panel.setLayout(new BorderLayout());
+            panel.add(new JLabel(text, SwingConstants.CENTER), BorderLayout.CENTER);
+            panel.setBackground(new Color(230, 235, 240));
+            view = panel;
+        } else {
+            JLabel label = new DesignerLabel(text, editable && selectedId != null && selectedId.equals(item.getString("id")));
+            label.setOpaque(true);
+            label.setBackground(Color.WHITE);
+            view = label;
+        }
+        view.setFont(new Font("Dialog", Font.PLAIN, 16));
+        view.setEnabled(editable || item.getJSONObject("props").getBooleanValue("enabled"));
+        view.setBorder(BorderFactory.createLineBorder(selectedId != null && selectedId.equals(item.getString("id")) && editable ? Color.BLUE : Color.GRAY, editable ? 2 : 1));
+        if (editable) {
+            installComponentEditing(view, item);
+        }
+        return view;
+    }
+
+    private void installComponentEditing(final JComponent view, final JSONObject item) {
+        final String id = item.getString("id");
+        MouseAdapter adapter = new MouseAdapter() {
+            private Point startPoint;
+            private Rectangle startBounds;
+            private String resizeHandle;
+            private boolean changed;
+
+            public void mousePressed(MouseEvent e) {
+                e.consume();
+                selectedId = id;
+                startPoint = SwingUtilities.convertPoint(view, e.getPoint(), canvas);
+                startBounds = view.getBounds();
+                resizeHandle = getResizeHandle(e.getPoint(), view);
+                changed = false;
+                pushHistory();
+                updateInspector();
+                view.setBorder(BorderFactory.createLineBorder(Color.BLUE, 2));
+                canvas.repaint();
+                canvas.requestFocusInWindow();
+            }
+
+            public void mouseDragged(MouseEvent e) {
+                Point current = SwingUtilities.convertPoint(view, e.getPoint(), canvas);
+                int dx = current.x - startPoint.x;
+                int dy = current.y - startPoint.y;
+                int nextX = startBounds.x;
+                int nextY = startBounds.y;
+                int nextWidth = startBounds.width;
+                int nextHeight = startBounds.height;
+                if (resizeHandle != null) {
+                    Rectangle resized = resizeBounds(startBounds, dx, dy, resizeHandle);
+                    nextX = resized.x;
+                    nextY = resized.y;
+                    nextWidth = resized.width;
+                    nextHeight = resized.height;
+                } else {
+                    nextX = clamp(snap(startBounds.x + dx), 0, CANVAS_WIDTH - startBounds.width);
+                    nextY = clamp(snap(startBounds.y + dy), 0, CANVAS_HEIGHT - startBounds.height);
+                }
+                item.put("x", nextX);
+                item.put("y", nextY);
+                item.put("width", nextWidth);
+                item.put("height", nextHeight);
+                view.setBounds(nextX, nextY, nextWidth, nextHeight);
+                changed = true;
+                markDirty();
+                updateInspector();
+                canvas.repaint();
+            }
+
+            public void mouseReleased(MouseEvent e) {
+                if (!changed && !undoStack.isEmpty()) {
+                    undoStack.pop();
+                } else if (changed) {
+                    renderAll();
+                }
+            }
+
+            public void mouseMoved(MouseEvent e) {
+                view.setCursor(cursorForHandle(getResizeHandle(e.getPoint(), view)));
+            }
+        };
+        view.addMouseListener(adapter);
+        view.addMouseMotionListener(adapter);
+    }
+
+    private String getResizeHandle(Point point, JComponent component) {
+        int w = component.getWidth();
+        int h = component.getHeight();
+        boolean left = point.x <= HANDLE_SIZE;
+        boolean right = point.x >= w - HANDLE_SIZE;
+        boolean top = point.y <= HANDLE_SIZE;
+        boolean bottom = point.y >= h - HANDLE_SIZE;
+        boolean middleX = point.x >= (w / 2) - HANDLE_SIZE && point.x <= (w / 2) + HANDLE_SIZE;
+        boolean middleY = point.y >= (h / 2) - HANDLE_SIZE && point.y <= (h / 2) + HANDLE_SIZE;
+        if (left && top) {
+            return "nw";
+        }
+        if (right && top) {
+            return "ne";
+        }
+        if (left && bottom) {
+            return "sw";
+        }
+        if (right && bottom) {
+            return "se";
+        }
+        if (middleX && top) {
+            return "n";
+        }
+        if (middleX && bottom) {
+            return "s";
+        }
+        if (left && middleY) {
+            return "w";
+        }
+        if (right && middleY) {
+            return "e";
+        }
+        return null;
+    }
+
+    private Rectangle resizeBounds(Rectangle startBounds, int dx, int dy, String handle) {
+        int minSize = 24;
+        int x = startBounds.x;
+        int y = startBounds.y;
+        int width = startBounds.width;
+        int height = startBounds.height;
+        if (handle.indexOf("e") >= 0) {
+            width = clamp(snap(startBounds.width + dx), minSize, CANVAS_WIDTH - startBounds.x);
+        }
+        if (handle.indexOf("s") >= 0) {
+            height = clamp(snap(startBounds.height + dy), minSize, CANVAS_HEIGHT - startBounds.y);
+        }
+        if (handle.indexOf("w") >= 0) {
+            int newX = clamp(snap(startBounds.x + dx), 0, startBounds.x + startBounds.width - minSize);
+            width = startBounds.x + startBounds.width - newX;
+            x = newX;
+        }
+        if (handle.indexOf("n") >= 0) {
+            int newY = clamp(snap(startBounds.y + dy), 0, startBounds.y + startBounds.height - minSize);
+            height = startBounds.y + startBounds.height - newY;
+            y = newY;
+        }
+        return new Rectangle(x, y, width, height);
+    }
+
+    private Cursor cursorForHandle(String handle) {
+        if (handle == null) {
+            return Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
+        }
+        if ("n".equals(handle) || "s".equals(handle)) {
+            return Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR);
+        }
+        if ("e".equals(handle) || "w".equals(handle)) {
+            return Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR);
+        }
+        if ("ne".equals(handle) || "sw".equals(handle)) {
+            return Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR);
+        }
+        return Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR);
+    }
+
+    private int snap(int value) {
+        return Math.round(value / (float) GRID_SIZE) * GRID_SIZE;
+    }
+
+    private int clamp(int value, int min, int max) {
+        return Math.max(min, Math.min(max, value));
+    }
+
+    private void applyInspector() {
+        JSONObject item = selectedItem();
+        if (item == null) {
+            return;
+        }
+        pushHistory();
+        try {
+            item.getJSONObject("props").put("text", textField.getText());
+            item.put("x", clamp(snap(Integer.parseInt(xField.getText())), 0, CANVAS_WIDTH));
+            item.put("y", clamp(snap(Integer.parseInt(yField.getText())), 0, CANVAS_HEIGHT));
+            item.put("width", clamp(snap(Integer.parseInt(widthField.getText())), 24, CANVAS_WIDTH));
+            item.put("height", clamp(snap(Integer.parseInt(heightField.getText())), 24, CANVAS_HEIGHT));
+            markDirty();
+            renderAll();
+        } catch (NumberFormatException e) {
+            if (!undoStack.isEmpty()) {
+                undoStack.pop();
+            }
+            JOptionPane.showMessageDialog(this, "Position and size must be numbers.");
+            updateInspector();
+        }
+    }
+
+    private void deleteSelected() {
+        if (selectedId == null) {
+            return;
+        }
+        pushHistory();
+        JSONArray components = schema.getJSONArray("components");
+        for (int i = components.size() - 1; i >= 0; i--) {
+            if (selectedId.equals(components.getJSONObject(i).getString("id"))) {
+                components.remove(i);
+            }
+        }
+        selectedId = null;
+        markDirty();
+        renderAll();
+    }
+
+    private JSONObject selectedItem() {
+        if (selectedId == null) {
+            return null;
+        }
+        JSONArray components = schema.getJSONArray("components");
+        for (int i = 0; i < components.size(); i++) {
+            JSONObject item = components.getJSONObject(i);
+            if (selectedId.equals(item.getString("id"))) {
+                return item;
+            }
+        }
+        return null;
+    }
+
+    private void updateInspector() {
+        JSONObject item = selectedItem();
+        boolean enabled = item != null;
+        textField.setEnabled(enabled);
+        xField.setEnabled(enabled);
+        yField.setEnabled(enabled);
+        widthField.setEnabled(enabled);
+        heightField.setEnabled(enabled);
+        if (!enabled) {
+            textField.setText("");
+            xField.setText("");
+            yField.setText("");
+            widthField.setText("");
+            heightField.setText("");
+            return;
+        }
+        textField.setText(item.getJSONObject("props").getString("text"));
+        xField.setText(String.valueOf(item.getIntValue("x")));
+        yField.setText(String.valueOf(item.getIntValue("y")));
+        widthField.setText(String.valueOf(item.getIntValue("width")));
+        heightField.setText(String.valueOf(item.getIntValue("height")));
+    }
+
+    private void pushHistory() {
+        undoStack.push(JSON.toJSONString(schema));
+        redoStack.clear();
+    }
+
+    private void undo() {
+        if (undoStack.isEmpty()) {
+            return;
+        }
+        redoStack.push(JSON.toJSONString(schema));
+        schema = JSON.parseObject(undoStack.pop());
+        selectedId = null;
+        markDirty();
+        renderAll();
+    }
+
+    private void redo() {
+        if (redoStack.isEmpty()) {
+            return;
+        }
+        undoStack.push(JSON.toJSONString(schema));
+        schema = JSON.parseObject(redoStack.pop());
+        selectedId = null;
+        markDirty();
+        renderAll();
+    }
+
+    private void markDirty() {
+        dirty = true;
+    }
+
+    private void updateStatus(String message) {
+        if (message != null) {
+            statusLabel.setText(message);
+            return;
+        }
+        statusLabel.setText((dirty ? "Unsaved" : "Saved") + " - imported current 工作面板. Ctrl+S save, Ctrl+Z undo, Ctrl+Y redo. File: " + DESIGN_FILE);
+    }
+
+    private static void paintHandles(Component component, Graphics graphics, boolean selected) {
+        if (!selected) {
+            return;
+        }
+        graphics.setColor(Color.BLUE);
+        int s = HANDLE_SIZE;
+        int half = s / 2;
+        int w = component.getWidth();
+        int h = component.getHeight();
+        int[][] points = new int[][]{
+                {0, 0}, {w / 2, 0}, {w, 0},
+                {0, h / 2}, {w, h / 2},
+                {0, h}, {w / 2, h}, {w, h}
+        };
+        for (int i = 0; i < points.length; i++) {
+            graphics.fillRect(points[i][0] - half, points[i][1] - half, s, s);
+        }
+    }
+
+    private static class DesignerButton extends JButton {
+        private final boolean selected;
+
+        DesignerButton(String text, boolean selected) {
+            super(text);
+            this.selected = selected;
+        }
+
+        protected void paintComponent(Graphics graphics) {
+            super.paintComponent(graphics);
+            paintHandles(this, graphics, selected);
+        }
+    }
+
+    private static class DesignerTextField extends JTextField {
+        private final boolean selected;
+
+        DesignerTextField(String text, boolean selected) {
+            super(text);
+            this.selected = selected;
+        }
+
+        protected void paintComponent(Graphics graphics) {
+            super.paintComponent(graphics);
+            paintHandles(this, graphics, selected);
+        }
+    }
+
+    private static class DesignerLabel extends JLabel {
+        private final boolean selected;
+
+        DesignerLabel(String text, boolean selected) {
+            super(text, SwingConstants.CENTER);
+            this.selected = selected;
+        }
+
+        protected void paintComponent(Graphics graphics) {
+            super.paintComponent(graphics);
+            paintHandles(this, graphics, selected);
+        }
+    }
+
+    private static class DesignerPanel extends JPanel {
+        private final boolean selected;
+
+        DesignerPanel(boolean selected) {
+            this.selected = selected;
+        }
+
+        protected void paintComponent(Graphics graphics) {
+            super.paintComponent(graphics);
+            paintHandles(this, graphics, selected);
+        }
+    }
+}