graph_edit.gd 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  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. #get slider properties
  122. var brk = param_data.get("automatable", false)
  123. var time = param_data.get("time", false)
  124. var outputduration = param_data.get("outputduration", false)
  125. var min = param_data.get("min", false)
  126. var max = param_data.get("max", false)
  127. var flag = param_data.get("flag", "")
  128. var minrange = param_data.get("minrange", 0)
  129. var maxrange = param_data.get("maxrange", 10)
  130. var step = param_data.get("step", 0.01)
  131. var value = param_data.get("value", 1)
  132. var exponential = param_data.get("exponential", false)
  133. #set labels and tooltips
  134. slider.get_node("SliderLabel").text = slider_label
  135. if brk == true:
  136. slider.get_node("SliderLabel").text += "~"
  137. slider.tooltip_text = slider_tooltip
  138. slider.get_node("SliderLabel").tooltip_text = slider_tooltip
  139. #set meta data
  140. var hslider = slider.get_node("HSplitContainer/HSlider")
  141. hslider.set_meta("brk", brk)
  142. hslider.set_meta("time", time)
  143. hslider.set_meta("min", min)
  144. hslider.set_meta("max", max)
  145. hslider.set_meta("flag", flag)
  146. #set slider params
  147. hslider.min_value = minrange
  148. hslider.max_value = maxrange
  149. hslider.step = step
  150. hslider.value = value
  151. hslider.exp_edit = exponential
  152. #add output duration meta to main if true
  153. if outputduration:
  154. graphnode.set_meta("outputduration", value)
  155. #scale automation window
  156. var automationwindow = slider.get_node("BreakFileMaker")
  157. if automationwindow.content_scale_factor < control_script.uiscale:
  158. automationwindow.size = automationwindow.size * control_script.uiscale
  159. automationwindow.content_scale_factor = control_script.uiscale
  160. graphnode.add_child(slider)
  161. elif param_data.get("uitype", "") == "checkbutton":
  162. #make a checkbutton
  163. var checkbutton = CheckButton.new()
  164. #get button text
  165. var checkbutton_label = param_data.get("paramname", "")
  166. var checkbutton_tooltip = param_data.get("paramdescription", "")
  167. #get checkbutton properties
  168. var flag = param_data.get("flag", "")
  169. checkbutton.text = checkbutton_label
  170. checkbutton.tooltip_text = checkbutton_tooltip
  171. var checkbutton_pressed = param_data.get("value", "false")
  172. #get button state
  173. if str(checkbutton_pressed).to_lower() == "true":
  174. checkbutton.button_pressed = true
  175. #set checkbutton meta
  176. checkbutton.set_meta("flag", flag)
  177. graphnode.add_child(checkbutton)
  178. elif param_data.get("uitype", "") == "optionbutton":
  179. #make optionbutton and label
  180. var label = Label.new()
  181. var optionbutton = OptionButton.new()
  182. var margin = MarginContainer.new()
  183. #get button text
  184. var optionbutton_label = param_data.get("paramname", "")
  185. var optionbutton_tooltip = param_data.get("paramdescription", "")
  186. #get optionbutton properties
  187. var options = JSON.parse_string(param_data.get("step", ""))
  188. var value = param_data.get("value", 1)
  189. var flag = param_data.get("flag", "")
  190. label.text = optionbutton_label
  191. optionbutton.tooltip_text = optionbutton_tooltip
  192. #fill option button
  193. for option in options:
  194. optionbutton.add_item(str(option))
  195. #select the given id
  196. optionbutton.select(int(value))
  197. #set flag meta
  198. optionbutton.set_meta("flag", flag)
  199. #add margin size for ertical spacing
  200. margin.add_theme_constant_override("margin_bottom", 4)
  201. graphnode.add_child(label)
  202. graphnode.add_child(optionbutton)
  203. graphnode.add_child(margin)
  204. graphnode.set_script(node_logic)
  205. add_child(graphnode, true)
  206. graphnode.connect("open_help", open_help)
  207. _register_inputs_in_node(graphnode) #link sliders for changes tracking
  208. _register_node_movement() #link nodes for tracking position changes for changes tracking
  209. if not skip_undo_redo:
  210. # Remove node with UndoRedo
  211. control_script.undo_redo.create_action("Add Node")
  212. control_script.undo_redo.add_undo_method(Callable(graph_edit, "remove_child").bind(graphnode))
  213. control_script.undo_redo.add_undo_method(Callable(graphnode, "queue_free"))
  214. control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
  215. control_script.undo_redo.commit_action()
  216. return graphnode
  217. return null
  218. func _on_connection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
  219. var to_graph_node = get_node(NodePath(to_node))
  220. # Get the type of the input port using GraphNode's built-in method
  221. var port_type = to_graph_node.get_input_port_type(to_port)
  222. # If port type is 1 and already has a connection, reject the request
  223. if port_type == 1:
  224. var connections = get_connection_list()
  225. var existing_connections = 0
  226. for conn in connections:
  227. if conn.to_node == to_node and conn.to_port == to_port:
  228. existing_connections += 1
  229. if existing_connections >= 1:
  230. var interface_settings = ConfigHandler.load_interface_settings()
  231. if interface_settings.disable_pvoc_warning == false:
  232. multiple_connections.popup_centered()
  233. return
  234. # If no conflict, allow the connection
  235. connect_node(from_node, from_port, to_node, to_port)
  236. control_script.changesmade = true
  237. func _on_graph_edit_disconnection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
  238. disconnect_node(from_node, from_port, to_node, to_port)
  239. control_script.changesmade = true
  240. func _on_graph_edit_node_selected(node: Node) -> void:
  241. selected_nodes[node] = true
  242. func _on_graph_edit_node_deselected(node: Node) -> void:
  243. selected_nodes[node] = false
  244. func _unhandled_key_input(event: InputEvent) -> void:
  245. if event is InputEventKey and event.pressed and not event.echo:
  246. if event.keycode == KEY_BACKSPACE:
  247. _on_graph_edit_delete_nodes_request(PackedStringArray(selected_nodes.keys().filter(func(k): return selected_nodes[k])))
  248. pass
  249. func _on_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> void:
  250. control_script.undo_redo.create_action("Delete Nodes (Undo only)")
  251. #get the number of inputs in the patch
  252. #var number_of_inputs = 0
  253. #for allnodes in get_children():
  254. #if allnodes.get_meta("command") == "inputfile":
  255. #number_of_inputs += 1
  256. for node in selected_nodes.keys():
  257. if selected_nodes[node]:
  258. #check if node is the output or the last input node and do nothing
  259. if node.get_meta("command") == "outputfile":
  260. pass
  261. else:
  262. # Store duplicate and state for undo
  263. var node_data = node.duplicate()
  264. var position = node.position_offset
  265. # Store all connections for undo
  266. var conns = []
  267. for con in get_connection_list():
  268. if con["to_node"] == node.name or con["from_node"] == node.name:
  269. conns.append(con)
  270. # Delete
  271. remove_connections_to_node(node)
  272. node.queue_free()
  273. control_script.changesmade = true
  274. # Register undo restore
  275. control_script.undo_redo.add_undo_method(Callable(self, "add_child").bind(node_data, true))
  276. control_script.undo_redo.add_undo_method(Callable(node_data, "set_position_offset").bind(position))
  277. for con in conns:
  278. control_script.undo_redo.add_undo_method(Callable(self, "connect_node").bind(
  279. con["from_node"], con["from_port"],
  280. con["to_node"], con["to_port"]
  281. ))
  282. control_script.undo_redo.add_undo_method(Callable(self, "set_node_selected").bind(node_data, true))
  283. control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
  284. control_script.undo_redo.add_undo_method(Callable(self, "_register_inputs_in_node").bind(node_data)) #link sliders for changes tracking
  285. control_script.undo_redo.add_undo_method(Callable(self, "_register_node_movement")) # link nodes for changes tracking
  286. # Clear selection
  287. selected_nodes = {}
  288. control_script.undo_redo.commit_action()
  289. func set_node_selected(node: Node, selected: bool) -> void:
  290. selected_nodes[node] = selected
  291. #
  292. func remove_connections_to_node(node):
  293. for con in get_connection_list():
  294. if con["to_node"] == node.name or con["from_node"] == node.name:
  295. disconnect_node(con["from_node"], con["from_port"], con["to_node"], con["to_port"])
  296. control_script.changesmade = true
  297. #copy and paste nodes with vertical offset on paste
  298. func copy_selected_nodes():
  299. copied_nodes_data.clear()
  300. copied_connections.clear()
  301. # Store selected nodes and their slider values
  302. for node in get_children():
  303. # Check if the node is selected and not an 'inputfile' or 'outputfile'
  304. if node is GraphNode and selected_nodes.get(node, false):
  305. if node.get_meta("command") == "outputfile":
  306. continue # Skip these nodes
  307. var node_data = {
  308. "name": node.name,
  309. "type": node.get_class(),
  310. "offset": node.position_offset,
  311. "slider_values": {}
  312. }
  313. for child in node.get_children():
  314. if child is HSlider or child is VSlider:
  315. node_data["slider_values"][child.name] = child.value
  316. copied_nodes_data.append(node_data)
  317. # Store connections between selected nodes
  318. for conn in get_connection_list():
  319. var from_ref = get_node_or_null(NodePath(conn["from_node"]))
  320. var to_ref = get_node_or_null(NodePath(conn["to_node"]))
  321. var is_from_selected = from_ref != null and selected_nodes.get(from_ref, false)
  322. var is_to_selected = to_ref != null and selected_nodes.get(to_ref, false)
  323. # Skip if any of the connected nodes are 'inputfile' or 'outputfile'
  324. 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")):
  325. continue
  326. if is_from_selected and is_to_selected:
  327. # Store connection as dictionary
  328. var conn_data = {
  329. "from_node": conn["from_node"],
  330. "from_port": conn["from_port"],
  331. "to_node": conn["to_node"],
  332. "to_port": conn["to_port"]
  333. }
  334. copied_connections.append(conn_data)
  335. func paste_copied_nodes():
  336. if copied_nodes_data.is_empty():
  337. return
  338. var name_map = {}
  339. var pasted_nodes = []
  340. # Step 1: Find topmost and bottommost Y of copied nodes
  341. var min_y = INF
  342. var max_y = -INF
  343. for node_data in copied_nodes_data:
  344. var y = node_data["offset"].y
  345. min_y = min(min_y, y)
  346. max_y = max(max_y, y)
  347. # Step 2: Decide where to paste the group
  348. var base_y_offset = max_y + 350 # Pasting below the lowest node
  349. # Step 3: Paste nodes, preserving vertical layout
  350. for node_data in copied_nodes_data:
  351. var original_node = get_node_or_null(NodePath(node_data["name"]))
  352. if not original_node:
  353. continue
  354. var new_node = original_node.duplicate()
  355. new_node.name = node_data["name"] + "_copy_" + str(randi() % 10000)
  356. var relative_y = node_data["offset"].y - min_y
  357. new_node.position_offset = Vector2(
  358. node_data["offset"].x,
  359. base_y_offset + relative_y
  360. )
  361. # Restore sliders
  362. for child in new_node.get_children():
  363. if child.name in node_data["slider_values"]:
  364. child.value = node_data["slider_values"][child.name]
  365. add_child(new_node, true)
  366. new_node.connect("open_help", open_help)
  367. _register_inputs_in_node(new_node) #link sliders for changes tracking
  368. _register_node_movement() # link nodes for changes tracking
  369. name_map[node_data["name"]] = new_node.name
  370. pasted_nodes.append(new_node)
  371. # Step 4: Reconnect new nodes
  372. for conn_data in copied_connections:
  373. var new_from = name_map.get(conn_data["from_node"], null)
  374. var new_to = name_map.get(conn_data["to_node"], null)
  375. if new_from and new_to:
  376. connect_node(new_from, conn_data["from_port"], new_to, conn_data["to_port"])
  377. # Step 5: Select pasted nodes
  378. for pasted_node in pasted_nodes:
  379. set_selected(pasted_node)
  380. selected_nodes[pasted_node] = true
  381. control_script.changesmade = true
  382. # Remove node with UndoRedo
  383. control_script.undo_redo.create_action("Paste Nodes")
  384. for pasted_node in pasted_nodes:
  385. control_script.undo_redo.add_undo_method(Callable(self, "remove_child").bind(pasted_node))
  386. control_script.undo_redo.add_undo_method(Callable(pasted_node, "queue_free"))
  387. control_script.undo_redo.add_undo_method(Callable(self, "remove_connections_to_node").bind(pasted_node))
  388. control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
  389. control_script.undo_redo.commit_action()
  390. #functions for tracking changes for save state detection
  391. func _register_inputs_in_node(node: Node):
  392. #tracks input to nodes sliders and codeedit to track if patch is saved
  393. # Track Sliders
  394. for slider in node.find_children("*", "HSlider", true, false):
  395. # Create a Callable to the correct method
  396. var callable = Callable(self, "_on_any_slider_changed")
  397. # Check if it's already connected, and connect if not
  398. if not slider.is_connected("value_changed", callable):
  399. slider.connect("value_changed", callable)
  400. for slider in node.find_children("*", "VBoxContainer", true, false):
  401. # Also connect to meta_changed if the slider has that signal
  402. if slider.has_signal("meta_changed"):
  403. var meta_callable = Callable(self, "_on_any_slider_meta_changed")
  404. if not slider.is_connected("meta_changed", meta_callable):
  405. slider.connect("meta_changed", meta_callable)
  406. # Track CodeEdits
  407. for editor in node.find_children("*", "CodeEdit", true, false):
  408. var callable = Callable(self, "_on_any_input_changed")
  409. if not editor.is_connected("text_changed", callable):
  410. editor.connect("text_changed", callable)
  411. func _on_any_slider_meta_changed():
  412. control_script.changesmade = true
  413. print("Meta changed in slider")
  414. func _register_node_movement():
  415. for graphnode in get_children():
  416. if graphnode is GraphNode:
  417. var callable = Callable(self, "_on_graphnode_moved")
  418. if not graphnode.is_connected("position_offset_changed", callable):
  419. graphnode.connect("position_offset_changed", callable)
  420. func _on_graphnode_moved():
  421. control_script.changesmade = true
  422. func _on_any_slider_changed(value: float) -> void:
  423. control_script.changesmade = true
  424. func _on_any_input_changed():
  425. control_script.changesmade = true
  426. func _track_changes():
  427. control_script.changesmade = true
  428. func _on_copy_nodes_request() -> void:
  429. graph_edit.copy_selected_nodes()
  430. #get_viewport().set_input_as_handled()
  431. func _on_paste_nodes_request() -> void:
  432. control_script.simulate_mouse_click() #hacky fix to stop tooltips getting stuck
  433. await get_tree().process_frame
  434. graph_edit.paste_copied_nodes()