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    private static class OldListenerInfo implements ListenerInfo {
051        private final SelectionChangedListener listener;
052
053        OldListenerInfo(SelectionChangedListener listener) {
054            this.listener = listener;
055        }
056
057        @Override
058        public void fire(SelectionChangeEvent event) {
059            listener.selectionChanged(event.getSelection());
060        }
061
062        @Override
063        public int hashCode() {
064            return Objects.hash(listener);
065        }
066
067        @Override
068        public boolean equals(Object o) {
069            if (this == o) return true;
070            if (o == null || getClass() != o.getClass()) return false;
071            OldListenerInfo that = (OldListenerInfo) o;
072            return Objects.equals(listener, that.listener);
073        }
074
075        @Override
076        public String toString() {
077            return "OldListenerInfo [listener=" + listener + ']';
078        }
079    }
080
081    private static class DataListenerInfo implements ListenerInfo {
082        private final DataSelectionListener listener;
083
084        DataListenerInfo(DataSelectionListener listener) {
085            this.listener = listener;
086        }
087
088        @Override
089        public void fire(SelectionChangeEvent event) {
090            listener.selectionChanged(event);
091        }
092
093        @Override
094        public int hashCode() {
095            return Objects.hash(listener);
096        }
097
098        @Override
099        public boolean equals(Object o) {
100            if (this == o) return true;
101            if (o == null || getClass() != o.getClass()) return false;
102            DataListenerInfo that = (DataListenerInfo) o;
103            return Objects.equals(listener, that.listener);
104        }
105
106        @Override
107        public String toString() {
108            return "DataListenerInfo [listener=" + listener + ']';
109        }
110    }
111
112    private final CopyOnWriteArrayList<ListenerInfo> inEDTListeners = new CopyOnWriteArrayList<>();
113    private final CopyOnWriteArrayList<ListenerInfo> immedatelyListeners = new CopyOnWriteArrayList<>();
114
115    /**
116     * Constructs a new {@code SelectionEventManager}.
117     */
118    protected SelectionEventManager() {
119        MainLayerManager layerManager = MainApplication.getLayerManager();
120        // We do not allow for destructing this object.
121        // Currently, this is a singleton class, so this is not required.
122        layerManager.addAndFireActiveLayerChangeListener(this);
123    }
124
125    /**
126     * Registers a new {@code SelectionChangedListener}.
127     *
128     * It is preferred to add a DataSelectionListener - that listener will receive more information about the event.
129     * @param listener listener to add
130     * @param fireMode Set this to IN_EDT_CONSOLIDATED if you want the event to be fired in the EDT thread.
131     *                 Set it to IMMEDIATELY if you want the event to fire in the thread that caused the selection update.
132     */
133    public void addSelectionListener(SelectionChangedListener listener, FireMode fireMode) {
134        if (fireMode == FireMode.IN_EDT) {
135            throw new UnsupportedOperationException("IN_EDT mode not supported, you probably want to use IN_EDT_CONSOLIDATED.");
136        } else if (fireMode == FireMode.IN_EDT_CONSOLIDATED) {
137            inEDTListeners.addIfAbsent(new OldListenerInfo(listener));
138        } else {
139            immedatelyListeners.addIfAbsent(new OldListenerInfo(listener));
140        }
141    }
142
143    /**
144     * Adds a selection listener that gets notified for selections immediately.
145     * @param listener The listener to add.
146     * @since 12098
147     */
148    public void addSelectionListener(DataSelectionListener listener) {
149        immedatelyListeners.addIfAbsent(new DataListenerInfo(listener));
150    }
151
152    /**
153     * Adds a selection listener that gets notified for selections later in the EDT thread.
154     * Events are sent in the right order but may be delayed.
155     * @param listener The listener to add.
156     * @since 12098
157     */
158    public void addSelectionListenerForEdt(DataSelectionListener listener) {
159        inEDTListeners.addIfAbsent(new DataListenerInfo(listener));
160    }
161
162    /**
163     * Unregisters a {@code SelectionChangedListener}.
164     * @param listener listener to remove
165     */
166    public void removeSelectionListener(SelectionChangedListener listener) {
167        remove(new OldListenerInfo(listener));
168    }
169
170    /**
171     * Unregisters a {@code DataSelectionListener}.
172     * @param listener listener to remove
173     * @since 12098
174     */
175    public void removeSelectionListener(DataSelectionListener listener) {
176        remove(new DataListenerInfo(listener));
177    }
178
179    private void remove(ListenerInfo searchListener) {
180        inEDTListeners.remove(searchListener);
181        immedatelyListeners.remove(searchListener);
182    }
183
184    @Override
185    public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
186        DataSet oldDataSet = e.getPreviousDataSet();
187        if (oldDataSet != null) {
188            // Fake a selection removal
189            // Relying on this allows components to not have to monitor layer changes.
190            // If we would not do this, e.g. the move command would have a hard time tracking which layer
191            // the last moved selection was in.
192            selectionChanged(new SelectionReplaceEvent(oldDataSet,
193                    new HashSet<>(oldDataSet.getAllSelected()), Stream.empty()));
194            oldDataSet.removeSelectionListener(this);
195        }
196        DataSet newDataSet = e.getSource().getActiveDataSet();
197        if (newDataSet != null) {
198            newDataSet.addSelectionListener(this);
199            // Fake a selection add
200            selectionChanged(new SelectionReplaceEvent(newDataSet,
201                    Collections.emptySet(), newDataSet.getAllSelected().stream()));
202        }
203    }
204
205    @Override
206    public void selectionChanged(SelectionChangeEvent event) {
207        fireEvent(immedatelyListeners, event);
208        try {
209            GuiHelper.runInEDTAndWaitWithException(() -> fireEvent(inEDTListeners, event));
210        } catch (ReportedException e) {
211            throw BugReport.intercept(e).put("event", event).put("inEDTListeners", inEDTListeners);
212        }
213    }
214
215    private static void fireEvent(List<ListenerInfo> listeners, SelectionChangeEvent event) {
216        for (ListenerInfo listener: listeners) {
217            try {
218                listener.fire(event);
219            } catch (DataIntegrityProblemException e) {
220                throw BugReport.intercept(e).put("event", event).put("listeners", listeners);
221            }
222        }
223    }
224
225    /**
226     * Only to be used during unit tests, to reset the state. Do not use it in plugins/other code.
227     * Called after the layer manager was reset by the test framework.
228     */
229    public void resetState() {
230        inEDTListeners.clear();
231        immedatelyListeners.clear();
232        MainApplication.getLayerManager().addAndFireActiveLayerChangeListener(this);
233    }
234}