source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/CommandStackDialog.java

Last change on this file was 19050, checked in by taylor.smock, 2 years ago

Revert most var changes from r19048, fix most new compile warnings and checkstyle issues

Also, document why various ErrorProne checks were originally disabled and fix
generic SonarLint issues.

  • Property svn:eol-style set to native
File size: 22.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.dialogs;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Component;
7import java.awt.Dimension;
8import java.awt.GridBagConstraints;
9import java.awt.GridBagLayout;
10import java.awt.event.ActionEvent;
11import java.awt.event.KeyEvent;
12import java.awt.event.MouseEvent;
13import java.util.ArrayList;
14import java.util.Arrays;
15import java.util.Collection;
16import java.util.LinkedHashSet;
17import java.util.List;
18import java.util.Set;
19
20import javax.swing.Box;
21import javax.swing.JComponent;
22import javax.swing.JLabel;
23import javax.swing.JPanel;
24import javax.swing.JPopupMenu;
25import javax.swing.JScrollPane;
26import javax.swing.JSeparator;
27import javax.swing.JTree;
28import javax.swing.event.TreeModelEvent;
29import javax.swing.event.TreeModelListener;
30import javax.swing.event.TreeSelectionEvent;
31import javax.swing.event.TreeSelectionListener;
32import javax.swing.tree.DefaultMutableTreeNode;
33import javax.swing.tree.DefaultTreeCellRenderer;
34import javax.swing.tree.DefaultTreeModel;
35import javax.swing.tree.MutableTreeNode;
36import javax.swing.tree.TreePath;
37import javax.swing.tree.TreeSelectionModel;
38
39import org.openstreetmap.josm.actions.AutoScaleAction;
40import org.openstreetmap.josm.actions.AutoScaleAction.AutoScaleMode;
41import org.openstreetmap.josm.actions.JosmAction;
42import org.openstreetmap.josm.command.Command;
43import org.openstreetmap.josm.command.PseudoCommand;
44import org.openstreetmap.josm.data.UndoRedoHandler;
45import org.openstreetmap.josm.data.UndoRedoHandler.CommandAddedEvent;
46import org.openstreetmap.josm.data.UndoRedoHandler.CommandQueueCleanedEvent;
47import org.openstreetmap.josm.data.UndoRedoHandler.CommandQueuePreciseListener;
48import org.openstreetmap.josm.data.UndoRedoHandler.CommandRedoneEvent;
49import org.openstreetmap.josm.data.UndoRedoHandler.CommandUndoneEvent;
50import org.openstreetmap.josm.data.osm.DataSet;
51import org.openstreetmap.josm.data.osm.OsmPrimitive;
52import org.openstreetmap.josm.gui.MainApplication;
53import org.openstreetmap.josm.gui.SideButton;
54import org.openstreetmap.josm.gui.layer.OsmDataLayer;
55import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
56import org.openstreetmap.josm.tools.GBC;
57import org.openstreetmap.josm.tools.InputMapUtils;
58import org.openstreetmap.josm.tools.Shortcut;
59import org.openstreetmap.josm.tools.SubclassFilteredCollection;
60
61/**
62 * Dialog displaying list of all executed commands (undo/redo buffer).
63 * @since 94
64 */
65public class CommandStackDialog extends ToggleDialog implements CommandQueuePreciseListener {
66
67 private final DefaultTreeModel undoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
68 private final DefaultTreeModel redoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
69
70 private final JTree undoTree = new JTree(undoTreeModel);
71 private final JTree redoTree = new JTree(redoTreeModel);
72
73 private DefaultMutableTreeNode undoRoot;
74 private DefaultMutableTreeNode redoRoot;
75
76 private final transient UndoRedoSelectionListener undoSelectionListener;
77 private final transient UndoRedoSelectionListener redoSelectionListener;
78
79 private final JScrollPane scrollPane;
80 private final JSeparator separator = new JSeparator();
81 // only visible, if separator is the top most component
82 private final Component spacer = Box.createRigidArea(new Dimension(0, 3));
83
84 // last operation is remembered to select the next undo/redo entry in the list
85 // after undo/redo command
86 private UndoRedoType lastOperation = UndoRedoType.UNDO;
87
88 // Actions for context menu and Enter key
89 private final SelectAction selectAction = new SelectAction();
90 private final SelectAndZoomAction selectAndZoomAction = new SelectAndZoomAction();
91
92 /**
93 * Constructs a new {@code CommandStackDialog}.
94 */
95 public CommandStackDialog() {
96 super(tr("Command Stack"), "commandstack", tr("Open a list of all commands (undo buffer)."),
97 Shortcut.registerShortcut("subwindow:commandstack", tr("Windows: {0}",
98 tr("Command Stack")), KeyEvent.VK_O, Shortcut.ALT_SHIFT), 100);
99 undoTree.addMouseListener(new MouseEventHandler());
100 undoTree.setRootVisible(false);
101 undoTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
102 undoTree.setShowsRootHandles(true);
103 undoTree.expandRow(0);
104 undoTree.setCellRenderer(new CommandCellRenderer());
105 undoSelectionListener = new UndoRedoSelectionListener(undoTree);
106 undoTree.getSelectionModel().addTreeSelectionListener(undoSelectionListener);
107 InputMapUtils.unassignCtrlShiftUpDown(undoTree, JComponent.WHEN_FOCUSED);
108
109 redoTree.addMouseListener(new MouseEventHandler());
110 redoTree.setRootVisible(false);
111 redoTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
112 redoTree.setShowsRootHandles(true);
113 redoTree.expandRow(0);
114 redoTree.setCellRenderer(new CommandCellRenderer());
115 redoSelectionListener = new UndoRedoSelectionListener(redoTree);
116 redoTree.getSelectionModel().addTreeSelectionListener(redoSelectionListener);
117 InputMapUtils.unassignCtrlShiftUpDown(redoTree, JComponent.WHEN_FOCUSED);
118
119 JPanel treesPanel = new JPanel(new GridBagLayout());
120
121 treesPanel.add(spacer, GBC.eol());
122 spacer.setVisible(false);
123 treesPanel.add(undoTree, GBC.eol().fill(GridBagConstraints.HORIZONTAL));
124 separator.setVisible(false);
125 treesPanel.add(separator, GBC.eol().fill(GridBagConstraints.HORIZONTAL));
126 treesPanel.add(redoTree, GBC.eol().fill(GridBagConstraints.HORIZONTAL));
127 treesPanel.add(Box.createRigidArea(new Dimension(0, 0)), GBC.std().weight(0, 1));
128 treesPanel.setBackground(redoTree.getBackground());
129
130 wireUpdateEnabledStateUpdater(selectAction, undoTree);
131 wireUpdateEnabledStateUpdater(selectAction, redoTree);
132
133 UndoRedoAction undoAction = new UndoRedoAction(UndoRedoType.UNDO);
134 wireUpdateEnabledStateUpdater(undoAction, undoTree);
135
136 UndoRedoAction redoAction = new UndoRedoAction(UndoRedoType.REDO);
137 wireUpdateEnabledStateUpdater(redoAction, redoTree);
138
139 scrollPane = (JScrollPane) createLayout(treesPanel, true, Arrays.asList(
140 new SideButton(selectAction),
141 new SideButton(undoAction),
142 new SideButton(redoAction)
143 ));
144
145 InputMapUtils.addEnterAction(undoTree, selectAndZoomAction);
146 InputMapUtils.addEnterAction(redoTree, selectAndZoomAction);
147 }
148
149 private static final class CommandCellRenderer extends DefaultTreeCellRenderer {
150 @Override
151 public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row,
152 boolean hasFocus) {
153 super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
154 DefaultMutableTreeNode v = (DefaultMutableTreeNode) value;
155 if (v.getUserObject() instanceof JLabel) {
156 JLabel l = (JLabel) v.getUserObject();
157 setIcon(l.getIcon());
158 setText(l.getText());
159 }
160 return this;
161 }
162 }
163
164 private void updateTitle() {
165 int undo = undoTreeModel.getChildCount(undoTreeModel.getRoot());
166 int redo = redoTreeModel.getChildCount(redoTreeModel.getRoot());
167 if (undo > 0 || redo > 0) {
168 setTitle(tr("Command Stack: Undo: {0} / Redo: {1}", undo, redo));
169 } else {
170 setTitle(tr("Command Stack"));
171 }
172 }
173
174 /**
175 * Selection listener for undo and redo area.
176 * If one is clicked, takes away the selection from the other, so
177 * it behaves as if it was one component.
178 */
179 private class UndoRedoSelectionListener implements TreeSelectionListener {
180 private final JTree source;
181
182 UndoRedoSelectionListener(JTree source) {
183 this.source = source;
184 }
185
186 @Override
187 public void valueChanged(TreeSelectionEvent e) {
188 if (source == undoTree) {
189 redoTree.getSelectionModel().removeTreeSelectionListener(redoSelectionListener);
190 redoTree.clearSelection();
191 redoTree.getSelectionModel().addTreeSelectionListener(redoSelectionListener);
192 }
193 if (source == redoTree) {
194 undoTree.getSelectionModel().removeTreeSelectionListener(undoSelectionListener);
195 undoTree.clearSelection();
196 undoTree.getSelectionModel().addTreeSelectionListener(undoSelectionListener);
197 }
198 }
199 }
200
201 /**
202 * Wires updater for enabled state to the events. Also updates dialog title if needed.
203 * @param updater updater
204 * @param tree tree on which wire updater
205 */
206 protected void wireUpdateEnabledStateUpdater(final IEnabledStateUpdating updater, JTree tree) {
207 addShowNotifyListener(updater);
208
209 tree.addTreeSelectionListener(e -> updater.updateEnabledState());
210
211 tree.getModel().addTreeModelListener(new TreeModelListener() {
212 @Override
213 public void treeNodesChanged(TreeModelEvent e) {
214 updater.updateEnabledState();
215 updateTitle();
216 }
217
218 @Override
219 public void treeNodesInserted(TreeModelEvent e) {
220 treeNodesChanged(e);
221 }
222
223 @Override
224 public void treeNodesRemoved(TreeModelEvent e) {
225 treeNodesChanged(e);
226 }
227
228 @Override
229 public void treeStructureChanged(TreeModelEvent e) {
230 treeNodesChanged(e);
231 }
232 });
233 }
234
235 @Override
236 public void showNotify() {
237 buildTrees();
238 for (IEnabledStateUpdating listener : showNotifyListener) {
239 listener.updateEnabledState();
240 }
241 UndoRedoHandler.getInstance().addCommandQueuePreciseListener(this);
242 }
243
244 /**
245 * Simple listener setup to update the button enabled state when the side dialog shows.
246 */
247 private final transient Set<IEnabledStateUpdating> showNotifyListener = new LinkedHashSet<>();
248
249 private void addShowNotifyListener(IEnabledStateUpdating listener) {
250 showNotifyListener.add(listener);
251 }
252
253 @Override
254 public void hideNotify() {
255 undoRoot = new DefaultMutableTreeNode();
256 redoRoot = new DefaultMutableTreeNode();
257 undoTreeModel.setRoot(undoRoot);
258 redoTreeModel.setRoot(redoRoot);
259 UndoRedoHandler.getInstance().removeCommandQueuePreciseListener(this);
260 }
261
262 /**
263 * Build the trees of undo and redo commands (initially or when
264 * they have changed).
265 */
266 private void buildTrees() {
267 setTitle(tr("Command Stack"));
268 buildUndoTree();
269 buildRedoTree();
270 ensureTreesConsistency();
271 }
272
273 private void buildUndoTree() {
274 List<Command> undoCommands = UndoRedoHandler.getInstance().getUndoCommands();
275 undoRoot = new DefaultMutableTreeNode();
276 for (Command undoCommand : undoCommands) {
277 undoRoot.add(getNodeForCommand(undoCommand));
278 }
279 undoTreeModel.setRoot(undoRoot);
280 }
281
282 private void buildRedoTree() {
283 List<Command> redoCommands = UndoRedoHandler.getInstance().getRedoCommands();
284 redoRoot = new DefaultMutableTreeNode();
285 for (Command redoCommand : redoCommands) {
286 redoRoot.add(getNodeForCommand(redoCommand));
287 }
288 redoTreeModel.setRoot(redoRoot);
289 }
290
291 private void ensureTreesConsistency() {
292 List<Command> undoCommands = UndoRedoHandler.getInstance().getUndoCommands();
293 List<Command> redoCommands = UndoRedoHandler.getInstance().getRedoCommands();
294 if (redoTreeModel.getChildCount(redoRoot) > 0) {
295 redoTree.scrollRowToVisible(0);
296 scrollPane.getHorizontalScrollBar().setValue(0);
297 }
298
299 separator.setVisible(!undoCommands.isEmpty() || !redoCommands.isEmpty());
300 spacer.setVisible(undoCommands.isEmpty() && !redoCommands.isEmpty());
301
302 // if one tree is empty, move selection to the other
303 switch (lastOperation) {
304 case UNDO:
305 if (undoCommands.isEmpty()) {
306 lastOperation = UndoRedoType.REDO;
307 }
308 break;
309 case REDO:
310 if (redoCommands.isEmpty()) {
311 lastOperation = UndoRedoType.UNDO;
312 }
313 break;
314 }
315
316 // select the next command to undo/redo
317 switch (lastOperation) {
318 case UNDO:
319 undoTree.setSelectionRow(undoTree.getRowCount()-1);
320 break;
321 case REDO:
322 redoTree.setSelectionRow(0);
323 break;
324 }
325
326 undoTree.scrollRowToVisible(undoTreeModel.getChildCount(undoRoot)-1);
327 scrollPane.getHorizontalScrollBar().setValue(0);
328 }
329
330 /**
331 * Wraps a command in a CommandListMutableTreeNode.
332 * Recursively adds child commands.
333 * @param c the command
334 * @return the resulting node
335 */
336 protected CommandListMutableTreeNode getNodeForCommand(PseudoCommand c) {
337 CommandListMutableTreeNode node = new CommandListMutableTreeNode(c);
338 if (c.getChildren() != null) {
339 List<PseudoCommand> children = new ArrayList<>(c.getChildren());
340 for (PseudoCommand child : children) {
341 node.add(getNodeForCommand(child));
342 }
343 }
344 return node;
345 }
346
347 /**
348 * Return primitives that are affected by some command
349 * @param c the command
350 * @return collection of affected primitives, only usable ones
351 */
352 protected static Collection<? extends OsmPrimitive> getAffectedPrimitives(PseudoCommand c) {
353 final OsmDataLayer currentLayer = MainApplication.getLayerManager().getEditLayer();
354 return new SubclassFilteredCollection<>(
355 c.getParticipatingPrimitives(),
356 o -> {
357 OsmPrimitive p = currentLayer.data.getPrimitiveById(o);
358 return p != null && p.isUsable();
359 }
360 );
361 }
362
363 protected boolean redoTreeIsEmpty() {
364 return redoTree.getRowCount() == 0;
365 }
366
367 @Override
368 public void cleaned(CommandQueueCleanedEvent e) {
369 if (isVisible()) {
370 buildTrees();
371 }
372 }
373
374 @Override
375 public void commandAdded(CommandAddedEvent e) {
376 if (isVisible()) {
377 undoRoot.add(getNodeForCommand(e.getCommand()));
378 undoTreeModel.nodeStructureChanged(undoRoot);
379 // fix 16911: make sure that redo tree is rebuild with empty list
380 if (!redoTreeIsEmpty())
381 buildRedoTree();
382 ensureTreesConsistency();
383 }
384 }
385
386 @Override
387 public void commandUndone(CommandUndoneEvent e) {
388 if (isVisible()) {
389 swapNode(undoTreeModel, undoRoot, undoRoot.getChildCount() - 1, redoTreeModel, redoRoot, 0);
390 }
391 }
392
393 @Override
394 public void commandRedone(CommandRedoneEvent e) {
395 if (isVisible()) {
396 swapNode(redoTreeModel, redoRoot, 0, undoTreeModel, undoRoot, undoRoot.getChildCount());
397 }
398 }
399
400 private void swapNode(DefaultTreeModel srcModel, DefaultMutableTreeNode srcRoot, int srcIndex,
401 DefaultTreeModel dstModel, DefaultMutableTreeNode dstRoot, int dstIndex) {
402 MutableTreeNode node = (MutableTreeNode) srcRoot.getChildAt(srcIndex);
403 srcRoot.remove(node);
404 srcModel.nodeStructureChanged(srcRoot);
405 dstRoot.insert(node, dstIndex);
406 dstModel.nodeStructureChanged(dstRoot);
407 ensureTreesConsistency();
408 }
409
410 /**
411 * Action that selects the objects that take part in a command.
412 */
413 public class SelectAction extends JosmAction implements IEnabledStateUpdating {
414
415 /**
416 * Constructs a new {@code SelectAction}.
417 */
418 public SelectAction() {
419 this(tr("Select"), "dialogs/select", tr("Selects the objects that take part in this command (unless currently deleted)"),
420 Shortcut.registerShortcut("command:stack:select", tr("Command Stack: Select"), KeyEvent.VK_UNDEFINED, Shortcut.NONE),
421 false, null, false);
422 }
423
424 /**
425 * Constructs a new {@code SelectAction} that calls
426 * {@link JosmAction#JosmAction(String, String, String, Shortcut, boolean, String, boolean)}
427 *
428 * The new super for all CommandStack actions.
429 *
430 * Use this super constructor to setup your action.
431 *
432 * @param name the action's text as displayed on the menu (if it is added to a menu)
433 * @param iconName the filename of the icon to use
434 * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note
435 * that html is not supported for menu actions on some platforms.
436 * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always
437 * do want a shortcut, remember you can always register it with group=none, so you
438 * won't be assigned a shortcut unless the user configures one. If you pass null here,
439 * the user CANNOT configure a shortcut for your action.
440 * @param registerInToolbar register this action for the toolbar preferences?
441 * @param toolbarId identifier for the toolbar preferences. The iconName is used, if this parameter is null
442 * @param installAdapters false, if you don't want to install layer changed and selection changed adapters
443 */
444 protected SelectAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar,
445 String toolbarId, boolean installAdapters) {
446 super(name, iconName, tooltip, shortcut, registerInToolbar, toolbarId, installAdapters);
447 }
448
449 @Override
450 public void actionPerformed(ActionEvent e) {
451 PseudoCommand command = getSelectedCommand();
452 if (command == null) {
453 return;
454 }
455
456 DataSet dataSet = MainApplication.getLayerManager().getEditDataSet();
457 if (dataSet == null) return;
458 dataSet.setSelected(getAffectedPrimitives(command));
459 }
460
461 @Override
462 public void updateEnabledState() {
463 setEnabled(!undoTree.isSelectionEmpty() || !redoTree.isSelectionEmpty());
464 }
465 }
466
467 /**
468 * Returns the selected undo/redo command
469 * @return the selected undo/redo command or {@code null}
470 */
471 public PseudoCommand getSelectedCommand() {
472 TreePath path;
473 if (!undoTree.isSelectionEmpty()) {
474 path = undoTree.getSelectionPath();
475 } else if (!redoTree.isSelectionEmpty()) {
476 path = redoTree.getSelectionPath();
477 } else {
478 // see #19514 for a possible cause
479 return null;
480 }
481 return path != null ? ((CommandListMutableTreeNode) path.getLastPathComponent()).getCommand() : null;
482 }
483
484 /**
485 * Action that selects the objects that take part in a command, then zoom to them.
486 */
487 public class SelectAndZoomAction extends SelectAction {
488 /**
489 * Constructs a new {@code SelectAndZoomAction}.
490 */
491 public SelectAndZoomAction() {
492 super(tr("Select and zoom"), "dialogs/autoscale/selection",
493 tr("Selects the objects that take part in this command (unless currently deleted), then and zooms to it"),
494 Shortcut.registerShortcut("command:stack:select_and_zoom", tr("Command Stack: Select and zoom"),
495 KeyEvent.VK_UNDEFINED, Shortcut.NONE), false, null, false);
496 }
497
498 @Override
499 public void actionPerformed(ActionEvent e) {
500 super.actionPerformed(e);
501 AutoScaleAction.autoScale(AutoScaleMode.SELECTION);
502 }
503 }
504
505 /**
506 * undo / redo switch to reduce duplicate code
507 */
508 protected enum UndoRedoType {
509 UNDO,
510 REDO
511 }
512
513 /**
514 * Action to undo or redo all commands up to (and including) the selected item.
515 */
516 protected class UndoRedoAction extends JosmAction implements IEnabledStateUpdating {
517 private final UndoRedoType type;
518 private final JTree tree;
519
520 /**
521 * constructor
522 * @param type decide whether it is an undo action or a redo action
523 */
524 public UndoRedoAction(UndoRedoType type) {
525 // This is really annoying. JEP 8300786 might fix this.
526 super(UndoRedoType.UNDO == type ? tr("Undo") : tr("Redo"),
527 UndoRedoType.UNDO == type ? "undo" : "redo",
528 UndoRedoType.UNDO == type ? tr("Undo the selected and all later commands")
529 : tr("Redo the selected and all earlier commands"),
530 UndoRedoType.UNDO == type
531 ? Shortcut.registerShortcut("command:stack:undo", tr("Command Stack: Undo"), KeyEvent.VK_UNDEFINED, Shortcut.NONE)
532 : Shortcut.registerShortcut("command:stack:redo", tr("Command Stack: Redo"), KeyEvent.VK_UNDEFINED, Shortcut.NONE),
533 false, false);
534 this.type = type;
535 if (UndoRedoType.UNDO == type) {
536 tree = undoTree;
537 } else {
538 tree = redoTree;
539 }
540 }
541
542 @Override
543 public void actionPerformed(ActionEvent e) {
544 lastOperation = type;
545 TreePath path = tree.getSelectionPath();
546
547 // we can only undo top level commands
548 if (path.getPathCount() != 2)
549 throw new IllegalStateException();
550
551 int idx = ((CommandListMutableTreeNode) path.getLastPathComponent()).getIndex();
552
553 // calculate the number of commands to undo/redo; then do it
554 switch (type) {
555 case UNDO:
556 int numUndo = ((DefaultMutableTreeNode) undoTreeModel.getRoot()).getChildCount() - idx;
557 UndoRedoHandler.getInstance().undo(numUndo);
558 break;
559 case REDO:
560 int numRedo = idx+1;
561 UndoRedoHandler.getInstance().redo(numRedo);
562 break;
563 }
564 MainApplication.getMap().repaint();
565 }
566
567 @Override
568 public void updateEnabledState() {
569 // do not allow execution if nothing is selected or a sub command was selected
570 setEnabled(!tree.isSelectionEmpty() && tree.getSelectionPath().getPathCount() == 2);
571 }
572 }
573
574 class MouseEventHandler extends PopupMenuLauncher {
575
576 MouseEventHandler() {
577 super(new CommandStackPopup());
578 }
579
580 @Override
581 public void mouseClicked(MouseEvent evt) {
582 if (isDoubleClick(evt)) {
583 selectAndZoomAction.actionPerformed(null);
584 }
585 }
586 }
587
588 private class CommandStackPopup extends JPopupMenu {
589 CommandStackPopup() {
590 add(selectAction);
591 add(selectAndZoomAction);
592 }
593 }
594}
Note: See TracBrowser for help on using the repository browser.