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.96k stars 1.16k forks source link

Native support for File Operations #3294

Closed znoraa closed 1 year ago

znoraa commented 1 year ago

Current it lacks components for file operations e.g. a file chooser to select local file/directory, support to drag/drop local file to the window of compose app, which is quite useful in desktop apps. Now have to use other libraries to achieve it (e.g. awt/swing). It'd be great to have native support from Compose

AlexeyTsvetkov commented 1 year ago

It's possible to add drag & drop to any Composable via onExternalDrag (#222). Here is one example on how you can use it:

https://github.com/JetBrains/compose-multiplatform/assets/654232/31f22092-d506-42c6-a2fd-24039f4912b8

@file:OptIn(ExperimentalComposeUiApi::class)

import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.*
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import java.net.URI
import java.nio.file.LinkOption
import java.nio.file.Path
import kotlin.io.path.exists
import kotlin.io.path.toPath

private object Colors {
    val default = Color.Gray
    val active = Color(29, 117, 223, 255)
    val fileItemBg = Color(233, 30, 99, 255)
    val fileItemFg = Color.White
}

@OptIn(ExperimentalComposeUiApi::class)
@Composable
@Preview
fun App() {
    MaterialTheme {
        Box(modifier = Modifier.fillMaxSize().background(Color.White)) {
            Column {
                val dragAndDropModifier = Modifier.padding(horizontal = 4.dp, vertical = 4.dp)
                var droppedFiles by remember { mutableStateOf<List<Path>>(emptyList()) }

                DragAndDropFileBox(dragAndDropModifier.size(height = 200.dp, width = 400.dp)) { dragData ->
                    if (dragData is DragData.FilesList) {
                        val newFiles = dragData.readFiles().mapNotNull {
                            URI(it).toPath().takeIf { it.exists(LinkOption.NOFOLLOW_LINKS) }
                        }
                        droppedFiles = (droppedFiles + newFiles).distinct()
                    }
                }

                FileListView(modifier = dragAndDropModifier, files = droppedFiles)
            }
        }
    }
}

@Composable
private fun FileListView(modifier: Modifier = Modifier, files: List<Path>) {
    LazyColumn(modifier) {
        items(files) {
            Box(
                Modifier.padding(bottom = 5.dp)
                    .background(
                        Colors.fileItemBg, shape = RoundedCornerShape(100.dp)
                    )
            ) {
                Text(
                    text = it.fileName.toString(),
                    color = Colors.fileItemFg,
                    modifier = Modifier.padding(5.dp),
                    fontSize = 14.sp
                )
            }
        }
    }
}

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun DragAndDropFileBox(modifier: Modifier = Modifier, onDrop: (DragData) -> Unit) {
    var isDragging by remember { mutableStateOf(false) }
    val dragNDropColor = if (isDragging) Colors.active else Colors.default

    Box(
        modifier = modifier
            .dashedBorder(color = dragNDropColor, strokeWidth = 2.dp, cornerRadiusDp = 8.dp)
            .onExternalDrag(
                onDragStart = { isDragging = true  },
                onDragExit = { isDragging = false },
                onDrop = { value ->
                    isDragging = false
                    onDrop(value.dragData)
                })
    ) {
        Column(modifier = Modifier.align(Alignment.Center)) {
            DragAndDropDescription(
                modifier = Modifier.align(Alignment.CenterHorizontally),
                color = dragNDropColor
            )
        }
    }
}

@Composable
fun DragAndDropDescription(modifier: Modifier, color: Color) {
    val modifier = modifier.padding(vertical = 2.dp)
    Text(
        "Drag & drop files here",
        fontSize = 14.sp,
        modifier = modifier,
        color = color
    )
}

fun Modifier.dashedBorder(strokeWidth: Dp, color: Color, cornerRadiusDp: Dp) = composed(
    factory = {
        val density = LocalDensity.current
        val strokeWidthPx = density.run { strokeWidth.toPx() }
        val cornerRadiusPx = density.run { cornerRadiusDp.toPx() }

        then(
            Modifier.drawWithCache {
                onDrawBehind {
                    val stroke = Stroke(
                        width = strokeWidthPx,
                        pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
                    )
                    drawRoundRect(
                        color = color,
                        style = stroke,
                        cornerRadius = CornerRadius(cornerRadiusPx)
                    )
                }
            }
        )
    }
)

fun main() = application {
    val windowState = rememberWindowState(width = 600.dp, height = 600.dp)
    Window(onCloseRequest = ::exitApplication, state = windowState) {
        App()
    }
}
AlexeyTsvetkov commented 1 year ago

@znoraa the questions regarding file dialogs have been asked previously. See #176, #197 and #1003 for possible solutions using AWT/Swing. For now, reimlementing file dialogs in Compose or providing bindings for system file dialogs is not on the roadmap.

It's unfortunate, that we don't have a tutorial for working with files. I've created #3309 for that

znoraa commented 1 year ago

@znoraa the questions regarding file dialogs have been asked previously. See #176, #197 and #1003 for possible solutions using AWT/Swing. For now, reimlementing file dialogs in Compose or providing bindings for system file dialogs is not on the roadmap.

It's unfortunate, that we don't have a tutorial for working with files. I've created #3309 for that

@AlexeyTsvetkov Sounds good! Thank you for the example code as well.

okushnikov commented 2 months ago

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