Explorar o código

Merge pull request #188 from j-p-higgins/ux_improvements

Ux improvements
Jonathan Higgins hai 3 meses
pai
achega
83081b6ccf

+ 9 - 0
Global/config_handler.gd

@@ -17,10 +17,19 @@ func _ready():
 	ensure_setting("interface_settings", "console_on_top", true)
 	ensure_setting("interface_settings", "console_on_top", true)
 	ensure_setting("interface_settings", "theme", 0)
 	ensure_setting("interface_settings", "theme", 0)
 	ensure_setting("interface_settings", "theme_custom_colour", "#865699")
 	ensure_setting("interface_settings", "theme_custom_colour", "#865699")
+	ensure_setting("interface_settings", "invert_theme", false)
+	ensure_setting("interface_settings", "high_contrast_selected_cables", false)
+	ensure_setting("interface_settings", "swap_zoom_and_move", false)
+	ensure_setting("interface_settings", "right_click_opens_explore", false)
+	ensure_setting("interface_settings", "ui_scale_multiplier", 1.0)
 	ensure_setting("interface_settings", "delete_intermediate", true)
 	ensure_setting("interface_settings", "delete_intermediate", true)
 	ensure_setting("interface_settings", "reuse_output_folder", true)
 	ensure_setting("interface_settings", "reuse_output_folder", true)
+	ensure_setting("interface_settings", "last_used_output_folder", "no_file")
+	ensure_setting("interface_settings", "last_used_input_folder", "no_file")
 	ensure_setting("interface_settings", "autoplay", true)
 	ensure_setting("interface_settings", "autoplay", true)
+	ensure_setting("interface_settings", "favourites", [])
 	ensure_setting("audio_settings", "device", "Default")
 	ensure_setting("audio_settings", "device", "Default")
+	
 
 
 	# Only save if we added anything new
 	# Only save if we added anything new
 	if !file_exists or config_changed:
 	if !file_exists or config_changed:

+ 13 - 1
addons/audio_preview/voice_preview_generator.gd

@@ -9,7 +9,10 @@ const IMAGE_HEIGHT: int = 64
 
 
 var image_compression: float = 10.0 # How many samples in one pixel
 var image_compression: float = 10.0 # How many samples in one pixel
 var background_color = Color(0, 0, 0, 0)
 var background_color = Color(0, 0, 0, 0)
-var foreground_color = Color.SILVER
+var foreground_color
+
+
+
 
 
 
 
 
 
@@ -26,6 +29,15 @@ var must_abort := false
 
 
 
 
 func generate_preview(stream: AudioStreamWAV, image_max_width: int = 500):
 func generate_preview(stream: AudioStreamWAV, image_max_width: int = 500):
+	#set colour based on theme
+	var interface_settings = ConfigHandler.load_interface_settings()
+	#check if the theme is inverted
+	if interface_settings.invert_theme:
+		foreground_color = Color(0.102, 0.102, 0.102, 0.6)
+	else:
+		foreground_color = Color(0.898, 0.898, 0.898, 0.6)
+	
+	
 	if not stream:
 	if not stream:
 		return
 		return
 	if stream.format == AudioStreamWAV.FORMAT_IMA_ADPCM:
 	if stream.format == AudioStreamWAV.FORMAT_IMA_ADPCM:

+ 1 - 1
export_presets.cfg

@@ -9,7 +9,7 @@ custom_features=""
 export_filter="all_resources"
 export_filter="all_resources"
 include_filter="*.thd, export_presets.cfg"
 include_filter="*.thd, export_presets.cfg"
 exclude_filter=""
 exclude_filter=""
-export_path="../SoundThread_Exports/v0.3.0-beta/SoundThread_v0.3.0-beta_windows/SoundThread.exe"
+export_path="../SoundThread_Exports/SoundThread_test.exe"
 patches=PackedStringArray()
 patches=PackedStringArray()
 encryption_include_filters=""
 encryption_include_filters=""
 encryption_exclude_filters=""
 encryption_exclude_filters=""

+ 10 - 1
project.godot

@@ -40,10 +40,10 @@ window/per_pixel_transparency/allowed=true
 
 
 outputnode="Controls from the output node"
 outputnode="Controls from the output node"
 inputnode="input file controls"
 inputnode="input file controls"
+invertable_background=""
 
 
 [gui]
 [gui]
 
 
-theme/custom="uid://cefwkdcoxihro"
 theme/custom_font="uid://cc7mj053bhfvh"
 theme/custom_font="uid://cc7mj053bhfvh"
 
 
 [input]
 [input]
@@ -83,6 +83,15 @@ save_as={
 "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"command_or_control_autoremap":true,"alt_pressed":false,"shift_pressed":true,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":83,"location":0,"echo":false,"script":null)
 "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"command_or_control_autoremap":true,"alt_pressed":false,"shift_pressed":true,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":83,"location":0,"echo":false,"script":null)
 ]
 ]
 }
 }
+auto_link_nodes={
+"deadzone": 0.2,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194325,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
+redo={
+"deadzone": 0.2,
+"events": []
+}
 
 
 [rendering]
 [rendering]
 
 

+ 28 - 17
scenes/Nodes/audioplayer.gd

@@ -17,11 +17,16 @@ var autoplay
 signal setnodetitle
 signal setnodetitle
 
 
 func _ready():
 func _ready():
+	var interface_settings = ConfigHandler.load_interface_settings()
 	#Setup file dialogue to access system files and only accept wav files
 	#Setup file dialogue to access system files and only accept wav files
 	#get_window().files_dropped.connect(_on_files_dropped)
 	#get_window().files_dropped.connect(_on_files_dropped)
 	file_dialog.access = FileDialog.ACCESS_FILESYSTEM
 	file_dialog.access = FileDialog.ACCESS_FILESYSTEM
 	file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
 	file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
 	file_dialog.filters = ["*.wav ; WAV audio files"]
 	file_dialog.filters = ["*.wav ; WAV audio files"]
+	var input_folder = interface_settings.last_used_input_folder
+	if input_folder != "no_file" and DirAccess.open(input_folder) != null:
+		file_dialog.current_dir = input_folder
+	
 	file_dialog.connect("file_selected", Callable(self, "_on_file_selected"))
 	file_dialog.connect("file_selected", Callable(self, "_on_file_selected"))
 	audio_player.connect("finished", Callable(self, "_on_audio_finished"))
 	audio_player.connect("finished", Callable(self, "_on_audio_finished"))
 	
 	
@@ -62,9 +67,14 @@ func _on_close_button_button_down() -> void:
 	$WavError.hide()
 	$WavError.hide()
 
 
 func _on_load_button_button_down() -> void:
 func _on_load_button_button_down() -> void:
+	var interface_settings = ConfigHandler.load_interface_settings()
+	var input_folder = interface_settings.last_used_input_folder
+	if input_folder != "no_file" and DirAccess.open(input_folder) != null:
+		file_dialog.current_dir = input_folder
 	file_dialog.popup_centered()
 	file_dialog.popup_centered()
 
 
 func _on_file_selected(path: String):
 func _on_file_selected(path: String):
+	ConfigHandler.save_interface_settings("last_used_input_folder", path.get_base_dir())
 	audio_player.stream = AudioStreamWAV.load_from_file(path, {
 	audio_player.stream = AudioStreamWAV.load_from_file(path, {
 		"compress/mode" = 0,
 		"compress/mode" = 0,
 		"edit/loop_mode" = 1})
 		"edit/loop_mode" = 1})
@@ -210,23 +220,24 @@ func _on_button_button_down() -> void:
 
 
 func _on_button_button_up() -> void:
 func _on_button_button_up() -> void:
 	rect_focus = false
 	rect_focus = false
-	if get_meta("loadenable") == true:
-		if $LoopRegion.size.x > 0:
-			set_meta("trimfile", true)
-			var length = $AudioStreamPlayer.stream.get_length()
-			var pixel_to_time = length / 399
-			var start = pixel_to_time * $LoopRegion.position.x
-			var starttime = convert_length(start)
-			$StartLabel.text = starttime
-			var end = start + (pixel_to_time * $LoopRegion.size.x)
-			var endtime = convert_length(end)
-			$EndLabel.text = endtime
-			set_meta("trimpoints", [start, end])
-		else:
-			set_meta("trimfile", false)
-			$StartLabel.text = "00:00.00"
-			var end = convert_length($AudioStreamPlayer.stream.get_length())
-			$EndLabel.text = end
+	if audio_player.stream != null:
+		if get_meta("loadenable") == true:
+			if $LoopRegion.size.x > 0:
+				set_meta("trimfile", true)
+				var length = $AudioStreamPlayer.stream.get_length()
+				var pixel_to_time = length / 399
+				var start = pixel_to_time * $LoopRegion.position.x
+				var starttime = convert_length(start)
+				$StartLabel.text = starttime
+				var end = start + (pixel_to_time * $LoopRegion.size.x)
+				var endtime = convert_length(end)
+				$EndLabel.text = endtime
+				set_meta("trimpoints", [start, end])
+			else:
+				set_meta("trimfile", false)
+				$StartLabel.text = "00:00.00"
+				var end = convert_length($AudioStreamPlayer.stream.get_length())
+				$EndLabel.text = end
 			
 			
 
 
 func convert_length(reportedseconds: float) -> String:
 func convert_length(reportedseconds: float) -> String:

+ 6 - 1
scenes/Nodes/inputfile.gd

@@ -1,5 +1,6 @@
 extends GraphNode
 extends GraphNode
 signal open_help
 signal open_help
+signal node_moved
 
 
 func _ready() -> void:
 func _ready() -> void:
 	#add button to title bar
 	#add button to title bar
@@ -11,7 +12,8 @@ func _ready() -> void:
 	titlebar.add_child(btn)
 	titlebar.add_child(btn)
 	
 	
 	$AudioPlayer.setnodetitle.connect(_set_node_title)
 	$AudioPlayer.setnodetitle.connect(_set_node_title)
-
+	
+	self.position_offset_changed.connect(_on_position_offset_changed)
 
 
 func _open_help():
 func _open_help():
 	open_help.emit(self.get_meta("command"), self.title)
 	open_help.emit(self.get_meta("command"), self.title)
@@ -21,3 +23,6 @@ func _set_node_title(file: String):
 	if file.length() > 30:
 	if file.length() > 30:
 		file = file.substr(0, 30) + "..."
 		file = file.substr(0, 30) + "..."
 	title = "Input File - " + file
 	title = "Input File - " + file
+
+func _on_position_offset_changed():
+	node_moved.emit(self, Rect2(position, size))

+ 36 - 0
scenes/Nodes/node_logic.gd

@@ -3,6 +3,7 @@ extends GraphNode
 @export var min_gap: float = 0.5  # editable value in inspector for the minimum gap between min and max
 @export var min_gap: float = 0.5  # editable value in inspector for the minimum gap between min and max
 signal open_help
 signal open_help
 signal inlet_removed
 signal inlet_removed
+signal node_moved
 
 
 func _ready() -> void:
 func _ready() -> void:
 	var sliders := _get_all_hsliders(self) #finds all sliders
 	var sliders := _get_all_hsliders(self) #finds all sliders
@@ -12,6 +13,16 @@ func _ready() -> void:
 		
 		
 	#add button to title bar
 	#add button to title bar
 	var titlebar = self.get_titlebar_hbox()
 	var titlebar = self.get_titlebar_hbox()
+	
+	#add randomise button
+	if sliders.size() > 0:
+		var rnd_btn = Button.new()
+		rnd_btn.text = "!"
+		rnd_btn.tooltip_text = "Randomise Slider Values"
+		rnd_btn.connect("pressed", Callable(self, "_randomise_sliders")) #pass key (process name) when button is pressed
+		titlebar.add_child(rnd_btn)
+	
+	#add help button
 	var btn = Button.new()
 	var btn = Button.new()
 	btn.text = "?"
 	btn.text = "?"
 	btn.tooltip_text = "Open help for " + self.title
 	btn.tooltip_text = "Open help for " + self.title
@@ -20,6 +31,8 @@ func _ready() -> void:
 	await get_tree().process_frame
 	await get_tree().process_frame
 	#reset_size()
 	#reset_size()
 	
 	
+	self.position_offset_changed.connect(_on_position_offset_changed)
+	
 	if self.has_node("addremoveinlets"):
 	if self.has_node("addremoveinlets"):
 		var addremove = self.get_node("addremoveinlets")
 		var addremove = self.get_node("addremoveinlets")
 		addremove.add_inlet.connect(add_inlet_to_node)
 		addremove.add_inlet.connect(add_inlet_to_node)
@@ -106,3 +119,26 @@ func remove_inlet_from_node():
 		#update the size of the graphnode to shrink to fit smaller ui
 		#update the size of the graphnode to shrink to fit smaller ui
 		update_minimum_size()
 		update_minimum_size()
 		size.y = get_combined_minimum_size().y
 		size.y = get_combined_minimum_size().y
+
+func _on_position_offset_changed():
+	node_moved.emit(self, Rect2(position, size))
+	
+	
+func _randomise_sliders():
+	var sliders := _get_all_hsliders(self) #finds all sliders
+	#links sliders to this script
+	for slider in sliders:
+		var min = slider.min_value
+		var max = slider.max_value
+		var expo = slider.exp_edit
+		var default = slider.get_meta("default_value")
+		
+		var rnd = randf()
+		var rnd_value
+		if expo:
+			rnd_value = min * pow(max / min, rnd)
+		else:
+			rnd_value = (rnd * (max - min)) + min
+		
+		slider.value = rnd_value
+	

+ 27 - 46
scenes/Nodes/nodes.tscn

@@ -52,6 +52,7 @@ layout_mode = 2
 
 
 [node name="AudioPlayer" parent="inputfile" groups=["inputnode"] instance=ExtResource("2_b6nw4")]
 [node name="AudioPlayer" parent="inputfile" groups=["inputnode"] instance=ExtResource("2_b6nw4")]
 layout_mode = 2
 layout_mode = 2
+size_flags_horizontal = 4
 metadata/loadenable = true
 metadata/loadenable = true
 metadata/inputfunction = "audioplayer"
 metadata/inputfunction = "audioplayer"
 
 
@@ -90,14 +91,15 @@ layout_mode = 2
 
 
 [node name="AudioPlayer" parent="preview" groups=["inputnode"] instance=ExtResource("2_b6nw4")]
 [node name="AudioPlayer" parent="preview" groups=["inputnode"] instance=ExtResource("2_b6nw4")]
 layout_mode = 2
 layout_mode = 2
+size_flags_horizontal = 4
 metadata/inputfunction = "audioplayer"
 metadata/inputfunction = "audioplayer"
 
 
 [node name="outputfile" type="GraphNode" parent="."]
 [node name="outputfile" type="GraphNode" parent="."]
 layout_mode = 0
 layout_mode = 0
 offset_left = 523.0
 offset_left = 523.0
 offset_top = 6.0
 offset_top = 6.0
-offset_right = 954.0
-offset_bottom = 519.0
+offset_right = 959.0
+offset_bottom = 555.0
 tooltip_text = "Handles everything to do with audio output. "
 tooltip_text = "Handles everything to do with audio output. "
 title = "Output File"
 title = "Output File"
 slot/0/left_enabled = true
 slot/0/left_enabled = true
@@ -217,6 +219,15 @@ slot/12/right_type = 0
 slot/12/right_color = Color(1, 1, 1, 1)
 slot/12/right_color = Color(1, 1, 1, 1)
 slot/12/right_icon = null
 slot/12/right_icon = null
 slot/12/draw_stylebox = true
 slot/12/draw_stylebox = true
+slot/13/left_enabled = false
+slot/13/left_type = 0
+slot/13/left_color = Color(1, 1, 1, 1)
+slot/13/left_icon = null
+slot/13/right_enabled = false
+slot/13/right_type = 0
+slot/13/right_color = Color(1, 1, 1, 1)
+slot/13/right_icon = null
+slot/13/draw_stylebox = true
 script = ExtResource("3_0jqh0")
 script = ExtResource("3_0jqh0")
 metadata/command = "outputfile"
 metadata/command = "outputfile"
 metadata/utility = true
 metadata/utility = true
@@ -265,6 +276,19 @@ text = "Reuse Last Output Folder"
 expand_icon = true
 expand_icon = true
 metadata/outputfunction = "reusefolder"
 metadata/outputfunction = "reusefolder"
 
 
+[node name="OutputFolderMargin" type="MarginContainer" parent="outputfile"]
+layout_mode = 2
+theme_override_constants/margin_left = 7
+theme_override_constants/margin_top = 6
+theme_override_constants/margin_right = 7
+theme_override_constants/margin_bottom = 9
+
+[node name="OutputFolderLabel" type="Label" parent="outputfile/OutputFolderMargin"]
+layout_mode = 2
+size_flags_horizontal = 3
+clip_text = true
+text_overrun_behavior = 3
+
 [node name="OpenOutputFolder" type="Button" parent="outputfile" groups=["outputnode"]]
 [node name="OpenOutputFolder" type="Button" parent="outputfile" groups=["outputnode"]]
 custom_minimum_size = Vector2(0, 43)
 custom_minimum_size = Vector2(0, 43)
 layout_mode = 2
 layout_mode = 2
@@ -294,6 +318,7 @@ theme_override_constants/margin_bottom = 2
 
 
 [node name="AudioPlayer" parent="outputfile" groups=["outputnode"] instance=ExtResource("2_b6nw4")]
 [node name="AudioPlayer" parent="outputfile" groups=["outputnode"] instance=ExtResource("2_b6nw4")]
 layout_mode = 2
 layout_mode = 2
+size_flags_horizontal = 4
 metadata/outputfunction = "audioplayer"
 metadata/outputfunction = "audioplayer"
 
 
 [node name="notes" type="GraphNode" parent="."]
 [node name="notes" type="GraphNode" parent="."]
@@ -802,50 +827,6 @@ size_flags_horizontal = 3
 text = "+"
 text = "+"
 metadata/calc = "+"
 metadata/calc = "+"
 
 
-[node name="GraphNode" type="GraphNode" parent="."]
-layout_mode = 0
-offset_left = 1273.0
-offset_top = 422.0
-offset_right = 1615.0
-offset_bottom = 509.0
-slot/0/left_enabled = true
-slot/0/left_type = 0
-slot/0/left_color = Color(1, 1, 1, 1)
-slot/0/left_icon = null
-slot/0/right_enabled = false
-slot/0/right_type = 0
-slot/0/right_color = Color(1, 1, 1, 1)
-slot/0/right_icon = null
-slot/0/draw_stylebox = true
-slot/1/left_enabled = true
-slot/1/left_type = 0
-slot/1/left_color = Color(1, 1, 1, 1)
-slot/1/left_icon = null
-slot/1/right_enabled = false
-slot/1/right_type = 0
-slot/1/right_color = Color(1, 1, 1, 1)
-slot/1/right_icon = null
-slot/1/draw_stylebox = true
-slot/2/left_enabled = false
-slot/2/left_type = 0
-slot/2/left_color = Color(1, 1, 1, 1)
-slot/2/left_icon = null
-slot/2/right_enabled = false
-slot/2/right_type = 0
-slot/2/right_color = Color(1, 1, 1, 1)
-slot/2/right_icon = null
-slot/2/draw_stylebox = true
-
-[node name="Control" type="Control" parent="GraphNode"]
-custom_minimum_size = Vector2(0, 20)
-layout_mode = 2
-
-[node name="Control2" type="Control" parent="GraphNode"]
-layout_mode = 2
-
-[node name="HSlider" type="HSlider" parent="GraphNode"]
-layout_mode = 2
-
 [connection signal="text_submitted" from="outputfile/FileNameField" to="outputfile" method="_on_file_name_field_text_submitted"]
 [connection signal="text_submitted" from="outputfile/FileNameField" to="outputfile" method="_on_file_name_field_text_submitted"]
 [connection signal="toggled" from="outputfile/DeleteIntermediateFilesToggle" to="outputfile" method="_on_delete_intermediate_files_toggle_toggled"]
 [connection signal="toggled" from="outputfile/DeleteIntermediateFilesToggle" to="outputfile" method="_on_delete_intermediate_files_toggle_toggled"]
 [connection signal="toggled" from="outputfile/ReuseFolderToggle" to="outputfile" method="_on_reuse_folder_toggle_toggled"]
 [connection signal="toggled" from="outputfile/ReuseFolderToggle" to="outputfile" method="_on_reuse_folder_toggle_toggled"]

+ 6 - 0
scenes/Nodes/outputfile.gd

@@ -1,5 +1,6 @@
 extends GraphNode
 extends GraphNode
 signal open_help
 signal open_help
+signal node_moved
 
 
 # Called when the node enters the scene tree for the first time.
 # Called when the node enters the scene tree for the first time.
 func _ready() -> void:
 func _ready() -> void:
@@ -11,6 +12,8 @@ func _ready() -> void:
 	btn.connect("pressed", Callable(self, "_open_help")) #pass key (process name) when button is pressed
 	btn.connect("pressed", Callable(self, "_open_help")) #pass key (process name) when button is pressed
 	titlebar.add_child(btn)
 	titlebar.add_child(btn)
 	
 	
+	self.position_offset_changed.connect(_on_position_offset_changed)
+	
 func init():
 func init():
 	var interface_settings = ConfigHandler.load_interface_settings()
 	var interface_settings = ConfigHandler.load_interface_settings()
 	$DeleteIntermediateFilesToggle.button_pressed = interface_settings.get("delete_intermediate", true)
 	$DeleteIntermediateFilesToggle.button_pressed = interface_settings.get("delete_intermediate", true)
@@ -32,3 +35,6 @@ func _on_delete_intermediate_files_toggle_toggled(toggled_on: bool) -> void:
 
 
 func _on_reuse_folder_toggle_toggled(toggled_on: bool) -> void:
 func _on_reuse_folder_toggle_toggled(toggled_on: bool) -> void:
 	ConfigHandler.save_interface_settings("reuse_output_folder", toggled_on)
 	ConfigHandler.save_interface_settings("reuse_output_folder", toggled_on)
+
+func _on_position_offset_changed():
+	node_moved.emit(self, Rect2(position, size))

+ 3 - 1
scenes/Nodes/valueslider.gd

@@ -71,7 +71,9 @@ func _on_h_slider_gui_input(event: InputEvent) -> void:
 		$HSplitContainer/HSlider/PopupMenu.set_position(local_pos)
 		$HSplitContainer/HSlider/PopupMenu.set_position(local_pos)
 		# Prevent default context menu or input propagation if needed
 		# Prevent default context menu or input propagation if needed
 		accept_event()
 		accept_event()
-
+	elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.double_click:
+		$HSplitContainer/HSlider.value = $HSplitContainer/HSlider.get_meta("default_value")
+		accept_event()
 
 
 func _on_popup_menu_index_pressed(index: int) -> void:
 func _on_popup_menu_index_pressed(index: int) -> void:
 	match index:
 	match index:

+ 4 - 2
scenes/Nodes/valueslider.tscn

@@ -1,7 +1,8 @@
-[gd_scene load_steps=3 format=3 uid="uid://dya5kxx132fgp"]
+[gd_scene load_steps=4 format=3 uid="uid://dya5kxx132fgp"]
 
 
 [ext_resource type="Script" uid="uid://bco7hof3wqck4" path="res://scenes/Nodes/valueslider.gd" id="1_4kxw6"]
 [ext_resource type="Script" uid="uid://bco7hof3wqck4" path="res://scenes/Nodes/valueslider.gd" id="1_4kxw6"]
 [ext_resource type="Script" uid="uid://duykdpsfmfw38" path="res://scenes/Nodes/breakfilemaker.gd" id="2_6ltu5"]
 [ext_resource type="Script" uid="uid://duykdpsfmfw38" path="res://scenes/Nodes/breakfilemaker.gd" id="2_6ltu5"]
+[ext_resource type="Script" uid="uid://c503vew41pw80" path="res://scenes/main/scripts/color_rect_theme_invert.gd" id="2_du0of"]
 
 
 [node name="VBoxContainer" type="VBoxContainer"]
 [node name="VBoxContainer" type="VBoxContainer"]
 custom_minimum_size = Vector2(270, 0)
 custom_minimum_size = Vector2(270, 0)
@@ -59,13 +60,14 @@ text = "Double click to add/remove automation"
 horizontal_alignment = 2
 horizontal_alignment = 2
 vertical_alignment = 1
 vertical_alignment = 1
 
 
-[node name="ColorRect" type="ColorRect" parent="BreakFileMaker"]
+[node name="ColorRect" type="ColorRect" parent="BreakFileMaker" groups=["invertable_background"]]
 offset_left = -20.0
 offset_left = -20.0
 offset_top = 255.0
 offset_top = 255.0
 offset_right = 716.0
 offset_right = 716.0
 offset_bottom = 300.0
 offset_bottom = 300.0
 mouse_filter = 2
 mouse_filter = 2
 color = Color(0.101961, 0.101961, 0.101961, 0.329412)
 color = Color(0.101961, 0.101961, 0.101961, 0.329412)
+script = ExtResource("2_du0of")
 
 
 [node name="AutomationEditor" type="Control" parent="BreakFileMaker"]
 [node name="AutomationEditor" type="Control" parent="BreakFileMaker"]
 layout_mode = 3
 layout_mode = 3

+ 4 - 2
scenes/main/audio_settings.tscn

@@ -1,6 +1,7 @@
-[gd_scene load_steps=2 format=3 uid="uid://dta7rfalv4uvd"]
+[gd_scene load_steps=3 format=3 uid="uid://dta7rfalv4uvd"]
 
 
 [ext_resource type="Script" uid="uid://c7krcoq5poxdn" path="res://scenes/main/scripts/audiosettings.gd" id="2_7qbns"]
 [ext_resource type="Script" uid="uid://c7krcoq5poxdn" path="res://scenes/main/scripts/audiosettings.gd" id="2_7qbns"]
+[ext_resource type="Script" uid="uid://c503vew41pw80" path="res://scenes/main/scripts/color_rect_theme_invert.gd" id="2_am4qw"]
 
 
 [node name="AudioSettings" type="Window"]
 [node name="AudioSettings" type="Window"]
 title = "Audio Settings"
 title = "Audio Settings"
@@ -11,10 +12,11 @@ unresizable = true
 always_on_top = true
 always_on_top = true
 script = ExtResource("2_7qbns")
 script = ExtResource("2_7qbns")
 
 
-[node name="ColorRect" type="ColorRect" parent="."]
+[node name="ColorRect" type="ColorRect" parent="." groups=["invertable_background"]]
 offset_right = 604.0
 offset_right = 604.0
 offset_bottom = 204.0
 offset_bottom = 204.0
 color = Color(0.101961, 0.101961, 0.101961, 0.6)
 color = Color(0.101961, 0.101961, 0.101961, 0.6)
+script = ExtResource("2_am4qw")
 
 
 [node name="VBoxContainer" type="VBoxContainer" parent="."]
 [node name="VBoxContainer" type="VBoxContainer" parent="."]
 offset_left = 12.0
 offset_left = 12.0

+ 73 - 5
scenes/main/control.tscn

@@ -1,9 +1,11 @@
-[gd_scene load_steps=11 format=3 uid="uid://bcs87y7ptx3ke"]
+[gd_scene load_steps=13 format=3 uid="uid://bcs87y7ptx3ke"]
 
 
 [ext_resource type="Script" uid="uid://bdlfvuljckmu1" path="res://scenes/main/scripts/control.gd" id="1_2f0aq"]
 [ext_resource type="Script" uid="uid://bdlfvuljckmu1" path="res://scenes/main/scripts/control.gd" id="1_2f0aq"]
 [ext_resource type="Script" uid="uid://l2yejnjysupr" path="res://scenes/main/scripts/graph_edit.gd" id="2_3ioqo"]
 [ext_resource type="Script" uid="uid://l2yejnjysupr" path="res://scenes/main/scripts/graph_edit.gd" id="2_3ioqo"]
+[ext_resource type="Texture2D" uid="uid://drn34trxhf80f" path="res://theme/images/open_explore.png" id="3_4na11"]
 [ext_resource type="PackedScene" uid="uid://b0wdj8v6o0wq0" path="res://scenes/menu/menu.tscn" id="3_dtf4o"]
 [ext_resource type="PackedScene" uid="uid://b0wdj8v6o0wq0" path="res://scenes/menu/menu.tscn" id="3_dtf4o"]
 [ext_resource type="Texture2D" uid="uid://cdwux1smquvpi" path="res://theme/images/logo.png" id="4_3ioqo"]
 [ext_resource type="Texture2D" uid="uid://cdwux1smquvpi" path="res://theme/images/logo.png" id="4_3ioqo"]
+[ext_resource type="Script" uid="uid://c503vew41pw80" path="res://scenes/main/scripts/color_rect_theme_invert.gd" id="4_mg8al"]
 [ext_resource type="PackedScene" uid="uid://dta7rfalv4uvd" path="res://scenes/main/audio_settings.tscn" id="5_dtf4o"]
 [ext_resource type="PackedScene" uid="uid://dta7rfalv4uvd" path="res://scenes/main/audio_settings.tscn" id="5_dtf4o"]
 [ext_resource type="Script" uid="uid://cyhaucukdha8a" path="res://scenes/main/scripts/console.gd" id="5_fbaj0"]
 [ext_resource type="Script" uid="uid://cyhaucukdha8a" path="res://scenes/main/scripts/console.gd" id="5_fbaj0"]
 [ext_resource type="Script" uid="uid://wja0lo4nobh1" path="res://scenes/main/scripts/about_menu.gd" id="5_yf4wl"]
 [ext_resource type="Script" uid="uid://wja0lo4nobh1" path="res://scenes/main/scripts/about_menu.gd" id="5_yf4wl"]
@@ -29,6 +31,15 @@ offset_top = 32.0
 right_disconnects = true
 right_disconnects = true
 script = ExtResource("2_3ioqo")
 script = ExtResource("2_3ioqo")
 
 
+[node name="Button" type="Button" parent="."]
+layout_mode = 0
+offset_left = 301.0
+offset_top = 44.0
+offset_right = 325.0
+offset_bottom = 71.0
+tooltip_text = "Explore available processes (Ctrl/Cmd + E)"
+icon = ExtResource("3_4na11")
+
 [node name="FileDialog" type="FileDialog" parent="."]
 [node name="FileDialog" type="FileDialog" parent="."]
 title = "Open a Directory"
 title = "Open a Directory"
 ok_button_text = "Select Current Folder"
 ok_button_text = "Select Current Folder"
@@ -51,6 +62,12 @@ unresizable = true
 borderless = true
 borderless = true
 popup_window = true
 popup_window = true
 
 
+[node name="ColorRect" type="ColorRect" parent="NoLocationPopup" groups=["invertable_background"]]
+offset_right = 459.0
+offset_bottom = 514.0
+color = Color(0.101961, 0.101961, 0.101961, 0.6)
+script = ExtResource("4_mg8al")
+
 [node name="TextureRect" type="TextureRect" parent="NoLocationPopup"]
 [node name="TextureRect" type="TextureRect" parent="NoLocationPopup"]
 offset_left = -6.0
 offset_left = -6.0
 offset_top = 16.0
 offset_top = 16.0
@@ -98,6 +115,12 @@ unresizable = true
 borderless = true
 borderless = true
 popup_window = true
 popup_window = true
 
 
+[node name="ColorRect" type="ColorRect" parent="NoInputPopup" groups=["invertable_background"]]
+offset_right = 506.0
+offset_bottom = 421.0
+color = Color(0.101961, 0.101961, 0.101961, 0.6)
+script = ExtResource("4_mg8al")
+
 [node name="Label" type="Label" parent="NoInputPopup"]
 [node name="Label" type="Label" parent="NoInputPopup"]
 offset_left = 14.0
 offset_left = 14.0
 offset_top = 7.0
 offset_top = 7.0
@@ -130,6 +153,12 @@ unresizable = true
 borderless = true
 borderless = true
 popup_window = true
 popup_window = true
 
 
+[node name="ColorRect" type="ColorRect" parent="WrongFolderPopup" groups=["invertable_background"]]
+offset_right = 506.0
+offset_bottom = 421.0
+color = Color(0.101961, 0.101961, 0.101961, 0.6)
+script = ExtResource("4_mg8al")
+
 [node name="Label" type="Label" parent="WrongFolderPopup"]
 [node name="Label" type="Label" parent="WrongFolderPopup"]
 offset_left = 14.0
 offset_left = 14.0
 offset_top = 13.0
 offset_top = 13.0
@@ -169,6 +198,12 @@ exclusive = true
 unresizable = true
 unresizable = true
 popup_window = true
 popup_window = true
 
 
+[node name="ColorRect" type="ColorRect" parent="AudioDevicePopup" groups=["invertable_background"]]
+offset_right = 506.0
+offset_bottom = 421.0
+color = Color(0.101961, 0.101961, 0.101961, 0.6)
+script = ExtResource("4_mg8al")
+
 [node name="Label" type="Label" parent="AudioDevicePopup"]
 [node name="Label" type="Label" parent="AudioDevicePopup"]
 offset_left = 14.0
 offset_left = 14.0
 offset_top = 7.0
 offset_top = 7.0
@@ -198,6 +233,12 @@ unresizable = true
 borderless = true
 borderless = true
 popup_window = true
 popup_window = true
 
 
+[node name="ColorRect" type="ColorRect" parent="MultipleConnectionsPopup" groups=["invertable_background"]]
+offset_right = 506.0
+offset_bottom = 421.0
+color = Color(0.101961, 0.101961, 0.101961, 0.6)
+script = ExtResource("4_mg8al")
+
 [node name="Label" type="Label" parent="MultipleConnectionsPopup"]
 [node name="Label" type="Label" parent="MultipleConnectionsPopup"]
 offset_left = 14.0
 offset_left = 14.0
 offset_top = 10.0
 offset_top = 10.0
@@ -259,6 +300,7 @@ offset_left = 12.0
 offset_top = 352.0
 offset_top = 352.0
 offset_right = 292.0
 offset_right = 292.0
 offset_bottom = 393.0
 offset_bottom = 393.0
+disabled = true
 text = "Stop Running Thread"
 text = "Stop Running Thread"
 
 
 [node name="ConsoleRightClick" type="PopupMenu" parent="Console"]
 [node name="ConsoleRightClick" type="PopupMenu" parent="Console"]
@@ -268,13 +310,13 @@ item_0/id = 0
 item_1/text = "Copy"
 item_1/text = "Copy"
 item_1/id = 1
 item_1/id = 1
 
 
-[node name="ColorRect" type="ColorRect" parent="."]
+[node name="MenuBarBackground" type="ColorRect" parent="."]
 layout_mode = 1
 layout_mode = 1
 anchors_preset = 10
 anchors_preset = 10
 anchor_right = 1.0
 anchor_right = 1.0
 offset_bottom = 35.0
 offset_bottom = 35.0
 grow_horizontal = 2
 grow_horizontal = 2
-color = Color(0.0646965, 0.0646965, 0.0646965, 1)
+color = Color(0.93408, 0.93408, 0.93408, 1)
 
 
 [node name="MenuBar" type="MenuBar" parent="."]
 [node name="MenuBar" type="MenuBar" parent="."]
 layout_mode = 1
 layout_mode = 1
@@ -373,6 +415,12 @@ unresizable = true
 borderless = true
 borderless = true
 popup_window = true
 popup_window = true
 
 
+[node name="ColorRect" type="ColorRect" parent="SaveChangesPopup" groups=["invertable_background"]]
+offset_right = 369.0
+offset_bottom = 114.0
+color = Color(0.101961, 0.101961, 0.101961, 0.6)
+script = ExtResource("4_mg8al")
+
 [node name="Label" type="Label" parent="SaveChangesPopup"]
 [node name="Label" type="Label" parent="SaveChangesPopup"]
 offset_left = -1.0
 offset_left = -1.0
 offset_top = 21.0
 offset_top = 21.0
@@ -409,14 +457,20 @@ visible = false
 [node name="SearchMenu" type="PopupPanel" parent="." groups=["popup_windows"]]
 [node name="SearchMenu" type="PopupPanel" parent="." groups=["popup_windows"]]
 auto_translate_mode = 1
 auto_translate_mode = 1
 position = Vector2i(100, 100)
 position = Vector2i(100, 100)
-size = Vector2i(600, 53)
+size = Vector2i(600, 77)
 script = ExtResource("6_fyarh")
 script = ExtResource("6_fyarh")
 
 
 [node name="VBoxContainer" type="VBoxContainer" parent="SearchMenu"]
 [node name="VBoxContainer" type="VBoxContainer" parent="SearchMenu"]
 offset_left = 4.0
 offset_left = 4.0
 offset_top = 4.0
 offset_top = 4.0
 offset_right = 596.0
 offset_right = 596.0
-offset_bottom = 49.0
+offset_bottom = 73.0
+
+[node name="ReplaceLabel" type="Label" parent="SearchMenu/VBoxContainer"]
+layout_mode = 2
+text = "Replace"
+clip_text = true
+text_overrun_behavior = 3
 
 
 [node name="SearchBar" type="LineEdit" parent="SearchMenu/VBoxContainer"]
 [node name="SearchBar" type="LineEdit" parent="SearchMenu/VBoxContainer"]
 layout_mode = 2
 layout_mode = 2
@@ -447,6 +501,12 @@ exclusive = true
 unresizable = true
 unresizable = true
 popup_window = true
 popup_window = true
 
 
+[node name="ColorRect" type="ColorRect" parent="CheckForUpdates/UpdatePopup" groups=["invertable_background"]]
+offset_right = 398.0
+offset_bottom = 313.0
+color = Color(0.101961, 0.101961, 0.101961, 0.6)
+script = ExtResource("4_mg8al")
+
 [node name="Label" type="Label" parent="CheckForUpdates/UpdatePopup"]
 [node name="Label" type="Label" parent="CheckForUpdates/UpdatePopup"]
 offset_left = 14.0
 offset_left = 14.0
 offset_top = 6.0
 offset_top = 6.0
@@ -483,6 +543,12 @@ transient = true
 exclusive = true
 exclusive = true
 borderless = true
 borderless = true
 
 
+[node name="ColorRect" type="ColorRect" parent="ProgressWindow" groups=["invertable_background"]]
+offset_right = 613.0
+offset_bottom = 121.0
+color = Color(0.101961, 0.101961, 0.101961, 0.6)
+script = ExtResource("4_mg8al")
+
 [node name="ProgressBar" type="ProgressBar" parent="ProgressWindow"]
 [node name="ProgressBar" type="ProgressBar" parent="ProgressWindow"]
 offset_left = 10.0
 offset_left = 10.0
 offset_top = 11.0
 offset_top = 11.0
@@ -511,10 +577,12 @@ text = "Stop Running Thread"
 [connection signal="copy_nodes_request" from="GraphEdit" to="GraphEdit" method="_on_copy_nodes_request"]
 [connection signal="copy_nodes_request" from="GraphEdit" to="GraphEdit" method="_on_copy_nodes_request"]
 [connection signal="delete_nodes_request" from="GraphEdit" to="GraphEdit" method="_on_graph_edit_delete_nodes_request"]
 [connection signal="delete_nodes_request" from="GraphEdit" to="GraphEdit" method="_on_graph_edit_delete_nodes_request"]
 [connection signal="disconnection_request" from="GraphEdit" to="GraphEdit" method="_on_graph_edit_disconnection_request"]
 [connection signal="disconnection_request" from="GraphEdit" to="GraphEdit" method="_on_graph_edit_disconnection_request"]
+[connection signal="gui_input" from="GraphEdit" to="GraphEdit" method="_on_gui_input"]
 [connection signal="node_deselected" from="GraphEdit" to="GraphEdit" method="_on_graph_edit_node_deselected"]
 [connection signal="node_deselected" from="GraphEdit" to="GraphEdit" method="_on_graph_edit_node_deselected"]
 [connection signal="node_selected" from="GraphEdit" to="GraphEdit" method="_on_graph_edit_node_selected"]
 [connection signal="node_selected" from="GraphEdit" to="GraphEdit" method="_on_graph_edit_node_selected"]
 [connection signal="paste_nodes_request" from="GraphEdit" to="GraphEdit" method="_on_paste_nodes_request"]
 [connection signal="paste_nodes_request" from="GraphEdit" to="GraphEdit" method="_on_paste_nodes_request"]
 [connection signal="popup_request" from="GraphEdit" to="." method="_on_graph_edit_popup_request"]
 [connection signal="popup_request" from="GraphEdit" to="." method="_on_graph_edit_popup_request"]
+[connection signal="button_down" from="Button" to="." method="open_explore"]
 [connection signal="dir_selected" from="FileDialog" to="." method="_on_file_dialog_dir_selected"]
 [connection signal="dir_selected" from="FileDialog" to="." method="_on_file_dialog_dir_selected"]
 [connection signal="close_requested" from="mainmenu" to="." method="_on_mainmenu_close_requested"]
 [connection signal="close_requested" from="mainmenu" to="." method="_on_mainmenu_close_requested"]
 [connection signal="meta_clicked" from="NoLocationPopup/RichTextLabel" to="." method="_on_rich_text_label_meta_clicked"]
 [connection signal="meta_clicked" from="NoLocationPopup/RichTextLabel" to="." method="_on_rich_text_label_meta_clicked"]

+ 3 - 1
scenes/main/help_window.tscn

@@ -1,6 +1,7 @@
-[gd_scene load_steps=2 format=3 uid="uid://cp5uwxjskqgr7"]
+[gd_scene load_steps=3 format=3 uid="uid://cp5uwxjskqgr7"]
 
 
 [ext_resource type="Script" uid="uid://bkte84glywsny" path="res://scenes/main/scripts/help_window.gd" id="1_ro77y"]
 [ext_resource type="Script" uid="uid://bkte84glywsny" path="res://scenes/main/scripts/help_window.gd" id="1_ro77y"]
+[ext_resource type="Script" uid="uid://c503vew41pw80" path="res://scenes/main/scripts/color_rect_theme_invert.gd" id="2_5iwbo"]
 
 
 [node name="HelpWindow" type="Window"]
 [node name="HelpWindow" type="Window"]
 auto_translate_mode = 1
 auto_translate_mode = 1
@@ -14,6 +15,7 @@ script = ExtResource("1_ro77y")
 offset_right = 604.0
 offset_right = 604.0
 offset_bottom = 506.0
 offset_bottom = 506.0
 color = Color(0.101961, 0.101961, 0.101961, 0.6)
 color = Color(0.101961, 0.101961, 0.101961, 0.6)
+script = ExtResource("2_5iwbo")
 
 
 [node name="HelpTitle" type="Label" parent="."]
 [node name="HelpTitle" type="Label" parent="."]
 offset_left = 12.0
 offset_left = 12.0

+ 11 - 0
scenes/main/scripts/color_rect_theme_invert.gd

@@ -0,0 +1,11 @@
+extends ColorRect
+
+
+# Called when the node enters the scene tree for the first time.
+func _ready() -> void:
+	var interface_settings = ConfigHandler.load_interface_settings()
+	#check if the theme is inverted
+	if interface_settings.invert_theme:
+		color = Color(0.898, 0.898, 0.898, 0.6)
+	else:
+		color = Color(0.102, 0.102, 0.102, 0.6)

+ 1 - 0
scenes/main/scripts/color_rect_theme_invert.gd.uid

@@ -0,0 +1 @@
+uid://c503vew41pw80

+ 221 - 22
scenes/main/scripts/control.gd

@@ -8,16 +8,20 @@ var delete_intermediate_outputs # tracks state of delete intermediate outputs to
 @onready var console_output: RichTextLabel = $Console/ConsoleOutput
 @onready var console_output: RichTextLabel = $Console/ConsoleOutput
 var undo_redo := UndoRedo.new() 
 var undo_redo := UndoRedo.new() 
 var output_audio_player #tracks the node that is the current output player for linking
 var output_audio_player #tracks the node that is the current output player for linking
-var input_audio_player #tracks node that is the current input player for linking
+var input_audio_player #potentially unused, remove? tracks node that is the current input player for linking
 var currentfile = "none" #tracks dir of currently loaded file for saving
 var currentfile = "none" #tracks dir of currently loaded file for saving
 var changesmade = false #tracks if user has made changes to the currently loaded save file
 var changesmade = false #tracks if user has made changes to the currently loaded save file
 var savestate # tracks what the user is trying to do when savechangespopup is called
 var savestate # tracks what the user is trying to do when savechangespopup is called
 var helpfile #tracks which help file the user was trying to load when savechangespopup is called
 var helpfile #tracks which help file the user was trying to load when savechangespopup is called
 var outfilename #links to the user name for outputfile field
 var outfilename #links to the user name for outputfile field
 var foldertoggle #links to the reuse folder button
 var foldertoggle #links to the reuse folder button
-var lastoutputfolder = "none" #tracks last output folder, this can in future be used to replace global.outfile but i cba right now
-var uiscale = 1.0 #tracks scaling for retina screens
+#var lastoutputfolder = "none" #tracks last output folder, this can in future be used to replace global.outfile but i cba right now
+var uiscale = 0.0 # tracks the overal ui scale after hidpi adjustment and user offset
+var retina_scaling = 1.0 #tracks scaling for retina screens
 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
 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
+var main_theme = preload("res://theme/main_theme.tres") #load the theme
+var default_input_node #stores a reference to the input node created on launch to allow auto loading a wav file
+var output_folder_label
 
 
 
 
 #scripts
 #scripts
@@ -51,11 +55,13 @@ func _ready() -> void:
 	
 	
 	get_tree().set_auto_accept_quit(false) #disable closing the app with the x and instead handle it internally
 	get_tree().set_auto_accept_quit(false) #disable closing the app with the x and instead handle it internally
 	
 	
+	
 	load_scripts()
 	load_scripts()
 	make_signal_connections()
 	make_signal_connections()
-	check_user_preferences()
 	hidpi_adjustment()
 	hidpi_adjustment()
+	check_user_preferences()
 	new_patch()
 	new_patch()
+	await get_tree().process_frame
 	load_from_filesystem()
 	load_from_filesystem()
 	check_cdp_location_set()
 	check_cdp_location_set()
 	
 	
@@ -77,20 +83,37 @@ func load_scripts():
 
 
 func make_signal_connections():
 func make_signal_connections():
 	get_node("SearchMenu").make_node.connect(graph_edit._make_node)
 	get_node("SearchMenu").make_node.connect(graph_edit._make_node)
+	get_node("SearchMenu").swap_node.connect(graph_edit._swap_node)
+	get_node("SearchMenu").connect_to_clicked_node.connect(graph_edit._connect_to_clicked_node)
 	get_node("mainmenu").make_node.connect(graph_edit._make_node)
 	get_node("mainmenu").make_node.connect(graph_edit._make_node)
 	get_node("mainmenu").open_help.connect(open_help.show_help_for_node)
 	get_node("mainmenu").open_help.connect(open_help.show_help_for_node)
 	get_node("Settings").open_cdp_location.connect(show_cdp_location)
 	get_node("Settings").open_cdp_location.connect(show_cdp_location)
 	get_node("Settings").console_on_top.connect(change_console_settings)
 	get_node("Settings").console_on_top.connect(change_console_settings)
+	get_node("Settings").invert_ui.connect(invert_theme_toggled)
+	get_node("Settings").swap_zoom_and_move.connect(swap_zoom_and_move)
+	get_node("Settings").ui_scale_multiplier_changed.connect(scale_ui)
+	get_window().files_dropped.connect(on_files_dropped)
 	
 	
 func hidpi_adjustment():
 func hidpi_adjustment():
 	#checks if display is hidpi and scales ui accordingly hidpi - 144
 	#checks if display is hidpi and scales ui accordingly hidpi - 144
 	if DisplayServer.screen_get_dpi(0) >= 144:
 	if DisplayServer.screen_get_dpi(0) >= 144:
-		uiscale = 2.0
-		get_window().content_scale_factor = uiscale
-		#goes through popup_windows group and scales all popups and resizes them
-		for window in get_tree().get_nodes_in_group("popup_windows"):
+		retina_scaling = 2.0
+	else:
+		retina_scaling = 1.0
+		
+
+func scale_ui(scale_multiplier: float):
+	var old_uiscale = uiscale
+	uiscale = retina_scaling * scale_multiplier
+	get_window().content_scale_factor = uiscale
+	#goes through popup_windows group and scales all popups and resizes them
+	for window in get_tree().get_nodes_in_group("popup_windows"):
+		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
+			window.size = (window.size / old_uiscale) * uiscale
+		else:
 			window.size = window.size * uiscale
 			window.size = window.size * uiscale
-			window.content_scale_factor = uiscale
+		window.content_scale_factor = uiscale
+	
 
 
 func load_from_filesystem():
 func load_from_filesystem():
 	#checks if user has opened a file from the system file menu and loads it
 	#checks if user has opened a file from the system file menu and loads it
@@ -100,6 +123,9 @@ func load_from_filesystem():
 		if FileAccess.file_exists(path) and path.get_extension().to_lower() == "thd":
 		if FileAccess.file_exists(path) and path.get_extension().to_lower() == "thd":
 			save_load.load_graph_edit(path)
 			save_load.load_graph_edit(path)
 			break
 			break
+		if FileAccess.file_exists(path) and path.get_extension().to_lower() == "wav":
+			default_input_node.get_node("AudioPlayer")._on_file_selected(path)
+			break
 
 
 func new_patch():
 func new_patch():
 	#clear old patch
 	#clear old patch
@@ -118,16 +144,28 @@ func new_patch():
 	effect.name = "inputfile"
 	effect.name = "inputfile"
 	get_node("GraphEdit").add_child(effect, true)
 	get_node("GraphEdit").add_child(effect, true)
 	effect.connect("open_help", Callable(open_help, "show_help_for_node"))
 	effect.connect("open_help", Callable(open_help, "show_help_for_node"))
+	if effect.has_signal("node_moved"):
+		effect.node_moved.connect(graph_edit._auto_link_nodes)
 	effect.position_offset = Vector2(20,80)
 	effect.position_offset = Vector2(20,80)
+	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
 	
 	
 	effect = Nodes.get_node(NodePath("outputfile")).duplicate()
 	effect = Nodes.get_node(NodePath("outputfile")).duplicate()
 	effect.name = "outputfile"
 	effect.name = "outputfile"
 	get_node("GraphEdit").add_child(effect, true)
 	get_node("GraphEdit").add_child(effect, true)
 	effect.init() #initialise ui from user prefs
 	effect.init() #initialise ui from user prefs
 	effect.connect("open_help", Callable(open_help, "show_help_for_node"))
 	effect.connect("open_help", Callable(open_help, "show_help_for_node"))
+	if effect.has_signal("node_moved"):
+		effect.node_moved.connect(graph_edit._auto_link_nodes)
 	effect.position_offset = Vector2((DisplayServer.screen_get_size().x - 480) / uiscale, 80)
 	effect.position_offset = Vector2((DisplayServer.screen_get_size().x - 480) / uiscale, 80)
 	graph_edit._register_node_movement() #link nodes for tracking position changes for changes tracking
 	graph_edit._register_node_movement() #link nodes for tracking position changes for changes tracking
 	
 	
+	#set label for last output folder
+	var interface_settings = ConfigHandler.load_interface_settings()
+	output_folder_label = effect.get_node("OutputFolderMargin/OutputFolderLabel")
+	if output_folder_label != null and interface_settings.last_used_output_folder != "no_file":
+		output_folder_label.text = interface_settings.last_used_output_folder
+		output_folder_label.get_parent().tooltip_text = interface_settings.last_used_output_folder
+	
 	changesmade = false #so it stops trying to save unchanged empty files
 	changesmade = false #so it stops trying to save unchanged empty files
 	get_window().title = "SoundThread"
 	get_window().title = "SoundThread"
 	link_output()
 	link_output()
@@ -179,6 +217,15 @@ func check_user_preferences():
 			RenderingServer.set_default_clear_color(Color("#98d4d2"))
 			RenderingServer.set_default_clear_color(Color("#98d4d2"))
 		3:
 		3:
 			RenderingServer.set_default_clear_color(Color(interface_settings.theme_custom_colour))
 			RenderingServer.set_default_clear_color(Color(interface_settings.theme_custom_colour))
+			
+	#set the theme to either the main theme or inverted theme depending on user preferences
+	invert_theme_toggled(interface_settings.invert_theme)
+	swap_zoom_and_move(interface_settings.swap_zoom_and_move)
+	
+	#scale ui
+	scale_ui(interface_settings.ui_scale_multiplier)
+
+		
 func show_cdp_location():
 func show_cdp_location():
 	$CdpLocationDialog.show()
 	$CdpLocationDialog.show()
 	
 	
@@ -252,8 +299,8 @@ func _input(event):
 		simulate_mouse_click()
 		simulate_mouse_click()
 		await get_tree().process_frame
 		await get_tree().process_frame
 		undo_redo.undo()
 		undo_redo.undo()
-	elif event.is_action_pressed("redo"):
-		undo_redo.redo()
+	#elif event.is_action_pressed("redo"):
+		#undo_redo.redo()
 	elif event.is_action_pressed("save"):
 	elif event.is_action_pressed("save"):
 		if currentfile == "none":
 		if currentfile == "none":
 			savestate = "saveas"
 			savestate = "saveas"
@@ -278,8 +325,6 @@ func _input(event):
 		savestate = "saveas"
 		savestate = "saveas"
 		$SaveDialog.popup_centered()
 		$SaveDialog.popup_centered()
 	
 	
-
-
 func simulate_mouse_click():
 func simulate_mouse_click():
 	#simulates clicking the middle mouse button in order to hide any visible tooltips
 	#simulates clicking the middle mouse button in order to hide any visible tooltips
 	var click_pos = get_viewport().get_mouse_position()
 	var click_pos = get_viewport().get_mouse_position()
@@ -299,19 +344,24 @@ func simulate_mouse_click():
 
 
 func _run_process() -> void:
 func _run_process() -> void:
 	#check if any of the inputfile nodes don't have files loaded
 	#check if any of the inputfile nodes don't have files loaded
+	var interface_settings = ConfigHandler.load_interface_settings()
 	for node in graph_edit.get_children():
 	for node in graph_edit.get_children():
 		if node.get_meta("command") == "inputfile" and node.get_node("AudioPlayer").has_meta("inputfile") == false:
 		if node.get_meta("command") == "inputfile" and node.get_node("AudioPlayer").has_meta("inputfile") == false:
 			$NoInputPopup.popup_centered()
 			$NoInputPopup.popup_centered()
 			return
 			return
 	#check if the reuse folder toggle is set and a folder has been previously chosen
 	#check if the reuse folder toggle is set and a folder has been previously chosen
-	if foldertoggle.button_pressed == true and lastoutputfolder != "none":
-		_on_file_dialog_dir_selected(lastoutputfolder)
+	var output_folder = interface_settings.last_used_output_folder
+	if foldertoggle.button_pressed == true and output_folder != "no_file" and DirAccess.open(output_folder) != null:
+		_on_file_dialog_dir_selected(output_folder)
 	else:
 	else:
 		$FileDialog.show()
 		$FileDialog.show()
 			
 			
 
 
 func _on_file_dialog_dir_selected(dir: String) -> void:
 func _on_file_dialog_dir_selected(dir: String) -> void:
-	lastoutputfolder = dir
+	ConfigHandler.save_interface_settings("last_used_output_folder", dir)
+	if output_folder_label != null:
+		output_folder_label.text = dir
+		output_folder_label.tooltip_text = dir
 	console_output.clear()
 	console_output.clear()
 	var interface_settings = ConfigHandler.load_interface_settings()
 	var interface_settings = ConfigHandler.load_interface_settings()
 	if interface_settings.disable_progress_bar == false:
 	if interface_settings.disable_progress_bar == false:
@@ -335,10 +385,35 @@ func _on_file_dialog_dir_selected(dir: String) -> void:
 	var second = str(time_dict.second).pad_zeros(2)
 	var second = str(time_dict.second).pad_zeros(2)
 	var time_str = hour + "-" + minute + "-" + second
 	var time_str = hour + "-" + minute + "-" + second
 	Global.outfile = dir + "/" + outfilename.text.get_basename() + "_" + Time.get_date_string_from_system() + "_" + time_str
 	Global.outfile = dir + "/" + outfilename.text.get_basename() + "_" + Time.get_date_string_from_system() + "_" + time_str
-	run_thread.log_console("Output directory and file name(s):" + Global.outfile, true)
-	await get_tree().process_frame
 	
 	
-	run_thread.run_thread_with_branches()
+	#check path and file name do not contain special characters
+	var check_characters = Global.outfile.get_basename().split("/")
+	var invalid_chars:= []
+	var regex = RegEx.new()
+	regex.compile("[^a-zA-Z0-9\\-_ :+]")
+	for string in check_characters:
+		if string != "":
+			var result = regex.search_all(string)
+			for matches in result:
+				var char = matches.get_string()
+				if invalid_chars.has(char) == false:
+					invalid_chars.append(char)
+
+	var invalid_string = " ".join(invalid_chars)
+	
+	if invalid_chars.size() == 0:
+		run_thread.log_console("Output directory and file name(s):" + Global.outfile, true)
+		await get_tree().process_frame
+		
+		run_thread.run_thread_with_branches()
+	else:
+		run_thread.log_console("[color=#9c2828][b]Error:[/b][/color] Chosen file name or folder path " + Global.outfile.get_basename() + " contains invalid characters.", true)
+		run_thread.log_console("File names and paths can only contain A-Z a-z 0-9 - _ + and space.", true)
+		run_thread.log_console("Chosen file name/path contains the following invalid characters: " + invalid_string, true)
+		if $ProgressWindow.visible:
+			$ProgressWindow.hide()
+		if !$Console.visible:
+			$Console.popup_centered()
 
 
 func _toggle_delete(toggled_on: bool):
 func _toggle_delete(toggled_on: bool):
 	delete_intermediate_outputs = toggled_on
 	delete_intermediate_outputs = toggled_on
@@ -351,7 +426,10 @@ func _on_console_close_requested() -> void:
 
 
 func _on_console_open_folder_button_down() -> void:
 func _on_console_open_folder_button_down() -> void:
 	$Console.hide()
 	$Console.hide()
-	OS.shell_open(Global.outfile.get_base_dir())
+	var interface_settings = ConfigHandler.load_interface_settings()
+	var output_folder = interface_settings.last_used_output_folder
+	if output_folder != "no_file" and DirAccess.open(output_folder) != null:
+		OS.shell_open(output_folder)
 
 
 
 
 func _on_ok_button_2_button_down() -> void:
 func _on_ok_button_2_button_down() -> void:
@@ -368,6 +446,7 @@ func _on_settings_button_index_pressed(index: int) -> void:
 	
 	
 	match index:
 	match index:
 		0:
 		0:
+			$Settings.cdpprogs_location = cdpprogs_location
 			$Settings.popup_centered()
 			$Settings.popup_centered()
 		1:
 		1:
 			$AudioSettings.popup_centered()
 			$AudioSettings.popup_centered()
@@ -587,8 +666,10 @@ func _notification(what):
 			get_tree().quit() # default behavior
 			get_tree().quit() # default behavior
 			
 			
 func _open_output_folder():
 func _open_output_folder():
-	if lastoutputfolder != "none":
-		OS.shell_open(lastoutputfolder)
+	var interface_settings = ConfigHandler.load_interface_settings()
+	var output_folder = interface_settings.last_used_output_folder
+	if output_folder != "no_file" and DirAccess.open(output_folder) != null:
+		OS.shell_open(output_folder)
 		
 		
 
 
 func _on_rich_text_label_meta_clicked(meta: Variant) -> void:
 func _on_rich_text_label_meta_clicked(meta: Variant) -> void:
@@ -599,6 +680,9 @@ func _on_rich_text_label_meta_clicked(meta: Variant) -> void:
 func _on_graph_edit_popup_request(at_position: Vector2) -> void:
 func _on_graph_edit_popup_request(at_position: Vector2) -> void:
 
 
 	effect_position = graph_edit.get_local_mouse_position()
 	effect_position = graph_edit.get_local_mouse_position()
+	
+	#give the search menu the ui scale
+	$SearchMenu.uiscale = uiscale
 
 
 	#get the mouse position in screen coordinates
 	#get the mouse position in screen coordinates
 	var mouse_screen_pos = DisplayServer.mouse_get_position()  
 	var mouse_screen_pos = DisplayServer.mouse_get_position()  
@@ -607,6 +691,38 @@ func _on_graph_edit_popup_request(at_position: Vector2) -> void:
 	#get the window size relative to its scaling for retina displays
 	#get the window size relative to its scaling for retina displays
 	var window_size = get_window().size * DisplayServer.screen_get_scale()
 	var window_size = get_window().size * DisplayServer.screen_get_scale()
 
 
+	#see if it was empty space or a node that was right clicked
+	var clicked_node
+	for child in graph_edit.get_children():
+		if child is GraphNode:
+			if Rect2(child.position, child.size).has_point(effect_position):
+				clicked_node = child
+				break
+	
+	if clicked_node and clicked_node.get_meta("command") != "outputfile":
+		var title = clicked_node.title
+		if Input.is_action_pressed("auto_link_nodes"):
+			$SearchMenu/VBoxContainer/ReplaceLabel.text = "Connect to " + title
+			$SearchMenu/VBoxContainer/ReplaceLabel.show()
+			$SearchMenu.replace_node = false
+			$SearchMenu.connect_to_node = true
+			$SearchMenu.node_to_connect_to = clicked_node
+		else:
+			$SearchMenu/VBoxContainer/ReplaceLabel.text = "Replace " + title
+			$SearchMenu/VBoxContainer/ReplaceLabel.show()
+			$SearchMenu.replace_node = true
+			$SearchMenu.connect_to_node = false
+			$SearchMenu.node_to_replace = clicked_node
+	else:
+		var interface_settings = ConfigHandler.load_interface_settings()
+		if interface_settings.right_click_opens_explore:
+			open_explore()
+			return
+		else:
+			$SearchMenu/VBoxContainer/ReplaceLabel.hide()
+			$SearchMenu.replace_node = false
+			$SearchMenu.connect_to_node = false
+	
 	#calculate the xy position of the mouse clamped to the size of the window and menu so it doesn't go off the screen
 	#calculate the xy position of the mouse clamped to the size of the window and menu so it doesn't go off the screen
 	var clamped_x = clamp(mouse_screen_pos.x, window_screen_pos.x, window_screen_pos.x + window_size.x - $SearchMenu.size.x)
 	var clamped_x = clamp(mouse_screen_pos.x, window_screen_pos.x, window_screen_pos.x + window_size.x - $SearchMenu.size.x)
 	var clamped_y = clamp(mouse_screen_pos.y, window_screen_pos.y, window_screen_pos.y + window_size.y - (420 * DisplayServer.screen_get_scale()))
 	var clamped_y = clamp(mouse_screen_pos.y, window_screen_pos.y, window_screen_pos.y + window_size.y - (420 * DisplayServer.screen_get_scale()))
@@ -657,3 +773,86 @@ func change_console_settings(toggled: bool):
 
 
 func _on_kill_process_button_down() -> void:
 func _on_kill_process_button_down() -> void:
 	run_thread._on_kill_process_button_down()
 	run_thread._on_kill_process_button_down()
+
+func invert_theme_toggled(toggled: bool):
+	if toggled:
+		var inverted = invert_theme(main_theme)
+		get_tree().root.theme = inverted # force refresh
+		$MenuBarBackground.color = Color(0.934, 0.934, 0.934)
+		for color_rect in get_tree().get_nodes_in_group("invertable_background"):
+			if color_rect is ColorRect:
+				color_rect.color = Color(0.898, 0.898, 0.898, 0.6)
+		
+	else:
+		get_tree().root.theme = main_theme # force refresheme = main_theme
+		$MenuBarBackground.color = Color(0.065, 0.065, 0.065)
+		
+		for color_rect in get_tree().get_nodes_in_group("invertable_background"):
+			if color_rect is ColorRect:
+				color_rect.color = Color(0.102, 0.102, 0.102, 0.6)
+		
+func invert_theme(theme: Theme) -> Theme:
+	var inverted_theme = theme.duplicate(true) # deep copy
+
+	# Check all types and color names in the theme
+	var types = inverted_theme.get_type_list()
+	for type in types:
+		var color_names = inverted_theme.get_color_list(type)
+		for cname in color_names:
+			var col = inverted_theme.get_color(cname, type)
+			var inverted = Color(1.0 - col.r, 1.0 - col.g, 1.0 - col.b, col.a)
+			inverted_theme.set_color(cname, type, inverted)
+
+		var style_names = inverted_theme.get_stylebox_list(type)
+		for sname in style_names:
+			if type == "GraphEdit" and sname == "panel":
+				continue
+			var sb = inverted_theme.get_stylebox(sname, type)
+			var new_sb = sb.duplicate()
+			if new_sb is StyleBoxFlat:
+				var col = new_sb.bg_color
+				new_sb.bg_color = Color(1.0 - col.r, 1.0 - col.g, 1.0 - col.b, col.a)
+			inverted_theme.set_stylebox(sname, type, new_sb)
+	
+	return inverted_theme
+
+func swap_zoom_and_move(toggled: bool):
+	if toggled:
+		graph_edit.set_panning_scheme(1)
+	else:
+		graph_edit.set_panning_scheme(0)
+
+func on_files_dropped(files):
+	var mouse_pos = graph_edit.get_local_mouse_position()
+	
+	#see if files were dropped on an input node
+	var dropped_node
+	for child in graph_edit.get_children():
+		if child is GraphNode:
+			if Rect2(child.position, child.size).has_point(mouse_pos):
+				dropped_node = child
+				break
+				
+	#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
+	if dropped_node and dropped_node.has_meta("command") and dropped_node.get_meta("command") == "inputfile":
+		if files[0].get_extension().to_lower() == "wav":
+			dropped_node.get_node("AudioPlayer")._on_file_selected(files[0])
+	else:
+		#else make a new input node at the mouse position and load it in
+		if files[0].get_extension().to_lower() == "wav":
+			var new_input_node = graph_edit._make_node("inputfile")
+			new_input_node.position_offset = mouse_pos
+			new_input_node.get_node("AudioPlayer")._on_file_selected(files[0])
+	
+	#remove first element from the array
+	files.remove_at(0)
+	
+	#check if there are any other files
+	if files.size() > 0:
+		var position_plus_offset = Vector2(mouse_pos.x, mouse_pos.y + 250) #apply a vertical offset from the mouse position so nodes dont overlap
+		for file in files:
+			if file.get_extension().to_lower() == "wav":
+				var new_input_node = graph_edit._make_node("inputfile")
+				new_input_node.position_offset = position_plus_offset
+				new_input_node.get_node("AudioPlayer")._on_file_selected(file)
+				position_plus_offset.y = position_plus_offset.y + 250

+ 255 - 40
scenes/main/scripts/graph_edit.gd

@@ -11,6 +11,10 @@ var node_data = {} #stores json with all nodes in it
 var valueslider = preload("res://scenes/Nodes/valueslider.tscn") #slider scene for use in nodes
 var valueslider = preload("res://scenes/Nodes/valueslider.tscn") #slider scene for use in nodes
 var addremoveinlets = preload("res://scenes/Nodes/addremoveinlets.tscn") #add remove inlets scene for use in nodes
 var addremoveinlets = preload("res://scenes/Nodes/addremoveinlets.tscn") #add remove inlets scene for use in nodes
 var node_logic = preload("res://scenes/Nodes/node_logic.gd") #load the script logic
 var node_logic = preload("res://scenes/Nodes/node_logic.gd") #load the script logic
+var selected_cables:= [] #used to track which cables are selected for changing colour and for deletion
+var theme_background #used to track if the theme has changed and if so change the cable selection colour
+var theme_custom_background
+var high_contrast_cables
 
 
 
 
 # Called when the node enters the scene tree for the first time.
 # Called when the node enters the scene tree for the first time.
@@ -26,6 +30,12 @@ func _ready() -> void:
 			node_data = result
 			node_data = result
 		else:
 		else:
 			push_error("Invalid JSON")
 			push_error("Invalid JSON")
+			
+	var interface_settings = ConfigHandler.load_interface_settings()
+	theme_background = interface_settings.theme
+	theme_custom_background = interface_settings.theme_custom_colour
+	high_contrast_cables = interface_settings.high_contrast_selected_cables
+	set_cable_colour(interface_settings.theme)
 
 
 func init(main_node: Node, graphedit: GraphEdit, openhelp: Callable, multipleconnections: Window) -> void:
 func init(main_node: Node, graphedit: GraphEdit, openhelp: Callable, multipleconnections: Window) -> void:
 	control_script = main_node
 	control_script = main_node
@@ -46,6 +56,8 @@ func _make_node(command: String, skip_undo_redo := false) -> GraphNode:
 			if command == "outputfile":
 			if command == "outputfile":
 				effect.init() #initialise ui from user prefs
 				effect.init() #initialise ui from user prefs
 			effect.connect("open_help", open_help)
 			effect.connect("open_help", open_help)
+			if effect.has_signal("node_moved"):
+				effect.node_moved.connect(_auto_link_nodes)
 			effect.set_position_offset((control_script.effect_position + graph_edit.scroll_offset) / graph_edit.zoom) #set node to current mouse position in graph edit
 			effect.set_position_offset((control_script.effect_position + graph_edit.scroll_offset) / graph_edit.zoom) #set node to current mouse position in graph edit
 			_register_inputs_in_node(effect) #link sliders for changes tracking
 			_register_inputs_in_node(effect) #link sliders for changes tracking
 			_register_node_movement() #link nodes for tracking position changes for changes tracking
 			_register_node_movement() #link nodes for tracking position changes for changes tracking
@@ -150,6 +162,7 @@ func _make_node(command: String, skip_undo_redo := false) -> GraphNode:
 						hslider.set_meta("min", min)
 						hslider.set_meta("min", min)
 						hslider.set_meta("max", max)
 						hslider.set_meta("max", max)
 						hslider.set_meta("flag", flag)
 						hslider.set_meta("flag", flag)
+						hslider.set_meta("default_value", value)
 						
 						
 						#set slider params
 						#set slider params
 						hslider.min_value = minrange
 						hslider.min_value = minrange
@@ -297,6 +310,7 @@ func _make_node(command: String, skip_undo_redo := false) -> GraphNode:
 			add_child(graphnode, true)
 			add_child(graphnode, true)
 			graphnode.connect("open_help", open_help)
 			graphnode.connect("open_help", open_help)
 			graphnode.connect("inlet_removed", Callable(self, "on_inlet_removed"))
 			graphnode.connect("inlet_removed", Callable(self, "on_inlet_removed"))
+			graphnode.node_moved.connect(_auto_link_nodes)
 			_register_inputs_in_node(graphnode) #link sliders for changes tracking
 			_register_inputs_in_node(graphnode) #link sliders for changes tracking
 			_register_node_movement() #link nodes for tracking position changes for changes tracking
 			_register_node_movement() #link nodes for tracking position changes for changes tracking
 			
 			
@@ -314,14 +328,23 @@ func _make_node(command: String, skip_undo_redo := false) -> GraphNode:
 	
 	
 
 
 func _on_connection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
 func _on_connection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
+	#check if this is trying to connect a node to itself and skip
+	if from_node == to_node:
+		return
+	
 	var to_graph_node = get_node(NodePath(to_node))
 	var to_graph_node = get_node(NodePath(to_node))
 	var from_graph_node = get_node(NodePath(from_node))
 	var from_graph_node = get_node(NodePath(from_node))
 
 
-	# Get the type of the input port using GraphNode's built-in method
-	var port_type = to_graph_node.get_input_port_type(to_port)
+	# Get the type of the ports
+	var to_port_type = to_graph_node.get_input_port_type(to_port)
+	var from_port_type = from_graph_node.get_output_port_type(from_port)
+	
+	#skip if this isnt a valid connection
+	if to_port_type != from_port_type:
+		return
 
 
 	# If port type is 1 and already has a connection, reject the request
 	# If port type is 1 and already has a connection, reject the request
-	if port_type == 1:
+	if to_port_type == 1:
 		var connections = get_connection_list()
 		var connections = get_connection_list()
 		var existing_connections = 0
 		var existing_connections = 0
 
 
@@ -359,43 +382,42 @@ func _unhandled_key_input(event: InputEvent) -> void:
 
 
 func _on_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> void:
 func _on_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> void:
 	control_script.undo_redo.create_action("Delete Nodes (Undo only)")
 	control_script.undo_redo.create_action("Delete Nodes (Undo only)")
-			
-	for node in selected_nodes.keys():
-		if is_instance_valid(node) and selected_nodes[node]:
-			if selected_nodes[node]:
-				#check if node is the output or the last input node and do nothing
-				if node.get_meta("command") == "outputfile":
-					pass
-				else:
-					# Store duplicate and state for undo
-					var node_data = node.duplicate()
-					var position = node.position_offset
-
-					# Store all connections for undo
-					var conns = []
-					for con in get_connection_list():
-						if con["to_node"] == node.name or con["from_node"] == node.name:
-							conns.append(con)
-
-					# Delete
-					remove_connections_to_node(node)
-					node.queue_free()
-					control_script.changesmade = true
-
-					# Register undo restore
-					control_script.undo_redo.add_undo_method(Callable(self, "add_child").bind(node_data, true))
-					control_script.undo_redo.add_undo_method(Callable(node_data, "set_position_offset").bind(position))
-					for con in conns:
-						control_script.undo_redo.add_undo_method(Callable(self, "connect_node").bind(
-							con["from_node"], con["from_port"],
-							con["to_node"], con["to_port"]
-						))
-					control_script.undo_redo.add_undo_method(Callable(self, "set_node_selected").bind(node_data, true))
-					control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
-					control_script.undo_redo.add_undo_method(Callable(self, "_register_inputs_in_node").bind(node_data)) #link sliders for changes tracking
-					control_script.undo_redo.add_undo_method(Callable(self, "_register_node_movement")) # link nodes for changes tracking
-
-	# Clear selection
+
+	for node_name in nodes:
+		var node: GraphNode = get_node_or_null(NodePath(node_name))
+		if node and is_instance_valid(node):
+			# Skip output nodes
+			if node.get_meta("command") == "outputfile":
+				continue
+
+			# Store duplicate and state for undo
+			var node_data = node.duplicate()
+			var position = node.position_offset
+
+			# Store all connections for undo
+			var conns := []
+			for con in get_connection_list():
+				if con["to_node"] == node.name or con["from_node"] == node.name:
+					conns.append(con)
+
+			# Delete
+			remove_connections_to_node(node)
+			node.queue_free()
+			control_script.changesmade = true
+
+			# Register undo restore
+			control_script.undo_redo.add_undo_method(Callable(self, "add_child").bind(node_data, true))
+			control_script.undo_redo.add_undo_method(Callable(node_data, "set_position_offset").bind(position))
+			for con in conns:
+				control_script.undo_redo.add_undo_method(Callable(self, "connect_node").bind(
+					con["from_node"], con["from_port"],
+					con["to_node"], con["to_port"]
+				))
+			control_script.undo_redo.add_undo_method(Callable(self, "set_node_selected").bind(node_data, true))
+			control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
+			control_script.undo_redo.add_undo_method(Callable(self, "_register_inputs_in_node").bind(node_data))
+			control_script.undo_redo.add_undo_method(Callable(self, "_register_node_movement"))
+
 	selected_nodes = {}
 	selected_nodes = {}
 
 
 	control_script.undo_redo.commit_action()
 	control_script.undo_redo.commit_action()
@@ -497,6 +519,7 @@ func paste_copied_nodes():
 
 
 		add_child(new_node, true)
 		add_child(new_node, true)
 		new_node.connect("open_help", open_help)
 		new_node.connect("open_help", open_help)
+		new_node.node_moved.connect(_auto_link_nodes)
 		_register_inputs_in_node(new_node) #link sliders for changes tracking
 		_register_inputs_in_node(new_node) #link sliders for changes tracking
 		_register_node_movement() # link nodes for changes tracking
 		_register_node_movement() # link nodes for changes tracking
 		name_map[node_data["name"]] = new_node.name
 		name_map[node_data["name"]] = new_node.name
@@ -591,3 +614,195 @@ func on_inlet_removed(node_name: StringName, port_index: int):
 	for conn in connections:
 	for conn in connections:
 		if conn.to_node == node_name and conn.to_port == port_index:
 		if conn.to_node == node_name and conn.to_port == port_index:
 			disconnect_node(conn.from_node, conn.from_port, conn.to_node, conn.to_port)
 			disconnect_node(conn.from_node, conn.from_port, conn.to_node, conn.to_port)
+			
+func _swap_node(old_node: GraphNode, command: String):
+	#store the position and name of the node to be replaced
+	var position = old_node.position_offset
+	var old_name = old_node.name
+	#gather all connections in the graph
+	var connections = get_connection_list()
+	var related_connections = []
+	
+	#filter the connections to get just those connected to the node to be replaced
+	for conn in connections:
+		if conn.from_node == old_name or conn.to_node == old_name:
+			related_connections.append(conn)
+			
+	#delete the old node
+	_on_graph_edit_delete_nodes_request([old_node.name])
+	
+	#make the new node and reposition it to the location of the old node
+	var new_node = _make_node(command)
+	new_node.position_offset = position
+	
+	#filter through all the connections to the old node
+	for conn in related_connections:
+		var from = conn.from_node
+		var from_port = conn.from_port
+		var to = conn.to_node
+		var to_port = conn.to_port
+		
+		#where the old node is referenced replace it with the name of the new node
+		if from == old_name:
+			from = new_node.name
+		if to == old_name:
+			to = new_node.name
+			
+		#check that the ports being connected to/from on the new node actually exist
+		if (from == new_node.name and new_node.is_slot_enabled_right(from_port)) or (to == new_node.name and new_node.is_slot_enabled_left(to_port)):
+			#check the two ports are the same type
+			if _same_port_type(from, from_port, to, to_port):
+				_on_connection_request(from, from_port, to, to_port)
+	
+func _connect_to_clicked_node(clicked_node: GraphNode, command: String):
+	var new_node_position = clicked_node.position_offset + Vector2(clicked_node.size.x + 50, 0)
+	#make the new node and reposition it to right of the node to connect to
+	var new_node = _make_node(command)
+	new_node.position_offset = new_node_position
+	
+	var clicked_node_has_outputs = clicked_node.get_output_port_count() > 0
+	var new_node_has_inputs = new_node.get_input_port_count() > 0
+	
+	if clicked_node_has_outputs and new_node_has_inputs:
+		if _same_port_type(clicked_node.name, 0, new_node.name, 0):
+			_on_connection_request(clicked_node.name, 0, new_node.name, 0)
+
+
+func _on_gui_input(event: InputEvent) -> void:
+	#check if this is an unhandled mouse click
+	if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
+		
+		#get dictionary of a cable if nearby
+		var closest_connection = get_closest_connection_at_point(get_local_mouse_position())
+		
+		#check if there is anything in that dictionary
+		if closest_connection.size() > 0:
+			#check if the background has changed colour for highlighted cable colour
+			var interface_settings = ConfigHandler.load_interface_settings()
+			if interface_settings.theme != theme_background or interface_settings.theme_custom_colour != theme_custom_background or interface_settings.high_contrast_selected_cables != high_contrast_cables:
+				#if bg has changed colour since last cable highlight reset to new bg and change cable colour
+				theme_background = interface_settings.theme
+				theme_custom_background = interface_settings.theme_custom_colour
+				high_contrast_cables = interface_settings.high_contrast_selected_cables
+				set_cable_colour(interface_settings.theme)
+				
+			#get details of nearby cable
+			var from_node = closest_connection.from_node
+			var from_port = closest_connection.from_port
+			var to_node = closest_connection.to_node
+			var to_port = closest_connection.to_port
+			
+			#check if user was holding shift and if so allow for multiple cables to be selected
+			if event.shift_pressed:
+				selected_cables.append(closest_connection)
+				set_connection_activity(from_node, from_port, to_node, to_port, 1)
+			
+			#if user double clicked unselect all cables and delete the nearest cable
+			elif event.double_click:
+					for conn in selected_cables:
+						set_connection_activity(conn.from_node, conn.from_port, conn.to_node, conn.to_port, 0)
+					_on_graph_edit_disconnection_request(from_node, from_port, to_node, to_port)
+					
+			#else just a single click, unselect any previously selected cables and select just the nearest
+			else:
+				for conn in selected_cables:
+					set_connection_activity(conn.from_node, conn.from_port, conn.to_node, conn.to_port, 0)
+				selected_cables = []
+				selected_cables.append(closest_connection)
+				set_connection_activity(from_node, from_port, to_node, to_port, 1)
+		
+		#user didnt click on a cable unselect all cables
+		else:
+			for conn in selected_cables:
+				set_connection_activity(conn.from_node, conn.from_port, conn.to_node, conn.to_port, 0)
+			selected_cables = []
+			
+	#if this is an unhandled delete check if there are any cables selected and deleted them
+	if event is InputEventKey and event.pressed:
+		if (event.keycode == KEY_BACKSPACE or event.keycode == KEY_DELETE) and selected_cables.size() > 0:
+			for conn in selected_cables:
+				_on_graph_edit_disconnection_request(conn.from_node, conn.from_port, conn.to_node, conn.to_port)
+				selected_cables = []
+	
+func set_cable_colour(theme_colour: int):
+	var background_colour
+	var cable_colour
+	var interface_settings = ConfigHandler.load_interface_settings()
+	match theme_colour:
+		0:
+			background_colour = Color("#2f4f4e")
+		1:
+			background_colour = Color("#000807")
+		2:
+			background_colour = Color("#98d4d2")
+		3:
+			background_colour = Color(interface_settings.theme_custom_colour)
+			
+	if interface_settings.high_contrast_selected_cables:
+		#180 colour shift from background and up sv
+		cable_colour = Color.from_hsv(fposmod(background_colour.h + 0.5, 1.0), clamp(background_colour.s + 0.2, 0, 1), clamp(background_colour.v + 0.2, 0, 1))
+		var luminance = 0.299 * background_colour.r + 0.587 * background_colour.g + 0.114 * background_colour.b
+		if luminance > 0.5 and cable_colour.get_luminance() > 0.5:
+			cable_colour = cable_colour.darkened(0.4)
+		elif luminance <= 0.5 and cable_colour.get_luminance() < 0.5:
+			#increase s and v again
+			cable_colour = Color.from_hsv(cable_colour.h, clamp(cable_colour.s + 0.2, 0, 0.8), clamp(cable_colour.v + 0.2, 0, 0.8))
+	else:
+		#keep hue but up saturation and variance
+		cable_colour = Color.from_hsv(background_colour.h, clamp(background_colour.s + 0.2, 0, 1), clamp(background_colour.v + 0.2, 0, 1))
+	#overide theme for cable highlight
+	add_theme_color_override("activity", cable_colour)
+
+func _auto_link_nodes(node: GraphNode, rect: Rect2):
+	#get all cables that overlap with the node being moved
+	var potential_connections = get_connections_intersecting_with_rect(rect)
+	
+	#if there are anyoverlapping and shift is being held down then
+	if potential_connections.size() > 0 and Input.is_action_pressed("auto_link_nodes"):
+		#sort through all the cables that overlap
+		for conn in potential_connections:
+			#get their info
+			var new_node_name = node.name
+			var new_node_has_inputs = node.get_input_port_count() > 0
+			var new_node_has_outputs = node.get_output_port_count() > 0
+			var from = conn.from_node
+			var from_port = conn.from_port
+			var to = conn.to_node
+			var to_port = conn.to_port
+			
+			if new_node_has_inputs and new_node_has_outputs:
+				#connect in the middle of the two nodes if they are the same port type
+				var from_matches = _same_port_type(from, from_port, new_node_name, 0)
+				var to_matches = _same_port_type(new_node_name, 0, to, to_port)
+				
+				if from_matches:
+					_on_connection_request(from, from_port, new_node_name, 0)
+				if to_matches:
+					_on_connection_request(new_node_name, 0, to, to_port)
+				#skip deleting cables if they are the same as the node being dragged or the ports don't match
+				if from_matches and to_matches and from != new_node_name and to != new_node_name:
+					_on_graph_edit_disconnection_request(from, from_port, to, to_port)
+
+			elif new_node_has_inputs:
+				#only has inputs check if the ports match and if they do connect but leave original connection in place
+				if _same_port_type(from, from_port, new_node_name, 0):
+					_on_connection_request(from, from_port, new_node_name, 0)
+					
+			elif new_node_has_outputs:
+				#only has outputs check if the ports match and if they do connect but leave original connection in place
+				if _same_port_type(new_node_name, 0, to, to_port):
+					_on_connection_request(new_node_name, 0, to, to_port)
+
+# function for checking if an inlet and an outlet are the same type
+func _same_port_type(from: String, from_port: int, to: String, to_port: int) -> bool:
+	var from_node = get_node_or_null(NodePath(from))
+	var to_node = get_node_or_null(NodePath(to))
+	#safety incase one somehow no longer exists
+	if from_node != null and to_node != null:
+		#check if the port types are the same e.g. both time or both pvoc
+		if from_node.get_output_port_type(from_port) == to_node.get_input_port_type(to_port):
+			return true
+		else:
+			return false
+	else:
+		return false

+ 2 - 0
scenes/main/scripts/run_thread.gd

@@ -34,6 +34,7 @@ func run_thread_with_branches():
 	process_successful = true
 	process_successful = true
 	progress_bar.value = 0
 	progress_bar.value = 0
 	progress_label.text = "Initialising Inputs"
 	progress_label.text = "Initialising Inputs"
+	console_window.find_child("KillProcess").disabled = false
 	# Detect platform: Determine if the OS is Windows
 	# Detect platform: Determine if the OS is Windows
 	var is_windows := OS.get_name() == "Windows"
 	var is_windows := OS.get_name() == "Windows"
 	
 	
@@ -633,6 +634,7 @@ func run_thread_with_branches():
 	progress_window.hide()
 	progress_window.hide()
 	progress_bar.value = 0
 	progress_bar.value = 0
 	progress_label.text = ""
 	progress_label.text = ""
+	console_window.find_child("KillProcess").disabled = true
 	if interface_settings.auto_close_console and process_successful == true:
 	if interface_settings.auto_close_console and process_successful == true:
 		console_window.hide()
 		console_window.hide()
 
 

+ 46 - 7
scenes/main/scripts/settings.gd

@@ -1,7 +1,15 @@
 extends Window
 extends Window
 signal open_cdp_location
 signal open_cdp_location
 signal console_on_top
 signal console_on_top
+signal invert_ui
+signal swap_zoom_and_move
+signal ui_scale_multiplier_changed
+
 var interface_settings
 var interface_settings
+var main_theme = preload("res://theme/main_theme.tres")
+var cdpprogs_location
+
+
 
 
 # Called when the node enters the scene tree for the first time.
 # Called when the node enters the scene tree for the first time.
 func _ready() -> void:
 func _ready() -> void:
@@ -19,12 +27,19 @@ func _on_close_requested() -> void:
 
 
 func _on_about_to_popup() -> void:
 func _on_about_to_popup() -> void:
 	interface_settings = ConfigHandler.load_interface_settings()
 	interface_settings = ConfigHandler.load_interface_settings()
-	$VBoxContainer/HBoxContainer5/ThemeList.select(interface_settings.theme, true)
-	$VBoxContainer/HBoxContainer/CustomColourPicker.color = Color(interface_settings.theme_custom_colour)
-	$VBoxContainer/HBoxContainer2/PvocWarning.button_pressed = interface_settings.disable_pvoc_warning
-	$VBoxContainer/HBoxContainer6/ProgressBar.button_pressed = interface_settings.disable_progress_bar
-	$VBoxContainer/HBoxContainer3/AutoCloseConsole.button_pressed = interface_settings.auto_close_console
-	$VBoxContainer/HBoxContainer4/ConsoleAlwaysOnTop.button_pressed = interface_settings.console_on_top
+	$MainContainer/HBoxContainer/VBoxContainer/HBoxContainer5/ThemeList.select(interface_settings.theme, true)
+	$MainContainer/HBoxContainer/VBoxContainer/HBoxContainer/CustomColourPicker.color = Color(interface_settings.theme_custom_colour)
+	$MainContainer/HBoxContainer/VBoxContainer/invert_ui_container/InvertUI.button_pressed = interface_settings.invert_theme
+	$MainContainer/HBoxContainer/VBoxContainer/high_contrast_cables_container/HighContrastCablesToggle.button_pressed = interface_settings.high_contrast_selected_cables
+	$MainContainer/HBoxContainer/VBoxContainer/ui_scale_container2/UIScaleOffsetSpinbox.value = interface_settings.ui_scale_multiplier
+	$MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer8/SwapZoomAndMoveToggle.button_pressed = interface_settings.swap_zoom_and_move
+	$MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer9/RightClickOpensExploreToggle.button_pressed = interface_settings.right_click_opens_explore
+	$MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer2/PvocWarning.button_pressed = interface_settings.disable_pvoc_warning
+	$MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer6/ProgressBar.button_pressed = interface_settings.disable_progress_bar
+	$MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer3/AutoCloseConsole.button_pressed = interface_settings.auto_close_console
+	$MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer4/ConsoleAlwaysOnTop.button_pressed = interface_settings.console_on_top
+	$MainContainer/HBoxContainer/VBoxContainer/HBoxContainer7/cdprogsLocationLabel.text = cdpprogs_location
+	$MainContainer/HBoxContainer/VBoxContainer/HBoxContainer7.tooltip_text = cdpprogs_location
 	
 	
 
 
 func _on_pvoc_warning_toggled(toggled_on: bool) -> void:
 func _on_pvoc_warning_toggled(toggled_on: bool) -> void:
@@ -45,6 +60,7 @@ func _on_console_always_on_top_toggled(toggled_on: bool) -> void:
 
 
 
 
 func _on_theme_list_item_selected(index: int) -> void:
 func _on_theme_list_item_selected(index: int) -> void:
+	interface_settings = ConfigHandler.load_interface_settings()
 	ConfigHandler.save_interface_settings("theme", index)
 	ConfigHandler.save_interface_settings("theme", index)
 	match index:
 	match index:
 		0:
 		0:
@@ -59,5 +75,28 @@ func _on_theme_list_item_selected(index: int) -> void:
 
 
 func _on_custom_colour_picker_color_changed(color: Color) -> void:
 func _on_custom_colour_picker_color_changed(color: Color) -> void:
 	ConfigHandler.save_interface_settings("theme_custom_colour", color.to_html(false))
 	ConfigHandler.save_interface_settings("theme_custom_colour", color.to_html(false))
-	if $VBoxContainer/HBoxContainer5/ThemeList.is_selected(3):
+	if $MainContainer/HBoxContainer/VBoxContainer/HBoxContainer5/ThemeList.is_selected(3):
 		RenderingServer.set_default_clear_color(color)
 		RenderingServer.set_default_clear_color(color)
+		
+
+func _on_invert_ui_toggled(toggled_on: bool) -> void:
+	ConfigHandler.save_interface_settings("invert_theme", toggled_on)
+	invert_ui.emit(toggled_on)
+
+
+func _on_swap_zoom_and_move_toggle_toggled(toggled_on: bool) -> void:
+	ConfigHandler.save_interface_settings("swap_zoom_and_move", toggled_on)
+	swap_zoom_and_move.emit(toggled_on)
+
+
+func _on_high_contrast_cables_toggle_toggled(toggled_on: bool) -> void:
+	ConfigHandler.save_interface_settings("high_contrast_selected_cables", toggled_on)
+
+
+func _on_ui_scale_offset_spinbox_value_changed(value: float) -> void:
+	ConfigHandler.save_interface_settings("ui_scale_multiplier", value)
+	ui_scale_multiplier_changed.emit(value)
+
+
+func _on_right_click_opens_explore_toggle_toggled(toggled_on: bool) -> void:
+	ConfigHandler.save_interface_settings("right_click_opens_explore", toggled_on)

+ 153 - 43
scenes/main/settings.tscn

@@ -1,64 +1,86 @@
-[gd_scene load_steps=2 format=3 uid="uid://c1a6elrpk4eks"]
+[gd_scene load_steps=3 format=3 uid="uid://c1a6elrpk4eks"]
 
 
 [ext_resource type="Script" uid="uid://co12pspac25gq" path="res://scenes/main/scripts/settings.gd" id="1_uey6c"]
 [ext_resource type="Script" uid="uid://co12pspac25gq" path="res://scenes/main/scripts/settings.gd" id="1_uey6c"]
+[ext_resource type="Script" uid="uid://c503vew41pw80" path="res://scenes/main/scripts/color_rect_theme_invert.gd" id="2_bym2s"]
 
 
 [node name="Settings" type="Window"]
 [node name="Settings" type="Window"]
 auto_translate_mode = 1
 auto_translate_mode = 1
 title = "SoundThread Settings"
 title = "SoundThread Settings"
 initial_position = 2
 initial_position = 2
-size = Vector2i(500, 410)
+size = Vector2i(950, 410)
 transient = true
 transient = true
 unresizable = true
 unresizable = true
 always_on_top = true
 always_on_top = true
 script = ExtResource("1_uey6c")
 script = ExtResource("1_uey6c")
 
 
-[node name="ColorRect" type="ColorRect" parent="."]
-offset_right = 506.0
-offset_bottom = 421.0
+[node name="ColorRect" type="ColorRect" parent="." groups=["invertable_background"]]
+offset_right = 955.0
+offset_bottom = 643.0
 color = Color(0.101961, 0.101961, 0.101961, 0.6)
 color = Color(0.101961, 0.101961, 0.101961, 0.6)
+script = ExtResource("2_bym2s")
 
 
-[node name="VBoxContainer" type="VBoxContainer" parent="."]
-offset_left = 12.0
-offset_top = 6.0
-offset_right = 490.0
-offset_bottom = 367.0
+[node name="MainContainer" type="VBoxContainer" parent="."]
+offset_left = 10.0
+offset_top = 10.0
+offset_right = 946.0
+offset_bottom = 385.0
 
 
-[node name="WindowTitle" type="Label" parent="VBoxContainer"]
+[node name="WindowTitle" type="Label" parent="MainContainer"]
 layout_mode = 2
 layout_mode = 2
 theme_override_font_sizes/font_size = 25
 theme_override_font_sizes/font_size = 25
 text = "SoundThread Settings"
 text = "SoundThread Settings"
 
 
-[node name="MarginContainer2" type="MarginContainer" parent="VBoxContainer"]
+[node name="HBoxContainer" type="HBoxContainer" parent="MainContainer"]
+layout_mode = 2
+theme_override_constants/separation = 36
+
+[node name="VBoxContainer" type="VBoxContainer" parent="MainContainer/HBoxContainer"]
+custom_minimum_size = Vector2(450, 0)
+layout_mode = 2
+
+[node name="MarginContainer2" type="MarginContainer" parent="MainContainer/HBoxContainer/VBoxContainer"]
 layout_mode = 2
 layout_mode = 2
 theme_override_constants/margin_bottom = 3
 theme_override_constants/margin_bottom = 3
 
 
-[node name="Label" type="Label" parent="VBoxContainer"]
+[node name="Label" type="Label" parent="MainContainer/HBoxContainer/VBoxContainer"]
 layout_mode = 2
 layout_mode = 2
 theme_override_font_sizes/font_size = 18
 theme_override_font_sizes/font_size = 18
-text = "Composers Desktop Project"
+text = "Composers Desktop Project Location"
+
+[node name="HBoxContainer7" type="HBoxContainer" parent="MainContainer/HBoxContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="Label3" type="Label" parent="MainContainer/HBoxContainer/VBoxContainer/HBoxContainer7"]
+layout_mode = 2
+text = "Current location: "
 
 
-[node name="ChangeCDP" type="Button" parent="VBoxContainer"]
+[node name="cdprogsLocationLabel" type="Label" parent="MainContainer/HBoxContainer/VBoxContainer/HBoxContainer7"]
+layout_mode = 2
+size_flags_horizontal = 3
+text_overrun_behavior = 3
+
+[node name="ChangeCDP" type="Button" parent="MainContainer/HBoxContainer/VBoxContainer"]
 layout_mode = 2
 layout_mode = 2
 text = "Change location for cdprogs folder"
 text = "Change location for cdprogs folder"
 
 
-[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer"]
+[node name="MarginContainer" type="MarginContainer" parent="MainContainer/HBoxContainer/VBoxContainer"]
 layout_mode = 2
 layout_mode = 2
 theme_override_constants/margin_bottom = 7
 theme_override_constants/margin_bottom = 7
 
 
-[node name="Label2" type="Label" parent="VBoxContainer"]
+[node name="Theme" type="Label" parent="MainContainer/HBoxContainer/VBoxContainer"]
 layout_mode = 2
 layout_mode = 2
 theme_override_font_sizes/font_size = 18
 theme_override_font_sizes/font_size = 18
-text = "User Interface"
+text = "Appearance"
 
 
-[node name="HBoxContainer5" type="HBoxContainer" parent="VBoxContainer"]
+[node name="HBoxContainer5" type="HBoxContainer" parent="MainContainer/HBoxContainer/VBoxContainer"]
 layout_mode = 2
 layout_mode = 2
 
 
-[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer5"]
+[node name="Label" type="Label" parent="MainContainer/HBoxContainer/VBoxContainer/HBoxContainer5"]
 custom_minimum_size = Vector2(183, 100)
 custom_minimum_size = Vector2(183, 100)
 layout_mode = 2
 layout_mode = 2
 text = "Theme: "
 text = "Theme: "
 
 
-[node name="ThemeList" type="ItemList" parent="VBoxContainer/HBoxContainer5"]
+[node name="ThemeList" type="ItemList" parent="MainContainer/HBoxContainer/VBoxContainer/HBoxContainer5"]
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 3
 size_flags_horizontal = 3
 auto_height = true
 auto_height = true
@@ -68,70 +90,158 @@ item_1/text = "Dark"
 item_2/text = "Light"
 item_2/text = "Light"
 item_3/text = "Custom"
 item_3/text = "Custom"
 
 
-[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
+[node name="HBoxContainer" type="HBoxContainer" parent="MainContainer/HBoxContainer/VBoxContainer"]
 layout_mode = 2
 layout_mode = 2
 
 
-[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer"]
+[node name="Label" type="Label" parent="MainContainer/HBoxContainer/VBoxContainer/HBoxContainer"]
 layout_mode = 2
 layout_mode = 2
 text = "Custom theme colour: "
 text = "Custom theme colour: "
 
 
-[node name="CustomColourPicker" type="ColorPickerButton" parent="VBoxContainer/HBoxContainer"]
+[node name="CustomColourPicker" type="ColorPickerButton" parent="MainContainer/HBoxContainer/VBoxContainer/HBoxContainer"]
 custom_minimum_size = Vector2(0, 25)
 custom_minimum_size = Vector2(0, 25)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 3
 size_flags_horizontal = 3
 color = Color(0.184314, 0.309804, 0.305882, 1)
 color = Color(0.184314, 0.309804, 0.305882, 1)
 edit_alpha = false
 edit_alpha = false
 
 
-[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer"]
+[node name="invert_ui_container" type="HBoxContainer" parent="MainContainer/HBoxContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="MainContainer/HBoxContainer/VBoxContainer/invert_ui_container"]
+layout_mode = 2
+text = "Invert UI colours:"
+
+[node name="InvertUI" type="CheckButton" parent="MainContainer/HBoxContainer/VBoxContainer/invert_ui_container"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="high_contrast_cables_container" type="HBoxContainer" parent="MainContainer/HBoxContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="MainContainer/HBoxContainer/VBoxContainer/high_contrast_cables_container"]
+layout_mode = 2
+text = "High Contrast Selected Cables:"
+
+[node name="HighContrastCablesToggle" type="CheckButton" parent="MainContainer/HBoxContainer/VBoxContainer/high_contrast_cables_container"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="ui_scale_container2" type="HBoxContainer" parent="MainContainer/HBoxContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="MainContainer/HBoxContainer/VBoxContainer/ui_scale_container2"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "UI Scale:"
+
+[node name="UIScaleOffsetSpinbox" type="SpinBox" parent="MainContainer/HBoxContainer/VBoxContainer/ui_scale_container2"]
+layout_mode = 2
+min_value = 0.1
+max_value = 5.0
+step = 0.1
+value = 1.0
+
+[node name="VBoxContainer2" type="VBoxContainer" parent="MainContainer/HBoxContainer"]
+custom_minimum_size = Vector2(450, 0)
+layout_mode = 2
+
+[node name="MarginContainer2" type="MarginContainer" parent="MainContainer/HBoxContainer/VBoxContainer2"]
+layout_mode = 2
+theme_override_constants/margin_bottom = 3
+
+[node name="Controls" type="Label" parent="MainContainer/HBoxContainer/VBoxContainer2"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 18
+text = "Controls"
+
+[node name="HBoxContainer8" type="HBoxContainer" parent="MainContainer/HBoxContainer/VBoxContainer2"]
+layout_mode = 2
+tooltip_text = "Default: Scroll Zooms, Ctrl/Cmd + Scroll Moves"
+
+[node name="Label" type="Label" parent="MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer8"]
+layout_mode = 2
+text = "Swap Zoom and Move:"
+
+[node name="SwapZoomAndMoveToggle" type="CheckButton" parent="MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer8"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="HBoxContainer9" type="HBoxContainer" parent="MainContainer/HBoxContainer/VBoxContainer2"]
+layout_mode = 2
+tooltip_text = "Default: right-click opens search menu. Note: Search menu still used for replace and connect"
+
+[node name="Label" type="Label" parent="MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer9"]
+layout_mode = 2
+text = "Right-click opens Explore Menu:"
+
+[node name="RightClickOpensExploreToggle" type="CheckButton" parent="MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer9"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="MarginContainer3" type="MarginContainer" parent="MainContainer/HBoxContainer/VBoxContainer2"]
+layout_mode = 2
+theme_override_constants/margin_bottom = 17
+
+[node name="Windows" type="Label" parent="MainContainer/HBoxContainer/VBoxContainer2"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 18
+text = "Popups and Dialogs"
+
+[node name="HBoxContainer2" type="HBoxContainer" parent="MainContainer/HBoxContainer/VBoxContainer2"]
 layout_mode = 2
 layout_mode = 2
 
 
-[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer2"]
+[node name="Label" type="Label" parent="MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer2"]
 layout_mode = 2
 layout_mode = 2
 text = "Disable frequency domain multiple input warning:"
 text = "Disable frequency domain multiple input warning:"
 
 
-[node name="PvocWarning" type="CheckButton" parent="VBoxContainer/HBoxContainer2"]
+[node name="PvocWarning" type="CheckButton" parent="MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer2"]
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 3
 size_flags_horizontal = 3
 
 
-[node name="HBoxContainer6" type="HBoxContainer" parent="VBoxContainer"]
+[node name="HBoxContainer6" type="HBoxContainer" parent="MainContainer/HBoxContainer/VBoxContainer2"]
 layout_mode = 2
 layout_mode = 2
 
 
-[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer6"]
+[node name="Label" type="Label" parent="MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer6"]
 layout_mode = 2
 layout_mode = 2
 text = "Show console instead of progress bar:"
 text = "Show console instead of progress bar:"
 
 
-[node name="ProgressBar" type="CheckButton" parent="VBoxContainer/HBoxContainer6"]
+[node name="ProgressBar" type="CheckButton" parent="MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer6"]
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 3
 size_flags_horizontal = 3
 
 
-[node name="HBoxContainer3" type="HBoxContainer" parent="VBoxContainer"]
+[node name="HBoxContainer3" type="HBoxContainer" parent="MainContainer/HBoxContainer/VBoxContainer2"]
 layout_mode = 2
 layout_mode = 2
 
 
-[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer3"]
+[node name="Label" type="Label" parent="MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer3"]
 layout_mode = 2
 layout_mode = 2
 text = "Auto close console when thread is complete: "
 text = "Auto close console when thread is complete: "
 
 
-[node name="AutoCloseConsole" type="CheckButton" parent="VBoxContainer/HBoxContainer3"]
+[node name="AutoCloseConsole" type="CheckButton" parent="MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer3"]
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 3
 size_flags_horizontal = 3
 
 
-[node name="HBoxContainer4" type="HBoxContainer" parent="VBoxContainer"]
+[node name="HBoxContainer4" type="HBoxContainer" parent="MainContainer/HBoxContainer/VBoxContainer2"]
 layout_mode = 2
 layout_mode = 2
 
 
-[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer4"]
+[node name="Label" type="Label" parent="MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer4"]
 layout_mode = 2
 layout_mode = 2
 text = "Console always on top: "
 text = "Console always on top: "
 
 
-[node name="ConsoleAlwaysOnTop" type="CheckButton" parent="VBoxContainer/HBoxContainer4"]
+[node name="ConsoleAlwaysOnTop" type="CheckButton" parent="MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer4"]
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 3
 size_flags_horizontal = 3
 
 
 [connection signal="about_to_popup" from="." to="." method="_on_about_to_popup"]
 [connection signal="about_to_popup" from="." to="." method="_on_about_to_popup"]
 [connection signal="close_requested" from="." to="." method="_on_close_requested"]
 [connection signal="close_requested" from="." to="." method="_on_close_requested"]
-[connection signal="button_down" from="VBoxContainer/ChangeCDP" to="." method="_on_change_cdp_button_down"]
-[connection signal="item_selected" from="VBoxContainer/HBoxContainer5/ThemeList" to="." method="_on_theme_list_item_selected"]
-[connection signal="color_changed" from="VBoxContainer/HBoxContainer/CustomColourPicker" to="." method="_on_custom_colour_picker_color_changed"]
-[connection signal="toggled" from="VBoxContainer/HBoxContainer2/PvocWarning" to="." method="_on_pvoc_warning_toggled"]
-[connection signal="toggled" from="VBoxContainer/HBoxContainer6/ProgressBar" to="." method="_on_progress_bar_toggled"]
-[connection signal="toggled" from="VBoxContainer/HBoxContainer3/AutoCloseConsole" to="." method="_on_auto_close_console_toggled"]
-[connection signal="toggled" from="VBoxContainer/HBoxContainer4/ConsoleAlwaysOnTop" to="." method="_on_console_always_on_top_toggled"]
+[connection signal="button_down" from="MainContainer/HBoxContainer/VBoxContainer/ChangeCDP" to="." method="_on_change_cdp_button_down"]
+[connection signal="item_selected" from="MainContainer/HBoxContainer/VBoxContainer/HBoxContainer5/ThemeList" to="." method="_on_theme_list_item_selected"]
+[connection signal="color_changed" from="MainContainer/HBoxContainer/VBoxContainer/HBoxContainer/CustomColourPicker" to="." method="_on_custom_colour_picker_color_changed"]
+[connection signal="toggled" from="MainContainer/HBoxContainer/VBoxContainer/invert_ui_container/InvertUI" to="." method="_on_invert_ui_toggled"]
+[connection signal="toggled" from="MainContainer/HBoxContainer/VBoxContainer/high_contrast_cables_container/HighContrastCablesToggle" to="." method="_on_high_contrast_cables_toggle_toggled"]
+[connection signal="value_changed" from="MainContainer/HBoxContainer/VBoxContainer/ui_scale_container2/UIScaleOffsetSpinbox" to="." method="_on_ui_scale_offset_spinbox_value_changed"]
+[connection signal="toggled" from="MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer8/SwapZoomAndMoveToggle" to="." method="_on_swap_zoom_and_move_toggle_toggled"]
+[connection signal="toggled" from="MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer9/RightClickOpensExploreToggle" to="." method="_on_right_click_opens_explore_toggle_toggled"]
+[connection signal="toggled" from="MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer2/PvocWarning" to="." method="_on_pvoc_warning_toggled"]
+[connection signal="toggled" from="MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer6/ProgressBar" to="." method="_on_progress_bar_toggled"]
+[connection signal="toggled" from="MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer3/AutoCloseConsole" to="." method="_on_auto_close_console_toggled"]
+[connection signal="toggled" from="MainContainer/HBoxContainer/VBoxContainer2/HBoxContainer4/ConsoleAlwaysOnTop" to="." method="_on_console_always_on_top_toggled"]

+ 208 - 7
scenes/menu/explore_menu.gd

@@ -4,6 +4,8 @@ var node_data = {} #stores node data for each node to display in help popup
 signal make_node(command)
 signal make_node(command)
 signal open_help(command)
 signal open_help(command)
 
 
+@onready var fav_button_logic = preload("res://scenes/menu/fav_button.gd")
+
 
 
 func _ready() -> void:
 func _ready() -> void:
 	
 	
@@ -20,8 +22,18 @@ func _ready() -> void:
 			push_error("Invalid JSON")
 			push_error("Invalid JSON")
 				
 				
 	fill_menu()
 	fill_menu()
+	load_search()
+	
+	#chec user prefs fr favourites and load them
+	var interface_settings = ConfigHandler.load_interface_settings()
+	var favourites = interface_settings.favourites
+	
+	_load_favourites(favourites)
 	
 	
 func fill_menu():
 func fill_menu():
+	var interface_settings = ConfigHandler.load_interface_settings()
+	var favourites = interface_settings.favourites
+	
 	for key in node_data.keys():
 	for key in node_data.keys():
 		var item = node_data[key]
 		var item = node_data[key]
 		var title = item.get("title", "")
 		var title = item.get("title", "")
@@ -75,6 +87,7 @@ func fill_menu():
 		
 		
 		var hbox = HBoxContainer.new()
 		var hbox = HBoxContainer.new()
 		var label = RichTextLabel.new()
 		var label = RichTextLabel.new()
+		var favbtn = Button.new()
 		var helpbtn = Button.new()
 		var helpbtn = Button.new()
 		var makebtn = Button.new()
 		var makebtn = Button.new()
 		var margin = MarginContainer.new()
 		var margin = MarginContainer.new()
@@ -86,6 +99,20 @@ func fill_menu():
 		label.set_v_size_flags(Control.SIZE_EXPAND_FILL)
 		label.set_v_size_flags(Control.SIZE_EXPAND_FILL)
 		label.fit_content = true
 		label.fit_content = true
 		
 		
+		favbtn.name = "fav_" + key
+		favbtn.add_theme_font_size_override("font_size", 20)
+		favbtn.tooltip_text = "Favourite " + title
+		favbtn.custom_minimum_size = Vector2(40, 40)
+		favbtn.set_h_size_flags(Control.SIZE_SHRINK_CENTER)
+		favbtn.toggle_mode = true
+		if favourites.has(key):
+			favbtn.text = "★"
+			favbtn.set_pressed_no_signal(true)
+		else:
+			favbtn.text = "☆"
+		favbtn.set_script(fav_button_logic)
+		favbtn.connect("toggled", Callable(self, "_favourite_process").bind(key, favourites)) #pass key (process name) when button is pressed
+		
 		helpbtn.text = "?"
 		helpbtn.text = "?"
 		helpbtn.tooltip_text = "Open help for " + title
 		helpbtn.tooltip_text = "Open help for " + title
 		helpbtn.custom_minimum_size = Vector2(40, 40)
 		helpbtn.custom_minimum_size = Vector2(40, 40)
@@ -102,6 +129,7 @@ func fill_menu():
 		
 		
 		container.add_child(hbox)
 		container.add_child(hbox)
 		hbox.add_child(label)
 		hbox.add_child(label)
+		hbox.add_child(favbtn)
 		hbox.add_child(helpbtn)
 		hbox.add_child(helpbtn)
 		hbox.add_child(makebtn)
 		hbox.add_child(makebtn)
 		container.add_child(margin)
 		container.add_child(margin)
@@ -109,7 +137,7 @@ func fill_menu():
 
 
 
 
 func _on_about_to_popup() -> void:
 func _on_about_to_popup() -> void:
-	fill_search("")
+	#fill_search("")
 	$"Control/select_effect/Search/Search for a process in SoundThread/MarginContainer/VBoxContainer/SearchBar".clear()
 	$"Control/select_effect/Search/Search for a process in SoundThread/MarginContainer/VBoxContainer/SearchBar".clear()
 	if $Control/select_effect.current_tab == 3:
 	if $Control/select_effect.current_tab == 3:
 		$"Control/select_effect/Search/Search for a process in SoundThread/MarginContainer/VBoxContainer/SearchBar".grab_focus()
 		$"Control/select_effect/Search/Search for a process in SoundThread/MarginContainer/VBoxContainer/SearchBar".grab_focus()
@@ -118,21 +146,32 @@ func _on_about_to_popup() -> void:
 
 
 
 
 func fill_search(filter: String):
 func fill_search(filter: String):
+	var interface_settings = ConfigHandler.load_interface_settings()
+	var favourites = interface_settings.favourites
 	# Remove all existing items from the VBoxContainer
 	# Remove all existing items from the VBoxContainer
 	var container = $"Control/select_effect/Search/Search for a process in SoundThread/MarginContainer/VBoxContainer/ScrollContainer/ItemContainer"
 	var container = $"Control/select_effect/Search/Search for a process in SoundThread/MarginContainer/VBoxContainer/ScrollContainer/ItemContainer"
-	for child in container.get_children():
-		child.queue_free()
+	#for child in container.get_children():
+		#child.queue_free()
 		
 		
 	var filters = filter.to_lower().split(" ", false)
 	var filters = filter.to_lower().split(" ", false)
 	
 	
+	
 	for key in node_data.keys():
 	for key in node_data.keys():
 		var item = node_data[key]
 		var item = node_data[key]
+		var search_element = container.find_child("search_" + key, true, false)
+		var search_margin = container.find_child("search_margin_" + key, true, false)
 		var title = item.get("title", "")
 		var title = item.get("title", "")
 		
 		
-		#filter out output node
-		if title == "Output File":
+		if search_element == null:
 			continue
 			continue
 		
 		
+		if filters.has("*"):
+			if favourites.has(key) == false:
+				search_element.hide()
+				search_margin.hide()
+				continue
+		
+		
 		var category = item.get("category", "")
 		var category = item.get("category", "")
 		var subcategory = item.get("subcategory", "")
 		var subcategory = item.get("subcategory", "")
 		var short_desc = item.get("short_description", "")
 		var short_desc = item.get("short_description", "")
@@ -146,26 +185,85 @@ func fill_search(filter: String):
 		if filter != "":
 		if filter != "":
 			var match_all_words = true
 			var match_all_words = true
 			for word in filters:
 			for word in filters:
+				if word == "*":
+					continue
+				
 				if word != "" and not searchable_text.findn(word) != -1:
 				if word != "" and not searchable_text.findn(word) != -1:
 					match_all_words = false
 					match_all_words = false
+					search_element.hide()
+					search_margin.hide()
 					break
 					break
 			if not match_all_words:
 			if not match_all_words:
 				continue
 				continue
+				
+		search_element.show()
+		search_margin.show()
+		
+		
+
+	
+
+func load_search():
+	var interface_settings = ConfigHandler.load_interface_settings()
+	var favourites = interface_settings.favourites
+	# Remove all existing items from the VBoxContainer
+	var container = $"Control/select_effect/Search/Search for a process in SoundThread/MarginContainer/VBoxContainer/ScrollContainer/ItemContainer"
+	for child in container.get_children():
+		child.queue_free()
+		
+	
+	
+	for key in node_data.keys():
+		var item = node_data[key]
+		var title = item.get("title", "")
+		
+		
+		#filter out output node
+		if title == "Output File":
+			continue
+		
+		var category = item.get("category", "")
+		var subcategory = item.get("subcategory", "")
+		var short_desc = item.get("short_description", "")
+		var command = key.replace("_", " ")
+		
+		# Combine all searchable text into one lowercase string
+		var searchable_text = "%s %s %s %s %s" % [title, short_desc, category, subcategory, key]
+		searchable_text = searchable_text.to_lower()
 		
 		
 		
 		
 		var hbox = HBoxContainer.new()
 		var hbox = HBoxContainer.new()
 		var label = RichTextLabel.new()
 		var label = RichTextLabel.new()
+		var favbtn = Button.new()
 		var helpbtn = Button.new()
 		var helpbtn = Button.new()
 		var makebtn = Button.new()
 		var makebtn = Button.new()
 		var margin = MarginContainer.new()
 		var margin = MarginContainer.new()
 		
 		
 		hbox.size.x = container.size.x
 		hbox.size.x = container.size.x
+		hbox.name = "search_" + key
+		margin.name = "search_margin_" + key
 		label.bbcode_enabled = true
 		label.bbcode_enabled = true
 		label.text = "[b]%s[/b]\n%s" % [title, short_desc]
 		label.text = "[b]%s[/b]\n%s" % [title, short_desc]
 		label.set_h_size_flags(Control.SIZE_EXPAND_FILL)
 		label.set_h_size_flags(Control.SIZE_EXPAND_FILL)
 		label.set_v_size_flags(Control.SIZE_EXPAND_FILL)
 		label.set_v_size_flags(Control.SIZE_EXPAND_FILL)
 		label.fit_content = true
 		label.fit_content = true
 		
 		
+		favbtn.name = "search_fav_" + key
+		favbtn.add_theme_font_size_override("font_size", 20)
+		favbtn.tooltip_text = "Favourite " + title
+		favbtn.custom_minimum_size = Vector2(40, 40)
+		favbtn.set_h_size_flags(Control.SIZE_SHRINK_CENTER)
+		favbtn.toggle_mode = true
+		if favourites.has(key):
+			favbtn.text = "★"
+			favbtn.set_pressed_no_signal(true)
+		else:
+			favbtn.text = "☆"
+		favbtn.set_script(fav_button_logic)
+		favbtn.connect("toggled", Callable(self, "_favourite_process").bind(key, favourites)) #pass key (process name) when button is pressed
+		
+		
+		
 		helpbtn.text = "?"
 		helpbtn.text = "?"
 		helpbtn.tooltip_text = "Open help for " + title
 		helpbtn.tooltip_text = "Open help for " + title
 		helpbtn.custom_minimum_size = Vector2(40, 40)
 		helpbtn.custom_minimum_size = Vector2(40, 40)
@@ -182,11 +280,11 @@ func fill_search(filter: String):
 		
 		
 		container.add_child(hbox)
 		container.add_child(hbox)
 		hbox.add_child(label)
 		hbox.add_child(label)
+		hbox.add_child(favbtn)
 		hbox.add_child(helpbtn)
 		hbox.add_child(helpbtn)
 		hbox.add_child(makebtn)
 		hbox.add_child(makebtn)
 		container.add_child(margin)
 		container.add_child(margin)
 
 
-	
 
 
 	
 	
 func _on_search_bar_text_changed(new_text: String) -> void:
 func _on_search_bar_text_changed(new_text: String) -> void:
@@ -203,5 +301,108 @@ func _open_help(key: String, title: String):
 
 
 
 
 func _on_select_effect_tab_changed(tab: int) -> void:
 func _on_select_effect_tab_changed(tab: int) -> void:
-	if tab == 3:
+	if tab == 4:
 		$"Control/select_effect/Search/Search for a process in SoundThread/MarginContainer/VBoxContainer/SearchBar".grab_focus()
 		$"Control/select_effect/Search/Search for a process in SoundThread/MarginContainer/VBoxContainer/SearchBar".grab_focus()
+
+func _favourite_process(toggled_on: bool, key: String, favourites: Array):
+	if toggled_on:
+		favourites.append(key)
+	else:
+		favourites.erase(key)
+	
+	ConfigHandler.save_interface_settings("favourites", favourites)
+	
+	#find all favourite buttons for this effect and set to the correct state if in serach of favourites window
+	if $Control/select_effect.current_tab == 3 or $Control/select_effect.current_tab == 4:
+		var button = $Control/select_effect.find_child("fav_" + key, true, false)
+		if button != null:
+			button.set_pressed_no_signal(toggled_on)
+			if toggled_on:
+				button.text = "★"
+			else:
+				button.text = "☆"
+	
+	if $Control/select_effect.current_tab != 4:
+		var button = $Control/select_effect.find_child("search_fav_" + key, true, false)
+		if button != null:
+			button.set_pressed_no_signal(toggled_on)
+			if toggled_on:
+				button.text = "★"
+			else:
+				button.text = "☆"
+	_load_favourites(favourites)
+		
+func refresh_menu():
+	pass
+
+
+func _load_favourites(favourites: Array):
+	var container = $"Control/select_effect/Favourites/Browse Favourites/MarginContainer/VBoxContainer/ScrollContainer/ItemContainer"
+	for child in container.get_children():
+		child.queue_free()
+		
+	if favourites.size() > 0:
+		for key in node_data.keys():
+			var item = node_data[key]
+			var title = item.get("title", "")
+			
+			if favourites.has(key) == false:
+				continue
+				
+			var category = item.get("category", "")
+			var subcategory = item.get("subcategory", "")
+			var short_desc = item.get("short_description", "")
+			var command = key.replace("_", " ")
+			
+			var hbox = HBoxContainer.new()
+			var label = RichTextLabel.new()
+			var favbtn = Button.new()
+			var helpbtn = Button.new()
+			var makebtn = Button.new()
+			var margin = MarginContainer.new()
+			
+			hbox.size.x = container.size.x
+			label.bbcode_enabled = true
+			label.text = "[b]%s[/b]\n%s" % [title, short_desc]
+			label.set_h_size_flags(Control.SIZE_EXPAND_FILL)
+			label.set_v_size_flags(Control.SIZE_EXPAND_FILL)
+			label.fit_content = true
+			
+			favbtn.add_theme_font_size_override("font_size", 20)
+			favbtn.tooltip_text = "Favourite " + title
+			favbtn.custom_minimum_size = Vector2(40, 40)
+			favbtn.set_h_size_flags(Control.SIZE_SHRINK_CENTER)
+			favbtn.toggle_mode = true
+			if favourites.has(key):
+				favbtn.text = "★"
+				favbtn.set_pressed_no_signal(true)
+			else:
+				favbtn.text = "☆"
+			favbtn.set_script(fav_button_logic)
+			favbtn.connect("toggled", Callable(self, "_favourite_process").bind(key, favourites)) #pass key (process name) when button is pressed
+			
+			helpbtn.text = "?"
+			helpbtn.tooltip_text = "Open help for " + title
+			helpbtn.custom_minimum_size = Vector2(40, 40)
+			helpbtn.set_h_size_flags(Control.SIZE_SHRINK_CENTER)
+			helpbtn.connect("pressed", Callable(self, "_open_help").bind(key, title)) #pass key (process name) when button is pressed
+			
+			makebtn.text = "+"
+			makebtn.tooltip_text = "Add " + title + " to thread"
+			makebtn.custom_minimum_size = Vector2(40, 40)
+			makebtn.set_h_size_flags(Control.SIZE_SHRINK_CENTER)
+			makebtn.connect("pressed", Callable(self, "_make_node").bind(key)) #pass key (process name) when button is pressed
+			
+			margin.add_theme_constant_override("margin_bottom", 3)
+			
+			container.add_child(hbox)
+			hbox.add_child(label)
+			hbox.add_child(favbtn)
+			hbox.add_child(helpbtn)
+			hbox.add_child(makebtn)
+			container.add_child(margin)
+	else:
+		var label = RichTextLabel.new()
+		label.text = "Press the star next to a process in the explore menu to add a favourite."
+		label.fit_content = true
+		container.add_child(label)

+ 14 - 0
scenes/menu/fav_button.gd

@@ -0,0 +1,14 @@
+extends Button
+
+
+# Called when the node enters the scene tree for the first time.
+func _ready() -> void:
+	connect("toggled", Callable(self, "_on_toggle"))
+
+
+# Called every frame. 'delta' is the elapsed time since the previous frame.
+func _on_toggle(toggled_on: bool):
+	if toggled_on:
+		text = "★"
+	else:
+		text = "☆"

+ 1 - 0
scenes/menu/fav_button.gd.uid

@@ -0,0 +1 @@
+uid://7vfoh5kr4ftq

+ 93 - 53
scenes/menu/menu.tscn

@@ -4,7 +4,7 @@
 
 
 [node name="Window" type="Window"]
 [node name="Window" type="Window"]
 transparent_bg = true
 transparent_bg = true
-size = Vector2i(650, 500)
+size = Vector2i(690, 500)
 borderless = true
 borderless = true
 transparent = true
 transparent = true
 popup_window = true
 popup_window = true
@@ -25,22 +25,20 @@ anchor_left = 0.5
 anchor_top = 0.5
 anchor_top = 0.5
 anchor_right = 0.5
 anchor_right = 0.5
 anchor_bottom = 0.5
 anchor_bottom = 0.5
-offset_left = -325.0
+offset_left = -345.0
 offset_top = -250.0
 offset_top = -250.0
-offset_right = 325.0
+offset_right = 345.0
 offset_bottom = 250.0
 offset_bottom = 250.0
 grow_horizontal = 2
 grow_horizontal = 2
 grow_vertical = 2
 grow_vertical = 2
-current_tab = 1
+current_tab = 0
 
 
 [node name="Time Domain" type="TabContainer" parent="Control/select_effect"]
 [node name="Time Domain" type="TabContainer" parent="Control/select_effect"]
-visible = false
 layout_mode = 2
 layout_mode = 2
-current_tab = 5
+current_tab = 0
 metadata/_tab_index = 0
 metadata/_tab_index = 0
 
 
 [node name="Distort" type="VBoxContainer" parent="Control/select_effect/Time Domain"]
 [node name="Distort" type="VBoxContainer" parent="Control/select_effect/Time Domain"]
-visible = false
 layout_mode = 2
 layout_mode = 2
 metadata/_tab_index = 0
 metadata/_tab_index = 0
 
 
@@ -52,17 +50,17 @@ theme_override_constants/margin_right = 5
 theme_override_constants/margin_bottom = 10
 theme_override_constants/margin_bottom = 10
 
 
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Time Domain/Distort/MarginContainer"]
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Time Domain/Distort/MarginContainer"]
-custom_minimum_size = Vector2(620, 425)
+custom_minimum_size = Vector2(660, 425)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 0
 size_flags_horizontal = 0
 horizontal_scroll_mode = 0
 horizontal_scroll_mode = 0
 
 
 [node name="DistortContainer" type="VBoxContainer" parent="Control/select_effect/Time Domain/Distort/MarginContainer/ScrollContainer"]
 [node name="DistortContainer" type="VBoxContainer" parent="Control/select_effect/Time Domain/Distort/MarginContainer/ScrollContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 
 
 [node name="Label" type="Label" parent="Control/select_effect/Time Domain/Distort/MarginContainer/ScrollContainer/DistortContainer"]
 [node name="Label" type="Label" parent="Control/select_effect/Time Domain/Distort/MarginContainer/ScrollContainer/DistortContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 4
 size_flags_horizontal = 4
 text = "Processes that modify and distort the waveshape of a sound. Many of these distortions find points where the sound crosses zero twice (pseudo-wavecycles) and applies modifications to each wavecycle individually."
 text = "Processes that modify and distort the waveshape of a sound. Many of these distortions find points where the sound crosses zero twice (pseudo-wavecycles) and applies modifications to each wavecycle individually."
@@ -85,17 +83,17 @@ theme_override_constants/margin_right = 5
 theme_override_constants/margin_bottom = 10
 theme_override_constants/margin_bottom = 10
 
 
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Time Domain/Extend/MarginContainer"]
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Time Domain/Extend/MarginContainer"]
-custom_minimum_size = Vector2(620, 425)
+custom_minimum_size = Vector2(660, 425)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 0
 size_flags_horizontal = 0
 horizontal_scroll_mode = 0
 horizontal_scroll_mode = 0
 
 
 [node name="ExtendContainer" type="VBoxContainer" parent="Control/select_effect/Time Domain/Extend/MarginContainer/ScrollContainer"]
 [node name="ExtendContainer" type="VBoxContainer" parent="Control/select_effect/Time Domain/Extend/MarginContainer/ScrollContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 
 
 [node name="Label" type="Label" parent="Control/select_effect/Time Domain/Extend/MarginContainer/ScrollContainer/ExtendContainer"]
 [node name="Label" type="Label" parent="Control/select_effect/Time Domain/Extend/MarginContainer/ScrollContainer/ExtendContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 4
 size_flags_horizontal = 4
 text = "Processes adjust the length of a sound through some form of repetition. Many of these processes invovle some aspect of randomness and so running them multiple times will yeild different results."
 text = "Processes adjust the length of a sound through some form of repetition. Many of these processes invovle some aspect of randomness and so running them multiple times will yeild different results."
@@ -118,17 +116,17 @@ theme_override_constants/margin_right = 5
 theme_override_constants/margin_bottom = 10
 theme_override_constants/margin_bottom = 10
 
 
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Time Domain/Filter/MarginContainer"]
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Time Domain/Filter/MarginContainer"]
-custom_minimum_size = Vector2(620, 425)
+custom_minimum_size = Vector2(660, 425)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 0
 size_flags_horizontal = 0
 horizontal_scroll_mode = 0
 horizontal_scroll_mode = 0
 
 
 [node name="FilterContainer" type="VBoxContainer" parent="Control/select_effect/Time Domain/Filter/MarginContainer/ScrollContainer"]
 [node name="FilterContainer" type="VBoxContainer" parent="Control/select_effect/Time Domain/Filter/MarginContainer/ScrollContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 
 
 [node name="Label" type="Label" parent="Control/select_effect/Time Domain/Filter/MarginContainer/ScrollContainer/FilterContainer"]
 [node name="Label" type="Label" parent="Control/select_effect/Time Domain/Filter/MarginContainer/ScrollContainer/FilterContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 4
 size_flags_horizontal = 4
 text = "Filters remove some frequencies from a sound while emphasising others. Many of these processes are filter banks which contain multiple bandpass filters. These can 'tune' a sound by emphasising specified frequencies."
 text = "Filters remove some frequencies from a sound while emphasising others. Many of these processes are filter banks which contain multiple bandpass filters. These can 'tune' a sound by emphasising specified frequencies."
@@ -151,17 +149,17 @@ theme_override_constants/margin_right = 5
 theme_override_constants/margin_bottom = 10
 theme_override_constants/margin_bottom = 10
 
 
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Time Domain/Granulate/MarginContainer"]
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Time Domain/Granulate/MarginContainer"]
-custom_minimum_size = Vector2(620, 425)
+custom_minimum_size = Vector2(660, 425)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 0
 size_flags_horizontal = 0
 horizontal_scroll_mode = 0
 horizontal_scroll_mode = 0
 
 
 [node name="GranulateContainer" type="VBoxContainer" parent="Control/select_effect/Time Domain/Granulate/MarginContainer/ScrollContainer"]
 [node name="GranulateContainer" type="VBoxContainer" parent="Control/select_effect/Time Domain/Granulate/MarginContainer/ScrollContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 
 
 [node name="Label" type="Label" parent="Control/select_effect/Time Domain/Granulate/MarginContainer/ScrollContainer/GranulateContainer"]
 [node name="Label" type="Label" parent="Control/select_effect/Time Domain/Granulate/MarginContainer/ScrollContainer/GranulateContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 4
 size_flags_horizontal = 4
 text = "Processes that split sounds into tiny chunks (grains) and processes each grain individually, Depending on how grains are processed, ordered and layerd many different effects can be achieved. "
 text = "Processes that split sounds into tiny chunks (grains) and processes each grain individually, Depending on how grains are processed, ordered and layerd many different effects can be achieved. "
@@ -184,23 +182,28 @@ theme_override_constants/margin_right = 5
 theme_override_constants/margin_bottom = 10
 theme_override_constants/margin_bottom = 10
 
 
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Time Domain/Misc/MarginContainer"]
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Time Domain/Misc/MarginContainer"]
-custom_minimum_size = Vector2(620, 425)
+custom_minimum_size = Vector2(660, 425)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 0
 size_flags_horizontal = 0
 horizontal_scroll_mode = 0
 horizontal_scroll_mode = 0
 
 
 [node name="MiscContainer" type="VBoxContainer" parent="Control/select_effect/Time Domain/Misc/MarginContainer/ScrollContainer"]
 [node name="MiscContainer" type="VBoxContainer" parent="Control/select_effect/Time Domain/Misc/MarginContainer/ScrollContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 
 
 [node name="Label" type="Label" parent="Control/select_effect/Time Domain/Misc/MarginContainer/ScrollContainer/MiscContainer"]
 [node name="Label" type="Label" parent="Control/select_effect/Time Domain/Misc/MarginContainer/ScrollContainer/MiscContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 4
 size_flags_horizontal = 4
 text = "These time domain processes do not fit neatly into other categories. Many of these are utilities for editing a soundfile however, several are also transformation processes that manipulate the audio in unique ways."
 text = "These time domain processes do not fit neatly into other categories. Many of these are utilities for editing a soundfile however, several are also transformation processes that manipulate the audio in unique ways."
 autowrap_mode = 3
 autowrap_mode = 3
 
 
+[node name="MarginContainer4" type="MarginContainer" parent="Control/select_effect/Time Domain/Misc/MarginContainer/ScrollContainer/MiscContainer"]
+layout_mode = 2
+theme_override_constants/margin_bottom = 5
+
 [node name="Reverb and Delay" type="VBoxContainer" parent="Control/select_effect/Time Domain"]
 [node name="Reverb and Delay" type="VBoxContainer" parent="Control/select_effect/Time Domain"]
+visible = false
 layout_mode = 2
 layout_mode = 2
 metadata/_tab_index = 5
 metadata/_tab_index = 5
 
 
@@ -212,22 +215,26 @@ theme_override_constants/margin_right = 5
 theme_override_constants/margin_bottom = 10
 theme_override_constants/margin_bottom = 10
 
 
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Time Domain/Reverb and Delay/MarginContainer"]
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Time Domain/Reverb and Delay/MarginContainer"]
-custom_minimum_size = Vector2(620, 425)
+custom_minimum_size = Vector2(660, 425)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 0
 size_flags_horizontal = 0
 horizontal_scroll_mode = 0
 horizontal_scroll_mode = 0
 
 
 [node name="ReverbContainer" type="VBoxContainer" parent="Control/select_effect/Time Domain/Reverb and Delay/MarginContainer/ScrollContainer"]
 [node name="ReverbContainer" type="VBoxContainer" parent="Control/select_effect/Time Domain/Reverb and Delay/MarginContainer/ScrollContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 
 
 [node name="Label" type="Label" parent="Control/select_effect/Time Domain/Reverb and Delay/MarginContainer/ScrollContainer/ReverbContainer"]
 [node name="Label" type="Label" parent="Control/select_effect/Time Domain/Reverb and Delay/MarginContainer/ScrollContainer/ReverbContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 4
 size_flags_horizontal = 4
 text = "These processes apply simple time-based effects to the sound such as reverb and delay. These are commonly used to emulate or give the impression of an acoustic space on a sound."
 text = "These processes apply simple time-based effects to the sound such as reverb and delay. These are commonly used to emulate or give the impression of an acoustic space on a sound."
 autowrap_mode = 3
 autowrap_mode = 3
 
 
+[node name="MarginContainer4" type="MarginContainer" parent="Control/select_effect/Time Domain/Reverb and Delay/MarginContainer/ScrollContainer/ReverbContainer"]
+layout_mode = 2
+theme_override_constants/margin_bottom = 5
+
 [node name="Synthesis" type="VBoxContainer" parent="Control/select_effect/Time Domain"]
 [node name="Synthesis" type="VBoxContainer" parent="Control/select_effect/Time Domain"]
 visible = false
 visible = false
 layout_mode = 2
 layout_mode = 2
@@ -241,17 +248,17 @@ theme_override_constants/margin_right = 5
 theme_override_constants/margin_bottom = 10
 theme_override_constants/margin_bottom = 10
 
 
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Time Domain/Synthesis/MarginContainer"]
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Time Domain/Synthesis/MarginContainer"]
-custom_minimum_size = Vector2(620, 425)
+custom_minimum_size = Vector2(660, 425)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 0
 size_flags_horizontal = 0
 horizontal_scroll_mode = 0
 horizontal_scroll_mode = 0
 
 
 [node name="SynthesisContainer" type="VBoxContainer" parent="Control/select_effect/Time Domain/Synthesis/MarginContainer/ScrollContainer"]
 [node name="SynthesisContainer" type="VBoxContainer" parent="Control/select_effect/Time Domain/Synthesis/MarginContainer/ScrollContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 
 
 [node name="Label" type="Label" parent="Control/select_effect/Time Domain/Synthesis/MarginContainer/ScrollContainer/SynthesisContainer"]
 [node name="Label" type="Label" parent="Control/select_effect/Time Domain/Synthesis/MarginContainer/ScrollContainer/SynthesisContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 4
 size_flags_horizontal = 4
 text = "Unlike the other nodes in SoundThread that all process sound, these processes synthesis sounds. These can be used as alternative sound sources but are also very useful as sound sources for testing out various other processes to get a feel for what they do."
 text = "Unlike the other nodes in SoundThread that all process sound, these processes synthesis sounds. These can be used as alternative sound sources but are also very useful as sound sources for testing out various other processes to get a feel for what they do."
@@ -262,8 +269,9 @@ layout_mode = 2
 theme_override_constants/margin_bottom = 5
 theme_override_constants/margin_bottom = 5
 
 
 [node name="Frequency Domain" type="TabContainer" parent="Control/select_effect"]
 [node name="Frequency Domain" type="TabContainer" parent="Control/select_effect"]
+visible = false
 layout_mode = 2
 layout_mode = 2
-current_tab = 2
+current_tab = 5
 metadata/_tab_index = 1
 metadata/_tab_index = 1
 
 
 [node name="Convert" type="VBoxContainer" parent="Control/select_effect/Frequency Domain"]
 [node name="Convert" type="VBoxContainer" parent="Control/select_effect/Frequency Domain"]
@@ -279,17 +287,17 @@ theme_override_constants/margin_right = 5
 theme_override_constants/margin_bottom = 10
 theme_override_constants/margin_bottom = 10
 
 
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Frequency Domain/Convert/MarginContainer"]
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Frequency Domain/Convert/MarginContainer"]
-custom_minimum_size = Vector2(620, 425)
+custom_minimum_size = Vector2(660, 425)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 0
 size_flags_horizontal = 0
 horizontal_scroll_mode = 0
 horizontal_scroll_mode = 0
 
 
 [node name="PVOCConvertContainer" type="VBoxContainer" parent="Control/select_effect/Frequency Domain/Convert/MarginContainer/ScrollContainer"]
 [node name="PVOCConvertContainer" type="VBoxContainer" parent="Control/select_effect/Frequency Domain/Convert/MarginContainer/ScrollContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 
 
 [node name="Label" type="Label" parent="Control/select_effect/Frequency Domain/Convert/MarginContainer/ScrollContainer/PVOCConvertContainer"]
 [node name="Label" type="Label" parent="Control/select_effect/Frequency Domain/Convert/MarginContainer/ScrollContainer/PVOCConvertContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 4
 size_flags_horizontal = 4
 text = "To use the frequency domain processes you must first convert the sound using Analyse and then once you have processed the frequency domain data convert it back to audio again using Resynthesise. For more information on the frequency domain processes, see Help > Tutorials > Frequency Domain."
 text = "To use the frequency domain processes you must first convert the sound using Analyse and then once you have processed the frequency domain data convert it back to audio again using Resynthesise. For more information on the frequency domain processes, see Help > Tutorials > Frequency Domain."
@@ -312,17 +320,17 @@ theme_override_constants/margin_right = 5
 theme_override_constants/margin_bottom = 10
 theme_override_constants/margin_bottom = 10
 
 
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Frequency Domain/Amplitude and Pitch/MarginContainer"]
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Frequency Domain/Amplitude and Pitch/MarginContainer"]
-custom_minimum_size = Vector2(620, 425)
+custom_minimum_size = Vector2(660, 425)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 0
 size_flags_horizontal = 0
 horizontal_scroll_mode = 0
 horizontal_scroll_mode = 0
 
 
 [node name="PVOCAmplitudePitchContainer" type="VBoxContainer" parent="Control/select_effect/Frequency Domain/Amplitude and Pitch/MarginContainer/ScrollContainer"]
 [node name="PVOCAmplitudePitchContainer" type="VBoxContainer" parent="Control/select_effect/Frequency Domain/Amplitude and Pitch/MarginContainer/ScrollContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 
 
 [node name="Label" type="Label" parent="Control/select_effect/Frequency Domain/Amplitude and Pitch/MarginContainer/ScrollContainer/PVOCAmplitudePitchContainer"]
 [node name="Label" type="Label" parent="Control/select_effect/Frequency Domain/Amplitude and Pitch/MarginContainer/ScrollContainer/PVOCAmplitudePitchContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 4
 size_flags_horizontal = 4
 text = "These processes operate on frequency domain analysis data to alter the loudness and/or pitch of some or all of the sound."
 text = "These processes operate on frequency domain analysis data to alter the loudness and/or pitch of some or all of the sound."
@@ -333,6 +341,7 @@ layout_mode = 2
 theme_override_constants/margin_bottom = 5
 theme_override_constants/margin_bottom = 5
 
 
 [node name="Combine" type="VBoxContainer" parent="Control/select_effect/Frequency Domain"]
 [node name="Combine" type="VBoxContainer" parent="Control/select_effect/Frequency Domain"]
+visible = false
 layout_mode = 2
 layout_mode = 2
 metadata/_tab_index = 2
 metadata/_tab_index = 2
 
 
@@ -344,17 +353,17 @@ theme_override_constants/margin_right = 5
 theme_override_constants/margin_bottom = 10
 theme_override_constants/margin_bottom = 10
 
 
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Frequency Domain/Combine/MarginContainer"]
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Frequency Domain/Combine/MarginContainer"]
-custom_minimum_size = Vector2(620, 425)
+custom_minimum_size = Vector2(660, 425)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 0
 size_flags_horizontal = 0
 horizontal_scroll_mode = 0
 horizontal_scroll_mode = 0
 
 
 [node name="PVOCCombineContainer" type="VBoxContainer" parent="Control/select_effect/Frequency Domain/Combine/MarginContainer/ScrollContainer"]
 [node name="PVOCCombineContainer" type="VBoxContainer" parent="Control/select_effect/Frequency Domain/Combine/MarginContainer/ScrollContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 
 
 [node name="Label" type="Label" parent="Control/select_effect/Frequency Domain/Combine/MarginContainer/ScrollContainer/PVOCCombineContainer"]
 [node name="Label" type="Label" parent="Control/select_effect/Frequency Domain/Combine/MarginContainer/ScrollContainer/PVOCCombineContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 4
 size_flags_horizontal = 4
 text = "These processes take multiple inputs and combine them into one output. This is not mixing in the conventional sense, instead these processes can be used to morph between sounds or create hybrids of sounds."
 text = "These processes take multiple inputs and combine them into one output. This is not mixing in the conventional sense, instead these processes can be used to morph between sounds or create hybrids of sounds."
@@ -377,17 +386,17 @@ theme_override_constants/margin_right = 5
 theme_override_constants/margin_bottom = 10
 theme_override_constants/margin_bottom = 10
 
 
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Frequency Domain/Formants/MarginContainer"]
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Frequency Domain/Formants/MarginContainer"]
-custom_minimum_size = Vector2(620, 425)
+custom_minimum_size = Vector2(660, 425)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 0
 size_flags_horizontal = 0
 horizontal_scroll_mode = 0
 horizontal_scroll_mode = 0
 
 
 [node name="PVOCFormantsContainer" type="VBoxContainer" parent="Control/select_effect/Frequency Domain/Formants/MarginContainer/ScrollContainer"]
 [node name="PVOCFormantsContainer" type="VBoxContainer" parent="Control/select_effect/Frequency Domain/Formants/MarginContainer/ScrollContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 
 
 [node name="Label" type="Label" parent="Control/select_effect/Frequency Domain/Formants/MarginContainer/ScrollContainer/PVOCFormantsContainer"]
 [node name="Label" type="Label" parent="Control/select_effect/Frequency Domain/Formants/MarginContainer/ScrollContainer/PVOCFormantsContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 4
 size_flags_horizontal = 4
 text = "These processes operate on frequency domain analysis data to manipulate or add formants to a sound. Formants are prominent peaks in the frequency spectrum that give sounds part of their timbre. They are essential in speech for discerning different vowel sounds."
 text = "These processes operate on frequency domain analysis data to manipulate or add formants to a sound. Formants are prominent peaks in the frequency spectrum that give sounds part of their timbre. They are essential in speech for discerning different vowel sounds."
@@ -410,17 +419,17 @@ theme_override_constants/margin_right = 5
 theme_override_constants/margin_bottom = 10
 theme_override_constants/margin_bottom = 10
 
 
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Frequency Domain/Time/MarginContainer"]
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Frequency Domain/Time/MarginContainer"]
-custom_minimum_size = Vector2(620, 425)
+custom_minimum_size = Vector2(660, 425)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 0
 size_flags_horizontal = 0
 horizontal_scroll_mode = 0
 horizontal_scroll_mode = 0
 
 
 [node name="PVOCTimeContainer" type="VBoxContainer" parent="Control/select_effect/Frequency Domain/Time/MarginContainer/ScrollContainer"]
 [node name="PVOCTimeContainer" type="VBoxContainer" parent="Control/select_effect/Frequency Domain/Time/MarginContainer/ScrollContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 
 
 [node name="Label" type="Label" parent="Control/select_effect/Frequency Domain/Time/MarginContainer/ScrollContainer/PVOCTimeContainer"]
 [node name="Label" type="Label" parent="Control/select_effect/Frequency Domain/Time/MarginContainer/ScrollContainer/PVOCTimeContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 4
 size_flags_horizontal = 4
 text = "These processes operate on frequency domain analysis data to transform something about how a sound evolves over time."
 text = "These processes operate on frequency domain analysis data to transform something about how a sound evolves over time."
@@ -431,7 +440,6 @@ layout_mode = 2
 theme_override_constants/margin_bottom = 5
 theme_override_constants/margin_bottom = 5
 
 
 [node name="Spectrum" type="VBoxContainer" parent="Control/select_effect/Frequency Domain"]
 [node name="Spectrum" type="VBoxContainer" parent="Control/select_effect/Frequency Domain"]
-visible = false
 layout_mode = 2
 layout_mode = 2
 metadata/_tab_index = 5
 metadata/_tab_index = 5
 
 
@@ -443,17 +451,17 @@ theme_override_constants/margin_right = 5
 theme_override_constants/margin_bottom = 10
 theme_override_constants/margin_bottom = 10
 
 
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Frequency Domain/Spectrum/MarginContainer"]
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Frequency Domain/Spectrum/MarginContainer"]
-custom_minimum_size = Vector2(620, 425)
+custom_minimum_size = Vector2(660, 425)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 0
 size_flags_horizontal = 0
 horizontal_scroll_mode = 0
 horizontal_scroll_mode = 0
 
 
 [node name="PVOCSpectrumContainer" type="VBoxContainer" parent="Control/select_effect/Frequency Domain/Spectrum/MarginContainer/ScrollContainer"]
 [node name="PVOCSpectrumContainer" type="VBoxContainer" parent="Control/select_effect/Frequency Domain/Spectrum/MarginContainer/ScrollContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 
 
 [node name="Label" type="Label" parent="Control/select_effect/Frequency Domain/Spectrum/MarginContainer/ScrollContainer/PVOCSpectrumContainer"]
 [node name="Label" type="Label" parent="Control/select_effect/Frequency Domain/Spectrum/MarginContainer/ScrollContainer/PVOCSpectrumContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 4
 size_flags_horizontal = 4
 text = "These processes operate on frequency domain analysis data to alter the spectral content of a sound. This can include removing certain frequencies or adding additional frequencies into the sound."
 text = "These processes operate on frequency domain analysis data to alter the spectral content of a sound. This can include removing certain frequencies or adding additional frequencies into the sound."
@@ -481,17 +489,17 @@ theme_override_constants/margin_right = 5
 theme_override_constants/margin_bottom = 10
 theme_override_constants/margin_bottom = 10
 
 
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Utilities/SoundThread/MarginContainer"]
 [node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Utilities/SoundThread/MarginContainer"]
-custom_minimum_size = Vector2(620, 425)
+custom_minimum_size = Vector2(660, 425)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 0
 size_flags_horizontal = 0
 horizontal_scroll_mode = 0
 horizontal_scroll_mode = 0
 
 
 [node name="UtilityContainer" type="VBoxContainer" parent="Control/select_effect/Utilities/SoundThread/MarginContainer/ScrollContainer"]
 [node name="UtilityContainer" type="VBoxContainer" parent="Control/select_effect/Utilities/SoundThread/MarginContainer/ScrollContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 
 
 [node name="Label" type="Label" parent="Control/select_effect/Utilities/SoundThread/MarginContainer/ScrollContainer/UtilityContainer"]
 [node name="Label" type="Label" parent="Control/select_effect/Utilities/SoundThread/MarginContainer/ScrollContainer/UtilityContainer"]
-custom_minimum_size = Vector2(620, 0)
+custom_minimum_size = Vector2(660, 0)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 4
 size_flags_horizontal = 4
 text = "The nodes in this section are not part of the Composers Desktop Project. They are small utilities that have been added to SoundThread to help make using CDP slightly easier."
 text = "The nodes in this section are not part of the Composers Desktop Project. They are small utilities that have been added to SoundThread to help make using CDP slightly easier."
@@ -501,12 +509,44 @@ autowrap_mode = 3
 layout_mode = 2
 layout_mode = 2
 theme_override_constants/margin_bottom = 5
 theme_override_constants/margin_bottom = 5
 
 
-[node name="Search" type="TabContainer" parent="Control/select_effect"]
+[node name="Favourites" type="TabContainer" parent="Control/select_effect"]
 visible = false
 visible = false
 layout_mode = 2
 layout_mode = 2
 current_tab = 0
 current_tab = 0
 metadata/_tab_index = 3
 metadata/_tab_index = 3
 
 
+[node name="Browse Favourites" type="VBoxContainer" parent="Control/select_effect/Favourites"]
+layout_mode = 2
+metadata/_tab_index = 0
+
+[node name="MarginContainer" type="MarginContainer" parent="Control/select_effect/Favourites/Browse Favourites"]
+layout_mode = 2
+theme_override_constants/margin_left = 15
+theme_override_constants/margin_top = 10
+theme_override_constants/margin_right = 5
+theme_override_constants/margin_bottom = 10
+
+[node name="VBoxContainer" type="VBoxContainer" parent="Control/select_effect/Favourites/Browse Favourites/MarginContainer"]
+custom_minimum_size = Vector2(660, 420)
+layout_mode = 2
+size_flags_horizontal = 0
+
+[node name="ScrollContainer" type="ScrollContainer" parent="Control/select_effect/Favourites/Browse Favourites/MarginContainer/VBoxContainer"]
+custom_minimum_size = Vector2(0, 10)
+layout_mode = 2
+size_flags_vertical = 3
+horizontal_scroll_mode = 0
+
+[node name="ItemContainer" type="VBoxContainer" parent="Control/select_effect/Favourites/Browse Favourites/MarginContainer/VBoxContainer/ScrollContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Search" type="TabContainer" parent="Control/select_effect"]
+visible = false
+layout_mode = 2
+current_tab = 0
+metadata/_tab_index = 4
+
 [node name="Search for a process in SoundThread" type="VBoxContainer" parent="Control/select_effect/Search"]
 [node name="Search for a process in SoundThread" type="VBoxContainer" parent="Control/select_effect/Search"]
 layout_mode = 2
 layout_mode = 2
 metadata/_tab_index = 0
 metadata/_tab_index = 0
@@ -519,7 +559,7 @@ theme_override_constants/margin_right = 5
 theme_override_constants/margin_bottom = 10
 theme_override_constants/margin_bottom = 10
 
 
 [node name="VBoxContainer" type="VBoxContainer" parent="Control/select_effect/Search/Search for a process in SoundThread/MarginContainer"]
 [node name="VBoxContainer" type="VBoxContainer" parent="Control/select_effect/Search/Search for a process in SoundThread/MarginContainer"]
-custom_minimum_size = Vector2(620, 420)
+custom_minimum_size = Vector2(660, 420)
 layout_mode = 2
 layout_mode = 2
 size_flags_horizontal = 0
 size_flags_horizontal = 0
 
 

+ 36 - 8
scenes/menu/search_menu.gd

@@ -4,7 +4,15 @@ extends PopupPanel
 @onready var scroll_container: ScrollContainer = $VBoxContainer/ScrollContainer
 @onready var scroll_container: ScrollContainer = $VBoxContainer/ScrollContainer
 @onready var search_bar = $VBoxContainer/SearchBar
 @onready var search_bar = $VBoxContainer/SearchBar
 var node_data = {} #stores node data for each node to display in help popup
 var node_data = {} #stores node data for each node to display in help popup
+var replace_node = false
+var node_to_replace
+var connect_to_node = false
+var node_to_connect_to
+var uiscale
+var favourites
 signal make_node(command)
 signal make_node(command)
+signal swap_node(node_to_replace, command)
+signal connect_to_clicked_node(node_to_connect_to, command)
 
 
 
 
 func _ready() -> void:
 func _ready() -> void:
@@ -25,9 +33,12 @@ func _ready() -> void:
 
 
 
 
 func _on_about_to_popup() -> void:
 func _on_about_to_popup() -> void:
+	var interface_settings = ConfigHandler.load_interface_settings()
+	favourites = interface_settings.favourites
 	display_items("") #populate menu when needed
 	display_items("") #populate menu when needed
 	search_bar.clear()
 	search_bar.clear()
 	search_bar.grab_focus()
 	search_bar.grab_focus()
+	
 
 
 func display_items(filter: String):
 func display_items(filter: String):
 	# Remove all existing items from the VBoxContainer
 	# Remove all existing items from the VBoxContainer
@@ -40,6 +51,11 @@ func display_items(filter: String):
 		var item = node_data[key]
 		var item = node_data[key]
 		var title = item.get("title", "")
 		var title = item.get("title", "")
 		
 		
+		#check if searching for favourites
+		if filters.has("*"):
+			if favourites.has(key) == false:
+				continue
+		
 		#filter out output node
 		#filter out output node
 		if title == "Output File":
 		if title == "Output File":
 			continue
 			continue
@@ -58,6 +74,9 @@ func display_items(filter: String):
 		if filter != "":
 		if filter != "":
 			var match_all_words = true
 			var match_all_words = true
 			for word in filters:
 			for word in filters:
+				if word == "*":
+					continue
+					
 				if word != "" and not searchable_text.findn(word) != -1:
 				if word != "" and not searchable_text.findn(word) != -1:
 					match_all_words = false
 					match_all_words = false
 					break
 					break
@@ -69,12 +88,16 @@ func display_items(filter: String):
 		btn.alignment = 0 #left align text
 		btn.alignment = 0 #left align text
 		btn.clip_text = true #clip off labels that are too long
 		btn.clip_text = true #clip off labels that are too long
 		btn.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS #and replace with ...
 		btn.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS #and replace with ...
+		var button_text = ""
+		if favourites.has(key):
+			button_text += "★ "
 		if category.to_lower() == "pvoc": #format node names correctly, only show the category for PVOC
 		if category.to_lower() == "pvoc": #format node names correctly, only show the category for PVOC
-			btn.text = "%s %s: %s - %s" % [category.to_upper(), subcategory.to_pascal_case(), title, short_desc]
+			button_text += "%s %s: %s - %s" % [category.to_upper(), subcategory.to_pascal_case(), title, short_desc]
 		elif title.to_lower() == "input file":
 		elif title.to_lower() == "input file":
-			btn.text = "%s - %s" % [title, short_desc]
+			button_text += "%s - %s" % [title, short_desc]
 		else:
 		else:
-			btn.text = "%s: %s - %s" % [subcategory.to_pascal_case(), title, short_desc]
+			button_text += "%s: %s - %s" % [subcategory.to_pascal_case(), title, short_desc]
+		btn.text = button_text
 		btn.connect("pressed", Callable(self, "_on_item_selected").bind(key)) #pass key (process name) when button is pressed
 		btn.connect("pressed", Callable(self, "_on_item_selected").bind(key)) #pass key (process name) when button is pressed
 		
 		
 		#apply custom focus theme for keyboard naviagation
 		#apply custom focus theme for keyboard naviagation
@@ -88,10 +111,10 @@ func display_items(filter: String):
 	
 	
 	#resize menu within certain bounds #50
 	#resize menu within certain bounds #50
 	await get_tree().process_frame
 	await get_tree().process_frame
-	if DisplayServer.screen_get_dpi(0) >= 144:
-		self.size.y = min((item_container.size.y + search_bar.size.y + 12) * 2, 820) #i think this will scale for retina screens but might be wrong
-	else:
-		self.size.y = min(item_container.size.y + search_bar.size.y + 12, 410)
+	#if DisplayServer.screen_get_dpi(0) >= 144:
+		#self.size.y = min((item_container.size.y + search_bar.size.y + 12) * 2, 820) #i think this will scale for retina screens but might be wrong
+	#else:
+	self.size.y = min((item_container.size.y + search_bar.size.y + 12) * uiscale, 410 * uiscale)
 	
 	
 	#highlight first button
 	#highlight first button
 	_on_search_bar_editing_toggled(true)
 	_on_search_bar_editing_toggled(true)
@@ -101,7 +124,12 @@ func _on_search_bar_text_changed(new_text: String) -> void:
 	
 	
 func _on_item_selected(key: String):
 func _on_item_selected(key: String):
 	self.hide()
 	self.hide()
-	make_node.emit(key) # send out signal to main patch
+	if replace_node == true:
+		swap_node.emit(node_to_replace, key)
+	elif connect_to_node == true:
+		connect_to_clicked_node.emit(node_to_connect_to, key)
+	else:
+		make_node.emit(key) # send out signal to main patch
 
 
 func _on_search_bar_text_submitted(new_text: String) -> void:
 func _on_search_bar_text_submitted(new_text: String) -> void:
 	var button = item_container.get_child(0)
 	var button = item_container.get_child(0)

BIN=BIN
theme/images/open_explore.png


+ 34 - 0
theme/images/open_explore.png.import

@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://drn34trxhf80f"
+path="res://.godot/imported/open_explore.png-853121c6f0e8091b3020ae9f7af7fdd5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://theme/images/open_explore.png"
+dest_files=["res://.godot/imported/open_explore.png-853121c6f0e8091b3020ae9f7af7fdd5.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 5 - 2
theme/main_theme.tres


Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio