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