2
$\begingroup$

I want to approach a script to make a tree of string list- like this ,(kind of advance version of Vertex group template list) enter image description here

I can make a template list tho for start, but don't know how to make subnode to that and attach to it.

    bl_info = {
        "name": "object-uilist-dev",
        "description": "",
        "author": "p2or",
        "version": (0, 1),
        "blender": (2, 80, 0),
        "location": "Text Editor",
        "warning": "", # used for warning icon and text in addons panel
        "wiki_url": "",
        "tracker_url": "",
        "category": "Development"
    }

    import bpy

    from bpy.props import (IntProperty,
                           BoolProperty,
                           StringProperty,
                           CollectionProperty)

    from bpy.types import (Operator,
                           Panel,
                           PropertyGroup,
                           UIList)

    # -------------------------------------------------------------------
    #   Operators
    # -------------------------------------------------------------------

    class CUSTOM_OT_actions(Operator):
        """Move items up and down, add and remove"""
        bl_idname = "custom.list_action"
        bl_label = "List Actions"
        bl_description = "Move items up and down, add and remove"
        bl_options = {'REGISTER'}

        action: bpy.props.EnumProperty(
            items=(
                ('UP', "Up", ""),
                ('DOWN', "Down", ""),
                ('REMOVE', "Remove", ""),
                ('ADD', "Add", "")))

        def invoke(self, context, event):
            scn = context.scene
            idx = scn.custom_index

            try:
                item = scn.custom[idx]
            except IndexError:
                pass
            else:
                if self.action == 'DOWN' and idx < len(scn.custom) - 1:
                    item_next = scn.custom[idx+1].name
                    scn.custom.move(idx, idx+1)
                    scn.custom_index += 1
                    info = 'Item "%s" moved to position %d' % (item.name, scn.custom_index + 1)
                    self.report({'INFO'}, info)

                elif self.action == 'UP' and idx >= 1:
                    item_prev = scn.custom[idx-1].name
                    scn.custom.move(idx, idx-1)
                    scn.custom_index -= 1
                    info = 'Item "%s" moved to position %d' % (item.name, scn.custom_index + 1)
                    self.report({'INFO'}, info)

                elif self.action == 'REMOVE':
                    info = 'Item "%s" removed from list' % (scn.custom[idx].name)
                    scn.custom_index -= 1
                    scn.custom.remove(idx)
                    self.report({'INFO'}, info)

            if self.action == 'ADD':
                item = scn.custom.add()
                item.name = "Your Name"
                item.id = len(scn.custom)
                scn.custom_index = len(scn.custom)-1
                info = '"%s" added to list' % (item.name)
                self.report({'INFO'}, info)
            return {"FINISHED"}


    class CUSTOM_OT_printItems(Operator):
        """Print all items and their properties to the console"""
        bl_idname = "custom.print_items"
        bl_label = "Print Items to Console"
        bl_description = "Print all items and their properties to the console"
        bl_options = {'REGISTER', 'UNDO'}

        reverse_order: BoolProperty(
            default=False,
            name="Reverse Order")

        @classmethod
        def poll(cls, context):
            return bool(context.scene.custom)

        def execute(self, context):
            scn = context.scene
            if self.reverse_order:
                for i in range(scn.custom_index, -1, -1):        
                    item = scn.custom[i]
                    print ("Name:", item.name,"-", "ID:", item.id )
            else:
                for item in scn.custom:
                    print ("Name:", item.name,"-", "ID", item.id)
            return{'FINISHED'}


    class CUSTOM_OT_clearList(Operator):
        """Clear all items of the list"""
        bl_idname = "custom.clear_list"
        bl_label = "Clear List"
        bl_description = "Clear all items of the list"
        bl_options = {'INTERNAL'}

        @classmethod
        def poll(cls, context):
            return bool(context.scene.custom)

        def invoke(self, context, event):
            return context.window_manager.invoke_confirm(self, event)

        def execute(self, context):
            if bool(context.scene.custom):
                context.scene.custom.clear()
                self.report({'INFO'}, "All items removed")
            else:
                self.report({'INFO'}, "Nothing to remove")
            return{'FINISHED'}


    # -------------------------------------------------------------------
    #   Drawing
    # -------------------------------------------------------------------

    class CUSTOM_UL_items(UIList):
        def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
            split = layout.split(factor=0.3)
            split.label(text="Index: %d" % (index))
            custom_icon = "COLOR"
            split.prop(item, "name", icon=custom_icon, emboss=False, text="")

        def invoke(self, context, event):
            pass   

    class CUSTOM_PT_objectList(Panel):
        """Adds a custom panel to the TEXT_EDITOR"""
        bl_idname = 'TEXT_PT_my_panel'
        bl_space_type = "TEXT_EDITOR"
        bl_region_type = "UI"
        bl_label = "Custom Object List Demo"

        def draw(self, context):
            layout = self.layout
            scn = bpy.context.scene

            rows = 2
            row = layout.row()
            row.template_list("CUSTOM_UL_items", "", scn, "custom", scn, "custom_index", rows=rows)

            col = row.column(align=True)
            col.operator("custom.list_action", icon='ZOOM_IN', text="").action = 'ADD'
            col.operator("custom.list_action", icon='ZOOM_OUT', text="").action = 'REMOVE'
            col.separator()
            col.operator("custom.list_action", icon='TRIA_UP', text="").action = 'UP'
            col.operator("custom.list_action", icon='TRIA_DOWN', text="").action = 'DOWN'

            row = layout.row()
            col = row.column(align=True)
            row = col.row(align=True)
            row.operator("custom.print_items", icon="LINENUMBERS_ON") #LINENUMBERS_OFF, ANIM
            row = col.row(align=True)
            row.operator("custom.clear_list", icon="X")


    # -------------------------------------------------------------------
    #   Collection
    # -------------------------------------------------------------------

    class CUSTOM_colorCollection(PropertyGroup):
        #name: StringProperty() -> Instantiated by default
        id: IntProperty()

    # -------------------------------------------------------------------
    #   Register & Unregister
    # -------------------------------------------------------------------

    classes = (
        CUSTOM_OT_actions,
        CUSTOM_OT_printItems,
        CUSTOM_OT_clearList,
        CUSTOM_UL_items,
        CUSTOM_PT_objectList,
        CUSTOM_colorCollection,
    )

    def register():
        from bpy.utils import register_class
        for cls in classes:
            register_class(cls)

        # Custom scene properties
        bpy.types.Scene.custom = CollectionProperty(type=CUSTOM_colorCollection)
        bpy.types.Scene.custom_index = IntProperty()


    def unregister():
        from bpy.utils import unregister_class
        for cls in reversed(classes):
            unregister_class(cls)

        del bpy.types.Scene.custom
        del bpy.types.Scene.custom_index


    if __name__ == "__main__":
        register()

There is a post about approaching tree of property on Blender artist, but that code doesn't work :/ https://blenderartists.org/t/blender-treelist-expanding-subelements-ui/1191260

$\endgroup$

1 Answer 1

4
$\begingroup$

As mentioned in my previous answer, name is instantiated by default. Notice that the coding style/quality of the example given is not particularly beautiful, try to revise the code under consideration of Create an interface which is similar to the material list box.

enter image description here

import bpy

from bpy.types import PropertyGroup

from bpy.props import (
    CollectionProperty,
    IntProperty,
    BoolProperty,
    StringProperty,
    PointerProperty,
)



#
# This is what I am using to hold a single tree node in my raw example data.
# The entire example data is stored in **bpy.context.scene.myNodes**
#
class MyListTreeNode(bpy.types.PropertyGroup):
    selfIndex : bpy.props.IntProperty(default=-1)
    parentIndex : bpy.props.IntProperty(default=-1)
    childCount : bpy.props.IntProperty(default=0)


#
#   This represents an item that in the collection being rendered by
#   props.template_list. This collection is stored in ______
#   The collection represents a currently visible subset of MyListTreeNode
#   plus some extra info to render in a treelike fashion, eg indent.
#
class MyListTreeItem(bpy.types.PropertyGroup):
    indent: bpy.props.IntProperty(default=0)
    expanded: bpy.props.BoolProperty(default=False)
    nodeIndex : bpy.props.IntProperty(default=-1) #index into the real tree data.
    childCount: bpy.props.IntProperty(default=0) #should equal myNodes[nodeIndex].childCount
    
  


def SetupNodeData():
    bpy.types.Scene.myNodes = bpy.props.CollectionProperty(type=MyListTreeNode)
    myNodes = bpy.context.scene.myNodes
    myNodes.clear()
    
    for i in range(5):
        node = myNodes.add()
        node.name = "node {}".format(i)
        node.selfIndex = len(myNodes)-1
        
    for i in range(4):
        node = myNodes.add()
        node.name = "subnode {}".format(i)
        node.selfIndex = len(myNodes)-1
        node.parentIndex = 2

    parentIndex = len(myNodes)-2
        
    for i in range(2):
        node = myNodes.add()
        node.name = "subnode {}".format(i)
        node.selfIndex = len(myNodes)-1
        node.parentIndex = parentIndex
        
    parentIndex = len(myNodes)-3
        
    for i in range(2):
        node = myNodes.add()
        node.name = "subnode {}".format(i)
        node.selfIndex = len(myNodes)-1
        node.parentIndex = parentIndex
        
    parentIndex = len(myNodes)-1
        
    for i in range(2):
        node = myNodes.add()
        node.name = "subnode {}".format(i)
        node.selfIndex = len(myNodes)-1
        node.parentIndex = parentIndex
        
    # calculate childCount for all nodes
    for  node in myNodes :
        if node.parentIndex != -1:
            parent = myNodes[node.parentIndex]
            parent.childCount = parent.childCount + 1
            
    print("++++ SetupNodeData ++++")
    print("Node count: {}".format(len(myNodes)))
    for i in range(len(myNodes)):
        node = myNodes[i]
        print("{} node:{} child:{}".format(i, node.name, node.childCount))
        
        

def NewListItem( treeList, node):
    item = treeList.add()
    item.name = node.name
    item.nodeIndex = node.selfIndex
    item.childCount = node.childCount
    return item


def SetupListFromNodeData():
    bpy.types.Scene.myListTree = bpy.props.CollectionProperty(type=MyListTreeItem)
    bpy.types.Scene.myListTree_index = IntProperty()
    
    treeList = bpy.context.scene.myListTree
    treeList.clear()
    
    myNodes = bpy.context.scene.myNodes
    
    for node in myNodes:
        #print("node name:{} parent:{} kids:{}".format(node.name, node.parentIndex, node.children))
        if -1 == node.parentIndex :
            NewListItem(treeList, node)

#
#   Inserts a new item into myListTree at position item_index
#   by copying data from node
#
def InsertBeneath( treeList, parentIndex, parentIndent, node):
    after_index =parentIndex + 1
    item = NewListItem(treeList,node)
    item.indent = parentIndent+1
    item_index = len(treeList) -1 #because add() appends to end.
    treeList.move(item_index,after_index)


def IsChild( child_node_index, parent_node_index, node_list):
    if child_node_index == -1:
        print("bad node index")
        return False
    
    child = node_list[child_node_index]
    if child.parentIndex == parent_node_index:
        return True
    return False



#
#   Operation to Expand a list item.
#
class MyListTreeItem_Expand(bpy.types.Operator):
    bl_idname = "object.mylisttree_expand" #NOT SURE WHAT TO PUT HERE.
    bl_label = "Tool Name"
    
    button_id: IntProperty(default=0)

    def execute(self, context):
        item_index = self.button_id
        item_list = context.scene.myListTree
        item = item_list[item_index]
        item_indent = item.indent
        
        nodeIndex = item.nodeIndex
        
        myNodes = context.scene.myNodes
        
        print(item)
        if item.expanded:
            print("=== Collapse Item {} ===".format(item_index))
            item.expanded = False
            
            nextIndex = item_index+1
            while True:
                if nextIndex >= len(item_list):
                    break
                if item_list[nextIndex].indent <= item_indent:
                    break
                item_list.remove(nextIndex)
        else:
            print("=== Expand Item {} ===".format(item_index))
            item.expanded = True
            
            for n in myNodes:
                if nodeIndex == n.parentIndex:
                    InsertBeneath(item_list, item_index, item_indent, n)
            
        return {'FINISHED'}
    

#
#   Several debug operations
#   (bundled into a single operator with an "action" property)
#
class MyListTreeItem_Debug(bpy.types.Operator):
    bl_idname = "object.mylisttree_debug"
    bl_label = "Debug"
    
    action: StringProperty(default="default")
    
    def execute(self, context):
        action = self.action
        if "print" == action:
            print("=== Debug Print ====")
        elif "reset3" == action:
            print("=== Debug Reset ====")
            SetupListFromNodeData()
        elif "clear" == action:
            print("=== Debug Clear ====")
            bpy.context.scene.myListTree.clear()
        else:
            print("unknown debug action: "+action)

        return {'FINISHED'}


#
#   My List UI class to draw my MyListTreeItem
#   (The most important thing it does is show how to draw a list item)
#
#note this naming convention is important. For more info search for _UL_ in:
# https://wiki.blender.org/wiki/Reference/Release_Notes/2.80/Python_API/Addons
class MYLISTTREEITEM_UL_basic(bpy.types.UIList):

    def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
        scene = data
        #print(data, item, active_data, active_propname)
        if self.layout_type in {'DEFAULT', 'COMPACT'}:
            
            for i in range(item.indent):
                split = layout.split(factor = 0.1)
            
            col = layout.column()
            
            #print("item:{} childCount:{}".format(item.name, item.childCount)) 
            if item.childCount == 0:
               op = col.operator("object.mylisttree_expand", text="", icon='DOT')
               op.button_id = index
               col.enabled = False
            #if False:
            #    pass
            elif item.expanded :
                op = col.operator("object.mylisttree_expand", text="", icon='TRIA_DOWN')
                op.button_id = index
            else:
                op = col.operator("object.mylisttree_expand", text="", icon='TRIA_RIGHT')
                op.button_id = index
            
            col = layout.column()
            col.label(text=item.name)
            

#
#   My Panel UI, assigned to view.
#
class SCENE_PT_mylisttree(bpy.types.Panel):

    bl_label = "My List Tree"
    bl_idname = "SCENE_PT_materials"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = "My Category"

    def draw(self, context):

        scn = context.scene
        layout = self.layout
        
        row = layout.row()
        row.template_list(
            "MYLISTTREEITEM_UL_basic",
            "",
            scn,
            "myListTree",
            scn,
            "myListTree_index",
            sort_lock = True
            )
            
        grid = layout.grid_flow( columns = 2 )
        
        grid.operator("object.mylisttree_debug", text="Reset").action = "reset3"
        grid.operator("object.mylisttree_debug", text="Clear").action = "clear"
        grid.operator("object.mylisttree_debug", text="Print").action = "print"


classes = (
        MyListTreeNode,
        MyListTreeItem,
        MyListTreeItem_Expand,
        MyListTreeItem_Debug,
        MYLISTTREEITEM_UL_basic,
        SCENE_PT_mylisttree)


def register():
    for cls in classes:
        bpy.utils.register_class(cls)

    SetupNodeData()
    SetupListFromNodeData()


def unregister():
    # fill this in.
    pass


if __name__ == "__main__":
    register()
$\endgroup$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.