diff --git a/doc/python_api/examples/bpy.types.FileHandler.1.py b/doc/python_api/examples/bpy.types.FileHandler.1.py new file mode 100644 index 00000000000..ebb498cd215 --- /dev/null +++ b/doc/python_api/examples/bpy.types.FileHandler.1.py @@ -0,0 +1,81 @@ +""" +Basic FileHandler for Operator that imports just one file +----------------- + +When creating a ``Operator`` that imports files, you may want to +add them 'drag-and-drop' support, File Handlers helps to define +a set of files extensions (:class:`FileHandler.bl_file_extensions`) +that the ``Operator`` support and a :class:`FileHandler.poll_drop` +function that can be used to check in what specific context the ``Operator`` +can be invoked with 'drag-and-drop' filepath data. + +Same as operators that uses the file select window, this operators +required a set of properties, when the ``Operator`` can import just one +file per execution it needs to define the following property: + +.. code-block:: python + filepath: bpy.props.StringProperty(subtype='FILE_PATH') + +This ``filepath`` property now will be used by the ``FileHandler`` to +set the 'drag-and-drop' filepath data. + +""" + +import bpy + + +class CurveTextImport(bpy.types.Operator): + """ Test importer that creates a text object from a .txt file """ + bl_idname = "curve.text_import" + bl_label = "Import a text file as text object" + + """ + This Operator supports import one .txt file at the time, we need the + following filepath property that the file handler will use to set file path data. + """ + filepath: bpy.props.StringProperty(subtype='FILE_PATH', options={'SKIP_SAVE'}) + + @classmethod + def poll(cls, context): + return (context.area and context.area.type == "VIEW_3D") + + def execute(self, context): + """ Calls to this Operator can set unfiltered filepaths, ensure the file extension is .txt. """ + if not self.filepath or not self.filepath.endswith(".txt"): + return {'CANCELLED'} + + with open(self.filepath) as file: + text_curve = bpy.data.curves.new(type="FONT", name="Text") + text_curve.body = ''.join(file.readlines()) + text_object = bpy.data.objects.new(name="Text", object_data=text_curve) + bpy.context.scene.collection.objects.link(text_object) + return {'FINISHED'} + + """ + By default the file handler invokes the operator with the filepath property set. + In this example if this property is set the operator is executed, if not the + file select window is invoked. + This depends on setting 'options={'SKIP_SAVE'}' to the property options to avoid + to reuse filepath data between operator calls. + """ + + def invoke(self, context, event): + if self.filepath: + return self.execute(context) + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} + + +class CURVE_FH_text_import(bpy.types.FileHandler): + bl_idname = "CURVE_FH_text_import" + bl_label = "File handler for curve text object import" + bl_import_operator = "curve.text_import" + bl_file_extensions = ".txt" + + @classmethod + def poll_drop(cls, context): + return (context.area and context.area.type == 'VIEW_3D') + + +bpy.utils.register_class(CurveTextImport) +bpy.utils.register_class(CURVE_FH_text_import) diff --git a/doc/python_api/examples/bpy.types.FileHandler.2.py b/doc/python_api/examples/bpy.types.FileHandler.2.py new file mode 100644 index 00000000000..7507a944ec6 --- /dev/null +++ b/doc/python_api/examples/bpy.types.FileHandler.2.py @@ -0,0 +1,91 @@ +""" +Basic FileHandler for Operator that imports multiple files +----------------- + +Also operators can be invoked with multiple files from 'drag-and-drop', +but for this it is require to define the following properties: + +.. code-block:: python + directory: StringProperty(subtype='FILE_PATH') + files: CollectionProperty(type=bpy.types.OperatorFileListElement) + +This ``directory`` and ``files`` properties now will be used by the +``FileHandler`` to set 'drag-and-drop' filepath data. + +""" + +import bpy +from mathutils import Vector + + +class ShaderScriptImport(bpy.types.Operator): + """Test importer that creates scripts nodes from .txt files""" + bl_idname = "shader.script_import" + bl_label = "Import a text file as a script node" + + """ + This Operator can import multiple .txt files, we need following directory and files + properties that the file handler will use to set files path data + """ + directory: bpy.props.StringProperty(subtype='FILE_PATH', options={'SKIP_SAVE'}) + files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={'SKIP_SAVE'}) + + @classmethod + def poll(cls, context): + return (context.region and context.region.type == 'WINDOW' + and context.area and context.area.ui_type == 'ShaderNodeTree' + and context.object and context.object.type == 'MESH' + and context.material) + + def execute(self, context): + """ The directory property need to be set. """ + if not self.directory: + return {'CANCELLED'} + x = 0.0 + y = 0.0 + for file in self.files: + """ + Calls to the operator can set unfiltered file names, + ensure the file extension is .txt + """ + if file.name.endswith(".txt"): + node_tree = context.material.node_tree + text_node = node_tree.nodes.new(type="ShaderNodeScript") + text_node.mode = 'EXTERNAL' + import os + filepath = os.path.join(self.directory, file.name) + text_node.filepath = filepath + text_node.location = Vector((x, y)) + x += 20.0 + y -= 20.0 + return {'FINISHED'} + + """ + By default the file handler invokes the operator with the directory and files properties set. + In this example if this properties are set the operator is executed, if not the + file select window is invoked. + This depends on setting 'options={'SKIP_SAVE'}' to the properties options to avoid + to reuse filepath data between operator calls. + """ + + def invoke(self, context, event): + if self.directory: + return self.execute(context) + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} + + +class SHADER_FH_script_import(bpy.types.FileHandler): + bl_idname = "SHADER_FH_script_import" + bl_label = "File handler for shader script node import" + bl_import_operator = "shader.script_import" + bl_file_extensions = ".txt" + + @classmethod + def poll_drop(cls, context): + return (context.region and context.region.type == 'WINDOW' + and context.area and context.area.ui_type == 'ShaderNodeTree') + + +bpy.utils.register_class(ShaderScriptImport) +bpy.utils.register_class(SHADER_FH_script_import) diff --git a/source/blender/blenkernel/BKE_file_handler.hh b/source/blender/blenkernel/BKE_file_handler.hh index d2e65d52c59..a66818c12b0 100644 --- a/source/blender/blenkernel/BKE_file_handler.hh +++ b/source/blender/blenkernel/BKE_file_handler.hh @@ -35,6 +35,11 @@ struct FileHandlerType { /** RNA integration. */ ExtensionRNA rna_ext; + + /** + * Return a vector of indices in #paths of file paths supported by the file handler. + */ + blender::Vector filter_supported_paths(const blender::Span paths) const; }; /** @@ -58,4 +63,12 @@ void file_handler_remove(FileHandlerType *file_handler); /** Return pointers to all registered file handlers. */ Span> file_handlers(); +/** + * Return a vector of file handlers that support any file path in `paths` and the call to + * `poll_drop` returns #true. Caller must check if each file handler have a valid + * `import_operator`. + */ +blender::Vector file_handlers_poll_file_drop( + const bContext *C, const blender::Span paths); + } // namespace blender::bke diff --git a/source/blender/blenkernel/intern/file_handler.cc b/source/blender/blenkernel/intern/file_handler.cc index f6c966e483c..9653ea470ee 100644 --- a/source/blender/blenkernel/intern/file_handler.cc +++ b/source/blender/blenkernel/intern/file_handler.cc @@ -6,6 +6,7 @@ #include "BKE_file_handler.hh" +#include "BLI_path_util.h" #include "BLI_string.h" namespace blender::bke { @@ -62,4 +63,58 @@ void file_handler_remove(FileHandlerType *file_handler) }); } +blender::Vector file_handlers_poll_file_drop( + const bContext *C, const blender::Span paths) +{ + blender::Vector path_extensions; + for (const std::string &path : paths) { + const char *extension = BLI_path_extension(path.c_str()); + if (!extension) { + continue; + } + path_extensions.append_non_duplicates(extension); + } + + blender::Vector result; + for (const std::unique_ptr &file_handler_ptr : file_handlers()) { + FileHandlerType &file_handler = *file_handler_ptr; + const auto &file_extensions = file_handler.file_extensions; + + const bool support_any_extension = std::any_of( + file_extensions.begin(), + file_extensions.end(), + [&path_extensions](const std::string &test_extension) { + return path_extensions.contains(test_extension); + }); + + if (!support_any_extension) { + continue; + } + + if (!(file_handler.poll_drop && file_handler.poll_drop(C, &file_handler))) { + continue; + } + + result.append(&file_handler); + } + return result; +} + +blender::Vector FileHandlerType::filter_supported_paths( + const blender::Span paths) const +{ + blender::Vector indices; + + for (const int idx : paths.index_range()) { + const char *extension = BLI_path_extension(paths[idx].c_str()); + if (!extension) { + continue; + } + if (file_extensions.contains(extension)) { + indices.append(idx); + } + } + return indices; +} + } // namespace blender::bke diff --git a/source/blender/editors/io/CMakeLists.txt b/source/blender/editors/io/CMakeLists.txt index ea2917a9809..7b4ae0e1667 100644 --- a/source/blender/editors/io/CMakeLists.txt +++ b/source/blender/editors/io/CMakeLists.txt @@ -17,6 +17,7 @@ set(INC ../../io/wavefront_obj ../../makesrna ../../windowmanager + ${CMAKE_BINARY_DIR}/source/blender/makesrna ) set(INC_SYS @@ -27,6 +28,7 @@ set(SRC io_alembic.cc io_cache.cc io_collada.cc + io_drop_import_file.cc io_gpencil_export.cc io_gpencil_import.cc io_gpencil_utils.cc @@ -39,6 +41,7 @@ set(SRC io_alembic.hh io_cache.hh io_collada.hh + io_drop_import_file.hh io_gpencil.hh io_obj.hh io_ops.hh @@ -53,6 +56,7 @@ set(LIB PRIVATE bf::depsgraph PRIVATE bf::dna PRIVATE bf::intern::guardedalloc + PRIVATE bf::intern::clog ) if(WITH_OPENCOLLADA) diff --git a/source/blender/editors/io/io_drop_import_file.cc b/source/blender/editors/io/io_drop_import_file.cc new file mode 100644 index 00000000000..fc92b68e557 --- /dev/null +++ b/source/blender/editors/io/io_drop_import_file.cc @@ -0,0 +1,250 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BLI_path_util.h" +#include "BLI_string.h" + +#include "BLT_translation.h" + +#include "BKE_file_handler.hh" + +#include "CLG_log.h" + +#include "DNA_space_types.h" + +#include "RNA_access.hh" +#include "RNA_define.hh" +#include "RNA_prototypes.h" + +#include "WM_api.hh" +#include "WM_types.hh" + +#include "UI_interface.hh" + +#include "io_drop_import_file.hh" + +static CLG_LogRef LOG = {"io.drop_import_file"}; + +/* Retuns the list of file paths stored in #WM_OT_drop_import_file operator properties. */ +static blender::Vector drop_import_file_paths(const wmOperator *op) +{ + blender::Vector result; + char dir[FILE_MAX], file[FILE_MAX]; + + RNA_string_get(op->ptr, "directory", dir); + + PropertyRNA *prop = RNA_struct_find_property(op->ptr, "files"); + int files_len = RNA_property_collection_length(op->ptr, prop); + + for (int i = 0; i < files_len; i++) { + PointerRNA fileptr; + RNA_property_collection_lookup_int(op->ptr, prop, i, &fileptr); + RNA_string_get(&fileptr, "name", file); + char file_path[FILE_MAX]; + BLI_path_join(file_path, sizeof(file_path), dir, file); + result.append(file_path); + } + return result; +} + +/** + * Return a vector of file handlers that support any file path in `paths` and the call to + * `poll_drop` returns #true. Unlike `bke::file_handlers_poll_file_drop`, it ensures that file + * handlers have a valid import operator. + */ +static blender::Vector drop_import_file_poll_file_handlers( + const bContext *C, const blender::Span paths, const bool quiet = true) +{ + using namespace blender; + auto file_handlers = bke::file_handlers_poll_file_drop(C, paths); + file_handlers.remove_if([quiet](const bke::FileHandlerType *file_handler) { + return WM_operatortype_find(file_handler->import_operator, quiet) == nullptr; + }); + return file_handlers; +} + +/** + * Creates a RNA pointer for the `FileHandlerType.import_operator` and sets on it all supported + * file paths from `paths`. + */ +static PointerRNA file_handler_import_operator_create_ptr( + const blender::bke::FileHandlerType *file_handler, const blender::Span paths) +{ + wmOperatorType *ot = WM_operatortype_find(file_handler->import_operator, false); + BLI_assert(ot != nullptr); + PointerRNA props; + WM_operator_properties_create_ptr(&props, ot); + + const auto supported_paths = file_handler->filter_supported_paths(paths); + + PropertyRNA *filepath_prop = RNA_struct_find_property_check(props, "filepath", PROP_STRING); + if (filepath_prop) { + RNA_property_string_set(&props, filepath_prop, paths[supported_paths[0]].c_str()); + } + + PropertyRNA *directory_prop = RNA_struct_find_property_check(props, "directory", PROP_STRING); + if (directory_prop) { + char dir[FILE_MAX]; + BLI_path_split_dir_part(paths[0].c_str(), dir, sizeof(dir)); + RNA_property_string_set(&props, directory_prop, dir); + } + + PropertyRNA *files_prop = RNA_struct_find_collection_property_check( + props, "files", &RNA_OperatorFileListElement); + if (files_prop) { + RNA_property_collection_clear(&props, files_prop); + for (const auto &index : supported_paths) { + char file[FILE_MAX]; + BLI_path_split_file_part(paths[index].c_str(), file, sizeof(file)); + + PointerRNA item_ptr{}; + RNA_property_collection_add(&props, files_prop, &item_ptr); + RNA_string_set(&item_ptr, "name", file); + } + } + const bool has_any_filepath_prop = filepath_prop || directory_prop || files_prop; + /** + * The `directory` and `files` properties are both required for handling multiple files, if + * only one is defined means that the other is missing. + */ + const bool has_missing_filepath_prop = bool(directory_prop) != bool(files_prop); + + if (!has_any_filepath_prop || has_missing_filepath_prop) { + const char *message = + "Expected operator properties filepath or files and directory not found. Refer to " + "FileHandler documentation for details."; + CLOG_WARN(&LOG, TIP_(message)); + } + return props; +} + +static int wm_drop_import_file_exec(bContext *C, wmOperator *op) +{ + auto paths = drop_import_file_paths(op); + if (paths.is_empty()) { + return OPERATOR_CANCELLED; + } + + auto file_handlers = drop_import_file_poll_file_handlers(C, paths, false); + if (file_handlers.is_empty()) { + return OPERATOR_CANCELLED; + } + + wmOperatorType *ot = WM_operatortype_find(file_handlers[0]->import_operator, false); + PointerRNA file_props = file_handler_import_operator_create_ptr(file_handlers[0], paths); + + WM_operator_name_call_ptr(C, ot, WM_OP_INVOKE_DEFAULT, &file_props, nullptr); + WM_operator_properties_free(&file_props); + return OPERATOR_FINISHED; +} + +static int wm_drop_import_file_invoke(bContext *C, wmOperator *op, const wmEvent * /*event*/) +{ + const auto paths = drop_import_file_paths(op); + if (paths.is_empty()) { + return OPERATOR_CANCELLED; + } + + auto file_handlers = drop_import_file_poll_file_handlers(C, paths, false); + if (file_handlers.size() == 1) { + return wm_drop_import_file_exec(C, op); + } + + /** + * Create a menu with all file handler import operators that can support any files in paths and + * let user decide which to use. + */ + uiPopupMenu *pup = UI_popup_menu_begin(C, "", ICON_NONE); + uiLayout *layout = UI_popup_menu_layout(pup); + uiLayoutSetOperatorContext(layout, WM_OP_INVOKE_DEFAULT); + + for (auto *file_handler : file_handlers) { + const PointerRNA file_props = file_handler_import_operator_create_ptr(file_handler, paths); + wmOperatorType *ot = WM_operatortype_find(file_handler->import_operator, false); + uiItemFullO_ptr(layout, + ot, + TIP_(ot->name), + ICON_NONE, + static_cast(file_props.data), + WM_OP_INVOKE_DEFAULT, + UI_ITEM_NONE, + nullptr); + } + + UI_popup_menu_end(C, pup); + return OPERATOR_INTERFACE; +} + +void WM_OT_drop_import_file(wmOperatorType *ot) +{ + ot->name = "Drop to Import File"; + ot->description = "Operator that allows file handlers to receive file drops"; + ot->idname = "WM_OT_drop_import_file"; + ot->flag = OPTYPE_INTERNAL; + ot->exec = wm_drop_import_file_exec; + ot->invoke = wm_drop_import_file_invoke; + + PropertyRNA *prop; + + prop = RNA_def_string_dir_path( + ot->srna, "directory", nullptr, FILE_MAX, "Directory", "Directory of the file"); + RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE); + + prop = RNA_def_collection_runtime(ot->srna, "files", &RNA_OperatorFileListElement, "Files", ""); + RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE); +} + +void drop_import_file_copy(bContext * /*C*/, wmDrag *drag, wmDropBox *drop) +{ + const auto paths = WM_drag_get_paths(drag); + + char dir[FILE_MAX]; + BLI_path_split_dir_part(paths[0].c_str(), dir, sizeof(dir)); + RNA_string_set(drop->ptr, "directory", dir); + + RNA_collection_clear(drop->ptr, "files"); + for (const auto &path : paths) { + char file[FILE_MAX]; + BLI_path_split_file_part(path.c_str(), file, sizeof(file)); + + PointerRNA itemptr{}; + RNA_collection_add(drop->ptr, "files", &itemptr); + RNA_string_set(&itemptr, "name", file); + } +} + +static bool drop_import_file_poll(bContext *C, wmDrag *drag, const wmEvent * /*event*/) +{ + if (drag->type != WM_DRAG_PATH) { + return false; + } + const auto paths = WM_drag_get_paths(drag); + return !drop_import_file_poll_file_handlers(C, paths, true).is_empty(); +} + +static char *drop_import_file_tooltip(bContext *C, + wmDrag *drag, + const int /*xy*/[2], + wmDropBox * /*drop*/) +{ + const auto paths = WM_drag_get_paths(drag); + const auto file_handlers = drop_import_file_poll_file_handlers(C, paths, true); + if (file_handlers.size() == 1) { + wmOperatorType *ot = WM_operatortype_find(file_handlers[0]->import_operator, false); + return BLI_strdup(TIP_(ot->name)); + } + + return BLI_strdup(TIP_("Multiple file handlers can be used, drop to pick which to use")); +} + +void ED_dropbox_drop_import_file() +{ + ListBase *lb = WM_dropboxmap_find("Window", SPACE_EMPTY, RGN_TYPE_WINDOW); + WM_dropbox_add(lb, + "WM_OT_drop_import_file", + drop_import_file_poll, + drop_import_file_copy, + nullptr, + drop_import_file_tooltip); +} diff --git a/source/blender/editors/io/io_drop_import_file.hh b/source/blender/editors/io/io_drop_import_file.hh new file mode 100644 index 00000000000..5c5f6e66af0 --- /dev/null +++ b/source/blender/editors/io/io_drop_import_file.hh @@ -0,0 +1,8 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#pragma once + +void WM_OT_drop_import_file(wmOperatorType *ot); +void ED_dropbox_drop_import_file(); diff --git a/source/blender/editors/io/io_ops.cc b/source/blender/editors/io/io_ops.cc index b255f43c7ce..50592214fab 100644 --- a/source/blender/editors/io/io_ops.cc +++ b/source/blender/editors/io/io_ops.cc @@ -23,6 +23,7 @@ #endif #include "io_cache.hh" +#include "io_drop_import_file.hh" #include "io_gpencil.hh" #include "io_obj.hh" #include "io_ply_ops.hh" @@ -74,4 +75,6 @@ void ED_operatortypes_io() WM_operatortype_append(WM_OT_stl_import); WM_operatortype_append(WM_OT_stl_export); #endif + WM_operatortype_append(WM_OT_drop_import_file); + ED_dropbox_drop_import_file(); } diff --git a/source/blender/makesrna/RNA_access.hh b/source/blender/makesrna/RNA_access.hh index 16b78d07f0d..626c2486d28 100644 --- a/source/blender/makesrna/RNA_access.hh +++ b/source/blender/makesrna/RNA_access.hh @@ -112,6 +112,22 @@ bool RNA_struct_idprops_contains_datablock(const StructRNA *type); bool RNA_struct_idprops_unset(PointerRNA *ptr, const char *identifier); PropertyRNA *RNA_struct_find_property(PointerRNA *ptr, const char *identifier); + +/** + * Same as `RNA_struct_find_property` but returns `nullptr` if the property type is no same to + * `property_type_check`. + */ +PropertyRNA *RNA_struct_find_property_check(PointerRNA &props, + const char *name, + const PropertyType property_type_check); +/** + * Same as `RNA_struct_find_property` but returns `nullptr` if the property type is not + * #PropertyType::PROP_COLLECTION or property struct type is different to `struct_type_check`. + */ +PropertyRNA *RNA_struct_find_collection_property_check(PointerRNA &props, + const char *name, + const StructRNA *struct_type_check); + bool RNA_struct_contains_property(PointerRNA *ptr, PropertyRNA *prop_test); unsigned int RNA_struct_count_properties(StructRNA *srna); diff --git a/source/blender/makesrna/intern/rna_access.cc b/source/blender/makesrna/intern/rna_access.cc index fb3bb01b006..bc7c9ddc0c9 100644 --- a/source/blender/makesrna/intern/rna_access.cc +++ b/source/blender/makesrna/intern/rna_access.cc @@ -41,6 +41,8 @@ #include "BKE_node.hh" #include "BKE_report.h" +#include "CLG_log.h" + #include "DEG_depsgraph.hh" #include "DEG_depsgraph_build.hh" @@ -61,6 +63,8 @@ const PointerRNA PointerRNA_NULL = {nullptr}; +static CLG_LogRef LOG = {"rna.access"}; + /* Init/Exit */ void RNA_init() @@ -780,6 +784,86 @@ PropertyRNA *RNA_struct_find_property(PointerRNA *ptr, const char *identifier) return nullptr; } +static const char *rna_property_type_identifier(PropertyType prop_type) +{ + switch (prop_type) { + case PROP_BOOLEAN: + return RNA_struct_identifier(&RNA_BoolProperty); + case PROP_INT: + return RNA_struct_identifier(&RNA_IntProperty); + case PROP_FLOAT: + return RNA_struct_identifier(&RNA_FloatProperty); + case PROP_STRING: + return RNA_struct_identifier(&RNA_StringProperty); + case PROP_ENUM: + return RNA_struct_identifier(&RNA_EnumProperty); + case PROP_POINTER: + return RNA_struct_identifier(&RNA_PointerProperty); + case PROP_COLLECTION: + return RNA_struct_identifier(&RNA_CollectionProperty); + default: + return RNA_struct_identifier(&RNA_Property); + } +} + +PropertyRNA *RNA_struct_find_property_check(PointerRNA &props, + const char *name, + const PropertyType property_type_check) +{ + PropertyRNA *prop = RNA_struct_find_property(&props, name); + if (!prop) { + return nullptr; + } + const PropertyType prop_type = RNA_property_type(prop); + if (prop_type == property_type_check) { + return prop; + } + CLOG_WARN(&LOG, + TIP_("'%s : %s()' expected, got '%s : %s()'"), + name, + rna_property_type_identifier(property_type_check), + name, + rna_property_type_identifier(prop_type)); + return nullptr; +} + +PropertyRNA *RNA_struct_find_collection_property_check(PointerRNA &props, + const char *name, + const StructRNA *struct_type_check) +{ + PropertyRNA *prop = RNA_struct_find_property(&props, name); + if (!prop) { + return nullptr; + } + + const PropertyType prop_type = RNA_property_type(prop); + const StructRNA *prop_struct_type = RNA_property_pointer_type(&props, prop); + if (prop_type == PROP_COLLECTION && prop_struct_type == struct_type_check) { + return prop; + } + + if (prop_type != PROP_COLLECTION) { + CLOG_WARN(&LOG, + TIP_("'%s : %s(type = %s)' expected, got '%s : %s()'"), + name, + rna_property_type_identifier(PROP_COLLECTION), + RNA_struct_identifier(struct_type_check), + name, + rna_property_type_identifier(prop_type)); + return nullptr; + } + + CLOG_WARN(&LOG, + TIP_("'%s : %s(type = %s)' expected, got '%s : %s(type = %s)'."), + name, + rna_property_type_identifier(PROP_COLLECTION), + RNA_struct_identifier(struct_type_check), + name, + rna_property_type_identifier(PROP_COLLECTION), + RNA_struct_identifier(prop_struct_type)); + return nullptr; +} + PropertyRNA *rna_struct_find_nested(PointerRNA *ptr, StructRNA *srna) { PropertyRNA *prop = nullptr;