Added resource table plugin
All checks were successful
Create tag and build when new code gets to main / BumpTag (push) Successful in 8s
Create tag and build when new code gets to main / Export (push) Successful in 1m13s

This commit is contained in:
2026-01-31 10:23:20 +01:00
parent cd150a4513
commit 158e18f1fe
182 changed files with 9266 additions and 1 deletions

View File

@@ -0,0 +1,36 @@
class_name ResourceTablesEditFormat
extends RefCounted
var editor_view : Control
## Override to define reading behaviour.
func get_value(entry, key : String):
pass
## Override to define writing behaviour. This is NOT supposed to save - use `save_entries`.
func set_value(entry, key : String, value, index : int):
pass
## Override to define how the data gets saved.
func save_entries(all_entries : Array, indices : Array):
pass
## Override to allow editing rows from the Inspector.
func create_resource(entry) -> Resource:
return Resource.new()
## Override to define duplication behaviour. `name_input` should be a suffix if multiple entries, and full name if one.
func duplicate_rows(rows : Array, name_input : String):
pass
## Override to define removal behaviour.
func delete_rows(rows : Array):
pass
## Override with `return true` if `resource_path` is defined and the Rename butoon should show.
func has_row_names():
return false
## Override to define import behaviour. Must return the `rows` value for the editor view.
func import_from_path(folderpath : String, insert_func : Callable, sort_by : String, sort_reverse : bool = false) -> Array:
return []

View File

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

View File

@@ -0,0 +1,88 @@
class_name ResourceTablesEditFormatCsv
extends ResourceTablesEditFormatTres
var import_data : ResourceTablesImport
var csv_rows := []
var resource_original_positions := {}
func get_value(entry, key : String):
return entry.get(key)
func set_value(entry, key : String, value, index : int):
entry.set(key, value)
csv_rows[resource_original_positions[entry]] = import_data.resource_to_strings(entry)
func save_entries(all_entries : Array, indices : Array, repeat : bool = true):
if timer == null or timer.time_left <= 0.0:
var space_after_delimiter := import_data.delimeter.ends_with(" ")
var file := FileAccess.open(import_data.edited_path, FileAccess.WRITE)
for x in csv_rows:
if space_after_delimiter:
for i in x.size():
if i == 0: continue
x[i] = " " + x[i]
file.store_csv_line(x, import_data.delimeter[0])
if repeat:
timer = editor_view.get_tree().create_timer(3.0)
timer.timeout.connect(save_entries.bind(all_entries, indices, false))
func create_resource(entry) -> Resource:
return entry
func duplicate_rows(rows : Array, name_input : String):
for x in rows:
var new_res = x.duplicate()
var index : int = resource_original_positions[x]
csv_rows.insert(index, import_data.resource_to_strings(new_res))
_bump_row_indices(index + 1, 1)
resource_original_positions[new_res] = index + 1
save_entries([], [])
func delete_rows(rows):
for x in rows:
var index : int = resource_original_positions[x]
csv_rows.remove_at(index)
_bump_row_indices(index, -1)
resource_original_positions.erase(x)
save_entries([], [])
func has_row_names():
return false
func _bump_row_indices(from : int, increment : int = 1):
for k in resource_original_positions:
if resource_original_positions[k] >= from:
resource_original_positions[k] += increment
func import_from_path(path : String, insert_func : Callable, sort_by : String, sort_reverse : bool = false) -> Array:
import_data = load(path)
var file := FileAccess.open(import_data.edited_path, FileAccess.READ)
csv_rows = ResourceTablesImportFormatCsv.import_as_arrays(import_data)
var rows := []
var res : Resource
resource_original_positions.clear()
for i in csv_rows.size():
if import_data.remove_first_row and i == 0:
continue
res = import_data.strings_to_resource(csv_rows[i], "")
res.resource_path = ""
insert_func.call(res, rows, sort_by, sort_reverse)
resource_original_positions[res] = i
editor_view.fill_property_data(rows[0])
return rows

View File

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

View File

@@ -0,0 +1,148 @@
class_name ResourceTablesEditFormatTres
extends ResourceTablesEditFormat
var timer : SceneTreeTimer
func get_value(entry, key : String):
return entry[key]
func set_value(entry, key : String, value, index : int):
var prev_value = entry[key]
if prev_value is StringName:
entry[key] = StringName(value)
return
if prev_value is String:
entry[key] = String(value)
return
if prev_value is float:
entry[key] = float(value)
return
if prev_value is int:
entry[key] = int(value)
return
entry[key] = value
func save_entries(all_entries : Array, indices : Array, repeat : bool = true):
# No need to save. Resources are saved with Ctrl+S
# (likely because plugin.edit_resource is called to show inspector)
return
func create_resource(entry) -> Resource:
return entry
func duplicate_rows(rows : Array, name_input : String):
var new_path := ""
if rows.size() == 1:
var new_row = rows[0].duplicate(true)
var res_extension := ".res" if rows[0].resource_path.ends_with(".res") else ".tres"
new_path = rows[0].resource_path.get_base_dir() + "/" + name_input + res_extension
while ResourceLoader.exists(new_path):
new_path = new_path.trim_suffix(res_extension) + "_copy" + res_extension
new_row.resource_path = new_path
ResourceSaver.save(new_row)
return
var new_row
for x in rows:
new_row = x.duplicate(true)
var res_extension := ".res" if x.resource_path.ends_with(".res") else ".tres"
new_path = x.resource_path.get_basename() + name_input + res_extension
while ResourceLoader.exists(new_path):
new_path = new_path.trim_suffix(res_extension) + "_copy" + res_extension
new_row.resource_path = new_path
ResourceSaver.save(new_row)
func rename_row(row, new_name : String):
var res_extension : String = ".res" if row.resource_path.ends_with(".res") else ".tres"
var new_path : String = row.resource_path.get_base_dir() + "/" + new_name + res_extension
while FileAccess.file_exists(new_path):
new_path = new_path.trim_suffix(res_extension) + "_copy" + res_extension
var new_row = row
DirAccess.open("res://").remove(row.resource_path)
new_row.resource_path = new_path
ResourceSaver.save(new_row)
func delete_rows(rows):
for x in rows:
DirAccess.open("res://").remove(x.resource_path)
func has_row_names():
return true
func import_from_path(folderpath : String, insert_func : Callable, sort_by : String, sort_reverse : bool = false) -> Array:
var solo_property := ""
var solo_property_split : Array[String] = []
if folderpath.contains("::"):
var found_at := folderpath.find("::")
solo_property = folderpath.substr(found_at + "::".length()).trim_suffix("/")
folderpath = folderpath.left(found_at)
for x in solo_property.split("::"):
solo_property_split.append(x)
var rows := []
var dir := DirAccess.open(folderpath)
if dir == null: return []
var file_stack : Array[String] = []
var folder_stack : Array[String] = [folderpath]
while folder_stack.size() > 0:
folderpath = folder_stack.pop_back()
for x in DirAccess.get_files_at(folderpath):
file_stack.append(folderpath.path_join(x))
for x in DirAccess.get_directories_at(folderpath):
folder_stack.append(folderpath.path_join(x))
var loaded_res_unique := {}
for x in file_stack:
if !x.ends_with(".tres") and !x.ends_with(".res"):
continue
if solo_property.is_empty():
loaded_res_unique[load(x)] = true
else:
_append_soloed_property(load(x), loaded_res_unique, solo_property_split)
for x in loaded_res_unique.keys():
if x == null: continue
insert_func.call(x, rows, sort_by, sort_reverse)
editor_view.fill_property_data_many(loaded_res_unique.keys())
return rows
func _append_soloed_property(current_res : Resource, result : Dictionary, solo_property_split : Array[String], solo_property_split_idx : int = -solo_property_split.size()):
var soloed_value = current_res[solo_property_split[solo_property_split_idx]]
if solo_property_split_idx == -1:
if soloed_value is Resource:
result[soloed_value] = true
elif soloed_value is Array:
for x in soloed_value:
result[x] = true
else:
if soloed_value is Resource:
_append_soloed_property(soloed_value, result, solo_property_split, solo_property_split_idx + 1)
elif soloed_value is Array:
for x in soloed_value:
_append_soloed_property(x, result, solo_property_split, solo_property_split_idx + 1)

View File

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

View File

@@ -0,0 +1,34 @@
class_name ResourceTablesExportFormatCsv
extends RefCounted
static func can_edit_path(path : String):
return path.ends_with(".csv")
static func export_to_file(entries_array : Array, column_names : Array, into_path : String, import_data : ResourceTablesImport):
var file := FileAccess.open(into_path, FileAccess.WRITE)
var line := PackedStringArray()
var uniques := {}
var space_after_delimiter := import_data.delimeter.ends_with(" ")
import_data.prop_names = column_names
import_data.prop_types = import_data.get_resource_property_types(entries_array[0], column_names, uniques)
import_data.uniques = uniques
import_data.resource_path = ""
line.resize(column_names.size())
if import_data.remove_first_row:
for j in column_names.size():
line[j] = String(column_names[j])
if space_after_delimiter and j != 0:
line[j] = " " + line[j]
file.store_csv_line(line, import_data.delimeter[0])
for i in entries_array.size():
for j in column_names.size():
line[j] = import_data.property_to_string((entries_array[i].get(column_names[j])), j)
if space_after_delimiter and j != 0:
line[j] = " " + line[j]
file.store_csv_line(line, import_data.delimeter[0])

View File

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

View File

@@ -0,0 +1,57 @@
class_name ResourceTablesImportFormatCsv
extends RefCounted
static func can_edit_path(path : String):
return path.ends_with(".csv")
static func get_properties(entries, import_data):
return Array(entries[0])
static func import_as_arrays(import_data) -> Array:
var file := FileAccess.open(import_data.edited_path, FileAccess.READ)
import_data.delimeter = ";"
var text_lines : Array[PackedStringArray] = [file.get_line().split(import_data.delimeter)]
var space_after_delimeter := false
var line := text_lines[0]
if line.size() == 0:
return []
if line.size() == 1:
import_data.delimeter = ","
line = line[0].split(import_data.delimeter)
text_lines[0] = line
if line.size() <= 1:
return []
if line[1].begins_with(" "):
for i in line.size():
line[i] = line[i].trim_prefix(" ")
text_lines[0] = line
space_after_delimeter = true
import_data.delimeter += " "
while !file.eof_reached():
line = file.get_csv_line(import_data.delimeter[0])
if space_after_delimeter:
for i in line.size():
line[i] = line[i].trim_prefix(" ")
if line.size() == text_lines[0].size():
text_lines.append(line)
elif line.size() != 1:
line.resize(text_lines[0].size())
text_lines.append(line)
var entries := []
entries.resize(text_lines.size())
for i in entries.size():
entries[i] = text_lines[i]
return entries

View File

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

View File

@@ -0,0 +1,245 @@
@tool
extends Control
@export var prop_list_item_scene : PackedScene
@export var formats_export : Array[Script]
@export var formats_import : Array[Script]
@onready var editor_view := $"../../.."
@onready var filename_options := $"Import/Margins/Scroll/Box/Grid/UseAsFilename"
@onready var classname_field := $"Import/Margins/Scroll/Box/Grid/Classname"
@onready var script_path_field := $"Import/Margins/Scroll/Box/Grid/HBoxContainer/LineEdit"
@onready var prop_list := $"Import/Margins/Scroll/Box"
@onready var format_settings := $"Import/Margins/Scroll/Box/StyleSettingsI"
@onready var file_dialog_use_script: FileDialog = $"Import/Margins/Scroll/Box/Grid/HBoxContainer/FileDialog"
var format_extension := ".csv"
var entries := []
var import_data : ResourceTablesImport
func _ready():
hide()
show()
if get_parent().get("size"):
get_parent().size = Vector2(600, 400)
file_dialog_use_script.file_selected.connect(_on_file_dialog_file_selected)
func _on_file_selected(path : String):
if !FileAccess.file_exists(path):
if path.get_extension() != "":
# Path is a file path: replace extension
path = path.get_basename() + ".csv"
else:
# Path is a directory: add filename, add extension
path = path.path_join(editor_view.current_path.trim_suffix("/").get_file()) + ".csv"
FileAccess.open(path, FileAccess.WRITE)
import_data = null
for x in DirAccess.get_files_at(path.get_base_dir()):
if !x.ends_with(".tres") and !x.ends_with(".res"):
continue
var found_res := load(path.get_base_dir().path_join(x))
if !(found_res is ResourceTablesImport):
continue
_import_settings_from_settings_file(found_res, path)
break
if import_data == null:
_create_new_settings_file(path)
_create_prop_editors()
$"Import/Margins/Scroll/Box/StyleSettingsI"._send_signal()
if editor_view.rows.size() > 0:
var using_script = editor_view.rows[0].get_script()
if using_script != null:
script_path_field.text = using_script.resource_path
await get_tree().process_frame
get_parent().popup_centered()
await get_tree().process_frame
get_parent().min_size = get_combined_minimum_size()
position = Vector2.ZERO
size = get_parent().size
func _on_files_selected(paths : PackedStringArray):
_on_file_selected(paths[0])
func _import_settings_from_settings_file(settings_file : ResourceTablesImport, textfile_path : String):
import_data = settings_file
filename_options.clear()
for i in import_data.prop_names.size():
filename_options.add_item(import_data.prop_names[i], i)
if import_data.new_script != null:
classname_field.text = import_data.new_script.get_global_name()
script_path_field.text = settings_file.new_script.resource_path
format_settings.set_format_array(import_data.enum_format)
for format_x in formats_import:
var new_importer = format_x.new()
if new_importer.can_edit_path(textfile_path):
entries = format_x.new().import_as_arrays(import_data)
break
func _create_new_settings_file(textfile_path : String):
import_data = ResourceTablesImport.new()
import_data.initialize(textfile_path)
for format_x in formats_import:
var new_importer = format_x.new()
if new_importer.can_edit_path(textfile_path):
entries = new_importer.import_as_arrays(import_data)
import_data.prop_names = new_importer.get_properties(entries, import_data)
break
classname_field.text = import_data.edited_path.get_file().get_basename()\
.capitalize().replace(" ", "")
import_data.script_classname = classname_field.text
if script_path_field.text:
var existing_resource : Resource = load(script_path_field.text).new()
var uniques := {}
import_data.prop_types = ResourceTablesImport.get_resource_property_types(existing_resource, import_data.prop_names, uniques)
import_data.uniques = uniques
else:
import_data.load_property_names_from_textfile(textfile_path, entries)
filename_options.clear()
for i in import_data.prop_names.size():
filename_options.add_item(import_data.prop_names[i], i)
func _create_prop_editors():
for x in prop_list.get_children():
if !x is GridContainer: x.free()
await get_tree().process_frame
for i in import_data.prop_names.size():
var new_node := prop_list_item_scene.instantiate()
prop_list.add_child(new_node)
var prop_type = import_data.prop_types[i]
new_node.display(import_data.prop_names[i], prop_type if !(prop_type is PackedStringArray) else ResourceTablesImport.PropType.ENUM)
new_node.connect_all_signals(self, i)
func _generate_class(save_script = true):
save_script = true # Built-ins didn't work in 3.x, won't change because dont wanna test rn
import_data.new_script = import_data.generate_script(entries, save_script)
if save_script:
import_data.new_script.resource_path = import_data.edited_path.get_basename() + ".gd"
ResourceSaver.save(import_data.new_script)
# Because when instanced, objects have a copy of the script
import_data.new_script = load(import_data.edited_path.get_basename() + ".gd")
func _on_import_to_tres_pressed():
if script_path_field.text != "":
import_data.load_external_script(load(script_path_field.text))
if import_data.new_script == null:
_generate_class()
DirAccess.open("res://").make_dir_recursive(import_data.edited_path.get_basename())
import_data.prop_used_as_filename = import_data.prop_names[filename_options.selected]
var new_res : Resource
for i in entries.size():
if import_data.remove_first_row and i == 0:
continue
new_res = import_data.strings_to_resource(entries[i], editor_view.current_path)
ResourceSaver.save(new_res)
await get_tree().process_frame
await get_tree().process_frame
editor_view.refresh()
close()
func _on_import_edit_pressed():
if import_data.new_script == null:
_generate_class(false)
import_data.prop_used_as_filename = ""
import_data.save()
await get_tree().process_frame
editor_view.display_folder(import_data.resource_path)
editor_view.node_columns.hidden_columns[editor_view.current_path] = {
"resource_path" : true,
"resource_local_to_scene" : true,
}
editor_view.save_data()
await get_tree().process_frame
editor_view.refresh()
close()
func _on_export_csv_pressed():
var exported_cols : Array = editor_view.columns.duplicate()
exported_cols.erase(&"resource_local_to_scene")
var column_properties : Dictionary = editor_view.node_columns.column_properties[editor_view.current_path]
for k in column_properties:
if column_properties[k].get(&"visibility", 1.0) == 0.0:
exported_cols.erase(k)
ResourceTablesExportFormatCsv.export_to_file(editor_view.rows, exported_cols, import_data.edited_path, import_data)
await get_tree().process_frame
editor_view.refresh()
close()
# Input controls
func _on_classname_field_text_changed(new_text : String):
import_data.script_classname = new_text.replace(" ", "")
func _on_remove_first_row_toggled(button_pressed : bool):
import_data.remove_first_row = button_pressed
# $"Export/Box2/Button".button_pressed = true
$"Export/Box3/CheckBox".button_pressed = button_pressed
func _on_list_item_type_selected(type : int, index : int):
import_data.prop_types[index] = type
func _on_list_item_name_changed(name : String, index : int):
import_data.prop_names[index] = name.replace(" ", "")
func _on_export_delimiter_pressed(del : String):
import_data.delimeter = del + import_data.delimeter.substr(1)
func _on_export_space_toggled(button_pressed : bool):
import_data.delimeter = (
import_data.delimeter[0]
if !button_pressed else
import_data.delimeter + " "
)
func _on_enum_format_changed(case, delimiter, bool_yes, bool_no):
import_data.enum_format = [case, delimiter, bool_yes, bool_no]
func close():
get_parent().hide()
# Handles reloading the import hint editor if a Resource script is chosen
func _on_file_dialog_file_selected(path: String) -> void:
script_path_field.text = path
_on_file_selected(import_data.edited_path)

View File

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

View File

@@ -0,0 +1,240 @@
[gd_scene load_steps=8 format=3 uid="uid://b413igx28kkvb"]
[ext_resource type="Script" uid="uid://b1uc8bum4bi01" path="res://addons/resources_spreadsheet_view/import_export/import_export_dialog.gd" id="1"]
[ext_resource type="Script" uid="uid://bpf5v6r8mrgpy" path="res://addons/resources_spreadsheet_view/import_export/formats_export/export_csv.gd" id="2_33c6s"]
[ext_resource type="Script" uid="uid://cmsa4mdn3o5rr" path="res://addons/resources_spreadsheet_view/import_export/formats_import/import_csv.gd" id="2_fxayt"]
[ext_resource type="PackedScene" uid="uid://b8llymigbprh6" path="res://addons/resources_spreadsheet_view/import_export/property_list_item.tscn" id="2_xfhmf"]
[ext_resource type="PackedScene" uid="uid://ckhf3bqy2rqjr" path="res://addons/resources_spreadsheet_view/import_export/import_export_enum_format.tscn" id="4"]
[ext_resource type="Script" uid="uid://dyxdmrhopt5i0" path="res://addons/resources_spreadsheet_view/editor_icon_button.gd" id="5_k5rhi"]
[sub_resource type="ButtonGroup" id="ButtonGroup_080hd"]
[node name="TabContainer" type="TabContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
current_tab = 1
script = ExtResource("1")
prop_list_item_scene = ExtResource("2_xfhmf")
formats_export = Array[Script]([ExtResource("2_33c6s")])
formats_import = Array[Script]([ExtResource("2_fxayt")])
[node name="Import" type="VBoxContainer" parent="."]
visible = false
layout_mode = 2
mouse_filter = 2
metadata/_tab_index = 0
[node name="Margins" type="MarginContainer" parent="Import"]
layout_mode = 2
size_flags_vertical = 3
[node name="Scroll" type="ScrollContainer" parent="Import/Margins"]
layout_mode = 2
horizontal_scroll_mode = 0
[node name="Box" type="VBoxContainer" parent="Import/Margins/Scroll"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Grid" type="GridContainer" parent="Import/Margins/Scroll/Box"]
layout_mode = 2
columns = 2
[node name="Label" type="Label" parent="Import/Margins/Scroll/Box/Grid"]
layout_mode = 2
text = "Column Used as Filename:"
[node name="UseAsFilename" type="OptionButton" parent="Import/Margins/Scroll/Box/Grid"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label4" type="Label" parent="Import/Margins/Scroll/Box/Grid"]
layout_mode = 2
text = "Use Script:"
[node name="HBoxContainer" type="HBoxContainer" parent="Import/Margins/Scroll/Box/Grid"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="Import/Margins/Scroll/Box/Grid/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "(create new GDScript)"
editable = false
[node name="Button2" type="Button" parent="Import/Margins/Scroll/Box/Grid/HBoxContainer"]
layout_mode = 2
script = ExtResource("5_k5rhi")
icon_name = "Clear"
[node name="Button" type="Button" parent="Import/Margins/Scroll/Box/Grid/HBoxContainer"]
layout_mode = 2
script = ExtResource("5_k5rhi")
icon_name = "Folder"
[node name="FileDialog" type="FileDialog" parent="Import/Margins/Scroll/Box/Grid/HBoxContainer"]
title = "Choose Script..."
initial_position = 4
size = Vector2i(800, 500)
ok_button_text = "Open"
mode_overrides_title = false
file_mode = 0
filters = PackedStringArray("*.gd", "*.cs")
[node name="Control" type="Control" parent="Import/Margins/Scroll/Box/Grid"]
layout_mode = 2
[node name="Label5" type="Label" parent="Import/Margins/Scroll/Box/Grid"]
self_modulate = Color(1, 0.84375, 0, 1)
layout_mode = 2
text = "WARNING: Importing with a pre-existing Script will not import datatypes not selectible below."
horizontal_alignment = 1
autowrap_mode = 3
[node name="Label2" type="Label" parent="Import/Margins/Scroll/Box/Grid"]
visible = false
layout_mode = 2
text = "Class Name"
[node name="Classname" type="LineEdit" parent="Import/Margins/Scroll/Box/Grid"]
visible = false
layout_mode = 2
caret_blink = true
caret_blink_interval = 0.5
[node name="Label3" type="Label" parent="Import/Margins/Scroll/Box/Grid"]
layout_mode = 2
text = "Flags:"
[node name="Flags" type="VBoxContainer" parent="Import/Margins/Scroll/Box/Grid"]
layout_mode = 2
[node name="RemoveFirstRow" type="CheckBox" parent="Import/Margins/Scroll/Box/Grid/Flags"]
layout_mode = 2
text = "First row contains property names"
[node name="HSeparator" type="HSeparator" parent="Import/Margins/Scroll/Box"]
layout_mode = 2
[node name="StyleSettingsI" parent="Import/Margins/Scroll/Box" instance=ExtResource("4")]
layout_mode = 2
[node name="Box" type="HBoxContainer" parent="Import"]
layout_mode = 2
mouse_filter = 2
alignment = 1
[node name="Ok2" type="Button" parent="Import/Box"]
layout_mode = 2
text = "Confirm and edit"
[node name="Ok" type="Button" parent="Import/Box"]
layout_mode = 2
text = "Convert to Resources and edit"
[node name="Cancel" type="Button" parent="Import/Box"]
layout_mode = 2
text = "Cancel"
[node name="Control" type="Control" parent="Import"]
layout_mode = 2
mouse_filter = 2
[node name="Export" type="VBoxContainer" parent="."]
layout_mode = 2
metadata/_tab_index = 1
[node name="Info" type="Label" parent="Export"]
layout_mode = 2
size_flags_vertical = 0
text = "The currently edited folder will be exported into the selected file.
Rows hidden by the filter will NOT be exported, and order follows the current sorting key. Rows on non-selected pages will not be removed.
Hidden columns will NOT be exported."
autowrap_mode = 2
[node name="HSeparator" type="HSeparator" parent="Export"]
layout_mode = 2
[node name="Box" type="HBoxContainer" parent="Export"]
layout_mode = 2
alignment = 1
[node name="Label2" type="Label" parent="Export/Box"]
layout_mode = 2
size_flags_horizontal = 3
text = "Delimiter:"
[node name="Button" type="Button" parent="Export/Box"]
layout_mode = 2
toggle_mode = true
button_pressed = true
button_group = SubResource("ButtonGroup_080hd")
text = "Comma (,)"
[node name="Button2" type="Button" parent="Export/Box"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_080hd")
text = "Semicolon (;)"
[node name="Button3" type="Button" parent="Export/Box"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_080hd")
text = "Tab"
[node name="CheckBox" type="CheckBox" parent="Export/Box"]
layout_mode = 2
text = "With space after"
[node name="Box3" type="HBoxContainer" parent="Export"]
layout_mode = 2
[node name="CheckBox" type="CheckBox" parent="Export/Box3"]
layout_mode = 2
text = "First row contains property names (CSV)"
[node name="StyleSettingsE" parent="Export" instance=ExtResource("4")]
layout_mode = 2
[node name="Control" type="Control" parent="Export"]
layout_mode = 2
size_flags_vertical = 3
[node name="Box2" type="HBoxContainer" parent="Export"]
layout_mode = 2
alignment = 1
[node name="Button" type="Button" parent="Export/Box2"]
layout_mode = 2
text = "Export to CSV"
[node name="Cancel" type="Button" parent="Export/Box2"]
layout_mode = 2
text = "Cancel"
[node name="Control2" type="Control" parent="Export"]
layout_mode = 2
[connection signal="pressed" from="Import/Margins/Scroll/Box/Grid/HBoxContainer/Button2" to="Import/Margins/Scroll/Box/Grid/HBoxContainer/LineEdit" method="set_text" binds= [""]]
[connection signal="pressed" from="Import/Margins/Scroll/Box/Grid/HBoxContainer/Button" to="Import/Margins/Scroll/Box/Grid/HBoxContainer/FileDialog" method="popup_centered"]
[connection signal="file_selected" from="Import/Margins/Scroll/Box/Grid/HBoxContainer/FileDialog" to="Import/Margins/Scroll/Box/Grid/HBoxContainer/LineEdit" method="set_text"]
[connection signal="text_changed" from="Import/Margins/Scroll/Box/Grid/Classname" to="." method="_on_classname_field_text_changed"]
[connection signal="toggled" from="Import/Margins/Scroll/Box/Grid/Flags/RemoveFirstRow" to="." method="_on_remove_first_row_toggled"]
[connection signal="format_changed" from="Import/Margins/Scroll/Box/StyleSettingsI" to="." method="_on_enum_format_changed"]
[connection signal="format_changed" from="Import/Margins/Scroll/Box/StyleSettingsI" to="Export/StyleSettingsE" method="_on_format_changed"]
[connection signal="pressed" from="Import/Box/Ok2" to="." method="_on_import_edit_pressed"]
[connection signal="pressed" from="Import/Box/Ok" to="." method="_on_import_to_tres_pressed"]
[connection signal="pressed" from="Import/Box/Cancel" to="." method="close"]
[connection signal="pressed" from="Export/Box/Button" to="." method="_on_export_delimiter_pressed" binds= [","]]
[connection signal="pressed" from="Export/Box/Button2" to="." method="_on_export_delimiter_pressed" binds= [";"]]
[connection signal="pressed" from="Export/Box/Button3" to="." method="_on_export_delimiter_pressed" binds= [" "]]
[connection signal="toggled" from="Export/Box/CheckBox" to="." method="_on_export_space_toggled"]
[connection signal="toggled" from="Export/Box3/CheckBox" to="." method="_on_remove_first_row_toggled"]
[connection signal="format_changed" from="Export/StyleSettingsE" to="." method="_on_enum_format_changed"]
[connection signal="pressed" from="Export/Box2/Button" to="." method="_on_export_csv_pressed"]
[connection signal="pressed" from="Export/Box2/Cancel" to="." method="close"]

View File

@@ -0,0 +1,30 @@
@tool
extends GridContainer
signal format_changed(case : int, delimiter : String, bool_yes : String, bool_no: String)
func set_format_array(format : Array):
_on_format_changed(format[0], format[1], format[2], format[3])
format_changed.emit(format[0], format[1], format[2], format[3])
func set_format(case : int, delimiter : String, bool_yes : String, bool_no: String):
_on_format_changed(case, delimiter, bool_yes, bool_no)
format_changed.emit(case, delimiter, bool_yes, bool_no)
func _send_signal(arg1 = null):
format_changed.emit(
$"HBoxContainer/Case".selected,
[" ", "_", "-"][$"HBoxContainer/Separator".selected],
$"HBoxContainer2/True".text,
$"HBoxContainer2/False".text
)
func _on_format_changed(case : int, delimiter : String, bool_yes : String, bool_no: String):
$"HBoxContainer/Case".selected = case
$"HBoxContainer/Separator".selected = [" ", "_", "-"].find(delimiter)
$"HBoxContainer2/True".text = bool_yes
$"HBoxContainer2/False".text = bool_no

View File

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

View File

@@ -0,0 +1,68 @@
[gd_scene load_steps=2 format=3 uid="uid://ckhf3bqy2rqjr"]
[ext_resource type="Script" uid="uid://cmbs7j2rx0xa3" path="res://addons/resources_spreadsheet_view/import_export/import_export_enum_format.gd" id="1"]
[node name="EnumFormat" type="GridContainer"]
columns = 2
script = ExtResource("1")
[node name="Label3" type="Label" parent="."]
layout_mode = 2
size_flags_horizontal = 3
text = "Enum word case/separator"
[node name="HBoxContainer" type="HBoxContainer" parent="."]
layout_mode = 2
size_flags_horizontal = 3
[node name="Case" type="OptionButton" parent="HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
clip_text = true
selected = 2
item_count = 4
popup/item_0/text = "all lower"
popup/item_1/text = "caps Except First"
popup/item_1/id = 1
popup/item_2/text = "Caps Every Word"
popup/item_2/id = 2
popup/item_3/text = "ALL CAPS"
popup/item_3/id = 3
[node name="Separator" type="OptionButton" parent="HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 0.5
clip_text = true
selected = 0
item_count = 3
popup/item_0/text = "Space \" \""
popup/item_1/text = "Underscore \"_\""
popup/item_1/id = 1
popup/item_2/text = "Kebab \"-\""
popup/item_2/id = 2
[node name="Label4" type="Label" parent="."]
layout_mode = 2
size_flags_horizontal = 3
text = "Boolean True/False"
[node name="HBoxContainer2" type="HBoxContainer" parent="."]
layout_mode = 2
size_flags_horizontal = 3
[node name="True" type="LineEdit" parent="HBoxContainer2"]
layout_mode = 2
size_flags_horizontal = 3
text = "Yes"
[node name="False" type="LineEdit" parent="HBoxContainer2"]
layout_mode = 2
size_flags_horizontal = 3
text = "No"
[connection signal="mouse_entered" from="Label3" to="." method="_on_Label3_mouse_entered"]
[connection signal="item_selected" from="HBoxContainer/Case" to="." method="_send_signal"]
[connection signal="item_selected" from="HBoxContainer/Separator" to="." method="_send_signal"]
[connection signal="text_changed" from="HBoxContainer2/True" to="." method="_send_signal"]
[connection signal="text_changed" from="HBoxContainer2/False" to="." method="_send_signal"]

View File

@@ -0,0 +1,12 @@
@tool
extends HBoxContainer
func display(name : String, type : int):
$"LineEdit".text = name
$"OptionButton".selected = type
func connect_all_signals(to : Object, index : int, prefix : String = "_on_list_item_"):
$"LineEdit".text_changed.connect(Callable(to, prefix + "name_changed").bind(index))
$"OptionButton".item_selected.connect(Callable(to, prefix + "type_selected").bind(index))

View File

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

View File

@@ -0,0 +1,36 @@
[gd_scene load_steps=2 format=3 uid="uid://b8llymigbprh6"]
[ext_resource type="Script" uid="uid://c0j7uonbiyaox" path="res://addons/resources_spreadsheet_view/import_export/property_list_item.gd" id="1"]
[node name="Entry" type="HBoxContainer"]
script = ExtResource("1")
[node name="LineEdit" type="LineEdit" parent="."]
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 0.5
text = "1"
[node name="OptionButton" type="OptionButton" parent="."]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 5
size_flags_stretch_ratio = 0.25
fit_to_longest_item = false
item_count = 8
popup/item_0/text = "Bool"
popup/item_0/id = 1
popup/item_1/text = "Integer Number"
popup/item_1/id = 2
popup/item_2/text = "Floating Point Number"
popup/item_2/id = 3
popup/item_3/text = "String/Other"
popup/item_3/id = 4
popup/item_4/text = "Color"
popup/item_4/id = 14
popup/item_5/text = "Resource Path"
popup/item_5/id = 17
popup/item_6/text = "Enumeration"
popup/item_6/id = 101
popup/item_7/text = "Array"
popup/item_7/id = 102

View File

@@ -0,0 +1,446 @@
@tool
class_name ResourceTablesImport
extends Resource
enum PropType {
BOOL,
INT,
FLOAT,
STRING,
COLOR,
OBJECT,
ENUM,
COLLECTION,
MAX,
}
enum NameCasing {
ALL_LOWER,
CAPS_WORD_EXCEPT_FIRST,
CAPS_WORD,
ALL_CAPS,
}
const SUFFIX := "_table_import.tres"
const TYPE_MAP := {
TYPE_STRING : PropType.STRING,
TYPE_FLOAT : PropType.FLOAT,
TYPE_BOOL : PropType.BOOL,
TYPE_INT : PropType.INT,
TYPE_OBJECT : PropType.OBJECT,
TYPE_COLOR : PropType.COLOR,
TYPE_ARRAY : PropType.COLLECTION,
TYPE_DICTIONARY : PropType.COLLECTION,
}
@export var prop_types : Array
@export var prop_names : Array
@export var edited_path := "res://"
@export var prop_used_as_filename := ""
@export var script_classname := ""
@export var remove_first_row := true
@export var new_script : Script
@export var view_script : Script = ResourceTablesEditFormatCsv
@export var delimeter := ";"
@export var enum_format : Array = [NameCasing.CAPS_WORD, " ", "Yes", "No"]
@export var uniques : Dictionary
func initialize(path):
edited_path = path
prop_types = []
prop_names = []
func save():
resource_path = edited_path.get_basename() + SUFFIX
ResourceSaver.save.call_deferred(self)
func string_to_property(string : String, col_index : int):
match prop_types[col_index]:
PropType.STRING:
return string
PropType.BOOL:
string = string.to_lower()
if string == enum_format[2].to_lower(): return true
if string == enum_format[3].to_lower(): return false
return !string in ["no", "disabled", "-", "false", "absent", "wrong", "off", "0", ""]
PropType.FLOAT:
return string.to_float()
PropType.INT:
return string.to_int()
PropType.COLOR:
return Color(string)
PropType.OBJECT:
return null if string == "" else load(string)
PropType.ENUM:
if string == "":
return int(uniques[col_index]["N_A"])
else:
if !uniques.has(col_index):
return -1
return int(uniques[col_index][string.capitalize().replace(" ", "_").to_upper()])
# if string.is_valid_int():
# return int(uniques[col_index][string.capitalize().replace(" ", "_").to_upper()])
# else:
# # If the enum is a string, we actually just want the key not the value
# var enum_keys : Dictionary = uniques[col_index]
# return int(enum_keys.find_key(string))
PropType.COLLECTION:
var result = str_to_var(string)
if result is Array:
for i in result.size():
if result[i] is String && result[i].begins_with("res://"):
result[i] = load(result[i])
if result is Dictionary:
for k in result:
if result[k] is String && result[k].begins_with("res://"):
result[k] = load(result[k])
if result == null:
result = []
return result
func property_to_string(value, col_index : int) -> String:
if value == null: return ""
match prop_types[col_index]:
PropType.STRING:
return str(value)
PropType.BOOL:
return enum_format[2] if value else enum_format[3]
PropType.FLOAT, PropType.INT:
return str(value)
PropType.COLOR:
return value.to_html()
PropType.OBJECT:
return value.resource_path
PropType.COLLECTION:
if value is Array:
var new_value := []
new_value.resize(value.size())
for i in value.size():
new_value[i] = value[i]
if value[i] is Resource:
new_value[i] = value[i].resource_path
value = new_value
if value is Dictionary:
var new_value := {}
for k in value:
new_value[k] = value[k]
if value[k] is Resource:
new_value[k] = value[k].resource_path
if k is Resource:
new_value[k.resource_path] = new_value[k]
value = new_value
return str(value)
PropType.ENUM:
var dict = uniques[col_index]
for k in dict:
if dict[k] == value:
return change_name_to_format(k, enum_format[0], enum_format[1])
return str(value)
func create_property_line_for_prop(col_index : int) -> String:
var result : String = "@export var " + prop_names[col_index] + " :"
match prop_types[col_index]:
PropType.STRING:
return result + "= \"\"\r\n"
PropType.BOOL:
return result + "= false\r\n"
PropType.FLOAT:
return result + "= 0.0\r\n"
PropType.INT:
return result + "= 0\r\n"
PropType.COLOR:
return result + "= Color.WHITE\r\n"
PropType.OBJECT:
return result + " Resource\r\n"
PropType.COLLECTION:
return result + "= []\r\n"
PropType.ENUM:
return result + " %s\r\n" % _escape_forbidden_enum_names(prop_names[col_index].capitalize().replace(" ", ""))
# return result.replace(
# "@export var",
# "@export_enum(" + _escape_forbidden_enum_names(
# prop_names[col_index].capitalize()\
# .replace(" ", "")
# ) + ") var"
# ) + "= 0\r\n"
return ""
func _escape_forbidden_enum_names(string : String) -> String:
if ClassDB.class_exists(string):
return string + "_"
# Not in ClassDB, but are engine types and can be property names
if string in [
"Color", "String", "Plane", "Projection",
"Basis", "Transform", "Variant",
]:
return string + "_"
return string
func create_enum_for_prop(col_index) -> String:
var result := (
"enum "
+ _escape_forbidden_enum_names(
prop_names[col_index].capitalize().replace(" ", "")
) + " {\r\n"
)
for k in uniques[col_index]:
result += (
"\t"
+ k # Enum Entry
+ " = "
+ str(uniques[col_index][k]) # Value
+ ",\r\n"
)
result += "\tMAX,\r\n}\r\n\r\n"
return result
func generate_script(entries, has_classname = true) -> GDScript:
var source := ""
# if has_classname and script_classname != "":
# source = "class_name " + script_classname + " \r\nextends Resource\r\n\r\n"
#
# else:
source = "extends Resource\r\n\r\n"
# Enums
uniques = get_uniques(entries)
for i in prop_types.size():
if prop_types[i] == PropType.ENUM:
source += create_enum_for_prop(i)
# Properties
for i in prop_names.size():
if (prop_names[i] != "resource_path") and (prop_names[i] != "resource_name"):
source += create_property_line_for_prop(i)
var created_script : GDScript = GDScript.new()
created_script.source_code = source
created_script.reload()
return created_script
func load_property_names_from_textfile(path : String, loaded_entries : Array):
prop_types.resize(prop_names.size())
prop_types.fill(4)
var enums_exist := false
for i in prop_names.size():
prop_names[i] = loaded_entries[0][i]\
.replace("\"", "")\
.replace(" ", "_")\
.replace("-", "_")\
.replace(".", "_")\
.replace(",", "_")\
.replace("\t", "_")\
.replace("/", "_")\
.replace("\\", "_")\
.to_lower()
var value = loaded_entries[1][i]
var value_cast = str_to_var(value)
# Don't guess Ints automatically - further rows might have floats
if value_cast is Array or value_cast is Dictionary:
prop_types[i] = ResourceTablesImport.PropType.COLLECTION
elif value.is_valid_float():
prop_types[i] = ResourceTablesImport.PropType.FLOAT
elif value.begins_with("res://") && prop_names[i] != "resource_path":
prop_types[i] = ResourceTablesImport.PropType.OBJECT
elif value.length() == 6 or value.length() == 8 or (value.length() > 0 and value[0] == "#"):
prop_types[i] = ResourceTablesImport.PropType.COLOR
else:
prop_types[i] = ResourceTablesImport.PropType.STRING
enums_exist = true
func load_external_script(script_res : Script):
new_script = script_res
var result := {}
for x in script_res.get_script_property_list():
if x.hint != PROPERTY_HINT_ENUM or x.type != TYPE_INT:
continue
var cur_value := ""
var result_for_prop := {}
result[prop_names.find(x.name)] = result_for_prop
var hint_arr : Array = x.hint_string.split(",")
for current_hint in hint_arr.size():
var colon_found : int = hint_arr[current_hint].rfind(":")
cur_value = hint_arr[current_hint]
if cur_value == "":
cur_value = "N_A"
if colon_found != -1:
var value_split := cur_value.split(":")
result_for_prop[value_split[1].to_upper()] = value_split[0]
else:
result_for_prop[cur_value.to_upper()] = result_for_prop.size()
func strings_to_resource(strings : Array, destination_path : String) -> Resource:
if destination_path == "":
destination_path = edited_path.get_base_dir().path_join("import/")
DirAccess.make_dir_recursive_absolute(destination_path)
# If a full path is provided this catches that case
var new_path : String = strings[prop_names.find(prop_used_as_filename)]
if !FileAccess.file_exists(new_path):
new_path = destination_path.path_join(new_path).trim_suffix(".tres") + ".tres"
if !FileAccess.file_exists(new_path):
new_path = (strings[prop_names.find(prop_used_as_filename)]
.trim_prefix(destination_path)
.trim_suffix(".tres") + ".tres"
)
if !new_path.begins_with("res://"):
new_path = destination_path.path_join(new_path)
DirAccess.make_dir_recursive_absolute(new_path.get_base_dir())
var new_res : Resource
if FileAccess.file_exists(new_path):
new_res = load(new_path)
else:
new_res = new_script.new()
new_res.resource_path = new_path
for i in mini(prop_names.size(), strings.size()):
var property_value = string_to_property(strings[i], i)
# This is awful, but the workaround for typed casting
# https://github.com/godotengine/godot/issues/72620
if property_value is Array or property_value is Dictionary:
var property_value_as_typed = new_res.get(prop_names[i])
property_value_as_typed.assign(property_value)
new_res.set(prop_names[i], property_value_as_typed)
else:
new_res.set(prop_names[i], property_value)
if prop_used_as_filename != "":
new_res.resource_path = new_path
return new_res
func resource_to_strings(res : Resource):
var strings := []
strings.resize(prop_names.size())
for i in prop_names.size():
strings[i] = property_to_string(res.get(prop_names[i]), i)
return PackedStringArray(strings)
func get_uniques(entries : Array) -> Dictionary:
var result := {}
for i in prop_types.size():
if prop_types[i] is PropType and prop_types[i] == PropType.ENUM:
var cur_value := ""
result[i] = {}
for j in entries.size():
if j == 0 and remove_first_row: continue
cur_value = entries[j][i].capitalize().to_upper().replace(" ", "_")
if cur_value == "":
cur_value = "N_A"
if !result[i].has(cur_value):
result[i][cur_value] = result[i].size()
return result
static func change_name_to_format(name : String, case : int, delim : String):
var string := name.capitalize().replace(" ", delim)
if case == NameCasing.ALL_LOWER:
return string.to_lower()
if case == NameCasing.CAPS_WORD_EXCEPT_FIRST:
return string[0].to_lower() + string.substr(1)
if case == NameCasing.CAPS_WORD:
return string
if case == NameCasing.ALL_CAPS:
return string.to_upper()
static func get_resource_property_types(res : Resource, properties : Array, uniques : Dictionary) -> Array:
var result : Array[PropType] = []
result.resize(properties.size())
result.fill(PropType.STRING)
var cur_type := 0
for x in res.get_property_list():
var poroperty_index := properties.find(x[&"name"])
if poroperty_index == -1: continue
if x[&"usage"] & PROPERTY_USAGE_EDITOR != 0:
if x[&"hint"] == PROPERTY_HINT_ENUM:
var enum_values : PackedStringArray = x[&"hint_string"].split(",")
var enum_value_dict := {}
var max_enum_value := 0
for i in enum_values.size():
var index_found : int = enum_values[i].find(":")
if index_found == -1:
enum_value_dict[enum_values[i].to_upper()] = max_enum_value
max_enum_value += 1
continue
var k = enum_values[i].left(index_found).to_upper()
var v = enum_values[i].right(index_found + 1).to_int()
enum_value_dict[k] = v
uniques[poroperty_index] = enum_value_dict
result[poroperty_index] = PropType.ENUM
else:
result[poroperty_index] = TYPE_MAP.get(x[&"type"], PropType.STRING)
return result

View File

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