Add support to Drag and Drop to FileHandlers #116047

Merged
Jesse Yurkovich merged 16 commits from guishe/blender:fh-dnd-support into main 2024-01-06 03:51:57 +01:00
10 changed files with 605 additions and 0 deletions

View File

@ -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'}
guishe marked this conversation as resolved Outdated

Should be CURVE_FH_text_import (here and a few other places etc.) to prevent "Warning: 'Curve_FH_text_import' doesn't have upper case alpha-numeric prefix"

Should be `CURVE_FH_text_import` (here and a few other places etc.) to prevent "Warning: 'Curve_FH_text_import' doesn't have upper case alpha-numeric prefix"
"""
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)

View File

@ -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
guishe marked this conversation as resolved Outdated

Adjust the casing of the name of this FileHandler too.

Adjust the casing of the name of this FileHandler too.
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)

View File

@ -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<int64_t> filter_supported_paths(const blender::Span<std::string> paths) const;
};
/**
@ -58,4 +63,12 @@ void file_handler_remove(FileHandlerType *file_handler);
/** Return pointers to all registered file handlers. */
Span<std::unique_ptr<FileHandlerType>> 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<FileHandlerType *> file_handlers_poll_file_drop(
const bContext *C, const blender::Span<std::string> paths);
} // namespace blender::bke

View File

@ -4,6 +4,7 @@
#include "BKE_file_handler.hh"
#include "BLI_path_util.h"
#include "BLI_string.h"
namespace blender::bke {
@ -60,4 +61,58 @@ void file_handler_remove(FileHandlerType *file_handler)
});
}
blender::Vector<FileHandlerType *> file_handlers_poll_file_drop(
const bContext *C, const blender::Span<std::string> paths)
{
blender::Vector<std::string> 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<FileHandlerType *> result;
for (const std::unique_ptr<FileHandlerType> &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<int64_t> FileHandlerType::filter_supported_paths(
const blender::Span<std::string> paths) const
{
blender::Vector<int64_t> 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

View File

@ -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)

View File

@ -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<std::string> drop_import_file_paths(const wmOperator *op)
{
blender::Vector<std::string> 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;
guishe marked this conversation as resolved Outdated

paths.empty()

paths.empty()
}
/**
* 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
guishe marked this conversation as resolved Outdated

file_handlers.empty()

file_handlers.empty()
* handlers have a valid import operator.
*/
static blender::Vector<blender::bke::FileHandlerType *> drop_import_file_poll_file_handlers(
const bContext *C, const blender::Span<std::string> 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<std::string> 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<IDProperty *>(file_props.data),
guishe marked this conversation as resolved
Review

Wondering if maybe we can just place this inside ED_keymap_screen as that's where the .blend file drag-drop box is added. Should use SPACE_EMPTY and RGN_TYPE_WINDOW values instead of 0 too.

Wondering if maybe we can just place this inside `ED_keymap_screen` as that's where the .blend file drag-drop box is added. Should use `SPACE_EMPTY` and `RGN_TYPE_WINDOW` values instead of 0 too.

had to duplicate drop_import_file_poll_file_handlers between io_drop_import_file.cc
and screen_ops.cc, only has the additional check for file handlers with valid operator.

this was on BKE_file_handlers_poll_file_drop but removing the wm dependency from bke dont know the right place to validate this

had to duplicate `drop_import_file_poll_file_handlers` between `io_drop_import_file.cc` and `screen_ops.cc`, only has the additional check for file handlers with valid operator. this was on `BKE_file_handlers_poll_file_drop` but removing the `wm` dependency from `bke` dont know the right place to validate this
Review

I didn't notice that my suggestion would cause this duplication. In that case I think it's fine to keep the WM_dropbox_add/ED_dropbox_drop_import_file call inside the "io_drop_import_file.cc" file as you originally had it.

Other than that, things look fine.

I didn't notice that my suggestion would cause this duplication. In that case I think it's fine to keep the WM_dropbox_add/ED_dropbox_drop_import_file call inside the "io_drop_import_file.cc" file as you originally had it. Other than that, things look fine.
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);
}

View File

@ -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();

View File

@ -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();
}

View File

@ -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);

View File

@ -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;