2
$\begingroup$

I would like to wrap my properties inside collapsible panels when using the ExportHelper or ImportHelper class but I can not figure it out how to draw them properly:

enter image description here

This is my code so far (reduced version of the Operator File Export template):

import bpy

# ExportHelper is a helper class, defines filename and
# invoke() function which calls the file selector.
from bpy_extras.io_utils import ExportHelper
from bpy.props import StringProperty, BoolProperty, EnumProperty
from bpy.types import Operator


class ExportSomeData(Operator, ExportHelper):
    """This appears in the tooltip of the operator and in the generated docs"""
    bl_idname = "export_test.some_data"  # important since its how bpy.ops.import_test.some_data is constructed
    bl_label = "Export Some Data"

    # ExportHelper mixin class uses this
    filename_ext = ".txt"

    def execute(self, context):
        return {'FINISHED'}


class SubPanel(bpy.types.Panel):
    bl_label = 'SubPanel'
    bl_idname = 'Sub_PT_Layout'
    bl_parent_id = 'export_test.some_data'
    bl_space_type = 'FILE_BROWSER'
    bl_region_type = 'TOOL_PROPS'

    def draw(self, context):
        layout = self.layout
        layout.label(text = 'test')

        use_setting: BoolProperty(
            name="Example Boolean",
            description="Example Tooltip",
            default=True,
        )


def register():
    bpy.utils.register_class(ExportSomeData)
    bpy.utils.register_class(SubPanel)


def unregister():
    bpy.utils.unregister_class(SubPanel)
    bpy.utils.unregister_class(ExportSomeData)


if __name__ == "__main__":
    register()

    # test call
    bpy.ops.export_test.some_data('INVOKE_DEFAULT')

Unfortunately I get the following error:

RuntimeError: Error: Registering panel class: parent 'export_test.some_data' for 'Sub_PT_Layout' not found

Q: How can I add a panel to the properties area of the file browser when using the ExportHelper or ImportHelper class?

$\endgroup$
1
  • $\begingroup$ As the error mentions your parent ID name is incorrect. You need to use the class name not the bl_idname also you are trying to parent a panel to an operator instead of another panel. I believe the panel you are looking for is bl_parent_id = 'FILE_PT_operator' however your test call will only include your defined operator not the full panel. $\endgroup$
    – Ratt
    Mar 30, 2021 at 7:16

1 Answer 1

5
$\begingroup$

You would have to add a draw() method along with a pass statement to the operator in order to skip drawing and declare a Panel class, get the actual operator from that class in order to draw it's properties. Demo based on the Operator Export template that comes with Blender:

enter image description here

import bpy

# ExportHelper is a helper class, defines filename and
# invoke() function which calls the file selector.
from bpy_extras.io_utils import ExportHelper
from bpy.props import StringProperty, BoolProperty, EnumProperty
from bpy.types import Operator, Panel


class ExportSomeData(Operator, ExportHelper):
    """This appears in the tooltip of the operator and in the generated docs"""
    bl_idname = "export_test.some_data"  # important since its how bpy.ops.import_test.some_data is constructed
    bl_label = "Export Some Data"

    # ExportHelper mixin class uses this
    filename_ext = ".txt"

    filter_glob: StringProperty(
        default="*.txt",
        options={'HIDDEN'},
        maxlen=255,  # Max internal buffer length, longer would be clamped.
    )

    # List of operator properties, the attributes will be assigned
    # to the class instance from the operator settings before calling.
    use_setting: BoolProperty(
        name="Example Boolean",
        description="Example Tooltip",
        default=True,
    )

    type: EnumProperty(
        name="Example Enum",
        description="Choose between two items",
        items=(
            ('OPT_A', "First Option", "Description one"),
            ('OPT_B', "Second Option", "Description two"),
        ),
        default='OPT_A',
    )

    def execute(self, context):
        return {'FINISHED'}
    
    def draw(self, context):
        pass


class CUSTOM_PT_export_settings(Panel):
    bl_space_type = 'FILE_BROWSER'
    bl_region_type = 'TOOL_PROPS'
    bl_label = "Settings Panel"
    bl_options = {'DEFAULT_CLOSED'}

    @classmethod
    def poll(cls, context):
        sfile = context.space_data
        operator = sfile.active_operator
        return operator.bl_idname == "EXPORT_TEST_OT_some_data"

    def draw(self, context):
        layout = self.layout
        layout.use_property_split = True
        layout.use_property_decorate = False  # No animation.

        sfile = context.space_data
        operator = sfile.active_operator

        layout.prop(operator, 'type')
        layout.prop(operator, 'use_setting')
        

# Only needed if you want to add into a dynamic menu
def menu_func_export(self, context):
    self.layout.operator(ExportSomeData.bl_idname, text="Text Export Operator")


def register():
    bpy.utils.register_class(ExportSomeData)
    bpy.utils.register_class(CUSTOM_PT_export_settings)
    bpy.types.TOPBAR_MT_file_export.append(menu_func_export)


def unregister():
    bpy.utils.unregister_class(CUSTOM_PT_export_settings)
    bpy.utils.unregister_class(ExportSomeData)
    bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)


if __name__ == "__main__":
    register()

    # test call
    bpy.ops.export_test.some_data('INVOKE_DEFAULT')

Make sure to add a poll() method to the panel, testing for the actual bl_idname of the operator otherwise the panel will appear for all other export operators as well.

@classmethod
def poll(cls, context):
    sfile = context.space_data
    operator = sfile.active_operator
    return operator.bl_idname == "EXPORT_TEST_OT_some_data"

Note: Once the export operator is registered you can use the python console and call its idname() method getting the proper bl_idname:

>>> bpy.ops.export_test.some_data.idname()
'EXPORT_TEST_OT_some_data'

Just for the sake, adding sub panels is possible as well:

enter image description here

import bpy

# ExportHelper is a helper class, defines filename and
# invoke() function which calls the file selector.
from bpy_extras.io_utils import ExportHelper
from bpy.props import StringProperty, BoolProperty, EnumProperty
from bpy.types import Operator, Panel


class ExportSomeData(Operator, ExportHelper):
    """This appears in the tooltip of the operator and in the generated docs"""
    bl_idname = "export_test.some_data"  # important since its how bpy.ops.import_test.some_data is constructed
    bl_label = "Export Some Data"
    bl_options = {'PRESET', 'UNDO'}

    # ExportHelper mixin class uses this
    filename_ext = ".txt"

    filter_glob: StringProperty(
        default="*.txt",
        options={'HIDDEN'},
        maxlen=255,  # Max internal buffer length, longer would be clamped.
    )

    # List of operator properties, the attributes will be assigned
    # to the class instance from the operator settings before calling.
    use_setting: BoolProperty(
        name="Example Boolean",
        description="Example Tooltip",
        default=True,
    )

    type: EnumProperty(
        name="Example Enum",
        description="Choose between two items",
        items=(
            ('OPT_A', "First Option", "Description one"),
            ('OPT_B', "Second Option", "Description two"),
        ),
        default='OPT_A',
    )

    def execute(self, context):
        return {'FINISHED'}
    
    def draw(self, context):
        self.layout.label(text="Hello World")
        #self.layout.prop(self, 'type')
        #pass


class EXAMPLE_PT_panel_1(Panel):
    bl_space_type = 'FILE_BROWSER'
    bl_region_type = 'TOOL_PROPS'
    bl_label = "Settings Panel 1"
    #bl_parent_id = "FILE_PT_operator" # Optional

    @classmethod
    def poll(cls, context):
        sfile = context.space_data
        operator = sfile.active_operator
        return operator.bl_idname == "EXPORT_TEST_OT_some_data"

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

        sfile = context.space_data
        operator = sfile.active_operator
        
        layout.label(text="This is the first Panel")
        layout.prop(operator, 'type')
        layout.prop(operator, 'use_setting')


class EXAMPLE_PT_panel_2(Panel):
    bl_space_type = 'FILE_BROWSER'
    bl_region_type = 'TOOL_PROPS'
    bl_label = "Settings Panel 2"
    bl_parent_id = "EXAMPLE_PT_panel_1"
    bl_options = {'DEFAULT_CLOSED'}

    @classmethod
    def poll(cls, context):
        sfile = context.space_data
        operator = sfile.active_operator
        return operator.bl_idname == "EXPORT_TEST_OT_some_data"

    def draw(self, context):
        layout = self.layout
        
        sfile = context.space_data
        operator = sfile.active_operator
        
        layout.label(text="This is the second Panel")
        layout.prop(operator, 'type')


# Only needed if you want to add into a dynamic menu
def menu_func_export(self, context):
    self.layout.operator(ExportSomeData.bl_idname, text="Text Export Operator")


def register():
    bpy.utils.register_class(ExportSomeData)
    bpy.utils.register_class(EXAMPLE_PT_panel_1)
    bpy.utils.register_class(EXAMPLE_PT_panel_2)
    bpy.types.TOPBAR_MT_file_export.append(menu_func_export)


def unregister():
    bpy.utils.unregister_class(EXAMPLE_PT_panel_2)
    bpy.utils.unregister_class(EXAMPLE_PT_panel_1)
    bpy.utils.unregister_class(ExportSomeData)
    bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)


if __name__ == "__main__":
    register()

    # test call
    bpy.ops.export_test.some_data('INVOKE_DEFAULT')

Related: How do a create a foldout UI panel?


If you'd like to add a checkbox to one of the panel headers, declare a BoolProperty in your operator (alternatively use the scope of the window manager):

class ExportSomeData(Operator, ExportHelper):
    ...
    bake = BoolProperty(default=False)
    ...

And call Layout.prop() in the dedicated draw_header() method of the panel:

def draw_header(self, context):
    sfile = context.space_data
    operator = sfile.active_operator

    self.layout.prop(operator, "bake", text="")

Related: How to add a checkbox to a panel header?

$\endgroup$
5
  • $\begingroup$ I just saw, this adds the "Settings Panel" to my own custom file browser AND to the standard one when you click File>Export>glTF 2.0 (.glb/gltf) $\endgroup$
    – Lala_Ghost
    Mar 30, 2021 at 8:40
  • $\begingroup$ How do you make a panel with a checkbox in it? Like the Bake Animation panel in the questions screenshot $\endgroup$
    – Lala_Ghost
    Mar 31, 2021 at 7:15
  • 1
    $\begingroup$ blender.stackexchange.com/questions/147054/… @Lala_Ghost $\endgroup$
    – brockmann
    Mar 31, 2021 at 7:54
  • $\begingroup$ But how do you access the state of the checkbox in the different panel class? $\endgroup$
    – Lala_Ghost
    Mar 31, 2021 at 8:53
  • 1
    $\begingroup$ Add a bool property to the operator, window manager or whatever @Lala_Ghost $\endgroup$
    – brockmann
    Mar 31, 2021 at 9:07

You must log in to answer this question.

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