diff --git a/pom.xml b/pom.xml index 27b4f6a..cb2f1fe 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.scijava pom-scijava - 12.0.0 + 17.0.0 @@ -59,6 +59,11 @@ http://imagej.net/User:Saalfeld axtimwalde + + Jan Eglinger + http://imagej.net/User:Eglinger + imagejan + @@ -92,6 +97,7 @@ Board of Regents of the University of Wisconsin-Madison. SciJava UI components for Java Swing. + 2.66.0 diff --git a/src/main/java/org/scijava/ui/swing/AbstractSwingUI.java b/src/main/java/org/scijava/ui/swing/AbstractSwingUI.java index be58659..d551b37 100644 --- a/src/main/java/org/scijava/ui/swing/AbstractSwingUI.java +++ b/src/main/java/org/scijava/ui/swing/AbstractSwingUI.java @@ -37,6 +37,7 @@ import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; +import java.io.FileFilter; import java.lang.reflect.InvocationTargetException; import javax.swing.JFileChooser; @@ -67,6 +68,7 @@ import org.scijava.ui.swing.menu.SwingJMenuBarCreator; import org.scijava.ui.swing.menu.SwingJPopupMenuCreator; import org.scijava.ui.viewer.DisplayViewer; +import org.scijava.widget.FileListWidget; import org.scijava.widget.FileWidget; /** @@ -161,6 +163,56 @@ public File chooseFile(final File file, final String style) { return result[0]; } + @Override + public File[] chooseFiles(final File parent, final File[] files, final FileFilter filter, final String style) { + final File[][] result = new File[1][]; + try { + // NB: We show the JFileChooser on the EDT because otherwise there could + // be a deadlock, particularly on macOS. + // See the {@link #chooseFile(File, String) chooseFile} method. + threadService.invoke(() -> { + final JFileChooser chooser = new JFileChooser(parent); + chooser.setMultiSelectionEnabled(true); + if (style.equals(FileListWidget.FILES_AND_DIRECTORIES)) { + chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); + } + else if (style.equals(FileListWidget.DIRECTORIES_ONLY)) { + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + } + else { + chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); + } + chooser.setSelectedFiles(files); + if (filter != null) { + javax.swing.filechooser.FileFilter fileFilter = new javax.swing.filechooser.FileFilter() { + + @Override + public String getDescription() { + return filter.toString(); + } + + @Override + public boolean accept(File f) { + if (filter.accept(f)) return true; + // directories should always be displayed + // independent from selection mode + return f.isDirectory(); + } + }; + chooser.setFileFilter(fileFilter); + } + int rval = chooser.showOpenDialog(appFrame); + if (rval == JFileChooser.APPROVE_OPTION) { + result[0] = chooser.getSelectedFiles(); + } + }); + } + catch (final InvocationTargetException | InterruptedException exc) { + log.error(exc); + } + return result[0]; + } + @Override public void showContextMenu(final String menuRoot, final Display display, final int x, final int y) diff --git a/src/main/java/org/scijava/ui/swing/widget/SwingFileListWidget.java b/src/main/java/org/scijava/ui/swing/widget/SwingFileListWidget.java new file mode 100644 index 0000000..6514b0c --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/widget/SwingFileListWidget.java @@ -0,0 +1,278 @@ +/* + * #%L + * SciJava Common shared library for SciJava software. + * %% + * Copyright (C) 2009 - 2017 Board of Regents of the University of + * Wisconsin-Madison, Broad Institute of MIT and Harvard, Max Planck + * Institute of Molecular Cell Biology and Genetics, University of + * Konstanz, and KNIME GmbH. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.ui.swing.widget; + +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.io.File; +import java.io.FileFilter; +import java.util.Collections; +import java.util.List; + +import javax.swing.Box; +import javax.swing.DefaultListModel; +import javax.swing.JButton; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.TransferHandler; + +import net.miginfocom.swing.MigLayout; + +import org.scijava.log.LogService; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; +import org.scijava.ui.UIService; +import org.scijava.widget.FileListWidget; +import org.scijava.widget.InputWidget; +import org.scijava.widget.WidgetModel; + +/** + * Swing implementation of {@link FileListWidget}. + * + * @author Jan Eglinger + */ +@Plugin(type = InputWidget.class) +public class SwingFileListWidget extends SwingInputWidget implements + FileListWidget, ActionListener, MouseListener { + + @Parameter + private UIService uiService; + + @Parameter + private LogService logService; + + private JList paths; + private JButton addFilesButton; + private JButton removeFilesButton; + private JButton clearButton; + + @Override + public File[] getValue() { + DefaultListModel listModel = // + (DefaultListModel) paths.getModel(); + List fileList = Collections.list(listModel.elements()); + return fileList.toArray(new File[fileList.size()]); + } + + @Override + public void set(final WidgetModel model) { + super.set(model); + paths = new JList<>(new DefaultListModel<>()); + //paths.setMinimumSize(new Dimension(150, 50)); + //paths.setMaximumSize(new Dimension(150, 50)); + paths.setDragEnabled(true); + final String style = model.getItem().getWidgetStyle(); + paths.setTransferHandler(new FileListTransferHandler(style)); + JScrollPane scrollPane = new JScrollPane(paths); + scrollPane.setPreferredSize(new Dimension(350, 100)); + paths.addMouseListener(this); + // scrollPane.addMouseListener(l); + + setToolTip(scrollPane); + getComponent().add(scrollPane); + + getComponent().add(Box.createHorizontalStrut(3)); + + // GroupLayout buttonLayout = new GroupLayout(getComponent()); + JPanel buttonPanel = new JPanel(new MigLayout()); + + addFilesButton = new JButton("Add files..."); + setToolTip(addFilesButton); + buttonPanel.add(addFilesButton, "wrap, grow"); + addFilesButton.addActionListener(this); + + removeFilesButton = new JButton("Remove selected"); + setToolTip(removeFilesButton); + buttonPanel.add(removeFilesButton, "wrap, grow"); + removeFilesButton.addActionListener(this); + + clearButton = new JButton("Clear list"); + setToolTip(clearButton); + buttonPanel.add(clearButton, "grow"); + clearButton.addActionListener(this); + + getComponent().add(buttonPanel); + + /* + getComponent().setLayout(buttonLayout); + buttonLayout.setVerticalGroup(buttonLayout.createSequentialGroup() + .addComponent(addFilesButton) + .addComponent(removeFilesButton) + .addComponent(clearButton)); + */ + + refreshWidget(); + } + + // -- Typed methods -- + + @Override + public boolean supports(final WidgetModel model) { + return super.supports(model) && model.isType(File[].class); + } + + // -- ActionListener methods -- + + @Override + public void actionPerformed(final ActionEvent e) { + DefaultListModel listModel = // + (DefaultListModel) paths.getModel(); + List fileList = Collections.list(listModel.elements()); + + if (e.getSource() == addFilesButton) { + // Add new files + // parse style attribute to allow choosing + // files and/or directories, and filter files + final WidgetModel widgetModel = get(); + final String widgetStyle = widgetModel.getItem().getWidgetStyle(); + FileFilter filter = SwingFileWidget.createFileFilter(widgetStyle); + + String style; + if (widgetModel.isStyle(FileListWidget.FILES_AND_DIRECTORIES)) { + style = FileListWidget.FILES_AND_DIRECTORIES; + } else if (widgetModel.isStyle(FileListWidget.DIRECTORIES_ONLY)) { + style = FileListWidget.DIRECTORIES_ONLY; + } else { + style = FileListWidget.FILES_ONLY; // default + } + + fileList = uiService.chooseFiles(null, fileList, filter, style); + if (fileList == null) + return; + for (File file : fileList) { + listModel.addElement(file); + } + } else if (e.getSource() == removeFilesButton) { + // Remove selected files + List selected = paths.getSelectedValuesList(); + for (File f : selected) { + listModel.removeElement(f); + } + } else if (e.getSource() == clearButton) { + // Clear the file selection + listModel.removeAllElements(); + } + paths.setModel(listModel); + updateModel(); + } + + // -- AbstractUIInputWidget methods --- + + @Override + protected void doRefresh() { + File[] files = (File[]) get().getValue(); + DefaultListModel listModel = new DefaultListModel<>(); + if (files != null) { + for (File file : files) { + listModel.addElement(file); + } + } + paths.setModel(listModel); + } + + @Override + public void mouseClicked(MouseEvent e) { + // handle double click + if (e.getClickCount() == 2) { + DefaultListModel listModel = // + (DefaultListModel) paths.getModel(); + // Remove selected files + List selected = paths.getSelectedValuesList(); + for (File f : selected) { + listModel.removeElement(f); + } + paths.setModel(listModel); + updateModel(); + } + } + + @Override + public void mousePressed(MouseEvent e) { + // Nothing to do + } + + @Override + public void mouseReleased(MouseEvent e) { + // Nothing to do + } + + @Override + public void mouseEntered(MouseEvent e) { + // Nothing to do + } + + @Override + public void mouseExited(MouseEvent e) { + // Nothing to do + } + + // -- Helper classes -- + + private class FileListTransferHandler extends TransferHandler { + + private final String style; + + public FileListTransferHandler(final String style) { + this.style = style; + } + + @Override + public boolean canImport(final TransferHandler.TransferSupport support) { + return SwingFileWidget.hasFiles(support); + } + + @SuppressWarnings("unchecked") + @Override + public boolean importData(final TransferHandler.TransferSupport support) { + final List allFiles = SwingFileWidget.getFiles(support); + if (allFiles == null) return false; + final FileFilter filter = SwingFileWidget.createFileFilter(style); + final List files = SwingFileWidget.filterFiles(allFiles, filter); + if (allFiles.size() != files.size()) { + logService.warn("Some files were excluded " + + "for not matching the input requirements (" + style + ")"); + } + final JList jlist = (JList) support.getComponent(); + final DefaultListModel model = (DefaultListModel) // + jlist.getModel(); + files.forEach(f -> model.addElement(f)); + jlist.setModel(model); + updateModel(); + return true; + } + } +} diff --git a/src/main/java/org/scijava/ui/swing/widget/SwingFileWidget.java b/src/main/java/org/scijava/ui/swing/widget/SwingFileWidget.java index d2cb44e..9102b4e 100644 --- a/src/main/java/org/scijava/ui/swing/widget/SwingFileWidget.java +++ b/src/main/java/org/scijava/ui/swing/widget/SwingFileWidget.java @@ -30,20 +30,30 @@ package org.scijava.ui.swing.widget; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; import javax.swing.Box; import javax.swing.JButton; import javax.swing.JPanel; import javax.swing.JTextField; +import javax.swing.TransferHandler; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import org.scijava.plugin.Parameter; import org.scijava.plugin.Plugin; import org.scijava.ui.UIService; +import org.scijava.widget.FileListWidget; import org.scijava.widget.FileWidget; import org.scijava.widget.InputWidget; import org.scijava.widget.WidgetModel; @@ -79,6 +89,9 @@ public void set(final WidgetModel model) { super.set(model); path = new JTextField(16); + path.setDragEnabled(true); + final String style = model.getItem().getWidgetStyle(); + path.setTransferHandler(new FileTransferHandler(style)); setToolTip(path); getComponent().add(path); path.getDocument().addDocumentListener(this); @@ -152,4 +165,171 @@ public void doRefresh() { if (text.equals(path.getText())) return; // no change path.setText(text); } + + // -- Utility methods -- + + /** + * Creates a {@link FileFilter} that filters files and/or directories + * according to the given widget style. + *

+ * It supports filtering files by extension as specified by the syntax + * {@code extensions:ext1/ext2} where {@code ext1}, {@code ext2}, etc., are + * extensions to accept. It also filters files and/or directories as specified + * by the following styles: + *

+ *
    + *
  • {@link FileWidget#OPEN_STYLE}
  • + *
  • {@link FileWidget#SAVE_STYLE}
  • + *
  • {@link FileListWidget#FILES_ONLY}
  • + *
  • {@link FileWidget#DIRECTORY_STYLE}
  • + *
  • {@link FileListWidget#DIRECTORIES_ONLY}
  • + *
  • {@link FileListWidget#FILES_AND_DIRECTORIES}
  • + *
+ * + * @param widgetStyle The style defining which files get accepted by the + * filter. + * @return A {@link FileFilter} that accepts files matching the given widget + * style. + */ + public static FileFilter createFileFilter(final String widgetStyle) { + final List filesOnlyStyles = Arrays.asList( + FileWidget.OPEN_STYLE, FileWidget.SAVE_STYLE, FileListWidget.FILES_ONLY + ); + final List dirsOnlyStyles = Arrays.asList( + FileWidget.DIRECTORY_STYLE, FileListWidget.DIRECTORIES_ONLY + ); + final List filesAndDirsStyles = Arrays.asList( + FileListWidget.FILES_AND_DIRECTORIES + ); + + final List exts = new ArrayList<>(); + boolean filesOnly = false, dirsOnly = false, filesAndDirs = false; + if (widgetStyle != null) { + // Extract extensions to be accepted. + for (final String token : widgetStyle.split(",")) { + if (filesOnlyStyles.contains(token)) filesOnly = true; + if (dirsOnlyStyles.contains(token)) dirsOnly = true; + if (filesAndDirsStyles.contains(token)) filesAndDirs = true; + if (token.startsWith("extensions")) { + String extensions = token.split(":")[1]; + for (final String ext : extensions.split("/")) + exts.add(ext); + } + } + } + // NB: If none of the styles was set, we do the default behavior. + final boolean defaultBehavior = !(filesOnly || dirsOnly || filesAndDirs); + + final boolean rejectFiles = dirsOnly; + // NB: We reject directories by default, if no styles are given. + final boolean rejectDirs = filesOnly || defaultBehavior; + return new FileFilter() { + @Override + public boolean accept(final File pathname) { + if (pathname.isFile() && rejectFiles) return false; + if (pathname.isDirectory() && rejectDirs) return false; + if (exts.isEmpty()) return true; + if (pathname.isDirectory()) return true; // Don't filter dirs by ext. + for (final String ext : exts) { + if (pathname.getName().endsWith("." + ext)) return true; + } + return false; + } + }; + } + + /** + * Checks whether the given drag and drop operation offers a list of files as + * one of its flavors. + * + * @param support The drag and drop operation that should be checked. + * @return True iff the operation can provide a list of files. + */ + public static boolean hasFiles( + final TransferHandler.TransferSupport support) + { + // Check the flavors to make sure one of them is file list. + for (final DataFlavor flavor : support.getDataFlavors()) { + if (flavor.isFlavorJavaFileListType()) return true; + } + return false; + } + + /** + * Gets the list of files associated with the given drag and drop operation. + * + * @param support The drag and drop operation from which files should be + * extracted. + * @return The list of files, or null if something goes wrong: operation does + * not provide a list; the list contains something other than + * {@link File} objects; or an exception is thrown. + */ + public static List getFiles( + final TransferHandler.TransferSupport support) + { + try { + final Object files = support.getTransferable().getTransferData( + DataFlavor.javaFileListFlavor); + + // NB: Be absolutely sure the files object is a List! + if (!(files instanceof List)) return null; + @SuppressWarnings("rawtypes") + final List list = (List) files; + for (int i=0; i listOfFiles = (List) list; + return listOfFiles; + } + catch (final UnsupportedFlavorException | IOException exc) { + return null; + } + } + + /** + * Filters the given list of files according to the specified + * {@link FileFilter}. + * + * @param list The list of files to filter. + * @param filter The filter to use. + * @return A newly created list including only the matching files. + */ + public static List filterFiles(final List list, + final FileFilter filter) + { + return list.stream().filter(filter::accept).collect(Collectors.toList()); + } + + // -- Helper classes -- + + private class FileTransferHandler extends TransferHandler { + + private final String style; + + public FileTransferHandler(final String style) { + this.style = style; + } + + @Override + public boolean canImport(final TransferHandler.TransferSupport support) { + if (!hasFiles(support)) return false; + final List allFiles = getFiles(support); + if (allFiles == null || allFiles.size() != 1) return false; + + final FileFilter filter = SwingFileWidget.createFileFilter(style); + final List files = SwingFileWidget.filterFiles(allFiles, filter); + return files.size() == 1; + } + + @Override + public boolean importData(TransferHandler.TransferSupport support) { + final List files = getFiles(support); + if (files == null || files.size() != 1) return false; + + final File file = files.get(0); + ((JTextField) support.getComponent()).setText(file.getAbsolutePath()); + return true; + } + } }