control.gd 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  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 final_output_dir
  9. var undo_redo := UndoRedo.new()
  10. var output_audio_player #tracks the node that is the current output player for linking
  11. var input_audio_player #tracks node that is the current input player for linking
  12. var outfile = "no file" #tracks dir of output file from cdp process
  13. var currentfile = "none" #tracks dir of currently loaded file for saving
  14. var changesmade = false #tracks if user has made changes to the currently loaded save file
  15. var savestate # tracks what the user is trying to do when savechangespopup is called
  16. var helpfile #tracks which help file the user was trying to load when savechangespopup is called
  17. var outfilename #links to the user name for outputfile field
  18. var foldertoggle #links to the reuse folder button
  19. var lastoutputfolder = "none" #tracks last output folder, this can in future be used to replace global.outfile but i cba right now
  20. var uiscale = 1.0 #tracks scaling for retina screens
  21. #scripts
  22. var open_help
  23. var run_thread
  24. var save_load
  25. # Called when the node enters the scene tree for the first time.
  26. func _ready() -> void:
  27. Nodes.hide()
  28. $mainmenu.hide()
  29. $NoLocationPopup.hide()
  30. $Console.hide()
  31. $NoInputPopup.hide()
  32. $MultipleConnectionsPopup.hide()
  33. $AudioSettings.hide()
  34. $AudioDevicePopup.hide()
  35. $SearchMenu.hide()
  36. $Settings.hide()
  37. $ProgressWindow.hide()
  38. $SaveDialog.access = FileDialog.ACCESS_FILESYSTEM
  39. $SaveDialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
  40. $SaveDialog.filters = ["*.thd"]
  41. $LoadDialog.access = FileDialog.ACCESS_FILESYSTEM
  42. $LoadDialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
  43. $LoadDialog.filters = ["*.thd"]
  44. get_tree().set_auto_accept_quit(false) #disable closing the app with the x and instead handle it internally
  45. load_scripts()
  46. make_signal_connections()
  47. check_user_preferences()
  48. hidpi_adjustment()
  49. new_patch()
  50. check_cdp_location_set()
  51. func load_scripts():
  52. #load and initialise scripts
  53. open_help = preload("res://scenes/main/scripts/open_help.gd").new()
  54. open_help.init(self)
  55. add_child(open_help)
  56. run_thread = preload("res://scenes/main/scripts/run_thread.gd").new()
  57. run_thread.init(self, $ProgressWindow, $ProgressWindow/ProgressLabel, $ProgressWindow/ProgressBar, $GraphEdit, $Console, $Console/ConsoleOutput)
  58. add_child(run_thread)
  59. graph_edit.init(self, $GraphEdit, Callable(open_help, "show_help_for_node"), $MultipleConnectionsPopup)
  60. save_load = preload("res://scenes/main/scripts/save_load.gd").new()
  61. 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"))
  62. add_child(save_load)
  63. func make_signal_connections():
  64. get_node("SearchMenu").make_node.connect(graph_edit._make_node)
  65. get_node("mainmenu").make_node.connect(graph_edit._make_node)
  66. get_node("mainmenu").open_help.connect(open_help.show_help_for_node)
  67. get_node("Settings").open_cdp_location.connect(show_cdp_location)
  68. get_node("Settings").console_on_top.connect(change_console_settings)
  69. func hidpi_adjustment():
  70. #checks if display is hidpi and scales ui accordingly hidpi - 144
  71. if DisplayServer.screen_get_dpi(0) >= 144:
  72. uiscale = 2.0
  73. get_window().content_scale_factor = uiscale
  74. #goes through popup_windows group and scales all popups and resizes them
  75. for window in get_tree().get_nodes_in_group("popup_windows"):
  76. window.size = window.size * uiscale
  77. window.content_scale_factor = uiscale
  78. #checks if user has opened a file from the system file menu and loads it
  79. var args = OS.get_cmdline_args()
  80. for arg in args:
  81. var path = arg.strip_edges()
  82. if FileAccess.file_exists(path) and path.get_extension().to_lower() == "thd":
  83. save_load.load_graph_edit(path)
  84. break
  85. func new_patch():
  86. #clear old patch
  87. graph_edit.clear_connections()
  88. for node in graph_edit.get_children():
  89. if node is GraphNode:
  90. node.queue_free()
  91. await get_tree().process_frame # Wait for nodes to actually be removed
  92. graph_edit.scroll_offset = Vector2(0, 0)
  93. #Generate input and output nodes
  94. var effect: GraphNode = Nodes.get_node(NodePath("inputfile")).duplicate()
  95. effect.name = "inputfile"
  96. get_node("GraphEdit").add_child(effect, true)
  97. effect.connect("open_help", Callable(open_help, "show_help_for_node"))
  98. effect.position_offset = Vector2(20,80)
  99. effect = Nodes.get_node(NodePath("outputfile")).duplicate()
  100. effect.name = "outputfile"
  101. get_node("GraphEdit").add_child(effect, true)
  102. effect.connect("open_help", Callable(open_help, "show_help_for_node"))
  103. effect.position_offset = Vector2((DisplayServer.screen_get_size().x - 480) / uiscale, 80)
  104. graph_edit._register_node_movement() #link nodes for tracking position changes for changes tracking
  105. changesmade = false #so it stops trying to save unchanged empty files
  106. Global.infile = "no_file" #resets input to stop processes running with old files
  107. get_window().title = "SoundThread"
  108. link_output()
  109. func link_output():
  110. #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
  111. for control in get_tree().get_nodes_in_group("outputnode"): #check all items in outputnode group
  112. if control.get_meta("outputfunction") == "deleteintermediate": #link delete intermediate files toggle to script
  113. control.toggled.connect(_toggle_delete)
  114. control.button_pressed = true
  115. elif control.get_meta("outputfunction") == "runprocess": #link runprocess button
  116. control.button_down.connect(_run_process)
  117. elif control.get_meta("outputfunction") == "recycle": #link recycle button
  118. control.button_down.connect(_recycle_outfile)
  119. elif control.get_meta("outputfunction") == "audioplayer": #link output audio player
  120. output_audio_player = control
  121. elif control.get_meta("outputfunction") == "filename":
  122. control.text = "outfile"
  123. outfilename = control
  124. elif control.get_meta("outputfunction") == "reusefolder":
  125. foldertoggle = control
  126. foldertoggle.button_pressed = true
  127. elif control.get_meta("outputfunction") == "openfolder":
  128. control.button_down.connect(_open_output_folder)
  129. for control in get_tree().get_nodes_in_group("inputnode"):
  130. if control.get_meta("inputfunction") == "audioplayer": #link input for recycle function
  131. print("input player found")
  132. input_audio_player = control
  133. func check_user_preferences():
  134. var interface_settings = ConfigHandler.load_interface_settings()
  135. var audio_settings = ConfigHandler.load_audio_settings()
  136. var audio_devices = AudioServer.get_output_device_list()
  137. $Console.always_on_top = interface_settings.console_on_top
  138. if audio_devices.has(audio_settings.device):
  139. AudioServer.set_output_device(audio_settings.device)
  140. else:
  141. $AudioDevicePopup.popup_centered()
  142. match interface_settings.theme:
  143. 0:
  144. RenderingServer.set_default_clear_color(Color("#2f4f4e"))
  145. 1:
  146. RenderingServer.set_default_clear_color(Color("#000807"))
  147. 2:
  148. RenderingServer.set_default_clear_color(Color("#98d4d2"))
  149. 3:
  150. RenderingServer.set_default_clear_color(Color(interface_settings.theme_custom_colour))
  151. func show_cdp_location():
  152. $CdpLocationDialog.show()
  153. func check_cdp_location_set():
  154. #checks if the location has been set and prompts user to set it
  155. var cdpprogs_settings = ConfigHandler.load_cdpprogs_settings()
  156. if cdpprogs_settings.location == "no_location":
  157. $NoLocationPopup.popup_centered()
  158. else:
  159. #if location is set, stores it in a variable
  160. cdpprogs_location = str(cdpprogs_settings.location)
  161. print(cdpprogs_location)
  162. func _on_ok_button_button_down() -> void:
  163. #after user has read dialog on where to find cdp progs this loads the file browser
  164. $NoLocationPopup.hide()
  165. if OS.get_name() == "Windows":
  166. $CdpLocationDialog.current_dir = "C:/"
  167. else:
  168. $CdpLocationDialog.current_dir = OS.get_environment("HOME")
  169. $CdpLocationDialog.show()
  170. func _on_cdp_location_dialog_dir_selected(dir: String) -> void:
  171. #saves default location for cdp programs in config file
  172. ConfigHandler.save_cdpprogs_settings(dir)
  173. cdpprogs_location = dir
  174. func _on_cdp_location_dialog_canceled() -> void:
  175. #cycles around the set location prompt if user cancels the file dialog
  176. check_cdp_location_set()
  177. func _input(event):
  178. if event.is_action_pressed("copy_node"):
  179. graph_edit.copy_selected_nodes()
  180. get_viewport().set_input_as_handled()
  181. elif event.is_action_pressed("paste_node"):
  182. simulate_mouse_click() #hacky fix to stop tooltips getting stuck
  183. await get_tree().process_frame
  184. graph_edit.paste_copied_nodes()
  185. get_viewport().set_input_as_handled()
  186. elif event.is_action_pressed("undo"):
  187. undo_redo.undo()
  188. elif event.is_action_pressed("redo"):
  189. undo_redo.redo()
  190. elif event.is_action_pressed("save"):
  191. if currentfile == "none":
  192. savestate = "saveas"
  193. $SaveDialog.popup_centered()
  194. else:
  195. save_load.save_graph_edit(currentfile)
  196. elif event.is_action_pressed("open_explore"):
  197. open_explore()
  198. func simulate_mouse_click():
  199. #simulates clicking the middle mouse button in order to hide any visible tooltips
  200. var click_pos = get_viewport().get_mouse_position()
  201. var down_event := InputEventMouseButton.new()
  202. down_event.button_index = MOUSE_BUTTON_MIDDLE
  203. down_event.pressed = true
  204. down_event.position = click_pos
  205. Input.parse_input_event(down_event)
  206. var up_event := InputEventMouseButton.new()
  207. up_event.button_index = MOUSE_BUTTON_MIDDLE
  208. up_event.pressed = false
  209. up_event.position = click_pos
  210. Input.parse_input_event(up_event)
  211. func _run_process() -> void:
  212. #check if any of the inputfile nodes don't have files loaded
  213. for node in graph_edit.get_children():
  214. if node.get_meta("command") == "inputfile" and node.get_node("AudioPlayer").has_meta("inputfile") == false:
  215. $NoInputPopup.popup_centered()
  216. return
  217. #check if the reuse folder toggle is set and a folder has been previously chosen
  218. if foldertoggle.button_pressed == true and lastoutputfolder != "none":
  219. _on_file_dialog_dir_selected(lastoutputfolder)
  220. else:
  221. $FileDialog.show()
  222. func _on_file_dialog_dir_selected(dir: String) -> void:
  223. lastoutputfolder = dir
  224. console_output.clear()
  225. var interface_settings = ConfigHandler.load_interface_settings()
  226. if interface_settings.disable_progress_bar == false:
  227. $ProgressWindow.show()
  228. else:
  229. if $Console.is_visible():
  230. $Console.hide()
  231. await get_tree().process_frame # Wait a frame to allow hide to complete
  232. $Console.popup_centered()
  233. else:
  234. $Console.popup_centered()
  235. await get_tree().process_frame
  236. run_thread.log_console("Generating processing queue", true)
  237. await get_tree().process_frame
  238. #get the current time in hh-mm-ss format as default : causes file name issues
  239. var time_dict = Time.get_time_dict_from_system()
  240. # Pad with zeros to ensure two digits for hour, minute, second
  241. var hour = str(time_dict.hour).pad_zeros(2)
  242. var minute = str(time_dict.minute).pad_zeros(2)
  243. var second = str(time_dict.second).pad_zeros(2)
  244. var time_str = hour + "-" + minute + "-" + second
  245. Global.outfile = dir + "/" + outfilename.text.get_basename() + "_" + Time.get_date_string_from_system() + "_" + time_str
  246. run_thread.log_console("Output directory and file name(s):" + Global.outfile, true)
  247. await get_tree().process_frame
  248. run_thread.run_thread_with_branches()
  249. func _toggle_delete(toggled_on: bool):
  250. delete_intermediate_outputs = toggled_on
  251. print(toggled_on)
  252. func _on_console_close_requested() -> void:
  253. $Console.hide()
  254. func _on_console_open_folder_button_down() -> void:
  255. $Console.hide()
  256. OS.shell_open(Global.outfile.get_base_dir())
  257. func _on_ok_button_2_button_down() -> void:
  258. $NoInputPopup.hide()
  259. func _on_ok_button_3_button_down() -> void:
  260. $MultipleConnectionsPopup.hide()
  261. func _on_settings_button_index_pressed(index: int) -> void:
  262. var interface_settings = ConfigHandler.load_interface_settings()
  263. match index:
  264. 0:
  265. $Settings.popup_centered()
  266. 1:
  267. $AudioSettings.popup_centered()
  268. 2:
  269. if $Console.is_visible():
  270. $Console.hide()
  271. await get_tree().process_frame # Wait a frame to allow hide to complete
  272. $Console.popup_centered()
  273. else:
  274. $Console.popup_centered()
  275. func _on_file_button_index_pressed(index: int) -> void:
  276. match index:
  277. 0:
  278. if changesmade == true:
  279. savestate = "newfile"
  280. $SaveChangesPopup.popup_centered()
  281. else:
  282. new_patch()
  283. currentfile = "none" #reset current file to none for save tracking
  284. print("new patch, changes made =")
  285. print(changesmade)
  286. print("current file =")
  287. print(currentfile)
  288. 1:
  289. if currentfile == "none":
  290. savestate = "saveas"
  291. $SaveDialog.popup_centered()
  292. else:
  293. save_load.save_graph_edit(currentfile)
  294. print("save pressed, changes made =")
  295. print(changesmade)
  296. print("current file =")
  297. print(currentfile)
  298. 2:
  299. savestate = "saveas"
  300. $SaveDialog.popup_centered()
  301. 3:
  302. if changesmade == true:
  303. savestate = "load"
  304. $SaveChangesPopup.popup_centered()
  305. else:
  306. $LoadDialog.popup_centered()
  307. func _on_save_dialog_file_selected(path: String) -> void:
  308. save_load.save_graph_edit(path) #save file
  309. #check what the user was trying to do before save and do that action
  310. if savestate == "newfile":
  311. new_patch()
  312. currentfile = "none" #reset current file to none for save tracking
  313. elif savestate == "load":
  314. $LoadDialog.popup_centered()
  315. elif savestate == "helpfile":
  316. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  317. save_load.load_graph_edit(helpfile)
  318. elif savestate == "quit":
  319. await get_tree().create_timer(0.25).timeout #little pause so that it feels like it actually saved even though it did
  320. get_tree().quit()
  321. elif savestate == "saveas":
  322. currentfile = path
  323. savestate = "none" #reset save state, not really needed but feels good
  324. func _on_load_dialog_file_selected(path: String) -> void:
  325. currentfile = path #tracking path here only means "save" only saves patches the user has loaded rather than overwriting help files
  326. save_load.load_graph_edit(path)
  327. func _on_help_button_index_pressed(index: int) -> void:
  328. match index:
  329. 0:
  330. pass
  331. 1:
  332. if changesmade == true:
  333. savestate = "helpfile"
  334. helpfile = "res://examples/getting_started.thd"
  335. $SaveChangesPopup.popup_centered()
  336. else:
  337. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  338. save_load.load_graph_edit("res://examples/getting_started.thd")
  339. 2:
  340. if changesmade == true:
  341. savestate = "helpfile"
  342. helpfile = "res://examples/navigating.thd"
  343. $SaveChangesPopup.popup_centered()
  344. else:
  345. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  346. save_load.load_graph_edit("res://examples/navigating.thd")
  347. 3:
  348. if changesmade == true:
  349. savestate = "helpfile"
  350. helpfile = "res://examples/building_a_thread.thd"
  351. $SaveChangesPopup.popup_centered()
  352. else:
  353. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  354. save_load.load_graph_edit("res://examples/building_a_thread.thd")
  355. 4:
  356. if changesmade == true:
  357. savestate = "helpfile"
  358. helpfile = "res://examples/frequency_domain.thd"
  359. $SaveChangesPopup.popup_centered()
  360. else:
  361. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  362. save_load.load_graph_edit("res://examples/frequency_domain.thd")
  363. 5:
  364. if changesmade == true:
  365. savestate = "helpfile"
  366. helpfile = "res://examples/automation.thd"
  367. $SaveChangesPopup.popup_centered()
  368. else:
  369. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  370. save_load.load_graph_edit("res://examples/automation.thd")
  371. 6:
  372. if changesmade == true:
  373. savestate = "helpfile"
  374. helpfile = "res://examples/trimming.thd"
  375. $SaveChangesPopup.popup_centered()
  376. else:
  377. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  378. save_load.load_graph_edit("res://examples/trimming.thd")
  379. 7:
  380. pass
  381. 8:
  382. if changesmade == true:
  383. savestate = "helpfile"
  384. helpfile = "res://examples/wetdry.thd"
  385. $SaveChangesPopup.popup_centered()
  386. else:
  387. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  388. save_load.load_graph_edit("res://examples/wetdry.thd")
  389. 9:
  390. if changesmade == true:
  391. savestate = "helpfile"
  392. helpfile = "res://examples/resonant_filters.thd"
  393. $SaveChangesPopup.popup_centered()
  394. else:
  395. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  396. save_load.load_graph_edit("res://examples/resonant_filters.thd")
  397. 10:
  398. pass
  399. 11:
  400. OS.shell_open("https://www.composersdesktop.com/docs/html/ccdpndex.htm")
  401. 12:
  402. OS.shell_open("https://github.com/j-p-higgins/SoundThread/issues")
  403. func _recycle_outfile():
  404. if outfile != "no file":
  405. input_audio_player.recycle_outfile(outfile)
  406. func _on_save_changes_button_down() -> void:
  407. $SaveChangesPopup.hide()
  408. if currentfile == "none":
  409. $SaveDialog.show()
  410. else:
  411. save_load.save_graph_edit(currentfile)
  412. if savestate == "newfile":
  413. new_patch()
  414. currentfile = "none" #reset current file to none for save tracking
  415. elif savestate == "load":
  416. $LoadDialog.popup_centered()
  417. elif savestate == "helpfile":
  418. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  419. save_load.load_graph_edit(helpfile)
  420. elif savestate == "quit":
  421. await get_tree().create_timer(0.25).timeout #little pause so that it feels like it actually saved even though it did
  422. get_tree().quit()
  423. savestate = "none"
  424. func _on_dont_save_changes_button_down() -> void:
  425. $SaveChangesPopup.hide()
  426. if savestate == "newfile":
  427. new_patch()
  428. currentfile = "none" #reset current file to none for save tracking
  429. elif savestate == "load":
  430. $LoadDialog.popup_centered()
  431. elif savestate == "helpfile":
  432. currentfile = "none" #reset current file to none for save tracking so user cant save over help file
  433. save_load.load_graph_edit(helpfile)
  434. elif savestate == "quit":
  435. get_tree().quit()
  436. savestate = "none"
  437. func _notification(what):
  438. if what == NOTIFICATION_WM_CLOSE_REQUEST:
  439. run_thread._on_kill_process_button_down()
  440. $Console.hide()
  441. if changesmade == true:
  442. savestate = "quit"
  443. $SaveChangesPopup.popup_centered()
  444. #$HelpWindow.hide()
  445. else:
  446. get_tree().quit() # default behavior
  447. func _open_output_folder():
  448. if lastoutputfolder != "none":
  449. OS.shell_open(lastoutputfolder)
  450. func _on_rich_text_label_meta_clicked(meta: Variant) -> void:
  451. print(str(meta))
  452. OS.shell_open(str(meta))
  453. func _on_graph_edit_popup_request(at_position: Vector2) -> void:
  454. effect_position = graph_edit.get_local_mouse_position()
  455. #get the mouse position in screen coordinates
  456. var mouse_screen_pos = DisplayServer.mouse_get_position()
  457. #get the window position in screen coordinates
  458. var window_screen_pos = get_window().position
  459. #get the window size relative to its scaling for retina displays
  460. var window_size = get_window().size * DisplayServer.screen_get_scale()
  461. #calculate the xy position of the mouse clamped to the size of the window and menu so it doesn't go off the screen
  462. var clamped_x = clamp(mouse_screen_pos.x, window_screen_pos.x, window_screen_pos.x + window_size.x - $SearchMenu.size.x)
  463. var clamped_y = clamp(mouse_screen_pos.y, window_screen_pos.y, window_screen_pos.y + window_size.y - (420 * DisplayServer.screen_get_scale()))
  464. #position and show the menu
  465. $SearchMenu.position = Vector2(clamped_x, clamped_y)
  466. $SearchMenu.popup()
  467. func _on_audio_settings_close_requested() -> void:
  468. $AudioSettings.hide()
  469. func _on_open_audio_settings_button_down() -> void:
  470. $AudioDevicePopup.hide()
  471. $AudioSettings.popup_centered()
  472. func _on_audio_device_popup_close_requested() -> void:
  473. $AudioDevicePopup.hide()
  474. func _on_mainmenu_close_requested() -> void:
  475. #closes menu if click is anywhere other than the menu as it is a window with popup set to true
  476. $mainmenu.hide()
  477. func open_explore():
  478. effect_position = graph_edit.get_local_mouse_position()
  479. #get the mouse position in screen coordinates
  480. var mouse_screen_pos = DisplayServer.mouse_get_position()
  481. #get the window position in screen coordinates
  482. var window_screen_pos = get_window().position
  483. #get the window size relative to its scaling for retina displays
  484. var window_size = get_window().size * DisplayServer.screen_get_scale()
  485. #get the size of the popup menu
  486. var popup_size = $mainmenu.size
  487. #calculate the xy position of the mouse clamped to the size of the window and menu so it doesn't go off the screen
  488. var clamped_x = clamp(mouse_screen_pos.x, window_screen_pos.x, window_screen_pos.x + window_size.x - popup_size.x)
  489. var clamped_y = clamp(mouse_screen_pos.y, window_screen_pos.y, window_screen_pos.y + window_size.y - popup_size.y)
  490. #position and show the menu
  491. $mainmenu.position = Vector2(clamped_x, clamped_y)
  492. $mainmenu.popup()
  493. func change_console_settings(toggled: bool):
  494. $Console.always_on_top = toggled
  495. func _on_kill_process_button_down() -> void:
  496. run_thread._on_kill_process_button_down()