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