SOLVED Insert object in Treeview

The post https://plugincafe.maxon.net/topic/10654/14102_using-customgui-listview gives a great example of using Treeview.
I tried to add an InsertUnder, but that does not work.
What am I doing wrong?
I tested it using R20 and R21.

# In CreateLayout() I added a simple button
        self.AddButton(1002, c4d.BFH_CENTER, name="Insert Under First")

# In Command() I added:
        if id == 1002:
            # Insert Under
            tex = TextureObject("Inserted under first item.")
            #self._listView.SetDragObject(root, userdata, tex)

            first = self._listView.GetFirst(self._treegui, self._listView)
            print "First: ", first                # seems ok. It returns the first object T1

            #InsertObject(self, root, userdata, obj, dragtype, dragobject, insertmode, bCopy):
            self._listView.InsertObject(self._treegui, self._listView, first, c4d.DRAGTYPE_FILENAME_OTHER, tex, c4d.INSERT_UNDER, True)
            
            # Refresh the TreeView
            self._treegui.Refresh()

# In ListView() I added:
    def InsertObject(self, root, userdata, obj, dragtype, dragobject, insertmode, bCopy):
        print "Insert Object obj: ", obj                       #seems ok, T1
        print "dragobject: ", dragobject                    #seems ok, Inserted under first item. 
        #self._listView.InsertObject(self._treegui, self._listView, first, c4d.DRAGTYPE_FILENAME_OTHER, tex, c4d.INSERT_UNDER, True)
        return True

Ok, after some more reading I now understand that I should insert the object in the listview myself.

    def InsertObject(self, root, userdata, obj, dragtype, dragobject, insertmode, bCopy):
        self.listOfTexture.append(dragobject)
        return True

03cbb8df-61c9-4f0c-9979-c2b2b0412f0d-image.png
But now it is inserted at the end (makes sense, because I use an append()).
My question is how to insert the new object under another one?

Something like this.
037ef554-259c-451c-9e5b-6fdaa71e4256-image.png

I am trying to make treeview of a folder with its subfolders and files.

Hi @pim

Basically as explained in TreeView made simple – Part 1 you have to override GetDown to make it actually return the first child.
Then GetNext will be called to retrieve the next children.

So I would say it's more about how to structure your data that matters.
But since you seem to use the previous example I extended it to also support children.

So first we will extend our TextureObject to support children.
To do so we will add a list that will store all children and also a weakref of our parent. (don't forget to import weakref).
If you don't know what is a weakref I encourage you to read weakref – Garbage-collectable references to objects.

    def __init__(self, texturePath):
        self.texturePath = texturePath
        self.otherData += texturePath
        self.children = [] # This will store all children of the current TextureObject.
        self._parent = None # This will store a weakreaf (so don't forget to import weakref) to the parent.

Then we will define some convenient functions in our TextureObject:

    def AddChild(self, obj):
        obj._parent = weakref.ref(self)
        self.children.append(obj)
        
    def GetChildren(self):
        return self.children

    def GetParent(self):
        if self._parent:
            return self._parent()
        
        return None

Once it's done it's time to adapt our TreeViewFunctions implementation to add support for children.
So the first thing to override is GetDown() to retrieve the first child of a given TextureObject like so.

    def GetDown(self, root, userdata, obj):
        """
        Return a child of a node, since we only want a list, we return None everytime
        """
        children = obj.GetChildren()
        if children:
            return children[0]

        return None

The treeview will call GetDown to retrieve the first child, then to retrieve the next child it will call GetNext on it.
So we need to adapt GetNext and GetPref to also support children.
Typically we will use the same logic, the only difference is is the current obj have a parent, look for self in the parent list instead of the global one stored in our TreeViewFunctions implementation.

    def GetNext(self, root, userdata, obj):
        """
        Returns the next Object to display after arg:'obj'
        """
        rValue = None

        # If does have a child it means it's a child.
        objParent = obj.GetParent()
        listToSearch = objParent.GetChildren() if objParent is not None else self.listOfTexture

        currentObjIndex = listToSearch.index(obj)
        nextIndex = currentObjIndex + 1
        if nextIndex < len(listToSearch):
            rValue = listToSearch[nextIndex]

        return rValue

    def GetPred(self, root, userdata, obj):
        """
        Returns the previous Object to display before arg:'obj'
        """
        rValue = None

        # If does have a child it means it's a child.
        objParent = obj.GetParent()
        listToSearch = objParent.GetChildren() if objParent is not None else self.listOfTexture

        currentObjIndex = listToSearch.index(obj)
        predIndex = currentObjIndex - 1
        if 0 <= predIndex < len(listToSearch):
            rValue = listToSearch[predIndex]

        return rValue

We have everything needed now to display everything.

However, we also need to adapt DeletePressed to support children.
DeletePressed is a global event without a passed obj.
Previously we iterated listOfTexture so we need to support children. To do so let's make an iterator that will iterate all nodes and all children.
And let use it in the DeletePressed function.

# This is a global function which accept a list and will iterate over each TextureObject
# of the passed list to retrieve all TextureObject and all their children.
def TextureObjectIterator(lst):
    
    for parentTex in lst:
        yield parentTex
        TextureObjectIterator(parentTex.GetChildren())


    def DeletePressed(self, root, userdata):
        "Called when a delete event is received."
		
		# Convert the iterator to a list to be able to reverse it
        for tex in reversed(list(TextureObjectIterator(self.listOfTexture))):
            if tex.IsSelected:
                objParent = tex.GetParent()
                listToRemove = objParent.GetChildren() if objParent is not None else self.listOfTexture
                listToRemove.remove(tex)

Now everything is done for the TreeViewFunctions implementation.
So let's add a new button in our GeDialog CreateLayout to add a child to the selected TextureObject.
And defines its behavior when clicked in the GeDialog Command.

		# In CreateLayout
        self.AddButton(1002, c4d.BFH_CENTER, name="Add Child to selected")
		
		# In Command
        if id == 1002:
            for parentTex in TextureObjectIterator(self._listView.listOfTexture):
                if not parentTex.IsSelected:
                    continue

                newID = len(parentTex.GetChildren()) + 1 
                tex = TextureObject("T{0}.{1}".format(str(parentTex), newID))
                parentTex.AddChild(tex)
                
            # Refresh the TreeView
            self._treegui.Refresh()

Now that we have everything we may also want to support the folding state to show/hide children in the treeview.
So let's enhance our TextureObject to support folding (very similar to selection state).

class TextureObject(object):

    _open = True
	
    @property
    def IsOpened(self):
        return self._open

    def Open(self):
        self._open = True
    
    def Close(self):
        self._open = False

Then in the TreeViewFunctions implementation we need to override IsOpened and Open.

    def IsOpened(self, root, userdata, obj):
        """
        Returns: (bool): Status If it's opened = True (folded) or closed = False.
        """
        return obj.IsOpened()

    def Open(self, root, userdata, obj, onoff):
        """
        Called when the user clicks on a folding state of an object to display/hide its children
        """
        if onoff:
            obj.Open()

        else:
            obj.Close()

And here you are, find the full code in pastebin, keep in mind it's only one way to store data.
How you decide to store data is up to you.

In this case, it would make more sense to have a TextureObject as root, so this way all TextureObject will be handled in the same way and the first level is not stored in a specific one level the only list.
Maybe it will be for part 3 of TreeView Exploration! 🙂

Cheers,
Maxime.

Thanks, great explanation!
One small issue. Delete doesn't work because objParent' is not defined.

Traceback (most recent call last):
File "scriptmanager", line 251, in DeletePressed
NameError: global name 'objParent' is not defined

Here the code that, I think, solves the issue:

    def DeletePressed(self, root, userdata):
        "Called when a delete event is received."
        for tex in reversed(list(TextureObjectIterator(self.listOfTexture))):
            if tex.IsSelected:
                objParent = tex.GetParent()               # Added
                listToRemove = objParent.GetChildren() if objParent is not None else self.listOfTexture
                listToRemove.remove(tex)