control.gd 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891
  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 cdpprogs_location #stores the cdp programs location from user prefs for easy access
  6. var delete_intermediate_outputs # tracks state of delete intermediate outputs toggle
  7. @onready var console_output: RichTextLabel = $Console/ConsoleOutput
  8. var undo_redo := UndoRedo.new()
  9. var output_audio_player #tracks the node that is the current output player for linking
  10. var input_audio_player #potentially unused, remove? tracks node that is the current input player for linking
  11. var currentfile = "none" #tracks dir of currently loaded file for saving
  12. var changesmade = false #tracks if user has made changes to the currently loaded save file
  13. var savestate # tracks what the user is trying to do when savechangespopup is called
  14. var helpfile #tracks which help file the user was trying to load when savechangespopup is called
  15. var outfilename #links to the user name for outputfile field
  16. var foldertoggle #links to the reuse folder button
  17. #var lastoutputfolder = "none" #tracks last output folder, this can in future be used to replace global.outfile but i cba right now
  18. var uiscale = 0.0 # tracks the overal ui scale after hidpi adjustment and user offset
  19. var retina_scaling = 1.0 #tracks scaling for retina screens
  20. var use_anyway #used to store the folder selected for cdprogs when it appears the wrong folder is selected but the user wants to use it anyway
  21. var main_theme = preload("res://theme/main_theme.tres") #load the theme
  22. var default_input_node #stores a reference to the input node created on launch to allow auto loading a wav file
  23. var output_folder_label
  24. @onready var fft_size_option_button = $FFTSize
  25. @onready var fft_overlap_option_button = $FFTOverlap
  26. #scripts
  27. var open_help
  28. var run_thread
  29. var save_load
  30. # Called when the node enters the scene tree for the first time.
  31. func _ready() -> void:
  32. $mainmenu.hide()
  33. $NoLocationPopup.hide()
  34. $Console.hide()
  35. $NoInputPopup.hide()
  36. $MultipleConnectionsPopup.hide()
  37. $AudioSettings.hide()
  38. $AudioDevicePopup.hide()
  39. $SearchMenu.hide()
  40. $Settings.hide()
  41. $ProgressWindow.hide()
  42. $WrongFolderPopup.hide()
  43. $SaveChangesPopup.hide()
  44. $SaveDialog.access = FileDialog.ACCESS_FILESYSTEM
  45. $SaveDialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
  46. $SaveDialog.filters = ["*.thd"]
  47. $LoadDialog.access = FileDialog.ACCESS_FILESYSTEM
  48. $LoadDialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
  49. $LoadDialog.filters = ["*.thd"]
  50. undo_redo.max_steps = 40
  51. get_tree().set_auto_accept_quit(false) #disable closing the app with the x and instead handle it internally
  52. load_scripts()
  53. make_signal_connections()
  54. hidpi_adjustment()
  55. check_user_preferences()
  56. new_patch()
  57. await get_tree().process_frame
  58. load_from_filesystem()
  59. check_cdp_location_set()
  60. func load_scripts():
  61. #load and initialise scripts
  62. open_help = preload("res://scenes/main/scripts/open_help.gd").new()
  63. open_help.init(self)
  64. add_child(open_help)
  65. run_thread = preload("res://scenes/main/scripts/run_thread.gd").new()
  66. run_thread.init(self, $ProgressWindow, $ProgressWindow/ProgressLabel, $ProgressWindow/ProgressBar, $GraphEdit, $Console, $Console/ConsoleOutput)
  67. add_child(run_thread)
  68. graph_edit.init(self, $GraphEdit, Callable(open_help, "show_help_for_node"), $MultipleConnectionsPopup)
  69. save_load = preload("res://scenes/main/scripts/save_load.gd").new()
  70. save_load.init(self, $GraphEdit, Callable(open_help, "show_help_for_node"), Callable(graph_edit, "_register_node_movement"), Callable(graph_edit, "_register_inputs_in_node"), Callable(self, "link_output"))
  71. add_child(save_load)
  72. func make_signal_connections():
  73. get_node("SearchMenu").make_node.connect(graph_edit._make_node)
  74. get_node("SearchMenu").swap_node.connect(graph_edit._swap_node)
  75. get_node("SearchMenu").connect_to_clicked_node.connect(graph_edit._connect_to_clicked_node)
  76. get_node("mainmenu").make_node.connect(graph_edit._make_node)
  77. get_node("mainmenu").open_help.connect(open_help.show_help_for_node)
  78. get_node("Settings").open_cdp_location.connect(show_cdp_location)
  79. get_node("Settings").console_on_top.connect(change_console_settings)
  80. get_node("Settings").invert_ui.connect(invert_theme_toggled)
  81. get_node("Settings").swap_zoom_and_move.connect(swap_zoom_and_move)
  82. get_node("Settings").ui_scale_multiplier_changed.connect(scale_ui)
  83. get_window().files_dropped.connect(on_files_dropped)
  84. func hidpi_adjustment():
  85. #checks if display is hidpi and scales ui accordingly hidpi - 144
  86. if DisplayServer.screen_get_dpi(0) >= 144:
  87. retina_scaling = 2.0
  88. else:
  89. retina_scaling = 1.0
  90. func scale_ui(scale_multiplier: float):
  91. var old_uiscale = uiscale
  92. uiscale = retina_scaling * scale_multiplier
  93. get_window().content_scale_factor = uiscale
  94. #goes through popup_windows group and scales all popups and resizes them
  95. for window in get_tree().get_nodes_in_group("popup_windows"):
  96. if old_uiscale != 0: #if ui scale = 0 this is the first time this is being adjusted so no need to revert values back to default first
  97. window.size = (window.size / old_uiscale) * uiscale
  98. else:
  99. window.size = window.size * uiscale
  100. window.content_scale_factor = uiscale
  101. func load_from_filesystem():
  102. #checks if user has opened a file from the system file menu and loads it
  103. var args = OS.get_cmdline_args()
  104. for arg in args:
  105. var path = arg.strip_edges()
  106. if FileAccess.file_exists(path) and path.get_extension().to_lower() == "thd":
  107. save_load.load_graph_edit(path)
  108. break
  109. if FileAccess.file_exists(path) and path.get_extension().to_lower() == "wav":
  110. default_input_node.get_node("AudioPlayer")._on_file_selected(path)
  111. break
  112. func new_patch():
  113. #clear old patch
  114. graph_edit.clear_connections()
  115. for node in graph_edit.get_children():
  116. if node is GraphNode:
  117. node.queue_free()
  118. await get_tree().process_frame # Wait for nodes to actually be removed
  119. undo_redo.clear_history()
  120. graph_edit.scroll_offset = Vector2(0, 0)
  121. #Generate input and output nodes
  122. var effect = Utilities.nodes["inputfile"].instantiate()
  123. effect.name = "inputfile"
  124. get_node("GraphEdit").add_child(effect, true)
  125. effect.connect("open_help", Callable(open_help, "show_help_for_node"))
  126. if effect.has_signal("node_moved"):
  127. effect.node_moved.connect(graph_edit._auto_link_nodes)
  128. effect.dragged.connect(graph_edit.node_position_changed.bind(effect))
  129. effect.position_offset = Vector2(20,80)
  130. default_input_node = effect #store a reference to this node to allow for loading into it directly if software launched with a wav file argument
  131. effect = Utilities.nodes["outputfile"].instantiate()
  132. effect.name = "outputfile"
  133. get_node("GraphEdit").add_child(effect, true)
  134. effect.init() #initialise ui from user prefs
  135. effect.connect("open_help", Callable(open_help, "show_help_for_node"))
  136. if effect.has_signal("node_moved"):
  137. effect.node_moved.connect(graph_edit._auto_link_nodes)
  138. effect.dragged.connect(graph_edit.node_position_changed.bind(effect))
  139. effect.position_offset = Vector2((DisplayServer.screen_get_size().x - 480) / uiscale, 80)
  140. graph_edit._register_node_movement() #link nodes for tracking position changes for changes tracking
  141. #set label for last output folder
  142. var interface_settings = ConfigHandler.load_interface_settings()
  143. output_folder_label = effect.get_node("OutputFolderMargin/OutputFolderLabel")
  144. if output_folder_label != null and interface_settings.last_used_output_folder != "no_file":
  145. output_folder_label.text = interface_settings.last_used_output_folder
  146. output_folder_label.get_parent().tooltip_text = interface_settings.last_used_output_folder
  147. changesmade = false #so it stops trying to save unchanged empty files
  148. get_window().title = "SoundThread"
  149. link_output()
  150. #set fft size and overlap to default
  151. $FFTSize.select(9)
  152. _on_fft_size_item_selected(9)
  153. $FFTOverlap.select(2)
  154. _on_fft_overlap_item_selected(2)
  155. func link_output():
  156. #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
  157. for control in get_tree().get_nodes_in_group("outputnode"): #check all items in outputnode group
  158. if control.has_meta("outputfunction"):
  159. if control.get_meta("outputfunction") == "deleteintermediate": #link delete intermediate files toggle to script
  160. control.toggled.connect(_toggle_delete)
  161. _toggle_delete(control.button_pressed)
  162. #control.button_pressed = interface_settings.get("delete_intermediate", true)
  163. elif control.get_meta("outputfunction") == "runprocess": #link runprocess button
  164. control.button_down.connect(_run_process)
  165. elif control.get_meta("outputfunction") == "audioplayer": #link output audio player
  166. output_audio_player = control
  167. elif control.get_meta("outputfunction") == "filename":
  168. control.text = "outfile"
  169. outfilename = control
  170. elif control.get_meta("outputfunction") == "reusefolder":
  171. foldertoggle = control
  172. #foldertoggle.button_pressed = interface_settings.get("reuse_output_folder", true)
  173. elif control.get_meta("outputfunction") == "openfolder":
  174. control.button_down.connect(_open_output_folder)
  175. #for control in get_tree().get_nodes_in_group("inputnode"):
  176. #if control.get_meta("inputfunction") == "audioplayer": #link input for recycle function
  177. #print("input player found")
  178. #input_audio_player = control
  179. func check_user_preferences():
  180. var interface_settings = ConfigHandler.load_interface_settings()
  181. var audio_settings = ConfigHandler.load_audio_settings()
  182. var audio_devices = AudioServer.get_output_device_list()
  183. $Console.always_on_top = interface_settings.console_on_top
  184. if audio_devices.has(audio_settings.device):
  185. AudioServer.set_output_device(audio_settings.device)
  186. else:
  187. $AudioDevicePopup.popup_centered()
  188. match interface_settings.theme:
  189. 0:
  190. RenderingServer.set_default_clear_color(Color("#2f4f4e"))
  191. 1:
  192. RenderingServer.set_default_clear_color(Color("#000807"))
  193. 2:
  194. RenderingServer.set_default_clear_color(Color("#98d4d2"))
  195. 3:
  196. RenderingServer.set_default_clear_color(Color(interface_settings.theme_custom_colour))
  197. #set the theme to either the main theme or inverted theme depending on user preferences
  198. invert_theme_toggled(interface_settings.invert_theme)
  199. swap_zoom_and_move(interface_settings.swap_zoom_and_move)
  200. #scale ui
  201. scale_ui(interface_settings.ui_scale_multiplier)
  202. func show_cdp_location():
  203. $CdpLocationDialog.show()
  204. func check_cdp_location_set():
  205. #checks if the location has been set and prompts user to set it
  206. var cdpprogs_settings = ConfigHandler.load_cdpprogs_settings()
  207. if cdpprogs_settings.location == "no_location":
  208. $NoLocationPopup.popup_centered()
  209. else:
  210. #if location is set, stores it in a variable
  211. cdpprogs_location = str(cdpprogs_settings.location)
  212. print(cdpprogs_location)
  213. func _on_ok_button_button_down() -> void:
  214. #after user has read dialog on where to find cdp progs this loads the file browser
  215. $NoLocationPopup.hide()
  216. if OS.get_name() == "Windows":
  217. $CdpLocationDialog.current_dir = "C:/"
  218. else:
  219. $CdpLocationDialog.current_dir = OS.get_environment("HOME")
  220. $CdpLocationDialog.show()
  221. func _on_cdp_location_dialog_dir_selected(dir: String) -> void:
  222. var is_windows = OS.get_name() == "Windows"
  223. var cdprogs_correct
  224. #check if the selected folder contains the hilite program as it has a reasonably unique name and will indicate that the CDP processes do exist in that folder
  225. if is_windows:
  226. cdprogs_correct = FileAccess.file_exists(dir + "/distort.exe")
  227. else:
  228. cdprogs_correct = FileAccess.file_exists(dir + "/distort")
  229. if cdprogs_correct:
  230. #if this location does seem to contain cdp programs
  231. #saves default location for cdp programs in config file
  232. ConfigHandler.save_cdpprogs_settings(dir)
  233. cdpprogs_location = dir
  234. else:
  235. #if it doesn't seem to contain the programs then try and extrapolate the correct folder from the one selected
  236. var selected_folder = dir.get_slice("/", (dir.get_slice_count("/") - 1))
  237. print(selected_folder)
  238. if selected_folder.to_lower() == "cdpr8":
  239. dir = dir + "/_cdp/_cdprogs"
  240. #run this function recursively to check if the programs do exist
  241. _on_cdp_location_dialog_dir_selected(dir)
  242. elif selected_folder.to_lower() == "_cdp":
  243. dir = dir + "/_cdprogs"
  244. #run this function recursively to check if the programs do exist
  245. _on_cdp_location_dialog_dir_selected(dir)
  246. else:
  247. #can't find them
  248. use_anyway = dir
  249. $WrongFolderPopup.popup_centered()
  250. func _on_cdp_location_dialog_canceled() -> void:
  251. #cycles around the set location prompt if user cancels the file dialog
  252. check_cdp_location_set()
  253. func _on_select_folder_button_button_down() -> void:
  254. $WrongFolderPopup.hide()
  255. _on_ok_button_button_down()
  256. func _on_use_anyway_button_button_down() -> void:
  257. $WrongFolderPopup.hide()
  258. ConfigHandler.save_cdpprogs_settings(use_anyway)
  259. cdpprogs_location = use_anyway
  260. func _input(event):
  261. if event.is_action_pressed("undo"):
  262. simulate_mouse_click()
  263. await get_tree().process_frame
  264. undo_redo.undo()
  265. elif event.is_action_pressed("redo"):
  266. undo_redo.redo()
  267. elif event.is_action_pressed("save"):
  268. if currentfile == "none":
  269. savestate = "saveas"
  270. $SaveDialog.popup_centered()
  271. else:
  272. save_load.save_graph_edit(currentfile)
  273. elif event.is_action_pressed("open_explore"):
  274. open_explore()
  275. elif event.is_action_pressed("search"):
  276. var pos = graph_edit.get_local_mouse_position()
  277. _on_graph_edit_popup_request(pos)
  278. elif event.is_action_pressed("run_thread"):
  279. _run_process()
  280. elif event.is_action_pressed("new"):
  281. if changesmade == true:
  282. savestate = "newfile"
  283. $SaveChangesPopup.popup_centered()
  284. else:
  285. new_patch()
  286. currentfile = "none" #reset current file to none for save tracking
  287. elif event.is_action_pressed("save_as"):
  288. savestate = "saveas"
  289. $SaveDialog.popup_centered()
  290. func simulate_mouse_click():
  291. #simulates clicking the middle mouse button in order to hide any visible tooltips
  292. var click_pos = get_viewport().get_mouse_position()
  293. var down_event := InputEventMouseButton.new()
  294. down_event.button_index = MOUSE_BUTTON_MIDDLE
  295. down_event.pressed = true
  296. down_event.position = click_pos
  297. Input.parse_input_event(down_event)
  298. var up_event := InputEventMouseButton.new()
  299. up_event.button_index = MOUSE_BUTTON_MIDDLE
  300. up_event.pressed = false
  301. up_event.position = click_pos
  302. Input.parse_input_event(up_event)
  303. func _run_process() -> void:
  304. #check if any of the inputfile nodes don't have files loaded
  305. var interface_settings = ConfigHandler.load_interface_settings()
  306. for node in graph_edit.get_children():
  307. if node.get_meta("command") == "inputfile" and node.get_node("AudioPlayer").has_meta("inputfile") == false:
  308. $NoInputPopup.popup_centered()
  309. return
  310. #check if the reuse folder toggle is set and a folder has been previously chosen
  311. var output_folder = interface_settings.last_used_output_folder
  312. if foldertoggle.button_pressed == true and output_folder != "no_file" and DirAccess.open(output_folder) != null:
  313. _on_file_dialog_dir_selected(output_folder)
  314. else:
  315. $FileDialog.show()
  316. func _on_file_dialog_dir_selected(dir: String) -> void:
  317. ConfigHandler.save_interface_settings("last_used_output_folder", dir)
  318. if output_folder_label != null:
  319. output_folder_label.text = dir
  320. output_folder_label.get_parent().tooltip_text = dir
  321. console_output.clear()
  322. var interface_settings = ConfigHandler.load_interface_settings()
  323. if interface_settings.disable_progress_bar == false:
  324. $ProgressWindow.show()
  325. else:
  326. if $Console.is_visible():
  327. $Console.hide()
  328. await get_tree().process_frame # Wait a frame to allow hide to complete
  329. $Console.popup_centered()
  330. else:
  331. $Console.popup_centered()
  332. await get_tree().process_frame
  333. run_thread.log_console("Generating processing queue", true)
  334. await get_tree().process_frame
  335. #get the current time in hh-mm-ss format as default : causes file name issues
  336. var time_dict = Time.get_time_dict_from_system()
  337. # Pad with zeros to ensure two digits for hour, minute, second
  338. var hour = str(time_dict.hour).pad_zeros(2)
  339. var minute = str(time_dict.minute).pad_zeros(2)
  340. var second = str(time_dict.second).pad_zeros(2)
  341. var time_str = hour + "-" + minute + "-" + second
  342. Global.outfile = dir + "/" + outfilename.text.get_basename() + "_" + Time.get_date_string_from_system() + "_" + time_str
  343. #check path and file name do not contain special characters
  344. var check_file_name = Global.check_for_invalid_chars(Global.outfile)
  345. if check_file_name["contains_invalid_characters"] == false:
  346. run_thread.log_console("Output directory and file name(s):" + Global.outfile, true)
  347. await get_tree().process_frame
  348. run_thread.run_thread_with_branches()
  349. else:
  350. run_thread.log_console("[color=#9c2828][b]Error:[/b][/color] Chosen file name or folder path " + Global.outfile.get_basename() + " contains invalid characters.", true)
  351. run_thread.log_console("File names and paths can only contain A-Z a-z 0-9 - _ + and space.", true)
  352. run_thread.log_console("Chosen file name/path contains the following invalid characters: " + " ".join(check_file_name["invalid_characters_found"]), true)
  353. if $ProgressWindow.visible:
  354. $ProgressWindow.hide()
  355. if !$Console.visible:
  356. $Console.popup_centered()
  357. func _toggle_delete(toggled_on: bool):
  358. delete_intermediate_outputs = toggled_on
  359. print(toggled_on)
  360. func _on_console_close_requested() -> void:
  361. $Console.hide()
  362. func _on_console_open_folder_button_down() -> void:
  363. $Console.hide()
  364. var interface_settings = ConfigHandler.load_interface_settings()
  365. var output_folder = interface_settings.last_used_output_folder
  366. if output_folder != "no_file" and DirAccess.open(output_folder) != null:
  367. OS.shell_open(output_folder)
  368. func _on_ok_button_2_button_down() -> void:
  369. $NoInputPopup.hide()
  370. func _on_ok_button_3_button_down() -> void:
  371. $MultipleConnectionsPopup.hide()
  372. func _on_settings_button_index_pressed(index: int) -> void:
  373. match index:
  374. 0:
  375. $Settings.cdpprogs_location = cdpprogs_location
  376. $Settings.popup_centered()
  377. 1:
  378. $AudioSettings.popup_centered()
  379. 2:
  380. if $Console.is_visible():
  381. $Console.hide()
  382. await get_tree().process_frame # Wait a frame to allow hide to complete
  383. $Console.popup_centered()
  384. else:
  385. $Console.popup_centered()
  386. func _on_file_button_index_pressed(index: int) -> void:
  387. match index:
  388. 0:
  389. if changesmade == true:
  390. savestate = "newfile"
  391. $SaveChangesPopup.popup_centered()
  392. else:
  393. new_patch()
  394. currentfile = "none" #reset current file to none for save tracking
  395. 1:
  396. if currentfile == "none":
  397. savestate = "saveas"
  398. $SaveDialog.popup_centered()
  399. else:
  400. save_load.save_graph_edit(currentfile)
  401. print("save pressed, changes made =")
  402. print(changesmade)
  403. print("current file =")
  404. print(currentfile)
  405. 2:
  406. savestate = "saveas"
  407. $SaveDialog.popup_centered()
  408. 3:
  409. if changesmade == true:
  410. savestate = "load"
  411. $SaveChangesPopup.popup_centered()
  412. else:
  413. $LoadDialog.popup_centered()
  414. func _on_save_dialog_file_selected(path: String) -> void:
  415. save_load.save_graph_edit(path) #save file
  416. #check what the user was trying to do before save and do that action
  417. if savestate == "newfile":
  418. new_patch()
  419. currentfile = "none" #reset current file to none for save tracking
  420. elif savestate == "load":
  421. $LoadDialog.popup_centered()
  422. elif savestate == "helpfile":
  423. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  424. save_load.load_graph_edit(helpfile)
  425. elif savestate == "quit":
  426. undo_redo.free()
  427. await get_tree().create_timer(0.25).timeout #little pause so that it feels like it actually saved even though it did
  428. get_tree().quit()
  429. elif savestate == "saveas":
  430. currentfile = path
  431. savestate = "none" #reset save state, not really needed but feels good
  432. func _on_load_dialog_file_selected(path: String) -> void:
  433. currentfile = path #tracking path here only means "save" only saves patches the user has loaded rather than overwriting help files
  434. save_load.load_graph_edit(path)
  435. func _on_help_button_index_pressed(index: int) -> void:
  436. match index:
  437. 0:
  438. pass
  439. 1:
  440. if changesmade == true:
  441. savestate = "helpfile"
  442. helpfile = "res://examples/getting_started.thd"
  443. $SaveChangesPopup.popup_centered()
  444. else:
  445. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  446. save_load.load_graph_edit("res://examples/getting_started.thd")
  447. 2:
  448. if changesmade == true:
  449. savestate = "helpfile"
  450. helpfile = "res://examples/navigating.thd"
  451. $SaveChangesPopup.popup_centered()
  452. else:
  453. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  454. save_load.load_graph_edit("res://examples/navigating.thd")
  455. 3:
  456. if changesmade == true:
  457. savestate = "helpfile"
  458. helpfile = "res://examples/building_a_thread.thd"
  459. $SaveChangesPopup.popup_centered()
  460. else:
  461. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  462. save_load.load_graph_edit("res://examples/building_a_thread.thd")
  463. 4:
  464. if changesmade == true:
  465. savestate = "helpfile"
  466. helpfile = "res://examples/frequency_domain.thd"
  467. $SaveChangesPopup.popup_centered()
  468. else:
  469. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  470. save_load.load_graph_edit("res://examples/frequency_domain.thd")
  471. 5:
  472. if changesmade == true:
  473. savestate = "helpfile"
  474. helpfile = "res://examples/automation.thd"
  475. $SaveChangesPopup.popup_centered()
  476. else:
  477. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  478. save_load.load_graph_edit("res://examples/automation.thd")
  479. 6:
  480. if changesmade == true:
  481. savestate = "helpfile"
  482. helpfile = "res://examples/trimming.thd"
  483. $SaveChangesPopup.popup_centered()
  484. else:
  485. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  486. save_load.load_graph_edit("res://examples/trimming.thd")
  487. 7:
  488. if changesmade == true:
  489. savestate = "helpfile"
  490. helpfile = "res://examples/multiple_inputs.thd"
  491. $SaveChangesPopup.popup_centered()
  492. else:
  493. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  494. save_load.load_graph_edit("res://examples/multiple_inputs.thd")
  495. 8:
  496. if changesmade == true:
  497. savestate = "helpfile"
  498. helpfile = "res://examples/preview_nodes.thd"
  499. $SaveChangesPopup.popup_centered()
  500. else:
  501. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  502. save_load.load_graph_edit("res://examples/preview_nodes.thd")
  503. 9:
  504. pass
  505. 10:
  506. if changesmade == true:
  507. savestate = "helpfile"
  508. helpfile = "res://examples/wetdry.thd"
  509. $SaveChangesPopup.popup_centered()
  510. else:
  511. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  512. save_load.load_graph_edit("res://examples/wetdry.thd")
  513. 11:
  514. if changesmade == true:
  515. savestate = "helpfile"
  516. helpfile = "res://examples/resonant_filters.thd"
  517. $SaveChangesPopup.popup_centered()
  518. else:
  519. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  520. save_load.load_graph_edit("res://examples/resonant_filters.thd")
  521. 12:
  522. pass
  523. 13:
  524. OS.shell_open("https://www.composersdesktop.com/docs/html/ccdpndex.htm")
  525. 14:
  526. OS.shell_open("https://github.com/j-p-higgins/SoundThread/issues")
  527. #func _recycle_outfile():
  528. #if outfile != "no file":
  529. #input_audio_player.recycle_outfile(outfile)
  530. func _on_save_changes_button_down() -> void:
  531. $SaveChangesPopup.hide()
  532. if currentfile == "none":
  533. $SaveDialog.show()
  534. else:
  535. save_load.save_graph_edit(currentfile)
  536. if savestate == "newfile":
  537. new_patch()
  538. currentfile = "none" #reset current file to none for save tracking
  539. elif savestate == "load":
  540. $LoadDialog.popup_centered()
  541. elif savestate == "helpfile":
  542. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  543. save_load.load_graph_edit(helpfile)
  544. elif savestate == "quit":
  545. undo_redo.free()
  546. await get_tree().create_timer(0.25).timeout #little pause so that it feels like it actually saved even though it did
  547. get_tree().quit()
  548. savestate = "none"
  549. func _on_dont_save_changes_button_down() -> void:
  550. $SaveChangesPopup.hide()
  551. if savestate == "newfile":
  552. new_patch()
  553. currentfile = "none" #reset current file to none for save tracking
  554. elif savestate == "load":
  555. $LoadDialog.popup_centered()
  556. elif savestate == "helpfile":
  557. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  558. save_load.load_graph_edit(helpfile)
  559. elif savestate == "quit":
  560. undo_redo.free()
  561. get_tree().quit()
  562. savestate = "none"
  563. func _on_cancel_changes_button_down() -> void:
  564. $SaveChangesPopup.hide()
  565. savestate = "none"
  566. func _notification(what):
  567. if what == NOTIFICATION_WM_CLOSE_REQUEST:
  568. run_thread._on_kill_process_button_down()
  569. $Console.hide()
  570. if changesmade == true:
  571. savestate = "quit"
  572. $SaveChangesPopup.popup_centered()
  573. #$HelpWindow.hide()
  574. else:
  575. undo_redo.free()
  576. get_tree().quit() # default behavior
  577. func _open_output_folder():
  578. var interface_settings = ConfigHandler.load_interface_settings()
  579. var output_folder = interface_settings.last_used_output_folder
  580. if output_folder != "no_file" and DirAccess.open(output_folder) != null:
  581. OS.shell_open(output_folder)
  582. func _on_rich_text_label_meta_clicked(meta: Variant) -> void:
  583. print(str(meta))
  584. OS.shell_open(str(meta))
  585. func _on_graph_edit_popup_request(at_position: Vector2) -> void:
  586. effect_position = graph_edit.get_local_mouse_position()
  587. #give the search menu the ui scale
  588. $SearchMenu.uiscale = uiscale
  589. #get the mouse position in screen coordinates
  590. var mouse_screen_pos = DisplayServer.mouse_get_position()
  591. #get the window position in screen coordinates
  592. var window_screen_pos = get_window().position
  593. #get the window size relative to its scaling for retina displays
  594. var window_size = get_window().size * DisplayServer.screen_get_scale()
  595. #see if it was empty space or a node that was right clicked
  596. var clicked_position = (effect_position + graph_edit.scroll_offset) / graph_edit.zoom
  597. var clicked_node
  598. for child in graph_edit.get_children():
  599. if child is GraphNode:
  600. if Rect2(child.position_offset, child.size).has_point(clicked_position):
  601. clicked_node = child
  602. break
  603. if clicked_node and clicked_node.get_meta("command") != "outputfile":
  604. var title = clicked_node.title
  605. if Input.is_action_pressed("auto_link_nodes"):
  606. $SearchMenu/VBoxContainer/ReplaceLabel.text = "Connect to " + title
  607. $SearchMenu/VBoxContainer/ReplaceLabel.show()
  608. $SearchMenu.replace_node = false
  609. $SearchMenu.connect_to_node = true
  610. $SearchMenu.node_to_connect_to = clicked_node
  611. else:
  612. $SearchMenu/VBoxContainer/ReplaceLabel.text = "Replace " + title
  613. $SearchMenu/VBoxContainer/ReplaceLabel.show()
  614. $SearchMenu.replace_node = true
  615. $SearchMenu.connect_to_node = false
  616. $SearchMenu.node_to_replace = clicked_node
  617. else:
  618. var interface_settings = ConfigHandler.load_interface_settings()
  619. if interface_settings.right_click_opens_explore:
  620. open_explore()
  621. return
  622. else:
  623. $SearchMenu/VBoxContainer/ReplaceLabel.hide()
  624. $SearchMenu.replace_node = false
  625. $SearchMenu.connect_to_node = false
  626. #calculate the xy position of the mouse clamped to the size of the window and menu so it doesn't go off the screen
  627. var clamped_x = clamp(mouse_screen_pos.x, window_screen_pos.x, window_screen_pos.x + window_size.x - $SearchMenu.size.x)
  628. var clamped_y = clamp(mouse_screen_pos.y, window_screen_pos.y, window_screen_pos.y + window_size.y - (420 * DisplayServer.screen_get_scale()))
  629. #position and show the menu
  630. $SearchMenu.position = Vector2(clamped_x, clamped_y)
  631. $SearchMenu.popup()
  632. func _on_audio_settings_close_requested() -> void:
  633. $AudioSettings.hide()
  634. func _on_open_audio_settings_button_down() -> void:
  635. $AudioDevicePopup.hide()
  636. $AudioSettings.popup_centered()
  637. func _on_audio_device_popup_close_requested() -> void:
  638. $AudioDevicePopup.hide()
  639. func _on_mainmenu_close_requested() -> void:
  640. #closes menu if click is anywhere other than the menu as it is a window with popup set to true
  641. $mainmenu.hide()
  642. func open_explore():
  643. effect_position = graph_edit.get_local_mouse_position()
  644. #get the mouse position in screen coordinates
  645. var mouse_screen_pos = DisplayServer.mouse_get_position()
  646. #get the window position in screen coordinates
  647. var window_screen_pos = get_window().position
  648. #get the window size relative to its scaling for retina displays
  649. var window_size = get_window().size * DisplayServer.screen_get_scale()
  650. #get the size of the popup menu
  651. var popup_size = $mainmenu.size
  652. #calculate the xy position of the mouse clamped to the size of the window and menu so it doesn't go off the screen
  653. var clamped_x = clamp(mouse_screen_pos.x, window_screen_pos.x, window_screen_pos.x + window_size.x - popup_size.x)
  654. var clamped_y = clamp(mouse_screen_pos.y, window_screen_pos.y, window_screen_pos.y + window_size.y - popup_size.y)
  655. #position and show the menu
  656. $mainmenu.position = Vector2(clamped_x, clamped_y)
  657. $mainmenu.popup()
  658. func change_console_settings(toggled: bool):
  659. $Console.always_on_top = toggled
  660. func _on_kill_process_button_down() -> void:
  661. run_thread._on_kill_process_button_down()
  662. func invert_theme_toggled(toggled: bool):
  663. if toggled:
  664. var inverted = invert_theme(main_theme)
  665. get_tree().root.theme = inverted # force refresh
  666. $MenuBarBackground.color = Color(0.934, 0.934, 0.934)
  667. for color_rect in get_tree().get_nodes_in_group("invertable_background"):
  668. if color_rect is ColorRect:
  669. color_rect.color = Color(0.898, 0.898, 0.898, 0.6)
  670. else:
  671. get_tree().root.theme = main_theme # force refresheme = main_theme
  672. $MenuBarBackground.color = Color(0.065, 0.065, 0.065)
  673. for color_rect in get_tree().get_nodes_in_group("invertable_background"):
  674. if color_rect is ColorRect:
  675. color_rect.color = Color(0.102, 0.102, 0.102, 0.6)
  676. func invert_theme(theme: Theme) -> Theme:
  677. var inverted_theme = theme.duplicate(true) # deep copy
  678. # Check all types and color names in the theme
  679. var types = inverted_theme.get_type_list()
  680. for type in types:
  681. var color_names = inverted_theme.get_color_list(type)
  682. for cname in color_names:
  683. var col = inverted_theme.get_color(cname, type)
  684. var inverted = Color(1.0 - col.r, 1.0 - col.g, 1.0 - col.b, col.a)
  685. inverted_theme.set_color(cname, type, inverted)
  686. var style_names = inverted_theme.get_stylebox_list(type)
  687. for sname in style_names:
  688. if type == "GraphEdit" and sname == "panel":
  689. continue
  690. var sb = inverted_theme.get_stylebox(sname, type)
  691. var new_sb = sb.duplicate()
  692. if new_sb is StyleBoxFlat:
  693. var col = new_sb.bg_color
  694. new_sb.bg_color = Color(1.0 - col.r, 1.0 - col.g, 1.0 - col.b, col.a)
  695. inverted_theme.set_stylebox(sname, type, new_sb)
  696. return inverted_theme
  697. func swap_zoom_and_move(toggled: bool):
  698. if toggled:
  699. graph_edit.set_panning_scheme(1)
  700. else:
  701. graph_edit.set_panning_scheme(0)
  702. func on_files_dropped(files):
  703. var mouse_pos = (graph_edit.get_local_mouse_position() + graph_edit.scroll_offset) / graph_edit.zoom
  704. #see if files were dropped on an input node
  705. var dropped_node
  706. for child in graph_edit.get_children():
  707. if child is GraphNode:
  708. if Rect2(child.position_offset, child.size).has_point(mouse_pos):
  709. dropped_node = child
  710. break
  711. #if they were dropped on a node and the first file in the array is a wav file replace the file in the input node
  712. if dropped_node and dropped_node.has_meta("command") and dropped_node.get_meta("command") == "inputfile":
  713. if files[0].get_extension().to_lower() == "wav":
  714. dropped_node.get_node("AudioPlayer")._on_file_selected(files[0])
  715. else:
  716. #else make a new input node at the mouse position and load it in
  717. if files[0].get_extension().to_lower() == "wav":
  718. var new_input_node = graph_edit._make_node("inputfile")
  719. new_input_node.position_offset = mouse_pos
  720. new_input_node.get_node("AudioPlayer")._on_file_selected(files[0])
  721. #remove first element from the array
  722. files.remove_at(0)
  723. #check if there are any other files
  724. if files.size() > 0:
  725. var position_plus_offset = Vector2(mouse_pos.x, mouse_pos.y + 250) #apply a vertical offset from the mouse position so nodes dont overlap
  726. for file in files:
  727. if file.get_extension().to_lower() == "wav":
  728. var new_input_node = graph_edit._make_node("inputfile")
  729. new_input_node.position_offset = position_plus_offset
  730. new_input_node.get_node("AudioPlayer")._on_file_selected(file)
  731. position_plus_offset.y = position_plus_offset.y + 250
  732. func _on_fft_size_item_selected(index: int) -> void:
  733. var fft_size
  734. if index == 13:
  735. fft_size = 16380
  736. else:
  737. fft_size = 1 << (index + 1)
  738. run_thread.fft_size = fft_size
  739. func _on_fft_overlap_item_selected(index: int) -> void:
  740. run_thread.fft_overlap = index + 1
  741. func _on_undo_button_button_down() -> void:
  742. simulate_mouse_click()
  743. await get_tree().process_frame
  744. undo_redo.undo()
  745. func _on_redo_button_button_down() -> void:
  746. simulate_mouse_click()
  747. await get_tree().process_frame
  748. undo_redo.redo()