File dialogs¶
An application with an Export button needs to ask where the file goes, and one with an Import button needs to ask which file to read. ComponentFactory.fileDialogs() returns the platform's own open, save and folder dialogs. Strata draws no file chooser of its own: the dialogs are the engine's, so they look and behave as the user expects on that system, and an engine without them reports that.
Asking¶
FileDialogs (org.inspirenxe.strata.api.file) has four calls, each taking a FileDialog that says what to ask:
| Call | Returns | Asks for |
|---|---|---|
open(dialog) |
CompletableFuture<Optional<Path>> |
One file to open |
openMany(dialog) |
CompletableFuture<List<Path>> |
One or more files to open, in the engine's order |
save(dialog) |
CompletableFuture<Optional<Path>> |
Where to save a file |
folder(dialog) |
CompletableFuture<Optional<Path>> |
A folder |
Each call shows the dialog and returns at once, without waiting for the user. The future completes later, on the UI thread, so its actions may change components. See Threads.
- When the user cancels, an
open,saveorfolderfuture completes with an emptyOptional, and anopenManyfuture with an empty list. - When the engine's dialog fails, the future completes exceptionally. Handle that with
exceptionally, as the example below does. - Strata writes and reads nothing. The engine may ask the user before it returns a file that exists, but your code does the saving.
/**
* Asks where to export the orders, and says what happened.
*/
public static void export(final ComponentFactory ui, final Consumer<Path> exporter, final Label status) {
final FileDialogs dialogs = ui.fileDialogs();
if (!dialogs.isSupported()) {
status.setText(Text.of("File dialogs are not supported here"));
return;
}
dialogs.save(FileDialog.of(Text.of("Export orders"))
.withSuggestedName("orders.csv")
.withFilter(Text.of("Spreadsheets"), "csv", "xlsx"))
// The future completes on the UI thread, so the actions may change components
.thenAccept(chosen -> chosen.ifPresentOrElse(file -> {
exporter.accept(file);
status.setText(Text.of("Exported to " + file.getFileName()));
}, () -> status.setText(Text.of("Export canceled"))))
.exceptionally(failure -> {
status.setText(Text.of("The dialog failed: " + failure.getMessage()));
return null;
});
}
What a dialog asks¶
FileDialog is an immutable record. FileDialog.of(title) makes one with the engine's choice of starting place, no suggested name and no filters, and the with methods return changed copies:
| Part | Set with | Means |
|---|---|---|
title |
FileDialog.of(title) |
The window title, as Text, so a translation key works |
start |
withStart(path) |
The folder or file the dialog starts at. Null in the record means the engine's choice |
suggestedName |
withSuggestedName(name) |
The file name a save dialog starts with. Other dialogs may ignore it |
filters |
withFilter(name, extensions...) |
One more filter, after the others |
A filter has a name and one or more extensions. The first filter is the one chosen when the dialog opens, and with no filters the dialog shows every file. Extensions are written without their dot and are kept in lower case. An extension that is empty, or holds a dot, a slash, a backslash or whitespace, throws IllegalArgumentException, and so does a filter with no extension. Filter.accepts(path) tests a file name against a filter, ignoring case, if you want to check what comes back. The dialog's filters do not apply to a folder dialog.
/**
* Asks for one or more files to import, starting in the folder the user imported from last time.
*/
public static void importFiles(final ComponentFactory ui, final Path lastFolder, final Consumer<Path> importer) {
ui.fileDialogs()
.openMany(FileDialog.of(Text.translatable("orders.import.title"))
.withStart(lastFolder)
.withFilter(Text.of("Orders"), "csv"))
.thenAccept(files -> files.forEach(importer)); // an empty list when canceled
}
/**
* Asks for a folder. The dialog's filters would not apply.
*/
public static void chooseFolder(final ComponentFactory ui, final Consumer<Path> chosen) {
ui.fileDialogs().folder(FileDialog.of(Text.of("Save reports in")))
.thenAccept(folder -> folder.ifPresent(chosen));
}
Engines without dialogs¶
isSupported() is false when the engine shows no file dialogs. Every call then returns a future that is already complete and empty, so code that only reacts to a chosen file does nothing, which is the same as a cancel. Check isSupported() when the screen should say so instead, as the export example does, or hide the button.
Which adapters offer dialogs is on their platform pages: Swing does, and the differences page compares them.
Testing¶
A test platform without dialogs is not supported, so every future completes empty. To test the code around a dialog, give the platform a FileDialogSource that returns futures the test completes. See Writing an adapter for the source's requirements.
Defaults¶
| What | Default |
|---|---|
FileDialog.of(title) |
No start, no suggested name, no filters |
| Platform without a dialog source | isSupported() false, futures complete empty at once |