diff --git a/core/object/object.cpp b/core/object/object.cpp index 42c54cb0791..b7621a95fef 100644 --- a/core/object/object.cpp +++ b/core/object/object.cpp @@ -42,6 +42,10 @@ #include "core/string/translation_server.h" #include "core/variant/typed_array.h" +// Static member initialization for signal emission callback +std::atomic Object::signal_emission_callback = nullptr; +std::atomic Object::signal_emission_callback_enabled = false; + #ifdef DEBUG_ENABLED struct _ObjectDebugLock { @@ -1223,6 +1227,12 @@ Error Object::emit_signalp(const StringName &p_name, const Variant **p_args, int uint32_t *slot_flags = slot_flags_stack; uint32_t slot_count = 0; + // STEP 1: Signal emission hook - Declare variables for callback capture + // This allows editor tools like the Signal Viewer to track signal emissions globally + // We capture the callback while holding the lock, but call it after releasing to prevent deadlocks + Object::SignalEmissionCallback emission_callback = nullptr; + bool should_call_callback = false; + { OBJ_SIGNAL_LOCK @@ -1237,6 +1247,12 @@ Error Object::emit_signalp(const StringName &p_name, const Variant **p_args, int return ERR_UNAVAILABLE; } + // Capture callback state under lock (thread-safe atomic load) + should_call_callback = signal_emission_callback_enabled.load(std::memory_order_acquire) && signal_emission_callback.load(std::memory_order_acquire); + if (should_call_callback) { + emission_callback = signal_emission_callback.load(std::memory_order_acquire); + } + if (s->slot_map.size() > MAX_SLOTS_ON_STACK) { slot_callables = (Callable *)memalloc(sizeof(Callable) * s->slot_map.size()); slot_flags = (uint32_t *)memalloc(sizeof(uint32_t) * s->slot_map.size()); @@ -1267,6 +1283,12 @@ Error Object::emit_signalp(const StringName &p_name, const Variant **p_args, int } } + // STEP 1: Call the emission callback outside the lock to prevent deadlocks + // if the callback re-enters signal code paths + if (should_call_callback && emission_callback) { + emission_callback(this, p_name, p_args, p_argcount); + } + OBJ_DEBUG_LOCK // If this is a ref-counted object, prevent it from being destroyed during signal diff --git a/core/object/object.h b/core/object/object.h index e64aa13180a..3f5e7582573 100644 --- a/core/object/object.h +++ b/core/object/object.h @@ -791,6 +791,29 @@ class Object { public: static constexpr bool _class_is_enabled = true; + // Signal tracking callback for editor tools (e.g., Signal Viewer) + // This callback is invoked whenever a signal is emitted, allowing tools to track signal activity + // The callback is only active when explicitly registered to minimize performance impact + typedef void (*SignalEmissionCallback)(Object *p_emitter, const StringName &p_signal, const Variant **p_args, int p_argcount); + +private: + static std::atomic signal_emission_callback; + static std::atomic signal_emission_callback_enabled; + +public: + static void set_signal_emission_callback(SignalEmissionCallback p_callback) { + signal_emission_callback.store(p_callback, std::memory_order_release); + signal_emission_callback_enabled.store(p_callback != nullptr, std::memory_order_release); + } + + static SignalEmissionCallback get_signal_emission_callback() { + return signal_emission_callback.load(std::memory_order_acquire); + } + + static bool is_signal_emission_callback_enabled() { + return signal_emission_callback_enabled.load(std::memory_order_acquire); + } + void notify_property_list_changed(); static void *get_class_ptr_static() { diff --git a/doc/classes/SignalViewerRuntime.xml b/doc/classes/SignalViewerRuntime.xml new file mode 100644 index 00000000000..3df1c92cf40 --- /dev/null +++ b/doc/classes/SignalViewerRuntime.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/editor/debugger/debug_adapter/debug_adapter_parser.cpp b/editor/debugger/debug_adapter/debug_adapter_parser.cpp index f98203aa9c2..7b8134aa329 100644 --- a/editor/debugger/debug_adapter/debug_adapter_parser.cpp +++ b/editor/debugger/debug_adapter/debug_adapter_parser.cpp @@ -514,7 +514,7 @@ Dictionary DebugAdapterParser::req_godot_put_msg(const Dictionary &p_params) con String msg = args["message"]; Array data = args["data"]; - EditorDebuggerNode::get_singleton()->get_default_debugger()->_put_msg(msg, data); + EditorDebuggerNode::get_singleton()->get_default_debugger()->put_msg(msg, data); return prepare_success_response(p_params); } diff --git a/editor/debugger/script_editor_debugger.cpp b/editor/debugger/script_editor_debugger.cpp index 552cf313d41..07e482dd6de 100644 --- a/editor/debugger/script_editor_debugger.cpp +++ b/editor/debugger/script_editor_debugger.cpp @@ -43,6 +43,7 @@ #include "editor/debugger/editor_visual_profiler.h" #include "editor/docks/filesystem_dock.h" #include "editor/docks/inspector_dock.h" +#include "editor/docks/signalize_dock.h" #include "editor/editor_log.h" #include "editor/editor_node.h" #include "editor/editor_string_names.h" @@ -72,7 +73,7 @@ using CameraOverride = EditorDebuggerNode::CameraOverride; -void ScriptEditorDebugger::_put_msg(const String &p_message, const Array &p_data, uint64_t p_thread_id) { +void ScriptEditorDebugger::put_msg(const String &p_message, const Array &p_data, uint64_t p_thread_id) { ERR_FAIL_COND(p_thread_id == Thread::UNASSIGNED_ID); if (is_session_active()) { Array msg = { p_message, p_thread_id, p_data }; @@ -98,7 +99,7 @@ void ScriptEditorDebugger::debug_skip_breakpoints() { } Array msg = { skip_breakpoints_value }; - _put_msg("set_skip_breakpoints", msg, debugging_thread_id != Thread::UNASSIGNED_ID ? debugging_thread_id : Thread::MAIN_ID); + put_msg("set_skip_breakpoints", msg, debugging_thread_id != Thread::UNASSIGNED_ID ? debugging_thread_id : Thread::MAIN_ID); } void ScriptEditorDebugger::debug_ignore_error_breaks() { @@ -110,27 +111,27 @@ void ScriptEditorDebugger::debug_ignore_error_breaks() { } Array msg = { ignore_error_breaks_value }; - _put_msg("set_ignore_error_breaks", msg); + put_msg("set_ignore_error_breaks", msg); } void ScriptEditorDebugger::debug_next() { ERR_FAIL_COND(!is_breaked()); - _put_msg("next", Array(), debugging_thread_id); + put_msg("next", Array(), debugging_thread_id); _clear_execution(); } void ScriptEditorDebugger::debug_step() { ERR_FAIL_COND(!is_breaked()); - _put_msg("step", Array(), debugging_thread_id); + put_msg("step", Array(), debugging_thread_id); _clear_execution(); } void ScriptEditorDebugger::debug_break() { ERR_FAIL_COND(is_breaked()); - _put_msg("break", Array()); + put_msg("break", Array()); } void ScriptEditorDebugger::debug_continue() { @@ -142,8 +143,8 @@ void ScriptEditorDebugger::debug_continue() { } _clear_execution(); - _put_msg("continue", Array(), debugging_thread_id); - _put_msg("servers:foreground", Array()); + put_msg("continue", Array(), debugging_thread_id); + put_msg("servers:foreground", Array()); } void ScriptEditorDebugger::update_tabs() { @@ -168,7 +169,7 @@ void ScriptEditorDebugger::clear_style() { void ScriptEditorDebugger::save_node(ObjectID p_id, const String &p_file) { Array msg = { p_id, p_file }; - _put_msg("scene:save_node", msg); + put_msg("scene:save_node", msg); } void ScriptEditorDebugger::_file_selected(const String &p_file) { @@ -252,7 +253,7 @@ void ScriptEditorDebugger::_file_selected(const String &p_file) { } void ScriptEditorDebugger::request_remote_tree() { - _put_msg("scene:request_scene_tree", Array()); + put_msg("scene:request_scene_tree", Array()); } const SceneDebuggerTree *ScriptEditorDebugger::get_remote_tree() { @@ -261,29 +262,29 @@ const SceneDebuggerTree *ScriptEditorDebugger::get_remote_tree() { void ScriptEditorDebugger::request_remote_evaluate(const String &p_expression, int p_stack_frame) { Array msg = { p_expression, p_stack_frame }; - _put_msg("evaluate", msg); + put_msg("evaluate", msg); } void ScriptEditorDebugger::update_remote_object(ObjectID p_obj_id, const String &p_prop, const Variant &p_value, const String &p_field) { Array msg = { p_obj_id, p_prop, p_value }; if (p_field.is_empty()) { - _put_msg("scene:set_object_property", msg); + put_msg("scene:set_object_property", msg); } else { msg.push_back(p_field); - _put_msg("scene:set_object_property_field", msg); + put_msg("scene:set_object_property_field", msg); } } void ScriptEditorDebugger::request_remote_objects(const TypedArray &p_obj_ids, bool p_update_selection) { ERR_FAIL_COND(p_obj_ids.is_empty()); Array msg = { p_obj_ids.duplicate(), p_update_selection }; - _put_msg("scene:inspect_objects", msg); + put_msg("scene:inspect_objects", msg); } void ScriptEditorDebugger::clear_inspector(bool p_send_msg) { inspector->clear_remote_inspector(); if (p_send_msg) { - _put_msg("scene:clear_selection", Array()); + put_msg("scene:clear_selection", Array()); } } @@ -304,7 +305,7 @@ void ScriptEditorDebugger::_remote_object_property_updated(ObjectID p_id, const } void ScriptEditorDebugger::_video_mem_request() { - _put_msg("servers:memory", Array()); + put_msg("servers:memory", Array()); } void ScriptEditorDebugger::_video_mem_export() { @@ -330,7 +331,7 @@ void ScriptEditorDebugger::_thread_debug_enter(uint64_t p_thread_id) { tabs->set_current_tab(0); } inspector->clear_cache(); // Take a chance to force remote objects update. - _put_msg("get_stack_dump", Array(), p_thread_id); + put_msg("get_stack_dump", Array(), p_thread_id); } void ScriptEditorDebugger::_select_thread(int p_index) { @@ -489,6 +490,52 @@ void ScriptEditorDebugger::_msg_servers_drawn(uint64_t p_thread_id, const Array can_request_idle_draw = true; } +void ScriptEditorDebugger::_msg_signal_viewer_signal_emitted(uint64_t p_thread_id, const Array &p_data) { + // Forward signal emission data to SignalizeDock + // Data format: [emitter_id, node_name, node_class, signal_name, count, connections_array] + + if (p_data.size() < 6) { + print_line("[ScriptEditorDebugger] WARNING: Invalid signal_viewer data, expected 6 elements"); + return; + } + + // Extract the data + ObjectID emitter_id = p_data[0]; + String node_name = p_data[1]; + String node_class = p_data[2]; + String signal_name = p_data[3]; + int count = p_data[4]; + Array signal_connections = p_data[5]; + + // Get SignalizeDock singleton and update it + SignalizeDock *signal_viewer = SignalizeDock::get_singleton(); + if (signal_viewer) { + signal_viewer->_on_runtime_signal_emitted(emitter_id, node_name, node_class, signal_name, count, signal_connections); + } else { + print_line("[ScriptEditorDebugger] WARNING: No SignalizeDock singleton"); + } +} + +void ScriptEditorDebugger::_msg_signal_viewer_node_signal_data(uint64_t p_thread_id, const Array &p_data) { + // Forward node signal data response to SignalizeDock + // Data format: [node_id, node_name, node_class, [signals_data]] + + if (p_data.is_empty()) { + print_line("[ScriptEditorDebugger] WARNING: Invalid node_signal_data, empty array"); + return; + } + + print_line(vformat("[ScriptEditorDebugger] Received node signal data with %d elements", p_data.size())); + + // Get SignalizeDock singleton and forward the data + SignalizeDock *signal_viewer = SignalizeDock::get_singleton(); + if (signal_viewer) { + signal_viewer->_on_node_signal_data_received(p_data); + } else { + print_line("[ScriptEditorDebugger] WARNING: No SignalizeDock singleton for node_signal_data"); + } +} + void ScriptEditorDebugger::_msg_stack_dump(uint64_t p_thread_id, const Array &p_data) { DebuggerMarshalls::ScriptStackDump stack; stack.deserialize(p_data); @@ -942,14 +989,28 @@ void ScriptEditorDebugger::_msg_embed_next_frame(uint64_t p_thread_id, const Arr void ScriptEditorDebugger::_parse_message(const String &p_msg, uint64_t p_thread_id, const Array &p_data) { emit_signal(SNAME("debug_data"), p_msg, p_data); + // DEBUG: Log signal_viewer messages + if (p_msg.contains("signal_viewer")) { + print_line(vformat("[ScriptEditorDebugger] Received message: '%s'", p_msg)); + } + ParseMessageFunc *fn_ptr = parse_message_handlers.getptr(p_msg); if (fn_ptr) { + if (p_msg.contains("signal_viewer")) { + print_line(vformat("[ScriptEditorDebugger] Found handler for: '%s'", p_msg)); + } (this->**fn_ptr)(p_thread_id, p_data); } else { + if (p_msg.contains("signal_viewer")) { + print_line(vformat("[ScriptEditorDebugger] No handler found for: '%s', trying plugins_capture", p_msg)); + } int colon_index = p_msg.find_char(':'); ERR_FAIL_COND_MSG(colon_index < 1, "Invalid message received"); bool parsed = EditorDebuggerNode::get_singleton()->plugins_capture(this, p_msg, p_data); + if (p_msg.contains("signal_viewer")) { + print_line(vformat("[ScriptEditorDebugger] plugins_capture returned: %s", parsed ? "TRUE" : "FALSE")); + } if (!parsed) { WARN_PRINT("Unknown message: " + p_msg); } @@ -970,6 +1031,8 @@ void ScriptEditorDebugger::_init_parse_message_handlers() { #endif // DISABLE_DEPRECATED parse_message_handlers["servers:memory_usage"] = &ScriptEditorDebugger::_msg_servers_memory_usage; parse_message_handlers["servers:drawn"] = &ScriptEditorDebugger::_msg_servers_drawn; + parse_message_handlers["signal_viewer:signal_emitted"] = &ScriptEditorDebugger::_msg_signal_viewer_signal_emitted; + parse_message_handlers["signal_viewer:node_signal_data"] = &ScriptEditorDebugger::_msg_signal_viewer_node_signal_data; parse_message_handlers["stack_dump"] = &ScriptEditorDebugger::_msg_stack_dump; parse_message_handlers["stack_frame_vars"] = &ScriptEditorDebugger::_msg_stack_frame_vars; parse_message_handlers["stack_frame_var"] = &ScriptEditorDebugger::_msg_stack_frame_var; @@ -1110,7 +1173,7 @@ void ScriptEditorDebugger::_notification(int p_what) { transform.columns[2] = -offset * zoom; Array msg = { transform }; - _put_msg("scene:transform_camera_2d", msg); + put_msg("scene:transform_camera_2d", msg); } // Node3D Editor @@ -1128,12 +1191,12 @@ void ScriptEditorDebugger::_notification(int p_what) { } msg.push_back(cam->get_near()); msg.push_back(cam->get_far()); - _put_msg("scene:transform_camera_3d", msg); + put_msg("scene:transform_camera_3d", msg); } } if (is_breaked() && can_request_idle_draw) { - _put_msg("servers:draw", Array()); + put_msg("servers:draw", Array()); can_request_idle_draw = false; } } @@ -1220,7 +1283,7 @@ void ScriptEditorDebugger::start(Ref p_peer) { _update_buttons_state(); Array quit_keys = DebuggerMarshalls::serialize_key_shortcut(ED_GET_SHORTCUT("editor/stop_running_project")); - _put_msg("scene:setup_scene", quit_keys); + put_msg("scene:setup_scene", quit_keys); if (EditorSettings::get_singleton()->get_project_metadata("debug_options", "autostart_profiler", false)) { profiler->set_profiling(true); @@ -1308,7 +1371,7 @@ void ScriptEditorDebugger::_profiler_activate(bool p_enable, int p_type) { Array msg_data = { p_enable }; switch (p_type) { case PROFILER_VISUAL: - _put_msg("profiler:visual", msg_data); + put_msg("profiler:visual", msg_data); break; case PROFILER_SCRIPTS_SERVERS: if (p_enable) { @@ -1320,7 +1383,7 @@ void ScriptEditorDebugger::_profiler_activate(bool p_enable, int p_type) { Array opts = { CLAMP(max_funcs, 16, 512), include_native }; msg_data.push_back(opts); } - _put_msg("profiler:servers", msg_data); + put_msg("profiler:servers", msg_data); break; default: ERR_FAIL_MSG("Invalid profiler type"); @@ -1360,7 +1423,7 @@ String ScriptEditorDebugger::get_var_value(const String &p_var) const { void ScriptEditorDebugger::_resources_reimported(const PackedStringArray &p_resources) { Array msg = { p_resources }; - _put_msg("scene:reload_cached_files", msg); + put_msg("scene:reload_cached_files", msg); } int ScriptEditorDebugger::_get_node_path_cache(const NodePath &p_path) { @@ -1373,7 +1436,7 @@ int ScriptEditorDebugger::_get_node_path_cache(const NodePath &p_path) { node_path_cache[p_path] = last_path_id; Array msg = { p_path, last_path_id }; - _put_msg("scene:live_node_path", msg); + put_msg("scene:live_node_path", msg); return last_path_id; } @@ -1389,7 +1452,7 @@ int ScriptEditorDebugger::_get_res_path_cache(const String &p_path) { res_path_cache[p_path] = last_path_id; Array msg = { p_path, last_path_id }; - _put_msg("scene:live_res_path", msg); + put_msg("scene:live_res_path", msg); return last_path_id; } @@ -1417,7 +1480,7 @@ void ScriptEditorDebugger::_method_changed(Object *p_base, const StringName &p_n //no pointers, sorry msg.push_back(*p_args[i]); } - _put_msg("scene:live_node_call", msg); + put_msg("scene:live_node_call", msg); return; } @@ -1433,7 +1496,7 @@ void ScriptEditorDebugger::_method_changed(Object *p_base, const StringName &p_n //no pointers, sorry msg.push_back(*p_args[i]); } - _put_msg("scene:live_res_call", msg); + put_msg("scene:live_res_call", msg); return; } @@ -1454,11 +1517,11 @@ void ScriptEditorDebugger::_property_changed(Object *p_base, const StringName &p Ref res = p_value; if (res.is_valid() && !res->get_path().is_empty()) { Array msg = { pathid, p_property, res->get_path() }; - _put_msg("scene:live_node_prop_res", msg); + put_msg("scene:live_node_prop_res", msg); } } else { Array msg = { pathid, p_property, p_value }; - _put_msg("scene:live_node_prop", msg); + put_msg("scene:live_node_prop", msg); } return; @@ -1474,11 +1537,11 @@ void ScriptEditorDebugger::_property_changed(Object *p_base, const StringName &p Ref res2 = p_value; if (res2.is_valid() && !res2->get_path().is_empty()) { Array msg = { pathid, p_property, res2->get_path() }; - _put_msg("scene:live_res_prop_res", msg); + put_msg("scene:live_res_prop_res", msg); } } else { Array msg = { pathid, p_property, p_value }; - _put_msg("scene:live_res_prop", msg); + put_msg("scene:live_res_prop", msg); } return; @@ -1524,7 +1587,7 @@ bool ScriptEditorDebugger::request_stack_dump(const int &p_frame) { ERR_FAIL_COND_V(!is_session_active() || p_frame < 0, false); Array msg = { p_frame }; - _put_msg("get_stack_frame_vars", msg, debugging_thread_id); + put_msg("get_stack_frame_vars", msg, debugging_thread_id); return true; } @@ -1573,56 +1636,56 @@ void ScriptEditorDebugger::update_live_edit_root() { } else { msg.push_back(""); } - _put_msg("scene:live_set_root", msg); + put_msg("scene:live_set_root", msg); live_edit_root->set_text(String(np)); } void ScriptEditorDebugger::live_debug_create_node(const NodePath &p_parent, const String &p_type, const String &p_name) { if (live_debug) { Array msg = { p_parent, p_type, p_name }; - _put_msg("scene:live_create_node", msg); + put_msg("scene:live_create_node", msg); } } void ScriptEditorDebugger::live_debug_instantiate_node(const NodePath &p_parent, const String &p_path, const String &p_name) { if (live_debug) { Array msg = { p_parent, p_path, p_name }; - _put_msg("scene:live_instantiate_node", msg); + put_msg("scene:live_instantiate_node", msg); } } void ScriptEditorDebugger::live_debug_remove_node(const NodePath &p_at) { if (live_debug) { Array msg = { p_at }; - _put_msg("scene:live_remove_node", msg); + put_msg("scene:live_remove_node", msg); } } void ScriptEditorDebugger::live_debug_remove_and_keep_node(const NodePath &p_at, ObjectID p_keep_id) { if (live_debug) { Array msg = { p_at, p_keep_id }; - _put_msg("scene:live_remove_and_keep_node", msg); + put_msg("scene:live_remove_and_keep_node", msg); } } void ScriptEditorDebugger::live_debug_restore_node(ObjectID p_id, const NodePath &p_at, int p_at_pos) { if (live_debug) { Array msg = { p_id, p_at, p_at_pos }; - _put_msg("scene:live_restore_node", msg); + put_msg("scene:live_restore_node", msg); } } void ScriptEditorDebugger::live_debug_duplicate_node(const NodePath &p_at, const String &p_new_name) { if (live_debug) { Array msg = { p_at, p_new_name }; - _put_msg("scene:live_duplicate_node", msg); + put_msg("scene:live_duplicate_node", msg); } } void ScriptEditorDebugger::live_debug_reparent_node(const NodePath &p_at, const NodePath &p_new_place, const String &p_new_name, int p_at_pos) { if (live_debug) { Array msg = { p_at, p_new_place, p_new_name, p_at_pos }; - _put_msg("scene:live_reparent_node", msg); + put_msg("scene:live_reparent_node", msg); } } @@ -1632,7 +1695,7 @@ bool ScriptEditorDebugger::get_debug_mute_audio() const { void ScriptEditorDebugger::set_debug_mute_audio(bool p_mute) { Array msg = { p_mute }; - _put_msg("scene:debug_mute_audio", msg); + put_msg("scene:debug_mute_audio", msg); debug_mute_audio = p_mute; } @@ -1645,14 +1708,14 @@ void ScriptEditorDebugger::set_camera_override(CameraOverride p_override) { p_override != CameraOverride::OVERRIDE_NONE, p_override == CameraOverride::OVERRIDE_EDITORS }; - _put_msg("scene:override_cameras", msg); + put_msg("scene:override_cameras", msg); camera_override = p_override; } void ScriptEditorDebugger::set_breakpoint(const String &p_path, int p_line, bool p_enabled) { Array msg = { p_path, p_line, p_enabled }; - _put_msg("breakpoint", msg, debugging_thread_id != Thread::UNASSIGNED_ID ? debugging_thread_id : Thread::MAIN_ID); + put_msg("breakpoint", msg, debugging_thread_id != Thread::UNASSIGNED_ID ? debugging_thread_id : Thread::MAIN_ID); TreeItem *path_item = breakpoints_tree->search_item_text(p_path); if (path_item == nullptr) { @@ -1695,11 +1758,11 @@ void ScriptEditorDebugger::set_breakpoint(const String &p_path, int p_line, bool } void ScriptEditorDebugger::reload_all_scripts() { - _put_msg("reload_all_scripts", Array(), debugging_thread_id != Thread::UNASSIGNED_ID ? debugging_thread_id : Thread::MAIN_ID); + put_msg("reload_all_scripts", Array(), debugging_thread_id != Thread::UNASSIGNED_ID ? debugging_thread_id : Thread::MAIN_ID); } void ScriptEditorDebugger::reload_scripts(const Vector &p_script_paths) { - _put_msg("reload_scripts", Variant(p_script_paths).operator Array(), debugging_thread_id != Thread::UNASSIGNED_ID ? debugging_thread_id : Thread::MAIN_ID); + put_msg("reload_scripts", Variant(p_script_paths).operator Array(), debugging_thread_id != Thread::UNASSIGNED_ID ? debugging_thread_id : Thread::MAIN_ID); } bool ScriptEditorDebugger::is_skip_breakpoints() const { @@ -1981,12 +2044,12 @@ void ScriptEditorDebugger::switch_to_debugger(int p_debugger_tab_idx) { } void ScriptEditorDebugger::send_message(const String &p_message, const Array &p_args) { - _put_msg(p_message, p_args); + put_msg(p_message, p_args); } void ScriptEditorDebugger::toggle_profiler(const String &p_profiler, bool p_enable, const Array &p_data) { Array msg_data = { p_enable, p_data }; - _put_msg("profiler:" + p_profiler, msg_data); + put_msg("profiler:" + p_profiler, msg_data); } ScriptEditorDebugger::ScriptEditorDebugger() { diff --git a/editor/debugger/script_editor_debugger.h b/editor/debugger/script_editor_debugger.h index d0859d746c5..81c345a08f7 100644 --- a/editor/debugger/script_editor_debugger.h +++ b/editor/debugger/script_editor_debugger.h @@ -210,6 +210,8 @@ class ScriptEditorDebugger : public MarginContainer { #endif // DISABLE_DEPRECATED void _msg_servers_memory_usage(uint64_t p_thread_id, const Array &p_data); void _msg_servers_drawn(uint64_t p_thread_id, const Array &p_data); + void _msg_signal_viewer_signal_emitted(uint64_t p_thread_id, const Array &p_data); + void _msg_signal_viewer_node_signal_data(uint64_t p_thread_id, const Array &p_data); void _msg_stack_dump(uint64_t p_thread_id, const Array &p_data); void _msg_stack_frame_vars(uint64_t p_thread_id, const Array &p_data); void _msg_stack_frame_var(uint64_t p_thread_id, const Array &p_data); @@ -275,7 +277,6 @@ class ScriptEditorDebugger : public MarginContainer { void _item_menu_id_pressed(int p_option); void _tab_changed(int p_tab); - void _put_msg(const String &p_message, const Array &p_data, uint64_t p_thread_id = Thread::MAIN_ID); void _export_csv(); void _clear_execution(); @@ -305,8 +306,12 @@ class ScriptEditorDebugger : public MarginContainer { void clear_inspector(bool p_send_msg = true); + // Send message to game process - public for SignalizeDock integration + void put_msg(const String &p_message, const Array &p_data, uint64_t p_thread_id = Thread::MAIN_ID); + // Needed by _live_edit_set, buttons state. void set_editor_remote_tree(const Tree *p_tree) { editor_remote_tree = p_tree; } + const Tree *get_editor_remote_tree() const { return editor_remote_tree; } void request_remote_tree(); const SceneDebuggerTree *get_remote_tree(); diff --git a/editor/docks/signalize_dock.cpp b/editor/docks/signalize_dock.cpp new file mode 100644 index 00000000000..8ed894ab9dd --- /dev/null +++ b/editor/docks/signalize_dock.cpp @@ -0,0 +1,3154 @@ +/**************************************************************************/ +/* signalize_dock.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* REDOT ENGINE */ +/* https://redotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2024-present Redot Engine contributors */ +/* (see REDOT_AUTHORS.md) */ +/* 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 "signalize_dock.h" + +#include "core/debugger/engine_debugger.h" +#include "core/input/input_event.h" +#include "core/object/class_db.h" +#include "core/object/script_instance.h" +#include "core/object/script_language.h" +#include "core/templates/hash_set.h" +#include "core/variant/callable.h" +#include "editor/debugger/editor_debugger_node.h" +#include "editor/debugger/script_editor_debugger.h" +#include "editor/editor_interface.h" +#include "editor/editor_node.h" +#include "editor/gui/window_wrapper.h" +#include "editor/inspector/editor_inspector.h" +#include "editor/run/editor_run_bar.h" +#include "editor/script/script_editor_plugin.h" +#include "editor/settings/editor_settings.h" +#include "scene/debugger/scene_debugger.h" +#include "scene/gui/box_container.h" +#include "scene/gui/color_picker.h" +#include "scene/gui/control.h" +#include "scene/gui/label.h" +#include "scene/gui/option_button.h" +#include "scene/gui/spin_box.h" +#include "scene/gui/tree.h" +#include "scene/main/timer.h" +#include "scene/resources/style_box_flat.h" +#include + +// Static member initialization +SignalizeDock *SignalizeDock::singleton_instance = nullptr; +const String SignalizeDock::MESSAGE_SIGNAL_EMITTED = "signal_viewer:signal_emitted"; +const String SignalizeDock::MESSAGE_NODE_SIGNAL_DATA = "signal_viewer:node_signal_data"; + +SignalizeDock::SignalizeDock() { + // Set singleton instance for global callback access + singleton_instance = this; + set_name("Signalize"); + set_h_size_flags(SIZE_EXPAND_FILL); + set_v_size_flags(SIZE_EXPAND_FILL); + + // Create WindowWrapper (not added yet - only added when floating) + window_wrapper = memnew(WindowWrapper); + window_wrapper->set_margins_enabled(true); + window_wrapper->set_window_title(TTR("Signalize - Signal Viewer")); + + // Create a content container that holds all the UI + // This container can be reparented between SignalizeDock (docked) and WindowWrapper (floating) + content_container = memnew(VBoxContainer); + content_container->set_h_size_flags(SIZE_EXPAND_FILL); + content_container->set_v_size_flags(SIZE_EXPAND_FILL); + add_child(content_container); + + // Top bar with search, refresh, and floating button + HBoxContainer *top_bar = memnew(HBoxContainer); + content_container->add_child(top_bar); + + title_label = memnew(Label("Signalize")); + title_label->set_h_size_flags(SIZE_EXPAND_FILL); + top_bar->add_child(title_label); + + search_box = memnew(LineEdit); + search_box->set_placeholder("Filter nodes..."); + search_box->set_h_size_flags(SIZE_EXPAND_FILL); + search_box->connect("text_changed", callable_mp(this, &SignalizeDock::_on_search_changed)); + top_bar->add_child(search_box); + + refresh_button = memnew(Button); + refresh_button->set_text("Build Graph"); + refresh_button->set_tooltip_text("Rebuild the signal graph from the edited scene"); + refresh_button->connect("pressed", callable_mp(this, &SignalizeDock::_on_refresh_pressed)); + top_bar->add_child(refresh_button); + + // Per-node inspection: Add button to inspect selected remote node + Button *inspect_button = memnew(Button); + inspect_button->set_text("Inspect Selected Node"); + // Note: Button doesn't have set_tooltip in Godot 4.x, tooltip is set via theme or custom logic + inspect_button->connect("pressed", callable_mp(this, &SignalizeDock::_on_inspect_selected_button_pressed)); + top_bar->add_child(inspect_button); + + // Connection color picker + connection_color_button = memnew(ColorPickerButton); + + // Load saved color from editor settings + Ref editor_settings = EditorSettings::get_singleton(); + if (editor_settings.is_valid()) { + Variant saved_color = editor_settings->get("signalize/connection_color"); + if (saved_color.get_type() == Variant::COLOR) { + custom_connection_color = saved_color; + } + + // Load verbosity level from editor settings + Variant saved_verbosity = editor_settings->get("signalize/verbosity_level"); + if (saved_verbosity.get_type() == Variant::INT) { + verbosity_level = saved_verbosity; + } + } + + connection_color_button->set_pick_color(custom_connection_color); + connection_color_button->set_tooltip_text("Connection line color"); + connection_color_button->connect("color_changed", callable_mp(this, &SignalizeDock::_on_connection_color_changed)); + top_bar->add_child(connection_color_button); + + // Settings button + settings_button = memnew(Button); + settings_button->set_theme_type_variation(SceneStringName(FlatButton)); + settings_button->set_tooltip_text("Signalize Settings"); + settings_button->connect("pressed", callable_mp(this, &SignalizeDock::_on_settings_pressed)); + top_bar->add_child(settings_button); + + // Make Floating button (using icon like ScriptEditor) + make_floating_button = memnew(Button); + make_floating_button->set_theme_type_variation(SceneStringName(FlatButton)); + // Icon will be set in NOTIFICATION_THEME_CHANGED + make_floating_button->set_tooltip_text("Make Signalize floating (Alt+F)"); + make_floating_button->connect("pressed", callable_mp(this, &SignalizeDock::_on_make_floating_pressed)); + top_bar->add_child(make_floating_button); + + // Graph view + graph_edit = memnew(GraphEdit); + graph_edit->set_h_size_flags(SIZE_EXPAND_FILL); + graph_edit->set_v_size_flags(SIZE_EXPAND_FILL); + graph_edit->set_zoom(0.8); + graph_edit->set_show_zoom_label(true); + content_container->add_child(graph_edit); + + // Connect to play/stop signals to rebuild graph with runtime nodes + EditorRunBar *run_bar = EditorRunBar::get_singleton(); + if (run_bar) { + run_bar->connect("play_pressed", callable_mp(this, &SignalizeDock::_on_play_pressed)); + run_bar->connect("stop_pressed", callable_mp(this, &SignalizeDock::_on_stop_pressed)); + } else { + ERR_PRINT("[Signalize] Could not connect to EditorRunBar signals"); + } + + // Try to connect to debugger for message handling + EditorDebuggerNode *debugger_node = EditorDebuggerNode::get_singleton(); + if (debugger_node) { + // Debugger node available + } + + // Create timer to check when game is running and send start_tracking message + game_start_check_timer = memnew(Timer); + game_start_check_timer->set_wait_time(0.5); // Check every 0.5 seconds + game_start_check_timer->set_one_shot(false); + game_start_check_timer->connect("timeout", callable_mp(this, &SignalizeDock::_on_game_start_check_timer_timeout)); + game_start_check_timer->set_autostart(false); + content_container->add_child(game_start_check_timer); + game_start_check_timer->set_process_internal(true); // Make sure timer processes + + // NOTE: Global signal tracking DISABLED by default + // We'll only enable it when a node is being inspected during gameplay + tracking_enabled = false; + was_playing_last_frame = false; + remote_scene_root_id = ObjectID(); // Initialize to invalid ID + + // Register inspector plugin to detect when nodes are clicked in remote tree + inspector_plugin = memnew(SignalizeInspectorPlugin); + inspector_plugin->set_signal_viewer_dock(this); + EditorInspector::add_inspector_plugin(inspector_plugin); + + // Register message capture to receive signal emissions from game process + if (EngineDebugger::get_singleton()) { + EngineDebugger::register_message_capture("signal_viewer", EngineDebugger::Capture(nullptr, _capture_signal_viewer_messages)); + // Also register for "scene" messages to detect node selection in remote tree + EngineDebugger::register_message_capture("scene", EngineDebugger::Capture(nullptr, _capture_signal_viewer_messages)); + } + + // Build initial graph from edited scene (only works when game is not running) + // Only build if a scene is already open (might not be during editor initialization) + EditorNode *editor_node = EditorNode::get_singleton(); + if (editor_node && EditorNode::get_editor_data().get_edited_scene_count() > 0) { + _build_graph(); + } +} + +void SignalizeDock::_on_test_signal() { + // Test handler - can be removed in production +} + +void SignalizeDock::_on_refresh_pressed() { + EditorDebuggerNode *debugger_node = EditorDebuggerNode::get_singleton(); + ScriptEditorDebugger *debugger = nullptr; + + if (debugger_node) { + debugger = debugger_node->get_current_debugger(); + } + + // If game is running, don't allow full graph refresh + if (debugger && debugger->is_session_active()) { + ERR_PRINT("[Signalize] Cannot refresh full graph while game is running. Use 'Inspect Selected Node' instead."); + return; + } + + // Clear the existing graph and rebuild from the edited scene + _clear_inspection(); + _build_graph(); +} + +void SignalizeDock::_on_make_floating_pressed() { + if (!window_wrapper || !content_container) { + return; + } + + if (!is_floating) { + // Create a shortcut for toggling floating + Ref make_floating_shortcut; + make_floating_shortcut.instantiate(); + make_floating_shortcut->set_name("signalize/make_floating"); + + Ref key_event; + key_event.instantiate(); + key_event->set_keycode(Key::F); + key_event->set_alt_pressed(true); + + Array events; + events.push_back(key_event); + make_floating_shortcut->set_events(events); + + // Reparent content_container from SignalizeDock to WindowWrapper + remove_child(content_container); + window_wrapper->set_wrapped_control(content_container, make_floating_shortcut); + + // Add WindowWrapper to the scene tree as a child of SignalizeDock's parent + if (get_parent()) { + get_parent()->add_child(window_wrapper); + } + + // Enable floating mode + window_wrapper->set_window_enabled(true); + is_floating = true; + } else { + // Disable floating mode first + window_wrapper->set_window_enabled(false); + + // Reparent content_container back to SignalizeDock + window_wrapper->release_wrapped_control(); + add_child(content_container); + + // Remove WindowWrapper from scene tree + if (window_wrapper->get_parent()) { + window_wrapper->get_parent()->remove_child(window_wrapper); + } + + is_floating = false; + } +} + +void SignalizeDock::_on_search_changed(const String &p_text) { + // Show/hide nodes based on search + String search_lower = p_text.to_lower(); + + for (const KeyValue &kv : node_graph_nodes) { + GraphNode *gn = kv.value; + if (!gn) { + continue; + } + + String node_name = gn->get_title(); + bool is_visible = search_lower.is_empty() || node_name.to_lower().contains(search_lower); + gn->set_visible(is_visible); + } +} + +void SignalizeDock::_on_connection_color_changed(const Color &p_color) { + // Update the custom connection color + custom_connection_color = p_color; + + // Save to editor settings + Ref editor_settings = EditorSettings::get_singleton(); + if (editor_settings.is_valid()) { + editor_settings->set("signalize/connection_color", p_color); + editor_settings->save(); + } + + // Note: We don't rebuild the graph here because: + // 1. Rebuilding triggers the color_changed signal again, creating an infinite loop + // 2. The new color will be applied automatically when the graph is next rebuilt for any reason + // 3. Connection highlights during runtime will use the new color immediately + // The user can force a rebuild by clicking the "Build Graph" button if they want to see the change immediately +} + +void SignalizeDock::_on_settings_pressed() { + // Create settings dialog if it doesn't exist + if (!settings_dialog) { + settings_dialog = memnew(AcceptDialog); + settings_dialog->set_title("Signalize Settings"); + settings_dialog->set_min_size(Size2(300, 100)); + add_child(settings_dialog); + + VBoxContainer *vbox = memnew(VBoxContainer); + settings_dialog->add_child(vbox); + + // Verbosity setting + HBoxContainer *verbosity_row = memnew(HBoxContainer); + vbox->add_child(verbosity_row); + + Label *verbosity_label = memnew(Label("Verbosity Level:")); + verbosity_label->set_h_size_flags(SIZE_EXPAND_FILL); + verbosity_row->add_child(verbosity_label); + + OptionButton *verbosity_option = memnew(OptionButton); + verbosity_option->add_item("Silent (errors only)", 0); + verbosity_option->add_item("Quiet (graph stats)", 1); + verbosity_option->add_item("Normal (inspector updates)", 2); + verbosity_option->add_item("Verbose (full output)", 3); + verbosity_option->select(verbosity_level); + verbosity_option->connect("item_selected", callable_mp(this, &SignalizeDock::_on_verbosity_changed)); + verbosity_row->add_child(verbosity_option); + + // Pulse duration setting + HBoxContainer *duration_row = memnew(HBoxContainer); + vbox->add_child(duration_row); + + Label *duration_label = memnew(Label("Connection Pulse Duration (seconds):")); + duration_label->set_h_size_flags(SIZE_EXPAND_FILL); + duration_row->add_child(duration_label); + + SpinBox *duration_spin = memnew(SpinBox); + duration_spin->set_min(0.1); + duration_spin->set_max(10.0); + duration_spin->set_step(0.1); + duration_spin->set_value(connection_pulse_duration); + duration_spin->connect("value_changed", callable_mp(this, &SignalizeDock::_on_pulse_duration_changed)); + duration_row->add_child(duration_spin); + } + + settings_dialog->popup_centered(); +} + +void SignalizeDock::_on_pulse_duration_changed(double p_value) { + connection_pulse_duration = p_value; +} + +void SignalizeDock::_on_verbosity_changed(int p_level) { + verbosity_level = p_level; + Ref editor_settings = EditorSettings::get_singleton(); + if (editor_settings.is_valid()) { + editor_settings->set("signalize/verbosity_level", verbosity_level); + editor_settings->save(); + } +} + +void SignalizeDock::_on_open_function_button_pressed(ObjectID p_node_id, const String &p_method_name) { + // Get the node + Object *obj = ObjectDB::get_instance(p_node_id); + if (!obj) { + ERR_PRINT(vformat("[Signalize] Cannot open function: node not found (ID: %s)", String::num_uint64((uint64_t)p_node_id))); + return; + } + + Node *node = Object::cast_to(obj); + if (!node) { + ERR_PRINT(vformat("[Signalize] Cannot open function: object is not a Node")); + return; + } + + // Get the script attached to this node + Ref