13

My view controller has a single FileChooser instance used for both opening and saving files.

Every time I call showOpenDialog() or showSaveDialog() from that instance, I expect the resulting dialog to be in the same directory as I left it the last time I called one of them.

Instead, every time I call one of those methods, the dialogs open in the user home directory.

How do I make the "current directory" of the dialogs persist across different invocations?


Example of the current behaviour:

import java.io.File;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;

/**
 * Demonstrates the use of an open dialog.
 * 
 * @author N99x
 */
public class FileChooserTest extends Application {

    private final FileChooser open = new FileChooser();
    private File lastOpened = null;

    @Override
    public void start(Stage primaryStage) {
        Label lbl = new Label("File Opened: <null>");
        lbl.setPadding(new Insets(8));
        Button btn = new Button();
        btn.setPadding(new Insets(8));
        btn.setText("Open");
        btn.setOnAction((ActionEvent event) -> {
            open.setInitialDirectory(lastOpened);
            File selected = open.showOpenDialog(primaryStage);
            if (selected == null) {
                lbl.setText("File Opened: <null>");
                // lastOpened = ??;
            } else {
                lbl.setText("File Opened: " + selected.getAbsolutePath());
                lastOpened = selected.getParentFile();
            }
        });

        VBox root = new VBox(lbl, btn);
        root.setPadding(new Insets(8));
        root.setAlignment(Pos.TOP_CENTER);
        Scene scene = new Scene(root, 300, 300);

        primaryStage.setTitle("FileChooser Testing!");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }

}

I managed to get around for part of the problem by storing the opened value, but this doesn't work if the dialog is closed or cancelled.

Stevoisiak
  • 23,794
  • 27
  • 122
  • 225
  • Does this answer your question? [Is there any way to make Java file selection dialogs remember the last directory?](https://stackoverflow.com/questions/8282048/is-there-any-way-to-make-java-file-selection-dialogs-remember-the-last-directory) – Mahozad Sep 27 '22 at 19:14

4 Answers4

8

How do I make the "current directory" of the dialogs persist across different invocations?

You could modify the Singleton Pattern approach for this

Whereby you would only use one FileChooser and monitor/control the initial directory there, yet not directly exposing the instance to modifications outside of the class


For example:

public class RetentionFileChooser {
    private static FileChooser instance = null;
    private static SimpleObjectProperty<File> lastKnownDirectoryProperty = new SimpleObjectProperty<>();

    private RetentionFileChooser(){ }

    private static FileChooser getInstance(){
        if(instance == null) {
            instance = new FileChooser();
            instance.initialDirectoryProperty().bindBidirectional(lastKnownDirectoryProperty);
            //Set the FileExtensions you want to allow
            instance.getExtensionFilters().setAll(new ExtensionFilter("png files (*.png)", "*.png"));
        }
        return instance;
    }

    public static File showOpenDialog(){
        return showOpenDialog(null);
    }

    public static File showOpenDialog(Window ownerWindow){
        File chosenFile = getInstance().showOpenDialog(ownerWindow);
        if(chosenFile != null){
            //Set the property to the directory of the chosenFile so the fileChooser will open here next
            lastKnownDirectoryProperty.setValue(chosenFile.getParentFile());
        }
        return chosenFile;
    }

    public static File showSaveDialog(){
        return showSaveDialog(null);
    }

    public static File showSaveDialog(Window ownerWindow){
        File chosenFile = getInstance().showSaveDialog(ownerWindow);
        if(chosenFile != null){
            //Set the property to the directory of the chosenFile so the fileChooser will open here next
            lastKnownDirectoryProperty.setValue(chosenFile.getParentFile());
        }
        return chosenFile;
    }
}

This will set the initial directory of the FileChooser to be the directory of the file the user last opened/saved when it is re-used

Example usage:

File chosenFile = RetentionFileChooser.showOpenDialog();

However there is a limitation here:

//Set the FileExtensions you want to allow 
instance.getExtensionFilters().setAll(new ExtensionFilter("png files (*.png)", "*.png"));

As without providing any ExtensionFilter's the FileChooser becomes less user-friendly, requiring the user to manually append the file type, yet providing the filters when creating the instance with no way of updating them limits it to those same filters

One way to improve upon this is to expose optional filters within RetentionFileChooser that can be provided using varargs, whereby you can choose when to modify the filters as you display the dialogs

For example, building on the previous:

public class RetentionFileChooser {
    public enum FilterMode {
        //Setup supported filters
        PNG_FILES("png files (*.png)", "*.png"),
        TXT_FILES("txt files (*.txt)", "*.txt");

        private ExtensionFilter extensionFilter;

        FilterMode(String extensionDisplayName, String... extensions){
            extensionFilter = new ExtensionFilter(extensionDisplayName, extensions);
        }

        public ExtensionFilter getExtensionFilter(){
            return extensionFilter;
        }
    }

    private static FileChooser instance = null;
    private static SimpleObjectProperty<File> lastKnownDirectoryProperty = new SimpleObjectProperty<>();

    private RetentionFileChooser(){ }

    private static FileChooser getInstance(FilterMode... filterModes){
        if(instance == null) {
            instance = new FileChooser();
            instance.initialDirectoryProperty().bindBidirectional(lastKnownDirectoryProperty);
        }
        //Set the filters to those provided
        //You could add check's to ensure that a default filter is included, adding it if need be
        instance.getExtensionFilters().setAll(
                Arrays.stream(filterModes)
                        .map(FilterMode::getExtensionFilter)
                        .collect(Collectors.toList()));
        return instance;
    }

    public static File showOpenDialog(FilterMode... filterModes){
        return showOpenDialog(null, filterModes);
    }

    public static File showOpenDialog(Window ownerWindow, FilterMode...filterModes){
        File chosenFile = getInstance(filterModes).showOpenDialog(ownerWindow);
        if(chosenFile != null){
            lastKnownDirectoryProperty.setValue(chosenFile.getParentFile());
        }
        return chosenFile;
    }

    public static File showSaveDialog(FilterMode... filterModes){
        return showSaveDialog(null, filterModes);
    }

    public static File showSaveDialog(Window ownerWindow, FilterMode... filterModes){
        File chosenFile = getInstance(filterModes).showSaveDialog(ownerWindow);
        if(chosenFile != null){
            lastKnownDirectoryProperty.setValue(chosenFile.getParentFile());
        }
        return chosenFile;
    }
}

Example usage:

//Note the previous example still holds
File chosenFile = RetentionFileChooser.showOpenDialog();
File file = RetentionFileChooser.showSaveDialog(FilterMode.PNG_FILES);

but this does not work if the dialog is closed or canceled.

Unfortunately FileChooser doesn't expose information on what directory was being examined before it was closed / terminated. If you de-compile the class and trace through, it eventually hits a native call. So even if FileChooser wasn't final allowing it to be subclassed, it is just as unlikely to be able to get this information

The only gain the above approach provides around this is that it doesn't change the initial directory if this scenario is hit


If you're interested, there are some very good answers on the native key word within this question and those linked:

Community
  • 1
  • 1
Peter
  • 1,592
  • 13
  • 20
  • A simple solution for file extension suggestions would be to reset the file extensions each time you call it. IE: `fileChooser.getExtensionFilters().clear(); fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("PNG", "*.png"));` – Stevoisiak Apr 07 '17 at 00:23
4
private static String lastVisitedDirectory=System.getProperty("user.home");

FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("View Pictures");
fileChooser.setInitialDirectory(new  File(lastVisitedDirectory));
fileChooser.getExtensionFilters().addAll(new FileChooser.ExtensionFilter("--"));

List<File> files = fileChooser.showOpenMultipleDialog(----);


lastVisitedDirectory=(files!=null && files.size()>=1)?files.get(0).getParent():System.getProperty("user.home"); 
GeertVc
  • 1,914
  • 17
  • 22
0

Maybe also interesting...

If you want to save the file chosen after you close your application and use it again for your initial file value after you start up your app again (and you don't have too much stuff you want to store) you can fall back to the good old .ini files for Windows.

A good helper class for this is ini4j. It has all you need for basic stuff. Clear, simple and no hassle.

GeertVc
  • 1,914
  • 17
  • 22
0

My suggestion, Create a non-appendable log-file and create a writer class using the FileReaders and Writers such that whenever you open a file, once the FileChooser is closed after a successful selection, you write the absolute path of the last accessed directory to the log-file.

Create a reader method that returns the path from the file. Whenever you open a FileChooser user the returned read value as the value to the method setInitialDirectory(the-read-path)

This will ensure that whenever you reopen the FileChooser, even if the last instance was cancelled, it will open the logged entry that was successful. I find that the file reader and writers for non append files are quite faster. You can try out this method if its suitable for your case.

Robert
  • 5,278
  • 43
  • 65
  • 115
briancollins081
  • 845
  • 9
  • 19