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