| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550 |
- extends GraphEdit
- var control_script
- var graph_edit
- var open_help
- var multiple_connections
- var selected_nodes = {} #used to track which nodes in the GraphEdit are selected
- var copied_nodes_data = [] #stores node data on ctrl+c
- var copied_connections = [] #stores all connections on ctrl+c
- var node_data = {} #stores json with all nodes in it
- var valueslider = preload("res://scenes/Nodes/valueslider.tscn") #slider scene for use in nodes
- var node_logic = preload("res://scenes/Nodes/node_logic.gd") #load the script logic
- # Called when the node enters the scene tree for the first time.
- func _ready() -> void:
- snapping_enabled = false
- show_grid = false
- zoom = 0.9
- #parse json
- var file = FileAccess.open("res://scenes/main/process_help.json", FileAccess.READ)
- if file:
- var result = JSON.parse_string(file.get_as_text())
- if typeof(result) == TYPE_DICTIONARY:
- node_data = result
- else:
- push_error("Invalid JSON")
- func init(main_node: Node, graphedit: GraphEdit, openhelp: Callable, multipleconnections: Window) -> void:
- control_script = main_node
- graph_edit = graphedit
- open_help = openhelp
- multiple_connections = multipleconnections
- func _make_node(command: String, skip_undo_redo := false) -> GraphNode:
- if node_data.has(command):
- var node_info = node_data[command]
-
- if node_info.get("category", "") == "utility":
- #Find utility node with matching name and create a version of it in the graph edit
- #and position it close to the origin right click to open the menu
- var effect: GraphNode = Nodes.get_node(NodePath(command)).duplicate()
- effect.name = command
- add_child(effect, true)
- if command == "outputfile":
- effect.init() #initialise ui from user prefs
- effect.connect("open_help", open_help)
- effect.set_position_offset((control_script.effect_position + graph_edit.scroll_offset) / graph_edit.zoom) #set node to current mouse position in graph edit
- _register_inputs_in_node(effect) #link sliders for changes tracking
- _register_node_movement() #link nodes for tracking position changes for changes tracking
- control_script.changesmade = true
- if not skip_undo_redo:
- # Remove node with UndoRedo
- control_script.undo_redo.create_action("Add Node")
- control_script.undo_redo.add_undo_method(Callable(graph_edit, "remove_child").bind(effect))
- control_script.undo_redo.add_undo_method(Callable(effect, "queue_free"))
- control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
- control_script.undo_redo.commit_action()
-
- return effect
- else: #auto generate node from json
- #get the title to display at the top of the node
- var title
- if node_info.get("category", "") == "pvoc":
- title = "%s: %s" % [node_info.get("category", "").to_upper(), node_info.get("title", "")]
- else:
- title = "%s: %s" % [node_info.get("subcategory", "").to_pascal_case(), node_info.get("title", "")]
- var shortdescription = node_info.get("short_description", "") #for tooltip
-
- #get node properties
- var stereo = node_info.get("stereo", false)
- var inputs = JSON.parse_string(node_info.get("inputtype", ""))
- var outputs = JSON.parse_string(node_info.get("outputtype", ""))
- var portcount = max(inputs.size(), outputs.size())
- var parameters = node_info.get("parameters", {})
-
- var graphnode = GraphNode.new()
- for i in range(portcount):
- #add a number of control nodes equal to whatever is higher input or output ports
- var control = Control.new()
- graphnode.add_child(control)
-
- #check if input or output is enabled
- var enable_input = i < inputs.size()
- var enable_output = i < outputs.size()
-
- #get the colour of the port for time or pvoc ins/outs
- var input_colour = Color("#ffffff90")
- var output_colour = Color("#ffffff90")
-
- if enable_input:
- if inputs[i] == 1:
- input_colour = Color("#000000b0")
- if enable_output:
- if outputs[i] == 1:
- output_colour = Color("#000000b0")
-
- #enable and set ports
- if enable_input == true and enable_output == false:
- graphnode.set_slot(i, true, inputs[i], input_colour, false, 0, output_colour)
- elif enable_input == false and enable_output == true:
- graphnode.set_slot(i, false, 0, input_colour, true, outputs[i], output_colour)
- elif enable_input == true and enable_output == true:
- graphnode.set_slot(i, true, inputs[i], input_colour, true, outputs[i], output_colour)
- else:
- pass
- #set meta data for the process
- graphnode.set_meta("command", command)
- graphnode.set_meta("stereo_input", stereo)
- if inputs.size() == 0 and outputs.size() > 0:
- graphnode.set_meta("input", true)
- else:
- graphnode.set_meta("input", false)
-
- #adjust size, position and title of the node
- graphnode.title = title
- graphnode.tooltip_text = shortdescription
- graphnode.size.x = 306
- graphnode.custom_minimum_size.y = 80
- graphnode.set_position_offset((control_script.effect_position + graph_edit.scroll_offset) / graph_edit.zoom)
- graphnode.name = command
-
- if parameters.is_empty():
- var noparams = Label.new()
- noparams.text = "No adjustable parameters"
-
- graphnode.add_child(noparams)
- else:
- for param_key in parameters.keys():
- var param_data = parameters[param_key]
- if param_data.get("uitype", "") == "hslider":
- #instance the slider scene
- var slider = valueslider.instantiate()
-
- #get slider text
- var slider_label = param_data.get("paramname", "")
- var slider_tooltip = param_data.get("paramdescription", "")
-
- #name slider
- slider.name = slider_label.replace(" ", "")
-
- #get slider properties
- var brk = param_data.get("automatable", false)
- var time = param_data.get("time", false)
- var outputduration = param_data.get("outputduration", false)
- var min = param_data.get("min", false)
- var max = param_data.get("max", false)
- var flag = param_data.get("flag", "")
- var minrange = param_data.get("minrange", 0)
- var maxrange = param_data.get("maxrange", 10)
- var step = param_data.get("step", 0.01)
- var value = param_data.get("value", 1)
- var exponential = param_data.get("exponential", false)
-
- #set labels and tooltips
- slider.get_node("SliderLabel").text = slider_label
- if brk == true:
- slider.get_node("SliderLabel").text += "~"
- slider.tooltip_text = slider_tooltip
- slider.get_node("SliderLabel").tooltip_text = slider_tooltip
-
- #set meta data
- var hslider = slider.get_node("HSplitContainer/HSlider")
- hslider.set_meta("brk", brk)
- hslider.set_meta("time", time)
- hslider.set_meta("min", min)
- hslider.set_meta("max", max)
- hslider.set_meta("flag", flag)
-
- #set slider params
- hslider.min_value = minrange
- hslider.max_value = maxrange
- hslider.step = step
- hslider.value = value
- hslider.exp_edit = exponential
-
- #add output duration meta to main if true
- if outputduration:
- graphnode.set_meta("outputduration", value)
-
- #scale automation window
- var automationwindow = slider.get_node("BreakFileMaker")
- if automationwindow.content_scale_factor < control_script.uiscale:
- automationwindow.size = automationwindow.size * control_script.uiscale
- automationwindow.content_scale_factor = control_script.uiscale
-
- graphnode.add_child(slider)
-
- elif param_data.get("uitype", "") == "checkbutton":
- #make a checkbutton
- var checkbutton = CheckButton.new()
-
- #get button text
- var checkbutton_label = param_data.get("paramname", "")
- var checkbutton_tooltip = param_data.get("paramdescription", "")
-
- #name checkbutton
- checkbutton.name = checkbutton_label.replace(" ", "")
-
- #get checkbutton properties
- var flag = param_data.get("flag", "")
-
- checkbutton.text = checkbutton_label
- checkbutton.tooltip_text = checkbutton_tooltip
-
- var checkbutton_pressed = param_data.get("value", "false")
- #get button state
- if str(checkbutton_pressed).to_lower() == "true":
- checkbutton.button_pressed = true
-
- #set checkbutton meta
- checkbutton.set_meta("flag", flag)
-
- graphnode.add_child(checkbutton)
- elif param_data.get("uitype", "") == "optionbutton":
- #make optionbutton and label
- var label = Label.new()
- var optionbutton = OptionButton.new()
- var margin = MarginContainer.new()
-
- #get button text
- var optionbutton_label = param_data.get("paramname", "")
- var optionbutton_tooltip = param_data.get("paramdescription", "")
-
- #name optionbutton
- optionbutton.name = optionbutton_label.replace(" ", "")
-
- #get optionbutton properties
- var options = JSON.parse_string(param_data.get("step", ""))
- var value = param_data.get("value", 1)
- var flag = param_data.get("flag", "")
-
- label.text = optionbutton_label
- optionbutton.tooltip_text = optionbutton_tooltip
-
- #fill option button
- for option in options:
- optionbutton.add_item(str(option))
-
-
- #select the given id
- optionbutton.select(int(value))
-
- #set flag meta
- optionbutton.set_meta("flag", flag)
-
- #add margin size for ertical spacing
- margin.add_theme_constant_override("margin_bottom", 4)
-
- graphnode.add_child(label)
- graphnode.add_child(optionbutton)
- graphnode.add_child(margin)
-
- control_script.changesmade = true
-
-
- graphnode.set_script(node_logic)
-
- add_child(graphnode, true)
- graphnode.connect("open_help", open_help)
- _register_inputs_in_node(graphnode) #link sliders for changes tracking
- _register_node_movement() #link nodes for tracking position changes for changes tracking
-
- if not skip_undo_redo:
- # Remove node with UndoRedo
- control_script.undo_redo.create_action("Add Node")
- control_script.undo_redo.add_undo_method(Callable(graph_edit, "remove_child").bind(graphnode))
- control_script.undo_redo.add_undo_method(Callable(graphnode, "queue_free"))
- control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
- control_script.undo_redo.commit_action()
-
- return graphnode
-
- return null
-
- func _on_connection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
- var to_graph_node = get_node(NodePath(to_node))
- var from_graph_node = get_node(NodePath(from_node))
- # Get the type of the input port using GraphNode's built-in method
- var port_type = to_graph_node.get_input_port_type(to_port)
- # If port type is 1 and already has a connection, reject the request
- if port_type == 1:
- var connections = get_connection_list()
- var existing_connections = 0
- for conn in connections:
- if conn.to_node == to_node and conn.to_port == to_port:
- existing_connections += 1
- if existing_connections >= 1:
- var interface_settings = ConfigHandler.load_interface_settings()
- if interface_settings.disable_pvoc_warning == false:
- multiple_connections.popup_centered()
- return
-
- if from_graph_node.get_meta("command") == "inputfile" and to_graph_node.get_meta("command") == "outputfile":
- return
- # If no conflict, allow the connection
- connect_node(from_node, from_port, to_node, to_port)
- control_script.changesmade = true
- func _on_graph_edit_disconnection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
- disconnect_node(from_node, from_port, to_node, to_port)
- control_script.changesmade = true
- func _on_graph_edit_node_selected(node: Node) -> void:
- selected_nodes[node] = true
- func _on_graph_edit_node_deselected(node: Node) -> void:
- selected_nodes[node] = false
- func _unhandled_key_input(event: InputEvent) -> void:
- if event is InputEventKey and event.pressed and not event.echo:
- if event.keycode == KEY_BACKSPACE:
- _on_graph_edit_delete_nodes_request(PackedStringArray(selected_nodes.keys().filter(func(k): return selected_nodes[k])))
- pass
- func _on_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> void:
- control_script.undo_redo.create_action("Delete Nodes (Undo only)")
-
- for node in selected_nodes.keys():
- if is_instance_valid(node) and selected_nodes[node]:
- if selected_nodes[node]:
- #check if node is the output or the last input node and do nothing
- if node.get_meta("command") == "outputfile":
- pass
- else:
- # Store duplicate and state for undo
- var node_data = node.duplicate()
- var position = node.position_offset
- # Store all connections for undo
- var conns = []
- for con in get_connection_list():
- if con["to_node"] == node.name or con["from_node"] == node.name:
- conns.append(con)
- # Delete
- remove_connections_to_node(node)
- node.queue_free()
- control_script.changesmade = true
- # Register undo restore
- control_script.undo_redo.add_undo_method(Callable(self, "add_child").bind(node_data, true))
- control_script.undo_redo.add_undo_method(Callable(node_data, "set_position_offset").bind(position))
- for con in conns:
- control_script.undo_redo.add_undo_method(Callable(self, "connect_node").bind(
- con["from_node"], con["from_port"],
- con["to_node"], con["to_port"]
- ))
- control_script.undo_redo.add_undo_method(Callable(self, "set_node_selected").bind(node_data, true))
- control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
- control_script.undo_redo.add_undo_method(Callable(self, "_register_inputs_in_node").bind(node_data)) #link sliders for changes tracking
- control_script.undo_redo.add_undo_method(Callable(self, "_register_node_movement")) # link nodes for changes tracking
- # Clear selection
- selected_nodes = {}
- control_script.undo_redo.commit_action()
- func set_node_selected(node: Node, selected: bool) -> void:
- selected_nodes[node] = selected
- #
- func remove_connections_to_node(node):
- for con in get_connection_list():
- if con["to_node"] == node.name or con["from_node"] == node.name:
- disconnect_node(con["from_node"], con["from_port"], con["to_node"], con["to_port"])
- control_script.changesmade = true
-
- #copy and paste nodes with vertical offset on paste
- func copy_selected_nodes():
- copied_nodes_data.clear()
- copied_connections.clear()
- # Store selected nodes and their slider values
- for node in get_children():
- # Check if the node is selected and not an 'inputfile' or 'outputfile'
- if node is GraphNode and selected_nodes.get(node, false):
- if node.get_meta("command") == "outputfile":
- continue # Skip these nodes
- var node_data = {
- "name": node.name,
- "type": node.get_class(),
- "offset": node.position_offset,
- "slider_values": {}
- }
- for child in node.get_children():
- if child is HSlider or child is VSlider:
- node_data["slider_values"][child.name] = child.value
- copied_nodes_data.append(node_data)
- # Store connections between selected nodes
- for conn in get_connection_list():
- var from_ref = get_node_or_null(NodePath(conn["from_node"]))
- var to_ref = get_node_or_null(NodePath(conn["to_node"]))
- var is_from_selected = from_ref != null and selected_nodes.get(from_ref, false)
- var is_to_selected = to_ref != null and selected_nodes.get(to_ref, false)
- # Skip if any of the connected nodes are 'inputfile' or 'outputfile'
- if (from_ref != null and (from_ref.get_meta("command") == "inputfile" or from_ref.get_meta("command") == "outputfile")) or (to_ref != null and (to_ref.get_meta("command") == "inputfile" or to_ref.get_meta("command") == "outputfile")):
- continue
- if is_from_selected and is_to_selected:
- # Store connection as dictionary
- var conn_data = {
- "from_node": conn["from_node"],
- "from_port": conn["from_port"],
- "to_node": conn["to_node"],
- "to_port": conn["to_port"]
- }
- copied_connections.append(conn_data)
- func paste_copied_nodes():
- if copied_nodes_data.is_empty():
- return
- var name_map = {}
- var pasted_nodes = []
- # Step 1: Find topmost and bottommost Y of copied nodes
- var min_y = INF
- var max_y = -INF
- for node_data in copied_nodes_data:
- var y = node_data["offset"].y
- min_y = min(min_y, y)
- max_y = max(max_y, y)
- # Step 2: Decide where to paste the group
- var base_y_offset = max_y + 350 # Pasting below the lowest node
- # Step 3: Paste nodes, preserving vertical layout
- for node_data in copied_nodes_data:
- var original_node = get_node_or_null(NodePath(node_data["name"]))
- if not original_node:
- continue
- var new_node = original_node.duplicate()
- new_node.name = node_data["name"] + "_copy_" + str(randi() % 10000)
- var relative_y = node_data["offset"].y - min_y
- new_node.position_offset = Vector2(
- node_data["offset"].x,
- base_y_offset + relative_y
- )
-
- # Restore sliders
- for child in new_node.get_children():
- if child.name in node_data["slider_values"]:
- child.value = node_data["slider_values"][child.name]
- add_child(new_node, true)
- new_node.connect("open_help", open_help)
- _register_inputs_in_node(new_node) #link sliders for changes tracking
- _register_node_movement() # link nodes for changes tracking
- name_map[node_data["name"]] = new_node.name
- pasted_nodes.append(new_node)
- # Step 4: Reconnect new nodes
- for conn_data in copied_connections:
- var new_from = name_map.get(conn_data["from_node"], null)
- var new_to = name_map.get(conn_data["to_node"], null)
- if new_from and new_to:
- connect_node(new_from, conn_data["from_port"], new_to, conn_data["to_port"])
- # Step 5: Select pasted nodes
- for pasted_node in pasted_nodes:
- set_selected(pasted_node)
- selected_nodes[pasted_node] = true
-
- control_script.changesmade = true
-
- # Remove node with UndoRedo
- control_script.undo_redo.create_action("Paste Nodes")
- for pasted_node in pasted_nodes:
- control_script.undo_redo.add_undo_method(Callable(self, "remove_child").bind(pasted_node))
- control_script.undo_redo.add_undo_method(Callable(pasted_node, "queue_free"))
- control_script.undo_redo.add_undo_method(Callable(self, "remove_connections_to_node").bind(pasted_node))
- control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
- control_script.undo_redo.commit_action()
-
- #functions for tracking changes for save state detection
- func _register_inputs_in_node(node: Node):
- #tracks input to nodes sliders and codeedit to track if patch is saved
- # Track Sliders
- for slider in node.find_children("*", "HSlider", true, false):
- # Create a Callable to the correct method
- var callable = Callable(self, "_on_any_slider_changed")
- # Check if it's already connected, and connect if not
- if not slider.is_connected("value_changed", callable):
- slider.connect("value_changed", callable)
-
- for slider in node.find_children("*", "VBoxContainer", true, false):
- # Also connect to meta_changed if the slider has that signal
- if slider.has_signal("meta_changed"):
- var meta_callable = Callable(self, "_on_any_slider_meta_changed")
- if not slider.is_connected("meta_changed", meta_callable):
- slider.connect("meta_changed", meta_callable)
-
- # Track CodeEdits
- for editor in node.find_children("*", "CodeEdit", true, false):
- var callable = Callable(self, "_on_any_input_changed")
- if not editor.is_connected("text_changed", callable):
- editor.connect("text_changed", callable)
-
- func _on_any_slider_meta_changed():
- control_script.changesmade = true
- print("Meta changed in slider")
-
- func _register_node_movement():
- for graphnode in get_children():
- if graphnode is GraphNode:
- var callable = Callable(self, "_on_graphnode_moved")
- if not graphnode.is_connected("position_offset_changed", callable):
- graphnode.connect("position_offset_changed", callable)
- func _on_graphnode_moved():
- control_script.changesmade = true
-
- func _on_any_slider_changed(value: float) -> void:
- control_script.changesmade = true
-
- func _on_any_input_changed():
- control_script.changesmade = true
- func _track_changes():
- control_script.changesmade = true
- func _on_copy_nodes_request() -> void:
- graph_edit.copy_selected_nodes()
- #get_viewport().set_input_as_handled()
- func _on_paste_nodes_request() -> void:
- control_script.simulate_mouse_click() #hacky fix to stop tooltips getting stuck
- await get_tree().process_frame
- graph_edit.paste_copied_nodes()
|