graph_edit.gd 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852
  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 last_pasted_nodes = [] #stores last pasted nodes for undo/redo
  10. var node_data = {} #stores json with all nodes in it
  11. var valueslider = preload("res://scenes/Nodes/valueslider.tscn") #slider scene for use in nodes
  12. var addremoveinlets = preload("res://scenes/Nodes/addremoveinlets.tscn") #add remove inlets scene for use in nodes
  13. var node_logic = preload("res://scenes/Nodes/node_logic.gd") #load the script logic
  14. var selected_cables:= [] #used to track which cables are selected for changing colour and for deletion
  15. var theme_background #used to track if the theme has changed and if so change the cable selection colour
  16. var theme_custom_background
  17. var high_contrast_cables
  18. # Called when the node enters the scene tree for the first time.
  19. func _ready() -> void:
  20. snapping_enabled = false
  21. show_grid = false
  22. zoom = 0.9
  23. #parse json
  24. var file = FileAccess.open("res://scenes/main/process_help.json", FileAccess.READ)
  25. if file:
  26. var result = JSON.parse_string(file.get_as_text())
  27. if typeof(result) == TYPE_DICTIONARY:
  28. node_data = result
  29. else:
  30. push_error("Invalid JSON")
  31. var interface_settings = ConfigHandler.load_interface_settings()
  32. theme_background = interface_settings.theme
  33. theme_custom_background = interface_settings.theme_custom_colour
  34. high_contrast_cables = interface_settings.high_contrast_selected_cables
  35. set_cable_colour(interface_settings.theme)
  36. func init(main_node: Node, graphedit: GraphEdit, openhelp: Callable, multipleconnections: Window) -> void:
  37. control_script = main_node
  38. graph_edit = graphedit
  39. open_help = openhelp
  40. multiple_connections = multipleconnections
  41. func _make_node(command: String, skip_undo_redo := false) -> GraphNode:
  42. if node_data.has(command):
  43. var node_info = node_data[command]
  44. if node_info.get("category", "") == "utility":
  45. #Find utility node with matching name and create a version of it in the graph edit
  46. #and position it close to the origin right click to open the menu
  47. #var effect: GraphNode = Nodes.get_node(NodePath(command)).duplicate()
  48. var effect = Utilities.nodes[command].instantiate()
  49. effect.name = command
  50. #add node and register it for undo redo
  51. control_script.undo_redo.create_action("Add Node")
  52. control_script.undo_redo.add_do_method(add_child.bind(effect, true))
  53. control_script.undo_redo.add_do_reference(effect)
  54. control_script.undo_redo.add_undo_method(delete_node.bind(effect))
  55. control_script.undo_redo.commit_action()
  56. if command == "outputfile":
  57. effect.init() #initialise ui from user prefs
  58. effect.connect("open_help", open_help)
  59. if effect.has_signal("node_moved"):
  60. effect.node_moved.connect(_auto_link_nodes)
  61. effect.dragged.connect(node_position_changed.bind(effect))
  62. effect.set_position_offset((control_script.effect_position + graph_edit.scroll_offset) / graph_edit.zoom) #set node to current mouse position in graph edit
  63. _register_inputs_in_node(effect) #link sliders for changes tracking
  64. _register_node_movement() #link nodes for tracking position changes for changes tracking
  65. control_script.changesmade = true
  66. return effect
  67. else: #auto generate node from json
  68. #get the title to display at the top of the node
  69. var title
  70. if node_info.get("category", "") == "pvoc":
  71. title = "%s: %s" % [node_info.get("category", "").to_upper(), node_info.get("title", "")]
  72. else:
  73. title = "%s: %s" % [node_info.get("subcategory", "").to_pascal_case(), node_info.get("title", "")]
  74. var shortdescription = node_info.get("short_description", "") #for tooltip
  75. #get node properties
  76. var stereo = node_info.get("stereo", false)
  77. var outputisstereo = node_info.get("outputisstereo", false) #used to identify the few processes that always output in stereo making the thread need to be stereo
  78. var inputs = JSON.parse_string(node_info.get("inputtype", ""))
  79. var outputs = JSON.parse_string(node_info.get("outputtype", ""))
  80. var portcount = max(inputs.size(), outputs.size())
  81. var parameters = node_info.get("parameters", {})
  82. var graphnode = GraphNode.new()
  83. #set meta data for the process
  84. graphnode.set_meta("command", command)
  85. graphnode.set_meta("stereo_input", stereo)
  86. graphnode.set_meta("output_is_stereo", outputisstereo)
  87. if inputs.size() == 0 and outputs.size() > 0:
  88. graphnode.set_meta("input", true)
  89. else:
  90. graphnode.set_meta("input", false)
  91. #adjust size, position and title of the node
  92. graphnode.title = title
  93. graphnode.tooltip_text = shortdescription
  94. graphnode.size.x = 306
  95. graphnode.custom_minimum_size.y = 80
  96. graphnode.set_position_offset((control_script.effect_position + graph_edit.scroll_offset) / graph_edit.zoom)
  97. graphnode.name = command
  98. #add one small control node to the top of the node to aline first inlet to top
  99. var first_inlet = Control.new()
  100. graphnode.add_child(first_inlet)
  101. if parameters.is_empty():
  102. var noparams = Label.new()
  103. noparams.text = "No adjustable parameters"
  104. noparams.custom_minimum_size.x = 270
  105. noparams.custom_minimum_size.y = 57
  106. noparams.vertical_alignment = 1
  107. graphnode.add_child(noparams)
  108. else:
  109. for param_key in parameters.keys():
  110. var param_data = parameters[param_key]
  111. if param_data.get("uitype", "") == "hslider":
  112. #instance the slider scene
  113. var slider = valueslider.instantiate()
  114. slider.undo_redo = control_script.undo_redo
  115. #get slider text
  116. var slider_label = param_data.get("paramname", "")
  117. var slider_tooltip = param_data.get("paramdescription", "")
  118. #name slider
  119. slider.name = slider_label.replace(" ", "")
  120. #get slider properties
  121. var brk = param_data.get("automatable", false)
  122. var time = param_data.get("time", false)
  123. var outputduration = param_data.get("outputduration", false)
  124. var minimum = param_data.get("min", false)
  125. var maximum = param_data.get("max", false)
  126. var flag = param_data.get("flag", "")
  127. var fftwindowsize = param_data.get("fftwindowsize", false)
  128. var fftwindowcount = param_data.get("fftwindowcount", false)
  129. var minrange = param_data.get("minrange", 0)
  130. var maxrange = param_data.get("maxrange", 10)
  131. var step = param_data.get("step", 0.01)
  132. var value = param_data.get("value", 1)
  133. var exponential = param_data.get("exponential", false)
  134. #set labels and tooltips
  135. slider.get_node("SliderLabel").text = slider_label
  136. if brk == true:
  137. slider.get_node("SliderLabel").text += "~"
  138. slider.tooltip_text = slider_tooltip
  139. slider.get_node("SliderLabel").tooltip_text = slider_tooltip
  140. #set meta data
  141. var hslider = slider.get_node("HSplitContainer/HSlider")
  142. hslider.set_meta("brk", brk)
  143. hslider.set_meta("time", time)
  144. hslider.set_meta("min", minimum)
  145. hslider.set_meta("max", maximum)
  146. hslider.set_meta("flag", flag)
  147. hslider.set_meta("default_value", value)
  148. hslider.set_meta("fftwindowsize", fftwindowsize)
  149. hslider.set_meta("fftwindowcount", fftwindowcount)
  150. #set slider params
  151. hslider.min_value = minrange
  152. hslider.max_value = maxrange
  153. hslider.step = step
  154. hslider.value = value
  155. slider.previous_value = value #used for undo redo
  156. hslider.exp_edit = exponential
  157. #add output duration meta to main if true
  158. if outputduration:
  159. graphnode.set_meta("outputduration", value)
  160. #scale automation window
  161. var automationwindow = slider.get_node("BreakFileMaker")
  162. if automationwindow.content_scale_factor < control_script.uiscale:
  163. automationwindow.size = automationwindow.size * control_script.uiscale
  164. automationwindow.content_scale_factor = control_script.uiscale
  165. graphnode.add_child(slider)
  166. elif param_data.get("uitype", "") == "checkbutton":
  167. #make a checkbutton
  168. var checkbutton = CheckButton.new()
  169. #get button text
  170. var checkbutton_label = param_data.get("paramname", "")
  171. var checkbutton_tooltip = param_data.get("paramdescription", "")
  172. #name checkbutton
  173. checkbutton.name = checkbutton_label.replace(" ", "")
  174. #get checkbutton properties
  175. var flag = param_data.get("flag", "")
  176. checkbutton.text = checkbutton_label
  177. checkbutton.tooltip_text = checkbutton_tooltip
  178. var checkbutton_pressed = param_data.get("value", "false")
  179. #get button state
  180. if str(checkbutton_pressed).to_lower() == "true":
  181. checkbutton.button_pressed = true
  182. #set checkbutton meta
  183. checkbutton.set_meta("flag", flag)
  184. graphnode.add_child(checkbutton)
  185. elif param_data.get("uitype", "") == "optionbutton":
  186. #make optionbutton and label
  187. var label = Label.new()
  188. var optionbutton = OptionButton.new()
  189. var margin = MarginContainer.new()
  190. #get button text
  191. var optionbutton_label = param_data.get("paramname", "")
  192. var optionbutton_tooltip = param_data.get("paramdescription", "")
  193. #name optionbutton
  194. optionbutton.name = optionbutton_label.replace(" ", "").to_lower()
  195. #add meta flag if this is a sample rate selector for running thread sample rate checks
  196. if optionbutton.name == "samplerate":
  197. graphnode.set_meta("node_sets_sample_rate", true)
  198. #get optionbutton properties
  199. var options = JSON.parse_string(param_data.get("step", ""))
  200. var value = param_data.get("value", 1)
  201. var flag = param_data.get("flag", "")
  202. label.text = optionbutton_label
  203. optionbutton.tooltip_text = optionbutton_tooltip
  204. #fill option button
  205. for option in options:
  206. optionbutton.add_item(str(option))
  207. #select the given id
  208. optionbutton.select(int(value))
  209. #set flag meta
  210. optionbutton.set_meta("flag", flag)
  211. #add margin size for vertical spacing
  212. margin.add_theme_constant_override("margin_bottom", 4)
  213. graphnode.add_child(label)
  214. graphnode.add_child(optionbutton)
  215. graphnode.add_child(margin)
  216. elif param_data.get("uitype", "") == "addremoveinlets":
  217. var addremove = addremoveinlets.instantiate()
  218. addremove.name = "addremoveinlets"
  219. addremove.undo_redo = control_script.undo_redo #link to main undo redo
  220. #get parameters
  221. var min_inlets = param_data.get("minrange", 0)
  222. var max_inlets = param_data.get("maxrange", 10)
  223. var default_inlets = param_data.get("value", 1)
  224. #set meta
  225. addremove.set_meta("min", min_inlets)
  226. addremove.set_meta("max", max_inlets)
  227. addremove.set_meta("default", default_inlets)
  228. graphnode.add_child(addremove)
  229. control_script.changesmade = true
  230. #add control nodes if number of child nodes is lower than the number of inlets or outlets
  231. for i in range(portcount - graphnode.get_child_count()):
  232. #add a number of control nodes equal to whatever is higher input or output ports
  233. var control = Control.new()
  234. control.custom_minimum_size.y = 57
  235. graphnode.add_child(control)
  236. if graphnode.has_node("addremoveinlets"):
  237. graphnode.move_child(graphnode.get_node("addremoveinlets"), graphnode.get_child_count() - 1)
  238. #add ports
  239. for i in range(portcount):
  240. #check if input or output is enabled
  241. var enable_input = i < inputs.size()
  242. var enable_output = i < outputs.size()
  243. #get the colour of the port for time or pvoc ins/outs
  244. var input_colour = Color("#ffffff90")
  245. var output_colour = Color("#ffffff90")
  246. if enable_input:
  247. if inputs[i] == 1:
  248. input_colour = Color("#000000b0")
  249. if enable_output:
  250. if outputs[i] == 1:
  251. output_colour = Color("#000000b0")
  252. #enable and set ports
  253. if enable_input == true and enable_output == false:
  254. graphnode.set_slot(i, true, inputs[i], input_colour, false, 0, output_colour)
  255. elif enable_input == false and enable_output == true:
  256. graphnode.set_slot(i, false, 0, input_colour, true, outputs[i], output_colour)
  257. elif enable_input == true and enable_output == true:
  258. graphnode.set_slot(i, true, inputs[i], input_colour, true, outputs[i], output_colour)
  259. else:
  260. pass
  261. graphnode.set_script(node_logic)
  262. control_script.undo_redo.create_action("Add Node")
  263. control_script.undo_redo.add_do_method(add_child.bind(graphnode))
  264. control_script.undo_redo.add_do_reference(graphnode)
  265. control_script.undo_redo.add_undo_method(delete_node.bind(graphnode))
  266. control_script.undo_redo.commit_action()
  267. graphnode.undo_redo = control_script.undo_redo
  268. graphnode.connect("open_help", open_help)
  269. graphnode.connect("inlet_removed", Callable(self, "on_inlet_removed"))
  270. graphnode.node_moved.connect(_auto_link_nodes)
  271. graphnode.dragged.connect(node_position_changed.bind(graphnode))
  272. _register_inputs_in_node(graphnode) #link sliders for changes tracking
  273. _register_node_movement() #link nodes for tracking position changes for changes tracking
  274. return graphnode
  275. return null
  276. func _on_connection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
  277. #check if this is trying to connect a node to itself and skip
  278. if from_node == to_node:
  279. return
  280. var to_graph_node = get_node(NodePath(to_node))
  281. var from_graph_node = get_node(NodePath(from_node))
  282. # Get the type of the ports
  283. var to_port_type = to_graph_node.get_input_port_type(to_port)
  284. var from_port_type = from_graph_node.get_output_port_type(from_port)
  285. #skip if the nodes are already connected
  286. if is_node_connected(from_node, from_port, to_node, to_port):
  287. return
  288. #skip if this isnt a valid connection
  289. if to_port_type != from_port_type:
  290. return
  291. # If port type is 1 and already has a connection, reject the request
  292. if to_port_type == 1:
  293. var connections = get_connection_list()
  294. var existing_connections = 0
  295. for conn in connections:
  296. if conn.to_node == to_node and conn.to_port == to_port:
  297. existing_connections += 1
  298. if existing_connections >= 1:
  299. var interface_settings = ConfigHandler.load_interface_settings()
  300. if interface_settings.disable_pvoc_warning == false:
  301. multiple_connections.popup_centered()
  302. return
  303. if from_graph_node.get_meta("command") == "inputfile" and to_graph_node.get_meta("command") == "outputfile":
  304. return
  305. # If no conflict, allow the connection
  306. control_script.undo_redo.create_action("Connect Nodes")
  307. control_script.undo_redo.add_do_method(connect_node.bind(from_node, from_port, to_node, to_port))
  308. control_script.undo_redo.add_undo_method(disconnect_node.bind(from_node, from_port, to_node, to_port))
  309. control_script.undo_redo.commit_action()
  310. control_script.changesmade = true
  311. func _on_graph_edit_disconnection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
  312. control_script.undo_redo.create_action("Disconnect Nodes")
  313. control_script.undo_redo.add_do_method(disconnect_node.bind(from_node, from_port, to_node, to_port))
  314. control_script.undo_redo.add_undo_method(connect_node.bind(from_node, from_port, to_node, to_port))
  315. control_script.undo_redo.commit_action()
  316. control_script.changesmade = true
  317. func _on_graph_edit_node_selected(node: Node) -> void:
  318. selected_nodes[node] = true
  319. func _on_graph_edit_node_deselected(node: Node) -> void:
  320. selected_nodes[node] = false
  321. func _unhandled_key_input(event: InputEvent) -> void:
  322. if event is InputEventKey and event.pressed and not event.echo:
  323. if event.keycode == KEY_BACKSPACE:
  324. _on_graph_edit_delete_nodes_request(PackedStringArray(selected_nodes.keys().filter(func(k): return selected_nodes[k])))
  325. pass
  326. func _on_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> void:
  327. if nodes.size() == 0:
  328. return
  329. control_script.undo_redo.create_action("Delete Nodes")
  330. #Collect node data for undo
  331. for node_name in nodes:
  332. var node: GraphNode = get_node_or_null(NodePath(node_name))
  333. if node and is_instance_valid(node):
  334. # Skip output nodes
  335. if node.get_meta("command") == "outputfile":
  336. continue
  337. #register redo
  338. control_script.undo_redo.add_do_method(delete_node.bind(node))
  339. #register undo
  340. control_script.undo_redo.add_undo_method(restore_node.bind(node))
  341. #store a reference to the removed node
  342. control_script.undo_redo.add_undo_reference(node)
  343. #remove deleted nodes from the selected nodes dictionary
  344. selected_nodes = {}
  345. #get all connections going to the deleted nodes and store them in an array
  346. var connections_to_restore = get_connections_to_nodes(nodes)
  347. #register undo method for restoring those connections
  348. control_script.undo_redo.add_undo_method(restore_connections.bind(connections_to_restore))
  349. control_script.undo_redo.commit_action()
  350. force_hide_tooltips()
  351. func delete_node(node_to_delete: GraphNode) -> void:
  352. remove_connections_to_node(node_to_delete)
  353. #remove child instead of queue free keeps a reference to the node in memory (until undo limit is hit) meaning nodes can be restored easily
  354. remove_child(node_to_delete)
  355. selected_nodes[node_to_delete] = false
  356. control_script.changesmade = true
  357. func restore_node(node_to_restore: GraphNode) -> void:
  358. add_child(node_to_restore)
  359. #relink everything
  360. if not node_to_restore.is_connected("open_help", open_help):
  361. node_to_restore.connect("open_help", open_help)
  362. if not node_to_restore.is_connected("node_moved", _auto_link_nodes):
  363. node_to_restore.node_moved.connect(_auto_link_nodes)
  364. if "undo_redo" in node_to_restore:
  365. node_to_restore.undo_redo = control_script.undo_redo
  366. for child in node_to_restore.get_children():
  367. if "undo_redo" in child:
  368. child.undo_redo = control_script.undo_redo
  369. if not node_to_restore.is_connected("dragged", node_position_changed):
  370. node_to_restore.dragged.connect(node_position_changed.bind(node_to_restore))
  371. set_node_selected(node_to_restore, true)
  372. _track_changes()
  373. _register_inputs_in_node(node_to_restore)
  374. _register_node_movement()
  375. control_script.changesmade = true
  376. func get_connections_to_nodes(nodes: Array) -> Array:
  377. var connections_to_nodes = []
  378. for con in get_connection_list():
  379. if (con["from_node"] in nodes or con["to_node"] in nodes) and !connections_to_nodes.has(con):
  380. connections_to_nodes.append(con)
  381. return connections_to_nodes
  382. func restore_connections(connections_to_restore: Array) -> void:
  383. for con in connections_to_restore:
  384. var from_node = con["from_node"]
  385. var from_port = con["from_port"]
  386. var to_node = con["to_node"]
  387. var to_port = con["to_port"]
  388. if has_node(NodePath(from_node)) and has_node(NodePath(to_node)):
  389. connect_node(from_node, from_port, to_node, to_port)
  390. func set_node_selected(node: Node, selected: bool) -> void:
  391. selected_nodes[node] = selected
  392. #
  393. func remove_connections_to_node(node):
  394. for con in get_connection_list():
  395. if con["to_node"] == node.name or con["from_node"] == node.name:
  396. disconnect_node(con["from_node"], con["from_port"], con["to_node"], con["to_port"])
  397. control_script.changesmade = true
  398. #copy and paste nodes with vertical offset on paste
  399. func copy_selected_nodes():
  400. if selected_nodes.size() == 0:
  401. return
  402. copied_nodes_data.clear()
  403. copied_connections.clear()
  404. var copied_node_names = []
  405. # Store selected nodes
  406. for node in get_children():
  407. # Check if the node is selected and not an 'outputfile'
  408. if node is GraphNode and selected_nodes.get(node, false):
  409. if node.get_meta("command") == "outputfile":
  410. continue # Skip these nodes
  411. #this is throwing errors, i think this is due to it trying to deep copy engine level things it doesn't need such as file dialog elements, seems to work anyway
  412. var copied_node = node.duplicate()
  413. copied_nodes_data.append(copied_node)
  414. copied_node_names.append(node.name)
  415. copied_connections = get_connections_to_nodes(copied_node_names)
  416. func paste_copied_nodes():
  417. if copied_nodes_data.is_empty():
  418. return
  419. var pasted_connections = copied_connections.duplicate(true)
  420. control_script.undo_redo.create_action("Paste Nodes")
  421. var pasted_nodes = []
  422. #Find topmost and bottommost Y of copied nodes and decide where to paste
  423. var min_y = INF
  424. var max_y = -INF
  425. for node in copied_nodes_data:
  426. var y = node.position_offset.y
  427. min_y = min(min_y, y)
  428. max_y = max(max_y, y)
  429. var base_y_offset = max_y + 350 # Pasting below the lowest node
  430. # Step 3: Paste nodes, preserving vertical layout
  431. for node in copied_nodes_data:
  432. #duplicate the copied node again so it can be pasted more than once and add using restore_node function
  433. var pasted_node = node.duplicate()
  434. #give the node a random name
  435. pasted_node.name = node.name + "_copy_" + str(randi() % 10000)
  436. control_script.undo_redo.add_do_method(restore_node.bind(pasted_node))
  437. control_script.undo_redo.add_do_reference(pasted_node)
  438. #adjust the offset of the pasted node to be below the copied node
  439. var relative_y = pasted_node.position_offset.y - min_y
  440. pasted_node.position_offset.y = base_y_offset + relative_y
  441. for con in pasted_connections:
  442. if con["from_node"] == node.name:
  443. con["from_node"] = pasted_node.name
  444. print(pasted_node.name)
  445. if con["to_node"] == node.name:
  446. con["to_node"] = pasted_node.name
  447. control_script.undo_redo.add_undo_method(delete_node.bind(pasted_node))
  448. pasted_nodes.append(pasted_node)
  449. control_script.undo_redo.add_do_method(restore_connections.bind(pasted_connections))
  450. control_script.undo_redo.commit_action()
  451. force_hide_tooltips()
  452. #Select pasted nodes
  453. for pasted_node in pasted_nodes:
  454. selected_nodes.clear()
  455. set_selected(pasted_node)
  456. selected_nodes[pasted_node] = true
  457. control_script.changesmade = true
  458. func force_hide_tooltips():
  459. #very janky fix that makes and removes a popup in one frame to force the engine to hide all visible popups to stop popups getting stuck
  460. #seems to be more reliable that faking a middle mouse click
  461. var popup := Popup.new()
  462. add_child(popup)
  463. popup.size = Vector2(1,1)
  464. popup.transparent_bg = true
  465. popup.borderless = true
  466. popup.unresizable = true
  467. await get_tree().process_frame
  468. popup.popup()
  469. popup.queue_free()
  470. #functions for tracking changes for save state detection
  471. func _register_inputs_in_node(node: Node):
  472. #tracks input to nodes sliders and codeedit to track if patch is saved
  473. # Track Sliders
  474. for slider in node.find_children("*", "HSlider", true, false):
  475. # Create a Callable to the correct method
  476. var callable = Callable(self, "_on_any_slider_changed")
  477. # Check if it's already connected, and connect if not
  478. if not slider.is_connected("value_changed", callable):
  479. slider.connect("value_changed", callable)
  480. for slider in node.find_children("*", "VBoxContainer", true, false):
  481. # Also connect to meta_changed if the slider has that signal
  482. if slider.has_signal("meta_changed"):
  483. var meta_callable = Callable(self, "_on_any_slider_meta_changed")
  484. if not slider.is_connected("meta_changed", meta_callable):
  485. slider.connect("meta_changed", meta_callable)
  486. # Track CodeEdits
  487. for editor in node.find_children("*", "CodeEdit", true, false):
  488. var callable = Callable(self, "_on_any_input_changed")
  489. if not editor.is_connected("text_changed", callable):
  490. editor.connect("text_changed", callable)
  491. func _on_any_slider_meta_changed():
  492. control_script.changesmade = true
  493. print("Meta changed in slider")
  494. func _register_node_movement():
  495. for graphnode in get_children():
  496. if graphnode is GraphNode:
  497. var callable = Callable(self, "_on_graphnode_moved")
  498. if not graphnode.is_connected("position_offset_changed", callable):
  499. graphnode.connect("position_offset_changed", callable)
  500. func _on_graphnode_moved():
  501. control_script.changesmade = true
  502. func _on_any_slider_changed(value: float) -> void:
  503. control_script.changesmade = true
  504. func _on_any_input_changed():
  505. control_script.changesmade = true
  506. func _track_changes():
  507. control_script.changesmade = true
  508. func _on_copy_nodes_request() -> void:
  509. graph_edit.copy_selected_nodes()
  510. #get_viewport().set_input_as_handled()
  511. func _on_paste_nodes_request() -> void:
  512. control_script.simulate_mouse_click() #hacky fix to stop tooltips getting stuck
  513. await get_tree().process_frame
  514. graph_edit.paste_copied_nodes()
  515. func on_inlet_removed(node_name: StringName, port_index: int):
  516. var connections = get_connection_list()
  517. for conn in connections:
  518. if conn.to_node == node_name and conn.to_port == port_index:
  519. disconnect_node(conn.from_node, conn.from_port, conn.to_node, conn.to_port)
  520. func _swap_node(old_node: GraphNode, command: String):
  521. #store the position and name of the node to be replaced
  522. var position = old_node.position_offset
  523. var old_name = old_node.name
  524. #gather all connections in the graph
  525. var connections = get_connection_list()
  526. var related_connections = []
  527. #filter the connections to get just those connected to the node to be replaced
  528. for conn in connections:
  529. if conn.from_node == old_name or conn.to_node == old_name:
  530. related_connections.append(conn)
  531. #delete the old node
  532. _on_graph_edit_delete_nodes_request([old_node.name])
  533. #make the new node and reposition it to the location of the old node
  534. var new_node = _make_node(command)
  535. new_node.position_offset = position
  536. #filter through all the connections to the old node
  537. for conn in related_connections:
  538. var from = conn.from_node
  539. var from_port = conn.from_port
  540. var to = conn.to_node
  541. var to_port = conn.to_port
  542. #where the old node is referenced replace it with the name of the new node
  543. if from == old_name:
  544. from = new_node.name
  545. if to == old_name:
  546. to = new_node.name
  547. #check that the ports being connected to/from on the new node actually exist
  548. if (from == new_node.name and new_node.is_slot_enabled_right(from_port)) or (to == new_node.name and new_node.is_slot_enabled_left(to_port)):
  549. #check the two ports are the same type
  550. if _same_port_type(from, from_port, to, to_port):
  551. _on_connection_request(from, from_port, to, to_port)
  552. func _connect_to_clicked_node(clicked_node: GraphNode, command: String):
  553. var new_node_position = clicked_node.position_offset + Vector2(clicked_node.size.x + 50, 0)
  554. #make the new node and reposition it to right of the node to connect to
  555. var new_node = _make_node(command)
  556. new_node.position_offset = new_node_position
  557. var clicked_node_has_outputs = clicked_node.get_output_port_count() > 0
  558. var new_node_has_inputs = new_node.get_input_port_count() > 0
  559. if clicked_node_has_outputs and new_node_has_inputs:
  560. if _same_port_type(clicked_node.name, 0, new_node.name, 0):
  561. _on_connection_request(clicked_node.name, 0, new_node.name, 0)
  562. func _on_gui_input(event: InputEvent) -> void:
  563. #check if this is an unhandled mouse click
  564. if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
  565. #get dictionary of a cable if nearby
  566. var closest_connection = get_closest_connection_at_point(get_local_mouse_position())
  567. #check if there is anything in that dictionary
  568. if closest_connection.size() > 0:
  569. #check if the background has changed colour for highlighted cable colour
  570. var interface_settings = ConfigHandler.load_interface_settings()
  571. if interface_settings.theme != theme_background or interface_settings.theme_custom_colour != theme_custom_background or interface_settings.high_contrast_selected_cables != high_contrast_cables:
  572. #if bg has changed colour since last cable highlight reset to new bg and change cable colour
  573. theme_background = interface_settings.theme
  574. theme_custom_background = interface_settings.theme_custom_colour
  575. high_contrast_cables = interface_settings.high_contrast_selected_cables
  576. set_cable_colour(interface_settings.theme)
  577. #get details of nearby cable
  578. var from_node = closest_connection.from_node
  579. var from_port = closest_connection.from_port
  580. var to_node = closest_connection.to_node
  581. var to_port = closest_connection.to_port
  582. #check if user was holding shift and if so allow for multiple cables to be selected
  583. if event.shift_pressed:
  584. selected_cables.append(closest_connection)
  585. set_connection_activity(from_node, from_port, to_node, to_port, 1)
  586. #if user double clicked unselect all cables and delete the nearest cable
  587. elif event.double_click:
  588. for conn in selected_cables:
  589. set_connection_activity(conn.from_node, conn.from_port, conn.to_node, conn.to_port, 0)
  590. _on_graph_edit_disconnection_request(from_node, from_port, to_node, to_port)
  591. #else just a single click, unselect any previously selected cables and select just the nearest
  592. else:
  593. for conn in selected_cables:
  594. set_connection_activity(conn.from_node, conn.from_port, conn.to_node, conn.to_port, 0)
  595. selected_cables = []
  596. selected_cables.append(closest_connection)
  597. set_connection_activity(from_node, from_port, to_node, to_port, 1)
  598. #user didnt click on a cable unselect all cables
  599. else:
  600. for conn in selected_cables:
  601. set_connection_activity(conn.from_node, conn.from_port, conn.to_node, conn.to_port, 0)
  602. selected_cables = []
  603. #if this is an unhandled delete check if there are any cables selected and deleted them
  604. if event is InputEventKey and event.pressed:
  605. if (event.keycode == KEY_BACKSPACE or event.keycode == KEY_DELETE) and selected_cables.size() > 0:
  606. for conn in selected_cables:
  607. _on_graph_edit_disconnection_request(conn.from_node, conn.from_port, conn.to_node, conn.to_port)
  608. selected_cables = []
  609. func set_cable_colour(theme_colour: int):
  610. var background_colour
  611. var cable_colour
  612. var interface_settings = ConfigHandler.load_interface_settings()
  613. match theme_colour:
  614. 0:
  615. background_colour = Color("#2f4f4e")
  616. 1:
  617. background_colour = Color("#000807")
  618. 2:
  619. background_colour = Color("#98d4d2")
  620. 3:
  621. background_colour = Color(interface_settings.theme_custom_colour)
  622. if interface_settings.high_contrast_selected_cables:
  623. #180 colour shift from background and up sv
  624. cable_colour = Color.from_hsv(fposmod(background_colour.h + 0.5, 1.0), clamp(background_colour.s + 0.2, 0, 1), clamp(background_colour.v + 0.2, 0, 1))
  625. var luminance = 0.299 * background_colour.r + 0.587 * background_colour.g + 0.114 * background_colour.b
  626. if luminance > 0.5 and cable_colour.get_luminance() > 0.5:
  627. cable_colour = cable_colour.darkened(0.4)
  628. elif luminance <= 0.5 and cable_colour.get_luminance() < 0.5:
  629. #increase s and v again
  630. cable_colour = Color.from_hsv(cable_colour.h, clamp(cable_colour.s + 0.2, 0, 0.8), clamp(cable_colour.v + 0.2, 0, 0.8))
  631. else:
  632. #keep hue but up saturation and variance
  633. cable_colour = Color.from_hsv(background_colour.h, clamp(background_colour.s + 0.2, 0, 1), clamp(background_colour.v + 0.2, 0, 1))
  634. #overide theme for cable highlight
  635. add_theme_color_override("activity", cable_colour)
  636. func _auto_link_nodes(node: GraphNode, rect: Rect2):
  637. #get all cables that overlap with the node being moved
  638. var potential_connections = get_connections_intersecting_with_rect(rect)
  639. #if there are anyoverlapping and shift is being held down then
  640. if potential_connections.size() > 0 and Input.is_action_pressed("auto_link_nodes"):
  641. #sort through all the cables that overlap
  642. for conn in potential_connections:
  643. #get their info
  644. var new_node_name = node.name
  645. var new_node_has_inputs = node.get_input_port_count() > 0
  646. var new_node_has_outputs = node.get_output_port_count() > 0
  647. var from = conn.from_node
  648. var from_port = conn.from_port
  649. var to = conn.to_node
  650. var to_port = conn.to_port
  651. if new_node_has_inputs and new_node_has_outputs:
  652. #connect in the middle of the two nodes if they are the same port type
  653. var from_matches = _same_port_type(from, from_port, new_node_name, 0)
  654. var to_matches = _same_port_type(new_node_name, 0, to, to_port)
  655. #skip deleting cables if they are the same as the node being dragged or the ports don't match
  656. if from_matches and to_matches and from != new_node_name and to != new_node_name:
  657. _on_graph_edit_disconnection_request(from, from_port, to, to_port)
  658. if from_matches:
  659. _on_connection_request(from, from_port, new_node_name, 0)
  660. if to_matches:
  661. _on_connection_request(new_node_name, 0, to, to_port)
  662. elif new_node_has_inputs:
  663. #only has inputs check if the ports match and if they do connect but leave original connection in place
  664. if _same_port_type(from, from_port, new_node_name, 0):
  665. _on_connection_request(from, from_port, new_node_name, 0)
  666. elif new_node_has_outputs:
  667. #only has outputs check if the ports match and if they do connect but leave original connection in place
  668. if _same_port_type(new_node_name, 0, to, to_port):
  669. _on_connection_request(new_node_name, 0, to, to_port)
  670. # function for checking if an inlet and an outlet are the same type
  671. func _same_port_type(from: String, from_port: int, to: String, to_port: int) -> bool:
  672. var from_node = get_node_or_null(NodePath(from))
  673. var to_node = get_node_or_null(NodePath(to))
  674. #safety incase one somehow no longer exists
  675. if from_node != null and to_node != null:
  676. #check if the port types are the same e.g. both time or both pvoc
  677. if from_node.get_output_port_type(from_port) == to_node.get_input_port_type(to_port):
  678. return true
  679. else:
  680. return false
  681. else:
  682. return false
  683. func node_position_changed(from: Vector2, to: Vector2, node: Node) -> void:
  684. control_script.undo_redo.create_action("Move Node")
  685. control_script.undo_redo.add_do_method(move_node.bind(node, to))
  686. control_script.undo_redo.add_undo_method(move_node.bind(node, from))
  687. control_script.undo_redo.commit_action()
  688. func move_node(node: Node, to: Vector2) -> void:
  689. node.position_offset = to