View Javadoc
1   package org.opentrafficsim.editor;
2   
3   import java.awt.event.ActionEvent;
4   import java.util.List;
5   
6   import javax.swing.AbstractAction;
7   import javax.swing.Action;
8   import javax.swing.JComponent;
9   import javax.swing.JTable;
10  import javax.swing.KeyStroke;
11  import javax.swing.event.DocumentEvent;
12  import javax.swing.event.DocumentListener;
13  import javax.swing.tree.TreePath;
14  
15  import org.opentrafficsim.editor.DocumentReader.NodeAnnotation;
16  
17  import de.javagl.treetable.JTreeTable;
18  
19  /**
20   * This class houses all the actions that are made available on the editor. They can be bound to a component with a key-stroke
21   * using {@link Actions#bind(JComponent, int, Action)}, or they can be programmatically invoked using
22   * {@link Actions#invoke(Action)}.
23   * <p>
24   * Copyright (c) 2026-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
25   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
26   * </p>
27   * @author Wouter Schakel
28   */
29  public class Actions
30  {
31  
32      /** Editor. */
33      private final OtsEditor editor;
34  
35      /** Tree table. */
36      private JTreeTable treeTable;
37  
38      /** Attributes table. */
39      private final JTable attributesTable;
40  
41      /** Actions (methods) to perform on nodes. */
42      private NodeActions nodeActions;
43  
44      /**
45       * Constructor.
46       * @param editor editor
47       * @param attributesTable attributes table
48       */
49      public Actions(final OtsEditor editor, final JTable attributesTable)
50      {
51          this.editor = editor;
52          this.attributesTable = attributesTable;
53      }
54  
55      /**
56       * Sets a new tree table as a new file is created or loaded.
57       * @param treeTable tree table
58       */
59      public void setTreeTable(final JTreeTable treeTable)
60      {
61          this.treeTable = treeTable;
62          this.nodeActions = new NodeActions(this.editor, this.treeTable);
63      }
64  
65      /**
66       * Returns the node actions.
67       * @return node actions
68       */
69      public NodeActions getNodeActions()
70      {
71          return this.nodeActions;
72      }
73  
74      // ----- Static helper methods. ----- //
75  
76      /**
77       * Binds a key stroke to an action on the component. The key stroke is taken from the action's
78       * {@code Action.ACCELERATOR_KEY} value.
79       * @param component component
80       * @param state int, for example JComponent.WHEN_FOCUSED
81       * @param action action
82       */
83      public static void bind(final JComponent component, final int state, final Action action)
84      {
85          // the action itself is used as a consistent and unique key that the input map and action map share
86          component.getInputMap(state).put((KeyStroke) action.getValue(Action.ACCELERATOR_KEY), action);
87          component.getActionMap().put(action, action);
88      }
89  
90      /**
91       * Invoke action programmatically. If the action is disabled it will be ignored.
92       * @param action action to invoke
93       */
94      public static void invoke(final Action action)
95      {
96          if (action != null && action.isEnabled())
97          {
98              action.actionPerformed(new ActionEvent(action, ActionEvent.ACTION_PERFORMED, null));
99          }
100     }
101 
102     /**
103      * Returns a document listener that executes the runnable on any change.
104      * @param runnable runnable to run on any change
105      * @return document listener that executes the runnable on any change
106      */
107     public static DocumentListener documentListener(final Runnable runnable)
108     {
109         return new DocumentListener()
110         {
111             @Override
112             public void insertUpdate(final DocumentEvent e)
113             {
114                 runnable.run();
115             }
116 
117             @Override
118             public void removeUpdate(final DocumentEvent e)
119             {
120                 runnable.run();
121             }
122 
123             @Override
124             public void changedUpdate(final DocumentEvent e)
125             {
126                 // this is only used for non-plain text changes
127             }
128         };
129     }
130 
131     // ----- Helper methods for the actions. ----- //
132 
133     /**
134      * Returns the selected tree node.
135      * @return selected tree node
136      */
137     private XsdTreeNode getSelectedTreeNode()
138     {
139         return (XsdTreeNode) this.treeTable.getTree().getSelectionPath().getLastPathComponent();
140     }
141 
142     /**
143      * Returns the selected attributes node.
144      * @return selected attributes node
145      */
146     private XsdTreeNode getSelectedAttributeNode()
147     {
148         // this should always be the same as the selected tree node, but is a bit more robust for attribute events
149         return ((AttributesTableModel) Actions.this.attributesTable.getModel()).getNode();
150     }
151 
152     // ----- Actions; pairs of methods to obtain them and private final fields that store them. ----- //
153 
154     /**
155      * Action to show the description of a tree node. Ignored of the node does not have a description.
156      * @return action to show the description of a tree node
157      */
158     public Action showTreeNodeDescription()
159     {
160         return this.showTreeNodeDescriptionAction;
161     }
162 
163     /** Action to show the description of a tree node. */
164     private final Action showTreeNodeDescriptionAction = new AbstractAction("Description...")
165     {
166         {
167             putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("pressed F1"));
168         }
169 
170         @Override
171         public void actionPerformed(final ActionEvent e)
172         {
173             XsdTreeNode node = getSelectedTreeNode();
174             node.getDescription().ifPresent((d) -> Actions.this.editor.dialogs().showDescription(d, node.getNodeName()));
175         }
176     };
177 
178     /**
179      * Action to show the invalid message on a tree node. Ignored if the node is valid.
180      * @return action to show the invalid message on a tree node
181      */
182     public Action showTreeNodeInvalid()
183     {
184         return this.showTreeNodeInvalidAction;
185     }
186 
187     /** Action to show the invalid message on a tree node. */
188     private final Action showTreeNodeInvalidAction = new AbstractAction("Invalid...")
189     {
190         {
191             putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("pressed F2"));
192         }
193 
194         @Override
195         public void actionPerformed(final ActionEvent e)
196         {
197             XsdTreeNode node = getSelectedTreeNode();
198             String status = node.isSelfValid() ? null : node.reportInvalidNode()
199                     .orElseGet(() -> node.reportInvalidValue().orElseGet(() -> node.reportInvalidId().orElse(null)));
200             if (status != null)
201             {
202                 Actions.this.editor.dialogs().showInvalidMessage(status, node.getNodeName());
203             }
204         }
205     };
206 
207     /**
208      * Action to show the description of a tree node. Ignored of the node does not have a description.
209      * @return action to show the description of a tree node
210      */
211     public Action showAttributeDescription()
212     {
213         return this.showAttributeDescriptionAction;
214     }
215 
216     /** Action to show the description of an attribute. */
217     private final Action showAttributeDescriptionAction = new AbstractAction("Description...")
218     {
219         {
220             putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("pressed F1"));
221         }
222 
223         @Override
224         public void actionPerformed(final ActionEvent e)
225         {
226             XsdTreeNode node = getSelectedAttributeNode();
227             int attribute = Actions.this.attributesTable.getSelectedRow();
228             NodeAnnotation.DESCRIPTION.get(node.getAttributeNode(attribute)).ifPresent(
229                     (d) -> Actions.this.editor.dialogs().showDescription(d, node.getAttributeNameByIndex(attribute)));
230         }
231     };
232 
233     /**
234      * Action to show the invalid message on an attribute. Ignored if the attribute is valid.
235      * @return action to show the invalid message on an attribute
236      */
237     public Action showAttributeInvalid()
238     {
239         return this.showAttributeInvalidAction;
240     }
241 
242     /** Action to show the invalid message on an attribute. */
243     private final Action showAttributeInvalidAction = new AbstractAction("Invalid...")
244     {
245         {
246             putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("pressed F2"));
247         }
248 
249         @Override
250         public void actionPerformed(final ActionEvent e)
251         {
252             XsdTreeNode node = getSelectedAttributeNode();
253             int attribute = Actions.this.attributesTable.getSelectedRow();
254             node.reportInvalidAttributeValue(attribute).ifPresent(
255                     (m) -> Actions.this.editor.dialogs().showInvalidMessage(m, node.getAttributeNameByIndex(attribute)));
256         }
257     };
258 
259     /**
260      * Action to add node.
261      * @return action to add node
262      */
263     public Action addNode()
264     {
265         return this.addNodeAction;
266     }
267 
268     /** Action to add node. */
269     private final Action addNodeAction = new AbstractAction("Add")
270     {
271         {
272             putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("ctrl W"));
273         }
274 
275         @Override
276         public void actionPerformed(final ActionEvent e)
277         {
278             XsdTreeNode node = getSelectedTreeNode();
279             if (node.isAddable())
280             {
281                 getNodeActions().add(node);
282             }
283         }
284     };
285 
286     /**
287      * Action to duplicate node.
288      * @return action to duplicate node
289      */
290     public Action duplicateNode()
291     {
292         return this.duplicateNodeAction;
293     }
294 
295     /** Action to duplicate node. */
296     private final Action duplicateNodeAction = new AbstractAction("Duplicate")
297     {
298         {
299             putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("ctrl D"));
300         }
301 
302         @Override
303         public void actionPerformed(final ActionEvent e)
304         {
305             XsdTreeNode node = getSelectedTreeNode();
306             if (node.isAddable())
307             {
308                 getNodeActions().duplicate(node);
309             }
310         }
311     };
312 
313     /**
314      * Action to delete node.
315      * @return action to delete node
316      */
317     public Action deleteNode()
318     {
319         return this.deleteNodeAction;
320     }
321 
322     /** Action to delete node. */
323     private final Action deleteNodeAction = new AbstractAction("Remove")
324     {
325         {
326             putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("DELETE"));
327         }
328 
329         @Override
330         public void actionPerformed(final ActionEvent e)
331         {
332             XsdTreeNode node = getSelectedTreeNode();
333             if (node.isRemovable())
334             {
335                 if (Actions.this.editor.dialogs().confirmNodeRemoval(node))
336                 {
337                     getNodeActions().remove(node);
338                 }
339             }
340         }
341     };
342 
343     /**
344      * Action to copy node.
345      * @return action to copy node
346      */
347     public Action copyNode()
348     {
349         return this.copyNodeAction;
350     }
351 
352     /** Action to copy node. */
353     private final Action copyNodeAction = new AbstractAction("Copy")
354     {
355         {
356             putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("ctrl C"));
357         }
358 
359         @Override
360         public void actionPerformed(final ActionEvent e)
361         {
362             XsdTreeNode node = getSelectedTreeNode();
363             if (node.isActive() && (node.isRemovable() || node.isAddable()))
364             {
365                 getNodeActions().copy(node);
366             }
367         }
368     };
369 
370     /**
371      * Action to cut node.
372      * @return action to cut node
373      */
374     public Action cutNode()
375     {
376         return this.cutNodeAction;
377     }
378 
379     /** Action to cut node. */
380     private final Action cutNodeAction = new AbstractAction("Cut")
381     {
382         {
383             putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("ctrl X"));
384         }
385 
386         @Override
387         public void actionPerformed(final ActionEvent e)
388         {
389             XsdTreeNode node = getSelectedTreeNode();
390             if (node.isActive() && node.isRemovable())
391             {
392                 getNodeActions().cut(node);
393             }
394         }
395     };
396 
397     /**
398      * Action to insert node.
399      * @return action to insert node
400      */
401     public Action insertNode()
402     {
403         return this.insertNodeAction;
404     }
405 
406     /** Action to insert node. */
407     private final Action insertNodeAction = new AbstractAction("Insert")
408     {
409         {
410             putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("INSERT"));
411         }
412 
413         @Override
414         public void actionPerformed(final ActionEvent e)
415         {
416             XsdTreeNode node = getSelectedTreeNode();
417             if (Actions.this.editor.getClipboard() != null && node.canContain(Actions.this.editor.getClipboard())
418                     && node.isActive() && node.isAddable())
419             {
420                 getNodeActions().insert(node);
421             }
422         }
423     };
424 
425     /**
426      * Action to paste node.
427      * @return action to paste node
428      */
429     public Action pasteNode()
430     {
431         return this.pasteNodeAction;
432     }
433 
434     /** Action to paste node. */
435     private final Action pasteNodeAction = new AbstractAction("Paste")
436     {
437         {
438             putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("ctrl V"));
439         }
440 
441         @Override
442         public void actionPerformed(final ActionEvent e)
443         {
444             XsdTreeNode node = getSelectedTreeNode();
445             if (Actions.this.editor.getClipboard() != null && node.canContain(Actions.this.editor.getClipboard())
446                     && (!node.isActive() || node.isAddable()))
447             {
448                 getNodeActions().paste(node);
449             }
450         }
451     };
452 
453     /**
454      * Action to revolve node.
455      * @return action to revolve node
456      */
457     public Action revolveNode()
458     {
459         return this.revolveNodeAction;
460     }
461 
462     /** Action to revolve node. */
463     private final Action revolveNodeAction = new AbstractAction("Revolve option")
464     {
465         {
466             putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("ctrl R"));
467         }
468 
469         @Override
470         public void actionPerformed(final ActionEvent e)
471         {
472             XsdTreeNode node = getSelectedTreeNode();
473             List<XsdOption> options = node.getOptions();
474             if (node.isChoice() && options.size() > 1)
475             {
476                 getNodeActions().revolveOption(node, options);
477             }
478         }
479     };
480 
481     /**
482      * Action to collapse all.
483      * @return action to collapse all
484      */
485     public Action collapseAll()
486     {
487         return this.collapseAllAction;
488     }
489 
490     /** Action to collapse all. */
491     private final Action collapseAllAction = new AbstractAction("Collapse all")
492     {
493         @Override
494         public void actionPerformed(final ActionEvent e)
495         {
496             for (int i = Actions.this.treeTable.getTree().getRowCount() - 1; i > 0; i--)
497             {
498                 Actions.this.treeTable.getTree().collapseRow(i);
499             }
500         }
501     };
502 
503     /**
504      * Action to expand or collapse node.
505      * @return action to expand or collapse node
506      */
507     public Action expandOrCollapseNode()
508     {
509         return this.expandOrCollapseNodeAction;
510     }
511 
512     /** Action to collapse node. */
513     private final Action expandOrCollapseNodeAction = new AbstractAction()
514     {
515         // name is set by XsdTreeListener as it depends on node expanded status
516         {
517             putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("ctrl E"));
518         }
519 
520         @Override
521         public void actionPerformed(final ActionEvent e)
522         {
523             TreePath path = Actions.this.treeTable.getTree().getSelectionPath();
524             XsdTreeNode node = (XsdTreeNode) path.getLastPathComponent();
525             boolean expanded = Actions.this.treeTable.getTree().isExpanded(path);
526             getNodeActions().expand(node, path, expanded);
527         }
528     };
529 
530     /**
531      * Action to move node up.
532      * @return action to move node up
533      */
534     public Action moveNodeUp()
535     {
536         return this.moveNodeUpAction;
537     }
538 
539     /** Action to move node up. */
540     private final Action moveNodeUpAction = new AbstractAction("Move up")
541     {
542         {
543             putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("ctrl UP"));
544         }
545 
546         @Override
547         public void actionPerformed(final ActionEvent e)
548         {
549             XsdTreeNode node = getSelectedTreeNode();
550             if (node.canMoveUp())
551             {
552                 getNodeActions().move(node, -1);
553             }
554         }
555     };
556 
557     /**
558      * Action to move node down.
559      * @return action to move node down
560      */
561     public Action moveNodeDown()
562     {
563         return this.moveNodeDownAction;
564     }
565 
566     /** Action to move node down. */
567     private final Action moveNodeDownAction = new AbstractAction("Move down")
568     {
569         {
570             putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("ctrl DOWN"));
571         }
572 
573         @Override
574         public void actionPerformed(final ActionEvent e)
575         {
576             XsdTreeNode node = getSelectedTreeNode();
577             if (node.canMoveDown())
578             {
579                 getNodeActions().move(node, 1);
580             }
581         }
582     };
583 
584 }