diff --git a/assets/font/SourceCodePro-VariableFont_wght.ttf b/assets/font/SourceCodePro-VariableFont_wght.ttf
deleted file mode 100644
index cf592055..00000000
Binary files a/assets/font/SourceCodePro-VariableFont_wght.ttf and /dev/null differ
diff --git a/assets/font/SourceCodePro-VariableFont_wght.ttf.import b/assets/font/SourceCodePro-VariableFont_wght.ttf.import
deleted file mode 100644
index ee568288..00000000
--- a/assets/font/SourceCodePro-VariableFont_wght.ttf.import
+++ /dev/null
@@ -1,36 +0,0 @@
-[remap]
-
-importer="font_data_dynamic"
-type="FontFile"
-uid="uid://bp0rn6rs2chy1"
-path="res://.godot/imported/SourceCodePro-VariableFont_wght.ttf-660a8b39808daa6fa4e7d4d38c4324e2.fontdata"
-
-[deps]
-
-source_file="res://assets/font/SourceCodePro-VariableFont_wght.ttf"
-dest_files=["res://.godot/imported/SourceCodePro-VariableFont_wght.ttf-660a8b39808daa6fa4e7d4d38c4324e2.fontdata"]
-
-[params]
-
-Rendering=null
-antialiasing=1
-generate_mipmaps=false
-disable_embedded_bitmaps=true
-multichannel_signed_distance_field=false
-msdf_pixel_range=8
-msdf_size=48
-allow_system_fallback=true
-force_autohinter=false
-modulate_color_glyphs=false
-hinting=1
-subpixel_positioning=4
-keep_rounding_remainders=true
-oversampling=0.0
-Fallbacks=null
-fallbacks=[]
-Compress=null
-compress=true
-preload=[]
-language_support={}
-script_support={}
-opentype_features={}
diff --git a/assets/icons/PlayAlt.svg b/assets/icons/PlayAlt.svg
index c6634138..3986dba9 100644
--- a/assets/icons/PlayAlt.svg
+++ b/assets/icons/PlayAlt.svg
@@ -7,7 +7,7 @@
version="1.1"
id="svg1"
sodipodi:docname="PlayAlt.svg"
- inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
+ inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
@@ -24,15 +24,15 @@
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="48.833333"
- inkscape:cx="12"
+ inkscape:cx="11.989761"
inkscape:cy="12"
inkscape:window-width="2560"
- inkscape:window-height="1372"
- inkscape:window-x="1920"
- inkscape:window-y="29"
+ inkscape:window-height="1360"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
diff --git a/assets/icons/PlayPause.svg b/assets/icons/PlayPause.svg
index c2e20c76..9a0695d6 100644
--- a/assets/icons/PlayPause.svg
+++ b/assets/icons/PlayPause.svg
@@ -1 +1,39 @@
-
\ No newline at end of file
+
+
diff --git a/assets/icons/RandomMode.svg b/assets/icons/RandomMode.svg
new file mode 100644
index 00000000..c7b5857c
--- /dev/null
+++ b/assets/icons/RandomMode.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/RandomMode.svg.import b/assets/icons/RandomMode.svg.import
new file mode 100644
index 00000000..63a786e4
--- /dev/null
+++ b/assets/icons/RandomMode.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cfdxexemm7766"
+path="res://.godot/imported/RandomMode.svg-0e6de5f36ee5ae23b97ec7abca32de16.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/icons/RandomMode.svg"
+dest_files=["res://.godot/imported/RandomMode.svg-0e6de5f36ee5ae23b97ec7abca32de16.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+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/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/assets/icons/Zone.svg b/assets/icons/Zone.svg
new file mode 100644
index 00000000..419aacc6
--- /dev/null
+++ b/assets/icons/Zone.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/Zone.svg.import b/assets/icons/Zone.svg.import
new file mode 100644
index 00000000..6f765465
--- /dev/null
+++ b/assets/icons/Zone.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bc0acgx0we044"
+path="res://.godot/imported/Zone.svg-4ee6a3e7c2a26955050d813c1a1ed98d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/icons/Zone.svg"
+dest_files=["res://.godot/imported/Zone.svg-4ee6a3e7c2a26955050d813c1a1ed98d.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+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/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/components/DataInputs/DataInputColor/DataInputColor.gd b/components/DataInputs/DataInputColor/DataInputColor.gd
new file mode 100644
index 00000000..39b71ebb
--- /dev/null
+++ b/components/DataInputs/DataInputColor/DataInputColor.gd
@@ -0,0 +1,44 @@
+ # Copyright (c) 2026 Liam Sherwin. All rights reserved.
+# This file is part of the Spectrum Lighting Controller, licensed under the GPL v3.0 or later.
+# See the LICENSE file for details.
+
+class_name DataInputColor extends DataInput
+## DataInput for Data.Type.COLOR
+
+
+## The LineEdit
+var _button: ColorPickerButton
+
+
+## Ready
+func _ready() -> void:
+ _data_type = Data.Type.COLOR
+ _button = $HBox/Button
+ _label = $HBox/Label
+ _outline = $HBox/Button/Outline
+ _focus_node = _button
+
+
+## Resets this DataInputString
+func _reset() -> void:
+ _button.set_pick_color(Color.WHITE)
+
+
+## Override this function to provide a SettingsModule to display
+func _settings_module_changed(p_module: SettingsModule) -> void:
+ _button.set_pick_color(p_module.get_getter().call())
+
+
+## Called when the editable state is changed
+func _set_editable(p_editable: bool) -> void:
+ _button.set_disabled(not p_editable)
+
+
+## Called when the button is pressed down
+func _on_button_button_down() -> void:
+ _update_outline_feedback(_module.get_setter().call())
+
+
+## Called when the color is changed on the color picker
+func _on_button_color_changed(p_color: Color) -> void:
+ _update_outline_feedback(_module.get_setter().call(p_color))
diff --git a/components/DataInputs/DataInputColor/DataInputColor.gd.uid b/components/DataInputs/DataInputColor/DataInputColor.gd.uid
new file mode 100644
index 00000000..1870cb11
--- /dev/null
+++ b/components/DataInputs/DataInputColor/DataInputColor.gd.uid
@@ -0,0 +1 @@
+uid://cgohncgxj4a8f
diff --git a/components/DataInputs/DataInputColor/DataInputColor.tscn b/components/DataInputs/DataInputColor/DataInputColor.tscn
new file mode 100644
index 00000000..4e5464b5
--- /dev/null
+++ b/components/DataInputs/DataInputColor/DataInputColor.tscn
@@ -0,0 +1,35 @@
+[gd_scene load_steps=3 format=3 uid="uid://rac2xe1lsdhe"]
+
+[ext_resource type="Script" uid="uid://cgohncgxj4a8f" path="res://components/DataInputs/DataInputColor/DataInputColor.gd" id="1_0hx11"]
+[ext_resource type="StyleBox" uid="uid://cftl4b5dnt57b" path="res://assets/styles/ResolveBoxBGLess.tres" id="2_v7583"]
+
+[node name="DataInputColor" type="PanelContainer"]
+custom_minimum_size = Vector2(0, 50)
+offset_right = 86.5625
+offset_bottom = 49.0
+script = ExtResource("1_0hx11")
+
+[node name="HBox" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="HBox"]
+visible = false
+layout_mode = 2
+
+[node name="Button" type="ColorPickerButton" parent="HBox"]
+custom_minimum_size = Vector2(150, 0)
+layout_mode = 2
+size_flags_horizontal = 10
+
+[node name="Outline" type="Panel" parent="HBox/Button"]
+modulate = Color(1, 1, 1, 0)
+layout_mode = 0
+offset_top = 721.0
+offset_right = 298.0
+offset_bottom = 721.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 1
+theme_override_styles/panel = ExtResource("2_v7583")
+
+[connection signal="color_changed" from="HBox/Button" to="." method="_on_button_color_changed"]
diff --git a/components/TO_BE_SORTED/PlaybackRow/PlaybackRow.gd b/components/TO_BE_SORTED/PlaybackRow/PlaybackRow.gd
index 51dd75fd..74b292a4 100644
--- a/components/TO_BE_SORTED/PlaybackRow/PlaybackRow.gd
+++ b/components/TO_BE_SORTED/PlaybackRow/PlaybackRow.gd
@@ -136,7 +136,7 @@ func load_auto_config(component: EngineComponent) -> void:
slider.deserialize(config)
- set_label(component.name)
+ set_label(component.name())
## Saves this PlayBack row into a dict
diff --git a/core/components/FixtureManifest.gd b/core/components/FixtureManifest.gd
index b475870e..45928d5f 100644
--- a/core/components/FixtureManifest.gd
+++ b/core/components/FixtureManifest.gd
@@ -10,6 +10,7 @@ class_name FixtureManifest extends EngineComponent
## Info: This FixtureManifest contains basic info for a given manifest file.
enum Type {Manifest, Info}
+
## Current type of this FixtureManifest
var type: Type = Type.Manifest
@@ -37,7 +38,7 @@ func _init(p_uuid: String = UUID_Util.v4(), p_name: String = _name) -> void:
super._init(p_uuid, p_name)
_set_self_class("FixtureManifest")
- _set_name("FixtureManifest")
+ set_name("FixtureManifest")
## Creates a new mode
@@ -65,13 +66,14 @@ func create_zone(p_mode: String, p_zone: String) -> bool:
## Creates a parameter in the given mode and zone
-func add_parameter(p_mode: String, p_zone: String, p_parameter: String, p_channels: Array[int], p_category: String = "") -> bool:
+func add_parameter(p_mode: String, p_zone: String, p_parameter: String, p_channels: Array[int], p_category: String = "", p_default_function: String = "") -> bool:
if not _modes.has(p_mode) or not _modes[p_mode].zones.has(p_zone):
return false
_modes[p_mode].zones[p_zone][p_parameter] = {
"attribute": p_parameter,
"offsets": p_channels.duplicate(),
+ "default_function": p_default_function,
"functions": {}
}
@@ -103,7 +105,7 @@ func remove_parameter(p_mode: String, p_zone: String, p_parameter: String) -> bo
## Adds a funtion to the given parameter
-func add_parameter_function(p_mode: String, p_zone: String, p_parameter: String, p_function: String, p_name: String, p_default: int, p_range: Array[int], p_can_fade: bool, p_vdim_effected: bool) -> bool:
+func add_parameter_function(p_mode: String, p_zone: String, p_parameter: String, p_function: String, p_name: String, p_default: int, p_range: Array[int], p_can_fade: bool, p_vdim_effected: bool, p_control_type: Fixture.ControlType) -> bool:
if not _modes.has(p_mode) or not _modes[p_mode].zones.has(p_zone) or not _modes[p_mode].zones[p_zone].has(p_parameter):
return false
@@ -113,6 +115,7 @@ func add_parameter_function(p_mode: String, p_zone: String, p_parameter: String,
"default": p_default,
"can_fade": p_can_fade,
"vdim_effected": p_vdim_effected,
+ "control_type": p_control_type,
"dmx_range": p_range.duplicate(),
"sets": []
}
@@ -168,9 +171,14 @@ func function_can_vdim(p_mode: String, p_zone: String, p_parameter: String, p_fu
return _modes.get(p_mode, {}).get("zones", {}).get(p_zone, {}).get(p_parameter, {}).get("functions", {}).get(p_function, {}).get("vdim_effected", false)
+## Checks if this FixtureManifest has a function that can vdim
+func function_control_type(p_mode: String, p_zone: String, p_parameter: String, p_function: String) -> Fixture.ControlType:
+ return _modes.get(p_mode, {}).get("zones", {}).get(p_zone, {}).get(p_parameter, {}).get("functions", {}).get(p_function, {}).get("control_type", Fixture.ControlType.VALUE)
+
+
## Returns the given mode
func get_mode(p_mode: String) -> Dictionary:
- return _modes.get(p_mode, {}).duplicate(true)
+ return _modes.get(p_mode, {})
## Returns all the modes in this manifest
@@ -245,7 +253,7 @@ func deserialize(p_serialized_data: Dictionary) -> void:
manufacturer = type_convert(p_serialized_data.get("manufacturer"), TYPE_STRING)
importer = type_convert(p_serialized_data.get("importer"), TYPE_STRING)
file_path = type_convert(p_serialized_data.get("file_path"), TYPE_STRING)
-
+
_modes = type_convert(p_serialized_data.get("modes"), TYPE_DICTIONARY)
_categorys = type_convert(p_serialized_data.get("categorys"), TYPE_DICTIONARY)
_force_defaults = Array(type_convert(p_serialized_data.get("force_defaults"), TYPE_ARRAY), TYPE_STRING, "", null)
diff --git a/core/components/Universe.gd b/core/components/Universe.gd
index 32fcbae9..2b072bc7 100644
--- a/core/components/Universe.gd
+++ b/core/components/Universe.gd
@@ -167,7 +167,7 @@ func _remove_output(p_output: DMXOutput, p_no_signal: bool = false, p_delete: bo
outputs_removed.emit([p_output])
if p_delete:
- p_output.local_delete()
+ p_output.delete()
return true
diff --git a/core/components/fixtures/DMXFixture.gd b/core/components/fixtures/DMXFixture.gd
index f495bd81..b2ea8627 100644
--- a/core/components/fixtures/DMXFixture.gd
+++ b/core/components/fixtures/DMXFixture.gd
@@ -39,6 +39,9 @@ var _raw_override_layers: Dictionary[String, Dictionary] = {}
## The FixtureManifest for this fixture
var _manifest: FixtureManifest = null
+## Stores all parameter subscriptions { "zone": { "parameter": Array[Callable] } }
+var _parameter_subscriptions: Dictionary[String, Dictionary]
+
## init
func _init(p_uuid: String = UUID_Util.v4(), p_name: String = _name) -> void:
@@ -119,6 +122,11 @@ func get_parameter_functions(p_zone: String, p_parameter: String) -> Array:
return _manifest.get_parameter_functions(_mode, p_zone, p_parameter)
+## Gets all the parameter functions
+func get_function_control_type(p_zone: String, p_parameter: String, p_function: String) -> ControlType:
+ return _manifest.function_control_type(_mode, p_zone, p_parameter, p_function)
+
+
## Gets the default value of a parameter
func get_default(p_zone: String, p_parameter: String, p_function: String = "", p_raw_dmx: bool = false) -> float:
if p_function == "":
@@ -126,7 +134,7 @@ func get_default(p_zone: String, p_parameter: String, p_function: String = "", p
var dmx_value: int = _manifest.get_mode(_mode).zones[p_zone][p_parameter].functions[p_function].default
var range: Array = _manifest.get_mode(_mode).zones[p_zone][p_parameter].functions[p_function].dmx_range
-
+
if p_raw_dmx:
return dmx_value
else:
@@ -145,8 +153,32 @@ func get_default_function(p_zone: String, p_parameter: String) -> String:
## Gets the current value, or the default
-func get_current_value(p_zone: String, p_parameter: String, p_allow_default: bool = true) -> float:
- return _active_values.get(p_zone, {}).get(p_parameter, {}).get("value", get_default(p_zone, p_parameter) if p_allow_default else 0.0)
+func get_current_value(p_zone: String, p_parameter: String, p_allow_default: bool = true, p_allow_override: bool = true) -> float:
+ if has_override(p_zone, p_parameter):
+ return _raw_override_layers[p_zone][p_parameter].value
+ else:
+ return _active_values.get(p_zone, {}).get(p_parameter, {}).get("value", get_default(p_zone, p_parameter) if p_allow_default else 0.0)
+
+
+## Gets the current function, or the default
+func get_current_function(p_zone: String, p_parameter: String, p_allow_default: bool = true, p_allow_override: bool = true) -> String:
+ if has_override(p_zone, p_parameter):
+ return _raw_override_layers[p_zone][p_parameter].function
+ else:
+ return _active_values.get(p_zone, {}).get(p_parameter, {}).get("function", get_default_function(p_zone, p_parameter) if p_allow_default else "")
+
+
+## Gets the current value, or the default
+func get_current_value_or_force_default(p_zone: String, p_parameter: String) -> float:
+ var value: float = _active_values.get(p_zone, {}).get(p_parameter, {}).get("value", -1)
+
+ if value == -1:
+ if has_force_default(p_parameter):
+ return get_default(p_zone, p_parameter)
+ else:
+ return 0.0
+ else:
+ return value
## Gets all the zones
@@ -162,6 +194,11 @@ func has_overrides() -> bool:
return _raw_override_layers != {}
+## Returns true if this Fixture has an override value
+func has_override(p_zone: String, p_parameter: String) -> bool:
+ return _raw_override_layers.has(p_zone) and _raw_override_layers[p_zone].has(p_parameter)
+
+
## Checks if this fixture has a parameter
func has_parameter(p_zone: String, p_parameter: String, p_function: String = "") -> bool:
if not _manifest:
@@ -178,11 +215,34 @@ func has_force_default(p_parameter: String) -> bool:
return _manifest.has_force_default(p_parameter)
+## Returns True if the given callable is subscribed to the given parameter
+func has_parameter_subscription(p_zone: String, p_parameter: String, p_callable: Callable) -> bool:
+ return _parameter_subscriptions.get(p_zone, {}).get(p_parameter, {}).has(p_callable)
+
+
## Checks if this DMXFixture has a function that can fade
func function_can_fade(p_zone: String, p_parameter: String, p_function: String) -> bool:
return _manifest.function_can_fade(_mode, p_zone, p_parameter, p_function)
+## Subscribes to a parameter on the given zone
+func subscribe_to_parameter(p_zone: String, p_parameter: String, p_callable: Callable) -> bool:
+ if has_parameter_subscription(p_zone, p_parameter, p_callable):
+ return false
+
+ _parameter_subscriptions.get_or_add(p_zone, {}).get_or_add(p_parameter, []).append(p_callable)
+ return true
+
+
+## Removes a subscription from the parameter in the given zone
+func remove_subscription(p_zone: String, p_parameter: String, p_callable: Callable) -> bool:
+ if not has_parameter_subscription(p_zone, p_parameter, p_callable):
+ return false
+
+ _parameter_subscriptions[p_zone][p_parameter].erase(p_callable)
+ return true
+
+
## Internl: Sets the channel
func _set_channel(p_channel: int, p_no_signal: bool = false) -> void:
_channel = p_channel
@@ -220,18 +280,30 @@ func _set_manifest(p_manifest: Variant, p_mode: String, p_no_signal: bool = fals
func _set_parameter(p_zone: String, p_parameter: String, p_function: String, p_value: Variant) -> void:
_active_values.get_or_add(p_zone, {})[p_parameter] = {"value": p_value, "function": p_function}
parameter_changed.emit(p_zone, p_parameter, p_function, p_value)
+
+ if has_override(p_zone, p_parameter):
+ return
+
+ _emit_parameter_subs(p_zone, p_parameter, p_function, p_value, false)
## Internal: Erases the parameter on the given layer
func _erase_parameter(p_zone: String, p_parameter: String) -> void:
_active_values.get_or_add(p_zone, {}).erase(p_parameter)
parameter_erased.emit(p_parameter, p_zone)
+
+ if has_override(p_zone, p_parameter):
+ return
+
+ _emit_parameter_subs(p_zone, p_parameter, get_default_function(p_zone, p_parameter), get_default(p_zone, p_parameter), false)
## Internal: Sets a parameter override to a float value
func _set_override(p_zone: String, p_parameter: String, p_function: String, p_value: float) -> void:
_raw_override_layers.get_or_add(p_zone, {})[p_parameter] = {"value": p_value, "function": p_function}
override_changed.emit(p_zone, p_parameter, p_function, p_value)
+
+ _emit_parameter_subs(p_zone, p_parameter, p_function, p_value, true)
## Internal: Erases the parameter override
@@ -240,6 +312,17 @@ func _erase_override(p_zone: String, p_parameter: String) -> void:
_raw_override_layers.erase(p_zone)
override_erased.emit(p_zone, p_parameter)
+
+ _emit_parameter_subs(p_zone, p_parameter, get_current_function(p_zone, p_parameter), get_current_value(p_zone, p_parameter), false)
+
+
+## Calls back all parameter subscripptions
+func _emit_parameter_subs(p_zone: String, p_parameter: String, p_function: String, p_value: float, p_override: bool) -> void:
+ for callable: Callable in _parameter_subscriptions.get(p_zone, {}).get(p_parameter, []).duplicate():
+ if not callable.is_valid():
+ _parameter_subscriptions[p_zone][p_parameter].erase(callable)
+ else:
+ callable.call(p_value, p_function, p_override)
## Internal: Erases all overrides
diff --git a/core/components/fixtures/Fixture.gd b/core/components/fixtures/Fixture.gd
index 5f9ef011..49e6e8fc 100644
--- a/core/components/fixtures/Fixture.gd
+++ b/core/components/fixtures/Fixture.gd
@@ -23,6 +23,17 @@ signal override_erased(zone: String, parameter: String)
signal all_override_removed()
+## Enum for ControlType
+enum ControlType {
+ INTENSITY, # % based, blackout/full
+ POSITION, # homeable, spatial
+ SELECT, # discrete choices
+ VALUE, # continuous, non-%
+ SPEED, # rate-based
+ SYSTEM # non-show controls
+}
+
+
## Root Zone
static var RootZone: String = "root"
@@ -98,6 +109,11 @@ func get_parameter_functions(p_zone: String, p_parameter: String) -> Array:
return []
+## Gets all the parameter functions
+func get_function_control_type(p_zone: String, p_parameter: String, p_function: String) -> ControlType:
+ return ControlType.VALUE
+
+
## Gets the default value of a parameter
func get_default(p_zone: String, p_parameter: String, p_function: String = "", p_raw_dmx: bool = false) -> float:
return 0.0
@@ -109,17 +125,17 @@ func get_default_function(p_zone: String, p_parameter: String) -> String:
## Gets the current value, or the default
-func get_current_value(p_zone: String, p_parameter: String, p_allow_default: bool = true) -> float:
+func get_current_value(p_zone: String, p_parameter: String, p_allow_default: bool = true, p_allow_override: bool = true) -> float:
return 0.0
-## Gets a value from the given layer id, parameter, and zone
-func get_current_value_layered(p_zone: String, p_parameter: String, p_layer_id: String, p_function: String = "", p_allow_default: bool = true) -> float:
- return 0.0
+## Gets the current value, or the default
+func get_current_function(p_zone: String, p_parameter: String, p_allow_default: bool = true, p_allow_override: bool = true) -> String:
+ return ""
-## Gets the current value from a given layer ID, the default is none is present, or 0 if p_parameter is not a force default
-func get_current_value_layered_or_force_default(p_zone: String, p_parameter: String, p_layer_id: String, p_function: String = "") -> float:
+## Gets the current value, or the default
+func get_current_value_or_force_default(p_zone: String, p_parameter: String) -> float:
return 0.0
@@ -133,6 +149,11 @@ func has_overrides() -> bool:
return false
+## Returns true if this Fixture has an override value
+func has_override(p_zone: String, p_parameter: String) -> bool:
+ return false
+
+
## Checks if this fixture has a parameter
func has_parameter(p_zone: String, p_parameter: String, p_function: String = "") -> bool:
return false
@@ -148,7 +169,17 @@ func function_can_fade(p_zone: String, p_parameter: String, p_function: String)
return false
-## Internal: Sets a parameter to a float value
+## Subscribes to a parameter on the given zone
+func subscribe_to_parameter(p_zone: String, p_parameter: String, p_callable: Callable) -> bool:
+ return false
+
+
+## Removes a subscription from the parameter in the given zone
+func remove_subscription(p_zone: String, p_parameter: String, p_callable: Callable) -> bool:
+ return false
+
+
+## Internal: Sets a parameter to a float values
func _set_parameter(p_zone: String, p_parameter: String, p_function: String, p_value: Variant) -> void:
return
diff --git a/core/global/Programmer.gd b/core/global/Programmer.gd
index 9921246f..d8aea57a 100644
--- a/core/global/Programmer.gd
+++ b/core/global/Programmer.gd
@@ -9,9 +9,6 @@ class_name CoreProgrammer extends Node
## Emitted when the programmer is cleared
signal cleared()
-## Emitted when the store mode state is changed
-signal store_mode_changed(store_mode_state: bool, class_hint: String)
-
## Save Modes
enum SaveMode {
@@ -32,6 +29,41 @@ enum MixMode {
Subtractive ## Uses Subtractive Mixing
}
+## Enum for Category
+enum Category {
+ DIMMER,
+ COLOR,
+ GOBO,
+ POSITION,
+ BEAM,
+ FOCUS,
+ SHAPERS,
+ CONTROL,
+ OTHER,
+}
+
+## Enum for Layer
+enum Layer {
+ VALUE,
+ START,
+ STOP,
+ CANFADE
+}
+
+
+## Stores the order of all parameters
+var _parameter_order: Dictionary[Category, Variant] = {
+ Category.DIMMER: ["Dimmer"],
+ Category.COLOR: [
+ "ColorAdd_R", "ColorAdd_G", "ColorAdd_B",
+ "ColorAdd_W", "ColorSub_R", "ColorSub_G", "ColorSub_B", "ColorSub_C", "ColorSub_M", "ColorSub_Y",
+ "ColorRGB_Red", "ColorRGB_Green", "ColorRGB_Blue",
+ "CTC", "Color"
+ ],
+ Category.POSITION: ["Pan", "Tilt"]
+}
+
+
## Current store mode state
var _store_mode_state: bool = false
@@ -50,6 +82,31 @@ func _init() -> void:
settings_manager.register_networked_callbacks({
"on_cleared": _clear,
})
+
+ _convert_order_array_to_dict()
+
+
+## Gets a Category enum as a string
+func get_category_as_string(p_category: Category) -> String:
+ return Category.keys()[p_category].capitalize()
+
+
+## Gets a Category from a string
+func get_category_from_string(p_category: String) -> Category:
+ var index: int = Category.keys().find(p_category.to_upper())
+
+ return (index as Category) if index != -1 else Category.OTHER
+
+
+## Gets the order index of a given parameter, or -1
+func get_parameter_index(p_category: Category, p_parameter: String) -> int:
+ var number_result: Dictionary = Utils.remove_numbers(p_parameter)
+ var index: int = _parameter_order.get(p_category, {}).get(number_result.string, -1)
+
+ if number_result.numbers:
+ index += number_result.numbers[0]
+
+ return index
## Clears the programmer
@@ -57,11 +114,6 @@ func clear() -> Promise:
return Network.send_command("Programmer", "clear")
-## Called when the programmer is cleared on the server
-func _clear() -> void:
- cleared.emit()
-
-
## Function to set the fixture data at the given chanel key
func set_parameter(p_fixtures: Array, p_parameter: String, p_function: String, p_value: float, p_zone: String) -> Promise:
return Network.send_command("Programmer", "set_parameter", [p_fixtures, p_parameter, p_function, p_value, p_zone])
@@ -77,57 +129,23 @@ func erase_parameter(p_fixtures: Array, p_parameter: String, p_zone: String) ->
return Network.send_command("Programmer", "erase_parameter", [p_fixtures, p_parameter, p_zone])
-## Saves the current state of this programmer to a scene
-func save_to_new_scene(fixtures: Array, mode: SaveMode = SaveMode.MODIFIED) -> Promise:
- return Network.send_command("Programmer", "save_to_new_scene", [fixtures, mode])
-
-
## Shortcut to set the color of fixtures
func shortcut_set_color(p_fixtures: Array, p_color: Color, p_mode: MixMode) -> Promise:
return Network.send_command("Programmer", "shortcut_set_color", [p_fixtures, p_color, p_mode])
-## Enters store mode
-func enter_store_mode(callback: Callable = _store_callback, class_hint: String = "EngineComponent") -> void:
- _store_mode_state = true
- _store_mode_callback = callback
- store_mode_changed.emit(_store_mode_state, class_hint)
-
-
-## Exits store mode
-func exit_store_mode() -> void:
- if not _store_mode_state:
- return
-
- _store_mode_state = false
- _store_mode_callback = Callable()
- store_mode_changed.emit(_store_mode_state, "")
-
-
-## Gets the store mode state
-func get_store_mode() -> bool:
- return _store_mode_state
+## Converts the parameter order array to a dictonary for faster lookup
+func _convert_order_array_to_dict() -> void:
+ for category: Category in _parameter_order:
+ var array_order: Array = _parameter_order[category]
+ var dict_order: Dictionary[String, int]
+
+ for index: int in range(0, array_order.size()):
+ dict_order[array_order[index]] = index
+
+ _parameter_order[category] = dict_order
-## Resolves the store mode by handing back a component to store to
-func resolve_store_mode(with: EngineComponent) -> void:
- exit_store_mode()
-
- var fixtures: Array = Values.get_selection_value("selected_fixtures")
- Network.send_command("Programmer", "store_into", [with, fixtures])
-
-
-## Resolves the store mode by handing back a classname for a new component
-func resolve_store_mode_with_new(classname: String) -> Promise:
- exit_store_mode()
-
- var fixtures: Array = Values.get_selection_value("selected_fixtures")
- var promise: Promise = Network.send_command("Programmer", "store_into_new", [classname, fixtures])
-
- promise.then(_store_callback)
- return promise
-
-
-## Store callback
-func _store_callback(component: EngineComponent) -> void:
- Interface.show_name_prompt(component)
+## Called when the programmer is cleared on the server
+func _clear() -> void:
+ cleared.emit()
diff --git a/core/static/Utils.gd b/core/static/Utils.gd
index ca290a71..fdceea5a 100644
--- a/core/static/Utils.gd
+++ b/core/static/Utils.gd
@@ -16,6 +16,47 @@ class_name Utils extends Object
## }
static var _signal_connections: Dictionary
+## Removes numbers regex
+static var number_regex := RegEx.new()
+
+
+## init
+static func _static_init() -> void:
+ number_regex.compile("\\d+")
+
+
+## Formats a 12 hour time from an 24 hour interger
+static func format_12_hour(p_hour_24: int) -> String:
+ var period: String = "AM"
+ var hour_12: int = p_hour_24
+
+ if p_hour_24 == 0:
+ hour_12 = 12
+ period = "AM"
+ elif p_hour_24 == 12:
+ hour_12 = 12
+ period = "PM"
+ elif p_hour_24 > 12:
+ hour_12 = p_hour_24 - 12
+ period = "PM"
+
+ return str("%02d" % hour_12) + period
+
+
+## Removes numbers from a string
+static func remove_numbers(p_string: String) -> Dictionary:
+ var matches: Array[RegExMatch] = number_regex.search_all(p_string)
+ var numbers: Array[int] = []
+
+ for m: RegExMatch in matches:
+ numbers.append(int(m.get_string()))
+
+ var cleaned: String = number_regex.sub(p_string, "", true)
+
+ return {
+ "string": cleaned,
+ "numbers": numbers
+ }
## Saves a JSON valid dictonary to a file, creates the file and folder if it does not exist
diff --git a/panels/TO_BE_SORTED/Clock/Clock.gd b/panels/TO_BE_SORTED/Clock/Clock.gd
deleted file mode 100644
index ec984827..00000000
--- a/panels/TO_BE_SORTED/Clock/Clock.gd
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (c) 2025 Liam Sherwin. All rights reserved.
-# This file is part of the Spectrum Lighting Engine, licensed under the GPL v3.0 or later.
-# See the LICENSE file for details.
-
-class_name UIClock extends UIPanel
-## A clock
-
-
-func _ready() -> void:
- $Control/Label.label_settings = $Control/Label.label_settings.duplicate()
- _set_font_size()
-
-
-func _process(delta: float) -> void:
- $Control/Label.text = Time.get_time_string_from_system()
-
-
-func _set_font_size() -> void:
- $Control/Label.label_settings.font_size = (size.x * 0.13)
diff --git a/panels/TO_BE_SORTED/ColorPicker/ColorPicker.gd b/panels/TO_BE_SORTED/ColorPicker/ColorPicker.gd
deleted file mode 100644
index 8a2dd7d1..00000000
--- a/panels/TO_BE_SORTED/ColorPicker/ColorPicker.gd
+++ /dev/null
@@ -1,81 +0,0 @@
-# Copyright (c) 2025 Liam Sherwin. All rights reserved.
-# This file is part of the Spectrum Lighting Engine, licensed under the GPL v3.0 or later.
-# See the LICENSE file for details.
-
-class_name UIColorPicker extends UIPanel
-## UI Panel for showing a color wheel
-
-
-## VSlider for the value input on the pad
-@export var _pad_value_slider: VSlider
-
-## The TextureRect for the Pad
-@export var _pad: TextureRect
-
-## Crosshair for the Pad
-@export var _crosshair: TextureRect
-
-## Mix Mode selection
-@export var _mix_mode: OptionButton
-
-
-## Hue
-var _hue: float = 1
-
-## Saturation
-var _sat: float = 1
-
-## Value
-var _value: float = 1
-
-
-## Time since last call
-var _last_call_time: int = 0
-
-## 45 times per second
-var _call_interval: int = 1.0 / 45.0
-
-
-## Updates the color
-func _update_color() -> void:
- var color: Color = Color.from_hsv(_hue, _sat, _value)
-
- _update_programmer(color)
-
-
-## Called when the color is changed, will only output CoreEngine.call_interval times per second to avoid overloading the server
-func _update_programmer(color: Color) -> void:
- var current_time = Time.get_ticks_msec() / 1000.0 # Convert milliseconds to seconds
-
- if current_time - _last_call_time >= _call_interval:
- Programmer.shortcut_set_color(Values.get_selection_value("selected_fixtures", []), color, _mix_mode.selected)
-
- _last_call_time = current_time
-
-
-## Called for all GUI imputs on the pad
-func _on_texture_rect_gui_input(event: InputEvent) -> void:
- if event is InputEventMouse and Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
- var mouse_pos: Vector2 = _pad.get_local_mouse_position().clamp(Vector2.ZERO, _pad.size)
-
- _hue = remap(mouse_pos.x, 0, _pad.size.x, 0, 1)
- _sat = remap(mouse_pos.y, _pad.size.y, 0, 0, 1)
-
- _crosshair.position = mouse_pos - _crosshair.size / 2
- _update_color()
-
-
-## Called when the pad value slider is moved
-func _on_pad_value_slider_value_changed(value: float) -> void:
- _value = value
- _pad.self_modulate = Color(value, value, value)
- _crosshair.modulate = Color(1-value , 1-value , 1-value)
- _update_color()
-
-
-## Called when the pad is resized
-func _on_pad_resized() -> void:
- _crosshair.position = Vector2(
- remap(_hue, 0, 1, 0, _pad.size.x),
- remap(_sat, 0, 1, _pad.size.y, 0)
- ) - _crosshair.size / 2
diff --git a/panels/TO_BE_SORTED/ColorPicker/ColorPicker.tscn b/panels/TO_BE_SORTED/ColorPicker/ColorPicker.tscn
deleted file mode 100644
index 5dd9c7d6..00000000
--- a/panels/TO_BE_SORTED/ColorPicker/ColorPicker.tscn
+++ /dev/null
@@ -1,284 +0,0 @@
-[gd_scene load_steps=10 format=3 uid="uid://dhp8aa07tyxbf"]
-
-[ext_resource type="StyleBox" uid="uid://daxhx5qr5qdeu" path="res://assets/styles/UIPanelBase.tres" id="1_dxfro"]
-[ext_resource type="Script" uid="uid://bltvgq1jnaqgt" path="res://panels/TO_BE_SORTED/ColorPicker/ColorPicker.gd" id="1_pckpl"]
-[ext_resource type="StyleBox" uid="uid://csle20exwqtti" path="res://assets/styles/PanelMenuBar.tres" id="2_dxq6u"]
-[ext_resource type="Texture2D" uid="uid://clt2djtbul2kf" path="res://assets/icons/ColorPalett.svg" id="3_pxh41"]
-[ext_resource type="Texture2D" uid="uid://2p7ffb5yq0hx" path="res://assets/asset_scripts/color_picker.png" id="5_dxq6u"]
-[ext_resource type="PackedScene" uid="uid://dfu5s8fjy0ex4" path="res://components/EditControls/EditControls.tscn" id="5_j1krd"]
-[ext_resource type="Texture2D" uid="uid://b7q628ocmror7" path="res://assets/icons/Crosshair.svg" id="7_7y88r"]
-
-[sub_resource type="ButtonGroup" id="ButtonGroup_7y88r"]
-resource_name = "dasda"
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7y88r"]
-corner_radius_top_left = 5
-corner_radius_top_right = 5
-corner_radius_bottom_right = 5
-corner_radius_bottom_left = 5
-
-[node name="PanelContainer" type="PanelContainer" node_paths=PackedStringArray("_pad_value_slider", "_pad", "_crosshair", "_mix_mode", "edit_controls")]
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-grow_horizontal = 2
-grow_vertical = 2
-theme_override_styles/panel = ExtResource("1_dxfro")
-script = ExtResource("1_pckpl")
-_pad_value_slider = NodePath("VBoxContainer/Picker/HBoxContainer/PanelContainer2/VBoxContainer/PadValueSlider")
-_pad = NodePath("VBoxContainer/Picker/HBoxContainer/PanelContainer/Pad")
-_crosshair = NodePath("VBoxContainer/Picker/HBoxContainer/PanelContainer/Pad/Crosshair")
-_mix_mode = NodePath("VBoxContainer/PanelContainer/HBoxContainer/PanelContainer/MixMode")
-edit_controls = NodePath("VBoxContainer/PanelContainer/HBoxContainer/EditControls")
-
-[node name="VBoxContainer" type="VBoxContainer" parent="."]
-layout_mode = 2
-
-[node name="PanelContainer" type="PanelContainer" parent="VBoxContainer"]
-layout_mode = 2
-theme_override_styles/panel = ExtResource("2_dxq6u")
-
-[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/PanelContainer"]
-layout_mode = 2
-
-[node name="Button" type="Button" parent="VBoxContainer/PanelContainer/HBoxContainer"]
-layout_mode = 2
-text = "ColorPicker"
-icon = ExtResource("3_pxh41")
-flat = true
-
-[node name="TabBar" type="PanelContainer" parent="VBoxContainer/PanelContainer/HBoxContainer"]
-layout_mode = 2
-size_flags_horizontal = 6
-
-[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/PanelContainer/HBoxContainer/TabBar"]
-layout_mode = 2
-
-[node name="ShowPickers" type="Button" parent="VBoxContainer/PanelContainer/HBoxContainer/TabBar/HBoxContainer"]
-custom_minimum_size = Vector2(80, 0)
-layout_mode = 2
-toggle_mode = true
-button_pressed = true
-button_group = SubResource("ButtonGroup_7y88r")
-text = "Picker"
-
-[node name="ShowSliders" type="Button" parent="VBoxContainer/PanelContainer/HBoxContainer/TabBar/HBoxContainer"]
-custom_minimum_size = Vector2(80, 0)
-layout_mode = 2
-toggle_mode = true
-button_group = SubResource("ButtonGroup_7y88r")
-text = "Sliders"
-
-[node name="PanelContainer" type="PanelContainer" parent="VBoxContainer/PanelContainer/HBoxContainer"]
-layout_mode = 2
-
-[node name="MixMode" type="OptionButton" parent="VBoxContainer/PanelContainer/HBoxContainer/PanelContainer"]
-layout_mode = 2
-selected = 0
-item_count = 2
-popup/item_0/text = "Additive"
-popup/item_0/id = 0
-popup/item_1/text = "Subtractive"
-popup/item_1/id = 1
-
-[node name="EditControls" parent="VBoxContainer/PanelContainer/HBoxContainer" instance=ExtResource("5_j1krd")]
-layout_mode = 2
-show_edit = false
-show_settings = false
-
-[node name="Picker" type="PanelContainer" parent="VBoxContainer"]
-layout_mode = 2
-size_flags_vertical = 3
-
-[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/Picker"]
-layout_mode = 2
-
-[node name="PanelContainer" type="PanelContainer" parent="VBoxContainer/Picker/HBoxContainer"]
-clip_children = 1
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_styles/panel = SubResource("StyleBoxFlat_7y88r")
-
-[node name="Pad" type="TextureRect" parent="VBoxContainer/Picker/HBoxContainer/PanelContainer"]
-layout_mode = 2
-texture = ExtResource("5_dxq6u")
-expand_mode = 1
-
-[node name="Crosshair" type="TextureRect" parent="VBoxContainer/Picker/HBoxContainer/PanelContainer/Pad"]
-modulate = Color(0.0475577, 0.0475576, 0.0475576, 1)
-layout_mode = 1
-offset_right = 24.0
-offset_bottom = 24.0
-texture = ExtResource("7_7y88r")
-stretch_mode = 2
-
-[node name="PanelContainer2" type="PanelContainer" parent="VBoxContainer/Picker/HBoxContainer"]
-custom_minimum_size = Vector2(100, 0)
-layout_mode = 2
-
-[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/Picker/HBoxContainer/PanelContainer2"]
-layout_mode = 2
-
-[node name="Label" type="Label" parent="VBoxContainer/Picker/HBoxContainer/PanelContainer2/VBoxContainer"]
-layout_mode = 2
-text = "Value"
-horizontal_alignment = 1
-
-[node name="PadValueSlider" type="VSlider" parent="VBoxContainer/Picker/HBoxContainer/PanelContainer2/VBoxContainer"]
-layout_mode = 2
-size_flags_horizontal = 4
-size_flags_vertical = 3
-max_value = 1.0
-step = 0.001
-value = 1.0
-tick_count = 10
-ticks_on_borders = true
-
-[node name="Sliders" type="PanelContainer" parent="VBoxContainer"]
-visible = false
-layout_mode = 2
-size_flags_vertical = 3
-
-[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/Sliders"]
-layout_mode = 2
-
-[node name="PanelContainer" type="PanelContainer" parent="VBoxContainer/Sliders/HBoxContainer"]
-layout_mode = 2
-
-[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer"]
-layout_mode = 2
-
-[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer/HBoxContainer"]
-layout_mode = 2
-
-[node name="VSlider" type="VSlider" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer/HBoxContainer/VBoxContainer"]
-layout_mode = 2
-size_flags_horizontal = 4
-size_flags_vertical = 3
-
-[node name="Label" type="Label" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer/HBoxContainer/VBoxContainer"]
-layout_mode = 2
-size_flags_horizontal = 4
-text = "Red"
-
-[node name="VBoxContainer2" type="VBoxContainer" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer/HBoxContainer"]
-layout_mode = 2
-
-[node name="VSlider2" type="VSlider" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer/HBoxContainer/VBoxContainer2"]
-layout_mode = 2
-size_flags_horizontal = 4
-size_flags_vertical = 3
-
-[node name="Label" type="Label" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer/HBoxContainer/VBoxContainer2"]
-layout_mode = 2
-size_flags_horizontal = 4
-text = "Green"
-
-[node name="VBoxContainer3" type="VBoxContainer" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer/HBoxContainer"]
-layout_mode = 2
-
-[node name="VSlider3" type="VSlider" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer/HBoxContainer/VBoxContainer3"]
-layout_mode = 2
-size_flags_horizontal = 4
-size_flags_vertical = 3
-
-[node name="Label" type="Label" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer/HBoxContainer/VBoxContainer3"]
-layout_mode = 2
-size_flags_horizontal = 4
-text = "Blue"
-
-[node name="PanelContainer2" type="PanelContainer" parent="VBoxContainer/Sliders/HBoxContainer"]
-layout_mode = 2
-
-[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer2"]
-layout_mode = 2
-
-[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer2/HBoxContainer"]
-layout_mode = 2
-
-[node name="VSlider" type="VSlider" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer2/HBoxContainer/VBoxContainer"]
-layout_mode = 2
-size_flags_horizontal = 4
-size_flags_vertical = 3
-
-[node name="Label" type="Label" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer2/HBoxContainer/VBoxContainer"]
-layout_mode = 2
-size_flags_horizontal = 4
-text = "Hue"
-
-[node name="VBoxContainer2" type="VBoxContainer" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer2/HBoxContainer"]
-layout_mode = 2
-
-[node name="VSlider2" type="VSlider" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer2/HBoxContainer/VBoxContainer2"]
-layout_mode = 2
-size_flags_horizontal = 4
-size_flags_vertical = 3
-
-[node name="Label" type="Label" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer2/HBoxContainer/VBoxContainer2"]
-layout_mode = 2
-size_flags_horizontal = 4
-text = "Sat"
-
-[node name="VBoxContainer3" type="VBoxContainer" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer2/HBoxContainer"]
-layout_mode = 2
-
-[node name="VSlider3" type="VSlider" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer2/HBoxContainer/VBoxContainer3"]
-layout_mode = 2
-size_flags_horizontal = 4
-size_flags_vertical = 3
-
-[node name="Label" type="Label" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer2/HBoxContainer/VBoxContainer3"]
-layout_mode = 2
-size_flags_horizontal = 4
-text = "Value"
-
-[node name="PanelContainer3" type="PanelContainer" parent="VBoxContainer/Sliders/HBoxContainer"]
-layout_mode = 2
-
-[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer3"]
-layout_mode = 2
-
-[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer3/HBoxContainer"]
-layout_mode = 2
-
-[node name="VSlider" type="VSlider" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer3/HBoxContainer/VBoxContainer"]
-layout_mode = 2
-size_flags_horizontal = 4
-size_flags_vertical = 3
-
-[node name="Label" type="Label" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer3/HBoxContainer/VBoxContainer"]
-layout_mode = 2
-size_flags_horizontal = 4
-text = "Cyan"
-
-[node name="VBoxContainer2" type="VBoxContainer" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer3/HBoxContainer"]
-layout_mode = 2
-
-[node name="VSlider2" type="VSlider" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer3/HBoxContainer/VBoxContainer2"]
-layout_mode = 2
-size_flags_horizontal = 4
-size_flags_vertical = 3
-
-[node name="Label" type="Label" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer3/HBoxContainer/VBoxContainer2"]
-layout_mode = 2
-size_flags_horizontal = 4
-text = "Magenta"
-
-[node name="VBoxContainer3" type="VBoxContainer" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer3/HBoxContainer"]
-layout_mode = 2
-
-[node name="VSlider3" type="VSlider" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer3/HBoxContainer/VBoxContainer3"]
-layout_mode = 2
-size_flags_horizontal = 4
-size_flags_vertical = 3
-
-[node name="Label" type="Label" parent="VBoxContainer/Sliders/HBoxContainer/PanelContainer3/HBoxContainer/VBoxContainer3"]
-layout_mode = 2
-size_flags_horizontal = 4
-text = "Yellow"
-
-[connection signal="toggled" from="VBoxContainer/PanelContainer/HBoxContainer/TabBar/HBoxContainer/ShowPickers" to="VBoxContainer/Picker" method="set_visible"]
-[connection signal="toggled" from="VBoxContainer/PanelContainer/HBoxContainer/TabBar/HBoxContainer/ShowSliders" to="VBoxContainer/Sliders" method="set_visible"]
-[connection signal="gui_input" from="VBoxContainer/Picker/HBoxContainer/PanelContainer/Pad" to="." method="_on_texture_rect_gui_input"]
-[connection signal="resized" from="VBoxContainer/Picker/HBoxContainer/PanelContainer/Pad" to="." method="_on_pad_resized"]
-[connection signal="value_changed" from="VBoxContainer/Picker/HBoxContainer/PanelContainer2/VBoxContainer/PadValueSlider" to="." method="_on_pad_value_slider_value_changed"]
diff --git a/panels/UIClock/UIClock.gd b/panels/UIClock/UIClock.gd
new file mode 100644
index 00000000..332dd90d
--- /dev/null
+++ b/panels/UIClock/UIClock.gd
@@ -0,0 +1,319 @@
+# Copyright (c) 2026 Liam Sherwin. All rights reserved.
+# This file is part of the Spectrum Lighting Engine, licensed under the GPL v3.0 or later.
+# See the LICENSE file for details.
+
+class_name UIClock extends UIPanel
+## A clock
+
+
+## Emitted when the show year state is changed
+signal show_year_changed(show_year: bool)
+
+## Emitted when the show month state is changed
+signal show_month_changed(show_month: bool)
+
+## Emitted when the show day state is changed
+signal show_day_changed(show_day: bool)
+
+## Emitted when the show hour state is changed
+signal show_hour_changed(show_hour: bool)
+
+## Emitted when the show minute state is changed
+signal show_minute_changed(show_minute: bool)
+
+## Emitted when the show second state is changed
+signal show_second_changed(show_second: bool)
+
+## Emitted when the show millisecond state is changed
+signal show_millisecond_changed(show_millisecond: bool)
+
+## Emitted when the use twelve hour state is changed
+signal use_twelve_hour_changed(use_twelve_hour: bool)
+
+## Emitted when the font scale multiplier is changed
+signal font_scale_multiplier_changed(font_scale_multiplier: float)
+
+## Emitted when the font color is changed
+signal font_color_changed(font_color: Color)
+
+
+## Contains english names for all months
+const Months: Array[String] = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
+
+## Contains english names for all week days
+const WeekDays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
+
+
+## The Label to show the time
+@export var label: Label
+
+
+## Shows the year
+var _show_year: bool = false
+
+## Shows the month
+var _show_month: bool = false
+
+## Shows the day
+var _show_day: bool = false
+
+## Shows the hour
+var _show_hour: bool = true
+
+## Shows the minute
+var _show_minute: bool = true
+
+## Shows the second
+var _show_second: bool = true
+
+## Shows the millisecond
+var _show_millisecond: bool = false
+
+## Displays the hour in AM/PM time
+var _use_twelve_hour: bool = false
+
+## The font scale multiplier
+var _font_scale_multiplier: float = 0.9
+
+## Font color
+var _font_color: Color = Color.WHITE
+
+
+## init
+func _init() -> void:
+ super._init()
+
+ _set_class_name("UIClock")
+
+ settings_manager.register_setting("ShowYear", Data.Type.BOOL, set_show_year, get_show_year, [show_year_changed])
+ settings_manager.register_setting("ShowMonth", Data.Type.BOOL, set_show_month, get_show_month, [show_month_changed])
+ settings_manager.register_setting("ShowDay", Data.Type.BOOL, set_show_day, get_show_day, [show_day_changed])
+ settings_manager.register_setting("ShowHour", Data.Type.BOOL, set_show_hour, get_show_hour, [show_hour_changed])
+ settings_manager.register_setting("ShowMinute", Data.Type.BOOL, set_show_minute, get_show_minute, [show_minute_changed])
+ settings_manager.register_setting("ShowSecond", Data.Type.BOOL, set_show_second, get_show_second, [show_second_changed])
+ settings_manager.register_setting("ShowMillisecond", Data.Type.BOOL, set_show_millisecond, get_show_millisecond, [show_millisecond_changed])
+
+ settings_manager.register_setting("UseTwelveHour", Data.Type.BOOL, set_use_twelve_hour, get_use_twelve_hour, [use_twelve_hour_changed])
+ settings_manager.register_setting("FontScaleMultiplier", Data.Type.FLOAT, set_font_scale_multiplier, get_font_scale_multiplier, [font_scale_multiplier_changed]).set_min_max(0.1, 5.0)
+ settings_manager.register_setting("FontColor", Data.Type.COLOR, set_font_color, get_font_color, [font_color_changed])
+
+## ready
+func _ready() -> void:
+ label.label_settings = label.label_settings.duplicate()
+
+ await get_tree().process_frame
+ await get_tree().process_frame
+
+ _update_font_size()
+
+
+## process
+func _process(delta: float) -> void:
+ var date_time: Dictionary = Time.get_datetime_dict_from_system()
+ var result: PackedStringArray = []
+
+ if _show_year:
+ result.append(str(date_time.year))
+
+ if _show_month:
+ result.append(Months[date_time.month])
+
+ if _show_day:
+ result.append(WeekDays[date_time.weekday])
+
+ if _show_hour:
+ result.append(Utils.format_12_hour(date_time.hour) if _use_twelve_hour else ("%02d" % date_time.hour))
+
+ if _show_minute:
+ result.append("%02d" % date_time.minute)
+
+ if _show_second:
+ result.append("%02d" % date_time.second)
+
+ if _show_millisecond:
+ var now_sec: float = Time.get_unix_time_from_system()
+ var ms_in_second: int = int((now_sec - floor(now_sec)) * 1000)
+ result.append("%03d" % ms_in_second)
+
+ label.set_text(":".join(result))
+
+
+## Sets the show year state
+func set_show_year(p_show_year: bool, p_no_signal: bool = false) -> void:
+ _show_year = p_show_year
+ queue(_update_font_size)
+
+ if not p_no_signal:
+ show_year_changed.emit(_show_year)
+
+
+## Sets the show month state
+func set_show_month(p_show_month: bool, p_no_signal: bool = false) -> void:
+ _show_month = p_show_month
+ queue(_update_font_size)
+
+ if not p_no_signal:
+ show_month_changed.emit(_show_month)
+
+
+## Sets the show day state
+func set_show_day(p_show_day: bool, p_no_signal: bool = false) -> void:
+ _show_day = p_show_day
+ queue(_update_font_size)
+
+ if not p_no_signal:
+ show_day_changed.emit(_show_day)
+
+
+## Sets the show hour state
+func set_show_hour(p_show_hour: bool, p_no_signal: bool = false) -> void:
+ _show_hour = p_show_hour
+ queue(_update_font_size)
+
+ if not p_no_signal:
+ show_hour_changed.emit(_show_hour)
+
+
+## Sets the show minute state
+func set_show_minute(p_show_minute: bool, p_no_signal: bool = false) -> void:
+ _show_minute = p_show_minute
+ queue(_update_font_size)
+
+ if not p_no_signal:
+ show_minute_changed.emit(_show_minute)
+
+
+## Sets the show second state
+func set_show_second(p_show_second: bool, p_no_signal: bool = false) -> void:
+ _show_second = p_show_second
+ queue(_update_font_size)
+
+ if not p_no_signal:
+ show_second_changed.emit(_show_second)
+
+
+## Sets the show millisecond state
+func set_show_millisecond(p_show_millisecond: bool, p_no_signal: bool = false) -> void:
+ _show_millisecond = p_show_millisecond
+ queue(_update_font_size)
+
+ if not p_no_signal:
+ show_millisecond_changed.emit(_show_millisecond)
+
+
+## Sets the use twelve hour state
+func set_use_twelve_hour(p_use_twelve_hour: bool, p_no_signal: bool = false) -> void:
+ _use_twelve_hour = p_use_twelve_hour
+ queue(_update_font_size)
+
+ if not p_no_signal:
+ use_twelve_hour_changed.emit(_use_twelve_hour)
+
+
+## Sets the font scale multiplier
+func set_font_scale_multiplier(p_font_scale_multiplier: float, p_no_signal: bool = false) -> void:
+ _font_scale_multiplier = p_font_scale_multiplier
+ queue(_update_font_size)
+
+ if not p_no_signal:
+ font_scale_multiplier_changed.emit(_font_scale_multiplier)
+
+
+## Sets the font color
+func set_font_color(p_font_color: Color, p_no_signal: bool = false) -> void:
+ _font_color = p_font_color
+ label.get_label_settings().set_font_color(_font_color)
+
+ if not p_no_signal:
+ font_color_changed.emit(_font_color)
+
+
+## Gets the show year state
+func get_show_year() -> bool:
+ return _show_year
+
+
+## Gets the show month state
+func get_show_month() -> bool:
+ return _show_month
+
+
+## Gets the show day state
+func get_show_day() -> bool:
+ return _show_day
+
+
+## Gets the show hour state
+func get_show_hour() -> bool:
+ return _show_hour
+
+
+## Gets the show minute state
+func get_show_minute() -> bool:
+ return _show_minute
+
+
+## Gets the show second state
+func get_show_second() -> bool:
+ return _show_second
+
+
+## Gets the show millisecond state
+func get_show_millisecond() -> bool:
+ return _show_millisecond
+
+
+## Gets the use twelve hour state
+func get_use_twelve_hour() -> bool:
+ return _use_twelve_hour
+
+
+## Gets the font scale multiplier
+func get_font_scale_multiplier() -> float:
+ return _font_scale_multiplier
+
+
+## Gets the font color
+func get_font_color() -> Color:
+ return _font_color
+
+
+## Serializes this UIClock
+func serialize() -> Dictionary:
+ return super.serialize().merged({
+ "show_year": _show_year,
+ "show_month": _show_month,
+ "show_day": _show_day,
+ "show_hour": _show_hour,
+ "show_minute": _show_minute,
+ "show_second": _show_second,
+ "show_millisecond": _show_millisecond,
+ "font_scale_multiplier": _font_scale_multiplier,
+ "font_color": var_to_str(_font_color),
+ })
+
+
+## Deserialize this UIClock
+func deserialize(p_serialized_data: Dictionary) -> void:
+ super.deserialize(p_serialized_data)
+ print("deser")
+ set_show_year(type_convert(p_serialized_data.get("show_year", _show_year), TYPE_BOOL), true)
+ set_show_month(type_convert(p_serialized_data.get("show_month", _show_month), TYPE_BOOL), true)
+ set_show_day(type_convert(p_serialized_data.get("show_day", _show_day), TYPE_BOOL), true)
+ set_show_hour(type_convert(p_serialized_data.get("show_hour", _show_hour), TYPE_BOOL), true)
+ set_show_minute(type_convert(p_serialized_data.get("show_minute", _show_minute), TYPE_BOOL), true)
+ set_show_second(type_convert(p_serialized_data.get("show_second", _show_second), TYPE_BOOL), true)
+ set_show_millisecond(type_convert(p_serialized_data.get("show_millisecond", _show_millisecond), TYPE_BOOL), true)
+
+ set_font_scale_multiplier(type_convert(p_serialized_data.get("font_scale_multiplier", _font_scale_multiplier), TYPE_FLOAT), true)
+ set_font_color(type_convert(str_to_var(type_convert(p_serialized_data.get("font_color", _font_color), TYPE_STRING)), TYPE_COLOR), true)
+
+
+## Sets font size
+func _update_font_size() -> void:
+ label.label_settings.font_size += 32
+ var text_width: int = label.label_settings.font.get_string_size(label.text, label.horizontal_alignment, -1, label.label_settings.font_size).x
+
+ if text_width > size.x and text_width > 0:
+ var scale_factor: float = size.x / text_width * _font_scale_multiplier
+ label.label_settings.set_font_size(int(label.label_settings.font_size * scale_factor))
diff --git a/panels/TO_BE_SORTED/Clock/Clock.gd.uid b/panels/UIClock/UIClock.gd.uid
similarity index 100%
rename from panels/TO_BE_SORTED/Clock/Clock.gd.uid
rename to panels/UIClock/UIClock.gd.uid
diff --git a/panels/TO_BE_SORTED/Clock/Clock.tscn b/panels/UIClock/UIClock.tscn
similarity index 64%
rename from panels/TO_BE_SORTED/Clock/Clock.tscn
rename to panels/UIClock/UIClock.tscn
index 065af0a0..00a95342 100644
--- a/panels/TO_BE_SORTED/Clock/Clock.tscn
+++ b/panels/UIClock/UIClock.tscn
@@ -1,13 +1,13 @@
[gd_scene load_steps=5 format=3 uid="uid://8mfy3g85o3g5"]
-[ext_resource type="Script" uid="uid://dt5d0ib35v1w4" path="res://panels/TO_BE_SORTED/Clock/Clock.gd" id="1_8tr61"]
-[ext_resource type="StyleBox" uid="uid://daxhx5qr5qdeu" path="res://assets/styles/UIPanelBase.tres" id="1_r82tc"]
-[ext_resource type="FontFile" uid="uid://crlak6jhg5jy2" path="res://assets/font/RubikMonoOne-Regular.ttf" id="2_546b1"]
+[ext_resource type="StyleBox" uid="uid://daxhx5qr5qdeu" path="res://assets/styles/UIPanelBase.tres" id="1_r3wuq"]
+[ext_resource type="Script" uid="uid://dt5d0ib35v1w4" path="res://panels/UIClock/UIClock.gd" id="2_xjxwu"]
+[ext_resource type="FontFile" uid="uid://crlak6jhg5jy2" path="res://assets/font/RubikMonoOne-Regular.ttf" id="3_bm64l"]
[sub_resource type="LabelSettings" id="LabelSettings_2atlb"]
-font = ExtResource("2_546b1")
+font = ExtResource("3_bm64l")
-[node name="Clock" type="PanelContainer"]
+[node name="UIClock" type="PanelContainer" node_paths=PackedStringArray("label")]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
@@ -15,8 +15,9 @@ grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
-theme_override_styles/panel = ExtResource("1_r82tc")
-script = ExtResource("1_8tr61")
+theme_override_styles/panel = ExtResource("1_r3wuq")
+script = ExtResource("2_xjxwu")
+label = NodePath("Control/Label")
[node name="Control" type="Control" parent="."]
layout_mode = 2
@@ -28,9 +29,9 @@ anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
-offset_left = -75.0
+offset_left = -80.0
offset_top = -10.0
-offset_right = 75.0
+offset_right = 80.0
offset_bottom = 10.0
grow_horizontal = 2
grow_vertical = 2
@@ -40,4 +41,4 @@ label_settings = SubResource("LabelSettings_2atlb")
horizontal_alignment = 1
vertical_alignment = 1
-[connection signal="resized" from="." to="." method="_set_font_size"]
+[connection signal="resized" from="." to="." method="_update_font_size"]
diff --git a/panels/UIColorPicker/UIColorPicker.gd b/panels/UIColorPicker/UIColorPicker.gd
new file mode 100644
index 00000000..4f1794c9
--- /dev/null
+++ b/panels/UIColorPicker/UIColorPicker.gd
@@ -0,0 +1,121 @@
+# Copyright (c) 2025 Liam Sherwin. All rights reserved.
+# This file is part of the Spectrum Lighting Engine, licensed under the GPL v3.0 or later.
+# See the LICENSE file for details.
+
+class_name UIColorPicker extends UIPanel
+## UI Panel for showing a color picker
+
+
+## The TextureRect for the Pad
+@export var _pad: TextureRect
+
+## Crosshair for the Pad
+@export var _crosshair: TextureRect
+
+## The VSlider for the Value
+@export var _value_slider: VSlider
+
+## Mix Mode selection
+@export var _mix_mode: OptionButton
+
+
+## The current color
+var _color: Color = Color.WHITE
+
+## Time since last call
+var _last_call_time: int = 0
+
+## 45 times per second
+var _call_interval: float = 1.0 / 45.0
+
+## All current selected fixtures
+var _fixtures: Array
+
+## The refernce fixture
+var _refernce_fixture: Fixture
+
+
+## init
+func _init() -> void:
+ super._init()
+
+ _set_class_name("UIColorPicker")
+ Values.connect_to_selection_value("selected_fixtures", _on_selected_fixture_changed)
+
+
+## Called when the color is changed, will only output CoreEngine.call_interval times per second to avoid overloading the server
+func _update_programmer() -> void:
+ var current_time: float = Time.get_ticks_msec() / 1000.0
+
+ if current_time - _last_call_time >= _call_interval:
+ _last_call_time = current_time
+ Programmer.shortcut_set_color(_fixtures, _color, _mix_mode.selected)
+
+
+## Called when the pad is resized
+func _update_pad() -> void:
+ _crosshair.position = Vector2(
+ remap(_color.h, 0, 1, 0, _pad.size.x),
+ remap(_color.s, 0, 1, _pad.size.y, 0)
+ ) - _crosshair.size / 2
+ _update_slider()
+
+
+## Updates the slider and backgroud color
+func _update_slider() -> void:
+ _pad.self_modulate = Color(_color.v, _color.v, _color.v)
+ _crosshair.modulate = Color(1-_color.v , 1-_color.v , 1-_color.v)
+ _value_slider.set_value_no_signal(_color.v)
+
+
+## Called when the selected fixtures change
+func _on_selected_fixture_changed(p_fixtures: Array) -> void:
+ if _refernce_fixture:
+ _refernce_fixture.remove_subscription(Fixture.RootZone, "ColorAdd_R", _on_fixture_red_changed)
+ _refernce_fixture.remove_subscription(Fixture.RootZone, "ColorAdd_G", _on_fixture_green_changed)
+ _refernce_fixture.remove_subscription(Fixture.RootZone, "ColorAdd_B", _on_fixture_blue_changed)
+
+ _fixtures = p_fixtures
+ _refernce_fixture = p_fixtures[0] if p_fixtures else null
+
+ if _refernce_fixture:
+ _refernce_fixture.subscribe_to_parameter(Fixture.RootZone, "ColorAdd_R", _on_fixture_red_changed)
+ _refernce_fixture.subscribe_to_parameter(Fixture.RootZone, "ColorAdd_G", _on_fixture_green_changed)
+ _refernce_fixture.subscribe_to_parameter(Fixture.RootZone, "ColorAdd_B", _on_fixture_blue_changed)
+
+
+## Called when the red parameter is changed on the fixture
+func _on_fixture_red_changed(p_red: float, _p_function: String, _p_override: bool) -> void:
+ _color.r = p_red
+ _update_pad()
+
+
+## Called when the green parameter is changed on the fixture
+func _on_fixture_green_changed(p_green: float, _p_function: String, _p_override: bool) -> void:
+ _color.g = p_green
+ _update_pad()
+
+
+## Called when the blue parameter is changed on the fixture
+func _on_fixture_blue_changed(p_blue: float, _p_function: String, _p_override: bool) -> void:
+ _color.b = p_blue
+ _update_pad()
+
+
+## Called for all GUI imputs on the pad
+func _on_texture_rect_gui_input(p_event: InputEvent) -> void:
+ if p_event is InputEventMouse and Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
+ var mouse_pos: Vector2 = _pad.get_local_mouse_position().clamp(Vector2.ZERO, _pad.size)
+
+ _color.h = remap(mouse_pos.x, 0, _pad.size.x, 0, 1)
+ _color.s = remap(mouse_pos.y, _pad.size.y, 0, 0, 1)
+
+ _crosshair.position = mouse_pos - _crosshair.size / 2
+ _update_programmer()
+
+
+## Called when the pad value slider is moved
+func _on_pad_value_slider_value_changed(p_value: float) -> void:
+ _color.v = p_value
+ _update_slider()
+ _update_programmer()
diff --git a/panels/TO_BE_SORTED/ColorPicker/ColorPicker.gd.uid b/panels/UIColorPicker/UIColorPicker.gd.uid
similarity index 100%
rename from panels/TO_BE_SORTED/ColorPicker/ColorPicker.gd.uid
rename to panels/UIColorPicker/UIColorPicker.gd.uid
diff --git a/panels/UIColorPicker/UIColorPicker.tscn b/panels/UIColorPicker/UIColorPicker.tscn
new file mode 100644
index 00000000..880bbc65
--- /dev/null
+++ b/panels/UIColorPicker/UIColorPicker.tscn
@@ -0,0 +1,110 @@
+[gd_scene load_steps=8 format=3 uid="uid://dhp8aa07tyxbf"]
+
+[ext_resource type="StyleBox" uid="uid://daxhx5qr5qdeu" path="res://assets/styles/UIPanelBase.tres" id="1_vgwxr"]
+[ext_resource type="Script" uid="uid://bltvgq1jnaqgt" path="res://panels/UIColorPicker/UIColorPicker.gd" id="2_ogrry"]
+[ext_resource type="PackedScene" uid="uid://dr0kfolsg46yu" path="res://panels/UIPanel/PanelMenuBar.tscn" id="3_g582g"]
+[ext_resource type="Texture2D" uid="uid://clt2djtbul2kf" path="res://assets/icons/ColorPalett.svg" id="4_ara1c"]
+[ext_resource type="Texture2D" uid="uid://2p7ffb5yq0hx" path="res://assets/asset_scripts/color_picker.png" id="6_3j0ua"]
+[ext_resource type="Texture2D" uid="uid://b7q628ocmror7" path="res://assets/icons/Crosshair.svg" id="7_b53g6"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7y88r"]
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[node name="UIColorPicker" type="PanelContainer" node_paths=PackedStringArray("_pad", "_crosshair", "_value_slider", "_mix_mode", "edit_controls", "menu_bar")]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_styles/panel = ExtResource("1_vgwxr")
+script = ExtResource("2_ogrry")
+_pad = NodePath("VBoxContainer/HBoxContainer/Picker/HBoxContainer/PanelContainer/Pad")
+_crosshair = NodePath("VBoxContainer/HBoxContainer/Picker/HBoxContainer/PanelContainer/Pad/Crosshair")
+_value_slider = NodePath("VBoxContainer/HBoxContainer/Picker/HBoxContainer/PanelContainer2/VBoxContainer/PadValueSlider")
+_mix_mode = NodePath("VBoxContainer/PanelMenuBar/HBoxContainer/PanelContainer/MixMode")
+edit_controls = NodePath("VBoxContainer/PanelMenuBar/HBoxContainer/EditControls")
+menu_bar = NodePath("VBoxContainer/PanelMenuBar")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="PanelMenuBar" parent="VBoxContainer" instance=ExtResource("3_g582g")]
+layout_mode = 2
+
+[node name="ComponentButton" parent="VBoxContainer/PanelMenuBar/HBoxContainer" index="0"]
+text = "ColorPicker"
+icon = ExtResource("4_ara1c")
+
+[node name="PanelContainer" type="PanelContainer" parent="VBoxContainer/PanelMenuBar/HBoxContainer" index="2"]
+layout_mode = 2
+
+[node name="MixMode" type="OptionButton" parent="VBoxContainer/PanelMenuBar/HBoxContainer/PanelContainer"]
+layout_mode = 2
+selected = 0
+item_count = 2
+popup/item_0/text = "Additive"
+popup/item_0/id = 0
+popup/item_1/text = "Subtractive"
+popup/item_1/id = 1
+
+[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="Picker" type="PanelContainer" parent="VBoxContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/HBoxContainer/Picker"]
+layout_mode = 2
+
+[node name="PanelContainer" type="PanelContainer" parent="VBoxContainer/HBoxContainer/Picker/HBoxContainer"]
+clip_children = 1
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_styles/panel = SubResource("StyleBoxFlat_7y88r")
+
+[node name="Pad" type="TextureRect" parent="VBoxContainer/HBoxContainer/Picker/HBoxContainer/PanelContainer"]
+layout_mode = 2
+texture = ExtResource("6_3j0ua")
+expand_mode = 1
+
+[node name="Crosshair" type="TextureRect" parent="VBoxContainer/HBoxContainer/Picker/HBoxContainer/PanelContainer/Pad"]
+modulate = Color(0.0475577, 0.0475576, 0.0475576, 1)
+layout_mode = 1
+offset_right = 24.0
+offset_bottom = 24.0
+texture = ExtResource("7_b53g6")
+stretch_mode = 2
+
+[node name="PanelContainer2" type="PanelContainer" parent="VBoxContainer/HBoxContainer/Picker/HBoxContainer"]
+custom_minimum_size = Vector2(100, 0)
+layout_mode = 2
+
+[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/HBoxContainer/Picker/HBoxContainer/PanelContainer2"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer/Picker/HBoxContainer/PanelContainer2/VBoxContainer"]
+layout_mode = 2
+text = "Value"
+horizontal_alignment = 1
+
+[node name="PadValueSlider" type="VSlider" parent="VBoxContainer/HBoxContainer/Picker/HBoxContainer/PanelContainer2/VBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 4
+size_flags_vertical = 3
+max_value = 1.0
+step = 0.001
+value = 1.0
+tick_count = 10
+ticks_on_borders = true
+
+[connection signal="gui_input" from="VBoxContainer/HBoxContainer/Picker/HBoxContainer/PanelContainer/Pad" to="." method="_on_texture_rect_gui_input"]
+[connection signal="resized" from="VBoxContainer/HBoxContainer/Picker/HBoxContainer/PanelContainer/Pad" to="." method="_update_pad"]
+[connection signal="value_changed" from="VBoxContainer/HBoxContainer/Picker/HBoxContainer/PanelContainer2/VBoxContainer/PadValueSlider" to="." method="_on_pad_value_slider_value_changed"]
+
+[editable path="VBoxContainer/PanelMenuBar"]
diff --git a/panels/UIDesk/UIDesk.gd b/panels/UIDesk/UIDesk.gd
index deaf4fd2..621b70de 100644
--- a/panels/UIDesk/UIDesk.gd
+++ b/panels/UIDesk/UIDesk.gd
@@ -318,12 +318,15 @@ func _on_edit_pressed() -> void:
## Called when the copy button is pressed
func _on_copy_pressed() -> void:
- var nodes_to_copy: Array = _selected_items.duplicate()
+ var nodes_to_copy: Array[UIDeskItemContainer] = _selected_items.duplicate()
select_none()
- for panel_container: Control in nodes_to_copy:
+ for panel_container: UIDeskItemContainer in nodes_to_copy:
+ var new_panel: UIPanel = UIDB.instance_panel(panel_container.get_panel().get_class_name())
+
+ new_panel.deserialize(panel_container.get_panel().serialize())
select_item(add_panel(
- panel_container.get_panel().duplicate(),
+ new_panel,
panel_container.position,
panel_container.size
))
diff --git a/panels/UIPanel/PanelMenuBar.tscn b/panels/UIPanel/PanelMenuBar.tscn
index e193675e..2a1a6bfd 100644
--- a/panels/UIPanel/PanelMenuBar.tscn
+++ b/panels/UIPanel/PanelMenuBar.tscn
@@ -23,7 +23,7 @@ flat = true
layout_mode = 2
size_flags_horizontal = 3
horizontal_scroll_mode = 3
-vertical_scroll_mode = 3
+vertical_scroll_mode = 0
[node name="EditControls" parent="HBoxContainer" instance=ExtResource("3_fnf3r")]
layout_mode = 2
diff --git a/panels/UIPanel/UIPanel.gd b/panels/UIPanel/UIPanel.gd
index f8ef6ef0..e76a80c1 100644
--- a/panels/UIPanel/UIPanel.gd
+++ b/panels/UIPanel/UIPanel.gd
@@ -91,15 +91,19 @@ func _init() -> void:
## Disables all the buttons in the given array
-static func disable_button_array(buttons: Array[Button]) -> void:
- for button: Button in buttons:
- button.disabled = true
+static func disable_button_array(p_buttons: Array[Button]) -> void:
+ set_button_array_enabled(p_buttons, true)
## Enables all the buttons in the given array
-static func enable_button_array(buttons: Array[Button]) -> void:
- for button: Button in buttons:
- button.disabled = false
+static func enable_button_array(p_buttons: Array[Button]) -> void:
+ set_button_array_enabled(p_buttons, false)
+
+
+## Sets an array of buttons enabled or disabled
+static func set_button_array_enabled(p_buttons: Array, p_disabled: bool) -> void:
+ for button: Button in p_buttons:
+ button.disabled = p_disabled
## Sets the move and resize handle
@@ -308,7 +312,8 @@ func serialize() -> Dictionary:
button_actions[button.name] = actions
return super.serialize().merged({
- "button_actions": button_actions
+ "button_actions": button_actions,
+ "show_menu_bar": get_menu_bar_visible()
})
@@ -316,6 +321,8 @@ func serialize() -> Dictionary:
func deserialize(p_serialized_data: Dictionary) -> void:
super.deserialize(p_serialized_data)
+ set_menu_bar_visible(type_convert(p_serialized_data.get("show_menu_bar", get_menu_bar_visible()), TYPE_BOOL))
+
var button_actions: Dictionary = type_convert(p_serialized_data.get("button_actions"), TYPE_DICTIONARY)
for button_name: Variant in button_actions.keys():
diff --git a/panels/UIProgrammer/ParameterController/ParameterController.gd b/panels/UIProgrammer/ParameterController/ParameterController.gd
index f6e16fa9..4a8bdf11 100644
--- a/panels/UIProgrammer/ParameterController/ParameterController.gd
+++ b/panels/UIProgrammer/ParameterController/ParameterController.gd
@@ -7,13 +7,16 @@ class_name ParameterController extends PanelContainer
## Emitted when the value is changed
-signal value_changed(parameter: String, function: String, value: float)
+signal value_changed(value: float)
-## Emitted when the erase is pressed
-signal erase_pressed(parameter: String)
+## Emitted when a function is selected
+signal function_selected(function: String)
+
+## Emitted when an override is added to the refernce fixture
+signal fixture_override_added()
-## Emitted when the random button is pressed
-signal random_pressed(parameter: String, function: String)
+## Emitted when the erase is pressed
+signal erase_pressed()
## FunctionList node for displaying all parameter functions
@@ -23,17 +26,30 @@ signal random_pressed(parameter: String, function: String)
@export var _slider: VSlider
## The NameLabel for the parameter
-@export var _name_label: Label
-
-## The zone label
-@export var _zone_label: Label
+@export var _title_button: Button
## TitleBar PanelContainer
@export var _title_bar: PanelContainer
+## The Label to show the value
+@export var _value_label: Label
+
+## The GridContainer to contain all preset buttons
+@export var _preset_container: GridContainer
+
+
+## Font size in PX for the preset buttons
+const PRESET_BUTTON_FONT_SIZE: int = 12
+
+## Number of Intensity percentage presets to show
+const NUM_INTENSITY_PRESETS: int = 5
+
+
+## The Fixture this ParameterControler is basing its value updates off
+var _refernce_fixture: Fixture
-## The category of this parameter
-var category: String = ""
+## Current override state of the refernce fixture
+var _override_state: bool = false
## The parameter this ParameterController controls
var _parameter: String = ""
@@ -50,48 +66,61 @@ var _title_stylebox: StyleBoxFlat = null
## Default color of this ParameterController
var _default_color: Color = Color(0.129, 0.129, 0.129)
-## Override color of this ParameterController
-var _override_color: Color = Color(1, 0.518, 0)
-
+## Ready
func _ready() -> void:
_title_stylebox = _title_bar.get_theme_stylebox("panel").duplicate()
_title_bar.add_theme_stylebox_override("panel", _title_stylebox)
+ _default_color = _title_stylebox.get_bg_color()
## Sets the parameter
-func set_parameter(parameter: String) -> void:
- _parameter = parameter
- _name_label.text = parameter
+func set_parameter(p_parameter: String) -> void:
+ _parameter = p_parameter
+ _title_button.set_text(_parameter)
## Sets the zone
-func set_zone(zone: String) -> void:
- _zone = zone
- _zone_label.text = zone
-
-
-## Gets the zone
-func get_zone() -> String:
- return _zone
-
-
-## Sets the state of the override background
-func set_override_bg(state: bool) -> void:
- if state:
- _title_stylebox.bg_color = _override_color
- else:
- _title_stylebox.bg_color = _default_color
+func set_zone(p_zone: String) -> void:
+ _zone = p_zone
## Sets the value of this parameter, no signal
func set_value(value: float) -> void:
_slider.set_value_no_signal(value)
+ _set_label_value(value)
+
+
+## Setst the value of this ParameterControler to the current fixture value
+func set_value_fixture() -> void:
+ if not _refernce_fixture:
+ return
+
+ set_value(_refernce_fixture.get_current_value(_zone, _parameter))
+ set_function(_refernce_fixture.get_current_function(_zone, _parameter))
## Sets the current selected function
func set_function(function: String) -> void:
_function_list.select(_functions.find(function))
+
+ if is_instance_valid(_refernce_fixture):
+ load_presets()
+ else:
+ load_presets.call_deferred()
+
+
+## Sets the state of the override background
+func set_override_bg(p_state: bool) -> void:
+ var target: Color
+
+ if p_state:
+ target = ThemeManager.Colors.Statuses.ProgrammerOverride
+ else:
+ target = _default_color
+
+ get_tree().create_tween().tween_property(_title_stylebox, "bg_color", target, ThemeManager.Constants.Times.InterfaceFadeFast)
+ _override_state = p_state
## Adds an item to the function list
@@ -106,31 +135,146 @@ func has_function(function: String) -> bool:
return _functions.has(function)
+## Gets the parameter
+func get_parameter() -> String:
+ return _parameter
+
+
+## Gets the zone
+func get_zone() -> String:
+ return _zone
+
+
+## Gets the current value
+func get_value() -> float:
+ return _slider.value
+
+
## Gets the current selected function
func get_function() -> String:
- return _function_list.get_item_text(_function_list.get_selected_id())
+ var selected: int = _function_list.get_selected_id() == -1
+
+ if selected:
+ return ""
+ else:
+ return _function_list.get_item_text(selected)
+
+
+## Gets the refernce fixture
+func get_refernce_fixture() -> Fixture:
+ return _refernce_fixture
+
+
+## Subscribes to the given fixture
+func subscribe(p_fixture: Fixture) -> void:
+ if is_instance_valid(_refernce_fixture):
+ remove_subscription()
+
+ _refernce_fixture = p_fixture
+ _refernce_fixture.subscribe_to_parameter(_zone, _parameter, _on_fixture_value_changed)
+
+ _override_state = _refernce_fixture.has_override(_zone, _parameter)
+
+
+## Removes the subscription to the fixture
+func remove_subscription() -> void:
+ if not is_instance_valid(_refernce_fixture):
+ return
+
+ _refernce_fixture.remove_subscription(_zone, _parameter, _on_fixture_value_changed)
+
+
+## Loads all the presets based on the current selected function
+func load_presets() -> void:
+ for old_preset: Control in _preset_container.get_children():
+ old_preset.queue_free()
+
+ if not _refernce_fixture:
+ return
+
+ match _refernce_fixture.get_function_control_type(_zone, _parameter, get_function()):
+ Fixture.ControlType.INTENSITY, Fixture.ControlType.VALUE:
+ var presets: Array[float] = _generate_preset_values(NUM_INTENSITY_PRESETS)
+
+ for value: float in presets:
+ _add_preset_button(value, str(int(value * 100)) + "%")
## Clears this ParameterController
func clear() -> void:
_functions.clear()
_function_list.clear()
-
_slider.set_value_no_signal(0)
+
+ remove_subscription()
set_override_bg(false)
+ load_presets()
-## Called when the slider value changes
-func _on_v_slider_value_changed(value: float) -> void:
- if _parameter:
- value_changed.emit(_zone, _parameter, get_function(), value)
+## Returns an array with preset values
+func _generate_preset_values(p_num_presets: int) -> Array[float]:
+ if p_num_presets <= 0:
+ return []
+ if p_num_presets == 1:
+ return [1.0]
+
+ var presets: Array[float] = []
+ var step: float = 1.0 / float(p_num_presets - 1)
+
+ for i in range(p_num_presets):
+ presets.append(i * step)
+
+ return presets
+
+
+## Adds a preset value button
+func _add_preset_button(p_value: float, p_text: String) -> void:
+ var new_button: Button = Button.new()
+
+ new_button.set_text(p_text)
+ new_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ new_button.pressed.connect(_set_value.bind(p_value))
+
+ _preset_container.add_child(new_button)
+
+
+## Sets and emits the value
+func _set_value(p_value: float) -> void:
+ value_changed.emit(p_value)
+ _set_label_value(p_value)
+
+
+## Sets the value on the label
+func _set_label_value(p_value: float) -> void:
+ _value_label.set_text(str(snappedf(p_value * 100, 0.1)) + "%")
+
+
+## Called when the value is changed on the selected fixture
+func _on_fixture_value_changed(p_value: float, p_function: String, p_override: bool) -> void:
+ set_value(p_value)
+ set_function(p_function)
+
+ if p_override != _override_state and p_override:
+ fixture_override_added.emit()
+ _override_state = p_override
## Called when the Erace button is pressed
func _on_erase_pressed() -> void:
- erase_pressed.emit(_zone, _parameter)
+ erase_pressed.emit()
+
+
+## Called when the default button is pressed
+func _on_default_pressed() -> void:
+ _set_value(_refernce_fixture.get_default(_zone, _parameter, get_function()))
+
+
+## Called when a function is selected
+func _on_function_list_item_selected(index: int) -> void:
+ function_selected.emit(get_function())
-## Called when the Random button is pressed
-func _on_random_pressed() -> void:
- random_pressed.emit(_zone, _parameter, get_function())
+## Called for each GUI input on the value label
+func _on_value_gui_input(p_event: InputEvent) -> void:
+ if p_event is InputEventMouseButton and p_event.button_index == MOUSE_BUTTON_LEFT and p_event.is_pressed():
+ _set_value(get_value())
diff --git a/panels/UIProgrammer/ParameterController/ParameterController.tscn b/panels/UIProgrammer/ParameterController/ParameterController.tscn
index 12b35605..fadd2c12 100644
--- a/panels/UIProgrammer/ParameterController/ParameterController.tscn
+++ b/panels/UIProgrammer/ParameterController/ParameterController.tscn
@@ -3,9 +3,8 @@
[ext_resource type="Theme" uid="uid://cyua45ur0ijqo" path="res://assets/Main.theme" id="1_7me6c"]
[ext_resource type="StyleBox" uid="uid://bxwylw4uk8cyk" path="res://assets/styles/ClassSettingsBG.tres" id="2_56yd5"]
[ext_resource type="Script" uid="uid://01ronclpv10b" path="res://panels/UIProgrammer/ParameterController/ParameterController.gd" id="3_437gf"]
-[ext_resource type="Texture2D" uid="uid://dyd2xybu6duv4" path="res://assets/icons/NewWindow.svg" id="4_sjuwc"]
+[ext_resource type="FontFile" uid="uid://crlak6jhg5jy2" path="res://assets/font/RubikMonoOne-Regular.ttf" id="4_33gbp"]
[ext_resource type="Texture2D" uid="uid://dlikb8or3xepr" path="res://assets/icons/Reset.svg" id="4_ukk8e"]
-[ext_resource type="Texture2D" uid="uid://c0lqh3h58ht3c" path="res://assets/icons/Random.svg" id="5_sjuwc"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_sjuwc"]
content_margin_left = 5.0
@@ -18,21 +17,36 @@ corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
-[sub_resource type="LabelSettings" id="LabelSettings_sjuwc"]
-font_size = 10
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_3ced7"]
+content_margin_left = 5.0
+content_margin_top = 5.0
+content_margin_right = 5.0
+content_margin_bottom = 5.0
+bg_color = Color(0.12156863, 0.12156863, 0.12156863, 1)
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+
+[sub_resource type="LabelSettings" id="LabelSettings_qnt7p"]
+font = ExtResource("4_33gbp")
+font_size = 26
-[node name="ParameterController" type="PanelContainer" node_paths=PackedStringArray("_function_list", "_slider", "_name_label", "_zone_label", "_title_bar")]
+[node name="ParameterController" type="PanelContainer" node_paths=PackedStringArray("_function_list", "_slider", "_title_button", "_title_bar", "_value_label", "_preset_container")]
custom_minimum_size = Vector2(219, 0)
-offset_right = 304.0
-offset_bottom = 425.0
+offset_right = 402.0
+offset_bottom = 319.0
+size_flags_horizontal = 3
+size_flags_vertical = 3
theme = ExtResource("1_7me6c")
theme_override_styles/panel = ExtResource("2_56yd5")
script = ExtResource("3_437gf")
-_function_list = NodePath("VBoxContainer/PanelContainer2/HBoxContainer/OptionButton")
+_function_list = NodePath("VBoxContainer/PanelContainer3/HBoxContainer/VBoxContainer/PanelContainer2/HBoxContainer/FunctionList")
_slider = NodePath("VBoxContainer/PanelContainer3/HBoxContainer/VSlider")
-_name_label = NodePath("VBoxContainer/TitleBar/HBoxContainer/VBoxContainer/NameLabel")
-_zone_label = NodePath("VBoxContainer/TitleBar/HBoxContainer/VBoxContainer/ZoneLabel")
+_title_button = NodePath("VBoxContainer/TitleBar/HBoxContainer/PanelContainer2/TitleButton")
_title_bar = NodePath("VBoxContainer/TitleBar")
+_value_label = NodePath("VBoxContainer/TitleBar/HBoxContainer/PanelContainer/Value")
+_preset_container = NodePath("VBoxContainer/PanelContainer3/HBoxContainer/VBoxContainer/PanelContainer/ScrollContainer/PresetContainer")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 2
@@ -44,26 +58,29 @@ theme_override_styles/panel = SubResource("StyleBoxFlat_sjuwc")
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/TitleBar"]
layout_mode = 2
-[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/TitleBar/HBoxContainer"]
+[node name="PanelContainer2" type="PanelContainer" parent="VBoxContainer/TitleBar/HBoxContainer"]
layout_mode = 2
-size_flags_horizontal = 3
+theme_override_styles/panel = SubResource("StyleBoxFlat_3ced7")
-[node name="NameLabel" type="Label" parent="VBoxContainer/TitleBar/HBoxContainer/VBoxContainer"]
+[node name="TitleButton" type="Button" parent="VBoxContainer/TitleBar/HBoxContainer/PanelContainer2"]
layout_mode = 2
-size_flags_horizontal = 3
+theme_override_colors/font_color = Color(1, 1, 1, 1)
text = "Dimmer"
-text_overrun_behavior = 3
+flat = true
+alignment = 0
+icon_alignment = 2
-[node name="ZoneLabel" type="Label" parent="VBoxContainer/TitleBar/HBoxContainer/VBoxContainer"]
+[node name="PanelContainer" type="PanelContainer" parent="VBoxContainer/TitleBar/HBoxContainer"]
layout_mode = 2
-size_flags_horizontal = 3
-text = "Dimmer"
-label_settings = SubResource("LabelSettings_sjuwc")
-text_overrun_behavior = 3
+size_flags_horizontal = 10
+theme_override_styles/panel = SubResource("StyleBoxFlat_3ced7")
-[node name="Erase" type="Button" parent="VBoxContainer/TitleBar/HBoxContainer"]
+[node name="Value" type="Label" parent="VBoxContainer/TitleBar/HBoxContainer/PanelContainer"]
layout_mode = 2
-icon = ExtResource("4_sjuwc")
+size_flags_horizontal = 10
+mouse_filter = 0
+text = "100%"
+label_settings = SubResource("LabelSettings_qnt7p")
[node name="PanelContainer3" type="PanelContainer" parent="VBoxContainer"]
layout_mode = 2
@@ -84,54 +101,48 @@ value = 1.0
tick_count = 20
ticks_on_borders = true
-[node name="PanelContainer" type="PanelContainer" parent="VBoxContainer/PanelContainer3/HBoxContainer"]
+[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/PanelContainer3/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
-[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer/PanelContainer3/HBoxContainer/PanelContainer"]
+[node name="PanelContainer" type="PanelContainer" parent="VBoxContainer/PanelContainer3/HBoxContainer/VBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
+size_flags_vertical = 3
-[node name="GridContainer" type="GridContainer" parent="VBoxContainer/PanelContainer3/HBoxContainer/PanelContainer/ScrollContainer"]
-visible = false
-layout_mode = 2
-size_flags_horizontal = 3
-columns = 2
-
-[node name="Button" type="Button" parent="VBoxContainer/PanelContainer3/HBoxContainer/PanelContainer/ScrollContainer/GridContainer"]
-custom_minimum_size = Vector2(80, 0)
+[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer/PanelContainer3/HBoxContainer/VBoxContainer/PanelContainer"]
layout_mode = 2
size_flags_horizontal = 3
-toggle_mode = true
-button_pressed = true
-text = "Open"
-[node name="Button5" type="Button" parent="VBoxContainer/PanelContainer3/HBoxContainer/PanelContainer/ScrollContainer/GridContainer"]
-custom_minimum_size = Vector2(80, 0)
+[node name="PresetContainer" type="GridContainer" parent="VBoxContainer/PanelContainer3/HBoxContainer/VBoxContainer/PanelContainer/ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
-toggle_mode = true
-text = "Close"
+columns = 5
-[node name="PanelContainer2" type="PanelContainer" parent="VBoxContainer"]
+[node name="PanelContainer2" type="PanelContainer" parent="VBoxContainer/PanelContainer3/HBoxContainer/VBoxContainer"]
layout_mode = 2
-[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/PanelContainer2"]
+[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/PanelContainer3/HBoxContainer/VBoxContainer/PanelContainer2"]
layout_mode = 2
-[node name="Erace" type="Button" parent="VBoxContainer/PanelContainer2/HBoxContainer"]
+[node name="Erace" type="Button" parent="VBoxContainer/PanelContainer3/HBoxContainer/VBoxContainer/PanelContainer2/HBoxContainer"]
layout_mode = 2
+size_flags_horizontal = 0
icon = ExtResource("4_ukk8e")
-[node name="Random" type="Button" parent="VBoxContainer/PanelContainer2/HBoxContainer"]
+[node name="Default" type="Button" parent="VBoxContainer/PanelContainer3/HBoxContainer/VBoxContainer/PanelContainer2/HBoxContainer"]
+custom_minimum_size = Vector2(40, 0)
layout_mode = 2
-icon = ExtResource("5_sjuwc")
+theme_override_fonts/font = ExtResource("4_33gbp")
+text = "d"
-[node name="OptionButton" type="OptionButton" parent="VBoxContainer/PanelContainer2/HBoxContainer"]
+[node name="FunctionList" type="OptionButton" parent="VBoxContainer/PanelContainer3/HBoxContainer/VBoxContainer/PanelContainer2/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
text_overrun_behavior = 3
-[connection signal="value_changed" from="VBoxContainer/PanelContainer3/HBoxContainer/VSlider" to="." method="_on_v_slider_value_changed"]
-[connection signal="pressed" from="VBoxContainer/PanelContainer2/HBoxContainer/Erace" to="." method="_on_erase_pressed"]
-[connection signal="pressed" from="VBoxContainer/PanelContainer2/HBoxContainer/Random" to="." method="_on_random_pressed"]
+[connection signal="gui_input" from="VBoxContainer/TitleBar/HBoxContainer/PanelContainer/Value" to="." method="_on_value_gui_input"]
+[connection signal="value_changed" from="VBoxContainer/PanelContainer3/HBoxContainer/VSlider" to="." method="_set_value"]
+[connection signal="pressed" from="VBoxContainer/PanelContainer3/HBoxContainer/VBoxContainer/PanelContainer2/HBoxContainer/Erace" to="." method="_on_erase_pressed"]
+[connection signal="pressed" from="VBoxContainer/PanelContainer3/HBoxContainer/VBoxContainer/PanelContainer2/HBoxContainer/Default" to="." method="_on_default_pressed"]
+[connection signal="item_selected" from="VBoxContainer/PanelContainer3/HBoxContainer/VBoxContainer/PanelContainer2/HBoxContainer/FunctionList" to="." method="_on_function_list_item_selected"]
diff --git a/panels/UIProgrammer/UIProgrammer.gd b/panels/UIProgrammer/UIProgrammer.gd
index afae0bd8..f42fdc12 100644
--- a/panels/UIProgrammer/UIProgrammer.gd
+++ b/panels/UIProgrammer/UIProgrammer.gd
@@ -6,335 +6,791 @@ class_name UIProgrammer extends UIPanel
## Programmer to adust the settings and paramiters of Fixtures
-## DimmerTabButton Button
-@export var _dimmer_tab_button: Button
+## Emitted when the number of controlers per page is changed
+signal controlers_per_page_changed(controlers: int)
-## ColorTabButton Button
-@export var _color_tab_button: Button
-## GoboTabButton Button
-@export var _gobo_tab_button: Button
+## The default number of ParameterControlers per page
+@export var default_controlers_per_page: int = 4
-## PositionTabButton Button
-@export var _position_tab_button: Button
+## Min size X for tab buttons
+@export var tab_button_min_size: int = 80
-## BeamTabButton Button
-@export var _beam_tab_button: Button
-## FocusTabButton Button
-@export var _focus_tab_button: Button
+@export_group("Nodes")
-## ShapersTabButton Button
-@export var _shapers_tab_button: Button
+## The HBoxContainer for all tab buttons
+@export var tab_button_container: HBoxContainer
-## ControlTabButton Button
-@export var _control_tab_button: Button
+## The HBoxContainer for all layer buttons
+@export var layer_button_container: HBoxContainer
## The HBoxContainer for ParameterControllers
-@export var _parameter_container: HBoxContainer
-
-## The ButtonGroup for all the tab buttons
-@export var button_group: ButtonGroup
-
-## The SaveToScene Button
-@export var save_to_scene: Button
+@export var controler_container: HBoxContainer
## Zone select button
@export var zone_select: OptionButton
+## Random Mode button
+@export var random_mode: OptionButton
-## All sorted tab buttons
-@onready var _tab_buttons: Dictionary[String, Button] = {
- "Dimmer": _dimmer_tab_button,
- "Color": _color_tab_button,
- "Gobo": _gobo_tab_button,
- "Position": _position_tab_button,
- "Beam": _beam_tab_button,
- "Focus": _focus_tab_button,
- "Shapers": _shapers_tab_button,
- "Control": _control_tab_button
-}
+## The Template placeholder item
+@export var template_placeholder: PanelContainer
-## The RefMap for tab buttons
-@onready var _button_map: RefMap = RefMap.from(_tab_buttons)
+## The OptionButton for selecting the current page
+@export var page_select_option: OptionButton
-## The current tab
-var _current_tab: String = "Dimmer"
+## The button to go to the previous page
+@export var prev_page_button: Button
-## All the currently selected fixtures
-var _fixtures: Array[Fixture]
+## The button to go to the next page
+@export var next_page_button: Button
+
+
+## RefMap for Programmer.Category: Button
+var _tab_buttons: RefMap = RefMap.new()
+
+## RefMap for Programmer.Layer: Button
+var _layer_buttons: RefMap = RefMap.new()
+
+## The ButtonGroup for all tab buttons
+var _tab_button_group: ButtonGroup = ButtonGroup.new()
+
+## The ButtonGroup for all layer buttons
+var _layer_button_group: ButtonGroup = ButtonGroup.new()
+
+## Contains all placeholder items
+var _placeholder_items: Array[Control]
+
+## Contains all ParameterControllers
+var _controlers: Array[ParameterController]
-## The current selected fixture zone
-var _current_zone: String = ""
+## Stores all pages by category { Category: { "zone": { index: { entry... } } }
+var _pages: Dictionary[Programmer.Category, Dictionary]
-## All the zones in the current selected fixtures
-var _selected_fixture_zones: Array[String] = []
+## Stores all parameters that are currenty being shown by page by zone
+var _shown_parameters: Dictionary[Programmer.Category, Dictionary]
-## RandomMode for random values
-var _random_mode: Programmer.RandomMode = Programmer.RandomMode.All
+## State for all category overrides colors
+var _category_overrides: Dictionary[Programmer.Category, bool]
-## All the current ParameterControllers
-var _parameter_controllers: Dictionary[String, Dictionary] = {}
+## Number of controlers per page
+var _controlers_per_page: int = default_controlers_per_page
-## All the current ParameterControllers sorted by categorie
-var _controller_categories: Dictionary[String, Array] = {}
+## The current category
+var _current_category: Programmer.Category = Programmer.Category.DIMMER
-## All current override values sorted by catigory
-var _current_values: Dictionary[String, Dictionary] = {}
+## The current zone
+var _current_zone: String = Fixture.RootZone
-## All the visible ParameterControllers
-var _visible_parameter_controllers: Array[ParameterController] = []
+## Current page number
+var _current_page: int = 0
-## Override color of this ParameterController
-var _tab_button_override_color: Color = Color(1, 0.518, 0)
+## The current programmer layer
+var _current_layer: Programmer.Layer = Programmer.Layer.VALUE
+
+## The OptionButton index of each zone
+var _zone_indexes: Dictionary[String, int] = {}
+
+## All zones that have override values in them
+var _zones_with_overrides: Dictionary[String, Variant]
+
+## Stores all fixture overrides: { "zone": { Programmer.Category: { "parameter": null } } }
+var _overrides_by_parameter: Dictionary[String, Dictionary]
+
+## All the currently selected fixtures
+var _fixtures: Array
## init
func _init() -> void:
super._init()
-
_set_class_name("UIProgrammer")
+
+ settings_manager.register_setting("ControlersPerPage", Data.Type.INT, set_controlers_per_page, get_controlers_per_page, [controlers_per_page_changed]).set_min_max(1, 10)
## ready
func _ready() -> void:
Values.connect_to_selection_value("selected_fixtures", _on_selected_fixtures_changed)
- button_group.pressed.connect(_on_tab_button_pressed)
+ Programmer.cleared.connect(_clear_overrides)
+
+ template_placeholder.get_parent().remove_child(template_placeholder)
- Programmer.cleared.connect(_clear_override_bg)
+ _create_controlers_and_placeholders()
+ _create_tab_buttons()
+ _create_layer_buttons()
-## Updates the tabs of categorys displayed to the user
-func _update_categorys(new_fixtures: Array) -> void:
- var controlers_to_hide: Array[ParameterController] = _visible_parameter_controllers.duplicate()
- var tabs_to_disable: Array[String] = _tab_buttons.keys()
- var new_visible_controllers: Array[ParameterController] = []
- var zone_select_index: int = 1
- var zone_to_select: int = 0
-
- _current_values.clear()
- zone_select.clear()
- _selected_fixture_zones = []
+## Sets the category to display
+func set_category(p_category: Programmer.Category) -> void:
+ _current_category = p_category
+ set_page(_current_page)
+
+
+## Sets the page number
+func set_page(p_page: int) -> void:
+ _current_page = abs(clamp(p_page, 0, get_page_count() - 1))
+ page_select_option.select(_current_page)
+ _update_page()
+
+
+## Sets the selected zone
+func set_zone(p_zone: String) -> void:
+ _current_zone = p_zone
+ _update_page()
+ _update_zone_override()
+
+
+## Sets the layer
+func set_layer(p_layer: Programmer.Layer) -> void:
+ _current_layer = p_layer
+
+
+## Sets the number of controlers per page
+func set_controlers_per_page(p_per_page: int) -> void:
+ if p_per_page == _controlers_per_page or p_per_page < 1:
+ return
+
+ _controlers_per_page = p_per_page
+ _create_controlers_and_placeholders()
+ _update_page()
+
+
+## Gets the number of controlers per page
+func get_controlers_per_page() -> int:
+ return _controlers_per_page
+
+
+## Gets the total number of pages
+func get_page_count() -> int:
+ var items: int = _pages.get(_current_category, {}).get(_current_zone, []).size()
+ return int(ceil(float(items) / float(_controlers_per_page)))
+
+
+## Seralizes this UIProgrammer
+func serialize() -> Dictionary:
+ return super.serialize().merged({
+ "controlers_per_page": get_controlers_per_page()
+ })
+
+
+## Deserializes this UIProgrammer
+func deserialize(p_serialized_data: Dictionary) -> void:
+ super.deserialize(p_serialized_data)
- zone_select.add_item("All", 0)
- zone_select.add_separator()
- zone_select.select(0)
+ set_controlers_per_page(type_convert(p_serialized_data.get("controlers_per_page", _controlers_per_page), TYPE_INT))
+
+
+## Creates the default set of placeholders and controlers
+func _create_controlers_and_placeholders() -> void:
+ for placeholder: PanelContainer in _placeholder_items:
+ placeholder.queue_free()
- for fixture: Fixture in new_fixtures:
- var zones: Array[String] = fixture.get_zones()
+ for controler: ParameterController in _controlers:
+ controler.queue_free()
+
+ _placeholder_items.clear()
+ _controlers.clear()
+
+ for index: int in range(0, _controlers_per_page):
+ var placeholder: PanelContainer = template_placeholder.duplicate()
+ placeholder.get_child(0).set_text(str(index + 1))
- if _current_zone not in zones:
- _current_zone = ""
+ var controler: ParameterController = preload("res://panels/UIProgrammer/ParameterController/ParameterController.tscn").instantiate()
+ controler.hide()
- Utils.sort_text_and_numbers(zones)
- Utils.array_move_to_start(zones, "root")
+ controler_container.add_child(placeholder)
+ controler_container.add_child(controler)
- for zone: String in zones:
- if zone not in _selected_fixture_zones:
- zone_select.add_item(zone, zone_select_index)
- _selected_fixture_zones.append(zone)
-
- if zone == _current_zone:
- zone_to_select = zone_select_index
-
- zone_select_index += 1
+ controler.value_changed.connect(_on_controler_value_changed.bind(controler))
+ controler.function_selected.connect(_on_controler_function_selected.bind(controler))
+ controler.fixture_override_added.connect(_on_controler_fixture_override_added.bind(controler))
+ controler.erase_pressed.connect(_on_controler_erase_pressed.bind(controler))
+
+ _placeholder_items.append(placeholder)
+ _controlers.append(controler)
+
+
+## Creates the tab buttons
+func _create_tab_buttons() -> void:
+ for category: Programmer.Category in Programmer.Category.values():
+ var tab_button: Button = Button.new()
+
+ tab_button.set_toggle_mode(true)
+ tab_button.set_disabled(true)
+ tab_button.set_button_group(_tab_button_group)
+
+ tab_button.set_text(Programmer.get_category_as_string(category).capitalize())
+ tab_button.set_custom_minimum_size(Vector2(tab_button_min_size, 0))
+
+ tab_button.pressed.connect(set_category.bind(category))
+ _tab_buttons.map(category, tab_button)
+ _category_overrides[category] = false
+
+ tab_button_container.add_child(tab_button)
+
+ _tab_buttons.left(Programmer.Category.DIMMER).set_pressed_no_signal(true)
+
+
+## Creates the layer buttons
+func _create_layer_buttons() -> void:
+ for layer: Programmer.Layer in Programmer.Layer.values():
+ var layer_button: Button = Button.new()
+
+ layer_button.set_toggle_mode(true)
+ layer_button.set_disabled(true)
+ layer_button.set_button_group(_layer_button_group)
+
+ layer_button.set_text(Programmer.Layer.keys()[layer].capitalize())
+ layer_button.set_custom_minimum_size(Vector2(tab_button_min_size, 0))
+
+ layer_button.pressed.connect(set_layer.bind(layer))
+ _layer_buttons.map(layer, layer_button)
+
+ layer_button_container.add_child(layer_button)
+
+ _layer_buttons.left(Programmer.Layer.VALUE).set_pressed_no_signal(true)
+
+
+## Updates the tabs of categorys displayed to the user
+func _update_categorys(p_fixtures: Array) -> void:
+ var zones: Dictionary = {Fixture.RootZone: null}
+ var used_categories: Dictionary[int, Variant]
+
+ _pages.clear()
+ zone_select.clear()
+
+ _zone_indexes.clear()
+ _zones_with_overrides.clear()
+ _shown_parameters.clear()
+
+ _fixtures = p_fixtures
+ _current_zone = Fixture.RootZone
+
+ for fixture: Fixture in p_fixtures:
+ for zone: String in fixture.get_zones():
var categories: Dictionary = fixture.get_parameter_categories(zone)
- var current_values: Dictionary = fixture.get_all_values()
- current_values.merge(fixture.get_all_override_values(), true)
+ if not zones.has(zone):
+ zones[zone] = null
- for parameter: String in categories:
- var controller: ParameterController
- if _parameter_controllers.get_or_add(zone, {}).has(parameter):
- controller = _parameter_controllers[zone][parameter]
-
- if fixture not in _fixtures:
- for function: String in fixture.get_parameter_functions(zone, parameter):
- if not controller.has_function(function):
- controller.add_function(function)
-
- tabs_to_disable.erase(controller.category)
- _button_map.left(controller.category).disabled = false
-
- if controller.category == _current_tab:
- controller.show()
-
- if controller not in new_visible_controllers:
- new_visible_controllers.append(controller)
-
- controlers_to_hide.erase(controller)
+ for parameter: String in categories.keys():
+ var category: int = Programmer.get_category_from_string(categories[parameter])
- else:
- controller = load("uid://bfk7wccmsndfr").instantiate()
- var category = categories[parameter]
-
- if category not in _tab_buttons.keys():
- category = "Control"
-
- controller.set_parameter(parameter)
- controller.set_zone(zone)
- controller.category = category
-
- for function: String in fixture.get_parameter_functions(zone, parameter):
- controller.add_function(function)
-
- _parameter_controllers.get_or_add(zone, {})[parameter] = controller
- _controller_categories.get_or_add(category, []).append(controller)
- new_visible_controllers.append(controller)
-
- tabs_to_disable.erase(category)
- _button_map.left(category).disabled = false
-
- if category != _current_tab:
- controller.hide()
-
- _parameter_container.add_child(controller)
- controller.value_changed.connect(_on_controller_value_changed.bind(controller))
- controller.random_pressed.connect(_on_controller_random_pressed.bind(controller))
- controller.erase_pressed.connect(_on_controller_erase_pressed.bind(controller))
+ _display_parameter(fixture, zone, parameter, category)
- var current: Dictionary = current_values.get(zone, {}).get(parameter, {})
- if current:
- controller.set_value(current.value)
- controller.set_function(current.function)
- controller.set_override_bg(true)
- _set_tab_button_override(_tab_buttons[controller.category], true)
+ if fixture.has_override(zone, parameter):
+ used_categories[category] = true
+ _zones_with_overrides[zone] = true
- _current_values.get_or_add(controller.category, {})[parameter] = {
- "value": current.value,
- "function": current.function
- }
+ _overrides_by_parameter.get_or_add(zone, {}).get_or_add(category, {})[parameter] = null
- for controller: ParameterController in controlers_to_hide:
- controller.clear()
- controller.hide()
+ for category: Programmer.Category in _pages.keys():
+ _set_category_override(category, used_categories.has(category))
- for tab_name: String in tabs_to_disable:
- _button_map.left(tab_name).disabled = true
- _set_tab_button_override(_button_map.left(tab_name), false)
+ zone_select.add_item(Fixture.RootZone, 0)
+ zone_select.set_item_icon(0, preload("res://assets/icons/Zone.svg"))
+ _zone_indexes[Fixture.RootZone] = 0
- _visible_parameter_controllers = new_visible_controllers
- _fixtures.assign(new_fixtures)
+ var sorted_zones: Array = Utils.sort_text_and_numbers(zones.keys())
+ for index: int in range(0, sorted_zones.size()):
+ var zone: String = sorted_zones[index]
+ var idx: int = index + 1
+
+ if _zone_indexes.has(zone):
+ continue
+
+ zone_select.add_item(zone, idx)
+ zone_select.set_item_icon(idx, preload("res://assets/icons/Zone.svg"))
+
+ zone_select.get_popup().set_item_icon_modulate(idx, ThemeManager.Colors.Statuses.ProgrammerOverride if _zones_with_overrides.has(zone) else Color.WHITE)
+ _zone_indexes[zone] = idx
+
+ _update_page()
+ _update_buttons()
+ _update_zone_override()
+
+
+## Displays a parameter controler for the given fixture
+func _display_parameter(p_fixture: Fixture, p_zone: String, p_parameter: String, p_category: Programmer.Category) -> void:
+ if _shown_parameters.get(p_category, {}).get(p_zone, {}).has(p_parameter):
+ return
+
+ var page: Dictionary = _pages.get_or_add(p_category, {}).get_or_add(p_zone, {})
+ var index: int = Programmer.get_parameter_index(p_category, p_parameter)
+
+ if index == -1:
+ index = _get_next_index(page)
- if zone_to_select == 0:
- _current_zone = ""
+ elif page.has(index):
+ page[_get_next_index(page)] = page[index]
+ page.erase(index)
- zone_select.select(zone_to_select)
- _update_zone_filter()
+ page[index] = {
+ "fixture": p_fixture,
+ "parameter": p_parameter,
+ }
+
+ _shown_parameters.get_or_add(p_category, {}).get_or_add(p_zone, {})[p_parameter] = null
-## Shows or hides ParameterControllers based on the current zone filter
-func _update_zone_filter() -> void:
- for controller: ParameterController in _visible_parameter_controllers:
- if controller.category != _current_tab:
- continue
+## Gets the next empty index starting from 0
+func _get_next_index(p_dict: Dictionary) -> int:
+ var end_index: int = 0
+
+ while p_dict.has(end_index):
+ end_index += 1
+
+ return end_index
+
+
+## Updates what ParameterControlers are shown on the current page
+func _update_page() -> void:
+ _update_page_buttons()
+
+ for index: int in range(0, _controlers_per_page):
+ var entrys_to_show: Dictionary = _pages.get(_current_category, {}).get(_current_zone, {})
+ var sorted_order: Array = entrys_to_show.keys()
+ var page_offset: int = _controlers_per_page * _current_page
- if controller.get_zone() == _current_zone or _current_zone == "":
- controller.show()
+ sorted_order.sort()
- elif controller.get_zone() != _current_zone:
- controller.hide()
+ if (sorted_order.size() - page_offset) > index:
+ _placeholder_items[index].hide()
+ _controlers[index].show()
+
+ var entry: Dictionary = entrys_to_show[sorted_order[index + page_offset]]
+ _update_controler(_controlers[index], entry.fixture, _current_zone, entry.parameter)
+
+ else:
+ _controlers[index].hide()
+ _placeholder_items[index].show()
+
+ _controlers[index].remove_subscription()
-## Clears the override BG on all of the ParameterControllers and tab buttons
-func _clear_override_bg() -> void:
- for zone: String in _parameter_controllers.keys():
- for controller: ParameterController in _parameter_controllers[zone].values():
- controller.set_override_bg(false)
+## Updated a ParameterControler with the given fixture, zone, and parameter
+func _update_controler(p_controler: ParameterController, p_fixture: Fixture, p_zone: String, p_parameter: String) -> void:
+ var old_value: float = p_controler.get_value()
+ p_controler.clear()
- for tab_button: Button in _tab_buttons.values():
- _set_tab_button_override(tab_button, false)
+ for function: String in p_fixture.get_parameter_functions(p_zone, p_parameter):
+ p_controler.add_function(function)
+
+ p_controler.set_zone(p_zone)
+ p_controler.set_parameter(p_parameter)
+ p_controler.set_function(p_fixture.get_current_function(p_zone, p_parameter))
+
+ var tween: Tween = get_tree().create_tween()
+ tween.tween_method(p_controler.set_value, old_value, p_fixture.get_current_value(p_zone, p_parameter), 0.2).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_QUINT)
+
+ p_controler.set_override_bg(p_fixture.has_override(p_zone, p_parameter))
+ p_controler.subscribe(p_fixture)
-## Sets the override state on the given tab button
-func _set_tab_button_override(tab_button: Button, state: bool) -> void:
- tab_button.begin_bulk_theme_override()
- if state:
- tab_button.add_theme_color_override("font_outline_color", _tab_button_override_color)
- tab_button.add_theme_color_override("font_color", _tab_button_override_color)
- tab_button.add_theme_color_override("font_focus_color", _tab_button_override_color)
- tab_button.add_theme_color_override("font_pressed_color", _tab_button_override_color)
- else:
- tab_button.remove_theme_color_override("font_outline_color")
- tab_button.remove_theme_color_override("font_color")
- tab_button.remove_theme_color_override("font_focus_color")
- tab_button.remove_theme_color_override("font_pressed_color")
- tab_button.end_bulk_theme_override()
+## Updates tab buttons to enable ones with parameters
+func _update_buttons() -> void:
+ for category: Programmer.Category in _tab_buttons.get_left():
+ _tab_buttons.left(category).set_disabled(not _pages.has(category))
+
+ var has_fixtures: bool = bool(_fixtures.size())
+
+ zone_select.set_disabled(not has_fixtures)
+ page_select_option.set_disabled(not has_fixtures)
+ random_mode.set_disabled(not has_fixtures)
+
+ set_button_array_enabled(_layer_buttons.get_right(), not has_fixtures)
+
+ if not has_fixtures:
+ next_page_button.set_disabled(true)
+ prev_page_button.set_disabled(true)
-## Called when the fixture selection changes
-func _on_selected_fixtures_changed(fixtures: Array) -> void:
- if visible and fixtures != _fixtures:
- _update_categorys(fixtures)
-
- save_to_scene.disabled = fixtures == []
+## Updates the page buttons
+func _update_page_buttons() -> void:
+ prev_page_button.set_disabled(_current_page == 0)
+ next_page_button.set_disabled(_current_page + 1 == get_page_count())
+
+ if get_page_count() == page_select_option.get_item_count():
+ return
+
+ page_select_option.clear()
+
+ for index in range(0, get_page_count()):
+ page_select_option.add_item(str(index))
+
+ page_select_option.select(_current_page)
+
+
+## Updates the override hilight on the zone select button
+func _update_zone_override() -> void:
+ _set_button_override(zone_select, _zones_with_overrides.has(_current_zone))
-## Called when any one of the tab buttons is pressed
-func _on_tab_button_pressed(button: Button) -> void:
- if _button_map.right(button) == _current_tab:
+## Enabled or disabled override on a Category
+func _set_category_override(p_category: Programmer.Category, p_override: bool) -> void:
+ if _category_overrides[p_category] == p_override:
return
- for controller: ParameterController in _controller_categories.get(_current_tab, []):
- controller.hide()
- #_visible_parameter_controllers.erase(controller)
+ _set_button_override(_tab_buttons.left(p_category), p_override)
+ _category_overrides[p_category] = p_override
+
+
+## Enables or disables override on a zone
+func _set_zone_override(p_zone: String, p_override: bool) -> void:
+ if not _zone_indexes.has(p_zone) or _zones_with_overrides.get(p_zone) == p_override:
+ return
- _current_tab = _button_map.right(button)
+ zone_select.get_popup().set_item_icon_modulate(_zone_indexes[p_zone], ThemeManager.Colors.Statuses.ProgrammerOverride if p_override else Color.WHITE)
+ _zones_with_overrides[p_zone] = p_override
+
+ if p_zone == _current_zone:
+ _set_button_override(zone_select, p_override)
+
+
+## Sets the override color on a BaseButton
+func _set_button_override(p_button: BaseButton, p_override: bool) -> void:
+ p_button.begin_bulk_theme_override()
- for controller: ParameterController in _controller_categories.get(_current_tab, []):
- if controller in _visible_parameter_controllers:
- controller.show()
+ if p_override:
+ p_button.add_theme_color_override("font_color", ThemeManager.Colors.Statuses.ProgrammerOverride)
+ p_button.add_theme_color_override("font_focus_color", ThemeManager.Colors.Statuses.ProgrammerOverride)
+ p_button.add_theme_color_override("font_pressed_color", ThemeManager.Colors.Statuses.ProgrammerOverride)
+ p_button.add_theme_color_override("font_hover_color", ThemeManager.Colors.Statuses.ProgrammerOverride)
+ p_button.add_theme_color_override("font_hover_pressed_color", ThemeManager.Colors.Statuses.ProgrammerOverride)
+ else:
+ p_button.remove_theme_color_override("font_color")
+ p_button.remove_theme_color_override("font_focus_color")
+ p_button.remove_theme_color_override("font_pressed_color")
+ p_button.remove_theme_color_override("font_hover_color")
+ p_button.remove_theme_color_override("font_hover_pressed_color")
- _update_zone_filter()
+ p_button.end_bulk_theme_override()
-## Called when a value is changed in a controller
-func _on_controller_value_changed(zone: String, parameter: String, function: String, value: float, controller: ParameterController) -> void:
- Programmer.set_parameter(_fixtures, parameter, function, value, zone)
- _current_values.get_or_add(controller.category, {})[parameter] = {"value": value, "function": function}
- controller.set_override_bg(true)
- _set_tab_button_override(_button_map.left(controller.category), true)
+## Handles setting overrides when a controler value is changed
+func _handle_controler_override(p_controler: ParameterController) -> void:
+ var parameter: String = p_controler.get_parameter()
+
+ if _overrides_by_parameter.get(_current_zone, {}).get(_current_category, {}).has(parameter):
+ return
+
+ _overrides_by_parameter.get_or_add(_current_zone, {}).get_or_add(_current_category, {})[parameter] = null
+
+ _set_category_override(_current_category, true)
+ _set_zone_override(_current_zone, true)
+
+ p_controler.set_override_bg(true)
-## Called when the random button is pressed on any of the controllers
-func _on_controller_random_pressed(zone: String, parameter: String, function: String, controller: ParameterController) -> void:
- Programmer.set_parameter_random(_fixtures, parameter, function, zone, _random_mode)
- _current_values.get_or_add(controller.category, {})[parameter] = {"value": 0, "function": function}
- controller.set_override_bg(true)
- _set_tab_button_override(_button_map.left(controller.category), true)
+## Handles removing overrides when a controler's erase button is pressed
+func _handle_controler_erase(p_controler: ParameterController) -> void:
+ var parameter: String = p_controler.get_parameter()
+
+ if not _overrides_by_parameter.get(_current_zone, {}).get(_current_category, {}).has(parameter):
+ return
+
+ _overrides_by_parameter[_current_zone][_current_category].erase(parameter)
+
+ if not _overrides_by_parameter[_current_zone][_current_category]:
+ _set_category_override(_current_category, false)
+ _overrides_by_parameter[_current_zone].erase(_current_category)
+
+ if not _overrides_by_parameter[_current_zone]:
+ _set_zone_override(_current_zone, false)
+ _overrides_by_parameter.erase(_current_zone)
+
+ p_controler.set_override_bg(false)
-## Called when the erase button is pressed on any of the controllers
-func _on_controller_erase_pressed(zone: String, parameter: String, controller: ParameterController) -> void:
- Programmer.erase_parameter(_fixtures, parameter, zone)
- controller.set_override_bg(false)
+## Clears all override colors
+func _clear_overrides() -> void:
+ for controler: ParameterController in _controlers:
+ controler.set_override_bg(false)
+ controler.set_value_fixture()
+
+ for category: Programmer.Category in _category_overrides:
+ _set_category_override(category, false)
+
+ for zone: String in _zones_with_overrides:
+ _set_zone_override(zone, false)
- if _current_values.get_or_add(controller.category, {}).erase(parameter) and not _current_values[controller.category]:
- _set_tab_button_override(_button_map.left(controller.category), false)
+ _zones_with_overrides.clear()
+ _overrides_by_parameter.clear()
-## Called when the Clear button is pressed
-func _on_erace_pressed() -> void:
- Programmer.clear()
+## Called when the fixture selection changes
+func _on_selected_fixtures_changed(fixtures: Array) -> void:
+ if visible and fixtures != _fixtures:
+ _update_categorys(fixtures)
-## Called when the RandomMode option is changed
-func _on_random_mode_item_selected(index: int) -> void:
- _random_mode = index
+## Called when the page back button is pressed
+func _on_page_back_pressed() -> void:
+ set_page(_current_page - 1)
-## Called when the ZoneSelect option is changed
+## Called when the page fowards button is pressed
+func _on_page_fowards_pressed() -> void:
+ set_page(_current_page + 1)
+
+
+## Called when a zone is selected
func _on_zone_select_item_selected(index: int) -> void:
- _current_zone = zone_select.get_item_text(index)
- if _current_zone == "All":
- _current_zone = ""
+ set_zone(zone_select.get_item_text(index))
+
+
+## Called when the clear button is pressed
+func _on_clear_pressed() -> void:
+ Programmer.clear()
+ _clear_overrides()
+
+
+## Called when the value on a ParameterControler is changed
+func _on_controler_value_changed(p_value: float, p_controler: ParameterController) -> void:
+ _handle_controler_override(p_controler)
- _update_zone_filter()
+ match _current_layer:
+ Programmer.Layer.VALUE:
+ Programmer.set_parameter(_fixtures, p_controler.get_parameter(), p_controler.get_function(), p_value, p_controler.get_zone())
+
+
+## Emitted when the function on the ParameterControler is changed
+func _on_controler_function_selected(p_function: String, p_controler: ParameterController) -> void:
+ _handle_controler_override(p_controler)
+ var value: float = p_controler.get_refernce_fixture().get_default(_current_zone, p_controler.get_parameter(), p_function)
+ Programmer.set_parameter(_fixtures, p_controler.get_parameter(), p_function, value, _current_zone)
+
+## Called when an override is added to a ParameterControlers refernce fixture
+func _on_controler_fixture_override_added(p_controler: ParameterController) -> void:
+ _handle_controler_override(p_controler)
-## Called when the SaveToScene button is pressed
-func _on_save_to_scene_pressed() -> void:
- Programmer.save_to_new_scene(_fixtures).then(func (scene: Scene):
- Interface.show_name_prompt(scene)
- )
+
+## Called when the erase button is pressed on a ParameterController
+func _on_controler_erase_pressed(p_controler: ParameterController) -> void:
+ Programmer.erase_parameter(_fixtures, p_controler.get_parameter(), p_controler.get_zone())
+ _handle_controler_erase(p_controler)
+
+ #if fixture is DMXFixture:
+ #_manifests[fixture.get_manifest()] = [fixture]
+
+
+ #var controlers_to_hide: Array[ParameterController] = _visible_parameter_controllers.duplicate()
+ #var tabs_to_disable: Array[String] = _tab_buttons.keys()
+ #var new_visible_controllers: Array[ParameterController] = []
+ #var zone_select_index: int = 1
+ #var zone_to_select: int = 0
+ #
+ #_current_values.clear()
+ #zone_select.clear()
+ #_selected_fixture_zones = []
+ #
+ #zone_select.add_item("All", 0)
+ #zone_select.set_item_icon(0, preload("res://assets/icons/Zone.svg"))
+ #zone_select.add_separator()
+ #zone_select.select(0)
+ #
+ #for fixture: Fixture in new_fixtures:
+ #var zones: Array[String] = fixture.get_zones()
+ #
+ #if _current_zone not in zones:
+ #_current_zone = ""
+ #
+ #Utils.sort_text_and_numbers(zones)
+ #Utils.array_move_to_start(zones, "root")
+ #
+ #for zone: String in zones:
+ #if zone not in _selected_fixture_zones:
+ #zone_select.add_item(zone, zone_select_index)
+ #zone_select.set_item_icon(zone_select_index + 1, preload("res://assets/icons/Zone.svg"))
+ #_selected_fixture_zones.append(zone)
+ #
+ #if zone == _current_zone:
+ #zone_to_select = zone_select_index
+ #
+ #zone_select_index += 1
+ #
+ #var categories: Dictionary = fixture.get_parameter_categories(zone)
+ #var current_values: Dictionary = fixture.get_all_values()
+ #
+ #current_values.merge(fixture.get_all_override_values(), true)
+ #
+ #for parameter: String in categories:
+ #var controller: ParameterController
+ #if _parameter_controllers.get_or_add(zone, {}).has(parameter):
+ #controller = _parameter_controllers[zone][parameter]
+ #
+ #if fixture not in _fixtures:
+ #for function: String in fixture.get_parameter_functions(zone, parameter):
+ #if not controller.has_function(function):
+ #controller.add_function(function)
+ #
+ #tabs_to_disable.erase(controller.category)
+ #_button_map.left(controller.category).disabled = false
+ #
+ #if controller.category == _current_tab:
+ #controller.show()
+ #
+ #if controller not in new_visible_controllers:
+ #new_visible_controllers.append(controller)
+ #
+ #controlers_to_hide.erase(controller)
+ #
+ #else:
+ #controller = load("uid://bfk7wccmsndfr").instantiate()
+ #var category = categories[parameter]
+ #
+ #if category not in _tab_buttons.keys():
+ #category = "Control"
+ #
+ #controller.set_parameter(parameter)
+ #controller.set_zone(zone)
+ #controller.category = category
+ #
+ #for function: String in fixture.get_parameter_functions(zone, parameter):
+ #controller.add_function(function)
+ #
+ #_parameter_controllers.get_or_add(zone, {})[parameter] = controller
+ #_controller_categories.get_or_add(category, []).append(controller)
+ #new_visible_controllers.append(controller)
+ #
+ #tabs_to_disable.erase(category)
+ #_button_map.left(category).disabled = false
+ #
+ #if category != _current_tab:
+ #controller.hide()
+ #
+ #_parameter_container.add_child(controller)
+ #controller.value_changed.connect(_on_controller_value_changed.bind(controller))
+ #controller.random_pressed.connect(_on_controller_random_pressed.bind(controller))
+ #controller.erase_pressed.connect(_on_controller_erase_pressed.bind(controller))
+ #
+ #var current: Dictionary = current_values.get(zone, {}).get(parameter, {})
+ #if current:
+ #controller.set_value(current.value)
+ #controller.set_function(current.function)
+ #controller.set_override_bg(true)
+ #_set_tab_button_override(_tab_buttons[controller.category], true)
+ #
+ #_current_values.get_or_add(controller.category, {})[parameter] = {
+ #"value": current.value,
+ #"function": current.function
+ #}
+ #
+ #for controller: ParameterController in controlers_to_hide:
+ #controller.clear()
+ #controller.hide()
+ #
+ #for tab_name: String in tabs_to_disable:
+ #_button_map.left(tab_name).disabled = true
+ #_set_tab_button_override(_button_map.left(tab_name), false)
+ #
+ #_visible_parameter_controllers = new_visible_controllers
+ #_fixtures.assign(new_fixtures)
+ #
+ #if zone_to_select == 0:
+ #_current_zone = ""
+ #
+ #zone_select.select(zone_to_select)
+ #_update_zone_filter()
+#
+#
+### Shows or hides ParameterControllers based on the current zone filter
+#func _update_zone_filter() -> void:
+ #for controller: ParameterController in _visible_parameter_controllers:
+ #if controller.category != _current_tab:
+ #continue
+ #
+ #if controller.get_zone() == _current_zone or _current_zone == "":
+ #controller.show()
+ #
+ #elif controller.get_zone() != _current_zone:
+ #controller.hide()
+#
+#
+### Clears the override BG on all of the ParameterControllers and tab buttons
+#func _clear_override_bg() -> void:
+ #for zone: String in _parameter_controllers.keys():
+ #for controller: ParameterController in _parameter_controllers[zone].values():
+ #controller.set_override_bg(false)
+ #
+ #for tab_button: Button in _tab_buttons.values():
+ #_set_tab_button_override(tab_button, false)
+#
+#
+
+#
+#
+
+#
+#
+### Called when any one of the tab buttons is pressed
+#func _on_tab_button_pressed(button: Button) -> void:
+ #if _button_map.right(button) == _current_tab:
+ #return
+ #
+ #for controller: ParameterController in _controller_categories.get(_current_tab, []):
+ #controller.hide()
+ ##_visible_parameter_controllers.erase(controller)
+ #
+ #_current_tab = _button_map.right(button)
+ #
+ #for controller: ParameterController in _controller_categories.get(_current_tab, []):
+ #if controller in _visible_parameter_controllers:
+ #controller.show()
+ #
+ #_update_zone_filter()
+#
+#
+### Called when a value is changed in a controller
+#func _on_controller_value_changed(zone: String, parameter: String, function: String, value: float, controller: ParameterController) -> void:
+ #Programmer.set_parameter(_fixtures, parameter, function, value, zone)
+ #_current_values.get_or_add(controller.category, {})[parameter] = {"value": value, "function": function}
+ #controller.set_override_bg(true)
+ #_set_tab_button_override(_button_map.left(controller.category), true)
+#
+#
+### Called when the random button is pressed on any of the controllers
+#func _on_controller_random_pressed(zone: String, parameter: String, function: String, controller: ParameterController) -> void:
+ #Programmer.set_parameter_random(_fixtures, parameter, function, zone, _random_mode)
+ #_current_values.get_or_add(controller.category, {})[parameter] = {"value": 0, "function": function}
+ #controller.set_override_bg(true)
+ #_set_tab_button_override(_button_map.left(controller.category), true)
+#
+#
+### Called when the erase button is pressed on any of the controllers
+#func _on_controller_erase_pressed(zone: String, parameter: String, controller: ParameterController) -> void:
+ #Programmer.erase_parameter(_fixtures, parameter, zone)
+ #controller.set_override_bg(false)
+ #
+ #if _current_values.get_or_add(controller.category, {}).erase(parameter) and not _current_values[controller.category]:
+ #_set_tab_button_override(_button_map.left(controller.category), false)
+#
+#
+### Called when the Clear button is pressed
+#func _on_erace_pressed() -> void:
+ #Programmer.clear()
+#
+#
+### Called when the RandomMode option is changed
+#func _on_random_mode_item_selected(index: int) -> void:
+ #_random_mode = index
+#
+#
+### Called when the ZoneSelect option is changed
+#func _on_zone_select_item_selected(index: int) -> void:
+ #_current_zone = zone_select.get_item_text(index)
+ #if _current_zone == "All":
+ #_current_zone = ""
+ #
+ #_update_zone_filter()
+#
+#
+### Called when the SaveToScene button is pressed
+#func _on_save_to_scene_pressed() -> void:
+ #Programmer.save_to_new_scene(_fixtures).then(func (scene: Scene):
+ #Interface.show_name_prompt(scene)
+ #)
diff --git a/panels/UIProgrammer/UIProgrammer.tscn b/panels/UIProgrammer/UIProgrammer.tscn
index 7aa4c3f1..4789f106 100644
--- a/panels/UIProgrammer/UIProgrammer.tscn
+++ b/panels/UIProgrammer/UIProgrammer.tscn
@@ -1,17 +1,43 @@
-[gd_scene load_steps=10 format=3 uid="uid://dnyj8vnwwbi2v"]
+[gd_scene load_steps=20 format=3 uid="uid://dnyj8vnwwbi2v"]
[ext_resource type="Theme" uid="uid://cyua45ur0ijqo" path="res://assets/Main.theme" id="1_8omv0"]
[ext_resource type="StyleBox" uid="uid://daxhx5qr5qdeu" path="res://assets/styles/UIPanelBase.tres" id="2_y0ly3"]
[ext_resource type="Script" uid="uid://ckvl7eb5awy56" path="res://panels/UIProgrammer/UIProgrammer.gd" id="3_sp6ey"]
[ext_resource type="PackedScene" uid="uid://dr0kfolsg46yu" path="res://panels/UIPanel/PanelMenuBar.tscn" id="4_rdv1j"]
[ext_resource type="Texture2D" uid="uid://dt0khkr0ltw70" path="res://assets/icons/Programmer.svg" id="5_fc45e"]
+[ext_resource type="Texture2D" uid="uid://c1cl6qetwg8st" path="res://assets/icons/Arrow_back.svg" id="6_sp6ey"]
[ext_resource type="Texture2D" uid="uid://dlikb8or3xepr" path="res://assets/icons/Reset.svg" id="6_xrr4a"]
[ext_resource type="StyleBox" uid="uid://csle20exwqtti" path="res://assets/styles/PanelMenuBar.tres" id="7_7jywt"]
-[ext_resource type="Texture2D" uid="uid://cjx13jidaqjyf" path="res://assets/icons/Scene.svg" id="8_3huj7"]
-
-[sub_resource type="ButtonGroup" id="ButtonGroup_alch7"]
-
-[node name="UIProgrammer" type="PanelContainer" node_paths=PackedStringArray("_dimmer_tab_button", "_color_tab_button", "_gobo_tab_button", "_position_tab_button", "_beam_tab_button", "_focus_tab_button", "_shapers_tab_button", "_control_tab_button", "_parameter_container", "save_to_scene", "zone_select", "edit_controls", "menu_bar")]
+[ext_resource type="Texture2D" uid="uid://kkblmodueva1" path="res://assets/icons/Arrow_foward.svg" id="7_rdv1j"]
+[ext_resource type="Texture2D" uid="uid://bc0acgx0we044" path="res://assets/icons/Zone.svg" id="8_fc45e"]
+[ext_resource type="Texture2D" uid="uid://cfdxexemm7766" path="res://assets/icons/RandomMode.svg" id="9_xrr4a"]
+[ext_resource type="FontFile" uid="uid://crlak6jhg5jy2" path="res://assets/font/RubikMonoOne-Regular.ttf" id="10_ikse1"]
+[ext_resource type="Texture2D" uid="uid://crxgau8v7k3yh" path="res://assets/icons/PlayAlt.svg" id="12_ikse1"]
+[ext_resource type="Texture2D" uid="uid://bf4227ei83gh1" path="res://assets/icons/play.svg" id="13_qpens"]
+[ext_resource type="Texture2D" uid="uid://huxujb0ev1gk" path="res://assets/icons/Loop.svg" id="14_qpens"]
+[ext_resource type="Texture2D" uid="uid://3r1jb0q1v7ri" path="res://assets/icons/AdvancedTime.svg" id="15_qwc1o"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_fc45e"]
+bg_color = Color(0.12941177, 0.12941177, 0.12941177, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.16206557, 0.1620656, 0.16206554, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+
+[sub_resource type="LabelSettings" id="LabelSettings_3huj7"]
+font = ExtResource("10_ikse1")
+font_size = 100
+font_color = Color(0.15, 0.15, 0.15, 1)
+
+[sub_resource type="ButtonGroup" id="ButtonGroup_qwc1o"]
+allow_unpress = true
+
+[node name="UIProgrammer" type="PanelContainer" node_paths=PackedStringArray("tab_button_container", "layer_button_container", "controler_container", "zone_select", "random_mode", "template_placeholder", "page_select_option", "prev_page_button", "next_page_button", "edit_controls", "menu_bar")]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
@@ -20,18 +46,15 @@ grow_vertical = 2
theme = ExtResource("1_8omv0")
theme_override_styles/panel = ExtResource("2_y0ly3")
script = ExtResource("3_sp6ey")
-_dimmer_tab_button = NodePath("VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/TabButtons/HBoxContainer/DimmerTabButton")
-_color_tab_button = NodePath("VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/TabButtons/HBoxContainer/ColorTabButton")
-_gobo_tab_button = NodePath("VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/TabButtons/HBoxContainer/GoboTabButton")
-_position_tab_button = NodePath("VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/TabButtons/HBoxContainer/PositionTabButton")
-_beam_tab_button = NodePath("VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/TabButtons/HBoxContainer/BeamTabButton")
-_focus_tab_button = NodePath("VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/TabButtons/HBoxContainer/FocusTabButton")
-_shapers_tab_button = NodePath("VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/TabButtons/HBoxContainer/ShapersTabButton")
-_control_tab_button = NodePath("VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/TabButtons/HBoxContainer/ControlTabButton")
-_parameter_container = NodePath("VBoxContainer/ScrollContainer/HBoxContainer")
-button_group = SubResource("ButtonGroup_alch7")
-save_to_scene = NodePath("VBoxContainer/PanelContainer2/HBoxContainer/SaveToScene")
-zone_select = NodePath("VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/ViewButtons/HBoxContainer/ZoneSelect")
+tab_button_container = NodePath("VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/TabButtons/TabButtonContainer")
+layer_button_container = NodePath("VBoxContainer/PanelContainer2/HBoxContainer/LayerControls/LayerButtonContainer")
+controler_container = NodePath("VBoxContainer/ScrollContainer/HBoxContainer")
+zone_select = NodePath("VBoxContainer/PanelMenuBar/HBoxContainer/ViewButtons/HBoxContainer/ZoneSelect")
+random_mode = NodePath("VBoxContainer/PanelMenuBar/HBoxContainer/ViewButtons/HBoxContainer/ValueMode")
+template_placeholder = NodePath("VBoxContainer/ScrollContainer/HBoxContainer/TemplatePlaceholder")
+page_select_option = NodePath("VBoxContainer/PanelMenuBar/HBoxContainer/PageButtons/HBoxContainer/PageSelect")
+prev_page_button = NodePath("VBoxContainer/PanelMenuBar/HBoxContainer/PageButtons/HBoxContainer/PageBack")
+next_page_button = NodePath("VBoxContainer/PanelMenuBar/HBoxContainer/PageButtons/HBoxContainer/PageFowards")
edit_controls = NodePath("VBoxContainer/PanelMenuBar/HBoxContainer/EditControls")
menu_bar = NodePath("VBoxContainer/PanelMenuBar")
@@ -45,140 +68,152 @@ layout_mode = 2
text = "Programmer"
icon = ExtResource("5_fc45e")
-[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer" index="0"]
-layout_mode = 2
-size_flags_horizontal = 3
-
-[node name="TabButtons" type="PanelContainer" parent="VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer"]
-layout_mode = 2
-size_flags_horizontal = 6
-
-[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/TabButtons"]
-layout_mode = 2
-
-[node name="DimmerTabButton" type="Button" parent="VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/TabButtons/HBoxContainer"]
-custom_minimum_size = Vector2(80, 0)
+[node name="PageButtons" type="PanelContainer" parent="VBoxContainer/PanelMenuBar/HBoxContainer" index="1"]
layout_mode = 2
-toggle_mode = true
-button_pressed = true
-button_group = SubResource("ButtonGroup_alch7")
-text = "Dimmer"
-[node name="ColorTabButton" type="Button" parent="VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/TabButtons/HBoxContainer"]
-custom_minimum_size = Vector2(80, 0)
+[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/PanelMenuBar/HBoxContainer/PageButtons"]
layout_mode = 2
-disabled = true
-toggle_mode = true
-button_group = SubResource("ButtonGroup_alch7")
-text = "Color"
-[node name="GoboTabButton" type="Button" parent="VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/TabButtons/HBoxContainer"]
-custom_minimum_size = Vector2(80, 0)
+[node name="PageBack" type="Button" parent="VBoxContainer/PanelMenuBar/HBoxContainer/PageButtons/HBoxContainer"]
layout_mode = 2
disabled = true
-toggle_mode = true
-button_group = SubResource("ButtonGroup_alch7")
-text = "Gobo"
-
-[node name="PositionTabButton" type="Button" parent="VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/TabButtons/HBoxContainer"]
-custom_minimum_size = Vector2(80, 0)
-layout_mode = 2
-disabled = true
-toggle_mode = true
-button_group = SubResource("ButtonGroup_alch7")
-text = "Position"
+icon = ExtResource("6_sp6ey")
-[node name="BeamTabButton" type="Button" parent="VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/TabButtons/HBoxContainer"]
-custom_minimum_size = Vector2(80, 0)
+[node name="PageSelect" type="OptionButton" parent="VBoxContainer/PanelMenuBar/HBoxContainer/PageButtons/HBoxContainer"]
+custom_minimum_size = Vector2(51, 0)
layout_mode = 2
-disabled = true
-toggle_mode = true
-button_group = SubResource("ButtonGroup_alch7")
-text = "Beam"
+selected = 0
+item_count = 1
+popup/item_0/text = "0"
+popup/item_0/id = 0
-[node name="FocusTabButton" type="Button" parent="VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/TabButtons/HBoxContainer"]
-custom_minimum_size = Vector2(80, 0)
+[node name="PageFowards" type="Button" parent="VBoxContainer/PanelMenuBar/HBoxContainer/PageButtons/HBoxContainer"]
layout_mode = 2
-disabled = true
-toggle_mode = true
-button_group = SubResource("ButtonGroup_alch7")
-text = "Focus
-"
+icon = ExtResource("7_rdv1j")
-[node name="ShapersTabButton" type="Button" parent="VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/TabButtons/HBoxContainer"]
-custom_minimum_size = Vector2(80, 0)
+[node name="TabButtons" type="PanelContainer" parent="VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer" index="0"]
layout_mode = 2
-disabled = true
-toggle_mode = true
-button_group = SubResource("ButtonGroup_alch7")
-text = "Shapers"
+size_flags_horizontal = 6
-[node name="ControlTabButton" type="Button" parent="VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/TabButtons/HBoxContainer"]
-custom_minimum_size = Vector2(80, 0)
+[node name="TabButtonContainer" type="HBoxContainer" parent="VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/TabButtons"]
layout_mode = 2
-disabled = true
-toggle_mode = true
-button_group = SubResource("ButtonGroup_alch7")
-text = "Control"
-[node name="ViewButtons" type="PanelContainer" parent="VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer"]
+[node name="ViewButtons" type="PanelContainer" parent="VBoxContainer/PanelMenuBar/HBoxContainer" index="3"]
layout_mode = 2
-[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/ViewButtons"]
+[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/PanelMenuBar/HBoxContainer/ViewButtons"]
layout_mode = 2
-[node name="ZoneSelect" type="OptionButton" parent="VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/ViewButtons/HBoxContainer"]
-custom_minimum_size = Vector2(150, 0)
+[node name="ZoneSelect" type="OptionButton" parent="VBoxContainer/PanelMenuBar/HBoxContainer/ViewButtons/HBoxContainer"]
+custom_minimum_size = Vector2(93, 0)
layout_mode = 2
size_flags_horizontal = 10
tooltip_text = "Zone Select"
+clip_text = true
selected = 0
+fit_to_longest_item = false
item_count = 1
-popup/item_0/text = "Zone"
+popup/item_0/text = "root"
+popup/item_0/icon = ExtResource("8_fc45e")
popup/item_0/id = 0
popup/item_0/separator = true
-[node name="RandomMode" type="OptionButton" parent="VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/ViewButtons/HBoxContainer"]
+[node name="ValueMode" type="OptionButton" parent="VBoxContainer/PanelMenuBar/HBoxContainer/ViewButtons/HBoxContainer"]
+visible = false
+custom_minimum_size = Vector2(137, 0)
layout_mode = 2
size_flags_horizontal = 8
tooltip_text = "Random Mode"
+clip_text = true
selected = 0
+fit_to_longest_item = false
item_count = 2
popup/item_0/text = "All"
+popup/item_0/icon = ExtResource("9_xrr4a")
popup/item_0/id = 0
-popup/item_1/text = "Individual"
+popup/item_1/text = "Relative"
+popup/item_1/icon = ExtResource("9_xrr4a")
popup/item_1/id = 1
-[node name="Erace" type="Button" parent="VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/ViewButtons/HBoxContainer"]
-layout_mode = 2
-icon = ExtResource("6_xrr4a")
-
[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/ScrollContainer"]
layout_mode = 2
+size_flags_horizontal = 3
size_flags_vertical = 3
+[node name="TemplatePlaceholder" type="PanelContainer" parent="VBoxContainer/ScrollContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_styles/panel = SubResource("StyleBoxFlat_fc45e")
+
+[node name="Label" type="Label" parent="VBoxContainer/ScrollContainer/HBoxContainer/TemplatePlaceholder"]
+layout_mode = 2
+size_flags_horizontal = 4
+text = "1"
+label_settings = SubResource("LabelSettings_3huj7")
+
[node name="PanelContainer2" type="PanelContainer" parent="VBoxContainer"]
-visible = false
layout_mode = 2
theme_override_styles/panel = ExtResource("7_7jywt")
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/PanelContainer2"]
layout_mode = 2
-[node name="SaveToScene" type="Button" parent="VBoxContainer/PanelContainer2/HBoxContainer"]
+[node name="LayerControls" type="PanelContainer" parent="VBoxContainer/PanelContainer2/HBoxContainer"]
+visible = false
layout_mode = 2
-tooltip_text = "Save To Scene
-"
-disabled = true
-icon = ExtResource("8_3huj7")
-[connection signal="item_selected" from="VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/ViewButtons/HBoxContainer/ZoneSelect" to="." method="_on_zone_select_item_selected"]
-[connection signal="item_selected" from="VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/ViewButtons/HBoxContainer/RandomMode" to="." method="_on_random_mode_item_selected"]
-[connection signal="pressed" from="VBoxContainer/PanelMenuBar/HBoxContainer/ScrollContainer/HBoxContainer/ViewButtons/HBoxContainer/Erace" to="." method="_on_erace_pressed"]
-[connection signal="pressed" from="VBoxContainer/PanelContainer2/HBoxContainer/SaveToScene" to="." method="_on_save_to_scene_pressed"]
+[node name="LayerButtonContainer" type="HBoxContainer" parent="VBoxContainer/PanelContainer2/HBoxContainer/LayerControls"]
+layout_mode = 2
+
+[node name="TransportControls" type="PanelContainer" parent="VBoxContainer/PanelContainer2/HBoxContainer"]
+visible = false
+layout_mode = 2
+size_flags_horizontal = 2
+
+[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/PanelContainer2/HBoxContainer/TransportControls"]
+layout_mode = 2
+
+[node name="PlayBackwards" type="Button" parent="VBoxContainer/PanelContainer2/HBoxContainer/TransportControls/HBoxContainer"]
+layout_mode = 2
+toggle_mode = true
+button_group = SubResource("ButtonGroup_qwc1o")
+icon = ExtResource("12_ikse1")
+
+[node name="PlayFowards" type="Button" parent="VBoxContainer/PanelContainer2/HBoxContainer/TransportControls/HBoxContainer"]
+layout_mode = 2
+toggle_mode = true
+button_group = SubResource("ButtonGroup_qwc1o")
+icon = ExtResource("13_qpens")
+
+[node name="Loop" type="Button" parent="VBoxContainer/PanelContainer2/HBoxContainer/TransportControls/HBoxContainer"]
+layout_mode = 2
+toggle_mode = true
+icon = ExtResource("14_qpens")
+
+[node name="SetSpeed" type="Button" parent="VBoxContainer/PanelContainer2/HBoxContainer/TransportControls/HBoxContainer"]
+layout_mode = 2
+icon = ExtResource("15_qwc1o")
+
+[node name="PanelContainer" type="PanelContainer" parent="VBoxContainer/PanelContainer2/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 10
+
+[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/PanelContainer2/HBoxContainer/PanelContainer"]
+layout_mode = 2
+
+[node name="Clear" type="Button" parent="VBoxContainer/PanelContainer2/HBoxContainer/PanelContainer/HBoxContainer"]
+layout_mode = 2
+icon = ExtResource("6_xrr4a")
+
+[connection signal="pressed" from="VBoxContainer/PanelMenuBar/HBoxContainer/PageButtons/HBoxContainer/PageBack" to="." method="_on_page_back_pressed"]
+[connection signal="item_selected" from="VBoxContainer/PanelMenuBar/HBoxContainer/PageButtons/HBoxContainer/PageSelect" to="." method="set_page"]
+[connection signal="pressed" from="VBoxContainer/PanelMenuBar/HBoxContainer/PageButtons/HBoxContainer/PageFowards" to="." method="_on_page_fowards_pressed"]
+[connection signal="item_selected" from="VBoxContainer/PanelMenuBar/HBoxContainer/ViewButtons/HBoxContainer/ZoneSelect" to="." method="_on_zone_select_item_selected"]
+[connection signal="item_selected" from="VBoxContainer/PanelMenuBar/HBoxContainer/ViewButtons/HBoxContainer/ValueMode" to="." method="_on_random_mode_item_selected"]
+[connection signal="pressed" from="VBoxContainer/PanelContainer2/HBoxContainer/PanelContainer/HBoxContainer/Clear" to="." method="_on_clear_pressed"]
[editable path="VBoxContainer/PanelMenuBar"]
diff --git a/panels/UIVirtualFixtures/FixtureContainer.gd b/panels/UIVirtualFixtures/FixtureContainer.gd
index e185b448..fecd1418 100644
--- a/panels/UIVirtualFixtures/FixtureContainer.gd
+++ b/panels/UIVirtualFixtures/FixtureContainer.gd
@@ -371,8 +371,6 @@ func _update_selection_box() -> void:
_selection_rect.position = Vector2(x, y)
_selection_rect.size = Vector2(w, h)
- var selection_changed: bool = false
-
for fixture: Fixture in _virtual_fixtures.get_left():
var vf: VirtualFixture = _virtual_fixtures.left(fixture)
var just_selected: bool = _selection_rect.intersection(Rect2(vf.position, vf.size)).size != Vector2.ZERO
@@ -380,14 +378,10 @@ func _update_selection_box() -> void:
if just_selected and fixture not in current_selected:
Values.add_to_selection_value("selected_fixtures", [fixture], false)
- selection_changed = true
elif not just_selected and fixture in current_selected and not Input.is_key_pressed(KEY_SHIFT):
Values.remove_from_selection_value("selected_fixtures", [fixture], false)
- selection_changed = true
-
- if selection_changed:
- Values.emit_selection_value("selected_fixtures")
+
#endregion
diff --git a/panels/UIVirtualFixtures/VirtualFixture/VirtualFixture.tscn b/panels/UIVirtualFixtures/VirtualFixture/VirtualFixture.tscn
index 2b1c8470..3e50237f 100644
--- a/panels/UIVirtualFixtures/VirtualFixture/VirtualFixture.tscn
+++ b/panels/UIVirtualFixtures/VirtualFixture/VirtualFixture.tscn
@@ -102,6 +102,7 @@ text = "Name"
label_settings = SubResource("LabelSettings_6gdxf")
horizontal_alignment = 1
vertical_alignment = 1
+autowrap_mode = 3
clip_text = true
[connection signal="gui_input" from="." to="." method="_on_gui_input"]
diff --git a/project.godot b/project.godot
index 03666e9e..61e2d62b 100644
--- a/project.godot
+++ b/project.godot
@@ -125,6 +125,11 @@ command_palette={
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
]
}
+screenshot={
+"deadzone": 0.2,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194343,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
[rendering]
diff --git a/scripts/classes/UIBase.gd b/scripts/classes/UIBase.gd
index 1313e627..2e212f6c 100644
--- a/scripts/classes/UIBase.gd
+++ b/scripts/classes/UIBase.gd
@@ -22,6 +22,18 @@ var _self_class_name: String = "UIBase"
## The name of this UIBase
var _ui_name: String = _self_class_name
+## Stores all queued callables for this frame
+var _queue_order: Array[Callable]
+
+## Stores arguments for each queued callable
+var _queue_args: Dictionary[Callable, Array]
+
+## True if _call_queue has already been call_defered()
+var _queue_call_defered: bool = false
+
+## True if _call_queue is running currently
+var _queue_is_calling: bool = false
+
## Settings for this component
var settings_manager: SettingsManager = SettingsManager.new()
@@ -44,6 +56,29 @@ func uuid() -> String:
return _uuid
+## Queues a callable for execution, will only all it to be called once per frame. Uses call_defered
+func queue(p_callable: Callable, p_args: Array[Variant] = []) -> bool:
+ if _queue_is_calling:
+ p_callable.callv(p_args)
+
+ return true
+
+ elif _queue_args.has(p_callable):
+ _queue_args[p_callable] = p_args
+
+ return false
+
+ else:
+ _queue_order.append(p_callable)
+ _queue_args[p_callable] = p_args
+
+ if not _queue_call_defered:
+ _call_queue.call_deferred()
+ _queue_call_defered = true
+
+ return true
+
+
## Sets the UI name
func set_ui_name(p_ui_name) -> void:
_ui_name = p_ui_name
@@ -71,6 +106,19 @@ func _set_class_name(p_class_name: String) -> void:
_class_tree.append(p_class_name)
+## Calls all the Callables in the queue
+func _call_queue() -> void:
+ _queue_is_calling = true
+
+ for callable: Callable in _queue_order:
+ callable.callv(_queue_args[callable])
+
+ _queue_order.clear()
+ _queue_args.clear()
+ _queue_is_calling = false
+ _queue_call_defered = false
+
+
## Serialize this ClientComponent into a Dictionary
func serialize() -> Dictionary:
return {
diff --git a/scripts/global/Input.gd b/scripts/global/Input.gd
index 4fb1a17d..f1b447be 100644
--- a/scripts/global/Input.gd
+++ b/scripts/global/Input.gd
@@ -28,6 +28,7 @@ var _internal_actions: Dictionary[String, Callable] = {
"store_mode": _handle_store_mode_action,
"ui_cancel": Interface.hide_all_popup_panels,
"command_palette": Interface.toggle_popup_visable.bind(Interface.WindowPopup.COMMAND_PALETTE, self),
+ "screenshot": Interface.take_screenshot,
}
## Allowed input events for shortcuts
diff --git a/scripts/global/Interface.gd b/scripts/global/Interface.gd
index cd5d6a40..faed93fb 100644
--- a/scripts/global/Interface.gd
+++ b/scripts/global/Interface.gd
@@ -718,6 +718,15 @@ func exit_resolve_mode() -> bool:
return true
+## Takes a screenshot of all screens
+func take_screenshot() -> void:
+ for window: UIWindow in _windows.get_right():
+ var file_name: String = "user://" + str(Time.get_datetime_string_from_system())
+
+ window.get_viewport().get_texture().get_image().save_png(file_name)
+ print("Screenshot saved as: ", file_name)
+
+
## Returns the SettingsManager object for ClientInterface
func settings() -> SettingsManager:
return settings_manager
diff --git a/scripts/global/ThemeManager.gd b/scripts/global/ThemeManager.gd
index 4a046018..f9f01a51 100644
--- a/scripts/global/ThemeManager.gd
+++ b/scripts/global/ThemeManager.gd
@@ -31,6 +31,7 @@ class Colors:
static var Caution = Color.YELLOW
static var Error = Color.RED
static var UnsavedData = Color.ORANGE
+ static var ProgrammerOverride = Color("ff8400ff")
static var UIPanelFlashColor = Color.WHITE * 1.5
static var UICorePrimarySideBarTabModulate = Color(0.51, 0.51, 0.51)
@@ -60,6 +61,7 @@ class StyleBoxes:
class Constants:
class Times:
static var InterfaceFadeTime = 0.15
+ static var InterfaceFadeFast = 0.08
static var DeskItemMoveTime = 0.1
static var DeskAreaMoveTime = 0.06
static var SelectBoxMoceTime = 0.06
diff --git a/scripts/global/UIDB.gd b/scripts/global/UIDB.gd
index 09506d72..2895cf40 100644
--- a/scripts/global/UIDB.gd
+++ b/scripts/global/UIDB.gd
@@ -1,4 +1,4 @@
-# Copyright (c) 2025 Liam Sherwin, All rights reserved.
+# Copyright (c) 2026 Liam Sherwin, All rights reserved.
# This file is part of the Spectrum Lighting Engine, licensed under the GPL v3.
class_name ClientUIDB extends Node
@@ -34,7 +34,10 @@ var _panels: Dictionary[String, PackedScene] = {
"UICore": load(_p("UICore")),
"UIDebug": load(_p("UIDebug")),
"UIVirtualFixtures": load(_p("UIVirtualFixtures")),
- "UIProgrammer": load(_p("UIProgrammer"))
+ "UIProgrammer": load(_p("UIProgrammer")),
+ "UIColorPicker": load(_p("UIColorPicker")),
+ "UIClock": load(_p("UIClock")),
+ "UIWindowManager": load(_p("UIWindowManager"))
}
@@ -62,6 +65,7 @@ var _data_inputs: Dictionary[Data.Type, PackedScene] = {
Data.Type.FLOAT: load(_d("DataInputFloat")),
Data.Type.VECTOR2: load(_d("DataInputVector2")),
Data.Type.VECTOR2I: load(_d("DataInputVector2")),
+ Data.Type.COLOR: load(_d("DataInputColor")),
Data.Type.ENUM: load(_d("DataInputEnum")),
Data.Type.BITFLAGS: load(_d("DataInputBitFlags")),
Data.Type.NAME: load(_d("DataInputString")),
@@ -78,14 +82,18 @@ var _data_inputs: Dictionary[Data.Type, PackedScene] = {
## All UIPanels sorted by category
var _panels_by_category: Dictionary[String, Array] = {
- "Panels": [
- "UIDesk",
- "UIPlaybacks",
+ "System": [
"UISettings",
"UISaveLoad",
- "UIDebug",
"UICore",
- "UIProgrammer"
+ "UIWindowManager",
+ ],
+ "Playbacks": [
+ "UIPlaybacks",
+ ],
+ "Programming": [
+ "UIProgrammer",
+ "UIColorPicker"
],
"Components": [
"UIUniverses",
@@ -94,7 +102,12 @@ var _panels_by_category: Dictionary[String, Array] = {
"UIFixtureGroups"
],
"Views": [
- "UIVirtualFixtures"
+ "UIVirtualFixtures",
+ "UIDesk",
+ ],
+ "Utils": [
+ "UIClock",
+ "UIDebug",
]
}
diff --git a/scripts/global/Values.gd b/scripts/global/Values.gd
index c2661985..d17d5273 100644
--- a/scripts/global/Values.gd
+++ b/scripts/global/Values.gd
@@ -78,13 +78,13 @@ func set_selection_value(value_name: String, value: Array, no_signal: bool = fal
selection_values[value_name] = value
if not no_signal:
- emit_signal(value_name + "_selection_value_callback", selection_values[value_name])
+ emit_signal(value_name + "_selection_value_callback", selection_values[value_name].duplicate())
## Emits a selection value signal
func emit_selection_value(value_name: String) -> void:
if value_name in selection_values:
- emit_signal(value_name + "_selection_value_callback", selection_values[value_name])
+ emit_signal(value_name + "_selection_value_callback", selection_values[value_name].duplicate())
## Add an array of items to a selection value