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
315 @Override
316 public void windowClosing(final WindowEvent e)
317 {
318 exit();
319 }
320 });
321
322
323 this.leftRightSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, UPDATE_SPLIT_WHILE_DRAGGING);
324 this.leftRightSplitPane.setDividerSize(DIVIDER_SIZE);
325 this.leftRightSplitPane.setResizeWeight(0.5);
326 add(this.leftRightSplitPane);
327 this.rightSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, UPDATE_SPLIT_WHILE_DRAGGING);
328 this.rightSplitPane.setDividerSize(DIVIDER_SIZE);
329 this.rightSplitPane.setResizeWeight(0.5);
330 this.rightSplitPane.setAlignmentX(0.5f);
331
332 JPanel rightContainer = new JPanel();
333 rightContainer.setLayout(new BoxLayout(rightContainer, BoxLayout.Y_AXIS));
334 rightContainer.setBorder(new LineBorder(null, -1));
335
336
337 JPanel controlsContainer = new JPanel();
338 controlsContainer.add(Box.createHorizontalGlue());
339 controlsContainer.setLayout(new BoxLayout(controlsContainer, BoxLayout.X_AXIS));
340 controlsContainer.setBorder(new LineBorder(null, -1));
341 controlsContainer.setMinimumSize(new Dimension(200, 28));
342 controlsContainer.setPreferredSize(new Dimension(200, 28));
343 JLabel scenarioLabel = new JLabel("Scenario: ");
344
345 controlsContainer.add(scenarioLabel);
346 this.scenario = new AppearanceControlComboBox<>();
347
348 this.scenario.addItem(new ScenarioWrapper(null));
349 this.scenario.setMinimumSize(new Dimension(50, 22));
350 this.scenario.setMaximumSize(new Dimension(250, 22));
351 this.scenario.setPreferredSize(new Dimension(200, 22));
352 this.scenario.addActionListener((a) ->
353 {
354 try
355 {
356 OtsEditor.this.evalWrapper.setDirty();
357 OtsEditor.this.evalWrapper
358 .getEval(OtsEditor.this.scenario.getItemAt(OtsEditor.this.scenario.getSelectedIndex()));
359 }
360 catch (CircularDependencyException exception)
361 {
362 showCircularInputParameters();
363 }
364 catch (RuntimeException exception)
365 {
366
367 }
368 });
369 controlsContainer.add(this.scenario);
370 controlsContainer.add(Box.createHorizontalStrut(2));
371 JButton playRun = new JButton();
372 playRun.setToolTipText("Run single run");
373 playRun.setIcon(loadIcon("./Play.png", 18, 18, -1, -1));
374 playRun.setMinimumSize(new Dimension(24, 24));
375 playRun.setMaximumSize(new Dimension(24, 24));
376 playRun.setPreferredSize(new Dimension(24, 24));
377 playRun.addActionListener((a) -> runSingle());
378 controlsContainer.add(playRun);
379 JButton playScenario = new JButton();
380 playScenario.setToolTipText("Run scenario (batch)");
381 playScenario.setIcon(loadIcon("./NextTrack.png", 18, 18, -1, -1));
382 playScenario.setMinimumSize(new Dimension(24, 24));
383 playScenario.setMaximumSize(new Dimension(24, 24));
384 playScenario.setPreferredSize(new Dimension(24, 24));
385 playScenario.addActionListener((a) -> runBatch(false));
386 controlsContainer.add(playScenario);
387 JButton playAll = new JButton();
388 playAll.setToolTipText("Run all (batch)");
389 playAll.setIcon(loadIcon("./Last_recor.png", 18, 18, -1, -1));
390 playAll.setMinimumSize(new Dimension(24, 24));
391 playAll.setMaximumSize(new Dimension(24, 24));
392 playAll.setPreferredSize(new Dimension(24, 24));
393 playAll.addActionListener((a) -> runBatch(true));
394 controlsContainer.add(playAll);
395 controlsContainer.add(Box.createHorizontalStrut(4));
396
397 rightContainer.add(controlsContainer);
398 rightContainer.add(this.rightSplitPane);
399 this.leftRightSplitPane.setRightComponent(rightContainer);
400
401 this.questionIcon = loadIcon("./Question.png", -1, -1, -1, -1);
402
403
404 UIManager.getInsets("TabbedPane.contentBorderInsets").set(-1, -1, 1, -1);
405 this.visualizationPane = new JTabbedPane(JTabbedPane.BOTTOM, JTabbedPane.SCROLL_TAB_LAYOUT);
406 this.visualizationPane.setPreferredSize(new Dimension(900, 900));
407 this.visualizationPane.setBorder(new LineBorder(Color.BLACK, 0));
408 this.leftRightSplitPane.setLeftComponent(this.visualizationPane);
409
410
411
412
413
414 UIManager.put("Tree.collapsedIcon",
415 new ImageIcon(ImageIO.read(Resource.getResourceAsStream("/Eclipse_collapsed.png"))));
416 UIManager.put("Tree.expandedIcon", new ImageIcon(ImageIO.read(Resource.getResourceAsStream("/Eclipse_expanded.png"))));
417
418
419 this.treeTable = new AppearanceControlTreeTable(new XsdTreeTableModel(null));
420 XsdTreeTableModel.applyColumnWidth(this.treeTable);
421 this.rightSplitPane.setTopComponent(new JScrollPane(this.treeTable));
422
423
424 AttributesTableModel tableModel = new AttributesTableModel(null, this.treeTable);
425 DefaultTableColumnModel columns = new DefaultTableColumnModel();
426 TableColumn column1 = new TableColumn(0);
427 column1.setHeaderValue(tableModel.getColumnName(0));
428 columns.addColumn(column1);
429 TableColumn column2 = new TableColumn(1);
430 column2.setHeaderValue(tableModel.getColumnName(1));
431 columns.addColumn(column2);
432 TableColumn column3 = new TableColumn(2);
433 column3.setHeaderValue(tableModel.getColumnName(2));
434 columns.addColumn(column3);
435 TableColumn column4 = new TableColumn(3);
436 column4.setHeaderValue(tableModel.getColumnName(3));
437 columns.addColumn(column4);
438 this.attributesTable = new JTable(tableModel, columns);
439 this.attributesTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
440 this.attributesTable.putClientProperty("terminateEditOnFocusLost", true);
441 this.attributesTable.setDefaultRenderer(String.class,
442 new AttributeCellRenderer(loadIcon("./Info.png", 12, 12, 16, 16)));
443 AttributesCellEditor editor = new AttributesCellEditor(this.attributesTable, this);
444 this.attributesTable.setDefaultEditor(String.class, editor);
445 this.attributesTable.addMouseListener(new AttributesMouseListener(this, this.attributesTable));
446 this.attributesTable.getSelectionModel()
447 .addListSelectionListener(new AttributesListSelectionListener(this, this.attributesTable));
448 AttributesTableModel.applyColumnWidth(this.attributesTable);
449 this.rightSplitPane.setBottomComponent(new JScrollPane(this.attributesTable));
450
451 addMenuBar();
452
453 this.statusLabel = new StatusLabel();
454 this.statusLabel.setForeground(STATUS_COLOR);
455 this.statusLabel.setHorizontalAlignment(SwingConstants.LEFT);
456 this.statusLabel.setBorder(new BevelBorder(BevelBorder.LOWERED));
457 add(this.statusLabel, BorderLayout.SOUTH);
458 removeStatusLabel();
459 }
460
461
462
463
464 private void runSingle()
465 {
466 if (!((XsdTreeNode) this.treeTable.getTree().getModel().getRoot()).isValid())
467 {
468 showInvalidMessage();
469 return;
470 }
471 int index = this.scenario.getSelectedIndex();
472 try
473 {
474 OtsEditor.this.evalWrapper.setDirty();
475 OtsEditor.this.evalWrapper.getEval(OtsEditor.this.scenario.getItemAt(index));
476 }
477 catch (CircularDependencyException ex)
478 {
479 showCircularInputParameters();
480 return;
481 }
482 File file;
483 try
484 {
485 file = File.createTempFile("ots_", ".xml");
486 }
487 catch (IOException exception)
488 {
489 showUnableToRun();
490 return;
491 }
492 save(file, (XsdTreeNodeRoot) this.treeTable.getTree().getModel().getRoot(), false);
493
494 if (index == 0)
495 {
496 OtsRunner.runSingle(file, null);
497 }
498 else
499 {
500 String scenario = this.scenario.getItemAt(index).getScenarioNode().getId();
501 OtsRunner.runSingle(file, scenario);
502 }
503 file.delete();
504 }
505
506
507
508
509
510 protected void runBatch(final boolean all)
511 {
512 if (!((XsdTreeNode) this.treeTable.getTree().getModel().getRoot()).isValid())
513 {
514 showInvalidMessage();
515 return;
516 }
517
518 if (all)
519 {
520 System.out.println("Running all.");
521 }
522 else
523 {
524 int index = this.scenario.getSelectedIndex();
525 if (index == 0)
526 {
527 System.out.println("Running all runs of the default scenario.");
528 }
529 else
530 {
531 System.out.println("Running all runs of scenario " + this.scenario.getItemAt(index) + ".");
532 }
533 }
534 }
535
536
537
538
539
540 public Undo getUndo()
541 {
542 return this.undo;
543 }
544
545
546
547
548
549
550 public void show(final XsdTreeNode node, final String attribute)
551 {
552 if (node.getParent() == null)
553 {
554 return;
555 }
556 if (this.treeTable.isEditing())
557 {
558 CellEditor editor = this.treeTable.getCellEditor();
559 if (editor != null)
560 {
561 editor.cancelCellEditing();
562 }
563 }
564 if (this.attributesTable.isEditing())
565 {
566 CellEditor editor = this.attributesTable.getCellEditor();
567 if (editor != null)
568 {
569 editor.cancelCellEditing();
570 }
571 }
572 List<XsdTreeNode> nodePath = node.getPath();
573 TreePath path = new TreePath(nodePath.toArray());
574 TreePath partialPath = new TreePath(nodePath.subList(0, nodePath.size() - 1).toArray());
575 this.treeTable.getTree().expandPath(partialPath);
576 this.treeTable.getTree().setSelectionPath(path);
577 this.treeTable.getTree().scrollPathToVisible(path);
578 this.treeTable.updateUI();
579 Rectangle bounds = this.treeTable.getTree().getPathBounds(path);
580 if (bounds == null)
581 {
582 return;
583 }
584 bounds.x += this.treeTable.getX();
585 bounds.y += this.treeTable.getY();
586 ((JComponent) this.treeTable.getParent()).scrollRectToVisible(bounds);
587
588 if (node.isActive())
589 {
590 this.attributesTable.setModel(new AttributesTableModel(node, this.treeTable));
591 }
592 else
593 {
594 this.attributesTable.setModel(new AttributesTableModel(null, this.treeTable));
595 }
596 if (attribute != null)
597 {
598 int index = node.getAttributeIndexByName(attribute);
599 this.attributesTable.setRowSelectionInterval(index, index);
600 }
601 else
602 {
603 this.attributesTable.getSelectionModel().clearSelection();
604 }
605 }
606
607
608
609
610
611 public void setStatusLabel(final String label)
612 {
613 this.statusLabel.setText(label);
614 }
615
616
617
618
619 public void removeStatusLabel()
620 {
621 this.statusLabel.setText(" ");
622 }
623
624
625
626
627 private void addMenuBar()
628 {
629 JMenuBar menuBar = new JMenuBar();
630 setJMenuBar(menuBar);
631
632 JMenu fileMenu = new JMenu("File");
633 menuBar.add(fileMenu);
634 JMenuItem newFile = new JMenuItem("New");
635 newFile.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK));
636 fileMenu.add(newFile);
637 newFile.addActionListener((a) -> newFile());
638 JMenuItem open = new JMenuItem("Open...");
639 open.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK));
640 fileMenu.add(open);
641 open.addActionListener((a) -> openFile());
642 this.recentFilesMenu = new JMenu("Recent files");
643 updateRecentFileMenu();
644 fileMenu.add(this.recentFilesMenu);
645 JMenuItem save = new JMenuItem("Save");
646 save.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK));
647 fileMenu.add(save);
648 save.addActionListener((a) -> saveFile());
649 JMenuItem saveAs = new JMenuItem("Save as...");
650 fileMenu.add(saveAs);
651 saveAs.addActionListener((a) -> saveFileAs((XsdTreeNodeRoot) OtsEditor.this.treeTable.getTree().getModel().getRoot()));
652 fileMenu.add(new JSeparator());
653 JMenuItem properties = new JMenuItem("Properties...");
654 fileMenu.add(properties);
655 properties.addActionListener((a) -> showProperties());
656 fileMenu.add(new JSeparator());
657 JMenuItem exit = new JMenuItem("Exit");
658 fileMenu.add(exit);
659 exit.addActionListener((a) -> exit());
660
661 JMenu editMenu = new JMenu("Edit");
662 menuBar.add(editMenu);
663
664 JMenuItem undoItem = new JMenuItem("Undo");
665 undoItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, ActionEvent.CTRL_MASK));
666 editMenu.add(undoItem);
667 undoItem.addActionListener((a) ->
668 {
669 if (undoItem.isEnabled())
670 {
671 OtsEditor.this.undo.undo();
672 }
673 });
674
675 JMenuItem redoItem = new JMenuItem("Redo");
676 redoItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, ActionEvent.CTRL_MASK));
677 editMenu.add(redoItem);
678 redoItem.addActionListener((a) ->
679 {
680 if (redoItem.isEnabled())
681 {
682 OtsEditor.this.undo.redo();
683 }
684 });
685 this.undo = new Undo(this, undoItem, redoItem);
686
687 JMenu navigateMenu = new JMenu("Navigate");
688 menuBar.add(navigateMenu);
689 this.backItem = new JMenuItem("Go back");
690 this.backItem.setEnabled(false);
691 this.backItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0));
692 navigateMenu.add(this.backItem);
693 this.backItem.addActionListener((a) ->
694 {
695 show(this.backNode.pollLast(), this.backAttribute.pollLast());
696 if (this.backNode.isEmpty())
697 {
698 this.backItem.setText("Go back");
699 this.backItem.setEnabled(false);
700 }
701 else
702 {
703 XsdTreeNode back = this.backNode.peekLast();
704 this.backItem.setText("Go back to " + back.getNodeName() + (back.isIdentifiable() ? " " + back.getId() : ""));
705 this.backItem.setEnabled(true);
706 }
707 });
708 this.coupledItem = new JMenuItem("Go to coupled item");
709 this.coupledItem.setEnabled(false);
710 this.coupledItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F4, 0));
711 navigateMenu.add(this.coupledItem);
712 this.coupledItem.addActionListener((a) ->
713 {
714 if (this.coupledNode != null)
715 {
716 this.backNode.add(this.candidateBackNode);
717 this.backAttribute.add(this.candidateBackAttribute);
718 while (this.backNode.size() > MAX_NAVIGATE)
719 {
720 this.backNode.remove();
721 this.backAttribute.remove();
722 }
723 XsdTreeNode back = OtsEditor.this.backNode.peekLast();
724 this.backItem.setText("Go back to " + back.getNodeName() + (back.isIdentifiable() ? " " + back.getId() : ""));
725 this.backItem.setEnabled(this.backNode.peekLast() != null);
726 show(OtsEditor.this.coupledNode, null);
727 }
728 });
729 }
730
731
732
733
734 private void updateRecentFileMenu()
735 {
736 this.recentFilesMenu.removeAll();
737 List<String> files = this.applicationStore.getRecentFiles("recent_files");
738 if (!files.isEmpty())
739 {
740 for (String file : files)
741 {
742 JMenuItem item = new JMenuItem(file);
743 item.addActionListener((i) ->
744 {
745 if (confirmDiscardChanges())
746 {
747 File f = new File(file);
748 this.lastDirectory = f.getParent() + File.separator;
749 this.lastFile = f.getName();
750 if (!loadFile(f, "File loaded", true))
751 {
752 boolean remove = JOptionPane.showConfirmDialog(OtsEditor.this,
753 "File could not be loaded. Do you want ro remove it from recent files?",
754 "Remove from recent files?", JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE,
755 this.questionIcon) == JOptionPane.OK_OPTION;
756 if (remove)
757 {
758 this.applicationStore.removeRecentFile("recent_files", file);
759 updateRecentFileMenu();
760 }
761 }
762 }
763 });
764 this.recentFilesMenu.add(item);
765 }
766 this.recentFilesMenu.add(new JSeparator());
767 }
768 JMenuItem item = new JMenuItem("Clear history");
769 item.setEnabled(!files.isEmpty());
770 item.addActionListener((i) ->
771 {
772 boolean clear = JOptionPane.showConfirmDialog(OtsEditor.this, "Are you sure you want to clear the recent files?",
773 "Clear recent files?", JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE,
774 this.questionIcon) == JOptionPane.OK_OPTION;
775 if (clear)
776 {
777 this.applicationStore.clearProperty("recent_files");
778 updateRecentFileMenu();
779 }
780 });
781 this.recentFilesMenu.add(item);
782 }
783
784
785
786
787
788
789
790
791 public void setCoupledNode(final XsdTreeNode coupledNode, final XsdTreeNode backNode, final String backAttribute)
792 {
793 if (coupledNode == null)
794 {
795 this.coupledItem.setEnabled(false);
796 this.coupledItem.setText("Go to coupled item");
797 }
798 else
799 {
800 this.coupledItem.setEnabled(true);
801 this.coupledItem.setText("Go to " + (backAttribute != null ? backNode.getAttributeValue(backAttribute)
802 : (backNode.isIdentifiable() ? backNode.getId() : backNode.getValue())));
803 }
804 this.coupledNode = coupledNode;
805 this.candidateBackNode = backNode;
806 this.candidateBackAttribute = backAttribute;
807 }
808
809
810
811
812
813 public void setUnsavedChanges(final boolean unsavedChanges)
814 {
815 this.unsavedChanges = unsavedChanges;
816 StringBuilder title = new StringBuilder("OTS | The Open Traffic Simulator | Editor");
817 if (this.lastFile != null)
818 {
819 title.append(" (").append(this.lastDirectory).append(this.lastFile).append(")");
820 }
821 if (this.unsavedChanges)
822 {
823 title.append(" *");
824 }
825 setTitle(title.toString());
826 }
827
828
829
830
831
832
833 @SuppressWarnings("checkstyle:hiddenfield")
834 public void setSchema(final Document xsdDocument) throws IOException
835 {
836 this.xsdDocument = xsdDocument;
837 this.undo.setIgnoreChanges(true);
838 initializeTree();
839 this.undo.clear();
840 setStatusLabel("Schema " + xsdDocument.getBaseURI() + " loaded");
841
842 setVisible(true);
843 this.leftRightSplitPane.setDividerLocation(0.65);
844 this.rightSplitPane.setDividerLocation(0.75);
845 setAppearance(getAppearance());
846
847 SwingUtilities.invokeLater(() -> checkAutosave());
848 }
849
850
851
852
853 private void checkAutosave()
854 {
855 Path tmpPath = Paths.get(System.getProperty("java.io.tmpdir") + "ots" + File.separator);
856 File tmpDir = tmpPath.toFile();
857 if (!tmpDir.exists())
858 {
859 tmpDir.mkdir();
860 }
861 Iterator<Path> it;
862 try
863 {
864 it = Files.newDirectoryStream(tmpPath, "autosave*.xml").iterator();
865 }
866 catch (IOException ioe)
867 {
868
869 return;
870 }
871 if (it.hasNext())
872 {
873 File file = it.next().toFile();
874 int userInput = JOptionPane.showConfirmDialog(this,
875 "Autosave file " + file.getName() + " (" + new Date(file.lastModified())
876 + ") detected. Do you want to load this file? ('No' removes the file)",
877 "Autosave file detected", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE,
878 this.questionIcon);
879 if (userInput == JOptionPane.OK_OPTION)
880 {
881 boolean loaded = loadFile(file, "Autosave file loaded", false);
882 if (!loaded)
883 {
884 boolean remove = JOptionPane.showConfirmDialog(OtsEditor.this,
885 "File could not be loaded. Do you want ro remove it from recent files?",
886 "Remove from recent files?", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE,
887 this.questionIcon) == JOptionPane.YES_OPTION;
888 if (remove)
889 {
890 file.delete();
891 }
892 }
893 setUnsavedChanges(true);
894 this.treeTable.updateUI();
895 file.delete();
896 }
897 else if (userInput == JOptionPane.NO_OPTION)
898 {
899 file.delete();
900 }
901 }
902 }
903
904
905
906
907 private void newFile()
908 {
909 if (confirmDiscardChanges())
910 {
911 try
912 {
913 XsdTreeNode node = (XsdTreeNode) this.treeTable.getTree().getModel().getRoot();
914 this.undo.startAction(ActionType.ADD, node, null);
915 initializeTree();
916 this.attributesTable.setModel(new AttributesTableModel(null, this.treeTable));
917 this.undo.clear();
918 }
919 catch (IOException exception)
920 {
921 JOptionPane.showMessageDialog(this, "Unable to reload schema.", "Unable to reload schema.",
922 JOptionPane.WARNING_MESSAGE);
923 }
924 }
925 }
926
927
928
929
930
931 private void initializeTree() throws IOException
932 {
933 this.scenario.removeAllItems();
934 this.scenario.addItem(new ScenarioWrapper(null));
935 setDefaultProperties();
936
937
938 XsdTreeTableModel treeModel = new XsdTreeTableModel(this.xsdDocument);
939 this.treeTable = new AppearanceControlTreeTable(treeModel);
940 this.treeTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
941 this.nodeActions = new NodeActions(this, this.treeTable);
942 this.treeTable.putClientProperty("terminateEditOnFocusLost", true);
943 treeModel.setTreeTable(this.treeTable);
944 this.treeTable.setDefaultRenderer(String.class, new StringCellRenderer(this.treeTable));
945 ((DefaultCellEditor) this.treeTable.getDefaultEditor(String.class)).setClickCountToStart(1);
946 XsdTreeTableModel.applyColumnWidth(this.treeTable);
947
948 addTreeTableListeners();
949
950 int dividerLocation = this.rightSplitPane.getDividerLocation();
951 this.rightSplitPane.setTopComponent(null);
952 this.rightSplitPane.setTopComponent(new JScrollPane(this.treeTable));
953 this.rightSplitPane.setDividerLocation(dividerLocation);
954
955 XsdTreeNodeRoot root = (XsdTreeNodeRoot) treeModel.getRoot();
956 EventListener listener = new ChangesListener(this, this.scenario);
957 root.addListener(listener, XsdTreeNodeRoot.NODE_CREATED);
958 root.addListener(listener, XsdTreeNodeRoot.NODE_REMOVED);
959 fireEvent(NEW_FILE, root);
960
961 setUnsavedChanges(false);
962 if (this.autosave != null)
963 {
964 this.autosave.cancel();
965 }
966 this.autosave = new TimerTask()
967 {
968
969 @Override
970 public void run()
971 {
972 if (OtsEditor.this.unsavedChanges)
973 {
974 setStatusLabel("Autosaving...");
975 File file = new File(System.getProperty("java.io.tmpdir") + "ots" + File.separator
976 + (OtsEditor.this.lastFile == null ? "autosave.xml" : "autosave_" + OtsEditor.this.lastFile));
977 save(file, root, false);
978 file.deleteOnExit();
979 setStatusLabel("Autosaved");
980 }
981 }
982 };
983 new Timer().scheduleAtFixedRate(this.autosave, AUTOSAVE_PERIOD_MS, AUTOSAVE_PERIOD_MS);
984 setAppearance(getAppearance());
985 }
986
987
988
989
990 private void setDefaultProperties()
991 {
992 this.properties.clear();
993 this.properties.add("xmlns:ots");
994 this.properties.add("http://www.opentrafficsim.org/ots");
995 this.properties.add("xmlns:xi");
996 this.properties.add("http://www.w3.org/2001/XInclude");
997 this.properties.add("xmlns:xsi");
998 this.properties.add("http://www.w3.org/2001/XMLSchema-instance");
999 this.properties.add("xsi:schemaLocation");
1000 this.properties.add(null);
1001 }
1002
1003
1004
1005
1006
1007 private void addTreeTableListeners() throws IOException
1008 {
1009
1010 DefaultCellEditor editor = (DefaultCellEditor) this.treeTable.getDefaultEditor(String.class);
1011 ((JTextField) editor.getComponent()).addKeyListener(new KeyAdapter()
1012 {
1013
1014 @Override
1015 public void keyReleased(final KeyEvent e)
1016 {
1017 int editorCol = OtsEditor.this.treeTable.convertColumnIndexToView(OtsEditor.this.treeTable.getSelectedColumn());
1018 if (editorCol == 1 || editorCol == 2)
1019 {
1020 int row = OtsEditor.this.treeTable.getSelectedRow();
1021 int col = OtsEditor.this.treeTable.convertColumnIndexToView(0);
1022 XsdTreeNode treeNode = (XsdTreeNode) OtsEditor.this.treeTable.getValueAt(row, col);
1023 if (editorCol == 1)
1024 {
1025 treeNode.setId(((JTextField) e.getComponent()).getText());
1026 }
1027 else if (editorCol == 2)
1028 {
1029 treeNode.setValue(((JTextField) e.getComponent()).getText());
1030 }
1031 }
1032 }
1033 });
1034
1035
1036 ((JTextField) editor.getComponent()).addFocusListener(new FocusListener()
1037 {
1038
1039 @Override
1040 public void focusGained(final FocusEvent e)
1041 {
1042 startUndoActionOnTreeTable();
1043 }
1044
1045
1046 @Override
1047 public void focusLost(final FocusEvent e)
1048 {
1049 startUndoActionOnTreeTable();
1050 }
1051 });
1052
1053
1054 editor.addCellEditorListener(new CellEditorListener()
1055 {
1056
1057 @Override
1058 public void editingStopped(final ChangeEvent e)
1059 {
1060 startUndoActionOnTreeTable();
1061 }
1062
1063
1064 @Override
1065 public void editingCanceled(final ChangeEvent e)
1066 {
1067 startUndoActionOnTreeTable();
1068 }
1069 });
1070
1071
1072 this.treeTable.getTree()
1073 .addTreeSelectionListener(new XsdTreeSelectionListener(this, this.treeTable, this.attributesTable));
1074
1075
1076 this.treeTable.getTree().setCellRenderer(new XsdTreeCellRenderer(this));
1077
1078
1079 this.treeTable.getTree().addTreeWillExpandListener(new TreeWillExpandListener()
1080 {
1081
1082 @Override
1083 public void treeWillExpand(final TreeExpansionEvent event) throws ExpandVetoException
1084 {
1085 OtsEditor.this.mayPresentChoice = false;
1086 }
1087
1088
1089 @Override
1090 public void treeWillCollapse(final TreeExpansionEvent event) throws ExpandVetoException
1091 {
1092 OtsEditor.this.mayPresentChoice = false;
1093 }
1094 });
1095
1096
1097
1098 this.treeTable.addMouseMotionListener(new MouseMotionAdapter()
1099 {
1100
1101 @Override
1102 public void mouseMoved(final MouseEvent e)
1103 {
1104 OtsEditor.this.mayPresentChoice = true;
1105
1106
1107 int row = OtsEditor.this.treeTable.rowAtPoint(e.getPoint());
1108 int col = OtsEditor.this.treeTable.convertColumnIndexToView(0);
1109 XsdTreeNode treeNode = (XsdTreeNode) OtsEditor.this.treeTable.getValueAt(row, col);
1110 try
1111 {
1112 if (!treeNode.isSelfValid())
1113 {
1114 OtsEditor.this.treeTable.getTree().setToolTipText(treeNode.reportInvalidNode());
1115 }
1116 else
1117 {
1118 OtsEditor.this.treeTable.getTree().setToolTipText(null);
1119 }
1120 }
1121 catch (Exception ex)
1122 {
1123 if (treeNode.isIdentifiable())
1124 {
1125 System.out.println("Node " + treeNode.getId() + " no valid.");
1126 }
1127 else
1128 {
1129 System.out.println("Node " + treeNode.getNodeName() + " no valid.");
1130 }
1131 }
1132 }
1133 });
1134
1135
1136 this.treeTable.addMouseListener(new XsdTreeMouseListener(this, this.treeTable, this.attributesTable));
1137
1138
1139 this.treeTable.addKeyListener(new XsdTreeKeyListener(this, this.treeTable));
1140 }
1141
1142
1143
1144
1145 public void startUndoActionOnTreeTable()
1146 {
1147 XsdTreeNode node = (XsdTreeNode) this.treeTable.getValueAt(this.treeTable.getSelectedRow(), 0);
1148 int col = this.treeTable.convertColumnIndexToView(this.treeTable.getSelectedColumn());
1149 if (col == 1)
1150 {
1151 this.undo.startAction(ActionType.ID_CHANGE, node, null);
1152 }
1153 else if (col == 2)
1154 {
1155 this.undo.startAction(ActionType.VALUE_CHANGE, node, null);
1156 }
1157 }
1158
1159
1160
1161
1162
1163
1164
1165 public void preparePopupRemoval(final JPopupMenu popup, final JComponent component)
1166 {
1167 popup.addPopupMenuListener(new PopupMenuListener()
1168 {
1169
1170 @Override
1171 public void popupMenuWillBecomeVisible(final PopupMenuEvent e)
1172 {
1173 }
1174
1175
1176 @Override
1177 public void popupMenuWillBecomeInvisible(final PopupMenuEvent e)
1178 {
1179 component.setComponentPopupMenu(null);
1180 OtsEditor.this.choiceNode = null;
1181 }
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
1282 @Override
1283 public EventListenerMap getEventListenerMap() throws RemoteException
1284 {
1285 return this.listenerMap;
1286 }
1287
1288
1289
1290
1291
1292
1293
1294
1295 public void addTab(final String name, final Icon icon, final Component component, final String tip)
1296 {
1297 this.visualizationPane.addTab(name, icon, component, tip);
1298 }
1299
1300
1301
1302
1303
1304
1305 public Component getTab(final String name)
1306 {
1307 for (int index = 0; index < this.visualizationPane.getTabCount(); index++)
1308 {
1309 if (this.visualizationPane.getTitleAt(index).equals(name))
1310 {
1311 return this.visualizationPane.getComponentAt(index);
1312 }
1313 }
1314 return null;
1315 }
1316
1317
1318
1319
1320
1321 public void focusTab(final String name)
1322 {
1323 for (int index = 0; index < this.visualizationPane.getTabCount(); index++)
1324 {
1325 if (this.visualizationPane.getTitleAt(index).equals(name))
1326 {
1327 this.visualizationPane.setSelectedIndex(index);
1328 }
1329 }
1330 }
1331
1332
1333
1334
1335
1336
1337
1338
1339 public boolean confirmNodeRemoval(final XsdTreeNode node)
1340 {
1341 return JOptionPane.showConfirmDialog(this, "Remove `" + node + "`?", "Remove?", JOptionPane.OK_CANCEL_OPTION,
1342 JOptionPane.QUESTION_MESSAGE, this.questionIcon) == JOptionPane.OK_OPTION;
1343 }
1344
1345
1346
1347
1348
1349 private boolean confirmDiscardChanges()
1350 {
1351 if (!this.unsavedChanges)
1352 {
1353 return true;
1354 }
1355 return JOptionPane.showConfirmDialog(this, "Discard unsaved changes?", "Discard unsaved changes?",
1356 JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, this.questionIcon) == JOptionPane.OK_OPTION;
1357 }
1358
1359
1360
1361
1362
1363 public void showDescription(final String description)
1364 {
1365 JOptionPane.showMessageDialog(OtsEditor.this,
1366 "<html><body><p style='width: 400px;'>" + description + "</p></body></html>");
1367 }
1368
1369
1370
1371
1372 public void showInvalidMessage()
1373 {
1374 JOptionPane.showMessageDialog(OtsEditor.this, "The setup is not valid. Make sure no red nodes remain.",
1375 "Setup is not valid", JOptionPane.INFORMATION_MESSAGE);
1376 }
1377
1378
1379
1380
1381 public void showCircularInputParameters()
1382 {
1383 JOptionPane.showMessageDialog(OtsEditor.this, "Input parameters have a circular dependency.",
1384 "Circular input parameter", JOptionPane.INFORMATION_MESSAGE);
1385 }
1386
1387
1388
1389
1390 public void showUnableToRun()
1391 {
1392 JOptionPane.showMessageDialog(OtsEditor.this, "Unable to run, temporary file could not be saved.", "Unable to run",
1393 JOptionPane.INFORMATION_MESSAGE);
1394 }
1395
1396
1397
1398
1399
1400
1401
1402
1403 public void optionsPopup(final List<String> allOptions, final JTable table, final Consumer<String> action)
1404 {
1405
1406 List<String> options = filterOptions(allOptions, "");
1407 OtsEditor.this.dropdownOptions = options;
1408 if (options.isEmpty())
1409 {
1410 return;
1411 }
1412 JPopupMenu popup = new JPopupMenu();
1413 int index = 0;
1414 for (String option : options)
1415 {
1416 JMenuItem item = new JMenuItem(option);
1417 item.setVisible(index++ < MAX_DROPDOWN_ITEMS);
1418 item.addActionListener(new ActionListener()
1419 {
1420
1421 @Override
1422 public void actionPerformed(final ActionEvent e)
1423 {
1424 action.accept(option);
1425 CellEditor cellEditor = table.getCellEditor();
1426 if (cellEditor != null)
1427 {
1428 cellEditor.cancelCellEditing();
1429 }
1430 OtsEditor.this.treeTable.updateUI();
1431 }
1432 });
1433 item.setFont(table.getFont());
1434 popup.add(item);
1435 }
1436 this.dropdownIndent = 0;
1437 popup.addMouseWheelListener(new MouseWheelListener()
1438 {
1439
1440
1441 @Override
1442 public void mouseWheelMoved(final MouseWheelEvent e)
1443 {
1444 OtsEditor.this.dropdownIndent += (e.getWheelRotation() * e.getScrollAmount());
1445 OtsEditor.this.dropdownIndent = OtsEditor.this.dropdownIndent < 0 ? 0 : OtsEditor.this.dropdownIndent;
1446 int maxIndent = OtsEditor.this.dropdownOptions.size() - MAX_DROPDOWN_ITEMS;
1447 if (maxIndent > 0)
1448 {
1449 OtsEditor.this.dropdownIndent =
1450 OtsEditor.this.dropdownIndent > maxIndent ? maxIndent : OtsEditor.this.dropdownIndent;
1451 showOptionsInScope(popup);
1452 }
1453 }
1454 });
1455 preparePopupRemoval(popup, table);
1456
1457 SwingUtilities.invokeLater(new Runnable()
1458 {
1459
1460 @Override
1461 public void run()
1462 {
1463 JTextField field = (JTextField) ((DefaultCellEditor) table.getDefaultEditor(String.class)).getComponent();
1464 table.setComponentPopupMenu(popup);
1465 popup.pack();
1466 popup.setInvoker(table);
1467 popup.setVisible(true);
1468 field.requestFocus();
1469 Rectangle rectangle = field.getBounds();
1470 placePopup(popup, rectangle, table);
1471 field.addKeyListener(new KeyAdapter()
1472 {
1473
1474 @Override
1475 public void keyTyped(final KeyEvent e)
1476 {
1477
1478 SwingUtilities.invokeLater(new Runnable()
1479 {
1480
1481 @Override
1482 public void run()
1483 {
1484 OtsEditor.this.dropdownIndent = 0;
1485 String currentValue = field.getText();
1486 OtsEditor.this.dropdownOptions = filterOptions(allOptions, currentValue);
1487 boolean anyVisible = showOptionsInScope(popup);
1488
1489
1490 if (!anyVisible)
1491 {
1492 JMenuItem item = new JMenuItem(currentValue);
1493 item.addActionListener(new ActionListener()
1494 {
1495
1496 @Override
1497 public void actionPerformed(final ActionEvent e)
1498 {
1499 action.accept(currentValue);
1500 CellEditor cellEditor = table.getCellEditor();
1501 if (cellEditor != null)
1502 {
1503 cellEditor.cancelCellEditing();
1504 }
1505 OtsEditor.this.treeTable.updateUI();
1506 }
1507 });
1508 item.setFont(table.getFont());
1509 popup.add(item);
1510 }
1511 popup.pack();
1512 placePopup(popup, rectangle, table);
1513 }
1514 });
1515 }
1516 });
1517 field.addActionListener(new ActionListener()
1518 {
1519
1520 @Override
1521 public void actionPerformed(final ActionEvent e)
1522 {
1523 popup.setVisible(false);
1524 table.setComponentPopupMenu(null);
1525 }
1526 });
1527 }
1528 });
1529 }
1530
1531
1532
1533
1534
1535
1536 private boolean showOptionsInScope(final JPopupMenu popup)
1537 {
1538 int optionIndex = 0;
1539 for (Component component : popup.getComponents())
1540 {
1541 JMenuItem item = (JMenuItem) component;
1542 boolean visible =
1543 optionIndex < MAX_DROPDOWN_ITEMS && this.dropdownOptions.indexOf(item.getText()) >= this.dropdownIndent;
1544 item.setVisible(visible);
1545 if (visible)
1546 {
1547 optionIndex++;
1548 }
1549 }
1550 popup.pack();
1551 return optionIndex > 0;
1552 }
1553
1554
1555
1556
1557
1558
1559
1560 private void placePopup(final JPopupMenu popup, final Rectangle rectangle, final JComponent parent)
1561 {
1562 Point pAttributes = parent.getLocationOnScreen();
1563
1564 Dimension windowSize = OtsEditor.this.getSize();
1565 Point pWindow = OtsEditor.this.getLocationOnScreen();
1566 if (pAttributes.y + (int) rectangle.getMaxY() + popup.getBounds().getHeight() > windowSize.height + pWindow.y - 1)
1567 {
1568 popup.setLocation(pAttributes.x + (int) rectangle.getMinX(),
1569 pAttributes.y + (int) rectangle.getMinY() - 1 - (int) popup.getBounds().getHeight());
1570 }
1571 else
1572 {
1573 popup.setLocation(pAttributes.x + (int) rectangle.getMinX(), pAttributes.y + (int) rectangle.getMaxY() - 1);
1574 }
1575 }
1576
1577
1578
1579
1580 void openFile()
1581 {
1582 if (!confirmDiscardChanges())
1583 {
1584 return;
1585 }
1586 FileDialog fileDialog = new FileDialog(this, "Open XML", FileDialog.LOAD);
1587 fileDialog.setFilenameFilter(new FilenameFilter()
1588 {
1589
1590 @Override
1591 public boolean accept(final File dir, final String name)
1592 {
1593 return name.toLowerCase().endsWith(".xml");
1594 }
1595 });
1596 fileDialog.setVisible(true);
1597 String fileName = fileDialog.getFile();
1598 if (fileName == null)
1599 {
1600 return;
1601 }
1602 if (!fileName.toLowerCase().endsWith(".xml"))
1603 {
1604 return;
1605 }
1606 this.lastDirectory = fileDialog.getDirectory();
1607 this.lastFile = fileName;
1608 File file = new File(this.lastDirectory + this.lastFile);
1609 boolean loaded = loadFile(file, "File loaded", true);
1610 if (!loaded)
1611 {
1612 JOptionPane.showMessageDialog(this, "Unable to read file.", "Unable to read file.", JOptionPane.WARNING_MESSAGE);
1613 }
1614 }
1615
1616
1617
1618
1619
1620
1621
1622
1623 private boolean loadFile(final File file, final String status, final boolean updateRecentFiles)
1624 {
1625 try
1626 {
1627 Document document = DocumentReader.open(file.toURI());
1628 this.undo.setIgnoreChanges(true);
1629 initializeTree();
1630 NamedNodeMap attributes = document.getFirstChild().getAttributes();
1631 for (int i = 0; i < attributes.getLength(); i++)
1632 {
1633 if (this.properties.contains(attributes.item(i).getNodeName()))
1634 {
1635 int index = this.properties.indexOf(attributes.item(i).getNodeName());
1636 this.properties.set(index, attributes.item(i).getNodeName());
1637 this.properties.set(index + 1, attributes.item(i).getNodeValue());
1638 }
1639 else
1640 {
1641 this.properties.add(attributes.item(i).getNodeName());
1642 this.properties.add(attributes.item(i).getNodeValue());
1643 }
1644 }
1645 XsdTreeNodeRoot root = (XsdTreeNodeRoot) OtsEditor.this.treeTable.getTree().getModel().getRoot();
1646 root.setDirectory(this.lastDirectory);
1647 root.loadXmlNodes(document.getFirstChild());
1648 this.undo.clear();
1649 setUnsavedChanges(false);
1650 setStatusLabel(status);
1651 this.undo.updateButtons();
1652 this.backItem.setEnabled(false);
1653 this.coupledItem.setEnabled(false);
1654 this.coupledItem.setText("Go to coupled item");
1655 this.treeTable.updateUI();
1656 if (updateRecentFiles)
1657 {
1658 this.applicationStore.addRecentFile("recent_files", file.getAbsolutePath());
1659 updateRecentFileMenu();
1660 }
1661 return true;
1662 }
1663 catch (SAXException | IOException | ParserConfigurationException exception)
1664 {
1665 return false;
1666 }
1667 }
1668
1669
1670
1671
1672 private void saveFile()
1673 {
1674 XsdTreeNodeRoot root = (XsdTreeNodeRoot) OtsEditor.this.treeTable.getTree().getModel().getRoot();
1675 if (this.lastFile == null)
1676 {
1677 saveFileAs(root);
1678 return;
1679 }
1680 save(new File(this.lastDirectory + this.lastFile), root, true);
1681 setUnsavedChanges(false);
1682 setStatusLabel("Saved");
1683 }
1684
1685
1686
1687
1688
1689 public void saveFileAs(final XsdTreeNode root)
1690 {
1691 FileDialog fileDialog = new FileDialog(this, "Save XML", FileDialog.SAVE);
1692 fileDialog.setFile("*.xml");
1693 fileDialog.setVisible(true);
1694 String fileName = fileDialog.getFile();
1695 if (fileName == null)
1696 {
1697 return;
1698 }
1699 if (!fileName.toLowerCase().endsWith(".xml"))
1700 {
1701 fileName = fileName + ".xml";
1702 }
1703 if (root instanceof XsdTreeNodeRoot)
1704 {
1705 this.lastDirectory = fileDialog.getDirectory();
1706 this.lastFile = fileName;
1707 }
1708 save(new File(fileDialog.getDirectory() + fileName), root, true);
1709 if (root instanceof XsdTreeNodeRoot)
1710 {
1711 this.undo.setIgnoreChanges(true);
1712 ((XsdTreeNodeRoot) root).setDirectory(this.lastDirectory);
1713 this.treeTable.updateUI();
1714 this.attributesTable.updateUI();
1715 setUnsavedChanges(false);
1716 this.undo.setIgnoreChanges(false);
1717 }
1718 setStatusLabel("Saved");
1719 }
1720
1721
1722
1723
1724
1725
1726
1727 private void save(final File file, final XsdTreeNode root, final boolean storeAsRecent)
1728 {
1729 try (FileOutputStream fileOutputStream = new FileOutputStream(file))
1730 {
1731 DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
1732 Document document = docBuilder.newDocument();
1733
1734
1735
1736
1737
1738
1739 document.setXmlStandalone(true);
1740 root.saveXmlNodes(document, document);
1741 Element xmlRoot = (Element) document.getChildNodes().item(0);
1742 Set<String> nameSpaces = new LinkedHashSet<>();
1743 nameSpaces.add("xmlns");
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 if (prop.startsWith("xmlns") && value != null && !value.isBlank())
1749 {
1750 nameSpaces.add(prop.substring(6));
1751 }
1752 }
1753 for (int i = 0; i < this.properties.size(); i = i + 2)
1754 {
1755 String prop = this.properties.get(i);
1756 String value = this.properties.get(i + 1);
1757 int semi = prop.indexOf(":");
1758 String nameSpace = semi < 0 ? null : prop.substring(0, semi);
1759 if (!nameSpaces.contains(nameSpace) && value != null && !value.isBlank())
1760 {
1761 JOptionPane.showMessageDialog(this,
1762 "Unable to save property " + prop + " as its namespace xmlns:" + nameSpace + " is not provided.",
1763 "Unable to save property.", JOptionPane.WARNING_MESSAGE);
1764 }
1765 else if (value != null && !value.isBlank())
1766 {
1767 xmlRoot.setAttribute(prop, value);
1768 }
1769 }
1770 StreamResult result = new StreamResult(fileOutputStream);
1771
1772 Transformer transformer = TransformerFactory.newInstance().newTransformer();
1773 transformer.setOutputProperty(OutputKeys.INDENT, "yes");
1774 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
1775 transformer.transform(new DOMSource(document), result);
1776
1777 fileOutputStream.close();
1778
1779 String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
1780 content = content.replace("<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
1781 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + System.lineSeparator());
1782 Files.write(file.toPath(), content.getBytes(StandardCharsets.UTF_8));
1783
1784 if (storeAsRecent)
1785 {
1786 this.applicationStore.addRecentFile("recent_files", file.getAbsolutePath());
1787 updateRecentFileMenu();
1788 }
1789 }
1790 catch (ParserConfigurationException | TransformerException | IOException exception)
1791 {
1792 JOptionPane.showMessageDialog(this, "Unable to save file.", "Unable to save file.", JOptionPane.WARNING_MESSAGE);
1793 }
1794 }
1795
1796
1797
1798
1799 private void showProperties()
1800 {
1801
1802 JPanel panel = new JPanel();
1803 panel.setLayout(new BorderLayout());
1804
1805 TableColumnModel columns = new DefaultTableColumnModel();
1806 TableColumn column1 = new TableColumn(0, 200);
1807 column1.setHeaderValue("Property");
1808 columns.addColumn(column1);
1809 TableColumn column2 = new TableColumn(1, 600);
1810 column2.setHeaderValue("Value");
1811 columns.addColumn(column2);
1812
1813 TableModel model = new AbstractTableModel()
1814 {
1815
1816 private static final long serialVersionUID = 20240314L;
1817
1818
1819 @Override
1820 public int getRowCount()
1821 {
1822 return OtsEditor.this.properties.size() / 2;
1823 }
1824
1825
1826 @Override
1827 public int getColumnCount()
1828 {
1829 return 2;
1830 }
1831
1832
1833 @Override
1834 public Object getValueAt(final int rowIndex, final int columnIndex)
1835 {
1836 return OtsEditor.this.properties.get(rowIndex * 2 + columnIndex);
1837 }
1838
1839
1840 @Override
1841 public boolean isCellEditable(final int rowIndex, final int columnIndex)
1842 {
1843 return columnIndex == 1;
1844 }
1845
1846
1847 @Override
1848 public void setValueAt(final Object aValue, final int rowIndex, final int columnIndex)
1849 {
1850 OtsEditor.this.properties.set(rowIndex * 2 + columnIndex, aValue.toString());
1851 OtsEditor.this.setUnsavedChanges(true);
1852 }
1853 };
1854
1855 JTable table = new JTable(model, columns);
1856 DefaultTableCellRenderer renderer = new DefaultTableCellRenderer()
1857 {
1858
1859 private static final long serialVersionUID = 20240314L;
1860
1861
1862 @Override
1863 public Component getTableCellRendererComponent(final JTable table, final Object value, final boolean isSelected,
1864 final boolean hasFocus, final int row, final int column)
1865 {
1866 Component component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
1867 if (table.convertColumnIndexToModel(column) == 0)
1868 {
1869 this.setOpaque(true);
1870 }
1871 else
1872 {
1873 this.setOpaque(false);
1874 }
1875 return component;
1876 }
1877 };
1878 renderer.setBackground(panel.getBackground());
1879 table.setDefaultRenderer(Object.class, renderer);
1880 JScrollPane scroll = new JScrollPane(table);
1881 scroll.setBorder(new EmptyBorder(0, 0, 0, 0));
1882 panel.add(scroll, BorderLayout.CENTER);
1883
1884 final JDialog propertyWindow = new JDialog(this, "Properties", true);
1885 propertyWindow.setMinimumSize(new Dimension(250, 150));
1886 propertyWindow.setPreferredSize(new Dimension(800, 200));
1887
1888 JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT));
1889 JButton defaults = new JButton("Set defaults...");
1890 defaults.addActionListener((a) ->
1891 {
1892 boolean set = JOptionPane.showConfirmDialog(propertyWindow, "Are you sure? This will reset all properties.",
1893 "Are you sure?", JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE,
1894 this.questionIcon) == JOptionPane.OK_OPTION;
1895 if (set)
1896 {
1897 setDefaultProperties();
1898 table.updateUI();
1899 OtsEditor.this.setUnsavedChanges(true);
1900 }
1901 });
1902 buttons.add(defaults);
1903 JButton ok = new JButton("Ok");
1904 ok.addActionListener((a) ->
1905 {
1906 table.getDefaultEditor(Object.class).stopCellEditing();
1907 propertyWindow.dispose();
1908 });
1909 buttons.add(ok);
1910 panel.add(buttons, BorderLayout.PAGE_END);
1911
1912 propertyWindow.getContentPane().add(panel);
1913 propertyWindow.pack();
1914 propertyWindow.setLocationRelativeTo(this);
1915 propertyWindow.setVisible(true);
1916 }
1917
1918
1919
1920
1921 private void exit()
1922 {
1923 if (confirmDiscardChanges())
1924 {
1925 System.exit(0);
1926 }
1927 }
1928
1929
1930
1931
1932
1933
1934
1935 public static String limitTooltip(final String message)
1936 {
1937 if (message == null)
1938 {
1939 return null;
1940 }
1941 if (message.length() < MAX_TOOLTIP_LENGTH)
1942 {
1943 return message;
1944 }
1945 return message.substring(0, MAX_TOOLTIP_LENGTH - 3) + "...";
1946 }
1947
1948
1949
1950
1951
1952
1953
1954 private static List<String> filterOptions(final List<String> options, final String currentValue)
1955 {
1956 return options.stream().filter((val) -> currentValue == null || currentValue.isEmpty() || val.startsWith(currentValue))
1957 .distinct().sorted().collect(Collectors.toList());
1958 }
1959
1960
1961
1962
1963
1964 public void addAttributeCellEditorListener(final CellEditorListener listener)
1965 {
1966 this.attributesTable.getDefaultEditor(String.class).addCellEditorListener(listener);
1967 }
1968
1969
1970
1971
1972
1973
1974 public void setClipboard(final XsdTreeNode clipboard, final boolean cut)
1975 {
1976 this.clipboard = clipboard;
1977 this.cut = cut;
1978 }
1979
1980
1981
1982
1983
1984 public XsdTreeNode getClipboard()
1985 {
1986 return this.clipboard;
1987 }
1988
1989
1990
1991
1992 public void removeClipboardWhenCut()
1993 {
1994 if (this.clipboard != null && this.cut)
1995 {
1996 if (this.clipboard.isRemovable())
1997 {
1998 this.clipboard.remove();
1999 }
2000 this.clipboard = null;
2001 }
2002 }
2003
2004
2005
2006
2007
2008 public NodeActions getNodeActions()
2009 {
2010 return this.nodeActions;
2011 }
2012
2013
2014 @Override
2015 public boolean addListener(final EventListener listener, final EventType eventType)
2016 {
2017 return Try.assign(() -> EventProducer.super.addListener(listener, eventType),
2018 "Local event producer should not give a RemoteException.");
2019 }
2020
2021
2022
2023
2024
2025
2026 public Eval getEval()
2027 {
2028 try
2029 {
2030 return this.evalWrapper.getEval(OtsEditor.this.scenario.getItemAt(OtsEditor.this.scenario.getSelectedIndex()));
2031 }
2032 catch (CircularDependencyException ex)
2033 {
2034 showCircularInputParameters();
2035 return this.evalWrapper.getLastValidEval();
2036 }
2037 catch (RuntimeException ex)
2038 {
2039
2040 return this.evalWrapper.getLastValidEval();
2041 }
2042 }
2043
2044
2045
2046
2047
2048 public void addEvalListener(final EvalListener listener)
2049 {
2050 this.evalWrapper.addListener(listener);
2051 }
2052
2053
2054
2055
2056
2057 public void removeEvalListener(final EvalListener listener)
2058 {
2059 this.evalWrapper.removeListener(listener);
2060 }
2061
2062
2063 @Override
2064 public void setAppearance(final Appearance appearance)
2065 {
2066 super.setAppearance(appearance);
2067
2068 if (this.treeTable != null)
2069 {
2070 changeFont((Component) this.treeTable.getTree().getCellRenderer(), appearance.getFont());
2071 changeFontSize((Component) this.treeTable.getTree().getCellRenderer());
2072 }
2073 }
2074
2075 }