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