001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.dialogs; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Component; 007import java.awt.Dimension; 008import java.awt.GridBagLayout; 009import java.awt.event.ActionEvent; 010import java.awt.event.KeyEvent; 011import java.awt.event.MouseEvent; 012import java.util.ArrayList; 013import java.util.Arrays; 014import java.util.Collection; 015import java.util.LinkedHashSet; 016import java.util.List; 017import java.util.Set; 018 019import javax.swing.AbstractAction; 020import javax.swing.Box; 021import javax.swing.JComponent; 022import javax.swing.JLabel; 023import javax.swing.JPanel; 024import javax.swing.JPopupMenu; 025import javax.swing.JScrollPane; 026import javax.swing.JSeparator; 027import javax.swing.JTree; 028import javax.swing.event.TreeModelEvent; 029import javax.swing.event.TreeModelListener; 030import javax.swing.event.TreeSelectionEvent; 031import javax.swing.event.TreeSelectionListener; 032import javax.swing.tree.DefaultMutableTreeNode; 033import javax.swing.tree.DefaultTreeCellRenderer; 034import javax.swing.tree.DefaultTreeModel; 035import javax.swing.tree.MutableTreeNode; 036import javax.swing.tree.TreePath; 037import javax.swing.tree.TreeSelectionModel; 038 039import org.openstreetmap.josm.actions.AutoScaleAction; 040import org.openstreetmap.josm.command.Command; 041import org.openstreetmap.josm.command.PseudoCommand; 042import org.openstreetmap.josm.data.UndoRedoHandler.CommandAddedEvent; 043import org.openstreetmap.josm.data.UndoRedoHandler.CommandQueueCleanedEvent; 044import org.openstreetmap.josm.data.UndoRedoHandler.CommandQueuePreciseListener; 045import org.openstreetmap.josm.data.UndoRedoHandler.CommandRedoneEvent; 046import org.openstreetmap.josm.data.UndoRedoHandler.CommandUndoneEvent; 047import org.openstreetmap.josm.data.osm.DataSet; 048import org.openstreetmap.josm.data.osm.OsmPrimitive; 049import org.openstreetmap.josm.gui.MainApplication; 050import org.openstreetmap.josm.gui.SideButton; 051import org.openstreetmap.josm.gui.layer.OsmDataLayer; 052import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; 053import org.openstreetmap.josm.tools.GBC; 054import org.openstreetmap.josm.tools.ImageProvider; 055import org.openstreetmap.josm.tools.InputMapUtils; 056import org.openstreetmap.josm.tools.Shortcut; 057import org.openstreetmap.josm.tools.SubclassFilteredCollection; 058 059/** 060 * Dialog displaying list of all executed commands (undo/redo buffer). 061 * @since 94 062 */ 063public class CommandStackDialog extends ToggleDialog implements CommandQueuePreciseListener { 064 065 private final DefaultTreeModel undoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode()); 066 private final DefaultTreeModel redoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode()); 067 068 private final JTree undoTree = new JTree(undoTreeModel); 069 private final JTree redoTree = new JTree(redoTreeModel); 070 071 private DefaultMutableTreeNode undoRoot; 072 private DefaultMutableTreeNode redoRoot; 073 074 private final transient UndoRedoSelectionListener undoSelectionListener; 075 private final transient UndoRedoSelectionListener redoSelectionListener; 076 077 private final JScrollPane scrollPane; 078 private final JSeparator separator = new JSeparator(); 079 // only visible, if separator is the top most component 080 private final Component spacer = Box.createRigidArea(new Dimension(0, 3)); 081 082 // last operation is remembered to select the next undo/redo entry in the list 083 // after undo/redo command 084 private UndoRedoType lastOperation = UndoRedoType.UNDO; 085 086 // Actions for context menu and Enter key 087 private final SelectAction selectAction = new SelectAction(); 088 private final SelectAndZoomAction selectAndZoomAction = new SelectAndZoomAction(); 089 090 /** 091 * Constructs a new {@code CommandStackDialog}. 092 */ 093 public CommandStackDialog() { 094 super(tr("Command Stack"), "commandstack", tr("Open a list of all commands (undo buffer)."), 095 Shortcut.registerShortcut("subwindow:commandstack", tr("Toggle: {0}", 096 tr("Command Stack")), KeyEvent.VK_O, Shortcut.ALT_SHIFT), 100); 097 undoTree.addMouseListener(new MouseEventHandler()); 098 undoTree.setRootVisible(false); 099 undoTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); 100 undoTree.setShowsRootHandles(true); 101 undoTree.expandRow(0); 102 undoTree.setCellRenderer(new CommandCellRenderer()); 103 undoSelectionListener = new UndoRedoSelectionListener(undoTree); 104 undoTree.getSelectionModel().addTreeSelectionListener(undoSelectionListener); 105 InputMapUtils.unassignCtrlShiftUpDown(undoTree, JComponent.WHEN_FOCUSED); 106 107 redoTree.addMouseListener(new MouseEventHandler()); 108 redoTree.setRootVisible(false); 109 redoTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); 110 redoTree.setShowsRootHandles(true); 111 redoTree.expandRow(0); 112 redoTree.setCellRenderer(new CommandCellRenderer()); 113 redoSelectionListener = new UndoRedoSelectionListener(redoTree); 114 redoTree.getSelectionModel().addTreeSelectionListener(redoSelectionListener); 115 InputMapUtils.unassignCtrlShiftUpDown(redoTree, JComponent.WHEN_FOCUSED); 116 117 JPanel treesPanel = new JPanel(new GridBagLayout()); 118 119 treesPanel.add(spacer, GBC.eol()); 120 spacer.setVisible(false); 121 treesPanel.add(undoTree, GBC.eol().fill(GBC.HORIZONTAL)); 122 separator.setVisible(false); 123 treesPanel.add(separator, GBC.eol().fill(GBC.HORIZONTAL)); 124 treesPanel.add(redoTree, GBC.eol().fill(GBC.HORIZONTAL)); 125 treesPanel.add(Box.createRigidArea(new Dimension(0, 0)), GBC.std().weight(0, 1)); 126 treesPanel.setBackground(redoTree.getBackground()); 127 128 wireUpdateEnabledStateUpdater(selectAction, undoTree); 129 wireUpdateEnabledStateUpdater(selectAction, redoTree); 130 131 UndoRedoAction undoAction = new UndoRedoAction(UndoRedoType.UNDO); 132 wireUpdateEnabledStateUpdater(undoAction, undoTree); 133 134 UndoRedoAction redoAction = new UndoRedoAction(UndoRedoType.REDO); 135 wireUpdateEnabledStateUpdater(redoAction, redoTree); 136 137 scrollPane = (JScrollPane) createLayout(treesPanel, true, Arrays.asList( 138 new SideButton(selectAction), 139 new SideButton(undoAction), 140 new SideButton(redoAction) 141 )); 142 143 InputMapUtils.addEnterAction(undoTree, selectAndZoomAction); 144 InputMapUtils.addEnterAction(redoTree, selectAndZoomAction); 145 } 146 147 private static class CommandCellRenderer extends DefaultTreeCellRenderer { 148 @Override 149 public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, 150 boolean hasFocus) { 151 super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); 152 DefaultMutableTreeNode v = (DefaultMutableTreeNode) value; 153 if (v.getUserObject() instanceof JLabel) { 154 JLabel l = (JLabel) v.getUserObject(); 155 setIcon(l.getIcon()); 156 setText(l.getText()); 157 } 158 return this; 159 } 160 } 161 162 private void updateTitle() { 163 int undo = undoTreeModel.getChildCount(undoTreeModel.getRoot()); 164 int redo = redoTreeModel.getChildCount(redoTreeModel.getRoot()); 165 if (undo > 0 || redo > 0) { 166 setTitle(tr("Command Stack: Undo: {0} / Redo: {1}", undo, redo)); 167 } else { 168 setTitle(tr("Command Stack")); 169 } 170 } 171 172 /** 173 * Selection listener for undo and redo area. 174 * If one is clicked, takes away the selection from the other, so 175 * it behaves as if it was one component. 176 */ 177 private class UndoRedoSelectionListener implements TreeSelectionListener { 178 private final JTree source; 179 180 UndoRedoSelectionListener(JTree source) { 181 this.source = source; 182 } 183 184 @Override 185 public void valueChanged(TreeSelectionEvent e) { 186 if (source == undoTree) { 187 redoTree.getSelectionModel().removeTreeSelectionListener(redoSelectionListener); 188 redoTree.clearSelection(); 189 redoTree.getSelectionModel().addTreeSelectionListener(redoSelectionListener); 190 } 191 if (source == redoTree) { 192 undoTree.getSelectionModel().removeTreeSelectionListener(undoSelectionListener); 193 undoTree.clearSelection(); 194 undoTree.getSelectionModel().addTreeSelectionListener(undoSelectionListener); 195 } 196 } 197 } 198 199 /** 200 * Wires updater for enabled state to the events. Also updates dialog title if needed. 201 * @param updater updater 202 * @param tree tree on which wire updater 203 */ 204 protected void wireUpdateEnabledStateUpdater(final IEnabledStateUpdating updater, JTree tree) { 205 addShowNotifyListener(updater); 206 207 tree.addTreeSelectionListener(e -> updater.updateEnabledState()); 208 209 tree.getModel().addTreeModelListener(new TreeModelListener() { 210 @Override 211 public void treeNodesChanged(TreeModelEvent e) { 212 updater.updateEnabledState(); 213 updateTitle(); 214 } 215 216 @Override 217 public void treeNodesInserted(TreeModelEvent e) { 218 treeNodesChanged(e); 219 } 220 221 @Override 222 public void treeNodesRemoved(TreeModelEvent e) { 223 treeNodesChanged(e); 224 } 225 226 @Override 227 public void treeStructureChanged(TreeModelEvent e) { 228 treeNodesChanged(e); 229 } 230 }); 231 } 232 233 @Override 234 public void showNotify() { 235 buildTrees(); 236 for (IEnabledStateUpdating listener : showNotifyListener) { 237 listener.updateEnabledState(); 238 } 239 MainApplication.undoRedo.addCommandQueuePreciseListener(this); 240 } 241 242 /** 243 * Simple listener setup to update the button enabled state when the side dialog shows. 244 */ 245 private final transient Set<IEnabledStateUpdating> showNotifyListener = new LinkedHashSet<>(); 246 247 private void addShowNotifyListener(IEnabledStateUpdating listener) { 248 showNotifyListener.add(listener); 249 } 250 251 @Override 252 public void hideNotify() { 253 undoRoot = new DefaultMutableTreeNode(); 254 redoRoot = new DefaultMutableTreeNode(); 255 undoTreeModel.setRoot(undoRoot); 256 redoTreeModel.setRoot(redoRoot); 257 MainApplication.undoRedo.removeCommandQueuePreciseListener(this); 258 } 259 260 /** 261 * Build the trees of undo and redo commands (initially or when 262 * they have changed). 263 */ 264 private void buildTrees() { 265 setTitle(tr("Command Stack")); 266 buildUndoTree(); 267 buildRedoTree(); 268 ensureTreesConsistency(); 269 } 270 271 private void buildUndoTree() { 272 List<Command> undoCommands = MainApplication.undoRedo.commands; 273 undoRoot = new DefaultMutableTreeNode(); 274 for (int i = 0; i < undoCommands.size(); ++i) { 275 undoRoot.add(getNodeForCommand(undoCommands.get(i))); 276 } 277 undoTreeModel.setRoot(undoRoot); 278 } 279 280 private void buildRedoTree() { 281 List<Command> redoCommands = MainApplication.undoRedo.redoCommands; 282 redoRoot = new DefaultMutableTreeNode(); 283 for (int i = 0; i < redoCommands.size(); ++i) { 284 redoRoot.add(getNodeForCommand(redoCommands.get(i))); 285 } 286 redoTreeModel.setRoot(redoRoot); 287 } 288 289 private void ensureTreesConsistency() { 290 List<Command> undoCommands = MainApplication.undoRedo.commands; 291 List<Command> redoCommands = MainApplication.undoRedo.redoCommands; 292 if (redoTreeModel.getChildCount(redoRoot) > 0) { 293 redoTree.scrollRowToVisible(0); 294 scrollPane.getHorizontalScrollBar().setValue(0); 295 } 296 297 separator.setVisible(!undoCommands.isEmpty() || !redoCommands.isEmpty()); 298 spacer.setVisible(undoCommands.isEmpty() && !redoCommands.isEmpty()); 299 300 // if one tree is empty, move selection to the other 301 switch (lastOperation) { 302 case UNDO: 303 if (undoCommands.isEmpty()) { 304 lastOperation = UndoRedoType.REDO; 305 } 306 break; 307 case REDO: 308 if (redoCommands.isEmpty()) { 309 lastOperation = UndoRedoType.UNDO; 310 } 311 break; 312 } 313 314 // select the next command to undo/redo 315 switch (lastOperation) { 316 case UNDO: 317 undoTree.setSelectionRow(undoTree.getRowCount()-1); 318 break; 319 case REDO: 320 redoTree.setSelectionRow(0); 321 break; 322 } 323 324 undoTree.scrollRowToVisible(undoTreeModel.getChildCount(undoRoot)-1); 325 scrollPane.getHorizontalScrollBar().setValue(0); 326 } 327 328 /** 329 * Wraps a command in a CommandListMutableTreeNode. 330 * Recursively adds child commands. 331 * @param c the command 332 * @return the resulting node 333 */ 334 protected CommandListMutableTreeNode getNodeForCommand(PseudoCommand c) { 335 CommandListMutableTreeNode node = new CommandListMutableTreeNode(c); 336 if (c.getChildren() != null) { 337 List<PseudoCommand> children = new ArrayList<>(c.getChildren()); 338 for (int i = 0; i < children.size(); ++i) { 339 node.add(getNodeForCommand(children.get(i))); 340 } 341 } 342 return node; 343 } 344 345 /** 346 * Return primitives that are affected by some command 347 * @param path GUI elements 348 * @return collection of affected primitives, onluy usable ones 349 */ 350 protected static Collection<? extends OsmPrimitive> getAffectedPrimitives(TreePath path) { 351 PseudoCommand c = ((CommandListMutableTreeNode) path.getLastPathComponent()).getCommand(); 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 @Override 363 public void cleaned(CommandQueueCleanedEvent e) { 364 if (isVisible()) { 365 buildTrees(); 366 } 367 } 368 369 @Override 370 public void commandAdded(CommandAddedEvent e) { 371 if (isVisible()) { 372 undoRoot.add(getNodeForCommand(e.getCommand())); 373 undoTreeModel.nodeStructureChanged(undoRoot); 374 ensureTreesConsistency(); 375 } 376 } 377 378 @Override 379 public void commandUndone(CommandUndoneEvent e) { 380 if (isVisible()) { 381 swapNode(undoTreeModel, undoRoot, undoRoot.getChildCount() - 1, redoTreeModel, redoRoot, 0); 382 } 383 } 384 385 @Override 386 public void commandRedone(CommandRedoneEvent e) { 387 if (isVisible()) { 388 swapNode(redoTreeModel, redoRoot, 0, undoTreeModel, undoRoot, undoRoot.getChildCount()); 389 } 390 } 391 392 private void swapNode(DefaultTreeModel srcModel, DefaultMutableTreeNode srcRoot, int srcIndex, 393 DefaultTreeModel dstModel, DefaultMutableTreeNode dstRoot, int dstIndex) { 394 MutableTreeNode node = (MutableTreeNode) srcRoot.getChildAt(srcIndex); 395 srcRoot.remove(node); 396 srcModel.nodeStructureChanged(srcRoot); 397 dstRoot.insert(node, dstIndex); 398 dstModel.nodeStructureChanged(dstRoot); 399 ensureTreesConsistency(); 400 } 401 402 /** 403 * Action that selects the objects that take part in a command. 404 */ 405 public class SelectAction extends AbstractAction implements IEnabledStateUpdating { 406 407 /** 408 * Constructs a new {@code SelectAction}. 409 */ 410 public SelectAction() { 411 putValue(NAME, tr("Select")); 412 putValue(SHORT_DESCRIPTION, tr("Selects the objects that take part in this command (unless currently deleted)")); 413 new ImageProvider("dialogs", "select").getResource().attachImageIcon(this, true); 414 } 415 416 @Override 417 public void actionPerformed(ActionEvent e) { 418 TreePath path; 419 if (!undoTree.isSelectionEmpty()) { 420 path = undoTree.getSelectionPath(); 421 } else if (!redoTree.isSelectionEmpty()) { 422 path = redoTree.getSelectionPath(); 423 } else 424 throw new IllegalStateException(); 425 426 DataSet dataSet = MainApplication.getLayerManager().getEditDataSet(); 427 if (dataSet == null) return; 428 dataSet.setSelected(getAffectedPrimitives(path)); 429 } 430 431 @Override 432 public void updateEnabledState() { 433 setEnabled(!undoTree.isSelectionEmpty() || !redoTree.isSelectionEmpty()); 434 } 435 } 436 437 /** 438 * Action that selects the objects that take part in a command, then zoom to them. 439 */ 440 public class SelectAndZoomAction extends SelectAction { 441 /** 442 * Constructs a new {@code SelectAndZoomAction}. 443 */ 444 public SelectAndZoomAction() { 445 putValue(NAME, tr("Select and zoom")); 446 putValue(SHORT_DESCRIPTION, 447 tr("Selects the objects that take part in this command (unless currently deleted), then and zooms to it")); 448 new ImageProvider("dialogs/autoscale", "selection").getResource().attachImageIcon(this, true); 449 } 450 451 @Override 452 public void actionPerformed(ActionEvent e) { 453 super.actionPerformed(e); 454 AutoScaleAction.autoScale("selection"); 455 } 456 } 457 458 /** 459 * undo / redo switch to reduce duplicate code 460 */ 461 protected enum UndoRedoType { 462 UNDO, 463 REDO 464 } 465 466 /** 467 * Action to undo or redo all commands up to (and including) the seleced item. 468 */ 469 protected class UndoRedoAction extends AbstractAction implements IEnabledStateUpdating { 470 private final UndoRedoType type; 471 private final JTree tree; 472 473 /** 474 * constructor 475 * @param type decide whether it is an undo action or a redo action 476 */ 477 public UndoRedoAction(UndoRedoType type) { 478 this.type = type; 479 if (UndoRedoType.UNDO == type) { 480 tree = undoTree; 481 putValue(NAME, tr("Undo")); 482 putValue(SHORT_DESCRIPTION, tr("Undo the selected and all later commands")); 483 new ImageProvider("undo").getResource().attachImageIcon(this, true); 484 } else { 485 tree = redoTree; 486 putValue(NAME, tr("Redo")); 487 putValue(SHORT_DESCRIPTION, tr("Redo the selected and all earlier commands")); 488 new ImageProvider("redo").getResource().attachImageIcon(this, true); 489 } 490 } 491 492 @Override 493 public void actionPerformed(ActionEvent e) { 494 lastOperation = type; 495 TreePath path = tree.getSelectionPath(); 496 497 // we can only undo top level commands 498 if (path.getPathCount() != 2) 499 throw new IllegalStateException(); 500 501 int idx = ((CommandListMutableTreeNode) path.getLastPathComponent()).getIndex(); 502 503 // calculate the number of commands to undo/redo; then do it 504 switch (type) { 505 case UNDO: 506 int numUndo = ((DefaultMutableTreeNode) undoTreeModel.getRoot()).getChildCount() - idx; 507 MainApplication.undoRedo.undo(numUndo); 508 break; 509 case REDO: 510 int numRedo = idx+1; 511 MainApplication.undoRedo.redo(numRedo); 512 break; 513 } 514 MainApplication.getMap().repaint(); 515 } 516 517 @Override 518 public void updateEnabledState() { 519 // do not allow execution if nothing is selected or a sub command was selected 520 setEnabled(!tree.isSelectionEmpty() && tree.getSelectionPath().getPathCount() == 2); 521 } 522 } 523 524 class MouseEventHandler extends PopupMenuLauncher { 525 526 MouseEventHandler() { 527 super(new CommandStackPopup()); 528 } 529 530 @Override 531 public void mouseClicked(MouseEvent evt) { 532 if (isDoubleClick(evt)) { 533 selectAndZoomAction.actionPerformed(null); 534 } 535 } 536 } 537 538 private class CommandStackPopup extends JPopupMenu { 539 CommandStackPopup() { 540 add(selectAction); 541 add(selectAndZoomAction); 542 } 543 } 544}