Index: trunk/src/org/openstreetmap/josm/gui/dialogs/properties/AbstractCopyAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/properties/AbstractCopyAction.java	(revision 13521)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/properties/AbstractCopyAction.java	(revision 13521)
@@ -0,0 +1,65 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.dialogs.properties;
+
+import java.awt.event.ActionEvent;
+import java.util.Collection;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import javax.swing.AbstractAction;
+import javax.swing.JTable;
+
+import org.openstreetmap.josm.data.osm.Tagged;
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
+import org.openstreetmap.josm.tools.Utils;
+
+/**
+ * Super class of all copy actions from tag table.
+ * @since 13521
+ */
+public abstract class AbstractCopyAction extends AbstractAction {
+
+    private final JTable tagTable;
+    private final Function<Integer, String> keySupplier;
+    private final Supplier<Collection<? extends Tagged>> objectSupplier;
+
+    /**
+     * Constructs a new {@code AbstractCopyAction}.
+     * @param tagTable the tag table
+     * @param keySupplier a supplier which returns the selected key for a given row index
+     * @param objectSupplier a supplier which returns the selected tagged object(s)
+     */
+    public AbstractCopyAction(JTable tagTable, Function<Integer, String> keySupplier, Supplier<Collection<? extends Tagged>> objectSupplier) {
+        this.tagTable = Objects.requireNonNull(tagTable);
+        this.keySupplier = Objects.requireNonNull(keySupplier);
+        this.objectSupplier = Objects.requireNonNull(objectSupplier);
+    }
+
+    protected abstract Collection<String> getString(Tagged p, String key);
+
+    @Override
+    public void actionPerformed(ActionEvent ae) {
+        int[] rows = tagTable.getSelectedRows();
+        Set<String> values = new TreeSet<>();
+        Collection<? extends Tagged> sel = objectSupplier.get();
+        if (rows.length == 0 || sel.isEmpty()) return;
+
+        for (int row: rows) {
+            String key = keySupplier.apply(row);
+            if (sel.isEmpty())
+                return;
+            for (Tagged p : sel) {
+                Collection<String> s = getString(p, key);
+                if (s != null) {
+                    values.addAll(s);
+                }
+            }
+        }
+        if (!values.isEmpty()) {
+            ClipboardUtils.copyString(Utils.join("\n", values));
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/properties/CopyAllKeyValueAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/properties/CopyAllKeyValueAction.java	(revision 13521)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/properties/CopyAllKeyValueAction.java	(revision 13521)
@@ -0,0 +1,50 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.dialogs.properties;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.KeyEvent;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import javax.swing.JTable;
+
+import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.data.osm.Tagged;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.tools.Shortcut;
+
+/**
+ * Copy the key and value of all the tags to clipboard.
+ * @since 13521
+ */
+public class CopyAllKeyValueAction extends AbstractCopyAction {
+
+    /**
+     * Constructs a new {@code CopyAllKeyValueAction}.
+     * @param tagTable the tag table
+     * @param keyFn a function which returns the selected key for a given row index
+     * @param objectSp a supplier which returns the selected tagged object(s)
+     */
+    public CopyAllKeyValueAction(JTable tagTable, Function<Integer, String> keyFn, Supplier<Collection<? extends Tagged>> objectSp) {
+        super(tagTable, keyFn, objectSp);
+        putValue(NAME, tr("Copy all Keys/Values"));
+        putValue(SHORT_DESCRIPTION, tr("Copy the key and value of all the tags to clipboard"));
+        Shortcut sc = Shortcut.registerShortcut("system:copytags", tr("Edit: {0}", tr("Copy Tags")), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE);
+        MainApplication.registerActionShortcut(this, sc);
+        sc.setAccelerator(this);
+    }
+
+    @Override
+    protected Collection<String> getString(Tagged p, String key) {
+        List<String> r = new LinkedList<>();
+        for (Entry<String, String> kv : p.getKeys().entrySet()) {
+            r.add(new Tag(kv.getKey(), kv.getValue()).toString());
+        }
+        return r;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/properties/CopyKeyValueAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/properties/CopyKeyValueAction.java	(revision 13521)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/properties/CopyKeyValueAction.java	(revision 13521)
@@ -0,0 +1,39 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.dialogs.properties;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import javax.swing.JTable;
+
+import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.data.osm.Tagged;
+
+/**
+ * Copy the key and value of the selected tag(s) to clipboard.
+ * @since 13521
+ */
+public class CopyKeyValueAction extends AbstractCopyAction {
+
+    /**
+     * Constructs a new {@code CopyKeyValueAction}.
+     * @param tagTable the tag table
+     * @param keyFn a function which returns the selected key for a given row index
+     * @param objectSp a supplier which returns the selected tagged object(s)
+     */
+    public CopyKeyValueAction(JTable tagTable, Function<Integer, String> keyFn, Supplier<Collection<? extends Tagged>> objectSp) {
+        super(tagTable, keyFn, objectSp);
+        putValue(NAME, tr("Copy selected Key(s)/Value(s)"));
+        putValue(SHORT_DESCRIPTION, tr("Copy the key and value of the selected tag(s) to clipboard"));
+    }
+
+    @Override
+    protected Collection<String> getString(Tagged p, String key) {
+        String v = p.get(key);
+        return v == null ? null : Collections.singleton(new Tag(key, v).toString());
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/properties/CopyValueAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/properties/CopyValueAction.java	(revision 13521)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/properties/CopyValueAction.java	(revision 13521)
@@ -0,0 +1,38 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.dialogs.properties;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import javax.swing.JTable;
+
+import org.openstreetmap.josm.data.osm.Tagged;
+
+/**
+ * Copy the value of the selected tag to clipboard.
+ * @since 13521
+ */
+public class CopyValueAction extends AbstractCopyAction {
+
+    /**
+     * Constructs a new {@code CopyValueAction}.
+     * @param tagTable the tag table
+     * @param keyFn a function which returns the selected key for a given row index
+     * @param objectSp a supplier which returns the selected tagged object(s)
+     */
+    public CopyValueAction(JTable tagTable, Function<Integer, String> keyFn, Supplier<Collection<? extends Tagged>> objectSp) {
+        super(tagTable, keyFn, objectSp);
+        putValue(NAME, tr("Copy Value"));
+        putValue(SHORT_DESCRIPTION, tr("Copy the value of the selected tag to clipboard"));
+    }
+
+    @Override
+    protected Collection<String> getString(Tagged p, String key) {
+        String v = p.get(key);
+        return v == null ? null : Collections.singleton(v);
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/properties/HelpAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/properties/HelpAction.java	(revision 13521)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/properties/HelpAction.java	(revision 13521)
@@ -0,0 +1,159 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.dialogs.properties;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+
+import javax.swing.AbstractAction;
+import javax.swing.JTable;
+import javax.swing.KeyStroke;
+
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.tools.HttpClient;
+import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.LanguageInfo;
+import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.tools.OpenBrowser;
+import org.openstreetmap.josm.tools.Utils;
+
+/**
+ * Launch browser with wiki help for selected object.
+ * @since 13521
+ */
+public class HelpAction extends AbstractAction {
+    private final JTable tagTable;
+    private final Function<Integer, String> tagKeySupplier;
+    private final Function<Integer, Map<String, Integer>> tagValuesSupplier;
+
+    private final JTable membershipTable;
+    private final Function<Integer, Relation> memberValueSupplier;
+
+    /**
+     * Constructs a new {@code HelpAction}.
+     * @param tagTable The tag table. Cannot be null
+     * @param tagKeySupplier Finds the key from given row of tag table. Cannot be null
+     * @param tagValuesSupplier Finds the values from given row of tag table (map of values and number of occurrences). Cannot be null
+     * @param membershipTable The membership table. Can be null
+     * @param memberValueSupplier Finds the parent relation from given row of membership table. Can be null
+     */
+    public HelpAction(JTable tagTable, Function<Integer, String> tagKeySupplier, Function<Integer, Map<String, Integer>> tagValuesSupplier,
+            JTable membershipTable, Function<Integer, Relation> memberValueSupplier) {
+        this.tagTable = Objects.requireNonNull(tagTable);
+        this.tagKeySupplier = Objects.requireNonNull(tagKeySupplier);
+        this.tagValuesSupplier = Objects.requireNonNull(tagValuesSupplier);
+        this.membershipTable = membershipTable;
+        this.memberValueSupplier = memberValueSupplier;
+        putValue(NAME, tr("Go to OSM wiki for tag help"));
+        putValue(SHORT_DESCRIPTION, tr("Launch browser with wiki help for selected object"));
+        new ImageProvider("dialogs", "search").getResource().attachImageIcon(this, true);
+        putValue(ACCELERATOR_KEY, getKeyStroke());
+    }
+
+    /**
+     * Returns the keystroke launching this action (F1).
+     * @return the keystroke launching this action
+     */
+    public KeyStroke getKeyStroke() {
+        return KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0);
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        try {
+            String base = Config.getPref().get("url.openstreetmap-wiki", "https://wiki.openstreetmap.org/wiki/");
+            String lang = LanguageInfo.getWikiLanguagePrefix();
+            final List<URI> uris = new ArrayList<>();
+            int row;
+            if (tagTable.getSelectedRowCount() == 1) {
+                row = tagTable.getSelectedRow();
+                String key = Utils.encodeUrl(tagKeySupplier.apply(row));
+                Map<String, Integer> m = tagValuesSupplier.apply(row);
+                if (!m.isEmpty()) {
+                    String val = Utils.encodeUrl(m.entrySet().iterator().next().getKey());
+
+                    uris.add(new URI(String.format("%s%sTag:%s=%s", base, lang, key, val)));
+                    uris.add(new URI(String.format("%sTag:%s=%s", base, key, val)));
+                    uris.add(new URI(String.format("%s%sKey:%s", base, lang, key)));
+                    uris.add(new URI(String.format("%sKey:%s", base, key)));
+                    uris.add(new URI(String.format("%s%sMap_Features", base, lang)));
+                    uris.add(new URI(String.format("%sMap_Features", base)));
+                }
+            } else if (membershipTable != null && membershipTable.getSelectedRowCount() == 1) {
+                row = membershipTable.getSelectedRow();
+                String type = (memberValueSupplier.apply(row)).get("type");
+                if (type != null) {
+                    type = Utils.encodeUrl(type);
+                }
+
+                if (type != null && !type.isEmpty()) {
+                    uris.add(new URI(String.format("%s%sRelation:%s", base, lang, type)));
+                    uris.add(new URI(String.format("%sRelation:%s", base, type)));
+                }
+
+                uris.add(new URI(String.format("%s%sRelations", base, lang)));
+                uris.add(new URI(String.format("%sRelations", base)));
+            } else {
+                // give the generic help page, if more than one element is selected
+                uris.add(new URI(String.format("%s%sMap_Features", base, lang)));
+                uris.add(new URI(String.format("%sMap_Features", base)));
+            }
+
+            MainApplication.worker.execute(() -> displayHelp(uris));
+        } catch (URISyntaxException e1) {
+            Logging.error(e1);
+        }
+    }
+
+    private void displayHelp(final List<URI> uris) {
+        try {
+            // find a page that actually exists in the wiki
+            HttpClient.Response conn;
+            for (URI u : uris) {
+                conn = HttpClient.create(u.toURL(), "HEAD").connect();
+
+                if (conn.getResponseCode() != 200) {
+                    conn.disconnect();
+                } else {
+                    long osize = conn.getContentLength();
+                    if (osize > -1) {
+                        conn.disconnect();
+
+                        final URI newURI = new URI(u.toString()
+                                .replace("=", "%3D") /* do not URLencode whole string! */
+                                .replaceFirst("/wiki/", "/w/index.php?redirect=no&title=")
+                        );
+                        conn = HttpClient.create(newURI.toURL(), "HEAD").connect();
+                    }
+
+                    /* redirect pages have different content length, but retrieving a "nonredirect"
+                     *  page using index.php and the direct-link method gives slightly different
+                     *  content lengths, so we have to be fuzzy.. (this is UGLY, recode if u know better)
+                     */
+                    if (osize > -1 && conn.getContentLength() != -1 && Math.abs(conn.getContentLength() - osize) > 200) {
+                        Logging.info("{0} is a mediawiki redirect", u);
+                        conn.disconnect();
+                    } else {
+                        conn.disconnect();
+
+                        OpenBrowser.displayUrl(u.toString());
+                        break;
+                    }
+                }
+            }
+        } catch (URISyntaxException | IOException e1) {
+            Logging.error(e1);
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java	(revision 13520)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java	(revision 13521)
@@ -14,7 +14,4 @@
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -24,5 +21,4 @@
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -80,5 +76,4 @@
 import org.openstreetmap.josm.data.osm.search.SearchCompiler;
 import org.openstreetmap.josm.data.osm.search.SearchSetting;
-import org.openstreetmap.josm.data.preferences.StringProperty;
 import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
 import org.openstreetmap.josm.gui.ExtendedDialog;
@@ -105,10 +100,6 @@
 import org.openstreetmap.josm.tools.AlphanumComparator;
 import org.openstreetmap.josm.tools.GBC;
-import org.openstreetmap.josm.tools.HttpClient;
-import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.InputMapUtils;
-import org.openstreetmap.josm.tools.LanguageInfo;
 import org.openstreetmap.josm.tools.Logging;
-import org.openstreetmap.josm.tools.OpenBrowser;
 import org.openstreetmap.josm.tools.Shortcut;
 import org.openstreetmap.josm.tools.Utils;
@@ -182,10 +173,15 @@
 
     private final transient DataSetListenerAdapter dataChangedAdapter = new DataSetListenerAdapter(this);
-    private final HelpAction helpAction = new HelpAction();
-    private final TaginfoAction taginfoAction = new TaginfoAction();
+    private final HelpAction helpAction = new HelpAction(tagTable, editHelper::getDataKey, editHelper::getDataValues,
+            membershipTable, x -> (Relation) membershipData.getValueAt(x, 0));
+    private final TaginfoAction taginfoAction = new TaginfoAction(tagTable, editHelper::getDataKey, editHelper::getDataValues,
+            membershipTable, x -> (Relation) membershipData.getValueAt(x, 0));
     private final PasteValueAction pasteValueAction = new PasteValueAction();
-    private final CopyValueAction copyValueAction = new CopyValueAction();
-    private final CopyKeyValueAction copyKeyValueAction = new CopyKeyValueAction();
-    private final CopyAllKeyValueAction copyAllKeyValueAction = new CopyAllKeyValueAction();
+    private final CopyValueAction copyValueAction = new CopyValueAction(
+            tagTable, editHelper::getDataKey, Main.main::getInProgressSelection);
+    private final CopyKeyValueAction copyKeyValueAction = new CopyKeyValueAction(
+            tagTable, editHelper::getDataKey, Main.main::getInProgressSelection);
+    private final CopyAllKeyValueAction copyAllKeyValueAction = new CopyAllKeyValueAction(
+            tagTable, editHelper::getDataKey, Main.main::getInProgressSelection);
     private final SearchAction searchActionSame = new SearchAction(true);
     private final SearchAction searchActionAny = new SearchAction(false);
@@ -1128,134 +1124,4 @@
     }
 
-    class HelpAction extends AbstractAction {
-        HelpAction() {
-            putValue(NAME, tr("Go to OSM wiki for tag help"));
-            putValue(SHORT_DESCRIPTION, tr("Launch browser with wiki help for selected object"));
-            new ImageProvider("dialogs", "search").getResource().attachImageIcon(this, true);
-            putValue(ACCELERATOR_KEY, getKeyStroke());
-        }
-
-        public KeyStroke getKeyStroke() {
-            return KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0);
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent e) {
-            try {
-                String base = Config.getPref().get("url.openstreetmap-wiki", "https://wiki.openstreetmap.org/wiki/");
-                String lang = LanguageInfo.getWikiLanguagePrefix();
-                final List<URI> uris = new ArrayList<>();
-                int row;
-                if (tagTable.getSelectedRowCount() == 1) {
-                    row = tagTable.getSelectedRow();
-                    String key = Utils.encodeUrl(editHelper.getDataKey(row));
-                    Map<String, Integer> m = editHelper.getDataValues(row);
-                    String val = Utils.encodeUrl(m.entrySet().iterator().next().getKey());
-
-                    uris.add(new URI(String.format("%s%sTag:%s=%s", base, lang, key, val)));
-                    uris.add(new URI(String.format("%sTag:%s=%s", base, key, val)));
-                    uris.add(new URI(String.format("%s%sKey:%s", base, lang, key)));
-                    uris.add(new URI(String.format("%sKey:%s", base, key)));
-                    uris.add(new URI(String.format("%s%sMap_Features", base, lang)));
-                    uris.add(new URI(String.format("%sMap_Features", base)));
-                } else if (membershipTable.getSelectedRowCount() == 1) {
-                    row = membershipTable.getSelectedRow();
-                    String type = ((Relation) membershipData.getValueAt(row, 0)).get("type");
-                    if (type != null) {
-                        type = Utils.encodeUrl(type);
-                    }
-
-                    if (type != null && !type.isEmpty()) {
-                        uris.add(new URI(String.format("%s%sRelation:%s", base, lang, type)));
-                        uris.add(new URI(String.format("%sRelation:%s", base, type)));
-                    }
-
-                    uris.add(new URI(String.format("%s%sRelations", base, lang)));
-                    uris.add(new URI(String.format("%sRelations", base)));
-                } else {
-                    // give the generic help page, if more than one element is selected
-                    uris.add(new URI(String.format("%s%sMap_Features", base, lang)));
-                    uris.add(new URI(String.format("%sMap_Features", base)));
-                }
-
-                MainApplication.worker.execute(() -> displayHelp(uris));
-            } catch (URISyntaxException e1) {
-                Logging.error(e1);
-            }
-        }
-
-        private void displayHelp(final List<URI> uris) {
-            try {
-                // find a page that actually exists in the wiki
-                HttpClient.Response conn;
-                for (URI u : uris) {
-                    conn = HttpClient.create(u.toURL(), "HEAD").connect();
-
-                    if (conn.getResponseCode() != 200) {
-                        conn.disconnect();
-                    } else {
-                        long osize = conn.getContentLength();
-                        if (osize > -1) {
-                            conn.disconnect();
-
-                            final URI newURI = new URI(u.toString()
-                                    .replace("=", "%3D") /* do not URLencode whole string! */
-                                    .replaceFirst("/wiki/", "/w/index.php?redirect=no&title=")
-                            );
-                            conn = HttpClient.create(newURI.toURL(), "HEAD").connect();
-                        }
-
-                        /* redirect pages have different content length, but retrieving a "nonredirect"
-                         *  page using index.php and the direct-link method gives slightly different
-                         *  content lengths, so we have to be fuzzy.. (this is UGLY, recode if u know better)
-                         */
-                        if (osize > -1 && conn.getContentLength() != -1 && Math.abs(conn.getContentLength() - osize) > 200) {
-                            Logging.info("{0} is a mediawiki redirect", u);
-                            conn.disconnect();
-                        } else {
-                            conn.disconnect();
-
-                            OpenBrowser.displayUrl(u.toString());
-                            break;
-                        }
-                    }
-                }
-            } catch (URISyntaxException | IOException e1) {
-                Logging.error(e1);
-            }
-        }
-    }
-
-    class TaginfoAction extends JosmAction {
-
-        final transient StringProperty TAGINFO_URL_PROP = new StringProperty("taginfo.url", "https://taginfo.openstreetmap.org/");
-
-        TaginfoAction() {
-            super(tr("Go to Taginfo"), "dialogs/taginfo", tr("Launch browser with Taginfo statistics for selected object"), null, false);
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent e) {
-            final String url;
-            if (tagTable.getSelectedRowCount() == 1) {
-                final int row = tagTable.getSelectedRow();
-                final String key = Utils.encodeUrl(editHelper.getDataKey(row)).replaceAll("\\+", "%20");
-                Map<String, Integer> values = editHelper.getDataValues(row);
-                if (values.size() == 1) {
-                    url = TAGINFO_URL_PROP.get() + "tags/" + key
-                            + '=' + Utils.encodeUrl(values.keySet().iterator().next()).replaceAll("\\+", "%20");
-                } else {
-                    url = TAGINFO_URL_PROP.get() + "keys/" + key;
-                }
-            } else if (membershipTable.getSelectedRowCount() == 1) {
-                final String type = ((Relation) membershipData.getValueAt(membershipTable.getSelectedRow(), 0)).get("type");
-                url = TAGINFO_URL_PROP.get() + "relations/" + type;
-            } else {
-                return;
-            }
-            OpenBrowser.displayUrl(url);
-        }
-    }
-
     class PasteValueAction extends AbstractAction {
         PasteValueAction() {
@@ -1277,83 +1143,4 @@
     }
 
-    abstract class AbstractCopyAction extends AbstractAction {
-
-        protected abstract Collection<String> getString(OsmPrimitive p, String key);
-
-        @Override
-        public void actionPerformed(ActionEvent ae) {
-            int[] rows = tagTable.getSelectedRows();
-            Set<String> values = new TreeSet<>();
-            Collection<OsmPrimitive> sel = Main.main.getInProgressSelection();
-            if (rows.length == 0 || sel.isEmpty()) return;
-
-            for (int row: rows) {
-                String key = editHelper.getDataKey(row);
-                if (sel.isEmpty())
-                    return;
-                for (OsmPrimitive p : sel) {
-                    Collection<String> s = getString(p, key);
-                    if (s != null) {
-                        values.addAll(s);
-                    }
-                }
-            }
-            if (!values.isEmpty()) {
-                ClipboardUtils.copyString(Utils.join("\n", values));
-            }
-        }
-    }
-
-    class CopyValueAction extends AbstractCopyAction {
-
-        /**
-         * Constructs a new {@code CopyValueAction}.
-         */
-        CopyValueAction() {
-            putValue(NAME, tr("Copy Value"));
-            putValue(SHORT_DESCRIPTION, tr("Copy the value of the selected tag to clipboard"));
-        }
-
-        @Override
-        protected Collection<String> getString(OsmPrimitive p, String key) {
-            String v = p.get(key);
-            return v == null ? null : Collections.singleton(v);
-        }
-    }
-
-    class CopyKeyValueAction extends AbstractCopyAction {
-
-        CopyKeyValueAction() {
-            putValue(NAME, tr("Copy selected Key(s)/Value(s)"));
-            putValue(SHORT_DESCRIPTION, tr("Copy the key and value of the selected tag(s) to clipboard"));
-        }
-
-        @Override
-        protected Collection<String> getString(OsmPrimitive p, String key) {
-            String v = p.get(key);
-            return v == null ? null : Collections.singleton(new Tag(key, v).toString());
-        }
-    }
-
-    class CopyAllKeyValueAction extends AbstractCopyAction {
-
-        CopyAllKeyValueAction() {
-            putValue(NAME, tr("Copy all Keys/Values"));
-            putValue(SHORT_DESCRIPTION, tr("Copy the key and value of all the tags to clipboard"));
-            Shortcut sc = Shortcut.registerShortcut("system:copytags", tr("Edit: {0}", tr("Copy Tags")), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE);
-            MainApplication.registerActionShortcut(this, sc);
-            sc.setAccelerator(this);
-        }
-
-        @Override
-        protected Collection<String> getString(OsmPrimitive p, String key) {
-            List<String> r = new LinkedList<>();
-            for (Entry<String, String> kv : p.getKeys().entrySet()) {
-                r.add(new Tag(kv.getKey(), kv.getValue()).toString());
-            }
-            return r;
-        }
-    }
-
     class SearchAction extends AbstractAction {
         private final boolean sameType;
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/properties/TaginfoAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/properties/TaginfoAction.java	(revision 13521)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/properties/TaginfoAction.java	(revision 13521)
@@ -0,0 +1,73 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.dialogs.properties;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+
+import javax.swing.JTable;
+
+import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.preferences.StringProperty;
+import org.openstreetmap.josm.tools.OpenBrowser;
+import org.openstreetmap.josm.tools.Utils;
+
+/**
+ * Launch browser with Taginfo statistics for selected object.
+ * @since 13521
+ */
+public class TaginfoAction extends JosmAction {
+
+    final transient StringProperty TAGINFO_URL_PROP = new StringProperty("taginfo.url", "https://taginfo.openstreetmap.org/");
+
+    private final JTable tagTable;
+    private final Function<Integer, String> tagKeySupplier;
+    private final Function<Integer, Map<String, Integer>> tagValuesSupplier;
+
+    private final JTable membershipTable;
+    private final Function<Integer, Relation> memberValueSupplier;
+
+    /**
+     * Constructs a new {@code TaginfoAction}.
+     * @param tagTable The tag table. Cannot be null
+     * @param tagKeySupplier Finds the key from given row of tag table. Cannot be null
+     * @param tagValuesSupplier Finds the values from given row of tag table (map of values and number of occurrences). Cannot be null
+     * @param membershipTable The membership table. Can be null
+     * @param memberValueSupplier Finds the parent relation from given row of membership table. Can be null
+     */
+    public TaginfoAction(JTable tagTable, Function<Integer, String> tagKeySupplier, Function<Integer, Map<String, Integer>> tagValuesSupplier,
+            JTable membershipTable, Function<Integer, Relation> memberValueSupplier) {
+        super(tr("Go to Taginfo"), "dialogs/taginfo", tr("Launch browser with Taginfo statistics for selected object"), null, false);
+        this.tagTable = Objects.requireNonNull(tagTable);
+        this.tagKeySupplier = Objects.requireNonNull(tagKeySupplier);
+        this.tagValuesSupplier = Objects.requireNonNull(tagValuesSupplier);
+        this.membershipTable = membershipTable;
+        this.memberValueSupplier = memberValueSupplier;
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        final String url;
+        if (tagTable.getSelectedRowCount() == 1) {
+            final int row = tagTable.getSelectedRow();
+            final String key = Utils.encodeUrl(tagKeySupplier.apply(row)).replaceAll("\\+", "%20");
+            Map<String, Integer> values = tagValuesSupplier.apply(row);
+            if (values.size() == 1) {
+                url = TAGINFO_URL_PROP.get() + "tags/" + key
+                        + '=' + Utils.encodeUrl(values.keySet().iterator().next()).replaceAll("\\+", "%20");
+            } else {
+                url = TAGINFO_URL_PROP.get() + "keys/" + key;
+            }
+        } else if (membershipTable != null && membershipTable.getSelectedRowCount() == 1) {
+            final String type = (memberValueSupplier.apply(membershipTable.getSelectedRow())).get("type");
+            url = TAGINFO_URL_PROP.get() + "relations/" + type;
+        } else {
+            return;
+        }
+        OpenBrowser.displayUrl(url);
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/history/TagInfoViewer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/history/TagInfoViewer.java	(revision 13520)
+++ trunk/src/org/openstreetmap/josm/gui/history/TagInfoViewer.java	(revision 13521)
@@ -4,7 +4,23 @@
 import java.awt.event.FocusEvent;
 import java.awt.event.FocusListener;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Supplier;
 
+import javax.swing.JPopupMenu;
 import javax.swing.JTable;
 import javax.swing.ListSelectionModel;
+
+import org.openstreetmap.josm.data.osm.Tagged;
+import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive;
+import org.openstreetmap.josm.gui.dialogs.properties.CopyAllKeyValueAction;
+import org.openstreetmap.josm.gui.dialogs.properties.CopyKeyValueAction;
+import org.openstreetmap.josm.gui.dialogs.properties.CopyValueAction;
+import org.openstreetmap.josm.gui.dialogs.properties.HelpAction;
+import org.openstreetmap.josm.gui.dialogs.properties.TaginfoAction;
+import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
 
 /**
@@ -16,5 +32,5 @@
  *   <li>on the right, it displays the list of tags for the version at {@link PointInTimeType#CURRENT_POINT_IN_TIME}</li>
  * </ul>
- *
+ * @since 1709
  */
 public class TagInfoViewer extends HistoryViewerPanel {
@@ -46,29 +62,42 @@
     @Override
     protected JTable buildReferenceTable() {
-        JTable table = new JTable(
-                model.getTagTableModel(PointInTimeType.REFERENCE_POINT_IN_TIME),
-                new TagTableColumnModel()
-        );
-        table.setName("table.referencetagtable");
-        setUpDataTransfer(table);
-        return table;
+        return buildTable(PointInTimeType.REFERENCE_POINT_IN_TIME, "table.referencetagtable", model::getReferencePointInTime);
     }
 
     @Override
     protected JTable buildCurrentTable() {
-        JTable table = new JTable(
-                model.getTagTableModel(PointInTimeType.CURRENT_POINT_IN_TIME),
-                new TagTableColumnModel()
-        );
-        table.setName("table.currenttagtable");
-        setUpDataTransfer(table);
-        return table;
+        return buildTable(PointInTimeType.CURRENT_POINT_IN_TIME, "table.currenttagtable", model::getCurrentPointInTime);
     }
 
-    private void setUpDataTransfer(JTable table) {
+    private JTable buildTable(PointInTimeType pointInTime, String name, Supplier<HistoryOsmPrimitive> histoSp) {
+        TagTableModel tagTableModel = model.getTagTableModel(pointInTime);
+        JTable table = new JTable(tagTableModel, new TagTableColumnModel());
+        table.setName(name);
         table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
         selectionSynchronizer.participateInSynchronizedSelection(table.getSelectionModel());
         table.setTransferHandler(new TagInfoTransferHandler());
         table.addFocusListener(new RepaintOnFocusChange());
+        JPopupMenu tagMenu = new JPopupMenu();
+
+        Function<Integer, String> tagKeyFn = x -> (String) table.getValueAt(x, 0);
+        Function<Integer, Map<String, Integer>> tagValuesFn = x -> {
+            Map<String, Integer> map = new HashMap<>();
+            String key = tagTableModel.getValue((String) table.getValueAt(x, 0));
+            if (key != null) {
+                map.put(key, 1);
+            }
+            return map;
+        };
+        Supplier<Collection<? extends Tagged>> objectSp = () -> Arrays.asList(histoSp.get());
+
+        tagMenu.add(new CopyValueAction(table, tagKeyFn, objectSp));
+        tagMenu.add(new CopyKeyValueAction(table, tagKeyFn, objectSp));
+        tagMenu.add(new CopyAllKeyValueAction(table, tagKeyFn, objectSp));
+        tagMenu.addSeparator();
+        tagMenu.add(new HelpAction(table, tagKeyFn, tagValuesFn, null, null));
+        tagMenu.add(new TaginfoAction(table, tagKeyFn, tagValuesFn, null, null));
+
+        table.addMouseListener(new PopupMenuLauncher(tagMenu));
+        return table;
     }
 }
