source: josm/trunk/src/org/openstreetmap/josm/gui/history/HistoryBrowserDialogManager.java@ 17318

Last change on this file since 17318 was 16798, checked in by GerdP, 4 years ago

fix #19556: bug reported. after cleaning up all activities

  • create copy of dialogs.values() before iterating over it with hide() as hide() changes dialogs
  • Property svn:eol-style set to native
File size: 9.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.history;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.Component;
8import java.awt.Dimension;
9import java.awt.Point;
10import java.util.ArrayList;
11import java.util.Collection;
12import java.util.Iterator;
13import java.util.LinkedHashMap;
14import java.util.LinkedList;
15import java.util.List;
16import java.util.Map.Entry;
17import java.util.Objects;
18
19import javax.swing.JOptionPane;
20import javax.swing.SwingUtilities;
21
22import org.openstreetmap.josm.data.osm.PrimitiveId;
23import org.openstreetmap.josm.data.osm.history.History;
24import org.openstreetmap.josm.data.osm.history.HistoryDataSet;
25import org.openstreetmap.josm.gui.MainApplication;
26import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
27import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
28import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
29import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
30import org.openstreetmap.josm.gui.util.WindowGeometry;
31import org.openstreetmap.josm.spi.preferences.Config;
32import org.openstreetmap.josm.tools.JosmRuntimeException;
33import org.openstreetmap.josm.tools.Logging;
34import org.openstreetmap.josm.tools.SubclassFilteredCollection;
35import org.openstreetmap.josm.tools.bugreport.BugReportExceptionHandler;
36
37/**
38 * Manager allowing to show/hide history dialogs.
39 * @since 2019
40 */
41public final class HistoryBrowserDialogManager implements LayerChangeListener {
42
43 private static boolean isUnloaded(PrimitiveId p) {
44 History h = HistoryDataSet.getInstance().getHistory(p);
45 if (h == null)
46 // reload if the history is not in the cache yet
47 return true;
48 else
49 // reload if the history object of the selected object is not in the cache yet
50 return !p.isNew() && h.getByVersion(p.getUniqueId()) == null;
51 }
52
53 private static final String WINDOW_GEOMETRY_PREF = HistoryBrowserDialogManager.class.getName() + ".geometry";
54
55 private static HistoryBrowserDialogManager instance;
56
57 private final LinkedHashMap<Long, HistoryBrowserDialog> dialogs = new LinkedHashMap<>();
58
59 private static final List<HistoryHook> hooks = new ArrayList<>();
60
61 private HistoryBrowserDialogManager() {
62 MainApplication.getLayerManager().addLayerChangeListener(this);
63 }
64
65 /**
66 * Replies the unique instance.
67 * @return the unique instance
68 */
69 public static synchronized HistoryBrowserDialogManager getInstance() {
70 if (instance == null) {
71 instance = new HistoryBrowserDialogManager();
72 }
73 return instance;
74 }
75
76 /**
77 * Determines if an history dialog exists for the given object id.
78 * @param id the object id
79 * @return {@code true} if an history dialog exists for the given object id, {@code false} otherwise
80 */
81 public boolean existsDialog(long id) {
82 return dialogs.containsKey(id);
83 }
84
85 private boolean hasDialogWithCloseUpperLeftCorner(Point p) {
86 return dialogs.values().stream()
87 .map(Component::getLocation)
88 .anyMatch(corner -> p.x >= corner.x - 5 && corner.x + 5 >= p.x && p.y >= corner.y - 5 && corner.y + 5 >= p.y);
89 }
90
91 private void placeOnScreen(HistoryBrowserDialog dialog) {
92 WindowGeometry geometry = new WindowGeometry(WINDOW_GEOMETRY_PREF, WindowGeometry.centerOnScreen(new Dimension(850, 500)));
93 geometry.applySafe(dialog);
94 Point p = dialog.getLocation();
95 while (hasDialogWithCloseUpperLeftCorner(p)) {
96 p.x += 20;
97 p.y += 20;
98 }
99 dialog.setLocation(p);
100 }
101
102 /**
103 * Hides the specified history dialog and cleans associated resources.
104 * @param dialog History dialog to hide
105 */
106 public void hide(HistoryBrowserDialog dialog) {
107 for (Iterator<Entry<Long, HistoryBrowserDialog>> it = dialogs.entrySet().iterator(); it.hasNext();) {
108 if (Objects.equals(it.next().getValue(), dialog)) {
109 it.remove();
110 if (dialogs.isEmpty()) {
111 new WindowGeometry(dialog).remember(WINDOW_GEOMETRY_PREF);
112 }
113 break;
114 }
115 }
116 dialog.setVisible(false);
117 dialog.dispose();
118
119 if (!dialogs.isEmpty()) {
120 // see #17270: set focus to last dialog
121 new LinkedList<>(dialogs.values()).getLast().toFront();
122 }
123 }
124
125 /**
126 * Hides and destroys all currently visible history browser dialogs
127 * @since 2448
128 */
129 public void hideAll() {
130 new ArrayList<>(dialogs.values()).forEach(this::hide);
131 }
132
133 /**
134 * Show history dialog for the given history.
135 * @param h History to show
136 * @since 2448
137 */
138 public void show(History h) {
139 if (h == null)
140 return;
141 if (existsDialog(h.getId())) {
142 dialogs.get(h.getId()).toFront();
143 } else {
144 HistoryBrowserDialog dialog = new HistoryBrowserDialog(h);
145 placeOnScreen(dialog);
146 dialog.setVisible(true);
147 dialogs.put(h.getId(), dialog);
148 }
149 }
150
151 /* ----------------------------------------------------------------------------- */
152 /* LayerChangeListener */
153 /* ----------------------------------------------------------------------------- */
154 @Override
155 public void layerAdded(LayerAddEvent e) {
156 // Do nothing
157 }
158
159 @Override
160 public void layerRemoving(LayerRemoveEvent e) {
161 // remove all history browsers if the number of layers drops to 0
162 if (e.getSource().getLayers().isEmpty()) {
163 hideAll();
164 }
165 }
166
167 @Override
168 public void layerOrderChanged(LayerOrderChangeEvent e) {
169 // Do nothing
170 }
171
172 /**
173 * Adds a new {@code HistoryHook}.
174 * @param hook hook to add
175 * @return {@code true} (as specified by {@link Collection#add})
176 * @since 13947
177 */
178 public static boolean addHistoryHook(HistoryHook hook) {
179 return hooks.add(Objects.requireNonNull(hook));
180 }
181
182 /**
183 * Removes an existing {@code HistoryHook}.
184 * @param hook hook to remove
185 * @return {@code true} if this list contained the specified element
186 * @since 13947
187 */
188 public static boolean removeHistoryHook(HistoryHook hook) {
189 return hooks.remove(Objects.requireNonNull(hook));
190 }
191
192 /**
193 * Show history dialog(s) for the given primitive(s).
194 * @param primitives The primitive(s) for which history will be displayed
195 */
196 public void showHistory(final Collection<? extends PrimitiveId> primitives) {
197 showHistory(MainApplication.getMainFrame(), primitives);
198 }
199
200 /**
201 * Show history dialog(s) for the given primitive(s).
202 * @param parent Parent component for displayed dialog boxes
203 * @param primitives The primitive(s) for which history will be displayed
204 * @since 16123
205 */
206 public void showHistory(Component parent, final Collection<? extends PrimitiveId> primitives) {
207 final List<PrimitiveId> realPrimitives = new ArrayList<>(primitives);
208 hooks.forEach(h -> h.modifyRequestedIds(realPrimitives));
209 final Collection<? extends PrimitiveId> notNewPrimitives = SubclassFilteredCollection.filter(realPrimitives, p1 -> !p1.isNew());
210 if (notNewPrimitives.isEmpty()) {
211 JOptionPane.showMessageDialog(
212 parent,
213 tr("Please select at least one already uploaded node, way, or relation."),
214 tr("Warning"),
215 JOptionPane.WARNING_MESSAGE);
216 return;
217 }
218 if (notNewPrimitives.size() > Config.getPref().getInt("warn.open.maxhistory", 5) &&
219 /* I18N english text for value 1 makes no real sense, never called for values <= maxhistory (usually 5) */
220 JOptionPane.OK_OPTION != JOptionPane.showConfirmDialog(MainApplication.getMainFrame(),
221 "<html>" + trn(
222 "You are about to open <b>{0}</b> history dialog.<br/>Do you want to continue?",
223 "You are about to open <b>{0}</b> different history dialogs simultaneously.<br/>Do you want to continue?",
224 notNewPrimitives.size(), notNewPrimitives.size()) + "</html>",
225 tr("Confirmation"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE)) {
226 return;
227 }
228
229 Collection<? extends PrimitiveId> toLoad = SubclassFilteredCollection.filter(notNewPrimitives, HistoryBrowserDialogManager::isUnloaded);
230 if (!toLoad.isEmpty()) {
231 MainApplication.worker.submit(new HistoryLoadTask(parent).addPrimitiveIds(toLoad));
232 }
233
234 Runnable r = () -> {
235 try {
236 for (PrimitiveId p : notNewPrimitives) {
237 final History h = HistoryDataSet.getInstance().getHistory(p);
238 if (h == null) {
239 Logging.warn("{0} not found in HistoryDataSet", p);
240 continue;
241 }
242 SwingUtilities.invokeLater(() -> show(h));
243 }
244 } catch (final JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
245 BugReportExceptionHandler.handleException(e);
246 }
247 };
248 MainApplication.worker.submit(r);
249 }
250}
Note: See TracBrowser for help on using the repository browser.