Index: trunk/src/org/openstreetmap/josm/actions/AboutAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/actions/AboutAction.java	(revision 2005)
+++ trunk/src/org/openstreetmap/josm/actions/AboutAction.java	(revision 2008)
@@ -67,4 +67,5 @@
         }
         revision = loadFile(u, manifest);
+        System.out.println("Revision: " + revision.getText());
 
         Pattern versionPattern = Pattern.compile(".*?(?:Revision|Main-Version): ([0-9]*(?: SVN)?).*", Pattern.CASE_INSENSITIVE|Pattern.DOTALL);
Index: trunk/src/org/openstreetmap/josm/actions/CopyAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/actions/CopyAction.java	(revision 2005)
+++ trunk/src/org/openstreetmap/josm/actions/CopyAction.java	(revision 2008)
@@ -17,5 +17,4 @@
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.osm.DataSet;
-import org.openstreetmap.josm.data.osm.DataSource;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
@@ -24,5 +23,4 @@
 import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
-import org.openstreetmap.josm.gui.OptionPaneUtil;
 import org.openstreetmap.josm.tools.Shortcut;
 
@@ -110,15 +108,4 @@
                     osm.visit(this);
                 }
-
-                // Used internally only (in PasteTagsAction), therefore no need to translate these
-                if(getCurrentDataSet().getSelectedNodes().size() > 0) {
-                    pasteBuffer.dataSources.add(new DataSource(null, "Copied Nodes"));
-                }
-                if(getCurrentDataSet().getSelectedWays().size() > 0) {
-                    pasteBuffer.dataSources.add(new DataSource(null, "Copied Ways"));
-                }
-                if(getCurrentDataSet().getSelectedRelations().size() > 0) {
-                    pasteBuffer.dataSources.add(new DataSource(null, "Copied Relations"));
-                }
             }
         }.visitAll();
@@ -135,5 +122,5 @@
         Collection<OsmPrimitive> sel = getCurrentDataSet().getSelected();
         if (sel.isEmpty()) {
-            OptionPaneUtil.showMessageDialog(
+            JOptionPane.showMessageDialog(
                     Main.parent,
                     tr("Please select something to copy."),
Index: trunk/src/org/openstreetmap/josm/actions/PasteTagsAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/actions/PasteTagsAction.java	(revision 2005)
+++ trunk/src/org/openstreetmap/josm/actions/PasteTagsAction.java	(revision 2008)
@@ -4,11 +4,12 @@
 
 import static org.openstreetmap.josm.tools.I18n.tr;
+import static org.openstreetmap.josm.tools.I18n.trn;
 
 import java.awt.event.ActionEvent;
 import java.awt.event.KeyEvent;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedList;
+import java.util.List;
 import java.util.Map;
 
@@ -18,6 +19,11 @@
 import org.openstreetmap.josm.command.SequenceCommand;
 import org.openstreetmap.josm.data.osm.DataSet;
-import org.openstreetmap.josm.data.osm.DataSource;
+import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.TagCollection;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.gui.conflict.tags.PasteTagsConflictResolverDialog;
 import org.openstreetmap.josm.tools.Shortcut;
 
@@ -31,15 +37,235 @@
     }
 
-    private void pasteKeys(Collection<Command> clist, Collection<? extends OsmPrimitive> pasteBufferSubset, Collection<OsmPrimitive> selectionSubset) {
-        /* scan the paste buffer, and add tags to each of the selected objects.
-         * If a tag already exists, it is overwritten */
-        if (selectionSubset == null || selectionSubset.isEmpty())
+    /**
+     * Replies true if the source for tag pasting is heterogeneous, i.e. if it doesn't consist of
+     * {@see OsmPrimitive}s of exactly one type
+     * 
+     * @return
+     */
+    protected boolean isHeteogeneousSource() {
+        int count = 0;
+        count = !getSourcePrimitivesByType(Node.class).isEmpty() ? count + 1 : count;
+        count = !getSourcePrimitivesByType(Way.class).isEmpty() ? count + 1 : count;
+        count = !getSourcePrimitivesByType(Relation.class).isEmpty() ? count + 1 : count;
+        return count > 1;
+    }
+
+    /**
+     * Replies the subset  of {@see OsmPrimitive}s of <code>type</code> from <code>superSet</code>.
+     * 
+     * @param <T>
+     * @param superSet  the super set of primitives
+     * @param type  the type
+     * @return
+     */
+    protected <T extends OsmPrimitive> Collection<? extends OsmPrimitive> getSubcollectionByType(Collection<? extends OsmPrimitive> superSet, Class<T> type) {
+        Collection<OsmPrimitive> ret = new ArrayList<OsmPrimitive>();
+        for (OsmPrimitive p : superSet) {
+            if (type.isInstance(p)) {
+                ret.add(p);
+            }
+        }
+        return ret;
+    }
+
+    /**
+     * Replies all primitives of type <code>type</code> in the current selection.
+     * 
+     * @param <T>
+     * @param type  the type
+     * @return all primitives of type <code>type</code> in the current selection.
+     */
+    protected <T extends OsmPrimitive> Collection<? extends OsmPrimitive> getSourcePrimitivesByType(Class<T> type) {
+        return getSubcollectionByType(Main.pasteBuffer.getSelected(), type);
+    }
+
+    /**
+     * Replies the collection of tags for all primitives of type <code>type</code> in the current
+     * selection
+     * 
+     * @param <T>
+     * @param type  the type
+     * @return the collection of tags for all primitives of type <code>type</code> in the current
+     * selection
+     */
+    protected <T extends OsmPrimitive> TagCollection getSourceTagsByType(Class<T> type) {
+        return TagCollection.unionOfAllPrimitives(getSourcePrimitivesByType(type));
+    }
+
+    /**
+     * Replies true if there is at least one tag in the current selection for primitives of
+     * type <code>type</code>
+     * 
+     * @param <T>
+     * @param type the type
+     * @return true if there is at least one tag in the current selection for primitives of
+     * type <code>type</code>
+     */
+    protected <T extends OsmPrimitive> boolean hasSourceTagsByType(Class<T> type) {
+        return ! getSourceTagsByType(type).isEmpty();
+    }
+
+    protected Command buildChangeCommand(Collection<? extends OsmPrimitive> selection, TagCollection tc) {
+        List<Command> commands = new ArrayList<Command>();
+        for (String key : tc.getKeys()) {
+            String value = tc.getValues(key).iterator().next();
+            value = value.equals("") ? null : value;
+            commands.add(new ChangePropertyCommand(selection,key,value));
+        }
+        if (!commands.isEmpty()) {
+            String title1 = trn("Pasting {0} tag", "Pasting {0} tags", tc.getKeys().size(), tc.getKeys().size());
+            String title2 = trn("to {0} primitive", "to {0} primtives", selection.size(), selection.size());
+            return new SequenceCommand(
+                    title1 + " " + title2,
+                    commands
+            );
+        }
+        return null;
+    }
+
+    protected Map<OsmPrimitiveType, Integer> getSourceStatistics() {
+        HashMap<OsmPrimitiveType, Integer> ret = new HashMap<OsmPrimitiveType, Integer>();
+        for (Class type: new Class[] {Node.class, Way.class, Relation.class}) {
+            if (!getSourceTagsByType(type).isEmpty()) {
+                ret.put(OsmPrimitiveType.from(type), getSourcePrimitivesByType(type).size());
+            }
+        }
+        return ret;
+    }
+
+    protected Map<OsmPrimitiveType, Integer> getTargetStatistics() {
+        HashMap<OsmPrimitiveType, Integer> ret = new HashMap<OsmPrimitiveType, Integer>();
+        for (Class type: new Class[] {Node.class, Way.class, Relation.class}) {
+            int count = getSubcollectionByType(getEditLayer().data.getSelected(), type).size();
+            if (count > 0) {
+                ret.put(OsmPrimitiveType.from(type), count);
+            }
+        }
+        return ret;
+    }
+
+    /**
+     * Pastes the tags from a homogeneous source (i.e. the {@see Main#pasteBuffer}s selection consisting
+     * of one type of {@see OsmPrimitive}s only.
+     * 
+     * Tags from a homogeneous source can be pasted to a heterogeneous target. All target primitives,
+     * regardless of their type, receive the same tags.
+     * 
+     * @param targets the collection of target primitives
+     */
+    protected void pasteFromHomogeneousSource(Collection<? extends OsmPrimitive> targets) {
+        TagCollection tc = null;
+        Class sourceType = null;
+        for (Class type : new Class[] {Node.class, Way.class, Relation.class}) {
+            TagCollection tc1 = getSourceTagsByType(type);
+            if (!tc1.isEmpty()) {
+                tc = tc1;
+                sourceType = type;
+            }
+        }
+        if (tc == null)
+            // no tags found to paste. Abort.
             return;
 
-        for (Iterator<? extends OsmPrimitive> it = pasteBufferSubset.iterator(); it.hasNext();) {
-            OsmPrimitive osm = it.next();
-
-            for (String key : osm.keySet()) {
-                clist.add(new ChangePropertyCommand(selectionSubset, key, osm.get(key)));
+
+        if (!tc.isApplicableToPrimitive()) {
+            PasteTagsConflictResolverDialog dialog = new PasteTagsConflictResolverDialog(Main.parent);
+            dialog.populate(tc, getSourceStatistics(), getTargetStatistics());
+            dialog.setVisible(true);
+            if (dialog.isCanceled())
+                return;
+            Command cmd = buildChangeCommand(targets, dialog.getResolution());
+            Main.main.undoRedo.add(cmd);
+        } else {
+            // no conflicts in the source tags to resolve. Just apply the tags
+            // to the target primitives
+            //
+            Command cmd = buildChangeCommand(targets, tc);
+            Main.main.undoRedo.add(cmd);
+        }
+    }
+
+    /**
+     * Replies true if there is at least one primitive of type <code>type</code> in the collection
+     * <code>selection</code>
+     * 
+     * @param <T>
+     * @param selection  the collection of primitives
+     * @param type  the type to look for
+     * @return true if there is at least one primitive of type <code>type</code> in the collection
+     * <code>selection</code>
+     */
+    protected <T extends OsmPrimitive> boolean hasTargetPrimitives(Collection<? extends OsmPrimitive> selection, Class<T> type) {
+        return !getSubcollectionByType(selection, type).isEmpty();
+    }
+
+    /**
+     * Replies true if this a heterogeneous source can be pasted without conflict to targets
+     * 
+     * @param targets the collection of target primitives
+     * @return true if this a heterogeneous source can be pasted without conflicts to targets
+     */
+    protected boolean canPasteFromHeterogeneousSourceWithoutConflict(Collection<? extends OsmPrimitive> targets) {
+        if (hasTargetPrimitives(targets, Node.class)) {
+            TagCollection tc = TagCollection.unionOfAllPrimitives(getSourcePrimitivesByType(Node.class));
+            if (!tc.isEmpty() && ! tc.isApplicableToPrimitive())
+                return false;
+        }
+        if (hasTargetPrimitives(targets, Way.class)) {
+            TagCollection tc = TagCollection.unionOfAllPrimitives(getSourcePrimitivesByType(Way.class));
+            if (!tc.isEmpty() && ! tc.isApplicableToPrimitive())
+                return false;
+        }
+        if (hasTargetPrimitives(targets, Relation.class)) {
+            TagCollection tc = TagCollection.unionOfAllPrimitives(getSourcePrimitivesByType(Relation.class));
+            if (!tc.isEmpty() && ! tc.isApplicableToPrimitive())
+                return false;
+        }
+        return true;
+    }
+
+    /**
+     * Pastes the tags in the current selection of the paste buffer to a set of target
+     * primitives.
+     * 
+     * @param targets the collection of target primitives
+     */
+    protected void pasteFromHeterogeneousSource(Collection<? extends OsmPrimitive> targets) {
+        if (canPasteFromHeterogeneousSourceWithoutConflict(targets)) {
+            if (hasSourceTagsByType(Node.class) && hasTargetPrimitives(targets, Node.class)) {
+                Command cmd = buildChangeCommand(targets, getSourceTagsByType(Node.class));
+                Main.main.undoRedo.add(cmd);
+            }
+            if (hasSourceTagsByType(Way.class) && hasTargetPrimitives(targets, Way.class)) {
+                Command cmd = buildChangeCommand(targets, getSourceTagsByType(Way.class));
+                Main.main.undoRedo.add(cmd);
+            }
+            if (hasSourceTagsByType(Relation.class) && hasTargetPrimitives(targets, Relation.class)) {
+                Command cmd = buildChangeCommand(targets,getSourceTagsByType(Relation.class));
+                Main.main.undoRedo.add(cmd);
+            }
+        } else {
+            PasteTagsConflictResolverDialog dialog = new PasteTagsConflictResolverDialog(Main.parent);
+            dialog.populate(
+                    getSourceTagsByType(Node.class),
+                    getSourceTagsByType(Way.class),
+                    getSourceTagsByType(Relation.class),
+                    getSourceStatistics(),
+                    getTargetStatistics()
+            );
+            dialog.setVisible(true);
+            if (dialog.isCanceled())
+                return;
+            if (hasSourceTagsByType(Node.class) && hasTargetPrimitives(targets, Node.class)) {
+                Command cmd = buildChangeCommand(getSubcollectionByType(targets, Node.class), dialog.getResolution(OsmPrimitiveType.NODE));
+                Main.main.undoRedo.add(cmd);
+            }
+            if (hasSourceTagsByType(Way.class) && hasTargetPrimitives(targets, Way.class)) {
+                Command cmd = buildChangeCommand(getSubcollectionByType(targets, Way.class), dialog.getResolution(OsmPrimitiveType.WAY));
+                Main.main.undoRedo.add(cmd);
+            }
+            if (hasSourceTagsByType(Relation.class) && hasTargetPrimitives(targets, Relation.class)) {
+                Command cmd = buildChangeCommand(getSubcollectionByType(targets, Relation.class), dialog.getResolution(OsmPrimitiveType.RELATION));
+                Main.main.undoRedo.add(cmd);
             }
         }
@@ -47,64 +273,11 @@
 
     public void actionPerformed(ActionEvent e) {
-        Collection<Command> clist = new LinkedList<Command>();
-        String pbSource = "Multiple Sources";
-        if(Main.pasteBuffer.dataSources.size() == 1) {
-            pbSource = ((DataSource) Main.pasteBuffer.dataSources.toArray()[0]).origin;
-        }
-
-        boolean pbNodes = Main.pasteBuffer.nodes.size() > 0;
-        boolean pbWays  = Main.pasteBuffer.ways.size() > 0;
-
-        boolean seNodes = getCurrentDataSet().getSelectedNodes().size() > 0;
-        boolean seWays  = getCurrentDataSet().getSelectedWays().size() > 0;
-        boolean seRels  = getCurrentDataSet().getSelectedRelations().size() > 0;
-
-        if(!seNodes && seWays && !seRels && pbNodes && pbSource.equals("Copied Nodes")) {
-            // Copy from nodes to ways
-            pasteKeys(clist, Main.pasteBuffer.nodes, getCurrentDataSet().getSelectedWays());
-        } else if(seNodes && !seWays && !seRels && pbWays && pbSource.equals("Copied Ways")) {
-            // Copy from ways to nodes
-            pasteKeys(clist, Main.pasteBuffer.ways, getCurrentDataSet().getSelectedNodes());
+        if (getCurrentDataSet().getSelected().isEmpty())
+            return;
+        if (isHeteogeneousSource()) {
+            pasteFromHeterogeneousSource(getCurrentDataSet().getSelected());
         } else {
-            // Copy from equal to equal
-            pasteKeys(clist, Main.pasteBuffer.nodes, getCurrentDataSet().getSelectedNodes());
-            pasteKeys(clist, Main.pasteBuffer.ways, getCurrentDataSet().getSelectedWays());
-            pasteKeys(clist, Main.pasteBuffer.relations, getCurrentDataSet().getSelectedRelations());
-        }
-        Main.main.undoRedo.add(new SequenceCommand(tr("Paste Tags"), clist));
-        getCurrentDataSet().setSelected(getCurrentDataSet().getSelected()); // to force selection listeners, in particular the tag panel, to update
-        Main.map.mapView.repaint();
-    }
-
-    private boolean containsSameKeysWithDifferentValues(Collection<? extends OsmPrimitive> osms) {
-        Map<String,String> kvSeen = new HashMap<String,String>();
-        for (OsmPrimitive osm:osms) {
-            for (String key : osm.keySet()) {
-                String value = osm.get(key);
-                if (! kvSeen.containsKey(key)) {
-                    kvSeen.put(key, value);
-                } else if (! kvSeen.get(key).equals(value))
-                    return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Determines whether to enable the widget depending on the contents of the paste
-     * buffer and current selection
-     * @param pasteBuffer
-     */
-    private void possiblyEnable(Collection<? extends OsmPrimitive> selection, DataSet pasteBuffer) {
-        /* only enable if there is something selected to paste into and
-            if we don't have conflicting keys in the pastebuffer */
-        DataSet ds = getCurrentDataSet();
-        if (ds == null || ds.getSelected().isEmpty() || pasteBuffer == null || pasteBuffer.allPrimitives().isEmpty()) {
-            setEnabled(false);
-            return;
-        }
-        setEnabled((!ds.getSelectedNodes().isEmpty() && ! containsSameKeysWithDifferentValues(pasteBuffer.nodes)) ||
-                (!ds.getSelectedWays().isEmpty() && ! containsSameKeysWithDifferentValues(pasteBuffer.ways)) ||
-                (! ds.getSelectedRelations().isEmpty() && ! containsSameKeysWithDifferentValues(pasteBuffer.relations)));
+            pasteFromHomogeneousSource(getCurrentDataSet().getSelected());
+        }
     }
 
@@ -119,5 +292,8 @@
             return;
         }
-        possiblyEnable(getCurrentDataSet().getSelected(), Main.pasteBuffer);
+        setEnabled(
+                !getCurrentDataSet().getSelected().isEmpty()
+                && !TagCollection.unionOfAllPrimitives(Main.pasteBuffer.getSelected()).isEmpty()
+        );
     }
 }
Index: trunk/src/org/openstreetmap/josm/command/ChangePropertyCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/ChangePropertyCommand.java	(revision 2005)
+++ trunk/src/org/openstreetmap/josm/command/ChangePropertyCommand.java	(revision 2008)
@@ -100,20 +100,18 @@
             if (value == null) {
                 switch(OsmPrimitiveType.from(primitive)) {
-                case NODE: msg = marktr("Remove \"{0}\" for node ''{1}''"); break;
-                case WAY: msg = marktr("Remove \"{0}\" for way ''{1}''"); break;
-                case RELATION: msg = marktr("Remove \"{0}\" for relation ''{1}''"); break;
+                    case NODE: msg = marktr("Remove \"{0}\" for node ''{1}''"); break;
+                    case WAY: msg = marktr("Remove \"{0}\" for way ''{1}''"); break;
+                    case RELATION: msg = marktr("Remove \"{0}\" for relation ''{1}''"); break;
                 }
                 text = tr(msg, key, primitive.getDisplayName(DefaultNameFormatter.getInstance()));
             } else {
                 switch(OsmPrimitiveType.from(primitive)) {
-                case NODE: msg = marktr("Set {0}={1} for node ''{2}''"); break;
-                case WAY: msg = marktr("Set {0}={1} for way ''{2}''"); break;
-                case RELATION: msg = marktr("Set {0}={1} for relation ''{2}''"); break;
+                    case NODE: msg = marktr("Set {0}={1} for node ''{2}''"); break;
+                    case WAY: msg = marktr("Set {0}={1} for way ''{2}''"); break;
+                    case RELATION: msg = marktr("Set {0}={1} for relation ''{2}''"); break;
                 }
                 text = tr(msg, key, value, primitive.getDisplayName(DefaultNameFormatter.getInstance()));
             }
-        }
-        else
-        {
+        } else {
             text = value == null
             ? tr("Remove \"{0}\" for {1} {2}", key, objects.size(), trn("object","objects",objects.size()))
Index: trunk/src/org/openstreetmap/josm/data/osm/Tag.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/Tag.java	(revision 2008)
+++ trunk/src/org/openstreetmap/josm/data/osm/Tag.java	(revision 2008)
@@ -0,0 +1,135 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.osm;
+
+/**
+ * Tag represents an immutable key/value-pair. Both the key and the value may
+ * be empty, but not null.
+ *
+ */
+public class Tag {
+
+    private String key;
+    private String value;
+
+    /**
+     * Create an empty tag whose key and value are empty.
+     */
+    public Tag(){
+        this.key = "";
+        this.value = "";
+    }
+
+    /**
+     * Create a tag whose key is <code>key</code> and whose value is
+     * empty.
+     * 
+     * @param key the key. If null, it is set to the empty key.
+     */
+    public Tag(String key) {
+        this.key = key == null ? "" : key;
+    }
+
+    /**
+     * Creates a tag for a key and a value. If key and/or value are null,
+     * the empty value "" is assumed.
+     * 
+     * @param key the key
+     * @param value  the value
+     */
+    public Tag(String key, String value) {
+        this.key = key == null ? "" : key;
+        this.value = value == null ? "" : value;
+    }
+
+    /**
+     * Creates clone of the tag <code>tag</code>.
+     * 
+     * @param tag the tag. If null, creates an empty tag.
+     */
+    public Tag(Tag tag) {
+        if (tag != null) {
+            key = tag.getKey();
+            value = tag.getValue();
+        }
+    }
+
+    /**
+     * Replies the key of the tag. This is never null.
+     * 
+     * @return the key of the tag
+     */
+    public String getKey() {
+        return key;
+    }
+
+    /**
+     * Replies the value of the tag. This is never null.
+     * 
+     * @return the value of the tag
+     */
+    public String getValue() {
+        return value;
+    }
+
+    @Override
+    public Tag clone() {
+        return new Tag(this);
+    }
+
+    /**
+     * Replies true if the key of this tag is equal to <code>key</code>.
+     * If <code>key</code> is null, assumes the empty key.
+     * 
+     * @param key the key
+     * @return true if the key of this tag is equal to <code>key</code>
+     */
+    public boolean matchesKey(String key) {
+        if (key == null) {
+            key = "";
+        }
+        return this.key.equals(key);
+    }
+
+    /**
+     * Normalizes the key and the value of the tag by
+     * <ul>
+     *   <li>removing leading and trailing white space</li>
+     * <ul>
+     * 
+     */
+    public void normalize() {
+        key = key.trim();
+        value = value.trim();
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((key == null) ? 0 : key.hashCode());
+        result = prime * result + ((value == null) ? 0 : value.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        Tag other = (Tag) obj;
+        if (key == null) {
+            if (other.key != null)
+                return false;
+        } else if (!key.equals(other.key))
+            return false;
+        if (value == null) {
+            if (other.value != null)
+                return false;
+        } else if (!value.equals(other.value))
+            return false;
+        return true;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/data/osm/TagCollection.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/TagCollection.java	(revision 2008)
+++ trunk/src/org/openstreetmap/josm/data/osm/TagCollection.java	(revision 2008)
@@ -0,0 +1,684 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.osm;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.Map.Entry;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+/**
+ * TagCollection is a collection of tags which can be used to manipulate
+ * tags managed by {@see OsmPrimitive}s.
+ * 
+ * A TagCollection can be created:
+ * <ul>
+ *  <li>from the tags managed by a specific {@see OsmPrimitive} with {@see #from(OsmPrimitive)}</li>
+ *  <li>from the union of all tags managed by a collection of {@see OsmPrimitive}s with {@see #unionOfAllPrimitives(Collection)}</li>
+ *  <li>from the union of all tags managed by a {@see DataSet} with {@see #unionOfAllPrimitives(DataSet)}</li>
+ *  <li>from the intersection of all tags managed by a collection of primitives with {@see #commonToAllPrimitives(Collection)}</li>
+ * </ul>
+ * 
+ * It  provides methods to query the collection, like {@see #size()}, {@see #hasTagsFor(String)}, etc.
+ * 
+ * Basic set operations allow to create the union, the intersection and  the difference
+ * of tag collections, see {@see #union(TagCollection)}, {@see #intersect(TagCollection)},
+ * and {@see #minus(TagCollection)}.
+ * 
+ *
+ */
+public class TagCollection implements Iterable<Tag> {
+
+    /**
+     * Creates a tag collection from the tags managed by a specific
+     * {@see OsmPrimitive}. If <code>primitive</code> is null, replies
+     * an empty tag collection.
+     * 
+     * @param primitive  the primitive
+     * @return a tag collection with the tags managed by a specific
+     * {@see OsmPrimitive}
+     */
+    public static TagCollection from(OsmPrimitive primitive) {
+        TagCollection tags = new TagCollection();
+        for (String key: primitive.keySet()) {
+            tags.add(new Tag(key, primitive.get(key)));
+        }
+        return tags;
+    }
+
+    /**
+     * Creates a tag collection from the union of the tags managed by
+     * a collection of primitives. Replies an empty tag collection,
+     * if <code>primitives</code> is null.
+     *
+     * @param primitives the primitives
+     * @return  a tag collection with the union of the tags managed by
+     * a collection of primitives
+     */
+    public static TagCollection unionOfAllPrimitives(Collection<? extends OsmPrimitive> primitives) {
+        TagCollection tags = new TagCollection();
+        if (primitives == null) return tags;
+        for (OsmPrimitive primitive: primitives) {
+            if (primitive == null) {
+                continue;
+            }
+            tags.add(TagCollection.from(primitive));
+        }
+        return tags;
+    }
+
+    /**
+     * Replies a tag collection with the tags which are common to all primitives in in
+     * <code>primitives</code>. Replies an empty tag collection of <code>primitives</code>
+     * is null.
+     * 
+     * @param primitives the primitives
+     * @return  a tag collection with the tags which are common to all primitives
+     */
+    public static TagCollection commonToAllPrimitives(Collection<? extends OsmPrimitive> primitives) {
+        TagCollection tags = new TagCollection();
+        if (primitives == null || primitives.isEmpty()) return tags;
+        // initialize with the first
+        //
+        tags.add(TagCollection.from(primitives.iterator().next()));
+
+        // intersect with the others
+        //
+        for (OsmPrimitive primitive: primitives) {
+            if (primitive == null) {
+                continue;
+            }
+            tags.add(tags.intersect(TagCollection.from(primitive)));
+        }
+        return tags;
+    }
+
+    /**
+     * Replies a tag collection with the union of the tags which are common to all primitives in
+     * the dataset <code>ds</code>. Returns an empty tag collection of <code>ds</code> is null.
+     * 
+     * @param ds the dataset
+     * @return a tag collection with the union of the tags which are common to all primitives in
+     * the dataset <code>ds</code>
+     */
+    public static TagCollection unionOfAllPrimitives(DataSet ds) {
+        TagCollection tags = new TagCollection();
+        if (ds == null) return tags;
+        tags.add(TagCollection.unionOfAllPrimitives(ds.nodes));
+        tags.add(TagCollection.unionOfAllPrimitives(ds.ways));
+        tags.add(TagCollection.unionOfAllPrimitives(ds.relations));
+        return tags;
+    }
+
+    private HashSet<Tag> tags;
+
+    /**
+     * Creates an empty tag collection
+     */
+    public TagCollection() {
+        tags = new HashSet<Tag>();
+    }
+
+    /**
+     * Creates a clone of the tag collection <code>other</code>. Creats an empty
+     * tag collection if <code>other</code> is null.
+     * 
+     * @param other the other collection
+     */
+    public TagCollection(TagCollection other) {
+        this();
+        if (other != null) {
+            tags.addAll(other.tags);
+        }
+    }
+
+    /**
+     * Replies the number of tags in this tag collection
+     * 
+     * @return the number of tags in this tag collection
+     */
+    public int size() {
+        return tags.size();
+    }
+
+    /**
+     * Replies true if this tag collection is empty
+     * 
+     * @return true if this tag collection is empty; false, otherwise
+     */
+    public boolean isEmpty() {
+        return size() == 0;
+    }
+
+    /**
+     * Adds a tag to the tag collection. If <code>tag</code> is null, nothing is added.
+     * 
+     * @param tag the tag to add
+     */
+    public void add(Tag tag){
+        if (tag == null) return;
+        if (tags.contains(tag)) return;
+        tags.add(tag);
+    }
+
+    /**
+     * Adds a collection of tags to the tag collection. If <code>tags</code> is null, nothing
+     * is added. null values in the collection are ignored.
+     * 
+     * @param tags the collection of tags
+     */
+    public void add(Collection<Tag> tags) {
+        if (tags == null) return;
+        for (Tag tag: tags){
+            add(tag);
+        }
+    }
+
+    /**
+     * Adds the tags of another tag collection to this collection. Adds nothing, if
+     * <code>tags</code> is null.
+     * 
+     * @param tags the other tag collection
+     */
+    public void add(TagCollection tags) {
+        if (tags == null) return;
+        this.tags.addAll(tags.tags);
+    }
+
+    /**
+     * Removes a specific tag from the tag collection. Does nothing if <code>tag</code> is
+     * null.
+     * 
+     * @param tag the tag to be removed
+     */
+    public void remove(Tag tag) {
+        if (tag == null) return;
+        tags.remove(tag);
+    }
+
+    /**
+     * Removes a collection of tags from the tag collection. Does nothing if <code>tags</code> is
+     * null.
+     * 
+     * @param tags the tags to be removed
+     */
+    public void remove(Collection<Tag> tags) {
+        if (tags == null) return;
+        this.tags.removeAll(tags);
+    }
+
+    /**
+     * Removes all tags in the tag collection <code>tags</code> from the current tag collection.
+     * Does nothing if <code>tags</code> is null.
+     * 
+     * @param tags the tag collection to be removed.
+     */
+    public void remove(TagCollection tags) {
+        if (tags == null) return;
+        this.tags.removeAll(tags.tags);
+    }
+
+    /**
+     * Removes all tags whose keys are equal to  <code>key</code>. Does nothing if <code>key</code>
+     * is null.
+     * 
+     * @param key the key to be removed
+     */
+    public void removeByKey(String key) {
+        if (key  == null) return;
+        Iterator<Tag> it = tags.iterator();
+        while(it.hasNext()) {
+            if (it.next().matchesKey(key)) {
+                it.remove();
+            }
+        }
+    }
+
+    /**
+     * Removes all tags whose key is in the collection <code>keys</code>. Does nothing if
+     * <code>keys</code> is null.
+     * 
+     * @param keys the collection of keys to be removed
+     */
+    public void removeByKey(Collection<String> keys) {
+        if (keys == null) return;
+        for (String key: keys) {
+            removeByKey(key);
+        }
+    }
+
+    /**
+     * Replies true if the this tag collection contains <code>tag</code>.
+     * 
+     * @param tag the tag to look up
+     * @return true if the this tag collection contains <code>tag</code>; false, otherwise
+     */
+    public boolean contains(Tag tag) {
+        return tags.contains(tag);
+    }
+
+    /**
+     * Replies true if this tag collection contains at least one tag with key <code>key</code>.
+     * 
+     * @param key the key to look up
+     * @return true if this tag collection contains at least one tag with key <code>key</code>; false, otherwise
+     */
+    public boolean containsKey(String key) {
+        if (key == null) return false;
+        for (Tag tag: tags) {
+            if (tag.matchesKey(key)) return true;
+        }
+        return false;
+    }
+
+    /**
+     * Replies true if this tag collection contains all tags in <code>tags</code>. Replies
+     * false, if tags is null.
+     * 
+     * @param tags the tags to look up
+     * @return true if this tag collection contains all tags in <code>tags</code>. Replies
+     * false, if tags is null.
+     */
+    public boolean containsAll(Collection<Tag> tags) {
+        if (tags == null) return false;
+        return tags.containsAll(tags);
+    }
+
+    /**
+     * Replies true if this tag collection at least one tag for every key in <code>keys</code>.
+     * Replies false, if <code>keys</code> is null. null values in <code>keys</code> are ignored.
+     * 
+     * @param keys the keys to lookup
+     * @return true if this tag collection at least one tag for every key in <code>keys</code>.
+     */
+    public boolean containsAllKeys(Collection<String> keys) {
+        if (keys == null) return false;
+        for (String key: keys) {
+            if (key == null) {
+                continue;
+            }
+            if (! containsKey(key)) return false;
+        }
+        return true;
+    }
+
+    /**
+     * Replies the number of tags with key <code>key</code>
+     * 
+     * @param key the key to look up
+     * @return the number of tags with key <code>key</code>. 0, if key is null.
+     */
+    public int getNumTagsFor(String key) {
+        if (key == null) return 0;
+        int count = 0;
+        for (Tag tag: tags) {
+            if (tag.matchesKey(key)) {
+                count++;
+            }
+        }
+        return count;
+    }
+
+    /**
+     * Replies true if there is at least one tag for the given key.
+     * 
+     * @param key the key to look up
+     * @return true if there is at least one tag for the given key. false, if key is null.
+     */
+    public boolean hasTagsFor(String key) {
+        return getNumTagsFor(key) > 0;
+    }
+
+    /**
+     * Replies true it there is at least one tag with a non empty value for key.
+     * Replies false if key is null.
+     * 
+     * @param key the key
+     * @return true it there is at least one tag with a non empty value for key.
+     */
+    public boolean hasValuesFor(String key) {
+        if (key == null) return false;
+        Set<String> values = getTagsFor(key).getValues();
+        values.remove("");
+        return !values.isEmpty();
+    }
+
+    /**
+     * Replies true if there is exactly one tag for <code>key</code> and
+     * if the value of this tag is not empty. Replies false if key is
+     * null.
+     * 
+     * @param key the key
+     * @return true if there is exactly one tag for <code>key</code> and
+     * if the value of this tag is not empty
+     */
+    public boolean hasUniqueNonEmptyValue(String key) {
+        if (key == null) return false;
+        Set<String> values = getTagsFor(key).getValues();
+        return values.size() == 1 && ! values.contains("");
+    }
+
+    /**
+     * Replies true if there is a tag with an empty value for <code>key</code>.
+     * Replies false, if key is null.
+     * 
+     * @param key the key
+     * @return true if there is a tag with an empty value for <code>key</code>
+     */
+    public boolean hasEmptyValue(String key) {
+        if (key == null) return false;
+        Set<String> values = getTagsFor(key).getValues();
+        return values.contains("");
+    }
+
+    /**
+     * Replies true if there is exactly one tag for <code>key</code> and if
+     * the value for this tag is empty. Replies false if key is null.
+     * 
+     * @param key the key
+     * @return  true if there is exactly one tag for <code>key</code> and if
+     * the value for this tag is empty
+     */
+    public boolean hasUniqueEmptyValue(String key) {
+        if (key == null) return false;
+        Set<String> values = getTagsFor(key).getValues();
+        return values.size() == 1 && values.contains("");
+    }
+
+    /**
+     * Replies a tag collection with the tags for a given key. Replies an empty collection
+     * if key is null.
+     * 
+     * @param key the key to look up
+     * @return a tag collection with the tags for a given key. Replies an empty collection
+     * if key is null.
+     */
+    public TagCollection getTagsFor(String key) {
+        TagCollection ret = new TagCollection();
+        if (key == null)
+            return ret;
+        for (Tag tag: tags) {
+            if (tag.matchesKey(key)) {
+                ret.add(tag);
+            }
+        }
+        return ret;
+    }
+
+    /**
+     * Replies a tag collection with all tags whose key is equal to one of the keys in
+     * <code>keys</code>. Replies an empty collection if keys is null.
+     * 
+     * @param keys the keys to look up
+     * @return a tag collection with all tags whose key is equal to one of the keys in
+     * <code>keys</code>
+     */
+    public TagCollection getTagsFor(Collection<String> keys) {
+        TagCollection ret = new TagCollection();
+        if (keys == null)
+            return ret;
+        for(String key : keys) {
+            if (key != null) {
+                ret.add(getTagsFor(key));
+            }
+        }
+        return ret;
+    }
+
+    /**
+     * Replies the tags of this tag collection as set
+     * 
+     * @return the tags of this tag collection as set
+     */
+    public Set<Tag> asSet() {
+        return new HashSet<Tag>(tags);
+    }
+
+    /**
+     * Replies the tags of this tag collection as list.
+     * Note that the order of the list is not preserved between method invocations.
+     * 
+     * @return the tags of this tag collection as list.
+     */
+    public List<Tag> asList() {
+        return new ArrayList<Tag>(tags);
+    }
+
+    /**
+     * Replies an iterator to iterate over the tags in this collection
+     * 
+     * @return the iterator
+     */
+    public Iterator<Tag> iterator() {
+        return tags.iterator();
+    }
+
+    /**
+     * Replies the set of keys of this tag collection.
+     * 
+     * @return the set of keys of this tag collection
+     */
+    public Set<String> getKeys() {
+        HashSet<String> ret = new HashSet<String>();
+        for (Tag tag: tags) {
+            ret.add(tag.getKey());
+        }
+        return ret;
+    }
+
+    /**
+     * Replies the set of keys which have at least 2 matching tags.
+     * 
+     * @return the set of keys which have at least 2 matching tags.
+     */
+    public Set<String> getKeysWithMultipleValues() {
+        HashMap<String, Integer> counters = new HashMap<String, Integer>();
+        for (Tag tag: tags) {
+            Integer v = counters.get(tag.getKey());
+            counters.put(tag.getKey(),(v==null) ? 1 : v+1);
+        }
+        Set<String> ret = new HashSet<String>();
+        for (Entry<String, Integer> e : counters.entrySet()) {
+            if (e.getValue() > 1) {
+                ret.add(e.getKey());
+            }
+        }
+        return ret;
+    }
+
+    /**
+     * Sets a unique tag for the key of this tag. All other tags with the same key are
+     * removed from the collection. Does nothing if tag is null.
+     * 
+     * @param tag the tag to set
+     */
+    public void setUniqueForKey(Tag tag) {
+        if (tag == null) return;
+        removeByKey(tag.getKey());
+        add(tag);
+    }
+
+    /**
+     * Sets a unique tag for the key of this tag. All other tags with the same key are
+     * removed from the collection. Assume the empty string for key and value if either
+     * key or value is null.
+     * 
+     * @param key the key
+     * @param value the value
+     */
+    public void setUniqueForKey(String key, String value) {
+        Tag tag = new Tag(key, value);
+        setUniqueForKey(tag);
+    }
+
+    /**
+     * Replies the set of values in this tag collection
+     * 
+     * @return the set of values
+     */
+    public Set<String> getValues() {
+        HashSet<String> ret = new HashSet<String>();
+        for (Tag tag: tags) {
+            ret.add(tag.getValue());
+        }
+        return ret;
+    }
+
+    /**
+     * Replies the set of values for a given key. Replies an empty collection if there
+     * are no values for the given key.
+     * 
+     * @param key the key to look up
+     * @return the set of values for a given key. Replies an empty collection if there
+     * are no values for the given key
+     */
+    public Set<String> getValues(String key) {
+        HashSet<String> ret = new HashSet<String>();
+        if (key == null) return ret;
+        for (Tag tag: tags) {
+            if (tag.matchesKey(key)) {
+                ret.add(tag.getValue());
+            }
+        }
+        return ret;
+    }
+
+    /**
+     * Replies true if for every key there is one tag only, i.e. exactly one value.
+     * 
+     * @return
+     */
+    public boolean isApplicableToPrimitive() {
+        return size() == getKeys().size();
+    }
+
+    /**
+     * Applies this tag collection to an {@see OsmPrimitive}. Does nothing if
+     * primitive is null
+     * 
+     * @param primitive  the primitive
+     * @throws IllegalStateException thrown if this tag collection can't be applied
+     * because there are keys with multiple values
+     */
+    public void applyTo(OsmPrimitive primitive) throws IllegalStateException {
+        if (primitive == null) return;
+        if (! isApplicableToPrimitive())
+            throw new IllegalStateException(tr("tag collection can't be applied to a primitive because there are keys with multiple values"));
+        for (Tag tag: tags) {
+            primitive.put(tag.getKey(), tag.getValue());
+        }
+    }
+
+    /**
+     * Applies this tag collection to a collection of {@see OsmPrimitive}s. Does nothing if
+     * primitives is null
+     * 
+     * @param primitives  the collection of primitives
+     * @throws IllegalStateException thrown if this tag collection can't be applied
+     * because there are keys with multiple values
+     */
+    public void applyTo(Collection<? extends OsmPrimitive> primitives) throws IllegalStateException{
+        if (primitives == null) return;
+        if (! isApplicableToPrimitive())
+            throw new IllegalStateException(tr("tag collection can't be applied to a primitive because there are keys with multiple values"));
+        for (OsmPrimitive primitive: primitives) {
+            applyTo(primitive);
+        }
+    }
+
+    /**
+     * Replaces the tags of an {@see OsmPrimitive} by the tags in this collection . Does nothing if
+     * primitive is null
+     * 
+     * @param primitive  the primitive
+     * @throws IllegalStateException thrown if this tag collection can't be applied
+     * because there are keys with multiple values
+     */
+    public void replaceTagsOf(OsmPrimitive primitive) throws IllegalStateException {
+        if (primitive == null) return;
+        if (! isApplicableToPrimitive())
+            throw new IllegalStateException(tr("tag collection can't be applied to a primitive because there are keys with multiple values"));
+        primitive.removeAll();
+        for (Tag tag: tags) {
+            primitive.put(tag.getKey(), tag.getValue());
+        }
+    }
+
+    /**
+     * Replaces the tags of a collection of{@see OsmPrimitive}s by the tags in this collection.
+     * Does nothing if primitives is null
+     * 
+     * @param primitive  the collection of primitives
+     * @throws IllegalStateException thrown if this tag collection can't be applied
+     * because there are keys with multiple values
+     */
+    public void replaceTagsOf(Collection<? extends OsmPrimitive> primitives) throws IllegalStateException {
+        if (primitives == null) return;
+        if (! isApplicableToPrimitive())
+            throw new IllegalStateException(tr("tag collection can't be applied to a primitive because there are keys with multiple values"));
+        for (OsmPrimitive primitive: primitives) {
+            replaceTagsOf(primitive);
+        }
+    }
+
+    /**
+     * Builds the intersection of this tag collection and another tag collection
+     * 
+     * @param other the other tag collection. If null, replies an empty tag collection.
+     * @return the intersection of this tag collection and another tag collection
+     */
+    public TagCollection intersect(TagCollection other) {
+        if (other == null) {
+            new TagCollection();
+        }
+        TagCollection ret = new TagCollection(this);
+        for (Tag tag: tags) {
+            if (other.contains(tag)) {
+                ret.add(tag);
+            }
+        }
+        return ret;
+    }
+
+    /**
+     * Replies the difference of this tag collection and another tag collection
+     * 
+     * @param other the other tag collection. May be null.
+     * @return the difference of this tag collection and another tag collection
+     */
+    public TagCollection minus(TagCollection other) {
+        TagCollection ret = new TagCollection(this);
+        if (other != null) {
+            ret.remove(other);
+        }
+        return ret;
+    }
+
+    /**
+     * Replies the union of this tag collection and another tag collection
+     * 
+     * @param other the other tag collection. May be null.
+     * @return the union of this tag collection and another tag collection
+     */
+    public TagCollection union(TagCollection other) {
+        TagCollection ret = new TagCollection(this);
+        if (other != null) {
+            ret.add(other);
+        }
+        return ret;
+    }
+
+
+    public TagCollection emptyTagsForKeysMissingIn(TagCollection other) {
+        TagCollection ret = new TagCollection();
+        for(String key: this.minus(other).getKeys()) {
+            ret.add(new Tag(key));
+        }
+        return ret;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/tags/MultiValueCellEditor.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/tags/MultiValueCellEditor.java	(revision 2008)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/tags/MultiValueCellEditor.java	(revision 2008)
@@ -0,0 +1,193 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.tags;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Component;
+import java.awt.Font;
+import java.awt.event.FocusAdapter;
+import java.awt.event.FocusEvent;
+import java.awt.event.KeyEvent;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import javax.swing.AbstractCellEditor;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JTable;
+import javax.swing.ListCellRenderer;
+import javax.swing.UIManager;
+import javax.swing.table.TableCellEditor;
+
+/**
+ * This is a table cell editor for selecting a possible tag value from a list of
+ * proposed tag values. The editor also allows to select all proposed valued or
+ * to remove the tag.
+ * 
+ * The editor responds intercepts some keys and interprets them as navigation keys. It
+ * forwards navigation events to {@see NavigationListener}s registred with this editor.
+ * You should register the parent table using this editor as {@see NavigationListener}.
+ * 
+ * {@see KeyEvent#VK_ENTER} and {@see KeyEvent#VK_TAB} trigger a {@see NavigationListener#gotoNextDecision()}.
+ */
+public class MultiValueCellEditor extends AbstractCellEditor implements TableCellEditor{
+
+
+    public static interface NavigationListener {
+        void gotoNextDecision();
+        void gotoPreviousDecision();
+    }
+
+    /** the combo box used as editor */
+    private JComboBox editor;
+    private DefaultComboBoxModel editorModel;
+    private CopyOnWriteArrayList<NavigationListener> listeners;
+
+    public void addNavigationListeners(NavigationListener listener) {
+        if (listener != null && ! (listeners.contains(listener))) {
+            listeners.add(listener);
+        }
+    }
+
+    public void removeavigationListeners(NavigationListener listener) {
+        if (listener != null && listeners.contains(listener)) {
+            listeners.remove(listener);
+        }
+    }
+
+    protected void fireGotoNextDecision() {
+        for (NavigationListener l: listeners) {
+            l.gotoNextDecision();
+        }
+    }
+
+    protected void fireGotoPreviousDecision() {
+        for (NavigationListener l: listeners) {
+            l.gotoPreviousDecision();
+        }
+    }
+
+    public MultiValueCellEditor() {
+        editorModel = new DefaultComboBoxModel();
+        editor = new JComboBox(editorModel) {
+            @Override
+            public void processKeyEvent(KeyEvent e) {
+                if (e.getID() == KeyEvent.KEY_PRESSED && e.getKeyCode() == KeyEvent.VK_ENTER) {
+                    fireGotoNextDecision();
+                } if (e.getID() == KeyEvent.KEY_PRESSED && e.getKeyCode() == KeyEvent.VK_TAB) {
+                    if (e.isShiftDown()) {
+                        fireGotoPreviousDecision();
+                    } else {
+                        fireGotoNextDecision();
+                    }
+                } else if ( e.getID() == KeyEvent.KEY_PRESSED && e.getKeyCode() == KeyEvent.VK_DELETE  || e.getKeyCode() == KeyEvent.VK_BACK_SPACE) {
+                    if (editorModel.getIndexOf(MultiValueDecisionType.KEEP_NONE) > 0) {
+                        editorModel.setSelectedItem(MultiValueDecisionType.KEEP_NONE);
+                        fireGotoNextDecision();
+                    }
+                } else if (e.getID() == KeyEvent.KEY_PRESSED && e.getKeyCode() == KeyEvent.VK_ESCAPE) {
+                    cancelCellEditing();
+                }
+                super.processKeyEvent(e);
+            }
+        };
+        editor.addFocusListener(
+                new FocusAdapter() {
+                    @Override
+                    public void focusGained(FocusEvent e) {
+                        editor.showPopup();
+                    }
+                }
+        );
+        editor.setRenderer(new EditorCellRenderer());
+        listeners = new CopyOnWriteArrayList<NavigationListener>();
+    }
+
+    protected void initEditor(MultiValueResolutionDecision decision) {
+        editorModel.removeAllElements();
+        for (String value: decision.getValues()) {
+            editorModel.addElement(value);
+        }
+        if (decision.canKeepNone()) {
+            editorModel.addElement(MultiValueDecisionType.KEEP_NONE);
+        }
+        if (decision.canKeepAll()) {
+            editorModel.addElement(MultiValueDecisionType.KEEP_ALL);
+        }
+        switch(decision.getDecisionType()) {
+            case UNDECIDED:
+                editor.setSelectedIndex(0);
+                break;
+            case KEEP_ONE:
+                editor.setSelectedItem(decision.getChosenValue());
+                break;
+            case KEEP_NONE:
+                editor.setSelectedItem(MultiValueDecisionType.KEEP_NONE);
+                break;
+            case KEEP_ALL:
+                editor.setSelectedItem(MultiValueDecisionType.KEEP_ALL);
+        }
+    }
+
+    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
+        MultiValueResolutionDecision decision = (MultiValueResolutionDecision)value;
+        initEditor(decision);
+        editor.requestFocus();
+        return editor;
+    }
+
+    public Object getCellEditorValue() {
+        return editor.getSelectedItem();
+    }
+
+
+    /**
+     * The cell renderer used in the combo box
+     *
+     */
+    static private class EditorCellRenderer extends JLabel implements ListCellRenderer {
+
+        public EditorCellRenderer() {
+            setOpaque(true);
+        }
+
+        protected void renderColors(boolean selected) {
+            if (selected) {
+                setForeground( UIManager.getColor("ComboBox.selectionForeground"));
+                setBackground(UIManager.getColor("ComboBox.selectionBackground"));
+            } else {
+                setForeground( UIManager.getColor("ComboBox.foreground"));
+                setBackground(UIManager.getColor("ComboBox.background"));
+            }
+        }
+
+        protected void renderValue(Object value) {
+            setFont(UIManager.getFont("ComboBox.font"));
+            if (String.class.isInstance(value)) {
+                setText(String.class.cast(value));
+            } else if (MultiValueDecisionType.class.isInstance(value)) {
+                switch(MultiValueDecisionType.class.cast(value)) {
+                    case KEEP_NONE:
+                        setText(tr("none"));
+                        setFont(UIManager.getFont("ComboBox.font").deriveFont(Font.ITALIC + Font.BOLD));
+                        break;
+                    case KEEP_ALL:
+                        setText(tr("all"));
+                        setFont(UIManager.getFont("ComboBox.font").deriveFont(Font.ITALIC + Font.BOLD));
+                        break;
+                    default:
+                        // don't display other values
+                }
+            }
+        }
+
+        public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
+                boolean cellHasFocus) {
+            renderColors(isSelected);
+            renderValue(value);
+            return this;
+        }
+    }
+}
+
Index: trunk/src/org/openstreetmap/josm/gui/conflict/tags/MultiValueCellRenderer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/tags/MultiValueCellRenderer.java	(revision 2008)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/tags/MultiValueCellRenderer.java	(revision 2008)
@@ -0,0 +1,92 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.tags;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Component;
+import java.awt.Font;
+
+import javax.swing.ImageIcon;
+import javax.swing.JLabel;
+import javax.swing.JTable;
+import javax.swing.UIManager;
+import javax.swing.table.TableCellRenderer;
+
+import org.openstreetmap.josm.tools.ImageProvider;
+
+public class MultiValueCellRenderer extends JLabel implements TableCellRenderer {
+
+    private ImageIcon iconDecided;
+    private ImageIcon iconUndecided;
+
+    public MultiValueCellRenderer() {
+        setOpaque(true);
+        iconDecided = ImageProvider.get("dialogs/conflict", "tagconflictresolved");
+        iconUndecided = ImageProvider.get("dialogs/conflict", "tagconflictunresolved");
+    }
+
+    protected void renderColors(boolean selected) {
+        if (selected) {
+            setForeground(UIManager.getColor("Table.selectionForeground"));
+            setBackground(UIManager.getColor("Table.selectionBackground"));
+        } else {
+            setForeground(UIManager.getColor("Table.foreground"));
+            setBackground(UIManager.getColor("Table.background"));
+        }
+    }
+
+    protected void renderValue(MultiValueResolutionDecision decision) {
+        switch(decision.getDecisionType()) {
+            case UNDECIDED:
+                setText(tr("Choose a value"));
+                setFont(getFont().deriveFont(Font.ITALIC));
+                setToolTipText(tr("Please decided which values to keep"));
+                break;
+            case KEEP_ONE:
+                setText(decision.getChosenValue());
+                setToolTipText(tr("Value ''{0}'' is going to be applied for key ''{1}''", decision.getChosenValue(), decision.getKey()));
+                break;
+            case KEEP_NONE:
+                setText(tr("deleted"));
+                setFont(getFont().deriveFont(Font.ITALIC));
+                setToolTipText(tr("The key ''{0}'' and all it's values are going to be removed", decision.getKey()));
+                break;
+            case KEEP_ALL:
+                setText(decision.getChosenValue());
+                setToolTipText(tr("All values joined as ''{0}'' are going to be applied for key ''{1}''", decision.getChosenValue(), decision.getKey()));
+                break;
+        }
+    }
+
+    protected void reset() {
+        setIcon(null);
+        setText("");
+        setFont(UIManager.getFont("Table.font"));
+    }
+
+    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
+            int row, int column) {
+
+        reset();
+        renderColors(isSelected);
+        MultiValueResolutionDecision decision = (MultiValueResolutionDecision)value;
+        switch(column) {
+            case 0:
+                if (decision.isDecided()) {
+                    setIcon(iconDecided);
+                } else {
+                    setIcon(iconUndecided);
+                }
+                break;
+
+            case 1:
+                setText(decision.getKey());
+                break;
+
+            case 2:
+                renderValue(decision);
+                break;
+        }
+        return this;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/tags/MultiValueDecisionType.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/tags/MultiValueDecisionType.java	(revision 2008)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/tags/MultiValueDecisionType.java	(revision 2008)
@@ -0,0 +1,18 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.tags;
+
+/**
+ * Represents a decision for a tag conflict due to multiple possible values.
+ *
+ *
+ */
+public enum MultiValueDecisionType {
+    /** not yet decided */
+    UNDECIDED,
+    /** keep exactly one values */
+    KEEP_ONE,
+    /** keep no value, delete the tag */
+    KEEP_NONE,
+    /** keep all values; concatenate them with ; */
+    KEEP_ALL,
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/tags/MultiValueResolutionDecision.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/tags/MultiValueResolutionDecision.java	(revision 2008)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/tags/MultiValueResolutionDecision.java	(revision 2008)
@@ -0,0 +1,312 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.tags;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.openstreetmap.josm.command.ChangePropertyCommand;
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.data.osm.TagCollection;
+/**
+ * Represents a decision for a conflict due to multiple possible value for a tag.
+ * 
+ *
+ */
+public class MultiValueResolutionDecision {
+
+    /** the type of decision */
+    private MultiValueDecisionType type;
+    /** the collection of tags for which a decision is needed */
+    private TagCollection  tags;
+    /** the selected value if {@see #type} is {@see MultiValueDecisionType#KEEP_ONE} */
+    private String value;
+
+    /**
+     * constuctor
+     */
+    public MultiValueResolutionDecision() {
+        type = MultiValueDecisionType.UNDECIDED;
+        tags = new TagCollection();
+        autoDecide();
+    }
+
+    /**
+     * Creates a new decision for the tag collection <code>tags</code>.
+     * All tags must have the same key.
+     * 
+     * @param tags the tags. Must not be null.
+     * @exception IllegalArgumentException  thrown if tags is null
+     * @exception IllegalArgumentException thrown if there are more than one keys
+     * @exception IllegalArgumentException thrown if tags is empty
+     */
+    public MultiValueResolutionDecision(TagCollection tags) throws IllegalArgumentException {
+        if (tags == null)
+            throw new IllegalArgumentException(tr("parameter ''{0}'' must not be null", "tags"));
+        if (tags.isEmpty())
+            throw new IllegalArgumentException(tr("parameter ''{0}'' must not be empty", "tags"));
+        if (tags.getKeys().size() != 1)
+            throw new IllegalArgumentException(tr("parameter ''{0}'' with tags for exactly one key expected. Got {1}", "tags", tags.getKeys().size()));
+        this.tags = tags;
+        autoDecide();
+    }
+
+    /**
+     * Tries to find the best decision based on the current values.
+     */
+    protected void autoDecide() {
+        this.type = MultiValueDecisionType.UNDECIDED;
+        // exactly one empty value ? -> delete the tag
+        if (tags.size() == 1 && tags.getValues().contains("")) {
+            this.type = MultiValueDecisionType.KEEP_NONE;
+
+            // exactly one non empty value? -> keep this value
+        } else if (tags.size() == 1) {
+            this.type = MultiValueDecisionType.KEEP_ONE;
+            this.value = tags.getValues().iterator().next();
+        }
+    }
+
+    /**
+     * Apply the decision to keep no value
+     */
+    public void keepNone() {
+        this.type = MultiValueDecisionType.KEEP_NONE;
+    }
+
+    /**
+     * Apply the decision to keep all values
+     */
+    public void keepAll() {
+        this.type = MultiValueDecisionType.KEEP_ALL;
+    }
+
+    /**
+     * Apply the decision to keep exactly one value
+     * 
+     * @param value  the value to keep
+     * @throws IllegalArgumentException thrown if value is null
+     * @throws IllegalStateException thrown if value is not in the list of known values for this tag
+     */
+    public void keepOne(String value) throws IllegalArgumentException, IllegalStateException {
+        if (value == null)
+            throw new IllegalArgumentException(tr("parameter ''{0}'' must not be null", "value"));
+        if (!tags.getValues().contains(value))
+            throw new IllegalStateException(tr("tag collection doesn't include the selected value ''{0}''", value));
+        this.value = value;
+        this.type = MultiValueDecisionType.KEEP_ONE;
+    }
+
+    /**
+     * sets a new value for this
+     * 
+     * @param value the new vlaue
+     */
+    public void setNew(String value) {
+        if (value == null) {
+            value = "";
+        }
+        this.value = value;
+        this.type = MultiValueDecisionType.KEEP_ONE;
+
+    }
+
+    /**
+     * marks this as undecided
+     * 
+     */
+    public void undecide() {
+        this.type = MultiValueDecisionType.UNDECIDED;
+    }
+
+    /**
+     * Replies the concatenation of all tag values (concatenated by a semicolon)
+     * 
+     * @return the concatenation of all tag values
+     */
+    protected String joinValues() {
+        StringBuffer buffer = new StringBuffer();
+        List<String> values = new ArrayList<String>(tags.getValues());
+        values.remove("");
+        Collections.sort(values);
+        Iterator<String> iter = values.iterator();
+        while (iter.hasNext()) {
+            buffer.append(iter.next());
+            if (iter.hasNext()) {
+                buffer.append(";");
+            }
+        }
+        return buffer.toString();
+    }
+
+    /**
+     * Replies the chosen value
+     * 
+     * @return the chosen value
+     * @throws IllegalStateException thrown if this resolution is not yet decided
+     */
+    public String getChosenValue() throws IllegalStateException {
+        switch(type) {
+            case UNDECIDED: throw new IllegalStateException(tr("not yet decided"));
+            case KEEP_ONE: return value;
+            case KEEP_NONE: return null;
+            case KEEP_ALL: return joinValues();
+        }
+        // should not happen
+        return null;
+    }
+
+    /**
+     * Replies the list of possible, non empty values
+     * 
+     * @return the list of possible, non empty values
+     */
+    public List<String> getValues() {
+        ArrayList<String> ret = new ArrayList<String>(tags.getValues());
+        ret.remove("");
+        Collections.sort(ret);
+        return ret;
+    }
+
+    /**
+     * Replies the key of the tag to be resolved by this resolution
+     * 
+     * @return the key of the tag to be resolved by this resolution
+     */
+    public String getKey() {
+        return tags.getKeys().iterator().next();
+    }
+
+    /**
+     * Replies true if the empty value is a possible value in this resolution
+     * 
+     * @return true if the empty value is a possible value in this resolution
+     */
+    public boolean canKeepNone() {
+        return tags.getValues().contains("");
+    }
+
+    /**
+     * Replies true, if this resolution has more than 1 possible non-empty values
+     * 
+     * @return true, if this resolution has more than 1 possible non-empty values
+     */
+    public boolean canKeepAll() {
+        return getValues().size() > 1;
+    }
+
+    /**
+     * Replies  true if this resolution is decided
+     * 
+     * @return true if this resolution is decided
+     */
+    public boolean isDecided() {
+        return !type.equals(MultiValueDecisionType.UNDECIDED);
+    }
+
+    /**
+     * Replies the type of the resolution
+     * 
+     * @return the type of the resolution
+     */
+    public MultiValueDecisionType getDecisionType() {
+        return type;
+    }
+
+    /**
+     * Applies the resolution to an {@see OsmPrimitive}
+     * 
+     * @param primitive the primitive
+     * @throws IllegalStateException thrown if this resolution is not resolved yet
+     * 
+     */
+    public void applyTo(OsmPrimitive primitive) throws IllegalStateException{
+        if (primitive == null) return;
+        if (!isDecided())
+            throw new IllegalStateException(tr("Not decided yet"));
+        String key = tags.getKeys().iterator().next();
+        String value = getChosenValue();
+        if (type.equals(MultiValueDecisionType.KEEP_NONE)) {
+            primitive.remove(key);
+        } else {
+            primitive.put(key, value);
+        }
+    }
+
+    /**
+     * Applies this resolution to a collection of primitives
+     * 
+     * @param primtives the collection of primitives
+     * @throws IllegalStateException thrown if this resolution is not resolved yet
+     */
+    public void applyTo(Collection<? extends OsmPrimitive> primtives) throws IllegalStateException {
+        if (primtives == null) return;
+        for (OsmPrimitive primitive: primtives) {
+            if (primitive == null) {
+                continue;
+            }
+            applyTo(primitive);
+        }
+    }
+
+    /**
+     * Builds a change command for applying this resolution to a primitive
+     * 
+     * @param primitive  the primitive
+     * @return the change command
+     * @throws IllegalArgumentException thrown if primitive is null
+     * @throws IllegalStateException thrown if this resolution is not resolved yet
+     */
+    public Command buildChangeCommand(OsmPrimitive primitive) throws IllegalArgumentException, IllegalStateException {
+        if (primitive == null)
+            throw new IllegalArgumentException(tr("parameter ''{0}'' must not be null", "primitive"));
+        if (!isDecided())
+            throw new IllegalStateException(tr("Not decided yet"));
+        String key = tags.getKeys().iterator().next();
+        String value = getChosenValue();
+        ChangePropertyCommand cmd = new ChangePropertyCommand(primitive, key,value);
+        return cmd;
+    }
+
+    /**
+     * Builds a change command for applying this resolution to a collection of primitives
+     * 
+     * @param primitives  the collection of primitives
+     * @return the change command
+     * @throws IllegalArgumentException thrown if primitives is null
+     * @throws IllegalStateException thrown if this resolution is not resolved yet
+     */
+    public Command buildChangeCommand(Collection<? extends OsmPrimitive> primitives) {
+        if (primitives == null)
+            throw new IllegalArgumentException(tr("parameter ''{0}'' must not be null", "primitives"));
+        if (!isDecided())
+            throw new IllegalStateException(tr("Not decided yet"));
+        String key = tags.getKeys().iterator().next();
+        String value = getChosenValue();
+        ChangePropertyCommand cmd = new ChangePropertyCommand(primitives, key,value);
+        return cmd;
+    }
+
+    /**
+     * Replies a tag representing the current resolution. Null, if this resolution is not resolved
+     * yet.
+     * 
+     * @return a tag representing the current resolution. Null, if this resolution is not resolved
+     * yet
+     */
+    public Tag getResolution() {
+        switch(type) {
+            case KEEP_ALL: return new Tag(getKey(), joinValues());
+            case KEEP_ONE: return new Tag(getKey(),value);
+            case KEEP_NONE: return new Tag(getKey(), "");
+            case UNDECIDED: return null;
+        }
+        return null;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/tags/PasteTagsConflictResolverDialog.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/tags/PasteTagsConflictResolverDialog.java	(revision 2008)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/tags/PasteTagsConflictResolverDialog.java	(revision 2008)
@@ -0,0 +1,526 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.tags;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+import static org.openstreetmap.josm.tools.I18n.trn;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Font;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.ImageIcon;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JTabbedPane;
+import javax.swing.JTable;
+import javax.swing.UIManager;
+import javax.swing.table.DefaultTableColumnModel;
+import javax.swing.table.DefaultTableModel;
+import javax.swing.table.TableCellRenderer;
+import javax.swing.table.TableColumn;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+import org.openstreetmap.josm.data.osm.TagCollection;
+import org.openstreetmap.josm.gui.SideButton;
+import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.WindowGeometry;
+
+public class PasteTagsConflictResolverDialog extends JDialog  implements PropertyChangeListener {
+    static private final Map<OsmPrimitiveType, String> PANE_TITLES;
+    static {
+        PANE_TITLES = new HashMap<OsmPrimitiveType, String>();
+        PANE_TITLES.put(OsmPrimitiveType.NODE, tr("Tags from nodes"));
+        PANE_TITLES.put(OsmPrimitiveType.WAY, tr("Tags from ways"));
+        PANE_TITLES.put(OsmPrimitiveType.RELATION, tr("Tags from relations"));
+    }
+
+    private enum Mode {
+        RESOLVING_ONE_TAGCOLLECTION_ONLY,
+        RESOLVING_TYPED_TAGCOLLECTIONS
+    }
+
+    private TagConflictResolver allPrimitivesResolver;
+    private Map<OsmPrimitiveType, TagConflictResolver> resolvers;
+    private JTabbedPane tpResolvers;
+    private Mode mode;
+    private boolean canceled = false;
+
+    private ImageIcon iconResolved;
+    private ImageIcon iconUnresolved;
+    private StatisticsTableModel statisticsModel;
+    private JPanel pnlTagResolver;
+
+    public PasteTagsConflictResolverDialog(Component owner) {
+        super(JOptionPane.getFrameForComponent(owner),true);
+        build();
+        iconResolved = ImageProvider.get("dialogs/conflict", "tagconflictresolved");
+        iconUnresolved = ImageProvider.get("dialogs/conflict", "tagconflictunresolved");
+    }
+
+    protected void build() {
+        setTitle(tr("Conflicts in pasted tags"));
+        allPrimitivesResolver = new TagConflictResolver();
+        resolvers = new HashMap<OsmPrimitiveType, TagConflictResolver>();
+        for (OsmPrimitiveType type: OsmPrimitiveType.values()) {
+            if (type.equals(OsmPrimitiveType.CHANGESET)) {
+                continue;
+            }
+            resolvers.put(type, new TagConflictResolver());
+            resolvers.get(type).getModel().addPropertyChangeListener(this);
+        }
+        tpResolvers = new JTabbedPane();
+        getContentPane().setLayout(new GridBagLayout());
+        mode = null;
+        GridBagConstraints gc = new GridBagConstraints();
+        gc.gridx = 0;
+        gc.gridy = 0;
+        gc.fill = GridBagConstraints.HORIZONTAL;
+        gc.weightx = 1.0;
+        gc.weighty = 0.0;
+        getContentPane().add(buildSourceAndTargetInfoPanel(), gc);
+        gc.gridx = 0;
+        gc.gridy = 1;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.weightx = 1.0;
+        gc.weighty = 1.0;
+        getContentPane().add(pnlTagResolver = new JPanel(), gc);
+        gc.gridx = 0;
+        gc.gridy = 2;
+        gc.fill = GridBagConstraints.HORIZONTAL;
+        gc.weightx = 1.0;
+        gc.weighty = 0.0;
+        getContentPane().add(buildButtonPanel(), gc);
+    }
+
+
+    protected JPanel buildButtonPanel() {
+        JPanel pnl = new JPanel();
+        pnl.setLayout(new FlowLayout(FlowLayout.CENTER));
+
+        // -- apply button
+        ApplyAction applyAction = new ApplyAction();
+        allPrimitivesResolver.getModel().addPropertyChangeListener(applyAction);
+        for (OsmPrimitiveType type: resolvers.keySet()) {
+            resolvers.get(type).getModel().addPropertyChangeListener(applyAction);
+        }
+        pnl.add(new SideButton(applyAction));
+
+        // -- cancel button
+        CancelAction cancelAction = new CancelAction();
+        pnl.add(new SideButton(cancelAction));
+
+        return pnl;
+    }
+
+    protected JPanel buildSourceAndTargetInfoPanel() {
+        JPanel pnl = new JPanel();
+        pnl.setLayout(new BorderLayout());
+        statisticsModel = new StatisticsTableModel();
+        pnl.add(new StatisticsInfoTable(statisticsModel), BorderLayout.CENTER);
+        return pnl;
+    }
+
+    protected void initResolver(OsmPrimitiveType type, TagCollection tc, Map<OsmPrimitiveType,Integer> targetStatistics) {
+        resolvers.get(type).getModel().populate(tc);
+        if (!tc.isEmpty() && targetStatistics.get(type) != null && targetStatistics.get(type) > 0) {
+            tpResolvers.add(PANE_TITLES.get(type), resolvers.get(type));
+        }
+    }
+
+    protected String formatStatisticsMessage(OsmPrimitiveType type, int count) {
+        String msg = "";
+        switch(type) {
+            case NODE: msg= trn("{0} node", "{0} nodes", count, count); break;
+            case WAY: msg= trn("{0} way", "{0} ways", count, count); break;
+            case RELATION: msg= trn("{0} relation", "{0} relations", count, count); break;
+        }
+        return msg;
+    }
+
+    public void populate(TagCollection tagsForAllPrimitives, Map<OsmPrimitiveType, Integer> sourceStatistics, Map<OsmPrimitiveType,Integer> targetStatistics) {
+        mode = Mode.RESOLVING_ONE_TAGCOLLECTION_ONLY;
+        tagsForAllPrimitives = tagsForAllPrimitives == null? new TagCollection() : tagsForAllPrimitives;
+        sourceStatistics = sourceStatistics == null ? new HashMap<OsmPrimitiveType, Integer>() :sourceStatistics;
+        targetStatistics = targetStatistics == null ? new HashMap<OsmPrimitiveType, Integer>() : targetStatistics;
+        allPrimitivesResolver.getModel().populate(tagsForAllPrimitives);
+        pnlTagResolver.setLayout(new BorderLayout());
+        pnlTagResolver.removeAll();
+        pnlTagResolver.add(allPrimitivesResolver, BorderLayout.CENTER);
+
+        statisticsModel.reset();
+        StatisticsInfo info = new StatisticsInfo();
+        info.numTags = tagsForAllPrimitives.getKeys().size();
+        for (OsmPrimitiveType type: sourceStatistics.keySet()) {
+            info.sourceInfo.put(type, sourceStatistics.get(type));
+        }
+        for (OsmPrimitiveType type: targetStatistics.keySet()) {
+            info.targetInfo.put(type, targetStatistics.get(type));
+        }
+        statisticsModel.append(info);
+        validate();
+    }
+
+    protected int getNumResolverTabs() {
+        return tpResolvers.getTabCount();
+    }
+
+    protected TagConflictResolver getResolver(int idx) {
+        return (TagConflictResolver)tpResolvers.getComponentAt(idx);
+    }
+
+    public void populate(TagCollection tagsForNodes, TagCollection tagsForWays, TagCollection tagsForRelations, Map<OsmPrimitiveType,Integer> sourceStatistics, Map<OsmPrimitiveType, Integer> targetStatistics) {
+        tagsForNodes = (tagsForNodes == null) ? new TagCollection() : tagsForNodes;
+        tagsForWays = (tagsForWays == null) ? new TagCollection() : tagsForWays;
+        tagsForRelations = (tagsForRelations == null) ? new TagCollection() : tagsForRelations;
+        if (tagsForNodes.isEmpty() && tagsForWays.isEmpty() && tagsForRelations.isEmpty()) {
+            populate(null,null,null);
+            return;
+        }
+        tpResolvers.removeAll();
+        initResolver(OsmPrimitiveType.NODE,tagsForNodes, targetStatistics);
+        initResolver(OsmPrimitiveType.WAY,tagsForWays, targetStatistics);
+        initResolver(OsmPrimitiveType.RELATION,tagsForRelations, targetStatistics);
+
+        pnlTagResolver.setLayout(new BorderLayout());
+        pnlTagResolver.removeAll();
+        pnlTagResolver.add(tpResolvers, BorderLayout.CENTER);
+        mode = Mode.RESOLVING_TYPED_TAGCOLLECTIONS;
+        validate();
+        statisticsModel.reset();
+        if (!tagsForNodes.isEmpty()) {
+            StatisticsInfo info = new StatisticsInfo();
+            info.numTags = tagsForNodes.getKeys().size();
+            int numTargets = targetStatistics.get(OsmPrimitiveType.NODE) == null ? 0 : targetStatistics.get(OsmPrimitiveType.NODE);
+            if (numTargets > 0) {
+                info.sourceInfo.put(OsmPrimitiveType.NODE, sourceStatistics.get(OsmPrimitiveType.NODE));
+                info.targetInfo.put(OsmPrimitiveType.NODE, numTargets);
+                statisticsModel.append(info);
+            }
+        }
+        if (!tagsForWays.isEmpty()) {
+            StatisticsInfo info = new StatisticsInfo();
+            info.numTags = tagsForWays.getKeys().size();
+            int numTargets = targetStatistics.get(OsmPrimitiveType.WAY) == null ? 0 : targetStatistics.get(OsmPrimitiveType.WAY);
+            if (numTargets > 0) {
+                info.sourceInfo.put(OsmPrimitiveType.WAY, sourceStatistics.get(OsmPrimitiveType.WAY));
+                info.targetInfo.put(OsmPrimitiveType.WAY, numTargets);
+                statisticsModel.append(info);
+            }
+        }
+        if (!tagsForRelations.isEmpty()) {
+            StatisticsInfo info = new StatisticsInfo();
+            info.numTags = tagsForRelations.getKeys().size();
+            int numTargets = targetStatistics.get(OsmPrimitiveType.RELATION) == null ? 0 : targetStatistics.get(OsmPrimitiveType.RELATION);
+            if (numTargets > 0) {
+                info.sourceInfo.put(OsmPrimitiveType.RELATION, sourceStatistics.get(OsmPrimitiveType.RELATION));
+                info.targetInfo.put(OsmPrimitiveType.RELATION, numTargets);
+                statisticsModel.append(info);
+            }
+        }
+
+        for (int i =0; i < getNumResolverTabs(); i++) {
+            if (!getResolver(i).getModel().isResolvedCompletely()) {
+                tpResolvers.setSelectedIndex(i);
+                break;
+            }
+        }
+    }
+
+    protected void setCanceled(boolean canceled) {
+        this.canceled = canceled;
+    }
+
+    public boolean isCanceled() {
+        return this.canceled;
+    }
+
+    class CancelAction extends AbstractAction {
+
+        public CancelAction() {
+            putValue(Action.SHORT_DESCRIPTION, tr("Cancel conflict resolution"));
+            putValue(Action.NAME, tr("Cancel"));
+            putValue(Action.SMALL_ICON, ImageProvider.get("", "cancel"));
+            setEnabled(true);
+        }
+
+        public void actionPerformed(ActionEvent arg0) {
+            setVisible(false);
+            setCanceled(true);
+        }
+    }
+
+    class ApplyAction extends AbstractAction implements PropertyChangeListener {
+
+        public ApplyAction() {
+            putValue(Action.SHORT_DESCRIPTION, tr("Apply resolved conflicts"));
+            putValue(Action.NAME, tr("Apply"));
+            putValue(Action.SMALL_ICON, ImageProvider.get("ok"));
+            updateEnabledState();
+        }
+
+        public void actionPerformed(ActionEvent arg0) {
+            setVisible(false);
+        }
+
+        protected void updateEnabledState() {
+            if (mode == null) {
+                setEnabled(false);
+            } else if (mode.equals(Mode.RESOLVING_ONE_TAGCOLLECTION_ONLY)) {
+                setEnabled(allPrimitivesResolver.getModel().isResolvedCompletely());
+            } else {
+                boolean enabled = true;
+                for (OsmPrimitiveType type: resolvers.keySet()) {
+                    enabled &= resolvers.get(type).getModel().isResolvedCompletely();
+                }
+                setEnabled(enabled);
+            }
+        }
+
+        public void propertyChange(PropertyChangeEvent evt) {
+            if (evt.getPropertyName().equals(TagConflictResolverModel.RESOLVED_COMPLETELY_PROP)) {
+                updateEnabledState();
+            }
+        }
+    }
+
+    @Override
+    public void setVisible(boolean visible) {
+        if (visible) {
+            new WindowGeometry(
+                    getClass().getName() + ".geometry",
+                    WindowGeometry.centerOnScreen(new Dimension(400,300))
+            ).applySafe(this);
+        } else {
+            new WindowGeometry(this).remember(getClass().getName() + ".geometry");
+        }
+        super.setVisible(visible);
+    }
+
+    public TagCollection getResolution() {
+        return allPrimitivesResolver.getModel().getResolution();
+    }
+
+    public TagCollection getResolution(OsmPrimitiveType type) {
+        if (type == null) return null;
+        return resolvers.get(type).getModel().getResolution();
+    }
+
+    public void propertyChange(PropertyChangeEvent evt) {
+        if (evt.getPropertyName().equals(TagConflictResolverModel.RESOLVED_COMPLETELY_PROP)) {
+            TagConflictResolverModel model = (TagConflictResolverModel)evt.getSource();
+            for (int i=0; i < tpResolvers.getTabCount();i++) {
+                TagConflictResolver resolver = (TagConflictResolver)tpResolvers.getComponentAt(i);
+                if (model == resolver.getModel()) {
+                    tpResolvers.setIconAt(i,
+                            (Boolean)evt.getNewValue() ? iconResolved : iconUnresolved
+
+                    );
+                }
+            }
+        }
+    }
+
+    static public class StatisticsInfo {
+        public int numTags;
+        public Map<OsmPrimitiveType, Integer> sourceInfo;
+        public Map<OsmPrimitiveType, Integer> targetInfo;
+
+        public StatisticsInfo() {
+            sourceInfo = new HashMap<OsmPrimitiveType, Integer>();
+            targetInfo = new HashMap<OsmPrimitiveType, Integer>();
+        }
+    }
+
+    static private class StatisticsTableColumnModel extends DefaultTableColumnModel {
+        public StatisticsTableColumnModel() {
+            TableCellRenderer renderer = new StatisticsInfoRenderer();
+            TableColumn col = null;
+
+            // column 0 - Paste
+            col = new TableColumn(0);
+            col.setHeaderValue(tr("Paste ..."));
+            col.setResizable(true);
+            col.setCellRenderer(renderer);
+            addColumn(col);
+
+            // column 1 - From
+            col = new TableColumn(1);
+            col.setHeaderValue(tr("From ..."));
+            col.setResizable(true);
+            col.setCellRenderer(renderer);
+            addColumn(col);
+
+            // column 2 - To
+            col = new TableColumn(2);
+            col.setHeaderValue(tr("To ..."));
+            col.setResizable(true);
+            col.setCellRenderer(renderer);
+            addColumn(col);
+        }
+    }
+
+    static private class StatisticsTableModel extends DefaultTableModel {
+        private static final String[] HEADERS = new String[] {tr("Paste ..."), tr("From ..."), tr("To ...") };
+        private List<StatisticsInfo> data;
+
+        public StatisticsTableModel() {
+            data = new ArrayList<StatisticsInfo>();
+        }
+
+        @Override
+        public Object getValueAt(int row, int column) {
+            if (row == 0)
+                return HEADERS[column];
+            else if (row -1 < data.size())
+                return data.get(row -1);
+            else
+                return null;
+        }
+
+        @Override
+        public boolean isCellEditable(int row, int column) {
+            return false;
+        }
+
+        @Override
+        public int getRowCount() {
+            if (data == null) return 1;
+            return data.size() + 1;
+        }
+
+        public void reset() {
+            data.clear();
+        }
+
+        public void append(StatisticsInfo info) {
+            data.add(info);
+            fireTableDataChanged();
+        }
+    }
+
+    static private class StatisticsInfoRenderer extends JLabel implements TableCellRenderer {
+        static private final Logger logger = Logger.getLogger(StatisticsInfoRenderer.class.getName());
+
+        protected void reset() {
+            setIcon(null);
+            setText("");
+            setFont(UIManager.getFont("Table.font"));
+        }
+        protected void renderNumTags(StatisticsInfo info) {
+            if (info == null) return;
+            setText(trn("{0} tag", "{0} tags", info.numTags, info.numTags));
+        }
+
+        protected void renderFrom(StatisticsInfo info) {
+            if (info == null) return;
+            if (info.sourceInfo == null) return;
+            if (info.sourceInfo.isEmpty()) return;
+            if (info.sourceInfo.size() == 1) {
+                setIcon(ImageProvider.get(info.sourceInfo.keySet().iterator().next()));
+            } else {
+                setIcon(ImageProvider.get("data", "object"));
+            }
+            String text = "";
+            for (OsmPrimitiveType type: info.sourceInfo.keySet()) {
+                int numPrimitives = info.sourceInfo.get(type) == null ? 0 : info.sourceInfo.get(type);
+                if (numPrimitives == 0) {
+                    continue;
+                }
+                String msg = "";
+                switch(type) {
+                    case NODE: msg = trn("{0} node", "{0} nodes", numPrimitives,numPrimitives); break;
+                    case WAY: msg = trn("{0} way", "{0} ways", numPrimitives, numPrimitives); break;
+                    case RELATION: msg = trn("{0} relation", "{0} relations", numPrimitives, numPrimitives); break;
+                }
+                text = text.equals("") ? msg : text + ", " + msg;
+            }
+            setText(text);
+        }
+
+        protected void renderTo(StatisticsInfo info) {
+            if (info == null) return;
+            if (info.targetInfo == null) return;
+            if (info.targetInfo.isEmpty()) return;
+            if (info.targetInfo.size() == 1) {
+                setIcon(ImageProvider.get(info.targetInfo.keySet().iterator().next()));
+            } else {
+                setIcon(ImageProvider.get("data", "object"));
+            }
+            String text = "";
+            for (OsmPrimitiveType type: info.targetInfo.keySet()) {
+                int numPrimitives = info.targetInfo.get(type) == null ? 0 : info.targetInfo.get(type);
+                if (numPrimitives == 0) {
+                    continue;
+                }
+                String msg = "";
+                switch(type) {
+                    case NODE: msg = trn("{0} node", "{0} nodes", numPrimitives,numPrimitives); break;
+                    case WAY: msg = trn("{0} way", "{0} ways", numPrimitives, numPrimitives); break;
+                    case RELATION: msg = trn("{0} relation", "{0} relations", numPrimitives, numPrimitives); break;
+                }
+                text = text.equals("") ? msg : text + ", " + msg;
+            }
+            setText(text);
+        }
+
+        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
+                boolean hasFocus, int row, int column) {
+            reset();
+            if (row == 0) {
+                setFont(getFont().deriveFont(Font.BOLD));
+                setText((String)value);
+            } else {
+                StatisticsInfo info = (StatisticsInfo) value;
+
+                switch(column) {
+                    case 0: renderNumTags(info); break;
+                    case 1: renderFrom(info); break;
+                    case 2: renderTo(info); break;
+                }
+            }
+            return this;
+        }
+    }
+
+    static private class StatisticsInfoTable extends JPanel {
+
+        private JTable infoTable;
+
+        protected void build(StatisticsTableModel model) {
+            infoTable = new JTable(model, new StatisticsTableColumnModel());
+            infoTable.setShowHorizontalLines(true);
+            infoTable.setShowVerticalLines(false);
+            infoTable.setEnabled(false);
+            setLayout(new BorderLayout());
+            add(infoTable, BorderLayout.CENTER);
+        }
+
+        public StatisticsInfoTable(StatisticsTableModel model) {
+            build(model);
+        }
+
+        @Override
+        public Insets getInsets() {
+            Insets insets = super.getInsets();
+            insets.bottom = 20;
+            return insets;
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagConflictResolver.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagConflictResolver.java	(revision 2008)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagConflictResolver.java	(revision 2008)
@@ -0,0 +1,25 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.tags;
+
+import java.awt.BorderLayout;
+
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+
+public class TagConflictResolver extends JPanel {
+
+    private TagConflictResolverModel model;
+
+    protected void build() {
+        setLayout(new BorderLayout());
+        add(new JScrollPane(new TagConflictResolverTable(model)), BorderLayout.CENTER);
+    }
+    public TagConflictResolver() {
+        this.model = new TagConflictResolverModel();
+        build();
+    }
+
+    public TagConflictResolverModel getModel() {
+        return model;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagConflictResolverColumnModel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagConflictResolverColumnModel.java	(revision 2008)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagConflictResolverColumnModel.java	(revision 2008)
@@ -0,0 +1,45 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.tags;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import javax.swing.table.DefaultTableColumnModel;
+import javax.swing.table.TableColumn;
+
+public class TagConflictResolverColumnModel extends DefaultTableColumnModel{
+
+    protected void createColumns() {
+        TableColumn col = null;
+        MultiValueCellRenderer renderer = new MultiValueCellRenderer();
+        MultiValueCellEditor editor = new MultiValueCellEditor();
+
+        // column 0 - State
+        col = new TableColumn(0);
+        col.setHeaderValue("");
+        col.setResizable(true);
+        col.setWidth(20);
+        col.setPreferredWidth(20);
+        col.setMaxWidth(30);
+        col.setCellRenderer(renderer);
+        addColumn(col);
+
+        // column 1 - Key
+        col = new TableColumn(1);
+        col.setHeaderValue(tr("Key"));
+        col.setResizable(true);
+        col.setCellRenderer(renderer);
+        addColumn(col);
+
+        // column 2 - Value
+        col = new TableColumn(2);
+        col.setHeaderValue(tr("Value"));
+        col.setResizable(true);
+        col.setCellRenderer(renderer);
+        col.setCellEditor(editor);
+        addColumn(col);
+    }
+
+    public TagConflictResolverColumnModel() {
+        createColumns();
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagConflictResolverModel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagConflictResolverModel.java	(revision 2008)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagConflictResolverModel.java	(revision 2008)
@@ -0,0 +1,146 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.tags;
+
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+
+import javax.swing.table.DefaultTableModel;
+import static org.openstreetmap.josm.tools.I18n.tr;
+import org.openstreetmap.josm.data.osm.TagCollection;
+
+public class TagConflictResolverModel extends DefaultTableModel {
+    static public final String RESOLVED_COMPLETELY_PROP = TagConflictResolverModel.class.getName() + ".resolvedCompletely";
+
+    private TagCollection tags;
+    private List<String> keys;
+    private HashMap<String, MultiValueResolutionDecision> decisions;
+    private boolean resolvedCompletely;
+    private PropertyChangeSupport support;
+
+    public TagConflictResolverModel() {
+        resolvedCompletely= false;
+        support = new PropertyChangeSupport(this);
+    }
+
+    public void addPropertyChangeListener(PropertyChangeListener listener) {
+        support.addPropertyChangeListener(listener);
+    }
+
+    public void removePropertyChangeListener(PropertyChangeListener listener) {
+        support.removePropertyChangeListener(listener);
+    }
+
+    protected void setResolvedCompletely(boolean resolvedCompletey) {
+        boolean oldValue = this.resolvedCompletely;
+        this.resolvedCompletely = resolvedCompletey;
+        if (oldValue != this.resolvedCompletely) {
+            support.firePropertyChange(RESOLVED_COMPLETELY_PROP, oldValue, this.resolvedCompletely);
+        }
+    }
+
+    protected void refreshResolvedCompletely() {
+        for (MultiValueResolutionDecision d : decisions.values()) {
+            if (!d.isDecided()) {
+                setResolvedCompletely(false);
+                return;
+            }
+        }
+        setResolvedCompletely(true);
+    }
+
+    protected void sort() {
+        Collections.sort(
+                keys,
+                new Comparator<String>() {
+                    public int compare(String o1, String o2) {
+                        if (decisions.get(o1).isDecided() && ! decisions.get(o2).isDecided())
+                            return 1;
+                        else if (!decisions.get(o1).isDecided() && decisions.get(o2).isDecided())
+                            return -1;
+                        return o1.compareTo(o2);
+                    }
+                }
+        );
+    }
+
+    protected void init() {
+        keys.clear();
+        keys.addAll(tags.getKeys());
+        for(String key: tags.getKeys()) {
+            MultiValueResolutionDecision decision = new MultiValueResolutionDecision(tags.getTagsFor(key));
+            decisions.put(key,decision);
+        }
+        refreshResolvedCompletely();
+    }
+
+    public void populate(TagCollection tags) {
+        if (tags == null)
+            throw new IllegalArgumentException(tr("parameter ''{0}'' must not be null", "tags"));
+        this.tags = tags;
+        keys = new ArrayList<String>();
+        decisions = new HashMap<String, MultiValueResolutionDecision>();
+        init();
+        sort();
+        fireTableDataChanged();
+    }
+
+
+    @Override
+    public int getRowCount() {
+        if (keys == null) return 0;
+        return keys.size();
+    }
+
+    @Override
+    public Object getValueAt(int row, int column) {
+        return decisions.get(keys.get(row));
+    }
+
+    @Override
+    public boolean isCellEditable(int row, int column) {
+        return column == 2;
+    }
+
+    @Override
+    public void setValueAt(Object value, int row, int column) {
+        MultiValueResolutionDecision decision = decisions.get(keys.get(row));
+        if (value instanceof String) {
+            decision.keepOne((String)value);
+        } else if (value instanceof MultiValueDecisionType) {
+            MultiValueDecisionType type = (MultiValueDecisionType)value;
+            switch(type) {
+                case KEEP_NONE:
+                    decision.keepNone();
+                    break;
+                case KEEP_ALL:
+                    decision.keepAll();
+                    break;
+            }
+        }
+        fireTableDataChanged();
+        refreshResolvedCompletely();
+    }
+
+    /**
+     * Replies true if each {@see MultiValueResolutionDecision} is decided.
+     * 
+     * @return true if each {@see MultiValueResolutionDecision} is decided; false
+     * otherwise
+     */
+    public boolean isResolvedCompletely() {
+        return resolvedCompletely;
+    }
+
+    public TagCollection getResolution() {
+        TagCollection tc = new TagCollection();
+        for (String key: keys) {
+            tc.add(decisions.get(key).getResolution());
+        }
+        return tc;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagConflictResolverTable.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagConflictResolverTable.java	(revision 2008)
+++ trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagConflictResolverTable.java	(revision 2008)
@@ -0,0 +1,112 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.tags;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+
+import javax.swing.AbstractAction;
+import javax.swing.JComponent;
+import javax.swing.JTable;
+import javax.swing.KeyStroke;
+import javax.swing.ListSelectionModel;
+
+public class TagConflictResolverTable extends JTable implements MultiValueCellEditor.NavigationListener {
+
+    private SelectNextColumnCellAction selectNextColumnCellAction;
+    private SelectPreviousColumnCellAction selectPreviousColumnCellAction;
+
+    public TagConflictResolverTable(TagConflictResolverModel model) {
+        super(model, new TagConflictResolverColumnModel());
+        build();
+    }
+
+    protected void build() {
+        setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
+        setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+        putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
+
+        // make ENTER behave like TAB
+        //
+        getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
+                KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "selectNextColumnCell");
+
+        // install custom navigation actions
+        //
+        selectNextColumnCellAction = new SelectNextColumnCellAction();
+        selectPreviousColumnCellAction = new SelectPreviousColumnCellAction();
+        getActionMap().put("selectNextColumnCell", selectNextColumnCellAction);
+        getActionMap().put("selectPreviousColumnCell", selectPreviousColumnCellAction);
+
+        ((MultiValueCellEditor)getColumnModel().getColumn(2).getCellEditor()).addNavigationListeners(this);
+    }
+
+    /**
+     * Action to be run when the user navigates to the next cell in the table, for instance by
+     * pressing TAB or ENTER. The action alters the standard navigation path from cell to cell: <ul>
+     * <li>it jumps over cells in the first column</li> <li>it automatically add a new empty row
+     * when the user leaves the last cell in the table</li> <ul>
+     *
+     *
+     */
+    class SelectNextColumnCellAction extends AbstractAction {
+        public void actionPerformed(ActionEvent e) {
+            run();
+        }
+
+        public void run() {
+            int col = getSelectedColumn();
+            int row = getSelectedRow();
+            if (getCellEditor() != null) {
+                getCellEditor().stopCellEditing();
+            }
+
+            if (col == 2 && row < getRowCount() - 1) {
+                row++;
+            } else if (row < getRowCount() - 1) {
+                col = 2;
+                row++;
+            }
+            changeSelection(row, col, false, false);
+            editCellAt(getSelectedRow(), getSelectedColumn());
+            getEditorComponent().requestFocusInWindow();
+        }
+    }
+
+    /**
+     * Action to be run when the user navigates to the previous cell in the table, for instance by
+     * pressing Shift-TAB
+     *
+     */
+    class SelectPreviousColumnCellAction extends AbstractAction {
+
+        public void actionPerformed(ActionEvent e) {
+            run();
+        }
+
+        public void run() {
+            int col = getSelectedColumn();
+            int row = getSelectedRow();
+            if (getCellEditor() != null) {
+                getCellEditor().stopCellEditing();
+            }
+
+            if (col <= 0 && row <= 0) {
+                // change nothing
+            } else if (row > 0) {
+                col = 2;
+                row--;
+            }
+            changeSelection(row, col, false, false);
+            editCellAt(getSelectedRow(), getSelectedColumn());
+            getEditorComponent().requestFocusInWindow();
+        }
+    }
+
+    public void gotoNextDecision() {
+        selectNextColumnCellAction.run();
+    }
+
+    public void gotoPreviousDecision() {
+        selectPreviousColumnCellAction.run();
+    }
+}
Index: trunk/src/org/openstreetmap/josm/tools/WindowGeometry.java
===================================================================
--- trunk/src/org/openstreetmap/josm/tools/WindowGeometry.java	(revision 2008)
+++ trunk/src/org/openstreetmap/josm/tools/WindowGeometry.java	(revision 2008)
@@ -0,0 +1,216 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.tools;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Dimension;
+import java.awt.Point;
+import java.awt.Toolkit;
+import java.awt.Window;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.openstreetmap.josm.Main;
+
+/**
+ * This is a helper class for persisting the geometry of a JOSM window to the preference store
+ * and for restoring it from the preference store.
+ * 
+ */
+public class WindowGeometry {
+
+    /**
+     * Replies a window geometry object for a window with a specific size which is
+     * centered on screen
+     * 
+     * @param extent  the size
+     * @return the geometry object
+     */
+    static public WindowGeometry centerOnScreen(Dimension extent) {
+        Point topLeft = new Point(
+                Math.max(0, (Toolkit.getDefaultToolkit().getScreenSize().width - extent.width) /2),
+                Math.max(0, (Toolkit.getDefaultToolkit().getScreenSize().height - extent.height) /2)
+        );
+        return new WindowGeometry(topLeft, extent);
+    }
+
+    /**
+     * Replies a window geometry object for a window which a specific size which is centered
+     * relative to a parent window
+     * 
+     * @param parent the parent window
+     * @param extent the size
+     * @return the geometry object
+     */
+    static public WindowGeometry centerInWindow(Window parent, Dimension extent) {
+        Point topLeft = new Point(
+                Math.max(0, (parent.getSize().width - extent.width) /2),
+                Math.max(0, (parent.getSize().height - extent.height) /2)
+        );
+        topLeft.x += parent.getLocation().x;
+        topLeft.y += parent.getLocation().y;
+        return new WindowGeometry(topLeft, extent);
+    }
+
+    /**
+     * Exception thrown by the WindowGeometry class if something goes wrong
+     * 
+     */
+    static public class WindowGeometryException extends Exception {
+        public WindowGeometryException(String message, Throwable cause) {
+            super(message, cause);
+        }
+
+        public WindowGeometryException(String message) {
+            super(message);
+        }
+    }
+
+    /** the top left point */
+    private Point topLeft;
+    /** the size */
+    private Dimension extent;
+
+    /**
+     * 
+     * @param topLeft the top left point
+     * @param extent the extent
+     */
+    public WindowGeometry(Point topLeft, Dimension extent) {
+        this.topLeft = topLeft;
+        this.extent = extent;
+    }
+
+    /**
+     * Creates a window geometry from the position and the size of a window.
+     * 
+     * @param window the window
+     */
+    public WindowGeometry(Window window)  {
+        this(window.getLocationOnScreen(), window.getSize());
+    }
+
+    protected int parseField(String preferenceKey, String preferenceValue, String field) throws WindowGeometryException {
+        String v = "";
+        try {
+            Pattern p = Pattern.compile(field + "=(\\d+)",Pattern.CASE_INSENSITIVE);
+            Matcher m = p.matcher(preferenceValue);
+            if (!m.matches())
+                throw new WindowGeometryException(tr("preference with key ''{0}'' doesn't include ''{1}=ddd''. Can't resstore window geometry from preferences.", preferenceKey, field));
+            v = m.group(1);
+            return Integer.parseInt(v);
+        } catch(WindowGeometryException e) {
+            throw e;
+        } catch(NumberFormatException e) {
+            throw new WindowGeometryException(tr("preference with key ''{0}'' doesn't provide an int value for ''{1}''. Got {2}. Can't resstore window geometry from preferences.", preferenceKey, field, v));
+        } catch(Exception e) {
+            throw new WindowGeometryException(tr("failed to parse field ''{1}'' in preference with key ''{0}''. Exception was: {2}. Can't resstore window geometry from preferences.", preferenceKey, field, e.toString()), e);
+        }
+    }
+
+    protected void initFromPreferences(String preferenceKey) throws WindowGeometryException {
+        String value = Main.pref.get(preferenceKey);
+        if (value == null)
+            throw new WindowGeometryException(tr("preference with key ''{0}'' doesn't exist. Can't resstore window geometry from preferences.", preferenceKey));
+        topLeft = new Point();
+        extent = new Dimension();
+        topLeft.x = parseField(preferenceKey, value, "x");
+        topLeft.y = parseField(preferenceKey, value, "y");
+        extent.width = parseField(preferenceKey, value, "width");
+        extent.height = parseField(preferenceKey, value, "height");
+    }
+
+    protected void initFromWindowGeometry(WindowGeometry other) {
+        this.topLeft = other.topLeft;
+        this.extent = other.extent;
+    }
+
+    /**
+     * Creates a window geometry from the values kept in the preference store under the
+     * key <code>preferenceKey</code>
+     * 
+     * @param preferenceKey the preference key
+     * @throws WindowGeometryException thrown if no such key exist or if the preference value has
+     * an illegal format
+     */
+    public WindowGeometry(String preferenceKey) throws WindowGeometryException {
+        initFromPreferences(preferenceKey);
+    }
+
+    /**
+     * Creates a window geometry from the values kept in the preference store under the
+     * key <code>preferenceKey</code>. Falls back to the <code>defaultGeometry</code> if
+     * something goes wrong.
+     * 
+     * @param preferenceKey the preference key
+     * @param defaultGeometry the default geometry
+     * 
+     */
+    public WindowGeometry(String preferenceKey, WindowGeometry defaultGeometry) {
+        try {
+            initFromPreferences(preferenceKey);
+        } catch(WindowGeometryException e) {
+            initFromWindowGeometry(defaultGeometry);
+        }
+    }
+
+    /**
+     * Remembers a window geometry under a specific preference key
+     * 
+     * @param preferenceKey the preference key
+     */
+    public void remember(String preferenceKey) {
+        StringBuffer value = new StringBuffer();
+        value.append("x=").append(topLeft.x).append(",")
+        .append("y=").append(topLeft.y).append(",")
+        .append("width=").append(extent.width).append(",")
+        .append("height=").append(extent.height);
+        Main.pref.put(preferenceKey, value.toString());
+    }
+
+    /**
+     * Replies the top left point for the geometry
+     * 
+     * @return  the top left point for the geometry
+     */
+    public Point getTopLeft() {
+        return topLeft;
+    }
+
+    /**
+     * Replies the size spezified by the geometry
+     * 
+     * @return the size spezified by the geometry
+     */
+    public Dimension getSize() {
+        return extent;
+    }
+
+    /**
+     * Applies this geometry to a window
+     * 
+     * @param window the window
+     */
+    public void apply(Window window) {
+        window.setLocation(topLeft);
+        window.setSize(extent);
+    }
+
+    /**
+     * Applies this geometry to a window. Makes sure that the window is not placed outside
+     * of the coordinate range of the current screen.
+     * 
+     * @param window the window
+     */
+    public void applySafe(Window window) {
+        Point p = new Point(topLeft);
+        if (p.x > Toolkit.getDefaultToolkit().getScreenSize().width - 10) {
+            p.x  = 0;
+        }
+        if (p.y >  Toolkit.getDefaultToolkit().getScreenSize().height - 10) {
+            p.y = 0;
+        }
+        window.setLocation(p);
+        window.setSize(extent);
+    }
+}
