/* * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) * Copyright (C) $$Year-Rel$$ The Jalview Authors * * This file is part of Jalview. * * Jalview is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * Jalview is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty * of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Jalview. If not, see . * The Jalview Authors are detailed in the 'AUTHORS' file. */ ////////////////////////////////////////////////////////////////// package jalview.io; import java.awt.Component; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.HeadlessException; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import java.util.Vector; import javax.swing.BoxLayout; import javax.swing.JCheckBox; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.ListCellRenderer; import javax.swing.SpringLayout; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.border.TitledBorder; import javax.swing.filechooser.FileFilter; import javax.swing.plaf.basic.BasicFileChooserUI; import jalview.bin.Cache; import jalview.gui.JvOptionPane; import jalview.util.ChannelProperties; import jalview.util.MessageManager; import jalview.util.Platform; import jalview.util.dialogrunner.DialogRunnerI; /** * Enhanced file chooser dialog box. * * NOTE: bug on Windows systems when filechooser opened on directory to view * files with colons in title. * * @author AMW * */ public class JalviewFileChooser extends JFileChooser implements DialogRunnerI, PropertyChangeListener { private static final long serialVersionUID = 1L; private Map callbacks = new HashMap<>(); File selectedFile = null; /** * backupfilesCheckBox = "Include backup files" checkbox includeBackupfiles = * flag set by checkbox */ private JCheckBox backupfilesCheckBox = null; protected boolean includeBackupFiles = false; /** * Factory method to return a file chooser that offers readable alignment file * formats * * @param directory * @param selected * @return */ public static JalviewFileChooser forRead(String directory, String selected) { return JalviewFileChooser.forRead(directory, selected, false); } public static JalviewFileChooser forRead(String directory, String selected, boolean allowBackupFiles) { List extensions = new ArrayList<>(); List descs = new ArrayList<>(); for (FileFormatI format : FileFormats.getInstance().getFormats()) { if (format.isReadable()) { extensions.add(format.getExtensions()); descs.add(format.getName()); } } return new JalviewFileChooser(directory, extensions.toArray(new String[extensions.size()]), descs.toArray(new String[descs.size()]), selected, true, allowBackupFiles); } /** * Factory method to return a file chooser that offers writable alignment file * formats * * @param directory * @param selected * @return */ public static JalviewFileChooser forWrite(String directory, String selected) { // TODO in Java 8, forRead and forWrite can be a single method // with a lambda expression parameter for isReadable/isWritable List extensions = new ArrayList<>(); List descs = new ArrayList<>(); for (FileFormatI format : FileFormats.getInstance().getFormats()) { if (format.isWritable()) { extensions.add(format.getExtensions()); descs.add(format.getName()); } } return new JalviewFileChooser(directory, extensions.toArray(new String[extensions.size()]), descs.toArray(new String[descs.size()]), selected, false); } public JalviewFileChooser(String dir) { super(safePath(dir)); setAccessory(new RecentlyOpened()); } public JalviewFileChooser(String dir, String[] suffix, String[] desc, String selected) { this(dir, suffix, desc, selected, true); } /** * Constructor for a single choice of file extension and description * * @param extension * @param desc */ public JalviewFileChooser(String extension, String desc) { this(Cache.getProperty("LAST_DIRECTORY"), new String[] { extension }, new String[] { desc }, desc, true); } JalviewFileChooser(String dir, String[] extensions, String[] descs, String selected, boolean acceptAny) { this(dir, extensions, descs, selected, acceptAny, false); } public JalviewFileChooser(String dir, String[] extensions, String[] descs, String selected, boolean acceptAny, boolean allowBackupFiles) { super(safePath(dir)); if (extensions.length == descs.length) { List formats = new ArrayList<>(); for (int i = 0; i < extensions.length; i++) { formats.add(new String[] { extensions[i], descs[i] }); } init(formats, selected, acceptAny, allowBackupFiles); } else { jalview.bin.Console.errPrintln("JalviewFileChooser arguments mismatch: " + extensions + ", " + descs); } } private static File safePath(String dir) { if (dir == null) { return null; } File f = new File(dir); if (f.getName().indexOf(':') > -1) { return null; } return f; } /** * Overridden for JalviewJS compatibility: only one thread in Javascript, so * we can't wait for user choice in another thread and then perform the * desired action */ @Override public int showOpenDialog(Component parent) { int value = super.showOpenDialog(this); if (!Platform.isJS()) /** * Java only * * @j2sIgnore */ { /* * code here is not run in JalviewJS, instead * propertyChange() is called for dialog action */ handleResponse(value); } return value; } /** * * @param formats * a list of {extensions, description} for each file format * @param selected * @param acceptAny * if true, 'any format' option is included */ void init(List formats, String selected, boolean acceptAny) { init(formats, selected, acceptAny, false); } void init(List formats, String selected, boolean acceptAny, boolean allowBackupFiles) { JalviewFileFilter chosen = null; // SelectAllFilter needs to be set first before adding further // file filters to fix bug on Mac OSX setAcceptAllFileFilterUsed(acceptAny); // add a "All known alignment files" option List allExtensions = new ArrayList<>(); for (String[] format : formats) { String[] extensions = format[0].split(","); for (String ext : extensions) { if (!allExtensions.contains(ext)) { allExtensions.add(ext); } } } allExtensions.sort(null); JalviewFileFilter alljvf = new JalviewFileFilter( allExtensions.toArray(new String[] {}), MessageManager.getString("label.all_known_alignment_files")); alljvf.setExtensionListInDescription(false); addChoosableFileFilter(alljvf); if (selected == null) { chosen = alljvf; } for (String[] format : formats) { JalviewFileFilter jvf = new JalviewFileFilter(format[0], format[1]); if (allowBackupFiles) { jvf.setParentJFC(this); } addChoosableFileFilter(jvf); if ((selected != null) && selected.equalsIgnoreCase(format[1])) { chosen = jvf; } } if (chosen != null) { setFileFilter(chosen); } if (allowBackupFiles) { JPanel multi = new JPanel(); multi.setLayout(new BoxLayout(multi, BoxLayout.PAGE_AXIS)); if (backupfilesCheckBox == null) { try { includeBackupFiles = Boolean.parseBoolean( Cache.getProperty(BackupFiles.NS + "_FC_INCLUDE")); } catch (Exception e) { includeBackupFiles = false; } backupfilesCheckBox = new JCheckBox( MessageManager.getString("label.include_backup_files"), includeBackupFiles); backupfilesCheckBox.setAlignmentX(Component.CENTER_ALIGNMENT); JalviewFileChooser jfc = this; backupfilesCheckBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { includeBackupFiles = backupfilesCheckBox.isSelected(); Cache.setProperty(BackupFiles.NS + "_FC_INCLUDE", String.valueOf(includeBackupFiles)); FileFilter f = jfc.getFileFilter(); // deselect the selected file if it's no longer choosable File selectedFile = jfc.getSelectedFile(); if (selectedFile != null && !f.accept(selectedFile)) { jfc.setSelectedFile(null); } // fake the OK button changing (to force it to upate) String s = jfc.getApproveButtonText(); jfc.firePropertyChange(APPROVE_BUTTON_TEXT_CHANGED_PROPERTY, null, s); // fake the file filter changing (its behaviour actually has) jfc.firePropertyChange(FILE_FILTER_CHANGED_PROPERTY, null, f); jfc.rescanCurrentDirectory(); jfc.revalidate(); jfc.repaint(); } }); } multi.add(new RecentlyOpened()); multi.add(backupfilesCheckBox); setAccessory(multi); } else { // set includeBackupFiles=false to avoid other file choosers from picking // up backup files (Just In Case) includeBackupFiles = false; setAccessory(new RecentlyOpened()); } } @Override public void setFileFilter(javax.swing.filechooser.FileFilter filter) { super.setFileFilter(filter); try { if (getUI() instanceof BasicFileChooserUI) { final BasicFileChooserUI fcui = (BasicFileChooserUI) getUI(); final String name = fcui.getFileName().trim(); if ((name == null) || (name.length() == 0)) { return; } EventQueue.invokeLater(new Thread() { @Override public void run() { String currentName = fcui.getFileName(); if ((currentName == null) || (currentName.length() == 0)) { fcui.setFileName(name); } } }); } } catch (Exception ex) { ex.printStackTrace(); // Some platforms do not have BasicFileChooserUI } } /** * Returns the selected file format, or null if none selected * * @return */ public FileFormatI getSelectedFormat() { if (getFileFilter() == null) { return null; } /* * logic here depends on option description being formatted as * formatName (extension, extension...) * or the 'no option selected' value * All Files * @see JalviewFileFilter.getDescription */ String format = getFileFilter().getDescription(); int parenPos = format.indexOf("("); if (parenPos > 0) { format = format.substring(0, parenPos).trim(); try { return FileFormats.getInstance().forName(format); } catch (IllegalArgumentException e) { jalview.bin.Console.errPrintln("Unexpected format: " + format); } } return null; } @Override public File getSelectedFile() { File f = super.getSelectedFile(); return f == null ? selectedFile : f; } @Override public int showSaveDialog(Component parent) throws HeadlessException { this.setAccessory(null); // Java 9,10,11 on OSX - clear selected file so name isn't auto populated this.setSelectedFile(null); return super.showSaveDialog(parent); } /** * If doing a Save, and an existing file is chosen or entered, prompt for * confirmation of overwrite. Proceed if Yes, else leave the file chooser * open. * * @see https://stackoverflow.com/questions/8581215/jfilechooser-and-checking-for-overwrite */ @Override public void approveSelection() { if (getDialogType() != SAVE_DIALOG) { super.approveSelection(); return; } selectedFile = getSelectedFile(); if (selectedFile == null) { // Workaround for Java 9,10 on OSX - no selected file, but there is a // filename typed in try { String filename = ((BasicFileChooserUI) getUI()).getFileName(); if (filename != null && filename.length() > 0) { selectedFile = new File(getCurrentDirectory(), filename); } } catch (Throwable x) { jalview.bin.Console.errPrintln( "Unexpected exception when trying to get filename."); x.printStackTrace(); } // TODO: ENSURE THAT FILES SAVED WITH A ':' IN THE NAME ARE REFUSED AND // THE // USER PROMPTED FOR A NEW FILENAME } if (selectedFile == null) { return; } if (getFileFilter() instanceof JalviewFileFilter) { JalviewFileFilter jvf = (JalviewFileFilter) getFileFilter(); if (!jvf.accept(selectedFile)) { String withExtension = getSelectedFile().getName() + "." + jvf.getAcceptableExtension(); selectedFile = (new File(getCurrentDirectory(), withExtension)); setSelectedFile(selectedFile); } } if (selectedFile.exists()) { int confirm = Cache.getDefault("CONFIRM_OVERWRITE_FILE", true) ? JvOptionPane.showConfirmDialog(this, MessageManager .getString("label.overwrite_existing_file"), MessageManager.getString("label.file_already_exists"), JvOptionPane.YES_NO_OPTION) : JOptionPane.YES_OPTION; if (confirm != JvOptionPane.YES_OPTION) { return; } } super.approveSelection(); } void recentListSelectionChanged(Object selection) { setSelectedFile(null); if (selection != null) { File file = new File((String) selection); if (getFileFilter() instanceof JalviewFileFilter) { JalviewFileFilter jvf = (JalviewFileFilter) this.getFileFilter(); if (!jvf.accept(file)) { setFileFilter(getChoosableFileFilters()[0]); } } if (!file.isAbsolute() && file.exists()) { file = file.getAbsoluteFile(); } setSelectedFile(file); } } class RecentlyOpened extends JPanel { private static final long serialVersionUID = 1L; JList list; RecentlyOpened() { setPreferredSize(new Dimension(300, 100)); String historyItems = Cache.getProperty("RECENT_FILE"); StringTokenizer st; Vector recent = new Vector<>(); if (historyItems != null) { st = new StringTokenizer(historyItems, "\t"); while (st.hasMoreTokens()) { recent.addElement(st.nextToken()); } } list = new JList<>(recent); list.setCellRenderer(new recentlyOpenedCellRenderer()); list.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent evt) { recentListSelectionChanged(list.getSelectedValue()); } }); TitledBorder recentlyOpenedBorder = new TitledBorder( MessageManager.getString("label.recently_opened")); recentlyOpenedBorder.setTitleFont( recentlyOpenedBorder.getTitleFont().deriveFont(10f)); this.setBorder(recentlyOpenedBorder); final JScrollPane scroller = new JScrollPane(list); SpringLayout layout = new SpringLayout(); layout.putConstraint(SpringLayout.WEST, scroller, 5, SpringLayout.WEST, this); layout.putConstraint(SpringLayout.NORTH, scroller, 5, SpringLayout.NORTH, this); // one size okay for all scroller.setPreferredSize(new Dimension(280, 105)); this.add(scroller); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { scroller.getHorizontalScrollBar() .setValue(scroller.getHorizontalScrollBar().getMaximum()); } }); } } class recentlyOpenedCellRenderer extends JLabel implements ListCellRenderer { private final static int maxChars = 46; private final static String ellipsis = "..."; @Override public Component getListCellRendererComponent( JList list, String value, int index, boolean isSelected, boolean cellHasFocus) { String filename = value.toString(); String displayFilename; if (filename.length() > maxChars) { StringBuilder displayFileSB = new StringBuilder(); File file = new File(filename); displayFileSB.append(file.getName()); if (file.getParent() != null) { File parent = file; boolean spaceleft = true; while (spaceleft && parent.getParent() != null) { parent = parent.getParentFile(); String name = parent.getName(); displayFileSB.insert(0, File.separator); if (displayFileSB.length() + name.length() < maxChars - 1) { displayFileSB.insert(0, name); } else { displayFileSB.insert(0, ellipsis); spaceleft = false; } } if (spaceleft && filename.startsWith(File.separator) && !(displayFileSB.charAt(0) == File.separatorChar)) { displayFileSB.insert(0, File.separator); } } displayFilename = displayFileSB.toString(); } else { displayFilename = filename; } this.setText(displayFilename.toString()); this.setToolTipText(filename); if (isSelected) { setBackground(list.getSelectionBackground()); setForeground(list.getSelectionForeground()); } else { setBackground(list.getBackground()); setForeground(list.getForeground()); } this.setHorizontalAlignment(SwingConstants.TRAILING); this.setEnabled(list.isEnabled()); this.setFont(list.getFont().deriveFont(12f)); this.setOpaque(true); return this; } } /* @Override public JalviewFileChooser setResponseHandler(Object response, Runnable action) { callbacks.put(response, new Callable() { @Override public Void call() { action.run(); return null; } }); return this; } */ @Override public DialogRunnerI setResponseHandler(Object response, Runnable action) { callbacks.put(response, action); return this; } @Override public void handleResponse(Object response) { /* * this test is for NaN in Chrome */ if (response != null && !response.equals(response)) { return; } Runnable action = callbacks.get(response); if (action != null) { try { action.run(); } catch (Exception e) { e.printStackTrace(); } } } /** * JalviewJS signals file selection by a property change event for property * "SelectedFile". This methods responds to that by running the response * action for 'OK' in the dialog. * * @param evt */ @Override public void propertyChange(PropertyChangeEvent evt) { // TODO other properties need runners... switch (evt.getPropertyName()) { /* * property name here matches that used in JFileChooser.js */ case "SelectedFile": handleResponse(APPROVE_OPTION); break; } } @Override protected JDialog createDialog(Component parent) throws HeadlessException { JDialog dialog = super.createDialog(parent); dialog.setIconImages(ChannelProperties.getIconList()); return dialog; } }