From 8ce53565224a295bb34aacdb50e150ba283ee7c4 Mon Sep 17 00:00:00 2001 From: Richard Antalik Date: Thu, 19 Dec 2024 15:48:06 +0100 Subject: [PATCH] VSE: Text editing in preview This commit implements most features needed for simple text editing. Active text strip can be edited in preview by pressing tab key, which enabled text editing mode. With this mode active, outline matches text boundary box and cursor is drawn. Cursor can be moved with usual keys. Pressing shift starts selection. Selection and navigation works when text is scaled or rotated. Mirrored text is not supported in this PR. it can be done, but the text is unreadable that way, so I kept it simple. Multi line text is supported. Pressing return key starts new line. Copy/paste operator uses OS copy paste buffer, so text from other apps can be pasted. Text is still limited to 512 characters. Text string property still exists in side panel and is limited to single line. Individual characters can not be styled in different way like in 3D viewport, but the code is mostly ready for such feature. Ref: #126547 Pull Request: https://projects.blender.org/blender/blender/pulls/127239 --- .../datafiles/userdef/userdef_default_theme.c | 2 + .../presets/interface_theme/Blender_Light.xml | 2 + .../keyconfig/keymap_data/blender_default.py | 54 ++ scripts/startup/bl_ui/space_sequencer.py | 30 +- .../blender/blenkernel/BKE_blender_version.h | 2 +- .../blenloader/intern/versioning_400.cc | 15 + .../blenloader/intern/versioning_userdef.cc | 4 + .../blender/editors/include/UI_resources.hh | 2 + source/blender/editors/interface/resources.cc | 6 + .../editors/space_sequencer/CMakeLists.txt | 1 + .../space_sequencer/sequencer_intern.hh | 17 + .../editors/space_sequencer/sequencer_ops.cc | 13 + .../space_sequencer/sequencer_preview_draw.cc | 192 ++++ .../space_sequencer/sequencer_select.cc | 4 +- .../space_sequencer/sequencer_text_edit.cc | 883 ++++++++++++++++++ .../space_sequencer/space_sequencer.cc | 12 +- source/blender/makesdna/DNA_sequence_types.h | 15 +- source/blender/makesdna/DNA_userdef_types.h | 2 +- source/blender/makesrna/intern/rna_userdef.cc | 12 + source/blender/sequencer/SEQ_effects.hh | 5 +- source/blender/sequencer/SEQ_transform.hh | 12 + source/blender/sequencer/intern/effects.cc | 153 +-- source/blender/sequencer/intern/sequencer.cc | 1 + .../sequencer/intern/strip_transform.cc | 25 + tests/data | 2 +- 25 files changed, 1386 insertions(+), 80 deletions(-) create mode 100644 source/blender/editors/space_sequencer/sequencer_text_edit.cc 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