JAL-3141 Found a (not ideal) fix for updating the FileChooser list of files when...
[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             if (Platform.isAMac())
263             {
264               // This is a kludge. Cannot find out how to get the file list to
265               // refresh on its own! This works the best out of a number of
266               // different attempts!
267               FileFilter ff = jfc.getFileFilter();
268               jfc.setFileFilter(jfc.getAcceptAllFileFilter());
269               jfc.setFileFilter(ff);
270
271               /*
272               jfc.setFileHidingEnabled(!jfc.isFileHidingEnabled());
273               jfc.setFileHidingEnabled(!jfc.isFileHidingEnabled());
274               */
275
276               /*
277               jfc.getRootPane()
278                       .paintImmediately(jfc.getRootPane().getBounds());
279               jfc.paintImmediately(jfc.getBounds());
280               */
281
282               /*
283               Object o = jfc.getClientProperty(
284                       CHOOSABLE_FILE_FILTER_CHANGED_PROPERTY);
285               jfc.firePropertyChange(CHOOSABLE_FILE_FILTER_CHANGED_PROPERTY,
286                       o, o);
287               */
288
289               /*
290               File f = jfc.getSelectedFile();
291               jfc.setSelectedFile(null);
292               if (f != null)
293               {
294                 jfc.setSelectedFile(f);
295               }
296               */
297
298               /*
299               Graphics g = jfc.getGraphics();
300               jfc.update(g);
301               jfc.paintAll(g);
302               */
303
304               /*
305               jfc.setFileHidingEnabled(!jfc.isFileHidingEnabled());
306               jfc.setFileHidingEnabled(!jfc.isFileHidingEnabled());
307               */
308               /*
309               jfc.invalidate();
310               Component[] c = jfc.getComponents();
311               for (int i = 0; i < c.length; i++)
312               {
313                 c[i].invalidate();
314                 System.out.println("INVALIDATING " + c[i].getName());
315               }
316               */
317             }
318
319             jfc.rescanCurrentDirectory();
320             jfc.revalidate();
321             jfc.repaint();
322           }
323         });
324       }
325       multi.add(new RecentlyOpened());
326       multi.add(backupfilesCheckBox);
327       setAccessory(multi);
328     }
329     else
330     {
331       // set includeBackupFiles=false to avoid other file choosers from picking
332       // up backup files (Just In Case)
333       includeBackupFiles = false;
334       setAccessory(new RecentlyOpened());
335     }
336   }
337
338   @Override
339   public void setFileFilter(javax.swing.filechooser.FileFilter filter)
340   {
341     super.setFileFilter(filter);
342
343     try
344     {
345       if (getUI() instanceof BasicFileChooserUI)
346       {
347         final BasicFileChooserUI fcui = (BasicFileChooserUI) getUI();
348         final String name = fcui.getFileName().trim();
349
350         if ((name == null) || (name.length() == 0))
351         {
352           return;
353         }
354
355         EventQueue.invokeLater(new Thread()
356         {
357           @Override
358           public void run()
359           {
360             String currentName = fcui.getFileName();
361             if ((currentName == null) || (currentName.length() == 0))
362             {
363               fcui.setFileName(name);
364             }
365           }
366         });
367       }
368     } catch (Exception ex)
369     {
370       ex.printStackTrace();
371       // Some platforms do not have BasicFileChooserUI
372     }
373   }
374
375   /**
376    * Returns the selected file format, or null if none selected
377    * 
378    * @return
379    */
380   public FileFormatI getSelectedFormat()
381   {
382     if (getFileFilter() == null)
383     {
384       return null;
385     }
386
387     /*
388      * logic here depends on option description being formatted as 
389      * formatName (extension, extension...)
390      * or the 'no option selected' value
391      * All Files
392      * @see JalviewFileFilter.getDescription
393      */
394     String format = getFileFilter().getDescription();
395     int parenPos = format.indexOf("(");
396     if (parenPos > 0)
397     {
398       format = format.substring(0, parenPos).trim();
399       try
400       {
401         return FileFormats.getInstance().forName(format);
402       } catch (IllegalArgumentException e)
403       {
404         System.err.println("Unexpected format: " + format);
405       }
406     }
407     return null;
408   }
409
410   File ourselectedFile = null;
411
412   @Override
413   public File getSelectedFile()
414   {
415     File selfile = super.getSelectedFile();
416     if (selfile == null && ourselectedFile != null)
417     {
418       return ourselectedFile;
419     }
420     return selfile;
421   }
422
423   @Override
424   public int showSaveDialog(Component parent) throws HeadlessException
425   {
426     this.setAccessory(null);
427
428     setDialogType(SAVE_DIALOG);
429
430     this.setSelectedFile(null);
431     int ret = showDialog(parent, MessageManager.getString("action.save"));
432     ourselectedFile = getSelectedFile();
433
434     if (getSelectedFile() == null)
435     {
436       // Workaround for Java 9,10 on OSX - no selected file, but there is a
437       // filename typed in
438       try
439       {
440         String filename = ((BasicFileChooserUI) getUI()).getFileName();
441         if (filename != null && filename.length() > 0)
442         {
443           ourselectedFile = new File(getCurrentDirectory(), filename);
444         }
445       } catch (Throwable x)
446       {
447         System.err.println(
448                 "Unexpected exception when trying to get filename.");
449         x.printStackTrace();
450       }
451     }
452     if (ourselectedFile == null)
453     {
454       return JalviewFileChooser.CANCEL_OPTION;
455     }
456     if (getFileFilter() instanceof JalviewFileFilter)
457     {
458       JalviewFileFilter jvf = (JalviewFileFilter) getFileFilter();
459
460       if (!jvf.accept(ourselectedFile))
461       {
462         String withExtension = getSelectedFile().getName() + "."
463                 + jvf.getAcceptableExtension();
464         ourselectedFile = (new File(getCurrentDirectory(), withExtension));
465         setSelectedFile(ourselectedFile);
466       }
467     }
468
469     // TODO: ENSURE THAT FILES SAVED WITH A ':' IN THE NAME ARE REFUSED AND THE
470     // USER PROMPTED FOR A NEW FILENAME.
471     // DO NOT need to confirm file overwrite if using backup files (the files
472     // aren't being overwritten!)
473     if ((ret == JalviewFileChooser.APPROVE_OPTION)
474             && ourselectedFile.exists() && (!BackupFiles.getEnabled()))
475     {
476       int confirm = JvOptionPane.showConfirmDialog(parent,
477               MessageManager.getString("label.overwrite_existing_file"),
478               MessageManager.getString("label.file_already_exists"),
479               JvOptionPane.YES_NO_OPTION);
480
481       if (confirm != JvOptionPane.YES_OPTION)
482       {
483         ret = JalviewFileChooser.CANCEL_OPTION;
484       }
485
486     }
487
488     return ret;
489   }
490
491   void recentListSelectionChanged(Object selection)
492   {
493     setSelectedFile(null);
494     if (selection != null)
495     {
496       File file = new File((String) selection);
497       if (getFileFilter() instanceof JalviewFileFilter)
498       {
499         JalviewFileFilter jvf = (JalviewFileFilter) this.getFileFilter();
500
501         if (!jvf.accept(file))
502         {
503           setFileFilter(getChoosableFileFilters()[0]);
504         }
505       }
506
507       setSelectedFile(file);
508     }
509   }
510
511   class RecentlyOpened extends JPanel
512   {
513     JList list;
514
515     public RecentlyOpened()
516     {
517
518       String historyItems = jalview.bin.Cache.getProperty("RECENT_FILE");
519       StringTokenizer st;
520       Vector recent = new Vector();
521
522       if (historyItems != null)
523       {
524         st = new StringTokenizer(historyItems, "\t");
525
526         while (st.hasMoreTokens())
527         {
528           recent.addElement(st.nextElement());
529         }
530       }
531
532       list = new JList(recent);
533
534       DefaultListCellRenderer dlcr = new DefaultListCellRenderer();
535       dlcr.setHorizontalAlignment(DefaultListCellRenderer.RIGHT);
536       list.setCellRenderer(dlcr);
537
538       list.addMouseListener(new MouseAdapter()
539       {
540         @Override
541         public void mousePressed(MouseEvent evt)
542         {
543           recentListSelectionChanged(list.getSelectedValue());
544         }
545       });
546
547       this.setBorder(new javax.swing.border.TitledBorder(
548               MessageManager.getString("label.recently_opened")));
549
550       final JScrollPane scroller = new JScrollPane(list);
551
552       SpringLayout layout = new SpringLayout();
553       layout.putConstraint(SpringLayout.WEST, scroller, 5,
554               SpringLayout.WEST, this);
555       layout.putConstraint(SpringLayout.NORTH, scroller, 5,
556               SpringLayout.NORTH, this);
557
558       if (new Platform().isAMac())
559       {
560         scroller.setPreferredSize(new Dimension(500, 100));
561       }
562       else
563       {
564         scroller.setPreferredSize(new Dimension(130, 200));
565       }
566
567       this.add(scroller);
568
569       javax.swing.SwingUtilities.invokeLater(new Runnable()
570       {
571         @Override
572         public void run()
573         {
574           scroller.getHorizontalScrollBar()
575                   .setValue(scroller.getHorizontalScrollBar().getMaximum());
576         }
577       });
578
579     }
580
581   }
582 }