diff --git a/src/main/kotlin/fr/quentixx/kfilebuilder/components/Icons.kt b/src/main/kotlin/fr/quentixx/kfilebuilder/components/Icons.kt index 26daa07..044b48e 100644 --- a/src/main/kotlin/fr/quentixx/kfilebuilder/components/Icons.kt +++ b/src/main/kotlin/fr/quentixx/kfilebuilder/components/Icons.kt @@ -1,5 +1,8 @@ package fr.quentixx.kfilebuilder.components +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.size import androidx.compose.material.Icon @@ -27,27 +30,37 @@ fun IconDir() = Icon( @Composable fun TemplateAddDirIcon( onClick: () -> Unit, - size: Dp = 20.dp -) = Icon( - painterResource("icons/Template_AddDirectory.png"), + size: Dp = 30.dp +) = Image( + painterResource("icons/Icon_NewDirectory.png"), null, Modifier .size(size) .clickable { onClick.invoke() } .setOnHoverHandCursorEnabled(), - Color.Black ) @Composable fun TemplateAddFileIcon( onClick: () -> Unit, - size: Dp = 20.dp -) = Icon( - painterResource("icons/Template_AddFile.png"), + size: Dp = 32.dp +) = Image( + painterResource("icons/Icon_NewFile.png"), + null, Modifier + .size(size) + .clickable { onClick.invoke() } + .setOnHoverHandCursorEnabled(), +) + +@Composable +fun SearchDirectoryIcon( + onClick: () -> Unit, + size: Dp = 30.dp +) = Image( + painterResource("icons/Icon_SearchDirectory.png"), null, Modifier .size(size) .clickable { onClick.invoke() } .setOnHoverHandCursorEnabled(), - Color.Black ) @Composable @@ -55,7 +68,7 @@ fun IconArrowDown( size: Dp = 10.dp ) = Icon( painterResource("icons/TreeArrow_Down.png"), - null, Modifier.size(size), Color.LightGray + null, Modifier.size(size), Color.Black ) @Composable @@ -63,7 +76,7 @@ fun IconArrowRight( size: Dp = 10.dp ) = Icon( painterResource("icons/TreeArrow_Right.png"), - null, Modifier.size(size), Color.LightGray + null, Modifier.size(size), Color.Black ) @Composable diff --git a/src/main/kotlin/fr/quentixx/kfilebuilder/treeview/TreeViewBuilder.kt b/src/main/kotlin/fr/quentixx/kfilebuilder/treeview/TreeViewBuilder.kt index f6c9853..18277bd 100644 --- a/src/main/kotlin/fr/quentixx/kfilebuilder/treeview/TreeViewBuilder.kt +++ b/src/main/kotlin/fr/quentixx/kfilebuilder/treeview/TreeViewBuilder.kt @@ -1,28 +1,41 @@ package fr.quentixx.kfilebuilder.treeview import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.text.BasicTextField import androidx.compose.material.Button import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import fr.quentixx.kfilebuilder.components.TemplateAddDirIcon -import fr.quentixx.kfilebuilder.components.TemplateAddFileIcon +import fr.quentixx.kfilebuilder.components.* import fr.quentixx.kfilebuilder.ext.setOnHoverHandCursorEnabled import fr.quentixx.kfilebuilder.data.Node import java.io.File +private data class NodeRowData( + val node: MutableState, + val hovered: Boolean = false, +) + +private fun interface NodeRowScope { + @Composable + fun consume(data: MutableState) +} + /** * Shows a tree view builder. */ @@ -33,7 +46,7 @@ fun TreeViewBuilder(mutableNode: MutableState) { Box( Modifier .fillMaxSize() - .background(Color.DarkGray) + .background(Color.LightGray) ) { LazyColumn( state = listState, @@ -62,7 +75,11 @@ fun TreeViewBuilder(mutableNode: MutableState) { */ @Composable private fun SourceNodeLine(mutableNode: MutableState) { - Row { + Spacer(Modifier.height(4.dp)) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(Modifier.width(16.dp)) RenderNodeElement(mutableNode, true) { Spacer(Modifier.width(32.dp)) it.value.apply { @@ -74,16 +91,23 @@ private fun SourceNodeLine(mutableNode: MutableState) { } @Composable -fun DirectoryNodeControls(node: MutableState) { - Spacer(Modifier.width(32.dp)) +private fun DirectoryNodeControls(node: MutableState) { + val NEW_DIR_PATH = "Nouveau dossier" + val NEW_FILE_PATH = "Nouveau fichier" + Spacer(Modifier.width(32.dp)) TemplateAddDirIcon( onClick = { + val idOfDirectory = node.value.children.count { it.path.contains(NEW_DIR_PATH) } + val nodeName = NEW_DIR_PATH + if (idOfDirectory > 0) { + " $idOfDirectory" + } else "" + node.value = node.value.copy( lastUpdated = System.currentTimeMillis() ).apply { children.add( - Node("New dir", true) + Node(nodeName, true) ) } } @@ -93,11 +117,16 @@ fun DirectoryNodeControls(node: MutableState) { TemplateAddFileIcon( onClick = { + val idOFile = node.value.children.count { it.path.contains(NEW_FILE_PATH) } + val nodeName = NEW_FILE_PATH + if (idOFile > 0) { + " $idOFile" + } else "" + node.value = node.value.copy( lastUpdated = System.currentTimeMillis() ).apply { children.add( - Node("New file", false) + Node( nodeName, false) ) } } @@ -111,7 +140,7 @@ private fun DeleteNodeButton(nodeRow: MutableState) { // TODO: Delete the node from parent children }, modifier = Modifier - .size(18.dp) + .size(30.dp) .setOnHoverHandCursorEnabled() ) { Icon( @@ -125,16 +154,11 @@ private fun DeleteNodeButton(nodeRow: MutableState) { @Composable private fun SelectNodeSourceButton(node: MutableState) { val isOverlayVisible = remember { mutableStateOf(false) } - Button( + SearchDirectoryIcon( onClick = { isOverlayVisible.value = true }, - modifier = Modifier - .size(80.dp, 32.dp) - .setOnHoverHandCursorEnabled() - ) { - Text(text = "Select") - } + ) if (isOverlayVisible.value) { OpenTreeViewSelectorWindow(node, onClose = { isOverlayVisible.value = false }) @@ -170,3 +194,109 @@ private fun OpenTreeViewSelectorWindow( } } + +@Composable +private fun NodeList( + mutableNode: MutableState, + paddingSave: Dp = 16.dp, + nodeRowScope: @Composable NodeRowScope +) { + val node = mutableNode.value + + Column { + node.children.forEach { + val mutableChild = remember { mutableStateOf(it) } + + NodeRow(mutableChild, paddingSave, nodeRowScope) + + // Update the current children in the loop with the new children information + mutableChild.value.apply { + it.path = path + it.lastUpdated = lastUpdated + } + } + } +} + +@Composable +private fun NodeRow( + mutableNode: MutableState, + paddingSave: Dp = 0.dp, + nodeRowConsumer: @Composable NodeRowScope +) { + var expanded by remember { mutableStateOf(true) } + val padding = paddingSave + 16.dp + val node = mutableNode.value + val isDir = node.isDirectory + val isEmptyDir = isDir && node.children.isEmpty() + + val interactionSource = remember { MutableInteractionSource() } + val isHovered = interactionSource.collectIsHoveredAsState().value + + val data = remember { mutableStateOf(NodeRowData(mutableNode, isHovered)) } + Row( + modifier = Modifier + .fillMaxWidth() + .height(36.dp) + .padding(start = padding) + .clickable { + if (!isEmptyDir) expanded = !expanded + } + .hoverable(interactionSource), + verticalAlignment = Alignment.CenterVertically + ) { + data.value = data.value.copy(hovered = isHovered) + + RenderNodeElement(mutableNode, expanded, data, nodeRowConsumer) + } + + if (expanded && node.isDirectory) { + NodeList(mutableNode, padding, nodeRowConsumer) + } +} + +@Composable +private fun RenderNodeElement( + mutableNode: MutableState, + expanded: Boolean, + data: MutableState = mutableStateOf(NodeRowData(mutableNode, false)), + nodeRowConsumer: @Composable NodeRowScope +) { + val node = mutableNode.value + var spacingSize = 8.dp + + if (node.children.isEmpty()) { + spacingSize *= 2 + } + + if (node.isDirectory) { + if (node.children.isNotEmpty()) { + if (expanded) IconArrowDown(8.dp) + else IconArrowRight(8.dp) + } + Spacer(Modifier.width(spacingSize)) + IconDir() + } else { + Spacer(Modifier.width(spacingSize)) + IconFile() + } + + Spacer(Modifier.width(8.dp)) + EditableHighlightedText(mutableNode) + + nodeRowConsumer.consume(data) +} + +@Composable +private fun EditableHighlightedText(mutableNode: MutableState) { + BasicTextField( + value = mutableNode.value.path, + onValueChange = { newText -> + mutableNode.value = mutableNode.value.copy( + path = newText, + lastUpdated = System.currentTimeMillis() + ) + }, + singleLine = true, + ) +} diff --git a/src/main/kotlin/fr/quentixx/kfilebuilder/treeview/TreeViewFileList.kt b/src/main/kotlin/fr/quentixx/kfilebuilder/treeview/TreeViewFileList.kt index d5293a6..efc0234 100644 --- a/src/main/kotlin/fr/quentixx/kfilebuilder/treeview/TreeViewFileList.kt +++ b/src/main/kotlin/fr/quentixx/kfilebuilder/treeview/TreeViewFileList.kt @@ -26,6 +26,7 @@ import fr.quentixx.kfilebuilder.components.IconDir import fr.quentixx.kfilebuilder.components.IconFile import fr.quentixx.kfilebuilder.ext.hasNoSubDirectories import fr.quentixx.kfilebuilder.data.Node +import fr.quentixx.kfilebuilder.ext.setOnHoverHandCursorEnabled import java.io.File @Composable @@ -44,29 +45,6 @@ fun FileList( } } -@Composable -fun NodeList( - mutableNode: MutableState, - paddingSave: Dp = 0.dp, - nodeRowScope: @Composable NodeRowScope -) { - val node = mutableNode.value - - Column { - node.children.forEach { - val mutableChild = remember { mutableStateOf(it) } - - NodeRow(mutableChild, paddingSave, nodeRowScope) - - // Update the current children in the loop with the new children information - mutableChild.value.apply { - it.path = path - it.lastUpdated = lastUpdated - } - } - } -} - @Composable fun FileRow( currentSelectedFile: MutableState, @@ -127,113 +105,3 @@ fun FileRow( FileList(currentSelectedFile, file, onlyDirectories, padding) } } - -data class NodeRowData( - val node: MutableState, - val hovered: Boolean = false, -) - -fun interface NodeRowScope { - @Composable - fun consume(data: MutableState) -} - -@Composable -fun NodeRow( - mutableNode: MutableState, - paddingSave: Dp = 0.dp, - nodeRowConsumer: @Composable NodeRowScope -) { - var expanded by remember { mutableStateOf(false) } - val padding = paddingSave + 16.dp - val node = mutableNode.value - val isDir = node.isDirectory - val isEmptyDir = isDir && node.children.isEmpty() - - val interactionSource = remember { MutableInteractionSource() } - val isHovered = interactionSource.collectIsHoveredAsState().value - - val data = remember { mutableStateOf(NodeRowData(mutableNode, isHovered)) } - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = padding) - .clickable { - if (!isEmptyDir) expanded = !expanded - } - .hoverable(interactionSource), - verticalAlignment = Alignment.CenterVertically - ) { - - data.value = data.value.copy(hovered = isHovered) - - RenderNodeElement(mutableNode, expanded, data, nodeRowConsumer) - } - - if (expanded && node.isDirectory) { - NodeList(mutableNode, padding, nodeRowConsumer) - } -} - -@Composable -fun RenderNodeElement( - mutableNode: MutableState, - expanded: Boolean, - data: MutableState = mutableStateOf(NodeRowData(mutableNode, false)), - nodeRowConsumer: @Composable NodeRowScope -) { - val node = mutableNode.value - var spacingSize = 8.dp - - if (node.children.isEmpty()) { - spacingSize *= 2 - } - - if (node.isDirectory) { - if (node.children.isNotEmpty()) { - if (expanded) IconArrowDown(8.dp) - else IconArrowRight(8.dp) - } - Spacer(Modifier.width(spacingSize)) - IconDir() - } else { - Spacer(Modifier.width(spacingSize)) - IconFile() - } - - Spacer(Modifier.width(8.dp)) - EditableHighlightedText(mutableNode) - - nodeRowConsumer.consume(data) -} - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -fun EditableHighlightedText(mutableNode: MutableState) { - val keyboardController = LocalSoftwareKeyboardController.current - - BasicTextField( - value = mutableNode.value.path, - onValueChange = { newText -> - mutableNode.value = mutableNode.value.copy( - path = newText, - lastUpdated = System.currentTimeMillis() - ) - }, - keyboardActions = KeyboardActions(onDone = { - // Cacher le clavier lors de l'appui sur "Entrée" - keyboardController?.hide() - }), - singleLine = true, - modifier = Modifier - .onPreviewKeyEvent { - if (it.key == Key.Enter) { - keyboardController?.hide() // Cacher le clavier lorsque "Entrée" est pressée - true // Empêcher la propagation de l'événement de clavier - } else { - false // Laisser passer les autres touches - } - } - ) -} diff --git a/src/main/resources/icons/Icon_NewDirectory.png b/src/main/resources/icons/Icon_NewDirectory.png new file mode 100644 index 0000000..26b6fcf Binary files /dev/null and b/src/main/resources/icons/Icon_NewDirectory.png differ diff --git a/src/main/resources/icons/Icon_NewFile.png b/src/main/resources/icons/Icon_NewFile.png new file mode 100644 index 0000000..eefdc66 Binary files /dev/null and b/src/main/resources/icons/Icon_NewFile.png differ diff --git a/src/main/resources/icons/Icon_SearchDirectory.png b/src/main/resources/icons/Icon_SearchDirectory.png new file mode 100644 index 0000000..d3dca7b Binary files /dev/null and b/src/main/resources/icons/Icon_SearchDirectory.png differ