source: josm/trunk/src/org/openstreetmap/josm/actions/OpenFileAction.java

Last change on this file was 19123, checked in by GerdP, 23 months ago

fix #23728: First geotagged image not fully selected

  • remove code in GeoImageLayer constructor which more or less randomly opens the ImageViewerDialog
  • fix layer actions "Jump to next marker" and "Jump to previous marker" so that they open or update the image viewer dialog
  • new code to check if a new geoimage layer was added by any open file or drag/drop action and - if so - to open the first image of the topmost new geoimage layer. If ImageViewerDialog is already open a new tab is added.
  • Property svn:eol-style set to native
File size: 19.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions;
3
4import static javax.swing.JFileChooser.FILES_AND_DIRECTORIES;
5import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
6import static org.openstreetmap.josm.tools.I18n.tr;
7import static org.openstreetmap.josm.tools.I18n.trn;
8
9import java.awt.GraphicsEnvironment;
10import java.awt.event.ActionEvent;
11import java.awt.event.KeyEvent;
12import java.io.BufferedReader;
13import java.io.File;
14import java.io.IOException;
15import java.nio.charset.StandardCharsets;
16import java.nio.file.Files;
17import java.util.ArrayList;
18import java.util.Arrays;
19import java.util.Collection;
20import java.util.Collections;
21import java.util.EnumSet;
22import java.util.HashSet;
23import java.util.LinkedHashSet;
24import java.util.LinkedList;
25import java.util.List;
26import java.util.Objects;
27import java.util.Set;
28import java.util.concurrent.Future;
29import java.util.regex.Matcher;
30import java.util.regex.Pattern;
31import java.util.stream.Stream;
32
33import javax.swing.JOptionPane;
34import javax.swing.SwingUtilities;
35import javax.swing.filechooser.FileFilter;
36
37import org.openstreetmap.josm.data.PreferencesUtils;
38import org.openstreetmap.josm.gui.HelpAwareOptionPane;
39import org.openstreetmap.josm.gui.MainApplication;
40import org.openstreetmap.josm.gui.MapFrame;
41import org.openstreetmap.josm.gui.Notification;
42import org.openstreetmap.josm.gui.PleaseWaitRunnable;
43import org.openstreetmap.josm.gui.io.importexport.AllFormatsImporter;
44import org.openstreetmap.josm.gui.io.importexport.FileImporter;
45import org.openstreetmap.josm.gui.io.importexport.Options;
46import org.openstreetmap.josm.gui.layer.Layer;
47import org.openstreetmap.josm.gui.layer.geoimage.GeoImageLayer;
48import org.openstreetmap.josm.gui.util.GuiHelper;
49import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
50import org.openstreetmap.josm.gui.widgets.FileChooserManager;
51import org.openstreetmap.josm.gui.widgets.NativeFileChooser;
52import org.openstreetmap.josm.io.OsmTransferException;
53import org.openstreetmap.josm.spi.preferences.Config;
54import org.openstreetmap.josm.tools.Logging;
55import org.openstreetmap.josm.tools.MultiMap;
56import org.openstreetmap.josm.tools.PlatformManager;
57import org.openstreetmap.josm.tools.Shortcut;
58import org.openstreetmap.josm.tools.Utils;
59import org.xml.sax.SAXException;
60
61/**
62 * Open a file chooser dialog and select a file to import.
63 *
64 * @author imi
65 * @since 1146
66 */
67public class OpenFileAction extends DiskAccessAction {
68
69 /**
70 * The {@link ExtensionFileFilter} matching .url files
71 */
72 public static final ExtensionFileFilter URL_FILE_FILTER = new ExtensionFileFilter("url", "url", tr("URL Files") + " (*.url)");
73
74 /**
75 * Create an open action. The name is "Open a file".
76 */
77 public OpenFileAction() {
78 super(tr("Open..."), "open", tr("Open a file."),
79 Shortcut.registerShortcut("system:open", tr("File: {0}", tr("Open...")), KeyEvent.VK_O, Shortcut.CTRL),
80 true, null, false);
81 setHelpId(ht("/Action/Open"));
82 }
83
84 @Override
85 public void actionPerformed(ActionEvent e) {
86 final AbstractFileChooser fc;
87 // If the user explicitly wants native file dialogs, let them use it.
88 // Rather unfortunately, this means that they will not be able to select files and directories.
89 if (Boolean.TRUE.equals(FileChooserManager.PROP_USE_NATIVE_FILE_DIALOG.get())
90 // This is almost redundant, as the JDK currently doesn't support this with (all?) native file choosers.
91 && !NativeFileChooser.supportsSelectionMode(FILES_AND_DIRECTORIES)) {
92 fc = createAndOpenFileChooser(true, true, null);
93 } else {
94 fc = createAndOpenFileChooser(true, true, null, null, FILES_AND_DIRECTORIES, true, null);
95 }
96 if (fc == null)
97 return;
98 File[] files = fc.getSelectedFiles();
99 OpenFileTask task = new OpenFileTask(Arrays.asList(files), fc.getFileFilter());
100 task.setOptions(Options.RECORD_HISTORY);
101 MainApplication.worker.submit(task);
102 }
103
104 @Override
105 protected void updateEnabledState() {
106 setEnabled(true);
107 }
108
109 /**
110 * Open a list of files. The complete list will be passed to batch importers.
111 * Filenames will not be saved in history.
112 * @param fileList A list of files
113 * @return the future task
114 * @since 11986 (return task)
115 */
116 public static Future<?> openFiles(List<File> fileList) {
117 return openFiles(fileList, (Options[]) null);
118 }
119
120 /**
121 * Open a list of files. The complete list will be passed to batch importers.
122 * @param fileList A list of files
123 * @param options The options to use
124 * @return the future task
125 * @since 17534 ({@link Options})
126 */
127 public static Future<?> openFiles(List<File> fileList, Options... options) {
128 OpenFileTask task = new OpenFileTask(fileList, null);
129 task.setOptions(options);
130 return MainApplication.worker.submit(task);
131 }
132
133 /**
134 * Task to open files.
135 */
136 public static class OpenFileTask extends PleaseWaitRunnable {
137 private final List<File> files;
138 private final List<File> successfullyOpenedFiles = new ArrayList<>();
139 private final Set<String> fileHistory = new LinkedHashSet<>();
140 private final Set<String> failedAll = new HashSet<>();
141 private final FileFilter fileFilter;
142 private boolean canceled;
143 private final EnumSet<Options> options = EnumSet.noneOf(Options.class);
144
145 /**
146 * Constructs a new {@code OpenFileTask}.
147 * @param files files to open
148 * @param fileFilter file filter
149 * @param title message for the user
150 */
151 public OpenFileTask(final List<File> files, final FileFilter fileFilter, final String title) {
152 super(title, false /* don't ignore exception */);
153 this.fileFilter = fileFilter;
154 this.files = new ArrayList<>(files.size());
155 for (final File file : files) {
156 if (file.exists()) {
157 this.files.add(PlatformManager.getPlatform().resolveFileLink(file));
158 } else if (file.getParentFile() != null) {
159 // try to guess an extension using the specified fileFilter
160 final File[] matchingFiles = file.getParentFile().listFiles((dir, name) ->
161 name.startsWith(file.getName()) && fileFilter != null && fileFilter.accept(new File(dir, name)));
162 if (matchingFiles != null && matchingFiles.length == 1) {
163 // use the unique match as filename
164 this.files.add(matchingFiles[0]);
165 } else {
166 // add original filename for error reporting later on
167 this.files.add(file);
168 }
169 } else {
170 String message = tr("Unable to locate file ''{0}''.", file.getPath());
171 Logging.warn(message);
172 new Notification(message).setIcon(JOptionPane.WARNING_MESSAGE).show();
173 }
174 }
175 }
176
177 /**
178 * Constructs a new {@code OpenFileTask}.
179 * @param files files to open
180 * @param fileFilter file filter
181 */
182 public OpenFileTask(List<File> files, FileFilter fileFilter) {
183 this(files, fileFilter, tr("Opening files"));
184 }
185
186 /**
187 * Set the options for the task.
188 * @param options The options to set
189 * @see Options
190 * @since 17556
191 */
192 public void setOptions(Options... options) {
193 this.options.clear();
194 if (options != null) {
195 Stream.of(options).filter(Objects::nonNull).forEach(this.options::add);
196 }
197 }
198
199 /**
200 * Determines if filename must be saved in history (for list of recently opened files).
201 * @return {@code true} if filename must be saved in history
202 */
203 public boolean isRecordHistory() {
204 return this.options.contains(Options.RECORD_HISTORY);
205 }
206
207 /**
208 * Get the options for this task
209 * @return A set of options
210 * @since 17534
211 */
212 public Set<Options> getOptions() {
213 return Collections.unmodifiableSet(this.options);
214 }
215
216 @Override
217 protected void cancel() {
218 this.canceled = true;
219 }
220
221 @Override
222 protected void finish() {
223 MapFrame map = MainApplication.getMap();
224 if (map != null) {
225 map.repaint();
226 }
227 }
228
229 protected void alertFilesNotMatchingWithImporter(Collection<File> files, FileImporter importer) {
230 final StringBuilder msg = new StringBuilder(128).append("<html>").append(
231 trn("Cannot open {0} file with the file importer ''{1}''.",
232 "Cannot open {0} files with the file importer ''{1}''.",
233 files.size(),
234 files.size(),
235 Utils.escapeReservedCharactersHTML(importer.filter.getDescription())
236 )
237 ).append("<br><ul>");
238 for (File f: files) {
239 msg.append("<li>").append(f.getAbsolutePath()).append("</li>");
240 }
241 msg.append("</ul></html>");
242
243 HelpAwareOptionPane.showMessageDialogInEDT(
244 MainApplication.getMainFrame(),
245 msg.toString(),
246 tr("Warning"),
247 JOptionPane.WARNING_MESSAGE,
248 ht("/Action/Open#ImporterCantImportFiles")
249 );
250 }
251
252 protected void alertFilesWithUnknownImporter(Collection<File> files) {
253 final StringBuilder msg = new StringBuilder(115 + 30 * files.size()).append("<html>").append(
254 trn("Cannot open {0} file because file does not exist or no suitable file importer is available.",
255 "Cannot open {0} files because files do not exist or no suitable file importer is available.",
256 files.size(),
257 files.size()
258 )
259 ).append("<br><ul>");
260 for (File f: files) {
261 msg.append("<li>").append(f.getAbsolutePath()).append(" (<i>")
262 .append(f.exists() ? tr("no importer") : tr("does not exist"))
263 .append("</i>)</li>");
264 }
265 msg.append("</ul></html>");
266
267 HelpAwareOptionPane.showMessageDialogInEDT(
268 MainApplication.getMainFrame(),
269 msg.toString(),
270 tr("Warning"),
271 JOptionPane.WARNING_MESSAGE,
272 ht("/Action/Open#MissingImporterForFiles")
273 );
274 }
275
276 @Override
277 protected void realRun() throws SAXException, IOException, OsmTransferException {
278 if (Utils.isEmpty(files)) return;
279 List<Layer> oldLayers = MainApplication.getLayerManager().getLayers();
280
281 /*
282 * Find the importer with the chosen file filter
283 */
284 FileImporter chosenImporter = null;
285 if (fileFilter != null) {
286 for (FileImporter importer : ExtensionFileFilter.getImporters()) {
287 if (fileFilter.equals(importer.filter)) {
288 chosenImporter = importer;
289 }
290 }
291 }
292 /*
293 * If the filter hasn't been changed in the dialog, chosenImporter is null now.
294 * When the filter has been set explicitly to AllFormatsImporter, treat this the same.
295 */
296 if (chosenImporter instanceof AllFormatsImporter) {
297 chosenImporter = null;
298 }
299 getProgressMonitor().setTicksCount(files.size());
300
301 if (chosenImporter != null) {
302 // The importer was explicitly chosen, so use it.
303 List<File> filesNotMatchingWithImporter = new LinkedList<>();
304 List<File> filesMatchingWithImporter = new LinkedList<>();
305 for (final File f : files) {
306 if (!chosenImporter.acceptFile(f)) {
307 if (f.isDirectory()) {
308 SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(MainApplication.getMainFrame(), tr(
309 "<html>Cannot open directory ''{0}''.<br>Please select a file.</html>",
310 f.getAbsolutePath()), tr("Open file"), JOptionPane.ERROR_MESSAGE));
311 // TODO when changing to Java 6: Don't cancel the task here but use different modality. (Currently 2 dialogs
312 // would block each other.)
313 return;
314 } else {
315 filesNotMatchingWithImporter.add(f);
316 }
317 } else {
318 filesMatchingWithImporter.add(f);
319 }
320 }
321
322 if (!filesNotMatchingWithImporter.isEmpty()) {
323 alertFilesNotMatchingWithImporter(filesNotMatchingWithImporter, chosenImporter);
324 }
325 if (!filesMatchingWithImporter.isEmpty()) {
326 importData(chosenImporter, filesMatchingWithImporter);
327 }
328 } else {
329 // find appropriate importer
330 MultiMap<FileImporter, File> importerMap = new MultiMap<>();
331 List<File> filesWithUnknownImporter = new LinkedList<>();
332 List<File> urlFiles = new LinkedList<>();
333 FILES: for (File f : files) {
334 for (FileImporter importer : ExtensionFileFilter.getImporters()) {
335 if (importer.acceptFile(f)) {
336 importerMap.put(importer, f);
337 continue FILES;
338 }
339 }
340 if (URL_FILE_FILTER.accept(f)) {
341 urlFiles.add(f);
342 } else {
343 filesWithUnknownImporter.add(f);
344 }
345 }
346 if (!filesWithUnknownImporter.isEmpty()) {
347 alertFilesWithUnknownImporter(filesWithUnknownImporter);
348 }
349 List<FileImporter> importers = new ArrayList<>(importerMap.keySet());
350 Collections.sort(importers);
351 Collections.reverse(importers);
352
353 for (FileImporter importer : importers) {
354 importData(importer, new ArrayList<>(importerMap.get(importer)));
355 }
356
357 Pattern urlPattern = Pattern.compile(".*(https?://.*)");
358 for (File urlFile: urlFiles) {
359 try (BufferedReader reader = Files.newBufferedReader(urlFile.toPath(), StandardCharsets.UTF_8)) {
360 String line;
361 while ((line = reader.readLine()) != null) {
362 Matcher m = urlPattern.matcher(line);
363 if (m.matches()) {
364 String url = m.group(1);
365 MainApplication.getMenu().openLocation.openUrl(false, url);
366 }
367 }
368 } catch (IOException | RuntimeException | LinkageError e) {
369 Logging.error(e);
370 GuiHelper.runInEDT(
371 () -> new Notification(Utils.getRootCause(e).getMessage()).setIcon(JOptionPane.ERROR_MESSAGE).show());
372 }
373 }
374 }
375
376 if (this.options.contains(Options.RECORD_HISTORY)) {
377 Collection<String> oldFileHistory = Config.getPref().getList("file-open.history");
378 fileHistory.addAll(oldFileHistory);
379 // remove the files which failed to load from the list
380 fileHistory.removeAll(failedAll);
381 int maxsize = Math.max(0, Config.getPref().getInt("file-open.history.max-size", 15));
382 PreferencesUtils.putListBounded(Config.getPref(), "file-open.history", maxsize, new ArrayList<>(fileHistory));
383 }
384 if (!canceled && !GraphicsEnvironment.isHeadless()) {
385 checkNewLayers(oldLayers);
386 }
387 }
388
389 private static void checkNewLayers(List<Layer> oldLayers) {
390 // We do have to wrap the EDT call in a worker call, since layers may be created in the EDT.
391 // And the layer(s) must be added to the layer list in order for the dialog to work properly.
392 MainApplication.worker.execute(() -> GuiHelper.runInEDT(() -> {
393 List<Layer> newLayers = MainApplication.getLayerManager().getLayers();
394 // see #23728: open first image of topmost new image layer
395 for (Layer l : newLayers) {
396 if (oldLayers.contains(l))
397 return;
398 if (l instanceof GeoImageLayer) {
399 GeoImageLayer imageLayer = (GeoImageLayer) l;
400 imageLayer.jumpToNextMarker();
401 return;
402 }
403 }
404 }));
405 }
406
407 /**
408 * Import data files with the given importer.
409 * @param importer file importer
410 * @param files data files to import
411 */
412 public void importData(FileImporter importer, List<File> files) {
413 importer.setOptions(this.options.toArray(new Options[0]));
414 if (importer.isBatchImporter()) {
415 if (canceled) return;
416 String msg = trn("Opening {0} file...", "Opening {0} files...", files.size(), files.size());
417 getProgressMonitor().setCustomText(msg);
418 getProgressMonitor().indeterminateSubTask(msg);
419 if (importer.importDataHandleExceptions(files, getProgressMonitor().createSubTaskMonitor(files.size(), false))) {
420 successfullyOpenedFiles.addAll(files);
421 }
422 } else {
423 for (File f : files) {
424 if (canceled) return;
425 getProgressMonitor().indeterminateSubTask(tr("Opening file ''{0}'' ...", f.getAbsolutePath()));
426 if (importer.importDataHandleExceptions(f, getProgressMonitor().createSubTaskMonitor(1, false))) {
427 successfullyOpenedFiles.add(f);
428 }
429 }
430 }
431 if (this.options.contains(Options.RECORD_HISTORY) && !importer.isBatchImporter()) {
432 for (File f : files) {
433 try {
434 if (successfullyOpenedFiles.contains(f)) {
435 fileHistory.add(f.getCanonicalPath());
436 } else {
437 failedAll.add(f.getCanonicalPath());
438 }
439 } catch (IOException e) {
440 Logging.warn(e);
441 }
442 }
443 }
444 }
445
446 /**
447 * Replies the list of files that have been successfully opened.
448 * @return The list of files that have been successfully opened.
449 */
450 public List<File> getSuccessfullyOpenedFiles() {
451 return successfullyOpenedFiles;
452 }
453 }
454}
Note: See TracBrowser for help on using the repository browser.