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.TreePath; 036import javax.swing.tree.TreeSelectionModel; 037 038import org.openstreetmap.josm.actions.AutoScaleAction; 039import org.openstreetmap.josm.command.Command; 040import org.openstreetmap.josm.command.PseudoCommand; 041import org.openstreetmap.josm.data.UndoRedoHandler.CommandQueueListener; 042import org.openstreetmap.josm.data.osm.DataSet; 043import org.openstreetmap.josm.data.osm.OsmPrimitive; 044import org.openstreetmap.josm.gui.MainApplication; 045import org.openstreetmap.josm.gui.SideButton; 046import org.openstreetmap.josm.gui.layer.OsmDataLayer; 047import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; 048import org.openstreetmap.josm.tools.GBC; 049import org.openstreetmap.josm.tools.ImageProvider; 050import org.openstreetmap.josm.tools.InputMapUtils; 051import org.openstreetmap.josm.tools.Shortcut; 052import org.openstreetmap.josm.tools.SubclassFilteredCollection; 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( 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 * Wires updater for enabled state to the events. Also updates dialog title if needed. 193 * @param updater updater 194 * @param tree tree on which wire updater 195 */ 196 protected void wireUpdateEnabledStateUpdater(final IEnabledStateUpdating updater, JTree tree) { 197 addShowNotifyListener(updater); 198 199 tree.addTreeSelectionListener(e -> updater.updateEnabledState()); 200 201 tree.getModel().addTreeModelListener(new TreeModelListener() { 202 @Override 203 public void treeNodesChanged(TreeModelEvent e) { 204 updater.updateEnabledState(); 205 updateTitle(); 206 } 207 208 @Override 209 public void treeNodesInserted(TreeModelEvent e) { 210 treeNodesChanged(e); 211 } 212 213 @Override 214 public void treeNodesRemoved(TreeModelEvent e) { 215 treeNodesChanged(e); 216 } 217 218 @Override 219 public void treeStructureChanged(TreeModelEvent e) { 220 treeNodesChanged(e); 221 } 222 }); 223 } 224 225 @Override 226 public void showNotify() { 227 buildTrees(); 228 for (IEnabledStateUpdating listener : showNotifyListener) { 229 listener.updateEnabledState(); 230 } 231 MainApplication.undoRedo.addCommandQueueListener(this); 232 } 233 234 /** 235 * Simple listener setup to update the button enabled state when the side dialog shows. 236 */ 237 private final transient Set<IEnabledStateUpdating> showNotifyListener = new LinkedHashSet<>(); 238 239 private void addShowNotifyListener(IEnabledStateUpdating listener) { 240 showNotifyListener.add(listener); 241 } 242 243 @Override 244 public void hideNotify() { 245 undoTreeModel.setRoot(new DefaultMutableTreeNode()); 246 redoTreeModel.setRoot(new DefaultMutableTreeNode()); 247 MainApplication.undoRedo.removeCommandQueueListener(this); 248 } 249 250 /** 251 * Build the trees of undo and redo commands (initially or when 252 * they have changed). 253 */ 254 private void buildTrees() { 255 setTitle(tr("Command Stack")); 256 if (MainApplication.getLayerManager().getEditLayer() == null) 257 return; 258 259 List<Command> undoCommands = MainApplication.undoRedo.commands; 260 DefaultMutableTreeNode undoRoot = new DefaultMutableTreeNode(); 261 for (int i = 0; i < undoCommands.size(); ++i) { 262 undoRoot.add(getNodeForCommand(undoCommands.get(i), i)); 263 } 264 undoTreeModel.setRoot(undoRoot); 265 266 List<Command> redoCommands = MainApplication.undoRedo.redoCommands; 267 DefaultMutableTreeNode redoRoot = new DefaultMutableTreeNode(); 268 for (int i = 0; i < redoCommands.size(); ++i) { 269 redoRoot.add(getNodeForCommand(redoCommands.get(i), i)); 270 } 271 redoTreeModel.setRoot(redoRoot); 272 if (redoTreeModel.getChildCount(redoRoot) > 0) { 273 redoTree.scrollRowToVisible(0); 274 scrollPane.getHorizontalScrollBar().setValue(0); 275 } 276 277 separator.setVisible(!undoCommands.isEmpty() || !redoCommands.isEmpty()); 278 spacer.setVisible(undoCommands.isEmpty() && !redoCommands.isEmpty()); 279 280 // if one tree is empty, move selection to the other 281 switch (lastOperation) { 282 case UNDO: 283 if (undoCommands.isEmpty()) { 284 lastOperation = UndoRedoType.REDO; 285 } 286 break; 287 case REDO: 288 if (redoCommands.isEmpty()) { 289 lastOperation = UndoRedoType.UNDO; 290 } 291 break; 292 } 293 294 // select the next command to undo/redo 295 switch (lastOperation) { 296 case UNDO: 297 undoTree.setSelectionRow(undoTree.getRowCount()-1); 298 break; 299 case REDO: 300 redoTree.setSelectionRow(0); 301 break; 302 } 303 304 undoTree.scrollRowToVisible(undoTreeModel.getChildCount(undoRoot)-1); 305 scrollPane.getHorizontalScrollBar().setValue(0); 306 } 307 308 /** 309 * Wraps a command in a CommandListMutableTreeNode. 310 * Recursively adds child commands. 311 * @param c the command 312 * @param idx index 313 * @return the resulting node 314 */ 315 protected CommandListMutableTreeNode getNodeForCommand(PseudoCommand c, int idx) { 316 CommandListMutableTreeNode node = new CommandListMutableTreeNode(c, idx); 317 if (c.getChildren() != null) { 318 List<PseudoCommand> children = new ArrayList<>(c.getChildren()); 319 for (int i = 0; i < children.size(); ++i) { 320 node.add(getNodeForCommand(children.get(i), i)); 321 } 322 } 323 return node; 324 } 325 326 /** 327 * Return primitives that are affected by some command 328 * @param path GUI elements 329 * @return collection of affected primitives, onluy usable ones 330 */ 331 protected static Collection<? extends OsmPrimitive> getAffectedPrimitives(TreePath path) { 332 PseudoCommand c = ((CommandListMutableTreeNode) path.getLastPathComponent()).getCommand(); 333 final OsmDataLayer currentLayer = MainApplication.getLayerManager().getEditLayer(); 334 return new SubclassFilteredCollection<>( 335 c.getParticipatingPrimitives(), 336 o -> { 337 OsmPrimitive p = currentLayer.data.getPrimitiveById(o); 338 return p != null && p.isUsable(); 339 } 340 ); 341 } 342 343 @Override 344 public void commandChanged(int queueSize, int redoSize) { 345 if (!isVisible()) 346 return; 347 buildTrees(); 348 } 349 350 /** 351 * Action that selects the objects that take part in a command. 352 */ 353 public class SelectAction extends AbstractAction implements IEnabledStateUpdating { 354 355 /** 356 * Constructs a new {@code SelectAction}. 357 */ 358 public SelectAction() { 359 putValue(NAME, tr("Select")); 360 putValue(SHORT_DESCRIPTION, tr("Selects the objects that take part in this command (unless currently deleted)")); 361 new ImageProvider("dialogs", "select").getResource().attachImageIcon(this, true); 362 } 363 364 @Override 365 public void actionPerformed(ActionEvent e) { 366 TreePath path; 367 if (!undoTree.isSelectionEmpty()) { 368 path = undoTree.getSelectionPath(); 369 } else if (!redoTree.isSelectionEmpty()) { 370 path = redoTree.getSelectionPath(); 371 } else 372 throw new IllegalStateException(); 373 374 DataSet dataSet = MainApplication.getLayerManager().getEditDataSet(); 375 if (dataSet == null) return; 376 dataSet.setSelected(getAffectedPrimitives(path)); 377 } 378 379 @Override 380 public void updateEnabledState() { 381 setEnabled(!undoTree.isSelectionEmpty() || !redoTree.isSelectionEmpty()); 382 } 383 } 384 385 /** 386 * Action that selects the objects that take part in a command, then zoom to them. 387 */ 388 public class SelectAndZoomAction extends SelectAction { 389 /** 390 * Constructs a new {@code SelectAndZoomAction}. 391 */ 392 public SelectAndZoomAction() { 393 putValue(NAME, tr("Select and zoom")); 394 putValue(SHORT_DESCRIPTION, 395 tr("Selects the objects that take part in this command (unless currently deleted), then and zooms to it")); 396 new ImageProvider("dialogs/autoscale", "selection").getResource().attachImageIcon(this, true); 397 } 398 399 @Override 400 public void actionPerformed(ActionEvent e) { 401 super.actionPerformed(e); 402 AutoScaleAction.autoScale("selection"); 403 } 404 } 405 406 /** 407 * undo / redo switch to reduce duplicate code 408 */ 409 protected enum UndoRedoType { 410 UNDO, 411 REDO 412 } 413 414 /** 415 * Action to undo or redo all commands up to (and including) the seleced item. 416 */ 417 protected class UndoRedoAction extends AbstractAction implements IEnabledStateUpdating { 418 private final UndoRedoType type; 419 private final JTree tree; 420 421 /** 422 * constructor 423 * @param type decide whether it is an undo action or a redo action 424 */ 425 public UndoRedoAction(UndoRedoType type) { 426 this.type = type; 427 if (UndoRedoType.UNDO == type) { 428 tree = undoTree; 429 putValue(NAME, tr("Undo")); 430 putValue(SHORT_DESCRIPTION, tr("Undo the selected and all later commands")); 431 new ImageProvider("undo").getResource().attachImageIcon(this, true); 432 } else { 433 tree = redoTree; 434 putValue(NAME, tr("Redo")); 435 putValue(SHORT_DESCRIPTION, tr("Redo the selected and all earlier commands")); 436 new ImageProvider("redo").getResource().attachImageIcon(this, true); 437 } 438 } 439 440 @Override 441 public void actionPerformed(ActionEvent e) { 442 lastOperation = type; 443 TreePath path = tree.getSelectionPath(); 444 445 // we can only undo top level commands 446 if (path.getPathCount() != 2) 447 throw new IllegalStateException(); 448 449 int idx = ((CommandListMutableTreeNode) path.getLastPathComponent()).getIndex(); 450 451 // calculate the number of commands to undo/redo; then do it 452 switch (type) { 453 case UNDO: 454 int numUndo = ((DefaultMutableTreeNode) undoTreeModel.getRoot()).getChildCount() - idx; 455 MainApplication.undoRedo.undo(numUndo); 456 break; 457 case REDO: 458 int numRedo = idx+1; 459 MainApplication.undoRedo.redo(numRedo); 460 break; 461 } 462 MainApplication.getMap().repaint(); 463 } 464 465 @Override 466 public void updateEnabledState() { 467 // do not allow execution if nothing is selected or a sub command was selected 468 setEnabled(!tree.isSelectionEmpty() && tree.getSelectionPath().getPathCount() == 2); 469 } 470 } 471 472 class MouseEventHandler extends PopupMenuLauncher { 473 474 MouseEventHandler() { 475 super(new CommandStackPopup()); 476 } 477 478 @Override 479 public void mouseClicked(MouseEvent evt) { 480 if (isDoubleClick(evt)) { 481 selectAndZoomAction.actionPerformed(null); 482 } 483 } 484 } 485 486 private class CommandStackPopup extends JPopupMenu { 487 CommandStackPopup() { 488 add(selectAction); 489 add(selectAndZoomAction); 490 } 491 } 492}