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

Last change on this file since 15889 was 14562, checked in by GerdP, 5 years ago

fix #16911 regression from r13729 : make sure that redo tree is cleared when a command is added after one or more undos

  • Property svn:eol-style set to native
File size: 20.5 KB
RevLine 
[6380]1// License: GPL. For details, see LICENSE file.
[626]2package org.openstreetmap.josm.gui.dialogs;
3
[301]4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Component;
[3262]7import java.awt.Dimension;
8import java.awt.GridBagLayout;
9import java.awt.event.ActionEvent;
[301]10import java.awt.event.KeyEvent;
[3262]11import java.awt.event.MouseEvent;
12import java.util.ArrayList;
[4363]13import java.util.Arrays;
[10788]14import java.util.Collection;
[3262]15import java.util.LinkedHashSet;
16import java.util.List;
17import java.util.Set;
18
19import javax.swing.AbstractAction;
20import javax.swing.Box;
[5200]21import javax.swing.JComponent;
[301]22import javax.swing.JLabel;
[3262]23import javax.swing.JPanel;
24import javax.swing.JPopupMenu;
[301]25import javax.swing.JScrollPane;
[3262]26import javax.swing.JSeparator;
[301]27import javax.swing.JTree;
[3262]28import javax.swing.event.TreeModelEvent;
29import javax.swing.event.TreeModelListener;
30import javax.swing.event.TreeSelectionEvent;
31import javax.swing.event.TreeSelectionListener;
[301]32import javax.swing.tree.DefaultMutableTreeNode;
33import javax.swing.tree.DefaultTreeCellRenderer;
34import javax.swing.tree.DefaultTreeModel;
[13729]35import javax.swing.tree.MutableTreeNode;
[3262]36import javax.swing.tree.TreePath;
37import javax.swing.tree.TreeSelectionModel;
[301]38
[5727]39import org.openstreetmap.josm.actions.AutoScaleAction;
[14221]40import org.openstreetmap.josm.actions.AutoScaleAction.AutoScaleMode;
[301]41import org.openstreetmap.josm.command.Command;
[3262]42import org.openstreetmap.josm.command.PseudoCommand;
[14134]43import org.openstreetmap.josm.data.UndoRedoHandler;
[13729]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;
[10453]49import org.openstreetmap.josm.data.osm.DataSet;
[3262]50import org.openstreetmap.josm.data.osm.OsmPrimitive;
[12630]51import org.openstreetmap.josm.gui.MainApplication;
[3262]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.ImageProvider;
[5200]57import org.openstreetmap.josm.tools.InputMapUtils;
[1084]58import org.openstreetmap.josm.tools.Shortcut;
[10788]59import org.openstreetmap.josm.tools.SubclassFilteredCollection;
[626]60
[7398]61/**
62 * Dialog displaying list of all executed commands (undo/redo buffer).
63 * @since 94
64 */
[13729]65public class CommandStackDialog extends ToggleDialog implements CommandQueuePreciseListener {
[626]66
[7398]67 private final DefaultTreeModel undoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
68 private final DefaultTreeModel redoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
[626]69
[7398]70 private final JTree undoTree = new JTree(undoTreeModel);
71 private final JTree redoTree = new JTree(redoTreeModel);
[3262]72
[13729]73 private DefaultMutableTreeNode undoRoot;
74 private DefaultMutableTreeNode redoRoot;
75
[9078]76 private final transient UndoRedoSelectionListener undoSelectionListener;
77 private final transient UndoRedoSelectionListener redoSelectionListener;
[3262]78
[9078]79 private final JScrollPane scrollPane;
80 private final JSeparator separator = new JSeparator();
[3262]81 // only visible, if separator is the top most component
[9078]82 private final Component spacer = Box.createRigidArea(new Dimension(0, 3));
[3262]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
[5727]88 // Actions for context menu and Enter key
[9078]89 private final SelectAction selectAction = new SelectAction();
90 private final SelectAndZoomAction selectAndZoomAction = new SelectAndZoomAction();
[6070]91
[6361]92 /**
93 * Constructs a new {@code CommandStackDialog}.
94 */
95 public CommandStackDialog() {
[1169]96 super(tr("Command Stack"), "commandstack", tr("Open a list of all commands (undo buffer)."),
[4958]97 Shortcut.registerShortcut("subwindow:commandstack", tr("Toggle: {0}",
[6726]98 tr("Command Stack")), KeyEvent.VK_O, Shortcut.ALT_SHIFT), 100);
[5958]99 undoTree.addMouseListener(new MouseEventHandler());
[3262]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);
[5200]107 InputMapUtils.unassignCtrlShiftUpDown(undoTree, JComponent.WHEN_FOCUSED);
[6070]108
[5958]109 redoTree.addMouseListener(new MouseEventHandler());
[3262]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);
[7398]117 InputMapUtils.unassignCtrlShiftUpDown(redoTree, JComponent.WHEN_FOCUSED);
[3262]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(GBC.HORIZONTAL));
124 separator.setVisible(false);
125 treesPanel.add(separator, GBC.eol().fill(GBC.HORIZONTAL));
126 treesPanel.add(redoTree, GBC.eol().fill(GBC.HORIZONTAL));
127 treesPanel.add(Box.createRigidArea(new Dimension(0, 0)), GBC.std().weight(0, 1));
128 treesPanel.setBackground(redoTree.getBackground());
129
[4363]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
[12279]139 scrollPane = (JScrollPane) createLayout(treesPanel, true, Arrays.asList(
[4363]140 new SideButton(selectAction),
141 new SideButton(undoAction),
142 new SideButton(redoAction)
[12279]143 ));
[6070]144
[5727]145 InputMapUtils.addEnterAction(undoTree, selectAndZoomAction);
146 InputMapUtils.addEnterAction(redoTree, selectAndZoomAction);
[3262]147 }
148
149 private static class CommandCellRenderer extends DefaultTreeCellRenderer {
[7398]150 @Override
[8509]151 public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row,
152 boolean hasFocus) {
[3262]153 super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
[8510]154 DefaultMutableTreeNode v = (DefaultMutableTreeNode) value;
[3262]155 if (v.getUserObject() instanceof JLabel) {
[8510]156 JLabel l = (JLabel) v.getUserObject();
[3262]157 setIcon(l.getIcon());
158 setText(l.getText());
[1169]159 }
[3262]160 return this;
161 }
162 }
163
[7398]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
[3262]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 {
[9078]180 private final JTree source;
[3262]181
[8836]182 UndoRedoSelectionListener(JTree source) {
[3262]183 this.source = source;
184 }
185
[6084]186 @Override
[3262]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 /**
[7398]202 * Wires updater for enabled state to the events. Also updates dialog title if needed.
[9246]203 * @param updater updater
204 * @param tree tree on which wire updater
[3262]205 */
206 protected void wireUpdateEnabledStateUpdater(final IEnabledStateUpdating updater, JTree tree) {
207 addShowNotifyListener(updater);
208
[10611]209 tree.addTreeSelectionListener(e -> updater.updateEnabledState());
[3262]210
211 tree.getModel().addTreeModelListener(new TreeModelListener() {
[6084]212 @Override
[3262]213 public void treeNodesChanged(TreeModelEvent e) {
214 updater.updateEnabledState();
[7398]215 updateTitle();
[3262]216 }
217
[6084]218 @Override
[3262]219 public void treeNodesInserted(TreeModelEvent e) {
[12866]220 treeNodesChanged(e);
[3262]221 }
222
[6084]223 @Override
[3262]224 public void treeNodesRemoved(TreeModelEvent e) {
[12866]225 treeNodesChanged(e);
[3262]226 }
227
[6084]228 @Override
[3262]229 public void treeStructureChanged(TreeModelEvent e) {
[12866]230 treeNodesChanged(e);
[3262]231 }
232 });
[1169]233 }
[626]234
[3262]235 @Override
236 public void showNotify() {
237 buildTrees();
238 for (IEnabledStateUpdating listener : showNotifyListener) {
239 listener.updateEnabledState();
[1750]240 }
[14134]241 UndoRedoHandler.getInstance().addCommandQueuePreciseListener(this);
[1169]242 }
[626]243
[3262]244 /**
245 * Simple listener setup to update the button enabled state when the side dialog shows.
246 */
[9078]247 private final transient Set<IEnabledStateUpdating> showNotifyListener = new LinkedHashSet<>();
[3262]248
249 private void addShowNotifyListener(IEnabledStateUpdating listener) {
250 showNotifyListener.add(listener);
251 }
252
253 @Override
254 public void hideNotify() {
[13824]255 undoRoot = new DefaultMutableTreeNode();
256 redoRoot = new DefaultMutableTreeNode();
257 undoTreeModel.setRoot(undoRoot);
258 redoTreeModel.setRoot(redoRoot);
[14134]259 UndoRedoHandler.getInstance().removeCommandQueuePreciseListener(this);
[3262]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"));
[13729]268 buildUndoTree();
269 buildRedoTree();
270 ensureTreesConsistency();
271 }
[3262]272
[13729]273 private void buildUndoTree() {
[14281]274 List<Command> undoCommands = UndoRedoHandler.getInstance().getUndoCommands();
[13729]275 undoRoot = new DefaultMutableTreeNode();
[8510]276 for (int i = 0; i < undoCommands.size(); ++i) {
[13943]277 undoRoot.add(getNodeForCommand(undoCommands.get(i)));
[1750]278 }
[3262]279 undoTreeModel.setRoot(undoRoot);
[13729]280 }
[3262]281
[13729]282 private void buildRedoTree() {
[14281]283 List<Command> redoCommands = UndoRedoHandler.getInstance().getRedoCommands();
[13729]284 redoRoot = new DefaultMutableTreeNode();
[8510]285 for (int i = 0; i < redoCommands.size(); ++i) {
[13943]286 redoRoot.add(getNodeForCommand(redoCommands.get(i)));
[3262]287 }
288 redoTreeModel.setRoot(redoRoot);
[13729]289 }
290
291 private void ensureTreesConsistency() {
[14281]292 List<Command> undoCommands = UndoRedoHandler.getInstance().getUndoCommands();
293 List<Command> redoCommands = UndoRedoHandler.getInstance().getRedoCommands();
[3262]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) {
[3443]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;
[3262]314 }
315
316 // select the next command to undo/redo
317 switch (lastOperation) {
[3443]318 case UNDO:
319 undoTree.setSelectionRow(undoTree.getRowCount()-1);
320 break;
321 case REDO:
322 redoTree.setSelectionRow(0);
323 break;
[3262]324 }
[4395]325
326 undoTree.scrollRowToVisible(undoTreeModel.getChildCount(undoRoot)-1);
327 scrollPane.getHorizontalScrollBar().setValue(0);
[1169]328 }
[626]329
[3262]330 /**
331 * Wraps a command in a CommandListMutableTreeNode.
332 * Recursively adds child commands.
[8958]333 * @param c the command
334 * @return the resulting node
[3262]335 */
[13943]336 protected CommandListMutableTreeNode getNodeForCommand(PseudoCommand c) {
337 CommandListMutableTreeNode node = new CommandListMutableTreeNode(c);
[3262]338 if (c.getChildren() != null) {
[7005]339 List<PseudoCommand> children = new ArrayList<>(c.getChildren());
[8510]340 for (int i = 0; i < children.size(); ++i) {
[13943]341 node.add(getNodeForCommand(children.get(i)));
[3262]342 }
343 }
344 return node;
345 }
[6070]346
[5727]347 /**
348 * Return primitives that are affected by some command
[6070]349 * @param path GUI elements
350 * @return collection of affected primitives, onluy usable ones
[5727]351 */
[10788]352 protected static Collection<? extends OsmPrimitive> getAffectedPrimitives(TreePath path) {
[5727]353 PseudoCommand c = ((CommandListMutableTreeNode) path.getLastPathComponent()).getCommand();
[12636]354 final OsmDataLayer currentLayer = MainApplication.getLayerManager().getEditLayer();
[10788]355 return new SubclassFilteredCollection<>(
[5727]356 c.getParticipatingPrimitives(),
[10611]357 o -> {
358 OsmPrimitive p = currentLayer.data.getPrimitiveById(o);
359 return p != null && p.isUsable();
[5727]360 }
361 );
362 }
[3262]363
[14562]364 protected boolean redoTreeIsEmpty() {
365 return redoTree.getRowCount() == 0;
366 }
367
[6084]368 @Override
[13729]369 public void cleaned(CommandQueueCleanedEvent e) {
370 if (isVisible()) {
371 buildTrees();
372 }
[626]373 }
[3262]374
[13729]375 @Override
376 public void commandAdded(CommandAddedEvent e) {
377 if (isVisible()) {
[13943]378 undoRoot.add(getNodeForCommand(e.getCommand()));
[13729]379 undoTreeModel.nodeStructureChanged(undoRoot);
[14562]380 // fix 16911: make sure that redo tree is rebuild with empty list
381 if (!redoTreeIsEmpty())
382 buildRedoTree();
[13729]383 ensureTreesConsistency();
384 }
385 }
386
387 @Override
388 public void commandUndone(CommandUndoneEvent e) {
389 if (isVisible()) {
390 swapNode(undoTreeModel, undoRoot, undoRoot.getChildCount() - 1, redoTreeModel, redoRoot, 0);
391 }
392 }
393
394 @Override
395 public void commandRedone(CommandRedoneEvent e) {
396 if (isVisible()) {
397 swapNode(redoTreeModel, redoRoot, 0, undoTreeModel, undoRoot, undoRoot.getChildCount());
398 }
399 }
400
401 private void swapNode(DefaultTreeModel srcModel, DefaultMutableTreeNode srcRoot, int srcIndex,
402 DefaultTreeModel dstModel, DefaultMutableTreeNode dstRoot, int dstIndex) {
403 MutableTreeNode node = (MutableTreeNode) srcRoot.getChildAt(srcIndex);
404 srcRoot.remove(node);
405 srcModel.nodeStructureChanged(srcRoot);
406 dstRoot.insert(node, dstIndex);
407 dstModel.nodeStructureChanged(dstRoot);
408 ensureTreesConsistency();
409 }
410
[7398]411 /**
412 * Action that selects the objects that take part in a command.
413 */
[3262]414 public class SelectAction extends AbstractAction implements IEnabledStateUpdating {
415
[6361]416 /**
417 * Constructs a new {@code SelectAction}.
418 */
[3262]419 public SelectAction() {
[8510]420 putValue(NAME, tr("Select"));
[3262]421 putValue(SHORT_DESCRIPTION, tr("Selects the objects that take part in this command (unless currently deleted)"));
[10369]422 new ImageProvider("dialogs", "select").getResource().attachImageIcon(this, true);
[3262]423 }
424
[5727]425 @Override
[3262]426 public void actionPerformed(ActionEvent e) {
427 TreePath path;
428 if (!undoTree.isSelectionEmpty()) {
429 path = undoTree.getSelectionPath();
430 } else if (!redoTree.isSelectionEmpty()) {
431 path = redoTree.getSelectionPath();
432 } else
433 throw new IllegalStateException();
434
[12636]435 DataSet dataSet = MainApplication.getLayerManager().getEditDataSet();
[10453]436 if (dataSet == null) return;
437 dataSet.setSelected(getAffectedPrimitives(path));
[3262]438 }
[6070]439
[5727]440 @Override
[3262]441 public void updateEnabledState() {
442 setEnabled(!undoTree.isSelectionEmpty() || !redoTree.isSelectionEmpty());
443 }
444 }
445
[7398]446 /**
447 * Action that selects the objects that take part in a command, then zoom to them.
448 */
[5727]449 public class SelectAndZoomAction extends SelectAction {
[6336]450 /**
451 * Constructs a new {@code SelectAndZoomAction}.
452 */
[5727]453 public SelectAndZoomAction() {
[8510]454 putValue(NAME, tr("Select and zoom"));
[8540]455 putValue(SHORT_DESCRIPTION,
456 tr("Selects the objects that take part in this command (unless currently deleted), then and zooms to it"));
[10369]457 new ImageProvider("dialogs/autoscale", "selection").getResource().attachImageIcon(this, true);
[5727]458 }
459
460 @Override
461 public void actionPerformed(ActionEvent e) {
462 super.actionPerformed(e);
[14221]463 AutoScaleAction.autoScale(AutoScaleMode.SELECTION);
[5727]464 }
465 }
[6070]466
[3262]467 /**
468 * undo / redo switch to reduce duplicate code
469 */
[9059]470 protected enum UndoRedoType {
471 UNDO,
472 REDO
473 }
[3262]474
475 /**
476 * Action to undo or redo all commands up to (and including) the seleced item.
477 */
478 protected class UndoRedoAction extends AbstractAction implements IEnabledStateUpdating {
[9078]479 private final UndoRedoType type;
480 private final JTree tree;
[3262]481
482 /**
483 * constructor
484 * @param type decide whether it is an undo action or a redo action
485 */
486 public UndoRedoAction(UndoRedoType type) {
487 this.type = type;
[9078]488 if (UndoRedoType.UNDO == type) {
[3443]489 tree = undoTree;
[8510]490 putValue(NAME, tr("Undo"));
[3443]491 putValue(SHORT_DESCRIPTION, tr("Undo the selected and all later commands"));
[10369]492 new ImageProvider("undo").getResource().attachImageIcon(this, true);
[9078]493 } else {
[3443]494 tree = redoTree;
[8510]495 putValue(NAME, tr("Redo"));
[3443]496 putValue(SHORT_DESCRIPTION, tr("Redo the selected and all earlier commands"));
[10369]497 new ImageProvider("redo").getResource().attachImageIcon(this, true);
[3262]498 }
499 }
500
[6084]501 @Override
[3262]502 public void actionPerformed(ActionEvent e) {
503 lastOperation = type;
504 TreePath path = tree.getSelectionPath();
505
506 // we can only undo top level commands
507 if (path.getPathCount() != 2)
508 throw new IllegalStateException();
509
510 int idx = ((CommandListMutableTreeNode) path.getLastPathComponent()).getIndex();
511
512 // calculate the number of commands to undo/redo; then do it
513 switch (type) {
[3443]514 case UNDO:
515 int numUndo = ((DefaultMutableTreeNode) undoTreeModel.getRoot()).getChildCount() - idx;
[14134]516 UndoRedoHandler.getInstance().undo(numUndo);
[3443]517 break;
518 case REDO:
519 int numRedo = idx+1;
[14134]520 UndoRedoHandler.getInstance().redo(numRedo);
[3443]521 break;
[3262]522 }
[12630]523 MainApplication.getMap().repaint();
[3262]524 }
525
[6084]526 @Override
[3262]527 public void updateEnabledState() {
528 // do not allow execution if nothing is selected or a sub command was selected
[8510]529 setEnabled(!tree.isSelectionEmpty() && tree.getSelectionPath().getPathCount() == 2);
[3262]530 }
531 }
532
[5958]533 class MouseEventHandler extends PopupMenuLauncher {
534
[8836]535 MouseEventHandler() {
[5958]536 super(new CommandStackPopup());
537 }
[6070]538
[3262]539 @Override
[5727]540 public void mouseClicked(MouseEvent evt) {
[5958]541 if (isDoubleClick(evt)) {
[5727]542 selectAndZoomAction.actionPerformed(null);
543 }
544 }
[3262]545 }
[6070]546
[3262]547 private class CommandStackPopup extends JPopupMenu {
[8836]548 CommandStackPopup() {
[5727]549 add(selectAction);
550 add(selectAndZoomAction);
[3262]551 }
552 }
[626]553}
Note: See TracBrowser for help on using the repository browser.