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