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}