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

Last change on this file was 19050, checked in by taylor.smock, 27 hours 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.