0
$\begingroup$

I am creating an addon for Blender. As part of the addon, I need the user to select a folder location along with some other input. I am creating a popup window to display to the user. The user enters the input values and selects a folder. Then the addon will process it. That folder selection part (although opening correctly) is crashing Blender. And I could not get a single example of this type of usage across SE or BA.

Below I have presented the bare minimum example code. You can simply copy it and paste it in a text editor within Blender and execute it. It will show a popup window like this:

Popup window with a field to select some folder.

After the user selects a folder, Blender will crash immediately. I am aware of the fact that I can show the folder selection UI with ImportHelper, I know that I can use window_manager's fileselect_add() to display the same folder selection window, but my goal is different. I want to show MY popup window exactly as designed above. The fact is, this popup is opening correctly. And if I comment out the folder selection part, everything else is working fine too. Just that folder selection part is not working. There is nothing specific in the crash report other than an Access Violation Exception which does not explain anything. If you run this code, you can see the result yourself.

So can someone please explain to me why it is crashing Blender, and if possible, a solution with minimum changes to the code below? I mean I am looking for solutions that does not force me to change my UI design or the user experience. Any changes in the background logic or the background architecture is highly welcome.

import bpy
from bpy.types import Operator

class SelectDirExample(Operator):
    bl_idname = "object.select_dir_example"
    bl_label = "Select Dir Example"
    bl_options = {'REGISTER', 'UNDO'}
    
    input1: bpy.props.FloatProperty(name="Input 1", default=1.0, min=0.0, max=100.0)
    input2: bpy.props.FloatProperty(name="Input 2", default=1.0, min=0.0, max=100.0)
    folder: bpy.props.StringProperty(name="Choose Folder", subtype="DIR_PATH")

    def invoke(self, context, event):
        
        return_code = context.window_manager.invoke_props_dialog(self, width = 300)
        return return_code

    def execute(self, context):
        
        input1 = self.input1
        input2 = self.input2
        folder = self.folder
        
        # My processing goes here
        print("Folder location is: " + str(folder))
        # Finished processing.
        
        return {'FINISHED'}

def register():
    bpy.utils.register_class(SelectDirExample)

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

if __name__ == "__main__":
    register()
    
bpy.ops.object.select_dir_example('INVOKE_DEFAULT')
$\endgroup$

1 Answer 1

2
$\begingroup$

Why does it crash?
Your code crashes blender because of a EXCEPTION_ACCESS_VIOLATION. For further information, see https://docs.blender.org/api/3.3/info_gotcha.html#help-my-script-crashes-blender. (you already know this, just for the sake of completeness)

Looking at the report it states:

Stack trace:
blender.exe         :0x00007FF616E36350  BLI_findstring
blender.exe         :0x00007FF611AE7CF0  IDP_AddToGroup
blender.exe         :0x00007FF612013EC0  RNA_property_string_set
blender.exe         :0x00007FF61658A070  file_browse_exec
blender.exe         :0x00007FF611DA05D0  wm_handler_fileselect_do
blender.exe         :0x00007FF611DA1F60  wm_handlers_do_intern
blender.exe         :0x00007FF611DA1390  wm_handlers_do
blender.exe         :0x00007FF611DA7920  wm_event_do_handlers
blender.exe         :0x00007FF611D8D820  WM_main
blender.exe         :0x00007FF610E82360  main
blender.exe         :0x00007FF617069750  __scrt_common_main_seh
KERNEL32.DLL        :0x00007FFB5D017600  BaseThreadInitThunk
ntdll.dll           :0x00007FFB5EF226D0  RtlUserThreadStart

Therefore I assume base on BLI_findstring the location of the folder:StringProperty has changed in memory after the selection, which then causes an EXCEPTION_ACCESS_VIOLATION.

Another attempt of explanation could be to add the following to the folder:StringProperty:

    def get_folder(self):
        return self.get("folder", "")
    def set_folder(self, value):
        self["folder"] = value
    folder: bpy.props.StringProperty(name="Choose Folder", subtype="DIR_PATH", get=get_folder, set=set_folder)

This will cause the following error:

Traceback (most recent call last):
  File "\Text", line 18, in set_folder
TypeError: unsubscriptable object
File "\Text", line 16, in set_folder
Traceback (most recent call last):
  File "\Text", line 16, in get_folder

because an instance of SelectDirExample is unsubscriptable to self['key'] = value. This getter, setter implementation maybe prevent the blender from crashing, because the default and setter, getter function are overwritten.

Solution 2 is preferred

Solution
I don't think an easy solution like this is possible because you are using a dialog which is a floating window and the folder selection will overwrite the current floating window. Also take a look at the info area/info-log of Blender, the Operator isn't executed/registered. Which means it hasn't returned {'FINISHED'} and execute was never called. Therefore, to implement something like this, you would need to call operator SelectDirExample again.

import bpy
from bpy.types import Operator, PropertyGroup

class SelectDirExample(Operator):
    bl_idname = "object.select_dir_example"
    bl_label = "Select Dir Example"
    bl_options = {'REGISTER', 'UNDO'}

    instances = 0
    invoked = False
    
    def setter_getter(name: str):
        return {
            'get': lambda self: getattr(bpy.context.scene.selected_dir, name),
            'set': lambda self, value: setattr(bpy.context.scene.selected_dir, name, value)
        }
    
    input1: bpy.props.FloatProperty(name="Input 1", default=1.0, min=0.0, max=100.0, **setter_getter('input1'))
    input2: bpy.props.FloatProperty(name="Input 2", default=1.0, min=0.0, max=100.0, **setter_getter('input2'))

    def get_folder(self):
        return bpy.context.scene.selected_dir.folder
    def set_folder(self, value):
        props = bpy.context.scene.selected_dir
        props.folder = value
        if SelectDirExample.instances != 0 or not SelectDirExample.invoked:
            return
        SelectDirExample.instances += 1
        bpy.ops.object.select_dir_example("INVOKE_DEFAULT", input1=props.input1, input2=props.input2, folder=props.folder)
        SelectDirExample.instances -= 1
        SelectDirExample.invoked = False
        
    folder: bpy.props.StringProperty(name="Choose Folder", subtype="DIR_PATH", get=get_folder, set=set_folder)

    def invoke(self, context, event):
        SelectDirExample.invoked = True
        return_code = context.window_manager.invoke_props_dialog(self, width = 300)
        return return_code

    def execute(self, context):
        
        input1 = self.input1
        input2 = self.input2
        folder = self.folder
        
        # My processing goes here
        print(f"Folder location is: {folder}")
        # Finished processing.
        
        SelectDirExample.invoked = False
        return {'FINISHED'}

class SelectDirExample_Props(PropertyGroup):
    input1: bpy.props.FloatProperty(name="Input 1", default=1.0, min=0.0, max=100.0)
    input2: bpy.props.FloatProperty(name="Input 2", default=1.0, min=0.0, max=100.0)
    folder: bpy.props.StringProperty(name="Choose Folder", subtype="DIR_PATH")

def register():
    bpy.utils.register_class(SelectDirExample)
    bpy.utils.register_class(SelectDirExample_Props)
    bpy.types.Scene.selected_dir = bpy.props.PointerProperty(type=SelectDirExample_Props)

def unregister():
    bpy.utils.unregister_class(SelectDirExample)
    bpy.utils.unregister_class(SelectDirExample_Props)
    del bpy.types.Scene.selected_folder

if __name__ == "__main__":
    register()

To not cause an EXCEPTION_ACCESS_VIOLATION another variable is used to store the folder path (which could also be inside the AddonPreferences). setter_getter is used to store the values into the same PropertyGroup as the folder. Otherwise, these values could not be moved along with the new operator call. They would be their default values because somehow the self in the set_folder is not the same as the self in the execute or invoke. A new instance with the stored variables will then be launched immediately, and it looks like before (only a movement in location of the dialog). The SelectDirExample.invoked variable is need otherwise it would launch two dialogs if the folder parameter is used on calling the operator e.g., bpy.ops.object.select_dir_example('INVOKE_DEFAULT', folder='<my_path>'). This does not apply if the call is from set_folder.

EDIT: Solution 2 This is a solution that uses an custom filebrowser operator.

import bpy
from bpy.types import Operator, PropertyGroup
from bpy_extras.io_utils import ExportHelper #, ImportHelper
import os

class SelectDirExample(Operator):
    bl_idname = "object.select_dir_example"
    bl_label = "Select Dir Example"
    bl_options = {'REGISTER', 'UNDO'}
    
    input1: bpy.props.FloatProperty(name="Input 1", default=1.0, min=0.0, max=100.0)
    input2: bpy.props.FloatProperty(name="Input 2", default=1.0, min=0.0, max=100.0)
    folder: bpy.props.StringProperty(name="Choose Folder")
    
    def draw_folder_selection(self, layout, property):
        row = layout.row(align=True)
        row.prop(self, property)
        ops = row.operator('select.directory_call_operator', text="", icon='FILEBROWSER')
        ops.operator_call = 'object.select_dir_example'
        # StringProperties need extra "" and \\ need to be doubled with my_string.replace("\\","\\\\")
        # E.g. my_string : StringProperty
        # f'my_string="{self.my_string}"'
        ops.operator_properties = f'input1={self.input1}, input2={self.input2}'
        ops.operator_folder_property_name = "folder"
        ops.old_directory = self.folder

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

        layout.prop(self, 'input1')
        layout.prop(self, 'input2')
        self.draw_folder_selection(layout, 'folder')
        
    def invoke(self, context, event):
        return_code = context.window_manager.invoke_props_dialog(self, width = 300)
        return return_code

    def execute(self, context):
        
        input1 = self.input1
        input2 = self.input2
        folder = self.folder
        
        # My processing goes here
        print(f"Folder location is: {folder}")
        # Finished processing.
        return {'FINISHED'}

# Use 'ImportHelper' when something is imported from this directory
class Select_OT_directory_call_operator(Operator, ExportHelper):
    bl_idname = "select.directory_call_operator"
    bl_label = "Select Directory"
    bl_description = " "
    bl_options = {'INTERNAL'}

    filename_ext = ""
    filename = ""
    filepath = ""
    filter_glob: bpy.props.StringProperty(
        default="",
        options={'HIDDEN'},
        maxlen=255,  # Max internal buffer length, longer would be clamped.
    )
    use_filter_folder = True
    
    directory: bpy.props.StringProperty(name="Folder Path", maxlen=1024, default="")

    operator_call : bpy.props.StringProperty(options={'HIDDEN'}, description="operator to call")
    operator_properties : bpy.props.StringProperty(options={'HIDDEN'}, description="operator properties diffrent from folder")
    operator_folder_property_name : bpy.props.StringProperty(options={'HIDDEN'}, description="folder property of the operator to apply the new path to")
    old_directory : bpy.props.StringProperty(options={'HIDDEN'}, description="old filepath of the operator used for cancel")
    
    def call_operator(self, filepath: str):
        filepath = filepath.replace("\\","\\\\") # needed because a str is placed into a str
        exec(f'bpy.ops.{self.operator_call}("INVOKE_DEFAULT", {self.operator_properties}, {self.operator_folder_property_name}="{filepath}")')
    
    def invoke(self, context, event):
        super().invoke(context, event)
        self.filepath = ""
        return {'RUNNING_MODAL'}
    
    def execute(self, context):
        user_path = self.properties.directory
        if (not os.path.isdir(user_path)):
            msg = f"Please select a directory not a file\n{user_path}"
            self.report({'ERROR'}, msg)
            self.cancel(context)
            return {'CANCELLED'}
        self.call_operator(user_path)
        return {'FINISHED'}
    
    def cancel(self, context):
        self.call_operator(self.old_directory)

def register():
    bpy.utils.register_class(SelectDirExample)
    bpy.utils.register_class(Select_OT_directory_call_operator)

def unregister():
    bpy.utils.unregister_class(SelectDirExample)
    bpy.utils.unregister_class(Select_OT_directory_call_operator)

if __name__ == "__main__":
    register()

With this solution there is no need define a setter and getter for the properties. Also the data of the properties do not need to be saved external.

EDIT 2 add invoke to delete filename on startup and use directory instead of filepath because this will only return the selected directory even if a file is selected

$\endgroup$
7
  • $\begingroup$ Wow! This works very well. Thank you so much! I will need some more time to understand why it works - I don't consider my understanding to be complete unless I get every bit of it, so I will experiment with it and come back. But I quickly tested and the solution works! That re-positioning of the dialog is kind of awkward, but I think I can take care of that in another way. You have actually provided 3 things: 1) One good reference to why the crash happens and I found explanations for some other issues I was facing 2) A good solution that works 3) An idea to re-open the dialog box! $\endgroup$
    – Jeet GT
    Commented Jun 20, 2023 at 18:49
  • $\begingroup$ Went through the code and experimented with it. I really appreciate your effort. The check for the instances is done in order to avoid a recursion - now I understand. However, I found two issues with this code apart from a typo. 1) If the user opens the file browser and clicks on cancel, the popup does not reopen because the setter is not called. 2) If the user opens the file browser 2nd time, it does not call the setter and the popup is closed. If we dont set SelectDirExample.invoked = False in the setter, it seems to work! But I'm not sure if it may have any other consequences anywhere else. $\endgroup$
    – Jeet GT
    Commented Jun 21, 2023 at 14:37
  • $\begingroup$ So the 1. Issue could be solved by using an own implementation of select directory with an operator(ImportHandler), which can will call the operator at the end, either on finished or cancel. This should also solve the 2. Issue, so we don't need the call in the folder setter. I tried to solve it like this, because you wanted minimal changes in the code. If you want an implementation with an ImportHanlder. I can add one to the solution if needed. $\endgroup$ Commented Jun 21, 2023 at 22:44
  • $\begingroup$ That will be very helpful, even for others who might come here for an accurate and complete solution. I wanted minimum changes to the UI or the user experience, code changes are welcome. Your solution definitely does not change anything in the user experience, it makes it better! Thank you so much for your help and guidance! $\endgroup$
    – Jeet GT
    Commented Jun 22, 2023 at 5:12
  • $\begingroup$ OK, I added the other solution with the custom filebrowser. $\endgroup$ Commented Jun 22, 2023 at 15:11

You must log in to answer this question.

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