add code step 2

This commit is contained in:
cakipaul 2024-12-23 09:30:31 +08:00
parent ac51d3031d
commit f032b08fc7
175 changed files with 13052 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf

34
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,34 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Debug GDScript Scene",
"type": "godot",
"request": "launch",
"project": "${workspaceFolder}",
// "project": "/Users/paul/Documents/dev/godot/MountainsAndSeas",
"port": 6007,
"address": "127.0.0.1",
"scene": "current",
"fixed_fps": 60,
"disable_vsync": false,
"preLaunchTask": ""
},
{
"name": "Debug GDScript Project",
"type": "godot",
"request": "launch",
"project": "${workspaceFolder}",
// "project": "/Users/paul/Documents/dev/godot/MountainsAndSeas",
"port": 6007,
"address": "127.0.0.1",
"scene": "main",
"fixed_fps": 60,
"disable_vsync": false,
"preLaunchTask": ""
}
]
}

33
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,33 @@
{
"workbench.colorTheme":"Bluloco Dark Godot",
"editor.fontFamily": "'Consolas'",
"editor.fontSize": 13,
"debug.console.fontSize": 13,
"terminal.integrated.fontSize": 13,
"chat.editor.fontSize": 13,
"workbench.sideBar.location": "left",
"project":"/Users/paul/Documents/dev/godot/MountainsAndSeas",
"cmake.configureOnOpen": true,
"godotTools.editorPath.godot4": "/Applications/Godot.app/Contents/MacOS/Godot",
// "godotTools.editorPath.godot4": "/Users/paul/Documents/dev/godot/godot/bin/godot.macos.editor.arm64",
// "godotTools.editorPath.godot4": "/opt/homebrew/bin/godot",
"godotTools.lsp.serverHost": "localhost",
"godotTools.lsp.serverPort": 6005,
"godotTools.lsp.serverProtocol": "tcp",
"editor.indentSize": "tabSize",
"godotFiles.godotCachePath": {
"win32": [
"%LOCALAPPDATA%/Godot/",
"~/AppData/Local/Godot/",
"%TEMP%/Godot/"
],
"darwin": [
"~/Library/Caches/Godot/"
],
"linux": [
"~/.cache/godot/",
"~/.var/app/org.godotengine.GodotSharp/cache/godot/",
"~/.var/app/org.godotengine.Godot/cache/godot/"
]
}
}

View File

@ -0,0 +1,21 @@
# MIT License
Copyright © 2023-present Hugo Locurcio and contributors
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,479 @@
extends CanvasLayer
@export var fps: Label
@export var frame_time: Label
@export var frame_number: Label
@export var frame_history_total_avg: Label
@export var frame_history_total_min: Label
@export var frame_history_total_max: Label
@export var frame_history_total_last: Label
@export var frame_history_cpu_avg: Label
@export var frame_history_cpu_min: Label
@export var frame_history_cpu_max: Label
@export var frame_history_cpu_last: Label
@export var frame_history_gpu_avg: Label
@export var frame_history_gpu_min: Label
@export var frame_history_gpu_max: Label
@export var frame_history_gpu_last: Label
@export var fps_graph: Panel
@export var total_graph: Panel
@export var cpu_graph: Panel
@export var gpu_graph: Panel
@export var information: Label
@export var settings: Label
## The number of frames to keep in history for graph drawing and best/worst calculations.
## Currently, this also affects how FPS is measured.
const HISTORY_NUM_FRAMES = 150
const GRAPH_SIZE = Vector2(150, 25)
const GRAPH_MIN_FPS = 10
const GRAPH_MAX_FPS = 160
const GRAPH_MIN_FRAMETIME = 1.0 / GRAPH_MIN_FPS
const GRAPH_MAX_FRAMETIME = 1.0 / GRAPH_MAX_FPS
## Debug menu display style.
enum Style {
HIDDEN, ## Debug menu is hidden.
VISIBLE_COMPACT, ## Debug menu is visible, with only the FPS, FPS cap (if any) and time taken to render the last frame.
VISIBLE_DETAILED, ## Debug menu is visible with full information, including graphs.
MAX, ## Represents the size of the Style enum.
}
## The style to use when drawing the debug menu.
var style := Style.HIDDEN:
set(value):
style = value
match style:
Style.HIDDEN:
visible = false
Style.VISIBLE_COMPACT, Style.VISIBLE_DETAILED:
visible = true
frame_number.visible = style == Style.VISIBLE_DETAILED
$DebugMenu/VBoxContainer/FrameTimeHistory.visible = style == Style.VISIBLE_DETAILED
$DebugMenu/VBoxContainer/FPSGraph.visible = style == Style.VISIBLE_DETAILED
$DebugMenu/VBoxContainer/TotalGraph.visible = style == Style.VISIBLE_DETAILED
$DebugMenu/VBoxContainer/CPUGraph.visible = style == Style.VISIBLE_DETAILED
$DebugMenu/VBoxContainer/GPUGraph.visible = style == Style.VISIBLE_DETAILED
information.visible = style == Style.VISIBLE_DETAILED
settings.visible = style == Style.VISIBLE_DETAILED
# Value of `Time.get_ticks_usec()` on the previous frame.
var last_tick := 0
var thread := Thread.new()
## Returns the sum of all values of an array (use as a parameter to `Array.reduce()`).
var sum_func := func avg(accum: float, number: float) -> float: return accum + number
# History of the last `HISTORY_NUM_FRAMES` rendered frames.
var frame_history_total: Array[float] = []
var frame_history_cpu: Array[float] = []
var frame_history_gpu: Array[float] = []
var fps_history: Array[float] = [] # Only used for graphs.
var frametime_avg := GRAPH_MIN_FRAMETIME
var frametime_cpu_avg := GRAPH_MAX_FRAMETIME
var frametime_gpu_avg := GRAPH_MIN_FRAMETIME
var frames_per_second := float(GRAPH_MIN_FPS)
var frame_time_gradient := Gradient.new()
func _init() -> void:
# This must be done here instead of `_ready()` to avoid having `visibility_changed` be emitted immediately.
visible = false
if not InputMap.has_action("cycle_debug_menu"):
# Create default input action if no user-defined override exists.
# We can't do it in the editor plugin's activation code as it doesn't seem to work there.
InputMap.add_action("cycle_debug_menu")
var event := InputEventKey.new()
event.keycode = KEY_F3
InputMap.action_add_event("cycle_debug_menu", event)
func _ready() -> void:
fps_graph.draw.connect(_fps_graph_draw)
total_graph.draw.connect(_total_graph_draw)
cpu_graph.draw.connect(_cpu_graph_draw)
gpu_graph.draw.connect(_gpu_graph_draw)
fps_history.resize(HISTORY_NUM_FRAMES)
frame_history_total.resize(HISTORY_NUM_FRAMES)
frame_history_cpu.resize(HISTORY_NUM_FRAMES)
frame_history_gpu.resize(HISTORY_NUM_FRAMES)
# NOTE: Both FPS and frametimes are colored following FPS logic
# (red = 10 FPS, yellow = 60 FPS, green = 110 FPS, cyan = 160 FPS).
# This makes the color gradient non-linear.
# Colors are taken from <https://tailwindcolor.com/>.
frame_time_gradient.set_color(0, Color8(239, 68, 68)) # red-500
frame_time_gradient.set_color(1, Color8(56, 189, 248)) # light-blue-400
frame_time_gradient.add_point(0.3333, Color8(250, 204, 21)) # yellow-400
frame_time_gradient.add_point(0.6667, Color8(128, 226, 95)) # 50-50 mix of lime-400 and green-400
get_viewport().size_changed.connect(update_settings_label)
# Display loading text while information is being queried,
# in case the user toggles the full debug menu just after starting the project.
information.text = "Loading hardware information...\n\n "
settings.text = "Loading project information..."
thread.start(
func():
# Disable thread safety checks as they interfere with this add-on.
# This only affects this particular thread, not other thread instances in the project.
# See <https://github.com/godotengine/godot/pull/78000> for details.
# Use a Callable so that this can be ignored on Godot 4.0 without causing a script error
# (thread safety checks were added in Godot 4.1).
if Engine.get_version_info()["hex"] >= 0x040100:
Callable(Thread, "set_thread_safety_checks_enabled").call(false)
# Enable required time measurements to display CPU/GPU frame time information.
# These lines are time-consuming operations, so run them in a separate thread.
RenderingServer.viewport_set_measure_render_time(get_viewport().get_viewport_rid(), true)
update_information_label()
update_settings_label()
)
func _input(event: InputEvent) -> void:
if event.is_action_pressed("cycle_debug_menu"):
style = wrapi(style + 1, 0, Style.MAX) as Style
func _exit_tree() -> void:
thread.wait_to_finish()
## Update hardware information label (this can change at runtime based on window
## size and graphics settings). This is only called when the window is resized.
## To update when graphics settings are changed, the function must be called manually
## using `DebugMenu.update_settings_label()`.
func update_settings_label() -> void:
settings.text = ""
if ProjectSettings.has_setting("application/config/version"):
settings.text += "Project Version: %s\n" % ProjectSettings.get_setting("application/config/version")
var rendering_method := str(ProjectSettings.get_setting_with_override("rendering/renderer/rendering_method"))
var rendering_method_string := rendering_method
match rendering_method:
"forward_plus":
rendering_method_string = "Forward+"
"mobile":
rendering_method_string = "Forward Mobile"
"gl_compatibility":
rendering_method_string = "Compatibility"
settings.text += "Rendering Method: %s\n" % rendering_method_string
var viewport := get_viewport()
# The size of the viewport rendering, which determines which resolution 3D is rendered at.
var viewport_render_size := Vector2i()
if viewport.content_scale_mode == Window.CONTENT_SCALE_MODE_VIEWPORT:
viewport_render_size = viewport.get_visible_rect().size
settings.text += "Viewport: %d×%d, Window: %d×%d\n" % [viewport.get_visible_rect().size.x, viewport.get_visible_rect().size.y, viewport.size.x, viewport.size.y]
else:
# Window size matches viewport size.
viewport_render_size = viewport.size
settings.text += "Viewport: %d×%d\n" % [viewport.size.x, viewport.size.y]
# Display 3D settings only if relevant.
if viewport.get_camera_3d():
var scaling_3d_mode_string := "(unknown)"
match viewport.scaling_3d_mode:
Viewport.SCALING_3D_MODE_BILINEAR:
scaling_3d_mode_string = "Bilinear"
Viewport.SCALING_3D_MODE_FSR:
scaling_3d_mode_string = "FSR 1.0"
Viewport.SCALING_3D_MODE_FSR2:
scaling_3d_mode_string = "FSR 2.2"
var antialiasing_3d_string := ""
if viewport.scaling_3d_mode == Viewport.SCALING_3D_MODE_FSR2:
# The FSR2 scaling mode includes its own temporal antialiasing implementation.
antialiasing_3d_string += (" + " if not antialiasing_3d_string.is_empty() else "") + "FSR 2.2"
if viewport.scaling_3d_mode != Viewport.SCALING_3D_MODE_FSR2 and viewport.use_taa:
# Godot's own TAA is ignored when using FSR2 scaling mode, as FSR2 provides its own TAA implementation.
antialiasing_3d_string += (" + " if not antialiasing_3d_string.is_empty() else "") + "TAA"
if viewport.msaa_3d >= Viewport.MSAA_2X:
antialiasing_3d_string += (" + " if not antialiasing_3d_string.is_empty() else "") + "%d× MSAA" % pow(2, viewport.msaa_3d)
if viewport.screen_space_aa == Viewport.SCREEN_SPACE_AA_FXAA:
antialiasing_3d_string += (" + " if not antialiasing_3d_string.is_empty() else "") + "FXAA"
settings.text += "3D scale (%s): %d%% = %d×%d" % [
scaling_3d_mode_string,
viewport.scaling_3d_scale * 100,
viewport_render_size.x * viewport.scaling_3d_scale,
viewport_render_size.y * viewport.scaling_3d_scale,
]
if not antialiasing_3d_string.is_empty():
settings.text += "\n3D Antialiasing: %s" % antialiasing_3d_string
var environment := viewport.get_camera_3d().get_world_3d().environment
if environment:
if environment.ssr_enabled:
settings.text += "\nSSR: %d Steps" % environment.ssr_max_steps
if environment.ssao_enabled:
settings.text += "\nSSAO: On"
if environment.ssil_enabled:
settings.text += "\nSSIL: On"
if environment.sdfgi_enabled:
settings.text += "\nSDFGI: %d Cascades" % environment.sdfgi_cascades
if environment.glow_enabled:
settings.text += "\nGlow: On"
if environment.volumetric_fog_enabled:
settings.text += "\nVolumetric Fog: On"
var antialiasing_2d_string := ""
if viewport.msaa_2d >= Viewport.MSAA_2X:
antialiasing_2d_string = "%d× MSAA" % pow(2, viewport.msaa_2d)
if not antialiasing_2d_string.is_empty():
settings.text += "\n2D Antialiasing: %s" % antialiasing_2d_string
## Update hardware/software information label (this never changes at runtime).
func update_information_label() -> void:
var adapter_string := ""
# Make "NVIDIA Corporation" and "NVIDIA" be considered identical (required when using OpenGL to avoid redundancy).
if RenderingServer.get_video_adapter_vendor().trim_suffix(" Corporation") in RenderingServer.get_video_adapter_name():
# Avoid repeating vendor name before adapter name.
# Trim redundant suffix sometimes reported by NVIDIA graphics cards when using OpenGL.
adapter_string = RenderingServer.get_video_adapter_name().trim_suffix("/PCIe/SSE2")
else:
adapter_string = RenderingServer.get_video_adapter_vendor() + " - " + RenderingServer.get_video_adapter_name().trim_suffix("/PCIe/SSE2")
# Graphics driver version information isn't always availble.
var driver_info := OS.get_video_adapter_driver_info()
var driver_info_string := ""
if driver_info.size() >= 2:
driver_info_string = driver_info[1]
else:
driver_info_string = "(unknown)"
var release_string := ""
if OS.has_feature("editor"):
# Editor build (implies `debug`).
release_string = "editor"
elif OS.has_feature("debug"):
# Debug export template build.
release_string = "debug"
else:
# Release export template build.
release_string = "release"
var rendering_method := str(ProjectSettings.get_setting_with_override("rendering/renderer/rendering_method"))
var rendering_driver := str(ProjectSettings.get_setting_with_override("rendering/rendering_device/driver"))
var graphics_api_string := rendering_driver
if rendering_method != "gl_compatibility":
if rendering_driver == "d3d12":
graphics_api_string = "Direct3D 12"
elif rendering_driver == "metal":
graphics_api_string = "Metal"
elif rendering_driver == "vulkan":
if OS.has_feature("macos") or OS.has_feature("ios"):
graphics_api_string = "Vulkan via MoltenVK"
else:
graphics_api_string = "Vulkan"
else:
if rendering_driver == "opengl3_angle":
graphics_api_string = "OpenGL via ANGLE"
elif OS.has_feature("mobile") or rendering_driver == "opengl3_es":
graphics_api_string = "OpenGL ES"
elif OS.has_feature("web"):
graphics_api_string = "WebGL"
elif rendering_driver == "opengl3":
graphics_api_string = "OpenGL"
information.text = (
"%s, %d threads\n" % [OS.get_processor_name().replace("(R)", "").replace("(TM)", ""), OS.get_processor_count()]
+ "%s %s (%s %s), %s %s\n" % [OS.get_name(), "64-bit" if OS.has_feature("64") else "32-bit", release_string, "double" if OS.has_feature("double") else "single", graphics_api_string, RenderingServer.get_video_adapter_api_version()]
+ "%s, %s" % [adapter_string, driver_info_string]
)
func _fps_graph_draw() -> void:
var fps_polyline := PackedVector2Array()
fps_polyline.resize(HISTORY_NUM_FRAMES)
for fps_index in fps_history.size():
fps_polyline[fps_index] = Vector2(
remap(fps_index, 0, fps_history.size(), 0, GRAPH_SIZE.x),
remap(clampf(fps_history[fps_index], GRAPH_MIN_FPS, GRAPH_MAX_FPS), GRAPH_MIN_FPS, GRAPH_MAX_FPS, GRAPH_SIZE.y, 0.0)
)
# Don't use antialiasing to speed up line drawing, but use a width that scales with
# viewport scale to keep the line easily readable on hiDPI displays.
fps_graph.draw_polyline(fps_polyline, frame_time_gradient.sample(remap(frames_per_second, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0)), 1.0)
func _total_graph_draw() -> void:
var total_polyline := PackedVector2Array()
total_polyline.resize(HISTORY_NUM_FRAMES)
for total_index in frame_history_total.size():
total_polyline[total_index] = Vector2(
remap(total_index, 0, frame_history_total.size(), 0, GRAPH_SIZE.x),
remap(clampf(frame_history_total[total_index], GRAPH_MIN_FPS, GRAPH_MAX_FPS), GRAPH_MIN_FPS, GRAPH_MAX_FPS, GRAPH_SIZE.y, 0.0)
)
# Don't use antialiasing to speed up line drawing, but use a width that scales with
# viewport scale to keep the line easily readable on hiDPI displays.
total_graph.draw_polyline(total_polyline, frame_time_gradient.sample(remap(1000.0 / frametime_avg, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0)), 1.0)
func _cpu_graph_draw() -> void:
var cpu_polyline := PackedVector2Array()
cpu_polyline.resize(HISTORY_NUM_FRAMES)
for cpu_index in frame_history_cpu.size():
cpu_polyline[cpu_index] = Vector2(
remap(cpu_index, 0, frame_history_cpu.size(), 0, GRAPH_SIZE.x),
remap(clampf(frame_history_cpu[cpu_index], GRAPH_MIN_FPS, GRAPH_MAX_FPS), GRAPH_MIN_FPS, GRAPH_MAX_FPS, GRAPH_SIZE.y, 0.0)
)
# Don't use antialiasing to speed up line drawing, but use a width that scales with
# viewport scale to keep the line easily readable on hiDPI displays.
cpu_graph.draw_polyline(cpu_polyline, frame_time_gradient.sample(remap(1000.0 / frametime_cpu_avg, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0)), 1.0)
func _gpu_graph_draw() -> void:
var gpu_polyline := PackedVector2Array()
gpu_polyline.resize(HISTORY_NUM_FRAMES)
for gpu_index in frame_history_gpu.size():
gpu_polyline[gpu_index] = Vector2(
remap(gpu_index, 0, frame_history_gpu.size(), 0, GRAPH_SIZE.x),
remap(clampf(frame_history_gpu[gpu_index], GRAPH_MIN_FPS, GRAPH_MAX_FPS), GRAPH_MIN_FPS, GRAPH_MAX_FPS, GRAPH_SIZE.y, 0.0)
)
# Don't use antialiasing to speed up line drawing, but use a width that scales with
# viewport scale to keep the line easily readable on hiDPI displays.
gpu_graph.draw_polyline(gpu_polyline, frame_time_gradient.sample(remap(1000.0 / frametime_gpu_avg, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0)), 1.0)
func _process(_delta: float) -> void:
if visible:
fps_graph.queue_redraw()
total_graph.queue_redraw()
cpu_graph.queue_redraw()
gpu_graph.queue_redraw()
# Difference between the last two rendered frames in milliseconds.
var frametime := (Time.get_ticks_usec() - last_tick) * 0.001
frame_history_total.push_back(frametime)
if frame_history_total.size() > HISTORY_NUM_FRAMES:
frame_history_total.pop_front()
# Frametimes are colored following FPS logic (red = 10 FPS, yellow = 60 FPS, green = 110 FPS, cyan = 160 FPS).
# This makes the color gradient non-linear.
frametime_avg = frame_history_total.reduce(sum_func) / frame_history_total.size()
frame_history_total_avg.text = str(frametime_avg).pad_decimals(2)
frame_history_total_avg.modulate = frame_time_gradient.sample(remap(1000.0 / frametime_avg, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
var frametime_min: float = frame_history_total.min()
frame_history_total_min.text = str(frametime_min).pad_decimals(2)
frame_history_total_min.modulate = frame_time_gradient.sample(remap(1000.0 / frametime_min, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
var frametime_max: float = frame_history_total.max()
frame_history_total_max.text = str(frametime_max).pad_decimals(2)
frame_history_total_max.modulate = frame_time_gradient.sample(remap(1000.0 / frametime_max, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
frame_history_total_last.text = str(frametime).pad_decimals(2)
frame_history_total_last.modulate = frame_time_gradient.sample(remap(1000.0 / frametime, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
var viewport_rid := get_viewport().get_viewport_rid()
var frametime_cpu := RenderingServer.viewport_get_measured_render_time_cpu(viewport_rid) + RenderingServer.get_frame_setup_time_cpu()
frame_history_cpu.push_back(frametime_cpu)
if frame_history_cpu.size() > HISTORY_NUM_FRAMES:
frame_history_cpu.pop_front()
frametime_cpu_avg = frame_history_cpu.reduce(sum_func) / frame_history_cpu.size()
frame_history_cpu_avg.text = str(frametime_cpu_avg).pad_decimals(2)
frame_history_cpu_avg.modulate = frame_time_gradient.sample(remap(1000.0 / frametime_cpu_avg, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
var frametime_cpu_min: float = frame_history_cpu.min()
frame_history_cpu_min.text = str(frametime_cpu_min).pad_decimals(2)
frame_history_cpu_min.modulate = frame_time_gradient.sample(remap(1000.0 / frametime_cpu_min, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
var frametime_cpu_max: float = frame_history_cpu.max()
frame_history_cpu_max.text = str(frametime_cpu_max).pad_decimals(2)
frame_history_cpu_max.modulate = frame_time_gradient.sample(remap(1000.0 / frametime_cpu_max, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
frame_history_cpu_last.text = str(frametime_cpu).pad_decimals(2)
frame_history_cpu_last.modulate = frame_time_gradient.sample(remap(1000.0 / frametime_cpu, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
var frametime_gpu := RenderingServer.viewport_get_measured_render_time_gpu(viewport_rid)
frame_history_gpu.push_back(frametime_gpu)
if frame_history_gpu.size() > HISTORY_NUM_FRAMES:
frame_history_gpu.pop_front()
frametime_gpu_avg = frame_history_gpu.reduce(sum_func) / frame_history_gpu.size()
frame_history_gpu_avg.text = str(frametime_gpu_avg).pad_decimals(2)
frame_history_gpu_avg.modulate = frame_time_gradient.sample(remap(1000.0 / frametime_gpu_avg, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
var frametime_gpu_min: float = frame_history_gpu.min()
frame_history_gpu_min.text = str(frametime_gpu_min).pad_decimals(2)
frame_history_gpu_min.modulate = frame_time_gradient.sample(remap(1000.0 / frametime_gpu_min, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
var frametime_gpu_max: float = frame_history_gpu.max()
frame_history_gpu_max.text = str(frametime_gpu_max).pad_decimals(2)
frame_history_gpu_max.modulate = frame_time_gradient.sample(remap(1000.0 / frametime_gpu_max, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
frame_history_gpu_last.text = str(frametime_gpu).pad_decimals(2)
frame_history_gpu_last.modulate = frame_time_gradient.sample(remap(1000.0 / frametime_gpu, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
frames_per_second = 1000.0 / frametime_avg
fps_history.push_back(frames_per_second)
if fps_history.size() > HISTORY_NUM_FRAMES:
fps_history.pop_front()
fps.text = str(floor(frames_per_second)) + " FPS"
var frame_time_color := frame_time_gradient.sample(remap(frames_per_second, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
fps.modulate = frame_time_color
frame_time.text = str(frametime).pad_decimals(2) + " mspf"
frame_time.modulate = frame_time_color
var vsync_string := ""
match DisplayServer.window_get_vsync_mode():
DisplayServer.VSYNC_ENABLED:
vsync_string = "V-Sync"
DisplayServer.VSYNC_ADAPTIVE:
vsync_string = "Adaptive V-Sync"
DisplayServer.VSYNC_MAILBOX:
vsync_string = "Mailbox V-Sync"
if Engine.max_fps > 0 or OS.low_processor_usage_mode:
# Display FPS cap determined by `Engine.max_fps` or low-processor usage mode sleep duration
# (the lowest FPS cap is used).
var low_processor_max_fps := roundi(1000000.0 / OS.low_processor_usage_mode_sleep_usec)
var fps_cap := low_processor_max_fps
if Engine.max_fps > 0:
fps_cap = mini(Engine.max_fps, low_processor_max_fps)
frame_time.text += " (cap: " + str(fps_cap) + " FPS"
if not vsync_string.is_empty():
frame_time.text += " + " + vsync_string
frame_time.text += ")"
else:
if not vsync_string.is_empty():
frame_time.text += " (" + vsync_string + ")"
frame_number.text = "Frame: " + str(Engine.get_frames_drawn())
last_tick = Time.get_ticks_usec()
func _on_visibility_changed() -> void:
if visible:
# Reset graphs to prevent them from looking strange before `HISTORY_NUM_FRAMES` frames
# have been drawn.
var frametime_last := (Time.get_ticks_usec() - last_tick) * 0.001
fps_history.resize(HISTORY_NUM_FRAMES)
fps_history.fill(1000.0 / frametime_last)
frame_history_total.resize(HISTORY_NUM_FRAMES)
frame_history_total.fill(frametime_last)
frame_history_cpu.resize(HISTORY_NUM_FRAMES)
var viewport_rid := get_viewport().get_viewport_rid()
frame_history_cpu.fill(RenderingServer.viewport_get_measured_render_time_cpu(viewport_rid) + RenderingServer.get_frame_setup_time_cpu())
frame_history_gpu.resize(HISTORY_NUM_FRAMES)
frame_history_gpu.fill(RenderingServer.viewport_get_measured_render_time_gpu(viewport_rid))

View File

@ -0,0 +1,401 @@
[gd_scene load_steps=3 format=3 uid="uid://cggqb75a8w8r"]
[ext_resource type="Script" path="res://addons/debug_menu/debug_menu.gd" id="1_p440y"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ki0n8"]
bg_color = Color(0, 0, 0, 0.25098)
[node name="CanvasLayer" type="CanvasLayer" node_paths=PackedStringArray("fps", "frame_time", "frame_number", "frame_history_total_avg", "frame_history_total_min", "frame_history_total_max", "frame_history_total_last", "frame_history_cpu_avg", "frame_history_cpu_min", "frame_history_cpu_max", "frame_history_cpu_last", "frame_history_gpu_avg", "frame_history_gpu_min", "frame_history_gpu_max", "frame_history_gpu_last", "fps_graph", "total_graph", "cpu_graph", "gpu_graph", "information", "settings")]
layer = 128
script = ExtResource("1_p440y")
fps = NodePath("DebugMenu/VBoxContainer/FPS")
frame_time = NodePath("DebugMenu/VBoxContainer/FrameTime")
frame_number = NodePath("DebugMenu/VBoxContainer/FrameNumber")
frame_history_total_avg = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/TotalAvg")
frame_history_total_min = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/TotalMin")
frame_history_total_max = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/TotalMax")
frame_history_total_last = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/TotalLast")
frame_history_cpu_avg = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/CPUAvg")
frame_history_cpu_min = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/CPUMin")
frame_history_cpu_max = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/CPUMax")
frame_history_cpu_last = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/CPULast")
frame_history_gpu_avg = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/GPUAvg")
frame_history_gpu_min = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/GPUMin")
frame_history_gpu_max = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/GPUMax")
frame_history_gpu_last = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/GPULast")
fps_graph = NodePath("DebugMenu/VBoxContainer/FPSGraph/Graph")
total_graph = NodePath("DebugMenu/VBoxContainer/TotalGraph/Graph")
cpu_graph = NodePath("DebugMenu/VBoxContainer/CPUGraph/Graph")
gpu_graph = NodePath("DebugMenu/VBoxContainer/GPUGraph/Graph")
information = NodePath("DebugMenu/VBoxContainer/Information")
settings = NodePath("DebugMenu/VBoxContainer/Settings")
[node name="DebugMenu" type="Control" parent="."]
custom_minimum_size = Vector2(400, 400)
layout_mode = 3
anchors_preset = 1
anchor_left = 1.0
anchor_right = 1.0
offset_left = -416.0
offset_top = 8.0
offset_right = -16.0
offset_bottom = 408.0
grow_horizontal = 0
size_flags_horizontal = 8
size_flags_vertical = 4
mouse_filter = 2
[node name="VBoxContainer" type="VBoxContainer" parent="DebugMenu"]
layout_mode = 1
anchors_preset = 1
anchor_left = 1.0
anchor_right = 1.0
offset_left = -300.0
offset_bottom = 374.0
grow_horizontal = 0
mouse_filter = 2
theme_override_constants/separation = 0
[node name="FPS" type="Label" parent="DebugMenu/VBoxContainer"]
modulate = Color(0, 1, 0, 1)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/line_spacing = 0
theme_override_constants/outline_size = 5
theme_override_font_sizes/font_size = 18
text = "60 FPS"
horizontal_alignment = 2
[node name="FrameTime" type="Label" parent="DebugMenu/VBoxContainer"]
modulate = Color(0, 1, 0, 1)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "16.67 mspf (cap: 123 FPS + Adaptive V-Sync)"
horizontal_alignment = 2
[node name="FrameNumber" type="Label" parent="DebugMenu/VBoxContainer"]
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "Frame: 1234"
horizontal_alignment = 2
[node name="FrameTimeHistory" type="GridContainer" parent="DebugMenu/VBoxContainer"]
layout_mode = 2
size_flags_horizontal = 8
mouse_filter = 2
theme_override_constants/h_separation = 0
theme_override_constants/v_separation = 0
columns = 5
[node name="Spacer" type="Control" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
custom_minimum_size = Vector2(60, 0)
layout_mode = 2
mouse_filter = 2
[node name="AvgHeader" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "Average"
horizontal_alignment = 2
[node name="MinHeader" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "Best"
horizontal_alignment = 2
[node name="MaxHeader" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "Worst"
horizontal_alignment = 2
[node name="LastHeader" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "Last"
horizontal_alignment = 2
[node name="TotalHeader" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "Total:"
horizontal_alignment = 2
[node name="TotalAvg" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
modulate = Color(0, 1, 0, 1)
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "123.45"
horizontal_alignment = 2
[node name="TotalMin" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
modulate = Color(0, 1, 0, 1)
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "123.45"
horizontal_alignment = 2
[node name="TotalMax" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
modulate = Color(0, 1, 0, 1)
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "123.45"
horizontal_alignment = 2
[node name="TotalLast" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
modulate = Color(0, 1, 0, 1)
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "123.45"
horizontal_alignment = 2
[node name="CPUHeader" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "CPU:"
horizontal_alignment = 2
[node name="CPUAvg" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
modulate = Color(0, 1, 0, 1)
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "123.45"
horizontal_alignment = 2
[node name="CPUMin" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
modulate = Color(0, 1, 0, 1)
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "12.34"
horizontal_alignment = 2
[node name="CPUMax" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
modulate = Color(0, 1, 0, 1)
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "123.45"
horizontal_alignment = 2
[node name="CPULast" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
modulate = Color(0, 1, 0, 1)
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "123.45"
horizontal_alignment = 2
[node name="GPUHeader" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "GPU:"
horizontal_alignment = 2
[node name="GPUAvg" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
modulate = Color(0, 1, 0, 1)
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "123.45"
horizontal_alignment = 2
[node name="GPUMin" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
modulate = Color(0, 1, 0, 1)
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "1.23"
horizontal_alignment = 2
[node name="GPUMax" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
modulate = Color(0, 1, 0, 1)
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "123.45"
horizontal_alignment = 2
[node name="GPULast" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
modulate = Color(0, 1, 0, 1)
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "123.45"
horizontal_alignment = 2
[node name="FPSGraph" type="HBoxContainer" parent="DebugMenu/VBoxContainer"]
layout_mode = 2
mouse_filter = 2
alignment = 2
[node name="Title" type="Label" parent="DebugMenu/VBoxContainer/FPSGraph"]
custom_minimum_size = Vector2(0, 27)
layout_mode = 2
size_flags_horizontal = 8
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "FPS: ↑"
vertical_alignment = 1
[node name="Graph" type="Panel" parent="DebugMenu/VBoxContainer/FPSGraph"]
custom_minimum_size = Vector2(150, 25)
layout_mode = 2
size_flags_vertical = 0
mouse_filter = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_ki0n8")
[node name="TotalGraph" type="HBoxContainer" parent="DebugMenu/VBoxContainer"]
layout_mode = 2
mouse_filter = 2
alignment = 2
[node name="Title" type="Label" parent="DebugMenu/VBoxContainer/TotalGraph"]
custom_minimum_size = Vector2(0, 27)
layout_mode = 2
size_flags_horizontal = 8
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "Total: ↓"
vertical_alignment = 1
[node name="Graph" type="Panel" parent="DebugMenu/VBoxContainer/TotalGraph"]
custom_minimum_size = Vector2(150, 25)
layout_mode = 2
size_flags_vertical = 0
mouse_filter = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_ki0n8")
[node name="CPUGraph" type="HBoxContainer" parent="DebugMenu/VBoxContainer"]
layout_mode = 2
mouse_filter = 2
alignment = 2
[node name="Title" type="Label" parent="DebugMenu/VBoxContainer/CPUGraph"]
custom_minimum_size = Vector2(0, 27)
layout_mode = 2
size_flags_horizontal = 8
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "CPU: ↓"
vertical_alignment = 1
[node name="Graph" type="Panel" parent="DebugMenu/VBoxContainer/CPUGraph"]
custom_minimum_size = Vector2(150, 25)
layout_mode = 2
size_flags_vertical = 0
mouse_filter = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_ki0n8")
[node name="GPUGraph" type="HBoxContainer" parent="DebugMenu/VBoxContainer"]
layout_mode = 2
mouse_filter = 2
alignment = 2
[node name="Title" type="Label" parent="DebugMenu/VBoxContainer/GPUGraph"]
custom_minimum_size = Vector2(0, 27)
layout_mode = 2
size_flags_horizontal = 8
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "GPU: ↓"
vertical_alignment = 1
[node name="Graph" type="Panel" parent="DebugMenu/VBoxContainer/GPUGraph"]
custom_minimum_size = Vector2(150, 25)
layout_mode = 2
size_flags_vertical = 0
mouse_filter = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_ki0n8")
[node name="Information" type="Label" parent="DebugMenu/VBoxContainer"]
modulate = Color(1, 1, 1, 0.752941)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "12th Gen Intel(R) Core(TM) i0-1234K
Windows 12 64-bit (double precision), Vulkan 1.2.34
NVIDIA GeForce RTX 1234, 123.45.67"
horizontal_alignment = 2
[node name="Settings" type="Label" parent="DebugMenu/VBoxContainer"]
modulate = Color(0.8, 0.84, 1, 0.752941)
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 12
text = "Project Version: 1.2.3
Rendering Method: Forward+
Window: 1234×567, Viewport: 1234×567
3D Scale (FSR 1.0): 100% = 1234×567
3D Antialiasing: TAA + 2× MSAA + FXAA
SSR: 123 Steps
SSAO: On
SSIL: On
SDFGI: 1 Cascades
Glow: On
Volumetric Fog: On
2D Antialiasing: 2× MSAA"
horizontal_alignment = 2
[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"]

View File

@ -0,0 +1,7 @@
[plugin]
name="Debug Menu"
description="In-game debug menu displaying performance metrics and hardware information"
author="Calinou"
version="1.2.0"
script="plugin.gd"

View File

@ -0,0 +1,29 @@
@tool
extends EditorPlugin
func _enter_tree() -> void:
add_autoload_singleton("DebugMenu", "res://addons/debug_menu/debug_menu.tscn")
# FIXME: This appears to do nothing.
# if not ProjectSettings.has_setting("application/config/version"):
# ProjectSettings.set_setting("application/config/version", "1.0.0")
#
# ProjectSettings.set_initial_value("application/config/version", "1.0.0")
# ProjectSettings.add_property_info({
# name = "application/config/version",
# type = TYPE_STRING,
# })
#
# if not InputMap.has_action("cycle_debug_menu"):
# InputMap.add_action("cycle_debug_menu")
# var event := InputEventKey.new()
# event.keycode = KEY_F3
# InputMap.action_add_event("cycle_debug_menu", event)
#
# ProjectSettings.save()
func _exit_tree() -> void:
remove_autoload_singleton("DebugMenu")
# Don't remove the project setting's value and input map action,
# as the plugin may be re-enabled in the future.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="64"
height="64"
viewBox="0 0 64 64"
version="1.1"
id="svg5"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
sodipodi:docname="circle.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:document-units="px"
showgrid="false"
inkscape:zoom="2"
inkscape:cx="-81"
inkscape:cy="16.25"
inkscape:window-width="1366"
inkscape:window-height="716"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<circle
style="opacity:1;fill:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round"
id="path846"
cx="32"
cy="32"
r="32" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="64"
height="64"
viewBox="0 0 64 64"
version="1.1"
id="svg5"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
sodipodi:docname="json.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:document-units="px"
showgrid="false"
inkscape:zoom="21.124815"
inkscape:cx="4.165717"
inkscape:cy="13.798937"
inkscape:window-width="1366"
inkscape:window-height="716"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
aria-label="{}"
id="text3762"
style="font-weight:600;font-size:13.3333px;line-height:1.25;font-family:'Open Sans';-inkscape-font-specification:'Open Sans Semi-Bold';text-align:center;letter-spacing:2px;text-anchor:middle;fill:#ffff00;stroke-width:0.249993"
transform="matrix(4.0001062,0,0,4.0001062,-8.4879998e-4,-8.5180006e-4)">
<path
d="m 3.9759191,9.8359333 q 0,-1.2109344 -1.7317665,-1.2109344 V 7.3815124 q 0.8789041,0 1.3020801,-0.292968 Q 3.9759191,6.789066 3.9759191,6.1835988 V 4.1588643 q 0,-1.0156224 0.7031232,-1.4908816 Q 5.3886759,2.1862131 6.8014328,2.1862131 V 3.3711059 Q 6.0592472,3.4036579 5.7207064,3.6770947 5.3886759,3.9440211 5.3886759,4.4778739 v 1.9335889 q 0,1.2955697 -1.4908816,1.5494753 v 0.078125 q 1.4908816,0.2343744 1.4908816,1.5429648 v 1.9466091 q 0,0.533853 0.3320305,0.80729 0.3320304,0.273437 1.0807264,0.286458 v 1.191403 Q 5.2975303,13.800767 4.6334695,13.299466 3.9759191,12.804676 3.9759191,11.691397 Z"
id="path13307"
style="stroke-width:0.249993" />
<path
d="m 12.115226,11.834626 q 0,1.015623 -0.65104,1.490882 -0.644529,0.475259 -2.070307,0.48828 v -1.191403 q 0.618488,-0.0065 0.963539,-0.253906 0.345051,-0.240885 0.345051,-0.839842 V 9.8229125 q 0,-0.7877584 0.345052,-1.2174448 0.345051,-0.4296864 1.14583,-0.5664048 V 7.9609381 Q 10.702469,7.7070325 10.702469,6.4114628 V 4.4778739 q 0,-0.5338528 -0.299478,-0.8007792 Q 10.110023,3.4036579 9.393879,3.3711059 V 2.1862131 q 1.451819,0 2.083328,0.5013008 0.638019,0.4947904 0.638019,1.6276 v 1.8684849 q 0,0.65104 0.410156,0.9244768 0.416665,0.2734368 1.230465,0.2734368 v 1.2434865 q -0.800779,0 -1.223955,0.2799472 -0.416666,0.2734368 -0.416666,0.9309872 z"
id="path13309"
style="stroke-width:0.249993" />
</g>
</g>
<style
id="style1218">.st0{fill:#999}</style>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="64"
height="64"
viewBox="0 0 64 64"
version="1.1"
id="svg5"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
sodipodi:docname="markdown.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:document-units="px"
showgrid="false"
inkscape:zoom="3.734375"
inkscape:cx="16.870293"
inkscape:cy="24.769875"
inkscape:window-width="1366"
inkscape:window-height="716"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
id="path1224"
style="fill:#ffffff;stroke-width:4.49999"
d="M 5.3360002,10.222707 V 53.777293 H 58.664 V 10.222707 Z m 6.6640468,12.093722 h 5.687486 l 5.687487,7.109358 5.695299,-7.109358 h 5.687486 V 41.691381 H 29.070319 V 30.582034 l -5.695299,7.109357 -5.687487,-7.109357 v 11.109347 h -5.687486 z m 31.28899,0 h 5.687486 v 9.953101 h 5.687487 l -8.531232,9.421851 -8.53123,-9.421851 h 5.687489 z" />
</g>
<style
id="style1218">.st0{fill:#999}</style>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="64"
height="64"
viewBox="0 0 64 64"
version="1.1"
id="svg5"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
sodipodi:docname="yaml.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:document-units="px"
showgrid="false"
inkscape:zoom="5.281204"
inkscape:cx="35.597943"
inkscape:cy="31.526902"
inkscape:window-width="1366"
inkscape:window-height="716"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
aria-label="YA ML"
id="text3762"
style="font-weight:800;font-size:8px;line-height:1;font-family:'Open Sans';-inkscape-font-specification:'Open Sans Ultra-Bold';letter-spacing:0px;fill:#ff5555;stroke-width:0.249995"
transform="matrix(4.0000825,0,0,4.0000825,-0.59442325,-6.6100002e-4)">
<path
d="M 4.7578125,3.2734375 5.7421875,1.15625 h 1.671875 L 5.5351562,4.6367187 V 6.8671875 H 3.9804688 V 4.6835937 L 2.1015625,1.15625 H 3.78125 Z"
id="path24552"
style="stroke-width:0.249995" />
<path
d="M 11.054688,6.8671875 10.773438,5.796875 H 8.9179688 L 8.6289063,6.8671875 H 6.9335937 L 8.796875,1.1328125 h 2.058594 l 1.886719,5.734375 z M 10.453125,4.53125 10.207031,3.59375 Q 10.121094,3.28125 9.9960938,2.7851563 9.875,2.2890625 9.8359375,2.0742188 9.8007813,2.2734375 9.6953125,2.7304688 9.59375,3.1875 9.2382813,4.53125 Z"
id="path24554"
style="stroke-width:0.249995" />
<path
d="M 5.2070312,14.867188 4.0390625,10.753906 h -0.035156 q 0.082031,1.050782 0.082031,1.632813 v 2.480469 H 2.71875 V 9.15625 h 2.0546875 l 1.1914062,4.054688 h 0.03125 L 7.1640625,9.15625 h 2.0585938 v 5.710938 H 7.8046875 v -2.503907 q 0,-0.195312 0.00391,-0.433593 0.00781,-0.238282 0.054687,-1.167969 H 7.828125 l -1.1523438,4.105469 z"
id="path24556"
style="stroke-width:0.249995" />
<path
d="M 10.453125,14.867188 V 9.15625 h 1.542969 v 4.464844 h 2.199219 v 1.246094 z"
id="path24558"
style="stroke-width:0.249995" />
</g>
</g>
<style
id="style1218">.st0{fill:#999}</style>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,65 @@
var path: String
var size: int
var total_lines: int
var source_code_lines: int
var comment_lines: int
var blank_lines: int
func _load_dir(at_path: String) -> int:
return _load_file_info(at_path)
func _load_file_info(at_path: String, skip_line_count: bool = false) -> int:
var file := FileAccess.open(at_path, FileAccess.READ)
if !file:
return FileAccess.get_open_error()
path = at_path
size = file.get_length()
total_lines = 0
source_code_lines = 0
comment_lines = 0
blank_lines = 0
if skip_line_count:
return OK
while not file.eof_reached():
var line: String = file.get_line()
if _is_comment(line):
comment_lines += 1
elif is_blank(line):
blank_lines += 1
else:
source_code_lines += 1
total_lines +=1
file.close()
return OK
func get_name() -> String:
return path.get_file()
func _get_extension() -> String:
return path.get_extension()
func _get_icon() -> String:
return ""
func _get_color() -> Color:
return Color.TRANSPARENT
func _is_comment(line: String) -> bool:
return false
func is_blank(line: String) -> bool:
return line.strip_edges().is_empty()
func _is_script() -> bool:
return false
func _is_scene() -> bool:
return false
func _is_resource() -> bool:
return false

View File

@ -0,0 +1,158 @@
const FileStatistics: Script = preload("./FileStatistics.gd")
const ConfigFileStatistics: Script = preload("./extensions/ConfigFileStatistics.gd")
const CSharpStatistics: Script = preload("./extensions/CSharpStatistics.gd")
const GDScriptStatistics: Script = preload("./extensions/GDScriptStatistics.gd")
const JSONStatistics: Script = preload("./extensions/JSONStatistics.gd")
const MarkdownStatistics: Script = preload("./extensions/MarkdownStatistics.gd")
const ResourceStatistics: Script = preload("./extensions/ResourceStatistics.gd")
const YAMLStatistics: Script = preload("./extensions/YAMLStatistics.gd")
var directories: PackedStringArray
var scenes: Array
var resources: Array
var scripts: Array
var misc: Array
func _load_dir(root := "res://") -> int:
if !path_force_included(root) && path_ignored(root):
return OK
var directory := DirAccess.open(root)
if !directory:
return DirAccess.get_open_error()
for dir in directory.get_directories():
_load_dir(root + dir + "/")
for file_name in directory.get_files():
var current_path: String = root + file_name
if !path_force_included(current_path) && path_ignored(current_path):
continue
var file_stats: FileStatistics = get_file_loader(current_path)
if file_stats._load_dir(current_path) == OK:
if file_stats._is_scene():
scenes.append(file_stats)
elif file_stats._is_script():
scripts.append(file_stats)
elif file_stats._is_resource():
resources.append(file_stats)
else:
misc.append(file_stats)
return OK
func get_used_langauges() -> PackedStringArray:
var languages: PackedStringArray = []
for file_stats in scripts:
var extension: String = file_stats._get_extension()
if not file_stats._get_extension() in languages:
languages.append(extension)
return languages
func get_total_lines(of_scripts: bool = true) -> int:
var source: Array = scripts if of_scripts else misc
var total: int
for file_stats in source:
total += file_stats.total_lines
return total
func get_total_code_lines(of_scripts: bool = true) -> int:
var source: Array = scripts if of_scripts else misc
var total: int
for file_stats in source:
total += file_stats.source_code_lines
return total
func get_total_comment_lines(of_scripts: bool = true) -> int:
var source: Array = scripts if of_scripts else misc
var total: int
for file_stats in source:
total += file_stats.comment_lines
return total
func get_total_blank_lines(of_scripts: bool = true) -> int:
var source: Array = scripts if of_scripts else misc
var total: int
for file_stats in source:
total += file_stats.blank_lines
return total
func get_total_nodes() -> int:
var total: int
for file_stats in scenes:
total += file_stats.node_count
return total
func get_total_connections() -> int:
var total: int
for file_stats in scenes:
total += file_stats.connection_count
return total
func get_total_scenes_size() -> int:
var total: int
for file_stats in scenes:
total += file_stats.size
return total
func get_total_scripts_size() -> int:
var total: int
for file_stats in scripts:
total += file_stats.size
return total
func get_other_files_size() -> int:
var total: int
for file_stats in misc:
total += file_stats.size
return total
func get_file_loader(file_path: String) -> FileStatistics:
match file_path.get_extension().to_lower():
"cs":
return CSharpStatistics.new()
"ini", "cfg":
return ConfigFileStatistics.new()
"gd":
return GDScriptStatistics.new()
"md":
return MarkdownStatistics.new()
"json":
return JSONStatistics.new()
"yml", "yaml":
return YAMLStatistics.new()
return ResourceStatistics.new()
func duplicate():
var clone = get_script().new()
clone.directories = directories
clone.scenes = scenes.duplicate()
clone.resources = resources.duplicate()
clone.scripts = scripts.duplicate()
clone.misc = misc.duplicate()
return clone
static func is_path_ignored(path: String) -> bool:
if not Engine.is_editor_hint():
return false
var ignores: PackedStringArray = ProjectSettings.get_setting("statistics/ignore")
for expression in ignores:
if path.matchn(expression):
return true
return false
static func path_ignored(path: String) -> bool:
#if not Engine.is_editor_hint():
#return false
var expressions: PackedStringArray = ProjectSettings.get_setting("statistics/ignore")
for expression in expressions:
if path.matchn(expression):
return true
return false
static func path_force_included(path: String) -> bool:
#if not Engine.is_editor_hint():
#return false
var expressions: PackedStringArray = ProjectSettings.get_setting("statistics/force_include")
for expression in expressions:
if path.matchn(expression):
return true
return false

View File

@ -0,0 +1,20 @@
extends "../FileStatistics.gd"
func _is_comment(line: String) -> bool:
line = line.strip_edges()
return (line.begins_with("//")
or line.begins_with("/*")
or line.begins_with("*")
or line.ends_with("*/"))
func _get_extension() -> String:
return "C#"
func _get_icon() -> String:
return "CSharpScript"
func _get_color() -> Color:
return Color.LIME_GREEN
func _is_script() -> bool:
return true

View File

@ -0,0 +1,13 @@
extends "../FileStatistics.gd"
func _get_extension() -> String:
return "Config File"
func _get_color() -> Color:
return Color.TEAL
func _get_icon() -> String:
return "File"
func _is_comment(line: String) -> bool:
return line.strip_edges().begins_with(";")

View File

@ -0,0 +1,17 @@
extends "../FileStatistics.gd"
func _is_comment(line: String) -> bool:
# TODO: Detect multi-line comments
return line.strip_edges().begins_with("#")
func _get_extension() -> String:
return "GDScript"
func _get_icon() -> String:
return "GDScript"
func _get_color() -> Color:
return Color.STEEL_BLUE
func _is_script() -> bool:
return true

View File

@ -0,0 +1,12 @@
extends "../FileStatistics.gd"
const ICON: Texture = preload("../../icons/json.svg")
func _get_extension() -> String:
return "JSON"
func _get_color() -> Color:
return Color.GOLD
func _get_icon() -> String:
return ICON.resource_path

View File

@ -0,0 +1,12 @@
extends "../FileStatistics.gd"
const ICON: Texture = preload("../../icons/markdown.svg")
func _get_extension() -> String:
return "Markdown"
func _get_color() -> Color:
return Color.GHOST_WHITE
func _get_icon() -> String:
return ICON.resource_path

View File

@ -0,0 +1,48 @@
extends "../FileStatistics.gd"
var local_to_scene: bool
var type: String
var base_node_type: String
var node_count: int
var connection_count: int
func _load_dir(at_path: String) -> int:
if ResourceLoader.exists(at_path):
var resource := ResourceLoader.load(at_path)
if resource:
_load_file_info(at_path, true)
_load_resource(resource)
return OK
return ERR_CANT_ACQUIRE_RESOURCE
func _load_resource(resource: Resource) -> void:
local_to_scene = resource.resource_local_to_scene
type = resource.get_class()
if resource is PackedScene:
var scene: PackedScene = resource as PackedScene
var state: SceneState = scene.get_state()
base_node_type = state.get_node_type(0)
node_count = state.get_node_count()
connection_count = state.get_connection_count()
func _get_extension() -> String:
return type
func _get_icon() -> String:
return type
func _get_color() -> Color:
if type == "VisualScript":
return Color.AZURE
var color = Color(hash(type)).inverted()
color.a = 1.0
return color
func _is_scene() -> bool:
return ClassDB.is_parent_class(type, "PackedScene")
func _is_script() -> bool:
return ClassDB.is_parent_class(type, "Script")
func _is_resource() -> bool:
return true

View File

@ -0,0 +1,15 @@
extends "../FileStatistics.gd"
const ICON: Texture = preload("../../icons/yaml.svg")
func _get_extension() -> String:
return "YAMl"
func _get_color() -> Color:
return Color.MEDIUM_PURPLE
func _get_icon() -> String:
return ICON.resource_path
func _is_comment(line: String) -> bool:
return line.strip_edges().begins_with("#")

View File

@ -0,0 +1,136 @@
@tool
extends "./TreeView.gd"
enum {
NAME_COLUMN,
EXTENSION_COLUMN,
TOTAL_LINES_COLUMN,
SOURCE_LINES_COLUMN,
COMMENT_LINES_COLUMN,
BLANK_LINES_COLUMN,
SIZE_COLUMN
}
var total_files: TreeItem
var total_lines: TreeItem
var total_code_lines: TreeItem
var total_comments_lines: TreeItem
var total_blank_lines: TreeItem
var total_size: TreeItem
@onready var graph: Control = $VSplitContainer/HSplitContainer/MarginContainer/PieGraph
func _ready() -> void:
tree = $VSplitContainer/Tree
summary_tree = $VSplitContainer/HSplitContainer/SummaryTree
tree.set_column_title(NAME_COLUMN, "File name")
tree.set_column_title(EXTENSION_COLUMN, "Extension")
tree.set_column_title(TOTAL_LINES_COLUMN, "Total lines")
tree.set_column_title(SOURCE_LINES_COLUMN, "Source lines")
tree.set_column_title(COMMENT_LINES_COLUMN, "Comment lines")
tree.set_column_title(BLANK_LINES_COLUMN, "Blank lines")
tree.set_column_title(SIZE_COLUMN, "Size")
tree.set_column_titles_visible(true)
tree.hide_root = true
tree.connect("item_activated", _on_item_activated)
tree.connect("column_title_clicked", _on_column_title_clicked)
var root: TreeItem = summary_tree.create_item()
total_files = summary_tree.create_item(root)
total_lines = summary_tree.create_item(root)
total_code_lines = summary_tree.create_item(root)
total_comments_lines = summary_tree.create_item(root)
total_blank_lines = summary_tree.create_item(root)
total_size = summary_tree.create_item(root)
total_files.set_text(0, "Total files")
total_lines.set_text(0, "Total lines")
total_code_lines.set_text(0, "Total code lines")
total_comments_lines.set_text(0, "Total comments lines")
total_blank_lines.set_text(0, "Total blank lines")
total_size.set_text(0, "Total size")
summary_tree.hide_root = true
func display(stats: ProjectStatistics) -> void:
super.display(stats)
total_files.set_text(1, str(stats.misc.size()))
total_lines.set_text(1, str(stats.get_total_lines(false)))
total_code_lines.set_text(1, str(stats.get_total_code_lines(false)))
total_comments_lines.set_text(1, str(stats.get_total_comment_lines(false)))
total_blank_lines.set_text(1, str(stats.get_total_blank_lines(false)))
total_size.set_text(1, String.humanize_size(stats.get_other_files_size()))
var series: Dictionary = {}
for file_stats in stats.misc:
var name: String = file_stats.get_name()
var extension: String = file_stats._get_extension()
var color: Color = file_stats._get_color()
var chart_data: ChartData
if not series.has(extension):
chart_data = ChartData.new()
chart_data.name = extension
chart_data.color = color
series[extension] = chart_data
else:
chart_data = series[extension]
chart_data.value += file_stats.size
graph.set_series(series)
func update_tree(stats: ProjectStatistics) -> void:
tree.clear()
var root: TreeItem = tree.create_item()
for file_stats in stats.misc:
var item: TreeItem = tree.create_item(root)
item.set_text(NAME_COLUMN, file_stats.get_name())
item.set_tooltip_text(NAME_COLUMN, file_stats.path)
item.set_metadata(NAME_COLUMN, file_stats.path)
item.set_text(EXTENSION_COLUMN, file_stats._get_extension())
item.set_metadata(EXTENSION_COLUMN, file_stats._get_icon())
item.set_text(TOTAL_LINES_COLUMN, str(file_stats.total_lines))
item.set_text(SOURCE_LINES_COLUMN, str(file_stats.source_code_lines))
item.set_text(COMMENT_LINES_COLUMN, str(file_stats.comment_lines))
item.set_text(BLANK_LINES_COLUMN, str(file_stats.blank_lines))
_format_size(item, SIZE_COLUMN, file_stats.size)
func update_icons() -> void:
var root: TreeItem = tree.get_root()
if not root:
return
var children:= root.get_children()
for next in children:
var icon_path: String = next.get_metadata(EXTENSION_COLUMN)
var icon: Texture
if has_theme_icon(icon_path, "EditorIcons"):
icon = get_theme_icon(icon_path, "EditorIcons")
elif ResourceLoader.exists(icon_path):
icon = ResourceLoader.load(icon_path)
next.set_icon(NAME_COLUMN, icon)
next.set_icon_max_width(NAME_COLUMN, 16)
next.set_icon(TOTAL_LINES_COLUMN, get_theme_icon("AnimationTrackList", "EditorIcons"))
next.set_icon(SOURCE_LINES_COLUMN, get_theme_icon("CodeFoldedRightArrow", "EditorIcons"))
next.set_icon(COMMENT_LINES_COLUMN, get_theme_icon("VisualShaderNodeComment", "EditorIcons"))
func _sort_by_column(column: int) -> void:
match column:
NAME_COLUMN:
stats.misc.sort_custom(sort_name)
EXTENSION_COLUMN:
stats.misc.sort_custom(sort_extension)
TOTAL_LINES_COLUMN:
stats.misc.sort_custom(sort_total_lines)
SOURCE_LINES_COLUMN:
stats.misc.sort_custom(sort_source_code_lines)
COMMENT_LINES_COLUMN:
stats.misc.sort_custom(sort_comment_lines)
BLANK_LINES_COLUMN:
stats.misc.sort_custom(sort_blank_lines)
SIZE_COLUMN:
stats.misc.sort_custom(sort_size)

View File

@ -0,0 +1,27 @@
[gd_scene load_steps=3 format=3 uid="uid://ddli62n5pmlvn"]
[ext_resource type="Script" path="res://addons/project-statistics/nodes/MiscView.gd" id="1"]
[ext_resource type="PackedScene" path="res://addons/project-statistics/nodes/ScriptsView.tscn" id="2"]
[node name="MiscView" instance=ExtResource("2")]
anchors_preset = 15
script = ExtResource("1")
[node name="VSplitContainer" parent="." index="0"]
layout_mode = 2
[node name="HSplitContainer" parent="VSplitContainer" index="0"]
layout_mode = 2
[node name="SummaryTree" parent="VSplitContainer/HSplitContainer" index="0"]
layout_mode = 2
[node name="MarginContainer" parent="VSplitContainer/HSplitContainer" index="1"]
layout_mode = 2
[node name="PieGraph" parent="VSplitContainer/HSplitContainer/MarginContainer" index="0"]
layout_mode = 2
[node name="Tree" parent="VSplitContainer" index="1"]
layout_mode = 2
column_titles_visible = true

View File

@ -0,0 +1,62 @@
@tool
extends "./StatisticsView.gd"
const BURNT_SIENNA: Color = Color("#EC6B56")
const CRAYOLA_MAIZE: Color = Color("#FFC154")
const KEPPEL: Color = Color("#47B39C")
var total_scenes: TreeItem
var total_resources: TreeItem
var total_scripts: TreeItem
var other_files: TreeItem
@onready var summary: Tree = $HSplitContainer/SummaryTree
@onready var graph: Control = $HSplitContainer/VBoxContainer/PieGraph
func _ready() -> void:
var root: TreeItem = summary.create_item()
total_scenes = summary.create_item(root)
total_resources = summary.create_item(root)
total_scripts = summary.create_item(root)
other_files = summary.create_item(root)
total_scenes.set_text(0, "Total scenes")
total_resources.set_text(0, "Total resources")
total_scripts.set_text(0, "Total scripts")
other_files.set_text(0, "Other files")
summary.hide_root = true
func display(stats: ProjectStatistics) -> void:
super.display(stats) #.display(stats)
total_scenes.set_text(1, str(stats.scenes.size()))
total_resources.set_text(1, str(stats.resources.size()))
total_scripts.set_text(1, str(stats.scripts.size()))
other_files.set_text(1, str(stats.misc.size()))
var series: Dictionary = {}
series["Scenes"] = _create_chart_data("Scenes", BURNT_SIENNA)
series["Resources"] = _create_chart_data("Resources", CRAYOLA_MAIZE)
series["Scripts"] = _create_chart_data("Scripts", KEPPEL)
series["Other"] = _create_chart_data("Other", Color.LIGHT_GRAY)
for file_stats in stats.scenes:
series["Scenes"].value += file_stats.size
for file_stats in stats.resources:
series["Resources"].value += file_stats.size
for file_stats in stats.scripts:
series["Scripts"].value += file_stats.size
for file_stats in stats.misc:
series["Other"].value += file_stats.size
graph.set_series(series)
func update_icons() -> void:
total_scenes.set_icon(0, get_theme_icon("PackedScene", "EditorIcons"))
total_resources.set_icon(0, get_theme_icon("Object", "EditorIcons"))
total_scripts.set_icon(0, get_theme_icon("Script", "EditorIcons"))
other_files.set_icon(0, get_theme_icon("File", "EditorIcons"))
func _create_chart_data(name: String, color: Color) -> ChartData:
var data = ChartData.new()
data.name = name
data.color = color
return data

View File

@ -0,0 +1,28 @@
[gd_scene load_steps=3 format=3 uid="uid://03754kkjn2ue"]
[ext_resource type="PackedScene" uid="uid://by7ltrt0iq35i" path="res://addons/project-statistics/nodes/charts/PieGraph.tscn" id="1"]
[ext_resource type="Script" path="res://addons/project-statistics/nodes/Overview.gd" id="3"]
[node name="Overview" type="MarginContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("3")
[node name="HSplitContainer" type="HSplitContainer" parent="."]
layout_mode = 2
size_flags_vertical = 3
[node name="SummaryTree" type="Tree" parent="HSplitContainer"]
layout_mode = 2
size_flags_horizontal = 3
columns = 2
hide_root = true
[node name="VBoxContainer" type="VBoxContainer" parent="HSplitContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PieGraph" parent="HSplitContainer/VBoxContainer" instance=ExtResource("1")]
layout_mode = 2
size_flags_vertical = 1

View File

@ -0,0 +1,5 @@
@tool
extends PanelContainer
func _ready() -> void:
set("custom_styles/panel", get_theme_stylebox("Content", "EditorStyles"))

View File

@ -0,0 +1,96 @@
@tool
extends "./TreeView.gd"
enum {
NAME_COLUMN,
RESOURCE_TYPE_COLUMN,
LOCAL_TO_SCENE_COLUMN,
SIZE_COLUMN
}
var total_resources: TreeItem
var total_size: TreeItem
@onready var graph: Control = $VSplitContainer/HSplitContainer/MarginContainer/PieGraph
func _ready() -> void:
tree = $VSplitContainer/Tree
summary_tree = $VSplitContainer/HSplitContainer/SummaryTree
tree.set_column_title(NAME_COLUMN, "File name")
tree.set_column_title(RESOURCE_TYPE_COLUMN, "Type")
tree.set_column_title(LOCAL_TO_SCENE_COLUMN, "Local to scene")
tree.set_column_title(SIZE_COLUMN, "Size")
tree.set_column_titles_visible(true)
tree.hide_root = true
tree.connect("item_activated", _on_item_activated)
tree.connect("column_title_clicked", _on_column_title_clicked)
var root: TreeItem = summary_tree.create_item()
total_resources = summary_tree.create_item(root)
total_size = summary_tree.create_item(root)
total_resources.set_text(0, "Total resources")
total_size.set_text(0, "Total size")
summary_tree.hide_root = true
func display(stats: ProjectStatistics) -> void:
#.display(stats)
super.display(stats)
total_resources.set_text(1, str(stats.resources.size()))
total_size.set_text(1, String.humanize_size(stats.get_total_scripts_size()))
var series: Dictionary = {}
for file_stats in stats.resources:
var name: String = file_stats.get_name()
var extension: String = file_stats._get_extension()
var color: Color = file_stats._get_color()
var chart_data: ChartData
if not series.has(extension):
chart_data = ChartData.new()
chart_data.name = extension
chart_data.color = color
series[extension] = chart_data
else:
chart_data = series[extension]
chart_data.value += file_stats.size
graph.set_series(series)
func update_tree(stats: ProjectStatistics) -> void:
tree.clear()
var root: TreeItem = tree.create_item()
for file_stats in stats.resources:
var item: TreeItem = tree.create_item(root)
item.set_cell_mode(LOCAL_TO_SCENE_COLUMN, TreeItem.CELL_MODE_CHECK)
item.set_text(NAME_COLUMN, file_stats.get_name())
item.set_tooltip_text(NAME_COLUMN, file_stats.path)
item.set_metadata(NAME_COLUMN, file_stats.path)
item.set_text(RESOURCE_TYPE_COLUMN, file_stats.type)
item.set_checked(LOCAL_TO_SCENE_COLUMN, file_stats.local_to_scene)
_format_size(item, SIZE_COLUMN, file_stats.size)
func update_icons() -> void:
var root: TreeItem = tree.get_root()
if not root:
return
var children:= root.get_children()
for next in children:
next.set_icon(NAME_COLUMN, get_theme_icon(next.get_text(RESOURCE_TYPE_COLUMN), "EditorIcons"))
next.set_icon(RESOURCE_TYPE_COLUMN, get_theme_icon("ClassList", "EditorIcons"))
func _sort_by_column(column: int) -> void:
match column:
NAME_COLUMN:
stats.resources.sort_custom(sort_name)
RESOURCE_TYPE_COLUMN:
stats.resources.sort_custom(sort_resource_type)
LOCAL_TO_SCENE_COLUMN:
stats.resources.sort_custom(sort_local_to_scene)
SIZE_COLUMN:
stats.resources.sort_custom(sort_size)

View File

@ -0,0 +1,28 @@
[gd_scene load_steps=3 format=3 uid="uid://vcehrmbfpknm"]
[ext_resource type="Script" path="res://addons/project-statistics/nodes/ResourcesView.gd" id="1"]
[ext_resource type="PackedScene" path="res://addons/project-statistics/nodes/ScriptsView.tscn" id="2"]
[node name="ResourcesView" instance=ExtResource("2")]
anchors_preset = 15
script = ExtResource("1")
[node name="VSplitContainer" parent="." index="0"]
layout_mode = 2
[node name="HSplitContainer" parent="VSplitContainer" index="0"]
layout_mode = 2
[node name="SummaryTree" parent="VSplitContainer/HSplitContainer" index="0"]
layout_mode = 2
[node name="MarginContainer" parent="VSplitContainer/HSplitContainer" index="1"]
layout_mode = 2
[node name="PieGraph" parent="VSplitContainer/HSplitContainer/MarginContainer" index="0"]
layout_mode = 2
[node name="Tree" parent="VSplitContainer" index="1"]
layout_mode = 2
columns = 4
column_titles_visible = true

View File

@ -0,0 +1,97 @@
@tool
extends "./TreeView.gd"
enum {
NAME_COLUMN,
BASE_NODE_TYPE_COLUMN,
NODE_COUNT_COLUMN,
NODE_CONNECTIONS_COLUMN,
LOCAL_TO_SCENE_COLUMN,
SIZE_COLUMN
}
var total_scenes: TreeItem
var total_nodes: TreeItem
var total_connections: TreeItem
var total_size: TreeItem
func _ready() -> void:
tree = $VSplitContainer/Tree as Tree
summary_tree = $VSplitContainer/SummaryTree as Tree
tree.set_column_title(NAME_COLUMN, "Scene")
tree.set_column_title(BASE_NODE_TYPE_COLUMN, "Base node")
tree.set_column_title(NODE_COUNT_COLUMN, "Node count")
tree.set_column_title(NODE_CONNECTIONS_COLUMN, "Node connection count")
tree.set_column_title(LOCAL_TO_SCENE_COLUMN, "Local to scene")
tree.set_column_title(SIZE_COLUMN, "Size")
tree.set_column_titles_visible(true)
tree.hide_root = true
tree.item_activated.connect(_on_item_activated)
tree.column_title_clicked.connect(_on_column_title_clicked)
var root: TreeItem = summary_tree.create_item()
total_scenes = summary_tree.create_item(root)
total_nodes = summary_tree.create_item(root)
total_connections = summary_tree.create_item(root)
total_size = summary_tree.create_item(root)
total_scenes.set_text(0, "Total scenes")
total_nodes.set_text(0, "Total nodes")
total_connections.set_text(0, "Total connections")
total_size.set_text(0, "Total size")
summary_tree.hide_root = true
func display(stats: ProjectStatistics) -> void:
super.display(stats)
total_scenes.set_text(1, str(stats.scenes.size()))
total_nodes.set_text(1, str(stats.get_total_nodes()))
total_connections.set_text(1, str(stats.get_total_connections()))
total_size.set_text(1, String.humanize_size(stats.get_total_scenes_size()))
func update_tree(stats: ProjectStatistics) -> void:
tree.clear()
var root: TreeItem = tree.create_item()
for file_stats in stats.scenes:
var item: TreeItem = tree.create_item(root)
item.set_cell_mode(LOCAL_TO_SCENE_COLUMN, TreeItem.CELL_MODE_CHECK)
item.set_text(NAME_COLUMN, file_stats.get_name())
item.set_tooltip_text(NAME_COLUMN, file_stats.path)
item.set_metadata(NAME_COLUMN, file_stats.path)
item.set_text(BASE_NODE_TYPE_COLUMN, file_stats.base_node_type)
item.set_metadata(BASE_NODE_TYPE_COLUMN, file_stats.base_node_type)
item.set_text(NODE_COUNT_COLUMN, str(file_stats.node_count))
item.set_text(NODE_CONNECTIONS_COLUMN, str(file_stats.connection_count))
item.set_checked(LOCAL_TO_SCENE_COLUMN, file_stats.local_to_scene)
_format_size(item, SIZE_COLUMN, file_stats.size)
func update_icons() -> void:
var root: TreeItem = tree.get_root()
if not root:
return
var children:= root.get_children()
for next in children:
next.set_icon(NAME_COLUMN, get_theme_icon("PackedScene", "EditorIcons"))
next.set_icon(BASE_NODE_TYPE_COLUMN, get_theme_icon(next.get_metadata(BASE_NODE_TYPE_COLUMN), "EditorIcons"))
next.set_icon(NODE_COUNT_COLUMN, get_theme_icon("Node", "EditorIcons"))
next.set_icon(NODE_CONNECTIONS_COLUMN, get_theme_icon("Slot", "EditorIcons"))
func _sort_by_column(column: int) -> void:
match column:
NAME_COLUMN:
stats.scenes.sort_custom(sort_name)
BASE_NODE_TYPE_COLUMN:
stats.scenes.sort_custom(sort_node_type)
NODE_COUNT_COLUMN:
stats.scenes.sort_custom(sort_node_count)
NODE_CONNECTIONS_COLUMN:
stats.scenes.sort_custom(sort_connection_count)
LOCAL_TO_SCENE_COLUMN:
stats.scenes.sort_custom(sort_local_to_scene)
SIZE_COLUMN:
stats.scenes.sort_custom(sort_size)

View File

@ -0,0 +1,28 @@
[gd_scene load_steps=2 format=3 uid="uid://dfmtk6v2uv63o"]
[ext_resource type="Script" path="res://addons/project-statistics/nodes/ScenesView.gd" id="1"]
[node name="ScenesView" type="MarginContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("1")
[node name="VSplitContainer" type="VSplitContainer" parent="."]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="SummaryTree" type="Tree" parent="VSplitContainer"]
custom_minimum_size = Vector2(2.08165e-12, 95)
layout_mode = 2
columns = 2
hide_root = true
[node name="Tree" type="Tree" parent="VSplitContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
columns = 6
column_titles_visible = true
hide_root = true

View File

@ -0,0 +1,135 @@
@tool
extends "./TreeView.gd"
enum {
NAME_COLUMN,
LANGUAGE_COLUMN,
TOTAL_LINES_COLUMN,
CODE_LINES_COLUMN,
COMMENT_LINES_COLUMN,
BLANK_LINES_COLUMN,
SIZE_COLUMN
}
var total_scripts: TreeItem
var used_languages: TreeItem
var total_lines: TreeItem
var total_code_lines: TreeItem
var total_comments_lines: TreeItem
var total_blank_lines: TreeItem
var total_size: TreeItem
@onready var graph: Control = $VSplitContainer/HSplitContainer/MarginContainer/PieGraph
func _ready() -> void:
tree = $VSplitContainer/Tree
summary_tree = $VSplitContainer/HSplitContainer/SummaryTree
tree.set_column_title(NAME_COLUMN, "File name")
tree.set_column_title(LANGUAGE_COLUMN, "Language")
tree.set_column_title(TOTAL_LINES_COLUMN, "Total lines")
tree.set_column_title(CODE_LINES_COLUMN, "Code lines")
tree.set_column_title(COMMENT_LINES_COLUMN, "Comment lines")
tree.set_column_title(BLANK_LINES_COLUMN, "Blank lines")
tree.set_column_title(SIZE_COLUMN, "Size")
tree.set_column_titles_visible(true)
tree.hide_root = true
tree.connect("item_activated", _on_item_activated)
tree.connect("column_title_clicked", _on_column_title_clicked)
var root: TreeItem = summary_tree.create_item()
total_scripts = summary_tree.create_item(root)
used_languages = summary_tree.create_item(root)
total_lines = summary_tree.create_item(root)
total_code_lines = summary_tree.create_item(root)
total_comments_lines = summary_tree.create_item(root)
total_blank_lines = summary_tree.create_item(root)
total_size = summary_tree.create_item(root)
total_scripts.set_text(0, "Total scripts")
used_languages.set_text(0, "Used languages")
total_lines.set_text(0, "Total lines")
total_code_lines.set_text(0, "Total code lines")
total_comments_lines.set_text(0, "Total comments lines")
total_blank_lines.set_text(0, "Total blank lines")
total_size.set_text(0, "Total size")
summary_tree.hide_root = true
func display(stats: ProjectStatistics) -> void:
super.display(stats)
total_scripts.set_text(1, str(stats.scripts.size()))
used_languages.set_text(1, ", ".join(stats.get_used_langauges()))
total_lines.set_text(1, str(stats.get_total_lines()))
total_code_lines.set_text(1, str(stats.get_total_code_lines()))
total_comments_lines.set_text(1, str(stats.get_total_comment_lines()))
total_blank_lines.set_text(1, str(stats.get_total_blank_lines()))
total_size.set_text(1, String.humanize_size(stats.get_total_scripts_size()))
var series: Dictionary = {}
for file_stats in stats.scripts:
var name: String = file_stats.get_name()
var extension: String = file_stats._get_extension()
var color: Color = file_stats._get_color()
var chart_data :ChartData
if not series.has(extension):
chart_data = ChartData.new()
chart_data.name = extension
chart_data.color = color
series[extension] = chart_data
else:
chart_data = series[extension]
chart_data.value += file_stats.size
graph.set_series(series)
func update_tree(stats: ProjectStatistics) -> void:
tree.clear()
var root: TreeItem = tree.create_item()
for file_stats:FileStatistics in stats.scripts:
var item: TreeItem = tree.create_item(root)
item.set_text(NAME_COLUMN, file_stats.get_name())
item.set_tooltip_text(NAME_COLUMN, file_stats.path)
item.set_metadata(NAME_COLUMN, file_stats.path)
item.set_text(LANGUAGE_COLUMN, file_stats._get_extension())
item.set_metadata(LANGUAGE_COLUMN, file_stats._get_icon())
item.set_text(TOTAL_LINES_COLUMN, str(file_stats.total_lines))
item.set_text(CODE_LINES_COLUMN, str(file_stats.source_code_lines))
item.set_text(COMMENT_LINES_COLUMN, str(file_stats.comment_lines))
item.set_text(BLANK_LINES_COLUMN, str(file_stats.blank_lines))
_format_size(item, SIZE_COLUMN, file_stats.size)
func update_icons() -> void:
var root: TreeItem = tree.get_root()
if not root:
return
var children:= root.get_children()
for next in children:
next.set_icon(NAME_COLUMN, get_theme_icon(next.get_metadata(LANGUAGE_COLUMN), "EditorIcons"))
next.set_icon(LANGUAGE_COLUMN, get_theme_icon("Script", "EditorIcons"))
next.set_icon(TOTAL_LINES_COLUMN, get_theme_icon("AnimationTrackList", "EditorIcons"))
next.set_icon(CODE_LINES_COLUMN, get_theme_icon("CodeFoldedRightArrow", "EditorIcons"))
next.set_icon(COMMENT_LINES_COLUMN, get_theme_icon("VisualShaderNodeComment", "EditorIcons"))
func _sort_by_column(column: int) -> void:
match column:
NAME_COLUMN:
stats.scripts.sort_custom(sort_name)
LANGUAGE_COLUMN:
stats.scripts.sort_custom(sort_extension)
TOTAL_LINES_COLUMN:
stats.scripts.sort_custom(sort_total_lines)
CODE_LINES_COLUMN:
stats.scripts.sort_custom(sort_source_code_lines)
COMMENT_LINES_COLUMN:
stats.scripts.sort_custom(sort_comment_lines)
BLANK_LINES_COLUMN:
stats.scripts.sort_custom(sort_blank_lines)
SIZE_COLUMN:
stats.scripts.sort_custom(sort_size)

View File

@ -0,0 +1,53 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://addons/project-statistics/nodes/charts/PieGraph.tscn" type="PackedScene" id=1]
[ext_resource path="res://addons/project-statistics/nodes/ScriptsView.gd" type="Script" id=2]
[node name="ScriptsView" type="MarginContainer"]
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 2 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="VSplitContainer" type="VSplitContainer" parent="."]
margin_right = 1024.0
margin_bottom = 600.0
[node name="HSplitContainer" type="HSplitContainer" parent="VSplitContainer"]
margin_right = 1024.0
margin_bottom = 180.0
rect_min_size = Vector2( 0, 175 )
[node name="SummaryTree" type="Tree" parent="VSplitContainer/HSplitContainer"]
margin_right = 506.0
margin_bottom = 180.0
rect_min_size = Vector2( 0, 180 )
size_flags_horizontal = 3
size_flags_vertical = 3
columns = 2
hide_root = true
[node name="MarginContainer" type="MarginContainer" parent="VSplitContainer/HSplitContainer"]
margin_left = 518.0
margin_right = 1024.0
margin_bottom = 180.0
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="PieGraph" parent="VSplitContainer/HSplitContainer/MarginContainer" instance=ExtResource( 1 )]
margin_right = 506.0
margin_bottom = 180.0
[node name="Tree" type="Tree" parent="VSplitContainer"]
margin_top = 192.0
margin_right = 1024.0
margin_bottom = 600.0
size_flags_horizontal = 3
size_flags_vertical = 3
columns = 7
hide_root = true
__meta__ = {
"_edit_use_anchors_": false
}

View File

@ -0,0 +1,36 @@
@tool
extends Control
class_name StatisticsPreview
const ProjectStatistics: Script = preload("../loaders/ProjectStatistics.gd")
var editor_interface: EditorInterface
@onready var overview: Control = $VSplitContainer/ScrollContainer/MarginContainer/TabContainer/Overview
@onready var scenes_view: Control = $VSplitContainer/ScrollContainer/MarginContainer/TabContainer/Scenes
@onready var resources_view: Control = $VSplitContainer/ScrollContainer/MarginContainer/TabContainer/Resources
@onready var scripts_view: Control = $VSplitContainer/ScrollContainer/MarginContainer/TabContainer/Scripts
@onready var misc_view: Control = $VSplitContainer/ScrollContainer/MarginContainer/TabContainer/Misc
func _on_refresh_pressed() -> void:
var stats: ProjectStatistics = ProjectStatistics.new()
stats._load_dir()
overview.display(stats)
scenes_view.display(stats)
resources_view.display(stats)
scripts_view.display(stats)
misc_view.display(stats)
func _on_file_selected(path: String) -> void:
if Engine.is_editor_hint():
editor_interface.select_file(path)
if not ResourceLoader.exists(path):
return
var resource: Resource = ResourceLoader.load(path)
if resource:
if resource is PackedScene:
editor_interface.open_scene_from_path(path)
else:
editor_interface.edit_resource(resource)

View File

@ -0,0 +1,82 @@
[gd_scene load_steps=7 format=3 uid="uid://bfqwgtk1grusq"]
[ext_resource type="Script" path="res://addons/project-statistics/nodes/StatisticsPreview.gd" id="1"]
[ext_resource type="PackedScene" uid="uid://03754kkjn2ue" path="res://addons/project-statistics/nodes/Overview.tscn" id="2"]
[ext_resource type="PackedScene" uid="uid://dfmtk6v2uv63o" path="res://addons/project-statistics/nodes/ScenesView.tscn" id="3"]
[ext_resource type="PackedScene" uid="uid://vcehrmbfpknm" path="res://addons/project-statistics/nodes/ResourcesView.tscn" id="4"]
[ext_resource type="PackedScene" path="res://addons/project-statistics/nodes/ScriptsView.tscn" id="5"]
[ext_resource type="PackedScene" uid="uid://ddli62n5pmlvn" path="res://addons/project-statistics/nodes/MiscView.tscn" id="6"]
[node name="StatisticsPreview" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1")
[node name="VSplitContainer" type="VSplitContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
dragger_visibility = 2
[node name="MarginContainer" type="MarginContainer" parent="VSplitContainer"]
layout_mode = 2
[node name="HSplitContainer" type="HSplitContainer" parent="VSplitContainer/MarginContainer"]
layout_mode = 2
dragger_visibility = 2
[node name="Label" type="Label" parent="VSplitContainer/MarginContainer/HSplitContainer"]
layout_mode = 2
text = "Project statistics:"
[node name="HBoxContainer" type="HBoxContainer" parent="VSplitContainer/MarginContainer/HSplitContainer"]
layout_mode = 2
size_flags_vertical = 0
alignment = 2
[node name="RefreshButton" type="Button" parent="VSplitContainer/MarginContainer/HSplitContainer/HBoxContainer"]
layout_mode = 2
text = "Refresh"
[node name="ScrollContainer" type="ScrollContainer" parent="VSplitContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="MarginContainer" type="MarginContainer" parent="VSplitContainer/ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Panel" type="Panel" parent="VSplitContainer/ScrollContainer/MarginContainer"]
layout_mode = 2
[node name="TabContainer" type="TabContainer" parent="VSplitContainer/ScrollContainer/MarginContainer"]
layout_mode = 2
[node name="Overview" parent="VSplitContainer/ScrollContainer/MarginContainer/TabContainer" instance=ExtResource("2")]
layout_mode = 2
[node name="Scenes" parent="VSplitContainer/ScrollContainer/MarginContainer/TabContainer" instance=ExtResource("3")]
visible = false
layout_mode = 2
[node name="Resources" parent="VSplitContainer/ScrollContainer/MarginContainer/TabContainer" instance=ExtResource("4")]
visible = false
layout_mode = 2
[node name="Scripts" parent="VSplitContainer/ScrollContainer/MarginContainer/TabContainer" instance=ExtResource("5")]
visible = false
layout_mode = 2
[node name="Misc" parent="VSplitContainer/ScrollContainer/MarginContainer/TabContainer" instance=ExtResource("6")]
visible = false
layout_mode = 2
[connection signal="pressed" from="VSplitContainer/MarginContainer/HSplitContainer/HBoxContainer/RefreshButton" to="." method="_on_refresh_pressed"]
[connection signal="file_selected" from="VSplitContainer/ScrollContainer/MarginContainer/TabContainer/Resources" to="." method="_on_file_selected"]
[connection signal="file_selected" from="VSplitContainer/ScrollContainer/MarginContainer/TabContainer/Scripts" to="." method="_on_file_selected"]
[connection signal="file_selected" from="VSplitContainer/ScrollContainer/MarginContainer/TabContainer/Misc" to="." method="_on_file_selected"]

View File

@ -0,0 +1,33 @@
@tool
extends Control
signal file_selected(path)
const ProjectStatistics: Script = preload("../loaders/ProjectStatistics.gd")
const FileStatistics: Script = preload("../loaders/FileStatistics.gd")
const PieChart: Script = preload("./charts/PieChart.gd")
const ChartData: Script = preload("./charts/ChartData.gd")
var stats: ProjectStatistics
func display(stats: ProjectStatistics) -> void:
self.stats = stats
await get_tree().process_frame
update_icons()
func update_icons() -> void:
pass
func _notification(what: int) -> void:
match what:
NOTIFICATION_THEME_CHANGED:
call_deferred("update_icons")
static func _format_size(item: TreeItem, column: int, size: int) -> void:
var size_text: PackedStringArray = String.humanize_size(size).split(" ")
var size_value: String = size_text[0]
var size_unit: String = size_text[1]
item.set_text(column, size_value)
item.set_suffix(column, size_unit)
item.set_tooltip_text(column, str(size) + " bytes")
item.set_text_alignment(column, HORIZONTAL_ALIGNMENT_CENTER)

View File

@ -0,0 +1,79 @@
@tool
extends "./StatisticsView.gd"
@onready var tree: Tree
@onready var summary_tree: Tree
func display(stats: ProjectStatistics) -> void:
#.display(stats)
super.display(stats)
update_tree(stats)
func update_tree(stats: ProjectStatistics) -> void:
pass
func _on_item_activated() -> void:
if tree.get_selected_column() == 0:
var path: String = tree.get_selected().get_metadata(0)
emit_signal("file_selected", path)
func _on_column_title_clicked(column: int, _mouse_button: int) -> void:
if stats:
stats = stats.duplicate()
_sort_by_column(column)
update_tree(stats)
update_icons()
func _sort_by_column(column: int) -> void:
pass
func sort_name(file1: FileStatistics, file2: FileStatistics) -> bool:
var result: int = file2.get_name().casecmp_to(file1.get_name())
return result == 1 if result != 0 else sort_path(file1, file2)
func sort_path(file1: FileStatistics, file2: FileStatistics) -> bool:
return file2.path.casecmp_to(file1.path) == 1
func sort_extension(file1: FileStatistics, file2: FileStatistics) -> bool:
var result: int = file2._get_extension().casecmp_to(file1._get_extension())
return result == 1 if result != 0 else sort_path(file1, file2)
func sort_resource_type(file1: FileStatistics, file2: FileStatistics) -> bool:
var result: int = file2.type.casecmp_to(file1.type)
return result == 1 if result != 0 else sort_path(file1, file2)
func sort_node_type(file1: FileStatistics, file2: FileStatistics) -> bool:
var result: int = file2.base_node_type.casecmp_to(file1.base_node_type)
return result == 1 if result != 0 else sort_path(file1, file2)
func sort_size(file1: FileStatistics, file2: FileStatistics) -> bool:
return file1.size > file2.size if file1.size != file2.size else sort_name(file1, file2)
func sort_total_lines(file1: FileStatistics, file2: FileStatistics) -> bool:
return file1.total_lines > file2.total_lines if file1.total_lines != file2.total_lines else sort_source_code_lines(file1, file2)
func sort_source_code_lines(file1: FileStatistics, file2: FileStatistics) -> bool:
return file1.source_code_lines > file2.source_code_lines if file1.source_code_lines != file2.source_code_lines else sort_comment_lines(file1, file2)
func sort_comment_lines(file1: FileStatistics, file2: FileStatistics) -> bool:
return file1.comment_lines > file2.comment_lines if file1.comment_lines != file2.comment_lines else sort_blank_lines(file1, file2)
func sort_blank_lines(file1: FileStatistics, file2: FileStatistics) -> bool:
return file1.blank_lines > file2.blank_lines if file1.blank_lines != file2.blank_lines else sort_size(file1, file2)
func sort_node_count(file1: FileStatistics, file2: FileStatistics) -> bool:
return file1.node_count > file2.node_count if file1.node_count != file2.node_count else sort_connection_count(file1, file2)
func sort_connection_count(file1: FileStatistics, file2: FileStatistics) -> bool:
return file1.connection_count > file2.connection_count if file1.connection_count != file2.connection_count else sort_name(file1, file2)
func sort_local_to_scene(file1: FileStatistics, file2: FileStatistics) -> bool:
return file1.local_to_scene if not file1.local_to_scene and file2.local_to_scene else sort_name(file1, file2)
func _notification(what: int) -> void:
match what:
NOTIFICATION_PREDELETE:
if is_instance_valid(tree):
tree.clear()
if is_instance_valid(tree):
summary_tree.clear()

View File

@ -0,0 +1,3 @@
var name: String
var value: float
var color: Color = Color.GRAY

View File

@ -0,0 +1,73 @@
@tool
extends Control
const ChartData: Script = preload("./ChartData.gd")
@export var radius := 50.0
var series: Dictionary
func _init() -> void:
#rect_clip_content = true
clip_contents = true
func clear() -> void:
series.clear()
queue_redraw()
func add_data(data: ChartData) -> void:
series[data.name] = data
queue_redraw()
func remove_data(name: String) -> void:
series.erase(name)
queue_redraw()
func set_radius(value: float) -> void:
radius = max(0, value)
self.rect_min_size = Vector2.ONE * radius * 2
func draw_filled_arc(center: Vector2, radius: float, start_angle: float, end_angle: float,
point_count: int, color: Color, antialiased: bool = false
) -> void:
var points: PackedVector2Array = []
points.push_back(center)
for i in range(point_count + 1):
var angle_point: float = start_angle + i * (end_angle - start_angle) / point_count
points.push_back(center + Vector2(cos(angle_point), sin(angle_point)) * radius)
#draw_polygon(points, [color], [], null, null, antialiased)
draw_polygon(points, [color])
func _draw() -> void:
var charts_data: Array = series.values()
var total: float = 0.0
for data in charts_data:
total += data.value
var center: Vector2 = get_rect().size / 2.0
var start_angle: float = 0.0
var i: int
for data in charts_data:
var percent: float = data.value / total
var end_angle: float = start_angle - percent * 2 * PI
draw_filled_arc(
center,
radius,
start_angle,
end_angle,
30 * percent + 2,
data.color,
true
)
start_angle = end_angle
var outline_width: float = 1.2
if not get_theme_constant("dark_theme", "Editor"):
draw_arc(center, radius - outline_width / 2, 0.0, 2 * PI, 32, Color.LIGHT_SLATE_GRAY, 1.5, true)
func _notification(what: int) -> void:
match what:
NOTIFICATION_THEME_CHANGED:
queue_redraw()

View File

@ -0,0 +1,49 @@
@tool
extends VSplitContainer
const ChartData: Script = preload("./ChartData.gd")
const PieChart: Script = preload("./PieChart.gd")
const CIRCLE_ICON: Texture = preload("../../icons/circle.svg")
@onready var chart: PieChart = $PieChart
@onready var key_tree: Tree = $KeyTree
func set_series(series: Dictionary) -> void:
chart.series = series
chart.queue_redraw()
key_tree.clear()
var root: TreeItem = key_tree.create_item()
var values: Array = series.values()
values.sort_custom(_sort_series)
var total: float = _get_total(values)
for data in values:
var item: TreeItem = key_tree.create_item(root)
item.set_text(0, data.name)
item.set_icon(0, CIRCLE_ICON)
item.set_icon_modulate(0, data.color)
item.set_icon_max_width(0, 8)
item.set_text(1, "%05.2f" % (data.value / total * 100))
item.set_suffix(1, "%")
chart.series = series
chart.queue_redraw()
func _get_total(series: Array) -> float:
var total: float
for data in series:
total += data.value
return total
func _sort_series(data1: ChartData, data2: ChartData) -> bool:
return data1.value > data2.value
func _notification(what: int) -> void:
match what:
NOTIFICATION_RESIZED, NOTIFICATION_SORT_CHILDREN:
if chart:
chart.radius = min(chart.get_rect().size.x, chart.get_rect().size.y) / 2 * 0.9

View File

@ -0,0 +1,29 @@
[gd_scene load_steps=3 format=3 uid="uid://by7ltrt0iq35i"]
[ext_resource type="Script" path="res://addons/project-statistics/nodes/charts/PieChart.gd" id="1"]
[ext_resource type="Script" path="res://addons/project-statistics/nodes/charts/PieGraph.gd" id="2"]
[node name="PieGraph" type="VSplitContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_vertical = 3
script = ExtResource("2")
[node name="PieChart" type="Control" parent="."]
clip_contents = true
custom_minimum_size = Vector2(120, 120)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource("1")
[node name="KeyTree" type="Tree" parent="."]
custom_minimum_size = Vector2(2.08165e-12, 100)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 8
columns = 2
hide_root = true

View File

@ -0,0 +1,7 @@
[plugin]
name="Statistics"
description="Shows your project statistics"
author="Abdera7mane"
version="0.1"
script="plugin.gd"

View File

@ -0,0 +1,42 @@
@tool
extends EditorPlugin
const IGNORE_PROPERTY: String = "statistics/ignore"
const FORCE_INCLUDE_PROPERTY: String = "statistics/force_include"
var DEFAULT_IGNORE = PackedStringArray([
"res://.import/*",
"res://.github/*",
"res://addons/*",
"*.import"
])
var statistics_preview: PackedScene = preload("./nodes/StatisticsPreview.tscn")
var preview: StatisticsPreview
func _enter_tree() -> void:
preview = statistics_preview.instantiate()
preview.editor_interface = get_editor_interface()
add_control_to_bottom_panel(preview, "Statistics")
_setup()
func _exit_tree() -> void:
remove_control_from_bottom_panel(preview)
preview.editor_interface = null
func _setup() -> void:
if not ProjectSettings.has_setting(IGNORE_PROPERTY):
ProjectSettings.set_setting(IGNORE_PROPERTY, DEFAULT_IGNORE)
ProjectSettings.add_property_info({
name = IGNORE_PROPERTY,
type = TYPE_PACKED_STRING_ARRAY
})
ProjectSettings.set_initial_value(IGNORE_PROPERTY, DEFAULT_IGNORE)
if not ProjectSettings.has_setting(FORCE_INCLUDE_PROPERTY):
ProjectSettings.set_setting(FORCE_INCLUDE_PROPERTY, PackedStringArray())
ProjectSettings.add_property_info({
name = FORCE_INCLUDE_PROPERTY,
type = TYPE_PACKED_STRING_ARRAY
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View File

@ -0,0 +1,9 @@
# MIT License
Copyright 2022 Gennady "Don Tnowe" Krupenyov
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,6 @@
@tool
extends Control
func _ready():
modulate = get_theme_color("accent_color", "Editor")

View File

@ -0,0 +1,12 @@
@tool
extends Button
@export var icon_name := "Node" :
set(v):
icon_name = v
if has_theme_icon(v, "EditorIcons"):
icon = get_theme_icon(v, "EditorIcons")
func _ready():
self.icon_name = (icon_name)

View File

@ -0,0 +1,458 @@
@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 : Object
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 remembered_paths_total_count := 0
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 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_filesystem_changed():
var file_total_count := _get_file_count_recursive(current_path)
if file_total_count != remembered_paths_total_count:
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 _get_file_count_recursive(path : String) -> int:
var editor_fs : EditorFileSystem = editor_interface.get_resource_filesystem()
var path_dir := editor_fs.get_filesystem_path(path)
if !path_dir: return 0
var file_total_count := 0
var folder_stack : Array[EditorFileSystemDirectory] = [path_dir]
while folder_stack.size() > 0:
path_dir = folder_stack.pop_back()
file_total_count += path_dir.get_file_count()
for i in path_dir.get_subdir_count():
folder_stack.append(path_dir.get_subdir(i))
return file_total_count
func display_folder(folderpath : String, sort_by : StringName = "", sort_reverse : bool = false, force_rebuild : bool = false, is_echo : bool = false):
if folderpath == "":
# You wouldn't just wanna edit ALL resources in the project, that's a long time to load!
return
if sort_by == "":
sort_by = &"resource_path"
$"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
if (
force_rebuild
or current_path != folderpath
or columns.size() != node_columns.get_child_count()
):
node_table_root.columns = columns.size()
for x in node_table_root.get_children():
x.free()
node_columns.columns = columns
var cells_left_to_free : int = node_table_root.get_child_count() - (last_row - first_row) * columns.size()
while cells_left_to_free > 0:
node_table_root.get_child(0).free()
cells_left_to_free -= 1
var color_rows : bool = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "color_rows")
for i in last_row - first_row:
_update_row(first_row + i, color_rows)
current_path = folderpath
remembered_paths_total_count = _get_file_count_recursive(folderpath)
node_columns.update()
grid_updated.emit()
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 : StringName, 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
remembered_paths.clear()
rows = io.import_from_path(path, insert_row_sorted, sort_by, sort_reverse)
func fill_property_data(res : Resource):
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 fill_property_data_many(resources : Array):
columns.clear()
column_types.clear()
column_hints.clear()
column_hint_strings.clear()
var column_values := []
var i := -1
var found_props := {}
for x in resources:
if x == null: continue
for y in x.get_property_list():
found_props[y[&"name"]] = y
y[&"owner_object"] = x
for x in found_props.values():
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(x[&"owner_object"], columns[i]))
_selection.initialize_editors(column_values, column_types, column_hints)
func insert_row_sorted(res : Resource, loaded_rows : Array, sort_by : StringName, sort_reverse : bool):
if search_cond != null and not search_cond.can_show(res, loaded_rows.size()):
return
if not sort_by in res:
return
var sort_value = io.get_value(res, sort_by)
for i in loaded_rows.size():
if sort_reverse == compare_values(sort_value, io.get_value(loaded_rows[i], sort_by)):
loaded_rows.insert(i, res)
return
remembered_paths[res.resource_path] = res
loaded_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 : StringName):
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_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
)
if columns[i] in rows[row_index]:
current_node.mouse_filter = MOUSE_FILTER_STOP
current_node.modulate.a = 1.0
else:
# Empty cell, can't click, property doesn't exist.
current_node.mouse_filter = MOUSE_FILTER_IGNORE
current_node.modulate.a = 0.0
continue
if columns[i] == &"resource_path":
column_editors[i].set_value(current_node, res_path)
else:
var cell_value = io.get_value(rows[row_index], columns[i])
if cell_value != null or column_types[i] == TYPE_OBJECT:
column_editors[i].set_value(current_node, cell_value)
if color_rows and column_types[i] == TYPE_COLOR:
next_color = cell_value
column_editors[i].set_color(current_node, next_color)
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 _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_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

View File

@ -0,0 +1,573 @@
[gd_scene load_steps=30 format=3 uid="uid://tmleonv20aqk"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/editor_view.gd" id="1_wfx75"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/editor_color_setter.gd" id="2_t2s7k"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/editor_icon_button.gd" id="3_7ja2l"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/main_screen/recent_paths.gd" id="4_umoob"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/main_screen/expression_textfield.gd" id="5_faq75"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/main_screen/table_pages.gd" id="5_ka2yn"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/main_screen/column_header_manager.gd" id="6_emnmd"]
[ext_resource type="PackedScene" uid="uid://d1s6oihqedvo5" path="res://addons/resources_spreadsheet_view/main_screen/table_header.tscn" id="7_3dx0v"]
[ext_resource type="PackedScene" uid="uid://ddqak780cwwfj" path="res://addons/resources_spreadsheet_view/typed_editors/dock_enum_array.tscn" id="8_234wn"]
[ext_resource type="PackedScene" uid="uid://c3a2cip8ffccv" path="res://addons/resources_spreadsheet_view/typed_editors/dock_array.tscn" id="9_nts08"]
[ext_resource type="PackedScene" uid="uid://b3a3bo6cfyh5t" path="res://addons/resources_spreadsheet_view/typed_editors/dock_color.tscn" id="10_nsma2"]
[ext_resource type="PackedScene" uid="uid://gtbf7b0wptv" path="res://addons/resources_spreadsheet_view/typed_editors/dock_number.tscn" id="11_q1ao4"]
[ext_resource type="PackedScene" uid="uid://rww3gpl052bn" path="res://addons/resources_spreadsheet_view/typed_editors/dock_texture.tscn" id="12_4kr6q"]
[ext_resource type="PackedScene" uid="uid://dhunxgcae6h1" path="res://addons/resources_spreadsheet_view/settings_grid.tscn" id="13_as1sh"]
[ext_resource type="PackedScene" uid="uid://p6x03dbvhnqw" path="res://addons/resources_spreadsheet_view/typed_editors/dock_dict.tscn" id="13_il556"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/main_screen/input_handler.gd" id="14_2t57a"]
[ext_resource type="PackedScene" uid="uid://b413igx28kkvb" path="res://addons/resources_spreadsheet_view/import_export/import_export_dialog.tscn" id="14_3p12b"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/main_screen/selection_manager.gd" id="15_mx6qn"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_cells/cell_editor_enum_array.gd" id="16_p7n52"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_cells/cell_editor_array.gd" id="17_sofdw"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_cells/cell_editor_color.gd" id="18_oeewr"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_cells/cell_editor_bool.gd" id="19_7x44x"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_cells/cell_editor_dict.gd" id="19_oeuko"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_cells/cell_editor_enum.gd" id="20_swsbn"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_cells/cell_editor_resource.gd" id="21_58wf8"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_cells/cell_editor_string.gd" id="22_bni8r"]
[ext_resource type="PackedScene" uid="uid://b51hnttsie7k5" path="res://addons/resources_spreadsheet_view/main_screen/selection_actions.tscn" id="23_m53sx"]
[sub_resource type="Gradient" id="Gradient_8kp6w"]
offsets = PackedFloat32Array(0, 0.995413, 1)
colors = PackedColorArray(1, 1, 1, 0.490196, 1, 1, 1, 0.0458716, 1, 1, 1, 0)
[sub_resource type="GradientTexture2D" id="GradientTexture2D_18il8"]
gradient = SubResource("Gradient_8kp6w")
[node name="Control" type="MarginContainer" node_paths=PackedStringArray("node_folder_path", "node_recent_paths", "node_table_root", "node_columns", "node_page_manager")]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
focus_neighbor_left = NodePath(".")
focus_neighbor_top = NodePath(".")
focus_neighbor_right = NodePath(".")
focus_neighbor_bottom = NodePath(".")
focus_next = NodePath(".")
focus_previous = NodePath(".")
focus_mode = 2
theme_override_constants/margin_left = 3
theme_override_constants/margin_right = 3
theme_override_constants/margin_bottom = 5
script = ExtResource("1_wfx75")
node_folder_path = NodePath("HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer/Path")
node_recent_paths = NodePath("HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer2/RecentPaths")
node_table_root = NodePath("HeaderContentSplit/MarginContainer/FooterContentSplit/Panel/Scroll/MarginContainer/TableGrid")
node_columns = NodePath("HeaderContentSplit/VBoxContainer/Columns/Columns")
node_page_manager = NodePath("HeaderContentSplit/VBoxContainer/HBoxContainer3/Pages")
metadata/_edit_lock_ = true
[node name="HeaderContentSplit" type="VBoxContainer" parent="."]
layout_mode = 2
[node name="VBoxContainer" type="VBoxContainer" parent="HeaderContentSplit"]
layout_mode = 2
[node name="MenuStrip" type="HBoxContainer" parent="HeaderContentSplit/VBoxContainer"]
layout_mode = 2
[node name="File" type="Button" parent="HeaderContentSplit/VBoxContainer/MenuStrip"]
layout_mode = 2
tooltip_text = "Settings"
focus_mode = 0
toggle_mode = true
button_pressed = true
text = "File"
flat = true
[node name="SearchProcess" type="Button" parent="HeaderContentSplit/VBoxContainer/MenuStrip"]
layout_mode = 2
tooltip_text = "Settings"
focus_mode = 0
toggle_mode = true
text = "Filter/Process"
flat = true
[node name="VisibleCols" type="MenuButton" parent="HeaderContentSplit/VBoxContainer/MenuStrip"]
layout_mode = 2
text = "Shown Columns"
[node name="VSeparator" type="Control" parent="HeaderContentSplit/VBoxContainer/MenuStrip"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Settings" type="Button" parent="HeaderContentSplit/VBoxContainer/MenuStrip"]
layout_mode = 2
tooltip_text = "Settings"
focus_mode = 0
text = "Settings"
flat = true
[node name="VSeparator2" type="VSeparator" parent="HeaderContentSplit/VBoxContainer/MenuStrip"]
layout_mode = 2
[node name="Info" type="Button" parent="HeaderContentSplit/VBoxContainer/MenuStrip"]
layout_mode = 2
focus_mode = 0
text = "About"
flat = true
[node name="HBoxContainer" type="HSplitContainer" parent="HeaderContentSplit/VBoxContainer"]
layout_mode = 2
split_offset = -249
[node name="HBoxContainer" type="HBoxContainer" parent="HeaderContentSplit/VBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 0
[node name="ColorRect4" type="ColorRect" parent="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer"]
modulate = Color(0, 0, 0, 1)
custom_minimum_size = Vector2(6, 18)
layout_mode = 2
script = ExtResource("2_t2s7k")
[node name="TextureRect" type="TextureRect" parent="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer/ColorRect4"]
layout_mode = 1
anchors_preset = 11
anchor_left = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_right = 48.0
grow_horizontal = 0
grow_vertical = 2
texture = SubResource("GradientTexture2D_18il8")
expand_mode = 1
[node name="ColorRect3" type="Control" parent="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer"]
custom_minimum_size = Vector2(2, 0)
layout_mode = 2
[node name="Label" type="Label" parent="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer"]
layout_mode = 2
text = "Resource Folder:"
[node name="Path" type="LineEdit" parent="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
caret_blink = true
[node name="SelectDir" type="Button" parent="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Open Folder"
script = ExtResource("3_7ja2l")
icon_name = "Folder"
[node name="DeletePath" type="Button" parent="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Remove Path from Recent"
script = ExtResource("3_7ja2l")
icon_name = "Remove"
[node name="HBoxContainer2" type="HBoxContainer" parent="HeaderContentSplit/VBoxContainer/HBoxContainer"]
layout_mode = 2
[node name="Label2" type="Label" parent="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer2"]
layout_mode = 2
text = "Open Recent:"
[node name="RecentPaths" type="OptionButton" parent="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer2" node_paths=PackedStringArray("editor_view")]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
clip_text = true
fit_to_longest_item = false
script = ExtResource("4_umoob")
editor_view = NodePath("../../../../..")
[node name="ImportExport" type="Button" parent="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer2"]
layout_mode = 2
text = "Import/Export CSV..."
script = ExtResource("3_7ja2l")
icon_name = "TextFile"
[node name="Search" type="VBoxContainer" parent="HeaderContentSplit/VBoxContainer"]
visible = false
layout_mode = 2
theme_override_constants/separation = 0
[node name="HBoxContainer" type="HBoxContainer" parent="HeaderContentSplit/VBoxContainer/Search"]
layout_mode = 2
[node name="ColorRect4" type="ColorRect" parent="HeaderContentSplit/VBoxContainer/Search/HBoxContainer"]
modulate = Color(0, 0, 0, 1)
custom_minimum_size = Vector2(6, 18)
layout_mode = 2
size_flags_vertical = 5
script = ExtResource("2_t2s7k")
[node name="TextureRect" type="TextureRect" parent="HeaderContentSplit/VBoxContainer/Search/HBoxContainer/ColorRect4"]
layout_mode = 1
anchors_preset = 11
anchor_left = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_right = 48.0
grow_horizontal = 0
grow_vertical = 2
texture = SubResource("GradientTexture2D_18il8")
expand_mode = 1
[node name="Label" type="Label" parent="HeaderContentSplit/VBoxContainer/Search/HBoxContainer"]
layout_mode = 2
text = "GDScript Filter and Process"
[node name="HSeparator" type="HSeparator" parent="HeaderContentSplit/VBoxContainer/Search/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Search" type="HBoxContainer" parent="HeaderContentSplit/VBoxContainer/Search"]
layout_mode = 2
[node name="ColorRect2" type="ColorRect" parent="HeaderContentSplit/VBoxContainer/Search/Search"]
modulate = Color(0, 0, 0, 1)
custom_minimum_size = Vector2(6, 18)
layout_mode = 2
size_flags_vertical = 5
script = ExtResource("2_t2s7k")
[node name="TextureRect" type="TextureRect" parent="HeaderContentSplit/VBoxContainer/Search/Search/ColorRect2"]
layout_mode = 1
anchors_preset = 11
anchor_left = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_right = 48.0
grow_horizontal = 0
grow_vertical = 2
texture = SubResource("GradientTexture2D_18il8")
expand_mode = 1
[node name="Label" type="Label" parent="HeaderContentSplit/VBoxContainer/Search/Search"]
layout_mode = 2
text = "Condition:"
[node name="Label2" type="Label" parent="HeaderContentSplit/VBoxContainer/Search/Search"]
layout_mode = 2
tooltip_text = "Enter an expression. The table only show rows where the expression returns `true`.
You can use `res.<property_name>` to get a property, and `index` to get row number. Hit ENTER to run the search.
Try out these:
- (res.number_property > 0 and res.number_property < 100)
- (res.text_property != \\\"\\\")
- (\\\"a\\\" in res.text_property)
- (index < 5)"
mouse_filter = 0
mouse_default_cursor_shape = 16
text = "(?)"
[node name="Filter" type="HBoxContainer" parent="HeaderContentSplit/VBoxContainer/Search/Search"]
layout_mode = 2
size_flags_horizontal = 3
script = ExtResource("5_faq75")
editor_view_path = NodePath("../../../../..")
title = "func f(res : Resource, index : int):"
default_text = "true"
default_text_ml = "return true"
function_save_key = "filter"
[node name="VSeparator" type="VSeparator" parent="HeaderContentSplit/VBoxContainer/Search/Search"]
layout_mode = 2
[node name="Label3" type="Label" parent="HeaderContentSplit/VBoxContainer/Search/Search"]
layout_mode = 2
text = "Process:"
[node name="Label4" type="Label" parent="HeaderContentSplit/VBoxContainer/Search/Search"]
layout_mode = 2
tooltip_text = "Enter an expression. The values in selected cells will be replaced with calculated new values.
You can use `value` to get the cell's value, `res.<property_name>` to get a property, `row_index` to get row number
and `cell_index` to get the cell's selection order. Hit ENTER to run the search.
These are some valid expressions:
- (res.property1 + res.property2)
- (res.property1.replace(\\\"old_string\\\", \\\"new_string\\\"))
- (load(\\\"res://path/to_resource.tres\\\"))
Don't forget quotation marks on strings and str() on non-strings."
mouse_filter = 0
mouse_default_cursor_shape = 16
text = "(?)"
[node name="Process" type="HBoxContainer" parent="HeaderContentSplit/VBoxContainer/Search/Search"]
layout_mode = 2
size_flags_horizontal = 3
script = ExtResource("5_faq75")
editor_view_path = NodePath("../../../../..")
mode = 1
title = "func f(value : Var, res : Resource, all_res : Array[Resource], row_index : int):"
default_text = "value"
default_text_ml = "return value"
function_save_key = "process"
[node name="HBoxContainer3" type="HBoxContainer" parent="HeaderContentSplit/VBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="HeaderContentSplit/VBoxContainer/HBoxContainer3"]
layout_mode = 2
text = "Grid"
[node name="Refresh" type="Button" parent="HeaderContentSplit/VBoxContainer/HBoxContainer3"]
layout_mode = 2
tooltip_text = "Refresh"
script = ExtResource("3_7ja2l")
icon_name = "Loop"
[node name="Pages" type="HBoxContainer" parent="HeaderContentSplit/VBoxContainer/HBoxContainer3" node_paths=PackedStringArray("node_editor_view_root")]
layout_mode = 2
script = ExtResource("5_ka2yn")
node_editor_view_root = NodePath("../../../..")
[node name="Label" type="Label" parent="HeaderContentSplit/VBoxContainer/HBoxContainer3/Pages"]
layout_mode = 2
text = "Page:"
[node name="Pagelist" type="HBoxContainer" parent="HeaderContentSplit/VBoxContainer/HBoxContainer3/Pages"]
layout_mode = 2
[node name="Label2" type="Label" parent="HeaderContentSplit/VBoxContainer/HBoxContainer3/Pages"]
layout_mode = 2
text = "Rows per page:"
[node name="LineEdit" type="SpinBox" parent="HeaderContentSplit/VBoxContainer/HBoxContainer3/Pages"]
layout_mode = 2
min_value = 2.0
max_value = 300.0
value = 50.0
[node name="HSeparator" type="HSeparator" parent="HeaderContentSplit/VBoxContainer/HBoxContainer3"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Sep" type="Control" parent="HeaderContentSplit/VBoxContainer"]
layout_mode = 2
[node name="Columns" type="Control" parent="HeaderContentSplit/VBoxContainer"]
clip_contents = true
layout_mode = 2
[node name="Columns" type="HBoxContainer" parent="HeaderContentSplit/VBoxContainer/Columns" node_paths=PackedStringArray("editor_view", "hide_columns_button", "grid")]
layout_mode = 0
theme_override_constants/separation = 0
script = ExtResource("6_emnmd")
table_header_scene = ExtResource("7_3dx0v")
editor_view = NodePath("../../../..")
hide_columns_button = NodePath("../../MenuStrip/VisibleCols")
grid = NodePath("../../../MarginContainer/FooterContentSplit/Panel/Scroll/MarginContainer/TableGrid")
[node name="Sep2" type="Control" parent="HeaderContentSplit/VBoxContainer"]
visible = false
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="HeaderContentSplit"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
mouse_filter = 2
[node name="FooterContentSplit" type="VBoxContainer" parent="HeaderContentSplit/MarginContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Panel" type="MarginContainer" parent="HeaderContentSplit/MarginContainer/FooterContentSplit"]
layout_mode = 2
size_flags_vertical = 3
mouse_filter = 2
[node name="Panel" type="Panel" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Panel"]
layout_mode = 2
[node name="Scroll" type="ScrollContainer" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Panel"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="MarginContainer" type="MarginContainer" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Panel/Scroll"]
layout_mode = 2
[node name="TableGrid" type="GridContainer" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Panel/Scroll/MarginContainer"]
layout_mode = 2
theme_override_constants/h_separation = 0
theme_override_constants/v_separation = 0
[node name="Label" type="Label" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Panel"]
self_modulate = Color(1, 1, 1, 0.498039)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
text = "No folder selected!
Please select a folder to edit using the text field or Open button above.
Then, Shift+Click or Ctrl+Click cells to edit them using the keyboard,
Inspector dock or this screen's bottom panels.
To find out keybindings available, open the \"About\" menu."
horizontal_alignment = 1
vertical_alignment = 1
[node name="Footer" type="VBoxContainer" parent="HeaderContentSplit/MarginContainer/FooterContentSplit"]
layout_mode = 2
[node name="PropertyEditors" type="VBoxContainer" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Footer"]
unique_name_in_owner = true
layout_mode = 2
[node name="EditEnumArray" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Footer/PropertyEditors" instance=ExtResource("8_234wn")]
visible = false
layout_mode = 2
[node name="EditArray" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Footer/PropertyEditors" instance=ExtResource("9_nts08")]
visible = false
layout_mode = 2
[node name="EditDict" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Footer/PropertyEditors" instance=ExtResource("13_il556")]
visible = false
layout_mode = 2
[node name="EditColor" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Footer/PropertyEditors" instance=ExtResource("10_nsma2")]
visible = false
layout_mode = 2
[node name="EditNumber" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Footer/PropertyEditors" instance=ExtResource("11_q1ao4")]
visible = false
layout_mode = 2
[node name="EditTexture" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Footer/PropertyEditors" instance=ExtResource("12_4kr6q")]
visible = false
layout_mode = 2
[node name="Control" type="Control" parent="."]
layout_mode = 2
mouse_filter = 2
metadata/_edit_lock_ = true
[node name="FileDialog" type="FileDialog" parent="Control"]
title = "Open"
size = Vector2i(800, 500)
min_size = Vector2i(800, 400)
ok_button_text = "Open"
mode_overrides_title = false
file_mode = 3
filters = PackedStringArray("*.tres")
[node name="FileDialogText" type="FileDialog" parent="Control"]
title = "Open"
size = Vector2i(800, 500)
min_size = Vector2i(800, 400)
ok_button_text = "Open"
mode_overrides_title = false
file_mode = 0
filters = PackedStringArray("*.csv")
[node name="Info" type="AcceptDialog" parent="Control"]
title = "About"
size = Vector2i(800, 500)
[node name="MarginContainer" type="MarginContainer" parent="Control/Info"]
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="Panel" type="Panel" parent="Control/Info/MarginContainer"]
layout_mode = 2
[node name="RichTextLabel" type="RichTextLabel" parent="Control/Info/MarginContainer"]
layout_mode = 2
bbcode_enabled = true
text = "Edit Resources as Table 2
\"Welp, it is what it sounds like!\"
Possible inputs:
- Ctrl + Click / Cmd + Click - Select multiple cells in one column
- Shift + Click - Select all cells between A and B in one column
- Up / Down / Shift+Tab / Tab - move cell selection up/down/left/right
- Left/Right - Move cursor along cell text
- Backspace/Delete - Erase text Left / Right from cursor
- Home/End - Move cursor to start/end of cell
- Ctrl + <move/erase> / Cmd + <move/erase> - Move through / Erase whole word
- Ctrl/Cmd + C/V - Copy cells / Paste text into cells
- Ctrl/Cmd + (Shift) + Z - The Savior
If clipboard contains as many lines as there are cells selected, each line is pasted into a separate cell.
Made by Don Tnowe. 2022.
https://twitter.com/don_tnowe
Issues and contribution:
https://github.com/don-tnowe/godot-resources-as-sheets-plugin"
[node name="Settings" type="AcceptDialog" parent="Control"]
title = "Settings"
size = Vector2i(500, 300)
min_size = Vector2i(500, 300)
[node name="Settings" parent="Control/Settings" instance=ExtResource("13_as1sh")]
[node name="ImportExport" type="Window" parent="Control"]
process_mode = 3
initial_position = 4
size = Vector2i(600, 400)
visible = false
transient = true
exclusive = true
min_size = Vector2i(600, 400)
[node name="ImportExport" parent="Control/ImportExport" instance=ExtResource("14_3p12b")]
[node name="SelectionActions" parent="Control" node_paths=PackedStringArray("editor_view", "selection") instance=ExtResource("23_m53sx")]
visible = false
layout_mode = 2
offset_left = -506.0
offset_top = 65.0
offset_right = -426.0
offset_bottom = 117.0
editor_view = NodePath("../..")
selection = NodePath("../../SelectionManager")
[node name="InputHandler" type="Node" parent="."]
script = ExtResource("14_2t57a")
[node name="SelectionManager" type="Control" parent="." node_paths=PackedStringArray("node_property_editors", "scrollbar")]
layout_mode = 2
mouse_filter = 2
script = ExtResource("15_mx6qn")
cell_editor_classes = Array[Script]([ExtResource("16_p7n52"), ExtResource("19_oeuko"), ExtResource("17_sofdw"), ExtResource("18_oeewr"), ExtResource("19_7x44x"), ExtResource("20_swsbn"), ExtResource("21_58wf8"), ExtResource("22_bni8r")])
node_property_editors = NodePath("../HeaderContentSplit/MarginContainer/FooterContentSplit/Footer/PropertyEditors")
scrollbar = NodePath("../HeaderContentSplit/MarginContainer/FooterContentSplit/Panel/Scroll")
metadata/_edit_lock_ = true
[connection signal="grid_updated" from="." to="HeaderContentSplit/VBoxContainer/HBoxContainer3/Pages" method="_on_grid_updated"]
[connection signal="gui_input" from="." to="InputHandler" method="_gui_input"]
[connection signal="pressed" from="HeaderContentSplit/VBoxContainer/MenuStrip/File" to="." method="_on_File_pressed"]
[connection signal="pressed" from="HeaderContentSplit/VBoxContainer/MenuStrip/SearchProcess" to="." method="_on_SearchProcess_pressed"]
[connection signal="about_to_popup" from="HeaderContentSplit/VBoxContainer/MenuStrip/VisibleCols" to="HeaderContentSplit/VBoxContainer/Columns/Columns" method="_on_visible_cols_about_to_popup"]
[connection signal="pressed" from="HeaderContentSplit/VBoxContainer/MenuStrip/Settings" to="Control/Settings" method="popup_centered"]
[connection signal="pressed" from="HeaderContentSplit/VBoxContainer/MenuStrip/Info" to="Control/Info" method="popup_centered"]
[connection signal="text_submitted" from="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer/Path" to="." method="_on_path_text_submitted"]
[connection signal="pressed" from="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer/SelectDir" to="Control/FileDialog" method="popup_centered"]
[connection signal="pressed" from="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer/DeletePath" to="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer2/RecentPaths" method="remove_selected_path_from_recent"]
[connection signal="pressed" from="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer2/ImportExport" to="Control/FileDialogText" method="popup_centered"]
[connection signal="pressed" from="HeaderContentSplit/VBoxContainer/HBoxContainer3/Refresh" to="." method="_on_path_text_submitted"]
[connection signal="value_changed" from="HeaderContentSplit/VBoxContainer/HBoxContainer3/Pages/LineEdit" to="HeaderContentSplit/VBoxContainer/HBoxContainer3/Pages" method="_on_LineEdit_value_changed"]
[connection signal="dir_selected" from="Control/FileDialog" to="." method="_on_FileDialog_dir_selected"]
[connection signal="file_selected" from="Control/FileDialog" to="." method="_on_FileDialog_dir_selected"]
[connection signal="dir_selected" from="Control/FileDialogText" to="Control/ImportExport/ImportExport" method="_on_file_selected"]
[connection signal="file_selected" from="Control/FileDialogText" to="Control/ImportExport/ImportExport" method="_on_file_selected"]
[connection signal="close_requested" from="Control/ImportExport" to="Control/ImportExport" method="hide"]
[connection signal="cells_rightclicked" from="SelectionManager" to="Control/SelectionActions" method="_on_grid_cells_rightclicked"]
[connection signal="cells_selected" from="SelectionManager" to="Control/SelectionActions" method="_on_grid_cells_selected"]

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,88 @@
class_name ResourceTablesEditFormatCsv
extends ResourceTablesEditFormatTres
var import_data
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_delimeter = import_data.delimeter.ends_with(" ")
var file = FileAccess.open(import_data.edited_path, FileAccess.WRITE)
for x in csv_rows:
if space_after_delimeter:
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 = 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 = resource_original_positions[x]
csv_rows.remove(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,87 @@
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):
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):
if rows.size() == 1:
var new_row = rows[0].duplicate()
new_row.resource_path = rows[0].resource_path.get_base_dir() + "/" + name_input + ".tres"
ResourceSaver.save(new_row)
return
var new_row
for x in rows:
new_row = x.duplicate()
new_row.resource_path = x.resource_path.get_basename() + name_input + ".tres"
ResourceSaver.save(new_row)
func rename_row(row, new_name : String):
var new_row = row
DirAccess.open("res://").remove(row.resource_path)
new_row.resource_path = row.resource_path.get_base_dir() + "/" + new_name + ".tres"
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 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 : Array[Resource] = []
var res : Resource = null
var cur_dir_types : Dictionary = {}
loaded_res.resize(file_stack.size())
for i in file_stack.size():
res = null
if file_stack[i].ends_with(".tres"):
res = load(file_stack[i])
loaded_res[i] = res
editor_view.fill_property_data_many(loaded_res)
for x in loaded_res:
if x == null: continue
insert_func.call(x, rows, sort_by, sort_reverse)
return rows

View File

@ -0,0 +1,32 @@
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):
var file = FileAccess.open(into_path, FileAccess.WRITE)
var line = PackedStringArray()
var space_after_delimeter = 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)
import_data.resource_path = ""
line.resize(column_names.size())
if import_data.remove_first_row:
for j in column_names.size():
line[j] = column_names[j]
if space_after_delimeter 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_delimeter and j != 0:
line[j] = " " + line[j]
file.store_csv_line(line, import_data.delimeter[0])

View File

@ -0,0 +1,58 @@
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 := [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]
import_data.prop_names = entries[0]
return entries

View File

@ -0,0 +1,222 @@
@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_world := $"Import/Margins/Scroll/Box/Grid/Classname"
@onready var prop_list := $"Import/Margins/Scroll/Box"
@onready var file_dialog = $"../../FileDialogText"
var format_extension := ".csv"
var entries := []
var property_used_as_filename := 0
var import_data : ResourceTablesImport
func _ready():
var create_file_button = Button.new()
file_dialog.get_child(3, true).get_child(3, true).add_child(create_file_button)
create_file_button.get_parent().move_child(create_file_button, 2)
create_file_button.text = "Create File"
create_file_button.visible = true
create_file_button.icon = get_theme_icon(&"New", &"EditorIcons")
create_file_button.pressed.connect(_on_create_file_pressed)
hide()
show()
get_parent().min_size = Vector2(600, 400)
get_parent().size = Vector2(600, 400)
func _on_create_file_pressed():
var new_name = (
file_dialog.get_child(3, true).get_child(3, true).get_child(1, true).text
)
if new_name == "":
new_name += editor_view.current_path.get_base_dir().get_file()
var file = FileAccess.open((
file_dialog.get_child(3, true).get_child(0, true).get_child(6, true).text
+ "/"
+ new_name.get_basename() + format_extension
), FileAccess.WRITE)
file_dialog.invalidate()
func _on_file_selected(path : String):
import_data = ResourceTablesImport.new()
import_data.initialize(path)
_reset_controls()
await get_tree().process_frame
_open_dialog(path)
get_parent().popup_centered()
position = Vector2.ZERO
func _open_dialog(path : String):
classname_world.text = import_data.edited_path.get_file().get_basename()\
.capitalize().replace(" ", "")
import_data.script_classname = classname_world.text
for x in formats_import:
if x.new().can_edit_path(path):
entries = x.new().import_as_arrays(import_data)
_load_property_names(path)
_create_prop_editors()
$"Import/Margins/Scroll/Box/StyleSettingsI"._send_signal()
func _load_property_names(path):
var prop_types = import_data.prop_types
prop_types.resize(import_data.prop_names.size())
prop_types.fill(4)
for i in import_data.prop_names.size():
import_data.prop_names[i] = entries[0][i]\
.replace("\"", "")\
.replace(" ", "_")\
.replace("-", "_")\
.replace(".", "_")\
.replace(",", "_")\
.replace("\t", "_")\
.replace("/", "_")\
.replace("\\", "_")\
.to_lower()
# Don't guess Ints automatically - further rows might have floats
if entries[1][i].is_valid_float():
prop_types[i] = ResourceTablesImport.PropType.FLOAT
elif entries[1][i].begins_with("res://"):
prop_types[i] = ResourceTablesImport.PropType.OBJECT
else:
prop_types[i] = ResourceTablesImport.PropType.STRING
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()
for i in import_data.prop_names.size():
var new_node = prop_list_item_scene.instantiate()
prop_list.add_child(new_node)
new_node.display(import_data.prop_names[i], import_data.prop_types[i])
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 _export_tres_folder():
DirAccess.open("res://").make_dir_recursive(import_data.edited_path.get_basename())
import_data.prop_used_as_filename = import_data.prop_names[property_used_as_filename]
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])
ResourceSaver.save(new_res)
func _on_import_to_tres_pressed():
_generate_class()
_export_tres_folder()
await get_tree().process_frame
editor_view.display_folder(import_data.edited_path.get_basename() + "/")
await get_tree().process_frame
editor_view.refresh()
close()
func _on_import_edit_pressed():
_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 = editor_view.columns.duplicate()
exported_cols.erase("resource_local_to_scene")
for x in editor_view.node_columns.hidden_columns[editor_view.current_path].keys():
exported_cols.erase(x)
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_world_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_filename_options_item_selected(index):
property_used_as_filename = index
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_delimeter_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 _reset_controls():
$"Export/Box/CheckBox".button_pressed = false
_on_remove_first_row_toggled(true)
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()

View File

@ -0,0 +1,188 @@
[gd_scene load_steps=7 format=3 uid="uid://b413igx28kkvb"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/import_export/import_export_dialog.gd" id="1"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/import_export/formats_export/export_csv.gd" id="2_33c6s"]
[ext_resource type="Script" 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"]
[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
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="."]
layout_mode = 2
mouse_filter = 2
[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 = "Use as filename:"
[node name="UseAsFilename" type="OptionButton" parent="Import/Margins/Scroll/Box/Grid"]
layout_mode = 2
size_flags_horizontal = 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="RemoveFirstRow" type="CheckBox" parent="Import/Margins/Scroll/Box/Grid"]
layout_mode = 2
text = "First row contains property names"
[node name="Control" type="Control" parent="Import/Margins/Scroll/Box/Grid"]
layout_mode = 2
[node name="Control2" type="Control" parent="Import/Margins/Scroll/Box/Grid"]
visible = false
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="."]
visible = false
layout_mode = 2
[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 = "Delimeter:"
[node name="Button" type="Button" parent="Export/Box"]
layout_mode = 2
toggle_mode = 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="item_selected" from="Import/Margins/Scroll/Box/Grid/UseAsFilename" to="." method="_on_filename_options_item_selected"]
[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/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_delimeter_pressed" binds= [","]]
[connection signal="pressed" from="Export/Box/Button2" to="." method="_on_export_delimeter_pressed" binds= [";"]]
[connection signal="pressed" from="Export/Box/Button3" to="." method="_on_export_delimeter_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,20 @@
@tool
extends GridContainer
signal format_changed(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, delimiter, bool_yes, bool_no):
$"HBoxContainer/Case".selected = case
$"HBoxContainer/Separator".selected = [" ", "_", "-"].find(delimiter)
$"HBoxContainer2/True".text = bool_yes
$"HBoxContainer2/False".text = bool_no

View File

@ -0,0 +1,69 @@
[gd_scene load_steps=2 format=3 uid="uid://ckhf3bqy2rqjr"]
[ext_resource type="Script" 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
item_count = 4
selected = 2
popup/item_0/text = "all lower"
popup/item_0/id = 0
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
item_count = 3
popup/item_0/text = "Space \" \""
popup/item_0/id = 0
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,34 @@
[gd_scene load_steps=2 format=3 uid="uid://b8llymigbprh6"]
[ext_resource type="Script" 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
item_count = 7
fit_to_longest_item = false
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

View File

@ -0,0 +1,292 @@
@tool
class_name ResourceTablesImport
extends Resource
enum PropType {
BOOL,
INT,
FLOAT,
STRING,
COLOR,
OBJECT,
ENUM,
MAX,
}
enum NameCasing {
ALL_LOWER,
CAPS_WORD_EXCEPT_FIRST,
CAPS_WORD,
ALL_CAPS,
}
const SUFFIX := "_spreadsheet_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,
}
@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 : GDScript
@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 load(string)
PropType.ENUM:
if string == "":
return int(uniques[col_index]["N_A"])
else:
return int(uniques[col_index][string.capitalize().replace(" ", "_").to_upper()])
func property_to_string(value, col_index : int) -> String:
if value == null: return ""
if col_index == 0:
if prop_names[col_index] == "resource_path":
return value.get_file().get_basename()
if prop_types[col_index] is PackedStringArray:
return prop_types[col_index][value].capitalize()
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.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 = "@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.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.new()
created_script.source_code = source
created_script.reload()
return created_script
func strings_to_resource(strings : Array):
var new_res = new_script.new()
for j in min(prop_names.size(), strings.size()):
new_res.set(prop_names[j], string_to_property(strings[j], j))
if prop_used_as_filename != "":
new_res.resource_path = edited_path.get_basename() + "/" + new_res.get(prop_used_as_filename) + ".tres"
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] == 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) -> Array:
var result = []
result.resize(properties.size())
result.fill(PropType.STRING)
var cur_type := 0
for x in res.get_property_list():
var found = properties.find(x["name"])
if found == -1: continue
if x["usage"] & PROPERTY_USAGE_EDITOR != 0:
if x["hint"] == PROPERTY_HINT_ENUM:
var enum_values = x["hint_string"].split(",")
for i in enum_values.size():
var index_found : int = enum_values[i].find(":")
if index_found == -1: continue
enum_values[i] = enum_values[i].left(index_found)
result[found] = enum_values
else:
result[found] = TYPE_MAP.get(x["type"], PropType.STRING)
return result

View File

@ -0,0 +1,153 @@
@tool
extends Control
const TablesPluginSettingsClass := preload("res://addons/resources_spreadsheet_view/settings_grid.gd")
@export var table_header_scene : PackedScene
@export @onready var editor_view : Control = $"../../../.."
@export @onready var hide_columns_button : BaseButton = $"../../MenuStrip/VisibleCols"
@export @onready var grid : GridContainer = $"../../../MarginContainer/FooterContentSplit/Panel/Scroll/MarginContainer/TableGrid"
var hidden_columns := {}
var columns := []:
set(v):
columns = v
for x in get_children():
x.queue_free()
var new_node : Control
for x in v:
new_node = table_header_scene.instantiate()
add_child(new_node)
new_node.manager = self
new_node.set_label(x)
new_node.get_node("Button").pressed.connect(editor_view._set_sorting.bind(x))
func _ready():
hide_columns_button\
.get_popup()\
.id_pressed\
.connect(_on_visible_cols_id_pressed)
$"../../../MarginContainer/FooterContentSplit/Panel/Scroll"\
.get_h_scroll_bar()\
.value_changed\
.connect(_on_h_scroll_changed)
func update():
_update_hidden_columns()
_update_column_sizes()
func hide_column(column_index : int):
hidden_columns[editor_view.current_path][editor_view.columns[column_index]] = true
editor_view.save_data()
update()
func select_column(column_index : int):
editor_view.select_column(column_index)
func _update_column_sizes():
if grid.get_child_count() == 0:
return
await get_tree().process_frame
var column_headers := get_children()
if grid.get_child_count() < column_headers.size(): return
if column_headers.size() != columns.size():
editor_view.refresh()
return
var clip_text : bool = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "clip_headers")
var min_width := 0
var cell : Control
for i in column_headers.size():
var header = column_headers[i]
cell = grid.get_child(i)
header.get_child(0).clip_text = clip_text
header.custom_minimum_size.x = 0
cell.custom_minimum_size.x = 0
header.size.x = 0
min_width = max(header.size.x, cell.size.x)
header.custom_minimum_size.x = min_width
cell.custom_minimum_size.x = header.get_minimum_size().x
header.size.x = min_width
grid.hide()
grid.show()
hide()
show()
await get_tree().process_frame
# Abort if the node has been deleted since.
if !is_instance_valid(column_headers[0]):
return
get_parent().custom_minimum_size.y = column_headers[0].size.y
for i in column_headers.size():
column_headers[i].position.x = grid.get_child(i).position.x
column_headers[i].size.x = grid.get_child(i).size.x
func _update_hidden_columns():
var current_path = editor_view.current_path
var rows_shown = editor_view.last_row - editor_view.first_row
if !hidden_columns.has(current_path):
hidden_columns[current_path] = {
"resource_local_to_scene" : true,
"resource_name" : true,
}
editor_view.save_data()
var visible_column_count = 0
for i in columns.size():
var column_visible = !hidden_columns[current_path].has(columns[i])
get_child(i).visible = column_visible
for j in rows_shown:
grid.get_child(j * columns.size() + i).visible = column_visible
if column_visible:
visible_column_count += 1
grid.columns = visible_column_count
func _on_h_scroll_changed(value):
position.x = -value
func _on_visible_cols_about_to_popup():
var current_path = editor_view.current_path
var popup = hide_columns_button.get_popup()
popup.clear()
popup.hide_on_checkable_item_selection = false
for i in columns.size():
popup.add_check_item(columns[i].capitalize(), i)
popup.set_item_checked(i, not hidden_columns[current_path].has(columns[i]))
func _on_visible_cols_id_pressed(id : int):
var current_path = editor_view.current_path
var popup = hide_columns_button.get_popup()
if popup.is_item_checked(id):
popup.set_item_checked(id, false)
hidden_columns[current_path][columns[id]] = true
else:
popup.set_item_checked(id, true)
hidden_columns[current_path].erase(columns[id])
editor_view.save_data()
update()

View File

@ -0,0 +1,173 @@
@tool
extends Control
@export var editor_view_path : NodePath
@export_enum("Filter", "Process", "Sort") var mode := 0
@export var title := ""
@export var default_text := "":
set(v):
default_text = v
if _textfield == null:
await ready
_textfield.text = v
@export_multiline var default_text_ml := "":
set(v):
default_text_ml = v
if _textfield_ml == null:
await ready
_textfield_ml.text = v
@export var function_save_key := ""
var _textfield : LineEdit
var _textfield_ml : TextEdit
var _togglable_popup : PopupPanel
var _saved_function_index_label : Label
var _saved_functions : Array = []
var _saved_function_selected := -1
func load_saved_functions(func_dict : Dictionary):
if !func_dict.has(function_save_key):
func_dict[function_save_key] = [default_text_ml]
_saved_functions = func_dict[function_save_key]
_on_saved_function_selected(0)
func _ready():
var toggle_button := Button.new()
var popup_box := VBoxContainer.new()
var popup_buttons_box := HBoxContainer.new()
var title_label := Label.new()
var submit_button := Button.new()
var move_label := Label.new()
var move_button_l := Button.new()
var move_button_r := Button.new()
_textfield = LineEdit.new()
_togglable_popup = PopupPanel.new()
_textfield_ml = TextEdit.new()
_saved_function_index_label = Label.new()
add_child(_textfield)
add_child(toggle_button)
_textfield.add_child(_togglable_popup)
_togglable_popup.add_child(popup_box)
popup_box.add_child(title_label)
popup_box.add_child(_textfield_ml)
popup_box.add_child(popup_buttons_box)
popup_buttons_box.add_child(submit_button)
popup_buttons_box.add_child(move_label)
popup_buttons_box.add_child(move_button_l)
popup_buttons_box.add_child(_saved_function_index_label)
popup_buttons_box.add_child(move_button_r)
title_label.text = title
toggle_button.icon = get_theme_icon("Collapse", "EditorIcons")
toggle_button.pressed.connect(_on_expand_pressed)
_textfield.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_textfield.text_submitted.connect(_on_text_submitted.unbind(1))
_textfield_ml.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_textfield_ml.size_flags_vertical = Control.SIZE_EXPAND_FILL
submit_button.text = "Run multiline!"
submit_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
submit_button.pressed.connect(_on_text_submitted)
move_label.text = "Choose saved:"
move_button_l.icon = get_theme_icon("PagePrevious", "EditorIcons")
move_button_l.pressed.connect(_on_saved_function_bumped.bind(-1))
_on_saved_function_selected(0)
move_button_r.icon = get_theme_icon("PageNext", "EditorIcons")
move_button_r.pressed.connect(_on_saved_function_bumped.bind(+1))
func _on_expand_pressed():
_togglable_popup.popup(Rect2i(_textfield.get_screen_position(), Vector2(size.x, 256.0)))
func _on_text_submitted():
[_table_filter, _table_process][mode].call()
_saved_functions[_saved_function_selected] = _textfield_ml.text
get_node(editor_view_path).save_data.call_deferred()
func _get_script_source_code(first_line : String):
var new_text := ""
if !_togglable_popup.visible:
new_text = _textfield.text
if new_text == "":
new_text = default_text
return first_line + "\treturn " + new_text
else:
new_text = _textfield_ml.text
if new_text == "":
new_text = default_text_ml
var text_split := new_text.split("\n")
for i in text_split.size():
text_split[i] = "\t" + text_split[i]
return first_line + "\n".join(text_split)
func _table_filter():
var new_script := GDScript.new()
new_script.source_code = _get_script_source_code("static func can_show(res, index):\n")
new_script.reload()
var editor_view := get_node(editor_view_path)
editor_view.search_cond = new_script
editor_view.refresh()
func _table_process():
var new_script := GDScript.new()
new_script.source_code = _get_script_source_code("static func get_result(value, res, all_res, row_index):\n")
new_script.reload()
var editor_view := get_node(editor_view_path)
var new_script_instance = new_script.new()
var values = editor_view.get_edited_cells_values()
var edited_rows : Array[int] = editor_view._selection.get_edited_rows()
var edited_resources := edited_rows.map(func(x): return editor_view.rows[x])
for i in values.size():
values[i] = new_script_instance.get_result(values[i], editor_view.rows[edited_rows[i]], edited_resources, i)
editor_view.set_edited_cells_values(values)
func _on_saved_function_selected(new_index : int):
if new_index < 0:
new_index = 0
if _saved_function_selected == _saved_functions.size() - 1 and _textfield_ml.text == default_text_ml:
_saved_functions.resize(_saved_functions.size() - 1)
elif _saved_function_selected >= 0:
_saved_functions[_saved_function_selected] = _textfield_ml.text
_saved_function_selected = new_index
if new_index >= _saved_functions.size():
_saved_functions.resize(new_index + 1)
for i in _saved_functions.size():
if _saved_functions[i] == null:
_saved_functions[i] = default_text_ml
_textfield_ml.text = _saved_functions[new_index]
_saved_function_index_label.text = "%d/%d" % [new_index + 1, _saved_functions.size()]
get_node(editor_view_path).save_data.call_deferred()
func _on_saved_function_bumped(increment : int):
_on_saved_function_selected(_saved_function_selected + increment)

View File

@ -0,0 +1,163 @@
@tool
extends Node
const TablesPluginEditorViewClass = preload("res://addons/resources_spreadsheet_view/editor_view.gd")
const TablesPluginSelectionManagerClass = preload("res://addons/resources_spreadsheet_view/main_screen/selection_manager.gd")
const TextEditingUtilsClass := preload("res://addons/resources_spreadsheet_view/text_editing_utils.gd")
@onready var editor_view : TablesPluginEditorViewClass = get_parent()
@onready var selection : TablesPluginSelectionManagerClass = get_node("../SelectionManager")
func _on_cell_gui_input(event : InputEvent, cell : Control):
if event is InputEventMouseButton:
editor_view.grab_focus()
if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
if !cell in selection.edited_cells:
selection.deselect_all_cells()
selection.select_cell(cell)
selection.rightclick_cells()
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
if Input.is_key_pressed(KEY_CTRL):
if cell in selection.edited_cells:
selection.deselect_cell(cell)
else:
selection.select_cell(cell)
elif Input.is_key_pressed(KEY_SHIFT):
selection.select_cells_to(cell)
else:
selection.deselect_all_cells()
selection.select_cell(cell)
func _gui_input(event : InputEvent):
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_RIGHT and event.is_pressed():
selection.rightclick_cells()
if event.button_index == MOUSE_BUTTON_LEFT:
editor_view.grab_focus()
if !event.pressed:
selection.deselect_all_cells()
func _input(event : InputEvent):
if !event is InputEventKey or !event.pressed:
return
if !editor_view.has_focus() or selection.edited_cells.size() == 0:
return
if event.keycode == KEY_CTRL or event.keycode == KEY_SHIFT:
# Modifier keys do not get processed.
return
# Ctrl + Z (before, and instead of, committing the action!)
if Input.is_key_pressed(KEY_CTRL):
if event.keycode == KEY_Z or event.keycode == KEY_Y:
return
_key_specific_action(event)
editor_view.grab_focus()
editor_view.editor_interface.get_resource_filesystem().scan()
func _key_specific_action(event : InputEvent):
var column = selection.get_cell_column(selection.edited_cells[0])
var ctrl_pressed := Input.is_key_pressed(KEY_CTRL)
# BETWEEN-CELL NAVIGATION
if event.keycode == KEY_UP:
_move_selection_on_grid(0, (-1 if !ctrl_pressed else -10))
elif event.keycode == KEY_DOWN:
_move_selection_on_grid(0, (1 if !ctrl_pressed else 10))
elif Input.is_key_pressed(KEY_SHIFT) and event.keycode == KEY_TAB:
_move_selection_on_grid((-1 if !ctrl_pressed else -10), 0)
elif event.keycode == KEY_TAB:
_move_selection_on_grid((1 if !ctrl_pressed else 10), 0)
# CURSOR MOVEMENT
if event.keycode == KEY_LEFT:
TextEditingUtilsClass.multi_move_left(
selection.edited_cells_text, selection.edit_cursor_positions, ctrl_pressed
)
elif event.keycode == KEY_RIGHT:
TextEditingUtilsClass.multi_move_right(
selection.edited_cells_text, selection.edit_cursor_positions, ctrl_pressed
)
elif event.keycode == KEY_HOME:
for i in selection.edit_cursor_positions.size():
selection.edit_cursor_positions[i] = 0
elif event.keycode == KEY_END:
for i in selection.edit_cursor_positions.size():
selection.edit_cursor_positions[i] = selection.edited_cells_text[i].length()
# Ctrl + C (so you can edit in a proper text editor instead of this wacky nonsense)
elif ctrl_pressed and event.keycode == KEY_C:
TextEditingUtilsClass.multi_copy(selection.edited_cells_text)
get_viewport().set_input_as_handled()
# The following actions do not work on non-editable cells.
if !selection.column_editors[column].is_text() or editor_view.columns[column] == "resource_path":
return
# Ctrl + V
elif ctrl_pressed and event.keycode == KEY_V:
editor_view.set_edited_cells_values(TextEditingUtilsClass.multi_paste(
selection.edited_cells_text, selection.edit_cursor_positions
))
get_viewport().set_input_as_handled()
# ERASING
elif event.keycode == KEY_BACKSPACE:
editor_view.set_edited_cells_values(TextEditingUtilsClass.multi_erase_left(
selection.edited_cells_text, selection.edit_cursor_positions, ctrl_pressed
))
elif event.keycode == KEY_DELETE:
editor_view.set_edited_cells_values(TextEditingUtilsClass.multi_erase_right(
selection.edited_cells_text, selection.edit_cursor_positions, ctrl_pressed
))
get_viewport().set_input_as_handled()
# And finally, text typing.
elif event.keycode == KEY_ENTER:
editor_view.set_edited_cells_values(TextEditingUtilsClass.multi_input(
"\n", selection.edited_cells_text, selection.edit_cursor_positions
))
elif event.unicode != 0 and event.unicode != 127:
editor_view.set_edited_cells_values(TextEditingUtilsClass.multi_input(
char(event.unicode), selection.edited_cells_text, selection.edit_cursor_positions
))
selection.queue_redraw()
func _move_selection_on_grid(move_h : int, move_v : int):
var selected_cells := selection.edited_cells.duplicate()
var child_count := editor_view.node_table_root.get_child_count()
var new_child_index := 0
for i in selected_cells.size():
new_child_index = (
selected_cells[i].get_index()
+ move_h
+ move_v * editor_view.columns.size()
)
if child_count < new_child_index: continue
selected_cells[i] = editor_view.node_table_root.get_child(new_child_index)
editor_view.grab_focus()
selection.deselect_all_cells()
selection.select_cells(selected_cells)

View File

@ -0,0 +1,54 @@
@tool
extends OptionButton
@export @onready var editor_view := $"../../../../.."
var recent_paths := []
func _ready():
item_selected.connect(_on_item_selected)
func load_paths(paths):
clear()
for x in paths:
add_path_to_recent(x, true)
selected = 0
func add_path_to_recent(path : String, is_loading : bool = false):
if path in recent_paths: return
var idx_in_array := recent_paths.find(path)
if idx_in_array != -1:
remove_item(idx_in_array)
recent_paths.remove_at(idx_in_array)
recent_paths.append(path)
add_item(path)
select(get_item_count() - 1)
if !is_loading:
editor_view.save_data()
func remove_selected_path_from_recent():
if get_item_count() == 0:
return
var idx_in_array = selected
recent_paths.remove_at(idx_in_array)
remove_item(idx_in_array)
if get_item_count() != 0:
select(0)
editor_view.display_folder(recent_paths[0])
editor_view.save_data()
func _on_item_selected(index : int):
editor_view.current_path = recent_paths[index]
editor_view.node_folder_path.text = recent_paths[index]
editor_view.refresh()

View File

@ -0,0 +1,182 @@
@tool
extends MarginContainer
enum {
EDITBOX_DUPLICATE = 1,
EDITBOX_RENAME,
EDITBOX_DELETE,
}
const TextEditingUtilsClass := preload("res://addons/resources_spreadsheet_view/text_editing_utils.gd")
const TablesPluginSettingsClass := preload("res://addons/resources_spreadsheet_view/settings_grid.gd")
@export @onready var editor_view := $"../.."
@export @onready var selection := $"../../SelectionManager"
@onready var editbox_node := $"Control/ColorRect/Popup"
@onready var editbox_label : Label = editbox_node.get_node("Panel/VBoxContainer/Label")
@onready var editbox_input : LineEdit = editbox_node.get_node("Panel/VBoxContainer/LineEdit")
var cell : Control
var editbox_action : int
func _ready():
editbox_input.get_node("../..").add_theme_stylebox_override(
"panel",
get_theme_stylebox(&"Content", &"EditorStyles")
)
editbox_input.text_submitted.connect(func(_new_text): _on_editbox_accepted())
close()
func _on_grid_cells_rightclicked(cells):
open(cells)
func _on_grid_cells_selected(cells):
open(cells, true, true)
func open(cells : Array, pin_to_cell : bool = false, from_leftclick : bool = false):
if cells.size() == 0 or (from_leftclick and !ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "context_menu_on_leftclick")):
hide()
cell = null
return
if pin_to_cell:
cell = cells[-1]
set_deferred(&"global_position", Vector2(
cell.global_position.x + cell.size.x,
cell.global_position.y
))
else:
cell = null
set_deferred(&"global_position", get_global_mouse_position() + Vector2.ONE)
show()
size = Vector2.ZERO
top_level = true
$"Control2/Label".text = str(cells.size()) + (" Cells" if cells.size() % 10 != 1 else " Cell")
$"GridContainer/Rename".visible = editor_view.has_row_names()
func close():
pass
func _input(event : InputEvent):
if !editor_view.is_visible_in_tree():
close()
return
if event is InputEventMouseButton and event.is_pressed():
close()
return
if event is InputEventKey:
if event.is_pressed() and event.is_command_or_control_pressed():
global_position = get_global_mouse_position() + Vector2.ONE
if cell != null:
global_position = Vector2(
cell.global_position.x + cell.size.x,
cell.global_position.y
)
# Dupe
if event.keycode == KEY_D:
_on_Duplicate_pressed()
return
# Rename
if event.keycode == KEY_R:
_on_Rename_pressed()
return
func _on_Duplicate_pressed():
_show_editbox(EDITBOX_DUPLICATE)
func _on_CbCopy_pressed():
TextEditingUtilsClass.multi_copy(selection.edited_cells_text)
func _on_CbPaste_pressed():
editor_view.set_edited_cells_values(
TextEditingUtilsClass.multi_paste(
selection.edited_cells_text,
selection.edit_cursor_positions
)
)
func _on_Rename_pressed():
_show_editbox(EDITBOX_RENAME)
func _on_Delete_pressed():
_show_editbox(EDITBOX_DELETE)
func _show_editbox(action):
editbox_action = action
match action:
EDITBOX_DUPLICATE:
if !editor_view.has_row_names():
_on_editbox_accepted()
return
if selection.edited_cells.size() == 1:
editbox_label.text = "Input new row's name..."
editbox_input.text = editor_view.get_last_selected_row()\
.resource_path.get_file().get_basename()
else:
editbox_label.text = "Input suffix to append to names..."
editbox_input.text = ""
EDITBOX_RENAME:
editbox_label.text = "Input new name for row..."
editbox_input.text = editor_view.get_last_selected_row()\
.resource_path.get_file().get_basename()
EDITBOX_DELETE:
editbox_label.text = "Really delete selected rows? (Irreversible!!!)"
editbox_input.text = editor_view.get_last_selected_row()\
.resource_path.get_file().get_basename()
show()
editbox_input.grab_focus()
editbox_input.caret_column = 999999999
editbox_node.size = Vector2.ZERO
editbox_node.show()
$"Control/ColorRect".show()
$"Control/ColorRect".top_level = true
$"Control/ColorRect".size = get_viewport_rect().size * 4.0
editbox_node.global_position = (
global_position
+ size * 0.5
- editbox_node.get_child(0).size * 0.5
)
func _on_editbox_closed():
editbox_node.hide()
$"Control/ColorRect".hide()
func _on_editbox_accepted():
match(editbox_action):
EDITBOX_DUPLICATE:
editor_view.duplicate_selected_rows(editbox_input.text)
EDITBOX_RENAME:
editor_view.rename_row(editor_view.get_last_selected_row(), editbox_input.text)
EDITBOX_DELETE:
editor_view.delete_selected_rows()
_on_editbox_closed()

View File

@ -0,0 +1,182 @@
[gd_scene load_steps=7 format=3 uid="uid://b51hnttsie7k5"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/editor_icon_button.gd" id="1"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/main_screen/selection_actions.gd" id="1_qv6ov"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/editor_color_setter.gd" id="2_a4ihj"]
[sub_resource type="Image" id="Image_1546g"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "LumAlpha8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="2"]
image = SubResource("Image_1546g")
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ydxuk"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(1, 0.365, 0.365, 1)
draw_center = false
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
corner_detail = 1
[node name="SelectionActions" type="MarginContainer"]
offset_right = 80.0
offset_bottom = 52.0
size_flags_horizontal = 9
mouse_filter = 2
script = ExtResource("1_qv6ov")
[node name="Control2" type="Panel" parent="."]
layout_mode = 2
mouse_filter = 2
[node name="ColorRect" type="ColorRect" parent="Control2"]
show_behind_parent = true
layout_mode = 0
mouse_filter = 2
[node name="ColorRect2" type="ColorRect" parent="Control2"]
modulate = Color(0, 0, 0, 1)
show_behind_parent = true
layout_mode = 1
anchors_preset = 9
anchor_bottom = 1.0
offset_left = -2.0
grow_vertical = 2
mouse_filter = 2
script = ExtResource("2_a4ihj")
[node name="Label" type="Label" parent="Control2"]
layout_mode = 0
offset_top = -26.0
offset_right = 57.0
text = "Actions"
vertical_alignment = 2
[node name="ColorRect3" type="Panel" parent="Control2/Label"]
show_behind_parent = true
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -2.0
offset_top = 2.0
offset_right = 2.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
[node name="GridContainer" type="GridContainer" parent="."]
layout_mode = 2
size_flags_horizontal = 9
size_flags_vertical = 9
mouse_filter = 2
columns = 3
[node name="Duplicate" type="Button" parent="GridContainer"]
layout_mode = 2
tooltip_text = "Duplicate Selected Rows (Ctrl+D)"
mouse_filter = 1
icon = SubResource("2")
script = ExtResource("1")
icon_name = "Duplicate"
[node name="CbCopy" type="Button" parent="GridContainer"]
layout_mode = 2
tooltip_text = "Copy to Clipboard (Ctrl+C)"
mouse_filter = 1
icon = SubResource("2")
script = ExtResource("1")
icon_name = "ActionCopy"
[node name="CbPaste" type="Button" parent="GridContainer"]
layout_mode = 2
tooltip_text = "Paste Clipboard (Ctrl+V)"
mouse_filter = 1
icon = SubResource("2")
script = ExtResource("1")
icon_name = "ActionPaste"
[node name="Rename" type="Button" parent="GridContainer"]
layout_mode = 2
tooltip_text = "Rename Selected Rows (Ctrl+R)"
mouse_filter = 1
icon = SubResource("2")
script = ExtResource("1")
icon_name = "Edit"
[node name="Delete" type="Button" parent="GridContainer"]
layout_mode = 2
tooltip_text = "DELETE Selected Rows"
mouse_filter = 1
icon = SubResource("2")
script = ExtResource("1")
icon_name = "Remove"
[node name="Control" type="Control" parent="."]
layout_mode = 2
mouse_filter = 2
[node name="ColorRect" type="ColorRect" parent="Control"]
visible = false
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_right = -80.0
offset_bottom = -52.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0, 0, 0, 0.498039)
[node name="Popup" type="MarginContainer" parent="Control/ColorRect"]
layout_mode = 0
offset_top = 100.0
offset_right = 140.0
offset_bottom = 196.0
[node name="Panel" type="PanelContainer" parent="Control/ColorRect/Popup"]
layout_mode = 2
mouse_filter = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_ydxuk")
[node name="VBoxContainer" type="VBoxContainer" parent="Control/ColorRect/Popup/Panel"]
layout_mode = 2
mouse_filter = 2
[node name="Label" type="Label" parent="Control/ColorRect/Popup/Panel/VBoxContainer"]
layout_mode = 2
text = "Input new name..."
[node name="LineEdit" type="LineEdit" parent="Control/ColorRect/Popup/Panel/VBoxContainer"]
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="Control/ColorRect/Popup/Panel/VBoxContainer"]
layout_mode = 2
alignment = 1
[node name="Button" type="Button" parent="Control/ColorRect/Popup/Panel/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "OK"
[node name="Button2" type="Button" parent="Control/ColorRect/Popup/Panel/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "Cancel"
[connection signal="pressed" from="GridContainer/Duplicate" to="." method="_on_Duplicate_pressed"]
[connection signal="pressed" from="GridContainer/CbCopy" to="." method="_on_CbCopy_pressed"]
[connection signal="pressed" from="GridContainer/CbPaste" to="." method="_on_CbPaste_pressed"]
[connection signal="pressed" from="GridContainer/Rename" to="." method="_on_Rename_pressed"]
[connection signal="pressed" from="GridContainer/Delete" to="." method="_on_Delete_pressed"]
[connection signal="pressed" from="Control/ColorRect/Popup/Panel/VBoxContainer/HBoxContainer/Button" to="." method="_on_editbox_accepted"]
[connection signal="pressed" from="Control/ColorRect/Popup/Panel/VBoxContainer/HBoxContainer/Button2" to="." method="_on_editbox_closed"]

View File

@ -0,0 +1,251 @@
@tool
extends Control
signal cells_selected(cells)
signal cells_rightclicked(cells)
const EditorViewClass = preload("res://addons/resources_spreadsheet_view/editor_view.gd")
@export var cell_editor_classes : Array[Script] = []
@export @onready var node_property_editors : VBoxContainer = $"../HeaderContentSplit/MarginContainer/FooterContentSplit/Footer/PropertyEditors"
@export @onready var scrollbar : ScrollContainer = $"../HeaderContentSplit/MarginContainer/FooterContentSplit/Panel/Scroll"
@onready var editor_view : EditorViewClass = get_parent()
var edited_cells : Array = []
var edited_cells_text : Array = []
var edit_cursor_positions : Array[int] = []
var all_cell_editors : Array = []
var column_editors : Array[Object] = []
var inspector_resource : Resource
func _ready():
# Load cell editors and instantiate them
for x in cell_editor_classes:
all_cell_editors.append(x.new())
all_cell_editors[all_cell_editors.size() - 1].hint_strings_array = editor_view.column_hint_strings
get_parent()\
.editor_interface\
.get_inspector()\
.property_edited\
.connect(_on_inspector_property_edited)
scrollbar.get_h_scroll_bar().value_changed.connect(queue_redraw.unbind(1), CONNECT_DEFERRED)
scrollbar.get_v_scroll_bar().value_changed.connect(queue_redraw.unbind(1), CONNECT_DEFERRED)
func _draw():
var font := get_theme_font("font", "Label")
var font_size := get_theme_font_size("font", "Label")
var label_padding_left := 2.0
var newline_char := 10
if edit_cursor_positions.size() != edited_cells.size():
return
for i in edited_cells.size():
if edit_cursor_positions[i] >= edited_cells_text[i].length():
continue
var char_size := Vector2(0, font.get_ascent(font_size))
var cursor_pos := Vector2(label_padding_left, 0)
var cell_text : String = edited_cells_text[i]
var cell : Control = edited_cells[i]
if cell is Label and cell.horizontal_alignment == HORIZONTAL_ALIGNMENT_RIGHT:
cursor_pos.x += cell.size.x - font.get_multiline_string_size(edited_cells[i].text, HORIZONTAL_ALIGNMENT_RIGHT, -1, font_size).x
for j in max(edit_cursor_positions[i], 0) + 1:
if j == 0: continue
if cell_text.unicode_at(j - 1) == newline_char:
cursor_pos.x = label_padding_left
cursor_pos.y += font.get_ascent(font_size)
continue
char_size = font.get_char_size(cell_text.unicode_at(j - 1), font_size)
cursor_pos.x += char_size.x
draw_rect(Rect2(cursor_pos + cell.global_position - global_position, Vector2(2, char_size.y)), Color(1, 1, 1, 0.5))
func initialize_editors(column_values, column_types, column_hints):
deselect_all_cells()
edited_cells.clear()
edited_cells_text.clear()
edit_cursor_positions.clear()
column_editors.clear()
for i in column_values.size():
for x in all_cell_editors:
if x.can_edit_value(column_values[i], column_types[i], column_hints[i], i):
column_editors.append(x)
break
func deselect_all_cells():
for x in edited_cells:
column_editors[get_cell_column(x)].set_selected(x, false)
edited_cells.clear()
edited_cells_text.clear()
edit_cursor_positions.clear()
_selection_changed()
func deselect_cell(cell : Control):
var idx := edited_cells.find(cell)
if idx == -1: return
column_editors[get_cell_column(cell)].set_selected(cell, false)
edited_cells.remove_at(idx)
if edited_cells_text.size() != 0:
edited_cells_text.remove_at(idx)
edit_cursor_positions.remove_at(idx)
_selection_changed()
func select_cell(cell : Control):
var column_index := get_cell_column(cell)
if can_select_cell(cell):
_add_cell_to_selection(cell)
_try_open_docks(cell)
inspector_resource = editor_view.rows[get_cell_row(cell)]
# inspector_resource = editor_view.rows[get_cell_row(cell)].duplicate()
# inspector_resource.resource_path = ""
editor_view.editor_plugin.get_editor_interface().edit_resource(inspector_resource)
_selection_changed()
func select_cells(cells : Array):
var last_selectible : Control = null
for x in cells:
if x.mouse_filter != MOUSE_FILTER_IGNORE and can_select_cell(x):
_add_cell_to_selection(x)
last_selectible = x
if last_selectible != null:
select_cell(last_selectible)
func select_cells_to(cell : Control):
var column_index := get_cell_column(cell)
if edited_cells.size() == 0 or column_index != get_cell_column(edited_cells[edited_cells.size() - 1]):
return
var row_start := get_cell_row(edited_cells[edited_cells.size() - 1]) - editor_view.first_row
var row_end := get_cell_row(cell) - editor_view.first_row
var edge_shift := -1 if row_start > row_end else 1
row_start += edge_shift
row_end += edge_shift
for i in range(row_start, row_end, edge_shift):
var cur_cell : Control = editor_view.node_table_root.get_child(i * editor_view.columns.size() + column_index)
if !cur_cell.visible or cur_cell.mouse_filter == MOUSE_FILTER_IGNORE:
# When search is active, some cells will be hidden.
# When showing several classes, empty cells will be non-selectable.
continue
column_editors[column_index].set_selected(cur_cell, true)
if !cur_cell in edited_cells:
edited_cells.append(cur_cell)
if column_editors[column_index].is_text():
edited_cells_text.append(str(cur_cell.text))
edit_cursor_positions.append(cur_cell.text.length())
_selection_changed()
func rightclick_cells():
cells_rightclicked.emit(edited_cells)
func can_select_cell(cell : Control) -> bool:
if edited_cells.size() == 0:
return true
if (
get_cell_column(cell)
!= get_cell_column(edited_cells[0])
):
return false
return !cell in edited_cells
func get_cell_column(cell : Control) -> int:
return cell.get_index() % editor_view.columns.size()
func get_cell_row(cell : Control) -> int:
return cell.get_index() / editor_view.columns.size() + editor_view.first_row
func get_edited_rows() -> Array[int]:
var rows : Array[int] = []
rows.resize(edited_cells.size())
for i in rows.size():
rows[i] = get_cell_row(edited_cells[i])
return rows
func _selection_changed():
queue_redraw()
cells_selected.emit(edited_cells)
func _add_cell_to_selection(cell : Control):
var column_editor = column_editors[get_cell_column(cell)]
column_editor.set_selected(cell, true)
edited_cells.append(cell)
if column_editor.is_text():
edited_cells_text.append(str(cell.text))
edit_cursor_positions.append(cell.text.length())
func _update_selected_cells_text():
if edited_cells_text.size() == 0:
return
var column_dtype : int = editor_view.column_types[get_cell_column(edited_cells[0])]
for i in edited_cells.size():
edited_cells_text[i] = editor_view.try_convert(edited_cells[i].text, column_dtype)
edit_cursor_positions[i] = edited_cells_text[i].length()
func _try_open_docks(cell : Control):
var column_index = get_cell_column(cell)
var row = editor_view.rows[get_cell_row(cell)]
var column = editor_view.columns[column_index]
var type = editor_view.column_types[column_index]
var hints = editor_view.column_hints[column_index]
for x in node_property_editors.get_children():
x.visible = x.try_edit_value(editor_view.io.get_value(row, column), type, hints)
x.get_node(x.path_property_name).text = column
func _on_inspector_property_edited(property : String):
if !editor_view.is_visible_in_tree(): return
if inspector_resource == null: return
if editor_view.columns[get_cell_column(edited_cells[0])] != property:
var columns := editor_view.columns
var previously_edited = edited_cells.duplicate()
var new_column := columns.find(property)
deselect_all_cells()
var index := 0
for i in previously_edited.size():
index = get_cell_row(previously_edited[i]) * columns.size() + new_column
_add_cell_to_selection(editor_view.node_table_root.get_child(index - editor_view.first_row))
var values = []
values.resize(edited_cells.size())
values.fill(inspector_resource[property])
editor_view.set_edited_cells_values.call_deferred(values)
_try_open_docks(edited_cells[0])

View File

@ -0,0 +1,34 @@
@tool
extends HBoxContainer
var manager : Control
func set_label(label : String):
$"Button".text = label.capitalize()
$"Button".tooltip_text = label + "\nClick to sort."
func _ready():
$"Button".gui_input.connect(_on_main_gui_input)
$"Button2".get_popup().id_pressed.connect(_on_list_id_pressed)
func _on_main_gui_input(event):
if event is InputEventMouseButton and event.pressed:
var popup = $"Button2".get_popup()
if event.button_index == MOUSE_BUTTON_RIGHT:
popup.visible = !popup.visible
popup.size = Vector2.ZERO
popup.position = Vector2i(get_global_mouse_position()) + get_viewport().position
else:
popup.visible = false
func _on_list_id_pressed(id : int):
if id == 0:
manager.select_column(get_index())
else:
manager.hide_column(get_index())

View File

@ -0,0 +1,28 @@
[gd_scene load_steps=3 format=3 uid="uid://d1s6oihqedvo5"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/main_screen/table_header.gd" id="1_5fd1m"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/editor_icon_button.gd" id="2_0ymob"]
[node name="Header" type="HBoxContainer"]
offset_right = 179.0
offset_bottom = 31.0
size_flags_horizontal = 3
script = ExtResource("1_5fd1m")
[node name="Button" type="Button" parent="."]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 0
text = "resource_name"
clip_text = true
[node name="Button2" type="MenuButton" parent="."]
layout_mode = 2
size_flags_horizontal = 9
item_count = 2
popup/item_0/text = "Select All"
popup/item_0/id = 0
popup/item_1/text = "Hide"
popup/item_1/id = 1
script = ExtResource("2_0ymob")
icon_name = "ArrowDown"

View File

@ -0,0 +1,110 @@
@tool
extends HBoxContainer
@export @onready var node_editor_view_root : Control = $"../../../.."
var rows_per_page := 50
var current_page := 0
var first_row := 0
var last_row := 50
func _on_grid_updated():
visible = true
var page_count = (node_editor_view_root.rows.size() - 1) / rows_per_page + 1
first_row = min(current_page, page_count) * rows_per_page
last_row = min(first_row + rows_per_page, node_editor_view_root.rows.size())
var pagelist_node = $"Pagelist"
for x in pagelist_node.get_children():
x.queue_free()
var button_group = ButtonGroup.new()
var btns = []
btns.resize(page_count)
for i in page_count:
var btn = Button.new()
btns[i] = btn
btn.toggle_mode = true
btn.button_group = button_group
btn.text = str(i + 1)
btn.pressed.connect(_on_button_pressed.bind(btn))
pagelist_node.add_child(btn)
btns[current_page].button_pressed = true
var sort_property = node_editor_view_root.sorting_by
if sort_property == "": sort_property = "resource_path"
var sort_type = node_editor_view_root.column_types[node_editor_view_root.columns.find(sort_property)]
var property_values = []
property_values.resize(page_count)
if(node_editor_view_root.rows.size() == 0):
return
for i in page_count:
property_values[i] = node_editor_view_root.rows[i * rows_per_page].get(sort_property)
if sort_type == TYPE_FLOAT or sort_type == TYPE_INT:
for i in page_count:
btns[i].text = str(property_values[i])
elif sort_type == TYPE_COLOR:
for i in page_count:
btns[i].self_modulate = property_values[i] * 0.75 + Color(0.25, 0.25, 0.25, 1.0)
elif sort_type == TYPE_STRING:
var strings = []
strings.resize(page_count)
for i in page_count:
strings[i] = property_values[i].get_file()
if strings[i] == "":
strings[i] = str(i)
_fill_buttons_with_prefixes(btns, strings, page_count)
elif sort_type == TYPE_OBJECT:
var strings = []
strings.resize(page_count + 1)
for i in page_count:
if is_instance_valid(property_values[i]):
strings[i] = property_values[i].resource_path.get_file()
_fill_buttons_with_prefixes(btns, strings, page_count)
func _fill_buttons_with_prefixes(btns : Array, strings : Array, page_count : int):
for i in page_count:
if strings[i] == null:
continue
if i == 0:
btns[0].text = strings[0][0]
continue
for j in strings[i].length():
if strings[i].unicode_at(j) != strings[i - 1].unicode_at(j):
btns[i].text = strings[i].left(j + 1)
btns[i - 1].text = strings[i - 1].left(max(j + 1, btns[i - 1].text.length()))
break
for i in page_count - 1:
btns[i].text = btns[i].text + "-" + btns[i + 1].text
btns[page_count - 1].text += "-[End]"
func _on_button_pressed(button):
button.button_pressed = true
current_page = button.get_index()
_update_view()
func _on_LineEdit_value_changed(value):
rows_per_page = value
current_page = 0
_update_view()
func _update_view():
_on_grid_updated()
node_editor_view_root.refresh(false)

View File

@ -0,0 +1,9 @@
[plugin]
name="Edit Resources as Table"
description="Edit Many Resources from one Folder as a table.
Heavily inspired by Multi-Cursor-Editing in text editors, so after selecting multiple cells (in the same column!) using Ctrl+Click or Shift+Click, most Basic-to-Intermediate movements should be available."
author="Don Tnowe"
version="2.7"
script="plugin.gd"

View File

@ -0,0 +1,43 @@
@tool
extends EditorPlugin
var editor_view : Control
var undo_redo : EditorUndoRedoManager
func _enter_tree() -> void:
editor_view = load(get_script().resource_path.get_base_dir() + "/editor_view.tscn").instantiate()
editor_view.editor_interface = get_editor_interface()
if editor_view.editor_interface == null:
# 4.2: now a singleton
editor_view.editor_interface = Engine.get_singleton("EditorInterface")
editor_view.editor_plugin = self
undo_redo = get_undo_redo()
get_editor_interface().get_editor_main_screen().add_child(editor_view)
_make_visible(false)
func _exit_tree() -> void:
if is_instance_valid(editor_view):
editor_view.queue_free()
func _get_plugin_name():
return "ResourceTables"
func _make_visible(visible):
if is_instance_valid(editor_view):
editor_view.visible = visible
if visible:
editor_view.display_folder(editor_view.current_path)
func _has_main_screen():
return true
func _get_plugin_icon():
# Until I add an actual icon, this'll do.
return get_editor_interface().get_base_control().get_theme_icon("VisualShaderNodeComment", "EditorIcons")

View File

@ -0,0 +1,14 @@
{
"hidden_columns": {
},
"recent_paths": [],
"table_functions": {
"filter": [
"return true"
],
"process": [
"return value"
]
}
}

View File

@ -0,0 +1,31 @@
@tool
extends GridContainer
const PREFIX = "addons/resources_spreadsheet_view/"
func _ready():
ProjectSettings.set_setting(PREFIX + "array_color_tint", 100.0 if ProjectSettings.get_setting(PREFIX + "color_arrays", true) else 0.0)
ProjectSettings.set_setting(PREFIX + "color_arrays", null)
for x in get_children():
var setting : String = PREFIX + x.name.to_snake_case()
if x is BaseButton:
x.toggled.connect(_set_setting.bind(setting))
if !ProjectSettings.has_setting(setting):
_set_setting(x.button_pressed, setting)
else:
x.button_pressed = ProjectSettings.get_setting(setting)
elif x is Range:
x.value_changed.connect(_set_setting.bind(setting))
if !ProjectSettings.has_setting(setting):
_set_setting(x.value, setting)
else:
x.value = ProjectSettings.get_setting(setting)
func _set_setting(new_value, setting : String):
ProjectSettings.set_setting(setting, new_value)

View File

@ -0,0 +1,100 @@
[gd_scene load_steps=2 format=3 uid="uid://dhunxgcae6h1"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/settings_grid.gd" id="1_s8s2f"]
[node name="Settings" type="ScrollContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 8.0
offset_top = 8.0
offset_right = -622.0
offset_bottom = -322.0
grow_horizontal = 2
grow_vertical = 2
[node name="RichTextLabel" type="VBoxContainer" parent="."]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="GridContainer" type="GridContainer" parent="RichTextLabel"]
layout_mode = 2
columns = 2
script = ExtResource("1_s8s2f")
[node name="Label" type="Label" parent="RichTextLabel/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Color-type cells style rows"
autowrap_mode = 2
[node name="ColorRows" type="CheckBox" parent="RichTextLabel/GridContainer"]
layout_mode = 2
button_pressed = true
text = "Enable"
[node name="Label2" type="Label" parent="RichTextLabel/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Collection item color tint"
autowrap_mode = 2
[node name="ArrayColorTint" type="SpinBox" parent="RichTextLabel/GridContainer"]
layout_mode = 2
value = 100.0
[node name="Label3" type="Label" parent="RichTextLabel/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Array cell min width"
autowrap_mode = 2
[node name="ArrayMinWidth" type="SpinBox" parent="RichTextLabel/GridContainer"]
layout_mode = 2
min_value = 32.0
max_value = 512.0
value = 128.0
[node name="Label8" type="Label" parent="RichTextLabel/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Resource preview size (Including Textures)"
autowrap_mode = 2
[node name="ResourcePreviewSize" type="SpinBox" parent="RichTextLabel/GridContainer"]
layout_mode = 2
min_value = 8.0
max_value = 1024.0
value = 32.0
[node name="Label7" type="Label" parent="RichTextLabel/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Clip header text (makes columns smaller)"
autowrap_mode = 2
[node name="ClipHeaders" type="CheckBox" parent="RichTextLabel/GridContainer"]
layout_mode = 2
text = "Enable"
[node name="Label5" type="Label" parent="RichTextLabel/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Duplicate arrays on edit (slower, but can be undone)"
autowrap_mode = 2
[node name="DupeArrays" type="CheckBox" parent="RichTextLabel/GridContainer"]
layout_mode = 2
button_pressed = true
text = "Enable"
[node name="Label6" type="Label" parent="RichTextLabel/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Show context menu on left-click (and not just rightclick)"
autowrap_mode = 2
[node name="ContextMenuOnLeftclick" type="CheckBox" parent="RichTextLabel/GridContainer"]
layout_mode = 2
text = "Enable"

View File

@ -0,0 +1,149 @@
extends RefCounted
const non_typing_paragraph := ""
const non_typing_space := ""
const whitespace_chars := [
32, # " "
44, # ","
58, # ":"
45, # "-"
59, # ";"
40, # "("
41, # ")"
46, # "."
182, # "¶" Linefeed
10, # "\n" Actual Linefeed
967, # "●" Whitespace
]
static func is_character_whitespace(text : String, idx : int) -> bool:
if idx <= 0: return true # Stop at the edges.
if idx >= text.length(): return true
return text.unicode_at(idx) in whitespace_chars
static func show_non_typing(text : String) -> String:
text = text\
.replace(non_typing_paragraph, "\n")\
.replace(non_typing_space, " ")
if text.ends_with("\n"):
text = text.left(text.length() - 1) + non_typing_paragraph
elif text.ends_with(" "):
text = text.left(text.length() - 1) + non_typing_space
return text
static func revert_non_typing(text : String) -> String:
if text.ends_with(non_typing_paragraph):
text = text.left(text.length() - 1) + "\n"
elif text.ends_with(non_typing_space):
text = text.left(text.length() - 1) + " "
return text
static func multi_erase_right(values : Array, cursor_positions : Array, ctrl_pressed : bool):
for i in values.size():
var start_pos = cursor_positions[i]
cursor_positions[i] = _step_cursor(values[i], cursor_positions[i], 1, ctrl_pressed)
cursor_positions[i] = min(
cursor_positions[i],
values[i].length()
)
values[i] = (
values[i].left(start_pos)
+ values[i].substr(cursor_positions[i])
)
cursor_positions[i] = start_pos
return values
static func multi_erase_left(values : Array, cursor_positions : Array, ctrl_pressed : bool):
for i in values.size():
var start_pos = cursor_positions[i]
cursor_positions[i] = _step_cursor(values[i], cursor_positions[i], -1, ctrl_pressed)
values[i] = (
values[i].substr(0, cursor_positions[i])
+ values[i].substr(start_pos)
)
return values
static func multi_move_left(values : Array, cursor_positions : Array, ctrl_pressed : bool):
for i in cursor_positions.size():
cursor_positions[i] = _step_cursor(values[i], cursor_positions[i], -1, ctrl_pressed)
static func multi_move_right(values : Array, cursor_positions : Array, ctrl_pressed : bool):
for i in cursor_positions.size():
cursor_positions[i] = _step_cursor(values[i], cursor_positions[i], 1, ctrl_pressed)
static func multi_paste(values : Array, cursor_positions : Array):
var pasted_lines := DisplayServer.clipboard_get().replace("\r", "").split("\n")
var paste_each_line := pasted_lines.size() == values.size()
for i in values.size():
if paste_each_line:
cursor_positions[i] += pasted_lines[i].length()
else:
cursor_positions[i] += DisplayServer.clipboard_get().length()
values[i] = (
values[i].left(cursor_positions[i])
+ (pasted_lines[i] if paste_each_line else DisplayServer.clipboard_get())
+ values[i].substr(cursor_positions[i])
)
return values
static func multi_copy(values : Array):
for i in values.size():
values[i] = values[i]
DisplayServer.clipboard_set("\n".join(values))
static func multi_input(input_char : String, values : Array, cursor_positions : Array):
for i in values.size():
values[i] = (
values[i].left(cursor_positions[i])
+ input_char
+ values[i].substr(cursor_positions[i])
)
cursor_positions[i] = min(cursor_positions[i] + 1, values[i].length())
return values
static func _step_cursor(text : String, start : int, step : int = 1, ctrl_pressed : bool = false) -> int:
var cur := start
if ctrl_pressed and is_character_whitespace(text, cur + step):
cur += step
while true:
cur += step
if !ctrl_pressed or is_character_whitespace(text, cur):
if cur > text.length():
return text.length()
if cur <= 0:
return 0
if ctrl_pressed and step < 0:
return cur + 1
return cur
return 0

View File

@ -0,0 +1,36 @@
[gd_scene format=3 uid="uid://ydrs54md3knl"]
[node name="Label" type="MarginContainer"]
offset_right = 16.0
offset_bottom = 16.0
size_flags_vertical = 9
mouse_filter = 0
[node name="Box" type="HFlowContainer" parent="."]
layout_mode = 2
mouse_filter = 2
[node name="Back" type="Control" parent="."]
show_behind_parent = true
layout_mode = 2
mouse_filter = 2
[node name="ColorRect" type="ColorRect" parent="Back"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 1.0
offset_top = 1.0
offset_right = -1.0
offset_bottom = -1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
color = Color(0.247059, 0.247059, 0.247059, 0.498039)
[node name="Selected" type="ColorRect" parent="."]
visible = false
layout_mode = 2
mouse_filter = 2
color = Color(1, 1, 1, 0.247059)

View File

@ -0,0 +1,33 @@
[gd_scene format=3 uid="uid://cghfjg6qt3rb1"]
[node name="Label" type="Label"]
offset_right = 20.0
offset_bottom = 23.0
size_flags_vertical = 9
mouse_filter = 0
[node name="Back" type="ColorRect" parent="."]
show_behind_parent = true
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 1.0
offset_top = 1.0
offset_right = -1.0
offset_bottom = -1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
color = Color(0.247059, 0.247059, 0.247059, 0.498039)
[node name="Selected" type="ColorRect" parent="."]
visible = false
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
color = Color(1, 1, 1, 0.247059)

View File

@ -0,0 +1,34 @@
class_name ResourceTablesCellEditor
extends RefCounted
const TextEditingUtilsClass := preload("res://addons/resources_spreadsheet_view/text_editing_utils.gd")
const CELL_SCENE_DIR = "res://addons/resources_spreadsheet_view/typed_cells/"
var hint_strings_array := []
## Override to define where the cell should be shown.
func can_edit_value(value, type, property_hint, column_index) -> bool:
return value != null
## Override to change how the cell is created; preload a scene or create nodes from code.
## Caller is an instance of [code]editor_view.tscn[/code].
func create_cell(caller : Control) -> Control:
return load(CELL_SCENE_DIR + "basic.tscn").instantiate()
## Override to change behaviour when the cell is clicked to be selected.
func set_selected(node : Control, selected : bool):
node.get_node("Selected").visible = selected
## Override to change how the value is displayed.
func set_value(node : Control, value):
node.text = TextEditingUtilsClass.show_non_typing(str(value))
## Override to prevent the cell from being edited as text.
func is_text():
return true
## Override to change behaviour when there are color cells to the left of this cell.
func set_color(node : Control, color : Color):
node.get_node("Back").modulate = color * 1.0

View File

@ -0,0 +1,49 @@
extends ResourceTablesCellEditor
const TablesPluginSettingsClass := preload("res://addons/resources_spreadsheet_view/settings_grid.gd")
func can_edit_value(value, type, property_hint, column_index) -> bool:
return type == TYPE_PACKED_STRING_ARRAY or type == TYPE_ARRAY
func create_cell(caller : Control) -> Control:
return load(CELL_SCENE_DIR + "array.tscn").instantiate()
func set_value(node : Control, value):
var children := node.get_node("Box").get_children()
node.custom_minimum_size.x = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "array_min_width")
var color_tint : float = 0.01 * ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "array_color_tint", 100.0)
while children.size() < value.size():
children.append(Label.new())
node.get_node("Box").add_child(children[children.size() - 1])
var column_hints = hint_strings_array[node.get_index() % hint_strings_array.size()]
for i in children.size():
if i >= value.size():
children[i].visible = false
else:
children[i].visible = true
_write_value_to_child(value[i], value[i], column_hints, children[i], color_tint)
func _write_value_to_child(value, key, hint_arr : PackedStringArray, child : Label, color_tint : float):
if value is Resource:
value = _resource_to_string(value)
child.text = str(value)
child.self_modulate = (
Color.WHITE * (1.0 - color_tint)
+
(Color(str(key).hash()) + Color(0.2, 0.2, 0.2, 1.0)) * color_tint
)
func _resource_to_string(res : Resource):
return res.resource_name if res.resource_name != "" else res.resource_path.get_file()
func is_text():
return false

View File

@ -0,0 +1,18 @@
extends ResourceTablesCellEditor
func can_edit_value(value, type, property_hint, column_index) -> bool:
return type == TYPE_BOOL
func set_value(node : Control, value):
if value is bool:
_set_value_internal(node, value)
else:
_set_value_internal(node, !node.text.begins_with("O"))
func _set_value_internal(node, value):
node.text = "ON" if value else "off"
node.self_modulate.a = 1.0 if value else 0.2

View File

@ -0,0 +1,35 @@
extends ResourceTablesCellEditor
var _cached_color := Color.WHITE
func create_cell(caller : Control) -> Control:
var node : Label = load(CELL_SCENE_DIR + "basic.tscn").instantiate()
var color := ColorRect.new()
node.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
node.custom_minimum_size.x = 56
node.add_child(color)
color.name = "Color"
_resize_color_rect.call_deferred(color)
return node
func _resize_color_rect(rect):
if !is_instance_valid(rect): return # Table refreshed twice, probably? Either way, this fix is easier
rect.size = Vector2(8, 0)
rect.set_anchors_and_offsets_preset(Control.PRESET_LEFT_WIDE, Control.PRESET_MODE_KEEP_WIDTH)
func can_edit_value(value, type, property_hint, property_hint_string) -> bool:
return type == TYPE_COLOR
func set_value(node : Control, value):
if value is String:
node.text = TextEditingUtilsClass.show_non_typing(str(value))
else:
node.text = value.to_html(true)
_cached_color = value
node.get_node("Color").color = value

View File

@ -0,0 +1,37 @@
extends "res://addons/resources_spreadsheet_view/typed_cells/cell_editor_array.gd"
func can_edit_value(value, type, property_hint, column_index) -> bool:
return type == TYPE_DICTIONARY
func create_cell(caller : Control) -> Control:
return load(CELL_SCENE_DIR + "array.tscn").instantiate()
func set_value(node : Control, value):
var children = node.get_node("Box").get_children()
node.custom_minimum_size.x = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "array_min_width")
var color_tint : float = 0.01 * ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "array_color_tint", 100.0)
while children.size() < value.size():
children.append(Label.new())
node.get_node("Box").add_child(children[children.size() - 1])
var column_hints = hint_strings_array[node.get_index() % hint_strings_array.size()]
var values : Array = value.values()
var keys : Array = value.keys()
for i in children.size():
if i >= values.size():
children[i].visible = false
else:
children[i].visible = true
if values[i] is Resource: values[i] = _resource_to_string(values[i])
if keys[i] is Resource: keys[i] = _resource_to_string(keys[i])
_write_value_to_child("%s%s" % [keys[i], values[i]], keys[i], column_hints, children[i], color_tint)
func is_text():
return false

View File

@ -0,0 +1,38 @@
extends ResourceTablesCellEditor
func can_edit_value(value, type, property_hint, column_index) -> bool:
return type == TYPE_INT and property_hint == PROPERTY_HINT_ENUM
func set_value(node : Control, value):
if value == null:
# Sometimes, when creating new property, becomes null
value = 0
var value_str : String
var key_found := -1
var hint_arr : Array = hint_strings_array[node.get_index() % hint_strings_array.size()]
for i in hint_arr.size():
var colon_found : int = hint_arr[i].rfind(":")
if colon_found == -1:
key_found = value
break
if hint_arr[i].substr(colon_found + 1).to_int() == value:
key_found = i
break
if key_found != -1:
value_str = hint_arr[key_found]
else:
value_str = "?:%s" % value
node.text = value_str
node.self_modulate = Color(node.text.hash()) + Color(0.25, 0.25, 0.25, 1.0)
node.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
func is_text():
return false

View File

@ -0,0 +1,39 @@
extends "res://addons/resources_spreadsheet_view/typed_cells/cell_editor_array.gd"
func can_edit_value(value, type, property_hint, column_index) -> bool:
if (
type != TYPE_PACKED_INT32_ARRAY
and type != TYPE_PACKED_INT64_ARRAY
and type != TYPE_ARRAY
) or property_hint != PROPERTY_HINT_TYPE_STRING:
return false
return hint_strings_array[column_index][0].begins_with("2/2:")
func _write_value_to_child(value, key, hint_arr : PackedStringArray, child : Label, color_tint : float):
var value_str : String
var key_found := -1
for i in hint_arr.size():
var colon_found := hint_arr[i].rfind(":")
if colon_found == -1:
key_found = value
break
if hint_arr[i].substr(colon_found + 1).to_int() == value:
key_found = i
break
if key_found == 0:
# Enum array hints have "2/3:" before list.
var found := hint_arr[0].find(":") + 1
value_str = hint_arr[0].substr(hint_arr[0].find(":") + 1)
elif key_found != -1:
value_str = hint_arr[key_found]
else:
value_str = "?:%s" % value
super(value_str, value_str, hint_arr, child, color_tint)

View File

@ -0,0 +1,60 @@
extends ResourceTablesCellEditor
const TablesPluginSettingsClass := preload("res://addons/resources_spreadsheet_view/settings_grid.gd")
var previewer : EditorResourcePreview
func can_edit_value(value, type, property_hint, column_index) -> bool:
return type == TYPE_OBJECT
func create_cell(caller : Control) -> Control:
if previewer == null:
previewer = caller.editor_plugin.get_editor_interface().get_resource_previewer()
var node = load(CELL_SCENE_DIR + "resource.tscn").instantiate()
return node
func set_value(node : Control, value):
if value == null:
node.get_node("Box/Tex").visible = false
node.get_node("Box/Label").text = "[empty]"
node.editor_description = ""
if !value is Resource: return
node.editor_description = value.resource_path
if value.resource_name == "":
node.get_node("Box/Label").text = "[" + value.resource_path.get_file().get_basename() + "]"
else:
node.get_node("Box/Label").text = value.resource_name
if value is Texture:
node.get_node("Box/Tex").visible = true
node.get_node("Box/Tex").texture = value
else:
node.get_node("Box/Tex").visible = false
previewer.queue_resource_preview(value.resource_path, self, &"_on_preview_loaded", node)
node.get_node("Box/Tex").custom_minimum_size = Vector2.ONE * ProjectSettings.get_setting(
TablesPluginSettingsClass.PREFIX + "resource_preview_size"
)
func set_color(node : Control, color : Color):
node.get_node("Back").modulate = color * 0.6 if node.editor_description == "" else color
func is_text():
return false
func _on_preview_loaded(path : String, preview : Texture, thumbnail_preview : Texture, node):
# Abort if the node has been deleted since.
if is_instance_valid(node):
node.get_node("Box/Tex").visible = true
node.get_node("Box/Tex").texture = preview

View File

@ -0,0 +1,5 @@
extends ResourceTablesCellEditor
func set_color(node : Control, color : Color):
node.get_node("Back").modulate = color * 0.6 if !node.text.is_valid_float() else color

View File

@ -0,0 +1,45 @@
[gd_scene format=3 uid="uid://clcndgxaty503"]
[node name="Label" type="MarginContainer"]
size_flags_vertical = 9
mouse_filter = 0
[node name="Back" type="Control" parent="."]
show_behind_parent = true
layout_mode = 2
mouse_filter = 2
[node name="ColorRect" type="ColorRect" parent="Back"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 1.0
offset_top = 1.0
offset_right = -2.0
offset_bottom = -1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
color = Color(0.247059, 0.247059, 0.247059, 0.498039)
[node name="Selected" type="ColorRect" parent="."]
visible = false
layout_mode = 2
mouse_filter = 2
color = Color(1, 1, 1, 0.247059)
[node name="Box" type="HBoxContainer" parent="."]
layout_mode = 2
mouse_filter = 2
[node name="Tex" type="TextureRect" parent="Box"]
layout_mode = 2
mouse_filter = 2
expand_mode = 1
stretch_mode = 5
[node name="Label" type="Label" parent="Box"]
layout_mode = 2
size_flags_horizontal = 3
text = "res.tres"

View File

@ -0,0 +1,222 @@
@tool
extends ResourceTablesDockEditor
@onready var recent_container := $"HBoxContainer/Control2/HBoxContainer/HFlowContainer"
@onready var contents_label := $"HBoxContainer/HBoxContainer/Panel/Label"
@onready var button_box := $"HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"
@onready var value_input := $"HBoxContainer/HBoxContainer/Control/VBoxContainer/LineEdit"
var _stored_value
var _stored_type := 0
func _ready():
super()
contents_label.text_changed.connect(_on_contents_edit_text_changed)
func try_edit_value(value, type, property_hint) -> bool:
if (
type != TYPE_ARRAY and type != TYPE_PACKED_STRING_ARRAY
and type != TYPE_PACKED_INT32_ARRAY and type != TYPE_PACKED_FLOAT32_ARRAY
and type != TYPE_PACKED_INT64_ARRAY and type != TYPE_PACKED_FLOAT64_ARRAY
):
return false
if sheet.column_hint_strings[sheet.get_selected_column()][0].begins_with("2/2:"):
# For enums, prefer the specialized dock.
return false
_stored_type = type
_stored_value = value.duplicate() # Generic arrays are passed by reference
contents_label.text = str(value)
var is_generic_array = _stored_type == TYPE_ARRAY and !value.is_typed()
button_box.get_child(1).visible = (
is_generic_array or value.get_typed_builtin() == TYPE_STRING
or _stored_type == TYPE_PACKED_STRING_ARRAY
)
button_box.get_child(2).visible = (
is_generic_array or value.get_typed_builtin() == TYPE_INT
or _stored_type == TYPE_PACKED_INT32_ARRAY or _stored_type == TYPE_PACKED_INT64_ARRAY
)
button_box.get_child(3).visible = (
is_generic_array or value.get_typed_builtin() == TYPE_FLOAT
or _stored_type == TYPE_PACKED_FLOAT32_ARRAY or _stored_type == TYPE_PACKED_FLOAT64_ARRAY
)
button_box.get_child(5).visible = (
is_generic_array or value.get_typed_builtin() == TYPE_OBJECT
)
if value.get_typed_builtin() == TYPE_OBJECT:
if !value_input is EditorResourcePicker:
var new_input := EditorResourcePicker.new()
new_input.size_flags_horizontal = SIZE_EXPAND_FILL
new_input.base_type = value.get_typed_class_name()
value_input.replace_by(new_input)
value_input.free()
value_input = new_input
else:
if !value_input is LineEdit:
var new_input := LineEdit.new()
new_input.size_flags_horizontal = SIZE_EXPAND_FILL
value_input.replace_by(new_input)
value_input.free()
value_input = new_input
return true
func _add_value(value):
_stored_value.append(value)
var values = sheet.get_edited_cells_values()
var cur_value
var dupe_array : bool = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "dupe_arrays")
for i in values.size():
cur_value = values[i]
if dupe_array:
cur_value = cur_value.duplicate()
cur_value.append(value)
values[i] = cur_value
sheet.set_edited_cells_values(values)
func _remove_value(value):
_stored_value.remove_at(_stored_value.find(value))
var values = sheet.get_edited_cells_values()
var cur_value : Array
var dupe_array : bool = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "dupe_arrays")
for i in values.size():
cur_value = values[i]
if dupe_array:
cur_value = cur_value.duplicate()
if cur_value.has(value): # erase() not defined in PoolArrays
cur_value.remove_at(cur_value.find(value))
values[i] = cur_value
sheet.set_edited_cells_values(values)
func _add_recent(value):
for x in recent_container.get_children():
if x.text == str(value):
return
if value is Resource and x.tooltip_text == value.resource_path:
return
var node := Button.new()
var value_str : String = str(value)
if value is Resource:
value_str = value.resource_path.get_file()
node.tooltip_text = value.resource_path
value = value.resource_path
node.text = value_str
node.self_modulate = Color(value_str.hash()) + Color(0.25, 0.25, 0.25, 1.0)
node.pressed.connect(_on_recent_clicked.bind(node, value))
recent_container.add_child(node)
func _on_recent_clicked(button, value):
var val = recent_container.get_child(1).selected
value_input.text = str(value)
if val == 0:
_add_value(value)
if val == 1:
_remove_value(value)
if val == 2:
button.queue_free()
func _on_Remove_pressed():
if value_input is EditorResourcePicker:
_remove_value(value_input.edited_resource)
elif str_to_var(value_input.text) != null:
_remove_value(str_to_var(value_input.text))
else:
_remove_value(value_input.text)
func _on_RemoveLast_pressed():
_stored_value.pop_back()
var values = sheet.get_edited_cells_values()
var cur_value : Array
var dupe_array : bool = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "dupe_arrays")
for i in values.size():
cur_value = values[i]
if dupe_array:
cur_value = cur_value.duplicate()
cur_value.pop_back()
values[i] = cur_value
sheet.set_edited_cells_values(values)
func _on_ClearRecent_pressed():
for i in recent_container.get_child_count():
if i == 0: continue
recent_container.get_child(i).free()
func _on_Float_pressed():
_add_value(value_input.text.to_float())
func _on_Int_pressed():
_add_value(value_input.text.to_int())
func _on_String_pressed():
_add_value(value_input.text)
_add_recent(value_input.text)
func _on_Variant_pressed():
if value_input is EditorResourcePicker:
_add_value(value_input.edited_resource)
else:
_add_value(str_to_var(value_input.text))
func _on_Resource_pressed():
if value_input is LineEdit:
_add_value(load(value_input.text))
elif value_input is EditorResourcePicker:
_add_value(value_input.edited_resource)
func _on_AddRecentFromSel_pressed():
for x in sheet.get_edited_cells_values():
for y in x:
_add_recent(y)
func _on_contents_edit_text_changed():
var value := str_to_var(contents_label.text)
if !value is Array:
return
var values = sheet.get_edited_cells_values()
for i in values.size():
values[i] = values[i].duplicate()
values[i].resize(value.size())
for j in value.size():
values[i][j] = value[j]
_stored_value = value
sheet.set_edited_cells_values(values)

View File

@ -0,0 +1,190 @@
[gd_scene load_steps=5 format=3 uid="uid://c3a2cip8ffccv"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_editors/dock_array.gd" id="1"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/editor_icon_button.gd" id="2"]
[sub_resource type="Image" id="Image_ytggl"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_3oshq"]
image = SubResource("Image_ytggl")
[node name="EditArray" type="VBoxContainer"]
anchors_preset = 10
anchor_right = 1.0
grow_horizontal = 2
mouse_filter = 0
script = ExtResource("1")
[node name="Header" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="Header"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="Header/HBoxContainer"]
layout_mode = 2
text = "EDIT: Array"
[node name="HSeparator" type="HSeparator" parent="Header/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
mouse_filter = 2
[node name="Label" type="Label" parent="Header"]
layout_mode = 2
text = "PROPERTY NAME"
[node name="HSeparator2" type="HSeparator" parent="Header"]
layout_mode = 2
size_flags_horizontal = 3
mouse_filter = 2
[node name="HBoxContainer" type="HSplitContainer" parent="."]
layout_mode = 2
size_flags_vertical = 3
split_offset = 380
[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer"]
layout_mode = 2
alignment = 2
[node name="Panel" type="MarginContainer" parent="HBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="TextEdit" parent="HBoxContainer/HBoxContainer/Panel"]
layout_mode = 2
size_flags_vertical = 5
text = "[]"
[node name="VSeparator2" type="VSeparator" parent="HBoxContainer/HBoxContainer"]
layout_mode = 2
[node name="Control" type="MarginContainer" parent="HBoxContainer/HBoxContainer"]
layout_mode = 2
[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer/HBoxContainer/Control"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer"]
layout_mode = 2
placeholder_text = "Input value to add/erase..."
clear_button_enabled = true
[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "Add:"
[node name="String" type="Button" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Add string"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2")
icon_name = "String"
[node name="Int" type="Button" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Add integer"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2")
icon_name = "int"
[node name="Float" type="Button" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Add float"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2")
icon_name = "float"
[node name="Variant" type="Button" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Guess type and add"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2")
icon_name = "Variant"
[node name="Object" type="Button" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Add resource (by path if string)"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2")
icon_name = "Object"
[node name="Label2" type="Label" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "Erase:"
[node name="Remove" type="Button" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Find and erase value in textbox"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2")
icon_name = "Remove"
[node name="Remove2" type="Button" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Remove last value"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2")
icon_name = "MoveLeft"
[node name="VSeparator" type="VSeparator" parent="HBoxContainer/HBoxContainer"]
layout_mode = 2
[node name="Control2" type="MarginContainer" parent="HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer/Control2"]
layout_mode = 2
alignment = 2
[node name="VSeparator2" type="VSeparator" parent="HBoxContainer/Control2/HBoxContainer"]
layout_mode = 2
[node name="HFlowContainer" type="HFlowContainer" parent="HBoxContainer/Control2/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="HBoxContainer/Control2/HBoxContainer/HFlowContainer"]
layout_mode = 2
text = "Recent:"
[node name="OptionButton" type="OptionButton" parent="HBoxContainer/Control2/HBoxContainer/HFlowContainer"]
layout_mode = 2
item_count = 3
selected = 0
fit_to_longest_item = false
popup/item_0/text = "Add"
popup/item_0/id = 0
popup/item_1/text = "Erase"
popup/item_1/id = 1
popup/item_2/text = "Remove From Recent"
popup/item_2/id = 2
[node name="AddRecentFromSel" type="Button" parent="HBoxContainer/Control2/HBoxContainer/HFlowContainer"]
layout_mode = 2
tooltip_text = "Add from selected cells"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2")
icon_name = "ListSelect"
[connection signal="pressed" from="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer/String" to="." method="_on_String_pressed"]
[connection signal="pressed" from="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer/Int" to="." method="_on_Int_pressed"]
[connection signal="pressed" from="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer/Float" to="." method="_on_Float_pressed"]
[connection signal="pressed" from="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer/Variant" to="." method="_on_Variant_pressed"]
[connection signal="pressed" from="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer/Object" to="." method="_on_Resource_pressed"]
[connection signal="pressed" from="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer/Remove" to="." method="_on_Remove_pressed"]
[connection signal="pressed" from="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer/Remove2" to="." method="_on_RemoveLast_pressed"]
[connection signal="pressed" from="HBoxContainer/Control2/HBoxContainer/HFlowContainer/AddRecentFromSel" to="." method="_on_AddRecentFromSel_pressed"]

View File

@ -0,0 +1,46 @@
@tool
class_name ResourceTablesDockEditor
extends Control
const TablesPluginSettingsClass := preload("res://addons/resources_spreadsheet_view/settings_grid.gd")
@export var path_property_name := NodePath("Header/Label")
var sheet : Control
var selection : Array
var _resize_target_height := 0.0
func _ready():
var parent := get_parent()
while parent != null and !parent.has_method(&"display_folder"):
parent = parent.get_parent()
sheet = parent
get_node(path_property_name).add_theme_font_override(&"normal", get_theme_font(&"bold", &"EditorFonts"))
$"Header".gui_input.connect(_on_header_gui_input)
$"Header".mouse_filter = MOUSE_FILTER_STOP
$"Header".mouse_default_cursor_shape = CURSOR_VSIZE
## Override to define when to show the dock and, if it can edit the value, how to handle it.
func try_edit_value(value, type : int, property_hint : String) -> bool:
return true
## Override to define behaviour when stretching the header to change size.
func resize_drag(to_height : float):
return
func _on_header_gui_input(event : InputEvent):
if event is InputEventMouseMotion:
var pressed := Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)
if pressed:
_resize_target_height -= event.relative.y
custom_minimum_size.y = clamp(_resize_target_height, 0.0, get_viewport().size.y * 0.75)
resize_drag(_resize_target_height)
get_child(1).visible = _resize_target_height > $"Header".size.y
if event is InputEventMouseButton:
_resize_target_height = custom_minimum_size.y

View File

@ -0,0 +1,139 @@
@tool
extends ResourceTablesDockEditor
@onready var _value_rect := $"EditColor/ColorProper/ColorRect"
@onready var _color_picker_panel := $"EditColor/VSeparator6/Panel"
@onready var _color_picker := $"EditColor/VSeparator6/Panel/MarginContainer/ColorPicker"
@onready var _custom_value_edit := $"EditColor/CustomX/Box/LineEdit"
var _stored_value := Color.WHITE
var _resize_height_small := 0.0
var _resize_expanded := true
func _ready():
super._ready()
_connect_buttons($"EditColor/RGBGrid", 0, 0)
_connect_buttons($"EditColor/RGBGrid", 5, 1)
_connect_buttons($"EditColor/RGBGrid", 10, 2)
_connect_buttons($"EditColor/HSVGrid", 0, 3)
_connect_buttons($"EditColor/HSVGrid", 5, 4)
_connect_buttons($"EditColor/HSVGrid", 10, 5)
_resize_height_small = get_child(1).get_minimum_size().y
func _connect_buttons(grid, start_index, property_bind):
grid.get_child(start_index + 0).pressed.connect(_increment_values_custom.bind(-1.0, property_bind))
grid.get_child(start_index + 1).pressed.connect(_increment_values.bind(-10.0, property_bind))
grid.get_child(start_index + 3).pressed.connect(_increment_values.bind(10.0, property_bind))
grid.get_child(start_index + 4).pressed.connect(_increment_values_custom.bind(1.0, property_bind))
func try_edit_value(value, type, property_hint) -> bool:
_color_picker_panel.top_level = false
if type != TYPE_COLOR:
return false
_set_stored_value(value)
_color_picker_panel.visible = false
return true
func resize_drag(to_height : float):
var expanded := to_height > _resize_height_small
if _resize_expanded == expanded:
return
_resize_expanded = expanded
$"EditColor/RGBGrid".visible = expanded
$"EditColor/ColorProper".visible = expanded
$"EditColor/HSVGrid".columns = 5 if expanded else 15
$"EditColor/CustomX/Label".visible = expanded
func _set_stored_value(v):
_stored_value = v
_color_picker.color = v
_value_rect.color = v
func _increment_values(by : float, property : int):
var cell_values = sheet.get_edited_cells_values()
match property:
0:
_stored_value.r += by / 255.0
for i in cell_values.size():
cell_values[i].r += by / 255.0
1:
_stored_value.g += by / 255.0
for i in cell_values.size():
cell_values[i].g += by / 255.0
2:
_stored_value.b += by / 255.0
for i in cell_values.size():
cell_values[i].b += by / 255.0
3:
# Hue has 360 degrees and loops
_stored_value.h += by / 360.0
for i in cell_values.size():
cell_values[i].h = fposmod(cell_values[i].h + by / 360.0, 1.0)
4:
_stored_value.s += by * 0.005
for i in cell_values.size():
cell_values[i].s += by * 0.005
5:
_stored_value.v += by * 0.005
for i in cell_values.size():
cell_values[i].v += by * 0.005
_set_stored_value(_stored_value)
sheet.set_edited_cells_values(cell_values)
func _increment_values_custom(multiplier : float, property : int):
if property == 4 or property == 5:
# Numbered buttons increment by 5 for Sat and Value, so hue is x0.5 effect. Negate it here
multiplier *= 2.0
_increment_values(_custom_value_edit.text.to_float() * multiplier, property)
func _on_Button_pressed():
_color_picker_panel.visible = !_color_picker_panel.visible
if _color_picker_panel.visible:
_color_picker_panel.top_level = true
_color_picker_panel.global_position = (
sheet.global_position
+ Vector2(0, sheet.size.y - _color_picker_panel.size.y)
+ Vector2(16, -16)
)
_color_picker_panel.global_position.y = clamp(
_color_picker_panel.global_position.y,
0,
sheet.editor_plugin.get_editor_interface().get_base_control().size.y
)
_color_picker.color = _stored_value
elif _color_picker.color != _stored_value:
_set_stored_value(_color_picker.color)
update_cell_values()
func _on_ColorPicker_gui_input(event : InputEvent):
if event is InputEventMouseButton and !event.pressed:
_set_stored_value(_color_picker.color)
update_cell_values()
func update_cell_values():
var values = sheet.get_edited_cells_values()
for i in values.size():
values[i] = _stored_value
sheet.set_edited_cells_values(values)

View File

@ -0,0 +1,295 @@
[gd_scene load_steps=2 format=3 uid="uid://b3a3bo6cfyh5t"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_editors/dock_color.gd" id="1"]
[node name="EditColor" type="VBoxContainer"]
offset_bottom = 131.0
grow_horizontal = 2
mouse_filter = 0
script = ExtResource("1")
[node name="Header" type="HBoxContainer" parent="."]
layout_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 9
[node name="HBoxContainer" type="HBoxContainer" parent="Header"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="Header/HBoxContainer"]
layout_mode = 2
text = "EDIT: Color"
[node name="HSeparator" type="HSeparator" parent="Header/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
mouse_filter = 2
[node name="Label" type="Label" parent="Header"]
layout_mode = 2
text = "PROPERTY NAME"
[node name="HSeparator2" type="HSeparator" parent="Header"]
layout_mode = 2
size_flags_horizontal = 3
mouse_filter = 2
[node name="EditColor" type="HBoxContainer" parent="."]
layout_mode = 2
size_flags_vertical = 3
alignment = 1
[node name="VSeparator7" type="Control" parent="EditColor"]
layout_mode = 2
size_flags_horizontal = 3
[node name="VSeparator3" type="Control" parent="EditColor"]
visible = false
layout_mode = 2
[node name="ButtonRowTemplate" type="Control" parent="EditColor"]
visible = false
layout_mode = 2
[node name="Button3" type="Button" parent="EditColor/ButtonRowTemplate"]
layout_mode = 0
text = "-X"
[node name="Button2" type="Button" parent="EditColor/ButtonRowTemplate"]
layout_mode = 0
text = "-5"
[node name="Label" type="Label" parent="EditColor/ButtonRowTemplate"]
layout_mode = 0
text = "Red"
[node name="Button5" type="Button" parent="EditColor/ButtonRowTemplate"]
layout_mode = 0
text = "+5"
[node name="Button6" type="Button" parent="EditColor/ButtonRowTemplate"]
layout_mode = 0
text = "+X"
[node name="RGBGrid" type="GridContainer" parent="EditColor"]
layout_mode = 2
columns = 5
[node name="Button3" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(1, 0.780392, 0.780392, 1)
layout_mode = 2
text = "-X"
[node name="Button2" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(1, 0.780392, 0.780392, 1)
layout_mode = 2
text = "-10"
[node name="Label" type="Label" parent="EditColor/RGBGrid"]
layout_mode = 2
text = "Red"
horizontal_alignment = 1
[node name="Button5" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(1, 0.780392, 0.780392, 1)
layout_mode = 2
text = "+10"
[node name="Button6" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(1, 0.780392, 0.780392, 1)
layout_mode = 2
text = "+X"
[node name="Button7" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(0.666667, 1, 0.745098, 1)
layout_mode = 2
text = "-X"
[node name="Button8" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(0.666667, 1, 0.745098, 1)
layout_mode = 2
text = "-10"
[node name="Label2" type="Label" parent="EditColor/RGBGrid"]
layout_mode = 2
text = "Green"
horizontal_alignment = 1
[node name="Button11" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(0.666667, 1, 0.745098, 1)
layout_mode = 2
text = "+10"
[node name="Button12" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(0.666667, 1, 0.745098, 1)
layout_mode = 2
text = "+X"
[node name="Button13" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(0.772549, 0.792157, 1, 1)
layout_mode = 2
text = "-X"
[node name="Button14" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(0.772549, 0.792157, 1, 1)
layout_mode = 2
text = "-10"
[node name="Label3" type="Label" parent="EditColor/RGBGrid"]
layout_mode = 2
text = "Blue"
horizontal_alignment = 1
[node name="Button17" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(0.772549, 0.792157, 1, 1)
layout_mode = 2
text = "+10"
[node name="Button18" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(0.772549, 0.792157, 1, 1)
layout_mode = 2
text = "+X"
[node name="VSeparator" type="VSeparator" parent="EditColor"]
layout_mode = 2
[node name="ColorProper" type="VBoxContainer" parent="EditColor"]
layout_mode = 2
[node name="ColorRect" type="ColorRect" parent="EditColor/ColorProper"]
layout_mode = 2
size_flags_vertical = 3
[node name="Button" type="Button" parent="EditColor/ColorProper"]
layout_mode = 2
text = "Choose Color"
[node name="VSeparator2" type="VSeparator" parent="EditColor"]
layout_mode = 2
[node name="HSVGrid" type="GridContainer" parent="EditColor"]
layout_mode = 2
columns = 5
[node name="Button3" type="Button" parent="EditColor/HSVGrid"]
modulate = Color(1, 0.913725, 0.776471, 1)
layout_mode = 2
text = "-X"
[node name="Button2" type="Button" parent="EditColor/HSVGrid"]
modulate = Color(0.898039, 1, 0.698039, 1)
layout_mode = 2
text = "-10"
[node name="Label" type="Label" parent="EditColor/HSVGrid"]
layout_mode = 2
text = "Hue"
horizontal_alignment = 1
[node name="Button5" type="Button" parent="EditColor/HSVGrid"]
modulate = Color(0.717647, 1, 0.980392, 1)
layout_mode = 2
text = "+10"
[node name="Button6" type="Button" parent="EditColor/HSVGrid"]
modulate = Color(0.74902, 0.729412, 1, 1)
layout_mode = 2
text = "+X"
[node name="Button7" type="Button" parent="EditColor/HSVGrid"]
layout_mode = 2
text = "-X"
[node name="Button8" type="Button" parent="EditColor/HSVGrid"]
layout_mode = 2
text = "-5"
[node name="Label2" type="Label" parent="EditColor/HSVGrid"]
layout_mode = 2
text = "Sat"
horizontal_alignment = 1
[node name="Button11" type="Button" parent="EditColor/HSVGrid"]
layout_mode = 2
text = "+5"
[node name="Button12" type="Button" parent="EditColor/HSVGrid"]
layout_mode = 2
text = "+X"
[node name="Button13" type="Button" parent="EditColor/HSVGrid"]
layout_mode = 2
text = "-X"
[node name="Button14" type="Button" parent="EditColor/HSVGrid"]
layout_mode = 2
text = "-5"
[node name="Label3" type="Label" parent="EditColor/HSVGrid"]
layout_mode = 2
text = "Value"
horizontal_alignment = 1
[node name="Button17" type="Button" parent="EditColor/HSVGrid"]
layout_mode = 2
text = "+5"
[node name="Button18" type="Button" parent="EditColor/HSVGrid"]
layout_mode = 2
text = "+X"
[node name="VSeparator4" type="VSeparator" parent="EditColor"]
layout_mode = 2
[node name="CustomX" type="VBoxContainer" parent="EditColor"]
layout_mode = 2
[node name="Label" type="Label" parent="EditColor/CustomX"]
layout_mode = 2
size_flags_vertical = 6
text = "Custom Value"
horizontal_alignment = 1
[node name="Box" type="HBoxContainer" parent="EditColor/CustomX"]
layout_mode = 2
[node name="Label2" type="Label" parent="EditColor/CustomX/Box"]
layout_mode = 2
size_flags_vertical = 6
text = "X ="
[node name="LineEdit" type="LineEdit" parent="EditColor/CustomX/Box"]
layout_mode = 2
size_flags_horizontal = 3
text = "20"
[node name="VSeparator6" type="Control" parent="EditColor"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Panel" type="PanelContainer" parent="EditColor/VSeparator6"]
visible = false
layout_mode = 1
anchors_preset = 2
anchor_top = 1.0
anchor_bottom = 1.0
grow_vertical = 0
[node name="Panel2" type="Panel" parent="EditColor/VSeparator6/Panel"]
self_modulate = Color(1.5, 1.5, 1.5, 1)
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="EditColor/VSeparator6/Panel"]
layout_mode = 2
[node name="ColorPicker" type="ColorPicker" parent="EditColor/VSeparator6/Panel/MarginContainer"]
layout_mode = 2
[node name="Button" type="Button" parent="EditColor/VSeparator6/Panel/MarginContainer/ColorPicker"]
layout_mode = 2
text = "OK"
[connection signal="pressed" from="EditColor/ColorProper/Button" to="." method="_on_Button_pressed"]
[connection signal="gui_input" from="EditColor/VSeparator6/Panel/MarginContainer/ColorPicker" to="." method="_on_ColorPicker_gui_input"]
[connection signal="pressed" from="EditColor/VSeparator6/Panel/MarginContainer/ColorPicker/Button" to="." method="_on_Button_pressed"]

View File

@ -0,0 +1,196 @@
@tool
extends "res://addons/resources_spreadsheet_view/typed_editors/dock_array.gd"
enum {
KEY_TYPE_STRINGNAME = 0,
KEY_TYPE_INT,
KEY_TYPE_FLOAT,
KEY_TYPE_OBJECT,
KEY_TYPE_VARIANT,
}
@onready var key_input := $"HBoxContainer/HBoxContainer/Control/VBoxContainer/KeyEdit/KeyEdit"
@onready var key_type := $"HBoxContainer/HBoxContainer/Control/VBoxContainer/KeyEdit/KeyType"
var _key_type_selected := 0
func try_edit_value(value, type, property_hint) -> bool:
if type != TYPE_DICTIONARY and type != TYPE_OBJECT:
return false
if value is Texture2D:
# For textures, prefer the specialized dock.
return false
key_type.visible = type != TYPE_OBJECT
_stored_type = type
if type == TYPE_DICTIONARY:
_stored_value = value.duplicate()
contents_label.text = var_to_str_no_sort(value)
return true
func _add_value(value):
var key = _get_key_from_box()
_stored_value[key] = value
var values = sheet.get_edited_cells_values()
var cur_value
var dupe_value : bool = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "dupe_arrays")
for i in values.size():
cur_value = values[i]
if dupe_value and (_stored_type == TYPE_DICTIONARY or cur_value.resource_path.rfind("::") != -1):
cur_value = cur_value.duplicate()
cur_value[key] = value
values[i] = cur_value
sheet.set_edited_cells_values(values)
super._add_recent(key)
func _remove_value(_value):
var key = _get_key_from_box()
_stored_value.erase(key)
var values = sheet.get_edited_cells_values()
var cur_value
var dupe_value : bool = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "dupe_arrays")
for i in values.size():
cur_value = values[i]
if dupe_value and (_stored_type == TYPE_DICTIONARY or cur_value.resource_path.rfind("::") != -1):
cur_value = cur_value.duplicate()
cur_value.erase(key)
values[i] = cur_value
sheet.set_edited_cells_values(values)
func _get_key_from_box():
if _stored_type == TYPE_OBJECT:
return StringName(key_input.text)
return _to_key(key_input.text, _key_type_selected)
func _to_key(from : String, key_type : int):
match key_type:
KEY_TYPE_STRINGNAME:
return StringName(from)
KEY_TYPE_INT:
return from.to_int()
KEY_TYPE_FLOAT:
return from.to_float()
KEY_TYPE_OBJECT:
return load(from)
KEY_TYPE_VARIANT:
return str_to_var(from)
func _on_Replace_pressed():
var old_key = _to_key(key_input.text, _key_type_selected)
var new_key = _to_key(value_input.text, _key_type_selected)
_stored_value[new_key] = _stored_value[old_key]
var values = sheet.get_edited_cells_values()
var cur_value
var dupe_value : bool = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "dupe_arrays")
for i in values.size():
cur_value = values[i]
if dupe_value and (_stored_type == TYPE_DICTIONARY or cur_value.resource_path.rfind("::") != -1):
cur_value = cur_value.duplicate()
cur_value[new_key] = cur_value[old_key]
values[i] = cur_value
sheet.set_edited_cells_values(values)
func _add_recent(_value):
pass
func _on_recent_clicked(button, value):
var val = recent_container.get_child(1).selected
key_input.text = str(value)
if val == 0:
# Do nothing! What if the value for the key doesn't match?
pass
if val == 1:
_remove_value(value)
if val == 2:
button.queue_free()
func _on_key_type_selected(index : int):
_key_type_selected = index
func _on_AddRecentFromSel_pressed():
for x in sheet.get_edited_cells_values():
if _stored_type == TYPE_OBJECT:
for y in x.get_property_list():
if y[&"usage"] & PROPERTY_USAGE_EDITOR != 0:
super._add_recent(y[&"name"])
else:
for y in x:
super._add_recent(y)
func _on_contents_edit_text_changed():
var value := str_to_var(contents_label.text)
if !value is Dictionary:
return
var values = sheet.get_edited_cells_values()
for i in values.size():
values[i] = value.duplicate()
_stored_value = value
sheet.set_edited_cells_values(values)
func var_to_str_no_sort(value, indent = " ", cur_indent = ""):
var lines : Array[String] = []
if value is Array:
cur_indent += indent
lines.resize(value.size())
for i in lines.size():
if value[i] is Array or value[i] is Dictionary:
lines[i] = "%s%s" % [cur_indent, var_to_str_no_sort(value[i])]
else:
lines[i] = "%s%s" % [cur_indent, var_to_str(value[i])]
cur_indent = cur_indent.substr(0, cur_indent.length() - indent.length())
return "[\n" + ",\n".join(lines) + "\n]"
if value is Dictionary:
var keys : Array = value.keys()
var values : Array = value.values()
cur_indent += indent
lines.resize(keys.size())
for i in lines.size():
if values[i] is Array or values[i] is Dictionary:
lines[i] = "%s%s : %s" % [cur_indent, var_to_str(keys[i]), var_to_str_no_sort(values[i])]
else:
lines[i] = "%s%s : %s" % [cur_indent, var_to_str(keys[i]), var_to_str(values[i])]
cur_indent = cur_indent.substr(0, cur_indent.length() - indent.length())
return "{\n" + ",\n".join(lines) + "\n}"
return ",\n".join(lines)

Some files were not shown because too many files have changed in this diff Show More