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:
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("")) }
So we can easily add the button code like this:
Button(
onClick = {},
modifier = Modifier.alignByBaseline(),
enabled = File(name.value.text).isDirectory
) {
Text("Find")
}
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
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()
}
}
}
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
}
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.
Top comments (2)
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 newWindow
andDialog
composables (withstate
parameter) break the above code. It worked fine inv0.4.0-build209
which I was using before.I browsed a bit through the new code and found that those composables provide a
WindowScope
orDialogScope
to the content composable. These contain awindow
ordialog
field, which in turn give us acontentPane
. So with a small change to the above code, we can attach to aWindow
orDialog
'sdropTarget
like this:Hope that helps someone :)
Awesome. Thanks for bringing this up. Maybe I should do some sort of follow-up to the series. Thanks. 👍