Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to build a tree from dynamic data? #4

Closed
lijingdoc opened this issue Feb 6, 2023 · 31 comments
Closed

How to build a tree from dynamic data? #4

lijingdoc opened this issue Feb 6, 2023 · 31 comments
Assignees
Labels
help wanted Extra attention is needed

Comments

@lijingdoc
Copy link

lijingdoc commented Feb 6, 2023

I have dynamic data, I want to build a tree from it.
Let me put an example.

I have my own Item

data class Item(
    val name: String,
    val childItems: List<Item>
)

I use dynamic Item.
For example:

val item = Item(
    "head", listOf(
        Item("1", listOf(
            Item("11", listOf()),
            Item("12", listOf())
        )),
        Item("2", listOf(
            Item("21", listOf()),
            Item("22", listOf())
        ))
    )
)

How can I create tree for this dynamic item?

private fun createTree(item: Item): Tree<DataSource<Item>> {
    val dataCreator: CreateDataScope<Item> = { name, item ->
        Item("", listOf())
    }

    return buildTree(dataCreator) {
        fun getSubBranches(item: =Item): List<Unit> {
            val branches: MutableList<Unit> = mutableListOf()

            item.childItems.forEach {
                branches.add(
                    Branch(it.name, item) {
                        getSubBranches(it)
                    }
                )
            }
            return branches
        }
        getSubBranches(item)
    }
}

I tried the above code, but I am getting 1 dimension tree and there are no subbranches.
How can I approach it?
Thank you!

@dingyi222666
Copy link
Owner

For dynamic data, we recommend using TreeNodeGenerator

@lijingdoc
Copy link
Author

Is there an example for this? @dingyi222666

@dingyi222666
Copy link
Owner

Like this.
If you don't understand it for now, it's okay, I may improve the DataSource api in the coming days.

@dingyi222666 dingyi222666 self-assigned this Feb 6, 2023
@lijingdoc
Copy link
Author

Thanks, @dingyi222666
I hope to get new update asap.

@dingyi222666
Copy link
Owner

Yes, but I still recommend using TreeNodeGenerator to generate the nodes. When I update the API for DataSource I may also update the example using TreeNodeGenerator.

@lijingdoc
Copy link
Author

Ok, got, thanks

@dingyi222666
Copy link
Owner

dingyi222666 commented Feb 6, 2023

No, it may not be necessary to update the API of the DataSource.
If this data does not need to be updated later, then this will work.

val item = Item(
    "head", listOf(
        Item(
            "1", listOf(
                Item("11", listOf()),
                Item("12", listOf())
            )
        ),
        Item(
            "2", listOf(
                Item("21", listOf()),
                Item("22", listOf())
            )
        )
    )
)

fun DataSourceScope<Item>.getSubBranches(item: Item): List<Unit> {
    val branches: MutableList<Unit> = mutableListOf()

    item.childItems.forEach {
        branches.add(
            Branch(it.name, item) {
                getSubBranches(it)
            }
        )
    }
    return branches
}

// The dataCreator is not needed because the data is already provided in Branch(name:String,data,T). 
// The dataCreator is called only when the set data is null.
return buildTree {
    getSubBranches(item)
}

@dingyi222666
Copy link
Owner

dingyi222666 commented Feb 6, 2023

This is an example of using TreeNodeGenerator.

private fun createTree(): Tree<Item> {

    val item = Item(
        "head", listOf(
            Item(
                "1", listOf(
                    Item("11", listOf()),
                    Item("12", listOf())
                )
            ),
            Item(
                "2", listOf(
                    Item("21", listOf()),
                    Item("22", listOf())
                )
            )
        )
    )

    return Tree.createTree<Item>().apply {
        generator = ItemTreeNodeGenerator(item)
        initTree()
    }
}

inner class ItemTreeNodeGenerator(
    private val rootItem: Item
) : TreeNodeGenerator<Item> {

    override suspend fun fetchNodeChildData(targetNode: TreeNode<Item>): Set<Item> {
        return targetNode.requireData().childItems.toSet()
    }

    override fun createNode(
        parentNode: TreeNode<Item>,
        currentData: Item,
        tree: AbstractTree<Item>
    ): TreeNode<Item> {
        return TreeNode(
            data = currentData,
            depth = parentNode.depth + 1,
            name = currentData.name,
            id = tree.generateId(),
            hasChild = currentData.childItems.isNotEmpty(),
            // It should be taken from the Item
            isChild = currentData.childItems.isNotEmpty(),
            expand = false
        )
    }

    override fun createRootNode(): TreeNode<Item> {
        return TreeNode(
            data = rootItem,
            depth = 0,
            name = rootItem.name,
            id = Tree.ROOT_NODE_ID,
            hasChild = true,
            isChild = true
        )
    }
}

@dingyi222666 dingyi222666 pinned this issue Feb 6, 2023
@dingyi222666 dingyi222666 added the help wanted Extra attention is needed label Feb 6, 2023
@lijingdoc
Copy link
Author

@dingyi222666 thanks for your kind help
It works well.

@lijingdoc
Copy link
Author

@dingyi222666
I have one more question.

If there are several root nodes, then how can I implement it?
For example:

val items: List<Item> = 
    listOf(
        Item(
            "root1", listOf(
                Item("1", listOf(
                    Item("11", listOf()),
                    Item("12", listOf())
                )),
                Item("2", listOf(
                    Item("21", listOf()),
                    Item("22", listOf())
                ))
            )
        ),
        Item(
            "root2", listOf(
                Item("1", listOf(
                    Item("11", listOf()),
                    Item("12", listOf())
                )),
                Item("2", listOf(
                    Item("21", listOf()),
                    Item("22", listOf())
                ))
            )
        )
    )

@dingyi222666
Copy link
Owner

dingyi222666 commented Feb 6, 2023

If you are using TreeNodeGenerator, this will work.

private fun createTree(): Tree<Item> {
    val items =
        listOf(
            Item(
                "root1", listOf(
                    Item("1", listOf(
                        Item("11", listOf()),
                        Item("12", listOf())
                    )),
                    Item("2", listOf(
                        Item("21", listOf()),
                        Item("22", listOf())
                    ))
                )
            ),
            Item(
                "root2", listOf(
                    Item("1", listOf(
                        Item("11", listOf()),
                        Item("12", listOf())
                    )),
                    Item("2", listOf(
                        Item("21", listOf()),
                        Item("22", listOf())
                    ))
                )
            )
        )

    val rootItem = Item("root_not_show", items)

    return Tree.createTree<Item>().apply {
        generator = ItemTreeNodeGenerator(rootItem)
        initTree()
    }
}

inner class ItemTreeNodeGenerator(
    private val rootItem: Item
) : TreeNodeGenerator<Item> {
    override suspend fun fetchNodeChildData(targetNode: TreeNode<Item>): Set<Item> {
        return targetNode.requireData().childItems.toSet()
    }

    override fun createNode(
        parentNode: TreeNode<Item>,
        currentData: Item,
        tree: AbstractTree<Item>
    ): TreeNode<Item> {
        return TreeNode(
            data = currentData,
            depth = parentNode.depth + 1,
            name = currentData.name,
            id = tree.generateId(),
            hasChild = currentData.childItems.isNotEmpty(),
            // It should be taken from the Item
            isChild = currentData.childItems.isNotEmpty(),
            expand = false
        )
    }

    override fun createRootNode(): TreeNode<Item> {
        return TreeNode(
            data = rootItem,
            // Set to -1 to not show the root node
            depth = -1,
            name = rootItem.name,
            id = Tree.ROOT_NODE_ID,
            hasChild = true,
            isChild = true
        )
    }

}

@dingyi222666
Copy link
Owner

If you use buildTree DSL, this will work.

private fun createTree(): Tree<DataSource<Item>> {
    val items =
        listOf(
            Item(
                "root1", listOf(
                    Item("1", listOf(
                        Item("11", listOf()),
                        Item("12", listOf())
                    )),
                    Item("2", listOf(
                        Item("21", listOf()),
                        Item("22", listOf())
                    ))
                )
            ),
            Item(
                "root2", listOf(
                    Item("1", listOf(
                        Item("11", listOf()),
                        Item("12", listOf())
                    )),
                    Item("2", listOf(
                        Item("21", listOf()),
                        Item("22", listOf())
                    ))
                )
            )
        )

    fun DataSourceScope<Item>.getSubBranches(items:List<Item>) {
        items.forEach {
            Branch(it.name, it) {
                getSubBranches(it.childItems)
            }

        }
    }

    return buildTree {
        getSubBranches(items)
    }
}

@lijingdoc
Copy link
Author

Thank you, @dingyi222666
It works well.

@BlueCatSoftware
Copy link

@dingyi222666
Now the problem is how do i update the data of the TreeView, if i'm using the TreeNodeGenerator and still maintaining the hierarchy of the Tree

@dingyi222666
Copy link
Owner

@dingyi222666 Now the problem is how do i update the data of the TreeView, if i'm using the TreeNodeGenerator and still maintaining the hierarchy of the Tree

tree.refresh()?

@BlueCatSoftware
Copy link

@dingyi222666 Now the problem is how do i update the data of the TreeView, if i'm using the TreeNodeGenerator and still maintaining the hierarchy of the Tree

tree.refresh()?

that doesn't work, i'm loading a folder from the device, with the functionality of creating, delete, rename etc, which i have added already and it works fine, but calling tree.refresh() seems not work though i'm calling it from a ViewBinder, which the constructor has the Tree instance

@dingyi222666
Copy link
Owner

@dingyi222666 Now the problem is how do i update the data of the TreeView, if i'm using the TreeNodeGenerator and still maintaining the hierarchy of the Tree

tree.refresh()?

that doesn't work, i'm loading a folder from the device, with the functionality of creating, delete, rename etc, which i have added already and it works fine, but calling tree.refresh() seems not work though i'm calling it from a ViewBinder, which the constructor has the Tree instance

Can you show me how your TreeNodeGenerator is implemented?

@BlueCatSoftware
Copy link

BlueCatSoftware commented Apr 17, 2023

@dingyi222666
here is the link , usage

@dingyi222666
Copy link
Owner

@dingyi222666 Now the problem is how do i update the data of the TreeView, if i'm using the TreeNodeGenerator and still maintaining the hierarchy of the Tree

tree.refresh()?

that doesn't work, i'm loading a folder from the device, with the functionality of creating, delete, rename etc, which i have added already and it works fine, but calling tree.refresh() seems not work though i'm calling it from a ViewBinder, which the constructor has the Tree instance

No, it should call treeView.refresh().
tree.refresh may only refresh the tree data, and not synchronize it to the treeView.

@BlueCatSoftware
Copy link

BlueCatSoftware commented Apr 18, 2023

@dingyi222666
non of the treeView.refresh() and treeView.refresh(false, node) works or do anything here, it was called from the ViewBinder here is the link

the treeView.refresh() used to work when i was using kotlin dsl thingy, since when i switched to TreeNodeGenerator it doesn't work, i think the bug is from the library

@dingyi222666
Copy link
Owner

@BlueCatSoftware
You also need to update your FileSet data.

The data flow should look like this

raw data -> node generator -> tree -> treeView

@BlueCatSoftware
Copy link

BlueCatSoftware commented Apr 18, 2023

@BlueCatSoftware You also need to update your FileSet data.

The data flow should look like this

raw data -> node generator -> tree -> treeView

private suspend fun refreshTreeView(treeView: TreeView<FileSet>, node: TreeNode<FileSet>){
        val rootItem = FileSet(project.root,
            transverseTree(project.root) as MutableSet<FileSet>
        )
        val generator = FileTreeNodeGenerator(rootItem)
        val tree = Tree.createTree<FileSet>().apply {
            this.generator = generator
            initTree()
        }
        treeView.tree = tree
        treeView.refresh(false, node)
    }

this works but doesn't retain the tree hierarchy, it literally reloads and collapses the tree.

At this point an example will definitely help

@BlueCatSoftware
Copy link

BlueCatSoftware commented Apr 19, 2023

@BlueCatSoftware See https://github.com/dingyi222666/UnLuacTool/blob/main/app/src/main/java/com/dingyi/unluactool/ui/editor/main/FileViewerFragment.kt#L361

yeah i am aware of this method, it fetches the data. i'm still confused, the problem is there's no method to update the node's data. i know how to fetch nodes data but doesn't know how to manipulate it.
I'm still very confused with the updating

@dingyi222666
Copy link
Owner

dingyi222666 commented Apr 19, 2023

@BlueCatSoftware
The FileSet class should have a parent attribute that points to the parent file. When updating, it needs to get the parent’s FileSet and refresh its child files.

Actually, I recommend you to use File directly, so that the node generator only needs File.listFile.

@BlueCatSoftware
Copy link

BlueCatSoftware commented Apr 19, 2023

@BlueCatSoftware The FileSet class should have a parent attribute that points to the parent file. When updating, it needs to get the parent’s FileSet and refresh its child files.

Actually, I recommend you to use File directly, so that the node generator only needs File.listFile.

i get your point, here is my problem UPDATING, how do i exactly update.
The big question is the UPDATING, what am i swapping or editing ?, is it the generator or what exactly ?

Sorry for too much question lol.

@dingyi222666
Copy link
Owner

dingyi222666 commented Apr 19, 2023

@BlueCatSoftware
I don't think you fully understand what I'm saying.

Let me give you a sample code.

class FileTreeNodeGenerator(val rootItem: FileSet) : TreeNodeGenerator<FileSet> {

    override fun createNode(
        parentNode: TreeNode<FileSet>,
        currentData: FileSet,
        tree: AbstractTree<FileSet>
    ): TreeNode<FileSet> {
        return TreeNode(
            currentData,
            parentNode.depth + 1,
            currentData.file.name,
            tree.generateId(),
            currentData.subDir.isNotEmpty(),
            currentData.subDir.isNotEmpty(),
            false
        )
    }

    override suspend fun fetchNodeChildData(targetNode: TreeNode<FileSet>): Set<FileSet> = withContext(Dispatcher.IO) {
        // refresh data here
        val fileSet = targetNode.requireData()
        fileSet.subDir.clear()
        for (file in dir.listFiles()) {
              fileSet.subDir.add(FileSet(fileSet))
        }
        fileSet.subDir.toSet()
    }

    override fun createRootNode(): TreeNode<FileSet> {
        return TreeNode(
            data = rootItem,
            depth = 0,
            name = rootItem.file.name,
            id = Tree.ROOT_NODE_ID,
            hasChild = true,
            isChild = true
        )
    }
}

data class FileSet(val file: File, val subDir: MutableSet<FileSet> = mutableSetOf())

@BlueCatSoftware
Copy link

@dingyi222666
oh this, i'm gonna try it out.
Thanks by the way

@BlueCatSoftware
Copy link

override suspend fun fetchNodeChildData(targetNode: TreeNode<FileSet>): Set<FileSet> =
        withContext(Dispatchers.IO) {
            val set = targetNode.requireData().subDir
            set.clear()
            val files = targetNode.requireData().file.listFiles() ?: return@withContext set
            for (file in files) {
                when {
                    file.isFile -> set.add(FileSet(file))
                    file.isDirectory -> {
                        val tempSet = mutableSetOf<FileSet>().apply {
                            addAll(transverseTree(file))
                        }
                        set.add(FileSet(file, subDir = tempSet))
                    }
                }
            }
            return@withContext set
        }

this definitely worked for me, i didn't know treeView.refresh() calls fetchNodeChildData() internally though.
what if the data is dynamic, e.g you are not loading data from the device file storage, but rather you want to insert data based on user's interaction?

@dingyi222666
Copy link
Owner

dingyi222666 commented Apr 19, 2023

override suspend fun fetchNodeChildData(targetNode: TreeNode<FileSet>): Set<FileSet> =
        withContext(Dispatchers.IO) {
            val set = targetNode.requireData().subDir
            set.clear()
            val files = targetNode.requireData().file.listFiles() ?: return@withContext set
            for (file in files) {
                when {
                    file.isFile -> set.add(FileSet(file))
                    file.isDirectory -> {
                        val tempSet = mutableSetOf<FileSet>().apply {
                            addAll(transverseTree(file))
                        }
                        set.add(FileSet(file, subDir = tempSet))
                    }
                }
            }
            return@withContext set
        }

this definitely worked for me, i didn't know treeView.refresh() calls fetchNodeChildData() internally though. what if the data is dynamic, e.g you are not loading data from the device file storage, but rather you want to insert data based on user's interaction?

Let’s take another look at this data flow: raw data -> node generator -> tree -> treeView.
If your raw data comes from multiple sources, then you need to provide your raw data externally.
To implement your example, you need to provide raw data (which is FileSet).
This data needs to be refreshed outside the node generator.
I’m afraid I can’t provide any more code examples, I think you need to think more about how to implement it, this is actually a simple problem.

Repository owner locked as off-topic and limited conversation to collaborators Apr 19, 2023
Repository owner unlocked this conversation Apr 20, 2023
@dingyi222666
Copy link
Owner

@BlueCatSoftware
See here

I'm soon to release version 1.2.0, which will improve some things, so feel free to update and use it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

3 participants