JAL-3416 Remove all JFrame/JInternalFrame icons (set to null)
[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 java.awt.Component;
25 import java.awt.Dimension;
26 import java.awt.EventQueue;
27 import java.awt.HeadlessException;
28 import java.awt.event.ActionEvent;
29 import java.awt.event.ActionListener;
30 import java.awt.event.MouseAdapter;
31 import java.awt.event.MouseEvent;
32 import java.beans.PropertyChangeEvent;
33 import java.beans.PropertyChangeListener;
34 import java.io.File;
35 import java.util.ArrayList;
36 import java.util.HashMap;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.StringTokenizer;
40 import java.util.Vector;
41
42 import javax.swing.BoxLayout;
43 import javax.swing.DefaultListCellRenderer;
44 import javax.swing.JCheckBox;
45 import javax.swing.JDialog;
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 import jalview.bin.Cache;
55 import jalview.gui.JvOptionPane;
56 import jalview.util.MessageManager;
57 import jalview.util.Platform;
58 import jalview.util.dialogrunner.DialogRunnerI;
59
60 /**
61  * Enhanced file chooser dialog box.
62  *
63  * NOTE: bug on Windows systems when filechooser opened on directory to view
64  * files with colons in title.
65  *
66  * @author AMW
67  *
68  */
69 public class JalviewFileChooser extends JFileChooser
70         implements DialogRunnerI, PropertyChangeListener
71 {
72   private static final long serialVersionUID = 1L;
73
74   private Map<Object, Runnable> callbacks = new HashMap<>();
75
76   File selectedFile = null;
77
78   /**
79    * backupfilesCheckBox = "Include backup files" checkbox includeBackupfiles =
80    * flag set by checkbox
81    */
82   private JCheckBox backupfilesCheckBox = null;
83
84   protected boolean includeBackupFiles = false;
85
86   /**
87    * Factory method to return a file chooser that offers readable alignment file
88    * formats
89    * 
90    * @param directory
91    * @param selected
92    * @return
93    */
94   public static JalviewFileChooser forRead(String directory,
95           String selected)
96   {
97     return JalviewFileChooser.forRead(directory, selected, false);
98   }
99
100   public static JalviewFileChooser forRead(String directory,
101           String selected, boolean allowBackupFiles)
102   {
103     List<String> extensions = new ArrayList<>();
104     List<String> descs = new ArrayList<>();
105     for (FileFormatI format : FileFormats.getInstance().getFormats())
106     {
107       if (format.isReadable())
108       {
109         extensions.add(format.getExtensions());
110         descs.add(format.getName());
111       }
112     }
113
114     return new JalviewFileChooser(directory,
115             extensions.toArray(new String[extensions.size()]),
116             descs.toArray(new String[descs.size()]), selected, true,
117             allowBackupFiles);
118   }
119
120   /**
121    * Factory method to return a file chooser that offers writable alignment file
122    * formats
123    * 
124    * @param directory
125    * @param selected
126    * @return
127    */
128   public static JalviewFileChooser forWrite(String directory,
129           String selected)
130   {
131     // TODO in Java 8, forRead and forWrite can be a single method
132     // with a lambda expression parameter for isReadable/isWritable
133     List<String> extensions = new ArrayList<>();
134     List<String> descs = new ArrayList<>();
135     for (FileFormatI format : FileFormats.getInstance().getFormats())
136     {
137       if (format.isWritable())
138       {
139         extensions.add(format.getExtensions());
140         descs.add(format.getName());
141       }
142     }
143     return new JalviewFileChooser(directory,
144             extensions.toArray(new String[extensions.size()]),
145             descs.toArray(new String[descs.size()]), selected, false);
146   }
147
148   public JalviewFileChooser(String dir)
149   {
150     super(safePath(dir));
151     setAccessory(new RecentlyOpened());
152   }
153
154   public JalviewFileChooser(String dir, String[] suffix, String[] desc,
155           String selected)
156   {
157     this(dir, suffix, desc, selected, true);
158   }
159
160   /**
161    * Constructor for a single choice of file extension and description
162    * 
163    * @param extension
164    * @param desc
165    */
166   public JalviewFileChooser(String extension, String desc)
167   {
168     this(Cache.getProperty("LAST_DIRECTORY"), new String[] { extension },
169             new String[]
170             { desc }, desc, true);
171   }
172
173   JalviewFileChooser(String dir, String[] extensions, String[] descs,
174           String selected, boolean acceptAny)
175   {
176     this(dir, extensions, descs, selected, acceptAny, false);
177   }
178
179   public JalviewFileChooser(String dir, String[] extensions, String[] descs,
180           String selected, boolean acceptAny, boolean allowBackupFiles)
181   {
182     super(safePath(dir));
183     if (extensions.length == descs.length)
184     {
185       List<String[]> formats = new ArrayList<>();
186       for (int i = 0; i < extensions.length; i++)
187       {
188         formats.add(new String[] { extensions[i], descs[i] });
189       }
190       init(formats, selected, acceptAny, allowBackupFiles);
191     }
192     else
193     {
194       System.err.println("JalviewFileChooser arguments mismatch: "
195               + extensions + ", " + descs);
196     }
197   }
198
199   private static File safePath(String dir)
200   {
201     if (dir == null)
202     {
203       return null;
204     }
205
206     File f = new File(dir);
207     if (f.getName().indexOf(':') > -1)
208     {
209       return null;
210     }
211     return f;
212   }
213
214   /**
215    * Overridden for JalviewJS compatibility: only one thread in Javascript, so
216    * we can't wait for user choice in another thread and then perform the
217    * desired action
218    */
219   @Override
220   public int showOpenDialog(Component parent)
221   {
222     int value = super.showOpenDialog(this);
223
224     if (!Platform.isJS())
225     /**
226      * Java only
227      * 
228      * @j2sIgnore
229      */
230     {
231       /*
232        * code here is not run in JalviewJS, instead
233        * propertyChange() is called for dialog action
234        */
235       handleResponse(value);
236     }
237     return value;
238   }
239
240   /**
241    * 
242    * @param formats
243    *          a list of {extensions, description} for each file format
244    * @param selected
245    * @param acceptAny
246    *          if true, 'any format' option is included
247    */
248   void init(List<String[]> formats, String selected, boolean acceptAny)
249   {
250     init(formats, selected, acceptAny, false);
251   }
252
253   void init(List<String[]> formats, String selected, boolean acceptAny,
254           boolean allowBackupFiles)
255   {
256
257     JalviewFileFilter chosen = null;
258
259     // SelectAllFilter needs to be set first before adding further
260     // file filters to fix bug on Mac OSX
261     setAcceptAllFileFilterUsed(acceptAny);
262
263     for (String[] format : formats)
264     {
265       JalviewFileFilter jvf = new JalviewFileFilter(format[0], format[1]);
266       if (allowBackupFiles)
267       {
268         jvf.setParentJFC(this);
269       }
270       addChoosableFileFilter(jvf);
271       if ((selected != null) && selected.equalsIgnoreCase(format[1]))
272       {
273         chosen = jvf;
274       }
275     }
276
277     if (chosen != null)
278     {
279       setFileFilter(chosen);
280     }
281
282     if (allowBackupFiles)
283     {
284       JPanel multi = new JPanel();
285       multi.setLayout(new BoxLayout(multi, BoxLayout.PAGE_AXIS));
286       if (backupfilesCheckBox == null)
287       {
288         try
289         {
290           includeBackupFiles = Boolean.parseBoolean(
291                   Cache.getProperty(BackupFiles.NS + "_FC_INCLUDE"));
292         } catch (Exception e)
293         {
294           includeBackupFiles = false;
295         }
296         backupfilesCheckBox = new JCheckBox(
297                 MessageManager.getString("label.include_backup_files"),
298                 includeBackupFiles);
299         backupfilesCheckBox.setAlignmentX(Component.CENTER_ALIGNMENT);
300         JalviewFileChooser jfc = this;
301         backupfilesCheckBox.addActionListener(new ActionListener()
302         {
303           @Override
304           public void actionPerformed(ActionEvent e)
305           {
306             includeBackupFiles = backupfilesCheckBox.isSelected();
307             Cache.setProperty(BackupFiles.NS + "_FC_INCLUDE",
308                     String.valueOf(includeBackupFiles));
309
310             FileFilter f = jfc.getFileFilter();
311             // deselect the selected file if it's no longer choosable
312             File selectedFile = jfc.getSelectedFile();
313             if (selectedFile != null && !f.accept(selectedFile))
314             {
315               jfc.setSelectedFile(null);
316             }
317             // fake the OK button changing (to force it to upate)
318             String s = jfc.getApproveButtonText();
319             jfc.firePropertyChange(APPROVE_BUTTON_TEXT_CHANGED_PROPERTY,
320                     null, s);
321             // fake the file filter changing (its behaviour actually has)
322             jfc.firePropertyChange(FILE_FILTER_CHANGED_PROPERTY, null, f);
323
324             jfc.rescanCurrentDirectory();
325             jfc.revalidate();
326             jfc.repaint();
327           }
328         });
329       }
330       multi.add(new RecentlyOpened());
331       multi.add(backupfilesCheckBox);
332       setAccessory(multi);
333     }
334     else
335     {
336       // set includeBackupFiles=false to avoid other file choosers from picking
337       // up backup files (Just In Case)
338       includeBackupFiles = false;
339       setAccessory(new RecentlyOpened());
340     }
341   }
342
343   @Override
344   public void setFileFilter(javax.swing.filechooser.FileFilter filter)
345   {
346     super.setFileFilter(filter);
347
348     try
349     {
350       if (getUI() instanceof BasicFileChooserUI)
351       {
352         final BasicFileChooserUI fcui = (BasicFileChooserUI) getUI();
353         final String name = fcui.getFileName().trim();
354
355         if ((name == null) || (name.length() == 0))
356         {
357           return;
358         }
359
360         EventQueue.invokeLater(new Thread()
361         {
362           @Override
363           public void run()
364           {
365             String currentName = fcui.getFileName();
366             if ((currentName == null) || (currentName.length() == 0))
367             {
368               fcui.setFileName(name);
369             }
370           }
371         });
372       }
373     } catch (Exception ex)
374     {
375       ex.printStackTrace();
376       // Some platforms do not have BasicFileChooserUI
377     }
378   }
379
380   /**
381    * Returns the selected file format, or null if none selected
382    * 
383    * @return
384    */
385   public FileFormatI getSelectedFormat()
386   {
387     if (getFileFilter() == null)
388     {
389       return null;
390     }
391
392     /*
393      * logic here depends on option description being formatted as 
394      * formatName (extension, extension...)
395      * or the 'no option selected' value
396      * All Files
397      * @see JalviewFileFilter.getDescription
398      */
399     String format = getFileFilter().getDescription();
400     int parenPos = format.indexOf("(");
401     if (parenPos > 0)
402     {
403       format = format.substring(0, parenPos).trim();
404       try
405       {
406         return FileFormats.getInstance().forName(format);
407       } catch (IllegalArgumentException e)
408       {
409         System.err.println("Unexpected format: " + format);
410       }
411     }
412     return null;
413   }
414
415   @Override
416   public File getSelectedFile()
417   {
418     File f = super.getSelectedFile();
419     return f == null ? selectedFile : f;
420   }
421
422   @Override
423   public int showSaveDialog(Component parent) throws HeadlessException
424   {
425     this.setAccessory(null);
426     // Java 9,10,11 on OSX - clear selected file so name isn't auto populated
427     this.setSelectedFile(null);
428
429     return super.showSaveDialog(parent);
430   }
431
432   /**
433    * If doing a Save, and an existing file is chosen or entered, prompt for
434    * confirmation of overwrite. Proceed if Yes, else leave the file chooser
435    * open.
436    * 
437    * @see https://stackoverflow.com/questions/8581215/jfilechooser-and-checking-for-overwrite
438    */
439   @Override
440   public void approveSelection()
441   {
442     if (getDialogType() != SAVE_DIALOG)
443     {
444       super.approveSelection();
445       return;
446     }
447
448     selectedFile = getSelectedFile();
449
450     if (selectedFile == null)
451     {
452       // Workaround for Java 9,10 on OSX - no selected file, but there is a
453       // filename typed in
454       try
455       {
456         String filename = ((BasicFileChooserUI) getUI()).getFileName();
457         if (filename != null && filename.length() > 0)
458         {
459           selectedFile = new File(getCurrentDirectory(), filename);
460         }
461       } catch (Throwable x)
462       {
463         System.err.println(
464                 "Unexpected exception when trying to get filename.");
465         x.printStackTrace();
466       }
467       // TODO: ENSURE THAT FILES SAVED WITH A ':' IN THE NAME ARE REFUSED AND
468       // THE
469       // USER PROMPTED FOR A NEW FILENAME
470     }
471
472     if (selectedFile == null)
473     {
474       return;
475     }
476
477     if (getFileFilter() instanceof JalviewFileFilter)
478     {
479       JalviewFileFilter jvf = (JalviewFileFilter) getFileFilter();
480
481       if (!jvf.accept(selectedFile))
482       {
483         String withExtension = getSelectedFile().getName() + "."
484                 + jvf.getAcceptableExtension();
485         selectedFile = (new File(getCurrentDirectory(), withExtension));
486         setSelectedFile(selectedFile);
487       }
488     }
489
490     if (selectedFile.exists())
491     {
492       int confirm = JvOptionPane.showConfirmDialog(this,
493               MessageManager.getString("label.overwrite_existing_file"),
494               MessageManager.getString("label.file_already_exists"),
495               JvOptionPane.YES_NO_OPTION);
496
497       if (confirm != JvOptionPane.YES_OPTION)
498       {
499         return;
500       }
501     }
502
503     super.approveSelection();
504   }
505
506   void recentListSelectionChanged(Object selection)
507   {
508     setSelectedFile(null);
509     if (selection != null)
510     {
511       File file = new File((String) selection);
512       if (getFileFilter() instanceof JalviewFileFilter)
513       {
514         JalviewFileFilter jvf = (JalviewFileFilter) this.getFileFilter();
515
516         if (!jvf.accept(file))
517         {
518           setFileFilter(getChoosableFileFilters()[0]);
519         }
520       }
521
522       setSelectedFile(file);
523     }
524   }
525
526   class RecentlyOpened extends JPanel
527   {
528     private static final long serialVersionUID = 1L;
529
530     JList<String> list;
531
532     RecentlyOpened()
533     {
534       setPreferredSize(new Dimension(300, 100));
535       String historyItems = Cache.getProperty("RECENT_FILE");
536       StringTokenizer st;
537       Vector<String> recent = new Vector<>();
538
539       if (historyItems != null)
540       {
541         st = new StringTokenizer(historyItems, "\t");
542
543         while (st.hasMoreTokens())
544         {
545           recent.addElement(st.nextToken());
546         }
547       }
548
549       list = new JList<>(recent);
550
551       DefaultListCellRenderer dlcr = new DefaultListCellRenderer();
552       dlcr.setHorizontalAlignment(DefaultListCellRenderer.RIGHT);
553       list.setCellRenderer(dlcr);
554
555       list.addMouseListener(new MouseAdapter()
556       {
557         @Override
558         public void mousePressed(MouseEvent evt)
559         {
560           recentListSelectionChanged(list.getSelectedValue());
561         }
562       });
563
564       this.setBorder(new javax.swing.border.TitledBorder(
565               MessageManager.getString("label.recently_opened")));
566
567       final JScrollPane scroller = new JScrollPane(list);
568
569       SpringLayout layout = new SpringLayout();
570       layout.putConstraint(SpringLayout.WEST, scroller, 5,
571               SpringLayout.WEST, this);
572       layout.putConstraint(SpringLayout.NORTH, scroller, 5,
573               SpringLayout.NORTH, this);
574
575       if (Platform.isAMacAndNotJS())
576       {
577         scroller.setPreferredSize(new Dimension(500, 100));
578       }
579       else
580       {
581         scroller.setPreferredSize(new Dimension(530, 200));
582       }
583
584       this.add(scroller);
585
586       javax.swing.SwingUtilities.invokeLater(new Runnable()
587       {
588         @Override
589         public void run()
590         {
591           scroller.getHorizontalScrollBar()
592                   .setValue(scroller.getHorizontalScrollBar().getMaximum());
593         }
594       });
595
596     }
597
598   }
599
600   @Override
601   public DialogRunnerI setResponseHandler(Object response, Runnable action)
602   {
603     callbacks.put(response, action);
604     return this;
605   }
606
607   @Override
608   public void handleResponse(Object response)
609   {
610     /*
611     * this test is for NaN in Chrome
612     */
613     if (response != null && !response.equals(response))
614     {
615       return;
616     }
617     Runnable action = callbacks.get(response);
618     if (action != null)
619     {
620       action.run();
621     }
622   }
623
624   /**
625    * JalviewJS signals file selection by a property change event for property
626    * "SelectedFile". This methods responds to that by running the response
627    * action for 'OK' in the dialog.
628    * 
629    * @param evt
630    */
631   @Override
632   public void propertyChange(PropertyChangeEvent evt)
633   {
634     // TODO other properties need runners...
635     switch (evt.getPropertyName())
636     {
637     /*
638      * property name here matches that used in JFileChooser.js
639      */
640     case "SelectedFile":
641       handleResponse(APPROVE_OPTION);
642       break;
643     }
644   }
645
646   @Override
647   protected JDialog createDialog(Component parent) throws HeadlessException
648   {
649     JDialog dialog = super.createDialog(parent);
650     dialog.setIconImage(null);
651     return dialog;
652   }
653
654 }