Explorar o código

Add read-only mode to AnimationTreeEditor plugins

SaracenOne %!s(int64=3) %!d(string=hai) anos
pai
achega
75f1357ced

+ 3 - 0
doc/classes/GraphEdit.xml

@@ -221,6 +221,9 @@
 		</method>
 	</methods>
 	<members>
+		<member name="arrange_nodes_button_hidden" type="bool" setter="set_arrange_nodes_button_hidden" getter="is_arrange_nodes_button_hidden" default="false">
+			If [code]true[/code], the Arrange Nodes button is hidden.
+		</member>
 		<member name="clip_contents" type="bool" setter="set_clip_contents" getter="is_clipping_contents" overrides="Control" default="true" />
 		<member name="connection_lines_antialiased" type="bool" setter="set_connection_lines_antialiased" getter="is_connection_lines_antialiased" default="true">
 			If [code]true[/code], the lines between nodes will use antialiasing.

+ 9 - 3
doc/classes/GraphNode.xml

@@ -156,7 +156,7 @@
 			<description>
 				Sets properties of the slot with ID [param idx].
 				If [param enable_left]/[param enable_right], a port will appear and the slot will be able to be connected from this side.
-				[param type_left]/[param type_right] is an arbitrary type of the port. Only ports with the same type values can be connected.
+				[param type_left]/[param type_right] is an arbitrary type of the port. Only ports with the same type values can be connected and negative values will disallow all connections to be made via user inputs.
 				[param color_left]/[param color_right] is the tint of the port's icon on this side.
 				[param custom_left]/[param custom_right] is a custom texture for this side's port.
 				[b]Note:[/b] This method only sets properties of the slot. To create the slot, add a [Control]-derived child to the GraphNode.
@@ -208,7 +208,7 @@
 			<param index="0" name="idx" type="int" />
 			<param index="1" name="type_left" type="int" />
 			<description>
-				Sets the left (input) type of the slot [param idx] to [param type_left].
+				Sets the left (input) type of the slot [param idx] to [param type_left]. If the value is negative, all connections will be disallowed to be created via user inputs.
 			</description>
 		</method>
 		<method name="set_slot_type_right">
@@ -216,7 +216,7 @@
 			<param index="0" name="idx" type="int" />
 			<param index="1" name="type_right" type="int" />
 			<description>
-				Sets the right (output) type of the slot [param idx] to [param type_right].
+				Sets the right (output) type of the slot [param idx] to [param type_right]. If the value is negative, all connections will be disallowed to be created via user inputs.
 			</description>
 		</method>
 	</methods>
@@ -224,6 +224,9 @@
 		<member name="comment" type="bool" setter="set_comment" getter="is_comment" default="false">
 			If [code]true[/code], the GraphNode is a comment node.
 		</member>
+		<member name="draggable" type="bool" setter="set_draggable" getter="is_draggable" default="true">
+			If [code]true[/code], the user can drag the GraphNode.
+		</member>
 		<member name="language" type="String" setter="set_language" getter="get_language" default="&quot;&quot;">
 			Language code used for line-breaking and text shaping algorithms, if left empty current locale is used instead.
 		</member>
@@ -239,6 +242,9 @@
 			If [code]true[/code], the user can resize the GraphNode.
 			[b]Note:[/b] Dragging the handle will only emit the [signal resize_request] signal, the GraphNode needs to be resized manually.
 		</member>
+		<member name="selectable" type="bool" setter="set_selectable" getter="is_selectable" default="true">
+			If [code]true[/code], the user can select the GraphNode.
+		</member>
 		<member name="selected" type="bool" setter="set_selected" getter="is_selected" default="false">
 			If [code]true[/code], the GraphNode is selected.
 		</member>

+ 4 - 0
editor/editor_spin_slider.cpp

@@ -149,6 +149,10 @@ void EditorSpinSlider::gui_input(const Ref<InputEvent> &p_event) {
 }
 
 void EditorSpinSlider::_grabber_gui_input(const Ref<InputEvent> &p_event) {
+	if (read_only) {
+		return;
+	}
+
 	Ref<InputEventMouseButton> mb = p_event;
 
 	if (grabbing_grabber) {

+ 91 - 69
editor/plugins/animation_blend_space_1d_editor.cpp

@@ -48,7 +48,9 @@ void AnimationNodeBlendSpace1DEditor::_blend_space_gui_input(const Ref<InputEven
 
 	if (tool_select->is_pressed() && k.is_valid() && k->is_pressed() && k->get_keycode() == Key::KEY_DELETE && !k->is_echo()) {
 		if (selected_point != -1) {
-			_erase_selected();
+			if (!read_only) {
+				_erase_selected();
+			}
 			accept_event();
 		}
 	}
@@ -56,62 +58,64 @@ void AnimationNodeBlendSpace1DEditor::_blend_space_gui_input(const Ref<InputEven
 	Ref<InputEventMouseButton> mb = p_event;
 
 	if (mb.is_valid() && mb->is_pressed() && ((tool_select->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) || (mb->get_button_index() == MouseButton::LEFT && tool_create->is_pressed()))) {
-		menu->clear();
-		animations_menu->clear();
-		animations_to_add.clear();
+		if (!read_only) {
+			menu->clear();
+			animations_menu->clear();
+			animations_to_add.clear();
 
-		List<StringName> classes;
-		ClassDB::get_inheriters_from_class("AnimationRootNode", &classes);
-		classes.sort_custom<StringName::AlphCompare>();
+			List<StringName> classes;
+			ClassDB::get_inheriters_from_class("AnimationRootNode", &classes);
+			classes.sort_custom<StringName::AlphCompare>();
 
-		menu->add_submenu_item(TTR("Add Animation"), "animations");
+			menu->add_submenu_item(TTR("Add Animation"), "animations");
 
-		AnimationTree *gp = AnimationTreeEditor::get_singleton()->get_tree();
-		ERR_FAIL_COND(!gp);
+			AnimationTree *gp = AnimationTreeEditor::get_singleton()->get_tree();
+			ERR_FAIL_COND(!gp);
 
-		if (gp->has_node(gp->get_animation_player())) {
-			AnimationPlayer *ap = Object::cast_to<AnimationPlayer>(gp->get_node(gp->get_animation_player()));
+			if (gp->has_node(gp->get_animation_player())) {
+				AnimationPlayer *ap = Object::cast_to<AnimationPlayer>(gp->get_node(gp->get_animation_player()));
 
-			if (ap) {
-				List<StringName> names;
-				ap->get_animation_list(&names);
+				if (ap) {
+					List<StringName> names;
+					ap->get_animation_list(&names);
 
-				for (const StringName &E : names) {
-					animations_menu->add_icon_item(get_theme_icon(SNAME("Animation"), SNAME("EditorIcons")), E);
-					animations_to_add.push_back(E);
+					for (const StringName &E : names) {
+						animations_menu->add_icon_item(get_theme_icon(SNAME("Animation"), SNAME("EditorIcons")), E);
+						animations_to_add.push_back(E);
+					}
 				}
 			}
-		}
 
-		for (const StringName &E : classes) {
-			String name = String(E).replace_first("AnimationNode", "");
-			if (name == "Animation" || name == "StartState" || name == "EndState") {
-				continue;
-			}
+			for (const StringName &E : classes) {
+				String name = String(E).replace_first("AnimationNode", "");
+				if (name == "Animation" || name == "StartState" || name == "EndState") {
+					continue;
+				}
 
-			int idx = menu->get_item_count();
-			menu->add_item(vformat(TTR("Add %s"), name), idx);
-			menu->set_item_metadata(idx, E);
-		}
+				int idx = menu->get_item_count();
+				menu->add_item(vformat(TTR("Add %s"), name), idx);
+				menu->set_item_metadata(idx, E);
+			}
 
-		Ref<AnimationNode> clipb = EditorSettings::get_singleton()->get_resource_clipboard();
-		if (clipb.is_valid()) {
+			Ref<AnimationNode> clipb = EditorSettings::get_singleton()->get_resource_clipboard();
+			if (clipb.is_valid()) {
+				menu->add_separator();
+				menu->add_item(TTR("Paste"), MENU_PASTE);
+			}
 			menu->add_separator();
-			menu->add_item(TTR("Paste"), MENU_PASTE);
-		}
-		menu->add_separator();
-		menu->add_item(TTR("Load..."), MENU_LOAD_FILE);
+			menu->add_item(TTR("Load..."), MENU_LOAD_FILE);
 
-		menu->set_position(blend_space_draw->get_screen_position() + mb->get_position());
-		menu->reset_size();
-		menu->popup();
+			menu->set_position(blend_space_draw->get_screen_position() + mb->get_position());
+			menu->reset_size();
+			menu->popup();
 
-		add_point_pos = (mb->get_position() / blend_space_draw->get_size()).x;
-		add_point_pos *= (blend_space->get_max_space() - blend_space->get_min_space());
-		add_point_pos += blend_space->get_min_space();
+			add_point_pos = (mb->get_position() / blend_space_draw->get_size()).x;
+			add_point_pos *= (blend_space->get_max_space() - blend_space->get_min_space());
+			add_point_pos += blend_space->get_min_space();
 
-		if (snap->is_pressed()) {
-			add_point_pos = Math::snapped(add_point_pos, blend_space->get_snap());
+			if (snap->is_pressed()) {
+				add_point_pos = Math::snapped(add_point_pos, blend_space->get_snap());
+			}
 		}
 	}
 
@@ -138,31 +142,33 @@ void AnimationNodeBlendSpace1DEditor::_blend_space_gui_input(const Ref<InputEven
 	}
 
 	if (mb.is_valid() && !mb->is_pressed() && dragging_selected_attempt && mb->get_button_index() == MouseButton::LEFT) {
-		if (dragging_selected) {
-			// move
-			float point = blend_space->get_blend_point_position(selected_point);
-			point += drag_ofs.x;
+		if (!read_only) {
+			if (dragging_selected) {
+				// move
+				float point = blend_space->get_blend_point_position(selected_point);
+				point += drag_ofs.x;
+
+				if (snap->is_pressed()) {
+					point = Math::snapped(point, blend_space->get_snap());
+				}
 
-			if (snap->is_pressed()) {
-				point = Math::snapped(point, blend_space->get_snap());
+				updating = true;
+				undo_redo->create_action(TTR("Move Node Point"));
+				undo_redo->add_do_method(blend_space.ptr(), "set_blend_point_position", selected_point, point);
+				undo_redo->add_undo_method(blend_space.ptr(), "set_blend_point_position", selected_point, blend_space->get_blend_point_position(selected_point));
+				undo_redo->add_do_method(this, "_update_space");
+				undo_redo->add_undo_method(this, "_update_space");
+				undo_redo->add_do_method(this, "_update_edited_point_pos");
+				undo_redo->add_undo_method(this, "_update_edited_point_pos");
+				undo_redo->commit_action();
+				updating = false;
+				_update_edited_point_pos();
 			}
 
-			updating = true;
-			undo_redo->create_action(TTR("Move Node Point"));
-			undo_redo->add_do_method(blend_space.ptr(), "set_blend_point_position", selected_point, point);
-			undo_redo->add_undo_method(blend_space.ptr(), "set_blend_point_position", selected_point, blend_space->get_blend_point_position(selected_point));
-			undo_redo->add_do_method(this, "_update_space");
-			undo_redo->add_undo_method(this, "_update_space");
-			undo_redo->add_do_method(this, "_update_edited_point_pos");
-			undo_redo->add_undo_method(this, "_update_edited_point_pos");
-			undo_redo->commit_action();
-			updating = false;
-			_update_edited_point_pos();
+			dragging_selected_attempt = false;
+			dragging_selected = false;
+			blend_space_draw->update();
 		}
-
-		dragging_selected_attempt = false;
-		dragging_selected = false;
-		blend_space_draw->update();
 	}
 
 	// *set* the blend
@@ -255,10 +261,12 @@ void AnimationNodeBlendSpace1DEditor::_blend_space_draw() {
 	for (int i = 0; i < blend_space->get_blend_point_count(); i++) {
 		float point = blend_space->get_blend_point_position(i);
 
-		if (dragging_selected && selected_point == i) {
-			point += drag_ofs.x;
-			if (snap->is_pressed()) {
-				point = Math::snapped(point, blend_space->get_snap());
+		if (!read_only) {
+			if (dragging_selected && selected_point == i) {
+				point += drag_ofs.x;
+				if (snap->is_pressed()) {
+					point = Math::snapped(point, blend_space->get_snap());
+				}
 			}
 		}
 
@@ -475,7 +483,7 @@ void AnimationNodeBlendSpace1DEditor::_update_edited_point_pos() {
 
 void AnimationNodeBlendSpace1DEditor::_update_tool_erase() {
 	bool point_valid = selected_point >= 0 && selected_point < blend_space->get_blend_point_count();
-	tool_erase->set_disabled(!point_valid);
+	tool_erase->set_disabled(!point_valid || read_only);
 
 	if (point_valid) {
 		Ref<AnimationNode> an = blend_space->get_blend_point_node(selected_point);
@@ -486,7 +494,11 @@ void AnimationNodeBlendSpace1DEditor::_update_tool_erase() {
 			open_editor->hide();
 		}
 
-		edit_hb->show();
+		if (!read_only) {
+			edit_hb->show();
+		} else {
+			edit_hb->hide();
+		}
 	} else {
 		edit_hb->hide();
 	}
@@ -590,10 +602,20 @@ bool AnimationNodeBlendSpace1DEditor::can_edit(const Ref<AnimationNode> &p_node)
 
 void AnimationNodeBlendSpace1DEditor::edit(const Ref<AnimationNode> &p_node) {
 	blend_space = p_node;
+	read_only = false;
 
 	if (!blend_space.is_null()) {
+		read_only = EditorNode::get_singleton()->is_resource_read_only(blend_space);
+
 		_update_space();
 	}
+
+	tool_create->set_disabled(read_only);
+	edit_value->set_editable(!read_only);
+	label_value->set_editable(!read_only);
+	min_value->set_editable(!read_only);
+	max_value->set_editable(!read_only);
+	sync->set_disabled(read_only);
 }
 
 AnimationNodeBlendSpace1DEditor *AnimationNodeBlendSpace1DEditor::singleton = nullptr;

+ 1 - 0
editor/plugins/animation_blend_space_1d_editor.h

@@ -46,6 +46,7 @@ class AnimationNodeBlendSpace1DEditor : public AnimationTreeNodeEditorPlugin {
 	GDCLASS(AnimationNodeBlendSpace1DEditor, AnimationTreeNodeEditorPlugin);
 
 	Ref<AnimationNodeBlendSpace1D> blend_space;
+	bool read_only = false;
 
 	HBoxContainer *goto_parent_hb = nullptr;
 	Button *goto_parent = nullptr;

+ 99 - 64
editor/plugins/animation_blend_space_2d_editor.cpp

@@ -60,11 +60,29 @@ void AnimationNodeBlendSpace2DEditor::edit(const Ref<AnimationNode> &p_node) {
 		blend_space->disconnect("triangles_updated", callable_mp(this, &AnimationNodeBlendSpace2DEditor::_blend_space_changed));
 	}
 	blend_space = p_node;
+	read_only = false;
 
 	if (!blend_space.is_null()) {
+		read_only = EditorNode::get_singleton()->is_resource_read_only(blend_space);
+
 		blend_space->connect("triangles_updated", callable_mp(this, &AnimationNodeBlendSpace2DEditor::_blend_space_changed));
 		_update_space();
 	}
+
+	tool_create->set_disabled(read_only);
+	interpolation->set_disabled(read_only);
+	max_x_value->set_editable(!read_only);
+	min_x_value->set_editable(!read_only);
+	max_y_value->set_editable(!read_only);
+	min_y_value->set_editable(!read_only);
+	label_x->set_editable(!read_only);
+	label_y->set_editable(!read_only);
+	edit_x->set_editable(!read_only);
+	edit_y->set_editable(!read_only);
+	tool_triangle->set_disabled(read_only);
+	auto_triangles->set_disabled(read_only);
+	sync->set_disabled(read_only);
+	interpolation->set_disabled(read_only);
 }
 
 StringName AnimationNodeBlendSpace2DEditor::get_blend_position_path() const {
@@ -76,7 +94,9 @@ void AnimationNodeBlendSpace2DEditor::_blend_space_gui_input(const Ref<InputEven
 	Ref<InputEventKey> k = p_event;
 	if (tool_select->is_pressed() && k.is_valid() && k->is_pressed() && k->get_keycode() == Key::KEY_DELETE && !k->is_echo()) {
 		if (selected_point != -1 || selected_triangle != -1) {
-			_erase_selected();
+			if (!read_only) {
+				_erase_selected();
+			}
 			accept_event();
 		}
 	}
@@ -84,57 +104,59 @@ void AnimationNodeBlendSpace2DEditor::_blend_space_gui_input(const Ref<InputEven
 	Ref<InputEventMouseButton> mb = p_event;
 
 	if (mb.is_valid() && mb->is_pressed() && ((tool_select->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) || (mb->get_button_index() == MouseButton::LEFT && tool_create->is_pressed()))) {
-		menu->clear();
-		animations_menu->clear();
-		animations_to_add.clear();
-		List<StringName> classes;
-		classes.sort_custom<StringName::AlphCompare>();
-
-		ClassDB::get_inheriters_from_class("AnimationRootNode", &classes);
-		menu->add_submenu_item(TTR("Add Animation"), "animations");
-
-		AnimationTree *gp = AnimationTreeEditor::get_singleton()->get_tree();
-		ERR_FAIL_COND(!gp);
-		if (gp && gp->has_node(gp->get_animation_player())) {
-			AnimationPlayer *ap = Object::cast_to<AnimationPlayer>(gp->get_node(gp->get_animation_player()));
-			if (ap) {
-				List<StringName> names;
-				ap->get_animation_list(&names);
-				for (const StringName &E : names) {
-					animations_menu->add_icon_item(get_theme_icon(SNAME("Animation"), SNAME("EditorIcons")), E);
-					animations_to_add.push_back(E);
+		if (!read_only) {
+			menu->clear();
+			animations_menu->clear();
+			animations_to_add.clear();
+			List<StringName> classes;
+			classes.sort_custom<StringName::AlphCompare>();
+
+			ClassDB::get_inheriters_from_class("AnimationRootNode", &classes);
+			menu->add_submenu_item(TTR("Add Animation"), "animations");
+
+			AnimationTree *gp = AnimationTreeEditor::get_singleton()->get_tree();
+			ERR_FAIL_COND(!gp);
+			if (gp && gp->has_node(gp->get_animation_player())) {
+				AnimationPlayer *ap = Object::cast_to<AnimationPlayer>(gp->get_node(gp->get_animation_player()));
+				if (ap) {
+					List<StringName> names;
+					ap->get_animation_list(&names);
+					for (const StringName &E : names) {
+						animations_menu->add_icon_item(get_theme_icon(SNAME("Animation"), SNAME("EditorIcons")), E);
+						animations_to_add.push_back(E);
+					}
 				}
 			}
-		}
 
-		for (const StringName &E : classes) {
-			String name = String(E).replace_first("AnimationNode", "");
-			if (name == "Animation" || name == "StartState" || name == "EndState") {
-				continue; // nope
+			for (const StringName &E : classes) {
+				String name = String(E).replace_first("AnimationNode", "");
+				if (name == "Animation" || name == "StartState" || name == "EndState") {
+					continue; // nope
+				}
+				int idx = menu->get_item_count();
+				menu->add_item(vformat(TTR("Add %s"), name), idx);
+				menu->set_item_metadata(idx, E);
 			}
-			int idx = menu->get_item_count();
-			menu->add_item(vformat(TTR("Add %s"), name), idx);
-			menu->set_item_metadata(idx, E);
-		}
 
-		Ref<AnimationNode> clipb = EditorSettings::get_singleton()->get_resource_clipboard();
-		if (clipb.is_valid()) {
+			Ref<AnimationNode> clipb = EditorSettings::get_singleton()->get_resource_clipboard();
+			if (clipb.is_valid()) {
+				menu->add_separator();
+				menu->add_item(TTR("Paste"), MENU_PASTE);
+			}
 			menu->add_separator();
-			menu->add_item(TTR("Paste"), MENU_PASTE);
-		}
-		menu->add_separator();
-		menu->add_item(TTR("Load..."), MENU_LOAD_FILE);
-
-		menu->set_position(blend_space_draw->get_screen_position() + mb->get_position());
-		menu->reset_size();
-		menu->popup();
-		add_point_pos = (mb->get_position() / blend_space_draw->get_size());
-		add_point_pos.y = 1.0 - add_point_pos.y;
-		add_point_pos *= (blend_space->get_max_space() - blend_space->get_min_space());
-		add_point_pos += blend_space->get_min_space();
-
-		if (snap->is_pressed()) {
-			add_point_pos = add_point_pos.snapped(blend_space->get_snap());
+			menu->add_item(TTR("Load..."), MENU_LOAD_FILE);
+
+			menu->set_position(blend_space_draw->get_screen_position() + mb->get_position());
+			menu->reset_size();
+			menu->popup();
+			add_point_pos = (mb->get_position() / blend_space_draw->get_size());
+			add_point_pos.y = 1.0 - add_point_pos.y;
+			add_point_pos *= (blend_space->get_max_space() - blend_space->get_min_space());
+			add_point_pos += blend_space->get_min_space();
+
+			if (snap->is_pressed()) {
+				add_point_pos = add_point_pos.snapped(blend_space->get_snap());
+			}
 		}
 	}
 
@@ -222,17 +244,19 @@ void AnimationNodeBlendSpace2DEditor::_blend_space_gui_input(const Ref<InputEven
 				point = point.snapped(blend_space->get_snap());
 			}
 
-			updating = true;
-			undo_redo->create_action(TTR("Move Node Point"));
-			undo_redo->add_do_method(blend_space.ptr(), "set_blend_point_position", selected_point, point);
-			undo_redo->add_undo_method(blend_space.ptr(), "set_blend_point_position", selected_point, blend_space->get_blend_point_position(selected_point));
-			undo_redo->add_do_method(this, "_update_space");
-			undo_redo->add_undo_method(this, "_update_space");
-			undo_redo->add_do_method(this, "_update_edited_point_pos");
-			undo_redo->add_undo_method(this, "_update_edited_point_pos");
-			undo_redo->commit_action();
-			updating = false;
-			_update_edited_point_pos();
+			if (!read_only) {
+				updating = true;
+				undo_redo->create_action(TTR("Move Node Point"));
+				undo_redo->add_do_method(blend_space.ptr(), "set_blend_point_position", selected_point, point);
+				undo_redo->add_undo_method(blend_space.ptr(), "set_blend_point_position", selected_point, blend_space->get_blend_point_position(selected_point));
+				undo_redo->add_do_method(this, "_update_space");
+				undo_redo->add_undo_method(this, "_update_space");
+				undo_redo->add_do_method(this, "_update_edited_point_pos");
+				undo_redo->add_undo_method(this, "_update_edited_point_pos");
+				undo_redo->commit_action();
+				updating = false;
+				_update_edited_point_pos();
+			}
 		}
 		dragging_selected_attempt = false;
 		dragging_selected = false;
@@ -259,7 +283,9 @@ void AnimationNodeBlendSpace2DEditor::_blend_space_gui_input(const Ref<InputEven
 
 	if (mm.is_valid() && dragging_selected_attempt) {
 		dragging_selected = true;
-		drag_ofs = ((mm->get_position() - drag_from) / blend_space_draw->get_size()) * (blend_space->get_max_space() - blend_space->get_min_space()) * Vector2(1, -1);
+		if (!read_only) {
+			drag_ofs = ((mm->get_position() - drag_from) / blend_space_draw->get_size()) * (blend_space->get_max_space() - blend_space->get_min_space()) * Vector2(1, -1);
+		}
 		blend_space_draw->update();
 		_update_edited_point_pos();
 	}
@@ -355,7 +381,10 @@ void AnimationNodeBlendSpace2DEditor::_add_animation_type(int p_index) {
 }
 
 void AnimationNodeBlendSpace2DEditor::_update_tool_erase() {
-	tool_erase->set_disabled(!(selected_point >= 0 && selected_point < blend_space->get_blend_point_count()) && !(selected_triangle >= 0 && selected_triangle < blend_space->get_triangle_count()));
+	tool_erase->set_disabled(
+			(!(selected_point >= 0 && selected_point < blend_space->get_blend_point_count()) && !(selected_triangle >= 0 && selected_triangle < blend_space->get_triangle_count())) ||
+			read_only);
+
 	if (selected_point >= 0 && selected_point < blend_space->get_blend_point_count()) {
 		Ref<AnimationNode> an = blend_space->get_blend_point_node(selected_point);
 		if (AnimationTreeEditor::get_singleton()->can_edit(an)) {
@@ -363,7 +392,11 @@ void AnimationNodeBlendSpace2DEditor::_update_tool_erase() {
 		} else {
 			open_editor->hide();
 		}
-		edit_hb->show();
+		if (!read_only) {
+			edit_hb->show();
+		} else {
+			edit_hb->hide();
+		}
 	} else {
 		edit_hb->hide();
 	}
@@ -503,10 +536,12 @@ void AnimationNodeBlendSpace2DEditor::_blend_space_draw() {
 	points.clear();
 	for (int i = 0; i < blend_space->get_blend_point_count(); i++) {
 		Vector2 point = blend_space->get_blend_point_position(i);
-		if (dragging_selected && selected_point == i) {
-			point += drag_ofs;
-			if (snap->is_pressed()) {
-				point = point.snapped(blend_space->get_snap());
+		if (!read_only) {
+			if (dragging_selected && selected_point == i) {
+				point += drag_ofs;
+				if (snap->is_pressed()) {
+					point = point.snapped(blend_space->get_snap());
+				}
 			}
 		}
 		point = (point - blend_space->get_min_space()) / (blend_space->get_max_space() - blend_space->get_min_space());

+ 1 - 0
editor/plugins/animation_blend_space_2d_editor.h

@@ -46,6 +46,7 @@ class AnimationNodeBlendSpace2DEditor : public AnimationTreeNodeEditorPlugin {
 	GDCLASS(AnimationNodeBlendSpace2DEditor, AnimationTreeNodeEditorPlugin);
 
 	Ref<AnimationNodeBlendSpace2D> blend_space;
+	bool read_only = false;
 
 	PanelContainer *panel = nullptr;
 	Button *tool_blend = nullptr;

+ 73 - 12
editor/plugins/animation_blend_tree_editor_plugin.cpp

@@ -134,6 +134,8 @@ void AnimationNodeBlendTreeEditor::_update_graph() {
 		GraphNode *node = memnew(GraphNode);
 		graph->add_child(node);
 
+		node->set_draggable(!read_only);
+
 		Ref<AnimationNode> agnode = blend_tree->get_node(E);
 		ERR_CONTINUE(!agnode.is_valid());
 
@@ -146,9 +148,10 @@ void AnimationNodeBlendTreeEditor::_update_graph() {
 		if (String(E) != "output") {
 			LineEdit *name = memnew(LineEdit);
 			name->set_text(E);
+			name->set_editable(!read_only);
 			name->set_expand_to_text_length_enabled(true);
 			node->add_child(name);
-			node->set_slot(0, false, 0, Color(), true, 0, get_theme_color(SNAME("font_color"), SNAME("Label")));
+			node->set_slot(0, false, 0, Color(), true, read_only ? -1 : 0, get_theme_color(SNAME("font_color"), SNAME("Label")));
 			name->connect("text_submitted", callable_mp(this, &AnimationNodeBlendTreeEditor::_node_renamed).bind(agnode), CONNECT_DEFERRED);
 			name->connect("focus_exited", callable_mp(this, &AnimationNodeBlendTreeEditor::_node_renamed_focus_out).bind(name, agnode), CONNECT_DEFERRED);
 			base = 1;
@@ -160,7 +163,7 @@ void AnimationNodeBlendTreeEditor::_update_graph() {
 			Label *in_name = memnew(Label);
 			node->add_child(in_name);
 			in_name->set_text(agnode->get_input_name(i));
-			node->set_slot(base + i, true, 0, get_theme_color(SNAME("font_color"), SNAME("Label")), false, 0, Color());
+			node->set_slot(base + i, true, read_only ? -1 : 0, get_theme_color(SNAME("font_color"), SNAME("Label")), false, 0, Color());
 		}
 
 		List<PropertyInfo> pinfo;
@@ -172,6 +175,7 @@ void AnimationNodeBlendTreeEditor::_update_graph() {
 			String base_path = AnimationTreeEditor::get_singleton()->get_base_path() + String(E) + "/" + F.name;
 			EditorProperty *prop = EditorInspector::instantiate_property_editor(AnimationTreeEditor::get_singleton()->get_tree(), F.type, base_path, F.hint, F.hint_string, F.usage);
 			if (prop) {
+				prop->set_read_only(read_only);
 				prop->set_object_and_property(AnimationTreeEditor::get_singleton()->get_tree(), base_path);
 				prop->update_property();
 				prop->set_name_split_ratio(0);
@@ -195,12 +199,16 @@ void AnimationNodeBlendTreeEditor::_update_graph() {
 
 		if (agnode->has_filter()) {
 			node->add_child(memnew(HSeparator));
-			Button *edit_filters = memnew(Button);
-			edit_filters->set_text(TTR("Edit Filters"));
-			edit_filters->set_icon(get_theme_icon(SNAME("AnimationFilter"), SNAME("EditorIcons")));
-			node->add_child(edit_filters);
-			edit_filters->connect("pressed", callable_mp(this, &AnimationNodeBlendTreeEditor::_edit_filters).bind(E), CONNECT_DEFERRED);
-			edit_filters->set_h_size_flags(SIZE_SHRINK_CENTER);
+			Button *inspect_filters = memnew(Button);
+			if (read_only) {
+				inspect_filters->set_text(TTR("Inspect Filters"));
+			} else {
+				inspect_filters->set_text(TTR("Edit Filters"));
+			}
+			inspect_filters->set_icon(get_theme_icon(SNAME("AnimationFilter"), SNAME("EditorIcons")));
+			node->add_child(inspect_filters);
+			inspect_filters->connect("pressed", callable_mp(this, &AnimationNodeBlendTreeEditor::_inspect_filters).bind(E), CONNECT_DEFERRED);
+			inspect_filters->set_h_size_flags(SIZE_SHRINK_CENTER);
 		}
 
 		Ref<AnimationNodeAnimation> anim = agnode;
@@ -208,6 +216,7 @@ void AnimationNodeBlendTreeEditor::_update_graph() {
 			MenuButton *mb = memnew(MenuButton);
 			mb->set_text(anim->get_animation());
 			mb->set_icon(get_theme_icon(SNAME("Animation"), SNAME("EditorIcons")));
+			mb->set_disabled(read_only);
 			Array options;
 
 			node->add_child(memnew(HSeparator));
@@ -370,10 +379,18 @@ void AnimationNodeBlendTreeEditor::_popup(bool p_has_input_ports, const Vector2
 }
 
 void AnimationNodeBlendTreeEditor::_popup_request(const Vector2 &p_position) {
+	if (read_only) {
+		return;
+	}
+
 	_popup(false, graph->get_screen_position() + graph->get_local_mouse_position(), p_position);
 }
 
 void AnimationNodeBlendTreeEditor::_connection_to_empty(const String &p_from, int p_from_slot, const Vector2 &p_release_position) {
+	if (read_only) {
+		return;
+	}
+
 	Ref<AnimationNode> node = blend_tree->get_node(p_from);
 	if (node.is_valid()) {
 		from_node = p_from;
@@ -382,6 +399,10 @@ void AnimationNodeBlendTreeEditor::_connection_to_empty(const String &p_from, in
 }
 
 void AnimationNodeBlendTreeEditor::_connection_from_empty(const String &p_to, int p_to_slot, const Vector2 &p_release_position) {
+	if (read_only) {
+		return;
+	}
+
 	Ref<AnimationNode> node = blend_tree->get_node(p_to);
 	if (node.is_valid()) {
 		to_node = p_to;
@@ -402,6 +423,10 @@ void AnimationNodeBlendTreeEditor::_node_dragged(const Vector2 &p_from, const Ve
 }
 
 void AnimationNodeBlendTreeEditor::_connection_request(const String &p_from, int p_from_index, const String &p_to, int p_to_index) {
+	if (read_only) {
+		return;
+	}
+
 	AnimationNodeBlendTree::ConnectionError err = blend_tree->can_connect_node(p_to, p_to_index, p_from);
 
 	if (err != AnimationNodeBlendTree::CONNECTION_OK) {
@@ -418,6 +443,10 @@ void AnimationNodeBlendTreeEditor::_connection_request(const String &p_from, int
 }
 
 void AnimationNodeBlendTreeEditor::_disconnection_request(const String &p_from, int p_from_index, const String &p_to, int p_to_index) {
+	if (read_only) {
+		return;
+	}
+
 	graph->disconnect_node(p_from, p_from_index, p_to, p_to_index);
 
 	updating = true;
@@ -445,6 +474,10 @@ void AnimationNodeBlendTreeEditor::_anim_selected(int p_index, Array p_options,
 }
 
 void AnimationNodeBlendTreeEditor::_delete_request(const String &p_which) {
+	if (read_only) {
+		return;
+	}
+
 	undo_redo->create_action(TTR("Delete Node"));
 	undo_redo->add_do_method(blend_tree.ptr(), "remove_node", p_which);
 	undo_redo->add_undo_method(blend_tree.ptr(), "add_node", p_which, blend_tree->get_node(p_which), blend_tree.ptr()->get_node_position(p_which));
@@ -464,6 +497,10 @@ void AnimationNodeBlendTreeEditor::_delete_request(const String &p_which) {
 }
 
 void AnimationNodeBlendTreeEditor::_delete_nodes_request(const TypedArray<StringName> &p_nodes) {
+	if (read_only) {
+		return;
+	}
+
 	List<StringName> to_erase;
 
 	if (p_nodes.is_empty()) {
@@ -495,6 +532,10 @@ void AnimationNodeBlendTreeEditor::_delete_nodes_request(const TypedArray<String
 }
 
 void AnimationNodeBlendTreeEditor::_node_selected(Object *p_node) {
+	if (read_only) {
+		return;
+	}
+
 	GraphNode *gn = Object::cast_to<GraphNode>(p_node);
 	ERR_FAIL_COND(!gn);
 
@@ -679,7 +720,7 @@ bool AnimationNodeBlendTreeEditor::_update_filters(const Ref<AnimationNode> &ano
 					}
 				}
 
-				ti->set_editable(0, true);
+				ti->set_editable(0, !read_only);
 				ti->set_selectable(0, true);
 				ti->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
 				ti->set_text(0, concat);
@@ -692,7 +733,7 @@ bool AnimationNodeBlendTreeEditor::_update_filters(const Ref<AnimationNode> &ano
 				ti = filters->create_item(ti);
 				ti->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
 				ti->set_text(0, concat);
-				ti->set_editable(0, true);
+				ti->set_editable(0, !read_only);
 				ti->set_selectable(0, true);
 				ti->set_checked(0, anode->is_path_filtered(path));
 				ti->set_metadata(0, path);
@@ -714,7 +755,7 @@ bool AnimationNodeBlendTreeEditor::_update_filters(const Ref<AnimationNode> &ano
 				ti = filters->create_item(ti);
 				ti->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
 				ti->set_text(0, types_text);
-				ti->set_editable(0, true);
+				ti->set_editable(0, !read_only);
 				ti->set_selectable(0, true);
 				ti->set_checked(0, anode->is_path_filtered(path));
 				ti->set_metadata(0, path);
@@ -727,7 +768,15 @@ bool AnimationNodeBlendTreeEditor::_update_filters(const Ref<AnimationNode> &ano
 	return true;
 }
 
-void AnimationNodeBlendTreeEditor::_edit_filters(const String &p_which) {
+void AnimationNodeBlendTreeEditor::_inspect_filters(const String &p_which) {
+	if (read_only) {
+		filter_dialog->set_title(TTR("Inspect Filtered Tracks:"));
+	} else {
+		filter_dialog->set_title(TTR("Edit Filtered Tracks:"));
+	}
+
+	filter_enabled->set_disabled(read_only);
+
 	Ref<AnimationNode> anode = blend_tree->get_node(p_which);
 	ERR_FAIL_COND(!anode.is_valid());
 
@@ -838,6 +887,10 @@ void AnimationNodeBlendTreeEditor::_notification(int p_what) {
 }
 
 void AnimationNodeBlendTreeEditor::_scroll_changed(const Vector2 &p_scroll) {
+	if (read_only) {
+		return;
+	}
+
 	if (updating) {
 		return;
 	}
@@ -944,13 +997,20 @@ void AnimationNodeBlendTreeEditor::edit(const Ref<AnimationNode> &p_node) {
 
 	blend_tree = p_node;
 
+	read_only = false;
+
 	if (blend_tree.is_null()) {
 		hide();
 	} else {
+		read_only = EditorNode::get_singleton()->is_resource_read_only(blend_tree);
+
 		blend_tree->connect("removed_from_graph", callable_mp(this, &AnimationNodeBlendTreeEditor::_removed_from_graph));
 
 		_update_graph();
 	}
+
+	add_node->set_disabled(read_only);
+	graph->set_arrange_nodes_button_hidden(read_only);
 }
 
 AnimationNodeBlendTreeEditor::AnimationNodeBlendTreeEditor() {
@@ -986,6 +1046,7 @@ AnimationNodeBlendTreeEditor::AnimationNodeBlendTreeEditor() {
 	graph->get_zoom_hbox()->move_child(add_node, 0);
 	add_node->get_popup()->connect("id_pressed", callable_mp(this, &AnimationNodeBlendTreeEditor::_add_node));
 	add_node->connect("about_to_popup", callable_mp(this, &AnimationNodeBlendTreeEditor::_update_options_menu).bind(false));
+	add_node->set_disabled(read_only);
 
 	add_options.push_back(AddOption("Animation", "AnimationNodeAnimation"));
 	add_options.push_back(AddOption("OneShot", "AnimationNodeOneShot", 2));

+ 4 - 1
editor/plugins/animation_blend_tree_editor_plugin.h

@@ -47,6 +47,9 @@ class AnimationNodeBlendTreeEditor : public AnimationTreeNodeEditorPlugin {
 	GDCLASS(AnimationNodeBlendTreeEditor, AnimationTreeNodeEditorPlugin);
 
 	Ref<AnimationNodeBlendTree> blend_tree;
+
+	bool read_only = false;
+
 	GraphEdit *graph = nullptr;
 	MenuButton *add_node = nullptr;
 	Vector2 position_from_popup_menu;
@@ -106,7 +109,7 @@ class AnimationNodeBlendTreeEditor : public AnimationTreeNodeEditorPlugin {
 	void _delete_nodes_request(const TypedArray<StringName> &p_nodes);
 
 	bool _update_filters(const Ref<AnimationNode> &anode);
-	void _edit_filters(const String &p_which);
+	void _inspect_filters(const String &p_which);
 	void _filter_edited();
 	void _filter_toggled();
 	Ref<AnimationNode> _filter_edit;

+ 47 - 30
editor/plugins/animation_state_machine_editor.cpp

@@ -56,7 +56,11 @@ bool AnimationNodeStateMachineEditor::can_edit(const Ref<AnimationNode> &p_node)
 void AnimationNodeStateMachineEditor::edit(const Ref<AnimationNode> &p_node) {
 	state_machine = p_node;
 
+	read_only = false;
+
 	if (state_machine.is_valid()) {
+		read_only = EditorNode::get_singleton()->is_resource_read_only(state_machine);
+
 		selected_transition_from = StringName();
 		selected_transition_to = StringName();
 		selected_transition_index = -1;
@@ -66,6 +70,9 @@ void AnimationNodeStateMachineEditor::edit(const Ref<AnimationNode> &p_node) {
 		_update_mode();
 		_update_graph();
 	}
+
+	tool_create->set_disabled(read_only);
+	tool_connect->set_disabled(read_only);
 }
 
 void AnimationNodeStateMachineEditor::_state_machine_gui_input(const Ref<InputEvent> &p_event) {
@@ -77,7 +84,9 @@ void AnimationNodeStateMachineEditor::_state_machine_gui_input(const Ref<InputEv
 	Ref<InputEventKey> k = p_event;
 	if (tool_select->is_pressed() && k.is_valid() && k->is_pressed() && k->get_keycode() == Key::KEY_DELETE && !k->is_echo()) {
 		if (selected_node != StringName() || !selected_nodes.is_empty() || selected_transition_to != StringName() || selected_transition_from != StringName()) {
-			_erase_selected();
+			if (!read_only) {
+				_erase_selected();
+			}
 			accept_event();
 		}
 	}
@@ -95,9 +104,11 @@ void AnimationNodeStateMachineEditor::_state_machine_gui_input(const Ref<InputEv
 	Ref<InputEventMouseButton> mb = p_event;
 
 	// Add new node
-	if (mb.is_valid() && mb->is_pressed() && !box_selecting && !connecting && ((tool_select->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) || (tool_create->is_pressed() && mb->get_button_index() == MouseButton::LEFT))) {
-		connecting_from = StringName();
-		_open_menu(mb->get_position());
+	if (!read_only) {
+		if (mb.is_valid() && mb->is_pressed() && !box_selecting && !connecting && ((tool_select->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) || (tool_create->is_pressed() && mb->get_button_index() == MouseButton::LEFT))) {
+			connecting_from = StringName();
+			_open_menu(mb->get_position());
+		}
 	}
 
 	// Select node or push a field inside
@@ -121,22 +132,24 @@ void AnimationNodeStateMachineEditor::_state_machine_gui_input(const Ref<InputEv
 				return;
 			}
 
-			if (node_rects[i].name.has_point(mb->get_position()) && state_machine->can_edit_node(node_rects[i].node_name)) { // edit name
-				Ref<StyleBox> line_sb = get_theme_stylebox(SNAME("normal"), SNAME("LineEdit"));
+			if (!read_only) {
+				if (node_rects[i].name.has_point(mb->get_position()) && state_machine->can_edit_node(node_rects[i].node_name)) { // edit name
+					Ref<StyleBox> line_sb = get_theme_stylebox(SNAME("normal"), SNAME("LineEdit"));
 
-				Rect2 edit_rect = node_rects[i].name;
-				edit_rect.position -= line_sb->get_offset();
-				edit_rect.size += line_sb->get_minimum_size();
+					Rect2 edit_rect = node_rects[i].name;
+					edit_rect.position -= line_sb->get_offset();
+					edit_rect.size += line_sb->get_minimum_size();
 
-				name_edit_popup->set_position(state_machine_draw->get_screen_position() + edit_rect.position);
-				name_edit_popup->set_size(edit_rect.size);
-				name_edit->set_text(node_rects[i].node_name);
-				name_edit_popup->popup();
-				name_edit->grab_focus();
-				name_edit->select_all();
+					name_edit_popup->set_position(state_machine_draw->get_screen_position() + edit_rect.position);
+					name_edit_popup->set_size(edit_rect.size);
+					name_edit->set_text(node_rects[i].node_name);
+					name_edit_popup->popup();
+					name_edit->grab_focus();
+					name_edit->select_all();
 
-				prev_name = node_rects[i].node_name;
-				return;
+					prev_name = node_rects[i].node_name;
+					return;
+				}
 			}
 
 			if (node_rects[i].edit.has_point(mb->get_position())) { //edit name
@@ -319,7 +332,7 @@ void AnimationNodeStateMachineEditor::_state_machine_gui_input(const Ref<InputEv
 	}
 
 	// Move mouse while connecting
-	if (mm.is_valid() && connecting) {
+	if (mm.is_valid() && connecting && !read_only) {
 		connecting_to = mm->get_position();
 		connecting_to_node = StringName();
 		state_machine_draw->update();
@@ -333,7 +346,7 @@ void AnimationNodeStateMachineEditor::_state_machine_gui_input(const Ref<InputEv
 	}
 
 	// Move mouse while moving a node
-	if (mm.is_valid() && dragging_selected_attempt) {
+	if (mm.is_valid() && dragging_selected_attempt && !read_only) {
 		dragging_selected = true;
 		drag_ofs = mm->get_position() - drag_from;
 		snap_x = StringName();
@@ -478,17 +491,21 @@ void AnimationNodeStateMachineEditor::_state_machine_gui_input(const Ref<InputEv
 }
 
 Control::CursorShape AnimationNodeStateMachineEditor::get_cursor_shape(const Point2 &p_pos) const {
-	// Put ibeam (text cursor) over names to make it clearer that they are editable.
-	Transform2D xform = panel->get_transform() * state_machine_draw->get_transform();
-	Point2 pos = xform.xform_inv(p_pos);
 	Control::CursorShape cursor_shape = get_default_cursor_shape();
-
-	for (int i = node_rects.size() - 1; i >= 0; i--) { // Inverse to draw order.
-		if (node_rects[i].node.has_point(pos)) {
-			if (node_rects[i].name.has_point(pos)) {
-				cursor_shape = Control::CURSOR_IBEAM;
+	if (!read_only) {
+		// Put ibeam (text cursor) over names to make it clearer that they are editable.
+		Transform2D xform = panel->get_transform() * state_machine_draw->get_transform();
+		Point2 pos = xform.xform_inv(p_pos);
+
+		for (int i = node_rects.size() - 1; i >= 0; i--) { // Inverse to draw order.
+			if (node_rects[i].node.has_point(pos)) {
+				if (node_rects[i].name.has_point(pos)) {
+					if (state_machine->can_edit_node(node_rects[i].node_name)) {
+						cursor_shape = Control::CURSOR_IBEAM;
+					}
+				}
+				break;
 			}
-			break;
 		}
 	}
 	return cursor_shape;
@@ -1848,9 +1865,9 @@ void AnimationNodeStateMachineEditor::_update_mode() {
 		tool_erase_hb->show();
 		bool nothing_selected = selected_nodes.is_empty() && selected_transition_from == StringName() && selected_transition_to == StringName();
 		bool start_end_selected = selected_nodes.size() == 1 && (*selected_nodes.begin() == state_machine->start_node || *selected_nodes.begin() == state_machine->end_node);
-		tool_erase->set_disabled(nothing_selected || start_end_selected);
+		tool_erase->set_disabled(nothing_selected || start_end_selected || read_only);
 
-		if (selected_nodes.is_empty() || start_end_selected) {
+		if (selected_nodes.is_empty() || start_end_selected || read_only) {
 			tool_group->set_disabled(true);
 			tool_group->set_visible(true);
 			tool_ungroup->set_visible(false);

+ 2 - 0
editor/plugins/animation_state_machine_editor.h

@@ -47,6 +47,8 @@ class AnimationNodeStateMachineEditor : public AnimationTreeNodeEditorPlugin {
 
 	Ref<AnimationNodeStateMachine> state_machine;
 
+	bool read_only = false;
+
 	Button *tool_select = nullptr;
 	Button *tool_create = nullptr;
 	Button *tool_connect = nullptr;

+ 55 - 23
scene/gui/graph_edit.cpp

@@ -607,13 +607,16 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
 									connecting_color = Object::cast_to<GraphNode>(to)->get_connection_input_color(E.to_port);
 									connecting_target = false;
 									connecting_to = pos;
-									just_disconnected = true;
 
-									emit_signal(SNAME("disconnection_request"), E.from, E.from_port, E.to, E.to_port);
-									to = get_node(String(connecting_from)); //maybe it was erased
-									if (Object::cast_to<GraphNode>(to)) {
-										connecting = true;
-										emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, false);
+									if (connecting_type >= 0) {
+										just_disconnected = true;
+
+										emit_signal(SNAME("disconnection_request"), E.from, E.from_port, E.to, E.to_port);
+										to = get_node(String(connecting_from)); //maybe it was erased
+										if (Object::cast_to<GraphNode>(to)) {
+											connecting = true;
+											emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, false);
+										}
 									}
 									return;
 								}
@@ -621,7 +624,6 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
 						}
 					}
 
-					connecting = true;
 					connecting_from = gn->get_name();
 					connecting_index = j;
 					connecting_out = true;
@@ -629,8 +631,11 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
 					connecting_color = gn->get_connection_output_color(j);
 					connecting_target = false;
 					connecting_to = pos;
-					just_disconnected = false;
-					emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, true);
+					if (connecting_type >= 0) {
+						connecting = true;
+						just_disconnected = false;
+						emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, true);
+					}
 					return;
 				}
 			}
@@ -657,11 +662,13 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
 									connecting_to = pos;
 									just_disconnected = true;
 
-									emit_signal(SNAME("disconnection_request"), E.from, E.from_port, E.to, E.to_port);
-									fr = get_node(String(connecting_from)); //maybe it was erased
-									if (Object::cast_to<GraphNode>(fr)) {
-										connecting = true;
-										emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, true);
+									if (connecting_type >= 0) {
+										emit_signal(SNAME("disconnection_request"), E.from, E.from_port, E.to, E.to_port);
+										fr = get_node(String(connecting_from)); //maybe it was erased
+										if (Object::cast_to<GraphNode>(fr)) {
+											connecting = true;
+											emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, true);
+										}
 									}
 									return;
 								}
@@ -669,7 +676,6 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
 						}
 					}
 
-					connecting = true;
 					connecting_from = gn->get_name();
 					connecting_index = j;
 					connecting_out = false;
@@ -677,8 +683,11 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
 					connecting_color = gn->get_connection_input_color(j);
 					connecting_target = false;
 					connecting_to = pos;
-					just_disconnected = false;
-					emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, false);
+					if (connecting_type >= 0) {
+						connecting = true;
+						just_disconnected = false;
+						emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, false);
+					}
 					return;
 				}
 			}
@@ -1127,7 +1136,7 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) {
 		drag_accum += mm->get_relative();
 		for (int i = get_child_count() - 1; i >= 0; i--) {
 			GraphNode *gn = Object::cast_to<GraphNode>(get_child(i));
-			if (gn && gn->is_selected()) {
+			if (gn && gn->is_selected() && gn->is_draggable()) {
 				Vector2 pos = (gn->get_drag_from() * zoom + drag_accum) / zoom;
 
 				// Snapping can be toggled temporarily by holding down Ctrl.
@@ -1166,7 +1175,9 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) {
 				} else if (gn->is_selected() && !box_selection_mode_additive) {
 					emit_signal(SNAME("node_deselected"), gn);
 				}
-				gn->set_selected(box_selection_mode_additive);
+				if (gn->is_selectable()) {
+					gn->set_selected(box_selection_mode_additive);
+				}
 			} else {
 				bool select = (previous_selected.find(gn) != nullptr);
 				if (gn->is_selected() && !select) {
@@ -1174,7 +1185,9 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) {
 				} else if (!gn->is_selected() && select) {
 					emit_signal(SNAME("node_selected"), gn);
 				}
-				gn->set_selected(select);
+				if (gn->is_selectable()) {
+					gn->set_selected(select);
+				}
 			}
 		}
 
@@ -1193,7 +1206,7 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) {
 						continue;
 					}
 
-					bool select = (previous_selected.find(gn) != nullptr);
+					bool select = (gn->is_selectable() && previous_selected.find(gn) != nullptr);
 					if (gn->is_selected() && !select) {
 						emit_signal(SNAME("node_deselected"), gn);
 					} else if (!gn->is_selected() && select) {
@@ -1285,7 +1298,7 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) {
 						GraphNode *o_gn = Object::cast_to<GraphNode>(get_child(i));
 						if (o_gn) {
 							if (o_gn == gn) {
-								o_gn->set_selected(true);
+								o_gn->set_selected(o_gn->is_selectable());
 							} else {
 								if (o_gn->is_selected()) {
 									emit_signal(SNAME("node_deselected"), o_gn);
@@ -1296,7 +1309,7 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) {
 					}
 				}
 
-				gn->set_selected(true);
+				gn->set_selected(gn->is_selectable());
 				for (int i = 0; i < get_child_count(); i++) {
 					GraphNode *o_gn = Object::cast_to<GraphNode>(get_child(i));
 					if (!o_gn) {
@@ -1711,6 +1724,19 @@ bool GraphEdit::is_minimap_enabled() const {
 	return minimap_button->is_pressed();
 }
 
+void GraphEdit::set_arrange_nodes_button_hidden(bool p_enable) {
+	arrange_nodes_button_hidden = p_enable;
+	if (arrange_nodes_button_hidden) {
+		layout_button->hide();
+	} else {
+		layout_button->show();
+	}
+}
+
+bool GraphEdit::is_arrange_nodes_button_hidden() const {
+	return arrange_nodes_button_hidden;
+}
+
 void GraphEdit::_minimap_toggled() {
 	if (is_minimap_enabled()) {
 		minimap->set_visible(true);
@@ -2343,6 +2369,9 @@ void GraphEdit::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_minimap_enabled", "enable"), &GraphEdit::set_minimap_enabled);
 	ClassDB::bind_method(D_METHOD("is_minimap_enabled"), &GraphEdit::is_minimap_enabled);
 
+	ClassDB::bind_method(D_METHOD("set_arrange_nodes_button_hidden", "enable"), &GraphEdit::set_arrange_nodes_button_hidden);
+	ClassDB::bind_method(D_METHOD("is_arrange_nodes_button_hidden"), &GraphEdit::is_arrange_nodes_button_hidden);
+
 	ClassDB::bind_method(D_METHOD("set_right_disconnects", "enable"), &GraphEdit::set_right_disconnects);
 	ClassDB::bind_method(D_METHOD("is_right_disconnects_enabled"), &GraphEdit::is_right_disconnects_enabled);
 
@@ -2382,6 +2411,9 @@ void GraphEdit::_bind_methods() {
 	ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "minimap_size", PROPERTY_HINT_NONE, "suffix:px"), "set_minimap_size", "get_minimap_size");
 	ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "minimap_opacity"), "set_minimap_opacity", "get_minimap_opacity");
 
+	ADD_GROUP("UI", "");
+	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "arrange_nodes_button_hidden"), "set_arrange_nodes_button_hidden", "is_arrange_nodes_button_hidden");
+
 	ADD_SIGNAL(MethodInfo("connection_request", PropertyInfo(Variant::STRING_NAME, "from"), PropertyInfo(Variant::INT, "from_slot"), PropertyInfo(Variant::STRING_NAME, "to"), PropertyInfo(Variant::INT, "to_slot")));
 	ADD_SIGNAL(MethodInfo("disconnection_request", PropertyInfo(Variant::STRING_NAME, "from"), PropertyInfo(Variant::INT, "from_slot"), PropertyInfo(Variant::STRING_NAME, "to"), PropertyInfo(Variant::INT, "to_slot")));
 	ADD_SIGNAL(MethodInfo("popup_request", PropertyInfo(Variant::VECTOR2, "position")));

+ 5 - 0
scene/gui/graph_edit.h

@@ -133,6 +133,8 @@ private:
 	void _pan_callback(Vector2 p_scroll_vec);
 	void _zoom_callback(Vector2 p_scroll_vec, Vector2 p_origin, bool p_alt);
 
+	bool arrange_nodes_button_hidden = false;
+
 	bool connecting = false;
 	String connecting_from;
 	bool connecting_out = false;
@@ -323,6 +325,9 @@ public:
 	void set_minimap_enabled(bool p_enable);
 	bool is_minimap_enabled() const;
 
+	void set_arrange_nodes_button_hidden(bool p_enable);
+	bool is_arrange_nodes_button_hidden() const;
+
 	GraphEditFilter *get_top_layer() const { return top_layer; }
 	GraphEditMinimap *get_minimap() const { return minimap; }
 	void get_connection_list(List<Connection> *r_connections) const;

+ 24 - 0
scene/gui/graph_node.cpp

@@ -1005,6 +1005,22 @@ bool GraphNode::is_resizable() const {
 	return resizable;
 }
 
+void GraphNode::set_draggable(bool p_draggable) {
+	draggable = p_draggable;
+}
+
+bool GraphNode::is_draggable() {
+	return draggable;
+}
+
+void GraphNode::set_selectable(bool p_selectable) {
+	selectable = p_selectable;
+}
+
+bool GraphNode::is_selectable() {
+	return selectable;
+}
+
 Vector<int> GraphNode::get_allowed_size_flags_horizontal() const {
 	Vector<int> flags;
 	flags.append(SIZE_FILL);
@@ -1066,6 +1082,12 @@ void GraphNode::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_resizable", "resizable"), &GraphNode::set_resizable);
 	ClassDB::bind_method(D_METHOD("is_resizable"), &GraphNode::is_resizable);
 
+	ClassDB::bind_method(D_METHOD("set_draggable", "draggable"), &GraphNode::set_draggable);
+	ClassDB::bind_method(D_METHOD("is_draggable"), &GraphNode::is_draggable);
+
+	ClassDB::bind_method(D_METHOD("set_selectable", "selectable"), &GraphNode::set_selectable);
+	ClassDB::bind_method(D_METHOD("is_selectable"), &GraphNode::is_selectable);
+
 	ClassDB::bind_method(D_METHOD("set_selected", "selected"), &GraphNode::set_selected);
 	ClassDB::bind_method(D_METHOD("is_selected"), &GraphNode::is_selected);
 
@@ -1091,6 +1113,8 @@ void GraphNode::_bind_methods() {
 	ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "position_offset", PROPERTY_HINT_NONE, "suffix:px"), "set_position_offset", "get_position_offset");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "show_close"), "set_show_close_button", "is_close_button_visible");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "resizable"), "set_resizable", "is_resizable");
+	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draggable"), "set_draggable", "is_draggable");
+	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "selectable"), "set_selectable", "is_selectable");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "selected"), "set_selected", "is_selected");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "comment"), "set_comment", "is_comment");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "overlay", PROPERTY_HINT_ENUM, "Disabled,Breakpoint,Position"), "set_overlay", "get_overlay");

+ 8 - 0
scene/gui/graph_node.h

@@ -67,6 +67,8 @@ private:
 	Vector2 position_offset;
 	bool comment = false;
 	bool resizable = false;
+	bool draggable = true;
+	bool selectable = true;
 
 	bool resizing = false;
 	Vector2 resizing_from;
@@ -183,6 +185,12 @@ public:
 	void set_resizable(bool p_enable);
 	bool is_resizable() const;
 
+	void set_draggable(bool p_draggable);
+	bool is_draggable();
+
+	void set_selectable(bool p_selectable);
+	bool is_selectable();
+
 	virtual Size2 get_minimum_size() const override;
 
 	virtual Vector<int> get_allowed_size_flags_horizontal() const override;