001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.event.ActionEvent;
007import java.io.File;
008import java.io.IOException;
009import java.util.Collection;
010import java.util.LinkedList;
011import java.util.List;
012
013import javax.swing.JFileChooser;
014import javax.swing.JOptionPane;
015import javax.swing.filechooser.FileFilter;
016
017import org.openstreetmap.josm.data.PreferencesUtils;
018import org.openstreetmap.josm.gui.ExtendedDialog;
019import org.openstreetmap.josm.gui.MainApplication;
020import org.openstreetmap.josm.gui.io.importexport.FileExporter;
021import org.openstreetmap.josm.gui.layer.Layer;
022import org.openstreetmap.josm.gui.layer.OsmDataLayer;
023import org.openstreetmap.josm.gui.util.GuiHelper;
024import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
025import org.openstreetmap.josm.spi.preferences.Config;
026import org.openstreetmap.josm.tools.Logging;
027import org.openstreetmap.josm.tools.Shortcut;
028
029/**
030 * Abstract superclass of save actions.
031 * @since 290
032 */
033public abstract class SaveActionBase extends DiskAccessAction {
034
035    /**
036     * Constructs a new {@code SaveActionBase}.
037     * @param name The action's text as displayed on the menu (if it is added to a menu)
038     * @param iconName The filename of the icon to use
039     * @param tooltip A longer description of the action that will be displayed in the tooltip
040     * @param shortcut A ready-created shortcut object or {@code null} if you don't want a shortcut
041     */
042    public SaveActionBase(String name, String iconName, String tooltip, Shortcut shortcut) {
043        super(name, iconName, tooltip, shortcut);
044    }
045
046    @Override
047    public void actionPerformed(ActionEvent e) {
048        if (!isEnabled())
049            return;
050        doSave();
051    }
052
053    /**
054     * Saves the active layer.
055     * @return {@code true} if the save operation succeeds
056     */
057    public boolean doSave() {
058        Layer layer = getLayerManager().getActiveLayer();
059        if (layer != null && layer.isSavable()) {
060            return doSave(layer);
061        }
062        return false;
063    }
064
065    /**
066     * Saves the given layer.
067     * @param layer layer to save
068     * @return {@code true} if the save operation succeeds
069     */
070    public boolean doSave(Layer layer) {
071        if (!layer.checkSaveConditions())
072            return false;
073        return doInternalSave(layer, getFile(layer));
074    }
075
076    /**
077     * Saves a layer to a given file.
078     * @param layer The layer to save
079     * @param file The destination file
080     * @param checkSaveConditions if {@code true}, checks preconditions before saving. Set it to {@code false} to skip it
081     * if preconditions have already been checked (as this check can prompt UI dialog in EDT it may be best in some cases
082     * to do it earlier).
083     * @return {@code true} if the layer has been successfully saved, {@code false} otherwise
084     * @since 7204
085     */
086    public static boolean doSave(Layer layer, File file, boolean checkSaveConditions) {
087        if (checkSaveConditions && !layer.checkSaveConditions())
088            return false;
089        return doInternalSave(layer, file);
090    }
091
092    private static boolean doInternalSave(Layer layer, File file) {
093        if (file == null)
094            return false;
095
096        try {
097            boolean exported = false;
098            boolean canceled = false;
099            for (FileExporter exporter : ExtensionFileFilter.getExporters()) {
100                if (exporter.acceptFile(file, layer)) {
101                    exporter.exportData(file, layer);
102                    exported = true;
103                    canceled = exporter.isCanceled();
104                    break;
105                }
106            }
107            if (!exported) {
108                GuiHelper.runInEDT(() ->
109                    JOptionPane.showMessageDialog(MainApplication.getMainFrame(), tr("No Exporter found! Nothing saved."), tr("Warning"),
110                        JOptionPane.WARNING_MESSAGE));
111                return false;
112            } else if (canceled) {
113                return false;
114            }
115            if (!layer.isRenamed()) {
116                layer.setName(file.getName());
117            }
118            layer.setAssociatedFile(file);
119            if (layer instanceof OsmDataLayer) {
120                ((OsmDataLayer) layer).onPostSaveToFile();
121            }
122            MainApplication.getMainFrame().repaint();
123        } catch (IOException e) {
124            Logging.error(e);
125            return false;
126        }
127        addToFileOpenHistory(file);
128        return true;
129    }
130
131    protected abstract File getFile(Layer layer);
132
133    /**
134     * Refreshes the enabled state
135     *
136     */
137    @Override
138    protected void updateEnabledState() {
139        Layer activeLayer = getLayerManager().getActiveLayer();
140        setEnabled(activeLayer != null && activeLayer.isSavable());
141    }
142
143    /**
144     * Creates a new "Save" dialog for a single {@link ExtensionFileFilter} and makes it visible.<br>
145     * When the user has chosen a file, checks the file extension, and confirms overwrite if needed.
146     *
147     * @param title The dialog title
148     * @param filter The dialog file filter
149     * @return The output {@code File}
150     * @see DiskAccessAction#createAndOpenFileChooser(boolean, boolean, String, FileFilter, int, String)
151     * @since 5456
152     */
153    public static File createAndOpenSaveFileChooser(String title, ExtensionFileFilter filter) {
154        AbstractFileChooser fc = createAndOpenFileChooser(false, false, title, filter, JFileChooser.FILES_ONLY, null);
155        return checkFileAndConfirmOverWrite(fc, filter.getDefaultExtension());
156    }
157
158    /**
159     * Creates a new "Save" dialog for a given file extension and makes it visible.<br>
160     * When the user has chosen a file, checks the file extension, and confirms overwrite if needed.
161     *
162     * @param title The dialog title
163     * @param extension The file extension
164     * @return The output {@code File}
165     * @see DiskAccessAction#createAndOpenFileChooser(boolean, boolean, String, String)
166     */
167    public static File createAndOpenSaveFileChooser(String title, String extension) {
168        AbstractFileChooser fc = createAndOpenFileChooser(false, false, title, extension);
169        return checkFileAndConfirmOverWrite(fc, extension);
170    }
171
172    /**
173     * Checks if selected filename has the given extension. If not, adds the extension and asks for overwrite if filename exists.
174     *
175     * @param fc FileChooser where file was already selected
176     * @param extension file extension
177     * @return the {@code File} or {@code null} if the user cancelled the dialog.
178     */
179    public static File checkFileAndConfirmOverWrite(AbstractFileChooser fc, String extension) {
180        if (fc == null)
181            return null;
182        File file = fc.getSelectedFile();
183
184        FileFilter ff = fc.getFileFilter();
185        if (!ff.accept(file)) {
186            // Extension of another filefilter given ?
187            for (FileFilter cff : fc.getChoosableFileFilters()) {
188                if (cff.accept(file)) {
189                    fc.setFileFilter(cff);
190                    return file;
191                }
192            }
193            // No filefilter accepts current filename, add default extension
194            String fn = file.getPath();
195            if (extension != null && ff.accept(new File(fn + '.' + extension))) {
196                fn += '.' + extension;
197            } else if (ff instanceof ExtensionFileFilter) {
198                fn += '.' + ((ExtensionFileFilter) ff).getDefaultExtension();
199            }
200            file = new File(fn);
201            if (!fc.getSelectedFile().exists() && !confirmOverwrite(file))
202                return null;
203        }
204        return file;
205    }
206
207    /**
208     * Asks user to confirm overwiting a file.
209     * @param file file to overwrite
210     * @return {@code true} if the file can be written
211     */
212    public static boolean confirmOverwrite(File file) {
213        if (file == null || file.exists()) {
214            return new ExtendedDialog(
215                    MainApplication.getMainFrame(),
216                    tr("Overwrite"),
217                    tr("Overwrite"), tr("Cancel"))
218                .setContent(tr("File exists. Overwrite?"))
219                .setButtonIcons("save_as", "cancel")
220                .showDialog()
221                .getValue() == 1;
222        }
223        return true;
224    }
225
226    static void addToFileOpenHistory(File file) {
227        final String filepath;
228        try {
229            filepath = file.getCanonicalPath();
230        } catch (IOException ign) {
231            Logging.warn(ign);
232            return;
233        }
234
235        int maxsize = Math.max(0, Config.getPref().getInt("file-open.history.max-size", 15));
236        Collection<String> oldHistory = Config.getPref().getList("file-open.history");
237        List<String> history = new LinkedList<>(oldHistory);
238        history.remove(filepath);
239        history.add(0, filepath);
240        PreferencesUtils.putListBounded(Config.getPref(), "file-open.history", maxsize, history);
241    }
242}