Index: trunk/src/org/openstreetmap/josm/actions/MoveAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/actions/MoveAction.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/actions/MoveAction.java	(revision 3262)
@@ -94,5 +94,5 @@
         ? Main.main.undoRedo.commands.getLast() : null;
 
-        if (c instanceof MoveCommand && affectedNodes.equals(((MoveCommand)c).getMovedNodes())) {
+        if (c instanceof MoveCommand && affectedNodes.equals(((MoveCommand)c).getParticipatingPrimitives())) {
             ((MoveCommand)c).moveAgain(distx, disty);
         } else {
Index: trunk/src/org/openstreetmap/josm/actions/mapmode/ExtrudeAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/actions/mapmode/ExtrudeAction.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/actions/mapmode/ExtrudeAction.java	(revision 3262)
@@ -190,7 +190,7 @@
                 // Better way of testing list equality non-order-sensitively?
                 if (c instanceof MoveCommand
-                        && ((MoveCommand)c).getMovedNodes().contains(n1)
-                        && ((MoveCommand)c).getMovedNodes().contains(n2)
-                        && ((MoveCommand)c).getMovedNodes().size() == 2) {
+                        && ((MoveCommand)c).getParticipatingPrimitives().contains(n1)
+                        && ((MoveCommand)c).getParticipatingPrimitives().contains(n2)
+                        && ((MoveCommand)c).getParticipatingPrimitives().size() == 2) {
                     // MoveCommand doesn't let us know how much it has already moved the selection
                     // so we have to do some ugly record-keeping.
Index: trunk/src/org/openstreetmap/josm/actions/mapmode/SelectAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/actions/mapmode/SelectAction.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/actions/mapmode/SelectAction.java	(revision 3262)
@@ -233,5 +233,5 @@
 
             if (mode == Mode.move) {
-                if (c instanceof MoveCommand && affectedNodes.equals(((MoveCommand)c).getMovedNodes())) {
+                if (c instanceof MoveCommand && affectedNodes.equals(((MoveCommand)c).getParticipatingPrimitives())) {
                     ((MoveCommand)c).moveAgain(dx,dy);
                 } else {
Index: trunk/src/org/openstreetmap/josm/command/AddCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/AddCommand.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/command/AddCommand.java	(revision 3262)
@@ -6,8 +6,7 @@
 
 import java.util.Collection;
+import java.util.Collections;
 
 import javax.swing.JLabel;
-import javax.swing.tree.DefaultMutableTreeNode;
-import javax.swing.tree.MutableTreeNode;
 
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
@@ -61,5 +60,5 @@
     }
 
-    @Override public MutableTreeNode description() {
+    @Override public JLabel getDescription() {
         String msg;
         switch(OsmPrimitiveType.from(osm)) {
@@ -70,13 +69,13 @@
         }
 
-        return new DefaultMutableTreeNode(
-                new JLabel(
-                        tr(msg,
-                                osm.getDisplayName(DefaultNameFormatter.getInstance())
-                        ),
-                        ImageProvider.get(OsmPrimitiveType.from(osm)),
-                        JLabel.HORIZONTAL
-                )
-        );
+        return new JLabel(
+                tr(msg, osm.getDisplayName(DefaultNameFormatter.getInstance())),
+                ImageProvider.get(OsmPrimitiveType.from(osm)),
+                JLabel.HORIZONTAL);
+    }
+
+    @Override
+    public Collection<OsmPrimitive> getParticipatingPrimitives() {
+        return Collections.singleton(osm);
     }
 }
Index: trunk/src/org/openstreetmap/josm/command/AddPrimitivesCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/AddPrimitivesCommand.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/command/AddPrimitivesCommand.java	(revision 3262)
@@ -6,9 +6,8 @@
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.List;
 
 import javax.swing.JLabel;
-import javax.swing.tree.DefaultMutableTreeNode;
-import javax.swing.tree.MutableTreeNode;
 
 import org.openstreetmap.josm.data.osm.Node;
@@ -56,10 +55,7 @@
     }
 
-    @Override
-    public MutableTreeNode description() {
-        return new DefaultMutableTreeNode(
-                new JLabel(trn("Added {0} object", "Added {0} objects", data.size(), data.size()), null,
-                        JLabel.HORIZONTAL
-                )
+    @Override public JLabel getDescription() {
+        return new JLabel(trn("Added {0} object", "Added {0} objects", data.size(), data.size()), null,
+                            JLabel.HORIZONTAL
         );
     }
@@ -71,3 +67,14 @@
     }
 
+    @Override
+    public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
+        Collection<OsmPrimitive> prims = new HashSet<OsmPrimitive>();
+        for (PrimitiveData d : data) {
+            OsmPrimitive osm = getLayer().data.getPrimitiveById(d);
+            if (osm == null)
+                throw new RuntimeException();
+            prims.add(osm);
+        }
+        return prims;
+    }
 }
Index: trunk/src/org/openstreetmap/josm/command/ChangeCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/ChangeCommand.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/command/ChangeCommand.java	(revision 3262)
@@ -8,6 +8,4 @@
 
 import javax.swing.JLabel;
-import javax.swing.tree.DefaultMutableTreeNode;
-import javax.swing.tree.MutableTreeNode;
 
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
@@ -52,5 +50,5 @@
     }
 
-    @Override public MutableTreeNode description() {
+    @Override public JLabel getDescription() {
         String msg = "";
         switch(OsmPrimitiveType.from(osm)) {
@@ -59,9 +57,8 @@
         case RELATION: msg = marktr("Change relation {0}"); break;
         }
-        return new DefaultMutableTreeNode(
-                new JLabel(tr(msg,
-                        osm.getDisplayName(DefaultNameFormatter.getInstance()),
-                        ImageProvider.get(OsmPrimitiveType.from(osm)),
-                        JLabel.HORIZONTAL)));
+        return new JLabel(tr(msg,
+                    osm.getDisplayName(DefaultNameFormatter.getInstance())),
+                    ImageProvider.get(OsmPrimitiveType.from(osm)),
+                    JLabel.HORIZONTAL);
     }
 }
Index: trunk/src/org/openstreetmap/josm/command/ChangeNodesCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/ChangeNodesCommand.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/command/ChangeNodesCommand.java	(revision 3262)
@@ -8,6 +8,4 @@
 
 import javax.swing.JLabel;
-import javax.swing.tree.DefaultMutableTreeNode;
-import javax.swing.tree.MutableTreeNode;
 
 import org.openstreetmap.josm.data.osm.Node;
@@ -48,10 +46,9 @@
     }
 
-    @Override public MutableTreeNode description() {
+    @Override public JLabel getDescription() {
         String msg = tr("Changed nodes of {0}", way.getDisplayName(DefaultNameFormatter.getInstance()));
-        return new DefaultMutableTreeNode(
-                new JLabel(msg,
+        return new JLabel(msg,
                         ImageProvider.get(OsmPrimitiveType.WAY),
-                        JLabel.HORIZONTAL));
+                        JLabel.HORIZONTAL);
     }
 }
Index: trunk/src/org/openstreetmap/josm/command/ChangePropertyCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/ChangePropertyCommand.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/command/ChangePropertyCommand.java	(revision 3262)
@@ -5,11 +5,11 @@
 import static org.openstreetmap.josm.tools.I18n.tr;
 
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 
 import javax.swing.JLabel;
-import javax.swing.tree.DefaultMutableTreeNode;
-import javax.swing.tree.MutableTreeNode;
 
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
@@ -92,5 +92,5 @@
     }
 
-    @Override public MutableTreeNode description() {
+    @Override public JLabel getDescription() {
         String text;
         if (objects.size() == 1) {
@@ -117,17 +117,27 @@
                     : tr("Set {0}={1} for {2} objects", key, value, objects.size());
         }
-        DefaultMutableTreeNode root = new DefaultMutableTreeNode(new JLabel(text, ImageProvider.get("data", "key"), JLabel.HORIZONTAL));
+        return new JLabel(text, ImageProvider.get("data", "key"), JLabel.HORIZONTAL);
+    }
+
+    @Override public Collection<PseudoCommand> getChildren() {
         if (objects.size() == 1)
-            return root;
-        for (OsmPrimitive osm : objects) {
-            root.add(new DefaultMutableTreeNode(
-                    new JLabel(
-                            osm.getDisplayName(DefaultNameFormatter.getInstance()),
-                            ImageProvider.get(OsmPrimitiveType.from(osm)),
-                            JLabel.HORIZONTAL)
-            )
-            );
+            return null;
+        List<PseudoCommand> children = new ArrayList<PseudoCommand>();
+        for (final OsmPrimitive osm : objects) {
+            children.add(new PseudoCommand() {
+                @Override public JLabel getDescription() {
+                    return new JLabel(
+                                osm.getDisplayName(DefaultNameFormatter.getInstance()),
+                                ImageProvider.get(OsmPrimitiveType.from(osm)),
+                                JLabel.HORIZONTAL);
+
+                }
+                @Override public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
+                    return Collections.singleton(osm);
+                }
+
+            });
         }
-        return root;
+        return children;
     }
 }
Index: trunk/src/org/openstreetmap/josm/command/ChangeRelationMemberRoleCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/ChangeRelationMemberRoleCommand.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/command/ChangeRelationMemberRoleCommand.java	(revision 3262)
@@ -7,6 +7,4 @@
 
 import javax.swing.JLabel;
-import javax.swing.tree.DefaultMutableTreeNode;
-import javax.swing.tree.MutableTreeNode;
 
 import org.openstreetmap.josm.Main;
@@ -67,7 +65,6 @@
     }
 
-    @Override public MutableTreeNode description() {
-        return new DefaultMutableTreeNode(
-                new JLabel(
+    @Override public JLabel getDescription() {
+        return new JLabel(
                         tr("Change relation member role for {0} {1}",
                                 OsmPrimitiveType.from(relation),
@@ -75,5 +72,5 @@
                         ),
                         ImageProvider.get(OsmPrimitiveType.from(relation)),
-                        JLabel.HORIZONTAL)
+                        JLabel.HORIZONTAL
         );
     }
Index: trunk/src/org/openstreetmap/josm/command/Command.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/Command.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/command/Command.java	(revision 3262)
@@ -9,4 +9,5 @@
 import java.util.Map.Entry;
 
+import javax.swing.tree.DefaultMutableTreeNode;
 import javax.swing.tree.MutableTreeNode;
 
@@ -30,5 +31,5 @@
  * @author imi
  */
-abstract public class Command {
+abstract public class Command extends PseudoCommand {
 
     private static final class CloneVisitor extends AbstractVisitor {
@@ -132,5 +133,4 @@
      * Replies the layer this command is (or was) applied to.
      *
-     * @return
      */
     protected  OsmDataLayer getLayer() {
@@ -150,5 +150,27 @@
             Collection<OsmPrimitive> added);
 
-    abstract public MutableTreeNode description();
+    /**
+     * Return the primitives that take part in this command.
+     */
+    @Override public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
+        return cloneMap.keySet();
+    }
+
+    /**
+     * Provide a description that can be presented in a list or tree view.
+     * This override will be removed when
+     * <code>description()</code> is removed.
+     */
+    @Override public Object getDescription() {
+        return ((DefaultMutableTreeNode) description()).getUserObject();
+    }
+
+    /**
+     * @deprecated use getDescription() and getChildren() instead
+     */
+    @Deprecated
+    public MutableTreeNode description() {
+        return null;
+    }
 
 }
Index: trunk/src/org/openstreetmap/josm/command/ConflictAddCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/ConflictAddCommand.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/command/ConflictAddCommand.java	(revision 3262)
@@ -8,6 +8,4 @@
 import javax.swing.JLabel;
 import javax.swing.JOptionPane;
-import javax.swing.tree.DefaultMutableTreeNode;
-import javax.swing.tree.MutableTreeNode;
 
 import org.openstreetmap.josm.Main;
@@ -65,7 +63,6 @@
     }
 
-    @Override public MutableTreeNode description() {
-        return new DefaultMutableTreeNode(
-                new JLabel(
+    @Override public JLabel getDescription() {
+        return new JLabel(
                         tr("Add conflict for ''{0}''",
                                 conflict.getMy().getDisplayName(DefaultNameFormatter.getInstance())
@@ -73,5 +70,4 @@
                         ImageProvider.get(OsmPrimitiveType.from(conflict.getMy())),
                         JLabel.HORIZONTAL
-                )
         );
     }
Index: trunk/src/org/openstreetmap/josm/command/CoordinateConflictResolveCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/CoordinateConflictResolveCommand.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/command/CoordinateConflictResolveCommand.java	(revision 3262)
@@ -7,6 +7,4 @@
 
 import javax.swing.JLabel;
-import javax.swing.tree.DefaultMutableTreeNode;
-import javax.swing.tree.MutableTreeNode;
 
 import org.openstreetmap.josm.data.conflict.Conflict;
@@ -40,12 +38,9 @@
     }
 
-    @Override
-    public MutableTreeNode description() {
-        return new DefaultMutableTreeNode(
-                new JLabel(
+    @Override public JLabel getDescription() {
+        return new JLabel(
                         tr("Resolve conflicts in coordinates in {0}",conflict.getMy().getId()),
                         ImageProvider.get("data", "object"),
                         JLabel.HORIZONTAL
-                )
         );
     }
Index: trunk/src/org/openstreetmap/josm/command/DeleteCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/DeleteCommand.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/command/DeleteCommand.java	(revision 3262)
@@ -148,9 +148,7 @@
     public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted,
             Collection<OsmPrimitive> added) {
-        deleted.addAll(toDelete);
-    }
-
-    @Override
-    public MutableTreeNode description() {
+    }
+
+    @Override public JLabel getDescription() {
         if (toDelete.size() == 1) {
             OsmPrimitive primitive = toDelete.iterator().next();
@@ -162,34 +160,54 @@
             }
 
-            return new DefaultMutableTreeNode(new JLabel(tr(msg, primitive.getDisplayName(DefaultNameFormatter.getInstance())),
-                    ImageProvider.get(OsmPrimitiveType.from(primitive)), JLabel.HORIZONTAL));
-        }
-
-        Set<OsmPrimitiveType> typesToDelete = new HashSet<OsmPrimitiveType>();
-        for (OsmPrimitive osm : toDelete) {
-            typesToDelete.add(OsmPrimitiveType.from(osm));
-        }
-        String msg = "";
-        String apiname = "object";
-        if (typesToDelete.size() > 1) {
-            msg = trn("Delete {0} object", "Delete {0} objects", toDelete.size(), toDelete.size());
+            return new JLabel(tr(msg, primitive.getDisplayName(DefaultNameFormatter.getInstance())),
+                    ImageProvider.get(OsmPrimitiveType.from(primitive)), JLabel.HORIZONTAL);
         } else {
-            OsmPrimitiveType t = typesToDelete.iterator().next();
-            apiname = t.getAPIName();
-            switch(t) {
-            case NODE: msg = trn("Delete {0} node", "Delete {0} nodes", toDelete.size(), toDelete.size()); break;
-            case WAY: msg = trn("Delete {0} way", "Delete {0} ways", toDelete.size(), toDelete.size()); break;
-            case RELATION: msg = trn("Delete {0} relation", "Delete {0} relations", toDelete.size(), toDelete.size()); break;
-            }
-        }
-        DefaultMutableTreeNode root = new DefaultMutableTreeNode(
-                new JLabel(msg, ImageProvider.get("data", apiname), JLabel.HORIZONTAL)
-        );
-        for (OsmPrimitive osm : toDelete) {
-            root.add(new DefaultMutableTreeNode(new JLabel(
-                    osm.getDisplayName(DefaultNameFormatter.getInstance()),
-                    ImageProvider.get(OsmPrimitiveType.from(osm)), JLabel.HORIZONTAL)));
-        }
-        return root;
+            Set<OsmPrimitiveType> typesToDelete = new HashSet<OsmPrimitiveType>();
+            for (OsmPrimitive osm : toDelete) {
+                typesToDelete.add(OsmPrimitiveType.from(osm));
+            }
+            String msg = "";
+            String apiname = "object";
+            if (typesToDelete.size() > 1) {
+                msg = trn("Delete {0} object", "Delete {0} objects", toDelete.size(), toDelete.size());
+            } else {
+                OsmPrimitiveType t = typesToDelete.iterator().next();
+                apiname = t.getAPIName();
+                switch(t) {
+                case NODE: msg = trn("Delete {0} node", "Delete {0} nodes", toDelete.size(), toDelete.size()); break;
+                case WAY: msg = trn("Delete {0} way", "Delete {0} ways", toDelete.size(), toDelete.size()); break;
+                case RELATION: msg = trn("Delete {0} relation", "Delete {0} relations", toDelete.size(), toDelete.size()); break;
+                }
+            }
+            return  new JLabel(msg, ImageProvider.get("data", apiname), JLabel.HORIZONTAL);
+        }
+    }
+
+    @Override public Collection<PseudoCommand> getChildren() {
+        if (toDelete.size() == 1) {
+            return null;
+        } else {
+            List<PseudoCommand> children = new ArrayList<PseudoCommand>();
+            for (final OsmPrimitive osm : toDelete) {
+                children.add(new PseudoCommand() {
+                    @Override public JLabel getDescription() {
+                        return new JLabel(
+                            tr("Deleted ''{0}''",
+                                osm.getDisplayName(DefaultNameFormatter.getInstance())),
+                            ImageProvider.get(OsmPrimitiveType.from(osm)), JLabel.HORIZONTAL);
+                        }
+                    @Override public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
+                        return Collections.singleton(osm);
+                    }
+
+                });
+            }
+            return children;
+
+        }
+    }
+
+    @Override public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
+        return toDelete;
     }
 
Index: trunk/src/org/openstreetmap/josm/command/DeletedStateConflictResolveCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/DeletedStateConflictResolveCommand.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/command/DeletedStateConflictResolveCommand.java	(revision 3262)
@@ -7,6 +7,4 @@
 
 import javax.swing.JLabel;
-import javax.swing.tree.DefaultMutableTreeNode;
-import javax.swing.tree.MutableTreeNode;
 
 import org.openstreetmap.josm.data.conflict.Conflict;
@@ -40,12 +38,9 @@
     }
 
-    @Override
-    public MutableTreeNode description() {
-        return new DefaultMutableTreeNode(
-                new JLabel(
+    @Override public JLabel getDescription() {
+        return new JLabel(
                         tr("Resolve conflicts in deleted state in {0}",conflict.getMy().getId()),
                         ImageProvider.get("data", "object"),
                         JLabel.HORIZONTAL
-                )
         );
     }
Index: trunk/src/org/openstreetmap/josm/command/ModifiedConflictResolveCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/ModifiedConflictResolveCommand.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/command/ModifiedConflictResolveCommand.java	(revision 3262)
@@ -8,6 +8,4 @@
 
 import javax.swing.JLabel;
-import javax.swing.tree.DefaultMutableTreeNode;
-import javax.swing.tree.MutableTreeNode;
 
 import org.openstreetmap.josm.data.conflict.Conflict;
@@ -35,6 +33,5 @@
     }
 
-    @Override
-    public MutableTreeNode description() {
+    @Override public JLabel getDescription() {
         String msg = "";
         switch(OsmPrimitiveType.from(conflict.getMy())) {
@@ -43,10 +40,8 @@
         case RELATION: msg = marktr("Set the ''modified'' flag for relation {0}"); break;
         }
-        return new DefaultMutableTreeNode(
-                new JLabel(
+        return new JLabel(
                         tr(msg,conflict.getMy().getId()),
                         ImageProvider.get("data", "object"),
                         JLabel.HORIZONTAL
-                )
         );
     }
Index: trunk/src/org/openstreetmap/josm/command/MoveCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/MoveCommand.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/command/MoveCommand.java	(revision 3262)
@@ -11,6 +11,4 @@
 
 import javax.swing.JLabel;
-import javax.swing.tree.DefaultMutableTreeNode;
-import javax.swing.tree.MutableTreeNode;
 
 import org.openstreetmap.josm.data.coor.LatLon;
@@ -118,10 +116,19 @@
     }
 
-    @Override public MutableTreeNode description() {
-        return new DefaultMutableTreeNode(new JLabel(trn("Move {0} node", "Move {0} nodes", nodes.size(), nodes.size()), ImageProvider.get("data", "node"), JLabel.HORIZONTAL));
+    @Override public JLabel getDescription() {
+        return new JLabel(trn("Move {0} node", "Move {0} nodes", nodes.size(), nodes.size()), ImageProvider.get("data", "node"), JLabel.HORIZONTAL);
     }
 
+    /**
+     * @Deprecated use getParticipatingPrimitives() instead
+     */
+    @Deprecated
     public Collection<Node> getMovedNodes() {
         return nodes;
     }
+
+    @Override
+    public Collection<Node> getParticipatingPrimitives() {
+        return nodes;
+    }
 }
Index: trunk/src/org/openstreetmap/josm/command/PseudoCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/PseudoCommand.java	(revision 3262)
+++ trunk/src/org/openstreetmap/josm/command/PseudoCommand.java	(revision 3262)
@@ -0,0 +1,38 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.command;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.MutableTreeNode;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.PrimitiveData;
+
+/**
+ * PseudoCommand is a reduced form of a command. It can be presented in a tree view
+ * as subcommand of real commands but it is just an empty shell and can not be
+ * executed or undone.
+ */
+abstract public class PseudoCommand {
+    /**
+     * Provide a description that can be presented in a list or tree view.
+     */
+    abstract public Object getDescription();
+
+    /**
+     * Return the primitives that take part in this command.
+     */
+    abstract public Collection<? extends OsmPrimitive> getParticipatingPrimitives();
+
+    /**
+     * Returns the subcommands of this command.
+     * Override for subclasses that have child commands.
+     * @return the subcommands, null if there are no child commands
+     */
+    public Collection<PseudoCommand> getChildren() {
+        return null;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/command/PurgePrimitivesCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/PurgePrimitivesCommand.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/command/PurgePrimitivesCommand.java	(revision 3262)
@@ -5,13 +5,13 @@
 import static org.openstreetmap.josm.tools.I18n.trn;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 import java.util.logging.Logger;
 
 import javax.swing.JLabel;
-import javax.swing.tree.DefaultMutableTreeNode;
-import javax.swing.tree.MutableTreeNode;
 
 import org.openstreetmap.josm.Main;
@@ -96,31 +96,38 @@
     }
 
-    protected MutableTreeNode getDescription(OsmPrimitive primitive) {
-        return new DefaultMutableTreeNode(
-                new JLabel(
-                        tr("Purged object ''{0}''", primitive.getDisplayName(DefaultNameFormatter.getInstance())),
+    @Override public JLabel getDescription() {
+        if (purgedPrimitives.size() == 1) {
+            return new JLabel(
+                tr("Purged object ''{0}''",
+                        purgedPrimitives.iterator().next().getDisplayName(DefaultNameFormatter.getInstance())),
+                ImageProvider.get("data", "object"),
+                JLabel.HORIZONTAL
+            );
+        } else {
+            return new JLabel(trn("Purged {0} object", "Purged {0} objects", purgedPrimitives.size(), purgedPrimitives.size()));
+        }
+    }
+
+    @Override public Collection<PseudoCommand> getChildren() {
+        if (purgedPrimitives.size() == 1)
+            return null;
+        List<PseudoCommand> children = new ArrayList<PseudoCommand>();
+        for (final OsmPrimitive osm : purgedPrimitives) {
+            children.add(new PseudoCommand() {
+                @Override public JLabel getDescription() {
+                    return new JLabel(
+                        tr("Purged object ''{0}''",
+                                osm.getDisplayName(DefaultNameFormatter.getInstance())),
                         ImageProvider.get("data", "object"),
                         JLabel.HORIZONTAL
-                )
-        );
-    }
-
-    protected MutableTreeNode getDescription(Collection<OsmPrimitive> primitives) {
-
-        DefaultMutableTreeNode root = new DefaultMutableTreeNode(
-                trn("Purged {0} object", "Purged {0} objects", primitives.size(), primitives.size())
-        );
-        for (OsmPrimitive p : primitives) {
-            root.add(getDescription(p));
-        }
-        return root;
-    }
-
-    @Override
-    public MutableTreeNode description() {
-        if (purgedPrimitives.size() == 1)
-            return getDescription(purgedPrimitives.iterator().next());
-        else
-            return getDescription(purgedPrimitives);
+                    );
+                }
+                @Override public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
+                    return Collections.singleton(osm);
+                }
+
+            });
+        }
+        return children;
     }
 
Index: trunk/src/org/openstreetmap/josm/command/RelationMemberConflictResolverCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/RelationMemberConflictResolverCommand.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/command/RelationMemberConflictResolverCommand.java	(revision 3262)
@@ -9,6 +9,4 @@
 
 import javax.swing.JLabel;
-import javax.swing.tree.DefaultMutableTreeNode;
-import javax.swing.tree.MutableTreeNode;
 
 import org.openstreetmap.josm.Main;
@@ -51,12 +49,9 @@
     }
 
-    @Override
-    public MutableTreeNode description() {
-        return new DefaultMutableTreeNode(
-                new JLabel(
+    @Override public JLabel getDescription() {
+        return new JLabel(
                         tr("Resolve conflicts in member list of relation {0}", my.getId()),
                         ImageProvider.get("data", "object"),
                         JLabel.HORIZONTAL
-                )
         );
     }
Index: trunk/src/org/openstreetmap/josm/command/RotateCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/RotateCommand.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/command/RotateCommand.java	(revision 3262)
@@ -9,6 +9,4 @@
 
 import javax.swing.JLabel;
-import javax.swing.tree.DefaultMutableTreeNode;
-import javax.swing.tree.MutableTreeNode;
 
 import org.openstreetmap.josm.data.coor.EastNorth;
@@ -138,6 +136,6 @@
     }
 
-    @Override public MutableTreeNode description() {
-        return new DefaultMutableTreeNode(new JLabel(trn("Rotate {0} node", "Rotate {0} nodes", nodes.size(), nodes.size()), ImageProvider.get("data", "node"), JLabel.HORIZONTAL));
+    @Override public JLabel getDescription() {
+        return new JLabel(trn("Rotate {0} node", "Rotate {0} nodes", nodes.size(), nodes.size()), ImageProvider.get("data", "node"), JLabel.HORIZONTAL);
     }
 
Index: trunk/src/org/openstreetmap/josm/command/SequenceCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/SequenceCommand.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/command/SequenceCommand.java	(revision 3262)
@@ -6,10 +6,12 @@
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
 
-import javax.swing.tree.DefaultMutableTreeNode;
-import javax.swing.tree.MutableTreeNode;
+import javax.swing.JLabel;
 
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.tools.ImageProvider;
 
 /**
@@ -88,10 +90,20 @@
     }
 
-    @Override public MutableTreeNode description() {
-        DefaultMutableTreeNode root = new DefaultMutableTreeNode(tr("Sequence")+": "+name);
+    @Override public JLabel getDescription() {
+        return new JLabel(tr("Sequence")+": "+name, ImageProvider.get("data", "sequence"), JLabel.HORIZONTAL);
+    }
+
+    @Override
+    public Collection<PseudoCommand> getChildren() {
+        return (List) Arrays.asList(sequence);
+    }
+
+    @Override
+    public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
+        Collection<OsmPrimitive> prims = new HashSet<OsmPrimitive>();
         for (Command c : sequence) {
-            root.add(c.description());
+            prims.addAll(c.getParticipatingPrimitives());
         }
-        return root;
+        return prims;
     }
 }
Index: trunk/src/org/openstreetmap/josm/command/TagConflictResolveCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/TagConflictResolveCommand.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/command/TagConflictResolveCommand.java	(revision 3262)
@@ -10,6 +10,4 @@
 
 import javax.swing.JLabel;
-import javax.swing.tree.DefaultMutableTreeNode;
-import javax.swing.tree.MutableTreeNode;
 
 import org.openstreetmap.josm.data.conflict.Conflict;
@@ -61,6 +59,5 @@
     }
 
-    @Override
-    public MutableTreeNode description() {
+    @Override public JLabel getDescription() {
         String msg = "";
         switch(OsmPrimitiveType.from(conflict.getMy())) {
@@ -69,10 +66,8 @@
         case RELATION: msg = marktr("Resolve {0} tag conflicts in relation {1}"); break;
         }
-        return new DefaultMutableTreeNode(
-                new JLabel(
+        return new JLabel(
                         tr(msg,getNumDecidedConflicts(), conflict.getMy().getId()),
                         ImageProvider.get("data", "object"),
                         JLabel.HORIZONTAL
-                )
         );
     }
Index: trunk/src/org/openstreetmap/josm/command/UndeletePrimitivesCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/UndeletePrimitivesCommand.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/command/UndeletePrimitivesCommand.java	(revision 3262)
@@ -11,6 +11,4 @@
 
 import javax.swing.JLabel;
-import javax.swing.tree.DefaultMutableTreeNode;
-import javax.swing.tree.MutableTreeNode;
 
 import org.openstreetmap.josm.data.osm.DataSet;
@@ -62,12 +60,9 @@
     }
 
-    @Override
-    public MutableTreeNode description() {
-        return new DefaultMutableTreeNode(
-                new JLabel(
+    @Override public JLabel getDescription() {
+        return new JLabel(
                         trn("Undelete {0} primitive", "Undelete {0} primitives", toUndelete.size(), toUndelete.size()),
                         ImageProvider.get("data", "object"),
                         JLabel.HORIZONTAL
-                )
         );
     }
Index: trunk/src/org/openstreetmap/josm/command/VersionConflictResolveCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/VersionConflictResolveCommand.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/command/VersionConflictResolveCommand.java	(revision 3262)
@@ -8,6 +8,4 @@
 
 import javax.swing.JLabel;
-import javax.swing.tree.DefaultMutableTreeNode;
-import javax.swing.tree.MutableTreeNode;
 
 import org.openstreetmap.josm.data.conflict.Conflict;
@@ -35,6 +33,5 @@
     }
 
-    @Override
-    public MutableTreeNode description() {
+    @Override public JLabel getDescription() {
         String msg = "";
         switch(OsmPrimitiveType.from(conflict.getMy())) {
@@ -43,10 +40,8 @@
         case RELATION: msg = marktr("Resolve version conflict for relation {0}"); break;
         }
-        return new DefaultMutableTreeNode(
-                new JLabel(
+        return new JLabel(
                         tr(msg,conflict.getMy().getId()),
                         ImageProvider.get("data", "object"),
                         JLabel.HORIZONTAL
-                )
         );
     }
Index: trunk/src/org/openstreetmap/josm/command/WayNodesConflictResolverCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/WayNodesConflictResolverCommand.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/command/WayNodesConflictResolverCommand.java	(revision 3262)
@@ -9,6 +9,4 @@
 
 import javax.swing.JLabel;
-import javax.swing.tree.DefaultMutableTreeNode;
-import javax.swing.tree.MutableTreeNode;
 
 import org.openstreetmap.josm.data.conflict.Conflict;
@@ -47,12 +45,9 @@
     }
 
-    @Override
-    public MutableTreeNode description() {
-        return new DefaultMutableTreeNode(
-                new JLabel(
+    @Override public JLabel getDescription() {
+        return new JLabel(
                         tr("Resolve conflicts in node list of way {0}", conflict.getMy().getId()),
                         ImageProvider.get("data", "object"),
                         JLabel.HORIZONTAL
-                )
         );
     }
Index: trunk/src/org/openstreetmap/josm/data/UndoRedoHandler.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/UndoRedoHandler.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/data/UndoRedoHandler.java	(revision 3262)
@@ -23,5 +23,5 @@
      * The stack for redoing commands
      */
-    private final Stack<Command> redoCommands = new Stack<Command>();
+    public final LinkedList<Command> redoCommands = new LinkedList<Command>();
 
     public final LinkedList<CommandQueueListener> listenerCommands = new LinkedList<CommandQueueListener>();
@@ -64,11 +64,22 @@
      * Undoes the last added command.
      */
-    synchronized public void undo() {
+    public void undo() {
+        undo(1);
+    }
+
+    /**
+     * Undoes multiple commands.
+     */
+    synchronized public void undo(int num) {
         if (commands.isEmpty())
             return;
         Collection<? extends OsmPrimitive> oldSelection = Main.main.getCurrentDataSet().getSelected();
-        final Command c = commands.removeLast();
-        c.undoCommand();
-        redoCommands.push(c);
+        for (int i=1; i<=num; ++i) {
+            final Command c = commands.removeLast();
+            c.undoCommand();
+            redoCommands.addFirst(c);
+            if (commands.isEmpty())
+                break;
+        }
         fireCommandsChanged();
         Collection<? extends OsmPrimitive> newSelection = Main.main.getCurrentDataSet().getSelected();
@@ -80,13 +91,23 @@
     /**
      * Redoes the last undoed command.
-     * TODO: This has to be moved to a central place in order to support multiple layers.
      */
     public void redo() {
+        redo(1);
+    }
+
+    /**
+     * Redoes multiple commands.
+     */
+    public void redo(int num) {
         if (redoCommands.isEmpty())
             return;
         Collection<? extends OsmPrimitive> oldSelection = Main.main.getCurrentDataSet().getSelected();
-        final Command c = redoCommands.pop();
-        c.executeCommand();
-        commands.add(c);
+        for (int i=0; i<num; ++i) {
+            final Command c = redoCommands.pop();
+            c.executeCommand();
+            commands.add(c);
+            if (redoCommands.isEmpty())
+                break;
+        }
         fireCommandsChanged();
         Collection<? extends OsmPrimitive> newSelection = Main.main.getCurrentDataSet().getSelected();
Index: trunk/src/org/openstreetmap/josm/data/osm/OsmPrimitive.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/OsmPrimitive.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/data/osm/OsmPrimitive.java	(revision 3262)
@@ -383,7 +383,8 @@
 
     /**
-     * Replies <code>true</code>, if the object is usable.
-     *
-     * @return <code>true</code>, if the object is unusable.
+     * Replies <code>true</code>, if the object is usable (i.e. complete
+     * and not deleted).
+     *
+     * @return <code>true</code>, if the object is usable.
      * @see #delete(boolean)
      */
Index: trunk/src/org/openstreetmap/josm/data/osm/Relation.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/Relation.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/data/osm/Relation.java	(revision 3262)
@@ -53,6 +53,5 @@
 
     /**
-     *
-     * @since 1926
+     * @return number of members
      */
     public int getMembersCount() {
@@ -60,19 +59,8 @@
     }
 
-    /**
-     *
-     * @param index
-     * @return
-     * @since 1926
-     */
     public RelationMember getMember(int index) {
         return members.get(index);
     }
 
-    /**
-     *
-     * @param member
-     * @since 1951
-     */
     public void addMember(RelationMember member) {
         members.add(member);
@@ -81,10 +69,4 @@
     }
 
-    /**
-     *
-     * @param index
-     * @param member
-     * @since 1951
-     */
     public void addMember(int index, RelationMember member) {
         members.add(index, member);
@@ -98,5 +80,4 @@
      * @param member
      * @return Member that was at the position
-     * @since 1951
      */
     public RelationMember setMember(int index, RelationMember member) {
@@ -114,5 +95,4 @@
      * @param index
      * @return Member that was at the position
-     * @since 1951
      */
     public RelationMember removeMember(int index) {
Index: trunk/src/org/openstreetmap/josm/data/osm/User.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/User.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/data/osm/User.java	(revision 3262)
@@ -190,3 +190,16 @@
         return true;
     }
+
+    @Override
+    public String toString() {
+        StringBuffer s = new StringBuffer();
+        s.append("id:"+uid);
+        if (names.size() == 1) {
+            s.append(" name:"+getName());
+        }
+        else if (names.size() > 1) {
+            s.append(String.format(" %d names:%s", names.size(), getName()));
+        }
+        return s.toString();
+    }
 }
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/CommandListMutableTreeNode.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/CommandListMutableTreeNode.java	(revision 3262)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/CommandListMutableTreeNode.java	(revision 3262)
@@ -0,0 +1,30 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.dialogs;
+
+import javax.swing.tree.DefaultMutableTreeNode;
+
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.command.PseudoCommand;
+
+/**
+ * MutableTreeNode implementation for Command list JTree
+ */
+public class CommandListMutableTreeNode extends DefaultMutableTreeNode {
+
+    protected PseudoCommand cmd;
+    protected int idx;
+
+    public CommandListMutableTreeNode(PseudoCommand cmd, int idx) {
+        super(cmd.getDescription());
+        this.cmd = cmd;
+        this.idx = idx;
+    }
+
+    public PseudoCommand getCommand() {
+        return cmd;
+    }
+
+    public int getIndex() {
+        return idx;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/CommandStackDialog.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/CommandStackDialog.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/CommandStackDialog.java	(revision 3262)
@@ -6,24 +6,68 @@
 import java.awt.BorderLayout;
 import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.GridBagLayout;
+import java.awt.Point;
+import java.awt.event.ActionEvent;
 import java.awt.event.KeyEvent;
-import java.util.Collection;
-
+import java.awt.event.MouseEvent;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.swing.AbstractAction;
+import javax.swing.Box;
 import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
 import javax.swing.JScrollPane;
+import javax.swing.JSeparator;
 import javax.swing.JTree;
+import javax.swing.event.TreeModelEvent;
+import javax.swing.event.TreeModelListener;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.event.TreeSelectionListener;
 import javax.swing.tree.DefaultMutableTreeNode;
 import javax.swing.tree.DefaultTreeCellRenderer;
 import javax.swing.tree.DefaultTreeModel;
+import javax.swing.tree.TreePath;
+import javax.swing.tree.TreeSelectionModel;
 
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.command.PseudoCommand;
+import org.openstreetmap.josm.data.osm.DatasetCollection;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.gui.SideButton;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer.CommandQueueListener;
+import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.Predicate;
 import org.openstreetmap.josm.tools.Shortcut;
 
 public class CommandStackDialog extends ToggleDialog implements CommandQueueListener {
 
-    private DefaultTreeModel treeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
-    private JTree tree = new JTree(treeModel);
+    private DefaultTreeModel undoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
+    private DefaultTreeModel redoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
+
+    private JTree undoTree = new JTree(undoTreeModel);
+    private JTree redoTree = new JTree(redoTreeModel);
+
+    private UndoRedoSelectionListener undoSelectionListener;
+    private UndoRedoSelectionListener redoSelectionListener;
+
+    private JScrollPane scrollPane;
+    private JSeparator separator = new JSeparator();
+    // only visible, if separator is the top most component
+    private Component spacer = Box.createRigidArea(new Dimension(0, 3));
+
+    // last operation is remembered to select the next undo/redo entry in the list
+    // after undo/redo command
+    private UndoRedoType lastOperation = UndoRedoType.UNDO;
 
     public CommandStackDialog(final MapFrame mapFrame) {
@@ -32,54 +76,381 @@
         Main.main.undoRedo.listenerCommands.add(this);
 
-        tree.setRootVisible(false);
-        tree.setShowsRootHandles(true);
-        tree.expandRow(0);
-        tree.setCellRenderer(new DefaultTreeCellRenderer(){
-            @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) {
-                super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
-                DefaultMutableTreeNode v = (DefaultMutableTreeNode)value;
-                if (v.getUserObject() instanceof JLabel) {
-                    JLabel l = (JLabel)v.getUserObject();
-                    setIcon(l.getIcon());
-                    setText(l.getText());
-                }
-                return this;
+        undoTree.addMouseListener(new PopupMenuHandler());
+        undoTree.setRootVisible(false);
+        undoTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
+        undoTree.setShowsRootHandles(true);
+        undoTree.expandRow(0);
+        undoTree.setCellRenderer(new CommandCellRenderer());
+        undoSelectionListener = new UndoRedoSelectionListener(undoTree);
+        undoTree.getSelectionModel().addTreeSelectionListener(undoSelectionListener);
+
+        redoTree.addMouseListener(new PopupMenuHandler());
+        redoTree.setRootVisible(false);
+        redoTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
+        redoTree.setShowsRootHandles(true);
+        redoTree.expandRow(0);
+        redoTree.setCellRenderer(new CommandCellRenderer());
+        redoSelectionListener = new UndoRedoSelectionListener(redoTree);
+        redoTree.getSelectionModel().addTreeSelectionListener(redoSelectionListener);
+
+        JPanel treesPanel = new JPanel(new GridBagLayout());
+
+        treesPanel.add(spacer, GBC.eol());
+        spacer.setVisible(false);
+        treesPanel.add(undoTree, GBC.eol().fill(GBC.HORIZONTAL));
+        separator.setVisible(false);
+        treesPanel.add(separator, GBC.eol().fill(GBC.HORIZONTAL));
+        treesPanel.add(redoTree, GBC.eol().fill(GBC.HORIZONTAL));
+        treesPanel.add(Box.createRigidArea(new Dimension(0, 0)), GBC.std().weight(0, 1));
+        treesPanel.setBackground(redoTree.getBackground());
+
+        scrollPane = new JScrollPane(treesPanel);
+        add(scrollPane, BorderLayout.CENTER);
+        add(createButtonPanel(), BorderLayout.SOUTH);
+    }
+
+    private static class CommandCellRenderer extends DefaultTreeCellRenderer {
+        @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) {
+            super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
+            DefaultMutableTreeNode v = (DefaultMutableTreeNode)value;
+            if (v.getUserObject() instanceof JLabel) {
+                JLabel l = (JLabel)v.getUserObject();
+                setIcon(l.getIcon());
+                setText(l.getText());
+            }
+            return this;
+        }
+    }
+
+    /**
+     * Selection listener for undo and redo area.
+     * If one is clicked, takes away the selection from the other, so
+     * it behaves as if it was one component.
+     */
+    private class UndoRedoSelectionListener implements TreeSelectionListener {
+        private JTree source;
+
+        public UndoRedoSelectionListener(JTree source) {
+            this.source = source;
+        }
+
+        @Override
+        public void valueChanged(TreeSelectionEvent e) {
+            if (source == undoTree) {
+                redoTree.getSelectionModel().removeTreeSelectionListener(redoSelectionListener);
+                redoTree.clearSelection();
+                redoTree.getSelectionModel().addTreeSelectionListener(redoSelectionListener);
+            }
+            if (source == redoTree) {
+                undoTree.getSelectionModel().removeTreeSelectionListener(undoSelectionListener);
+                undoTree.clearSelection();
+                undoTree.getSelectionModel().addTreeSelectionListener(undoSelectionListener);
+            }
+        }
+    }
+
+    protected JPanel createButtonPanel() {
+        JPanel buttonPanel = getButtonPanel(3);
+
+        SelectAction selectAction = new SelectAction();
+        wireUpdateEnabledStateUpdater(selectAction, undoTree);
+        wireUpdateEnabledStateUpdater(selectAction, redoTree);
+        buttonPanel.add(new SideButton(selectAction));
+
+        UndoRedoAction undoAction = new UndoRedoAction(UndoRedoType.UNDO);
+        wireUpdateEnabledStateUpdater(undoAction, undoTree);
+        buttonPanel.add(new SideButton(undoAction));
+
+        UndoRedoAction redoAction = new UndoRedoAction(UndoRedoType.REDO);
+        wireUpdateEnabledStateUpdater(redoAction, redoTree);
+        buttonPanel.add(new SideButton(redoAction));
+
+        return buttonPanel;
+    }
+
+    /**
+     * Interface to provide a callback for enabled state update.
+     */
+    protected interface IEnabledStateUpdating {
+        void updateEnabledState();
+    }
+
+    /**
+     * Wires updater for enabled state to the events.
+     */
+    protected void wireUpdateEnabledStateUpdater(final IEnabledStateUpdating updater, JTree tree) {
+        addShowNotifyListener(updater);
+
+        tree.addTreeSelectionListener(new TreeSelectionListener() {
+            @Override
+            public void valueChanged(TreeSelectionEvent e) {
+                updater.updateEnabledState();
             }
         });
-        tree.setVisibleRowCount(8);
-        add(new JScrollPane(tree), BorderLayout.CENTER);
-    }
-
-    @Override public void setVisible(boolean v) {
-        if (v) {
-            buildList();
-        } else if (tree != null) {
-            treeModel.setRoot(new DefaultMutableTreeNode());
-        }
-        super.setVisible(v);
-    }
-
-    private void buildList() {
-        if(Main.main.undoRedo.commands.size() != 0) {
-            setTitle(tr("Command Stack: {0}", Main.main.undoRedo.commands.size()));
-        } else {
-            setTitle(tr("Command Stack"));
-        }
+
+        tree.getModel().addTreeModelListener(new TreeModelListener() {
+            @Override
+            public void treeNodesChanged(TreeModelEvent e) {
+                updater.updateEnabledState();
+            }
+
+            @Override
+            public void treeNodesInserted(TreeModelEvent e) {
+                updater.updateEnabledState();
+            }
+
+            @Override
+            public void treeNodesRemoved(TreeModelEvent e) {
+                updater.updateEnabledState();
+            }
+
+            @Override
+            public void treeStructureChanged(TreeModelEvent e) {
+                updater.updateEnabledState();
+            }
+        });
+    }
+
+    @Override
+    public void showNotify() {
+        buildTrees();
+        for (IEnabledStateUpdating listener : showNotifyListener) {
+            listener.updateEnabledState();
+        }
+    }
+
+    /**
+     * Simple listener setup to update the button enabled state when the side dialog shows.
+     */
+    Set<IEnabledStateUpdating> showNotifyListener = new LinkedHashSet<IEnabledStateUpdating>();
+
+    private void addShowNotifyListener(IEnabledStateUpdating listener) {
+        showNotifyListener.add(listener);
+    }
+
+    @Override
+    public void hideNotify() {
+        undoTreeModel.setRoot(new DefaultMutableTreeNode());
+        redoTreeModel.setRoot(new DefaultMutableTreeNode());
+    }
+
+    /**
+     * Build the trees of undo and redo commands (initially or when
+     * they have changed).
+     */
+    private void buildTrees() {
+        setTitle(tr("Command Stack"));
         if (Main.map == null || Main.map.mapView == null || Main.map.mapView.getEditLayer() == null)
             return;
-        Collection<Command> commands = Main.main.undoRedo.commands;
-        DefaultMutableTreeNode root = new DefaultMutableTreeNode();
-        for (Command c : commands) {
-            root.add(c.description());
-        }
-        treeModel.setRoot(root);
-        tree.scrollRowToVisible(treeModel.getChildCount(root)-1);
-    }
-
+
+        List<Command> undoCommands = Main.main.undoRedo.commands;
+        DefaultMutableTreeNode undoRoot = new DefaultMutableTreeNode();
+        for (int i=0; i<undoCommands.size(); ++i) {
+            undoRoot.add(getNodeForCommand(undoCommands.get(i), i));
+        }
+        undoTreeModel.setRoot(undoRoot);
+        undoTree.scrollRowToVisible(undoTreeModel.getChildCount(undoRoot)-1);
+        scrollPane.getHorizontalScrollBar().setValue(0);
+
+        List<Command> redoCommands = Main.main.undoRedo.redoCommands;
+        DefaultMutableTreeNode redoRoot = new DefaultMutableTreeNode();
+        for (int i=0; i<redoCommands.size(); ++i) {
+            redoRoot.add(getNodeForCommand(redoCommands.get(i), i));
+        }
+        redoTreeModel.setRoot(redoRoot);
+        if (redoTreeModel.getChildCount(redoRoot) > 0) {
+            redoTree.scrollRowToVisible(0);
+            scrollPane.getHorizontalScrollBar().setValue(0);
+        }
+
+        separator.setVisible(!undoCommands.isEmpty() || !redoCommands.isEmpty());
+        spacer.setVisible(undoCommands.isEmpty() && !redoCommands.isEmpty());
+
+        // if one tree is empty, move selection to the other
+        switch (lastOperation) {
+            case UNDO:
+                if (undoCommands.isEmpty()) {
+                    lastOperation = UndoRedoType.REDO;
+                }
+                break;
+            case REDO:
+                if (redoCommands.isEmpty()) {
+                    lastOperation = UndoRedoType.UNDO;
+                }
+                break;
+        }
+
+        // select the next command to undo/redo
+        switch (lastOperation) {
+            case UNDO:
+                undoTree.setSelectionRow(undoTree.getRowCount()-1);
+                break;
+            case REDO:
+                redoTree.setSelectionRow(0);
+                break;
+        }
+    }
+
+    /**
+     * Wraps a command in a CommandListMutableTreeNode.
+     * Recursively adds child commands.
+     */
+    protected CommandListMutableTreeNode getNodeForCommand(PseudoCommand c, int idx) {
+        CommandListMutableTreeNode node = new CommandListMutableTreeNode(c, idx);
+        if (c.getChildren() != null) {
+            List<PseudoCommand> children = new ArrayList<PseudoCommand>(c.getChildren());
+            for (int i=0; i<children.size(); ++i) {
+                node.add(getNodeForCommand(children.get(i), i));
+            }
+        }
+        return node;
+    }
+
+    @Override
     public void commandChanged(int queueSize, int redoSize) {
         if (!isVisible())
             return;
-        treeModel.setRoot(new DefaultMutableTreeNode());
-        buildList();
+        buildTrees();
+    }
+
+    public class SelectAction extends AbstractAction implements IEnabledStateUpdating {
+
+        public SelectAction() {
+            super();
+            putValue(NAME,tr("Select"));
+            putValue(SHORT_DESCRIPTION, tr("Selects the objects that take part in this command (unless currently deleted)"));
+            putValue(SMALL_ICON, ImageProvider.get("dialogs","select"));
+
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            TreePath path;
+            undoTree.getSelectionPath();
+            if (!undoTree.isSelectionEmpty()) {
+                path = undoTree.getSelectionPath();
+            } else if (!redoTree.isSelectionEmpty()) {
+                path = redoTree.getSelectionPath();
+            } else
+                throw new IllegalStateException();
+
+            if (Main.map == null || Main.map.mapView == null || Main.map.mapView.getEditLayer() == null) return;
+            PseudoCommand c = ((CommandListMutableTreeNode) path.getLastPathComponent()).getCommand();
+
+            final OsmDataLayer currentLayer = Main.map.mapView.getEditLayer();
+
+            DatasetCollection<OsmPrimitive> prims = new DatasetCollection<OsmPrimitive>(
+                    c.getParticipatingPrimitives(),
+                    new Predicate<OsmPrimitive>(){
+                        @Override
+                        public boolean evaluate(OsmPrimitive o) {
+                            OsmPrimitive p = currentLayer.data.getPrimitiveById(o);
+                            return p != null && p.isUsable();
+                        }
+                    }
+            );
+            Main.map.mapView.getEditLayer().data.setSelected(prims);
+        }
+
+        @Override
+        public void updateEnabledState() {
+            setEnabled(!undoTree.isSelectionEmpty() || !redoTree.isSelectionEmpty());
+        }
+
+    }
+
+    /**
+     * undo / redo switch to reduce duplicate code
+     */
+    protected enum UndoRedoType {UNDO, REDO};
+
+    /**
+     * Action to undo or redo all commands up to (and including) the seleced item.
+     */
+    protected class UndoRedoAction extends AbstractAction implements IEnabledStateUpdating {
+        private UndoRedoType type;
+        private JTree tree;
+
+        /**
+         * constructor
+         * @param type decide whether it is an undo action or a redo action
+         */
+        public UndoRedoAction(UndoRedoType type) {
+            super();
+            this.type = type;
+            switch (type) {
+                case UNDO:
+                    tree = undoTree;
+                    putValue(NAME,tr("Undo"));
+                    putValue(SHORT_DESCRIPTION, tr("Undo the selected and all later commands"));
+                    putValue(SMALL_ICON, ImageProvider.get("undo"));
+                    break;
+                case REDO:
+                    tree = redoTree;
+                    putValue(NAME,tr("Redo"));
+                    putValue(SHORT_DESCRIPTION, tr("Redo the selected and all earlier commands"));
+                    putValue(SMALL_ICON, ImageProvider.get("redo"));
+                    break;
+            }
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            lastOperation = type;
+            TreePath path = tree.getSelectionPath();
+
+            // we can only undo top level commands
+            if (path.getPathCount() != 2)
+                throw new IllegalStateException();
+
+            int idx = ((CommandListMutableTreeNode) path.getLastPathComponent()).getIndex();
+
+            // calculate the number of commands to undo/redo; then do it
+            switch (type) {
+                case UNDO:
+                    int numUndo = ((DefaultMutableTreeNode) undoTreeModel.getRoot()).getChildCount() - idx;
+                    Main.main.undoRedo.undo(numUndo);
+                    break;
+                case REDO:
+                    int numRedo = idx+1;
+                    Main.main.undoRedo.redo(numRedo);
+                    break;
+            }
+            Main.map.repaint();
+        }
+
+        @Override
+        public void updateEnabledState() {
+            // do not allow execution if nothing is selected or a sub command was selected
+            setEnabled(!tree.isSelectionEmpty() && tree.getSelectionPath().getPathCount()==2);
+        }
+    }
+
+    class PopupMenuHandler extends PopupMenuLauncher {
+        @Override
+        public void launch(MouseEvent evt) {
+            Point p = evt.getPoint();
+            JTree tree = (JTree) evt.getSource();
+            int row = tree.getRowForLocation(p.x, p.y);
+            if (row != -1) {
+                TreePath path = tree.getPathForLocation(p.x, p.y);
+                // right click on unselected element -> select it first
+                if (!tree.isPathSelected(path)) {
+                    tree.setSelectionPath(path);
+                }
+                TreePath[] selPaths = tree.getSelectionPaths();
+
+                CommandStackPopup menu = new CommandStackPopup(selPaths);
+                menu.show(tree, p.x, p.y-3);
+            }
+        }
+    }
+
+    private class CommandStackPopup extends JPopupMenu {
+        private TreePath[] sel;
+        public CommandStackPopup(TreePath[] sel){
+            this.sel = sel;
+            add(new SelectAction());
+        }
     }
 }
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/InspectPrimitiveDialog.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/InspectPrimitiveDialog.java	(revision 3262)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/InspectPrimitiveDialog.java	(revision 3262)
@@ -0,0 +1,226 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.dialogs;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.GridBagLayout;
+import java.util.Collection;
+import java.util.List;
+
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.DatasetCollection;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.data.osm.User;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.tools.DateUtils;
+import org.openstreetmap.josm.tools.GBC;
+
+/**
+ * Panel to inspect one or more OsmPrimitives.
+ *
+ * Gives an unfiltered view of the object's internal state.
+ * Might be useful for power users to give more detailed bug reports and
+ * to better understand the JOSM data representation.
+ *
+ * TODO: show conflicts
+ */
+public class InspectPrimitiveDialog extends ExtendedDialog {
+    protected Collection<OsmPrimitive> primitives;
+    protected JTextArea textArea;
+
+    public InspectPrimitiveDialog(Collection<OsmPrimitive> primitives) {
+        super(Main.parent, tr("Advanced object info"), new String[] {"Close"});
+        this.primitives = primitives;
+        setPreferredSize(new Dimension(450, 350));
+
+        setButtonIcons(new String[] {"ok.png"});
+        JPanel p = buildPanel();
+        textArea.setText(buildText());
+        setContent(p, false);
+    }
+
+    protected JPanel buildPanel() {
+        JPanel p = new JPanel(new GridBagLayout());
+        textArea = new JTextArea();
+        textArea.setFont(new Font("Monospaced", textArea.getFont().getStyle(), textArea.getFont().getSize()));
+        textArea.setEditable(false);
+
+        JScrollPane scroll = new JScrollPane(textArea);
+
+        p.add(scroll, GBC.std().fill());
+        return p;
+    }
+
+    protected String buildText() {
+        StringBuffer s = new StringBuffer();
+        for (Node n : new DatasetCollection<Node>(primitives, OsmPrimitive.nodePredicate)) {
+            s.append("Node id="+n.getUniqueId());
+            if (!checkDataSet(n)) {
+                s.append(" not in data set");
+                continue;
+            }
+            if (n.isIncomplete()) {
+                s.append(" incomplete\n");
+                addWayReferrer(s, n);
+                addRelationReferrer(s, n);
+                continue;
+            }
+            s.append(String.format(" lat=%f; lon=%f; ", n.getCoor().lat(), n.getCoor().lon()));
+            addCommon(s, n);
+            addAttributes(s, n);
+            addWayReferrer(s, n);
+            addRelationReferrer(s, n);
+            s.append('\n');
+        }
+
+        for (Way w : new DatasetCollection<Way>(primitives, OsmPrimitive.wayPredicate)) {
+            s.append("Way id="+ w.getUniqueId());
+            if (!checkDataSet(w)) {
+                s.append(" not in data set");
+                continue;
+            }
+            if (w.isIncomplete()) {
+                s.append(" incomplete\n");
+                addRelationReferrer(s, w);
+                continue;
+            }
+            s.append(String.format(" %d nodes; ", w.getNodes().size()));
+            addCommon(s, w);
+            addAttributes(s, w);
+            addRelationReferrer(s, w);
+
+            s.append("  nodes:\n");
+            for (Node n : w.getNodes()) {
+                s.append(String.format("    %d\n", n.getUniqueId()));
+            }
+            s.append('\n');
+        }
+
+        for (Relation r : new DatasetCollection<Relation>(primitives, OsmPrimitive.relationPredicate)) {
+            s.append("Relation id="+r.getUniqueId());
+            if (!checkDataSet(r)) {
+                s.append(" not in data set");
+                continue;
+            }
+            if (r.isIncomplete()) {
+                s.append(" incomplete\n");
+                addRelationReferrer(s, r);
+                continue;
+            }
+            s.append(String.format(" %d members; ",r.getMembersCount()));
+            addCommon(s, r);
+            addAttributes(s, r);
+            addRelationReferrer(s, r);
+
+            s.append("  members:\n");
+            for (RelationMember m : r.getMembers() ) {
+                s.append(String.format("    %d %s\n", m.getMember().getUniqueId(), m.getRole()));
+            }
+            s.append('\n');
+        }
+
+        return s.toString().trim();
+    }
+
+    protected void addCommon(StringBuffer s, OsmPrimitive o) {
+        s.append(String.format("Data set: %X; User: [%s]; ChangeSet id: %H; Timestamp: %s, Version: %d",
+                    o.getDataSet().hashCode(),
+                    userString(o.getUser()),
+                    o.getChangesetId(),
+                    DateUtils.fromDate(o.getTimestamp()),
+                    o.getVersion()));
+
+        /* selected state is left out: not interesting as it is always selected */
+        if (o.isDeleted()) {
+            s.append("; deleted");
+        }
+        if (o.isModified()) {
+            s.append("; modified");
+        }
+        if (o.isFiltered()) {
+            s.append("; filtered/hidden");
+        }
+        if (o.isDisabled()) {
+            s.append("; filtered/disabled");
+        }
+        if (o.hasDirectionKeys()) {
+            s.append("; has direction keys");
+            if (o.reversedDirection()) {
+                s.append(" (reversed)");
+            }
+        }
+        s.append("\n");
+    }
+
+    protected void addAttributes(StringBuffer s, OsmPrimitive o) {
+        if (o.hasKeys()) {
+            s.append("  tags:\n");
+            for (String key: o.keySet()) {
+                s.append(String.format("    \"%s\"=\"%s\"\n", key, o.get(key)));
+            }
+        }
+    }
+
+    protected void addWayReferrer(StringBuffer s, Node n) {
+        // add way referrer
+        List<OsmPrimitive> refs = n.getReferrers();
+        DatasetCollection<Way> wayRefs = new DatasetCollection<Way>(refs, OsmPrimitive.wayPredicate);
+        if (wayRefs.size() > 0) {
+            s.append("  way referrer:\n");
+            for (Way w : wayRefs) {
+                s.append("    "+w.getUniqueId()+"\n");
+            }
+        }
+    }
+
+    protected void addRelationReferrer(StringBuffer s, OsmPrimitive o) {
+        List<OsmPrimitive> refs = o.getReferrers();
+        DatasetCollection<Relation> relRefs = new DatasetCollection<Relation>(refs, OsmPrimitive.relationPredicate);
+        if (relRefs.size() > 0) {
+            s.append("  relation referrer:\n");
+            for (Relation r : relRefs) {
+                s.append("    "+r.getUniqueId()+"\n");
+            }
+        }
+    }
+
+    /**
+     * See if primitive is in a data set properly.
+     * This does not hold for primitives that are new and deleted.
+     */
+    protected boolean checkDataSet(OsmPrimitive o) {
+        DataSet ds = o.getDataSet();
+        if (ds == null)
+            return false;
+        return ds.getPrimitiveById(o) != null;
+    }
+
+    protected String userString(User user) {
+        if (user == null)
+            return "<null>";
+
+        List<String> names = user.getNames();
+
+        StringBuffer us = new StringBuffer();
+
+        us.append("id:"+user.getId());
+        if (names.size() == 1) {
+            us.append(" name:"+user.getName());
+        }
+        else if (names.size() > 1) {
+            us.append(String.format(" %d names:%s", names.size(), user.getName()));
+        }
+        return us.toString();
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/SelectionListDialog.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/SelectionListDialog.java	(revision 3261)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/SelectionListDialog.java	(revision 3262)
@@ -94,4 +94,5 @@
     private ZoomToListSelection actZoomToListSelection;
     private DownloadSelectedIncompleteMembersAction actDownloadSelectedIncompleteMembers;
+    private InspectAction actInspect;
 
     /**
@@ -176,4 +177,7 @@
         lstPrimitives.getSelectionModel().addListSelectionListener(actDownloadSelectedIncompleteMembers);
 
+        actInspect = new InspectAction();
+        lstPrimitives.getSelectionModel().addListSelectionListener(actInspect);
+
         lstPrimitives.addMouseListener(new SelectionPopupMenuLauncher());
         lstPrimitives.addMouseListener(new DblClickHandler());
@@ -248,4 +252,6 @@
             addSeparator();
             add(actDownloadSelectedIncompleteMembers);
+            addSeparator();
+            add(actInspect);
         }
     }
@@ -741,5 +747,5 @@
      *
      */
-    class DownloadSelectedIncompleteMembersAction extends AbstractAction implements ListSelectionListener{
+    class DownloadSelectedIncompleteMembersAction extends AbstractAction implements ListSelectionListener {
         public DownloadSelectedIncompleteMembersAction() {
             putValue(SHORT_DESCRIPTION, tr("Download incomplete members of selected relations"));
@@ -771,4 +777,27 @@
         protected void updateEnabledState() {
             setEnabled(!model.getSelectedRelationsWithIncompleteMembers().isEmpty());
+        }
+
+        public void valueChanged(ListSelectionEvent e) {
+            updateEnabledState();
+        }
+    }
+
+    class InspectAction extends AbstractAction implements ListSelectionListener {
+        public InspectAction() {
+            putValue(SHORT_DESCRIPTION, tr("Get detailed information on the internal state of the objects."));
+            putValue(NAME, tr("Inspect"));
+            updateEnabledState();
+        }
+
+        public void actionPerformed(ActionEvent e) {
+            Collection<OsmPrimitive> sel = model.getSelected();
+            if (sel.isEmpty()) return;
+            InspectPrimitiveDialog inspectDialog = new InspectPrimitiveDialog(sel);
+            inspectDialog.showDialog();
+        }
+
+        public void updateEnabledState() {
+            setEnabled(!model.getSelected().isEmpty());
         }
 
