Merge branch 'feature/JAL-3169cancelOverwrite' into trialMerge
[jalview.git] / src / jalview / io / JalviewFileChooser.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  *
5  * This file is part of Jalview.
6  *
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *
12  * Jalview is distributed in the hope that it will be useful, but
13  * WITHOUT ANY WARRANTY; without even the implied warranty
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
15  * PURPOSE.  See the GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 //////////////////////////////////////////////////////////////////
22 package jalview.io;
23
24 import jalview.bin.Cache;
25 import jalview.gui.JvOptionPane;
26 import jalview.util.MessageManager;
27 import jalview.util.Platform;
28 import jalview.util.dialogrunner.DialogRunner;
29 import jalview.util.dialogrunner.DialogRunnerI;
30 import jalview.util.dialogrunner.Response;
31 import jalview.util.dialogrunner.RunResponse;
32
33 import java.awt.Component;
34 import java.awt.Dimension;
35 import java.awt.EventQueue;
36 import java.awt.HeadlessException;
37 import java.awt.event.MouseAdapter;
38 import java.awt.event.MouseEvent;
39 import java.beans.PropertyChangeEvent;
40 import java.beans.PropertyChangeListener;
41 import java.io.File;
42 import java.util.ArrayList;
43 import java.util.List;
44 import java.util.StringTokenizer;
45 import java.util.Vector;
46
47 import javax.swing.DefaultListCellRenderer;
48 import javax.swing.JFileChooser;
49 import javax.swing.JList;
50 import javax.swing.JPanel;
51 import javax.swing.JScrollPane;
52 import javax.swing.SpringLayout;
53 import javax.swing.plaf.basic.BasicFileChooserUI;
54
55 /**
56  * Enhanced file chooser dialog box.
57  *
58  * NOTE: bug on Windows systems when filechooser opened on directory to view
59  * files with colons in title.
60  *
61  * @author AMW
62  *
63  */
64 public class JalviewFileChooser extends JFileChooser
65         implements PropertyChangeListener, DialogRunnerI<JalviewFileChooser>
66 {
67   DialogRunner<JalviewFileChooser> runner = new DialogRunner<>(this);
68   
69   /**
70    * Factory method to return a file chooser that offers readable alignment file
71    * formats
72    * 
73    * @param directory
74    * @param selected
75    * @return
76    */
77   public static JalviewFileChooser forRead(String directory,
78           String selected)
79   {
80     List<String> extensions = new ArrayList<>();
81     List<String> descs = new ArrayList<>();
82     for (FileFormatI format : FileFormats.getInstance().getFormats())
83     {
84       if (format.isReadable())
85       {
86         extensions.add(format.getExtensions());
87         descs.add(format.getName());
88       }
89     }
90     return new JalviewFileChooser(directory,
91             extensions.toArray(new String[extensions.size()]),
92             descs.toArray(new String[descs.size()]), selected, true);
93   }
94
95   /**
96    * Factory method to return a file chooser that offers writable alignment file
97    * formats
98    * 
99    * @param directory
100    * @param selected
101    * @return
102    */
103   public static JalviewFileChooser forWrite(String directory,
104           String selected)
105   {
106     // TODO in Java 8, forRead and forWrite can be a single method
107     // with a lambda expression parameter for isReadable/isWritable
108     List<String> extensions = new ArrayList<>();
109     List<String> descs = new ArrayList<>();
110     for (FileFormatI format : FileFormats.getInstance().getFormats())
111     {
112       if (format.isWritable())
113       {
114         extensions.add(format.getExtensions());
115         descs.add(format.getName());
116       }
117     }
118     return new JalviewFileChooser(directory,
119             extensions.toArray(new String[extensions.size()]),
120             descs.toArray(new String[descs.size()]), selected, false);
121   }
122
123   public JalviewFileChooser(String dir)
124   {
125     super(safePath(dir));
126     setAccessory(new RecentlyOpened());
127   }
128
129   public JalviewFileChooser(String dir, String[] suffix, String[] desc,
130           String selected)
131   {
132     this(dir, suffix, desc, selected, true);
133   }
134
135   /**
136    * Constructor for a single choice of file extension and description
137    * 
138    * @param extension
139    * @param desc
140    */
141   public JalviewFileChooser(String extension, String desc)
142   {
143     this(Cache.getProperty("LAST_DIRECTORY"), new String[] { extension },
144             new String[]
145             { desc }, desc, true);
146   }
147
148   JalviewFileChooser(String dir, String[] extensions, String[] descs,
149           String selected, boolean allFiles)
150   {
151     super(safePath(dir));
152     if (extensions.length == descs.length)
153     {
154       List<String[]> formats = new ArrayList<>();
155       for (int i = 0; i < extensions.length; i++)
156       {
157         formats.add(new String[] { extensions[i], descs[i] });
158       }
159       init(formats, selected, allFiles);
160     }
161     else
162     {
163       System.err.println("JalviewFileChooser arguments mismatch: "
164               + extensions + ", " + descs);
165     }
166   }
167
168   @Override
169   public void propertyChange(PropertyChangeEvent evt)
170   {
171     // TODO other properties need runners...
172     switch (evt.getPropertyName())
173     {
174     case "SelectedFile": 
175       runner.run(APPROVE_OPTION);
176       break;
177     }
178   }
179
180   private static File safePath(String dir)
181   {
182     if (dir == null)
183     {
184       return null;
185     }
186
187     File f = new File(dir);
188     if (f.getName().indexOf(':') > -1)
189     {
190       return null;
191     }
192     return f;
193   }
194
195   /**
196    * Overridden for JalviewJS compatibility: only one thread in Javascript, 
197    * so we can't wait for user choice in another thread and then perform the 
198    * desired action
199    */
200   @Override
201   public int showOpenDialog(Component parent)
202   {
203     runner.resetResponses();
204     int value = super.showOpenDialog(this);
205     /**
206      * @j2sNative
207      */
208     {
209       runner.firstRun(value);
210     }
211     return value;
212   }
213
214   /**
215    * 
216    * @param formats
217    *          a list of {extensions, description} for each file format
218    * @param selected
219    * @param allFiles
220    *          if true, 'any format' option is included
221    */
222   void init(List<String[]> formats, String selected, boolean allFiles)
223   {
224
225     JalviewFileFilter chosen = null;
226
227     // SelectAllFilter needs to be set first before adding further
228     // file filters to fix bug on Mac OSX
229     setAcceptAllFileFilterUsed(allFiles);
230
231     for (String[] format : formats)
232     {
233       JalviewFileFilter jvf = new JalviewFileFilter(format[0], format[1]);
234       addChoosableFileFilter(jvf);
235       if ((selected != null) && selected.equalsIgnoreCase(format[1]))
236       {
237         chosen = jvf;
238       }
239     }
240
241     if (chosen != null)
242     {
243       setFileFilter(chosen);
244     }
245
246     setAccessory(new RecentlyOpened());
247   }
248
249   @Override
250   public void setFileFilter(javax.swing.filechooser.FileFilter filter)
251   {
252     super.setFileFilter(filter);
253
254     try
255     {
256       if (getUI() instanceof BasicFileChooserUI)
257       {
258         final BasicFileChooserUI fcui = (BasicFileChooserUI) getUI();
259         final String name = fcui.getFileName().trim();
260
261         if ((name == null) || (name.length() == 0))
262         {
263           return;
264         }
265
266         EventQueue.invokeLater(new Thread()
267         {
268           @Override
269           public void run()
270           {
271             String currentName = fcui.getFileName();
272             if ((currentName == null) || (currentName.length() == 0))
273             {
274               fcui.setFileName(name);
275             }
276           }
277         });
278       }
279     } catch (Exception ex)
280     {
281       ex.printStackTrace();
282       // Some platforms do not have BasicFileChooserUI
283     }
284   }
285
286   /**
287    * Returns the selected file format, or null if none selected
288    * 
289    * @return
290    */
291   public FileFormatI getSelectedFormat()
292   {
293     if (getFileFilter() == null)
294     {
295       return null;
296     }
297
298     /*
299      * logic here depends on option description being formatted as 
300      * formatName (extension, extension...)
301      * or the 'no option selected' value
302      * All Files
303      * @see JalviewFileFilter.getDescription
304      */
305     String format = getFileFilter().getDescription();
306     int parenPos = format.indexOf("(");
307     if (parenPos > 0)
308     {
309       format = format.substring(0, parenPos).trim();
310       try
311       {
312         return FileFormats.getInstance().forName(format);
313       } catch (IllegalArgumentException e)
314       {
315         System.err.println("Unexpected format: " + format);
316       }
317     }
318     return null;
319   }
320   
321   File ourselectedFile = null;
322
323   @Override
324   public File getSelectedFile()
325   {
326     File selfile = super.getSelectedFile();
327     if (selfile == null && ourselectedFile != null)
328     {
329       return ourselectedFile;
330     }
331     return selfile;
332   }
333
334   @Override
335   public int showSaveDialog(Component parent) throws HeadlessException
336   {
337     this.setAccessory(null);
338     // Java 9,10,11 on OSX - clear selected file so name isn't auto populated
339     this.setSelectedFile(null);
340
341     return super.showSaveDialog(parent);
342   }
343
344   /**
345    * If doing a Save, and an existing file is chosen or entered, prompt for
346    * confirmation of overwrite. Proceed if Yes, else leave the file chooser
347    * open.
348    * 
349    * @see https://stackoverflow.com/questions/8581215/jfilechooser-and-checking-for-overwrite
350    */
351   @Override
352   public void approveSelection()
353   {
354     if (getDialogType() != SAVE_DIALOG)
355     {
356       super.approveSelection();
357       return;
358     }
359
360     ourselectedFile = getSelectedFile();
361
362     if (ourselectedFile == null)
363     {
364       // Workaround for Java 9,10 on OSX - no selected file, but there is a
365       // filename typed in
366       try
367       {
368         String filename = ((BasicFileChooserUI) getUI()).getFileName();
369         if (filename != null && filename.length() > 0)
370         {
371           ourselectedFile = new File(getCurrentDirectory(), filename);
372         }
373       } catch (Throwable x)
374       {
375         System.err.println(
376                 "Unexpected exception when trying to get filename.");
377         x.printStackTrace();
378       }
379       // TODO: ENSURE THAT FILES SAVED WITH A ':' IN THE NAME ARE REFUSED AND
380       // THE
381       // USER PROMPTED FOR A NEW FILENAME
382     }
383     if (ourselectedFile == null)
384     {
385       return;
386     }
387
388     if (getFileFilter() instanceof JalviewFileFilter)
389     {
390       JalviewFileFilter jvf = (JalviewFileFilter) getFileFilter();
391
392       if (!jvf.accept(ourselectedFile))
393       {
394         String withExtension = getSelectedFile().getName() + "."
395                 + jvf.getAcceptableExtension();
396         ourselectedFile = (new File(getCurrentDirectory(), withExtension));
397         setSelectedFile(ourselectedFile);
398       }
399     }
400
401     if (ourselectedFile.exists())
402     {
403       int confirm = JvOptionPane.showConfirmDialog(this,
404               MessageManager.getString("label.overwrite_existing_file"),
405               MessageManager.getString("label.file_already_exists"),
406               JvOptionPane.YES_NO_OPTION);
407
408       if (confirm != JvOptionPane.YES_OPTION)
409       {
410         return;
411       }
412     }
413
414     super.approveSelection();
415   }
416
417   void recentListSelectionChanged(Object selection)
418   {
419     setSelectedFile(null);
420     if (selection != null)
421     {
422       File file = new File((String) selection);
423       if (getFileFilter() instanceof JalviewFileFilter)
424       {
425         JalviewFileFilter jvf = (JalviewFileFilter) this.getFileFilter();
426
427         if (!jvf.accept(file))
428         {
429           setFileFilter(getChoosableFileFilters()[0]);
430         }
431       }
432
433       setSelectedFile(file);
434     }
435   }
436
437   class RecentlyOpened extends JPanel
438   {
439     JList<String> list;
440
441     public RecentlyOpened()
442     {
443
444       setPreferredSize(new Dimension(300,100));
445       String historyItems = jalview.bin.Cache.getProperty("RECENT_FILE");
446       StringTokenizer st;
447       Vector<String> recent = new Vector<>();
448
449       if (historyItems != null)
450       {
451         st = new StringTokenizer(historyItems, "\t");
452
453         while (st.hasMoreTokens())
454         {
455           recent.addElement(st.nextToken());
456         }
457       }
458
459       list = new JList<>(recent);
460   
461       DefaultListCellRenderer dlcr = new DefaultListCellRenderer();
462 //      dlcr.setHorizontalAlignment(DefaultListCellRenderer.RIGHT);
463       list.setCellRenderer(dlcr);
464
465       list.addMouseListener(new MouseAdapter()
466       {
467         @Override
468         public void mousePressed(MouseEvent evt)
469         {
470           recentListSelectionChanged(list.getSelectedValue());
471         }
472       });
473
474       this.setBorder(new javax.swing.border.TitledBorder(
475               MessageManager.getString("label.recently_opened")));
476
477       final JScrollPane scroller = new JScrollPane(list);
478
479       SpringLayout layout = new SpringLayout();
480       layout.putConstraint(SpringLayout.WEST, scroller, 5,
481               SpringLayout.WEST, this);
482       layout.putConstraint(SpringLayout.NORTH, scroller, 5,
483               SpringLayout.NORTH, this);
484
485       if (Platform.isAMac())
486       {
487         scroller.setPreferredSize(new Dimension(500, 100));
488       }
489       else
490       {
491         scroller.setPreferredSize(new Dimension(530, 200));
492       }
493
494       this.add(scroller);
495
496       javax.swing.SwingUtilities.invokeLater(new Runnable()
497       {
498         @Override
499         public void run()
500         {
501           scroller.getHorizontalScrollBar()
502                   .setValue(scroller.getHorizontalScrollBar().getMaximum());
503         }
504       });
505
506     }
507
508   }
509
510   @Override
511   public JalviewFileChooser addResponse(RunResponse action)
512   {
513     return runner.addResponse(action);
514   }
515
516 }