1
$\begingroup$

I am making use of the ExportHelper to export objects either as and FBX or glTF without choosing the format beforehand. But when I don't define the filename_ext the script does not add the specified extension to the text field input and marks the text field in red when the file already exists.

Is there a way to dynamically change the filename_ext?

Here is a tweaked ExportHelper script. Try to make the filename_ext dynamic based on the EnumProperty "Example Enum". I couldn't get it to work properly. Having the check of the EnumProperty inside the draw function makes it behave strange.

Go to File > Export > glTF 2.0 and right click on the dropdown next to Format in the file browser and click Edit Source. Go into the Text Editor edit the new init.py and look for the function on_export_format_changed.

I am not skilled enough to implement it myself.

import bpy


def write_some_data(context, filepath, use_some_setting):
    print("running write_some_data...")
    f = open(filepath, 'w', encoding='utf-8')
    f.write("Hello World %s" % use_some_setting)
    f.close()

    return {'FINISHED'}


# 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 = ".gltf"

    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 draw(self, context):
        if self.type == 'OPT_A':
            self.filename_ext = '.gltf'

        if self.type == 'OPT_B':
            self.filename_ext = '.fbx'
        layout = self.layout
        layout.use_property_split = False
        layout.use_property_decorate = False  #No animation
        sfile = context.space_data
        operator = sfile.active_operator

        row = layout.row()
        row.prop(operator, 'type')


    def execute(self, context):
        if self.type == 'OPT_A':
            self.filename_ext = '.gltf'

        if self.type == 'OPT_B':
            self.filename_ext = '.fbx'
        return write_some_data(context, self.filepath, self.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")


# Register and add to the "file selector" menu (required to use F3 search "Text Export Operator" for quick access).
def register():
    bpy.utils.register_class(ExportSomeData)
    bpy.types.TOPBAR_MT_file_export.append(menu_func_export)


def unregister():
    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')
$\endgroup$

3 Answers 3

1
+50
$\begingroup$

ExportHelper is a mix-in-class to simplify some preselections passed to the Filebrowser, but not good for manipulating an active one. The FileBrowser itself is a tricky thing because it uses several operators and classes, like FileOperators, SpaceFileBrowser or FileSelectParams, even without ExportHelper.

Following an example of a modified FileBrowser. First a few basic notes because I don't know which point you're struggling more. Feel free to scroll down if bored.

FileBrowser is an interface for selecting files and folders with filtering options. It does not import or export anything itself. For this a function must be called when FileBrowser is finished. In your example return write_some_data(context, self.filepath, self.use_setting) in the execute() methode passes parameters filepath and use_settings from ExportHelper to your export function.

In the export function you are using the built-in python function open() with mode write 'w'. That opens (or creates if not exist) by default a textfile with filename and extention at the location all passed from filepath and at last inserts data into the file. link There are file formats using ASCII/Text like .obj where it can make sense to save a model as textfile.

To export a blendermodel in another format you better use the right ExportOperator to avoid problems. The built-in-operators can be found in Blender API > Export Scene Operators [https://docs.blender.org/api/current/bpy.ops.export_scene.html#module-bpy.ops.export_scene]. Each of these exporters use filepath to store the file at a given location without opening the filebrowser. The gltf-exporter has an attribute export_format to select one of three output formats and embedding options.

Blenders filepath includes the entire filetree from mount to extension. Windows example: C:\Users[USERNAME]\Documents\my_model.blend

FileSelectParams filename is extracted name + extension of filepath used by FileBrowser - same as bpy.path.basename(filepath). Example: my_model.blend

ExportHelper filename_ext is used to add a given extension to user input if none

In example there is a custom operator to call the FileBrowser, manipulate input and initiate the export. Best to test via sidebar (N-Panel).

import bpy
import os # needed for file operations
from bpy.props import StringProperty, BoolProperty, EnumProperty, PointerProperty
from bpy.types import Panel, Operator, PropertyGroup

### UI                      
class TESTPANEL_PT_Main(Panel):
    bl_idname = 'TEST_PANEL_PT_main'
    bl_label = "Test Panel"                      
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI' # sidebar
    bl_category = "TEST"
    def draw(self, context):
        fbprops = context.scene.fbprops     
        layout = self.layout
        row = layout.row()
        row.label(text="Export Filename:")
        row.prop(fbprops, 'use_blendername', text="blendername") 
        row = layout.row()
        row.enabled = False if fbprops.use_blendername else True
        row.prop(fbprops, 'file_name', text="") 
        layout.prop(fbprops, 'use_filecheck')
        layout.operator('filebrowser.export', text="open filebrowser")

### OPERATOR
class Filebrowser_OT_export(Operator):
    bl_idname = "filebrowser.export"
    bl_label = "Export File"
    
    filepath: StringProperty(name="File Path",  
                             maxlen= 1024,
                             subtype = 'FILE_PATH',
                             )          

    check_existing: BoolProperty(name="Check Existing",
                                 default=True,
                                 options={'HIDDEN'},
                                 )

  # open filebrowser with custom settings
    def invoke(self, context, event):  
        fbprops = context.scene.fbprops
        dir = os.path.dirname(fbprops.file_path)                                          # custom dir
        filename = fbprops.file_name + fbprops.file_formatlist[int(fbprops.file_format)]  # build filename (name+ext)
        self.filepath = os.path.join(dir, filename)                                       # build filepath         
        self.check_existing = fbprops.use_filecheck                                       # set check
        context.window_manager.fileselect_add(self) # open filebrowser
        return {'RUNNING_MODAL'}

  # draw custom props    
    def draw(self, context):
        fbprops = context.scene.fbprops
        layout = self.layout
        layout.use_property_split = False
        layout.use_property_decorate = False  
        # use format change
        row = layout.row()
        row.alignment='RIGHT'
        row.label(text="Change Format")
        row.prop(fbprops, 'use_formatchange', text=" ")      
        # format select
        row = layout.row(align=True)
        row.alignment='RIGHT'
        row.enabled = fbprops.use_formatchange
        row.prop(fbprops, 'file_format', text="")
        row.label(text="")        
      
  # filebrowser events
    def check(self, context):
        # filebrowser opens at end of operator
        # so we need to check if open to avoid first call error
        if len(context.window_manager.windows) > 1:                                # at least two windows
            if [a for a in context.window.screen.areas if a.type=='FILE_BROWSER']: # search filebrowser
              # get params
                space = context.space_data
                params = space.params
              # change params 
                fbprops = context.scene.fbprops   
                if fbprops.use_formatchange:           
                    basename_recent = params.filename.split('.')[0]                              # might be changed by user
                    name = basename_recent + fbprops.file_formatlist[int(fbprops.file_format)]   # build filename (basename+ext)
                    params.filename = name                                                       # set filename           
                
    def execute(self, context):
      # store recent selection
        fbprops = context.scene.fbprops 
        fbprops.file_path = self.filepath                
        name = bpy.path.basename(self.filepath).split('.')[0]
        fbprops.file_name = name               
        # for testpanel only
        check_names() 

      # get selected export format
        export_filename = bpy.path.basename(self.filepath)  # recent filename  
        export_format = export_filename.split('.')[1]       # recent format

      # export Blender
        if export_format == 'blend':                          
            bpy.ops.wm.save_mainfile(filepath=self.filepath)        

      # export FBX
        elif export_format == 'fbx':                          
            bpy.ops.export_scene.fbx(filepath=self.filepath)  

      # export GLB or GTLF
        else:    # gltf three different format and embedding options ; op needs gltf_format
            gltf_formats = ['GLB', 'GLTF_EMBEDDED', 'GLTF_SEPARATE']                   
            gltf_export_format =  gltf_formats[int(fbprops.file_format)-2]  # blend and fbx first in enum               
            bpy.ops.export_scene.gltf(filepath=self.filepath, export_format=gltf_export_format) 

        return {'FINISHED'} 

    def cancel(self, context):
      # store recent selection
        fbprops = context.scene.fbprops 
        fbprops.file_path = self.filepath                
        name = bpy.path.basename(self.filepath).split('.')[0]
        fbprops.file_name = name       
        # for testpanel only
        check_names()   
        return

### FUNCTION
def check_names():  # for testpanel only
    if not bpy.context.scene.fbprops.blend_name == bpy.context.scene.fbprops.file_name:
        bpy.context.scene.fbprops.use_blendername = False
    else:
        bpy.context.scene.fbprops.use_blendername = True          

### PROPERTIES  to store and update custom settings
class Filebrowser_Props(PropertyGroup):
  # blenderstart
    blend_path = bpy.context.blend_data.filepath  # loaded blendpath or empty if new 
    if not blend_path:
        blend_path = "Untitled"
    blend_name = bpy.path.display_name(blend_path, has_ext=True)    

  # testpanel
    def update_name(self,context):  
        if self.use_blendername:
            self.file_name = self.blend_name   # blendername ('Untitled' if new) 
    use_blendername: BoolProperty(default=False, description="Use Blendername", update=update_name)

  # custom settings         
    file_path: StringProperty(default=blend_path) 
    file_name: StringProperty(default=blend_name) 
    file_format: EnumProperty(name="Format Select",
                          items=[
                           ('0', 'Blend', '.blend'),            
                           ('1', 'FBX', 'fbx'),         
                           ('2', 'glTF Binary',   'glTF Binary (.glb)'),            
                           ('3', 'glTF Embedded', 'glTF Embedded (.gltf)'),         
                           ('4', 'glTF Separate', 'glTF Separate (.gltf + .bin + textures),'),          
                           ],
                          default='0',)                 
    file_formatlist = ['.blend', '.fbx', '.glb', '.gltf', '.gtlf']  # needed because .gltf two times
    use_formatchange: BoolProperty(default=True, description="Change fileformat to selected")
    use_filecheck: BoolProperty(default=True, description="Check and warn on overwriting existing files")
        
def register():
    bpy.utils.register_class(TESTPANEL_PT_Main)
    bpy.utils.register_class(Filebrowser_OT_export)  
    bpy.utils.register_class(Filebrowser_Props) 
    bpy.types.Scene.fbprops = PointerProperty(type=Filebrowser_Props) 

def unregister():
    bpy.utils.unregister_class(Filebrowser_Props) 
    bpy.utils.unregister_class(Filebrowser_OT_export) 
    bpy.utils.unregister_class(TESTPANEL_PT_Main) 
    bpy.types.Scene.fbprops
    
if __name__ == "__main__":
    register()
    check_names()  # for testpanel at start 

Some additional notes:

In Operator we need an invoke() method to open and a draw() method to show custom properties in FileBrowser. User events in FileBrowser are catched by method check(). File export in execute(). PropertyGroup Filebrowser_Props to store the last user input. Custom properties are marked with underscore in the middle for difference (file_path, file_name). In TestPanel you can preselect file_name and use_filecheck (the red alert thing if file exists) with which the FileBrowser is opened.

If more export options are desired: all properties in File > Export > glTF 2.0 are included in ExportSceneOperator bpy.ops.export_scene.gltf. You find the correct attribute with mouseover - if Preferences > Interface > Display > Tooltips > User Tooltips and Python Tooltips are checked. Tested in 2.9.3 and 3.3.2

$\endgroup$
1
  • $\begingroup$ I think this is a bit more complicated than needed, but the essential thing is the check function inside the Filebrowser_OT_export class $\endgroup$
    – Lala_Ghost
    Commented Jan 16, 2023 at 9:22
0
$\begingroup$

To dynamically change the filename_ext based on the value of the EnumProperty, you can move the code that sets the filename_ext from the draw function to the execute function. The execute function is called when the operator is actually executed, so this will ensure that the filename_ext is set correctly before the export occurs.

Here is an example of how you could modify your code to achieve this:

import bpy

def write_some_data(context, filepath, use_some_setting):
    print("running write_some_data...")
    f = open(filepath, 'w', encoding='utf-8')
    f.write("Hello World %s" % use_some_setting)
    f.close()

    return {'FINISHED'}

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 = ".gltf"

    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 draw(self, context):
        layout = self.layout
        layout.use_property_split = False
        layout.use_property_decorate = False  #No animation
        sfile = context.space_data
        operator = sfile.active_operator

        row = layout.row()
        row.prop(operator, 'type')

    def execute(self, context):
        if self.type == 'OPT_A':
            self.filename_ext = '.gltf'

        if self.type == 'OPT_B':
            self.filename_ext = '.fbx'
        return write_some_data(context, self.filepath, self.use_setting)

def menu_func_export(self, context):
    self.layout.operator(ExportSomeData.bl_idname, text="Text Export Operator")

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

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

register()
$\endgroup$
1
  • $\begingroup$ When I run your code and then go to File > Export > Text Export Operator it still does not have the function I need. My example already has the if statements in the execute function. Should I try to elaborate more precisely what I need? $\endgroup$
    – Lala_Ghost
    Commented Dec 19, 2022 at 13:27
0
$\begingroup$

Needed the same functionality and implemented it like this in Blender 3.6.4:

  • Set filename_ext to one of the extensions chosen by default.
  • Set filter_glob's default to a wildcard list of all extensions supported.
  • Add an EnumProperty filetype storing all extensions supported.
  • Handle draw to offer this property in the right sidebar.
  • Add a filetype_update callback to set the filename_ext.
import bpy
import bpy_extras

class CUSTOM_OT_ModelExport(bpy.types.Operator, bpy_extras.io_utils.ExportHelper):
    """Save a custom model file"""
    bl_idname = "export_scene.fbx_gltf"
    bl_label = "Export Custom Model"
    bl_options = {'UNDO'}
    filename_ext = ".fbx"
    filter_glob: bpy.props.StringProperty(default="*.fbx;*.gltf", options={'HIDDEN'})

    def filetype_update(self, context):
        CUSTOM_OT_ModelExport.filename_ext = "." + self.filetype.lower()

    filetype: bpy.props.EnumProperty(name="File Type", items=(
        ('FBX', "FBX", "", 1),
        ('GLTF', "glTF", "", 2),
    ), default='FBX', update=filetype_update)

    def draw(self, context):
        self.layout.prop(self, "filetype")
$\endgroup$

You must log in to answer this question.

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