JAL-3048 AlignFrame -> Save dialog refactored - patched ‘File exists - Overwrite...
[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     int value = showOpenDialog(this);
197     runner.run(value);
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   Component saveparent;
308   RunResponse overwriteCheck = new RunResponse(
309           JalviewFileChooser.APPROVE_OPTION)
310   {
311     @Override
312     public void run()
313     {
314       // JBP Note - this code was executed regardless of 'SAVE' being pressed
315       // need to see if there were side effects
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       // All good, so we continue to save
328       returned = new Response(JalviewFileChooser.APPROVE_OPTION);
329
330       // TODO: ENSURE THAT FILES SAVED WITH A ':' IN THE NAME ARE REFUSED AND THE
331       // USER PROMPTED FOR A NEW FILENAME
332       /**
333        * @j2sNative
334        */
335       {
336         if (getSelectedFile().exists())
337         {
338           // TODO JAL-3048 - may not need to raise this for browser saves
339
340           // yes/no cancel
341           int confirm = JvOptionPane.showConfirmDialog(saveparent,
342                   MessageManager.getString("label.overwrite_existing_file"),
343                   MessageManager.getString("label.file_already_exists"),
344                   JvOptionPane.YES_NO_OPTION);
345
346           if (confirm != JvOptionPane.YES_OPTION)
347           {
348             returned = new Response(JalviewFileChooser.CANCEL_OPTION);
349           }
350         }
351       }
352     };
353   };
354
355   @Override
356   public int showSaveDialog(Component parent) throws HeadlessException
357   {
358     this.setAccessory(null);
359
360     /*
361      * Save dialog is opened until user picks a file format 
362      */
363     if (!runner.isRegistered(overwriteCheck))
364     {
365       // first call for this instance
366       runner.firstResponse(overwriteCheck);
367     }
368     else
369     {
370       // reset response flags
371       runner.resetResponses();
372     }
373
374     setDialogType(SAVE_DIALOG);
375     saveparent = parent;
376
377     int value = showDialog(parent, MessageManager.getString("action.save"));
378     
379     runner.run(value);
380     return value;
381   }
382
383   void recentListSelectionChanged(Object selection)
384   {
385     setSelectedFile(null);
386     if (selection != null)
387     {
388       File file = new File((String) selection);
389       if (getFileFilter() instanceof JalviewFileFilter)
390       {
391         JalviewFileFilter jvf = (JalviewFileFilter) this.getFileFilter();
392
393         if (!jvf.accept(file))
394         {
395           setFileFilter(getChoosableFileFilters()[0]);
396         }
397       }
398
399       setSelectedFile(file);
400     }
401   }
402
403   class RecentlyOpened extends JPanel
404   {
405     JList list;
406
407     public RecentlyOpened()
408     {
409
410       String historyItems = jalview.bin.Cache.getProperty("RECENT_FILE");
411       StringTokenizer st;
412       Vector recent = new Vector();
413
414       if (historyItems != null)
415       {
416         st = new StringTokenizer(historyItems, "\t");
417
418         while (st.hasMoreTokens())
419         {
420           recent.addElement(st.nextElement());
421         }
422       }
423
424       list = new JList(recent);
425
426       DefaultListCellRenderer dlcr = new DefaultListCellRenderer();
427       dlcr.setHorizontalAlignment(DefaultListCellRenderer.RIGHT);
428       list.setCellRenderer(dlcr);
429
430       list.addMouseListener(new MouseAdapter()
431       {
432         @Override
433         public void mousePressed(MouseEvent evt)
434         {
435           recentListSelectionChanged(list.getSelectedValue());
436         }
437       });
438
439       this.setBorder(new javax.swing.border.TitledBorder(
440               MessageManager.getString("label.recently_opened")));
441
442       final JScrollPane scroller = new JScrollPane(list);
443
444       SpringLayout layout = new SpringLayout();
445       layout.putConstraint(SpringLayout.WEST, scroller, 5,
446               SpringLayout.WEST, this);
447       layout.putConstraint(SpringLayout.NORTH, scroller, 5,
448               SpringLayout.NORTH, this);
449
450       if (new Platform().isAMac())
451       {
452         scroller.setPreferredSize(new Dimension(500, 100));
453       }
454       else
455       {
456         scroller.setPreferredSize(new Dimension(130, 200));
457       }
458
459       this.add(scroller);
460
461       javax.swing.SwingUtilities.invokeLater(new Runnable()
462       {
463         @Override
464         public void run()
465         {
466           scroller.getHorizontalScrollBar()
467                   .setValue(scroller.getHorizontalScrollBar().getMaximum());
468         }
469       });
470
471     }
472
473   }
474
475   @Override
476   public JalviewFileChooser response(RunResponse action)
477   {
478     return runner.response(action);
479   }
480
481 }