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