control.gd 66 KB

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