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.osm.DataIntegrityProblemException; 012import org.openstreetmap.josm.data.osm.DataSelectionListener; 013import org.openstreetmap.josm.data.osm.DataSet; 014import org.openstreetmap.josm.gui.MainApplication; 015import org.openstreetmap.josm.gui.layer.MainLayerManager; 016import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent; 017import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener; 018import org.openstreetmap.josm.gui.util.GuiHelper; 019import org.openstreetmap.josm.tools.bugreport.BugReport; 020import org.openstreetmap.josm.tools.bugreport.ReportedException; 021 022/** 023 * Similar like {@link DatasetEventManager}, just for selection events. 024 * 025 * It allows to register listeners to global selection events for the selection in the current edit layer. 026 * 027 * If you want to listen to selections to a specific data layer, 028 * you can register a listener to that layer by using {@link DataSet#addSelectionListener(DataSelectionListener)} 029 * 030 * @since 2912 031 */ 032public class SelectionEventManager implements DataSelectionListener, ActiveLayerChangeListener { 033 034 private static final SelectionEventManager INSTANCE = new SelectionEventManager(); 035 036 /** 037 * Returns the unique instance. 038 * @return the unique instance 039 */ 040 public static SelectionEventManager getInstance() { 041 return INSTANCE; 042 } 043 044 private interface ListenerInfo { 045 void fire(SelectionChangeEvent event); 046 } 047 048 private static class DataListenerInfo implements ListenerInfo { 049 private final DataSelectionListener listener; 050 051 DataListenerInfo(DataSelectionListener listener) { 052 this.listener = listener; 053 } 054 055 @Override 056 public void fire(SelectionChangeEvent event) { 057 listener.selectionChanged(event); 058 } 059 060 @Override 061 public int hashCode() { 062 return Objects.hash(listener); 063 } 064 065 @Override 066 public boolean equals(Object o) { 067 if (this == o) return true; 068 if (o == null || getClass() != o.getClass()) return false; 069 DataListenerInfo that = (DataListenerInfo) o; 070 return Objects.equals(listener, that.listener); 071 } 072 073 @Override 074 public String toString() { 075 return "DataListenerInfo [listener=" + listener + ']'; 076 } 077 } 078 079 private final CopyOnWriteArrayList<ListenerInfo> inEDTListeners = new CopyOnWriteArrayList<>(); 080 private final CopyOnWriteArrayList<ListenerInfo> immediatelyListeners = new CopyOnWriteArrayList<>(); 081 082 /** 083 * Constructs a new {@code SelectionEventManager}. 084 */ 085 protected SelectionEventManager() { 086 MainLayerManager layerManager = MainApplication.getLayerManager(); 087 // We do not allow for destructing this object. 088 // Currently, this is a singleton class, so this is not required. 089 layerManager.addAndFireActiveLayerChangeListener(this); 090 } 091 092 /** 093 * Adds a selection listener that gets notified for selections immediately. 094 * @param listener The listener to add. 095 * @since 12098 096 */ 097 public void addSelectionListener(DataSelectionListener listener) { 098 immediatelyListeners.addIfAbsent(new DataListenerInfo(listener)); 099 } 100 101 /** 102 * Adds a selection listener that gets notified for selections later in the EDT thread. 103 * Events are sent in the right order but may be delayed. 104 * @param listener The listener to add. 105 * @since 12098 106 */ 107 public void addSelectionListenerForEdt(DataSelectionListener listener) { 108 inEDTListeners.addIfAbsent(new DataListenerInfo(listener)); 109 } 110 111 /** 112 * Unregisters a {@code DataSelectionListener}. 113 * @param listener listener to remove 114 * @since 12098 115 */ 116 public void removeSelectionListener(DataSelectionListener listener) { 117 remove(new DataListenerInfo(listener)); 118 } 119 120 private void remove(ListenerInfo searchListener) { 121 inEDTListeners.remove(searchListener); 122 immediatelyListeners.remove(searchListener); 123 } 124 125 @Override 126 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) { 127 DataSet oldDataSet = e.getPreviousDataSet(); 128 if (oldDataSet != null) { 129 // Fake a selection removal 130 // Relying on this allows components to not have to monitor layer changes. 131 // If we would not do this, e.g. the move command would have a hard time tracking which layer 132 // the last moved selection was in. 133 selectionChanged(new SelectionReplaceEvent(oldDataSet, 134 new HashSet<>(oldDataSet.getAllSelected()), Stream.empty())); 135 oldDataSet.removeSelectionListener(this); 136 } 137 DataSet newDataSet = e.getSource().getActiveDataSet(); 138 if (newDataSet != null) { 139 newDataSet.addSelectionListener(this); 140 // Fake a selection add 141 selectionChanged(new SelectionReplaceEvent(newDataSet, 142 Collections.emptySet(), newDataSet.getAllSelected().stream())); 143 } 144 } 145 146 @Override 147 public void selectionChanged(SelectionChangeEvent event) { 148 fireEvent(immediatelyListeners, event); 149 try { 150 GuiHelper.runInEDTAndWaitWithException(() -> fireEvent(inEDTListeners, event)); 151 } catch (ReportedException e) { 152 throw BugReport.intercept(e).put("event", event).put("inEDTListeners", inEDTListeners); 153 } 154 } 155 156 private static void fireEvent(List<ListenerInfo> listeners, SelectionChangeEvent event) { 157 for (ListenerInfo listener: listeners) { 158 try { 159 listener.fire(event); 160 } catch (DataIntegrityProblemException e) { 161 throw BugReport.intercept(e).put("event", event).put("listeners", listeners); 162 } 163 } 164 } 165 166 /** 167 * Only to be used during unit tests, to reset the state. Do not use it in plugins/other code. 168 * Called after the layer manager was reset by the test framework. 169 */ 170 public void resetState() { 171 inEDTListeners.clear(); 172 immediatelyListeners.clear(); 173 MainApplication.getLayerManager().addAndFireActiveLayerChangeListener(this); 174 } 175}