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