IO: Add initial support for File Handlers registration #112466

Merged
Jesse Yurkovich merged 15 commits from guishe/blender:file-handler into main 2023-12-09 05:06:24 +01:00
8 changed files with 422 additions and 1 deletions

View File

@ -1208,6 +1208,10 @@ class AssetShelf(StructRNA, metaclass=RNAMeta):
__slots__ = ()
class FileHandler(StructRNA, metaclass=RNAMeta):
__slots__ = ()
class NodeTree(bpy_types.ID, metaclass=RNAMetaPropGroup):
__slots__ = ()

View File

@ -0,0 +1,51 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_vector.hh"
#include "DNA_windowmanager_types.h"
#include "RNA_types.hh"
#define FH_MAX_FILE_EXTENSIONS_STR 512
struct FileHandlerType {
/** Unique name. */
char idname[OP_MAX_TYPENAME];
/** For UI text. */
char label[OP_MAX_TYPENAME];
/** Import operator name. */
char import_operator[OP_MAX_TYPENAME];
/** Formatted string of file extensions supported by the file handler, each extension should
* start with a `.` and be separated by `;`. For Example: `".blend;.ble"`. */
char file_extensions_str[FH_MAX_FILE_EXTENSIONS_STR];
/** Check if file handler can be used on file drop. */
bool (*poll_drop)(const struct bContext *C, FileHandlerType *file_handle_type);
/** List of file extensions supported by the file handler. */
blender::Vector<std::string> file_extensions;
/** RNA integration. */
ExtensionRNA rna_ext;
};
/**
* Adds a new `file_handler` to the `file_handlers` list, also loads all the file extensions from
* the formatted `FileHandlerType.file_extensions_str` string to `FileHandlerType.file_extensions`
* list.
*
* The new `file_handler` is expected to have a unique `FileHandlerType.idname`.
*/
void BKE_file_handler_add(std::unique_ptr<FileHandlerType> file_handler);
/** Returns a `file_handler` that have a specific `idname`, otherwise return `nullptr`. */
FileHandlerType *BKE_file_handler_find(const char *idname);
/** Removes and frees a specific `file_handler` from the `file_handlers` list, the `file_handler`
* pointer will be not longer valid for use. */
void BKE_file_handler_remove(FileHandlerType *file_handler);
/** Return a reference of the #RawVector with all `file_handlers` registered. */
const blender::RawVector<std::unique_ptr<FileHandlerType>> &BKE_file_handlers();

View File

@ -129,6 +129,7 @@ set(SRC
intern/fcurve.cc
intern/fcurve_cache.cc
intern/fcurve_driver.cc
intern/file_handler.cc
intern/fluid.cc
intern/fmodifier.cc
intern/freestyle.cc
@ -385,6 +386,7 @@ set(SRC
BKE_effect.h
BKE_fcurve.h
BKE_fcurve_driver.h
BKE_file_handler.hh
BKE_fluid.h
BKE_freestyle.h
BKE_geometry_fields.hh
@ -851,6 +853,7 @@ if(WITH_GTESTS)
intern/main_test.cc
intern/nla_test.cc
intern/tracking_test.cc
intern/file_handler_test.cc
)
set(TEST_INC
../editors/include

View File

@ -0,0 +1,59 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BKE_file_handler.hh"
#include "BLI_string.h"
static blender::RawVector<std::unique_ptr<FileHandlerType>> &file_handlers()
{
static blender::RawVector<std::unique_ptr<FileHandlerType>> file_handlers;
return file_handlers;
}
const blender::RawVector<std::unique_ptr<FileHandlerType>> &BKE_file_handlers()
{
return file_handlers();
}
FileHandlerType *BKE_file_handler_find(const char *name)
{
auto itr = std::find_if(file_handlers().begin(),
file_handlers().end(),
[name](const std::unique_ptr<FileHandlerType> &file_handler) {
return STREQ(name, file_handler->idname);
});
if (itr != file_handlers().end()) {
return itr->get();
}
return nullptr;
}
void BKE_file_handler_add(std::unique_ptr<FileHandlerType> file_handler)
{
BLI_assert(BKE_file_handler_find(file_handler->idname) != nullptr);
/** Load all extensions from the string list into the list. */
const char char_separator = ';';
const char *char_begin = file_handler->file_extensions_str;
const char *char_end = BLI_strchr_or_end(char_begin, char_separator);
while (char_begin[0]) {
if (char_end - char_begin > 1) {
std::string file_extension(char_begin, char_end - char_begin);
file_handler->file_extensions.append(file_extension);
}
char_begin = char_end[0] ? char_end + 1 : char_end;
char_end = BLI_strchr_or_end(char_begin, char_separator);
}
file_handlers().append(std::move(file_handler));
}
void BKE_file_handler_remove(FileHandlerType *file_handler)
{
file_handlers().remove_if(
[file_handler](const std::unique_ptr<FileHandlerType> &test_file_handler) {
return test_file_handler.get() == file_handler;
});
}

View File

@ -0,0 +1,113 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: Apache-2.0 */
#include "BKE_file_handler.hh"
#include "testing/testing.h"
namespace blender::tests {
#define MAX_FILE_HANDLERS_TEST_SIZE 8
static FileHandlerType *file_handlers[MAX_FILE_HANDLERS_TEST_SIZE];
static void file_handler_add_test(const int test_number,
const char *idname,
const char *label,
const char *file_extensions_str,
blender::Vector<std::string> expected_file_extensions)
{
EXPECT_LE(test_number, MAX_FILE_HANDLERS_TEST_SIZE);
EXPECT_GE(test_number, 1);
EXPECT_EQ(BKE_file_handlers().size(), test_number - 1);
std::unique_ptr<FileHandlerType> file_handler = std::make_unique<FileHandlerType>();
file_handlers[test_number - 1] = file_handler.get();
strcpy(file_handler->idname, idname);
strcpy(file_handler->file_extensions_str, file_extensions_str);
strcpy(file_handler->label, label);
BKE_file_handler_add(std::move(file_handler));
EXPECT_EQ(BKE_file_handlers().size(), test_number);
EXPECT_EQ(BKE_file_handlers()[test_number - 1].get(), file_handlers[test_number - 1]);
EXPECT_EQ(BKE_file_handlers()[test_number - 1]->file_extensions, expected_file_extensions);
}
TEST(file_handler, add)
{
file_handler_add_test(1,
"Test_FH_blender1",
"File Handler Test 1",
".blender;.blend;.ble",
guishe marked this conversation as resolved
Review

This label and the next two ones seem to be off-by-one, is this expected? or copy-paste glitch?

This label and the next two ones seem to be off-by-one, is this expected? or copy-paste glitch?
{".blender", ".blend", ".ble"});
file_handler_add_test(2, "Test_FH_blender2", "File Handler Test 2", ".ble", {".ble"});
file_handler_add_test(3, "Test_FH_blender3", "File Handler Test 3", ";;.ble", {".ble"});
file_handler_add_test(4, "Test_FH_blender4", "File Handler Test 4", ";.ble;", {".ble"});
file_handler_add_test(5, "Test_FH_blender5", "File Handler Test 5", "d", {});
file_handler_add_test(6, "Test_FH_blender6", "File Handler Test 6", ";;", {});
file_handler_add_test(7, "Test_FH_blender7", "File Handler Test 7", ".", {});
file_handler_add_test(8, "Test_FH_blender8", "File Handler Test 8", "", {});
}
TEST(file_handler, find)
guishe marked this conversation as resolved
Review

Useless empty line ;)

Useless empty line ;)
{
EXPECT_EQ(BKE_file_handlers().size(), MAX_FILE_HANDLERS_TEST_SIZE);
EXPECT_EQ(BKE_file_handler_find("Test_FH_blender1"), file_handlers[0]);
EXPECT_EQ(BKE_file_handler_find("Test_FH_blender2"), file_handlers[1]);
EXPECT_EQ(BKE_file_handler_find("Test_FH_blender3"), file_handlers[2]);
EXPECT_EQ(BKE_file_handler_find("Test_FH_blender4"), file_handlers[3]);
EXPECT_EQ(BKE_file_handler_find("Test_FH_blender5"), file_handlers[4]);
EXPECT_EQ(BKE_file_handler_find("Test_FH_blender6"), file_handlers[5]);
EXPECT_EQ(BKE_file_handler_find("Test_FH_blender7"), file_handlers[6]);
EXPECT_EQ(BKE_file_handler_find("Test_FH_blender8"), file_handlers[7]);
EXPECT_EQ(BKE_file_handler_find("Test_FH_blende"), nullptr);
EXPECT_EQ(BKE_file_handler_find("TstFH_blen"), nullptr);
}
TEST(file_handler, remove)
{
EXPECT_EQ(BKE_file_handlers().size(), MAX_FILE_HANDLERS_TEST_SIZE);
BKE_file_handler_remove(BKE_file_handler_find("Test_FH_blender2"));
EXPECT_EQ(BKE_file_handlers().size(), MAX_FILE_HANDLERS_TEST_SIZE - 1);
EXPECT_EQ(BKE_file_handler_find("Test_FH_blender2"), nullptr);
/** `FileHandlerType` pointer in `file_handlers[1]` is not longer valid. */
EXPECT_EQ(BKE_file_handler_find("Test_FH_blender1"), file_handlers[0]);
EXPECT_EQ(BKE_file_handler_find("Test_FH_blender3"), file_handlers[2]);
EXPECT_EQ(BKE_file_handler_find("Test_FH_blender4"), file_handlers[3]);
EXPECT_EQ(BKE_file_handler_find("Test_FH_blender5"), file_handlers[4]);
EXPECT_EQ(BKE_file_handler_find("Test_FH_blender6"), file_handlers[5]);
EXPECT_EQ(BKE_file_handler_find("Test_FH_blender7"), file_handlers[6]);
EXPECT_EQ(BKE_file_handler_find("Test_FH_blender8"), file_handlers[7]);
EXPECT_EQ(BKE_file_handlers()[0].get(), file_handlers[0]);
EXPECT_EQ(BKE_file_handlers()[1].get(), file_handlers[2]);
EXPECT_EQ(BKE_file_handlers()[2].get(), file_handlers[3]);
EXPECT_EQ(BKE_file_handlers()[3].get(), file_handlers[4]);
EXPECT_EQ(BKE_file_handlers()[4].get(), file_handlers[5]);
EXPECT_EQ(BKE_file_handlers()[5].get(), file_handlers[6]);
EXPECT_EQ(BKE_file_handlers()[6].get(), file_handlers[7]);
BKE_file_handler_remove(BKE_file_handler_find("Test_FH_blender8"));
EXPECT_EQ(BKE_file_handlers().size(), MAX_FILE_HANDLERS_TEST_SIZE - 2);
EXPECT_EQ(BKE_file_handler_find("Test_FH_blender8"), nullptr);
/** `FileHandlerType` pointer in `file_handlers[7]` is not longer valid. */
EXPECT_EQ(BKE_file_handler_find("Test_FH_blender1"), file_handlers[0]);
EXPECT_EQ(BKE_file_handler_find("Test_FH_blender3"), file_handlers[2]);
EXPECT_EQ(BKE_file_handler_find("Test_FH_blender4"), file_handlers[3]);
EXPECT_EQ(BKE_file_handler_find("Test_FH_blender5"), file_handlers[4]);
EXPECT_EQ(BKE_file_handler_find("Test_FH_blender6"), file_handlers[5]);
EXPECT_EQ(BKE_file_handler_find("Test_FH_blender7"), file_handlers[6]);
EXPECT_EQ(BKE_file_handlers()[0].get(), file_handlers[0]);
EXPECT_EQ(BKE_file_handlers()[1].get(), file_handlers[2]);
EXPECT_EQ(BKE_file_handlers()[2].get(), file_handlers[3]);
EXPECT_EQ(BKE_file_handlers()[3].get(), file_handlers[4]);
EXPECT_EQ(BKE_file_handlers()[4].get(), file_handlers[5]);
EXPECT_EQ(BKE_file_handlers()[5].get(), file_handlers[6]);
}
} // namespace blender::tests

View File

@ -822,3 +822,9 @@ typedef enum AssetShelfSettings_DisplayFlag {
ASSETSHELF_SHOW_NAMES = (1 << 0),
} AssetShelfSettings_DisplayFlag;
ENUM_OPERATORS(AssetShelfSettings_DisplayFlag, ASSETSHELF_SHOW_NAMES);
typedef struct FileHandler {
DNA_DEFINE_CXX_METHODS(FileHandler)
/** Runtime. */
struct FileHandlerType *type;
} FileHandler;

View File

@ -13,6 +13,7 @@
#include "BLT_translation.h"
#include "BKE_file_handler.hh"
#include "BKE_idprop.h"
#include "BKE_screen.hh"
@ -1452,6 +1453,122 @@ static void rna_UILayout_property_decorate_set(PointerRNA *ptr, bool value)
uiLayoutSetPropDecorate(static_cast<uiLayout *>(ptr->data), value);
}
/* File Handler */
static bool file_handler_poll_drop(const bContext *C, FileHandlerType *file_handler_type)
{
extern FunctionRNA rna_FileHandler_poll_drop_func;
PointerRNA ptr = RNA_pointer_create(
nullptr, file_handler_type->rna_ext.srna, nullptr); /* dummy */
FunctionRNA *func = &rna_FileHandler_poll_drop_func;
ParameterList list;
RNA_parameter_list_create(&list, &ptr, func);
RNA_parameter_set_lookup(&list, "context", &C);
file_handler_type->rna_ext.call((bContext *)C, &ptr, func, &list);
void *ret;
RNA_parameter_get_lookup(&list, "is_usable", &ret);
/* Get the value before freeing. */
const bool is_usable = *(bool *)ret;
RNA_parameter_list_free(&list);
return is_usable;
}
static bool rna_FileHandler_unregister(Main * /*bmain*/, StructRNA *type)
{
FileHandlerType *file_handler_type = static_cast<FileHandlerType *>(
RNA_struct_blender_type_get(type));
if (!file_handler_type) {
return false;
}
RNA_struct_free_extension(type, &file_handler_type->rna_ext);
RNA_struct_free(&BLENDER_RNA, type);
BKE_file_handler_remove(file_handler_type);
return true;
}
static StructRNA *rna_FileHandler_register(Main *bmain,
ReportList *reports,
void *data,
const char *identifier,
StructValidateFunc validate,
StructCallbackFunc call,
StructFreeFunc free)
{
FileHandlerType dummy_file_handler_type{};
FileHandler dummy_file_handler{};
dummy_file_handler.type = &dummy_file_handler_type;
/* Setup dummy file handler type to store static properties in. */
PointerRNA dummy_file_handler_ptr = RNA_pointer_create(
nullptr, &RNA_FileHandler, &dummy_file_handler);
bool have_function[1];
/* Validate the python class. */
if (validate(&dummy_file_handler_ptr, data, have_function) != 0) {
return nullptr;
}
if (strlen(identifier) >= sizeof(dummy_file_handler_type.idname)) {
BKE_reportf(reports,
RPT_ERROR,
"Registering file handler class: '%s' is too long, maximum length is %d",
identifier,
(int)sizeof(dummy_file_handler_type.idname));
return nullptr;
}
/* Check if there is a file handler registered with the same `idname`, and remove it. */
auto registered_file_handler = BKE_file_handler_find(dummy_file_handler_type.idname);
if (registered_file_handler) {
rna_FileHandler_unregister(bmain, registered_file_handler->rna_ext.srna);
}
if (!RNA_struct_available_or_report(reports, dummy_file_handler_type.idname)) {
return nullptr;
}
if (!RNA_struct_bl_idname_ok_or_report(reports, dummy_file_handler_type.idname, "_FH_")) {
return nullptr;
}
/* Create the new file handler type. */
std::unique_ptr<FileHandlerType> file_handler_type = std::make_unique<FileHandlerType>();
*file_handler_type = dummy_file_handler_type;
file_handler_type->rna_ext.srna = RNA_def_struct_ptr(
&BLENDER_RNA, file_handler_type->idname, &RNA_FileHandler);
file_handler_type->rna_ext.data = data;
file_handler_type->rna_ext.call = call;
file_handler_type->rna_ext.free = free;
RNA_struct_blender_type_set(file_handler_type->rna_ext.srna, file_handler_type.get());
file_handler_type->poll_drop = have_function[0] ? file_handler_poll_drop : nullptr;
auto srna = file_handler_type->rna_ext.srna;
BKE_file_handler_add(std::move(file_handler_type));
return srna;
}
static StructRNA *rna_FileHandler_refine(PointerRNA *file_handler_ptr)
{
FileHandler *file_handler = (FileHandler *)file_handler_ptr->data;
guishe marked this conversation as resolved

Can this parsing be moved into file_handler.cc?

Can this parsing be moved into `file_handler.cc`?
return (file_handler->type && file_handler->type->rna_ext.srna) ?
file_handler->type->rna_ext.srna :
&RNA_FileHandler;
}
#else /* RNA_RUNTIME */
static void rna_def_ui_layout(BlenderRNA *brna)
@ -2198,6 +2315,72 @@ static void rna_def_asset_shelf(BlenderRNA *brna)
RNA_def_property_update(prop, NC_SPACE | ND_REGIONS_ASSET_SHELF, nullptr);
}
static void rna_def_file_handler(BlenderRNA *brna)
{
StructRNA *srna;
PropertyRNA *prop;
srna = RNA_def_struct(brna, "FileHandler", nullptr);
RNA_def_struct_ui_text(srna,
"File Handler Type",
"Extends functionality to operators that manages files, such as adding "
"drag and drop support");
RNA_def_struct_refine_func(srna, "rna_FileHandler_refine");
RNA_def_struct_register_funcs(
srna, "rna_FileHandler_register", "rna_FileHandler_unregister", nullptr);
RNA_def_struct_translation_context(srna, BLT_I18NCONTEXT_DEFAULT_BPYRNA);
RNA_def_struct_flag(srna, STRUCT_PUBLIC_NAMESPACE_INHERIT);
/* registration */
prop = RNA_def_property(srna, "bl_idname", PROP_STRING, PROP_NONE);
RNA_def_property_string_sdna(prop, nullptr, "type->idname");
RNA_def_property_flag(prop, PROP_REGISTER);
RNA_def_property_ui_text(
prop,
"ID Name",
"If this is set, the file handler gets a custom ID, otherwise it takes the "
"name of the class used to define the file handler (for example, if the "
"class name is \"OBJECT_FH_hello\", and bl_idname is not set by the "
"script, then bl_idname = \"OBJECT_FH_hello\")");
prop = RNA_def_property(srna, "bl_import_operator", PROP_STRING, PROP_NONE);
RNA_def_property_string_sdna(prop, nullptr, "type->import_operator");
RNA_def_property_flag(prop, PROP_REGISTER_OPTIONAL);
RNA_def_property_ui_text(
guishe marked this conversation as resolved
Review

with the extensions given in bl_file_extensions

`with the extensions given in bl_file_extensions`
prop,
"Operator",
"Operator that can handle import files with the extensions given in bl_file_extensions");
prop = RNA_def_property(srna, "bl_label", PROP_STRING, PROP_NONE);
RNA_def_property_string_sdna(prop, nullptr, "type->label");
RNA_def_property_flag(prop, PROP_REGISTER);
RNA_def_property_ui_text(prop, "Label", "The file handler label");
prop = RNA_def_property(srna, "bl_file_extensions", PROP_STRING, PROP_NONE);
RNA_def_property_string_sdna(prop, nullptr, "type->file_extensions_str");
RNA_def_property_flag(prop, PROP_REGISTER);
RNA_def_property_ui_text(
prop,
"File Extensions",
"Formatted string of file extensions supported by the file handler, each extension should "
"start with a \".\" and be separated by \";\"."
"\nFor Example: `\".blend;.ble\"`");
PropertyRNA *parm;
guishe marked this conversation as resolved
Review

typo: handler

a non-null output -> True

Would also add precision: can be used to handle the drop of a drag-and-drop action. At some point I would expect we add more poll callbacks for other types of file handling.

typo: `handler` `a non-null output` -> `True` Would also add precision: `can be used to handle the drop of a drag-and-drop action`. At some point I would expect we add more poll callbacks for other types of file handling.
FunctionRNA *func;
guishe marked this conversation as resolved
Review

visible is very confusing name here? Also for booleans, we recommend using is_ or use_ prefixes, e.g. here could be is_usable...

See also https://wiki.blender.org/wiki/Source/Architecture/RNA#Defining_Structs_and_Properties

`visible` is very confusing name here? Also for booleans, we recommend using `is_` or `use_` prefixes, e.g. here could be `is_usable`... See also https://wiki.blender.org/wiki/Source/Architecture/RNA#Defining_Structs_and_Properties
func = RNA_def_function(srna, "poll_drop", nullptr);
RNA_def_function_ui_description(
func,
"If this method returns True, can be used to handle the drop of a drag-and-drop action");
RNA_def_function_flag(func, FUNC_NO_SELF | FUNC_REGISTER_OPTIONAL);
RNA_def_function_return(func, RNA_def_boolean(func, "is_usable", true, "", ""));
parm = RNA_def_pointer(func, "context", "Context", "", "");
RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED);
}
void RNA_def_ui(BlenderRNA *brna)
{
rna_def_ui_layout(brna);
@ -2206,6 +2389,7 @@ void RNA_def_ui(BlenderRNA *brna)
rna_def_header(brna);
rna_def_menu(brna);
rna_def_asset_shelf(brna);
rna_def_file_handler(brna);
}
#endif /* RNA_RUNTIME */

View File

@ -8900,7 +8900,8 @@ PyDoc_STRVAR(pyrna_register_class_doc,
" :class:`bpy.types.Panel`, :class:`bpy.types.UIList`,\n"
" :class:`bpy.types.Menu`, :class:`bpy.types.Header`,\n"
" :class:`bpy.types.Operator`, :class:`bpy.types.KeyingSetInfo`,\n"
" :class:`bpy.types.RenderEngine`, :class:`bpy.types.AssetShelf`\n"
" :class:`bpy.types.RenderEngine`, :class:`bpy.types.AssetShelf`,\n"
" :class:`bpy.types.FileHandler`\n"
" :type cls: class\n"
" :raises ValueError:\n"
" if the class is not a subclass of a registerable blender class.\n"