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