Remove SEQ_ prefix for blender::seq namespace and ED_sequencer for blender::ed::vse namespace Pull Request: https://projects.blender.org/blender/blender/pulls/135560
895 lines
26 KiB
C++
895 lines
26 KiB
C++
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
|
|
/** \file
|
|
* \ingroup spseq
|
|
*/
|
|
|
|
#include <cstddef>
|
|
|
|
#include "DNA_sequence_types.h"
|
|
|
|
#include "BLI_math_matrix.hh"
|
|
#include "BLI_math_vector.hh"
|
|
#include "BLI_string.h"
|
|
#include "BLI_string_utf8.h"
|
|
|
|
#include "BKE_context.hh"
|
|
#include "BKE_scene.hh"
|
|
|
|
#include "SEQ_effects.hh"
|
|
#include "SEQ_relations.hh"
|
|
#include "SEQ_select.hh"
|
|
#include "SEQ_time.hh"
|
|
#include "SEQ_transform.hh"
|
|
|
|
#include "WM_api.hh"
|
|
|
|
#include "RNA_define.hh"
|
|
|
|
#include "UI_view2d.hh"
|
|
|
|
#include "ED_screen.hh"
|
|
|
|
/* Own include. */
|
|
#include "sequencer_intern.hh"
|
|
|
|
namespace blender::ed::vse {
|
|
|
|
static bool sequencer_text_editing_poll(bContext *C)
|
|
{
|
|
if (!sequencer_editing_initialized_and_active(C)) {
|
|
return false;
|
|
}
|
|
|
|
const Strip *strip = seq::select_active_get(CTX_data_scene(C));
|
|
if (strip == nullptr || strip->type != STRIP_TYPE_TEXT || !seq::effects_can_render_text(strip)) {
|
|
return false;
|
|
}
|
|
|
|
const TextVars *data = static_cast<TextVars *>(strip->effectdata);
|
|
if (data == nullptr || data->runtime == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool sequencer_text_editing_active_poll(bContext *C)
|
|
{
|
|
const Strip *strip = seq::select_active_get(CTX_data_scene(C));
|
|
if (strip == nullptr || !sequencer_text_editing_poll(C)) {
|
|
return false;
|
|
}
|
|
|
|
if (ED_screen_animation_no_scrub(CTX_wm_manager(C))) {
|
|
return false;
|
|
}
|
|
|
|
const Scene *scene = CTX_data_scene(C);
|
|
|
|
if (!seq::time_strip_intersects_frame(scene, strip, BKE_scene_frame_get(scene))) {
|
|
return false;
|
|
}
|
|
|
|
return (strip->flag & SEQ_FLAG_TEXT_EDITING_ACTIVE) != 0;
|
|
}
|
|
|
|
int2 strip_text_cursor_offset_to_position(const TextVarsRuntime *text, int cursor_offset)
|
|
{
|
|
cursor_offset = std::clamp(cursor_offset, 0, text->character_count);
|
|
|
|
int2 cursor_position{0, 0};
|
|
for (const seq::LineInfo &line : text->lines) {
|
|
if (cursor_offset < line.characters.size()) {
|
|
cursor_position.x = cursor_offset;
|
|
break;
|
|
}
|
|
cursor_offset -= line.characters.size();
|
|
cursor_position.y += 1;
|
|
}
|
|
|
|
cursor_position.y = std::clamp(cursor_position.y, 0, int(text->lines.size() - 1));
|
|
cursor_position.x = std::clamp(
|
|
cursor_position.x, 0, int(text->lines[cursor_position.y].characters.size() - 1));
|
|
|
|
return cursor_position;
|
|
}
|
|
|
|
static const seq::CharInfo &character_at_cursor_pos_get(const TextVarsRuntime *text,
|
|
const int2 cursor_pos)
|
|
{
|
|
return text->lines[cursor_pos.y].characters[cursor_pos.x];
|
|
}
|
|
|
|
static const seq::CharInfo &character_at_cursor_offset_get(const TextVarsRuntime *text,
|
|
const int cursor_offset)
|
|
{
|
|
const int2 cursor_pos = strip_text_cursor_offset_to_position(text, cursor_offset);
|
|
return character_at_cursor_pos_get(text, cursor_pos);
|
|
}
|
|
|
|
static int cursor_position_to_offset(const TextVarsRuntime *text, int2 cursor_position)
|
|
{
|
|
return character_at_cursor_pos_get(text, cursor_position).index;
|
|
}
|
|
|
|
static void text_selection_cancel(TextVars *data)
|
|
{
|
|
data->selection_start_offset = 0;
|
|
data->selection_end_offset = 0;
|
|
}
|
|
|
|
IndexRange strip_text_selection_range_get(const TextVars *data)
|
|
{
|
|
/* Ensure, that selection start < selection end. */
|
|
int sel_start_offset = data->selection_start_offset;
|
|
int sel_end_offset = data->selection_end_offset;
|
|
if (sel_start_offset > sel_end_offset) {
|
|
std::swap(sel_start_offset, sel_end_offset);
|
|
}
|
|
|
|
return IndexRange(sel_start_offset, sel_end_offset - sel_start_offset);
|
|
}
|
|
|
|
static bool text_has_selection(const TextVars *data)
|
|
{
|
|
return !strip_text_selection_range_get(data).is_empty();
|
|
}
|
|
|
|
static void delete_selected_text(TextVars *data)
|
|
{
|
|
if (!text_has_selection(data)) {
|
|
return;
|
|
}
|
|
|
|
TextVarsRuntime *text = data->runtime;
|
|
IndexRange sel_range = strip_text_selection_range_get(data);
|
|
|
|
seq::CharInfo char_start = character_at_cursor_offset_get(text, sel_range.first());
|
|
seq::CharInfo char_end = character_at_cursor_offset_get(text, sel_range.last());
|
|
|
|
char *addr_start = const_cast<char *>(char_start.str_ptr);
|
|
char *addr_end = const_cast<char *>(char_end.str_ptr) + char_end.byte_length;
|
|
|
|
std::memmove(addr_start, addr_end, BLI_strnlen(addr_end, sizeof(data->text)) + 1);
|
|
|
|
const int2 sel_start = strip_text_cursor_offset_to_position(text, sel_range.first());
|
|
data->cursor_offset = cursor_position_to_offset(text, sel_start);
|
|
text_selection_cancel(data);
|
|
}
|
|
|
|
static void text_editing_update(const bContext *C)
|
|
{
|
|
Strip *strip = seq::select_active_get(CTX_data_scene(C));
|
|
seq::relations_invalidate_cache_raw(CTX_data_scene(C), strip);
|
|
WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER, CTX_data_scene(C));
|
|
}
|
|
|
|
enum {
|
|
LINE_BEGIN,
|
|
LINE_END,
|
|
TEXT_BEGIN,
|
|
TEXT_END,
|
|
PREV_CHAR,
|
|
NEXT_CHAR,
|
|
PREV_WORD,
|
|
NEXT_WORD,
|
|
PREV_LINE,
|
|
NEXT_LINE,
|
|
};
|
|
|
|
static const EnumPropertyItem move_type_items[] = {
|
|
{LINE_BEGIN, "LINE_BEGIN", 0, "Line Begin", ""},
|
|
{LINE_END, "LINE_END", 0, "Line End", ""},
|
|
{TEXT_BEGIN, "TEXT_BEGIN", 0, "Text Begin", ""},
|
|
{TEXT_END, "TEXT_END", 0, "Text End", ""},
|
|
{PREV_CHAR, "PREVIOUS_CHARACTER", 0, "Previous Character", ""},
|
|
{NEXT_CHAR, "NEXT_CHARACTER", 0, "Next Character", ""},
|
|
{PREV_WORD, "PREVIOUS_WORD", 0, "Previous Word", ""},
|
|
{NEXT_WORD, "NEXT_WORD", 0, "Next Word", ""},
|
|
{PREV_LINE, "PREVIOUS_LINE", 0, "Previous Line", ""},
|
|
{NEXT_LINE, "NEXT_LINE", 0, "Next Line", ""},
|
|
{0, nullptr, 0, nullptr, nullptr},
|
|
};
|
|
|
|
static int2 cursor_move_by_character(int2 cursor_position, const TextVarsRuntime *text, int offset)
|
|
{
|
|
const seq::LineInfo &cur_line = text->lines[cursor_position.y];
|
|
/* Move to next line. */
|
|
if (cursor_position.x + offset > cur_line.characters.size() - 1 &&
|
|
cursor_position.y < text->lines.size() - 1)
|
|
{
|
|
cursor_position.x = 0;
|
|
cursor_position.y++;
|
|
}
|
|
/* Move to previous line. */
|
|
else if (cursor_position.x + offset < 0 && cursor_position.y > 0) {
|
|
cursor_position.y--;
|
|
cursor_position.x = text->lines[cursor_position.y].characters.size() - 1;
|
|
}
|
|
else {
|
|
cursor_position.x += offset;
|
|
const int position_max = text->lines[cursor_position.y].characters.size() - 1;
|
|
cursor_position.x = std::clamp(cursor_position.x, 0, position_max);
|
|
}
|
|
return cursor_position;
|
|
}
|
|
|
|
static int2 cursor_move_by_line(int2 cursor_position, const TextVarsRuntime *text, int offset)
|
|
{
|
|
const seq::LineInfo &cur_line = text->lines[cursor_position.y];
|
|
const int cur_pos_x = cur_line.characters[cursor_position.x].position.x;
|
|
|
|
const int line_max = text->lines.size() - 1;
|
|
const int new_line_index = std::clamp(cursor_position.y + offset, 0, line_max);
|
|
const seq::LineInfo &new_line = text->lines[new_line_index];
|
|
|
|
if (cursor_position.y == new_line_index) {
|
|
return cursor_position;
|
|
}
|
|
|
|
/* Find character in another line closest to current position. */
|
|
int best_distance = std::numeric_limits<int>::max();
|
|
int best_character_index = 0;
|
|
|
|
for (int i : new_line.characters.index_range()) {
|
|
seq::CharInfo character = new_line.characters[i];
|
|
const int distance = std::abs(character.position.x - cur_pos_x);
|
|
if (distance < best_distance) {
|
|
best_distance = distance;
|
|
best_character_index = i;
|
|
}
|
|
}
|
|
|
|
cursor_position.x = best_character_index;
|
|
cursor_position.y = new_line_index;
|
|
return cursor_position;
|
|
}
|
|
|
|
static int2 cursor_move_line_end(int2 cursor_position, const TextVarsRuntime *text)
|
|
{
|
|
const seq::LineInfo &cur_line = text->lines[cursor_position.y];
|
|
cursor_position.x = cur_line.characters.size() - 1;
|
|
return cursor_position;
|
|
}
|
|
|
|
static bool is_whitespace_transition(char chr1, char chr2)
|
|
{
|
|
return ELEM(chr1, ' ', '\t', '\n') && !ELEM(chr2, ' ', '\t', '\n');
|
|
}
|
|
|
|
static int2 cursor_move_prev_word(int2 cursor_position, const TextVarsRuntime *text)
|
|
{
|
|
cursor_position = cursor_move_by_character(cursor_position, text, -1);
|
|
|
|
while (cursor_position.x > 0 || cursor_position.y > 0) {
|
|
const seq::CharInfo character = character_at_cursor_pos_get(text, cursor_position);
|
|
const int2 prev_cursor_pos = cursor_move_by_character(cursor_position, text, -1);
|
|
const seq::CharInfo prev_character = character_at_cursor_pos_get(text, prev_cursor_pos);
|
|
|
|
if (is_whitespace_transition(prev_character.str_ptr[0], character.str_ptr[0])) {
|
|
break;
|
|
}
|
|
cursor_position = prev_cursor_pos;
|
|
}
|
|
return cursor_position;
|
|
}
|
|
|
|
static int2 cursor_move_next_word(int2 cursor_position, const TextVarsRuntime *text)
|
|
{
|
|
const int maxline = text->lines.size() - 1;
|
|
const int maxchar = text->lines.last().characters.size() - 1;
|
|
|
|
while ((cursor_position.x < maxchar) || (cursor_position.y < maxline)) {
|
|
const seq::CharInfo character = character_at_cursor_pos_get(text, cursor_position);
|
|
cursor_position = cursor_move_by_character(cursor_position, text, 1);
|
|
const seq::CharInfo next_character = character_at_cursor_pos_get(text, cursor_position);
|
|
|
|
if (is_whitespace_transition(next_character.str_ptr[0], character.str_ptr[0])) {
|
|
break;
|
|
}
|
|
}
|
|
return cursor_position;
|
|
}
|
|
|
|
static int sequencer_text_cursor_move_exec(bContext *C, wmOperator *op)
|
|
{
|
|
const Strip *strip = seq::select_active_get(CTX_data_scene(C));
|
|
TextVars *data = static_cast<TextVars *>(strip->effectdata);
|
|
const TextVarsRuntime *text = data->runtime;
|
|
|
|
if (RNA_boolean_get(op->ptr, "select_text") && !text_has_selection(data)) {
|
|
data->selection_start_offset = data->cursor_offset;
|
|
}
|
|
|
|
int2 cursor_position = strip_text_cursor_offset_to_position(text, data->cursor_offset);
|
|
|
|
switch (RNA_enum_get(op->ptr, "type")) {
|
|
case PREV_CHAR:
|
|
cursor_position = cursor_move_by_character(cursor_position, text, -1);
|
|
break;
|
|
case NEXT_CHAR:
|
|
cursor_position = cursor_move_by_character(cursor_position, text, 1);
|
|
break;
|
|
case PREV_LINE:
|
|
cursor_position = cursor_move_by_line(cursor_position, text, -1);
|
|
break;
|
|
case NEXT_LINE:
|
|
cursor_position = cursor_move_by_line(cursor_position, text, 1);
|
|
break;
|
|
case LINE_BEGIN:
|
|
cursor_position.x = 0;
|
|
break;
|
|
case LINE_END:
|
|
cursor_position = cursor_move_line_end(cursor_position, text);
|
|
break;
|
|
case TEXT_BEGIN:
|
|
cursor_position = {0, 0};
|
|
break;
|
|
case TEXT_END:
|
|
cursor_position.y = text->lines.size() - 1;
|
|
cursor_position = cursor_move_line_end(cursor_position, text);
|
|
break;
|
|
case PREV_WORD:
|
|
cursor_position = cursor_move_prev_word(cursor_position, text);
|
|
break;
|
|
case NEXT_WORD:
|
|
cursor_position = cursor_move_next_word(cursor_position, text);
|
|
break;
|
|
}
|
|
|
|
data->cursor_offset = cursor_position_to_offset(text, cursor_position);
|
|
if (RNA_boolean_get(op->ptr, "select_text")) {
|
|
data->selection_end_offset = data->cursor_offset;
|
|
}
|
|
|
|
if (!RNA_boolean_get(op->ptr, "select_text") ||
|
|
data->cursor_offset == data->selection_start_offset)
|
|
{
|
|
text_selection_cancel(data);
|
|
}
|
|
|
|
WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER, CTX_data_scene(C));
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
void SEQUENCER_OT_text_cursor_move(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Move cursor";
|
|
ot->description = "Move cursor in text";
|
|
ot->idname = "SEQUENCER_OT_text_cursor_move";
|
|
|
|
/* api callbacks */
|
|
ot->exec = sequencer_text_cursor_move_exec;
|
|
ot->poll = sequencer_text_editing_active_poll;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
|
|
|
/* properties */
|
|
RNA_def_enum(ot->srna,
|
|
"type",
|
|
move_type_items,
|
|
LINE_BEGIN,
|
|
"Type",
|
|
"Where to move cursor to, to make a selection");
|
|
|
|
PropertyRNA *prop = RNA_def_boolean(
|
|
ot->srna, "select_text", false, "Select Text", "Select text while moving cursor");
|
|
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
|
|
}
|
|
|
|
static bool text_insert(TextVars *data, const char *buf)
|
|
{
|
|
const TextVarsRuntime *text = data->runtime;
|
|
|
|
const bool selection_was_deleted = text_has_selection(data);
|
|
delete_selected_text(data);
|
|
|
|
const size_t in_str_len = BLI_strnlen(buf, sizeof(buf));
|
|
const size_t text_str_len = BLI_strnlen(data->text, sizeof(data->text));
|
|
|
|
if (text_str_len + in_str_len + 1 > sizeof(data->text)) {
|
|
return selection_was_deleted;
|
|
}
|
|
|
|
const seq::CharInfo cur_char = character_at_cursor_offset_get(text, data->cursor_offset);
|
|
char *cursor_addr = const_cast<char *>(cur_char.str_ptr);
|
|
const size_t move_str_len = BLI_strnlen(cursor_addr, sizeof(data->text)) + 1;
|
|
|
|
std::memmove(cursor_addr + in_str_len, cursor_addr, move_str_len);
|
|
std::memcpy(cursor_addr, buf, in_str_len);
|
|
|
|
data->cursor_offset += 1;
|
|
return true;
|
|
}
|
|
|
|
static int sequencer_text_insert_exec(bContext *C, wmOperator *op)
|
|
{
|
|
const Strip *strip = seq::select_active_get(CTX_data_scene(C));
|
|
TextVars *data = static_cast<TextVars *>(strip->effectdata);
|
|
|
|
char str[512];
|
|
RNA_string_get(op->ptr, "string", str);
|
|
|
|
const size_t in_buf_len = BLI_strnlen(str, sizeof(str));
|
|
if (in_buf_len == 0) {
|
|
return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH;
|
|
}
|
|
|
|
if (!text_insert(data, str)) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
text_editing_update(C);
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
static int sequencer_text_insert_invoke(bContext *C, wmOperator *op, const wmEvent *event)
|
|
{
|
|
char str[6];
|
|
BLI_strncpy(str, event->utf8_buf, BLI_str_utf8_size_safe(event->utf8_buf) + 1);
|
|
RNA_string_set(op->ptr, "string", str);
|
|
return sequencer_text_insert_exec(C, op);
|
|
}
|
|
|
|
void SEQUENCER_OT_text_insert(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Insert Character";
|
|
ot->description = "Insert text at cursor position";
|
|
ot->idname = "SEQUENCER_OT_text_insert";
|
|
|
|
/* api callbacks */
|
|
ot->exec = sequencer_text_insert_exec;
|
|
ot->invoke = sequencer_text_insert_invoke;
|
|
ot->poll = sequencer_text_editing_active_poll;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_UNDO;
|
|
|
|
/* properties */
|
|
RNA_def_string(
|
|
ot->srna, "string", nullptr, 512, "String", "String to be inserted at cursor position");
|
|
}
|
|
|
|
enum { DEL_NEXT_SEL, DEL_PREV_SEL };
|
|
static const EnumPropertyItem delete_type_items[] = {
|
|
{DEL_NEXT_SEL, "NEXT_OR_SELECTION", 0, "Next or Selection", ""},
|
|
{DEL_PREV_SEL, "PREVIOUS_OR_SELECTION", 0, "Previous or Selection", ""},
|
|
{0, nullptr, 0, nullptr, nullptr},
|
|
};
|
|
|
|
static void delete_character(const seq::CharInfo character, const TextVars *data)
|
|
{
|
|
char *cursor_addr = const_cast<char *>(character.str_ptr);
|
|
char *next_char_addr = cursor_addr + character.byte_length;
|
|
std::memmove(cursor_addr, next_char_addr, BLI_strnlen(next_char_addr, sizeof(data->text)) + 1);
|
|
}
|
|
|
|
static int sequencer_text_delete_exec(bContext *C, wmOperator *op)
|
|
{
|
|
const Strip *strip = seq::select_active_get(CTX_data_scene(C));
|
|
TextVars *data = static_cast<TextVars *>(strip->effectdata);
|
|
const TextVarsRuntime *text = data->runtime;
|
|
const int type = RNA_enum_get(op->ptr, "type");
|
|
|
|
if (text_has_selection(data)) {
|
|
delete_selected_text(data);
|
|
text_editing_update(C);
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
if (type == DEL_NEXT_SEL) {
|
|
if (data->cursor_offset >= text->character_count) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
delete_character(character_at_cursor_offset_get(text, data->cursor_offset), data);
|
|
}
|
|
if (type == DEL_PREV_SEL) {
|
|
if (data->cursor_offset == 0) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
delete_character(character_at_cursor_offset_get(text, data->cursor_offset - 1), data);
|
|
data->cursor_offset -= 1;
|
|
}
|
|
|
|
text_editing_update(C);
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
void SEQUENCER_OT_text_delete(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Delete Character";
|
|
ot->description = "Delete text at cursor position";
|
|
ot->idname = "SEQUENCER_OT_text_delete";
|
|
|
|
/* api callbacks */
|
|
ot->exec = sequencer_text_delete_exec;
|
|
ot->poll = sequencer_text_editing_active_poll;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_UNDO;
|
|
|
|
/* properties */
|
|
RNA_def_enum(ot->srna,
|
|
"type",
|
|
delete_type_items,
|
|
DEL_NEXT_SEL,
|
|
"Type",
|
|
"Which part of the text to delete");
|
|
}
|
|
|
|
static int sequencer_text_line_break_exec(bContext *C, wmOperator * /*op*/)
|
|
{
|
|
const Strip *strip = seq::select_active_get(CTX_data_scene(C));
|
|
TextVars *data = static_cast<TextVars *>(strip->effectdata);
|
|
|
|
if (!text_insert(data, "\n")) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
text_editing_update(C);
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
void SEQUENCER_OT_text_line_break(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Insert Line Break";
|
|
ot->description = "Insert line break at cursor position";
|
|
ot->idname = "SEQUENCER_OT_text_line_break";
|
|
|
|
/* api callbacks */
|
|
ot->exec = sequencer_text_line_break_exec;
|
|
ot->poll = sequencer_text_editing_active_poll;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_UNDO;
|
|
}
|
|
|
|
static int sequencer_text_select_all(bContext *C, wmOperator * /*op*/)
|
|
{
|
|
const Strip *strip = seq::select_active_get(CTX_data_scene(C));
|
|
TextVars *data = static_cast<TextVars *>(strip->effectdata);
|
|
data->selection_start_offset = 0;
|
|
data->selection_end_offset = data->runtime->character_count;
|
|
text_editing_update(C);
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
void SEQUENCER_OT_text_select_all(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Select All";
|
|
ot->description = "Select all characters";
|
|
ot->idname = "SEQUENCER_OT_text_select_all";
|
|
|
|
/* api callbacks */
|
|
ot->exec = sequencer_text_select_all;
|
|
ot->poll = sequencer_text_editing_active_poll;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_UNDO;
|
|
}
|
|
|
|
static int sequencer_text_deselect_all(bContext *C, wmOperator * /*op*/)
|
|
{
|
|
Strip *strip = seq::select_active_get(CTX_data_scene(C));
|
|
TextVars *data = static_cast<TextVars *>(strip->effectdata);
|
|
|
|
if (!text_has_selection(data)) {
|
|
/* Exit edit mode, so text can be translated by mouse. */
|
|
strip->flag &= ~SEQ_FLAG_TEXT_EDITING_ACTIVE;
|
|
}
|
|
else {
|
|
text_selection_cancel(data);
|
|
}
|
|
|
|
text_editing_update(C);
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
void SEQUENCER_OT_text_deselect_all(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Deselect All";
|
|
ot->description = "Deselect all characters";
|
|
ot->idname = "SEQUENCER_OT_text_deselect_all";
|
|
|
|
/* api callbacks */
|
|
ot->exec = sequencer_text_deselect_all;
|
|
ot->poll = sequencer_text_editing_active_poll;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_UNDO;
|
|
}
|
|
|
|
static int sequencer_text_edit_mode_toggle(bContext *C, wmOperator * /*op*/)
|
|
{
|
|
Strip *strip = seq::select_active_get(CTX_data_scene(C));
|
|
if (sequencer_text_editing_active_poll(C)) {
|
|
strip->flag &= ~SEQ_FLAG_TEXT_EDITING_ACTIVE;
|
|
}
|
|
else {
|
|
strip->flag |= SEQ_FLAG_TEXT_EDITING_ACTIVE;
|
|
}
|
|
|
|
WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER, CTX_data_scene(C));
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
void SEQUENCER_OT_text_edit_mode_toggle(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Edit Text";
|
|
ot->description = "Toggle text editing";
|
|
ot->idname = "SEQUENCER_OT_text_edit_mode_toggle";
|
|
|
|
/* api callbacks */
|
|
ot->exec = sequencer_text_edit_mode_toggle;
|
|
ot->poll = sequencer_text_editing_poll;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_UNDO;
|
|
}
|
|
|
|
static int find_closest_cursor_offset(const TextVars *data, float2 mouse_loc)
|
|
{
|
|
const TextVarsRuntime *text = data->runtime;
|
|
int best_cursor_offset = 0;
|
|
float best_distance = std::numeric_limits<float>::max();
|
|
|
|
for (const seq::LineInfo &line : text->lines) {
|
|
for (const seq::CharInfo &character : line.characters) {
|
|
const float distance = math::distance(mouse_loc, character.position);
|
|
if (distance < best_distance) {
|
|
best_distance = distance;
|
|
best_cursor_offset = character.index;
|
|
}
|
|
}
|
|
}
|
|
|
|
return best_cursor_offset;
|
|
}
|
|
|
|
static void cursor_set_by_mouse_position(const bContext *C, const wmEvent *event)
|
|
{
|
|
const Scene *scene = CTX_data_scene(C);
|
|
const Strip *strip = seq::select_active_get(scene);
|
|
TextVars *data = static_cast<TextVars *>(strip->effectdata);
|
|
const View2D *v2d = UI_view2d_fromcontext(C);
|
|
|
|
int2 mval_region;
|
|
WM_event_drag_start_mval(event, CTX_wm_region(C), mval_region);
|
|
float2 mouse_loc;
|
|
UI_view2d_region_to_view(v2d, mval_region.x, mval_region.y, &mouse_loc.x, &mouse_loc.y);
|
|
|
|
/* Convert cursor coordinates to domain of CharInfo::position. */
|
|
const blender::float2 view_offs{-scene->r.xsch / 2.0f, -scene->r.ysch / 2.0f};
|
|
const float view_aspect = scene->r.xasp / scene->r.yasp;
|
|
blender::float3x3 transform_mat = seq::image_transform_matrix_get(CTX_data_scene(C), strip);
|
|
// MSVC 2019 can't decide here for some reason, pick the template for it.
|
|
transform_mat = blender::math::invert<float, 3>(transform_mat);
|
|
|
|
mouse_loc.x /= view_aspect;
|
|
mouse_loc = blender::math::transform_point(transform_mat, mouse_loc);
|
|
mouse_loc -= view_offs;
|
|
data->cursor_offset = find_closest_cursor_offset(data, float2(mouse_loc));
|
|
}
|
|
|
|
static int sequencer_text_cursor_set_modal(bContext *C, wmOperator * /*op*/, const wmEvent *event)
|
|
{
|
|
const Scene *scene = CTX_data_scene(C);
|
|
const Strip *strip = seq::select_active_get(scene);
|
|
TextVars *data = static_cast<TextVars *>(strip->effectdata);
|
|
bool make_selection = false;
|
|
|
|
switch (event->type) {
|
|
case LEFTMOUSE:
|
|
if (event->val == KM_RELEASE) {
|
|
cursor_set_by_mouse_position(C, event);
|
|
if (make_selection) {
|
|
data->selection_end_offset = data->cursor_offset;
|
|
}
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
break;
|
|
case MIDDLEMOUSE:
|
|
case RIGHTMOUSE:
|
|
return OPERATOR_FINISHED;
|
|
case MOUSEMOVE:
|
|
make_selection = true;
|
|
if (!text_has_selection(data)) {
|
|
data->selection_start_offset = data->cursor_offset;
|
|
}
|
|
cursor_set_by_mouse_position(C, event);
|
|
data->selection_end_offset = data->cursor_offset;
|
|
break;
|
|
}
|
|
|
|
WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER, CTX_data_scene(C));
|
|
return OPERATOR_RUNNING_MODAL;
|
|
}
|
|
|
|
static int sequencer_text_cursor_set_invoke(bContext *C, wmOperator *op, const wmEvent *event)
|
|
{
|
|
const Scene *scene = CTX_data_scene(C);
|
|
Strip *strip = seq::select_active_get(scene);
|
|
TextVars *data = static_cast<TextVars *>(strip->effectdata);
|
|
const View2D *v2d = UI_view2d_fromcontext(C);
|
|
|
|
int2 mval_region;
|
|
WM_event_drag_start_mval(event, CTX_wm_region(C), mval_region);
|
|
float2 mouse_loc;
|
|
UI_view2d_region_to_view(v2d, mval_region.x, mval_region.y, &mouse_loc.x, &mouse_loc.y);
|
|
|
|
if (!strip_point_image_isect(scene, strip, mouse_loc)) {
|
|
strip->flag &= ~SEQ_FLAG_TEXT_EDITING_ACTIVE;
|
|
return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH;
|
|
}
|
|
|
|
text_selection_cancel(data);
|
|
cursor_set_by_mouse_position(C, event);
|
|
|
|
WM_event_add_modal_handler(C, op);
|
|
WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER, CTX_data_scene(C));
|
|
return OPERATOR_RUNNING_MODAL;
|
|
}
|
|
|
|
void SEQUENCER_OT_text_cursor_set(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Set Cursor";
|
|
ot->description = "Set cursor position in text";
|
|
ot->idname = "SEQUENCER_OT_text_cursor_set";
|
|
|
|
/* api callbacks */
|
|
ot->invoke = sequencer_text_cursor_set_invoke;
|
|
ot->modal = sequencer_text_cursor_set_modal;
|
|
ot->poll = sequencer_text_editing_active_poll;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
|
|
|
/* properties */
|
|
|
|
PropertyRNA *prop = RNA_def_boolean(
|
|
ot->srna, "select_text", false, "Select Text", "Select text while moving cursor");
|
|
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
|
|
}
|
|
|
|
static void text_edit_copy(const TextVars *data)
|
|
{
|
|
const TextVarsRuntime *text = data->runtime;
|
|
const IndexRange selection_range = strip_text_selection_range_get(data);
|
|
const seq::CharInfo start = character_at_cursor_offset_get(text, selection_range.first());
|
|
const seq::CharInfo end = character_at_cursor_offset_get(text, selection_range.last());
|
|
const size_t len = end.str_ptr + end.byte_length - start.str_ptr;
|
|
|
|
char clipboard_buf[sizeof(data->text)] = {0};
|
|
memcpy(clipboard_buf, start.str_ptr, math::min(len, sizeof(clipboard_buf)));
|
|
WM_clipboard_text_set(clipboard_buf, false);
|
|
}
|
|
|
|
static int sequencer_text_edit_copy_exec(bContext *C, wmOperator * /*op*/)
|
|
{
|
|
const Strip *strip = seq::select_active_get(CTX_data_scene(C));
|
|
const TextVars *data = static_cast<TextVars *>(strip->effectdata);
|
|
|
|
if (!text_has_selection(data)) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
text_edit_copy(data);
|
|
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
void SEQUENCER_OT_text_edit_copy(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Copy Text";
|
|
ot->description = "Copy text to clipboard";
|
|
ot->idname = "SEQUENCER_OT_text_edit_copy";
|
|
|
|
/* api callbacks */
|
|
ot->exec = sequencer_text_edit_copy_exec;
|
|
ot->poll = sequencer_text_editing_active_poll;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_UNDO;
|
|
}
|
|
|
|
static int sequencer_text_edit_paste_exec(bContext *C, wmOperator * /*op*/)
|
|
{
|
|
const Strip *strip = seq::select_active_get(CTX_data_scene(C));
|
|
TextVars *data = static_cast<TextVars *>(strip->effectdata);
|
|
const TextVarsRuntime *text = data->runtime;
|
|
|
|
int clipboard_len;
|
|
char *clipboard_buf = WM_clipboard_text_get(false, true, &clipboard_len);
|
|
|
|
if (clipboard_len == 0) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
delete_selected_text(data);
|
|
const int max_str_len = sizeof(data->text) - (BLI_strnlen(data->text, sizeof(data->text)) + 1);
|
|
|
|
/* Maximum bytes that can be filled into `data->text`. */
|
|
const int fillable_len = std::min(clipboard_len, max_str_len);
|
|
|
|
/* Truncated string could contain invalid utf-8 sequence, thus ensure the length inserted is
|
|
* always valid. */
|
|
size_t valid_str_len;
|
|
const int extra_offset = BLI_strnlen_utf8_ex(clipboard_buf, fillable_len, &valid_str_len);
|
|
|
|
const seq::CharInfo cur_char = character_at_cursor_offset_get(text, data->cursor_offset);
|
|
char *cursor_addr = const_cast<char *>(cur_char.str_ptr);
|
|
const size_t move_str_len = BLI_strnlen(cursor_addr, sizeof(data->text)) + 1;
|
|
|
|
std::memmove(cursor_addr + valid_str_len, cursor_addr, move_str_len);
|
|
std::memcpy(cursor_addr, clipboard_buf, valid_str_len);
|
|
|
|
data->cursor_offset += extra_offset;
|
|
|
|
MEM_freeN(clipboard_buf);
|
|
text_editing_update(C);
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
void SEQUENCER_OT_text_edit_paste(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Paste Text";
|
|
ot->description = "Paste text to clipboard";
|
|
ot->idname = "SEQUENCER_OT_text_edit_paste";
|
|
|
|
/* api callbacks */
|
|
ot->exec = sequencer_text_edit_paste_exec;
|
|
ot->poll = sequencer_text_editing_active_poll;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_UNDO;
|
|
}
|
|
|
|
static int sequencer_text_edit_cut_exec(bContext *C, wmOperator * /*op*/)
|
|
{
|
|
const Strip *strip = seq::select_active_get(CTX_data_scene(C));
|
|
TextVars *data = static_cast<TextVars *>(strip->effectdata);
|
|
|
|
if (!text_has_selection(data)) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
text_edit_copy(data);
|
|
delete_selected_text(data);
|
|
|
|
text_editing_update(C);
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
void SEQUENCER_OT_text_edit_cut(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Cut Text";
|
|
ot->description = "Cut text to clipboard";
|
|
ot->idname = "SEQUENCER_OT_text_edit_cut";
|
|
|
|
/* api callbacks */
|
|
ot->exec = sequencer_text_edit_cut_exec;
|
|
ot->poll = sequencer_text_editing_active_poll;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_UNDO;
|
|
}
|
|
|
|
} // namespace blender::ed::vse
|