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