2

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

pyCod3R
  • 1,926
  • 4
  • 15
NamanDeep
  • 461
  • 3
  • 12

1 Answers1

4

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()

pyCod3R
  • 1,926
  • 4
  • 15