JAL-3141 Found a good fix for updating the FileChooser list of files when 'Include...
[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
29 import java.awt.Component;
30 import java.awt.Dimension;
31 import java.awt.EventQueue;
32 import java.awt.HeadlessException;
33 import java.awt.event.ActionEvent;
34 import java.awt.event.ActionListener;
35 import java.awt.event.MouseAdapter;
36 import java.awt.event.MouseEvent;
37 import java.io.File;
38 import java.util.ArrayList;
39 import java.util.List;
40 import java.util.StringTokenizer;
41 import java.util.Vector;
42
43 import javax.swing.BoxLayout;
44 import javax.swing.DefaultListCellRenderer;
45 import javax.swing.JCheckBox;
46 import javax.swing.JFileChooser;
47 import javax.swing.JList;
48 import javax.swing.JPanel;
49 import javax.swing.JScrollPane;
50 import javax.swing.SpringLayout;
51 import javax.swing.filechooser.FileFilter;
52 import javax.swing.plaf.basic.BasicFileChooserUI;
53
54 /**
55  * Enhanced file chooser dialog box.
56  *
57  * NOTE: bug on Windows systems when filechooser opened on directory to view
58  * files with colons in title.
59  *
60  * @author AMW
61  *
62  */
63 public class JalviewFileChooser extends JFileChooser
64 {
65   /**
66    * backupfilesCheckBox = "Include backup files" checkbox includeBackupfiles =
67    * flag set by checkbox
68    */
69   private JCheckBox backupfilesCheckBox = null;
70
71   protected boolean includeBackupFiles = false;
72
73   /**
74    * Factory method to return a file chooser that offers readable alignment file
75    * formats
76    * 
77    * @param directory
78    * @param selected
79    * @return
80    */
81   public static JalviewFileChooser forRead(String directory,
82           String selected)
83   {
84     List<String> extensions = new ArrayList<>();
85     List<String> descs = new ArrayList<>();
86     for (FileFormatI format : FileFormats.getInstance().getFormats())
87     {
88       if (format.isReadable())
89       {
90         extensions.add(format.getExtensions());
91         descs.add(format.getName());
92       }
93     }
94     return new JalviewFileChooser(directory,
95             extensions.toArray(new String[extensions.size()]),
96             descs.toArray(new String[descs.size()]), selected, true);
97   }
98
99   /**
100    * Factory method to return a file chooser that offers writable alignment file
101    * formats
102    * 
103    * @param directory
104    * @param selected
105    * @return
106    */
107   public static JalviewFileChooser forWrite(String directory,
108           String selected)
109   {
110     // TODO in Java 8, forRead and forWrite can be a single method
111     // with a lambda expression parameter for isReadable/isWritable
112     List<String> extensions = new ArrayList<>();
113     List<String> descs = new ArrayList<>();
114     for (FileFormatI format : FileFormats.getInstance().getFormats())
115     {
116       if (format.isWritable())
117       {
118         extensions.add(format.getExtensions());
119         descs.add(format.getName());
120       }
121     }
122     return new JalviewFileChooser(directory,
123             extensions.toArray(new String[extensions.size()]),
124             descs.toArray(new String[descs.size()]), selected, false);
125   }
126
127   public JalviewFileChooser(String dir)
128   {
129     super(safePath(dir));
130     setAccessory(new RecentlyOpened());
131   }
132
133   public JalviewFileChooser(String dir, String[] suffix, String[] desc,
134           String selected)
135   {
136     this(dir, suffix, desc, selected, true);
137   }
138
139   /**
140    * Constructor for a single choice of file extension and description
141    * 
142    * @param extension
143    * @param desc
144    */
145   public JalviewFileChooser(String extension, String desc)
146   {
147     this(Cache.getProperty("LAST_DIRECTORY"), new String[] { extension },
148             new String[]
149             { desc }, desc, true);
150   }
151
152   JalviewFileChooser(String dir, String[] extensions, String[] descs,
153           String selected, boolean allFiles)
154   {
155     this(dir, extensions, descs, selected, allFiles, false);
156   }
157
158   public JalviewFileChooser(String dir, String[] extensions, String[] descs,
159           String selected, boolean allFiles, boolean allowBackupFiles)
160   {
161     super(safePath(dir));
162     if (extensions.length == descs.length)
163     {
164       List<String[]> formats = new ArrayList<>();
165       for (int i = 0; i < extensions.length; i++)
166       {
167         formats.add(new String[] { extensions[i], descs[i] });
168       }
169       init(formats, selected, allFiles, allowBackupFiles);
170     }
171     else
172     {
173       System.err.println("JalviewFileChooser arguments mismatch: "
174               + extensions + ", " + descs);
175     }
176   }
177
178   private static File safePath(String dir)
179   {
180     if (dir == null)
181     {
182       return null;
183     }
184
185     File f = new File(dir);
186     if (f.getName().indexOf(':') > -1)
187     {
188       return null;
189     }
190     return f;
191   }
192
193   /**
194    * 
195    * @param formats
196    *          a list of {extensions, description} for each file format
197    * @param selected
198    * @param allFiles
199    *          if true, 'any format' option is included
200    */
201   void init(List<String[]> formats, String selected, boolean allFiles)
202   {
203     init(formats, selected, allFiles, false);
204   }
205
206   void init(List<String[]> formats, String selected, boolean allFiles,
207           boolean allowBackupFiles)
208   {
209
210     JalviewFileFilter chosen = null;
211
212     // SelectAllFilter needs to be set first before adding further
213     // file filters to fix bug on Mac OSX
214     setAcceptAllFileFilterUsed(allFiles);
215
216     for (String[] format : formats)
217     {
218       JalviewFileFilter jvf = new JalviewFileFilter(format[0], format[1]);
219       if (allowBackupFiles)
220       {
221         jvf.setParentJFC(this);
222       }
223       addChoosableFileFilter(jvf);
224       if ((selected != null) && selected.equalsIgnoreCase(format[1]))
225       {
226         chosen = jvf;
227       }
228     }
229
230     if (chosen != null)
231     {
232       setFileFilter(chosen);
233     }
234
235     if (allowBackupFiles)
236     {
237       JPanel multi = new JPanel();
238       multi.setLayout(new BoxLayout(multi, BoxLayout.PAGE_AXIS));
239       if (backupfilesCheckBox == null)
240       {
241         try {
242           includeBackupFiles = Boolean.parseBoolean(
243                   Cache.getProperty(BackupFiles.NS + "_FC_INCLUDE"));
244         } catch (Exception e)
245         {
246           includeBackupFiles = false;
247         }
248         backupfilesCheckBox = new JCheckBox(
249                 MessageManager.getString("label.include_backup_files"),
250                 includeBackupFiles);
251         backupfilesCheckBox.setAlignmentX(Component.CENTER_ALIGNMENT);
252         JalviewFileChooser jfc = this;
253         backupfilesCheckBox.addActionListener(new ActionListener()
254         {
255           @Override
256           public void actionPerformed(ActionEvent e)
257           {
258             includeBackupFiles = backupfilesCheckBox.isSelected();
259             Cache.setProperty(BackupFiles.NS + "_FC_INCLUDE",
260                     String.valueOf(includeBackupFiles));
261
262             FileFilter f = jfc.getFileFilter();
263             // deselect the selected file if it's no longer choosable
264             File selectedFile = jfc.getSelectedFile();
265             if (selectedFile != null && !f.accept(selectedFile))
266             {
267               jfc.setSelectedFile(null);
268             }
269             // fake the OK button changing (to force it to upate)
270             String s = jfc.getApproveButtonText();
271             jfc.firePropertyChange(
272                     APPROVE_BUTTON_TEXT_CHANGED_PROPERTY, null, s);
273             // fake the file filter changing (its behaviour actually has)
274             jfc.firePropertyChange(FILE_FILTER_CHANGED_PROPERTY, null, f);
275
276             jfc.rescanCurrentDirectory();
277             jfc.revalidate();
278             jfc.repaint();
279           }
280         });
281       }
282       multi.add(new RecentlyOpened());
283       multi.add(backupfilesCheckBox);
284       setAccessory(multi);
285     }
286     else
287     {
288       // set includeBackupFiles=false to avoid other file choosers from picking
289       // up backup files (Just In Case)
290       includeBackupFiles = false;
291       setAccessory(new RecentlyOpened());
292     }
293   }
294
295   @Override
296   public void setFileFilter(javax.swing.filechooser.FileFilter filter)
297   {
298     super.setFileFilter(filter);
299
300     try
301     {
302       if (getUI() instanceof BasicFileChooserUI)
303       {
304         final BasicFileChooserUI fcui = (BasicFileChooserUI) getUI();
305         final String name = fcui.getFileName().trim();
306
307         if ((name == null) || (name.length() == 0))
308         {
309           return;
310         }
311
312         EventQueue.invokeLater(new Thread()
313         {
314           @Override
315           public void run()
316           {
317             String currentName = fcui.getFileName();
318             if ((currentName == null) || (currentName.length() == 0))
319             {
320               fcui.setFileName(name);
321             }
322           }
323         });
324       }
325     } catch (Exception ex)
326     {
327       ex.printStackTrace();
328       // Some platforms do not have BasicFileChooserUI
329     }
330   }
331
332   /**
333    * Returns the selected file format, or null if none selected
334    * 
335    * @return
336    */
337   public FileFormatI getSelectedFormat()
338   {
339     if (getFileFilter() == null)
340     {
341       return null;
342     }
343
344     /*
345      * logic here depends on option description being formatted as 
346      * formatName (extension, extension...)
347      * or the 'no option selected' value
348      * All Files
349      * @see JalviewFileFilter.getDescription
350      */
351     String format = getFileFilter().getDescription();
352     int parenPos = format.indexOf("(");
353     if (parenPos > 0)
354     {
355       format = format.substring(0, parenPos).trim();
356       try
357       {
358         return FileFormats.getInstance().forName(format);
359       } catch (IllegalArgumentException e)
360       {
361         System.err.println("Unexpected format: " + format);
362       }
363     }
364     return null;
365   }
366
367   File ourselectedFile = null;
368
369   @Override
370   public File getSelectedFile()
371   {
372     File selfile = super.getSelectedFile();
373     if (selfile == null && ourselectedFile != null)
374     {
375       return ourselectedFile;
376     }
377     return selfile;
378   }
379
380   @Override
381   public int showSaveDialog(Component parent) throws HeadlessException
382   {
383     this.setAccessory(null);
384
385     setDialogType(SAVE_DIALOG);
386
387     this.setSelectedFile(null);
388     int ret = showDialog(parent, MessageManager.getString("action.save"));
389     ourselectedFile = getSelectedFile();
390
391     if (getSelectedFile() == null)
392     {
393       // Workaround for Java 9,10 on OSX - no selected file, but there is a
394       // filename typed in
395       try
396       {
397         String filename = ((BasicFileChooserUI) getUI()).getFileName();
398         if (filename != null && filename.length() > 0)
399         {
400           ourselectedFile = new File(getCurrentDirectory(), filename);
401         }
402       } catch (Throwable x)
403       {
404         System.err.println(
405                 "Unexpected exception when trying to get filename.");
406         x.printStackTrace();
407       }
408     }
409     if (ourselectedFile == null)
410     {
411       return JalviewFileChooser.CANCEL_OPTION;
412     }
413     if (getFileFilter() instanceof JalviewFileFilter)
414     {
415       JalviewFileFilter jvf = (JalviewFileFilter) getFileFilter();
416
417       if (!jvf.accept(ourselectedFile))
418       {
419         String withExtension = getSelectedFile().getName() + "."
420                 + jvf.getAcceptableExtension();
421         ourselectedFile = (new File(getCurrentDirectory(), withExtension));
422         setSelectedFile(ourselectedFile);
423       }
424     }
425
426     // TODO: ENSURE THAT FILES SAVED WITH A ':' IN THE NAME ARE REFUSED AND THE
427     // USER PROMPTED FOR A NEW FILENAME.
428     // DO NOT need to confirm file overwrite if using backup files (the files
429     // aren't being overwritten!)
430     if ((ret == JalviewFileChooser.APPROVE_OPTION)
431             && ourselectedFile.exists() && (!BackupFiles.getEnabled()))
432     {
433       int confirm = JvOptionPane.showConfirmDialog(parent,
434               MessageManager.getString("label.overwrite_existing_file"),
435               MessageManager.getString("label.file_already_exists"),
436               JvOptionPane.YES_NO_OPTION);
437
438       if (confirm != JvOptionPane.YES_OPTION)
439       {
440         ret = JalviewFileChooser.CANCEL_OPTION;
441       }
442
443     }
444
445     return ret;
446   }
447
448   void recentListSelectionChanged(Object selection)
449   {
450     setSelectedFile(null);
451     if (selection != null)
452     {
453       File file = new File((String) selection);
454       if (getFileFilter() instanceof JalviewFileFilter)
455       {
456         JalviewFileFilter jvf = (JalviewFileFilter) this.getFileFilter();
457
458         if (!jvf.accept(file))
459         {
460           setFileFilter(getChoosableFileFilters()[0]);
461         }
462       }
463
464       setSelectedFile(file);
465     }
466   }
467
468   class RecentlyOpened extends JPanel
469   {
470     JList list;
471
472     public RecentlyOpened()
473     {
474
475       String historyItems = jalview.bin.Cache.getProperty("RECENT_FILE");
476       StringTokenizer st;
477       Vector recent = new Vector();
478
479       if (historyItems != null)
480       {
481         st = new StringTokenizer(historyItems, "\t");
482
483         while (st.hasMoreTokens())
484         {
485           recent.addElement(st.nextElement());
486         }
487       }
488
489       list = new JList(recent);
490
491       DefaultListCellRenderer dlcr = new DefaultListCellRenderer();
492       dlcr.setHorizontalAlignment(DefaultListCellRenderer.RIGHT);
493       list.setCellRenderer(dlcr);
494
495       list.addMouseListener(new MouseAdapter()
496       {
497         @Override
498         public void mousePressed(MouseEvent evt)
499         {
500           recentListSelectionChanged(list.getSelectedValue());
501         }
502       });
503
504       this.setBorder(new javax.swing.border.TitledBorder(
505               MessageManager.getString("label.recently_opened")));
506
507       final JScrollPane scroller = new JScrollPane(list);
508
509       SpringLayout layout = new SpringLayout();
510       layout.putConstraint(SpringLayout.WEST, scroller, 5,
511               SpringLayout.WEST, this);
512       layout.putConstraint(SpringLayout.NORTH, scroller, 5,
513               SpringLayout.NORTH, this);
514
515       if (new Platform().isAMac())
516       {
517         scroller.setPreferredSize(new Dimension(500, 100));
518       }
519       else
520       {
521         scroller.setPreferredSize(new Dimension(130, 200));
522       }
523
524       this.add(scroller);
525
526       javax.swing.SwingUtilities.invokeLater(new Runnable()
527       {
528         @Override
529         public void run()
530         {
531           scroller.getHorizontalScrollBar()
532                   .setValue(scroller.getHorizontalScrollBar().getMaximum());
533         }
534       });
535
536     }
537
538   }
539 }