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 transient UndoRedoSelectionListener undoSelectionListener; 066 private transient UndoRedoSelectionListener redoSelectionListener; 067 068 private JScrollPane scrollPane; 069 private JSeparator separator = new JSeparator(); 070 // only visible, if separator is the top most component 071 private 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 SelectAction selectAction = new SelectAction(); 079 private 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 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 */ 200 protected void wireUpdateEnabledStateUpdater(final IEnabledStateUpdating updater, JTree tree) { 201 addShowNotifyListener(updater); 202 203 tree.addTreeSelectionListener(new TreeSelectionListener() { 204 @Override 205 public void valueChanged(TreeSelectionEvent e) { 206 updater.updateEnabledState(); 207 } 208 }); 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 updater.updateEnabledState(); 220 updateTitle(); 221 } 222 223 @Override 224 public void treeNodesRemoved(TreeModelEvent e) { 225 updater.updateEnabledState(); 226 updateTitle(); 227 } 228 229 @Override 230 public void treeStructureChanged(TreeModelEvent e) { 231 updater.updateEnabledState(); 232 updateTitle(); 233 } 234 }); 235 } 236 237 @Override 238 public void showNotify() { 239 buildTrees(); 240 for (IEnabledStateUpdating listener : showNotifyListener) { 241 listener.updateEnabledState(); 242 } 243 Main.main.undoRedo.addCommandQueueListener(this); 244 } 245 246 /** 247 * Simple listener setup to update the button enabled state when the side dialog shows. 248 */ 249 private transient Set<IEnabledStateUpdating> showNotifyListener = new LinkedHashSet<>(); 250 251 private void addShowNotifyListener(IEnabledStateUpdating listener) { 252 showNotifyListener.add(listener); 253 } 254 255 @Override 256 public void hideNotify() { 257 undoTreeModel.setRoot(new DefaultMutableTreeNode()); 258 redoTreeModel.setRoot(new DefaultMutableTreeNode()); 259 Main.main.undoRedo.removeCommandQueueListener(this); 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")); 268 if (!Main.main.hasEditLayer()) 269 return; 270 271 List<Command> undoCommands = Main.main.undoRedo.commands; 272 DefaultMutableTreeNode undoRoot = new DefaultMutableTreeNode(); 273 for (int i = 0; i < undoCommands.size(); ++i) { 274 undoRoot.add(getNodeForCommand(undoCommands.get(i), i)); 275 } 276 undoTreeModel.setRoot(undoRoot); 277 278 List<Command> redoCommands = Main.main.undoRedo.redoCommands; 279 DefaultMutableTreeNode redoRoot = new DefaultMutableTreeNode(); 280 for (int i = 0; i < redoCommands.size(); ++i) { 281 redoRoot.add(getNodeForCommand(redoCommands.get(i), i)); 282 } 283 redoTreeModel.setRoot(redoRoot); 284 if (redoTreeModel.getChildCount(redoRoot) > 0) { 285 redoTree.scrollRowToVisible(0); 286 scrollPane.getHorizontalScrollBar().setValue(0); 287 } 288 289 separator.setVisible(!undoCommands.isEmpty() || !redoCommands.isEmpty()); 290 spacer.setVisible(undoCommands.isEmpty() && !redoCommands.isEmpty()); 291 292 // if one tree is empty, move selection to the other 293 switch (lastOperation) { 294 case UNDO: 295 if (undoCommands.isEmpty()) { 296 lastOperation = UndoRedoType.REDO; 297 } 298 break; 299 case REDO: 300 if (redoCommands.isEmpty()) { 301 lastOperation = UndoRedoType.UNDO; 302 } 303 break; 304 } 305 306 // select the next command to undo/redo 307 switch (lastOperation) { 308 case UNDO: 309 undoTree.setSelectionRow(undoTree.getRowCount()-1); 310 break; 311 case REDO: 312 redoTree.setSelectionRow(0); 313 break; 314 } 315 316 undoTree.scrollRowToVisible(undoTreeModel.getChildCount(undoRoot)-1); 317 scrollPane.getHorizontalScrollBar().setValue(0); 318 } 319 320 /** 321 * Wraps a command in a CommandListMutableTreeNode. 322 * Recursively adds child commands. 323 * @param c the command 324 * @return the resulting node 325 */ 326 protected CommandListMutableTreeNode getNodeForCommand(PseudoCommand c, int idx) { 327 CommandListMutableTreeNode node = new CommandListMutableTreeNode(c, idx); 328 if (c.getChildren() != null) { 329 List<PseudoCommand> children = new ArrayList<>(c.getChildren()); 330 for (int i = 0; i < children.size(); ++i) { 331 node.add(getNodeForCommand(children.get(i), i)); 332 } 333 } 334 return node; 335 } 336 337 /** 338 * Return primitives that are affected by some command 339 * @param path GUI elements 340 * @return collection of affected primitives, onluy usable ones 341 */ 342 protected static FilteredCollection<? extends OsmPrimitive> getAffectedPrimitives(TreePath path) { 343 PseudoCommand c = ((CommandListMutableTreeNode) path.getLastPathComponent()).getCommand(); 344 final OsmDataLayer currentLayer = Main.main.getEditLayer(); 345 return new FilteredCollection<>( 346 c.getParticipatingPrimitives(), 347 new Predicate<OsmPrimitive>() { 348 @Override 349 public boolean evaluate(OsmPrimitive o) { 350 OsmPrimitive p = currentLayer.data.getPrimitiveById(o); 351 return p != null && p.isUsable(); 352 } 353 } 354 ); 355 } 356 357 @Override 358 public void commandChanged(int queueSize, int redoSize) { 359 if (!isVisible()) 360 return; 361 buildTrees(); 362 } 363 364 /** 365 * Action that selects the objects that take part in a command. 366 */ 367 public class SelectAction extends AbstractAction implements IEnabledStateUpdating { 368 369 /** 370 * Constructs a new {@code SelectAction}. 371 */ 372 public SelectAction() { 373 putValue(NAME, tr("Select")); 374 putValue(SHORT_DESCRIPTION, tr("Selects the objects that take part in this command (unless currently deleted)")); 375 putValue(SMALL_ICON, ImageProvider.get("dialogs", "select")); 376 } 377 378 @Override 379 public void actionPerformed(ActionEvent e) { 380 TreePath path; 381 if (!undoTree.isSelectionEmpty()) { 382 path = undoTree.getSelectionPath(); 383 } else if (!redoTree.isSelectionEmpty()) { 384 path = redoTree.getSelectionPath(); 385 } else 386 throw new IllegalStateException(); 387 388 OsmDataLayer editLayer = Main.main.getEditLayer(); 389 if (editLayer == null) return; 390 editLayer.data.setSelected(getAffectedPrimitives(path)); 391 } 392 393 @Override 394 public void updateEnabledState() { 395 setEnabled(!undoTree.isSelectionEmpty() || !redoTree.isSelectionEmpty()); 396 } 397 } 398 399 /** 400 * Action that selects the objects that take part in a command, then zoom to them. 401 */ 402 public class SelectAndZoomAction extends SelectAction { 403 /** 404 * Constructs a new {@code SelectAndZoomAction}. 405 */ 406 public SelectAndZoomAction() { 407 putValue(NAME, tr("Select and zoom")); 408 putValue(SHORT_DESCRIPTION, 409 tr("Selects the objects that take part in this command (unless currently deleted), then and zooms to it")); 410 putValue(SMALL_ICON, ImageProvider.get("dialogs/autoscale", "selection")); 411 } 412 413 @Override 414 public void actionPerformed(ActionEvent e) { 415 super.actionPerformed(e); 416 if (!Main.main.hasEditLayer()) return; 417 AutoScaleAction.autoScale("selection"); 418 } 419 } 420 421 /** 422 * undo / redo switch to reduce duplicate code 423 */ 424 protected enum UndoRedoType { 425 UNDO, 426 REDO 427 } 428 429 /** 430 * Action to undo or redo all commands up to (and including) the seleced item. 431 */ 432 protected class UndoRedoAction extends AbstractAction implements IEnabledStateUpdating { 433 private UndoRedoType type; 434 private JTree tree; 435 436 /** 437 * constructor 438 * @param type decide whether it is an undo action or a redo action 439 */ 440 public UndoRedoAction(UndoRedoType type) { 441 super(); 442 this.type = type; 443 switch (type) { 444 case UNDO: 445 tree = undoTree; 446 putValue(NAME, tr("Undo")); 447 putValue(SHORT_DESCRIPTION, tr("Undo the selected and all later commands")); 448 putValue(SMALL_ICON, ImageProvider.get("undo")); 449 break; 450 case REDO: 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 break; 456 } 457 } 458 459 @Override 460 public void actionPerformed(ActionEvent e) { 461 lastOperation = type; 462 TreePath path = tree.getSelectionPath(); 463 464 // we can only undo top level commands 465 if (path.getPathCount() != 2) 466 throw new IllegalStateException(); 467 468 int idx = ((CommandListMutableTreeNode) path.getLastPathComponent()).getIndex(); 469 470 // calculate the number of commands to undo/redo; then do it 471 switch (type) { 472 case UNDO: 473 int numUndo = ((DefaultMutableTreeNode) undoTreeModel.getRoot()).getChildCount() - idx; 474 Main.main.undoRedo.undo(numUndo); 475 break; 476 case REDO: 477 int numRedo = idx+1; 478 Main.main.undoRedo.redo(numRedo); 479 break; 480 } 481 Main.map.repaint(); 482 } 483 484 @Override 485 public void updateEnabledState() { 486 // do not allow execution if nothing is selected or a sub command was selected 487 setEnabled(!tree.isSelectionEmpty() && tree.getSelectionPath().getPathCount() == 2); 488 } 489 } 490 491 class MouseEventHandler extends PopupMenuLauncher { 492 493 MouseEventHandler() { 494 super(new CommandStackPopup()); 495 } 496 497 @Override 498 public void mouseClicked(MouseEvent evt) { 499 if (isDoubleClick(evt)) { 500 selectAndZoomAction.actionPerformed(null); 501 } 502 } 503 } 504 505 private class CommandStackPopup extends JPopupMenu { 506 CommandStackPopup() { 507 add(selectAction); 508 add(selectAndZoomAction); 509 } 510 } 511}