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.FlowLayout;
9 import java.awt.Graphics;
10 import java.awt.Image;
11 import java.awt.Point;
12 import java.awt.Rectangle;
13 import java.awt.event.ActionEvent;
14 import java.awt.event.ActionListener;
15 import java.awt.event.FocusEvent;
16 import java.awt.event.FocusListener;
17 import java.awt.event.KeyAdapter;
18 import java.awt.event.KeyEvent;
19 import java.awt.event.MouseEvent;
20 import java.awt.event.MouseMotionAdapter;
21 import java.awt.event.MouseWheelEvent;
22 import java.awt.event.MouseWheelListener;
23 import java.awt.event.WindowAdapter;
24 import java.awt.event.WindowEvent;
25 import java.awt.image.BufferedImage;
26 import java.io.File;
27 import java.io.FileOutputStream;
28 import java.io.FilenameFilter;
29 import java.io.IOException;
30 import java.nio.charset.StandardCharsets;
31 import java.nio.file.Files;
32 import java.nio.file.Path;
33 import java.nio.file.Paths;
34 import java.rmi.RemoteException;
35 import java.util.ArrayList;
36 import java.util.Date;
37 import java.util.Iterator;
38 import java.util.LinkedHashMap;
39 import java.util.LinkedHashSet;
40 import java.util.LinkedList;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.Map.Entry;
44 import java.util.Set;
45 import java.util.Timer;
46 import java.util.TimerTask;
47 import java.util.function.Consumer;
48 import java.util.stream.Collectors;
49
50 import javax.imageio.ImageIO;
51 import javax.swing.Box;
52 import javax.swing.BoxLayout;
53 import javax.swing.CellEditor;
54 import javax.swing.DefaultCellEditor;
55 import javax.swing.Icon;
56 import javax.swing.ImageIcon;
57 import javax.swing.JButton;
58 import javax.swing.JComboBox;
59 import javax.swing.JComponent;
60 import javax.swing.JDialog;
61 import javax.swing.JLabel;
62 import javax.swing.JMenu;
63 import javax.swing.JMenuBar;
64 import javax.swing.JMenuItem;
65 import javax.swing.JOptionPane;
66 import javax.swing.JPanel;
67 import javax.swing.JPopupMenu;
68 import javax.swing.JScrollPane;
69 import javax.swing.JSeparator;
70 import javax.swing.JSplitPane;
71 import javax.swing.JTabbedPane;
72 import javax.swing.JTable;
73 import javax.swing.JTextField;
74 import javax.swing.KeyStroke;
75 import javax.swing.ListSelectionModel;
76 import javax.swing.SwingConstants;
77 import javax.swing.SwingUtilities;
78 import javax.swing.UIManager;
79 import javax.swing.border.BevelBorder;
80 import javax.swing.border.EmptyBorder;
81 import javax.swing.border.LineBorder;
82 import javax.swing.event.CellEditorListener;
83 import javax.swing.event.ChangeEvent;
84 import javax.swing.event.PopupMenuEvent;
85 import javax.swing.event.PopupMenuListener;
86 import javax.swing.event.TreeExpansionEvent;
87 import javax.swing.event.TreeWillExpandListener;
88 import javax.swing.table.AbstractTableModel;
89 import javax.swing.table.DefaultTableCellRenderer;
90 import javax.swing.table.DefaultTableColumnModel;
91 import javax.swing.table.TableColumn;
92 import javax.swing.table.TableColumnModel;
93 import javax.swing.table.TableModel;
94 import javax.swing.tree.ExpandVetoException;
95 import javax.swing.tree.TreePath;
96 import javax.xml.parsers.DocumentBuilder;
97 import javax.xml.parsers.DocumentBuilderFactory;
98 import javax.xml.parsers.ParserConfigurationException;
99 import javax.xml.transform.OutputKeys;
100 import javax.xml.transform.Transformer;
101 import javax.xml.transform.TransformerException;
102 import javax.xml.transform.TransformerFactory;
103 import javax.xml.transform.dom.DOMSource;
104 import javax.xml.transform.stream.StreamResult;
105
106 import org.djutils.eval.Eval;
107 import org.djutils.event.EventListener;
108 import org.djutils.event.EventListenerMap;
109 import org.djutils.event.EventProducer;
110 import org.djutils.event.EventType;
111 import org.djutils.exceptions.Try;
112 import org.djutils.metadata.MetaData;
113 import org.djutils.metadata.ObjectDescriptor;
114 import org.opentrafficsim.base.Resource;
115 import org.opentrafficsim.editor.EvalWrapper.EvalListener;
116 import org.opentrafficsim.editor.Undo.ActionType;
117 import org.opentrafficsim.editor.listeners.AttributesListSelectionListener;
118 import org.opentrafficsim.editor.listeners.AttributesMouseListener;
119 import org.opentrafficsim.editor.listeners.ChangesListener;
120 import org.opentrafficsim.editor.listeners.XsdTreeKeyListener;
121 import org.opentrafficsim.editor.listeners.XsdTreeMouseListener;
122 import org.opentrafficsim.editor.listeners.XsdTreeSelectionListener;
123 import org.opentrafficsim.editor.render.AttributeCellRenderer;
124 import org.opentrafficsim.editor.render.AttributesCellEditor;
125 import org.opentrafficsim.editor.render.StringCellRenderer;
126 import org.opentrafficsim.editor.render.XsdTreeCellRenderer;
127 import org.opentrafficsim.road.network.factory.xml.CircularDependencyException;
128 import org.opentrafficsim.swing.gui.Appearance;
129 import org.opentrafficsim.swing.gui.AppearanceApplication;
130 import org.opentrafficsim.swing.gui.AppearanceControlComboBox;
131 import org.w3c.dom.Document;
132 import org.w3c.dom.Element;
133 import org.w3c.dom.NamedNodeMap;
134 import org.xml.sax.SAXException;
135
136 import de.javagl.treetable.JTreeTable;
137
138
139
140
141
142
143
144
145
146
147
148
149 public class OtsEditor extends AppearanceApplication implements EventProducer
150 {
151
152
153 private static final long serialVersionUID = 20230217L;
154
155
156 public static final EventType NEW_FILE = new EventType("NEWFILE",
157 new MetaData("New file", "New file", new ObjectDescriptor("Root", "New root element", XsdTreeNodeRoot.class)));
158
159
160 public static final EventType SELECTION_CHANGED = new EventType("SELECTIONCHANGED", new MetaData("Selection",
161 "Selection changed", new ObjectDescriptor("Selected node", "Selected node", XsdTreeNode.class)));
162
163
164 private static final int DIVIDER_SIZE = 3;
165
166
167 private static final long AUTOSAVE_PERIOD_MS = 60000;
168
169
170 private static final boolean UPDATE_SPLIT_WHILE_DRAGGING = true;
171
172
173 public static final Color INACTIVE_COLOR = new Color(160, 160, 160);
174
175
176 public static final Color STATUS_COLOR = new Color(128, 128, 128);
177
178
179 public static final Color INVALID_COLOR = new Color(255, 240, 240);
180
181
182 public static final Color EXPRESSION_COLOR = new Color(252, 250, 239);
183
184
185 private static final int MAX_TOOLTIP_LENGTH = 96;
186
187
188 private static final int MAX_DROPDOWN_ITEMS = 20;
189
190
191 private static final int MAX_NAVIGATE = 50;
192
193
194 private int dropdownIndent = 0;
195
196
197 private List<String> dropdownOptions = new ArrayList<>();
198
199
200 private final EventListenerMap listenerMap = new EventListenerMap();
201
202
203 private final JSplitPane leftRightSplitPane;
204
205
206 private final JTabbedPane visualizationPane;
207
208
209 private final JSplitPane rightSplitPane;
210
211
212 private final JComboBox<ScenarioWrapper> scenario;
213
214
215 private EvalWrapper evalWrapper = new EvalWrapper(this);
216
217
218 private JTreeTable treeTable;
219
220
221 private final JTable attributesTable;
222
223
224 private final JLabel statusLabel;
225
226
227 private boolean mayPresentChoice = true;
228
229
230 private XsdTreeNode choiceNode;
231
232
233 private Map<String, Icon> customIcons = new LinkedHashMap<>();
234
235
236 private final ImageIcon questionIcon;
237
238
239 private Document xsdDocument;
240
241
242 private String lastDirectory;
243
244
245 private String lastFile;
246
247
248 private boolean unsavedChanges = false;
249
250
251 private Undo undo;
252
253
254 private TimerTask autosave;
255
256
257
258
259 private JMenuItem backItem;
260
261
262 private XsdTreeNode candidateBackNode;
263
264
265 private final LinkedList<XsdTreeNode> backNode = new LinkedList<>();
266
267
268 private String candidateBackAttribute;
269
270
271 private final LinkedList<String> backAttribute = new LinkedList<>();
272
273
274 private JMenuItem coupledItem;
275
276
277 private XsdTreeNode coupledNode;
278
279
280
281
282 private XsdTreeNode clipboard;
283
284
285 private boolean cut;
286
287
288 private NodeActions nodeActions;
289
290
291 private ApplicationStore applicationStore = new ApplicationStore("ots", "editor");
292
293
294 private JMenu recentFilesMenu;
295
296
297
298
299
300 private final List<String> properties = new ArrayList<>();
301
302
303
304
305
306 public OtsEditor() throws IOException
307 {
308 super();
309
310 setSize(1280, 720);
311 setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
312 addWindowListener(new WindowAdapter()
313 {
314 @Override
315 public void windowClosing(final WindowEvent e)
316 {
317 exit();
318 }
319 });
320
321
322 this.leftRightSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, UPDATE_SPLIT_WHILE_DRAGGING);
323 this.leftRightSplitPane.setDividerSize(DIVIDER_SIZE);
324 this.leftRightSplitPane.setResizeWeight(0.5);
325 add(this.leftRightSplitPane);
326 this.rightSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, UPDATE_SPLIT_WHILE_DRAGGING);
327 this.rightSplitPane.setDividerSize(DIVIDER_SIZE);
328 this.rightSplitPane.setResizeWeight(0.5);
329 this.rightSplitPane.setAlignmentX(0.5f);
330
331 JPanel rightContainer = new JPanel();
332 rightContainer.setLayout(new BoxLayout(rightContainer, BoxLayout.Y_AXIS));
333 rightContainer.setBorder(new LineBorder(null, -1));
334
335
336 JPanel controlsContainer = new JPanel();
337 controlsContainer.add(Box.createHorizontalGlue());
338 controlsContainer.setLayout(new BoxLayout(controlsContainer, BoxLayout.X_AXIS));
339 controlsContainer.setBorder(new LineBorder(null, -1));
340 controlsContainer.setMinimumSize(new Dimension(200, 28));
341 controlsContainer.setPreferredSize(new Dimension(200, 28));
342 JLabel scenarioLabel = new JLabel("Scenario: ");
343
344 controlsContainer.add(scenarioLabel);
345 this.scenario = new AppearanceControlComboBox<>();
346
347 this.scenario.addItem(new ScenarioWrapper(null));
348 this.scenario.setMinimumSize(new Dimension(50, 22));
349 this.scenario.setMaximumSize(new Dimension(250, 22));
350 this.scenario.setPreferredSize(new Dimension(200, 22));
351 this.scenario.addActionListener((a) ->
352 {
353 try
354 {
355 OtsEditor.this.evalWrapper.setDirty();
356 OtsEditor.this.evalWrapper
357 .getEval(OtsEditor.this.scenario.getItemAt(OtsEditor.this.scenario.getSelectedIndex()));
358 }
359 catch (CircularDependencyException exception)
360 {
361 showCircularInputParameters();
362 }
363 catch (RuntimeException exception)
364 {
365
366 }
367 });
368 controlsContainer.add(this.scenario);
369 controlsContainer.add(Box.createHorizontalStrut(2));
370 JButton playRun = new JButton();
371 playRun.setToolTipText("Run single run");
372 playRun.setIcon(loadIcon("./Play.png", 18, 18, -1, -1));
373 playRun.setMinimumSize(new Dimension(24, 24));
374 playRun.setMaximumSize(new Dimension(24, 24));
375 playRun.setPreferredSize(new Dimension(24, 24));
376 playRun.addActionListener((a) -> runSingle());
377 controlsContainer.add(playRun);
378 JButton playScenario = new JButton();
379 playScenario.setToolTipText("Run scenario (batch)");
380 playScenario.setIcon(loadIcon("./NextTrack.png", 18, 18, -1, -1));
381 playScenario.setMinimumSize(new Dimension(24, 24));
382 playScenario.setMaximumSize(new Dimension(24, 24));
383 playScenario.setPreferredSize(new Dimension(24, 24));
384 playScenario.addActionListener((a) -> runBatch(false));
385 controlsContainer.add(playScenario);
386 JButton playAll = new JButton();
387 playAll.setToolTipText("Run all (batch)");
388 playAll.setIcon(loadIcon("./Last_recor.png", 18, 18, -1, -1));
389 playAll.setMinimumSize(new Dimension(24, 24));
390 playAll.setMaximumSize(new Dimension(24, 24));
391 playAll.setPreferredSize(new Dimension(24, 24));
392 playAll.addActionListener((a) -> runBatch(true));
393 controlsContainer.add(playAll);
394 controlsContainer.add(Box.createHorizontalStrut(4));
395
396 rightContainer.add(controlsContainer);
397 rightContainer.add(this.rightSplitPane);
398 this.leftRightSplitPane.setRightComponent(rightContainer);
399
400 this.questionIcon = loadIcon("./Question.png", -1, -1, -1, -1);
401
402
403 UIManager.getInsets("TabbedPane.contentBorderInsets").set(-1, -1, 1, -1);
404 this.visualizationPane = new JTabbedPane(JTabbedPane.BOTTOM, JTabbedPane.SCROLL_TAB_LAYOUT);
405 this.visualizationPane.setPreferredSize(new Dimension(900, 900));
406 this.visualizationPane.setBorder(new LineBorder(Color.BLACK, 0));
407 this.leftRightSplitPane.setLeftComponent(this.visualizationPane);
408
409
410
411
412
413 UIManager.put("Tree.collapsedIcon",
414 new ImageIcon(ImageIO.read(Resource.getResourceAsStream("/Eclipse_collapsed.png"))));
415 UIManager.put("Tree.expandedIcon", new ImageIcon(ImageIO.read(Resource.getResourceAsStream("/Eclipse_expanded.png"))));
416
417
418 this.treeTable = new AppearanceControlTreeTable(new XsdTreeTableModel(null));
419 XsdTreeTableModel.applyColumnWidth(this.treeTable);
420 this.rightSplitPane.setTopComponent(new JScrollPane(this.treeTable));
421
422
423 AttributesTableModel tableModel = new AttributesTableModel(null, this.treeTable);
424 DefaultTableColumnModel columns = new DefaultTableColumnModel();
425 TableColumn column1 = new TableColumn(0);
426 column1.setHeaderValue(tableModel.getColumnName(0));
427 columns.addColumn(column1);
428 TableColumn column2 = new TableColumn(1);
429 column2.setHeaderValue(tableModel.getColumnName(1));
430 columns.addColumn(column2);
431 TableColumn column3 = new TableColumn(2);
432 column3.setHeaderValue(tableModel.getColumnName(2));
433 columns.addColumn(column3);
434 TableColumn column4 = new TableColumn(3);
435 column4.setHeaderValue(tableModel.getColumnName(3));
436 columns.addColumn(column4);
437 this.attributesTable = new JTable(tableModel, columns);
438 this.attributesTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
439 this.attributesTable.putClientProperty("terminateEditOnFocusLost", true);
440 this.attributesTable.setDefaultRenderer(String.class,
441 new AttributeCellRenderer(loadIcon("./Info.png", 12, 12, 16, 16)));
442 AttributesCellEditor editor = new AttributesCellEditor(this.attributesTable, this);
443 this.attributesTable.setDefaultEditor(String.class, editor);
444 this.attributesTable.addMouseListener(new AttributesMouseListener(this, this.attributesTable));
445 this.attributesTable.getSelectionModel()
446 .addListSelectionListener(new AttributesListSelectionListener(this, this.attributesTable));
447 AttributesTableModel.applyColumnWidth(this.attributesTable);
448 this.rightSplitPane.setBottomComponent(new JScrollPane(this.attributesTable));
449
450 addMenuBar();
451
452 this.statusLabel = new StatusLabel();
453 this.statusLabel.setForeground(STATUS_COLOR);
454 this.statusLabel.setHorizontalAlignment(SwingConstants.LEFT);
455 this.statusLabel.setBorder(new BevelBorder(BevelBorder.LOWERED));
456 add(this.statusLabel, BorderLayout.SOUTH);
457 removeStatusLabel();
458 }
459
460
461
462
463 private void runSingle()
464 {
465 if (!((XsdTreeNode) this.treeTable.getTree().getModel().getRoot()).isValid())
466 {
467 showInvalidMessage();
468 return;
469 }
470 int index = this.scenario.getSelectedIndex();
471 try
472 {
473 OtsEditor.this.evalWrapper.setDirty();
474 OtsEditor.this.evalWrapper.getEval(OtsEditor.this.scenario.getItemAt(index));
475 }
476 catch (CircularDependencyException ex)
477 {
478 showCircularInputParameters();
479 return;
480 }
481 File file;
482 try
483 {
484 file = File.createTempFile("ots_", ".xml");
485 }
486 catch (IOException exception)
487 {
488 showUnableToRun();
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 scenario = this.scenario.getItemAt(index).getScenarioNode().getId();
500 OtsRunner.runSingle(file, scenario);
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 showInvalidMessage();
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 this.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 if (node.isActive())
601 {
602 this.attributesTable.setModel(new AttributesTableModel(node, this.treeTable));
603 }
604 else
605 {
606 this.attributesTable.setModel(new AttributesTableModel(null, this.treeTable));
607 }
608 if (attribute != null)
609 {
610 int index = node.getAttributeIndexByName(attribute);
611 this.attributesTable.setRowSelectionInterval(index, index);
612 }
613 else
614 {
615 this.attributesTable.getSelectionModel().clearSelection();
616 }
617 }
618
619
620
621
622
623 public void setStatusLabel(final String label)
624 {
625 this.statusLabel.setText(label);
626 }
627
628
629
630
631 public void removeStatusLabel()
632 {
633 this.statusLabel.setText(" ");
634 }
635
636
637
638
639 private void addMenuBar()
640 {
641 JMenuBar menuBar = new JMenuBar();
642 setJMenuBar(menuBar);
643
644 JMenu fileMenu = new JMenu("File");
645 menuBar.add(fileMenu);
646 JMenuItem newFile = new JMenuItem("New");
647 newFile.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK));
648 fileMenu.add(newFile);
649 newFile.addActionListener((a) -> newFile());
650 JMenuItem open = new JMenuItem("Open...");
651 open.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK));
652 fileMenu.add(open);
653 open.addActionListener((a) -> openFile());
654 this.recentFilesMenu = new JMenu("Recent files");
655 updateRecentFileMenu();
656 fileMenu.add(this.recentFilesMenu);
657 JMenuItem save = new JMenuItem("Save");
658 save.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK));
659 fileMenu.add(save);
660 save.addActionListener((a) -> saveFile());
661 JMenuItem saveAs = new JMenuItem("Save as...");
662 fileMenu.add(saveAs);
663 saveAs.addActionListener((a) -> saveFileAs((XsdTreeNodeRoot) OtsEditor.this.treeTable.getTree().getModel().getRoot()));
664 fileMenu.add(new JSeparator());
665 JMenuItem properties = new JMenuItem("Properties...");
666 fileMenu.add(properties);
667 properties.addActionListener((a) -> showProperties());
668 fileMenu.add(new JSeparator());
669 JMenuItem exit = new JMenuItem("Exit");
670 fileMenu.add(exit);
671 exit.addActionListener((a) -> exit());
672
673 JMenu editMenu = new JMenu("Edit");
674 menuBar.add(editMenu);
675
676 JMenuItem undoItem = new JMenuItem("Undo");
677 undoItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, ActionEvent.CTRL_MASK));
678 editMenu.add(undoItem);
679 undoItem.addActionListener((a) ->
680 {
681 if (undoItem.isEnabled())
682 {
683 OtsEditor.this.undo.undo();
684 }
685 });
686
687 JMenuItem redoItem = new JMenuItem("Redo");
688 redoItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, ActionEvent.CTRL_MASK));
689 editMenu.add(redoItem);
690 redoItem.addActionListener((a) ->
691 {
692 if (redoItem.isEnabled())
693 {
694 OtsEditor.this.undo.redo();
695 }
696 });
697 this.undo = new Undo(this, undoItem, redoItem);
698
699 JMenu navigateMenu = new JMenu("Navigate");
700 menuBar.add(navigateMenu);
701 this.backItem = new JMenuItem("Go back");
702 this.backItem.setEnabled(false);
703 this.backItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0));
704 navigateMenu.add(this.backItem);
705 this.backItem.addActionListener((a) ->
706 {
707 show(this.backNode.pollLast(), this.backAttribute.pollLast());
708 if (this.backNode.isEmpty())
709 {
710 this.backItem.setText("Go back");
711 this.backItem.setEnabled(false);
712 }
713 else
714 {
715 XsdTreeNode back = this.backNode.peekLast();
716 this.backItem.setText("Go back to " + back.getNodeName() + (back.isIdentifiable() ? " " + back.getId() : ""));
717 this.backItem.setEnabled(true);
718 }
719 });
720 this.coupledItem = new JMenuItem("Go to coupled item");
721 this.coupledItem.setEnabled(false);
722 this.coupledItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F4, 0));
723 navigateMenu.add(this.coupledItem);
724 this.coupledItem.addActionListener((a) ->
725 {
726 if (this.coupledNode != null)
727 {
728 this.backNode.add(this.candidateBackNode);
729 this.backAttribute.add(this.candidateBackAttribute);
730 while (this.backNode.size() > MAX_NAVIGATE)
731 {
732 this.backNode.remove();
733 this.backAttribute.remove();
734 }
735 XsdTreeNode back = OtsEditor.this.backNode.peekLast();
736 this.backItem.setText("Go back to " + back.getNodeName() + (back.isIdentifiable() ? " " + back.getId() : ""));
737 this.backItem.setEnabled(this.backNode.peekLast() != null);
738 show(OtsEditor.this.coupledNode, null);
739 }
740 });
741 }
742
743
744
745
746 private void updateRecentFileMenu()
747 {
748 this.recentFilesMenu.removeAll();
749 List<String> files = this.applicationStore.getRecentFiles("recent_files");
750 if (!files.isEmpty())
751 {
752 for (String file : files)
753 {
754 JMenuItem item = new JMenuItem(file);
755 item.addActionListener((i) ->
756 {
757 if (confirmDiscardChanges())
758 {
759 File f = new File(file);
760 this.lastDirectory = f.getParent() + File.separator;
761 this.lastFile = f.getName();
762 if (!loadFile(f, "File loaded", true))
763 {
764 boolean remove = JOptionPane.showConfirmDialog(OtsEditor.this,
765 "File could not be loaded. Do you want ro remove it from recent files?",
766 "Remove from recent files?", JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE,
767 this.questionIcon) == JOptionPane.OK_OPTION;
768 if (remove)
769 {
770 this.applicationStore.removeRecentFile("recent_files", file);
771 updateRecentFileMenu();
772 }
773 }
774 }
775 });
776 this.recentFilesMenu.add(item);
777 }
778 this.recentFilesMenu.add(new JSeparator());
779 }
780 JMenuItem item = new JMenuItem("Clear history");
781 item.setEnabled(!files.isEmpty());
782 item.addActionListener((i) ->
783 {
784 boolean clear = JOptionPane.showConfirmDialog(OtsEditor.this, "Are you sure you want to clear the recent files?",
785 "Clear recent files?", JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE,
786 this.questionIcon) == JOptionPane.OK_OPTION;
787 if (clear)
788 {
789 this.applicationStore.clearProperty("recent_files");
790 updateRecentFileMenu();
791 }
792 });
793 this.recentFilesMenu.add(item);
794 }
795
796
797
798
799
800
801
802
803 public void setCoupledNode(final XsdTreeNode coupledNode, final XsdTreeNode backNode, final String backAttribute)
804 {
805 if (coupledNode == null)
806 {
807 this.coupledItem.setEnabled(false);
808 this.coupledItem.setText("Go to coupled item");
809 }
810 else
811 {
812 this.coupledItem.setEnabled(true);
813 this.coupledItem.setText("Go to " + (backAttribute != null ? backNode.getAttributeValue(backAttribute)
814 : (backNode.isIdentifiable() ? backNode.getId() : backNode.getValue())));
815 }
816 this.coupledNode = coupledNode;
817 this.candidateBackNode = backNode;
818 this.candidateBackAttribute = backAttribute;
819 }
820
821
822
823
824
825 public void setUnsavedChanges(final boolean unsavedChanges)
826 {
827 this.unsavedChanges = unsavedChanges;
828 StringBuilder title = new StringBuilder("OTS | The Open Traffic Simulator | Editor");
829 if (this.lastFile != null)
830 {
831 title.append(" (").append(this.lastDirectory).append(this.lastFile).append(")");
832 }
833 if (this.unsavedChanges)
834 {
835 title.append(" *");
836 }
837 setTitle(title.toString());
838 }
839
840
841
842
843
844
845 @SuppressWarnings("checkstyle:hiddenfield")
846 public void setSchema(final Document xsdDocument) throws IOException
847 {
848 this.xsdDocument = xsdDocument;
849 this.undo.setIgnoreChanges(true);
850 initializeTree();
851 this.undo.clear();
852 setStatusLabel("Schema " + xsdDocument.getBaseURI() + " loaded");
853
854 setVisible(true);
855 this.leftRightSplitPane.setDividerLocation(0.65);
856 this.rightSplitPane.setDividerLocation(0.75);
857 setAppearance(getAppearance());
858
859 SwingUtilities.invokeLater(() -> checkAutosave());
860 }
861
862
863
864
865 private void checkAutosave()
866 {
867 Path tmpPath = Paths.get(System.getProperty("java.io.tmpdir") + "ots" + File.separator);
868 File tmpDir = tmpPath.toFile();
869 if (!tmpDir.exists())
870 {
871 tmpDir.mkdir();
872 }
873 Iterator<Path> it;
874 try
875 {
876 it = Files.newDirectoryStream(tmpPath, "autosave*.xml").iterator();
877 }
878 catch (IOException ioe)
879 {
880
881 return;
882 }
883 if (it.hasNext())
884 {
885 File file = it.next().toFile();
886 int userInput = JOptionPane.showConfirmDialog(this,
887 "Autosave file " + file.getName() + " (" + new Date(file.lastModified())
888 + ") detected. Do you want to load this file? ('No' removes the file)",
889 "Autosave file detected", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE,
890 this.questionIcon);
891 if (userInput == JOptionPane.OK_OPTION)
892 {
893 boolean loaded = loadFile(file, "Autosave file loaded", false);
894 if (!loaded)
895 {
896 boolean remove = JOptionPane.showConfirmDialog(OtsEditor.this,
897 "File could not be loaded. Do you want ro remove it from recent files?",
898 "Remove from recent files?", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE,
899 this.questionIcon) == JOptionPane.YES_OPTION;
900 if (remove)
901 {
902 file.delete();
903 }
904 }
905 setUnsavedChanges(true);
906 this.treeTable.updateUI();
907 file.delete();
908 }
909 else if (userInput == JOptionPane.NO_OPTION)
910 {
911 file.delete();
912 }
913 }
914 }
915
916
917
918
919 private void newFile()
920 {
921 if (confirmDiscardChanges())
922 {
923 try
924 {
925 XsdTreeNode node = (XsdTreeNode) this.treeTable.getTree().getModel().getRoot();
926 this.undo.startAction(ActionType.ADD, node, null);
927 initializeTree();
928 this.attributesTable.setModel(new AttributesTableModel(null, this.treeTable));
929 this.undo.clear();
930 }
931 catch (IOException exception)
932 {
933 JOptionPane.showMessageDialog(this, "Unable to reload schema.", "Unable to reload schema.",
934 JOptionPane.WARNING_MESSAGE);
935 }
936 }
937 }
938
939
940
941
942
943 private void initializeTree() throws IOException
944 {
945 this.scenario.removeAllItems();
946 this.scenario.addItem(new ScenarioWrapper(null));
947 setDefaultProperties();
948
949
950 XsdTreeTableModel treeModel = new XsdTreeTableModel(this.xsdDocument);
951 this.treeTable = new AppearanceControlTreeTable(treeModel);
952 this.treeTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
953 this.nodeActions = new NodeActions(this, this.treeTable);
954 this.treeTable.putClientProperty("terminateEditOnFocusLost", true);
955 treeModel.setTreeTable(this.treeTable);
956 this.treeTable.setDefaultRenderer(String.class, new StringCellRenderer(this.treeTable));
957 ((DefaultCellEditor) this.treeTable.getDefaultEditor(String.class)).setClickCountToStart(1);
958 XsdTreeTableModel.applyColumnWidth(this.treeTable);
959
960 addTreeTableListeners();
961
962 int dividerLocation = this.rightSplitPane.getDividerLocation();
963 this.rightSplitPane.setTopComponent(null);
964 this.rightSplitPane.setTopComponent(new JScrollPane(this.treeTable));
965 this.rightSplitPane.setDividerLocation(dividerLocation);
966
967 XsdTreeNodeRoot root = (XsdTreeNodeRoot) treeModel.getRoot();
968 EventListener listener = new ChangesListener(this, this.scenario);
969 root.addListener(listener, XsdTreeNodeRoot.NODE_CREATED);
970 root.addListener(listener, XsdTreeNodeRoot.NODE_REMOVED);
971 fireEvent(NEW_FILE, root);
972
973 setUnsavedChanges(false);
974 if (this.autosave != null)
975 {
976 this.autosave.cancel();
977 }
978 this.autosave = new TimerTask()
979 {
980 @Override
981 public void run()
982 {
983 if (OtsEditor.this.unsavedChanges)
984 {
985 setStatusLabel("Autosaving...");
986 File file = new File(System.getProperty("java.io.tmpdir") + "ots" + File.separator
987 + (OtsEditor.this.lastFile == null ? "autosave.xml" : "autosave_" + OtsEditor.this.lastFile));
988 save(file, root, false);
989 file.deleteOnExit();
990 setStatusLabel("Autosaved");
991 }
992 }
993 };
994 new Timer().scheduleAtFixedRate(this.autosave, AUTOSAVE_PERIOD_MS, AUTOSAVE_PERIOD_MS);
995 setAppearance(getAppearance());
996 }
997
998
999
1000
1001 private void setDefaultProperties()
1002 {
1003 this.properties.clear();
1004 this.properties.add("xmlns:ots");
1005 this.properties.add("http://www.opentrafficsim.org/ots");
1006 this.properties.add("xmlns:xi");
1007 this.properties.add("http://www.w3.org/2001/XInclude");
1008 this.properties.add("xmlns:xsi");
1009 this.properties.add("http://www.w3.org/2001/XMLSchema-instance");
1010 this.properties.add("xsi:schemaLocation");
1011 this.properties.add(null);
1012 }
1013
1014
1015
1016
1017
1018 private void addTreeTableListeners() throws IOException
1019 {
1020
1021 DefaultCellEditor editor = (DefaultCellEditor) this.treeTable.getDefaultEditor(String.class);
1022 ((JTextField) editor.getComponent()).addKeyListener(new KeyAdapter()
1023 {
1024 @Override
1025 public void keyReleased(final KeyEvent e)
1026 {
1027 int editorCol = OtsEditor.this.treeTable.convertColumnIndexToView(OtsEditor.this.treeTable.getSelectedColumn());
1028 if (editorCol == 1 || editorCol == 2)
1029 {
1030 int row = OtsEditor.this.treeTable.getSelectedRow();
1031 int col = OtsEditor.this.treeTable.convertColumnIndexToView(0);
1032 XsdTreeNode treeNode = (XsdTreeNode) OtsEditor.this.treeTable.getValueAt(row, col);
1033 if (editorCol == 1)
1034 {
1035 treeNode.setId(((JTextField) e.getComponent()).getText());
1036 }
1037 else if (editorCol == 2)
1038 {
1039 treeNode.setValue(((JTextField) e.getComponent()).getText());
1040 }
1041 }
1042 }
1043 });
1044
1045
1046 ((JTextField) editor.getComponent()).addFocusListener(new FocusListener()
1047 {
1048 @Override
1049 public void focusGained(final FocusEvent e)
1050 {
1051 startUndoActionOnTreeTable();
1052 }
1053
1054 @Override
1055 public void focusLost(final FocusEvent e)
1056 {
1057 startUndoActionOnTreeTable();
1058 }
1059 });
1060
1061
1062 editor.addCellEditorListener(new CellEditorListener()
1063 {
1064 @Override
1065 public void editingStopped(final ChangeEvent e)
1066 {
1067 startUndoActionOnTreeTable();
1068 }
1069
1070 @Override
1071 public void editingCanceled(final ChangeEvent e)
1072 {
1073 startUndoActionOnTreeTable();
1074 }
1075 });
1076
1077
1078 this.treeTable.getTree()
1079 .addTreeSelectionListener(new XsdTreeSelectionListener(this, this.treeTable, this.attributesTable));
1080
1081
1082 this.treeTable.getTree().setCellRenderer(new XsdTreeCellRenderer(this));
1083
1084
1085 this.treeTable.getTree().addTreeWillExpandListener(new TreeWillExpandListener()
1086 {
1087 @Override
1088 public void treeWillExpand(final TreeExpansionEvent event) throws ExpandVetoException
1089 {
1090 OtsEditor.this.mayPresentChoice = false;
1091 }
1092
1093 @Override
1094 public void treeWillCollapse(final TreeExpansionEvent event) throws ExpandVetoException
1095 {
1096 OtsEditor.this.mayPresentChoice = false;
1097 }
1098 });
1099
1100
1101
1102 this.treeTable.addMouseMotionListener(new MouseMotionAdapter()
1103 {
1104 @Override
1105 public void mouseMoved(final MouseEvent e)
1106 {
1107 OtsEditor.this.mayPresentChoice = true;
1108
1109
1110 int row = OtsEditor.this.treeTable.rowAtPoint(e.getPoint());
1111 int col = OtsEditor.this.treeTable.convertColumnIndexToView(0);
1112 XsdTreeNode treeNode = (XsdTreeNode) OtsEditor.this.treeTable.getValueAt(row, col);
1113 try
1114 {
1115 if (!treeNode.isSelfValid())
1116 {
1117 OtsEditor.this.treeTable.getTree().setToolTipText(treeNode.reportInvalidNode());
1118 }
1119 else
1120 {
1121 OtsEditor.this.treeTable.getTree().setToolTipText(null);
1122 }
1123 }
1124 catch (Exception ex)
1125 {
1126 if (treeNode.isIdentifiable())
1127 {
1128 System.out.println("Node " + treeNode.getId() + " no valid.");
1129 }
1130 else
1131 {
1132 System.out.println("Node " + treeNode.getNodeName() + " no valid.");
1133 }
1134 }
1135 }
1136 });
1137
1138
1139 this.treeTable.addMouseListener(new XsdTreeMouseListener(this, this.treeTable, this.attributesTable));
1140
1141
1142 this.treeTable.addKeyListener(new XsdTreeKeyListener(this, this.treeTable));
1143 }
1144
1145
1146
1147
1148 public void startUndoActionOnTreeTable()
1149 {
1150 XsdTreeNode node = (XsdTreeNode) this.treeTable.getValueAt(this.treeTable.getSelectedRow(), 0);
1151 int col = this.treeTable.convertColumnIndexToView(this.treeTable.getSelectedColumn());
1152 if (col == 1)
1153 {
1154 this.undo.startAction(ActionType.ID_CHANGE, node, null);
1155 }
1156 else if (col == 2)
1157 {
1158 this.undo.startAction(ActionType.VALUE_CHANGE, node, null);
1159 }
1160 }
1161
1162
1163
1164
1165
1166
1167
1168 public void preparePopupRemoval(final JPopupMenu popup, final JComponent component)
1169 {
1170 popup.addPopupMenuListener(new PopupMenuListener()
1171 {
1172 @Override
1173 public void popupMenuWillBecomeVisible(final PopupMenuEvent e)
1174 {
1175 }
1176
1177 @Override
1178 public void popupMenuWillBecomeInvisible(final PopupMenuEvent e)
1179 {
1180 component.setComponentPopupMenu(null);
1181 OtsEditor.this.choiceNode = null;
1182 }
1183
1184 @Override
1185 public void popupMenuCanceled(final PopupMenuEvent e)
1186 {
1187 }
1188 });
1189 }
1190
1191
1192
1193
1194
1195
1196
1197 public void setCustomIcon(final String path, final ImageIcon icon)
1198 {
1199 this.customIcons.put(path, icon);
1200 }
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212 public static ImageIcon loadIcon(final String image, final int width, final int height, final int bgWidth,
1213 final int bgHeight) throws IOException
1214 {
1215 Image im = ImageIO.read(Resource.getResourceAsStream(image));
1216 if (width > 0 || height > 0)
1217 {
1218 im = im.getScaledInstance(width > 0 ? width : im.getWidth(null), height > 0 ? height : im.getHeight(null),
1219 Image.SCALE_SMOOTH);
1220 }
1221 if (bgWidth > 0 && bgHeight > 0)
1222 {
1223 BufferedImage bg = new BufferedImage(bgWidth > 0 ? bgWidth : im.getWidth(null),
1224 bgHeight > 0 ? bgHeight : im.getHeight(null), BufferedImage.TYPE_INT_ARGB);
1225 Graphics g = bg.getGraphics();
1226 g.drawImage(im, (bg.getWidth() - im.getWidth(null)) / 2, (bg.getHeight() - im.getHeight(null)) / 2, null);
1227 im = bg;
1228 }
1229 return new ImageIcon(im);
1230 }
1231
1232
1233
1234
1235
1236
1237 public Icon getCustomIcon(final String path)
1238 {
1239 Icon icon = this.customIcons.get(path);
1240 if (icon != null)
1241 {
1242 return icon;
1243 }
1244 for (Entry<String, Icon> entry : this.customIcons.entrySet())
1245 {
1246 if (path.endsWith(entry.getKey()))
1247 {
1248 return entry.getValue();
1249 }
1250 }
1251 return null;
1252 }
1253
1254
1255
1256
1257
1258 public XsdTreeNode getChoiceNode()
1259 {
1260 return this.choiceNode;
1261 }
1262
1263
1264
1265
1266
1267 public void setChoiceNode(final XsdTreeNode choiceNode)
1268 {
1269 this.choiceNode = choiceNode;
1270 }
1271
1272
1273
1274
1275
1276 public boolean mayPresentChoice()
1277 {
1278 return this.mayPresentChoice;
1279 }
1280
1281 @Override
1282 public EventListenerMap getEventListenerMap() throws RemoteException
1283 {
1284 return this.listenerMap;
1285 }
1286
1287
1288
1289
1290
1291
1292
1293
1294 public void addTab(final String name, final Icon icon, final Component component, final String tip)
1295 {
1296 this.visualizationPane.addTab(name, icon, component, tip);
1297 }
1298
1299
1300
1301
1302
1303
1304 public Component getTab(final String name)
1305 {
1306 for (int index = 0; index < this.visualizationPane.getTabCount(); index++)
1307 {
1308 if (this.visualizationPane.getTitleAt(index).equals(name))
1309 {
1310 return this.visualizationPane.getComponentAt(index);
1311 }
1312 }
1313 return null;
1314 }
1315
1316
1317
1318
1319
1320 public void focusTab(final String name)
1321 {
1322 for (int index = 0; index < this.visualizationPane.getTabCount(); index++)
1323 {
1324 if (this.visualizationPane.getTitleAt(index).equals(name))
1325 {
1326 this.visualizationPane.setSelectedIndex(index);
1327 }
1328 }
1329 }
1330
1331
1332
1333
1334
1335
1336
1337
1338 public boolean confirmNodeRemoval(final XsdTreeNode node)
1339 {
1340 return JOptionPane.showConfirmDialog(this, "Remove `" + node + "`?", "Remove?", JOptionPane.OK_CANCEL_OPTION,
1341 JOptionPane.QUESTION_MESSAGE, this.questionIcon) == JOptionPane.OK_OPTION;
1342 }
1343
1344
1345
1346
1347
1348 private boolean confirmDiscardChanges()
1349 {
1350 if (!this.unsavedChanges)
1351 {
1352 return true;
1353 }
1354 return JOptionPane.showConfirmDialog(this, "Discard unsaved changes?", "Discard unsaved changes?",
1355 JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, this.questionIcon) == JOptionPane.OK_OPTION;
1356 }
1357
1358
1359
1360
1361
1362 public void showDescription(final String description)
1363 {
1364 JOptionPane.showMessageDialog(OtsEditor.this,
1365 "<html><body><p style='width: 400px;'>" + description + "</p></body></html>");
1366 }
1367
1368
1369
1370
1371 public void showInvalidMessage()
1372 {
1373 JOptionPane.showMessageDialog(OtsEditor.this, "The setup is not valid. Make sure no red nodes remain.",
1374 "Setup is not valid", JOptionPane.INFORMATION_MESSAGE);
1375 }
1376
1377
1378
1379
1380 public void showCircularInputParameters()
1381 {
1382 JOptionPane.showMessageDialog(OtsEditor.this, "Input parameters have a circular dependency.",
1383 "Circular input parameter", JOptionPane.INFORMATION_MESSAGE);
1384 }
1385
1386
1387
1388
1389 public void showUnableToRun()
1390 {
1391 JOptionPane.showMessageDialog(OtsEditor.this, "Unable to run, temporary file could not be saved.", "Unable to run",
1392 JOptionPane.INFORMATION_MESSAGE);
1393 }
1394
1395
1396
1397
1398
1399
1400
1401
1402 public void optionsPopup(final List<String> allOptions, final JTable table, final Consumer<String> action)
1403 {
1404
1405 List<String> options = filterOptions(allOptions, "");
1406 OtsEditor.this.dropdownOptions = options;
1407 if (options.isEmpty())
1408 {
1409 return;
1410 }
1411 JPopupMenu popup = new JPopupMenu();
1412 int index = 0;
1413 for (String option : options)
1414 {
1415 JMenuItem item = new JMenuItem(option);
1416 item.setVisible(index++ < MAX_DROPDOWN_ITEMS);
1417 item.addActionListener(new ActionListener()
1418 {
1419 @Override
1420 public void actionPerformed(final ActionEvent e)
1421 {
1422 action.accept(option);
1423 CellEditor cellEditor = table.getCellEditor();
1424 if (cellEditor != null)
1425 {
1426 cellEditor.cancelCellEditing();
1427 }
1428 OtsEditor.this.treeTable.updateUI();
1429 }
1430 });
1431 item.setFont(table.getFont());
1432 popup.add(item);
1433 }
1434 this.dropdownIndent = 0;
1435 popup.addMouseWheelListener(new MouseWheelListener()
1436 {
1437
1438 @Override
1439 public void mouseWheelMoved(final MouseWheelEvent e)
1440 {
1441 OtsEditor.this.dropdownIndent += (e.getWheelRotation() * e.getScrollAmount());
1442 OtsEditor.this.dropdownIndent = OtsEditor.this.dropdownIndent < 0 ? 0 : OtsEditor.this.dropdownIndent;
1443 int maxIndent = OtsEditor.this.dropdownOptions.size() - MAX_DROPDOWN_ITEMS;
1444 if (maxIndent > 0)
1445 {
1446 OtsEditor.this.dropdownIndent =
1447 OtsEditor.this.dropdownIndent > maxIndent ? maxIndent : OtsEditor.this.dropdownIndent;
1448 showOptionsInScope(popup);
1449 }
1450 }
1451 });
1452 preparePopupRemoval(popup, table);
1453
1454 SwingUtilities.invokeLater(new Runnable()
1455 {
1456 @Override
1457 public void run()
1458 {
1459 JTextField field = (JTextField) ((DefaultCellEditor) table.getDefaultEditor(String.class)).getComponent();
1460 table.setComponentPopupMenu(popup);
1461 popup.pack();
1462 popup.setInvoker(table);
1463 popup.setVisible(true);
1464 field.requestFocus();
1465 Rectangle rectangle = field.getBounds();
1466 placePopup(popup, rectangle, table);
1467 field.addKeyListener(new KeyAdapter()
1468 {
1469 @Override
1470 public void keyTyped(final KeyEvent e)
1471 {
1472
1473 SwingUtilities.invokeLater(new Runnable()
1474 {
1475 @Override
1476 public void run()
1477 {
1478 OtsEditor.this.dropdownIndent = 0;
1479 String currentValue = field.getText();
1480 OtsEditor.this.dropdownOptions = filterOptions(allOptions, currentValue);
1481 boolean anyVisible = showOptionsInScope(popup);
1482
1483
1484 if (!anyVisible)
1485 {
1486 JMenuItem item = new JMenuItem(currentValue);
1487 item.addActionListener(new ActionListener()
1488 {
1489 @Override
1490 public void actionPerformed(final ActionEvent e)
1491 {
1492 action.accept(currentValue);
1493 CellEditor cellEditor = table.getCellEditor();
1494 if (cellEditor != null)
1495 {
1496 cellEditor.cancelCellEditing();
1497 }
1498 OtsEditor.this.treeTable.updateUI();
1499 }
1500 });
1501 item.setFont(table.getFont());
1502 popup.add(item);
1503 }
1504 popup.pack();
1505 placePopup(popup, rectangle, table);
1506 }
1507 });
1508 }
1509 });
1510 field.addActionListener(new ActionListener()
1511 {
1512 @Override
1513 public void actionPerformed(final ActionEvent e)
1514 {
1515 popup.setVisible(false);
1516 table.setComponentPopupMenu(null);
1517 }
1518 });
1519 }
1520 });
1521 }
1522
1523
1524
1525
1526
1527
1528 private boolean showOptionsInScope(final JPopupMenu popup)
1529 {
1530 int optionIndex = 0;
1531 for (Component component : popup.getComponents())
1532 {
1533 JMenuItem item = (JMenuItem) component;
1534 boolean visible =
1535 optionIndex < MAX_DROPDOWN_ITEMS && this.dropdownOptions.indexOf(item.getText()) >= this.dropdownIndent;
1536 item.setVisible(visible);
1537 if (visible)
1538 {
1539 optionIndex++;
1540 }
1541 }
1542 popup.pack();
1543 return optionIndex > 0;
1544 }
1545
1546
1547
1548
1549
1550
1551
1552 private void placePopup(final JPopupMenu popup, final Rectangle rectangle, final JComponent parent)
1553 {
1554 Point pAttributes = parent.getLocationOnScreen();
1555
1556 Dimension windowSize = OtsEditor.this.getSize();
1557 Point pWindow = OtsEditor.this.getLocationOnScreen();
1558 if (pAttributes.y + (int) rectangle.getMaxY() + popup.getBounds().getHeight() > windowSize.height + pWindow.y - 1)
1559 {
1560 popup.setLocation(pAttributes.x + (int) rectangle.getMinX(),
1561 pAttributes.y + (int) rectangle.getMinY() - 1 - (int) popup.getBounds().getHeight());
1562 }
1563 else
1564 {
1565 popup.setLocation(pAttributes.x + (int) rectangle.getMinX(), pAttributes.y + (int) rectangle.getMaxY() - 1);
1566 }
1567 }
1568
1569
1570
1571
1572 void openFile()
1573 {
1574 if (!confirmDiscardChanges())
1575 {
1576 return;
1577 }
1578 FileDialog fileDialog = new FileDialog(this, "Open XML", FileDialog.LOAD);
1579 fileDialog.setFilenameFilter(new FilenameFilter()
1580 {
1581 @Override
1582 public boolean accept(final File dir, final String name)
1583 {
1584 return name.toLowerCase().endsWith(".xml");
1585 }
1586 });
1587 fileDialog.setVisible(true);
1588 String fileName = fileDialog.getFile();
1589 if (fileName == null)
1590 {
1591 return;
1592 }
1593 if (!fileName.toLowerCase().endsWith(".xml"))
1594 {
1595 return;
1596 }
1597 this.lastDirectory = fileDialog.getDirectory();
1598 this.lastFile = fileName;
1599 File file = new File(this.lastDirectory + this.lastFile);
1600 boolean loaded = loadFile(file, "File loaded", true);
1601 if (!loaded)
1602 {
1603 JOptionPane.showMessageDialog(this, "Unable to read file.", "Unable to read file.", JOptionPane.WARNING_MESSAGE);
1604 }
1605 }
1606
1607
1608
1609
1610
1611
1612
1613
1614 private boolean loadFile(final File file, final String status, final boolean updateRecentFiles)
1615 {
1616 try
1617 {
1618 Document document = DocumentReader.open(file.toURI());
1619 this.undo.setIgnoreChanges(true);
1620 initializeTree();
1621 NamedNodeMap attributes = document.getFirstChild().getAttributes();
1622 for (int i = 0; i < attributes.getLength(); i++)
1623 {
1624 if (this.properties.contains(attributes.item(i).getNodeName()))
1625 {
1626 int index = this.properties.indexOf(attributes.item(i).getNodeName());
1627 this.properties.set(index, attributes.item(i).getNodeName());
1628 this.properties.set(index + 1, attributes.item(i).getNodeValue());
1629 }
1630 else
1631 {
1632 this.properties.add(attributes.item(i).getNodeName());
1633 this.properties.add(attributes.item(i).getNodeValue());
1634 }
1635 }
1636 XsdTreeNodeRoot root = (XsdTreeNodeRoot) OtsEditor.this.treeTable.getTree().getModel().getRoot();
1637 root.setDirectory(this.lastDirectory);
1638 root.loadXmlNodes(document.getFirstChild());
1639 this.undo.clear();
1640 setUnsavedChanges(false);
1641 setStatusLabel(status);
1642 this.undo.updateButtons();
1643 this.backItem.setEnabled(false);
1644 this.coupledItem.setEnabled(false);
1645 this.coupledItem.setText("Go to coupled item");
1646 this.treeTable.updateUI();
1647 if (updateRecentFiles)
1648 {
1649 this.applicationStore.addRecentFile("recent_files", file.getAbsolutePath());
1650 updateRecentFileMenu();
1651 }
1652 return true;
1653 }
1654 catch (SAXException | IOException | ParserConfigurationException exception)
1655 {
1656 return false;
1657 }
1658 }
1659
1660
1661
1662
1663 private void saveFile()
1664 {
1665 XsdTreeNodeRoot root = (XsdTreeNodeRoot) OtsEditor.this.treeTable.getTree().getModel().getRoot();
1666 if (this.lastFile == null)
1667 {
1668 saveFileAs(root);
1669 return;
1670 }
1671 save(new File(this.lastDirectory + this.lastFile), root, true);
1672 setUnsavedChanges(false);
1673 setStatusLabel("Saved");
1674 }
1675
1676
1677
1678
1679
1680 public void saveFileAs(final XsdTreeNode root)
1681 {
1682 FileDialog fileDialog = new FileDialog(this, "Save XML", FileDialog.SAVE);
1683 fileDialog.setFile("*.xml");
1684 fileDialog.setVisible(true);
1685 String fileName = fileDialog.getFile();
1686 if (fileName == null)
1687 {
1688 return;
1689 }
1690 if (!fileName.toLowerCase().endsWith(".xml"))
1691 {
1692 fileName = fileName + ".xml";
1693 }
1694 if (root instanceof XsdTreeNodeRoot)
1695 {
1696 this.lastDirectory = fileDialog.getDirectory();
1697 this.lastFile = fileName;
1698 }
1699 save(new File(fileDialog.getDirectory() + fileName), root, true);
1700 if (root instanceof XsdTreeNodeRoot)
1701 {
1702 this.undo.setIgnoreChanges(true);
1703 ((XsdTreeNodeRoot) root).setDirectory(this.lastDirectory);
1704 this.treeTable.updateUI();
1705 this.attributesTable.updateUI();
1706 setUnsavedChanges(false);
1707 this.undo.setIgnoreChanges(false);
1708 }
1709 setStatusLabel("Saved");
1710 }
1711
1712
1713
1714
1715
1716
1717
1718 private void save(final File file, final XsdTreeNode root, final boolean storeAsRecent)
1719 {
1720 try (FileOutputStream fileOutputStream = new FileOutputStream(file))
1721 {
1722 DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
1723 Document document = docBuilder.newDocument();
1724
1725
1726
1727
1728
1729
1730 document.setXmlStandalone(true);
1731 root.saveXmlNodes(document, document);
1732 Element xmlRoot = (Element) document.getChildNodes().item(0);
1733 Set<String> nameSpaces = new LinkedHashSet<>();
1734 nameSpaces.add("xmlns");
1735 for (int i = 0; i < this.properties.size(); i = i + 2)
1736 {
1737 String prop = this.properties.get(i);
1738 String value = this.properties.get(i + 1);
1739 if (prop.startsWith("xmlns") && value != null && !value.isBlank())
1740 {
1741 nameSpaces.add(prop.substring(6));
1742 }
1743 }
1744 for (int i = 0; i < this.properties.size(); i = i + 2)
1745 {
1746 String prop = this.properties.get(i);
1747 String value = this.properties.get(i + 1);
1748 int semi = prop.indexOf(":");
1749 String nameSpace = semi < 0 ? null : prop.substring(0, semi);
1750 if (!nameSpaces.contains(nameSpace) && value != null && !value.isBlank())
1751 {
1752 JOptionPane.showMessageDialog(this,
1753 "Unable to save property " + prop + " as its namespace xmlns:" + nameSpace + " is not provided.",
1754 "Unable to save property.", JOptionPane.WARNING_MESSAGE);
1755 }
1756 else if (value != null && !value.isBlank())
1757 {
1758 xmlRoot.setAttribute(prop, value);
1759 }
1760 }
1761 StreamResult result = new StreamResult(fileOutputStream);
1762
1763 Transformer transformer = TransformerFactory.newInstance().newTransformer();
1764 transformer.setOutputProperty(OutputKeys.INDENT, "yes");
1765 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
1766 transformer.transform(new DOMSource(document), result);
1767
1768 fileOutputStream.close();
1769
1770 String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
1771 content = content.replace("<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
1772 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + System.lineSeparator());
1773 Files.write(file.toPath(), content.getBytes(StandardCharsets.UTF_8));
1774
1775 if (storeAsRecent)
1776 {
1777 this.applicationStore.addRecentFile("recent_files", file.getAbsolutePath());
1778 updateRecentFileMenu();
1779 }
1780 }
1781 catch (ParserConfigurationException | TransformerException | IOException exception)
1782 {
1783 JOptionPane.showMessageDialog(this, "Unable to save file.", "Unable to save file.", JOptionPane.WARNING_MESSAGE);
1784 }
1785 }
1786
1787
1788
1789
1790 private void showProperties()
1791 {
1792
1793 JPanel panel = new JPanel();
1794 panel.setLayout(new BorderLayout());
1795
1796 TableColumnModel columns = new DefaultTableColumnModel();
1797 TableColumn column1 = new TableColumn(0, 200);
1798 column1.setHeaderValue("Property");
1799 columns.addColumn(column1);
1800 TableColumn column2 = new TableColumn(1, 600);
1801 column2.setHeaderValue("Value");
1802 columns.addColumn(column2);
1803
1804 TableModel model = new AbstractTableModel()
1805 {
1806
1807 private static final long serialVersionUID = 20240314L;
1808
1809 @Override
1810 public int getRowCount()
1811 {
1812 return OtsEditor.this.properties.size() / 2;
1813 }
1814
1815 @Override
1816 public int getColumnCount()
1817 {
1818 return 2;
1819 }
1820
1821 @Override
1822 public Object getValueAt(final int rowIndex, final int columnIndex)
1823 {
1824 return OtsEditor.this.properties.get(rowIndex * 2 + columnIndex);
1825 }
1826
1827 @Override
1828 public boolean isCellEditable(final int rowIndex, final int columnIndex)
1829 {
1830 return columnIndex == 1;
1831 }
1832
1833 @Override
1834 public void setValueAt(final Object aValue, final int rowIndex, final int columnIndex)
1835 {
1836 OtsEditor.this.properties.set(rowIndex * 2 + columnIndex, aValue.toString());
1837 OtsEditor.this.setUnsavedChanges(true);
1838 }
1839 };
1840
1841 JTable table = new JTable(model, columns);
1842 DefaultTableCellRenderer renderer = new DefaultTableCellRenderer()
1843 {
1844
1845 private static final long serialVersionUID = 20240314L;
1846
1847 @Override
1848 public Component getTableCellRendererComponent(final JTable table, final Object value, final boolean isSelected,
1849 final boolean hasFocus, final int row, final int column)
1850 {
1851 Component component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
1852 if (table.convertColumnIndexToModel(column) == 0)
1853 {
1854 this.setOpaque(true);
1855 }
1856 else
1857 {
1858 this.setOpaque(false);
1859 }
1860 return component;
1861 }
1862 };
1863 renderer.setBackground(panel.getBackground());
1864 table.setDefaultRenderer(Object.class, renderer);
1865 JScrollPane scroll = new JScrollPane(table);
1866 scroll.setBorder(new EmptyBorder(0, 0, 0, 0));
1867 panel.add(scroll, BorderLayout.CENTER);
1868
1869 final JDialog propertyWindow = new JDialog(this, "Properties", true);
1870 propertyWindow.setMinimumSize(new Dimension(250, 150));
1871 propertyWindow.setPreferredSize(new Dimension(800, 200));
1872
1873 JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT));
1874 JButton defaults = new JButton("Set defaults...");
1875 defaults.addActionListener((a) ->
1876 {
1877 boolean set = JOptionPane.showConfirmDialog(propertyWindow, "Are you sure? This will reset all properties.",
1878 "Are you sure?", JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE,
1879 this.questionIcon) == JOptionPane.OK_OPTION;
1880 if (set)
1881 {
1882 setDefaultProperties();
1883 table.updateUI();
1884 OtsEditor.this.setUnsavedChanges(true);
1885 }
1886 });
1887 buttons.add(defaults);
1888 JButton ok = new JButton("Ok");
1889 ok.addActionListener((a) ->
1890 {
1891 table.getDefaultEditor(Object.class).stopCellEditing();
1892 propertyWindow.dispose();
1893 });
1894 buttons.add(ok);
1895 panel.add(buttons, BorderLayout.PAGE_END);
1896
1897 propertyWindow.getContentPane().add(panel);
1898 propertyWindow.pack();
1899 propertyWindow.setLocationRelativeTo(this);
1900 propertyWindow.setVisible(true);
1901 }
1902
1903
1904
1905
1906 private void exit()
1907 {
1908 if (confirmDiscardChanges())
1909 {
1910 System.exit(0);
1911 }
1912 }
1913
1914
1915
1916
1917
1918
1919
1920 public static String limitTooltip(final String message)
1921 {
1922 if (message == null)
1923 {
1924 return null;
1925 }
1926 if (message.length() < MAX_TOOLTIP_LENGTH)
1927 {
1928 return message;
1929 }
1930 return message.substring(0, MAX_TOOLTIP_LENGTH - 3) + "...";
1931 }
1932
1933
1934
1935
1936
1937
1938
1939 private static List<String> filterOptions(final List<String> options, final String currentValue)
1940 {
1941 return options.stream().filter((val) -> currentValue == null || currentValue.isEmpty() || val.startsWith(currentValue))
1942 .distinct().sorted().collect(Collectors.toList());
1943 }
1944
1945
1946
1947
1948
1949 public void addAttributeCellEditorListener(final CellEditorListener listener)
1950 {
1951 this.attributesTable.getDefaultEditor(String.class).addCellEditorListener(listener);
1952 }
1953
1954
1955
1956
1957
1958
1959 public void setClipboard(final XsdTreeNode clipboard, final boolean cut)
1960 {
1961 this.clipboard = clipboard;
1962 this.cut = cut;
1963 }
1964
1965
1966
1967
1968
1969 public XsdTreeNode getClipboard()
1970 {
1971 return this.clipboard;
1972 }
1973
1974
1975
1976
1977 public void removeClipboardWhenCut()
1978 {
1979 if (this.clipboard != null && this.cut)
1980 {
1981 if (this.clipboard.isRemovable())
1982 {
1983 this.clipboard.remove();
1984 }
1985 this.clipboard = null;
1986 }
1987 }
1988
1989
1990
1991
1992
1993 public NodeActions getNodeActions()
1994 {
1995 return this.nodeActions;
1996 }
1997
1998 @Override
1999 public boolean addListener(final EventListener listener, final EventType eventType)
2000 {
2001 return Try.assign(() -> EventProducer.super.addListener(listener, eventType),
2002 "Local event producer should not give a RemoteException.");
2003 }
2004
2005
2006
2007
2008
2009
2010 public Eval getEval()
2011 {
2012 try
2013 {
2014 return this.evalWrapper.getEval(OtsEditor.this.scenario.getItemAt(OtsEditor.this.scenario.getSelectedIndex()));
2015 }
2016 catch (CircularDependencyException ex)
2017 {
2018 showCircularInputParameters();
2019 return this.evalWrapper.getLastValidEval();
2020 }
2021 catch (RuntimeException ex)
2022 {
2023
2024 return this.evalWrapper.getLastValidEval();
2025 }
2026 }
2027
2028
2029
2030
2031
2032 public void addEvalListener(final EvalListener listener)
2033 {
2034 this.evalWrapper.addListener(listener);
2035 }
2036
2037
2038
2039
2040
2041 public void removeEvalListener(final EvalListener listener)
2042 {
2043 this.evalWrapper.removeListener(listener);
2044 }
2045
2046 @Override
2047 public void setAppearance(final Appearance appearance)
2048 {
2049 super.setAppearance(appearance);
2050
2051 if (this.treeTable != null)
2052 {
2053 changeFont((Component) this.treeTable.getTree().getCellRenderer(), appearance.getFont());
2054 changeFontSize((Component) this.treeTable.getTree().getCellRenderer());
2055 }
2056 }
2057
2058 }