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