294 lines
9.6 KiB
GDScript
294 lines
9.6 KiB
GDScript
@tool
|
|
extends Node
|
|
|
|
class_name SVGTextureHelper
|
|
|
|
# May use a class name if clutter is not a concern
|
|
# Map property names → default values
|
|
const PROPERTY_MAPPINGS: Dictionary = {
|
|
"expand_mode": TextureRect.EXPAND_IGNORE_SIZE,
|
|
"expand_icon": true,
|
|
"ignore_texture_size": true
|
|
}
|
|
|
|
@export var svg_resource := SVGResource.new(): set = _set_svg_resource
|
|
@export var target_property: String = "" : set = _set_target_property
|
|
|
|
var _available_texture_properties: Array[String] = []
|
|
var _is_saving_scene: bool = false
|
|
var _cached_texture: Texture2D # Store the actual texture to restore after save
|
|
|
|
func _ready() -> void:
|
|
# Ensure the parent is a Control node.
|
|
if not get_parent() is Control:
|
|
push_error("SVGTextureHelper must be a child of a Control node.")
|
|
queue_free()
|
|
return
|
|
|
|
# Detect available texture properties
|
|
_detect_texture_properties()
|
|
|
|
# Auto-select first available property if none is set
|
|
if target_property.is_empty() and not _available_texture_properties.is_empty():
|
|
target_property = _available_texture_properties[0]
|
|
|
|
# Connect to the parent's resize signal to trigger re-renders.
|
|
get_parent().resized.connect(_queue_render)
|
|
|
|
# Change parent texture properties
|
|
_update_parent_properties()
|
|
|
|
# Connect to save signals if in editor
|
|
if Engine.is_editor_hint():
|
|
_connect_save_signals()
|
|
|
|
# Perform initial render if we have a resource.
|
|
if svg_resource:
|
|
_queue_render()
|
|
|
|
func _connect_save_signals() -> void:
|
|
if Engine.is_editor_hint():
|
|
if get_tree():
|
|
get_tree().node_configuration_warning_changed.connect(_on_scene_tree_changed)
|
|
|
|
|
|
func _on_editor_visibility_changed() -> void:
|
|
# This is a heuristic that often correlates with save operations
|
|
if Engine.is_editor_hint():
|
|
_prepare_for_potential_save()
|
|
|
|
func _on_scene_tree_changed(node: Node) -> void:
|
|
# Another heuristic for detecting editor operations that might include saving
|
|
if Engine.is_editor_hint() and is_ancestor_of(node):
|
|
_prepare_for_potential_save()
|
|
|
|
# Override the notification method to catch save notifications
|
|
func _notification(what: int) -> void:
|
|
match what:
|
|
NOTIFICATION_EDITOR_PRE_SAVE:
|
|
_prepare_for_save()
|
|
NOTIFICATION_EDITOR_POST_SAVE:
|
|
_restore_after_save()
|
|
# Also catch when the node is about to be saved
|
|
NOTIFICATION_WM_CLOSE_REQUEST:
|
|
if Engine.is_editor_hint():
|
|
_prepare_for_save()
|
|
|
|
func _prepare_for_potential_save() -> void:
|
|
# Use a timer to briefly set texture to null, then restore
|
|
# This catches most save scenarios
|
|
if _is_saving_scene:
|
|
return
|
|
|
|
_prepare_for_save()
|
|
# Restore after a brief delay
|
|
get_tree().create_timer(0.1).timeout.connect(_restore_after_save, CONNECT_ONE_SHOT)
|
|
|
|
func _prepare_for_save() -> void:
|
|
if not Engine.is_editor_hint() or not get_parent() or target_property.is_empty():
|
|
return
|
|
|
|
if not _has_property(get_parent(), target_property):
|
|
return
|
|
|
|
_is_saving_scene = true
|
|
|
|
# Store the current texture
|
|
_cached_texture = get_parent().get(target_property) as Texture2D
|
|
|
|
# Set texture to null to prevent it from being saved
|
|
get_parent().set(target_property, null)
|
|
|
|
print_rich("[color=yellow]SVGTextureHelper: Texture temporarily set to null for saving[/color]")
|
|
|
|
func _restore_after_save() -> void:
|
|
if not Engine.is_editor_hint() or not _is_saving_scene:
|
|
return
|
|
|
|
_is_saving_scene = false
|
|
|
|
# Restore the cached texture
|
|
if get_parent() and not target_property.is_empty() and _cached_texture:
|
|
if _has_property(get_parent(), target_property):
|
|
get_parent().set(target_property, _cached_texture)
|
|
print_rich("[color=green]SVGTextureHelper: Texture restored after save[/color]")
|
|
|
|
# Clear the cache
|
|
_cached_texture = null
|
|
|
|
func _detect_texture_properties() -> void:
|
|
_available_texture_properties.clear()
|
|
|
|
if not get_parent():
|
|
return
|
|
|
|
# Get all properties from the parent control
|
|
var property_list = get_parent().get_property_list()
|
|
|
|
for prop_info in property_list:
|
|
var prop_name: String = prop_info.name
|
|
var prop_type = prop_info.type
|
|
var prop_class_name = prop_info.class_name
|
|
|
|
# Check if this property accepts a Texture2D
|
|
# This covers properties with type TYPE_OBJECT and class_name "Texture2D"
|
|
# or properties that are explicitly documented as texture properties
|
|
if (prop_type == TYPE_OBJECT and
|
|
(prop_class_name == "Texture2D" or
|
|
prop_class_name == "Texture" or
|
|
prop_class_name == "ImageTexture" or
|
|
prop_class_name == "CompressedTexture2D")):
|
|
_available_texture_properties.append(prop_name)
|
|
# Also check for commonly named texture properties
|
|
elif (prop_name.to_lower().contains("texture") or
|
|
prop_name.to_lower().contains("icon") or
|
|
prop_name in ["normal", "pressed", "hover", "disabled", "focused"]):
|
|
# Verify it can actually accept a texture by checking if it's an Object type
|
|
if prop_type == TYPE_OBJECT:
|
|
_available_texture_properties.append(prop_name)
|
|
|
|
# Sort alphabetically for better UX
|
|
_available_texture_properties.sort()
|
|
|
|
# Ensure we have at least some common fallbacks
|
|
if _available_texture_properties.is_empty():
|
|
# Add common texture property names as fallbacks
|
|
var common_properties = ["texture", "icon", "normal", "pressed"]
|
|
for prop in common_properties:
|
|
if _has_property(get_parent(), prop):
|
|
_available_texture_properties.append(prop)
|
|
|
|
|
|
func _set_target_property(new_property: String) -> void:
|
|
if target_property == new_property:
|
|
return
|
|
|
|
target_property = new_property
|
|
|
|
# Re-apply texture if we have one
|
|
if svg_resource and svg_resource.texture and get_parent():
|
|
_on_texture_updated(svg_resource.texture)
|
|
|
|
func _update_parent_properties() -> void:
|
|
if get_parent() == null:
|
|
return
|
|
|
|
for prop in PROPERTY_MAPPINGS.keys():
|
|
if _has_property(get_parent(), prop):
|
|
get_parent().set(prop, PROPERTY_MAPPINGS[prop])
|
|
|
|
func _has_property(target: Object, prop_name: StringName) -> bool:
|
|
for info in target.get_property_list():
|
|
if info.name == prop_name:
|
|
return true
|
|
return false
|
|
|
|
func _set_svg_resource(new_resource: SVGResource) -> void:
|
|
if svg_resource == new_resource:
|
|
return
|
|
|
|
# Disconnect from old resource if it exists
|
|
if svg_resource:
|
|
if svg_resource.is_connected("texture_updated", _on_texture_updated):
|
|
svg_resource.texture_updated.disconnect(_on_texture_updated)
|
|
# Disconnect from the changed signal
|
|
if svg_resource.is_connected("changed", _on_resource_changed):
|
|
svg_resource.changed.disconnect(_on_resource_changed)
|
|
|
|
svg_resource = new_resource
|
|
|
|
if svg_resource:
|
|
svg_resource.texture_updated.connect(_on_texture_updated)
|
|
# CONNECT TO THE CHANGED SIGNAL
|
|
svg_resource.changed.connect(_on_resource_changed)
|
|
|
|
# If the resource already has a texture, apply it immediately.
|
|
if svg_resource.texture:
|
|
_on_texture_updated(svg_resource.texture)
|
|
|
|
# Queue a render to ensure it's the correct size.
|
|
_queue_render()
|
|
|
|
# Handles resource changes
|
|
func _on_resource_changed() -> void:
|
|
# This gets called when svg_file_path or render_scale changes
|
|
_queue_render()
|
|
|
|
func _on_texture_updated(new_texture: Texture2D) -> void:
|
|
if get_parent() and not target_property.is_empty():
|
|
# Don't update texture if we're in the middle of a save operation
|
|
if _is_saving_scene:
|
|
_cached_texture = new_texture # Update our cache instead
|
|
return
|
|
|
|
# Validate that the property exists before setting it
|
|
if _has_property(get_parent(), target_property):
|
|
# Set the new texture on the parent control
|
|
get_parent().set_deferred(target_property, new_texture)
|
|
else:
|
|
push_warning("Property '%s' not found on parent node '%s'" % [target_property, get_parent().name])
|
|
|
|
## Called on resize or when the resource changes.
|
|
func _queue_render() -> void:
|
|
if svg_resource == null or svg_resource.svg_string.is_empty():
|
|
get_parent().set(target_property, null)
|
|
return
|
|
# Don't render during save operations
|
|
if _is_saving_scene:
|
|
return
|
|
|
|
# Check for valid conditions before requesting a render.
|
|
if not is_instance_valid(svg_resource) or not is_instance_valid(get_parent()):
|
|
return
|
|
|
|
if Engine.is_editor_hint() or get_tree():
|
|
var target_size: Vector2i = get_parent().size
|
|
var rescale_factor := 1.0
|
|
# If parent size is invalid or zero, use a minimal 1x1 placeholder.
|
|
# This prevents rendering a huge texture during initialization in the editor.
|
|
# The 'resized' signal will trigger a correct render once the size is calculated.
|
|
if target_size.x <= 0 or target_size.y <= 0:
|
|
target_size = Vector2i(1, 1)
|
|
|
|
var stretch_mode : TextureRect.StretchMode = get_parent().stretch_mode if "stretch_mode" in get_parent() else TextureRect.STRETCH_SCALE
|
|
if (
|
|
stretch_mode == TextureRect.StretchMode.STRETCH_KEEP or
|
|
stretch_mode == TextureRect.StretchMode.STRETCH_TILE or
|
|
stretch_mode == TextureRect.StretchMode.STRETCH_KEEP_CENTERED
|
|
):
|
|
rescale_factor = 1.0
|
|
else:
|
|
rescale_factor = (target_size.x / svg_resource.original_size.x) * svg_resource.render_scale
|
|
|
|
var image := Image.new()
|
|
var error := image.load_svg_from_string(Marshalls.base64_to_utf8(svg_resource.svg_string), rescale_factor)
|
|
|
|
if error != OK:
|
|
push_error("Failed to render SVG: " + str(error))
|
|
return
|
|
|
|
var image_texture := ImageTexture.create_from_image(image)
|
|
if (
|
|
stretch_mode == TextureRect.StretchMode.STRETCH_KEEP or
|
|
stretch_mode == TextureRect.StretchMode.STRETCH_TILE or
|
|
stretch_mode == TextureRect.StretchMode.STRETCH_KEEP_CENTERED
|
|
):
|
|
image_texture.set_size_override(svg_resource.original_size)
|
|
else:
|
|
get_parent().set(target_property, image_texture)
|
|
|
|
|
|
# Helper function to refresh the property list in the editor
|
|
func _refresh_properties() -> void:
|
|
if Engine.is_editor_hint():
|
|
_detect_texture_properties()
|
|
notify_property_list_changed()
|
|
|
|
# Manual save preparation - you can call this from editor plugins or scripts
|
|
func manual_prepare_for_save() -> void:
|
|
_prepare_for_save()
|
|
|
|
# Manual save restoration - you can call this from editor plugins or scripts
|
|
func manual_restore_after_save() -> void:
|
|
_restore_after_save()
|