1 package org.opentrafficsim.editor;
2
3 import java.awt.BorderLayout;
4 import java.awt.Color;
5 import java.awt.Component;
6 import java.awt.Dimension;
7 import java.awt.FileDialog;
8 import java.awt.Point;
9 import java.awt.Rectangle;
10 import java.awt.event.ActionEvent;
11 import java.awt.event.KeyAdapter;
12 import java.awt.event.KeyEvent;
13 import java.awt.event.MouseAdapter;
14 import java.awt.event.MouseEvent;
15 import java.awt.event.MouseWheelEvent;
16 import java.awt.event.MouseWheelListener;
17 import java.awt.event.WindowAdapter;
18 import java.awt.event.WindowEvent;
19 import java.io.File;
20 import java.io.FileOutputStream;
21 import java.io.IOException;
22 import java.nio.charset.StandardCharsets;
23 import java.nio.file.Files;
24 import java.nio.file.Path;
25 import java.nio.file.Paths;
26 import java.util.ArrayList;
27 import java.util.Date;
28 import java.util.Iterator;
29 import java.util.LinkedHashMap;
30 import java.util.LinkedHashSet;
31 import java.util.LinkedList;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Map.Entry;
35 import java.util.Optional;
36 import java.util.Set;
37 import java.util.Timer;
38 import java.util.TimerTask;
39 import java.util.function.Consumer;
40 import java.util.stream.Collectors;
41
42 import javax.imageio.ImageIO;
43 import javax.swing.Box;
44 import javax.swing.BoxLayout;
45 import javax.swing.CellEditor;
46 import javax.swing.DefaultCellEditor;
47 import javax.swing.Icon;
48 import javax.swing.ImageIcon;
49 import javax.swing.JButton;
50 import javax.swing.JComboBox;
51 import javax.swing.JComponent;
52 import javax.swing.JLabel;
53 import javax.swing.JMenu;
54 import javax.swing.JMenuBar;
55 import javax.swing.JMenuItem;
56 import javax.swing.JOptionPane;
57 import javax.swing.JPanel;
58 import javax.swing.JPopupMenu;
59 import javax.swing.JScrollPane;
60 import javax.swing.JSeparator;
61 import javax.swing.JSplitPane;
62 import javax.swing.JTabbedPane;
63 import javax.swing.JTable;
64 import javax.swing.JTextField;
65 import javax.swing.KeyStroke;
66 import javax.swing.ListSelectionModel;
67 import javax.swing.SwingConstants;
68 import javax.swing.SwingUtilities;
69 import javax.swing.UIManager;
70 import javax.swing.border.BevelBorder;
71 import javax.swing.border.LineBorder;
72 import javax.swing.event.CellEditorListener;
73 import javax.swing.event.PopupMenuEvent;
74 import javax.swing.event.PopupMenuListener;
75 import javax.swing.plaf.basic.BasicSplitPaneUI;
76 import javax.swing.table.DefaultTableColumnModel;
77 import javax.swing.table.TableColumn;
78 import javax.swing.tree.TreePath;
79 import javax.xml.parsers.DocumentBuilder;
80 import javax.xml.parsers.DocumentBuilderFactory;
81 import javax.xml.parsers.ParserConfigurationException;
82 import javax.xml.transform.OutputKeys;
83 import javax.xml.transform.Transformer;
84 import javax.xml.transform.TransformerException;
85 import javax.xml.transform.TransformerFactory;
86 import javax.xml.transform.dom.DOMSource;
87 import javax.xml.transform.stream.StreamResult;
88
89 import org.djutils.eval.Eval;
90 import org.djutils.event.EventListener;
91 import org.djutils.event.EventListenerMap;
92 import org.djutils.event.EventProducer;
93 import org.djutils.event.EventType;
94 import org.djutils.exceptions.Try;
95 import org.djutils.io.ResourceResolver;
96 import org.djutils.metadata.MetaData;
97 import org.djutils.metadata.ObjectDescriptor;
98 import org.opentrafficsim.editor.EvalWrapper.EvalListener;
99 import org.opentrafficsim.editor.Undo.ActionType;
100 import org.opentrafficsim.editor.decoration.DefaultDecorator;
101 import org.opentrafficsim.editor.listeners.AttributesListSelectionListener;
102 import org.opentrafficsim.editor.listeners.AttributesMouseListener;
103 import org.opentrafficsim.editor.listeners.ChangesListener;
104 import org.opentrafficsim.editor.listeners.PopupValueSelectedListener;
105 import org.opentrafficsim.editor.listeners.XsdTreeEditorListener;
106 import org.opentrafficsim.editor.listeners.XsdTreeKeyListener;
107 import org.opentrafficsim.editor.listeners.XsdTreeListener;
108 import org.opentrafficsim.editor.render.AttributeCellRenderer;
109 import org.opentrafficsim.editor.render.AttributesCellEditor;
110 import org.opentrafficsim.editor.render.StringCellRenderer;
111 import org.opentrafficsim.editor.render.XsdTreeCellRenderer;
112 import org.opentrafficsim.road.network.factory.xml.CircularDependencyException;
113 import org.opentrafficsim.swing.gui.Appearance;
114 import org.opentrafficsim.swing.gui.AppearanceApplication;
115 import org.opentrafficsim.swing.gui.AppearanceControlComboBox;
116 import org.w3c.dom.Document;
117 import org.w3c.dom.Element;
118 import org.w3c.dom.NamedNodeMap;
119 import org.xml.sax.SAXException;
120
121 import de.javagl.treetable.JTreeTable;
122
123
124
125
126
127
128
129
130
131
132
133
134 public class OtsEditor extends AppearanceApplication implements EventProducer
135 {
136
137
138 private static final long serialVersionUID = 20230217L;
139
140
141 public static final EventType NEW_FILE = new EventType("NEWFILE",
142 new MetaData("New file", "New file", new ObjectDescriptor("Root", "New root element", XsdTreeNodeRoot.class)));
143
144
145 public static final EventType SELECTION_CHANGED = new EventType("SELECTIONCHANGED", new MetaData("Selection",
146 "Selection changed", new ObjectDescriptor("Selected node", "Selected node", XsdTreeNode.class)));
147
148
149 private static final int DIVIDER_SIZE = 4;
150
151
152 private static final long AUTOSAVE_PERIOD_MS = 60000;
153
154
155 private static final boolean UPDATE_SPLIT_WHILE_DRAGGING = true;
156
157
158 public static final Color INACTIVE_COLOR = new Color(160, 160, 160);
159
160
161 private int dropdownIndent = 0;
162
163
164 private List<String> dropdownOptions = new ArrayList<>();
165
166
167 private final EventListenerMap listenerMap = new EventListenerMap();
168
169
170 private final JSplitPane leftRightSplitPane;
171
172
173 private final JTabbedPane visualizationPane;
174
175
176 private final JSplitPane rightSplitPane;
177
178
179 private final JComboBox<ScenarioWrapper> scenario;
180
181
182 private EvalWrapper evalWrapper = new EvalWrapper(this);
183
184
185 private JTreeTable treeTable;
186
187
188 private final JTable attributesTable;
189
190
191 private final JLabel statusLabel;
192
193
194 private boolean mayPresentChoice = true;
195
196
197 private XsdTreeNode choiceNode;
198
199
200 private Map<String, Icon> customIcons = new LinkedHashMap<>();
201
202
203 private final ImageIcon questionIcon;
204
205
206 private Document xsdDocument;
207
208
209 private String lastDirectory;
210
211
212 private String lastFile;
213
214
215 private boolean unsavedChanges = false;
216
217
218 private Undo undo;
219
220
221 private TimerTask autosave;
222
223
224
225
226 private JMenuItem backItem;
227
228
229 private XsdTreeNode candidateBackNode;
230
231
232 private final LinkedList<XsdTreeNode> backNode = new LinkedList<>();
233
234
235 private String candidateBackAttribute;
236
237
238 private final LinkedList<String> backAttribute = new LinkedList<>();
239
240
241 private JMenuItem coupledItem;
242
243
244 private XsdTreeNode coupledNode;
245
246
247
248
249 private XsdTreeNode clipboard;
250
251
252 private boolean cut;
253
254
255 private NodeActions nodeActions;
256
257
258 private static final ApplicationStore APPLICATION_STORE = new ApplicationStore("OTS", "editor");
259
260
261 private JMenu recentFilesMenu;
262
263
264
265
266
267 private final List<String> properties = new ArrayList<>();
268
269
270
271
272
273 public OtsEditor() throws IOException
274 {
275 super();
276 setSize(1280, 720);
277 setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
278 addWindowListener(new WindowAdapter()
279 {
280 @Override
281 public void windowClosing(final WindowEvent e)
282 {
283 exit();
284 }
285 });
286
287
288 this.leftRightSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, UPDATE_SPLIT_WHILE_DRAGGING);
289 this.leftRightSplitPane.setDividerSize(DIVIDER_SIZE);
290 this.leftRightSplitPane.setResizeWeight(0.5);
291 makeClickFlippable(this.leftRightSplitPane);
292 add(this.leftRightSplitPane);
293 this.rightSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, UPDATE_SPLIT_WHILE_DRAGGING);
294 this.rightSplitPane.setDividerSize(DIVIDER_SIZE);
295 this.rightSplitPane.setResizeWeight(0.5);
296 this.rightSplitPane.setAlignmentX(0.5f);
297 makeClickFlippable(this.rightSplitPane);
298 JPanel rightContainer = new JPanel();
299 rightContainer.setLayout(new BoxLayout(rightContainer, BoxLayout.Y_AXIS));
300 rightContainer.setBorder(new LineBorder(null, -1));
301
302
303 JPanel controlsContainer = new JPanel();
304 controlsContainer.add(Box.createHorizontalGlue());
305 controlsContainer.setLayout(new BoxLayout(controlsContainer, BoxLayout.X_AXIS));
306 controlsContainer.setBorder(new LineBorder(null, -1));
307 controlsContainer.setMinimumSize(new Dimension(200, 28));
308 controlsContainer.setPreferredSize(new Dimension(200, 28));
309 controlsContainer.add(new JLabel("Scenario: "));
310 this.scenario = new AppearanceControlComboBox<>();
311 this.scenario.addItem(new ScenarioWrapper(null));
312 this.scenario.setMinimumSize(new Dimension(50, 22));
313 this.scenario.setMaximumSize(new Dimension(250, 22));
314 this.scenario.setPreferredSize(new Dimension(200, 22));
315 this.scenario.addActionListener((a) ->
316 {
317 try
318 {
319 OtsEditor.this.evalWrapper.setDirty();
320 OtsEditor.this.evalWrapper
321 .getEval(OtsEditor.this.scenario.getItemAt(OtsEditor.this.scenario.getSelectedIndex()));
322 }
323 catch (CircularDependencyException ex)
324 {
325 showCircularInputParameters(ex.getMessage());
326 }
327 catch (RuntimeException ex)
328 {
329 showInvalidExpression(ex.getMessage());
330 }
331 });
332 controlsContainer.add(this.scenario);
333 controlsContainer.add(Box.createHorizontalStrut(2));
334 JButton playRun = new JButton();
335 playRun.setToolTipText("Run single run");
336 playRun.setIcon(DefaultDecorator.loadIcon("./Play.png", 18, 18, -1, -1));
337 Dimension iconDimension = new Dimension(24, 24);
338 playRun.setMinimumSize(iconDimension);
339 playRun.setMaximumSize(iconDimension);
340 playRun.setPreferredSize(iconDimension);
341 playRun.addActionListener((a) -> runSingle());
342 controlsContainer.add(playRun);
343 JButton playScenario = new JButton();
344 playScenario.setToolTipText("Run scenario (batch)");
345 playScenario.setIcon(DefaultDecorator.loadIcon("./NextTrack.png", 18, 18, -1, -1));
346 playScenario.setMinimumSize(iconDimension);
347 playScenario.setMaximumSize(iconDimension);
348 playScenario.setPreferredSize(iconDimension);
349 playScenario.addActionListener((a) -> runBatch(false));
350 controlsContainer.add(playScenario);
351 JButton playAll = new JButton();
352 playAll.setToolTipText("Run all (batch)");
353 playAll.setIcon(DefaultDecorator.loadIcon("./Last_recor.png", 18, 18, -1, -1));
354 playAll.setMinimumSize(iconDimension);
355 playAll.setMaximumSize(iconDimension);
356 playAll.setPreferredSize(iconDimension);
357 playAll.addActionListener((a) -> runBatch(true));
358 controlsContainer.add(playAll);
359 controlsContainer.add(Box.createHorizontalStrut(4));
360
361 rightContainer.add(controlsContainer);
362 rightContainer.add(this.rightSplitPane);
363 this.leftRightSplitPane.setRightComponent(rightContainer);
364
365 this.questionIcon = DefaultDecorator.loadIcon("./Question.png", -1, -1, -1, -1);
366
367
368 UIManager.getInsets("TabbedPane.contentBorderInsets").set(-1, -1, 1, -1);
369 this.visualizationPane = new JTabbedPane(JTabbedPane.BOTTOM, JTabbedPane.SCROLL_TAB_LAYOUT);
370 this.visualizationPane.setPreferredSize(new Dimension(900, 900));
371 this.visualizationPane.setBorder(new LineBorder(Color.BLACK, 0));
372 this.leftRightSplitPane.setLeftComponent(this.visualizationPane);
373
374
375
376
377
378 UIManager.put("Tree.collapsedIcon",
379 new ImageIcon(ImageIO.read(ResourceResolver.resolve("/Eclipse_collapsed.png").openStream())));
380 UIManager.put("Tree.expandedIcon",
381 new ImageIcon(ImageIO.read(ResourceResolver.resolve("/Eclipse_expanded.png").openStream())));
382
383
384 this.treeTable = new AppearanceControlTreeTable(new XsdTreeTableModel(null));
385 XsdTreeTableModel.applyColumnWidth(this.treeTable);
386 this.rightSplitPane.setTopComponent(new JScrollPane(this.treeTable));
387
388
389 AttributesTableModel tableModel = new AttributesTableModel(null, this.treeTable);
390 DefaultTableColumnModel columns = new DefaultTableColumnModel();
391 for (int i = 0; i < tableModel.getColumnCount(); i++)
392 {
393 TableColumn column = new TableColumn(i);
394 column.setHeaderValue(tableModel.getColumnName(i));
395 columns.addColumn(column);
396 }
397 this.attributesTable = new JTable(tableModel, columns);
398 this.attributesTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
399 this.attributesTable.putClientProperty("terminateEditOnFocusLost", true);
400 this.attributesTable.setDefaultRenderer(String.class,
401 new AttributeCellRenderer(DefaultDecorator.loadIcon("./Info.png", 12, 12, 16, 16)));
402 AttributesCellEditor editor = new AttributesCellEditor(this.attributesTable, this);
403 this.attributesTable.setDefaultEditor(String.class, editor);
404 this.attributesTable.addMouseListener(new AttributesMouseListener(this, this.attributesTable));
405 this.attributesTable.getSelectionModel()
406 .addListSelectionListener(new AttributesListSelectionListener(this, this.attributesTable));
407 AttributesTableModel.applyColumnWidth(this.attributesTable);
408 this.rightSplitPane.setBottomComponent(new JScrollPane(this.attributesTable));
409
410 addMenuBar();
411
412 this.statusLabel = new StatusLabel();
413 this.statusLabel.setHorizontalAlignment(SwingConstants.LEFT);
414 this.statusLabel.setBorder(new BevelBorder(BevelBorder.LOWERED));
415 add(this.statusLabel, BorderLayout.SOUTH);
416 removeStatusLabel();
417 }
418
419
420
421
422
423 private void makeClickFlippable(final JSplitPane pane)
424 {
425 ((BasicSplitPaneUI) pane.getUI()).getDivider().addMouseListener(new MouseAdapter()
426 {
427 @Override
428 public void mouseClicked(final MouseEvent e)
429 {
430 int size = pane.getOrientation() == JSplitPane.HORIZONTAL_SPLIT ? pane.getWidth() : pane.getHeight();
431 int target = size - pane.getDividerLocation();
432 int minimum = pane.getMinimumDividerLocation();
433 int maximum = pane.getMaximumDividerLocation();
434 pane.setDividerLocation(Math.min(Math.max(target, minimum), maximum));
435 }
436 });
437 }
438
439
440
441
442
443 public static Color getInvalidColor()
444 {
445 return APPLICATION_STORE.getColor("invalid_color");
446 }
447
448
449
450
451
452 public static Color getExpressionColor()
453 {
454 return APPLICATION_STORE.getColor("expression_color");
455 }
456
457
458
459
460 private void runSingle()
461 {
462 if (!((XsdTreeNode) this.treeTable.getTree().getModel().getRoot()).isValid())
463 {
464 showInvalidToRunMessage();
465 return;
466 }
467 int index = this.scenario.getSelectedIndex();
468 try
469 {
470 OtsEditor.this.evalWrapper.setDirty();
471 if (OtsEditor.this.evalWrapper.getEval(OtsEditor.this.scenario.getItemAt(index)) == null)
472 {
473 return;
474 }
475 }
476 catch (CircularDependencyException ex)
477 {
478 showCircularInputParameters(ex.getMessage());
479 return;
480 }
481 File file;
482 try
483 {
484 file = File.createTempFile("ots_", ".xml");
485 }
486 catch (IOException exception)
487 {
488 showUnableToRunFromTempFile();
489 return;
490 }
491 save(file, (XsdTreeNodeRoot) this.treeTable.getTree().getModel().getRoot(), false);
492
493 if (index == 0)
494 {
495 OtsRunner.runSingle(file, null);
496 }
497 else
498 {
499 String selectedScenario = this.scenario.getItemAt(index).scenarioNode().getId();
500 OtsRunner.runSingle(file, selectedScenario);
501 }
502 file.delete();
503 }
504
505
506
507
508
509 protected void runBatch(final boolean all)
510 {
511 if (!((XsdTreeNode) this.treeTable.getTree().getModel().getRoot()).isValid())
512 {
513 showInvalidToRunMessage();
514 return;
515 }
516
517 if (all)
518 {
519 System.out.println("Running all.");
520 }
521 else
522 {
523 int index = this.scenario.getSelectedIndex();
524 if (index == 0)
525 {
526 System.out.println("Running all runs of the default scenario.");
527 }
528 else
529 {
530 System.out.println("Running all runs of scenario " + this.scenario.getItemAt(index) + ".");
531 }
532 }
533 }
534
535
536
537
538
539 public Undo getUndo()
540 {
541 return this.undo;
542 }
543
544
545
546
547
548 public void collapse(final XsdTreeNode node)
549 {
550 TreePath path = this.treeTable.getTree().getSelectionPath();
551 if (this.treeTable.getTree().isExpanded(path))
552 {
553 getNodeActions().expand(node, path, true);
554 }
555 }
556
557
558
559
560
561
562 public void show(final XsdTreeNode node, final String attribute)
563 {
564 if (node.getParent() == null)
565 {
566 return;
567 }
568 if (this.treeTable.isEditing())
569 {
570 CellEditor editor = this.treeTable.getCellEditor();
571 if (editor != null)
572 {
573 editor.cancelCellEditing();
574 }
575 }
576 if (this.attributesTable.isEditing())
577 {
578 CellEditor editor = this.attributesTable.getCellEditor();
579 if (editor != null)
580 {
581 editor.cancelCellEditing();
582 }
583 }
584 List<XsdTreeNode> nodePath = node.getPath();
585 TreePath path = new TreePath(nodePath.toArray());
586 TreePath partialPath = new TreePath(nodePath.subList(0, nodePath.size() - 1).toArray());
587 this.treeTable.getTree().expandPath(partialPath);
588 this.treeTable.getTree().setSelectionPath(path);
589 this.treeTable.getTree().scrollPathToVisible(path);
590 this.treeTable.updateUI();
591 Rectangle bounds = this.treeTable.getTree().getPathBounds(path);
592 if (bounds == null)
593 {
594 return;
595 }
596 bounds.x += this.treeTable.getX();
597 bounds.y += this.treeTable.getY();
598 ((JComponent) this.treeTable.getParent()).scrollRectToVisible(bounds);
599
600 this.attributesTable.setModel(new AttributesTableModel(node.isActive() ? node : null, this.treeTable));
601 if (attribute != null)
602 {
603 int index = node.getAttributeIndexByName(attribute);
604 this.attributesTable.setRowSelectionInterval(index, index);
605 }
606 else
607 {
608 this.attributesTable.getSelectionModel().clearSelection();
609 }
610 }
611
612
613
614
615
616 public void setStatusLabel(final String label)
617 {
618 this.statusLabel.setText(label);
619 }
620
621
622
623
624 public void removeStatusLabel()
625 {
626 this.statusLabel.setText(" ");
627 }
628
629
630
631
632 private void addMenuBar()
633 {
634 JMenuBar menuBar = new JMenuBar();
635 setJMenuBar(menuBar);
636 JMenu fileMenu = new JMenu("File");
637 menuBar.add(fileMenu);
638 JMenuItem newFile = new JMenuItem("New");
639 newFile.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK));
640 fileMenu.add(newFile);
641 newFile.addActionListener((a) -> newFile());
642 JMenuItem open = new JMenuItem("Open...");
643 open.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK));
644 fileMenu.add(open);
645 open.addActionListener((a) -> openFile());
646 this.recentFilesMenu = new JMenu("Recent files");
647 updateRecentFileMenu();
648 fileMenu.add(this.recentFilesMenu);
649 JMenuItem save = new JMenuItem("Save");
650 save.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK));
651 fileMenu.add(save);
652 save.addActionListener((a) -> saveFile());
653 JMenuItem saveAs = new JMenuItem("Save as...");
654 fileMenu.add(saveAs);
655 saveAs.addActionListener((a) -> saveFileAs((XsdTreeNodeRoot) OtsEditor.this.treeTable.getTree().getModel().getRoot()));
656 fileMenu.add(new JSeparator());
657 JMenuItem propertiesItem = new JMenuItem("Properties...");
658 fileMenu.add(propertiesItem);
659 propertiesItem.addActionListener((a) -> new PropertiesDialog(this, this.properties, this.questionIcon));
660 fileMenu.add(new JSeparator());
661 JMenuItem exit = new JMenuItem("Exit");
662 fileMenu.add(exit);
663 exit.addActionListener((a) -> exit());
664
665 JMenu editMenu = new JMenu("Edit");
666 menuBar.add(editMenu);
667 JMenuItem undoItem = new JMenuItem("Undo");
668 undoItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, ActionEvent.CTRL_MASK));
669 editMenu.add(undoItem);
670 undoItem.addActionListener((a) -> OtsEditor.this.undo.undo());
671 JMenuItem redoItem = new JMenuItem("Redo");
672 redoItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, ActionEvent.CTRL_MASK));
673 editMenu.add(redoItem);
674 redoItem.addActionListener((a) -> OtsEditor.this.undo.redo());
675 this.undo = new Undo(this, undoItem, redoItem);
676
677 JMenu navigateMenu = new JMenu("Navigate");
678 menuBar.add(navigateMenu);
679 this.backItem = new JMenuItem("Go back");
680 this.backItem.setEnabled(false);
681 this.backItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0));
682 navigateMenu.add(this.backItem);
683 this.backItem.addActionListener((a) ->
684 {
685 show(this.backNode.pollLast(), this.backAttribute.pollLast());
686 if (this.backNode.isEmpty())
687 {
688 this.backItem.setText("Go back");
689 this.backItem.setEnabled(false);
690 }
691 else
692 {
693 XsdTreeNode back = this.backNode.peekLast();
694 this.backItem.setText("Go back to " + back.getNodeName() + (back.isIdentifiable() ? " " + back.getId() : ""));
695 this.backItem.setEnabled(true);
696 }
697 });
698 this.coupledItem = new JMenuItem("Go to coupled item");
699 this.coupledItem.setEnabled(false);
700 this.coupledItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F4, 0));
701 navigateMenu.add(this.coupledItem);
702 this.coupledItem.addActionListener((a) ->
703 {
704 if (this.coupledNode != null)
705 {
706 this.backNode.add(this.candidateBackNode);
707 this.backAttribute.add(this.candidateBackAttribute);
708 while (this.backNode.size() > APPLICATION_STORE.getInt("max_navigate"))
709 {
710 this.backNode.remove();
711 this.backAttribute.remove();
712 }
713 XsdTreeNode back = OtsEditor.this.backNode.peekLast();
714 this.backItem.setText("Go back to " + back.getNodeName() + (back.isIdentifiable() ? " " + back.getId() : ""));
715 this.backItem.setEnabled(this.backNode.peekLast() != null);
716 show(OtsEditor.this.coupledNode, null);
717 }
718 });
719 }
720
721
722
723
724 private void updateRecentFileMenu()
725 {
726 this.recentFilesMenu.removeAll();
727 List<String> files = APPLICATION_STORE.getRecentFiles("recent_files");
728 if (!files.isEmpty())
729 {
730 for (String file : files)
731 {
732 JMenuItem item = new JMenuItem(file);
733 item.addActionListener((i) ->
734 {
735 if (confirmDiscardChanges())
736 {
737 File f = new File(file);
738 this.lastDirectory = f.getParent() + File.separator;
739 this.lastFile = f.getName();
740 if (!loadFile(f, "File loaded", true))
741 {
742 boolean remove = JOptionPane.showConfirmDialog(OtsEditor.this,
743 "File could not be loaded. Do you want to remove it from recent files?",
744 "Remove from recent files?", JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE,
745 this.questionIcon) == JOptionPane.OK_OPTION;
746 if (remove)
747 {
748 APPLICATION_STORE.removeRecentFile("recent_files", file);
749 updateRecentFileMenu();
750 }
751 }
752 }
753 });
754 this.recentFilesMenu.add(item);
755 }
756 this.recentFilesMenu.add(new JSeparator());
757 }
758 JMenuItem item = new JMenuItem("Clear history");
759 item.setEnabled(!files.isEmpty());
760 item.addActionListener((i) ->
761 {
762 boolean clear = JOptionPane.showConfirmDialog(OtsEditor.this, "Are you sure you want to clear the recent files?",
763 "Clear recent files?", JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE,
764 this.questionIcon) == JOptionPane.OK_OPTION;
765 if (clear)
766 {
767 APPLICATION_STORE.clearProperty("recent_files");
768 updateRecentFileMenu();
769 }
770 });
771 this.recentFilesMenu.add(item);
772 }
773
774
775
776
777
778
779
780
781 public void setCoupledNode(final XsdTreeNode toNode, final XsdTreeNode fromNode, final String fromAttribute)
782 {
783 if (toNode == null)
784 {
785 this.coupledItem.setEnabled(false);
786 this.coupledItem.setText("Go to coupled item");
787 }
788 else
789 {
790 this.coupledItem.setEnabled(true);
791 this.coupledItem.setText("Go to " + (fromAttribute != null ? fromNode.getAttributeValue(fromAttribute)
792 : (fromNode.isIdentifiable() ? fromNode.getId() : fromNode.getValue())));
793 }
794 this.coupledNode = toNode;
795 this.candidateBackNode = fromNode;
796 this.candidateBackAttribute = fromAttribute;
797 }
798
799
800
801
802
803 public void setUnsavedChanges(final boolean unsavedChanges)
804 {
805 this.unsavedChanges = unsavedChanges;
806 StringBuilder title = new StringBuilder("OTS | The Open Traffic Simulator | Editor");
807 if (this.lastFile != null)
808 {
809 title.append(" (").append(this.lastDirectory).append(this.lastFile).append(")");
810 }
811 if (this.unsavedChanges)
812 {
813 title.append(" *");
814 }
815 setTitle(title.toString());
816 }
817
818
819
820
821
822
823 @SuppressWarnings("checkstyle:hiddenfield")
824 public void setSchema(final Document xsdDocument) throws IOException
825 {
826 this.xsdDocument = xsdDocument;
827 this.undo.setIgnoreChanges(true);
828 initializeTree();
829 this.undo.clear();
830 setStatusLabel("Schema " + xsdDocument.getBaseURI() + " loaded");
831 setVisible(true);
832 this.leftRightSplitPane.setDividerLocation(0.65);
833 this.rightSplitPane.setDividerLocation(0.75);
834 setAppearance(getAppearance());
835 SwingUtilities.invokeLater(() -> checkAutosave());
836 }
837
838
839
840
841 private void checkAutosave()
842 {
843 Path tmpPath = Paths.get(System.getProperty("java.io.tmpdir") + "ots" + File.separator);
844 File tmpDir = tmpPath.toFile();
845 if (!tmpDir.exists())
846 {
847 tmpDir.mkdir();
848 }
849 Iterator<Path> it;
850 try
851 {
852 it = Files.newDirectoryStream(tmpPath, "autosave*.xml").iterator();
853 }
854 catch (IOException ioe)
855 {
856
857 return;
858 }
859 if (it.hasNext())
860 {
861 File file = it.next().toFile();
862 int userInput = JOptionPane.showConfirmDialog(this,
863 "Autosave file " + file.getName() + " (" + new Date(file.lastModified())
864 + ") detected. Do you want to load this file? ('No' removes the file)",
865 "Autosave file detected", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE,
866 this.questionIcon);
867 if (userInput == JOptionPane.OK_OPTION)
868 {
869 boolean loaded = loadFile(file, "Autosave file loaded", false);
870 if (!loaded)
871 {
872 boolean remove = JOptionPane.showConfirmDialog(OtsEditor.this,
873 "Autosave file could not be loaded. Do you want ro remove it?", "Remove autosave?",
874 JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE,
875 this.questionIcon) == JOptionPane.YES_OPTION;
876 if (remove)
877 {
878 file.delete();
879 }
880 }
881 setUnsavedChanges(true);
882 this.treeTable.updateUI();
883 file.delete();
884 }
885 else if (userInput == JOptionPane.NO_OPTION)
886 {
887 file.delete();
888 }
889 }
890 }
891
892
893
894
895 private void newFile()
896 {
897 if (confirmDiscardChanges())
898 {
899 try
900 {
901 this.undo.setIgnoreChanges(true);
902 initializeTree();
903 this.attributesTable.setModel(new AttributesTableModel(null, this.treeTable));
904 this.undo.clear();
905 }
906 catch (IOException exception)
907 {
908 JOptionPane.showMessageDialog(this, "Unable to reload schema.", "Unable to reload schema.",
909 JOptionPane.WARNING_MESSAGE);
910 }
911 }
912 }
913
914
915
916
917
918 private void initializeTree() throws IOException
919 {
920 this.scenario.removeAllItems();
921 this.scenario.addItem(new ScenarioWrapper(null));
922 setDefaultProperties();
923
924
925 XsdTreeTableModel treeModel = new XsdTreeTableModel(this.xsdDocument);
926 this.treeTable = new AppearanceControlTreeTable(treeModel);
927 this.treeTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
928 this.nodeActions = new NodeActions(this, this.treeTable);
929 this.treeTable.putClientProperty("terminateEditOnFocusLost", true);
930 treeModel.setTreeTable(this.treeTable);
931 this.treeTable.setDefaultRenderer(String.class, new StringCellRenderer(this.treeTable));
932 ((DefaultCellEditor) this.treeTable.getDefaultEditor(String.class)).setClickCountToStart(1);
933 XsdTreeTableModel.applyColumnWidth(this.treeTable);
934
935 this.treeTable.getTree().setCellRenderer(new XsdTreeCellRenderer(this));
936
937
938
939
940 new XsdTreeEditorListener(this, this.treeTable);
941
942
943
944
945
946
947 new XsdTreeListener(this, this.treeTable, this.attributesTable);
948
949
950 new XsdTreeKeyListener(this, this.treeTable);
951
952 int dividerLocation = this.rightSplitPane.getDividerLocation();
953 this.rightSplitPane.setTopComponent(new JScrollPane(this.treeTable));
954 this.rightSplitPane.setDividerLocation(dividerLocation);
955
956 XsdTreeNodeRoot root = (XsdTreeNodeRoot) treeModel.getRoot();
957 EventListener listener = new ChangesListener(this, this.scenario);
958 root.addListener(listener, XsdTreeNodeRoot.NODE_CREATED);
959 root.addListener(listener, XsdTreeNodeRoot.NODE_REMOVED);
960 fireEvent(NEW_FILE, root);
961
962 setUnsavedChanges(false);
963 if (this.autosave != null)
964 {
965 this.autosave.cancel();
966 }
967 this.autosave = new TimerTask()
968 {
969 @Override
970 public void run()
971 {
972 if (OtsEditor.this.unsavedChanges)
973 {
974 setStatusLabel("Autosaving...");
975 File file = new File(System.getProperty("java.io.tmpdir") + "ots" + File.separator
976 + (OtsEditor.this.lastFile == null ? "autosave.xml" : "autosave_" + OtsEditor.this.lastFile));
977 save(file, root, false);
978 file.deleteOnExit();
979 setStatusLabel("Autosaved");
980 }
981 }
982 };
983 new Timer().scheduleAtFixedRate(this.autosave, AUTOSAVE_PERIOD_MS, AUTOSAVE_PERIOD_MS);
984 setAppearance(getAppearance());
985 }
986
987
988
989
990 void setDefaultProperties()
991 {
992 this.properties.clear();
993 this.properties.add("xmlns:ots");
994 this.properties.add("http://www.opentrafficsim.org/ots");
995 this.properties.add("xmlns:xi");
996 this.properties.add("http://www.w3.org/2001/XInclude");
997 this.properties.add("xmlns:xsi");
998 this.properties.add("http://www.w3.org/2001/XMLSchema-instance");
999 this.properties.add("xsi:schemaLocation");
1000 this.properties.add(null);
1001 }
1002
1003
1004
1005
1006 public void startUndoActionOnTreeTable()
1007 {
1008
1009 SwingUtilities.invokeLater(() ->
1010 {
1011 XsdTreeNode node = (XsdTreeNode) this.treeTable.getValueAt(this.treeTable.getSelectedRow(),
1012 this.treeTable.convertColumnIndexToView(XsdTreeTableModel.TREE_COLUMN));
1013 int col = this.treeTable.convertColumnIndexToModel(this.treeTable.getSelectedColumn());
1014 if (col == XsdTreeTableModel.ID_COLUMN)
1015 {
1016 this.undo.startAction(ActionType.ID_CHANGE, node, null);
1017 }
1018 else if (col == XsdTreeTableModel.VALUE_COLUMN)
1019 {
1020 this.undo.startAction(ActionType.VALUE_CHANGE, node, null);
1021 }
1022 });
1023 }
1024
1025
1026
1027
1028
1029
1030 public XsdTreeNode getTreeNodeAtPoint(final Point point)
1031 {
1032 int row = this.treeTable.rowAtPoint(point);
1033 int col = this.treeTable.convertColumnIndexToView(XsdTreeTableModel.TREE_COLUMN);
1034 return (XsdTreeNode) this.treeTable.getValueAt(row, col);
1035 }
1036
1037
1038
1039
1040
1041
1042
1043 public void preparePopupRemoval(final JPopupMenu popup, final JComponent component)
1044 {
1045 popup.addPopupMenuListener(new PopupMenuListener()
1046 {
1047 @Override
1048 public void popupMenuWillBecomeVisible(final PopupMenuEvent e)
1049 {
1050 }
1051
1052 @Override
1053 public void popupMenuWillBecomeInvisible(final PopupMenuEvent e)
1054 {
1055 component.setComponentPopupMenu(null);
1056 OtsEditor.this.choiceNode = null;
1057 }
1058
1059 @Override
1060 public void popupMenuCanceled(final PopupMenuEvent e)
1061 {
1062 }
1063 });
1064 }
1065
1066
1067
1068
1069
1070
1071
1072 public void setCustomIcon(final String path, final ImageIcon icon)
1073 {
1074 this.customIcons.put(path, icon);
1075 }
1076
1077
1078
1079
1080
1081
1082 public Optional<Icon> getCustomIcon(final String path)
1083 {
1084 Icon icon = this.customIcons.get(path);
1085 if (icon != null)
1086 {
1087 return Optional.of(icon);
1088 }
1089 for (Entry<String, Icon> entry : this.customIcons.entrySet())
1090 {
1091 if (path.endsWith(entry.getKey()))
1092 {
1093 return Optional.of(entry.getValue());
1094 }
1095 }
1096 return Optional.empty();
1097 }
1098
1099
1100
1101
1102
1103 public XsdTreeNode getChoiceNode()
1104 {
1105 return this.choiceNode;
1106 }
1107
1108
1109
1110
1111
1112 public void setChoiceNode(final XsdTreeNode choiceNode)
1113 {
1114 this.choiceNode = choiceNode;
1115 }
1116
1117
1118
1119
1120
1121 public void setMayPresentChoice(final boolean mayPresentChoice)
1122 {
1123 this.mayPresentChoice = mayPresentChoice;
1124 }
1125
1126
1127
1128
1129
1130 public boolean mayPresentChoice()
1131 {
1132 return this.mayPresentChoice;
1133 }
1134
1135 @Override
1136 public EventListenerMap getEventListenerMap()
1137 {
1138 return this.listenerMap;
1139 }
1140
1141
1142
1143
1144
1145
1146
1147
1148 public void addTab(final String name, final Icon icon, final Component component, final String tip)
1149 {
1150 this.visualizationPane.addTab(name, icon, component, tip);
1151 }
1152
1153
1154
1155
1156
1157
1158 public Optional<Component> getTab(final String name)
1159 {
1160 for (int index = 0; index < this.visualizationPane.getTabCount(); index++)
1161 {
1162 if (this.visualizationPane.getTitleAt(index).equals(name))
1163 {
1164 return Optional.of(this.visualizationPane.getComponentAt(index));
1165 }
1166 }
1167 return Optional.empty();
1168 }
1169
1170
1171
1172
1173
1174 public void focusTab(final String name)
1175 {
1176 for (int index = 0; index < this.visualizationPane.getTabCount(); index++)
1177 {
1178 if (this.visualizationPane.getTitleAt(index).equals(name))
1179 {
1180 this.visualizationPane.setSelectedIndex(index);
1181 }
1182 }
1183 }
1184
1185
1186
1187
1188
1189
1190
1191
1192 public boolean confirmNodeRemoval(final XsdTreeNode node)
1193 {
1194 return JOptionPane.showConfirmDialog(this, "Remove `" + node + "`?", "Remove?", JOptionPane.OK_CANCEL_OPTION,
1195 JOptionPane.QUESTION_MESSAGE, this.questionIcon) == JOptionPane.OK_OPTION;
1196 }
1197
1198
1199
1200
1201
1202 private boolean confirmDiscardChanges()
1203 {
1204 if (!this.unsavedChanges)
1205 {
1206 return true;
1207 }
1208 return JOptionPane.showConfirmDialog(this, "Discard unsaved changes?", "Discard unsaved changes?",
1209 JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, this.questionIcon) == JOptionPane.OK_OPTION;
1210 }
1211
1212
1213
1214
1215
1216 public void showDescription(final String description)
1217 {
1218 JOptionPane.showMessageDialog(OtsEditor.this,
1219 "<html><body><p style='width: 400px;'>" + description + "</p></body></html>");
1220 }
1221
1222
1223
1224
1225 public void showInvalidToRunMessage()
1226 {
1227 JOptionPane.showMessageDialog(OtsEditor.this, "The setup is not valid. Make sure no red nodes remain.",
1228 "Setup is not valid", JOptionPane.INFORMATION_MESSAGE);
1229 }
1230
1231
1232
1233
1234
1235 public void showCircularInputParameters(final String message)
1236 {
1237 JOptionPane.showMessageDialog(OtsEditor.this, "Input parameters have a circular dependency: " + message,
1238 "Circular input parameter", JOptionPane.INFORMATION_MESSAGE);
1239 }
1240
1241
1242
1243
1244
1245 public void showInvalidExpression(final String message)
1246 {
1247 JOptionPane.showMessageDialog(OtsEditor.this, "An expression is not valid: " + message, "Expression not valid",
1248 JOptionPane.INFORMATION_MESSAGE);
1249 }
1250
1251
1252
1253
1254 public void showUnableToRunFromTempFile()
1255 {
1256 JOptionPane.showMessageDialog(OtsEditor.this, "Unable to run, temporary file could not be saved.", "Unable to run",
1257 JOptionPane.INFORMATION_MESSAGE);
1258 }
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268 public void valueOptionsPopup(final List<String> allOptions, final JTable table, final Consumer<String> action)
1269 {
1270
1271 List<String> options = filterOptions(allOptions, "");
1272 OtsEditor.this.dropdownOptions = options;
1273 if (options.isEmpty())
1274 {
1275 return;
1276 }
1277 JPopupMenu popup = new JPopupMenu();
1278 int index = 0;
1279 int maxDropdown = APPLICATION_STORE.getInt("max_dropdown_items");
1280 for (String option : options)
1281 {
1282 JMenuItem item = new JMenuItem(option);
1283 item.setVisible(index++ < maxDropdown);
1284 item.addActionListener(new PopupValueSelectedListener(option, table, action, this.treeTable));
1285 item.setFont(table.getFont());
1286 popup.add(item);
1287 }
1288 this.dropdownIndent = 0;
1289 popup.addMouseWheelListener(new MouseWheelListener()
1290 {
1291 @Override
1292 public void mouseWheelMoved(final MouseWheelEvent e)
1293 {
1294 OtsEditor.this.dropdownIndent += (e.getWheelRotation() * e.getScrollAmount());
1295 OtsEditor.this.dropdownIndent = OtsEditor.this.dropdownIndent < 0 ? 0 : OtsEditor.this.dropdownIndent;
1296 int maxIndent = OtsEditor.this.dropdownOptions.size() - maxDropdown;
1297 if (maxIndent > 0)
1298 {
1299 OtsEditor.this.dropdownIndent =
1300 OtsEditor.this.dropdownIndent > maxIndent ? maxIndent : OtsEditor.this.dropdownIndent;
1301 showOptionsInScope(popup);
1302 }
1303 }
1304 });
1305 preparePopupRemoval(popup, table);
1306
1307 SwingUtilities.invokeLater(() ->
1308 {
1309 JTextField field = (JTextField) ((DefaultCellEditor) table.getDefaultEditor(String.class)).getComponent();
1310 table.setComponentPopupMenu(popup);
1311 popup.pack();
1312 popup.setInvoker(table);
1313 popup.setVisible(true);
1314 field.requestFocus();
1315 Rectangle rectangle = field.getBounds();
1316 placePopup(popup, rectangle, table);
1317 field.addKeyListener(new KeyAdapter()
1318 {
1319 @Override
1320 public void keyTyped(final KeyEvent e)
1321 {
1322
1323 SwingUtilities.invokeLater(() ->
1324 {
1325 OtsEditor.this.dropdownIndent = 0;
1326 String currentValue = field.getText();
1327 OtsEditor.this.dropdownOptions = filterOptions(allOptions, currentValue);
1328 boolean anyVisible = showOptionsInScope(popup);
1329
1330
1331 if (!anyVisible)
1332 {
1333 JMenuItem item = new JMenuItem(currentValue);
1334 item.addActionListener(
1335 new PopupValueSelectedListener(currentValue, table, action, OtsEditor.this.treeTable));
1336 item.setFont(table.getFont());
1337 popup.add(item);
1338 }
1339 popup.pack();
1340 placePopup(popup, rectangle, table);
1341 });
1342 }
1343 });
1344 field.addActionListener((e) ->
1345 {
1346 popup.setVisible(false);
1347 table.setComponentPopupMenu(null);
1348 });
1349 });
1350 }
1351
1352
1353
1354
1355
1356
1357
1358 private static List<String> filterOptions(final List<String> options, final String currentValue)
1359 {
1360 return options.stream().filter((val) -> currentValue == null || currentValue.isEmpty() || val.startsWith(currentValue))
1361 .distinct().sorted().collect(Collectors.toList());
1362 }
1363
1364
1365
1366
1367
1368
1369 private boolean showOptionsInScope(final JPopupMenu popup)
1370 {
1371 int optionIndex = 0;
1372 int maxDropdown = APPLICATION_STORE.getInt("max_dropdown_items");
1373 for (Component component : popup.getComponents())
1374 {
1375 JMenuItem item = (JMenuItem) component;
1376 boolean visible = optionIndex < maxDropdown && this.dropdownOptions.indexOf(item.getText()) >= this.dropdownIndent;
1377 item.setVisible(visible);
1378 if (visible)
1379 {
1380 optionIndex++;
1381 }
1382 }
1383 popup.pack();
1384 return optionIndex > 0;
1385 }
1386
1387
1388
1389
1390
1391
1392
1393 private void placePopup(final JPopupMenu popup, final Rectangle rectangle, final JComponent parent)
1394 {
1395 Point pAttributes = parent.getLocationOnScreen();
1396
1397 Dimension windowSize = OtsEditor.this.getSize();
1398 Point pWindow = OtsEditor.this.getLocationOnScreen();
1399 if (pAttributes.y + (int) rectangle.getMaxY() + popup.getBounds().getHeight() > windowSize.height + pWindow.y - 1)
1400 {
1401
1402 popup.setLocation(pAttributes.x + (int) rectangle.getMinX(),
1403 pAttributes.y + (int) rectangle.getMinY() - 1 - (int) popup.getBounds().getHeight());
1404 }
1405 else
1406 {
1407
1408 popup.setLocation(pAttributes.x + (int) rectangle.getMinX(), pAttributes.y + (int) rectangle.getMaxY() - 1);
1409 }
1410 }
1411
1412
1413
1414
1415 void openFile()
1416 {
1417 if (!confirmDiscardChanges())
1418 {
1419 return;
1420 }
1421 FileDialog fileDialog = new FileDialog(this, "Open XML", FileDialog.LOAD);
1422 fileDialog.setFilenameFilter((dir, name) -> name.toLowerCase().endsWith(".xml"));
1423 fileDialog.setVisible(true);
1424 String fileName = fileDialog.getFile();
1425 if (fileName == null)
1426 {
1427 return;
1428 }
1429 if (!fileName.toLowerCase().endsWith(".xml"))
1430 {
1431 return;
1432 }
1433 this.lastDirectory = fileDialog.getDirectory();
1434 this.lastFile = fileName;
1435 File file = new File(this.lastDirectory + this.lastFile);
1436 boolean loaded = loadFile(file, "File loaded", true);
1437 if (!loaded)
1438 {
1439 JOptionPane.showMessageDialog(this, "Unable to read file.", "Unable to read file.", JOptionPane.WARNING_MESSAGE);
1440 }
1441 }
1442
1443
1444
1445
1446
1447
1448
1449
1450 private boolean loadFile(final File file, final String postLoadStatus, final boolean updateRecentFiles)
1451 {
1452 try
1453 {
1454 Document document = DocumentReader.open(file.toURI());
1455 this.undo.setIgnoreChanges(true);
1456 initializeTree();
1457
1458 NamedNodeMap attributes = document.getFirstChild().getAttributes();
1459 for (int i = 0; i < attributes.getLength(); i++)
1460 {
1461 if (this.properties.contains(attributes.item(i).getNodeName()))
1462 {
1463 int index = this.properties.indexOf(attributes.item(i).getNodeName());
1464 this.properties.set(index, attributes.item(i).getNodeName());
1465 this.properties.set(index + 1, attributes.item(i).getNodeValue());
1466 }
1467 else
1468 {
1469 this.properties.add(attributes.item(i).getNodeName());
1470 this.properties.add(attributes.item(i).getNodeValue());
1471 }
1472 }
1473 XsdTreeNodeRoot root = (XsdTreeNodeRoot) OtsEditor.this.treeTable.getTree().getModel().getRoot();
1474 root.setDirectory(this.lastDirectory);
1475 root.loadXmlNodes(document.getFirstChild());
1476 this.undo.clear();
1477 setUnsavedChanges(false);
1478 setStatusLabel(postLoadStatus);
1479 this.undo.updateButtons();
1480 this.backItem.setEnabled(false);
1481 this.coupledItem.setEnabled(false);
1482 this.coupledItem.setText("Go to coupled item");
1483 this.treeTable.updateUI();
1484 if (updateRecentFiles)
1485 {
1486 APPLICATION_STORE.addRecentFile("recent_files", file.getAbsolutePath());
1487 updateRecentFileMenu();
1488 }
1489 return true;
1490 }
1491 catch (SAXException | IOException | ParserConfigurationException exception)
1492 {
1493 return false;
1494 }
1495 }
1496
1497
1498
1499
1500 private void saveFile()
1501 {
1502 XsdTreeNodeRoot root = (XsdTreeNodeRoot) OtsEditor.this.treeTable.getTree().getModel().getRoot();
1503 if (this.lastFile == null)
1504 {
1505 saveFileAs(root);
1506 return;
1507 }
1508 save(new File(this.lastDirectory + this.lastFile), root, true);
1509 setUnsavedChanges(false);
1510 setStatusLabel("Saved");
1511 }
1512
1513
1514
1515
1516
1517 public void saveFileAs(final XsdTreeNode root)
1518 {
1519 FileDialog fileDialog = new FileDialog(this, "Save XML", FileDialog.SAVE);
1520 fileDialog.setFile("*.xml");
1521 fileDialog.setVisible(true);
1522 String fileName = fileDialog.getFile();
1523 if (fileName == null)
1524 {
1525 return;
1526 }
1527 if (!fileName.toLowerCase().endsWith(".xml"))
1528 {
1529 fileName = fileName + ".xml";
1530 }
1531 if (root instanceof XsdTreeNodeRoot)
1532 {
1533 this.lastDirectory = fileDialog.getDirectory();
1534 this.lastFile = fileName;
1535 }
1536 save(new File(fileDialog.getDirectory() + fileName), root, true);
1537 if (root instanceof XsdTreeNodeRoot)
1538 {
1539 this.undo.setIgnoreChanges(true);
1540 ((XsdTreeNodeRoot) root).setDirectory(this.lastDirectory);
1541 this.treeTable.updateUI();
1542 this.attributesTable.updateUI();
1543 setUnsavedChanges(false);
1544 this.undo.setIgnoreChanges(false);
1545 }
1546 setStatusLabel("Saved");
1547 }
1548
1549
1550
1551
1552
1553
1554
1555 private void save(final File file, final XsdTreeNode root, final boolean storeAsRecent)
1556 {
1557 try (FileOutputStream fileOutputStream = new FileOutputStream(file))
1558 {
1559 DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
1560 Document document = docBuilder.newDocument();
1561
1562
1563
1564
1565
1566
1567 document.setXmlStandalone(true);
1568 root.saveXmlNodes(document, document);
1569 Element xmlRoot = (Element) document.getChildNodes().item(0);
1570 Set<String> nameSpaces = new LinkedHashSet<>();
1571 nameSpaces.add("xmlns");
1572 for (int i = 0; i < this.properties.size(); i = i + 2)
1573 {
1574 String prop = this.properties.get(i);
1575 String value = this.properties.get(i + 1);
1576 if (prop.startsWith("xmlns") && value != null && !value.isBlank())
1577 {
1578 nameSpaces.add(prop.substring(6));
1579 }
1580 }
1581 for (int i = 0; i < this.properties.size(); i = i + 2)
1582 {
1583 String prop = this.properties.get(i);
1584 String value = this.properties.get(i + 1);
1585 int semi = prop.indexOf(":");
1586 String nameSpace = semi < 0 ? null : prop.substring(0, semi);
1587 if (!nameSpaces.contains(nameSpace) && value != null && !value.isBlank())
1588 {
1589 JOptionPane.showMessageDialog(this,
1590 "Unable to save property " + prop + " as its namespace xmlns:" + nameSpace + " is not provided.",
1591 "Unable to save property.", JOptionPane.WARNING_MESSAGE);
1592 }
1593 else if (value != null && !value.isBlank())
1594 {
1595 xmlRoot.setAttribute(prop, value);
1596 }
1597 }
1598 StreamResult result = new StreamResult(fileOutputStream);
1599
1600 Transformer transformer = TransformerFactory.newInstance().newTransformer();
1601 transformer.setOutputProperty(OutputKeys.INDENT, "yes");
1602 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
1603 transformer.transform(new DOMSource(document), result);
1604
1605 fileOutputStream.close();
1606
1607 String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
1608 content = content.replace("<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
1609 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + System.lineSeparator());
1610 Files.write(file.toPath(), content.getBytes(StandardCharsets.UTF_8));
1611
1612 if (storeAsRecent)
1613 {
1614 APPLICATION_STORE.addRecentFile("recent_files", file.getAbsolutePath());
1615 updateRecentFileMenu();
1616 }
1617 }
1618 catch (ParserConfigurationException | TransformerException | IOException exception)
1619 {
1620 JOptionPane.showMessageDialog(this, "Unable to save file.", "Unable to save file.", JOptionPane.WARNING_MESSAGE);
1621 }
1622 }
1623
1624
1625
1626
1627 private void exit()
1628 {
1629 if (confirmDiscardChanges())
1630 {
1631 System.exit(0);
1632 }
1633 }
1634
1635
1636
1637
1638
1639
1640
1641 public static String limitTooltip(final String message)
1642 {
1643 int maxTooltipLength = APPLICATION_STORE.getInt("max_tooltip_length");
1644 if (message == null || message.length() < maxTooltipLength)
1645 {
1646 return message;
1647 }
1648 return message.substring(0, maxTooltipLength - 3) + "...";
1649 }
1650
1651
1652
1653
1654
1655 public void addAttributeCellEditorListener(final CellEditorListener listener)
1656 {
1657 this.attributesTable.getDefaultEditor(String.class).addCellEditorListener(listener);
1658 }
1659
1660
1661
1662
1663
1664
1665 @SuppressWarnings("hiddenfield")
1666 public void setClipboard(final XsdTreeNode clipboard, final boolean cut)
1667 {
1668 this.clipboard = clipboard;
1669 this.cut = cut;
1670 }
1671
1672
1673
1674
1675
1676 public XsdTreeNode getClipboard()
1677 {
1678 return this.clipboard;
1679 }
1680
1681
1682
1683
1684 public void removeClipboardWhenCut()
1685 {
1686 if (this.clipboard != null && this.cut)
1687 {
1688 if (this.clipboard.isRemovable())
1689 {
1690 this.clipboard.remove();
1691 }
1692 this.clipboard = null;
1693 }
1694 }
1695
1696
1697
1698
1699
1700 public NodeActions getNodeActions()
1701 {
1702 return this.nodeActions;
1703 }
1704
1705 @Override
1706 public boolean addListener(final EventListener listener, final EventType eventType)
1707 {
1708 return Try.assign(() -> EventProducer.super.addListener(listener, eventType),
1709 "Local event producer should not give a RemoteException.");
1710 }
1711
1712
1713
1714
1715
1716
1717 public Eval getEval()
1718 {
1719 try
1720 {
1721 Eval eval = this.evalWrapper.getEval(OtsEditor.this.scenario.getItemAt(OtsEditor.this.scenario.getSelectedIndex()));
1722 return eval == null ? this.evalWrapper.getLastValidEval() : eval;
1723 }
1724 catch (CircularDependencyException ex)
1725 {
1726 showCircularInputParameters(ex.getMessage());
1727 return this.evalWrapper.getLastValidEval();
1728 }
1729 catch (RuntimeException ex)
1730 {
1731
1732 return this.evalWrapper.getLastValidEval();
1733 }
1734 }
1735
1736
1737
1738
1739
1740 public void addEvalListener(final EvalListener listener)
1741 {
1742 this.evalWrapper.addListener(listener);
1743 }
1744
1745
1746
1747
1748
1749 public void removeEvalListener(final EvalListener listener)
1750 {
1751 this.evalWrapper.removeListener(listener);
1752 }
1753
1754 @Override
1755 public void setAppearance(final Appearance appearance)
1756 {
1757 super.setAppearance(appearance);
1758
1759 if (this.treeTable != null)
1760 {
1761 changeFont((Component) this.treeTable.getTree().getCellRenderer(), appearance.getFont());
1762 changeFontSize((Component) this.treeTable.getTree().getCellRenderer());
1763 }
1764 }
1765
1766 }