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}