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