IO: Add support for Drag and Drop to FileHandlers

Added support to Drag and Drop to file handlers, part of #111242.

If file handlers are registered with an import operator they can now be
invoked with drag and drop path data.

Import operators must either declare a `filepath` StringProperty or both
a `directory` StringProperty and a `files` CollectionProperty depending
on if they support single or multiple files respectively.

Multiple FileHandlers could be valid for handling a dropped path. When
this happens a menu is shown so the user can choose which exact handler
to use for the file.

Pull Request: https://projects.blender.org/blender/blender/pulls/116047
This commit is contained in:
Guillermo Venegas 2024-01-06 03:51:45 +01:00 committed by Jesse Yurkovich
parent 09063a3632
commit 1254fee589
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'}
"""
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
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

@ -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<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;
}
/**
* 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<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),
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;