DEV Community

loading...
Cover image for From Swing to Jetpack Compose Desktop #2

From Swing to Jetpack Compose Desktop #2

Thomas Künneth
Developer. Speaker. Listener. Loves writing. GDE Android. Confessing mobile computing addict ;-)
Updated on ・3 min read

Welcome to the second post about my journey of transforming a Java Swing app to Jetpack Compose for Desktop. Today I will pick just one topic. That is, start working on it. If you recall the ui, we have a text field which shall contain the base directory to search for duplicate files:

Entering the base directory

The idea is to enter a valid path and then click Find. Obviously the button should be active only if that condition is met. The composable FirstRow currently remembers one state:

val name = remember { mutableStateOf(TextFieldValue("")) }
Enter fullscreen mode Exit fullscreen mode

So we can easily add the button code like this:

Button(
        onClick = {},
        modifier = Modifier.alignByBaseline(),
        enabled = File(name.value.text).isDirectory
) {
    Text("Find")
}
Enter fullscreen mode Exit fullscreen mode

The text field works with the native clipboard, so if you happen to have the path as a string you can paste it with Control-V or Cmd-V. But that's not particularly desktop-py, is it? A key feature of Desktop operating systems is supporting multiple windows. So, we would want to pick the folder from the native file manager, right?

I am a Jetpack Compose for Desktop newbie and may well have missed this, but so far I have not seen drag and drop support. That's why I decided to do this on my own. Jetpack Compose for Desktop top-level windows can easily interact with Swing, so we can borrow the drag and drop capabilities from there. Let's see how this works in general:

val target = object : DropTarget() {
    @Synchronized
    override fun drop(evt: DropTargetDropEvent) {
        try {
            evt.acceptDrop(DnDConstants.ACTION_REFERENCE)
            val droppedFiles = evt
                    .transferable.getTransferData(
                            DataFlavor.javaFileListFlavor) as List<*>
            for (file in droppedFiles) {
                println((file as File).absolutePath)
            }
        } catch (ex: Exception) {
            ex.printStackTrace()
        }
    }
}
AppManager.windows.first().window.contentPane.dropTarget = target
Enter fullscreen mode Exit fullscreen mode

As the DropTarget is Swing stuff I will not elaborate on this. But please keep in mind that this code just prints the names of the dropped files. To get the text field updated, we will need to alter the for loop. More on this soon. But let's take a look at the last line first (no pun intended). AppManager.windows gives us a list of all windows of an application. TKDupeFinder has just one, the main window. So we can get this with first(). This is an instance of androidx.compose.desktop.AppFrame. window is a ComposeWindow which extends JFrame. And that is why we can access contentPane and set a drop target.

Nice, isn't it? To conclude this session, here is how to update the text field. First, the slightly changed main() function:

fun main() {
    invokeLater {
        AppWindow(title = "TKDupeFinder",
                size = IntSize(600, 400)).show {
            TKDupeFinderContent()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

TKDupeFinderContent is a composable. As you will see, it remembers the name and passes it to FirstRow (this is new, too). I do this because upon a drag I need to update name.

@Composable
fun TKDupeFinderContent() {
    val name = remember { mutableStateOf(TextFieldValue("")) }
    DesktopMaterialTheme {
        Column() {
            FirstRow(name)
            SecondRow()
            ThirdRow()
        }
    }
    val target = object : DropTarget() {
        @Synchronized
        override fun drop(evt: DropTargetDropEvent) {
            try {
                evt.acceptDrop(DnDConstants.ACTION_REFERENCE)
                val droppedFiles = evt
                        .transferable.getTransferData(
                                DataFlavor.javaFileListFlavor) as List<*>
                droppedFiles.first()?.let {
                    name.value = TextFieldValue((it as File).absolutePath)
                }
            } catch (ex: Exception) {
                ex.printStackTrace()
            }
        }
    }
    AppManager.windows.first().window.contentPane.dropTarget = target
}

Enter fullscreen mode Exit fullscreen mode

My code assumes that only one file or folder is dragged onto the window. If there are more I just use the first one (droppedFiles.first()). The path (absolutePath) must be wrapped in a TextFieldValue. The following clip shows how this looks on macOS.

Pretty cool, isn't it? The next post will cover the actual search for duplicates. So stay tuned. If you have missed the first part, you can read it here. The TKDupeFinder repo is on GitHub.

Discussion (2)

Collapse
thijsiez profile image
Thijs Koppen • Edited

Hej Thomas, thanks a lot for this write-up, really helped me out a lot! I just updated to v0.4.0 and I feel that the new Window and Dialog composables (with state parameter) break the above code. It worked fine in v0.4.0-build209 which I was using before.

I browsed a bit through the new code and found that those composables provide a WindowScope or DialogScope to the content composable. These contain a window or dialog field, which in turn give us a contentPane. So with a small change to the above code, we can attach to a Window or Dialog's dropTarget like this:

Window(state) {
    window.contentPane.dropTarget = object : DropTarget() {
        ...
    }
}
Enter fullscreen mode Exit fullscreen mode

Hope that helps someone :)

Collapse
tkuenneth profile image
Thomas Künneth Author

Awesome. Thanks for bringing this up. Maybe I should do some sort of follow-up to the series. Thanks. 👍