implements
@Override
public void actionPerformed(final ActionEvent e) {
- final Color choice = showColorDialog(choose, "Select a color", color);
+ final Color choice = showColorDialog(choose, "Select a Color", color);
if (choice == null) return;
color = choice;
updateModel();
@@ -141,7 +141,7 @@ public boolean supports(final WidgetModel model) {
public static Color showColorDialog(final Component component,
final String title, final Color initialColor) throws HeadlessException
{
- final JColorChooser pane = createColorChooser(initialColor);
+ final JColorChooser pane = getSharedColorChooser(initialColor);
class ColorTracker implements ActionListener {
@@ -167,12 +167,34 @@ public Color getColor() {
JColorChooser.createDialog(component, title, true, pane, ok, null);
dialog.setVisible(true);
+ dialog.dispose();
return ok.getColor();
}
// -- Helper methods --
+ private static JColorChooser sharedChooser;
+ private static String sharedChooserLaf;
+
+ /**
+ * Returns a chooser shared by all color widgets, so that state such as the
+ * recent colors of the swatches panel survives between dialogs. It is
+ * rebuilt if the look and feel changed, since a chooser that is not shown
+ * does not get updated and would keep the old UI.
+ */
+ private static synchronized JColorChooser getSharedColorChooser(
+ final Color initialColor)
+ {
+ final String laf = UIManager.getLookAndFeel().getClass().getName();
+ if (sharedChooser == null || !laf.equals(sharedChooserLaf)) {
+ sharedChooser = createColorChooser(initialColor);
+ sharedChooserLaf = laf;
+ }
+ sharedChooser.setColor(initialColor != null ? initialColor : Color.white);
+ return sharedChooser;
+ }
+
/**
* Creates a new {@link JColorChooser} with panels in a desirable order.
*
diff --git a/src/main/java/org/scijava/ui/swing/widget/SwingDateWidget.java b/src/main/java/org/scijava/ui/swing/widget/SwingDateWidget.java
index e55f529..aa344b5 100644
--- a/src/main/java/org/scijava/ui/swing/widget/SwingDateWidget.java
+++ b/src/main/java/org/scijava/ui/swing/widget/SwingDateWidget.java
@@ -29,15 +29,18 @@
package org.scijava.ui.swing.widget;
+import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Date;
import javax.swing.JPanel;
+import javax.swing.UIManager;
-import net.sourceforge.jdatepicker.impl.JDatePanelImpl;
-import net.sourceforge.jdatepicker.impl.JDatePickerImpl;
-import net.sourceforge.jdatepicker.impl.UtilDateModel;
+import org.jdatepicker.ComponentColorDefaults;
+import org.jdatepicker.ComponentColorDefaults.Key;
+import org.jdatepicker.JDatePicker;
+import org.jdatepicker.UtilDateModel;
import org.scijava.log.LogService;
import org.scijava.plugin.Parameter;
@@ -75,8 +78,9 @@ public void set(final WidgetModel model) {
super.set(model);
dateModel = new UtilDateModel();
- final JDatePickerImpl datePicker =
- new JDatePickerImpl(new JDatePanelImpl(dateModel), null);
+ applyThemeColors();
+ // NB: the picker must be built on our model, or user picks never reach it
+ final JDatePicker datePicker = new JDatePicker(dateModel);
setToolTip(datePicker);
getComponent().add(datePicker);
datePicker.addActionListener(this);
@@ -84,6 +88,39 @@ public void set(final WidgetModel model) {
refreshWidget();
}
+ // -- Helper methods --
+
+ /**
+ * Maps the picker colors to the current look and feel. Note that
+ * {@link ComponentColorDefaults} is a global singleton read when a picker
+ * is created, so existing pickers keep their colors on a theme change
+ */
+ private static void applyThemeColors() {
+ setColor(Key.BG_MONTH_SELECTOR, "TableHeader.background");
+ setColor(Key.FG_MONTH_SELECTOR, "TableHeader.foreground");
+ setColor(Key.BG_GRID_HEADER, "TableHeader.background");
+ setColor(Key.FG_GRID_HEADER, "TableHeader.foreground");
+ setColor(Key.BG_GRID, "Table.background");
+ setColor(Key.BG_GRID_NOT_SELECTABLE, "Panel.background");
+ setColor(Key.FG_GRID_THIS_MONTH, "Table.foreground");
+ setColor(Key.FG_GRID_OTHER_MONTH, "Label.disabledForeground");
+ setColor(Key.FG_GRID_TODAY, "Component.accentColor");
+ setColor(Key.FG_GRID_SELECTED, "Table.selectionForeground");
+ setColor(Key.BG_GRID_SELECTED, "Table.selectionBackground");
+ setColor(Key.FG_GRID_TODAY_SELECTED, "Table.selectionForeground");
+ setColor(Key.BG_GRID_TODAY_SELECTED, "Table.selectionBackground");
+ setColor(Key.BG_TODAY_SELECTOR, "Panel.background");
+ setColor(Key.FG_TODAY_SELECTOR_ENABLED, "Label.foreground");
+ setColor(Key.FG_TODAY_SELECTOR_DISABLED, "Label.disabledForeground");
+ setColor(Key.POPUP_BORDER, "Component.borderColor");
+ }
+
+ /** Sets a picker color from a UIManager key, if the key is defined */
+ private static void setColor(final Key key, final String uiKey) {
+ final Color color = UIManager.getColor(uiKey);
+ if (color != null) ComponentColorDefaults.getInstance().setColor(key, color);
+ }
+
// -- Typed methods --
@Override
diff --git a/src/main/java/org/scijava/ui/swing/widget/SwingFileListWidget.java b/src/main/java/org/scijava/ui/swing/widget/SwingFileListWidget.java
index 2a2b1ce..8aa9dde 100644
--- a/src/main/java/org/scijava/ui/swing/widget/SwingFileListWidget.java
+++ b/src/main/java/org/scijava/ui/swing/widget/SwingFileListWidget.java
@@ -29,7 +29,9 @@
package org.scijava.ui.swing.widget;
+import java.awt.Cursor;
import java.awt.Dimension;
+import java.awt.datatransfer.Clipboard;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetAdapter;
@@ -38,33 +40,43 @@
import java.awt.dnd.DropTargetEvent;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
+import java.io.UncheckedIOException;
import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import java.util.TooManyListenersException;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.swing.AbstractAction;
import javax.swing.Box;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
+import javax.swing.JComponent;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
+import javax.swing.KeyStroke;
+import javax.swing.SwingWorker;
import javax.swing.TransferHandler;
import net.miginfocom.swing.MigLayout;
import org.scijava.log.LogService;
-import org.scijava.module.ModuleService;
import org.scijava.plugin.Parameter;
import org.scijava.plugin.Plugin;
-import org.scijava.thread.ThreadService;
import org.scijava.widget.FileListWidget;
import org.scijava.widget.FileWidget;
import org.scijava.widget.InputWidget;
@@ -82,11 +94,7 @@ public class SwingFileListWidget extends SwingInputWidget implements
@Parameter
private LogService logService;
- @Parameter
- private ModuleService moduleService;
-
- @Parameter
- private ThreadService threadService;
+ private boolean loadingFolder;
private JList paths;
private JButton addFilesButton;
@@ -99,33 +107,33 @@ public File[] getValue() {
DefaultListModel listModel = //
(DefaultListModel) paths.getModel();
List fileList = Collections.list(listModel.elements());
- return fileList.toArray(new File[fileList.size()]);
+ return fileList.toArray(new File[0]);
}
@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));
+ paths.setTransferHandler(new FileListTransferHandler(style, paths
+ .getTransferHandler()));
JScrollPane scrollPane = new JScrollPane(paths);
scrollPane.setPreferredSize(new Dimension(350, 100));
paths.addMouseListener(this);
// scrollPane.addMouseListener(l);
setToolTip(scrollPane);
- getComponent().add(scrollPane);
+ getComponent().add(scrollPane, "grow");
getComponent().add(Box.createHorizontalStrut(3));
- JPanel buttonPanel = new JPanel(new MigLayout());
+ // no insets and a 3 px vertical gap, to match the other file widgets
+ JPanel buttonPanel = new JPanel(new MigLayout("ins 0, gapy 3"));
// Set button label dependent on widget style
- String filesLabel = model.isStyle(DIRECTORIES_ONLY) ? "Add folders..."
- : "Add files...";
+ String filesLabel = model.isStyle(DIRECTORIES_ONLY) ? "Add Folders..."
+ : "Add Files...";
addFilesButton = new JButton(filesLabel);
setToolTip(addFilesButton);
buttonPanel.add(addFilesButton, "wrap, grow");
@@ -133,8 +141,10 @@ public void set(final WidgetModel model) {
// Only show folder button if style allows files
if (!model.isStyle(DIRECTORIES_ONLY)) {
- addFolderButton = new JButton("Add folder content...");
- setToolTip(addFolderButton);
+ addFolderButton = new JButton("Add Folder...");
+ // set directly: setToolTip() would replace it with the description
+ addFolderButton.setToolTipText("Adds all matching files from the " +
+ "chosen folder
and its subfolders (recursive)");
// add TransferHandler to accept dropped folders
addFolderButton.setTransferHandler(new FolderTransferHandler());
@@ -149,12 +159,33 @@ public void set(final WidgetModel model) {
addFolderButton.addActionListener(this);
}
- removeFilesButton = new JButton("Remove selected");
- setToolTip(removeFilesButton);
+ removeFilesButton = new JButton("Remove");
+ // set directly: setToolTip() would replace it with the description
+ removeFilesButton.setToolTipText("Removes the selected entries " +
+ "from the list
(Delete or double-clicking an entry also removes it)");
+ removeFilesButton.setEnabled(false);
+ paths.addListSelectionListener(e -> {
+ if (!e.getValueIsAdjusting())
+ removeFilesButton.setEnabled(!paths.isSelectionEmpty());
+ });
+ // Delete (or Backspace, e.g. the macOS delete key) removes the selection
+ final String removeKey = "removeSelected";
+ for (final int key : new int[] { KeyEvent.VK_DELETE,
+ KeyEvent.VK_BACK_SPACE })
+ {
+ paths.getInputMap().put(KeyStroke.getKeyStroke(key, 0), removeKey);
+ }
+ paths.getActionMap().put(removeKey, new AbstractAction() {
+
+ @Override
+ public void actionPerformed(final ActionEvent e) {
+ removeSelected();
+ }
+ });
buttonPanel.add(removeFilesButton, "wrap, grow");
removeFilesButton.addActionListener(this);
- clearButton = new JButton("Clear list");
+ clearButton = new JButton("Clear");
setToolTip(clearButton);
buttonPanel.add(clearButton, "grow");
clearButton.addActionListener(this);
@@ -196,27 +227,27 @@ public void actionPerformed(final ActionEvent e) {
style = FileListWidget.FILES_ONLY; // default
}
- fileList = ui().chooseFiles(null, fileList, filter, style);
+ fileList = chooseFiles(null, fileList, filter, style);
if (fileList == null)
return;
- fileList.forEach(file -> listModel.addElement(file));
+ addUnique(fileList);
+ updateModel();
+ return;
} else if (e.getSource() == addFolderButton) {
- File folder = ui().chooseFile(null, FileWidget.DIRECTORY_STYLE);
+ File folder = chooseFile(null, FileWidget.DIRECTORY_STYLE);
if (folder == null)
return;
- List fileList = getFilesFromFolder(folder);
- fileList.forEach(file -> listModel.addElement(file));
+ addFolderContent(folder);
+ return;
} else if (e.getSource() == removeFilesButton) {
- // Remove selected files
- List selected = paths.getSelectedValuesList();
- for (File f : selected) {
- listModel.removeElement(f);
- }
+ removeSelected();
+ return;
} else if (e.getSource() == clearButton) {
// Clear the file selection
listModel.removeAllElements();
}
paths.setModel(listModel);
+ updateButtons();
updateModel();
}
@@ -232,21 +263,14 @@ protected void doRefresh() {
}
}
paths.setModel(listModel);
+ updateButtons();
}
@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();
+ removeSelected();
}
}
@@ -272,23 +296,89 @@ public void mouseExited(MouseEvent e) {
// -- Helper methods --
- private List getFilesFromFolder(File inputFolder) {
- // get files in folder and add to listModel
- List fileList = new ArrayList<>();
- final WidgetModel widgetModel = get();
- final String widgetStyle = widgetModel.getItem().getWidgetStyle();
- FileFilter filter = SwingFileWidget.createFileFilter(widgetStyle);
- try {
- fileList = Arrays
- .asList((Files.walk(inputFolder.toPath())
- .filter(path -> filter.accept(path.toFile())))
- .map(path -> path.toFile())
- .toArray(File[]::new));
- } catch (IOException exc) {
- logService
- .error("Error when trying to retrieve file list", exc);
+ /** Removes the selected entries from the list */
+ private void removeSelected() {
+ final DefaultListModel listModel = //
+ (DefaultListModel) paths.getModel();
+ for (final File f : paths.getSelectedValuesList()) {
+ listModel.removeElement(f);
+ }
+ paths.setModel(listModel);
+ updateButtons();
+ updateModel();
+ }
+
+ /** Adds the given files to the list, skipping those already listed */
+ private void addUnique(final List files) {
+ final DefaultListModel current = //
+ (DefaultListModel) paths.getModel();
+ // Replace the model at once, instead of firing an event per file
+ final DefaultListModel merged = new DefaultListModel<>();
+ final Set seen = new HashSet<>();
+ Collections.list(current.elements()).forEach(f -> {
+ merged.addElement(f);
+ seen.add(f);
+ });
+ files.stream().filter(seen::add).forEach(merged::addElement);
+ paths.setModel(merged);
+ updateButtons();
+ }
+
+ /** Enables the buttons that only make sense for a non-empty list */
+ private void updateButtons() {
+ clearButton.setEnabled(paths.getModel().getSize() > 0);
+ }
+
+ /**
+ * Adds the files of a folder (and its subfolders) that match the widget
+ * style. The folder is scanned in the background, so the UI stays responsive
+ */
+ private void addFolderContent(final File folder) {
+ if (loadingFolder) return;
+ loadingFolder = true;
+ final FileFilter filter = SwingFileWidget.createFileFilter(get().getItem()
+ .getWidgetStyle());
+ getComponent().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+ if (addFolderButton != null) addFolderButton.setEnabled(false);
+ new SwingWorker, Void>() {
+
+ @Override
+ protected List doInBackground() {
+ return getFilesFromFolder(folder, filter);
+ }
+
+ @Override
+ protected void done() {
+ try {
+ addUnique(get());
+ updateModel();
+ }
+ catch (final InterruptedException | ExecutionException exc) {
+ logService.error("Error when trying to retrieve file list", exc);
+ }
+ finally {
+ loadingFolder = false;
+ getComponent().setCursor(Cursor.getDefaultCursor());
+ if (addFolderButton != null) addFolderButton.setEnabled(true);
+ }
+ }
+ }.execute();
+ }
+
+ /** Walks the folder tree. Must not be called on the EDT */
+ private List getFilesFromFolder(final File inputFolder,
+ final FileFilter filter)
+ {
+ try (final Stream walk = Files.walk(
+ inputFolder.toPath()))
+ {
+ return walk.map(Path::toFile).filter(filter::accept)
+ .collect(Collectors.toList());
+ }
+ catch (IOException | UncheckedIOException exc) {
+ logService.error("Error when trying to retrieve file list", exc);
+ return new ArrayList<>();
}
- return fileList;
}
// -- Helper classes --
@@ -296,9 +386,33 @@ private List getFilesFromFolder(File inputFolder) {
private class FileListTransferHandler extends TransferHandler {
private final String style;
+ private final TransferHandler fallback;
- public FileListTransferHandler(final String style) {
+ public FileListTransferHandler(final String style,
+ final TransferHandler fallback)
+ {
this.style = style;
+ this.fallback = fallback;
+ }
+
+ // Keep copy and drag out of the list working (default handler)
+ @Override
+ public int getSourceActions(final JComponent c) {
+ return fallback == null ? NONE : fallback.getSourceActions(c);
+ }
+
+ @Override
+ public void exportToClipboard(final JComponent comp,
+ final Clipboard clip, final int action) throws IllegalStateException
+ {
+ if (fallback != null) fallback.exportToClipboard(comp, clip, action);
+ }
+
+ @Override
+ public void exportAsDrag(final JComponent comp, final InputEvent e,
+ final int action)
+ {
+ if (fallback != null) fallback.exportAsDrag(comp, e, action);
}
@Override
@@ -306,7 +420,6 @@ 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);
@@ -317,11 +430,7 @@ public boolean importData(final TransferHandler.TransferSupport support) {
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);
+ addUnique(files);
updateModel();
return true;
}
@@ -345,20 +454,13 @@ public boolean importData(final TransferHandler.TransferSupport support) {
if (!folder.isDirectory())
return false;
- // get all files matching filter from folder and subfolders
- List fileList = getFilesFromFolder(folder);
-
- // add files to model
- DefaultListModel model = //
- (DefaultListModel) paths.getModel();
- fileList.forEach(file -> model.addElement(file));
- paths.setModel(model);
- updateModel();
+ // add all files matching filter from folder and subfolders
+ addFolderContent(folder);
return true;
}
}
- private class ButtonDropTargetListener extends DropTargetAdapter {
+ private static class ButtonDropTargetListener extends DropTargetAdapter {
@Override
public void drop(DropTargetDropEvent dtde) {
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 8080118..04ba064 100644
--- a/src/main/java/org/scijava/ui/swing/widget/SwingFileWidget.java
+++ b/src/main/java/org/scijava/ui/swing/widget/SwingFileWidget.java
@@ -29,11 +29,12 @@
package org.scijava.ui.swing.widget;
+import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.UnsupportedFlavorException;
-import java.awt.dnd.InvalidDnDOperationException;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
+import java.awt.event.InputEvent;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
@@ -45,15 +46,16 @@
import javax.swing.Box;
import javax.swing.JButton;
+import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JTextField;
+import javax.swing.Timer;
import javax.swing.TransferHandler;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
-import org.scijava.plugin.Parameter;
+import com.formdev.flatlaf.FlatClientProperties;
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;
@@ -69,9 +71,6 @@ public class SwingFileWidget extends SwingInputWidget implements
FileWidget, ActionListener, DocumentListener
{
- @Parameter
- private UIService uiService;
-
private JTextField path;
private JButton browse;
@@ -90,16 +89,21 @@ public void set(final WidgetModel model) {
super.set(model);
path = new JTextField(16);
+ path.putClientProperty(FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, true);
path.setDragEnabled(true);
+ path.putClientProperty(FlatClientProperties.PLACEHOLDER_TEXT, model
+ .isStyle(FileWidget.DIRECTORY_STYLE)
+ ? "Browse, drop, or paste a folder path"
+ : "Browse, drop, or paste a file path");
final String style = model.getItem().getWidgetStyle();
- path.setTransferHandler(new FileTransferHandler(style));
+ path.setTransferHandler(new FileTransferHandler(style, path.getTransferHandler()));
setToolTip(path);
getComponent().add(path);
path.getDocument().addDocumentListener(this);
getComponent().add(Box.createHorizontalStrut(3));
- browse = new JButton("Browse");
+ browse = new JButton("Browse...");
setToolTip(browse);
getComponent().add(browse);
browse.addActionListener(this);
@@ -135,7 +139,7 @@ else if (model.isStyle(FileWidget.SAVE_STYLE)) {
else {
style = FileWidget.OPEN_STYLE;
}
- file = uiService.chooseFile(file, style);
+ file = chooseFile(file, style);
if (file == null) return;
path.setText(file.getAbsolutePath());
@@ -337,63 +341,91 @@ public static List filterFiles(final List list,
// -- Helper classes --
+ /** Briefly outlines the path field as invalid (FlatLaf only) */
+ private void flashError() {
+ path.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR);
+ final Timer timer = new Timer(1000, e -> path.putClientProperty(FlatClientProperties.OUTLINE, null));
+ timer.setRepeats(false);
+ timer.start();
+ }
+
+ /**
+ * Handles file drops, delegating everything else (text paste, copy, cut,
+ * text drag) to the default handler of the text field
+ */
private class FileTransferHandler extends TransferHandler {
private final String style;
+ private final TransferHandler fallback;
- public FileTransferHandler(final String style) {
+ public FileTransferHandler(final String style, final TransferHandler fallback) {
this.style = style;
+ this.fallback = fallback;
+ }
+
+ @Override
+ public int getSourceActions(final JComponent c) {
+ return fallback == null ? NONE : fallback.getSourceActions(c);
+ }
+
+ @Override
+ public void exportToClipboard(final JComponent comp,
+ final Clipboard clip, final int action) throws IllegalStateException
+ {
+ if (fallback != null) fallback.exportToClipboard(comp, clip, action);
+ }
+
+ @Override
+ public void exportAsDrag(final JComponent comp, final InputEvent e,
+ final int action)
+ {
+ if (fallback != null) fallback.exportAsDrag(comp, e, action);
}
@Override
public boolean canImport(final TransferHandler.TransferSupport support) {
- if (!hasFiles(support)) return false;
-
- // We wish to test the content of the transfer data and
- // determine if they are (a) files and (b) files we are
- // actually interested in processing. So we need to call
- // getTransferData() so that we can inspect the file names.
- // Unfortunately, this will not always work.
- // Under Windows, the Transferable instance
- // will have transfer data ONLY while the mouse button is
- // depressed. However, when the user releases the mouse
- // button, this method will be called one last time. And when
- // when this method attempts to getTransferData, Java will throw
- // an InvalidDnDOperationException. Since we know that the
- // exception is coming, we simply catch it and ignore it.
- // See:
- // https://coderanch.com/t/664525/java/Invalid-Drag-Drop-Exception
+ if (!hasFiles(support)) {
+ return fallback != null && fallback.canImport(support);
+ }
+
+ // Transfer data may not be readable while dragging (Windows, and
+ // macOS). Accept and validate on drop
try {
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;
- } catch (InvalidDnDOperationException exc) {
+ if (allFiles == null || allFiles.isEmpty()) return true;
+ if (allFiles.size() != 1) return false;
+ final FileFilter filter = SwingFileWidget.createFileFilter(style);
+ return SwingFileWidget.filterFiles(allFiles, filter).size() == 1;
+ }
+ catch (final RuntimeException exc) {
return true;
}
}
@Override
- public boolean importData(TransferHandler.TransferSupport support) {
- final List files = getFiles(support);
- if (files == null) {
- String text = getText(support);
- if (text.equals(""))
+ public boolean importData(final TransferHandler.TransferSupport support) {
+ if (hasFiles(support)) {
+ final List files;
+ try {
+ files = getFiles(support);
+ }
+ catch (final RuntimeException exc) {
+ flashError();
return false;
- // TODO check if text matches filter/style
- ((JTextField) support.getComponent()).setText(text);
+ }
+ if (files == null || files.size() != 1) {
+ flashError();
+ return false;
+ }
+ final FileFilter filter = SwingFileWidget.createFileFilter(style);
+ if (SwingFileWidget.filterFiles(files, filter).isEmpty()) {
+ flashError();
+ return false;
+ }
+ path.setText(files.get(0).getAbsolutePath());
return true;
}
- if (files.size() != 1) return false;
-
- final File file = files.get(0);
- ((JTextField) support.getComponent()).setText(file.getAbsolutePath());
- return true;
+ return fallback != null && fallback.importData(support);
}
}
}
diff --git a/src/main/java/org/scijava/ui/swing/widget/SwingInputWidget.java b/src/main/java/org/scijava/ui/swing/widget/SwingInputWidget.java
index f6177a6..055eb88 100644
--- a/src/main/java/org/scijava/ui/swing/widget/SwingInputWidget.java
+++ b/src/main/java/org/scijava/ui/swing/widget/SwingInputWidget.java
@@ -30,6 +30,10 @@
package org.scijava.ui.swing.widget;
import java.awt.Color;
+import java.awt.Window;
+import java.io.File;
+import java.io.FileFilter;
+import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -43,6 +47,7 @@
import org.scijava.ui.AbstractUIInputWidget;
import org.scijava.ui.UserInterface;
+import org.scijava.ui.swing.AbstractSwingUI;
import org.scijava.ui.swing.SwingUI;
import org.scijava.widget.WidgetModel;
@@ -64,8 +69,11 @@ public abstract class SwingInputWidget extends
public void set(final WidgetModel model) {
super.set(model);
uiComponent = new JPanel();
+ // Zero column gaps: empty trailing columns must not take up space, so that
+ // single-component widgets align with the right edge. Widgets needing
+ // spacing between components add it explicitly
final MigLayout layout =
- new MigLayout("fillx,ins 3 0 3 0", "[fill,grow|pref]");
+ new MigLayout("fillx,ins 3 0 3 0", "[fill,grow]0[pref]0[pref]");
uiComponent.setLayout(layout);
}
@@ -98,6 +106,41 @@ protected UserInterface ui() {
// -- Helper methods --
+ /**
+ * Asks the user for a file, with the dialog shown relative to this widget
+ * rather than the application frame.
+ *
+ * @see org.scijava.ui.UserInterface#chooseFile(File, String)
+ */
+ protected File chooseFile(final File file, final String style) {
+ final Window parent = SwingUtilities.getWindowAncestor(getComponent());
+ final UserInterface ui = ui();
+ if (ui instanceof AbstractSwingUI) {
+ return ((AbstractSwingUI) ui).chooseFile(parent, file, style);
+ }
+ return ui.chooseFile(file, style);
+ }
+
+ /**
+ * Asks the user for files, with the dialog shown relative to this widget
+ * rather than the application frame.
+ *
+ * @see org.scijava.ui.UserInterface#chooseFiles(File, List, FileFilter,
+ * String)
+ */
+ protected List chooseFiles(final File start, final List files,
+ final FileFilter filter, final String style)
+ {
+ final Window parent = SwingUtilities.getWindowAncestor(getComponent());
+ final UserInterface ui = ui();
+ if (ui instanceof AbstractSwingUI) {
+ final File[] result = ((AbstractSwingUI) ui).chooseFiles(parent, start,
+ files.toArray(new File[0]), filter, style);
+ return result == null ? null : Arrays.asList(result);
+ }
+ return ui.chooseFiles(start, files, filter, style);
+ }
+
/** Assigns the model's description as the given component's tool tip. */
protected void setToolTip(final JComponent c) {
tooltipped.add(c);
diff --git a/src/main/java/org/scijava/ui/swing/widget/SwingNumberWidget.java b/src/main/java/org/scijava/ui/swing/widget/SwingNumberWidget.java
index e2f0e80..db74ca8 100644
--- a/src/main/java/org/scijava/ui/swing/widget/SwingNumberWidget.java
+++ b/src/main/java/org/scijava/ui/swing/widget/SwingNumberWidget.java
@@ -133,7 +133,9 @@ else if (model.isStyle(NumberWidget.SLIDER_STYLE)) {
spinner.setPreferredSize(spinnerSize);
fixSpinner(type);
setToolTip(spinner);
- getComponent().add(spinner);
+ // add spacing only if there is a slider or scroll bar before the spinner
+ getComponent().add(spinner, slider != null || scrollBar != null
+ ? "gapleft 6" : "");
limitWidth(200);
spinner.addChangeListener(this);
diff --git a/src/main/java/org/scijava/ui/swing/widget/SwingTextWidget.java b/src/main/java/org/scijava/ui/swing/widget/SwingTextWidget.java
index 3be3765..1ac5f27 100644
--- a/src/main/java/org/scijava/ui/swing/widget/SwingTextWidget.java
+++ b/src/main/java/org/scijava/ui/swing/widget/SwingTextWidget.java
@@ -41,6 +41,7 @@
import javax.swing.text.DocumentFilter;
import javax.swing.text.JTextComponent;
+import com.formdev.flatlaf.FlatClientProperties;
import org.scijava.log.LogService;
import org.scijava.plugin.Plugin;
import org.scijava.widget.InputWidget;
@@ -102,10 +103,12 @@ public void set(final WidgetModel model) {
}
else if (model.isStyle(TextWidget.PASSWORD_STYLE)) {
textComponent = new JPasswordField("", columns);
+ textComponent.putClientProperty(FlatClientProperties.STYLE, "showRevealButton: true; showCapsLock: true");
}
else {
textComponent = new JTextField("", columns);
}
+ textComponent.putClientProperty(FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, true);
setToolTip(textComponent);
getComponent().add(addScrollPane ? new JScrollPane(textComponent) : textComponent);
limitLength();
diff --git a/src/test/java/org/scijava/ui/swing/widget/SwingWidgetLayoutDemo.java b/src/test/java/org/scijava/ui/swing/widget/SwingWidgetLayoutDemo.java
new file mode 100644
index 0000000..97470e6
--- /dev/null
+++ b/src/test/java/org/scijava/ui/swing/widget/SwingWidgetLayoutDemo.java
@@ -0,0 +1,199 @@
+/*
+ * #%L
+ * SciJava UI components for Java Swing.
+ * %%
+ * Copyright (C) 2010 - 2026 SciJava developers.
+ * %%
+ * 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.io.File;
+import java.util.Date;
+
+import org.scijava.Context;
+import org.scijava.ItemVisibility;
+import org.scijava.command.Command;
+import org.scijava.command.CommandService;
+import org.scijava.object.ObjectService;
+import org.scijava.plugin.Parameter;
+import org.scijava.ui.UIService;
+import org.scijava.util.ColorRGB;
+import org.scijava.widget.Button;
+import org.scijava.widget.ChoiceWidget;
+import org.scijava.widget.FileListWidget;
+import org.scijava.widget.FileWidget;
+import org.scijava.widget.NumberWidget;
+import org.scijava.widget.TextWidget;
+
+/**
+ * Shows one instance of every widget supported by the Swing UI in a single
+ * dialog, for visual testing of layout and alignment (e.g., that all widgets
+ * share the same right edge).
+ */
+public class SwingWidgetLayoutDemo implements Command {
+
+ /** Dummy type to populate the object widget. */
+ public static class Thing {
+
+ private final String name;
+
+ public Thing(final String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+ }
+
+ // -- Message --
+
+ @Parameter(visibility = ItemVisibility.MESSAGE)
+ private String message = "Every supported widget, for layout testing";
+
+ // -- Text --
+
+ @Parameter(label = "Text (default)", persist = false)
+ private String text = "text";
+
+ @Parameter(label = "Text (field)", style = TextWidget.FIELD_STYLE,
+ persist = false)
+ private String field = "field";
+
+ @Parameter(label = "Text (password)", style = TextWidget.PASSWORD_STYLE,
+ persist = false)
+ private String password = "secret";
+
+ @Parameter(label = "Text (area)", style = TextWidget.AREA_STYLE,
+ persist = false)
+ private String area = "area\nmultiple lines";
+
+ @Parameter(label = "Character", persist = false)
+ private Character character = 'c';
+
+ // -- Number --
+
+ @Parameter(label = "Number (spinner)", persist = false)
+ private Double spinner = 1.5;
+
+ @Parameter(label = "Number (integer)", min = "0", max = "100",
+ persist = false)
+ private Integer integer = 10;
+
+ @Parameter(label = "Number (slider)", style = NumberWidget.SLIDER_STYLE,
+ min = "0", max = "10", stepSize = "0.01", persist = false)
+ private Double slider = 1d;
+
+ @Parameter(label = "Number (scroll bar)",
+ style = NumberWidget.SCROLL_BAR_STYLE, min = "0", max = "10",
+ stepSize = "0.01", persist = false)
+ private Double scrollBar = 1d;
+
+ // -- Boolean --
+
+ @Parameter(label = "Toggle", persist = false)
+ private Boolean toggle = true;
+
+ // -- Choices --
+
+ @Parameter(label = "Choice (combo box)", choices = { "One", "Two", "Three" },
+ persist = false)
+ private String choice = "One";
+
+ @Parameter(label = "Choice (radio, horizontal)",
+ style = ChoiceWidget.RADIO_BUTTON_HORIZONTAL_STYLE,
+ choices = { "One", "Two", "Three" }, persist = false)
+ private String radioH = "One";
+
+ @Parameter(label = "Choice (radio, vertical)",
+ style = ChoiceWidget.RADIO_BUTTON_VERTICAL_STYLE,
+ choices = { "One", "Two", "Three" }, persist = false)
+ private String radioV = "One";
+
+ @Parameter(label = "Object", persist = false)
+ private Thing thing;
+
+ // -- Files --
+
+ @Parameter(label = "File (open)", style = FileWidget.OPEN_STYLE,
+ persist = false)
+ private File open;
+
+ @Parameter(label = "File (open, tif/png)",
+ style = FileWidget.OPEN_STYLE + ",extensions:tif/png", persist = false)
+ private File openFiltered;
+
+ @Parameter(label = "File (save)", style = FileWidget.SAVE_STYLE,
+ persist = false)
+ private File save;
+
+ @Parameter(label = "Folder", style = FileWidget.DIRECTORY_STYLE,
+ persist = false)
+ private File directory;
+
+ @Parameter(label = "File list (files)", style = FileListWidget.FILES_ONLY,
+ persist = false)
+ private File[] files;
+
+ @Parameter(label = "File list (folders)",
+ style = FileListWidget.DIRECTORIES_ONLY, persist = false)
+ private File[] folders;
+
+ @Parameter(label = "File list (both)",
+ style = FileListWidget.FILES_AND_DIRECTORIES, persist = false)
+ private File[] both;
+
+ // -- Other --
+
+ @Parameter(label = "Date", persist = false)
+ private Date date = new Date();
+
+ @Parameter(label = "Color", persist = false)
+ private ColorRGB color = new ColorRGB(255, 128, 0);
+
+ @Parameter(label = "Button", callback = "onButton", persist = false)
+ private Button button;
+
+ @Override
+ public void run() {
+ // Nothing to do
+ }
+
+ @SuppressWarnings("unused")
+ private void onButton() {
+ System.out.println("Button pressed");
+ }
+
+ public static void main(final String... args) throws Exception {
+ final Context context = new Context();
+ final ObjectService objectService = context.service(ObjectService.class);
+ objectService.addObject(new Thing("Thing A"));
+ objectService.addObject(new Thing("Thing B"));
+ context.service(UIService.class).showUI();
+ context.service(CommandService.class).run(SwingWidgetLayoutDemo.class,
+ true);
+ }
+}