|
|
@@ -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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|