Clover icon

Coverage Report

  1. Project Clover database Mon Sep 2 2024 17:57:51 BST
  2. Package jalview.io

File JalviewFileChooser.java

 

Coverage histogram

../../img/srcFileCovDistChart5.png
43% of files have more coverage

Code metrics

88
222
28
3
782
558
88
0.4
7.93
9.33
3.14

Classes

Class Line # Actions
JalviewFileChooser 75 166 73
0.3015267330.2%
JalviewFileChooser.RecentlyOpened 566 23 5
0.9333333493.3%
JalviewFileChooser.recentlyOpenedCellRenderer 633 33 10
0.869565287%
 

Contributing tests

This file is covered by 62 tests. .

Source view

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 java.awt.Component;
25    import java.awt.Dimension;
26    import java.awt.EventQueue;
27    import java.awt.HeadlessException;
28    import java.awt.event.ActionEvent;
29    import java.awt.event.ActionListener;
30    import java.awt.event.MouseAdapter;
31    import java.awt.event.MouseEvent;
32    import java.beans.PropertyChangeEvent;
33    import java.beans.PropertyChangeListener;
34    import java.io.File;
35    import java.util.ArrayList;
36    import java.util.HashMap;
37    import java.util.List;
38    import java.util.Map;
39    import java.util.StringTokenizer;
40    import java.util.Vector;
41   
42    import javax.swing.BoxLayout;
43    import javax.swing.JCheckBox;
44    import javax.swing.JDialog;
45    import javax.swing.JFileChooser;
46    import javax.swing.JLabel;
47    import javax.swing.JList;
48    import javax.swing.JOptionPane;
49    import javax.swing.JPanel;
50    import javax.swing.JScrollPane;
51    import javax.swing.ListCellRenderer;
52    import javax.swing.SpringLayout;
53    import javax.swing.SwingConstants;
54    import javax.swing.SwingUtilities;
55    import javax.swing.border.TitledBorder;
56    import javax.swing.filechooser.FileFilter;
57    import javax.swing.plaf.basic.BasicFileChooserUI;
58   
59    import jalview.bin.Cache;
60    import jalview.gui.JvOptionPane;
61    import jalview.util.ChannelProperties;
62    import jalview.util.MessageManager;
63    import jalview.util.Platform;
64    import jalview.util.dialogrunner.DialogRunnerI;
65   
66    /**
67    * Enhanced file chooser dialog box.
68    *
69    * NOTE: bug on Windows systems when filechooser opened on directory to view
70    * files with colons in title.
71    *
72    * @author AMW
73    *
74    */
 
75    public class JalviewFileChooser extends JFileChooser
76    implements DialogRunnerI, PropertyChangeListener
77    {
78    private static final long serialVersionUID = 1L;
79   
80    private Map<Object, Runnable> callbacks = new HashMap<>();
81   
82    File selectedFile = null;
83   
84    /**
85    * backupfilesCheckBox = "Include backup files" checkbox includeBackupfiles =
86    * flag set by checkbox
87    */
88    private JCheckBox backupfilesCheckBox = null;
89   
90    protected boolean includeBackupFiles = false;
91   
92    /**
93    * Factory method to return a file chooser that offers readable alignment file
94    * formats
95    *
96    * @param directory
97    * @param selected
98    * @return
99    */
 
100  75 toggle public static JalviewFileChooser forRead(String directory,
101    String selected)
102    {
103  75 return JalviewFileChooser.forRead(directory, selected, false);
104    }
105   
 
106  75 toggle public static JalviewFileChooser forRead(String directory,
107    String selected, boolean allowBackupFiles)
108    {
109  75 List<String> extensions = new ArrayList<>();
110  75 List<String> descs = new ArrayList<>();
111  75 for (FileFormatI format : FileFormats.getInstance().getFormats())
112    {
113  1650 if (format.isReadable())
114    {
115  1425 extensions.add(format.getExtensions());
116  1425 descs.add(format.getName());
117    }
118    }
119   
120  75 return new JalviewFileChooser(directory,
121    extensions.toArray(new String[extensions.size()]),
122    descs.toArray(new String[descs.size()]), selected, true,
123    allowBackupFiles);
124    }
125   
126    /**
127    * Factory method to return a file chooser that offers writable alignment file
128    * formats
129    *
130    * @param directory
131    * @param selected
132    * @return
133    */
 
134  0 toggle public static JalviewFileChooser forWrite(String directory,
135    String selected)
136    {
137    // TODO in Java 8, forRead and forWrite can be a single method
138    // with a lambda expression parameter for isReadable/isWritable
139  0 List<String> extensions = new ArrayList<>();
140  0 List<String> descs = new ArrayList<>();
141  0 for (FileFormatI format : FileFormats.getInstance().getFormats())
142    {
143  0 if (format.isWritable())
144    {
145  0 extensions.add(format.getExtensions());
146  0 descs.add(format.getName());
147    }
148    }
149  0 return new JalviewFileChooser(directory,
150    extensions.toArray(new String[extensions.size()]),
151    descs.toArray(new String[descs.size()]), selected, false);
152    }
153   
 
154  0 toggle public JalviewFileChooser(String dir)
155    {
156  0 super(safePath(dir));
157  0 setAccessory(new RecentlyOpened());
158    }
159   
 
160  0 toggle public JalviewFileChooser(String dir, String[] suffix, String[] desc,
161    String selected)
162    {
163  0 this(dir, suffix, desc, selected, true);
164    }
165   
166    /**
167    * Constructor for a single choice of file extension and description
168    *
169    * @param extension
170    * @param desc
171    */
 
172  0 toggle public JalviewFileChooser(String extension, String desc)
173    {
174  0 this(Cache.getProperty("LAST_DIRECTORY"), new String[] { extension },
175    new String[]
176    { desc }, desc, true);
177    }
178   
 
179  0 toggle JalviewFileChooser(String dir, String[] extensions, String[] descs,
180    String selected, boolean acceptAny)
181    {
182  0 this(dir, extensions, descs, selected, acceptAny, false);
183    }
184   
 
185  73 toggle public JalviewFileChooser(String dir, String[] extensions, String[] descs,
186    String selected, boolean acceptAny, boolean allowBackupFiles)
187    {
188  73 super(safePath(dir));
189  73 if (extensions.length == descs.length)
190    {
191  73 List<String[]> formats = new ArrayList<>();
192  1460 for (int i = 0; i < extensions.length; i++)
193    {
194  1387 formats.add(new String[] { extensions[i], descs[i] });
195    }
196  73 init(formats, selected, acceptAny, allowBackupFiles);
197    }
198    else
199    {
200  0 jalview.bin.Console
201    .errPrintln("JalviewFileChooser arguments mismatch: "
202    + extensions + ", " + descs);
203    }
204    }
205   
 
206  75 toggle private static File safePath(String dir)
207    {
208  75 if (dir == null)
209    {
210  42 return null;
211    }
212   
213  33 File f = new File(dir);
214  33 if (f.getName().indexOf(':') > -1)
215    {
216  0 return null;
217    }
218  33 return f;
219    }
220   
221    /**
222    * Overridden for JalviewJS compatibility: only one thread in Javascript, so
223    * we can't wait for user choice in another thread and then perform the
224    * desired action
225    */
 
226  0 toggle @Override
227    public int showOpenDialog(Component parent)
228    {
229  0 int value = super.showOpenDialog(this);
230   
231  0 if (!Platform.isJS())
232    /**
233    * Java only
234    *
235    * @j2sIgnore
236    */
237    {
238    /*
239    * code here is not run in JalviewJS, instead
240    * propertyChange() is called for dialog action
241    */
242  0 handleResponse(value);
243    }
244  0 return value;
245    }
246   
247    /**
248    *
249    * @param formats
250    * a list of {extensions, description} for each file format
251    * @param selected
252    * @param acceptAny
253    * if true, 'any format' option is included
254    */
 
255  0 toggle void init(List<String[]> formats, String selected, boolean acceptAny)
256    {
257  0 init(formats, selected, acceptAny, false);
258    }
259   
 
260  73 toggle void init(List<String[]> formats, String selected, boolean acceptAny,
261    boolean allowBackupFiles)
262    {
263   
264  73 JalviewFileFilter chosen = null;
265   
266    // SelectAllFilter needs to be set first before adding further
267    // file filters to fix bug on Mac OSX
268  73 setAcceptAllFileFilterUsed(acceptAny);
269   
270    // add a "All known alignment files" option
271  72 List<String> allExtensions = new ArrayList<>();
272  72 for (String[] format : formats)
273    {
274  1368 String[] extensions = format[0].split(",");
275  1368 for (String ext : extensions)
276    {
277  2016 if (!allExtensions.contains(ext))
278    {
279  2016 allExtensions.add(ext);
280    }
281    }
282    }
283  72 allExtensions.sort(null);
284  72 JalviewFileFilter alljvf = new JalviewFileFilter(
285    allExtensions.toArray(new String[] {}),
286    MessageManager.getString("label.all_known_alignment_files"));
287  72 alljvf.setExtensionListInDescription(false);
288  72 addChoosableFileFilter(alljvf);
289   
290  72 if (selected == null)
291    {
292  14 chosen = alljvf;
293    }
294   
295  72 for (String[] format : formats)
296    {
297  1368 JalviewFileFilter jvf = new JalviewFileFilter(format[0], format[1]);
298  1368 if (allowBackupFiles)
299    {
300  0 jvf.setParentJFC(this);
301    }
302  1368 addChoosableFileFilter(jvf);
303  1368 if ((selected != null) && selected.equalsIgnoreCase(format[1]))
304    {
305  58 chosen = jvf;
306    }
307    }
308   
309  72 if (chosen != null)
310    {
311  72 setFileFilter(chosen);
312    }
313   
314  72 if (allowBackupFiles)
315    {
316  0 JPanel multi = new JPanel();
317  0 multi.setLayout(new BoxLayout(multi, BoxLayout.PAGE_AXIS));
318  0 if (backupfilesCheckBox == null)
319    {
320  0 try
321    {
322  0 includeBackupFiles = Boolean.parseBoolean(
323    Cache.getProperty(BackupFiles.NS + "_FC_INCLUDE"));
324    } catch (Exception e)
325    {
326  0 includeBackupFiles = false;
327    }
328  0 backupfilesCheckBox = new JCheckBox(
329    MessageManager.getString("label.include_backup_files"),
330    includeBackupFiles);
331  0 backupfilesCheckBox.setAlignmentX(Component.CENTER_ALIGNMENT);
332  0 JalviewFileChooser jfc = this;
333  0 backupfilesCheckBox.addActionListener(new ActionListener()
334    {
 
335  0 toggle @Override
336    public void actionPerformed(ActionEvent e)
337    {
338  0 includeBackupFiles = backupfilesCheckBox.isSelected();
339  0 Cache.setProperty(BackupFiles.NS + "_FC_INCLUDE",
340    String.valueOf(includeBackupFiles));
341   
342  0 FileFilter f = jfc.getFileFilter();
343    // deselect the selected file if it's no longer choosable
344  0 File selectedFile = jfc.getSelectedFile();
345  0 if (selectedFile != null && !f.accept(selectedFile))
346    {
347  0 jfc.setSelectedFile(null);
348    }
349    // fake the OK button changing (to force it to upate)
350  0 String s = jfc.getApproveButtonText();
351  0 jfc.firePropertyChange(APPROVE_BUTTON_TEXT_CHANGED_PROPERTY,
352    null, s);
353    // fake the file filter changing (its behaviour actually has)
354  0 jfc.firePropertyChange(FILE_FILTER_CHANGED_PROPERTY, null, f);
355   
356  0 jfc.rescanCurrentDirectory();
357  0 jfc.revalidate();
358  0 jfc.repaint();
359    }
360    });
361    }
362  0 multi.add(new RecentlyOpened());
363  0 multi.add(backupfilesCheckBox);
364  0 setAccessory(multi);
365    }
366    else
367    {
368    // set includeBackupFiles=false to avoid other file choosers from picking
369    // up backup files (Just In Case)
370  72 includeBackupFiles = false;
371  72 setAccessory(new RecentlyOpened());
372    }
373    }
374   
 
375  366 toggle @Override
376    public void setFileFilter(javax.swing.filechooser.FileFilter filter)
377    {
378  366 super.setFileFilter(filter);
379   
380  365 try
381    {
382  365 if (getUI() instanceof BasicFileChooserUI)
383    {
384  365 final BasicFileChooserUI fcui = (BasicFileChooserUI) getUI();
385  365 final String name = fcui.getFileName().trim();
386   
387  365 if ((name == null) || (name.length() == 0))
388    {
389  365 return;
390    }
391   
392  0 EventQueue.invokeLater(new Thread()
393    {
 
394  0 toggle @Override
395    public void run()
396    {
397  0 String currentName = fcui.getFileName();
398  0 if ((currentName == null) || (currentName.length() == 0))
399    {
400  0 fcui.setFileName(name);
401    }
402    }
403    });
404    }
405    } catch (Exception ex)
406    {
407  0 ex.printStackTrace();
408    // Some platforms do not have BasicFileChooserUI
409    }
410    }
411   
412    /**
413    * Returns the selected file format, or null if none selected
414    *
415    * @return
416    */
 
417  0 toggle public FileFormatI getSelectedFormat()
418    {
419  0 if (getFileFilter() == null)
420    {
421  0 return null;
422    }
423   
424    /*
425    * logic here depends on option description being formatted as
426    * formatName (extension, extension...)
427    * or the 'no option selected' value
428    * All Files
429    * @see JalviewFileFilter.getDescription
430    */
431  0 String format = getFileFilter().getDescription();
432  0 int parenPos = format.indexOf("(");
433  0 if (parenPos > 0)
434    {
435  0 format = format.substring(0, parenPos).trim();
436  0 try
437    {
438  0 return FileFormats.getInstance().forName(format);
439    } catch (IllegalArgumentException e)
440    {
441  0 jalview.bin.Console.errPrintln("Unexpected format: " + format);
442    }
443    }
444  0 return null;
445    }
446   
 
447  75 toggle @Override
448    public File getSelectedFile()
449    {
450  75 File f = super.getSelectedFile();
451  75 return f == null ? selectedFile : f;
452    }
453   
 
454  0 toggle @Override
455    public int showSaveDialog(Component parent) throws HeadlessException
456    {
457  0 this.setAccessory(null);
458    // Java 9,10,11 on OSX - clear selected file so name isn't auto populated
459  0 this.setSelectedFile(null);
460   
461  0 return super.showSaveDialog(parent);
462    }
463   
464    /**
465    * If doing a Save, and an existing file is chosen or entered, prompt for
466    * confirmation of overwrite. Proceed if Yes, else leave the file chooser
467    * open.
468    *
469    * @see https://stackoverflow.com/questions/8581215/jfilechooser-and-checking-for-overwrite
470    */
 
471  0 toggle @Override
472    public void approveSelection()
473    {
474  0 if (getDialogType() != SAVE_DIALOG)
475    {
476  0 super.approveSelection();
477  0 return;
478    }
479   
480  0 selectedFile = getSelectedFile();
481   
482  0 if (selectedFile == null)
483    {
484    // Workaround for Java 9,10 on OSX - no selected file, but there is a
485    // filename typed in
486  0 try
487    {
488  0 String filename = ((BasicFileChooserUI) getUI()).getFileName();
489  0 if (filename != null && filename.length() > 0)
490    {
491  0 selectedFile = new File(getCurrentDirectory(), filename);
492    }
493    } catch (Throwable x)
494    {
495  0 jalview.bin.Console.errPrintln(
496    "Unexpected exception when trying to get filename.");
497  0 x.printStackTrace();
498    }
499    // TODO: ENSURE THAT FILES SAVED WITH A ':' IN THE NAME ARE REFUSED AND
500    // THE
501    // USER PROMPTED FOR A NEW FILENAME
502    }
503   
504  0 if (selectedFile == null)
505    {
506  0 return;
507    }
508   
509  0 if (getFileFilter() instanceof JalviewFileFilter)
510    {
511  0 JalviewFileFilter jvf = (JalviewFileFilter) getFileFilter();
512   
513  0 if (!jvf.accept(selectedFile))
514    {
515  0 String withExtension = getSelectedFile().getName() + "."
516    + jvf.getAcceptableExtension();
517  0 selectedFile = (new File(getCurrentDirectory(), withExtension));
518  0 setSelectedFile(selectedFile);
519    }
520    }
521   
522  0 if (selectedFile.exists())
523    {
524  0 int confirm = Cache.getDefault("CONFIRM_OVERWRITE_FILE", true)
525    ? JvOptionPane.showConfirmDialog(this,
526    MessageManager
527    .getString("label.overwrite_existing_file"),
528    MessageManager.getString("label.file_already_exists"),
529    JvOptionPane.YES_NO_OPTION)
530    : JOptionPane.YES_OPTION;
531   
532  0 if (confirm != JvOptionPane.YES_OPTION)
533    {
534  0 return;
535    }
536    }
537   
538  0 super.approveSelection();
539    }
540   
 
541  0 toggle void recentListSelectionChanged(Object selection)
542    {
543  0 setSelectedFile(null);
544  0 if (selection != null)
545    {
546  0 File file = new File((String) selection);
547  0 if (getFileFilter() instanceof JalviewFileFilter)
548    {
549  0 JalviewFileFilter jvf = (JalviewFileFilter) this.getFileFilter();
550   
551  0 if (!jvf.accept(file))
552    {
553  0 setFileFilter(getChoosableFileFilters()[0]);
554    }
555    }
556   
557  0 if (!file.isAbsolute() && file.exists())
558    {
559  0 file = file.getAbsoluteFile();
560    }
561   
562  0 setSelectedFile(file);
563    }
564    }
565   
 
566    class RecentlyOpened extends JPanel
567    {
568    private static final long serialVersionUID = 1L;
569   
570    JList<String> list;
571   
 
572  72 toggle RecentlyOpened()
573    {
574  72 setPreferredSize(new Dimension(300, 100));
575  72 String historyItems = Cache.getProperty("RECENT_FILE");
576  72 StringTokenizer st;
577  72 Vector<String> recent = new Vector<>();
578   
579  72 if (historyItems != null)
580    {
581  61 st = new StringTokenizer(historyItems, "\t");
582   
583  360 while (st.hasMoreTokens())
584    {
585  299 recent.addElement(st.nextToken());
586    }
587    }
588   
589  72 list = new JList<>(recent);
590  72 list.setCellRenderer(new recentlyOpenedCellRenderer());
591   
592  72 list.addMouseListener(new MouseAdapter()
593    {
 
594  0 toggle @Override
595    public void mousePressed(MouseEvent evt)
596    {
597  0 recentListSelectionChanged(list.getSelectedValue());
598    }
599    });
600   
601  72 TitledBorder recentlyOpenedBorder = new TitledBorder(
602    MessageManager.getString("label.recently_opened"));
603  72 recentlyOpenedBorder.setTitleFont(
604    recentlyOpenedBorder.getTitleFont().deriveFont(10f));
605  72 this.setBorder(recentlyOpenedBorder);
606   
607  72 final JScrollPane scroller = new JScrollPane(list);
608   
609  72 SpringLayout layout = new SpringLayout();
610  72 layout.putConstraint(SpringLayout.WEST, scroller, 5,
611    SpringLayout.WEST, this);
612  72 layout.putConstraint(SpringLayout.NORTH, scroller, 5,
613    SpringLayout.NORTH, this);
614   
615    // one size okay for all
616  72 scroller.setPreferredSize(new Dimension(280, 105));
617  72 this.add(scroller);
618   
619  72 SwingUtilities.invokeLater(new Runnable()
620    {
 
621  71 toggle @Override
622    public void run()
623    {
624  71 scroller.getHorizontalScrollBar()
625    .setValue(scroller.getHorizontalScrollBar().getMaximum());
626    }
627    });
628   
629    }
630   
631    }
632   
 
633    class recentlyOpenedCellRenderer extends JLabel
634    implements ListCellRenderer<String>
635    {
636    private final static int maxChars = 46;
637   
638    private final static String ellipsis = "...";
639   
 
640  299 toggle @Override
641    public Component getListCellRendererComponent(
642    JList<? extends String> list, String value, int index,
643    boolean isSelected, boolean cellHasFocus)
644    {
645  299 String filename = value.toString();
646  299 String displayFilename;
647  299 if (filename.length() > maxChars)
648    {
649  164 StringBuilder displayFileSB = new StringBuilder();
650  164 File file = new File(filename);
651  164 displayFileSB.append(file.getName());
652  164 if (file.getParent() != null)
653    {
654  164 File parent = file;
655  164 boolean spaceleft = true;
656  983 while (spaceleft && parent.getParent() != null)
657    {
658  819 parent = parent.getParentFile();
659  819 String name = parent.getName();
660  819 displayFileSB.insert(0, File.separator);
661  819 if (displayFileSB.length() + name.length() < maxChars - 1)
662    {
663  655 displayFileSB.insert(0, name);
664    }
665    else
666    {
667  164 displayFileSB.insert(0, ellipsis);
668  164 spaceleft = false;
669    }
670    }
671  164 if (spaceleft && filename.startsWith(File.separator)
672    && !(displayFileSB.charAt(0) == File.separatorChar))
673    {
674  0 displayFileSB.insert(0, File.separator);
675    }
676    }
677  164 displayFilename = displayFileSB.toString();
678    }
679    else
680    {
681  135 displayFilename = filename;
682    }
683  299 this.setText(displayFilename.toString());
684  299 this.setToolTipText(filename);
685  299 if (isSelected)
686    {
687  0 setBackground(list.getSelectionBackground());
688  0 setForeground(list.getSelectionForeground());
689    }
690    else
691    {
692  299 setBackground(list.getBackground());
693  299 setForeground(list.getForeground());
694    }
695  299 this.setHorizontalAlignment(SwingConstants.TRAILING);
696  299 this.setEnabled(list.isEnabled());
697  299 this.setFont(list.getFont().deriveFont(12f));
698  299 this.setOpaque(true);
699  299 return this;
700    }
701   
702    }
703   
704    /*
705    @Override
706    public JalviewFileChooser setResponseHandler(Object response,
707    Runnable action)
708    {
709    callbacks.put(response, new Callable<Void>()
710    {
711    @Override
712    public Void call()
713    {
714    action.run();
715    return null;
716    }
717    });
718    return this;
719    }
720    */
721   
 
722  0 toggle @Override
723    public DialogRunnerI setResponseHandler(Object response, Runnable action)
724    {
725  0 callbacks.put(response, action);
726  0 return this;
727    }
728   
 
729  0 toggle @Override
730    public void handleResponse(Object response)
731    {
732    /*
733    * this test is for NaN in Chrome
734    */
735  0 if (response != null && !response.equals(response))
736    {
737  0 return;
738    }
739  0 Runnable action = callbacks.get(response);
740  0 if (action != null)
741    {
742  0 try
743    {
744  0 action.run();
745    } catch (Exception e)
746    {
747  0 e.printStackTrace();
748    }
749    }
750    }
751   
752    /**
753    * JalviewJS signals file selection by a property change event for property
754    * "SelectedFile". This methods responds to that by running the response
755    * action for 'OK' in the dialog.
756    *
757    * @param evt
758    */
 
759  0 toggle @Override
760    public void propertyChange(PropertyChangeEvent evt)
761    {
762    // TODO other properties need runners...
763  0 switch (evt.getPropertyName())
764    {
765    /*
766    * property name here matches that used in JFileChooser.js
767    */
768  0 case "SelectedFile":
769  0 handleResponse(APPROVE_OPTION);
770  0 break;
771    }
772    }
773   
 
774  0 toggle @Override
775    protected JDialog createDialog(Component parent) throws HeadlessException
776    {
777  0 JDialog dialog = super.createDialog(parent);
778  0 dialog.setIconImages(ChannelProperties.getIconList());
779  0 return dialog;
780    }
781   
782    }