Index: trunk/src/org/openstreetmap/josm/actions/AbstractUploadAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/actions/AbstractUploadAction.java	(revision 15608)
+++ trunk/src/org/openstreetmap/josm/actions/AbstractUploadAction.java	(revision 15609)
@@ -12,5 +12,5 @@
  * Abstract super-class of all upload actions.
  * Listens to layer change events to update its enabled state.
- * @since xxx
+ * @since 15513
  */
 public abstract class AbstractUploadAction extends JosmAction {
Index: trunk/src/org/openstreetmap/josm/data/osm/AbstractDataSourceChangeEvent.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/AbstractDataSourceChangeEvent.java	(revision 15609)
+++ trunk/src/org/openstreetmap/josm/data/osm/AbstractDataSourceChangeEvent.java	(revision 15609)
@@ -0,0 +1,43 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.osm;
+
+import java.util.Set;
+
+import org.openstreetmap.josm.data.DataSource;
+import org.openstreetmap.josm.tools.CheckParameterUtil;
+
+/**
+ * The base class for data source change events
+ *
+ * @author Taylor Smock
+ * @since 15609
+ */
+public abstract class AbstractDataSourceChangeEvent implements DataSourceChangeEvent {
+
+    private DataSet source;
+    private Set<DataSource> old;
+
+    /**
+     * Create a Data Source change event
+     *
+     * @param source The DataSet that is originating the change
+     * @param old    The previous set of DataSources
+     */
+    public AbstractDataSourceChangeEvent(DataSet source, Set<DataSource> old) {
+        CheckParameterUtil.ensureParameterNotNull(source, "source");
+        CheckParameterUtil.ensureParameterNotNull(old, "old");
+        this.source = source;
+        this.old = old;
+    }
+
+    @Override
+    public Set<DataSource> getOldDataSources() {
+        return old;
+    }
+
+    @Override
+    public DataSet getSource() {
+        return source;
+    }
+}
+
Index: trunk/src/org/openstreetmap/josm/data/osm/DataSet.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/DataSet.java	(revision 15608)
+++ trunk/src/org/openstreetmap/josm/data/osm/DataSet.java	(revision 15609)
@@ -11,4 +11,5 @@
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
@@ -43,4 +44,6 @@
 import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
 import org.openstreetmap.josm.data.osm.event.DataSetListener;
+import org.openstreetmap.josm.data.osm.event.DataSourceAddedEvent;
+import org.openstreetmap.josm.data.osm.event.DataSourceRemovedEvent;
 import org.openstreetmap.josm.data.osm.event.FilterChangedEvent;
 import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
@@ -166,4 +169,9 @@
     private final Collection<DataSource> dataSources = new LinkedList<>();
 
+    /**
+     * A list of listeners that listen to DataSource changes on this layer
+     */
+    private final ListenerList<DataSourceListener> dataSourceListeners = ListenerList.create();
+
     private final ConflictCollection conflicts = new ConflictCollection();
 
@@ -226,7 +234,10 @@
                         .collect(Collectors.toList()));
             }
+            DataSourceAddedEvent addedEvent = new DataSourceAddedEvent(this,
+                    new LinkedHashSet<>(dataSources), copyFrom.dataSources.stream());
             for (DataSource source : copyFrom.dataSources) {
                 dataSources.add(new DataSource(source));
             }
+            dataSourceListeners.fireEvent(d -> d.dataSourceChange(addedEvent));
             version = copyFrom.version;
             uploadPolicy = copyFrom.uploadPolicy;
@@ -272,4 +283,6 @@
      */
     public synchronized boolean addDataSources(Collection<DataSource> sources) {
+        DataSourceAddedEvent addedEvent = new DataSourceAddedEvent(this,
+                new LinkedHashSet<>(dataSources), sources.stream());
         boolean changed = dataSources.addAll(sources);
         if (changed) {
@@ -277,4 +290,5 @@
             cachedDataSourceBounds = null;
         }
+        dataSourceListeners.fireEvent(d -> d.dataSourceChange(addedEvent));
         return changed;
     }
@@ -573,4 +587,28 @@
     public void removeHighlightUpdateListener(HighlightUpdateListener listener) {
         highlightUpdateListeners.removeListener(listener);
+    }
+
+    /**
+     * Adds a listener that gets notified whenever the data sources change
+     *
+     * @param listener The listener
+     * @see #removeDataSourceListener
+     * @see #getDataSources
+     * @since 15609
+     */
+    public void addDataSourceListener(DataSourceListener listener) {
+        dataSourceListeners.addListener(listener);
+    }
+
+    /**
+     * Removes a listener that gets notified whenever the data sources change
+     *
+     * @param listener The listener
+     * @see #addDataSourceListener
+     * @see #getDataSources
+     * @since 15609
+     */
+    public void removeDataSourceListener(DataSourceListener listener) {
+        dataSourceListeners.removeListener(listener);
     }
 
@@ -1085,5 +1123,10 @@
             synchronized (from) {
                 if (!from.dataSources.isEmpty()) {
-                    if (dataSources.addAll(from.dataSources)) {
+                    DataSourceAddedEvent addedEvent = new DataSourceAddedEvent(
+                            this, new LinkedHashSet<>(dataSources), from.dataSources.stream());
+                    DataSourceRemovedEvent clearEvent = new DataSourceRemovedEvent(
+                            this, new LinkedHashSet<>(from.dataSources), from.dataSources.stream());
+                    if (from.dataSources.stream().filter(dataSource -> !dataSources.contains(dataSource))
+                            .map(dataSources::add).filter(Boolean.TRUE::equals).count() > 0) {
                         cachedDataSourceArea = null;
                         cachedDataSourceBounds = null;
@@ -1092,4 +1135,6 @@
                     from.cachedDataSourceArea = null;
                     from.cachedDataSourceBounds = null;
+                    dataSourceListeners.fireEvent(d -> d.dataSourceChange(addedEvent));
+                    from.dataSourceListeners.fireEvent(d -> d.dataSourceChange(clearEvent));
                 }
             }
Index: trunk/src/org/openstreetmap/josm/data/osm/DataSourceChangeEvent.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/DataSourceChangeEvent.java	(revision 15609)
+++ trunk/src/org/openstreetmap/josm/data/osm/DataSourceChangeEvent.java	(revision 15609)
@@ -0,0 +1,77 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.osm;
+
+import java.util.Set;
+
+import org.openstreetmap.josm.data.DataSource;
+
+/**
+ * The event that is fired when the data source list is changed.
+ *
+ * @author Taylor Smock
+ * @since 15609
+ */
+public interface DataSourceChangeEvent {
+    /**
+     * Gets the previous data source list
+     * <p>
+     * This collection cannot be modified and will not change.
+     *
+     * @return The old data source list
+     */
+    Set<DataSource> getOldDataSources();
+
+    /**
+     * Gets the new data sources. New data sources are added to the end of the
+     * collection.
+     * <p>
+     * This collection cannot be modified and will not change.
+     *
+     * @return The new data sources
+     */
+    Set<DataSource> getDataSources();
+
+    /**
+     * Gets the Data Sources that have been removed from the selection.
+     * <p>
+     * Those are the primitives contained in {@link #getOldDataSources()} but not in
+     * {@link #getDataSources()}
+     * <p>
+     * This collection cannot be modified and will not change.
+     *
+     * @return The DataSources that were removed
+     */
+    Set<DataSource> getRemoved();
+
+    /**
+     * Gets the data sources that have been added to the selection.
+     * <p>
+     * Those are the data sources contained in {@link #getDataSources()} but not in
+     * {@link #getOldDataSources()}
+     * <p>
+     * This collection cannot be modified and will not change.
+     *
+     * @return The data sources that were added
+     */
+    Set<DataSource> getAdded();
+
+    /**
+     * Gets the data set that triggered this selection event.
+     *
+     * @return The data set.
+     */
+    DataSet getSource();
+
+    /**
+     * Test if this event did not change anything.
+     * <p>
+     * This will return <code>false</code> for all events that are sent to
+     * listeners, so you don't need to test it.
+     *
+     * @return <code>true</code> if this did not change the selection.
+     */
+    default boolean isNop() {
+        return getAdded().isEmpty() && getRemoved().isEmpty();
+    }
+}
+
Index: trunk/src/org/openstreetmap/josm/data/osm/DataSourceListener.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/DataSourceListener.java	(revision 15609)
+++ trunk/src/org/openstreetmap/josm/data/osm/DataSourceListener.java	(revision 15609)
@@ -0,0 +1,22 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.osm;
+
+/**
+ * This is a listener that listens to selection change events in the data set.
+ *
+ * @author Taylor Smock
+ * @since 15609
+ */
+@FunctionalInterface
+public interface DataSourceListener {
+    /**
+     * Called whenever the data source list is changed.
+     *
+     * You get notified about the new data source list, the sources that were added
+     * and removed and the dataset that triggered the event.
+     *
+     * @param event The data source change event.
+     * @see DataSourceChangeEvent
+     */
+    void dataSourceChange(DataSourceChangeEvent event);
+}
Index: trunk/src/org/openstreetmap/josm/data/osm/event/DataSourceAddedEvent.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/event/DataSourceAddedEvent.java	(revision 15609)
+++ trunk/src/org/openstreetmap/josm/data/osm/event/DataSourceAddedEvent.java	(revision 15609)
@@ -0,0 +1,66 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.osm.event;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.openstreetmap.josm.data.DataSource;
+import org.openstreetmap.josm.data.osm.AbstractDataSourceChangeEvent;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.tools.CheckParameterUtil;
+
+/**
+ * There is a new data source
+ *
+ * @author Taylor Smock
+ * @since 15609
+ */
+public class DataSourceAddedEvent extends AbstractDataSourceChangeEvent {
+    private Set<DataSource> current;
+    private Set<DataSource> removed;
+    private final Set<DataSource> added;
+
+    /**
+     * Create a Data Source change event
+     *
+     * @param source         The DataSet that is originating the change
+     * @param old            The previous set of DataSources
+     * @param newDataSources The data sources that are being added
+     */
+    public DataSourceAddedEvent(DataSet source, Set<DataSource> old, Stream<DataSource> newDataSources) {
+        super(source, old);
+        CheckParameterUtil.ensureParameterNotNull(newDataSources, "newDataSources");
+        this.added = newDataSources.collect(Collectors.toCollection(LinkedHashSet::new));
+    }
+
+    @Override
+    public Set<DataSource> getDataSources() {
+        if (current == null) {
+            current = new LinkedHashSet<>(getOldDataSources());
+            current.addAll(added);
+        }
+        return current;
+    }
+
+    @Override
+    public synchronized Set<DataSource> getRemoved() {
+        if (removed == null) {
+            removed = getOldDataSources().stream().filter(s -> !getDataSources().contains(s))
+                    .collect(Collectors.toCollection(LinkedHashSet::new));
+        }
+        return removed;
+    }
+
+    @Override
+    public synchronized Set<DataSource> getAdded() {
+        return added;
+    }
+
+    @Override
+    public String toString() {
+        return "DataSourceAddedEvent [current=" + current + ", removed=" + removed + ", added=" + added + ']';
+    }
+}
+
Index: trunk/src/org/openstreetmap/josm/data/osm/event/DataSourceRemovedEvent.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/event/DataSourceRemovedEvent.java	(revision 15609)
+++ trunk/src/org/openstreetmap/josm/data/osm/event/DataSourceRemovedEvent.java	(revision 15609)
@@ -0,0 +1,66 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.osm.event;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.openstreetmap.josm.data.DataSource;
+import org.openstreetmap.josm.data.osm.AbstractDataSourceChangeEvent;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.tools.CheckParameterUtil;
+
+/**
+ * A data source was removed
+ *
+ * @author Taylor Smock
+ * @since 15609
+ */
+public class DataSourceRemovedEvent extends AbstractDataSourceChangeEvent {
+    private Set<DataSource> current;
+    private final Set<DataSource> removed;
+    private Set<DataSource> added;
+
+    /**
+     * Create a Data Source change event
+     *
+     * @param source             The DataSet that is originating the change
+     * @param old                The previous set of DataSources
+     * @param removedDataSources The data sources that are being removed
+     */
+    public DataSourceRemovedEvent(DataSet source, Set<DataSource> old, Stream<DataSource> removedDataSources) {
+        super(source, old);
+        CheckParameterUtil.ensureParameterNotNull(removedDataSources, "removedDataSources");
+        this.removed = removedDataSources.collect(Collectors.toCollection(LinkedHashSet::new));
+    }
+
+    @Override
+    public Set<DataSource> getDataSources() {
+        if (current == null) {
+            current = getOldDataSources().stream().filter(s -> !removed.contains(s))
+                    .collect(Collectors.toCollection(LinkedHashSet::new));
+        }
+        return current;
+    }
+
+    @Override
+    public synchronized Set<DataSource> getRemoved() {
+        return removed;
+    }
+
+    @Override
+    public synchronized Set<DataSource> getAdded() {
+        if (added == null) {
+            added = getDataSources().stream().filter(s -> !getOldDataSources().contains(s))
+                    .collect(Collectors.toCollection(LinkedHashSet::new));
+        }
+        return added;
+    }
+
+    @Override
+    public String toString() {
+        return "DataSourceAddedEvent [current=" + current + ", removed=" + removed + ", added=" + added + ']';
+    }
+}
+
