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

Last change on this file was 18801, checked in by taylor.smock, 8 months ago

Fix #22832: Code cleanup and some simplification, documentation fixes (patch by gaben)

There should not be any functional changes in this patch; it is intended to do
the following:

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