Index: trunk/src/org/openstreetmap/josm/corrector/ReverseWayTagCorrector.java
===================================================================
--- trunk/src/org/openstreetmap/josm/corrector/ReverseWayTagCorrector.java	(revision 1761)
+++ trunk/src/org/openstreetmap/josm/corrector/ReverseWayTagCorrector.java	(revision 1762)
@@ -44,6 +44,7 @@
         public String apply(String text) {
             Matcher m = startPattern.matcher(text);
-            if (!m.lookingAt())
+            if (!m.lookingAt()) {
                 m = endPattern.matcher(text);
+            }
 
             if (m.lookingAt()) {
@@ -62,14 +63,14 @@
 
     private static PrefixSuffixSwitcher[] prefixSuffixSwitchers =
-            new PrefixSuffixSwitcher[] {
-                new PrefixSuffixSwitcher("left", "right"),
-                new PrefixSuffixSwitcher("forward", "backward"),
-                new PrefixSuffixSwitcher("forwards", "backwards")
-            };
+        new PrefixSuffixSwitcher[] {
+        new PrefixSuffixSwitcher("left", "right"),
+        new PrefixSuffixSwitcher("forward", "backward"),
+        new PrefixSuffixSwitcher("forwards", "backwards")
+    };
 
     @Override
     public Collection<Command> execute(Way oldway, Way way) throws UserCancelException {
         Map<OsmPrimitive, List<TagCorrection>> tagCorrectionsMap =
-                new HashMap<OsmPrimitive, List<TagCorrection>>();
+            new HashMap<OsmPrimitive, List<TagCorrection>>();
 
         ArrayList<OsmPrimitive> primitives = new ArrayList<OsmPrimitive>();
@@ -86,7 +87,7 @@
 
                 if (key.equals("oneway")) {
-                    if (value.equals("-1"))
+                    if (value.equals("-1")) {
                         newValue = OsmUtils.trueval;
-                    else {
+                    } else {
                         Boolean boolValue = OsmUtils.getOsmBoolean(value);
                         if (boolValue != null && boolValue.booleanValue()) {
@@ -97,17 +98,19 @@
                     for (PrefixSuffixSwitcher prefixSuffixSwitcher : prefixSuffixSwitchers) {
                         newKey = prefixSuffixSwitcher.apply(key);
-                        if (!key.equals(newKey))
+                        if (!key.equals(newKey)) {
                             break;
+                        }
                     }
                 }
 
-                if (!key.equals(newKey) || !value.equals(newValue))
+                if (!key.equals(newKey) || !value.equals(newValue)) {
                     tagCorrectionsMap.get(primitive).add(
                             new TagCorrection(key, value, newKey, newValue));
+                }
             }
         }
 
         Map<OsmPrimitive, List<RoleCorrection>> roleCorrectionMap =
-                new HashMap<OsmPrimitive, List<RoleCorrection>>();
+            new HashMap<OsmPrimitive, List<RoleCorrection>>();
         roleCorrectionMap.put(way, new ArrayList<RoleCorrection>());
 
@@ -115,5 +118,5 @@
             int position = 0;
             for (RelationMember member : relation.members) {
-                if (!member.member.realEqual(oldway, true)
+                if (!member.member.hasEqualSemanticAttributes(oldway)
                         || member.role.length() == 0) {
                     position++;
@@ -131,7 +134,8 @@
                 }
 
-                if (found)
+                if (found) {
                     roleCorrectionMap.get(way).add(
                             new RoleCorrection(relation, position, member, newRole));
+                }
 
                 position++;
Index: trunk/src/org/openstreetmap/josm/data/osm/Node.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/Node.java	(revision 1761)
+++ trunk/src/org/openstreetmap/josm/data/osm/Node.java	(revision 1762)
@@ -107,22 +107,4 @@
     }
 
-    /**
-     * @deprecated
-     * @see #hasEqualSemanticAttributes(OsmPrimitive)
-     * @see #hasEqualTechnicalAttributes(OsmPrimitive)
-     */
-    @Deprecated
-    @Override public boolean realEqual(OsmPrimitive osm, boolean semanticOnly) {
-        if (osm instanceof Node) {
-            if (super.realEqual(osm, semanticOnly)) {
-                if ((coor == null) && ((Node)osm).coor == null)
-                    return true;
-                if (coor != null)
-                    return coor.equals(((Node)osm).coor);
-            }
-        }
-        return false;
-    }
-
     @Override
     public boolean hasEqualSemanticAttributes(OsmPrimitive other) {
Index: trunk/src/org/openstreetmap/josm/data/osm/OsmPrimitive.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/OsmPrimitive.java	(revision 1761)
+++ trunk/src/org/openstreetmap/josm/data/osm/OsmPrimitive.java	(revision 1762)
@@ -284,28 +284,4 @@
         clearCached();
         clearErrors();
-    }
-
-    /**
-     * Perform an equality compare for all non-volatile fields not only for the id
-     * but for the whole object (for conflict resolving)
-     * @param semanticOnly if <code>true</code>, modified flag and timestamp are not compared
-     * 
-     * @deprecated
-     * @see #hasEqualSemanticAttributes(OsmPrimitive)
-     * @see #hasEqualTechnicalAttributes(OsmPrimitive)
-     */
-    @Deprecated
-    public boolean realEqual(OsmPrimitive osm, boolean semanticOnly) {
-        return id == osm.id
-        && incomplete == osm.incomplete
-        && deleted == osm.deleted
-        && (semanticOnly || (
-                modified == osm.modified
-                && timestamp == osm.timestamp
-                && version == osm.version
-                && visible == osm.visible
-                && (user == null ? osm.user==null : user==osm.user))
-        )
-        && (keys == null ? osm.keys==null : keys.equals(osm.keys));
     }
 
Index: trunk/src/org/openstreetmap/josm/data/osm/Relation.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/Relation.java	(revision 1761)
+++ trunk/src/org/openstreetmap/josm/data/osm/Relation.java	(revision 1762)
@@ -68,9 +68,4 @@
         // adding members in string increases memory usage a lot and overflows for looped relations
         return "{Relation id="+id+" version="+version+"}";
-    }
-
-    @Deprecated
-    @Override public boolean realEqual(OsmPrimitive osm, boolean semanticOnly) {
-        return osm instanceof Relation ? super.realEqual(osm, semanticOnly) && members.equals(((Relation)osm).members) : false;
     }
 
Index: trunk/src/org/openstreetmap/josm/data/osm/Way.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/Way.java	(revision 1761)
+++ trunk/src/org/openstreetmap/josm/data/osm/Way.java	(revision 1762)
@@ -99,9 +99,4 @@
         if (incomplete) return "{Way id="+id+" version="+version+" (incomplete)}";
         return "{Way id="+id+" version="+version+" nodes="+Arrays.toString(nodes.toArray())+"}";
-    }
-
-    @Deprecated
-    @Override public boolean realEqual(OsmPrimitive osm, boolean semanticOnly) {
-        return osm instanceof Way ? super.realEqual(osm, semanticOnly) && nodes.equals(((Way)osm).nodes) : false;
     }
 
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/relation/GenericRelationEditor.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/relation/GenericRelationEditor.java	(revision 1761)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/relation/GenericRelationEditor.java	(revision 1762)
@@ -9,10 +9,13 @@
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
 import java.awt.event.KeyEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Map.Entry;
 
 import javax.swing.JLabel;
@@ -23,4 +26,5 @@
 import javax.swing.JTable;
 import javax.swing.ListSelectionModel;
+import javax.swing.SwingUtilities;
 import javax.swing.event.ListSelectionEvent;
 import javax.swing.event.ListSelectionListener;
@@ -44,4 +48,6 @@
 import org.openstreetmap.josm.gui.SideButton;
 import org.openstreetmap.josm.gui.dialogs.ConflictDialog;
+import org.openstreetmap.josm.gui.dialogs.relation.ac.AutoCompletionCache;
+import org.openstreetmap.josm.gui.dialogs.relation.ac.AutoCompletionList;
 import org.openstreetmap.josm.io.OsmServerObjectReader;
 import org.openstreetmap.josm.io.OsmTransferException;
@@ -94,17 +100,8 @@
 public class GenericRelationEditor extends RelationEditor {
 
+    // We need this twice, so cache result
+    protected final static String applyChangesText = tr("Apply Changes");
+
     private JLabel status;
-
-    /**
-     * The property data.
-     */
-    private final DefaultTableModel propertyData = new DefaultTableModel() {
-        @Override public boolean isCellEditable(int row, int column) {
-            return true;
-        }
-        @Override public Class<?> getColumnClass(int columnIndex) {
-            return String.class;
-        }
-    };
 
     /**
@@ -123,9 +120,12 @@
      * The properties and membership lists.
      */
-    private final JTable propertyTable = new JTable(propertyData);
     private final JTable memberTable = new JTable(memberData);
 
-    // We need this twice, so cache result
-    protected final static String applyChangesText = tr("Apply Changes");
+    /** the tag table and its model */
+    private TagEditorModel tagEditorModel;
+    private TagTable tagTable;
+    private AutoCompletionCache acCache;
+    private AutoCompletionList acList;
+
 
     /**
@@ -141,6 +141,42 @@
         // Initalizes ExtendedDialog
         super(relation, selectedMembers);
+        acCache = AutoCompletionCache.getCacheForLayer(Main.map.mapView.getEditLayer());
+        acList = new AutoCompletionList();
 
         JPanel bothTables = setupBasicLayout(selectedMembers);
+        if (relation != null) {
+            this.tagEditorModel.initFromPrimitive(relation);
+        } else {
+            tagEditorModel.clear();
+        }
+        tagEditorModel.ensureOneTag();
+        addWindowListener(
+                new WindowAdapter() {
+                    protected void requestFocusInTopLeftCell() {
+                        SwingUtilities.invokeLater(new Runnable(){
+                            public void run()
+                            {
+                                tagEditorModel.ensureOneTag();
+                                tagTable.requestFocusInCell(0, 0);
+                            }
+                        });
+                    }
+                    @Override public void windowGainedFocus(WindowEvent e) {
+                        requestFocusInTopLeftCell();
+                    }
+                    @Override
+                    public void windowActivated(WindowEvent e) {
+                        requestFocusInTopLeftCell();
+                    }
+                    @Override
+                    public void windowDeiconified(WindowEvent e) {
+                        requestFocusInTopLeftCell();
+                    }
+                    @Override
+                    public void windowOpened(WindowEvent e) {
+                        requestFocusInTopLeftCell();
+                    }
+                }
+        );
 
         JTabbedPane tabPane = new JTabbedPane();
@@ -165,27 +201,16 @@
      */
     private JPanel setupBasicLayout(Collection<RelationMember> selectedMembers) {
-        // setting up the properties table
-        propertyData.setColumnIdentifiers(new String[]{tr("Key"),tr("Value")});
-        propertyTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
-        propertyData.addTableModelListener(new TableModelListener() {
-            public void tableChanged(TableModelEvent tme) {
-                if (tme.getType() == TableModelEvent.UPDATE) {
-                    int row = tme.getFirstRow();
-
-                    if (!(tme.getColumn() == 0 && row == propertyData.getRowCount() -1)) {
-                        clone.entrySet().clear();
-                        for (int i = 0; i < propertyData.getRowCount(); i++) {
-                            String key = propertyData.getValueAt(i, 0).toString();
-                            String value = propertyData.getValueAt(i, 1).toString();
-                            if (key.length() > 0 && value.length() > 0) {
-                                clone.put(key, value);
-                            }
-                        }
-                        refreshTables();
-                    }
-                }
-            }
-        });
-        propertyTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
+        // setting up the tag table
+        //
+        tagEditorModel = new TagEditorModel();
+        tagTable = new TagTable(tagEditorModel);
+        acCache.initFromJOSMDataset();
+        TagCellEditor editor = ((TagCellEditor)tagTable.getColumnModel().getColumn(0).getCellEditor());
+        editor.setAutoCompletionCache(acCache);
+        editor.setAutoCompletionList(acList);
+        editor = ((TagCellEditor)tagTable.getColumnModel().getColumn(1).getCellEditor());
+        editor.setAutoCompletionCache(acCache);
+        editor.setAutoCompletionList(acList);
+
 
         // setting up the member table
@@ -198,5 +223,5 @@
                 if (tme.getType() == TableModelEvent.UPDATE && tme.getColumn() == 0) {
                     int row = tme.getFirstRow();
-                    clone.members.get(row).role = memberData.getValueAt(row, 0).toString();
+                    getClone().members.get(row).role = memberData.getValueAt(row, 0).toString();
                 }
             }
@@ -230,6 +255,21 @@
         JPanel bothTables = new JPanel();
         bothTables.setLayout(new GridBagLayout());
-        bothTables.add(new JLabel(tr("Tags (empty value deletes tag)")), GBC.eol().fill(GBC.HORIZONTAL));
-        bothTables.add(new JScrollPane(propertyTable), GBC.eop().fill(GBC.BOTH));
+        bothTables.add(new JLabel(tr("Tags")), GBC.eol().fill(GBC.HORIZONTAL));
+        final JScrollPane scrollPane = new JScrollPane(tagTable);
+
+        // this adapters ensures that the width of the tag table columns is adjusted
+        // to the width of the scroll pane viewport. Also tried to overwrite
+        // getPreferredViewportSize() in JTable, but did not work.
+        //
+        scrollPane.addComponentListener(
+                new ComponentAdapter() {
+                    @Override public void componentResized(ComponentEvent e) {
+                        super.componentResized(e);
+                        Dimension d = scrollPane.getViewport().getExtentSize();
+                        tagTable.adjustColumnWidth(d.width);
+                    }
+                }
+        );
+        bothTables.add(scrollPane, GBC.eop().fill(GBC.BOTH));
         bothTables.add(status = new JLabel(tr("Members")), GBC.eol().fill(GBC.HORIZONTAL));
         // this is not exactly pretty but the four buttons simply don't fit in one line.
@@ -310,5 +350,5 @@
                     mem.role = memberTable.getValueAt(row, 0).toString();
                     mem.member = (OsmPrimitive) memberTable.getValueAt(row, 1);
-                    clone.members.remove(mem);
+                    getClone().members.remove(mem);
                 }
                 refreshTables();
@@ -337,7 +377,7 @@
         // TODO: sort only selected rows
 
-        for (i = 1; i < clone.members.size(); ++i)
+        for (i = 1; i < getClone().members.size(); ++i)
         {
-            RelationMember  m = clone.members.get(i);
+            RelationMember  m = getClone().members.get(i);
             if (m.member.incomplete)
                 // TODO: emit some message that sorting failed
@@ -373,7 +413,7 @@
         }
 
-        for (i = 0; i < clone.members.size(); ++i)
+        for (i = 0; i < getClone().members.size(); ++i)
         {
-            RelationMember  m = clone.members.get(i);
+            RelationMember  m = getClone().members.get(i);
             Integer         m2 = null;
             Node            searchNode = null;
@@ -410,5 +450,5 @@
                 {
                     m2 = points.get(searchNode).first();
-                    if (m.member == clone.members.get(m2).member)
+                    if (m.member == getClone().members.get(m2).member)
                     {
                         m2 = points.get(searchNode).last();
@@ -418,5 +458,5 @@
             catch(java.util.NoSuchElementException e) {}
 
-            if ((m2 == null) && ((i+1) < clone.members.size()))
+            if ((m2 == null) && ((i+1) < getClone().members.size()))
             {
                 // TODO: emit some message that sorting failed
@@ -429,5 +469,5 @@
                 try
                 {
-                    Way next = (Way)clone.members.get(m2).member;
+                    Way next = (Way)getClone().members.get(m2).member;
                     lastWayStartUsed = searchNode.equals(next.firstNode());
                 }
@@ -436,13 +476,13 @@
                 }
 
-                if ((m2 < clone.members.size()) && ((i+1) < clone.members.size()))
-                {
-                    RelationMember  a = clone.members.get(i+1);
-                    RelationMember  b = clone.members.get(m2);
+                if ((m2 < getClone().members.size()) && ((i+1) < getClone().members.size()))
+                {
+                    RelationMember  a = getClone().members.get(i+1);
+                    RelationMember  b = getClone().members.get(m2);
 
                     if (m2 != (i+1))
                     {
-                        clone.members.set(i+1, b);
-                        clone.members.set(m2, a);
+                        getClone().members.set(i+1, b);
+                        getClone().members.set(m2, a);
 
                         try
@@ -499,13 +539,15 @@
      */
     private void applyChanges() {
-        if (GenericRelationEditor.this.relation == null) {
+        if (getRelation()== null) {
             // If the user wanted to create a new relation, but hasn't added any members or
             // tags, don't add an empty relation
-            if(clone.members.size() == 0 && !clone.isTagged())
+            if(getClone().members.size() == 0 && tagEditorModel.getKeys().isEmpty())
                 return;
-            Main.main.undoRedo.add(new AddCommand(clone));
+            tagEditorModel.applyToPrimitive(getClone());
+            Main.main.undoRedo.add(new AddCommand(getClone()));
             DataSet.fireSelectionChanged(Main.ds.getSelected());
-        } else if (!GenericRelationEditor.this.relation.realEqual(clone, true)) {
-            Main.main.undoRedo.add(new ChangeCommand(GenericRelationEditor.this.relation, clone));
+        } else if (getRelation().hasEqualSemanticAttributes(getClone())) {
+            tagEditorModel.applyToPrimitive(getClone());
+            Main.main.undoRedo.add(new ChangeCommand(getRelation(), getClone()));
             DataSet.fireSelectionChanged(Main.ds.getSelected());
         }
@@ -532,14 +574,8 @@
         int numLinked = 0;
 
-        propertyData.setRowCount(0);
-        for (Entry<String, String> e : clone.entrySet()) {
-            propertyData.addRow(new Object[]{e.getKey(), e.getValue()});
-        }
-        propertyData.addRow(new Object[]{"", ""});
-
         // re-load membership data
 
         memberData.setRowCount(0);
-        for (int i=0; i<clone.members.size(); i++) {
+        for (int i=0; i<getClone().members.size(); i++) {
 
             // this whole section is aimed at finding out whether the
@@ -550,5 +586,5 @@
             // it should cache results, so... FIXME ;-)
 
-            RelationMember em = clone.members.get(i);
+            RelationMember em = getClone().members.get(i);
             WayConnectionType link = WayConnectionType.none;
             RelationMember m = em;
@@ -562,5 +598,5 @@
                     break;
                 } else if (m.member instanceof Relation) {
-                    if (m.member == this.relation) {
+                    if (m.member == this.getRelation()) {
                         break;
                     }
@@ -572,8 +608,8 @@
             }
             if (way1 != null) {
-                int next = (i+1) % clone.members.size();
+                int next = (i+1) % getClone().members.size();
                 while (next != i) {
-                    m = clone.members.get(next);
-                    next = (next + 1) % clone.members.size();
+                    m = getClone().members.get(next);
+                    next = (next + 1) % getClone().members.size();
                     depth = 0;
                     while (m != null && depth < 10) {
@@ -582,5 +618,5 @@
                             break;
                         } else if (m.member instanceof Relation) {
-                            if (m.member == this.relation) {
+                            if (m.member == this.getRelation()) {
                                 break;
                             }
@@ -631,5 +667,5 @@
             memberData.addRow(new Object[]{em.role, em.member, link});
         }
-        status.setText(tr("Members: {0} (linked: {1})", clone.members.size(), numLinked));
+        status.setText(tr("Members: {0} (linked: {1})", getClone().members.size(), numLinked));
     }
 
@@ -657,7 +693,7 @@
             int[] rows = memberTable.getSelectedRows();
             if (rows.length > 0) {
-                clone.members.add(rows[0], em);
+                getClone().members.add(rows[0], em);
             } else {
-                clone.members.add(em);
+                getClone().members.add(em);
             }
         }
@@ -667,5 +703,5 @@
     private void deleteSelected() {
         for (OsmPrimitive p : Main.ds.getSelected()) {
-            Relation c = new Relation(clone);
+            Relation c = new Relation(getClone());
             for (RelationMember rm : c.members) {
                 if (rm.member == p)
@@ -674,5 +710,5 @@
                     mem.role = rm.role;
                     mem.member = rm.member;
-                    clone.members.remove(mem);
+                    getClone().members.remove(mem);
                 }
             }
@@ -687,13 +723,13 @@
         // check if user attempted to move anything beyond the boundary of the list
         if (rows[0] + direction < 0) return;
-        if (rows[rows.length-1] + direction >= clone.members.size()) return;
-
-        RelationMember m[] = new RelationMember[clone.members.size()];
+        if (rows[rows.length-1] + direction >= getClone().members.size()) return;
+
+        RelationMember m[] = new RelationMember[getClone().members.size()];
 
         // first move all selected rows from the member list into a new array,
         // displaced by the move amount
         for (Integer i: rows) {
-            m[i+direction] = clone.members.get(i);
-            clone.members.set(i, null);
+            m[i+direction] = getClone().members.get(i);
+            getClone().members.set(i, null);
         }
 
@@ -701,5 +737,5 @@
         // elements.
         int i = 0;
-        for (RelationMember rm : clone.members) {
+        for (RelationMember rm : getClone().members) {
             if (rm != null) {
                 while (m[i] != null) {
@@ -711,6 +747,6 @@
 
         // and write the array back into the member list.
-        clone.members.clear();
-        clone.members.addAll(Arrays.asList(m));
+        getClone().members.clear();
+        getClone().members.addAll(Arrays.asList(m));
         refreshTables();
         ListSelectionModel lsm = memberTable.getSelectionModel();
@@ -724,5 +760,5 @@
     private void downloadRelationMembers() {
         boolean download = false;
-        for (RelationMember member : clone.members) {
+        for (RelationMember member : getClone().members) {
             if (member.member.incomplete) {
                 download = true;
@@ -731,5 +767,5 @@
         }
         if (download) {
-            OsmServerObjectReader reader = new OsmServerObjectReader(clone.id, OsmPrimitiveType.RELATION, true);
+            OsmServerObjectReader reader = new OsmServerObjectReader(getClone().id, OsmPrimitiveType.RELATION, true);
             try {
                 DataSet dataSet = reader.parseOsm();
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/relation/RelationDialogManager.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/relation/RelationDialogManager.java	(revision 1762)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/relation/RelationDialogManager.java	(revision 1762)
@@ -0,0 +1,49 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.dialogs.relation;
+
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.util.HashMap;
+
+import org.openstreetmap.josm.data.osm.Relation;
+
+/**
+ * RelationDialogManager keeps track of the open relation editors.
+ *
+ */
+public class RelationDialogManager extends WindowAdapter {
+
+    private HashMap<Relation, RelationEditor> openDialogs;
+
+    public RelationDialogManager(){
+        openDialogs = new HashMap<Relation, RelationEditor>();
+    }
+
+    public void register(Relation relation, RelationEditor editor) {
+        openDialogs.put(relation, editor);
+        editor.addWindowListener(this);
+    }
+
+    public boolean isOpenInEditor(Relation relation) {
+        return openDialogs.keySet().contains(relation);
+    }
+
+    public RelationEditor getEditorForRelation(Relation relation) {
+        return openDialogs.get(relation);
+    }
+
+    @Override
+    public void windowClosed(WindowEvent e) {
+        RelationEditor editor = ((RelationEditor)e.getWindow());
+        Relation editedRelation = null;
+        for (Relation r : openDialogs.keySet()) {
+            if (openDialogs.get(r).equals(editor)) {
+                editedRelation = r;
+                break;
+            }
+        }
+        if (editedRelation != null) {
+            openDialogs.remove(editedRelation);
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/relation/RelationEditor.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/relation/RelationEditor.java	(revision 1761)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/relation/RelationEditor.java	(revision 1762)
@@ -16,4 +16,19 @@
 public abstract class RelationEditor extends ExtendedDialog {
 
+    /** keeps track of open relation editors */
+    static private RelationDialogManager relationDialogManager;
+
+    /**
+     * Replies the singleton {@see RelationDialogManager}
+     * 
+     * @return the singleton {@see RelationDialogManager}
+     */
+    static public RelationDialogManager getRelationDialogManager() {
+        if (relationDialogManager == null) {
+            relationDialogManager = new RelationDialogManager();
+        }
+        return relationDialogManager;
+    }
+
     public static ArrayList<Class<RelationEditor>> editors = new ArrayList<Class<RelationEditor>>();
 
@@ -22,6 +37,6 @@
      * editing.
      */
-    protected Relation relation;
-    protected Relation clone;
+    private Relation relation;
+    private Relation clone;
 
     /**
@@ -57,5 +72,11 @@
             }
         }
-        return new GenericRelationEditor(r, selectedMembers);
+        if (getRelationDialogManager().isOpenInEditor(r))
+            return getRelationDialogManager().getEditorForRelation(r);
+        else {
+            RelationEditor editor = new GenericRelationEditor(r, selectedMembers);
+            getRelationDialogManager().register(r, editor);
+            return editor;
+        }
     }
 
@@ -84,3 +105,11 @@
         }
     }
+
+    protected Relation getRelation() {
+        return relation;
+    }
+
+    protected Relation getClone() {
+        return clone;
+    }
 }
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/relation/RunnableAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/relation/RunnableAction.java	(revision 1762)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/relation/RunnableAction.java	(revision 1762)
@@ -0,0 +1,17 @@
+package org.openstreetmap.josm.gui.dialogs.relation;
+
+import java.awt.event.ActionEvent;
+
+import javax.swing.AbstractAction;
+
+public abstract class RunnableAction extends AbstractAction implements Runnable {
+
+	public RunnableAction() {
+	}
+
+	public abstract void run();
+
+	public void actionPerformed(ActionEvent arg0) {
+		run();
+	}
+}
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/relation/TagCellEditor.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/relation/TagCellEditor.java	(revision 1762)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/relation/TagCellEditor.java	(revision 1762)
@@ -0,0 +1,207 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.dialogs.relation;
+
+import java.awt.Component;
+import java.util.logging.Logger;
+
+import javax.swing.AbstractCellEditor;
+import javax.swing.JTable;
+import javax.swing.table.TableCellEditor;
+
+import org.openstreetmap.josm.gui.dialogs.relation.ac.AutoCompletionCache;
+import org.openstreetmap.josm.gui.dialogs.relation.ac.AutoCompletionItemPritority;
+import org.openstreetmap.josm.gui.dialogs.relation.ac.AutoCompletionList;
+import org.openstreetmap.josm.gui.dialogs.relation.ac.AutoCompletionListItem;
+
+/**
+ * This is the table cell editor for the tag editor dialog.
+ * 
+ */
+@SuppressWarnings("serial")
+public class TagCellEditor extends AbstractCellEditor implements TableCellEditor{
+
+    /** the logger object */
+    static private Logger logger = Logger.getLogger(TagCellEditor.class.getName());
+
+    private TagFieldEditor editor = null;
+    private TagModel currentTag = null;
+    private TagEditorModel tagEditorModel = null;
+    private int currentColumn = 0;
+
+    /** the cache of auto completion items derived from the current JOSM data set */
+    private AutoCompletionCache acCache = null;
+
+    /** user input is matched against this list of auto completion items */
+    private AutoCompletionList autoCompletionList = null;
+
+    /**
+     * constructor
+     */
+    public TagCellEditor() {
+        editor = new TagFieldEditor();
+        acCache = new AutoCompletionCache();
+    }
+
+    /**
+     * initializes  the auto completion list when the table cell editor starts
+     * to edit the key of a tag. In this case the auto completion list is
+     * initialized with the set of standard key values and the set of current key
+     * values from the the current JOSM data set. Keys already present in the
+     * current tag model are removed from the auto completion list.
+     * 
+     * @param model  the tag editor model
+     * @param currentTag  the current tag
+     */
+    protected void initAutoCompletionListForKeys(TagEditorModel model, TagModel currentTag) {
+
+        if (autoCompletionList == null) {
+            logger.warning("autoCompletionList is null. Make sure an instance of AutoCompletionList is injected into TableCellEditor.");
+            return;
+        }
+
+        autoCompletionList.clear();
+
+        // add the list of keys in the current data set
+        //
+        for (String key : acCache.getKeys()) {
+            autoCompletionList.add(
+                    new AutoCompletionListItem(key, AutoCompletionItemPritority.IS_IN_DATASET)
+            );
+        }
+
+        // remove the keys already present in the current tag model
+        //
+        for (String key : model.getKeys()) {
+            if (! key.equals(currentTag.getName())) {
+                autoCompletionList.remove(key);
+            }
+        }
+        autoCompletionList.fireTableDataChanged();
+    }
+
+
+    /**
+     * initializes the auto completion list when the cell editor starts to edit
+     * a tag value. In this case the auto completion list is initialized with the
+     * set of standard values for a given key and the set of values present in the
+     * current data set for the given key.
+     * 
+     * @param forKey the key
+     */
+    protected void initAutoCompletionListForValues(String forKey) {
+
+        if (autoCompletionList == null) {
+            logger.warning("autoCompletionList is null. Make sure an instance of AutoCompletionList is injected into TableCellEditor.");
+            return;
+        }
+        autoCompletionList.clear();
+
+        for (String value : acCache.getValues(forKey)) {
+            autoCompletionList.add(
+                    new AutoCompletionListItem(value, AutoCompletionItemPritority.IS_IN_DATASET)
+            );
+        }
+    }
+
+
+    /**
+     * replies the table cell editor
+     */
+    public Component getTableCellEditorComponent(JTable table,
+            Object value, boolean isSelected, int row, int column) {
+        currentTag = (TagModel) value;
+
+        if (column == 0) {
+            editor.setText(currentTag.getName());
+            currentColumn = 0;
+            TagEditorModel model = (TagEditorModel)table.getModel();
+            initAutoCompletionListForKeys(model, currentTag);
+            return editor;
+        } else if (column == 1) {
+
+            if (currentTag.getValueCount() == 0) {
+                editor.setText("");
+            } else if (currentTag.getValueCount() == 1) {
+                editor.setText(currentTag.getValues().get(0));
+            } else {
+                editor.setText("");
+            }
+            currentColumn = 1;
+            initAutoCompletionListForValues(currentTag.getName());
+            return editor;
+        } else {
+            logger.warning("column this table cell editor is requested for is out of range. column=" + column);
+            return null;
+        }
+    }
+
+    public Object getCellEditorValue() {
+        return editor.getText();
+    }
+
+    @Override
+    public void cancelCellEditing() {
+        super.cancelCellEditing();
+    }
+
+    @Override
+    public boolean stopCellEditing() {
+        if (tagEditorModel == null) {
+            logger.warning("no tag editor model set. Can't update edited values. Please set tag editor model first");
+            return super.stopCellEditing();
+        }
+
+        if (currentColumn == 0) {
+            tagEditorModel.updateTagName(currentTag, editor.getText());
+        } else if (currentColumn == 1){
+            if (currentTag.getValueCount() > 1 && ! editor.getText().equals("")) {
+                tagEditorModel.updateTagValue(currentTag, editor.getText());
+            } else if (currentTag.getValueCount() <= 1) {
+                tagEditorModel.updateTagValue(currentTag, editor.getText());
+            }
+        }
+
+        return super.stopCellEditing();
+    }
+
+    /**
+     * replies the {@link AutoCompletionList} this table cell editor synchronizes with
+     * 
+     * @return the auto completion list
+     */
+    public AutoCompletionList getAutoCompletionList() {
+        return autoCompletionList;
+    }
+
+    /**
+     * sets the {@link AutoCompletionList} this table cell editor synchronizes with
+     * @param autoCompletionList the auto completion list
+     */
+    public void setAutoCompletionList(AutoCompletionList autoCompletionList) {
+        this.autoCompletionList = autoCompletionList;
+        editor.setAutoCompletionList(autoCompletionList);
+    }
+
+    public void setAutoCompletionCache(AutoCompletionCache acCache) {
+        this.acCache = acCache;
+    }
+
+    public void autoCompletionItemSelected(String item) {
+        editor.setText(item);
+        editor.selectAll();
+        editor.requestFocus();
+    }
+
+    public TagFieldEditor getEditor() {
+        return editor;
+    }
+
+    /**
+     * sets the tag editor model
+     * 
+     * @param tagEditorModel  the tag editor model
+     */
+    public void setTagEditorModel(TagEditorModel tagEditorModel) {
+        this.tagEditorModel = tagEditorModel;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/relation/TagCellRenderer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/relation/TagCellRenderer.java	(revision 1762)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/relation/TagCellRenderer.java	(revision 1762)
@@ -0,0 +1,146 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.dialogs.relation;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Font;
+import java.util.logging.Logger;
+
+import javax.swing.BorderFactory;
+import javax.swing.ImageIcon;
+import javax.swing.JLabel;
+import javax.swing.JTable;
+import javax.swing.border.Border;
+import javax.swing.border.EmptyBorder;
+import javax.swing.table.TableCellRenderer;
+
+
+/**
+ * This is the table cell renderer for cells for the table of tags
+ * in the tag editor dialog.
+ * 
+ *
+ */
+public class TagCellRenderer extends JLabel implements TableCellRenderer  {
+
+    private static Logger logger = Logger.getLogger(TagCellRenderer.class.getName());
+
+    public static final Color BG_COLOR_SELECTED = new Color(143,170,255);
+    public static final Color BG_COLOR_HIGHLIGHTED = new Color(255,255,204);
+
+    public static final Border BORDER_EMPHASIZED = BorderFactory.createLineBorder(new Color(253,75,45));
+
+    /** the icon displayed for deleting a tag */
+    private ImageIcon deleteIcon = null;
+
+    private Font fontStandard = null;
+    private Font fontItalic = null;
+
+    public TagCellRenderer() {
+        fontStandard = getFont();
+        fontItalic = fontStandard.deriveFont(Font.ITALIC);
+        setOpaque(true);
+        setBorder(new EmptyBorder(5,5,5,5));
+    }
+
+    /**
+     * renders the name of a tag in the second column of
+     * the table
+     * 
+     * @param tag  the tag
+     */
+    protected void renderTagName(TagModel tag) {
+        setText(tag.getName());
+    }
+
+    /**
+     * renders the value of a a tag in the third column of
+     * the table
+     * 
+     * @param tag  the  tag
+     */
+    protected void renderTagValue(TagModel tag) {
+        if (tag.getValueCount() == 0) {
+            setText("");
+        } else if (tag.getValueCount() == 1) {
+            setText(tag.getValues().get(0));
+        } else if (tag.getValueCount() >  1) {
+            setText(tr("<multiple>"));
+            setFont(fontItalic);
+        }
+    }
+
+
+
+    /**
+     * resets the renderer
+     */
+    protected void resetRenderer() {
+        setText("");
+        setIcon(null);
+        setFont(fontStandard);
+    }
+
+    protected TagEditorModel getModel(JTable table) {
+        return (TagEditorModel)table.getModel();
+    }
+
+    /**
+     * renders the background color. The default color is white. It is
+     * set to {@see TableCellRenderer#BG_COLOR_HIGHLIGHTED} if this cell
+     * displays the tag which is suggested by the currently selected
+     * preset.
+     * 
+     * @param tagModel the tag model
+     * @param model the tag editor model
+     */
+    protected void renderBackgroundColor(TagModel tagModel, TagEditorModel model) {
+        setBackground(Color.WHITE); // standard color
+    }
+
+
+    /**
+     * replies the cell renderer component for a specific cell
+     * 
+     * @param table  the table
+     * @param value the value to be rendered
+     * @param isSelected  true, if the value is selected
+     * @param hasFocus true, if the cell has focus
+     * @param rowIndex the row index
+     * @param vColIndex the column index
+     * 
+     * @return the renderer component
+     */
+    public Component getTableCellRendererComponent(JTable table, Object value,
+            boolean isSelected, boolean hasFocus, int rowIndex, int vColIndex) {
+        resetRenderer();
+
+        // set background color
+        //
+        if (isSelected){
+            setBackground(BG_COLOR_SELECTED);
+        } else {
+            renderBackgroundColor(getModel(table).get(rowIndex), getModel(table));
+        }
+
+
+        switch(vColIndex) {
+        case 0: renderTagName((TagModel)value); break;
+        case 1: renderTagValue((TagModel)value); break;
+
+        default: throw new RuntimeException("unexpected index in switch statement");
+        }
+        if (hasFocus && isSelected) {
+            if (table.getSelectedColumnCount() == 1 && table.getSelectedRowCount() == 1) {
+                boolean success = table.editCellAt(rowIndex, vColIndex);
+
+                if (table.getEditorComponent() != null) {
+                    table.getEditorComponent().requestFocusInWindow();
+                }
+            }
+        }
+        return this;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/relation/TagEditorModel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/relation/TagEditorModel.java	(revision 1762)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/relation/TagEditorModel.java	(revision 1762)
@@ -0,0 +1,459 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.dialogs.relation;
+
+import static org.openstreetmap.josm.tools.I18n.trn;
+
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.logging.Logger;
+
+import javax.swing.table.AbstractTableModel;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.command.ChangePropertyCommand;
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.command.SequenceCommand;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+
+
+/**
+ * TagEditorModel is a table model.
+ * 
+ * 
+ * @author gubaer
+ *
+ */
+@SuppressWarnings("serial")
+public class TagEditorModel extends AbstractTableModel {
+    static private final Logger logger = Logger.getLogger(TagEditorModel.class.getName());
+
+    static public final String PROP_DIRTY = TagEditorModel.class.getName() + ".dirty";
+
+    /** the list holding the tags */
+    private  ArrayList<TagModel> tags = null;
+
+    /** indicates whether the model is dirty */
+    private boolean dirty =  false;
+    private PropertyChangeSupport propChangeSupport = null;
+
+    /**
+     * constructor
+     */
+    public TagEditorModel(){
+        tags = new ArrayList<TagModel>();
+        propChangeSupport = new PropertyChangeSupport(this);
+    }
+
+    public void addPropertyChangeListener(PropertyChangeListener listener) {
+        propChangeSupport.addPropertyChangeListener(listener);
+    }
+
+    public void removeProperyChangeListener(PropertyChangeListener listener) {
+        propChangeSupport.removePropertyChangeListener(listener);
+    }
+
+    protected void fireDirtyStateChanged(final boolean oldValue, final boolean newValue) {
+        propChangeSupport.firePropertyChange(PROP_DIRTY, oldValue, newValue);
+    }
+
+    protected void setDirty(boolean newValue) {
+        boolean oldValue = dirty;
+        dirty = newValue;
+        if (oldValue != newValue) {
+            fireDirtyStateChanged(oldValue, newValue);
+        }
+    }
+
+    public int getColumnCount() {
+        return 2;
+    }
+
+    public int getRowCount() {
+        return tags.size();
+    }
+
+    public Object getValueAt(int rowIndex, int columnIndex) {
+        if (rowIndex >= getRowCount())
+            throw new IndexOutOfBoundsException("unexpected rowIndex: rowIndex=" + rowIndex);
+
+        TagModel tag = tags.get(rowIndex);
+        switch(columnIndex) {
+        case 0:
+        case 1: return tag;
+
+        default:
+            throw new IndexOutOfBoundsException("unexpected columnIndex: columnIndex=" + columnIndex);
+        }
+    }
+
+
+    /**
+     * removes all tags in the model
+     */
+    public void clear() {
+        tags.clear();
+        setDirty(true);
+        fireTableDataChanged();
+    }
+
+    /**
+     * adds a tag to the model
+     * 
+     * @param tag the tag. Must not be null.
+     * 
+     * @exception IllegalArgumentException thrown, if tag is null
+     */
+    public void add(TagModel tag) {
+        if (tag == null)
+            throw new IllegalArgumentException("argument 'tag' must not be null");
+        tags.add(tag);
+        setDirty(true);
+        fireTableDataChanged();
+    }
+
+
+    public void prepend(TagModel tag) {
+        if (tag == null)
+            throw new IllegalArgumentException("argument 'tag' must not be null");
+        tags.add(0, tag);
+        setDirty(true);
+        fireTableDataChanged();
+    }
+
+
+    /**
+     * adds a tag given by a name/value pair to the tag editor model.
+     * 
+     * If there is no tag with name <code>name</name> yet, a new {@link TagModel} is created
+     * and append to this model.
+     * 
+     * If there is a tag with name <code>name</name>, <code>value</code> is merged to the list
+     * of values for this tag.
+     * 
+     * @param name the name; converted to "" if null
+     * @param value the value; converted to "" if null
+     */
+    public void add(String name, String value) {
+        name = (name == null) ? "" : name;
+        value = (value == null) ? "" : value;
+
+        TagModel tag = get(name);
+        if (tag == null) {
+            tag = new TagModel(name, value);
+            add(tag);
+        } else {
+            tag.addValue(value);
+        }
+        setDirty(true);
+    }
+
+
+    /**
+     * replies the tag with name <code>name</code>; null, if no such tag exists
+     * @param name the tag name
+     * @return the tag with name <code>name</code>; null, if no such tag exists
+     */
+    public TagModel get(String name) {
+        name = (name == null) ? "" : name;
+        for (TagModel tag : tags) {
+            if (tag.getName().equals(name))
+                return tag;
+        }
+        return null;
+    }
+
+    public TagModel get(int idx) {
+        TagModel tagModel = tags.get(idx);
+        return tagModel;
+    }
+
+
+
+    @Override public boolean isCellEditable(int row, int col) {
+        // all cells are editable
+        return true;
+    }
+
+
+    /**
+     * deletes the names of the tags given by tagIndices
+     * 
+     * @param tagIndices a list of tag indices
+     */
+    public void deleteTagNames(int [] tagIndices) {
+        if (tags == null)
+            return;
+        for (int tagIdx : tagIndices) {
+            TagModel tag = tags.get(tagIdx);
+            if (tag != null) {
+                tag.setName("");
+            }
+        }
+        fireTableDataChanged();
+        setDirty(true);
+    }
+
+    /**
+     * deletes the values of the tags given by tagIndices
+     * 
+     * @param tagIndices the lit of tag indices
+     */
+    public void deleteTagValues(int [] tagIndices) {
+        if (tags == null)
+            return;
+        for (int tagIdx : tagIndices) {
+            TagModel tag = tags.get(tagIdx);
+            if (tag != null) {
+                tag.setValue("");
+            }
+        }
+        fireTableDataChanged();
+        setDirty(true);
+    }
+
+    /**
+     * deletes the tags given by tagIndices
+     * 
+     * @param tagIndices the list of tag indices
+     */
+    public void deleteTags(int [] tagIndices) {
+        if (tags == null)
+            return;
+        ArrayList<TagModel> toDelete = new ArrayList<TagModel>();
+        for (int tagIdx : tagIndices) {
+            TagModel tag = tags.get(tagIdx);
+            if (tag != null) {
+                toDelete.add(tag);
+            }
+        }
+        for (TagModel tag : toDelete) {
+            tags.remove(tag);
+        }
+        fireTableDataChanged();
+        setDirty(true);
+    }
+
+
+    /**
+     * creates a new tag and appends it to the model
+     */
+    public void appendNewTag() {
+        TagModel tag = new TagModel();
+        tags.add(tag);
+        fireTableDataChanged();
+        setDirty(true);
+    }
+
+    /**
+     * makes sure the model includes at least one (empty) tag
+     */
+    public void ensureOneTag() {
+        if (tags.size() == 0) {
+            appendNewTag();
+        }
+    }
+
+    /**
+     * initializes the model with the tags in the current JOSM selection
+     */
+    public void initFromJOSMSelection() {
+        Collection<OsmPrimitive> selection = Main.ds.getSelected();
+        clear();
+        for (OsmPrimitive element : selection) {
+            for (String key : element.keySet()) {
+                String value = element.get(key);
+                add(key,value);
+            }
+        }
+        sort();
+        setDirty(false);
+    }
+
+    /**
+     * initializes the model with the tags of an OSM primitive
+     * 
+     * @param primitive the OSM primitive
+     */
+    public void initFromPrimitive(OsmPrimitive primitive) {
+        clear();
+        for (String key : primitive.keySet()) {
+            String value = primitive.get(key);
+            add(key,value);
+        }
+        sort();
+        setDirty(false);
+    }
+
+
+    public void applyToPrimitive(OsmPrimitive primitive) {
+        for (TagModel tag: tags) {
+            // tag still holds an unchanged list of different values for the same key.
+            // no property change command required
+            if (tag.getValueCount() > 1) {
+                continue;
+            }
+
+            // tag name holds an empty key. Don't apply it to the selection.
+            //
+            if (tag.getName().trim().equals("")) {
+                continue;
+            }
+            primitive.put(tag.getName(), tag.getValue());
+        }
+    }
+
+    /**
+     * checks whether the tag model includes a tag with a given key
+     * 
+     * @param key  the key
+     * @return true, if the tag model includes the tag; false, otherwise
+     */
+    public boolean includesTag(String key) {
+        if (key == null) return false;
+        for (TagModel tag : tags) {
+            if (tag.getName().equals(key))
+                return true;
+        }
+        return false;
+    }
+
+
+    protected Command createUpdateTagCommand(Collection<OsmPrimitive> primitives, TagModel tag) {
+
+        // tag still holds an unchanged list of different values for the same key.
+        // no property change command required
+        if (tag.getValueCount() > 1)
+            return null;
+
+        // tag name holds an empty key. Don't apply it to the selection.
+        //
+        if (tag.getName().trim().equals(""))
+            return null;
+
+        String newkey = tag.getName();
+        String newvalue = tag.getValue();
+
+        ChangePropertyCommand command = new ChangePropertyCommand(primitives,newkey, newvalue);
+        return command;
+    }
+
+    protected Command createDeleteTagsCommand(Collection<OsmPrimitive> primitives) {
+
+        List<String> currentkeys = getKeys();
+        ArrayList<Command> commands = new ArrayList<Command>();
+
+        for (OsmPrimitive primitive : primitives) {
+            if (primitive.keys == null) {
+                continue;
+            }
+            for (String oldkey : primitive.keys.keySet()) {
+                if (!currentkeys.contains(oldkey)) {
+                    ChangePropertyCommand deleteCommand =
+                        new ChangePropertyCommand(primitive,oldkey,null);
+                    commands.add(deleteCommand);
+                }
+            }
+        }
+
+        SequenceCommand command = new SequenceCommand(
+                trn("Remove old keys from up to {0} object", "Remove old keys from up to {0} objects", primitives.size(), primitives.size()),
+                commands
+        );
+
+        return command;
+    }
+
+    /**
+     * updates the tags of the primitives in the current selection with the
+     * values in the current tag model
+     * 
+     */
+    public void updateJOSMSelection() {
+        ArrayList<Command> commands = new ArrayList<Command>();
+        Collection<OsmPrimitive> selection = Main.ds.getSelected();
+        if (selection == null)
+            return;
+        for (TagModel tag : tags) {
+            Command command = createUpdateTagCommand(selection,tag);
+            if (command != null) {
+                commands.add(command);
+            }
+        }
+        Command deleteCommand = createDeleteTagsCommand(selection);
+        if (deleteCommand != null) {
+            commands.add(deleteCommand);
+        }
+
+        SequenceCommand command = new SequenceCommand(
+                trn("Updating properties of up to {0} object", "Updating properties of up to {0} objects", selection.size(), selection.size()),
+                commands
+        );
+
+        // executes the commands and adds them to the undo/redo chains
+        Main.main.undoRedo.add(command);
+    }
+
+
+    /**
+     * replies the list of keys of the tags managed by this model
+     * 
+     * @return the list of keys managed by this model
+     */
+    public List<String> getKeys() {
+        ArrayList<String> keys = new ArrayList<String>();
+        for (TagModel tag: tags) {
+            if (!tag.getName().trim().equals("")) {
+                keys.add(tag.getName());
+            }
+        }
+        return keys;
+    }
+
+    /**
+     * sorts the current tags according alphabetical order of names
+     */
+    protected void sort() {
+        java.util.Collections.sort(
+                tags,
+                new Comparator<TagModel>() {
+                    public int compare(TagModel self, TagModel other) {
+                        return self.getName().compareTo(other.getName());
+                    }
+                }
+        );
+    }
+
+    /**
+     * updates the name of a tag and sets the dirty state to  true if
+     * the new name is different from the old name.
+     * 
+     * @param tag   the tag
+     * @param newName  the new name
+     */
+    public void updateTagName(TagModel tag, String newName) {
+        String oldName = tag.getName();
+        tag.setName(newName);
+        if (! newName.equals(oldName)) {
+            setDirty(true);
+        }
+    }
+
+    /**
+     * updates the value value of a tag and sets the dirty state to true if the
+     * new name is different from the old name
+     * 
+     * @param tag  the tag
+     * @param newValue  the new value
+     */
+    public void updateTagValue(TagModel tag, String newValue) {
+        String oldValue = tag.getValue();
+        tag.setValue(newValue);
+        if (! newValue.equals(oldValue)) {
+            setDirty(true);
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/relation/TagFieldEditor.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/relation/TagFieldEditor.java	(revision 1762)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/relation/TagFieldEditor.java	(revision 1762)
@@ -0,0 +1,129 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.dialogs.relation;
+
+import java.awt.event.FocusAdapter;
+import java.awt.event.FocusEvent;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.util.logging.Logger;
+
+import javax.swing.JTextField;
+import javax.swing.text.AttributeSet;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.Document;
+import javax.swing.text.PlainDocument;
+
+import org.openstreetmap.josm.gui.dialogs.relation.ac.AutoCompletionList;
+
+/**
+ * TagFieldEditor is an editor for tag names or tag values. It supports auto completion
+ * from a list of auto completion items.
+ *
+ */
+public class TagFieldEditor extends JTextField  {
+
+    static private Logger logger = Logger.getLogger(TagFieldEditor.class.getName());
+
+    /**
+     * The document model for the editor
+     */
+    class AutoCompletionDocument extends PlainDocument {
+
+        /**
+         * inserts a string at a specific position
+         * 
+         */
+        @Override
+        public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
+            if (autoCompletionList == null) {
+                super.insertString(offs, str, a);
+                return;
+            }
+            String currentText = getText(0, getLength());
+            String prefix = currentText.substring(0, offs);
+            autoCompletionList.applyFilter(prefix+str);
+            if (autoCompletionList.getFilteredSize()>0) {
+                // there are matches. Insert the new text and highlight the
+                // auto completed suffix
+                //
+                String matchingString = autoCompletionList.getFilteredItem(0).getValue();
+                remove(0,getLength());
+                super.insertString(0,matchingString,a);
+
+                // highlight from end to insert position
+                //
+                setCaretPosition(getLength());
+                moveCaretPosition(offs + str.length());
+            } else {
+                // there are no matches. Insert the new text, do not highlight
+                //
+                String newText = prefix + str;
+                remove(0,getLength());
+                super.insertString(0,newText,a);
+                setCaretPosition(getLength());
+            }
+        }
+    }
+
+    /** the auto completion list user input is matched against */
+    protected AutoCompletionList autoCompletionList = null;
+
+    /**
+     * creates the default document model for this editor
+     * 
+     */
+    @Override
+    protected Document createDefaultModel() {
+        return new AutoCompletionDocument();
+    }
+
+    /**
+     * constructor
+     */
+    public TagFieldEditor() {
+
+        addFocusListener(
+                new FocusAdapter() {
+                    @Override public void focusGained(FocusEvent e) {
+                        selectAll();
+                        applyFilter(getText());
+                    }
+                }
+        );
+
+        addKeyListener(
+                new KeyAdapter() {
+
+                    @Override
+                    public void keyReleased(KeyEvent e) {
+                        if (getText().equals("")) {
+                            applyFilter("");
+                        }
+                    }
+                }
+        );
+    }
+
+    protected void applyFilter(String filter) {
+        if (autoCompletionList != null) {
+            autoCompletionList.applyFilter(filter);
+        }
+    }
+
+    /**
+     * 
+     * @return the auto completion list; may be null, if no auto completion list is set
+     */
+    public AutoCompletionList getAutoCompletionList() {
+        return autoCompletionList;
+    }
+
+    /**
+     * sets the auto completion list
+     * @param autoCompletionList the auto completion list; if null, auto completion is
+     *   disabled
+     */
+    public void setAutoCompletionList(AutoCompletionList autoCompletionList) {
+        this.autoCompletionList = autoCompletionList;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/relation/TagModel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/relation/TagModel.java	(revision 1762)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/relation/TagModel.java	(revision 1762)
@@ -0,0 +1,130 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.dialogs.relation;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TagModel {
+	
+	/** the name of the tag */
+	private String name = null;
+	
+	/** the list of values */
+	private ArrayList<String> values = null;
+	
+	/**
+	 * constructor
+	 */
+	public TagModel() {
+		values = new ArrayList<String>();
+		setName("");
+		setValue("");
+	}
+	
+	/**
+	 * constructor 
+	 * @param name the tag name 
+	 */
+	public TagModel(String name) {
+		this();
+		setName(name);
+	}
+	
+	/**
+	 * constructor 
+	 * 
+	 * @param name the tag name 
+	 * @param value the tag value 
+	 */
+	public TagModel(String name, String value) {
+		this();
+		setName(name);
+		setValue(value);
+	}
+	
+	/**
+	 * sets the name. Converts name to "" if null.
+	 * @param name the tag name 
+	 */
+	public void setName(String name) {
+		name = (name == null) ? "" : name;
+		this.name = name; 
+	}
+	
+	/** 
+	 * @return the tag name 
+	 */
+	public String getName(){
+		return name;
+	}
+	
+	/**
+	 * removes all values from the list of values 
+	 */
+	public void clearValues() {
+		this.values.clear();
+	}
+	
+	/**
+	 * sets a unique value for this tag. Converts value to "", if null.
+	 * @param value the value. 
+	 */
+	public void setValue(String value) {
+		value = (value == null) ? "" : value;
+		clearValues();
+		this.values.add(value);
+	}
+	
+	/**
+	 * 
+	 * @param value the value to be checked; converted to "" if null 
+	 * @return true, if the values of this tag include <code>value</code>; false otherwise 
+	 */
+	public boolean hasValue(String value) {
+		value = (value == null) ? "" : value;
+		return values.contains(value);
+	}
+	
+	public void addValue(String value) {
+		value = (value == null) ? "" : value;
+		if (hasValue(value)) {
+			return; 
+		}
+		values.add(value);
+	}
+	
+	
+	/**
+	 * removes a value from the list of values. Converts value to "" if null
+	 * @param value the value 
+	 */
+	public void removeValue(String value){
+		value = (value == null) ? "" : value; 
+		values.remove(value);
+	}	
+	
+	public List<String> getValues() {
+		return values;
+	}
+	
+	public String getValue() {
+		if (getValueCount() == 0) {
+			return "";
+		} else if (getValueCount() == 1) {
+			return values.get(0);
+		} else {
+			StringBuilder sb = new StringBuilder();			
+			for (int i =0; i < values.size(); i++) {
+				sb.append(values.get(i));
+				if (i + 1 < values.size()) {
+					sb.append(";");
+				}
+			}
+			return sb.toString();
+		}
+	}
+	
+	public int getValueCount() {
+		return values.size();
+	}
+}
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/relation/TagTable.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/relation/TagTable.java	(revision 1762)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/relation/TagTable.java	(revision 1762)
@@ -0,0 +1,442 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.dialogs.relation;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.AWTException;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.Robot;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.JComponent;
+import javax.swing.JTable;
+import javax.swing.KeyStroke;
+import javax.swing.ListSelectionModel;
+import javax.swing.SwingUtilities;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.DefaultTableColumnModel;
+import javax.swing.table.TableColumn;
+import javax.swing.table.TableColumnModel;
+import javax.swing.table.TableModel;
+
+import org.openstreetmap.josm.gui.dialogs.relation.ac.AutoCompletionCache;
+import org.openstreetmap.josm.gui.dialogs.relation.ac.AutoCompletionList;
+
+/**
+ * This is the tabular editor component for OSM tags.
+ * 
+ */
+@SuppressWarnings("serial")
+public class TagTable extends JTable  {
+
+    private static Logger logger = Logger.getLogger(TagTable.class.getName());
+
+    /** the table cell editor used by this table */
+    private TagCellEditor editor = null;
+
+    /**
+     * The table has two columns. The first column is used for editing rendering and
+     * editing tag keys, the second for rendering and editing tag values.
+     * 
+     */
+    static class TagTableColumnModel extends DefaultTableColumnModel {
+
+        public TagTableColumnModel() {
+            TableColumn col = null;
+            TagCellRenderer renderer = new TagCellRenderer();
+
+
+            // column 0 - tag key
+            col = new TableColumn(0);
+            col.setHeaderValue(tr("Key"));
+            col.setResizable(true);
+            col.setCellRenderer(renderer);
+            addColumn(col);
+
+            // column 1 - tag value
+            col = new TableColumn(1);
+            col.setHeaderValue(tr("Value"));
+            col.setResizable(true);
+            col.setCellRenderer(renderer);
+            addColumn(col);
+
+        }
+    }
+
+    /**
+     * Action to be run when the user navigates to the next cell in the table,
+     * for instance by pressing TAB or ENTER. The action alters the standard
+     * navigation path from cell to cell:
+     * <ul>
+     *   <li>it jumps over cells in the first column</li>
+     *   <li>it automatically add a new empty row when the user leaves the
+     *   last cell in the table</li>
+     * <ul>
+     * 
+     * @author gubaer
+     *
+     */
+    class SelectNextColumnCellAction extends AbstractAction  {
+        public void actionPerformed(ActionEvent e) {
+            run();
+        }
+
+        public void run() {
+            int col = getSelectedColumn();
+            int row = getSelectedRow();
+            if (getCellEditor() != null) {
+                getCellEditor().stopCellEditing();
+            }
+
+            if (col == 0) {
+                col++;
+            } else if (col == 1 && row < getRowCount()-1) {
+                col=0;
+                row++;
+            } else if (col == 1 && row == getRowCount()-1){
+                // we are at the end. Append an empty row and move the focus
+                // to its second column
+                TagEditorModel model = (TagEditorModel)getModel();
+                model.appendNewTag();
+                col=0;
+                row++;
+            }
+            changeSelection(row, col, false, false);
+        }
+    }
+
+
+    /**
+     * Action to be run when the user navigates to the previous cell in the table,
+     * for instance by pressing Shift-TAB
+     *
+     */
+    class SelectPreviousColumnCellAction extends AbstractAction  {
+
+        public void actionPerformed(ActionEvent e) {
+            int col = getSelectedColumn();
+            int row = getSelectedRow();
+            if (getCellEditor() != null) {
+                getCellEditor().stopCellEditing();
+            }
+
+
+            if (col == 0 && row == 0) {
+                // change nothing
+            } else if (col == 1) {
+                col--;
+            } else {
+                col = 1;
+                row--;
+            }
+            changeSelection(row, col, false, false);
+        }
+    }
+
+    /**
+     * Action to be run when the user invokes a delete action on the table, for
+     * instance by pressing DEL.
+     * 
+     * Depending on the shape on the current selection the action deletes individual
+     * values or entire tags from the model.
+     * 
+     * If the current selection consists of cells in the second column only, the keys of
+     * the selected tags are set to the empty string.
+     * 
+     * If the current selection consists of cell in the third column only, the values of the
+     * selected tags are set to the empty string.
+     * 
+     *  If the current selection consists of cells in the second and the third column,
+     *  the selected tags are removed from the model.
+     * 
+     *  This action listens to the table selection. It becomes enabled when the selection
+     *  is non-empty, otherwise it is disabled.
+     * 
+     *
+     */
+    class DeleteAction extends RunnableAction implements ListSelectionListener {
+
+        /**
+         * delete a selection of tag names
+         */
+        protected void deleteTagNames() {
+            int[] rows = getSelectedRows();
+            TagEditorModel model = (TagEditorModel)getModel();
+            model.deleteTagNames(rows);
+        }
+
+        /**
+         * delete a selection of tag values
+         */
+        protected void deleteTagValues() {
+            int[] rows = getSelectedRows();
+            TagEditorModel model = (TagEditorModel)getModel();
+            model.deleteTagValues(rows);
+        }
+
+        /**
+         * delete a selection of tags
+         */
+        protected void deleteTags() {
+            int[] rows = getSelectedRows();
+            TagEditorModel model = (TagEditorModel)getModel();
+            model.deleteTags(rows);
+        }
+
+        /**
+         * constructor
+         */
+        public DeleteAction() {
+            putValue(Action.NAME, tr("Delete"));
+            getSelectionModel().addListSelectionListener(this);
+            getColumnModel().getSelectionModel().addListSelectionListener(this);
+        }
+
+
+
+        @Override
+        public void run() {
+            if (!isEnabled())
+                return;
+            getCellEditor().stopCellEditing();
+            if (getSelectedColumnCount() == 1) {
+                if (getSelectedColumn() == 0) {
+                    deleteTagNames();
+                } else if (getSelectedColumn() == 1) {
+                    deleteTagValues();
+                } else
+                    // should not happen
+                    //
+                    throw new IllegalStateException("unexpected selected clolumn: getSelectedColumn() is " + getSelectedColumn());
+            } else if (getSelectedColumnCount() == 2) {
+                deleteTags();
+            }
+            TagEditorModel model = (TagEditorModel)getModel();
+            if (model.getRowCount() == 0) {
+                model.ensureOneTag();
+                requestFocusInCell(0, 0);
+            }
+        }
+
+        /**
+         * listens to the table selection model
+         */
+        public void valueChanged(ListSelectionEvent e) {
+            if (isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) {
+                setEnabled(false);
+            } else if (!isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) {
+                setEnabled(true);
+            } else if (getSelectedColumnCount() > 1 || getSelectedRowCount() > 1) {
+                setEnabled(true);
+            } else {
+                setEnabled(false);
+            }
+
+        }
+    }
+
+    /**
+     * Action to be run when the user adds a new tag.
+     * 
+     *
+     */
+    class AddAction extends RunnableAction {
+
+        public AddAction() {
+            putValue(Action.NAME, tr("Add"));
+        }
+
+        @Override
+        public void run() {
+            getCellEditor().stopCellEditing();
+            ((TagEditorModel)getModel()).appendNewTag();
+            final int rowIdx = getModel().getRowCount()-1;
+            requestFocusInCell(rowIdx, 0);
+        }
+    }
+
+
+    /** the delete action */
+    private RunnableAction deleteAction = null;
+
+    /** the add action */
+    private RunnableAction addAction = null;
+
+    /**
+     * 
+     * @return the delete action used by this table
+     */
+    public RunnableAction getDeleteAction() {
+        return deleteAction;
+    }
+
+    public RunnableAction getAddAction() {
+        return addAction;
+    }
+
+    /**
+     * initialize the table
+     */
+    protected void init() {
+        setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
+        setCellSelectionEnabled(true);
+        setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
+        putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
+
+        // make ENTER behave like TAB
+        //
+        getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
+        .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "selectNextColumnCell");
+
+        // install custom navigation actions
+        //
+        getActionMap().put("selectNextColumnCell", new SelectNextColumnCellAction());
+        getActionMap().put("selectPreviousColumnCell", new SelectPreviousColumnCellAction());
+
+        // create a delete action. Installing this action in the input and action map
+        // didn't work. We therefore handle delete requests in processKeyBindings(...)
+        //
+        deleteAction = new DeleteAction();
+
+        // create the add action
+        //
+        addAction = new AddAction();
+        getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
+        .put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, KeyEvent.CTRL_MASK), "addTag");
+        getActionMap().put("addTag", addAction);
+
+        // create the table cell editor and set it to key and value columns
+        //
+        editor = new TagCellEditor();
+        editor.setTagEditorModel((TagEditorModel)getModel());
+        getColumnModel().getColumn(0).setCellEditor(editor);
+        getColumnModel().getColumn(1).setCellEditor(editor);
+    }
+
+    /**
+     * constructor
+     * 
+     * @param model
+     * @param columnModel
+     */
+    public TagTable(TableModel model) {
+        super(model, new TagTableColumnModel());
+        init();
+    }
+
+
+
+    /**
+     * adjusts the width of the columns for the tag name and the tag value
+     * to the width of the scroll panes viewport.
+     * 
+     * Note: {@see #getPreferredScrollableViewportSize()} did not work as expected
+     * 
+     * @param scrollPaneWidth the width of the scroll panes viewport
+     */
+    public void adjustColumnWidth(int scrollPaneWidth) {
+        TableColumnModel tcm = getColumnModel();
+        int width = scrollPaneWidth;
+        width = width / 2;
+        if (width > 0) {
+            tcm.getColumn(0).setMinWidth(width);
+            tcm.getColumn(0).setMaxWidth(width);
+            tcm.getColumn(1).setMinWidth(width);
+            tcm.getColumn(1).setMaxWidth(width);
+        }
+    }
+
+
+    @Override protected boolean processKeyBinding(KeyStroke ks, KeyEvent e,
+            int condition, boolean pressed) {
+
+        // handle delete key
+        //
+        if (e.getKeyCode() == KeyEvent.VK_DELETE) {
+            getDeleteAction().run();
+        }
+        return super.processKeyBinding(ks, e, condition, pressed);
+    }
+
+
+    /**
+     * @param autoCompletionList
+     */
+    public void setAutoCompletionList(AutoCompletionList autoCompletionList) {
+        if (autoCompletionList == null)
+            return;
+        if (editor != null) {
+            editor.setAutoCompletionList(autoCompletionList);
+        }
+    }
+
+    public void setAutoCompletionCache(AutoCompletionCache acCache) {
+        if (acCache == null) {
+            logger.warning("argument acCache should not be null. Aborting.");
+            return;
+        }
+        if (editor != null) {
+            editor.setAutoCompletionCache(acCache);
+        }
+    }
+
+    public AutoCompletionList getAutoCompletionList() {
+        if (editor != null)
+            return editor.getAutoCompletionList();
+        else
+            return null;
+    }
+
+    public TagCellEditor getTableCellEditor() {
+        return editor;
+    }
+
+    public void  addOKAccelatorListener(KeyListener l) {
+        addKeyListener(l);
+        if (editor == null) {
+            logger.warning("editor is null. cannot register OK accelator listener.");
+        }
+        editor.getEditor().addKeyListener(l);
+    }
+
+    public void requestFocusInCell(final int row, final int col) {
+
+        // the following code doesn't work reliably. If a table cell
+        // gains focus using editCellAt() and requestFocusInWindow()
+        // it isn't possible to tab to the next table cell using TAB or
+        // ENTER. Don't know why.
+        //
+        // tblTagEditor.editCellAt(row, col);
+        // if (tblTagEditor.getEditorComponent() != null) {
+        //	tblTagEditor.getEditorComponent().requestFocusInWindow();
+        // }
+
+        // this is a workaround. We move the focus to the respective cell
+        // using a simulated mouse click. In this case one can tab out of
+        // the cell using TAB and ENTER.
+        //
+        Rectangle r = getCellRect(row,col, false);
+        Point p = new Point(r.x + r.width/2, r.y + r.height/2);
+        SwingUtilities.convertPointToScreen(p, this);
+        // logger.info("simulating mouse click event at point " + p.toString());
+
+        try {
+            Robot robot = new Robot();
+            robot.mouseMove(p.x,p.y);
+            robot.mousePress(InputEvent.BUTTON1_MASK);
+            robot.mouseRelease(InputEvent.BUTTON1_MASK);
+        } catch(AWTException e) {
+            logger.log(Level.SEVERE, "failed to simulate mouse click event at (" + r.x + "," + r.y + "). Exception: " + e.toString());
+            return;
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/relation/ac/AutoCompletionCache.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/relation/ac/AutoCompletionCache.java	(revision 1762)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/relation/ac/AutoCompletionCache.java	(revision 1762)
@@ -0,0 +1,156 @@
+package org.openstreetmap.josm.gui.dialogs.relation.ac;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.layer.Layer.LayerChangeListener;
+
+/**
+ * AutoCompletionCache temporarily holds a cache of keys with a list of
+ * possible auto completion values for each key.
+ * 
+ * The cache can initialize itself from the current JOSM data set such that
+ * <ol>
+ *   <li>any key used in a tag in the data set is part of the key list in the cache</li>
+ *   <li>any value used in a tag for a specific key is part of the autocompletion list of
+ *     this key</li>
+ * </ol>
+ * 
+ * Building up auto completion lists should not
+ * slow down tabbing from input field to input field. Looping through the complete
+ * data set in order to build up the auto completion list for a specific input
+ * field is not efficient enough, hence this cache.
+ *
+ */
+public class AutoCompletionCache {
+
+    private static HashMap<OsmDataLayer, AutoCompletionCache> caches;
+    static {
+        caches = new HashMap<OsmDataLayer, AutoCompletionCache>();
+        Layer.listeners.add(new LayerChangeListener() {
+            public void activeLayerChange(Layer oldLayer, Layer newLayer) {
+                // do nothing
+            }
+
+            public void layerAdded(Layer newLayer) {
+                // do noting
+            }
+
+            public void layerRemoved(Layer oldLayer) {
+                if (oldLayer instanceof OsmDataLayer) {
+                    caches.remove(oldLayer);
+                }
+            }
+        }
+        );
+    }
+
+    static public AutoCompletionCache getCacheForLayer(OsmDataLayer layer) {
+        AutoCompletionCache cache = caches.get(layer);
+        if (cache == null) {
+            cache = new AutoCompletionCache(layer);
+            caches.put(layer, cache);
+        }
+        return cache;
+    }
+
+
+    /** the cache */
+    private HashMap<String, ArrayList<String>> cache;
+    private OsmDataLayer layer;
+
+    /**
+     * constructor
+     */
+    public AutoCompletionCache(OsmDataLayer layer) {
+        cache = new HashMap<String, ArrayList<String>>();
+        this.layer = layer;
+    }
+
+    public AutoCompletionCache() {
+        this(null);
+    }
+
+    /**
+     * make sure, <code>key</code> is in the cache
+     * 
+     * @param key  the key
+     */
+    protected void cacheKey(String key) {
+        if (cache.containsKey(key))
+            return;
+        else {
+            cache.put(key, new ArrayList<String>());
+        }
+    }
+
+    /**
+     * make sure, value is one of the auto completion values allowed for key
+     * 
+     * @param key the key
+     * @param value the value
+     */
+    protected void cacheValue(String key, String value) {
+        cacheKey(key);
+        ArrayList<String> values = cache.get(key);
+        if (!values.contains(value)) {
+            values.add(value);
+        }
+    }
+
+    /**
+     * make sure, the keys and values of all tags held by primitive are
+     * in the auto completion cache
+     * 
+     * @param primitive an OSM primitive
+     */
+    protected void cachePrimitive(OsmPrimitive primitive) {
+        for (String key: primitive.keySet()) {
+            String value = primitive.get(key);
+            cacheValue(key, value);
+        }
+    }
+
+    /**
+     * initializes the cache from the primitives in the dataset of
+     * {@see #layer}
+     * 
+     */
+    public void initFromJOSMDataset() {
+        cache = new HashMap<String, ArrayList<String>>();
+        if (layer == null)
+            return;
+        Collection<OsmPrimitive> ds = layer.data.allNonDeletedPrimitives();
+        for (OsmPrimitive primitive : ds) {
+            cachePrimitive(primitive);
+        }
+    }
+
+    /**
+     * replies the keys held by the cache
+     * 
+     * @return the list of keys held by the cache
+     */
+    public List<String> getKeys() {
+        return new ArrayList<String>(cache.keySet());
+    }
+
+    /**
+     * replies the auto completion values allowed for a specific key. Replies
+     * an empty list if key is null or if key is not in {@link #getKeys()}.
+     * 
+     * @param key
+     * @return the list of auto completion values
+     */
+    public List<String> getValues(String key) {
+        if (!cache.containsKey(key))
+            return new ArrayList<String>();
+        else
+            return cache.get(key);
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/relation/ac/AutoCompletionItemPritority.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/relation/ac/AutoCompletionItemPritority.java	(revision 1762)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/relation/ac/AutoCompletionItemPritority.java	(revision 1762)
@@ -0,0 +1,23 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.dialogs.relation.ac;
+
+public enum AutoCompletionItemPritority implements Comparable<AutoCompletionItemPritority> {
+
+    /** indicates that a value is in the current selection */
+    IS_IN_SELECTION,
+
+    /** indicates that this is a standard value, i.e. a standard tag name
+     *  or a standard value for a given tag name
+     */
+    IS_IN_STANDARD,
+
+
+    /**
+     * indicates that this is an arbitrary value from the data set, i.e.
+     * the value of a tag name=xxx
+     */
+    IS_IN_DATASET,
+
+    /** unknown priority. This is the lowest priority. */
+    UNKNOWN
+}
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/relation/ac/AutoCompletionList.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/relation/ac/AutoCompletionList.java	(revision 1762)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/relation/ac/AutoCompletionList.java	(revision 1762)
@@ -0,0 +1,281 @@
+package org.openstreetmap.josm.gui.dialogs.relation.ac;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.swing.JTable;
+import javax.swing.table.AbstractTableModel;
+
+/**
+ * AutoCompletionList manages a list of {@see AutoCompletionListItem}s.
+ * 
+ * The list is sorted, items with higher priority first, then according to lexicographic order
+ * on the value of the {@see AutoCompletionListItem}.
+ * 
+ * AutoCompletionList maintains two views on the list of {@see AutoCompletionListItem}s.
+ * <ol>
+ *   <li>the bare, unfiltered view which includes all items</li>
+ *   <li>a filtered view, which includes only items which match a current filter expression</li>
+ * </ol>
+ * 
+ * AutoCompletionList is an {@link AbstractTableModel} which serves the list of filtered
+ * items to a {@link JTable}.
+ * 
+ */
+public class AutoCompletionList extends AbstractTableModel {
+
+    /** the bare list of AutoCompletionItems */
+    private ArrayList<AutoCompletionListItem> list = null;
+    /**  the filtered list of AutoCompletionItems */
+    private ArrayList<AutoCompletionListItem> filtered = null;
+    /** the filter expression */
+    private String filter = null;
+
+    /**
+     * constructor
+     */
+    public AutoCompletionList() {
+        list = new ArrayList<AutoCompletionListItem>();
+        filtered = new ArrayList<AutoCompletionListItem>();
+    }
+
+
+    /**
+     * applies a filter expression to the list of {@see AutoCompletionListItem}s.
+     * 
+     * The matching criterion is a case insensitive substring match.
+     * 
+     * @param filter  the filter expression; must not be null
+     * 
+     * @exception IllegalArgumentException thrown, if filter is null
+     */
+    public void applyFilter(String filter) {
+        if (filter == null)
+            throw new IllegalArgumentException("argument 'filter' must not be null");
+        this.filter = filter;
+        filter();
+    }
+
+    /**
+     * clears the current filter
+     * 
+     */
+    public void clearFilter() {
+        filter = null;
+        filter();
+    }
+
+    /**
+     * @return the current filter expression; null, if no filter expression is set
+     */
+    public String getFilter() {
+        return filter;
+    }
+
+
+    /**
+     * adds an AutoCompletionListItem to the list. Only adds the item if it
+     * is not null and if not in the list yet.
+     * 
+     * @param item the item
+     */
+    public void add(AutoCompletionListItem item) {
+        if (item == null)
+            return;
+        appendOrUpdatePriority(item);
+        sort();
+        filter();
+    }
+
+
+    /**
+     * adds another AutoCompletionList to this list. An item is only
+     * added it is not null and if it does not exist in the list yet.
+     * 
+     * @param other another auto completion list; must not be null
+     * @exception IllegalArgumentException thrown, if other is null
+     */
+    public void add(AutoCompletionList other) {
+        if (other == null)
+            throw new IllegalArgumentException("argument 'other' must not be null");
+        for (AutoCompletionListItem item : other.list) {
+            appendOrUpdatePriority(item);
+        }
+        sort();
+        filter();
+    }
+
+
+    /**
+     * adds a list of AutoCompletionListItem to this list. Only items which
+     * are not null and which do not exist yet in the list are added.
+     * 
+     * @param other a list of AutoCompletionListItem; must not be null
+     * @exception IllegalArgumentException thrown, if other is null
+     */
+    public void add(List<AutoCompletionListItem> other) {
+        if (other == null)
+            throw new IllegalArgumentException("argument 'other' must not be null");
+        for (AutoCompletionListItem toadd : other) {
+            appendOrUpdatePriority(toadd);
+        }
+        sort();
+        filter();
+    }
+
+    protected void appendOrUpdatePriority(AutoCompletionListItem toadd) {
+        AutoCompletionListItem item = lookup(toadd.getValue());
+        if (item == null) {
+            // new item does not exist yet. Add it to the list
+            //
+            list.add(toadd);
+        } else {
+            // new item already exists. Update priority if necessary
+            //
+            if (toadd.getPriority().compareTo(item.getPriority()) < 0) {
+                item.setPriority(toadd.getPriority());
+            }
+        }
+    }
+
+    /**
+     * checks whether a specific item is already in the list. Matches for the
+     * the value <strong>and</strong> the priority of the item
+     * 
+     * @param item the item to check
+     * @return true, if item is in the list; false, otherwise
+     */
+    public boolean contains(AutoCompletionListItem item) {
+        if (item == null)
+            return false;
+        return list.contains(item);
+    }
+
+    /**
+     * checks whether an item with the given value is already in the list. Ignores
+     * priority of the items.
+     * 
+     * @param value the value of an auto completion item
+     * @return true, if value is in the list; false, otherwise
+     */
+    public boolean contains(String value) {
+        if (value == null)
+            return false;
+        for (AutoCompletionListItem item: list) {
+            if (item.getValue().equals(value))
+                return true;
+        }
+        return false;
+    }
+
+    /**
+     * 
+     * @param value a specific value
+     * @return  the auto completion item for this value; null, if there is no
+     *   such auto completion item
+     */
+    public AutoCompletionListItem lookup(String value) {
+        if (value == null)
+            return null;
+        for (AutoCompletionListItem item : list) {
+            if (item.getValue().equals(value))
+                return item;
+        }
+        return null;
+    }
+
+
+    /**
+     * removes the auto completion item with key <code>key</code>
+     * @param key  the key;
+     */
+    public void remove(String key) {
+        if (key == null)
+            return;
+        for (int i=0;i< list.size();i++) {
+            AutoCompletionListItem item = list.get(i);
+            if (item.getValue().equals(key)) {
+                list.remove(i);
+                return;
+            }
+        }
+    }
+
+    /**
+     * sorts the list
+     */
+    protected void sort() {
+        Collections.sort(list);
+    }
+
+    protected void filter() {
+        filtered.clear();
+        if (filter == null) {
+            // Collections.copy throws an exception "Source does not fit in dest"
+            // Collections.copy(filtered, list);
+            filtered.ensureCapacity(list.size());
+            for (AutoCompletionListItem item: list) {
+                filtered.add(item);
+            }
+            return;
+        }
+
+        // apply the pattern to list of possible values. If it matches, add the
+        // value to the list of filtered values
+        //
+        for (AutoCompletionListItem item : list) {
+            if (item.getValue().startsWith(filter)) {
+                filtered.add(item);
+            }
+        }
+        fireTableDataChanged();
+    }
+
+    /**
+     * replies the number of filtered items
+     * 
+     * @return the number of filtered items
+     */
+    public int getFilteredSize() {
+        return this.filtered.size();
+    }
+
+    /**
+     * replies the idx-th item from the list of filtered items
+     * @param idx the index; must be in the range 0<= idx < {@see #getFilteredSize()}
+     * @return the item
+     * 
+     * @exception IndexOutOfBoundsException thrown, if idx is out of bounds
+     */
+    public AutoCompletionListItem getFilteredItem(int idx) {
+        if (idx < 0 || idx >= getFilteredSize())
+            throw new IndexOutOfBoundsException("idx out of bounds. idx=" + idx);
+        return filtered.get(idx);
+    }
+
+
+    /**
+     * removes all elements from the auto completion list
+     * 
+     */
+    public void clear() {
+        list.clear();
+        fireTableDataChanged();
+    }
+
+
+    public int getColumnCount() {
+        return 1;
+    }
+
+    public int getRowCount() {
+
+        return list == null ? 0 : getFilteredSize();
+    }
+
+    public Object getValueAt(int rowIndex, int columnIndex) {
+        return list == null ? null : getFilteredItem(rowIndex);
+    }
+
+}
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/relation/ac/AutoCompletionListItem.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/relation/ac/AutoCompletionListItem.java	(revision 1762)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/relation/ac/AutoCompletionListItem.java	(revision 1762)
@@ -0,0 +1,122 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.dialogs.relation.ac;
+
+/**
+ * Represents an entry in the list of auto completion values.
+ * 
+ *  An AutoCompletionListItem has a <em>priority</em> and a <em>value</em>.
+ * 
+ *  The priority helps to sort the auto completion items according to their importance. For instance,
+ *  in an auto completion list for tag names, standard tag names would be assigned a higher
+ *  priority than arbitrary tag names present in the current data set. There are three priority levels,
+ *  {@see AutoCompletionItemPritority}.
+ *
+ * The value is a string which will be displayed in the auto completion list.
+ * 
+ */
+public class AutoCompletionListItem implements Comparable<AutoCompletionListItem>{
+
+    /** the pritority of this item */
+    private  AutoCompletionItemPritority priority;
+    /** the value of this item */
+    private String value;
+
+    /**
+     * constructor
+     */
+    public AutoCompletionListItem() {
+        value = "";
+        priority = AutoCompletionItemPritority.UNKNOWN;
+    }
+
+    public AutoCompletionListItem(String value, AutoCompletionItemPritority priority) {
+        this.value = value;
+        this.priority = priority;
+    }
+
+    /**
+     * 
+     * @return the priority
+     */
+    public AutoCompletionItemPritority getPriority() {
+        return priority;
+    }
+
+    /**
+     * sets the priority
+     * @param priority  the priority
+     */
+    public void setPriority(AutoCompletionItemPritority priority) {
+        this.priority = priority;
+    }
+
+    /**
+     * 
+     * @return the value
+     */
+    public String getValue() {
+        return value;
+    }
+
+    /**
+     * sets the value
+     * @param value the value; must not be null
+     * @exception IllegalArgumentException thrown, if value if null
+     */
+    public void setValue(String value) {
+        if (value == null)
+            throw new IllegalArgumentException("argument 'value' must not be null");
+        this.value = value;
+    }
+
+    @Override public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("<AutoCompletionItemPritority: ");
+        sb.append("value='");
+        sb.append(value);
+        sb.append("',");
+        sb.append("priority='");
+        sb.append(priority.toString());
+        sb.append("'>");
+        return sb.toString();
+    }
+
+    @Override public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result
+        + ((priority == null) ? 0 : priority.hashCode());
+        result = prime * result + ((value == null) ? 0 : value.hashCode());
+        return result;
+    }
+
+    @Override public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        final AutoCompletionListItem other = (AutoCompletionListItem)obj;
+        if (priority == null) {
+            if (other.priority != null)
+                return false;
+        } else if (!priority.equals(other.priority))
+            return false;
+        if (value == null) {
+            if (other.value != null)
+                return false;
+        } else if (!value.equals(other.value))
+            return false;
+        return true;
+    }
+
+
+    public int compareTo(AutoCompletionListItem other) {
+        int ret = this.priority.compareTo(other.priority);
+        if (ret != 0)
+            return ret;
+        else
+            return this.value.compareTo(other.value);
+    }
+}
