JAL-2446 merged to spike branch
[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.MouseAdapter;
34 import java.awt.event.MouseEvent;
35 import java.io.File;
36 import java.util.ArrayList;
37 import java.util.List;
38 import java.util.StringTokenizer;
39 import java.util.Vector;
40
41 import javax.swing.DefaultListCellRenderer;
42 import javax.swing.JFileChooser;
43 import javax.swing.JList;
44 import javax.swing.JPanel;
45 import javax.swing.JScrollPane;
46 import javax.swing.SpringLayout;
47 import javax.swing.plaf.basic.BasicFileChooserUI;
48
49 /**
50  * Enhanced file chooser dialog box.
51  *
52  * NOTE: bug on Windows systems when filechooser opened on directory to view
53  * files with colons in title.
54  *
55  * @author AMW
56  *
57  */
58 public class JalviewFileChooser extends JFileChooser
59 {
60   /**
61    * Factory method to return a file chooser that offers readable alignment file
62    * formats
63    * 
64    * @param directory
65    * @param selected
66    * @return
67    */
68   public static JalviewFileChooser forRead(String directory, String selected)
69   {
70     List<String> extensions = new ArrayList<String>();
71     List<String> descs = new ArrayList<String>();
72     for (FileFormatI format : FileFormats.getInstance().getFormats())
73     {
74       if (format.isReadable())
75       {
76         extensions.add(format.getExtensions());
77         descs.add(format.getName());
78       }
79     }
80     return new JalviewFileChooser(directory,
81             extensions.toArray(new String[extensions.size()]),
82             descs.toArray(new String[descs.size()]), selected, true);
83   }
84
85   /**
86    * Factory method to return a file chooser that offers writable alignment file
87    * formats
88    * 
89    * @param directory
90    * @param selected
91    * @return
92    */
93   public static JalviewFileChooser forWrite(String directory,
94           String selected)
95   {
96     // TODO in Java 8, forRead and forWrite can be a single method
97     // with a lambda expression parameter for isReadable/isWritable
98     List<String> extensions = new ArrayList<String>();
99     List<String> descs = new ArrayList<String>();
100     for (FileFormatI format : FileFormats.getInstance().getFormats())
101     {
102       if (format.isWritable())
103       {
104         extensions.add(format.getExtensions());
105         descs.add(format.getName());
106       }
107     }
108     return new JalviewFileChooser(directory,
109             extensions.toArray(new String[extensions.size()]),
110             descs.toArray(new String[descs.size()]), selected, false);
111   }
112
113   public JalviewFileChooser(String dir)
114   {
115     super(safePath(dir));
116     setAccessory(new RecentlyOpened());
117   }
118
119   public JalviewFileChooser(String dir, String[] suffix, String[] desc,
120           String selected)
121   {
122     this(dir, suffix, desc, selected, true);
123   }
124
125   /**
126    * Constructor for a single choice of file extension and description
127    * 
128    * @param extension
129    * @param desc
130    */
131   public JalviewFileChooser(String extension, String desc)
132   {
133     this(Cache.getProperty("LAST_DIRECTORY"), new String[] { extension },
134             new String[] { desc }, desc, true);
135   }
136
137   JalviewFileChooser(String dir, String[] extensions, String[] descs,
138           String selected, boolean allFiles)
139   {
140     super(safePath(dir));
141     if (extensions.length == descs.length)
142     {
143       List<String[]> formats = new ArrayList<String[]>();
144       for (int i = 0; i < extensions.length; i++)
145       {
146         formats.add(new String[] { extensions[i], descs[i] });
147       }
148       init(formats, selected, allFiles);
149     }
150     else
151     {
152       System.err.println("JalviewFileChooser arguments mismatch: "
153               + extensions + ", " + descs);
154     }
155   }
156
157   private static File safePath(String dir)
158   {
159     if (dir == null)
160     {
161       return null;
162     }
163
164     File f = new File(dir);
165     if (f.getName().indexOf(':') > -1)
166     {
167       return null;
168     }
169     return f;
170   }
171
172   /**
173    * 
174    * @param formats
175    *          a list of {extensions, description} for each file format
176    * @param selected
177    * @param allFiles
178    *          if true, 'any format' option is included
179    */
180   void init(List<String[]> formats, String selected, boolean allFiles)
181   {
182
183     JalviewFileFilter chosen = null;
184
185     // SelectAllFilter needs to be set first before adding further
186     // file filters to fix bug on Mac OSX
187     setAcceptAllFileFilterUsed(allFiles);
188
189     for (String[] format : formats)
190     {
191       JalviewFileFilter jvf = new JalviewFileFilter(format[0], format[1]);
192       addChoosableFileFilter(jvf);
193       if ((selected != null) && selected.equalsIgnoreCase(format[1]))
194       {
195         chosen = jvf;
196       }
197     }
198
199     if (chosen != null)
200     {
201       setFileFilter(chosen);
202     }
203
204     setAccessory(new RecentlyOpened());
205   }
206
207   @Override
208   public void setFileFilter(javax.swing.filechooser.FileFilter filter)
209   {
210     super.setFileFilter(filter);
211
212     try
213     {
214       if (getUI() instanceof BasicFileChooserUI)
215       {
216         final BasicFileChooserUI fcui = (BasicFileChooserUI) getUI();
217         final String name = fcui.getFileName().trim();
218
219         if ((name == null) || (name.length() == 0))
220         {
221           return;
222         }
223
224         EventQueue.invokeLater(new Thread()
225         {
226           @Override
227           public void run()
228           {
229             String currentName = fcui.getFileName();
230             if ((currentName == null) || (currentName.length() == 0))
231             {
232               fcui.setFileName(name);
233             }
234           }
235         });
236       }
237     } catch (Exception ex)
238     {
239       ex.printStackTrace();
240       // Some platforms do not have BasicFileChooserUI
241     }
242   }
243
244   /**
245    * Returns the selected file format, or null if none selected
246    * 
247    * @return
248    */
249   public FileFormatI getSelectedFormat()
250   {
251     if (getFileFilter() == null)
252     {
253       return null;
254     }
255
256     /*
257      * logic here depends on option description being formatted as 
258      * formatName (extension, extension...)
259      * or the 'no option selected' value
260      * All Files
261      * @see JalviewFileFilter.getDescription
262      */
263     String format = getFileFilter().getDescription();
264     int parenPos = format.indexOf("(");
265     if (parenPos > 0)
266     {
267       format = format.substring(0, parenPos).trim();
268       try
269       {
270         return FileFormats.getInstance().forName(format);
271       } catch (IllegalArgumentException e)
272       {
273         System.err.println("Unexpected format: " + format);
274       }
275     }
276     return null;
277   }
278
279   @Override
280   public int showSaveDialog(Component parent) throws HeadlessException
281   {
282     this.setAccessory(null);
283
284     setDialogType(SAVE_DIALOG);
285
286     int ret = showDialog(parent, MessageManager.getString("action.save"));
287
288     if (getFileFilter() instanceof JalviewFileFilter)
289     {
290       JalviewFileFilter jvf = (JalviewFileFilter) getFileFilter();
291
292       if (!jvf.accept(getSelectedFile()))
293       {
294         String withExtension = getSelectedFile() + "."
295                 + jvf.getAcceptableExtension();
296         setSelectedFile(new File(withExtension));
297       }
298     }
299     // TODO: ENSURE THAT FILES SAVED WITH A ':' IN THE NAME ARE REFUSED AND THE
300     // USER PROMPTED FOR A NEW FILENAME
301     if ((ret == JalviewFileChooser.APPROVE_OPTION)
302             && getSelectedFile().exists())
303     {
304       int confirm = JvOptionPane.showConfirmDialog(parent,
305               MessageManager.getString("label.overwrite_existing_file"),
306               MessageManager.getString("label.file_already_exists"),
307               JvOptionPane.YES_NO_OPTION);
308
309       if (confirm != JvOptionPane.YES_OPTION)
310       {
311         ret = JalviewFileChooser.CANCEL_OPTION;
312       }
313     }
314
315     return ret;
316   }
317
318   void recentListSelectionChanged(Object selection)
319   {
320     setSelectedFile(null);
321     if (selection != null)
322     {
323       File file = new File((String) selection);
324       if (getFileFilter() instanceof JalviewFileFilter)
325       {
326         JalviewFileFilter jvf = (JalviewFileFilter) this.getFileFilter();
327
328         if (!jvf.accept(file))
329         {
330           setFileFilter(getChoosableFileFilters()[0]);
331         }
332       }
333
334       setSelectedFile(file);
335     }
336   }
337
338   class RecentlyOpened extends JPanel
339   {
340     JList list;
341
342     public RecentlyOpened()
343     {
344
345       String historyItems = jalview.bin.Cache.getProperty("RECENT_FILE");
346       StringTokenizer st;
347       Vector recent = new Vector();
348
349       if (historyItems != null)
350       {
351         st = new StringTokenizer(historyItems, "\t");
352
353         while (st.hasMoreTokens())
354         {
355           recent.addElement(st.nextElement());
356         }
357       }
358
359       list = new JList(recent);
360
361       DefaultListCellRenderer dlcr = new DefaultListCellRenderer();
362       dlcr.setHorizontalAlignment(DefaultListCellRenderer.RIGHT);
363       list.setCellRenderer(dlcr);
364
365       list.addMouseListener(new MouseAdapter()
366       {
367         @Override
368         public void mousePressed(MouseEvent evt)
369         {
370           recentListSelectionChanged(list.getSelectedValue());
371         }
372       });
373
374       this.setBorder(new javax.swing.border.TitledBorder(MessageManager
375               .getString("label.recently_opened")));
376
377       final JScrollPane scroller = new JScrollPane(list);
378
379       SpringLayout layout = new SpringLayout();
380       layout.putConstraint(SpringLayout.WEST, scroller, 5,
381               SpringLayout.WEST, this);
382       layout.putConstraint(SpringLayout.NORTH, scroller, 5,
383               SpringLayout.NORTH, this);
384
385       if (new Platform().isAMac())
386       {
387         scroller.setPreferredSize(new Dimension(500, 100));
388       }
389       else
390       {
391         scroller.setPreferredSize(new Dimension(130, 200));
392       }
393
394       this.add(scroller);
395
396       javax.swing.SwingUtilities.invokeLater(new Runnable()
397       {
398         @Override
399         public void run()
400         {
401           scroller.getHorizontalScrollBar().setValue(
402                   scroller.getHorizontalScrollBar().getMaximum());
403         }
404       });
405
406     }
407
408   }
409 }