graph_edit.gd 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. extends GraphEdit
  2. var control_script
  3. var graph_edit
  4. var open_help
  5. var multiple_connections
  6. var selected_nodes = {} #used to track which nodes in the GraphEdit are selected
  7. var copied_nodes_data = [] #stores node data on ctrl+c
  8. var copied_connections = [] #stores all connections on ctrl+c
  9. var node_data = {} #stores json with all nodes in it
  10. var valueslider = preload("res://scenes/Nodes/valueslider.tscn") #slider scene for use in nodes
  11. var node_logic = preload("res://scenes/Nodes/node_logic.gd") #load the script logic
  12. # Called when the node enters the scene tree for the first time.
  13. func _ready() -> void:
  14. snapping_enabled = false
  15. show_grid = false
  16. zoom = 0.9
  17. #parse json
  18. var file = FileAccess.open("res://scenes/main/process_help.json", FileAccess.READ)
  19. if file:
  20. var result = JSON.parse_string(file.get_as_text())
  21. if typeof(result) == TYPE_DICTIONARY:
  22. node_data = result
  23. else:
  24. push_error("Invalid JSON")
  25. func init(main_node: Node, graphedit: GraphEdit, openhelp: Callable, multipleconnections: Window) -> void:
  26. control_script = main_node
  27. graph_edit = graphedit
  28. open_help = openhelp
  29. multiple_connections = multipleconnections
  30. func _make_node(command: String, skip_undo_redo := false) -> GraphNode:
  31. if node_data.has(command):
  32. var node_info = node_data[command]
  33. if node_info.get("category", "") == "utility":
  34. #Find utility node with matching name and create a version of it in the graph edit
  35. #and position it close to the origin right click to open the menu
  36. var effect: GraphNode = Nodes.get_node(NodePath(command)).duplicate()
  37. effect.name = command
  38. add_child(effect, true)
  39. if command == "outputfile":
  40. effect.init() #initialise ui from user prefs
  41. effect.connect("open_help", open_help)
  42. effect.set_position_offset((control_script.effect_position + graph_edit.scroll_offset) / graph_edit.zoom) #set node to current mouse position in graph edit
  43. _register_inputs_in_node(effect) #link sliders for changes tracking
  44. _register_node_movement() #link nodes for tracking position changes for changes tracking
  45. control_script.changesmade = true
  46. if not skip_undo_redo:
  47. # Remove node with UndoRedo
  48. control_script.undo_redo.create_action("Add Node")
  49. control_script.undo_redo.add_undo_method(Callable(graph_edit, "remove_child").bind(effect))
  50. control_script.undo_redo.add_undo_method(Callable(effect, "queue_free"))
  51. control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
  52. control_script.undo_redo.commit_action()
  53. return effect
  54. else: #auto generate node from json
  55. #get the title to display at the top of the node
  56. var title
  57. if node_info.get("category", "") == "pvoc":
  58. title = "%s: %s" % [node_info.get("category", "").to_upper(), node_info.get("title", "")]
  59. else:
  60. title = "%s: %s" % [node_info.get("subcategory", "").to_pascal_case(), node_info.get("title", "")]
  61. var shortdescription = node_info.get("short_description", "") #for tooltip
  62. #get node properties
  63. var stereo = node_info.get("stereo", false)
  64. var inputs = JSON.parse_string(node_info.get("inputtype", ""))
  65. var outputs = JSON.parse_string(node_info.get("outputtype", ""))
  66. var portcount = max(inputs.size(), outputs.size())
  67. var parameters = node_info.get("parameters", {})
  68. var graphnode = GraphNode.new()
  69. for i in range(portcount):
  70. #add a number of control nodes equal to whatever is higher input or output ports
  71. var control = Control.new()
  72. graphnode.add_child(control)
  73. #check if input or output is enabled
  74. var enable_input = i < inputs.size()
  75. var enable_output = i < outputs.size()
  76. #get the colour of the port for time or pvoc ins/outs
  77. var input_colour = Color("#ffffff90")
  78. var output_colour = Color("#ffffff90")
  79. if enable_input:
  80. if inputs[i] == 1:
  81. input_colour = Color("#000000b0")
  82. if enable_output:
  83. if outputs[i] == 1:
  84. output_colour = Color("#000000b0")
  85. #enable and set ports
  86. if enable_input == true and enable_output == false:
  87. graphnode.set_slot(i, true, inputs[i], input_colour, false, 0, output_colour)
  88. elif enable_input == false and enable_output == true:
  89. graphnode.set_slot(i, false, 0, input_colour, true, outputs[i], output_colour)
  90. elif enable_input == true and enable_output == true:
  91. graphnode.set_slot(i, true, inputs[i], input_colour, true, outputs[i], output_colour)
  92. else:
  93. pass
  94. #set meta data for the process
  95. graphnode.set_meta("command", command)
  96. graphnode.set_meta("stereo_input", stereo)
  97. if inputs.size() == 0 and outputs.size() > 0:
  98. graphnode.set_meta("input", true)
  99. else:
  100. graphnode.set_meta("input", false)
  101. #adjust size, position and title of the node
  102. graphnode.title = title
  103. graphnode.tooltip_text = shortdescription
  104. graphnode.size.x = 306
  105. graphnode.custom_minimum_size.y = 80
  106. graphnode.set_position_offset((control_script.effect_position + graph_edit.scroll_offset) / graph_edit.zoom)
  107. graphnode.name = command
  108. if parameters.is_empty():
  109. var noparams = Label.new()
  110. noparams.text = "No adjustable parameters"
  111. graphnode.add_child(noparams)
  112. else:
  113. for param_key in parameters.keys():
  114. var param_data = parameters[param_key]
  115. if param_data.get("uitype", "") == "hslider":
  116. #instance the slider scene
  117. var slider = valueslider.instantiate()
  118. #get slider text
  119. var slider_label = param_data.get("paramname", "")
  120. var slider_tooltip = param_data.get("paramdescription", "")
  121. #name slider
  122. slider.name = slider_label.replace(" ", "")
  123. #get slider properties
  124. var brk = param_data.get("automatable", false)
  125. var time = param_data.get("time", false)
  126. var outputduration = param_data.get("outputduration", false)
  127. var min = param_data.get("min", false)
  128. var max = param_data.get("max", false)
  129. var flag = param_data.get("flag", "")
  130. var minrange = param_data.get("minrange", 0)
  131. var maxrange = param_data.get("maxrange", 10)
  132. var step = param_data.get("step", 0.01)
  133. var value = param_data.get("value", 1)
  134. var exponential = param_data.get("exponential", false)
  135. #set labels and tooltips
  136. slider.get_node("SliderLabel").text = slider_label
  137. if brk == true:
  138. slider.get_node("SliderLabel").text += "~"
  139. slider.tooltip_text = slider_tooltip
  140. slider.get_node("SliderLabel").tooltip_text = slider_tooltip
  141. #set meta data
  142. var hslider = slider.get_node("HSplitContainer/HSlider")
  143. hslider.set_meta("brk", brk)
  144. hslider.set_meta("time", time)
  145. hslider.set_meta("min", min)
  146. hslider.set_meta("max", max)
  147. hslider.set_meta("flag", flag)
  148. #set slider params
  149. hslider.min_value = minrange
  150. hslider.max_value = maxrange
  151. hslider.step = step
  152. hslider.value = value
  153. hslider.exp_edit = exponential
  154. #add output duration meta to main if true
  155. if outputduration:
  156. graphnode.set_meta("outputduration", value)
  157. #scale automation window
  158. var automationwindow = slider.get_node("BreakFileMaker")
  159. if automationwindow.content_scale_factor < control_script.uiscale:
  160. automationwindow.size = automationwindow.size * control_script.uiscale
  161. automationwindow.content_scale_factor = control_script.uiscale
  162. graphnode.add_child(slider)
  163. elif param_data.get("uitype", "") == "checkbutton":
  164. #make a checkbutton
  165. var checkbutton = CheckButton.new()
  166. #get button text
  167. var checkbutton_label = param_data.get("paramname", "")
  168. var checkbutton_tooltip = param_data.get("paramdescription", "")
  169. #name checkbutton
  170. checkbutton.name = checkbutton_label.replace(" ", "")
  171. #get checkbutton properties
  172. var flag = param_data.get("flag", "")
  173. checkbutton.text = checkbutton_label
  174. checkbutton.tooltip_text = checkbutton_tooltip
  175. var checkbutton_pressed = param_data.get("value", "false")
  176. #get button state
  177. if str(checkbutton_pressed).to_lower() == "true":
  178. checkbutton.button_pressed = true
  179. #set checkbutton meta
  180. checkbutton.set_meta("flag", flag)
  181. graphnode.add_child(checkbutton)
  182. elif param_data.get("uitype", "") == "optionbutton":
  183. #make optionbutton and label
  184. var label = Label.new()
  185. var optionbutton = OptionButton.new()
  186. var margin = MarginContainer.new()
  187. #get button text
  188. var optionbutton_label = param_data.get("paramname", "")
  189. var optionbutton_tooltip = param_data.get("paramdescription", "")
  190. #name optionbutton
  191. optionbutton.name = optionbutton_label.replace(" ", "")
  192. #get optionbutton properties
  193. var options = JSON.parse_string(param_data.get("step", ""))
  194. var value = param_data.get("value", 1)
  195. var flag = param_data.get("flag", "")
  196. label.text = optionbutton_label
  197. optionbutton.tooltip_text = optionbutton_tooltip
  198. #fill option button
  199. for option in options:
  200. optionbutton.add_item(str(option))
  201. #select the given id
  202. optionbutton.select(int(value))
  203. #set flag meta
  204. optionbutton.set_meta("flag", flag)
  205. #add margin size for ertical spacing
  206. margin.add_theme_constant_override("margin_bottom", 4)
  207. graphnode.add_child(label)
  208. graphnode.add_child(optionbutton)
  209. graphnode.add_child(margin)
  210. control_script.changesmade = true
  211. graphnode.set_script(node_logic)
  212. add_child(graphnode, true)
  213. graphnode.connect("open_help", open_help)
  214. _register_inputs_in_node(graphnode) #link sliders for changes tracking
  215. _register_node_movement() #link nodes for tracking position changes for changes tracking
  216. if not skip_undo_redo:
  217. # Remove node with UndoRedo
  218. control_script.undo_redo.create_action("Add Node")
  219. control_script.undo_redo.add_undo_method(Callable(graph_edit, "remove_child").bind(graphnode))
  220. control_script.undo_redo.add_undo_method(Callable(graphnode, "queue_free"))
  221. control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
  222. control_script.undo_redo.commit_action()
  223. return graphnode
  224. return null
  225. func _on_connection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
  226. var to_graph_node = get_node(NodePath(to_node))
  227. var from_graph_node = get_node(NodePath(from_node))
  228. # Get the type of the input port using GraphNode's built-in method
  229. var port_type = to_graph_node.get_input_port_type(to_port)
  230. # If port type is 1 and already has a connection, reject the request
  231. if port_type == 1:
  232. var connections = get_connection_list()
  233. var existing_connections = 0
  234. for conn in connections:
  235. if conn.to_node == to_node and conn.to_port == to_port:
  236. existing_connections += 1
  237. if existing_connections >= 1:
  238. var interface_settings = ConfigHandler.load_interface_settings()
  239. if interface_settings.disable_pvoc_warning == false:
  240. multiple_connections.popup_centered()
  241. return
  242. if from_graph_node.get_meta("command") == "inputfile" and to_graph_node.get_meta("command") == "outputfile":
  243. return
  244. # If no conflict, allow the connection
  245. connect_node(from_node, from_port, to_node, to_port)
  246. control_script.changesmade = true
  247. func _on_graph_edit_disconnection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
  248. disconnect_node(from_node, from_port, to_node, to_port)
  249. control_script.changesmade = true
  250. func _on_graph_edit_node_selected(node: Node) -> void:
  251. selected_nodes[node] = true
  252. func _on_graph_edit_node_deselected(node: Node) -> void:
  253. selected_nodes[node] = false
  254. func _unhandled_key_input(event: InputEvent) -> void:
  255. if event is InputEventKey and event.pressed and not event.echo:
  256. if event.keycode == KEY_BACKSPACE:
  257. _on_graph_edit_delete_nodes_request(PackedStringArray(selected_nodes.keys().filter(func(k): return selected_nodes[k])))
  258. pass
  259. func _on_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> void:
  260. control_script.undo_redo.create_action("Delete Nodes (Undo only)")
  261. for node in selected_nodes.keys():
  262. if is_instance_valid(node) and selected_nodes[node]:
  263. if selected_nodes[node]:
  264. #check if node is the output or the last input node and do nothing
  265. if node.get_meta("command") == "outputfile":
  266. pass
  267. else:
  268. # Store duplicate and state for undo
  269. var node_data = node.duplicate()
  270. var position = node.position_offset
  271. # Store all connections for undo
  272. var conns = []
  273. for con in get_connection_list():
  274. if con["to_node"] == node.name or con["from_node"] == node.name:
  275. conns.append(con)
  276. # Delete
  277. remove_connections_to_node(node)
  278. node.queue_free()
  279. control_script.changesmade = true
  280. # Register undo restore
  281. control_script.undo_redo.add_undo_method(Callable(self, "add_child").bind(node_data, true))
  282. control_script.undo_redo.add_undo_method(Callable(node_data, "set_position_offset").bind(position))
  283. for con in conns:
  284. control_script.undo_redo.add_undo_method(Callable(self, "connect_node").bind(
  285. con["from_node"], con["from_port"],
  286. con["to_node"], con["to_port"]
  287. ))
  288. control_script.undo_redo.add_undo_method(Callable(self, "set_node_selected").bind(node_data, true))
  289. control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
  290. control_script.undo_redo.add_undo_method(Callable(self, "_register_inputs_in_node").bind(node_data)) #link sliders for changes tracking
  291. control_script.undo_redo.add_undo_method(Callable(self, "_register_node_movement")) # link nodes for changes tracking
  292. # Clear selection
  293. selected_nodes = {}
  294. control_script.undo_redo.commit_action()
  295. func set_node_selected(node: Node, selected: bool) -> void:
  296. selected_nodes[node] = selected
  297. #
  298. func remove_connections_to_node(node):
  299. for con in get_connection_list():
  300. if con["to_node"] == node.name or con["from_node"] == node.name:
  301. disconnect_node(con["from_node"], con["from_port"], con["to_node"], con["to_port"])
  302. control_script.changesmade = true
  303. #copy and paste nodes with vertical offset on paste
  304. func copy_selected_nodes():
  305. copied_nodes_data.clear()
  306. copied_connections.clear()
  307. # Store selected nodes and their slider values
  308. for node in get_children():
  309. # Check if the node is selected and not an 'inputfile' or 'outputfile'
  310. if node is GraphNode and selected_nodes.get(node, false):
  311. if node.get_meta("command") == "outputfile":
  312. continue # Skip these nodes
  313. var node_data = {
  314. "name": node.name,
  315. "type": node.get_class(),
  316. "offset": node.position_offset,
  317. "slider_values": {}
  318. }
  319. for child in node.get_children():
  320. if child is HSlider or child is VSlider:
  321. node_data["slider_values"][child.name] = child.value
  322. copied_nodes_data.append(node_data)
  323. # Store connections between selected nodes
  324. for conn in get_connection_list():
  325. var from_ref = get_node_or_null(NodePath(conn["from_node"]))
  326. var to_ref = get_node_or_null(NodePath(conn["to_node"]))
  327. var is_from_selected = from_ref != null and selected_nodes.get(from_ref, false)
  328. var is_to_selected = to_ref != null and selected_nodes.get(to_ref, false)
  329. # Skip if any of the connected nodes are 'inputfile' or 'outputfile'
  330. 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")):
  331. continue
  332. if is_from_selected and is_to_selected:
  333. # Store connection as dictionary
  334. var conn_data = {
  335. "from_node": conn["from_node"],
  336. "from_port": conn["from_port"],
  337. "to_node": conn["to_node"],
  338. "to_port": conn["to_port"]
  339. }
  340. copied_connections.append(conn_data)
  341. func paste_copied_nodes():
  342. if copied_nodes_data.is_empty():
  343. return
  344. var name_map = {}
  345. var pasted_nodes = []
  346. # Step 1: Find topmost and bottommost Y of copied nodes
  347. var min_y = INF
  348. var max_y = -INF
  349. for node_data in copied_nodes_data:
  350. var y = node_data["offset"].y
  351. min_y = min(min_y, y)
  352. max_y = max(max_y, y)
  353. # Step 2: Decide where to paste the group
  354. var base_y_offset = max_y + 350 # Pasting below the lowest node
  355. # Step 3: Paste nodes, preserving vertical layout
  356. for node_data in copied_nodes_data:
  357. var original_node = get_node_or_null(NodePath(node_data["name"]))
  358. if not original_node:
  359. continue
  360. var new_node = original_node.duplicate()
  361. new_node.name = node_data["name"] + "_copy_" + str(randi() % 10000)
  362. var relative_y = node_data["offset"].y - min_y
  363. new_node.position_offset = Vector2(
  364. node_data["offset"].x,
  365. base_y_offset + relative_y
  366. )
  367. # Restore sliders
  368. for child in new_node.get_children():
  369. if child.name in node_data["slider_values"]:
  370. child.value = node_data["slider_values"][child.name]
  371. add_child(new_node, true)
  372. new_node.connect("open_help", open_help)
  373. _register_inputs_in_node(new_node) #link sliders for changes tracking
  374. _register_node_movement() # link nodes for changes tracking
  375. name_map[node_data["name"]] = new_node.name
  376. pasted_nodes.append(new_node)
  377. # Step 4: Reconnect new nodes
  378. for conn_data in copied_connections:
  379. var new_from = name_map.get(conn_data["from_node"], null)
  380. var new_to = name_map.get(conn_data["to_node"], null)
  381. if new_from and new_to:
  382. connect_node(new_from, conn_data["from_port"], new_to, conn_data["to_port"])
  383. # Step 5: Select pasted nodes
  384. for pasted_node in pasted_nodes:
  385. set_selected(pasted_node)
  386. selected_nodes[pasted_node] = true
  387. control_script.changesmade = true
  388. # Remove node with UndoRedo
  389. control_script.undo_redo.create_action("Paste Nodes")
  390. for pasted_node in pasted_nodes:
  391. control_script.undo_redo.add_undo_method(Callable(self, "remove_child").bind(pasted_node))
  392. control_script.undo_redo.add_undo_method(Callable(pasted_node, "queue_free"))
  393. control_script.undo_redo.add_undo_method(Callable(self, "remove_connections_to_node").bind(pasted_node))
  394. control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
  395. control_script.undo_redo.commit_action()
  396. #functions for tracking changes for save state detection
  397. func _register_inputs_in_node(node: Node):
  398. #tracks input to nodes sliders and codeedit to track if patch is saved
  399. # Track Sliders
  400. for slider in node.find_children("*", "HSlider", true, false):
  401. # Create a Callable to the correct method
  402. var callable = Callable(self, "_on_any_slider_changed")
  403. # Check if it's already connected, and connect if not
  404. if not slider.is_connected("value_changed", callable):
  405. slider.connect("value_changed", callable)
  406. for slider in node.find_children("*", "VBoxContainer", true, false):
  407. # Also connect to meta_changed if the slider has that signal
  408. if slider.has_signal("meta_changed"):
  409. var meta_callable = Callable(self, "_on_any_slider_meta_changed")
  410. if not slider.is_connected("meta_changed", meta_callable):
  411. slider.connect("meta_changed", meta_callable)
  412. # Track CodeEdits
  413. for editor in node.find_children("*", "CodeEdit", true, false):
  414. var callable = Callable(self, "_on_any_input_changed")
  415. if not editor.is_connected("text_changed", callable):
  416. editor.connect("text_changed", callable)
  417. func _on_any_slider_meta_changed():
  418. control_script.changesmade = true
  419. print("Meta changed in slider")
  420. func _register_node_movement():
  421. for graphnode in get_children():
  422. if graphnode is GraphNode:
  423. var callable = Callable(self, "_on_graphnode_moved")
  424. if not graphnode.is_connected("position_offset_changed", callable):
  425. graphnode.connect("position_offset_changed", callable)
  426. func _on_graphnode_moved():
  427. control_script.changesmade = true
  428. func _on_any_slider_changed(value: float) -> void:
  429. control_script.changesmade = true
  430. func _on_any_input_changed():
  431. control_script.changesmade = true
  432. func _track_changes():
  433. control_script.changesmade = true
  434. func _on_copy_nodes_request() -> void:
  435. graph_edit.copy_selected_nodes()
  436. #get_viewport().set_input_as_handled()
  437. func _on_paste_nodes_request() -> void:
  438. control_script.simulate_mouse_click() #hacky fix to stop tooltips getting stuck
  439. await get_tree().process_frame
  440. graph_edit.paste_copied_nodes()