diff --git a/core/debugger/remote_debugger.cpp b/core/debugger/remote_debugger.cpp index e2ed7245a24..fc1b7b74f99 100644 --- a/core/debugger/remote_debugger.cpp +++ b/core/debugger/remote_debugger.cpp @@ -37,6 +37,7 @@ #include "core/debugger/script_debugger.h" #include "core/input/input.h" #include "core/io/resource_loader.h" +#include "core/math/expression.h" #include "core/object/script_language.h" #include "core/os/os.h" #include "servers/display_server.h" @@ -529,6 +530,41 @@ void RemoteDebugger::debug(bool p_can_continue, bool p_is_error_breakpoint) { } else if (command == "set_skip_breakpoints") { ERR_FAIL_COND(data.is_empty()); script_debugger->set_skip_breakpoints(data[0]); + } else if (command == "evaluate") { + String expression_str = data[0]; + int frame = data[1]; + + ScriptInstance *breaked_instance = script_debugger->get_break_language()->debug_get_stack_level_instance(frame); + if (!breaked_instance) { + break; + } + + List locals; + List local_vals; + + script_debugger->get_break_language()->debug_get_stack_level_locals(frame, &locals, &local_vals); + ERR_FAIL_COND(locals.size() != local_vals.size()); + + PackedStringArray locals_vector; + for (const String &S : locals) { + locals_vector.append(S); + } + + Array local_vals_array; + for (const Variant &V : local_vals) { + local_vals_array.append(V); + } + + Expression expression; + expression.parse(expression_str, locals_vector); + const Variant return_val = expression.execute(local_vals_array, breaked_instance->get_owner()); + + DebuggerMarshalls::ScriptStackVariable stvar; + stvar.name = expression_str; + stvar.value = return_val; + stvar.type = 3; + + send_message("evaluation_return", stvar.serialize()); } else { bool captured = false; ERR_CONTINUE(_try_capture(command, data, captured) != OK); diff --git a/core/extension/gdextension_library_loader.cpp b/core/extension/gdextension_library_loader.cpp index 5ba4933c357..d5f2eb668f5 100644 --- a/core/extension/gdextension_library_loader.cpp +++ b/core/extension/gdextension_library_loader.cpp @@ -259,6 +259,10 @@ bool GDExtensionLibraryLoader::has_library_changed() const { return false; } +bool GDExtensionLibraryLoader::library_exists() const { + return FileAccess::exists(resource_path); +} + Error GDExtensionLibraryLoader::parse_gdextension_file(const String &p_path) { resource_path = p_path; diff --git a/core/extension/gdextension_library_loader.h b/core/extension/gdextension_library_loader.h index f4372a75d41..f781611b305 100644 --- a/core/extension/gdextension_library_loader.h +++ b/core/extension/gdextension_library_loader.h @@ -77,6 +77,7 @@ class GDExtensionLibraryLoader : public GDExtensionLoader { virtual void close_library() override; virtual bool is_library_open() const override; virtual bool has_library_changed() const override; + virtual bool library_exists() const override; Error parse_gdextension_file(const String &p_path); }; diff --git a/core/extension/gdextension_loader.h b/core/extension/gdextension_loader.h index 7d779858b72..22895503291 100644 --- a/core/extension/gdextension_loader.h +++ b/core/extension/gdextension_loader.h @@ -42,6 +42,7 @@ class GDExtensionLoader : public RefCounted { virtual void close_library() = 0; virtual bool is_library_open() const = 0; virtual bool has_library_changed() const = 0; + virtual bool library_exists() const = 0; }; #endif // GDEXTENSION_LOADER_H diff --git a/core/extension/gdextension_manager.cpp b/core/extension/gdextension_manager.cpp index 01efe0d96e9..fff938858fd 100644 --- a/core/extension/gdextension_manager.cpp +++ b/core/extension/gdextension_manager.cpp @@ -302,7 +302,8 @@ bool GDExtensionManager::ensure_extensions_loaded(const HashSet &p_exten for (const String &loaded_extension : loaded_extensions) { if (!p_extensions.has(loaded_extension)) { // The extension may not have a .gdextension file. - if (!FileAccess::exists(loaded_extension)) { + const Ref extension = GDExtensionManager::get_singleton()->get_extension(loaded_extension); + if (!extension->get_loader()->library_exists()) { extensions_removed.push_back(loaded_extension); } } diff --git a/core/string/ustring.cpp b/core/string/ustring.cpp index 391a203d5b5..e6f7492a189 100644 --- a/core/string/ustring.cpp +++ b/core/string/ustring.cpp @@ -221,18 +221,35 @@ void CharString::copy_from(const char *p_cstr) { /* String */ /*************************************************************************/ -Error String::parse_url(String &r_scheme, String &r_host, int &r_port, String &r_path) const { - // Splits the URL into scheme, host, port, path. Strip credentials when present. +Error String::parse_url(String &r_scheme, String &r_host, int &r_port, String &r_path, String &r_fragment) const { + // Splits the URL into scheme, host, port, path, fragment. Strip credentials when present. String base = *this; r_scheme = ""; r_host = ""; r_port = 0; r_path = ""; + r_fragment = ""; + int pos = base.find("://"); // Scheme if (pos != -1) { - r_scheme = base.substr(0, pos + 3).to_lower(); - base = base.substr(pos + 3, base.length() - pos - 3); + bool is_scheme_valid = true; + for (int i = 0; i < pos; i++) { + if (!is_ascii_alphanumeric_char(base[i]) && base[i] != '+' && base[i] != '-' && base[i] != '.') { + is_scheme_valid = false; + break; + } + } + if (is_scheme_valid) { + r_scheme = base.substr(0, pos + 3).to_lower(); + base = base.substr(pos + 3, base.length() - pos - 3); + } + } + pos = base.find("#"); + // Fragment + if (pos != -1) { + r_fragment = base.substr(pos + 1); + base = base.substr(0, pos); } pos = base.find("/"); // Path diff --git a/core/string/ustring.h b/core/string/ustring.h index 5d4b209c252..aa62c9cb188 100644 --- a/core/string/ustring.h +++ b/core/string/ustring.h @@ -452,7 +452,7 @@ class String { String c_escape_multiline() const; String c_unescape() const; String json_escape() const; - Error parse_url(String &r_scheme, String &r_host, int &r_port, String &r_path) const; + Error parse_url(String &r_scheme, String &r_host, int &r_port, String &r_path, String &r_fragment) const; String property_name_encode() const; diff --git a/doc/classes/@GlobalScope.xml b/doc/classes/@GlobalScope.xml index 63d20242d62..55d00b6cf90 100644 --- a/doc/classes/@GlobalScope.xml +++ b/doc/classes/@GlobalScope.xml @@ -1209,7 +1209,7 @@ - Returns [code]-1[/code] if [param x] is negative, [code]1[/code] if [param x] is positive, and [code]0[/code] if if [param x] is zero. + Returns [code]-1[/code] if [param x] is negative, [code]1[/code] if [param x] is positive, and [code]0[/code] if [param x] is zero. [codeblock] signi(-6) # Returns -1 signi(0) # Returns 0 diff --git a/doc/classes/Animation.xml b/doc/classes/Animation.xml index 887e9cda819..609d7eff391 100644 --- a/doc/classes/Animation.xml +++ b/doc/classes/Animation.xml @@ -34,6 +34,14 @@ $DOCS_URL/tutorials/animation/index.html + + + + + + Adds a marker to this Animation. + + @@ -271,12 +279,60 @@ Returns the index of the specified track. If the track is not found, return -1. + + + + + Returns the name of the marker located at the given time. + + + + + + + Returns the given marker's color. + + + + + + Returns every marker in this Animation, sorted ascending by time. + + + + + + + Returns the given marker's time. + + + + + + + Returns the closest marker that comes after the given time. If no such marker exists, an empty string is returned. + + + + + + + Returns the closest marker that comes before the given time. If no such marker exists, an empty string is returned. + + Returns the amount of tracks in the animation. + + + + + Returns [code]true[/code] if this Animation contains a marker with the given name. + + @@ -320,6 +376,13 @@ Returns the interpolated position value at the given time (in seconds). The [param track_idx] must be the index of a 3D position track. + + + + + Removes the marker with the given name from this Animation. + + @@ -363,6 +426,14 @@ Returns the interpolated scale value at the given time (in seconds). The [param track_idx] must be the index of a 3D scale track. + + + + + + Sets the given marker's color. + + diff --git a/doc/classes/AnimationPlayer.xml b/doc/classes/AnimationPlayer.xml index 1ca8ac2fa54..9aeb4b71625 100644 --- a/doc/classes/AnimationPlayer.xml +++ b/doc/classes/AnimationPlayer.xml @@ -75,6 +75,24 @@ Returns the node which node path references will travel from. + + + + Returns the end time of the section currently being played. + + + + + + Returns the start time of the section currently being played. + + + + + + Returns [code]true[/code] if an animation is currently playing with section. + + @@ -110,6 +128,54 @@ This method is a shorthand for [method play] with [code]custom_speed = -1.0[/code] and [code]from_end = true[/code], so see its description for more information. + + + + + + + + + + Plays the animation with key [param name] and the section starting from [param start_time] and ending on [param end_time]. See also [method play]. + Setting [param start_time] to a value outside the range of the animation means the start of the animation will be used instead, and setting [param end_time] to a value outside the range of the animation means the end of the animation will be used instead. [param start_time] cannot be equal to [param end_time]. + + + + + + + + + + Plays the animation with key [param name] and the section starting from [param start_time] and ending on [param end_time] in reverse. + This method is a shorthand for [method play_section] with [code]custom_speed = -1.0[/code] and [code]from_end = true[/code], see its description for more information. + + + + + + + + + + + + Plays the animation with key [param name] and the section starting from [param start_marker] and ending on [param end_marker]. + If the start marker is empty, the section starts from the beginning of the animation. If the end marker is empty, the section ends on the end of the animation. See also [method play]. + + + + + + + + + + Plays the animation with key [param name] and the section starting from [param start_marker] and ending on [param end_marker] in reverse. + This method is a shorthand for [method play_section_with_markers] with [code]custom_speed = -1.0[/code] and [code]from_end = true[/code], see its description for more information. + + @@ -139,6 +205,12 @@ [b]Note:[/b] If a looped animation is currently playing, the queued animation will never play unless the looped animation is stopped somehow. + + + + Resets the current section if section is set. + + @@ -180,6 +252,23 @@ Sets the node which node path references will travel from. + + + + + + Changes the start and end times of the section being played. The current playback position will be clamped within the new section. See also [method play_section]. + + + + + + + + Changes the start and end markers of the section being played. The current playback position will be clamped within the new section. See also [method play_section_with_markers]. + If the argument is empty, the section uses the beginning or end of the animation. If both are empty, it means that the section is not set. + + diff --git a/doc/classes/CameraAttributesPhysical.xml b/doc/classes/CameraAttributesPhysical.xml index faedfee7121..e2036162c79 100644 --- a/doc/classes/CameraAttributesPhysical.xml +++ b/doc/classes/CameraAttributesPhysical.xml @@ -25,7 +25,7 @@ The maximum luminance (in EV100) used when calculating auto exposure. When calculating scene average luminance, color values will be clamped to at least this value. This limits the auto-exposure from exposing below a certain brightness, resulting in a cut off point where the scene will remain bright. - The minimum luminance luminance (in EV100) used when calculating auto exposure. When calculating scene average luminance, color values will be clamped to at least this value. This limits the auto-exposure from exposing above a certain brightness, resulting in a cut off point where the scene will remain dark. + The minimum luminance (in EV100) used when calculating auto exposure. When calculating scene average luminance, color values will be clamped to at least this value. This limits the auto-exposure from exposing above a certain brightness, resulting in a cut off point where the scene will remain dark. Size of the aperture of the camera, measured in f-stops. An f-stop is a unitless ratio between the focal length of the camera and the diameter of the aperture. A high aperture setting will result in a smaller aperture which leads to a dimmer image and sharper focus. A low aperture results in a wide aperture which lets in more light resulting in a brighter, less-focused image. Default is appropriate for outdoors at daytime (i.e. for use with a default [DirectionalLight3D]), for indoor lighting, a value between 2 and 4 is more appropriate. diff --git a/doc/classes/EditorExportPlugin.xml b/doc/classes/EditorExportPlugin.xml index 42e1968eb03..aa8e4b3d56c 100644 --- a/doc/classes/EditorExportPlugin.xml +++ b/doc/classes/EditorExportPlugin.xml @@ -159,6 +159,15 @@ Return a [PackedStringArray] of additional features this preset, for the given [param platform], should have. + + + + + + [b]Optional.[/b] + Validates [param option] and returns the visibility for the specified [param platform]. The default implementation returns [code]true[/code] for all options. + + diff --git a/doc/classes/EditorInspector.xml b/doc/classes/EditorInspector.xml index cfdc172fd1d..6b25be490ed 100644 --- a/doc/classes/EditorInspector.xml +++ b/doc/classes/EditorInspector.xml @@ -28,6 +28,7 @@ + diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index 5eb8ac61990..748a6211144 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -772,7 +772,7 @@ Translations are provided by the community. If you spot a mistake, [url=$DOCS_URL/contributing/documentation/editor_and_docs_localization.html]contribute to editor translations on Weblate![/url] - The preferred monitor to display the editor. + The preferred monitor to display the editor. If [b]Auto[/b], the editor will remember the last screen it was displayed on across restarts. Expanding main editor window content to the title, if supported by [DisplayServer]. See [constant DisplayServer.WINDOW_FLAG_EXTEND_TO_TITLE]. @@ -826,9 +826,6 @@ The preferred monitor to display the project manager. - - If [code]true[/code], the editor window will remember its size, position, and which screen it was displayed on across restarts. - If [code]false[/code], the editor will save all scenes when confirming the [b]Save[/b] action when quitting the editor or quitting to the project list. If [code]true[/code], the editor will ask to save each scene individually. diff --git a/doc/classes/PopupMenu.xml b/doc/classes/PopupMenu.xml index 004bbe2286b..d73cda74602 100644 --- a/doc/classes/PopupMenu.xml +++ b/doc/classes/PopupMenu.xml @@ -776,7 +776,7 @@ [StyleBox] for the right side of labeled separator. See [method add_separator]. - [StyleBox] for the the background panel. + [StyleBox] for the background panel. [StyleBox] used for the separators. See [method add_separator]. diff --git a/doc/classes/PopupPanel.xml b/doc/classes/PopupPanel.xml index 399e2854025..b581f32686c 100644 --- a/doc/classes/PopupPanel.xml +++ b/doc/classes/PopupPanel.xml @@ -10,7 +10,7 @@ - [StyleBox] for the the background panel. + [StyleBox] for the background panel. diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index af5ec41b60b..4266bab2a17 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -1369,7 +1369,7 @@ macOS specific override for the shortcut to select the word currently under the caret. - If no selection is currently active with the last caret in text fields, searches for the next occurrence of the the word currently under the caret and moves the caret to the next occurrence. The action can be performed sequentially for other occurrences of the word under the last caret. + If no selection is currently active with the last caret in text fields, searches for the next occurrence of the word currently under the caret and moves the caret to the next occurrence. The action can be performed sequentially for other occurrences of the word under the last caret. If a selection is currently active with the last caret in text fields, searches for the next occurrence of the selection, adds a caret, selects the next occurrence then deselects the previous selection and its associated caret. The action can be performed sequentially for other occurrences of the selection of the last caret. The viewport is adjusted to the latest newly added caret. [b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified. diff --git a/doc/classes/RenderingServer.xml b/doc/classes/RenderingServer.xml index a57f6adec8d..91af70b565f 100644 --- a/doc/classes/RenderingServer.xml +++ b/doc/classes/RenderingServer.xml @@ -913,7 +913,7 @@ Transforms both the current and previous stored transform for a canvas light. - This allows transforming a light without creating a "glitch" in the interpolation, which is is particularly useful for large worlds utilizing a shifting origin. + This allows transforming a light without creating a "glitch" in the interpolation, which is particularly useful for large worlds utilizing a shifting origin. diff --git a/doc/classes/Sprite2D.xml b/doc/classes/Sprite2D.xml index d73cb02d94a..239c4dcf095 100644 --- a/doc/classes/Sprite2D.xml +++ b/doc/classes/Sprite2D.xml @@ -76,7 +76,7 @@ If [code]true[/code], texture is cut from a larger atlas texture. See [member region_rect]. - If [code]true[/code], the outermost pixels get blurred out. [member region_enabled] must be [code]true[/code]. + If [code]true[/code], the area outside of the [member region_rect] is clipped to avoid bleeding of the surrounding texture pixels. [member region_enabled] must be [code]true[/code]. The region of the atlas texture to display. [member region_enabled] must be [code]true[/code]. diff --git a/doc/classes/TreeItem.xml b/doc/classes/TreeItem.xml index 861a53aaad1..132ecc3f926 100644 --- a/doc/classes/TreeItem.xml +++ b/doc/classes/TreeItem.xml @@ -73,6 +73,13 @@ Removes the button at index [param button_index] in column [param column]. + + + + + Returns the column's auto translate mode. + + @@ -493,6 +500,15 @@ Selects the given [param column]. + + + + + + Sets the given column's auto translate mode to [param mode]. + All columns use [constant Node.AUTO_TRANSLATE_MODE_INHERIT] by default, which uses the same auto translate mode as the [Tree] itself. + + diff --git a/doc/classes/Vector4i.xml b/doc/classes/Vector4i.xml index 8f54b767e00..b351f2ccb61 100644 --- a/doc/classes/Vector4i.xml +++ b/doc/classes/Vector4i.xml @@ -216,7 +216,7 @@ - Gets the remainder of each component of the [Vector4i] with the the given [int]. This operation uses truncated division, which is often not desired as it does not work well with negative numbers. Consider using [method @GlobalScope.posmod] instead if you want to handle negative numbers. + Gets the remainder of each component of the [Vector4i] with the given [int]. This operation uses truncated division, which is often not desired as it does not work well with negative numbers. Consider using [method @GlobalScope.posmod] instead if you want to handle negative numbers. [codeblock] print(Vector4i(10, -20, 30, -40) % 7) # Prints "(3, -6, 2, -5)" [/codeblock] diff --git a/drivers/gles3/shaders/canvas.glsl b/drivers/gles3/shaders/canvas.glsl index 76881c8032d..5e7fb3b3389 100644 --- a/drivers/gles3/shaders/canvas.glsl +++ b/drivers/gles3/shaders/canvas.glsl @@ -579,7 +579,8 @@ void main() { #endif if (bool(read_draw_data_flags & FLAGS_CLIP_RECT_UV)) { - uv = clamp(uv, read_draw_data_src_rect.xy, read_draw_data_src_rect.xy + abs(read_draw_data_src_rect.zw)); + vec2 half_texpixel = read_draw_data_color_texture_pixel_size * 0.5; + uv = clamp(uv, read_draw_data_src_rect.xy + half_texpixel, read_draw_data_src_rect.xy + abs(read_draw_data_src_rect.zw) - half_texpixel); } #endif diff --git a/drivers/vulkan/godot_vulkan.h b/drivers/vulkan/godot_vulkan.h new file mode 100644 index 00000000000..f911c5520a8 --- /dev/null +++ b/drivers/vulkan/godot_vulkan.h @@ -0,0 +1,42 @@ +/**************************************************************************/ +/* godot_vulkan.h */ +/**************************************************************************/ +/* 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 GODOT_VULKAN_H +#define GODOT_VULKAN_H + +#ifdef USE_VOLK +#include +#else +#include +#define VK_NO_STDINT_H +#include +#endif + +#endif // GODOT_VULKAN_H diff --git a/drivers/vulkan/rendering_context_driver_vulkan.h b/drivers/vulkan/rendering_context_driver_vulkan.h index f9352d617b8..26de3862061 100644 --- a/drivers/vulkan/rendering_context_driver_vulkan.h +++ b/drivers/vulkan/rendering_context_driver_vulkan.h @@ -40,11 +40,7 @@ #define VK_TRACK_DEVICE_MEMORY #endif -#ifdef USE_VOLK -#include -#else -#include -#endif +#include "drivers/vulkan/godot_vulkan.h" class RenderingContextDriverVulkan : public RenderingContextDriver { public: diff --git a/drivers/vulkan/rendering_device_driver_vulkan.h b/drivers/vulkan/rendering_device_driver_vulkan.h index 81f4256941e..cc15c0a0fef 100644 --- a/drivers/vulkan/rendering_device_driver_vulkan.h +++ b/drivers/vulkan/rendering_device_driver_vulkan.h @@ -43,11 +43,7 @@ #endif #include "thirdparty/vulkan/vk_mem_alloc.h" -#ifdef USE_VOLK -#include -#else -#include -#endif +#include "drivers/vulkan/godot_vulkan.h" // Design principles: // - Vulkan structs are zero-initialized and fields not requiring a non-zero value are omitted (except in cases where expresivity reasons apply). diff --git a/drivers/vulkan/vulkan_hooks.h b/drivers/vulkan/vulkan_hooks.h index bb30b29cec9..82bcc9a0644 100644 --- a/drivers/vulkan/vulkan_hooks.h +++ b/drivers/vulkan/vulkan_hooks.h @@ -31,11 +31,7 @@ #ifndef VULKAN_HOOKS_H #define VULKAN_HOOKS_H -#ifdef USE_VOLK -#include -#else -#include -#endif +#include "drivers/vulkan/godot_vulkan.h" class VulkanHooks { private: diff --git a/editor/animation_track_editor.cpp b/editor/animation_track_editor.cpp index d277ba2f6d7..63f86607e5a 100644 --- a/editor/animation_track_editor.cpp +++ b/editor/animation_track_editor.cpp @@ -39,6 +39,7 @@ #include "editor/editor_string_names.h" #include "editor/editor_undo_redo_manager.h" #include "editor/gui/editor_spin_slider.h" +#include "editor/gui/editor_validation_panel.h" #include "editor/gui/scene_tree_editor.h" #include "editor/inspector_dock.h" #include "editor/multi_node_edit.h" @@ -48,6 +49,7 @@ #include "scene/animation/animation_player.h" #include "scene/animation/tween.h" #include "scene/gui/check_box.h" +#include "scene/gui/color_picker.h" #include "scene/gui/grid_container.h" #include "scene/gui/option_button.h" #include "scene/gui/panel_container.h" @@ -1467,7 +1469,7 @@ void AnimationTimelineEdit::_notification(int p_what) { case NOTIFICATION_DRAW: { int key_range = get_size().width - get_buttons_width() - get_name_limit(); - if (!animation.is_valid()) { + if (animation.is_null()) { return; } @@ -1522,6 +1524,18 @@ void AnimationTimelineEdit::_notification(int p_what) { } } + PackedStringArray markers = animation->get_marker_names(); + if (markers.size() > 0) { + float min_marker = animation->get_marker_time(markers[0]); + float max_marker = animation->get_marker_time(markers[markers.size() - 1]); + if (min_marker < time_min) { + time_min = min_marker; + } + if (max_marker > time_max) { + time_max = max_marker; + } + } + float extra = (zoomw / scale) * 0.5; time_max += extra; @@ -1701,7 +1715,7 @@ void AnimationTimelineEdit::set_zoom(Range *p_zoom) { } void AnimationTimelineEdit::auto_fit() { - if (!animation.is_valid()) { + if (animation.is_null()) { return; } @@ -1780,7 +1794,7 @@ void AnimationTimelineEdit::update_play_position() { } void AnimationTimelineEdit::update_values() { - if (!animation.is_valid() || editing) { + if (animation.is_null() || editing) { return; } @@ -1792,6 +1806,7 @@ void AnimationTimelineEdit::update_values() { time_icon->set_tooltip_text(TTR("Animation length (frames)")); if (track_edit) { track_edit->editor->_update_key_edit(); + track_edit->editor->marker_edit->_update_key_edit(); } } else { length->set_value(animation->get_length()); @@ -1821,7 +1836,7 @@ void AnimationTimelineEdit::update_values() { } void AnimationTimelineEdit::_play_position_draw() { - if (!animation.is_valid() || play_position_pos < 0) { + if (animation.is_null() || play_position_pos < 0) { return; } @@ -1972,6 +1987,7 @@ AnimationTimelineEdit::AnimationTimelineEdit() { Control *expander = memnew(Control); expander->set_h_size_flags(SIZE_EXPAND_FILL); + expander->set_mouse_filter(MOUSE_FILTER_IGNORE); len_hb->add_child(expander); time_icon = memnew(TextureRect); time_icon->set_v_size_flags(SIZE_SHRINK_CENTER); @@ -2124,6 +2140,62 @@ void AnimationTrackEdit::_notification(int p_what) { draw_line(Point2(limit, 0), Point2(limit, get_size().height), h_line_color, Math::round(EDSCALE)); } + // Marker sections. + + { + float scale = timeline->get_zoom_scale(); + int limit_end = get_size().width - timeline->get_buttons_width(); + + PackedStringArray section = editor->get_selected_section(); + if (section.size() == 2) { + StringName start_marker = section[0]; + StringName end_marker = section[1]; + double start_time = animation->get_marker_time(start_marker); + double end_time = animation->get_marker_time(end_marker); + + // When AnimationPlayer is playing, don't move the preview rect, so it still indicates the playback section. + AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); + if (editor->is_marker_moving_selection() && !(player && player->is_playing())) { + start_time += editor->get_marker_moving_selection_offset(); + end_time += editor->get_marker_moving_selection_offset(); + } + + if (start_time < animation->get_length() && end_time >= 0) { + float start_ofs = MAX(0, start_time) - timeline->get_value(); + float end_ofs = MIN(animation->get_length(), end_time) - timeline->get_value(); + start_ofs = start_ofs * scale + limit; + end_ofs = end_ofs * scale + limit; + start_ofs = MAX(start_ofs, limit); + end_ofs = MIN(end_ofs, limit_end); + Rect2 rect; + rect.set_position(Vector2(start_ofs, 0)); + rect.set_size(Vector2(end_ofs - start_ofs, get_size().height)); + + draw_rect(rect, Color(1, 0.1, 0.1, 0.2)); + } + } + } + + // Marker overlays. + + { + float scale = timeline->get_zoom_scale(); + PackedStringArray markers = animation->get_marker_names(); + for (const StringName marker : markers) { + double time = animation->get_marker_time(marker); + if (editor->is_marker_selected(marker) && editor->is_marker_moving_selection()) { + time += editor->get_marker_moving_selection_offset(); + } + if (time >= 0) { + float offset = time - timeline->get_value(); + offset = offset * scale + limit; + Color marker_color = animation->get_marker_color(marker); + marker_color.a = 0.2; + draw_line(Point2(offset, 0), Point2(offset, get_size().height), marker_color); + } + } + } + // Keyframes. draw_bg(limit, get_size().width - timeline->get_buttons_width() - outer_margin); @@ -2352,7 +2424,7 @@ void AnimationTrackEdit::_notification(int p_what) { } int AnimationTrackEdit::get_key_height() const { - if (!animation.is_valid()) { + if (animation.is_null()) { return 0; } @@ -2360,7 +2432,7 @@ int AnimationTrackEdit::get_key_height() const { } Rect2 AnimationTrackEdit::get_key_rect(int p_index, float p_pixels_sec) { - if (!animation.is_valid()) { + if (animation.is_null()) { return Rect2(); } Rect2 rect = Rect2(-type_icon->get_width() / 2, 0, type_icon->get_width(), get_size().height); @@ -2399,7 +2471,7 @@ void AnimationTrackEdit::draw_key_link(int p_index, float p_pixels_sec, int p_x, } void AnimationTrackEdit::draw_key(int p_index, float p_pixels_sec, int p_x, bool p_selected, int p_clip_left, int p_clip_right) { - if (!animation.is_valid()) { + if (animation.is_null()) { return; } @@ -2573,7 +2645,7 @@ void AnimationTrackEdit::set_editor(AnimationTrackEditor *p_editor) { } void AnimationTrackEdit::_play_position_draw() { - if (!animation.is_valid() || play_position_pos < 0) { + if (animation.is_null() || play_position_pos < 0) { return; } @@ -3522,6 +3594,64 @@ void AnimationTrackEditGroup::_notification(int p_what) { draw_style_box(stylebox_header, Rect2(Point2(), get_size())); + int limit = timeline->get_name_limit(); + + // Section preview. + + { + float scale = timeline->get_zoom_scale(); + int limit_end = get_size().width - timeline->get_buttons_width(); + + PackedStringArray section = editor->get_selected_section(); + if (section.size() == 2) { + StringName start_marker = section[0]; + StringName end_marker = section[1]; + double start_time = editor->get_current_animation()->get_marker_time(start_marker); + double end_time = editor->get_current_animation()->get_marker_time(end_marker); + + // When AnimationPlayer is playing, don't move the preview rect, so it still indicates the playback section. + AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); + if (editor->is_marker_moving_selection() && !(player && player->is_playing())) { + start_time += editor->get_marker_moving_selection_offset(); + end_time += editor->get_marker_moving_selection_offset(); + } + + if (start_time < editor->get_current_animation()->get_length() && end_time >= 0) { + float start_ofs = MAX(0, start_time) - timeline->get_value(); + float end_ofs = MIN(editor->get_current_animation()->get_length(), end_time) - timeline->get_value(); + start_ofs = start_ofs * scale + limit; + end_ofs = end_ofs * scale + limit; + start_ofs = MAX(start_ofs, limit); + end_ofs = MIN(end_ofs, limit_end); + Rect2 rect; + rect.set_position(Vector2(start_ofs, 0)); + rect.set_size(Vector2(end_ofs - start_ofs, get_size().height)); + + draw_rect(rect, Color(1, 0.1, 0.1, 0.2)); + } + } + } + + // Marker overlays. + + { + float scale = timeline->get_zoom_scale(); + PackedStringArray markers = editor->get_current_animation()->get_marker_names(); + for (const StringName marker : markers) { + double time = editor->get_current_animation()->get_marker_time(marker); + if (editor->is_marker_selected(marker) && editor->is_marker_moving_selection()) { + time += editor->get_marker_moving_selection_offset(); + } + if (time >= 0) { + float offset = time - timeline->get_value(); + offset = offset * scale + limit; + Color marker_color = editor->get_current_animation()->get_marker_color(marker); + marker_color.a = 0.2; + draw_line(Point2(offset, 0), Point2(offset, get_size().height), marker_color); + } + } + } + draw_line(Point2(), Point2(get_size().width, 0), h_line_color, Math::round(EDSCALE)); draw_line(Point2(timeline->get_name_limit(), 0), Point2(timeline->get_name_limit(), get_size().height), v_line_color, Math::round(EDSCALE)); draw_line(Point2(get_size().width - timeline->get_buttons_width() - outer_margin, 0), Point2(get_size().width - timeline->get_buttons_width() - outer_margin, get_size().height), v_line_color, Math::round(EDSCALE)); @@ -3590,6 +3720,10 @@ void AnimationTrackEditGroup::set_root(Node *p_root) { queue_redraw(); } +void AnimationTrackEditGroup::set_editor(AnimationTrackEditor *p_editor) { + editor = p_editor; +} + void AnimationTrackEditGroup::_zoom_changed() { queue_redraw(); } @@ -3623,6 +3757,9 @@ void AnimationTrackEditor::set_animation(const Ref &p_anim, bool p_re read_only = p_read_only; timeline->set_animation(p_anim, read_only); + marker_edit->set_animation(p_anim, read_only); + marker_edit->set_play_position(timeline->get_play_position()); + _cancel_bezier_edit(); _update_tracks(); @@ -3873,6 +4010,7 @@ void AnimationTrackEditor::_track_grab_focus(int p_track) { void AnimationTrackEditor::set_anim_pos(float p_pos) { timeline->set_play_position(p_pos); + marker_edit->set_play_position(p_pos); for (int i = 0; i < track_edits.size(); i++) { track_edits[i]->set_play_position(p_pos); } @@ -4043,7 +4181,7 @@ void AnimationTrackEditor::insert_transform_key(Node3D *p_node, const String &p_ if (!keying) { return; } - if (!animation.is_valid()) { + if (animation.is_null()) { return; } @@ -4083,7 +4221,7 @@ bool AnimationTrackEditor::has_track(Node3D *p_node, const String &p_sub, const if (!keying) { return false; } - if (!animation.is_valid()) { + if (animation.is_null()) { return false; } @@ -4230,6 +4368,22 @@ void AnimationTrackEditor::insert_node_value_key(Node *p_node, const String &p_p _query_insert(id); } +PackedStringArray AnimationTrackEditor::get_selected_section() const { + return marker_edit->get_selected_section(); +} + +bool AnimationTrackEditor::is_marker_selected(const StringName &p_marker) const { + return marker_edit->is_marker_selected(p_marker); +} + +bool AnimationTrackEditor::is_marker_moving_selection() const { + return marker_edit->is_moving_selection(); +} + +float AnimationTrackEditor::get_marker_moving_selection_offset() const { + return marker_edit->get_moving_selection_offset(); +} + void AnimationTrackEditor::insert_value_key(const String &p_property, bool p_advance) { EditorSelectionHistory *history = EditorNode::get_singleton()->get_editor_selection_history(); @@ -4316,7 +4470,7 @@ void AnimationTrackEditor::_confirm_insert_list() { PropertyInfo AnimationTrackEditor::_find_hint_for_track(int p_idx, NodePath &r_base_path, Variant *r_current_val) { r_base_path = NodePath(); - ERR_FAIL_COND_V(!animation.is_valid(), PropertyInfo()); + ERR_FAIL_COND_V(animation.is_null(), PropertyInfo()); ERR_FAIL_INDEX_V(p_idx, animation->get_track_count(), PropertyInfo()); if (!root) { @@ -4769,6 +4923,7 @@ void AnimationTrackEditor::_update_tracks() { g->set_root(root); g->set_tooltip_text(tooltip); g->set_timeline(timeline); + g->set_editor(this); groups.push_back(g); VBoxContainer *vb = memnew(VBoxContainer); vb->add_theme_constant_override("separation", 0); @@ -4860,12 +5015,13 @@ void AnimationTrackEditor::_snap_mode_changed(int p_mode) { if (key_edit) { key_edit->set_use_fps(use_fps); } + marker_edit->set_use_fps(use_fps); step->set_step(use_fps ? FPS_DECIMAL : SECOND_DECIMAL); _update_step_spinbox(); } void AnimationTrackEditor::_update_step_spinbox() { - if (!animation.is_valid()) { + if (animation.is_null()) { return; } step->set_block_signals(true); @@ -4978,6 +5134,7 @@ void AnimationTrackEditor::_notification(int p_what) { void AnimationTrackEditor::_update_scroll(double) { _redraw_tracks(); _redraw_groups(); + marker_edit->queue_redraw(); } void AnimationTrackEditor::_update_step(double p_new_step) { @@ -5253,6 +5410,8 @@ void AnimationTrackEditor::_timeline_value_changed(double) { bezier_edit->queue_redraw(); bezier_edit->update_play_position(); + + marker_edit->update_play_position(); } int AnimationTrackEditor::_get_track_selected() { @@ -5445,6 +5604,8 @@ void AnimationTrackEditor::_key_selected(int p_key, bool p_single, int p_track) _redraw_tracks(); _update_key_edit(); + + marker_edit->_clear_selection(marker_edit->is_selection_active()); } void AnimationTrackEditor::_key_deselected(int p_key, int p_track) { @@ -5513,7 +5674,7 @@ void AnimationTrackEditor::_clear_selection(bool p_update) { void AnimationTrackEditor::_update_key_edit() { _clear_key_edit(); - if (!animation.is_valid()) { + if (animation.is_null()) { return; } @@ -5600,6 +5761,8 @@ void AnimationTrackEditor::_select_at_anim(const Ref &p_anim, int p_t selection.insert(sk, ki); _update_key_edit(); + + marker_edit->_clear_selection(marker_edit->is_selection_active()); } void AnimationTrackEditor::_move_selection_commit() { @@ -7311,6 +7474,15 @@ AnimationTrackEditor::AnimationTrackEditor() { box_selection_container->set_clip_contents(true); timeline_vbox->add_child(box_selection_container); + marker_edit = memnew(AnimationMarkerEdit); + timeline->get_child(0)->add_child(marker_edit); + marker_edit->set_editor(this); + marker_edit->set_timeline(timeline); + marker_edit->set_h_size_flags(SIZE_EXPAND_FILL); + marker_edit->set_anchors_and_offsets_preset(Control::LayoutPreset::PRESET_FULL_RECT); + marker_edit->connect(SceneStringName(draw), callable_mp(this, &AnimationTrackEditor::_redraw_groups)); + marker_edit->connect(SceneStringName(draw), callable_mp(this, &AnimationTrackEditor::_redraw_tracks)); + scroll = memnew(ScrollContainer); box_selection_container->add_child(scroll); scroll->set_anchors_and_offsets_preset(PRESET_FULL_RECT); @@ -7826,3 +7998,1203 @@ AnimationTrackKeyEditEditor::AnimationTrackKeyEditEditor(Ref p_animat AnimationTrackKeyEditEditor::~AnimationTrackKeyEditEditor() { } + +void AnimationMarkerEdit::_zoom_changed() { + queue_redraw(); + play_position->queue_redraw(); +} + +void AnimationMarkerEdit::_menu_selected(int p_index) { + switch (p_index) { + case MENU_KEY_INSERT: { + _insert_marker(insert_at_pos); + } break; + case MENU_KEY_RENAME: { + if (selection.size() > 0) { + _rename_marker(*selection.last()); + } + } break; + case MENU_KEY_DELETE: { + _delete_selected_markers(); + } break; + case MENU_KEY_TOGGLE_MARKER_NAMES: { + should_show_all_marker_names = !should_show_all_marker_names; + queue_redraw(); + } break; + } +} + +void AnimationMarkerEdit::_play_position_draw() { + if (animation.is_null() || play_position_pos < 0) { + return; + } + + float scale = timeline->get_zoom_scale(); + int h = get_size().height; + + int px = (play_position_pos - timeline->get_value()) * scale + timeline->get_name_limit(); + + if (px >= timeline->get_name_limit() && px < (get_size().width - timeline->get_buttons_width())) { + Color color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor)); + play_position->draw_line(Point2(px, 0), Point2(px, h), color, Math::round(2 * EDSCALE)); + } +} + +bool AnimationMarkerEdit::_try_select_at_ui_pos(const Point2 &p_pos, bool p_aggregate, bool p_deselectable) { + int limit = timeline->get_name_limit(); + int limit_end = get_size().width - timeline->get_buttons_width(); + // Left Border including space occupied by keyframes on t=0. + int limit_start_hitbox = limit - type_icon->get_width(); + + if (p_pos.x >= limit_start_hitbox && p_pos.x <= limit_end) { + int key_idx = -1; + float key_distance = 1e20; + PackedStringArray names = animation->get_marker_names(); + for (int i = 0; i < names.size(); i++) { + Rect2 rect = const_cast(this)->get_key_rect(timeline->get_zoom_scale()); + float offset = animation->get_marker_time(names[i]) - timeline->get_value(); + offset = offset * timeline->get_zoom_scale() + limit; + rect.position.x += offset; + if (rect.has_point(p_pos)) { + if (const_cast(this)->is_key_selectable_by_distance()) { + float distance = Math::abs(offset - p_pos.x); + if (key_idx == -1 || distance < key_distance) { + key_idx = i; + key_distance = distance; + } + } else { + // First one does it. + break; + } + } + } + + if (key_idx != -1) { + if (p_aggregate) { + StringName name = names[key_idx]; + if (selection.has(name)) { + if (p_deselectable) { + call_deferred("_deselect_key", name); + moving_selection_pivot = 0.0f; + moving_selection_mouse_begin_x = 0.0f; + } + } else { + call_deferred("_select_key", name, false); + moving_selection_attempt = true; + moving_selection_effective = false; + select_single_attempt = StringName(); + moving_selection_pivot = animation->get_marker_time(name); + moving_selection_mouse_begin_x = p_pos.x; + } + + } else { + StringName name = names[key_idx]; + if (!selection.has(name)) { + call_deferred("_select_key", name, true); + select_single_attempt = StringName(); + } else { + select_single_attempt = name; + } + + moving_selection_attempt = true; + moving_selection_effective = false; + moving_selection_pivot = animation->get_marker_time(name); + moving_selection_mouse_begin_x = p_pos.x; + } + + if (read_only) { + moving_selection_attempt = false; + moving_selection_pivot = 0.0f; + moving_selection_mouse_begin_x = 0.0f; + } + return true; + } + } + + return false; +} + +bool AnimationMarkerEdit::_is_ui_pos_in_current_section(const Point2 &p_pos) { + int limit = timeline->get_name_limit(); + int limit_end = get_size().width - timeline->get_buttons_width(); + + if (p_pos.x >= limit && p_pos.x <= limit_end) { + PackedStringArray section = get_selected_section(); + if (!section.is_empty()) { + StringName start_marker = section[0]; + StringName end_marker = section[1]; + float start_offset = (animation->get_marker_time(start_marker) - timeline->get_value()) * timeline->get_zoom_scale() + limit; + float end_offset = (animation->get_marker_time(end_marker) - timeline->get_value()) * timeline->get_zoom_scale() + limit; + return p_pos.x >= start_offset && p_pos.x <= end_offset; + } + } + + return false; +} + +HBoxContainer *AnimationMarkerEdit::_create_hbox_labeled_control(const String &p_text, Control *p_control) const { + HBoxContainer *hbox = memnew(HBoxContainer); + Label *label = memnew(Label); + label->set_text(p_text); + hbox->add_child(label); + hbox->add_child(p_control); + hbox->set_h_size_flags(SIZE_EXPAND_FILL); + label->set_h_size_flags(SIZE_EXPAND_FILL); + label->set_stretch_ratio(1.0); + p_control->set_h_size_flags(SIZE_EXPAND_FILL); + p_control->set_stretch_ratio(1.0); + return hbox; +} + +void AnimationMarkerEdit::_update_key_edit() { + _clear_key_edit(); + if (animation.is_null()) { + return; + } + + if (selection.size() == 1) { + key_edit = memnew(AnimationMarkerKeyEdit); + key_edit->animation = animation; + key_edit->animation_read_only = read_only; + key_edit->marker_name = *selection.begin(); + key_edit->use_fps = timeline->is_using_fps(); + key_edit->marker_edit = this; + + EditorNode::get_singleton()->push_item(key_edit); + + InspectorDock::get_singleton()->set_info(TTR("Marker name is read-only in the inspector."), TTR("A marker's name can only be changed by right-clicking it in the animation editor and selecting \"Rename Marker\", in order to make sure that marker names are all unique."), true); + } else if (selection.size() > 1) { + multi_key_edit = memnew(AnimationMultiMarkerKeyEdit); + multi_key_edit->animation = animation; + multi_key_edit->animation_read_only = read_only; + multi_key_edit->marker_edit = this; + for (const StringName &name : selection) { + multi_key_edit->marker_names.push_back(name); + } + + EditorNode::get_singleton()->push_item(multi_key_edit); + } +} + +void AnimationMarkerEdit::_clear_key_edit() { + if (key_edit) { + // If key edit is the object being inspected, remove it first. + if (InspectorDock::get_inspector_singleton()->get_edited_object() == key_edit) { + EditorNode::get_singleton()->push_item(nullptr); + } + + // Then actually delete it. + memdelete(key_edit); + key_edit = nullptr; + } + + if (multi_key_edit) { + if (InspectorDock::get_inspector_singleton()->get_edited_object() == multi_key_edit) { + EditorNode::get_singleton()->push_item(nullptr); + } + + memdelete(multi_key_edit); + multi_key_edit = nullptr; + } +} + +void AnimationMarkerEdit::_bind_methods() { + ClassDB::bind_method("_clear_selection_for_anim", &AnimationMarkerEdit::_clear_selection_for_anim); + ClassDB::bind_method("_select_key", &AnimationMarkerEdit::_select_key); + ClassDB::bind_method("_deselect_key", &AnimationMarkerEdit::_deselect_key); +} + +void AnimationMarkerEdit::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_THEME_CHANGED: { + if (animation.is_null()) { + return; + } + + type_icon = get_editor_theme_icon(SNAME("Marker")); + selected_icon = get_editor_theme_icon(SNAME("MarkerSelected")); + } break; + + case NOTIFICATION_DRAW: { + if (animation.is_null()) { + return; + } + + int limit = timeline->get_name_limit(); + + Ref font = get_theme_font(SceneStringName(font), SNAME("Label")); + Color color = get_theme_color(SceneStringName(font_color), SNAME("Label")); + int hsep = get_theme_constant(SNAME("h_separation"), SNAME("ItemList")); + Color linecolor = color; + linecolor.a = 0.2; + + // SECTION PREVIEW // + + { + float scale = timeline->get_zoom_scale(); + int limit_end = get_size().width - timeline->get_buttons_width(); + + PackedStringArray section = get_selected_section(); + if (section.size() == 2) { + StringName start_marker = section[0]; + StringName end_marker = section[1]; + double start_time = animation->get_marker_time(start_marker); + double end_time = animation->get_marker_time(end_marker); + + // When AnimationPlayer is playing, don't move the preview rect, so it still indicates the playback section. + AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); + if (moving_selection && !(player && player->is_playing())) { + start_time += moving_selection_offset; + end_time += moving_selection_offset; + } + + if (start_time < animation->get_length() && end_time >= 0) { + float start_ofs = MAX(0, start_time) - timeline->get_value(); + float end_ofs = MIN(animation->get_length(), end_time) - timeline->get_value(); + start_ofs = start_ofs * scale + limit; + end_ofs = end_ofs * scale + limit; + start_ofs = MAX(start_ofs, limit); + end_ofs = MIN(end_ofs, limit_end); + Rect2 rect; + rect.set_position(Vector2(start_ofs, 0)); + rect.set_size(Vector2(end_ofs - start_ofs, get_size().height)); + + draw_rect(rect, Color(1, 0.1, 0.1, 0.2)); + } + } + } + + // KEYFRAMES // + + draw_bg(limit, get_size().width - timeline->get_buttons_width()); + + { + float scale = timeline->get_zoom_scale(); + int limit_end = get_size().width - timeline->get_buttons_width(); + + PackedStringArray names = animation->get_marker_names(); + for (int i = 0; i < names.size(); i++) { + StringName name = names[i]; + bool is_selected = selection.has(name); + float offset = animation->get_marker_time(name) - timeline->get_value(); + if (is_selected && moving_selection) { + offset += moving_selection_offset; + } + + offset = offset * scale + limit; + + draw_key(name, scale, int(offset), is_selected, limit, limit_end); + + const int font_size = 16; + Size2 string_size = font->get_string_size(name, HORIZONTAL_ALIGNMENT_LEFT, -1.0, font_size); + if (int(offset) <= limit_end && int(offset) >= limit && should_show_all_marker_names) { + float bottom = get_size().height + string_size.y - font->get_descent(font_size); + float extrusion = MAX(0, offset + string_size.x - limit_end); // How much the string would extrude outside limit_end if unadjusted. + Color marker_color = animation->get_marker_color(name); + draw_string(font, Point2(offset - extrusion, bottom), name, HORIZONTAL_ALIGNMENT_LEFT, -1.0, font_size, marker_color); + draw_string_outline(font, Point2(offset - extrusion, bottom), name, HORIZONTAL_ALIGNMENT_LEFT, -1.0, font_size, 1, color); + } + } + } + + draw_fg(limit, get_size().width - timeline->get_buttons_width()); + + // BUTTONS // + + { + int ofs = get_size().width - timeline->get_buttons_width(); + + draw_line(Point2(ofs, 0), Point2(ofs, get_size().height), linecolor, Math::round(EDSCALE)); + + ofs += hsep; + } + + draw_line(Vector2(0, get_size().height), get_size(), linecolor, Math::round(EDSCALE)); + } break; + + case NOTIFICATION_MOUSE_ENTER: + hovered = true; + queue_redraw(); + break; + case NOTIFICATION_MOUSE_EXIT: + hovered = false; + // When the mouse cursor exits the track, we're no longer hovering any keyframe. + hovering_marker = StringName(); + queue_redraw(); + break; + } +} + +void AnimationMarkerEdit::gui_input(const Ref &p_event) { + ERR_FAIL_COND(p_event.is_null()); + + if (animation.is_null()) { + return; + } + + if (p_event->is_pressed()) { + if (ED_IS_SHORTCUT("animation_marker_edit/rename_marker", p_event)) { + if (!read_only) { + _menu_selected(MENU_KEY_RENAME); + } + } + + if (ED_IS_SHORTCUT("animation_marker_edit/delete_selection", p_event)) { + if (!read_only) { + _menu_selected(MENU_KEY_DELETE); + } + } + + if (ED_IS_SHORTCUT("animation_marker_edit/toggle_marker_names", p_event)) { + if (!read_only) { + _menu_selected(MENU_KEY_TOGGLE_MARKER_NAMES); + } + } + } + + Ref mb = p_event; + + if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { + Point2 pos = mb->get_position(); + if (_try_select_at_ui_pos(pos, mb->is_command_or_control_pressed() || mb->is_shift_pressed(), true)) { + accept_event(); + } else if (!_is_ui_pos_in_current_section(pos)) { + _clear_selection_for_anim(animation); + } + } + + if (mb.is_valid() && moving_selection_attempt) { + if (!mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { + moving_selection_attempt = false; + if (moving_selection && moving_selection_effective) { + if (Math::abs(moving_selection_offset) > CMP_EPSILON) { + _move_selection_commit(); + accept_event(); // So play position doesn't snap to the end of move selection. + } + } else if (select_single_attempt) { + call_deferred("_select_key", select_single_attempt, true); + + // First select click should not affect play position. + if (!selection.has(select_single_attempt)) { + accept_event(); + } else { + // Second click and onwards should snap to marker time. + double ofs = animation->get_marker_time(select_single_attempt); + timeline->set_play_position(ofs); + timeline->emit_signal(SNAME("timeline_changed"), ofs, mb->is_alt_pressed()); + accept_event(); + } + } else { + // First select click should not affect play position. + if (!selection.has(select_single_attempt)) { + accept_event(); + } + } + + moving_selection = false; + select_single_attempt = StringName(); + } + + if (moving_selection && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) { + moving_selection_attempt = false; + moving_selection = false; + _move_selection_cancel(); + } + } + + if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) { + Point2 pos = mb->get_position(); + if (pos.x >= timeline->get_name_limit() && pos.x <= get_size().width - timeline->get_buttons_width()) { + // Can do something with menu too! show insert key. + float offset = (pos.x - timeline->get_name_limit()) / timeline->get_zoom_scale(); + if (!read_only) { + bool selected = _try_select_at_ui_pos(pos, mb->is_command_or_control_pressed() || mb->is_shift_pressed(), false); + + menu->clear(); + menu->add_icon_item(get_editor_theme_icon(SNAME("Key")), TTR("Insert Marker..."), MENU_KEY_INSERT); + + if (selected || selection.size() > 0) { + menu->add_icon_item(get_editor_theme_icon(SNAME("Edit")), TTR("Rename Marker"), MENU_KEY_RENAME); + menu->add_icon_item(get_editor_theme_icon(SNAME("Remove")), TTR("Delete Marker(s)"), MENU_KEY_DELETE); + } + + menu->add_icon_item(get_editor_theme_icon(should_show_all_marker_names ? SNAME("GuiChecked") : SNAME("GuiUnchecked")), TTR("Show All Marker Names"), MENU_KEY_TOGGLE_MARKER_NAMES); + menu->reset_size(); + + moving_selection_attempt = false; + moving_selection = false; + + menu->set_position(get_screen_position() + get_local_mouse_position()); + menu->popup(); + + insert_at_pos = offset + timeline->get_value(); + accept_event(); + } + } + } + + Ref mm = p_event; + + if (mm.is_valid()) { + const StringName previous_hovering_marker = hovering_marker; + + // Hovering compressed keyframes for editing is not possible. + const float scale = timeline->get_zoom_scale(); + const int limit = timeline->get_name_limit(); + const int limit_end = get_size().width - timeline->get_buttons_width(); + // Left Border including space occupied by keyframes on t=0. + const int limit_start_hitbox = limit - type_icon->get_width(); + const Point2 pos = mm->get_position(); + + if (pos.x >= limit_start_hitbox && pos.x <= limit_end) { + // Use the same logic as key selection to ensure that hovering accurately represents + // which key will be selected when clicking. + int key_idx = -1; + float key_distance = 1e20; + + hovering_marker = StringName(); + + PackedStringArray names = animation->get_marker_names(); + + // Hovering should happen in the opposite order of drawing for more accurate overlap hovering. + for (int i = names.size() - 1; i >= 0; i--) { + StringName name = names[i]; + Rect2 rect = get_key_rect(scale); + float offset = animation->get_marker_time(name) - timeline->get_value(); + offset = offset * scale + limit; + rect.position.x += offset; + + if (rect.has_point(pos)) { + if (is_key_selectable_by_distance()) { + const float distance = Math::abs(offset - pos.x); + if (key_idx == -1 || distance < key_distance) { + key_idx = i; + key_distance = distance; + hovering_marker = name; + } + } else { + // First one does it. + hovering_marker = name; + break; + } + } + } + + if (hovering_marker != previous_hovering_marker) { + // Required to draw keyframe hover feedback on the correct keyframe. + queue_redraw(); + } + } + } + + if (mm.is_valid() && mm->get_button_mask().has_flag(MouseButtonMask::LEFT) && moving_selection_attempt) { + if (!moving_selection) { + moving_selection = true; + _move_selection_begin(); + } + + float moving_begin_time = ((moving_selection_mouse_begin_x - timeline->get_name_limit()) / timeline->get_zoom_scale()) + timeline->get_value(); + float new_time = ((mm->get_position().x - timeline->get_name_limit()) / timeline->get_zoom_scale()) + timeline->get_value(); + float delta = new_time - moving_begin_time; + float snapped_time = editor->snap_time(moving_selection_pivot + delta); + + float offset = 0.0; + if (Math::abs(editor->get_moving_selection_offset()) > CMP_EPSILON || (snapped_time > moving_selection_pivot && delta > CMP_EPSILON) || (snapped_time < moving_selection_pivot && delta < -CMP_EPSILON)) { + offset = snapped_time - moving_selection_pivot; + moving_selection_effective = true; + } + + _move_selection(offset); + } +} + +String AnimationMarkerEdit::get_tooltip(const Point2 &p_pos) const { + if (animation.is_null()) { + return Control::get_tooltip(p_pos); + } + + int limit = timeline->get_name_limit(); + int limit_end = get_size().width - timeline->get_buttons_width(); + // Left Border including space occupied by keyframes on t=0. + int limit_start_hitbox = limit - type_icon->get_width(); + + if (p_pos.x >= limit_start_hitbox && p_pos.x <= limit_end) { + int key_idx = -1; + float key_distance = 1e20; + + PackedStringArray names = animation->get_marker_names(); + + // Select should happen in the opposite order of drawing for more accurate overlap select. + for (int i = names.size() - 1; i >= 0; i--) { + StringName name = names[i]; + Rect2 rect = const_cast(this)->get_key_rect(timeline->get_zoom_scale()); + float offset = animation->get_marker_time(name) - timeline->get_value(); + offset = offset * timeline->get_zoom_scale() + limit; + rect.position.x += offset; + + if (rect.has_point(p_pos)) { + if (const_cast(this)->is_key_selectable_by_distance()) { + float distance = ABS(offset - p_pos.x); + if (key_idx == -1 || distance < key_distance) { + key_idx = i; + key_distance = distance; + } + } else { + // First one does it. + break; + } + } + } + + if (key_idx != -1) { + String name = names[key_idx]; + String text = TTR("Time (s):") + " " + TS->format_number(rtos(Math::snapped(animation->get_marker_time(name), 0.0001))) + "\n"; + text += TTR("Marker:") + " " + name + "\n"; + return text; + } + } + + return Control::get_tooltip(p_pos); +} + +int AnimationMarkerEdit::get_key_height() const { + if (animation.is_null()) { + return 0; + } + + return type_icon->get_height(); +} + +Rect2 AnimationMarkerEdit::get_key_rect(float p_pixels_sec) const { + if (animation.is_null()) { + return Rect2(); + } + + Rect2 rect = Rect2(-type_icon->get_width() / 2, get_size().height - type_icon->get_size().height, type_icon->get_width(), type_icon->get_size().height); + + // Make it a big easier to click. + rect.position.x -= rect.size.x * 0.5; + rect.size.x *= 2; + return rect; +} + +PackedStringArray AnimationMarkerEdit::get_selected_section() const { + if (selection.size() >= 2) { + PackedStringArray arr; + arr.push_back(""); // Marker with smallest time. + arr.push_back(""); // Marker with largest time. + double min_time = INFINITY; + double max_time = -INFINITY; + for (const StringName &marker_name : selection) { + double time = animation->get_marker_time(marker_name); + if (time < min_time) { + arr.set(0, marker_name); + min_time = time; + } + if (time > max_time) { + arr.set(1, marker_name); + max_time = time; + } + } + return arr; + } + + return PackedStringArray(); +} + +bool AnimationMarkerEdit::is_marker_selected(const StringName &p_marker) const { + return selection.has(p_marker); +} + +bool AnimationMarkerEdit::is_key_selectable_by_distance() const { + return true; +} + +void AnimationMarkerEdit::draw_key(const StringName &p_name, float p_pixels_sec, int p_x, bool p_selected, int p_clip_left, int p_clip_right) { + if (animation.is_null()) { + return; + } + + if (p_x < p_clip_left || p_x > p_clip_right) { + return; + } + + Ref icon_to_draw = p_selected ? selected_icon : type_icon; + + Vector2 ofs(p_x - icon_to_draw->get_width() / 2, int(get_size().height - icon_to_draw->get_height())); + + // Don't apply custom marker color when the key is selected. + Color marker_color = p_selected ? Color(1, 1, 1) : animation->get_marker_color(p_name); + + // Use a different color for the currently hovered key. + // The color multiplier is chosen to work with both dark and light editor themes, + // and on both unselected and selected key icons. + draw_texture( + icon_to_draw, + ofs, + p_name == hovering_marker ? get_theme_color(SNAME("folder_icon_color"), SNAME("FileDialog")) : marker_color); +} + +void AnimationMarkerEdit::draw_bg(int p_clip_left, int p_clip_right) { +} + +void AnimationMarkerEdit::draw_fg(int p_clip_left, int p_clip_right) { +} + +Ref AnimationMarkerEdit::get_animation() const { + return animation; +} + +void AnimationMarkerEdit::set_animation(const Ref &p_animation, bool p_read_only) { + if (animation.is_valid()) { + _clear_selection_for_anim(animation); + } + animation = p_animation; + read_only = p_read_only; + type_icon = get_editor_theme_icon(SNAME("Marker")); + selected_icon = get_editor_theme_icon(SNAME("MarkerSelected")); + + queue_redraw(); +} + +Size2 AnimationMarkerEdit::get_minimum_size() const { + Ref texture = get_editor_theme_icon(SNAME("Object")); + Ref font = get_theme_font(SceneStringName(font), SNAME("Label")); + int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label")); + int separation = get_theme_constant(SNAME("v_separation"), SNAME("ItemList")); + + int max_h = MAX(texture->get_height(), font->get_height(font_size)); + max_h = MAX(max_h, get_key_height()); + + return Vector2(1, max_h + separation); +} + +void AnimationMarkerEdit::set_timeline(AnimationTimelineEdit *p_timeline) { + timeline = p_timeline; + timeline->connect("zoom_changed", callable_mp(this, &AnimationMarkerEdit::_zoom_changed)); + timeline->connect("name_limit_changed", callable_mp(this, &AnimationMarkerEdit::_zoom_changed)); +} + +void AnimationMarkerEdit::set_editor(AnimationTrackEditor *p_editor) { + editor = p_editor; +} + +void AnimationMarkerEdit::set_play_position(float p_pos) { + play_position_pos = p_pos; + play_position->queue_redraw(); +} + +void AnimationMarkerEdit::update_play_position() { + play_position->queue_redraw(); +} + +void AnimationMarkerEdit::set_use_fps(bool p_use_fps) { + if (key_edit) { + key_edit->use_fps = p_use_fps; + key_edit->notify_property_list_changed(); + } +} + +void AnimationMarkerEdit::_move_selection_begin() { + moving_selection = true; + moving_selection_offset = 0; +} + +void AnimationMarkerEdit::_move_selection(float p_offset) { + moving_selection_offset = p_offset; + queue_redraw(); +} + +void AnimationMarkerEdit::_move_selection_commit() { + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Animation Move Markers")); + + for (HashSet::Iterator E = selection.last(); E; --E) { + StringName name = *E; + double time = animation->get_marker_time(name); + float newpos = time + moving_selection_offset; + undo_redo->add_do_method(animation.ptr(), "remove_marker", name); + undo_redo->add_do_method(animation.ptr(), "add_marker", name, newpos); + undo_redo->add_do_method(animation.ptr(), "set_marker_color", name, animation->get_marker_color(name)); + undo_redo->add_undo_method(animation.ptr(), "remove_marker", name); + undo_redo->add_undo_method(animation.ptr(), "add_marker", name, time); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", name, animation->get_marker_color(name)); + + // add_marker will overwrite the overlapped key on the redo pass, so we add it back on the undo pass. + if (StringName overlap = animation->get_marker_at_time(newpos)) { + if (select_single_attempt == overlap) { + select_single_attempt = ""; + } + undo_redo->add_undo_method(animation.ptr(), "add_marker", overlap, newpos); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", overlap, animation->get_marker_color(overlap)); + } + } + + moving_selection = false; + AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); + if (player) { + PackedStringArray selected_section = get_selected_section(); + if (selected_section.size() >= 2) { + undo_redo->add_do_method(player, "set_section_with_markers", selected_section[0], selected_section[1]); + undo_redo->add_undo_method(player, "set_section_with_markers", selected_section[0], selected_section[1]); + } + } + undo_redo->add_do_method(timeline, "queue_redraw"); + undo_redo->add_undo_method(timeline, "queue_redraw"); + undo_redo->add_do_method(this, "queue_redraw"); + undo_redo->add_undo_method(this, "queue_redraw"); + undo_redo->commit_action(); + _update_key_edit(); +} + +void AnimationMarkerEdit::_delete_selected_markers() { + if (selection.size()) { + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Animation Delete Keys")); + for (const StringName &name : selection) { + double time = animation->get_marker_time(name); + undo_redo->add_do_method(animation.ptr(), "remove_marker", name); + undo_redo->add_undo_method(animation.ptr(), "add_marker", name, time); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", name, animation->get_marker_color(name)); + } + _clear_selection_for_anim(animation); + + undo_redo->add_do_method(this, "queue_redraw"); + undo_redo->add_undo_method(this, "queue_redraw"); + undo_redo->commit_action(); + _update_key_edit(); + } +} + +void AnimationMarkerEdit::_move_selection_cancel() { + moving_selection = false; + queue_redraw(); +} + +void AnimationMarkerEdit::_clear_selection(bool p_update) { + AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); + if (player) { + player->reset_section(); + } + + selection.clear(); + + if (p_update) { + queue_redraw(); + } + + _clear_key_edit(); +} + +void AnimationMarkerEdit::_clear_selection_for_anim(const Ref &p_anim) { + if (animation != p_anim) { + return; + } + + _clear_selection(true); +} + +void AnimationMarkerEdit::_select_key(const StringName &p_name, bool is_single) { + if (is_single) { + _clear_selection(false); + } + + selection.insert(p_name); + + AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); + if (player) { + if (selection.size() >= 2) { + PackedStringArray selected_section = get_selected_section(); + double start_time = animation->get_marker_time(selected_section[0]); + double end_time = animation->get_marker_time(selected_section[1]); + player->set_section(start_time, end_time); + } else { + player->reset_section(); + } + } + + queue_redraw(); + _update_key_edit(); + + editor->_clear_selection(editor->is_selection_active()); +} + +void AnimationMarkerEdit::_deselect_key(const StringName &p_name) { + selection.erase(p_name); + + AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); + if (player) { + if (selection.size() >= 2) { + PackedStringArray selected_section = get_selected_section(); + double start_time = animation->get_marker_time(selected_section[0]); + double end_time = animation->get_marker_time(selected_section[1]); + player->set_section(start_time, end_time); + } else { + player->reset_section(); + } + } + + queue_redraw(); + _update_key_edit(); +} + +void AnimationMarkerEdit::_insert_marker(float p_ofs) { + if (editor->is_snap_timeline_enabled()) { + p_ofs = editor->snap_time(p_ofs); + } + + marker_insert_confirm->popup_centered(Size2(200, 100) * EDSCALE); + marker_insert_color->set_pick_color(Color(1, 1, 1)); + + String base = "new_marker"; + int count = 1; + while (true) { + String attempt = base; + if (count > 1) { + attempt += vformat("_%d", count); + } + if (animation->has_marker(attempt)) { + count++; + continue; + } + base = attempt; + break; + } + + marker_insert_new_name->set_text(base); + _marker_insert_new_name_changed(base); + marker_insert_ofs = p_ofs; +} + +void AnimationMarkerEdit::_rename_marker(const StringName &p_name) { + marker_rename_confirm->popup_centered(Size2i(200, 0) * EDSCALE); + marker_rename_prev_name = p_name; + marker_rename_new_name->set_text(p_name); +} + +void AnimationMarkerEdit::_marker_insert_confirmed() { + StringName name = marker_insert_new_name->get_text(); + + if (animation->has_marker(name)) { + marker_insert_error_dialog->set_text(vformat(TTR("Marker '%s' already exists!"), name)); + marker_insert_error_dialog->popup_centered(); + return; + } + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + + undo_redo->create_action(TTR("Add Marker Key")); + undo_redo->add_do_method(animation.ptr(), "add_marker", name, marker_insert_ofs); + undo_redo->add_undo_method(animation.ptr(), "remove_marker", name); + StringName existing_marker = animation->get_marker_at_time(marker_insert_ofs); + if (existing_marker) { + undo_redo->add_undo_method(animation.ptr(), "add_marker", existing_marker, marker_insert_ofs); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", existing_marker, animation->get_marker_color(existing_marker)); + } + undo_redo->add_do_method(animation.ptr(), "set_marker_color", name, marker_insert_color->get_pick_color()); + + undo_redo->add_do_method(this, "queue_redraw"); + undo_redo->add_undo_method(this, "queue_redraw"); + + undo_redo->commit_action(); + + marker_insert_confirm->hide(); +} + +void AnimationMarkerEdit::_marker_insert_new_name_changed(const String &p_text) { + marker_insert_confirm->get_ok_button()->set_disabled(p_text.is_empty()); +} + +void AnimationMarkerEdit::_marker_rename_confirmed() { + StringName new_name = marker_rename_new_name->get_text(); + StringName prev_name = marker_rename_prev_name; + + if (new_name == StringName()) { + marker_rename_error_dialog->set_text(TTR("Empty marker names are not allowed.")); + marker_rename_error_dialog->popup_centered(); + return; + } + + if (new_name != prev_name && animation->has_marker(new_name)) { + marker_rename_error_dialog->set_text(vformat(TTR("Marker '%s' already exists!"), new_name)); + marker_rename_error_dialog->popup_centered(); + return; + } + + if (prev_name != new_name) { + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Rename Marker")); + undo_redo->add_do_method(animation.ptr(), "remove_marker", prev_name); + undo_redo->add_do_method(animation.ptr(), "add_marker", new_name, animation->get_marker_time(prev_name)); + undo_redo->add_do_method(animation.ptr(), "set_marker_color", new_name, animation->get_marker_color(prev_name)); + undo_redo->add_undo_method(animation.ptr(), "remove_marker", new_name); + undo_redo->add_undo_method(animation.ptr(), "add_marker", prev_name, animation->get_marker_time(prev_name)); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", prev_name, animation->get_marker_color(prev_name)); + undo_redo->add_do_method(this, "_select_key", new_name, true); + undo_redo->add_undo_method(this, "_select_key", prev_name, true); + undo_redo->commit_action(); + select_single_attempt = StringName(); + } + marker_rename_confirm->hide(); +} + +void AnimationMarkerEdit::_marker_rename_new_name_changed(const String &p_text) { + marker_rename_confirm->get_ok_button()->set_disabled(p_text.is_empty()); +} + +AnimationMarkerEdit::AnimationMarkerEdit() { + play_position = memnew(Control); + play_position->set_mouse_filter(MOUSE_FILTER_PASS); + add_child(play_position); + play_position->connect(SceneStringName(draw), callable_mp(this, &AnimationMarkerEdit::_play_position_draw)); + set_focus_mode(FOCUS_CLICK); + set_mouse_filter(MOUSE_FILTER_PASS); // Scroll has to work too for selection. + + menu = memnew(PopupMenu); + add_child(menu); + menu->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationMarkerEdit::_menu_selected)); + menu->add_shortcut(ED_SHORTCUT("animation_marker_edit/rename_marker", TTR("Rename Marker"), Key::R), MENU_KEY_RENAME); + menu->add_shortcut(ED_SHORTCUT("animation_marker_edit/delete_selection", TTR("Delete Markers (s)"), Key::KEY_DELETE), MENU_KEY_DELETE); + menu->add_shortcut(ED_SHORTCUT("animation_marker_edit/toggle_marker_names", TTR("Show All Marker Names"), Key::M), MENU_KEY_TOGGLE_MARKER_NAMES); + + marker_insert_confirm = memnew(ConfirmationDialog); + marker_insert_confirm->set_title(TTR("Insert Marker")); + marker_insert_confirm->set_hide_on_ok(false); + marker_insert_confirm->connect(SceneStringName(confirmed), callable_mp(this, &AnimationMarkerEdit::_marker_insert_confirmed)); + add_child(marker_insert_confirm); + VBoxContainer *marker_insert_vbox = memnew(VBoxContainer); + marker_insert_vbox->set_anchors_and_offsets_preset(Control::LayoutPreset::PRESET_FULL_RECT); + marker_insert_confirm->add_child(marker_insert_vbox); + marker_insert_new_name = memnew(LineEdit); + marker_insert_new_name->connect(SceneStringName(text_changed), callable_mp(this, &AnimationMarkerEdit::_marker_insert_new_name_changed)); + marker_insert_confirm->register_text_enter(marker_insert_new_name); + marker_insert_vbox->add_child(_create_hbox_labeled_control(TTR("Marker Name"), marker_insert_new_name)); + marker_insert_color = memnew(ColorPickerButton); + marker_insert_color->set_edit_alpha(false); + marker_insert_color->get_popup()->connect("about_to_popup", callable_mp(EditorNode::get_singleton(), &EditorNode::setup_color_picker).bind(marker_insert_color->get_picker())); + marker_insert_vbox->add_child(_create_hbox_labeled_control(TTR("Marker Color"), marker_insert_color)); + marker_insert_error_dialog = memnew(AcceptDialog); + marker_insert_error_dialog->set_ok_button_text(TTR("Close")); + marker_insert_error_dialog->set_title(TTR("Error!")); + marker_insert_confirm->add_child(marker_insert_error_dialog); + + marker_rename_confirm = memnew(ConfirmationDialog); + marker_rename_confirm->set_title(TTR("Rename Marker")); + marker_rename_confirm->set_hide_on_ok(false); + marker_rename_confirm->connect(SceneStringName(confirmed), callable_mp(this, &AnimationMarkerEdit::_marker_rename_confirmed)); + add_child(marker_rename_confirm); + VBoxContainer *marker_rename_vbox = memnew(VBoxContainer); + marker_rename_vbox->set_anchors_and_offsets_preset(Control::LayoutPreset::PRESET_FULL_RECT); + marker_rename_confirm->add_child(marker_rename_vbox); + Label *marker_rename_new_name_label = memnew(Label); + marker_rename_new_name_label->set_text(TTR("Change Marker Name:")); + marker_rename_vbox->add_child(marker_rename_new_name_label); + marker_rename_new_name = memnew(LineEdit); + marker_rename_new_name->connect(SceneStringName(text_changed), callable_mp(this, &AnimationMarkerEdit::_marker_rename_new_name_changed)); + marker_rename_confirm->register_text_enter(marker_rename_new_name); + marker_rename_vbox->add_child(marker_rename_new_name); + + marker_rename_error_dialog = memnew(AcceptDialog); + marker_rename_error_dialog->set_ok_button_text(TTR("Close")); + marker_rename_error_dialog->set_title(TTR("Error!")); + marker_rename_confirm->add_child(marker_rename_error_dialog); +} + +AnimationMarkerEdit::~AnimationMarkerEdit() { +} + +float AnimationMarkerKeyEdit::get_time() const { + return animation->get_marker_time(marker_name); +} + +void AnimationMarkerKeyEdit::_bind_methods() { + ClassDB::bind_method(D_METHOD("_hide_script_from_inspector"), &AnimationMarkerKeyEdit::_hide_script_from_inspector); + ClassDB::bind_method(D_METHOD("_hide_metadata_from_inspector"), &AnimationMarkerKeyEdit::_hide_metadata_from_inspector); + ClassDB::bind_method(D_METHOD("_dont_undo_redo"), &AnimationMarkerKeyEdit::_dont_undo_redo); + ClassDB::bind_method(D_METHOD("_is_read_only"), &AnimationMarkerKeyEdit::_is_read_only); + ClassDB::bind_method(D_METHOD("_set_marker_name"), &AnimationMarkerKeyEdit::_set_marker_name); +} + +void AnimationMarkerKeyEdit::_set_marker_name(const StringName &p_name) { + marker_name = p_name; +} + +bool AnimationMarkerKeyEdit::_set(const StringName &p_name, const Variant &p_value) { + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + + if (p_name == "color") { + Color color = p_value; + Color prev_color = animation->get_marker_color(marker_name); + if (color != prev_color) { + undo_redo->create_action(TTR("Edit Marker Color"), UndoRedo::MERGE_ENDS); + undo_redo->add_do_method(animation.ptr(), "set_marker_color", marker_name, color); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", marker_name, prev_color); + undo_redo->add_do_method(marker_edit, "queue_redraw"); + undo_redo->add_undo_method(marker_edit, "queue_redraw"); + undo_redo->commit_action(); + } + return true; + } + + return false; +} + +bool AnimationMarkerKeyEdit::_get(const StringName &p_name, Variant &r_ret) const { + if (p_name == "name") { + r_ret = marker_name; + return true; + } + + if (p_name == "color") { + r_ret = animation->get_marker_color(marker_name); + return true; + } + + return false; +} + +void AnimationMarkerKeyEdit::_get_property_list(List *p_list) const { + if (animation.is_null()) { + return; + } + + p_list->push_back(PropertyInfo(Variant::STRING_NAME, "name", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_READ_ONLY | PROPERTY_USAGE_EDITOR)); + p_list->push_back(PropertyInfo(Variant::COLOR, "color", PROPERTY_HINT_COLOR_NO_ALPHA)); +} + +void AnimationMultiMarkerKeyEdit::_bind_methods() { + ClassDB::bind_method(D_METHOD("_hide_script_from_inspector"), &AnimationMultiMarkerKeyEdit::_hide_script_from_inspector); + ClassDB::bind_method(D_METHOD("_hide_metadata_from_inspector"), &AnimationMultiMarkerKeyEdit::_hide_metadata_from_inspector); + ClassDB::bind_method(D_METHOD("_dont_undo_redo"), &AnimationMultiMarkerKeyEdit::_dont_undo_redo); + ClassDB::bind_method(D_METHOD("_is_read_only"), &AnimationMultiMarkerKeyEdit::_is_read_only); +} + +bool AnimationMultiMarkerKeyEdit::_set(const StringName &p_name, const Variant &p_value) { + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + if (p_name == "color") { + Color color = p_value; + + undo_redo->create_action(TTR("Multi Edit Marker Color"), UndoRedo::MERGE_ENDS); + + for (const StringName &marker_name : marker_names) { + undo_redo->add_do_method(animation.ptr(), "set_marker_color", marker_name, color); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", marker_name, animation->get_marker_color(marker_name)); + } + + undo_redo->add_do_method(marker_edit, "queue_redraw"); + undo_redo->add_undo_method(marker_edit, "queue_redraw"); + undo_redo->commit_action(); + + return true; + } + + return false; +} + +bool AnimationMultiMarkerKeyEdit::_get(const StringName &p_name, Variant &r_ret) const { + if (p_name == "color") { + r_ret = animation->get_marker_color(marker_names[0]); + return true; + } + + return false; +} + +void AnimationMultiMarkerKeyEdit::_get_property_list(List *p_list) const { + if (animation.is_null()) { + return; + } + + p_list->push_back(PropertyInfo(Variant::COLOR, "color", PROPERTY_HINT_COLOR_NO_ALPHA)); +} + +// AnimationMarkerKeyEditEditorPlugin + +void AnimationMarkerKeyEditEditor::_time_edit_entered() { +} + +void AnimationMarkerKeyEditEditor::_time_edit_exited() { + real_t new_time = spinner->get_value(); + + if (use_fps) { + real_t fps = animation->get_step(); + if (fps > 0) { + fps = 1.0 / fps; + } + new_time /= fps; + } + + real_t prev_time = animation->get_marker_time(marker_name); + + if (Math::is_equal_approx(new_time, prev_time)) { + return; // No change. + } + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Animation Change Marker Time"), UndoRedo::MERGE_ENDS); + + Color color = animation->get_marker_color(marker_name); + undo_redo->add_do_method(animation.ptr(), "add_marker", marker_name, new_time); + undo_redo->add_do_method(animation.ptr(), "set_marker_color", marker_name, color); + undo_redo->add_undo_method(animation.ptr(), "remove_marker", marker_name); + undo_redo->add_undo_method(animation.ptr(), "add_marker", marker_name, prev_time); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", marker_name, color); + StringName existing_marker = animation->get_marker_at_time(new_time); + if (existing_marker) { + undo_redo->add_undo_method(animation.ptr(), "add_marker", existing_marker, animation->get_marker_time(existing_marker)); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", existing_marker, animation->get_marker_color(existing_marker)); + } + AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton(); + if (ape) { + AnimationTrackEditor *ate = ape->get_track_editor(); + if (ate) { + AnimationMarkerEdit *ame = ate->marker_edit; + undo_redo->add_do_method(ame, "queue_redraw"); + undo_redo->add_undo_method(ame, "queue_redraw"); + } + } + undo_redo->commit_action(); +} + +AnimationMarkerKeyEditEditor::AnimationMarkerKeyEditEditor(Ref p_animation, const StringName &p_name, bool p_use_fps) { + if (p_animation.is_null()) { + return; + } + + animation = p_animation; + use_fps = p_use_fps; + marker_name = p_name; + + set_label("Time"); + + spinner = memnew(EditorSpinSlider); + spinner->set_focus_mode(Control::FOCUS_CLICK); + spinner->set_min(0); + spinner->set_allow_greater(true); + spinner->set_allow_lesser(true); + + float time = animation->get_marker_time(marker_name); + + if (use_fps) { + spinner->set_step(FPS_DECIMAL); + real_t fps = animation->get_step(); + if (fps > 0) { + fps = 1.0 / fps; + } + spinner->set_value(time * fps); + } else { + spinner->set_step(SECOND_DECIMAL); + spinner->set_value(time); + spinner->set_max(animation->get_length()); + } + + add_child(spinner); + + spinner->connect("grabbed", callable_mp(this, &AnimationMarkerKeyEditEditor::_time_edit_entered), CONNECT_DEFERRED); + spinner->connect("ungrabbed", callable_mp(this, &AnimationMarkerKeyEditEditor::_time_edit_exited), CONNECT_DEFERRED); + spinner->connect("value_focus_entered", callable_mp(this, &AnimationMarkerKeyEditEditor::_time_edit_entered), CONNECT_DEFERRED); + spinner->connect("value_focus_exited", callable_mp(this, &AnimationMarkerKeyEditEditor::_time_edit_exited), CONNECT_DEFERRED); +} + +AnimationMarkerKeyEditEditor::~AnimationMarkerKeyEditEditor() { +} diff --git a/editor/animation_track_editor.h b/editor/animation_track_editor.h index 6b9140ddaa7..0da474afd40 100644 --- a/editor/animation_track_editor.h +++ b/editor/animation_track_editor.h @@ -41,9 +41,11 @@ #include "scene/gui/tree.h" #include "scene/resources/animation.h" +class AnimationMarkerEdit; class AnimationTrackEditor; class AnimationTrackEdit; class CheckBox; +class ColorPickerButton; class EditorSpinSlider; class HSlider; class OptionButton; @@ -52,6 +54,7 @@ class SceneTreeDialog; class SpinBox; class TextureRect; class ViewPanner; +class EditorValidationPanel; class AnimationTrackKeyEdit : public Object { GDCLASS(AnimationTrackKeyEdit, Object); @@ -128,6 +131,58 @@ class AnimationMultiTrackKeyEdit : public Object { void _get_property_list(List *p_list) const; }; +class AnimationMarkerKeyEdit : public Object { + GDCLASS(AnimationMarkerKeyEdit, Object); + +public: + bool animation_read_only = false; + + Ref animation; + StringName marker_name; + bool use_fps = false; + + AnimationMarkerEdit *marker_edit = nullptr; + + bool _hide_script_from_inspector() { return true; } + bool _hide_metadata_from_inspector() { return true; } + bool _dont_undo_redo() { return true; } + + bool _is_read_only() { return animation_read_only; } + + float get_time() const; + +protected: + static void _bind_methods(); + void _set_marker_name(const StringName &p_name); + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List *p_list) const; +}; + +class AnimationMultiMarkerKeyEdit : public Object { + GDCLASS(AnimationMultiMarkerKeyEdit, Object); + +public: + bool animation_read_only = false; + + Ref animation; + Vector marker_names; + + AnimationMarkerEdit *marker_edit = nullptr; + + bool _hide_script_from_inspector() { return true; } + bool _hide_metadata_from_inspector() { return true; } + bool _dont_undo_redo() { return true; } + + bool _is_read_only() { return animation_read_only; } + +protected: + static void _bind_methods(); + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List *p_list) const; +}; + class AnimationTimelineEdit : public Range { GDCLASS(AnimationTimelineEdit, Range); @@ -218,6 +273,140 @@ class AnimationTimelineEdit : public Range { AnimationTimelineEdit(); }; +class AnimationMarkerEdit : public Control { + GDCLASS(AnimationMarkerEdit, Control); + friend class AnimationTimelineEdit; + + enum { + MENU_KEY_INSERT, + MENU_KEY_RENAME, + MENU_KEY_DELETE, + MENU_KEY_TOGGLE_MARKER_NAMES, + }; + + AnimationTimelineEdit *timeline = nullptr; + Control *play_position = nullptr; // Separate control used to draw so updates for only position changed are much faster. + float play_position_pos = 0.0f; + + HashSet selection; + + Ref animation; + bool read_only = false; + + Ref type_icon; + Ref selected_icon; + + PopupMenu *menu = nullptr; + + bool hovered = false; + StringName hovering_marker; + + void _zoom_changed(); + + Ref icon_cache; + + void _menu_selected(int p_index); + + void _play_position_draw(); + bool _try_select_at_ui_pos(const Point2 &p_pos, bool p_aggregate, bool p_deselectable); + bool _is_ui_pos_in_current_section(const Point2 &p_pos); + + float insert_at_pos = 0.0f; + bool moving_selection_attempt = false; + bool moving_selection_effective = false; + float moving_selection_offset = 0.0f; + float moving_selection_pivot = 0.0f; + float moving_selection_mouse_begin_x = 0.0f; + float moving_selection_mouse_begin_y = 0.0f; + StringName select_single_attempt; + bool moving_selection = false; + void _move_selection_begin(); + void _move_selection(float p_offset); + void _move_selection_commit(); + void _move_selection_cancel(); + + void _clear_selection_for_anim(const Ref &p_anim); + void _select_key(const StringName &p_name, bool is_single = false); + void _deselect_key(const StringName &p_name); + + void _insert_marker(float p_ofs); + void _rename_marker(const StringName &p_name); + void _delete_selected_markers(); + + ConfirmationDialog *marker_insert_confirm = nullptr; + LineEdit *marker_insert_new_name = nullptr; + ColorPickerButton *marker_insert_color = nullptr; + AcceptDialog *marker_insert_error_dialog = nullptr; + float marker_insert_ofs = 0; + + ConfirmationDialog *marker_rename_confirm = nullptr; + LineEdit *marker_rename_new_name = nullptr; + StringName marker_rename_prev_name; + + AcceptDialog *marker_rename_error_dialog = nullptr; + + bool should_show_all_marker_names = false; + + ////////////// edit menu stuff + + void _marker_insert_confirmed(); + void _marker_insert_new_name_changed(const String &p_text); + void _marker_rename_confirmed(); + void _marker_rename_new_name_changed(const String &p_text); + + AnimationTrackEditor *editor = nullptr; + + HBoxContainer *_create_hbox_labeled_control(const String &p_text, Control *p_control) const; + + void _update_key_edit(); + void _clear_key_edit(); + + AnimationMarkerKeyEdit *key_edit = nullptr; + AnimationMultiMarkerKeyEdit *multi_key_edit = nullptr; + +protected: + static void _bind_methods(); + void _notification(int p_what); + + virtual void gui_input(const Ref &p_event) override; + +public: + virtual String get_tooltip(const Point2 &p_pos) const override; + + virtual int get_key_height() const; + virtual Rect2 get_key_rect(float p_pixels_sec) const; + virtual bool is_key_selectable_by_distance() const; + virtual void draw_key(const StringName &p_name, float p_pixels_sec, int p_x, bool p_selected, int p_clip_left, int p_clip_right); + virtual void draw_bg(int p_clip_left, int p_clip_right); + virtual void draw_fg(int p_clip_left, int p_clip_right); + + Ref get_animation() const; + AnimationTimelineEdit *get_timeline() const { return timeline; } + AnimationTrackEditor *get_editor() const { return editor; } + bool is_selection_active() const { return !selection.is_empty(); } + bool is_moving_selection() const { return moving_selection; } + float get_moving_selection_offset() const { return moving_selection_offset; } + void set_animation(const Ref &p_animation, bool p_read_only); + virtual Size2 get_minimum_size() const override; + + void set_timeline(AnimationTimelineEdit *p_timeline); + void set_editor(AnimationTrackEditor *p_editor); + + void set_play_position(float p_pos); + void update_play_position(); + + void set_use_fps(bool p_use_fps); + + PackedStringArray get_selected_section() const; + bool is_marker_selected(const StringName &p_marker) const; + + // For use by AnimationTrackEditor. + void _clear_selection(bool p_update); + + AnimationMarkerEdit(); + ~AnimationMarkerEdit(); +}; + class AnimationTrackEdit : public Control { GDCLASS(AnimationTrackEdit, Control); friend class AnimationTimelineEdit; @@ -367,6 +556,7 @@ class AnimationTrackEditGroup : public Control { NodePath node; Node *root = nullptr; AnimationTimelineEdit *timeline = nullptr; + AnimationTrackEditor *editor = nullptr; void _zoom_changed(); @@ -380,6 +570,7 @@ class AnimationTrackEditGroup : public Control { virtual Size2 get_minimum_size() const override; void set_timeline(AnimationTimelineEdit *p_timeline); void set_root(Node *p_root); + void set_editor(AnimationTrackEditor *p_editor); AnimationTrackEditGroup(); }; @@ -388,6 +579,7 @@ class AnimationTrackEditor : public VBoxContainer { GDCLASS(AnimationTrackEditor, VBoxContainer); friend class AnimationTimelineEdit; friend class AnimationBezierTrackEdit; + friend class AnimationMarkerKeyEditEditor; Ref animation; bool read_only = false; @@ -405,6 +597,7 @@ class AnimationTrackEditor : public VBoxContainer { Label *info_message = nullptr; AnimationTimelineEdit *timeline = nullptr; + AnimationMarkerEdit *marker_edit = nullptr; HSlider *zoom = nullptr; EditorSpinSlider *step = nullptr; TextureRect *zoom_icon = nullptr; @@ -743,6 +936,10 @@ class AnimationTrackEditor : public VBoxContainer { float get_moving_selection_offset() const; float snap_time(float p_value, bool p_relative = false); bool is_grouping_tracks(); + PackedStringArray get_selected_section() const; + bool is_marker_selected(const StringName &p_marker) const; + bool is_marker_moving_selection() const; + float get_marker_moving_selection_offset() const; /** If `p_from_mouse_event` is `true`, handle Shift key presses for precise snapping. */ void goto_prev_step(bool p_from_mouse_event); @@ -781,4 +978,23 @@ class AnimationTrackKeyEditEditor : public EditorProperty { ~AnimationTrackKeyEditEditor(); }; +// AnimationMarkerKeyEditEditorPlugin + +class AnimationMarkerKeyEditEditor : public EditorProperty { + GDCLASS(AnimationMarkerKeyEditEditor, EditorProperty); + + Ref animation; + StringName marker_name; + bool use_fps = false; + + EditorSpinSlider *spinner = nullptr; + + void _time_edit_entered(); + void _time_edit_exited(); + +public: + AnimationMarkerKeyEditEditor(Ref p_animation, const StringName &p_name, bool p_use_fps); + ~AnimationMarkerKeyEditEditor(); +}; + #endif // ANIMATION_TRACK_EDITOR_H diff --git a/editor/debugger/debug_adapter/debug_adapter_protocol.cpp b/editor/debugger/debug_adapter/debug_adapter_protocol.cpp index 4febb8bf047..f847d3be7b1 100644 --- a/editor/debugger/debug_adapter/debug_adapter_protocol.cpp +++ b/editor/debugger/debug_adapter/debug_adapter_protocol.cpp @@ -966,7 +966,7 @@ void DebugAdapterProtocol::on_debug_stack_frame_var(const Array &p_data) { List scope_ids = stackframe_list.find(frame)->value; ERR_FAIL_COND(scope_ids.size() != 3); - ERR_FAIL_INDEX(stack_var.type, 3); + ERR_FAIL_INDEX(stack_var.type, 4); int var_id = scope_ids.get(stack_var.type); DAP::Variable variable; diff --git a/editor/debugger/editor_debugger_inspector.cpp b/editor/debugger/editor_debugger_inspector.cpp index cb5e4375a64..e085e2e4480 100644 --- a/editor/debugger/editor_debugger_inspector.cpp +++ b/editor/debugger/editor_debugger_inspector.cpp @@ -223,7 +223,7 @@ Object *EditorDebuggerInspector::get_object(ObjectID p_id) { return nullptr; } -void EditorDebuggerInspector::add_stack_variable(const Array &p_array) { +void EditorDebuggerInspector::add_stack_variable(const Array &p_array, int p_offset) { DebuggerMarshalls::ScriptStackVariable var; var.deserialize(p_array); String n = var.name; @@ -248,6 +248,9 @@ void EditorDebuggerInspector::add_stack_variable(const Array &p_array) { case 2: type = "Globals/"; break; + case 3: + type = "Evaluated/"; + break; default: type = "Unknown/"; } @@ -258,7 +261,15 @@ void EditorDebuggerInspector::add_stack_variable(const Array &p_array) { pinfo.hint = h; pinfo.hint_string = hs; - variables->prop_list.push_back(pinfo); + if ((p_offset == -1) || variables->prop_list.is_empty()) { + variables->prop_list.push_back(pinfo); + } else { + List::Element *current = variables->prop_list.front(); + for (int i = 0; i < p_offset; i++) { + current = current->next(); + } + variables->prop_list.insert_before(current, pinfo); + } variables->prop_values[type + n] = v; variables->update(); edit(variables); diff --git a/editor/debugger/editor_debugger_inspector.h b/editor/debugger/editor_debugger_inspector.h index 73dd773750d..fac95259436 100644 --- a/editor/debugger/editor_debugger_inspector.h +++ b/editor/debugger/editor_debugger_inspector.h @@ -90,7 +90,7 @@ class EditorDebuggerInspector : public EditorInspector { // Stack Dump variables String get_stack_variable(const String &p_var); - void add_stack_variable(const Array &p_arr); + void add_stack_variable(const Array &p_arr, int p_offset = -1); void clear_stack_variables(); }; diff --git a/editor/debugger/editor_debugger_server.cpp b/editor/debugger/editor_debugger_server.cpp index c0efc6a1fc6..9ec60581324 100644 --- a/editor/debugger/editor_debugger_server.cpp +++ b/editor/debugger/editor_debugger_server.cpp @@ -77,8 +77,8 @@ Error EditorDebuggerServerTCP::start(const String &p_uri) { // Optionally override if (!p_uri.is_empty() && p_uri != "tcp://") { - String scheme, path; - Error err = p_uri.parse_url(scheme, bind_host, bind_port, path); + String scheme, path, fragment; + Error err = p_uri.parse_url(scheme, bind_host, bind_port, path, fragment); ERR_FAIL_COND_V(err != OK, ERR_INVALID_PARAMETER); ERR_FAIL_COND_V(!bind_host.is_valid_ip_address() && bind_host != "*", ERR_INVALID_PARAMETER); } diff --git a/editor/debugger/editor_expression_evaluator.cpp b/editor/debugger/editor_expression_evaluator.cpp new file mode 100644 index 00000000000..e8b1e33d205 --- /dev/null +++ b/editor/debugger/editor_expression_evaluator.cpp @@ -0,0 +1,148 @@ +/**************************************************************************/ +/* editor_expression_evaluator.cpp */ +/**************************************************************************/ +/* 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. */ +/**************************************************************************/ + +#include "editor_expression_evaluator.h" + +#include "editor/debugger/editor_debugger_inspector.h" +#include "editor/debugger/script_editor_debugger.h" +#include "scene/gui/button.h" +#include "scene/gui/check_box.h" + +void EditorExpressionEvaluator::on_start() { + expression_input->set_editable(false); + evaluate_btn->set_disabled(true); + + if (clear_on_run_checkbox->is_pressed()) { + inspector->clear_stack_variables(); + } +} + +void EditorExpressionEvaluator::set_editor_debugger(ScriptEditorDebugger *p_editor_debugger) { + editor_debugger = p_editor_debugger; +} + +void EditorExpressionEvaluator::add_value(const Array &p_array) { + inspector->add_stack_variable(p_array, 0); + inspector->set_v_scroll(0); + inspector->set_h_scroll(0); +} + +void EditorExpressionEvaluator::_evaluate() { + const String &expression = expression_input->get_text(); + if (expression.is_empty()) { + return; + } + + if (!editor_debugger->is_session_active()) { + return; + } + + Array expr_data; + expr_data.push_back(expression); + expr_data.push_back(editor_debugger->get_stack_script_frame()); + editor_debugger->send_message("evaluate", expr_data); + + expression_input->clear(); +} + +void EditorExpressionEvaluator::_clear() { + inspector->clear_stack_variables(); +} + +void EditorExpressionEvaluator::_remote_object_selected(ObjectID p_id) { + editor_debugger->emit_signal(SNAME("remote_object_requested"), p_id); +} + +void EditorExpressionEvaluator::_on_expression_input_changed(const String &p_expression) { + evaluate_btn->set_disabled(p_expression.is_empty()); +} + +void EditorExpressionEvaluator::_on_debugger_breaked(bool p_breaked, bool p_can_debug) { + expression_input->set_editable(p_breaked); + evaluate_btn->set_disabled(!p_breaked); +} + +void EditorExpressionEvaluator::_on_debugger_clear_execution(Ref