diff --git a/release/datafiles/userdef/userdef_default_theme.c b/release/datafiles/userdef/userdef_default_theme.c index 83f83b53e37..96c6733780c 100644 --- a/release/datafiles/userdef/userdef_default_theme.c +++ b/release/datafiles/userdef/userdef_default_theme.c @@ -702,6 +702,8 @@ const bTheme U_theme_default = { .row_alternate = RGBA(0xffffff05), .anim_preview_range = RGBA(0xa14d0066), .metadatatext = RGBA(0xffffffff), + .text_strip_cursor = RGBA(0x71a8ffff), + .selected_text = RGBA(0xffffff4d), }, .space_image = { .back = RGBA(0x30303000), diff --git a/scripts/presets/interface_theme/Blender_Light.xml b/scripts/presets/interface_theme/Blender_Light.xml index a4d4594e148..7d03b791947 100644 --- a/scripts/presets/interface_theme/Blender_Light.xml +++ b/scripts/presets/interface_theme/Blender_Light.xml @@ -836,6 +836,8 @@ metadatatext="#ffffff" preview_range="#a14d0066" row_alternate="#ffffff0d" + text_strip_cursor="#71a8ff" + selected_text= "#19191a4d" > flag &= ~(1 << 6); + return true; +} + static bool all_scenes_use(Main *bmain, const blender::Span engines) { if (!bmain->scenes.first) { @@ -5304,6 +5310,15 @@ void blo_do_versions_400(FileData *fd, Library * /*lib*/, Main *bmain) } } + if (!MAIN_VERSION_FILE_ATLEAST(bmain, 404, 15)) { + LISTBASE_FOREACH (Scene *, scene, &bmain->scenes) { + Editing *ed = SEQ_editing_get(scene); + if (ed != nullptr) { + SEQ_for_each_callback(&ed->seqbase, versioning_clear_strip_unused_flag, scene); + } + } + } + /* Always run this versioning; meshes are written with the legacy format which always needs to * be converted to the new format on file load. Can be moved to a subversion check in a larger * breaking release. */ diff --git a/source/blender/blenloader/intern/versioning_userdef.cc b/source/blender/blenloader/intern/versioning_userdef.cc index b30aa208d73..769b8bf54f1 100644 --- a/source/blender/blenloader/intern/versioning_userdef.cc +++ b/source/blender/blenloader/intern/versioning_userdef.cc @@ -214,6 +214,10 @@ static void do_versions_theme(const UserDef *userdef, bTheme *btheme) FROM_DEFAULT_V4_UCHAR(space_view3d.face_front); } + if (!USER_VERSION_ATLEAST(404, 12)) { + FROM_DEFAULT_V4_UCHAR(space_sequencer.text_strip_cursor); + FROM_DEFAULT_V4_UCHAR(space_sequencer.selected_text); + } /** * Always bump subversion in BKE_blender_version.h when adding versioning * code here, and wrap it inside a USER_VERSION_ATLEAST check. diff --git a/source/blender/editors/include/UI_resources.hh b/source/blender/editors/include/UI_resources.hh index b87fdb95008..6ca8e84f765 100644 --- a/source/blender/editors/include/UI_resources.hh +++ b/source/blender/editors/include/UI_resources.hh @@ -219,6 +219,8 @@ enum ThemeColorID { TH_SEQ_COLOR, TH_SEQ_ACTIVE, TH_SEQ_SELECTED, + TH_SEQ_TEXT_CURSOR, + TH_SEQ_SELECTED_TEXT, TH_EDGE_SHARP, TH_EDITMESH_ACTIVE, diff --git a/source/blender/editors/interface/resources.cc b/source/blender/editors/interface/resources.cc index fa82e9b6293..89450728b65 100644 --- a/source/blender/editors/interface/resources.cc +++ b/source/blender/editors/interface/resources.cc @@ -721,6 +721,12 @@ const uchar *UI_ThemeGetColorPtr(bTheme *btheme, int spacetype, int colorid) case TH_SEQ_SELECTED: cp = ts->selected_strip; break; + case TH_SEQ_TEXT_CURSOR: + cp = ts->text_strip_cursor; + break; + case TH_SEQ_SELECTED_TEXT: + cp = ts->selected_text; + break; case TH_CONSOLE_OUTPUT: cp = ts->console_output; diff --git a/source/blender/editors/space_sequencer/CMakeLists.txt b/source/blender/editors/space_sequencer/CMakeLists.txt index d2cd36a371b..3d8e6a73b9e 100644 --- a/source/blender/editors/space_sequencer/CMakeLists.txt +++ b/source/blender/editors/space_sequencer/CMakeLists.txt @@ -41,6 +41,7 @@ set(SRC sequencer_scopes.cc sequencer_select.cc sequencer_strips_batch.cc + sequencer_text_edit.cc sequencer_thumbnails.cc sequencer_timeline_draw.cc sequencer_view.cc diff --git a/source/blender/editors/space_sequencer/sequencer_intern.hh b/source/blender/editors/space_sequencer/sequencer_intern.hh index a9569494dd4..1023ec749da 100644 --- a/source/blender/editors/space_sequencer/sequencer_intern.hh +++ b/source/blender/editors/space_sequencer/sequencer_intern.hh @@ -281,6 +281,7 @@ Sequence *find_nearest_seq(const Scene *scene, const View2D *v2d, const int mval[2], eSeqHandle *r_hand); +bool seq_point_image_isect(const Scene *scene, const Sequence *seq, float point_view[2]); /* `sequencer_add.cc` */ @@ -380,6 +381,22 @@ int right_fake_key_frame_get(const bContext *C, const Sequence *seq); bool retiming_keys_can_be_displayed(const SpaceSeq *sseq); rctf seq_retiming_keys_box_get(const Scene *scene, const View2D *v2d, const Sequence *seq); +/* `sequencer_text_edit.cc` */ +bool sequencer_text_editing_active_poll(bContext *C); +void SEQUENCER_OT_text_cursor_move(wmOperatorType *ot); +void SEQUENCER_OT_text_insert(wmOperatorType *ot); +void SEQUENCER_OT_text_delete(wmOperatorType *ot); +void SEQUENCER_OT_text_line_break(wmOperatorType *ot); +void SEQUENCER_OT_text_select_all(wmOperatorType *ot); +void SEQUENCER_OT_text_deselect_all(wmOperatorType *ot); +void SEQUENCER_OT_text_edit_mode_toggle(wmOperatorType *ot); +void SEQUENCER_OT_text_cursor_set(wmOperatorType *ot); +void SEQUENCER_OT_text_edit_copy(wmOperatorType *ot); +void SEQUENCER_OT_text_edit_paste(wmOperatorType *ot); +void SEQUENCER_OT_text_edit_cut(wmOperatorType *ot); +blender::int2 seq_text_cursor_offset_to_position(const TextVarsRuntime *text, int cursor_offset); +blender::IndexRange seq_text_selection_range_get(const TextVars *data); + /* `sequencer_timeline_draw.cc` */ blender::Vector sequencer_visible_strips_get(const bContext *C); blender::Vector sequencer_visible_strips_get(const Scene *scene, const View2D *v2d); diff --git a/source/blender/editors/space_sequencer/sequencer_ops.cc b/source/blender/editors/space_sequencer/sequencer_ops.cc index be465ff5591..5b0eff3d761 100644 --- a/source/blender/editors/space_sequencer/sequencer_ops.cc +++ b/source/blender/editors/space_sequencer/sequencer_ops.cc @@ -80,6 +80,19 @@ void sequencer_operatortypes() WM_operatortype_append(SEQUENCER_OT_retiming_segment_speed_set); WM_operatortype_append(SEQUENCER_OT_retiming_key_delete); + /* `sequencer_text_edit.cc` */ + WM_operatortype_append(SEQUENCER_OT_text_cursor_move); + WM_operatortype_append(SEQUENCER_OT_text_insert); + WM_operatortype_append(SEQUENCER_OT_text_delete); + WM_operatortype_append(SEQUENCER_OT_text_line_break); + WM_operatortype_append(SEQUENCER_OT_text_select_all); + WM_operatortype_append(SEQUENCER_OT_text_deselect_all); + WM_operatortype_append(SEQUENCER_OT_text_edit_mode_toggle); + WM_operatortype_append(SEQUENCER_OT_text_cursor_set); + WM_operatortype_append(SEQUENCER_OT_text_edit_copy); + WM_operatortype_append(SEQUENCER_OT_text_edit_paste); + WM_operatortype_append(SEQUENCER_OT_text_edit_cut); + /* `sequencer_select.cc` */ WM_operatortype_append(SEQUENCER_OT_select_all); WM_operatortype_append(SEQUENCER_OT_select); diff --git a/source/blender/editors/space_sequencer/sequencer_preview_draw.cc b/source/blender/editors/space_sequencer/sequencer_preview_draw.cc index 510f99a0a47..0a65e1fea70 100644 --- a/source/blender/editors/space_sequencer/sequencer_preview_draw.cc +++ b/source/blender/editors/space_sequencer/sequencer_preview_draw.cc @@ -6,15 +6,24 @@ * \ingroup spseq */ +#include #include #include #include "BLF_api.hh" #include "BLI_blenlib.h" +#include "BLI_index_range.hh" +#include "BLI_math_matrix.hh" +#include "BLI_math_matrix_types.hh" #include "BLI_math_rotation.h" +#include "BLI_math_vector_types.hh" +#include "BLI_rect.h" #include "BLI_utildefines.h" +#include "BLI_vector.hh" +#include "DNA_view2d_types.h" +#include "GPU_primitive.hh" #include "IMB_imbuf_types.hh" #include "DNA_scene_types.h" @@ -43,6 +52,7 @@ #include "BIF_glutil.hh" #include "SEQ_channels.hh" +#include "SEQ_effects.hh" #include "SEQ_iterator.hh" #include "SEQ_prefetch.hh" #include "SEQ_proxy.hh" @@ -1070,6 +1080,187 @@ static void seq_draw_image_origin_and_outline(const bContext *C, Sequence *seq, GPU_line_smooth(false); } +static void text_selection_draw(const bContext *C, const Sequence *seq, uint pos) +{ + const TextVars *data = static_cast(seq->effectdata); + const TextVarsRuntime *text = data->runtime; + const Scene *scene = CTX_data_scene(C); + + if (data->selection_start_offset == -1 || seq_text_selection_range_get(data).is_empty()) { + return; + } + + const blender::IndexRange sel_range = seq_text_selection_range_get(data); + const blender::int2 selection_start = seq_text_cursor_offset_to_position(text, + sel_range.first()); + const blender::int2 selection_end = seq_text_cursor_offset_to_position(text, sel_range.last()); + const int line_start = selection_start.y; + const int line_end = selection_end.y; + + for (int line_index = line_start; line_index <= line_end; line_index++) { + const blender::seq::LineInfo line = text->lines[line_index]; + blender::seq::CharInfo character_start = line.characters.first(); + blender::seq::CharInfo character_end = line.characters.last(); + + if (line_index == selection_start.y) { + character_start = line.characters[selection_start.x]; + } + if (line_index == selection_end.y) { + character_end = line.characters[selection_end.x]; + } + + const float line_y = character_start.position.y + text->font_descender; + + const blender::float3 view_offs{-scene->r.xsch / 2.0f, -scene->r.ysch / 2.0f, 0.0f}; + const float view_aspect = scene->r.xasp / scene->r.yasp; + blender::float4x4 transform_mat; + SEQ_image_transform_matrix_get(scene, seq, transform_mat.ptr()); + blender::float4x3 selection_quad{ + {character_start.position.x, line_y, 0.0f}, + {character_start.position.x, line_y + text->line_height, 0.0f}, + {character_end.position.x + character_end.advance_x, line_y + text->line_height, 0.0f}, + {character_end.position.x + character_end.advance_x, line_y, 0.0f}, + }; + + immBegin(GPU_PRIM_TRIS, 6); + immUniformThemeColor(TH_SEQ_SELECTED_TEXT); + + for (int i : blender::IndexRange(0, 4)) { + selection_quad[i] += view_offs; + selection_quad[i] = blender::math::transform_point(transform_mat, selection_quad[i]); + selection_quad[i].x *= view_aspect; + } + for (int i : blender::Vector{0, 1, 2, 2, 3, 0}) { + immVertex2f(pos, selection_quad[i][0], selection_quad[i][1]); + } + + immEnd(); + } +} + +static blender::float2 coords_region_view_align(const View2D *v2d, const blender::float2 coords) +{ + blender::int2 coords_view; + UI_view2d_view_to_region(v2d, coords.x, coords.y, &coords_view.x, &coords_view.y); + coords_view.x = std::round(coords_view.x); + coords_view.y = std::round(coords_view.y); + blender::float2 coords_region_aligned; + UI_view2d_region_to_view( + v2d, coords_view.x, coords_view.y, &coords_region_aligned.x, &coords_region_aligned.y); + return coords_region_aligned; +} + +static void text_edit_draw_cursor(const bContext *C, const Sequence *seq, uint pos) +{ + const TextVars *data = static_cast(seq->effectdata); + const TextVarsRuntime *text = data->runtime; + const Scene *scene = CTX_data_scene(C); + + const blender::float3 view_offs{-scene->r.xsch / 2.0f, -scene->r.ysch / 2.0f, 0.0f}; + const float view_aspect = scene->r.xasp / scene->r.yasp; + blender::float4x4 transform_mat; + SEQ_image_transform_matrix_get(scene, seq, transform_mat.ptr()); + const blender::int2 cursor_position = seq_text_cursor_offset_to_position(text, + data->cursor_offset); + const float cursor_width = 10; + blender::float2 cursor_coords = + text->lines[cursor_position.y].characters[cursor_position.x].position; + /* Clamp cursor coords to be inside of text boundbox. Compensate for cursor width, but also line + * width hardcoded in shader. */ + rcti text_boundbox = text->text_boundbox; + text_boundbox.xmax -= cursor_width + U.pixelsize; + text_boundbox.xmin += U.pixelsize; + + cursor_coords.x = std::clamp( + cursor_coords.x, float(text_boundbox.xmin), float(text_boundbox.xmax)); + cursor_coords = coords_region_view_align(UI_view2d_fromcontext(C), cursor_coords); + + blender::float4x3 cursor_quad{ + {cursor_coords.x, cursor_coords.y, 0.0f}, + {cursor_coords.x, cursor_coords.y + text->line_height, 0.0f}, + {cursor_coords.x + cursor_width, cursor_coords.y + text->line_height, 0.0f}, + {cursor_coords.x + cursor_width, cursor_coords.y, 0.0f}, + }; + const blender::float3 descender_offs{0.0f, float(text->font_descender), 0.0f}; + + immBegin(GPU_PRIM_TRIS, 6); + immUniformThemeColor(TH_SEQ_TEXT_CURSOR); + + for (int i : blender::IndexRange(0, 4)) { + cursor_quad[i] += descender_offs + view_offs; + cursor_quad[i] = blender::math::transform_point(transform_mat, cursor_quad[i]); + cursor_quad[i].x *= view_aspect; + } + for (int i : blender::Vector{0, 1, 2, 2, 3, 0}) { + immVertex2f(pos, cursor_quad[i][0], cursor_quad[i][1]); + } + + immEnd(); +} + +static void text_edit_draw_box(const bContext *C, const Sequence *seq, uint pos) +{ + const TextVars *data = static_cast(seq->effectdata); + const TextVarsRuntime *text = data->runtime; + const Scene *scene = CTX_data_scene(C); + + const blender::float3 view_offs{-scene->r.xsch / 2.0f, -scene->r.ysch / 2.0f, 0.0f}; + const float view_aspect = scene->r.xasp / scene->r.yasp; + blender::float4x4 transform_mat; + SEQ_image_transform_matrix_get(CTX_data_scene(C), seq, transform_mat.ptr()); + blender::float4x3 box_quad{ + {float(text->text_boundbox.xmin), float(text->text_boundbox.ymin), 0.0f}, + {float(text->text_boundbox.xmin), float(text->text_boundbox.ymax), 0.0f}, + {float(text->text_boundbox.xmax), float(text->text_boundbox.ymax), 0.0f}, + {float(text->text_boundbox.xmax), float(text->text_boundbox.ymin), 0.0f}, + }; + + GPU_blend(GPU_BLEND_NONE); + immBindBuiltinProgram(GPU_SHADER_3D_LINE_DASHED_UNIFORM_COLOR); + blender::float3 col; + UI_GetThemeColorShade3fv(TH_SEQ_ACTIVE, -50, col); + immUniformColor3fv(col); + immUniform1f("lineWidth", U.pixelsize); + immUniform1f("dash_width", 10.0f); + immBegin(GPU_PRIM_LINE_LOOP, 4); + + for (int i : blender::IndexRange(0, 4)) { + box_quad[i] += view_offs; + box_quad[i] = blender::math::transform_point(transform_mat, box_quad[i]); + box_quad[i].x *= view_aspect; + immVertex2f(pos, box_quad[i][0], box_quad[i][1]); + } + + immEnd(); + immUnbindProgram(); +} + +static void text_edit_draw(const bContext *C) +{ + if (!sequencer_text_editing_active_poll(const_cast(C))) { + return; + } + const Sequence *seq = SEQ_select_active_get(CTX_data_scene(C)); + if (!SEQ_effects_can_render_text(seq)) { + return; + } + + GPUVertFormat *format = immVertexFormat(); + const uint pos = GPU_vertformat_attr_add(format, "pos", GPU_COMP_F32, 2, GPU_FETCH_FLOAT); + GPU_line_smooth(true); + GPU_blend(GPU_BLEND_ALPHA); + immBindBuiltinProgram(GPU_SHADER_3D_UNIFORM_COLOR); + + text_selection_draw(C, seq, pos); + text_edit_draw_cursor(C, seq, pos); + + immUnbindProgram(); + GPU_blend(GPU_BLEND_NONE); + GPU_line_smooth(false); + + text_edit_draw_box(C, seq, pos); +} + void sequencer_draw_preview(const bContext *C, Scene *scene, ARegion *region, @@ -1156,6 +1347,7 @@ void sequencer_draw_preview(const bContext *C, Sequence *active_seq = SEQ_select_active_get(scene); for (Sequence *seq : strips) { seq_draw_image_origin_and_outline(C, seq, seq == active_seq); + text_edit_draw(C); } } diff --git a/source/blender/editors/space_sequencer/sequencer_select.cc b/source/blender/editors/space_sequencer/sequencer_select.cc index 4cce996aad7..a4a87568dad 100644 --- a/source/blender/editors/space_sequencer/sequencer_select.cc +++ b/source/blender/editors/space_sequencer/sequencer_select.cc @@ -438,12 +438,12 @@ void recurs_sel_seq(Sequence *seq_meta) } } -static bool seq_point_image_isect(const Scene *scene, const Sequence *seq, float point[2]) +bool seq_point_image_isect(const Scene *scene, const Sequence *seq, float point_view[2]) { float seq_image_quad[4][2]; SEQ_image_transform_final_quad_get(scene, seq, seq_image_quad); return isect_point_quad_v2( - point, seq_image_quad[0], seq_image_quad[1], seq_image_quad[2], seq_image_quad[3]); + point_view, seq_image_quad[0], seq_image_quad[1], seq_image_quad[2], seq_image_quad[3]); } static void sequencer_select_do_updates(bContext *C, Scene *scene) diff --git a/source/blender/editors/space_sequencer/sequencer_text_edit.cc b/source/blender/editors/space_sequencer/sequencer_text_edit.cc new file mode 100644 index 00000000000..d71f2844cbd --- /dev/null +++ b/source/blender/editors/space_sequencer/sequencer_text_edit.cc @@ -0,0 +1,883 @@ +/* SPDX-FileCopyrightText: 2024 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup spseq + */ + +#include + +#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" + +using namespace blender; + +static bool sequencer_text_editing_poll(bContext *C) +{ + if (!sequencer_editing_initialized_and_active(C)) { + return false; + } + + const Sequence *seq = SEQ_select_active_get(CTX_data_scene(C)); + if (seq == nullptr || seq->type != SEQ_TYPE_TEXT || !SEQ_effects_can_render_text(seq)) { + return false; + } + + const TextVars *data = static_cast(seq->effectdata); + if (data == nullptr || data->runtime == nullptr) { + return false; + } + + return true; +} + +bool sequencer_text_editing_active_poll(bContext *C) +{ + const Sequence *seq = SEQ_select_active_get(CTX_data_scene(C)); + if (seq == 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, seq, BKE_scene_frame_get(scene))) { + return false; + } + + return (seq->flag & SEQ_FLAG_TEXT_EDITING_ACTIVE) != 0; +} + +int2 seq_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 = seq_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 seq_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 !seq_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 = seq_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_start.str_ptr); + char *addr_end = const_cast(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 = seq_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) +{ + Sequence *seq = SEQ_select_active_get(CTX_data_scene(C)); + SEQ_relations_invalidate_cache_raw(CTX_data_scene(C), seq); + 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::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 Sequence *seq = SEQ_select_active_get(CTX_data_scene(C)); + TextVars *data = static_cast(seq->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 = seq_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; + 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 false; + } + + const seq::CharInfo cur_char = character_at_cursor_offset_get(text, data->cursor_offset); + char *cursor_addr = const_cast(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 Sequence *seq = SEQ_select_active_get(CTX_data_scene(C)); + TextVars *data = static_cast(seq->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(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 Sequence *seq = SEQ_select_active_get(CTX_data_scene(C)); + TextVars *data = static_cast(seq->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 Sequence *seq = SEQ_select_active_get(CTX_data_scene(C)); + TextVars *data = static_cast(seq->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 Sequence *seq = SEQ_select_active_get(CTX_data_scene(C)); + TextVars *data = static_cast(seq->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*/) +{ + Sequence *seq = SEQ_select_active_get(CTX_data_scene(C)); + TextVars *data = static_cast(seq->effectdata); + + if (!text_has_selection(data)) { + /* Exit edit mode, so text can be translated by mouse. */ + seq->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*/) +{ + Sequence *seq = SEQ_select_active_get(CTX_data_scene(C)); + if (sequencer_text_editing_active_poll(C)) { + seq->flag &= ~SEQ_FLAG_TEXT_EDITING_ACTIVE; + } + else { + seq->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::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 Sequence *seq = SEQ_select_active_get(scene); + TextVars *data = static_cast(seq->effectdata); + const View2D *v2d = UI_view2d_fromcontext(C); + + int2 mval_region; + WM_event_drag_start_mval(event, CTX_wm_region(C), mval_region); + float3 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::float3 view_offs{-scene->r.xsch / 2.0f, -scene->r.ysch / 2.0f, 0.0f}; + const float view_aspect = scene->r.xasp / scene->r.yasp; + blender::float4x4 transform_mat; + SEQ_image_transform_matrix_get(CTX_data_scene(C), seq, transform_mat.ptr()); + transform_mat = blender::math::invert(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 Sequence *seq = SEQ_select_active_get(scene); + TextVars *data = static_cast(seq->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); + Sequence *seq = SEQ_select_active_get(scene); + TextVars *data = static_cast(seq->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 (!seq_point_image_isect(scene, seq, mouse_loc)) { + seq->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 = seq_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 Sequence *seq = SEQ_select_active_get(CTX_data_scene(C)); + const TextVars *data = static_cast(seq->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 Sequence *seq = SEQ_select_active_get(CTX_data_scene(C)); + TextVars *data = static_cast(seq->effectdata); + const TextVarsRuntime *text = data->runtime; + delete_selected_text(data); + + int clipboard_len; + char *clipboard_buf = WM_clipboard_text_get(false, true, &clipboard_len); + + if (clipboard_len == 0) { + return OPERATOR_CANCELLED; + } + + const int max_str_len = sizeof(data->text) - (BLI_strnlen(data->text, sizeof(data->text)) + 1); + clipboard_len = std::min(clipboard_len, max_str_len); + + const seq::CharInfo cur_char = character_at_cursor_offset_get(text, data->cursor_offset); + char *cursor_addr = const_cast(cur_char.str_ptr); + const size_t move_str_len = BLI_strnlen(cursor_addr, sizeof(data->text)) + 1; + + std::memmove(cursor_addr + clipboard_len, cursor_addr, move_str_len); + std::memcpy(cursor_addr, clipboard_buf, clipboard_len); + + data->cursor_offset += BLI_strlen_utf8(clipboard_buf); + + 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 Sequence *seq = SEQ_select_active_get(CTX_data_scene(C)); + TextVars *data = static_cast(seq->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; +} diff --git a/source/blender/editors/space_sequencer/space_sequencer.cc b/source/blender/editors/space_sequencer/space_sequencer.cc index c559c029c71..944b52940b3 100644 --- a/source/blender/editors/space_sequencer/space_sequencer.cc +++ b/source/blender/editors/space_sequencer/space_sequencer.cc @@ -788,11 +788,16 @@ static void sequencer_preview_region_init(wmWindowManager *wm, ARegion *region) WM_event_add_keymap_handler_v2d_mask(®ion->runtime->handlers, keymap); #endif + /* Own keymap. */ + keymap = WM_keymap_ensure(wm->defaultconf, "SequencerPreview", SPACE_SEQ, RGN_TYPE_WINDOW); + WM_event_add_keymap_handler_v2d_mask(®ion->runtime->handlers, keymap); + keymap = WM_keymap_ensure(wm->defaultconf, "SequencerCommon", SPACE_SEQ, RGN_TYPE_WINDOW); WM_event_add_keymap_handler_v2d_mask(®ion->runtime->handlers, keymap); - /* Own keymap. */ - keymap = WM_keymap_ensure(wm->defaultconf, "SequencerPreview", SPACE_SEQ, RGN_TYPE_WINDOW); + /* Do this instead of adding V2D keymap flag to `art->keymapflag` text editing keymap conflicts + * with V2D keymap. This seems to be only way to define order of evaluation. */ + keymap = WM_keymap_ensure(wm->defaultconf, "View2D", SPACE_EMPTY, RGN_TYPE_WINDOW); WM_event_add_keymap_handler_v2d_mask(®ion->runtime->handlers, keymap); ListBase *lb = WM_dropboxmap_find("Sequencer", SPACE_SEQ, RGN_TYPE_PREVIEW); @@ -1122,8 +1127,7 @@ void ED_spacetype_sequencer() art->on_view2d_changed = sequencer_preview_region_view2d_changed; art->draw = sequencer_preview_region_draw; art->listener = sequencer_preview_region_listener; - art->keymapflag = ED_KEYMAP_TOOL | ED_KEYMAP_GIZMO | ED_KEYMAP_VIEW2D | ED_KEYMAP_FRAMES | - ED_KEYMAP_GPENCIL; + art->keymapflag = ED_KEYMAP_TOOL | ED_KEYMAP_GIZMO | ED_KEYMAP_GPENCIL; BLI_addhead(&st->regiontypes, art); /* List-view/buttons. */ diff --git a/source/blender/makesdna/DNA_sequence_types.h b/source/blender/makesdna/DNA_sequence_types.h index e5f08a3aa04..eec53118532 100644 --- a/source/blender/makesdna/DNA_sequence_types.h +++ b/source/blender/makesdna/DNA_sequence_types.h @@ -32,12 +32,15 @@ struct bSound; namespace blender::seq { struct MediaPresence; struct ThumbnailCache; +struct TextVarsRuntime; } // namespace blender::seq using MediaPresence = blender::seq::MediaPresence; using ThumbnailCache = blender::seq::ThumbnailCache; +using TextVarsRuntime = blender::seq::TextVarsRuntime; #else typedef struct MediaPresence MediaPresence; typedef struct ThumbnailCache ThumbnailCache; +typedef struct TextVarsRuntime TextVarsRuntime; #endif /* -------------------------------------------------------------------- */ @@ -453,9 +456,17 @@ typedef struct TextVars { float outline_width; char flag; char align; + char _pad[2]; + + /* Ofssets in bytes relative to #TextVars::text. */ + int cursor_offset; + int selection_start_offset; + int selection_end_offset; + char align_y DNA_DEPRECATED /* Only used for versioning. */; char anchor_x, anchor_y; - char _pad[7]; + char _pad1; + TextVarsRuntime *runtime; } TextVars; /** #TextVars.flag */ @@ -627,7 +638,7 @@ enum { SEQ_OVERLAP = (1 << 3), SEQ_FILTERY = (1 << 4), SEQ_MUTE = (1 << 5), - /* SEQ_FLAG_SKIP_THUMBNAILS = (1 << 6), */ /* no longer used */ + SEQ_FLAG_TEXT_EDITING_ACTIVE = (1 << 6), SEQ_REVERSE_FRAMES = (1 << 7), SEQ_IPO_FRAME_LOCKED = (1 << 8), SEQ_EFFECT_NOT_LOADED = (1 << 9), diff --git a/source/blender/makesdna/DNA_userdef_types.h b/source/blender/makesdna/DNA_userdef_types.h index f02e3b462b1..1a8fb4978a6 100644 --- a/source/blender/makesdna/DNA_userdef_types.h +++ b/source/blender/makesdna/DNA_userdef_types.h @@ -360,7 +360,7 @@ typedef struct ThemeSpace { /** For sequence editor. */ unsigned char movie[4], movieclip[4], mask[4], image[4], scene[4], audio[4]; unsigned char effect[4], transition[4], meta[4], text_strip[4], color_strip[4]; - unsigned char active_strip[4], selected_strip[4]; + unsigned char active_strip[4], selected_strip[4], text_strip_cursor[4], selected_text[4]; /** For dopesheet - scale factor for size of keyframes (i.e. height of channels). */ float keyframe_scale_fac; diff --git a/source/blender/makesrna/intern/rna_userdef.cc b/source/blender/makesrna/intern/rna_userdef.cc index 08f1d3e6375..a1ad6f07421 100644 --- a/source/blender/makesrna/intern/rna_userdef.cc +++ b/source/blender/makesrna/intern/rna_userdef.cc @@ -3869,6 +3869,18 @@ static void rna_def_userdef_theme_space_seq(BlenderRNA *brna) RNA_def_property_array(prop, 4); RNA_def_property_ui_text(prop, "Alternate Rows", "Overlay color on every other row"); RNA_def_property_update(prop, 0, "rna_userdef_theme_update"); + + prop = RNA_def_property(srna, "text_strip_cursor", PROP_FLOAT, PROP_COLOR_GAMMA); + RNA_def_property_float_sdna(prop, nullptr, "text_strip_cursor"); + RNA_def_property_array(prop, 4); + RNA_def_property_ui_text(prop, "Text Strip Cursor", "Text strip editing cursor"); + RNA_def_property_update(prop, 0, "rna_userdef_theme_update"); + + prop = RNA_def_property(srna, "selected_text", PROP_FLOAT, PROP_COLOR_GAMMA); + RNA_def_property_float_sdna(prop, nullptr, "selected_text"); + RNA_def_property_array(prop, 4); + RNA_def_property_ui_text(prop, "Selected text", "Text strip editing selection"); + RNA_def_property_update(prop, 0, "rna_userdef_theme_update"); } static void rna_def_userdef_theme_space_action(BlenderRNA *brna) diff --git a/source/blender/sequencer/SEQ_effects.hh b/source/blender/sequencer/SEQ_effects.hh index c52d8fcd56a..e95277b2c90 100644 --- a/source/blender/sequencer/SEQ_effects.hh +++ b/source/blender/sequencer/SEQ_effects.hh @@ -92,10 +92,12 @@ SeqEffectHandle SEQ_effect_handle_get(Sequence *seq); int SEQ_effect_get_num_inputs(int seq_type); void SEQ_effect_text_font_unload(TextVars *data, bool do_id_user); void SEQ_effect_text_font_load(TextVars *data, bool do_id_user); +bool SEQ_effects_can_render_text(const Sequence *seq); namespace blender::seq { struct CharInfo { + int index = 0; const char *str_ptr = nullptr; int byte_length = 0; float2 position{0.0f, 0.0f}; @@ -111,11 +113,12 @@ struct LineInfo { struct TextVarsRuntime { Vector lines; - rcti text_boundbox; + rcti text_boundbox; /* Boundbox used for box drawing and selection. */ int line_height; int font_descender; int character_count; int font; + bool editing_is_active; /* UI uses this to differentiate behavior. */ }; } // namespace blender::seq diff --git a/source/blender/sequencer/SEQ_transform.hh b/source/blender/sequencer/SEQ_transform.hh index 61d117a0a87..107b176be2e 100644 --- a/source/blender/sequencer/SEQ_transform.hh +++ b/source/blender/sequencer/SEQ_transform.hh @@ -129,3 +129,15 @@ void SEQ_image_transform_bounding_box_from_collection(Scene *scene, bool apply_rotation, float r_min[2], float r_max[2]); + +/** + * Get strip image transformation matrix. Pivot point is set to correspond with viewport coordinate + * system + * + * \param scene: Scene in which strips are located + * \param seq: Strip that is used to construct the matrix + * \param r_transform_matrix: Return value + */ +void SEQ_image_transform_matrix_get(const Scene *scene, + const Sequence *seq, + float r_transform_matrix[4][4]); diff --git a/source/blender/sequencer/intern/effects.cc b/source/blender/sequencer/intern/effects.cc index 2933cdcdf99..5d1a349a4c9 100644 --- a/source/blender/sequencer/intern/effects.cc +++ b/source/blender/sequencer/intern/effects.cc @@ -9,6 +9,7 @@ */ #include +#include #include #include #include @@ -2612,6 +2613,21 @@ static ImBuf *do_gaussian_blur_effect(const SeqRenderData *context, /** \name Text Effect * \{ */ +/* `data->text[0] == 0` is ignored on purpose in order to make it possible to edit */ +bool SEQ_effects_can_render_text(const Sequence *seq) +{ + TextVars *data = static_cast(seq->effectdata); + if (data->text_size < 1.0f || + ((data->color[3] == 0.0f) && + (data->shadow_color[3] == 0.0f || (data->flag & SEQ_TEXT_SHADOW) == 0) && + (data->outline_color[3] == 0.0f || data->outline_width <= 0.0f || + (data->flag & SEQ_TEXT_OUTLINE) == 0))) + { + return false; + } + return true; +} + static void init_text_effect(Sequence *seq) { if (seq->effectdata) { @@ -2706,6 +2722,7 @@ static void free_text_effect(Sequence *seq, const bool do_id_user) SEQ_effect_text_font_unload(data, do_id_user); if (data) { + MEM_delete(data->runtime); MEM_freeN(data); seq->effectdata = nullptr; } @@ -2722,6 +2739,7 @@ static void copy_text_effect(Sequence *dst, const Sequence *src, const int flag) dst->effectdata = MEM_dupallocN(src->effectdata); TextVars *data = static_cast(dst->effectdata); + data->runtime = nullptr; data->text_blf_id = -1; SEQ_effect_text_font_load(data, (flag & LIB_ID_CREATE_NO_USER_REFCOUNT) == 0); } @@ -2733,13 +2751,7 @@ static int num_inputs_text() static StripEarlyOut early_out_text(const Sequence *seq, float /*fac*/) { - TextVars *data = static_cast(seq->effectdata); - if (data->text[0] == 0 || data->text_size < 1.0f || - ((data->color[3] == 0.0f) && - (data->shadow_color[3] == 0.0f || (data->flag & SEQ_TEXT_SHADOW) == 0) && - (data->outline_color[3] == 0.0f || data->outline_width <= 0.0f || - (data->flag & SEQ_TEXT_OUTLINE) == 0))) - { + if (!SEQ_effects_can_render_text(seq)) { return StripEarlyOut::UseInput1; } return StripEarlyOut::NoInput; @@ -2999,29 +3011,29 @@ static void jump_flooding_pass(Span input, } namespace blender::seq { -static void text_draw(const TextVarsRuntime &runtime, float color[4]) +static void text_draw(const TextVarsRuntime *runtime, float color[4]) { - for (const LineInfo &line : runtime.lines) { + for (const LineInfo &line : runtime->lines) { for (const CharInfo &character : line.characters) { - BLF_position(runtime.font, character.position.x, character.position.y, 0.0f); - BLF_buffer_col(runtime.font, color); - BLF_draw_buffer(runtime.font, character.str_ptr, character.byte_length); + BLF_position(runtime->font, character.position.x, character.position.y, 0.0f); + BLF_buffer_col(runtime->font, color); + BLF_draw_buffer(runtime->font, character.str_ptr, character.byte_length); } } } static rcti draw_text_outline(const SeqRenderData *context, const TextVars *data, - const TextVarsRuntime &runtime, + const TextVarsRuntime *runtime, ColorManagedDisplay *display, ImBuf *out) { /* Outline width of 1.0 maps to half of text line height. */ - const int outline_width = int(runtime.line_height * 0.5f * data->outline_width); + const int outline_width = int(runtime->line_height * 0.5f * data->outline_width); if (outline_width < 1 || data->outline_color[3] <= 0.0f || ((data->flag & SEQ_TEXT_OUTLINE) == 0)) { - return runtime.text_boundbox; + return runtime->text_boundbox; } const int2 size = int2(context->rectx, context->recty); @@ -3029,11 +3041,11 @@ static rcti draw_text_outline(const SeqRenderData *context, /* Draw white text into temporary buffer. */ const size_t pixel_count = size_t(size.x) * size.y; Array tmp_buf(pixel_count, uchar4(0)); - BLF_buffer(runtime.font, nullptr, (uchar *)tmp_buf.data(), size.x, size.y, display); + BLF_buffer(runtime->font, nullptr, (uchar *)tmp_buf.data(), size.x, size.y, display); text_draw(runtime, float4(1.0f)); - rcti outline_rect = runtime.text_boundbox; + rcti outline_rect = runtime->text_boundbox; BLI_rcti_pad(&outline_rect, outline_width + 1, outline_width + 1); outline_rect.xmin = clamp_i(outline_rect.xmin, 0, size.x - 1); outline_rect.xmax = clamp_i(outline_rect.xmax, 0, size.x - 1); @@ -3129,7 +3141,7 @@ static rcti draw_text_outline(const SeqRenderData *context, } } }); - BLF_buffer(runtime.font, nullptr, out->byte_buffer.data, size.x, size.y, display); + BLF_buffer(runtime->font, nullptr, out->byte_buffer.data, size.x, size.y, display); return outline_rect; } @@ -3252,17 +3264,20 @@ static blender::Vector build_character_info(const TextVars *data, int blender::Vector characters; const size_t len_max = BLI_strnlen(data->text, sizeof(data->text)); int byte_offset = 0; + int char_index = 0; while (byte_offset <= len_max) { const char *str = data->text + byte_offset; const int char_length = BLI_str_utf8_size_safe(str); CharInfo char_info; + char_info.index = char_index; char_info.str_ptr = str; char_info.byte_length = char_length; char_info.advance_x = BLF_glyph_advance(font, str); characters.append(char_info); byte_offset += char_length; + char_index++; } return characters; } @@ -3277,7 +3292,7 @@ static int wrap_width_get(const TextVars *data, const int2 image_size) /* Lines must contain CharInfo for newlines and \0, as UI must know where they begin. */ static void apply_word_wrapping(const TextVars *data, - TextVarsRuntime &runtime, + TextVarsRuntime *runtime, const int2 image_size, blender::Vector &characters) { @@ -3305,18 +3320,18 @@ static void apply_word_wrapping(const TextVars *data, /* Second pass: Fill lines with characters. */ char_position = {0.0f, 0.0f}; - runtime.lines.append(LineInfo()); + runtime->lines.append(LineInfo()); for (CharInfo &character : characters) { character.position = char_position; - runtime.lines.last().characters.append(character); - runtime.lines.last().width = char_position.x; + runtime->lines.last().characters.append(character); + runtime->lines.last().width = char_position.x; char_position.x += character.advance_x; if (character.do_wrap || character.str_ptr[0] == '\n') { - runtime.lines.append(LineInfo()); + runtime->lines.append(LineInfo()); char_position.x = 0; - char_position.y -= runtime.line_height; + char_position.y -= runtime->line_height; } } } @@ -3377,59 +3392,67 @@ static float2 anchor_offset_get(const TextVars *data, int width_max, int text_he return anchor_offset; } -static void apply_text_alignment(const TextVars *data, - TextVarsRuntime &runtime, - const int2 image_size) +static void calc_boundbox(const TextVars *data, TextVarsRuntime *runtime, const int2 image_size) { - const int width_max = text_box_width_get(runtime.lines); - const int text_height = runtime.lines.size() * runtime.line_height; + const int text_height = runtime->lines.size() * runtime->line_height; + + int width_max = text_box_width_get(runtime->lines); + + /* Add width to empty text, so there is something to draw or select. */ + if (width_max == 0) { + width_max = text_height * 2; + } const float2 image_center{data->loc[0] * image_size.x, data->loc[1] * image_size.y}; - const float2 line_height_offset{0.0f, float(-runtime.line_height - BLF_descender(runtime.font))}; const float2 anchor = anchor_offset_get(data, width_max, text_height); - Vector line_boxes; + runtime->text_boundbox.xmin = anchor.x + image_center.x; + runtime->text_boundbox.xmax = anchor.x + image_center.x + width_max; + runtime->text_boundbox.ymin = anchor.y + image_center.y - text_height; + runtime->text_boundbox.ymax = runtime->text_boundbox.ymin + text_height; +} - for (LineInfo &line : runtime.lines) { +static void apply_text_alignment(const TextVars *data, + TextVarsRuntime *runtime, + const int2 image_size) +{ + const int width_max = text_box_width_get(runtime->lines); + const int text_height = runtime->lines.size() * runtime->line_height; + + const float2 image_center{data->loc[0] * image_size.x, data->loc[1] * image_size.y}; + const float2 line_height_offset{0.0f, + float(-runtime->line_height - BLF_descender(runtime->font))}; + const float2 anchor = anchor_offset_get(data, width_max, text_height); + + for (LineInfo &line : runtime->lines) { const float2 alignment_x = horizontal_alignment_offset_get(data, line.width, width_max); const float2 alignment = math::round(image_center + line_height_offset + alignment_x + anchor); for (CharInfo &character : line.characters) { character.position += alignment; } - - /* Get text box for line. - * This has to be done, because some fonts do not define a descender value, - * but define their height. In that case, box has unwanted offset in Y axis. */ - rcti line_box; - size_t str_len = line.characters.last().str_ptr - line.characters.first().str_ptr; - BLF_boundbox(runtime.font, line.characters.first().str_ptr, str_len, &line_box, nullptr); - BLI_rcti_translate( - &line_box, line.characters.first().position.x, line.characters.first().position.y); - line_boxes.append(line_box); - } - - runtime.text_boundbox = line_boxes.first(); - for (const rcti &box : line_boxes) { - BLI_rcti_union(&runtime.text_boundbox, &box); } } -static void calc_text_runtime(const Sequence *seq, - int font, - const int2 image_size, - TextVarsRuntime &r_runtime) +static void calc_text_runtime(const Sequence *seq, int font, const int2 image_size) { - const TextVars *data = static_cast(seq->effectdata); + TextVars *data = static_cast(seq->effectdata); - r_runtime.font = font; - r_runtime.line_height = BLF_height_max(font); - r_runtime.font_descender = BLF_descender(font); - r_runtime.character_count = BLI_strlen_utf8(data->text); + if (data->runtime != nullptr) { + MEM_delete(data->runtime); + } + + data->runtime = MEM_new(__func__); + TextVarsRuntime *runtime = data->runtime; + runtime->font = font; + runtime->line_height = BLF_height_max(font); + runtime->font_descender = BLF_descender(font); + runtime->character_count = BLI_strlen_utf8(data->text); blender::Vector characters_temp = build_character_info(data, font); - apply_word_wrapping(data, r_runtime, image_size, characters_temp); - apply_text_alignment(data, r_runtime, image_size); + apply_word_wrapping(data, runtime, image_size, characters_temp); + apply_text_alignment(data, runtime, image_size); + calc_boundbox(data, runtime, image_size); } static ImBuf *do_text_effect(const SeqRenderData *context, @@ -3454,8 +3477,8 @@ static ImBuf *do_text_effect(const SeqRenderData *context, const int font = text_effect_font_init(context, seq, font_flags); - TextVarsRuntime runtime; - calc_text_runtime(seq, font, {out->x, out->y}, runtime); + calc_text_runtime(seq, font, {out->x, out->y}); + TextVarsRuntime *runtime = data->runtime; rcti outline_rect = draw_text_outline(context, data, runtime, display, out); BLF_buffer(font, nullptr, out->byte_buffer.data, out->x, out->y, display); @@ -3465,17 +3488,17 @@ static ImBuf *do_text_effect(const SeqRenderData *context, /* Draw shadow. */ if (data->flag & SEQ_TEXT_SHADOW) { - draw_text_shadow(context, data, runtime.line_height, outline_rect, out); + draw_text_shadow(context, data, runtime->line_height, outline_rect, out); } /* Draw box under text. */ if (data->flag & SEQ_TEXT_BOX) { if (out->byte_buffer.data) { const int margin = data->box_margin * out->x; - const int minx = runtime.text_boundbox.xmin - margin; - const int maxx = runtime.text_boundbox.xmax + margin; - const int miny = runtime.text_boundbox.ymin - margin; - const int maxy = runtime.text_boundbox.ymax + margin; + const int minx = runtime->text_boundbox.xmin - margin; + const int maxx = runtime->text_boundbox.xmax + margin; + const int miny = runtime->text_boundbox.ymin - margin; + const int maxy = runtime->text_boundbox.ymax + margin; float corner_radius = data->box_roundness * (maxy - miny) / 2.0f; fill_rect_alpha_under(out, data->box_color, minx, miny, maxx, maxy, corner_radius); } diff --git a/source/blender/sequencer/intern/sequencer.cc b/source/blender/sequencer/intern/sequencer.cc index c4144fa9280..e035eb9acf1 100644 --- a/source/blender/sequencer/intern/sequencer.cc +++ b/source/blender/sequencer/intern/sequencer.cc @@ -871,6 +871,7 @@ static bool seq_read_data_cb(Sequence *seq, void *user_data) if (seq->type == SEQ_TYPE_TEXT) { TextVars *t = static_cast(seq->effectdata); t->text_blf_id = SEQ_FONT_NOT_LOADED; + t->runtime = nullptr; } BLO_read_struct(reader, IDProperty, &seq->prop); diff --git a/source/blender/sequencer/intern/strip_transform.cc b/source/blender/sequencer/intern/strip_transform.cc index a4cc30f1890..d13574bdb3b 100644 --- a/source/blender/sequencer/intern/strip_transform.cc +++ b/source/blender/sequencer/intern/strip_transform.cc @@ -8,6 +8,7 @@ * \ingroup bke */ +#include "BLI_math_matrix_types.hh" #include "DNA_scene_types.h" #include "DNA_sequence_types.h" @@ -16,6 +17,7 @@ #include "BLI_math_rotation.h" #include "BLI_math_vector.h" #include "BLI_math_vector_types.hh" +#include "BLI_rect.h" #include "SEQ_animation.hh" #include "SEQ_channels.hh" @@ -642,6 +644,29 @@ void SEQ_image_transform_origin_offset_pixelspace_get(const Scene *scene, mul_v2_v2(r_origin, viewport_pixel_aspect); } +void SEQ_image_transform_matrix_get(const Scene *scene, + const Sequence *seq, + float r_transform_matrix[4][4]) +{ + float image_size[2] = {float(scene->r.xsch), float(scene->r.ysch)}; + if (ELEM(seq->type, SEQ_TYPE_MOVIE, SEQ_TYPE_IMAGE)) { + image_size[0] = seq->strip->stripdata->orig_width; + image_size[1] = seq->strip->stripdata->orig_height; + } + + StripTransform *transform = seq->strip->transform; + float rotation_matrix[3][3]; + axis_angle_to_mat3_single(rotation_matrix, 'Z', transform->rotation); + loc_rot_size_to_mat4(r_transform_matrix, + blender::float3{transform->xofs, transform->yofs, 0.0f}, + rotation_matrix, + blender::float3{transform->scale_x, transform->scale_y, 1.0f}); + const float origin[2] = {image_size[0] * transform->origin[0], + image_size[1] * transform->origin[1]}; + const float pivot[3] = {origin[0] - (image_size[0] / 2), origin[1] - (image_size[1] / 2), 0.0f}; + transform_pivot_set_m4(r_transform_matrix, pivot); +} + static void seq_image_transform_quad_get_ex(const Scene *scene, const Sequence *seq, bool apply_rotation, diff --git a/tests/data b/tests/data index 97542f876bd..bc0b4f993fd 160000 --- a/tests/data +++ b/tests/data @@ -1 +1 @@ -Subproject commit 97542f876bdca3e4917ba7e3b8c88d26a9d86afd +Subproject commit bc0b4f993fd99d84c93050b20ae432abcff4c55e