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