summaryrefslogtreecommitdiff
path: root/addons/gdUnit4/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'addons/gdUnit4/src/ui')
-rw-r--r--addons/gdUnit4/src/ui/GdUnitConsole.gd91
-rw-r--r--addons/gdUnit4/src/ui/GdUnitConsole.gd.uid1
-rw-r--r--addons/gdUnit4/src/ui/GdUnitConsole.tscn64
-rw-r--r--addons/gdUnit4/src/ui/GdUnitFonts.gd36
-rw-r--r--addons/gdUnit4/src/ui/GdUnitFonts.gd.uid1
-rw-r--r--addons/gdUnit4/src/ui/GdUnitInspector.gd34
-rw-r--r--addons/gdUnit4/src/ui/GdUnitInspector.gd.uid1
-rw-r--r--addons/gdUnit4/src/ui/GdUnitInspector.tscn70
-rw-r--r--addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd31
-rw-r--r--addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd.uid1
-rw-r--r--addons/gdUnit4/src/ui/GdUnitUiTools.gd151
-rw-r--r--addons/gdUnit4/src/ui/GdUnitUiTools.gd.uid1
-rw-r--r--addons/gdUnit4/src/ui/ScriptEditorControls.gd100
-rw-r--r--addons/gdUnit4/src/ui/ScriptEditorControls.gd.uid1
-rw-r--r--addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd45
-rw-r--r--addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd.uid1
-rw-r--r--addons/gdUnit4/src/ui/menu/GdUnitEditorFileSystemContextMenuHandler.gd37
-rw-r--r--addons/gdUnit4/src/ui/menu/GdUnitEditorFileSystemContextMenuHandler.gd.uid1
-rw-r--r--addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.gd63
-rw-r--r--addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.gd.uid1
-rw-r--r--addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.tscn70
-rw-r--r--addons/gdUnit4/src/ui/menu/GdUnitScriptEditorContextMenuHandler.gd41
-rw-r--r--addons/gdUnit4/src/ui/menu/GdUnitScriptEditorContextMenuHandler.gd.uid1
-rw-r--r--addons/gdUnit4/src/ui/parts/InspectorMonitor.gd54
-rw-r--r--addons/gdUnit4/src/ui/parts/InspectorMonitor.gd.uid1
-rw-r--r--addons/gdUnit4/src/ui/parts/InspectorMonitor.tscn94
-rw-r--r--addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd49
-rw-r--r--addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd.uid1
-rw-r--r--addons/gdUnit4/src/ui/parts/InspectorProgressBar.tscn33
-rw-r--r--addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd209
-rw-r--r--addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd.uid1
-rw-r--r--addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn465
-rw-r--r--addons/gdUnit4/src/ui/parts/InspectorToolBar.gd104
-rw-r--r--addons/gdUnit4/src/ui/parts/InspectorToolBar.gd.uid1
-rw-r--r--addons/gdUnit4/src/ui/parts/InspectorToolBar.tscn202
-rw-r--r--addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd1210
-rw-r--r--addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd.uid1
-rw-r--r--addons/gdUnit4/src/ui/parts/InspectorTreePanel.tscn240
-rw-r--r--addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd56
-rw-r--r--addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd.uid1
-rw-r--r--addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn36
-rw-r--r--addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd327
-rw-r--r--addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd.uid1
-rw-r--r--addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn349
-rw-r--r--addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd255
-rw-r--r--addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd.uid1
-rw-r--r--addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.tscn148
-rw-r--r--addons/gdUnit4/src/ui/settings/logo.pngbin0 -> 49775 bytes
-rw-r--r--addons/gdUnit4/src/ui/settings/logo.png.import40
-rw-r--r--addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd129
-rw-r--r--addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd.uid1
-rw-r--r--addons/gdUnit4/src/ui/templates/TestSuiteTemplate.tscn127
52 files changed, 4979 insertions, 0 deletions
diff --git a/addons/gdUnit4/src/ui/GdUnitConsole.gd b/addons/gdUnit4/src/ui/GdUnitConsole.gd
new file mode 100644
index 0000000..eb1e625
--- /dev/null
+++ b/addons/gdUnit4/src/ui/GdUnitConsole.gd
@@ -0,0 +1,91 @@
+@tool
+extends Control
+
+const GdUnitUpdateClient = preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd")
+const TITLE = "gdUnit4 ${version} Console"
+
+@onready var header := $VBoxContainer/Header
+@onready var title: RichTextLabel = $VBoxContainer/Header/header_title
+@onready var output: RichTextLabel = $VBoxContainer/Console/TextEdit
+
+
+var _test_reporter: GdUnitConsoleTestReporter
+
+
+@warning_ignore("return_value_discarded")
+func _ready() -> void:
+ GdUnitFonts.init_fonts(output)
+ GdUnit4Version.init_version_label(title)
+ GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event)
+ GdUnitSignals.instance().gdunit_message.connect(_on_gdunit_message)
+ GdUnitSignals.instance().gdunit_client_connected.connect(_on_gdunit_client_connected)
+ GdUnitSignals.instance().gdunit_client_disconnected.connect(_on_gdunit_client_disconnected)
+ _test_reporter = GdUnitConsoleTestReporter.new(GdUnitRichTextMessageWriter.new(output))
+
+
+func _notification(what: int) -> void:
+ if what == EditorSettings.NOTIFICATION_EDITOR_SETTINGS_CHANGED:
+ _test_reporter.init_colors()
+ if what == NOTIFICATION_PREDELETE:
+ var instance := GdUnitSignals.instance()
+ if instance.gdunit_event.is_connected(_on_gdunit_event):
+ instance.gdunit_event.disconnect(_on_gdunit_event)
+ if instance.gdunit_message.is_connected(_on_gdunit_event):
+ instance.gdunit_message.disconnect(_on_gdunit_message)
+ if instance.gdunit_client_connected.is_connected(_on_gdunit_event):
+ instance.gdunit_client_connected.disconnect(_on_gdunit_client_connected)
+ if instance.gdunit_client_disconnected.is_connected(_on_gdunit_event):
+ instance.gdunit_client_disconnected.disconnect(_on_gdunit_client_disconnected)
+
+
+func setup_update_notification(control: Button) -> void:
+ if not GdUnitSettings.is_update_notification_enabled():
+ _test_reporter.println_message("The search for updates is deactivated.", Color.CORNFLOWER_BLUE)
+ return
+
+ _test_reporter.print_message("Searching for updates... ", Color.CORNFLOWER_BLUE)
+ var update_client := GdUnitUpdateClient.new()
+ add_child(update_client)
+ var response :GdUnitUpdateClient.HttpResponse = await update_client.request_latest_version()
+ if response.status() != 200:
+ _test_reporter.println_message("Information cannot be retrieved from GitHub!", Color.INDIAN_RED)
+ _test_reporter.println_message("Error: %s" % response.response(), Color.INDIAN_RED)
+ return
+ var latest_version := update_client.extract_latest_version(response)
+ if not latest_version.is_greater(GdUnit4Version.current()):
+ _test_reporter.println_message("GdUnit4 is up-to-date.", Color.FOREST_GREEN)
+ return
+
+ _test_reporter.println_message("A new update is available %s" % latest_version, Color.YELLOW)
+ _test_reporter.println_message("Open the GdUnit4 settings and check the update tab.", Color.YELLOW)
+
+ control.icon = GdUnitUiTools.get_icon("Notification", Color.YELLOW)
+ var tween := create_tween()
+ tween.tween_property(control, "self_modulate", Color.VIOLET, .2).set_trans(Tween.TransitionType.TRANS_LINEAR)
+ tween.tween_property(control, "self_modulate", Color.YELLOW, .2).set_trans(Tween.TransitionType.TRANS_BOUNCE)
+ tween.parallel()
+ tween.tween_property(control, "scale", Vector2.ONE*1.05, .4).set_trans(Tween.TransitionType.TRANS_LINEAR)
+ tween.tween_property(control, "scale", Vector2.ONE, .4).set_trans(Tween.TransitionType.TRANS_BOUNCE)
+ tween.set_loops(-1)
+ tween.play()
+
+
+func _on_gdunit_event(event: GdUnitEvent) -> void:
+ match event.type():
+ GdUnitEvent.SESSION_START:
+ _test_reporter.test_session = GdUnitTestSession.new(GdUnitTestDiscoverGuard.instance().get_discovered_tests(), "")
+ GdUnitEvent.SESSION_CLOSE:
+ _test_reporter.test_session = null
+
+
+func _on_gdunit_client_connected(client_id: int) -> void:
+ _test_reporter.clear()
+ _test_reporter.println_message("GdUnit Test Client connected with id: %d" % client_id, Color.hex(0x9887c4))
+
+
+func _on_gdunit_client_disconnected(client_id: int) -> void:
+ _test_reporter.println_message("GdUnit Test Client disconnected with id: %d" % client_id, Color.hex(0x9887c4))
+
+
+func _on_gdunit_message(message: String) -> void:
+ _test_reporter.println_message(message, Color.CORNFLOWER_BLUE)
diff --git a/addons/gdUnit4/src/ui/GdUnitConsole.gd.uid b/addons/gdUnit4/src/ui/GdUnitConsole.gd.uid
new file mode 100644
index 0000000..f3327c6
--- /dev/null
+++ b/addons/gdUnit4/src/ui/GdUnitConsole.gd.uid
@@ -0,0 +1 @@
+uid://yk3dmu2vt5w2
diff --git a/addons/gdUnit4/src/ui/GdUnitConsole.tscn b/addons/gdUnit4/src/ui/GdUnitConsole.tscn
new file mode 100644
index 0000000..25f45b5
--- /dev/null
+++ b/addons/gdUnit4/src/ui/GdUnitConsole.tscn
@@ -0,0 +1,64 @@
+[gd_scene format=3 uid="uid://dm0wvfyeew7vd"]
+
+[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/GdUnitConsole.gd" id="1"]
+
+[node name="Control" type="Control"]
+use_parent_material = true
+clip_contents = true
+custom_minimum_size = Vector2(0, 200)
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+script = ExtResource("1")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+use_parent_material = true
+clip_contents = true
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="Header" type="PanelContainer" parent="VBoxContainer"]
+auto_translate_mode = 2
+custom_minimum_size = Vector2(0, 32)
+layout_mode = 2
+localize_numeral_system = false
+mouse_filter = 2
+
+[node name="header_title" type="RichTextLabel" parent="VBoxContainer/Header"]
+auto_translate_mode = 2
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+localize_numeral_system = false
+mouse_filter = 2
+bbcode_enabled = true
+scroll_active = false
+autowrap_mode = 0
+shortcut_keys_enabled = false
+
+[node name="Console" type="ScrollContainer" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="TextEdit" type="RichTextLabel" parent="VBoxContainer/Console"]
+use_parent_material = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+focus_mode = 2
+bbcode_enabled = true
+scroll_following = true
+context_menu_enabled = true
+selection_enabled = true
diff --git a/addons/gdUnit4/src/ui/GdUnitFonts.gd b/addons/gdUnit4/src/ui/GdUnitFonts.gd
new file mode 100644
index 0000000..0cb0f5d
--- /dev/null
+++ b/addons/gdUnit4/src/ui/GdUnitFonts.gd
@@ -0,0 +1,36 @@
+@tool
+class_name GdUnitFonts
+extends RefCounted
+
+
+static func init_fonts(item: CanvasItem) -> float:
+ # set default size
+ item.set("theme_override_font_sizes/font_size", 16)
+
+ if Engine.is_editor_hint():
+ var base_control := EditorInterface.get_base_control()
+ # source modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs
+ # https://github.com/godotengine/godot/blob/9ee1873ae1e09c217ac24a5800007f63cb895615/editor/editor_log.cpp#L65
+ var output_source_mono := base_control.get_theme_font("output_source_mono", "EditorFonts")
+ var output_source_bold_italic := base_control.get_theme_font("output_source_bold_italic", "EditorFonts")
+ var output_source_italic := base_control.get_theme_font("output_source_italic", "EditorFonts")
+ var output_source_bold := base_control.get_theme_font("output_source_bold", "EditorFonts")
+ var output_source := base_control.get_theme_font("output_source", "EditorFonts")
+ var settings := EditorInterface.get_editor_settings()
+ var scale_factor := EditorInterface.get_editor_scale()
+ var font_size: float = settings.get_setting("interface/editor/main_font_size")
+
+ font_size *= scale_factor
+ item.set("theme_override_fonts/normal_font", output_source)
+ item.set("theme_override_fonts/bold_font", output_source_bold)
+ item.set("theme_override_fonts/italics_font", output_source_italic)
+ item.set("theme_override_fonts/bold_italics_font", output_source_bold_italic)
+ item.set("theme_override_fonts/mono_font", output_source_mono)
+ item.set("theme_override_font_sizes/font_size", font_size)
+ item.set("theme_override_font_sizes/normal_font_size", font_size)
+ item.set("theme_override_font_sizes/bold_font_size", font_size)
+ item.set("theme_override_font_sizes/italics_font_size", font_size)
+ item.set("theme_override_font_sizes/bold_italics_font_size", font_size)
+ item.set("theme_override_font_sizes/mono_font_size", font_size)
+ return font_size
+ return 16.0
diff --git a/addons/gdUnit4/src/ui/GdUnitFonts.gd.uid b/addons/gdUnit4/src/ui/GdUnitFonts.gd.uid
new file mode 100644
index 0000000..f94bc32
--- /dev/null
+++ b/addons/gdUnit4/src/ui/GdUnitFonts.gd.uid
@@ -0,0 +1 @@
+uid://cxol0xus2k25t
diff --git a/addons/gdUnit4/src/ui/GdUnitInspector.gd b/addons/gdUnit4/src/ui/GdUnitInspector.gd
new file mode 100644
index 0000000..81d896f
--- /dev/null
+++ b/addons/gdUnit4/src/ui/GdUnitInspector.gd
@@ -0,0 +1,34 @@
+@tool
+class_name GdUnitInspecor
+extends Panel
+
+
+var _command_handler := GdUnitCommandHandler.instance()
+var _wait_time := 0.0
+
+
+func _ready() -> void:
+ @warning_ignore("return_value_discarded")
+ GdUnitSignals.instance().gdunit_event.connect(func(event: GdUnitEvent) -> void:
+ if event.type() != GdUnitEvent.SESSION_START:
+ return
+
+ var control: Control = get_parent_control()
+ # if the tab is floating we dont need to set as current
+ if control is TabContainer:
+ var tab_container :TabContainer = control
+ for tab_index in tab_container.get_tab_count():
+ if tab_container.get_tab_title(tab_index) == "GdUnit":
+ tab_container.set_current_tab(tab_index)
+ )
+
+ # propagete the test_counters_changed signal to the progress bar
+ @warning_ignore("unsafe_property_access", "unsafe_method_access")
+ %MainPanel.test_counters_changed.connect(%ProgressBar._on_test_counter_changed)
+
+
+func _process(delta: float) -> void:
+ _wait_time += delta
+ if _wait_time > 5.0:
+ _wait_time = 0
+ _command_handler._do_process()
diff --git a/addons/gdUnit4/src/ui/GdUnitInspector.gd.uid b/addons/gdUnit4/src/ui/GdUnitInspector.gd.uid
new file mode 100644
index 0000000..8f4c2ab
--- /dev/null
+++ b/addons/gdUnit4/src/ui/GdUnitInspector.gd.uid
@@ -0,0 +1 @@
+uid://b26rlm46xtlm6
diff --git a/addons/gdUnit4/src/ui/GdUnitInspector.tscn b/addons/gdUnit4/src/ui/GdUnitInspector.tscn
new file mode 100644
index 0000000..cbaf7c9
--- /dev/null
+++ b/addons/gdUnit4/src/ui/GdUnitInspector.tscn
@@ -0,0 +1,70 @@
+[gd_scene format=3 uid="uid://mpo5o6d4uybu"]
+
+[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/InspectorToolBar.tscn" id="1"]
+[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/InspectorProgressBar.tscn" id="2"]
+[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn" id="3"]
+[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/InspectorMonitor.tscn" id="4"]
+[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/GdUnitInspector.gd" id="5"]
+[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/InspectorTreePanel.tscn" id="7"]
+[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/network/GdUnitServer.tscn" id="7_721no"]
+
+[node name="GdUnit" type="Panel"]
+use_parent_material = true
+clip_contents = true
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+size_flags_horizontal = 11
+size_flags_vertical = 3
+focus_mode = 2
+script = ExtResource("5")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+use_parent_material = true
+clip_contents = true
+layout_mode = 0
+anchor_right = 1.0
+anchor_bottom = 1.0
+size_flags_vertical = 11
+theme_override_constants/separation = 0
+
+[node name="Header" type="VBoxContainer" parent="VBoxContainer"]
+use_parent_material = true
+clip_contents = true
+layout_mode = 2
+size_flags_horizontal = 9
+size_flags_vertical = 0
+
+[node name="ToolBar" parent="VBoxContainer/Header" instance=ExtResource("1")]
+layout_mode = 2
+size_flags_vertical = 1
+
+[node name="ProgressBar" parent="VBoxContainer/Header" instance=ExtResource("2")]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 5
+max_value = 0.0
+
+[node name="StatusBar" parent="VBoxContainer/Header" instance=ExtResource("3")]
+layout_mode = 2
+size_flags_horizontal = 11
+
+[node name="MainPanel" parent="VBoxContainer" instance=ExtResource("7")]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="Monitor" parent="VBoxContainer" instance=ExtResource("4")]
+layout_mode = 2
+
+[node name="event_server" parent="." instance=ExtResource("7_721no")]
+
+[connection signal="select_error_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [7]]
+[connection signal="select_error_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [7]]
+[connection signal="select_failure_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [6]]
+[connection signal="select_failure_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [6]]
+[connection signal="select_flaky_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [5]]
+[connection signal="select_flaky_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [5]]
+[connection signal="select_skipped_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [2]]
+[connection signal="select_skipped_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [2]]
+[connection signal="tree_view_mode_changed" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_status_bar_tree_view_mode_changed"]
+[connection signal="jump_to_orphan_nodes" from="VBoxContainer/Monitor" to="VBoxContainer/MainPanel" method="select_first_orphan"]
diff --git a/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd b/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd
new file mode 100644
index 0000000..8dd0265
--- /dev/null
+++ b/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd
@@ -0,0 +1,31 @@
+class_name GdUnitInspectorTreeConstants
+extends RefCounted
+
+
+# the inspector panel presantation
+enum TREE_VIEW_MODE {
+ TREE,
+ FLAT
+}
+
+
+# The inspector sort modes
+enum SORT_MODE {
+ UNSORTED,
+ NAME_ASCENDING,
+ NAME_DESCENDING,
+ EXECUTION_TIME
+}
+
+
+enum STATE {
+ INITIAL,
+ RUNNING,
+ SKIPPED,
+ SUCCESS,
+ WARNING,
+ FLAKY,
+ FAILED,
+ ERROR,
+ ABORDED,
+}
diff --git a/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd.uid b/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd.uid
new file mode 100644
index 0000000..463a301
--- /dev/null
+++ b/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd.uid
@@ -0,0 +1 @@
+uid://bfaaukho0a14
diff --git a/addons/gdUnit4/src/ui/GdUnitUiTools.gd b/addons/gdUnit4/src/ui/GdUnitUiTools.gd
new file mode 100644
index 0000000..0bcdb1d
--- /dev/null
+++ b/addons/gdUnit4/src/ui/GdUnitUiTools.gd
@@ -0,0 +1,151 @@
+class_name GdUnitUiTools
+extends RefCounted
+
+
+static var _spinner: AnimatedTexture
+
+
+enum ImageFlipMode {
+ HORIZONTAl,
+ VERITCAL
+}
+
+
+## Returns the icon by name, if it exists.
+static func get_icon(icon_name: String, color: = Color.BLACK) -> Texture2D:
+ if not Engine.is_editor_hint():
+ return null
+ var icon := EditorInterface.get_base_control().get_theme_icon(icon_name, "EditorIcons")
+ if icon == null:
+ return null
+ if color != Color.BLACK:
+ icon = _modulate_texture(icon, color)
+ return icon
+
+
+## Returns the icon flipped
+static func get_flipped_icon(icon_name: String, mode: = ImageFlipMode.HORIZONTAl) -> Texture2D:
+ if not Engine.is_editor_hint():
+ return null
+ var icon := EditorInterface.get_base_control().get_theme_icon(icon_name, "EditorIcons")
+ if icon == null:
+ return null
+ return ImageTexture.create_from_image(_flip_image(icon, mode))
+
+
+static func get_spinner() -> AnimatedTexture:
+ if _spinner != null:
+ return _spinner
+ _spinner = AnimatedTexture.new()
+ _spinner.frames = 8
+ _spinner.speed_scale = 2.5
+ for frame in _spinner.frames:
+ _spinner.set_frame_texture(frame, get_icon("Progress%d" % (frame+1)))
+ _spinner.set_frame_duration(frame, 0.2)
+ return _spinner
+
+
+static func get_color_animated_icon(icon_name :String, from :Color, to :Color) -> AnimatedTexture:
+ if not Engine.is_editor_hint():
+ return null
+ var texture := AnimatedTexture.new()
+ texture.frames = 8
+ texture.speed_scale = 2.5
+ var color := from
+ for frame in texture.frames:
+ color = lerp(color, to, .2)
+ texture.set_frame_texture(frame, get_icon(icon_name, color))
+ texture.set_frame_duration(frame, 0.2)
+ return texture
+
+
+static func get_run_overall_icon() -> Texture2D:
+ if not Engine.is_editor_hint():
+ return null
+ var icon := EditorInterface.get_base_control().get_theme_icon("Play", "EditorIcons")
+ var image := _merge_images(icon.get_image(), Vector2i(-2, 0), icon.get_image(), Vector2i(3, 0))
+ return ImageTexture.create_from_image(image)
+
+
+static func get_GDScript_icon(status: String, color: Color) -> Texture2D:
+ if not Engine.is_editor_hint():
+ return null
+ var icon_a := EditorInterface.get_base_control().get_theme_icon("GDScript", "EditorIcons")
+ var icon_b := EditorInterface.get_base_control().get_theme_icon(status, "EditorIcons")
+ var overlay_image := _modulate_image(icon_b.get_image(), color)
+ var image := _merge_images_scaled(icon_a.get_image(), Vector2i(0, 0), overlay_image, Vector2i(5, 5))
+ return ImageTexture.create_from_image(image)
+
+
+static func get_CSharpScript_icon(status: String, color: Color) -> Texture2D:
+ if not Engine.is_editor_hint():
+ return null
+ var icon_a := EditorInterface.get_base_control().get_theme_icon("CSharpScript", "EditorIcons")
+ var icon_b := EditorInterface.get_base_control().get_theme_icon(status, "EditorIcons")
+ var overlay_image := _modulate_image(icon_b.get_image(), color)
+ var image := _merge_images_scaled(icon_a.get_image(), Vector2i(0, 0), overlay_image, Vector2i(5, 5))
+ return ImageTexture.create_from_image(image)
+
+
+static func _modulate_texture(texture: Texture2D, color: Color) -> Texture2D:
+ var image := _modulate_image(texture.get_image(), color)
+ return ImageTexture.create_from_image(image)
+
+
+static func _modulate_image(image: Image, color: Color) -> Image:
+ var data: PackedByteArray = image.data["data"]
+ for pixel in range(0, data.size(), 4):
+ var pixel_a := _to_color(data, pixel)
+ if pixel_a.a8 != 0:
+ pixel_a = pixel_a.lerp(color, .9)
+ data[pixel + 0] = pixel_a.r8
+ data[pixel + 1] = pixel_a.g8
+ data[pixel + 2] = pixel_a.b8
+ data[pixel + 3] = pixel_a.a8
+ var output_image := Image.new()
+ output_image.set_data(image.get_width(), image.get_height(), image.has_mipmaps(), image.get_format(), data)
+ return output_image
+
+
+static func _merge_images(image1: Image, offset1: Vector2i, image2: Image, offset2: Vector2i) -> Image:
+ ## we need to fix the image to have the same size to avoid merge conflicts
+ if image1.get_height() < image2.get_height():
+ image1.resize(image2.get_width(), image2.get_height())
+ # Create a new Image for the merged result
+ var merged_image := Image.create(image1.get_width(), image1.get_height(), false, Image.FORMAT_RGBA8)
+ merged_image.blit_rect_mask(image1, image2, Rect2(Vector2.ZERO, image1.get_size()), offset1)
+ merged_image.blit_rect_mask(image1, image2, Rect2(Vector2.ZERO, image2.get_size()), offset2)
+ return merged_image
+
+
+@warning_ignore("narrowing_conversion")
+static func _merge_images_scaled(image1: Image, offset1: Vector2i, image2: Image, offset2: Vector2i) -> Image:
+ ## we need to fix the image to have the same size to avoid merge conflicts
+ if image1.get_height() < image2.get_height():
+ image1.resize(image2.get_width(), image2.get_height())
+ # Create a new Image for the merged result
+ var merged_image := Image.create(image1.get_width(), image1.get_height(), false, image1.get_format())
+ merged_image.blend_rect(image1, Rect2(Vector2.ZERO, image1.get_size()), offset1)
+ @warning_ignore("narrowing_conversion")
+ image2.resize(image2.get_width()/1.3, image2.get_height()/1.3)
+ merged_image.blend_rect(image2, Rect2(Vector2.ZERO, image2.get_size()), offset2)
+ return merged_image
+
+
+static func _flip_image(texture: Texture2D, mode: ImageFlipMode) -> Image:
+ var flipped_image := Image.new()
+ flipped_image.copy_from(texture.get_image())
+ if mode == ImageFlipMode.VERITCAL:
+ flipped_image.flip_x()
+ else:
+ flipped_image.flip_y()
+ return flipped_image
+
+
+static func _to_color(data: PackedByteArray, position: int) -> Color:
+ var pixel_a := Color()
+ pixel_a.r8 = data[position + 0]
+ pixel_a.g8 = data[position + 1]
+ pixel_a.b8 = data[position + 2]
+ pixel_a.a8 = data[position + 3]
+ return pixel_a
diff --git a/addons/gdUnit4/src/ui/GdUnitUiTools.gd.uid b/addons/gdUnit4/src/ui/GdUnitUiTools.gd.uid
new file mode 100644
index 0000000..4d4f2be
--- /dev/null
+++ b/addons/gdUnit4/src/ui/GdUnitUiTools.gd.uid
@@ -0,0 +1 @@
+uid://cv34ysw4mo25c
diff --git a/addons/gdUnit4/src/ui/ScriptEditorControls.gd b/addons/gdUnit4/src/ui/ScriptEditorControls.gd
new file mode 100644
index 0000000..5e07fe1
--- /dev/null
+++ b/addons/gdUnit4/src/ui/ScriptEditorControls.gd
@@ -0,0 +1,100 @@
+# A tool to provide extended script editor functionallity
+class_name ScriptEditorControls
+extends RefCounted
+
+# https://github.com/godotengine/godot/blob/master/editor/plugins/script_editor_plugin.h
+# the Editor menu popup items
+enum {
+ FILE_NEW,
+ FILE_NEW_TEXTFILE,
+ FILE_OPEN,
+ FILE_REOPEN_CLOSED,
+ FILE_OPEN_RECENT,
+ FILE_SAVE,
+ FILE_SAVE_AS,
+ FILE_SAVE_ALL,
+ FILE_THEME,
+ FILE_RUN,
+ FILE_CLOSE,
+ CLOSE_DOCS,
+ CLOSE_ALL,
+ CLOSE_OTHER_TABS,
+ TOGGLE_SCRIPTS_PANEL,
+ SHOW_IN_FILE_SYSTEM,
+ FILE_COPY_PATH,
+ FILE_TOOL_RELOAD_SOFT,
+ SEARCH_IN_FILES,
+ REPLACE_IN_FILES,
+ SEARCH_HELP,
+ SEARCH_WEBSITE,
+ HELP_SEARCH_FIND,
+ HELP_SEARCH_FIND_NEXT,
+ HELP_SEARCH_FIND_PREVIOUS,
+ WINDOW_MOVE_UP,
+ WINDOW_MOVE_DOWN,
+ WINDOW_NEXT,
+ WINDOW_PREV,
+ WINDOW_SORT,
+ WINDOW_SELECT_BASE = 100
+}
+
+
+# Saves the given script and closes if requested by <close=true>
+# The script is saved when is opened in the editor.
+# The script is closed when <close> is set to true.
+static func save_an_open_script(script_path: String, close:=false) -> bool:
+ #prints("save_an_open_script", script_path, close)
+ if !Engine.is_editor_hint():
+ return false
+ var editor := EditorInterface.get_script_editor()
+ var editor_popup := _menu_popup()
+ # search for the script in all opened editor scrips
+ for open_script in editor.get_open_scripts():
+ if open_script.resource_path == script_path:
+ # select the script in the editor
+ EditorInterface.edit_script(open_script, 0);
+ # save and close
+ editor_popup.id_pressed.emit(FILE_SAVE)
+ if close:
+ editor_popup.id_pressed.emit(FILE_CLOSE)
+ return true
+ return false
+
+
+# Saves all opened script
+static func save_all_open_script() -> void:
+ if Engine.is_editor_hint():
+ _menu_popup().id_pressed.emit(FILE_SAVE_ALL)
+
+
+static func close_open_editor_scripts() -> void:
+ if Engine.is_editor_hint():
+ _menu_popup().id_pressed.emit(CLOSE_ALL)
+
+
+# Edits the given script.
+# The script is openend in the current editor and selected in the file system dock.
+# The line and column on which to open the script can also be specified.
+# The script will be open with the user-configured editor for the script's language which may be an external editor.
+static func edit_script(script_path: String, line_number := -1) -> void:
+ var file_system := EditorInterface.get_resource_filesystem()
+ file_system.update_file(script_path)
+ var file_system_dock := EditorInterface.get_file_system_dock()
+ file_system_dock.navigate_to_path(script_path)
+ EditorInterface.select_file(script_path)
+ var script: GDScript = load(script_path)
+ EditorInterface.edit_script(script, line_number)
+
+
+static func _menu_popup() -> PopupMenu:
+ @warning_ignore("unsafe_method_access")
+ return EditorInterface.get_script_editor().get_child(0).get_child(0).get_child(0).get_popup()
+
+
+static func _print_menu(popup: PopupMenu) -> void:
+ for itemIndex in popup.item_count:
+ prints("get_item_id", popup.get_item_id(itemIndex))
+ prints("get_item_accelerator", popup.get_item_accelerator(itemIndex))
+ prints("get_item_shortcut", popup.get_item_shortcut(itemIndex))
+ prints("get_item_text", popup.get_item_text(itemIndex))
+ prints()
diff --git a/addons/gdUnit4/src/ui/ScriptEditorControls.gd.uid b/addons/gdUnit4/src/ui/ScriptEditorControls.gd.uid
new file mode 100644
index 0000000..d9eeb69
--- /dev/null
+++ b/addons/gdUnit4/src/ui/ScriptEditorControls.gd.uid
@@ -0,0 +1 @@
+uid://bp8qk8bp1l8gm
diff --git a/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd b/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd
new file mode 100644
index 0000000..3849e31
--- /dev/null
+++ b/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd
@@ -0,0 +1,45 @@
+class_name GdUnitContextMenuItem
+
+
+var command_id: String:
+ set(value):
+ command_id = value
+ get:
+ return command_id
+
+var name: StringName:
+ set(value):
+ name = value
+ get:
+ return name
+
+var visible: Callable:
+ set(value):
+ visible = value
+ get:
+ return visible
+
+var icon: Texture2D:
+ get:
+ return GdUnitCommandHandler.instance().command_icon(command_id)
+
+
+func _init(p_command_id: String, p_name: StringName, p_is_visible: Callable) -> void:
+ assert(p_command_id != null and not p_command_id.is_empty(), "(%s) missing command id " % p_command_id)
+ assert(p_is_visible != null, "(%s) missing parameter 'GdUnitCommand'" % p_name)
+
+ self.command_id = p_command_id
+ self.name = p_name
+ self.visible = p_is_visible
+
+
+func shortcut() -> Shortcut:
+ return GdUnitCommandHandler.instance().command_shortcut(command_id)
+
+
+func is_visible(...args: Array) -> bool:
+ return visible.callv(args)
+
+
+func execute(...args: Array) -> void:
+ GdUnitCommandHandler.instance().command_execute(command_id, args)
diff --git a/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd.uid b/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd.uid
new file mode 100644
index 0000000..03b589b
--- /dev/null
+++ b/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd.uid
@@ -0,0 +1 @@
+uid://7xcqjry47b4y
diff --git a/addons/gdUnit4/src/ui/menu/GdUnitEditorFileSystemContextMenuHandler.gd b/addons/gdUnit4/src/ui/menu/GdUnitEditorFileSystemContextMenuHandler.gd
new file mode 100644
index 0000000..0c92b92
--- /dev/null
+++ b/addons/gdUnit4/src/ui/menu/GdUnitEditorFileSystemContextMenuHandler.gd
@@ -0,0 +1,37 @@
+@tool
+extends EditorContextMenuPlugin
+
+var _context_menus: Array[GdUnitContextMenuItem] = []
+
+
+func _init() -> void:
+ var is_test_suite := func is_visible(script: Script, is_ts: bool) -> bool:
+ if script == null:
+ return false
+ return GdUnitTestSuiteScanner.is_test_suite(script) == is_ts
+ _context_menus.append(GdUnitContextMenuItem.new(
+ GdUnitCommandFileSystemRunTests.ID,
+ "Run Testsuites",
+ is_test_suite.bind(true)
+ ))
+ _context_menus.append(GdUnitContextMenuItem.new(
+ GdUnitCommandFileSystemDebugTests.ID,
+ "Debug Testsuites",
+ is_test_suite.bind(true)
+ ))
+
+ # setup shortcuts
+ for menu_item in _context_menus:
+ if menu_item.shortcut():
+ var cb := func call(...files: Array) -> void:
+ var paths: Array = files[0]
+ menu_item.execute.callv(paths)
+ add_menu_shortcut(menu_item.shortcut(), cb)
+
+
+func _popup_menu(paths: PackedStringArray) -> void:
+ for menu_item in _context_menus:
+ if menu_item.shortcut():
+ add_context_menu_item_from_shortcut(menu_item.name, menu_item.shortcut(), menu_item.icon)
+ else:
+ add_context_menu_item(menu_item.name, menu_item.execute.bindv(paths).unbind(1), menu_item.icon)
diff --git a/addons/gdUnit4/src/ui/menu/GdUnitEditorFileSystemContextMenuHandler.gd.uid b/addons/gdUnit4/src/ui/menu/GdUnitEditorFileSystemContextMenuHandler.gd.uid
new file mode 100644
index 0000000..fde0854
--- /dev/null
+++ b/addons/gdUnit4/src/ui/menu/GdUnitEditorFileSystemContextMenuHandler.gd.uid
@@ -0,0 +1 @@
+uid://bgas7gt1plcao
diff --git a/addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.gd b/addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.gd
new file mode 100644
index 0000000..2d6071a
--- /dev/null
+++ b/addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.gd
@@ -0,0 +1,63 @@
+@tool
+class_name GdUnitInspectorContextMenu
+extends PopupMenu
+
+
+const CONTEXT_MENU_RUN_ID = 0
+const CONTEXT_MENU_DEBUG_ID = 1
+const CONTEXT_MENU_RERUN_UNTIL_ID = 2
+# id 3 is the seperator
+const CONTEXT_MENU_COLLAPSE_ALL = 4
+const CONTEXT_MENU_EXPAND_ALL = 5
+
+
+var command_handler: GdUnitCommandHandler
+
+
+func _ready() -> void:
+ if not Engine.is_editor_hint():
+ return
+ command_handler = GdUnitCommandHandler.instance()
+ _setup_item(CONTEXT_MENU_RUN_ID, "Run Tests", GdUnitCommandInspectorRunTests.ID)
+ _setup_item(CONTEXT_MENU_DEBUG_ID, "Debug Tests", GdUnitCommandInspectorDebugTests.ID)
+ _setup_item(CONTEXT_MENU_RERUN_UNTIL_ID, "Run Tests Until Fail", GdUnitCommandInspectorRerunTestsUntilFailure.ID)
+ _setup_item(CONTEXT_MENU_EXPAND_ALL, "Expand All", GdUnitCommandInspectorTreeExpand.ID)
+ _setup_item(CONTEXT_MENU_COLLAPSE_ALL, "Collapse All", GdUnitCommandInspectorTreeCollapse.ID)
+
+
+func _setup_item(item_id: int, item_name: String, command_id: String) -> void:
+ set_item_text(item_id, item_name)
+ set_item_icon(item_id, command_handler.command_icon(command_id))
+ set_item_shortcut(item_id, command_handler.command_shortcut(command_id))
+
+
+func disable_items() -> void:
+ set_item_disabled(CONTEXT_MENU_RUN_ID, true)
+ set_item_disabled(CONTEXT_MENU_DEBUG_ID, true)
+ set_item_disabled(CONTEXT_MENU_RERUN_UNTIL_ID, true)
+
+
+func enable_items() -> void:
+ set_item_disabled(CONTEXT_MENU_RUN_ID, false)
+ set_item_disabled(CONTEXT_MENU_DEBUG_ID, false)
+ set_item_disabled(CONTEXT_MENU_RERUN_UNTIL_ID, false)
+
+
+func _on_tree_item_mouse_selected(mouse_position: Vector2, mouse_button_index: int, source: Tree) -> void:
+ if mouse_button_index == MOUSE_BUTTON_RIGHT:
+ position = source.get_screen_position() + mouse_position
+ popup()
+
+
+func _on_index_pressed(index: int) -> void:
+ match index:
+ CONTEXT_MENU_RUN_ID:
+ command_handler.command_execute(GdUnitCommandInspectorRunTests.ID)
+ CONTEXT_MENU_DEBUG_ID:
+ command_handler.command_execute(GdUnitCommandInspectorDebugTests.ID)
+ CONTEXT_MENU_RERUN_UNTIL_ID:
+ command_handler.command_execute(GdUnitCommandInspectorRerunTestsUntilFailure.ID)
+ CONTEXT_MENU_EXPAND_ALL:
+ command_handler.command_execute(GdUnitCommandInspectorTreeExpand.ID)
+ CONTEXT_MENU_COLLAPSE_ALL:
+ command_handler.command_execute(GdUnitCommandInspectorTreeCollapse.ID)
diff --git a/addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.gd.uid b/addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.gd.uid
new file mode 100644
index 0000000..f1c00de
--- /dev/null
+++ b/addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.gd.uid
@@ -0,0 +1 @@
+uid://ch44lt0cpk77q
diff --git a/addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.tscn b/addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.tscn
new file mode 100644
index 0000000..5d87a41
--- /dev/null
+++ b/addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.tscn
@@ -0,0 +1,70 @@
+[gd_scene format=3 uid="uid://bjcc71p2h1trr"]
+
+[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.gd" id="1_0x0vc"]
+
+[sub_resource type="DPITexture" id="DPITexture_miuuy"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><path fill=\"#e0e0e0\" d=\"M4 12a1 1 0 0 0 1.555.832l6-4a1 1 0 0 0 0-1.664l-6-4A1 1 0 0 0 4 4z\"/></svg>
+"
+saturation = 2.0
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1)
+}
+
+[sub_resource type="DPITexture" id="DPITexture_ern2r"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><path fill=\"#e0e0e0\" d=\"M3 3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zm4.975 0A1 1 0 0 0 7 3.998v8a1 1 0 0 0 1.625.781l5-4a1 1 0 0 0 0-1.56l-5-4A1 1 0 0 0 7.975 3z\"/></svg>
+"
+saturation = 2.0
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1)
+}
+
+[sub_resource type="DPITexture" id="DPITexture_qdci2"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><path fill=\"#e0e0e0\" d=\"m8 9.669-3.536 2.583H7v2.537h2v-2.537h2.536zm0-3.314L4.464 3.772H7V1.235h2v2.537h2.536zm-7.296.73h14.591v1.831H.704z\"/></svg>
+"
+saturation = 2.0
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1)
+}
+
+[sub_resource type="DPITexture" id="DPITexture_hed0i"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><path fill=\"#e0e0e0\" d=\"m8 16-3.5-2.5H7V11h2v2.5h2.5zM8 0 4.5 2.5H7V5h2V2.5h2.5zM1 7h14v2H1z\"/></svg>
+"
+saturation = 2.0
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1)
+}
+
+[node name="contextMenu" type="PopupMenu" unique_id=1824556050]
+auto_translate_mode = 2
+oversampling_override = 1.0
+size = Vector2i(241, 170)
+visible = true
+item_count = 6
+item_0/text = "Run Tests"
+item_0/icon = SubResource("DPITexture_miuuy")
+item_0/id = 0
+item_1/text = "Debug Tests"
+item_1/icon = SubResource("DPITexture_ern2r")
+item_1/id = 1
+item_2/text = "Run Tests Until Fail"
+item_2/icon = SubResource("DPITexture_miuuy")
+item_2/id = 2
+item_3/id = 3
+item_3/separator = true
+item_4/text = "Collapse All"
+item_4/icon = SubResource("DPITexture_qdci2")
+item_4/id = 4
+item_5/text = "Expand All"
+item_5/icon = SubResource("DPITexture_hed0i")
+item_5/id = 5
+script = ExtResource("1_0x0vc")
+
+[connection signal="index_pressed" from="." to="." method="_on_index_pressed"]
diff --git a/addons/gdUnit4/src/ui/menu/GdUnitScriptEditorContextMenuHandler.gd b/addons/gdUnit4/src/ui/menu/GdUnitScriptEditorContextMenuHandler.gd
new file mode 100644
index 0000000..26f5612
--- /dev/null
+++ b/addons/gdUnit4/src/ui/menu/GdUnitScriptEditorContextMenuHandler.gd
@@ -0,0 +1,41 @@
+@tool
+extends EditorContextMenuPlugin
+
+
+var _context_menus: Array[GdUnitContextMenuItem] = []
+
+
+func _init() -> void:
+ var is_test_suite := func is_visible(script: Script, is_ts: bool) -> bool:
+ return GdUnitTestSuiteScanner.is_test_suite(script) == is_ts
+ _context_menus.append(GdUnitContextMenuItem.new(
+ GdUnitCommandScriptEditorRunTests.ID,
+ "Run Tests",
+ is_test_suite.bind(true)
+ ))
+ _context_menus.append(GdUnitContextMenuItem.new(
+ GdUnitCommandScriptEditorDebugTests.ID,
+ "Debug Tests",
+ is_test_suite.bind(true)
+ ))
+ _context_menus.append(GdUnitContextMenuItem.new(
+ GdUnitCommandScriptEditorCreateTest.ID,
+ "Create Test",
+ is_test_suite.bind(false)
+ ))
+
+ # setup shortcuts
+ for menu_item in _context_menus:
+ if menu_item.shortcut():
+ add_menu_shortcut(menu_item.shortcut(), menu_item.execute.unbind(1))
+
+
+func _popup_menu(_paths: PackedStringArray) -> void:
+ var current_script := EditorInterface.get_script_editor().get_current_script()
+
+ for menu_item in _context_menus:
+ if menu_item.is_visible(current_script):
+ if menu_item.shortcut():
+ add_context_menu_item_from_shortcut(menu_item.name, menu_item.shortcut())
+ else:
+ add_context_menu_item(menu_item.name, menu_item.execute.unbind(1))
diff --git a/addons/gdUnit4/src/ui/menu/GdUnitScriptEditorContextMenuHandler.gd.uid b/addons/gdUnit4/src/ui/menu/GdUnitScriptEditorContextMenuHandler.gd.uid
new file mode 100644
index 0000000..8ab0661
--- /dev/null
+++ b/addons/gdUnit4/src/ui/menu/GdUnitScriptEditorContextMenuHandler.gd.uid
@@ -0,0 +1 @@
+uid://f40ddt1acw6h
diff --git a/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd b/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd
new file mode 100644
index 0000000..c36b3ec
--- /dev/null
+++ b/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd
@@ -0,0 +1,54 @@
+@tool
+extends PanelContainer
+
+signal jump_to_orphan_nodes()
+
+@onready var ICON_GREEN := GdUnitUiTools.get_icon("Unlinked", Color.WEB_GREEN)
+@onready var ICON_RED := GdUnitUiTools.get_color_animated_icon("Unlinked", Color.YELLOW, Color.ORANGE_RED)
+
+@onready var _button_time: Button = %btn_time
+@onready var _time: Label = %time_value
+@onready var _orphans: Label = %orphan_value
+@onready var _orphan_button: Button = %btn_orphan
+
+var total_elapsed_time := 0
+var total_orphans := 0
+
+
+func _ready() -> void:
+ @warning_ignore("return_value_discarded")
+ GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event)
+ _time.text = ""
+ _orphans.text = "0"
+ _button_time.icon = GdUnitUiTools.get_icon("Time")
+ _orphan_button.icon = ICON_GREEN
+
+
+func status_changed(elapsed_time: int, orphan_nodes: int) -> void:
+ total_elapsed_time += elapsed_time
+ total_orphans += orphan_nodes
+ _time.text = LocalTime.elapsed(total_elapsed_time)
+ _orphans.text = str(total_orphans)
+ if total_orphans > 0:
+ _orphan_button.icon = ICON_RED
+
+
+func _on_gdunit_event(event: GdUnitEvent) -> void:
+ match event.type():
+ GdUnitEvent.INIT:
+ _orphan_button.icon = ICON_GREEN
+ total_elapsed_time = 0
+ total_orphans = 0
+ status_changed(0, 0)
+ GdUnitEvent.TESTCASE_BEFORE:
+ pass
+ GdUnitEvent.TESTCASE_AFTER:
+ status_changed(0, event.orphan_nodes())
+ GdUnitEvent.TESTSUITE_BEFORE:
+ pass
+ GdUnitEvent.TESTSUITE_AFTER:
+ status_changed(event.elapsed_time(), event.orphan_nodes())
+
+
+func _on_ToolButton_pressed() -> void:
+ jump_to_orphan_nodes.emit()
diff --git a/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd.uid b/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd.uid
new file mode 100644
index 0000000..ea72742
--- /dev/null
+++ b/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd.uid
@@ -0,0 +1 @@
+uid://buiiqjcucwy8g
diff --git a/addons/gdUnit4/src/ui/parts/InspectorMonitor.tscn b/addons/gdUnit4/src/ui/parts/InspectorMonitor.tscn
new file mode 100644
index 0000000..0262b8c
--- /dev/null
+++ b/addons/gdUnit4/src/ui/parts/InspectorMonitor.tscn
@@ -0,0 +1,94 @@
+[gd_scene load_steps=6 format=3 uid="uid://djp8ait0bxpsc"]
+
+[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/InspectorMonitor.gd" id="3"]
+
+[sub_resource type="Image" id="Image_sx31i"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 4, 227, 227, 227, 36, 227, 227, 227, 36, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 131, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 5, 225, 225, 225, 76, 224, 224, 224, 255, 224, 224, 224, 255, 226, 226, 226, 77, 255, 255, 255, 5, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 99, 224, 224, 224, 232, 224, 224, 224, 244, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 244, 224, 224, 224, 233, 224, 224, 224, 97, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 135, 224, 224, 224, 247, 224, 224, 224, 115, 234, 234, 234, 12, 224, 224, 224, 130, 224, 224, 224, 130, 234, 234, 234, 12, 225, 225, 225, 116, 224, 224, 224, 248, 224, 224, 224, 132, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 77, 224, 224, 224, 251, 224, 224, 224, 64, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 66, 224, 224, 224, 252, 225, 225, 225, 75, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 201, 224, 224, 224, 146, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 2, 224, 224, 224, 146, 224, 224, 224, 106, 255, 255, 255, 0, 225, 225, 225, 150, 224, 224, 224, 195, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 24, 224, 224, 224, 255, 226, 226, 226, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 233, 233, 233, 23, 225, 225, 225, 166, 224, 224, 224, 237, 228, 228, 228, 47, 255, 255, 255, 0, 225, 225, 225, 51, 224, 224, 224, 255, 224, 224, 224, 16, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 67, 224, 224, 224, 255, 225, 225, 225, 215, 227, 227, 227, 9, 255, 255, 255, 0, 255, 255, 255, 0, 223, 223, 223, 239, 224, 224, 224, 253, 224, 224, 224, 49, 255, 255, 255, 0, 230, 230, 230, 30, 224, 224, 224, 230, 224, 224, 224, 255, 224, 224, 224, 49, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 41, 224, 224, 224, 255, 225, 225, 225, 101, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 139, 224, 224, 224, 139, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 5, 225, 225, 225, 117, 224, 224, 224, 255, 224, 224, 224, 33, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 6, 224, 224, 224, 240, 226, 226, 226, 87, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 96, 224, 224, 224, 236, 255, 255, 255, 3, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 143, 224, 224, 224, 211, 224, 224, 224, 8, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 232, 232, 232, 11, 224, 224, 224, 216, 225, 225, 225, 141, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 238, 238, 238, 15, 224, 224, 224, 220, 224, 224, 224, 178, 238, 238, 238, 15, 255, 255, 255, 0, 225, 225, 225, 51, 225, 225, 225, 51, 255, 255, 255, 0, 227, 227, 227, 18, 224, 224, 224, 184, 224, 224, 224, 218, 238, 238, 238, 15, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 36, 224, 224, 224, 212, 224, 224, 224, 232, 225, 225, 225, 133, 224, 224, 224, 251, 224, 224, 224, 240, 225, 225, 225, 135, 224, 224, 224, 234, 224, 224, 224, 208, 225, 225, 225, 34, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 230, 230, 230, 10, 224, 224, 224, 107, 224, 224, 224, 197, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 196, 224, 224, 224, 104, 224, 224, 224, 8, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_ugpqy"]
+image = SubResource("Image_sx31i")
+
+[sub_resource type="Image" id="Image_gkq5u"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 251, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 234, 22, 138, 22, 247, 22, 138, 22, 253, 22, 138, 22, 253, 22, 138, 22, 247, 22, 138, 22, 233, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 251, 22, 138, 22, 236, 255, 255, 255, 0, 22, 138, 22, 255, 255, 255, 255, 0, 23, 138, 23, 233, 22, 138, 22, 254, 22, 138, 22, 255, 22, 138, 22, 255, 22, 138, 22, 255, 22, 138, 22, 255, 22, 138, 22, 253, 23, 138, 23, 233, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 236, 22, 138, 22, 253, 22, 138, 22, 236, 22, 138, 22, 251, 255, 255, 255, 0, 22, 138, 22, 247, 22, 138, 22, 255, 22, 138, 22, 248, 22, 138, 22, 233, 23, 138, 23, 233, 22, 138, 22, 249, 22, 138, 22, 255, 22, 138, 22, 246, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 236, 22, 138, 22, 251, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 249, 22, 138, 22, 253, 23, 138, 23, 232, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 234, 22, 138, 22, 255, 22, 138, 22, 253, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 251, 22, 138, 22, 255, 22, 138, 22, 251, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 24, 139, 24, 231, 23, 138, 23, 231, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 23, 138, 23, 234, 22, 138, 22, 255, 22, 138, 22, 253, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 23, 138, 23, 231, 23, 138, 23, 234, 22, 138, 22, 249, 22, 138, 22, 255, 22, 138, 22, 246, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 23, 138, 23, 233, 22, 138, 22, 247, 22, 138, 22, 249, 24, 139, 24, 231, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 23, 138, 23, 245, 22, 138, 22, 255, 22, 138, 22, 255, 22, 138, 22, 255, 22, 138, 22, 253, 23, 138, 23, 233, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 234, 22, 138, 22, 254, 22, 138, 22, 255, 22, 138, 22, 253, 23, 138, 23, 231, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 23, 138, 23, 241, 22, 138, 22, 253, 22, 138, 22, 253, 22, 138, 22, 246, 22, 138, 22, 233, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 247, 22, 138, 22, 255, 22, 138, 22, 248, 23, 138, 23, 232, 255, 255, 255, 0, 255, 255, 255, 0, 23, 138, 23, 245, 23, 138, 23, 241, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 253, 22, 138, 22, 255, 22, 138, 22, 233, 255, 255, 255, 0, 255, 255, 255, 0, 23, 138, 23, 231, 22, 138, 22, 255, 22, 138, 22, 253, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 253, 22, 138, 22, 255, 23, 138, 23, 233, 255, 255, 255, 0, 255, 255, 255, 0, 23, 138, 23, 234, 22, 138, 22, 255, 22, 138, 22, 253, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 247, 22, 138, 22, 255, 22, 138, 22, 249, 22, 138, 22, 234, 23, 138, 23, 234, 22, 138, 22, 249, 22, 138, 22, 255, 22, 138, 22, 246, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 22, 138, 22, 233, 22, 138, 22, 253, 22, 138, 22, 255, 22, 138, 22, 255, 22, 138, 22, 255, 22, 138, 22, 255, 22, 138, 22, 253, 22, 138, 22, 233, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 23, 138, 23, 233, 22, 138, 22, 246, 22, 138, 22, 253, 22, 138, 22, 253, 22, 138, 22, 246, 23, 138, 23, 233, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_nj5du"]
+image = SubResource("Image_gkq5u")
+
+[node name="Monitor" type="PanelContainer"]
+clip_contents = true
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_right = -793.0
+offset_bottom = -564.0
+size_flags_horizontal = 9
+size_flags_vertical = 9
+script = ExtResource("3")
+
+[node name="HBoxContainer" type="HBoxContainer" parent="."]
+layout_mode = 2
+size_flags_vertical = 4
+
+[node name="timer" type="HBoxContainer" parent="HBoxContainer"]
+layout_mode = 2
+
+[node name="btn_time" type="Button" parent="HBoxContainer/timer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 4
+auto_translate = false
+localize_numeral_system = false
+tooltip_text = "Shows the total elapsed time of test execution."
+mouse_force_pass_scroll_events = false
+button_mask = 0
+shortcut_feedback = false
+shortcut_in_tooltip = false
+text = "Time"
+icon = SubResource("ImageTexture_ugpqy")
+flat = true
+
+[node name="time_value" type="Label" parent="HBoxContainer/timer"]
+unique_name_in_owner = true
+use_parent_material = true
+layout_mode = 2
+size_flags_horizontal = 3
+auto_translate = false
+localize_numeral_system = false
+max_lines_visible = 1
+
+[node name="orphan" type="HBoxContainer" parent="HBoxContainer/timer"]
+layout_mode = 2
+
+[node name="btn_orphan" type="Button" parent="HBoxContainer/timer/orphan"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 4
+auto_translate = false
+localize_numeral_system = false
+tooltip_text = "Shows the total orphan nodes detected."
+text = "Orphans"
+icon = SubResource("ImageTexture_nj5du")
+
+[node name="orphan_value" type="Label" parent="HBoxContainer/timer/orphan"]
+unique_name_in_owner = true
+use_parent_material = true
+layout_mode = 2
+size_flags_horizontal = 3
+auto_translate = false
+localize_numeral_system = false
+text = "0"
+max_lines_visible = 1
diff --git a/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd
new file mode 100644
index 0000000..d368ed3
--- /dev/null
+++ b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd
@@ -0,0 +1,49 @@
+@tool
+extends ProgressBar
+
+
+@onready var status: Label = $Label
+@onready var style: StyleBoxFlat = get("theme_override_styles/fill")
+
+var _state: GdUnitInspectorTreeConstants.STATE
+
+func _ready() -> void:
+ style.bg_color = Color.DARK_GREEN
+ value = 0
+ max_value = 0
+ update_text()
+
+
+func update_text() -> void:
+ status.text = "%d:%d" % [value, max_value]
+
+
+func _on_test_counter_changed(index: int, total: int, state: GdUnitInspectorTreeConstants.STATE) -> void:
+ value = index
+ max_value = total
+ update_text()
+
+ # inital state
+ if index == 0:
+ style.bg_color = Color.DARK_GREEN
+
+ # do only update the state is higher prio than current state
+ if state <= _state:
+ return
+ _state = state
+
+ if is_flaky(state):
+ style.bg_color = Color.WEB_GREEN
+ if is_failed(state):
+ style.bg_color = Color.DARK_RED
+
+
+func is_failed(state: GdUnitInspectorTreeConstants.STATE) -> bool:
+ return state in [
+ GdUnitInspectorTreeConstants.STATE.FAILED,
+ GdUnitInspectorTreeConstants.STATE.ERROR,
+ GdUnitInspectorTreeConstants.STATE.ABORDED]
+
+
+func is_flaky(state: GdUnitInspectorTreeConstants.STATE) -> bool:
+ return state == GdUnitInspectorTreeConstants.STATE.FLAKY
diff --git a/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd.uid b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd.uid
new file mode 100644
index 0000000..61df203
--- /dev/null
+++ b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd.uid
@@ -0,0 +1 @@
+uid://de8bjgxoj10u3
diff --git a/addons/gdUnit4/src/ui/parts/InspectorProgressBar.tscn b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.tscn
new file mode 100644
index 0000000..1824230
--- /dev/null
+++ b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.tscn
@@ -0,0 +1,33 @@
+[gd_scene load_steps=3 format=3 uid="uid://dva3tonxsxrlk"]
+
+[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd" id="1"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ayfir"]
+bg_color = Color(0, 0.392157, 0, 1)
+
+[node name="ProgressBar" type="ProgressBar"]
+custom_minimum_size = Vector2(0, 20)
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_vertical = 9
+theme_override_styles/fill = SubResource("StyleBoxFlat_ayfir")
+rounded = true
+allow_greater = true
+show_percentage = false
+script = ExtResource("1")
+
+[node name="Label" type="Label" parent="."]
+use_parent_material = true
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_vertical = 3
+horizontal_alignment = 1
+vertical_alignment = 1
+max_lines_visible = 1
diff --git a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd
new file mode 100644
index 0000000..1863459
--- /dev/null
+++ b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd
@@ -0,0 +1,209 @@
+@tool
+extends PanelContainer
+
+signal select_failure_next()
+signal select_failure_prevous()
+signal select_error_next()
+signal select_error_prevous()
+signal select_flaky_next()
+signal select_flaky_prevous()
+signal select_skipped_next()
+signal select_skipped_prevous()
+
+@warning_ignore("unused_signal")
+signal tree_view_mode_changed(flat :bool)
+
+@onready var _errors: Label = %error_value
+@onready var _failures: Label = %failure_value
+@onready var _flaky_value: Label = %flaky_value
+@onready var _skipped_value: Label = %skipped_value
+#@onready var _button_failure_up: Button = %btn_failure_up
+#@onready var _button_failure_down: Button = %btn_failure_down
+@onready var _button_sync: Button = %btn_tree_sync
+@onready var _button_view_mode: MenuButton = %btn_tree_mode
+@onready var _button_sort_mode: MenuButton = %btn_tree_sort
+
+@onready var _icon_errors: TextureRect = %icon_errors
+@onready var _icon_failures: TextureRect = %icon_failures
+@onready var _icon_flaky: TextureRect = %icon_flaky
+@onready var _icon_skipped: TextureRect = %icon_skipped
+
+var total_failed := 0
+var total_errors := 0
+var total_flaky := 0
+var total_skipped := 0
+
+
+var icon_mappings := {
+ # tree sort modes
+ 0x100 + GdUnitInspectorTreeConstants.SORT_MODE.UNSORTED : GdUnitUiTools.get_icon("TripleBar"),
+ 0x100 + GdUnitInspectorTreeConstants.SORT_MODE.NAME_ASCENDING : GdUnitUiTools.get_icon("Sort"),
+ 0x100 + GdUnitInspectorTreeConstants.SORT_MODE.NAME_DESCENDING : GdUnitUiTools.get_flipped_icon("Sort"),
+ 0x100 + GdUnitInspectorTreeConstants.SORT_MODE.EXECUTION_TIME : GdUnitUiTools.get_icon("History"),
+ # tree view modes
+ 0x200 + GdUnitInspectorTreeConstants.TREE_VIEW_MODE.TREE : GdUnitUiTools.get_icon("Tree", Color.GHOST_WHITE),
+ 0x200 + GdUnitInspectorTreeConstants.TREE_VIEW_MODE.FLAT : GdUnitUiTools.get_icon("AnimationTrackGroup", Color.GHOST_WHITE)
+}
+
+
+@warning_ignore("return_value_discarded")
+func _ready() -> void:
+ _failures.text = "0"
+ _errors.text = "0"
+ _flaky_value.text = "0"
+ _skipped_value.text = "0"
+ _icon_failures.texture = GdUnitUiTools.get_icon("StatusError", Color.SKY_BLUE)
+ _icon_errors.texture = GdUnitUiTools.get_icon("StatusError", Color.DARK_RED)
+ _icon_flaky.texture = GdUnitUiTools.get_icon("CheckBox", Color.GREEN_YELLOW)
+ _icon_skipped.texture = GdUnitUiTools.get_icon("CheckBox", Color.WEB_GRAY)
+
+ #_button_failure_up.icon = GdUnitUiTools.get_icon("ArrowUp")
+ #_button_failure_down.icon = GdUnitUiTools.get_icon("ArrowDown")
+ _button_sync.icon = GdUnitUiTools.get_icon("Loop")
+ _set_sort_mode_menu_options()
+ _set_view_mode_menu_options()
+ GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event)
+ GdUnitSignals.instance().gdunit_settings_changed.connect(_on_settings_changed)
+
+
+func _set_sort_mode_menu_options() -> void:
+ _button_sort_mode.icon = GdUnitUiTools.get_icon("Sort")
+ # construct context sort menu according to the available modes
+ var context_menu :PopupMenu = _button_sort_mode.get_popup()
+ context_menu.clear()
+
+ if not context_menu.index_pressed.is_connected(_on_sort_mode_changed):
+ @warning_ignore("return_value_discarded")
+ context_menu.index_pressed.connect(_on_sort_mode_changed)
+
+ var configured_sort_mode := GdUnitSettings.get_inspector_tree_sort_mode()
+ for sort_mode: String in GdUnitInspectorTreeConstants.SORT_MODE.keys():
+ var enum_value :int = GdUnitInspectorTreeConstants.SORT_MODE.get(sort_mode)
+ var icon :Texture2D = icon_mappings[0x100 + enum_value]
+ context_menu.add_icon_check_item(icon, normalise(sort_mode), enum_value)
+ context_menu.set_item_checked(enum_value, configured_sort_mode == enum_value)
+
+
+func _set_view_mode_menu_options() -> void:
+ _button_view_mode.icon = GdUnitUiTools.get_icon("Tree", Color.GHOST_WHITE)
+ # construct context tree view menu according to the available modes
+ var context_menu :PopupMenu = _button_view_mode.get_popup()
+ context_menu.clear()
+
+ if not context_menu.index_pressed.is_connected(_on_tree_view_mode_changed):
+ @warning_ignore("return_value_discarded")
+ context_menu.index_pressed.connect(_on_tree_view_mode_changed)
+
+ var configured_tree_view_mode := GdUnitSettings.get_inspector_tree_view_mode()
+ for tree_view_mode: String in GdUnitInspectorTreeConstants.TREE_VIEW_MODE.keys():
+ var enum_value :int = GdUnitInspectorTreeConstants.TREE_VIEW_MODE.get(tree_view_mode)
+ var icon :Texture2D = icon_mappings[0x200 + enum_value]
+ context_menu.add_icon_check_item(icon, normalise(tree_view_mode), enum_value)
+ context_menu.set_item_checked(enum_value, configured_tree_view_mode == enum_value)
+
+
+func normalise(value: String) -> String:
+ var parts := value.to_lower().split("_")
+ parts[0] = parts[0].capitalize()
+ return " ".join(parts)
+
+
+func status_changed(errors: int, failed: int, flaky: int, skipped: int) -> void:
+ total_failed += failed
+ total_errors += errors
+ total_flaky += flaky
+ total_skipped += skipped
+ _failures.text = str(total_failed)
+ _errors.text = str(total_errors)
+ _flaky_value.text = str(total_flaky)
+ _skipped_value.text = str(total_skipped)
+
+
+func disable_buttons(value :bool) -> void:
+ _button_sync.set_disabled(value)
+ _button_sort_mode.set_disabled(value)
+ _button_view_mode.set_disabled(value)
+
+
+func _on_gdunit_event(event: GdUnitEvent) -> void:
+ match event.type():
+ GdUnitEvent.DISCOVER_START:
+ disable_buttons(true)
+
+ GdUnitEvent.DISCOVER_END:
+ disable_buttons(false)
+
+ GdUnitEvent.INIT:
+ total_errors = 0
+ total_failed = 0
+ total_flaky = 0
+ total_skipped = 0
+ status_changed(total_errors, total_failed, total_flaky, total_skipped)
+
+ GdUnitEvent.TESTCASE_AFTER:
+ status_changed(event.error_count(), event.failed_count(), event.is_flaky(), event.is_skipped())
+
+ GdUnitEvent.TESTSUITE_AFTER:
+ status_changed(event.error_count(), event.failed_count(), event.is_flaky(), 0)
+
+ GdUnitEvent.SESSION_START:
+ disable_buttons(true)
+
+ GdUnitEvent.SESSION_CLOSE:
+ disable_buttons(false)
+
+
+func _on_btn_error_up_pressed() -> void:
+ select_error_prevous.emit()
+
+
+func _on_btn_error_down_pressed() -> void:
+ select_error_next.emit()
+
+
+func _on_failure_up_pressed() -> void:
+ select_failure_prevous.emit()
+
+
+func _on_failure_down_pressed() -> void:
+ select_failure_next.emit()
+
+
+func _on_btn_flaky_up_pressed() -> void:
+ select_flaky_prevous.emit()
+
+
+func _on_btn_flaky_down_pressed() -> void:
+ select_flaky_next.emit()
+
+
+func _on_btn_skipped_up_pressed() -> void:
+ select_skipped_prevous.emit()
+
+
+func _on_btn_skipped_down_pressed() -> void:
+ select_skipped_next.emit()
+
+
+func _on_btn_tree_sync_pressed() -> void:
+ await GdUnitTestDiscoverer.run()
+
+
+func _on_sort_mode_changed(index: int) -> void:
+ var selected_sort_mode :GdUnitInspectorTreeConstants.SORT_MODE = GdUnitInspectorTreeConstants.SORT_MODE.values()[index]
+ GdUnitSettings.set_inspector_tree_sort_mode(selected_sort_mode)
+
+
+func _on_tree_view_mode_changed(index: int) ->void:
+ var selected_tree_mode :GdUnitInspectorTreeConstants.TREE_VIEW_MODE = GdUnitInspectorTreeConstants.TREE_VIEW_MODE.values()[index]
+ GdUnitSettings.set_inspector_tree_view_mode(selected_tree_mode)
+
+
+################################################################################
+# external signal receiver
+################################################################################
+func _on_settings_changed(property :GdUnitProperty) -> void:
+ if property.name() == GdUnitSettings.INSPECTOR_TREE_SORT_MODE:
+ _set_sort_mode_menu_options()
+ if property.name() == GdUnitSettings.INSPECTOR_TREE_VIEW_MODE:
+ _set_view_mode_menu_options()
diff --git a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd.uid b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd.uid
new file mode 100644
index 0000000..e4c2c25
--- /dev/null
+++ b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd.uid
@@ -0,0 +1 @@
+uid://c0whlxhcst61i
diff --git a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn
new file mode 100644
index 0000000..d4ed816
--- /dev/null
+++ b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn
@@ -0,0 +1,465 @@
+[gd_scene load_steps=26 format=3 uid="uid://c22l4odk7qesc"]
+
+[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd" id="3"]
+
+[sub_resource type="DPITexture" id="DPITexture_mb3ih"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><path fill=\"#e0e0e0\" d=\"M8 1v2H6a5 5 0 0 0-4 8l1.414-1.414A3 3 0 0 1 6 5h2v2l4-3-4-3zm6 4-1.414 1.414A3 3 0 0 1 10 11H8V9l-4 3 4 3v-2h2a5 5 0 0 0 4-8z\"/></svg>
+"
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(1, 0.87, 0.4, 1)
+}
+
+[sub_resource type="DPITexture" id="DPITexture_wo03e"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><path fill=\"#e0e0e0\" d=\"M9 1v2h6V1zM4 1a1 1 0 0 0-.691.291l-2 2a1 1 0 0 0 1.414 1.414l.293-.293v7.172l-.293-.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l2-2a1 1 0 0 0-1.414-1.414l-.293.293V4.412l.293.293a1 1 0 0 0 1.414-1.414l-2-2A1 1 0 0 0 4 1zm5 6v2h4V7zm0 6v2h2v-2z\"/></svg>
+"
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(1, 0.87, 0.4, 1)
+}
+
+[sub_resource type="DPITexture" id="DPITexture_ixycx"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><path fill=\"none\" stroke=\"#e0e0e0\" d=\"M2 4h12zm0 4h12zm0 4h12z\"/></svg>
+"
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(1, 0.87, 0.4, 1)
+}
+
+[sub_resource type="Image" id="Image_c80wp"]
+data = {
+"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 231, 231, 231, 21, 224, 224, 224, 194, 224, 224, 224, 196, 232, 232, 232, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 212, 232, 232, 232, 22, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 194, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 197, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 176, 224, 224, 224, 200, 224, 224, 224, 253, 224, 224, 224, 255, 225, 225, 225, 199, 224, 224, 224, 179, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 171, 224, 224, 224, 195, 224, 224, 224, 253, 224, 224, 224, 255, 224, 224, 224, 195, 225, 225, 225, 175, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 196, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 199, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 233, 233, 233, 23, 224, 224, 224, 213, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 215, 224, 224, 224, 24, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 233, 233, 233, 23, 224, 224, 224, 198, 224, 224, 224, 201, 224, 224, 224, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_eis20"]
+image = SubResource("Image_c80wp")
+
+[sub_resource type="DPITexture" id="DPITexture_t2qd7"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><path fill=\"#e0e0e0\" d=\"M5 8a4 4 0 1 1 4 4v2a6 6 0 1 0-6-6H1l3 4 3-4zm3-3h2v2h2v2H8z\"/></svg>
+"
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(1, 0.87, 0.4, 1)
+}
+
+[sub_resource type="Image" id="Image_jh28t"]
+data = {
+"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 248, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 248, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_1mh1t"]
+image = SubResource("Image_jh28t")
+
+[sub_resource type="Image" id="Image_lpjla"]
+data = {
+"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 248, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 248, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_bq8kn"]
+image = SubResource("Image_lpjla")
+
+[sub_resource type="Image" id="Image_bwbka"]
+data = {
+"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 249, 249, 255, 230, 246, 246, 252, 230, 249, 249, 255, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 246, 252, 237, 246, 246, 252, 255, 246, 246, 252, 248, 0, 0, 0, 0, 246, 246, 252, 254, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 246, 252, 236, 246, 246, 252, 254, 246, 246, 252, 247, 0, 0, 0, 0, 246, 246, 252, 254, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 246, 253, 231, 246, 246, 253, 232, 246, 246, 252, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 246, 252, 243, 246, 246, 252, 255, 246, 246, 252, 242, 246, 246, 252, 230, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 246, 252, 242, 246, 246, 252, 253, 246, 246, 252, 241, 246, 246, 252, 230, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 246, 252, 244, 246, 246, 252, 255, 246, 246, 252, 241, 246, 246, 252, 230, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 246, 252, 244, 246, 246, 252, 255, 246, 246, 252, 241, 246, 246, 252, 230, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_8lbfl"]
+image = SubResource("Image_bwbka")
+
+[sub_resource type="Image" id="Image_ki3oo"]
+data = {
+"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 232, 151, 12, 11, 242, 151, 12, 11, 250, 151, 12, 11, 254, 151, 12, 11, 254, 151, 12, 11, 250, 151, 12, 11, 242, 151, 12, 10, 232, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 10, 238, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 11, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 237, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 10, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 10, 232, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 240, 151, 12, 10, 234, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 11, 234, 151, 12, 11, 241, 151, 12, 11, 255, 151, 12, 11, 253, 151, 11, 10, 232, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 242, 151, 12, 11, 255, 151, 12, 11, 254, 151, 12, 10, 234, 0, 0, 0, 0, 151, 12, 10, 234, 151, 12, 11, 253, 151, 12, 11, 253, 151, 12, 11, 234, 0, 0, 0, 0, 151, 12, 11, 234, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 241, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 250, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 10, 234, 0, 0, 0, 0, 151, 12, 10, 234, 151, 12, 10, 234, 0, 0, 0, 0, 151, 12, 11, 234, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 250, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 10, 234, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 234, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 10, 234, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 10, 234, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 250, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 11, 234, 0, 0, 0, 0, 151, 12, 11, 234, 151, 12, 10, 234, 0, 0, 0, 0, 151, 12, 11, 234, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 250, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 242, 151, 12, 11, 255, 151, 12, 11, 254, 151, 12, 11, 234, 0, 0, 0, 0, 151, 12, 11, 234, 151, 12, 11, 253, 151, 12, 11, 253, 151, 12, 11, 234, 0, 0, 0, 0, 151, 12, 11, 234, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 10, 241, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 10, 232, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 241, 151, 12, 11, 234, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 11, 234, 151, 12, 11, 241, 151, 12, 11, 255, 151, 12, 11, 253, 151, 11, 10, 231, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 237, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 237, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 11, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 232, 151, 12, 11, 242, 151, 12, 11, 250, 151, 12, 11, 253, 151, 12, 11, 253, 151, 12, 11, 250, 151, 12, 11, 241, 151, 11, 10, 232, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_ivm1h"]
+image = SubResource("Image_ki3oo")
+
+[sub_resource type="Image" id="Image_uqb0l"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 255, 224, 224, 224, 255, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 195, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 224, 224, 224, 255, 224, 224, 224, 255, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 194, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 178, 224, 224, 224, 194, 230, 230, 230, 20, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 194, 224, 224, 224, 179, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 180, 224, 224, 224, 180, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_1oriu"]
+image = SubResource("Image_uqb0l")
+
+[sub_resource type="Image" id="Image_j00vj"]
+data = {
+"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 181, 224, 224, 224, 180, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 180, 224, 224, 224, 195, 231, 231, 231, 21, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 195, 224, 224, 224, 178, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 195, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 224, 224, 224, 255, 224, 224, 224, 255, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 194, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 255, 224, 224, 224, 255, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_ikyhk"]
+image = SubResource("Image_j00vj")
+
+[sub_resource type="Image" id="Image_0oden"]
+data = {
+"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147, 198, 223, 232, 147, 197, 222, 242, 147, 197, 222, 250, 147, 197, 222, 254, 147, 197, 222, 254, 147, 197, 222, 250, 147, 197, 222, 242, 147, 197, 222, 232, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 238, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 197, 222, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147, 198, 222, 237, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 232, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 240, 147, 198, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 234, 147, 197, 222, 241, 147, 197, 222, 255, 147, 197, 222, 253, 147, 197, 222, 232, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 242, 147, 197, 222, 255, 147, 197, 222, 254, 147, 198, 222, 234, 0, 0, 0, 0, 147, 198, 222, 234, 147, 197, 222, 253, 147, 197, 222, 253, 147, 197, 222, 234, 0, 0, 0, 0, 147, 197, 222, 234, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 241, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 250, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 234, 0, 0, 0, 0, 147, 198, 222, 234, 147, 198, 222, 234, 0, 0, 0, 0, 147, 197, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 250, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 234, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 234, 0, 0, 0, 0, 0, 0, 0, 0, 147, 198, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 250, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 197, 222, 234, 0, 0, 0, 0, 147, 197, 222, 234, 147, 198, 222, 234, 0, 0, 0, 0, 147, 197, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 250, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 242, 147, 197, 222, 255, 147, 197, 222, 254, 147, 198, 222, 234, 0, 0, 0, 0, 147, 197, 222, 234, 147, 197, 222, 253, 147, 197, 222, 253, 147, 197, 222, 234, 0, 0, 0, 0, 147, 197, 222, 234, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 241, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 232, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 241, 147, 197, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 197, 222, 234, 147, 197, 222, 241, 147, 197, 222, 255, 147, 197, 222, 253, 147, 197, 221, 231, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147, 198, 222, 237, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 237, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 197, 222, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147, 198, 222, 232, 147, 197, 222, 242, 147, 197, 222, 250, 147, 197, 222, 253, 147, 197, 222, 253, 147, 197, 222, 250, 147, 197, 222, 241, 147, 197, 222, 232, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_suo5c"]
+image = SubResource("Image_0oden")
+
+[sub_resource type="Image" id="Image_ipq44"]
+data = {
+"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 237, 170, 253, 57, 252, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 254, 58, 242, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 252, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 254, 58, 242, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 58, 234, 170, 253, 57, 247, 171, 255, 57, 231, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 58, 234, 170, 253, 57, 253, 170, 253, 57, 255, 170, 254, 57, 243, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 58, 234, 170, 253, 57, 253, 170, 253, 57, 255, 170, 254, 57, 247, 171, 255, 58, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 170, 254, 58, 232, 170, 254, 57, 232, 0, 0, 0, 0, 170, 253, 58, 234, 170, 253, 57, 253, 170, 253, 57, 255, 170, 254, 57, 247, 171, 255, 58, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 170, 254, 58, 232, 170, 253, 57, 251, 170, 253, 57, 251, 170, 254, 58, 236, 170, 253, 57, 253, 170, 253, 57, 255, 170, 254, 57, 247, 171, 255, 58, 230, 0, 0, 0, 0, 170, 254, 58, 242, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 170, 254, 57, 232, 170, 253, 57, 251, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 254, 57, 247, 171, 255, 58, 230, 0, 0, 0, 0, 170, 254, 58, 242, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 170, 254, 57, 232, 170, 253, 57, 251, 170, 253, 57, 255, 170, 254, 57, 247, 171, 255, 58, 230, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 254, 57, 232, 170, 253, 57, 244, 171, 255, 58, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 252, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 252, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 254, 57, 237, 170, 253, 57, 252, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 252, 170, 254, 57, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_d5kq4"]
+image = SubResource("Image_ipq44")
+
+[sub_resource type="Image" id="Image_8d0da"]
+data = {
+"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 237, 129, 139, 130, 252, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 242, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 252, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 242, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 131, 234, 129, 139, 130, 247, 130, 141, 130, 231, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 131, 234, 129, 139, 130, 253, 129, 139, 130, 255, 129, 139, 130, 243, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 131, 234, 129, 139, 130, 253, 129, 139, 130, 255, 129, 139, 130, 247, 131, 141, 131, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 130, 140, 131, 232, 129, 140, 130, 232, 0, 0, 0, 0, 129, 139, 131, 234, 129, 139, 130, 253, 129, 139, 130, 255, 129, 139, 130, 247, 131, 141, 131, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 130, 140, 131, 232, 129, 139, 130, 251, 129, 139, 130, 251, 129, 139, 130, 236, 129, 139, 130, 253, 129, 139, 130, 255, 129, 139, 130, 247, 131, 141, 131, 230, 0, 0, 0, 0, 129, 139, 130, 242, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 129, 140, 130, 232, 129, 139, 130, 251, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 247, 131, 141, 131, 230, 0, 0, 0, 0, 129, 139, 130, 242, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 130, 140, 130, 232, 129, 139, 130, 251, 129, 139, 130, 255, 129, 139, 130, 247, 131, 141, 131, 230, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 130, 140, 130, 232, 129, 139, 130, 244, 131, 141, 131, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 252, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 252, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 130, 139, 130, 237, 129, 139, 130, 252, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 252, 129, 139, 130, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_qagbu"]
+image = SubResource("Image_8d0da")
+
+[node name="StatusBar" type="PanelContainer"]
+clip_contents = true
+anchors_preset = 10
+anchor_right = 1.0
+offset_right = -807.0
+offset_bottom = 31.0
+grow_horizontal = 2
+size_flags_horizontal = 3
+size_flags_vertical = 0
+script = ExtResource("3")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 2
+size_flags_vertical = 0
+
+[node name="tree_tools" type="HBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 0
+
+[node name="Label" type="Label" parent="VBoxContainer/tree_tools"]
+layout_mode = 2
+size_flags_horizontal = 0
+text = "Statistics"
+
+[node name="tree_buttons" type="HBoxContainer" parent="VBoxContainer/tree_tools"]
+layout_mode = 2
+size_flags_horizontal = 10
+size_flags_vertical = 4
+alignment = 2
+
+[node name="VSeparator" type="VSeparator" parent="VBoxContainer/tree_tools/tree_buttons"]
+layout_mode = 2
+
+[node name="btn_tree_sync" type="Button" parent="VBoxContainer/tree_tools/tree_buttons"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Run discover tests."
+icon = SubResource("DPITexture_mb3ih")
+
+[node name="btn_tree_sort" type="MenuButton" parent="VBoxContainer/tree_tools/tree_buttons"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Sets tree sorting mode."
+icon = SubResource("DPITexture_wo03e")
+flat = false
+item_count = 4
+popup/item_0/text = "Unsorted"
+popup/item_0/icon = SubResource("DPITexture_ixycx")
+popup/item_0/checkable = 1
+popup/item_0/id = 0
+popup/item_1/text = "Name ascending"
+popup/item_1/icon = SubResource("DPITexture_wo03e")
+popup/item_1/checkable = 1
+popup/item_1/checked = true
+popup/item_1/id = 1
+popup/item_2/text = "Name descending"
+popup/item_2/icon = SubResource("ImageTexture_eis20")
+popup/item_2/checkable = 1
+popup/item_2/id = 2
+popup/item_3/text = "Execution time"
+popup/item_3/icon = SubResource("DPITexture_t2qd7")
+popup/item_3/checkable = 1
+popup/item_3/id = 3
+
+[node name="btn_tree_mode" type="MenuButton" parent="VBoxContainer/tree_tools/tree_buttons"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Sets tree presentation mode."
+icon = SubResource("ImageTexture_1mh1t")
+flat = false
+item_count = 2
+popup/item_0/text = "Tree"
+popup/item_0/icon = SubResource("ImageTexture_bq8kn")
+popup/item_0/checkable = 1
+popup/item_0/checked = true
+popup/item_0/id = 0
+popup/item_1/text = "Flat"
+popup/item_1/icon = SubResource("ImageTexture_8lbfl")
+popup/item_1/checkable = 1
+popup/item_1/id = 1
+
+[node name="HSeparator" type="HSeparator" parent="VBoxContainer"]
+layout_mode = 2
+theme_override_constants/separation = 0
+
+[node name="status_bar" type="HFlowContainer" parent="VBoxContainer"]
+layout_direction = 2
+layout_mode = 2
+size_flags_vertical = 2
+
+[node name="error" type="VBoxContainer" parent="VBoxContainer/status_bar"]
+custom_minimum_size = Vector2(0, 48)
+layout_mode = 2
+size_flags_vertical = 0
+theme_override_constants/separation = -2
+
+[node name="icon" type="HBoxContainer" parent="VBoxContainer/status_bar/error"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="icon_errors" type="TextureRect" parent="VBoxContainer/status_bar/error/icon"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 10
+size_flags_vertical = 4
+tooltip_text = "Error Tests"
+texture = SubResource("ImageTexture_ivm1h")
+stretch_mode = 3
+
+[node name="btn_up" type="Button" parent="VBoxContainer/status_bar/error/icon"]
+layout_mode = 2
+size_flags_horizontal = 10
+size_flags_vertical = 4
+tooltip_text = "Jump to the previous error test"
+icon = SubResource("ImageTexture_1oriu")
+
+[node name="counter" type="HBoxContainer" parent="VBoxContainer/status_bar/error"]
+auto_translate_mode = 2
+layout_mode = 2
+localize_numeral_system = false
+
+[node name="error_value" type="Label" parent="VBoxContainer/status_bar/error/counter"]
+unique_name_in_owner = true
+use_parent_material = true
+custom_minimum_size = Vector2(32, 0)
+layout_mode = 2
+size_flags_horizontal = 10
+text = "0"
+horizontal_alignment = 2
+justification_flags = 0
+visible_characters = 3
+visible_ratio = 3.0
+
+[node name="btn_down" type="Button" parent="VBoxContainer/status_bar/error/counter"]
+layout_mode = 2
+size_flags_horizontal = 4
+size_flags_vertical = 4
+tooltip_text = "Jump to the next error test"
+icon = SubResource("ImageTexture_ikyhk")
+
+[node name="VSeparator" type="VSeparator" parent="VBoxContainer/status_bar"]
+layout_mode = 2
+
+[node name="failure" type="VBoxContainer" parent="VBoxContainer/status_bar"]
+custom_minimum_size = Vector2(0, 48)
+layout_mode = 2
+theme_override_constants/separation = -2
+
+[node name="icon" type="HBoxContainer" parent="VBoxContainer/status_bar/failure"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="icon_failures" type="TextureRect" parent="VBoxContainer/status_bar/failure/icon"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 10
+size_flags_vertical = 4
+tooltip_text = "Failed Tests"
+texture = SubResource("ImageTexture_suo5c")
+stretch_mode = 3
+
+[node name="btn_up" type="Button" parent="VBoxContainer/status_bar/failure/icon"]
+layout_mode = 2
+size_flags_horizontal = 10
+size_flags_vertical = 4
+tooltip_text = "Jump to the previous failed test"
+icon = SubResource("ImageTexture_1oriu")
+
+[node name="counter" type="HBoxContainer" parent="VBoxContainer/status_bar/failure"]
+auto_translate_mode = 2
+layout_mode = 2
+localize_numeral_system = false
+
+[node name="failure_value" type="Label" parent="VBoxContainer/status_bar/failure/counter"]
+unique_name_in_owner = true
+use_parent_material = true
+custom_minimum_size = Vector2(32, 0)
+layout_mode = 2
+size_flags_horizontal = 10
+text = "0"
+horizontal_alignment = 2
+justification_flags = 0
+visible_characters = 3
+visible_ratio = 3.0
+
+[node name="btn_down" type="Button" parent="VBoxContainer/status_bar/failure/counter"]
+layout_mode = 2
+size_flags_horizontal = 4
+size_flags_vertical = 4
+tooltip_text = "Jump to the next failed test"
+icon = SubResource("ImageTexture_ikyhk")
+
+[node name="VSeparator2" type="VSeparator" parent="VBoxContainer/status_bar"]
+layout_mode = 2
+
+[node name="flaky" type="VBoxContainer" parent="VBoxContainer/status_bar"]
+custom_minimum_size = Vector2(0, 48)
+layout_mode = 2
+theme_override_constants/separation = -2
+
+[node name="icon" type="HBoxContainer" parent="VBoxContainer/status_bar/flaky"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="icon_flaky" type="TextureRect" parent="VBoxContainer/status_bar/flaky/icon"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 10
+size_flags_vertical = 4
+tooltip_text = "Flaky Tests"
+texture = SubResource("ImageTexture_d5kq4")
+stretch_mode = 3
+
+[node name="btn_up" type="Button" parent="VBoxContainer/status_bar/flaky/icon"]
+layout_mode = 2
+size_flags_horizontal = 10
+size_flags_vertical = 4
+tooltip_text = "Jump to the previous flaky test"
+icon = SubResource("ImageTexture_1oriu")
+
+[node name="counter" type="HBoxContainer" parent="VBoxContainer/status_bar/flaky"]
+auto_translate_mode = 2
+layout_mode = 2
+localize_numeral_system = false
+
+[node name="flaky_value" type="Label" parent="VBoxContainer/status_bar/flaky/counter"]
+unique_name_in_owner = true
+use_parent_material = true
+custom_minimum_size = Vector2(32, 0)
+layout_mode = 2
+size_flags_horizontal = 10
+text = "0"
+horizontal_alignment = 2
+justification_flags = 0
+visible_characters = 3
+visible_ratio = 3.0
+
+[node name="btn_down" type="Button" parent="VBoxContainer/status_bar/flaky/counter"]
+layout_mode = 2
+size_flags_horizontal = 4
+size_flags_vertical = 4
+tooltip_text = "Jump to the next flaky test"
+icon = SubResource("ImageTexture_ikyhk")
+
+[node name="VSeparator3" type="VSeparator" parent="VBoxContainer/status_bar"]
+layout_mode = 2
+
+[node name="skipped" type="VBoxContainer" parent="VBoxContainer/status_bar"]
+custom_minimum_size = Vector2(0, 48)
+layout_mode = 2
+theme_override_constants/separation = -2
+
+[node name="icon" type="HBoxContainer" parent="VBoxContainer/status_bar/skipped"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="icon_skipped" type="TextureRect" parent="VBoxContainer/status_bar/skipped/icon"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 10
+size_flags_vertical = 4
+tooltip_text = "Skipped Tests"
+texture = SubResource("ImageTexture_qagbu")
+stretch_mode = 3
+
+[node name="btn_up" type="Button" parent="VBoxContainer/status_bar/skipped/icon"]
+layout_mode = 2
+size_flags_horizontal = 10
+size_flags_vertical = 4
+tooltip_text = "Jump to the previous skipped test"
+icon = SubResource("ImageTexture_1oriu")
+
+[node name="counter" type="HBoxContainer" parent="VBoxContainer/status_bar/skipped"]
+auto_translate_mode = 2
+layout_mode = 2
+localize_numeral_system = false
+
+[node name="skipped_value" type="Label" parent="VBoxContainer/status_bar/skipped/counter"]
+unique_name_in_owner = true
+use_parent_material = true
+custom_minimum_size = Vector2(32, 0)
+layout_mode = 2
+size_flags_horizontal = 10
+text = "0"
+horizontal_alignment = 2
+justification_flags = 0
+visible_characters = 3
+visible_ratio = 3.0
+
+[node name="btn_down" type="Button" parent="VBoxContainer/status_bar/skipped/counter"]
+layout_mode = 2
+size_flags_horizontal = 4
+size_flags_vertical = 4
+tooltip_text = "Jump to the next skipped test"
+icon = SubResource("ImageTexture_ikyhk")
+
+[connection signal="pressed" from="VBoxContainer/tree_tools/tree_buttons/btn_tree_sync" to="." method="_on_btn_tree_sync_pressed"]
+[connection signal="pressed" from="VBoxContainer/status_bar/error/icon/btn_up" to="." method="_on_btn_error_up_pressed"]
+[connection signal="pressed" from="VBoxContainer/status_bar/error/counter/btn_down" to="." method="_on_btn_error_down_pressed"]
+[connection signal="pressed" from="VBoxContainer/status_bar/failure/icon/btn_up" to="." method="_on_failure_up_pressed"]
+[connection signal="pressed" from="VBoxContainer/status_bar/failure/counter/btn_down" to="." method="_on_failure_down_pressed"]
+[connection signal="pressed" from="VBoxContainer/status_bar/flaky/icon/btn_up" to="." method="_on_btn_flaky_up_pressed"]
+[connection signal="pressed" from="VBoxContainer/status_bar/flaky/counter/btn_down" to="." method="_on_btn_flaky_down_pressed"]
+[connection signal="pressed" from="VBoxContainer/status_bar/skipped/icon/btn_up" to="." method="_on_btn_skipped_up_pressed"]
+[connection signal="pressed" from="VBoxContainer/status_bar/skipped/counter/btn_down" to="." method="_on_btn_skipped_down_pressed"]
diff --git a/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd b/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd
new file mode 100644
index 0000000..4a92c52
--- /dev/null
+++ b/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd
@@ -0,0 +1,104 @@
+@tool
+extends PanelContainer
+
+
+const InspectorTreeMainPanel := preload("res://addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd")
+
+@onready var _version_label: Control = %version
+@onready var _button_wiki: Button = %help
+@onready var _tool_button: Button = %tool
+@onready var _button_run_overall: Button = %run_overall
+@onready var _button_run: Button = %run
+@onready var _button_run_debug: Button = %debug
+@onready var _button_stop: Button = %stop
+
+
+var inspector: InspectorTreeMainPanel
+var command_handler: GdUnitCommandHandler
+
+
+func _ready() -> void:
+ command_handler = GdUnitCommandHandler.instance()
+ inspector = get_parent().get_parent().find_child("MainPanel", false, false)
+ if inspector == null:
+ push_error("Internal error, can't connect to the test inspector!")
+ else:
+ inspector.tree_item_selected.connect(_on_inspector_selected)
+
+ GdUnit4Version.init_version_label(_version_label)
+
+ GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event)
+ GdUnitSignals.instance().gdunit_settings_changed.connect(_on_settings_changed)
+ _init_buttons()
+
+
+func _init_buttons() -> void:
+ _init_button(_button_run_overall, GdUnitCommandRunTestsOverall.ID)
+ _init_button(_button_run, GdUnitCommandInspectorRunTests.ID)
+ _init_button(_button_run_debug, GdUnitCommandInspectorDebugTests.ID)
+ _init_button(_button_stop, GdUnitCommandStopTestSession.ID)
+
+ _button_stop.icon = command_handler.command_icon(GdUnitCommandStopTestSession.ID)
+ _tool_button.icon = GdUnitUiTools.get_icon("Tools")
+ _button_wiki.icon = GdUnitUiTools.get_icon("HelpSearch")
+ # Set run buttons initial disabled
+ _button_run.disabled = true
+ _button_run_debug.disabled = true
+
+
+func _init_button(button: Button, comand_id: String) -> void:
+ button.set_meta("GdUnitCommand", comand_id)
+ button.icon = command_handler.command_icon(comand_id)
+ button.shortcut = command_handler.command_shortcut(comand_id)
+ if button == _button_run_overall:
+ button.visible = GdUnitSettings.is_inspector_toolbar_button_show()
+
+
+func _on_inspector_selected(item: TreeItem) -> void:
+ var button_disabled := item == null
+ _button_run.disabled = button_disabled
+ _button_run_debug.disabled = button_disabled
+
+
+func _on_gdunit_event(event: GdUnitEvent) -> void:
+ if event.type() == GdUnitEvent.SESSION_START:
+ _button_run_overall.disabled = true
+ _button_run.disabled = true
+ _button_run_debug.disabled = true
+ _button_stop.disabled = false
+ return
+ if event.type() == GdUnitEvent.SESSION_CLOSE:
+ _button_run_overall.disabled = false
+ _button_stop.disabled = true
+
+
+func _on_button_pressed(source: BaseButton) -> void:
+ var command_id: String = source.get_meta("GdUnitCommand")
+ await command_handler.command_execute(command_id)
+
+
+func _on_wiki_pressed() -> void:
+ var status := OS.shell_open("https://godot-gdunit-labs.github.io/gdUnit4/latest")
+ if status != OK:
+ push_error("Can't open GdUnit4 documentaion page: %s" % error_string(status))
+
+
+func _on_btn_tool_pressed() -> void:
+ var settings_dlg: Window = EditorInterface.get_base_control().find_child("GdUnitSettingsDialog", false, false)
+ if settings_dlg == null:
+ settings_dlg = preload("res://addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn").instantiate()
+ EditorInterface.get_base_control().add_child(settings_dlg, true)
+ settings_dlg.popup_centered_ratio(.60)
+
+
+func _on_settings_changed(property: GdUnitProperty) -> void:
+ # needs to wait a frame to be command handler notified first for settings changes
+ await get_tree().process_frame
+
+ _button_run_overall.visible = GdUnitSettings.is_inspector_toolbar_button_show()
+
+ if property.name().begins_with(GdUnitSettings.GROUP_SHORTCUT_INSPECTOR):
+ _button_run.shortcut = command_handler.command_shortcut(GdUnitCommandInspectorRunTests.ID)
+ _button_run_debug.shortcut = command_handler.command_shortcut(GdUnitCommandInspectorDebugTests.ID)
+ _button_run_overall.shortcut = command_handler.command_shortcut(GdUnitCommandRunTestsOverall.ID)
+ _button_stop.shortcut = command_handler.command_shortcut(GdUnitCommandStopTestSession.ID)
diff --git a/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd.uid b/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd.uid
new file mode 100644
index 0000000..69c8748
--- /dev/null
+++ b/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd.uid
@@ -0,0 +1 @@
+uid://bepy3y6bvaume
diff --git a/addons/gdUnit4/src/ui/parts/InspectorToolBar.tscn b/addons/gdUnit4/src/ui/parts/InspectorToolBar.tscn
new file mode 100644
index 0000000..8cd529e
--- /dev/null
+++ b/addons/gdUnit4/src/ui/parts/InspectorToolBar.tscn
@@ -0,0 +1,202 @@
+[gd_scene load_steps=17 format=3 uid="uid://dx7xy4dgi3wwb"]
+
+[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/InspectorToolBar.gd" id="3"]
+
+[sub_resource type="DPITexture" id="DPITexture_c7rhl"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><path fill=\"#e0e0e0\" d=\"M6.973 7.441a4 4 0 1 0-1.414-1.414L3.293 8.293l1.414 1.414zM9 2a2 2 0 0 1 0 4 2 2 0 0 1 0-4zm-9 8v6h2a3 3 0 1 0 0-6zm5 3a3 3 0 1 0 6 0 3 3 0 0 0-6 0zm6 0a3 3 0 0 0 3 3h1v-2h-1a1 1 0 0 1 0-2h1v-2h-1a3 3 0 0 0-3 3zm-9-1a1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2z\"/></svg>
+"
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(1, 0.87, 0.4, 1)
+}
+
+[sub_resource type="DPITexture" id="DPITexture_3erui"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><path fill=\"#e0e0e0\" d=\"M4 1 3 3l1 2v4H2v3.5a2.5 2.5 0 0 0 5 0V9H5V5l1-2-1-2zm6 .174a3 3 0 0 0 0 5.652V14a1 1 0 0 0 2 0V6.824a3 3 0 0 0 0-5.648V4a1 1 0 0 1-2 0z\"/></svg>
+"
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(1, 0.87, 0.4, 1)
+}
+
+[sub_resource type="InputEventKey" id="InputEventKey_p22nw"]
+alt_pressed = true
+pressed = true
+keycode = 4194338
+physical_keycode = 4194338
+
+[sub_resource type="Shortcut" id="Shortcut_3lcek"]
+events = [SubResource("InputEventKey_p22nw")]
+
+[sub_resource type="Image" id="Image_ndw0i"]
+data = {
+"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 195, 224, 224, 224, 210, 224, 224, 224, 56, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 195, 224, 224, 224, 210, 224, 224, 224, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 253, 224, 224, 224, 139, 224, 224, 224, 8, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 253, 224, 224, 224, 139, 224, 224, 224, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 215, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 215, 224, 224, 224, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 253, 224, 224, 224, 139, 224, 224, 224, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 183, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 182, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 252, 225, 225, 225, 134, 255, 255, 255, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 212, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 212, 226, 226, 226, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 252, 225, 225, 225, 134, 255, 255, 255, 6, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 252, 225, 225, 225, 134, 255, 255, 255, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 225, 225, 225, 191, 224, 224, 224, 206, 226, 226, 226, 52, 0, 0, 0, 0, 0, 0, 0, 0, 225, 225, 225, 191, 224, 224, 224, 206, 226, 226, 226, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_eoihf"]
+image = SubResource("Image_ndw0i")
+
+[sub_resource type="InputEventKey" id="InputEventKey_6gwbs"]
+alt_pressed = true
+pressed = true
+keycode = 4194336
+physical_keycode = 4194336
+
+[sub_resource type="Shortcut" id="Shortcut_2i8uq"]
+events = [SubResource("InputEventKey_6gwbs")]
+
+[sub_resource type="DPITexture" id="DPITexture_87jj1"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><path fill=\"#e0e0e0\" d=\"M4 12a1 1 0 0 0 1.555.832l6-4a1 1 0 0 0 0-1.664l-6-4A1 1 0 0 0 4 4z\"/></svg>
+"
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(1, 0.87, 0.4, 1)
+}
+
+[sub_resource type="InputEventKey" id="InputEventKey_fewwl"]
+alt_pressed = true
+pressed = true
+keycode = 4194337
+physical_keycode = 4194337
+
+[sub_resource type="Shortcut" id="Shortcut_f3lkx"]
+events = [SubResource("InputEventKey_fewwl")]
+
+[sub_resource type="DPITexture" id="DPITexture_7oobd"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><path fill=\"#e0e0e0\" d=\"M3 3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zm4.975 0A1 1 0 0 0 7 3.998v8a1 1 0 0 0 1.625.781l5-4a1 1 0 0 0 0-1.56l-5-4A1 1 0 0 0 7.975 3z\"/></svg>
+"
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(1, 0.87, 0.4, 1)
+}
+
+[sub_resource type="InputEventKey" id="InputEventKey_lvbnb"]
+alt_pressed = true
+pressed = true
+keycode = 4194339
+physical_keycode = 4194339
+
+[sub_resource type="Shortcut" id="Shortcut_6idxu"]
+events = [SubResource("InputEventKey_lvbnb")]
+
+[sub_resource type="DPITexture" id="DPITexture_qf2s1"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><rect width=\"10\" height=\"10\" x=\"3\" y=\"3\" fill=\"#e0e0e0\" rx=\"1\"/></svg>
+"
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(1, 0.87, 0.4, 1)
+}
+
+[node name="ToolBar" type="PanelContainer"]
+anchors_preset = 10
+anchor_right = 1.0
+offset_right = -894.0
+offset_bottom = 24.0
+grow_horizontal = 2
+size_flags_horizontal = 3
+size_flags_vertical = 4
+script = ExtResource("3")
+
+[node name="HBoxContainer" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="tools" type="HBoxContainer" parent="HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 0
+size_flags_vertical = 4
+
+[node name="help" type="Button" parent="HBoxContainer/tools"]
+unique_name_in_owner = true
+layout_mode = 2
+icon = SubResource("DPITexture_c7rhl")
+
+[node name="tool" type="Button" parent="HBoxContainer/tools"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "GdUnit Settings"
+icon = SubResource("DPITexture_3erui")
+
+[node name="controls" type="HBoxContainer" parent="HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 6
+size_flags_vertical = 4
+alignment = 1
+
+[node name="VSeparator3" type="VSeparator" parent="HBoxContainer/controls"]
+layout_mode = 2
+
+[node name="run_overall" type="Button" parent="HBoxContainer/controls"]
+unique_name_in_owner = true
+use_parent_material = true
+layout_mode = 2
+tooltip_text = "Run overall tests"
+shortcut = SubResource("Shortcut_3lcek")
+icon = SubResource("ImageTexture_eoihf")
+metadata/GdUnitCommand = "Run Tests Overall"
+
+[node name="run" type="Button" parent="HBoxContainer/controls"]
+unique_name_in_owner = true
+use_parent_material = true
+layout_mode = 2
+tooltip_text = "Rerun unit tests"
+disabled = true
+shortcut = SubResource("Shortcut_2i8uq")
+icon = SubResource("DPITexture_87jj1")
+metadata/GdUnitCommand = "Run Inspector Tests"
+
+[node name="debug" type="Button" parent="HBoxContainer/controls"]
+unique_name_in_owner = true
+use_parent_material = true
+layout_mode = 2
+tooltip_text = "Rerun unit tests (Debug)"
+disabled = true
+shortcut = SubResource("Shortcut_f3lkx")
+icon = SubResource("DPITexture_7oobd")
+metadata/GdUnitCommand = "Debug Inspector Tests"
+
+[node name="stop" type="Button" parent="HBoxContainer/controls"]
+unique_name_in_owner = true
+use_parent_material = true
+layout_mode = 2
+tooltip_text = "Stops runing unit tests"
+disabled = true
+shortcut = SubResource("Shortcut_6idxu")
+icon = SubResource("DPITexture_qf2s1")
+metadata/GdUnitCommand = "Stop Test Session"
+
+[node name="VSeparator4" type="VSeparator" parent="HBoxContainer/controls"]
+layout_mode = 2
+
+[node name="CenterContainer" type="HBoxContainer" parent="HBoxContainer"]
+use_parent_material = true
+layout_mode = 2
+size_flags_horizontal = 10
+size_flags_vertical = 4
+alignment = 2
+
+[node name="version" type="Label" parent="HBoxContainer/CenterContainer"]
+unique_name_in_owner = true
+auto_translate_mode = 2
+use_parent_material = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 13
+localize_numeral_system = false
+text = "gdUnit4 6.1.0"
+horizontal_alignment = 1
+justification_flags = 160
+
+[connection signal="pressed" from="HBoxContainer/tools/help" to="." method="_on_wiki_pressed"]
+[connection signal="pressed" from="HBoxContainer/tools/tool" to="." method="_on_btn_tool_pressed"]
+[connection signal="pressed" from="HBoxContainer/controls/run_overall" to="." method="_on_button_pressed" flags=18]
+[connection signal="pressed" from="HBoxContainer/controls/run" to="." method="_on_button_pressed" flags=18]
+[connection signal="pressed" from="HBoxContainer/controls/debug" to="." method="_on_button_pressed" flags=18]
+[connection signal="pressed" from="HBoxContainer/controls/stop" to="." method="_on_button_pressed" flags=18]
diff --git a/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd b/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd
new file mode 100644
index 0000000..f55dbee
--- /dev/null
+++ b/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd
@@ -0,0 +1,1210 @@
+@tool
+extends VSplitContainer
+
+## Will be emitted when the test index counter is changed
+signal test_counters_changed(index: int, total: int, state: GdUnitInspectorTreeConstants.STATE)
+signal tree_item_selected(item: TreeItem)
+
+
+@onready var _tree: Tree = $Panel/Tree
+@onready var _report_list: Node = $report/ScrollContainer/list
+@onready var _report_template: RichTextLabel = $report/report_template
+@onready var _context_menu: GdUnitInspectorContextMenu = $contextMenu
+@onready var _discover_hint: Control = %discover_hint
+@onready var _spinner: Button = %spinner
+
+# loading tree icons
+@onready var ICON_SPINNER := GdUnitUiTools.get_spinner()
+@onready var ICON_FOLDER := GdUnitUiTools.get_icon("Folder")
+# gdscript icons
+@onready var ICON_GDSCRIPT_TEST_DEFAULT := GdUnitUiTools.get_icon("GDScript", Color.LIGHT_GRAY)
+@onready var ICON_GDSCRIPT_TEST_SUCCESS := GdUnitUiTools.get_GDScript_icon("StatusSuccess", Color.DARK_GREEN)
+@onready var ICON_GDSCRIPT_TEST_FLAKY := GdUnitUiTools.get_GDScript_icon("CheckBox", Color.GREEN_YELLOW)
+@onready var ICON_GDSCRIPT_TEST_FAILED := GdUnitUiTools.get_GDScript_icon("StatusError", Color.SKY_BLUE)
+@onready var ICON_GDSCRIPT_TEST_ERROR := GdUnitUiTools.get_GDScript_icon("StatusError", Color.DARK_RED)
+@onready var ICON_GDSCRIPT_TEST_SUCCESS_ORPHAN := GdUnitUiTools.get_GDScript_icon("Unlinked", Color.DARK_GREEN)
+@onready var ICON_GDSCRIPT_TEST_FAILED_ORPHAN := GdUnitUiTools.get_GDScript_icon("Unlinked", Color.SKY_BLUE)
+@onready var ICON_GDSCRIPT_TEST_ERRORS_ORPHAN := GdUnitUiTools.get_GDScript_icon("Unlinked", Color.DARK_RED)
+# csharp script icons
+@onready var ICON_CSSCRIPT_TEST_DEFAULT := GdUnitUiTools.get_icon("CSharpScript", Color.LIGHT_GRAY)
+@onready var ICON_CSSCRIPT_TEST_SUCCESS := GdUnitUiTools.get_CSharpScript_icon("StatusSuccess", Color.DARK_GREEN)
+@onready var ICON_CSSCRIPT_TEST_FAILED := GdUnitUiTools.get_CSharpScript_icon("StatusError", Color.SKY_BLUE)
+@onready var ICON_CSSCRIPT_TEST_ERROR := GdUnitUiTools.get_CSharpScript_icon("StatusError", Color.DARK_RED)
+@onready var ICON_CSSCRIPT_TEST_SUCCESS_ORPHAN := GdUnitUiTools.get_CSharpScript_icon("Unlinked", Color.DARK_GREEN)
+@onready var ICON_CSSCRIPT_TEST_FAILED_ORPHAN := GdUnitUiTools.get_CSharpScript_icon("Unlinked", Color.SKY_BLUE)
+@onready var ICON_CSSCRIPT_TEST_ERRORS_ORPHAN := GdUnitUiTools.get_CSharpScript_icon("Unlinked", Color.DARK_RED)
+
+
+enum GdUnitType {
+ FOLDER,
+ TEST_SUITE,
+ TEST_CASE,
+ TEST_GROUP
+}
+
+const META_GDUNIT_PROGRESS_COUNT_MAX := "gdUnit_progress_count_max"
+const META_GDUNIT_PROGRESS_INDEX := "gdUnit_progress_index"
+const META_TEST_CASE := "gdunit_test_case"
+const META_GDUNIT_NAME := "gdUnit_name"
+const META_GDUNIT_STATE := "gdUnit_state"
+const META_GDUNIT_TYPE := "gdUnit_type"
+const META_GDUNIT_SUCCESS_TESTS := "gdUnit_suite_success_tests"
+const META_GDUNIT_REPORT := "gdUnit_report"
+const META_GDUNIT_ORPHAN := "gdUnit_orphan"
+const META_GDUNIT_EXECUTION_TIME := "gdUnit_execution_time"
+const META_GDUNIT_ORIGINAL_INDEX = "gdunit_original_index"
+const STATE = GdUnitInspectorTreeConstants.STATE
+
+
+var _tree_root: TreeItem
+var _current_selected_item: TreeItem = null
+var _current_tree_view_mode := GdUnitSettings.get_inspector_tree_view_mode()
+var _run_test_recovery := true
+
+
+## Used for debugging purposes only
+func print_tree_item_ids(parent: TreeItem) -> TreeItem:
+ for child in parent.get_children():
+ if child.has_meta(META_TEST_CASE):
+ var test_case: GdUnitTestCase = child.get_meta(META_TEST_CASE)
+ prints(test_case.guid, test_case.test_name)
+
+ if child.get_child_count() > 0:
+ print_tree_item_ids(child)
+
+ return null
+
+
+func _find_tree_item(parent: TreeItem, item_name: String) -> TreeItem:
+ for child in parent.get_children():
+ if child.get_meta(META_GDUNIT_NAME) == item_name:
+ return child
+ return null
+
+
+func _find_tree_item_by_id(parent: TreeItem, id: GdUnitGUID) -> TreeItem:
+ for child in parent.get_children():
+ if is_test_id(child, id):
+ return child
+ if child.get_child_count() > 0:
+ var item := _find_tree_item_by_id(child, id)
+ if item != null:
+ return item
+
+ return null
+
+
+func _find_tree_item_by_test_suite(parent: TreeItem, suite_path: String, suite_name: String) -> TreeItem:
+ for child in parent.get_children():
+ if child.get_meta(META_GDUNIT_TYPE) == GdUnitType.TEST_SUITE:
+ var test_case: GdUnitTestCase = child.get_meta(META_TEST_CASE)
+ if test_case.suite_resource_path == suite_path and test_case.suite_name == suite_name:
+ return child
+ if child.get_child_count() > 0:
+ var item := _find_tree_item_by_test_suite(child, suite_path, suite_name)
+ if item != null:
+ return item
+ return null
+
+
+func _find_first_item_by_state(parent: TreeItem, item_state: STATE, reverse := false) -> TreeItem:
+ var itmes := parent.get_children()
+ if reverse:
+ itmes.reverse()
+ for item in itmes:
+ if is_test_case(item) and (is_item_state(item, item_state)):
+ return item
+ var failure_item := _find_first_item_by_state(item, item_state, reverse)
+ if failure_item != null:
+ return failure_item
+ return null
+
+
+func _find_last_item_by_state(parent: TreeItem, item_state: STATE) -> TreeItem:
+ return _find_first_item_by_state(parent, item_state, true)
+
+
+func _find_item_by_state(current: TreeItem, item_state: STATE, prev := false) -> TreeItem:
+ var next := current.get_prev_in_tree() if prev else current.get_next_in_tree()
+ if next == null or next == _tree_root:
+ return null
+ if is_test_case(next) and is_item_state(next, item_state):
+ return next
+ return _find_item_by_state(next, item_state, prev)
+
+
+func is_item_state(item: TreeItem, item_state: STATE) -> bool:
+ return item.has_meta(META_GDUNIT_STATE) and item.get_meta(META_GDUNIT_STATE) == item_state
+
+
+func is_state_running(item: TreeItem) -> bool:
+ return is_item_state(item, STATE.RUNNING)
+
+
+func is_state_success(item: TreeItem) -> bool:
+ return is_item_state(item, STATE.SUCCESS)
+
+
+func is_state_warning(item: TreeItem) -> bool:
+ return is_item_state(item, STATE.WARNING)
+
+
+func is_state_failed(item: TreeItem) -> bool:
+ return is_item_state(item, STATE.FAILED)
+
+
+func is_state_error(item: TreeItem) -> bool:
+ return is_item_state(item, STATE.ERROR) or is_item_state(item, STATE.ABORDED)
+
+
+func is_item_state_orphan(item: TreeItem) -> bool:
+ return item.has_meta(META_GDUNIT_ORPHAN)
+
+
+func is_test_suite(item: TreeItem) -> bool:
+ return item.has_meta(META_GDUNIT_TYPE) and item.get_meta(META_GDUNIT_TYPE) == GdUnitType.TEST_SUITE
+
+
+func is_test_case(item: TreeItem) -> bool:
+ return item.has_meta(META_GDUNIT_TYPE) and item.get_meta(META_GDUNIT_TYPE) == GdUnitType.TEST_CASE
+
+
+func is_folder(item: TreeItem) -> bool:
+ return item.has_meta(META_GDUNIT_TYPE) and item.get_meta(META_GDUNIT_TYPE) == GdUnitType.FOLDER
+
+
+func is_test_id(item: TreeItem, id: GdUnitGUID) -> bool:
+ if not item.has_meta(META_TEST_CASE):
+ return false
+
+ var test_case: GdUnitTestCase = item.get_meta(META_TEST_CASE)
+ return test_case.guid.equals(id)
+
+
+func disable_test_recovery() -> void:
+ _run_test_recovery = false
+
+
+@warning_ignore("return_value_discarded")
+func _ready() -> void:
+ if Engine.is_editor_hint():
+ var base_control := EditorInterface.get_base_control()
+ base_control.set_meta("GdUnit4Inspector", self)
+
+ _spinner.icon = GdUnitUiTools.get_spinner()
+ init_tree()
+ GdUnitSignals.instance().gdunit_settings_changed.connect(_on_settings_changed)
+ GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event)
+ GdUnitSignals.instance().gdunit_test_discover_added.connect(on_test_case_discover_added)
+ GdUnitSignals.instance().gdunit_test_discover_deleted.connect(on_test_case_discover_deleted)
+ GdUnitSignals.instance().gdunit_test_discover_modified.connect(on_test_case_discover_modified)
+ if _run_test_recovery:
+ GdUnitTestDiscoverer.restore_last_session()
+
+
+# we need current to manually redraw bacause of the animation bug
+# https://github.com/godotengine/godot/issues/69330
+func _process(_delta: float) -> void:
+ if is_visible_in_tree():
+ queue_redraw()
+
+
+func init_tree() -> void:
+ cleanup_tree()
+ _tree.deselect_all()
+ _tree.set_hide_root(true)
+ _tree.ensure_cursor_is_visible()
+ _tree.set_allow_reselect(true)
+ _tree.set_allow_rmb_select(true)
+ _tree.set_columns(2)
+ _tree.set_column_clip_content(0, true)
+ _tree.set_column_expand_ratio(0, 1)
+ _tree.set_column_custom_minimum_width(0, 240)
+ _tree.set_column_expand_ratio(1, 0)
+ _tree.set_column_custom_minimum_width(1, 100)
+ _tree_root = _tree.create_item()
+ _tree_root.set_text(0, "tree_root")
+ _tree_root.set_meta(META_GDUNIT_NAME, "tree_root")
+ _tree_root.set_meta(META_GDUNIT_PROGRESS_COUNT_MAX, 0)
+ _tree_root.set_meta(META_GDUNIT_PROGRESS_INDEX, 0)
+ _tree_root.set_meta(META_GDUNIT_STATE, STATE.INITIAL)
+ _tree_root.set_meta(META_GDUNIT_SUCCESS_TESTS, 0)
+ # fix tree icon scaling
+ var scale_factor := EditorInterface.get_editor_scale() if Engine.is_editor_hint() else 1.0
+ _tree.set("theme_override_constants/icon_max_width", 16 * scale_factor)
+
+
+func cleanup_tree() -> void:
+ clear_reports()
+ if not _tree_root:
+ return
+ _free_recursive()
+ _tree.clear()
+ _current_selected_item = null
+
+
+func _free_recursive(items:=_tree_root.get_children()) -> void:
+ for item in items:
+ _free_recursive(item.get_children())
+ item.call_deferred("free")
+
+
+func sort_tree_items(parent: TreeItem) -> void:
+ _sort_tree_items(parent, GdUnitSettings.get_inspector_tree_sort_mode())
+ _tree.queue_redraw()
+
+
+static func _sort_tree_items(parent: TreeItem, sort_mode: GdUnitInspectorTreeConstants.SORT_MODE) -> void:
+ parent.visible = false
+ var items := parent.get_children()
+ # first remove all childs before sorting
+ for item in items:
+ parent.remove_child(item)
+
+ # do sort by selected sort mode
+ match sort_mode:
+ GdUnitInspectorTreeConstants.SORT_MODE.UNSORTED:
+ items.sort_custom(sort_items_by_original_index)
+
+ GdUnitInspectorTreeConstants.SORT_MODE.NAME_ASCENDING:
+ items.sort_custom(sort_items_by_name.bind(true))
+
+ GdUnitInspectorTreeConstants.SORT_MODE.NAME_DESCENDING:
+ items.sort_custom(sort_items_by_name.bind(false))
+
+ GdUnitInspectorTreeConstants.SORT_MODE.EXECUTION_TIME:
+ items.sort_custom(sort_items_by_execution_time)
+
+ # readding sorted childs
+ for item in items:
+ parent.add_child(item)
+ if item.get_child_count() > 0:
+ _sort_tree_items(item, sort_mode)
+ parent.visible = true
+
+
+static func sort_items_by_name(a: TreeItem, b: TreeItem, ascending: bool) -> bool:
+ var type_a: GdUnitType = a.get_meta(META_GDUNIT_TYPE)
+ var type_b: GdUnitType = b.get_meta(META_GDUNIT_TYPE)
+
+ # Sort folders to the top
+ if type_a == GdUnitType.FOLDER and type_b != GdUnitType.FOLDER:
+ return true
+ if type_b == GdUnitType.FOLDER and type_a != GdUnitType.FOLDER:
+ return false
+
+ # sort by name
+ var name_a: String = a.get_meta(META_GDUNIT_NAME)
+ var name_b: String = b.get_meta(META_GDUNIT_NAME)
+ var comparison := name_a.naturalnocasecmp_to(name_b)
+
+ return comparison < 0 if ascending else comparison > 0
+
+
+static func sort_items_by_execution_time(a: TreeItem, b: TreeItem) -> bool:
+ var type_a: GdUnitType = a.get_meta(META_GDUNIT_TYPE)
+ var type_b: GdUnitType = b.get_meta(META_GDUNIT_TYPE)
+
+ # Sort folders to the top
+ if type_a == GdUnitType.FOLDER and type_b != GdUnitType.FOLDER:
+ return true
+ if type_b == GdUnitType.FOLDER and type_a != GdUnitType.FOLDER:
+ return false
+
+ var execution_time_a :int = a.get_meta(META_GDUNIT_EXECUTION_TIME)
+ var execution_time_b :int = b.get_meta(META_GDUNIT_EXECUTION_TIME)
+ # if has same execution time sort by name
+ if execution_time_a == execution_time_b:
+ var name_a :String = a.get_meta(META_GDUNIT_NAME)
+ var name_b :String = b.get_meta(META_GDUNIT_NAME)
+ return name_a.naturalnocasecmp_to(name_b) > 0
+ return execution_time_a > execution_time_b
+
+
+static func sort_items_by_original_index(a: TreeItem, b: TreeItem) -> bool:
+ var type_a: GdUnitType = a.get_meta(META_GDUNIT_TYPE)
+ var type_b: GdUnitType = b.get_meta(META_GDUNIT_TYPE)
+
+ # Sort folders to the top
+ if type_a == GdUnitType.FOLDER and type_b != GdUnitType.FOLDER:
+ return true
+ if type_b == GdUnitType.FOLDER and type_a != GdUnitType.FOLDER:
+ return false
+
+ var index_a :int = a.get_meta(META_GDUNIT_ORIGINAL_INDEX)
+ var index_b :int = b.get_meta(META_GDUNIT_ORIGINAL_INDEX)
+
+ # Sorting by index
+ return index_a < index_b
+
+
+func restructure_tree(parent: TreeItem, tree_mode: GdUnitInspectorTreeConstants.TREE_VIEW_MODE) -> void:
+ _current_tree_view_mode = tree_mode
+
+ match tree_mode:
+ GdUnitInspectorTreeConstants.TREE_VIEW_MODE.FLAT:
+ restructure_tree_to_flat(parent)
+ GdUnitInspectorTreeConstants.TREE_VIEW_MODE.TREE:
+ restructure_tree_to_tree(parent)
+ recalculate_counters(_tree_root)
+ # finally apply actual sort mode
+ sort_tree_items(_tree_root)
+
+
+# Restructure into flat mode
+func restructure_tree_to_flat(parent: TreeItem) -> void:
+ var folders := flatmap_folders(parent)
+ # Store current folder paths and their test suites
+ for folder_path: String in folders:
+ var test_suites: Array[TreeItem] = folders[folder_path]
+ if test_suites.is_empty():
+ continue
+
+ # Create flat folder and move test suites into it
+ var folder := _tree.create_item(parent)
+ folder.set_meta(META_GDUNIT_NAME, folder_path)
+ update_item_total_counter(folder)
+ set_state_initial(folder, GdUnitType.FOLDER)
+
+ # Move test suites under the flat folder
+ for test_suite in test_suites:
+ var old_parent := test_suite.get_parent()
+ old_parent.remove_child(test_suite)
+ folder.add_child(test_suite)
+
+ # Cleanup old folder structure
+ cleanup_empty_folders(parent)
+
+
+# Restructure into hierarchical tree mode
+func restructure_tree_to_tree(parent: TreeItem) -> void:
+ var items_to_process := parent.get_children().duplicate()
+
+ for item: TreeItem in items_to_process:
+ if is_folder(item):
+ var folder_path: String = item.get_meta(META_GDUNIT_NAME)
+ var parts := folder_path.split("/")
+
+ if parts.size() > 1:
+ var current_parent := parent
+ # Build folder hierarchy
+ for part in parts:
+ var next := _find_tree_item(current_parent, part)
+ if not next:
+ next = _tree.create_item(current_parent)
+ next.set_meta(META_GDUNIT_NAME, part)
+ set_state_initial(next, GdUnitType.FOLDER)
+ current_parent = next
+
+ # Move test suites to deepest folder
+ var test_suites := item.get_children()
+ for test_suite in test_suites:
+ item.remove_child(test_suite)
+ current_parent.add_child(test_suite)
+
+ # Remove the flat folder
+ item.get_parent().remove_child(item)
+ item.free()
+
+
+func flatmap_folders(parent: TreeItem) -> Dictionary:
+ var folder_map := {}
+
+ for item in parent.get_children():
+ if is_folder(item):
+ var current_path: String = item.get_meta(META_GDUNIT_NAME)
+ # Get parent folder paths
+ var parent_path := get_parent_folder_path(item)
+ if parent_path:
+ current_path = parent_path + "/" + current_path
+
+ # Collect direct children of this folder
+ var children: Array[TreeItem] = []
+ for child in item.get_children():
+ if is_test_suite(child):
+ children.append(child)
+
+ # Add children to existing path or create new entry
+ if not children.is_empty():
+ if folder_map.has(current_path):
+ @warning_ignore("unsafe_method_access")
+ folder_map[current_path].append_array(children)
+ else:
+ folder_map[current_path] = children
+
+ # Recursively process subfolders
+ var sub_folders := flatmap_folders(item)
+ for path: String in sub_folders.keys():
+ if folder_map.has(path):
+ @warning_ignore("unsafe_method_access")
+ folder_map[path].append_array(sub_folders[path])
+ else:
+ folder_map[path] = sub_folders[path]
+ return folder_map
+
+
+func get_parent_folder_path(item: TreeItem) -> String:
+ var path := ""
+ var parent := item.get_parent()
+
+ while parent != _tree_root:
+ if is_folder(parent):
+ path = parent.get_meta(META_GDUNIT_NAME) + ("/" + path if path else "")
+ parent = parent.get_parent()
+
+ return path
+
+
+func cleanup_empty_folders(parent: TreeItem) -> void:
+ var folders: Array[TreeItem] = []
+ # First collect all folders to avoid modification during iteration
+ for item in parent.get_children():
+ if is_folder(item):
+ folders.append(item)
+
+ # Process collected folders
+ for folder in folders:
+ cleanup_empty_folders(folder)
+ # Remove folder if it has no children after cleanup
+ if folder.get_child_count() == 0:
+ parent.remove_child(folder)
+ folder.free()
+
+
+func reset_tree_state(parent: TreeItem) -> void:
+ if parent == _tree_root:
+ _tree_root.set_meta(META_GDUNIT_PROGRESS_INDEX, 0)
+ _tree_root.set_meta(META_GDUNIT_STATE, STATE.INITIAL)
+ test_counters_changed.emit(0, 0, STATE.INITIAL)
+
+ for item in parent.get_children():
+ set_state_initial(item, get_item_type(item))
+ reset_tree_state(item)
+
+
+func select_item(item: TreeItem) -> TreeItem:
+ if item != null:
+ # enshure the parent is collapsed
+ do_collapse_parent(item)
+ item.select(0)
+ _tree.ensure_cursor_is_visible()
+ _tree.scroll_to_item(item, true)
+ return item
+
+
+func do_collapse_parent(item: TreeItem) -> void:
+ if item != null:
+ item.collapsed = false
+ do_collapse_parent(item.get_parent())
+
+
+func do_collapse_all(collapse: bool, parent := _tree_root) -> void:
+ for item in parent.get_children():
+ item.collapsed = collapse
+ if not collapse:
+ do_collapse_all(collapse, item)
+
+
+func set_state_initial(item: TreeItem, type: GdUnitType) -> void:
+ item.set_text(0, str(item.get_meta(META_GDUNIT_NAME)))
+ item.set_custom_color(0, Color.LIGHT_GRAY)
+ item.set_tooltip_text(0, "")
+ item.set_text_overrun_behavior(0, TextServer.OVERRUN_TRIM_CHAR)
+ item.set_expand_right(0, true)
+
+ item.set_custom_color(1, Color.LIGHT_GRAY)
+ item.set_text(1, "")
+ item.set_expand_right(1, true)
+ item.set_tooltip_text(1, "")
+
+ item.set_meta(META_GDUNIT_STATE, STATE.INITIAL)
+ item.set_meta(META_GDUNIT_TYPE, type)
+ item.set_meta(META_GDUNIT_SUCCESS_TESTS, 0)
+ item.set_meta(META_GDUNIT_EXECUTION_TIME, 0)
+ if item.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX) and item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX) > 0:
+ item.set_text(0, "(0/%d) %s" % [item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX), item.get_meta(META_GDUNIT_NAME)])
+ item.remove_meta(META_GDUNIT_REPORT)
+ item.remove_meta(META_GDUNIT_ORPHAN)
+
+ set_item_icon_by_state(item)
+
+
+func set_state_running(item: TreeItem, is_running: bool) -> void:
+ if is_state_running(item):
+ return
+ if is_item_state(item, STATE.INITIAL):
+ item.set_custom_color(0, Color.DARK_GREEN)
+ item.set_custom_color(1, Color.DARK_GREEN)
+ item.set_meta(META_GDUNIT_STATE, STATE.RUNNING)
+ item.collapsed = false
+
+ if is_running:
+ item.set_icon(0, ICON_SPINNER)
+ else:
+ set_item_icon_by_state(item)
+ for child in item.get_children():
+ set_item_icon_by_state(child)
+
+ var parent := item.get_parent()
+ if parent != _tree_root:
+ set_state_running(parent, is_running)
+
+
+func set_state_succeded(item: TreeItem) -> void:
+ # Do not overwrite higher states
+ if is_state_error(item) or is_state_failed(item):
+ return
+ if item == _tree_root:
+ return
+ item.set_custom_color(0, Color.GREEN)
+ item.set_custom_color(1, Color.GREEN)
+ item.set_meta(META_GDUNIT_STATE, STATE.SUCCESS)
+ item.collapsed = GdUnitSettings.is_inspector_node_collapse()
+ set_item_icon_by_state(item)
+
+
+func set_state_flaky(item: TreeItem, event: GdUnitEvent) -> void:
+ # Do not overwrite higher states
+ if is_state_error(item):
+ return
+ var retry_count := event.statistic(GdUnitEvent.RETRY_COUNT)
+ item.set_meta(META_GDUNIT_STATE, STATE.FLAKY)
+ if retry_count > 1:
+ var item_text: String = item.get_meta(META_GDUNIT_NAME)
+ if item.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX):
+ var success_count: int = item.get_meta(META_GDUNIT_SUCCESS_TESTS)
+ item_text = "(%d/%d) %s" % [success_count, item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX), item.get_meta(META_GDUNIT_NAME)]
+ item.set_text(0, "%s (%s retries)" % [item_text, retry_count])
+ item.set_custom_color(0, Color.GREEN_YELLOW)
+ item.set_custom_color(1, Color.GREEN_YELLOW)
+ item.collapsed = false
+ set_item_icon_by_state(item)
+
+
+func set_state_skipped(item: TreeItem) -> void:
+ item.set_meta(META_GDUNIT_STATE, STATE.SKIPPED)
+ item.set_text(1, "(skipped)")
+ item.set_text_alignment(1, HORIZONTAL_ALIGNMENT_RIGHT)
+ item.set_custom_color(0, Color.DARK_GRAY)
+ item.set_custom_color(1, Color.DARK_GRAY)
+ item.collapsed = false
+ set_item_icon_by_state(item)
+
+
+func set_state_warnings(item: TreeItem) -> void:
+ # Do not overwrite higher states
+ if is_state_error(item) or is_state_failed(item):
+ return
+ item.set_meta(META_GDUNIT_STATE, STATE.WARNING)
+ item.set_custom_color(0, Color.YELLOW)
+ item.set_custom_color(1, Color.YELLOW)
+ item.collapsed = false
+ set_item_icon_by_state(item)
+
+
+func set_state_failed(item: TreeItem, event: GdUnitEvent) -> void:
+ # Do not overwrite higher states
+ if is_state_error(item):
+ return
+ var retry_count := event.statistic(GdUnitEvent.RETRY_COUNT)
+ if retry_count > 1:
+ var item_text: String = item.get_meta(META_GDUNIT_NAME)
+ if item.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX):
+ var success_count: int = item.get_meta(META_GDUNIT_SUCCESS_TESTS)
+ item_text = "(%d/%d) %s" % [success_count, item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX), item.get_meta(META_GDUNIT_NAME)]
+ item.set_text(0, "%s (%s retries)" % [item_text, retry_count])
+ item.set_meta(META_GDUNIT_STATE, STATE.FAILED)
+ item.set_custom_color(0, Color.LIGHT_BLUE)
+ item.set_custom_color(1, Color.LIGHT_BLUE)
+ item.collapsed = false
+ set_item_icon_by_state(item)
+
+
+func set_state_error(item: TreeItem) -> void:
+ item.set_meta(META_GDUNIT_STATE, STATE.ERROR)
+ item.set_custom_color(0, Color.ORANGE_RED)
+ item.set_custom_color(1, Color.ORANGE_RED)
+ set_item_icon_by_state(item)
+ item.collapsed = false
+
+
+func set_state_aborted(item: TreeItem) -> void:
+ item.set_meta(META_GDUNIT_STATE, STATE.ABORDED)
+ item.set_custom_color(0, Color.ORANGE_RED)
+ item.set_custom_color(1, Color.ORANGE_RED)
+ item.clear_custom_bg_color(0)
+ item.set_text(1, "(aborted)")
+ item.set_text_alignment(1, HORIZONTAL_ALIGNMENT_RIGHT)
+ set_item_icon_by_state(item)
+ item.collapsed = false
+
+
+func set_state_orphan(item: TreeItem, event: GdUnitEvent) -> void:
+ var orphan_count := event.statistic(GdUnitEvent.ORPHAN_NODES)
+ if orphan_count == 0:
+ return
+ if item.has_meta(META_GDUNIT_ORPHAN):
+ orphan_count += item.get_meta(META_GDUNIT_ORPHAN)
+ item.set_meta(META_GDUNIT_ORPHAN, orphan_count)
+ if item.get_meta(META_GDUNIT_STATE) != STATE.FAILED:
+ item.set_custom_color(0, Color.YELLOW)
+ item.set_custom_color(1, Color.YELLOW)
+ item.set_tooltip_text(0, "Total <%d> orphan nodes detected." % orphan_count)
+ set_item_icon_by_state(item)
+
+
+func update_state(item: TreeItem, event: GdUnitEvent, add_reports := true) -> void:
+ # we do not show the root
+ if item == null:
+ return
+
+ if event.is_skipped():
+ set_state_skipped(item)
+ elif event.is_success() and event.is_flaky():
+ set_state_flaky(item, event)
+ elif event.is_success():
+ set_state_succeded(item)
+ elif event.is_error():
+ set_state_error(item)
+ elif event.is_failed():
+ set_state_failed(item, event)
+ elif event.is_warning():
+ set_state_warnings(item)
+ if add_reports:
+ for report in event.reports():
+ add_report(item, report)
+ set_state_orphan(item, event)
+
+ var parent := item.get_parent()
+ if parent == null:
+ return
+
+ var item_state: int = item.get_meta(META_GDUNIT_STATE)
+ var parent_state: int = parent.get_meta(META_GDUNIT_STATE)
+ if item_state <= parent_state:
+ return
+ update_state(item.get_parent(), event, false)
+
+
+func add_report(item: TreeItem, report: GdUnitReport) -> void:
+ var reports: Array[GdUnitReport] = []
+ if item.has_meta(META_GDUNIT_REPORT):
+ reports = get_item_reports(item)
+ reports.append(report)
+ item.set_meta(META_GDUNIT_REPORT, reports)
+
+
+func abort_running(items:=_tree_root.get_children()) -> void:
+ for item in items:
+ if item.get_icon(0) == ICON_SPINNER:
+ set_state_aborted(item)
+ abort_running(item.get_children())
+
+
+func _on_select_next_item_by_state(item_state: int) -> TreeItem:
+ var current_selected := _tree.get_selected()
+ # If nothing is selected, the first error is selected or the next one in the vicinity of the current selection is found
+ current_selected = _find_first_item_by_state(_tree_root, item_state) if current_selected == null else _find_item_by_state(current_selected, item_state)
+ # If no next failure found, then we try to select first
+ if current_selected == null:
+ current_selected = _find_first_item_by_state(_tree_root, item_state)
+ return select_item(current_selected)
+
+
+func _on_select_previous_item_by_state(item_state: int) -> TreeItem:
+ var current_selected := _tree.get_selected()
+ # If nothing is selected, the first error is selected or the next one in the vicinity of the current selection is found
+ current_selected = _find_last_item_by_state(_tree_root, item_state) if current_selected == null else _find_item_by_state(current_selected, item_state, true)
+ # If no next failure found, then we try to select first last
+ if current_selected == null:
+ current_selected = _find_last_item_by_state(_tree_root, item_state)
+ return select_item(current_selected)
+
+
+func select_first_orphan() -> void:
+ for parent in _tree_root.get_children():
+ if not is_state_success(parent):
+ for item in parent.get_children():
+ if is_item_state_orphan(item):
+ parent.set_collapsed(false)
+ @warning_ignore("return_value_discarded")
+ select_item(item)
+ return
+
+
+func clear_reports() -> void:
+ for child in _report_list.get_children():
+ _report_list.remove_child(child)
+ child.queue_free()
+
+
+func show_failed_report(selected_item: TreeItem) -> void:
+ clear_reports()
+ if selected_item == null or not selected_item.has_meta(META_GDUNIT_REPORT):
+ return
+ # add new reports
+ for report in get_item_reports(selected_item):
+ var reportNode: RichTextLabel = _report_template.duplicate()
+ _report_list.add_child(reportNode)
+ reportNode.push_color(Color.DARK_TURQUOISE)
+ reportNode.append_text(report.to_string())
+ reportNode.pop()
+ reportNode.visible = true
+
+
+func update_test_suite(event: GdUnitEvent) -> void:
+ var item := _find_tree_item_by_test_suite(_tree_root, event.resource_path(), event.suite_name())
+ if not item:
+ push_error("[InspectorTreeMainPanel#update_test_suite] Internal Error: Can't find test suite item '{_suite_name}' for {_resource_path} ".format(event))
+ return
+ if event.type() == GdUnitEvent.TESTSUITE_AFTER:
+ update_item_elapsed_time_counter(item, event.elapsed_time())
+ update_state(item, event)
+ set_state_running(item, false)
+
+
+func update_test_case(event: GdUnitEvent) -> void:
+ var item := _find_tree_item_by_id(_tree_root, event.guid())
+ if not item:
+ #push_error("Internal Error: Can't find test id %s" % [event.guid()])
+ return
+ if event.type() == GdUnitEvent.TESTCASE_BEFORE:
+ set_state_running(item, true)
+ # force scrolling to current test case
+ _tree.scroll_to_item(item, true)
+ return
+
+ if event.type() == GdUnitEvent.TESTCASE_AFTER:
+ update_item_elapsed_time_counter(item, event.elapsed_time())
+ if event.is_success() or event.is_warning():
+ update_item_processed_counter(item)
+ update_state(item, event)
+ update_progress_counters(item)
+
+
+func create_item(parent: TreeItem, test: GdUnitTestCase, item_name: String, type: GdUnitType) -> TreeItem:
+ var item := _tree.create_item(parent)
+ item.collapsed = true
+ item.set_meta(META_GDUNIT_ORIGINAL_INDEX, item.get_index())
+ item.set_text(0, item_name)
+ match type:
+ GdUnitType.TEST_CASE:
+ item.set_meta(META_TEST_CASE, test)
+ GdUnitType.TEST_GROUP:
+ # We need to create a copy of the test record meta with a new uniqe guid
+ item.set_meta(META_TEST_CASE, GdUnitTestCase.from(test.suite_resource_path, test.source_file, test.line_number, test.test_name))
+ GdUnitType.TEST_SUITE:
+ # We need to create a copy of the test record meta with a new uniqe guid
+ item.set_meta(META_TEST_CASE, GdUnitTestCase.from(test.suite_resource_path, test.source_file, test.line_number, test.suite_name))
+
+ item.set_meta(META_GDUNIT_NAME, item_name)
+ set_state_initial(item, type)
+ update_item_total_counter(item)
+ return item
+
+
+func set_item_icon_by_state(item :TreeItem) -> void:
+ if item == _tree_root:
+ return
+ var state :STATE = item.get_meta(META_GDUNIT_STATE)
+ var is_orphan := is_item_state_orphan(item)
+ var resource_path := get_item_source_file(item)
+ item.set_icon(0, get_icon_by_file_type(resource_path, state, is_orphan))
+ if item.get_meta(META_GDUNIT_TYPE) == GdUnitType.FOLDER:
+ item.set_icon_modulate(0, Color.SKY_BLUE)
+
+
+func update_item_total_counter(item: TreeItem) -> void:
+ if item == null:
+ return
+
+ var child_count := get_total_child_count(item)
+ if child_count > 0:
+ item.set_meta(META_GDUNIT_PROGRESS_COUNT_MAX, child_count)
+ item.set_text(0, "(0/%d) %s" % [child_count, item.get_meta(META_GDUNIT_NAME)])
+
+ update_item_total_counter(item.get_parent())
+
+
+func get_total_child_count(item: TreeItem) -> int:
+ var total_count := 0
+ for child in item.get_children():
+ total_count += child.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX) if child.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX) else 1
+ return total_count
+
+
+func update_item_processed_counter(item: TreeItem, add_count := 1) -> void:
+ if item == _tree_root:
+ return
+
+ var success_count: int = item.get_meta(META_GDUNIT_SUCCESS_TESTS) + add_count
+ item.set_meta(META_GDUNIT_SUCCESS_TESTS, success_count)
+ if item.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX):
+ item.set_text(0, "(%d/%d) %s" % [success_count, item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX), item.get_meta(META_GDUNIT_NAME)])
+
+ update_item_processed_counter(item.get_parent(), add_count)
+
+
+func update_progress_counters(item: TreeItem) -> void:
+ var index: int = _tree_root.get_meta(META_GDUNIT_PROGRESS_INDEX) + 1
+ var total_test: int = _tree_root.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX)
+ var state: STATE = item.get_meta(META_GDUNIT_STATE)
+ test_counters_changed.emit(index, total_test, state)
+ _tree_root.set_meta(META_GDUNIT_PROGRESS_INDEX, index)
+
+
+func recalculate_counters(parent: TreeItem) -> void:
+ # Reset the counter first
+ if parent.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX):
+ parent.set_meta(META_GDUNIT_PROGRESS_COUNT_MAX, 0)
+ if parent.has_meta(META_GDUNIT_PROGRESS_INDEX):
+ parent.set_meta(META_GDUNIT_PROGRESS_INDEX, 0)
+ if parent.has_meta(META_GDUNIT_SUCCESS_TESTS):
+ parent.set_meta(META_GDUNIT_SUCCESS_TESTS, 0)
+
+ # Calculate new count based on children
+ var total_count := 0
+ var success_count := 0
+ var progress_index := 0
+
+ for child in parent.get_children():
+ if child.get_child_count() > 0:
+ # Recursively update child counters first
+ recalculate_counters(child)
+ # Add child's counters to parent
+ if child.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX):
+ total_count += child.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX)
+ if child.has_meta(META_GDUNIT_SUCCESS_TESTS):
+ success_count += child.get_meta(META_GDUNIT_SUCCESS_TESTS)
+ if child.has_meta(META_GDUNIT_PROGRESS_INDEX):
+ progress_index += child.get_meta(META_GDUNIT_PROGRESS_INDEX)
+ elif is_test_case(child):
+ # Count individual test cases
+ total_count += 1
+ # Count completed tests
+ if is_state_success(child) or is_state_warning(child) or is_state_failed(child) or is_state_error(child):
+ progress_index += 1
+ if is_state_success(child) or is_state_warning(child):
+ success_count += 1
+
+ # Update the counters
+ if total_count > 0:
+ parent.set_meta(META_GDUNIT_PROGRESS_COUNT_MAX, total_count)
+ parent.set_meta(META_GDUNIT_PROGRESS_INDEX, progress_index)
+ parent.set_meta(META_GDUNIT_SUCCESS_TESTS, success_count)
+
+ # Update the display text
+ parent.set_text(0, "(%d/%d) %s" % [success_count, total_count, parent.get_meta(META_GDUNIT_NAME)])
+
+
+func update_item_elapsed_time_counter(item: TreeItem, time: int) -> void:
+ item.set_text(1, "%s" % LocalTime.elapsed(time))
+ item.set_text_alignment(1, HORIZONTAL_ALIGNMENT_RIGHT)
+ item.set_meta(META_GDUNIT_EXECUTION_TIME, time)
+
+ var parent := item.get_parent()
+ if parent == _tree_root:
+ return
+ var elapsed_time :int = parent.get_meta(META_GDUNIT_EXECUTION_TIME) + time
+ var type :GdUnitType = item.get_meta(META_GDUNIT_TYPE)
+ match type:
+ GdUnitType.TEST_CASE:
+ return
+ GdUnitType.TEST_SUITE:
+ update_item_elapsed_time_counter(parent, elapsed_time)
+ #GdUnitType.FOLDER:
+ # update_item_elapsed_time_counter(parent, elapsed_time)
+
+
+func get_icon_by_file_type(path: String, state: STATE, orphans: bool) -> Texture2D:
+ if path.get_extension() == "gd":
+ match state:
+ STATE.INITIAL:
+ return ICON_GDSCRIPT_TEST_DEFAULT
+ STATE.SUCCESS:
+ return ICON_GDSCRIPT_TEST_SUCCESS_ORPHAN if orphans else ICON_GDSCRIPT_TEST_SUCCESS
+ STATE.ERROR:
+ return ICON_GDSCRIPT_TEST_ERRORS_ORPHAN if orphans else ICON_GDSCRIPT_TEST_ERROR
+ STATE.FAILED:
+ return ICON_GDSCRIPT_TEST_FAILED_ORPHAN if orphans else ICON_GDSCRIPT_TEST_FAILED
+ STATE.WARNING:
+ return ICON_GDSCRIPT_TEST_SUCCESS_ORPHAN if orphans else ICON_GDSCRIPT_TEST_DEFAULT
+ STATE.FLAKY:
+ return ICON_GDSCRIPT_TEST_SUCCESS_ORPHAN if orphans else ICON_GDSCRIPT_TEST_FLAKY
+ _:
+ return ICON_GDSCRIPT_TEST_DEFAULT
+ if path.get_extension() == "cs":
+ match state:
+ STATE.INITIAL:
+ return ICON_CSSCRIPT_TEST_DEFAULT
+ STATE.SUCCESS:
+ return ICON_CSSCRIPT_TEST_SUCCESS_ORPHAN if orphans else ICON_CSSCRIPT_TEST_SUCCESS
+ STATE.ERROR:
+ return ICON_CSSCRIPT_TEST_ERRORS_ORPHAN if orphans else ICON_CSSCRIPT_TEST_ERROR
+ STATE.FAILED:
+ return ICON_CSSCRIPT_TEST_FAILED_ORPHAN if orphans else ICON_CSSCRIPT_TEST_FAILED
+ STATE.WARNING:
+ return ICON_CSSCRIPT_TEST_SUCCESS_ORPHAN if orphans else ICON_CSSCRIPT_TEST_DEFAULT
+ _:
+ return ICON_CSSCRIPT_TEST_DEFAULT
+ match state:
+ STATE.INITIAL:
+ return ICON_FOLDER
+ STATE.ERROR:
+ return ICON_FOLDER
+ STATE.FAILED:
+ return ICON_FOLDER
+ _:
+ return ICON_FOLDER
+
+
+func on_test_case_discover_added(test_case: GdUnitTestCase) -> void:
+ var test_root_folder := GdUnitSettings.test_root_folder().replace("res://", "")
+ var fully_qualified_name := test_case.fully_qualified_name.trim_suffix(test_case.display_name)
+ var parts := fully_qualified_name.split(".", false)
+ parts.append(test_case.display_name)
+ # Skip tree structure until test root folder
+ var index := parts.find(test_root_folder)
+ if index != -1:
+ parts = parts.slice(index+1)
+
+ match _current_tree_view_mode:
+ GdUnitInspectorTreeConstants.TREE_VIEW_MODE.FLAT:
+ create_items_tree_mode_flat(test_case, parts)
+ GdUnitInspectorTreeConstants.TREE_VIEW_MODE.TREE:
+ create_items_tree_mode_tree(test_case, parts)
+
+
+func create_items_tree_mode_tree(test_case: GdUnitTestCase, parts: PackedStringArray) -> void:
+ var parent := _tree_root
+ var is_suite_assigned := false
+ var suite_name := test_case.suite_name.split(".")[-1]
+ for item_name in parts:
+ var next := _find_tree_item(parent, item_name)
+ if next != null:
+ parent = next
+ continue
+
+ if not is_suite_assigned and suite_name == item_name:
+ next = create_item(parent, test_case, item_name, GdUnitType.TEST_SUITE)
+ is_suite_assigned = true
+ elif item_name == test_case.display_name:
+ next = create_item(parent, test_case, item_name, GdUnitType.TEST_CASE)
+ # On grouped tests (parameterized tests)
+ elif item_name == test_case.test_name:
+ next = create_item(parent, test_case, item_name, GdUnitType.TEST_GROUP)
+ else:
+ next = create_item(parent, test_case, item_name, GdUnitType.FOLDER)
+ parent = next
+
+
+func create_items_tree_mode_flat(test_case: GdUnitTestCase, parts: PackedStringArray) -> void:
+ # All parts except the last two (suite name and test name/display name)
+ var slice_index := -2 if parts[-1] == test_case.test_name else -3
+ var path_parts := parts.slice(0, slice_index)
+ var folder_path := "/".join(path_parts)
+
+ # Find or create flat folder
+ var folder_item: TreeItem
+ if folder_path.is_empty():
+ folder_item = _tree_root
+ else:
+ folder_item = _find_tree_item(_tree_root, folder_path)
+ if folder_item == null:
+ folder_item = create_item(_tree_root, test_case, folder_path, GdUnitType.FOLDER)
+
+ # Find suite under the flat folder (second to last part)
+ var suite_item := _find_tree_item(folder_item, test_case.suite_name)
+ if suite_item == null:
+ suite_item = create_item(folder_item, test_case, test_case.suite_name, GdUnitType.TEST_SUITE)
+
+ # Add test case or group under the suite
+ if test_case.test_name != test_case.display_name:
+ # It's a parameterized test group
+ var group_item := _find_tree_item(suite_item, test_case.test_name)
+ if group_item == null:
+ group_item = create_item(suite_item, test_case, test_case.test_name, GdUnitType.TEST_GROUP)
+ create_item(group_item, test_case, test_case.display_name, GdUnitType.TEST_CASE)
+ else:
+ create_item(suite_item, test_case, test_case.display_name, GdUnitType.TEST_CASE)
+
+
+func on_test_case_discover_deleted(test_case: GdUnitTestCase) -> void:
+ var item := _find_tree_item_by_id(_tree_root, test_case.guid)
+ if item != null:
+ var parent := item.get_parent()
+ parent.remove_child(item)
+
+ # update the cached counters
+ var item_success_count: int = item.get_meta(META_GDUNIT_SUCCESS_TESTS)
+ var item_total_test_count: int = item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX, 0)
+ var total_test_count: int = parent.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX, 0)
+ parent.set_meta(META_GDUNIT_PROGRESS_COUNT_MAX, total_test_count-item_total_test_count)
+
+ # propagate counter update to all parents
+ update_item_total_counter(parent)
+ update_item_processed_counter(parent, -item_success_count)
+
+
+func on_test_case_discover_modified(test_case: GdUnitTestCase) -> void:
+ var item := _find_tree_item_by_id(_tree_root, test_case.guid)
+ if item != null:
+ item.set_meta(META_TEST_CASE, test_case)
+ item.set_text(0, test_case.display_name)
+ item.set_meta(META_GDUNIT_NAME, test_case.display_name)
+
+
+func get_item_reports(item: TreeItem) -> Array[GdUnitReport]:
+ return item.get_meta(META_GDUNIT_REPORT)
+
+
+func get_item_test_line_number(item: TreeItem) -> int:
+ if item == null or not item.has_meta(META_TEST_CASE):
+ return -1
+
+ var test_case: GdUnitTestCase = item.get_meta(META_TEST_CASE)
+ return test_case.line_number
+
+
+func get_item_source_file(item: TreeItem) -> String:
+ if item == null or not item.has_meta(META_TEST_CASE):
+ return ""
+
+ var test_case: GdUnitTestCase = item.get_meta(META_TEST_CASE)
+ return test_case.source_file
+
+
+func get_item_type(item: TreeItem) -> GdUnitType:
+ if item == null or not item.has_meta(META_GDUNIT_TYPE):
+ return GdUnitType.FOLDER
+ return item.get_meta(META_GDUNIT_TYPE)
+
+
+func _dump_tree_as_json(dump_name: String) -> void:
+ var dict := _to_json(_tree_root)
+ var file := FileAccess.open("res://%s.json" % dump_name, FileAccess.WRITE)
+ file.store_string(JSON.stringify(dict, "\t"))
+
+
+func _to_json(parent :TreeItem) -> Dictionary:
+ var item_as_dict := GdObjects.obj2dict(parent)
+ item_as_dict["TreeItem"]["childrens"] = parent.get_children().map(func(item: TreeItem) -> Dictionary:
+ return _to_json(item))
+ return item_as_dict
+
+
+func extract_resource_path(event: GdUnitEvent) -> String:
+ return ProjectSettings.localize_path(event.resource_path())
+
+
+func collect_test_cases(item: TreeItem, tests: Array[GdUnitTestCase] = []) -> Array[GdUnitTestCase]:
+ for next in item.get_children():
+ collect_test_cases(next, tests)
+
+ if is_test_case(item):
+ var test: GdUnitTestCase = item.get_meta(META_TEST_CASE)
+ if not tests.has(test):
+ tests.append(test)
+
+ return tests
+
+
+func test_session_start() -> void:
+ _context_menu.disable_items()
+ reset_tree_state(_tree_root)
+ clear_reports()
+
+
+func test_session_stop() -> void:
+ _context_menu.enable_items()
+ abort_running()
+ sort_tree_items(_tree_root)
+ # wait until the tree redraw
+ await get_tree().process_frame
+ var failure_item := _find_first_item_by_state(_tree_root, STATE.FAILED)
+ select_item( failure_item if failure_item else _current_selected_item)
+
+
+################################################################################
+# Tree signal receiver
+################################################################################
+func _on_tree_item_mouse_selected(mouse_position: Vector2, mouse_button_index: int) -> void:
+ if mouse_button_index == MOUSE_BUTTON_RIGHT:
+ _context_menu.position = get_screen_position() + mouse_position
+ _context_menu.popup()
+
+
+func _on_Tree_item_selected() -> void:
+ _current_selected_item = _tree.get_selected()
+ show_failed_report(_current_selected_item)
+ tree_item_selected.emit(_current_selected_item)
+
+
+# Opens the test suite
+func _on_Tree_item_activated() -> void:
+ var selected_item := _tree.get_selected()
+ var line_number := get_item_test_line_number(selected_item)
+ if line_number != -1:
+ var script_path := ProjectSettings.localize_path(get_item_source_file(selected_item))
+ var resource: Script = load(script_path)
+
+ if selected_item.has_meta(META_GDUNIT_REPORT):
+ var reports := get_item_reports(selected_item)
+ var report_line_number := reports[0].line_number()
+ # if number -1 we use original stored line number of the test case
+ # in non debug mode the line number is not available
+ if report_line_number != -1:
+ line_number = report_line_number
+
+ EditorInterface.get_file_system_dock().navigate_to_path(script_path)
+ EditorInterface.edit_script(resource, line_number)
+ elif selected_item.get_meta(META_GDUNIT_TYPE) == GdUnitType.FOLDER:
+ # Toggle collapse if dir
+ selected_item.collapsed = not selected_item.collapsed
+
+
+################################################################################
+# external signal receiver
+################################################################################
+
+func _on_gdunit_event(event: GdUnitEvent) -> void:
+ match event.type():
+ GdUnitEvent.DISCOVER_START:
+ _tree_root.visible = false
+ _discover_hint.visible = true
+ init_tree()
+
+ GdUnitEvent.DISCOVER_END:
+ sort_tree_items(_tree_root)
+ select_item(_tree_root.get_first_child())
+ _discover_hint.visible = false
+ _tree_root.visible = true
+ #_dump_tree_as_json("tree_example_discovered")
+
+ GdUnitEvent.TESTCASE_BEFORE:
+ update_test_case(event)
+
+ GdUnitEvent.TESTCASE_AFTER:
+ update_test_case(event)
+
+ GdUnitEvent.TESTSUITE_BEFORE:
+ update_test_suite(event)
+
+ GdUnitEvent.TESTSUITE_AFTER:
+ update_test_suite(event)
+
+ GdUnitEvent.SESSION_START:
+ test_session_start()
+
+ GdUnitEvent.SESSION_CLOSE:
+ await test_session_stop()
+
+
+func _on_settings_changed(property :GdUnitProperty) -> void:
+ match property.name():
+ GdUnitSettings.INSPECTOR_TREE_SORT_MODE:
+ sort_tree_items(_tree_root)
+ #_dump_tree_as_json("tree_sorted_by_%s" % GdUnitInspectorTreeConstants.SORT_MODE.keys()[property.value()])
+
+ GdUnitSettings.INSPECTOR_TREE_VIEW_MODE:
+ restructure_tree(_tree_root, GdUnitSettings.get_inspector_tree_view_mode())
diff --git a/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd.uid b/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd.uid
new file mode 100644
index 0000000..587b139
--- /dev/null
+++ b/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd.uid
@@ -0,0 +1 @@
+uid://m0w5xmvdst0k
diff --git a/addons/gdUnit4/src/ui/parts/InspectorTreePanel.tscn b/addons/gdUnit4/src/ui/parts/InspectorTreePanel.tscn
new file mode 100644
index 0000000..2754f74
--- /dev/null
+++ b/addons/gdUnit4/src/ui/parts/InspectorTreePanel.tscn
@@ -0,0 +1,240 @@
+[gd_scene format=3 uid="uid://bqfpidewtpeg0"]
+
+[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd" id="1"]
+[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/menu/GdUnitInspectorContextMenu.tscn" id="2_o6s0p"]
+
+[sub_resource type="DPITexture" id="DPITexture_miuuy"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><path fill=\"#e0e0e0\" d=\"M4 12a1 1 0 0 0 1.555.832l6-4a1 1 0 0 0 0-1.664l-6-4A1 1 0 0 0 4 4z\"/></svg>
+"
+saturation = 2.0
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1)
+}
+
+[sub_resource type="DPITexture" id="DPITexture_ern2r"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><path fill=\"#e0e0e0\" d=\"M3 3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zm4.975 0A1 1 0 0 0 7 3.998v8a1 1 0 0 0 1.625.781l5-4a1 1 0 0 0 0-1.56l-5-4A1 1 0 0 0 7.975 3z\"/></svg>
+"
+saturation = 2.0
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1)
+}
+
+[sub_resource type="DPITexture" id="DPITexture_qdci2"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><path fill=\"#e0e0e0\" d=\"m8 9.669-3.536 2.583H7v2.537h2v-2.537h2.536zm0-3.314L4.464 3.772H7V1.235h2v2.537h2.536zm-7.296.73h14.591v1.831H.704z\"/></svg>
+"
+saturation = 2.0
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1)
+}
+
+[sub_resource type="DPITexture" id="DPITexture_hed0i"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><path fill=\"#e0e0e0\" d=\"m8 16-3.5-2.5H7V11h2v2.5h2.5zM8 0 4.5 2.5H7V5h2V2.5h2.5zM1 7h14v2H1z\"/></svg>
+"
+saturation = 2.0
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1)
+}
+
+[sub_resource type="DPITexture" id="DPITexture_8v04w"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><g fill=\"#e0e0e0\"><path d=\"M9 1v3.055a4 4 0 0 1 1.027.426l2.159-2.159A7 7 0 0 0 9 1z\"/><path fill-opacity=\".2\" d=\"M7 1.08A7 7 0 0 0 3.814 2.4l2.159 2.159A4 4 0 0 1 7 4.13V1.08zM2.4 3.814A7 7 0 0 0 1.078 7h3.055a4 4 0 0 1 .426-1.027L2.4 3.815zm11.2 0-2.159 2.159A4 4 0 0 1 11.869 7h3.05a7 7 0 0 0-1.32-3.186zM1.08 9a7 7 0 0 0 1.32 3.186l2.158-2.159A4 4 0 0 1 4.13 9H1.08zm10.786 0a4 4 0 0 1-.425 1.027l2.158 2.159A7 7 0 0 0 14.921 9h-3.055zm-5.894 2.441L3.814 13.6a7 7 0 0 0 3.185 1.322v-3.055a4 4 0 0 1-1.027-.426zm4.055 0a4 4 0 0 1-1.028.428v3.05a7 7 0 0 0 3.186-1.32l-2.158-2.158z\"/></g></svg>
+"
+saturation = 2.0
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1)
+}
+
+[sub_resource type="DPITexture" id="DPITexture_arwmg"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><g fill=\"#e0e0e0\"><path fill-opacity=\".2\" d=\"M9 1.078v3.055a4 4 0 0 1 1.027.426L12.186 2.4A7 7 0 0 0 9 1.078zM7 1.08A7 7 0 0 0 3.814 2.4l2.159 2.159A4 4 0 0 1 7 4.13V1.079zM2.4 3.814A7 7 0 0 0 1.078 7h3.055a4 4 0 0 1 .426-1.027L2.4 3.814zM1.08 9a7 7 0 0 0 1.32 3.185l2.159-2.158A4 4 0 0 1 4.131 9H1.08zm10.787 0a4 4 0 0 1-.426 1.027l2.159 2.158A7 7 0 0 0 14.922 9h-3.055zm-5.894 2.441L3.814 13.6A7 7 0 0 0 7 14.922v-3.055a4 4 0 0 1-1.027-.426zm4.054 0A4 4 0 0 1 9 11.869v3.05a7 7 0 0 0 3.186-1.32l-2.159-2.158z\"/><path d=\"m13.6 3.8-2.158 2.158a4 4 0 0 1 .428 1.028h3.05A7 7 0 0 0 13.6 3.8z\"/></g></svg>
+"
+saturation = 2.0
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1)
+}
+
+[sub_resource type="DPITexture" id="DPITexture_rqglq"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><g fill=\"#e0e0e0\"><path fill-opacity=\".2\" d=\"M9 1.078v3.055a4 4 0 0 1 1.027.426L12.186 2.4A7 7 0 0 0 9 1.078zM7 1.08A7 7 0 0 0 3.814 2.4l2.159 2.159A4 4 0 0 1 7 4.13V1.079zM2.4 3.814A7 7 0 0 0 1.078 7h3.055a4 4 0 0 1 .426-1.027L2.4 3.814zm11.2 0-2.159 2.159A4 4 0 0 1 11.869 7h3.05a7 7 0 0 0-1.32-3.186zM1.08 9a7 7 0 0 0 1.32 3.185l2.158-2.158A4 4 0 0 1 4.13 9H1.08zm4.892 2.441L3.814 13.6a7 7 0 0 0 3.185 1.322v-3.055a4 4 0 0 1-1.027-.426zm4.055 0a4 4 0 0 1-1.028.428v3.05a7 7 0 0 0 3.186-1.32l-2.158-2.158z\"/><path d=\"M11.867 9a4 4 0 0 1-.426 1.027l2.158 2.159A7 7 0 0 0 14.922 9h-3.055z\"/></g></svg>
+"
+saturation = 2.0
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1)
+}
+
+[sub_resource type="DPITexture" id="DPITexture_ltb1l"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><g fill=\"#e0e0e0\"><path fill-opacity=\".2\" d=\"M9 1.078v3.055a4 4 0 0 1 1.027.426L12.186 2.4A7 7 0 0 0 9 1.078zM7 1.08A7 7 0 0 0 3.814 2.4l2.159 2.159A4 4 0 0 1 7 4.13V1.079zM2.4 3.814A7 7 0 0 0 1.078 7h3.055a4 4 0 0 1 .426-1.027L2.4 3.814zm11.2 0-2.159 2.159A4 4 0 0 1 11.869 7h3.05a7 7 0 0 0-1.32-3.186zM1.08 9a7 7 0 0 0 1.32 3.185l2.158-2.158A4 4 0 0 1 4.13 9H1.08zm10.786 0a4 4 0 0 1-.425 1.027l2.158 2.158A7 7 0 0 0 14.921 9h-3.055zm-5.894 2.441L3.814 13.6a7 7 0 0 0 3.185 1.322v-3.055a4 4 0 0 1-1.027-.426z\"/><path d=\"M10.027 11.4A4 4 0 0 1 9 11.828v3.05a7 7 0 0 0 3.185-1.32z\"/></g></svg>
+"
+saturation = 2.0
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1)
+}
+
+[sub_resource type="DPITexture" id="DPITexture_2lq8w"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><g fill=\"#e0e0e0\"><path fill-opacity=\".2\" d=\"M9 1.078v3.055a4 4 0 0 1 1.027.426L12.186 2.4A7 7 0 0 0 9 1.078zM7 1.08A7 7 0 0 0 3.814 2.4l2.159 2.159A4 4 0 0 1 7 4.13V1.079zM2.4 3.814A7 7 0 0 0 1.078 7h3.055a4 4 0 0 1 .426-1.027L2.4 3.814zm11.2 0-2.159 2.159A4 4 0 0 1 11.869 7h3.05a7 7 0 0 0-1.32-3.186zM1.08 9a7 7 0 0 0 1.32 3.185l2.158-2.158A4 4 0 0 1 4.13 9H1.08zm10.786 0a4 4 0 0 1-.425 1.027l2.158 2.158A7 7 0 0 0 14.921 9h-3.055zm-1.84 2.441a4 4 0 0 1-1.027.428v3.05a7 7 0 0 0 3.186-1.32l-2.158-2.158z\"/><path d=\"m5.973 11.4-2.159 2.158A7 7 0 0 0 7 14.88v-3.054a4 4 0 0 1-1.027-.426z\"/></g></svg>
+"
+saturation = 2.0
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1)
+}
+
+[sub_resource type="DPITexture" id="DPITexture_kwwmp"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><g fill=\"#e0e0e0\"><path fill-opacity=\".2\" d=\"M9 1.078v3.055a4 4 0 0 1 1.027.426L12.186 2.4A7 7 0 0 0 9 1.078zM7 1.08A7 7 0 0 0 3.814 2.4l2.159 2.159A4 4 0 0 1 7 4.13V1.079zM2.4 3.814A7 7 0 0 0 1.078 7h3.055a4 4 0 0 1 .426-1.027L2.4 3.814zm11.2 0-2.159 2.159A4 4 0 0 1 11.869 7h3.05a7 7 0 0 0-1.32-3.186zM11.867 9a4 4 0 0 1-.426 1.027l2.158 2.158A7 7 0 0 0 14.922 9h-3.055zm-5.895 2.441L3.814 13.6A7 7 0 0 0 7 14.922v-3.055a4 4 0 0 1-1.028-.426zm4.055 0A4 4 0 0 1 9 11.869v3.05a7 7 0 0 0 3.185-1.32l-2.158-2.158z\"/><path d=\"M1.08 9a7 7 0 0 0 1.32 3.186l2.159-2.159A4 4 0 0 1 4.131 9H1.08z\"/></g></svg>
+"
+saturation = 2.0
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1)
+}
+
+[sub_resource type="DPITexture" id="DPITexture_t1rin"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><g fill=\"#e0e0e0\"><path fill-opacity=\".2\" d=\"M9 1.078v3.055a4 4 0 0 1 1.027.426L12.186 2.4A7 7 0 0 0 9 1.078zM7 1.08A7 7 0 0 0 3.814 2.4l2.159 2.159A4 4 0 0 1 7 4.13V1.079zm6.6 2.734-2.159 2.159A4 4 0 0 1 11.869 7h3.05a7 7 0 0 0-1.32-3.186zM1.08 9a7 7 0 0 0 1.32 3.185l2.158-2.158A4 4 0 0 1 4.13 9H1.08zm10.787 0a4 4 0 0 1-.426 1.027l2.158 2.158A7 7 0 0 0 14.921 9h-3.054zm-5.895 2.441L3.814 13.6a7 7 0 0 0 3.185 1.322v-3.055a4 4 0 0 1-1.027-.426zm4.055 0A4 4 0 0 1 9 11.869v3.05a7 7 0 0 0 3.185-1.32l-2.158-2.158z\"/><path d=\"M2.4 3.8a7 7 0 0 0-1.322 3.186h3.055a4 4 0 0 1 .426-1.028L2.4 3.8z\"/></g></svg>
+"
+saturation = 2.0
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1)
+}
+
+[sub_resource type="DPITexture" id="DPITexture_pi1kv"]
+_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><g fill=\"#e0e0e0\"><path fill-opacity=\".2\" d=\"M9 1.078v3.055a4 4 0 0 1 1.027.426L12.186 2.4A7 7 0 0 0 9 1.078zM2.4 3.814A7 7 0 0 0 1.078 7h3.055a4 4 0 0 1 .426-1.027L2.4 3.814zm11.2 0-2.159 2.159A4 4 0 0 1 11.869 7h3.05a7 7 0 0 0-1.32-3.186zM1.08 9a7 7 0 0 0 1.32 3.185l2.158-2.158A4 4 0 0 1 4.13 9H1.08zm10.786 0a4 4 0 0 1-.425 1.027l2.158 2.158A7 7 0 0 0 14.921 9h-3.055zm-5.894 2.441L3.814 13.6a7 7 0 0 0 3.185 1.322v-3.055a4 4 0 0 1-1.027-.426zm4.055 0a4 4 0 0 1-1.028.428v3.05a7 7 0 0 0 3.186-1.32l-2.158-2.158z\"/><path d=\"M7 1a7 7 0 0 0-3.186 1.32l2.159 2.159A4 4 0 0 1 7 4.05z\"/></g></svg>
+"
+saturation = 2.0
+color_map = {
+Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
+Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
+Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1)
+}
+
+[sub_resource type="AnimatedTexture" id="AnimatedTexture_eor1x"]
+frames = 8
+speed_scale = 2.5
+frame_0/texture = SubResource("DPITexture_8v04w")
+frame_0/duration = 0.2
+frame_1/texture = SubResource("DPITexture_arwmg")
+frame_1/duration = 0.2
+frame_2/texture = SubResource("DPITexture_rqglq")
+frame_2/duration = 0.2
+frame_3/texture = SubResource("DPITexture_ltb1l")
+frame_3/duration = 0.2
+frame_4/texture = SubResource("DPITexture_2lq8w")
+frame_4/duration = 0.2
+frame_5/texture = SubResource("DPITexture_kwwmp")
+frame_5/duration = 0.2
+frame_6/texture = SubResource("DPITexture_t1rin")
+frame_6/duration = 0.2
+frame_7/texture = SubResource("DPITexture_pi1kv")
+frame_7/duration = 0.2
+
+[node name="MainPanel" type="VSplitContainer" unique_id=1044515559]
+use_parent_material = true
+clip_contents = true
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_right = -924.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+split_offsets = PackedInt32Array(200)
+split_offset = 200
+script = ExtResource("1")
+
+[node name="contextMenu" parent="." unique_id=73764904 instance=ExtResource("2_o6s0p")]
+visible = false
+item_0/icon = SubResource("DPITexture_miuuy")
+item_1/icon = SubResource("DPITexture_ern2r")
+item_2/text = "Run Tests Until Fail"
+item_2/icon = SubResource("DPITexture_miuuy")
+item_4/icon = SubResource("DPITexture_qdci2")
+item_5/icon = SubResource("DPITexture_hed0i")
+
+[node name="Panel" type="PanelContainer" parent="." unique_id=641912870]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="Tree" type="Tree" parent="Panel" unique_id=741772884]
+use_parent_material = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/icon_max_width = 16
+columns = 2
+allow_reselect = true
+allow_rmb_select = true
+hide_root = true
+select_mode = 1
+
+[node name="discover_hint" type="HBoxContainer" parent="Panel" unique_id=555683311]
+unique_name_in_owner = true
+visible = false
+use_parent_material = true
+layout_mode = 2
+alignment = 1
+
+[node name="spinner" type="Button" parent="Panel/discover_hint" unique_id=200472082]
+unique_name_in_owner = true
+clip_contents = true
+custom_minimum_size = Vector2(64, 64)
+layout_mode = 2
+size_flags_stretch_ratio = 1.94
+disabled = true
+button_mask = 0
+text = "Discover Tests"
+icon = SubResource("AnimatedTexture_eor1x")
+flat = true
+alignment = 2
+
+[node name="report" type="PanelContainer" parent="." unique_id=503703488]
+clip_contents = true
+layout_mode = 2
+size_flags_horizontal = 11
+size_flags_vertical = 11
+
+[node name="report_template" type="RichTextLabel" parent="report" unique_id=817912164]
+auto_translate_mode = 2
+use_parent_material = true
+clip_contents = false
+layout_mode = 2
+size_flags_horizontal = 3
+localize_numeral_system = false
+focus_mode = 2
+bbcode_enabled = true
+fit_content = true
+selection_enabled = true
+
+[node name="ScrollContainer" type="ScrollContainer" parent="report" unique_id=1479075289]
+use_parent_material = true
+custom_minimum_size = Vector2(0, 80)
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 11
+
+[node name="list" type="VBoxContainer" parent="report/ScrollContainer" unique_id=1104634565]
+clip_contents = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[connection signal="item_activated" from="Panel/Tree" to="." method="_on_Tree_item_activated"]
+[connection signal="item_mouse_selected" from="Panel/Tree" to="." method="_on_tree_item_mouse_selected"]
+[connection signal="item_selected" from="Panel/Tree" to="." method="_on_Tree_item_selected"]
diff --git a/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd
new file mode 100644
index 0000000..b5cc837
--- /dev/null
+++ b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd
@@ -0,0 +1,56 @@
+@tool
+class_name GdUnitInputCapture
+extends Control
+
+signal input_completed(input_event: InputEventKey)
+
+
+var _tween: Tween
+var _input_event: InputEventKey
+
+
+func _ready() -> void:
+ reset()
+ self_modulate = Color.WHITE
+ _tween = create_tween()
+ @warning_ignore("return_value_discarded")
+ _tween.set_loops()
+ @warning_ignore("return_value_discarded")
+ _tween.tween_property(%Label, "self_modulate", Color(1, 1, 1, .8), 1.0).from_current().set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_IN_OUT)
+
+
+func reset() -> void:
+ _input_event = InputEventKey.new()
+
+
+func _input(event: InputEvent) -> void:
+ if not is_visible_in_tree():
+ return
+ if event is InputEventKey and event.is_pressed() and not event.is_echo():
+ var _event := event as InputEventKey
+ match _event.keycode:
+ KEY_CTRL:
+ _input_event.ctrl_pressed = true
+ KEY_SHIFT:
+ _input_event.shift_pressed = true
+ KEY_ALT:
+ _input_event.alt_pressed = true
+ KEY_META:
+ _input_event.meta_pressed = true
+ _:
+ _input_event.keycode = _event.keycode
+ _apply_input_modifiers(_event)
+ accept_event()
+
+ if event is InputEventKey and not event.is_pressed():
+ input_completed.emit(_input_event)
+ hide()
+
+
+func _apply_input_modifiers(event: InputEvent) -> void:
+ if event is InputEventWithModifiers:
+ var _event := event as InputEventWithModifiers
+ _input_event.meta_pressed = _event.meta_pressed or _input_event.meta_pressed
+ _input_event.alt_pressed = _event.alt_pressed or _input_event.alt_pressed
+ _input_event.shift_pressed = _event.shift_pressed or _input_event.shift_pressed
+ _input_event.ctrl_pressed = _event.ctrl_pressed or _input_event.ctrl_pressed
diff --git a/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd.uid b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd.uid
new file mode 100644
index 0000000..4b9116e
--- /dev/null
+++ b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd.uid
@@ -0,0 +1 @@
+uid://b6rgevbcd0k5n
diff --git a/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn
new file mode 100644
index 0000000..e656bf8
--- /dev/null
+++ b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn
@@ -0,0 +1,36 @@
+[gd_scene format=3 uid="uid://pmnkxrhglak5"]
+
+[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd" id="1_gki1u"]
+
+[node name="GdUnitInputMapper" type="Control"]
+modulate = Color(0.929099, 0.929099, 0.929099, 0.936189)
+top_level = true
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+script = ExtResource("1_gki1u")
+
+[node name="Label" type="Label" parent="."]
+unique_name_in_owner = true
+self_modulate = Color(0.401913, 0.401913, 0.401913, 0.461723)
+top_level = true
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -60.5
+offset_top = -19.5
+offset_right = 60.5
+offset_bottom = 19.5
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_colors/font_color = Color(1, 1, 1, 1)
+theme_override_font_sizes/font_size = 26
+text = "Press keys for shortcut"
diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd
new file mode 100644
index 0000000..14a8bd5
--- /dev/null
+++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd
@@ -0,0 +1,327 @@
+@tool
+extends Window
+
+const EAXAMPLE_URL := "https://github.com/MikeSchulze/gdUnit4-examples/archive/refs/heads/master.zip"
+const GdUnitTools := preload ("res://addons/gdUnit4/src/core/GdUnitTools.gd")
+const GdUnitUpdateClient = preload ("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd")
+
+@onready var _update_client: GdUnitUpdateClient = $GdUnitUpdateClient
+@onready var _version_label: RichTextLabel = %version
+@onready var _btn_install: Button = %btn_install_examples
+@onready var _progress_bar: ProgressBar = %ProgressBar
+@onready var _progress_text: Label = %progress_lbl
+@onready var _properties_template: Control = $property_template
+@onready var _properties_common: Control = % "common-content"
+@onready var _properties_ui: Control = % "ui-content"
+@onready var _properties_shortcuts: Control = % "shortcut-content"
+@onready var _properties_report: Control = % "report-content"
+@onready var _input_capture: GdUnitInputCapture = %GdUnitInputCapture
+@onready var _property_error: Window = % "propertyError"
+@onready var _tab_container: TabContainer = %Properties
+@onready var _update_tab: Control = %Update
+
+var _font_size: float
+
+
+func _ready() -> void:
+ set_name("GdUnitSettingsDialog")
+ # initialize for testing
+ if not Engine.is_editor_hint():
+ GdUnitSettings.setup()
+ GdUnit4Version.init_version_label(_version_label)
+ _font_size = GdUnitFonts.init_fonts(_version_label)
+ setup_properties(_properties_common, GdUnitSettings.COMMON_SETTINGS)
+ setup_properties(_properties_ui, GdUnitSettings.UI_SETTINGS)
+ setup_properties(_properties_report, GdUnitSettings.REPORT_SETTINGS)
+ setup_properties(_properties_shortcuts, GdUnitSettings.SHORTCUT_SETTINGS)
+ check_for_update()
+
+
+func _sort_by_key(left: GdUnitProperty, right: GdUnitProperty) -> bool:
+ return left.name() < right.name()
+
+
+func setup_properties(properties_parent: Control, property_category: String) -> void:
+ # Do remove first potential previous added properties (could be happened when the dlg is opened at twice)
+ for child in properties_parent.get_children():
+ properties_parent.remove_child(child)
+
+ var category_properties := GdUnitSettings.list_settings(property_category)
+ # sort by key
+ category_properties.sort_custom(_sort_by_key)
+ var theme_ := Theme.new()
+ theme_.set_constant("h_separation", "GridContainer", 12)
+ var last_category := "!"
+ var min_size_overall := 0.0
+ var labels := []
+ var inputs := []
+ var info_labels := []
+ var grid: GridContainer = null
+ for p in category_properties:
+ var min_size_ := 0.0
+ var property: GdUnitProperty = p
+ var current_category := property.category()
+ if not grid or current_category != last_category:
+ grid = GridContainer.new()
+ grid.columns = 4
+ grid.theme = theme_
+
+ var sub_category: Control = _properties_template.get_child(3).duplicate()
+ var category_label: Label = sub_category.get_child(0)
+ category_label.text = current_category.capitalize()
+ sub_category.custom_minimum_size.y = _font_size + 16
+ properties_parent.add_child(sub_category)
+ properties_parent.add_child(grid)
+ last_category = current_category
+ # property name
+ var label: Label = _properties_template.get_child(0).duplicate()
+ label.text = _to_human_readable(property.name())
+ labels.append(label)
+ grid.add_child(label)
+
+ # property reset btn
+ var reset_btn: Button = _properties_template.get_child(1).duplicate()
+ reset_btn.icon = _get_btn_icon("Reload")
+ reset_btn.disabled = property.value() == property.default()
+ grid.add_child(reset_btn)
+
+ # property type specific input element
+ var input: Node = _create_input_element(property, reset_btn)
+ inputs.append(input)
+ grid.add_child(input)
+ @warning_ignore("return_value_discarded")
+ reset_btn.pressed.connect(_on_btn_property_reset_pressed.bind(property, input, reset_btn))
+ # property help text
+ var info: Label = _properties_template.get_child(2).duplicate()
+ info.text = property.help()
+ info_labels.append(info)
+ grid.add_child(info)
+ if min_size_overall < min_size_:
+ min_size_overall = min_size_
+
+ for controls: Array in [labels, inputs, info_labels]:
+ var _size: float = controls.map(func(c: Control) -> float: return c.size.x).max()
+ min_size_overall += _size
+ for control: Control in controls:
+ control.custom_minimum_size.x = _size
+ properties_parent.custom_minimum_size.x = min_size_overall
+
+
+func _create_input_element(property: GdUnitProperty, reset_btn: Button) -> Node:
+ if property.is_selectable_value():
+ var options := OptionButton.new()
+ options.alignment = HORIZONTAL_ALIGNMENT_CENTER
+ for value in property.value_set():
+ options.add_item(value)
+ options.item_selected.connect(_on_option_selected.bind(property, reset_btn))
+ options.select(property.int_value())
+ return options
+ if property.type() == TYPE_BOOL:
+ var check_btn := CheckButton.new()
+ check_btn.toggled.connect(_on_property_text_changed.bind(property, reset_btn))
+ check_btn.button_pressed = property.value()
+ return check_btn
+ if property.type() in [TYPE_INT, TYPE_STRING]:
+ var input := LineEdit.new()
+ input.text_changed.connect(_on_property_text_changed.bind(property, reset_btn))
+ input.set_context_menu_enabled(false)
+ input.set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER)
+ input.set_expand_to_text_length_enabled(true)
+ input.text = str(property.value())
+ return input
+ if property.type() == TYPE_PACKED_INT32_ARRAY:
+ var key_input_button := Button.new()
+ var value:PackedInt32Array = property.value()
+ key_input_button.text = to_shortcut(value)
+ key_input_button.pressed.connect(_on_shortcut_change.bind(key_input_button, property, reset_btn))
+ return key_input_button
+ return Control.new()
+
+
+func to_shortcut(keys: PackedInt32Array) -> String:
+ var input_event := InputEventKey.new()
+ for key in keys:
+ match key:
+ KEY_CTRL: input_event.ctrl_pressed = true
+ KEY_SHIFT: input_event.shift_pressed = true
+ KEY_ALT: input_event.alt_pressed = true
+ KEY_META: input_event.meta_pressed = true
+ _:
+ input_event.keycode = key as Key
+ return input_event.as_text()
+
+
+func to_keys(input_event: InputEventKey) -> PackedInt32Array:
+ var keys := PackedInt32Array()
+ if input_event.ctrl_pressed:
+ keys.append(KEY_CTRL)
+ if input_event.shift_pressed:
+ keys.append(KEY_SHIFT)
+ if input_event.alt_pressed:
+ keys.append(KEY_ALT)
+ if input_event.meta_pressed:
+ keys.append(KEY_META)
+ keys.append(input_event.keycode)
+ return keys
+
+
+func _to_human_readable(value: String) -> String:
+ return value.split("/")[-1].capitalize()
+
+
+func _get_btn_icon(p_name: String) -> Texture2D:
+ if not Engine.is_editor_hint():
+ var placeholder := PlaceholderTexture2D.new()
+ placeholder.size = Vector2(8, 8)
+ return placeholder
+ return GdUnitUiTools.get_icon(p_name)
+
+
+func _install_examples() -> void:
+ _init_progress(5)
+ update_progress("Downloading examples")
+ await get_tree().process_frame
+ var tmp_path := GdUnitFileAccess.create_temp_dir("download")
+ var zip_file := tmp_path + "/examples.zip"
+ var response: GdUnitUpdateClient.HttpResponse = await _update_client.request_zip_package(EAXAMPLE_URL, zip_file)
+ if response.status() != 200:
+ push_warning("Examples cannot be retrieved from GitHub! \n Error code: %d : %s" % [response.status(), response.response()])
+ update_progress("Install examples failed! Try it later again.")
+ await get_tree().create_timer(3).timeout
+ stop_progress()
+ return
+ # extract zip to tmp
+ update_progress("Install examples into project")
+ var result := GdUnitFileAccess.extract_zip(zip_file, "res://gdUnit4-examples/")
+ if result.is_error():
+ update_progress("Install examples failed! %s" % result.error_message())
+ await get_tree().create_timer(3).timeout
+ stop_progress()
+ return
+ update_progress("Refresh project")
+ await rescan()
+ await reimport("res://gdUnit4-examples/")
+
+ update_progress("Examples successfully installed")
+ await get_tree().create_timer(3).timeout
+ stop_progress()
+
+
+func rescan() -> void:
+ await get_tree().process_frame
+ var fs := EditorInterface.get_resource_filesystem()
+ fs.scan_sources()
+ while fs.is_scanning():
+ await get_tree().create_timer(1).timeout
+
+
+func reimport(path: String) -> void:
+ await get_tree().process_frame
+ var files := DirAccess.get_files_at(path)
+ EditorInterface.get_resource_filesystem().reimport_files(files)
+ for directory in DirAccess.get_directories_at(path):
+ reimport(directory)
+
+
+func check_for_update() -> void:
+ if not GdUnitSettings.is_update_notification_enabled():
+ return
+ var response :GdUnitUpdateClient.HttpResponse = await _update_client.request_latest_version()
+ if response.status() != 200:
+ printerr("Latest version information cannot be retrieved from GitHub!")
+ printerr("Error: %s" % response.response())
+ return
+ var latest_version := _update_client.extract_latest_version(response)
+ if latest_version.is_greater(GdUnit4Version.current()):
+ var tab_index := _tab_container.get_tab_idx_from_control(_update_tab)
+ _tab_container.set_tab_button_icon(tab_index, GdUnitUiTools.get_icon("Notification", Color.YELLOW))
+ _tab_container.set_tab_tooltip(tab_index, "An new update is available.")
+
+
+func _on_btn_report_bug_pressed() -> void:
+ @warning_ignore("return_value_discarded")
+ OS.shell_open("https://github.com/MikeSchulze/gdUnit4/issues/new?assignees=MikeSchulze&labels=bug&projects=projects%2F5&template=bug_report.yml&title=GD-XXX%3A+Describe+the+issue+briefly")
+
+
+func _on_btn_request_feature_pressed() -> void:
+ @warning_ignore("return_value_discarded")
+ OS.shell_open("https://github.com/MikeSchulze/gdUnit4/issues/new?assignees=MikeSchulze&labels=enhancement&projects=&template=feature_request.md&title=")
+
+
+func _on_btn_install_examples_pressed() -> void:
+ _btn_install.disabled = true
+ await _install_examples()
+ _btn_install.disabled = false
+
+
+func _on_btn_close_pressed() -> void:
+ hide()
+
+
+func _on_btn_property_reset_pressed(property: GdUnitProperty, input: Node, reset_btn: Button) -> void:
+ if input is CheckButton:
+ var is_default_pressed: bool = property.default()
+ (input as CheckButton).button_pressed = is_default_pressed
+ elif input is LineEdit:
+ (input as LineEdit).text = str(property.default())
+ # we have to update manually for text input fields because of no change event is emited
+ _on_property_text_changed(property.default(), property, reset_btn)
+ elif input is OptionButton:
+ (input as OptionButton).select(0)
+ _on_option_selected(0, property, reset_btn)
+ elif input is Button:
+ var value: PackedInt32Array = property.default()
+ (input as Button).text = to_shortcut(value)
+ _on_property_text_changed(value, property, reset_btn)
+
+
+func _on_property_text_changed(new_value: Variant, property: GdUnitProperty, reset_btn: Button) -> void:
+ property.set_value(new_value)
+ reset_btn.disabled = property.value() == property.default()
+ var error: Variant = GdUnitSettings.update_property(property)
+ if error:
+ var label: Label = _property_error.get_child(0) as Label
+ label.set_text(str(error))
+ var control := gui_get_focus_owner()
+ _property_error.show()
+ if control != null:
+ _property_error.position = control.global_position + Vector2(self.position) + Vector2(40, 40)
+
+
+func _on_option_selected(index: int, property: GdUnitProperty, reset_btn: Button) -> void:
+ property.set_value(index)
+ reset_btn.disabled = property.value() == property.default()
+ GdUnitSettings.update_property(property)
+
+
+func _on_shortcut_change(input_button: Button, property: GdUnitProperty, reset_btn: Button) -> void:
+ _input_capture.set_custom_minimum_size(_properties_shortcuts.get_size())
+ _input_capture.visible = true
+ _input_capture.show()
+ _properties_shortcuts.visible = false
+ set_process_input(false)
+ _input_capture.reset()
+ var input_event: InputEventKey = await _input_capture.input_completed
+ input_button.text = input_event.as_text()
+ _on_property_text_changed(to_keys(input_event), property, reset_btn)
+ _properties_shortcuts.visible = true
+ set_process_input(true)
+
+
+func _init_progress(max_value: int) -> void:
+ _progress_bar.visible = true
+ _progress_bar.max_value = max_value
+ _progress_bar.value = 0
+
+
+func _progress() -> void:
+ _progress_bar.value += 1
+
+
+func stop_progress() -> void:
+ _progress_bar.visible = false
+
+
+func update_progress(message: String) -> void:
+ _progress_text.text = message
+ _progress_bar.value += 1
diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd.uid b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd.uid
new file mode 100644
index 0000000..d92d2e4
--- /dev/null
+++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd.uid
@@ -0,0 +1 @@
+uid://eqhu8casglok
diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn
new file mode 100644
index 0000000..08afee4
--- /dev/null
+++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn
@@ -0,0 +1,349 @@
+[gd_scene load_steps=9 format=3]
+
+[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd" id="2"]
+[ext_resource type="Texture2D" path="res://addons/gdUnit4/src/ui/settings/logo.png" id="3_isfyl"]
+[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/templates/TestSuiteTemplate.tscn" id="4"]
+[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.tscn" id="4_nf72w"]
+[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn" id="5_n1jtv"]
+[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn" id="5_xu3j8"]
+[ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd" id="8_2ggr0"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hbbq5"]
+content_margin_left = 10.0
+content_margin_right = 10.0
+bg_color = Color(0.172549, 0.113725, 0.141176, 1)
+border_width_left = 4
+border_width_top = 4
+border_width_right = 4
+border_width_bottom = 4
+border_color = Color(0.87451, 0.0705882, 0.160784, 1)
+border_blend = true
+corner_radius_top_left = 8
+corner_radius_top_right = 8
+corner_radius_bottom_right = 8
+corner_radius_bottom_left = 8
+shadow_color = Color(0, 0, 0, 0.756863)
+shadow_size = 10
+shadow_offset = Vector2(10, 10)
+
+[node name="GdUnitSettingsDialog" type="Window"]
+disable_3d = true
+gui_embed_subwindows = true
+title = "GdUnit4 Settings"
+initial_position = 1
+size = Vector2i(1400, 600)
+visible = false
+wrap_controls = true
+exclusive = true
+extend_to_title = true
+script = ExtResource("2")
+
+[node name="property_template" type="Control" parent="."]
+visible = false
+layout_mode = 3
+anchors_preset = 0
+offset_left = 4.0
+offset_top = 4.0
+offset_right = 4.0
+offset_bottom = 4.0
+size_flags_horizontal = 0
+
+[node name="Label" type="Label" parent="property_template"]
+layout_mode = 1
+anchors_preset = 4
+anchor_top = 0.5
+anchor_bottom = 0.5
+offset_top = -11.5
+offset_right = 1.0
+offset_bottom = 11.5
+grow_vertical = 2
+
+[node name="btn_reset" type="Button" parent="property_template"]
+layout_mode = 0
+offset_right = 12.0
+offset_bottom = 40.0
+tooltip_text = "Reset to default value"
+clip_text = true
+
+[node name="info" type="Label" parent="property_template"]
+clip_contents = true
+layout_mode = 1
+anchors_preset = 4
+anchor_top = 0.5
+anchor_bottom = 0.5
+offset_top = -11.5
+offset_right = 316.0
+offset_bottom = 11.5
+grow_vertical = 2
+size_flags_horizontal = 3
+max_lines_visible = 1
+
+[node name="sub_category" type="Panel" parent="property_template"]
+layout_mode = 1
+anchors_preset = 10
+anchor_right = 1.0
+offset_right = -220.0
+grow_horizontal = 2
+size_flags_horizontal = 3
+
+[node name="Label" type="Label" parent="property_template/sub_category"]
+layout_mode = 1
+anchors_preset = 4
+anchor_top = 0.5
+anchor_bottom = 0.5
+offset_left = 4.0
+offset_top = -11.5
+offset_right = 5.0
+offset_bottom = 11.5
+grow_vertical = 2
+theme_override_colors/font_color = Color(0.439216, 0.45098, 1, 1)
+
+[node name="GdUnitUpdateClient" type="Node" parent="."]
+script = ExtResource("8_2ggr0")
+
+[node name="Panel" type="Panel" parent="."]
+use_parent_material = true
+clip_contents = true
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="PanelContainer" type="PanelContainer" parent="Panel"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="v" type="VBoxContainer" parent="Panel/PanelContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="MarginContainer" type="MarginContainer" parent="Panel/PanelContainer/v"]
+use_parent_material = true
+layout_mode = 2
+size_flags_vertical = 3
+theme_override_constants/margin_left = 4
+theme_override_constants/margin_top = 4
+theme_override_constants/margin_right = 4
+
+[node name="GridContainer" type="HBoxContainer" parent="Panel/PanelContainer/v/MarginContainer"]
+use_parent_material = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="PanelContainer" type="MarginContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer"]
+use_parent_material = true
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="Panel" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer"]
+use_parent_material = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="CenterContainer" type="CenterContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/Panel"]
+use_parent_material = true
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="logo" type="TextureRect" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/Panel/CenterContainer"]
+custom_minimum_size = Vector2(120, 120)
+layout_mode = 2
+size_flags_horizontal = 0
+size_flags_vertical = 4
+texture = ExtResource("3_isfyl")
+expand_mode = 1
+stretch_mode = 5
+
+[node name="CenterContainer2" type="MarginContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/Panel"]
+use_parent_material = true
+custom_minimum_size = Vector2(0, 30)
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="version" type="RichTextLabel" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/Panel/CenterContainer2"]
+unique_name_in_owner = true
+auto_translate_mode = 2
+use_parent_material = true
+clip_contents = false
+layout_mode = 2
+size_flags_horizontal = 3
+localize_numeral_system = false
+bbcode_enabled = true
+scroll_active = false
+meta_underlined = false
+
+[node name="VBoxContainer" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer"]
+custom_minimum_size = Vector2(200, 0)
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+alignment = 2
+
+[node name="btn_report_bug" type="Button" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+tooltip_text = "Press to create a bug report"
+text = "Report Bug"
+
+[node name="btn_request_feature" type="Button" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+tooltip_text = "Press to create a feature request"
+text = "Request Feature"
+
+[node name="btn_install_examples" type="Button" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+tooltip_text = "Press to install the advanced test examples"
+disabled = true
+text = "Install Examples"
+
+[node name="Properties" type="TabContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+current_tab = 0
+
+[node name="Common" type="ScrollContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties"]
+layout_mode = 2
+metadata/_tab_index = 0
+
+[node name="common-content" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties/Common"]
+unique_name_in_owner = true
+clip_contents = true
+custom_minimum_size = Vector2(1026, 0)
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="Hooks" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties" instance=ExtResource("4_nf72w")]
+visible = false
+layout_mode = 2
+
+[node name="UI" type="ScrollContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties"]
+visible = false
+layout_mode = 2
+metadata/_tab_index = 2
+
+[node name="ui-content" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties/UI"]
+unique_name_in_owner = true
+clip_contents = true
+custom_minimum_size = Vector2(741, 0)
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="Shortcuts" type="ScrollContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties"]
+visible = false
+layout_mode = 2
+metadata/_tab_index = 3
+
+[node name="shortcut-content" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties/Shortcuts"]
+unique_name_in_owner = true
+clip_contents = true
+custom_minimum_size = Vector2(683, 0)
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="Report" type="ScrollContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties"]
+visible = false
+layout_mode = 2
+metadata/_tab_index = 4
+
+[node name="report-content" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties/Report"]
+unique_name_in_owner = true
+clip_contents = true
+custom_minimum_size = Vector2(667, 0)
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="Templates" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties" instance=ExtResource("4")]
+visible = false
+layout_mode = 2
+metadata/_tab_index = 5
+
+[node name="Update" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties" instance=ExtResource("5_n1jtv")]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+metadata/_tab_index = 6
+
+[node name="GdUnitInputCapture" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties" instance=ExtResource("5_xu3j8")]
+unique_name_in_owner = true
+visible = false
+modulate = Color(1.54884e-09, 1.54884e-09, 1.54884e-09, 0.1)
+z_index = 1
+z_as_relative = false
+layout_mode = 2
+size_flags_horizontal = 1
+size_flags_vertical = 1
+
+[node name="propertyError" type="PopupPanel" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties"]
+unique_name_in_owner = true
+initial_position = 1
+size = Vector2i(400, 100)
+theme_override_styles/panel = SubResource("StyleBoxFlat_hbbq5")
+
+[node name="Label" type="Label" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties/propertyError"]
+offset_left = 10.0
+offset_top = 4.0
+offset_right = 390.0
+offset_bottom = 96.0
+theme_override_colors/font_color = Color(0.858824, 0, 0.109804, 1)
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="MarginContainer2" type="MarginContainer" parent="Panel/PanelContainer/v"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/margin_left = 4
+theme_override_constants/margin_right = 4
+theme_override_constants/margin_bottom = 4
+
+[node name="HBoxContainer" type="HBoxContainer" parent="Panel/PanelContainer/v/MarginContainer2"]
+layout_mode = 2
+size_flags_horizontal = 3
+alignment = 2
+
+[node name="ProgressBar" type="ProgressBar" parent="Panel/PanelContainer/v/MarginContainer2/HBoxContainer"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="progress_lbl" type="Label" parent="Panel/PanelContainer/v/MarginContainer2/HBoxContainer/ProgressBar"]
+unique_name_in_owner = true
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+clip_text = true
+
+[node name="btn_close" type="Button" parent="Panel/PanelContainer/v/MarginContainer2/HBoxContainer"]
+custom_minimum_size = Vector2(200, 0)
+layout_mode = 2
+text = "Close"
+
+[connection signal="close_requested" from="." to="." method="_on_btn_close_pressed"]
+[connection signal="pressed" from="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer/btn_report_bug" to="." method="_on_btn_report_bug_pressed"]
+[connection signal="pressed" from="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer/btn_request_feature" to="." method="_on_btn_request_feature_pressed"]
+[connection signal="pressed" from="Panel/PanelContainer/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer/btn_install_examples" to="." method="_on_btn_install_examples_pressed"]
+[connection signal="pressed" from="Panel/PanelContainer/v/MarginContainer2/HBoxContainer/btn_close" to="." method="_on_btn_close_pressed"]
diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd
new file mode 100644
index 0000000..3b01f34
--- /dev/null
+++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd
@@ -0,0 +1,255 @@
+@tool
+extends ScrollContainer
+
+
+@onready var _hooks_tree: Tree = %hooks_tree
+@onready var _hook_description: RichTextLabel = %hook_description
+@onready var _btn_move_up: Button = %hook_actions/btn_move_up
+@onready var _btn_move_down: Button = %hook_actions/btn_move_down
+@onready var _btn_delete: Button = %hook_actions/btn_delete_hook
+@onready var _select_hook_dlg: FileDialog = %select_hook_dlg
+@onready var _error_msg_popup :AcceptDialog = %error_msg_popup
+
+var _selected_hook_item: TreeItem = null
+var _root: TreeItem
+var _system_box_style: StyleBoxFlat
+var _priority_box_style: StyleBoxFlat
+
+func _ready() -> void:
+ _setup_styles()
+ _setup_buttons()
+ _setup_tree()
+ _load_registered_hooks()
+
+
+func _setup_styles() -> void:
+ _system_box_style = StyleBoxFlat.new()
+ _system_box_style.bg_color = Color(1.0, 0.76, 0.03, 1)
+ _system_box_style.corner_radius_top_left = 6
+ _system_box_style.corner_radius_top_right = 6
+ _system_box_style.corner_radius_bottom_left = 6
+ _system_box_style.corner_radius_bottom_right = 6
+ _priority_box_style = _system_box_style.duplicate()
+ _priority_box_style.bg_color = Color(0.26, 0.54, 0.89, 1)
+
+
+func _setup_buttons() -> void:
+ #if Engine.is_editor_hint():
+ # _btn_move_up.icon = GdUnitUiTools.get_icon("MoveUp")
+ # _btn_move_down.icon = GdUnitUiTools.get_icon("MoveDown")
+ # _btn_add.icon = GdUnitUiTools.get_icon("Add")
+ # _btn_delete.icon = GdUnitUiTools.get_icon("Remove")
+ pass
+
+
+func _setup_tree() -> void:
+ _hooks_tree.clear()
+ _root = _hooks_tree.create_item()
+ _hooks_tree.set_columns(2)
+ _hooks_tree.set_column_custom_minimum_width(1, 32)
+ _hooks_tree.set_column_expand(1, false)
+ _hooks_tree.set_hide_root(true)
+ _hooks_tree.set_hide_folding(true)
+ _hooks_tree.set_select_mode(Tree.SELECT_SINGLE)
+ _hooks_tree.item_selected.connect(_on_hook_selected)
+ _hooks_tree.item_edited.connect(_on_item_edited)
+
+
+func _load_registered_hooks() -> void:
+ var hook_service := GdUnitTestSessionHookService.instance()
+ for hook: GdUnitTestSessionHook in hook_service.enigne_hooks:
+ _create_hook_tree_item(hook)
+
+ # Select first item if any
+ if _root.get_child_count() > 0:
+ var first_item: TreeItem = _root.get_first_child()
+ first_item.select(0)
+ _on_hook_selected()
+
+
+func _create_hook_tree_item(hook: GdUnitTestSessionHook) -> TreeItem:
+ var item: TreeItem = _hooks_tree.create_item(_root)
+ item.set_custom_minimum_height(26)
+ # Column 0: Hook info with custom drawing
+ item.set_cell_mode(0, TreeItem.CELL_MODE_CUSTOM)
+ item.set_custom_draw_callback(0, _draw_hook_item)
+ item.set_editable(0, false)
+ item.set_metadata(0, hook)
+ # Column 1: Checkbox for enable/disable
+ item.set_cell_mode(1, TreeItem.CELL_MODE_CHECK)
+ item.set_checked(1, GdUnitTestSessionHookService.is_enabled(hook))
+ item.set_editable(1, true)
+ item.set_custom_bg_color(1, _hook_bg_color(hook))
+ item.set_tooltip_text(1, "Enable/Disable the Hook")
+ item.propagate_check(1)
+
+ if _is_system_hook(hook):
+ item.set_tooltip_text(0, "System hook - (Read-only)")
+ else:
+ item.set_tooltip_text(0, "User hook")
+ return item
+
+
+func _hook_bg_color(hook: GdUnitTestSessionHook) -> Color:
+ if _is_system_hook(hook):
+ return Color(0.133, 0.118, 0.090, 1) # Brownish background for system hooks
+ return Color(0.176, 0.196, 0.235, 1) # Dark background #2d3142
+
+
+func _draw_hook_item(item: TreeItem, rect: Rect2) -> void:
+ var hook := _get_hook(item)
+ var is_system := _is_system_hook(hook)
+ var is_selected := item == _selected_hook_item
+
+ # Draw background
+ var bg_color := _hook_bg_color(hook) # Dark background #2d3142
+ if is_selected:
+ bg_color = bg_color.lerp(Color(0.2, 0.4, 0.6, 0.3), 0.5) # Blue tint for selection
+ _hooks_tree.draw_rect(rect, bg_color)
+
+ # Draw left border for system hooks
+ if is_system:
+ var border_rect := Rect2(rect.position.x, rect.position.y, 3, rect.size.y)
+ _hooks_tree.draw_rect(border_rect, Color(1.0, 0.76, 0.03, 1)) # Yellow border
+
+ var font := _hooks_tree.get_theme_default_font()
+
+ # Draw hook name
+ var hook_name := hook.name
+ var text_pos := Vector2(rect.position.x + ( 15 if is_system else 12), rect.position.y + 18)
+ var text_color := Color(0.95, 0.95, 0.95, 1)
+ _hooks_tree.draw_string(font, text_pos, hook_name, HORIZONTAL_ALIGNMENT_LEFT, -1, 14, text_color)
+
+ # Draw system badge if needed
+ if is_system:
+ var badge_x := rect.position.x + rect.end.x - 100
+ var badge_y := rect.position.y + 14
+ var system_badge_rect := Rect2(badge_x, badge_y-8, 48, 16)
+ _hooks_tree.draw_style_box(_system_box_style, system_badge_rect)
+
+ var system_text_pos := Vector2(badge_x + 4, badge_y + 4)
+ var system_font_size := 10
+ _hooks_tree.draw_string(font, system_text_pos, "SYSTEM", HORIZONTAL_ALIGNMENT_CENTER, -1, system_font_size, Color(0.1, 0.1, 0.1, 1))
+
+
+func _create_hook_display_text(hook_name: String, priority: int, is_system: bool) -> String:
+ var text := hook_name + "\n"
+ text += "Priority: [color=#4299e1][bgcolor=#4299e1] " + str(priority) + " [/bgcolor][/color]"
+
+ if is_system:
+ text += " [color=#1a202c][bgcolor=#ffc107] SYSTEM [/bgcolor][/color]"
+
+ return text
+
+
+func _update_hook_description() -> void:
+ if _selected_hook_item == null:
+ _hook_description.text = "[i]Select a hook to view its description[/i]"
+ return
+ _hook_description.text = _get_hook(_selected_hook_item).description
+
+
+func _update_hook_buttons() -> void:
+ # Is nothing selected disable the move and delete buttons
+ if _selected_hook_item == null:
+ _btn_move_up.disabled = true
+ _btn_move_down.disabled = true
+ _btn_delete.disabled = true
+ return
+
+ var hook := _get_hook(_selected_hook_item)
+ var is_system := _is_system_hook(hook)
+
+ # Disable the move and delete buttons for system hooks by default
+ if is_system:
+ _btn_move_up.disabled = true
+ _btn_move_down.disabled = true
+ _btn_delete.disabled = true
+ return
+
+ var prev_item: TreeItem = _selected_hook_item.get_prev()
+ var next_item: TreeItem = _selected_hook_item.get_next()
+
+ if prev_item != null:
+ var prev_hook := _get_hook(prev_item)
+ _btn_move_up.disabled = _is_system_hook(prev_hook)
+
+ _btn_move_down.disabled = next_item == null
+ _btn_delete.disabled = false
+
+
+static func _get_hook(item: TreeItem) -> GdUnitTestSessionHook:
+ return item.get_metadata(0)
+
+
+static func _is_system_hook(hook: GdUnitTestSessionHook) -> bool:
+ if hook == null:
+ return false
+ return hook.get_meta("SYSTEM_HOOK")
+
+
+func _on_hook_selected() -> void:
+ _selected_hook_item = _hooks_tree.get_selected()
+ _update_hook_buttons()
+ _update_hook_description()
+
+
+func _on_item_edited() -> void:
+ var selected_hook_item := _hooks_tree.get_selected()
+ if selected_hook_item != null:
+ var hook := _get_hook(selected_hook_item)
+ var is_enabled := selected_hook_item.is_checked(1)
+ GdUnitTestSessionHookService.instance().enable_hook(hook, is_enabled)
+
+
+func _on_btn_add_hook_pressed() -> void:
+ _select_hook_dlg.show()
+
+
+func _on_select_hook_dlg_file_selected(path: String) -> void:
+ _select_hook_dlg.set_current_path(path)
+ _on_select_hook_dlg_confirmed()
+
+
+func _on_select_hook_dlg_confirmed() -> void:
+ _select_hook_dlg.hide()
+ var result := GdUnitTestSessionHookService.instance().load_hook(_select_hook_dlg.get_current_path())
+ if result.is_error():
+ _error_msg_popup.dialog_text = result.error_message()
+ _error_msg_popup.show()
+ return
+
+ var hook: GdUnitTestSessionHook = result.value()
+ result = GdUnitTestSessionHookService.instance().register(hook)
+ if result.is_error():
+ _error_msg_popup.dialog_text = result.error_message()
+ _error_msg_popup.show()
+ return
+
+ var hook_added := _create_hook_tree_item(hook)
+ _hooks_tree.set_selected(hook_added, 0)
+
+
+func _on_btn_delete_hook_pressed() -> void:
+ if _selected_hook_item != null:
+ _root.remove_child(_selected_hook_item)
+ GdUnitTestSessionHookService.instance()\
+ .unregister(_get_hook(_selected_hook_item))
+ _selected_hook_item = null
+ _update_hook_buttons()
+
+
+func _on_btn_move_up_pressed() -> void:
+ var prev := _selected_hook_item.get_prev()
+ _selected_hook_item.move_before(prev)
+ GdUnitTestSessionHookService.instance()\
+ .move_before(_get_hook(_selected_hook_item), _get_hook(prev))
+ _update_hook_buttons()
+
+
+func _on_btn_move_down_pressed() -> void:
+ var next := _selected_hook_item.get_next()
+ _selected_hook_item.move_after(next)
+ GdUnitTestSessionHookService.instance()\
+ .move_after(_get_hook(_selected_hook_item), _get_hook(next))
+ _update_hook_buttons()
diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd.uid b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd.uid
new file mode 100644
index 0000000..cbb5b86
--- /dev/null
+++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd.uid
@@ -0,0 +1 @@
+uid://cmclisg3m8k2n
diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.tscn b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.tscn
new file mode 100644
index 0000000..8eec309
--- /dev/null
+++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.tscn
@@ -0,0 +1,148 @@
+[gd_scene format=3 uid="uid://41l7a46fol5m"]
+
+[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd" id="1_8yffn"]
+
+[sub_resource type="Image" id="Image_h5sr5"]
+data = {
+"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 234, 234, 234, 12, 224, 224, 224, 255, 224, 224, 224, 255, 234, 234, 234, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 1, 225, 225, 225, 174, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 173, 255, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 123, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 122, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 74, 224, 224, 224, 253, 224, 224, 224, 253, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 253, 224, 224, 224, 253, 224, 224, 224, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 228, 228, 228, 37, 224, 224, 224, 240, 224, 224, 224, 255, 224, 224, 224, 122, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 123, 224, 224, 224, 255, 224, 224, 224, 239, 228, 228, 228, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 200, 224, 224, 224, 255, 224, 224, 224, 172, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 1, 224, 224, 224, 173, 224, 224, 224, 255, 225, 225, 225, 199, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 180, 224, 224, 224, 193, 234, 234, 234, 12, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 234, 234, 234, 12, 224, 224, 224, 193, 224, 224, 224, 179, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 180, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 181, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 180, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_77fm0"]
+image = SubResource("Image_h5sr5")
+
+[sub_resource type="Image" id="Image_77fm0"]
+data = {
+"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 181, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 180, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 181, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 181, 224, 224, 224, 193, 234, 234, 234, 12, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 234, 234, 234, 12, 224, 224, 224, 193, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 200, 224, 224, 224, 255, 224, 224, 224, 173, 255, 255, 255, 1, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 1, 225, 225, 225, 174, 224, 224, 224, 255, 225, 225, 225, 199, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 228, 228, 228, 37, 224, 224, 224, 239, 224, 224, 224, 255, 224, 224, 224, 122, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 123, 224, 224, 224, 255, 224, 224, 224, 239, 227, 227, 227, 36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 74, 224, 224, 224, 253, 224, 224, 224, 253, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 253, 224, 224, 224, 253, 224, 224, 224, 73, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 123, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 122, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 1, 224, 224, 224, 173, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 172, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 234, 234, 234, 12, 224, 224, 224, 255, 224, 224, 224, 255, 234, 234, 234, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_rewru"]
+image = SubResource("Image_77fm0")
+
+[sub_resource type="Image" id="Image_kppp6"]
+data = {
+"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_manhx"]
+image = SubResource("Image_kppp6")
+
+[sub_resource type="Image" id="Image_rewru"]
+data = {
+"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 227, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 225, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 73, 224, 224, 224, 226, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 225, 226, 226, 226, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+"format": "RGBA8",
+"height": 16,
+"mipmaps": false,
+"width": 16
+}
+
+[sub_resource type="ImageTexture" id="ImageTexture_4h4u1"]
+image = SubResource("Image_rewru")
+
+[node name="Hooks" type="ScrollContainer"]
+custom_minimum_size = Vector2(400, 300)
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+script = ExtResource("1_8yffn")
+metadata/_tab_index = 1
+
+[node name="HBoxContainer" type="HBoxContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="hooks_content" type="VBoxContainer" parent="HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="hooks_tree" type="Tree" parent="HBoxContainer/hooks_content"]
+unique_name_in_owner = true
+layout_direction = 2
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+hide_folding = true
+hide_root = true
+
+[node name="hook_description" type="RichTextLabel" parent="HBoxContainer/hooks_content"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(0, 120)
+layout_mode = 2
+size_flags_vertical = 2
+bbcode_enabled = true
+text = "The test result Html reporting hook."
+scroll_active = false
+
+[node name="hook_actions" type="VBoxContainer" parent="HBoxContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(80, 0)
+layout_mode = 2
+size_flags_horizontal = 0
+theme_override_constants/separation = 5
+
+[node name="btn_move_up" type="Button" parent="HBoxContainer/hook_actions"]
+layout_mode = 2
+tooltip_text = "Move hook up in priority"
+disabled = true
+icon = SubResource("ImageTexture_77fm0")
+icon_alignment = 1
+
+[node name="btn_move_down" type="Button" parent="HBoxContainer/hook_actions"]
+layout_mode = 2
+tooltip_text = "Move hook down in priority"
+disabled = true
+icon = SubResource("ImageTexture_rewru")
+icon_alignment = 1
+
+[node name="btn_add_hook" type="Button" parent="HBoxContainer/hook_actions"]
+layout_mode = 2
+tooltip_text = "Add new hook"
+icon = SubResource("ImageTexture_manhx")
+icon_alignment = 1
+
+[node name="btn_delete_hook" type="Button" parent="HBoxContainer/hook_actions"]
+layout_mode = 2
+tooltip_text = "Delete selected hook"
+disabled = true
+icon = SubResource("ImageTexture_4h4u1")
+icon_alignment = 1
+
+[node name="select_hook_dlg" type="FileDialog" parent="."]
+unique_name_in_owner = true
+disable_3d = true
+title = "Open a File"
+initial_position = 3
+current_screen = 0
+ok_button_text = "Open"
+file_mode = 0
+filters = PackedStringArray("*.gd")
+
+[node name="error_msg_popup" type="AcceptDialog" parent="."]
+unique_name_in_owner = true
+initial_position = 3
+current_screen = 0
+
+[connection signal="pressed" from="HBoxContainer/hook_actions/btn_move_up" to="." method="_on_btn_move_up_pressed"]
+[connection signal="pressed" from="HBoxContainer/hook_actions/btn_move_down" to="." method="_on_btn_move_down_pressed"]
+[connection signal="pressed" from="HBoxContainer/hook_actions/btn_add_hook" to="." method="_on_btn_add_hook_pressed"]
+[connection signal="pressed" from="HBoxContainer/hook_actions/btn_delete_hook" to="." method="_on_btn_delete_hook_pressed"]
+[connection signal="confirmed" from="select_hook_dlg" to="." method="_on_select_hook_dlg_confirmed"]
+[connection signal="file_selected" from="select_hook_dlg" to="." method="_on_select_hook_dlg_file_selected"]
diff --git a/addons/gdUnit4/src/ui/settings/logo.png b/addons/gdUnit4/src/ui/settings/logo.png
new file mode 100644
index 0000000..12de79f
--- /dev/null
+++ b/addons/gdUnit4/src/ui/settings/logo.png
Binary files differ
diff --git a/addons/gdUnit4/src/ui/settings/logo.png.import b/addons/gdUnit4/src/ui/settings/logo.png.import
new file mode 100644
index 0000000..0141c5e
--- /dev/null
+++ b/addons/gdUnit4/src/ui/settings/logo.png.import
@@ -0,0 +1,40 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bo6npdrbk02dk"
+path="res://.godot/imported/logo.png-deda0e4ba02a0b9e4e4a830029a5817f.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/gdUnit4/src/ui/settings/logo.png"
+dest_files=["res://.godot/imported/logo.png-deda0e4ba02a0b9e4e4a830029a5817f.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
diff --git a/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd b/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd
new file mode 100644
index 0000000..be5c352
--- /dev/null
+++ b/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd
@@ -0,0 +1,129 @@
+@tool
+extends MarginContainer
+
+@onready var _template_editor :CodeEdit = $VBoxContainer/EdiorLayout/Editor
+@onready var _tags_editor :CodeEdit = $Tags/MarginContainer/TextEdit
+@onready var _title_bar :Panel = $VBoxContainer/sub_category
+@onready var _save_button :Button = $VBoxContainer/Panel/HBoxContainer/Save
+@onready var _selected_type :OptionButton = $VBoxContainer/EdiorLayout/Editor/MarginContainer/HBoxContainer/SelectType
+@onready var _show_tags :PopupPanel = $Tags
+
+
+var gd_key_words :PackedStringArray = ["extends", "class_name", "const", "var", "onready", "func", "void", "pass"]
+var gdunit_key_words :PackedStringArray = ["GdUnitTestSuite", "before", "after", "before_test", "after_test"]
+var _selected_template :int
+
+
+func _ready() -> void:
+ setup_editor_colors()
+ setup_fonts()
+ setup_supported_types()
+ load_template(GdUnitTestSuiteTemplate.TEMPLATE_ID_GD)
+ setup_tags_help()
+
+
+func _notification(what :int) -> void:
+ if what == EditorSettings.NOTIFICATION_EDITOR_SETTINGS_CHANGED:
+ setup_fonts()
+
+
+func setup_editor_colors() -> void:
+ if not Engine.is_editor_hint():
+ return
+
+ var background_color := get_editor_color("text_editor/theme/highlighting/background_color", Color(0.1155, 0.132, 0.1595, 1))
+ var text_color := get_editor_color("text_editor/theme/highlighting/text_color", Color(0.8025, 0.81, 0.8225, 1))
+ var selection_color := get_editor_color("text_editor/theme/highlighting/selection_color", Color(0.44, 0.73, 0.98, 0.4))
+
+ for e :CodeEdit in [_template_editor, _tags_editor]:
+ var editor :CodeEdit = e
+ editor.add_theme_color_override("background_color", background_color)
+ editor.add_theme_color_override("font_color", text_color)
+ editor.add_theme_color_override("font_readonly_color", text_color)
+ editor.add_theme_color_override("font_selected_color", selection_color)
+ setup_highlighter(editor)
+
+
+func setup_highlighter(editor :CodeEdit) -> void:
+ var highlighter := CodeHighlighter.new()
+ editor.set_syntax_highlighter(highlighter)
+ var number_color := get_editor_color("text_editor/theme/highlighting/number_color", Color(0.63, 1, 0.88, 1))
+ var symbol_color := get_editor_color("text_editor/theme/highlighting/symbol_color", Color(0.67, 0.79, 1, 1))
+ var function_color := get_editor_color("text_editor/theme/highlighting/function_color", Color(0.34, 0.7, 1, 1))
+ var member_variable_color := get_editor_color("text_editor/theme/highlighting/member_variable_color", Color(0.736, 0.88, 1, 1))
+ var comment_color := get_editor_color("text_editor/theme/highlighting/comment_color", Color(0.8025, 0.81, 0.8225, 0.5))
+ var keyword_color := get_editor_color("text_editor/theme/highlighting/keyword_color", Color(1, 0.44, 0.52, 1))
+ var base_type_color := get_editor_color("text_editor/theme/highlighting/base_type_color", Color(0.26, 1, 0.76, 1))
+ var annotation_color := get_editor_color("text_editor/theme/highlighting/gdscript/annotation_color", Color(1, 0.7, 0.45, 1))
+
+ highlighter.clear_color_regions()
+ highlighter.clear_keyword_colors()
+ highlighter.add_color_region("#", "", comment_color, true)
+ highlighter.add_color_region("${", "}", Color.YELLOW)
+ highlighter.add_color_region("'", "'", Color.YELLOW)
+ highlighter.add_color_region("\"", "\"", Color.YELLOW)
+ highlighter.number_color = number_color
+ highlighter.symbol_color = symbol_color
+ highlighter.function_color = function_color
+ highlighter.member_variable_color = member_variable_color
+ highlighter.add_keyword_color("@", annotation_color)
+ highlighter.add_keyword_color("warning_ignore", annotation_color)
+ for word in gd_key_words:
+ highlighter.add_keyword_color(word, keyword_color)
+ for word in gdunit_key_words:
+ highlighter.add_keyword_color(word, base_type_color)
+
+
+## Using this function to avoid null references to colors on inital Godot installations.
+## For more details show https://github.com/MikeSchulze/gdUnit4/issues/533
+func get_editor_color(property_name: String, default: Color) -> Color:
+ var settings := EditorInterface.get_editor_settings()
+ return settings.get_setting(property_name) if settings.has_setting(property_name) else default
+
+
+func setup_fonts() -> void:
+ if _template_editor:
+ @warning_ignore("return_value_discarded")
+ GdUnitFonts.init_fonts(_template_editor)
+ var font_size := GdUnitFonts.init_fonts(_tags_editor)
+ _title_bar.size.y = font_size + 16
+ _title_bar.custom_minimum_size.y = font_size + 16
+
+
+func setup_supported_types() -> void:
+ _selected_type.clear()
+ _selected_type.add_item("GD - GDScript", GdUnitTestSuiteTemplate.TEMPLATE_ID_GD)
+ _selected_type.add_item("C# - CSharpScript", GdUnitTestSuiteTemplate.TEMPLATE_ID_CS)
+
+
+func setup_tags_help() -> void:
+ _tags_editor.set_text(GdUnitTestSuiteTemplate.load_tags(_selected_template))
+
+
+func load_template(template_id :int) -> void:
+ _selected_template = template_id
+ _template_editor.set_text(GdUnitTestSuiteTemplate.load_template(template_id))
+
+
+func _on_Restore_pressed() -> void:
+ _template_editor.set_text(GdUnitTestSuiteTemplate.default_template(_selected_template))
+ GdUnitTestSuiteTemplate.reset_to_default(_selected_template)
+ _save_button.disabled = true
+
+
+func _on_Save_pressed() -> void:
+ GdUnitTestSuiteTemplate.save_template(_selected_template, _template_editor.get_text())
+ _save_button.disabled = true
+
+
+func _on_Tags_pressed() -> void:
+ _show_tags.popup_centered_ratio(.5)
+
+
+func _on_Editor_text_changed() -> void:
+ _save_button.disabled = false
+
+
+func _on_SelectType_item_selected(index :int) -> void:
+ load_template(_selected_type.get_item_id(index))
+ setup_tags_help()
diff --git a/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd.uid b/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd.uid
new file mode 100644
index 0000000..2c03cb1
--- /dev/null
+++ b/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd.uid
@@ -0,0 +1 @@
+uid://dh7k6amf26gro
diff --git a/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.tscn b/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.tscn
new file mode 100644
index 0000000..5ccc443
--- /dev/null
+++ b/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.tscn
@@ -0,0 +1,127 @@
+[gd_scene load_steps=2 format=3 uid="uid://dte0m2endcgtu"]
+
+[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd" id="1"]
+
+[node name="TestSuiteTemplate" type="MarginContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+script = ExtResource("1")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="sub_category" type="Panel" parent="VBoxContainer"]
+custom_minimum_size = Vector2(0, 30)
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Label" type="Label" parent="VBoxContainer/sub_category"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 4.0
+offset_right = 4.0
+offset_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+text = "Test Suite Template
+"
+
+[node name="EdiorLayout" type="VBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="Editor" type="CodeEdit" parent="VBoxContainer/EdiorLayout"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/EdiorLayout/Editor"]
+layout_mode = 1
+anchors_preset = 12
+anchor_top = 1.0
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_top = -31.0
+grow_horizontal = 2
+grow_vertical = 0
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/EdiorLayout/Editor/MarginContainer"]
+layout_mode = 2
+size_flags_vertical = 8
+alignment = 2
+
+[node name="Tags" type="Button" parent="VBoxContainer/EdiorLayout/Editor/MarginContainer/HBoxContainer"]
+layout_mode = 2
+tooltip_text = "Shows supported tags."
+text = "Supported Tags"
+
+[node name="SelectType" type="OptionButton" parent="VBoxContainer/EdiorLayout/Editor/MarginContainer/HBoxContainer"]
+layout_mode = 2
+tooltip_text = "Select the script type specific template."
+item_count = 2
+selected = 0
+popup/item_0/text = "GD - GDScript"
+popup/item_0/id = 1000
+popup/item_1/text = "C# - CSharpScript"
+popup/item_1/id = 2000
+
+[node name="Panel" type="MarginContainer" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/Panel"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+alignment = 2
+
+[node name="Restore" type="Button" parent="VBoxContainer/Panel/HBoxContainer"]
+layout_mode = 2
+text = "Restore"
+
+[node name="Save" type="Button" parent="VBoxContainer/Panel/HBoxContainer"]
+layout_mode = 2
+disabled = true
+text = "Save"
+
+[node name="Tags" type="PopupPanel" parent="."]
+size = Vector2i(300, 100)
+unresizable = false
+content_scale_aspect = 4
+
+[node name="MarginContainer" type="MarginContainer" parent="Tags"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 4.0
+offset_top = 4.0
+offset_right = -856.0
+offset_bottom = -552.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="TextEdit" type="CodeEdit" parent="Tags/MarginContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+editable = false
+context_menu_enabled = false
+shortcut_keys_enabled = false
+virtual_keyboard_enabled = false
+
+[connection signal="text_changed" from="VBoxContainer/EdiorLayout/Editor" to="." method="_on_Editor_text_changed"]
+[connection signal="pressed" from="VBoxContainer/EdiorLayout/Editor/MarginContainer/HBoxContainer/Tags" to="." method="_on_Tags_pressed"]
+[connection signal="item_selected" from="VBoxContainer/EdiorLayout/Editor/MarginContainer/HBoxContainer/SelectType" to="." method="_on_SelectType_item_selected"]
+[connection signal="pressed" from="VBoxContainer/Panel/HBoxContainer/Restore" to="." method="_on_Restore_pressed"]
+[connection signal="pressed" from="VBoxContainer/Panel/HBoxContainer/Save" to="." method="_on_Save_pressed"]