Index: /trunk/src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java	(revision 11329)
+++ /trunk/src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java	(revision 11330)
@@ -315,5 +315,5 @@
                 getInputStream()) {
             byte[] data = Utils.readBytesFromStream(in);
-            if (data == null || data.length == 0) {
+            if (data.length == 0) {
                 cf.clear();
                 throw new IllegalArgumentException("Could not read data from: " + baseUrl);
Index: /trunk/src/org/openstreetmap/josm/data/validation/TestError.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/TestError.java	(revision 11329)
+++ /trunk/src/org/openstreetmap/josm/data/validation/TestError.java	(revision 11330)
@@ -31,16 +31,16 @@
     private boolean ignored;
     /** Severity */
-    private Severity severity;
+    private final Severity severity;
     /** The error message */
-    private String message;
+    private final String message;
     /** Deeper error description */
     private final String description;
     private final String descriptionEn;
     /** The affected primitives */
-    private Collection<? extends OsmPrimitive> primitives;
+    private final Collection<? extends OsmPrimitive> primitives;
     /** The primitives or way segments to be highlighted */
     private final Collection<?> highlighted;
     /** The tester that raised this error */
-    private Test tester;
+    private final Test tester;
     /** Internal code used by testers to classify errors */
     private final int code;
Index: /trunk/src/org/openstreetmap/josm/gui/conflict/pair/AbstractListMergeModel.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/conflict/pair/AbstractListMergeModel.java	(revision 11330)
+++ /trunk/src/org/openstreetmap/josm/gui/conflict/pair/AbstractListMergeModel.java	(revision 11330)
@@ -0,0 +1,879 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair;
+
+import static org.openstreetmap.josm.gui.conflict.pair.ComparePairType.MY_WITH_MERGED;
+import static org.openstreetmap.josm.gui.conflict.pair.ComparePairType.MY_WITH_THEIR;
+import static org.openstreetmap.josm.gui.conflict.pair.ComparePairType.THEIR_WITH_MERGED;
+import static org.openstreetmap.josm.gui.conflict.pair.ListRole.MERGED_ENTRIES;
+import static org.openstreetmap.josm.gui.conflict.pair.ListRole.MY_ENTRIES;
+import static org.openstreetmap.josm.gui.conflict.pair.ListRole.THEIR_ENTRIES;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.swing.AbstractListModel;
+import javax.swing.ComboBoxModel;
+import javax.swing.DefaultListSelectionModel;
+import javax.swing.JOptionPane;
+import javax.swing.JTable;
+import javax.swing.ListSelectionModel;
+import javax.swing.table.DefaultTableModel;
+import javax.swing.table.TableModel;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.command.conflict.ConflictResolveCommand;
+import org.openstreetmap.josm.data.conflict.Conflict;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.PrimitiveId;
+import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.gui.HelpAwareOptionPane;
+import org.openstreetmap.josm.gui.help.HelpUtil;
+import org.openstreetmap.josm.gui.util.ChangeNotifier;
+import org.openstreetmap.josm.gui.widgets.OsmPrimitivesTableModel;
+import org.openstreetmap.josm.tools.CheckParameterUtil;
+import org.openstreetmap.josm.tools.Utils;
+
+/**
+ * ListMergeModel is a model for interactively comparing and merging two list of entries
+ * of type T. It maintains three lists of entries of type T:
+ * <ol>
+ *   <li>the list of <em>my</em> entries</li>
+ *   <li>the list of <em>their</em> entries</li>
+ *   <li>the list of <em>merged</em> entries</li>
+ * </ol>
+ *
+ * A ListMergeModel is a factory for three {@link TableModel}s and three {@link ListSelectionModel}s:
+ * <ol>
+ *   <li>the table model and the list selection for for a  {@link JTable} which shows my entries.
+ *    See {@link #getMyTableModel()} and {@link AbstractListMergeModel#getMySelectionModel()}</li>
+ *   <li>dito for their entries and merged entries</li>
+ * </ol>
+ *
+ * A ListMergeModel can be ''frozen''. If it's frozen, it doesn't accept additional merge
+ * decisions. {@link PropertyChangeListener}s can register for property value changes of
+ * {@link #FROZEN_PROP}.
+ *
+ * ListMergeModel is an abstract class. Three methods have to be implemented by subclasses:
+ * <ul>
+ *   <li>{@link AbstractListMergeModel#cloneEntryForMergedList} - clones an entry of type T</li>
+ *   <li>{@link AbstractListMergeModel#isEqualEntry} - checks whether two entries are equals </li>
+ *   <li>{@link AbstractListMergeModel#setValueAt(DefaultTableModel, Object, int, int)} - handles values edited in
+ *     a JTable, dispatched from {@link TableModel#setValueAt(Object, int, int)} </li>
+ * </ul>
+ * A ListMergeModel is used in combination with a {@link AbstractListMerger}.
+ *
+ * @param <T> the type of the list entries
+ * @param <C> the type of conflict resolution command
+ * @see AbstractListMerger
+ */
+public abstract class AbstractListMergeModel<T extends PrimitiveId, C extends ConflictResolveCommand> extends ChangeNotifier {
+    public static final String FROZEN_PROP = AbstractListMergeModel.class.getName() + ".frozen";
+
+    private static final int MAX_DELETED_PRIMITIVE_IN_DIALOG = 5;
+
+    protected Map<ListRole, ArrayList<T>> entries;
+
+    protected EntriesTableModel myEntriesTableModel;
+    protected EntriesTableModel theirEntriesTableModel;
+    protected EntriesTableModel mergedEntriesTableModel;
+
+    protected EntriesSelectionModel myEntriesSelectionModel;
+    protected EntriesSelectionModel theirEntriesSelectionModel;
+    protected EntriesSelectionModel mergedEntriesSelectionModel;
+
+    private final Set<PropertyChangeListener> listeners;
+    private boolean isFrozen;
+    private final ComparePairListModel comparePairListModel;
+
+    private DataSet myDataset;
+    private Map<PrimitiveId, PrimitiveId> mergedMap;
+
+    /**
+     * Creates a clone of an entry of type T suitable to be included in the
+     * list of merged entries
+     *
+     * @param entry the entry
+     * @return the cloned entry
+     */
+    protected abstract T cloneEntryForMergedList(T entry);
+
+    /**
+     * checks whether two entries are equal. This is not necessarily the same as
+     * e1.equals(e2).
+     *
+     * @param e1  the first entry
+     * @param e2  the second entry
+     * @return true, if the entries are equal, false otherwise.
+     */
+    public abstract boolean isEqualEntry(T e1, T e2);
+
+    /**
+     * Handles method dispatches from {@link TableModel#setValueAt(Object, int, int)}.
+     *
+     * @param model the table model
+     * @param value  the value to be set
+     * @param row  the row index
+     * @param col the column index
+     *
+     * @see TableModel#setValueAt(Object, int, int)
+     */
+    protected abstract void setValueAt(DefaultTableModel model, Object value, int row, int col);
+
+    /**
+     * Replies primitive from my dataset referenced by entry
+     * @param entry entry
+     * @return Primitive from my dataset referenced by entry
+     */
+    public OsmPrimitive getMyPrimitive(T entry) {
+        return getMyPrimitiveById(entry);
+    }
+
+    public final OsmPrimitive getMyPrimitiveById(PrimitiveId entry) {
+        OsmPrimitive result = myDataset.getPrimitiveById(entry);
+        if (result == null && mergedMap != null) {
+            PrimitiveId id = mergedMap.get(entry);
+            if (id == null && entry instanceof OsmPrimitive) {
+                id = mergedMap.get(((OsmPrimitive) entry).getPrimitiveId());
+            }
+            if (id != null) {
+                result = myDataset.getPrimitiveById(id);
+            }
+        }
+        return result;
+    }
+
+    protected void buildMyEntriesTableModel() {
+        myEntriesTableModel = new EntriesTableModel(MY_ENTRIES);
+    }
+
+    protected void buildTheirEntriesTableModel() {
+        theirEntriesTableModel = new EntriesTableModel(THEIR_ENTRIES);
+    }
+
+    protected void buildMergedEntriesTableModel() {
+        mergedEntriesTableModel = new EntriesTableModel(MERGED_ENTRIES);
+    }
+
+    protected List<T> getMergedEntries() {
+        return entries.get(MERGED_ENTRIES);
+    }
+
+    protected List<T> getMyEntries() {
+        return entries.get(MY_ENTRIES);
+    }
+
+    protected List<T> getTheirEntries() {
+        return entries.get(THEIR_ENTRIES);
+    }
+
+    public int getMyEntriesSize() {
+        return getMyEntries().size();
+    }
+
+    public int getMergedEntriesSize() {
+        return getMergedEntries().size();
+    }
+
+    public int getTheirEntriesSize() {
+        return getTheirEntries().size();
+    }
+
+    /**
+     * Constructs a new {@code ListMergeModel}.
+     */
+    public AbstractListMergeModel() {
+        entries = new EnumMap<>(ListRole.class);
+        for (ListRole role : ListRole.values()) {
+            entries.put(role, new ArrayList<T>());
+        }
+
+        buildMyEntriesTableModel();
+        buildTheirEntriesTableModel();
+        buildMergedEntriesTableModel();
+
+        myEntriesSelectionModel = new EntriesSelectionModel(entries.get(MY_ENTRIES));
+        theirEntriesSelectionModel = new EntriesSelectionModel(entries.get(THEIR_ENTRIES));
+        mergedEntriesSelectionModel = new EntriesSelectionModel(entries.get(MERGED_ENTRIES));
+
+        listeners = new HashSet<>();
+        comparePairListModel = new ComparePairListModel();
+
+        setFrozen(true);
+    }
+
+    public void addPropertyChangeListener(PropertyChangeListener listener) {
+        synchronized (listeners) {
+            if (listener != null && !listeners.contains(listener)) {
+                listeners.add(listener);
+            }
+        }
+    }
+
+    public void removePropertyChangeListener(PropertyChangeListener listener) {
+        synchronized (listeners) {
+            if (listener != null && listeners.contains(listener)) {
+                listeners.remove(listener);
+            }
+        }
+    }
+
+    protected void fireFrozenChanged(boolean oldValue, boolean newValue) {
+        synchronized (listeners) {
+            PropertyChangeEvent evt = new PropertyChangeEvent(this, FROZEN_PROP, oldValue, newValue);
+            listeners.forEach(listener -> listener.propertyChange(evt));
+            }
+        }
+
+    public final void setFrozen(boolean isFrozen) {
+        boolean oldValue = this.isFrozen;
+        this.isFrozen = isFrozen;
+        fireFrozenChanged(oldValue, this.isFrozen);
+    }
+
+    public final boolean isFrozen() {
+        return isFrozen;
+    }
+
+    public OsmPrimitivesTableModel getMyTableModel() {
+        return myEntriesTableModel;
+    }
+
+    public OsmPrimitivesTableModel getTheirTableModel() {
+        return theirEntriesTableModel;
+    }
+
+    public OsmPrimitivesTableModel getMergedTableModel() {
+        return mergedEntriesTableModel;
+    }
+
+    public EntriesSelectionModel getMySelectionModel() {
+        return myEntriesSelectionModel;
+    }
+
+    public EntriesSelectionModel getTheirSelectionModel() {
+        return theirEntriesSelectionModel;
+    }
+
+    public EntriesSelectionModel getMergedSelectionModel() {
+        return mergedEntriesSelectionModel;
+    }
+
+    protected void fireModelDataChanged() {
+        myEntriesTableModel.fireTableDataChanged();
+        theirEntriesTableModel.fireTableDataChanged();
+        mergedEntriesTableModel.fireTableDataChanged();
+        fireStateChanged();
+    }
+
+    protected void copyToTop(ListRole role, int ... rows) {
+        copy(role, rows, 0);
+        mergedEntriesSelectionModel.setSelectionInterval(0, rows.length -1);
+    }
+
+    /**
+     * Copies the nodes given by indices in rows from the list of my nodes to the
+     * list of merged nodes. Inserts the nodes at the top of the list of merged
+     * nodes.
+     *
+     * @param rows the indices
+     */
+    public void copyMyToTop(int ... rows) {
+        copyToTop(MY_ENTRIES, rows);
+    }
+
+    /**
+     * Copies the nodes given by indices in rows from the list of their nodes to the
+     * list of merged nodes. Inserts the nodes at the top of the list of merged
+     * nodes.
+     *
+     * @param rows the indices
+     */
+    public void copyTheirToTop(int ... rows) {
+        copyToTop(THEIR_ENTRIES, rows);
+    }
+
+    /**
+     * Copies the nodes given by indices in rows from the list of  nodes in source to the
+     * list of merged nodes. Inserts the nodes at the end of the list of merged
+     * nodes.
+     *
+     * @param source the list of nodes to copy from
+     * @param rows the indices
+     */
+
+    public void copyToEnd(ListRole source, int ... rows) {
+        copy(source, rows, getMergedEntriesSize());
+        mergedEntriesSelectionModel.setSelectionInterval(getMergedEntriesSize()-rows.length, getMergedEntriesSize() -1);
+
+    }
+
+    /**
+     * Copies the nodes given by indices in rows from the list of my nodes to the
+     * list of merged nodes. Inserts the nodes at the end of the list of merged
+     * nodes.
+     *
+     * @param rows the indices
+     */
+    public void copyMyToEnd(int ... rows) {
+        copyToEnd(MY_ENTRIES, rows);
+    }
+
+    /**
+     * Copies the nodes given by indices in rows from the list of their nodes to the
+     * list of merged nodes. Inserts the nodes at the end of the list of merged
+     * nodes.
+     *
+     * @param rows the indices
+     */
+    public void copyTheirToEnd(int ... rows) {
+        copyToEnd(THEIR_ENTRIES, rows);
+    }
+
+    public void clearMerged() {
+        getMergedEntries().clear();
+        fireModelDataChanged();
+    }
+
+    protected final void initPopulate(OsmPrimitive my, OsmPrimitive their, Map<PrimitiveId, PrimitiveId> mergedMap) {
+        CheckParameterUtil.ensureParameterNotNull(my, "my");
+        CheckParameterUtil.ensureParameterNotNull(their, "their");
+        this.myDataset = my.getDataSet();
+        this.mergedMap = mergedMap;
+        getMergedEntries().clear();
+        getMyEntries().clear();
+        getTheirEntries().clear();
+    }
+
+    protected void alertCopyFailedForDeletedPrimitives(List<PrimitiveId> deletedIds) {
+        List<String> items = new ArrayList<>();
+        for (int i = 0; i < Math.min(MAX_DELETED_PRIMITIVE_IN_DIALOG, deletedIds.size()); i++) {
+            items.add(deletedIds.get(i).toString());
+        }
+        if (deletedIds.size() > MAX_DELETED_PRIMITIVE_IN_DIALOG) {
+            items.add(tr("{0} more...", deletedIds.size() - MAX_DELETED_PRIMITIVE_IN_DIALOG));
+        }
+        StringBuilder sb = new StringBuilder();
+        sb.append("<html>")
+          .append(tr("The following objects could not be copied to the target object<br>because they are deleted in the target dataset:"))
+          .append(Utils.joinAsHtmlUnorderedList(items))
+          .append("</html>");
+        HelpAwareOptionPane.showOptionDialog(
+                Main.parent,
+                sb.toString(),
+                tr("Merging deleted objects failed"),
+                JOptionPane.WARNING_MESSAGE,
+                HelpUtil.ht("/Dialog/Conflict#MergingDeletedPrimitivesFailed")
+        );
+    }
+
+    private void copy(ListRole sourceRole, int[] rows, int position) {
+        if (position < 0 || position > getMergedEntriesSize())
+            throw new IllegalArgumentException("Position must be between 0 and "+getMergedEntriesSize()+" but is "+position);
+        List<T> newItems = new ArrayList<>(rows.length);
+        List<T> source = entries.get(sourceRole);
+        List<PrimitiveId> deletedIds = new ArrayList<>();
+        for (int row: rows) {
+            T entry = source.get(row);
+            OsmPrimitive primitive = getMyPrimitive(entry);
+            if (!primitive.isDeleted()) {
+                T clone = cloneEntryForMergedList(entry);
+                newItems.add(clone);
+            } else {
+                deletedIds.add(primitive.getPrimitiveId());
+            }
+        }
+        getMergedEntries().addAll(position, newItems);
+        fireModelDataChanged();
+        if (!deletedIds.isEmpty()) {
+            alertCopyFailedForDeletedPrimitives(deletedIds);
+        }
+    }
+
+    public void copyAll(ListRole source) {
+        getMergedEntries().clear();
+
+        int[] rows = new int[entries.get(source).size()];
+        for (int i = 0; i < rows.length; i++) {
+            rows[i] = i;
+        }
+        copy(source, rows, 0);
+    }
+
+    /**
+     * Copies the nodes given by indices in rows from the list of  nodes <code>source</code> to the
+     * list of merged nodes. Inserts the nodes before row given by current.
+     *
+     * @param source the list of nodes to copy from
+     * @param rows the indices
+     * @param current the row index before which the nodes are inserted
+     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
+     */
+    protected void copyBeforeCurrent(ListRole source, int[] rows, int current) {
+        copy(source, rows, current);
+        mergedEntriesSelectionModel.setSelectionInterval(current, current + rows.length-1);
+    }
+
+    /**
+     * Copies the nodes given by indices in rows from the list of my nodes to the
+     * list of merged nodes. Inserts the nodes before row given by current.
+     *
+     * @param rows the indices
+     * @param current the row index before which the nodes are inserted
+     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
+     */
+    public void copyMyBeforeCurrent(int[] rows, int current) {
+        copyBeforeCurrent(MY_ENTRIES, rows, current);
+    }
+
+    /**
+     * Copies the nodes given by indices in rows from the list of their nodes to the
+     * list of merged nodes. Inserts the nodes before row given by current.
+     *
+     * @param rows the indices
+     * @param current the row index before which the nodes are inserted
+     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
+     */
+    public void copyTheirBeforeCurrent(int[] rows, int current) {
+        copyBeforeCurrent(THEIR_ENTRIES, rows, current);
+    }
+
+    /**
+     * Copies the nodes given by indices in rows from the list of  nodes <code>source</code> to the
+     * list of merged nodes. Inserts the nodes after the row given by current.
+     *
+     * @param source the list of nodes to copy from
+     * @param rows the indices
+     * @param current the row index after which the nodes are inserted
+     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
+     */
+    protected void copyAfterCurrent(ListRole source, int[] rows, int current) {
+        copy(source, rows, current + 1);
+        mergedEntriesSelectionModel.setSelectionInterval(current+1, current + rows.length-1);
+        fireStateChanged();
+    }
+
+    /**
+     * Copies the nodes given by indices in rows from the list of my nodes to the
+     * list of merged nodes. Inserts the nodes after the row given by current.
+     *
+     * @param rows the indices
+     * @param current the row index after which the nodes are inserted
+     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
+     */
+    public void copyMyAfterCurrent(int[] rows, int current) {
+        copyAfterCurrent(MY_ENTRIES, rows, current);
+    }
+
+    /**
+     * Copies the nodes given by indices in rows from the list of my nodes to the
+     * list of merged nodes. Inserts the nodes after the row given by current.
+     *
+     * @param rows the indices
+     * @param current the row index after which the nodes are inserted
+     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
+     */
+    public void copyTheirAfterCurrent(int[] rows, int current) {
+        copyAfterCurrent(THEIR_ENTRIES, rows, current);
+    }
+
+    /**
+     * Moves the nodes given by indices in rows  up by one position in the list
+     * of merged nodes.
+     *
+     * @param rows the indices
+     *
+     */
+    public void moveUpMerged(int ... rows) {
+        if (rows == null || rows.length == 0)
+            return;
+        if (rows[0] == 0)
+            // can't move up
+            return;
+        List<T> mergedEntries = getMergedEntries();
+        for (int row: rows) {
+            T n = mergedEntries.get(row);
+            mergedEntries.remove(row);
+            mergedEntries.add(row -1, n);
+        }
+        fireModelDataChanged();
+        mergedEntriesSelectionModel.clearSelection();
+        for (int row: rows) {
+            mergedEntriesSelectionModel.addSelectionInterval(row-1, row-1);
+        }
+    }
+
+    /**
+     * Moves the nodes given by indices in rows down by one position in the list
+     * of merged nodes.
+     *
+     * @param rows the indices
+     */
+    public void moveDownMerged(int ... rows) {
+        if (rows == null || rows.length == 0)
+            return;
+        List<T> mergedEntries = getMergedEntries();
+        if (rows[rows.length -1] == mergedEntries.size() -1)
+            // can't move down
+            return;
+        for (int i = rows.length-1; i >= 0; i--) {
+            int row = rows[i];
+            T n = mergedEntries.get(row);
+            mergedEntries.remove(row);
+            mergedEntries.add(row +1, n);
+        }
+        fireModelDataChanged();
+        mergedEntriesSelectionModel.clearSelection();
+        for (int row: rows) {
+            mergedEntriesSelectionModel.addSelectionInterval(row+1, row+1);
+        }
+    }
+
+    /**
+     * Removes the nodes given by indices in rows from the list
+     * of merged nodes.
+     *
+     * @param rows the indices
+     */
+    public void removeMerged(int ... rows) {
+        if (rows == null || rows.length == 0)
+            return;
+
+        List<T> mergedEntries = getMergedEntries();
+
+        for (int i = rows.length-1; i >= 0; i--) {
+            mergedEntries.remove(rows[i]);
+        }
+        fireModelDataChanged();
+        mergedEntriesSelectionModel.clearSelection();
+    }
+
+    /**
+     * Replies true if the list of my entries and the list of their
+     * entries are equal
+     *
+     * @return true, if the lists are equal; false otherwise
+     */
+    protected boolean myAndTheirEntriesEqual() {
+
+        if (getMyEntriesSize() != getTheirEntriesSize())
+            return false;
+        for (int i = 0; i < getMyEntriesSize(); i++) {
+            if (!isEqualEntry(getMyEntries().get(i), getTheirEntries().get(i)))
+                return false;
+        }
+        return true;
+    }
+
+    /**
+     * This an adapter between a {@link JTable} and one of the three entry lists
+     * in the role {@link ListRole} managed by the {@link AbstractListMergeModel}.
+     *
+     * From the point of view of the {@link JTable} it is a {@link TableModel}.
+     *
+     * @see AbstractListMergeModel#getMyTableModel()
+     * @see AbstractListMergeModel#getTheirTableModel()
+     * @see AbstractListMergeModel#getMergedTableModel()
+     */
+    public class EntriesTableModel extends DefaultTableModel implements OsmPrimitivesTableModel {
+        private final ListRole role;
+
+        /**
+         *
+         * @param role the role
+         */
+        public EntriesTableModel(ListRole role) {
+            this.role = role;
+        }
+
+        @Override
+        public int getRowCount() {
+            int count = Math.max(getMyEntries().size(), getMergedEntries().size());
+            return Math.max(count, getTheirEntries().size());
+        }
+
+        @Override
+        public Object getValueAt(int row, int column) {
+            if (row < entries.get(role).size())
+                return entries.get(role).get(row);
+            return null;
+        }
+
+        @Override
+        public boolean isCellEditable(int row, int column) {
+            return false;
+        }
+
+        @Override
+        public void setValueAt(Object value, int row, int col) {
+            AbstractListMergeModel.this.setValueAt(this, value, row, col);
+        }
+
+        /**
+         * Returns the list merge model.
+         * @return the list merge model
+         */
+        public AbstractListMergeModel<T, C> getListMergeModel() {
+            return AbstractListMergeModel.this;
+        }
+
+        /**
+         * replies true if the {@link ListRole} of this {@link EntriesTableModel}
+         * participates in the current {@link ComparePairType}
+         *
+         * @return true, if the if the {@link ListRole} of this {@link EntriesTableModel}
+         * participates in the current {@link ComparePairType}
+         *
+         * @see AbstractListMergeModel.ComparePairListModel#getSelectedComparePair()
+         */
+        public boolean isParticipatingInCurrentComparePair() {
+            return getComparePairListModel()
+            .getSelectedComparePair()
+            .isParticipatingIn(role);
+        }
+
+        /**
+         * replies true if the entry at <code>row</code> is equal to the entry at the
+         * same position in the opposite list of the current {@link ComparePairType}.
+         *
+         * @param row  the row number
+         * @return true if the entry at <code>row</code> is equal to the entry at the
+         * same position in the opposite list of the current {@link ComparePairType}
+         * @throws IllegalStateException if this model is not participating in the
+         *   current  {@link ComparePairType}
+         * @see ComparePairType#getOppositeRole(ListRole)
+         * @see #getRole()
+         * @see #getOppositeEntries()
+         */
+        public boolean isSamePositionInOppositeList(int row) {
+            if (!isParticipatingInCurrentComparePair())
+                throw new IllegalStateException(tr("List in role {0} is currently not participating in a compare pair.", role.toString()));
+            if (row >= getEntries().size()) return false;
+            if (row >= getOppositeEntries().size()) return false;
+
+            T e1 = getEntries().get(row);
+            T e2 = getOppositeEntries().get(row);
+            return isEqualEntry(e1, e2);
+        }
+
+        /**
+         * replies true if the entry at the current position is present in the opposite list
+         * of the current {@link ComparePairType}.
+         *
+         * @param row the current row
+         * @return true if the entry at the current position is present in the opposite list
+         * of the current {@link ComparePairType}.
+         * @throws IllegalStateException if this model is not participating in the
+         *   current {@link ComparePairType}
+         * @see ComparePairType#getOppositeRole(ListRole)
+         * @see #getRole()
+         * @see #getOppositeEntries()
+         */
+        public boolean isIncludedInOppositeList(int row) {
+            if (!isParticipatingInCurrentComparePair())
+                throw new IllegalStateException(tr("List in role {0} is currently not participating in a compare pair.", role.toString()));
+
+            if (row >= getEntries().size()) return false;
+            T e1 = getEntries().get(row);
+            return getOppositeEntries().stream().anyMatch(e2 -> isEqualEntry(e1, e2));
+            }
+
+        protected List<T> getEntries() {
+            return entries.get(role);
+        }
+
+        /**
+         * replies the opposite list of entries with respect to the current {@link ComparePairType}
+         *
+         * @return the opposite list of entries
+         */
+        protected List<T> getOppositeEntries() {
+            ListRole opposite = getComparePairListModel().getSelectedComparePair().getOppositeRole(role);
+            return entries.get(opposite);
+        }
+
+        public ListRole getRole() {
+            return role;
+        }
+
+        @Override
+        public OsmPrimitive getReferredPrimitive(int idx) {
+            Object value = getValueAt(idx, 1);
+            if (value instanceof OsmPrimitive) {
+                return (OsmPrimitive) value;
+            } else if (value instanceof RelationMember) {
+                return ((RelationMember) value).getMember();
+            } else {
+                Main.error("Unknown object type: "+value);
+                return null;
+            }
+        }
+    }
+
+    /**
+     * This is the selection model to be used in a {@link JTable} which displays
+     * an entry list managed by {@link AbstractListMergeModel}.
+     *
+     * The model ensures that only rows displaying an entry in the entry list
+     * can be selected. "Empty" rows can't be selected.
+     *
+     * @see AbstractListMergeModel#getMySelectionModel()
+     * @see AbstractListMergeModel#getMergedSelectionModel()
+     * @see AbstractListMergeModel#getTheirSelectionModel()
+     *
+     */
+    protected class EntriesSelectionModel extends DefaultListSelectionModel {
+        private final transient List<T> entries;
+
+        public EntriesSelectionModel(List<T> nodes) {
+            this.entries = nodes;
+        }
+
+        @Override
+        public void addSelectionInterval(int index0, int index1) {
+            if (entries.isEmpty()) return;
+            if (index0 > entries.size() - 1) return;
+            index0 = Math.min(entries.size()-1, index0);
+            index1 = Math.min(entries.size()-1, index1);
+            super.addSelectionInterval(index0, index1);
+        }
+
+        @Override
+        public void insertIndexInterval(int index, int length, boolean before) {
+            if (entries.isEmpty()) return;
+            if (before) {
+                int newindex = Math.min(entries.size()-1, index);
+                if (newindex < index - length) return;
+                length = length - (index - newindex);
+                super.insertIndexInterval(newindex, length, before);
+            } else {
+                if (index > entries.size() -1) return;
+                length = Math.min(entries.size()-1 - index, length);
+                super.insertIndexInterval(index, length, before);
+            }
+        }
+
+        @Override
+        public void moveLeadSelectionIndex(int leadIndex) {
+            if (entries.isEmpty()) return;
+            leadIndex = Math.max(0, leadIndex);
+            leadIndex = Math.min(entries.size() - 1, leadIndex);
+            super.moveLeadSelectionIndex(leadIndex);
+        }
+
+        @Override
+        public void removeIndexInterval(int index0, int index1) {
+            if (entries.isEmpty()) return;
+            index0 = Math.max(0, index0);
+            index0 = Math.min(entries.size() - 1, index0);
+
+            index1 = Math.max(0, index1);
+            index1 = Math.min(entries.size() - 1, index1);
+            super.removeIndexInterval(index0, index1);
+        }
+
+        @Override
+        public void removeSelectionInterval(int index0, int index1) {
+            if (entries.isEmpty()) return;
+            index0 = Math.max(0, index0);
+            index0 = Math.min(entries.size() - 1, index0);
+
+            index1 = Math.max(0, index1);
+            index1 = Math.min(entries.size() - 1, index1);
+            super.removeSelectionInterval(index0, index1);
+        }
+
+        @Override
+        public void setAnchorSelectionIndex(int anchorIndex) {
+            if (entries.isEmpty()) return;
+            anchorIndex = Math.min(entries.size() - 1, anchorIndex);
+            super.setAnchorSelectionIndex(anchorIndex);
+        }
+
+        @Override
+        public void setLeadSelectionIndex(int leadIndex) {
+            if (entries.isEmpty()) return;
+            leadIndex = Math.min(entries.size() - 1, leadIndex);
+            super.setLeadSelectionIndex(leadIndex);
+        }
+
+        @Override
+        public void setSelectionInterval(int index0, int index1) {
+            if (entries.isEmpty()) return;
+            index0 = Math.max(0, index0);
+            index0 = Math.min(entries.size() - 1, index0);
+
+            index1 = Math.max(0, index1);
+            index1 = Math.min(entries.size() - 1, index1);
+
+            super.setSelectionInterval(index0, index1);
+        }
+    }
+
+    public ComparePairListModel getComparePairListModel() {
+        return this.comparePairListModel;
+    }
+
+    public class ComparePairListModel extends AbstractListModel<ComparePairType> implements ComboBoxModel<ComparePairType> {
+
+        private int selectedIdx;
+        private final List<ComparePairType> compareModes;
+
+        /**
+         * Constructs a new {@code ComparePairListModel}.
+         */
+        public ComparePairListModel() {
+            this.compareModes = new ArrayList<>();
+            compareModes.add(MY_WITH_THEIR);
+            compareModes.add(MY_WITH_MERGED);
+            compareModes.add(THEIR_WITH_MERGED);
+            selectedIdx = 0;
+        }
+
+        @Override
+        public ComparePairType getElementAt(int index) {
+            if (index < compareModes.size())
+                return compareModes.get(index);
+            throw new IllegalArgumentException(tr("Unexpected value of parameter ''index''. Got {0}.", index));
+        }
+
+        @Override
+        public int getSize() {
+            return compareModes.size();
+        }
+
+        @Override
+        public Object getSelectedItem() {
+            return compareModes.get(selectedIdx);
+        }
+
+        @Override
+        public void setSelectedItem(Object anItem) {
+            int i = compareModes.indexOf(anItem);
+            if (i < 0)
+                throw new IllegalStateException(tr("Item {0} not found in list.", anItem));
+            selectedIdx = i;
+            fireModelDataChanged();
+        }
+
+        public ComparePairType getSelectedComparePair() {
+            return compareModes.get(selectedIdx);
+        }
+    }
+
+    /**
+     * Builds the command to resolve conflicts in the list.
+     *
+     * @param conflict the conflict data set
+     * @return the command
+     * @throws IllegalStateException if the merge is not yet frozen
+     */
+    public abstract C buildResolveCommand(Conflict<? extends OsmPrimitive> conflict);
+}
Index: /trunk/src/org/openstreetmap/josm/gui/conflict/pair/AbstractListMerger.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/conflict/pair/AbstractListMerger.java	(revision 11330)
+++ /trunk/src/org/openstreetmap/josm/gui/conflict/pair/AbstractListMerger.java	(revision 11330)
@@ -0,0 +1,944 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.conflict.pair;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+import static org.openstreetmap.josm.tools.I18n.trn;
+
+import java.awt.FlowLayout;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.Collection;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.JToggleButton;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.command.conflict.ConflictResolveCommand;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.PrimitiveId;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.util.AdjustmentSynchronizer;
+import org.openstreetmap.josm.gui.widgets.JosmComboBox;
+import org.openstreetmap.josm.gui.widgets.OsmPrimitivesTable;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * A UI component for resolving conflicts in two lists of entries of type T.
+ *
+ * @param <T> the type of the entries
+ * @param <C> the type of conflict resolution command
+ * @see AbstractListMergeModel
+ * @since 1631
+ */
+public abstract class AbstractListMerger<T extends PrimitiveId, C extends ConflictResolveCommand> extends JPanel
+implements PropertyChangeListener, ChangeListener, IConflictResolver {
+    protected OsmPrimitivesTable myEntriesTable;
+    protected OsmPrimitivesTable mergedEntriesTable;
+    protected OsmPrimitivesTable theirEntriesTable;
+
+    protected transient AbstractListMergeModel<T, C> model;
+
+    private CopyStartLeftAction copyStartLeftAction;
+    private CopyBeforeCurrentLeftAction copyBeforeCurrentLeftAction;
+    private CopyAfterCurrentLeftAction copyAfterCurrentLeftAction;
+    private CopyEndLeftAction copyEndLeftAction;
+    private CopyAllLeft copyAllLeft;
+
+    private CopyStartRightAction copyStartRightAction;
+    private CopyBeforeCurrentRightAction copyBeforeCurrentRightAction;
+    private CopyAfterCurrentRightAction copyAfterCurrentRightAction;
+    private CopyEndRightAction copyEndRightAction;
+    private CopyAllRight copyAllRight;
+
+    private MoveUpMergedAction moveUpMergedAction;
+    private MoveDownMergedAction moveDownMergedAction;
+    private RemoveMergedAction removeMergedAction;
+    private FreezeAction freezeAction;
+
+    private transient AdjustmentSynchronizer adjustmentSynchronizer;
+
+    private JLabel lblMyVersion;
+    private JLabel lblMergedVersion;
+    private JLabel lblTheirVersion;
+
+    private JLabel lblFrozenState;
+
+    protected abstract JScrollPane buildMyElementsTable();
+
+    protected abstract JScrollPane buildMergedElementsTable();
+
+    protected abstract JScrollPane buildTheirElementsTable();
+
+    protected JScrollPane embeddInScrollPane(JTable table) {
+        JScrollPane pane = new JScrollPane(table);
+        if (adjustmentSynchronizer == null) {
+            adjustmentSynchronizer = new AdjustmentSynchronizer();
+        }
+        return pane;
+    }
+
+    protected void wireActionsToSelectionModels() {
+        myEntriesTable.getSelectionModel().addListSelectionListener(copyStartLeftAction);
+
+        myEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentLeftAction);
+        mergedEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentLeftAction);
+
+        myEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentLeftAction);
+        mergedEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentLeftAction);
+
+        myEntriesTable.getSelectionModel().addListSelectionListener(copyEndLeftAction);
+
+        theirEntriesTable.getSelectionModel().addListSelectionListener(copyStartRightAction);
+
+        theirEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentRightAction);
+        mergedEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentRightAction);
+
+        theirEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentRightAction);
+        mergedEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentRightAction);
+
+        theirEntriesTable.getSelectionModel().addListSelectionListener(copyEndRightAction);
+
+        mergedEntriesTable.getSelectionModel().addListSelectionListener(moveUpMergedAction);
+        mergedEntriesTable.getSelectionModel().addListSelectionListener(moveDownMergedAction);
+        mergedEntriesTable.getSelectionModel().addListSelectionListener(removeMergedAction);
+
+        model.addChangeListener(copyAllLeft);
+        model.addChangeListener(copyAllRight);
+        model.addPropertyChangeListener(copyAllLeft);
+        model.addPropertyChangeListener(copyAllRight);
+    }
+
+    protected JPanel buildLeftButtonPanel() {
+        JPanel pnl = new JPanel(new GridBagLayout());
+        GridBagConstraints gc = new GridBagConstraints();
+
+        gc.gridx = 0;
+        gc.gridy = 0;
+        copyStartLeftAction = new CopyStartLeftAction();
+        JButton btn = new JButton(copyStartLeftAction);
+        btn.setName("button.copystartleft");
+        pnl.add(btn, gc);
+
+        gc.gridx = 0;
+        gc.gridy = 1;
+        copyBeforeCurrentLeftAction = new CopyBeforeCurrentLeftAction();
+        btn = new JButton(copyBeforeCurrentLeftAction);
+        btn.setName("button.copybeforecurrentleft");
+        pnl.add(btn, gc);
+
+        gc.gridx = 0;
+        gc.gridy = 2;
+        copyAfterCurrentLeftAction = new CopyAfterCurrentLeftAction();
+        btn = new JButton(copyAfterCurrentLeftAction);
+        btn.setName("button.copyaftercurrentleft");
+        pnl.add(btn, gc);
+
+        gc.gridx = 0;
+        gc.gridy = 3;
+        copyEndLeftAction = new CopyEndLeftAction();
+        btn = new JButton(copyEndLeftAction);
+        btn.setName("button.copyendleft");
+        pnl.add(btn, gc);
+
+        gc.gridx = 0;
+        gc.gridy = 4;
+        copyAllLeft = new CopyAllLeft();
+        btn = new JButton(copyAllLeft);
+        btn.setName("button.copyallleft");
+        pnl.add(btn, gc);
+
+        return pnl;
+    }
+
+    protected JPanel buildRightButtonPanel() {
+        JPanel pnl = new JPanel(new GridBagLayout());
+        GridBagConstraints gc = new GridBagConstraints();
+
+        gc.gridx = 0;
+        gc.gridy = 0;
+        copyStartRightAction = new CopyStartRightAction();
+        pnl.add(new JButton(copyStartRightAction), gc);
+
+        gc.gridx = 0;
+        gc.gridy = 1;
+        copyBeforeCurrentRightAction = new CopyBeforeCurrentRightAction();
+        pnl.add(new JButton(copyBeforeCurrentRightAction), gc);
+
+        gc.gridx = 0;
+        gc.gridy = 2;
+        copyAfterCurrentRightAction = new CopyAfterCurrentRightAction();
+        pnl.add(new JButton(copyAfterCurrentRightAction), gc);
+
+        gc.gridx = 0;
+        gc.gridy = 3;
+        copyEndRightAction = new CopyEndRightAction();
+        pnl.add(new JButton(copyEndRightAction), gc);
+
+        gc.gridx = 0;
+        gc.gridy = 4;
+        copyAllRight = new CopyAllRight();
+        pnl.add(new JButton(copyAllRight), gc);
+
+        return pnl;
+    }
+
+    protected JPanel buildMergedListControlButtons() {
+        JPanel pnl = new JPanel(new GridBagLayout());
+        GridBagConstraints gc = new GridBagConstraints();
+
+        gc.gridx = 0;
+        gc.gridy = 0;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.HORIZONTAL;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.3;
+        gc.weighty = 0.0;
+        moveUpMergedAction = new MoveUpMergedAction();
+        pnl.add(new JButton(moveUpMergedAction), gc);
+
+        gc.gridx = 1;
+        gc.gridy = 0;
+        moveDownMergedAction = new MoveDownMergedAction();
+        pnl.add(new JButton(moveDownMergedAction), gc);
+
+        gc.gridx = 2;
+        gc.gridy = 0;
+        removeMergedAction = new RemoveMergedAction();
+        pnl.add(new JButton(removeMergedAction), gc);
+
+        return pnl;
+    }
+
+    protected JPanel buildAdjustmentLockControlPanel(JCheckBox cb) {
+        JPanel panel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
+        panel.add(new JLabel(tr("lock scrolling")));
+        panel.add(cb);
+        return panel;
+    }
+
+    protected JPanel buildComparePairSelectionPanel() {
+        JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT));
+        p.add(new JLabel(tr("Compare ")));
+        JosmComboBox<ComparePairType> cbComparePair = new JosmComboBox<>(model.getComparePairListModel());
+        cbComparePair.setRenderer(new ComparePairListCellRenderer());
+        p.add(cbComparePair);
+        return p;
+    }
+
+    protected JPanel buildFrozeStateControlPanel() {
+        JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT));
+        lblFrozenState = new JLabel();
+        p.add(lblFrozenState);
+        freezeAction = new FreezeAction();
+        JToggleButton btn = new JToggleButton(freezeAction);
+        freezeAction.adapt(btn);
+        btn.setName("button.freeze");
+        p.add(btn);
+
+        return p;
+    }
+
+    protected final void build() {
+        setLayout(new GridBagLayout());
+        GridBagConstraints gc = new GridBagConstraints();
+
+        // ------------------
+        gc.gridx = 0;
+        gc.gridy = 0;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.NONE;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        gc.insets = new Insets(10, 0, 0, 0);
+        lblMyVersion = new JLabel(tr("My version"));
+        lblMyVersion.setToolTipText(tr("List of elements in my dataset, i.e. the local dataset"));
+        add(lblMyVersion, gc);
+
+        gc.gridx = 2;
+        gc.gridy = 0;
+        lblMergedVersion = new JLabel(tr("Merged version"));
+        lblMergedVersion.setToolTipText(
+                tr("List of merged elements. They will replace the list of my elements when the merge decisions are applied."));
+        add(lblMergedVersion, gc);
+
+        gc.gridx = 4;
+        gc.gridy = 0;
+        lblTheirVersion = new JLabel(tr("Their version"));
+        lblTheirVersion.setToolTipText(tr("List of elements in their dataset, i.e. the server dataset"));
+        add(lblTheirVersion, gc);
+
+        // ------------------------------
+        gc.gridx = 0;
+        gc.gridy = 1;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.HORIZONTAL;
+        gc.anchor = GridBagConstraints.FIRST_LINE_START;
+        gc.weightx = 0.33;
+        gc.weighty = 0.0;
+        gc.insets = new Insets(0, 0, 0, 0);
+        JCheckBox cbLockMyScrolling = new JCheckBox();
+        cbLockMyScrolling.setName("checkbox.lockmyscrolling");
+        add(buildAdjustmentLockControlPanel(cbLockMyScrolling), gc);
+
+        gc.gridx = 2;
+        gc.gridy = 1;
+        JCheckBox cbLockMergedScrolling = new JCheckBox();
+        cbLockMergedScrolling.setName("checkbox.lockmergedscrolling");
+        add(buildAdjustmentLockControlPanel(cbLockMergedScrolling), gc);
+
+        gc.gridx = 4;
+        gc.gridy = 1;
+        JCheckBox cbLockTheirScrolling = new JCheckBox();
+        cbLockTheirScrolling.setName("checkbox.locktheirscrolling");
+        add(buildAdjustmentLockControlPanel(cbLockTheirScrolling), gc);
+
+        // --------------------------------
+        gc.gridx = 0;
+        gc.gridy = 2;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.anchor = GridBagConstraints.FIRST_LINE_START;
+        gc.weightx = 0.33;
+        gc.weighty = 1.0;
+        gc.insets = new Insets(0, 0, 0, 0);
+        JScrollPane pane = buildMyElementsTable();
+        lblMyVersion.setLabelFor(pane);
+        adjustmentSynchronizer.adapt(cbLockMyScrolling, pane.getVerticalScrollBar());
+        add(pane, gc);
+
+        gc.gridx = 1;
+        gc.gridy = 2;
+        gc.fill = GridBagConstraints.NONE;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        add(buildLeftButtonPanel(), gc);
+
+        gc.gridx = 2;
+        gc.gridy = 2;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.anchor = GridBagConstraints.FIRST_LINE_START;
+        gc.weightx = 0.33;
+        gc.weighty = 0.0;
+        pane = buildMergedElementsTable();
+        lblMergedVersion.setLabelFor(pane);
+        adjustmentSynchronizer.adapt(cbLockMergedScrolling, pane.getVerticalScrollBar());
+        add(pane, gc);
+
+        gc.gridx = 3;
+        gc.gridy = 2;
+        gc.fill = GridBagConstraints.NONE;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        add(buildRightButtonPanel(), gc);
+
+        gc.gridx = 4;
+        gc.gridy = 2;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.anchor = GridBagConstraints.FIRST_LINE_START;
+        gc.weightx = 0.33;
+        gc.weighty = 0.0;
+        pane = buildTheirElementsTable();
+        lblTheirVersion.setLabelFor(pane);
+        adjustmentSynchronizer.adapt(cbLockTheirScrolling, pane.getVerticalScrollBar());
+        add(pane, gc);
+
+        // ----------------------------------
+        gc.gridx = 2;
+        gc.gridy = 3;
+        gc.gridwidth = 1;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.anchor = GridBagConstraints.CENTER;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        add(buildMergedListControlButtons(), gc);
+
+        // -----------------------------------
+        gc.gridx = 0;
+        gc.gridy = 4;
+        gc.gridwidth = 2;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.HORIZONTAL;
+        gc.anchor = GridBagConstraints.LINE_START;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        add(buildComparePairSelectionPanel(), gc);
+
+        gc.gridx = 2;
+        gc.gridy = 4;
+        gc.gridwidth = 3;
+        gc.gridheight = 1;
+        gc.fill = GridBagConstraints.HORIZONTAL;
+        gc.anchor = GridBagConstraints.LINE_START;
+        gc.weightx = 0.0;
+        gc.weighty = 0.0;
+        add(buildFrozeStateControlPanel(), gc);
+
+        wireActionsToSelectionModels();
+    }
+
+    /**
+     * Constructs a new {@code ListMerger}.
+     * @param model list merger model
+     */
+    public AbstractListMerger(AbstractListMergeModel<T, C> model) {
+        this.model = model;
+        model.addChangeListener(this);
+        build();
+        model.addPropertyChangeListener(this);
+    }
+
+    /**
+     * Base class of all other Copy* inner classes.
+     */
+    abstract static class CopyAction extends AbstractAction implements ListSelectionListener {
+
+        protected CopyAction(String iconName, String actionName, String shortDescription) {
+            ImageIcon icon = ImageProvider.get("dialogs/conflict", iconName);
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, actionName);
+            }
+            putValue(Action.SHORT_DESCRIPTION, shortDescription);
+            setEnabled(false);
+        }
+    }
+
+    /**
+     * Action for copying selected nodes in the list of my nodes to the list of merged
+     * nodes. Inserts the nodes at the beginning of the list of merged nodes.
+     */
+    class CopyStartLeftAction extends CopyAction {
+
+        CopyStartLeftAction() {
+            super(/* ICON(dialogs/conflict/)*/ "copystartleft", tr("> top"),
+                tr("Copy my selected nodes to the start of the merged node list"));
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            model.copyMyToTop(myEntriesTable.getSelectedRows());
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {
+            setEnabled(!myEntriesTable.getSelectionModel().isSelectionEmpty());
+        }
+    }
+
+    /**
+     * Action for copying selected nodes in the list of my nodes to the list of merged
+     * nodes. Inserts the nodes at the end of the list of merged nodes.
+     */
+    class CopyEndLeftAction extends CopyAction {
+
+        CopyEndLeftAction() {
+            super(/* ICON(dialogs/conflict/)*/ "copyendleft", tr("> bottom"),
+                tr("Copy my selected elements to the end of the list of merged elements."));
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            model.copyMyToEnd(myEntriesTable.getSelectedRows());
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {
+            setEnabled(!myEntriesTable.getSelectionModel().isSelectionEmpty());
+        }
+    }
+
+    /**
+     * Action for copying selected nodes in the list of my nodes to the list of merged
+     * nodes. Inserts the nodes before the first selected row in the list of merged nodes.
+     */
+    class CopyBeforeCurrentLeftAction extends CopyAction {
+
+        CopyBeforeCurrentLeftAction() {
+            super(/* ICON(dialogs/conflict/)*/ "copybeforecurrentleft", tr("> before"),
+                    tr("Copy my selected elements before the first selected element in the list of merged elements."));
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            int[] mergedRows = mergedEntriesTable.getSelectedRows();
+            if (mergedRows == null || mergedRows.length == 0)
+                return;
+            int[] myRows = myEntriesTable.getSelectedRows();
+            int current = mergedRows[0];
+            model.copyMyBeforeCurrent(myRows, current);
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {
+            setEnabled(
+                    !myEntriesTable.getSelectionModel().isSelectionEmpty()
+                    && !mergedEntriesTable.getSelectionModel().isSelectionEmpty()
+            );
+        }
+    }
+
+    /**
+     * Action for copying selected nodes in the list of my nodes to the list of merged
+     * nodes. Inserts the nodes after the first selected row in the list of merged nodes.
+     */
+    class CopyAfterCurrentLeftAction extends CopyAction {
+
+        CopyAfterCurrentLeftAction() {
+            super(/* ICON(dialogs/conflict/)*/ "copyaftercurrentleft", tr("> after"),
+                    tr("Copy my selected elements after the first selected element in the list of merged elements."));
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            int[] mergedRows = mergedEntriesTable.getSelectedRows();
+            if (mergedRows == null || mergedRows.length == 0)
+                return;
+            int[] myRows = myEntriesTable.getSelectedRows();
+            int current = mergedRows[0];
+            model.copyMyAfterCurrent(myRows, current);
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {
+            setEnabled(
+                    !myEntriesTable.getSelectionModel().isSelectionEmpty()
+                    && !mergedEntriesTable.getSelectionModel().isSelectionEmpty()
+            );
+        }
+    }
+
+    class CopyStartRightAction extends CopyAction {
+
+        CopyStartRightAction() {
+            super(/* ICON(dialogs/conflict/)*/ "copystartright", tr("< top"),
+                tr("Copy their selected element to the start of the list of merged elements."));
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            model.copyTheirToTop(theirEntriesTable.getSelectedRows());
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {
+            setEnabled(!theirEntriesTable.getSelectionModel().isSelectionEmpty());
+        }
+    }
+
+    class CopyEndRightAction extends CopyAction {
+
+        CopyEndRightAction() {
+            super(/* ICON(dialogs/conflict/)*/ "copyendright", tr("< bottom"),
+                tr("Copy their selected elements to the end of the list of merged elements."));
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            model.copyTheirToEnd(theirEntriesTable.getSelectedRows());
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {
+            setEnabled(!theirEntriesTable.getSelectionModel().isSelectionEmpty());
+        }
+    }
+
+    class CopyBeforeCurrentRightAction extends CopyAction {
+
+        CopyBeforeCurrentRightAction() {
+            super(/* ICON(dialogs/conflict/)*/ "copybeforecurrentright", tr("< before"),
+                    tr("Copy their selected elements before the first selected element in the list of merged elements."));
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            int[] mergedRows = mergedEntriesTable.getSelectedRows();
+            if (mergedRows == null || mergedRows.length == 0)
+                return;
+            int[] myRows = theirEntriesTable.getSelectedRows();
+            int current = mergedRows[0];
+            model.copyTheirBeforeCurrent(myRows, current);
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {
+            setEnabled(
+                    !theirEntriesTable.getSelectionModel().isSelectionEmpty()
+                    && !mergedEntriesTable.getSelectionModel().isSelectionEmpty()
+            );
+        }
+    }
+
+    class CopyAfterCurrentRightAction extends CopyAction {
+
+        CopyAfterCurrentRightAction() {
+            super(/* ICON(dialogs/conflict/)*/ "copyaftercurrentright", tr("< after"),
+                    tr("Copy their selected element after the first selected element in the list of merged elements"));
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            int[] mergedRows = mergedEntriesTable.getSelectedRows();
+            if (mergedRows == null || mergedRows.length == 0)
+                return;
+            int[] myRows = theirEntriesTable.getSelectedRows();
+            int current = mergedRows[0];
+            model.copyTheirAfterCurrent(myRows, current);
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {
+            setEnabled(
+                    !theirEntriesTable.getSelectionModel().isSelectionEmpty()
+                    && !mergedEntriesTable.getSelectionModel().isSelectionEmpty()
+            );
+        }
+    }
+
+    class CopyAllLeft extends AbstractAction implements ChangeListener, PropertyChangeListener {
+
+        CopyAllLeft() {
+            ImageIcon icon = ImageProvider.get("dialogs/conflict", "useallleft");
+            putValue(Action.SMALL_ICON, icon);
+            putValue(Action.SHORT_DESCRIPTION, tr("Copy all my elements to the target"));
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            model.copyAll(ListRole.MY_ENTRIES);
+            model.setFrozen(true);
+        }
+
+        private void updateEnabledState() {
+            setEnabled(model.getMergedEntries().isEmpty() && !model.isFrozen());
+        }
+
+        @Override
+        public void stateChanged(ChangeEvent e) {
+            updateEnabledState();
+        }
+
+        @Override
+        public void propertyChange(PropertyChangeEvent evt) {
+            updateEnabledState();
+        }
+    }
+
+    class CopyAllRight extends AbstractAction implements ChangeListener, PropertyChangeListener {
+
+        CopyAllRight() {
+            ImageIcon icon = ImageProvider.get("dialogs/conflict", "useallright");
+            putValue(Action.SMALL_ICON, icon);
+            putValue(Action.SHORT_DESCRIPTION, tr("Copy all their elements to the target"));
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            model.copyAll(ListRole.THEIR_ENTRIES);
+            model.setFrozen(true);
+        }
+
+        private void updateEnabledState() {
+            setEnabled(model.getMergedEntries().isEmpty() && !model.isFrozen());
+        }
+
+        @Override
+        public void stateChanged(ChangeEvent e) {
+            updateEnabledState();
+        }
+
+        @Override
+        public void propertyChange(PropertyChangeEvent evt) {
+            updateEnabledState();
+        }
+    }
+
+    class MoveUpMergedAction extends AbstractAction implements ListSelectionListener {
+
+        MoveUpMergedAction() {
+            ImageIcon icon = ImageProvider.get("dialogs/conflict", "moveup");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, tr("Up"));
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Move up the selected entries by one position."));
+            setEnabled(false);
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            int[] rows = mergedEntriesTable.getSelectedRows();
+            model.moveUpMerged(rows);
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {
+            int[] rows = mergedEntriesTable.getSelectedRows();
+            setEnabled(
+                    rows != null
+                    && rows.length > 0
+                    && rows[0] != 0
+            );
+        }
+    }
+
+    /**
+     * Action for moving the currently selected entries in the list of merged entries
+     * one position down
+     *
+     */
+    class MoveDownMergedAction extends AbstractAction implements ListSelectionListener {
+
+        MoveDownMergedAction() {
+            ImageIcon icon = ImageProvider.get("dialogs/conflict", "movedown");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, tr("Down"));
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Move down the selected entries by one position."));
+            setEnabled(false);
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            int[] rows = mergedEntriesTable.getSelectedRows();
+            model.moveDownMerged(rows);
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {
+            int[] rows = mergedEntriesTable.getSelectedRows();
+            setEnabled(
+                    rows != null
+                    && rows.length > 0
+                    && rows[rows.length -1] != mergedEntriesTable.getRowCount() -1
+            );
+        }
+    }
+
+    /**
+     * Action for removing the selected entries in the list of merged entries
+     * from the list of merged entries.
+     *
+     */
+    class RemoveMergedAction extends AbstractAction implements ListSelectionListener {
+
+        RemoveMergedAction() {
+            ImageIcon icon = ImageProvider.get("dialogs/conflict", "remove");
+            putValue(Action.SMALL_ICON, icon);
+            if (icon == null) {
+                putValue(Action.NAME, tr("Remove"));
+            }
+            putValue(Action.SHORT_DESCRIPTION, tr("Remove the selected entries from the list of merged elements."));
+            setEnabled(false);
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            int[] rows = mergedEntriesTable.getSelectedRows();
+            model.removeMerged(rows);
+        }
+
+        @Override
+        public void valueChanged(ListSelectionEvent e) {
+            int[] rows = mergedEntriesTable.getSelectedRows();
+            setEnabled(
+                    rows != null
+                    && rows.length > 0
+            );
+        }
+    }
+
+    private interface FreezeActionProperties {
+        String PROP_SELECTED = FreezeActionProperties.class.getName() + ".selected";
+    }
+
+    /**
+     * Action for freezing the current state of the list merger
+     *
+     */
+    private final class FreezeAction extends AbstractAction implements ItemListener, FreezeActionProperties {
+
+        private FreezeAction() {
+            putValue(Action.NAME, tr("Freeze"));
+            putValue(Action.SHORT_DESCRIPTION, tr("Freeze the current list of merged elements."));
+            putValue(PROP_SELECTED, Boolean.FALSE);
+            setEnabled(true);
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            // do nothing
+        }
+
+        /**
+         * Java 1.5 doesn't known Action.SELECT_KEY. Wires a toggle button to this action
+         * such that the action gets notified about item state changes and the button gets
+         * notified about selection state changes of the action.
+         *
+         * @param btn a toggle button
+         */
+        public void adapt(final JToggleButton btn) {
+            btn.addItemListener(this);
+            addPropertyChangeListener(evt -> {
+                    if (evt.getPropertyName().equals(PROP_SELECTED)) {
+                        btn.setSelected((Boolean) evt.getNewValue());
+                    }
+                });
+        }
+
+        @Override
+        public void itemStateChanged(ItemEvent e) {
+            int state = e.getStateChange();
+            if (state == ItemEvent.SELECTED) {
+                putValue(Action.NAME, tr("Unfreeze"));
+                putValue(Action.SHORT_DESCRIPTION, tr("Unfreeze the list of merged elements and start merging."));
+                model.setFrozen(true);
+            } else if (state == ItemEvent.DESELECTED) {
+                putValue(Action.NAME, tr("Freeze"));
+                putValue(Action.SHORT_DESCRIPTION, tr("Freeze the current list of merged elements."));
+                model.setFrozen(false);
+            }
+            boolean isSelected = (Boolean) getValue(PROP_SELECTED);
+            if (isSelected != (e.getStateChange() == ItemEvent.SELECTED)) {
+                putValue(PROP_SELECTED, e.getStateChange() == ItemEvent.SELECTED);
+            }
+
+        }
+    }
+
+    protected void handlePropertyChangeFrozen(boolean oldValue, boolean newValue) {
+        myEntriesTable.getSelectionModel().clearSelection();
+        myEntriesTable.setEnabled(!newValue);
+        theirEntriesTable.getSelectionModel().clearSelection();
+        theirEntriesTable.setEnabled(!newValue);
+        mergedEntriesTable.getSelectionModel().clearSelection();
+        mergedEntriesTable.setEnabled(!newValue);
+        freezeAction.putValue(FreezeActionProperties.PROP_SELECTED, newValue);
+        if (newValue) {
+            lblFrozenState.setText(
+                    tr("<html>Click <strong>{0}</strong> to start merging my and their entries.</html>",
+                            freezeAction.getValue(Action.NAME))
+            );
+        } else {
+            lblFrozenState.setText(
+                    tr("<html>Click <strong>{0}</strong> to finish merging my and their entries.</html>",
+                            freezeAction.getValue(Action.NAME))
+            );
+        }
+    }
+
+    @Override
+    public void propertyChange(PropertyChangeEvent evt) {
+        if (evt.getPropertyName().equals(AbstractListMergeModel.FROZEN_PROP)) {
+            handlePropertyChangeFrozen((Boolean) evt.getOldValue(), (Boolean) evt.getNewValue());
+        }
+    }
+
+    /**
+     * Returns the model.
+     * @return the model
+     */
+    public AbstractListMergeModel<T, C> getModel() {
+        return model;
+    }
+
+    @Override
+    public void stateChanged(ChangeEvent e) {
+        lblMyVersion.setText(
+                trn("My version ({0} entry)", "My version ({0} entries)", model.getMyEntriesSize(), model.getMyEntriesSize())
+        );
+        lblMergedVersion.setText(
+                trn("Merged version ({0} entry)", "Merged version ({0} entries)", model.getMergedEntriesSize(), model.getMergedEntriesSize())
+        );
+        lblTheirVersion.setText(
+                trn("Their version ({0} entry)", "Their version ({0} entries)", model.getTheirEntriesSize(), model.getTheirEntriesSize())
+        );
+    }
+
+    /**
+     * Adds all registered listeners by this merger
+     * @see #unregisterListeners()
+     * @since 10454
+     */
+    public void registerListeners() {
+        myEntriesTable.registerListeners();
+        mergedEntriesTable.registerListeners();
+        theirEntriesTable.registerListeners();
+    }
+
+    /**
+     * Removes all registered listeners by this merger
+     * @since 10454
+     */
+    public void unregisterListeners() {
+        myEntriesTable.unregisterListeners();
+        mergedEntriesTable.unregisterListeners();
+        theirEntriesTable.unregisterListeners();
+    }
+
+    protected final <P extends OsmPrimitive> OsmDataLayer findLayerFor(P primitive) {
+        if (primitive != null) {
+            Iterable<OsmDataLayer> layers = Main.getLayerManager().getLayersOfType(OsmDataLayer.class);
+            // Find layer with same dataset
+            for (OsmDataLayer layer : layers) {
+                if (layer.data == primitive.getDataSet()) {
+                    return layer;
+                }
+            }
+            // Conflict after merging layers: a dataset could be no more in any layer, try to find another layer with same primitive
+            for (OsmDataLayer layer : layers) {
+                final Collection<? extends OsmPrimitive> collection;
+                if (primitive instanceof Way) {
+                    collection = layer.data.getWays();
+                } else if (primitive instanceof Relation) {
+                    collection = layer.data.getRelations();
+                } else {
+                    collection = layer.data.allPrimitives();
+                }
+                for (OsmPrimitive p : collection) {
+                    if (p.getPrimitiveId().equals(primitive.getPrimitiveId())) {
+                        return layer;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void decideRemaining(MergeDecisionType decision) {
+        if (!model.isFrozen()) {
+            model.copyAll(MergeDecisionType.KEEP_MINE.equals(decision) ? ListRole.MY_ENTRIES : ListRole.THEIR_ENTRIES);
+            model.setFrozen(true);
+        }
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/gui/conflict/pair/ConflictResolver.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/conflict/pair/ConflictResolver.java	(revision 11329)
+++ /trunk/src/org/openstreetmap/josm/gui/conflict/pair/ConflictResolver.java	(revision 11330)
@@ -163,5 +163,5 @@
      * @param evt the event
      * @see TagMergeModel
-     * @see ListMergeModel
+     * @see AbstractListMergeModel
      * @see PropertiesMergeModel
      */
@@ -181,5 +181,5 @@
             }
             updateResolvedCompletely();
-        } else if (evt.getPropertyName().equals(ListMergeModel.FROZEN_PROP)) {
+        } else if (evt.getPropertyName().equals(AbstractListMergeModel.FROZEN_PROP)) {
             boolean frozen = (Boolean) evt.getNewValue();
             if (evt.getSource() == nodeListMerger.getModel() && my instanceof Way) {
Index: unk/src/org/openstreetmap/josm/gui/conflict/pair/ListMergeModel.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/conflict/pair/ListMergeModel.java	(revision 11329)
+++ 	(revision )
@@ -1,879 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.conflict.pair;
-
-import static org.openstreetmap.josm.gui.conflict.pair.ComparePairType.MY_WITH_MERGED;
-import static org.openstreetmap.josm.gui.conflict.pair.ComparePairType.MY_WITH_THEIR;
-import static org.openstreetmap.josm.gui.conflict.pair.ComparePairType.THEIR_WITH_MERGED;
-import static org.openstreetmap.josm.gui.conflict.pair.ListRole.MERGED_ENTRIES;
-import static org.openstreetmap.josm.gui.conflict.pair.ListRole.MY_ENTRIES;
-import static org.openstreetmap.josm.gui.conflict.pair.ListRole.THEIR_ENTRIES;
-import static org.openstreetmap.josm.tools.I18n.tr;
-
-import java.beans.PropertyChangeEvent;
-import java.beans.PropertyChangeListener;
-import java.util.ArrayList;
-import java.util.EnumMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import javax.swing.AbstractListModel;
-import javax.swing.ComboBoxModel;
-import javax.swing.DefaultListSelectionModel;
-import javax.swing.JOptionPane;
-import javax.swing.JTable;
-import javax.swing.ListSelectionModel;
-import javax.swing.table.DefaultTableModel;
-import javax.swing.table.TableModel;
-
-import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.command.conflict.ConflictResolveCommand;
-import org.openstreetmap.josm.data.conflict.Conflict;
-import org.openstreetmap.josm.data.osm.DataSet;
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.PrimitiveId;
-import org.openstreetmap.josm.data.osm.RelationMember;
-import org.openstreetmap.josm.gui.HelpAwareOptionPane;
-import org.openstreetmap.josm.gui.help.HelpUtil;
-import org.openstreetmap.josm.gui.util.ChangeNotifier;
-import org.openstreetmap.josm.gui.widgets.OsmPrimitivesTableModel;
-import org.openstreetmap.josm.tools.CheckParameterUtil;
-import org.openstreetmap.josm.tools.Utils;
-
-/**
- * ListMergeModel is a model for interactively comparing and merging two list of entries
- * of type T. It maintains three lists of entries of type T:
- * <ol>
- *   <li>the list of <em>my</em> entries</li>
- *   <li>the list of <em>their</em> entries</li>
- *   <li>the list of <em>merged</em> entries</li>
- * </ol>
- *
- * A ListMergeModel is a factory for three {@link TableModel}s and three {@link ListSelectionModel}s:
- * <ol>
- *   <li>the table model and the list selection for for a  {@link JTable} which shows my entries.
- *    See {@link #getMyTableModel()} and {@link ListMergeModel#getMySelectionModel()}</li>
- *   <li>dito for their entries and merged entries</li>
- * </ol>
- *
- * A ListMergeModel can be ''frozen''. If it's frozen, it doesn't accept additional merge
- * decisions. {@link PropertyChangeListener}s can register for property value changes of
- * {@link #FROZEN_PROP}.
- *
- * ListMergeModel is an abstract class. Three methods have to be implemented by subclasses:
- * <ul>
- *   <li>{@link ListMergeModel#cloneEntryForMergedList} - clones an entry of type T</li>
- *   <li>{@link ListMergeModel#isEqualEntry} - checks whether two entries are equals </li>
- *   <li>{@link ListMergeModel#setValueAt(DefaultTableModel, Object, int, int)} - handles values edited in
- *     a JTable, dispatched from {@link TableModel#setValueAt(Object, int, int)} </li>
- * </ul>
- * A ListMergeModel is used in combination with a {@link ListMerger}.
- *
- * @param <T> the type of the list entries
- * @param <C> the type of conflict resolution command
- * @see ListMerger
- */
-public abstract class ListMergeModel<T extends PrimitiveId, C extends ConflictResolveCommand> extends ChangeNotifier {
-    public static final String FROZEN_PROP = ListMergeModel.class.getName() + ".frozen";
-
-    private static final int MAX_DELETED_PRIMITIVE_IN_DIALOG = 5;
-
-    protected Map<ListRole, ArrayList<T>> entries;
-
-    protected EntriesTableModel myEntriesTableModel;
-    protected EntriesTableModel theirEntriesTableModel;
-    protected EntriesTableModel mergedEntriesTableModel;
-
-    protected EntriesSelectionModel myEntriesSelectionModel;
-    protected EntriesSelectionModel theirEntriesSelectionModel;
-    protected EntriesSelectionModel mergedEntriesSelectionModel;
-
-    private final Set<PropertyChangeListener> listeners;
-    private boolean isFrozen;
-    private final ComparePairListModel comparePairListModel;
-
-    private DataSet myDataset;
-    private Map<PrimitiveId, PrimitiveId> mergedMap;
-
-    /**
-     * Creates a clone of an entry of type T suitable to be included in the
-     * list of merged entries
-     *
-     * @param entry the entry
-     * @return the cloned entry
-     */
-    protected abstract T cloneEntryForMergedList(T entry);
-
-    /**
-     * checks whether two entries are equal. This is not necessarily the same as
-     * e1.equals(e2).
-     *
-     * @param e1  the first entry
-     * @param e2  the second entry
-     * @return true, if the entries are equal, false otherwise.
-     */
-    public abstract boolean isEqualEntry(T e1, T e2);
-
-    /**
-     * Handles method dispatches from {@link TableModel#setValueAt(Object, int, int)}.
-     *
-     * @param model the table model
-     * @param value  the value to be set
-     * @param row  the row index
-     * @param col the column index
-     *
-     * @see TableModel#setValueAt(Object, int, int)
-     */
-    protected abstract void setValueAt(DefaultTableModel model, Object value, int row, int col);
-
-    /**
-     * Replies primitive from my dataset referenced by entry
-     * @param entry entry
-     * @return Primitive from my dataset referenced by entry
-     */
-    public OsmPrimitive getMyPrimitive(T entry) {
-        return getMyPrimitiveById(entry);
-    }
-
-    public final OsmPrimitive getMyPrimitiveById(PrimitiveId entry) {
-        OsmPrimitive result = myDataset.getPrimitiveById(entry);
-        if (result == null && mergedMap != null) {
-            PrimitiveId id = mergedMap.get(entry);
-            if (id == null && entry instanceof OsmPrimitive) {
-                id = mergedMap.get(((OsmPrimitive) entry).getPrimitiveId());
-            }
-            if (id != null) {
-                result = myDataset.getPrimitiveById(id);
-            }
-        }
-        return result;
-    }
-
-    protected void buildMyEntriesTableModel() {
-        myEntriesTableModel = new EntriesTableModel(MY_ENTRIES);
-    }
-
-    protected void buildTheirEntriesTableModel() {
-        theirEntriesTableModel = new EntriesTableModel(THEIR_ENTRIES);
-    }
-
-    protected void buildMergedEntriesTableModel() {
-        mergedEntriesTableModel = new EntriesTableModel(MERGED_ENTRIES);
-    }
-
-    protected List<T> getMergedEntries() {
-        return entries.get(MERGED_ENTRIES);
-    }
-
-    protected List<T> getMyEntries() {
-        return entries.get(MY_ENTRIES);
-    }
-
-    protected List<T> getTheirEntries() {
-        return entries.get(THEIR_ENTRIES);
-    }
-
-    public int getMyEntriesSize() {
-        return getMyEntries().size();
-    }
-
-    public int getMergedEntriesSize() {
-        return getMergedEntries().size();
-    }
-
-    public int getTheirEntriesSize() {
-        return getTheirEntries().size();
-    }
-
-    /**
-     * Constructs a new {@code ListMergeModel}.
-     */
-    public ListMergeModel() {
-        entries = new EnumMap<>(ListRole.class);
-        for (ListRole role : ListRole.values()) {
-            entries.put(role, new ArrayList<T>());
-        }
-
-        buildMyEntriesTableModel();
-        buildTheirEntriesTableModel();
-        buildMergedEntriesTableModel();
-
-        myEntriesSelectionModel = new EntriesSelectionModel(entries.get(MY_ENTRIES));
-        theirEntriesSelectionModel = new EntriesSelectionModel(entries.get(THEIR_ENTRIES));
-        mergedEntriesSelectionModel = new EntriesSelectionModel(entries.get(MERGED_ENTRIES));
-
-        listeners = new HashSet<>();
-        comparePairListModel = new ComparePairListModel();
-
-        setFrozen(true);
-    }
-
-    public void addPropertyChangeListener(PropertyChangeListener listener) {
-        synchronized (listeners) {
-            if (listener != null && !listeners.contains(listener)) {
-                listeners.add(listener);
-            }
-        }
-    }
-
-    public void removePropertyChangeListener(PropertyChangeListener listener) {
-        synchronized (listeners) {
-            if (listener != null && listeners.contains(listener)) {
-                listeners.remove(listener);
-            }
-        }
-    }
-
-    protected void fireFrozenChanged(boolean oldValue, boolean newValue) {
-        synchronized (listeners) {
-            PropertyChangeEvent evt = new PropertyChangeEvent(this, FROZEN_PROP, oldValue, newValue);
-            listeners.forEach(listener -> listener.propertyChange(evt));
-            }
-        }
-
-    public final void setFrozen(boolean isFrozen) {
-        boolean oldValue = this.isFrozen;
-        this.isFrozen = isFrozen;
-        fireFrozenChanged(oldValue, this.isFrozen);
-    }
-
-    public final boolean isFrozen() {
-        return isFrozen;
-    }
-
-    public OsmPrimitivesTableModel getMyTableModel() {
-        return myEntriesTableModel;
-    }
-
-    public OsmPrimitivesTableModel getTheirTableModel() {
-        return theirEntriesTableModel;
-    }
-
-    public OsmPrimitivesTableModel getMergedTableModel() {
-        return mergedEntriesTableModel;
-    }
-
-    public EntriesSelectionModel getMySelectionModel() {
-        return myEntriesSelectionModel;
-    }
-
-    public EntriesSelectionModel getTheirSelectionModel() {
-        return theirEntriesSelectionModel;
-    }
-
-    public EntriesSelectionModel getMergedSelectionModel() {
-        return mergedEntriesSelectionModel;
-    }
-
-    protected void fireModelDataChanged() {
-        myEntriesTableModel.fireTableDataChanged();
-        theirEntriesTableModel.fireTableDataChanged();
-        mergedEntriesTableModel.fireTableDataChanged();
-        fireStateChanged();
-    }
-
-    protected void copyToTop(ListRole role, int ... rows) {
-        copy(role, rows, 0);
-        mergedEntriesSelectionModel.setSelectionInterval(0, rows.length -1);
-    }
-
-    /**
-     * Copies the nodes given by indices in rows from the list of my nodes to the
-     * list of merged nodes. Inserts the nodes at the top of the list of merged
-     * nodes.
-     *
-     * @param rows the indices
-     */
-    public void copyMyToTop(int ... rows) {
-        copyToTop(MY_ENTRIES, rows);
-    }
-
-    /**
-     * Copies the nodes given by indices in rows from the list of their nodes to the
-     * list of merged nodes. Inserts the nodes at the top of the list of merged
-     * nodes.
-     *
-     * @param rows the indices
-     */
-    public void copyTheirToTop(int ... rows) {
-        copyToTop(THEIR_ENTRIES, rows);
-    }
-
-    /**
-     * Copies the nodes given by indices in rows from the list of  nodes in source to the
-     * list of merged nodes. Inserts the nodes at the end of the list of merged
-     * nodes.
-     *
-     * @param source the list of nodes to copy from
-     * @param rows the indices
-     */
-
-    public void copyToEnd(ListRole source, int ... rows) {
-        copy(source, rows, getMergedEntriesSize());
-        mergedEntriesSelectionModel.setSelectionInterval(getMergedEntriesSize()-rows.length, getMergedEntriesSize() -1);
-
-    }
-
-    /**
-     * Copies the nodes given by indices in rows from the list of my nodes to the
-     * list of merged nodes. Inserts the nodes at the end of the list of merged
-     * nodes.
-     *
-     * @param rows the indices
-     */
-    public void copyMyToEnd(int ... rows) {
-        copyToEnd(MY_ENTRIES, rows);
-    }
-
-    /**
-     * Copies the nodes given by indices in rows from the list of their nodes to the
-     * list of merged nodes. Inserts the nodes at the end of the list of merged
-     * nodes.
-     *
-     * @param rows the indices
-     */
-    public void copyTheirToEnd(int ... rows) {
-        copyToEnd(THEIR_ENTRIES, rows);
-    }
-
-    public void clearMerged() {
-        getMergedEntries().clear();
-        fireModelDataChanged();
-    }
-
-    protected final void initPopulate(OsmPrimitive my, OsmPrimitive their, Map<PrimitiveId, PrimitiveId> mergedMap) {
-        CheckParameterUtil.ensureParameterNotNull(my, "my");
-        CheckParameterUtil.ensureParameterNotNull(their, "their");
-        this.myDataset = my.getDataSet();
-        this.mergedMap = mergedMap;
-        getMergedEntries().clear();
-        getMyEntries().clear();
-        getTheirEntries().clear();
-    }
-
-    protected void alertCopyFailedForDeletedPrimitives(List<PrimitiveId> deletedIds) {
-        List<String> items = new ArrayList<>();
-        for (int i = 0; i < Math.min(MAX_DELETED_PRIMITIVE_IN_DIALOG, deletedIds.size()); i++) {
-            items.add(deletedIds.get(i).toString());
-        }
-        if (deletedIds.size() > MAX_DELETED_PRIMITIVE_IN_DIALOG) {
-            items.add(tr("{0} more...", deletedIds.size() - MAX_DELETED_PRIMITIVE_IN_DIALOG));
-        }
-        StringBuilder sb = new StringBuilder();
-        sb.append("<html>")
-          .append(tr("The following objects could not be copied to the target object<br>because they are deleted in the target dataset:"))
-          .append(Utils.joinAsHtmlUnorderedList(items))
-          .append("</html>");
-        HelpAwareOptionPane.showOptionDialog(
-                Main.parent,
-                sb.toString(),
-                tr("Merging deleted objects failed"),
-                JOptionPane.WARNING_MESSAGE,
-                HelpUtil.ht("/Dialog/Conflict#MergingDeletedPrimitivesFailed")
-        );
-    }
-
-    private void copy(ListRole sourceRole, int[] rows, int position) {
-        if (position < 0 || position > getMergedEntriesSize())
-            throw new IllegalArgumentException("Position must be between 0 and "+getMergedEntriesSize()+" but is "+position);
-        List<T> newItems = new ArrayList<>(rows.length);
-        List<T> source = entries.get(sourceRole);
-        List<PrimitiveId> deletedIds = new ArrayList<>();
-        for (int row: rows) {
-            T entry = source.get(row);
-            OsmPrimitive primitive = getMyPrimitive(entry);
-            if (!primitive.isDeleted()) {
-                T clone = cloneEntryForMergedList(entry);
-                newItems.add(clone);
-            } else {
-                deletedIds.add(primitive.getPrimitiveId());
-            }
-        }
-        getMergedEntries().addAll(position, newItems);
-        fireModelDataChanged();
-        if (!deletedIds.isEmpty()) {
-            alertCopyFailedForDeletedPrimitives(deletedIds);
-        }
-    }
-
-    public void copyAll(ListRole source) {
-        getMergedEntries().clear();
-
-        int[] rows = new int[entries.get(source).size()];
-        for (int i = 0; i < rows.length; i++) {
-            rows[i] = i;
-        }
-        copy(source, rows, 0);
-    }
-
-    /**
-     * Copies the nodes given by indices in rows from the list of  nodes <code>source</code> to the
-     * list of merged nodes. Inserts the nodes before row given by current.
-     *
-     * @param source the list of nodes to copy from
-     * @param rows the indices
-     * @param current the row index before which the nodes are inserted
-     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
-     */
-    protected void copyBeforeCurrent(ListRole source, int[] rows, int current) {
-        copy(source, rows, current);
-        mergedEntriesSelectionModel.setSelectionInterval(current, current + rows.length-1);
-    }
-
-    /**
-     * Copies the nodes given by indices in rows from the list of my nodes to the
-     * list of merged nodes. Inserts the nodes before row given by current.
-     *
-     * @param rows the indices
-     * @param current the row index before which the nodes are inserted
-     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
-     */
-    public void copyMyBeforeCurrent(int[] rows, int current) {
-        copyBeforeCurrent(MY_ENTRIES, rows, current);
-    }
-
-    /**
-     * Copies the nodes given by indices in rows from the list of their nodes to the
-     * list of merged nodes. Inserts the nodes before row given by current.
-     *
-     * @param rows the indices
-     * @param current the row index before which the nodes are inserted
-     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
-     */
-    public void copyTheirBeforeCurrent(int[] rows, int current) {
-        copyBeforeCurrent(THEIR_ENTRIES, rows, current);
-    }
-
-    /**
-     * Copies the nodes given by indices in rows from the list of  nodes <code>source</code> to the
-     * list of merged nodes. Inserts the nodes after the row given by current.
-     *
-     * @param source the list of nodes to copy from
-     * @param rows the indices
-     * @param current the row index after which the nodes are inserted
-     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
-     */
-    protected void copyAfterCurrent(ListRole source, int[] rows, int current) {
-        copy(source, rows, current + 1);
-        mergedEntriesSelectionModel.setSelectionInterval(current+1, current + rows.length-1);
-        fireStateChanged();
-    }
-
-    /**
-     * Copies the nodes given by indices in rows from the list of my nodes to the
-     * list of merged nodes. Inserts the nodes after the row given by current.
-     *
-     * @param rows the indices
-     * @param current the row index after which the nodes are inserted
-     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
-     */
-    public void copyMyAfterCurrent(int[] rows, int current) {
-        copyAfterCurrent(MY_ENTRIES, rows, current);
-    }
-
-    /**
-     * Copies the nodes given by indices in rows from the list of my nodes to the
-     * list of merged nodes. Inserts the nodes after the row given by current.
-     *
-     * @param rows the indices
-     * @param current the row index after which the nodes are inserted
-     * @throws IllegalArgumentException if current &lt; 0 or &gt;= #nodes in list of merged nodes
-     */
-    public void copyTheirAfterCurrent(int[] rows, int current) {
-        copyAfterCurrent(THEIR_ENTRIES, rows, current);
-    }
-
-    /**
-     * Moves the nodes given by indices in rows  up by one position in the list
-     * of merged nodes.
-     *
-     * @param rows the indices
-     *
-     */
-    public void moveUpMerged(int ... rows) {
-        if (rows == null || rows.length == 0)
-            return;
-        if (rows[0] == 0)
-            // can't move up
-            return;
-        List<T> mergedEntries = getMergedEntries();
-        for (int row: rows) {
-            T n = mergedEntries.get(row);
-            mergedEntries.remove(row);
-            mergedEntries.add(row -1, n);
-        }
-        fireModelDataChanged();
-        mergedEntriesSelectionModel.clearSelection();
-        for (int row: rows) {
-            mergedEntriesSelectionModel.addSelectionInterval(row-1, row-1);
-        }
-    }
-
-    /**
-     * Moves the nodes given by indices in rows down by one position in the list
-     * of merged nodes.
-     *
-     * @param rows the indices
-     */
-    public void moveDownMerged(int ... rows) {
-        if (rows == null || rows.length == 0)
-            return;
-        List<T> mergedEntries = getMergedEntries();
-        if (rows[rows.length -1] == mergedEntries.size() -1)
-            // can't move down
-            return;
-        for (int i = rows.length-1; i >= 0; i--) {
-            int row = rows[i];
-            T n = mergedEntries.get(row);
-            mergedEntries.remove(row);
-            mergedEntries.add(row +1, n);
-        }
-        fireModelDataChanged();
-        mergedEntriesSelectionModel.clearSelection();
-        for (int row: rows) {
-            mergedEntriesSelectionModel.addSelectionInterval(row+1, row+1);
-        }
-    }
-
-    /**
-     * Removes the nodes given by indices in rows from the list
-     * of merged nodes.
-     *
-     * @param rows the indices
-     */
-    public void removeMerged(int ... rows) {
-        if (rows == null || rows.length == 0)
-            return;
-
-        List<T> mergedEntries = getMergedEntries();
-
-        for (int i = rows.length-1; i >= 0; i--) {
-            mergedEntries.remove(rows[i]);
-        }
-        fireModelDataChanged();
-        mergedEntriesSelectionModel.clearSelection();
-    }
-
-    /**
-     * Replies true if the list of my entries and the list of their
-     * entries are equal
-     *
-     * @return true, if the lists are equal; false otherwise
-     */
-    protected boolean myAndTheirEntriesEqual() {
-
-        if (getMyEntriesSize() != getTheirEntriesSize())
-            return false;
-        for (int i = 0; i < getMyEntriesSize(); i++) {
-            if (!isEqualEntry(getMyEntries().get(i), getTheirEntries().get(i)))
-                return false;
-        }
-        return true;
-    }
-
-    /**
-     * This an adapter between a {@link JTable} and one of the three entry lists
-     * in the role {@link ListRole} managed by the {@link ListMergeModel}.
-     *
-     * From the point of view of the {@link JTable} it is a {@link TableModel}.
-     *
-     * @see ListMergeModel#getMyTableModel()
-     * @see ListMergeModel#getTheirTableModel()
-     * @see ListMergeModel#getMergedTableModel()
-     */
-    public class EntriesTableModel extends DefaultTableModel implements OsmPrimitivesTableModel {
-        private final ListRole role;
-
-        /**
-         *
-         * @param role the role
-         */
-        public EntriesTableModel(ListRole role) {
-            this.role = role;
-        }
-
-        @Override
-        public int getRowCount() {
-            int count = Math.max(getMyEntries().size(), getMergedEntries().size());
-            return Math.max(count, getTheirEntries().size());
-        }
-
-        @Override
-        public Object getValueAt(int row, int column) {
-            if (row < entries.get(role).size())
-                return entries.get(role).get(row);
-            return null;
-        }
-
-        @Override
-        public boolean isCellEditable(int row, int column) {
-            return false;
-        }
-
-        @Override
-        public void setValueAt(Object value, int row, int col) {
-            ListMergeModel.this.setValueAt(this, value, row, col);
-        }
-
-        /**
-         * Returns the list merge model.
-         * @return the list merge model
-         */
-        public ListMergeModel<T, C> getListMergeModel() {
-            return ListMergeModel.this;
-        }
-
-        /**
-         * replies true if the {@link ListRole} of this {@link EntriesTableModel}
-         * participates in the current {@link ComparePairType}
-         *
-         * @return true, if the if the {@link ListRole} of this {@link EntriesTableModel}
-         * participates in the current {@link ComparePairType}
-         *
-         * @see ListMergeModel.ComparePairListModel#getSelectedComparePair()
-         */
-        public boolean isParticipatingInCurrentComparePair() {
-            return getComparePairListModel()
-            .getSelectedComparePair()
-            .isParticipatingIn(role);
-        }
-
-        /**
-         * replies true if the entry at <code>row</code> is equal to the entry at the
-         * same position in the opposite list of the current {@link ComparePairType}.
-         *
-         * @param row  the row number
-         * @return true if the entry at <code>row</code> is equal to the entry at the
-         * same position in the opposite list of the current {@link ComparePairType}
-         * @throws IllegalStateException if this model is not participating in the
-         *   current  {@link ComparePairType}
-         * @see ComparePairType#getOppositeRole(ListRole)
-         * @see #getRole()
-         * @see #getOppositeEntries()
-         */
-        public boolean isSamePositionInOppositeList(int row) {
-            if (!isParticipatingInCurrentComparePair())
-                throw new IllegalStateException(tr("List in role {0} is currently not participating in a compare pair.", role.toString()));
-            if (row >= getEntries().size()) return false;
-            if (row >= getOppositeEntries().size()) return false;
-
-            T e1 = getEntries().get(row);
-            T e2 = getOppositeEntries().get(row);
-            return isEqualEntry(e1, e2);
-        }
-
-        /**
-         * replies true if the entry at the current position is present in the opposite list
-         * of the current {@link ComparePairType}.
-         *
-         * @param row the current row
-         * @return true if the entry at the current position is present in the opposite list
-         * of the current {@link ComparePairType}.
-         * @throws IllegalStateException if this model is not participating in the
-         *   current {@link ComparePairType}
-         * @see ComparePairType#getOppositeRole(ListRole)
-         * @see #getRole()
-         * @see #getOppositeEntries()
-         */
-        public boolean isIncludedInOppositeList(int row) {
-            if (!isParticipatingInCurrentComparePair())
-                throw new IllegalStateException(tr("List in role {0} is currently not participating in a compare pair.", role.toString()));
-
-            if (row >= getEntries().size()) return false;
-            T e1 = getEntries().get(row);
-            return getOppositeEntries().stream().anyMatch(e2 -> isEqualEntry(e1, e2));
-            }
-
-        protected List<T> getEntries() {
-            return entries.get(role);
-        }
-
-        /**
-         * replies the opposite list of entries with respect to the current {@link ComparePairType}
-         *
-         * @return the opposite list of entries
-         */
-        protected List<T> getOppositeEntries() {
-            ListRole opposite = getComparePairListModel().getSelectedComparePair().getOppositeRole(role);
-            return entries.get(opposite);
-        }
-
-        public ListRole getRole() {
-            return role;
-        }
-
-        @Override
-        public OsmPrimitive getReferredPrimitive(int idx) {
-            Object value = getValueAt(idx, 1);
-            if (value instanceof OsmPrimitive) {
-                return (OsmPrimitive) value;
-            } else if (value instanceof RelationMember) {
-                return ((RelationMember) value).getMember();
-            } else {
-                Main.error("Unknown object type: "+value);
-                return null;
-            }
-        }
-    }
-
-    /**
-     * This is the selection model to be used in a {@link JTable} which displays
-     * an entry list managed by {@link ListMergeModel}.
-     *
-     * The model ensures that only rows displaying an entry in the entry list
-     * can be selected. "Empty" rows can't be selected.
-     *
-     * @see ListMergeModel#getMySelectionModel()
-     * @see ListMergeModel#getMergedSelectionModel()
-     * @see ListMergeModel#getTheirSelectionModel()
-     *
-     */
-    protected class EntriesSelectionModel extends DefaultListSelectionModel {
-        private final transient List<T> entries;
-
-        public EntriesSelectionModel(List<T> nodes) {
-            this.entries = nodes;
-        }
-
-        @Override
-        public void addSelectionInterval(int index0, int index1) {
-            if (entries.isEmpty()) return;
-            if (index0 > entries.size() - 1) return;
-            index0 = Math.min(entries.size()-1, index0);
-            index1 = Math.min(entries.size()-1, index1);
-            super.addSelectionInterval(index0, index1);
-        }
-
-        @Override
-        public void insertIndexInterval(int index, int length, boolean before) {
-            if (entries.isEmpty()) return;
-            if (before) {
-                int newindex = Math.min(entries.size()-1, index);
-                if (newindex < index - length) return;
-                length = length - (index - newindex);
-                super.insertIndexInterval(newindex, length, before);
-            } else {
-                if (index > entries.size() -1) return;
-                length = Math.min(entries.size()-1 - index, length);
-                super.insertIndexInterval(index, length, before);
-            }
-        }
-
-        @Override
-        public void moveLeadSelectionIndex(int leadIndex) {
-            if (entries.isEmpty()) return;
-            leadIndex = Math.max(0, leadIndex);
-            leadIndex = Math.min(entries.size() - 1, leadIndex);
-            super.moveLeadSelectionIndex(leadIndex);
-        }
-
-        @Override
-        public void removeIndexInterval(int index0, int index1) {
-            if (entries.isEmpty()) return;
-            index0 = Math.max(0, index0);
-            index0 = Math.min(entries.size() - 1, index0);
-
-            index1 = Math.max(0, index1);
-            index1 = Math.min(entries.size() - 1, index1);
-            super.removeIndexInterval(index0, index1);
-        }
-
-        @Override
-        public void removeSelectionInterval(int index0, int index1) {
-            if (entries.isEmpty()) return;
-            index0 = Math.max(0, index0);
-            index0 = Math.min(entries.size() - 1, index0);
-
-            index1 = Math.max(0, index1);
-            index1 = Math.min(entries.size() - 1, index1);
-            super.removeSelectionInterval(index0, index1);
-        }
-
-        @Override
-        public void setAnchorSelectionIndex(int anchorIndex) {
-            if (entries.isEmpty()) return;
-            anchorIndex = Math.min(entries.size() - 1, anchorIndex);
-            super.setAnchorSelectionIndex(anchorIndex);
-        }
-
-        @Override
-        public void setLeadSelectionIndex(int leadIndex) {
-            if (entries.isEmpty()) return;
-            leadIndex = Math.min(entries.size() - 1, leadIndex);
-            super.setLeadSelectionIndex(leadIndex);
-        }
-
-        @Override
-        public void setSelectionInterval(int index0, int index1) {
-            if (entries.isEmpty()) return;
-            index0 = Math.max(0, index0);
-            index0 = Math.min(entries.size() - 1, index0);
-
-            index1 = Math.max(0, index1);
-            index1 = Math.min(entries.size() - 1, index1);
-
-            super.setSelectionInterval(index0, index1);
-        }
-    }
-
-    public ComparePairListModel getComparePairListModel() {
-        return this.comparePairListModel;
-    }
-
-    public class ComparePairListModel extends AbstractListModel<ComparePairType> implements ComboBoxModel<ComparePairType> {
-
-        private int selectedIdx;
-        private final List<ComparePairType> compareModes;
-
-        /**
-         * Constructs a new {@code ComparePairListModel}.
-         */
-        public ComparePairListModel() {
-            this.compareModes = new ArrayList<>();
-            compareModes.add(MY_WITH_THEIR);
-            compareModes.add(MY_WITH_MERGED);
-            compareModes.add(THEIR_WITH_MERGED);
-            selectedIdx = 0;
-        }
-
-        @Override
-        public ComparePairType getElementAt(int index) {
-            if (index < compareModes.size())
-                return compareModes.get(index);
-            throw new IllegalArgumentException(tr("Unexpected value of parameter ''index''. Got {0}.", index));
-        }
-
-        @Override
-        public int getSize() {
-            return compareModes.size();
-        }
-
-        @Override
-        public Object getSelectedItem() {
-            return compareModes.get(selectedIdx);
-        }
-
-        @Override
-        public void setSelectedItem(Object anItem) {
-            int i = compareModes.indexOf(anItem);
-            if (i < 0)
-                throw new IllegalStateException(tr("Item {0} not found in list.", anItem));
-            selectedIdx = i;
-            fireModelDataChanged();
-        }
-
-        public ComparePairType getSelectedComparePair() {
-            return compareModes.get(selectedIdx);
-        }
-    }
-
-    /**
-     * Builds the command to resolve conflicts in the list.
-     *
-     * @param conflict the conflict data set
-     * @return the command
-     * @throws IllegalStateException if the merge is not yet frozen
-     */
-    public abstract C buildResolveCommand(Conflict<? extends OsmPrimitive> conflict);
-}
Index: unk/src/org/openstreetmap/josm/gui/conflict/pair/ListMerger.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/conflict/pair/ListMerger.java	(revision 11329)
+++ 	(revision )
@@ -1,944 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.conflict.pair;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
-import static org.openstreetmap.josm.tools.I18n.trn;
-
-import java.awt.FlowLayout;
-import java.awt.GridBagConstraints;
-import java.awt.GridBagLayout;
-import java.awt.Insets;
-import java.awt.event.ActionEvent;
-import java.awt.event.ItemEvent;
-import java.awt.event.ItemListener;
-import java.beans.PropertyChangeEvent;
-import java.beans.PropertyChangeListener;
-import java.util.Collection;
-
-import javax.swing.AbstractAction;
-import javax.swing.Action;
-import javax.swing.ImageIcon;
-import javax.swing.JButton;
-import javax.swing.JCheckBox;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.JScrollPane;
-import javax.swing.JTable;
-import javax.swing.JToggleButton;
-import javax.swing.event.ChangeEvent;
-import javax.swing.event.ChangeListener;
-import javax.swing.event.ListSelectionEvent;
-import javax.swing.event.ListSelectionListener;
-
-import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.command.conflict.ConflictResolveCommand;
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.PrimitiveId;
-import org.openstreetmap.josm.data.osm.Relation;
-import org.openstreetmap.josm.data.osm.Way;
-import org.openstreetmap.josm.gui.layer.OsmDataLayer;
-import org.openstreetmap.josm.gui.util.AdjustmentSynchronizer;
-import org.openstreetmap.josm.gui.widgets.JosmComboBox;
-import org.openstreetmap.josm.gui.widgets.OsmPrimitivesTable;
-import org.openstreetmap.josm.tools.ImageProvider;
-
-/**
- * A UI component for resolving conflicts in two lists of entries of type T.
- *
- * @param <T> the type of the entries
- * @param <C> the type of conflict resolution command
- * @see ListMergeModel
- * @since 1631
- */
-public abstract class ListMerger<T extends PrimitiveId, C extends ConflictResolveCommand> extends JPanel
-implements PropertyChangeListener, ChangeListener, IConflictResolver {
-    protected OsmPrimitivesTable myEntriesTable;
-    protected OsmPrimitivesTable mergedEntriesTable;
-    protected OsmPrimitivesTable theirEntriesTable;
-
-    protected transient ListMergeModel<T, C> model;
-
-    private CopyStartLeftAction copyStartLeftAction;
-    private CopyBeforeCurrentLeftAction copyBeforeCurrentLeftAction;
-    private CopyAfterCurrentLeftAction copyAfterCurrentLeftAction;
-    private CopyEndLeftAction copyEndLeftAction;
-    private CopyAllLeft copyAllLeft;
-
-    private CopyStartRightAction copyStartRightAction;
-    private CopyBeforeCurrentRightAction copyBeforeCurrentRightAction;
-    private CopyAfterCurrentRightAction copyAfterCurrentRightAction;
-    private CopyEndRightAction copyEndRightAction;
-    private CopyAllRight copyAllRight;
-
-    private MoveUpMergedAction moveUpMergedAction;
-    private MoveDownMergedAction moveDownMergedAction;
-    private RemoveMergedAction removeMergedAction;
-    private FreezeAction freezeAction;
-
-    private transient AdjustmentSynchronizer adjustmentSynchronizer;
-
-    private JLabel lblMyVersion;
-    private JLabel lblMergedVersion;
-    private JLabel lblTheirVersion;
-
-    private JLabel lblFrozenState;
-
-    protected abstract JScrollPane buildMyElementsTable();
-
-    protected abstract JScrollPane buildMergedElementsTable();
-
-    protected abstract JScrollPane buildTheirElementsTable();
-
-    protected JScrollPane embeddInScrollPane(JTable table) {
-        JScrollPane pane = new JScrollPane(table);
-        if (adjustmentSynchronizer == null) {
-            adjustmentSynchronizer = new AdjustmentSynchronizer();
-        }
-        return pane;
-    }
-
-    protected void wireActionsToSelectionModels() {
-        myEntriesTable.getSelectionModel().addListSelectionListener(copyStartLeftAction);
-
-        myEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentLeftAction);
-        mergedEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentLeftAction);
-
-        myEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentLeftAction);
-        mergedEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentLeftAction);
-
-        myEntriesTable.getSelectionModel().addListSelectionListener(copyEndLeftAction);
-
-        theirEntriesTable.getSelectionModel().addListSelectionListener(copyStartRightAction);
-
-        theirEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentRightAction);
-        mergedEntriesTable.getSelectionModel().addListSelectionListener(copyBeforeCurrentRightAction);
-
-        theirEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentRightAction);
-        mergedEntriesTable.getSelectionModel().addListSelectionListener(copyAfterCurrentRightAction);
-
-        theirEntriesTable.getSelectionModel().addListSelectionListener(copyEndRightAction);
-
-        mergedEntriesTable.getSelectionModel().addListSelectionListener(moveUpMergedAction);
-        mergedEntriesTable.getSelectionModel().addListSelectionListener(moveDownMergedAction);
-        mergedEntriesTable.getSelectionModel().addListSelectionListener(removeMergedAction);
-
-        model.addChangeListener(copyAllLeft);
-        model.addChangeListener(copyAllRight);
-        model.addPropertyChangeListener(copyAllLeft);
-        model.addPropertyChangeListener(copyAllRight);
-    }
-
-    protected JPanel buildLeftButtonPanel() {
-        JPanel pnl = new JPanel(new GridBagLayout());
-        GridBagConstraints gc = new GridBagConstraints();
-
-        gc.gridx = 0;
-        gc.gridy = 0;
-        copyStartLeftAction = new CopyStartLeftAction();
-        JButton btn = new JButton(copyStartLeftAction);
-        btn.setName("button.copystartleft");
-        pnl.add(btn, gc);
-
-        gc.gridx = 0;
-        gc.gridy = 1;
-        copyBeforeCurrentLeftAction = new CopyBeforeCurrentLeftAction();
-        btn = new JButton(copyBeforeCurrentLeftAction);
-        btn.setName("button.copybeforecurrentleft");
-        pnl.add(btn, gc);
-
-        gc.gridx = 0;
-        gc.gridy = 2;
-        copyAfterCurrentLeftAction = new CopyAfterCurrentLeftAction();
-        btn = new JButton(copyAfterCurrentLeftAction);
-        btn.setName("button.copyaftercurrentleft");
-        pnl.add(btn, gc);
-
-        gc.gridx = 0;
-        gc.gridy = 3;
-        copyEndLeftAction = new CopyEndLeftAction();
-        btn = new JButton(copyEndLeftAction);
-        btn.setName("button.copyendleft");
-        pnl.add(btn, gc);
-
-        gc.gridx = 0;
-        gc.gridy = 4;
-        copyAllLeft = new CopyAllLeft();
-        btn = new JButton(copyAllLeft);
-        btn.setName("button.copyallleft");
-        pnl.add(btn, gc);
-
-        return pnl;
-    }
-
-    protected JPanel buildRightButtonPanel() {
-        JPanel pnl = new JPanel(new GridBagLayout());
-        GridBagConstraints gc = new GridBagConstraints();
-
-        gc.gridx = 0;
-        gc.gridy = 0;
-        copyStartRightAction = new CopyStartRightAction();
-        pnl.add(new JButton(copyStartRightAction), gc);
-
-        gc.gridx = 0;
-        gc.gridy = 1;
-        copyBeforeCurrentRightAction = new CopyBeforeCurrentRightAction();
-        pnl.add(new JButton(copyBeforeCurrentRightAction), gc);
-
-        gc.gridx = 0;
-        gc.gridy = 2;
-        copyAfterCurrentRightAction = new CopyAfterCurrentRightAction();
-        pnl.add(new JButton(copyAfterCurrentRightAction), gc);
-
-        gc.gridx = 0;
-        gc.gridy = 3;
-        copyEndRightAction = new CopyEndRightAction();
-        pnl.add(new JButton(copyEndRightAction), gc);
-
-        gc.gridx = 0;
-        gc.gridy = 4;
-        copyAllRight = new CopyAllRight();
-        pnl.add(new JButton(copyAllRight), gc);
-
-        return pnl;
-    }
-
-    protected JPanel buildMergedListControlButtons() {
-        JPanel pnl = new JPanel(new GridBagLayout());
-        GridBagConstraints gc = new GridBagConstraints();
-
-        gc.gridx = 0;
-        gc.gridy = 0;
-        gc.gridwidth = 1;
-        gc.gridheight = 1;
-        gc.fill = GridBagConstraints.HORIZONTAL;
-        gc.anchor = GridBagConstraints.CENTER;
-        gc.weightx = 0.3;
-        gc.weighty = 0.0;
-        moveUpMergedAction = new MoveUpMergedAction();
-        pnl.add(new JButton(moveUpMergedAction), gc);
-
-        gc.gridx = 1;
-        gc.gridy = 0;
-        moveDownMergedAction = new MoveDownMergedAction();
-        pnl.add(new JButton(moveDownMergedAction), gc);
-
-        gc.gridx = 2;
-        gc.gridy = 0;
-        removeMergedAction = new RemoveMergedAction();
-        pnl.add(new JButton(removeMergedAction), gc);
-
-        return pnl;
-    }
-
-    protected JPanel buildAdjustmentLockControlPanel(JCheckBox cb) {
-        JPanel panel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
-        panel.add(new JLabel(tr("lock scrolling")));
-        panel.add(cb);
-        return panel;
-    }
-
-    protected JPanel buildComparePairSelectionPanel() {
-        JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT));
-        p.add(new JLabel(tr("Compare ")));
-        JosmComboBox<ComparePairType> cbComparePair = new JosmComboBox<>(model.getComparePairListModel());
-        cbComparePair.setRenderer(new ComparePairListCellRenderer());
-        p.add(cbComparePair);
-        return p;
-    }
-
-    protected JPanel buildFrozeStateControlPanel() {
-        JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT));
-        lblFrozenState = new JLabel();
-        p.add(lblFrozenState);
-        freezeAction = new FreezeAction();
-        JToggleButton btn = new JToggleButton(freezeAction);
-        freezeAction.adapt(btn);
-        btn.setName("button.freeze");
-        p.add(btn);
-
-        return p;
-    }
-
-    protected final void build() {
-        setLayout(new GridBagLayout());
-        GridBagConstraints gc = new GridBagConstraints();
-
-        // ------------------
-        gc.gridx = 0;
-        gc.gridy = 0;
-        gc.gridwidth = 1;
-        gc.gridheight = 1;
-        gc.fill = GridBagConstraints.NONE;
-        gc.anchor = GridBagConstraints.CENTER;
-        gc.weightx = 0.0;
-        gc.weighty = 0.0;
-        gc.insets = new Insets(10, 0, 0, 0);
-        lblMyVersion = new JLabel(tr("My version"));
-        lblMyVersion.setToolTipText(tr("List of elements in my dataset, i.e. the local dataset"));
-        add(lblMyVersion, gc);
-
-        gc.gridx = 2;
-        gc.gridy = 0;
-        lblMergedVersion = new JLabel(tr("Merged version"));
-        lblMergedVersion.setToolTipText(
-                tr("List of merged elements. They will replace the list of my elements when the merge decisions are applied."));
-        add(lblMergedVersion, gc);
-
-        gc.gridx = 4;
-        gc.gridy = 0;
-        lblTheirVersion = new JLabel(tr("Their version"));
-        lblTheirVersion.setToolTipText(tr("List of elements in their dataset, i.e. the server dataset"));
-        add(lblTheirVersion, gc);
-
-        // ------------------------------
-        gc.gridx = 0;
-        gc.gridy = 1;
-        gc.gridwidth = 1;
-        gc.gridheight = 1;
-        gc.fill = GridBagConstraints.HORIZONTAL;
-        gc.anchor = GridBagConstraints.FIRST_LINE_START;
-        gc.weightx = 0.33;
-        gc.weighty = 0.0;
-        gc.insets = new Insets(0, 0, 0, 0);
-        JCheckBox cbLockMyScrolling = new JCheckBox();
-        cbLockMyScrolling.setName("checkbox.lockmyscrolling");
-        add(buildAdjustmentLockControlPanel(cbLockMyScrolling), gc);
-
-        gc.gridx = 2;
-        gc.gridy = 1;
-        JCheckBox cbLockMergedScrolling = new JCheckBox();
-        cbLockMergedScrolling.setName("checkbox.lockmergedscrolling");
-        add(buildAdjustmentLockControlPanel(cbLockMergedScrolling), gc);
-
-        gc.gridx = 4;
-        gc.gridy = 1;
-        JCheckBox cbLockTheirScrolling = new JCheckBox();
-        cbLockTheirScrolling.setName("checkbox.locktheirscrolling");
-        add(buildAdjustmentLockControlPanel(cbLockTheirScrolling), gc);
-
-        // --------------------------------
-        gc.gridx = 0;
-        gc.gridy = 2;
-        gc.gridwidth = 1;
-        gc.gridheight = 1;
-        gc.fill = GridBagConstraints.BOTH;
-        gc.anchor = GridBagConstraints.FIRST_LINE_START;
-        gc.weightx = 0.33;
-        gc.weighty = 1.0;
-        gc.insets = new Insets(0, 0, 0, 0);
-        JScrollPane pane = buildMyElementsTable();
-        lblMyVersion.setLabelFor(pane);
-        adjustmentSynchronizer.adapt(cbLockMyScrolling, pane.getVerticalScrollBar());
-        add(pane, gc);
-
-        gc.gridx = 1;
-        gc.gridy = 2;
-        gc.fill = GridBagConstraints.NONE;
-        gc.anchor = GridBagConstraints.CENTER;
-        gc.weightx = 0.0;
-        gc.weighty = 0.0;
-        add(buildLeftButtonPanel(), gc);
-
-        gc.gridx = 2;
-        gc.gridy = 2;
-        gc.fill = GridBagConstraints.BOTH;
-        gc.anchor = GridBagConstraints.FIRST_LINE_START;
-        gc.weightx = 0.33;
-        gc.weighty = 0.0;
-        pane = buildMergedElementsTable();
-        lblMergedVersion.setLabelFor(pane);
-        adjustmentSynchronizer.adapt(cbLockMergedScrolling, pane.getVerticalScrollBar());
-        add(pane, gc);
-
-        gc.gridx = 3;
-        gc.gridy = 2;
-        gc.fill = GridBagConstraints.NONE;
-        gc.anchor = GridBagConstraints.CENTER;
-        gc.weightx = 0.0;
-        gc.weighty = 0.0;
-        add(buildRightButtonPanel(), gc);
-
-        gc.gridx = 4;
-        gc.gridy = 2;
-        gc.fill = GridBagConstraints.BOTH;
-        gc.anchor = GridBagConstraints.FIRST_LINE_START;
-        gc.weightx = 0.33;
-        gc.weighty = 0.0;
-        pane = buildTheirElementsTable();
-        lblTheirVersion.setLabelFor(pane);
-        adjustmentSynchronizer.adapt(cbLockTheirScrolling, pane.getVerticalScrollBar());
-        add(pane, gc);
-
-        // ----------------------------------
-        gc.gridx = 2;
-        gc.gridy = 3;
-        gc.gridwidth = 1;
-        gc.gridheight = 1;
-        gc.fill = GridBagConstraints.BOTH;
-        gc.anchor = GridBagConstraints.CENTER;
-        gc.weightx = 0.0;
-        gc.weighty = 0.0;
-        add(buildMergedListControlButtons(), gc);
-
-        // -----------------------------------
-        gc.gridx = 0;
-        gc.gridy = 4;
-        gc.gridwidth = 2;
-        gc.gridheight = 1;
-        gc.fill = GridBagConstraints.HORIZONTAL;
-        gc.anchor = GridBagConstraints.LINE_START;
-        gc.weightx = 0.0;
-        gc.weighty = 0.0;
-        add(buildComparePairSelectionPanel(), gc);
-
-        gc.gridx = 2;
-        gc.gridy = 4;
-        gc.gridwidth = 3;
-        gc.gridheight = 1;
-        gc.fill = GridBagConstraints.HORIZONTAL;
-        gc.anchor = GridBagConstraints.LINE_START;
-        gc.weightx = 0.0;
-        gc.weighty = 0.0;
-        add(buildFrozeStateControlPanel(), gc);
-
-        wireActionsToSelectionModels();
-    }
-
-    /**
-     * Constructs a new {@code ListMerger}.
-     * @param model list merger model
-     */
-    public ListMerger(ListMergeModel<T, C> model) {
-        this.model = model;
-        model.addChangeListener(this);
-        build();
-        model.addPropertyChangeListener(this);
-    }
-
-    /**
-     * Base class of all other Copy* inner classes.
-     */
-    abstract static class CopyAction extends AbstractAction implements ListSelectionListener {
-
-        protected CopyAction(String iconName, String actionName, String shortDescription) {
-            ImageIcon icon = ImageProvider.get("dialogs/conflict", iconName);
-            putValue(Action.SMALL_ICON, icon);
-            if (icon == null) {
-                putValue(Action.NAME, actionName);
-            }
-            putValue(Action.SHORT_DESCRIPTION, shortDescription);
-            setEnabled(false);
-        }
-    }
-
-    /**
-     * Action for copying selected nodes in the list of my nodes to the list of merged
-     * nodes. Inserts the nodes at the beginning of the list of merged nodes.
-     */
-    class CopyStartLeftAction extends CopyAction {
-
-        CopyStartLeftAction() {
-            super(/* ICON(dialogs/conflict/)*/ "copystartleft", tr("> top"),
-                tr("Copy my selected nodes to the start of the merged node list"));
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent e) {
-            model.copyMyToTop(myEntriesTable.getSelectedRows());
-        }
-
-        @Override
-        public void valueChanged(ListSelectionEvent e) {
-            setEnabled(!myEntriesTable.getSelectionModel().isSelectionEmpty());
-        }
-    }
-
-    /**
-     * Action for copying selected nodes in the list of my nodes to the list of merged
-     * nodes. Inserts the nodes at the end of the list of merged nodes.
-     */
-    class CopyEndLeftAction extends CopyAction {
-
-        CopyEndLeftAction() {
-            super(/* ICON(dialogs/conflict/)*/ "copyendleft", tr("> bottom"),
-                tr("Copy my selected elements to the end of the list of merged elements."));
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent e) {
-            model.copyMyToEnd(myEntriesTable.getSelectedRows());
-        }
-
-        @Override
-        public void valueChanged(ListSelectionEvent e) {
-            setEnabled(!myEntriesTable.getSelectionModel().isSelectionEmpty());
-        }
-    }
-
-    /**
-     * Action for copying selected nodes in the list of my nodes to the list of merged
-     * nodes. Inserts the nodes before the first selected row in the list of merged nodes.
-     */
-    class CopyBeforeCurrentLeftAction extends CopyAction {
-
-        CopyBeforeCurrentLeftAction() {
-            super(/* ICON(dialogs/conflict/)*/ "copybeforecurrentleft", tr("> before"),
-                    tr("Copy my selected elements before the first selected element in the list of merged elements."));
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent e) {
-            int[] mergedRows = mergedEntriesTable.getSelectedRows();
-            if (mergedRows == null || mergedRows.length == 0)
-                return;
-            int[] myRows = myEntriesTable.getSelectedRows();
-            int current = mergedRows[0];
-            model.copyMyBeforeCurrent(myRows, current);
-        }
-
-        @Override
-        public void valueChanged(ListSelectionEvent e) {
-            setEnabled(
-                    !myEntriesTable.getSelectionModel().isSelectionEmpty()
-                    && !mergedEntriesTable.getSelectionModel().isSelectionEmpty()
-            );
-        }
-    }
-
-    /**
-     * Action for copying selected nodes in the list of my nodes to the list of merged
-     * nodes. Inserts the nodes after the first selected row in the list of merged nodes.
-     */
-    class CopyAfterCurrentLeftAction extends CopyAction {
-
-        CopyAfterCurrentLeftAction() {
-            super(/* ICON(dialogs/conflict/)*/ "copyaftercurrentleft", tr("> after"),
-                    tr("Copy my selected elements after the first selected element in the list of merged elements."));
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent e) {
-            int[] mergedRows = mergedEntriesTable.getSelectedRows();
-            if (mergedRows == null || mergedRows.length == 0)
-                return;
-            int[] myRows = myEntriesTable.getSelectedRows();
-            int current = mergedRows[0];
-            model.copyMyAfterCurrent(myRows, current);
-        }
-
-        @Override
-        public void valueChanged(ListSelectionEvent e) {
-            setEnabled(
-                    !myEntriesTable.getSelectionModel().isSelectionEmpty()
-                    && !mergedEntriesTable.getSelectionModel().isSelectionEmpty()
-            );
-        }
-    }
-
-    class CopyStartRightAction extends CopyAction {
-
-        CopyStartRightAction() {
-            super(/* ICON(dialogs/conflict/)*/ "copystartright", tr("< top"),
-                tr("Copy their selected element to the start of the list of merged elements."));
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent e) {
-            model.copyTheirToTop(theirEntriesTable.getSelectedRows());
-        }
-
-        @Override
-        public void valueChanged(ListSelectionEvent e) {
-            setEnabled(!theirEntriesTable.getSelectionModel().isSelectionEmpty());
-        }
-    }
-
-    class CopyEndRightAction extends CopyAction {
-
-        CopyEndRightAction() {
-            super(/* ICON(dialogs/conflict/)*/ "copyendright", tr("< bottom"),
-                tr("Copy their selected elements to the end of the list of merged elements."));
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent arg0) {
-            model.copyTheirToEnd(theirEntriesTable.getSelectedRows());
-        }
-
-        @Override
-        public void valueChanged(ListSelectionEvent e) {
-            setEnabled(!theirEntriesTable.getSelectionModel().isSelectionEmpty());
-        }
-    }
-
-    class CopyBeforeCurrentRightAction extends CopyAction {
-
-        CopyBeforeCurrentRightAction() {
-            super(/* ICON(dialogs/conflict/)*/ "copybeforecurrentright", tr("< before"),
-                    tr("Copy their selected elements before the first selected element in the list of merged elements."));
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent e) {
-            int[] mergedRows = mergedEntriesTable.getSelectedRows();
-            if (mergedRows == null || mergedRows.length == 0)
-                return;
-            int[] myRows = theirEntriesTable.getSelectedRows();
-            int current = mergedRows[0];
-            model.copyTheirBeforeCurrent(myRows, current);
-        }
-
-        @Override
-        public void valueChanged(ListSelectionEvent e) {
-            setEnabled(
-                    !theirEntriesTable.getSelectionModel().isSelectionEmpty()
-                    && !mergedEntriesTable.getSelectionModel().isSelectionEmpty()
-            );
-        }
-    }
-
-    class CopyAfterCurrentRightAction extends CopyAction {
-
-        CopyAfterCurrentRightAction() {
-            super(/* ICON(dialogs/conflict/)*/ "copyaftercurrentright", tr("< after"),
-                    tr("Copy their selected element after the first selected element in the list of merged elements"));
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent e) {
-            int[] mergedRows = mergedEntriesTable.getSelectedRows();
-            if (mergedRows == null || mergedRows.length == 0)
-                return;
-            int[] myRows = theirEntriesTable.getSelectedRows();
-            int current = mergedRows[0];
-            model.copyTheirAfterCurrent(myRows, current);
-        }
-
-        @Override
-        public void valueChanged(ListSelectionEvent e) {
-            setEnabled(
-                    !theirEntriesTable.getSelectionModel().isSelectionEmpty()
-                    && !mergedEntriesTable.getSelectionModel().isSelectionEmpty()
-            );
-        }
-    }
-
-    class CopyAllLeft extends AbstractAction implements ChangeListener, PropertyChangeListener {
-
-        CopyAllLeft() {
-            ImageIcon icon = ImageProvider.get("dialogs/conflict", "useallleft");
-            putValue(Action.SMALL_ICON, icon);
-            putValue(Action.SHORT_DESCRIPTION, tr("Copy all my elements to the target"));
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent arg0) {
-            model.copyAll(ListRole.MY_ENTRIES);
-            model.setFrozen(true);
-        }
-
-        private void updateEnabledState() {
-            setEnabled(model.getMergedEntries().isEmpty() && !model.isFrozen());
-        }
-
-        @Override
-        public void stateChanged(ChangeEvent e) {
-            updateEnabledState();
-        }
-
-        @Override
-        public void propertyChange(PropertyChangeEvent evt) {
-            updateEnabledState();
-        }
-    }
-
-    class CopyAllRight extends AbstractAction implements ChangeListener, PropertyChangeListener {
-
-        CopyAllRight() {
-            ImageIcon icon = ImageProvider.get("dialogs/conflict", "useallright");
-            putValue(Action.SMALL_ICON, icon);
-            putValue(Action.SHORT_DESCRIPTION, tr("Copy all their elements to the target"));
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent arg0) {
-            model.copyAll(ListRole.THEIR_ENTRIES);
-            model.setFrozen(true);
-        }
-
-        private void updateEnabledState() {
-            setEnabled(model.getMergedEntries().isEmpty() && !model.isFrozen());
-        }
-
-        @Override
-        public void stateChanged(ChangeEvent e) {
-            updateEnabledState();
-        }
-
-        @Override
-        public void propertyChange(PropertyChangeEvent evt) {
-            updateEnabledState();
-        }
-    }
-
-    class MoveUpMergedAction extends AbstractAction implements ListSelectionListener {
-
-        MoveUpMergedAction() {
-            ImageIcon icon = ImageProvider.get("dialogs/conflict", "moveup");
-            putValue(Action.SMALL_ICON, icon);
-            if (icon == null) {
-                putValue(Action.NAME, tr("Up"));
-            }
-            putValue(Action.SHORT_DESCRIPTION, tr("Move up the selected entries by one position."));
-            setEnabled(false);
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent arg0) {
-            int[] rows = mergedEntriesTable.getSelectedRows();
-            model.moveUpMerged(rows);
-        }
-
-        @Override
-        public void valueChanged(ListSelectionEvent e) {
-            int[] rows = mergedEntriesTable.getSelectedRows();
-            setEnabled(
-                    rows != null
-                    && rows.length > 0
-                    && rows[0] != 0
-            );
-        }
-    }
-
-    /**
-     * Action for moving the currently selected entries in the list of merged entries
-     * one position down
-     *
-     */
-    class MoveDownMergedAction extends AbstractAction implements ListSelectionListener {
-
-        MoveDownMergedAction() {
-            ImageIcon icon = ImageProvider.get("dialogs/conflict", "movedown");
-            putValue(Action.SMALL_ICON, icon);
-            if (icon == null) {
-                putValue(Action.NAME, tr("Down"));
-            }
-            putValue(Action.SHORT_DESCRIPTION, tr("Move down the selected entries by one position."));
-            setEnabled(false);
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent arg0) {
-            int[] rows = mergedEntriesTable.getSelectedRows();
-            model.moveDownMerged(rows);
-        }
-
-        @Override
-        public void valueChanged(ListSelectionEvent e) {
-            int[] rows = mergedEntriesTable.getSelectedRows();
-            setEnabled(
-                    rows != null
-                    && rows.length > 0
-                    && rows[rows.length -1] != mergedEntriesTable.getRowCount() -1
-            );
-        }
-    }
-
-    /**
-     * Action for removing the selected entries in the list of merged entries
-     * from the list of merged entries.
-     *
-     */
-    class RemoveMergedAction extends AbstractAction implements ListSelectionListener {
-
-        RemoveMergedAction() {
-            ImageIcon icon = ImageProvider.get("dialogs/conflict", "remove");
-            putValue(Action.SMALL_ICON, icon);
-            if (icon == null) {
-                putValue(Action.NAME, tr("Remove"));
-            }
-            putValue(Action.SHORT_DESCRIPTION, tr("Remove the selected entries from the list of merged elements."));
-            setEnabled(false);
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent arg0) {
-            int[] rows = mergedEntriesTable.getSelectedRows();
-            model.removeMerged(rows);
-        }
-
-        @Override
-        public void valueChanged(ListSelectionEvent e) {
-            int[] rows = mergedEntriesTable.getSelectedRows();
-            setEnabled(
-                    rows != null
-                    && rows.length > 0
-            );
-        }
-    }
-
-    private interface FreezeActionProperties {
-        String PROP_SELECTED = FreezeActionProperties.class.getName() + ".selected";
-    }
-
-    /**
-     * Action for freezing the current state of the list merger
-     *
-     */
-    private final class FreezeAction extends AbstractAction implements ItemListener, FreezeActionProperties {
-
-        private FreezeAction() {
-            putValue(Action.NAME, tr("Freeze"));
-            putValue(Action.SHORT_DESCRIPTION, tr("Freeze the current list of merged elements."));
-            putValue(PROP_SELECTED, Boolean.FALSE);
-            setEnabled(true);
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent arg0) {
-            // do nothing
-        }
-
-        /**
-         * Java 1.5 doesn't known Action.SELECT_KEY. Wires a toggle button to this action
-         * such that the action gets notified about item state changes and the button gets
-         * notified about selection state changes of the action.
-         *
-         * @param btn a toggle button
-         */
-        public void adapt(final JToggleButton btn) {
-            btn.addItemListener(this);
-            addPropertyChangeListener(evt -> {
-                    if (evt.getPropertyName().equals(PROP_SELECTED)) {
-                        btn.setSelected((Boolean) evt.getNewValue());
-                    }
-                });
-        }
-
-        @Override
-        public void itemStateChanged(ItemEvent e) {
-            int state = e.getStateChange();
-            if (state == ItemEvent.SELECTED) {
-                putValue(Action.NAME, tr("Unfreeze"));
-                putValue(Action.SHORT_DESCRIPTION, tr("Unfreeze the list of merged elements and start merging."));
-                model.setFrozen(true);
-            } else if (state == ItemEvent.DESELECTED) {
-                putValue(Action.NAME, tr("Freeze"));
-                putValue(Action.SHORT_DESCRIPTION, tr("Freeze the current list of merged elements."));
-                model.setFrozen(false);
-            }
-            boolean isSelected = (Boolean) getValue(PROP_SELECTED);
-            if (isSelected != (e.getStateChange() == ItemEvent.SELECTED)) {
-                putValue(PROP_SELECTED, e.getStateChange() == ItemEvent.SELECTED);
-            }
-
-        }
-    }
-
-    protected void handlePropertyChangeFrozen(boolean oldValue, boolean newValue) {
-        myEntriesTable.getSelectionModel().clearSelection();
-        myEntriesTable.setEnabled(!newValue);
-        theirEntriesTable.getSelectionModel().clearSelection();
-        theirEntriesTable.setEnabled(!newValue);
-        mergedEntriesTable.getSelectionModel().clearSelection();
-        mergedEntriesTable.setEnabled(!newValue);
-        freezeAction.putValue(FreezeActionProperties.PROP_SELECTED, newValue);
-        if (newValue) {
-            lblFrozenState.setText(
-                    tr("<html>Click <strong>{0}</strong> to start merging my and their entries.</html>",
-                            freezeAction.getValue(Action.NAME))
-            );
-        } else {
-            lblFrozenState.setText(
-                    tr("<html>Click <strong>{0}</strong> to finish merging my and their entries.</html>",
-                            freezeAction.getValue(Action.NAME))
-            );
-        }
-    }
-
-    @Override
-    public void propertyChange(PropertyChangeEvent evt) {
-        if (evt.getPropertyName().equals(ListMergeModel.FROZEN_PROP)) {
-            handlePropertyChangeFrozen((Boolean) evt.getOldValue(), (Boolean) evt.getNewValue());
-        }
-    }
-
-    /**
-     * Returns the model.
-     * @return the model
-     */
-    public ListMergeModel<T, C> getModel() {
-        return model;
-    }
-
-    @Override
-    public void stateChanged(ChangeEvent e) {
-        lblMyVersion.setText(
-                trn("My version ({0} entry)", "My version ({0} entries)", model.getMyEntriesSize(), model.getMyEntriesSize())
-        );
-        lblMergedVersion.setText(
-                trn("Merged version ({0} entry)", "Merged version ({0} entries)", model.getMergedEntriesSize(), model.getMergedEntriesSize())
-        );
-        lblTheirVersion.setText(
-                trn("Their version ({0} entry)", "Their version ({0} entries)", model.getTheirEntriesSize(), model.getTheirEntriesSize())
-        );
-    }
-
-    /**
-     * Adds all registered listeners by this merger
-     * @see #unregisterListeners()
-     * @since 10454
-     */
-    public void registerListeners() {
-        myEntriesTable.registerListeners();
-        mergedEntriesTable.registerListeners();
-        theirEntriesTable.registerListeners();
-    }
-
-    /**
-     * Removes all registered listeners by this merger
-     * @since 10454
-     */
-    public void unregisterListeners() {
-        myEntriesTable.unregisterListeners();
-        mergedEntriesTable.unregisterListeners();
-        theirEntriesTable.unregisterListeners();
-    }
-
-    protected final <P extends OsmPrimitive> OsmDataLayer findLayerFor(P primitive) {
-        if (primitive != null) {
-            Iterable<OsmDataLayer> layers = Main.getLayerManager().getLayersOfType(OsmDataLayer.class);
-            // Find layer with same dataset
-            for (OsmDataLayer layer : layers) {
-                if (layer.data == primitive.getDataSet()) {
-                    return layer;
-                }
-            }
-            // Conflict after merging layers: a dataset could be no more in any layer, try to find another layer with same primitive
-            for (OsmDataLayer layer : layers) {
-                final Collection<? extends OsmPrimitive> collection;
-                if (primitive instanceof Way) {
-                    collection = layer.data.getWays();
-                } else if (primitive instanceof Relation) {
-                    collection = layer.data.getRelations();
-                } else {
-                    collection = layer.data.allPrimitives();
-                }
-                for (OsmPrimitive p : collection) {
-                    if (p.getPrimitiveId().equals(primitive.getPrimitiveId())) {
-                        return layer;
-                    }
-                }
-            }
-        }
-        return null;
-    }
-
-    @Override
-    public void decideRemaining(MergeDecisionType decision) {
-        if (!model.isFrozen()) {
-            model.copyAll(MergeDecisionType.KEEP_MINE.equals(decision) ? ListRole.MY_ENTRIES : ListRole.THEIR_ENTRIES);
-            model.setFrozen(true);
-        }
-    }
-}
Index: /trunk/src/org/openstreetmap/josm/gui/conflict/pair/ListRole.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/conflict/pair/ListRole.java	(revision 11329)
+++ /trunk/src/org/openstreetmap/josm/gui/conflict/pair/ListRole.java	(revision 11330)
@@ -3,5 +3,5 @@
 
 /**
- * Enumeration of roles entry lists play in {@link ListMergeModel}
+ * Enumeration of roles entry lists play in {@link AbstractListMergeModel}
  */
 public enum ListRole {
Index: /trunk/src/org/openstreetmap/josm/gui/conflict/pair/PairTable.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/conflict/pair/PairTable.java	(revision 11329)
+++ /trunk/src/org/openstreetmap/josm/gui/conflict/pair/PairTable.java	(revision 11330)
@@ -15,5 +15,5 @@
 public abstract class PairTable extends OsmPrimitivesTable {
 
-    private final transient ListMergeModel<? extends PrimitiveId, ? extends ConflictResolveCommand> model;
+    private final transient AbstractListMergeModel<? extends PrimitiveId, ? extends ConflictResolveCommand> model;
 
     /**
@@ -25,5 +25,5 @@
      * @param sm selection model
      */
-    public PairTable(String name, ListMergeModel<? extends PrimitiveId, ? extends ConflictResolveCommand> model,
+    public PairTable(String name, AbstractListMergeModel<? extends PrimitiveId, ? extends ConflictResolveCommand> model,
             OsmPrimitivesTableModel dm, TableColumnModel cm, ListSelectionModel sm) {
         super(dm, cm, sm);
Index: /trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListMergeModel.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListMergeModel.java	(revision 11329)
+++ /trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListMergeModel.java	(revision 11330)
@@ -15,5 +15,5 @@
 import org.openstreetmap.josm.data.osm.PrimitiveId;
 import org.openstreetmap.josm.data.osm.Way;
-import org.openstreetmap.josm.gui.conflict.pair.ListMergeModel;
+import org.openstreetmap.josm.gui.conflict.pair.AbstractListMergeModel;
 import org.openstreetmap.josm.gui.conflict.pair.ListRole;
 
@@ -22,5 +22,5 @@
  * @since 1622
  */
-public class NodeListMergeModel extends ListMergeModel<Node, WayNodesConflictResolverCommand> {
+public class NodeListMergeModel extends AbstractListMergeModel<Node, WayNodesConflictResolverCommand> {
 
     /**
Index: /trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListMerger.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListMerger.java	(revision 11329)
+++ /trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListMerger.java	(revision 11330)
@@ -9,5 +9,5 @@
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.Way;
-import org.openstreetmap.josm.gui.conflict.pair.ListMerger;
+import org.openstreetmap.josm.gui.conflict.pair.AbstractListMerger;
 
 /**
@@ -15,5 +15,5 @@
  * @since 1622
  */
-public class NodeListMerger extends ListMerger<Node, WayNodesConflictResolverCommand> {
+public class NodeListMerger extends AbstractListMerger<Node, WayNodesConflictResolverCommand> {
 
     /**
Index: /trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListTable.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListTable.java	(revision 11329)
+++ /trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListTable.java	(revision 11330)
@@ -7,5 +7,5 @@
 import org.openstreetmap.josm.command.conflict.WayNodesConflictResolverCommand;
 import org.openstreetmap.josm.data.osm.Node;
-import org.openstreetmap.josm.gui.conflict.pair.ListMergeModel;
+import org.openstreetmap.josm.gui.conflict.pair.AbstractListMergeModel;
 import org.openstreetmap.josm.gui.conflict.pair.PairTable;
 import org.openstreetmap.josm.gui.widgets.OsmPrimitivesTableModel;
@@ -24,5 +24,5 @@
      * @param sm selection model
      */
-    public NodeListTable(String name, ListMergeModel<Node, WayNodesConflictResolverCommand> model,
+    public NodeListTable(String name, AbstractListMergeModel<Node, WayNodesConflictResolverCommand> model,
             OsmPrimitivesTableModel dm, ListSelectionModel sm) {
         super(name, model, dm, new NodeListColumnModel(new NodeListTableCellRenderer()), sm);
Index: /trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListTableCellRenderer.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListTableCellRenderer.java	(revision 11329)
+++ /trunk/src/org/openstreetmap/josm/gui/conflict/pair/nodes/NodeListTableCellRenderer.java	(revision 11330)
@@ -16,5 +16,5 @@
 import org.openstreetmap.josm.gui.DefaultNameFormatter;
 import org.openstreetmap.josm.gui.conflict.ConflictColors;
-import org.openstreetmap.josm.gui.conflict.pair.ListMergeModel;
+import org.openstreetmap.josm.gui.conflict.pair.AbstractListMergeModel;
 import org.openstreetmap.josm.tools.ImageProvider;
 
@@ -55,5 +55,5 @@
      * @param isSelected true, if the current row is selected
      */
-    protected void renderNode(ListMergeModel<Node, WayNodesConflictResolverCommand>.EntriesTableModel model, Node node,
+    protected void renderNode(AbstractListMergeModel<Node, WayNodesConflictResolverCommand>.EntriesTableModel model, Node node,
             int row, boolean isSelected) {
         setIcon(icon);
@@ -90,5 +90,5 @@
      * @param row the row index
      */
-    protected void renderRowId(ListMergeModel<Node, WayNodesConflictResolverCommand>.EntriesTableModel model, int row) {
+    protected void renderRowId(AbstractListMergeModel<Node, WayNodesConflictResolverCommand>.EntriesTableModel model, int row) {
         setIcon(null);
         setBorder(rowNumberBorder);
@@ -132,6 +132,6 @@
      */
     @SuppressWarnings("unchecked")
-    protected ListMergeModel<Node, WayNodesConflictResolverCommand>.EntriesTableModel getModel(JTable table) {
-        return (ListMergeModel<Node, WayNodesConflictResolverCommand>.EntriesTableModel) table.getModel();
+    protected AbstractListMergeModel<Node, WayNodesConflictResolverCommand>.EntriesTableModel getModel(JTable table) {
+        return (AbstractListMergeModel<Node, WayNodesConflictResolverCommand>.EntriesTableModel) table.getModel();
     }
 }
Index: /trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberListMergeModel.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberListMergeModel.java	(revision 11329)
+++ /trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberListMergeModel.java	(revision 11330)
@@ -14,5 +14,5 @@
 import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.RelationMember;
-import org.openstreetmap.josm.gui.conflict.pair.ListMergeModel;
+import org.openstreetmap.josm.gui.conflict.pair.AbstractListMergeModel;
 import org.openstreetmap.josm.gui.conflict.pair.ListRole;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
@@ -22,5 +22,5 @@
  * @since 1631
  */
-public class RelationMemberListMergeModel extends ListMergeModel<RelationMember, RelationMemberConflictResolverCommand> {
+public class RelationMemberListMergeModel extends AbstractListMergeModel<RelationMember, RelationMemberConflictResolverCommand> {
 
     @Override
Index: /trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberMerger.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberMerger.java	(revision 11329)
+++ /trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberMerger.java	(revision 11330)
@@ -9,5 +9,5 @@
 import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.RelationMember;
-import org.openstreetmap.josm.gui.conflict.pair.ListMerger;
+import org.openstreetmap.josm.gui.conflict.pair.AbstractListMerger;
 
 /**
@@ -15,5 +15,5 @@
  * @since 1631
  */
-public class RelationMemberMerger extends ListMerger<RelationMember, RelationMemberConflictResolverCommand> {
+public class RelationMemberMerger extends AbstractListMerger<RelationMember, RelationMemberConflictResolverCommand> {
 
     /**
Index: /trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberTable.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberTable.java	(revision 11329)
+++ /trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberTable.java	(revision 11330)
@@ -7,5 +7,5 @@
 import org.openstreetmap.josm.command.conflict.RelationMemberConflictResolverCommand;
 import org.openstreetmap.josm.data.osm.RelationMember;
-import org.openstreetmap.josm.gui.conflict.pair.ListMergeModel;
+import org.openstreetmap.josm.gui.conflict.pair.AbstractListMergeModel;
 import org.openstreetmap.josm.gui.conflict.pair.PairTable;
 import org.openstreetmap.josm.gui.widgets.OsmPrimitivesTableModel;
@@ -24,5 +24,5 @@
      * @param sm selection model
      */
-    public RelationMemberTable(String name, ListMergeModel<RelationMember, RelationMemberConflictResolverCommand> model,
+    public RelationMemberTable(String name, AbstractListMergeModel<RelationMember, RelationMemberConflictResolverCommand> model,
             OsmPrimitivesTableModel dm, ListSelectionModel sm) {
         super(name, model, dm, new RelationMemberListColumnModel(), sm);
Index: /trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberTableCellRenderer.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberTableCellRenderer.java	(revision 11329)
+++ /trunk/src/org/openstreetmap/josm/gui/conflict/pair/relation/RelationMemberTableCellRenderer.java	(revision 11330)
@@ -15,5 +15,5 @@
 import org.openstreetmap.josm.gui.DefaultNameFormatter;
 import org.openstreetmap.josm.gui.conflict.ConflictColors;
-import org.openstreetmap.josm.gui.conflict.pair.ListMergeModel;
+import org.openstreetmap.josm.gui.conflict.pair.AbstractListMergeModel;
 import org.openstreetmap.josm.tools.ImageProvider;
 
@@ -45,5 +45,5 @@
     }
 
-    protected void renderBackground(ListMergeModel<RelationMember, RelationMemberConflictResolverCommand>.EntriesTableModel model,
+    protected void renderBackground(AbstractListMergeModel<RelationMember, RelationMemberConflictResolverCommand>.EntriesTableModel model,
             RelationMember member, int row, int col, boolean isSelected) {
         Color bgc = ConflictColors.BGCOLOR.get();
@@ -78,5 +78,5 @@
     }
 
-    protected void renderForeground(ListMergeModel<RelationMember, RelationMemberConflictResolverCommand>.EntriesTableModel model,
+    protected void renderForeground(AbstractListMergeModel<RelationMember, RelationMemberConflictResolverCommand>.EntriesTableModel model,
             RelationMember member, int row, int col, boolean isSelected) {
         Color fgc = ConflictColors.FGCOLOR.get();
@@ -148,6 +148,6 @@
      */
     @SuppressWarnings("unchecked")
-    protected ListMergeModel<RelationMember, RelationMemberConflictResolverCommand>.EntriesTableModel getModel(JTable table) {
-        return (ListMergeModel<RelationMember, RelationMemberConflictResolverCommand>.EntriesTableModel) table.getModel();
+    protected AbstractListMergeModel<RelationMember, RelationMemberConflictResolverCommand>.EntriesTableModel getModel(JTable table) {
+        return (AbstractListMergeModel<RelationMember, RelationMemberConflictResolverCommand>.EntriesTableModel) table.getModel();
     }
 }
Index: /trunk/src/org/openstreetmap/josm/gui/dialogs/changeset/query/OpenAndCloseStateRestrictionPanel.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/dialogs/changeset/query/OpenAndCloseStateRestrictionPanel.java	(revision 11329)
+++ /trunk/src/org/openstreetmap/josm/gui/dialogs/changeset/query/OpenAndCloseStateRestrictionPanel.java	(revision 11330)
@@ -23,4 +23,7 @@
  */
 public class OpenAndCloseStateRestrictionPanel extends JPanel implements RestrictionPanel {
+
+    private static final String PREF_ROOT = "changeset-query.advanced.open-restrictions";
+    private static final String PREF_QUERY_TYPE = PREF_ROOT + ".query-type";
 
     private final JRadioButton rbOpenOnly = new JRadioButton();
@@ -105,11 +108,10 @@
      */
     public void rememberSettings() {
-        String prefRoot = "changeset-query.advanced.open-restrictions";
         if (rbBoth.isSelected()) {
-            Main.pref.put(prefRoot + ".query-type", "both");
+            Main.pref.put(PREF_QUERY_TYPE, "both");
         } else if (rbOpenOnly.isSelected()) {
-            Main.pref.put(prefRoot + ".query-type", "open");
+            Main.pref.put(PREF_QUERY_TYPE, "open");
         } else if (rbClosedOnly.isSelected()) {
-            Main.pref.put(prefRoot + ".query-type", "closed");
+            Main.pref.put(PREF_QUERY_TYPE, "closed");
         }
     }
@@ -119,6 +121,5 @@
      */
     public void restoreFromSettings() {
-        String prefRoot = "changeset-query.advanced.open-restrictions";
-        String v = Main.pref.get(prefRoot + ".query-type", "open");
+        String v = Main.pref.get(PREF_QUERY_TYPE, "open");
         rbBoth.setSelected("both".equals(v));
         rbOpenOnly.setSelected("open".equals(v));
Index: /trunk/src/org/openstreetmap/josm/gui/dialogs/changeset/query/UserRestrictionPanel.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/dialogs/changeset/query/UserRestrictionPanel.java	(revision 11329)
+++ /trunk/src/org/openstreetmap/josm/gui/dialogs/changeset/query/UserRestrictionPanel.java	(revision 11330)
@@ -34,4 +34,7 @@
  */
 public class UserRestrictionPanel extends JPanel implements RestrictionPanel {
+    private static final String PREF_ROOT = "changeset-query.advanced.user-restrictions";
+    private static final String PREF_QUERY_TYPE = PREF_ROOT + ".query-type";
+
     private final ButtonGroup bgUserRestrictions = new ButtonGroup();
     private final JRadioButton rbRestrictToMyself = new JRadioButton();
@@ -257,14 +260,13 @@
      */
     public void rememberSettings() {
-        String prefRoot = "changeset-query.advanced.user-restrictions";
         if (rbRestrictToMyself.isSelected()) {
-            Main.pref.put(prefRoot + ".query-type", "mine");
+            Main.pref.put(PREF_QUERY_TYPE, "mine");
         } else if (rbRestrictToUid.isSelected()) {
-            Main.pref.put(prefRoot + ".query-type", "uid");
+            Main.pref.put(PREF_QUERY_TYPE, "uid");
         } else if (rbRestrictToUserName.isSelected()) {
-            Main.pref.put(prefRoot + ".query-type", "username");
-        }
-        Main.pref.put(prefRoot + ".uid", tfUid.getText());
-        Main.pref.put(prefRoot + ".username", tfUserName.getText());
+            Main.pref.put(PREF_QUERY_TYPE, "username");
+        }
+        Main.pref.put(PREF_ROOT + ".uid", tfUid.getText());
+        Main.pref.put(PREF_ROOT + ".username", tfUserName.getText());
     }
 
@@ -273,6 +275,5 @@
      */
     public void restoreFromSettings() {
-        String prefRoot = "changeset-query.advanced.user-restrictions";
-        String v = Main.pref.get(prefRoot + ".query-type", "mine");
+        String v = Main.pref.get(PREF_QUERY_TYPE, "mine");
         if ("mine".equals(v)) {
             JosmUserIdentityManager im = JosmUserIdentityManager.getInstance();
@@ -287,9 +288,9 @@
             rbRestrictToUserName.setSelected(true);
         }
-        tfUid.setText(Main.pref.get(prefRoot + ".uid", ""));
+        tfUid.setText(Main.pref.get(PREF_ROOT + ".uid", ""));
         if (!valUid.isValid()) {
             tfUid.setText("");
         }
-        tfUserName.setText(Main.pref.get(prefRoot + ".username", ""));
+        tfUserName.setText(Main.pref.get(PREF_ROOT + ".username", ""));
     }
 
Index: /trunk/src/org/openstreetmap/josm/tools/Utils.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/tools/Utils.java	(revision 11329)
+++ /trunk/src/org/openstreetmap/josm/tools/Utils.java	(revision 11330)
@@ -1366,10 +1366,10 @@
      *
      * @param stream input stream
-     * @return byte array of data in input stream or null if stream is null
+     * @return byte array of data in input stream (empty if stream is null)
      * @throws IOException if any I/O error occurs
      */
     public static byte[] readBytesFromStream(InputStream stream) throws IOException {
         if (stream == null) {
-            return null;
+            return new byte[0];
         }
         try {
@@ -1386,10 +1386,8 @@
             } while (!finished);
             if (bout.size() == 0)
-                return null;
+                return new byte[0];
             return bout.toByteArray();
         } finally {
-            if (stream != null) {
-                stream.close();
-            }
+            stream.close();
         }
     }
Index: /trunk/test/unit/org/openstreetmap/josm/tools/UtilsTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/tools/UtilsTest.java	(revision 11329)
+++ /trunk/test/unit/org/openstreetmap/josm/tools/UtilsTest.java	(revision 11330)
@@ -3,5 +3,4 @@
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
 
 import java.io.IOException;
@@ -170,11 +169,9 @@
     /**
      * Tests if readBytesFromStream handles null streams (might happen when there is no data on error stream)
-     * @throws IOException
-     *
+     * @throws IOException in case of I/O error
      */
     @Test
     public void testNullStreamForReadBytesFromStream() throws IOException {
-        assertNull("Null on null stream", Utils.readBytesFromStream(null));
+        assertEquals("Empty on null stream", 0, Utils.readBytesFromStream(null).length);
     }
-
 }
