VSE: Add drag-and-drop support for text datablocks and text files

Note that to support "multiple text files dragged in at once" we need to
use file handlers, which can't be passed any custom data (e.g. can't set
"text" property before calling SEQUENCER_OT_effect_strip_add).  To get
around this I just added a new SEQUENCER_OT_text_strip_add rather than
try to change any file handler code.

This should be backwards compatible; scripts can continue to call
effect_strip_add. But we might want to move to the new operator for 6.0
(see #154702).

Ref: !163440
This commit is contained in:
John Kiril Swenson 2026-08-29 17:19:17 -05:00
parent 5ae3e6f918
commit ea1680d241
6 changed files with 237 additions and 3 deletions

View file

@ -421,6 +421,13 @@ class SEQUENCER_FH_sound_strip(FileHandler, SequencerFileHandlerBase):
bl_file_extensions = ";".join(bpy.path.extensions_audio)
class SEQUENCER_FH_text_strip(FileHandler, SequencerFileHandlerBase):
bl_idname = "SEQUENCER_FH_text_strip"
bl_label = "Text strip"
bl_import_operator = "SEQUENCER_OT_text_strip_add"
bl_file_extensions = ".txt"
classes = (
SequencerCrossfadeSounds,
SequencerSplitMulticam,
@ -431,4 +438,5 @@ classes = (
SEQUENCER_FH_image_strip,
SEQUENCER_FH_movie_strip,
SEQUENCER_FH_sound_strip,
SEQUENCER_FH_text_strip,
)

View file

@ -720,7 +720,7 @@ class SEQUENCER_MT_add(Menu):
layout.operator_context = 'INVOKE_REGION_WIN'
layout.operator("sequencer.effect_strip_add", text="Color", icon='COLOR').type = 'COLOR'
layout.operator("sequencer.effect_strip_add", text="Text", icon='FONT_DATA').type = 'TEXT'
layout.operator("sequencer.text_strip_add", text="Text", icon='FONT_DATA')
layout.separator()
total, nonsound = selected_strips_count(context)

View file

@ -16,10 +16,12 @@
#include "DNA_sound_types.h"
#include "MEM_guardedalloc.h"
#include "BLI_fileops.hh"
#include "BLI_listbase.hh"
#include "BLI_math_base_c.hh"
#include "BLI_path_utils.hh"
#include "BLI_string.hh"
#include "BLI_string_ref.hh"
#include "BLI_string_utf8.hh"
#include "BLI_utildefines.hh"
@ -1949,6 +1951,161 @@ void SEQUENCER_OT_image_strip_add(wmOperatorType *ot)
/** \} */
/* -------------------------------------------------------------------- */
/** \name Add Text Strip
* \{ */
static Strip *sequencer_add_text_strip(bContext *C,
wmOperator *op,
seq::LoadData *load_data,
const StringRef text)
{
Scene *scene = CTX_data_sequencer_scene(C);
Editing *ed = seq::editing_ensure(scene);
load_data->effect.type = STRIP_TYPE_TEXT;
Strip *strip = seq::add_effect_strip(scene, ed->current_strips(), load_data);
seq_load_apply_generic_options(C, op, strip);
TextVars *textvars = static_cast<TextVars *>(strip->effectdata);
if (!text.is_empty()) {
/* Make sure to delete default text ("Text") before overriding it. */
MEM_SAFE_DELETE(textvars->text_ptr);
textvars->text_ptr = BLI_strdupn(text.data(), text.size());
textvars->text_len_bytes = text.size();
textvars->cursor_offset = BLI_strlen_utf8(textvars->text_ptr);
}
textvars->runtime = MEM_new<seq::TextVarsRuntime>(__func__);
seq::text_effect_update_runtime(nullptr, *textvars, int2(scene->r.xsch, scene->r.ysch));
return strip;
}
static Strip *sequencer_add_text_strip_from_file(bContext *C,
wmOperator *op,
seq::LoadData *load_data)
{
size_t buf_len;
char *buf = BLI_file_read_text_as_mem(load_data->path, 1, &buf_len);
if (buf == nullptr) {
BKE_reportf(op->reports, RPT_ERROR, "File '%s' could not be loaded", load_data->path);
return nullptr;
}
buf[buf_len] = '\0';
Strip *strip = sequencer_add_text_strip(C, op, load_data, StringRef(buf, buf_len));
MEM_delete(buf);
return strip;
}
static wmOperatorStatus sequencer_add_text_strip_exec(bContext *C, wmOperator *op)
{
if ((op->flag & OP_IS_INVOKE) && !WM_operator_poll_or_report_error(C, op->type, op->reports)) {
return OPERATOR_CANCELLED;
}
Scene *scene = CTX_data_sequencer_scene(C);
seq::LoadData load_data;
if (!sequencer_add_generic_exec(C, op, &load_data, scene, 1)) {
return OPERATOR_CANCELLED;
}
if (RNA_collection_is_empty(op->ptr, "files")) {
sequencer_add_text_strip(C, op, &load_data, RNA_string_get(op->ptr, "text"));
}
else {
char directory[FILE_MAX];
RNA_string_get(op->ptr, "directory", directory);
RNA_BEGIN (op->ptr, itemptr, "files") {
char filename[FILE_MAX];
RNA_string_get(&itemptr, "name", filename);
BLI_path_join(load_data.path, sizeof(load_data.path), directory, filename);
STRNCPY(load_data.name, filename);
const Strip *strip = sequencer_add_text_strip_from_file(C, op, &load_data);
if (strip != nullptr) {
load_data.start_frame += strip->right_handle(scene) - strip->left_handle();
}
}
RNA_END;
}
DEG_id_tag_update(&scene->id, ID_RECALC_SEQUENCER_STRIPS);
sequencer_select_do_updates(C, scene);
move_strips(C, op);
sequencer_add_free(C, op);
return OPERATOR_FINISHED;
}
static wmOperatorStatus sequencer_add_text_strip_invoke(bContext *C,
wmOperator *op,
const wmEvent *event)
{
sequencer_add_init(C, op);
const wmEvent *drop_event = is_drag_and_drop(op, event) ? event : nullptr;
sequencer_generic_invoke_xy__internal(
C, op, SEQPROP_LENGTH | SEQPROP_NOPATHS, STRIP_TYPE_TEXT, drop_event);
return sequencer_add_text_strip_exec(C, op);
}
static bool sequencer_add_text_strip_poll_property(const bContext * /*C*/,
wmOperator *op,
const PropertyRNA *prop)
{
const char *prop_id = RNA_property_identifier(prop);
if (STR_ELEM(prop_id, "filepath", "directory", "filename", "relative_path")) {
return !RNA_collection_is_empty(op->ptr, "files");
}
if (STREQ(prop_id, "text")) {
return RNA_collection_is_empty(op->ptr, "files");
}
return true;
}
void SEQUENCER_OT_text_strip_add(wmOperatorType *ot)
{
PropertyRNA *prop;
/* Identifiers. */
ot->name = "Add Text Strip";
ot->idname = "SEQUENCER_OT_text_strip_add";
ot->description = "Add a text strip to the sequencer";
/* API callbacks. */
ot->invoke = sequencer_add_text_strip_invoke;
ot->exec = sequencer_add_text_strip_exec;
ot->poll = ED_operator_sequencer_active_editable;
ot->poll_property = sequencer_add_text_strip_poll_property;
ot->cancel = sequencer_add_free;
/* Flags. */
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
WM_operator_properties_filesel(ot,
FILE_TYPE_FOLDER | FILE_TYPE_TEXT,
FILE_SPECIAL,
FILE_OPENFILE,
WM_FILESEL_FILEPATH | WM_FILESEL_RELPATH | WM_FILESEL_FILES |
WM_FILESEL_DIRECTORY,
FILE_DEFAULTDISPLAY,
FILE_SORT_DEFAULT);
sequencer_generic_props__internal(ot, SEQPROP_STARTFRAME | SEQPROP_LENGTH | SEQPROP_MOVE);
prop = RNA_def_string(
ot->srna, "text", nullptr, 0, "Text", "Initialize the strip with this text");
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Add Effect Strip
* \{ */
@ -2049,16 +2206,22 @@ static wmOperatorStatus sequencer_add_effect_strip_invoke(bContext *C,
return OPERATOR_CANCELLED;
}
sequencer_add_init(C, op);
int prop_flag = SEQPROP_LENGTH;
/* When invoking an effect strip which uses inputs, skip guessing of the channel. */
StripType type = StripType(RNA_enum_get(op->ptr, "type"));
if (is_op_for_effect_with_inputs(CTX_data_sequencer_scene(C), type)) {
prop_flag |= SEQPROP_NOCHAN;
}
if (is_drag_and_drop(op, event)) {
prop_flag |= SEQPROP_NOPATHS;
}
sequencer_generic_invoke_xy__internal(C, op, prop_flag, type, event);
return sequencer_add_effect_strip_exec(C, op);
const wmOperatorStatus retval = sequencer_add_effect_strip_exec(C, op);
sequencer_add_free(C, op);
return retval;
}
static bool sequencer_add_effect_strip_poll_property(const bContext *C,

View file

@ -10,6 +10,7 @@
#include "DNA_scene_types.h"
#include "DNA_sound_types.h"
#include "DNA_text_types.h"
#include "BLI_listbase.hh"
#include "BLI_math_base_c.hh"
@ -25,6 +26,7 @@
#include "BKE_main.hh"
#include "BKE_mask.hh"
#include "BKE_movieclip.hh"
#include "BKE_text.h"
#include "SEQ_add.hh"
#include "SEQ_channels.hh"
@ -192,6 +194,26 @@ static bool mask_drop_poll(bContext *C, wmDrag *drag, const wmEvent *event)
return false;
}
static bool is_text(wmDrag *drag)
{
if (drag->type == WM_DRAG_PATH) {
return WM_drag_get_paths(drag).size() == 1 &&
BLI_path_extension_check(WM_drag_get_single_path(drag), ".txt");
}
return WM_drag_is_ID_type(drag, ID_TXT);
}
static bool text_drop_poll(bContext *C, wmDrag *drag, const wmEvent *event)
{
if (is_text(drag) && (drag->type != WM_DRAG_PATH ||
test_single_file_handler_poll(C, drag, "SEQUENCER_FH_text_strip")))
{
return generic_poll_operations(C, event, TH_SEQ_TEXT);
}
return false;
}
static bool is_sound(wmDrag *drag)
{
if (drag->type == WM_DRAG_PATH) {
@ -365,6 +387,12 @@ static void sequencer_drop_copy(bContext *C, wmDrag *drag, wmDropBox *drop)
Main *bmain = CTX_data_main(C);
RNA_enum_set(drop->ptr, "mask", BLI_findindex(&bmain->masks, id));
}
else if (id_type == ID_TXT) {
size_t buf_len;
char *buf = txt_to_buf(id_cast<Text *>(id), &buf_len);
RNA_string_set(drop->ptr, "text", buf);
MEM_delete(buf);
}
else if (id_type == ID_SO) {
bSound *sound = id_cast<bSound *>(id);
RNA_string_set(drop->ptr, "filepath", sound->filepath);
@ -778,6 +806,20 @@ static void mask_drop_on_enter(wmDropBox *drop, wmDrag *drag)
coords->only_audio = false;
}
static void text_drop_on_enter(wmDropBox *drop, wmDrag * /*drag*/)
{
if (generic_drop_draw_handling(drop)) {
return;
}
SeqDropCoords *coords = static_cast<SeqDropCoords *>(drop->draw_data);
coords->strip_length = seq::default_strip_length(coords->fps);
coords->num_channels = 1;
coords->num_audio = 0;
coords->playback_rate = 0.0f;
coords->only_audio = false;
}
static void sequencer_drop_on_exit(wmDropBox *drop, wmDrag * /*drag*/)
{
SeqDropCoords *coords = static_cast<SeqDropCoords *>(drop->draw_data);
@ -845,6 +887,13 @@ static void sequencer_dropboxes_add_to_lb(ListBaseT<wmDropBox> *lb)
drop->on_enter = mask_drop_on_enter;
drop->on_exit = sequencer_drop_on_exit;
drop = WM_dropbox_add(
lb, "SEQUENCER_OT_text_strip_add", text_drop_poll, sequencer_drop_copy, nullptr, nullptr);
drop->draw_droptip = nop_draw_droptip_fn;
drop->draw_in_view = draw_strip_in_view;
drop->on_enter = text_drop_on_enter;
drop->on_exit = sequencer_drop_on_exit;
drop = WM_dropbox_add(
lb, "SEQUENCER_OT_sound_strip_add", sound_drop_poll, sequencer_drop_copy, nullptr, nullptr);
drop->draw_droptip = nop_draw_droptip_fn;
@ -898,6 +947,11 @@ static bool mask_drop_preview_poll(bContext * /*C*/, wmDrag *drag, const wmEvent
return WM_drag_is_ID_type(drag, ID_MSK);
}
static bool text_drop_preview_poll(bContext * /*C*/, wmDrag *drag, const wmEvent * /*event*/)
{
return is_text(drag);
}
static bool sound_drop_preview_poll(bContext * /*C*/, wmDrag *drag, const wmEvent * /*event*/)
{
if (drag->type == WM_DRAG_PATH) {
@ -947,6 +1001,13 @@ static void sequencer_preview_dropboxes_add_to_lb(ListBaseT<wmDropBox> *lb)
nullptr,
nullptr);
WM_dropbox_add(lb,
"SEQUENCER_OT_text_strip_add",
text_drop_preview_poll,
sequencer_drop_copy,
nullptr,
nullptr);
WM_dropbox_add(lb,
"SEQUENCER_OT_sound_strip_add",
sound_drop_preview_poll,

View file

@ -326,6 +326,7 @@ void SEQUENCER_OT_movieclip_strip_add(wmOperatorType *ot);
void SEQUENCER_OT_mask_strip_add(wmOperatorType *ot);
void SEQUENCER_OT_sound_strip_add(wmOperatorType *ot);
void SEQUENCER_OT_image_strip_add(wmOperatorType *ot);
void SEQUENCER_OT_text_strip_add(wmOperatorType *ot);
void SEQUENCER_OT_effect_strip_add(wmOperatorType *ot);
void SEQUENCER_OT_add_scene_strip_from_scene_asset(wmOperatorType *ot);

View file

@ -118,6 +118,7 @@ void sequencer_operatortypes()
WM_operatortype_append(SEQUENCER_OT_movie_strip_add);
WM_operatortype_append(SEQUENCER_OT_sound_strip_add);
WM_operatortype_append(SEQUENCER_OT_image_strip_add);
WM_operatortype_append(SEQUENCER_OT_text_strip_add);
WM_operatortype_append(SEQUENCER_OT_effect_strip_add);
WM_operatortype_append(SEQUENCER_OT_add_scene_strip_from_scene_asset);