diff --git a/scripts/modules/bpy_types.py b/scripts/modules/bpy_types.py index 13ad32a21d6..edceb2d03d7 100644 --- a/scripts/modules/bpy_types.py +++ b/scripts/modules/bpy_types.py @@ -1208,6 +1208,10 @@ class AssetShelf(StructRNA, metaclass=RNAMeta): __slots__ = () +class FileHandler(StructRNA, metaclass=RNAMeta): + __slots__ = () + + class NodeTree(bpy_types.ID, metaclass=RNAMetaPropGroup): __slots__ = () diff --git a/source/blender/blenkernel/BKE_file_handler.hh b/source/blender/blenkernel/BKE_file_handler.hh new file mode 100644 index 00000000000..4d70330a1a4 --- /dev/null +++ b/source/blender/blenkernel/BKE_file_handler.hh @@ -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 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 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> &BKE_file_handlers(); diff --git a/source/blender/blenkernel/CMakeLists.txt b/source/blender/blenkernel/CMakeLists.txt index b0cb5521673..ef1c004a79d 100644 --- a/source/blender/blenkernel/CMakeLists.txt +++ b/source/blender/blenkernel/CMakeLists.txt @@ -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 diff --git a/source/blender/blenkernel/intern/file_handler.cc b/source/blender/blenkernel/intern/file_handler.cc new file mode 100644 index 00000000000..5416f01b8c3 --- /dev/null +++ b/source/blender/blenkernel/intern/file_handler.cc @@ -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> &file_handlers() +{ + static blender::RawVector> file_handlers; + return file_handlers; +} + +const blender::RawVector> &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 &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 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 &test_file_handler) { + return test_file_handler.get() == file_handler; + }); +} diff --git a/source/blender/blenkernel/intern/file_handler_test.cc b/source/blender/blenkernel/intern/file_handler_test.cc new file mode 100644 index 00000000000..eeb66cdd26b --- /dev/null +++ b/source/blender/blenkernel/intern/file_handler_test.cc @@ -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 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 file_handler = std::make_unique(); + + 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", + {".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) +{ + 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 diff --git a/source/blender/makesdna/DNA_screen_types.h b/source/blender/makesdna/DNA_screen_types.h index eac25b8e4ea..c940da22633 100644 --- a/source/blender/makesdna/DNA_screen_types.h +++ b/source/blender/makesdna/DNA_screen_types.h @@ -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; diff --git a/source/blender/makesrna/intern/rna_ui.cc b/source/blender/makesrna/intern/rna_ui.cc index 52cf1790b44..06db0f3e668 100644 --- a/source/blender/makesrna/intern/rna_ui.cc +++ b/source/blender/makesrna/intern/rna_ui.cc @@ -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(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( + 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 file_handler_type = std::make_unique(); + *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; + 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( + 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; + FunctionRNA *func; + + 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 */ diff --git a/source/blender/python/intern/bpy_rna.cc b/source/blender/python/intern/bpy_rna.cc index 6a03a058cf6..97ffb79a662 100644 --- a/source/blender/python/intern/bpy_rna.cc +++ b/source/blender/python/intern/bpy_rna.cc @@ -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"