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