fix: 修复聊天系统编译错误

- 修复 WebSocketManager/SocketIOClient 函数缩进错误
- 重命名 is_connected() 避免与 Object 基类冲突
- 修复 tscn 文件多余前导空格
- 修复测试文件 GUT 断言函数调用
- 添加 GUT 测试框架
This commit is contained in:
WhaleTown Developer
2026-01-08 00:11:12 +08:00
parent 16f24ab26f
commit c8e73bec59
255 changed files with 21876 additions and 91 deletions

View File

@@ -0,0 +1,13 @@
[gd_resource type="Theme" load_steps=3 format=3 uid="uid://dssgvu257o1si"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_u716c"]
bg_color = Color(0.43137255, 0.8784314, 0.6156863, 0.5254902)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ht2pf"]
bg_color = Color(0, 0.44705883, 0.23921569, 1)
[resource]
Button/colors/font_hover_pressed_color = Color(1, 1, 1, 1)
Button/colors/font_pressed_color = Color(1, 1, 1, 1)
Button/styles/hover = SubResource("StyleBoxFlat_u716c")
Button/styles/pressed = SubResource("StyleBoxFlat_ht2pf")

View File

@@ -0,0 +1,543 @@
@tool
extends Control
var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')
var GutConfigGui = load('res://addons/gut/gui/gut_config_gui.gd')
var AboutWindow = load("res://addons/gut/gui/about.tscn")
var _interface = null;
var _is_running = false :
set(val):
_is_running = val
_disable_run_buttons(_is_running)
var _gut_config = load('res://addons/gut/gut_config.gd').new()
var _gut_config_gui = null
var _gut_plugin = null
var _light_color = Color(0, 0, 0, .5) :
set(val):
_light_color = val
if(is_inside_tree()):
_ctrls.light.queue_redraw()
var _panel_button = null
var _user_prefs = null
var _shell_out_panel = null
var menu_manager = null :
set(val):
menu_manager = val
if(val != null):
_apply_shortcuts()
menu_manager.toggle_windowed.connect(_on_toggle_windowed)
menu_manager.about.connect(show_about)
menu_manager.run_all.connect(_run_all)
menu_manager.show_gut.connect(_on_show_gut)
@onready var _ctrls = {
about = %ExtraButtons/About,
light = %StatusIndicator,
output_button = %ExtraButtons/OutputBtn,
run_button = $layout/ControlBar/RunAll,
run_externally_dialog = $ShellOutOptions,
run_mode = %ExtraButtons/RunMode,
run_at_cursor = $layout/ControlBar/RunAtCursor,
run_results_button = %ExtraButtons/RunResultsBtn,
settings = $layout/RSplit/sc/Settings,
settings_button = %ExtraButtons/Settings,
shortcut_dialog = $ShortcutDialog,
shortcuts_button = %ExtraButtons/Shortcuts,
results = {
bar = $layout/ControlBar2,
errors = %errors_value,
failing = %failing_value,
orphans = %orphans_value,
passing = %passing_value,
pending = %pending_value,
warnings = %warnings_value,
},
}
@onready var results_v_split = %VSplitResults
@onready var results_h_split = %HSplitResults
@onready var results_tree = %RunResults
@onready var results_text = %OutputText
@onready var make_floating_btn = %MakeFloating
func _ready():
if(get_parent() is SubViewport):
return
GutEditorGlobals.create_temp_directory()
_user_prefs = GutEditorGlobals.user_prefs
_gut_config_gui = GutConfigGui.new(_ctrls.settings)
_ctrls.results.bar.connect('draw', _on_results_bar_draw.bind(_ctrls.results.bar))
hide_settings(!_ctrls.settings_button.button_pressed)
_gut_config.load_options(GutEditorGlobals.editor_run_gut_config_path)
_gut_config_gui.set_options(_gut_config.options)
_ctrls.shortcuts_button.icon = get_theme_icon('Shortcut', 'EditorIcons')
_ctrls.settings_button.icon = get_theme_icon('Tools', 'EditorIcons')
_ctrls.run_results_button.icon = get_theme_icon('AnimationTrackGroup', 'EditorIcons') # Tree
_ctrls.output_button.icon = get_theme_icon('Font', 'EditorIcons')
make_floating_btn.icon = get_theme_icon("MakeFloating", 'EditorIcons')
make_floating_btn.text = ''
_ctrls.about.icon = get_theme_icon('Info', 'EditorIcons')
_ctrls.about.text = ''
_ctrls.run_mode.icon = get_theme_icon("ViewportSpeed", 'EditorIcons')
results_tree.set_output_control(results_text)
var check_import = load('res://addons/gut/images/HSplitContainer.svg')
if(check_import == null):
results_tree.add_centered_text("GUT got some new images that are not imported yet. Please restart Godot.")
print('GUT got some new images that are not imported yet. Please restart Godot.')
else:
results_tree.add_centered_text("Let's run some tests!")
_ctrls.run_externally_dialog.load_from_file()
_apply_options_to_controls()
results_vert_layout()
func _process(_delta):
if(_is_running):
if(_ctrls.run_externally_dialog.should_run_externally()):
if(!is_instance_valid(_shell_out_panel)):
_is_running = false
show_me()
elif(!_interface.is_playing_scene()):
_is_running = false
results_text.add_text("\ndone")
load_result_output()
show_me()
# ---------------
# Private
# ---------------
func _apply_options_to_controls():
hide_settings(_user_prefs.hide_settings.value)
hide_result_tree(_user_prefs.hide_result_tree.value)
hide_output_text(_user_prefs.hide_output_text.value)
results_tree.set_show_orphans(!_gut_config.options.hide_orphans)
var shell_dialog_size = _user_prefs.run_externally_options_dialog_size.value
if(shell_dialog_size != Vector2i(-1, -1)):
_ctrls.run_externally_dialog.size = Vector2i(shell_dialog_size)
if(_user_prefs.shortcuts_dialog_size.value != Vector2i(-1, -1)):
_ctrls.shortcut_dialog.size = _user_prefs.shortcuts_dialog_size.value
var mode_ind = 'Ed'
if(_ctrls.run_externally_dialog.run_mode == _ctrls.run_externally_dialog.RUN_MODE_BLOCKING):
mode_ind = 'ExB'
elif(_ctrls.run_externally_dialog.run_mode == _ctrls.run_externally_dialog.RUN_MODE_NON_BLOCKING):
mode_ind = 'ExN'
_ctrls.run_mode.text = mode_ind
_ctrls.run_at_cursor.apply_gut_config(_gut_config)
func _disable_run_buttons(should):
_ctrls.run_button.disabled = should
_ctrls.run_at_cursor.disabled = should
func _is_test_script(script):
var from = script.get_base_script()
while(from and from.resource_path != 'res://addons/gut/test.gd'):
from = from.get_base_script()
return from != null
func _show_errors(errs):
results_text.clear()
var text = "Cannot run tests, you have a configuration error:\n"
for e in errs:
text += str('* ', e, "\n")
text += "Check your settings ----->"
results_text.add_text(text)
hide_output_text(false)
hide_settings(false)
func _save_user_prefs():
_user_prefs.hide_settings.value = !_ctrls.settings_button.button_pressed
_user_prefs.hide_result_tree.value = !_ctrls.run_results_button.button_pressed
_user_prefs.hide_output_text.value = !_ctrls.output_button.button_pressed
_user_prefs.shortcuts_dialog_size.value = _ctrls.shortcut_dialog.size
_user_prefs.run_externally.value = _ctrls.run_externally_dialog.run_mode != _ctrls.run_externally_dialog.RUN_MODE_EDITOR
_user_prefs.run_externally_options_dialog_size.value = _ctrls.run_externally_dialog.size
_user_prefs.save_it()
func _save_config():
_save_user_prefs()
_gut_config.options = _gut_config_gui.get_options(_gut_config.options)
var w_result = _gut_config.write_options(GutEditorGlobals.editor_run_gut_config_path)
if(w_result != OK):
push_error(str('Could not write options to ', GutEditorGlobals.editor_run_gut_config_path, ': ', w_result))
else:
_gut_config_gui.mark_saved()
func _run_externally():
_shell_out_panel = GutUtils.RunExternallyScene.instantiate()
_shell_out_panel.bottom_panel = self
_shell_out_panel.blocking_mode = _ctrls.run_externally_dialog.run_mode
_shell_out_panel.additional_arguments = _ctrls.run_externally_dialog.get_additional_arguments_array()
add_child(_shell_out_panel)
_shell_out_panel.run_tests()
func _run_tests():
show_me()
if(_is_running):
push_error("GUT: Cannot run tests, tests are already running.")
return
clear_results()
GutEditorGlobals.create_temp_directory()
_light_color = Color.BLUE
var issues = _gut_config_gui.get_config_issues()
if(issues.size() > 0):
_show_errors(issues)
return
write_file(GutEditorGlobals.editor_run_bbcode_results_path, 'Run in progress')
write_file(GutEditorGlobals.editor_run_json_results_path, '')
_save_config()
_apply_options_to_controls()
results_text.clear()
results_tree.clear()
results_tree.add_centered_text('Running...')
_is_running = true
results_text.add_text('Running...')
if(_ctrls.run_externally_dialog.should_run_externally()):
_gut_plugin.make_bottom_panel_item_visible(self)
_run_externally()
else:
_interface.play_custom_scene('res://addons/gut/gui/run_from_editor.tscn')
func _apply_shortcuts():
if(menu_manager != null):
menu_manager.apply_gut_shortcuts(_ctrls.shortcut_dialog)
_ctrls.run_button.shortcut = \
_ctrls.shortcut_dialog.scbtn_run_all.get_shortcut()
_ctrls.run_at_cursor.get_script_button().shortcut = \
_ctrls.shortcut_dialog.scbtn_run_current_script.get_shortcut()
_ctrls.run_at_cursor.get_inner_button().shortcut = \
_ctrls.shortcut_dialog.scbtn_run_current_inner.get_shortcut()
_ctrls.run_at_cursor.get_test_button().shortcut = \
_ctrls.shortcut_dialog.scbtn_run_current_test.get_shortcut()
# Took this out because it seems to break using the shortcut when docked.
# Though it does allow the shortcut to work when windowed. Shortcuts
# are weird.
# make_floating_btn.shortcut = \
# _ctrls.shortcut_dialog.scbtn_windowed.get_shortcut()
if(_panel_button != null):
_panel_button.shortcut = _ctrls.shortcut_dialog.scbtn_panel.get_shortcut()
func _run_all():
_gut_config.options.selected = null
_gut_config.options.inner_class = null
_gut_config.options.unit_test_name = null
_run_tests()
# ---------------
# Events
# ---------------
func _on_results_bar_draw(bar):
bar.draw_rect(Rect2(Vector2(0, 0), bar.size), Color(0, 0, 0, .2))
func _on_Light_draw():
var l = _ctrls.light
l.draw_circle(Vector2(l.size.x / 2, l.size.y / 2), l.size.x / 2, _light_color)
func _on_RunAll_pressed():
_run_all()
func _on_Shortcuts_pressed():
_ctrls.shortcut_dialog.popup_centered()
func _on_sortcut_dialog_confirmed() -> void:
_apply_shortcuts()
_ctrls.shortcut_dialog.save_shortcuts()
_save_user_prefs()
func _on_RunAtCursor_run_tests(what):
_gut_config.options.selected = what.script
_gut_config.options.inner_class = what.inner_class
_gut_config.options.unit_test_name = what.method
_run_tests()
func _on_Settings_pressed():
hide_settings(!_ctrls.settings_button.button_pressed)
_save_config()
func _on_OutputBtn_pressed():
hide_output_text(!_ctrls.output_button.button_pressed)
_save_config()
func _on_RunResultsBtn_pressed():
hide_result_tree(! _ctrls.run_results_button.button_pressed)
_save_config()
# Currently not used, but will be when I figure out how to put
# colors into the text results
func _on_UseColors_pressed():
pass
func _on_shell_out_options_confirmed() -> void:
_ctrls.run_externally_dialog.save_to_file()
_save_user_prefs()
_apply_options_to_controls()
func _on_run_mode_pressed() -> void:
_ctrls.run_externally_dialog.popup_centered()
func _on_toggle_windowed():
_gut_plugin.toggle_windowed()
func _on_to_window_pressed() -> void:
_gut_plugin.toggle_windowed()
func _on_show_gut() -> void:
show_hide()
func _on_about_pressed() -> void:
show_about()
# ---------------
# Public
# ---------------
func load_shortcuts():
_ctrls.shortcut_dialog.load_shortcuts()
_apply_shortcuts()
func hide_result_tree(should):
results_tree.visible = !should
_ctrls.run_results_button.button_pressed = !should
func hide_settings(should):
var s_scroll = _ctrls.settings.get_parent()
s_scroll.visible = !should
# collapse only collapses the first control, so we move
# settings around to be the collapsed one
if(should):
s_scroll.get_parent().move_child(s_scroll, 0)
else:
s_scroll.get_parent().move_child(s_scroll, 1)
$layout/RSplit.collapsed = should
_ctrls.settings_button.button_pressed = !should
func hide_output_text(should):
results_text.visible = !should
_ctrls.output_button.button_pressed = !should
func clear_results():
_light_color = Color(0, 0, 0, .5)
_ctrls.results.passing.text = "0"
_ctrls.results.passing.get_parent().visible = false
_ctrls.results.failing.text = "0"
_ctrls.results.failing.get_parent().visible = false
_ctrls.results.pending.text = "0"
_ctrls.results.pending.get_parent().visible = false
_ctrls.results.errors.text = "0"
_ctrls.results.errors.get_parent().visible = false
_ctrls.results.warnings.text = "0"
_ctrls.results.warnings.get_parent().visible = false
_ctrls.results.orphans.text = "0"
_ctrls.results.orphans.get_parent().visible = false
func load_result_json():
var summary = get_file_as_text(GutEditorGlobals.editor_run_json_results_path)
var test_json_conv = JSON.new()
if (test_json_conv.parse(summary) != OK):
return
var results = test_json_conv.get_data()
results_tree.load_json_results(results)
var summary_json = results['test_scripts']['props']
_ctrls.results.passing.text = str(int(summary_json.passing))
_ctrls.results.passing.get_parent().visible = true
_ctrls.results.failing.text = str(int(summary_json.failures))
_ctrls.results.failing.get_parent().visible = true
_ctrls.results.pending.text = str(int(summary_json.pending) + int(summary_json.risky))
_ctrls.results.pending.get_parent().visible = _ctrls.results.pending.text != '0'
_ctrls.results.errors.text = str(int(summary_json.errors))
_ctrls.results.errors.get_parent().visible = _ctrls.results.errors.text != '0'
_ctrls.results.warnings.text = str(int(summary_json.warnings))
_ctrls.results.warnings.get_parent().visible = _ctrls.results.warnings.text != '0'
_ctrls.results.orphans.text = str(int(summary_json.orphans))
_ctrls.results.orphans.get_parent().visible = _ctrls.results.orphans.text != '0' and !_gut_config.options.hide_orphans
if(summary_json.tests == 0):
_light_color = Color(1, 0, 0, .75)
elif(summary_json.failures != 0):
_light_color = Color(1, 0, 0, .75)
elif(summary_json.pending != 0 or summary_json.risky != 0):
_light_color = Color(1, 1, 0, .75)
else:
_light_color = Color(0, 1, 0, .75)
_ctrls.light.visible = true
func load_result_text():
results_text.load_file(GutEditorGlobals.editor_run_bbcode_results_path)
func load_result_output():
load_result_text()
load_result_json()
func set_interface(value):
_interface = value
results_tree.set_interface(_interface)
func set_plugin(value):
_gut_plugin = value
func set_panel_button(value):
_panel_button = value
func write_file(path, content):
var f = FileAccess.open(path, FileAccess.WRITE)
if(f != null):
f.store_string(content)
f = null;
return FileAccess.get_open_error()
func get_file_as_text(path):
var to_return = ''
var f = FileAccess.open(path, FileAccess.READ)
if(f != null):
to_return = f.get_as_text()
f = null
return to_return
func get_text_output_control():
return results_text
func add_output_text(text):
results_text.add_text(text)
func show_about():
var about = AboutWindow.instantiate()
add_child(about)
about.popup_centered()
about.confirmed.connect(about.queue_free)
func show_me():
if(owner is Window):
owner.grab_focus()
else:
_gut_plugin.make_bottom_panel_item_visible(self)
func show_hide():
if(owner is Window):
if(owner.has_focus()):
var win_to_focus_on = EditorInterface.get_editor_main_screen().get_parent()
while(win_to_focus_on != null and win_to_focus_on is not Window):
win_to_focus_on = win_to_focus_on.get_parent()
if(win_to_focus_on != null):
win_to_focus_on.grab_focus()
else:
owner.grab_focus()
else:
pass
# We don't have to do anything when we are docked because the GUT
# bottom panel has the shortcut and it does the toggling all on its
# own.
func get_shortcut_dialog():
return _ctrls.shortcut_dialog
func results_vert_layout():
if(results_tree.get_parent() != results_v_split):
results_tree.reparent(results_v_split)
results_text.reparent(results_v_split)
results_v_split.visible = true
results_h_split.visible = false
func results_horiz_layout():
if(results_tree.get_parent() != results_h_split):
results_tree.reparent(results_h_split)
results_text.reparent(results_h_split)
results_v_split.visible = false
results_h_split.visible = true

View File

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

View File

@@ -0,0 +1,295 @@
[gd_scene load_steps=10 format=3 uid="uid://b3bostcslstem"]
[ext_resource type="Script" uid="uid://dtvnb0xatk0my" path="res://addons/gut/gui/GutBottomPanel.gd" id="1"]
[ext_resource type="PackedScene" uid="uid://0yunjxtaa8iw" path="res://addons/gut/gui/RunAtCursor.tscn" id="3"]
[ext_resource type="Texture2D" uid="uid://cr6tvdv0ve6cv" path="res://addons/gut/gui/play.png" id="4"]
[ext_resource type="Texture2D" uid="uid://bvo0uao7deu0q" path="res://addons/gut/icon.png" id="4_xv2r3"]
[ext_resource type="PackedScene" uid="uid://4gyyn12um08h" path="res://addons/gut/gui/RunResults.tscn" id="5"]
[ext_resource type="PackedScene" uid="uid://bqmo4dj64c7yl" path="res://addons/gut/gui/OutputText.tscn" id="6"]
[ext_resource type="PackedScene" uid="uid://dj5ve0bq7xa5j" path="res://addons/gut/gui/ShortcutDialog.tscn" id="7_srqj5"]
[ext_resource type="PackedScene" uid="uid://ckv5eh8xyrwbk" path="res://addons/gut/gui/ShellOutOptions.tscn" id="7_xv2r3"]
[sub_resource type="Shortcut" id="9"]
[node name="GutBottomPanel" type="Control"]
custom_minimum_size = Vector2(250, 250)
layout_mode = 3
anchor_left = -0.0025866
anchor_top = -0.00176575
anchor_right = 0.997413
anchor_bottom = 0.998234
offset_left = 2.64868
offset_top = 1.05945
offset_right = 2.64862
offset_bottom = 1.05945
script = ExtResource("1")
[node name="layout" type="VBoxContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
[node name="ControlBar" type="HBoxContainer" parent="layout"]
layout_mode = 2
[node name="RunAll" type="Button" parent="layout/ControlBar"]
layout_mode = 2
size_flags_vertical = 11
shortcut = SubResource("9")
text = "Run All"
icon = ExtResource("4")
[node name="Sep3" type="ColorRect" parent="layout/ControlBar"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="RunAtCursor" parent="layout/ControlBar" instance=ExtResource("3")]
custom_minimum_size = Vector2(648, 0)
layout_mode = 2
[node name="CenterContainer2" type="CenterContainer" parent="layout/ControlBar"]
layout_mode = 2
size_flags_horizontal = 3
[node name="MakeFloating" type="Button" parent="layout/ControlBar"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Move the GUT panel to a window."
icon = ExtResource("4_xv2r3")
flat = true
[node name="ControlBar2" type="HBoxContainer" parent="layout"]
layout_mode = 2
[node name="Sep2" type="ColorRect" parent="layout/ControlBar2"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
color = Color(1, 1, 1, 0)
[node name="StatusIndicator" type="Control" parent="layout/ControlBar2"]
unique_name_in_owner = true
custom_minimum_size = Vector2(30, 30)
layout_mode = 2
[node name="Passing" type="HBoxContainer" parent="layout/ControlBar2"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Passing"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/ControlBar2/Passing"]
layout_mode = 2
text = "Pass"
[node name="passing_value" type="Label" parent="layout/ControlBar2/Passing"]
unique_name_in_owner = true
layout_mode = 2
text = "---"
[node name="Failing" type="HBoxContainer" parent="layout/ControlBar2"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Failing"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/ControlBar2/Failing"]
layout_mode = 2
text = "Fail"
[node name="failing_value" type="Label" parent="layout/ControlBar2/Failing"]
unique_name_in_owner = true
layout_mode = 2
text = "---"
[node name="Pending" type="HBoxContainer" parent="layout/ControlBar2"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Pending"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/ControlBar2/Pending"]
layout_mode = 2
text = "Risky"
[node name="pending_value" type="Label" parent="layout/ControlBar2/Pending"]
unique_name_in_owner = true
layout_mode = 2
text = "---"
[node name="Orphans" type="HBoxContainer" parent="layout/ControlBar2"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Orphans"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/ControlBar2/Orphans"]
layout_mode = 2
text = "Orphans"
[node name="orphans_value" type="Label" parent="layout/ControlBar2/Orphans"]
unique_name_in_owner = true
layout_mode = 2
text = "---"
[node name="Errors" type="HBoxContainer" parent="layout/ControlBar2"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Errors"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/ControlBar2/Errors"]
layout_mode = 2
text = "Errors"
[node name="errors_value" type="Label" parent="layout/ControlBar2/Errors"]
unique_name_in_owner = true
layout_mode = 2
text = "---"
[node name="Warnings" type="HBoxContainer" parent="layout/ControlBar2"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Warnings"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/ControlBar2/Warnings"]
layout_mode = 2
text = "Warnings"
[node name="warnings_value" type="Label" parent="layout/ControlBar2/Warnings"]
unique_name_in_owner = true
layout_mode = 2
text = "---"
[node name="CenterContainer" type="CenterContainer" parent="layout/ControlBar2"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ExtraButtons" type="HBoxContainer" parent="layout/ControlBar2"]
unique_name_in_owner = true
layout_mode = 2
[node name="Sep1" type="ColorRect" parent="layout/ControlBar2/ExtraButtons"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="RunMode" type="Button" parent="layout/ControlBar2/ExtraButtons"]
layout_mode = 2
tooltip_text = "Run Mode. Run tests through the editor or externally in a different process."
text = "ExN"
icon = ExtResource("4_xv2r3")
[node name="Sep2" type="ColorRect" parent="layout/ControlBar2/ExtraButtons"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="RunResultsBtn" type="Button" parent="layout/ControlBar2/ExtraButtons"]
layout_mode = 2
tooltip_text = "Show/Hide Result Tree"
toggle_mode = true
button_pressed = true
icon = ExtResource("4_xv2r3")
[node name="OutputBtn" type="Button" parent="layout/ControlBar2/ExtraButtons"]
layout_mode = 2
tooltip_text = "Show/Hide Text Output"
toggle_mode = true
button_pressed = true
icon = ExtResource("4_xv2r3")
[node name="Settings" type="Button" parent="layout/ControlBar2/ExtraButtons"]
layout_mode = 2
tooltip_text = "GUT Settings"
toggle_mode = true
button_pressed = true
icon = ExtResource("4_xv2r3")
[node name="Sep3" type="ColorRect" parent="layout/ControlBar2/ExtraButtons"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="Shortcuts" type="Button" parent="layout/ControlBar2/ExtraButtons"]
layout_mode = 2
size_flags_vertical = 11
tooltip_text = "GUT Shortcuts"
icon = ExtResource("4_xv2r3")
[node name="About" type="Button" parent="layout/ControlBar2/ExtraButtons"]
layout_mode = 2
tooltip_text = "About"
text = "A"
[node name="RSplit" type="HSplitContainer" parent="layout"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="CResults" type="VBoxContainer" parent="layout/RSplit"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="HSplitResults" type="HSplitContainer" parent="layout/RSplit/CResults"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="RunResults" parent="layout/RSplit/CResults/HSplitResults" instance=ExtResource("5")]
unique_name_in_owner = true
custom_minimum_size = Vector2(255, 255)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="OutputText" parent="layout/RSplit/CResults/HSplitResults" instance=ExtResource("6")]
unique_name_in_owner = true
layout_mode = 2
[node name="VSplitResults" type="VSplitContainer" parent="layout/RSplit/CResults"]
unique_name_in_owner = true
visible = false
layout_mode = 2
size_flags_vertical = 3
[node name="sc" type="ScrollContainer" parent="layout/RSplit"]
custom_minimum_size = Vector2(500, 2.08165e-12)
layout_mode = 2
size_flags_vertical = 3
[node name="Settings" type="VBoxContainer" parent="layout/RSplit/sc"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="ShortcutDialog" parent="." instance=ExtResource("7_srqj5")]
visible = false
[node name="ShellOutOptions" parent="." instance=ExtResource("7_xv2r3")]
size = Vector2i(1300, 1336)
visible = false
[connection signal="pressed" from="layout/ControlBar/RunAll" to="." method="_on_RunAll_pressed"]
[connection signal="run_tests" from="layout/ControlBar/RunAtCursor" to="." method="_on_RunAtCursor_run_tests"]
[connection signal="pressed" from="layout/ControlBar/MakeFloating" to="." method="_on_to_window_pressed"]
[connection signal="draw" from="layout/ControlBar2/StatusIndicator" to="." method="_on_Light_draw"]
[connection signal="pressed" from="layout/ControlBar2/ExtraButtons/RunMode" to="." method="_on_run_mode_pressed"]
[connection signal="pressed" from="layout/ControlBar2/ExtraButtons/RunResultsBtn" to="." method="_on_RunResultsBtn_pressed"]
[connection signal="pressed" from="layout/ControlBar2/ExtraButtons/OutputBtn" to="." method="_on_OutputBtn_pressed"]
[connection signal="pressed" from="layout/ControlBar2/ExtraButtons/Settings" to="." method="_on_Settings_pressed"]
[connection signal="pressed" from="layout/ControlBar2/ExtraButtons/Shortcuts" to="." method="_on_Shortcuts_pressed"]
[connection signal="pressed" from="layout/ControlBar2/ExtraButtons/About" to="." method="_on_about_pressed"]
[connection signal="confirmed" from="ShortcutDialog" to="." method="_on_sortcut_dialog_confirmed"]
[connection signal="confirmed" from="ShellOutOptions" to="." method="_on_shell_out_options_confirmed"]

View File

@@ -0,0 +1,326 @@
@tool
extends Control
const RUNNER_JSON_PATH = 'res://.gut_editor_config.json'
var GutConfig = load('res://addons/gut/gut_config.gd')
var GutRunnerScene = load('res://addons/gut/gui/GutRunner.tscn')
var GutConfigGui = load('res://addons/gut/gui/gut_config_gui.gd')
var _config = GutConfig.new()
var _config_gui = null
var _gut_runner = null
var _tree_root : TreeItem = null
var _script_icon = load('res://addons/gut/images/Script.svg')
var _folder_icon = load('res://addons/gut/images/Folder.svg')
var _tree_scripts = {}
var _tree_directories = {}
const TREE_SCRIPT = 'Script'
const TREE_DIR = 'Directory'
@onready var _ctrls = {
run_tests_button = $VBox/Buttons/RunTests,
run_selected = $VBox/Buttons/RunSelected,
test_tree = $VBox/Tabs/Tests,
settings_vbox = $VBox/Tabs/SettingsScroll/Settings,
tabs = $VBox/Tabs,
bg = $Bg
}
@export var bg_color : Color = Color(.36, .36, .36) :
get: return bg_color
set(val):
bg_color = val
if(is_inside_tree()):
$Bg.color = bg_color
func _ready():
if Engine.is_editor_hint():
return
_gut_runner = GutRunnerScene.instantiate()
$Bg.color = bg_color
_ctrls.tabs.set_tab_title(0, 'Tests')
_ctrls.tabs.set_tab_title(1, 'Settings')
_config_gui = GutConfigGui.new(_ctrls.settings_vbox)
_ctrls.test_tree.hide_root = true
add_child(_gut_runner)
# TODO This might not need to be called deferred after changing GutUtils to
# an all static class.
call_deferred('_post_ready')
func _draw():
if Engine.is_editor_hint():
return
var gut = _gut_runner.get_gut()
if(!gut.is_running()):
var r = Rect2(Vector2(0, 0), get_rect().size)
draw_rect(r, Color.BLACK, false, 2)
func _post_ready():
var gut = _gut_runner.get_gut()
gut.start_run.connect(_on_gut_run_started)
gut.end_run.connect(_on_gut_run_ended)
_refresh_tree_and_settings()
func _set_meta_for_script_tree_item(item, script, test=null):
var meta = {
type = TREE_SCRIPT,
script = script.path,
inner_class = script.inner_class_name,
test = ''
}
if(test != null):
meta.test = test.name
item.set_metadata(0, meta)
func _set_meta_for_directory_tree_item(item, path, temp_item):
var meta = {
type = TREE_DIR,
path = path,
temp_item = temp_item
}
item.set_metadata(0, meta)
func _get_script_tree_item(script, parent_item):
if(!_tree_scripts.has(script.path)):
var item = _ctrls.test_tree.create_item(parent_item)
item.set_text(0, script.path.get_file())
item.set_icon(0, _script_icon)
_tree_scripts[script.path] = item
_set_meta_for_script_tree_item(item, script)
return _tree_scripts[script.path]
func _get_directory_tree_item(path):
var parent = _tree_root
if(!_tree_directories.has(path)):
var item : TreeItem = null
if(parent != _tree_root):
item = parent.create_child(0)
else:
item = parent.create_child()
_tree_directories[path] = item
item.collapsed = false
item.set_text(0, path)
item.set_icon(0, _folder_icon)
item.set_icon_modulate(0, Color.ROYAL_BLUE)
# temp_item is used in calls with move_before since you must use
# move_before or move_after to reparent tree items. This ensures that
# there is an item on all directories. These are deleted later.
var temp_item = item.create_child()
temp_item.set_text(0, '<temp>')
_set_meta_for_directory_tree_item(item, path, temp_item)
return _tree_directories[path]
func _find_dir_item_to_move_before(path):
var max_matching_len = 0
var best_parent = null
# Go through all the directory items finding the one that has the longest
# path that contains our path.
for key in _tree_directories.keys():
if(path != key and path.begins_with(key) and key.length() > max_matching_len):
max_matching_len = key.length()
best_parent = _tree_directories[key]
var to_return = null
if(best_parent != null):
to_return = best_parent.get_metadata(0).temp_item
return to_return
func _reorder_dir_items():
var the_keys = _tree_directories.keys()
the_keys.sort()
for key in _tree_directories.keys():
var to_move = _tree_directories[key]
to_move.collapsed = false
var move_before = _find_dir_item_to_move_before(key)
if(move_before != null):
to_move.move_before(move_before)
var new_text = key.substr(move_before.get_parent().get_metadata(0).path.length())
to_move.set_text(0, new_text)
func _remove_dir_temp_items():
for key in _tree_directories.keys():
var item = _tree_directories[key].get_metadata(0).temp_item
_tree_directories[key].remove_child(item)
func _add_dir_and_script_tree_items():
var tree : Tree = _ctrls.test_tree
tree.clear()
_tree_root = _ctrls.test_tree.create_item()
var scripts = _gut_runner.get_gut().get_test_collector().scripts
for script in scripts:
var dir_item = _get_directory_tree_item(script.path.get_base_dir())
var item = _get_script_tree_item(script, dir_item)
if(script.inner_class_name != ''):
var inner_item = tree.create_item(item)
inner_item.set_text(0, script.inner_class_name)
_set_meta_for_script_tree_item(inner_item, script)
item = inner_item
for test in script.tests:
var test_item = tree.create_item(item)
test_item.set_text(0, test.name)
_set_meta_for_script_tree_item(test_item, script, test)
func _populate_tree():
_add_dir_and_script_tree_items()
_tree_root.set_collapsed_recursive(true)
_tree_root.set_collapsed(false)
_reorder_dir_items()
_remove_dir_temp_items()
func _refresh_tree_and_settings():
_config.apply_options(_gut_runner.get_gut())
_gut_runner.set_gut_config(_config)
_populate_tree()
# ---------------------------
# Events
# ---------------------------
func _on_gut_run_started():
_ctrls.run_tests_button.disabled = true
_ctrls.run_selected.visible = false
_ctrls.tabs.visible = false
_ctrls.bg.visible = false
_ctrls.run_tests_button.text = 'Running'
queue_redraw()
func _on_gut_run_ended():
_ctrls.run_tests_button.disabled = false
_ctrls.run_selected.visible = true
_ctrls.tabs.visible = true
_ctrls.bg.visible = true
_ctrls.run_tests_button.text = 'Run All'
queue_redraw()
func _on_run_tests_pressed():
run_all()
func _on_run_selected_pressed():
run_selected()
func _on_tests_item_activated():
run_selected()
# ---------------------------
# Public
# ---------------------------
func get_gut():
return _gut_runner.get_gut()
func get_config():
return _config
func run_all():
_config.options.selected = ''
_config.options.inner_class_name = ''
_config.options.unit_test_name = ''
run_tests()
func run_tests(options = null):
if(options == null):
_config.options = _config_gui.get_options(_config.options)
else:
_config.options = options
# We ar running from within the game, so we should not exit, ever.
_config.options.should_exit_on_success = false
_config.options.should_exit = false
_gut_runner.get_gut().get_test_collector().clear()
_gut_runner.set_gut_config(_config)
_gut_runner.run_tests()
func run_selected():
var sel_item = _ctrls.test_tree.get_selected()
if(sel_item == null):
return
var options = _config_gui.get_options(_config.options)
var meta = sel_item.get_metadata(0)
if(meta.type == TREE_SCRIPT):
options.selected = meta.script.get_file()
options.inner_class_name = meta.inner_class
options.unit_test_name = meta.test
elif(meta.type == TREE_DIR):
options.dirs = [meta.path]
options.include_subdirectories = true
options.selected = ''
options.inner_class_name = ''
options.unit_test_name = ''
run_tests(options)
func load_config_file(path):
_config.load_options(path)
_config.options.selected = ''
_config.options.inner_class_name = ''
_config.options.unit_test_name = ''
_config_gui.load_file(path)
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2025 Tom "Butch" Wesley
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ##############################################################################

View File

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

View File

@@ -0,0 +1,63 @@
[gd_scene load_steps=2 format=3 uid="uid://4jb53yqktyfg"]
[ext_resource type="Script" uid="uid://cqlvpwidawld6" path="res://addons/gut/gui/GutControl.gd" id="1_eprql"]
[node name="GutControl" type="Control"]
layout_mode = 3
anchors_preset = 0
offset_right = 295.0
offset_bottom = 419.0
script = ExtResource("1_eprql")
[node name="Bg" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.36, 0.36, 0.36, 1)
[node name="VBox" type="VBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Tabs" type="TabContainer" parent="VBox"]
layout_mode = 2
size_flags_vertical = 3
[node name="Tests" type="Tree" parent="VBox/Tabs"]
layout_mode = 2
size_flags_vertical = 3
hide_root = true
[node name="SettingsScroll" type="ScrollContainer" parent="VBox/Tabs"]
visible = false
layout_mode = 2
size_flags_vertical = 3
[node name="Settings" type="VBoxContainer" parent="VBox/Tabs/SettingsScroll"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Buttons" type="HBoxContainer" parent="VBox"]
layout_mode = 2
[node name="RunTests" type="Button" parent="VBox/Buttons"]
layout_mode = 2
size_flags_horizontal = 3
text = "Run All"
[node name="RunSelected" type="Button" parent="VBox/Buttons"]
layout_mode = 2
size_flags_horizontal = 3
text = "Run Selected"
[connection signal="item_activated" from="VBox/Tabs/Tests" to="." method="_on_tests_item_activated"]
[connection signal="pressed" from="VBox/Buttons/RunTests" to="." method="_on_run_tests_pressed"]
[connection signal="pressed" from="VBox/Buttons/RunSelected" to="." method="_on_run_selected_pressed"]

View File

@@ -0,0 +1,88 @@
@tool
extends Window
var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')
@onready var _chk_always_on_top = $Layout/WinControls/OnTop
var _bottom_panel = null
var _ready_to_go = false
var _gut_shortcuts = []
var gut_plugin = null
var interface = null
func _unhandled_key_input(event: InputEvent) -> void:
if(event is InputEventKey):
if(_gut_shortcuts.has(event.as_text_keycode())):
get_tree().root.push_input(event)
func _ready() -> void:
var pref_size = GutEditorGlobals.user_prefs.gut_window_size.value
if(pref_size.x < 0):
size = Vector2(800, 800)
else:
size = pref_size
always_on_top = GutEditorGlobals.user_prefs.gut_window_on_top.value
_chk_always_on_top.button_pressed = always_on_top
# --------
# Events
# --------
func _on_on_top_toggled(toggled_on: bool) -> void:
always_on_top = toggled_on
GutEditorGlobals.user_prefs.gut_window_on_top.value = toggled_on
func _on_size_changed() -> void:
if(_ready_to_go):
GutEditorGlobals.user_prefs.gut_window_size.value = size
func _on_close_requested() -> void:
gut_plugin.toggle_windowed()
func _on_vert_layout_pressed() -> void:
_bottom_panel.results_vert_layout()
func _on_horiz_layout_pressed() -> void:
_bottom_panel.results_horiz_layout()
# --------
# Public
# --------
func add_gut_panel(panel : Control):
$Layout.add_child(panel)
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
panel.visible = true
_bottom_panel = panel
_ready_to_go = true
panel.owner = self
# This stunk to figure out.
theme = interface.get_editor_theme()
var settings = interface.get_editor_settings()
$ColorRect.color = settings.get_setting("interface/theme/base_color")
set_gut_shortcuts(_bottom_panel._ctrls.shortcut_dialog)
func remove_panel():
$Layout.remove_child(_bottom_panel)
_bottom_panel.owner = null
func set_gut_shortcuts(shortcuts_dialog):
_gut_shortcuts.clear()
for btn in shortcuts_dialog.all_buttons:
_gut_shortcuts.append(btn.get_input_event().as_text_keycode())

View File

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

View File

@@ -0,0 +1,127 @@
[gd_scene load_steps=10 format=3 uid="uid://dnnvwlplf1km7"]
[ext_resource type="Script" uid="uid://crp2af6k4nmf5" path="res://addons/gut/gui/GutEditorWindow.gd" id="1_qevl3"]
[ext_resource type="Texture2D" uid="uid://ljc2viafngwd" path="res://addons/gut/images/HSplitContainer.svg" id="2_xw0o2"]
[ext_resource type="Texture2D" uid="uid://bhew20crsywxr" path="res://addons/gut/images/VSplitContainer.svg" id="3_fqfwy"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qevl3"]
content_margin_left = 8.0
content_margin_top = 8.0
content_margin_right = 8.0
content_margin_bottom = 8.0
bg_color = Color(0.115499996, 0.132, 0.15949999, 1)
corner_detail = 1
anti_aliasing = false
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_af010"]
content_margin_left = 8.0
content_margin_top = 12.0
content_margin_right = 8.0
content_margin_bottom = 8.0
bg_color = Color(0.21, 0.24, 0.29, 1)
border_color = Color(0.08399999, 0.095999986, 0.116, 1)
corner_radius_top_left = 6
corner_radius_top_right = 6
corner_radius_bottom_right = 6
corner_radius_bottom_left = 6
corner_detail = 5
anti_aliasing = false
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_xw0o2"]
content_margin_left = 0.0
content_margin_top = 8.0
content_margin_right = 0.0
content_margin_bottom = 0.0
bg_color = Color(0.21, 0.24, 0.29, 1)
border_color = Color(0.08399999, 0.095999986, 0.116, 1)
corner_radius_bottom_right = 6
corner_radius_bottom_left = 6
corner_detail = 5
anti_aliasing = false
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_fqfwy"]
content_margin_left = 12.0
content_margin_top = 8.0
content_margin_right = 12.0
content_margin_bottom = 8.0
bg_color = Color(0.14699998, 0.16799998, 0.203, 1)
corner_radius_top_left = 6
corner_radius_top_right = 6
corner_radius_bottom_right = 6
corner_radius_bottom_left = 6
corner_detail = 5
anti_aliasing = false
[sub_resource type="Theme" id="Theme_fqfwy"]
Editor/colors/accent_color = Color(0.44, 0.73, 0.98, 1)
Editor/colors/background = Color(0.115499996, 0.132, 0.15949999, 1)
Editor/colors/base_color = Color(0.21, 0.24, 0.29, 1)
EditorStyles/styles/Background = SubResource("StyleBoxFlat_qevl3")
EditorStyles/styles/BottomPanel = SubResource("StyleBoxFlat_af010")
EditorStyles/styles/Content = SubResource("StyleBoxFlat_xw0o2")
Panel/styles/panel = SubResource("StyleBoxFlat_fqfwy")
[sub_resource type="ButtonGroup" id="ButtonGroup_qevl3"]
[node name="GutEditorWindow" type="Window"]
oversampling_override = 1.0
title = "GUT"
position = Vector2i(0, 36)
size = Vector2i(1408, 1728)
min_size = Vector2i(800, 600)
script = ExtResource("1_qevl3")
[node name="ColorRect" type="ColorRect" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme = SubResource("Theme_fqfwy")
color = Color(0.18717614, 0.18717614, 0.18717614, 1)
[node name="Layout" type="VBoxContainer" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="WinControls" type="HBoxContainer" parent="Layout"]
layout_mode = 2
[node name="MenuBar" type="MenuBar" parent="Layout/WinControls"]
custom_minimum_size = Vector2(300, 0)
layout_mode = 2
size_flags_horizontal = 3
flat = true
prefer_global_menu = false
[node name="CenterContainer" type="CenterContainer" parent="Layout/WinControls"]
layout_mode = 2
size_flags_horizontal = 3
[node name="OnTop" type="CheckButton" parent="Layout/WinControls"]
layout_mode = 2
text = "Always on Top"
[node name="HorizLayout" type="Button" parent="Layout/WinControls"]
texture_filter = 1
layout_mode = 2
toggle_mode = true
button_pressed = true
button_group = SubResource("ButtonGroup_qevl3")
icon = ExtResource("2_xw0o2")
icon_alignment = 1
[node name="VertLayout" type="Button" parent="Layout/WinControls"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_qevl3")
icon = ExtResource("3_fqfwy")
[connection signal="close_requested" from="." to="." method="_on_close_requested"]
[connection signal="size_changed" from="." to="." method="_on_size_changed"]
[connection signal="toggled" from="Layout/WinControls/OnTop" to="." method="_on_on_top_toggled"]
[connection signal="pressed" from="Layout/WinControls/HorizLayout" to="." method="_on_horiz_layout_pressed"]
[connection signal="pressed" from="Layout/WinControls/VertLayout" to="." method="_on_vert_layout_pressed"]

View File

@@ -0,0 +1,36 @@
[gd_scene load_steps=4 format=3 uid="uid://bjkn8mhx2fmt1"]
[ext_resource type="Script" uid="uid://b8lvgepb64m8t" path="res://addons/gut/gui/gut_logo.gd" id="1_ba6lh"]
[ext_resource type="Texture2D" uid="uid://dyxbmyvpkkcvs" path="res://addons/gut/images/GutIconV2_base.png" id="2_ba6lh"]
[ext_resource type="Texture2D" uid="uid://dx0yxxn5q7doc" path="res://addons/gut/images/eyey.png" id="3_rc8fb"]
[node name="Logo" type="Node2D"]
script = ExtResource("1_ba6lh")
[node name="BaseLogo" type="Sprite2D" parent="."]
scale = Vector2(0.5, 0.5)
texture = ExtResource("2_ba6lh")
[node name="LeftEye" type="Sprite2D" parent="BaseLogo"]
visible = false
position = Vector2(-238, 16)
texture = ExtResource("3_rc8fb")
[node name="RightEye" type="Sprite2D" parent="BaseLogo"]
visible = false
position = Vector2(239, 16)
texture = ExtResource("3_rc8fb")
[node name="ResetTimer" type="Timer" parent="."]
wait_time = 5.0
one_shot = true
[node name="FaceButton" type="Button" parent="."]
modulate = Color(1, 1, 1, 0)
offset_left = -141.0
offset_top = -113.0
offset_right = 140.0
offset_bottom = 175.0
[connection signal="timeout" from="ResetTimer" to="." method="_on_reset_timer_timeout"]
[connection signal="pressed" from="FaceButton" to="." method="_on_face_button_pressed"]

243
addons/gut/gui/GutRunner.gd Normal file
View File

@@ -0,0 +1,243 @@
# ##############################################################################
# This class joins together GUT, GUT Gui, GutConfig and is THE way to kick off a
# run of a test suite.
#
# This creates its own instance of gut.gd that it manages. You can set the
# gut.gd instance if you need to for testing.
#
# Set gut_config to an instance of a configured gut_config.gd instance prior to
# running tests.
#
# This will create a GUI and wire it up and apply gut_config.gd options.
#
# Running tests: Call run_tests
# ##############################################################################
extends Node2D
const EXIT_OK = 0
const EXIT_ERROR = 1
var Gut = load('res://addons/gut/gut.gd')
var ResultExporter = load('res://addons/gut/result_exporter.gd')
var GutConfig = load('res://addons/gut/gut_config.gd')
var runner_json_path = null
var result_bbcode_path = null
var result_json_path = null
var lgr = GutUtils.get_logger()
var gut_config = null
var error_tracker = GutUtils.get_error_tracker()
var _hid_gut = null;
# Lazy loaded gut instance. Settable for testing purposes.
var gut = _hid_gut :
get:
if(_hid_gut == null):
_hid_gut = Gut.new(lgr)
_hid_gut.error_tracker = error_tracker
return _hid_gut
set(val):
_hid_gut = val
var _wrote_results = false
var _ran_from_editor = false
@onready var _gut_layer = $GutLayer
@onready var _gui = $GutLayer/GutScene
func _ready():
GutUtils.WarningsManager.apply_warnings_dictionary(
GutUtils.warnings_at_start)
func _exit_tree():
if(!_wrote_results and _ran_from_editor):
_write_results_for_gut_panel()
func _setup_gui(show_gui):
if(show_gui):
_gui.gut = gut
var printer = gut.logger.get_printer('gui')
printer.set_textbox(_gui.get_textbox())
else:
gut.logger.disable_printer('gui', true)
_gui.visible = false
var opts = gut_config.options
_gui.set_font_size(opts.font_size)
_gui.set_font(opts.font_name)
if(opts.font_color != null and opts.font_color.is_valid_html_color()):
_gui.set_default_font_color(Color(opts.font_color))
if(opts.background_color != null and opts.background_color.is_valid_html_color()):
_gui.set_background_color(Color(opts.background_color))
_gui.set_opacity(min(1.0, float(opts.opacity) / 100))
_gui.use_compact_mode(opts.compact_mode)
func _write_results_for_gut_panel():
var content = _gui.get_textbox().get_parsed_text() #_gut.logger.get_gui_bbcode()
var f = FileAccess.open(result_bbcode_path, FileAccess.WRITE)
if(f != null):
f.store_string(content)
f = null # closes file
else:
push_error('Could not save bbcode, result = ', FileAccess.get_open_error())
var exporter = ResultExporter.new()
# TODO this should be checked and _wrote_results should maybe not be set, or
# maybe we do not care. Whichever, it should be clear.
var _f_result = exporter.write_json_file(gut, result_json_path)
_wrote_results = true
func _handle_quit(should_exit, should_exit_on_success, override_exit_code=EXIT_OK):
var quitting_time = should_exit or \
(should_exit_on_success and gut.get_fail_count() == 0)
if(!quitting_time):
if(should_exit_on_success):
lgr.log("There are failing tests, exit manually.")
_gui.use_compact_mode(false)
return
# For some reason, tests fail asserting that quit was called with 0 if we
# do not do this, but everything is defaulted so I don't know why it gets
# null.
var exit_code = GutUtils.nvl(override_exit_code, EXIT_OK)
if(gut.get_fail_count() > 0):
exit_code = EXIT_ERROR
# Overwrite the exit code with the post_script's exit code if it is set
var post_hook_inst = gut.get_post_run_script_instance()
if(post_hook_inst != null and post_hook_inst.get_exit_code() != null):
exit_code = post_hook_inst.get_exit_code()
quit(exit_code)
func _end_run(override_exit_code=EXIT_OK):
if(_ran_from_editor):
_write_results_for_gut_panel()
GutErrorTracker.deregister_logger(error_tracker)
_handle_quit(gut_config.options.should_exit,
gut_config.options.should_exit_on_success,
override_exit_code)
# -------------
# Events
# -------------
func _on_tests_finished():
_end_run()
# -------------
# Public
# -------------
# For internal use only, but still public. Consider it "protected" and you
# don't have my permission to call this, unless "you" is "me".
func run_from_editor():
_ran_from_editor = true
var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')
runner_json_path = GutUtils.nvl(runner_json_path, GutEditorGlobals.editor_run_gut_config_path)
result_bbcode_path = GutUtils.nvl(result_bbcode_path, GutEditorGlobals.editor_run_bbcode_results_path)
result_json_path = GutUtils.nvl(result_json_path, GutEditorGlobals.editor_run_json_results_path)
if(gut_config == null):
gut_config = GutConfig.new()
gut_config.load_options(runner_json_path)
call_deferred('run_tests')
func run_tests(show_gui=true):
_setup_gui(show_gui)
if(gut_config.options.dirs.size() + gut_config.options.tests.size() == 0):
var err_text = "You do not have any directories configured, so GUT " + \
"doesn't know where to find the tests. Tell GUT where to find the " + \
"tests and GUT shall run the tests."
lgr.error(err_text)
push_error(err_text)
_end_run(EXIT_ERROR)
return
var install_check_text = GutUtils.make_install_check_text()
if(install_check_text != GutUtils.INSTALL_OK_TEXT):
print("\n\n", GutUtils.version_numbers.get_version_text())
lgr.error(install_check_text)
push_error(install_check_text)
_end_run(EXIT_ERROR)
return
gut.add_children_to = self
if(gut.get_parent() == null):
if(gut_config.options.gut_on_top):
_gut_layer.add_child(gut)
else:
add_child(gut)
if(!gut.end_run.is_connected(_on_tests_finished)):
gut.end_run.connect(_on_tests_finished)
gut_config.apply_options(gut)
var run_rest_of_scripts = gut_config.options.unit_test_name == ''
GutErrorTracker.register_logger(error_tracker)
gut.test_scripts(run_rest_of_scripts)
func set_gut_config(which):
gut_config = which
# for backwards compatibility
func get_gut():
return gut
func quit(exit_code):
# Sometimes quitting takes a few seconds. This gives some indicator
# of what is going on.
_gui.set_title("Exiting")
await get_tree().process_frame
lgr.info(str('Exiting with code ', exit_code))
get_tree().quit(exit_code)
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2025 Tom "Butch" Wesley
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ##############################################################################

View File

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

View File

@@ -0,0 +1,12 @@
[gd_scene load_steps=3 format=3 uid="uid://bqy3ikt6vu4b5"]
[ext_resource type="Script" uid="uid://eg8k46gd42a4" path="res://addons/gut/gui/GutRunner.gd" id="1"]
[ext_resource type="PackedScene" uid="uid://m28heqtswbuq" path="res://addons/gut/GutScene.tscn" id="2_6ruxb"]
[node name="GutRunner" type="Node2D"]
script = ExtResource("1")
[node name="GutLayer" type="CanvasLayer" parent="."]
layer = 128
[node name="GutScene" parent="GutLayer" instance=ExtResource("2_6ruxb")]

View File

@@ -0,0 +1,7 @@
[gd_resource type="Theme" load_steps=2 format=3 uid="uid://cstkhwkpajvqu"]
[ext_resource type="FontFile" uid="uid://c6c7gnx36opr0" path="res://addons/gut/fonts/AnonymousPro-Regular.ttf" id="1_df57p"]
[resource]
default_font = ExtResource("1_df57p")
Label/fonts/font = ExtResource("1_df57p")

161
addons/gut/gui/MinGui.tscn Normal file
View File

@@ -0,0 +1,161 @@
[gd_scene load_steps=5 format=3 uid="uid://cnqqdfsn80ise"]
[ext_resource type="Theme" uid="uid://cstkhwkpajvqu" path="res://addons/gut/gui/GutSceneTheme.tres" id="1_farmq"]
[ext_resource type="FontFile" uid="uid://bnh0lslf4yh87" path="res://addons/gut/fonts/CourierPrime-Regular.ttf" id="2_a2e2l"]
[ext_resource type="Script" uid="uid://blvhsbnsvfyow" path="res://addons/gut/gui/gut_gui.gd" id="2_eokrf"]
[ext_resource type="PackedScene" uid="uid://bvrqqgjpyouse" path="res://addons/gut/gui/ResizeHandle.tscn" id="4_xrhva"]
[node name="Min" type="Panel"]
clip_contents = true
custom_minimum_size = Vector2(280, 145)
offset_right = 280.0
offset_bottom = 145.0
theme = ExtResource("1_farmq")
script = ExtResource("2_eokrf")
[node name="MainBox" type="VBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
metadata/_edit_layout_mode = 1
[node name="TitleBar" type="Panel" parent="MainBox"]
custom_minimum_size = Vector2(0, 25)
layout_mode = 2
[node name="TitleBox" type="HBoxContainer" parent="MainBox/TitleBar"]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = 2.0
offset_bottom = 3.0
grow_horizontal = 2
grow_vertical = 2
metadata/_edit_layout_mode = 1
[node name="Spacer1" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Title" type="Label" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
text = "Title"
[node name="Spacer2" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="TimeLabel" type="Label" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
text = "0.000s"
[node name="Body" type="HBoxContainer" parent="MainBox"]
layout_mode = 2
size_flags_vertical = 3
[node name="LeftMargin" type="CenterContainer" parent="MainBox/Body"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="BodyRows" type="VBoxContainer" parent="MainBox/Body"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ProgressBars" type="HBoxContainer" parent="MainBox/Body/BodyRows"]
layout_mode = 2
size_flags_horizontal = 3
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/Body/BodyRows/ProgressBars"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer"]
layout_mode = 2
text = "T:"
[node name="ProgressTest" type="ProgressBar" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer"]
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
size_flags_horizontal = 3
value = 25.0
[node name="HBoxContainer2" type="HBoxContainer" parent="MainBox/Body/BodyRows/ProgressBars"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer2"]
layout_mode = 2
text = "S:"
[node name="ProgressScript" type="ProgressBar" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer2"]
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
size_flags_horizontal = 3
value = 75.0
[node name="PathDisplay" type="VBoxContainer" parent="MainBox/Body/BodyRows"]
clip_contents = true
layout_mode = 2
size_flags_vertical = 3
[node name="Path" type="Label" parent="MainBox/Body/BodyRows/PathDisplay"]
layout_mode = 2
theme_override_fonts/font = ExtResource("2_a2e2l")
theme_override_font_sizes/font_size = 14
text = "res://test/integration/whatever"
clip_text = true
text_overrun_behavior = 3
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/Body/BodyRows/PathDisplay"]
clip_contents = true
layout_mode = 2
[node name="S3" type="CenterContainer" parent="MainBox/Body/BodyRows/PathDisplay/HBoxContainer"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="File" type="Label" parent="MainBox/Body/BodyRows/PathDisplay/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_fonts/font = ExtResource("2_a2e2l")
theme_override_font_sizes/font_size = 14
text = "test_this_thing.gd"
text_overrun_behavior = 3
[node name="Footer" type="HBoxContainer" parent="MainBox/Body/BodyRows"]
layout_mode = 2
[node name="HandleLeft" parent="MainBox/Body/BodyRows/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_xrhva")]
layout_mode = 2
orientation = 0
resize_control = NodePath("../../../../..")
vertical_resize = false
[node name="SwitchModes" type="Button" parent="MainBox/Body/BodyRows/Footer"]
layout_mode = 2
text = "Expand"
[node name="CenterContainer" type="CenterContainer" parent="MainBox/Body/BodyRows/Footer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Continue" type="Button" parent="MainBox/Body/BodyRows/Footer"]
layout_mode = 2
text = "Continue
"
[node name="HandleRight" parent="MainBox/Body/BodyRows/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_xrhva")]
layout_mode = 2
resize_control = NodePath("../../../../..")
vertical_resize = false
[node name="RightMargin" type="CenterContainer" parent="MainBox/Body"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="CenterContainer" type="CenterContainer" parent="MainBox"]
custom_minimum_size = Vector2(2.08165e-12, 2)
layout_mode = 2

View File

@@ -0,0 +1,215 @@
[gd_scene load_steps=5 format=3 uid="uid://duxblir3vu8x7"]
[ext_resource type="Theme" uid="uid://cstkhwkpajvqu" path="res://addons/gut/gui/GutSceneTheme.tres" id="1_5hlsm"]
[ext_resource type="Script" uid="uid://blvhsbnsvfyow" path="res://addons/gut/gui/gut_gui.gd" id="2_fue6q"]
[ext_resource type="FontFile" uid="uid://bnh0lslf4yh87" path="res://addons/gut/fonts/CourierPrime-Regular.ttf" id="2_u5uc1"]
[ext_resource type="PackedScene" uid="uid://bvrqqgjpyouse" path="res://addons/gut/gui/ResizeHandle.tscn" id="4_2r8a8"]
[node name="Large" type="Panel"]
custom_minimum_size = Vector2(500, 150)
offset_right = 632.0
offset_bottom = 260.0
theme = ExtResource("1_5hlsm")
script = ExtResource("2_fue6q")
[node name="MainBox" type="VBoxContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
metadata/_edit_layout_mode = 1
[node name="TitleBar" type="Panel" parent="MainBox"]
custom_minimum_size = Vector2(0, 25)
layout_mode = 2
[node name="TitleBox" type="HBoxContainer" parent="MainBox/TitleBar"]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = 2.0
offset_bottom = 3.0
grow_horizontal = 2
grow_vertical = 2
metadata/_edit_layout_mode = 1
[node name="Spacer1" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Title" type="Label" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
text = "Title"
[node name="Spacer2" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="TimeLabel" type="Label" parent="MainBox/TitleBar/TitleBox"]
custom_minimum_size = Vector2(90, 0)
layout_mode = 2
text = "999.999s"
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox"]
layout_mode = 2
size_flags_vertical = 3
[node name="VBoxContainer" type="VBoxContainer" parent="MainBox/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="OutputBG" type="ColorRect" parent="MainBox/HBoxContainer/VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
color = Color(0.0745098, 0.0705882, 0.0784314, 1)
metadata/_edit_layout_mode = 1
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG"]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="S2" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="TestOutput" type="RichTextLabel" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
focus_mode = 2
bbcode_enabled = true
scroll_following = true
autowrap_mode = 0
selection_enabled = true
[node name="S1" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="ControlBox" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer"]
layout_mode = 2
[node name="S1" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="ProgressBars" type="VBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
custom_minimum_size = Vector2(2.08165e-12, 2.08165e-12)
layout_mode = 2
[node name="TestBox" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars"]
layout_mode = 2
[node name="Label" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/TestBox"]
custom_minimum_size = Vector2(60, 0)
layout_mode = 2
size_flags_horizontal = 3
text = "Tests"
[node name="ProgressTest" type="ProgressBar" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/TestBox"]
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
value = 25.0
[node name="ScriptBox" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars"]
layout_mode = 2
[node name="Label" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/ScriptBox"]
custom_minimum_size = Vector2(60, 0)
layout_mode = 2
size_flags_horizontal = 3
text = "Scripts"
[node name="ProgressScript" type="ProgressBar" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/ScriptBox"]
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
value = 75.0
[node name="PathDisplay" type="VBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Path" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay"]
layout_mode = 2
size_flags_vertical = 6
theme_override_fonts/font = ExtResource("2_u5uc1")
theme_override_font_sizes/font_size = 14
text = "res://test/integration/whatever"
text_overrun_behavior = 3
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay"]
layout_mode = 2
size_flags_vertical = 3
[node name="S3" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay/HBoxContainer"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="File" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_fonts/font = ExtResource("2_u5uc1")
theme_override_font_sizes/font_size = 14
text = "test_this_thing.gd"
text_overrun_behavior = 3
[node name="Buttons" type="VBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
layout_mode = 2
[node name="Continue" type="Button" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/Buttons"]
layout_mode = 2
size_flags_vertical = 4
text = "Continue
"
[node name="WordWrap" type="CheckButton" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/Buttons"]
layout_mode = 2
text = "Word Wrap"
[node name="S3" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="BottomPad" type="CenterContainer" parent="MainBox"]
custom_minimum_size = Vector2(0, 5)
layout_mode = 2
[node name="Footer" type="HBoxContainer" parent="MainBox"]
layout_mode = 2
[node name="SidePad1" type="CenterContainer" parent="MainBox/Footer"]
custom_minimum_size = Vector2(2, 2.08165e-12)
layout_mode = 2
[node name="ResizeHandle3" parent="MainBox/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_2r8a8")]
custom_minimum_size = Vector2(25, 25)
layout_mode = 2
orientation = 0
resize_control = NodePath("../../..")
[node name="SwitchModes" type="Button" parent="MainBox/Footer"]
layout_mode = 2
text = "Compact
"
[node name="CenterContainer" type="CenterContainer" parent="MainBox/Footer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ResizeHandle2" parent="MainBox/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_2r8a8")]
custom_minimum_size = Vector2(25, 25)
layout_mode = 2
resize_control = NodePath("../../..")
[node name="SidePad2" type="CenterContainer" parent="MainBox/Footer"]
custom_minimum_size = Vector2(2, 2.08165e-12)
layout_mode = 2
[node name="BottomPad2" type="CenterContainer" parent="MainBox"]
custom_minimum_size = Vector2(2.08165e-12, 2)
layout_mode = 2

View File

@@ -0,0 +1,352 @@
@tool
extends VBoxContainer
var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')
var PanelControls = load('res://addons/gut/gui/panel_controls.gd')
# ##############################################################################
# Keeps search results from the TextEdit
# ##############################################################################
class TextEditSearcher:
var te : TextEdit
var _last_term = ''
var _last_pos = Vector2(-1, -1)
var _ignore_caret_change = false
func set_text_edit(which):
te = which
te.caret_changed.connect(_on_caret_changed)
func _on_caret_changed():
if(_ignore_caret_change):
_ignore_caret_change = false
else:
_last_pos = _get_caret();
func _get_caret():
return Vector2(te.get_caret_column(), te.get_caret_line())
func _set_caret_and_sel(pos, len):
te.set_caret_line(pos.y)
te.set_caret_column(pos.x)
if(len > 0):
te.select(pos.y, pos.x, pos.y, pos.x + len)
func _find(term, search_flags):
var pos = _get_caret()
if(term == _last_term):
if(search_flags == 0):
pos = _last_pos
pos.x += 1
else:
pos = _last_pos
pos.x -= 1
var result = te.search(term, search_flags, pos.y, pos.x)
# print('searching from ', pos, ' for "', term, '" = ', result)
if(result.y != -1):
_ignore_caret_change = true
_set_caret_and_sel(result, term.length())
_last_pos = result
_last_term = term
func find_next(term):
_find(term, 0)
func find_prev(term):
_find(term, te.SEARCH_BACKWARDS)
# ##############################################################################
# Start OutputText control code
# ##############################################################################
@onready var _ctrls = {
output = $Output,
settings_bar = $Settings,
use_colors = $Settings/UseColors,
word_wrap = $Settings/WordWrap,
copy_button = $Toolbar/CopyButton,
clear_button = $Toolbar/ClearButton,
show_search = $Toolbar/ShowSearch,
caret_position = $Toolbar/LblPosition,
search_bar = {
bar = $Search,
search_term = $Search/SearchTerm,
}
}
var _sr = TextEditSearcher.new()
var _highlighter : CodeHighlighter
var _font_name = null
var _user_prefs = GutEditorGlobals.user_prefs
var _font_name_pctrl = null
var _font_size_pctrl = null
var keywords = [
['Failed', Color.RED],
['Passed', Color.GREEN],
['Pending', Color.YELLOW],
['Risky', Color.YELLOW],
['Orphans', Color.YELLOW],
['WARNING', Color.YELLOW],
['ERROR', Color.RED],
['ExpectedError', Color.LIGHT_BLUE],
]
# Automatically used when running the OutputText scene from the editor. Changes
# to this method only affect test-running the control through the editor.
func _test_running_setup():
_ctrls.use_colors.text = 'use colors'
_ctrls.show_search.text = 'search'
_ctrls.word_wrap.text = 'ww'
set_all_fonts("CourierPrime")
set_font_size(30)
_ctrls.output.queue_redraw()
load_file('user://.gut_editor.bbcode')
await get_tree().process_frame
show_search(true)
_ctrls.output.set_caret_line(0)
_ctrls.output.scroll_vertical = 0
_ctrls.output.caret_changed.connect(_on_caret_changed)
func _ready():
if(get_parent() is SubViewport):
return
_sr.set_text_edit(_ctrls.output)
_ctrls.use_colors.icon = get_theme_icon('RichTextEffect', 'EditorIcons')
_ctrls.show_search.icon = get_theme_icon('Search', 'EditorIcons')
_ctrls.word_wrap.icon = get_theme_icon('Loop', 'EditorIcons')
_setup_colors()
_ctrls.use_colors.button_pressed = true
_use_highlighting(true)
if(get_parent() == get_tree().root):
_test_running_setup()
_ctrls.settings_bar.visible = false
_add_other_ctrls()
func _add_other_ctrls():
var fname = GutUtils.gut_fonts.DEFAULT_CUSTOM_FONT_NAME
if(_user_prefs != null):
fname = _user_prefs.output_font_name.value
_font_name_pctrl = PanelControls.SelectControl.new('Font', fname, GutUtils.avail_fonts,
"The font, you know, for the text below. Change it, see what it does.")
_font_name_pctrl.changed.connect(_on_font_name_changed)
_font_name_pctrl.label.size_flags_horizontal = SIZE_SHRINK_BEGIN
_ctrls.settings_bar.add_child(_font_name_pctrl)
set_all_fonts(fname)
var fsize = 30
if(_user_prefs != null):
fsize = _user_prefs.output_font_size.value
_font_size_pctrl = PanelControls.NumberControl.new('Font Size', fsize , 5, 100,
"The size of 'The Font'.")
_font_size_pctrl.changed.connect(_on_font_size_changed)
_font_size_pctrl.label.size_flags_horizontal = SIZE_SHRINK_BEGIN
_ctrls.settings_bar.add_child(_font_size_pctrl)
set_font_size(fsize)
# ------------------
# Private
# ------------------
# Call this after changes in colors and the like to get them to apply. reloads
# the text of the output control.
func _refresh_output():
var orig_pos = _ctrls.output.scroll_vertical
var text = _ctrls.output.text
_ctrls.output.text = text
_ctrls.output.scroll_vertical = orig_pos
func _create_highlighter(default_color=Color(1, 1, 1, 1)):
var to_return = CodeHighlighter.new()
to_return.function_color = default_color
to_return.number_color = default_color
to_return.symbol_color = default_color
to_return.member_variable_color = default_color
for keyword in keywords:
to_return.add_keyword_color(keyword[0], keyword[1])
return to_return
func _setup_colors():
_ctrls.output.clear()
_highlighter = _create_highlighter()
_ctrls.output.queue_redraw()
func _use_highlighting(should):
if(should):
_ctrls.output.syntax_highlighter = _highlighter
else:
_ctrls.output.syntax_highlighter = null
_refresh_output()
# ------------------
# Events
# ------------------
func _on_caret_changed():
var txt = str("line:",_ctrls.output.get_caret_line(), ' col:', _ctrls.output.get_caret_column())
_ctrls.caret_position.text = str(txt)
func _on_font_size_changed():
set_font_size(_font_size_pctrl.value)
if(_user_prefs != null):
_user_prefs.output_font_size.value = _font_size_pctrl.value
_user_prefs.output_font_size.save_it()
func _on_font_name_changed():
set_all_fonts(_font_name_pctrl.text)
if(_user_prefs != null):
_user_prefs.output_font_name.value = _font_name_pctrl.text
_user_prefs.output_font_name.save_it()
func _on_CopyButton_pressed():
copy_to_clipboard()
func _on_UseColors_pressed():
_use_highlighting(_ctrls.use_colors.button_pressed)
func _on_ClearButton_pressed():
clear()
func _on_ShowSearch_pressed():
show_search(_ctrls.show_search.button_pressed)
func _on_SearchTerm_focus_entered():
_ctrls.search_bar.search_term.call_deferred('select_all')
func _on_SearchNext_pressed():
_sr.find_next(_ctrls.search_bar.search_term.text)
func _on_SearchPrev_pressed():
_sr.find_prev(_ctrls.search_bar.search_term.text)
func _on_SearchTerm_text_changed(new_text):
if(new_text == ''):
_ctrls.output.deselect()
else:
_sr.find_next(new_text)
func _on_SearchTerm_text_entered(new_text):
if(Input.is_physical_key_pressed(KEY_SHIFT)):
_sr.find_prev(new_text)
else:
_sr.find_next(new_text)
func _on_SearchTerm_gui_input(event):
if(event is InputEventKey and !event.pressed and event.keycode == KEY_ESCAPE):
show_search(false)
func _on_WordWrap_pressed():
if(_ctrls.word_wrap.button_pressed):
_ctrls.output.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
else:
_ctrls.output.wrap_mode = TextEdit.LINE_WRAPPING_NONE
_ctrls.output.queue_redraw()
func _on_settings_pressed():
_ctrls.settings_bar.visible = $Toolbar/ShowSettings.button_pressed
# ------------------
# Public
# ------------------
func show_search(should):
_ctrls.search_bar.bar.visible = should
if(should):
_ctrls.search_bar.search_term.grab_focus()
_ctrls.search_bar.search_term.select_all()
_ctrls.show_search.button_pressed = should
func search(text, start_pos, highlight=true):
return _sr.find_next(text)
func copy_to_clipboard():
var selected = _ctrls.output.get_selected_text()
if(selected != ''):
DisplayServer.clipboard_set(selected)
else:
DisplayServer.clipboard_set(_ctrls.output.text)
func clear():
_ctrls.output.text = ''
func _set_font(custom_name, theme_font_name):
var font = GutUtils.gut_fonts.get_font_for_theme_font_name(theme_font_name, custom_name)
_ctrls.output.add_theme_font_override(theme_font_name, font)
func set_all_fonts(base_name):
_font_name = GutUtils.nvl(base_name, 'Default')
_set_font(base_name, 'font')
_set_font(base_name, 'normal_font')
_set_font(base_name, 'bold_font')
_set_font(base_name, 'italics_font')
_set_font(base_name, 'bold_italics_font')
func set_font_size(new_size):
_ctrls.output.set("theme_override_font_sizes/font_size", new_size)
func set_use_colors(value):
pass
func get_use_colors():
return false;
func get_rich_text_edit():
return _ctrls.output
func load_file(path):
var f = FileAccess.open(path, FileAccess.READ)
if(f == null):
return
var t = f.get_as_text()
f = null # closes file
_ctrls.output.text = t
_ctrls.output.scroll_vertical = _ctrls.output.get_line_count()
_ctrls.output.set_deferred('scroll_vertical', _ctrls.output.get_line_count())
func add_text(text):
if(is_inside_tree()):
_ctrls.output.text += text
func scroll_to_line(line):
_ctrls.output.scroll_vertical = line
_ctrls.output.set_caret_line(line)

View File

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

View File

@@ -0,0 +1,121 @@
[gd_scene load_steps=5 format=3 uid="uid://bqmo4dj64c7yl"]
[ext_resource type="Script" uid="uid://cax5phqs8acmu" path="res://addons/gut/gui/OutputText.gd" id="1"]
[ext_resource type="Texture2D" uid="uid://bvo0uao7deu0q" path="res://addons/gut/icon.png" id="2_b4xqv"]
[sub_resource type="DPITexture" id="DPITexture_lygvu"]
_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><path fill=\"#ff5d5d\" d=\"M2 1v8.586l1.293-1.293a1 1 0 0 1 1.414 0L7 10.587l2.293-2.293a1 1 0 0 1 1.414 0L13 10.586l1-1V6H9V1H2zm8 0v4h4zm-6 9.414-2 2V15h12v-2.586l-.293.293a1 1 0 0 1-1.414 0L10 10.414l-2.293 2.293a1 1 0 0 1-1.414 0L4 10.414z\"/></svg>
"
[sub_resource type="CodeHighlighter" id="CodeHighlighter_8ynmy"]
number_color = Color(1, 1, 1, 1)
symbol_color = Color(1, 1, 1, 1)
function_color = Color(1, 1, 1, 1)
member_variable_color = Color(1, 1, 1, 1)
keyword_colors = {
"ERROR": Color(1, 0, 0, 1),
"ExpectedError": Color(0.6784314, 0.84705883, 0.9019608, 1),
"Failed": Color(1, 0, 0, 1),
"Orphans": Color(1, 1, 0, 1),
"Passed": Color(0, 1, 0, 1),
"Pending": Color(1, 1, 0, 1),
"Risky": Color(1, 1, 0, 1),
"WARNING": Color(1, 1, 0, 1)
}
[node name="OutputText" type="VBoxContainer"]
offset_right = 862.0
offset_bottom = 523.0
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource("1")
[node name="Toolbar" type="HBoxContainer" parent="."]
layout_mode = 2
size_flags_horizontal = 3
[node name="ShowSearch" type="Button" parent="Toolbar"]
layout_mode = 2
tooltip_text = "Search"
toggle_mode = true
icon = ExtResource("2_b4xqv")
[node name="ShowSettings" type="Button" parent="Toolbar"]
layout_mode = 2
tooltip_text = "Settings"
toggle_mode = true
text = "..."
[node name="CenterContainer" type="CenterContainer" parent="Toolbar"]
layout_mode = 2
size_flags_horizontal = 3
[node name="LblPosition" type="Label" parent="Toolbar"]
layout_mode = 2
[node name="CopyButton" type="Button" parent="Toolbar"]
layout_mode = 2
text = " Copy "
[node name="ClearButton" type="Button" parent="Toolbar"]
layout_mode = 2
text = " Clear "
[node name="Settings" type="HBoxContainer" parent="."]
visible = false
layout_mode = 2
[node name="WordWrap" type="Button" parent="Settings"]
layout_mode = 2
tooltip_text = "Word Wrap"
toggle_mode = true
icon = SubResource("DPITexture_lygvu")
[node name="UseColors" type="Button" parent="Settings"]
layout_mode = 2
tooltip_text = "Colorized Text"
toggle_mode = true
button_pressed = true
icon = SubResource("DPITexture_lygvu")
[node name="Output" type="TextEdit" parent="."]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_font_sizes/font_size = 30
deselect_on_focus_loss_enabled = false
virtual_keyboard_enabled = false
middle_mouse_paste_enabled = false
scroll_smooth = true
syntax_highlighter = SubResource("CodeHighlighter_8ynmy")
highlight_all_occurrences = true
highlight_current_line = true
[node name="Search" type="HBoxContainer" parent="."]
visible = false
layout_mode = 2
[node name="SearchTerm" type="LineEdit" parent="Search"]
layout_mode = 2
size_flags_horizontal = 3
[node name="SearchNext" type="Button" parent="Search"]
layout_mode = 2
text = "Next"
[node name="SearchPrev" type="Button" parent="Search"]
layout_mode = 2
text = "Prev"
[connection signal="pressed" from="Toolbar/ShowSearch" to="." method="_on_ShowSearch_pressed"]
[connection signal="pressed" from="Toolbar/ShowSettings" to="." method="_on_settings_pressed"]
[connection signal="pressed" from="Toolbar/CopyButton" to="." method="_on_CopyButton_pressed"]
[connection signal="pressed" from="Toolbar/ClearButton" to="." method="_on_ClearButton_pressed"]
[connection signal="pressed" from="Settings/WordWrap" to="." method="_on_WordWrap_pressed"]
[connection signal="pressed" from="Settings/UseColors" to="." method="_on_UseColors_pressed"]
[connection signal="focus_entered" from="Search/SearchTerm" to="." method="_on_SearchTerm_focus_entered"]
[connection signal="gui_input" from="Search/SearchTerm" to="." method="_on_SearchTerm_gui_input"]
[connection signal="text_changed" from="Search/SearchTerm" to="." method="_on_SearchTerm_text_changed"]
[connection signal="text_submitted" from="Search/SearchTerm" to="." method="_on_SearchTerm_text_entered"]
[connection signal="pressed" from="Search/SearchNext" to="." method="_on_SearchNext_pressed"]
[connection signal="pressed" from="Search/SearchPrev" to="." method="_on_SearchPrev_pressed"]

View File

@@ -0,0 +1,107 @@
@tool
extends ColorRect
# #############################################################################
# Resize Handle control. Place onto a control. Set the orientation, then
# set the control that this should resize. Then you can resize the control
# by dragging this thing around. It's pretty neat.
# #############################################################################
enum ORIENTATION {
LEFT,
RIGHT
}
@export var orientation := ORIENTATION.RIGHT :
get: return orientation
set(val):
orientation = val
queue_redraw()
@export var resize_control : Control = null
@export var vertical_resize := true
var _line_width = .5
var _line_color = Color(.4, .4, .4)
var _active_line_color = Color(.3, .3, .3)
var _invalid_line_color = Color(1, 0, 0)
var _line_space = 3
var _num_lines = 8
var _mouse_down = false
# Called when the node enters the scene tree for the first time.
func _draw():
var c = _line_color
if(resize_control == null):
c = _invalid_line_color
elif(_mouse_down):
c = _active_line_color
if(orientation == ORIENTATION.LEFT):
_draw_resize_handle_left(c)
else:
_draw_resize_handle_right(c)
func _gui_input(event):
if(resize_control == null):
return
if(orientation == ORIENTATION.LEFT):
_handle_left_input(event)
else:
_handle_right_input(event)
# Draw the lines in the corner to show where you can
# drag to resize the dialog
func _draw_resize_handle_right(draw_color):
var br = size
for i in range(_num_lines):
var start = br - Vector2(i * _line_space, 0)
var end = br - Vector2(0, i * _line_space)
draw_line(start, end, draw_color, _line_width, true)
func _draw_resize_handle_left(draw_color):
var bl = Vector2(0, size.y)
for i in range(_num_lines):
var start = bl + Vector2(i * _line_space, 0)
var end = bl - Vector2(0, i * _line_space)
draw_line(start, end, draw_color, _line_width, true)
func _handle_right_input(event : InputEvent):
if(event is InputEventMouseMotion):
if(_mouse_down and
event.global_position.x > 0 and
event.global_position.y < DisplayServer.window_get_size().y):
if(vertical_resize):
resize_control.size.y += event.relative.y
resize_control.size.x += event.relative.x
elif(event is InputEventMouseButton):
if(event.button_index == MOUSE_BUTTON_LEFT):
_mouse_down = event.pressed
queue_redraw()
func _handle_left_input(event : InputEvent):
if(event is InputEventMouseMotion):
if(_mouse_down and
event.global_position.x > 0 and
event.global_position.y < DisplayServer.window_get_size().y):
var start_size = resize_control.size
resize_control.size.x -= event.relative.x
if(resize_control.size.x != start_size.x):
resize_control.global_position.x += event.relative.x
if(vertical_resize):
resize_control.size.y += event.relative.y
elif(event is InputEventMouseButton):
if(event.button_index == MOUSE_BUTTON_LEFT):
_mouse_down = event.pressed
queue_redraw()

View File

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

View File

@@ -0,0 +1,8 @@
[gd_scene load_steps=2 format=3 uid="uid://bvrqqgjpyouse"]
[ext_resource type="Script" uid="uid://duf6rfdqr6yoc" path="res://addons/gut/gui/ResizeHandle.gd" id="1_oi5ed"]
[node name="ResizeHandle" type="ColorRect"]
custom_minimum_size = Vector2(20, 20)
color = Color(1, 1, 1, 0)
script = ExtResource("1_oi5ed")

View File

@@ -0,0 +1,368 @@
@tool
extends Tree
var _show_orphans = true
var show_orphans = true :
get: return _show_orphans
set(val): _show_orphans = val
var _hide_passing = true
var hide_passing = true :
get: return _hide_passing
set(val): _hide_passing = val
var _icons = {
red = load('res://addons/gut/images/red.png'),
green = load('res://addons/gut/images/green.png'),
yellow = load('res://addons/gut/images/yellow.png'),
}
@export var script_entry_color : Color = Color(0, 0, 0, .2) :
set(val):
if(val != null):
script_entry_color = val
@export var column_0_color : Color = Color(1, 1, 1, 0) :
set(val):
if(val != null):
column_0_color = val
@export var column_1_color : Color = Color(0, 0, 0, .2):
set(val):
if(val != null):
column_1_color = val
var _max_icon_width = 10
var _root : TreeItem
@onready var lbl_overlay = $TextOverlay
signal selected(script_path, inner_class, test_name, line_number)
func _debug_ready():
hide_passing = false
load_json_file('user://gut_temp_directory/gut_editor.json')
func _ready():
_root = create_item()
set_hide_root(true)
columns = 2
set_column_expand(0, true)
set_column_expand_ratio(0, 5)
set_column_expand_ratio(1, 1)
set_column_expand(1, true)
item_selected.connect(_on_tree_item_selected)
if(get_parent() == get_tree().root):
_debug_ready()
# -------------------
# Private
# -------------------
func _get_line_number_from_assert_msg(msg):
var line = -1
if(msg.find('at line') > 0):
line = msg.split("at line")[-1].split(" ")[-1].to_int()
return line
func _get_path_and_inner_class_name_from_test_path(path):
var to_return = {
path = '',
inner_class = ''
}
to_return.path = path
if !path.ends_with('.gd'):
var loc = path.find('.gd')
to_return.inner_class = path.split('.')[-1]
to_return.path = path.substr(0, loc + 3)
return to_return
func _find_script_item_with_path(path):
var items = _root.get_children()
var to_return = null
var idx = 0
while(idx < items.size() and to_return == null):
var item = items[idx]
if(item.get_metadata(0).path == path):
to_return = item
else:
idx += 1
return to_return
func _add_script_tree_item(script_path, script_json):
var path_info = _get_path_and_inner_class_name_from_test_path(script_path)
var item_text = script_path
var parent = _root
if(path_info.inner_class != ''):
parent = _find_script_item_with_path(path_info.path)
item_text = path_info.inner_class
if(parent == null):
parent = _add_script_tree_item(path_info.path, {})
var item = create_item(parent)
item.set_text(0, item_text)
var meta = {
"type":"script",
"path":path_info.path,
"inner_class":path_info.inner_class,
"json":script_json,
"inner_passing":0,
"inner_tests":0
}
item.set_metadata(0, meta)
item.set_custom_bg_color(0, script_entry_color)
item.set_custom_bg_color(1, script_entry_color)
return item
func _add_assert_item(text, icon, parent_item):
# print(' * adding assert')
var assert_item = create_item(parent_item)
assert_item.set_icon_max_width(0, _max_icon_width)
assert_item.set_text(0, text)
assert_item.set_metadata(0, {"type":"assert"})
assert_item.set_icon(0, icon)
assert_item.set_custom_bg_color(0, column_0_color)
assert_item.set_custom_bg_color(1, column_1_color)
return assert_item
func _add_test_tree_item(test_name, test_json, script_item):
# print(' * adding test ', test_name)
var no_orphans_to_show = !_show_orphans or (_show_orphans and test_json.orphan_count == 0)
if(_hide_passing and test_json['status'] == 'pass' and no_orphans_to_show):
return
var item = create_item(script_item)
var status = test_json['status']
var meta = {"type":"test", "json":test_json}
item.set_text(0, test_name)
item.set_text(1, status)
item.set_text_alignment(1, HORIZONTAL_ALIGNMENT_RIGHT)
item.set_custom_bg_color(1, column_1_color)
item.set_metadata(0, meta)
item.set_icon_max_width(0, _max_icon_width)
item.set_custom_bg_color(0, column_0_color)
if(status == 'pass' and no_orphans_to_show):
item.set_icon(0, _icons.green)
elif(status == 'fail'):
item.set_icon(0, _icons.red)
else:
item.set_icon(0, _icons.yellow)
if(!_hide_passing):
for passing in test_json.passing:
_add_assert_item('pass: ' + passing, _icons.green, item)
for failure in test_json.failing:
_add_assert_item("fail: " + failure.replace("\n", ''), _icons.red, item)
for pending in test_json.pending:
_add_assert_item("pending: " + pending.replace("\n", ''), _icons.yellow, item)
var orphan_text = 'orphans'
if(test_json.orphan_count == 1):
orphan_text = 'orphan'
orphan_text = str(int(test_json.orphan_count), ' ', orphan_text)
if(!no_orphans_to_show):
var orphan_item = _add_assert_item(orphan_text, _icons.yellow, item)
for o in test_json.orphans:
var orphan_entry = create_item(orphan_item)
orphan_entry.set_text(0, o)
orphan_entry.set_custom_bg_color(0, column_0_color)
orphan_entry.set_custom_bg_color(1, column_1_color)
return item
func _add_script_to_tree(key, script_json):
var tests = script_json['tests']
var test_keys = tests.keys()
var s_item = _add_script_tree_item(key, script_json)
var bad_count = 0
for test_key in test_keys:
var t_item = _add_test_tree_item(test_key, tests[test_key], s_item)
if(tests[test_key].status != 'pass'):
bad_count += 1
elif(t_item != null):
t_item.collapsed = true
if(s_item.get_children().size() == 0):
if(script_json.props.skipped):
_add_assert_item("Skipped", _icons.yellow, s_item)
s_item.set_text(1, "Skipped")
else:
s_item.free()
else:
var total_text = str('All ', test_keys.size(), ' passed')
if(bad_count == 0):
s_item.collapsed = true
else:
total_text = str(int(test_keys.size() - bad_count), '/', int(test_keys.size()), ' passed')
s_item.set_text(1, total_text)
func _free_childless_scripts():
var items = _root.get_children()
for item in items:
var next_item = item.get_next()
if(item.get_children().size() == 0):
item.free()
item = next_item
func _show_all_passed():
if(_root.get_children().size() == 0):
add_centered_text('Everything passed!')
func _load_result_tree(j):
var scripts = j['test_scripts']['scripts']
var script_keys = scripts.keys()
# if we made it here, the json is valid and we did something, otherwise the
# 'nothing to see here' should be visible.
clear_centered_text()
var add_count = 0
for key in script_keys:
add_count += 1
_add_script_to_tree(key, scripts[key])
_free_childless_scripts()
if(add_count == 0):
add_centered_text('Nothing was run')
else:
_show_all_passed()
# -------------------
# Events
# -------------------
func _on_tree_item_selected():
var item = get_selected()
var item_meta = item.get_metadata(0)
var item_type = null
# Only select the left side of the tree item, cause I like that better.
# you can still click the right, but only the left gets highlighted.
if(item.is_selected(1)):
item.deselect(1)
item.select(0)
if(item_meta == null):
return
else:
item_type = item_meta.type
var script_path = '';
var line = -1;
var test_name = ''
var inner_class = ''
if(item_type == 'test'):
var s_item = item.get_parent()
script_path = s_item.get_metadata(0)['path']
inner_class = s_item.get_metadata(0)['inner_class']
line = -1
test_name = item.get_text(0)
elif(item_type == 'assert'):
var s_item = item.get_parent().get_parent()
script_path = s_item.get_metadata(0)['path']
inner_class = s_item.get_metadata(0)['inner_class']
line = _get_line_number_from_assert_msg(item.get_text(0))
test_name = item.get_parent().get_text(0)
elif(item_type == 'script'):
script_path = item.get_metadata(0)['path']
if(item.get_parent() != _root):
inner_class = item.get_text(0)
line = -1
test_name = ''
else:
return
selected.emit(script_path, inner_class, test_name, line)
# -------------------
# Public
# -------------------
func load_json_file(path):
var file = FileAccess.open(path, FileAccess.READ)
var text = ''
if(file != null):
text = file.get_as_text()
if(text != ''):
var test_json_conv = JSON.new()
var result = test_json_conv.parse(text)
if(result != OK):
add_centered_text(str(path, " has invalid json in it \n",
'Error ', result, "@", test_json_conv.get_error_line(), "\n",
test_json_conv.get_error_message()))
return
var data = test_json_conv.get_data()
load_json_results(data)
else:
add_centered_text(str(path, ' was empty or does not exist.'))
func load_json_results(j):
clear()
if(_root == null):
_root = create_item()
_load_result_tree(j)
#func clear():
#clear()
#_root = create_item()
func set_summary_min_width(width):
set_column_custom_minimum_width(1, width)
func add_centered_text(t):
lbl_overlay.visible = true
lbl_overlay.text = t
func clear_centered_text():
lbl_overlay.visible = false
lbl_overlay.text = ''
func collapse_all():
set_collapsed_on_all(_root, true)
func expand_all():
set_collapsed_on_all(_root, false)
func set_collapsed_on_all(item, value):
item.set_collapsed_recursive(value)
if(item == _root and value):
item.set_collapsed(false)

View File

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

View File

@@ -0,0 +1,34 @@
[gd_scene load_steps=2 format=3 uid="uid://dls5r5f6157nq"]
[ext_resource type="Script" uid="uid://dehdhn78qv5tr" path="res://addons/gut/gui/ResultsTree.gd" id="1_b4uub"]
[node name="ResultsTree" type="Tree"]
offset_right = 1082.0
offset_bottom = 544.0
size_flags_horizontal = 3
size_flags_vertical = 3
columns = 2
hide_root = true
script = ExtResource("1_b4uub")
[node name="TextOverlay" type="Label" parent="."]
visible = false
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="ResultsTree" type="VBoxContainer" parent="."]
custom_minimum_size = Vector2(10, 10)
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_right = -70.0
offset_bottom = -104.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3

View File

@@ -0,0 +1,171 @@
@tool
extends Control
var EditorCaretContextNotifier = load('res://addons/gut/editor_caret_context_notifier.gd')
@onready var _ctrls = {
btn_script = $HBox/BtnRunScript,
btn_inner = $HBox/BtnRunInnerClass,
btn_method = $HBox/BtnRunMethod,
lbl_none = $HBox/LblNoneSelected,
arrow_1 = $HBox/Arrow1,
arrow_2 = $HBox/Arrow2
}
var _caret_notifier = null
var _last_info = {
script = null,
inner_class = null,
method = null
}
var disabled = false :
set(val):
disabled = val
if(is_inside_tree()):
_ctrls.btn_script.disabled = val
_ctrls.btn_inner.disabled = val
_ctrls.btn_method.disabled = val
var method_prefix = 'test_'
var inner_class_prefix = 'Test'
var menu_manager = null :
set(val):
menu_manager = val
menu_manager.run_script.connect(_on_BtnRunScript_pressed)
menu_manager.run_at_cursor.connect(run_at_cursor)
menu_manager.rerun.connect(rerun)
menu_manager.run_inner_class.connect(_on_BtnRunInnerClass_pressed)
menu_manager.run_test.connect(_on_BtnRunMethod_pressed)
_update_buttons(_last_info)
signal run_tests(what)
func _ready():
_ctrls.lbl_none.visible = true
_ctrls.btn_script.visible = false
_ctrls.btn_inner.visible = false
_ctrls.btn_method.visible = false
_ctrls.arrow_1.visible = false
_ctrls.arrow_2.visible = false
_caret_notifier = EditorCaretContextNotifier.new()
add_child(_caret_notifier)
_caret_notifier.it_changed.connect(_on_caret_notifer_changed)
disabled = disabled
func _on_caret_notifer_changed(data):
if(data.is_test_script):
_last_info = data
_update_buttons(_last_info)
# ----------------
# Private
# ----------------
func _update_buttons(info):
_ctrls.lbl_none.visible = false
_ctrls.btn_script.visible = info.script != null
if(info.script != null and info.is_test_script):
_ctrls.btn_script.text = info.script.resource_path.get_file()
_ctrls.btn_inner.visible = info.inner_class != null
_ctrls.arrow_1.visible = info.inner_class != null
_ctrls.btn_inner.text = str(info.inner_class)
_ctrls.btn_inner.tooltip_text = str("Run all tests in Inner-Test-Class ", info.inner_class)
var is_test_method = info.method != null and info.method.begins_with(method_prefix)
_ctrls.btn_method.visible = is_test_method
_ctrls.arrow_2.visible = is_test_method
if(is_test_method):
_ctrls.btn_method.text = str(info.method)
_ctrls.btn_method.tooltip_text = str("Run test ", info.method)
if(menu_manager != null):
menu_manager.disable_menu("run_script", info.script == null)
menu_manager.disable_menu("run_inner_class", info.inner_class == null)
menu_manager.disable_menu("run_at_cursor", info.script == null)
menu_manager.disable_menu("run_test", is_test_method)
menu_manager.disable_menu("rerun", _last_run_info == {})
# The button's new size won't take effect until the next frame.
# This appears to be what was causing the button to not be clickable the
# first time.
_update_size.call_deferred()
func _update_size():
custom_minimum_size.x = _ctrls.btn_method.size.x + _ctrls.btn_method.position.x
var _last_run_info = {}
func _emit_run_tests(info):
_last_run_info = info.duplicate()
run_tests.emit(info)
# ----------------
# Events
# ----------------
func _on_BtnRunScript_pressed():
var info = _last_info.duplicate()
info.script = info.script.resource_path.get_file()
info.inner_class = null
info.method = null
_emit_run_tests(info)
func _on_BtnRunInnerClass_pressed():
var info = _last_info.duplicate()
info.script = info.script.resource_path.get_file()
info.method = null
_emit_run_tests(info)
func _on_BtnRunMethod_pressed():
var info = _last_info.duplicate()
info.script = info.script.resource_path.get_file()
_emit_run_tests(info)
# ----------------
# Public
# ----------------
func rerun():
if(_last_run_info != {}):
_emit_run_tests(_last_run_info)
func run_at_cursor():
if(_ctrls.btn_method.visible):
_on_BtnRunMethod_pressed()
elif(_ctrls.btn_inner.visible):
_on_BtnRunInnerClass_pressed()
elif(_ctrls.btn_script.visible):
_on_BtnRunScript_pressed()
else:
print("nothing selected")
func get_script_button():
return _ctrls.btn_script
func get_inner_button():
return _ctrls.btn_inner
func get_test_button():
return _ctrls.btn_method
func set_inner_class_prefix(value):
_caret_notifier.inner_class_prefix = value
func apply_gut_config(gut_config):
_caret_notifier.script_prefix = gut_config.options.prefix
_caret_notifier.script_suffix = gut_config.options.suffix

View File

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

View File

@@ -0,0 +1,60 @@
[gd_scene load_steps=3 format=3 uid="uid://0yunjxtaa8iw"]
[ext_resource type="Script" uid="uid://c4gmgdl1xwflw" path="res://addons/gut/gui/RunAtCursor.gd" id="1"]
[ext_resource type="Texture2D" uid="uid://6wra5rxmfsrl" path="res://addons/gut/gui/arrow.png" id="3"]
[node name="RunAtCursor" type="Control"]
custom_minimum_size = Vector2(510, 0)
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_right = 1.0
offset_bottom = -527.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource("1")
[node name="HBox" type="HBoxContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="LblNoneSelected" type="Label" parent="HBox"]
visible = false
layout_mode = 2
text = "<None>"
[node name="BtnRunScript" type="Button" parent="HBox"]
layout_mode = 2
text = "test_test.gd"
[node name="Arrow1" type="TextureButton" parent="HBox"]
custom_minimum_size = Vector2(24, 0)
layout_mode = 2
texture_normal = ExtResource("3")
stretch_mode = 3
[node name="BtnRunInnerClass" type="Button" parent="HBox"]
layout_mode = 2
tooltip_text = "Run all tests in Inner-Test-Class TestAssertNe"
text = "TestAssertNe"
[node name="Arrow2" type="TextureButton" parent="HBox"]
custom_minimum_size = Vector2(24, 0)
layout_mode = 2
texture_normal = ExtResource("3")
stretch_mode = 3
[node name="BtnRunMethod" type="Button" parent="HBox"]
layout_mode = 2
tooltip_text = "Run test test_fails_with_integers_equal"
text = "test_fails_with_integers_equal"
[connection signal="pressed" from="HBox/BtnRunScript" to="." method="_on_BtnRunScript_pressed"]
[connection signal="pressed" from="HBox/BtnRunInnerClass" to="." method="_on_BtnRunInnerClass_pressed"]
[connection signal="pressed" from="HBox/BtnRunMethod" to="." method="_on_BtnRunMethod_pressed"]

View File

@@ -0,0 +1,216 @@
@tool
extends Control
# I'm probably going to put this back in later and I don't want to create it
# again. Yeah, yeah, yeah.
# class DotsAnimator:
# var text = ''
# var dot = '.'
# var max_dots = 3
# var dot_delay = .5
# var _anim_text = ''
# var _elapsed_time = 0.0
# var _cur_dots = 0
# func get_animated_text():
# return _anim_text
# func add_time(delta):
# _elapsed_time += delta
# if(_elapsed_time > dot_delay):
# _elapsed_time = 0
# _cur_dots += 1
# if(_cur_dots > max_dots):
# _cur_dots = 0
# _anim_text = text.rpad(text.length() + _cur_dots, dot)
var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')
@onready var btn_kill_it = $BgControl/VBox/Kill
@onready var bg_control = $BgControl
var _pipe_results = {}
var _debug_mode = false
var _std_thread : Thread
var _escape_regex : RegEx = RegEx.new()
var _text_buffer = ''
var bottom_panel = null :
set(val):
bottom_panel = val
bottom_panel.resized.connect(_on_bottom_panel_resized)
var blocking_mode = "Blocking"
var additional_arguments = []
var remove_escape_characters = true
@export var bg_color = Color.WHITE:
set(val):
bg_color = val
if(is_inside_tree()):
bg_control.get("theme_override_styles/panel").bg_color = bg_color
func _debug_ready():
_debug_mode = true
additional_arguments = ['-gselect', 'test_awaiter.gd', '-gconfig', 'res://.gutconfig.json'] # '-gunit_test_name', 'test_can_clear_spies'
blocking_mode = "NonBlocking"
run_tests()
func _ready():
_escape_regex.compile("\\x1b\\[[0-9;]*m")
btn_kill_it.visible = false
if(get_parent() == get_tree().root):
_debug_ready.call_deferred()
bg_color = bg_color
func _process(_delta: float) -> void:
if(_pipe_results != {}):
if(!OS.is_process_running(_pipe_results.pid)):
_end_non_blocking()
# ----------
# Private
# ----------
func _center_me():
position = get_parent().size / 2.0 - size / 2.0
func _output_text(text, should_scroll = true):
if(_debug_mode):
print(text)
else:
if(remove_escape_characters):
text = _escape_regex.sub(text, '', true)
if(bottom_panel != null):
bottom_panel.add_output_text(text)
if(should_scroll):
_scroll_output_pane(-1)
else:
_text_buffer += text
func _scroll_output_pane(line):
if(!_debug_mode and bottom_panel != null):
var txt_ctrl = bottom_panel.get_text_output_control().get_rich_text_edit()
if(line == -1):
line = txt_ctrl.get_line_count()
txt_ctrl.scroll_vertical = line
func _add_arguments_to_output():
if(additional_arguments.size() != 0):
_output_text(
str("Run Mode arguments: ", ' '.join(additional_arguments), "\n\n")
)
func _load_json():
if(_debug_mode):
pass # could load file and print it if we want.
elif(bottom_panel != null):
bottom_panel.load_result_json()
func _run_blocking(options):
btn_kill_it.visible = false
var output = []
await get_tree().create_timer(.1).timeout
OS.execute(OS.get_executable_path(), options, output, true)
_output_text(output[0])
_add_arguments_to_output()
_scroll_output_pane(-1)
_load_json()
queue_free()
func _read_non_blocking_stdio():
while(OS.is_process_running(_pipe_results.pid)):
while(_pipe_results.stderr.get_length() > 0):
_output_text(_pipe_results.stderr.get_line() + "\n")
while(_pipe_results.stdio.get_length() > 0):
_output_text(_pipe_results.stdio.get_line() + "\n")
# without this, things start to lock up.
await get_tree().process_frame
func _run_non_blocking(options):
_pipe_results = OS.execute_with_pipe(OS.get_executable_path(), options, false)
_std_thread = Thread.new()
_std_thread.start(_read_non_blocking_stdio)
btn_kill_it.visible = true
func _end_non_blocking():
_add_arguments_to_output()
_scroll_output_pane(-1)
_load_json()
_pipe_results = {}
_std_thread.wait_to_finish()
_std_thread = null
queue_free()
if(_debug_mode):
get_tree().quit()
# ----------------
# Events
# ----------------
func _on_kill_pressed() -> void:
if(_pipe_results != {} and OS.is_process_running(_pipe_results.pid)):
OS.kill(_pipe_results.pid)
btn_kill_it.visible = false
func _on_color_rect_gui_input(event: InputEvent) -> void:
if(event is InputEventMouseMotion):
if(event.button_mask == MOUSE_BUTTON_MASK_LEFT):
position += event.relative
func _on_bottom_panel_resized():
_center_me()
# ----------------
# Public
# ----------------
func run_tests():
_center_me()
var options = ["-s", "res://addons/gut/gut_cmdln.gd", "-graie", "-gdisable_colors",
"-gconfig", GutEditorGlobals.editor_run_gut_config_path]
options.append_array(additional_arguments)
if(blocking_mode == 'Blocking'):
_run_blocking(options)
else:
_run_non_blocking(options)
func get_godot_help():
_text_buffer = ''
var options = ["--help", "--headless"]
await _run_blocking(options)
return _text_buffer
func get_gut_help():
_text_buffer = ''
var options = ["-s", "res://addons/gut/gut_cmdln.gd", "-gh", "--headless"]
await _run_blocking(options)
return _text_buffer

View File

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

View File

@@ -0,0 +1,65 @@
[gd_scene load_steps=3 format=3 uid="uid://cftcb0e6g7tu1"]
[ext_resource type="Script" uid="uid://bi8pg352un4om" path="res://addons/gut/gui/RunExternally.gd" id="1_lrqqi"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_haowt"]
bg_color = Color(0.025935074, 0.17817128, 0.30283752, 1)
corner_radius_top_left = 20
corner_radius_top_right = 20
corner_radius_bottom_right = 20
corner_radius_bottom_left = 20
shadow_size = 5
shadow_offset = Vector2(7, 7)
[node name="DoShellOut" type="Control"]
layout_mode = 3
anchors_preset = 0
offset_right = 774.0
offset_bottom = 260.0
script = ExtResource("1_lrqqi")
bg_color = Color(0.025935074, 0.17817128, 0.30283752, 1)
[node name="BgControl" type="Panel" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_haowt")
[node name="VBox" type="VBoxContainer" parent="BgControl"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Spacer" type="CenterContainer" parent="BgControl/VBox"]
layout_mode = 2
size_flags_vertical = 3
[node name="Title" type="Label" parent="BgControl/VBox"]
layout_mode = 2
text = "Running Tests"
horizontal_alignment = 1
[node name="Spacer2" type="CenterContainer" parent="BgControl/VBox"]
visible = false
layout_mode = 2
size_flags_vertical = 3
[node name="Kill" type="Button" parent="BgControl/VBox"]
visible = false
custom_minimum_size = Vector2(200, 50)
layout_mode = 2
size_flags_horizontal = 4
text = "Stop"
[node name="Spacer3" type="CenterContainer" parent="BgControl/VBox"]
layout_mode = 2
size_flags_vertical = 3
[connection signal="gui_input" from="BgControl" to="." method="_on_color_rect_gui_input"]
[connection signal="pressed" from="BgControl/VBox/Kill" to="." method="_on_kill_pressed"]

View File

@@ -0,0 +1,250 @@
@tool
extends Control
var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')
var _interface = null
var _output_control = null
@onready var _ctrls = {
tree = $VBox/Output/Scroll/Tree,
toolbar = {
toolbar = $VBox/Toolbar,
collapse = $VBox/Toolbar/Collapse,
collapse_all = $VBox/Toolbar/CollapseAll,
expand = $VBox/Toolbar/Expand,
expand_all = $VBox/Toolbar/ExpandAll,
hide_passing = $VBox/Toolbar/HidePassing,
show_script = $VBox/Toolbar/ShowScript,
scroll_output = $VBox/Toolbar/ScrollOutput
}
}
func _ready():
if(get_parent() is SubViewport):
return
var f = null
if ($FontSampler.get_label_settings() == null) :
f = get_theme_default_font()
else :
f = $FontSampler.get_label_settings().font
var s_size = f.get_string_size("000 of 000 passed")
_ctrls.tree.set_summary_min_width(s_size.x)
_set_toolbutton_icon(_ctrls.toolbar.collapse, 'CollapseTree', 'c')
_set_toolbutton_icon(_ctrls.toolbar.collapse_all, 'CollapseTree', 'c')
_set_toolbutton_icon(_ctrls.toolbar.expand, 'ExpandTree', 'e')
_set_toolbutton_icon(_ctrls.toolbar.expand_all, 'ExpandTree', 'e')
_set_toolbutton_icon(_ctrls.toolbar.show_script, 'Script', 'ss')
_set_toolbutton_icon(_ctrls.toolbar.scroll_output, 'Font', 'so')
_ctrls.tree.hide_passing = true
_ctrls.toolbar.hide_passing.button_pressed = false
_ctrls.tree.show_orphans = true
_ctrls.tree.selected.connect(_on_item_selected)
if(get_parent() == get_tree().root):
_test_running_setup()
call_deferred('_update_min_width')
func _test_running_setup():
_ctrls.tree.hide_passing = true
_ctrls.tree.show_orphans = true
_ctrls.toolbar.hide_passing.text = '[hp]'
_ctrls.tree.load_json_file(GutEditorGlobals.editor_run_json_results_path)
func _set_toolbutton_icon(btn, icon_name, text):
if(Engine.is_editor_hint()):
btn.icon = get_theme_icon(icon_name, 'EditorIcons')
else:
btn.text = str('[', text, ']')
func _update_min_width():
custom_minimum_size.x = _ctrls.toolbar.toolbar.size.x
func _open_script_in_editor(path, line_number):
if(_interface == null):
print('Too soon, wait a bit and try again.')
return
var r = load(path)
if(line_number != null and line_number != -1):
_interface.edit_script(r, line_number)
else:
_interface.edit_script(r)
if(_ctrls.toolbar.show_script.pressed):
_interface.set_main_screen_editor('Script')
# starts at beginning of text edit and searches for each search term, moving
# through the text as it goes; ensuring that, when done, it found the first
# occurance of the last srting that happend after the first occurance of
# each string before it. (Generic way of searching for a method name in an
# inner class that may have be a duplicate of a method name in a different
# inner class)
func _get_line_number_for_seq_search(search_strings, te):
if(te == null):
print("No Text editor to get line number for")
return 0;
var result = null
var line = Vector2i(0, 0)
var s_flags = 0
var i = 0
var string_found = true
while(i < search_strings.size() and string_found):
result = te.search(search_strings[i], s_flags, line.y, line.x)
if(result.x != -1):
line = result
else:
string_found = false
i += 1
return line.y
func _goto_code(path, line, method_name='', inner_class =''):
if(_interface == null):
print('going to ', [path, line, method_name, inner_class])
return
_open_script_in_editor(path, line)
if(line == -1):
var search_strings = []
if(inner_class != ''):
search_strings.append(inner_class)
if(method_name != ''):
search_strings.append(method_name)
await get_tree().process_frame
line = _get_line_number_for_seq_search(search_strings,
_interface.get_script_editor().get_current_editor().get_base_editor())
if(line != null and line != -1):
_interface.get_script_editor().goto_line(line)
func _goto_output(path, method_name, inner_class):
if(_output_control == null):
return
var search_strings = [path]
if(inner_class != ''):
search_strings.append(inner_class)
if(method_name != ''):
search_strings.append(method_name)
var line = _get_line_number_for_seq_search(search_strings, _output_control.get_rich_text_edit())
if(line != null and line != -1):
_output_control.scroll_to_line(line)
# --------------
# Events
# --------------
func _on_Collapse_pressed():
collapse_selected()
func _on_Expand_pressed():
expand_selected()
func _on_CollapseAll_pressed():
collapse_all()
func _on_ExpandAll_pressed():
expand_all()
func _on_Hide_Passing_pressed():
_ctrls.tree.hide_passing = !_ctrls.toolbar.hide_passing.button_pressed
_ctrls.tree.load_json_file(GutEditorGlobals.editor_run_json_results_path)
func _on_item_selected(script_path, inner_class, test_name, line):
if(_ctrls.toolbar.show_script.button_pressed):
_goto_code(script_path, line, test_name, inner_class)
if(_ctrls.toolbar.scroll_output.button_pressed):
_goto_output(script_path, test_name, inner_class)
# --------------
# Public
# --------------
func add_centered_text(t):
_ctrls.tree.add_centered_text(t)
func clear_centered_text():
_ctrls.tree.clear_centered_text()
func clear():
_ctrls.tree.clear()
clear_centered_text()
func set_interface(which):
_interface = which
func collapse_all():
_ctrls.tree.collapse_all()
func expand_all():
_ctrls.tree.expand_all()
func collapse_selected():
var item = _ctrls.tree.get_selected()
if(item != null):
_ctrls.tree.set_collapsed_on_all(item, true)
func expand_selected():
var item = _ctrls.tree.get_selected()
if(item != null):
_ctrls.tree.set_collapsed_on_all(item, false)
func set_show_orphans(should):
_ctrls.tree.show_orphans = should
func set_font(font_name, size):
pass
# var dyn_font = FontFile.new()
# var font_data = FontFile.new()
# font_data.font_path = 'res://addons/gut/fonts/' + font_name + '-Regular.ttf'
# font_data.antialiased = true
# dyn_font.font_data = font_data
#
# _font = dyn_font
# _font.size = size
# _font_size = size
func set_output_control(value):
_output_control = value
func load_json_results(j):
_ctrls.tree.load_json_results(j)

View File

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

View File

@@ -0,0 +1,109 @@
[gd_scene load_steps=4 format=3 uid="uid://4gyyn12um08h"]
[ext_resource type="Script" uid="uid://chnko3073tkcv" path="res://addons/gut/gui/RunResults.gd" id="1"]
[ext_resource type="Texture2D" uid="uid://bvo0uao7deu0q" path="res://addons/gut/icon.png" id="2_1k8e0"]
[ext_resource type="PackedScene" uid="uid://dls5r5f6157nq" path="res://addons/gut/gui/ResultsTree.tscn" id="2_o808v"]
[node name="RunResults" type="Control"]
custom_minimum_size = Vector2(345, 0)
layout_mode = 3
anchors_preset = 0
offset_right = 709.0
offset_bottom = 321.0
script = ExtResource("1")
[node name="VBox" type="VBoxContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
[node name="Toolbar" type="HBoxContainer" parent="VBox"]
layout_mode = 2
size_flags_horizontal = 0
[node name="Expand" type="Button" parent="VBox/Toolbar"]
visible = false
layout_mode = 2
icon = ExtResource("2_1k8e0")
[node name="Collapse" type="Button" parent="VBox/Toolbar"]
visible = false
layout_mode = 2
icon = ExtResource("2_1k8e0")
[node name="Sep" type="ColorRect" parent="VBox/Toolbar"]
visible = false
custom_minimum_size = Vector2(2, 0)
layout_mode = 2
[node name="LblAll" type="Label" parent="VBox/Toolbar"]
visible = false
layout_mode = 2
text = "All:"
[node name="ExpandAll" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
icon = ExtResource("2_1k8e0")
[node name="CollapseAll" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
icon = ExtResource("2_1k8e0")
[node name="Sep2" type="ColorRect" parent="VBox/Toolbar"]
custom_minimum_size = Vector2(2, 0)
layout_mode = 2
[node name="HidePassing" type="CheckBox" parent="VBox/Toolbar"]
layout_mode = 2
size_flags_horizontal = 4
text = "Passing"
[node name="Sep3" type="ColorRect" parent="VBox/Toolbar"]
custom_minimum_size = Vector2(2, 0)
layout_mode = 2
[node name="LblSync" type="Label" parent="VBox/Toolbar"]
layout_mode = 2
text = "Sync:"
[node name="ShowScript" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
toggle_mode = true
button_pressed = true
icon = ExtResource("2_1k8e0")
[node name="ScrollOutput" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
toggle_mode = true
button_pressed = true
icon = ExtResource("2_1k8e0")
[node name="Output" type="Panel" parent="VBox"]
self_modulate = Color(1, 1, 1, 0.541176)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Scroll" type="ScrollContainer" parent="VBox/Output"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Tree" parent="VBox/Output/Scroll" instance=ExtResource("2_o808v")]
layout_mode = 2
[node name="FontSampler" type="Label" parent="."]
visible = false
layout_mode = 0
offset_right = 40.0
offset_bottom = 14.0
text = "000 of 000 passed"
[connection signal="pressed" from="VBox/Toolbar/Expand" to="." method="_on_Expand_pressed"]
[connection signal="pressed" from="VBox/Toolbar/Collapse" to="." method="_on_Collapse_pressed"]
[connection signal="pressed" from="VBox/Toolbar/ExpandAll" to="." method="_on_ExpandAll_pressed"]
[connection signal="pressed" from="VBox/Toolbar/CollapseAll" to="." method="_on_CollapseAll_pressed"]
[connection signal="pressed" from="VBox/Toolbar/HidePassing" to="." method="_on_Hide_Passing_pressed"]

View File

@@ -0,0 +1,7 @@
[gd_scene format=3 uid="uid://cvvvtsah38l0e"]
[node name="Settings" type="VBoxContainer"]
offset_right = 388.0
offset_bottom = 586.0
size_flags_horizontal = 3
size_flags_vertical = 3

View File

@@ -0,0 +1,321 @@
@tool
extends ConfirmationDialog
const RUN_MODE_EDITOR = 'Editor'
const RUN_MODE_BLOCKING = 'Blocking'
const RUN_MODE_NON_BLOCKING = 'NonBlocking'
var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')
@onready var _bad_arg_dialog = $AcceptDialog
@onready var _main_container = $ScrollContainer/VBoxContainer
var _blurb_style_box = StyleBoxEmpty.new()
var _opt_maker_setup = false
var _arg_vbox : VBoxContainer = null
var _my_ok_button : Button = null
# Run mode button stuff
var _run_mode_theme = load('res://addons/gut/gui/EditorRadioButton.tres')
var _button_group = ButtonGroup.new()
var _btn_in_editor : Button = null
var _btn_blocking : Button = null
var _btn_non_blocking : Button = null
var _txt_additional_arguments = null
var _btn_godot_help = null
var _btn_gut_help = null
var opt_maker = null
var default_path = GutEditorGlobals.run_externally_options_path
# I like this. It holds values loaded/saved which makes for an easy
# reset mechanism. Hit OK; values get written to this object (not the file
# system). Hit Cancel; values are reloaded from this object. Call the
# save/load methods to interact with the file system.
#
# Downside: If the keys/sections in the config file change, this ends up
# preserving old data. So you gotta find a way to clean it out
# somehow.
# Downside solved: Clear the config file at the start of the save method.
var _config_file = ConfigFile.new()
var _run_mode = RUN_MODE_EDITOR
var run_mode = _run_mode:
set(val):
_run_mode = val
if(is_inside_tree()):
_btn_in_editor.button_pressed = _run_mode == RUN_MODE_EDITOR
if(_btn_in_editor.button_pressed):
_btn_in_editor.pressed.emit()
_btn_blocking.button_pressed = _run_mode == RUN_MODE_BLOCKING
if(_btn_blocking.button_pressed):
_btn_blocking.pressed.emit()
_btn_non_blocking.button_pressed = _run_mode == RUN_MODE_NON_BLOCKING
if(_btn_non_blocking.button_pressed):
_btn_non_blocking.pressed.emit()
get():
return _run_mode
var additional_arguments = '' :
get():
if(_opt_maker_setup):
return opt_maker.controls.additional_arguments.value
else:
return additional_arguments
func _debug_ready():
popup_centered()
default_path = GutEditorGlobals.temp_directory.path_join('test_external_run_options.cfg')
exclusive = false
var save_btn = Button.new()
save_btn.text = 'save'
save_btn.pressed.connect(func():
save_to_file()
print(_config_file.encode_to_text()))
save_btn.position = Vector2(100, 20)
save_btn.size = Vector2(100, 100)
get_tree().root.add_child(save_btn)
var load_btn = Button.new()
load_btn.text = 'load'
load_btn.pressed.connect(func():
load_from_file()
print(_config_file.encode_to_text()))
load_btn.position = Vector2(100, 130)
load_btn.size = Vector2(100, 100)
get_tree().root.add_child(load_btn)
var show_btn = Button.new()
show_btn.text = 'Show'
show_btn.pressed.connect(popup_centered)
show_btn.position = Vector2(100, 250)
show_btn.size = Vector2(100, 100)
get_tree().root.add_child(show_btn)
func _ready():
opt_maker = GutUtils.OptionMaker.new(_main_container)
_add_controls()
if(get_parent() == get_tree().root):
_debug_ready.call_deferred()
_my_ok_button = Button.new()
_my_ok_button.text = 'OK'
_my_ok_button.pressed.connect(_validate_and_confirm)
get_ok_button().add_sibling(_my_ok_button)
get_ok_button().modulate.a = 0.0
get_ok_button().text = ''
get_ok_button().disabled = true
canceled.connect(reset)
_button_group.pressed.connect(_on_mode_button_pressed)
run_mode = run_mode
func _validate_and_confirm():
if(validate_arguments()):
_save_to_config_file(_config_file)
confirmed.emit()
hide()
else:
var dlg_text = str("Invalid arguments. The following cannot be used:\n",
' '.join(_invalid_args))
if(run_mode == RUN_MODE_BLOCKING):
dlg_text += str("\nThese cannot be used with blocking mode:\n",
' '.join(_invalid_blocking_args))
_bad_arg_dialog.dialog_text = dlg_text
_bad_arg_dialog.popup_centered()
func _on_mode_button_pressed(which):
if(which == _btn_in_editor):
_arg_vbox.modulate.a = .3
else:
_arg_vbox.modulate.a = 1.0
_txt_additional_arguments.value_ctrl.editable = which != _btn_in_editor
if(which == _btn_in_editor):
_run_mode = RUN_MODE_EDITOR
elif(which == _btn_blocking):
_run_mode = RUN_MODE_BLOCKING
elif(which == _btn_non_blocking):
_run_mode = RUN_MODE_NON_BLOCKING
func _add_run_mode_button(text, desc_label, description):
var btn = Button.new()
btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
btn.toggle_mode = true
btn.text = text
btn.button_group = _button_group
btn.theme = _run_mode_theme
btn.pressed.connect(func(): desc_label.text = str('[b]', text, "[/b]\n", description))
return btn
func _add_blurb(text):
var ctrl = opt_maker.add_blurb(text)
ctrl.set("theme_override_styles/normal", _blurb_style_box)
return ctrl
func _add_title(text):
var ctrl = opt_maker.add_title(text)
ctrl.get_child(0).horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
return ctrl
func _add_controls():
_add_title("Run Modes")
_add_blurb(
"Choose how GUT will launch tests. Normally you just run them through the editor, but now " +
"you can run them externally. This is an experimental feature. It has been tested on Mac " +
"and Windows. Your results may vary. Feedback welcome at [url]https://github.com/bitwes/Gut/issues[/url].\n ")
var button_desc_box = HBoxContainer.new()
var button_box = VBoxContainer.new()
var button_desc = RichTextLabel.new()
button_desc.fit_content = true
button_desc.bbcode_enabled = true
button_desc.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button_desc.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_main_container.add_child(button_desc_box)
button_desc_box.add_child(button_box)
button_desc_box.add_child(button_desc)
_btn_in_editor = _add_run_mode_button("In Editor (default)", button_desc,
"This is the default. Runs through the editor. When an error occurs " +
"the debugger is invoked. [b]print[/b] output " +
"appears in the Output panel and errors show up in the Debugger panel.")
button_box.add_child(_btn_in_editor)
_btn_blocking = _add_run_mode_button("Externally - Blocking", button_desc,
"Debugger is not enabled, and cannot be enabled. All output (print, errors, warnings, etc) " +
"appears in the GUT panel, and [b]not[/b] the Output or Debugger panels. \n" +
"The Editor cannot be used while tests are running. If you are trying to test for errors, this " +
"mode provides the best output.")
button_box.add_child(_btn_blocking)
_btn_non_blocking = _add_run_mode_button("Externally - NonBlocking", button_desc,
"Debugger is not enabled, and cannot be enabled. All output (print, errors, warnings, etc) " +
"appears in the GUT panel, and [b]not[/b] the Output or Debugger panels. \n" +
"Test output is streamed to the GUT panel. The editor is not blocked, but can be less " +
"responsive when there is a lot of output. This is the only mode that supports the --headless argument." )
button_box.add_child(_btn_non_blocking)
_add_title("Command Line Arguments")
_arg_vbox = VBoxContainer.new()
_main_container.add_child(_arg_vbox)
opt_maker.base_container = _arg_vbox
_txt_additional_arguments = opt_maker.add_value("additional_arguments", additional_arguments, '', '')
_txt_additional_arguments.value_ctrl.placeholder_text = "Put your arguments here. Ex: --verbose -glog 0"
_txt_additional_arguments.value_ctrl.select_all_on_focus = false
_add_blurb(
"Supply any command line options for GUT and/or Godot when running externally. You cannot use " +
"spaces in values. See the Godot and GUT documentation for valid arguments. GUT arguments " +
"specified here take precedence over your config.")
_add_blurb("[b]Be Careful[/b] There are plenty of argument combinations that may make this " +
"act wrong/odd/bad/horrible. Some arguments you might [i]want[/i] " +
"to use but [b]shouldn't[/b] are checked for, but not that many. Choose your arguments carefully (generally good advice).")
opt_maker.base_container = _main_container
_add_title("Display CLI Help")
_add_blurb("You can use these buttons to get a list of valid GUT and Godot options. They print the CLI help text for each to the [b]Output Panel[/b].")
_btn_godot_help = Button.new()
_btn_godot_help.text = "Print Godot CLI Help"
_main_container.add_child(_btn_godot_help)
_btn_godot_help.pressed.connect(func():
await _show_help("get_godot_help"))
_btn_gut_help = Button.new()
_btn_gut_help.text = "Print GUT CLI Help"
_main_container.add_child(_btn_gut_help)
_btn_gut_help.pressed.connect(func():
await _show_help("get_gut_help"))
_opt_maker_setup = true
func _show_help(help_method_name):
_btn_godot_help.disabled = true
_btn_gut_help.disabled = true
var re = GutUtils.RunExternallyScene.instantiate()
add_child(re)
re.visible = false
var text = await re.call(help_method_name)
print(text)
re.queue_free()
_btn_godot_help.disabled = false
_btn_gut_help.disabled = false
if(GutEditorGlobals.gut_plugin != null):
GutEditorGlobals.gut_plugin.show_output_panel()
func _save_to_config_file(f : ConfigFile):
f.clear()
f.set_value('main', 'run_mode', run_mode)
f.set_value('main', 'additional_arguments', opt_maker.controls.additional_arguments.value)
func save_to_file(path = default_path):
_save_to_config_file(_config_file)
_config_file.save(path)
func _load_from_config_file(f):
run_mode = f.get_value('main', 'run_mode', RUN_MODE_EDITOR)
opt_maker.controls.additional_arguments.value = \
f.get_value('main', 'additional_arguments', '')
func load_from_file(path = default_path):
_config_file.load(path)
_load_from_config_file(_config_file)
func reset():
_load_from_config_file(_config_file)
func get_additional_arguments_array():
return additional_arguments.split(" ", false)
func should_run_externally():
return run_mode != RUN_MODE_EDITOR
var _invalid_args = [
'-d', '--debug',
'-s', '--script',
'-e', '--editor'
]
var _invalid_blocking_args = [
'--headless'
]
func validate_arguments():
var arg_array = get_additional_arguments_array()
var i = 0
var invalid_found = false
while i < _invalid_args.size() and !invalid_found:
if(arg_array.has(_invalid_args[i])):
invalid_found = true
i += 1
if(run_mode == RUN_MODE_BLOCKING):
i = 0
while i < _invalid_blocking_args.size() and !invalid_found:
if(arg_array.has(_invalid_blocking_args[i])):
invalid_found = true
i += 1
return !invalid_found
func get_godot_help():
return ''

View File

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

View File

@@ -0,0 +1,34 @@
[gd_scene load_steps=2 format=3 uid="uid://ckv5eh8xyrwbk"]
[ext_resource type="Script" uid="uid://c64u22kybimgi" path="res://addons/gut/gui/ShellOutOptions.gd" id="1_ht2pf"]
[node name="ShellOutOptions" type="ConfirmationDialog"]
oversampling_override = 1.0
title = "GUT Run Mode (Experimental)"
position = Vector2i(0, 36)
size = Vector2i(516, 557)
visible = true
script = ExtResource("1_ht2pf")
[node name="ScrollContainer" type="ScrollContainer" parent="."]
custom_minimum_size = Vector2(500, 500)
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 8.0
offset_top = 8.0
offset_right = -8.0
offset_bottom = -49.0
grow_horizontal = 2
grow_vertical = 2
[node name="VBoxContainer" type="VBoxContainer" parent="ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="AcceptDialog" type="AcceptDialog" parent="."]
oversampling_override = 1.0
size = Vector2i(399, 106)
dialog_text = "Invalid arguments. The following cannot be used:
-d --debug -s --script"

View File

@@ -0,0 +1,154 @@
@tool
extends Control
@onready var _ctrls = {
shortcut_label = $Layout/lblShortcut,
set_button = $Layout/SetButton,
save_button = $Layout/SaveButton,
cancel_button = $Layout/CancelButton,
clear_button = $Layout/ClearButton
}
signal changed
signal start_edit
signal end_edit
const NO_SHORTCUT = '<None>'
var _source_event = InputEventKey.new()
var _pre_edit_event = null
var _key_disp = NO_SHORTCUT
var _editing = false
var _modifier_keys = [KEY_ALT, KEY_CTRL, KEY_META, KEY_SHIFT]
# Called when the node enters the scene tree for the first time.
func _ready():
set_process_unhandled_key_input(false)
func _display_shortcut():
if(_key_disp == ''):
_key_disp = NO_SHORTCUT
_ctrls.shortcut_label.text = _key_disp
func _is_shift_only_modifier():
return _source_event.shift_pressed and \
!(_source_event.alt_pressed or \
_source_event.ctrl_pressed or \
_source_event.meta_pressed) \
and !_is_modifier(_source_event.keycode)
func _has_modifier(event):
return event.alt_pressed or event.ctrl_pressed or \
event.meta_pressed or event.shift_pressed
func _is_modifier(keycode):
return _modifier_keys.has(keycode)
func _edit_mode(should):
_editing = should
set_process_unhandled_key_input(should)
_ctrls.set_button.visible = !should
_ctrls.save_button.visible = should
_ctrls.save_button.disabled = should
_ctrls.cancel_button.visible = should
_ctrls.clear_button.visible = !should
if(should and to_s() == ''):
_ctrls.shortcut_label.text = 'press buttons'
else:
_ctrls.shortcut_label.text = to_s()
if(should):
emit_signal("start_edit")
else:
emit_signal("end_edit")
# ---------------
# Events
# ---------------
func _unhandled_key_input(event):
if(event is InputEventKey):
if(event.pressed):
if(_has_modifier(event) and !_is_modifier(event.get_keycode_with_modifiers())):
_source_event = event
_key_disp = OS.get_keycode_string(event.get_keycode_with_modifiers())
else:
_source_event = InputEventKey.new()
_key_disp = NO_SHORTCUT
_display_shortcut()
_ctrls.save_button.disabled = !is_valid()
func _on_SetButton_pressed():
_pre_edit_event = _source_event.duplicate(true)
_edit_mode(true)
func _on_SaveButton_pressed():
_edit_mode(false)
_pre_edit_event = null
emit_signal('changed')
func _on_CancelButton_pressed():
cancel()
func _on_ClearButton_pressed():
clear_shortcut()
# ---------------
# Public
# ---------------
func to_s():
return OS.get_keycode_string(_source_event.get_keycode_with_modifiers())
func is_valid():
return _has_modifier(_source_event) and !_is_shift_only_modifier()
func get_shortcut():
var to_return = Shortcut.new()
to_return.events.append(_source_event)
return to_return
func get_input_event():
return _source_event
func set_shortcut(sc):
if(sc == null or sc.events == null || sc.events.size() <= 0):
clear_shortcut()
else:
_source_event = sc.events[0]
_key_disp = to_s()
_display_shortcut()
func clear_shortcut():
_source_event = InputEventKey.new()
_key_disp = NO_SHORTCUT
_display_shortcut()
func disable_set(should):
_ctrls.set_button.disabled = should
func disable_clear(should):
_ctrls.clear_button.disabled = should
func cancel():
if(_editing):
_edit_mode(false)
_source_event = _pre_edit_event
_key_disp = to_s()
_display_shortcut()

View File

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

View File

@@ -0,0 +1,55 @@
[gd_scene load_steps=2 format=3 uid="uid://sfb1fw8j6ufu"]
[ext_resource type="Script" uid="uid://k6hvvpekp0xw" path="res://addons/gut/gui/ShortcutButton.gd" id="1"]
[node name="ShortcutButton" type="Control"]
custom_minimum_size = Vector2(210, 30)
layout_mode = 3
anchor_right = 0.123
anchor_bottom = 0.04
offset_right = 68.304
offset_bottom = 6.08
script = ExtResource("1")
[node name="Layout" type="HBoxContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
[node name="lblShortcut" type="Label" parent="Layout"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 7
text = "<None>"
horizontal_alignment = 2
[node name="CenterContainer" type="CenterContainer" parent="Layout"]
custom_minimum_size = Vector2(10, 0)
layout_mode = 2
[node name="SetButton" type="Button" parent="Layout"]
custom_minimum_size = Vector2(60, 0)
layout_mode = 2
text = "Set"
[node name="SaveButton" type="Button" parent="Layout"]
visible = false
custom_minimum_size = Vector2(60, 0)
layout_mode = 2
text = "Save"
[node name="CancelButton" type="Button" parent="Layout"]
visible = false
custom_minimum_size = Vector2(60, 0)
layout_mode = 2
text = "Cancel"
[node name="ClearButton" type="Button" parent="Layout"]
custom_minimum_size = Vector2(60, 0)
layout_mode = 2
text = "Clear"
[connection signal="pressed" from="Layout/SetButton" to="." method="_on_SetButton_pressed"]
[connection signal="pressed" from="Layout/SaveButton" to="." method="_on_SaveButton_pressed"]
[connection signal="pressed" from="Layout/CancelButton" to="." method="_on_CancelButton_pressed"]
[connection signal="pressed" from="Layout/ClearButton" to="." method="_on_ClearButton_pressed"]

View File

@@ -0,0 +1,120 @@
@tool
extends ConfirmationDialog
var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')
var default_path = GutEditorGlobals.editor_shortcuts_path
@onready var scbtn_run_all = $Scroll/Layout/CRunAll/ShortcutButton
@onready var scbtn_run_current_script = $Scroll/Layout/CRunCurrentScript/ShortcutButton
@onready var scbtn_run_current_inner = $Scroll/Layout/CRunCurrentInner/ShortcutButton
@onready var scbtn_run_current_test = $Scroll/Layout/CRunCurrentTest/ShortcutButton
@onready var scbtn_run_at_cursor = $Scroll/Layout/CRunAtCursor/ShortcutButton
@onready var scbtn_rerun = $Scroll/Layout/CRerun/ShortcutButton
@onready var scbtn_panel = $Scroll/Layout/CPanelButton/ShortcutButton
@onready var scbtn_windowed = $Scroll/Layout/CToggleWindowed/ShortcutButton
@onready var all_buttons = [
scbtn_run_all, scbtn_run_current_script, scbtn_run_current_inner,
scbtn_run_current_test, scbtn_run_at_cursor, scbtn_rerun,
scbtn_panel, scbtn_windowed
]
func _debug_ready():
popup_centered()
var btn = Button.new()
btn.text = "show"
get_tree().root.add_child(btn)
btn.pressed.connect(popup)
btn.position = Vector2(100, 100)
btn.size = Vector2(100, 100)
size_changed.connect(func(): title = str(size))
func _ready():
for scbtn in all_buttons:
scbtn.connect('start_edit', _on_edit_start.bind(scbtn))
scbtn.connect('end_edit', _on_edit_end)
canceled.connect(_on_cancel)
# Sizing this window on different monitors, especially compared to what it
# looks like if you just run this project is annoying. This is what I came
# up with after getting annoyed. You probably won't be looking at this
# very often so it's fine...until it isn't.
size = Vector2(DisplayServer.screen_get_size()) * Vector2(.5, .8)
if(get_parent() == get_tree().root):
_debug_ready.call_deferred()
func _cancel_all():
for scbtn in all_buttons:
scbtn.cancel()
# ------------
# Events
# ------------
func _on_cancel():
_cancel_all()
load_shortcuts()
func _on_edit_start(which):
for scbtn in all_buttons:
if(scbtn != which):
scbtn.disable_set(true)
scbtn.disable_clear(true)
func _on_edit_end():
for scbtn in all_buttons:
scbtn.disable_set(false)
scbtn.disable_clear(false)
# ------------
# Public
# ------------
func save_shortcuts():
save_shortcuts_to_file(default_path)
func save_shortcuts_to_file(path):
var f = ConfigFile.new()
f.set_value('main', 'panel_button', scbtn_panel.get_shortcut())
f.set_value('main', 'rerun', scbtn_rerun.get_shortcut())
f.set_value('main', 'run_all', scbtn_run_all.get_shortcut())
f.set_value('main', 'run_at_cursor', scbtn_run_at_cursor.get_shortcut())
f.set_value('main', 'run_current_inner', scbtn_run_current_inner.get_shortcut())
f.set_value('main', 'run_current_script', scbtn_run_current_script.get_shortcut())
f.set_value('main', 'run_current_test', scbtn_run_current_test.get_shortcut())
f.set_value('main', 'toggle_windowed', scbtn_windowed.get_shortcut())
f.save(path)
func load_shortcuts():
load_shortcuts_from_file(default_path)
func load_shortcuts_from_file(path):
var f = ConfigFile.new()
# as long as this shortcut is never modified, this is fine, otherwise
# each thing should get its own default instead.
var empty = Shortcut.new()
f.load(path)
scbtn_panel.set_shortcut(f.get_value('main', 'panel_button', empty))
scbtn_rerun.set_shortcut(f.get_value('main', 'rerun', empty))
scbtn_run_all.set_shortcut(f.get_value('main', 'run_all', empty))
scbtn_run_at_cursor.set_shortcut(f.get_value('main', 'run_at_cursor', empty))
scbtn_run_current_inner.set_shortcut(f.get_value('main', 'run_current_inner', empty))
scbtn_run_current_script.set_shortcut(f.get_value('main', 'run_current_script', empty))
scbtn_run_current_test.set_shortcut(f.get_value('main', 'run_current_test', empty))
scbtn_windowed.set_shortcut(f.get_value('main', 'toggle_windowed', empty))

View File

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

View File

@@ -0,0 +1,207 @@
[gd_scene load_steps=3 format=3 uid="uid://dj5ve0bq7xa5j"]
[ext_resource type="Script" uid="uid://dc5jgemxslgvl" path="res://addons/gut/gui/ShortcutDialog.gd" id="1_qq8qn"]
[ext_resource type="PackedScene" uid="uid://sfb1fw8j6ufu" path="res://addons/gut/gui/ShortcutButton.tscn" id="2_i3wie"]
[node name="ShortcutDialog" type="ConfirmationDialog"]
oversampling_override = 1.0
title = "GUT Shortcuts"
position = Vector2i(0, 36)
size = Vector2i(1920, 1728)
visible = true
script = ExtResource("1_qq8qn")
[node name="Scroll" type="ScrollContainer" parent="."]
offset_left = 8.0
offset_top = 8.0
offset_right = 1912.0
offset_bottom = 1679.0
[node name="Layout" type="VBoxContainer" parent="Scroll"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="ShortcutDescription" type="RichTextLabel" parent="Scroll/Layout"]
custom_minimum_size = Vector2(0, 20)
layout_mode = 2
bbcode_enabled = true
text = "Shortcuts for:
- Buttons in Panel
- Project->Tools->GUT menu items
Shortcuts that only apply to menus are labeled."
fit_content = true
scroll_active = false
[node name="TopPad" type="CenterContainer" parent="Scroll/Layout"]
custom_minimum_size = Vector2(0, 5)
layout_mode = 2
[node name="CPanelButton" type="HBoxContainer" parent="Scroll/Layout"]
layout_mode = 2
[node name="Label" type="Label" parent="Scroll/Layout/CPanelButton"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Show/Hide GUT"
[node name="ShortcutButton" parent="Scroll/Layout/CPanelButton" instance=ExtResource("2_i3wie")]
layout_mode = 2
size_flags_horizontal = 3
[node name="ShortcutDescription2" type="RichTextLabel" parent="Scroll/Layout"]
custom_minimum_size = Vector2(0, 20)
layout_mode = 2
bbcode_enabled = true
text = "[i]Show/hide the gut panel or move focus to/away from the GUT window.
[/i]"
fit_content = true
scroll_active = false
[node name="CRunAll" type="HBoxContainer" parent="Scroll/Layout"]
layout_mode = 2
[node name="Label" type="Label" parent="Scroll/Layout/CRunAll"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Run All"
[node name="ShortcutButton" parent="Scroll/Layout/CRunAll" instance=ExtResource("2_i3wie")]
layout_mode = 2
size_flags_horizontal = 3
[node name="ShortcutDescription3" type="RichTextLabel" parent="Scroll/Layout"]
custom_minimum_size = Vector2(0, 20)
layout_mode = 2
bbcode_enabled = true
text = "[i]Run the entire test suite.[/i]"
fit_content = true
scroll_active = false
[node name="CRunCurrentScript" type="HBoxContainer" parent="Scroll/Layout"]
layout_mode = 2
[node name="Label" type="Label" parent="Scroll/Layout/CRunCurrentScript"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Run Current Script"
[node name="ShortcutButton" parent="Scroll/Layout/CRunCurrentScript" instance=ExtResource("2_i3wie")]
layout_mode = 2
size_flags_horizontal = 3
[node name="ShortcutDescription4" type="RichTextLabel" parent="Scroll/Layout"]
custom_minimum_size = Vector2(0, 20)
layout_mode = 2
bbcode_enabled = true
text = "[i]Run all tests in the currently selected script.[/i]"
fit_content = true
scroll_active = false
[node name="CRunCurrentInner" type="HBoxContainer" parent="Scroll/Layout"]
layout_mode = 2
[node name="Label" type="Label" parent="Scroll/Layout/CRunCurrentInner"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Run Current Inner Class"
[node name="ShortcutButton" parent="Scroll/Layout/CRunCurrentInner" instance=ExtResource("2_i3wie")]
layout_mode = 2
size_flags_horizontal = 3
[node name="ShortcutDescription5" type="RichTextLabel" parent="Scroll/Layout"]
custom_minimum_size = Vector2(0, 20)
layout_mode = 2
bbcode_enabled = true
text = "[i]Run only the currently selected inner test class if one is selected.[/i]"
fit_content = true
scroll_active = false
[node name="CRunCurrentTest" type="HBoxContainer" parent="Scroll/Layout"]
layout_mode = 2
[node name="Label" type="Label" parent="Scroll/Layout/CRunCurrentTest"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Run Current Test"
[node name="ShortcutButton" parent="Scroll/Layout/CRunCurrentTest" instance=ExtResource("2_i3wie")]
layout_mode = 2
size_flags_horizontal = 3
[node name="ShortcutDescription6" type="RichTextLabel" parent="Scroll/Layout"]
custom_minimum_size = Vector2(0, 20)
layout_mode = 2
bbcode_enabled = true
text = "[i]Run only the currently selected test, if one is selected[/i]"
fit_content = true
scroll_active = false
[node name="CRunAtCursor" type="HBoxContainer" parent="Scroll/Layout"]
layout_mode = 2
[node name="Label" type="Label" parent="Scroll/Layout/CRunAtCursor"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Run At Cursor (menu only)"
[node name="ShortcutButton" parent="Scroll/Layout/CRunAtCursor" instance=ExtResource("2_i3wie")]
layout_mode = 2
size_flags_horizontal = 3
[node name="ShortcutDescription7" type="RichTextLabel" parent="Scroll/Layout"]
custom_minimum_size = Vector2(0, 20)
layout_mode = 2
bbcode_enabled = true
text = "[i]Run the most specific test/inner class/script based on where the cursor is.[/i]"
fit_content = true
scroll_active = false
[node name="CRerun" type="HBoxContainer" parent="Scroll/Layout"]
layout_mode = 2
[node name="Label" type="Label" parent="Scroll/Layout/CRerun"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Rerun (menu only)"
[node name="ShortcutButton" parent="Scroll/Layout/CRerun" instance=ExtResource("2_i3wie")]
layout_mode = 2
size_flags_horizontal = 3
[node name="ShortcutDescription8" type="RichTextLabel" parent="Scroll/Layout"]
custom_minimum_size = Vector2(0, 20)
layout_mode = 2
bbcode_enabled = true
text = "[i]Rerun the test(s) that were last run."
fit_content = true
scroll_active = false
[node name="CToggleWindowed" type="HBoxContainer" parent="Scroll/Layout"]
layout_mode = 2
[node name="Label" type="Label" parent="Scroll/Layout/CToggleWindowed"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Toggle Windowed"
[node name="ShortcutButton" parent="Scroll/Layout/CToggleWindowed" instance=ExtResource("2_i3wie")]
layout_mode = 2
size_flags_horizontal = 3
[node name="ShortcutDescription9" type="RichTextLabel" parent="Scroll/Layout"]
custom_minimum_size = Vector2(0, 20)
layout_mode = 2
bbcode_enabled = true
text = "[i]Toggle GUT in the bottom panel or a separate window.[/i]"
fit_content = true
scroll_active = false

125
addons/gut/gui/about.gd Normal file
View File

@@ -0,0 +1,125 @@
@tool
extends AcceptDialog
var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')
var _bbcode = \
"""
[center]GUT {gut_version}[/center]
[center][b]GUT Links[/b]
{gut_link_table}[/center]
[center][b]VSCode Extension Links[/b]
{vscode_link_table}[/center]
[center]You can support GUT development at
{donate_link}
Thanks for using GUT!
[/center]
"""
var _gut_links = [
[&"Documentation", &"https://gut.readthedocs.io"],
[&"What's New", &"https://github.com/bitwes/Gut/releases/tag/v{gut_version}"],
[&"Repo", &"https://github.com/bitwes/gut"],
[&"Report Bugs", &"https://github.com/bitwes/gut/issues"]
]
var _vscode_links = [
["Repo", "https://github.com/bitwes/gut-extension"],
["Market Place", "https://marketplace.visualstudio.com/items?itemName=bitwes.gut-extension"]
]
var _donate_link = "https://buymeacoffee.com/bitwes"
@onready var _logo = $Logo
func _ready():
if(get_parent() is SubViewport):
return
_vert_center_logo()
$Logo.disabled = true
$HBox/Scroll/RichTextLabel.text = _make_text()
func _color_link(link_text):
return str("[color=ROYAL_BLUE]", link_text, "[/color]")
func _link_table(entries):
var text = ''
for entry in entries:
text += str("[cell][right]", entry[0], "[/right][/cell]")
var link = str("[url]", entry[1], "[/url]")
if(entry[1].length() > 60):
link = str("[url=", entry[1], "]", entry[1].substr(0, 50), "...[/url]")
text += str("[cell][left]", _color_link(link), "[/left][/cell]\n")
return str('[table=2]', text, '[/table]')
func _make_text():
var gut_link_table = _link_table(_gut_links)
var vscode_link_table = _link_table(_vscode_links)
var text = _bbcode.format({
"gut_link_table":gut_link_table,
"vscode_link_table":vscode_link_table,
"donate_link":_color_link(str('[url]', _donate_link, '[/url]')),
"gut_version":GutUtils.version_numbers.gut_version,
})
return text
func _vert_center_logo():
_logo.position.y = size.y / 2.0
# -----------
# Events
# -----------
func _on_rich_text_label_meta_clicked(meta: Variant) -> void:
OS.shell_open(str(meta))
func _on_mouse_entered() -> void:
pass#_logo.active = true
func _on_mouse_exited() -> void:
pass#_logo.active = false
var _odd_ball_eyes_l = 1.1
var _odd_ball_eyes_r = .7
func _on_rich_text_label_meta_hover_started(meta: Variant) -> void:
if(meta == _gut_links[0][1]):
_logo.set_eye_color(Color.RED)
elif(meta.find("releases/tag/") > 0):
_logo.set_eye_color(Color.GREEN)
elif(meta == _gut_links[2][1]):
_logo.set_eye_color(Color.PURPLE)
elif(meta == _gut_links[3][1]):
_logo.set_eye_scale(1.2)
elif(meta == _vscode_links[0][1]):
_logo.set_eye_scale(.5, .5)
elif(meta == _vscode_links[1][1]):
_logo.set_eye_scale(_odd_ball_eyes_l, _odd_ball_eyes_r)
var temp = _odd_ball_eyes_l
_odd_ball_eyes_l = _odd_ball_eyes_r
_odd_ball_eyes_r = temp
elif(meta == _donate_link):
_logo.active = false
func _on_rich_text_label_meta_hover_ended(meta: Variant) -> void:
if(meta == _donate_link):
_logo.active = true
func _on_logo_pressed() -> void:
_logo.disabled = !_logo.disabled

View File

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

56
addons/gut/gui/about.tscn Normal file
View File

@@ -0,0 +1,56 @@
[gd_scene load_steps=5 format=3 uid="uid://dqbkylpsatcqm"]
[ext_resource type="Script" uid="uid://g7qu8ihdt3pd" path="res://addons/gut/gui/about.gd" id="1_bg86c"]
[ext_resource type="PackedScene" uid="uid://bjkn8mhx2fmt1" path="res://addons/gut/gui/GutLogo.tscn" id="3_kpic4"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_q8rky"]
bg_color = Color(0, 0, 0, 0.49803922)
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_kpic4"]
[node name="About" type="AcceptDialog"]
oversampling_override = 1.0
title = "About GUT"
position = Vector2i(0, 36)
size = Vector2i(1500, 800)
visible = true
min_size = Vector2i(800, 800)
script = ExtResource("1_bg86c")
[node name="HBox" type="HBoxContainer" parent="."]
offset_left = 8.0
offset_top = 8.0
offset_right = 1492.0
offset_bottom = 751.0
alignment = 1
[node name="MakeRoomForLogo" type="CenterContainer" parent="HBox"]
custom_minimum_size = Vector2(200, 0)
layout_mode = 2
[node name="Scroll" type="ScrollContainer" parent="HBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="RichTextLabel" type="RichTextLabel" parent="HBox/Scroll"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_styles/normal = SubResource("StyleBoxFlat_q8rky")
theme_override_styles/focus = SubResource("StyleBoxEmpty_kpic4")
bbcode_enabled = true
fit_content = true
[node name="Logo" parent="." instance=ExtResource("3_kpic4")]
modulate = Color(0.74509805, 0.74509805, 0.74509805, 1)
position = Vector2(151, 265)
scale = Vector2(0.8, 0.8)
active = true
disabled = true
[connection signal="mouse_entered" from="." to="." method="_on_mouse_entered"]
[connection signal="mouse_exited" from="." to="." method="_on_mouse_exited"]
[connection signal="meta_clicked" from="HBox/Scroll/RichTextLabel" to="." method="_on_rich_text_label_meta_clicked"]
[connection signal="meta_hover_ended" from="HBox/Scroll/RichTextLabel" to="." method="_on_rich_text_label_meta_hover_ended"]
[connection signal="meta_hover_started" from="HBox/Scroll/RichTextLabel" to="." method="_on_rich_text_label_meta_hover_started"]
[connection signal="pressed" from="Logo" to="." method="_on_logo_pressed"]

BIN
addons/gut/gui/arrow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 B

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://6wra5rxmfsrl"
path="res://.godot/imported/arrow.png-2b5b2d838b5b3467cf300ac2da1630d9.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/gut/gui/arrow.png"
dest_files=["res://.godot/imported/arrow.png-2b5b2d838b5b3467cf300ac2da1630d9.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

View File

@@ -0,0 +1,68 @@
@tool
static var GutUserPreferences = load("res://addons/gut/gui/gut_user_preferences.gd")
static var temp_directory = 'user://gut_temp_directory'
static var editor_run_gut_config_path = 'gut_editor_config.json':
# This avoids having to use path_join wherever we want to reference this
# path. The value is not supposed to change. Could it be a constant
# instead? Probably, but I didn't like repeating the directory part.
# Do I like that this is a bit witty. Absolutely.
get: return temp_directory.path_join(editor_run_gut_config_path)
# Should this print a message or something instead? Probably, but then I'd
# be repeating even more code than if this was just a constant. So I didn't,
# even though I wanted to make the message a easter eggish fun message.
# I didn't, so this dumb comment will have to serve as the easter eggish fun.
set(v):
print("Be sure to document your code. Never trust comments.")
static var editor_run_bbcode_results_path = 'gut_editor.bbcode':
get: return temp_directory.path_join(editor_run_bbcode_results_path)
set(v): pass
static var editor_run_json_results_path = 'gut_editor.json':
get: return temp_directory.path_join(editor_run_json_results_path)
set(v): pass
static var editor_shortcuts_path = 'gut_editor_shortcuts.cfg' :
get: return temp_directory.path_join(editor_shortcuts_path)
set(v): pass
static var run_externally_options_path = 'gut_editor_run_externally.cfg' :
get: return temp_directory.path_join(run_externally_options_path)
set(v): pass
static var _user_prefs = null
static var user_prefs = _user_prefs :
# workaround not being able to reference EditorInterface when not in
# the editor. This shouldn't be referenced by anything not in the
# editor.
get:
if(_user_prefs == null and Engine.is_editor_hint()):
# This is sometimes used when not in the editor. Avoid parser error
# for EditorInterface.
_user_prefs = GutUserPreferences.new(GutUtils.get_editor_interface().get_editor_settings())
return _user_prefs
static var gut_plugin = null
static func create_temp_directory():
DirAccess.make_dir_recursive_absolute(temp_directory)
static func is_being_edited_in_editor(which):
if(!Engine.is_editor_hint()):
return false
var trav = which
var is_scene_root = false
var editor_root = which.get_tree().edited_scene_root
while(trav != null and !is_scene_root):
is_scene_root = editor_root == trav
if(!is_scene_root):
trav = trav.get_parent()
return is_scene_root

View File

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

View File

@@ -0,0 +1,282 @@
var PanelControls = load("res://addons/gut/gui/panel_controls.gd")
var GutConfig = load('res://addons/gut/gut_config.gd')
const DIRS_TO_LIST = 6
# specific titles that we need to do stuff with
var _titles = {
dirs = null
}
var _cfg_ctrls = {}
var opt_maker = null
func _init(cont):
opt_maker = GutUtils.OptionMaker.new(cont)
_cfg_ctrls = opt_maker.controls
# _base_container = cont
func _add_save_load():
var ctrl = PanelControls.SaveLoadControl.new('Config', '', '')
ctrl.save_path_chosen.connect(_on_save_path_chosen)
ctrl.load_path_chosen.connect(_on_load_path_chosen)
#_cfg_ctrls['save_load'] = ctrl
opt_maker.add_ctrl('save_load', ctrl)
return ctrl
# ------------------
# Events
# ------------------
func _on_save_path_chosen(path):
save_file(path)
func _on_load_path_chosen(path):
load_file.bind(path).call_deferred()
# ------------------
# Public
# ------------------
func get_config_issues():
var to_return = []
var has_directory = false
for i in range(DIRS_TO_LIST):
var key = str('directory_', i)
var path = _cfg_ctrls[key].value
if(path != null and path != ''):
has_directory = true
if(!DirAccess.dir_exists_absolute(path)):
_cfg_ctrls[key].mark_invalid(true)
to_return.append(str('Test directory ', path, ' does not exist.'))
else:
_cfg_ctrls[key].mark_invalid(false)
else:
_cfg_ctrls[key].mark_invalid(false)
if(!has_directory):
to_return.append('You do not have any directories set.')
_titles.dirs.mark_invalid(true)
else:
_titles.dirs.mark_invalid(false)
if(!_cfg_ctrls.suffix.value.ends_with('.gd')):
_cfg_ctrls.suffix.mark_invalid(true)
to_return.append("Script suffix must end in '.gd'")
else:
_cfg_ctrls.suffix.mark_invalid(false)
return to_return
func clear():
opt_maker.clear()
func save_file(path):
var gcfg = GutConfig.new()
gcfg.options = get_options({})
gcfg.save_file(path)
func load_file(path):
var gcfg = GutConfig.new()
gcfg.load_options(path)
clear()
set_options(gcfg.options)
# --------------
# SUPER dumb but VERY fun hack to hide settings. The various _add methods will
# return what they add. If you want to hide it, just assign the result to this.
# YES, I could have just put .visible at the end, but I didn't think of that
# until just now, and this was fun, non-permanent and the .visible at the end
# isn't as obvious as hide_this =
#
# Also, we can't just skip adding the controls because other things are looking
# for them and things start to blow up if you don't add them.
var hide_this = null :
set(val):
val.visible = false
# --------------
func set_options(opts):
var options = opts.duplicate()
# _add_title('Save/Load')
_add_save_load()
opt_maker.add_title("Settings")
opt_maker.add_number("log_level", options.log_level, "Log Level", 0, 3,
"Detail level for log messages.\n" + \
"\t0: Errors and failures only.\n" + \
"\t1: Adds all test names + warnings + info\n" + \
"\t2: Shows all asserts\n" + \
"\t3: Adds more stuff probably, maybe not.")
opt_maker.add_float("wait_log_delay", options.wait_log_delay, "Wait Log Delay", 0.1, 0.0, 999.1,
"How long to wait before displaying 'Awaiting' messages.")
opt_maker.add_boolean('ignore_pause', options.ignore_pause, 'Ignore Pause',
"Ignore calls to pause_before_teardown")
opt_maker.add_boolean('hide_orphans', options.hide_orphans, 'Hide Orphans',
'Do not display orphan counts in output.')
opt_maker.add_boolean('should_exit', options.should_exit, 'Exit on Finish',
"Exit when tests finished.")
opt_maker.add_boolean('should_exit_on_success', options.should_exit_on_success, 'Exit on Success',
"Exit if there are no failures. Does nothing if 'Exit on Finish' is enabled.")
opt_maker.add_select('double_strategy', 'Script Only', ['Include Native', 'Script Only'], 'Double Strategy',
'"Include Native" will include native methods in Doubles. "Script Only" will not. ' + "\n" + \
'The native method override warning is disabled when creating Doubles.' + "\n" + \
'This is the default, you can override this at the script level or when creating doubles.')
_cfg_ctrls.double_strategy.value = GutUtils.get_enum_value(
options.double_strategy, GutUtils.DOUBLE_STRATEGY, GutUtils.DOUBLE_STRATEGY.SCRIPT_ONLY)
opt_maker.add_title("Fail Error Types")
opt_maker.add_boolean("error_tracking", !options.no_error_tracking, 'Track Errors',
"Enable/Disable GUT's ability to detect engine and push errors.")
opt_maker.add_boolean('engine_errors_cause_failure', options.failure_error_types.has(GutConfig.FAIL_ERROR_TYPE_ENGINE),
'Engine', 'Any script/engine error that occurs during a test will cause the test to fail.')
opt_maker.add_boolean('push_error_errors_cause_failure', options.failure_error_types.has(GutConfig.FAIL_ERROR_TYPE_PUSH_ERROR),
'Push', 'Any error generated by a call to push_error that occurs during a test will cause the test to fail.')
opt_maker.add_boolean('gut_errors_cause_failure', options.failure_error_types.has(GutConfig.FAIL_ERROR_TYPE_GUT),
'GUT', 'Any internal GUT error that occurs while a test is running will cause it to fail..')
opt_maker.add_title('Runner Appearance')
hide_this = opt_maker.add_boolean("gut_on_top", options.gut_on_top, "On Top",
"The GUT Runner appears above children added during tests.")
opt_maker.add_number('opacity', options.opacity, 'Opacity', 0, 100,
"The opacity of GUT when tests are running.")
hide_this = opt_maker.add_boolean('should_maximize', options.should_maximize, 'Maximize',
"Maximize GUT when tests are being run.")
opt_maker.add_boolean('compact_mode', options.compact_mode, 'Compact Mode',
'The runner will be in compact mode. This overrides Maximize.')
opt_maker.add_select('font_name', options.font_name, GutUtils.avail_fonts, 'Font',
"The font to use for text output in the Gut Runner.")
opt_maker.add_number('font_size', options.font_size, 'Font Size', 5, 100,
"The font size for text output in the Gut Runner.")
hide_this = opt_maker.add_color('font_color', options.font_color, 'Font Color',
"The font color for text output in the Gut Runner.")
opt_maker.add_color('background_color', options.background_color, 'Background Color',
"The background color for text output in the Gut Runner.")
opt_maker.add_boolean('disable_colors', options.disable_colors, 'Disable Formatting',
'Disable formatting and colors used in the Runner. Does not affect panel output.')
_titles.dirs = opt_maker.add_title('Test Directories')
opt_maker.add_boolean('include_subdirs', options.include_subdirs, 'Include Subdirs',
"Include subdirectories of the directories configured below.")
var dirs_to_load = options.configured_dirs
if(options.dirs.size() > dirs_to_load.size()):
dirs_to_load = options.dirs
for i in range(DIRS_TO_LIST):
var value = ''
if(dirs_to_load.size() > i):
value = dirs_to_load[i]
var test_dir = opt_maker.add_directory(str('directory_', i), value, str(i))
test_dir.enabled_button.visible = true
test_dir.enabled_button.button_pressed = options.dirs.has(value)
opt_maker.add_title("XML Output")
opt_maker.add_save_file_anywhere("junit_xml_file", options.junit_xml_file, "Output Path",
"Path and filename where GUT should create a JUnit compliant XML file. " +
"This file will contain the results of the last test run. To avoid " +
"overriding the file use Include Timestamp.")
opt_maker.add_boolean("junit_xml_timestamp", options.junit_xml_timestamp, "Include Timestamp",
"Include a timestamp in the filename so that each run gets its own xml file.")
opt_maker.add_title('Hooks')
opt_maker.add_file('pre_run_script', options.pre_run_script, 'Pre-Run Hook',
'This script will be run by GUT before any tests are run.')
opt_maker.add_file('post_run_script', options.post_run_script, 'Post-Run Hook',
'This script will be run by GUT after all tests are run.')
opt_maker.add_title('Misc')
opt_maker.add_value('prefix', options.prefix, 'Script Prefix',
"The filename prefix for all test scripts.")
opt_maker.add_value('suffix', options.suffix, 'Script Suffix',
"Script suffix, including .gd extension. For example '_foo.gd'.")
opt_maker.add_float('paint_after', options.paint_after, 'Paint After', .05, 0.0, 1.0,
"How long GUT will wait before pausing for 1 frame to paint the screen. 0 is never.")
func get_options(base_opts):
var to_return = base_opts.duplicate()
# Settings
to_return.log_level = _cfg_ctrls.log_level.value
to_return.wait_log_delay = _cfg_ctrls.wait_log_delay.value
to_return.ignore_pause = _cfg_ctrls.ignore_pause.value
to_return.hide_orphans = _cfg_ctrls.hide_orphans.value
to_return.should_exit = _cfg_ctrls.should_exit.value
to_return.should_exit_on_success = _cfg_ctrls.should_exit_on_success.value
to_return.double_strategy = _cfg_ctrls.double_strategy.value
# Runner Appearance
to_return.font_name = _cfg_ctrls.font_name.text
to_return.font_size = _cfg_ctrls.font_size.value
to_return.should_maximize = _cfg_ctrls.should_maximize.value
to_return.compact_mode = _cfg_ctrls.compact_mode.value
to_return.opacity = _cfg_ctrls.opacity.value
to_return.background_color = _cfg_ctrls.background_color.value.to_html()
to_return.font_color = _cfg_ctrls.font_color.value.to_html()
to_return.disable_colors = _cfg_ctrls.disable_colors.value
to_return.gut_on_top = _cfg_ctrls.gut_on_top.value
to_return.paint_after = _cfg_ctrls.paint_after.value
# Fail Error Types
to_return.no_error_tracking = !_cfg_ctrls.error_tracking
var fail_error_types = []
if(_cfg_ctrls.engine_errors_cause_failure.value):
fail_error_types.append(GutConfig.FAIL_ERROR_TYPE_ENGINE)
if(_cfg_ctrls.push_error_errors_cause_failure.value):
fail_error_types.append(GutConfig.FAIL_ERROR_TYPE_PUSH_ERROR)
if(_cfg_ctrls.gut_errors_cause_failure.value):
fail_error_types.append(GutConfig.FAIL_ERROR_TYPE_GUT)
to_return.failure_error_types = fail_error_types
# Directories
to_return.include_subdirs = _cfg_ctrls.include_subdirs.value
var dirs = []
var configured_dirs = []
for i in range(DIRS_TO_LIST):
var key = str('directory_', i)
var ctrl = _cfg_ctrls[key]
if(ctrl.value != '' and ctrl.value != null):
configured_dirs.append(ctrl.value)
if(ctrl.enabled_button.button_pressed):
dirs.append(ctrl.value)
to_return.dirs = dirs
to_return.configured_dirs = configured_dirs
# XML Output
to_return.junit_xml_file = _cfg_ctrls.junit_xml_file.value
to_return.junit_xml_timestamp = _cfg_ctrls.junit_xml_timestamp.value
# Hooks
to_return.pre_run_script = _cfg_ctrls.pre_run_script.value
to_return.post_run_script = _cfg_ctrls.post_run_script.value
# Misc
to_return.prefix = _cfg_ctrls.prefix.value
to_return.suffix = _cfg_ctrls.suffix.value
return to_return
func mark_saved():
for key in _cfg_ctrls:
_cfg_ctrls[key].mark_unsaved(false)

View File

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

239
addons/gut/gui/gut_gui.gd Normal file
View File

@@ -0,0 +1,239 @@
extends Control
# ##############################################################################
# This is the decoupled GUI for gut.gd
#
# This is a "generic" interface between a GUI and gut.gd. It assumes there are
# certain controls with specific names. It will then interact with those
# controls based on signals emitted from gut.gd in order to give the user
# feedback about the progress of the test run and the results.
#
# Optional controls are marked as such in the _ctrls dictionary. The names
# of the controls can be found in _populate_ctrls.
# ##############################################################################
var _gut = null
var _ctrls = {
btn_continue = null,
path_dir = null,
path_file = null,
prog_script = null,
prog_test = null,
rtl = null, # optional
rtl_bg = null, # required if rtl exists
switch_modes = null,
time_label = null,
title = null,
title_bar = null,
tgl_word_wrap = null, # optional
}
var _title_mouse = {
down = false
}
signal switch_modes()
var _max_position = Vector2(100, 100)
func _ready():
_populate_ctrls()
_ctrls.btn_continue.visible = false
_ctrls.btn_continue.pressed.connect(_on_continue_pressed)
_ctrls.switch_modes.pressed.connect(_on_switch_modes_pressed)
_ctrls.title_bar.gui_input.connect(_on_title_bar_input)
if(_ctrls.tgl_word_wrap != null):
_ctrls.tgl_word_wrap.toggled.connect(_on_word_wrap_toggled)
_ctrls.prog_script.value = 0
_ctrls.prog_test.value = 0
_ctrls.path_dir.text = ''
_ctrls.path_file.text = ''
_ctrls.time_label.text = ''
_max_position = get_display_size() - Vector2(30, _ctrls.title_bar.size.y)
func _process(_delta):
if(_gut != null and _gut.is_running()):
set_elapsed_time(_gut.get_elapsed_time())
# ------------------
# Private
# ------------------
func get_display_size():
return get_viewport().get_visible_rect().size
func _populate_ctrls():
# Brute force, but flexible. This allows for all the controls to exist
# anywhere, and as long as they all have the right name, they will be
# found.
_ctrls.btn_continue = _get_first_child_named('Continue', self)
_ctrls.path_dir = _get_first_child_named('Path', self)
_ctrls.path_file = _get_first_child_named('File', self)
_ctrls.prog_script = _get_first_child_named('ProgressScript', self)
_ctrls.prog_test = _get_first_child_named('ProgressTest', self)
_ctrls.rtl = _get_first_child_named('TestOutput', self)
_ctrls.rtl_bg = _get_first_child_named('OutputBG', self)
_ctrls.switch_modes = _get_first_child_named("SwitchModes", self)
_ctrls.time_label = _get_first_child_named('TimeLabel', self)
_ctrls.title = _get_first_child_named("Title", self)
_ctrls.title_bar = _get_first_child_named("TitleBar", self)
_ctrls.tgl_word_wrap = _get_first_child_named("WordWrap", self)
func _get_first_child_named(obj_name, parent_obj):
if(parent_obj == null):
return null
var kids = parent_obj.get_children()
var index = 0
var to_return = null
while(index < kids.size() and to_return == null):
if(str(kids[index]).find(str(obj_name, ':')) != -1):
to_return = kids[index]
else:
to_return = _get_first_child_named(obj_name, kids[index])
if(to_return == null):
index += 1
return to_return
# ------------------
# Events
# ------------------
func _on_title_bar_input(event : InputEvent):
if(event is InputEventMouseMotion):
if(_title_mouse.down):
position += event.relative
position.x = clamp(position.x, 0, _max_position.x)
position.y = clamp(position.y, 0, _max_position.y)
elif(event is InputEventMouseButton):
if(event.button_index == MOUSE_BUTTON_LEFT):
_title_mouse.down = event.pressed
func _on_continue_pressed():
_gut.end_teardown_pause()
func _on_gut_start_run():
if(_ctrls.rtl != null):
_ctrls.rtl.clear()
set_num_scripts(_gut.get_test_collector().scripts.size())
func _on_gut_end_run():
_ctrls.prog_test.value = _ctrls.prog_test.max_value
_ctrls.prog_script.value = _ctrls.prog_script.max_value
func _on_gut_start_script(script_obj):
next_script(script_obj.get_full_name(), script_obj.tests.size())
func _on_gut_end_script():
pass
func _on_gut_start_test(test_name):
next_test(test_name)
func _on_gut_end_test():
pass
func _on_gut_start_pause():
pause_before_teardown()
func _on_gut_end_pause():
_ctrls.btn_continue.visible = false
func _on_switch_modes_pressed():
switch_modes.emit()
func _on_word_wrap_toggled(toggled):
_ctrls.rtl.autowrap_mode = toggled
# ------------------
# Public
# ------------------
func set_num_scripts(val):
_ctrls.prog_script.value = 0
_ctrls.prog_script.max_value = val
func next_script(path, num_tests):
_ctrls.prog_script.value += 1
_ctrls.prog_test.value = 0
_ctrls.prog_test.max_value = num_tests
_ctrls.path_dir.text = path.get_base_dir()
_ctrls.path_file.text = path.get_file()
func next_test(__test_name):
_ctrls.prog_test.value += 1
func pause_before_teardown():
_ctrls.btn_continue.visible = true
func set_gut(g):
if(_gut == g):
return
_gut = g
g.start_run.connect(_on_gut_start_run)
g.end_run.connect(_on_gut_end_run)
g.start_script.connect(_on_gut_start_script)
g.end_script.connect(_on_gut_end_script)
g.start_test.connect(_on_gut_start_test)
g.end_test.connect(_on_gut_end_test)
g.start_pause_before_teardown.connect(_on_gut_start_pause)
g.end_pause_before_teardown.connect(_on_gut_end_pause)
func get_gut():
return _gut
func get_textbox():
return _ctrls.rtl
func set_elapsed_time(t):
_ctrls.time_label.text = str("%6.1f" % t, 's')
func set_bg_color(c):
_ctrls.rtl_bg.color = c
func set_title(text):
_ctrls.title.text = text
func to_top_left():
self.position = Vector2(5, 5)
func to_bottom_right():
var win_size = get_display_size()
self.position = win_size - Vector2(self.size) - Vector2(5, 5)
func align_right():
var win_size = get_display_size()
self.position.x = win_size.x - self.size.x -5
self.position.y = 5
self.size.y = win_size.y - 10

View File

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

225
addons/gut/gui/gut_logo.gd Normal file
View File

@@ -0,0 +1,225 @@
@tool
extends Node2D
class Eyeball:
extends Node2D
var _should_draw_laser = false
var _laser_end_pos = Vector2.ZERO
var _laser_timer : Timer = null
var _color_tween : Tween
var _size_tween : Tween
var sprite : Sprite2D = null
var default_position = Vector2(0, 0)
var move_radius = 25
var move_center = Vector2(0, 0)
var default_color = Color(0.31, 0.31, 0.31)
var _color = default_color :
set(val):
_color = val
queue_redraw()
var color = _color :
set(val):
_start_color_tween(_color, val)
get(): return _color
var default_size = 70
var _size = default_size :
set(val):
_size = val
queue_redraw()
var size = _size :
set(val):
_start_size_tween(_size, val)
get(): return _size
func _init(node):
sprite = node
default_position = sprite.position
move_center = sprite.position
# hijack the original sprite, because I want to draw it here but keep
# the original in the scene for layout.
position = sprite.position
sprite.get_parent().add_child(self)
sprite.visible = false
func _ready():
_laser_timer = Timer.new()
_laser_timer.wait_time = .1
_laser_timer.one_shot = true
add_child(_laser_timer)
_laser_timer.timeout.connect(func(): _should_draw_laser = false)
func _process(_delta):
if(_should_draw_laser):
queue_redraw()
func _start_color_tween(old_color, new_color):
if(_color_tween != null and _color_tween.is_running()):
_color_tween.kill()
_color_tween = create_tween()
_color_tween.tween_property(self, '_color', new_color, .3).from(old_color)
_color_tween.play()
func _start_size_tween(old_size, new_size):
if(_size_tween != null and _size_tween.is_running()):
_size_tween.kill()
_size_tween = create_tween()
_size_tween.tween_property(self, '_size', new_size, .3).from(old_size)
_size_tween.play()
var _laser_size = 20.0
func _draw() -> void:
draw_circle(Vector2.ZERO, size, color, true, -1, true)
if(_should_draw_laser):
var end_pos = (_laser_end_pos - global_position) * 2
var laser_size = _laser_size * (float(size)/float(default_size))
draw_line(Vector2.ZERO, end_pos, color, laser_size)
draw_line(Vector2.ZERO, end_pos, Color(1, 1, 1, .5), laser_size * .8)
# There's a bug in here where the eye shakes like crazy. It's a feature
# now. Don't fix it.
func look_at_local_position(local_pos):
var dir = position.direction_to(local_pos)
var dist = position.distance_to(local_pos)
position = move_center + (dir * min(dist, move_radius))
position.x = clamp(position.x, move_center.x - move_radius, move_center.x + move_radius)
position.y = clamp(position.y, move_center.y - move_radius, move_center.y + move_radius)
func reset():
color = default_color
size = default_size
func eye_laser(global_pos):
_should_draw_laser = true
_laser_end_pos = global_pos
_laser_timer.start()
func _stop_laser():
_should_draw_laser = false
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')
# Active means it's actively doing stuff. When this is not active the eyes
# won't follow, but you can still make the sizes change by calling methods on
# this.
@export var active = false :
set(val):
active = val
if(!active and is_inside_tree()):
left_eye.position = left_eye.default_position
right_eye.position = right_eye.default_position
# When disabled, this will reset to default and you can't make it do anything.
@export var disabled = false :
set(val):
disabled = val
if(disabled and is_inside_tree()):
left_eye.position = left_eye.default_position
right_eye.position = right_eye.default_position
left_eye.reset()
right_eye.reset()
modulate = Color.GRAY
$BaseLogo.texture = _no_shine
else:
$BaseLogo.texture = _normal
modulate = Color.WHITE
@onready var _reset_timer = $ResetTimer
@onready var _face_button = $FaceButton
@onready var left_eye : Eyeball = Eyeball.new($BaseLogo/LeftEye)
@onready var right_eye : Eyeball = Eyeball.new($BaseLogo/RightEye)
var _no_shine = load("res://addons/gut/images/GutIconV2_no_shine.png")
var _normal = load("res://addons/gut/images/GutIconV2_base.png")
var _is_in_edited_scene = false
signal pressed
func _debug_ready():
position = Vector2(500, 500)
active = true
func _ready():
_is_in_edited_scene = GutEditorGlobals.is_being_edited_in_editor(self)
if(get_parent() == get_tree().root):
_debug_ready()
disabled = disabled
active = active
left_eye.move_center.x -= 20
right_eye.move_center.x += 10
_face_button.modulate.a = 0.0
func _process(_delta):
if(active and !disabled and !_is_in_edited_scene):
left_eye.look_at_local_position(get_local_mouse_position())
right_eye.look_at_local_position(get_local_mouse_position())
# ----------------
# Events
# ----------------
func _on_reset_timer_timeout() -> void:
left_eye.reset()
right_eye.reset()
func _on_face_button_pressed() -> void:
pressed.emit()
# ----------------
# Public
# ----------------
func set_eye_scale(left, right=left):
if(disabled or _is_in_edited_scene):
return
left_eye.size = left_eye.default_size * left
right_eye.size = right_eye.default_size * right
_reset_timer.start()
func reset_eye_size():
if(disabled or _is_in_edited_scene):
return
left_eye.size = left_eye.default_size
right_eye.size = right_eye.default_size
func set_eye_color(left, right=left):
if(disabled or _is_in_edited_scene):
return
left_eye.color = left
right_eye.color = right
_reset_timer.start()
func reset_eye_color():
if(disabled or _is_in_edited_scene):
return
left_eye.color = left_eye.default_color
right_eye.color = right_eye.default_color
# I removed the eye lasers because they aren't ready yet. I've already spent
# too much time on this logo. It's great, I love it...but it's been long
# enough. This gives me, or someone else, something to do later.
#func eye_lasers(global_pos):
#left_eye.eye_laser(global_pos)
#right_eye.eye_laser(global_pos)

View File

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

View File

@@ -0,0 +1,79 @@
class GutEditorPref:
var gut_pref_prefix = 'gut/'
var pname = '__not_set__'
var default = null
var value = '__not_set__'
var _settings = null
func _init(n, d, s):
pname = n
default = d
_settings = s
load_it()
func _prefstr():
var to_return = str(gut_pref_prefix, pname)
return to_return
func save_it():
_settings.set_setting(_prefstr(), value)
func load_it():
if(_settings.has_setting(_prefstr())):
value = _settings.get_setting(_prefstr())
else:
value = default
func erase():
_settings.erase(_prefstr())
const EMPTY = '-- NOT_SET --'
# -- Editor ONLY Settings --
var output_font_name = null
var output_font_size = null
var hide_result_tree = null
var hide_output_text = null
var hide_settings = null
var use_colors = null # ? might be output panel
var run_externally = null
var run_externally_options_dialog_size = null
var shortcuts_dialog_size = null
var gut_window_size = null
var gut_window_on_top = null
func _init(editor_settings):
output_font_name = GutEditorPref.new('output_font_name', 'CourierPrime', editor_settings)
output_font_size = GutEditorPref.new('output_font_size', 30, editor_settings)
hide_result_tree = GutEditorPref.new('hide_result_tree', false, editor_settings)
hide_output_text = GutEditorPref.new('hide_output_text', false, editor_settings)
hide_settings = GutEditorPref.new('hide_settings', false, editor_settings)
use_colors = GutEditorPref.new('use_colors', true, editor_settings)
run_externally = GutEditorPref.new('run_externally', false, editor_settings)
run_externally_options_dialog_size = GutEditorPref.new('run_externally_options_dialog_size', Vector2i(-1, -1), editor_settings)
shortcuts_dialog_size = GutEditorPref.new('shortcuts_dialog_size', Vector2i(-1, -1), editor_settings)
gut_window_size = GutEditorPref.new('editor_window_size', Vector2i(-1, -1), editor_settings)
gut_window_on_top = GutEditorPref.new('editor_window_on_top', false, editor_settings)
func save_it():
for prop in get_property_list():
var val = get(prop.name)
if(val is GutEditorPref):
val.save_it()
func load_it():
for prop in get_property_list():
var val = get(prop.name)
if(val is GutEditorPref):
val.load_it()
func erase_all():
for prop in get_property_list():
var val = get(prop.name)
if(val is GutEditorPref):
val.erase()

View File

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

View File

@@ -0,0 +1,124 @@
var PanelControls = load("res://addons/gut/gui/panel_controls.gd")
# All titles so we can free them when we want.
var _all_titles = []
var base_container = null
# All the various PanelControls indexed by thier keys.
var controls = {}
func _init(cont):
base_container = cont
func add_title(text):
var row = PanelControls.BaseGutPanelControl.new(text, text)
base_container.add_child(row)
row.connect('draw', _on_title_cell_draw.bind(row))
_all_titles.append(row)
return row
func add_ctrl(key, ctrl):
controls[key] = ctrl
base_container.add_child(ctrl)
func add_number(key, value, disp_text, v_min, v_max, hint=''):
var ctrl = PanelControls.NumberControl.new(disp_text, value, v_min, v_max, hint)
add_ctrl(key, ctrl)
return ctrl
func add_float(key, value, disp_text, step, v_min, v_max, hint=''):
var ctrl = PanelControls.FloatControl.new(disp_text, value, step, v_min, v_max, hint)
add_ctrl(key, ctrl)
return ctrl
func add_select(key, value, values, disp_text, hint=''):
var ctrl = PanelControls.SelectControl.new(disp_text, value, values, hint)
add_ctrl(key, ctrl)
return ctrl
func add_value(key, value, disp_text, hint=''):
var ctrl = PanelControls.StringControl.new(disp_text, value, hint)
add_ctrl(key, ctrl)
return ctrl
func add_multiline_text(key, value, disp_text, hint=''):
var ctrl = PanelControls.MultiLineStringControl.new(disp_text, value, hint)
add_ctrl(key, ctrl)
return ctrl
func add_boolean(key, value, disp_text, hint=''):
var ctrl = PanelControls.BooleanControl.new(disp_text, value, hint)
add_ctrl(key, ctrl)
return ctrl
func add_directory(key, value, disp_text, hint=''):
var ctrl = PanelControls.DirectoryControl.new(disp_text, value, hint)
add_ctrl(key, ctrl)
ctrl.dialog.title = disp_text
return ctrl
func add_file(key, value, disp_text, hint=''):
var ctrl = PanelControls.DirectoryControl.new(disp_text, value, hint)
add_ctrl(key, ctrl)
ctrl.dialog.file_mode = ctrl.dialog.FILE_MODE_OPEN_FILE
ctrl.dialog.title = disp_text
return ctrl
func add_save_file_anywhere(key, value, disp_text, hint=''):
var ctrl = PanelControls.DirectoryControl.new(disp_text, value, hint)
add_ctrl(key, ctrl)
ctrl.dialog.file_mode = ctrl.dialog.FILE_MODE_SAVE_FILE
ctrl.dialog.access = ctrl.dialog.ACCESS_FILESYSTEM
ctrl.dialog.title = disp_text
return ctrl
func add_color(key, value, disp_text, hint=''):
var ctrl = PanelControls.ColorControl.new(disp_text, value, hint)
add_ctrl(key, ctrl)
return ctrl
var _blurbs = 0
func add_blurb(text):
var ctrl = RichTextLabel.new()
ctrl.fit_content = true
ctrl.bbcode_enabled = true
ctrl.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
ctrl.text = text
add_ctrl(str("blurb_", _blurbs), ctrl)
return ctrl
# ------------------
# Events
# ------------------
func _on_title_cell_draw(which):
which.draw_rect(Rect2(Vector2(0, 0), which.size), Color(0, 0, 0, .15))
# ------------------
# Public
# ------------------
func clear():
for key in controls:
controls[key].free()
controls.clear()
for entry in _all_titles:
entry.free()
_all_titles.clear()

View File

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

View File

@@ -0,0 +1,484 @@
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
class BaseGutPanelControl:
extends HBoxContainer
var label = Label.new()
var _lbl_unsaved = Label.new()
var _lbl_invalid = Label.new()
var value = null:
get: return get_value()
set(val): set_value(val)
signal changed
func _init(title, val, hint=""):
size_flags_horizontal = SIZE_EXPAND_FILL
mouse_filter = MOUSE_FILTER_PASS
label.size_flags_horizontal = label.SIZE_EXPAND_FILL
label.mouse_filter = label.MOUSE_FILTER_STOP
add_child(label)
_lbl_unsaved.text = '*'
_lbl_unsaved.visible = false
add_child(_lbl_unsaved)
_lbl_invalid.text = '!'
_lbl_invalid.visible = false
add_child(_lbl_invalid)
label.text = title
label.tooltip_text = hint
func mark_unsaved(is_it=true):
_lbl_unsaved.visible = is_it
func mark_invalid(is_it):
_lbl_invalid.visible = is_it
# -- Virtual --
#
# value_ctrl (all should declare the value_ctrl)
#
func set_value(value):
pass
func get_value():
pass
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
class NumberControl:
extends BaseGutPanelControl
var value_ctrl = SpinBox.new()
func _init(title, val, v_min, v_max, hint=""):
super._init(title, val, hint)
value_ctrl.value = val
value_ctrl.size_flags_horizontal = value_ctrl.SIZE_EXPAND_FILL
value_ctrl.min_value = v_min
value_ctrl.max_value = v_max
value_ctrl.value_changed.connect(_on_value_changed)
value_ctrl.select_all_on_focus = true
add_child(value_ctrl)
func _on_value_changed(new_value):
changed.emit()
func get_value():
return value_ctrl.value
func set_value(val):
value_ctrl.value = val
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
class FloatControl:
extends NumberControl
func _init(title, val, step, v_min, v_max, hint=""):
super._init(title, val, v_min, v_max, hint)
value_ctrl.step = step
value_ctrl.value = val
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
class StringControl:
extends BaseGutPanelControl
var value_ctrl = LineEdit.new()
func _init(title, val, hint=""):
super._init(title, val, hint)
value_ctrl.size_flags_horizontal = value_ctrl.SIZE_EXPAND_FILL
value_ctrl.text = val
value_ctrl.text_changed.connect(_on_text_changed)
value_ctrl.select_all_on_focus = true
add_child(value_ctrl)
if(title == ''):
label.visible = false
func _on_text_changed(new_value):
changed.emit()
func get_value():
return value_ctrl.text
func set_value(val):
value_ctrl.text = val
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
class MultiLineStringControl:
extends BaseGutPanelControl
var value_ctrl = TextEdit.new()
func _init(title, val, hint=""):
super._init(title, val, hint)
var vbox = VBoxContainer.new()
vbox.size_flags_horizontal = SIZE_EXPAND_FILL
add_child(vbox)
label.reparent(vbox)
value_ctrl.size_flags_horizontal = value_ctrl.SIZE_EXPAND_FILL
value_ctrl.text = val
value_ctrl.text_changed.connect(_on_text_changed)
value_ctrl.scroll_fit_content_height = true
vbox.add_child(value_ctrl)
func _on_text_changed(new_value):
changed.emit()
func get_value():
return value_ctrl.text
func set_value(val):
value_ctrl.text = val
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
class BooleanControl:
extends BaseGutPanelControl
var value_ctrl = CheckBox.new()
func _init(title, val, hint=""):
super._init(title, val, hint)
value_ctrl.button_pressed = val
value_ctrl.toggled.connect(_on_button_toggled)
add_child(value_ctrl)
func _on_button_toggled(new_value):
changed.emit()
func get_value():
return value_ctrl.button_pressed
func set_value(val):
value_ctrl.button_pressed = val
# ------------------------------------------------------------------------------
# value is "selected" and is gettable and settable
# text is the text value of the selected item, it is gettable only
# ------------------------------------------------------------------------------
class SelectControl:
extends BaseGutPanelControl
var value_ctrl = OptionButton.new()
var text = '' :
get: return value_ctrl.get_item_text(value_ctrl.selected)
set(val): pass
func _init(title, val, choices, hint=""):
super._init(title, val, hint)
var select_idx = 0
for i in range(choices.size()):
value_ctrl.add_item(choices[i])
if(val == choices[i]):
select_idx = i
value_ctrl.selected = select_idx
value_ctrl.size_flags_horizontal = value_ctrl.SIZE_EXPAND_FILL
value_ctrl.item_selected.connect(_on_item_selected)
add_child(value_ctrl)
func _on_item_selected(idx):
changed.emit()
func get_value():
return value_ctrl.selected
func set_value(val):
value_ctrl.selected = val
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
class ColorControl:
extends BaseGutPanelControl
var value_ctrl = ColorPickerButton.new()
func _init(title, val, hint=""):
super._init(title, val, hint)
value_ctrl.size_flags_horizontal = value_ctrl.SIZE_EXPAND_FILL
value_ctrl.color = val
add_child(value_ctrl)
func get_value():
return value_ctrl.color
func set_value(val):
value_ctrl.color = val
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
class DirectoryControl:
extends BaseGutPanelControl
var value_ctrl := LineEdit.new()
var dialog := FileDialog.new()
var enabled_button = CheckButton.new()
var _btn_dir := Button.new()
func _init(title, val, hint=""):
super._init(title, val, hint)
label.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
_btn_dir.text = '...'
_btn_dir.pressed.connect(_on_dir_button_pressed)
value_ctrl.text = val
value_ctrl.size_flags_horizontal = value_ctrl.SIZE_EXPAND_FILL
value_ctrl.select_all_on_focus = true
value_ctrl.text_changed.connect(_on_value_changed)
dialog.file_mode = dialog.FILE_MODE_OPEN_DIR
dialog.unresizable = false
dialog.dir_selected.connect(_on_selected)
dialog.file_selected.connect(_on_selected)
enabled_button.button_pressed = true
enabled_button.visible = false
add_child(enabled_button)
add_child(value_ctrl)
add_child(_btn_dir)
add_child(dialog)
func _update_display():
var is_empty = value_ctrl.text == ''
enabled_button.button_pressed = !is_empty
enabled_button.disabled = is_empty
func _ready():
if(Engine.is_editor_hint()):
dialog.size = Vector2(1000, 700)
else:
dialog.size = Vector2(500, 350)
_update_display()
func _on_value_changed(new_text):
_update_display()
func _on_selected(path):
value_ctrl.text = path
_update_display()
func _on_dir_button_pressed():
dialog.current_dir = value_ctrl.text
dialog.popup_centered()
func get_value():
return value_ctrl.text
func set_value(val):
value_ctrl.text = val
# ------------------------------------------------------------------------------
# Features:
# Buttons to pick res://, user://, or anywhere on the OS.
# ------------------------------------------------------------------------------
class FileDialogSuperPlus:
extends FileDialog
var show_diretory_types = true :
set(val) :
show_diretory_types = val
_update_display()
var show_res = true :
set(val) :
show_res = val
_update_display()
var show_user = true :
set(val) :
show_user = val
_update_display()
var show_os = true :
set(val) :
show_os = val
_update_display()
var _dir_type_hbox = null
var _btn_res = null
var _btn_user = null
var _btn_os = null
func _ready():
_init_controls()
_update_display()
func _init_controls():
_dir_type_hbox = HBoxContainer.new()
_btn_res = Button.new()
_btn_user = Button.new()
_btn_os = Button.new()
var spacer1 = CenterContainer.new()
spacer1.size_flags_horizontal = spacer1.SIZE_EXPAND_FILL
var spacer2 = spacer1.duplicate()
_dir_type_hbox.add_child(spacer1)
_dir_type_hbox.add_child(_btn_res)
_dir_type_hbox.add_child(_btn_user)
_dir_type_hbox.add_child(_btn_os)
_dir_type_hbox.add_child(spacer2)
_btn_res.text = 'res://'
_btn_user.text = 'user://'
_btn_os.text = ' OS '
get_vbox().add_child(_dir_type_hbox)
get_vbox().move_child(_dir_type_hbox, 0)
_btn_res.pressed.connect(func(): access = ACCESS_RESOURCES)
_btn_user.pressed.connect(func(): access = ACCESS_USERDATA)
_btn_os.pressed.connect(func(): access = ACCESS_FILESYSTEM)
func _update_display():
if(is_inside_tree()):
_dir_type_hbox.visible = show_diretory_types
_btn_res.visible = show_res
_btn_user.visible = show_user
_btn_os.visible = show_os
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
class SaveLoadControl:
extends BaseGutPanelControl
var btn_load = Button.new()
var btn_save = Button.new()
var dlg_load := FileDialogSuperPlus.new()
var dlg_save := FileDialogSuperPlus.new()
signal save_path_chosen(path)
signal load_path_chosen(path)
func _init(title, val, hint):
super._init(title, val, hint)
btn_load.text = "Load"
btn_load.custom_minimum_size.x = 100
btn_load.pressed.connect(_on_load_pressed)
add_child(btn_load)
btn_save.text = "Save As"
btn_save.custom_minimum_size.x = 100
btn_save.pressed.connect(_on_save_pressed)
add_child(btn_save)
dlg_load.file_mode = dlg_load.FILE_MODE_OPEN_FILE
dlg_load.unresizable = false
dlg_load.dir_selected.connect(_on_load_selected)
dlg_load.file_selected.connect(_on_load_selected)
add_child(dlg_load)
dlg_save.file_mode = dlg_save.FILE_MODE_SAVE_FILE
dlg_save.unresizable = false
dlg_save.dir_selected.connect(_on_save_selected)
dlg_save.file_selected.connect(_on_save_selected)
add_child(dlg_save)
func _ready():
if(Engine.is_editor_hint()):
dlg_load.size = Vector2(1000, 700)
dlg_save.size = Vector2(1000, 700)
else:
dlg_load.size = Vector2(500, 350)
dlg_save.size = Vector2(500, 350)
func _on_load_selected(path):
load_path_chosen.emit(path)
func _on_save_selected(path):
save_path_chosen.emit(path)
func _on_load_pressed():
dlg_load.popup_centered()
func _on_save_pressed():
dlg_save.popup_centered()
# ------------------------------------------------------------------------------
# This one was never used in gut_config_gui...but I put some work into it and
# I'm a sucker for that kinda thing. Delete this when you get tired of looking
# at it.
# ------------------------------------------------------------------------------
# class Vector2Ctrl:
# extends VBoxContainer
# var value = Vector2(-1, -1) :
# get:
# return get_value()
# set(val):
# set_value(val)
# var disabled = false :
# get:
# return get_disabled()
# set(val):
# set_disabled(val)
# var x_spin = SpinBox.new()
# var y_spin = SpinBox.new()
# func _init():
# add_child(_make_one('x: ', x_spin))
# add_child(_make_one('y: ', y_spin))
# func _make_one(txt, spinner):
# var hbox = HBoxContainer.new()
# var lbl = Label.new()
# lbl.text = txt
# hbox.add_child(lbl)
# hbox.add_child(spinner)
# spinner.min_value = -1
# spinner.max_value = 10000
# spinner.size_flags_horizontal = spinner.SIZE_EXPAND_FILL
# return hbox
# func set_value(v):
# if(v != null):
# x_spin.value = v[0]
# y_spin.value = v[1]
# # Returns array instead of vector2 b/c that is what is stored in
# # in the dictionary and what is expected everywhere else.
# func get_value():
# return [x_spin.value, y_spin.value]
# func set_disabled(should):
# get_parent().visible = !should
# x_spin.visible = !should
# y_spin.visible = !should
# func get_disabled():
# pass

View File

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

BIN
addons/gut/gui/play.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cr6tvdv0ve6cv"
path="res://.godot/imported/play.png-5c90e88e8136487a183a099d67a7de24.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/gut/gui/play.png"
dest_files=["res://.godot/imported/play.png-5c90e88e8136487a183a099d67a7de24.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

View File

@@ -0,0 +1,19 @@
# ------------------------------------------------------------------------------
# This is the entry point when running tests from the editor.
#
# This script should conform to, or ignore, the strictest warning settings.
# ------------------------------------------------------------------------------
extends Node2D
var GutLoader : Object
func _init() -> void:
GutLoader = load("res://addons/gut/gut_loader.gd")
@warning_ignore("unsafe_method_access")
func _ready() -> void:
var runner : Node = load("res://addons/gut/gui/GutRunner.tscn").instantiate()
add_child(runner)
runner.run_from_editor()
GutLoader.restore_ignore_addons()

View File

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

View File

@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://bgj3fm5d8yvjw"]
[ext_resource type="Script" uid="uid://bwf2iuidqfkpl" path="res://addons/gut/gui/run_from_editor.gd" id="1_53pap"]
[node name="RunFromEditor" type="Node2D"]
script = ExtResource("1_53pap")