From 96456b0fb8ec699bd0c1b62661b9b06dcf808582 Mon Sep 17 00:00:00 2001 From: Robin Meersman Date: Sun, 24 Nov 2024 18:34:48 +0000 Subject: [PATCH] feat: rename and move file/directory support in filetree --- .../local/filemanager/FileManagerLocal.kt | 1 - .../writand/screens/components/DragAndDrop.kt | 183 --------------- .../re/writand/screens/components/PopUps.kt | 57 +++++ .../DirectoryProviderScreen.kt | 14 +- .../DirectoryProviderViewModel.kt | 15 +- .../re/writand/screens/filetree/Filetree.kt | 10 +- .../screens/filetree/WFiletreeViewModel.kt | 14 +- .../screens/filetree/WTreeComponent.kt | 209 +++++++++++++++--- .../re/writand/utils/WFileTreeAbstraction.kt | 81 +++++-- .../drawable/baseline_drive_file_move_16.xml | 5 + .../res/drawable/baseline_more_horiz_16.xml | 5 + 11 files changed, 350 insertions(+), 244 deletions(-) delete mode 100644 app/src/main/java/be/re/writand/screens/components/DragAndDrop.kt create mode 100644 app/src/main/res/drawable/baseline_drive_file_move_16.xml create mode 100644 app/src/main/res/drawable/baseline_more_horiz_16.xml diff --git a/app/src/main/java/be/re/writand/data/local/filemanager/FileManagerLocal.kt b/app/src/main/java/be/re/writand/data/local/filemanager/FileManagerLocal.kt index 3a60c69..d14a4ff 100644 --- a/app/src/main/java/be/re/writand/data/local/filemanager/FileManagerLocal.kt +++ b/app/src/main/java/be/re/writand/data/local/filemanager/FileManagerLocal.kt @@ -5,7 +5,6 @@ import be.re.writand.utils.GenerateId import be.re.writand.utils.WFileTreeAbstraction import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import okio.Path.Companion.toPath import java.io.File import java.io.FileFilter import java.nio.file.Files diff --git a/app/src/main/java/be/re/writand/screens/components/DragAndDrop.kt b/app/src/main/java/be/re/writand/screens/components/DragAndDrop.kt deleted file mode 100644 index a7821bf..0000000 --- a/app/src/main/java/be/re/writand/screens/components/DragAndDrop.kt +++ /dev/null @@ -1,183 +0,0 @@ -package be.re.writand.screens.components - -import android.util.Log -import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.boundsInWindow -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.dp -import androidx.compose.ui.zIndex - -/** - * Currently this is unused, but the goal is to implement this feature into the filetree. - * based on: [this video](https://www.youtube.com/watch?v=ST99k8qK6SM) - */ - -internal class DragTargetInfo { - var isDragging: Boolean by mutableStateOf(false) - var dragPosition by mutableStateOf(Offset.Zero) - var dragOffset by mutableStateOf(Offset.Zero) - var draggableComposable by mutableStateOf<(@Composable () -> Unit)?>(null) - var dataToDrop by mutableStateOf(null) -} - -internal val LocalDragTargetInfo = compositionLocalOf { DragTargetInfo() } - -@Composable -fun DragTarget( - modifier: Modifier = Modifier, - dataToDrop: T, - onStartDragging: () -> Unit, - onStopDragging: () -> Unit, - content: @Composable () -> Unit -) { - var currentPosition by remember { mutableStateOf(Offset.Zero) } - val currentState = LocalDragTargetInfo.current - - Box( - modifier = modifier - .onGloballyPositioned { - currentPosition = it.windowToLocal(Offset.Zero) - } - .pointerInput(Unit) { - detectDragGesturesAfterLongPress( - onDragStart = { - onStartDragging() - currentState.dataToDrop = dataToDrop - currentState.isDragging = true - currentState.dragPosition = currentPosition + it - currentState.draggableComposable = content - - Log.d("DRAGGING", "states has been set") - }, - onDrag = { change, dragAmount -> - change.consume() - currentState.dragOffset += dragAmount - }, - onDragEnd = { - onStopDragging() - currentState.dragPosition = Offset.Zero - currentState.isDragging = false - }, - onDragCancel = { - onStopDragging() - currentState.dragPosition = Offset.Zero - currentState.isDragging = false - } - ) - } - ) { - content() - } -} - -@Composable -fun DropItem( - modifier: Modifier = Modifier, - content: @Composable (BoxScope.(isInBound: Boolean, data: T?) -> Unit) -) { - val dragInfo = LocalDragTargetInfo.current - val dragPosition = dragInfo.dragPosition - val dragOffset = dragInfo.dragOffset - var isCurrentDropTarget by remember { mutableStateOf(false) } - - Box( - modifier = modifier.onGloballyPositioned { - it.boundsInWindow().let { rect -> - isCurrentDropTarget = rect.contains(dragPosition + dragOffset) - } - } - ) { - val data = - if (isCurrentDropTarget && !dragInfo.isDragging) dragInfo.dataToDrop as T? else null - content(isCurrentDropTarget, data) - } -} - -@Composable -fun DraggableScreen( - modifier: Modifier = Modifier, - content: @Composable BoxScope.() -> Unit -) { - val state = remember { DragTargetInfo() } - CompositionLocalProvider( - LocalDragTargetInfo provides state - ) { - Box(modifier = modifier.fillMaxSize()) { - content() - if (state.isDragging) { - var targetSize by remember { mutableStateOf(IntSize.Zero) } - Box(modifier = Modifier - .graphicsLayer { - val offset = state.dragPosition + state.dragOffset - scaleX = 1.3f - scaleY = 1.3f - alpha = if (targetSize == IntSize.Zero) 0f else .9f - translationX = offset.x.minus(targetSize.width / 2) - translationY = offset.y.minus(targetSize.height / 2) - } - .onGloballyPositioned { - targetSize = it.size - }) { - state.draggableComposable?.invoke() - } - } - } - } -} - -@Composable -fun DraggableLazyColumnScreen( - modifier: Modifier = Modifier, - content: LazyListScope.() -> Unit -) { - val state = remember { DragTargetInfo() } - CompositionLocalProvider( - LocalDragTargetInfo provides state - ) { - val listState = rememberLazyListState() - LazyColumn( - state = listState, - modifier = modifier.fillMaxSize(), - contentPadding = PaddingValues(10.dp) - ) { - content() - } - if(state.isDragging) { - var targetSize by remember { mutableStateOf(IntSize.Zero) } - Box( - modifier = Modifier - .zIndex(1.0f) - .graphicsLayer { - val offset = state.dragPosition + state.dragOffset - Offset(0f, listState.firstVisibleItemScrollOffset.toFloat()) - alpha = if (targetSize == IntSize.Zero) 0f else .9f - translationX = offset.x - targetSize.width / 2 - translationY = offset.y - targetSize.height / 2 - } - .onGloballyPositioned { - Log.d("DRAGGING", "original targetsize = $targetSize, new value = ${it.size}") - targetSize = it.size - }) { - state.draggableComposable?.invoke() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/screens/components/PopUps.kt b/app/src/main/java/be/re/writand/screens/components/PopUps.kt index af41ab3..3bb8503 100644 --- a/app/src/main/java/be/re/writand/screens/components/PopUps.kt +++ b/app/src/main/java/be/re/writand/screens/components/PopUps.kt @@ -203,3 +203,60 @@ fun RemoveFilePopUp(filename: String, hidePopUp: () -> Unit, onConfirm: () -> Un } } } + +@Composable +fun RenameFilePopUp(oldName: String, hidePopUp: () -> Unit, onConfirm: (String) -> Unit) { + val (newName, setNewName) = remember { mutableStateOf(oldName) } + WPopup( + titleBar = { + TopPopUpBar( + title = "Are you sure?", + showExit = false + ) + }, + bottomBar = { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + WBorderButton( + text = "Cancel", + onClick = hidePopUp + ) + + WButton( + text = "Rename", + onClick = { + onConfirm(newName) + hidePopUp() + } + ) + } + }, + height = 200.dp, + width = 500.dp, + onDismiss = hidePopUp + ) { padding -> + Column( + verticalArrangement = Arrangement.Center, + modifier = Modifier + .padding(padding) + .fillMaxSize() + ) { + Row( + modifier = Modifier.padding(10.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + WLabelAndTextField( + title = "Filename", + value = newName, + onTextChange = { setNewName(it) } + ) + } + } + } +} diff --git a/app/src/main/java/be/re/writand/screens/directoryprovider/DirectoryProviderScreen.kt b/app/src/main/java/be/re/writand/screens/directoryprovider/DirectoryProviderScreen.kt index 8508567..69e7ed1 100644 --- a/app/src/main/java/be/re/writand/screens/directoryprovider/DirectoryProviderScreen.kt +++ b/app/src/main/java/be/re/writand/screens/directoryprovider/DirectoryProviderScreen.kt @@ -23,14 +23,13 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Close -import androidx.compose.material3.Button import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -54,6 +53,11 @@ import be.re.writand.screens.components.WPopup import be.re.writand.screens.components.WText import be.re.writand.ui.theme.MainGreen +enum class WProviderType { + OpenDirectoryProvider, + MoveFileProvider +} + @Composable private fun TopBar(onDismiss: () -> Unit, onBack: () -> Unit) { Row( @@ -167,9 +171,15 @@ private fun ShortcutColumnItem(name: String, icon: Int, onClick: () -> Unit) { @Composable fun DirectoryProvider( vm: DirectoryProviderViewModel = hiltViewModel(), + type: WProviderType = WProviderType.OpenDirectoryProvider, + root: String = "/storage/emulated/0", onDismiss: () -> Unit, onConfirm: (String) -> Unit ) { + LaunchedEffect(Unit) { + vm.initProvider(root) + } + // The shortcuts used in the sidebar, each element is a pair that links the name to its path val shortcuts = mapOf( Pair("Documents", Environment.DIRECTORY_DOCUMENTS), diff --git a/app/src/main/java/be/re/writand/screens/directoryprovider/DirectoryProviderViewModel.kt b/app/src/main/java/be/re/writand/screens/directoryprovider/DirectoryProviderViewModel.kt index 281ddbd..013de9f 100644 --- a/app/src/main/java/be/re/writand/screens/directoryprovider/DirectoryProviderViewModel.kt +++ b/app/src/main/java/be/re/writand/screens/directoryprovider/DirectoryProviderViewModel.kt @@ -1,5 +1,6 @@ package be.re.writand.screens.directoryprovider +import android.util.Log import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import be.re.writand.data.local.filemanager.FileType @@ -24,18 +25,22 @@ class DirectoryProviderViewModel @Inject constructor( private val listDirectoriesUseCase: ListDirectoriesUseCase, private val createFileUseCase: CreateFileUseCase ) : WViewModel() { - private val root = "/storage/emulated/0" + private lateinit var root: String val separator: String = File.separator private val _uiState = MutableStateFlow(DirectoryProviderUiState.Empty) val uiState: StateFlow = _uiState - val currentPath = mutableStateListOf(root) + val currentPath = mutableStateListOf() val selectedItem = mutableStateOf(null) - init { - listCurrentDirectory() + fun initProvider(path: String = "/storage/emulated/0") { + if (!this::root.isInitialized) { + root = path + currentPath.add(root) + listCurrentDirectory() + } } fun createNewDirectory(name: String) { @@ -73,7 +78,7 @@ class DirectoryProviderViewModel @Inject constructor( fun moveUp() { if (currentPath.size > 1) { - currentPath.removeLast() + currentPath.removeAt(currentPath.lastIndex) selectedItem.value = null listCurrentDirectory() } diff --git a/app/src/main/java/be/re/writand/screens/filetree/Filetree.kt b/app/src/main/java/be/re/writand/screens/filetree/Filetree.kt index 9228589..b8a378d 100644 --- a/app/src/main/java/be/re/writand/screens/filetree/Filetree.kt +++ b/app/src/main/java/be/re/writand/screens/filetree/Filetree.kt @@ -2,19 +2,13 @@ package be.re.writand.screens.filetree import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Info -import androidx.compose.material.icons.filled.Settings -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable @@ -144,8 +138,10 @@ fun WFiletree( isDirectory = { id -> vm.isDirectory(id) }, getIsOpened = { id -> vm.getIsOpened(id) }, getPath = { id -> vm.getPath(id) }, + findNode = { p -> vm.findNode(p) }, + projectRoot = root, toggleIsOpened = { id -> vm.toggleOpen(id) }, - onMove = { from, to -> vm.moveFile(from, to) }, + onMove = { from, to, name -> vm.moveFile(from, to, name) }, onCreate = { node, name, type -> vm.createFile(name, type, node) }, onDelete = { node -> vm.removeFile(node) }, onSelect = onSelect diff --git a/app/src/main/java/be/re/writand/screens/filetree/WFiletreeViewModel.kt b/app/src/main/java/be/re/writand/screens/filetree/WFiletreeViewModel.kt index 9019037..385e6c4 100644 --- a/app/src/main/java/be/re/writand/screens/filetree/WFiletreeViewModel.kt +++ b/app/src/main/java/be/re/writand/screens/filetree/WFiletreeViewModel.kt @@ -100,16 +100,24 @@ class WFiletreeViewModel @Inject constructor( } } - fun moveFile(from: FileNode, to: FileNode) { + fun moveFile(from: FileNode, to: FileNode, name: String) { if (!this::fileTree.isInitialized) return val fromPath = fileTree.getPath(from.item)!! - val toPath = fileTree.getPath(to.item)!! + val toPath = if (from != to) { + Paths.get(fileTree.getPath(to.item)!!.toString(), name) + } else { + Paths.get(fileTree.getPath(from.item)!!.parent.toString(), name) + } - fileTree.move(from, to) + fileTree.move(from, to, name) launchCatching { withContext(Dispatchers.IO) { moveFileUseCase(fromPath, toPath) } } } + + fun findNode(path: String): FileNode? { + return fileTree.find(Paths.get(path)) + } } \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/screens/filetree/WTreeComponent.kt b/app/src/main/java/be/re/writand/screens/filetree/WTreeComponent.kt index 666f320..b821d7d 100644 --- a/app/src/main/java/be/re/writand/screens/filetree/WTreeComponent.kt +++ b/app/src/main/java/be/re/writand/screens/filetree/WTreeComponent.kt @@ -1,47 +1,52 @@ package be.re.writand.screens.filetree +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp +import be.re.writand.R import be.re.writand.data.local.filemanager.FileType import be.re.writand.screens.WUIGlobals import be.re.writand.screens.components.CreateFilePopUp import be.re.writand.screens.components.CreationType import be.re.writand.screens.components.RemoveFilePopUp -import be.re.writand.screens.components.WBorderButton -import be.re.writand.screens.components.WButton -import be.re.writand.screens.components.WCheckbox -import be.re.writand.screens.components.WDangerButton -import be.re.writand.screens.components.WLabelAndTextField +import be.re.writand.screens.components.RenameFilePopUp import be.re.writand.screens.components.WPopup import be.re.writand.screens.components.WText +import be.re.writand.screens.directoryprovider.DirectoryProvider +import be.re.writand.screens.directoryprovider.WProviderType import be.re.writand.utils.FileNode import java.nio.file.Path -// TODO: add drag and drop functionality to move files from 1 place to another - // based on: https://stackoverflow.com/a/71709816 /** @@ -52,6 +57,7 @@ import java.nio.file.Path * @param[isDirectory] a function to determine if the node stands for a file or a directory. * @param[getIsOpened] a function to access the state for a given node representing a directory. * @param[getPath] a function to retrieve the path of a node. + * @param[findNode] a function to search the filetree for the node corresponding to a given path. * @param[toggleIsOpened] the state changer function, toggles between opened and closed. * @param[onMove] the action function to move a file / directory from a to b. * @param[onCreate] the action function to create a new file / directory. @@ -61,13 +67,15 @@ import java.nio.file.Path @Composable fun WTreeComponent( root: FileNode, + projectRoot: String, modifier: Modifier = Modifier, getFilename: (ULong) -> String?, isDirectory: (ULong) -> Boolean, getIsOpened: (ULong) -> Boolean, getPath: (ULong) -> Path?, + findNode: (String) -> FileNode?, toggleIsOpened: (ULong) -> Unit, - onMove: (FileNode, FileNode) -> Unit, + onMove: (FileNode, FileNode, String) -> Unit, onCreate: (FileNode, String, FileType) -> Unit, onDelete: (FileNode) -> Unit, onSelect: (Path?) -> Unit @@ -78,6 +86,8 @@ fun WTreeComponent( depth = 0, getFilename = getFilename, getPath = getPath, + findNode = findNode, + projectRoot = projectRoot, isDirectory = isDirectory, getIsOpened = getIsOpened, toggleIsOpened = toggleIsOpened, @@ -100,10 +110,12 @@ fun LazyListScope.WTreeItems( depth: Int, getFilename: (ULong) -> String?, getPath: (ULong) -> Path?, + findNode: (String) -> FileNode?, + projectRoot: String, isDirectory: (ULong) -> Boolean, getIsOpened: (ULong) -> Boolean, toggleIsOpened: (ULong) -> Unit, - onMove: (FileNode, FileNode) -> Unit, + onMove: (FileNode, FileNode, String) -> Unit, onCreate: (FileNode, String, FileType) -> Unit, onDelete: (FileNode) -> Unit, onSelect: (Path?) -> Unit @@ -114,6 +126,8 @@ fun LazyListScope.WTreeItems( depth = depth, getFilename = getFilename, getPath = getPath, + findNode = findNode, + projectRoot = projectRoot, isDirectory = isDirectory, getIsOpened = getIsOpened, toggleIsOpened = toggleIsOpened, @@ -135,10 +149,12 @@ fun LazyListScope.WTreeItem( depth: Int, getFilename: (ULong) -> String?, getPath: (ULong) -> Path?, + findNode: (String) -> FileNode?, + projectRoot: String, isDirectory: (ULong) -> Boolean, getIsOpened: (ULong) -> Boolean, toggleIsOpened: (ULong) -> Unit, - onMove: (FileNode, FileNode) -> Unit, + onMove: (FileNode, FileNode, String) -> Unit, onCreate: (FileNode, String, FileType) -> Unit, onDelete: (FileNode) -> Unit, onSelect: (Path?) -> Unit @@ -149,6 +165,10 @@ fun LazyListScope.WTreeItem( item { val (showAddPopUp, setShowAddPopUp) = remember { mutableStateOf(false) } val (showRemovePopUp, setShowRemovePopUp) = remember { mutableStateOf(false) } + val (isShowMore, setIsShowMore) = remember { mutableStateOf(false) } + val (showMovePopUp, setShowMovePopUp) = remember { mutableStateOf(false) } + val (showRenamePopUp, setShowRenamePopUp) = remember { mutableStateOf(false) } + Row { Filename( root = root, @@ -160,32 +180,151 @@ fun LazyListScope.WTreeItem( toggleIsOpened = toggleIsOpened, onSelect = onSelect ) - if (isDir) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = "Add a file or directory", - modifier = Modifier - .size(WUIGlobals.treeIconSize) - .clickable { - setShowAddPopUp(true) - } - ) - } + + Spacer(modifier = Modifier.width(10.dp)) + Icon( - imageVector = Icons.Default.Delete, - contentDescription = "Delete the current file or directory", + painter = painterResource(R.drawable.baseline_more_horiz_16), + contentDescription = "Show more", modifier = Modifier .size(WUIGlobals.treeIconSize) .clickable { - setShowRemovePopUp(true) + setIsShowMore(true) } ) + + if (isShowMore) WPopup( + width = 300.dp, + height = 200.dp, + onDismiss = { setIsShowMore(false) }, + titleBar = { /* Left empty on purpose */ } + ) { padding -> + Column( + verticalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxSize() + .padding(padding) + .padding(10.dp) + ) { + if (isDir) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(horizontal = 0.dp, vertical = 10.dp) + .clickable { + setIsShowMore(false) + setShowAddPopUp(true) + } + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = "Add a file/directory", + modifier = Modifier + .size(WUIGlobals.iconSize) + ) + WText( + text = "Add a file/directory", + modifier = Modifier + .padding(horizontal = 5.dp) + ) + } + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(horizontal = 0.dp, vertical = 10.dp) + .clickable { + setIsShowMore(false) + setShowMovePopUp(true) + } + ) { + Icon( + painter = painterResource(R.drawable.baseline_drive_file_move_16), + contentDescription = "Move file/directory", + modifier = Modifier + .size(WUIGlobals.iconSize) + ) + + WText( + text = "Move $filename", + modifier = Modifier + .padding(horizontal = 5.dp) + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(horizontal = 0.dp, vertical = 10.dp) + .clickable { + setIsShowMore(false) + setShowRenamePopUp(true) + } + ) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = "Rename file/directory", + modifier = Modifier + .size(WUIGlobals.iconSize) + ) + + WText( + text = "Rename $filename", + modifier = Modifier + .padding(horizontal = 5.dp) + ) + } + + Column { + HorizontalDivider( + modifier = Modifier.padding( + horizontal = 0.dp, + vertical = 5.dp + ) + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(WUIGlobals.cornerRadius)) + .background(Color.Red) + .padding(horizontal = 5.dp, vertical = 10.dp) + .clickable { + setIsShowMore(false) + setShowRemovePopUp(true) + } + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Delete", + modifier = Modifier + .size(WUIGlobals.iconSize) + ) + + WText( + text = "Delete $filename", + color = Color.White, + modifier = Modifier.padding(horizontal = 5.dp) + ) + } + } + } + } + } + if (showRenamePopUp) RenameFilePopUp( + oldName = filename, + hidePopUp = { setShowRenamePopUp(false) }, + onConfirm = { name -> onMove(root, root, name) } + ) + if (showAddPopUp) CreateFilePopUp( - type = CreationType.ALL, hidePopUp = { setShowAddPopUp(false) }, - onConfirm = { name, type -> onCreate(root, name, type) } + onConfirm = { name, type -> onCreate(root, name, type) }, + type = CreationType.ALL ) if (showRemovePopUp) RemoveFilePopUp( @@ -193,6 +332,20 @@ fun LazyListScope.WTreeItem( hidePopUp = { setShowRemovePopUp(false) }, onConfirm = { onDelete(root) } ) + + if (showMovePopUp) DirectoryProvider( + type = WProviderType.MoveFileProvider, + root = projectRoot, + onDismiss = { setShowMovePopUp(false) } + ) { result -> + + // find goal node in tree according to result: + val goal = findNode(result) + goal?.let { + getFilename(root.item)?.let { filename -> onMove(root, it, filename) } + } + setShowMovePopUp(false) + } } if (getIsOpened(root.item)) { WTreeItems( @@ -200,6 +353,8 @@ fun LazyListScope.WTreeItem( depth = depth + 1, getFilename = getFilename, getPath = getPath, + findNode = findNode, + projectRoot = projectRoot, isDirectory = isDirectory, getIsOpened = getIsOpened, toggleIsOpened = toggleIsOpened, diff --git a/app/src/main/java/be/re/writand/utils/WFileTreeAbstraction.kt b/app/src/main/java/be/re/writand/utils/WFileTreeAbstraction.kt index 683e9ff..64ff349 100644 --- a/app/src/main/java/be/re/writand/utils/WFileTreeAbstraction.kt +++ b/app/src/main/java/be/re/writand/utils/WFileTreeAbstraction.kt @@ -1,16 +1,12 @@ package be.re.writand.utils +import android.util.Log import androidx.compose.runtime.MutableState -import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.neverEqualPolicy -import androidx.compose.runtime.snapshots.MutableSnapshot import java.nio.file.Path import java.nio.file.Paths -import java.util.AbstractQueue import java.util.LinkedList -import java.util.PriorityQueue -import java.util.Queue import javax.inject.Inject import kotlin.io.path.isDirectory import kotlin.io.path.name @@ -33,10 +29,36 @@ class WFileTreeAbstraction @Inject constructor( _root.value = _root.value } + /** + * Update all the children after a directory is renamed. Makes sure this change + * is reflected in the index. + * @param[node] the node which is renamed. + * @param[oldName] the old name of the node. + * @param[newName] the new name of the node. + */ + private fun renameChildren(node: FileNode, oldName: String, newName: String) { + val queue = LinkedList() + queue.addAll(node.children) + + while (queue.isNotEmpty()) { + val top = queue.poll()!! + index[top.item] = Paths.get(index[top.item]!!.toString().replace(oldName, newName)) + + if (top.children.isNotEmpty()) { + queue.addAll(top.children) + } + } + } + private val comparator = compareBy { index[it.item]?.isDirectory() } .reversed() .thenBy { index[it.item]?.name } + /** + * Gets the path according to [id]. + * @param[id] the id of the node. + * @return the path of the node. + */ fun getPath(id: ULong): Path? { return index[id] } @@ -53,7 +75,7 @@ class WFileTreeAbstraction @Inject constructor( val queue = LinkedList() queue.add(node) - while(!queue.isEmpty()) { + while (!queue.isEmpty()) { val top = queue.poll()!! queue.addAll(top.children) index.remove(top.item) @@ -78,26 +100,53 @@ class WFileTreeAbstraction @Inject constructor( /** * Move a file from 1 location to another. + * If [from] and [to] are equal, a renaming operation took place * @param[from] the source location to be moved. * @param[to] the destination directory. */ - fun move(from: FileNode, to: FileNode) { - from.parent?.children?.remove(from) - to.children.add(from) - from.parent = to - to.children.sortWith(comparator) + fun move(from: FileNode, to: FileNode, name: String) { + val oldName = index[from.item]!!.fileName ?: throw IllegalArgumentException() + if (from != to) { + from.parent?.children?.remove(from) + to.children.add(from) + from.parent = to + to.children.sortWith(comparator) - // update the path stored in the index - val parentPath = index[to.item] ?: throw IllegalArgumentException() - val fromPath = index[from.item] ?: throw IllegalArgumentException() - index[from.item] = Paths.get(parentPath.toString(), fromPath.name) + // update the path stored in the index + val parentPath = index[to.item] ?: throw IllegalArgumentException() + index[from.item] = Paths.get(parentPath.toString(), name) + } else { + val path = index[from.item] ?: throw IllegalArgumentException() + index[from.item] = Paths.get(path.parent.toString(), name) + } + + if (from.children.isNotEmpty()) renameChildren(from, oldName.toString(), name) triggerRerender() } + /** + * Find a node according to a given path. + * @param[path] the path to find. + * @return null if no node is found, or the node corresponding to [path]. + */ + fun find(path: Path): FileNode? { + var currentNode: FileNode? = _root.value + while (currentNode != null) { + val currentPath = index[currentNode.item] + if (currentPath == path) return currentNode + currentNode = currentNode.children.find { + path.contains(index[it.item]?.fileName) + } + } + return null + } + override var value: FileNode get() = _root.value - set(value) {_root.value = value} + set(value) { + _root.value = value + } override fun component1(): FileNode { return _root.value diff --git a/app/src/main/res/drawable/baseline_drive_file_move_16.xml b/app/src/main/res/drawable/baseline_drive_file_move_16.xml new file mode 100644 index 0000000..9376b24 --- /dev/null +++ b/app/src/main/res/drawable/baseline_drive_file_move_16.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_more_horiz_16.xml b/app/src/main/res/drawable/baseline_more_horiz_16.xml new file mode 100644 index 0000000..03d8658 --- /dev/null +++ b/app/src/main/res/drawable/baseline_more_horiz_16.xml @@ -0,0 +1,5 @@ + + + + +