diff --git a/src/editor/graph/graph_edit.cpp b/src/editor/graph/graph_edit.cpp index 3bc848ec..f8e0f8ac 100644 --- a/src/editor/graph/graph_edit.cpp +++ b/src/editor/graph/graph_edit.cpp @@ -239,6 +239,7 @@ void OrchestratorGraphEdit::_notification(int p_what) _action_menu->connect("action_selected", callable_mp(this, &OrchestratorGraphEdit::_on_action_menu_action_selected)); // Wire up our signals + connect("child_entered_tree", callable_mp(this, &OrchestratorGraphEdit::_resort_child_nodes_on_add)); connect("connection_from_empty", callable_mp(this, &OrchestratorGraphEdit::_on_attempt_connection_from_empty)); connect("connection_to_empty", callable_mp(this, &OrchestratorGraphEdit::_on_attempt_connection_to_empty)); connect("connection_request", callable_mp(this, &OrchestratorGraphEdit::_on_connection)); @@ -284,8 +285,6 @@ void OrchestratorGraphEdit::_notification(int p_what) void OrchestratorGraphEdit::_bind_methods() { - ClassDB::bind_method(D_METHOD("_synchronize_child_order"), &OrchestratorGraphEdit::_synchronize_child_order); - ADD_SIGNAL(MethodInfo("nodes_changed")); ADD_SIGNAL(MethodInfo("focus_requested", PropertyInfo(Variant::OBJECT, "target"))); ADD_SIGNAL(MethodInfo("collapse_selected_to_function")); @@ -530,6 +529,34 @@ void OrchestratorGraphEdit::_move_selected(const Vector2& p_delta) } } +void OrchestratorGraphEdit::_resort_child_nodes_on_add(Node* p_node) +{ + if (_is_comment_node(p_node)) + { + const int position = _get_connection_layer_index(); + + // Comment nodes should always be before the "_connection_layer" + // This needs to be deferred, don't change. + call_deferred("move_child", p_node, position); + } +} + +int OrchestratorGraphEdit::_get_connection_layer_index() const +{ + int index = 0; // generally this is the first child; however, comments will causes resorts + for (; index < get_child_count(); index++) + { + if (get_child(index)->get_name().match("_connection_layer")) + break; + } + return index; +} + +bool OrchestratorGraphEdit::_is_comment_node(Node* p_node) const +{ + return Object::cast_to(p_node); +} + void OrchestratorGraphEdit::_gui_input(const Ref& p_event) { // In Godot 4.2, the UI delete events only apply to GraphNode and not GraphElement objects @@ -557,7 +584,7 @@ void OrchestratorGraphEdit::_gui_input(const Ref& p_event) // This is to avoid triggering the display text or our internal hover_connection logic. Ref me = p_event; - if (me.is_valid() && !_is_position_within_node_rect(me->get_position())) + if (me.is_valid() && !_is_position_valid_for_knot(me->get_position())) { Ref mm = p_event; if (mm.is_valid()) @@ -585,6 +612,71 @@ void OrchestratorGraphEdit::_gui_input(const Ref& p_event) } } + Ref mb = p_event; + if (mb.is_valid()) + { + if (mb->get_button_index() == MOUSE_BUTTON_LEFT && mb->is_pressed()) + { + // This checks whether the LMB click should trigger box-selection + // + // While GraphEdit manages this, this information isn't directly exposed as signals, and our + // implementation needs this detail to know if we should ignore selecting specific custom + // graph elements, like GraphEdit does for GraphFrame in Godot 4.3. + GraphElement* element = nullptr; + for (int i = get_child_count() - 1; i >= 0; i--) + { + // Only interested in graph elements + GraphElement* selected = Object::cast_to(get_child(i)); + if (!selected) + continue; + + const Rect2 rect2(Point2(), selected->get_size()); + if (rect2.has_point((mb->get_position() - selected->get_position()) / get_zoom())) + { + OrchestratorGraphNodeComment* comment = Object::cast_to(selected); + if (comment && comment->_has_point((mb->get_position() - selected->get_position()) / get_zoom())) + { + element = selected; + break; + } + } + } + + if (!element) + { + _box_selection = true; + _box_selection_from = mb->get_position(); + } + } + + if (mb->get_button_index() == MOUSE_BUTTON_LEFT && !mb->is_pressed() && _box_selection) + _box_selection = false; + } + + // Our implementation needs to detect box selection and its rect to know whether the selection + // fully encloses our comment node implementations, similar to GraphFrame in Godot 4.3 + Ref mm = p_event; + if (mm.is_valid() && _box_selection) + { + const Vector2 selection_to = mm->get_position(); + const Rect2 select_rect = Rect2(_box_selection_from.min(selection_to), (_box_selection_from - selection_to).abs()); + + for (int i = get_child_count() - 1; i >= 0; i--) + { + GraphElement* element = Object::cast_to(get_child(i)); + if (!element) + continue; + + const bool is_comment = _is_comment_node(element); + const Rect2 r = element->get_rect(); + const bool should_be_selected = is_comment ? select_rect.encloses(r) : select_rect.intersects(r); + + // This must be deferred, don't change + if (is_comment && !should_be_selected) + element->call_deferred("set_selected", false); + } + } + const Ref key = p_event; if (key.is_valid() && key->is_pressed()) { @@ -851,11 +943,16 @@ void OrchestratorGraphEdit::_notify(const String& p_text, const String& p_title) dialog->popup_centered(); } -bool OrchestratorGraphEdit::_is_position_within_node_rect(const Vector2& p_position) const +bool OrchestratorGraphEdit::_is_position_valid_for_knot(const Vector2& p_position) const { for (int i = 0; i < get_child_count(); ++i) { GraphNode* child = Object::cast_to(get_child(i)); + + // Skip/ignore any comment nodes from knot logic validity + if (_is_comment_node(child)) + continue; + if (child && child->get_rect().has_point(p_position)) return true; } @@ -1164,8 +1261,6 @@ void OrchestratorGraphEdit::_synchronize_graph_with_script(bool p_apply_position _synchronize_graph_connections_with_script(); - call_deferred("_synchronize_child_order"); - if (p_apply_position) { // These must be deferred, don't change. @@ -1262,19 +1357,6 @@ void OrchestratorGraphEdit::_synchronize_graph_node(Ref p_node) } } -void OrchestratorGraphEdit::_synchronize_child_order() -{ - // Always place comment nodes above the nodes that are contained within their rects. - for_each_graph_node([&](OrchestratorGraphNode* node) { - if (OrchestratorGraphNodeComment* comment = Object::cast_to(node)) - { - // Raises the child - move_child(comment, -1); - comment->call_deferred("raise_request_node_reorder"); - } - }); -} - void OrchestratorGraphEdit::_complete_spawn(const Ref& p_spawned, const Callable& p_callback) { if (p_spawned.is_valid()) diff --git a/src/editor/graph/graph_edit.h b/src/editor/graph/graph_edit.h index 22ff97db..71ff34a6 100644 --- a/src/editor/graph/graph_edit.h +++ b/src/editor/graph/graph_edit.h @@ -134,6 +134,8 @@ class OrchestratorGraphEdit : public GraphEdit HashMap>> _knots; //! Knots for each graph connection GDExtensionGodotVersion _version; //! Godot version bool _is_43p{ false }; //! Is Godot 4.3+ + bool _box_selection{ false }; //! Is graph doing box selection? + Vector2 _box_selection_from; //! Mouse position box selection started from OrchestratorScriptAutowireSelections* _autowire{ nullptr }; OrchestratorGraphEdit() = default; @@ -145,6 +147,19 @@ class OrchestratorGraphEdit : public GraphEdit /// @param p_delta the delta to move selected nodes by void _move_selected(const Vector2& p_delta); + /// Sorts child nodes after a node is added as a child. + /// @param p_node the node that was added + void _resort_child_nodes_on_add(Node* p_node); + + /// Gets the child index for the GraphEdit's _connection_layer control. + /// @return the child index of the connection layer control + int _get_connection_layer_index() const; + + /// Checks whether the specified node is a comment node + /// @param p_node the node to check + /// @return true if it's a comment node, false otherwise + bool _is_comment_node(Node* p_node) const; + public: // The OrchestratorGraphEdit maintains a static clipboard so that data can be shared across different graph // instances easily in the tab view, and so these methods are called by the MainView during the @@ -266,10 +281,10 @@ class OrchestratorGraphEdit : public GraphEdit /// @param p_title the notification window title text void _notify(const String& p_text, const String& p_title); - /// Checks whether the specified position is within any node rect. + /// Checks whether the specified position is valid for knot operations /// @param p_position the position to check - /// @return true if the position is within any node rect, false otherwise - bool _is_position_within_node_rect(const Vector2& p_position) const; + /// @return true if the position is valid for knot operations, false otherwise + bool _is_position_valid_for_knot(const Vector2& p_position) const; /// Caches the graph knots for use. /// Copies the knot data from the OScriptGraph to this GraphEdit instance. @@ -346,9 +361,6 @@ class OrchestratorGraphEdit : public GraphEdit /// @param p_node the node to update. void _synchronize_graph_node(Ref p_node); - /// Synchronizes the child order - void _synchronize_child_order(); - /// Perform any post-steps after spawning a node /// @param p_spawned the spawned node /// @param p_callback a callback that is called after spawning the node diff --git a/src/editor/graph/graph_node.cpp b/src/editor/graph/graph_node.cpp index 4d5670e9..9510d58f 100644 --- a/src/editor/graph/graph_node.cpp +++ b/src/editor/graph/graph_node.cpp @@ -97,28 +97,28 @@ void OrchestratorGraphNode::_notification(int p_what) void OrchestratorGraphNode::_gui_input(const Ref& p_event) { - Ref button = p_event; - if (button.is_null() || !button->is_pressed()) - return; - - if (button->is_double_click() && button->get_button_index() == MOUSE_BUTTON_LEFT) + const Ref button = p_event; + if (button.is_valid() && button->is_pressed()) { - if (_node->can_jump_to_definition()) + if (button->is_double_click() && button->get_button_index() == MOUSE_BUTTON_LEFT) { - if (Object* target = _node->get_jump_target_for_double_click()) + if (_node->can_jump_to_definition()) { - _graph->request_focus(target); - accept_event(); + if (Object* target = _node->get_jump_target_for_double_click()) + { + _graph->request_focus(target); + accept_event(); + } } } - return; - } - else if (button->get_button_index() == MOUSE_BUTTON_RIGHT) - { - // Show menu - _show_context_menu(button->get_position()); - accept_event(); + else if (button->get_button_index() == MOUSE_BUTTON_RIGHT) + { + // Show menu + _show_context_menu(button->get_position()); + accept_event(); + } } + return GraphNode::_gui_input(p_event); } OrchestratorGraphEdit* OrchestratorGraphNode::get_graph() diff --git a/src/editor/graph/nodes/graph_node_comment.cpp b/src/editor/graph/nodes/graph_node_comment.cpp index f36f184e..64404138 100644 --- a/src/editor/graph/nodes/graph_node_comment.cpp +++ b/src/editor/graph/nodes/graph_node_comment.cpp @@ -26,6 +26,9 @@ OrchestratorGraphNodeComment::OrchestratorGraphNodeComment(OrchestratorGraphEdit : OrchestratorGraphNode(p_graph, p_node) , _comment_node(p_node) { + // Since _has_point is const, we need to cache this + _title_hbox = get_titlebar_hbox(); + MarginContainer* container = memnew(MarginContainer); container->add_theme_constant_override("margin_top", 4); container->add_theme_constant_override("margin_bottom", 4); @@ -46,7 +49,6 @@ OrchestratorGraphNodeComment::OrchestratorGraphNodeComment(OrchestratorGraphEdit void OrchestratorGraphNodeComment::_bind_methods() { - ClassDB::bind_method(D_METHOD("raise_request_node_reorder"), &OrchestratorGraphNodeComment::raise_request_node_reorder); } void OrchestratorGraphNodeComment::_update_pins() @@ -92,31 +94,68 @@ void OrchestratorGraphNodeComment::_notification(int p_what) connect("raise_request", callable_mp(this, &OrchestratorGraphNodeComment::_on_raise_request)); } +bool OrchestratorGraphNodeComment::_has_point(const Vector2& p_point) const +{ + Ref sb_panel = get_theme_stylebox("panel"); + Ref sb_titlebar = get_theme_stylebox("titlebar"); + Ref resizer = get_theme_icon("resizer"); + + if (Rect2(get_size() - resizer->get_size(), resizer->get_size()).has_point(p_point)) + return true; + + // Grab titlebar + int titlebar_height = _title_hbox->get_size().height + sb_titlebar->get_minimum_size().height; + if (Rect2(0, 0, get_size().width, titlebar_height).has_point(p_point)) + return true; + + // Allow grabbing on all sides of comment + Rect2 rect = Rect2(0, 0, get_size().width, get_size().height); + Rect2 no_drag_rect = rect.grow(-16); + + if (rect.has_point(p_point) && !no_drag_rect.has_point(p_point)) + return true; + + return false; +} + void OrchestratorGraphNodeComment::_gui_input(const Ref& p_event) { - Ref mb = p_event; - if (mb.is_valid()) + Ref mb = p_event; + if (mb.is_valid()) + { + if (mb->is_double_click() && mb->get_button_index() == MOUSE_BUTTON_LEFT) + { + if (is_group_selected()) + deselect_group(); + else + select_group(); + + accept_event(); + return; + } + } + return OrchestratorGraphNode::_gui_input(p_event); +} + +void OrchestratorGraphNodeComment::_on_raise_request() +{ + // When comment nodes are raised, their order must always be behind the connection layer. + // This guarantees that connection wires render properly. + if (OrchestratorGraphEdit* graph_edit = Object::cast_to(get_parent())) { - if (mb->is_double_click() && mb->get_button_index() == MOUSE_BUTTON_LEFT) + int position = 0; + for (int index = 0; index < graph_edit->get_child_count(); index++) { - if (is_group_selected()) - deselect_group(); - else - select_group(); + Node* child = graph_edit->get_child(index); - accept_event(); - return; + OrchestratorGraphNodeComment* comment = Object::cast_to(child); + if (comment && comment != this) + graph_edit->call_deferred("move_child", comment, position++); } - } - return OrchestratorGraphNode::_gui_input(p_event); -} -void OrchestratorGraphNodeComment::_on_raise_request() -{ - // This call must be deferred because the Godot GraphNode implementation raises this node - // after this method has been called, so we want to guarantee that we reorder the nodes - // of the scene after this node has been properly raised. - call_deferred("raise_request_node_reorder"); + graph_edit->call_deferred("move_child", this, position); + graph_edit->call_deferred("move_child", graph_edit->find_child("_connection_layer", false, false), position + 1); + } } bool OrchestratorGraphNodeComment::is_group_selected() @@ -142,13 +181,4 @@ void OrchestratorGraphNodeComment::deselect_group() List intersections = get_elements_within_global_rect(); for (GraphElement* child : intersections) child->set_selected(false); -} - -void OrchestratorGraphNodeComment::raise_request_node_reorder() -{ - // This guarantees that any node that intersects with a comment node will be repositioned - // in the scene after the comment, so that the rendering order appears correct. - List intersections = get_elements_within_global_rect(); - for (GraphElement* node : intersections) - get_parent()->move_child(node, -1); } \ No newline at end of file diff --git a/src/editor/graph/nodes/graph_node_comment.h b/src/editor/graph/nodes/graph_node_comment.h index 4404db2c..1617e88c 100644 --- a/src/editor/graph/nodes/graph_node_comment.h +++ b/src/editor/graph/nodes/graph_node_comment.h @@ -33,15 +33,12 @@ class OrchestratorGraphNodeComment : public OrchestratorGraphNode protected: Label* _label{ nullptr }; + HBoxContainer* _title_hbox{ nullptr }; Ref _comment_node; bool _selected{ false }; OrchestratorGraphNodeComment() = default; - /// Reorders graph nodes that intersect the comment node, making sure that any - /// other nodes that intersect are positioned after this comment node. - void raise_request_node_reorder(); - /// Called when the comment node is raised, brought to the front. void _on_raise_request(); @@ -65,6 +62,7 @@ class OrchestratorGraphNodeComment : public OrchestratorGraphNode //~ End Object Interface //~ Begin Control Interface + bool _has_point(const Vector2& p_point) const override; void _gui_input(const Ref& p_event) override; //~ End Control Interface };