1
$\begingroup$

I created a panel called "Platforms", which displays some information. In that panel, I have an "Add" operator, which creates expandable rows (which can all be individually deleted by pressing a "Delete" button). To do so, I wrote the following code

import bpy

def update_platform_side(self, context):
    if self.platform_side == "blue":
        context.scene.platform_panel_props.nblue += 1
    elif self.platform_side == "red":
        context.scene.platform_panel_props.nred += 1
    else:
        pass

class PLATFORM_PlatformPanelProps(bpy.types.PropertyGroup):
    ntot: bpy.props.IntProperty(
        name = "Total",
        description = "Total number of platforms",
        min = 0,
        default = 0,
    )
    nred: bpy.props.IntProperty(
        name = "Red",
        description = "Number of red platforms",
        min = 0,
        default = 0,
    )
    nblue: bpy.props.IntProperty(
        name = "Blue",
        description = "Number of blue platforms",
        min = 0,
        default = 0,
    )
    side: bpy.props.EnumProperty(
        name = "Side",
        description = "Choose the platform side (either 'blue' or 'red')",
        update = update_platform_side,
        items = [
            ("platform_blue", "Blue", "Set the platform to 'blue' side"),
            ("platform_red", "Red", "Set the platform to 'red' side")
        ],
        default = "platform_blue",
    )
    is_expanded: bpy.props.BoolProperty(
        name = "Platform expand",
        description = "Expands a platform panel to show all its attributes",
        default = False,
    )


class PLATFORM_PT_Platform(bpy.types.Panel):
    bl_idname = 'PLATFORM_PT_Platform'
    bl_label = 'Platforms'
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'platform'
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        icon = 'TRIA_RIGHT' if not context.scene.is_expanded else 'TRIA_DOWN'

        box = layout.box()
        row = box.row(align = True)
        row.label(text = f"Red {context.scene.platform_panel_props.nred}")
        row.label(text = f"Blue {context.scene.platform_panel_props.nblue}")
        row.label(text = f"Total {context.scene.platform_panel_props.ntot}")

        box.operator('platform.add_platform', text = "Add")

        for i in range(context.scene.platform_panel_props.ntot):
            row = layout.row()
            row.prop(context.scene, 'is_expanded', icon=icon, icon_only=True)
            row.label(text = f'Platform {i + 1}')
            row.alert = True
            row.operator('platform.delete_platform', text = "Delete")

            if context.scene.is_expanded:
                box = layout.box()
                row = box.row(align = True)
                row.label(text = "Side")

class PLATFORM_OT_AddPlatform(bpy.types.Operator):
    """ Create a new platform with its sensor(s) and/or effector(s) """
    bl_idname = "platform.add_platform"
    bl_label = "Create new platform"

    def execute(self, context):
        context.scene.platform_panel_props.ntot += 1
        return {'FINISHED'}

class PLATFORM_OT_DeletePlatform(bpy.types.Operator):
    """ Delete a platform with its sensor(s) and/or effector(s) """
    bl_idname = "platform.delete_platform"
    bl_label = "Delete platform"

    def execute(self, context):
        context.scene.platform_panel_props.ntot -= 1
        return {'FINISHED'}

classes = (
            PLATFORM_PlatformPanelProps,
            PLATFORM_PT_Platform,
            PLATFORM_OT_AddPlatform,
            PLATFORM_OT_DeletePlatform,
        )

if __name__ == '__main__':
    for my_class in classes:
        bpy.utils.register_class(my_class)
    bpy.types.Scene.is_expanded = bpy.props.BoolProperty(default = False)
    bpy.types.Scene.platform_panel_props = bpy.props.PointerProperty(
                                                type = PLATFORM_PlatformPanelProps,
                                            )

The provided code runs, but not as intended: When I add several expandable rows, I cannot expand a particular one (i.e. they all expand simultaneously). How can I solve that issue? I also do not get why I have to declare bpy.types.Scene.is_expanded in the main to be able to expand the rows, instead of just calling platform_panel_props.is_expanded where is_expanded or context.scene.is_expanded is used?

$\endgroup$

1 Answer 1

1
$\begingroup$

The problem is that you want to control multiple properties with a single variable, which is not possible. I think you need to read about bpy.props.CollectionProperty. It lets you create a dynamically-adjustable collection of properties. I modified your code to make it work like I assumed you wanted.

enter image description here

import bpy

# We don't need a custom update callback since the numbers are calculated on the fly.


class PLATFORM_PlatformPanelProps(bpy.types.PropertyGroup):
    # I used static methods but these could be global functions, or directly used inside the draw method.
    # One could also use a cache feature and an update on prop modification but the computation cost is negligible.
    @staticmethod
    def ntot(context):
        return len(context.scene.platform_panel_props)
    
    @staticmethod
    def nred(context):
        return len([prop for prop in context.scene.platform_panel_props if prop.side == "platform_red"])
    
    @staticmethod
    def nblue(context):
        return len([prop for prop in context.scene.platform_panel_props if prop.side == "platform_blue"])

    side: bpy.props.EnumProperty(
        name = "Side",
        description = "Choose the platform side (either 'blue' or 'red')",
        items = [
            ("platform_blue", "Blue", "Set the platform to 'blue' side"),
            ("platform_red", "Red", "Set the platform to 'red' side")
        ],
        default = "platform_blue",
    )
    is_expanded: bpy.props.BoolProperty(
        name = "Platform expand",
        description = "Expands a platform panel to show all its attributes",
        default = False,
    )


class PLATFORM_PT_Platform(bpy.types.Panel):
    bl_idname = 'PLATFORM_PT_Platform'
    bl_label = 'Platforms'
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'platform'
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout

        box = layout.box()
        row = box.row(align = True)
        row.label(text = f"Red {PLATFORM_PlatformPanelProps.nred(context)}")
        row.label(text = f"Blue {PLATFORM_PlatformPanelProps.nblue(context)}")
        row.label(text = f"Total {PLATFORM_PlatformPanelProps.ntot(context)}")

        box.operator('platform.add_platform', text = "Add")

        # Here you want to iterate over your properties and keep the index for the Delete button
        for i, prop in enumerate(context.scene.platform_panel_props):
            row = layout.row()
            icon = 'TRIA_RIGHT' if not prop.is_expanded else 'TRIA_DOWN'
            row.prop(prop, 'is_expanded', icon=icon, icon_only=True)
            row.label(text = f'Platform {i + 1}')
            row.alert = True
            row.operator('platform.delete_platform', text = "Delete").index=i

            if prop.is_expanded:
                box = layout.box()
                row = box.row(align = True)
                row.prop(prop, "side")

class PLATFORM_OT_AddPlatform(bpy.types.Operator):
    """ Create a new platform with its sensor(s) and/or effector(s) """
    bl_idname = "platform.add_platform"
    bl_label = "Create new platform"

    def execute(self, context):
        context.scene.platform_panel_props.add()
        return {'FINISHED'}

class PLATFORM_OT_DeletePlatform(bpy.types.Operator):
    """ Delete a platform with its sensor(s) and/or effector(s) """
    bl_idname = "platform.delete_platform"
    bl_label = "Delete platform"
    # We use an index here so we can delete a specific item from the list, not the last or first one
    index: bpy.props.IntProperty()

    def execute(self, context):
        context.scene.platform_panel_props.remove(self.index)
        return {'FINISHED'}

classes = (
            PLATFORM_PlatformPanelProps,
            PLATFORM_PT_Platform,
            PLATFORM_OT_AddPlatform,
            PLATFORM_OT_DeletePlatform,
        )

if __name__ == '__main__':
    for my_class in classes:
        bpy.utils.register_class(my_class)
    # We only need a single collection property after all.
    bpy.types.Scene.platform_panel_props = bpy.props.CollectionProperty(
                                                type = PLATFORM_PlatformPanelProps,
                                            )
$\endgroup$
1
  • $\begingroup$ Very neat, thank you. I will give your pointer to the bpy.props.CollectionProperty a read, thanks $\endgroup$
    – aheuchamps
    Sep 29 at 17:39

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .