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