JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
15.9k stars 1.16k forks source link

How to use a FileChooser() in Compose. #1003

Open virogu opened 3 years ago

virogu commented 3 years ago

How to use a FileChooser() in Compose.
The FileDialog() function can not do what i want, it can not select a dictionary or select file wih a filter

virogu commented 3 years ago

I have tried to use the JFileChooser() like this. It did achieve the effect I wanted. But it will be very stuck when it is displayed, and it will take a long time for the file selection box to pop up. `
val jfc = JFileChooser() val path = preferences[prefKey, ""] if (path.isNotEmpty() && File(path).exists()) { jfc.currentDirectory = File(path) } else { jfc.currentDirectory = projectRootPath } jfc.fileSelectionMode = fileChooserType jfc.fileFilter = object : FileFilter() { override fun accept(f: File): Boolean { return when (fileChooserType) { JFileChooser.FILES_ONLY -> { f.isDirectory || f.name.endsWith(filesFilter) } JFileChooser.FILES_AND_DIRECTORIES -> { f.isDirectory || (f.isFile && f.name.endsWith(filesFilter)) } JFileChooser.DIRECTORIES_ONLY -> { f.isDirectory } else -> { false } } }

        override fun getDescription(): String {
            return description
        }
    }
    val status = jfc.showOpenDialog(null)

` image

Genie23 commented 3 years ago

Hello,

I found myself with the same lens just a few days ago. After asking the question (and getting a link to one of the examples that uses jswing dialogs as an answer) I wrote this function :

class Dialog {
    enum class Mode { LOAD, SAVE }
}

@OptIn(DelicateCoroutinesApi::class)
@Composable
fun WindowScope.DialogFile(
    mode:Dialog.Mode = Dialog.Mode.LOAD,
    title:String = "Choisissez un fichier",
    extensions:List<FileNameExtensionFilter> = listOf(),
    onResult: (files: List<File>) -> Unit
) {
    DisposableEffect(Unit) {
        val job = GlobalScope.launch(Dispatchers.Swing) {
            val fileChooser = JFileChooser()
            fileChooser.dialogTitle = title
            fileChooser.isMultiSelectionEnabled = mode == Dialog.Mode.LOAD
            fileChooser.isAcceptAllFileFilterUsed = extensions.isEmpty()
            extensions.forEach { fileChooser.addChoosableFileFilter(it) }

            var returned:Int;
            if(mode == Dialog.Mode.LOAD) {
                returned = fileChooser.showOpenDialog(window)
            }
            else {
                returned = fileChooser.showSaveDialog(window)
            }

            onResult(when(returned) {
                JFileChooser.APPROVE_OPTION -> {
                    if(mode == Dialog.Mode.LOAD) {
                        val files = fileChooser.selectedFiles.filter { it.canRead() }
                        if(files.isNotEmpty()) {
                            files
                        }
                        else {
                            listOf()
                        }
                    } else {
                        if(!fileChooser.fileFilter.accept(fileChooser.selectedFile)) {
                            val ext = (fileChooser.fileFilter as FileNameExtensionFilter).extensions[0]
                            fileChooser.selectedFile = File(fileChooser.selectedFile.absolutePath + ".$ext")
                        }
                        listOf(fileChooser.selectedFile)
                    }
                };
                else -> listOf();
            })
        }

        onDispose {
            job.cancel()
        }
    }
}

However, as it is @Composable, it is not possible to use it in a callback (like the onClick function of a button for example).

Usage (in this example I use a window to retrieve one or more PDF files) :

DialogFile(mode = Dialog.Mode.LOAD, title = "Your dialog title", extensions = listOf(FileNameExtensionFilter("PDF Files", "pdf"))) { files ->
    // files is a List<File> object
}

In LOAD mode, you can select several documents, as long as they exist. It returns a list of File objects corresponding to all the selected files and accessible in reading.

In SAVE mode, you can either choose a file that exists or type in the name you want. If the file does not have an extension that matches the currently selected filter, the extension is added to the entered name. It returns a list of a File object corresponding to the selected file (existing or not). If you want to check if this file exists before overwriting it, you have to do this check yourself using the canRead() method of the File class for example.

And to make your dialog box look native (and this is true for all the swing dialog boxes I've tested so far), just add this at the beginning of your main function :

UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
akurasov commented 3 years ago

@virogu does it work for you?

virogu commented 3 years ago

Hello,

I found myself with the same lens just a few days ago. After asking the question (and getting a link to one of the examples that uses jswing dialogs as an answer) I wrote this function :

class Dialog {
    enum class Mode { LOAD, SAVE }
}

@OptIn(DelicateCoroutinesApi::class)
@Composable
fun WindowScope.DialogFile(
    mode:Dialog.Mode = Dialog.Mode.LOAD,
    title:String = "Choisissez un fichier",
    extensions:List<FileNameExtensionFilter> = listOf(),
    onResult: (files: List<File>) -> Unit
) {
    DisposableEffect(Unit) {
        val job = GlobalScope.launch(Dispatchers.Swing) {
            val fileChooser = JFileChooser()
            fileChooser.dialogTitle = title
            fileChooser.isMultiSelectionEnabled = mode == Dialog.Mode.LOAD
            fileChooser.isAcceptAllFileFilterUsed = extensions.isEmpty()
            extensions.forEach { fileChooser.addChoosableFileFilter(it) }

            var returned:Int;
            if(mode == Dialog.Mode.LOAD) {
                returned = fileChooser.showOpenDialog(window)
            }
            else {
                returned = fileChooser.showSaveDialog(window)
            }

            onResult(when(returned) {
                JFileChooser.APPROVE_OPTION -> {
                    if(mode == Dialog.Mode.LOAD) {
                        val files = fileChooser.selectedFiles.filter { it.canRead() }
                        if(files.isNotEmpty()) {
                            files
                        }
                        else {
                            listOf()
                        }
                    } else {
                        if(!fileChooser.fileFilter.accept(fileChooser.selectedFile)) {
                            val ext = (fileChooser.fileFilter as FileNameExtensionFilter).extensions[0]
                            fileChooser.selectedFile = File(fileChooser.selectedFile.absolutePath + ".$ext")
                        }
                        listOf(fileChooser.selectedFile)
                    }
                };
                else -> listOf();
            })
        }

        onDispose {
            job.cancel()
        }
    }
}

However, as it is @Composable, it is not possible to use it in a callback (like the onClick function of a button for example).

Usage (in this example I use a window to retrieve one or more PDF files) :

DialogFile(mode = Dialog.Mode.LOAD, title = "Your dialog title", extensions = listOf(FileNameExtensionFilter("PDF Files", "pdf"))) { files ->
    // files is a List<File> object
}

In LOAD mode, you can select several documents, as long as they exist. It returns a list of File objects corresponding to all the selected files and accessible in reading.

In SAVE mode, you can either choose a file that exists or type in the name you want. If the file does not have an extension that matches the currently selected filter, the extension is added to the entered name. It returns a list of a File object corresponding to the selected file (existing or not). If you want to check if this file exists before overwriting it, you have to do this check yourself using the canRead() method of the File class for example.

And to make your dialog box look native (and this is true for all the swing dialog boxes I've tested so far), just add this at the beginning of your main function :

UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())

Thanks for your advice The main problem I encountered when using JFileChooser() was lag. I needed to use setCurrentDirectory() to set a default open path. Setting this would cause the dialog box to pop up for a long time, especially when there are many files in the folder.

Genie23 commented 3 years ago

Indeed, JFileChooser tends to lag a bit, mainly on Windows - but file explorer in general tends to lag on my computer too. I used it for my first application to allow the selection of PDF files (my application is used to merge PDFs which can be of different page formats). I also tested it on macOS and two Linux (Fedora and Debian), I had less lags (I had lags, but it's on virtual machines and it wasn't unusual compared to other actions I did on these machines).

akurasov commented 3 years ago

I think any FileChooser application will lag in a huge folder, because it needs to read all files before displaying due to sort order.

virogu commented 3 years ago

I think any FileChooser application will lag in a huge folder, because it needs to read all files before displaying due to sort order.

I have changed my computer system to Win10, and now it won’t be so lagging when it opens. Before that, it was Win11. I guess JFileChooser() will be more lagging on win11.

Genie23 commented 3 years ago

It's true that I hadn't thought about it until now, but on my Windows (also 10), the JFileChooser opens in my My Documents folder, which contains almost 500 items (folders or files) not counting what the folders inside contain, whereas on my virtual machines, of course, I have a lot less stuff (the file chooser opens in the user's folder, which contains about ten folders at most).

So that explains it, even if browsing on Windows, in a sub-folder of My Documents, which contains only a few folders, is also quite slow after all. I think that the list of files of the folders already browsed is maybe kept somewhere in memory, which would explain why a folder that doesn't contain many subfolders and files lags so much after arriving from a folder that contained so many items.

yuchuangu85 commented 2 years ago

I use it like this:

@Composable
fun FileChooserDialog(
    title: String,
    onResult: (result: File) -> Unit
) {
    val fileChooser = JFileChooser(FileSystemView.getFileSystemView())
    fileChooser.currentDirectory = File(System.getProperty("user.dir"))
    fileChooser.dialogTitle = title
    fileChooser.fileSelectionMode = JFileChooser.FILES_AND_DIRECTORIES
    fileChooser.isAcceptAllFileFilterUsed = true
    fileChooser.selectedFile = null
    fileChooser.currentDirectory = null
    if (fileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
        val file = fileChooser.selectedFile
        println("choose file or folder is: $file")
        onResult(file)
    } else {
//        onResult(java.io.File(""))
        println("No Selection ")
    }
}
IARI commented 2 years ago

@Genie23 if you can't use it in a callback, I don't understand how I can even use it at all (probably I'm missing something fundamental about compose here)

aoussou commented 2 years ago

@Genie23 if you can't use it in a callback, I don't understand how I can even use it at all (probably I'm missing something fundamental about compose here)

@IARI It's rather hacky, but based on https://github.com/JetBrains/compose-jb/issues/1003#issuecomment-1001187264 you can do something like

@Composable
@Preview
fun App() {
    var text by remember { mutableStateOf("select file") }
    var selectedFileString by remember { mutableStateOf("") }

    MaterialTheme {
        Column(
            modifier = Modifier.fillMaxSize(),
            horizontalAlignment = Alignment.CenterHorizontally
            ) {
            Button(onClick = {
                selectedFileString = fileChooserDialog(text)
            }) {
                Text(text)
            }

            Text(text = selectedFileString.ifBlank { "no file selected" })
        }
    }

}

fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        App()
    }
}

fun fileChooserDialog(
    title: String?
): String {
    val fileChooser = JFileChooser(FileSystemView.getFileSystemView())
    fileChooser.currentDirectory = File(System.getProperty("user.dir"))
    fileChooser.dialogTitle = title
    fileChooser.fileSelectionMode = JFileChooser.FILES_AND_DIRECTORIES
    fileChooser.isAcceptAllFileFilterUsed = true
    fileChooser.selectedFile = null
    fileChooser.currentDirectory = null
    val file = if (fileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
        fileChooser.selectedFile.toString()
    } else {

        ""

    }

    return file

}
olk90 commented 2 years ago

I've implemented a rudimentary file chooser in compose, that serves my purpose. It's a work-in-progress, though, but is there any chance that we might see a standard-compose file chooser in the future?

https://github.com/olk90/compose-fileChooser

fourlastor commented 1 year ago

I found a way to use native dialogs in compose - https://github.com/fourlastor/nla-editor/pull/17 this PR uses LWJGL's native folder dialogs implementation to show the user a native file picker

virogu commented 1 year ago

It seems that this error has nothing to do with "JFileChooser". " java.lang.IllegalStateException: Already resumed, but proposed with update kotlin.Unit" This error is thrown out by the ”suspendCancellableCoroutine“ method,It usually occurs when repeatedly calls the resume method or call the "resume" method after the scope has been canceled, like this suspend fun test() = suspendCancellableCoroutine<String> { it.resume("") it.resume("") // Maybe throw this error } some code in 'CancellableContinuationImpl' ` private fun resumeImpl( proposedUpdate: Any?, resumeMode: Int, onCancellation: ((cause: Throwable) -> Unit)? = null ) { _state.loop { state -> when (state) { is NotCompleted -> { val update = resumedState(state, proposedUpdate, resumeMode, onCancellation, idempotent = null) if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure detachChildIfNonResuable() dispatchResume(resumeMode) // dispatch resume, but it might get cancelled in process return // done } is CancelledContinuation -> { /*

` ”at androidx.compose.runtime.BroadcastFrameClock$FrameAwaiter.resume(BroadcastFrameClock.kt:42)“

It may be a problem with the compose itself

okushnikov commented 2 weeks ago

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.