Ticket #21923: 21923.patch

File 21923.patch, 49.1 KB (added by Bjoeni, 3 years ago)
  • src/org/openstreetmap/josm/actions/SessionLoadAction.java

     
    205205                    postLoadTasks = reader.getPostLoadTasks();
    206206                    viewport = reader.getViewport();
    207207                    projectionChoice = reader.getProjectionChoice();
     208                    SessionSaveAction.setCurrentSession(file, zip, reader.getLayers());
    208209                } finally {
    209210                    if (tempFile) {
    210211                        Utils.deleteFile(file);
  • src/org/openstreetmap/josm/actions/SessionSaveAction.java

     
     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 xxx
     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/SessionSaveAs"));
     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        MainApplication.addMapFrameListener(this);
     126        MainApplication.getLayerManager().addLayerChangeListener(this);
     127    }
     128
     129    @Override
     130    public void actionPerformed(ActionEvent e) {
     131        try {
     132            saveSession(false, false);
     133        } catch (UserCancelException ignore) {
     134            Logging.trace(ignore);
     135        }
     136    }
     137
     138    @Override
     139    public void destroy() {
     140        MainApplication.removeMapFrameListener(this);
     141        super.destroy();
     142    }
     143
     144    /**
     145     * Attempts to save the session.
     146     * @param saveAs true shows the dialog
     147     * @param forceSaveAll saves all layers
     148     * @return if the session and all layers were successfully saved
     149     * @throws UserCancelException when the user has cancelled the save process
     150     */
     151    public boolean saveSession(boolean saveAs, boolean forceSaveAll) throws UserCancelException {
     152        if (!isEnabled()) {
     153            return false;
     154        }
     155
     156        removeFileOnSuccess = null;
     157
     158        SessionSaveAsDialog dlg = new SessionSaveAsDialog();
     159        if (saveAs) {
     160            dlg.showDialog();
     161            if (dlg.getValue() != 1) {
     162                throw new UserCancelException();
     163            }
     164        }
     165
     166        // TODO: resolve dependencies for layers excluded by the user
     167        List<Layer> layersOut = layers.stream()
     168                .filter(layer -> exporters.get(layer) != null && exporters.get(layer).shallExport())
     169                .collect(Collectors.toList());
     170
     171        boolean zipRequired = layersOut.stream().map(l -> exporters.get(l))
     172                .anyMatch(ex -> ex != null && ex.requiresZip());
     173
     174        saveAs = !doGetFile(saveAs, zipRequired);
     175
     176        String fn = sessionFile.getName();
     177
     178        if (!saveAs && layersInSessionFile != null) {
     179            List<String> missingLayers = layersInSessionFile.stream()
     180                    .map(WeakReference::get)
     181                    .filter(Objects::nonNull)
     182                    .filter(l -> !layersOut.contains(l))
     183                    .map(Layer::getName)
     184                    .collect(Collectors.toList());
     185
     186            if (!missingLayers.isEmpty() &&
     187                    !ConditionalOptionPaneUtil.showConfirmationDialog(
     188                            "savesession_layerremoved",
     189                            null,
     190                            new JLabel("<html>"
     191                                    + trn("The following layer has been removed since the session was last saved:",
     192                                          "The following layers have been removed since the session was last saved:", missingLayers.size())
     193                                    + "<ul><li>"
     194                                    + String.join("<li>", missingLayers)
     195                                    + "</ul><br>"
     196                                    + tr("You are about to overwrite the session file \"{0}\". Would you like to proceed?", fn)),
     197                            tr("Layers removed"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE,
     198                            JOptionPane.OK_OPTION)) {
     199                throw new UserCancelException();
     200            }
     201        }
     202        setCurrentLayers(layersOut);
     203
     204
     205        if (fn.indexOf('.') == -1) {
     206            sessionFile = new File(sessionFile.getPath() + (isZipSessionFile ? ".joz" : ".jos"));
     207            if (!SaveActionBase.confirmOverwrite(sessionFile)) {
     208                throw new UserCancelException();
     209            }
     210        }
     211
     212        Stream<Layer> layersToSaveStream = layersOut.stream()
     213                .filter(layer -> layer.isSavable()
     214                        && layer instanceof AbstractModifiableLayer
     215                        && ((AbstractModifiableLayer) layer).requiresSaveToFile()
     216                        && exporters.get(layer) != null
     217                        && !exporters.get(layer).requiresZip());
     218
     219        boolean success = true;
     220        if (forceSaveAll || SAVE_LOCAL_FILES_PROPERTY.get()) {
     221            // individual files must be saved before the session file as the location may change
     222            if (layersToSaveStream
     223                .map(layer -> SaveAction.getInstance().doSave(layer, true))
     224                .collect(Collectors.toList()) // force evaluation of all elements
     225                .contains(false)) {
     226
     227                new Notification(tr("Not all local files referenced by the session file could be saved."
     228                        + "<br>Make sure you save them before closing JOSM."))
     229                    .setIcon(JOptionPane.WARNING_MESSAGE)
     230                    .setDuration(Notification.TIME_LONG)
     231                    .show();
     232                success = false;
     233            }
     234        } else if (layersToSaveStream.anyMatch(l -> true)) {
     235            new Notification(tr("Not all local files referenced by the session file are saved yet."
     236                    + "<br>Make sure you save them before closing JOSM."))
     237                .setIcon(JOptionPane.INFORMATION_MESSAGE)
     238                .setDuration(Notification.TIME_LONG)
     239                .show();
     240        }
     241
     242        int active = -1;
     243        Layer activeLayer = getLayerManager().getActiveLayer();
     244        if (activeLayer != null) {
     245            active = layersOut.indexOf(activeLayer);
     246        }
     247
     248        SessionWriter sw = new SessionWriter(layersOut, active, exporters, dependencies, isZipSessionFile);
     249        try {
     250            Notification savingNotification = showSavingNotification(sessionFile.getName());
     251            sw.write(sessionFile);
     252            SaveActionBase.addToFileOpenHistory(sessionFile);
     253            if (removeFileOnSuccess != null) {
     254                PreferencesUtils.removeFromList(Config.getPref(), "file-open.history", removeFileOnSuccess.getCanonicalPath());
     255                removeFileOnSuccess.delete();
     256                removeFileOnSuccess = null;
     257            }
     258            showSavedNotification(savingNotification, sessionFile.getName());
     259        } catch (IOException ex) {
     260            Logging.error(ex);
     261            HelpAwareOptionPane.showMessageDialogInEDT(
     262                    MainApplication.getMainFrame(),
     263                    tr("<html>Could not save session file ''{0}''.<br>Error is:<br>{1}</html>",
     264                            sessionFile.getName(), Utils.escapeReservedCharactersHTML(ex.getMessage())),
     265                    tr("IO Error"),
     266                    JOptionPane.ERROR_MESSAGE,
     267                    null
     268            );
     269            success = false;
     270        }
     271        return success;
     272    }
     273
     274    /**
     275     * Sets the current session file. Asks the user if necessary
     276     * @param saveAs alwas ask the user
     277     * @param zipRequired zip
     278     * @return if the user was asked
     279     * @throws UserCancelException when the user has cancelled the save process
     280     */
     281    protected boolean doGetFile(boolean saveAs, boolean zipRequired) throws UserCancelException {
     282        if (!saveAs && sessionFile != null) {
     283
     284            if (isZipSessionFile || !zipRequired)
     285                return true;
     286
     287            Logging.info("Converting *.jos to *.joz because a new layer has been added that requires zip format");
     288            String oldPath = sessionFile.getAbsolutePath();
     289            int i = oldPath.lastIndexOf('.');
     290            File jozFile = new File(i < 0 ? oldPath : oldPath.substring(0, i) + ".joz");
     291            if (!jozFile.exists()) {
     292                removeFileOnSuccess = sessionFile;
     293                setCurrentSession(jozFile, true);
     294                return true;
     295            }
     296            Logging.warn("Asking user to choose a new location for the *.joz file because it already exists");
     297        }
     298
     299        doGetFileChooser(zipRequired);
     300        return false;
     301    }
     302
     303    protected void doGetFileChooser(boolean zipRequired) throws UserCancelException {
     304        AbstractFileChooser fc;
     305
     306        if (zipRequired) {
     307            fc = createAndOpenFileChooser(false, false, tr("Save Session"), joz, JFileChooser.FILES_ONLY, "lastDirectory");
     308        } else {
     309            fc = createAndOpenFileChooser(false, false, tr("Save Session"), Arrays.asList(jos, joz), jos,
     310                    JFileChooser.FILES_ONLY, "lastDirectory");
     311        }
     312
     313        if (fc == null) {
     314            throw new UserCancelException();
     315        }
     316
     317        File f = fc.getSelectedFile();
     318        FileFilter ff = fc.getFileFilter();
     319        boolean zip;
     320
     321        if (zipRequired || joz.equals(ff)) {
     322            zip = true;
     323        } else if (jos.equals(ff)) {
     324            zip = false;
     325        } else {
     326            zip = Utils.hasExtension(f.getName(), "joz");
     327        }
     328        setCurrentSession(f, zip);
     329    }
     330
     331    /**
     332     * The "Save Session" dialog
     333     */
     334    public class SessionSaveAsDialog extends ExtendedDialog {
     335
     336        /**
     337         * Constructs a new {@code SessionSaveAsDialog}.
     338         */
     339        public SessionSaveAsDialog() {
     340            super(MainApplication.getMainFrame(), tr("Save Session"), tr("Save As"), tr("Cancel"));
     341            configureContextsensitiveHelp("Action/SessionSaveAs", true /* show help button */);
     342            initialize();
     343            setButtonIcons("save_as", "cancel");
     344            setDefaultButton(1);
     345            setRememberWindowGeometry(getClass().getName() + ".geometry",
     346                    WindowGeometry.centerInWindow(MainApplication.getMainFrame(), new Dimension(450, 450)));
     347            setContent(build(), false);
     348        }
     349
     350        /**
     351         * Initializes action.
     352         */
     353        public final void initialize() {
     354            layers = new ArrayList<>(getLayerManager().getLayers());
     355            exporters = new HashMap<>();
     356            dependencies = new MultiMap<>();
     357
     358            Set<Layer> noExporter = new HashSet<>();
     359
     360            for (Layer layer : layers) {
     361                SessionLayerExporter exporter = null;
     362                try {
     363                    exporter = SessionWriter.getSessionLayerExporter(layer);
     364                } catch (IllegalArgumentException | JosmRuntimeException e) {
     365                    Logging.error(e);
     366                }
     367                if (exporter != null) {
     368                    exporters.put(layer, exporter);
     369                    Collection<Layer> deps = exporter.getDependencies();
     370                    if (deps != null) {
     371                        dependencies.putAll(layer, deps);
     372                    } else {
     373                        dependencies.putVoid(layer);
     374                    }
     375                } else {
     376                    noExporter.add(layer);
     377                    exporters.put(layer, null);
     378                }
     379            }
     380
     381            int numNoExporter = 0;
     382            WHILE: while (numNoExporter != noExporter.size()) {
     383                numNoExporter = noExporter.size();
     384                for (Layer layer : layers) {
     385                    if (noExporter.contains(layer)) continue;
     386                    for (Layer depLayer : dependencies.get(layer)) {
     387                        if (noExporter.contains(depLayer)) {
     388                            noExporter.add(layer);
     389                            exporters.put(layer, null);
     390                            break WHILE;
     391                        }
     392                    }
     393                }
     394            }
     395        }
     396
     397        protected final Component build() {
     398            JPanel op = new JPanel(new GridBagLayout());
     399            JPanel ip = new JPanel(new GridBagLayout());
     400            for (Layer layer : layers) {
     401                Component exportPanel;
     402                SessionLayerExporter exporter = exporters.get(layer);
     403                if (exporter == null) {
     404                    if (!exporters.containsKey(layer)) throw new AssertionError();
     405                    exportPanel = getDisabledExportPanel(layer);
     406                } else {
     407                    exportPanel = exporter.getExportPanel();
     408                }
     409                if (exportPanel == null) continue;
     410                JPanel wrapper = new JPanel(new GridBagLayout());
     411                wrapper.setBorder(BorderFactory.createEtchedBorder(EtchedBorder.RAISED));
     412                wrapper.add(exportPanel, GBC.std().fill(GBC.HORIZONTAL));
     413                ip.add(wrapper, GBC.eol().fill(GBC.HORIZONTAL).insets(2, 2, 4, 2));
     414            }
     415            ip.add(GBC.glue(0, 1), GBC.eol().fill(GBC.VERTICAL));
     416            JScrollPane sp = new JScrollPane(ip);
     417            sp.setBorder(BorderFactory.createEmptyBorder());
     418            JPanel p = new JPanel(new GridBagLayout());
     419            p.add(sp, GBC.eol().fill());
     420            final JTabbedPane tabs = new JTabbedPane();
     421            tabs.addTab(tr("Layers"), p);
     422            op.add(tabs, GBC.eol().fill());
     423            JCheckBox chkSaveLocal = new JCheckBox(tr("Save all local files to disk"), SAVE_LOCAL_FILES_PROPERTY.get());
     424            chkSaveLocal.addChangeListener(l -> {
     425                SAVE_LOCAL_FILES_PROPERTY.put(chkSaveLocal.isSelected());
     426            });
     427            op.add(chkSaveLocal);
     428            return op;
     429        }
     430
     431        protected final Component getDisabledExportPanel(Layer layer) {
     432            JPanel p = new JPanel(new GridBagLayout());
     433            JCheckBox include = new JCheckBox();
     434            include.setEnabled(false);
     435            JLabel lbl = new JLabel(layer.getName(), layer.getIcon(), SwingConstants.LEADING);
     436            lbl.setToolTipText(tr("No exporter for this layer"));
     437            lbl.setLabelFor(include);
     438            lbl.setEnabled(false);
     439            p.add(include, GBC.std());
     440            p.add(lbl, GBC.std());
     441            p.add(GBC.glue(1, 0), GBC.std().fill(GBC.HORIZONTAL));
     442            return p;
     443        }
     444    }
     445
     446    @Override
     447    protected void updateEnabledState() {
     448        setEnabled(MainApplication.isDisplayingMapView());
     449    }
     450
     451    @Override
     452    public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
     453        updateEnabledState();
     454    }
     455
     456    @Override
     457    public void layerAdded(LayerAddEvent e) {
     458        // not used
     459    }
     460
     461    @Override
     462    public void layerRemoving(LayerRemoveEvent e) {
     463        if (e.isLastLayer()) { //TODO CHECK
     464            setCurrentSession(null, false);
     465        }
     466    }
     467
     468    @Override
     469    public void layerOrderChanged(LayerOrderChangeEvent e) {
     470        // not used
     471    }
     472
     473    /**
     474     * Sets the current session file and the layers included in that file
     475     * @param file file
     476     * @param zip if it is a zip session file
     477     * @param layers layers that are currently represented in the session file
     478     */
     479    public static void setCurrentSession(File file, boolean zip, List<Layer> layers) {
     480        setCurrentLayers(layers);
     481        setCurrentSession(file, zip);
     482    }
     483
     484    /**
     485     * Sets the current session file
     486     * @param file file
     487     * @param zip if it is a zip session file
     488     */
     489    public static void setCurrentSession(File file, boolean zip) {
     490        sessionFile = file;
     491        isZipSessionFile = zip;
     492        if (file == null) {
     493            tooltip = TOOLTIP_DEFAULT;
     494        } else {
     495            tooltip = tr("Save the current session file \"{0}\".", file.getName());
     496        }
     497        getInstance().setTooltip(tooltip);
     498    }
     499
     500    /**
     501     * Sets the layers that are currently represented in the session file
     502     * @param layers layers
     503     */
     504    public static void setCurrentLayers(List<Layer> layers) {
     505        layersInSessionFile = layers.stream()
     506                .filter(l -> l instanceof AbstractModifiableLayer)
     507                .map(WeakReference::new)
     508                .collect(Collectors.toList());
     509    }
     510
     511    /**
     512     * Returns the tooltip for the component
     513     * @return the tooltip for the component
     514     */
     515    public static String getTooltip() {
     516        return tooltip;
     517    }
     518
     519}
  • src/org/openstreetmap/josm/actions/SessionSaveAsAction.java

     
    44import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
    55import static org.openstreetmap.josm.tools.I18n.tr;
    66
    7 import java.awt.Component;
    8 import java.awt.Dimension;
    9 import java.awt.GridBagLayout;
    107import java.awt.event.ActionEvent;
    11 import java.io.File;
    12 import java.io.IOException;
    13 import java.util.ArrayList;
    14 import java.util.Arrays;
    15 import java.util.Collection;
    16 import java.util.HashMap;
    17 import java.util.HashSet;
    18 import java.util.List;
    19 import java.util.Map;
    20 import java.util.Set;
    21 import java.util.stream.Collectors;
    22 import java.util.stream.Stream;
    23 
    24 import javax.swing.BorderFactory;
    25 import javax.swing.JCheckBox;
    26 import javax.swing.JFileChooser;
    27 import javax.swing.JLabel;
    28 import javax.swing.JOptionPane;
    29 import javax.swing.JPanel;
    30 import javax.swing.JScrollPane;
    31 import javax.swing.JTabbedPane;
    32 import javax.swing.SwingConstants;
    33 import javax.swing.border.EtchedBorder;
    34 import javax.swing.filechooser.FileFilter;
     8import java.awt.event.KeyEvent;
    359
    36 import org.openstreetmap.josm.data.preferences.BooleanProperty;
    37 import org.openstreetmap.josm.gui.ExtendedDialog;
    38 import org.openstreetmap.josm.gui.HelpAwareOptionPane;
    3910import org.openstreetmap.josm.gui.MainApplication;
    40 import org.openstreetmap.josm.gui.MapFrame;
    41 import org.openstreetmap.josm.gui.MapFrameListener;
    42 import org.openstreetmap.josm.gui.Notification;
    43 import org.openstreetmap.josm.gui.layer.AbstractModifiableLayer;
    44 import org.openstreetmap.josm.gui.layer.Layer;
    45 import org.openstreetmap.josm.gui.util.WindowGeometry;
    46 import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
    47 import org.openstreetmap.josm.io.session.SessionLayerExporter;
    48 import org.openstreetmap.josm.io.session.SessionWriter;
    49 import org.openstreetmap.josm.tools.GBC;
    50 import org.openstreetmap.josm.tools.JosmRuntimeException;
    5111import org.openstreetmap.josm.tools.Logging;
    52 import org.openstreetmap.josm.tools.MultiMap;
     12import org.openstreetmap.josm.tools.Shortcut;
    5313import org.openstreetmap.josm.tools.UserCancelException;
    54 import org.openstreetmap.josm.tools.Utils;
    5514
    5615/**
    57  * Saves a JOSM session
     16 * Saves a JOSM session to a new file
    5817 * @since 4685
    5918 */
    60 public class SessionSaveAsAction extends DiskAccessAction implements MapFrameListener {
    61 
    62     private transient List<Layer> layers;
    63     private transient Map<Layer, SessionLayerExporter> exporters;
    64     private transient MultiMap<Layer, Layer> dependencies;
    65 
    66     private static final BooleanProperty SAVE_LOCAL_FILES_PROPERTY = new BooleanProperty("session.savelocal", true);
     19public class SessionSaveAsAction extends SessionSaveAction {
    6720
    6821    /**
    6922     * Constructs a new {@code SessionSaveAsAction}.
     
    7932     * @param installAdapters False, if you don't want to install layer changed and selection changed adapters
    8033     */
    8134    protected SessionSaveAsAction(boolean toolbar, boolean installAdapters) {
     35
    8236        super(tr("Save Session As..."), "session", tr("Save the current session to a new file."),
    83                 null, toolbar, "save_as-session", installAdapters);
     37                Shortcut.registerShortcut("system:savesessionas", tr("File: {0}", tr("Save Session As...")),
     38                        KeyEvent.VK_S, Shortcut.ALT_CTRL_SHIFT),
     39                toolbar, "save_as-session", installAdapters);
     40
    8441        setHelpId(ht("/Action/SessionSaveAs"));
    8542        MainApplication.addMapFrameListener(this);
    8643    }
     
    8845    @Override
    8946    public void actionPerformed(ActionEvent e) {
    9047        try {
    91             saveSession();
     48            saveSession(true, false);
    9249        } catch (UserCancelException ignore) {
    9350            Logging.trace(ignore);
    9451        }
    9552    }
    9653
    97     @Override
    98     public void destroy() {
    99         MainApplication.removeMapFrameListener(this);
    100         super.destroy();
    101     }
    102 
    103     /**
    104      * Attempts to save the session.
    105      * @throws UserCancelException when the user has cancelled the save process.
    106      * @since 8913
    107      */
    108     public void saveSession() throws UserCancelException {
    109         if (!isEnabled()) {
    110             return;
    111         }
    112 
    113         SessionSaveAsDialog dlg = new SessionSaveAsDialog();
    114         dlg.showDialog();
    115         if (dlg.getValue() != 1) {
    116             throw new UserCancelException();
    117         }
    118 
    119         boolean zipRequired = layers.stream().map(l -> exporters.get(l))
    120                 .anyMatch(ex -> ex != null && ex.requiresZip());
    121 
    122         FileFilter joz = new ExtensionFileFilter("joz", "joz", tr("Session file (archive) (*.joz)"));
    123         FileFilter jos = new ExtensionFileFilter("jos", "jos", tr("Session file (*.jos)"));
    124 
    125         AbstractFileChooser fc;
    126 
    127         if (zipRequired) {
    128             fc = createAndOpenFileChooser(false, false, tr("Save Session"), joz, JFileChooser.FILES_ONLY, "lastDirectory");
    129         } else {
    130             fc = createAndOpenFileChooser(false, false, tr("Save Session"), Arrays.asList(jos, joz), jos,
    131                     JFileChooser.FILES_ONLY, "lastDirectory");
    132         }
    133 
    134         if (fc == null) {
    135             throw new UserCancelException();
    136         }
    137 
    138         File file = fc.getSelectedFile();
    139         String fn = file.getName();
    140 
    141         boolean zip;
    142         FileFilter ff = fc.getFileFilter();
    143         if (zipRequired || joz.equals(ff)) {
    144             zip = true;
    145         } else if (jos.equals(ff)) {
    146             zip = false;
    147         } else {
    148             if (Utils.hasExtension(fn, "joz")) {
    149                 zip = true;
    150             } else {
    151                 zip = false;
    152             }
    153         }
    154         if (fn.indexOf('.') == -1) {
    155             file = new File(file.getPath() + (zip ? ".joz" : ".jos"));
    156             if (!SaveActionBase.confirmOverwrite(file)) {
    157                 throw new UserCancelException();
    158             }
    159         }
    160 
    161         // TODO: resolve dependencies for layers excluded by the user
    162         List<Layer> layersOut = layers.stream()
    163                 .filter(layer -> exporters.get(layer) != null && exporters.get(layer).shallExport())
    164                 .collect(Collectors.toList());
    165 
    166         Stream<Layer> layersToSaveStream = layersOut.stream()
    167                 .filter(layer -> layer.isSavable()
    168                         && layer instanceof AbstractModifiableLayer
    169                         && ((AbstractModifiableLayer) layer).requiresSaveToFile()
    170                         && exporters.get(layer) != null
    171                         && !exporters.get(layer).requiresZip());
    172 
    173         if (SAVE_LOCAL_FILES_PROPERTY.get()) {
    174             // individual files must be saved before the session file as the location may change
    175             if (layersToSaveStream
    176                 .map(layer -> SaveAction.getInstance().doSave(layer, true))
    177                 .collect(Collectors.toList()) // force evaluation of all elements
    178                 .contains(false)) {
    179 
    180                 new Notification(tr("Not all local files referenced by the session file could be saved."
    181                         + "<br>Make sure you save them before closing JOSM."))
    182                     .setIcon(JOptionPane.WARNING_MESSAGE)
    183                     .setDuration(Notification.TIME_LONG)
    184                     .show();
    185             }
    186         } else if (layersToSaveStream.anyMatch(l -> true)) {
    187             new Notification(tr("Not all local files referenced by the session file are saved yet."
    188                     + "<br>Make sure you save them before closing JOSM."))
    189                 .setIcon(JOptionPane.INFORMATION_MESSAGE)
    190                 .setDuration(Notification.TIME_LONG)
    191                 .show();
    192         }
    193 
    194         int active = -1;
    195         Layer activeLayer = getLayerManager().getActiveLayer();
    196         if (activeLayer != null) {
    197             active = layersOut.indexOf(activeLayer);
    198         }
    199 
    200         SessionWriter sw = new SessionWriter(layersOut, active, exporters, dependencies, zip);
    201         try {
    202             Notification savingNotification = showSavingNotification(file.getName());
    203             sw.write(file);
    204             SaveActionBase.addToFileOpenHistory(file);
    205             showSavedNotification(savingNotification, file.getName());
    206         } catch (IOException ex) {
    207             Logging.error(ex);
    208             HelpAwareOptionPane.showMessageDialogInEDT(
    209                     MainApplication.getMainFrame(),
    210                     tr("<html>Could not save session file ''{0}''.<br>Error is:<br>{1}</html>",
    211                             file.getName(), Utils.escapeReservedCharactersHTML(ex.getMessage())),
    212                     tr("IO Error"),
    213                     JOptionPane.ERROR_MESSAGE,
    214                     null
    215             );
    216         }
    217     }
    218 
    219     /**
    220      * The "Save Session" dialog
    221      */
    222     public class SessionSaveAsDialog extends ExtendedDialog {
    223 
    224         /**
    225          * Constructs a new {@code SessionSaveAsDialog}.
    226          */
    227         public SessionSaveAsDialog() {
    228             super(MainApplication.getMainFrame(), tr("Save Session"), tr("Save As"), tr("Cancel"));
    229             configureContextsensitiveHelp("Action/SessionSaveAs", true /* show help button */);
    230             initialize();
    231             setButtonIcons("save_as", "cancel");
    232             setDefaultButton(1);
    233             setRememberWindowGeometry(getClass().getName() + ".geometry",
    234                     WindowGeometry.centerInWindow(MainApplication.getMainFrame(), new Dimension(450, 450)));
    235             setContent(build(), false);
    236         }
    237 
    238         /**
    239          * Initializes action.
    240          */
    241         public final void initialize() {
    242             layers = new ArrayList<>(getLayerManager().getLayers());
    243             exporters = new HashMap<>();
    244             dependencies = new MultiMap<>();
    245 
    246             Set<Layer> noExporter = new HashSet<>();
    247 
    248             for (Layer layer : layers) {
    249                 SessionLayerExporter exporter = null;
    250                 try {
    251                     exporter = SessionWriter.getSessionLayerExporter(layer);
    252                 } catch (IllegalArgumentException | JosmRuntimeException e) {
    253                     Logging.error(e);
    254                 }
    255                 if (exporter != null) {
    256                     exporters.put(layer, exporter);
    257                     Collection<Layer> deps = exporter.getDependencies();
    258                     if (deps != null) {
    259                         dependencies.putAll(layer, deps);
    260                     } else {
    261                         dependencies.putVoid(layer);
    262                     }
    263                 } else {
    264                     noExporter.add(layer);
    265                     exporters.put(layer, null);
    266                 }
    267             }
    268 
    269             int numNoExporter = 0;
    270             WHILE: while (numNoExporter != noExporter.size()) {
    271                 numNoExporter = noExporter.size();
    272                 for (Layer layer : layers) {
    273                     if (noExporter.contains(layer)) continue;
    274                     for (Layer depLayer : dependencies.get(layer)) {
    275                         if (noExporter.contains(depLayer)) {
    276                             noExporter.add(layer);
    277                             exporters.put(layer, null);
    278                             break WHILE;
    279                         }
    280                     }
    281                 }
    282             }
    283         }
    284 
    285         protected final Component build() {
    286             JPanel op = new JPanel(new GridBagLayout());
    287             JPanel ip = new JPanel(new GridBagLayout());
    288             for (Layer layer : layers) {
    289                 JPanel wrapper = new JPanel(new GridBagLayout());
    290                 wrapper.setBorder(BorderFactory.createEtchedBorder(EtchedBorder.RAISED));
    291                 Component exportPanel;
    292                 SessionLayerExporter exporter = exporters.get(layer);
    293                 if (exporter == null) {
    294                     if (!exporters.containsKey(layer)) throw new AssertionError();
    295                     exportPanel = getDisabledExportPanel(layer);
    296                 } else {
    297                     exportPanel = exporter.getExportPanel();
    298                 }
    299                 wrapper.add(exportPanel, GBC.std().fill(GBC.HORIZONTAL));
    300                 ip.add(wrapper, GBC.eol().fill(GBC.HORIZONTAL).insets(2, 2, 4, 2));
    301             }
    302             ip.add(GBC.glue(0, 1), GBC.eol().fill(GBC.VERTICAL));
    303             JScrollPane sp = new JScrollPane(ip);
    304             sp.setBorder(BorderFactory.createEmptyBorder());
    305             JPanel p = new JPanel(new GridBagLayout());
    306             p.add(sp, GBC.eol().fill());
    307             final JTabbedPane tabs = new JTabbedPane();
    308             tabs.addTab(tr("Layers"), p);
    309             op.add(tabs, GBC.eol().fill());
    310             JCheckBox chkSaveLocal = new JCheckBox(tr("Save all local files to disk"), SAVE_LOCAL_FILES_PROPERTY.get());
    311             chkSaveLocal.addChangeListener(l -> {
    312                 SAVE_LOCAL_FILES_PROPERTY.put(chkSaveLocal.isSelected());
    313             });
    314             op.add(chkSaveLocal);
    315             return op;
    316         }
    317 
    318         protected final Component getDisabledExportPanel(Layer layer) {
    319             JPanel p = new JPanel(new GridBagLayout());
    320             JCheckBox include = new JCheckBox();
    321             include.setEnabled(false);
    322             JLabel lbl = new JLabel(layer.getName(), layer.getIcon(), SwingConstants.LEADING);
    323             lbl.setToolTipText(tr("No exporter for this layer"));
    324             lbl.setLabelFor(include);
    325             lbl.setEnabled(false);
    326             p.add(include, GBC.std());
    327             p.add(lbl, GBC.std());
    328             p.add(GBC.glue(1, 0), GBC.std().fill(GBC.HORIZONTAL));
    329             return p;
    330         }
    331     }
    332 
    333     @Override
    334     protected void updateEnabledState() {
    335         setEnabled(MainApplication.isDisplayingMapView());
    336     }
    337 
    338     @Override
    339     public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
    340         updateEnabledState();
    341     }
    34254}
  • src/org/openstreetmap/josm/gui/MainMenu.java

     
    9696import org.openstreetmap.josm.actions.SearchNotesDownloadAction;
    9797import org.openstreetmap.josm.actions.SelectAllAction;
    9898import org.openstreetmap.josm.actions.SelectNonBranchingWaySequencesAction;
     99import org.openstreetmap.josm.actions.SessionSaveAction;
    99100import org.openstreetmap.josm.actions.SessionSaveAsAction;
    100101import org.openstreetmap.josm.actions.ShowStatusReportAction;
    101102import org.openstreetmap.josm.actions.SimplifyWayAction;
     
    176177    public final SaveAction save = SaveAction.getInstance();
    177178    /** File / Save As... **/
    178179    public final SaveAsAction saveAs = SaveAsAction.getInstance();
     180    /** File / Session &gt; Save Session **/
     181    public SessionSaveAction sessionSave = SessionSaveAction.getInstance();
    179182    /** File / Session &gt; Save Session As... **/
    180     public SessionSaveAsAction sessionSaveAs;
     183    public SessionSaveAsAction sessionSaveAs = new SessionSaveAsAction();
    181184    /** File / Export to GPX... **/
    182185    public final GpxExportAction gpxExport = new GpxExportAction();
    183186    /** File / Download from OSM... **/
     
    738741        fileMenu.addSeparator();
    739742        add(fileMenu, save);
    740743        add(fileMenu, saveAs);
    741         sessionSaveAs = new SessionSaveAsAction();
    742         ExpertToggleAction.addVisibilitySwitcher(fileMenu.add(sessionSaveAs));
     744        add(fileMenu, sessionSave, true);
     745        add(fileMenu, sessionSaveAs, true);
    743746        add(fileMenu, gpxExport, true);
    744747        fileMenu.addSeparator();
    745748        add(fileMenu, download);
  • src/org/openstreetmap/josm/gui/io/SaveLayersDialog.java

     
    4040import javax.swing.event.TableModelEvent;
    4141import javax.swing.event.TableModelListener;
    4242
    43 import org.openstreetmap.josm.actions.SessionSaveAsAction;
     43import org.openstreetmap.josm.actions.JosmAction;
     44import org.openstreetmap.josm.actions.SessionSaveAction;
    4445import org.openstreetmap.josm.actions.UploadAction;
    4546import org.openstreetmap.josm.gui.ExceptionDialogUtil;
    4647import org.openstreetmap.josm.gui.MainApplication;
     
    9293    private final UploadAndSaveProgressRenderer pnlUploadLayers = new UploadAndSaveProgressRenderer();
    9394
    9495    private final SaveAndProceedAction saveAndProceedAction = new SaveAndProceedAction();
    95     private final SaveSessionAction saveSessionAction = new SaveSessionAction();
     96    private final SaveSessionButtonAction saveSessionAction = new SaveSessionButtonAction();
    9697    private final DiscardAndProceedAction discardAndProceedAction = new DiscardAndProceedAction();
    9798    private final CancelAction cancelAction = new CancelAction();
    9899    private transient SaveAndUploadTask saveAndUploadTask;
     
    432433        }
    433434    }
    434435
    435     class SaveSessionAction extends SessionSaveAsAction {
     436    class SaveSessionButtonAction extends JosmAction {
    436437
    437         SaveSessionAction() {
    438             super(false, false);
     438        SaveSessionButtonAction() {
     439            super(tr("Save Session"), "session", SessionSaveAction.getTooltip(), null, false, null, false);
    439440        }
    440441
    441442        @Override
    442443        public void actionPerformed(ActionEvent e) {
    443444            try {
    444                 saveSession();
    445                 setUserAction(UserAction.PROCEED);
    446                 closeDialog();
     445                if (SessionSaveAction.getInstance().saveSession(false, true)) {
     446                    setUserAction(UserAction.PROCEED);
     447                    closeDialog();
     448                }
    447449            } catch (UserCancelException ignore) {
    448450                Logging.trace(ignore);
    449451            }
  • src/org/openstreetmap/josm/gui/layer/LayerManager.java

     
    131131        LayerRemoveEvent(LayerManager source, Layer removedLayer) {
    132132            super(source);
    133133            this.removedLayer = removedLayer;
    134             this.lastLayer = source.getLayers().size() == 1;
     134            this.lastLayer = source.getLayers().isEmpty();
    135135        }
    136136
    137137        /**
  • src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java

     
    758758     * @return GPX data
    759759     */
    760760    public static GpxData toGpxData(DataSet data, File file) {
    761         GpxData gpxData = new GpxData();
     761        GpxData gpxData = new GpxData(true);
    762762        fillGpxData(gpxData, data, file, GpxConstants.GPX_PREFIX);
     763        gpxData.endUpdate();
    763764        return gpxData;
    764765    }
    765766
     
    10101011
    10111012        @Override
    10121013        public void actionPerformed(ActionEvent e) {
     1014            String name = getName().replaceAll("^" + tr("Converted from: {0}", ""), "");
    10131015            final GpxData gpxData = toGpxData();
    1014             final GpxLayer gpxLayer = new GpxLayer(gpxData, tr("Converted from: {0}", getName()));
     1016            final GpxLayer gpxLayer = new GpxLayer(gpxData, tr("Converted from: {0}", name), true);
    10151017            if (getAssociatedFile() != null) {
    10161018                String filename = getAssociatedFile().getName().replaceAll(Pattern.quote(".gpx.osm") + '$', "") + ".gpx";
    10171019                gpxLayer.setAssociatedFile(new File(getAssociatedFile().getParentFile(), filename));
     1020                gpxLayer.getGpxData().setModified(true);
    10181021            }
    10191022            MainApplication.getLayerManager().addLayer(gpxLayer, false);
    10201023            if (Config.getPref().getBoolean("marker.makeautomarkers", true) && !gpxData.waypoints.isEmpty()) {
  • src/org/openstreetmap/josm/gui/layer/gpx/ConvertToDataLayerAction.java

     
    7171            if (err > 0) {
    7272                SimplifyWayAction.simplifyWays(ways, err);
    7373            }
    74             final OsmDataLayer osmLayer = new OsmDataLayer(ds, tr("Converted from: {0}", layer.getName()), null);
     74            String name = layer.getName().replaceAll("^" + tr("Converted from: {0}", ""), "");
     75            final OsmDataLayer osmLayer = new OsmDataLayer(ds, tr("Converted from: {0}", name), null);
    7576            if (layer.getAssociatedFile() != null) {
    7677                osmLayer.setAssociatedFile(new File(layer.getAssociatedFile().getParentFile(),
    7778                        layer.getAssociatedFile().getName() + ".osm"));
  • src/org/openstreetmap/josm/io/session/GenericSessionExporter.java

     
    191191            String zipPath = "layers/" + String.format("%02d", support.getLayerIndex()) + "/data." + extension;
    192192            file.appendChild(support.createTextNode(zipPath));
    193193            addDataFile(support.getOutputStreamZip(zipPath));
     194            layer.setAssociatedFile(null);
     195            if (layer instanceof AbstractModifiableLayer) {
     196                ((AbstractModifiableLayer) layer).onPostSaveToFile();
     197            }
    194198        } else {
    195199            try {
    196200                File f = layer.getAssociatedFile();
  • src/org/openstreetmap/josm/tools/ListenerList.java

     
    143143     * @return <code>true</code> if any are registered.
    144144     */
    145145    public boolean hasListeners() {
    146         return !listeners.isEmpty();
     146        return !listeners.isEmpty() || weakListeners.stream().map(l -> l.listener.get()).anyMatch(Objects::nonNull);
    147147    }
    148148
    149149    /**
  • test/unit/org/openstreetmap/josm/actions/SessionSaveActionTest.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.actions;
     3
     4import static org.junit.Assert.assertFalse;
     5import static org.junit.jupiter.api.Assertions.assertEquals;
     6import static org.junit.jupiter.api.Assertions.assertTrue;
     7
     8import java.io.File;
     9import java.io.IOException;
     10import java.util.Arrays;
     11import java.util.Collections;
     12
     13import org.junit.jupiter.api.Test;
     14import org.junit.jupiter.api.extension.RegisterExtension;
     15import org.openstreetmap.josm.TestUtils;
     16import org.openstreetmap.josm.gui.MainApplication;
     17import org.openstreetmap.josm.gui.layer.GpxLayer;
     18import org.openstreetmap.josm.gui.layer.OsmDataLayer;
     19import org.openstreetmap.josm.io.session.SessionWriterTest;
     20import org.openstreetmap.josm.testutils.JOSMTestRules;
     21import org.openstreetmap.josm.testutils.mockers.JOptionPaneSimpleMocker;
     22
     23import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
     24
     25/**
     26 * Unit tests for class {@link SessionSaveAsAction}.
     27 */
     28class SessionSaveActionTest {
     29    /**
     30     * Setup test.
     31     */
     32    @RegisterExtension
     33    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
     34    public JOSMTestRules test = new JOSMTestRules().main().projection();
     35
     36    /**
     37     * Unit test of {@link SessionSaveAction}
     38     * @throws IOException Temp file could not be created
     39     */
     40    @Test
     41    void testSaveAction() throws IOException {
     42        TestUtils.assumeWorkingJMockit();
     43
     44        File jos = File.createTempFile("session", ".jos");
     45        File joz = new File(jos.getAbsolutePath().replaceFirst(".jos$", ".joz"));
     46        assertTrue(jos.exists());
     47        assertFalse(joz.exists());
     48
     49        String overrideStr = "javax.swing.JLabel[,0,0,0x0,invalid,alignmentX=0.0,alignmentY=0.0,border=,flags=8388608,maximumSize=,minimumSize=,"
     50                + "preferredSize=,defaultIcon=,disabledIcon=,horizontalAlignment=LEADING,horizontalTextPosition=TRAILING,iconTextGap=4,"
     51                + "labelFor=,text=<html>The following layer has been removed since the session was last saved:<ul><li>OSM layer name</ul>"
     52                + "<br>You are about to overwrite the session file \"" + joz.getName()
     53                + "\". Would you like to proceed?,verticalAlignment=CENTER,verticalTextPosition=CENTER]";
     54
     55        SessionSaveAction saveAction = SessionSaveAction.getInstance();
     56        saveAction.setEnabled(true);
     57
     58        OsmDataLayer osm = SessionWriterTest.createOsmLayer();
     59        GpxLayer gpx = SessionWriterTest.createGpxLayer();
     60
     61        JOptionPaneSimpleMocker mocker = new JOptionPaneSimpleMocker(Collections.singletonMap(overrideStr, 0));
     62        SessionSaveAction.setCurrentSession(jos, false, Arrays.asList(gpx, osm)); //gpx and OSM layer
     63        MainApplication.getLayerManager().addLayer(gpx); //only gpx layer
     64        saveAction.actionPerformed(null); //Complain that OSM layer was removed
     65        assertEquals(1, mocker.getInvocationLog().size());
     66        assertFalse(jos.exists());
     67        assertTrue(joz.exists()); //converted jos to joz since the session includes files
     68
     69        mocker = new JOptionPaneSimpleMocker(Collections.singletonMap(overrideStr, 0));
     70        joz.delete();
     71        saveAction.actionPerformed(null); //Do not complain about removed layers
     72        assertEquals(0, mocker.getInvocationLog().size());
     73        assertTrue(joz.exists());
     74
     75        joz.delete();
     76    }
     77}
  • test/unit/org/openstreetmap/josm/actions/SessionSaveAsActionTest.java

    Property changes on: test\unit\org\openstreetmap\josm\actions\SessionSaveActionTest.java
    ___________________________________________________________________
    Added: svn:mime-type
    ## -0,0 +1 ##
    +text/plain
     
    33
    44import static org.junit.jupiter.api.Assertions.assertFalse;
    55
    6 import org.junit.jupiter.api.extension.RegisterExtension;
    76import org.junit.jupiter.api.Test;
     7import org.junit.jupiter.api.extension.RegisterExtension;
    88import org.openstreetmap.josm.testutils.JOSMTestRules;
    99
    1010import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
     
    1919     */
    2020    @RegisterExtension
    2121    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
    22     public JOSMTestRules test = new JOSMTestRules();
     22    public JOSMTestRules test = new JOSMTestRules().main();
    2323
    2424    /**
    2525     * Unit test of {@link SessionSaveAsAction#actionPerformed}