VisualUiEditorPanel.java 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001
  1. package com.mes.ui;
  2. import com.alibaba.fastjson2.JSON;
  3. import com.alibaba.fastjson2.JSONArray;
  4. import com.alibaba.fastjson2.JSONObject;
  5. import javax.swing.BorderFactory;
  6. import javax.swing.BoxLayout;
  7. import javax.swing.JButton;
  8. import javax.swing.JComponent;
  9. import javax.swing.JLabel;
  10. import javax.swing.JOptionPane;
  11. import javax.swing.JPanel;
  12. import javax.swing.JSplitPane;
  13. import javax.swing.JTextField;
  14. import javax.swing.KeyStroke;
  15. import javax.swing.SwingConstants;
  16. import javax.swing.SwingUtilities;
  17. import javax.swing.TransferHandler;
  18. import java.awt.BorderLayout;
  19. import java.awt.Color;
  20. import java.awt.Component;
  21. import java.awt.Container;
  22. import java.awt.Cursor;
  23. import java.awt.Dimension;
  24. import java.awt.Font;
  25. import java.awt.Graphics;
  26. import java.awt.GridLayout;
  27. import java.awt.Point;
  28. import java.awt.Rectangle;
  29. import java.awt.datatransfer.DataFlavor;
  30. import java.awt.datatransfer.StringSelection;
  31. import java.awt.event.ActionEvent;
  32. import java.awt.event.FocusAdapter;
  33. import java.awt.event.FocusEvent;
  34. import java.awt.event.MouseAdapter;
  35. import java.awt.event.MouseEvent;
  36. import java.io.File;
  37. import java.io.IOException;
  38. import java.nio.charset.StandardCharsets;
  39. import java.nio.file.Files;
  40. import java.util.ArrayDeque;
  41. import java.util.Deque;
  42. import java.util.HashMap;
  43. import java.util.HashSet;
  44. import java.util.Map;
  45. import java.util.Set;
  46. public class VisualUiEditorPanel extends JPanel {
  47. private static final int CANVAS_WIDTH = 990;
  48. private static final int CANVAS_HEIGHT = 550;
  49. private static final int GRID_SIZE = 8;
  50. private static final int HANDLE_SIZE = 10;
  51. private static final String DESIGN_FILE = "ui-designs/op40-main.ui.json";
  52. private final File designFile;
  53. private final Container sourceContainer;
  54. private final Map<String, Component> sourceComponents = new HashMap<String, Component>();
  55. private final JPanel canvas;
  56. private JSplitPane sideContent;
  57. private JButton sideToggleButton;
  58. private final JLabel statusLabel;
  59. private final Deque<String> undoStack = new ArrayDeque<String>();
  60. private final Deque<String> redoStack = new ArrayDeque<String>();
  61. private JTextField textField;
  62. private JTextField xField;
  63. private JTextField yField;
  64. private JTextField widthField;
  65. private JTextField heightField;
  66. private JSONObject schema;
  67. private String selectedId;
  68. private boolean dirty;
  69. public VisualUiEditorPanel() {
  70. this(null);
  71. }
  72. public VisualUiEditorPanel(Container sourceContainer) {
  73. this.sourceContainer = sourceContainer;
  74. this.designFile = new File(System.getProperty("user.dir"), DESIGN_FILE);
  75. this.schema = loadSchema();
  76. if (sourceContainer != null) {
  77. indexSourceComponents(sourceContainer, sourceComponents);
  78. }
  79. setLayout(new BorderLayout(8, 8));
  80. setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
  81. JPanel palette = createPalette();
  82. JPanel inspector = createInspector();
  83. this.canvas = createCanvas();
  84. this.sideContent = new JSplitPane(JSplitPane.VERTICAL_SPLIT, palette, inspector);
  85. this.sideToggleButton = new JButton("<");
  86. this.statusLabel = new JLabel("Ready");
  87. sideContent.setResizeWeight(0.35);
  88. sideContent.setPreferredSize(new Dimension(250, 500));
  89. add(canvas, BorderLayout.CENTER);
  90. add(statusLabel, BorderLayout.SOUTH);
  91. installShortcuts();
  92. renderAll();
  93. }
  94. private JPanel createPalette() {
  95. JPanel panel = new JPanel(new GridLayout(0, 1, 6, 6));
  96. panel.setBorder(BorderFactory.createTitledBorder("Palette"));
  97. addPaletteButton(panel, "Label", "label");
  98. addPaletteButton(panel, "Button", "button");
  99. addPaletteButton(panel, "Text Field", "textfield");
  100. addPaletteButton(panel, "Panel", "panel");
  101. return panel;
  102. }
  103. private void addPaletteButton(JPanel panel, String title, final String type) {
  104. JButton button = new JButton(title);
  105. button.setFocusable(false);
  106. button.addActionListener(new java.awt.event.ActionListener() {
  107. public void actionPerformed(ActionEvent e) {
  108. addComponentAt(type, 40 + schema.getJSONArray("components").size() * 16, 40 + schema.getJSONArray("components").size() * 16);
  109. }
  110. });
  111. button.setTransferHandler(new TransferHandler("text") {
  112. protected java.awt.datatransfer.Transferable createTransferable(JComponent c) {
  113. return new StringSelection(type);
  114. }
  115. public int getSourceActions(JComponent c) {
  116. return COPY;
  117. }
  118. });
  119. button.addMouseMotionListener(new MouseAdapter() {
  120. public void mouseDragged(MouseEvent e) {
  121. JComponent component = (JComponent) e.getSource();
  122. component.getTransferHandler().exportAsDrag(component, e, TransferHandler.COPY);
  123. }
  124. });
  125. panel.add(button);
  126. }
  127. public JPanel createExternalToolPanel() {
  128. final JPanel panel = new JPanel(new BorderLayout());
  129. sideToggleButton.setFocusable(false);
  130. sideToggleButton.setPreferredSize(new Dimension(28, 40));
  131. sideToggleButton.setToolTipText("Collapse palette");
  132. sideToggleButton.addActionListener(new java.awt.event.ActionListener() {
  133. public void actionPerformed(ActionEvent e) {
  134. toggleSideBar(panel);
  135. }
  136. });
  137. panel.add(sideToggleButton, BorderLayout.WEST);
  138. panel.add(sideContent, BorderLayout.CENTER);
  139. return panel;
  140. }
  141. private void toggleSideBar(JPanel panel) {
  142. boolean expanded = sideContent.getParent() == panel;
  143. if (expanded) {
  144. panel.remove(sideContent);
  145. sideToggleButton.setText(">");
  146. sideToggleButton.setToolTipText("Expand palette");
  147. panel.setPreferredSize(new Dimension(30, 500));
  148. } else {
  149. panel.add(sideContent, BorderLayout.CENTER);
  150. sideToggleButton.setText("<");
  151. sideToggleButton.setToolTipText("Collapse palette");
  152. panel.setPreferredSize(null);
  153. }
  154. panel.revalidate();
  155. panel.repaint();
  156. java.awt.Window window = SwingUtilities.getWindowAncestor(panel);
  157. if (window != null) {
  158. window.pack();
  159. }
  160. }
  161. private JPanel createInspector() {
  162. JPanel panel = new JPanel();
  163. panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
  164. panel.setBorder(BorderFactory.createTitledBorder("Properties"));
  165. textField = addProperty(panel, "Text");
  166. xField = addProperty(panel, "X");
  167. yField = addProperty(panel, "Y");
  168. widthField = addProperty(panel, "Width");
  169. heightField = addProperty(panel, "Height");
  170. addButton(panel, "Apply", new Runnable() {
  171. public void run() {
  172. applyInspector();
  173. }
  174. });
  175. addButton(panel, "Delete", new Runnable() {
  176. public void run() {
  177. deleteSelected();
  178. }
  179. });
  180. addButton(panel, "Import Current", new Runnable() {
  181. public void run() {
  182. importCurrentLayout();
  183. }
  184. });
  185. addButton(panel, "Apply To Live", new Runnable() {
  186. public void run() {
  187. applyToSourceContainer();
  188. }
  189. });
  190. addButton(panel, "Save", new Runnable() {
  191. public void run() {
  192. saveDesign();
  193. }
  194. });
  195. return panel;
  196. }
  197. private void addButton(JPanel panel, String title, final Runnable runnable) {
  198. JButton button = new JButton(title);
  199. button.setAlignmentX(Component.LEFT_ALIGNMENT);
  200. button.addActionListener(new java.awt.event.ActionListener() {
  201. public void actionPerformed(ActionEvent e) {
  202. runnable.run();
  203. }
  204. });
  205. panel.add(button);
  206. }
  207. private JTextField addProperty(JPanel panel, String label) {
  208. JLabel jLabel = new JLabel(label);
  209. jLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
  210. JTextField field = new JTextField();
  211. field.setMaximumSize(new Dimension(Integer.MAX_VALUE, 28));
  212. field.setAlignmentX(Component.LEFT_ALIGNMENT);
  213. field.addFocusListener(new FocusAdapter() {
  214. public void focusLost(FocusEvent e) {
  215. applyInspector();
  216. }
  217. });
  218. panel.add(jLabel);
  219. panel.add(field);
  220. return field;
  221. }
  222. private JPanel createCanvas() {
  223. JPanel panel = new JPanel(null);
  224. panel.setBackground(new Color(245, 247, 250));
  225. panel.setPreferredSize(new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT));
  226. panel.setBorder(BorderFactory.createLineBorder(new Color(180, 190, 200)));
  227. panel.setFocusable(true);
  228. panel.addMouseListener(new MouseAdapter() {
  229. public void mousePressed(MouseEvent e) {
  230. selectedId = null;
  231. updateInspector();
  232. renderAll();
  233. canvas.requestFocusInWindow();
  234. }
  235. });
  236. panel.setTransferHandler(new TransferHandler() {
  237. public boolean canImport(TransferSupport support) {
  238. return support.isDataFlavorSupported(DataFlavor.stringFlavor);
  239. }
  240. public boolean importData(TransferSupport support) {
  241. if (!canImport(support)) {
  242. return false;
  243. }
  244. try {
  245. String type = (String) support.getTransferable().getTransferData(DataFlavor.stringFlavor);
  246. Point point = support.getDropLocation().getDropPoint();
  247. addComponentAt(type, point.x, point.y);
  248. return true;
  249. } catch (Exception e) {
  250. return false;
  251. }
  252. }
  253. });
  254. return panel;
  255. }
  256. private void installShortcuts() {
  257. bindShortcut("save", KeyStroke.getKeyStroke("control S"), new Runnable() {
  258. public void run() {
  259. saveDesign();
  260. }
  261. });
  262. bindShortcut("undo", KeyStroke.getKeyStroke("control Z"), new Runnable() {
  263. public void run() {
  264. undo();
  265. }
  266. });
  267. bindShortcut("redo", KeyStroke.getKeyStroke("control Y"), new Runnable() {
  268. public void run() {
  269. redo();
  270. }
  271. });
  272. bindShortcut("redoShift", KeyStroke.getKeyStroke("control shift Z"), new Runnable() {
  273. public void run() {
  274. redo();
  275. }
  276. });
  277. bindShortcut("delete", KeyStroke.getKeyStroke("DELETE"), new Runnable() {
  278. public void run() {
  279. deleteSelected();
  280. }
  281. });
  282. }
  283. private void bindShortcut(String name, KeyStroke keyStroke, final Runnable action) {
  284. getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(keyStroke, name);
  285. getActionMap().put(name, new javax.swing.AbstractAction() {
  286. public void actionPerformed(ActionEvent e) {
  287. if (!(java.awt.KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() instanceof JTextField)) {
  288. action.run();
  289. }
  290. }
  291. });
  292. }
  293. private JSONObject loadSchema() {
  294. if (designFile.exists()) {
  295. try {
  296. String text = new String(Files.readAllBytes(designFile.toPath()), StandardCharsets.UTF_8);
  297. JSONObject loaded = JSON.parseObject(text);
  298. if (loaded != null && loaded.getJSONArray("components") != null) {
  299. return loaded;
  300. }
  301. } catch (Exception e) {
  302. JOptionPane.showMessageDialog(this, "Failed to load UI design: " + e.getMessage());
  303. }
  304. }
  305. return sourceContainer == null ? emptySchema() : schemaFromContainer(sourceContainer);
  306. }
  307. private JSONObject emptySchema() {
  308. JSONObject root = new JSONObject();
  309. JSONObject canvasObject = new JSONObject();
  310. canvasObject.put("width", CANVAS_WIDTH);
  311. canvasObject.put("height", CANVAS_HEIGHT);
  312. canvasObject.put("gridSize", GRID_SIZE);
  313. root.put("version", 1);
  314. root.put("source", "empty");
  315. root.put("canvas", canvasObject);
  316. root.put("components", new JSONArray());
  317. return root;
  318. }
  319. private JSONObject schemaFromContainer(Container container) {
  320. JSONObject root = emptySchema();
  321. root.put("source", "runtime-swing-container");
  322. JSONArray components = root.getJSONArray("components");
  323. sourceComponents.clear();
  324. Component[] children = container.getComponents();
  325. for (int i = 0; i < children.length; i++) {
  326. Component child = children[i];
  327. if (!(child instanceof JComponent) || !child.isVisible()) {
  328. continue;
  329. }
  330. JSONObject item = componentToJson((JComponent) child, i);
  331. components.add(item);
  332. sourceComponents.put(item.getString("sourceId"), child);
  333. }
  334. return root;
  335. }
  336. private JSONObject componentToJson(JComponent component, int index) {
  337. Rectangle bounds = component.getBounds();
  338. String type = getComponentType(component);
  339. String sourceId = type + "-" + index;
  340. component.putClientProperty("visualUiEditor.sourceId", sourceId);
  341. JSONObject item = new JSONObject();
  342. JSONObject props = new JSONObject();
  343. item.put("id", sourceId);
  344. item.put("sourceId", sourceId);
  345. item.put("type", type);
  346. item.put("x", bounds.x);
  347. item.put("y", bounds.y);
  348. item.put("width", Math.max(bounds.width, 24));
  349. item.put("height", Math.max(bounds.height, 24));
  350. props.put("text", getComponentText(component));
  351. props.put("enabled", component.isEnabled());
  352. item.put("props", props);
  353. item.put("style", new JSONObject());
  354. return item;
  355. }
  356. public static void applySavedDesign(Container container) {
  357. JSONObject savedSchema = readSavedSchema();
  358. if (savedSchema == null || container == null) {
  359. return;
  360. }
  361. Map<String, Component> componentsBySourceId = new HashMap<String, Component>();
  362. indexSourceComponents(container, componentsBySourceId);
  363. applySchemaToContainer(savedSchema, container, componentsBySourceId);
  364. }
  365. private static JSONObject readSavedSchema() {
  366. File file = new File(System.getProperty("user.dir"), DESIGN_FILE);
  367. if (!file.exists()) {
  368. return null;
  369. }
  370. try {
  371. String text = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
  372. JSONObject loaded = JSON.parseObject(text);
  373. if (loaded != null && loaded.getJSONArray("components") != null) {
  374. return loaded;
  375. }
  376. } catch (Exception e) {
  377. System.err.println("Failed to load saved UI design: " + e.getMessage());
  378. }
  379. return null;
  380. }
  381. private static void indexSourceComponents(Container container, Map<String, Component> target) {
  382. target.clear();
  383. Component[] children = container.getComponents();
  384. for (int i = 0; i < children.length; i++) {
  385. Component child = children[i];
  386. if (!(child instanceof JComponent)) {
  387. continue;
  388. }
  389. JComponent component = (JComponent) child;
  390. String sourceId = (String) component.getClientProperty("visualUiEditor.sourceId");
  391. if (sourceId == null || sourceId.length() == 0) {
  392. sourceId = getComponentType(component) + "-" + i;
  393. component.putClientProperty("visualUiEditor.sourceId", sourceId);
  394. }
  395. target.put(sourceId, component);
  396. }
  397. }
  398. private static void applySchemaToContainer(JSONObject schema, Container container, Map<String, Component> componentsBySourceId) {
  399. Set<String> appliedSourceIds = new HashSet<String>();
  400. JSONArray components = schema.getJSONArray("components");
  401. for (int i = 0; i < components.size(); i++) {
  402. JSONObject item = components.getJSONObject(i);
  403. String sourceId = item.getString("sourceId");
  404. Component component = sourceId == null ? null : componentsBySourceId.get(sourceId);
  405. if (component == null) {
  406. String id = item.getString("id");
  407. component = id == null ? null : componentsBySourceId.get(id);
  408. }
  409. if (component == null) {
  410. component = createRuntimeComponent(item);
  411. String newSourceId = item.getString("id");
  412. if (newSourceId != null) {
  413. ((JComponent) component).putClientProperty("visualUiEditor.sourceId", newSourceId);
  414. componentsBySourceId.put(newSourceId, component);
  415. appliedSourceIds.add(newSourceId);
  416. }
  417. container.add(component);
  418. } else {
  419. String existingSourceId = (String) ((JComponent) component).getClientProperty("visualUiEditor.sourceId");
  420. if (existingSourceId != null) {
  421. appliedSourceIds.add(existingSourceId);
  422. }
  423. }
  424. component.setVisible(true);
  425. component.setBounds(item.getIntValue("x"), item.getIntValue("y"), item.getIntValue("width"), item.getIntValue("height"));
  426. applyText(component, item.getJSONObject("props").getString("text"));
  427. }
  428. for (Map.Entry<String, Component> entry : componentsBySourceId.entrySet()) {
  429. if (!appliedSourceIds.contains(entry.getKey())) {
  430. entry.getValue().setVisible(false);
  431. }
  432. }
  433. container.revalidate();
  434. container.repaint();
  435. }
  436. private static JComponent createRuntimeComponent(JSONObject item) {
  437. String type = item.getString("type");
  438. String text = item.getJSONObject("props").getString("text");
  439. if ("button".equals(type)) {
  440. return new JButton(text);
  441. }
  442. if ("textfield".equals(type)) {
  443. return new JTextField(text);
  444. }
  445. if ("panel".equals(type)) {
  446. JPanel panel = new JPanel(new BorderLayout());
  447. panel.add(new JLabel(text, SwingConstants.CENTER), BorderLayout.CENTER);
  448. panel.setBackground(new Color(230, 235, 240));
  449. return panel;
  450. }
  451. JLabel label = new JLabel(text, SwingConstants.CENTER);
  452. label.setOpaque(true);
  453. label.setBackground(Color.WHITE);
  454. return label;
  455. }
  456. private static String getComponentType(JComponent component) {
  457. if (component instanceof JButton) {
  458. return "button";
  459. }
  460. if (component instanceof JTextField) {
  461. return "textfield";
  462. }
  463. if (component instanceof JLabel) {
  464. return "label";
  465. }
  466. if (component instanceof JPanel) {
  467. return "panel";
  468. }
  469. return "component";
  470. }
  471. private static String getComponentText(JComponent component) {
  472. if (component instanceof JButton) {
  473. return ((JButton) component).getText();
  474. }
  475. if (component instanceof JTextField) {
  476. return ((JTextField) component).getText();
  477. }
  478. if (component instanceof JLabel) {
  479. return ((JLabel) component).getText();
  480. }
  481. return component.getClass().getSimpleName();
  482. }
  483. private void saveDesign() {
  484. try {
  485. File parent = designFile.getParentFile();
  486. if (parent != null && !parent.exists()) {
  487. parent.mkdirs();
  488. }
  489. Files.write(designFile.toPath(), JSON.toJSONString(schema).getBytes(StandardCharsets.UTF_8));
  490. dirty = false;
  491. applyToSourceContainer();
  492. updateStatus("Saved and applied " + designFile.getPath());
  493. } catch (IOException e) {
  494. JOptionPane.showMessageDialog(this, "Save failed: " + e.getMessage());
  495. }
  496. }
  497. private void importCurrentLayout() {
  498. if (sourceContainer == null) {
  499. return;
  500. }
  501. pushHistory();
  502. schema = schemaFromContainer(sourceContainer);
  503. selectedId = null;
  504. markDirty();
  505. renderAll();
  506. }
  507. private void applyToSourceContainer() {
  508. if (sourceContainer == null) {
  509. return;
  510. }
  511. indexSourceComponents(sourceContainer, sourceComponents);
  512. applySchemaToContainer(schema, sourceContainer, sourceComponents);
  513. updateStatus("Applied to current 工作面板 instance.");
  514. }
  515. private static void applyText(Component component, String text) {
  516. if (component instanceof JButton) {
  517. ((JButton) component).setText(text);
  518. } else if (component instanceof JTextField) {
  519. ((JTextField) component).setText(text);
  520. } else if (component instanceof JLabel) {
  521. ((JLabel) component).setText(text);
  522. }
  523. }
  524. private void addComponentAt(String type, int x, int y) {
  525. pushHistory();
  526. JSONObject item = new JSONObject();
  527. JSONObject props = new JSONObject();
  528. String id = type + "-" + System.currentTimeMillis();
  529. int width = defaultWidth(type);
  530. int height = defaultHeight(type);
  531. item.put("id", id);
  532. item.put("type", type);
  533. item.put("x", clamp(snap(x), 0, CANVAS_WIDTH - width));
  534. item.put("y", clamp(snap(y), 0, CANVAS_HEIGHT - height));
  535. item.put("width", width);
  536. item.put("height", height);
  537. props.put("text", defaultText(type));
  538. props.put("enabled", true);
  539. item.put("props", props);
  540. item.put("style", new JSONObject());
  541. schema.getJSONArray("components").add(item);
  542. selectedId = id;
  543. markDirty();
  544. renderAll();
  545. }
  546. private int defaultWidth(String type) {
  547. return "panel".equals(type) || "textfield".equals(type) ? 180 : 120;
  548. }
  549. private int defaultHeight(String type) {
  550. return "panel".equals(type) ? 120 : 36;
  551. }
  552. private String defaultText(String type) {
  553. if ("label".equals(type)) {
  554. return "Label";
  555. }
  556. if ("button".equals(type)) {
  557. return "Button";
  558. }
  559. if ("textfield".equals(type)) {
  560. return "Text";
  561. }
  562. return "Panel";
  563. }
  564. private void renderAll() {
  565. renderCanvas(canvas, true);
  566. updateInspector();
  567. updateStatus(null);
  568. }
  569. private void renderCanvas(JPanel target, boolean editable) {
  570. target.removeAll();
  571. JSONArray components = schema.getJSONArray("components");
  572. for (int i = 0; i < components.size(); i++) {
  573. JSONObject item = components.getJSONObject(i);
  574. JComponent view = createView(item, editable);
  575. view.setBounds(item.getIntValue("x"), item.getIntValue("y"), item.getIntValue("width"), item.getIntValue("height"));
  576. target.add(view);
  577. }
  578. target.revalidate();
  579. target.repaint();
  580. }
  581. private JComponent createView(final JSONObject item, boolean editable) {
  582. String type = item.getString("type");
  583. String text = item.getJSONObject("props").getString("text");
  584. JComponent view;
  585. if ("button".equals(type)) {
  586. view = new DesignerButton(text, editable && selectedId != null && selectedId.equals(item.getString("id")));
  587. } else if ("textfield".equals(type)) {
  588. JTextField field = new DesignerTextField(text, editable && selectedId != null && selectedId.equals(item.getString("id")));
  589. field.setEditable(false);
  590. view = field;
  591. } else if ("panel".equals(type)) {
  592. JPanel panel = new DesignerPanel(editable && selectedId != null && selectedId.equals(item.getString("id")));
  593. panel.setLayout(new BorderLayout());
  594. panel.add(new JLabel(text, SwingConstants.CENTER), BorderLayout.CENTER);
  595. panel.setBackground(new Color(230, 235, 240));
  596. view = panel;
  597. } else {
  598. JLabel label = new DesignerLabel(text, editable && selectedId != null && selectedId.equals(item.getString("id")));
  599. label.setOpaque(true);
  600. label.setBackground(Color.WHITE);
  601. view = label;
  602. }
  603. view.setFont(new Font("Dialog", Font.PLAIN, 16));
  604. view.setEnabled(editable || item.getJSONObject("props").getBooleanValue("enabled"));
  605. view.setBorder(BorderFactory.createLineBorder(selectedId != null && selectedId.equals(item.getString("id")) && editable ? Color.BLUE : Color.GRAY, editable ? 2 : 1));
  606. if (editable) {
  607. installComponentEditing(view, item);
  608. }
  609. return view;
  610. }
  611. private void installComponentEditing(final JComponent view, final JSONObject item) {
  612. final String id = item.getString("id");
  613. MouseAdapter adapter = new MouseAdapter() {
  614. private Point startPoint;
  615. private Rectangle startBounds;
  616. private String resizeHandle;
  617. private boolean changed;
  618. public void mousePressed(MouseEvent e) {
  619. e.consume();
  620. selectedId = id;
  621. startPoint = SwingUtilities.convertPoint(view, e.getPoint(), canvas);
  622. startBounds = view.getBounds();
  623. resizeHandle = getResizeHandle(e.getPoint(), view);
  624. changed = false;
  625. pushHistory();
  626. updateInspector();
  627. view.setBorder(BorderFactory.createLineBorder(Color.BLUE, 2));
  628. canvas.repaint();
  629. canvas.requestFocusInWindow();
  630. }
  631. public void mouseDragged(MouseEvent e) {
  632. Point current = SwingUtilities.convertPoint(view, e.getPoint(), canvas);
  633. int dx = current.x - startPoint.x;
  634. int dy = current.y - startPoint.y;
  635. int nextX = startBounds.x;
  636. int nextY = startBounds.y;
  637. int nextWidth = startBounds.width;
  638. int nextHeight = startBounds.height;
  639. if (resizeHandle != null) {
  640. Rectangle resized = resizeBounds(startBounds, dx, dy, resizeHandle);
  641. nextX = resized.x;
  642. nextY = resized.y;
  643. nextWidth = resized.width;
  644. nextHeight = resized.height;
  645. } else {
  646. nextX = clamp(snap(startBounds.x + dx), 0, CANVAS_WIDTH - startBounds.width);
  647. nextY = clamp(snap(startBounds.y + dy), 0, CANVAS_HEIGHT - startBounds.height);
  648. }
  649. item.put("x", nextX);
  650. item.put("y", nextY);
  651. item.put("width", nextWidth);
  652. item.put("height", nextHeight);
  653. view.setBounds(nextX, nextY, nextWidth, nextHeight);
  654. changed = true;
  655. markDirty();
  656. updateInspector();
  657. canvas.repaint();
  658. }
  659. public void mouseReleased(MouseEvent e) {
  660. if (!changed && !undoStack.isEmpty()) {
  661. undoStack.pop();
  662. } else if (changed) {
  663. renderAll();
  664. }
  665. }
  666. public void mouseMoved(MouseEvent e) {
  667. view.setCursor(cursorForHandle(getResizeHandle(e.getPoint(), view)));
  668. }
  669. };
  670. view.addMouseListener(adapter);
  671. view.addMouseMotionListener(adapter);
  672. }
  673. private String getResizeHandle(Point point, JComponent component) {
  674. int w = component.getWidth();
  675. int h = component.getHeight();
  676. boolean left = point.x <= HANDLE_SIZE;
  677. boolean right = point.x >= w - HANDLE_SIZE;
  678. boolean top = point.y <= HANDLE_SIZE;
  679. boolean bottom = point.y >= h - HANDLE_SIZE;
  680. boolean middleX = point.x >= (w / 2) - HANDLE_SIZE && point.x <= (w / 2) + HANDLE_SIZE;
  681. boolean middleY = point.y >= (h / 2) - HANDLE_SIZE && point.y <= (h / 2) + HANDLE_SIZE;
  682. if (left && top) {
  683. return "nw";
  684. }
  685. if (right && top) {
  686. return "ne";
  687. }
  688. if (left && bottom) {
  689. return "sw";
  690. }
  691. if (right && bottom) {
  692. return "se";
  693. }
  694. if (middleX && top) {
  695. return "n";
  696. }
  697. if (middleX && bottom) {
  698. return "s";
  699. }
  700. if (left && middleY) {
  701. return "w";
  702. }
  703. if (right && middleY) {
  704. return "e";
  705. }
  706. return null;
  707. }
  708. private Rectangle resizeBounds(Rectangle startBounds, int dx, int dy, String handle) {
  709. int minSize = 24;
  710. int x = startBounds.x;
  711. int y = startBounds.y;
  712. int width = startBounds.width;
  713. int height = startBounds.height;
  714. if (handle.indexOf("e") >= 0) {
  715. width = clamp(snap(startBounds.width + dx), minSize, CANVAS_WIDTH - startBounds.x);
  716. }
  717. if (handle.indexOf("s") >= 0) {
  718. height = clamp(snap(startBounds.height + dy), minSize, CANVAS_HEIGHT - startBounds.y);
  719. }
  720. if (handle.indexOf("w") >= 0) {
  721. int newX = clamp(snap(startBounds.x + dx), 0, startBounds.x + startBounds.width - minSize);
  722. width = startBounds.x + startBounds.width - newX;
  723. x = newX;
  724. }
  725. if (handle.indexOf("n") >= 0) {
  726. int newY = clamp(snap(startBounds.y + dy), 0, startBounds.y + startBounds.height - minSize);
  727. height = startBounds.y + startBounds.height - newY;
  728. y = newY;
  729. }
  730. return new Rectangle(x, y, width, height);
  731. }
  732. private Cursor cursorForHandle(String handle) {
  733. if (handle == null) {
  734. return Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
  735. }
  736. if ("n".equals(handle) || "s".equals(handle)) {
  737. return Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR);
  738. }
  739. if ("e".equals(handle) || "w".equals(handle)) {
  740. return Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR);
  741. }
  742. if ("ne".equals(handle) || "sw".equals(handle)) {
  743. return Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR);
  744. }
  745. return Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR);
  746. }
  747. private int snap(int value) {
  748. return Math.round(value / (float) GRID_SIZE) * GRID_SIZE;
  749. }
  750. private int clamp(int value, int min, int max) {
  751. return Math.max(min, Math.min(max, value));
  752. }
  753. private void applyInspector() {
  754. JSONObject item = selectedItem();
  755. if (item == null) {
  756. return;
  757. }
  758. pushHistory();
  759. try {
  760. item.getJSONObject("props").put("text", textField.getText());
  761. item.put("x", clamp(snap(Integer.parseInt(xField.getText())), 0, CANVAS_WIDTH));
  762. item.put("y", clamp(snap(Integer.parseInt(yField.getText())), 0, CANVAS_HEIGHT));
  763. item.put("width", clamp(snap(Integer.parseInt(widthField.getText())), 24, CANVAS_WIDTH));
  764. item.put("height", clamp(snap(Integer.parseInt(heightField.getText())), 24, CANVAS_HEIGHT));
  765. markDirty();
  766. renderAll();
  767. } catch (NumberFormatException e) {
  768. if (!undoStack.isEmpty()) {
  769. undoStack.pop();
  770. }
  771. JOptionPane.showMessageDialog(this, "Position and size must be numbers.");
  772. updateInspector();
  773. }
  774. }
  775. private void deleteSelected() {
  776. if (selectedId == null) {
  777. return;
  778. }
  779. pushHistory();
  780. JSONArray components = schema.getJSONArray("components");
  781. for (int i = components.size() - 1; i >= 0; i--) {
  782. if (selectedId.equals(components.getJSONObject(i).getString("id"))) {
  783. components.remove(i);
  784. }
  785. }
  786. selectedId = null;
  787. markDirty();
  788. renderAll();
  789. }
  790. private JSONObject selectedItem() {
  791. if (selectedId == null) {
  792. return null;
  793. }
  794. JSONArray components = schema.getJSONArray("components");
  795. for (int i = 0; i < components.size(); i++) {
  796. JSONObject item = components.getJSONObject(i);
  797. if (selectedId.equals(item.getString("id"))) {
  798. return item;
  799. }
  800. }
  801. return null;
  802. }
  803. private void updateInspector() {
  804. JSONObject item = selectedItem();
  805. boolean enabled = item != null;
  806. textField.setEnabled(enabled);
  807. xField.setEnabled(enabled);
  808. yField.setEnabled(enabled);
  809. widthField.setEnabled(enabled);
  810. heightField.setEnabled(enabled);
  811. if (!enabled) {
  812. textField.setText("");
  813. xField.setText("");
  814. yField.setText("");
  815. widthField.setText("");
  816. heightField.setText("");
  817. return;
  818. }
  819. textField.setText(item.getJSONObject("props").getString("text"));
  820. xField.setText(String.valueOf(item.getIntValue("x")));
  821. yField.setText(String.valueOf(item.getIntValue("y")));
  822. widthField.setText(String.valueOf(item.getIntValue("width")));
  823. heightField.setText(String.valueOf(item.getIntValue("height")));
  824. }
  825. private void pushHistory() {
  826. undoStack.push(JSON.toJSONString(schema));
  827. redoStack.clear();
  828. }
  829. private void undo() {
  830. if (undoStack.isEmpty()) {
  831. return;
  832. }
  833. redoStack.push(JSON.toJSONString(schema));
  834. schema = JSON.parseObject(undoStack.pop());
  835. selectedId = null;
  836. markDirty();
  837. renderAll();
  838. }
  839. private void redo() {
  840. if (redoStack.isEmpty()) {
  841. return;
  842. }
  843. undoStack.push(JSON.toJSONString(schema));
  844. schema = JSON.parseObject(redoStack.pop());
  845. selectedId = null;
  846. markDirty();
  847. renderAll();
  848. }
  849. private void markDirty() {
  850. dirty = true;
  851. }
  852. private void updateStatus(String message) {
  853. if (message != null) {
  854. statusLabel.setText(message);
  855. return;
  856. }
  857. statusLabel.setText((dirty ? "Unsaved" : "Saved") + " - imported current 工作面板. Ctrl+S save, Ctrl+Z undo, Ctrl+Y redo. File: " + DESIGN_FILE);
  858. }
  859. private static void paintHandles(Component component, Graphics graphics, boolean selected) {
  860. if (!selected) {
  861. return;
  862. }
  863. graphics.setColor(Color.BLUE);
  864. int s = HANDLE_SIZE;
  865. int half = s / 2;
  866. int w = component.getWidth();
  867. int h = component.getHeight();
  868. int[][] points = new int[][]{
  869. {0, 0}, {w / 2, 0}, {w, 0},
  870. {0, h / 2}, {w, h / 2},
  871. {0, h}, {w / 2, h}, {w, h}
  872. };
  873. for (int i = 0; i < points.length; i++) {
  874. graphics.fillRect(points[i][0] - half, points[i][1] - half, s, s);
  875. }
  876. }
  877. private static class DesignerButton extends JButton {
  878. private final boolean selected;
  879. DesignerButton(String text, boolean selected) {
  880. super(text);
  881. this.selected = selected;
  882. }
  883. protected void paintComponent(Graphics graphics) {
  884. super.paintComponent(graphics);
  885. paintHandles(this, graphics, selected);
  886. }
  887. }
  888. private static class DesignerTextField extends JTextField {
  889. private final boolean selected;
  890. DesignerTextField(String text, boolean selected) {
  891. super(text);
  892. this.selected = selected;
  893. }
  894. protected void paintComponent(Graphics graphics) {
  895. super.paintComponent(graphics);
  896. paintHandles(this, graphics, selected);
  897. }
  898. }
  899. private static class DesignerLabel extends JLabel {
  900. private final boolean selected;
  901. DesignerLabel(String text, boolean selected) {
  902. super(text, SwingConstants.CENTER);
  903. this.selected = selected;
  904. }
  905. protected void paintComponent(Graphics graphics) {
  906. super.paintComponent(graphics);
  907. paintHandles(this, graphics, selected);
  908. }
  909. }
  910. private static class DesignerPanel extends JPanel {
  911. private final boolean selected;
  912. DesignerPanel(boolean selected) {
  913. this.selected = selected;
  914. }
  915. protected void paintComponent(Graphics graphics) {
  916. super.paintComponent(graphics);
  917. paintHandles(this, graphics, selected);
  918. }
  919. }
  920. }