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    private File file;
031
032    /**
033     * Constructs a new {@code SaveActionBase}.
034     * @param name The action's text as displayed on the menu (if it is added to a menu)
035     * @param iconName The filename of the icon to use
036     * @param tooltip A longer description of the action that will be displayed in the tooltip
037     * @param shortcut A ready-created shortcut object or {@code null} if you don't want a shortcut
038     */
039    public SaveActionBase(String name, String iconName, String tooltip, Shortcut shortcut) {
040        super(name, iconName, tooltip, shortcut);
041    }
042
043    @Override
044    public void actionPerformed(ActionEvent e) {
045        if (!isEnabled())
046            return;
047        doSave();
048    }
049
050    /**
051     * Saves the active layer.
052     * @return {@code true} if the save operation succeeds
053     */
054    public boolean doSave() {
055        if (Main.isDisplayingMapView()) {
056            Layer layer = Main.map.mapView.getActiveLayer();
057            if (layer != null && layer.isSavable()) {
058                return doSave(layer);
059            }
060        }
061        return false;
062    }
063
064    /**
065     * Saves the given layer.
066     * @param layer layer to save
067     * @return {@code true} if the save operation succeeds
068     */
069    public boolean doSave(Layer layer) {
070        if (!layer.checkSaveConditions())
071            return false;
072        file = getFile(layer);
073        return doInternalSave(layer, file);
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.exporters) {
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                JOptionPane.showMessageDialog(Main.parent, tr("No Exporter found! Nothing saved."), tr("Warning"),
109                        JOptionPane.WARNING_MESSAGE);
110                return false;
111            } else if (canceled) {
112                return false;
113            }
114            if (!layer.isRenamed()) {
115                layer.setName(file.getName());
116            }
117            layer.setAssociatedFile(file);
118            if (layer instanceof OsmDataLayer) {
119                ((OsmDataLayer) layer).onPostSaveToFile();
120            }
121            Main.parent.repaint();
122        } catch (IOException e) {
123            Main.error(e);
124            return false;
125        }
126        addToFileOpenHistory(file);
127        return true;
128    }
129
130    protected abstract File getFile(Layer layer);
131
132    /**
133     * Refreshes the enabled state
134     *
135     */
136    @Override
137    protected void updateEnabledState() {
138        boolean check = Main.isDisplayingMapView()
139        && Main.map.mapView.getActiveLayer() != null;
140        if (!check) {
141            setEnabled(false);
142            return;
143        }
144        Layer layer = Main.map.mapView.getActiveLayer();
145        setEnabled(layer != null && layer.isSavable());
146    }
147
148    /**
149     * Creates a new "Save" dialog for a single {@link ExtensionFileFilter} and makes it visible.<br>
150     * When the user has chosen a file, checks the file extension, and confirms overwrite if needed.
151     *
152     * @param title The dialog title
153     * @param filter The dialog file filter
154     * @return The output {@code File}
155     * @see DiskAccessAction#createAndOpenFileChooser(boolean, boolean, String, FileFilter, int, String)
156     * @since 5456
157     */
158    public static File createAndOpenSaveFileChooser(String title, ExtensionFileFilter filter) {
159        AbstractFileChooser fc = createAndOpenFileChooser(false, false, title, filter, JFileChooser.FILES_ONLY, null);
160        return checkFileAndConfirmOverWrite(fc, filter.getDefaultExtension());
161    }
162
163    /**
164     * Creates a new "Save" dialog for a given file extension and makes it visible.<br>
165     * When the user has chosen a file, checks the file extension, and confirms overwrite if needed.
166     *
167     * @param title The dialog title
168     * @param extension The file extension
169     * @return The output {@code File}
170     * @see DiskAccessAction#createAndOpenFileChooser(boolean, boolean, String, String)
171     */
172    public static File createAndOpenSaveFileChooser(String title, String extension) {
173        AbstractFileChooser fc = createAndOpenFileChooser(false, false, title, extension);
174        return checkFileAndConfirmOverWrite(fc, extension);
175    }
176
177    /**
178     * Checks if selected filename has the given extension. If not, adds the extension and asks for overwrite if filename exists.
179     *
180     * @param fc FileChooser where file was already selected
181     * @param extension file extension
182     * @return the {@code File} or {@code null} if the user cancelled the dialog.
183     */
184    public static File checkFileAndConfirmOverWrite(AbstractFileChooser fc, String extension) {
185        if (fc == null)
186            return null;
187        File file = fc.getSelectedFile();
188
189        FileFilter ff = fc.getFileFilter();
190        if (!ff.accept(file)) {
191            // Extension of another filefilter given ?
192            for (FileFilter cff : fc.getChoosableFileFilters()) {
193                if (cff.accept(file)) {
194                    fc.setFileFilter(cff);
195                    return file;
196                }
197            }
198            // No filefilter accepts current filename, add default extension
199            String fn = file.getPath();
200            if (extension != null && ff.accept(new File(fn + '.' + extension))) {
201                fn += '.' + extension;
202            } else if (ff instanceof ExtensionFileFilter) {
203                fn += '.' + ((ExtensionFileFilter) ff).getDefaultExtension();
204            }
205            file = new File(fn);
206            if (!fc.getSelectedFile().exists() && !confirmOverwrite(file))
207                return null;
208        }
209        return file;
210    }
211
212    /**
213     * Asks user to confirm overwiting a file.
214     * @param file file to overwrite
215     * @return {@code true} if the file can be written
216     */
217    public static boolean confirmOverwrite(File file) {
218        if (file == null || file.exists()) {
219            ExtendedDialog dialog = new ExtendedDialog(
220                    Main.parent,
221                    tr("Overwrite"),
222                    new String[] {tr("Overwrite"), tr("Cancel")}
223            );
224            dialog.setContent(tr("File exists. Overwrite?"));
225            dialog.setButtonIcons(new String[] {"save_as", "cancel"});
226            dialog.showDialog();
227            return dialog.getValue() == 1;
228        }
229        return true;
230    }
231
232    static void addToFileOpenHistory(File file) {
233        final String filepath;
234        try {
235            filepath = file.getCanonicalPath();
236        } catch (IOException ign) {
237            Main.warn(ign);
238            return;
239        }
240
241        int maxsize = Math.max(0, Main.pref.getInteger("file-open.history.max-size", 15));
242        Collection<String> oldHistory = Main.pref.getCollection("file-open.history");
243        List<String> history = new LinkedList<>(oldHistory);
244        history.remove(filepath);
245        history.add(0, filepath);
246        Main.pref.putCollectionBounded("file-open.history", maxsize, history);
247    }
248}