Skip to content

Commit d0ebd47

Browse files
committed
Add preference for native file choosers
1 parent 82933c0 commit d0ebd47

3 files changed

Lines changed: 143 additions & 56 deletions

File tree

‎src/main/java/sc/fiji/snt/SNTPrefs.java‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,26 @@ public static void setLookAndFeel(final String laf) {
487487
Prefs.set("snt.laf", laf);
488488
}
489489

490+
/**
491+
* Gets whether SNT's open/save dialogs should use the OS native file dialogs (FlatLaf's {@code SystemFileChooser})
492+
* instead of SNT's own {@code FileChooser}.
493+
*
494+
* @return true if native OS file dialogs should be used when possible
495+
*/
496+
public static boolean getUseNativeFileChooser() {
497+
return Prefs.get("snt.nativeFileChooser", false);
498+
}
499+
500+
/**
501+
* Sets whether SNT's simple open/save dialogs should use native OS file dialogs.
502+
*
503+
* @param useNative the new preference
504+
* @see #getUseNativeFileChooser()
505+
*/
506+
public static void setUseNativeFileChooser(final boolean useNative) {
507+
Prefs.set("snt.nativeFileChooser", useNative);
508+
}
509+
490510
/**
491511
* Gets the current number of threads setting.
492512
*

‎src/main/java/sc/fiji/snt/gui/GuiUtils.java‎

Lines changed: 114 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.formdev.flatlaf.*;
2626
import com.formdev.flatlaf.extras.FlatSVGIcon;
2727
import com.formdev.flatlaf.icons.FlatClearIcon;
28+
import com.formdev.flatlaf.util.SystemFileChooser;
2829
import com.formdev.flatlaf.util.UIScale;
2930
import com.jidesoft.plaf.LookAndFeelFactory;
3031
import com.jidesoft.popup.JidePopup;
@@ -1264,12 +1265,10 @@ public void changedUpdate(final DocumentEvent e) {
12641265
}
12651266

12661267
public File getOpenFile(final String title, final File file, final String... allowedExtensions) {
1267-
final JFileChooser chooser = fileChooser(title, file, JFileChooser.OPEN_DIALOG, JFileChooser.FILES_ONLY);
1268-
if (allowedExtensions != null && allowedExtensions.length > 0) {
1269-
chooser.addChoosableFileFilter(new FileNameExtensionFilter(
1270-
"Reconstruction files (" + String.join(",", allowedExtensions) + ")", allowedExtensions));
1271-
}
1272-
return (File) getOpenFileChooserResult(chooser);
1268+
final FileNameExtensionFilter filter = (allowedExtensions == null || allowedExtensions.length == 0) ? null
1269+
: new FileNameExtensionFilter(
1270+
"Reconstruction files (" + String.join(",", allowedExtensions) + ")", allowedExtensions);
1271+
return (File) showOpenDialog(title, JFileChooser.FILES_ONLY, false, file, false, filter);
12731272
}
12741273

12751274
public File getSaveFile(final String title, final File file, final String... allowedExtensions) {
@@ -1300,76 +1299,135 @@ public File getSaveFile(final String title, final File file, final String... all
13001299
}
13011300

13021301
public File[] getReconstructionFiles(final File selectedFile) {
1303-
final JFileChooser fileChooser = getReconstructionFileChooser(null);
1304-
if (selectedFile != null) fileChooser.setSelectedFile(selectedFile);
1305-
return (File[])getOpenFileChooserResult(fileChooser);
1302+
final FileNameExtensionFilter filter = new FileNameExtensionFilter(
1303+
"Reconstruction files (.traces, .swc, .json, .ndf, .xml)", "traces", "swc", "json", "ndf", "xml");
1304+
return (File[]) showOpenDialog("Choose Reconstruction File(s)", JFileChooser.FILES_ONLY, true,
1305+
selectedFile, false, filter);
13061306
}
13071307

13081308
public File getFile(final File file, final String extensionWithoutPeriod) {
1309-
final JFileChooser fileChooser = GuiUtils.getDnDFileChooser();
1310-
fileChooser.setDialogTitle("/".equals(extensionWithoutPeriod) ? "Choose Directory" : "Choose " + extensionWithoutPeriod.toUpperCase() + " File");
1311-
fileChooser.setFileSelectionMode("/".equals(extensionWithoutPeriod) ? JFileChooser.DIRECTORIES_ONLY : JFileChooser.FILES_ONLY);
1312-
fileChooser.setDialogType(JFileChooser.OPEN_DIALOG);
1313-
fileChooser.setMultiSelectionEnabled(false);
1314-
fileChooser.addChoosableFileFilter(
1315-
new FileNameExtensionFilter(extensionWithoutPeriod.toUpperCase(), extensionWithoutPeriod));
1316-
if (file != null) {
1317-
if ("/".equals(extensionWithoutPeriod)) {
1318-
fileChooser.setCurrentDirectory(file);
1319-
} else {
1320-
fileChooser.setSelectedFile(file);
1321-
}
1322-
}
1323-
fileChooser.setMultiSelectionEnabled(false);
1324-
return (File) getOpenFileChooserResult(fileChooser);
1309+
final boolean dir = "/".equals(extensionWithoutPeriod);
1310+
final String title = dir ? "Choose Directory" : "Choose " + extensionWithoutPeriod.toUpperCase() + " File";
1311+
final int selectionMode = dir ? JFileChooser.DIRECTORIES_ONLY : JFileChooser.FILES_ONLY;
1312+
// NB: a "/"-named extension filter is meaningless in DIRECTORIES_ONLY mode (both classic
1313+
// and native choosers only ever list directories there), so it is only added for files
1314+
final FileNameExtensionFilter filter = dir ? null
1315+
: new FileNameExtensionFilter(extensionWithoutPeriod.toUpperCase(), extensionWithoutPeriod);
1316+
return (File) showOpenDialog(title, selectionMode, false, file, dir, filter);
13251317
}
13261318

13271319
public File getImageFile(final File file) {
1328-
final JFileChooser fileChooser = GuiUtils.getDnDFileChooser();
1329-
fileChooser.setDialogTitle("Choose Image File");
1330-
fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
1331-
fileChooser.setDialogType(JFileChooser.OPEN_DIALOG);
1332-
fileChooser.addChoosableFileFilter(
1333-
new FileNameExtensionFilter("IJ 'native' formats (.tif, .png, .raw, .zip, etc.)", "tif", "tiff", "dcm",
1334-
"avi", "fits", "pgm", "jpg", "gif", "bmp", "zip", "png", "raw"));
1335-
fileChooser.setMultiSelectionEnabled(false);
1336-
if (file != null)
1337-
fileChooser.setSelectedFile(file);
1338-
fileChooser.setMultiSelectionEnabled(false);
1339-
return (File) getOpenFileChooserResult(fileChooser);
1320+
final FileNameExtensionFilter filter = new FileNameExtensionFilter(
1321+
"IJ 'native' formats (.tif, .png, .raw, .zip, etc.)", "tif", "tiff", "dcm",
1322+
"avi", "fits", "pgm", "jpg", "gif", "bmp", "zip", "png", "raw");
1323+
return (File) showOpenDialog("Choose Image File", JFileChooser.FILES_ONLY, false, file, false, filter);
13401324
}
13411325

13421326
public File getReconstructionFile(final File file, final String extension) {
1343-
FileNameExtensionFilter filter = switch (extension) {
1327+
final FileNameExtensionFilter filter = switch (extension) {
13441328
case "swc" -> new FileNameExtensionFilter("SWC files (.swc)", "swc");
13451329
case "traces" -> new FileNameExtensionFilter("SNT TRACES files (.traces)", "traces");
13461330
case "json" -> new FileNameExtensionFilter("JSON files (.json)", "json");
13471331
case "ndf" -> new FileNameExtensionFilter("NeuronJ NDF files (.ndf)", "ndf");
13481332
case "xml" -> new FileNameExtensionFilter("Neurolucida XML files (.xml)", "xml");
13491333
case "labels" -> new FileNameExtensionFilter("AmiraMesh labels (.labels)", "labels");
1350-
case null, default -> null;
1334+
case null, default -> new FileNameExtensionFilter(
1335+
"Reconstruction files (.traces, .swc, .json, .ndf, .xml)", "traces", "swc", "json", "ndf", "xml");
13511336
};
1352-
final JFileChooser fileChooser = getReconstructionFileChooser(filter);
1353-
fileChooser.setSelectedFile(file);
1354-
if (extension == null)
1355-
fileChooser.setDialogTitle("Choose Reconstruction File");
1356-
else if ("labels".equals(extension))
1357-
fileChooser.setDialogTitle("Choose LABELS File");
1358-
else
1359-
fileChooser.setDialogTitle("Choose Reconstruction (" + extension.toUpperCase() + ") File");
1360-
fileChooser.setMultiSelectionEnabled(false);
1361-
return (File)getOpenFileChooserResult(fileChooser);
1337+
final String title;
1338+
if (extension == null) title = "Choose Reconstruction File";
1339+
else if ("labels".equals(extension)) title = "Choose LABELS File";
1340+
else title = "Choose Reconstruction (" + extension.toUpperCase() + ") File";
1341+
return (File) showOpenDialog(title, JFileChooser.FILES_ONLY, false, file, false, filter);
13621342
}
13631343

1364-
private JFileChooser getReconstructionFileChooser(final FileNameExtensionFilter filter) {
1344+
/**
1345+
* Shows an "Open" dialog, honoring {@link SNTPrefs#getUseNativeFileChooser()}: either SNT's
1346+
* classic {@link FileChooser} (with its accessory toolbar and drag-and-drop support) or
1347+
* FlatLaf's {@link SystemFileChooser} (native OS dialog). Only intended for the simple,
1348+
* extension-filter-based dialogs built by the {@code getXxxFile(s)} methods above - dialogs that
1349+
* need custom {@code FileFilter}s, {@code FILES_AND_DIRECTORIES} selection, or drag-and-drop
1350+
* (e.g. {@link #getSaveFile}, {@link #fileChooser}, {@link #getDnDFileChooser()} and their
1351+
* callers) always use the classic chooser and are not affected by this method or the preference.
1352+
*
1353+
* @param title the dialog title
1354+
* @param selectionMode {@code JFileChooser.FILES_ONLY} or {@code JFileChooser.DIRECTORIES_ONLY}
1355+
* @param multiSelection whether multiple files may be selected
1356+
* @param preset a file/directory to pre-select or pre-navigate to, or {@code null}
1357+
* @param presetIsDirectory whether {@code preset} should be treated as a directory to navigate
1358+
* to ({@code setCurrentDirectory}) rather than a file to pre-select
1359+
* ({@code setSelectedFile})
1360+
* @param filter the (single) choosable extension filter to add, or {@code null} for none
1361+
* @return a {@code File} (single-selection) or {@code File[]} (multi-selection), or {@code null}
1362+
* if the dialog was cancelled
1363+
*/
1364+
private Object showOpenDialog(final String title, final int selectionMode, final boolean multiSelection,
1365+
final File preset, final boolean presetIsDirectory, final FileNameExtensionFilter filter) {
1366+
if (SNTPrefs.getUseNativeFileChooser()) {
1367+
return showNativeOpenDialog(title, selectionMode, multiSelection, preset, presetIsDirectory, filter);
1368+
}
13651369
final JFileChooser fileChooser = GuiUtils.getDnDFileChooser();
1366-
fileChooser.setDialogTitle("Choose Reconstruction File(s)");
1367-
fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
1370+
fileChooser.setDialogTitle(title);
1371+
fileChooser.setFileSelectionMode(selectionMode);
13681372
fileChooser.setDialogType(JFileChooser.OPEN_DIALOG);
1369-
fileChooser.addChoosableFileFilter(Objects.requireNonNullElseGet(filter, () -> new FileNameExtensionFilter(
1370-
"Reconstruction files (.traces, .swc, .json, .ndf, .xml)", "traces", "swc", "json", "ndf", "xml")));
1371-
fileChooser.setMultiSelectionEnabled(true);
1372-
return fileChooser;
1373+
fileChooser.setMultiSelectionEnabled(multiSelection);
1374+
if (filter != null) fileChooser.addChoosableFileFilter(filter);
1375+
if (preset != null) {
1376+
if (presetIsDirectory) fileChooser.setCurrentDirectory(preset);
1377+
else fileChooser.setSelectedFile(preset);
1378+
}
1379+
return getOpenFileChooserResult(fileChooser);
1380+
}
1381+
1382+
private Object showNativeOpenDialog(final String title, final int selectionMode, final boolean multiSelection,
1383+
final File preset, final boolean presetIsDirectory, final FileNameExtensionFilter filter) {
1384+
// Unlike JFileChooser.showOpenDialog(), SystemFileChooser.showOpenDialog() throws
1385+
// IllegalStateException if not called on the EDT
1386+
if (!SwingUtilities.isEventDispatchThread()) {
1387+
final Object[] result = new Object[1];
1388+
try {
1389+
SwingUtilities.invokeAndWait(() -> result[0] = showNativeOpenDialogOnEDT(
1390+
title, selectionMode, multiSelection, preset, presetIsDirectory, filter));
1391+
} catch (final InterruptedException e) {
1392+
Thread.currentThread().interrupt();
1393+
return null;
1394+
} catch (final java.lang.reflect.InvocationTargetException e) {
1395+
SNTUtils.error("Native file dialog failed", e.getCause());
1396+
return null;
1397+
}
1398+
return result[0];
1399+
}
1400+
return showNativeOpenDialogOnEDT(title, selectionMode, multiSelection, preset, presetIsDirectory, filter);
1401+
}
1402+
1403+
private Object showNativeOpenDialogOnEDT(final String title, final int selectionMode,
1404+
final boolean multiSelection, final File preset, final boolean presetIsDirectory,
1405+
final FileNameExtensionFilter filter) {
1406+
final SystemFileChooser fileChooser = new SystemFileChooser();
1407+
fileChooser.setDialogTitle(title);
1408+
fileChooser.setFileSelectionMode(selectionMode == JFileChooser.DIRECTORIES_ONLY
1409+
? SystemFileChooser.DIRECTORIES_ONLY : SystemFileChooser.FILES_ONLY);
1410+
fileChooser.setMultiSelectionEnabled(multiSelection);
1411+
if (filter != null) {
1412+
fileChooser.addChoosableFileFilter(
1413+
new SystemFileChooser.FileNameExtensionFilter(filter.getDescription(), filter.getExtensions()));
1414+
}
1415+
if (preset != null) {
1416+
if (presetIsDirectory) fileChooser.setCurrentDirectory(preset);
1417+
else fileChooser.setSelectedFile(preset);
1418+
}
1419+
// HACK: On macOS this seems to help to ensure prompt is displayed as frontmost
1420+
final boolean focused = parent instanceof Window && parent.hasFocus();
1421+
if (focused) ((Window) parent).toBack();
1422+
if (fileChooser.showOpenDialog(parent) == SystemFileChooser.APPROVE_OPTION) {
1423+
// NB: unlike JFileChooser, getSelectedFiles() returns the selection in single-selection
1424+
// mode too - see FlatLaf's SystemFileChooser docs
1425+
final File[] result = fileChooser.getSelectedFiles();
1426+
if (result.length > 0) SNTPrefs.setLastKnownDir(result[0]);
1427+
return multiSelection ? result : (result.length > 0 ? result[0] : null);
1428+
}
1429+
if (focused) ((Window) parent).toFront();
1430+
return null;
13731431
}
13741432

13751433
private Object getOpenFileChooserResult(final JFileChooser fileChooser) {

‎src/main/java/sc/fiji/snt/gui/cmds/PrefsCmd.java‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,13 @@ public class PrefsCmd extends OptionsPlugin {
117117
@Parameter(label="Remember window locations", description="Whether position of dialogs should be preserved across restarts")
118118
private boolean persistentWinLoc;
119119

120+
@Parameter(label="Use native file dialogs", description="""
121+
Whether Open/Save dialogs should use the operating system's native file dialogs
122+
instead of SNT's own file chooser. Native dialogsnlook and feel like the rest of
123+
your OS, but lack SNT's toolbar (hidden files, filter by pattern, recent locations,
124+
etc.). Both support drag-and-drop.""")
125+
private boolean nativeFileChooser;
126+
120127
@Parameter(label = "Look and feel (L&F)", required = false, persist = false,
121128
description = "How should SNT look? NB: This may also affect other Swing-based dialogs in Fiji.",
122129
initializer = "initLookAndFeel")
@@ -148,6 +155,7 @@ public void run() {
148155
snt.getPrefs().setSomaDisplayTriangle(somaDisplayOption==PathNodeCanvas.SOMA_RENDER_TRIANGLE);
149156
applyWorkspaceChange();
150157
snt.getPrefs().setSaveWinLocations(persistentWinLoc);
158+
SNTPrefs.setUseNativeFileChooser(nativeFileChooser);
151159
snt.getPrefs().setSaveCompressedTraces(compressTraces);
152160
snt.getPrefs().set2DDisplayCanvas(force2DDisplayCanvas);
153161
SNTPrefs.setThreads(Math.max(0, nThreads));
@@ -203,6 +211,7 @@ private void init() {
203211
try {
204212
snt = sntService.getInstance();
205213
persistentWinLoc = snt.getPrefs().isSaveWinLocations();
214+
nativeFileChooser = SNTPrefs.getUseNativeFileChooser();
206215
force2DDisplayCanvas = snt.getPrefs().is2DDisplayCanvas();
207216
compressTraces = snt.getPrefs().isSaveCompressedTraces();
208217
nThreads = SNTPrefs.getThreads();

0 commit comments

Comments
 (0)