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