diff options
Diffstat (limited to 'scene')
46 files changed, 2338 insertions, 1591 deletions
diff --git a/scene/2d/parallax_2d.cpp b/scene/2d/parallax_2d.cpp index 555f3b031c..aacab3213d 100644 --- a/scene/2d/parallax_2d.cpp +++ b/scene/2d/parallax_2d.cpp @@ -144,7 +144,7 @@ void Parallax2D::set_repeat_size(const Size2 &p_repeat_size) { return; } - repeat_size = p_repeat_size.max(Vector2(0, 0)); + repeat_size = p_repeat_size.maxf(0); _update_process(); _update_repeat(); diff --git a/scene/3d/cpu_particles_3d.cpp b/scene/3d/cpu_particles_3d.cpp index 0dc9834539..db7b80683c 100644 --- a/scene/3d/cpu_particles_3d.cpp +++ b/scene/3d/cpu_particles_3d.cpp @@ -880,7 +880,7 @@ void CPUParticles3D::_particles_process(double p_delta) { } break; case EMISSION_SHAPE_RING: { real_t ring_random_angle = Math::randf() * Math_TAU; - real_t ring_random_radius = Math::randf() * (emission_ring_radius - emission_ring_inner_radius) + emission_ring_inner_radius; + real_t ring_random_radius = Math::sqrt(Math::randf() * (emission_ring_radius - emission_ring_inner_radius * emission_ring_inner_radius) + emission_ring_inner_radius * emission_ring_inner_radius); Vector3 axis = emission_ring_axis == Vector3(0.0, 0.0, 0.0) ? Vector3(0.0, 0.0, 1.0) : emission_ring_axis.normalized(); Vector3 ortho_axis; if (axis.abs() == Vector3(1.0, 0.0, 0.0)) { diff --git a/scene/3d/decal.cpp b/scene/3d/decal.cpp index 8415fb38cb..485599d0fb 100644 --- a/scene/3d/decal.cpp +++ b/scene/3d/decal.cpp @@ -31,7 +31,7 @@ #include "decal.h" void Decal::set_size(const Vector3 &p_size) { - size = p_size.max(Vector3(0.001, 0.001, 0.001)); + size = p_size.maxf(0.001); RS::get_singleton()->decal_set_size(decal, size); update_gizmos(); } diff --git a/scene/3d/fog_volume.cpp b/scene/3d/fog_volume.cpp index 8af386f282..54631a8dff 100644 --- a/scene/3d/fog_volume.cpp +++ b/scene/3d/fog_volume.cpp @@ -73,7 +73,7 @@ bool FogVolume::_get(const StringName &p_name, Variant &r_property) const { void FogVolume::set_size(const Vector3 &p_size) { size = p_size; - size = size.max(Vector3()); + size = size.maxf(0); RS::get_singleton()->fog_volume_set_size(_get_volume(), size); update_gizmos(); } diff --git a/scene/3d/gpu_particles_collision_3d.cpp b/scene/3d/gpu_particles_collision_3d.cpp index 8fd5f25749..3a05ec9c9e 100644 --- a/scene/3d/gpu_particles_collision_3d.cpp +++ b/scene/3d/gpu_particles_collision_3d.cpp @@ -382,7 +382,7 @@ Vector3i GPUParticlesCollisionSDF3D::get_estimated_cell_size() const { float cell_size = aabb.get_longest_axis_size() / float(subdiv); Vector3i sdf_size = Vector3i(aabb.size / cell_size); - sdf_size = sdf_size.max(Vector3i(1, 1, 1)); + sdf_size = sdf_size.maxi(1); return sdf_size; } @@ -395,7 +395,7 @@ Ref<Image> GPUParticlesCollisionSDF3D::bake() { float cell_size = aabb.get_longest_axis_size() / float(subdiv); Vector3i sdf_size = Vector3i(aabb.size / cell_size); - sdf_size = sdf_size.max(Vector3i(1, 1, 1)); + sdf_size = sdf_size.maxi(1); if (bake_begin_function) { bake_begin_function(100); diff --git a/scene/3d/occluder_instance_3d.cpp b/scene/3d/occluder_instance_3d.cpp index 2f77185d0d..150771545b 100644 --- a/scene/3d/occluder_instance_3d.cpp +++ b/scene/3d/occluder_instance_3d.cpp @@ -192,7 +192,7 @@ void QuadOccluder3D::set_size(const Size2 &p_size) { return; } - size = p_size.max(Size2()); + size = p_size.maxf(0); _update(); } @@ -236,7 +236,7 @@ void BoxOccluder3D::set_size(const Vector3 &p_size) { return; } - size = p_size.max(Vector3()); + size = p_size.maxf(0); _update(); } diff --git a/scene/3d/voxel_gi.cpp b/scene/3d/voxel_gi.cpp index 938d6e5699..fbdda67526 100644 --- a/scene/3d/voxel_gi.cpp +++ b/scene/3d/voxel_gi.cpp @@ -294,7 +294,7 @@ VoxelGI::Subdiv VoxelGI::get_subdiv() const { void VoxelGI::set_size(const Vector3 &p_size) { // Prevent very small size dimensions as these breaks baking if other size dimensions are set very high. - size = p_size.max(Vector3(1.0, 1.0, 1.0)); + size = p_size.maxf(1.0); update_gizmos(); } diff --git a/scene/3d/xr_hand_modifier_3d.cpp b/scene/3d/xr_hand_modifier_3d.cpp index 1e78a4630f..baaa9eee48 100644 --- a/scene/3d/xr_hand_modifier_3d.cpp +++ b/scene/3d/xr_hand_modifier_3d.cpp @@ -70,6 +70,11 @@ void XRHandModifier3D::_get_joint_data() { return; } + if (has_stored_previous_transforms) { + previous_relative_transforms.clear(); + has_stored_previous_transforms = false; + } + // Table of bone names for different rig types. static const String bone_names[XRHandTracker::HAND_JOINT_MAX] = { "Palm", @@ -196,6 +201,18 @@ void XRHandModifier3D::_process_modification() { // Skip if no tracking data if (!tracker->get_has_tracking_data()) { + if (!has_stored_previous_transforms) { + return; + } + + // Apply previous relative transforms if they are stored. + for (int joint = 0; joint < XRHandTracker::HAND_JOINT_MAX; joint++) { + if (bone_update == BONE_UPDATE_FULL) { + skeleton->set_bone_pose_position(joints[joint].bone, previous_relative_transforms[joint].origin); + } + + skeleton->set_bone_pose_rotation(joints[joint].bone, Quaternion(previous_relative_transforms[joint].basis)); + } return; } @@ -223,6 +240,12 @@ void XRHandModifier3D::_process_modification() { return; } + if (!has_stored_previous_transforms) { + previous_relative_transforms.resize(XRHandTracker::HAND_JOINT_MAX); + has_stored_previous_transforms = true; + } + Transform3D *previous_relative_transforms_ptr = previous_relative_transforms.ptrw(); + for (int joint = 0; joint < XRHandTracker::HAND_JOINT_MAX; joint++) { // Get the skeleton bone (skip if none). const int bone = joints[joint].bone; @@ -233,6 +256,7 @@ void XRHandModifier3D::_process_modification() { // Calculate the relative relationship to the parent bone joint. const int parent_joint = joints[joint].parent_joint; const Transform3D relative_transform = inv_transforms[parent_joint] * transforms[joint]; + previous_relative_transforms_ptr[joint] = relative_transform; // Update the bone position if enabled by update mode. if (bone_update == BONE_UPDATE_FULL) { diff --git a/scene/3d/xr_hand_modifier_3d.h b/scene/3d/xr_hand_modifier_3d.h index 67d1694d41..3d78f32b64 100644 --- a/scene/3d/xr_hand_modifier_3d.h +++ b/scene/3d/xr_hand_modifier_3d.h @@ -73,6 +73,9 @@ private: BoneUpdate bone_update = BONE_UPDATE_FULL; JointData joints[XRHandTracker::HAND_JOINT_MAX]; + bool has_stored_previous_transforms = false; + Vector<Transform3D> previous_relative_transforms; + void _get_joint_data(); void _tracker_changed(StringName p_tracker_name, XRServer::TrackerType p_tracker_type); }; diff --git a/scene/animation/animation_mixer.cpp b/scene/animation/animation_mixer.cpp index 5a3a5f9bc0..d22b58346f 100644 --- a/scene/animation/animation_mixer.cpp +++ b/scene/animation/animation_mixer.cpp @@ -1617,7 +1617,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { } if (seeked) { // Seek. - int idx = a->track_find_key(i, time, is_external_seeking ? Animation::FIND_MODE_NEAREST : Animation::FIND_MODE_EXACT, true); + int idx = a->track_find_key(i, time, Animation::FIND_MODE_NEAREST, true); if (idx < 0) { continue; } @@ -1630,6 +1630,9 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { double at_anim_pos = 0.0; switch (anim->get_loop_mode()) { case Animation::LOOP_NONE: { + if (!is_external_seeking && ((!backward && time >= pos + (double)anim->get_length()) || (backward && time <= pos))) { + continue; // Do nothing if current time is outside of length when started. + } at_anim_pos = MIN((double)anim->get_length(), time - pos); // Seek to end. } break; case Animation::LOOP_LINEAR: { @@ -1641,7 +1644,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { default: break; } - if (player2->is_playing()) { + if (player2->is_playing() || !is_external_seeking) { player2->seek(at_anim_pos, false, p_update_only); player2->play(anim_name); t->playing = true; @@ -2090,7 +2093,7 @@ Ref<AnimatedValuesBackup> AnimationMixer::apply_reset(bool p_user_initiated) { void AnimationMixer::capture(const StringName &p_name, double p_duration, Tween::TransitionType p_trans_type, Tween::EaseType p_ease_type) { ERR_FAIL_COND(!active); ERR_FAIL_COND(!has_animation(p_name)); - ERR_FAIL_COND(Math::is_zero_approx(p_duration)); + ERR_FAIL_COND(p_duration <= 0); Ref<Animation> reference_animation = get_animation(p_name); if (!cache_valid) { diff --git a/scene/animation/animation_player.cpp b/scene/animation/animation_player.cpp index 7140161eca..e4808a0ecc 100644 --- a/scene/animation/animation_player.cpp +++ b/scene/animation/animation_player.cpp @@ -370,73 +370,21 @@ void AnimationPlayer::play_backwards(const StringName &p_name, double p_custom_b play(p_name, p_custom_blend, -1, true); } -void AnimationPlayer::play_with_capture(const StringName &p_name, double p_duration, double p_custom_blend, float p_custom_scale, bool p_from_end, Tween::TransitionType p_trans_type, Tween::EaseType p_ease_type) { - StringName name = p_name; - if (name == StringName()) { - name = playback.assigned; - } - - if (signbit(p_duration)) { - double max_dur = 0; - Ref<Animation> anim = get_animation(name); - if (anim.is_valid()) { - double current_pos = playback.current.pos; - if (playback.assigned != name) { - current_pos = p_from_end ? anim->get_length() : 0; - } - for (int i = 0; i < anim->get_track_count(); i++) { - if (anim->track_get_type(i) != Animation::TYPE_VALUE) { - continue; - } - if (anim->value_track_get_update_mode(i) != Animation::UPDATE_CAPTURE) { - continue; - } - if (anim->track_get_key_count(i) == 0) { - continue; - } - max_dur = MAX(max_dur, p_from_end ? current_pos - anim->track_get_key_time(i, anim->track_get_key_count(i) - 1) : anim->track_get_key_time(i, 0) - current_pos); - } - } - p_duration = max_dur; +void AnimationPlayer::play(const StringName &p_name, double p_custom_blend, float p_custom_scale, bool p_from_end) { + if (auto_capture) { + play_with_capture(p_name, -1.0, p_custom_blend, p_custom_scale, p_from_end); + } else { + _play(p_name, p_custom_blend, p_custom_scale, p_from_end); } - - capture(name, p_duration, p_trans_type, p_ease_type); - play(name, p_custom_blend, p_custom_scale, p_from_end); } -void AnimationPlayer::play(const StringName &p_name, double p_custom_blend, float p_custom_scale, bool p_from_end) { +void AnimationPlayer::_play(const StringName &p_name, double p_custom_blend, float p_custom_scale, bool p_from_end) { StringName name = p_name; if (name == StringName()) { name = playback.assigned; } -#ifdef TOOLS_ENABLED - if (!Engine::get_singleton()->is_editor_hint()) { - bool warn_enabled = false; - if (capture_cache.animation.is_null()) { - Ref<Animation> anim = get_animation(name); - if (anim.is_valid()) { - for (int i = 0; i < anim->get_track_count(); i++) { - if (anim->track_get_type(i) != Animation::TYPE_VALUE) { - continue; - } - if (anim->value_track_get_update_mode(i) != Animation::UPDATE_CAPTURE) { - continue; - } - if (anim->track_get_key_count(i) == 0) { - continue; - } - warn_enabled = true; - } - } - } - if (warn_enabled) { - WARN_PRINT_ONCE_ED("Capture track found. If you want to interpolate animation with captured frame, you can use play_with_capture() instead of play()."); - } - } -#endif - ERR_FAIL_COND_MSG(!animation_set.has(name), vformat("Animation not found: %s.", name)); Playback &c = playback; @@ -525,6 +473,47 @@ void AnimationPlayer::play(const StringName &p_name, double p_custom_blend, floa } } +void AnimationPlayer::_capture(const StringName &p_name, bool p_from_end, double p_duration, Tween::TransitionType p_trans_type, Tween::EaseType p_ease_type) { + StringName name = p_name; + if (name == StringName()) { + name = playback.assigned; + } + + Ref<Animation> anim = get_animation(name); + if (anim.is_null() || !anim->is_capture_included()) { + return; + } + if (signbit(p_duration)) { + double max_dur = 0; + double current_pos = playback.current.pos; + if (playback.assigned != name) { + current_pos = p_from_end ? anim->get_length() : 0; + } + for (int i = 0; i < anim->get_track_count(); i++) { + if (anim->track_get_type(i) != Animation::TYPE_VALUE) { + continue; + } + if (anim->value_track_get_update_mode(i) != Animation::UPDATE_CAPTURE) { + continue; + } + if (anim->track_get_key_count(i) == 0) { + continue; + } + max_dur = MAX(max_dur, p_from_end ? current_pos - anim->track_get_key_time(i, anim->track_get_key_count(i) - 1) : anim->track_get_key_time(i, 0) - current_pos); + } + p_duration = max_dur; + } + if (Math::is_zero_approx(p_duration)) { + return; + } + capture(name, p_duration, p_trans_type, p_ease_type); +} + +void AnimationPlayer::play_with_capture(const StringName &p_name, double p_duration, double p_custom_blend, float p_custom_scale, bool p_from_end, Tween::TransitionType p_trans_type, Tween::EaseType p_ease_type) { + _capture(p_name, p_from_end, p_duration, p_trans_type, p_ease_type); + _play(p_name, p_custom_blend, p_custom_scale, p_from_end); +} + bool AnimationPlayer::is_playing() const { return playing; } @@ -725,6 +714,14 @@ double AnimationPlayer::get_blend_time(const StringName &p_animation1, const Str } } +void AnimationPlayer::set_auto_capture(bool p_auto_capture) { + auto_capture = p_auto_capture; +} + +bool AnimationPlayer::is_auto_capture() const { + return auto_capture; +} + #ifdef TOOLS_ENABLED void AnimationPlayer::get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const { const String pf = p_function; @@ -815,9 +812,12 @@ void AnimationPlayer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_default_blend_time", "sec"), &AnimationPlayer::set_default_blend_time); ClassDB::bind_method(D_METHOD("get_default_blend_time"), &AnimationPlayer::get_default_blend_time); + ClassDB::bind_method(D_METHOD("set_auto_capture", "auto_capture"), &AnimationPlayer::set_auto_capture); + ClassDB::bind_method(D_METHOD("is_auto_capture"), &AnimationPlayer::is_auto_capture); + ClassDB::bind_method(D_METHOD("play", "name", "custom_blend", "custom_speed", "from_end"), &AnimationPlayer::play, DEFVAL(StringName()), DEFVAL(-1), DEFVAL(1.0), DEFVAL(false)); ClassDB::bind_method(D_METHOD("play_backwards", "name", "custom_blend"), &AnimationPlayer::play_backwards, DEFVAL(StringName()), DEFVAL(-1)); - ClassDB::bind_method(D_METHOD("play_with_capture", "name", "duration", "custom_blend", "custom_speed", "from_end", "trans_type", "ease_type"), &AnimationPlayer::play_with_capture, DEFVAL(-1.0), DEFVAL(-1), DEFVAL(1.0), DEFVAL(false), DEFVAL(Tween::TRANS_LINEAR), DEFVAL(Tween::EASE_IN)); + ClassDB::bind_method(D_METHOD("play_with_capture", "name", "duration", "custom_blend", "custom_speed", "from_end", "trans_type", "ease_type"), &AnimationPlayer::play_with_capture, DEFVAL(StringName()), DEFVAL(-1.0), DEFVAL(-1), DEFVAL(1.0), DEFVAL(false), DEFVAL(Tween::TRANS_LINEAR), DEFVAL(Tween::EASE_IN)); ClassDB::bind_method(D_METHOD("pause"), &AnimationPlayer::pause); ClassDB::bind_method(D_METHOD("stop", "keep_state"), &AnimationPlayer::stop, DEFVAL(false)); ClassDB::bind_method(D_METHOD("is_playing"), &AnimationPlayer::is_playing); @@ -855,6 +855,7 @@ void AnimationPlayer::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "current_animation_position", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "", "get_current_animation_position"); ADD_GROUP("Playback Options", "playback_"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "playback_auto_capture"), "set_auto_capture", "is_auto_capture"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "playback_default_blend_time", PROPERTY_HINT_RANGE, "0,4096,0.01,suffix:s"), "set_default_blend_time", "get_default_blend_time"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "speed_scale", PROPERTY_HINT_RANGE, "-4,4,0.001,or_less,or_greater"), "set_speed_scale", "get_speed_scale"); diff --git a/scene/animation/animation_player.h b/scene/animation/animation_player.h index 13e1e37ca9..3b229e7699 100644 --- a/scene/animation/animation_player.h +++ b/scene/animation/animation_player.h @@ -56,6 +56,7 @@ private: float speed_scale = 1.0; double default_blend_time = 0.0; + bool auto_capture = true; bool is_stopping = false; struct PlaybackData { @@ -108,6 +109,8 @@ private: bool reset_on_save = true; bool movie_quit_on_finish = false; + void _play(const StringName &p_name, double p_custom_blend = -1, float p_custom_scale = 1.0, bool p_from_end = false); + void _capture(const StringName &p_name, bool p_from_end = false, double p_duration = -1.0, Tween::TransitionType p_trans_type = Tween::TRANS_LINEAR, Tween::EaseType p_ease_type = Tween::EASE_IN); void _process_playback_data(PlaybackData &cd, double p_delta, float p_blend, bool p_seeked, bool p_started, bool p_is_current = false); void _blend_playback_data(double p_delta, bool p_started); void _stop_internal(bool p_reset, bool p_keep_state); @@ -158,9 +161,12 @@ public: void set_default_blend_time(double p_default); double get_default_blend_time() const; + void set_auto_capture(bool p_auto_capture); + bool is_auto_capture() const; + void play(const StringName &p_name = StringName(), double p_custom_blend = -1, float p_custom_scale = 1.0, bool p_from_end = false); void play_backwards(const StringName &p_name = StringName(), double p_custom_blend = -1); - void play_with_capture(const StringName &p_name, double p_duration = -1.0, double p_custom_blend = -1, float p_custom_scale = 1.0, bool p_from_end = false, Tween::TransitionType p_trans_type = Tween::TRANS_LINEAR, Tween::EaseType p_ease_type = Tween::EASE_IN); + void play_with_capture(const StringName &p_name = StringName(), double p_duration = -1.0, double p_custom_blend = -1, float p_custom_scale = 1.0, bool p_from_end = false, Tween::TransitionType p_trans_type = Tween::TRANS_LINEAR, Tween::EaseType p_ease_type = Tween::EASE_IN); void queue(const StringName &p_name); Vector<String> get_queue(); void clear_queue(); diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index 4f90504e35..8131fe7aaa 100644 --- a/scene/gui/code_edit.cpp +++ b/scene/gui/code_edit.cpp @@ -624,16 +624,31 @@ Control::CursorShape CodeEdit::get_cursor_shape(const Point2 &p_pos) const { return TextEdit::get_cursor_shape(p_pos); } +void CodeEdit::_unhide_carets() { + // Unfold caret and selection origin. + for (int i = 0; i < get_caret_count(); i++) { + if (_is_line_hidden(get_caret_line(i))) { + unfold_line(get_caret_line(i)); + } + if (has_selection(i) && _is_line_hidden(get_selection_origin_line(i))) { + unfold_line(get_selection_origin_line(i)); + } + } +} + /* Text manipulation */ // Overridable actions void CodeEdit::_handle_unicode_input_internal(const uint32_t p_unicode, int p_caret) { start_action(EditAction::ACTION_TYPING); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { if (p_caret != -1 && p_caret != i) { continue; } + if (p_caret == -1 && multicaret_edit_ignore_caret(i)) { + continue; + } bool had_selection = has_selection(i); String selection_text = (had_selection ? get_selected_text(i) : ""); @@ -691,6 +706,7 @@ void CodeEdit::_handle_unicode_input_internal(const uint32_t p_unicode, int p_ca insert_text_at_caret(chr, i); } } + end_multicaret_edit(); end_action(); } @@ -705,66 +721,80 @@ void CodeEdit::_backspace_internal(int p_caret) { } begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { if (p_caret != -1 && p_caret != i) { continue; } + if (p_caret == -1 && multicaret_edit_ignore_caret(i)) { + continue; + } - int cc = get_caret_column(i); - int cl = get_caret_line(i); + int to_line = get_caret_line(i); + int to_column = get_caret_column(i); - if (cc == 0 && cl == 0) { + if (to_column == 0 && to_line == 0) { continue; } - if (cl > 0 && _is_line_hidden(cl - 1)) { - unfold_line(get_caret_line(i) - 1); + if (to_line > 0 && _is_line_hidden(to_line - 1)) { + unfold_line(to_line - 1); } - int prev_line = cc ? cl : cl - 1; - int prev_column = cc ? (cc - 1) : (get_line(cl - 1).length()); + int from_line = to_column > 0 ? to_line : to_line - 1; + int from_column = to_column > 0 ? (to_column - 1) : (get_line(to_line - 1).length()); - merge_gutters(prev_line, cl); + merge_gutters(from_line, to_line); - if (auto_brace_completion_enabled && cc > 0) { - int idx = _get_auto_brace_pair_open_at_pos(cl, cc); + if (auto_brace_completion_enabled && to_column > 0) { + int idx = _get_auto_brace_pair_open_at_pos(to_line, to_column); if (idx != -1) { - prev_column = cc - auto_brace_completion_pairs[idx].open_key.length(); + from_column = to_column - auto_brace_completion_pairs[idx].open_key.length(); - if (_get_auto_brace_pair_close_at_pos(cl, cc) == idx) { - cc += auto_brace_completion_pairs[idx].close_key.length(); + if (_get_auto_brace_pair_close_at_pos(to_line, to_column) == idx) { + to_column += auto_brace_completion_pairs[idx].close_key.length(); } - - remove_text(prev_line, prev_column, cl, cc); - - set_caret_line(prev_line, false, true, 0, i); - set_caret_column(prev_column, i == 0, i); - - adjust_carets_after_edit(i, prev_line, prev_column, cl, cc); - continue; } } // For space indentation we need to do a basic unindent if there are no chars to the left, acting the same way as tabs. - if (indent_using_spaces && cc != 0) { - if (get_first_non_whitespace_column(cl) >= cc) { - prev_column = cc - _calculate_spaces_till_next_left_indent(cc); - prev_line = cl; + if (indent_using_spaces && to_column != 0) { + if (get_first_non_whitespace_column(to_line) >= to_column) { + from_column = to_column - _calculate_spaces_till_next_left_indent(to_column); + from_line = to_line; } } - remove_text(prev_line, prev_column, cl, cc); - - set_caret_line(prev_line, false, true, 0, i); - set_caret_column(prev_column, i == 0, i); + remove_text(from_line, from_column, to_line, to_column); - adjust_carets_after_edit(i, prev_line, prev_column, cl, cc); + set_caret_line(from_line, false, true, -1, i); + set_caret_column(from_column, i == 0, i); } - merge_overlapping_carets(); + + end_multicaret_edit(); end_complex_operation(); } +void CodeEdit::_cut_internal(int p_caret) { + // Overridden to unfold lines. + _copy_internal(p_caret); + + if (!is_editable()) { + return; + } + + if (has_selection(p_caret)) { + delete_selection(p_caret); + return; + } + if (p_caret == -1) { + delete_lines(); + } else { + unfold_line(get_caret_line(p_caret)); + remove_line_at(get_caret_line(p_caret)); + } +} + /* Indent management */ void CodeEdit::set_indent_size(const int p_size) { ERR_FAIL_COND_MSG(p_size <= 0, "Indend size must be greater than 0."); @@ -838,13 +868,17 @@ void CodeEdit::do_indent() { } begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { + if (multicaret_edit_ignore_caret(i)) { + continue; + } int spaces_to_add = _calculate_spaces_till_next_right_indent(get_caret_column(i)); if (spaces_to_add > 0) { insert_text_at_caret(String(" ").repeat(spaces_to_add), i); } } + end_multicaret_edit(); end_complex_operation(); } @@ -854,51 +888,28 @@ void CodeEdit::indent_lines() { } begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &c : caret_edit_order) { - // This value informs us by how much we changed selection position by indenting right. - // Default is 1 for tab indentation. - int selection_offset = 1; - - int start_line = get_caret_line(c); - int end_line = start_line; - if (has_selection(c)) { - start_line = get_selection_from_line(c); - end_line = get_selection_to_line(c); + begin_multicaret_edit(); - // Ignore the last line if the selection is not past the first column. - if (get_selection_to_column(c) == 0) { - selection_offset = 0; - end_line--; - } - } - - for (int i = start_line; i <= end_line; i++) { + Vector<Point2i> line_ranges = get_line_ranges_from_carets(); + for (Point2i line_range : line_ranges) { + for (int i = line_range.x; i <= line_range.y; i++) { const String line_text = get_line(i); - if (line_text.size() == 0 && has_selection(c)) { + if (line_text.size() == 0) { + // Ignore empty lines. continue; } - if (!indent_using_spaces) { - set_line(i, '\t' + line_text); - continue; + if (indent_using_spaces) { + int spaces_to_add = _calculate_spaces_till_next_right_indent(get_first_non_whitespace_column(i)); + insert_text(String(" ").repeat(spaces_to_add), i, 0, false); + } else { + insert_text("\t", i, 0, false); } - - // We don't really care where selection is - we just need to know indentation level at the beginning of the line. - // Since we will add this many spaces, we want to move the whole selection and caret by this much. - int spaces_to_add = _calculate_spaces_till_next_right_indent(get_first_non_whitespace_column(i)); - set_line(i, String(" ").repeat(spaces_to_add) + line_text); - selection_offset = spaces_to_add; } - - // Fix selection and caret being off after shifting selection right. - if (has_selection(c)) { - select(start_line, get_selection_from_column(c) + selection_offset, get_selection_to_line(c), get_selection_to_column(c) + selection_offset, c); - } - set_caret_column(get_caret_column(c) + selection_offset, false, c); } + + end_multicaret_edit(); end_complex_operation(); - queue_redraw(); } void CodeEdit::unindent_lines() { @@ -907,76 +918,25 @@ void CodeEdit::unindent_lines() { } begin_complex_operation(); + begin_multicaret_edit(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &c : caret_edit_order) { - // Moving caret and selection after unindenting can get tricky because - // changing content of line can move caret and selection on its own (if new line ends before previous position of either) - // therefore we just remember initial values and at the end of the operation offset them by number of removed characters. - int removed_characters = 0; - int initial_selection_end_column = 0; - int initial_cursor_column = get_caret_column(c); - - int start_line = get_caret_line(c); - int end_line = start_line; - if (has_selection(c)) { - start_line = get_selection_from_line(c); - end_line = get_selection_to_line(c); - - // Ignore the last line if the selection is not past the first column. - initial_selection_end_column = get_selection_to_column(c); - if (initial_selection_end_column == 0) { - end_line--; - } - } - - bool first_line_edited = false; - bool last_line_edited = false; - - for (int i = start_line; i <= end_line; i++) { - String line_text = get_line(i); + Vector<Point2i> line_ranges = get_line_ranges_from_carets(); + for (Point2i line_range : line_ranges) { + for (int i = line_range.x; i <= line_range.y; i++) { + const String line_text = get_line(i); if (line_text.begins_with("\t")) { - line_text = line_text.substr(1, line_text.length()); - - set_line(i, line_text); - removed_characters = 1; - - first_line_edited = (i == start_line) ? true : first_line_edited; - last_line_edited = (i == end_line) ? true : last_line_edited; - continue; - } - - if (line_text.begins_with(" ")) { - // When unindenting we aim to remove spaces before line that has selection no matter what is selected. - // Here we remove only enough spaces to align text to nearest full multiple of indentation_size. - // In case where selection begins at the start of indentation_size multiple we remove whole indentation level. + remove_text(i, 0, i, 1); + } else if (line_text.begins_with(" ")) { + // Remove only enough spaces to align text to nearest full multiple of indentation_size. int spaces_to_remove = _calculate_spaces_till_next_left_indent(get_first_non_whitespace_column(i)); - line_text = line_text.substr(spaces_to_remove, line_text.length()); - - set_line(i, line_text); - removed_characters = spaces_to_remove; - - first_line_edited = (i == start_line) ? true : first_line_edited; - last_line_edited = (i == end_line) ? true : last_line_edited; + remove_text(i, 0, i, spaces_to_remove); } } - - if (has_selection(c)) { - // Fix selection being off by one on the first line. - if (first_line_edited) { - select(get_selection_from_line(c), get_selection_from_column(c) - removed_characters, get_selection_to_line(c), initial_selection_end_column, c); - } - - // Fix selection being off by one on the last line. - if (last_line_edited) { - select(get_selection_from_line(c), get_selection_from_column(c), get_selection_to_line(c), initial_selection_end_column - removed_characters, c); - } - } - set_caret_column(initial_cursor_column - removed_characters, false, c); } + + end_multicaret_edit(); end_complex_operation(); - queue_redraw(); } void CodeEdit::convert_indent(int p_from_line, int p_to_line) { @@ -992,27 +952,6 @@ void CodeEdit::convert_indent(int p_from_line, int p_to_line) { ERR_FAIL_COND(p_to_line >= get_line_count()); ERR_FAIL_COND(p_to_line < p_from_line); - // Store caret states. - Vector<int> caret_columns; - Vector<Pair<int, int>> from_selections; - Vector<Pair<int, int>> to_selections; - caret_columns.resize(get_caret_count()); - from_selections.resize(get_caret_count()); - to_selections.resize(get_caret_count()); - for (int c = 0; c < get_caret_count(); c++) { - caret_columns.write[c] = get_caret_column(c); - - // Set "selection_from_line" to -1 to allow checking if there was a selection later. - if (!has_selection(c)) { - from_selections.write[c].first = -1; - continue; - } - from_selections.write[c].first = get_selection_from_line(c); - from_selections.write[c].second = get_selection_from_column(c); - to_selections.write[c].first = get_selection_to_line(c); - to_selections.write[c].second = get_selection_to_column(c); - } - // Check lines within range. const char32_t from_indent_char = indent_using_spaces ? '\t' : ' '; int size_diff = indent_using_spaces ? indent_size - 1 : -(indent_size - 1); @@ -1044,23 +983,10 @@ void CodeEdit::convert_indent(int p_from_line, int p_to_line) { line_changed = true; if (!changed_indentation) { begin_complex_operation(); + begin_multicaret_edit(); changed_indentation = true; } - // Calculate new caret state. - for (int c = 0; c < get_caret_count(); c++) { - if (get_caret_line(c) != i || caret_columns[c] <= j) { - continue; - } - caret_columns.write[c] += size_diff; - - if (from_selections.write[c].first == -1) { - continue; - } - from_selections.write[c].second = from_selections[c].first == i ? from_selections[c].second + size_diff : from_selections[c].second; - to_selections.write[c].second = to_selections[c].first == i ? to_selections[c].second + size_diff : to_selections[c].second; - } - // Calculate new line. line = line.left(j + ((size_diff < 0) ? size_diff : 0)) + indent_text + line.substr(j + 1); @@ -1069,6 +995,7 @@ void CodeEdit::convert_indent(int p_from_line, int p_to_line) { } if (line_changed) { + // Use set line to preserve carets visual position. set_line(i, line); } } @@ -1077,16 +1004,9 @@ void CodeEdit::convert_indent(int p_from_line, int p_to_line) { return; } - // Restore caret states. - for (int c = 0; c < get_caret_count(); c++) { - set_caret_column(caret_columns[c], c == 0, c); - if (from_selections.write[c].first != -1) { - select(from_selections.write[c].first, from_selections.write[c].second, to_selections.write[c].first, to_selections.write[c].second, c); - } - } merge_overlapping_carets(); + end_multicaret_edit(); end_complex_operation(); - queue_redraw(); } int CodeEdit::_calculate_spaces_till_next_left_indent(int p_column) const { @@ -1107,15 +1027,22 @@ void CodeEdit::_new_line(bool p_split_current_line, bool p_above) { } begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + + for (int i = 0; i < get_caret_count(); i++) { + if (multicaret_edit_ignore_caret(i)) { + continue; + } // When not splitting the line, we need to factor in indentation from the end of the current line. const int cc = p_split_current_line ? get_caret_column(i) : get_line(get_caret_line(i)).length(); const int cl = get_caret_line(i); const String line = get_line(cl); - String ins = "\n"; + String ins = ""; + if (!p_above) { + ins = "\n"; + } // Append current indentation. int space_count = 0; @@ -1138,6 +1065,9 @@ void CodeEdit::_new_line(bool p_split_current_line, bool p_above) { } break; } + if (p_above) { + ins += "\n"; + } if (is_line_folded(cl)) { unfold_line(cl); @@ -1183,33 +1113,22 @@ void CodeEdit::_new_line(bool p_split_current_line, bool p_above) { } } - bool first_line = false; - if (!p_split_current_line) { + if (p_split_current_line) { + insert_text_at_caret(ins, i); + } else { + insert_text(ins, cl, p_above ? 0 : get_line(cl).length(), p_above, p_above); deselect(i); - - if (p_above) { - if (cl > 0) { - set_caret_line(cl - 1, false, true, 0, i); - set_caret_column(get_line(get_caret_line(i)).length(), i == 0, i); - } else { - set_caret_column(0, i == 0, i); - first_line = true; - } - } else { - set_caret_column(line.length(), i == 0, i); - } + set_caret_line(p_above ? cl : cl + 1, false, true, -1, i); + set_caret_column(get_line(get_caret_line(i)).length(), i == 0, i); } - - insert_text_at_caret(ins, i); - - if (first_line) { - set_caret_line(0, i == 0, true, 0, i); - } else if (brace_indent) { + if (brace_indent) { + // Move to inner indented line. set_caret_line(get_caret_line(i) - 1, false, true, 0, i); set_caret_column(get_line(get_caret_line(i)).length(), i == 0, i); } } + end_multicaret_edit(); end_complex_operation(); } @@ -1700,27 +1619,8 @@ void CodeEdit::fold_line(int p_line) { _set_line_as_hidden(i, true); } - for (int i = 0; i < get_caret_count(); i++) { - // Fix selection. - if (has_selection(i)) { - if (_is_line_hidden(get_selection_from_line(i)) && _is_line_hidden(get_selection_to_line(i))) { - deselect(i); - } else if (_is_line_hidden(get_selection_from_line(i))) { - select(p_line, 9999, get_selection_to_line(i), get_selection_to_column(i), i); - } else if (_is_line_hidden(get_selection_to_line(i))) { - select(get_selection_from_line(i), get_selection_from_column(i), p_line, 9999, i); - } - } - - // Reset caret. - if (_is_line_hidden(get_caret_line(i))) { - set_caret_line(p_line, false, false, 0, i); - set_caret_column(get_line(p_line).length(), false, i); - } - } - - merge_overlapping_carets(); - queue_redraw(); + // Collapse any carets in the hidden area. + collapse_carets(p_line, get_line(p_line).length(), end_line, get_line(end_line).length(), true); } void CodeEdit::unfold_line(int p_line) { @@ -1769,6 +1669,23 @@ void CodeEdit::toggle_foldable_line(int p_line) { fold_line(p_line); } +void CodeEdit::toggle_foldable_lines_at_carets() { + begin_multicaret_edit(); + int previous_line = -1; + Vector<int> sorted = get_sorted_carets(); + for (int caret_idx : sorted) { + if (multicaret_edit_ignore_caret(caret_idx)) { + continue; + } + int line_idx = get_caret_line(caret_idx); + if (line_idx != previous_line) { + toggle_foldable_line(line_idx); + previous_line = line_idx; + } + } + end_multicaret_edit(); +} + bool CodeEdit::is_line_folded(int p_line) const { ERR_FAIL_INDEX_V(p_line, get_line_count(), false); return p_line + 1 < get_line_count() && !_is_line_hidden(p_line) && _is_line_hidden(p_line + 1); @@ -1795,49 +1712,29 @@ void CodeEdit::create_code_region() { WARN_PRINT_ONCE("Cannot create code region without any one line comment delimiters"); return; } + String region_name = atr(ETR("New Code Region")); + begin_complex_operation(); - // Merge selections if selection starts on the same line the previous one ends. - Vector<int> caret_edit_order = get_caret_index_edit_order(); - Vector<int> carets_to_remove; - for (int i = 1; i < caret_edit_order.size(); i++) { - int current_caret = caret_edit_order[i - 1]; - int next_caret = caret_edit_order[i]; - if (get_selection_from_line(current_caret) == get_selection_to_line(next_caret)) { - select(get_selection_from_line(next_caret), get_selection_from_column(next_caret), get_selection_to_line(current_caret), get_selection_to_column(current_caret), next_caret); - carets_to_remove.append(current_caret); - } - } - // Sort and remove backwards to preserve indices. - carets_to_remove.sort(); - for (int i = carets_to_remove.size() - 1; i >= 0; i--) { - remove_caret(carets_to_remove[i]); - } - - // Adding start and end region tags. - int first_region_start = -1; - for (int caret_idx : get_caret_index_edit_order()) { - if (!has_selection(caret_idx)) { - continue; - } - int from_line = get_selection_from_line(caret_idx); - if (first_region_start == -1 || from_line < first_region_start) { - first_region_start = from_line; - } - int to_line = get_selection_to_line(caret_idx); - set_line(to_line, get_line(to_line) + "\n" + code_region_end_string); - insert_line_at(from_line, code_region_start_string + " " + atr(ETR("New Code Region"))); - fold_line(from_line); + begin_multicaret_edit(); + Vector<Point2i> line_ranges = get_line_ranges_from_carets(true, false); + + // Add start and end region tags. + int line_offset = 0; + for (Point2i line_range : line_ranges) { + insert_text("\n" + code_region_end_string, line_range.y + line_offset, get_line(line_range.y + line_offset).length()); + insert_line_at(line_range.x + line_offset, code_region_start_string + " " + region_name); + fold_line(line_range.x + line_offset); + line_offset += 2; } + int first_region_start = line_ranges[0].x; // Select name of the first region to allow quick edit. remove_secondary_carets(); - set_caret_line(first_region_start); - int tag_length = code_region_start_string.length() + atr(ETR("New Code Region")).length() + 1; - set_caret_column(tag_length); + int tag_length = code_region_start_string.length() + region_name.length() + 1; select(first_region_start, code_region_start_string.length() + 1, first_region_start, tag_length); + end_multicaret_edit(); end_complex_operation(); - queue_redraw(); } String CodeEdit::get_code_region_start_tag() const { @@ -2236,8 +2133,12 @@ void CodeEdit::confirm_code_completion(bool p_replace) { char32_t caret_last_completion_char = 0; begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + + for (int i = 0; i < get_caret_count(); i++) { + if (multicaret_edit_ignore_caret(i)) { + continue; + } int caret_line = get_caret_line(i); const String &insert_text = code_completion_options[code_completion_current_selected].insert_text; @@ -2270,8 +2171,6 @@ void CodeEdit::confirm_code_completion(bool p_replace) { // Replace. remove_text(caret_line, get_caret_column(i) - code_completion_base.length(), caret_remove_line, caret_col); - adjust_carets_after_edit(i, caret_line, caret_col - code_completion_base.length(), caret_remove_line, caret_col); - set_caret_column(get_caret_column(i) - code_completion_base.length(), false, i); insert_text_at_caret(insert_text, i); } else { // Get first non-matching char. @@ -2287,8 +2186,6 @@ void CodeEdit::confirm_code_completion(bool p_replace) { // Remove base completion text. remove_text(caret_line, get_caret_column(i) - code_completion_base.length(), caret_line, get_caret_column(i)); - adjust_carets_after_edit(i, caret_line, get_caret_column(i) - code_completion_base.length(), caret_line, get_caret_column(i)); - set_caret_column(get_caret_column(i) - code_completion_base.length(), false, i); // Merge with text. insert_text_at_caret(insert_text.substr(0, code_completion_base.length()), i); @@ -2313,12 +2210,10 @@ void CodeEdit::confirm_code_completion(bool p_replace) { if (has_string_delimiter(String::chr(last_completion_char))) { if (post_brace_pair != -1 && last_char_matches) { remove_text(caret_line, get_caret_column(i), caret_line, get_caret_column(i) + 1); - adjust_carets_after_edit(i, caret_line, get_caret_column(i), caret_line, get_caret_column(i) + 1); } } else { if (pre_brace_pair != -1 && pre_brace_pair != post_brace_pair && last_char_matches) { remove_text(caret_line, get_caret_column(i), caret_line, get_caret_column(i) + 1); - adjust_carets_after_edit(i, caret_line, get_caret_column(i), caret_line, get_caret_column(i) + 1); } else if (auto_brace_completion_enabled && pre_brace_pair != -1) { insert_text_at_caret(auto_brace_completion_pairs[pre_brace_pair].close_key, i); set_caret_column(get_caret_column(i) - auto_brace_completion_pairs[pre_brace_pair].close_key.length(), i == 0, i); @@ -2329,13 +2224,16 @@ void CodeEdit::confirm_code_completion(bool p_replace) { pre_brace_pair = _get_auto_brace_pair_open_at_pos(caret_line, get_caret_column(i) + 1); if (pre_brace_pair != -1 && pre_brace_pair == _get_auto_brace_pair_close_at_pos(caret_line, get_caret_column(i) - 1)) { remove_text(caret_line, get_caret_column(i) - 2, caret_line, get_caret_column(i)); - adjust_carets_after_edit(i, caret_line, get_caret_column(i) - 2, caret_line, get_caret_column(i)); - if (_get_auto_brace_pair_close_at_pos(caret_line, get_caret_column(i) - 1) != pre_brace_pair) { - set_caret_column(get_caret_column(i) - 1, i == 0, i); + if (_get_auto_brace_pair_close_at_pos(caret_line, get_caret_column(i) + 1) != pre_brace_pair) { + set_caret_column(get_caret_column(i) + 1, i == 0, i); + } else { + set_caret_column(get_caret_column(i) + 2, i == 0, i); } } } } + + end_multicaret_edit(); end_complex_operation(); cancel_code_completion(); @@ -2418,65 +2316,154 @@ void CodeEdit::set_symbol_lookup_word_as_valid(bool p_valid) { } /* Text manipulation */ -void CodeEdit::duplicate_lines() { +void CodeEdit::move_lines_up() { begin_complex_operation(); + begin_multicaret_edit(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &caret_index : caret_edit_order) { - // The text that will be inserted. All lines in one string. - String insert_text; - - // The new line position of the caret after the operation. - int new_caret_line = get_caret_line(caret_index); - // The new column position of the caret after the operation. - int new_caret_column = get_caret_column(caret_index); - // The caret positions of the selection. Stays -1 if there is no selection. - int select_from_line = -1; - int select_to_line = -1; - int select_from_column = -1; - int select_to_column = -1; - // Number of lines of the selection. - int select_num_lines = -1; - - if (has_selection(caret_index)) { - select_from_line = get_selection_from_line(caret_index); - select_to_line = get_selection_to_line(caret_index); - select_from_column = get_selection_from_column(caret_index); - select_to_column = get_selection_to_column(caret_index); - select_num_lines = select_to_line - select_from_line + 1; - - for (int i = select_from_line; i <= select_to_line; i++) { - insert_text += "\n" + get_line(i); - unfold_line(i); - } - new_caret_line = select_to_line + select_num_lines; - } else { - insert_text = "\n" + get_line(new_caret_line); - new_caret_line++; + // Move lines up by swapping each line with the one above it. + Vector<Point2i> line_ranges = get_line_ranges_from_carets(); + for (Point2i line_range : line_ranges) { + if (line_range.x == 0) { + continue; + } + unfold_line(line_range.x - 1); + for (int line = line_range.x; line <= line_range.y; line++) { + unfold_line(line); + swap_lines(line - 1, line); + } + } - unfold_line(get_caret_line(caret_index)); + // Fix selection if it ended at column 0, since it wasn't moved. + for (int i = 0; i < get_caret_count(); i++) { + if (has_selection(i) && get_selection_to_column(i) == 0 && get_selection_to_line(i) != 0) { + if (is_caret_after_selection_origin(i)) { + set_caret_line(get_caret_line(i) - 1, false, true, -1, i); + } else { + set_selection_origin_line(get_selection_origin_line(i) - 1, true, -1, i); + } } + } - // The text will be inserted at the end of the current line. - set_caret_column(get_line(get_caret_line(caret_index)).length(), false, caret_index); + end_multicaret_edit(); + end_complex_operation(); +} - deselect(caret_index); +void CodeEdit::move_lines_down() { + begin_complex_operation(); + begin_multicaret_edit(); - insert_text_at_caret(insert_text, caret_index); - set_caret_line(new_caret_line, false, true, 0, caret_index); - set_caret_column(new_caret_column, true, caret_index); + Vector<Point2i> line_ranges = get_line_ranges_from_carets(); - if (select_from_line != -1) { - // Advance the selection by the number of duplicated lines. - select_from_line += select_num_lines; - select_to_line += select_num_lines; + // Fix selection if it ended at column 0, since it won't be moved. + for (int i = 0; i < get_caret_count(); i++) { + if (has_selection(i) && get_selection_to_column(i) == 0 && get_selection_to_line(i) != get_line_count() - 1) { + if (is_caret_after_selection_origin(i)) { + set_caret_line(get_caret_line(i) + 1, false, true, -1, i); + } else { + set_selection_origin_line(get_selection_origin_line(i) + 1, true, -1, i); + } + } + } - select(select_from_line, select_from_column, select_to_line, select_to_column, caret_index); + // Move lines down by swapping each line with the one below it. + for (Point2i line_range : line_ranges) { + if (line_range.y == get_line_count() - 1) { + continue; + } + unfold_line(line_range.y + 1); + for (int line = line_range.y; line >= line_range.x; line--) { + unfold_line(line); + swap_lines(line + 1, line); } } + end_multicaret_edit(); + end_complex_operation(); +} + +void CodeEdit::delete_lines() { + begin_complex_operation(); + begin_multicaret_edit(); + + Vector<Point2i> line_ranges = get_line_ranges_from_carets(); + int line_offset = 0; + for (Point2i line_range : line_ranges) { + // Remove last line of range separately to preserve carets. + unfold_line(line_range.y + line_offset); + remove_line_at(line_range.y + line_offset); + if (line_range.x != line_range.y) { + remove_text(line_range.x + line_offset, 0, line_range.y + line_offset, 0); + } + line_offset += line_range.x - line_range.y - 1; + } + + // Deselect all. + deselect(); + + end_multicaret_edit(); + end_complex_operation(); +} + +void CodeEdit::duplicate_selection() { + begin_complex_operation(); + begin_multicaret_edit(); + + // Duplicate lines from carets without selections first. + for (int i = 0; i < get_caret_count(); i++) { + if (multicaret_edit_ignore_caret(i)) { + continue; + } + for (int l = get_selection_from_line(i); l <= get_selection_to_line(i); l++) { + unfold_line(l); + } + if (has_selection(i)) { + continue; + } + + String text_to_insert = get_line(get_caret_line(i)) + "\n"; + // Insert new text before the line, so the caret is on the second one. + insert_text(text_to_insert, get_caret_line(i), 0); + } + + // Duplicate selections. + for (int i = 0; i < get_caret_count(); i++) { + if (multicaret_edit_ignore_caret(i)) { + continue; + } + if (!has_selection(i)) { + continue; + } + + // Insert new text before the selection, so the caret is on the second one. + insert_text(get_selected_text(i), get_selection_from_line(i), get_selection_from_column(i)); + } + + end_multicaret_edit(); + end_complex_operation(); +} + +void CodeEdit::duplicate_lines() { + begin_complex_operation(); + begin_multicaret_edit(); + + Vector<Point2i> line_ranges = get_line_ranges_from_carets(false, false); + int line_offset = 0; + for (Point2i line_range : line_ranges) { + // The text that will be inserted. All lines in one string. + String text_to_insert; + + for (int i = line_range.x + line_offset; i <= line_range.y + line_offset; i++) { + text_to_insert += get_line(i) + "\n"; + unfold_line(i); + } + + // Insert new text before the line. + insert_text(text_to_insert, line_range.x + line_offset, 0); + line_offset += line_range.y - line_range.x + 1; + } + + end_multicaret_edit(); end_complex_operation(); - queue_redraw(); } /* Visual */ @@ -2578,6 +2565,7 @@ void CodeEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("fold_all_lines"), &CodeEdit::fold_all_lines); ClassDB::bind_method(D_METHOD("unfold_all_lines"), &CodeEdit::unfold_all_lines); ClassDB::bind_method(D_METHOD("toggle_foldable_line", "line"), &CodeEdit::toggle_foldable_line); + ClassDB::bind_method(D_METHOD("toggle_foldable_lines_at_carets"), &CodeEdit::toggle_foldable_lines_at_carets); ClassDB::bind_method(D_METHOD("is_line_folded", "line"), &CodeEdit::is_line_folded); ClassDB::bind_method(D_METHOD("get_folded_lines"), &CodeEdit::get_folded_lines); @@ -2679,6 +2667,10 @@ void CodeEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_symbol_lookup_word_as_valid", "valid"), &CodeEdit::set_symbol_lookup_word_as_valid); /* Text manipulation */ + ClassDB::bind_method(D_METHOD("move_lines_up"), &CodeEdit::move_lines_up); + ClassDB::bind_method(D_METHOD("move_lines_down"), &CodeEdit::move_lines_down); + ClassDB::bind_method(D_METHOD("delete_lines"), &CodeEdit::delete_lines); + ClassDB::bind_method(D_METHOD("duplicate_selection"), &CodeEdit::duplicate_selection); ClassDB::bind_method(D_METHOD("duplicate_lines"), &CodeEdit::duplicate_lines); /* Inspector */ @@ -2846,10 +2838,12 @@ void CodeEdit::_gutter_clicked(int p_line, int p_gutter) { if (p_gutter == line_number_gutter) { remove_secondary_carets(); - set_selection_mode(TextEdit::SelectionMode::SELECTION_MODE_LINE, p_line, 0); - select(p_line, 0, p_line + 1, 0); - set_caret_line(p_line + 1); - set_caret_column(0); + set_selection_mode(TextEdit::SelectionMode::SELECTION_MODE_LINE); + if (p_line == get_line_count() - 1) { + select(p_line, 0, p_line, INT_MAX); + } else { + select(p_line, 0, p_line + 1, 0); + } return; } diff --git a/scene/gui/code_edit.h b/scene/gui/code_edit.h index 1770d4f4d8..56f8cce548 100644 --- a/scene/gui/code_edit.h +++ b/scene/gui/code_edit.h @@ -309,11 +309,14 @@ protected: static void _bind_compatibility_methods(); #endif + virtual void _unhide_carets() override; + /* Text manipulation */ // Overridable actions virtual void _handle_unicode_input_internal(const uint32_t p_unicode, int p_caret) override; virtual void _backspace_internal(int p_caret) override; + virtual void _cut_internal(int p_caret) override; GDVIRTUAL1(_confirm_code_completion, bool) GDVIRTUAL1(_request_code_completion, bool) @@ -409,6 +412,7 @@ public: void fold_all_lines(); void unfold_all_lines(); void toggle_foldable_line(int p_line); + void toggle_foldable_lines_at_carets(); bool is_line_folded(int p_line) const; TypedArray<int> get_folded_lines() const; @@ -489,6 +493,10 @@ public: void set_symbol_lookup_word_as_valid(bool p_valid); /* Text manipulation */ + void move_lines_up(); + void move_lines_down(); + void delete_lines(); + void duplicate_selection(); void duplicate_lines(); CodeEdit(); diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp index d430fe9bfc..7ac7ceb6bc 100644 --- a/scene/gui/control.cpp +++ b/scene/gui/control.cpp @@ -142,8 +142,8 @@ Size2 Control::_edit_get_scale() const { void Control::_edit_set_rect(const Rect2 &p_edit_rect) { ERR_FAIL_COND_MSG(!Engine::get_singleton()->is_editor_hint(), "This function can only be used from editor plugins."); - set_position((get_position() + get_transform().basis_xform(p_edit_rect.position)).snapped(Vector2(1, 1)), ControlEditorToolbar::get_singleton()->is_anchors_mode_enabled()); - set_size(p_edit_rect.size.snapped(Vector2(1, 1)), ControlEditorToolbar::get_singleton()->is_anchors_mode_enabled()); + set_position((get_position() + get_transform().basis_xform(p_edit_rect.position)).snappedf(1), ControlEditorToolbar::get_singleton()->is_anchors_mode_enabled()); + set_size(p_edit_rect.size.snappedf(1), ControlEditorToolbar::get_singleton()->is_anchors_mode_enabled()); } Rect2 Control::_edit_get_rect() const { diff --git a/scene/gui/graph_edit.cpp b/scene/gui/graph_edit.cpp index ef9c7e35ed..646e45b27a 100644 --- a/scene/gui/graph_edit.cpp +++ b/scene/gui/graph_edit.cpp @@ -501,7 +501,7 @@ void GraphEdit::_graph_element_resize_request(const Vector2 &p_new_minsize, Node // Snap the new size to the grid if snapping is enabled. Vector2 new_size = p_new_minsize; if (snapping_enabled ^ Input::get_singleton()->is_key_pressed(Key::CTRL)) { - new_size = new_size.snapped(Vector2(snapping_distance, snapping_distance)); + new_size = new_size.snappedf(snapping_distance); } // Disallow resizing the frame to a size smaller than the minimum size of the attached nodes. @@ -851,7 +851,7 @@ void GraphEdit::_set_position_of_frame_attached_nodes(GraphFrame *p_frame, const Vector2 pos = (attached_node->get_drag_from() * zoom + drag_accum) / zoom; if (snapping_enabled ^ Input::get_singleton()->is_key_pressed(Key::CTRL)) { - pos = pos.snapped(Vector2(snapping_distance, snapping_distance)); + pos = pos.snappedf(snapping_distance); } // Recursively move graph frames. @@ -1678,7 +1678,7 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) { // Snapping can be toggled temporarily by holding down Ctrl. // This is done here as to not toggle the grid when holding down Ctrl. if (snapping_enabled ^ Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) { - pos = pos.snapped(Vector2(snapping_distance, snapping_distance)); + pos = pos.snappedf(snapping_distance); } graph_element->set_position_offset(pos); diff --git a/scene/gui/graph_edit_arranger.cpp b/scene/gui/graph_edit_arranger.cpp index 49998beb42..fa1059c667 100644 --- a/scene/gui/graph_edit_arranger.cpp +++ b/scene/gui/graph_edit_arranger.cpp @@ -180,7 +180,7 @@ void GraphEditArranger::arrange_nodes() { if (graph_edit->is_snapping_enabled()) { float snapping_distance = graph_edit->get_snapping_distance(); - pos = pos.snapped(Vector2(snapping_distance, snapping_distance)); + pos = pos.snappedf(snapping_distance); } graph_node->set_position_offset(pos); graph_node->set_drag(false); diff --git a/scene/gui/progress_bar.cpp b/scene/gui/progress_bar.cpp index b2617e6fc7..90ce01e383 100644 --- a/scene/gui/progress_bar.cpp +++ b/scene/gui/progress_bar.cpp @@ -41,7 +41,7 @@ Size2 ProgressBar::get_minimum_size() const { TextLine tl = TextLine(txt, theme_cache.font, theme_cache.font_size); minimum_size.height = MAX(minimum_size.height, theme_cache.background_style->get_minimum_size().height + tl.get_size().y); } else { // this is needed, else the progressbar will collapse - minimum_size = minimum_size.max(Size2(1, 1)); + minimum_size = minimum_size.maxf(1); } return minimum_size; } diff --git a/scene/gui/rich_text_label.compat.inc b/scene/gui/rich_text_label.compat.inc index 626278a405..97739c4b79 100644 --- a/scene/gui/rich_text_label.compat.inc +++ b/scene/gui/rich_text_label.compat.inc @@ -38,9 +38,14 @@ void RichTextLabel::_add_image_bind_compat_80410(const Ref<Texture2D> &p_image, add_image(p_image, p_width, p_height, p_color, p_alignment, p_region, Variant(), false, String(), false); } +bool RichTextLabel::_remove_paragraph_bind_compat_91098(int p_paragraph) { + return remove_paragraph(p_paragraph, false); +} + void RichTextLabel::_bind_compatibility_methods() { ClassDB::bind_compatibility_method(D_METHOD("push_meta", "data"), &RichTextLabel::_push_meta_bind_compat_89024); ClassDB::bind_compatibility_method(D_METHOD("add_image", "image", "width", "height", "color", "inline_align", "region"), &RichTextLabel::_add_image_bind_compat_80410, DEFVAL(0), DEFVAL(0), DEFVAL(Color(1.0, 1.0, 1.0)), DEFVAL(INLINE_ALIGNMENT_CENTER), DEFVAL(Rect2())); + ClassDB::bind_compatibility_method(D_METHOD("remove_paragraph", "paragraph"), &RichTextLabel::_remove_paragraph_bind_compat_91098); } #endif // DISABLE_DEPRECATED diff --git a/scene/gui/rich_text_label.cpp b/scene/gui/rich_text_label.cpp index 0773181239..19b02f33c6 100644 --- a/scene/gui/rich_text_label.cpp +++ b/scene/gui/rich_text_label.cpp @@ -3342,7 +3342,7 @@ void RichTextLabel::_remove_frame(HashSet<Item *> &r_erase_list, ItemFrame *p_fr } } -bool RichTextLabel::remove_paragraph(const int p_paragraph) { +bool RichTextLabel::remove_paragraph(int p_paragraph, bool p_no_invalidate) { _stop_thread(); MutexLock data_lock(data_mutex); @@ -3391,8 +3391,44 @@ bool RichTextLabel::remove_paragraph(const int p_paragraph) { selection.click_frame = nullptr; selection.click_item = nullptr; - deselect(); + selection.active = false; + + if (p_no_invalidate) { + // Do not invalidate cache, only update vertical offsets of the paragraphs after deleted one and scrollbar. + int to_line = main->first_invalid_line.load() - 1; + float total_height = (p_paragraph == 0) ? 0 : _calculate_line_vertical_offset(main->lines[p_paragraph - 1]); + for (int i = p_paragraph; i < to_line; i++) { + MutexLock lock(main->lines[to_line - 1].text_buf->get_mutex()); + main->lines[i].offset.y = total_height; + total_height = _calculate_line_vertical_offset(main->lines[i]); + } + updating_scroll = true; + vscroll->set_max(total_height); + updating_scroll = false; + + main->first_invalid_line.store(MAX(main->first_invalid_line.load() - 1, 0)); + main->first_resized_line.store(MAX(main->first_resized_line.load() - 1, 0)); + main->first_invalid_font_line.store(MAX(main->first_invalid_font_line.load() - 1, 0)); + } else { + // Invalidate cache after the deleted paragraph. + main->first_invalid_line.store(MIN(main->first_invalid_line.load(), p_paragraph)); + main->first_resized_line.store(MIN(main->first_resized_line.load(), p_paragraph)); + main->first_invalid_font_line.store(MIN(main->first_invalid_font_line.load(), p_paragraph)); + } + queue_redraw(); + + return true; +} + +bool RichTextLabel::invalidate_paragraph(int p_paragraph) { + _stop_thread(); + MutexLock data_lock(data_mutex); + + if (p_paragraph >= (int)main->lines.size() || p_paragraph < 0) { + return false; + } + // Invalidate cache. main->first_invalid_line.store(MIN(main->first_invalid_line.load(), p_paragraph)); main->first_resized_line.store(MIN(main->first_resized_line.load(), p_paragraph)); main->first_invalid_font_line.store(MIN(main->first_invalid_font_line.load(), p_paragraph)); @@ -5851,7 +5887,8 @@ void RichTextLabel::_bind_methods() { ClassDB::bind_method(D_METHOD("add_image", "image", "width", "height", "color", "inline_align", "region", "key", "pad", "tooltip", "size_in_percent"), &RichTextLabel::add_image, DEFVAL(0), DEFVAL(0), DEFVAL(Color(1.0, 1.0, 1.0)), DEFVAL(INLINE_ALIGNMENT_CENTER), DEFVAL(Rect2()), DEFVAL(Variant()), DEFVAL(false), DEFVAL(String()), DEFVAL(false)); ClassDB::bind_method(D_METHOD("update_image", "key", "mask", "image", "width", "height", "color", "inline_align", "region", "pad", "tooltip", "size_in_percent"), &RichTextLabel::update_image, DEFVAL(0), DEFVAL(0), DEFVAL(Color(1.0, 1.0, 1.0)), DEFVAL(INLINE_ALIGNMENT_CENTER), DEFVAL(Rect2()), DEFVAL(false), DEFVAL(String()), DEFVAL(false)); ClassDB::bind_method(D_METHOD("newline"), &RichTextLabel::add_newline); - ClassDB::bind_method(D_METHOD("remove_paragraph", "paragraph"), &RichTextLabel::remove_paragraph); + ClassDB::bind_method(D_METHOD("remove_paragraph", "paragraph", "no_invalidate"), &RichTextLabel::remove_paragraph, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("invalidate_paragraph", "paragraph"), &RichTextLabel::invalidate_paragraph); ClassDB::bind_method(D_METHOD("push_font", "font", "font_size"), &RichTextLabel::push_font, DEFVAL(0)); ClassDB::bind_method(D_METHOD("push_font_size", "font_size"), &RichTextLabel::push_font_size); ClassDB::bind_method(D_METHOD("push_normal"), &RichTextLabel::push_normal); diff --git a/scene/gui/rich_text_label.h b/scene/gui/rich_text_label.h index 371f6724d7..189ee1da6e 100644 --- a/scene/gui/rich_text_label.h +++ b/scene/gui/rich_text_label.h @@ -134,6 +134,7 @@ protected: #ifndef DISABLE_DEPRECATED void _push_meta_bind_compat_89024(const Variant &p_meta); void _add_image_bind_compat_80410(const Ref<Texture2D> &p_image, const int p_width, const int p_height, const Color &p_color, InlineAlignment p_alignment, const Rect2 &p_region); + bool _remove_paragraph_bind_compat_91098(int p_paragraph); static void _bind_compatibility_methods(); #endif @@ -664,7 +665,8 @@ public: void add_image(const Ref<Texture2D> &p_image, int p_width = 0, int p_height = 0, const Color &p_color = Color(1.0, 1.0, 1.0), InlineAlignment p_alignment = INLINE_ALIGNMENT_CENTER, const Rect2 &p_region = Rect2(), const Variant &p_key = Variant(), bool p_pad = false, const String &p_tooltip = String(), bool p_size_in_percent = false); void update_image(const Variant &p_key, BitField<ImageUpdateMask> p_mask, const Ref<Texture2D> &p_image, int p_width = 0, int p_height = 0, const Color &p_color = Color(1.0, 1.0, 1.0), InlineAlignment p_alignment = INLINE_ALIGNMENT_CENTER, const Rect2 &p_region = Rect2(), bool p_pad = false, const String &p_tooltip = String(), bool p_size_in_percent = false); void add_newline(); - bool remove_paragraph(const int p_paragraph); + bool remove_paragraph(int p_paragraph, bool p_no_invalidate = false); + bool invalidate_paragraph(int p_paragraph); void push_dropcap(const String &p_string, const Ref<Font> &p_font, int p_size, const Rect2 &p_dropcap_margins = Rect2(), const Color &p_color = Color(1, 1, 1), int p_ol_size = 0, const Color &p_ol_color = Color(0, 0, 0, 0)); void _push_def_font(DefaultFont p_def_font); void _push_def_font_var(DefaultFont p_def_font, const Ref<Font> &p_font, int p_size = -1); diff --git a/scene/gui/tab_container.cpp b/scene/gui/tab_container.cpp index e2feb59a8c..dc53cf82e6 100644 --- a/scene/gui/tab_container.cpp +++ b/scene/gui/tab_container.cpp @@ -147,9 +147,7 @@ void TabContainer::_notification(int p_what) { if (get_tab_count() > 0) { _refresh_tab_names(); } - } break; - case NOTIFICATION_POST_ENTER_TREE: { if (setup_current_tab >= -1) { set_current_tab(setup_current_tab); setup_current_tab = -2; @@ -191,6 +189,25 @@ void TabContainer::_notification(int p_what) { } } break; + case NOTIFICATION_VISIBILITY_CHANGED: { + if (!is_visible() || setup_current_tab > -2) { + return; + } + + updating_visibility = true; + + // As the visibility change notification will be triggered for all children soon after, + // beat it to the punch and make sure that the correct node is the only one visible first. + // Otherwise, it can prevent a tab change done right before this container was made visible. + Vector<Control *> controls = _get_tab_controls(); + int current = get_current_tab(); + for (int i = 0; i < controls.size(); i++) { + controls[i]->set_visible(i == current); + } + + updating_visibility = false; + } break; + case NOTIFICATION_TRANSLATION_CHANGED: case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: case NOTIFICATION_THEME_CHANGED: { @@ -611,6 +628,7 @@ void TabContainer::set_current_tab(int p_current) { setup_current_tab = p_current; return; } + tab_bar->set_current_tab(p_current); } diff --git a/scene/gui/text_edit.compat.inc b/scene/gui/text_edit.compat.inc new file mode 100644 index 0000000000..bf73229868 --- /dev/null +++ b/scene/gui/text_edit.compat.inc @@ -0,0 +1,41 @@ +/**************************************************************************/ +/* text_edit.compat.inc */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef DISABLE_DEPRECATED + +void TextEdit::_set_selection_mode_compat_86978(SelectionMode p_mode, int p_line, int p_column, int p_caret) { + set_selection_mode(p_mode); +} + +void TextEdit::_bind_compatibility_methods() { + ClassDB::bind_compatibility_method(D_METHOD("set_selection_mode", "mode", "line", "column", "caret_index"), &TextEdit::_set_selection_mode_compat_86978, DEFVAL(-1), DEFVAL(-1), DEFVAL(0)); +} + +#endif diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 38b4ffc8ae..4fda49a877 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -29,6 +29,7 @@ /**************************************************************************/ #include "text_edit.h" +#include "text_edit.compat.inc" #include "core/config/project_settings.h" #include "core/input/input.h" @@ -451,7 +452,7 @@ void TextEdit::_notification(int p_what) { callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); } if (text_changed_dirty) { - callable_mp(this, &TextEdit::_text_changed_emit).call_deferred(); + callable_mp(this, &TextEdit::_emit_text_changed).call_deferred(); } _update_wrap_at_column(true); } break; @@ -565,9 +566,9 @@ void TextEdit::_notification(int p_what) { Vector<BraceMatchingData> brace_matching; if (highlight_matching_braces_enabled) { - brace_matching.resize(carets.size()); + brace_matching.resize(get_caret_count()); - for (int caret = 0; caret < carets.size(); caret++) { + for (int caret = 0; caret < get_caret_count(); caret++) { if (get_caret_line(caret) < 0 || get_caret_line(caret) >= text.size() || get_caret_column(caret) < 0) { continue; } @@ -1104,7 +1105,7 @@ void TextEdit::_notification(int p_what) { // Draw selections. float char_w = theme_cache.font->get_char_size(' ', theme_cache.font_size).width; - for (int c = 0; c < carets.size(); c++) { + for (int c = 0; c < get_caret_count(); c++) { if (!clipped && has_selection(c) && line >= get_selection_from_line(c) && line <= get_selection_to_line(c)) { int sel_from = (line > get_selection_from_line(c)) ? TS->shaped_text_get_range(rid).x : get_selection_from_column(c); int sel_to = (line < get_selection_to_line(c)) ? TS->shaped_text_get_range(rid).y : get_selection_to_column(c); @@ -1257,7 +1258,7 @@ void TextEdit::_notification(int p_what) { } Color gl_color = current_color; - for (int c = 0; c < carets.size(); c++) { + for (int c = 0; c < get_caret_count(); c++) { if (has_selection(c) && line >= get_selection_from_line(c) && line <= get_selection_to_line(c)) { // Selection int sel_from = (line > get_selection_from_line(c)) ? TS->shaped_text_get_range(rid).x : get_selection_from_column(c); int sel_to = (line < get_selection_to_line(c)) ? TS->shaped_text_get_range(rid).y : get_selection_to_column(c); @@ -1271,7 +1272,7 @@ void TextEdit::_notification(int p_what) { float char_pos = char_ofs + char_margin + ofs_x; if (char_pos >= xmargin_beg) { if (highlight_matching_braces_enabled) { - for (int c = 0; c < carets.size(); c++) { + for (int c = 0; c < get_caret_count(); c++) { if ((brace_matching[c].open_match_line == line && brace_matching[c].open_match_column == glyphs[j].start) || (get_caret_column(c) == glyphs[j].start && get_caret_line(c) == line && carets_wrap_index[c] == line_wrap_index && (brace_matching[c].open_matching || brace_matching[c].open_mismatch))) { if (brace_matching[c].open_mismatch) { @@ -1562,10 +1563,15 @@ void TextEdit::_notification(int p_what) { case MainLoop::NOTIFICATION_OS_IME_UPDATE: { if (has_focus()) { + bool had_ime_text = has_ime_text(); ime_text = DisplayServer::get_singleton()->ime_get_text(); ime_selection = DisplayServer::get_singleton()->ime_get_selection(); - if (!ime_text.is_empty() && has_selection()) { + if (!had_ime_text && has_ime_text()) { + _cancel_drag_and_drop_text(); + } + + if (has_ime_text() && has_selection()) { delete_selection(); } @@ -1576,7 +1582,7 @@ void TextEdit::_notification(int p_what) { } break; case NOTIFICATION_DRAG_BEGIN: { - selecting_mode = SelectionMode::SELECTION_MODE_NONE; + set_selection_mode(SelectionMode::SELECTION_MODE_NONE); drag_action = true; dragging_minimap = false; dragging_selection = false; @@ -1587,19 +1593,31 @@ void TextEdit::_notification(int p_what) { case NOTIFICATION_DRAG_END: { if (is_drag_successful()) { if (selection_drag_attempt) { - selection_drag_attempt = false; + // Dropped elsewhere. if (is_editable() && !Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) { delete_selection(); } else if (deselect_on_focus_loss_enabled) { deselect(); } } - } else { - selection_drag_attempt = false; } + if (drag_caret_index >= 0) { + if (drag_caret_index < carets.size()) { + remove_caret(drag_caret_index); + } + drag_caret_index = -1; + } + selection_drag_attempt = false; drag_action = false; drag_caret_force_displayed = false; } break; + + case NOTIFICATION_MOUSE_EXIT_SELF: { + if (drag_caret_force_displayed) { + drag_caret_force_displayed = false; + queue_redraw(); + } + } break; } } @@ -1702,15 +1720,17 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { if (mb->get_button_index() == MouseButton::WHEEL_RIGHT) { h_scroll->set_value(h_scroll->get_value() + (100 * mb->get_factor())); } + if (mb->get_button_index() == MouseButton::LEFT) { _reset_caret_blink_timer(); apply_ime(); Point2i pos = get_line_column_at_pos(mpos); - int row = pos.y; + int line = pos.y; int col = pos.x; + // Gutters. int left_margin = theme_cache.style_normal->get_margin(SIDE_LEFT); for (int i = 0; i < gutters.size(); i++) { if (!gutters[i].draw || gutters[i].width <= 0) { @@ -1718,14 +1738,14 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { } if (mpos.x >= left_margin && mpos.x <= left_margin + gutters[i].width) { - emit_signal(SNAME("gutter_clicked"), row, i); + emit_signal(SNAME("gutter_clicked"), line, i); return; } left_margin += gutters[i].width; } - // Minimap + // Minimap. if (draw_minimap) { _update_minimap_click(); if (dragging_minimap) { @@ -1733,121 +1753,86 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { } } + // Update caret. + int caret = carets.size() - 1; int prev_col = get_caret_column(caret); int prev_line = get_caret_line(caret); + int mouse_over_selection_caret = get_selection_at_line_column(line, col, true); + const int triple_click_timeout = 600; const int triple_click_tolerance = 5; bool is_triple_click = (!mb->is_double_click() && (OS::get_singleton()->get_ticks_msec() - last_dblclk) < triple_click_timeout && mb->get_position().distance_to(last_dblclk_pos) < triple_click_tolerance); if (!mb->is_double_click() && !is_triple_click) { if (mb->is_alt_pressed()) { - prev_line = row; + prev_line = line; prev_col = col; // Remove caret at clicked location. - if (carets.size() > 1) { - for (int i = 0; i < carets.size(); i++) { - // Deselect if clicked on caret or its selection. - if ((get_caret_column(i) == col && get_caret_line(i) == row) || is_mouse_over_selection(true, i)) { - remove_caret(i); - last_dblclk = 0; - return; - } + if (get_caret_count() > 1) { + // Deselect if clicked on caret or its selection. + int clicked_caret = get_selection_at_line_column(line, col, true, false); + if (clicked_caret != -1) { + remove_caret(clicked_caret); + last_dblclk = 0; + return; } } - if (is_mouse_over_selection()) { + if (mouse_over_selection_caret >= 0) { + // Did not remove selection under mouse, don't add a new caret. return; } - caret = add_caret(row, col); + // Create new caret at clicked location. + caret = add_caret(line, col); if (caret == -1) { return; } - carets.write[caret].selection.selecting_line = row; - carets.write[caret].selection.selecting_column = col; - last_dblclk = 0; - } else if (!mb->is_shift_pressed() && !is_mouse_over_selection()) { - caret = 0; - remove_secondary_carets(); - } - } - - _push_current_op(); - set_caret_line(row, false, true, 0, caret); - set_caret_column(col, false, caret); - selection_drag_attempt = false; - - if (selecting_enabled && mb->is_shift_pressed() && (get_caret_column(caret) != prev_col || get_caret_line(caret) != prev_line)) { - if (!has_selection(caret)) { - carets.write[caret].selection.active = true; - selecting_mode = SelectionMode::SELECTION_MODE_POINTER; - carets.write[caret].selection.from_column = prev_col; - carets.write[caret].selection.from_line = prev_line; - carets.write[caret].selection.to_column = carets[caret].column; - carets.write[caret].selection.to_line = carets[caret].line; - - if (get_selection_from_line(caret) > get_selection_to_line(caret) || (get_selection_from_line(caret) == get_selection_to_line(caret) && get_selection_from_column(caret) > get_selection_to_column(caret))) { - SWAP(carets.write[caret].selection.from_column, carets.write[caret].selection.to_column); - SWAP(carets.write[caret].selection.from_line, carets.write[caret].selection.to_line); - carets.write[caret].selection.shiftclick_left = false; - } else { - carets.write[caret].selection.shiftclick_left = true; - } - carets.write[caret].selection.selecting_line = prev_line; - carets.write[caret].selection.selecting_column = prev_col; - caret_index_edit_dirty = true; - merge_overlapping_carets(); - queue_redraw(); - } else { - if (carets[caret].line < get_selection_line(caret) || (carets[caret].line == get_selection_line(caret) && carets[caret].column < get_selection_column(caret))) { - if (carets[caret].selection.shiftclick_left) { - carets.write[caret].selection.shiftclick_left = !carets[caret].selection.shiftclick_left; - } - carets.write[caret].selection.from_column = carets[caret].column; - carets.write[caret].selection.from_line = carets[caret].line; - - } else if (carets[caret].line > get_selection_line(caret) || (carets[caret].line == get_selection_line(caret) && carets[caret].column > get_selection_column(caret))) { - if (!carets[caret].selection.shiftclick_left) { - SWAP(carets.write[caret].selection.from_column, carets.write[caret].selection.to_column); - SWAP(carets.write[caret].selection.from_line, carets.write[caret].selection.to_line); - carets.write[caret].selection.shiftclick_left = !carets[caret].selection.shiftclick_left; - } - carets.write[caret].selection.to_column = carets[caret].column; - carets.write[caret].selection.to_line = carets[caret].line; - + } else if (!mb->is_shift_pressed()) { + if (drag_and_drop_selection_enabled && mouse_over_selection_caret >= 0) { + // Try to drag and drop. + set_selection_mode(SelectionMode::SELECTION_MODE_NONE); + selection_drag_attempt = true; + drag_and_drop_origin_caret_index = mouse_over_selection_caret; + last_dblclk = 0; + // Don't update caret until we know if it is not drag and drop. + return; } else { - deselect(caret); + // A regular click clears all other carets. + caret = 0; + remove_secondary_carets(); + deselect(); } - caret_index_edit_dirty = true; - merge_overlapping_carets(); - queue_redraw(); } - } else if (drag_and_drop_selection_enabled && is_mouse_over_selection()) { - set_selection_mode(SelectionMode::SELECTION_MODE_NONE, get_selection_line(caret), get_selection_column(caret), caret); - // We use the main caret for dragging, so reset this one. - set_caret_line(prev_line, false, true, 0, caret); - set_caret_column(prev_col, false, caret); - selection_drag_attempt = true; - } else if (caret == 0) { - deselect(); - set_selection_mode(SelectionMode::SELECTION_MODE_POINTER, row, col); - } - if (is_triple_click) { - // Triple-click select line. - selecting_mode = SelectionMode::SELECTION_MODE_LINE; + _push_current_op(); + set_caret_line(line, false, true, -1, caret); + set_caret_column(col, false, caret); selection_drag_attempt = false; - _update_selection_mode_line(); + bool caret_moved = get_caret_column(caret) != prev_col || get_caret_line(caret) != prev_line; + + if (selecting_enabled && mb->is_shift_pressed() && !has_selection(caret) && caret_moved) { + // Select from the previous caret position. + select(prev_line, prev_col, line, col, caret); + } + + // Start regular select mode. + set_selection_mode(SelectionMode::SELECTION_MODE_POINTER); + _update_selection_mode_pointer(true); + } else if (is_triple_click) { + // Start triple-click select line mode. + set_selection_mode(SelectionMode::SELECTION_MODE_LINE); + _update_selection_mode_line(true); last_dblclk = 0; - } else if (mb->is_double_click() && text[get_caret_line(caret)].length()) { - // Double-click select word. - selecting_mode = SelectionMode::SELECTION_MODE_WORD; - _update_selection_mode_word(); + } else if (mb->is_double_click()) { + // Start double-click select word mode. + set_selection_mode(SelectionMode::SELECTION_MODE_WORD); + _update_selection_mode_word(true); last_dblclk = OS::get_singleton()->get_ticks_msec(); last_dblclk_pos = mb->get_position(); } @@ -1863,34 +1848,20 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { _push_current_op(); _reset_caret_blink_timer(); apply_ime(); + _cancel_drag_and_drop_text(); Point2i pos = get_line_column_at_pos(mpos); - int row = pos.y; - int col = pos.x; + int mouse_line = pos.y; + int mouse_column = pos.x; - bool selection_clicked = false; if (is_move_caret_on_right_click_enabled()) { - if (has_selection()) { - for (int i = 0; i < get_caret_count(); i++) { - int from_line = get_selection_from_line(i); - int to_line = get_selection_to_line(i); - int from_column = get_selection_from_column(i); - int to_column = get_selection_to_column(i); - - if (row >= from_line && row <= to_line && (row != from_line || col >= from_column) && (row != to_line || col <= to_column)) { - // Right click in one of the selected text - selection_clicked = true; - break; - } - } - } + bool selection_clicked = get_selection_at_line_column(mouse_line, mouse_column, true) >= 0; if (!selection_clicked) { deselect(); remove_secondary_carets(); - set_caret_line(row, false, false); - set_caret_column(col); + set_caret_line(mouse_line, false, false, -1); + set_caret_column(mouse_column); } - merge_overlapping_carets(); } if (context_menu_enabled) { @@ -1908,22 +1879,20 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { } if (mb->get_button_index() == MouseButton::LEFT) { - if (selection_drag_attempt && is_mouse_over_selection()) { + if (!drag_action && selection_drag_attempt && is_mouse_over_selection()) { + // This is not a drag and drop attempt, update the caret. + selection_drag_attempt = false; remove_secondary_carets(); + deselect(); Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); - set_caret_line(pos.y, false, true, 0, 0); + set_caret_line(pos.y, false, true, -1, 0); set_caret_column(pos.x, true, 0); - - deselect(); } dragging_minimap = false; dragging_selection = false; can_drag_minimap = false; click_select_held->stop(); - if (!drag_action) { - selection_drag_attempt = false; - } if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) { DisplayServer::get_singleton()->clipboard_set_primary(get_selected_text()); } @@ -1958,7 +1927,8 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { mpos.x = get_size().x - mpos.x; } - if (mm->get_button_mask().has_flag(MouseButtonMask::LEFT) && get_viewport()->gui_get_drag_data() == Variant()) { // Ignore if dragging. + if (mm->get_button_mask().has_flag(MouseButtonMask::LEFT) && get_viewport()->gui_get_drag_data() == Variant()) { + // Update if not in drag and drop. _reset_caret_blink_timer(); if (draw_minimap && !dragging_selection) { @@ -2011,10 +1981,19 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { if (drag_action && can_drop_data(mpos, get_viewport()->gui_get_drag_data())) { apply_ime(); + // Update drag and drop caret. drag_caret_force_displayed = true; Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); - set_caret_line(pos.y, false, true, 0, 0); - set_caret_column(pos.x, true, 0); + + if (drag_caret_index == -1) { + // Force create a new caret for drag and drop. + carets.push_back(Caret()); + drag_caret_index = carets.size() - 1; + } + + drag_caret_force_displayed = true; + set_caret_line(pos.y, false, true, -1, drag_caret_index); + set_caret_column(pos.x, true, drag_caret_index); dragging_selection = true; } } @@ -2043,6 +2022,8 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { return; } + _cancel_drag_and_drop_text(); + _reset_caret_blink_timer(); // Allow unicode handling if: @@ -2321,42 +2302,36 @@ void TextEdit::_new_line(bool p_split_current_line, bool p_above) { } begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { - bool first_line = false; - if (!p_split_current_line) { - deselect(i); - if (p_above) { - if (get_caret_line(i) > 0) { - set_caret_line(get_caret_line(i) - 1, false, true, 0, i); - set_caret_column(text[get_caret_line(i)].length(), i == 0, i); - } else { - set_caret_column(0, i == 0, i); - first_line = true; - } - } else { - set_caret_column(text[get_caret_line(i)].length(), i == 0, i); - } - } - - insert_text_at_caret("\n", i); + begin_multicaret_edit(); - if (first_line) { - set_caret_line(0, i == 0, true, 0, i); + for (int i = 0; i < get_caret_count(); i++) { + if (multicaret_edit_ignore_caret(i)) { + continue; + } + if (p_split_current_line) { + insert_text_at_caret("\n", i); + } else { + int line = get_caret_line(i); + insert_text("\n", line, p_above ? 0 : text[line].length(), p_above, p_above); + deselect(i); + set_caret_line(p_above ? line : line + 1, false, true, -1, i); + set_caret_column(0, i == 0, i); } } + + end_multicaret_edit(); end_complex_operation(); } void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { // Handle selection. if (p_select) { _pre_shift_selection(i); } else if (has_selection(i) && !p_move_by_word) { // If a selection is active, move caret to start of selection. - set_caret_line(get_selection_from_line(i), false, true, 0, i); + set_caret_line(get_selection_from_line(i), false, true, -1, i); set_caret_column(get_selection_from_column(i), i == 0, i); deselect(i); continue; @@ -2368,7 +2343,7 @@ void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) { int cc = get_caret_column(i); // If the caret is at the start of the line, and not on the first line, move it up to the end of the previous line. if (cc == 0 && get_caret_line(i) > 0) { - set_caret_line(get_caret_line(i) - 1, false, true, 0, i); + set_caret_line(get_caret_line(i) - 1, false, true, -1, i); set_caret_column(text[get_caret_line(i)].length(), i == 0, i); } else { PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(i))->get_rid()); @@ -2389,7 +2364,8 @@ void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) { // If the caret is at the start of the line, and not on the first line, move it up to the end of the previous line. if (get_caret_column(i) == 0) { if (get_caret_line(i) > 0) { - set_caret_line(get_caret_line(i) - get_next_visible_line_offset_from(CLAMP(get_caret_line(i) - 1, 0, text.size() - 1), -1), false, true, 0, i); + int new_caret_line = get_caret_line(i) - get_next_visible_line_offset_from(CLAMP(get_caret_line(i) - 1, 0, text.size() - 1), -1); + set_caret_line(new_caret_line, false, true, -1, i); set_caret_column(text[get_caret_line(i)].length(), i == 0, i); } } else { @@ -2400,23 +2376,19 @@ void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) { } } } - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { // Handle selection. if (p_select) { _pre_shift_selection(i); } else if (has_selection(i) && !p_move_by_word) { // If a selection is active, move caret to end of selection. - set_caret_line(get_selection_to_line(i), false, true, 0, i); + set_caret_line(get_selection_to_line(i), false, true, -1, i); set_caret_column(get_selection_to_column(i), i == 0, i); deselect(i); continue; @@ -2428,7 +2400,7 @@ void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) { int cc = get_caret_column(i); // If the caret is at the end of the line, and not on the last line, move it down to the beginning of the next line. if (cc == text[get_caret_line(i)].length() && get_caret_line(i) < text.size() - 1) { - set_caret_line(get_caret_line(i) + 1, false, true, 0, i); + set_caret_line(get_caret_line(i) + 1, false, true, -1, i); set_caret_column(0, i == 0, i); } else { PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(i))->get_rid()); @@ -2449,7 +2421,8 @@ void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) { // If we are at the end of the line, move the caret to the next line down. if (get_caret_column(i) == text[get_caret_line(i)].length()) { if (get_caret_line(i) < text.size() - 1) { - set_caret_line(get_caret_line(i) + get_next_visible_line_offset_from(CLAMP(get_caret_line(i) + 1, 0, text.size() - 1), 1), false, false, 0, i); + int new_caret_line = get_caret_line(i) + get_next_visible_line_offset_from(CLAMP(get_caret_line(i) + 1, 0, text.size() - 1), 1); + set_caret_line(new_caret_line, false, false, -1, i); set_caret_column(0, i == 0, i); } } else { @@ -2460,17 +2433,13 @@ void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) { } } } - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_up(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2490,17 +2459,13 @@ void TextEdit::_move_caret_up(bool p_select) { set_caret_line(new_line, i == 0, false, 0, i); } } - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_down(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2516,17 +2481,13 @@ void TextEdit::_move_caret_down(bool p_select) { int new_line = get_caret_line(i) + get_next_visible_line_offset_from(CLAMP(get_caret_line(i) + 1, 0, text.size() - 1), 1); set_caret_line(new_line, i == 0, false, 0, i); } - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_to_line_start(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2551,17 +2512,13 @@ void TextEdit::_move_caret_to_line_start(bool p_select) { } else { set_caret_column(row_start_col, i == 0, i); } - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_to_line_end(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2580,17 +2537,13 @@ void TextEdit::_move_caret_to_line_end(bool p_select) { } else { set_caret_column(row_end_col, i == 0, i); } - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_page_up(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2600,17 +2553,13 @@ void TextEdit::_move_caret_page_up(bool p_select) { Point2i next_line = get_next_visible_line_index_offset_from(get_caret_line(i), get_caret_wrap_index(i), -get_visible_line_count()); int n_line = get_caret_line(i) - next_line.x + 1; set_caret_line(n_line, i == 0, false, next_line.y, i); - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_page_down(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2620,10 +2569,6 @@ void TextEdit::_move_caret_page_down(bool p_select) { Point2i next_line = get_next_visible_line_index_offset_from(get_caret_line(i), get_caret_wrap_index(i), get_visible_line_count()); int n_line = get_caret_line(i) + next_line.x - 1; set_caret_line(n_line, i == 0, false, next_line.y, i); - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } @@ -2634,58 +2579,47 @@ void TextEdit::_do_backspace(bool p_word, bool p_all_to_left) { } start_action(EditAction::ACTION_BACKSPACE); - Vector<int> carets_to_remove; + begin_multicaret_edit(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (int i = 0; i < caret_edit_order.size(); i++) { - int caret_idx = caret_edit_order[i]; - if (get_caret_column(caret_idx) == 0 && get_caret_line(caret_idx) == 0 && !has_selection(caret_idx)) { + Vector<int> sorted_carets = get_sorted_carets(); + sorted_carets.reverse(); + for (int i = 0; i < sorted_carets.size(); i++) { + int caret_index = sorted_carets[i]; + if (multicaret_edit_ignore_caret(caret_index)) { continue; } - if (has_selection(caret_idx) || (!p_all_to_left && !p_word) || get_caret_column(caret_idx) == 0) { - backspace(caret_idx); + if (get_caret_column(caret_index) == 0 && get_caret_line(caret_index) == 0 && !has_selection(caret_index)) { continue; } - if (p_all_to_left) { - int caret_current_column = get_caret_column(caret_idx); - set_caret_column(0, caret_idx == 0, caret_idx); - _remove_text(get_caret_line(caret_idx), 0, get_caret_line(caret_idx), caret_current_column); - adjust_carets_after_edit(caret_idx, get_caret_line(caret_idx), caret_current_column, get_caret_line(caret_idx), get_caret_column(caret_idx)); - - // Check for any overlapping carets since we removed the entire line. - for (int j = i + 1; j < caret_edit_order.size(); j++) { - // Selection only end on this line, only the one as carets cannot overlap. - if (has_selection(caret_edit_order[j]) && get_selection_from_line(caret_edit_order[j]) != get_caret_line(caret_idx) && get_selection_to_line(caret_edit_order[j]) == get_caret_line(caret_idx)) { - carets.write[caret_edit_order[j]].selection.to_column = 0; - break; - } - - // Check for caret. - if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx) || (has_selection(caret_edit_order[j]) && get_selection_from_line(caret_edit_order[j]) != get_caret_line(caret_idx))) { - break; - } + if (has_selection(caret_index) || (!p_all_to_left && !p_word) || get_caret_column(caret_index) == 0) { + backspace(caret_index); + continue; + } - deselect(caret_edit_order[j]); - carets_to_remove.push_back(caret_edit_order[j]); - set_caret_column(0, caret_idx == 0, caret_idx); - i = j; - } + if (p_all_to_left) { + // Remove everything to left of caret to the start of the line. + int caret_current_column = get_caret_column(caret_index); + _remove_text(get_caret_line(caret_index), 0, get_caret_line(caret_index), caret_current_column); + collapse_carets(get_caret_line(caret_index), 0, get_caret_line(caret_index), caret_current_column); + set_caret_column(0, caret_index == 0, caret_index); + _offset_carets_after(get_caret_line(caret_index), caret_current_column, get_caret_line(caret_index), 0); continue; } if (p_word) { - // Save here as the caret may change when resolving overlaps. - int from_column = get_caret_column(caret_idx); - int column = get_caret_column(caret_idx); + // Remove text to the start of the word left of the caret. + int from_column = get_caret_column(caret_index); + int column = get_caret_column(caret_index); // Check for the case "<word><space><caret>" and ignore the space. // No need to check for column being 0 since it is checked above. - if (is_whitespace(text[get_caret_line(caret_idx)][get_caret_column(caret_idx) - 1])) { + if (is_whitespace(text[get_caret_line(caret_index)][get_caret_column(caret_index) - 1])) { column -= 1; } + // Get a list with the indices of the word bounds of the given text line. - const PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(caret_idx))->get_rid()); + const PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(caret_index))->get_rid()); if (words.is_empty() || column <= words[0]) { // If "words" is empty, meaning no words are left, we can remove everything until the beginning of the line. column = 0; @@ -2699,57 +2633,14 @@ void TextEdit::_do_backspace(bool p_word, bool p_all_to_left) { } } - // Check for any other carets in this range. - int overlapping_caret_index = -1; - for (int j = i + 1; j < caret_edit_order.size(); j++) { - // Check caret and selection in on the right line. - if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx) && (!has_selection(caret_edit_order[j]) || get_selection_to_line(caret_edit_order[j]) != get_caret_line(caret_idx))) { - break; - } - - // If it has a selection, check it ends with in the range. - if ((has_selection(caret_edit_order[j]) && get_selection_to_column(caret_edit_order[j]) < column)) { - break; - } - - // If it has a selection and it starts outside our word, we need to adjust the selection, and handle it later to prevent overlap. - if ((has_selection(caret_edit_order[j]) && get_selection_from_column(caret_edit_order[j]) < column)) { - carets.write[caret_edit_order[j]].selection.to_column = column; - overlapping_caret_index = caret_edit_order[j]; - break; - } - - // Otherwise we can remove it. - if (get_caret_column(caret_edit_order[j]) > column || (has_selection(caret_edit_order[j]) && get_selection_from_column(caret_edit_order[j]) > column)) { - deselect(caret_edit_order[j]); - carets_to_remove.push_back(caret_edit_order[j]); - set_caret_column(0, caret_idx == 0, caret_idx); - i = j; - } - } - - _remove_text(get_caret_line(caret_idx), column, get_caret_line(caret_idx), from_column); - - set_caret_line(get_caret_line(caret_idx), false, true, 0, caret_idx); - set_caret_column(column, caret_idx == 0, caret_idx); - adjust_carets_after_edit(caret_idx, get_caret_line(caret_idx), column, get_caret_line(caret_idx), from_column); - - // Now we can clean up the overlapping caret. - if (overlapping_caret_index != -1) { - backspace(overlapping_caret_index); - i++; - carets_to_remove.push_back(overlapping_caret_index); - set_caret_column(get_caret_column(overlapping_caret_index), caret_idx == 0, caret_idx); - } - continue; + _remove_text(get_caret_line(caret_index), column, get_caret_line(caret_index), from_column); + collapse_carets(get_caret_line(caret_index), column, get_caret_line(caret_index), from_column); + set_caret_column(column, caret_index == 0, caret_index); + _offset_carets_after(get_caret_line(caret_index), from_column, get_caret_line(caret_index), column); } } - // Sort and remove backwards to preserve indexes. - carets_to_remove.sort(); - for (int i = carets_to_remove.size() - 1; i >= 0; i--) { - remove_caret(carets_to_remove[i]); - } + end_multicaret_edit(); end_action(); } @@ -2759,61 +2650,40 @@ void TextEdit::_delete(bool p_word, bool p_all_to_right) { } start_action(EditAction::ACTION_DELETE); - Vector<int> carets_to_remove; + begin_multicaret_edit(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (int i = 0; i < caret_edit_order.size(); i++) { - int caret_idx = caret_edit_order[i]; - if (has_selection(caret_idx)) { - delete_selection(caret_idx); + Vector<int> sorted_carets = get_sorted_carets(); + for (int i = 0; i < sorted_carets.size(); i++) { + int caret_index = sorted_carets[i]; + if (multicaret_edit_ignore_caret(caret_index)) { continue; } - int curline_len = text[get_caret_line(caret_idx)].length(); - if (get_caret_line(caret_idx) == text.size() - 1 && get_caret_column(caret_idx) == curline_len) { + if (has_selection(caret_index)) { + delete_selection(caret_index); + continue; + } + + int curline_len = text[get_caret_line(caret_index)].length(); + if (get_caret_line(caret_index) == text.size() - 1 && get_caret_column(caret_index) == curline_len) { continue; // Last line, last column: Nothing to do. } - int next_line = get_caret_column(caret_idx) < curline_len ? get_caret_line(caret_idx) : get_caret_line(caret_idx) + 1; + int next_line = get_caret_column(caret_index) < curline_len ? get_caret_line(caret_index) : get_caret_line(caret_index) + 1; int next_column; if (p_all_to_right) { - // Get caret furthest to the left. - for (int j = i + 1; j < caret_edit_order.size(); j++) { - if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx)) { - break; - } - - if (has_selection(caret_edit_order[j]) && get_selection_from_line(caret_edit_order[j]) != get_caret_line(caret_idx)) { - break; - } - - if (!has_selection(caret_edit_order[j])) { - i = j; - caret_idx = caret_edit_order[i]; - } - } - - if (get_caret_column(caret_idx) == curline_len) { + if (get_caret_column(caret_index) == curline_len) { continue; } // Delete everything to right of caret. next_column = curline_len; - next_line = get_caret_line(caret_idx); - - // Remove overlapping carets. - for (int j = i - 1; j >= 0; j--) { - if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx)) { - break; - } - carets_to_remove.push_back(caret_edit_order[j]); - } - - } else if (p_word && get_caret_column(caret_idx) < curline_len - 1) { + next_line = get_caret_line(caret_index); + } else if (p_word && get_caret_column(caret_index) < curline_len - 1) { // Delete next word to right of caret. - int line = get_caret_line(caret_idx); - int column = get_caret_column(caret_idx); + int line = get_caret_line(caret_index); + int column = get_caret_column(caret_index); PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(line)->get_rid()); for (int j = 1; j < words.size(); j = j + 2) { @@ -2825,49 +2695,22 @@ void TextEdit::_delete(bool p_word, bool p_all_to_right) { next_line = line; next_column = column; - - // Remove overlapping carets. - for (int j = i - 1; j >= 0; j--) { - if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx)) { - break; - } - - if (get_caret_column(caret_edit_order[j]) > column) { - break; - } - carets_to_remove.push_back(caret_edit_order[j]); - } } else { // Delete one character. if (caret_mid_grapheme_enabled) { - next_column = get_caret_column(caret_idx) < curline_len ? (get_caret_column(caret_idx) + 1) : 0; + next_column = get_caret_column(caret_index) < curline_len ? (get_caret_column(caret_index) + 1) : 0; } else { - next_column = get_caret_column(caret_idx) < curline_len ? TS->shaped_text_next_character_pos(text.get_line_data(get_caret_line(caret_idx))->get_rid(), (get_caret_column(caret_idx))) : 0; - } - - // Remove overlapping carets. - if (i > 0) { - int prev_caret_idx = caret_edit_order[i - 1]; - if (get_caret_line(prev_caret_idx) == next_line && get_caret_column(prev_caret_idx) == next_column) { - carets_to_remove.push_back(prev_caret_idx); - } + next_column = get_caret_column(caret_index) < curline_len ? TS->shaped_text_next_character_pos(text.get_line_data(get_caret_line(caret_index))->get_rid(), (get_caret_column(caret_index))) : 0; } } - _remove_text(get_caret_line(caret_idx), get_caret_column(caret_idx), next_line, next_column); - adjust_carets_after_edit(caret_idx, get_caret_line(caret_idx), get_caret_column(caret_idx), next_line, next_column); - } - - // Sort and remove backwards to preserve indexes. - carets_to_remove.sort(); - for (int i = carets_to_remove.size() - 1; i >= 0; i--) { - remove_caret(carets_to_remove[i]); + _remove_text(get_caret_line(caret_index), get_caret_column(caret_index), next_line, next_column); + collapse_carets(get_caret_line(caret_index), get_caret_column(caret_index), next_line, next_column); + _offset_carets_after(next_line, next_column, get_caret_line(caret_index), get_caret_column(caret_index)); } - // If we are deleting from the end of a line, due to column preservation we could still overlap with another caret. - merge_overlapping_carets(); + end_multicaret_edit(); end_action(); - queue_redraw(); } void TextEdit::_move_caret_document_start(bool p_select) { @@ -2878,12 +2721,8 @@ void TextEdit::_move_caret_document_start(bool p_select) { deselect(); } - set_caret_line(0, false); + set_caret_line(0, false, true, -1); set_caret_column(0); - - if (p_select) { - _post_shift_selection(0); - } } void TextEdit::_move_caret_document_end(bool p_select) { @@ -2894,12 +2733,8 @@ void TextEdit::_move_caret_document_end(bool p_select) { deselect(); } - set_caret_line(get_last_unhidden_line(), true, false, 9999); + set_caret_line(get_last_unhidden_line(), true, false, -1); set_caret_column(text[get_caret_line()].length()); - - if (p_select) { - _post_shift_selection(0); - } } bool TextEdit::_clear_carets_and_selection() { @@ -2917,51 +2752,6 @@ bool TextEdit::_clear_carets_and_selection() { return false; } -void TextEdit::_get_above_below_caret_line_column(int p_old_line, int p_old_wrap_index, int p_old_column, bool p_below, int &p_new_line, int &p_new_column, int p_last_fit_x) const { - if (p_last_fit_x == -1) { - p_last_fit_x = _get_column_x_offset_for_line(p_old_column, p_old_line, p_old_column); - } - - // Calculate the new line and wrap index. - p_new_line = p_old_line; - int caret_wrap_index = p_old_wrap_index; - if (p_below) { - if (caret_wrap_index < get_line_wrap_count(p_new_line)) { - caret_wrap_index++; - } else { - p_new_line++; - caret_wrap_index = 0; - } - } else { - if (caret_wrap_index == 0) { - p_new_line--; - caret_wrap_index = get_line_wrap_count(p_new_line); - } else { - caret_wrap_index--; - } - } - - // Boundary checks. - if (p_new_line < 0) { - p_new_line = 0; - } - if (p_new_line >= text.size()) { - p_new_line = text.size() - 1; - } - - p_new_column = _get_char_pos_for_line(p_last_fit_x, p_new_line, caret_wrap_index); - if (p_new_column != 0 && get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE && caret_wrap_index < get_line_wrap_count(p_new_line)) { - Vector<String> rows = get_line_wrapped_text(p_new_line); - int row_end_col = 0; - for (int i = 0; i < caret_wrap_index + 1; i++) { - row_end_col += rows[i].length(); - } - if (p_new_column >= row_end_col) { - p_new_column -= 1; - } - } -} - void TextEdit::_update_placeholder() { if (theme_cache.font.is_null() || theme_cache.font_size <= 0) { return; // Not in tree? @@ -3127,53 +2917,48 @@ void TextEdit::drop_data(const Point2 &p_point, const Variant &p_data) { if (p_data.get_type() == Variant::STRING && is_editable()) { Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); - int caret_row_tmp = pos.y; - int caret_column_tmp = pos.x; + int drop_at_line = pos.y; + int drop_at_column = pos.x; + int selection_index = get_selection_at_line_column(drop_at_line, drop_at_column, !Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)); + + // Remove drag caret before the complex operation starts so it won't appear in undo. + remove_caret(drag_caret_index); + + if (selection_drag_attempt && selection_index >= 0 && selection_index == drag_and_drop_origin_caret_index) { + // Dropped onto original selection, do nothing. + selection_drag_attempt = false; + return; + } + + begin_complex_operation(); + begin_multicaret_edit(); if (selection_drag_attempt) { + // Drop from self. selection_drag_attempt = false; - if (!is_mouse_over_selection(!Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL))) { - // Set caret back at selection for undo / redo. - set_caret_line(get_selection_to_line(), false, false); - set_caret_column(get_selection_to_column()); - - begin_complex_operation(); - if (!Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) { - if (caret_row_tmp > get_selection_to_line()) { - caret_row_tmp = caret_row_tmp - (get_selection_to_line() - get_selection_from_line()); - } else if (caret_row_tmp == get_selection_to_line() && caret_column_tmp >= get_selection_to_column()) { - caret_column_tmp = caret_column_tmp - (get_selection_to_column() - get_selection_from_column()); - } - delete_selection(); - } else { - deselect(); - } + if (!Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) { + // Delete all selections. + int temp_caret = add_caret(drop_at_line, drop_at_column); - remove_secondary_carets(); - set_caret_line(caret_row_tmp, true, false); - set_caret_column(caret_column_tmp); - insert_text_at_caret(p_data); - end_complex_operation(); - } - } else if (is_mouse_over_selection()) { - remove_secondary_carets(); - caret_row_tmp = get_selection_from_line(); - caret_column_tmp = get_selection_from_column(); - set_caret_line(caret_row_tmp, true, false); - set_caret_column(caret_column_tmp); - insert_text_at_caret(p_data); - grab_focus(); - } else { - remove_secondary_carets(); - deselect(); - set_caret_line(caret_row_tmp, true, false); - set_caret_column(caret_column_tmp); - insert_text_at_caret(p_data); - grab_focus(); - } + delete_selection(); - if (caret_row_tmp != get_caret_line() || caret_column_tmp != get_caret_column()) { - select(caret_row_tmp, caret_column_tmp, get_caret_line(), get_caret_column()); + // Use a temporary caret to update the drop at position. + drop_at_line = get_caret_line(temp_caret); + drop_at_column = get_caret_column(temp_caret); + } } + remove_secondary_carets(); + deselect(); + + // Insert the dragged text. + set_caret_line(drop_at_line, true, false, -1); + set_caret_column(drop_at_column); + insert_text_at_caret(p_data); + + select(drop_at_line, drop_at_column, get_caret_line(), get_caret_column()); + grab_focus(); + adjust_viewport_to_caret(); + end_multicaret_edit(); + end_complex_operation(); } } @@ -3459,7 +3244,7 @@ void TextEdit::_clear() { clear_undo_history(); text.clear(); remove_secondary_carets(); - set_caret_line(0, false); + set_caret_line(0, false, true, -1); set_caret_column(0); first_visible_col = 0; first_visible_line = 0; @@ -3532,17 +3317,36 @@ void TextEdit::set_line(int p_line, const String &p_new_text) { return; } begin_complex_operation(); - _remove_text(p_line, 0, p_line, text[p_line].length()); - _insert_text(p_line, 0, p_new_text); - for (int i = 0; i < carets.size(); i++) { - if (get_caret_line(i) == p_line && get_caret_column(i) > p_new_text.length()) { - set_caret_column(p_new_text.length(), false, i); + + int old_column = text[p_line].length(); + + // Set the affected carets column to update their last offset x. + for (int i = 0; i < get_caret_count(); i++) { + if (_is_line_col_in_range(get_caret_line(i), get_caret_column(i), p_line, 0, p_line, old_column)) { + set_caret_column(get_caret_column(i), false, i); } + if (has_selection(i) && _is_line_col_in_range(get_selection_origin_line(i), get_selection_origin_column(i), p_line, 0, p_line, old_column)) { + set_selection_origin_column(get_selection_origin_column(i), i); + } + } + + _remove_text(p_line, 0, p_line, old_column); + int new_line, new_column; + _insert_text(p_line, 0, p_new_text, &new_line, &new_column); - if (has_selection(i) && p_line == get_selection_to_line(i) && get_selection_to_column(i) > text[p_line].length()) { - carets.write[i].selection.to_column = text[p_line].length(); + // Don't offset carets that were on the old line. + _offset_carets_after(p_line, old_column, new_line, new_column, false, false); + + // Set the caret lines to update the column to match visually. + for (int i = 0; i < get_caret_count(); i++) { + if (_is_line_col_in_range(get_caret_line(i), get_caret_column(i), p_line, 0, p_line, old_column)) { + set_caret_line(get_caret_line(i), false, true, 0, i); + } + if (has_selection(i) && _is_line_col_in_range(get_selection_origin_line(i), get_selection_origin_column(i), p_line, 0, p_line, old_column)) { + set_selection_origin_line(get_selection_origin_line(i), true, 0, i); } } + merge_overlapping_carets(); end_complex_operation(); } @@ -3596,71 +3400,163 @@ void TextEdit::swap_lines(int p_from_line, int p_to_line) { ERR_FAIL_INDEX(p_from_line, text.size()); ERR_FAIL_INDEX(p_to_line, text.size()); - String tmp = get_line(p_from_line); - String tmp2 = get_line(p_to_line); + if (p_from_line == p_to_line) { + return; + } + + String from_line_text = get_line(p_from_line); + String to_line_text = get_line(p_to_line); begin_complex_operation(); - set_line(p_to_line, tmp); - set_line(p_from_line, tmp2); + begin_multicaret_edit(); + // Don't use set_line to avoid clamping and updating carets. + _remove_text(p_to_line, 0, p_to_line, text[p_to_line].length()); + _insert_text(p_to_line, 0, from_line_text); + _remove_text(p_from_line, 0, p_from_line, text[p_from_line].length()); + _insert_text(p_from_line, 0, to_line_text); + + // Swap carets. + for (int i = 0; i < get_caret_count(); i++) { + bool selected = has_selection(i); + if (get_caret_line(i) == p_from_line || get_caret_line(i) == p_to_line) { + int caret_new_line = get_caret_line(i) == p_from_line ? p_to_line : p_from_line; + int caret_column = get_caret_column(i); + set_caret_line(caret_new_line, false, true, -1, i); + set_caret_column(caret_column, false, i); + } + if (selected && (get_selection_origin_line(i) == p_from_line || get_selection_origin_line(i) == p_to_line)) { + int origin_new_line = get_selection_origin_line(i) == p_from_line ? p_to_line : p_from_line; + int origin_column = get_selection_origin_column(i); + select(origin_new_line, origin_column, get_caret_line(i), get_caret_column(i), i); + } + } + // If only part of a selection was changed, it may now overlap. + merge_overlapping_carets(); + + end_multicaret_edit(); end_complex_operation(); } -void TextEdit::insert_line_at(int p_at, const String &p_text) { - ERR_FAIL_INDEX(p_at, text.size()); +void TextEdit::insert_line_at(int p_line, const String &p_text) { + ERR_FAIL_INDEX(p_line, text.size()); - _insert_text(p_at, 0, p_text + "\n"); + // Use a complex operation so subsequent calls aren't merged together. + begin_complex_operation(); - for (int i = 0; i < carets.size(); i++) { - if (get_caret_line(i) >= p_at) { - // Offset caret when located after inserted line. - set_caret_line(get_caret_line(i) + 1, false, true, 0, i); - } - if (has_selection(i)) { - if (get_selection_from_line(i) >= p_at) { - // Offset selection when located after inserted line. - select(get_selection_from_line(i) + 1, get_selection_from_column(i), get_selection_to_line(i) + 1, get_selection_to_column(i), i); - } else if (get_selection_to_line(i) >= p_at) { - // Extend selection that includes inserted line. - select(get_selection_from_line(i), get_selection_from_column(i), get_selection_to_line(i) + 1, get_selection_to_column(i), i); + int new_line, new_column; + _insert_text(p_line, 0, p_text + "\n", &new_line, &new_column); + _offset_carets_after(p_line, 0, new_line, new_column); + + end_complex_operation(); +} + +void TextEdit::remove_line_at(int p_line, bool p_move_carets_down) { + ERR_FAIL_INDEX(p_line, text.size()); + + if (get_line_count() == 1) { + // Only one line, just remove contents. + begin_complex_operation(); + int line_length = get_line(p_line).length(); + _remove_text(p_line, 0, p_line, line_length); + collapse_carets(p_line, 0, p_line, line_length, true); + end_complex_operation(); + return; + } + + begin_complex_operation(); + + bool is_last_line = p_line == get_line_count() - 1; + int from_line = is_last_line ? p_line - 1 : p_line; + int next_line = is_last_line ? p_line : p_line + 1; + int from_column = is_last_line ? get_line(from_line).length() : 0; + int next_column = is_last_line ? get_line(next_line).length() : 0; + + if ((!is_last_line && p_move_carets_down) || (p_line != 0 && !p_move_carets_down)) { + // Set the carets column to update their last offset x. + for (int i = 0; i < get_caret_count(); i++) { + if (get_caret_line(i) == p_line) { + set_caret_column(get_caret_column(i), false, i); + } + if (has_selection(i) && get_selection_origin_line(i) == p_line) { + set_selection_origin_column(get_selection_origin_column(i), i); } } } - // Need to apply the above adjustments to the undo / redo carets. - current_op.end_carets = carets; - queue_redraw(); + // Remove line. + _remove_text(from_line, from_column, next_line, next_column); + + begin_multicaret_edit(); + if ((is_last_line && p_move_carets_down) || (p_line == 0 && !p_move_carets_down)) { + // Collapse carets. + collapse_carets(from_line, from_column, next_line, next_column, true); + } else { + // Move carets to visually line up. + int target_line = p_move_carets_down ? p_line : p_line - 1; + for (int i = 0; i < get_caret_count(); i++) { + bool selected = has_selection(i); + if (get_caret_line(i) == p_line) { + set_caret_line(target_line, i == 0, true, 0, i); + } + if (selected && get_selection_origin_line(i) == p_line) { + set_selection_origin_line(target_line, true, 0, i); + select(get_selection_origin_line(i), get_selection_origin_column(i), get_caret_line(i), get_caret_column(i), i); + } + } + + merge_overlapping_carets(); + } + _offset_carets_after(next_line, next_column, from_line, from_column); + end_multicaret_edit(); + end_complex_operation(); } void TextEdit::insert_text_at_caret(const String &p_text, int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { if (p_caret != -1 && p_caret != i) { continue; } + if (p_caret == -1 && multicaret_edit_ignore_caret(i)) { + continue; + } delete_selection(i); int from_line = get_caret_line(i); int from_col = get_caret_column(i); - int new_column, new_line; + int new_line, new_column; _insert_text(from_line, from_col, p_text, &new_line, &new_column); _update_scrollbars(); + _offset_carets_after(from_line, from_col, new_line, new_column); - set_caret_line(new_line, false, true, 0, i); + set_caret_line(new_line, false, true, -1, i); set_caret_column(new_column, i == 0, i); - - adjust_carets_after_edit(i, new_line, new_column, from_line, from_col); } if (has_ime_text()) { _update_ime_text(); } + end_multicaret_edit(); + end_complex_operation(); +} + +void TextEdit::insert_text(const String &p_text, int p_line, int p_column, bool p_before_selection_begin, bool p_before_selection_end) { + ERR_FAIL_INDEX(p_line, text.size()); + ERR_FAIL_INDEX(p_column, text[p_line].length() + 1); + + begin_complex_operation(); + + int new_line, new_column; + _insert_text(p_line, p_column, p_text, &new_line, &new_column); + + _offset_carets_after(p_line, p_column, new_line, new_column, p_before_selection_begin, p_before_selection_end); + end_complex_operation(); - queue_redraw(); } void TextEdit::remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column) { @@ -3671,7 +3567,13 @@ void TextEdit::remove_text(int p_from_line, int p_from_column, int p_to_line, in ERR_FAIL_COND(p_to_line < p_from_line); ERR_FAIL_COND(p_to_line == p_from_line && p_to_column < p_from_column); + begin_complex_operation(); + _remove_text(p_from_line, p_from_column, p_to_line, p_to_column); + collapse_carets(p_from_line, p_from_column, p_to_line, p_to_column); + _offset_carets_after(p_to_line, p_to_column, p_from_line, p_from_column); + + end_complex_operation(); } int TextEdit::get_last_unhidden_line() const { @@ -4040,7 +3942,7 @@ void TextEdit::undo() { _push_current_op(); if (undo_stack_pos == nullptr) { - if (!undo_stack.size()) { + if (undo_stack.is_empty()) { return; // Nothing to undo. } @@ -4059,6 +3961,7 @@ void TextEdit::undo() { current_op.version = op.prev_version; if (undo_stack_pos->get().chain_backward) { + // This was part of a complex operation, undo until the chain forward at the start of the complex operation. while (true) { ERR_BREAK(!undo_stack_pos->prev()); undo_stack_pos = undo_stack_pos->prev(); @@ -4072,9 +3975,9 @@ void TextEdit::undo() { } _update_scrollbars(); - bool dirty_carets = carets.size() != undo_stack_pos->get().start_carets.size(); + bool dirty_carets = get_caret_count() != undo_stack_pos->get().start_carets.size(); if (!dirty_carets) { - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (carets[i].line != undo_stack_pos->get().start_carets[i].line || carets[i].column != undo_stack_pos->get().start_carets[i].column) { dirty_carets = true; break; @@ -4084,11 +3987,11 @@ void TextEdit::undo() { carets = undo_stack_pos->get().start_carets; - if (dirty_carets && !caret_pos_dirty) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); - } - caret_pos_dirty = true; + _unhide_carets(); + + if (dirty_carets) { + _caret_changed(); + _selection_changed(); } adjust_viewport_to_caret(); } @@ -4113,6 +4016,7 @@ void TextEdit::redo() { _do_text_op(op, false); current_op.version = op.version; if (undo_stack_pos->get().chain_forward) { + // This was part of a complex operation, redo until the chain backward at the end of the complex operation. while (true) { ERR_BREAK(!undo_stack_pos->next()); undo_stack_pos = undo_stack_pos->next(); @@ -4126,9 +4030,9 @@ void TextEdit::redo() { } _update_scrollbars(); - bool dirty_carets = carets.size() != undo_stack_pos->get().end_carets.size(); + bool dirty_carets = get_caret_count() != undo_stack_pos->get().end_carets.size(); if (!dirty_carets) { - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (carets[i].line != undo_stack_pos->get().end_carets[i].line || carets[i].column != undo_stack_pos->get().end_carets[i].column) { dirty_carets = true; break; @@ -4139,11 +4043,11 @@ void TextEdit::redo() { carets = undo_stack_pos->get().end_carets; undo_stack_pos = undo_stack_pos->next(); - if (dirty_carets && !caret_pos_dirty) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); - } - caret_pos_dirty = true; + _unhide_carets(); + + if (dirty_carets) { + _caret_changed(); + _selection_changed(); } adjust_viewport_to_caret(); } @@ -4358,13 +4262,7 @@ Point2i TextEdit::get_line_column_at_pos(const Point2i &p_pos, bool p_allow_out_ } } - if (row < 0) { - row = 0; - } - - if (row >= text.size()) { - row = text.size() - 1; - } + row = CLAMP(row, 0, text.size() - 1); int visible_lines = get_visible_line_count_in_range(first_vis_line, row); if (rows > visible_lines) { @@ -4510,29 +4408,13 @@ bool TextEdit::is_dragging_cursor() const { } bool TextEdit::is_mouse_over_selection(bool p_edges, int p_caret) const { - for (int i = 0; i < carets.size(); i++) { - if (p_caret != -1 && p_caret != i) { - continue; - } - - if (!has_selection(i)) { - continue; - } - - Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); - int row = pos.y; - int col = pos.x; - if (p_edges) { - if ((row == get_selection_from_line(i) && col == get_selection_from_column(i)) || (row == get_selection_to_line(i) && col == get_selection_to_column(i))) { - return true; - } - } + Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); + int line = pos.y; + int column = pos.x; - if (row >= get_selection_from_line(i) && row <= get_selection_to_line(i) && (row > get_selection_from_line(i) || col > get_selection_from_column(i)) && (row < get_selection_to_line(i) || col < get_selection_to_column(i))) { - return true; - } + if ((p_caret == -1 && get_selection_at_line_column(line, column, p_edges) != -1) || (p_caret != -1 && _selection_contains(p_caret, line, column, p_edges))) { + return true; } - return false; } @@ -4619,270 +4501,401 @@ bool TextEdit::is_multiple_carets_enabled() const { return multi_carets_enabled; } -int TextEdit::add_caret(int p_line, int p_col) { +int TextEdit::add_caret(int p_line, int p_column) { if (!multi_carets_enabled) { return -1; } + _cancel_drag_and_drop_text(); p_line = CLAMP(p_line, 0, text.size() - 1); - p_col = CLAMP(p_col, 0, get_line(p_line).length()); + p_column = CLAMP(p_column, 0, get_line(p_line).length()); - for (int i = 0; i < carets.size(); i++) { - if (get_caret_line(i) == p_line && get_caret_column(i) == p_col) { + if (!is_in_mulitcaret_edit()) { + // Carets cannot overlap. + if (get_selection_at_line_column(p_line, p_column, true, false) != -1) { return -1; } - - if (has_selection(i)) { - if (p_line >= get_selection_from_line(i) && p_line <= get_selection_to_line(i) && (p_line > get_selection_from_line(i) || p_col >= get_selection_from_column(i)) && (p_line < get_selection_to_line(i) || p_col <= get_selection_to_column(i))) { - return -1; - } - } } carets.push_back(Caret()); - set_caret_line(p_line, false, false, 0, carets.size() - 1); - set_caret_column(p_col, false, carets.size() - 1); - caret_index_edit_dirty = true; - return carets.size() - 1; + int new_index = carets.size() - 1; + set_caret_line(p_line, false, false, -1, new_index); + set_caret_column(p_column, false, new_index); + _caret_changed(new_index); + + if (is_in_mulitcaret_edit()) { + multicaret_edit_ignore_carets.insert(new_index); + merge_overlapping_carets(); + } + return new_index; } void TextEdit::remove_caret(int p_caret) { ERR_FAIL_COND_MSG(carets.size() <= 1, "The main caret should not be removed."); ERR_FAIL_INDEX(p_caret, carets.size()); + + _caret_changed(p_caret); carets.remove_at(p_caret); - caret_index_edit_dirty = true; + + if (drag_caret_index >= 0) { + if (p_caret == drag_caret_index) { + drag_caret_index = -1; + } else if (p_caret < drag_caret_index) { + drag_caret_index -= 1; + } + } } void TextEdit::remove_secondary_carets() { + if (carets.size() == 1) { + return; + } + + _caret_changed(); carets.resize(1); - caret_index_edit_dirty = true; - queue_redraw(); + + if (drag_caret_index >= 0) { + drag_caret_index = -1; + } } -void TextEdit::merge_overlapping_carets() { - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (int i = 0; i < caret_edit_order.size() - 1; i++) { - int first_caret = caret_edit_order[i]; - int second_caret = caret_edit_order[i + 1]; +int TextEdit::get_caret_count() const { + // Don't include drag caret. + if (drag_caret_index >= 0) { + return carets.size() - 1; + } + return carets.size(); +} - // Both have selection. - if (has_selection(first_caret) && has_selection(second_caret)) { - bool should_merge = false; - if (get_selection_from_line(first_caret) >= get_selection_from_line(second_caret) && get_selection_from_line(first_caret) <= get_selection_to_line(second_caret) && (get_selection_from_line(first_caret) > get_selection_from_line(second_caret) || get_selection_from_column(first_caret) >= get_selection_from_column(second_caret)) && (get_selection_from_line(first_caret) < get_selection_to_line(second_caret) || get_selection_from_column(first_caret) <= get_selection_to_column(second_caret))) { - should_merge = true; - } +void TextEdit::add_caret_at_carets(bool p_below) { + const int last_line_max_wrap = get_line_wrap_count(text.size() - 1); + + begin_multicaret_edit(); + int view_target_caret = -1; + int view_line = p_below ? -1 : INT_MAX; + int num_carets = get_caret_count(); + for (int i = 0; i < num_carets; i++) { + const int caret_line = get_caret_line(i); + const int caret_column = get_caret_column(i); + const bool is_selected = has_selection(i) || carets[i].last_fit_x != carets[i].selection.origin_last_fit_x; + const int selection_origin_line = get_selection_origin_line(i); + const int selection_origin_column = get_selection_origin_column(i); + const int caret_wrap_index = get_caret_wrap_index(i); + const int selection_origin_wrap_index = !is_selected ? -1 : get_line_wrap_index_at_column(selection_origin_line, selection_origin_column); + + if (caret_line == 0 && !p_below && (caret_wrap_index == 0 || selection_origin_wrap_index == 0)) { + // Can't add above the first line. + continue; + } + if (caret_line == text.size() - 1 && p_below && (caret_wrap_index == last_line_max_wrap || selection_origin_wrap_index == last_line_max_wrap)) { + // Can't add below the last line. + continue; + } - if (get_selection_to_line(first_caret) >= get_selection_from_line(second_caret) && get_selection_to_line(first_caret) <= get_selection_to_line(second_caret) && (get_selection_to_line(first_caret) > get_selection_from_line(second_caret) || get_selection_to_column(first_caret) >= get_selection_from_column(second_caret)) && (get_selection_to_line(first_caret) < get_selection_to_line(second_caret) || get_selection_to_column(first_caret) <= get_selection_to_column(second_caret))) { - should_merge = true; - } + // Add a new caret. + int new_caret_index = add_caret(caret_line, caret_column); - if (!should_merge) { - continue; - } + // Copy the selection origin and last fit. + set_selection_origin_line(selection_origin_line, true, -1, new_caret_index); + set_selection_origin_column(selection_origin_column, new_caret_index); + carets.write[new_caret_index].last_fit_x = carets[i].last_fit_x; + carets.write[new_caret_index].selection.origin_last_fit_x = carets[i].selection.origin_last_fit_x; - // Save the newest one for Click + Drag. - int caret_to_save = first_caret; - int caret_to_remove = second_caret; - if (first_caret < second_caret) { - caret_to_save = second_caret; - caret_to_remove = first_caret; + // Move the caret up or down one visible line. + if (!p_below) { + // Move caret up. + if (caret_wrap_index > 0) { + set_caret_line(caret_line, false, false, caret_wrap_index - 1, new_caret_index); + } else { + int new_line = caret_line - get_next_visible_line_offset_from(caret_line - 1, -1); + if (is_line_wrapped(new_line)) { + set_caret_line(new_line, false, false, get_line_wrap_count(new_line), new_caret_index); + } else { + set_caret_line(new_line, false, false, 0, new_caret_index); + } } - - int from_line = MIN(get_selection_from_line(caret_to_save), get_selection_from_line(caret_to_remove)); - int to_line = MAX(get_selection_to_line(caret_to_save), get_selection_to_line(caret_to_remove)); - int from_col = get_selection_from_column(caret_to_save); - int to_col = get_selection_to_column(caret_to_save); - int selection_line = get_selection_line(caret_to_save); - int selection_col = get_selection_column(caret_to_save); - - bool at_from = (get_caret_line(caret_to_save) == get_selection_from_line(caret_to_save) && get_caret_column(caret_to_save) == get_selection_from_column(caret_to_save)); - - if (at_from) { - if (get_selection_line(caret_to_remove) > get_selection_line(caret_to_save) || (get_selection_line(caret_to_remove) == get_selection_line(caret_to_save) && get_selection_column(caret_to_remove) >= get_selection_column(caret_to_save))) { - selection_line = get_selection_line(caret_to_remove); - selection_col = get_selection_column(caret_to_remove); + // Move selection origin up. + if (is_selected) { + if (selection_origin_wrap_index > 0) { + set_selection_origin_line(caret_line, false, selection_origin_wrap_index - 1, new_caret_index); + } else { + int new_line = selection_origin_line - get_next_visible_line_offset_from(selection_origin_line - 1, -1); + if (is_line_wrapped(new_line)) { + set_selection_origin_line(new_line, false, get_line_wrap_count(new_line), new_caret_index); + } else { + set_selection_origin_line(new_line, false, 0, new_caret_index); + } } - } else if (get_selection_line(caret_to_remove) < get_selection_line(caret_to_save) || (get_selection_line(caret_to_remove) == get_selection_line(caret_to_save) && get_selection_column(caret_to_remove) <= get_selection_column(caret_to_save))) { - selection_line = get_selection_line(caret_to_remove); - selection_col = get_selection_column(caret_to_remove); } - - if (get_selection_from_line(caret_to_remove) < get_selection_from_line(caret_to_save) || (get_selection_from_line(caret_to_remove) == get_selection_from_line(caret_to_save) && get_selection_from_column(caret_to_remove) <= get_selection_from_column(caret_to_save))) { - from_col = get_selection_from_column(caret_to_remove); + if (get_caret_line(new_caret_index) < view_line) { + view_line = get_caret_line(new_caret_index); + view_target_caret = new_caret_index; + } + } else { + // Move caret down. + if (caret_wrap_index < get_line_wrap_count(caret_line)) { + set_caret_line(caret_line, false, false, caret_wrap_index + 1, new_caret_index); } else { - to_col = get_selection_to_column(caret_to_remove); + int new_line = caret_line + get_next_visible_line_offset_from(CLAMP(caret_line + 1, 0, text.size() - 1), 1); + set_caret_line(new_line, false, false, 0, new_caret_index); + } + // Move selection origin down. + if (is_selected) { + if (selection_origin_wrap_index < get_line_wrap_count(selection_origin_line)) { + set_selection_origin_line(selection_origin_line, false, selection_origin_wrap_index + 1, new_caret_index); + } else { + int new_line = selection_origin_line + get_next_visible_line_offset_from(CLAMP(selection_origin_line + 1, 0, text.size() - 1), 1); + set_selection_origin_line(new_line, false, 0, new_caret_index); + } + } + if (get_caret_line(new_caret_index) > view_line) { + view_line = get_caret_line(new_caret_index); + view_target_caret = new_caret_index; } + } + if (is_selected) { + // Make sure selection is active. + select(get_selection_origin_line(new_caret_index), get_selection_origin_column(new_caret_index), get_caret_line(new_caret_index), get_caret_column(new_caret_index), new_caret_index); + carets.write[new_caret_index].last_fit_x = carets[i].last_fit_x; + carets.write[new_caret_index].selection.origin_last_fit_x = carets[i].selection.origin_last_fit_x; + } - select(from_line, from_col, to_line, to_col, caret_to_save); - set_selection_mode(selecting_mode, selection_line, selection_col, caret_to_save); - set_caret_line((at_from ? from_line : to_line), caret_to_save == 0, true, 0, caret_to_save); - set_caret_column((at_from ? from_col : to_col), caret_to_save == 0, caret_to_save); - remove_caret(caret_to_remove); - i--; - caret_edit_order = get_caret_index_edit_order(); - continue; + bool check_edges = !has_selection(0) || !has_selection(new_caret_index); + bool will_merge_with_main_caret = _selection_contains(0, get_caret_line(new_caret_index), get_caret_column(new_caret_index), check_edges, false) || _selection_contains(new_caret_index, get_caret_line(0), get_caret_column(0), check_edges, false); + if (will_merge_with_main_caret) { + // Move next to the main caret so it stays the main caret after merging. + Caret new_caret = carets[new_caret_index]; + carets.remove_at(new_caret_index); + carets.insert(0, new_caret); + i++; } + } - // Only first has selection. - if (has_selection(first_caret)) { - if (get_caret_line(second_caret) >= get_selection_from_line(first_caret) && get_caret_line(second_caret) <= get_selection_to_line(first_caret) && (get_caret_line(second_caret) > get_selection_from_line(first_caret) || get_caret_column(second_caret) >= get_selection_from_column(first_caret)) && (get_caret_line(second_caret) < get_selection_to_line(first_caret) || get_caret_column(second_caret) <= get_selection_to_column(first_caret))) { - remove_caret(second_caret); - caret_edit_order = get_caret_index_edit_order(); - i--; - } - continue; + // Show the topmost caret if added above or bottommost caret if added below. + if (view_target_caret >= 0 && view_target_caret < get_caret_count()) { + adjust_viewport_to_caret(view_target_caret); + } + + merge_overlapping_carets(); + end_multicaret_edit(); +} + +struct _CaretSortComparator { + _FORCE_INLINE_ bool operator()(const Vector3i &a, const Vector3i &b) const { + // x is column, y is line, z is caret index. + if (a.y == b.y) { + return a.x < b.x; } + return a.y < b.y; + } +}; - // Only second has selection. - if (has_selection(second_caret)) { - if (get_caret_line(first_caret) >= get_selection_from_line(second_caret) && get_caret_line(first_caret) <= get_selection_to_line(second_caret) && (get_caret_line(first_caret) > get_selection_from_line(second_caret) || get_caret_column(first_caret) >= get_selection_from_column(second_caret)) && (get_caret_line(first_caret) < get_selection_to_line(second_caret) || get_caret_column(first_caret) <= get_selection_to_column(second_caret))) { - remove_caret(first_caret); - caret_edit_order = get_caret_index_edit_order(); - i--; - } +Vector<int> TextEdit::get_sorted_carets(bool p_include_ignored_carets) const { + // Returns caret indexes sorted by selection start or caret position from top to bottom of text. + Vector<Vector3i> caret_line_col_indexes; + for (int i = 0; i < get_caret_count(); i++) { + if (!p_include_ignored_carets && multicaret_edit_ignore_caret(i)) { continue; } + caret_line_col_indexes.push_back(Vector3i(get_selection_from_column(i), get_selection_from_line(i), i)); + } + caret_line_col_indexes.sort_custom<_CaretSortComparator>(); + Vector<int> sorted; + sorted.resize(caret_line_col_indexes.size()); + for (int i = 0; i < caret_line_col_indexes.size(); i++) { + sorted.set(i, caret_line_col_indexes[i].z); + } + return sorted; +} - // Both have no selection. - if (get_caret_line(first_caret) == get_caret_line(second_caret) && get_caret_column(first_caret) == get_caret_column(second_caret)) { - // Save the newest one for Click + Drag. - if (first_caret < second_caret) { - remove_caret(first_caret); - } else { - remove_caret(second_caret); +void TextEdit::collapse_carets(int p_from_line, int p_from_column, int p_to_line, int p_to_column, bool p_inclusive) { + // Collapse carets in the selected range to the from position. + + // Clamp the collapse target position. + int collapse_line = CLAMP(p_from_line, 0, text.size() - 1); + int collapse_column = CLAMP(p_from_column, 0, text[collapse_line].length()); + + // Swap the lines if they are in the wrong order. + if (p_from_line > p_to_line) { + SWAP(p_from_line, p_to_line); + SWAP(p_from_column, p_to_column); + } + if (p_from_line == p_to_line && p_from_column > p_to_column) { + SWAP(p_from_column, p_to_column); + } + bool any_collapsed = false; + + // Intentionally includes carets in the multicaret_edit_ignore list so that they are moved together. + for (int i = 0; i < get_caret_count(); i++) { + bool is_caret_in = _is_line_col_in_range(get_caret_line(i), get_caret_column(i), p_from_line, p_from_column, p_to_line, p_to_column, p_inclusive); + if (!has_selection(i)) { + if (is_caret_in) { + // Caret was in the collapsed area. + set_caret_line(collapse_line, false, true, -1, i); + set_caret_column(collapse_column, false, i); + if (is_in_mulitcaret_edit()) { + multicaret_edit_ignore_carets.insert(i); + } + any_collapsed = true; + } + } else { + bool is_origin_in = _is_line_col_in_range(get_selection_origin_line(i), get_selection_origin_column(i), p_from_line, p_from_column, p_to_line, p_to_column, p_inclusive); + + if (is_caret_in && is_origin_in) { + // Selection was completely encapsulated. + deselect(i); + set_caret_line(collapse_line, false, true, -1, i); + set_caret_column(collapse_column, false, i); + if (is_in_mulitcaret_edit()) { + multicaret_edit_ignore_carets.insert(i); + } + any_collapsed = true; + } else if (is_caret_in) { + // Only caret was inside. + set_caret_line(collapse_line, false, true, -1, i); + set_caret_column(collapse_column, false, i); + any_collapsed = true; + } else if (is_origin_in) { + // Only selection origin was inside. + set_selection_origin_line(collapse_line, true, -1, i); + set_selection_origin_column(collapse_column, i); + any_collapsed = true; } - i--; - caret_edit_order = get_caret_index_edit_order(); - continue; } + if (!p_inclusive && !any_collapsed) { + if ((get_caret_line(i) == collapse_line && get_caret_column(i) == collapse_column) || (get_selection_origin_line(i) == collapse_line && get_selection_origin_column(i) == collapse_column)) { + // Make sure to queue a merge, even if we didn't include it. + any_collapsed = true; + } + } + } + if (any_collapsed) { + merge_overlapping_carets(); } } -int TextEdit::get_caret_count() const { - return carets.size(); -} +void TextEdit::merge_overlapping_carets() { + if (is_in_mulitcaret_edit()) { + // Queue merge to be performed the end of the multicaret edit. + multicaret_edit_merge_queued = true; + return; + } -void TextEdit::add_caret_at_carets(bool p_below) { - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &caret_index : caret_edit_order) { - const int caret_line = get_caret_line(caret_index); - const int caret_column = get_caret_column(caret_index); - - // The last fit x will be cleared if the caret has a selection, - // but if it does not have a selection the last fit x will be - // transferred to the new caret. - int caret_from_column = 0, caret_to_column = 0, caret_last_fit_x = carets[caret_index].last_fit_x; - if (has_selection(caret_index)) { - // If the selection goes over multiple lines, deselect it. - if (get_selection_from_line(caret_index) != get_selection_to_line(caret_index)) { - deselect(caret_index); + multicaret_edit_merge_queued = false; + multicaret_edit_ignore_carets.clear(); + + if (get_caret_count() == 1) { + return; + } + + Vector<int> sorted_carets = get_sorted_carets(true); + for (int i = 0; i < sorted_carets.size() - 1; i++) { + int first_caret = sorted_carets[i]; + int second_caret = sorted_carets[i + 1]; + + bool merge_carets; + if (!has_selection(first_caret) || !has_selection(second_caret)) { + // Merge if touching. + merge_carets = get_selection_from_line(second_caret) < get_selection_to_line(first_caret) || (get_selection_from_line(second_caret) == get_selection_to_line(first_caret) && get_selection_from_column(second_caret) <= get_selection_to_column(first_caret)); + } else { + // Merge two selections if overlapping. + merge_carets = get_selection_from_line(second_caret) < get_selection_to_line(first_caret) || (get_selection_from_line(second_caret) == get_selection_to_line(first_caret) && get_selection_from_column(second_caret) < get_selection_to_column(first_caret)); + } + + if (!merge_carets) { + continue; + } + + // Save the newest one for Click + Drag. + int caret_to_save = first_caret; + int caret_to_remove = second_caret; + if (first_caret < second_caret) { + caret_to_save = second_caret; + caret_to_remove = first_caret; + } + + if (get_selection_from_line(caret_to_save) != get_selection_from_line(caret_to_remove) || get_selection_to_line(caret_to_save) != get_selection_to_line(caret_to_remove) || get_selection_from_column(caret_to_save) != get_selection_from_column(caret_to_remove) || get_selection_to_column(caret_to_save) != get_selection_to_column(caret_to_remove)) { + // Selections are not the same, merge them into one bigger selection. + int new_from_line = MIN(get_selection_from_line(caret_to_remove), get_selection_from_line(caret_to_save)); + int new_to_line = MAX(get_selection_to_line(caret_to_remove), get_selection_to_line(caret_to_save)); + int new_from_col; + int new_to_col; + if (get_selection_from_line(caret_to_remove) < get_selection_from_line(caret_to_save)) { + new_from_col = get_selection_from_column(caret_to_remove); + } else if (get_selection_from_line(caret_to_remove) > get_selection_from_line(caret_to_save)) { + new_from_col = get_selection_from_column(caret_to_save); } else { - caret_from_column = get_selection_from_column(caret_index); - caret_to_column = get_selection_to_column(caret_index); - caret_last_fit_x = -1; - carets.write[caret_index].last_fit_x = _get_column_x_offset_for_line(caret_column, caret_line, caret_column); + new_from_col = MIN(get_selection_from_column(caret_to_remove), get_selection_from_column(caret_to_save)); + } + if (get_selection_to_line(caret_to_remove) < get_selection_to_line(caret_to_save)) { + new_to_col = get_selection_to_column(caret_to_save); + } else if (get_selection_to_line(caret_to_remove) > get_selection_to_line(caret_to_save)) { + new_to_col = get_selection_to_column(caret_to_remove); + } else { + new_to_col = MAX(get_selection_to_column(caret_to_remove), get_selection_to_column(caret_to_save)); } - } - // Get the line and column of the new caret as if you would move the caret by pressing the arrow keys. - int new_caret_line, new_caret_column, new_caret_from_column = 0, new_caret_to_column = 0; - _get_above_below_caret_line_column(caret_line, get_caret_wrap_index(caret_index), caret_column, p_below, new_caret_line, new_caret_column, caret_last_fit_x); + // Use the direction from the last caret or the saved one. + int caret_dir_to_copy; + if (has_selection(caret_to_remove) && has_selection(caret_to_save)) { + caret_dir_to_copy = caret_to_remove == get_caret_count() - 1 ? caret_to_remove : caret_to_save; + } else { + caret_dir_to_copy = !has_selection(caret_to_remove) ? caret_to_save : caret_to_remove; + } - // If the caret does have a selection calculate the new from and to columns. - if (caret_from_column != caret_to_column) { - // We only need to calculate the selection columns if the column of the caret changed. - if (caret_column != new_caret_column) { - int _; // Unused placeholder for p_new_line. - _get_above_below_caret_line_column(caret_line, get_caret_wrap_index(caret_index), caret_from_column, p_below, _, new_caret_from_column); - _get_above_below_caret_line_column(caret_line, get_caret_wrap_index(caret_index), caret_to_column, p_below, _, new_caret_to_column); + if (is_caret_after_selection_origin(caret_dir_to_copy)) { + select(new_from_line, new_from_col, new_to_line, new_to_col, caret_to_save); } else { - new_caret_from_column = caret_from_column; - new_caret_to_column = caret_to_column; + select(new_to_line, new_to_col, new_from_line, new_from_col, caret_to_save); } } - // Add the new caret. - const int new_caret_index = add_caret(new_caret_line, new_caret_column); - - if (new_caret_index == -1) { - continue; + if (caret_to_save == 0) { + adjust_viewport_to_caret(caret_to_save); } - // Also add the selection if there should be one. - if (new_caret_from_column != new_caret_to_column) { - select(new_caret_line, new_caret_from_column, new_caret_line, new_caret_to_column, new_caret_index); - // Necessary to properly modify the selection after adding the new caret. - carets.write[new_caret_index].selection.selecting_line = new_caret_line; - carets.write[new_caret_index].selection.selecting_column = new_caret_column == new_caret_from_column ? new_caret_to_column : new_caret_from_column; - continue; + remove_caret(caret_to_remove); + + // Update the rest of the sorted list. + for (int j = i; j < sorted_carets.size(); j++) { + if (sorted_carets[j] > caret_to_remove) { + // Shift the index since a caret before it was removed. + sorted_carets.write[j] -= 1; + } } + // Remove the caret from the sorted array. + sorted_carets.remove_at(caret_to_remove == first_caret ? i : i + 1); - // Copy the last fit x over. - carets.write[new_caret_index].last_fit_x = carets[caret_index].last_fit_x; + // Process the caret again, since it and the next caret might also overlap. + i--; } +} - merge_overlapping_carets(); - queue_redraw(); +// Starts a multicaret edit operation. Call this before iterating over the carets and call [end_multicaret_edit] afterwards. +void TextEdit::begin_multicaret_edit() { + multicaret_edit_count++; } -Vector<int> TextEdit::get_caret_index_edit_order() { - if (!caret_index_edit_dirty) { - return caret_index_edit_order; +void TextEdit::end_multicaret_edit() { + if (multicaret_edit_count > 0) { + multicaret_edit_count--; + } + if (multicaret_edit_count != 0) { + return; } - caret_index_edit_order.clear(); - caret_index_edit_order.push_back(0); - for (int i = 1; i < carets.size(); i++) { - int j = 0; - - int line = has_selection(i) ? get_selection_to_line(i) : carets[i].line; - int col = has_selection(i) ? get_selection_to_column(i) : carets[i].column; - - for (; j < caret_index_edit_order.size(); j++) { - int idx = caret_index_edit_order[j]; - int other_line = has_selection(idx) ? get_selection_to_line(idx) : carets[idx].line; - int other_col = has_selection(idx) ? get_selection_to_column(idx) : carets[idx].column; - if (line > other_line || (line == other_line && col > other_col)) { - break; - } - } - caret_index_edit_order.insert(j, i); + // This was the last multicaret edit operation. + if (multicaret_edit_merge_queued) { + merge_overlapping_carets(); } - caret_index_edit_dirty = false; - return caret_index_edit_order; + multicaret_edit_ignore_carets.clear(); } -void TextEdit::adjust_carets_after_edit(int p_caret, int p_from_line, int p_from_col, int p_to_line, int p_to_col) { - int edit_height = p_from_line - p_to_line; - int edit_size = ((edit_height == 0) ? p_from_col : 0) - p_to_col; - - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (int j = 0; j < caret_edit_order.size(); j++) { - if (caret_edit_order[j] == p_caret) { - return; - } - - // Adjust caret. - // set_caret_line could adjust the column, so save here. - int cc = get_caret_column(caret_edit_order[j]); - if (edit_height != 0) { - set_caret_line(get_caret_line(caret_edit_order[j]) + edit_height, false, true, 0, caret_edit_order[j]); - } - if (get_caret_line(p_caret) == get_caret_line(caret_edit_order[j])) { - set_caret_column(cc + edit_size, false, caret_edit_order[j]); - } +bool TextEdit::is_in_mulitcaret_edit() const { + return multicaret_edit_count > 0; +} - // Adjust selection. - if (!has_selection(caret_edit_order[j])) { - continue; - } - if (edit_height != 0) { - carets.write[caret_edit_order[j]].selection.from_line += edit_height; - carets.write[caret_edit_order[j]].selection.to_line += edit_height; - } - if (get_caret_line(p_caret) == get_selection_from_line(caret_edit_order[j])) { - carets.write[caret_edit_order[j]].selection.from_column += edit_size; - } - } +bool TextEdit::multicaret_edit_ignore_caret(int p_caret) const { + return multicaret_edit_ignore_carets.has(p_caret); } bool TextEdit::is_caret_visible(int p_caret) const { @@ -4902,16 +4915,10 @@ void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_ } setting_caret_line = true; - if (p_line < 0) { - p_line = 0; - } - - if (p_line >= text.size()) { - p_line = text.size() - 1; - } + p_line = CLAMP(p_line, 0, text.size() - 1); if (!p_can_be_hidden) { - if (_is_line_hidden(CLAMP(p_line, 0, text.size() - 1))) { + if (_is_line_hidden(p_line)) { int move_down = get_next_visible_line_offset_from(p_line, 1) - 1; if (p_line + move_down <= text.size() - 1 && !_is_line_hidden(p_line + move_down)) { p_line += move_down; @@ -4920,7 +4927,7 @@ void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_ if (p_line - move_up > 0 && !_is_line_hidden(p_line - move_up)) { p_line -= move_up; } else { - WARN_PRINT(("Caret set to hidden line " + itos(p_line) + " and there are no nonhidden lines.")); + WARN_PRINT("Caret set to hidden line " + itos(p_line) + " and there are no nonhidden lines."); } } } @@ -4928,31 +4935,36 @@ void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_ bool caret_moved = get_caret_line(p_caret) != p_line; carets.write[p_caret].line = p_line; - int n_col = _get_char_pos_for_line(carets[p_caret].last_fit_x, p_line, p_wrap_index); - if (n_col != 0 && get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE && p_wrap_index < get_line_wrap_count(p_line)) { - Vector<String> rows = get_line_wrapped_text(p_line); - int row_end_col = 0; - for (int i = 0; i < p_wrap_index + 1; i++) { - row_end_col += rows[i].length(); - } - if (n_col >= row_end_col) { - n_col -= 1; + int n_col; + if (p_wrap_index >= 0) { + // Keep caret in same visual x position it was at previously. + n_col = _get_char_pos_for_line(carets[p_caret].last_fit_x, p_line, p_wrap_index); + if (n_col != 0 && get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE && p_wrap_index < get_line_wrap_count(p_line)) { + // Offset by one to not go past the end of the wrapped line. + if (n_col >= text.get_line_wrap_ranges(p_line)[p_wrap_index].y) { + n_col -= 1; + } } + } else { + // Clamp the column. + n_col = MIN(get_caret_column(p_caret), get_line(p_line).length()); } caret_moved = (caret_moved || get_caret_column(p_caret) != n_col); carets.write[p_caret].column = n_col; + // Unselect if the caret moved to the selection origin. + if (p_wrap_index >= 0 && has_selection(p_caret) && get_caret_line(p_caret) == get_selection_origin_line(p_caret) && get_caret_column(p_caret) == get_selection_origin_column(p_caret)) { + deselect(p_caret); + } + if (is_inside_tree() && p_adjust_viewport) { adjust_viewport_to_caret(p_caret); } setting_caret_line = false; - if (caret_moved && !caret_pos_dirty) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); - } - caret_pos_dirty = true; + if (caret_moved) { + _caret_changed(p_caret); } } @@ -4961,29 +4973,32 @@ int TextEdit::get_caret_line(int p_caret) const { return carets[p_caret].line; } -void TextEdit::set_caret_column(int p_col, bool p_adjust_viewport, int p_caret) { +void TextEdit::set_caret_column(int p_column, bool p_adjust_viewport, int p_caret) { ERR_FAIL_INDEX(p_caret, carets.size()); - if (p_col < 0) { - p_col = 0; - } - if (p_col > get_line(get_caret_line(p_caret)).length()) { - p_col = get_line(get_caret_line(p_caret)).length(); - } - bool caret_moved = get_caret_column(p_caret) != p_col; - carets.write[p_caret].column = p_col; + p_column = CLAMP(p_column, 0, get_line(get_caret_line(p_caret)).length()); + + bool caret_moved = get_caret_column(p_caret) != p_column; + carets.write[p_caret].column = p_column; carets.write[p_caret].last_fit_x = _get_column_x_offset_for_line(get_caret_column(p_caret), get_caret_line(p_caret), get_caret_column(p_caret)); + if (!has_selection(p_caret)) { + // Set the selection origin last fit x to be the same, so we can tell if there was a selection. + carets.write[p_caret].selection.origin_last_fit_x = carets[p_caret].last_fit_x; + } + + // Unselect if the caret moved to the selection origin. + if (has_selection(p_caret) && get_caret_line(p_caret) == get_selection_origin_line(p_caret) && get_caret_column(p_caret) == get_selection_origin_column(p_caret)) { + deselect(p_caret); + } + if (is_inside_tree() && p_adjust_viewport) { adjust_viewport_to_caret(p_caret); } - if (caret_moved && !caret_pos_dirty) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); - } - caret_pos_dirty = true; + if (caret_moved) { + _caret_changed(p_caret); } } @@ -4998,7 +5013,7 @@ int TextEdit::get_caret_wrap_index(int p_caret) const { } String TextEdit::get_word_under_caret(int p_caret) const { - ERR_FAIL_COND_V(p_caret > carets.size(), ""); + ERR_FAIL_COND_V(p_caret >= carets.size() || p_caret < -1, ""); StringBuilder selected_text; for (int c = 0; c < carets.size(); c++) { @@ -5059,20 +5074,8 @@ bool TextEdit::is_drag_and_drop_selection_enabled() const { return drag_and_drop_selection_enabled; } -void TextEdit::set_selection_mode(SelectionMode p_mode, int p_line, int p_column, int p_caret) { - ERR_FAIL_INDEX(p_caret, carets.size()); - +void TextEdit::set_selection_mode(SelectionMode p_mode) { selecting_mode = p_mode; - if (p_line >= 0) { - ERR_FAIL_INDEX(p_line, text.size()); - carets.write[p_caret].selection.selecting_line = p_line; - carets.write[p_caret].selection.selecting_column = CLAMP(carets[p_caret].selection.selecting_column, 0, text[carets[p_caret].selection.selecting_line].length()); - } - if (p_column >= 0) { - ERR_FAIL_INDEX(carets[p_caret].selection.selecting_line, text.size()); - ERR_FAIL_INDEX(p_column, text[carets[p_caret].selection.selecting_line].length() + 1); - carets.write[p_caret].selection.selecting_column = p_column; - } } TextEdit::SelectionMode TextEdit::get_selection_mode() const { @@ -5090,16 +5093,12 @@ void TextEdit::select_all() { } remove_secondary_carets(); + set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT); select(0, 0, text.size() - 1, text[text.size() - 1].length()); - set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT, 0, 0); - carets.write[0].selection.shiftclick_left = true; - set_caret_line(get_selection_to_line(), false); - set_caret_column(get_selection_to_column(), false); - queue_redraw(); } void TextEdit::select_word_under_caret(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= carets.size() || p_caret < -1); _push_current_op(); if (!selecting_enabled) { @@ -5140,8 +5139,6 @@ void TextEdit::select_word_under_caret(int p_caret) { } select(get_caret_line(c), begin, get_caret_line(c), end, c); - // Move the caret to the end of the word for easier editing. - set_caret_column(end, false, c); } merge_overlapping_carets(); } @@ -5234,53 +5231,37 @@ void TextEdit::skip_selection_for_next_occurrence() { } } -void TextEdit::select(int p_from_line, int p_from_column, int p_to_line, int p_to_column, int p_caret) { - ERR_FAIL_INDEX(p_caret, carets.size()); +void TextEdit::select(int p_origin_line, int p_origin_column, int p_caret_line, int p_caret_column, int p_caret) { + ERR_FAIL_INDEX(p_caret, get_caret_count()); + + p_caret_line = CLAMP(p_caret_line, 0, text.size() - 1); + p_caret_column = CLAMP(p_caret_column, 0, text[p_caret_line].length()); + set_caret_line(p_caret_line, false, true, -1, p_caret); + set_caret_column(p_caret_column, false, p_caret); + if (!selecting_enabled) { return; } - p_from_line = CLAMP(p_from_line, 0, text.size() - 1); - p_from_column = CLAMP(p_from_column, 0, text[p_from_line].length()); - p_to_line = CLAMP(p_to_line, 0, text.size() - 1); - p_to_column = CLAMP(p_to_column, 0, text[p_to_line].length()); - - carets.write[p_caret].selection.from_line = p_from_line; - carets.write[p_caret].selection.from_column = p_from_column; - carets.write[p_caret].selection.to_line = p_to_line; - carets.write[p_caret].selection.to_column = p_to_column; + p_origin_line = CLAMP(p_origin_line, 0, text.size() - 1); + p_origin_column = CLAMP(p_origin_column, 0, text[p_origin_line].length()); + set_selection_origin_line(p_origin_line, true, -1, p_caret); + set_selection_origin_column(p_origin_column, p_caret); - carets.write[p_caret].selection.active = true; - - if (get_selection_from_line(p_caret) == get_selection_to_line(p_caret)) { - if (get_selection_from_column(p_caret) == get_selection_to_column(p_caret)) { - carets.write[p_caret].selection.active = false; - - } else if (get_selection_from_column(p_caret) > get_selection_to_column(p_caret)) { - carets.write[p_caret].selection.shiftclick_left = false; - SWAP(carets.write[p_caret].selection.from_column, carets.write[p_caret].selection.to_column); - } else { - carets.write[p_caret].selection.shiftclick_left = true; - } - } else if (get_selection_from_line(p_caret) > get_selection_to_line(p_caret)) { - carets.write[p_caret].selection.shiftclick_left = false; - SWAP(carets.write[p_caret].selection.from_line, carets.write[p_caret].selection.to_line); - SWAP(carets.write[p_caret].selection.from_column, carets.write[p_caret].selection.to_column); - } else { - carets.write[p_caret].selection.shiftclick_left = true; + bool had_selection = has_selection(p_caret); + bool activate = p_origin_line != p_caret_line || p_origin_column != p_caret_column; + carets.write[p_caret].selection.active = activate; + if (had_selection != activate) { + _selection_changed(p_caret); } - - caret_index_edit_dirty = true; - queue_redraw(); } bool TextEdit::has_selection(int p_caret) const { - ERR_FAIL_COND_V(p_caret > carets.size(), false); + ERR_FAIL_COND_V(p_caret >= carets.size() || p_caret < -1, false); + if (p_caret >= 0) { + return carets[p_caret].selection.active; + } for (int i = 0; i < carets.size(); i++) { - if (p_caret != -1 && p_caret != i) { - continue; - } - if (carets[i].selection.active) { return true; } @@ -5289,100 +5270,268 @@ bool TextEdit::has_selection(int p_caret) const { } String TextEdit::get_selected_text(int p_caret) { - ERR_FAIL_COND_V(p_caret > carets.size(), ""); + ERR_FAIL_COND_V(p_caret >= carets.size() || p_caret < -1, ""); - StringBuilder selected_text; - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (int i = caret_edit_order.size() - 1; i >= 0; i--) { - int caret_idx = caret_edit_order[i]; - if (p_caret != -1 && p_caret != caret_idx) { - continue; + if (p_caret >= 0) { + if (!has_selection(p_caret)) { + return ""; } + return _base_get_text(get_selection_from_line(p_caret), get_selection_from_column(p_caret), get_selection_to_line(p_caret), get_selection_to_column(p_caret)); + } + + StringBuilder selected_text; + Vector<int> sorted_carets = get_sorted_carets(); + for (int i = 0; i < sorted_carets.size(); i++) { + int caret_index = sorted_carets[i]; - if (!has_selection(caret_idx)) { + if (!has_selection(caret_index)) { continue; } - selected_text += _base_get_text(get_selection_from_line(caret_idx), get_selection_from_column(caret_idx), get_selection_to_line(caret_idx), get_selection_to_column(caret_idx)); - if (p_caret == -1 && i != 0) { + if (selected_text.get_string_length() != 0) { selected_text += "\n"; } + selected_text += _base_get_text(get_selection_from_line(caret_index), get_selection_from_column(caret_index), get_selection_to_line(caret_index), get_selection_to_column(caret_index)); } return selected_text.as_string(); } -int TextEdit::get_selection_line(int p_caret) const { +int TextEdit::get_selection_at_line_column(int p_line, int p_column, bool p_include_edges, bool p_only_selections) const { + // Return the caret index of the found selection, or -1. + for (int i = 0; i < get_caret_count(); i++) { + if (_selection_contains(i, p_line, p_column, p_include_edges, p_only_selections)) { + return i; + } + } + return -1; +} + +Vector<Point2i> TextEdit::get_line_ranges_from_carets(bool p_only_selections, bool p_merge_adjacent) const { + // Get a series of line ranges that cover all lines that have a caret or selection. + // For each Point2i range, x is the first line and y is the last line. + Vector<Point2i> ret; + int last_to_line = INT_MIN; + + Vector<int> sorted_carets = get_sorted_carets(); + for (int i = 0; i < sorted_carets.size(); i++) { + int caret_index = sorted_carets[i]; + if (p_only_selections && !has_selection(caret_index)) { + continue; + } + Point2i range = Point2i(get_selection_from_line(caret_index), get_selection_to_line(caret_index)); + if (has_selection(caret_index) && get_selection_to_column(caret_index) == 0) { + // Dont include selection end line if it ends at column 0. + range.y--; + } + if (range.x == last_to_line || (p_merge_adjacent && range.x - 1 == last_to_line)) { + // Merge if starts on the same line or adjacent line. + ret.write[ret.size() - 1].y = range.y; + } else { + ret.append(range); + } + last_to_line = range.y; + } + return ret; +} + +TypedArray<Vector2i> TextEdit::get_line_ranges_from_carets_typed_array(bool p_only_selections, bool p_merge_adjacent) const { + // Wrapper for `get_line_ranges_from_carets` to return a datatype that can be exposed. + TypedArray<Vector2i> ret; + Vector<Point2i> ranges = get_line_ranges_from_carets(p_only_selections, p_merge_adjacent); + for (const Point2i &range : ranges) { + ret.push_back(range); + } + return ret; +} + +void TextEdit::set_selection_origin_line(int p_line, bool p_can_be_hidden, int p_wrap_index, int p_caret) { + if (!selecting_enabled) { + return; + } + ERR_FAIL_INDEX(p_caret, carets.size()); + p_line = CLAMP(p_line, 0, text.size() - 1); + + if (!p_can_be_hidden) { + if (_is_line_hidden(p_line)) { + int move_down = get_next_visible_line_offset_from(p_line, 1) - 1; + if (p_line + move_down <= text.size() - 1 && !_is_line_hidden(p_line + move_down)) { + p_line += move_down; + } else { + int move_up = get_next_visible_line_offset_from(p_line, -1) - 1; + if (p_line - move_up > 0 && !_is_line_hidden(p_line - move_up)) { + p_line -= move_up; + } else { + WARN_PRINT("Selection origin set to hidden line " + itos(p_line) + " and there are no nonhidden lines."); + } + } + } + } + + bool selection_moved = get_selection_origin_line(p_caret) != p_line; + carets.write[p_caret].selection.origin_line = p_line; + + int n_col; + if (p_wrap_index >= 0) { + // Keep selection origin in same visual x position it was at previously. + n_col = _get_char_pos_for_line(carets[p_caret].selection.origin_last_fit_x, p_line, p_wrap_index); + if (n_col != 0 && get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE && p_wrap_index < get_line_wrap_count(p_line)) { + // Offset by one to not go past the end of the wrapped line. + if (n_col >= text.get_line_wrap_ranges(p_line)[p_wrap_index].y) { + n_col -= 1; + } + } + } else { + // Clamp the column. + n_col = MIN(get_selection_origin_column(p_caret), get_line(p_line).length()); + } + selection_moved = (selection_moved || get_selection_origin_column(p_caret) != n_col); + carets.write[p_caret].selection.origin_column = n_col; + + // Unselect if the selection origin moved to the caret. + if (p_wrap_index >= 0 && has_selection(p_caret) && get_caret_line(p_caret) == get_selection_origin_line(p_caret) && get_caret_column(p_caret) == get_selection_origin_column(p_caret)) { + deselect(p_caret); + } + + if (selection_moved && has_selection(p_caret)) { + _selection_changed(p_caret); + } +} + +void TextEdit::set_selection_origin_column(int p_column, int p_caret) { + if (!selecting_enabled) { + return; + } + ERR_FAIL_INDEX(p_caret, carets.size()); + + p_column = CLAMP(p_column, 0, get_line(get_selection_origin_line(p_caret)).length()); + + bool selection_moved = get_selection_origin_column(p_caret) != p_column; + + carets.write[p_caret].selection.origin_column = p_column; + + carets.write[p_caret].selection.origin_last_fit_x = _get_column_x_offset_for_line(get_selection_origin_column(p_caret), get_selection_origin_line(p_caret), get_selection_origin_column(p_caret)); + + // Unselect if the selection origin moved to the caret. + if (has_selection(p_caret) && get_caret_line(p_caret) == get_selection_origin_line(p_caret) && get_caret_column(p_caret) == get_selection_origin_column(p_caret)) { + deselect(p_caret); + } + + if (selection_moved && has_selection(p_caret)) { + _selection_changed(p_caret); + } +} + +int TextEdit::get_selection_origin_line(int p_caret) const { ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); - ERR_FAIL_COND_V(!has_selection(p_caret), -1); - return carets[p_caret].selection.selecting_line; + return carets[p_caret].selection.origin_line; } -int TextEdit::get_selection_column(int p_caret) const { +int TextEdit::get_selection_origin_column(int p_caret) const { ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); - ERR_FAIL_COND_V(!has_selection(p_caret), -1); - return carets[p_caret].selection.selecting_column; + return carets[p_caret].selection.origin_column; } int TextEdit::get_selection_from_line(int p_caret) const { ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); - ERR_FAIL_COND_V(!has_selection(p_caret), -1); - return carets[p_caret].selection.from_line; + if (!has_selection(p_caret)) { + return carets[p_caret].line; + } + return MIN(carets[p_caret].selection.origin_line, carets[p_caret].line); } int TextEdit::get_selection_from_column(int p_caret) const { ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); - ERR_FAIL_COND_V(!has_selection(p_caret), -1); - return carets[p_caret].selection.from_column; + if (!has_selection(p_caret)) { + return carets[p_caret].column; + } + if (carets[p_caret].selection.origin_line < carets[p_caret].line) { + return carets[p_caret].selection.origin_column; + } else if (carets[p_caret].selection.origin_line > carets[p_caret].line) { + return carets[p_caret].column; + } else { + return MIN(carets[p_caret].selection.origin_column, carets[p_caret].column); + } } int TextEdit::get_selection_to_line(int p_caret) const { ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); - ERR_FAIL_COND_V(!has_selection(p_caret), -1); - return carets[p_caret].selection.to_line; + if (!has_selection(p_caret)) { + return carets[p_caret].line; + } + return MAX(carets[p_caret].selection.origin_line, carets[p_caret].line); } int TextEdit::get_selection_to_column(int p_caret) const { ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); - ERR_FAIL_COND_V(!has_selection(p_caret), -1); - return carets[p_caret].selection.to_column; + if (!has_selection(p_caret)) { + return carets[p_caret].column; + } + if (carets[p_caret].selection.origin_line < carets[p_caret].line) { + return carets[p_caret].column; + } else if (carets[p_caret].selection.origin_line > carets[p_caret].line) { + return carets[p_caret].selection.origin_column; + } else { + return MAX(carets[p_caret].selection.origin_column, carets[p_caret].column); + } +} + +bool TextEdit::is_caret_after_selection_origin(int p_caret) const { + ERR_FAIL_INDEX_V(p_caret, carets.size(), false); + if (!has_selection(p_caret)) { + return true; + } + return carets[p_caret].line > carets[p_caret].selection.origin_line || (carets[p_caret].line == carets[p_caret].selection.origin_line && carets[p_caret].column >= carets[p_caret].selection.origin_column); } void TextEdit::deselect(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); - for (int i = 0; i < carets.size(); i++) { - if (p_caret != -1 && p_caret != i) { - continue; + ERR_FAIL_COND(p_caret >= carets.size() || p_caret < -1); + bool selection_changed = false; + if (p_caret >= 0) { + selection_changed = carets.write[p_caret].selection.active; + carets.write[p_caret].selection.active = false; + } else { + for (int i = 0; i < carets.size(); i++) { + selection_changed |= carets.write[i].selection.active; + carets.write[i].selection.active = false; } - carets.write[i].selection.active = false; } - caret_index_edit_dirty = true; - queue_redraw(); + if (selection_changed) { + _selection_changed(p_caret); + } } void TextEdit::delete_selection(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { if (p_caret != -1 && p_caret != i) { continue; } + if (p_caret == -1 && multicaret_edit_ignore_caret(i)) { + continue; + } if (!has_selection(i)) { continue; } - selecting_mode = SelectionMode::SELECTION_MODE_NONE; - _remove_text(get_selection_from_line(i), get_selection_from_column(i), get_selection_to_line(i), get_selection_to_column(i)); - set_caret_line(get_selection_from_line(i), false, false, 0, i); - set_caret_column(get_selection_from_column(i), i == 0, i); - carets.write[i].selection.active = false; + int selection_from_line = get_selection_from_line(i); + int selection_from_column = get_selection_from_column(i); + int selection_to_line = get_selection_to_line(i); + int selection_to_column = get_selection_to_column(i); + + _remove_text(selection_from_line, selection_from_column, selection_to_line, selection_to_column); + _offset_carets_after(selection_to_line, selection_to_column, selection_from_line, selection_from_column); + merge_overlapping_carets(); - adjust_carets_after_edit(i, carets[i].selection.from_line, carets[i].selection.from_column, carets[i].selection.to_line, carets[i].selection.to_column); + deselect(i); + set_caret_line(selection_from_line, false, false, -1, i); + set_caret_column(selection_from_column, i == 0, i); } + end_multicaret_edit(); end_complex_operation(); - queue_redraw(); } /* Line wrapping. */ @@ -6224,8 +6373,10 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("swap_lines", "from_line", "to_line"), &TextEdit::swap_lines); ClassDB::bind_method(D_METHOD("insert_line_at", "line", "text"), &TextEdit::insert_line_at); + ClassDB::bind_method(D_METHOD("remove_line_at", "line", "move_carets_down"), &TextEdit::remove_line_at, DEFVAL(true)); ClassDB::bind_method(D_METHOD("insert_text_at_caret", "text", "caret_index"), &TextEdit::insert_text_at_caret, DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("insert_text", "text", "line", "column", "before_selection_begin", "before_selection_end"), &TextEdit::insert_text, DEFVAL(true), DEFVAL(false)); ClassDB::bind_method(D_METHOD("remove_text", "from_line", "from_column", "to_line", "to_column"), &TextEdit::remove_text); ClassDB::bind_method(D_METHOD("get_last_unhidden_line"), &TextEdit::get_last_unhidden_line); @@ -6311,7 +6462,7 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_search_text", "search_text"), &TextEdit::set_search_text); ClassDB::bind_method(D_METHOD("set_search_flags", "flags"), &TextEdit::set_search_flags); - ClassDB::bind_method(D_METHOD("search", "text", "flags", "from_line", "from_colum"), &TextEdit::search); + ClassDB::bind_method(D_METHOD("search", "text", "flags", "from_line", "from_column"), &TextEdit::search); /* Tooltip */ ClassDB::bind_method(D_METHOD("set_tooltip_request_func", "callback"), &TextEdit::set_tooltip_request_func); @@ -6355,15 +6506,20 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_multiple_carets_enabled", "enabled"), &TextEdit::set_multiple_carets_enabled); ClassDB::bind_method(D_METHOD("is_multiple_carets_enabled"), &TextEdit::is_multiple_carets_enabled); - ClassDB::bind_method(D_METHOD("add_caret", "line", "col"), &TextEdit::add_caret); + ClassDB::bind_method(D_METHOD("add_caret", "line", "column"), &TextEdit::add_caret); ClassDB::bind_method(D_METHOD("remove_caret", "caret"), &TextEdit::remove_caret); ClassDB::bind_method(D_METHOD("remove_secondary_carets"), &TextEdit::remove_secondary_carets); - ClassDB::bind_method(D_METHOD("merge_overlapping_carets"), &TextEdit::merge_overlapping_carets); ClassDB::bind_method(D_METHOD("get_caret_count"), &TextEdit::get_caret_count); ClassDB::bind_method(D_METHOD("add_caret_at_carets", "below"), &TextEdit::add_caret_at_carets); - ClassDB::bind_method(D_METHOD("get_caret_index_edit_order"), &TextEdit::get_caret_index_edit_order); - ClassDB::bind_method(D_METHOD("adjust_carets_after_edit", "caret", "from_line", "from_col", "to_line", "to_col"), &TextEdit::adjust_carets_after_edit); + ClassDB::bind_method(D_METHOD("get_sorted_carets", "include_ignored_carets"), &TextEdit::get_sorted_carets, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("collapse_carets", "from_line", "from_column", "to_line", "to_column", "inclusive"), &TextEdit::collapse_carets, DEFVAL(false)); + + ClassDB::bind_method(D_METHOD("merge_overlapping_carets"), &TextEdit::merge_overlapping_carets); + ClassDB::bind_method(D_METHOD("begin_multicaret_edit"), &TextEdit::begin_multicaret_edit); + ClassDB::bind_method(D_METHOD("end_multicaret_edit"), &TextEdit::end_multicaret_edit); + ClassDB::bind_method(D_METHOD("is_in_mulitcaret_edit"), &TextEdit::is_in_mulitcaret_edit); + ClassDB::bind_method(D_METHOD("multicaret_edit_ignore_caret", "caret_index"), &TextEdit::multicaret_edit_ignore_caret); ClassDB::bind_method(D_METHOD("is_caret_visible", "caret_index"), &TextEdit::is_caret_visible, DEFVAL(0)); ClassDB::bind_method(D_METHOD("get_caret_draw_pos", "caret_index"), &TextEdit::get_caret_draw_pos, DEFVAL(0)); @@ -6394,27 +6550,33 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_drag_and_drop_selection_enabled", "enable"), &TextEdit::set_drag_and_drop_selection_enabled); ClassDB::bind_method(D_METHOD("is_drag_and_drop_selection_enabled"), &TextEdit::is_drag_and_drop_selection_enabled); - ClassDB::bind_method(D_METHOD("set_selection_mode", "mode", "line", "column", "caret_index"), &TextEdit::set_selection_mode, DEFVAL(-1), DEFVAL(-1), DEFVAL(0)); + ClassDB::bind_method(D_METHOD("set_selection_mode", "mode"), &TextEdit::set_selection_mode); ClassDB::bind_method(D_METHOD("get_selection_mode"), &TextEdit::get_selection_mode); ClassDB::bind_method(D_METHOD("select_all"), &TextEdit::select_all); ClassDB::bind_method(D_METHOD("select_word_under_caret", "caret_index"), &TextEdit::select_word_under_caret, DEFVAL(-1)); ClassDB::bind_method(D_METHOD("add_selection_for_next_occurrence"), &TextEdit::add_selection_for_next_occurrence); ClassDB::bind_method(D_METHOD("skip_selection_for_next_occurrence"), &TextEdit::skip_selection_for_next_occurrence); - ClassDB::bind_method(D_METHOD("select", "from_line", "from_column", "to_line", "to_column", "caret_index"), &TextEdit::select, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("select", "origin_line", "origin_column", "caret_line", "caret_column", "caret_index"), &TextEdit::select, DEFVAL(0)); ClassDB::bind_method(D_METHOD("has_selection", "caret_index"), &TextEdit::has_selection, DEFVAL(-1)); ClassDB::bind_method(D_METHOD("get_selected_text", "caret_index"), &TextEdit::get_selected_text, DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("get_selection_at_line_column", "line", "column", "include_edges", "only_selections"), &TextEdit::get_selection_at_line_column, DEFVAL(true), DEFVAL(true)); + ClassDB::bind_method(D_METHOD("get_line_ranges_from_carets", "only_selections", "merge_adjacent"), &TextEdit::get_line_ranges_from_carets_typed_array, DEFVAL(false), DEFVAL(true)); - ClassDB::bind_method(D_METHOD("get_selection_line", "caret_index"), &TextEdit::get_selection_line, DEFVAL(0)); - ClassDB::bind_method(D_METHOD("get_selection_column", "caret_index"), &TextEdit::get_selection_column, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("get_selection_origin_line", "caret_index"), &TextEdit::get_selection_origin_line, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("get_selection_origin_column", "caret_index"), &TextEdit::get_selection_origin_column, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("set_selection_origin_line", "line", "can_be_hidden", "wrap_index", "caret_index"), &TextEdit::set_selection_origin_line, DEFVAL(true), DEFVAL(-1), DEFVAL(0)); + ClassDB::bind_method(D_METHOD("set_selection_origin_column", "column", "caret_index"), &TextEdit::set_selection_origin_column, DEFVAL(0)); ClassDB::bind_method(D_METHOD("get_selection_from_line", "caret_index"), &TextEdit::get_selection_from_line, DEFVAL(0)); ClassDB::bind_method(D_METHOD("get_selection_from_column", "caret_index"), &TextEdit::get_selection_from_column, DEFVAL(0)); ClassDB::bind_method(D_METHOD("get_selection_to_line", "caret_index"), &TextEdit::get_selection_to_line, DEFVAL(0)); ClassDB::bind_method(D_METHOD("get_selection_to_column", "caret_index"), &TextEdit::get_selection_to_column, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("is_caret_after_selection_origin", "caret_index"), &TextEdit::is_caret_after_selection_origin, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("deselect", "caret_index"), &TextEdit::deselect, DEFVAL(-1)); ClassDB::bind_method(D_METHOD("delete_selection", "caret_index"), &TextEdit::delete_selection, DEFVAL(-1)); @@ -6550,6 +6712,14 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("is_menu_visible"), &TextEdit::is_menu_visible); ClassDB::bind_method(D_METHOD("menu_option", "option"), &TextEdit::menu_option); + /* Deprecated */ +#ifndef DISABLE_DEPRECATED + ClassDB::bind_method(D_METHOD("adjust_carets_after_edit", "caret", "from_line", "from_col", "to_line", "to_col"), &TextEdit::adjust_carets_after_edit); + ClassDB::bind_method(D_METHOD("get_caret_index_edit_order"), &TextEdit::get_caret_index_edit_order); + ClassDB::bind_method(D_METHOD("get_selection_line", "caret_index"), &TextEdit::get_selection_line, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("get_selection_column", "caret_index"), &TextEdit::get_selection_column, DEFVAL(0)); +#endif + /* Inspector */ ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_MULTILINE_TEXT), "set_text", "get_text"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "placeholder_text", PROPERTY_HINT_MULTILINE_TEXT), "set_placeholder", "get_placeholder"); @@ -6617,7 +6787,7 @@ void TextEdit::_bind_methods() { ADD_SIGNAL(MethodInfo("gutter_added")); ADD_SIGNAL(MethodInfo("gutter_removed")); - /* Theme items */ + // Theme items /* Search */ BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, TextEdit, search_result_color); BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, TextEdit, search_result_border_color); @@ -6690,6 +6860,10 @@ void TextEdit::_unhide_all_lines() { queue_redraw(); } +void TextEdit::_unhide_carets() { + // Override for functionality. +} + void TextEdit::_set_line_as_hidden(int p_line, bool p_hidden) { ERR_FAIL_INDEX(p_line, text.size()); @@ -6717,14 +6891,17 @@ void TextEdit::_set_symbol_lookup_word(const String &p_symbol) { // Overridable actions void TextEdit::_handle_unicode_input_internal(const uint32_t p_unicode, int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); if (!editable) { return; } start_action(EditAction::ACTION_TYPING); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { + if (p_caret == -1 && multicaret_edit_ignore_caret(i)) { + continue; + } if (p_caret != -1 && p_caret != i) { continue; } @@ -6742,11 +6919,12 @@ void TextEdit::_handle_unicode_input_internal(const uint32_t p_unicode, int p_ca const char32_t chr[2] = { (char32_t)p_unicode, 0 }; insert_text_at_caret(chr, i); } + end_multicaret_edit(); end_action(); } void TextEdit::_backspace_internal(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); if (!editable) { return; } @@ -6757,194 +6935,163 @@ void TextEdit::_backspace_internal(int p_caret) { } begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { + if (p_caret == -1 && multicaret_edit_ignore_caret(i)) { + continue; + } if (p_caret != -1 && p_caret != i) { continue; } - int cc = get_caret_column(i); - int cl = get_caret_line(i); + int to_line = get_caret_line(i); + int to_column = get_caret_column(i); - if (cc == 0 && cl == 0) { + if (to_column == 0 && to_line == 0) { continue; } - int prev_line = cc ? cl : cl - 1; - int prev_column = cc ? (cc - 1) : (text[cl - 1].length()); + int from_line = to_column > 0 ? to_line : to_line - 1; + int from_column = to_column > 0 ? (to_column - 1) : (text[to_line - 1].length()); - merge_gutters(prev_line, cl); + merge_gutters(from_line, to_line); - if (_is_line_hidden(cl)) { - _set_line_as_hidden(prev_line, true); - } - _remove_text(prev_line, prev_column, cl, cc); - - set_caret_line(prev_line, false, true, 0, i); - set_caret_column(prev_column, i == 0, i); + _remove_text(from_line, from_column, to_line, to_column); + collapse_carets(from_line, from_column, to_line, to_column); + _offset_carets_after(to_line, to_column, from_line, from_column); - adjust_carets_after_edit(i, prev_line, prev_column, cl, cc); + set_caret_line(from_line, false, true, -1, i); + set_caret_column(from_column, i == 0, i); } - merge_overlapping_carets(); + end_multicaret_edit(); end_complex_operation(); } void TextEdit::_cut_internal(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); + + _copy_internal(p_caret); + if (!editable) { return; } if (has_selection(p_caret)) { - DisplayServer::get_singleton()->clipboard_set(get_selected_text(p_caret)); delete_selection(p_caret); - cut_copy_line = ""; return; } + // Remove full lines. begin_complex_operation(); - Vector<int> carets_to_remove; - - StringBuilder clipboard; - // This is the exception and has to edit in reverse order else the string copied to the clipboard will be backwards. - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (int i = caret_edit_order.size() - 1; i >= 0; i--) { - int caret_idx = caret_edit_order[i]; - if (p_caret != -1 && p_caret != caret_idx) { - continue; - } - - int cl = get_caret_line(caret_idx); - int cc = get_caret_column(caret_idx); - int indent_level = get_indent_level(cl); - double hscroll = get_h_scroll(); - - // Check for overlapping carets. - // We don't need to worry about selections as that is caught before this entire section. - for (int j = i - 1; j >= 0; j--) { - if (get_caret_line(caret_edit_order[j]) == cl) { - carets_to_remove.push_back(caret_edit_order[j]); - i = j; - } - } - - clipboard += text[cl]; - if (p_caret == -1 && caret_idx != 0) { - clipboard += "\n"; - } - - if (cl == 0 && get_line_count() > 1) { - _remove_text(cl, 0, cl + 1, 0); - adjust_carets_after_edit(caret_idx, cl, 0, cl + 1, text[cl].length()); - } else { - _remove_text(cl, 0, cl, text[cl].length()); - set_caret_column(0, false, caret_idx); - backspace(caret_idx); - set_caret_line(get_caret_line(caret_idx) + 1, caret_idx == 0, 0, 0, caret_idx); - } - - // Correct the visually perceived caret column taking care of indentation level of the lines. - int diff_indent = indent_level - get_indent_level(get_caret_line(caret_idx)); - cc += diff_indent; - if (diff_indent != 0) { - cc += diff_indent > 0 ? -1 : 1; - } - - // Restore horizontal scroll and caret column modified by the backspace() call. - set_h_scroll(hscroll); - set_caret_column(cc, caret_idx == 0, caret_idx); + begin_multicaret_edit(); + Vector<Point2i> line_ranges; + if (p_caret == -1) { + line_ranges = get_line_ranges_from_carets(); + } else { + line_ranges.push_back(Point2i(get_caret_line(p_caret), get_caret_line(p_caret))); } - - // Sort and remove backwards to preserve indexes. - carets_to_remove.sort(); - for (int i = carets_to_remove.size() - 1; i >= 0; i--) { - remove_caret(carets_to_remove[i]); + int line_offset = 0; + for (Point2i line_range : line_ranges) { + // Preserve carets on the last line. + remove_line_at(line_range.y + line_offset); + if (line_range.x != line_range.y) { + remove_text(line_range.x + line_offset, 0, line_range.y + line_offset, 0); + } + line_offset += line_range.x - line_range.y - 1; } + end_multicaret_edit(); end_complex_operation(); - - String clipboard_string = clipboard.as_string(); - DisplayServer::get_singleton()->clipboard_set(clipboard_string); - cut_copy_line = clipboard_string; } void TextEdit::_copy_internal(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); if (has_selection(p_caret)) { DisplayServer::get_singleton()->clipboard_set(get_selected_text(p_caret)); cut_copy_line = ""; return; } + // Copy full lines. StringBuilder clipboard; - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (int i = caret_edit_order.size() - 1; i >= 0; i--) { - int caret_idx = caret_edit_order[i]; - if (p_caret != -1 && p_caret != caret_idx) { - continue; - } - - int cl = get_caret_line(caret_idx); - if (text[cl].length() != 0) { - clipboard += _base_get_text(cl, 0, cl, text[cl].length()); - if (p_caret == -1 && i != 0) { - clipboard += "\n"; + Vector<Point2i> line_ranges; + if (p_caret == -1) { + // When there are multiple carets on a line, only copy it once. + line_ranges = get_line_ranges_from_carets(false, true); + } else { + line_ranges.push_back(Point2i(get_caret_line(p_caret), get_caret_line(p_caret))); + } + for (Point2i line_range : line_ranges) { + for (int i = line_range.x; i <= line_range.y; i++) { + if (text[i].length() != 0) { + clipboard += _base_get_text(i, 0, i, text[i].length()); } + clipboard += "\n"; } } String clipboard_string = clipboard.as_string(); DisplayServer::get_singleton()->clipboard_set(clipboard_string); - cut_copy_line = clipboard_string; + // Set the cut copy line so we know to paste as a line. + if (get_caret_count() == 1) { + cut_copy_line = clipboard_string; + } else { + cut_copy_line = ""; + } } void TextEdit::_paste_internal(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); if (!editable) { return; } String clipboard = DisplayServer::get_singleton()->clipboard_get(); + + // Paste a full line. Ignore '\r' characters that may have been added to the clipboard by the OS. + if (get_caret_count() == 1 && !has_selection(0) && !cut_copy_line.is_empty() && cut_copy_line == clipboard.replace("\r", "")) { + insert_text(clipboard, get_caret_line(), 0); + return; + } + + // Paste text at each caret or one line per caret. Vector<String> clipboad_lines = clipboard.split("\n"); - bool insert_line_per_caret = p_caret == -1 && carets.size() > 1 && clipboad_lines.size() == carets.size(); + bool insert_line_per_caret = p_caret == -1 && get_caret_count() > 1 && clipboad_lines.size() == get_caret_count(); begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - int clipboad_line = clipboad_lines.size() - 1; - for (const int &i : caret_edit_order) { - if (p_caret != -1 && p_caret != i) { + begin_multicaret_edit(); + Vector<int> sorted_carets = get_sorted_carets(); + for (int i = 0; i < get_caret_count(); i++) { + int caret_index = sorted_carets[i]; + if (p_caret != -1 && p_caret != caret_index) { continue; } - if (has_selection(i)) { - delete_selection(i); - } else if (!cut_copy_line.is_empty() && cut_copy_line == clipboard) { - set_caret_column(0, i == 0, i); - String ins = "\n"; - clipboard += ins; + if (has_selection(caret_index)) { + delete_selection(caret_index); } if (insert_line_per_caret) { - clipboard = clipboad_lines[clipboad_line]; + clipboard = clipboad_lines[i]; } - insert_text_at_caret(clipboard, i); - clipboad_line--; + insert_text_at_caret(clipboard, caret_index); } + end_multicaret_edit(); end_complex_operation(); } void TextEdit::_paste_primary_clipboard_internal(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); if (!is_editable() || !DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) { return; } String paste_buffer = DisplayServer::get_singleton()->clipboard_get_primary(); - if (carets.size() == 1) { + if (get_caret_count() == 1) { Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); deselect(); - set_caret_line(pos.y, true, false); + set_caret_line(pos.y, true, false, -1); set_caret_column(pos.x); } @@ -7203,10 +7350,26 @@ int TextEdit::_get_char_pos_for_line(int p_px, int p_line, int p_wrap_index) con } /* Caret */ +void TextEdit::_caret_changed(int p_caret) { + queue_redraw(); + + if (has_selection(p_caret)) { + _selection_changed(p_caret); + } + + if (caret_pos_dirty) { + return; + } + + if (is_inside_tree()) { + callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); + } + caret_pos_dirty = true; +} + void TextEdit::_emit_caret_changed() { emit_signal(SNAME("caret_changed")); caret_pos_dirty = false; - caret_index_edit_dirty = true; } void TextEdit::_reset_caret_blink_timer() { @@ -7251,60 +7414,152 @@ int TextEdit::_get_column_x_offset_for_line(int p_char, int p_line, int p_column } } -/* Selection */ -void TextEdit::_click_selection_held() { - // Warning: is_mouse_button_pressed(MouseButton::LEFT) returns false for double+ clicks, so this doesn't work for MODE_WORD - // and MODE_LINE. However, moving the mouse triggers _gui_input, which calls these functions too, so that's not a huge problem. - // I'm unsure if there's an actual fix that doesn't have a ton of side effects. - if (Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT) && get_selection_mode() != SelectionMode::SELECTION_MODE_NONE) { - switch (get_selection_mode()) { - case SelectionMode::SELECTION_MODE_POINTER: { - _update_selection_mode_pointer(); - } break; - case SelectionMode::SELECTION_MODE_WORD: { - _update_selection_mode_word(); - } break; - case SelectionMode::SELECTION_MODE_LINE: { - _update_selection_mode_line(); - } break; - default: { - break; +bool TextEdit::_is_line_col_in_range(int p_line, int p_column, int p_from_line, int p_from_column, int p_to_line, int p_to_column, bool p_include_edges) const { + if (p_line >= p_from_line && p_line <= p_to_line && (p_line > p_from_line || p_column > p_from_column) && (p_line < p_to_line || p_column < p_to_column)) { + return true; + } + if (p_include_edges) { + if ((p_line == p_from_line && p_column == p_from_column) || (p_line == p_to_line && p_column == p_to_column)) { + return true; + } + } + return false; +} + +void TextEdit::_offset_carets_after(int p_old_line, int p_old_column, int p_new_line, int p_new_column, bool p_include_selection_begin, bool p_include_selection_end) { + // Moves all carets at or after old_line and old_column. + // Called after deleting or inserting text so that the carets stay with the text they are at. + + int edit_height = p_new_line - p_old_line; + int edit_size = p_new_column - p_old_column; + if (edit_height == 0 && edit_size == 0) { + return; + } + + // Intentionally includes carets in the multicaret_edit_ignore list so that they are moved together. + for (int i = 0; i < get_caret_count(); i++) { + bool selected = has_selection(i); + bool caret_at_end = selected && is_caret_after_selection_origin(i); + bool include_caret_at = caret_at_end ? p_include_selection_end : p_include_selection_begin; + + // Move caret. + int caret_line = get_caret_line(i); + int caret_column = get_caret_column(i); + bool caret_after = caret_line > p_old_line || (caret_line == p_old_line && caret_column > p_old_column); + bool caret_at = caret_line == p_old_line && caret_column == p_old_column; + if (caret_after || (caret_at && include_caret_at)) { + caret_line += edit_height; + if (caret_line == p_new_line) { + caret_column += edit_size; } + + if (edit_height != 0) { + set_caret_line(caret_line, false, true, -1, i); + } + set_caret_column(caret_column, false, i); } - } else { + + // Move selection origin. + if (!selected) { + continue; + } + bool include_selection_origin_at = !caret_at_end ? p_include_selection_end : p_include_selection_begin; + + int selection_origin_line = get_selection_origin_line(i); + int selection_origin_column = get_selection_origin_column(i); + bool selection_origin_after = selection_origin_line > p_old_line || (selection_origin_line == p_old_line && selection_origin_column > p_old_column); + bool selection_origin_at = selection_origin_line == p_old_line && selection_origin_column == p_old_column; + if (selection_origin_after || (selection_origin_at && include_selection_origin_at)) { + selection_origin_line += edit_height; + if (selection_origin_line == p_new_line) { + selection_origin_column += edit_size; + } + select(selection_origin_line, selection_origin_column, caret_line, caret_column, i); + } + } + if (!p_include_selection_begin && p_include_selection_end && has_selection()) { + // It is possible that two adjacent selections now overlap. + merge_overlapping_carets(); + } +} + +void TextEdit::_cancel_drag_and_drop_text() { + // Cancel the drag operation if drag originated from here. + if (selection_drag_attempt && get_viewport()) { + get_viewport()->gui_cancel_drag(); + } +} + +/* Selection */ +void TextEdit::_selection_changed(int p_caret) { + if (!selecting_enabled) { + return; + } + + _cancel_drag_and_drop_text(); + queue_redraw(); +} + +void TextEdit::_click_selection_held() { + // Update the selection mode on a timer so it is updated when the view scrolls even if the mouse isn't moving. + if (!Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT) || get_selection_mode() == SelectionMode::SELECTION_MODE_NONE) { click_select_held->stop(); + return; + } + switch (get_selection_mode()) { + case SelectionMode::SELECTION_MODE_POINTER: { + _update_selection_mode_pointer(); + } break; + case SelectionMode::SELECTION_MODE_WORD: { + _update_selection_mode_word(); + } break; + case SelectionMode::SELECTION_MODE_LINE: { + _update_selection_mode_line(); + } break; + default: { + break; + } } } -void TextEdit::_update_selection_mode_pointer() { - dragging_selection = true; +void TextEdit::_update_selection_mode_pointer(bool p_initial) { Point2 mp = get_local_mouse_pos(); Point2i pos = get_line_column_at_pos(mp); int line = pos.y; - int col = pos.x; - int caret_idx = carets.size() - 1; - - select(carets[caret_idx].selection.selecting_line, carets[caret_idx].selection.selecting_column, line, col, caret_idx); + int column = pos.x; + int caret_index = get_caret_count() - 1; + + if (p_initial && !has_selection(caret_index)) { + set_selection_origin_line(line, true, -1, caret_index); + set_selection_origin_column(column, caret_index); + // Set the word begin and end to the column in case the mode changes later. + carets.write[caret_index].selection.word_begin_column = column; + carets.write[caret_index].selection.word_end_column = column; + } else { + select(get_selection_origin_line(caret_index), get_selection_origin_column(caret_index), line, column, caret_index); + } + adjust_viewport_to_caret(caret_index); - set_caret_line(line, false, true, 0, caret_idx); - set_caret_column(col, true, caret_idx); - queue_redraw(); + if (has_selection(caret_index)) { + // Only set to true if any selection has been made. + dragging_selection = true; + } click_select_held->start(); merge_overlapping_carets(); } -void TextEdit::_update_selection_mode_word() { +void TextEdit::_update_selection_mode_word(bool p_initial) { dragging_selection = true; Point2 mp = get_local_mouse_pos(); Point2i pos = get_line_column_at_pos(mp); int line = pos.y; - int col = pos.x; - int caret_idx = carets.size() - 1; + int column = pos.x; + int caret_index = get_caret_count() - 1; - int caret_pos = CLAMP(col, 0, text[line].length()); + int caret_pos = CLAMP(column, 0, text[line].length()); int beg = caret_pos; int end = beg; PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(line)->get_rid()); @@ -7316,70 +7571,57 @@ void TextEdit::_update_selection_mode_word() { } } - /* Initial selection. */ - if (!has_selection(caret_idx)) { - select(line, beg, line, end, caret_idx); - carets.write[caret_idx].selection.selecting_column = beg; - carets.write[caret_idx].selection.selected_word_beg = beg; - carets.write[caret_idx].selection.selected_word_end = end; - carets.write[caret_idx].selection.selected_word_origin = beg; - set_caret_line(line, false, true, 0, caret_idx); - set_caret_column(end, true, caret_idx); + if (p_initial && !has_selection(caret_index)) { + // Set the selection origin if there is no existing selection. + select(line, beg, line, end, caret_index); + carets.write[caret_index].selection.word_begin_column = beg; + carets.write[caret_index].selection.word_end_column = end; } else { - if ((col <= carets[caret_idx].selection.selected_word_origin && line == get_selection_line(caret_idx)) || line < get_selection_line(caret_idx)) { - carets.write[caret_idx].selection.selecting_column = carets[caret_idx].selection.selected_word_end; - select(line, beg, get_selection_line(caret_idx), carets[caret_idx].selection.selected_word_end, caret_idx); - set_caret_line(line, false, true, 0, caret_idx); - set_caret_column(beg, true, caret_idx); - } else { - carets.write[caret_idx].selection.selecting_column = carets[caret_idx].selection.selected_word_beg; - select(get_selection_line(caret_idx), carets[caret_idx].selection.selected_word_beg, line, end, caret_idx); - set_caret_line(get_selection_to_line(caret_idx), false, true, 0, caret_idx); - set_caret_column(get_selection_to_column(caret_idx), true, caret_idx); - } + // Expand the word selection to the mouse. + int origin_line = get_selection_origin_line(caret_index); + bool is_new_selection_dir_right = line > origin_line || (line == origin_line && column >= carets[caret_index].selection.word_begin_column); + int origin_col = is_new_selection_dir_right ? carets[caret_index].selection.word_begin_column : carets[caret_index].selection.word_end_column; + int caret_col = is_new_selection_dir_right ? end : beg; + + select(origin_line, origin_col, line, caret_col, caret_index); } + adjust_viewport_to_caret(caret_index); if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) { DisplayServer::get_singleton()->clipboard_set_primary(get_selected_text()); } - queue_redraw(); - click_select_held->start(); merge_overlapping_carets(); } -void TextEdit::_update_selection_mode_line() { +void TextEdit::_update_selection_mode_line(bool p_initial) { dragging_selection = true; Point2 mp = get_local_mouse_pos(); Point2i pos = get_line_column_at_pos(mp); int line = pos.y; - int col = pos.x; - int caret_idx = carets.size() - 1; - - col = 0; - if (line < carets[caret_idx].selection.selecting_line) { - // Caret is above us. - set_caret_line(line - 1, false, true, 0, caret_idx); - carets.write[caret_idx].selection.selecting_column = has_selection(caret_idx) - ? text[get_selection_line(caret_idx)].length() - : 0; - } else { - // Caret is below us. - set_caret_line(line + 1, false, true, 0, caret_idx); - carets.write[caret_idx].selection.selecting_column = 0; - col = text[line].length(); + int caret_index = get_caret_count() - 1; + + int origin_line = p_initial && !has_selection(caret_index) ? line : get_selection_origin_line(); + bool line_below = line >= origin_line; + int origin_col = line_below ? 0 : get_line(origin_line).length(); + int caret_line = line_below ? line + 1 : line; + int caret_col = caret_line < text.size() ? 0 : get_line(text.size() - 1).length(); + + select(origin_line, origin_col, caret_line, caret_col, caret_index); + adjust_viewport_to_caret(caret_index); + + if (p_initial) { + // Set the word begin and end to the start and end of the origin line in case the mode changes later. + carets.write[caret_index].selection.word_begin_column = 0; + carets.write[caret_index].selection.word_end_column = get_line(origin_line).length(); } - set_caret_column(0, false, caret_idx); - select(carets[caret_idx].selection.selecting_line, carets[caret_idx].selection.selecting_column, line, col, caret_idx); if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) { DisplayServer::get_singleton()->clipboard_set_primary(get_selected_text()); } - queue_redraw(); - click_select_held->start(); merge_overlapping_carets(); } @@ -7389,23 +7631,23 @@ void TextEdit::_pre_shift_selection(int p_caret) { return; } - if (!has_selection(p_caret) || get_selection_mode() == SelectionMode::SELECTION_MODE_NONE) { - carets.write[p_caret].selection.active = true; - set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT, get_caret_line(p_caret), get_caret_column(p_caret), p_caret); + set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT); + if (has_selection(p_caret)) { return; } - - set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT, get_selection_line(p_caret), get_selection_column(p_caret), p_caret); + // Prepare selection to start at current caret position. + set_selection_origin_line(get_caret_line(p_caret), true, -1, p_caret); + set_selection_origin_column(get_caret_column(p_caret), p_caret); + carets.write[p_caret].selection.active = true; + carets.write[p_caret].selection.word_begin_column = get_caret_column(p_caret); + carets.write[p_caret].selection.word_end_column = get_caret_column(p_caret); } -void TextEdit::_post_shift_selection(int p_caret) { - if (!selecting_enabled) { - return; - } - - if (has_selection(p_caret) && get_selection_mode() == SelectionMode::SELECTION_MODE_SHIFT) { - select(get_selection_line(p_caret), get_selection_column(p_caret), get_caret_line(p_caret), get_caret_column(p_caret), p_caret); +bool TextEdit::_selection_contains(int p_caret, int p_line, int p_column, bool p_include_edges, bool p_only_selections) const { + if (!has_selection(p_caret)) { + return !p_only_selections && p_line == get_caret_line(p_caret) && p_column == get_caret_column(p_caret); } + return _is_line_col_in_range(p_line, p_column, get_selection_from_line(p_caret), get_selection_from_column(p_caret), get_selection_to_line(p_caret), get_selection_to_column(p_caret), p_include_edges); } /* Line Wrapping */ @@ -7780,9 +8022,43 @@ Dictionary TextEdit::_get_line_syntax_highlighting(int p_line) { return syntax_highlighter.is_null() && !setting_text ? Dictionary() : syntax_highlighter->get_line_syntax_highlighting(p_line); } +/* Deprecated. */ +#ifndef DISABLE_DEPRECATED +Vector<int> TextEdit::get_caret_index_edit_order() { + Vector<int> carets_order = get_sorted_carets(); + carets_order.reverse(); + return carets_order; +} + +void TextEdit::adjust_carets_after_edit(int p_caret, int p_from_line, int p_from_col, int p_to_line, int p_to_col) { +} + +int TextEdit::get_selection_line(int p_caret) const { + return get_selection_origin_line(p_caret); +} + +int TextEdit::get_selection_column(int p_caret) const { + return get_selection_origin_column(p_caret); +} +#endif + /*** Super internal Core API. Everything builds on it. ***/ -void TextEdit::_text_changed_emit() { +void TextEdit::_text_changed() { + _cancel_drag_and_drop_text(); + queue_redraw(); + + if (text_changed_dirty || setting_text) { + return; + } + + if (is_inside_tree()) { + callable_mp(this, &TextEdit::_emit_text_changed).call_deferred(); + } + text_changed_dirty = true; +} + +void TextEdit::_emit_text_changed() { emit_signal(SNAME("text_changed")); text_changed_dirty = false; } @@ -7918,12 +8194,7 @@ void TextEdit::_base_insert_text(int p_line, int p_char, const String &p_text, i input_direction = (TextDirection)dir; } - if (!text_changed_dirty && !setting_text) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_text_changed_emit).call_deferred(); - } - text_changed_dirty = true; - } + _text_changed(); emit_signal(SNAME("lines_edited_from"), p_line, r_end_line); } @@ -7964,12 +8235,7 @@ void TextEdit::_base_remove_text(int p_from_line, int p_from_column, int p_to_li text.remove_range(p_from_line, p_to_line); text.set(p_from_line, pre_text + post_text, structured_text_parser(st_parser, st_args, pre_text + post_text)); - if (!text_changed_dirty && !setting_text) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_text_changed_emit).call_deferred(); - } - text_changed_dirty = true; - } + _text_changed(); emit_signal(SNAME("lines_edited_from"), p_to_line, p_from_line); } diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index 1099295d3b..efade39876 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -389,18 +389,12 @@ private: /* Caret. */ struct Selection { bool active = false; - bool shiftclick_left = false; - int selecting_line = 0; - int selecting_column = 0; - int selected_word_beg = 0; - int selected_word_end = 0; - int selected_word_origin = 0; - - int from_line = 0; - int from_column = 0; - int to_line = 0; - int to_column = 0; + int origin_line = 0; + int origin_column = 0; + int origin_last_fit_x = 0; + int word_begin_column = 0; + int word_end_column = 0; }; struct Caret { @@ -415,11 +409,13 @@ private: // Vector containing all the carets, index '0' is the "main caret" and should never be removed. Vector<Caret> carets; - Vector<int> caret_index_edit_order; bool setting_caret_line = false; bool caret_pos_dirty = false; - bool caret_index_edit_dirty = true; + + int multicaret_edit_count = 0; + bool multicaret_edit_merge_queued = false; + HashSet<int> multicaret_edit_ignore_carets; CaretType caret_type = CaretType::CARET_TYPE_LINE; @@ -438,12 +434,18 @@ private: bool drag_action = false; bool drag_caret_force_displayed = false; + void _caret_changed(int p_caret = -1); void _emit_caret_changed(); void _reset_caret_blink_timer(); void _toggle_draw_caret(); int _get_column_x_offset_for_line(int p_char, int p_line, int p_column) const; + bool _is_line_col_in_range(int p_line, int p_column, int p_from_line, int p_from_column, int p_to_line, int p_to_column, bool p_include_edges = true) const; + + void _offset_carets_after(int p_old_line, int p_old_column, int p_new_line, int p_new_column, bool p_include_selection_begin = true, bool p_include_selection_end = true); + + void _cancel_drag_and_drop_text(); /* Selection. */ SelectionMode selecting_mode = SelectionMode::SELECTION_MODE_NONE; @@ -456,18 +458,23 @@ private: bool selection_drag_attempt = false; bool dragging_selection = false; + int drag_and_drop_origin_caret_index = -1; + int drag_caret_index = -1; Timer *click_select_held = nullptr; uint64_t last_dblclk = 0; Vector2 last_dblclk_pos; + + void _selection_changed(int p_caret = -1); void _click_selection_held(); - void _update_selection_mode_pointer(); - void _update_selection_mode_word(); - void _update_selection_mode_line(); + void _update_selection_mode_pointer(bool p_initial = false); + void _update_selection_mode_word(bool p_initial = false); + void _update_selection_mode_line(bool p_initial = false); void _pre_shift_selection(int p_caret); - void _post_shift_selection(int p_caret); + + bool _selection_contains(int p_caret, int p_line, int p_column, bool p_include_edges = true, bool p_only_selections = true) const; /* Line wrapping. */ LineWrappingMode line_wrapping_mode = LineWrappingMode::LINE_WRAPPING_NONE; @@ -599,7 +606,8 @@ private: /*** Super internal Core API. Everything builds on it. ***/ bool text_changed_dirty = false; - void _text_changed_emit(); + void _text_changed(); + void _emit_text_changed(); void _insert_text(int p_line, int p_char, const String &p_text, int *r_end_line = nullptr, int *r_end_char = nullptr); void _remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column); @@ -625,13 +633,15 @@ private: void _move_caret_document_end(bool p_select); bool _clear_carets_and_selection(); - // Used in add_caret_at_carets - void _get_above_below_caret_line_column(int p_old_line, int p_old_wrap_index, int p_old_column, bool p_below, int &p_new_line, int &p_new_column, int p_last_fit_x = -1) const; - protected: void _notification(int p_what); static void _bind_methods(); +#ifndef DISABLE_DEPRECATED + void _set_selection_mode_compat_86978(SelectionMode p_mode, int p_line = -1, int p_column = -1, int p_caret = 0); + static void _bind_compatibility_methods(); +#endif // DISABLE_DEPRECATED + virtual void _update_theme_item_cache() override; /* Internal API for CodeEdit, pending public API. */ @@ -659,6 +669,7 @@ protected: bool _is_line_hidden(int p_line) const; void _unhide_all_lines(); + virtual void _unhide_carets(); // Symbol lookup. String lookup_symbol_word; @@ -765,9 +776,11 @@ public: void swap_lines(int p_from_line, int p_to_line); - void insert_line_at(int p_at, const String &p_text); - void insert_text_at_caret(const String &p_text, int p_caret = -1); + void insert_line_at(int p_line, const String &p_text); + void remove_line_at(int p_line, bool p_move_carets_down = true); + void insert_text_at_caret(const String &p_text, int p_caret = -1); + void insert_text(const String &p_text, int p_line, int p_column, bool p_before_selection_begin = true, bool p_before_selection_end = false); void remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column); int get_last_unhidden_line() const; @@ -851,15 +864,20 @@ public: void set_multiple_carets_enabled(bool p_enabled); bool is_multiple_carets_enabled() const; - int add_caret(int p_line, int p_col); + int add_caret(int p_line, int p_column); void remove_caret(int p_caret); void remove_secondary_carets(); - void merge_overlapping_carets(); int get_caret_count() const; void add_caret_at_carets(bool p_below); - Vector<int> get_caret_index_edit_order(); - void adjust_carets_after_edit(int p_caret, int p_from_line, int p_from_col, int p_to_line, int p_to_col); + Vector<int> get_sorted_carets(bool p_include_ignored_carets = false) const; + void collapse_carets(int p_from_line, int p_from_column, int p_to_line, int p_to_column, bool p_inclusive = false); + + void merge_overlapping_carets(); + void begin_multicaret_edit(); + void end_multicaret_edit(); + bool is_in_mulitcaret_edit() const; + bool multicaret_edit_ignore_caret(int p_caret) const; bool is_caret_visible(int p_caret = 0) const; Point2 get_caret_draw_pos(int p_caret = 0) const; @@ -867,7 +885,7 @@ public: void set_caret_line(int p_line, bool p_adjust_viewport = true, bool p_can_be_hidden = true, int p_wrap_index = 0, int p_caret = 0); int get_caret_line(int p_caret = 0) const; - void set_caret_column(int p_col, bool p_adjust_viewport = true, int p_caret = 0); + void set_caret_column(int p_column, bool p_adjust_viewport = true, int p_caret = 0); int get_caret_column(int p_caret = 0) const; int get_caret_wrap_index(int p_caret = 0) const; @@ -884,27 +902,34 @@ public: void set_drag_and_drop_selection_enabled(const bool p_enabled); bool is_drag_and_drop_selection_enabled() const; - void set_selection_mode(SelectionMode p_mode, int p_line = -1, int p_column = -1, int p_caret = 0); + void set_selection_mode(SelectionMode p_mode); SelectionMode get_selection_mode() const; void select_all(); void select_word_under_caret(int p_caret = -1); void add_selection_for_next_occurrence(); void skip_selection_for_next_occurrence(); - void select(int p_from_line, int p_from_column, int p_to_line, int p_to_column, int p_caret = 0); + void select(int p_origin_line, int p_origin_column, int p_caret_line, int p_caret_column, int p_caret = 0); bool has_selection(int p_caret = -1) const; String get_selected_text(int p_caret = -1); + int get_selection_at_line_column(int p_line, int p_column, bool p_include_edges = true, bool p_only_selections = true) const; + Vector<Point2i> get_line_ranges_from_carets(bool p_only_selections = false, bool p_merge_adjacent = true) const; + TypedArray<Vector2i> get_line_ranges_from_carets_typed_array(bool p_only_selections = false, bool p_merge_adjacent = true) const; - int get_selection_line(int p_caret = 0) const; - int get_selection_column(int p_caret = 0) const; + void set_selection_origin_line(int p_line, bool p_can_be_hidden = true, int p_wrap_index = -1, int p_caret = 0); + void set_selection_origin_column(int p_column, int p_caret = 0); + int get_selection_origin_line(int p_caret = 0) const; + int get_selection_origin_column(int p_caret = 0) const; int get_selection_from_line(int p_caret = 0) const; int get_selection_from_column(int p_caret = 0) const; int get_selection_to_line(int p_caret = 0) const; int get_selection_to_column(int p_caret = 0) const; + bool is_caret_after_selection_origin(int p_caret = 0) const; + void deselect(int p_caret = -1); void delete_selection(int p_caret = -1); @@ -1043,6 +1068,15 @@ public: Color get_font_color() const; + /* Deprecated. */ +#ifndef DISABLE_DEPRECATED + Vector<int> get_caret_index_edit_order(); + void adjust_carets_after_edit(int p_caret, int p_from_line, int p_from_col, int p_to_line, int p_to_col); + + int get_selection_line(int p_caret = 0) const; + int get_selection_column(int p_caret = 0) const; +#endif + TextEdit(const String &p_placeholder = String()); }; diff --git a/scene/gui/texture_button.cpp b/scene/gui/texture_button.cpp index 0b197c8c02..df90257e03 100644 --- a/scene/gui/texture_button.cpp +++ b/scene/gui/texture_button.cpp @@ -103,7 +103,7 @@ bool TextureButton::has_point(const Point2 &p_point) const { point *= scale; // finally, we need to check if the point is inside a rectangle with a position >= 0,0 and a size <= mask_size - rect.position = Point2().max(_texture_region.position); + rect.position = _texture_region.position.maxf(0); rect.size = mask_size.min(_texture_region.size); } diff --git a/scene/gui/texture_progress_bar.cpp b/scene/gui/texture_progress_bar.cpp index fc9ac2ab18..bbe5ddf1c3 100644 --- a/scene/gui/texture_progress_bar.cpp +++ b/scene/gui/texture_progress_bar.cpp @@ -249,7 +249,7 @@ Point2 TextureProgressBar::get_relative_center() { p += rad_center_off; p.x /= progress->get_width(); p.y /= progress->get_height(); - p = p.clamp(Point2(), Point2(1, 1)); + p = p.clampf(0, 1); return p; } diff --git a/scene/gui/tree.cpp b/scene/gui/tree.cpp index b17d345f1f..376ace2fe2 100644 --- a/scene/gui/tree.cpp +++ b/scene/gui/tree.cpp @@ -3429,7 +3429,7 @@ Rect2 Tree::_get_content_rect() const { const real_t v_size = v_scroll->is_visible() ? (v_scroll->get_combined_minimum_size().x + theme_cache.scrollbar_h_separation) : 0; const real_t h_size = h_scroll->is_visible() ? (h_scroll->get_combined_minimum_size().y + theme_cache.scrollbar_v_separation) : 0; const Point2 scroll_begin = _get_scrollbar_layout_rect().get_end() - Vector2(v_size, h_size); - const Size2 offset = (content_rect.get_end() - scroll_begin).max(Vector2(0, 0)); + const Size2 offset = (content_rect.get_end() - scroll_begin).maxf(0); return content_rect.grow_individual(0, 0, -offset.x, -offset.y); } diff --git a/scene/main/canvas_item.compat.inc b/scene/main/canvas_item.compat.inc new file mode 100644 index 0000000000..7136fded15 --- /dev/null +++ b/scene/main/canvas_item.compat.inc @@ -0,0 +1,41 @@ +/**************************************************************************/ +/* canvas_item.compat.inc */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef DISABLE_DEPRECATED + +void CanvasItem::_draw_circle_compat_84472(const Point2 &p_pos, real_t p_radius, const Color &p_color) { + draw_circle(p_pos, p_radius, p_color, true, -1.0, false); +} + +void CanvasItem::_bind_compatibility_methods() { + ClassDB::bind_compatibility_method(D_METHOD("draw_circle", "position", "radius", "color"), &CanvasItem::_draw_circle_compat_84472); +} + +#endif diff --git a/scene/main/canvas_item.cpp b/scene/main/canvas_item.cpp index 56aa453407..cabba0f2ed 100644 --- a/scene/main/canvas_item.cpp +++ b/scene/main/canvas_item.cpp @@ -29,6 +29,7 @@ /**************************************************************************/ #include "canvas_item.h" +#include "canvas_item.compat.inc" #include "scene/2d/canvas_group.h" #include "scene/main/canvas_layer.h" @@ -726,11 +727,40 @@ void CanvasItem::draw_rect(const Rect2 &p_rect, const Color &p_color, bool p_fil } } -void CanvasItem::draw_circle(const Point2 &p_pos, real_t p_radius, const Color &p_color) { +void CanvasItem::draw_circle(const Point2 &p_pos, real_t p_radius, const Color &p_color, bool p_filled, real_t p_width, bool p_antialiased) { ERR_THREAD_GUARD; ERR_DRAW_GUARD; - RenderingServer::get_singleton()->canvas_item_add_circle(canvas_item, p_pos, p_radius, p_color); + if (p_filled) { + if (p_width != -1.0) { + WARN_PRINT("The draw_circle() \"width\" argument has no effect when \"filled\" is \"true\"."); + } + + RenderingServer::get_singleton()->canvas_item_add_circle(canvas_item, p_pos, p_radius, p_color); + } else if (p_width >= 2.0 * p_radius) { + RenderingServer::get_singleton()->canvas_item_add_circle(canvas_item, p_pos, p_radius + 0.5 * p_width, p_color); + } else { + // Tessellation count is hardcoded. Keep in sync with the same variable in `RendererCanvasCull::canvas_item_add_circle()`. + const int circle_segments = 64; + + Vector<Vector2> points; + points.resize(circle_segments + 1); + + Vector2 *points_ptr = points.ptrw(); + const real_t circle_point_step = Math_TAU / circle_segments; + + for (int i = 0; i < circle_segments; i++) { + float angle = i * circle_point_step; + points_ptr[i].x = Math::cos(angle) * p_radius; + points_ptr[i].y = Math::sin(angle) * p_radius; + points_ptr[i] += p_pos; + } + points_ptr[circle_segments] = points_ptr[0]; + + Vector<Color> colors = { p_color }; + + RenderingServer::get_singleton()->canvas_item_add_polyline(canvas_item, points, colors, p_width, p_antialiased); + } } void CanvasItem::draw_texture(const Ref<Texture2D> &p_texture, const Point2 &p_pos, const Color &p_modulate) { @@ -1163,7 +1193,7 @@ void CanvasItem::_bind_methods() { ClassDB::bind_method(D_METHOD("draw_multiline", "points", "color", "width"), &CanvasItem::draw_multiline, DEFVAL(-1.0)); ClassDB::bind_method(D_METHOD("draw_multiline_colors", "points", "colors", "width"), &CanvasItem::draw_multiline_colors, DEFVAL(-1.0)); ClassDB::bind_method(D_METHOD("draw_rect", "rect", "color", "filled", "width"), &CanvasItem::draw_rect, DEFVAL(true), DEFVAL(-1.0)); - ClassDB::bind_method(D_METHOD("draw_circle", "position", "radius", "color"), &CanvasItem::draw_circle); + ClassDB::bind_method(D_METHOD("draw_circle", "position", "radius", "color", "filled", "width", "antialiased"), &CanvasItem::draw_circle, DEFVAL(true), DEFVAL(-1.0), DEFVAL(false)); ClassDB::bind_method(D_METHOD("draw_texture", "texture", "position", "modulate"), &CanvasItem::draw_texture, DEFVAL(Color(1, 1, 1, 1))); ClassDB::bind_method(D_METHOD("draw_texture_rect", "texture", "rect", "tile", "modulate", "transpose"), &CanvasItem::draw_texture_rect, DEFVAL(Color(1, 1, 1, 1)), DEFVAL(false)); ClassDB::bind_method(D_METHOD("draw_texture_rect_region", "texture", "rect", "src_rect", "modulate", "transpose", "clip_uv"), &CanvasItem::draw_texture_rect_region, DEFVAL(Color(1, 1, 1, 1)), DEFVAL(false), DEFVAL(true)); diff --git a/scene/main/canvas_item.h b/scene/main/canvas_item.h index 8cec086ca6..ae7b195ead 100644 --- a/scene/main/canvas_item.h +++ b/scene/main/canvas_item.h @@ -166,6 +166,12 @@ protected: void _notification(int p_what); static void _bind_methods(); + +#ifndef DISABLE_DEPRECATED + void _draw_circle_compat_84472(const Point2 &p_pos, real_t p_radius, const Color &p_color); + static void _bind_compatibility_methods(); +#endif + void _validate_property(PropertyInfo &p_property) const; _FORCE_INLINE_ void set_hide_clip_children(bool p_value) { hide_clip_children = p_value; } @@ -273,7 +279,7 @@ public: void draw_multiline(const Vector<Point2> &p_points, const Color &p_color, real_t p_width = -1.0); void draw_multiline_colors(const Vector<Point2> &p_points, const Vector<Color> &p_colors, real_t p_width = -1.0); void draw_rect(const Rect2 &p_rect, const Color &p_color, bool p_filled = true, real_t p_width = -1.0); - void draw_circle(const Point2 &p_pos, real_t p_radius, const Color &p_color); + void draw_circle(const Point2 &p_pos, real_t p_radius, const Color &p_color, bool p_filled = true, real_t p_width = -1.0, bool p_antialiased = false); void draw_texture(const Ref<Texture2D> &p_texture, const Point2 &p_pos, const Color &p_modulate = Color(1, 1, 1, 1)); void draw_texture_rect(const Ref<Texture2D> &p_texture, const Rect2 &p_rect, bool p_tile = false, const Color &p_modulate = Color(1, 1, 1), bool p_transpose = false); void draw_texture_rect_region(const Ref<Texture2D> &p_texture, const Rect2 &p_rect, const Rect2 &p_src_rect, const Color &p_modulate = Color(1, 1, 1), bool p_transpose = false, bool p_clip_uv = false); diff --git a/scene/main/status_indicator.cpp b/scene/main/status_indicator.cpp index 54b2ff75ca..891974f68f 100644 --- a/scene/main/status_indicator.cpp +++ b/scene/main/status_indicator.cpp @@ -30,6 +30,8 @@ #include "status_indicator.h" +#include "scene/gui/popup_menu.h" + void StatusIndicator::_notification(int p_what) { ERR_MAIN_THREAD_GUARD; #ifdef TOOLS_ENABLED @@ -43,12 +45,22 @@ void StatusIndicator::_notification(int p_what) { if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_STATUS_INDICATOR)) { if (visible && iid == DisplayServer::INVALID_INDICATOR_ID) { iid = DisplayServer::get_singleton()->create_status_indicator(icon, tooltip, callable_mp(this, &StatusIndicator::_callback)); + PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(menu)); + if (pm) { + RID menu_rid = pm->bind_global_menu(); + DisplayServer::get_singleton()->status_indicator_set_menu(iid, menu_rid); + } } } } break; case NOTIFICATION_EXIT_TREE: { if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_STATUS_INDICATOR)) { if (iid != DisplayServer::INVALID_INDICATOR_ID) { + PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(menu)); + if (pm) { + pm->unbind_global_menu(); + DisplayServer::get_singleton()->status_indicator_set_menu(iid, RID()); + } DisplayServer::get_singleton()->delete_status_indicator(iid); iid = DisplayServer::INVALID_INDICATOR_ID; } @@ -66,11 +78,15 @@ void StatusIndicator::_bind_methods() { ClassDB::bind_method(D_METHOD("get_icon"), &StatusIndicator::get_icon); ClassDB::bind_method(D_METHOD("set_visible", "visible"), &StatusIndicator::set_visible); ClassDB::bind_method(D_METHOD("is_visible"), &StatusIndicator::is_visible); + ClassDB::bind_method(D_METHOD("set_menu", "menu"), &StatusIndicator::set_menu); + ClassDB::bind_method(D_METHOD("get_menu"), &StatusIndicator::get_menu); + ClassDB::bind_method(D_METHOD("get_rect"), &StatusIndicator::get_rect); ADD_SIGNAL(MethodInfo("pressed", PropertyInfo(Variant::INT, "mouse_button"), PropertyInfo(Variant::VECTOR2I, "mouse_position"))); ADD_PROPERTY(PropertyInfo(Variant::STRING, "tooltip", PROPERTY_HINT_MULTILINE_TEXT), "set_tooltip", "get_tooltip"); - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "icon", PROPERTY_HINT_RESOURCE_TYPE, "Image"), "set_icon", "get_icon"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "icon", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_icon", "get_icon"); + ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "menu", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "PopupMenu"), "set_menu", "get_menu"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "visible"), "set_visible", "is_visible"); } @@ -78,7 +94,7 @@ void StatusIndicator::_callback(MouseButton p_index, const Point2i &p_pos) { emit_signal(SNAME("pressed"), p_index, p_pos); } -void StatusIndicator::set_icon(const Ref<Image> &p_icon) { +void StatusIndicator::set_icon(const Ref<Texture2D> &p_icon) { ERR_MAIN_THREAD_GUARD; icon = p_icon; if (iid != DisplayServer::INVALID_INDICATOR_ID) { @@ -86,7 +102,7 @@ void StatusIndicator::set_icon(const Ref<Image> &p_icon) { } } -Ref<Image> StatusIndicator::get_icon() const { +Ref<Texture2D> StatusIndicator::get_icon() const { return icon; } @@ -102,6 +118,30 @@ String StatusIndicator::get_tooltip() const { return tooltip; } +void StatusIndicator::set_menu(const NodePath &p_menu) { + PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(menu)); + if (pm) { + pm->unbind_global_menu(); + if (iid != DisplayServer::INVALID_INDICATOR_ID) { + DisplayServer::get_singleton()->status_indicator_set_menu(iid, RID()); + } + } + + menu = p_menu; + + pm = Object::cast_to<PopupMenu>(get_node_or_null(menu)); + if (pm) { + if (iid != DisplayServer::INVALID_INDICATOR_ID) { + RID menu_rid = pm->bind_global_menu(); + DisplayServer::get_singleton()->status_indicator_set_menu(iid, menu_rid); + } + } +} + +NodePath StatusIndicator::get_menu() const { + return menu; +} + void StatusIndicator::set_visible(bool p_visible) { ERR_MAIN_THREAD_GUARD; if (visible == p_visible) { @@ -122,8 +162,18 @@ void StatusIndicator::set_visible(bool p_visible) { if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_STATUS_INDICATOR)) { if (visible && iid == DisplayServer::INVALID_INDICATOR_ID) { iid = DisplayServer::get_singleton()->create_status_indicator(icon, tooltip, callable_mp(this, &StatusIndicator::_callback)); + PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(menu)); + if (pm) { + RID menu_rid = pm->bind_global_menu(); + DisplayServer::get_singleton()->status_indicator_set_menu(iid, menu_rid); + } } if (!visible && iid != DisplayServer::INVALID_INDICATOR_ID) { + PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(menu)); + if (pm) { + pm->unbind_global_menu(); + DisplayServer::get_singleton()->status_indicator_set_menu(iid, RID()); + } DisplayServer::get_singleton()->delete_status_indicator(iid); iid = DisplayServer::INVALID_INDICATOR_ID; } @@ -133,3 +183,10 @@ void StatusIndicator::set_visible(bool p_visible) { bool StatusIndicator::is_visible() const { return visible; } + +Rect2 StatusIndicator::get_rect() const { + if (iid == DisplayServer::INVALID_INDICATOR_ID) { + return Rect2(); + } + return DisplayServer::get_singleton()->status_indicator_get_rect(iid); +} diff --git a/scene/main/status_indicator.h b/scene/main/status_indicator.h index aa3aa68d78..cd38da6e6c 100644 --- a/scene/main/status_indicator.h +++ b/scene/main/status_indicator.h @@ -37,10 +37,11 @@ class StatusIndicator : public Node { GDCLASS(StatusIndicator, Node); - Ref<Image> icon; + Ref<Texture2D> icon; String tooltip; bool visible = true; DisplayServer::IndicatorID iid = DisplayServer::INVALID_INDICATOR_ID; + NodePath menu; protected: void _notification(int p_what); @@ -49,14 +50,19 @@ protected: void _callback(MouseButton p_index, const Point2i &p_pos); public: - void set_icon(const Ref<Image> &p_icon); - Ref<Image> get_icon() const; + void set_icon(const Ref<Texture2D> &p_icon); + Ref<Texture2D> get_icon() const; void set_tooltip(const String &p_tooltip); String get_tooltip() const; + void set_menu(const NodePath &p_menu); + NodePath get_menu() const; + void set_visible(bool p_visible); bool is_visible() const; + + Rect2 get_rect() const; }; #endif // STATUS_INDICATOR_H diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp index 73ce166123..9dbe10f30b 100644 --- a/scene/main/viewport.cpp +++ b/scene/main/viewport.cpp @@ -688,6 +688,18 @@ void Viewport::_process_picking() { physics_picking_events.clear(); return; } +#ifndef _3D_DISABLED + if (use_xr) { + if (XRServer::get_singleton() != nullptr) { + Ref<XRInterface> xr_interface = XRServer::get_singleton()->get_primary_interface(); + if (xr_interface.is_valid() && xr_interface->is_initialized() && xr_interface->get_view_count() > 1) { + WARN_PRINT_ONCE("Object picking can't be used when stereo rendering, this will be turned off!"); + physics_object_picking = false; // don't try again. + return; + } + } + } +#endif _drop_physics_mouseover(true); @@ -856,9 +868,10 @@ void Viewport::_process_picking() { if (send_event) { co->_input_event_call(this, ev, res[i].shape); - if (physics_object_picking_first_only) { - break; - } + } + + if (physics_object_picking_first_only) { + break; } } } @@ -970,7 +983,7 @@ void Viewport::_set_size(const Size2i &p_size, const Size2i &p_size_2d_override, stretch_transform_new.scale(scale); } - Size2i new_size = p_size.max(Size2i(2, 2)); + Size2i new_size = p_size.maxi(2); if (size == new_size && size_allocated == p_allocated && stretch_transform == stretch_transform_new && p_size_2d_override == size_2d_override) { return; } @@ -1721,7 +1734,6 @@ void Viewport::_gui_input_event(Ref<InputEvent> p_event) { gui.mouse_focus_mask.set_flag(button_mask); } else { gui.mouse_focus = gui_find_control(mpos); - gui.last_mouse_focus = gui.mouse_focus; if (!gui.mouse_focus) { return; @@ -2306,6 +2318,7 @@ void Viewport::_gui_force_drag(Control *p_base, const Variant &p_data, Control * gui.dragging = true; gui.drag_data = p_data; gui.mouse_focus = nullptr; + gui.mouse_focus_mask.clear(); if (p_control) { _gui_set_drag_preview(p_base, p_control); @@ -2378,9 +2391,6 @@ void Viewport::_gui_remove_control(Control *p_control) { gui.forced_mouse_focus = false; gui.mouse_focus_mask.clear(); } - if (gui.last_mouse_focus == p_control) { - gui.last_mouse_focus = nullptr; - } if (gui.key_focus == p_control) { gui.key_focus = nullptr; } @@ -2758,7 +2768,7 @@ bool Viewport::_sub_windows_forward_input(const Ref<InputEvent> &p_event) { Size2i min_size = gui.currently_dragged_subwindow->get_min_size(); Size2i min_size_clamped = gui.currently_dragged_subwindow->get_clamped_minimum_size(); - min_size_clamped = min_size_clamped.max(Size2i(1, 1)); + min_size_clamped = min_size_clamped.maxi(1); Rect2i r = gui.subwindow_resize_from_rect; @@ -2819,7 +2829,7 @@ bool Viewport::_sub_windows_forward_input(const Ref<InputEvent> &p_event) { Size2i max_size = gui.currently_dragged_subwindow->get_max_size(); if ((max_size.x > 0 || max_size.y > 0) && (max_size.x >= min_size.x && max_size.y >= min_size.y)) { - max_size = max_size.max(Size2i(1, 1)); + max_size = max_size.maxi(1); if (r.size.x > max_size.x) { r.size.x = max_size.x; @@ -3578,6 +3588,13 @@ bool Viewport::gui_is_drag_successful() const { return gui.drag_successful; } +void Viewport::gui_cancel_drag() { + ERR_MAIN_THREAD_GUARD; + if (gui_is_dragging()) { + _perform_drop(); + } +} + void Viewport::set_input_as_handled() { ERR_MAIN_THREAD_GUARD; if (!handle_input_locally) { diff --git a/scene/main/viewport.h b/scene/main/viewport.h index 21832a454c..394d48143c 100644 --- a/scene/main/viewport.h +++ b/scene/main/viewport.h @@ -345,7 +345,6 @@ private: bool key_event_accepted = false; HashMap<int, ObjectID> touch_focus; Control *mouse_focus = nullptr; - Control *last_mouse_focus = nullptr; Control *mouse_click_grabber = nullptr; BitField<MouseButtonMask> mouse_focus_mask; Control *key_focus = nullptr; @@ -616,6 +615,7 @@ public: bool gui_is_dragging() const; bool gui_is_drag_successful() const; + void gui_cancel_drag(); Control *gui_find_control(const Point2 &p_global); diff --git a/scene/main/window.cpp b/scene/main/window.cpp index 0ccc056a8d..929720fcf4 100644 --- a/scene/main/window.cpp +++ b/scene/main/window.cpp @@ -415,7 +415,7 @@ Size2i Window::_clamp_limit_size(const Size2i &p_limit_size) { if (max_window_size != Size2i()) { return p_limit_size.clamp(Vector2i(), max_window_size); } else { - return p_limit_size.max(Vector2i()); + return p_limit_size.maxi(0); } } @@ -1036,7 +1036,7 @@ void Window::_update_window_size() { } if (embedder) { - size = size.max(Size2i(1, 1)); + size = size.maxi(1); embedder->_sub_window_update(this); } else if (window_id != DisplayServer::INVALID_WINDOW_ID) { diff --git a/scene/resources/2d/tile_set.cpp b/scene/resources/2d/tile_set.cpp index 57cc4ad602..6649cb9b82 100644 --- a/scene/resources/2d/tile_set.cpp +++ b/scene/resources/2d/tile_set.cpp @@ -4650,7 +4650,7 @@ Ref<Texture2D> TileSetAtlasSource::get_texture() const { void TileSetAtlasSource::set_margins(Vector2i p_margins) { if (p_margins.x < 0 || p_margins.y < 0) { WARN_PRINT("Atlas source margins should be positive."); - margins = p_margins.max(Vector2i()); + margins = p_margins.maxi(0); } else { margins = p_margins; } @@ -4666,7 +4666,7 @@ Vector2i TileSetAtlasSource::get_margins() const { void TileSetAtlasSource::set_separation(Vector2i p_separation) { if (p_separation.x < 0 || p_separation.y < 0) { WARN_PRINT("Atlas source separation should be positive."); - separation = p_separation.max(Vector2i()); + separation = p_separation.maxi(0); } else { separation = p_separation; } @@ -4682,7 +4682,7 @@ Vector2i TileSetAtlasSource::get_separation() const { void TileSetAtlasSource::set_texture_region_size(Vector2i p_tile_size) { if (p_tile_size.x <= 0 || p_tile_size.y <= 0) { WARN_PRINT("Atlas source tile_size should be strictly positive."); - texture_region_size = p_tile_size.max(Vector2i(1, 1)); + texture_region_size = p_tile_size.maxi(1); } else { texture_region_size = p_tile_size; } diff --git a/scene/resources/animation.cpp b/scene/resources/animation.cpp index cd530f100e..8ffb629ba9 100644 --- a/scene/resources/animation.cpp +++ b/scene/resources/animation.cpp @@ -247,6 +247,7 @@ bool Animation::_set(const StringName &p_name, const Variant &p_value) { } vt->update_mode = UpdateMode(um); } + capture_included = capture_included || (vt->update_mode == UPDATE_CAPTURE); Vector<real_t> times = d["times"]; Array values = d["values"]; @@ -966,6 +967,28 @@ void Animation::remove_track(int p_track) { memdelete(t); tracks.remove_at(p_track); emit_changed(); + _check_capture_included(); +} + +void Animation::set_capture_included(bool p_capture_included) { + capture_included = p_capture_included; +} + +bool Animation::is_capture_included() const { + return capture_included; +} + +void Animation::_check_capture_included() { + capture_included = false; + for (int i = 0; i < tracks.size(); i++) { + if (tracks[i]->type == TYPE_VALUE) { + ValueTrack *vt = static_cast<ValueTrack *>(tracks[i]); + if (vt->update_mode == UPDATE_CAPTURE) { + capture_included = true; + break; + } + } + } } int Animation::get_track_count() const { @@ -2681,6 +2704,8 @@ void Animation::value_track_set_update_mode(int p_track, UpdateMode p_mode) { ValueTrack *vt = static_cast<ValueTrack *>(t); vt->update_mode = p_mode; + + capture_included = capture_included || (p_mode == UPDATE_CAPTURE); emit_changed(); } @@ -3870,9 +3895,13 @@ void Animation::_bind_methods() { ClassDB::bind_method(D_METHOD("compress", "page_size", "fps", "split_tolerance"), &Animation::compress, DEFVAL(8192), DEFVAL(120), DEFVAL(4.0)); + ClassDB::bind_method(D_METHOD("_set_capture_included", "capture_included"), &Animation::set_capture_included); + ClassDB::bind_method(D_METHOD("is_capture_included"), &Animation::is_capture_included); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "length", PROPERTY_HINT_RANGE, "0.001,99999,0.001,suffix:s"), "set_length", "get_length"); ADD_PROPERTY(PropertyInfo(Variant::INT, "loop_mode", PROPERTY_HINT_ENUM, "None,Linear,Ping-Pong"), "set_loop_mode", "get_loop_mode"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "step", PROPERTY_HINT_RANGE, "0,4096,0.001,suffix:s"), "set_step", "get_step"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "capture_included", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_READ_ONLY | PROPERTY_USAGE_NO_EDITOR), "_set_capture_included", "is_capture_included"); BIND_ENUM_CONSTANT(TYPE_VALUE); BIND_ENUM_CONSTANT(TYPE_POSITION_3D); diff --git a/scene/resources/animation.h b/scene/resources/animation.h index 6005172c11..cc7bbae8a3 100644 --- a/scene/resources/animation.h +++ b/scene/resources/animation.h @@ -268,6 +268,8 @@ private: double length = 1.0; real_t step = 1.0 / 30; LoopMode loop_mode = LOOP_NONE; + bool capture_included = false; + void _check_capture_included(); void _track_update_hash(int p_track); @@ -392,6 +394,9 @@ public: int add_track(TrackType p_type, int p_at_pos = -1); void remove_track(int p_track); + void set_capture_included(bool p_capture_included); + bool is_capture_included() const; + int get_track_count() const; TrackType track_get_type(int p_track) const; diff --git a/scene/resources/audio_stream_wav.cpp b/scene/resources/audio_stream_wav.cpp index 0185c6ef85..ba5dad088f 100644 --- a/scene/resources/audio_stream_wav.cpp +++ b/scene/resources/audio_stream_wav.cpp @@ -86,15 +86,15 @@ void AudioStreamPlaybackWAV::seek(double p_time) { offset = uint64_t(p_time * base->mix_rate) << MIX_FRAC_BITS; } -template <typename Depth, bool is_stereo, bool is_ima_adpcm> -void AudioStreamPlaybackWAV::do_resample(const Depth *p_src, AudioFrame *p_dst, int64_t &p_offset, int32_t &p_increment, uint32_t p_amount, IMA_ADPCM_State *p_ima_adpcm) { +template <typename Depth, bool is_stereo, bool is_ima_adpcm, bool is_qoa> +void AudioStreamPlaybackWAV::do_resample(const Depth *p_src, AudioFrame *p_dst, int64_t &p_offset, int32_t &p_increment, uint32_t p_amount, IMA_ADPCM_State *p_ima_adpcm, QOA_State *p_qoa) { // this function will be compiled branchless by any decent compiler - int32_t final, final_r, next, next_r; + int32_t final = 0, final_r = 0, next = 0, next_r = 0; while (p_amount) { p_amount--; int64_t pos = p_offset >> MIX_FRAC_BITS; - if (is_stereo && !is_ima_adpcm) { + if (is_stereo && !is_ima_adpcm && !is_qoa) { pos <<= 1; } @@ -175,32 +175,77 @@ void AudioStreamPlaybackWAV::do_resample(const Depth *p_src, AudioFrame *p_dst, } } else { - final = p_src[pos]; - if (is_stereo) { - final_r = p_src[pos + 1]; - } + if (is_qoa) { + if (pos != p_qoa->cache_pos) { // Prevents triple decoding on lower mix rates. + for (int i = 0; i < 2; i++) { + // Sign operations prevent triple decoding on backward loops, maxing prevents pop. + uint32_t interp_pos = MIN(pos + (i * sign) + (sign < 0), p_qoa->desc->samples - 1); + uint32_t new_data_ofs = 8 + interp_pos / QOA_FRAME_LEN * p_qoa->frame_len; + + if (p_qoa->data_ofs != new_data_ofs) { + p_qoa->data_ofs = new_data_ofs; + const uint8_t *src_ptr = (const uint8_t *)base->data; + src_ptr += p_qoa->data_ofs + AudioStreamWAV::DATA_PAD; + qoa_decode_frame(src_ptr, p_qoa->frame_len, p_qoa->desc, p_qoa->dec, &p_qoa->dec_len); + } - if constexpr (sizeof(Depth) == 1) { /* conditions will not exist anymore when compiled! */ - final <<= 8; + uint32_t dec_idx = (interp_pos % QOA_FRAME_LEN) * p_qoa->desc->channels; + + if ((sign > 0 && i == 0) || (sign < 0 && i == 1)) { + final = p_qoa->dec[dec_idx]; + p_qoa->cache[0] = final; + if (is_stereo) { + final_r = p_qoa->dec[dec_idx + 1]; + p_qoa->cache_r[0] = final_r; + } + } else { + next = p_qoa->dec[dec_idx]; + p_qoa->cache[1] = next; + if (is_stereo) { + next_r = p_qoa->dec[dec_idx + 1]; + p_qoa->cache_r[1] = next_r; + } + } + } + p_qoa->cache_pos = pos; + } else { + final = p_qoa->cache[0]; + if (is_stereo) { + final_r = p_qoa->cache_r[0]; + } + + next = p_qoa->cache[1]; + if (is_stereo) { + next_r = p_qoa->cache_r[1]; + } + } + } else { + final = p_src[pos]; if (is_stereo) { - final_r <<= 8; + final_r = p_src[pos + 1]; } - } - if (is_stereo) { - next = p_src[pos + 2]; - next_r = p_src[pos + 3]; - } else { - next = p_src[pos + 1]; - } + if constexpr (sizeof(Depth) == 1) { /* conditions will not exist anymore when compiled! */ + final <<= 8; + if (is_stereo) { + final_r <<= 8; + } + } - if constexpr (sizeof(Depth) == 1) { - next <<= 8; if (is_stereo) { - next_r <<= 8; + next = p_src[pos + 2]; + next_r = p_src[pos + 3]; + } else { + next = p_src[pos + 1]; } - } + if constexpr (sizeof(Depth) == 1) { + next <<= 8; + if (is_stereo) { + next_r <<= 8; + } + } + } int32_t frac = int64_t(p_offset & MIX_FRAC_MASK); final = final + ((next - final) * frac >> MIX_FRAC_BITS); @@ -240,6 +285,9 @@ int AudioStreamPlaybackWAV::mix(AudioFrame *p_buffer, float p_rate_scale, int p_ case AudioStreamWAV::FORMAT_IMA_ADPCM: len *= 2; break; + case AudioStreamWAV::FORMAT_QOA: + len = qoa.desc->samples * qoa.desc->channels; + break; } if (base->stereo) { @@ -368,27 +416,34 @@ int AudioStreamPlaybackWAV::mix(AudioFrame *p_buffer, float p_rate_scale, int p_ switch (base->format) { case AudioStreamWAV::FORMAT_8_BITS: { if (is_stereo) { - do_resample<int8_t, true, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm); + do_resample<int8_t, true, false, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa); } else { - do_resample<int8_t, false, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm); + do_resample<int8_t, false, false, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa); } } break; case AudioStreamWAV::FORMAT_16_BITS: { if (is_stereo) { - do_resample<int16_t, true, false>((int16_t *)data, dst_buff, offset, increment, target, ima_adpcm); + do_resample<int16_t, true, false, false>((int16_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa); } else { - do_resample<int16_t, false, false>((int16_t *)data, dst_buff, offset, increment, target, ima_adpcm); + do_resample<int16_t, false, false, false>((int16_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa); } } break; case AudioStreamWAV::FORMAT_IMA_ADPCM: { if (is_stereo) { - do_resample<int8_t, true, true>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm); + do_resample<int8_t, true, true, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa); } else { - do_resample<int8_t, false, true>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm); + do_resample<int8_t, false, true, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa); } } break; + case AudioStreamWAV::FORMAT_QOA: { + if (is_stereo) { + do_resample<uint8_t, true, false, true>((uint8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa); + } else { + do_resample<uint8_t, false, false, true>((uint8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa); + } + } break; } dst_buff += target; @@ -412,6 +467,16 @@ void AudioStreamPlaybackWAV::tag_used_streams() { AudioStreamPlaybackWAV::AudioStreamPlaybackWAV() {} +AudioStreamPlaybackWAV::~AudioStreamPlaybackWAV() { + if (qoa.desc) { + memfree(qoa.desc); + } + + if (qoa.dec) { + memfree(qoa.dec); + } +} + ///////////////////// void AudioStreamWAV::set_format(Format p_format) { @@ -475,6 +540,10 @@ double AudioStreamWAV::get_length() const { case AudioStreamWAV::FORMAT_IMA_ADPCM: len *= 2; break; + case AudioStreamWAV::FORMAT_QOA: + qoa_desc desc = { 0, 0, 0, { { { 0 }, { 0 } } } }; + qoa_decode_header((uint8_t *)data + DATA_PAD, QOA_MIN_FILESIZE, &desc); + len = desc.samples * desc.channels; } if (stereo) { @@ -526,8 +595,8 @@ Vector<uint8_t> AudioStreamWAV::get_data() const { } Error AudioStreamWAV::save_to_wav(const String &p_path) { - if (format == AudioStreamWAV::FORMAT_IMA_ADPCM) { - WARN_PRINT("Saving IMA_ADPC samples are not supported yet"); + if (format == AudioStreamWAV::FORMAT_IMA_ADPCM || format == AudioStreamWAV::FORMAT_QOA) { + WARN_PRINT("Saving IMA_ADPCM and QOA samples is not supported yet"); return ERR_UNAVAILABLE; } @@ -548,6 +617,7 @@ Error AudioStreamWAV::save_to_wav(const String &p_path) { byte_pr_sample = 1; break; case AudioStreamWAV::FORMAT_16_BITS: + case AudioStreamWAV::FORMAT_QOA: byte_pr_sample = 2; break; case AudioStreamWAV::FORMAT_IMA_ADPCM: @@ -590,6 +660,7 @@ Error AudioStreamWAV::save_to_wav(const String &p_path) { } break; case AudioStreamWAV::FORMAT_16_BITS: + case AudioStreamWAV::FORMAT_QOA: for (unsigned int i = 0; i < data_bytes / 2; i++) { uint16_t data_point = decode_uint16(&read_data[i * 2]); file->store_16(data_point); @@ -607,6 +678,16 @@ Ref<AudioStreamPlayback> AudioStreamWAV::instantiate_playback() { Ref<AudioStreamPlaybackWAV> sample; sample.instantiate(); sample->base = Ref<AudioStreamWAV>(this); + + if (format == AudioStreamWAV::FORMAT_QOA) { + sample->qoa.desc = (qoa_desc *)memalloc(sizeof(qoa_desc)); + qoa_decode_header((uint8_t *)data + DATA_PAD, QOA_MIN_FILESIZE, sample->qoa.desc); + sample->qoa.frame_len = qoa_max_frame_size(sample->qoa.desc); + int samples_len = (sample->qoa.desc->samples > QOA_FRAME_LEN ? QOA_FRAME_LEN : sample->qoa.desc->samples); + int alloc_len = sample->qoa.desc->channels * samples_len * sizeof(int16_t); + sample->qoa.dec = (int16_t *)memalloc(alloc_len); + } + return sample; } @@ -639,7 +720,7 @@ void AudioStreamWAV::_bind_methods() { ClassDB::bind_method(D_METHOD("save_to_wav", "path"), &AudioStreamWAV::save_to_wav); ADD_PROPERTY(PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_data", "get_data"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "format", PROPERTY_HINT_ENUM, "8-Bit,16-Bit,IMA-ADPCM"), "set_format", "get_format"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "format", PROPERTY_HINT_ENUM, "8-Bit,16-Bit,IMA-ADPCM,QOA"), "set_format", "get_format"); ADD_PROPERTY(PropertyInfo(Variant::INT, "loop_mode", PROPERTY_HINT_ENUM, "Disabled,Forward,Ping-Pong,Backward"), "set_loop_mode", "get_loop_mode"); ADD_PROPERTY(PropertyInfo(Variant::INT, "loop_begin"), "set_loop_begin", "get_loop_begin"); ADD_PROPERTY(PropertyInfo(Variant::INT, "loop_end"), "set_loop_end", "get_loop_end"); @@ -649,6 +730,7 @@ void AudioStreamWAV::_bind_methods() { BIND_ENUM_CONSTANT(FORMAT_8_BITS); BIND_ENUM_CONSTANT(FORMAT_16_BITS); BIND_ENUM_CONSTANT(FORMAT_IMA_ADPCM); + BIND_ENUM_CONSTANT(FORMAT_QOA); BIND_ENUM_CONSTANT(LOOP_DISABLED); BIND_ENUM_CONSTANT(LOOP_FORWARD); diff --git a/scene/resources/audio_stream_wav.h b/scene/resources/audio_stream_wav.h index 959d1ceca0..146142d8a4 100644 --- a/scene/resources/audio_stream_wav.h +++ b/scene/resources/audio_stream_wav.h @@ -31,7 +31,11 @@ #ifndef AUDIO_STREAM_WAV_H #define AUDIO_STREAM_WAV_H +#define QOA_IMPLEMENTATION +#define QOA_NO_STDIO + #include "servers/audio/audio_stream.h" +#include "thirdparty/misc/qoa.h" class AudioStreamWAV; @@ -54,14 +58,25 @@ class AudioStreamPlaybackWAV : public AudioStreamPlayback { int32_t window_ofs = 0; } ima_adpcm[2]; + struct QOA_State { + qoa_desc *desc = nullptr; + uint32_t data_ofs = 0; + uint32_t frame_len = 0; + int16_t *dec = nullptr; + uint32_t dec_len = 0; + int64_t cache_pos = -1; + int16_t cache[2] = { 0, 0 }; + int16_t cache_r[2] = { 0, 0 }; + } qoa; + int64_t offset = 0; int sign = 1; bool active = false; friend class AudioStreamWAV; Ref<AudioStreamWAV> base; - template <typename Depth, bool is_stereo, bool is_ima_adpcm> - void do_resample(const Depth *p_src, AudioFrame *p_dst, int64_t &p_offset, int32_t &p_increment, uint32_t p_amount, IMA_ADPCM_State *p_ima_adpcm); + template <typename Depth, bool is_stereo, bool is_ima_adpcm, bool is_qoa> + void do_resample(const Depth *p_src, AudioFrame *p_dst, int64_t &p_offset, int32_t &p_increment, uint32_t p_amount, IMA_ADPCM_State *p_ima_adpcm, QOA_State *p_qoa); public: virtual void start(double p_from_pos = 0.0) override; @@ -78,6 +93,7 @@ public: virtual void tag_used_streams() override; AudioStreamPlaybackWAV(); + ~AudioStreamPlaybackWAV(); }; class AudioStreamWAV : public AudioStream { @@ -88,7 +104,8 @@ public: enum Format { FORMAT_8_BITS, FORMAT_16_BITS, - FORMAT_IMA_ADPCM + FORMAT_IMA_ADPCM, + FORMAT_QOA, }; // Keep the ResourceImporterWAV `edit/loop_mode` enum hint in sync with these options. diff --git a/scene/resources/material.cpp b/scene/resources/material.cpp index b381096df8..15b40e776c 100644 --- a/scene/resources/material.cpp +++ b/scene/resources/material.cpp @@ -688,6 +688,9 @@ void BaseMaterial3D::_update_shader() { case BLEND_MODE_MUL: code += "blend_mul"; break; + case BLEND_MODE_PREMULT_ALPHA: + code += "blend_premul_alpha"; + break; case BLEND_MODE_MAX: break; // Internal value, skip. } @@ -1819,6 +1822,11 @@ void fragment() {)"; vec3 detail = mix(ALBEDO.rgb, ALBEDO.rgb * detail_tex.rgb, detail_tex.a); )"; } break; + case BLEND_MODE_PREMULT_ALPHA: { + // This is unlikely to ever be used for detail textures, and in order for it to function in the editor, another bit must be used in MaterialKey, + // but there are only 5 bits left, so I'm going to leave this disabled unless it's actually requested. + //code += "\tvec3 detail = (1.0-detail_tex.a)*ALBEDO.rgb+detail_tex.rgb;\n"; + } break; case BLEND_MODE_MAX: break; // Internal value, skip. } @@ -3040,7 +3048,7 @@ void BaseMaterial3D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "alpha_hash_scale", PROPERTY_HINT_RANGE, "0,2,0.01"), "set_alpha_hash_scale", "get_alpha_hash_scale"); ADD_PROPERTY(PropertyInfo(Variant::INT, "alpha_antialiasing_mode", PROPERTY_HINT_ENUM, "Disabled,Alpha Edge Blend,Alpha Edge Clip"), "set_alpha_antialiasing", "get_alpha_antialiasing"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "alpha_antialiasing_edge", PROPERTY_HINT_RANGE, "0,1,0.01"), "set_alpha_antialiasing_edge", "get_alpha_antialiasing_edge"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "blend_mode", PROPERTY_HINT_ENUM, "Mix,Add,Subtract,Multiply"), "set_blend_mode", "get_blend_mode"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "blend_mode", PROPERTY_HINT_ENUM, "Mix,Add,Subtract,Multiply,Premultiplied Alpha"), "set_blend_mode", "get_blend_mode"); ADD_PROPERTY(PropertyInfo(Variant::INT, "cull_mode", PROPERTY_HINT_ENUM, "Back,Front,Disabled"), "set_cull_mode", "get_cull_mode"); ADD_PROPERTY(PropertyInfo(Variant::INT, "depth_draw_mode", PROPERTY_HINT_ENUM, "Opaque Only,Always,Never"), "set_depth_draw_mode", "get_depth_draw_mode"); ADD_PROPERTYI(PropertyInfo(Variant::BOOL, "no_depth_test"), "set_flag", "get_flag", FLAG_DISABLE_DEPTH_TEST); @@ -3269,6 +3277,7 @@ void BaseMaterial3D::_bind_methods() { BIND_ENUM_CONSTANT(BLEND_MODE_ADD); BIND_ENUM_CONSTANT(BLEND_MODE_SUB); BIND_ENUM_CONSTANT(BLEND_MODE_MUL); + BIND_ENUM_CONSTANT(BLEND_MODE_PREMULT_ALPHA); BIND_ENUM_CONSTANT(ALPHA_ANTIALIASING_OFF); BIND_ENUM_CONSTANT(ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE); diff --git a/scene/resources/material.h b/scene/resources/material.h index 073403f71e..ecf79c581b 100644 --- a/scene/resources/material.h +++ b/scene/resources/material.h @@ -219,6 +219,7 @@ public: BLEND_MODE_ADD, BLEND_MODE_SUB, BLEND_MODE_MUL, + BLEND_MODE_PREMULT_ALPHA, BLEND_MODE_MAX }; diff --git a/scene/resources/particle_process_material.cpp b/scene/resources/particle_process_material.cpp index 685625ab72..0b65b33240 100644 --- a/scene/resources/particle_process_material.cpp +++ b/scene/resources/particle_process_material.cpp @@ -634,7 +634,7 @@ void ParticleProcessMaterial::_update_shader() { if (emission_shape == EMISSION_SHAPE_RING) { code += " \n"; code += " float ring_spawn_angle = rand_from_seed(alt_seed) * 2.0 * pi;\n"; - code += " float ring_random_radius = rand_from_seed(alt_seed) * (emission_ring_radius - emission_ring_inner_radius) + emission_ring_inner_radius;\n"; + code += " float ring_random_radius = sqrt(rand_from_seed(alt_seed) * (emission_ring_radius - emission_ring_inner_radius * emission_ring_inner_radius) + emission_ring_inner_radius * emission_ring_inner_radius);\n"; code += " vec3 axis = emission_ring_axis == vec3(0.0) ? vec3(0.0, 0.0, 1.0) : normalize(emission_ring_axis);\n"; code += " vec3 ortho_axis = vec3(0.0);\n"; code += " if (abs(axis) == vec3(1.0, 0.0, 0.0)) {\n"; @@ -1136,9 +1136,9 @@ void ParticleProcessMaterial::_update_shader() { code += " if (COLLIDED) emit_count = sub_emitter_amount_at_collision;\n"; } break; case SUB_EMITTER_AT_END: { - code += " float unit_delta = DELTA/LIFETIME;\n"; - code += " float end_time = CUSTOM.w * 0.95;\n"; // if we do at the end we might miss it, as it can just get deactivated by emitter - code += " if (CUSTOM.y < end_time && (CUSTOM.y + unit_delta) >= end_time) emit_count = sub_emitter_amount_at_end;\n"; + code += " if ((CUSTOM.y / CUSTOM.w * LIFETIME) > (LIFETIME - DELTA)) {\n"; + code += " emit_count = sub_emitter_amount_at_end;\n"; + code += " }\n"; } break; default: { } diff --git a/scene/resources/visual_shader.cpp b/scene/resources/visual_shader.cpp index 6f1aa5c850..4b51f6c471 100644 --- a/scene/resources/visual_shader.cpp +++ b/scene/resources/visual_shader.cpp @@ -4928,6 +4928,10 @@ String VisualShaderNodeExpression::generate_code(Shader::Mode p_mode, VisualShad return code; } +bool VisualShaderNodeExpression::is_output_port_expandable(int p_port) const { + return false; +} + void VisualShaderNodeExpression::_bind_methods() { ClassDB::bind_method(D_METHOD("set_expression", "expression"), &VisualShaderNodeExpression::set_expression); ClassDB::bind_method(D_METHOD("get_expression"), &VisualShaderNodeExpression::get_expression); diff --git a/scene/resources/visual_shader.h b/scene/resources/visual_shader.h index d7270f3ac6..d32e2465b9 100644 --- a/scene/resources/visual_shader.h +++ b/scene/resources/visual_shader.h @@ -878,6 +878,7 @@ public: String get_expression() const; virtual String generate_code(Shader::Mode p_mode, VisualShader::Type p_type, int p_id, const String *p_input_vars, const String *p_output_vars, bool p_for_preview = false) const override; + virtual bool is_output_port_expandable(int p_port) const override; VisualShaderNodeExpression(); }; |
