209 lines
7.2 KiB
GDScript
209 lines
7.2 KiB
GDScript
@tool
|
|
extends RefCounted
|
|
## theme.* command handlers — create/modify Theme resources.
|
|
|
|
const Modules := preload("res://addons/godot_mcp_toolkit/core/modules.gd")
|
|
const FileGuard = Modules.FileGuard
|
|
const Helpers = Modules.CommandHelpers
|
|
|
|
const _VALID_PROPERTY_TYPES := ["color", "constant", "font", "font_size", "icon", "stylebox"]
|
|
|
|
|
|
static func register(registry: MCPToolkitCommandRegistry, _server: Node) -> void:
|
|
registry.add("theme.edit", func(parameters: Dictionary) -> Dictionary:
|
|
return await _cmd_theme_edit(parameters)
|
|
, MCPToolkitCommandOptions.new().mark_scene_independent())
|
|
|
|
|
|
# -- Commands -----------------------------------------------------------------
|
|
|
|
|
|
static func _cmd_theme_edit(parameters: Dictionary) -> Dictionary:
|
|
var err = MCPToolkitError.require(parameters, ["file_path", "edits"])
|
|
if err != null:
|
|
return err
|
|
|
|
var file_path := str(parameters.get("file_path", ""))
|
|
file_path = Helpers.normalize_editor_path(file_path)
|
|
var edits_raw = parameters.get("edits", null)
|
|
|
|
if typeof(edits_raw) != TYPE_ARRAY:
|
|
return MCPToolkitError.fail("INVALID_PARAMS", "edits must be an Array of edit descriptors")
|
|
|
|
var guard := FileGuard.resolve_safe(file_path)
|
|
if guard["error"] != null:
|
|
return MCPToolkitError.fail("PATH_DENIED", str(guard["reason"]))
|
|
if not file_path.get_extension().to_lower() in ["tres", "res"]:
|
|
return MCPToolkitError.fail("INVALID_PATH",
|
|
"theme.edit writes .tres/.res files (got %s)" % file_path)
|
|
|
|
# Load existing or create new Theme.
|
|
var theme: Theme
|
|
if FileAccess.file_exists(file_path):
|
|
var loaded = ResourceLoader.load(file_path, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
if loaded == null or not (loaded is Theme):
|
|
return MCPToolkitError.fail("INVALID_CLASS",
|
|
"Resource at %s is not a Theme" % file_path)
|
|
theme = loaded as Theme
|
|
else:
|
|
theme = Theme.new()
|
|
|
|
var edits: Array = edits_raw
|
|
var edits_applied := 0
|
|
|
|
for i in range(edits.size()):
|
|
var edit = edits[i]
|
|
if typeof(edit) != TYPE_DICTIONARY:
|
|
return MCPToolkitError.fail("INVALID_PARAMS",
|
|
"edits[%d] must be a dictionary" % i)
|
|
|
|
# Validate required keys per edit.
|
|
for key in ["type_name", "property_type", "property", "value"]:
|
|
if not edit.has(key):
|
|
return MCPToolkitError.fail("INVALID_PARAMS",
|
|
"edits[%d] missing required key '%s'" % [i, key])
|
|
|
|
var type_name := str(edit["type_name"])
|
|
var property_type := str(edit["property_type"])
|
|
var property_name := str(edit["property"])
|
|
var value = edit["value"]
|
|
|
|
if property_type not in _VALID_PROPERTY_TYPES:
|
|
return MCPToolkitError.fail("INVALID_PARAMS",
|
|
"edits[%d] invalid property_type '%s'; must be one of: %s" % [
|
|
i, property_type, ", ".join(_VALID_PROPERTY_TYPES)])
|
|
|
|
var apply_err := _apply_edit(theme, type_name, property_type, property_name, value, i)
|
|
if not apply_err.is_empty():
|
|
return MCPToolkitError.fail("INVALID_PARAMS", apply_err)
|
|
edits_applied += 1
|
|
|
|
# Save.
|
|
var dir_result := Helpers.ensure_parent_dir(file_path, "theme.edit")
|
|
if dir_result.has("error"):
|
|
return dir_result
|
|
var save_err := ResourceSaver.save(theme, file_path)
|
|
if save_err != OK:
|
|
return MCPToolkitError.fail("SAVE_FAILED",
|
|
"ResourceSaver.save returned %d (path=%s)" % [save_err, file_path])
|
|
ResourceLoader.load(file_path, "", ResourceLoader.CACHE_MODE_REPLACE)
|
|
await Helpers.ensure_file_indexed(file_path)
|
|
|
|
return MCPToolkitSuccess.ok({
|
|
"path": file_path,
|
|
"edits_applied": edits_applied,
|
|
})
|
|
|
|
|
|
# -- Edit helpers -------------------------------------------------------------
|
|
|
|
|
|
## Project a {r,g,b,a} dict onto a Color with opaque-BLACK channel defaults
|
|
## (paint semantics: r/g/b default 0.0, a defaults 1.0). Distinct from
|
|
## Coerce.color_from_dict (opaque-WHITE / tint) — do not conflate. Non-dict -> opaque black.
|
|
static func _color_from_dict_opaque(d: Variant) -> Color:
|
|
if typeof(d) != TYPE_DICTIONARY:
|
|
return Color(0, 0, 0, 1)
|
|
return Color(
|
|
float(d.get("r", 0.0)), float(d.get("g", 0.0)),
|
|
float(d.get("b", 0.0)), float(d.get("a", 1.0)))
|
|
|
|
|
|
static func _apply_edit(
|
|
theme: Theme, type_name: String, property_type: String,
|
|
property_name: String, value: Variant, index: int,
|
|
) -> String:
|
|
match property_type:
|
|
"color":
|
|
if typeof(value) != TYPE_DICTIONARY:
|
|
return "edits[%d] color value must be {r, g, b, a?}" % index
|
|
theme.set_color(property_name, type_name, _color_from_dict_opaque(value))
|
|
|
|
"constant":
|
|
theme.set_constant(property_name, type_name, int(value))
|
|
|
|
"font":
|
|
var font_path := str(value)
|
|
var font = load(font_path)
|
|
if font == null or not (font is Font):
|
|
return "edits[%d] font not found or not a Font: %s" % [index, font_path]
|
|
theme.set_font(property_name, type_name, font as Font)
|
|
|
|
"font_size":
|
|
theme.set_font_size(property_name, type_name, int(value))
|
|
|
|
"icon":
|
|
var icon_path := str(value)
|
|
var icon = load(icon_path)
|
|
if icon == null or not (icon is Texture2D):
|
|
return "edits[%d] icon not found or not a Texture2D: %s" % [index, icon_path]
|
|
theme.set_icon(property_name, type_name, icon as Texture2D)
|
|
|
|
"stylebox":
|
|
if typeof(value) != TYPE_DICTIONARY:
|
|
return "edits[%d] stylebox value must be a dictionary with 'type' key" % index
|
|
var sb_type := str(value.get("type", ""))
|
|
var sb = _create_stylebox(sb_type, value, index)
|
|
if sb is String:
|
|
return sb as String
|
|
theme.set_stylebox(property_name, type_name, sb as StyleBox)
|
|
|
|
return ""
|
|
|
|
|
|
static func _create_stylebox(sb_type: String, value: Dictionary, index: int) -> Variant:
|
|
match sb_type:
|
|
"StyleBoxFlat":
|
|
var sb := StyleBoxFlat.new()
|
|
if value.has("bg_color") and typeof(value["bg_color"]) == TYPE_DICTIONARY:
|
|
sb.bg_color = _color_from_dict_opaque(value["bg_color"])
|
|
if value.has("corner_radius"):
|
|
var r := int(value["corner_radius"])
|
|
sb.corner_radius_top_left = r
|
|
sb.corner_radius_top_right = r
|
|
sb.corner_radius_bottom_left = r
|
|
sb.corner_radius_bottom_right = r
|
|
if value.has("content_margin"):
|
|
var m := float(value["content_margin"])
|
|
sb.content_margin_left = m
|
|
sb.content_margin_top = m
|
|
sb.content_margin_right = m
|
|
sb.content_margin_bottom = m
|
|
if value.has("border_width"):
|
|
var w := int(value["border_width"])
|
|
sb.border_width_left = w
|
|
sb.border_width_top = w
|
|
sb.border_width_right = w
|
|
sb.border_width_bottom = w
|
|
if value.has("border_color") and typeof(value["border_color"]) == TYPE_DICTIONARY:
|
|
sb.border_color = _color_from_dict_opaque(value["border_color"])
|
|
return sb
|
|
|
|
"StyleBoxTexture":
|
|
var sb := StyleBoxTexture.new()
|
|
if value.has("texture"):
|
|
var tex = load(str(value["texture"]))
|
|
if tex == null or not (tex is Texture2D):
|
|
return "edits[%d] stylebox texture not found: %s" % [index, str(value["texture"])]
|
|
sb.texture = tex as Texture2D
|
|
if value.has("content_margin"):
|
|
var m := float(value["content_margin"])
|
|
sb.content_margin_left = m
|
|
sb.content_margin_top = m
|
|
sb.content_margin_right = m
|
|
sb.content_margin_bottom = m
|
|
return sb
|
|
|
|
"StyleBoxLine":
|
|
var sb := StyleBoxLine.new()
|
|
if value.has("color") and typeof(value["color"]) == TYPE_DICTIONARY:
|
|
sb.color = _color_from_dict_opaque(value["color"])
|
|
if value.has("thickness"):
|
|
sb.thickness = int(value["thickness"])
|
|
if value.has("vertical"):
|
|
sb.vertical = bool(value["vertical"])
|
|
return sb
|
|
|
|
_:
|
|
return "edits[%d] unknown stylebox type '%s'; must be StyleBoxFlat, StyleBoxTexture, or StyleBoxLine" % [index, sb_type]
|