preliminary hack fix for synthetic reference issue
[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(this));
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(this));
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
427   @Override
428   public DialogRunnerI setResponseHandler(Object response, Runnable action)
429   {
430         callbacks.put(response,  action);
431         return this;
432   }
433
434   @Override
435   public void handleResponse(Object response)
436   {
437     /*
438          * this test is for NaN in Chrome
439          */
440     if (response != null && !response.equals(response))
441     {
442       return;
443     }
444     Runnable action = callbacks.get(response);
445     if (action != null)
446     {
447       action.run();
448     }
449   }
450
451   /**
452    * JalviewJS signals file selection by a property change event
453    * for property "SelectedFile".  This methods responds to that by
454    * running the response action for 'OK' in the dialog.
455    * 
456    * @param evt
457    */
458   @Override
459   public void propertyChange(PropertyChangeEvent evt)
460   {
461     // TODO other properties need runners...
462     switch (evt.getPropertyName())
463     {
464     /*
465      * property name here matches that used in JFileChooser.js
466      */
467     case "SelectedFile": 
468       handleResponse(APPROVE_OPTION);
469       break;
470     }
471   }
472 }
473
474 class RecentlyOpened extends JPanel
475 {
476   private static final long serialVersionUID = 1L;
477   JList<String> list;
478   private JalviewFileChooser chooser;
479
480   RecentlyOpened(JalviewFileChooser chooser) 
481   {
482         this.chooser = chooser;
483     setPreferredSize(new Dimension(300,100));
484     String historyItems = Cache.getProperty("RECENT_FILE");
485     StringTokenizer st;
486     Vector<String> recent = new Vector<>();
487
488     if (historyItems != null)
489     {
490       st = new StringTokenizer(historyItems, "\t");
491
492       while (st.hasMoreTokens())
493       {
494         recent.addElement(st.nextToken());
495       }
496     }
497
498     list = new JList<>(recent);
499
500     DefaultListCellRenderer dlcr = new DefaultListCellRenderer();
501 //    dlcr.setHorizontalAlignment(DefaultListCellRenderer.RIGHT);
502     list.setCellRenderer(dlcr);
503
504     list.addMouseListener(new MouseAdapter()
505     {
506       @Override
507       public void mousePressed(MouseEvent evt)
508       {
509         chooser.recentListSelectionChanged(list.getSelectedValue());
510       }
511     });
512
513     this.setBorder(new javax.swing.border.TitledBorder(
514             MessageManager.getString("label.recently_opened")));
515
516     final JScrollPane scroller = new JScrollPane(list);
517
518     SpringLayout layout = new SpringLayout();
519     layout.putConstraint(SpringLayout.WEST, scroller, 5,
520             SpringLayout.WEST, this);
521     layout.putConstraint(SpringLayout.NORTH, scroller, 5,
522             SpringLayout.NORTH, this);
523
524     if (Platform.isAMac())
525     {
526       scroller.setPreferredSize(new Dimension(500, 100));
527     }
528     else
529     {
530       scroller.setPreferredSize(new Dimension(530, 200));
531     }
532
533     this.add(scroller);
534
535     javax.swing.SwingUtilities.invokeLater(new Runnable()
536     {
537       @Override
538       public void run()
539       {
540         scroller.getHorizontalScrollBar()
541                 .setValue(scroller.getHorizontalScrollBar().getMaximum());
542       }
543     });
544
545   }
546
547 }