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