ajhahn.de
← Theria
GDScript 322 lines
@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 ''