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