You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			414 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			GDScript
		
	
			
		
		
	
	
			414 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			GDScript
		
	
| @tool
 | |
| extends Control
 | |
| 
 | |
| signal grid_updated()
 | |
| 
 | |
| const TablesPluginSettingsClass := preload("res://addons/resources_spreadsheet_view/settings_grid.gd")
 | |
| 
 | |
| @export @onready var node_folder_path : LineEdit = $"HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer/Path"
 | |
| @export @onready var node_recent_paths : OptionButton = $"HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer2/RecentPaths"
 | |
| @export @onready var node_table_root : GridContainer = $"HeaderContentSplit/MarginContainer/FooterContentSplit/Panel/Scroll/MarginContainer/TableGrid"
 | |
| @export @onready var node_columns : HBoxContainer = $"HeaderContentSplit/VBoxContainer/Columns/Columns"
 | |
| @export @onready var node_page_manager : Control = $"HeaderContentSplit/VBoxContainer/HBoxContainer3/Pages"
 | |
| 
 | |
| @onready var _on_cell_gui_input : Callable = $"InputHandler"._on_cell_gui_input
 | |
| @onready var _selection := $"SelectionManager"
 | |
| 
 | |
| var editor_interface : EditorInterface
 | |
| var editor_plugin : EditorPlugin
 | |
| 
 | |
| var current_path := ""
 | |
| var save_data_path : String = get_script().resource_path.get_base_dir() + "/saved_state.json"
 | |
| var sorting_by := ""
 | |
| var sorting_reverse := false
 | |
| 
 | |
| var columns := []
 | |
| var column_types := []
 | |
| var column_hints := []
 | |
| var column_hint_strings := []
 | |
| var rows := []
 | |
| var remembered_paths := {}
 | |
| var table_functions_dict := {}
 | |
| 
 | |
| var search_cond : RefCounted
 | |
| var io : RefCounted
 | |
| 
 | |
| var first_row := 0
 | |
| var last_row := 0
 | |
| 
 | |
| 
 | |
| func _ready():
 | |
| 	editor_interface.get_resource_filesystem().filesystem_changed.connect(_on_filesystem_changed)
 | |
| 	if FileAccess.file_exists(save_data_path):
 | |
| 		var file = FileAccess.open(save_data_path, FileAccess.READ)
 | |
| 		var as_text = file.get_as_text()
 | |
| 		var as_var = JSON.parse_string(as_text)
 | |
| 
 | |
| 		node_recent_paths.load_paths(as_var.get("recent_paths", []))
 | |
| 		node_columns.hidden_columns = as_var.get("hidden_columns", {})
 | |
| 		table_functions_dict = as_var.get("table_functions", {})
 | |
| 		for x in $"HeaderContentSplit/VBoxContainer/Search/Search".get_children():
 | |
| 			if x.has_method(&"load_saved_functions"):
 | |
| 				x.load_saved_functions(table_functions_dict)
 | |
| 
 | |
| 	if node_recent_paths.recent_paths.size() >= 1:
 | |
| 		display_folder(node_recent_paths.recent_paths[0], "resource_name", false, true)
 | |
| 
 | |
| 
 | |
| func _on_filesystem_changed():
 | |
| 	var editor_fs := editor_interface.get_resource_filesystem()
 | |
| 	var path := editor_fs.get_filesystem_path(current_path)
 | |
| 	if !path: return
 | |
| 
 | |
| 	var file_total_count := 0
 | |
| 	var folder_stack : Array[EditorFileSystemDirectory] = [path]
 | |
| 	while folder_stack.size() > 0:
 | |
| 		path = folder_stack.pop_back()
 | |
| 		file_total_count += path.get_file_count()
 | |
| 		for i in path.get_subdir_count():
 | |
| 			folder_stack.append(path.get_subdir(i))
 | |
| 
 | |
| 	if file_total_count != remembered_paths.size():
 | |
| 		refresh()
 | |
| 
 | |
| 	else:
 | |
| 		for k in remembered_paths:
 | |
| 			if !is_instance_valid(remembered_paths[k]):
 | |
| 				continue
 | |
| 
 | |
| 			if remembered_paths[k].resource_path != k:
 | |
| 				var res = remembered_paths[k]
 | |
| 				remembered_paths.erase(k)
 | |
| 				remembered_paths[res.resource_path] = res
 | |
| 				refresh()
 | |
| 				break
 | |
| 
 | |
| 
 | |
| func display_folder(folderpath : String, sort_by : String = "", sort_reverse : bool = false, force_rebuild : bool = false, is_echo : bool = false):
 | |
| 	if folderpath == "": return  # Root folder resources tend to have MANY properties.
 | |
| 	$"HeaderContentSplit/MarginContainer/FooterContentSplit/Panel/Label".visible = false
 | |
| 	if folderpath.get_extension() == "":
 | |
| 		folderpath = folderpath.trim_suffix("/") + "/"
 | |
| 
 | |
| 	if folderpath.ends_with(".tres") and !folderpath.ends_with(ResourceTablesImport.SUFFIX):
 | |
| 		folderpath = folderpath.get_base_dir() + "/"
 | |
| 
 | |
| 	node_recent_paths.add_path_to_recent(folderpath)
 | |
| 	first_row = node_page_manager.first_row
 | |
| 	_load_resources_from_path(folderpath, sort_by, sort_reverse)
 | |
| 	last_row = min(first_row + node_page_manager.rows_per_page, rows.size())
 | |
| 
 | |
| 	if columns.size() == 0: return
 | |
| 
 | |
| 	node_folder_path.text = folderpath
 | |
| 	_update_table(
 | |
| 		force_rebuild
 | |
| 		or current_path != folderpath
 | |
| 		or columns.size() != node_columns.get_child_count()
 | |
| 	)
 | |
| 	current_path = folderpath
 | |
| 	node_columns.update()
 | |
| 
 | |
| 	emit_signal("grid_updated")
 | |
| 
 | |
| 
 | |
| func refresh(force_rebuild : bool = true):
 | |
| 	display_folder(current_path, sorting_by, sorting_reverse, force_rebuild)
 | |
| 
 | |
| 
 | |
| func _load_resources_from_path(path : String, sort_by : String, sort_reverse : bool):
 | |
| 	if path.ends_with("/"):
 | |
| 		io = ResourceTablesEditFormatTres.new()
 | |
| 
 | |
| 	else:
 | |
| 		var loaded = load(path)
 | |
| 		if loaded is ResourceTablesImport:
 | |
| 			io = loaded.view_script.new()
 | |
| 
 | |
| 		else:
 | |
| 			io = ResourceTablesEditFormatTres.new()
 | |
| 	
 | |
| 	io.editor_view = self
 | |
| 	rows = io.import_from_path(path, insert_row_sorted, sort_by, sort_reverse)
 | |
| 
 | |
| 
 | |
| func fill_property_data(res):
 | |
| 	columns.clear()
 | |
| 	column_types.clear()
 | |
| 	column_hints.clear()
 | |
| 	column_hint_strings.clear()
 | |
| 	var column_values := []
 | |
| 	var i := -1
 | |
| 	for x in res.get_property_list():
 | |
| 		if x["usage"] & PROPERTY_USAGE_EDITOR != 0 and x["name"] != "script":
 | |
| 			i += 1
 | |
| 			columns.append(x["name"])
 | |
| 			column_types.append(x["type"])
 | |
| 			column_hints.append(x["hint"])
 | |
| 			column_hint_strings.append(x["hint_string"].split(","))
 | |
| 			column_values.append(io.get_value(res, columns[i]))
 | |
| 
 | |
| 	_selection.initialize_editors(column_values, column_types, column_hints)
 | |
| 
 | |
| 
 | |
| func insert_row_sorted(res : Resource, rows : Array, sort_by : String, sort_reverse : bool):
 | |
| 	if search_cond != null && !search_cond.can_show(res, rows.size()):
 | |
| 		return
 | |
| 		
 | |
| 	for i in rows.size():
 | |
| 		if sort_reverse == compare_values(io.get_value(res, sort_by), io.get_value(rows[i], sort_by)):
 | |
| 			rows.insert(i, res)
 | |
| 			return
 | |
| 	
 | |
| 	rows.append(res)
 | |
| 
 | |
| 
 | |
| func compare_values(a, b) -> bool:
 | |
| 	if a == null or b == null: return b == null
 | |
| 	if a is Color:
 | |
| 		return a.h > b.h if a.h != b.h else a.v > b.v
 | |
| 
 | |
| 	if a is Resource:
 | |
| 		return a.resource_path > b.resource_path
 | |
| 	
 | |
| 	if a is Array:
 | |
| 		return a.size() > b.size()
 | |
| 		
 | |
| 	return a > b
 | |
| 
 | |
| 
 | |
| func _set_sorting(sort_by):
 | |
| 	var sort_reverse : bool = !(sorting_by != sort_by or sorting_reverse)
 | |
| 	sorting_reverse = sort_reverse
 | |
| 	display_folder(current_path, sort_by, sort_reverse)
 | |
| 	sorting_by = sort_by
 | |
| 
 | |
| 
 | |
| func _update_table(columns_changed : bool):
 | |
| 	if columns_changed:
 | |
| 		node_table_root.columns = columns.size()
 | |
| 		for x in node_table_root.get_children():
 | |
| 			x.free()
 | |
| 
 | |
| 		node_columns.columns = columns
 | |
| 
 | |
| 	var to_free = node_table_root.get_child_count() - (last_row - first_row) * columns.size()
 | |
| 	while to_free > 0:
 | |
| 		node_table_root.get_child(0).free()
 | |
| 		to_free -= 1
 | |
| 	
 | |
| 	var color_rows = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "color_rows")
 | |
| 	
 | |
| 	_update_row_range(
 | |
| 		first_row,
 | |
| 		last_row,
 | |
| 		color_rows
 | |
| 	)
 | |
| 
 | |
| 
 | |
| func _update_row_range(first : int, last : int, color_rows : bool):
 | |
| 	for i in last - first:
 | |
| 		_update_row(first + i, color_rows)
 | |
| 
 | |
| 
 | |
| func _update_row(row_index : int, color_rows : bool = true):
 | |
| 	var current_node : Control
 | |
| 	var next_color := Color.WHITE
 | |
| 	var column_editors : Array = _selection.column_editors
 | |
| 	var res_path : String = rows[row_index].resource_path.get_basename().substr(current_path.length())
 | |
| 	for i in columns.size():
 | |
| 		if node_table_root.get_child_count() <= (row_index - first_row) * columns.size() + i:
 | |
| 			current_node = column_editors[i].create_cell(self)
 | |
| 			current_node.gui_input.connect(_on_cell_gui_input.bind(current_node))
 | |
| 			node_table_root.add_child(current_node)
 | |
| 
 | |
| 		else:
 | |
| 			current_node = node_table_root.get_child((row_index - first_row) * columns.size() + i)
 | |
| 			current_node.tooltip_text = (
 | |
| 				columns[i].capitalize()
 | |
| 				+ "\n---\n"
 | |
| 				+ "Of " + res_path
 | |
| 			)
 | |
| 		
 | |
| 		column_editors[i].set_value(current_node, io.get_value(rows[row_index], columns[i]))
 | |
| 		if columns[i] == "resource_path":
 | |
| 			column_editors[i].set_value(current_node, res_path)
 | |
| 
 | |
| 		if color_rows and column_types[i] == TYPE_COLOR:
 | |
| 			next_color = io.get_value(rows[row_index], columns[i])
 | |
| 
 | |
| 		column_editors[i].set_color(current_node, next_color)
 | |
| 
 | |
| 
 | |
| func save_data():
 | |
| 	var file = FileAccess.open(save_data_path, FileAccess.WRITE)
 | |
| 	file.store_string(JSON.stringify(
 | |
| 		{
 | |
| 			"recent_paths" : node_recent_paths.recent_paths,
 | |
| 			"hidden_columns" : node_columns.hidden_columns,
 | |
| 			"table_functions" : table_functions_dict,
 | |
| 		}
 | |
| 	, "  "))
 | |
| 
 | |
| 
 | |
| func _on_path_text_submitted(new_text : String = ""):
 | |
| 	if new_text != "":
 | |
| 		current_path = new_text
 | |
| 		display_folder(new_text, "", false, true)
 | |
| 
 | |
| 	else:
 | |
| 		refresh()
 | |
| 
 | |
| 
 | |
| func _on_FileDialog_dir_selected(dir : String):
 | |
| 	node_folder_path.text = dir
 | |
| 	display_folder(dir)
 | |
| 
 | |
| 
 | |
| func get_selected_column() -> int:
 | |
| 	return _selection.get_cell_column(_selection.edited_cells[0])
 | |
| 
 | |
| 
 | |
| func select_column(column_index : int):
 | |
| 	_selection.deselect_all_cells()
 | |
| 	_selection.select_cell(node_table_root.get_child(column_index))
 | |
| 	_selection.select_cells_to(node_table_root.get_child(
 | |
| 		column_index + columns.size()
 | |
| 		* (last_row - first_row - 1))
 | |
| 	)
 | |
| 
 | |
| 
 | |
| func set_edited_cells_values(new_cell_values : Array):
 | |
| 	var edited_rows = _selection.get_edited_rows()
 | |
| 	var column = _selection.get_cell_column(_selection.edited_cells[0])
 | |
| 	var edited_cells_resources = _get_row_resources(edited_rows)
 | |
| 
 | |
| 	# Duplicated here since if using text editing, edited_cells_text needs to modified
 | |
| 	# but here, it would be converted from a String breaking editing
 | |
| 	new_cell_values = new_cell_values.duplicate()
 | |
| 
 | |
| 	editor_plugin.undo_redo.create_action("Set Cell Values")
 | |
| 	editor_plugin.undo_redo.add_undo_method(
 | |
| 		self,
 | |
| 		&"_update_resources",
 | |
| 		edited_cells_resources.duplicate(),
 | |
| 		edited_rows.duplicate(),
 | |
| 		column,
 | |
| 		get_edited_cells_values()
 | |
| 	)
 | |
| 	editor_plugin.undo_redo.add_undo_method(
 | |
| 		_selection,
 | |
| 		&"_update_selected_cells_text"
 | |
| 	)
 | |
| 	editor_plugin.undo_redo.add_do_method(
 | |
| 		self,
 | |
| 		&"_update_resources",
 | |
| 		edited_cells_resources.duplicate(),
 | |
| 		edited_rows.duplicate(),
 | |
| 		column,
 | |
| 		new_cell_values.duplicate()
 | |
| 	)
 | |
| 	editor_plugin.undo_redo.commit_action(true)
 | |
| 	# editor_interface.get_resource_filesystem().scan()
 | |
| 
 | |
| 
 | |
| func rename_row(row, new_name):
 | |
| 	if !has_row_names(): return
 | |
| 		
 | |
| 	io.rename_row(row, new_name)
 | |
| 	refresh()
 | |
| 
 | |
| 
 | |
| func duplicate_selected_rows(new_name : String):
 | |
| 	io.duplicate_rows(_get_row_resources(_selection.get_edited_rows()), new_name)
 | |
| 	refresh()
 | |
| 
 | |
| 
 | |
| func delete_selected_rows():
 | |
| 	io.delete_rows(_get_row_resources(_selection.get_edited_rows()))
 | |
| 	refresh()
 | |
| 	refresh.call_deferred()
 | |
| 
 | |
| 
 | |
| func has_row_names():
 | |
| 	return io.has_row_names()
 | |
| 
 | |
| 
 | |
| func get_last_selected_row():
 | |
| 	return rows[_selection.get_cell_row(_selection.edited_cells[-1])]
 | |
| 
 | |
| 
 | |
| func get_edited_cells_values() -> Array:
 | |
| 	var cells : Array = _selection.edited_cells.duplicate()
 | |
| 	var column_index : int = _selection.get_cell_column(_selection.edited_cells[0])
 | |
| 	var cell_editor : Object = _selection.column_editors[column_index]
 | |
| 
 | |
| 	var result := []
 | |
| 	result.resize(cells.size())
 | |
| 	for i in cells.size():
 | |
| 		result[i] = io.get_value(rows[_selection.get_cell_row(cells[i])], columns[column_index])
 | |
| 	
 | |
| 	return result
 | |
| 
 | |
| 
 | |
| func _update_resources(update_rows : Array, update_row_indices : Array[int], update_column : int, values : Array):
 | |
| 	var column_editor = _selection.column_editors[update_column]
 | |
| 	for i in update_rows.size():
 | |
| 		var row = update_row_indices[i]
 | |
| 		var update_cell = node_table_root.get_child((row - first_row) * columns.size() + update_column)
 | |
| 
 | |
| 		column_editor.set_value(update_cell, values[i])
 | |
| 		if values[i] is String:
 | |
| 			values[i] = try_convert(values[i], column_types[update_column])
 | |
| 
 | |
| 		if values[i] == null:
 | |
| 			continue
 | |
| 		
 | |
| 		io.set_value(
 | |
| 			update_rows[i],
 | |
| 			columns[update_column],
 | |
| 			values[i],
 | |
| 			row
 | |
| 		)
 | |
| 		continue
 | |
| 		if column_types[update_column] == TYPE_COLOR:
 | |
| 			for j in columns.size() - update_column:
 | |
| 				if j != 0 and column_types[j + update_column] == TYPE_COLOR:
 | |
| 					break
 | |
| 
 | |
| 				_selection.column_editors[j + update_column].set_color(
 | |
| 					update_cell.get_parent().get_child(
 | |
| 						row * columns.size() + update_column + j - first_row
 | |
| 					),
 | |
| 					values[i]
 | |
| 				)
 | |
| 
 | |
| 	node_columns._update_column_sizes()
 | |
| 	io.save_entries(rows, update_row_indices)
 | |
| 
 | |
| 
 | |
| func try_convert(value, type):
 | |
| 	if type == TYPE_BOOL:
 | |
| 		# "off" displayed in lowercase, "ON" in uppercase.
 | |
| 		return value[0] == "o"
 | |
| 
 | |
| 	# If it can't convert, throws exception and returns null.
 | |
| 	return convert(value, type)
 | |
| 
 | |
| 
 | |
| func _get_row_resources(row_indices) -> Array:
 | |
| 	var arr := []
 | |
| 	arr.resize(row_indices.size())
 | |
| 	for i in arr.size():
 | |
| 		arr[i] = rows[row_indices[i]]
 | |
| 
 | |
| 	return arr
 | |
| 
 | |
| 
 | |
| func _on_File_pressed():
 | |
| 	node_folder_path.get_parent().get_parent().visible = !node_folder_path.get_parent().get_parent().visible
 | |
| 
 | |
| 
 | |
| func _on_SearchProcess_pressed():
 | |
| 	$"HeaderContentSplit/VBoxContainer/Search".visible = !$"HeaderContentSplit/VBoxContainer/Search".visible
 |