graph_edit.gd 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847
  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. control_script.undo_redo.create_action("Delete Nodes")
  328. #Collect node data for undo
  329. for node_name in nodes:
  330. var node: GraphNode = get_node_or_null(NodePath(node_name))
  331. if node and is_instance_valid(node):
  332. # Skip output nodes
  333. if node.get_meta("command") == "outputfile":
  334. continue
  335. #register redo
  336. control_script.undo_redo.add_do_method(delete_node.bind(node))
  337. #register undo
  338. control_script.undo_redo.add_undo_method(restore_node.bind(node))
  339. #store a reference to the removed node
  340. control_script.undo_redo.add_undo_reference(node)
  341. #remove deleted nodes from the selected nodes dictionary
  342. selected_nodes = {}
  343. #get all connections going to the deleted nodes and store them in an array
  344. var connections_to_restore = get_connections_to_nodes(nodes)
  345. #register undo method for restoring those connections
  346. control_script.undo_redo.add_undo_method(restore_connections.bind(connections_to_restore))
  347. control_script.undo_redo.commit_action()
  348. force_hide_tooltips()
  349. func delete_node(node_to_delete: GraphNode) -> void:
  350. remove_connections_to_node(node_to_delete)
  351. #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
  352. remove_child(node_to_delete)
  353. control_script.changesmade = true
  354. func restore_node(node_to_restore: GraphNode) -> void:
  355. add_child(node_to_restore)
  356. #relink everything
  357. if not node_to_restore.is_connected("open_help", open_help):
  358. node_to_restore.connect("open_help", open_help)
  359. if not node_to_restore.is_connected("node_moved", _auto_link_nodes):
  360. node_to_restore.node_moved.connect(_auto_link_nodes)
  361. if "undo_redo" in node_to_restore:
  362. node_to_restore.undo_redo = control_script.undo_redo
  363. for child in node_to_restore.get_children():
  364. if "undo_redo" in child:
  365. child.undo_redo = control_script.undo_redo
  366. if not node_to_restore.is_connected("dragged", node_position_changed):
  367. node_to_restore.dragged.connect(node_position_changed.bind(node_to_restore))
  368. set_node_selected(node_to_restore, true)
  369. _track_changes()
  370. _register_inputs_in_node(node_to_restore)
  371. _register_node_movement()
  372. control_script.changesmade = true
  373. func get_connections_to_nodes(nodes: Array) -> Array:
  374. var connections_to_nodes = []
  375. for con in get_connection_list():
  376. if (con["from_node"] in nodes or con["to_node"] in nodes) and !connections_to_nodes.has(con):
  377. connections_to_nodes.append(con)
  378. return connections_to_nodes
  379. func restore_connections(connections_to_restore: Array) -> void:
  380. for con in connections_to_restore:
  381. var from_node = con["from_node"]
  382. var from_port = con["from_port"]
  383. var to_node = con["to_node"]
  384. var to_port = con["to_port"]
  385. if has_node(NodePath(from_node)) and has_node(NodePath(to_node)):
  386. connect_node(from_node, from_port, to_node, to_port)
  387. func set_node_selected(node: Node, selected: bool) -> void:
  388. selected_nodes[node] = selected
  389. #
  390. func remove_connections_to_node(node):
  391. for con in get_connection_list():
  392. if con["to_node"] == node.name or con["from_node"] == node.name:
  393. disconnect_node(con["from_node"], con["from_port"], con["to_node"], con["to_port"])
  394. control_script.changesmade = true
  395. #copy and paste nodes with vertical offset on paste
  396. func copy_selected_nodes():
  397. if selected_nodes.size() == 0:
  398. return
  399. copied_nodes_data.clear()
  400. copied_connections.clear()
  401. var copied_node_names = []
  402. # Store selected nodes
  403. for node in get_children():
  404. # Check if the node is selected and not an 'outputfile'
  405. if node is GraphNode and selected_nodes.get(node, false):
  406. if node.get_meta("command") == "outputfile":
  407. continue # Skip these nodes
  408. #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
  409. var copied_node = node.duplicate()
  410. copied_nodes_data.append(copied_node)
  411. copied_node_names.append(node.name)
  412. copied_connections = get_connections_to_nodes(copied_node_names)
  413. func paste_copied_nodes():
  414. if copied_nodes_data.is_empty():
  415. return
  416. var pasted_connections = copied_connections.duplicate(true)
  417. control_script.undo_redo.create_action("Paste Nodes")
  418. var pasted_nodes = []
  419. #Find topmost and bottommost Y of copied nodes and decide where to paste
  420. var min_y = INF
  421. var max_y = -INF
  422. for node in copied_nodes_data:
  423. var y = node.position_offset.y
  424. min_y = min(min_y, y)
  425. max_y = max(max_y, y)
  426. var base_y_offset = max_y + 350 # Pasting below the lowest node
  427. # Step 3: Paste nodes, preserving vertical layout
  428. for node in copied_nodes_data:
  429. #duplicate the copied node again so it can be pasted more than once and add using restore_node function
  430. var pasted_node = node.duplicate()
  431. #give the node a random name
  432. pasted_node.name = node.name + "_copy_" + str(randi() % 10000)
  433. control_script.undo_redo.add_do_method(restore_node.bind(pasted_node))
  434. control_script.undo_redo.add_do_reference(pasted_node)
  435. #adjust the offset of the pasted node to be below the copied node
  436. var relative_y = pasted_node.position_offset.y - min_y
  437. pasted_node.position_offset.y = base_y_offset + relative_y
  438. for con in pasted_connections:
  439. if con["from_node"] == node.name:
  440. con["from_node"] = pasted_node.name
  441. print(pasted_node.name)
  442. if con["to_node"] == node.name:
  443. con["to_node"] = pasted_node.name
  444. control_script.undo_redo.add_undo_method(delete_node.bind(pasted_node))
  445. pasted_nodes.append(pasted_node)
  446. control_script.undo_redo.add_do_method(restore_connections.bind(pasted_connections))
  447. control_script.undo_redo.commit_action()
  448. force_hide_tooltips()
  449. #Select pasted nodes
  450. for pasted_node in pasted_nodes:
  451. selected_nodes.clear()
  452. set_selected(pasted_node)
  453. selected_nodes[pasted_node] = true
  454. control_script.changesmade = true
  455. func force_hide_tooltips():
  456. #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
  457. #seems to be more reliable that faking a middle mouse click
  458. var popup := Popup.new()
  459. add_child(popup)
  460. popup.size = Vector2(1,1)
  461. popup.transparent_bg = true
  462. popup.borderless = true
  463. popup.unresizable = true
  464. await get_tree().process_frame
  465. popup.popup()
  466. popup.queue_free()
  467. #functions for tracking changes for save state detection
  468. func _register_inputs_in_node(node: Node):
  469. #tracks input to nodes sliders and codeedit to track if patch is saved
  470. # Track Sliders
  471. for slider in node.find_children("*", "HSlider", true, false):
  472. # Create a Callable to the correct method
  473. var callable = Callable(self, "_on_any_slider_changed")
  474. # Check if it's already connected, and connect if not
  475. if not slider.is_connected("value_changed", callable):
  476. slider.connect("value_changed", callable)
  477. for slider in node.find_children("*", "VBoxContainer", true, false):
  478. # Also connect to meta_changed if the slider has that signal
  479. if slider.has_signal("meta_changed"):
  480. var meta_callable = Callable(self, "_on_any_slider_meta_changed")
  481. if not slider.is_connected("meta_changed", meta_callable):
  482. slider.connect("meta_changed", meta_callable)
  483. # Track CodeEdits
  484. for editor in node.find_children("*", "CodeEdit", true, false):
  485. var callable = Callable(self, "_on_any_input_changed")
  486. if not editor.is_connected("text_changed", callable):
  487. editor.connect("text_changed", callable)
  488. func _on_any_slider_meta_changed():
  489. control_script.changesmade = true
  490. print("Meta changed in slider")
  491. func _register_node_movement():
  492. for graphnode in get_children():
  493. if graphnode is GraphNode:
  494. var callable = Callable(self, "_on_graphnode_moved")
  495. if not graphnode.is_connected("position_offset_changed", callable):
  496. graphnode.connect("position_offset_changed", callable)
  497. func _on_graphnode_moved():
  498. control_script.changesmade = true
  499. func _on_any_slider_changed(value: float) -> void:
  500. control_script.changesmade = true
  501. func _on_any_input_changed():
  502. control_script.changesmade = true
  503. func _track_changes():
  504. control_script.changesmade = true
  505. func _on_copy_nodes_request() -> void:
  506. graph_edit.copy_selected_nodes()
  507. #get_viewport().set_input_as_handled()
  508. func _on_paste_nodes_request() -> void:
  509. control_script.simulate_mouse_click() #hacky fix to stop tooltips getting stuck
  510. await get_tree().process_frame
  511. graph_edit.paste_copied_nodes()
  512. func on_inlet_removed(node_name: StringName, port_index: int):
  513. var connections = get_connection_list()
  514. for conn in connections:
  515. if conn.to_node == node_name and conn.to_port == port_index:
  516. disconnect_node(conn.from_node, conn.from_port, conn.to_node, conn.to_port)
  517. func _swap_node(old_node: GraphNode, command: String):
  518. #store the position and name of the node to be replaced
  519. var position = old_node.position_offset
  520. var old_name = old_node.name
  521. #gather all connections in the graph
  522. var connections = get_connection_list()
  523. var related_connections = []
  524. #filter the connections to get just those connected to the node to be replaced
  525. for conn in connections:
  526. if conn.from_node == old_name or conn.to_node == old_name:
  527. related_connections.append(conn)
  528. #delete the old node
  529. _on_graph_edit_delete_nodes_request([old_node.name])
  530. #make the new node and reposition it to the location of the old node
  531. var new_node = _make_node(command)
  532. new_node.position_offset = position
  533. #filter through all the connections to the old node
  534. for conn in related_connections:
  535. var from = conn.from_node
  536. var from_port = conn.from_port
  537. var to = conn.to_node
  538. var to_port = conn.to_port
  539. #where the old node is referenced replace it with the name of the new node
  540. if from == old_name:
  541. from = new_node.name
  542. if to == old_name:
  543. to = new_node.name
  544. #check that the ports being connected to/from on the new node actually exist
  545. 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)):
  546. #check the two ports are the same type
  547. if _same_port_type(from, from_port, to, to_port):
  548. _on_connection_request(from, from_port, to, to_port)
  549. func _connect_to_clicked_node(clicked_node: GraphNode, command: String):
  550. var new_node_position = clicked_node.position_offset + Vector2(clicked_node.size.x + 50, 0)
  551. #make the new node and reposition it to right of the node to connect to
  552. var new_node = _make_node(command)
  553. new_node.position_offset = new_node_position
  554. var clicked_node_has_outputs = clicked_node.get_output_port_count() > 0
  555. var new_node_has_inputs = new_node.get_input_port_count() > 0
  556. if clicked_node_has_outputs and new_node_has_inputs:
  557. if _same_port_type(clicked_node.name, 0, new_node.name, 0):
  558. _on_connection_request(clicked_node.name, 0, new_node.name, 0)
  559. func _on_gui_input(event: InputEvent) -> void:
  560. #check if this is an unhandled mouse click
  561. if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
  562. #get dictionary of a cable if nearby
  563. var closest_connection = get_closest_connection_at_point(get_local_mouse_position())
  564. #check if there is anything in that dictionary
  565. if closest_connection.size() > 0:
  566. #check if the background has changed colour for highlighted cable colour
  567. var interface_settings = ConfigHandler.load_interface_settings()
  568. 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:
  569. #if bg has changed colour since last cable highlight reset to new bg and change cable colour
  570. theme_background = interface_settings.theme
  571. theme_custom_background = interface_settings.theme_custom_colour
  572. high_contrast_cables = interface_settings.high_contrast_selected_cables
  573. set_cable_colour(interface_settings.theme)
  574. #get details of nearby cable
  575. var from_node = closest_connection.from_node
  576. var from_port = closest_connection.from_port
  577. var to_node = closest_connection.to_node
  578. var to_port = closest_connection.to_port
  579. #check if user was holding shift and if so allow for multiple cables to be selected
  580. if event.shift_pressed:
  581. selected_cables.append(closest_connection)
  582. set_connection_activity(from_node, from_port, to_node, to_port, 1)
  583. #if user double clicked unselect all cables and delete the nearest cable
  584. elif event.double_click:
  585. for conn in selected_cables:
  586. set_connection_activity(conn.from_node, conn.from_port, conn.to_node, conn.to_port, 0)
  587. _on_graph_edit_disconnection_request(from_node, from_port, to_node, to_port)
  588. #else just a single click, unselect any previously selected cables and select just the nearest
  589. else:
  590. for conn in selected_cables:
  591. set_connection_activity(conn.from_node, conn.from_port, conn.to_node, conn.to_port, 0)
  592. selected_cables = []
  593. selected_cables.append(closest_connection)
  594. set_connection_activity(from_node, from_port, to_node, to_port, 1)
  595. #user didnt click on a cable unselect all cables
  596. else:
  597. for conn in selected_cables:
  598. set_connection_activity(conn.from_node, conn.from_port, conn.to_node, conn.to_port, 0)
  599. selected_cables = []
  600. #if this is an unhandled delete check if there are any cables selected and deleted them
  601. if event is InputEventKey and event.pressed:
  602. if (event.keycode == KEY_BACKSPACE or event.keycode == KEY_DELETE) and selected_cables.size() > 0:
  603. for conn in selected_cables:
  604. _on_graph_edit_disconnection_request(conn.from_node, conn.from_port, conn.to_node, conn.to_port)
  605. selected_cables = []
  606. func set_cable_colour(theme_colour: int):
  607. var background_colour
  608. var cable_colour
  609. var interface_settings = ConfigHandler.load_interface_settings()
  610. match theme_colour:
  611. 0:
  612. background_colour = Color("#2f4f4e")
  613. 1:
  614. background_colour = Color("#000807")
  615. 2:
  616. background_colour = Color("#98d4d2")
  617. 3:
  618. background_colour = Color(interface_settings.theme_custom_colour)
  619. if interface_settings.high_contrast_selected_cables:
  620. #180 colour shift from background and up sv
  621. 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))
  622. var luminance = 0.299 * background_colour.r + 0.587 * background_colour.g + 0.114 * background_colour.b
  623. if luminance > 0.5 and cable_colour.get_luminance() > 0.5:
  624. cable_colour = cable_colour.darkened(0.4)
  625. elif luminance <= 0.5 and cable_colour.get_luminance() < 0.5:
  626. #increase s and v again
  627. 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))
  628. else:
  629. #keep hue but up saturation and variance
  630. cable_colour = Color.from_hsv(background_colour.h, clamp(background_colour.s + 0.2, 0, 1), clamp(background_colour.v + 0.2, 0, 1))
  631. #overide theme for cable highlight
  632. add_theme_color_override("activity", cable_colour)
  633. func _auto_link_nodes(node: GraphNode, rect: Rect2):
  634. #get all cables that overlap with the node being moved
  635. var potential_connections = get_connections_intersecting_with_rect(rect)
  636. #if there are anyoverlapping and shift is being held down then
  637. if potential_connections.size() > 0 and Input.is_action_pressed("auto_link_nodes"):
  638. #sort through all the cables that overlap
  639. for conn in potential_connections:
  640. #get their info
  641. var new_node_name = node.name
  642. var new_node_has_inputs = node.get_input_port_count() > 0
  643. var new_node_has_outputs = node.get_output_port_count() > 0
  644. var from = conn.from_node
  645. var from_port = conn.from_port
  646. var to = conn.to_node
  647. var to_port = conn.to_port
  648. if new_node_has_inputs and new_node_has_outputs:
  649. #connect in the middle of the two nodes if they are the same port type
  650. var from_matches = _same_port_type(from, from_port, new_node_name, 0)
  651. var to_matches = _same_port_type(new_node_name, 0, to, to_port)
  652. if from_matches:
  653. _on_connection_request(from, from_port, new_node_name, 0)
  654. if to_matches:
  655. _on_connection_request(new_node_name, 0, to, to_port)
  656. #skip deleting cables if they are the same as the node being dragged or the ports don't match
  657. if from_matches and to_matches and from != new_node_name and to != new_node_name:
  658. _on_graph_edit_disconnection_request(from, from_port, to, to_port)
  659. elif new_node_has_inputs:
  660. #only has inputs check if the ports match and if they do connect but leave original connection in place
  661. if _same_port_type(from, from_port, new_node_name, 0):
  662. _on_connection_request(from, from_port, new_node_name, 0)
  663. elif new_node_has_outputs:
  664. #only has outputs check if the ports match and if they do connect but leave original connection in place
  665. if _same_port_type(new_node_name, 0, to, to_port):
  666. _on_connection_request(new_node_name, 0, to, to_port)
  667. # function for checking if an inlet and an outlet are the same type
  668. func _same_port_type(from: String, from_port: int, to: String, to_port: int) -> bool:
  669. var from_node = get_node_or_null(NodePath(from))
  670. var to_node = get_node_or_null(NodePath(to))
  671. #safety incase one somehow no longer exists
  672. if from_node != null and to_node != null:
  673. #check if the port types are the same e.g. both time or both pvoc
  674. if from_node.get_output_port_type(from_port) == to_node.get_input_port_type(to_port):
  675. return true
  676. else:
  677. return false
  678. else:
  679. return false
  680. func node_position_changed(from: Vector2, to: Vector2, node: Node) -> void:
  681. control_script.undo_redo.create_action("Move Node")
  682. control_script.undo_redo.add_do_method(move_node.bind(node, to))
  683. control_script.undo_redo.add_undo_method(move_node.bind(node, from))
  684. control_script.undo_redo.commit_action()
  685. func move_node(node: Node, to: Vector2) -> void:
  686. node.position_offset = to