control.gd 64 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793
  1. extends Control
  2. var mainmenu_visible : bool = false #used to test if mainmenu is open
  3. var effect_position = Vector2(40,40) #tracks mouse position for node placement offset
  4. @onready var graph_edit = $GraphEdit
  5. var selected_nodes = {} #used to track which nodes in the GraphEdit are selected
  6. var cdpprogs_location #stores the cdp programs location from user prefs for easy access
  7. var delete_intermediate_outputs # tracks state of delete intermediate outputs toggle
  8. @onready var console_output: RichTextLabel = $Console/ConsoleOutput
  9. var final_output_dir
  10. var copied_nodes_data = [] #stores node data on ctrl+c
  11. var copied_connections = [] #stores all connections on ctrl+c
  12. var undo_redo := UndoRedo.new()
  13. var output_audio_player #tracks the node that is the current output player for linking
  14. var input_audio_player #tracks node that is the current input player for linking
  15. var outfile = "no file" #tracks dir of output file from cdp process
  16. var currentfile = "none" #tracks dir of currently loaded file for saving
  17. var changesmade = false #tracks if user has made changes to the currently loaded save file
  18. var savestate # tracks what the user is trying to do when savechangespopup is called
  19. var helpfile #tracks which help file the user was trying to load when savechangespopup is called
  20. var outfilename #links to the user name for outputfile field
  21. var foldertoggle #links to the reuse folder button
  22. var lastoutputfolder = "none" #tracks last output folder, this can in future be used to replace global.outfile but i cba right now
  23. var process_successful #tracks if the last run process was successful
  24. var help_data := {} #stores help data for each node to display in help popup
  25. var HelpWindowScene = preload("res://scenes/main/help_window.tscn")
  26. var uiscale = 1.0 #tracks scaling for retina screens
  27. # Called when the node enters the scene tree for the first time.
  28. func _ready() -> void:
  29. Nodes.hide()
  30. $mainmenu.hide()
  31. #$"mainmenu/select_effect/Time Domain".show()
  32. #$"mainmenu/select_effect/Time Domain/Distort".show()
  33. #$"mainmenu/select_effect/Frequency Domain/Convert".show()
  34. $NoLocationPopup.hide()
  35. $Console.hide()
  36. $NoInputPopup.hide()
  37. $MultipleConnectionsPopup.hide()
  38. $AudioSettings.hide()
  39. $AudioDevicePopup.hide()
  40. $SearchMenu.hide()
  41. $SaveDialog.access = FileDialog.ACCESS_FILESYSTEM
  42. $SaveDialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
  43. $SaveDialog.filters = ["*.thd"]
  44. $LoadDialog.access = FileDialog.ACCESS_FILESYSTEM
  45. $LoadDialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
  46. $LoadDialog.filters = ["*.thd"]
  47. #Goes through all nodes in scene and checks for buttons in the make_node_buttons group
  48. #Associates all buttons with the _on_button_pressed fuction and passes the button as an argument
  49. for child in get_tree().get_nodes_in_group("make_node_buttons"):
  50. if child is Button:
  51. child.pressed.connect(_on_button_pressed.bind(child))
  52. get_node("SearchMenu").make_node.connect(_make_node_from_search_menu)
  53. check_cdp_location_set()
  54. check_user_preferences()
  55. new_patch()
  56. get_tree().set_auto_accept_quit(false)
  57. #Check export config for version number and set about menu to current version
  58. #Assumes version of mac + linux builds is the same as windows
  59. #Requires manual update for alpha and beta builds but once the -beta is removed will be fully automatic so long as version is updated on export
  60. var export_config = ConfigFile.new()
  61. export_config.load("res://export_presets.cfg")
  62. $MenuBar/About.set_item_text(0, "SoundThread v" + export_config.get_value("preset.0.options", "application/product_version", "version unknown") + "-alpha")
  63. #checks if display is hidpi and scales ui accordingly hidpi - 144
  64. if DisplayServer.screen_get_dpi(0) >= 144:
  65. uiscale = 2.0
  66. get_window().content_scale_factor = uiscale
  67. #goes through popup_windows group and scales all popups and resizes them
  68. for window in get_tree().get_nodes_in_group("popup_windows"):
  69. window.size = window.size * uiscale
  70. window.content_scale_factor = uiscale
  71. #checks if user has opened a file from the system file menu and loads it
  72. var args = OS.get_cmdline_args()
  73. for arg in args:
  74. var path = arg.strip_edges()
  75. if FileAccess.file_exists(path) and path.get_extension().to_lower() == "thd":
  76. load_graph_edit(path)
  77. break
  78. var file = FileAccess.open("res://scenes/main/process_help.json", FileAccess.READ)
  79. if file:
  80. help_data = JSON.parse_string(file.get_as_text())
  81. func new_patch():
  82. #clear old patch
  83. graph_edit.clear_connections()
  84. for node in graph_edit.get_children():
  85. if node is GraphNode:
  86. node.queue_free()
  87. #Generate input and output nodes
  88. var effect: GraphNode = Nodes.get_node(NodePath("inputfile")).duplicate()
  89. get_node("GraphEdit").add_child(effect, true)
  90. effect.position_offset = Vector2(20,80)
  91. effect = Nodes.get_node(NodePath("outputfile")).duplicate()
  92. get_node("GraphEdit").add_child(effect, true)
  93. effect.position_offset = Vector2((DisplayServer.screen_get_size().x - 480) ,80)
  94. _register_node_movement() #link nodes for tracking position changes for changes tracking
  95. changesmade = false #so it stops trying to save unchanged empty files
  96. Global.infile = "no_file" #resets input to stop processes running with old files
  97. get_window().title = "SoundThread"
  98. link_output()
  99. func link_output():
  100. #links various buttons and function in the input nodes - this is called after they are created so that it still works on new and loading files
  101. for control in get_tree().get_nodes_in_group("outputnode"): #check all items in outputnode group
  102. if control.get_meta("outputfunction") == "deleteintermediate": #link delete intermediate files toggle to script
  103. control.toggled.connect(_toggle_delete)
  104. control.button_pressed = true
  105. elif control.get_meta("outputfunction") == "runprocess": #link runprocess button
  106. control.button_down.connect(_run_process)
  107. elif control.get_meta("outputfunction") == "recycle": #link recycle button
  108. control.button_down.connect(_recycle_outfile)
  109. elif control.get_meta("outputfunction") == "audioplayer": #link output audio player
  110. output_audio_player = control
  111. elif control.get_meta("outputfunction") == "filename":
  112. control.text = "outfile"
  113. outfilename = control
  114. elif control.get_meta("outputfunction") == "reusefolder":
  115. foldertoggle = control
  116. foldertoggle.button_pressed = true
  117. elif control.get_meta("outputfunction") == "openfolder":
  118. control.button_down.connect(_open_output_folder)
  119. for control in get_tree().get_nodes_in_group("inputnode"):
  120. if control.get_meta("inputfunction") == "audioplayer": #link input for recycle function
  121. print("input player found")
  122. input_audio_player = control
  123. func check_user_preferences():
  124. var interface_settings = ConfigHandler.load_interface_settings()
  125. var audio_settings = ConfigHandler.load_audio_settings()
  126. var audio_devices = AudioServer.get_output_device_list()
  127. $MenuBar/SettingsButton.set_item_checked(1, interface_settings.disable_pvoc_warning)
  128. $MenuBar/SettingsButton.set_item_checked(2, interface_settings.auto_close_console)
  129. $MenuBar/SettingsButton.set_item_checked(3, interface_settings.console_on_top)
  130. $MenuBar/SettingsButton.set_item_checked(3, interface_settings.use_search)
  131. $Console.always_on_top = interface_settings.console_on_top
  132. if audio_devices.has(audio_settings.device):
  133. AudioServer.set_output_device(audio_settings.device)
  134. else:
  135. $AudioDevicePopup.popup()
  136. func check_cdp_location_set():
  137. #checks if the location has been set and prompts user to set it
  138. var cdpprogs_settings = ConfigHandler.load_cdpprogs_settings()
  139. if cdpprogs_settings.location == "no_location":
  140. $NoLocationPopup.show()
  141. else:
  142. #if location is set, stores it in a variable
  143. cdpprogs_location = str(cdpprogs_settings.location)
  144. print(cdpprogs_location)
  145. func _on_ok_button_button_down() -> void:
  146. #after user has read dialog on where to find cdp progs this loads the file browser
  147. $NoLocationPopup.hide()
  148. if OS.get_name() == "Windows":
  149. $CdpLocationDialog.current_dir = "C:/"
  150. else:
  151. $CdpLocationDialog.current_dir = "~/"
  152. $CdpLocationDialog.show()
  153. func _on_cdp_location_dialog_dir_selected(dir: String) -> void:
  154. #saves default location for cdp programs in config file
  155. ConfigHandler.save_cdpprogs_settings(dir)
  156. cdpprogs_location = dir
  157. func _on_cdp_location_dialog_canceled() -> void:
  158. #cycles around the set location prompt if user cancels the file dialog
  159. check_cdp_location_set()
  160. # Called every frame. 'delta' is the elapsed time since the previous frame.
  161. func _process(delta: float) -> void:
  162. #showmenu()
  163. pass
  164. func _input(event):
  165. if event.is_action_pressed("copy_node"):
  166. copy_selected_nodes()
  167. get_viewport().set_input_as_handled()
  168. elif event.is_action_pressed("paste_node"):
  169. simulate_mouse_click() #hacky fix to stop tooltips getting stuck
  170. await get_tree().process_frame
  171. paste_copied_nodes()
  172. get_viewport().set_input_as_handled()
  173. elif event.is_action_pressed("undo"):
  174. undo_redo.undo()
  175. elif event.is_action_pressed("redo"):
  176. undo_redo.redo()
  177. elif event.is_action_pressed("save"):
  178. if currentfile == "none":
  179. savestate = "saveas"
  180. $SaveDialog.popup_centered()
  181. else:
  182. save_graph_edit(currentfile)
  183. if event is InputEventMouseButton and event.pressed:
  184. if event.button_index == MOUSE_BUTTON_LEFT and event.double_click:
  185. var pos = get_viewport().get_mouse_position()
  186. var clicked_node = find_graphedit_node_under(pos)
  187. if clicked_node:
  188. await get_tree().process_frame #wait a frame to stop nodes jumping across graph edit
  189. deselect_all_nodes() #deselect nodes to avoid dragging issues
  190. show_help_for_node(clicked_node.get_meta("command"), clicked_node.title)
  191. func find_graphedit_node_under(mouse_pos: Vector2) -> GraphNode:
  192. #find the node that was double clicked on
  193. for node in graph_edit.get_children():
  194. if node is GraphNode and node.get_global_rect().has_point(mouse_pos):
  195. return node
  196. return null
  197. func deselect_all_nodes():
  198. for node in $GraphEdit.get_children():
  199. if node is GraphNode:
  200. node.selected = false
  201. selected_nodes[node] = false
  202. func show_help_for_node(node_name: String, node_title: String):
  203. #check if there is already a help window open for this node and pop it up instead of making a new one
  204. for child in get_tree().current_scene.get_children():
  205. if child is Window and child.title == "Help - " + node_title:
  206. # Found existing window, bring it to front
  207. if child.is_visible():
  208. child.hide()
  209. child.popup()
  210. else:
  211. child.popup()
  212. return
  213. if help_data.has(node_name):
  214. #looks up the help data from the json and stores it in info
  215. var info = help_data[node_name]
  216. #makes an instance of the help_window scene
  217. var help_window = HelpWindowScene.instantiate()
  218. help_window.title = "Help - " + node_title
  219. help_window.get_node("HelpTitle").text = node_title
  220. var output = ""
  221. output += info.get("short_description", "") + "\n\n"
  222. var parameters = info.get("parameters", {})
  223. #checks if there are parameters and if there are places them in a table
  224. if parameters.size() > 0:
  225. output += "[table=3]\n"
  226. output += "[cell][b]Parameter Name[/b][/cell][cell][b]Description[/b][/cell][cell][b]Automatable[/b][/cell]\n"
  227. for key in parameters.keys(): #scans through all parameters
  228. var param = parameters[key]
  229. var name = param.get("paramname", "")
  230. var desc = param.get("paramdescription", "")
  231. var automatable = param.get("automatable", false)
  232. var autom_text = "[center]✓[/center]" if automatable else "[center]𐄂[/center]" #replaces true and false with ticks and crosses
  233. output += "[cell]%s[/cell][cell]%s[/cell][cell]%s[/cell]\n" % [name, desc, autom_text] #places each param detail into cells of the table
  234. output += "[/table]\n\n" #ends the table
  235. output += "[b]Functionality[/b]\n"
  236. var description_text = info.get("description", "")
  237. output += description_text.strip_edges()
  238. #check if this is a cdp process or a utility and display the cdp process if it is one
  239. var category = info.get("category", "")
  240. if category != "utility":
  241. output += "\n\n[b]CDP Process[/b]\nThis node runs the CDP Process: " + node_name.replace("_", " ")
  242. help_window.get_node("HelpText").bbcode_text = output
  243. help_window.get_node("HelpText").scroll_to_line(0) #scrolls to the first line of the help file just incase
  244. # Add to the current scene tree to show it
  245. get_tree().current_scene.add_child(help_window)
  246. if help_window.content_scale_factor < uiscale:
  247. help_window.size = help_window.size * uiscale
  248. help_window.content_scale_factor = uiscale
  249. help_window.popup()
  250. else:
  251. # If no help available, even though there always should be, show a window saying no help found
  252. var help_window = HelpWindowScene.instance()
  253. help_window.title = "Help - " + node_title
  254. help_window.get_node("HelpTitle").text = node_title
  255. help_window.get_node("HelpText").bbcode_text = "No help found."
  256. get_tree().current_scene.add_child(help_window)
  257. help_window.popup()
  258. func simulate_mouse_click():
  259. #simulates clicking the middle mouse button in order to hide any visible tooltips
  260. var click_pos = get_viewport().get_mouse_position()
  261. var down_event := InputEventMouseButton.new()
  262. down_event.button_index = MOUSE_BUTTON_MIDDLE
  263. down_event.pressed = true
  264. down_event.position = click_pos
  265. Input.parse_input_event(down_event)
  266. var up_event := InputEventMouseButton.new()
  267. up_event.button_index = MOUSE_BUTTON_MIDDLE
  268. up_event.pressed = false
  269. up_event.position = click_pos
  270. Input.parse_input_event(up_event)
  271. func _make_node_from_search_menu(command: String):
  272. #close menu
  273. $SearchMenu.hide()
  274. #Find node with matching name to button and create a version of it in the graph edit
  275. #and position it close to the origin right click to open the menu
  276. var effect: GraphNode = Nodes.get_node(NodePath(command)).duplicate()
  277. get_node("GraphEdit").add_child(effect, true)
  278. effect.set_position_offset((effect_position + graph_edit.scroll_offset) / graph_edit.zoom) #set node to current mouse position in graph edit
  279. _register_inputs_in_node(effect) #link sliders for changes tracking
  280. _register_node_movement() #link nodes for tracking position changes for changes tracking
  281. changesmade = true
  282. # Remove node with UndoRedo
  283. undo_redo.create_action("Add Node")
  284. undo_redo.add_undo_method(Callable(graph_edit, "remove_child").bind(effect))
  285. undo_redo.add_undo_method(Callable(effect, "queue_free"))
  286. undo_redo.add_undo_method(Callable(self, "_track_changes"))
  287. undo_redo.commit_action()
  288. func _on_button_pressed(button: Button):
  289. #close menu
  290. $mainmenu.hide()
  291. mainmenu_visible = false
  292. #Find node with matching name to button and create a version of it in the graph edit
  293. #and position it close to the origin right click to open the menu
  294. var effect: GraphNode = Nodes.get_node(NodePath(button.name)).duplicate()
  295. get_node("GraphEdit").add_child(effect, true)
  296. effect.set_position_offset((effect_position + graph_edit.scroll_offset) / graph_edit.zoom) #set node to current mouse position in graph edit
  297. _register_inputs_in_node(effect) #link sliders for changes tracking
  298. _register_node_movement() #link nodes for tracking position changes for changes tracking
  299. changesmade = true
  300. # Remove node with UndoRedo
  301. undo_redo.create_action("Add Node")
  302. undo_redo.add_undo_method(Callable(graph_edit, "remove_child").bind(effect))
  303. undo_redo.add_undo_method(Callable(effect, "queue_free"))
  304. undo_redo.add_undo_method(Callable(self, "_track_changes"))
  305. undo_redo.commit_action()
  306. func _on_graph_edit_connection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
  307. #get_node("GraphEdit").connect_node(from_node, from_port, to_node, to_port)
  308. var graph_edit = get_node("GraphEdit")
  309. var to_graph_node = graph_edit.get_node(NodePath(to_node))
  310. # Get the type of the input port using GraphNode's built-in method
  311. var port_type = to_graph_node.get_input_port_type(to_port)
  312. # If port type is 1 and already has a connection, reject the request
  313. if port_type == 1:
  314. var connections = graph_edit.get_connection_list()
  315. var existing_connections = 0
  316. for conn in connections:
  317. if conn.to_node == to_node and conn.to_port == to_port:
  318. existing_connections += 1
  319. if existing_connections >= 1:
  320. var interface_settings = ConfigHandler.load_interface_settings()
  321. if interface_settings.disable_pvoc_warning == false:
  322. $MultipleConnectionsPopup.show()
  323. return
  324. # If no conflict, allow the connection
  325. graph_edit.connect_node(from_node, from_port, to_node, to_port)
  326. changesmade = true
  327. func _on_graph_edit_disconnection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
  328. get_node("GraphEdit").disconnect_node(from_node, from_port, to_node, to_port)
  329. changesmade = true
  330. func _on_graph_edit_node_selected(node: Node) -> void:
  331. selected_nodes[node] = true
  332. func _on_graph_edit_node_deselected(node: Node) -> void:
  333. selected_nodes[node] = false
  334. func _unhandled_key_input(event: InputEvent) -> void:
  335. if event is InputEventKey and event.pressed and not event.echo:
  336. if event.keycode == KEY_BACKSPACE:
  337. _on_graph_edit_delete_nodes_request(PackedStringArray(selected_nodes.keys().filter(func(k): return selected_nodes[k])))
  338. pass
  339. func _on_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> void:
  340. var graph_edit = get_node("GraphEdit")
  341. undo_redo.create_action("Delete Nodes (Undo only)")
  342. for node in selected_nodes.keys():
  343. if selected_nodes[node]:
  344. if node.get_meta("command") == "inputfile" or node.get_meta("command") == "outputfile":
  345. print("can't delete input or output")
  346. else:
  347. # Store duplicate and state for undo
  348. var node_data = node.duplicate()
  349. var position = node.position_offset
  350. # Store all connections for undo
  351. var conns = []
  352. for con in graph_edit.get_connection_list():
  353. if con["to_node"] == node.name or con["from_node"] == node.name:
  354. conns.append(con)
  355. # Delete
  356. remove_connections_to_node(node)
  357. node.queue_free()
  358. changesmade = true
  359. # Register undo restore
  360. undo_redo.add_undo_method(Callable(graph_edit, "add_child").bind(node_data, true))
  361. undo_redo.add_undo_method(Callable(node_data, "set_position_offset").bind(position))
  362. for con in conns:
  363. undo_redo.add_undo_method(Callable(graph_edit, "connect_node").bind(
  364. con["from_node"], con["from_port"],
  365. con["to_node"], con["to_port"]
  366. ))
  367. undo_redo.add_undo_method(Callable(self, "set_node_selected").bind(node_data, true))
  368. undo_redo.add_undo_method(Callable(self, "_track_changes"))
  369. undo_redo.add_undo_method(Callable(self, "_register_inputs_in_node").bind(node_data)) #link sliders for changes tracking
  370. undo_redo.add_undo_method(Callable(self, "_register_node_movement")) # link nodes for changes tracking
  371. # Clear selection
  372. selected_nodes = {}
  373. undo_redo.commit_action()
  374. func set_node_selected(node: Node, selected: bool) -> void:
  375. selected_nodes[node] = selected
  376. #
  377. func remove_connections_to_node(node):
  378. for con in get_node("GraphEdit").get_connection_list():
  379. if con["to_node"] == node.name or con["from_node"] == node.name:
  380. get_node("GraphEdit").disconnect_node(con["from_node"], con["from_port"], con["to_node"], con["to_port"])
  381. changesmade = true
  382. #copy and paste nodes with vertical offset on paste
  383. func copy_selected_nodes():
  384. copied_nodes_data.clear()
  385. copied_connections.clear()
  386. var graph_edit = get_node("GraphEdit")
  387. # Store selected nodes and their slider values
  388. for node in graph_edit.get_children():
  389. # Check if the node is selected and not an 'inputfile' or 'outputfile'
  390. if node is GraphNode and selected_nodes.get(node, false):
  391. if node.get_meta("command") == "inputfile" or node.get_meta("command") == "outputfile":
  392. continue # Skip these nodes
  393. var node_data = {
  394. "name": node.name,
  395. "type": node.get_class(),
  396. "offset": node.position_offset,
  397. "slider_values": {}
  398. }
  399. for child in node.get_children():
  400. if child is HSlider or child is VSlider:
  401. node_data["slider_values"][child.name] = child.value
  402. copied_nodes_data.append(node_data)
  403. # Store connections between selected nodes
  404. for conn in graph_edit.get_connection_list():
  405. var from_ref = graph_edit.get_node_or_null(NodePath(conn["from_node"]))
  406. var to_ref = graph_edit.get_node_or_null(NodePath(conn["to_node"]))
  407. var is_from_selected = from_ref != null and selected_nodes.get(from_ref, false)
  408. var is_to_selected = to_ref != null and selected_nodes.get(to_ref, false)
  409. # Skip if any of the connected nodes are 'inputfile' or 'outputfile'
  410. 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")):
  411. continue
  412. if is_from_selected and is_to_selected:
  413. # Store connection as dictionary
  414. var conn_data = {
  415. "from_node": conn["from_node"],
  416. "from_port": conn["from_port"],
  417. "to_node": conn["to_node"],
  418. "to_port": conn["to_port"]
  419. }
  420. copied_connections.append(conn_data)
  421. func paste_copied_nodes():
  422. if copied_nodes_data.is_empty():
  423. return
  424. var graph_edit = get_node("GraphEdit")
  425. var name_map = {}
  426. var pasted_nodes = []
  427. # Step 1: Find topmost and bottommost Y of copied nodes
  428. var min_y = INF
  429. var max_y = -INF
  430. for node_data in copied_nodes_data:
  431. var y = node_data["offset"].y
  432. min_y = min(min_y, y)
  433. max_y = max(max_y, y)
  434. # Step 2: Decide where to paste the group
  435. var base_y_offset = max_y + 350 # Pasting below the lowest node
  436. # Step 3: Paste nodes, preserving vertical layout
  437. for node_data in copied_nodes_data:
  438. var original_node = graph_edit.get_node_or_null(NodePath(node_data["name"]))
  439. if not original_node:
  440. continue
  441. var new_node = original_node.duplicate()
  442. new_node.name = node_data["name"] + "_copy_" + str(randi() % 10000)
  443. var relative_y = node_data["offset"].y - min_y
  444. new_node.position_offset = Vector2(
  445. node_data["offset"].x,
  446. base_y_offset + relative_y
  447. )
  448. # Restore sliders
  449. for child in new_node.get_children():
  450. if child.name in node_data["slider_values"]:
  451. child.value = node_data["slider_values"][child.name]
  452. graph_edit.add_child(new_node, true)
  453. _register_inputs_in_node(new_node) #link sliders for changes tracking
  454. _register_node_movement() # link nodes for changes tracking
  455. name_map[node_data["name"]] = new_node.name
  456. pasted_nodes.append(new_node)
  457. # Step 4: Reconnect new nodes
  458. for conn_data in copied_connections:
  459. var new_from = name_map.get(conn_data["from_node"], null)
  460. var new_to = name_map.get(conn_data["to_node"], null)
  461. if new_from and new_to:
  462. graph_edit.connect_node(new_from, conn_data["from_port"], new_to, conn_data["to_port"])
  463. # Step 5: Select pasted nodes
  464. for pasted_node in pasted_nodes:
  465. graph_edit.set_selected(pasted_node)
  466. selected_nodes[pasted_node] = true
  467. changesmade = true
  468. # Remove node with UndoRedo
  469. undo_redo.create_action("Paste Nodes")
  470. for pasted_node in pasted_nodes:
  471. undo_redo.add_undo_method(Callable(graph_edit, "remove_child").bind(pasted_node))
  472. undo_redo.add_undo_method(Callable(pasted_node, "queue_free"))
  473. undo_redo.add_undo_method(Callable(self, "remove_connections_to_node").bind(pasted_node))
  474. undo_redo.add_undo_method(Callable(self, "_track_changes"))
  475. undo_redo.commit_action()
  476. #functions for tracking changes for save state detection
  477. func _register_inputs_in_node(node: Node):
  478. #tracks input to nodes sliders and codeedit to track if patch is saved
  479. # Track Sliders
  480. for slider in node.find_children("*", "HSlider", true, false):
  481. # Create a Callable to the correct method
  482. var callable = Callable(self, "_on_any_slider_changed")
  483. # Check if it's already connected, and connect if not
  484. if not slider.is_connected("value_changed", callable):
  485. slider.connect("value_changed", callable)
  486. for slider in node.find_children("*", "VBoxContainer", true, false):
  487. # Also connect to meta_changed if the slider has that signal
  488. if slider.has_signal("meta_changed"):
  489. var meta_callable = Callable(self, "_on_any_slider_meta_changed")
  490. if not slider.is_connected("meta_changed", meta_callable):
  491. slider.connect("meta_changed", meta_callable)
  492. # Track CodeEdits
  493. for editor in node.find_children("*", "CodeEdit", true, false):
  494. var callable = Callable(self, "_on_any_input_changed")
  495. if not editor.is_connected("text_changed", callable):
  496. editor.connect("text_changed", callable)
  497. func _on_any_slider_meta_changed():
  498. changesmade = true
  499. print("Meta changed in slider")
  500. func _register_node_movement():
  501. for graphnode in graph_edit.get_children():
  502. if graphnode is GraphNode:
  503. var callable = Callable(self, "_on_graphnode_moved")
  504. if not graphnode.is_connected("position_offset_changed", callable):
  505. graphnode.connect("position_offset_changed", callable)
  506. func _on_graphnode_moved():
  507. changesmade = true
  508. func _on_any_slider_changed(value: float) -> void:
  509. changesmade = true
  510. func _on_any_input_changed():
  511. changesmade = true
  512. func _track_changes():
  513. changesmade = true
  514. func _run_process() -> void:
  515. if Global.infile == "no_file":
  516. $NoInputPopup.show()
  517. else:
  518. if foldertoggle.button_pressed == true and lastoutputfolder != "none":
  519. _on_file_dialog_dir_selected(lastoutputfolder)
  520. else:
  521. $FileDialog.show()
  522. func _on_file_dialog_dir_selected(dir: String) -> void:
  523. lastoutputfolder = dir
  524. console_output.clear()
  525. if $Console.is_visible():
  526. $Console.hide()
  527. await get_tree().process_frame # Wait a frame to allow hide to complete
  528. $Console.popup()
  529. else:
  530. $Console.popup()
  531. await get_tree().process_frame
  532. log_console("Generating processing queue", true)
  533. await get_tree().process_frame
  534. #get the current time in hh-mm-ss format as default : causes file name issues
  535. var time_dict = Time.get_time_dict_from_system()
  536. # Pad with zeros to ensure two digits for hour, minute, second
  537. var hour = str(time_dict.hour).pad_zeros(2)
  538. var minute = str(time_dict.minute).pad_zeros(2)
  539. var second = str(time_dict.second).pad_zeros(2)
  540. var time_str = hour + "-" + minute + "-" + second
  541. Global.outfile = dir + "/" + outfilename.text.get_basename() + "_" + Time.get_date_string_from_system() + "_" + time_str
  542. log_console("Output directory and file name(s):" + Global.outfile, true)
  543. await get_tree().process_frame
  544. run_thread_with_branches()
  545. func run_thread_with_branches():
  546. # Detect platform: Determine if the OS is Windows
  547. var is_windows := OS.get_name() == "Windows"
  548. # Choose appropriate commands based on OS
  549. var delete_cmd = "del" if is_windows else "rm"
  550. var rename_cmd = "ren" if is_windows else "mv"
  551. var path_sep := "/" # Always use forward slash for paths
  552. # Get all node connections in the GraphEdit
  553. var connections = graph_edit.get_connection_list()
  554. # Prepare data structures for graph traversal
  555. var graph = {} # forward adjacency list
  556. var reverse_graph = {} # reverse adjacency list (for input lookup)
  557. var indegree = {} # used for topological sort
  558. var all_nodes = {} # map of node name -> GraphNode reference
  559. log_console("Mapping thread.", true)
  560. await get_tree().process_frame # Let UI update
  561. #Step 0: check thread is valid
  562. var is_valid = path_exists_through_all_nodes()
  563. if is_valid == false:
  564. log_console("[color=#9c2828][b]Error: Valid Thread not found[/b][/color]", true)
  565. log_console("Threads must contain at least one processing node and a valid path from the Input File to the Output File.", true)
  566. await get_tree().process_frame # Let UI update
  567. return
  568. else:
  569. log_console("[color=#638382][b]Valid Thread found[/b][/color]", true)
  570. await get_tree().process_frame # Let UI update
  571. # Step 1: Gather nodes from the GraphEdit
  572. for child in graph_edit.get_children():
  573. if child is GraphNode:
  574. var name = str(child.name)
  575. all_nodes[name] = child
  576. if not child.has_meta("utility"):
  577. graph[name] = []
  578. reverse_graph[name] = []
  579. indegree[name] = 0 # Start with zero incoming edges
  580. # Step 2: Build graph relationships from connections
  581. for conn in connections:
  582. var from = str(conn["from_node"])
  583. var to = str(conn["to_node"])
  584. if graph.has(from) and graph.has(to):
  585. graph[from].append(to)
  586. reverse_graph[to].append(from)
  587. indegree[to] += 1 # Count incoming edges
  588. # Step 3: Topological sort to get execution order
  589. var sorted = [] # Sorted list of node names
  590. var queue = [] # Queue of nodes with 0 indegree
  591. for node in graph.keys():
  592. if indegree[node] == 0:
  593. queue.append(node)
  594. while not queue.is_empty():
  595. var current = queue.pop_front()
  596. sorted.append(current)
  597. for neighbor in graph[current]:
  598. indegree[neighbor] -= 1
  599. if indegree[neighbor] == 0:
  600. queue.append(neighbor)
  601. # If not all nodes were processed, there's a cycle
  602. if sorted.size() != graph.size():
  603. log_console("[color=#9c2828][b]Error: Thread not valid[/b][/color]", true)
  604. log_console("Threads cannot contain loops.", true)
  605. return
  606. # Step 4: Start processing audio
  607. var batch_lines = [] # Holds all batch file commands
  608. var intermediate_files = [] # Files to delete later
  609. var breakfiles = [] #breakfiles to delete later
  610. # Dictionary to keep track of each node's output file
  611. var output_files = {}
  612. var process_count = 0
  613. # Start with the original input file
  614. var starting_infile = Global.infile
  615. #If trim is enabled trim input audio
  616. if Global.trim_infile == true:
  617. run_command(cdpprogs_location + "/sfedit cut 1" + " \"%s\"" % starting_infile + " \"%s_trimmed.wav\"" % Global.outfile + " " + str(Global.infile_start) + " " + str(Global.infile_stop))
  618. starting_infile = Global.outfile + "_trimmed.wav"
  619. # Mark trimmed file for cleanup if needed
  620. if delete_intermediate_outputs:
  621. intermediate_files.append(Global.outfile + "_trimmed.wav")
  622. var current_infile = starting_infile
  623. # Iterate over the processing nodes in topological order
  624. for node_name in sorted:
  625. var node = all_nodes[node_name]
  626. # Find upstream nodes connected to the current node
  627. var inputs = reverse_graph[node_name]
  628. var input_files = []
  629. for input_node in inputs:
  630. input_files.append(output_files[input_node])
  631. # Merge inputs if this node has more than one input
  632. if input_files.size() > 1:
  633. # Prepare final merge output file name
  634. var runmerge = merge_many_files(process_count, input_files)
  635. var merge_output = runmerge[0]
  636. var converted_files = runmerge[1]
  637. # Track the output and intermediate files
  638. current_infile = merge_output
  639. if delete_intermediate_outputs:
  640. intermediate_files.append(merge_output)
  641. for f in converted_files:
  642. intermediate_files.append(f)
  643. # If only one input, use that
  644. elif input_files.size() == 1:
  645. current_infile = input_files[0]
  646. ## If no input, use the original input file
  647. else:
  648. current_infile = starting_infile
  649. # Build the command for the current node's audio processing
  650. var slider_data = _get_slider_values_ordered(node)
  651. if node.get_slot_type_right(0) == 1: #detect if process outputs pvoc data
  652. if typeof(current_infile) == TYPE_ARRAY:
  653. #check if infile is an array meaning that the last pvoc process was run in dual mono mode
  654. # Process left and right seperately
  655. var pvoc_stereo_files = []
  656. for infile in current_infile:
  657. var makeprocess = make_process(node, process_count, infile, slider_data)
  658. # run the command
  659. run_command(makeprocess[0])
  660. await get_tree().process_frame
  661. var output_file = makeprocess[1]
  662. pvoc_stereo_files.append(output_file)
  663. # Mark file for cleanup if needed
  664. if delete_intermediate_outputs:
  665. for file in makeprocess[2]:
  666. breakfiles.append(file)
  667. intermediate_files.append(output_file)
  668. process_count += 1
  669. output_files[node_name] = pvoc_stereo_files
  670. else:
  671. var input_stereo = is_stereo(current_infile)
  672. if input_stereo == true:
  673. #audio file is stereo and needs to be split for pvoc processing
  674. var pvoc_stereo_files = []
  675. ##Split stereo to c1/c2
  676. run_command(cdpprogs_location + "/housekeep chans 2 " + "\"%s\"" % current_infile)
  677. # Process left and right seperately
  678. for channel in ["c1", "c2"]:
  679. var dual_mono_file = current_infile.get_basename() + "_%s.wav" % channel
  680. var makeprocess = make_process(node, process_count, dual_mono_file, slider_data)
  681. # run the command
  682. run_command(makeprocess[0])
  683. await get_tree().process_frame
  684. var output_file = makeprocess[1]
  685. pvoc_stereo_files.append(output_file)
  686. # Mark file for cleanup if needed
  687. if delete_intermediate_outputs:
  688. for file in makeprocess[2]:
  689. breakfiles.append(file)
  690. intermediate_files.append(output_file)
  691. #Delete c1 and c2 because they can be in the wrong folder and if the same infile is used more than once
  692. #with this stereo process CDP will throw errors in the console even though its fine
  693. if is_windows:
  694. dual_mono_file = dual_mono_file.replace("/", "\\")
  695. run_command("%s \"%s\"" % [delete_cmd, dual_mono_file])
  696. process_count += 1
  697. # Store output file path for this node
  698. output_files[node_name] = pvoc_stereo_files
  699. else:
  700. #input file is mono run through process
  701. var makeprocess = make_process(node, process_count, current_infile, slider_data)
  702. # run the command
  703. run_command(makeprocess[0])
  704. await get_tree().process_frame
  705. var output_file = makeprocess[1]
  706. # Store output file path for this node
  707. output_files[node_name] = output_file
  708. # Mark file for cleanup if needed
  709. if delete_intermediate_outputs:
  710. for file in makeprocess[2]:
  711. breakfiles.append(file)
  712. intermediate_files.append(output_file)
  713. # Increase the process step count
  714. process_count += 1
  715. else:
  716. #Process outputs audio
  717. #check if this is the last pvoc process in a stereo processing chain
  718. if node.get_meta("command") == "pvoc_synth" and typeof(current_infile) == TYPE_ARRAY:
  719. #check if infile is an array meaning that the last pvoc process was run in dual mono mode
  720. # Process left and right seperately
  721. var pvoc_stereo_files = []
  722. for infile in current_infile:
  723. var makeprocess = make_process(node, process_count, infile, slider_data)
  724. # run the command
  725. run_command(makeprocess[0])
  726. await get_tree().process_frame
  727. var output_file = makeprocess[1]
  728. pvoc_stereo_files.append(output_file)
  729. # Mark file for cleanup if needed
  730. if delete_intermediate_outputs:
  731. for file in makeprocess[2]:
  732. breakfiles.append(file)
  733. intermediate_files.append(output_file)
  734. process_count += 1
  735. #interleave left and right
  736. var output_file = Global.outfile.get_basename() + str(process_count) + "_interleaved.wav"
  737. run_command(cdpprogs_location + "/submix interleave" + " \"%s\"" % pvoc_stereo_files[0] + " \"%s\"" % pvoc_stereo_files[1] + " \"%s\"" % output_file)
  738. # Store output file path for this node
  739. output_files[node_name] = output_file
  740. # Mark file for cleanup if needed
  741. if delete_intermediate_outputs:
  742. intermediate_files.append(output_file)
  743. else:
  744. #Detect if input file is mono or stereo
  745. var input_stereo = is_stereo(current_infile)
  746. if input_stereo == true:
  747. if node.get_meta("stereo_input") == true: #audio file is stereo and process is stereo, run file through process
  748. var makeprocess = make_process(node, process_count, current_infile, slider_data)
  749. # run the command
  750. run_command(makeprocess[0])
  751. await get_tree().process_frame
  752. var output_file = makeprocess[1]
  753. # Store output file path for this node
  754. output_files[node_name] = output_file
  755. # Mark file for cleanup if needed
  756. if delete_intermediate_outputs:
  757. for file in makeprocess[2]:
  758. breakfiles.append(file)
  759. intermediate_files.append(output_file)
  760. else: #audio file is stereo and process is mono, split stereo, process and recombine
  761. ##Split stereo to c1/c2
  762. run_command(cdpprogs_location + "/housekeep chans 2 " + "\"%s\"" % current_infile)
  763. # Process left and right seperately
  764. var dual_mono_output = []
  765. for channel in ["c1", "c2"]:
  766. var dual_mono_file = current_infile.get_basename() + "_%s.wav" % channel
  767. var makeprocess = make_process(node, process_count, dual_mono_file, slider_data)
  768. # run the command
  769. run_command(makeprocess[0])
  770. await get_tree().process_frame
  771. var output_file = makeprocess[1]
  772. dual_mono_output.append(output_file)
  773. # Mark file for cleanup if needed
  774. if delete_intermediate_outputs:
  775. for file in makeprocess[2]:
  776. breakfiles.append(file)
  777. intermediate_files.append(output_file)
  778. #Delete c1 and c2 because they can be in the wrong folder and if the same infile is used more than once
  779. #with this stereo process CDP will throw errors in the console even though its fine
  780. if is_windows:
  781. dual_mono_file = dual_mono_file.replace("/", "\\")
  782. run_command("%s \"%s\"" % [delete_cmd, dual_mono_file])
  783. process_count += 1
  784. var output_file = Global.outfile.get_basename() + str(process_count) + "_interleaved.wav"
  785. run_command(cdpprogs_location + "/submix interleave" + " \"%s\"" % dual_mono_output[0] + " \"%s\"" % dual_mono_output[1] + " \"%s\"" % output_file)
  786. # Store output file path for this node
  787. output_files[node_name] = output_file
  788. # Mark file for cleanup if needed
  789. if delete_intermediate_outputs:
  790. intermediate_files.append(output_file)
  791. else: #audio file is mono, run through the process
  792. var makeprocess = make_process(node, process_count, current_infile, slider_data)
  793. # run the command
  794. run_command(makeprocess[0])
  795. await get_tree().process_frame
  796. var output_file = makeprocess[1]
  797. # Store output file path for this node
  798. output_files[node_name] = output_file
  799. # Mark file for cleanup if needed
  800. if delete_intermediate_outputs:
  801. for file in makeprocess[2]:
  802. breakfiles.append(file)
  803. intermediate_files.append(output_file)
  804. # Increase the process step count
  805. process_count += 1
  806. # FINAL OUTPUT STAGE
  807. # Collect all nodes that are connected to the outputfile node
  808. var output_inputs := []
  809. for conn in connections:
  810. var to_node = str(conn["to_node"])
  811. if all_nodes.has(to_node) and all_nodes[to_node].get_meta("command") == "outputfile":
  812. output_inputs.append(str(conn["from_node"]))
  813. # List to hold the final output files to be merged (if needed)
  814. var final_outputs := []
  815. for node_name in output_inputs:
  816. if output_files.has(node_name):
  817. final_outputs.append(output_files[node_name])
  818. # If multiple outputs go to the outputfile node, merge them
  819. if final_outputs.size() > 1:
  820. var runmerge = merge_many_files(process_count, final_outputs)
  821. final_output_dir = runmerge[0]
  822. var converted_files = runmerge[1]
  823. if delete_intermediate_outputs:
  824. for f in converted_files:
  825. intermediate_files.append(f)
  826. # Only one output, no merge needed
  827. elif final_outputs.size() == 1:
  828. var single_output = final_outputs[0]
  829. final_output_dir = single_output
  830. intermediate_files.erase(single_output)
  831. # CLEANUP: Delete intermediate files after processing and rename final output
  832. log_console("Cleaning up intermediate files.", true)
  833. for file_path in intermediate_files:
  834. # Adjust file path format for Windows if needed
  835. var fixed_path = file_path
  836. if is_windows:
  837. fixed_path = fixed_path.replace("/", "\\")
  838. run_command("%s \"%s\"" % [delete_cmd, fixed_path])
  839. await get_tree().process_frame
  840. #delete break files
  841. for file_path in breakfiles:
  842. # Adjust file path format for Windows if needed
  843. var fixed_path = file_path
  844. if is_windows:
  845. fixed_path = fixed_path.replace("/", "\\")
  846. run_command("%s \"%s\"" % [delete_cmd, fixed_path])
  847. await get_tree().process_frame
  848. var final_filename = "\"%s.wav\"" % Global.outfile
  849. var final_output_dir_fixed_path = final_output_dir
  850. if is_windows:
  851. final_output_dir_fixed_path = final_output_dir_fixed_path.replace("/", "\\")
  852. run_command(rename_cmd + " \"%s\"" % final_output_dir_fixed_path + " " + final_filename.get_file())
  853. else:
  854. run_command(rename_cmd + " \"%s\"" % final_output_dir_fixed_path + " \"%s.wav\"" % Global.outfile)
  855. final_output_dir = Global.outfile + ".wav"
  856. output_audio_player.play_outfile(final_output_dir)
  857. outfile = final_output_dir
  858. var interface_settings = ConfigHandler.load_interface_settings() #checks if close console is enabled and closes console on a success
  859. if interface_settings.auto_close_console and process_successful == true:
  860. $Console.hide()
  861. func is_stereo(file: String) -> bool:
  862. var output = run_command(cdpprogs_location + "/sfprops -c " + "\"%s\"" % file)
  863. output = int(output[0].strip_edges()) #convert output from cmd to clean int
  864. if output == 1:
  865. return false
  866. elif output == 2:
  867. return true
  868. elif output == 1026: #ignore pvoc .ana files
  869. return false
  870. else:
  871. log_console("[color=#9c2828]Error: Only mono and stereo files are supported[/color]", true)
  872. return false
  873. func merge_many_files(process_count: int, input_files: Array) -> Array:
  874. var merge_output = "%s_merge_%d.wav" % [Global.outfile.get_basename(), process_count]
  875. var converted_files := [] # Track any mono->stereo converted files
  876. var inputs_to_merge := [] # Files to be used in the final merge
  877. var mono_files := []
  878. var stereo_files := []
  879. # STEP 1: Check each file's channel count
  880. for f in input_files:
  881. var output = run_command(cdpprogs_location + "/sfprops -c " + "\"%s\"" % f)
  882. if process_successful == true and output.size() > 0:
  883. var channels = int(output[0].strip_edges())
  884. if channels == 1:
  885. mono_files.append(f)
  886. elif channels == 2:
  887. stereo_files.append(f)
  888. else:
  889. push_error("Unexpected channel count in file: %s" % f)
  890. else:
  891. push_error("Failed to get channel count for file: %s" % f)
  892. # STEP 2: Convert mono to stereo if there is a mix
  893. if mono_files.size() > 0 and stereo_files.size() > 0:
  894. for mono_file in mono_files:
  895. var stereo_file = "%s_stereo.wav" % mono_file.get_basename()
  896. run_command(cdpprogs_location + "/submix interleave" + " \"%s\"" % mono_file + " \"%s\"" % mono_file + " \"%s\"" % stereo_file)
  897. if process_successful == false:
  898. log_console("Failed to interleave mono file: %s" % mono_file, true)
  899. else:
  900. converted_files.append(stereo_file)
  901. inputs_to_merge.append(stereo_file)
  902. # Add existing stereo files
  903. inputs_to_merge += stereo_files
  904. else:
  905. # All mono or all stereo — use input_files directly
  906. inputs_to_merge = input_files.duplicate()
  907. # STEP 3: Merge all input files (converted or original)
  908. var quoted_inputs := []
  909. for f in inputs_to_merge:
  910. quoted_inputs.append("\"%s\"" % f)
  911. run_command(cdpprogs_location + "/submix mergemany " + " ".join(quoted_inputs) + " \"%s\"" % merge_output)
  912. if process_successful == false:
  913. log_console("Failed to to merge files to" + merge_output, true)
  914. return [merge_output, converted_files]
  915. func _get_slider_values_ordered(node: Node) -> Array:
  916. var results := []
  917. for child in node.get_children():
  918. if child is Range:
  919. var flag = child.get_meta("flag") if child.has_meta("flag") else ""
  920. var time
  921. var brk_data = []
  922. var min_slider = child.min_value
  923. var max_slider = child.max_value
  924. if child.has_meta("time"):
  925. time = child.get_meta("time")
  926. else:
  927. time = false
  928. if child.has_meta("brk_data"):
  929. brk_data = child.get_meta("brk_data")
  930. results.append([flag, child.value, time, brk_data, min_slider, max_slider])
  931. elif child.get_child_count() > 0:
  932. var nested := _get_slider_values_ordered(child)
  933. results.append_array(nested)
  934. return results
  935. func make_process(node: Node, process_count: int, current_infile: String, slider_data: Array) -> Array:
  936. # Determine output extension: .wav or .ana based on the node's slot type
  937. var extension = ".wav" if node.get_slot_type_right(0) == 0 else ".ana"
  938. # Construct output filename for this step
  939. var output_file = "%s_%d%s" % [Global.outfile.get_basename(), process_count, extension]
  940. # Get the command name from metadata or default to node name
  941. var command_name = str(node.get_meta("command"))
  942. command_name = command_name.replace("_", " ")
  943. # Start building the command line
  944. var line = "%s/%s \"%s\" \"%s\" " % [cdpprogs_location, command_name, current_infile, output_file]
  945. var cleanup = []
  946. # Append parameter values from the sliders, include flags if present
  947. var slider_count = 0
  948. for entry in slider_data:
  949. var flag = entry[0]
  950. var value = entry[1]
  951. var time = entry[2] #checks if slider is a time percentage slider
  952. var brk_data = entry[3]
  953. var min_slider = entry[4]
  954. var max_slider = entry[5]
  955. if brk_data.size() > 0: #if breakpoint data is present on slider
  956. #Sort all points by time
  957. var sorted_brk_data = []
  958. sorted_brk_data = brk_data.duplicate()
  959. sorted_brk_data.sort_custom(sort_points)
  960. var calculated_brk = []
  961. #get length of input file in seconds
  962. var infile_length = run_command(cdpprogs_location + "/sfprops -d " + "\"%s\"" % current_infile)
  963. infile_length = float(infile_length[0].strip_edges())
  964. #scale values from automation window to the right length for file and correct slider values
  965. #need to check how time is handled in all files that accept it, zigzag is x = outfile position, y = infile position
  966. #if time == true:
  967. #for point in sorted_brk_data:
  968. #var new_x = infile_length * (point.x / 700) #time
  969. #var new_y = infile_length * (remap(point.y, 255, 0, min_slider, max_slider) / 100) #slider value scaled as a percentage of infile time
  970. #calculated_brk.append(Vector2(new_x, new_y))
  971. #else:
  972. for i in range(sorted_brk_data.size()):
  973. var point = sorted_brk_data[i]
  974. var new_x = infile_length * (point.x / 700) #time
  975. if i == sorted_brk_data.size() - 1: #check if this is last automation point
  976. new_x = infile_length + 0.1 # force last point's x to infile_length + 100ms to make sure the file is defo over
  977. var new_y = remap(point.y, 255, 0, min_slider, max_slider) #slider value
  978. calculated_brk.append(Vector2(new_x, new_y))
  979. #make text file
  980. var brk_file_path = output_file.get_basename() + "_" + str(slider_count) + ".txt"
  981. write_breakfile(calculated_brk, brk_file_path)
  982. #append text file in place of value
  983. line += ("\"%s\" " % brk_file_path)
  984. cleanup.append(brk_file_path)
  985. else:
  986. if time == true:
  987. var infile_length = run_command(cdpprogs_location + "/sfprops -d " + "\"%s\"" % current_infile)
  988. infile_length = float(infile_length[0].strip_edges())
  989. value = infile_length * (value / 100) #calculate percentage time of the input file
  990. line += ("%s%.2f " % [flag, value]) if flag.begins_with("-") else ("%.2f " % value)
  991. slider_count += 1
  992. return [line.strip_edges(), output_file, cleanup]
  993. func sort_points(a, b):
  994. return a.x < b.x
  995. func write_breakfile(points: Array, path: String):
  996. var file = FileAccess.open(path, FileAccess.WRITE)
  997. if file:
  998. for point in points:
  999. var line = str(point.x) + " " + str(point.y) + "\n"
  1000. file.store_string(line)
  1001. file.close()
  1002. else:
  1003. print("Failed to open file for writing.")
  1004. func run_command(command: String) -> Array:
  1005. var is_windows = OS.get_name() == "Windows"
  1006. var output: Array = []
  1007. var error: Array = []
  1008. var exit_code := 0
  1009. if is_windows:
  1010. exit_code = OS.execute("cmd.exe", ["/C", command], output, true, false)
  1011. else:
  1012. exit_code = OS.execute("sh", [command], output, true, false)
  1013. var output_str := ""
  1014. for item in output:
  1015. output_str += item + "\n"
  1016. var error_str := ""
  1017. for item in error:
  1018. error_str += item + "\n"
  1019. console_output.append_text(command + "\n")
  1020. console_output.scroll_to_line(console_output.get_line_count() - 1)
  1021. if exit_code == 0:
  1022. if output_str.contains("ERROR:"): #checks if CDP reported an error but passed exit code 0 anyway
  1023. console_output.append_text("[color=#9c2828][b]Processes failed[/b][/color]\n")
  1024. console_output.scroll_to_line(console_output.get_line_count() - 1)
  1025. console_output.append_text(output_str + "\n")
  1026. process_successful = false
  1027. else:
  1028. console_output.append_text("[color=#638382]Processes ran successfully[/color]\n")
  1029. console_output.scroll_to_line(console_output.get_line_count() - 1)
  1030. console_output.append_text(output_str + "\n")
  1031. process_successful = true
  1032. else:
  1033. console_output.append_text("[color=#9c2828][b]Processes failed with exit code: %d[/b][/color]\n" % exit_code)
  1034. console_output.scroll_to_line(console_output.get_line_count() - 1)
  1035. console_output.append_text(output_str + "\n")
  1036. console_output.append_text(error_str + "\n")
  1037. if output_str.contains("as an internal or external command"): #check for cdprogs location error on windows
  1038. console_output.append_text("[color=#9c2828][b]Please make sure your cdprogs folder is set to the correct location in the Settings menu. The default location is C:\\CDPR8\\_cdp\\_cdprogs[/b][/color]\n")
  1039. if output_str.contains("command not found"): #check for cdprogs location error on unix systems
  1040. console_output.append_text("[color=#9c2828][b]Please make sure your cdprogs folder is set to the correct location in the Settings menu. The default location is ~/cdpr8/_cdp/_cdprogs[/b][/color]\n")
  1041. process_successful = false
  1042. return output
  1043. func path_exists_through_all_nodes() -> bool:
  1044. var all_nodes = {}
  1045. var graph = {}
  1046. var input_node_name = ""
  1047. var output_node_name = ""
  1048. # Gather all relevant nodes
  1049. for child in graph_edit.get_children():
  1050. if child is GraphNode:
  1051. var name = str(child.name)
  1052. all_nodes[name] = child
  1053. var command = child.get_meta("command")
  1054. if command == "inputfile":
  1055. input_node_name = name
  1056. elif command == "outputfile":
  1057. output_node_name = name
  1058. # Skip utility nodes, include others
  1059. if command in ["inputfile", "outputfile"] or not child.has_meta("utility"):
  1060. graph[name] = []
  1061. # Ensure both input and output were found
  1062. if input_node_name == "" or output_node_name == "":
  1063. print("Input or output node not found!")
  1064. return false
  1065. # Add edges to graph from the connection list
  1066. var connection_list = graph_edit.get_connection_list()
  1067. for conn in connection_list:
  1068. var from = str(conn["from_node"])
  1069. var to = str(conn["to_node"])
  1070. if graph.has(from):
  1071. graph[from].append(to)
  1072. # BFS traversal to check path and depth
  1073. var visited = {}
  1074. var queue = [ { "node": input_node_name, "depth": 0 } ]
  1075. var has_intermediate = false
  1076. while queue.size() > 0:
  1077. var current = queue.pop_front()
  1078. var current_node = current["node"]
  1079. var depth = current["depth"]
  1080. if current_node in visited:
  1081. continue
  1082. visited[current_node] = true
  1083. if current_node == output_node_name and depth >= 2:
  1084. has_intermediate = true
  1085. if graph.has(current_node):
  1086. for neighbor in graph[current_node]:
  1087. queue.append({ "node": neighbor, "depth": depth + 1 })
  1088. return has_intermediate
  1089. func _toggle_delete(toggled_on: bool):
  1090. delete_intermediate_outputs = toggled_on
  1091. print(toggled_on)
  1092. func _on_console_close_requested() -> void:
  1093. $Console.hide()
  1094. func log_console(text: String, update: bool) -> void:
  1095. console_output.append_text(text + "\n \n")
  1096. console_output.scroll_to_line(console_output.get_line_count() - 1)
  1097. if update == true:
  1098. await get_tree().process_frame # Optional: ensure UI updates
  1099. func _on_console_open_folder_button_down() -> void:
  1100. $Console.hide()
  1101. OS.shell_open(Global.outfile.get_base_dir())
  1102. func _on_ok_button_2_button_down() -> void:
  1103. $NoInputPopup.hide()
  1104. func _on_ok_button_3_button_down() -> void:
  1105. $MultipleConnectionsPopup.hide()
  1106. func _on_settings_button_index_pressed(index: int) -> void:
  1107. var interface_settings = ConfigHandler.load_interface_settings()
  1108. match index:
  1109. 0:
  1110. $CdpLocationDialog.show()
  1111. 1:
  1112. if interface_settings.disable_pvoc_warning == false:
  1113. $MenuBar/SettingsButton.set_item_checked(index, true)
  1114. ConfigHandler.save_interface_settings("disable_pvoc_warning", true)
  1115. else:
  1116. $MenuBar/SettingsButton.set_item_checked(index, false)
  1117. ConfigHandler.save_interface_settings("disable_pvoc_warning", false)
  1118. 2:
  1119. if interface_settings.auto_close_console == false:
  1120. $MenuBar/SettingsButton.set_item_checked(index, true)
  1121. ConfigHandler.save_interface_settings("auto_close_console", true)
  1122. else:
  1123. $MenuBar/SettingsButton.set_item_checked(index, false)
  1124. ConfigHandler.save_interface_settings("auto_close_console", false)
  1125. 3:
  1126. if interface_settings.console_on_top == false:
  1127. $MenuBar/SettingsButton.set_item_checked(index, true)
  1128. ConfigHandler.save_interface_settings("console_on_top", true)
  1129. $Console.always_on_top = true
  1130. else:
  1131. $MenuBar/SettingsButton.set_item_checked(index, false)
  1132. ConfigHandler.save_interface_settings("console_on_top", false)
  1133. $Console.always_on_top = false
  1134. 4:
  1135. if interface_settings.use_search == false:
  1136. $MenuBar/SettingsButton.set_item_checked(index, true)
  1137. ConfigHandler.save_interface_settings("use_search", true)
  1138. else:
  1139. $MenuBar/SettingsButton.set_item_checked(index, false)
  1140. ConfigHandler.save_interface_settings("use_search", false)
  1141. 5:
  1142. $AudioSettings.popup()
  1143. 6:
  1144. if $Console.is_visible():
  1145. $Console.hide()
  1146. await get_tree().process_frame # Wait a frame to allow hide to complete
  1147. $Console.popup()
  1148. else:
  1149. $Console.popup()
  1150. func _on_file_button_index_pressed(index: int) -> void:
  1151. match index:
  1152. 0:
  1153. if changesmade == true:
  1154. savestate = "newfile"
  1155. $SaveChangesPopup.show()
  1156. else:
  1157. new_patch()
  1158. currentfile = "none" #reset current file to none for save tracking
  1159. 1:
  1160. if currentfile == "none":
  1161. savestate = "saveas"
  1162. $SaveDialog.popup_centered()
  1163. else:
  1164. save_graph_edit(currentfile)
  1165. 2:
  1166. savestate = "saveas"
  1167. $SaveDialog.popup_centered()
  1168. 3:
  1169. if changesmade == true:
  1170. savestate = "load"
  1171. $SaveChangesPopup.show()
  1172. else:
  1173. $LoadDialog.popup_centered()
  1174. func save_graph_edit(path: String):
  1175. var file = FileAccess.open(path, FileAccess.WRITE)
  1176. if file == null:
  1177. print("Failed to open file for saving")
  1178. return
  1179. var node_data_list = []
  1180. var connection_data_list = []
  1181. for node in graph_edit.get_children():
  1182. if node is GraphNode:
  1183. var offset = node.position_offset
  1184. var node_data = {
  1185. "name": node.name,
  1186. "command": node.get_meta("command"),
  1187. "offset": { "x": offset.x, "y": offset.y },
  1188. "slider_values": {},
  1189. "notes":{}
  1190. }
  1191. for child in node.find_children("*", "Slider", true, false):
  1192. var relative_path = node.get_path_to(child)
  1193. var path_str = str(relative_path)
  1194. # Save slider value
  1195. node_data["slider_values"][path_str] = {
  1196. "value": child.value,
  1197. "editable": child.editable,
  1198. "meta": {}
  1199. }
  1200. # Save all metadata
  1201. for key in child.get_meta_list():
  1202. node_data["slider_values"][path_str]["meta"][str(key)] = child.get_meta(key)
  1203. for child in node.find_children("*", "CodeEdit", true, false):
  1204. node_data["notes"][child.name] = child.text
  1205. node_data_list.append(node_data)
  1206. for conn in graph_edit.get_connection_list():
  1207. connection_data_list.append({
  1208. "from_node": conn["from_node"],
  1209. "from_port": conn["from_port"],
  1210. "to_node": conn["to_node"],
  1211. "to_port": conn["to_port"]
  1212. })
  1213. var graph_data = {
  1214. "nodes": node_data_list,
  1215. "connections": connection_data_list
  1216. }
  1217. var json = JSON.new()
  1218. var json_string = json.stringify(graph_data, "\t")
  1219. file.store_string(json_string)
  1220. file.close()
  1221. print("Graph saved.")
  1222. changesmade = false
  1223. get_window().title = "SoundThread - " + path.get_file().trim_suffix(".thd")
  1224. func load_graph_edit(path: String):
  1225. var file = FileAccess.open(path, FileAccess.READ)
  1226. if file == null:
  1227. print("Failed to open file for loading")
  1228. return
  1229. var json_text = file.get_as_text()
  1230. file.close()
  1231. var json = JSON.new()
  1232. if json.parse(json_text) != OK:
  1233. print("Error parsing JSON")
  1234. return
  1235. var graph_data = json.get_data()
  1236. graph_edit.clear_connections()
  1237. for node in graph_edit.get_children():
  1238. if node is GraphNode:
  1239. node.queue_free()
  1240. await get_tree().process_frame # Ensure nodes are cleared
  1241. for node_data in graph_data["nodes"]:
  1242. var command_name = node_data.get("command", "")
  1243. var template = Nodes.get_node_or_null(command_name)
  1244. if not template:
  1245. print("Template not found for command:", command_name)
  1246. continue
  1247. var new_node: GraphNode = template.duplicate()
  1248. new_node.name = node_data["name"]
  1249. new_node.position_offset = Vector2(node_data["offset"]["x"], node_data["offset"]["y"])
  1250. new_node.set_meta("command", command_name)
  1251. graph_edit.add_child(new_node)
  1252. _register_node_movement() #link nodes for tracking position changes for changes tracking
  1253. # Restore sliders
  1254. for slider_path_str in node_data["slider_values"]:
  1255. var slider = new_node.get_node_or_null(slider_path_str)
  1256. if slider and (slider is HSlider or slider is VSlider):
  1257. var slider_info = node_data["slider_values"][slider_path_str]
  1258. if typeof(slider_info) == TYPE_DICTIONARY:
  1259. slider.value = slider_info.get("value", slider.value)
  1260. # Restore enabled/disabled
  1261. if slider_info.has("editable"):
  1262. slider.editable = slider_info["editable"]
  1263. # Restore metadata
  1264. if slider_info.has("meta"):
  1265. for key in slider_info["meta"]:
  1266. var value = slider_info["meta"][key]
  1267. # Convert arrays of stringified Vector2s back to real Vector2s
  1268. if key == "brk_data" and typeof(value) == TYPE_ARRAY:
  1269. var new_array: Array = []
  1270. for item in value:
  1271. if typeof(item) == TYPE_STRING:
  1272. # Extract values from "(x, y)"
  1273. var numbers: PackedStringArray = item.strip_edges().trim_prefix("(").trim_suffix(")").split(",")
  1274. if numbers.size() == 2:
  1275. var x = float(numbers[0])
  1276. var y = float(numbers[1])
  1277. new_array.append(Vector2(x, y))
  1278. value = new_array
  1279. slider.set_meta(key, value)
  1280. else:
  1281. # Legacy support: just value
  1282. slider.value = slider_info
  1283. # Restore notes
  1284. for codeedit_name in node_data["notes"]:
  1285. var codeedit = new_node.find_child(codeedit_name, true, false)
  1286. if codeedit and (codeedit is CodeEdit):
  1287. codeedit.text = node_data["notes"][codeedit_name]
  1288. _register_inputs_in_node(new_node) #link sliders for changes tracking
  1289. # Restore connections
  1290. for conn in graph_data["connections"]:
  1291. graph_edit.connect_node(
  1292. conn["from_node"], conn["from_port"],
  1293. conn["to_node"], conn["to_port"]
  1294. )
  1295. link_output()
  1296. print("Graph loaded.")
  1297. get_window().title = "SoundThread - " + path.get_file().trim_suffix(".thd")
  1298. func _on_save_dialog_file_selected(path: String) -> void:
  1299. save_graph_edit(path) #save file
  1300. #check what the user was trying to do before save and do that action
  1301. if savestate == "newfile":
  1302. new_patch()
  1303. currentfile = "none" #reset current file to none for save tracking
  1304. elif savestate == "load":
  1305. $LoadDialog.popup_centered()
  1306. elif savestate == "helpfile":
  1307. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1308. load_graph_edit(helpfile)
  1309. elif savestate == "quit":
  1310. await get_tree().create_timer(0.25).timeout #little pause so that it feels like it actually saved even though it did
  1311. get_tree().quit()
  1312. savestate = "none" #reset save state, not really needed but feels good
  1313. func _on_load_dialog_file_selected(path: String) -> void:
  1314. currentfile = path #tracking path here only means "save" only saves patches the user has loaded rather than overwriting help files
  1315. load_graph_edit(path)
  1316. func _on_help_button_index_pressed(index: int) -> void:
  1317. match index:
  1318. 0:
  1319. pass
  1320. 1:
  1321. if changesmade == true:
  1322. savestate = "helpfile"
  1323. helpfile = "res://examples/getting_started.thd"
  1324. $SaveChangesPopup.show()
  1325. else:
  1326. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1327. load_graph_edit("res://examples/getting_started.thd")
  1328. 2:
  1329. if changesmade == true:
  1330. savestate = "helpfile"
  1331. helpfile = "res://examples/navigating.thd"
  1332. $SaveChangesPopup.show()
  1333. else:
  1334. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1335. load_graph_edit("res://examples/navigating.thd")
  1336. 3:
  1337. if changesmade == true:
  1338. savestate = "helpfile"
  1339. helpfile = "res://examples/building_a_thread.thd"
  1340. $SaveChangesPopup.show()
  1341. else:
  1342. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1343. load_graph_edit("res://examples/building_a_thread.thd")
  1344. 4:
  1345. if changesmade == true:
  1346. savestate = "helpfile"
  1347. helpfile = "res://examples/frequency_domain.thd"
  1348. $SaveChangesPopup.show()
  1349. else:
  1350. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1351. load_graph_edit("res://examples/frequency_domain.thd")
  1352. 5:
  1353. if changesmade == true:
  1354. savestate = "helpfile"
  1355. helpfile = "res://examples/automation.thd"
  1356. $SaveChangesPopup.show()
  1357. else:
  1358. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1359. load_graph_edit("res://examples/automation.thd")
  1360. 6:
  1361. if changesmade == true:
  1362. savestate = "helpfile"
  1363. helpfile = "res://examples/trimming.thd"
  1364. $SaveChangesPopup.show()
  1365. else:
  1366. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1367. load_graph_edit("res://examples/trimming.thd")
  1368. 7:
  1369. pass
  1370. 8:
  1371. if changesmade == true:
  1372. savestate = "helpfile"
  1373. helpfile = "res://examples/wetdry.thd"
  1374. $SaveChangesPopup.show()
  1375. else:
  1376. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1377. load_graph_edit("res://examples/wetdry.thd")
  1378. 9:
  1379. pass
  1380. 10:
  1381. OS.shell_open("https://www.composersdesktop.com/docs/html/ccdpndex.htm")
  1382. 11:
  1383. OS.shell_open("https://github.com/j-p-higgins/SoundThread/issues")
  1384. func _recycle_outfile():
  1385. if outfile != "no file":
  1386. input_audio_player.recycle_outfile(outfile)
  1387. func _on_save_changes_button_down() -> void:
  1388. $SaveChangesPopup.hide()
  1389. if currentfile == "none":
  1390. $SaveDialog.show()
  1391. else:
  1392. save_graph_edit(currentfile)
  1393. if savestate == "newfile":
  1394. new_patch()
  1395. currentfile = "none" #reset current file to none for save tracking
  1396. elif savestate == "load":
  1397. $LoadDialog.popup_centered()
  1398. elif savestate == "helpfile":
  1399. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1400. load_graph_edit(helpfile)
  1401. elif savestate == "quit":
  1402. await get_tree().create_timer(0.25).timeout #little pause so that it feels like it actually saved even though it did
  1403. get_tree().quit()
  1404. savestate = "none"
  1405. func _on_dont_save_changes_button_down() -> void:
  1406. $SaveChangesPopup.hide()
  1407. if savestate == "newfile":
  1408. new_patch()
  1409. currentfile = "none" #reset current file to none for save tracking
  1410. elif savestate == "load":
  1411. $LoadDialog.popup_centered()
  1412. elif savestate == "helpfile":
  1413. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  1414. load_graph_edit(helpfile)
  1415. elif savestate == "quit":
  1416. get_tree().quit()
  1417. savestate = "none"
  1418. func _notification(what):
  1419. if what == NOTIFICATION_WM_CLOSE_REQUEST:
  1420. $Console.hide()
  1421. if changesmade == true:
  1422. savestate = "quit"
  1423. $SaveChangesPopup.show()
  1424. #$HelpWindow.hide()
  1425. else:
  1426. get_tree().quit() # default behavior
  1427. func _open_output_folder():
  1428. if lastoutputfolder != "none":
  1429. OS.shell_open(lastoutputfolder)
  1430. func _on_rich_text_label_meta_clicked(meta: Variant) -> void:
  1431. print(str(meta))
  1432. OS.shell_open(str(meta))
  1433. func _on_graph_edit_popup_request(at_position: Vector2) -> void:
  1434. var interface_settings = ConfigHandler.load_interface_settings()
  1435. effect_position = graph_edit.get_local_mouse_position()
  1436. if interface_settings.use_search == false:
  1437. #get the mouse position in screen coordinates
  1438. var mouse_screen_pos = DisplayServer.mouse_get_position()
  1439. #get the window position in screen coordinates
  1440. var window_screen_pos = get_window().position
  1441. #get the window size relative to its scaling for retina displays
  1442. var window_size = get_window().size * DisplayServer.screen_get_scale()
  1443. #get the size of the popup menu
  1444. var popup_size = $mainmenu.size
  1445. #calculate the xy position of the mouse clamped to the size of the window and menu so it doesn't go off the screen
  1446. var clamped_x = clamp(mouse_screen_pos.x, window_screen_pos.x, window_screen_pos.x + window_size.x - popup_size.x)
  1447. var clamped_y = clamp(mouse_screen_pos.y, window_screen_pos.y, window_screen_pos.y + window_size.y - popup_size.y)
  1448. #position and show the menu
  1449. $mainmenu.position = Vector2(clamped_x, clamped_y)
  1450. $mainmenu.popup()
  1451. else:
  1452. #get the mouse position in screen coordinates
  1453. var mouse_screen_pos = DisplayServer.mouse_get_position()
  1454. #get the window position in screen coordinates
  1455. var window_screen_pos = get_window().position
  1456. #get the window size relative to its scaling for retina displays
  1457. var window_size = get_window().size * DisplayServer.screen_get_scale()
  1458. #calculate the xy position of the mouse clamped to the size of the window and menu so it doesn't go off the screen
  1459. var clamped_x = clamp(mouse_screen_pos.x, window_screen_pos.x, window_screen_pos.x + window_size.x - $SearchMenu.size.x)
  1460. var clamped_y = clamp(mouse_screen_pos.y, window_screen_pos.y, window_screen_pos.y + window_size.y - (420 * DisplayServer.screen_get_scale()))
  1461. #position and show the menu
  1462. $SearchMenu.position = Vector2(clamped_x, clamped_y)
  1463. $SearchMenu.popup()
  1464. func _on_audio_settings_close_requested() -> void:
  1465. $AudioSettings.hide()
  1466. func _on_open_audio_settings_button_down() -> void:
  1467. $AudioDevicePopup.hide()
  1468. $AudioSettings.popup()
  1469. func _on_audio_device_popup_close_requested() -> void:
  1470. $AudioDevicePopup.hide()
  1471. func _on_mainmenu_close_requested() -> void:
  1472. #closes menu if click is anywhere other than the menu as it is a window with popup set to true
  1473. $mainmenu.hide()