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