Skip to content

Commit

Permalink
GH-737 Allow knot node operations within comment nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
Naros committed Aug 25, 2024
1 parent 11b75f7 commit 1253bc2
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 73 deletions.
120 changes: 101 additions & 19 deletions src/editor/graph/graph_edit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -526,6 +525,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<OrchestratorGraphNodeComment>(p_node);
}

void OrchestratorGraphEdit::_gui_input(const Ref<InputEvent>& p_event)
{
// In Godot 4.2, the UI delete events only apply to GraphNode and not GraphElement objects
Expand Down Expand Up @@ -553,7 +580,7 @@ void OrchestratorGraphEdit::_gui_input(const Ref<InputEvent>& p_event)

// This is to avoid triggering the display text or our internal hover_connection logic.
Ref<InputEventMouse> 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<InputEventMouseMotion> mm = p_event;
if (mm.is_valid())
Expand Down Expand Up @@ -581,6 +608,71 @@ void OrchestratorGraphEdit::_gui_input(const Ref<InputEvent>& p_event)
}
}

Ref<InputEventMouseButton> 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<GraphElement>(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<OrchestratorGraphNodeComment>(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<InputEventMouseMotion> 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<GraphElement>(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<InputEventKey> key = p_event;
if (key.is_valid() && key->is_pressed())
{
Expand Down Expand Up @@ -847,11 +939,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<GraphNode>(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;
}
Expand Down Expand Up @@ -1160,8 +1257,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.
Expand Down Expand Up @@ -1258,19 +1353,6 @@ void OrchestratorGraphEdit::_synchronize_graph_node(Ref<OScriptNode> 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<OrchestratorGraphNodeComment>(node))
{
// Raises the child
move_child(comment, -1);
comment->call_deferred("raise_request_node_reorder");
}
});
}

void OrchestratorGraphEdit::_complete_spawn(const Ref<OScriptNode>& p_spawned, const Callable& p_callback)
{
if (p_spawned.is_valid())
Expand Down
24 changes: 18 additions & 6 deletions src/editor/graph/graph_edit.h
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ class OrchestratorGraphEdit : public GraphEdit
HashMap<uint64_t, Vector<Ref<KnotPoint>>> _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;
Expand All @@ -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 <code>_connection_layer</code> 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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -346,9 +361,6 @@ class OrchestratorGraphEdit : public GraphEdit
/// @param p_node the node to update.
void _synchronize_graph_node(Ref<OScriptNode> 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
Expand Down
32 changes: 16 additions & 16 deletions src/editor/graph/graph_node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,28 +97,28 @@ void OrchestratorGraphNode::_notification(int p_what)

void OrchestratorGraphNode::_gui_input(const Ref<InputEvent>& p_event)
{
Ref<InputEventMouseButton> 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<InputEventMouseButton> 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()
Expand Down
86 changes: 58 additions & 28 deletions src/editor/graph/nodes/graph_node_comment.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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()
Expand Down Expand Up @@ -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<StyleBox> sb_panel = get_theme_stylebox("panel");
Ref<StyleBox> sb_titlebar = get_theme_stylebox("titlebar");
Ref<Texture2D> 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<InputEvent>& p_event)
{
Ref<InputEventMouseButton> mb = p_event;
if (mb.is_valid())
Ref<InputEventMouseButton> 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<OrchestratorGraphEdit>(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<OrchestratorGraphNodeComment>(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()
Expand All @@ -142,13 +181,4 @@ void OrchestratorGraphNodeComment::deselect_group()
List<GraphElement*> 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<GraphElement*> intersections = get_elements_within_global_rect();
for (GraphElement* node : intersections)
get_parent()->move_child(node, -1);
}
Loading

0 comments on commit 1253bc2

Please sign in to comment.