001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm.event;
003
004import java.util.Collections;
005import java.util.HashSet;
006import java.util.List;
007import java.util.Objects;
008import java.util.concurrent.CopyOnWriteArrayList;
009import java.util.stream.Stream;
010
011import org.openstreetmap.josm.data.SelectionChangedListener;
012import org.openstreetmap.josm.data.osm.DataIntegrityProblemException;
013import org.openstreetmap.josm.data.osm.DataSelectionListener;
014import org.openstreetmap.josm.data.osm.DataSet;
015import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
016import org.openstreetmap.josm.gui.MainApplication;
017import org.openstreetmap.josm.gui.layer.MainLayerManager;
018import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
019import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
020import org.openstreetmap.josm.gui.util.GuiHelper;
021import org.openstreetmap.josm.tools.bugreport.BugReport;
022import org.openstreetmap.josm.tools.bugreport.ReportedException;
023
024/**
025 * Similar like {@link DatasetEventManager}, just for selection events.
026 *
027 * It allows to register listeners to global selection events for the selection in the current edit layer.
028 *
029 * If you want to listen to selections to a specific data layer,
030 * you can register a listener to that layer by using {@link DataSet#addSelectionListener(DataSelectionListener)}
031 *
032 * @since 2912
033 */
034public class SelectionEventManager implements DataSelectionListener, ActiveLayerChangeListener {
035
036    private static final SelectionEventManager INSTANCE = new SelectionEventManager();
037
038    /**
039     * Returns the unique instance.
040     * @return the unique instance
041     */
042    public static SelectionEventManager getInstance() {
043        return INSTANCE;
044    }
045
046    private interface ListenerInfo {
047        void fire(SelectionChangeEvent event);
048    }
049
050    /**
051     * @deprecated to be removed
052     */
053    @Deprecated
054    private static class OldListenerInfo implements ListenerInfo {
055        private final SelectionChangedListener listener;
056
057        OldListenerInfo(SelectionChangedListener listener) {
058            this.listener = listener;
059        }
060
061        @Override
062        public void fire(SelectionChangeEvent event) {
063            listener.selectionChanged(event.getSelection());
064        }
065
066        @Override
067        public int hashCode() {
068            return Objects.hash(listener);
069        }
070
071        @Override
072        public boolean equals(Object o) {
073            if (this == o) return true;
074            if (o == null || getClass() != o.getClass()) return false;
075            OldListenerInfo that = (OldListenerInfo) o;
076            return Objects.equals(listener, that.listener);
077        }
078
079        @Override
080        public String toString() {
081            return "OldListenerInfo [listener=" + listener + ']';
082        }
083    }
084
085    private static class DataListenerInfo implements ListenerInfo {
086        private final DataSelectionListener listener;
087
088        DataListenerInfo(DataSelectionListener listener) {
089            this.listener = listener;
090        }
091
092        @Override
093        public void fire(SelectionChangeEvent event) {
094            listener.selectionChanged(event);
095        }
096
097        @Override
098        public int hashCode() {
099            return Objects.hash(listener);
100        }
101
102        @Override
103        public boolean equals(Object o) {
104            if (this == o) return true;
105            if (o == null || getClass() != o.getClass()) return false;
106            DataListenerInfo that = (DataListenerInfo) o;
107            return Objects.equals(listener, that.listener);
108        }
109
110        @Override
111        public String toString() {
112            return "DataListenerInfo [listener=" + listener + ']';
113        }
114    }
115
116    private final CopyOnWriteArrayList<ListenerInfo> inEDTListeners = new CopyOnWriteArrayList<>();
117    private final CopyOnWriteArrayList<ListenerInfo> immedatelyListeners = new CopyOnWriteArrayList<>();
118
119    /**
120     * Constructs a new {@code SelectionEventManager}.
121     */
122    protected SelectionEventManager() {
123        MainLayerManager layerManager = MainApplication.getLayerManager();
124        // We do not allow for destructing this object.
125        // Currently, this is a singleton class, so this is not required.
126        layerManager.addAndFireActiveLayerChangeListener(this);
127    }
128
129    /**
130     * Registers a new {@code SelectionChangedListener}.
131     *
132     * It is preferred to add a DataSelectionListener - that listener will receive more information about the event.
133     * @param listener listener to add
134     * @param fireMode Set this to IN_EDT_CONSOLIDATED if you want the event to be fired in the EDT thread.
135     *                 Set it to IMMEDIATELY if you want the event to fire in the thread that caused the selection update.
136     * @deprecated Use {@link #addSelectionListener(DataSelectionListener)} or {@link #addSelectionListenerForEdt(DataSelectionListener)}
137     */
138    @Deprecated
139    public void addSelectionListener(SelectionChangedListener listener, FireMode fireMode) {
140        if (fireMode == FireMode.IN_EDT) {
141            throw new UnsupportedOperationException("IN_EDT mode not supported, you probably want to use IN_EDT_CONSOLIDATED.");
142        } else if (fireMode == FireMode.IN_EDT_CONSOLIDATED) {
143            inEDTListeners.addIfAbsent(new OldListenerInfo(listener));
144        } else {
145            immedatelyListeners.addIfAbsent(new OldListenerInfo(listener));
146        }
147    }
148
149    /**
150     * Adds a selection listener that gets notified for selections immediately.
151     * @param listener The listener to add.
152     * @since 12098
153     */
154    public void addSelectionListener(DataSelectionListener listener) {
155        immedatelyListeners.addIfAbsent(new DataListenerInfo(listener));
156    }
157
158    /**
159     * Adds a selection listener that gets notified for selections later in the EDT thread.
160     * Events are sent in the right order but may be delayed.
161     * @param listener The listener to add.
162     * @since 12098
163     */
164    public void addSelectionListenerForEdt(DataSelectionListener listener) {
165        inEDTListeners.addIfAbsent(new DataListenerInfo(listener));
166    }
167
168    /**
169     * Unregisters a {@code SelectionChangedListener}.
170     * @param listener listener to remove
171     * @deprecated use {@link #removeSelectionListener(DataSelectionListener)}
172     */
173    @Deprecated
174    public void removeSelectionListener(SelectionChangedListener listener) {
175        remove(new OldListenerInfo(listener));
176    }
177
178    /**
179     * Unregisters a {@code DataSelectionListener}.
180     * @param listener listener to remove
181     * @since 12098
182     */
183    public void removeSelectionListener(DataSelectionListener listener) {
184        remove(new DataListenerInfo(listener));
185    }
186
187    private void remove(ListenerInfo searchListener) {
188        inEDTListeners.remove(searchListener);
189        immedatelyListeners.remove(searchListener);
190    }
191
192    @Override
193    public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
194        DataSet oldDataSet = e.getPreviousDataSet();
195        if (oldDataSet != null) {
196            // Fake a selection removal
197            // Relying on this allows components to not have to monitor layer changes.
198            // If we would not do this, e.g. the move command would have a hard time tracking which layer
199            // the last moved selection was in.
200            selectionChanged(new SelectionReplaceEvent(oldDataSet,
201                    new HashSet<>(oldDataSet.getAllSelected()), Stream.empty()));
202            oldDataSet.removeSelectionListener(this);
203        }
204        DataSet newDataSet = e.getSource().getActiveDataSet();
205        if (newDataSet != null) {
206            newDataSet.addSelectionListener(this);
207            // Fake a selection add
208            selectionChanged(new SelectionReplaceEvent(newDataSet,
209                    Collections.emptySet(), newDataSet.getAllSelected().stream()));
210        }
211    }
212
213    @Override
214    public void selectionChanged(SelectionChangeEvent event) {
215        fireEvent(immedatelyListeners, event);
216        try {
217            GuiHelper.runInEDTAndWaitWithException(() -> fireEvent(inEDTListeners, event));
218        } catch (ReportedException e) {
219            throw BugReport.intercept(e).put("event", event).put("inEDTListeners", inEDTListeners);
220        }
221    }
222
223    private static void fireEvent(List<ListenerInfo> listeners, SelectionChangeEvent event) {
224        for (ListenerInfo listener: listeners) {
225            try {
226                listener.fire(event);
227            } catch (DataIntegrityProblemException e) {
228                throw BugReport.intercept(e).put("event", event).put("listeners", listeners);
229            }
230        }
231    }
232
233    /**
234     * Only to be used during unit tests, to reset the state. Do not use it in plugins/other code.
235     * Called after the layer manager was reset by the test framework.
236     */
237    public void resetState() {
238        inEDTListeners.clear();
239        immedatelyListeners.clear();
240        MainApplication.getLayerManager().addAndFireActiveLayerChangeListener(this);
241    }
242}