source: josm/trunk/src/org/openstreetmap/josm/actions/SessionSaveAction.java@ 18466

Last change on this file since 18466 was 18466, checked in by taylor.smock, 3 years ago

Fix #21813: Improve marker handling in sessions and #21923: Improve session workflow/Add "save session" (patch by Bjoeni)

  • Allow saving a previously saved session
  • Add "File" -> "Save Session"
  • Add shortcuts for saving sessions
  • Add warning if a layer in a session is being removed when saving over the session
  • Improve GPX marker handling
File size: 20.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.Component;
9import java.awt.Dimension;
10import java.awt.GridBagLayout;
11import java.awt.event.ActionEvent;
12import java.awt.event.KeyEvent;
13import java.io.File;
14import java.io.IOException;
15import java.lang.ref.WeakReference;
16import java.util.ArrayList;
17import java.util.Arrays;
18import java.util.Collection;
19import java.util.HashMap;
20import java.util.HashSet;
21import java.util.List;
22import java.util.Map;
23import java.util.Objects;
24import java.util.Set;
25import java.util.stream.Collectors;
26import java.util.stream.Stream;
27
28import javax.swing.BorderFactory;
29import javax.swing.JCheckBox;
30import javax.swing.JFileChooser;
31import javax.swing.JLabel;
32import javax.swing.JOptionPane;
33import javax.swing.JPanel;
34import javax.swing.JScrollPane;
35import javax.swing.JTabbedPane;
36import javax.swing.SwingConstants;
37import javax.swing.border.EtchedBorder;
38import javax.swing.filechooser.FileFilter;
39
40import org.openstreetmap.josm.data.PreferencesUtils;
41import org.openstreetmap.josm.data.preferences.BooleanProperty;
42import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
43import org.openstreetmap.josm.gui.ExtendedDialog;
44import org.openstreetmap.josm.gui.HelpAwareOptionPane;
45import org.openstreetmap.josm.gui.MainApplication;
46import org.openstreetmap.josm.gui.MapFrame;
47import org.openstreetmap.josm.gui.MapFrameListener;
48import org.openstreetmap.josm.gui.Notification;
49import org.openstreetmap.josm.gui.layer.AbstractModifiableLayer;
50import org.openstreetmap.josm.gui.layer.Layer;
51import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
52import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
53import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
54import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
55import org.openstreetmap.josm.gui.util.WindowGeometry;
56import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
57import org.openstreetmap.josm.io.session.SessionLayerExporter;
58import org.openstreetmap.josm.io.session.SessionWriter;
59import org.openstreetmap.josm.spi.preferences.Config;
60import org.openstreetmap.josm.tools.GBC;
61import org.openstreetmap.josm.tools.JosmRuntimeException;
62import org.openstreetmap.josm.tools.Logging;
63import org.openstreetmap.josm.tools.MultiMap;
64import org.openstreetmap.josm.tools.Shortcut;
65import org.openstreetmap.josm.tools.UserCancelException;
66import org.openstreetmap.josm.tools.Utils;
67
68/**
69 * Saves a JOSM session
70 * @since 18466
71 */
72public class SessionSaveAction extends DiskAccessAction implements MapFrameListener, LayerChangeListener {
73
74 private transient List<Layer> layers;
75 private transient Map<Layer, SessionLayerExporter> exporters;
76 private transient MultiMap<Layer, Layer> dependencies;
77
78 private static final BooleanProperty SAVE_LOCAL_FILES_PROPERTY = new BooleanProperty("session.savelocal", true);
79 private static final String TOOLTIP_DEFAULT = tr("Save the current session.");
80
81 protected FileFilter joz = new ExtensionFileFilter("joz", "joz", tr("Session file (archive) (*.joz)"));
82 protected FileFilter jos = new ExtensionFileFilter("jos", "jos", tr("Session file (*.jos)"));
83
84 private File removeFileOnSuccess;
85
86 private static String tooltip = TOOLTIP_DEFAULT;
87 protected static File sessionFile;
88 protected static boolean isZipSessionFile;
89 protected static List<WeakReference<Layer>> layersInSessionFile;
90
91 private static final SessionSaveAction instance = new SessionSaveAction();
92
93 /**
94 * Returns the instance
95 * @return the instance
96 */
97 public static final SessionSaveAction getInstance() {
98 return instance;
99 }
100
101 /**
102 * Constructs a new {@code SessionSaveAction}.
103 */
104 public SessionSaveAction() {
105 this(true, false);
106 updateEnabledState();
107 }
108
109 /**
110 * Constructs a new {@code SessionSaveAction}.
111 * @param toolbar Register this action for the toolbar preferences?
112 * @param installAdapters False, if you don't want to install layer changed and selection changed adapters
113 */
114 protected SessionSaveAction(boolean toolbar, boolean installAdapters) {
115 this(tr("Save Session"), "session", TOOLTIP_DEFAULT,
116 Shortcut.registerShortcut("system:savesession", tr("File: {0}", tr("Save Session...")), KeyEvent.VK_S, Shortcut.ALT_CTRL),
117 toolbar, "save-session", installAdapters);
118 setHelpId(ht("/Action/SessionSave"));
119 }
120
121 protected SessionSaveAction(String name, String iconName, String tooltip,
122 Shortcut shortcut, boolean register, String toolbarId, boolean installAdapters) {
123
124 super(name, iconName, tooltip, shortcut, register, toolbarId, installAdapters);
125 addListeners();
126 }
127
128 @Override
129 public void actionPerformed(ActionEvent e) {
130 try {
131 saveSession(false, false);
132 } catch (UserCancelException ignore) {
133 Logging.trace(ignore);
134 }
135 }
136
137 @Override
138 public void destroy() {
139 removeListeners();
140 super.destroy();
141 }
142
143 /**
144 * Attempts to save the session.
145 * @param saveAs true shows the dialog
146 * @param forceSaveAll saves all layers
147 * @return if the session and all layers were successfully saved
148 * @throws UserCancelException when the user has cancelled the save process
149 */
150 public boolean saveSession(boolean saveAs, boolean forceSaveAll) throws UserCancelException {
151 if (!isEnabled()) {
152 return false;
153 }
154
155 removeFileOnSuccess = null;
156
157 SessionSaveAsDialog dlg = new SessionSaveAsDialog();
158 if (saveAs) {
159 dlg.showDialog();
160 if (dlg.getValue() != 1) {
161 throw new UserCancelException();
162 }
163 }
164
165 // TODO: resolve dependencies for layers excluded by the user
166 List<Layer> layersOut = layers.stream()
167 .filter(layer -> exporters.get(layer) != null && exporters.get(layer).shallExport())
168 .collect(Collectors.toList());
169
170 boolean zipRequired = layersOut.stream().map(l -> exporters.get(l))
171 .anyMatch(ex -> ex != null && ex.requiresZip());
172
173 saveAs = !doGetFile(saveAs, zipRequired);
174
175 String fn = sessionFile.getName();
176
177 if (!saveAs && layersInSessionFile != null) {
178 List<String> missingLayers = layersInSessionFile.stream()
179 .map(WeakReference::get)
180 .filter(Objects::nonNull)
181 .filter(l -> !layersOut.contains(l))
182 .map(Layer::getName)
183 .collect(Collectors.toList());
184
185 if (!missingLayers.isEmpty() &&
186 !ConditionalOptionPaneUtil.showConfirmationDialog(
187 "savesession_layerremoved",
188 null,
189 new JLabel("<html>"
190 + trn("The following layer has been removed since the session was last saved:",
191 "The following layers have been removed since the session was last saved:", missingLayers.size())
192 + "<ul><li>"
193 + String.join("<li>", missingLayers)
194 + "</ul><br>"
195 + tr("You are about to overwrite the session file \"{0}\". Would you like to proceed?", fn)),
196 tr("Layers removed"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE,
197 JOptionPane.OK_OPTION)) {
198 throw new UserCancelException();
199 }
200 }
201 setCurrentLayers(layersOut);
202
203
204 if (fn.indexOf('.') == -1) {
205 sessionFile = new File(sessionFile.getPath() + (isZipSessionFile ? ".joz" : ".jos"));
206 if (!SaveActionBase.confirmOverwrite(sessionFile)) {
207 throw new UserCancelException();
208 }
209 }
210
211 Stream<Layer> layersToSaveStream = layersOut.stream()
212 .filter(layer -> layer.isSavable()
213 && layer instanceof AbstractModifiableLayer
214 && ((AbstractModifiableLayer) layer).requiresSaveToFile()
215 && exporters.get(layer) != null
216 && !exporters.get(layer).requiresZip());
217
218 boolean success = true;
219 if (forceSaveAll || SAVE_LOCAL_FILES_PROPERTY.get()) {
220 // individual files must be saved before the session file as the location may change
221 if (layersToSaveStream
222 .map(layer -> SaveAction.getInstance().doSave(layer, true))
223 .collect(Collectors.toList()) // force evaluation of all elements
224 .contains(false)) {
225
226 new Notification(tr("Not all local files referenced by the session file could be saved."
227 + "<br>Make sure you save them before closing JOSM."))
228 .setIcon(JOptionPane.WARNING_MESSAGE)
229 .setDuration(Notification.TIME_LONG)
230 .show();
231 success = false;
232 }
233 } else if (layersToSaveStream.anyMatch(l -> true)) {
234 new Notification(tr("Not all local files referenced by the session file are saved yet."
235 + "<br>Make sure you save them before closing JOSM."))
236 .setIcon(JOptionPane.INFORMATION_MESSAGE)
237 .setDuration(Notification.TIME_LONG)
238 .show();
239 }
240
241 int active = -1;
242 Layer activeLayer = getLayerManager().getActiveLayer();
243 if (activeLayer != null) {
244 active = layersOut.indexOf(activeLayer);
245 }
246
247 SessionWriter sw = new SessionWriter(layersOut, active, exporters, dependencies, isZipSessionFile);
248 try {
249 Notification savingNotification = showSavingNotification(sessionFile.getName());
250 sw.write(sessionFile);
251 SaveActionBase.addToFileOpenHistory(sessionFile);
252 if (removeFileOnSuccess != null) {
253 PreferencesUtils.removeFromList(Config.getPref(), "file-open.history", removeFileOnSuccess.getCanonicalPath());
254 removeFileOnSuccess.delete();
255 removeFileOnSuccess = null;
256 }
257 showSavedNotification(savingNotification, sessionFile.getName());
258 } catch (IOException ex) {
259 Logging.error(ex);
260 HelpAwareOptionPane.showMessageDialogInEDT(
261 MainApplication.getMainFrame(),
262 tr("<html>Could not save session file ''{0}''.<br>Error is:<br>{1}</html>",
263 sessionFile.getName(), Utils.escapeReservedCharactersHTML(ex.getMessage())),
264 tr("IO Error"),
265 JOptionPane.ERROR_MESSAGE,
266 null
267 );
268 success = false;
269 }
270 return success;
271 }
272
273 /**
274 * Sets the current session file. Asks the user if necessary
275 * @param saveAs alwas ask the user
276 * @param zipRequired zip
277 * @return if the user was asked
278 * @throws UserCancelException when the user has cancelled the save process
279 */
280 protected boolean doGetFile(boolean saveAs, boolean zipRequired) throws UserCancelException {
281 if (!saveAs && sessionFile != null) {
282
283 if (isZipSessionFile || !zipRequired)
284 return true;
285
286 Logging.info("Converting *.jos to *.joz because a new layer has been added that requires zip format");
287 String oldPath = sessionFile.getAbsolutePath();
288 int i = oldPath.lastIndexOf('.');
289 File jozFile = new File(i < 0 ? oldPath : oldPath.substring(0, i) + ".joz");
290 if (!jozFile.exists()) {
291 removeFileOnSuccess = sessionFile;
292 setCurrentSession(jozFile, true);
293 return true;
294 }
295 Logging.warn("Asking user to choose a new location for the *.joz file because it already exists");
296 }
297
298 doGetFileChooser(zipRequired);
299 return false;
300 }
301
302 protected void doGetFileChooser(boolean zipRequired) throws UserCancelException {
303 AbstractFileChooser fc;
304
305 if (zipRequired) {
306 fc = createAndOpenFileChooser(false, false, tr("Save Session"), joz, JFileChooser.FILES_ONLY, "lastDirectory");
307 } else {
308 fc = createAndOpenFileChooser(false, false, tr("Save Session"), Arrays.asList(jos, joz), jos,
309 JFileChooser.FILES_ONLY, "lastDirectory");
310 }
311
312 if (fc == null) {
313 throw new UserCancelException();
314 }
315
316 File f = fc.getSelectedFile();
317 FileFilter ff = fc.getFileFilter();
318 boolean zip;
319
320 if (zipRequired || joz.equals(ff)) {
321 zip = true;
322 } else if (jos.equals(ff)) {
323 zip = false;
324 } else {
325 zip = Utils.hasExtension(f.getName(), "joz");
326 }
327 setCurrentSession(f, zip);
328 }
329
330 /**
331 * The "Save Session" dialog
332 */
333 public class SessionSaveAsDialog extends ExtendedDialog {
334
335 /**
336 * Constructs a new {@code SessionSaveAsDialog}.
337 */
338 public SessionSaveAsDialog() {
339 super(MainApplication.getMainFrame(), tr("Save Session"), tr("Save As"), tr("Cancel"));
340 configureContextsensitiveHelp("Action/SessionSaveAs", true /* show help button */);
341 initialize();
342 setButtonIcons("save_as", "cancel");
343 setDefaultButton(1);
344 setRememberWindowGeometry(getClass().getName() + ".geometry",
345 WindowGeometry.centerInWindow(MainApplication.getMainFrame(), new Dimension(450, 450)));
346 setContent(build(), false);
347 }
348
349 /**
350 * Initializes action.
351 */
352 public final void initialize() {
353 layers = new ArrayList<>(getLayerManager().getLayers());
354 exporters = new HashMap<>();
355 dependencies = new MultiMap<>();
356
357 Set<Layer> noExporter = new HashSet<>();
358
359 for (Layer layer : layers) {
360 SessionLayerExporter exporter = null;
361 try {
362 exporter = SessionWriter.getSessionLayerExporter(layer);
363 } catch (IllegalArgumentException | JosmRuntimeException e) {
364 Logging.error(e);
365 }
366 if (exporter != null) {
367 exporters.put(layer, exporter);
368 Collection<Layer> deps = exporter.getDependencies();
369 if (deps != null) {
370 dependencies.putAll(layer, deps);
371 } else {
372 dependencies.putVoid(layer);
373 }
374 } else {
375 noExporter.add(layer);
376 exporters.put(layer, null);
377 }
378 }
379
380 int numNoExporter = 0;
381 WHILE: while (numNoExporter != noExporter.size()) {
382 numNoExporter = noExporter.size();
383 for (Layer layer : layers) {
384 if (noExporter.contains(layer)) continue;
385 for (Layer depLayer : dependencies.get(layer)) {
386 if (noExporter.contains(depLayer)) {
387 noExporter.add(layer);
388 exporters.put(layer, null);
389 break WHILE;
390 }
391 }
392 }
393 }
394 }
395
396 protected final Component build() {
397 JPanel op = new JPanel(new GridBagLayout());
398 JPanel ip = new JPanel(new GridBagLayout());
399 for (Layer layer : layers) {
400 Component exportPanel;
401 SessionLayerExporter exporter = exporters.get(layer);
402 if (exporter == null) {
403 if (!exporters.containsKey(layer)) throw new AssertionError();
404 exportPanel = getDisabledExportPanel(layer);
405 } else {
406 exportPanel = exporter.getExportPanel();
407 }
408 if (exportPanel == null) continue;
409 JPanel wrapper = new JPanel(new GridBagLayout());
410 wrapper.setBorder(BorderFactory.createEtchedBorder(EtchedBorder.RAISED));
411 wrapper.add(exportPanel, GBC.std().fill(GBC.HORIZONTAL));
412 ip.add(wrapper, GBC.eol().fill(GBC.HORIZONTAL).insets(2, 2, 4, 2));
413 }
414 ip.add(GBC.glue(0, 1), GBC.eol().fill(GBC.VERTICAL));
415 JScrollPane sp = new JScrollPane(ip);
416 sp.setBorder(BorderFactory.createEmptyBorder());
417 JPanel p = new JPanel(new GridBagLayout());
418 p.add(sp, GBC.eol().fill());
419 final JTabbedPane tabs = new JTabbedPane();
420 tabs.addTab(tr("Layers"), p);
421 op.add(tabs, GBC.eol().fill());
422 JCheckBox chkSaveLocal = new JCheckBox(tr("Save all local files to disk"), SAVE_LOCAL_FILES_PROPERTY.get());
423 chkSaveLocal.addChangeListener(l -> {
424 SAVE_LOCAL_FILES_PROPERTY.put(chkSaveLocal.isSelected());
425 });
426 op.add(chkSaveLocal);
427 return op;
428 }
429
430 protected final Component getDisabledExportPanel(Layer layer) {
431 JPanel p = new JPanel(new GridBagLayout());
432 JCheckBox include = new JCheckBox();
433 include.setEnabled(false);
434 JLabel lbl = new JLabel(layer.getName(), layer.getIcon(), SwingConstants.LEADING);
435 lbl.setToolTipText(tr("No exporter for this layer"));
436 lbl.setLabelFor(include);
437 lbl.setEnabled(false);
438 p.add(include, GBC.std());
439 p.add(lbl, GBC.std());
440 p.add(GBC.glue(1, 0), GBC.std().fill(GBC.HORIZONTAL));
441 return p;
442 }
443 }
444
445 protected void addListeners() {
446 MainApplication.addMapFrameListener(this);
447 MainApplication.getLayerManager().addLayerChangeListener(this);
448 }
449
450 protected void removeListeners() {
451 MainApplication.removeMapFrameListener(this);
452 MainApplication.getLayerManager().removeLayerChangeListener(this);
453 }
454
455 @Override
456 protected void updateEnabledState() {
457 setEnabled(MainApplication.isDisplayingMapView());
458 }
459
460 @Override
461 public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
462 updateEnabledState();
463 }
464
465 @Override
466 public void layerAdded(LayerAddEvent e) {
467 // not used
468 }
469
470 @Override
471 public void layerRemoving(LayerRemoveEvent e) {
472 if (e.isLastLayer()) {
473 setCurrentSession(null, false);
474 }
475 }
476
477 @Override
478 public void layerOrderChanged(LayerOrderChangeEvent e) {
479 // not used
480 }
481
482 /**
483 * Sets the current session file and the layers included in that file
484 * @param file file
485 * @param zip if it is a zip session file
486 * @param layers layers that are currently represented in the session file
487 */
488 public static void setCurrentSession(File file, boolean zip, List<Layer> layers) {
489 setCurrentLayers(layers);
490 setCurrentSession(file, zip);
491 }
492
493 /**
494 * Sets the current session file
495 * @param file file
496 * @param zip if it is a zip session file
497 */
498 public static void setCurrentSession(File file, boolean zip) {
499 sessionFile = file;
500 isZipSessionFile = zip;
501 if (file == null) {
502 tooltip = TOOLTIP_DEFAULT;
503 } else {
504 tooltip = tr("Save the current session file \"{0}\".", file.getName());
505 }
506 getInstance().setTooltip(tooltip);
507 }
508
509 /**
510 * Sets the layers that are currently represented in the session file
511 * @param layers layers
512 */
513 public static void setCurrentLayers(List<Layer> layers) {
514 layersInSessionFile = layers.stream()
515 .filter(l -> l instanceof AbstractModifiableLayer)
516 .map(WeakReference::new)
517 .collect(Collectors.toList());
518 }
519
520 /**
521 * Returns the tooltip for the component
522 * @return the tooltip for the component
523 */
524 public static String getTooltip() {
525 return tooltip;
526 }
527
528}
Note: See TracBrowser for help on using the repository browser.