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