IO: Add initial support for File Handlers registration

Adds initial support for File Handler registration with the python API
for design task #68935. File Handlers will allow developers to associate
additional UI behavior and capability to operators traditionally used
only within the file browser.

The initial commit should have no user visible changes, but will serve
as the foundation for providing file drag & drop capabilities to
operators that can manage files (#111242).

See the PR for an example of python usage.
See design task #68935 for remaining work to be explored in the future.

Pull Request: https://projects.blender.org/blender/blender/pulls/112466
This commit is contained in:
Guillermo Venegas
2023-12-09 05:06:10 +01:00
committed by Jesse Yurkovich
parent 85012ccb1f
commit 7c5fa8bf6c
8 changed files with 422 additions and 1 deletions
+4
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__ = ()
@@ -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();
+3
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
@@ -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;
});
}
@@ -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",
{".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
@@ -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;
+184
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;
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 */
+2 -1
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"