diff --git a/AppInfo.gd b/AppInfo.gd
deleted file mode 100644
index 6ce44157..00000000
--- a/AppInfo.gd
+++ /dev/null
@@ -1,41 +0,0 @@
-# Stores basic information about GodSVG.
-class_name AppInfo extends RefCounted
-
-const project_founder_and_manager: Array[String] = ["MewPurPur"]
-
-
-# The developers and artists who have contributed significant patches to the MIT-licensed
-# source code of GodSVG are listed here. What counts as significant is decided arbitrarily.
-# The translators for each locale are listed in their corresponding PO files.
-# Entries are formatted as follows:
-# Option 1: Real Name (GitHub username)
-# Option 2: GitHub username
-
-const authors: Array[String] = [
- "Aaron Franke (aaronfranke)",
- "ajreckof",
- "aladvs",
- "Alex2782",
- "DevPoodle",
- "ilikefrogs101",
- "Kiisu-Master",
- "MewPurPur",
- "Qainguin",
- "Serem Titus (SeremTitus)",
- "Swarkin",
- "thiagola92",
- "Tom Blackwell (Volts-s)",
- "WeaverSong",
-]
-
-# GodSVG is a non-profit project developed by voluntary contributors.
-# Thousands of hours of work have gone into it so far.
-# Everyone who has generously sponsored this effort is listed here.
-
-const donors: Array[String] = []
-
-const golden_donors: Array[String] = []
-
-const diamond_donors: Array[String] = [
- "Aaron Franke (aaronfranke)",
-]
diff --git a/app_info.json b/app_info.json
new file mode 100644
index 00000000..a3bc3720
--- /dev/null
+++ b/app_info.json
@@ -0,0 +1,28 @@
+{
+ "project_founder_and_manager": [
+ "MewPurPur"
+ ],
+ "authors": [
+ "Aaron Franke (aaronfranke)",
+ "ajreckof",
+ "aladvs",
+ "Alex2782",
+ "DevPoodle",
+ "ilikefrogs101",
+ "Kiisu-Master",
+ "MewPurPur",
+ "Qainguin",
+ "Serem Titus (SeremTitus)",
+ "Swarkin",
+ "thiagola92",
+ "Tom Blackwell (Volts-s)",
+ "WeaverSong}"
+ ],
+ "donors": [
+ "Racer911dash1"
+ ],
+ "golden_donors": [],
+ "diamond_donors": [
+ "Aaron Franke (aaronfranke)"
+ ]
+}
diff --git a/export_presets.cfg b/export_presets.cfg
index 858a3f1b..b4795455 100644
--- a/export_presets.cfg
+++ b/export_presets.cfg
@@ -3,6 +3,7 @@
name="Windows Desktop"
platform="Windows Desktop"
runnable=true
+advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
@@ -13,6 +14,7 @@ encryption_include_filters=""
encryption_exclude_filters=""
encrypt_pck=false
encrypt_directory=false
+script_export_mode=2
[preset.0.options]
@@ -20,10 +22,8 @@ custom_template/debug=""
custom_template/release=""
debug/export_console_wrapper=1
binary_format/embed_pck=true
-texture_format/bptc=true
-texture_format/s3tc=true
-texture_format/etc=false
-texture_format/etc2=false
+texture_format/s3tc_bptc=true
+texture_format/etc2_astc=false
binary_format/architecture="x86_64"
codesign/enable=false
codesign/timestamp=true
@@ -32,7 +32,7 @@ codesign/digest_algorithm=1
codesign/description=""
codesign/custom_options=PackedStringArray()
application/modify_resources=true
-application/icon=""
+application/icon="res://visual/icon.ico"
application/console_wrapper_icon=""
application/icon_interpolation=4
application/file_version=""
@@ -43,6 +43,8 @@ application/file_description="GodSVG"
application/copyright="2023-present GodSVG contributors"
application/trademarks=""
application/export_angle=0
+application/export_d3d12=0
+application/d3d12_agility_sdk_multiarch=true
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
ssh_remote_deploy/port="22"
@@ -60,12 +62,17 @@ Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorActi
ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
Remove-Item -Recurse -Force '{temp_dir}'"
+texture_format/bptc=true
+texture_format/s3tc=true
+texture_format/etc=false
+texture_format/etc2=false
[preset.1]
name="Linux/X11"
-platform="Linux/X11"
+platform="Linux"
runnable=true
+advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
@@ -76,6 +83,7 @@ encryption_include_filters=""
encryption_exclude_filters=""
encrypt_pck=false
encrypt_directory=false
+script_export_mode=2
[preset.1.options]
@@ -83,10 +91,8 @@ custom_template/debug=""
custom_template/release=""
debug/export_console_wrapper=1
binary_format/embed_pck=true
-texture_format/bptc=true
-texture_format/s3tc=true
-texture_format/etc=false
-texture_format/etc2=false
+texture_format/s3tc_bptc=true
+texture_format/etc2_astc=false
binary_format/architecture="x86_64"
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
@@ -100,12 +106,17 @@ unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\")
rm -rf \"{temp_dir}\""
+texture_format/bptc=true
+texture_format/s3tc=true
+texture_format/etc=false
+texture_format/etc2=false
[preset.2]
name="macOS"
platform="macOS"
runnable=true
+advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
@@ -116,6 +127,7 @@ encryption_include_filters=""
encryption_exclude_filters=""
encrypt_pck=false
encrypt_directory=false
+script_export_mode=2
[preset.2.options]
@@ -136,6 +148,7 @@ application/copyright_localized={}
application/min_macos_version="10.12"
application/export_angle=0
display/high_res=true
+application/additional_plist_content=""
xcode/platform_build="14C18"
xcode/sdk_version="13.1"
xcode/sdk_build="22C55"
@@ -194,6 +207,148 @@ privacy/network_volumes_usage_description=""
privacy/network_volumes_usage_description_localized={}
privacy/removable_volumes_usage_description=""
privacy/removable_volumes_usage_description_localized={}
+privacy/tracking_enabled=false
+privacy/tracking_domains=PackedStringArray()
+privacy/collected_data/name/collected=false
+privacy/collected_data/name/linked_to_user=false
+privacy/collected_data/name/used_for_tracking=false
+privacy/collected_data/name/collection_purposes=0
+privacy/collected_data/email_address/collected=false
+privacy/collected_data/email_address/linked_to_user=false
+privacy/collected_data/email_address/used_for_tracking=false
+privacy/collected_data/email_address/collection_purposes=0
+privacy/collected_data/phone_number/collected=false
+privacy/collected_data/phone_number/linked_to_user=false
+privacy/collected_data/phone_number/used_for_tracking=false
+privacy/collected_data/phone_number/collection_purposes=0
+privacy/collected_data/physical_address/collected=false
+privacy/collected_data/physical_address/linked_to_user=false
+privacy/collected_data/physical_address/used_for_tracking=false
+privacy/collected_data/physical_address/collection_purposes=0
+privacy/collected_data/other_contact_info/collected=false
+privacy/collected_data/other_contact_info/linked_to_user=false
+privacy/collected_data/other_contact_info/used_for_tracking=false
+privacy/collected_data/other_contact_info/collection_purposes=0
+privacy/collected_data/health/collected=false
+privacy/collected_data/health/linked_to_user=false
+privacy/collected_data/health/used_for_tracking=false
+privacy/collected_data/health/collection_purposes=0
+privacy/collected_data/fitness/collected=false
+privacy/collected_data/fitness/linked_to_user=false
+privacy/collected_data/fitness/used_for_tracking=false
+privacy/collected_data/fitness/collection_purposes=0
+privacy/collected_data/payment_info/collected=false
+privacy/collected_data/payment_info/linked_to_user=false
+privacy/collected_data/payment_info/used_for_tracking=false
+privacy/collected_data/payment_info/collection_purposes=0
+privacy/collected_data/credit_info/collected=false
+privacy/collected_data/credit_info/linked_to_user=false
+privacy/collected_data/credit_info/used_for_tracking=false
+privacy/collected_data/credit_info/collection_purposes=0
+privacy/collected_data/other_financial_info/collected=false
+privacy/collected_data/other_financial_info/linked_to_user=false
+privacy/collected_data/other_financial_info/used_for_tracking=false
+privacy/collected_data/other_financial_info/collection_purposes=0
+privacy/collected_data/precise_location/collected=false
+privacy/collected_data/precise_location/linked_to_user=false
+privacy/collected_data/precise_location/used_for_tracking=false
+privacy/collected_data/precise_location/collection_purposes=0
+privacy/collected_data/coarse_location/collected=false
+privacy/collected_data/coarse_location/linked_to_user=false
+privacy/collected_data/coarse_location/used_for_tracking=false
+privacy/collected_data/coarse_location/collection_purposes=0
+privacy/collected_data/sensitive_info/collected=false
+privacy/collected_data/sensitive_info/linked_to_user=false
+privacy/collected_data/sensitive_info/used_for_tracking=false
+privacy/collected_data/sensitive_info/collection_purposes=0
+privacy/collected_data/contacts/collected=false
+privacy/collected_data/contacts/linked_to_user=false
+privacy/collected_data/contacts/used_for_tracking=false
+privacy/collected_data/contacts/collection_purposes=0
+privacy/collected_data/emails_or_text_messages/collected=false
+privacy/collected_data/emails_or_text_messages/linked_to_user=false
+privacy/collected_data/emails_or_text_messages/used_for_tracking=false
+privacy/collected_data/emails_or_text_messages/collection_purposes=0
+privacy/collected_data/photos_or_videos/collected=false
+privacy/collected_data/photos_or_videos/linked_to_user=false
+privacy/collected_data/photos_or_videos/used_for_tracking=false
+privacy/collected_data/photos_or_videos/collection_purposes=0
+privacy/collected_data/audio_data/collected=false
+privacy/collected_data/audio_data/linked_to_user=false
+privacy/collected_data/audio_data/used_for_tracking=false
+privacy/collected_data/audio_data/collection_purposes=0
+privacy/collected_data/gameplay_content/collected=false
+privacy/collected_data/gameplay_content/linked_to_user=false
+privacy/collected_data/gameplay_content/used_for_tracking=false
+privacy/collected_data/gameplay_content/collection_purposes=0
+privacy/collected_data/customer_support/collected=false
+privacy/collected_data/customer_support/linked_to_user=false
+privacy/collected_data/customer_support/used_for_tracking=false
+privacy/collected_data/customer_support/collection_purposes=0
+privacy/collected_data/other_user_content/collected=false
+privacy/collected_data/other_user_content/linked_to_user=false
+privacy/collected_data/other_user_content/used_for_tracking=false
+privacy/collected_data/other_user_content/collection_purposes=0
+privacy/collected_data/browsing_history/collected=false
+privacy/collected_data/browsing_history/linked_to_user=false
+privacy/collected_data/browsing_history/used_for_tracking=false
+privacy/collected_data/browsing_history/collection_purposes=0
+privacy/collected_data/search_hhistory/collected=false
+privacy/collected_data/search_hhistory/linked_to_user=false
+privacy/collected_data/search_hhistory/used_for_tracking=false
+privacy/collected_data/search_hhistory/collection_purposes=0
+privacy/collected_data/user_id/collected=false
+privacy/collected_data/user_id/linked_to_user=false
+privacy/collected_data/user_id/used_for_tracking=false
+privacy/collected_data/user_id/collection_purposes=0
+privacy/collected_data/device_id/collected=false
+privacy/collected_data/device_id/linked_to_user=false
+privacy/collected_data/device_id/used_for_tracking=false
+privacy/collected_data/device_id/collection_purposes=0
+privacy/collected_data/purchase_history/collected=false
+privacy/collected_data/purchase_history/linked_to_user=false
+privacy/collected_data/purchase_history/used_for_tracking=false
+privacy/collected_data/purchase_history/collection_purposes=0
+privacy/collected_data/product_interaction/collected=false
+privacy/collected_data/product_interaction/linked_to_user=false
+privacy/collected_data/product_interaction/used_for_tracking=false
+privacy/collected_data/product_interaction/collection_purposes=0
+privacy/collected_data/advertising_data/collected=false
+privacy/collected_data/advertising_data/linked_to_user=false
+privacy/collected_data/advertising_data/used_for_tracking=false
+privacy/collected_data/advertising_data/collection_purposes=0
+privacy/collected_data/other_usage_data/collected=false
+privacy/collected_data/other_usage_data/linked_to_user=false
+privacy/collected_data/other_usage_data/used_for_tracking=false
+privacy/collected_data/other_usage_data/collection_purposes=0
+privacy/collected_data/crash_data/collected=false
+privacy/collected_data/crash_data/linked_to_user=false
+privacy/collected_data/crash_data/used_for_tracking=false
+privacy/collected_data/crash_data/collection_purposes=0
+privacy/collected_data/performance_data/collected=false
+privacy/collected_data/performance_data/linked_to_user=false
+privacy/collected_data/performance_data/used_for_tracking=false
+privacy/collected_data/performance_data/collection_purposes=0
+privacy/collected_data/other_diagnostic_data/collected=false
+privacy/collected_data/other_diagnostic_data/linked_to_user=false
+privacy/collected_data/other_diagnostic_data/used_for_tracking=false
+privacy/collected_data/other_diagnostic_data/collection_purposes=0
+privacy/collected_data/environment_scanning/collected=false
+privacy/collected_data/environment_scanning/linked_to_user=false
+privacy/collected_data/environment_scanning/used_for_tracking=false
+privacy/collected_data/environment_scanning/collection_purposes=0
+privacy/collected_data/hands/collected=false
+privacy/collected_data/hands/linked_to_user=false
+privacy/collected_data/hands/used_for_tracking=false
+privacy/collected_data/hands/collection_purposes=0
+privacy/collected_data/head/collected=false
+privacy/collected_data/head/linked_to_user=false
+privacy/collected_data/head/used_for_tracking=false
+privacy/collected_data/head/collection_purposes=0
+privacy/collected_data/other_data_types/collected=false
+privacy/collected_data/other_data_types/linked_to_user=false
+privacy/collected_data/other_data_types/used_for_tracking=false
+privacy/collected_data/other_data_types/collection_purposes=0
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
ssh_remote_deploy/port="22"
@@ -211,6 +366,7 @@ rm -rf \"{temp_dir}\""
name="Web"
platform="Web"
runnable=true
+advanced_options=false
dedicated_server=false
custom_features=""
export_filter="exclude"
@@ -222,12 +378,14 @@ encryption_include_filters=""
encryption_exclude_filters=""
encrypt_pck=false
encrypt_directory=false
+script_export_mode=2
[preset.3.options]
custom_template/debug=""
custom_template/release="/home/volter/Desktop/godot/bin/godot.web.template_release.wasm32.zip"
variant/extensions_support=false
+variant/thread_support=true
vram_texture_compression/for_desktop=true
vram_texture_compression/for_mobile=false
html/export_icon=true
@@ -237,6 +395,7 @@ html/canvas_resize_policy=2
html/focus_canvas_on_start=true
html/experimental_virtual_keyboard=false
progressive_web_app/enabled=false
+progressive_web_app/ensure_cross_origin_isolation_headers=true
progressive_web_app/offline_page=""
progressive_web_app/display=1
progressive_web_app/orientation=0
@@ -250,6 +409,7 @@ progressive_web_app/background_color=Color(0, 0, 0, 1)
name="Android"
platform="Android"
runnable=true
+advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
@@ -260,12 +420,16 @@ encryption_include_filters=""
encryption_exclude_filters=""
encrypt_pck=false
encrypt_directory=false
+script_export_mode=2
[preset.4.options]
custom_template/debug=""
custom_template/release=""
gradle_build/use_gradle_build=false
+gradle_build/gradle_build_directory=""
+gradle_build/android_source_template=""
+gradle_build/compress_native_libraries=false
gradle_build/export_format=0
gradle_build/min_sdk=""
gradle_build/target_sdk=""
diff --git a/godot_only/scripts/tests.gd b/godot_only/scripts/tests.gd
index d019c71f..7c96348c 100644
--- a/godot_only/scripts/tests.gd
+++ b/godot_only/scripts/tests.gd
@@ -32,7 +32,7 @@ func pathdata_tests(print_success := false) -> bool:
var tests_passed := true
for test in tests.keys():
- var result := PathDataParser.path_data_to_arrays(test)
+ var result := PathdataParser.pathdata_to_arrays(test)
var expected: Array = tests[test]
if result != expected:
tests_passed = false
diff --git a/src/DB.gd b/src/DB.gd
index f9a9344c..f4e04e38 100644
--- a/src/DB.gd
+++ b/src/DB.gd
@@ -1,50 +1,84 @@
class_name DB extends RefCounted
-const known_tags = ["svg", "circle", "ellipse", "rect", "path", "line", "stop"]
+enum AttributeType {NUMERIC, COLOR, LIST, PATHDATA, ENUM, TRANSFORM_LIST, ID, UNKNOWN}
-const known_tag_attributes = { # Dictionary{String: Array[String]}
- "svg": TagSVG.known_attributes,
- "circle": TagCircle.known_attributes,
- "ellipse": TagEllipse.known_attributes,
- "rect": TagRect.known_attributes,
- "path": TagPath.known_attributes,
- "line": TagLine.known_attributes,
- "stop": TagStop.known_attributes,
+const recognized_tags = ["svg", "g", "circle", "ellipse", "rect", "path", "line", "stop",
+ "linearGradient", "radialGradient"]
+
+const tag_icons = {
+ "circle": preload("res://visual/icons/tag/circle.svg"),
+ "ellipse": preload("res://visual/icons/tag/ellipse.svg"),
+ "rect": preload("res://visual/icons/tag/rect.svg"),
+ "path": preload("res://visual/icons/tag/path.svg"),
+ "line": preload("res://visual/icons/tag/line.svg"),
+ "g": preload("res://visual/icons/tag/g.svg"),
+ "linearGradient": preload("res://visual/icons/tag/linearGradient.svg"),
+ "radialGradient": preload("res://visual/icons/tag/radialGradient.svg"),
+ "stop": preload("res://visual/icons/tag/stop.svg"),
}
+const unrecognized_tag_icon = preload("res://visual/icons/tag/unrecognized.svg")
-const attribute_defaults = {
- "viewBox": "",
- "width": "0",
- "height": "0",
- "x": "0",
- "y": "0",
- "x1": "0",
- "y1": "0",
- "x2": "0",
- "y2": "0",
- "cx": "0",
- "cy": "0",
- "r": "0",
- "rx": "0",
- "ry": "0",
- "opacity": "1",
- "fill": "black",
- "fill-opacity": "1",
- "stroke": "none",
- "stroke-opacity": "1",
- "stroke-width": "1",
- "stroke-linecap": "butt",
- "stroke-linejoin": "miter",
- "d": "",
- "transform": "",
- "offset": "0",
- "stop-color": "black",
- "stop-opacity": "1",
+const recognized_attributes = { # Dictionary{String: Array[String]}
+ # TODO this is just propagated_attributes, but it ruins the const because of Godot bug.
+ "svg": ["xmlns", "width", "height", "viewBox", "fill", "fill-opacity", "stroke",
+ "stroke-opacity", "stroke-width", "stroke-linecap", "stroke-linejoin"],
+ "g": ["transform", "opacity", "fill", "fill-opacity", "stroke", "stroke-opacity",
+ "stroke-width", "stroke-linecap", "stroke-linejoin"],
+ "linearGradient": ["id", "gradientTransform", "gradientUnits", "x1", "y1", "x2", "y2"],
+ "radialGradient": ["id", "gradientTransform", "gradientUnits", "cx", "cy", "r"],
+ "circle": ["transform", "opacity", "fill", "fill-opacity", "stroke", "stroke-opacity",
+ "stroke-width", "cx", "cy", "r"],
+ "ellipse": ["transform", "opacity", "fill", "fill-opacity", "stroke", "stroke-opacity",
+ "stroke-width", "cx", "cy", "rx", "ry"],
+ "rect": ["transform", "opacity", "fill", "fill-opacity", "stroke", "stroke-opacity",
+ "stroke-width", "stroke-linejoin", "x", "y", "width", "height", "rx", "ry"],
+ "path": ["transform", "opacity", "fill", "fill-opacity", "stroke", "stroke-opacity",
+ "stroke-width", "stroke-linecap", "stroke-linejoin", "d"],
+ "line": ["transform", "opacity", "stroke", "stroke-opacity", "stroke-width",
+ "stroke-linecap", "x1", "y1", "x2", "y2"],
+ "stop": ["offset", "stop-color", "stop-opacity"],
+}
+
+const propagated_attributes = ["fill", "fill-opacity", "stroke", "stroke-opacity",
+ "stroke-width", "stroke-linecap", "stroke-linejoin"]
+
+const attribute_types = {
+ "viewBox": AttributeType.LIST,
+ "width": AttributeType.NUMERIC,
+ "height": AttributeType.NUMERIC,
+ "x": AttributeType.NUMERIC,
+ "y": AttributeType.NUMERIC,
+ "x1": AttributeType.NUMERIC,
+ "y1": AttributeType.NUMERIC,
+ "x2": AttributeType.NUMERIC,
+ "y2": AttributeType.NUMERIC,
+ "cx": AttributeType.NUMERIC,
+ "cy": AttributeType.NUMERIC,
+ "r": AttributeType.NUMERIC,
+ "rx": AttributeType.NUMERIC,
+ "ry": AttributeType.NUMERIC,
+ "opacity": AttributeType.NUMERIC,
+ "fill": AttributeType.COLOR,
+ "fill-opacity": AttributeType.NUMERIC,
+ "stroke": AttributeType.COLOR,
+ "stroke-opacity": AttributeType.NUMERIC,
+ "stroke-width": AttributeType.NUMERIC,
+ "stroke-linecap": AttributeType.ENUM,
+ "stroke-linejoin": AttributeType.ENUM,
+ "d": AttributeType.PATHDATA,
+ "transform": AttributeType.TRANSFORM_LIST,
+ "offset": AttributeType.NUMERIC,
+ "stop-color": AttributeType.COLOR,
+ "stop-opacity": AttributeType.NUMERIC,
+ "id": AttributeType.ID,
+ "gradientTransform": AttributeType.TRANSFORM_LIST,
+ "gradientUnits": AttributeType.ENUM,
}
const attribute_enum_values = {
"stroke-linecap": ["butt", "round", "square"],
"stroke-linejoin": ["miter", "round", "bevel"],
+ "gradientUnits": ["userSpaceOnUse", "objectBoundingBox"]
}
const attribute_numeric_bounds = {
@@ -70,51 +104,43 @@ const attribute_numeric_bounds = {
}
-static func is_tag_known(tag_name: String) -> bool:
- return tag_name in known_tags
-
-static func is_attribute_known(tag_name: String, attribute_name: String) -> bool:
- if not known_tag_attributes.has(tag_name):
- return false
- return attribute_name in known_tag_attributes[tag_name]
+static func is_attribute_recognized(tag_name: String, attribute_name: String) -> bool:
+ return recognized_attributes.has(tag_name) and\
+ attribute_name in recognized_attributes[tag_name]
static func get_tag_icon(tag_name: String) -> Texture2D:
- match tag_name:
- "circle": return TagCircle.icon
- "ellipse": return TagEllipse.icon
- "rect": return TagRect.icon
- "path": return TagPath.icon
- "line": return TagLine.icon
- "stop": return TagStop.icon
- _: return TagUnknown.icon
+ return tag_icons[tag_name] if tag_icons.has(tag_name) else unrecognized_tag_icon
+static func get_attribute_type(attribute_name: String) -> AttributeType:
+ return attribute_types[attribute_name] if attribute_types.has(attribute_name)\
+ else AttributeType.UNKNOWN
+
+# Creates an attribute with a certain value.
static func attribute(name: String, initial_value := "") -> Attribute:
- match name:
- "viewBox": return AttributeList.new(name, initial_value)
- "width": return AttributeNumeric.new(name, initial_value)
- "height": return AttributeNumeric.new(name, initial_value)
- "x": return AttributeNumeric.new(name, initial_value)
- "y": return AttributeNumeric.new(name, initial_value)
- "x1": return AttributeNumeric.new(name, initial_value)
- "y1": return AttributeNumeric.new(name, initial_value)
- "x2": return AttributeNumeric.new(name, initial_value)
- "y2": return AttributeNumeric.new(name, initial_value)
- "cx": return AttributeNumeric.new(name, initial_value)
- "cy": return AttributeNumeric.new(name, initial_value)
- "r": return AttributeNumeric.new(name, initial_value)
- "rx": return AttributeNumeric.new(name, initial_value)
- "ry": return AttributeNumeric.new(name, initial_value)
- "opacity": return AttributeNumeric.new(name, initial_value)
- "fill": return AttributeColor.new(name, initial_value)
- "fill-opacity": return AttributeNumeric.new(name, initial_value)
- "stroke": return AttributeColor.new(name, initial_value)
- "stroke-opacity": return AttributeNumeric.new(name, initial_value)
- "stroke-width": return AttributeNumeric.new(name, initial_value)
- "stroke-linecap": return AttributeEnum.new(name, initial_value)
- "stroke-linejoin": return AttributeEnum.new(name, initial_value)
- "d": return AttributePath.new(name, initial_value)
- "transform": return AttributeTransform.new(name, initial_value)
- "offset": return AttributeNumeric.new(name, initial_value)
- "stop-color": return AttributeColor.new(name, initial_value)
- "stop-opacity": return AttributeNumeric.new(name, initial_value)
+ match get_attribute_type(name):
+ AttributeType.NUMERIC: return AttributeNumeric.new(name, initial_value)
+ AttributeType.COLOR: return AttributeColor.new(name, initial_value)
+ AttributeType.LIST: return AttributeList.new(name, initial_value)
+ AttributeType.PATHDATA: return AttributePathdata.new(name, initial_value)
+ AttributeType.ENUM: return AttributeEnum.new(name, initial_value)
+ AttributeType.TRANSFORM_LIST: return AttributeTransformList.new(name, initial_value)
+ AttributeType.ID: return AttributeID.new(name, initial_value)
_: return Attribute.new(name, initial_value)
+
+static func tag(name: String, user_setup_value = null) -> Tag:
+ var new_tag: Tag
+ match name:
+ "svg": new_tag = TagSVG.new()
+ "g": new_tag = TagG.new()
+ "circle": new_tag = TagCircle.new()
+ "ellipse": new_tag = TagEllipse.new()
+ "rect": new_tag = TagRect.new()
+ "path": new_tag = TagPath.new()
+ "line": new_tag = TagLine.new()
+ "linearGradient": new_tag = TagLinearGradient.new()
+ "radialGradient": new_tag = TagRadialGradient.new()
+ "stop": new_tag = TagStop.new()
+ _: new_tag = TagUnrecognized.new(name)
+ if user_setup_value != null:
+ new_tag.user_setup(user_setup_value)
+ return new_tag
diff --git a/src/FileUtils.gd b/src/FileUtils.gd
index 15b9a20d..58351a15 100644
--- a/src/FileUtils.gd
+++ b/src/FileUtils.gd
@@ -3,7 +3,7 @@ class_name FileUtils extends RefCounted
const GoodFileDialogType = preload("res://src/ui_parts/good_file_dialog.gd")
-const AlertDialog := preload("res://src/ui_parts/alert_dialog.tscn")
+const AlertDialog = preload("res://src/ui_parts/alert_dialog.tscn")
const ImportWarningDialog = preload("res://src/ui_parts/import_warning_dialog.tscn")
const GoodFileDialog = preload("res://src/ui_parts/good_file_dialog.tscn")
const ExportDialog = preload("res://src/ui_parts/export_dialog.tscn")
@@ -42,16 +42,17 @@ quality := 0.8, lossless := true) -> void:
static func generate_image_from_tags(upscale_amount := 1.0) -> Image:
var export_svg := SVG.root_tag.duplicate()
- if export_svg.attributes.viewBox.get_list().is_empty():
- export_svg.attributes.viewBox.set_list([0, 0, export_svg.width, export_svg.height])
+ if export_svg.get_attribute_list("viewBox").is_empty():
+ export_svg.set_attribute("viewBox",
+ PackedFloat32Array([0, 0, export_svg.width, export_svg.height]))
# First ensure there are dimensions.
# Otherwise changing one side could influence the other.
- export_svg.attributes.width.set_num(export_svg.width)
- export_svg.attributes.height.set_num(export_svg.height)
- export_svg.attributes.width.set_num(export_svg.width * upscale_amount)
- export_svg.attributes.height.set_num(export_svg.height * upscale_amount)
+ export_svg.set_attribute("width", export_svg.width)
+ export_svg.set_attribute("height", export_svg.height)
+ export_svg.set_attribute("width", export_svg.width * upscale_amount)
+ export_svg.set_attribute("height", export_svg.height * upscale_amount)
var img := Image.new()
- img.load_svg_from_string(SVGParser.svg_to_text(export_svg))
+ img.load_svg_from_string(SVGParser.root_to_text(export_svg))
img.fix_alpha_edges() # See godot issue 82579.
return img
@@ -111,12 +112,12 @@ static func open_import_dialog() -> void:
HandlerGUI.add_overlay(svg_import_dialog)
svg_import_dialog.file_selected.connect(apply_svg_from_path)
-static func open_reference_load_dialog() -> void:
+static func open_reference_load_dialog(callable: Callable) -> void:
if FileUtils._is_native_preferred():
DisplayServer.file_dialog_show(TranslationServer.translate("Load an image file"),
Utils.get_last_dir(), "", false, DisplayServer.FILE_DIALOG_MODE_OPEN_FILE,
PackedStringArray(["*.png,*.jpeg,*.jpg,*.webp,*.svg"]),
- native_reference_image_load)
+ native_reference_image_load.bind(callable))
# TODO: Add Web Support
#elif OS.has_feature("web"):
#HandlerGUI.web_load_reference_image()
@@ -126,7 +127,7 @@ static func open_reference_load_dialog() -> void:
GoodFileDialogType.FileMode.SELECT,
PackedStringArray(["png", "jpeg", "jpg", "webp", "svg"]))
HandlerGUI.add_overlay(image_import_dialog)
- image_import_dialog.file_selected.connect(load_reference_image)
+ image_import_dialog.file_selected.connect(load_reference_image.bind(callable))
static func native_svg_import(has_selected: bool, files: PackedStringArray,
_filter_idx: int) -> void:
@@ -134,15 +135,15 @@ _filter_idx: int) -> void:
apply_svg_from_path(files[0])
static func native_reference_image_load(has_selected: bool, files: PackedStringArray,
-_filter_idx: int) -> void:
+_filter_idx: int, callable: Callable) -> void:
if has_selected:
- load_reference_image(files[0])
+ load_reference_image(files[0], callable)
-static func load_reference_image(path: String) -> void:
+static func load_reference_image(path: String, callable: Callable) -> void:
var img = Image.new()
img.load(path)
img.save_png("user://reference_image.png")
- Indications.imported_reference.emit()
+ callable.call()
static func apply_svg_from_path(path: String) -> int:
var svg_file := FileAccess.open(path, FileAccess.READ)
diff --git a/src/GlobalSettings.gd b/src/GlobalSettings.gd
index d2537387..48f8b0e1 100644
--- a/src/GlobalSettings.gd
+++ b/src/GlobalSettings.gd
@@ -11,11 +11,12 @@ const config_path = "user://config.tres"
# Don't have the language setting here, so it's not reset.
const default_config = {
- "autoformat": {
+ "formatting": {
"general_number_precision": 3,
"general_angle_precision": 1,
"xml_add_trailing_newline": false,
"xml_shorthand_tags": true,
+ "xml_pretty_formatting": false,
"number_enable_autoformatting": false,
"number_remove_zero_padding": false,
"number_remove_leading_zero": true,
@@ -25,13 +26,15 @@ const default_config = {
"color_convert_named_to_hex": true,
"color_use_shorthand_hex_code": true,
"color_use_short_named_colors": false,
- "path_compress_numbers": true,
- "path_minimize_spacing": true,
- "path_remove_spacing_after_flags": false,
- "path_remove_consecutive_commands": true,
- "transform_compress_numbers": true,
- "transform_minimize_spacing": true,
- "transform_remove_unnecessary_params": true,
+ "pathdata_enable_autoformatting": true,
+ "pathdata_compress_numbers": true,
+ "pathdata_minimize_spacing": true,
+ "pathdata_remove_spacing_after_flags": false,
+ "pathdata_remove_consecutive_commands": true,
+ "transform_list_enable_autoformatting": true,
+ "transform_list_compress_numbers": true,
+ "transform_list_minimize_spacing": true,
+ "transform_list_remove_unnecessary_params": true,
},
"theming": {
"highlighting_symbol_color": Color("abc9ff"),
@@ -66,6 +69,7 @@ const default_config = {
# No way to fetch defaults otherwise.
var default_input_events := {} # Dictionary{String: Array[InputEvent]}
+# Stores whether the keybinds should be modifiable.
const keybinds_dict = {
"file": {
"import": true,
@@ -146,6 +150,7 @@ var general_number_precision := 3
var general_angle_precision := 1
var xml_add_trailing_newline := false
var xml_shorthand_tags := true
+var xml_pretty_formatting := false
var number_enable_autoformatting := false
var number_remove_zero_padding := true
var number_remove_leading_zero := false
@@ -154,13 +159,15 @@ var color_convert_rgb_to_hex := false
var color_convert_named_to_hex := true
var color_use_shorthand_hex_code := true
var color_use_short_named_colors := false
-var path_compress_numbers := true
-var path_minimize_spacing := true
-var path_remove_spacing_after_flags := false
-var path_remove_consecutive_commands := true
-var transform_compress_numbers := true
-var transform_minimize_spacing := true
-var transform_remove_unnecessary_params := true
+var pathdata_enable_autoformatting := true
+var pathdata_compress_numbers := true
+var pathdata_minimize_spacing := true
+var pathdata_remove_spacing_after_flags := false
+var pathdata_remove_consecutive_commands := true
+var transform_list_enable_autoformatting := true
+var transform_list_compress_numbers := true
+var transform_list_minimize_spacing := true
+var transform_list_remove_unnecessary_params := true
# Theming
var highlighting_symbol_color := Color("abc9ff")
@@ -291,6 +298,5 @@ func reset_palettes() -> void:
# Just a helper.
func get_validity_color(error_condition: bool, warning_condition := false) -> Color:
- return GlobalSettings.basic_color_error if error_condition else\
- GlobalSettings.basic_color_warning if warning_condition else\
- GlobalSettings.basic_color_valid
+ return basic_color_error if error_condition else\
+ basic_color_warning if warning_condition else basic_color_valid
diff --git a/src/HandlerGUI.gd b/src/HandlerGUI.gd
index 7e377839..b6a93592 100644
--- a/src/HandlerGUI.gd
+++ b/src/HandlerGUI.gd
@@ -15,6 +15,7 @@ var popup_overlay_stack: Array[Control]
func _enter_tree() -> void:
+ process_mode = PROCESS_MODE_ALWAYS
get_tree().root.auto_translate_mode = Node.AUTO_TRANSLATE_MODE_DISABLED
get_window().files_dropped.connect(_on_files_dropped)
get_window().dpi_changed.connect(update_ui_scale)
diff --git a/src/Indications.gd b/src/Indications.gd
index 25ca7f91..99faf813 100644
--- a/src/Indications.gd
+++ b/src/Indications.gd
@@ -20,32 +20,30 @@ signal hover_changed
signal selection_changed
signal proposed_drop_changed
-signal requested_scroll_to_tag_editor(tid: PackedInt32Array)
+signal requested_scroll_to_tag_editor(xid: PackedInt32Array)
# The viewport listens for this signal to put you in handle-placing mode.
signal handle_added
-signal imported_reference
-
-# The PackedInt32Array holds the hierarchical orders. TID means Tag ID.
+# The PackedInt32Array holds the hierarchical orders. XID means XMLNode ID.
# For example, the 5th child of the 2nd child of the root tag would be (1, 4).
# PackedInt32Array() means it's invalid.
-var hovered_tid := PackedInt32Array()
-var selected_tids: Array[PackedInt32Array] = []
-var selection_pivot_tid := PackedInt32Array()
+var hovered_xid := PackedInt32Array()
+var selected_xids: Array[PackedInt32Array] = []
+var selection_pivot_xid := PackedInt32Array()
# Semi-hovered means the tag has inner selections, but it is not selected itself.
# For example, individual path commands.
# Note that you can't have a selected tag and an inner selection simultaneously!
-var semi_hovered_tid := PackedInt32Array()
-var semi_selected_tid := PackedInt32Array()
+var semi_hovered_xid := PackedInt32Array()
+var semi_selected_xid := PackedInt32Array()
# Inner stuff aren't in a tree, so they use an int. -1 means invalid.
var inner_hovered := -1
var inner_selections: Array[int] = []
var inner_selection_pivot := -1
# When dragging tags in the inspector.
-var proposed_drop_tid := PackedInt32Array()
+var proposed_drop_xid := PackedInt32Array()
signal zoom_changed
@@ -66,37 +64,37 @@ func set_viewport_size(new_value: Vector2i) -> void:
func _ready() -> void:
- SVG.root_tag.tags_added.connect(_on_tags_added)
- SVG.root_tag.tags_deleted.connect(_on_tags_deleted)
- SVG.root_tag.tags_moved_in_parent.connect(_on_tags_moved_in_parent)
- SVG.root_tag.tags_moved_to.connect(_on_tags_moved_to)
- SVG.root_tag.changed_unknown.connect(clear_all_selections)
+ SVG.tags_added.connect(_on_tags_added)
+ SVG.tags_deleted.connect(_on_tags_deleted)
+ SVG.tags_moved_in_parent.connect(_on_tags_moved_in_parent)
+ SVG.tags_moved_to.connect(_on_tags_moved_to)
+ SVG.changed_unknown.connect(clear_all_selections)
# Override the selected tags with a single new selected tag.
# If inner_idx is given, this will be an inner selection.
-func normal_select(tid: PackedInt32Array, inner_idx := -1) -> void:
- if tid.is_empty():
+func normal_select(xid: PackedInt32Array, inner_idx := -1) -> void:
+ if xid.is_empty():
return
if inner_idx == -1:
- var old_selected_tids := selected_tids.duplicate()
- if not semi_selected_tid.is_empty():
- semi_selected_tid.clear()
+ var old_selected_xids := selected_xids.duplicate()
+ if not semi_selected_xid.is_empty():
+ semi_selected_xid.clear()
inner_selections.clear()
- if selected_tids.size() == 1 and selected_tids[0] == tid:
+ if selected_xids.size() == 1 and selected_xids[0] == xid:
return
- selection_pivot_tid = tid.duplicate()
- selected_tids = [tid.duplicate()]
- if old_selected_tids != selected_tids:
+ selection_pivot_xid = xid.duplicate()
+ selected_xids = [xid.duplicate()]
+ if old_selected_xids != selected_xids:
selection_changed.emit()
else:
- selected_tids.clear()
+ selected_xids.clear()
var old_inner_selections := inner_selections.duplicate()
- if semi_selected_tid == tid and\
+ if semi_selected_xid == xid and\
inner_selections.size() == 1 and inner_selections[0] == inner_idx:
return
- semi_selected_tid = tid.duplicate()
+ semi_selected_xid = xid.duplicate()
inner_selection_pivot = inner_idx
inner_selections = [inner_idx]
if inner_selections != old_inner_selections:
@@ -104,25 +102,25 @@ func normal_select(tid: PackedInt32Array, inner_idx := -1) -> void:
# If the tag was selected, unselect it. If it was unselected, select it.
# If inner_idx is given, this will be an inner selection.
-func ctrl_select(tid: PackedInt32Array, inner_idx := -1) -> void:
- if tid.is_empty():
+func ctrl_select(xid: PackedInt32Array, inner_idx := -1) -> void:
+ if xid.is_empty():
return
if inner_idx == -1:
inner_selections.clear()
- var tid_idx := selected_tids.find(tid)
- if tid_idx == -1:
- selection_pivot_tid = tid.duplicate()
- selected_tids.append(tid.duplicate())
+ var xid_idx := selected_xids.find(xid)
+ if xid_idx == -1:
+ selection_pivot_xid = xid.duplicate()
+ selected_xids.append(xid.duplicate())
else:
- selected_tids.remove_at(tid_idx)
- if selected_tids.is_empty():
- selection_pivot_tid = PackedInt32Array()
+ selected_xids.remove_at(xid_idx)
+ if selected_xids.is_empty():
+ selection_pivot_xid = PackedInt32Array()
else:
- if semi_selected_tid != tid:
- normal_select(tid, inner_idx)
+ if semi_selected_xid != xid:
+ normal_select(xid, inner_idx)
else:
- selected_tids.clear()
+ selected_xids.clear()
var idx_idx := inner_selections.find(inner_idx)
if idx_idx == -1:
inner_selection_pivot = inner_idx
@@ -136,47 +134,47 @@ func ctrl_select(tid: PackedInt32Array, inner_idx := -1) -> void:
# Select all tags with the same depth from the tag to the last selected tag.
# Similarly for inner selections if inner_idx is given, but without tree logic.
-func shift_select(tid: PackedInt32Array, inner_idx := -1) -> void:
- if tid.is_empty():
+func shift_select(xid: PackedInt32Array, inner_idx := -1) -> void:
+ if xid.is_empty():
return
if inner_idx == -1:
- if selection_pivot_tid.is_empty():
- if selected_tids.is_empty():
- normal_select(tid, inner_idx)
+ if selection_pivot_xid.is_empty():
+ if selected_xids.is_empty():
+ normal_select(xid, inner_idx)
return
- if tid == selection_pivot_tid:
+ if xid == selection_pivot_xid:
return
- var old_selected_tids := selected_tids.duplicate()
+ var old_selected_xids := selected_xids.duplicate()
- if tid.size() != selection_pivot_tid.size():
- if not tid in selected_tids:
- selected_tids.append(tid)
+ if xid.size() != selection_pivot_xid.size():
+ if not xid in selected_xids:
+ selected_xids.append(xid)
selection_changed.emit()
return
- var parent_tag := tid.duplicate()
+ var parent_tag := xid.duplicate()
parent_tag.resize(parent_tag.size() - 1)
- var tid_idx := tid[-1]
- var selection_pivot_tid_idx := selection_pivot_tid[-1]
+ var xid_idx := xid[-1]
+ var selection_pivot_xid_idx := selection_pivot_xid[-1]
- var first_idx := mini(tid_idx, selection_pivot_tid_idx)
- var last_idx := maxi(tid_idx, selection_pivot_tid_idx)
+ var first_idx := mini(xid_idx, selection_pivot_xid_idx)
+ var last_idx := maxi(xid_idx, selection_pivot_xid_idx)
for i in range(first_idx, last_idx + 1):
- var new_tid := parent_tag.duplicate()
- new_tid.append(i)
- if not new_tid in selected_tids:
- selected_tids.append(new_tid)
+ var new_xid := parent_tag.duplicate()
+ new_xid.append(i)
+ if not new_xid in selected_xids:
+ selected_xids.append(new_xid)
- if selected_tids == old_selected_tids:
+ if selected_xids == old_selected_xids:
return
else:
if inner_selection_pivot == -1:
if inner_selections.is_empty():
- normal_select(tid, inner_idx)
+ normal_select(xid, inner_idx)
return
var old_inner_selections := inner_selections.duplicate()
@@ -194,79 +192,80 @@ func shift_select(tid: PackedInt32Array, inner_idx := -1) -> void:
# Select all tags.
func select_all() -> void:
clear_inner_selection()
- var tid_list := SVG.root_tag.get_all_tids()
- if selected_tids == tid_list:
+ var tag_list: Array[Tag] = SVG.root_tag.get_all_tags()
+ var xid_list: Array[PackedInt32Array] = tag_list.map(func(tag): return tag.xid)
+ if selected_xids == xid_list:
return
- for tid in SVG.root_tag.get_all_tids():
- if not tid in selected_tids:
- selected_tids.append(tid)
+ for xid in xid_list:
+ if not xid in selected_xids:
+ selected_xids.append(xid)
selection_changed.emit()
# Clear the selected tags.
func clear_selection() -> void:
- if not selected_tids.is_empty():
- selected_tids.clear()
- selection_pivot_tid.clear()
+ if not selected_xids.is_empty():
+ selected_xids.clear()
+ selection_pivot_xid.clear()
selection_changed.emit()
# Clear the inner selection.
func clear_inner_selection() -> void:
- if not inner_selections.is_empty() or not semi_selected_tid.is_empty():
+ if not inner_selections.is_empty() or not semi_selected_xid.is_empty():
inner_selections.clear()
- semi_selected_tid.clear()
+ semi_selected_xid.clear()
inner_selection_pivot = -1
selection_changed.emit()
# Clear the selected tags or the inner selection.
func clear_all_selections() -> void:
- if not inner_selections.is_empty() or not semi_selected_tid.is_empty() or\
- not selected_tids.is_empty():
- selected_tids.clear()
+ if not inner_selections.is_empty() or not semi_selected_xid.is_empty() or\
+ not selected_xids.is_empty():
+ selected_xids.clear()
inner_selections.clear()
- semi_selected_tid.clear()
+ semi_selected_xid.clear()
selection_changed.emit()
# Set the hovered tag.
-func set_hovered(tid: PackedInt32Array, inner_idx := -1) -> void:
+func set_hovered(xid: PackedInt32Array, inner_idx := -1) -> void:
if inner_idx == -1:
- if hovered_tid != tid:
- hovered_tid = tid.duplicate()
- if not tid.is_empty():
+ if hovered_xid != xid:
+ hovered_xid = xid.duplicate()
+ if not xid.is_empty():
inner_hovered = -1
- semi_hovered_tid = PackedInt32Array()
+ semi_hovered_xid = PackedInt32Array()
hover_changed.emit()
else:
- if semi_hovered_tid != tid:
- semi_hovered_tid = tid.duplicate()
+ if semi_hovered_xid != xid:
+ semi_hovered_xid = xid.duplicate()
inner_hovered = inner_idx
- if not tid.is_empty():
- hovered_tid.clear()
+ if not xid.is_empty():
+ hovered_xid.clear()
hover_changed.emit()
elif inner_hovered != inner_idx:
inner_hovered = inner_idx
- if not tid.is_empty():
- hovered_tid.clear()
+ if not xid.is_empty():
+ hovered_xid.clear()
hover_changed.emit()
# If the tag is hovered, make it not hovered.
-func remove_hovered(tid: PackedInt32Array, inner_idx := -1) -> void:
+func remove_hovered(xid: PackedInt32Array, inner_idx := -1) -> void:
if inner_idx == -1:
- if hovered_tid == tid:
- hovered_tid.clear()
+ if hovered_xid == xid:
+ hovered_xid.clear()
hover_changed.emit()
else:
- if semi_hovered_tid == tid and inner_hovered == inner_idx:
- semi_hovered_tid.clear()
+ if semi_hovered_xid == xid and inner_hovered == inner_idx:
+ semi_hovered_xid.clear()
inner_hovered = -1
hover_changed.emit()
# Clear the hovered tag.
func clear_hovered() -> void:
- if not hovered_tid.is_empty():
- hovered_tid.clear()
+ if not hovered_xid.is_empty():
+ hovered_xid.clear()
hover_changed.emit()
# Clear the inner hover.
@@ -276,108 +275,108 @@ func clear_inner_hovered() -> void:
hover_changed.emit()
# Returns whether the given tag or inner editor is hovered.
-func is_hovered(tid: PackedInt32Array, cmd_idx := -1, propagate := false) -> bool:
+func is_hovered(xid: PackedInt32Array, cmd_idx := -1, propagate := false) -> bool:
if propagate:
if cmd_idx == -1:
- return Utils.is_tid_parent_or_self(hovered_tid, tid)
+ return Utils.is_xid_parent_or_self(hovered_xid, xid)
else:
- return Utils.is_tid_parent_or_self(hovered_tid, tid) or\
- (semi_hovered_tid == tid and inner_hovered == cmd_idx)
+ return Utils.is_xid_parent_or_self(hovered_xid, xid) or\
+ (semi_hovered_xid == xid and inner_hovered == cmd_idx)
else:
if cmd_idx == -1:
- return hovered_tid == tid
+ return hovered_xid == xid
else:
- return semi_hovered_tid == tid and inner_hovered == cmd_idx
+ return semi_hovered_xid == xid and inner_hovered == cmd_idx
# Returns whether the given tag or inner editor is selected.
-func is_selected(tid: PackedInt32Array, cmd_idx := -1, propagate := false) -> bool:
+func is_selected(xid: PackedInt32Array, cmd_idx := -1, propagate := false) -> bool:
if propagate:
if cmd_idx == -1:
- for selected_tid in selected_tids:
- if Utils.is_tid_parent_or_self(selected_tid, tid):
+ for selected_xid in selected_xids:
+ if Utils.is_xid_parent_or_self(selected_xid, xid):
return true
return false
else:
- for selected_tid in selected_tids:
- if Utils.is_tid_parent_or_self(selected_tid, tid):
+ for selected_xid in selected_xids:
+ if Utils.is_xid_parent_or_self(selected_xid, xid):
return true
- return semi_selected_tid == tid and cmd_idx in inner_selections
+ return semi_selected_xid == xid and cmd_idx in inner_selections
else:
if cmd_idx == -1:
- return tid in selected_tids
+ return xid in selected_xids
else:
- return semi_selected_tid == tid and cmd_idx in inner_selections
+ return semi_selected_xid == xid and cmd_idx in inner_selections
-func set_proposed_drop_tid(tid: PackedInt32Array) -> void:
- if proposed_drop_tid != tid:
- proposed_drop_tid = tid.duplicate()
+func set_proposed_drop_xid(xid: PackedInt32Array) -> void:
+ if proposed_drop_xid != xid:
+ proposed_drop_xid = xid.duplicate()
proposed_drop_changed.emit()
-func clear_proposed_drop_tid() -> void:
- if not proposed_drop_tid.is_empty():
- proposed_drop_tid.clear()
+func clear_proposed_drop_xid() -> void:
+ if not proposed_drop_xid.is_empty():
+ proposed_drop_xid.clear()
proposed_drop_changed.emit()
-func _on_tags_added(tids: Array[PackedInt32Array]) -> void:
- selected_tids = tids.duplicate()
+func _on_tags_added(xids: Array[PackedInt32Array]) -> void:
+ selected_xids = xids.duplicate()
# If selected tags were deleted, remove them from the list of selected tags.
-func _on_tags_deleted(tids: Array[PackedInt32Array]) -> void:
- tids = tids.duplicate() # For some reason, it breaks without this.
- var old_selected_tids := selected_tids.duplicate()
- for deleted_tid in tids:
- for i in range(selected_tids.size() - 1, -1, -1):
- var tid := selected_tids[i]
- if Utils.is_tid_parent_or_self(deleted_tid, tid):
- selected_tids.remove_at(i)
- if old_selected_tids != selected_tids:
+func _on_tags_deleted(xids: Array[PackedInt32Array]) -> void:
+ xids = xids.duplicate() # For some reason, it breaks without this.
+ var old_selected_xids := selected_xids.duplicate()
+ for deleted_xid in xids:
+ for i in range(selected_xids.size() - 1, -1, -1):
+ var xid := selected_xids[i]
+ if Utils.is_xid_parent_or_self(deleted_xid, xid):
+ selected_xids.remove_at(i)
+ if old_selected_xids != selected_xids:
selection_changed.emit()
-# If selected tags were moved up or down, change the TIDs and their children.
-func _on_tags_moved_in_parent(parent_tid: PackedInt32Array, indices: Array[int]) -> void:
- var old_selected_tids := selected_tids.duplicate()
- var tids_to_select: Array[PackedInt32Array] = []
- var tids_to_unselect: Array[PackedInt32Array] = []
+# If selected tags were moved up or down, change the XIDs and their children.
+func _on_tags_moved_in_parent(parent_xid: PackedInt32Array, indices: Array[int]) -> void:
+ var old_selected_xids := selected_xids.duplicate()
+ var xids_to_select: Array[PackedInt32Array] = []
+ var xids_to_unselect: Array[PackedInt32Array] = []
for index_idx in indices.size():
if index_idx == indices[index_idx]:
continue
# For the tags that have moved, get their old.
- var old_moved_tid := parent_tid.duplicate()
- old_moved_tid.append(indices[index_idx])
+ var old_moved_xid := parent_xid.duplicate()
+ old_moved_xid.append(indices[index_idx])
- # If the TID or a child of it is found, append it.
- for tid in selected_tids:
- if Utils.is_tid_parent_or_self(old_moved_tid, tid):
- var new_selected_tid := tid.duplicate()
- new_selected_tid[parent_tid.size()] = index_idx
- tids_to_unselect.append(tid)
- tids_to_select.append(new_selected_tid)
- for tid in tids_to_unselect:
- selected_tids.erase(tid)
- selected_tids += tids_to_select
+ # If the XID or a child of it is found, append it.
+ for xid in selected_xids:
+ if Utils.is_xid_parent_or_self(old_moved_xid, xid):
+ var new_selected_xid := xid.duplicate()
+ new_selected_xid[parent_xid.size()] = index_idx
+ xids_to_unselect.append(xid)
+ xids_to_select.append(new_selected_xid)
+ for xid in xids_to_unselect:
+ selected_xids.erase(xid)
+ selected_xids += xids_to_select
- if old_selected_tids != selected_tids:
+ if old_selected_xids != selected_xids:
selection_changed.emit()
-# If selected tags were moved to a location, change the TIDs and their children.
-func _on_tags_moved_to(tids: Array[PackedInt32Array], location: PackedInt32Array) -> void:
- tids = tids.duplicate()
- var new_selected_tids: Array[PackedInt32Array] = []
- for moved_idx in tids.size():
- var moved_tid := tids[moved_idx]
- for tid in selected_tids:
- if Utils.is_tid_parent_or_self(moved_tid, tid):
- var new_location := Utils.get_parent_tid(location)
+# If selected tags were moved to a location, change the XIDs and their children.
+func _on_tags_moved_to(xids: Array[PackedInt32Array], location: PackedInt32Array) -> void:
+ xids = xids.duplicate()
+ var new_selected_xids: Array[PackedInt32Array] = []
+ for moved_idx in xids.size():
+ var moved_xid := xids[moved_idx]
+ for xid in selected_xids:
+ if Utils.is_xid_parent_or_self(moved_xid, xid):
+ var new_location := Utils.get_parent_xid(location)
new_location.append(moved_idx + location[-1])
- for ii in range(moved_tid.size(), tid.size()):
- new_location.append(tid[ii])
- new_selected_tids.append(new_location)
- if selected_tids != new_selected_tids:
- selected_tids = new_selected_tids
+ for ii in range(moved_xid.size(), xid.size()):
+ new_location.append(xid[ii])
+ new_selected_xids.append(new_location)
+ if selected_xids != new_selected_xids:
+ selected_xids = new_selected_xids
selection_changed.emit()
@@ -385,10 +384,10 @@ func respond_to_key_input(event: InputEventKey) -> void:
# Path commands using keys.
if inner_selections.is_empty() or event.is_command_or_control_pressed():
# If a single path tag is selected, add the new command at the end.
- if selected_tids.size() == 1:
- var tag_ref := SVG.root_tag.get_tag(selected_tids[0])
+ if selected_xids.size() == 1:
+ var tag_ref := SVG.root_tag.get_tag(selected_xids[0])
if tag_ref.name == "path":
- var path_attrib: AttributePath = tag_ref.attributes.d
+ var path_attrib: AttributePathdata = tag_ref.get_attribute("d")
for action_name in path_actions_dict.keys():
if event.is_action_pressed(action_name):
var path_cmd_count := path_attrib.get_command_count()
@@ -399,27 +398,25 @@ func respond_to_key_input(event: InputEventKey) -> void:
path_attrib.get_command(path_cmd_count - 1) is\
PathCommand.CloseCommand):
return
- path_attrib.insert_command(path_cmd_count, path_cmd_char, Vector2.ZERO,
- Attribute.SyncMode.INTERMEDIATE)
- normal_select(selected_tids[0], path_cmd_count)
+ path_attrib.insert_command(path_cmd_count, path_cmd_char, Vector2.ZERO)
+ normal_select(selected_xids[0], path_cmd_count)
handle_added.emit()
break
return
# If path commands are selected, insert after the last one.
for action_name in path_actions_dict.keys():
- var tag_ref := SVG.root_tag.get_tag(semi_selected_tid)
+ var tag_ref := SVG.root_tag.get_tag(semi_selected_xid)
if tag_ref.name == "path":
if event.is_action_pressed(action_name):
- var path_attrib: AttributePath = tag_ref.attributes.d
+ var path_attrib: AttributePathdata = tag_ref.get_attribute("d")
var path_cmd_char: String = path_actions_dict[action_name]
var last_selection: int = inner_selections.max()
# Z after a Z is syntactically invalid.
if path_attrib.get_command(last_selection) is PathCommand.CloseCommand and\
path_cmd_char in "Zz":
return
- path_attrib.insert_command(last_selection + 1, path_cmd_char, Vector2.ZERO,
- Attribute.SyncMode.INTERMEDIATE)
- normal_select(semi_selected_tid, last_selection + 1)
+ path_attrib.insert_command(last_selection + 1, path_cmd_char, Vector2.ZERO)
+ normal_select(semi_selected_xid, last_selection + 1)
handle_added.emit()
break
@@ -427,58 +424,58 @@ func respond_to_key_input(event: InputEventKey) -> void:
# Operations on selected tags.
func delete_selected() -> void:
- if not selected_tids.is_empty():
- SVG.root_tag.delete_tags(selected_tids)
- elif not inner_selections.is_empty() and not semi_selected_tid.is_empty():
+ if not selected_xids.is_empty():
+ SVG.root_tag.delete_tags(selected_xids)
+ elif not inner_selections.is_empty() and not semi_selected_xid.is_empty():
inner_selections.sort()
inner_selections.reverse()
- var tag_ref := SVG.root_tag.get_tag(semi_selected_tid)
+ var tag_ref := SVG.root_tag.get_tag(semi_selected_xid)
match tag_ref.name:
- "path": tag_ref.attributes.d.delete_commands(inner_selections)
+ "path": tag_ref.get_attribute("d").delete_commands(inner_selections)
clear_inner_selection()
clear_inner_hovered()
func move_up_selected() -> void:
- SVG.root_tag.move_tags_in_parent(selected_tids, false)
+ SVG.root_tag.move_tags_in_parent(selected_xids, false)
func move_down_selected() -> void:
- SVG.root_tag.move_tags_in_parent(selected_tids, true)
+ SVG.root_tag.move_tags_in_parent(selected_xids, true)
-func view_in_list(tid: PackedInt32Array) -> void:
- if tid.is_empty():
+func view_in_list(xid: PackedInt32Array) -> void:
+ if xid.is_empty():
return
- requested_scroll_to_tag_editor.emit(tid)
+ requested_scroll_to_tag_editor.emit(xid)
func duplicate_selected() -> void:
- SVG.root_tag.duplicate_tags(selected_tids)
+ SVG.root_tag.duplicate_tags(selected_xids)
func insert_inner_after_selection(new_command: String) -> void:
- var tag_ref := SVG.root_tag.get_tag(semi_selected_tid)
+ var tag_ref := SVG.root_tag.get_tag(semi_selected_xid)
match tag_ref.name:
"path":
- var path_attrib: AttributePath = tag_ref.attributes.d
+ var path_attrib: AttributePathdata = tag_ref.get_attribute("d")
var last_selection: int = inner_selections.max()
# Z after a Z is syntactically invalid.
if path_attrib.get_command(last_selection) is PathCommand.CloseCommand and\
new_command in "Zz":
return
path_attrib.insert_command(last_selection + 1, new_command)
- normal_select(semi_selected_tid, last_selection + 1)
+ normal_select(semi_selected_xid, last_selection + 1)
-enum SelectionContext {
+enum Context {
VIEWPORT,
TAG_EDITOR
}
-func get_selection_context(popup_method: Callable, context: SelectionContext) -> ContextPopup:
+func get_selection_context(popup_method: Callable, context: Context) -> ContextPopup:
var btn_arr: Array[Button] = []
- if not selected_tids.is_empty():
- var filtered_tids := Utils.filter_descendant_tids(selected_tids)
+ if not selected_xids.is_empty():
+ var filtered_xids := Utils.filter_descendant_xids(selected_xids)
var can_move_down := true
var can_move_up := true
- for base_tid in filtered_tids:
- if not Utils.are_tid_parents_same(base_tid, filtered_tids[0]):
+ for base_xid in filtered_xids:
+ if not Utils.are_xid_parents_same(base_xid, filtered_xids[0]):
can_move_down = false
can_move_up = false
break
@@ -486,26 +483,26 @@ func get_selection_context(popup_method: Callable, context: SelectionContext) ->
if can_move_up or can_move_down:
can_move_down = false
can_move_up = false
- var parent_tid := Utils.get_parent_tid(filtered_tids[0])
- var filtered_count := filtered_tids.size()
- var parent_child_count := SVG.root_tag.get_tag(parent_tid).get_child_count()
- for base_tid in filtered_tids:
- if not can_move_up and base_tid[-1] >= filtered_count:
+ var parent_xid := Utils.get_parent_xid(filtered_xids[0])
+ var filtered_count := filtered_xids.size()
+ var parent_child_count := SVG.root_tag.get_tag(parent_xid).get_child_count()
+ for base_xid in filtered_xids:
+ if not can_move_up and base_xid[-1] >= filtered_count:
can_move_up = true
- if not can_move_down and base_tid[-1] < parent_child_count - filtered_count:
+ if not can_move_down and base_xid[-1] < parent_child_count - filtered_count:
can_move_down = true
- if context == SelectionContext.VIEWPORT:
+ if context == Context.VIEWPORT:
btn_arr.append(ContextPopup.create_button(
TranslationServer.translate("View In List"),
- view_in_list.bind(selected_tids[0]), false,
+ view_in_list.bind(selected_xids[0]), false,
load("res://visual/icons/ViewInList.svg")))
btn_arr.append(ContextPopup.create_button(TranslationServer.translate("Duplicate"),
duplicate_selected, false, load("res://visual/icons/Duplicate.svg"),
"duplicate"))
- if selected_tids.size() == 1 and not SVG.root_tag.get_tag(
- selected_tids[0]).possible_conversions.is_empty():
+ if selected_xids.size() == 1 and not SVG.root_tag.get_tag(
+ selected_xids[0]).possible_conversions.is_empty():
btn_arr.append(ContextPopup.create_button(
TranslationServer.translate("Convert To"),
popup_convert_to_context.bind(popup_method), false,
@@ -524,11 +521,11 @@ func get_selection_context(popup_method: Callable, context: SelectionContext) ->
btn_arr.append(ContextPopup.create_button(TranslationServer.translate("Delete"),
delete_selected, false, load("res://visual/icons/Delete.svg"), "delete"))
- elif not inner_selections.is_empty() and not semi_selected_tid.is_empty():
- if context == SelectionContext.VIEWPORT:
+ elif not inner_selections.is_empty() and not semi_selected_xid.is_empty():
+ if context == Context.VIEWPORT:
btn_arr.append(ContextPopup.create_button(
TranslationServer.translate("View In List"),
- view_in_list.bind(semi_selected_tid), false,
+ view_in_list.bind(semi_selected_xid), false,
load("res://visual/icons/ViewInList.svg")))
if inner_selections.size() == 1:
btn_arr.append(ContextPopup.create_button(
@@ -549,21 +546,21 @@ func get_selection_context(popup_method: Callable, context: SelectionContext) ->
func popup_convert_to_context(popup_method: Callable) -> void:
# The "Convert To" context popup.
- if not selected_tids.is_empty():
+ if not selected_xids.is_empty():
var btn_arr: Array[Button] = []
- var tag := SVG.root_tag.get_tag(selected_tids[0])
+ var tag := SVG.root_tag.get_tag(selected_xids[0])
for tag_name in tag.possible_conversions:
var btn := ContextPopup.create_button(tag_name,
convert_selected_tag_to.bind(tag_name), !tag.can_replace(tag_name),
- load("res://visual/icons/tag/%s.svg" % tag_name))
+ DB.get_tag_icon(tag_name))
btn.add_theme_font_override("font", load("res://visual/fonts/FontMono.ttf"))
btn_arr.append(btn)
var context_popup := ContextPopup.new()
context_popup.setup(btn_arr, true)
popup_method.call(context_popup)
- elif not inner_selections.is_empty() and not semi_selected_tid.is_empty():
- var cmd_char: String = SVG.root_tag.get_tag(semi_selected_tid).\
- attributes.d.get_command(inner_selections[0]).command_char
+ elif not inner_selections.is_empty() and not semi_selected_xid.is_empty():
+ var cmd_char: String = SVG.root_tag.get_tag(semi_selected_xid).\
+ get_attribute("d").get_command(inner_selections[0]).command_char
var command_picker := PathCommandPopup.instantiate()
popup_method.call(command_picker)
command_picker.force_relativity(Utils.is_string_lower(cmd_char))
@@ -571,7 +568,7 @@ func popup_convert_to_context(popup_method: Callable) -> void:
command_picker.path_command_picked.connect(convert_selected_command_to)
func popup_insert_command_after_context(popup_method: Callable) -> void:
- var cmd_char: String = SVG.root_tag.get_tag(semi_selected_tid).attributes.d.\
+ var cmd_char: String = SVG.root_tag.get_tag(semi_selected_xid).get_attribute("d").\
get_command(inner_selections.max()).command_char
var command_picker := PathCommandPopup.instantiate()
@@ -585,10 +582,10 @@ func popup_insert_command_after_context(popup_method: Callable) -> void:
"Q", "T": command_picker.disable_invalid(["S"])
func convert_selected_tag_to(tag_name: String) -> void:
- var tid := selected_tids[0]
- SVG.root_tag.replace_tag(tid, SVG.root_tag.get_tag(tid).get_replacement(tag_name))
+ var xid := selected_xids[0]
+ SVG.root_tag.replace_tag(xid, SVG.root_tag.get_tag(xid).get_replacement(tag_name))
func convert_selected_command_to(cmd_type: String) -> void:
- var tag_ref := SVG.root_tag.get_tag(semi_selected_tid)
+ var tag_ref := SVG.root_tag.get_tag(semi_selected_xid)
match tag_ref.name:
- "path": tag_ref.attributes.d.convert_command(inner_selections[0], cmd_type)
+ "path": tag_ref.get_attribute("d").convert_command(inner_selections[0], cmd_type)
diff --git a/src/SVG.gd b/src/SVG.gd
index d847f3d1..49d95d76 100644
--- a/src/SVG.gd
+++ b/src/SVG.gd
@@ -2,26 +2,31 @@
# The SVG text, and the native TagSVG representation.
extends Node
+signal changed_unknown
+signal resized
+
+signal attribute_somewhere_changed(xid: PackedInt32Array)
+signal tags_added(xids: Array[PackedInt32Array])
+signal tags_deleted(xids: Array[PackedInt32Array])
+signal tags_moved_in_parent(parent_xid: PackedInt32Array, old_indices: Array[int])
+signal tags_moved_to(xids: Array[PackedInt32Array], location: PackedInt32Array)
+signal tag_layout_changed # Emitted together with any of the above 5.
+
signal parsing_finished(error_id: SVGParser.ParseError)
-signal svg_text_changed()
-const DEFAULT = ''
+const DEFAULT = ''
-var text := "":
- set(value):
- text = value
- svg_text_changed.emit()
+var text := ""
-var root_tag := TagSVG.new()
+var root_tag := TagRoot.new()
var UR := UndoRedo.new()
func _ready() -> void:
UR.version_changed.connect(_on_undo_redo)
- root_tag.changed_unknown.connect(update_text.bind(false))
- root_tag.attribute_changed.connect(update_text)
- root_tag.child_attribute_changed.connect(update_text)
- root_tag.tag_layout_changed.connect(update_text)
+ changed_unknown.connect(update_text.bind(false))
+ attribute_somewhere_changed.connect(update_text.unbind(1))
+ tag_layout_changed.connect(update_text)
var cmdline_args := OS.get_cmdline_args()
var load_cmdl := false
@@ -43,45 +48,50 @@ func _ready() -> void:
UR.clear_history()
-
func _exit_tree() -> void:
UR.free()
-func update_tags() -> void:
- var svg_parse_result := SVGParser.text_to_svg(text)
+func sync() -> void:
+ var old_size := root_tag.get_size()
+ var svg_parse_result := SVGParser.text_to_root(text)
parsing_finished.emit(svg_parse_result.error)
if svg_parse_result.error == SVGParser.ParseError.OK:
- root_tag.replace_self(svg_parse_result.svg)
+ root_tag = svg_parse_result.svg
+ root_tag.attribute_somewhere_changed.connect(attribute_somewhere_changed.emit)
+ root_tag.tags_added.connect(tags_added.emit)
+ root_tag.tags_deleted.connect(tags_deleted.emit)
+ root_tag.tags_moved_in_parent.connect(tags_moved_in_parent.emit)
+ root_tag.tags_moved_to.connect(tags_moved_to.emit)
+ root_tag.tag_layout_changed.connect(tag_layout_changed.emit)
+ changed_unknown.emit()
+ if root_tag.get_size() != old_size:
+ resized.emit()
func update_text(undo_redo := true) -> void:
if undo_redo:
UR.create_action("")
- UR.add_do_property(self, "text", SVGParser.svg_to_text(root_tag))
+ UR.add_do_property(self, "text", SVGParser.root_to_text(root_tag))
UR.add_undo_property(self, "text", GlobalSettings.save_data.svg_text)
UR.commit_action()
GlobalSettings.modify_save_data("svg_text", text)
else:
- text = SVGParser.svg_to_text(root_tag)
+ text = SVGParser.root_to_text(root_tag)
func undo() -> void:
if UR.has_undo():
UR.undo()
- update_tags()
+ sync()
func redo() -> void:
if UR.has_redo():
UR.redo()
- update_tags()
+ sync()
func _on_undo_redo() -> void:
GlobalSettings.modify_save_data("svg_text", text)
-
-func refresh() -> void:
- SVG.root_tag.replace_self(SVG.root_tag.duplicate())
-
-func apply_svg_text(svg_text: String,) -> void:
+func apply_svg_text(svg_text: String) -> void:
text = svg_text
GlobalSettings.modify_save_data("svg_text", text)
- update_tags()
+ sync()
diff --git a/src/ThemeGenerator.gd b/src/ThemeGenerator.gd
index 46ff40bf..f1493eaa 100644
--- a/src/ThemeGenerator.gd
+++ b/src/ThemeGenerator.gd
@@ -13,7 +13,6 @@ const common_text_color = Color("ddeeff")
const common_subtle_text_color = Color("ffffff55")
const common_inner_color_disabled = Color("0e0e12")
const common_border_color_disabled = Color("1e1f24")
-const common_separator_color = Color("414159", 0.6)
const common_button_inner_color_normal = Color("1c1e38")
const common_button_border_color_normal = Color("313859")
@@ -247,6 +246,8 @@ static func setup_button(theme: Theme) -> void:
normal_left_connected_button_stylebox.bg_color = connected_button_inner_color_normal
normal_left_connected_button_stylebox.border_color = connected_button_border_color_normal
theme.set_stylebox("normal", "LeftConnectedButton", normal_left_connected_button_stylebox)
+ # Disabled theme is not currently used, but is needed for correct spacing.
+ theme.set_stylebox("disabled", "LeftConnectedButton", normal_left_connected_button_stylebox)
var hover_left_connected_button_stylebox := left_connected_button_stylebox.duplicate()
hover_left_connected_button_stylebox.bg_color = connected_button_inner_color_hover
@@ -311,6 +312,8 @@ static func setup_button(theme: Theme) -> void:
normal_right_connected_button_stylebox.bg_color = connected_button_inner_color_normal
normal_right_connected_button_stylebox.border_color = connected_button_border_color_normal
theme.set_stylebox("normal", "RightConnectedButton", normal_right_connected_button_stylebox)
+ # Disabled theme is not currently used, but is needed for correct spacing.
+ theme.set_stylebox("disabled", "RightConnectedButton", normal_right_connected_button_stylebox)
var hover_right_connected_button_stylebox := right_connected_button_stylebox.duplicate()
hover_right_connected_button_stylebox.bg_color = connected_button_inner_color_hover
@@ -709,6 +712,7 @@ static func setup_lineedit(theme: Theme) -> void:
var empty_stylebox := StyleBoxEmpty.new()
theme.set_stylebox("hover", "GoodColorPickerLineEdit", empty_stylebox)
theme.set_stylebox("focus", "GoodColorPickerLineEdit", empty_stylebox)
+ theme.set_stylebox("read_only", "GoodColorPickerLineEdit", empty_stylebox)
static func setup_scrollbar(theme: Theme) -> void:
theme.add_type("HScrollBar")
@@ -764,13 +768,14 @@ static func setup_scrollbar(theme: Theme) -> void:
static func setup_separator(theme: Theme) -> void:
theme.add_type("HSeparator")
var stylebox := StyleBoxLine.new()
- stylebox.color = common_separator_color
+ stylebox.color = common_panel_border_color
stylebox.thickness = 2
theme.set_stylebox("separator", "HSeparator", stylebox)
theme.add_type("SmallHSeparator")
theme.set_type_variation("SmallHSeparator", "HSeparator")
var small_stylebox := stylebox.duplicate()
+ small_stylebox.color = Color(common_panel_border_color, 0.5)
small_stylebox.grow_begin = -3
small_stylebox.grow_end = -3
theme.set_stylebox("separator", "SmallHSeparator", small_stylebox)
diff --git a/src/Utils.gd b/src/Utils.gd
index 350dcd8c..fcab334f 100644
--- a/src/Utils.gd
+++ b/src/Utils.gd
@@ -5,17 +5,15 @@ enum CustomNotification {
LANGUAGE_CHANGED = 300,
UI_SCALE_CHANGED = 301,
THEME_CHANGED = 302,
- NUMBER_PRECISION_CHANGED = 303,
- HIGHLIGHT_COLORS_CHANGED = 304,
- BASIC_COLORS_CHANGED = 305,
- HANDLE_VISUALS_CHANGED = 306,
+ SHORTCUTS_CHANGED = 303,
+ NUMBER_PRECISION_CHANGED = 304,
+ HIGHLIGHT_COLORS_CHANGED = 305,
+ BASIC_COLORS_CHANGED = 306,
+ HANDLE_VISUALS_CHANGED = 307,
}
-# Enum with values to be used for set_value() of attribute editors.
-# REGULAR means that the attribute will update if the new value is different.
-# INTERMEDIATE and FINAL cause the attribute update to have the corresponding sync mode.
-# FINAL also causes the equivalence check to be skipped.
-enum UpdateType {REGULAR, INTERMEDIATE, FINAL}
+static func custom_notify(notif: CustomNotification) -> void:
+ Engine.get_main_loop().get_root().propagate_notification(notif)
enum InteractionType {NONE = 0, HOVERED = 1, SELECTED = 2, HOVERED_SELECTED = 3}
@@ -53,6 +51,16 @@ cp3: Vector2) -> PackedVector2Array:
return Utils.get_cubic_bezier_points(
cp1, 2/3.0 * (cp2 - cp1), 2/3.0 * (cp2 - cp3), cp3)
+# Calculate quadratic bezier point coordinate along an axis.
+static func quadratic_bezier_point(p0: float, p1: float, p2: float, t: float) -> float:
+ var u := 1.0 - t
+ return u * u * p0 + 2 * u * t * p1 + t * t * p2
+
+# Calculate cubic bezier point coordinate along an axis.
+static func cubic_bezier_point(p0: float, p1: float, p2: float, p3: float, t: float) -> float:
+ var u := 1.0 - t
+ return u * u * u * p0 + 3 * u * u * t * p1 + 3 * u * t * t * p2 + t * t * t * p3
+
# Ellipse parametric equation.
static func E(c: Vector2, r: Vector2, cosine: float, sine: float, t: float) -> Vector2:
var xt := r.x * cos(t)
@@ -67,20 +75,20 @@ static func Et(r: Vector2, cosine: float, sine: float, t: float) -> Vector2:
# [1] > [1, 2] > [1, 0] > [0]
-static func compare_tids(tid1: PackedInt32Array, tid2: PackedInt32Array) -> bool:
- var smaller_tid_size := mini(tid1.size(), tid2.size())
- for i in smaller_tid_size:
- if tid1[i] < tid2[i]:
+static func compare_xids(xid1: PackedInt32Array, xid2: PackedInt32Array) -> bool:
+ var smaller_xid_size := mini(xid1.size(), xid2.size())
+ for i in smaller_xid_size:
+ if xid1[i] < xid2[i]:
return true
- elif tid1[i] > tid2[i]:
+ elif xid1[i] > xid2[i]:
return false
- return tid1.size() > smaller_tid_size
+ return xid1.size() > smaller_xid_size
-static func compare_tids_r(tid1: PackedInt32Array, tid2: PackedInt32Array) -> bool:
- return compare_tids(tid2, tid1)
+static func compare_xids_r(xid1: PackedInt32Array, xid2: PackedInt32Array) -> bool:
+ return compare_xids(xid2, xid1)
# Indirect parent, i.e. ancestor. Passing the root tag as parent will return false.
-static func is_tid_parent(parent: PackedInt32Array, child: PackedInt32Array) -> bool:
+static func is_xid_parent(parent: PackedInt32Array, child: PackedInt32Array) -> bool:
if parent.is_empty():
return false
var parent_size := parent.size()
@@ -92,38 +100,38 @@ static func is_tid_parent(parent: PackedInt32Array, child: PackedInt32Array) ->
return false
return true
-static func is_tid_parent_or_self(parent: PackedInt32Array,
+static func is_xid_parent_or_self(parent: PackedInt32Array,
child: PackedInt32Array) -> bool:
- return is_tid_parent(parent, child) or parent == child
+ return is_xid_parent(parent, child) or parent == child
-static func get_parent_tid(tid: PackedInt32Array) -> PackedInt32Array:
- var parent_tid := tid.duplicate()
- parent_tid.resize(tid.size() - 1)
- return parent_tid
+static func get_parent_xid(xid: PackedInt32Array) -> PackedInt32Array:
+ var parent_xid := xid.duplicate()
+ parent_xid.resize(xid.size() - 1)
+ return parent_xid
-static func are_tid_parents_same(tid1: PackedInt32Array, tid2: PackedInt32Array) -> bool:
- if tid1.size() != tid2.size():
+static func are_xid_parents_same(xid1: PackedInt32Array, xid2: PackedInt32Array) -> bool:
+ if xid1.size() != xid2.size():
return false
- for i in tid1.size() - 1:
- if tid1[i] != tid2[i]:
+ for i in xid1.size() - 1:
+ if xid1[i] != xid2[i]:
return false
return true
# Filter out all descendants.
-static func filter_descendant_tids(tids: Array[PackedInt32Array]) -> Array[PackedInt32Array]:
- var new_tids: Array[PackedInt32Array] = tids.duplicate()
- new_tids.sort_custom(Utils.compare_tids_r)
+static func filter_descendant_xids(xids: Array[PackedInt32Array]) -> Array[PackedInt32Array]:
+ var new_xids: Array[PackedInt32Array] = xids.duplicate()
+ new_xids.sort_custom(Utils.compare_xids_r)
# Linear scan to filter out the descendants.
- var last_accepted := new_tids[0]
+ var last_accepted := new_xids[0]
var i := 1
- while i < new_tids.size():
- var tid := new_tids[i]
- if Utils.is_tid_parent_or_self(last_accepted, tid):
- new_tids.remove_at(i)
+ while i < new_xids.size():
+ var xid := new_xids[i]
+ if Utils.is_xid_parent_or_self(last_accepted, xid):
+ new_xids.remove_at(i)
else:
- last_accepted = new_tids[i]
+ last_accepted = new_xids[i]
i += 1
- return new_tids
+ return new_xids
static func is_event_drag(event: InputEvent) -> bool:
diff --git a/src/data_classes/Attribute.gd b/src/data_classes/Attribute.gd
index 2781188f..6f13836b 100644
--- a/src/data_classes/Attribute.gd
+++ b/src/data_classes/Attribute.gd
@@ -2,58 +2,34 @@
# If the Attribute's data type is known, one of the inheriting classes should be used.
class_name Attribute extends RefCounted
-signal value_changed(new_value: String)
-signal propagate_value_changed(undo_redo: bool)
+signal value_changed(changed: bool, save_changed: bool)
var name: String
var _value: String
+var _saved_value: String # Value that is stored in UndoRedo.
-enum SyncMode {LOUD, INTERMEDIATE, FINAL, NO_PROPAGATION, SILENT}
-
-# LOUD means the attribute will emit value_changed and be noticed everywhere.
-
-# INTERMEDIATE is the same as LOUD, but doesn't create an UndoRedo action.
-# Can be used to update an attribute continuously (i.e. dragging a color).
-
-# FINAL is the same as LOUD, but it runs even if the new value is the same.
-# This can be used to force an UndoRedo action after some intermediate changes.
-# Note that the attribute is not responsible for making sure the new value is
-# different from the previous one in the UndoRedo, this must be handled in the widgets.
-
-# NO_PROPAGATION means the tag won't learn about it. This can allow the attribute change
-# to be noted by an attribute editor without the SVG text being updated.
-# This can be used, for example, to update two attributes corresponding to 2D coordinates
-# without the first one causing an update to the SVG text.
-
-# SILENT means the attribute update is ignored fully. It only makes sense
-# if there is logic for updating the corresponding attribute editor despite that.
-
-func set_value(new_value: String, sync_mode := SyncMode.LOUD) -> void:
+func set_value(new_value: String, save := true) -> void:
var proposed_new_value := format(new_value)
- if proposed_new_value != _value or sync_mode == SyncMode.FINAL:
+ var changed := false
+ var saved_changed := false
+ if proposed_new_value != _value:
_value = proposed_new_value
+ changed = true
_sync()
- if sync_mode != SyncMode.SILENT:
- value_changed.emit(proposed_new_value)
- if sync_mode != SyncMode.NO_PROPAGATION:
- propagate_value_changed.emit(sync_mode != SyncMode.INTERMEDIATE)
+ if save and proposed_new_value != _saved_value:
+ _saved_value = proposed_new_value
+ saved_changed = true
+ value_changed.emit(changed, saved_changed)
func get_value() -> String:
return _value
-# Override these functions in extending classes to update the non-string representation.
func _sync() -> void:
- return
+ pass
func format(text: String) -> String:
return text
-func get_default() -> String:
- if name in DB.attribute_defaults:
- return DB.attribute_defaults[name]
- else:
- return ""
-
func _init(new_name: String, init_value := "") -> void:
name = new_name
- set_value(init_value, SyncMode.FINAL)
+ set_value(init_value)
diff --git a/src/data_classes/AttributeColor.gd b/src/data_classes/AttributeColor.gd
index 1a69d101..07570f42 100644
--- a/src/data_classes/AttributeColor.gd
+++ b/src/data_classes/AttributeColor.gd
@@ -3,24 +3,17 @@ class_name AttributeColor extends Attribute
# No direct color representation for this attribute type. There are too many quirks.
-func set_value(new_value: String, sync_mode := SyncMode.LOUD) -> void:
- super(new_value if (new_value.is_empty() or ColorParser.is_valid(new_value))\
- else get_default(), sync_mode)
+func set_value(new_value: String, save := true) -> void:
+ super(new_value if ColorParser.is_valid(new_value) else "", save)
func format(text: String) -> String:
if GlobalSettings.color_enable_autoformatting:
- var new_text := ColorParser.format_text(text)
- return get_default() if ColorParser.are_colors_same(new_text, get_default()) else\
- new_text
+ return ColorParser.format_text(text)
else:
return text
-
func get_color() -> Color:
- if _value.is_empty():
- return ColorParser.string_to_color(DB.attribute_defaults[name])
- else:
- return ColorParser.string_to_color(_value)
+ return ColorParser.string_to_color(_value)
const special_colors = ["none", "currentColor"]
diff --git a/src/data_classes/AttributeEnum.gd b/src/data_classes/AttributeEnum.gd
index 3aba6ee3..9806e2ba 100644
--- a/src/data_classes/AttributeEnum.gd
+++ b/src/data_classes/AttributeEnum.gd
@@ -1,8 +1,6 @@
# An attribute with only a set of meaningful values.
class_name AttributeEnum extends Attribute
-func set_value(new_value: String, sync_mode := SyncMode.LOUD) -> void:
- if new_value.is_empty() or new_value in DB.attribute_enum_values[name]:
- super(new_value, sync_mode)
- else:
- super(get_default(), sync_mode)
+func set_value(new_value: String, save := true) -> void:
+ super(new_value if (new_value.is_empty() or\
+ new_value in DB.attribute_enum_values[name]) else "", save)
diff --git a/src/data_classes/AttributeID.gd b/src/data_classes/AttributeID.gd
new file mode 100644
index 00000000..f955082a
--- /dev/null
+++ b/src/data_classes/AttributeID.gd
@@ -0,0 +1,6 @@
+# An attribute representing an element's id
+class_name AttributeID extends Attribute
+
+func set_value(new_value: String, save := true) -> void:
+ super(new_value if IDParser.get_validity(new_value) != IDParser.ValidityLevel.INVALID\
+ else "", save)
diff --git a/src/data_classes/AttributeList.gd b/src/data_classes/AttributeList.gd
index 21784376..edd2305e 100644
--- a/src/data_classes/AttributeList.gd
+++ b/src/data_classes/AttributeList.gd
@@ -6,9 +6,9 @@ var _list: PackedFloat32Array
func _sync() -> void:
_list = ListParser.string_to_list(get_value())
-func set_list(new_list: PackedFloat32Array, sync_mode := SyncMode.LOUD) -> void:
+func set_list(new_list: PackedFloat32Array, save := true) -> void:
_list = new_list
- super.set_value(ListParser.list_to_string(new_list), sync_mode)
+ super.set_value(ListParser.list_to_string(new_list), save)
func get_list() -> PackedFloat32Array:
return _list
@@ -17,14 +17,14 @@ func get_list_size() -> int:
return _list.size()
# Just a helper to handle Rect2.
-func set_rect(new_rect: Rect2, sync_mode := SyncMode.LOUD) -> void:
+func set_rect(new_rect: Rect2, save := true) -> void:
set_list(PackedFloat32Array([new_rect.position.x, new_rect.position.y,
- new_rect.size.x, new_rect.size.y]), sync_mode)
+ new_rect.size.x, new_rect.size.y]), save)
-func set_list_element(idx: int, new_value: float, sync_mode := SyncMode.LOUD) -> void:
+func set_list_element(idx: int, new_value: float, save := true) -> void:
_list[idx] = new_value
- set_value(ListParser.list_to_string(_list), sync_mode)
+ set_value(ListParser.list_to_string(_list), save)
func get_list_element(idx: int) -> float:
return _list[idx] if idx < _list.size() else NAN
diff --git a/src/data_classes/AttributeNumeric.gd b/src/data_classes/AttributeNumeric.gd
index ef9f4d7f..10ce298a 100644
--- a/src/data_classes/AttributeNumeric.gd
+++ b/src/data_classes/AttributeNumeric.gd
@@ -4,9 +4,7 @@ class_name AttributeNumeric extends Attribute
var _number := NAN
func _sync() -> void:
- if _value.is_empty():
- _number = NumberParser.text_to_num(DB.attribute_defaults[name])
- else:
+ if not _value.is_empty():
_number = NumberParser.text_to_num(_value)
func format(text: String) -> String:
@@ -15,10 +13,10 @@ func format(text: String) -> String:
else:
return text
-func set_num(new_number: float, sync_mode := SyncMode.LOUD) -> void:
+func set_num(new_number: float, save := true) -> void:
_number = new_number
super.set_value(NumberParser.num_to_text(new_number) if is_finite(_number) else "",
- sync_mode)
+ save)
func get_num() -> float:
return _number
diff --git a/src/data_classes/AttributePath.gd b/src/data_classes/AttributePathdata.gd
similarity index 78%
rename from src/data_classes/AttributePath.gd
rename to src/data_classes/AttributePathdata.gd
index 2ca375aa..185337bb 100644
--- a/src/data_classes/AttributePath.gd
+++ b/src/data_classes/AttributePathdata.gd
@@ -1,22 +1,25 @@
# The "d" attribute of [TagPath].
-class_name AttributePath extends Attribute
+class_name AttributePathdata extends Attribute
var _commands: Array[PathCommand]
func _sync() -> void:
- _commands = PathDataParser.parse_path_data(get_value())
+ _commands = PathdataParser.parse_pathdata(get_value())
locate_start_points()
func autoformat(text: String) -> String:
- return PathDataParser.path_commands_to_text(PathDataParser.parse_path_data(text))
+ return PathdataParser.path_commands_to_text(PathdataParser.parse_pathdata(text))
-func set_commands(new_commands: Array[PathCommand], sync_mode := SyncMode.LOUD) -> void:
+func get_commands() -> Array[PathCommand]:
+ return _commands
+
+func set_commands(new_commands: Array[PathCommand], save := true) -> void:
_commands = new_commands
- sync_after_commands_change(sync_mode)
+ sync_after_commands_change(save)
-func sync_after_commands_change(sync_mode := SyncMode.LOUD) -> void:
- set_value(PathDataParser.path_commands_to_text(_commands), sync_mode)
+func sync_after_commands_change(save := true) -> void:
+ set_value(PathdataParser.path_commands_to_text(_commands), save)
func locate_start_points() -> void:
@@ -113,51 +116,50 @@ func get_implied_T_control(idx: int) -> Vector2:
func set_command_property(idx: int, property: String, new_value: float,
-sync_mode := SyncMode.LOUD) -> void:
+save := true) -> void:
var cmd := get_command(idx)
- if cmd.get(property) != new_value or sync_mode == SyncMode.FINAL:
+ if cmd.get(property) != new_value:
cmd.set(property, new_value)
- sync_after_commands_change(sync_mode)
+ sync_after_commands_change(save)
-func insert_command(idx: int, command_char: String, vec := Vector2.ZERO,
-sync_mode := SyncMode.LOUD) -> void:
- var new_cmd: PathCommand = PathCommand.translation_dict[command_char.to_upper()].new()
- var relative := Utils.is_string_lower(command_char)
+func insert_command(idx: int, cmd_char: String, vec := Vector2.ZERO, save := true) -> void:
+ var new_cmd: PathCommand = PathCommand.translation_dict[cmd_char.to_upper()].new()
+ var relative := Utils.is_string_lower(cmd_char)
if relative:
new_cmd.toggle_relative()
_commands.insert(idx, new_cmd)
locate_start_points()
- if not command_char in "Zz":
- if not command_char in "Vv":
+ if not cmd_char in "Zz":
+ if not cmd_char in "Vv":
new_cmd.x = vec.x
- if not command_char in "Hh":
+ if not cmd_char in "Hh":
new_cmd.y = vec.y
- if command_char in "Qq":
+ if cmd_char in "Qq":
new_cmd.x1 = lerpf(0.0 if relative else new_cmd.start.x, vec.x, 0.5)
new_cmd.y1 = lerpf(0.0 if relative else new_cmd.start.y, vec.y, 0.5)
- elif command_char in "Ss":
+ elif cmd_char in "Ss":
new_cmd.x2 = lerpf(0.0 if relative else new_cmd.start.x, vec.x, 2/3.0)
new_cmd.y2 = lerpf(0.0 if relative else new_cmd.start.y, vec.y, 2/3.0)
- elif command_char in "Cc":
+ elif cmd_char in "Cc":
new_cmd.x1 = lerpf(0.0 if relative else new_cmd.start.x, vec.x, 1/3.0)
new_cmd.y1 = lerpf(0.0 if relative else new_cmd.start.y, vec.y, 1/3.0)
new_cmd.x2 = lerpf(0.0 if relative else new_cmd.start.x, vec.x, 2/3.0)
new_cmd.y2 = lerpf(0.0 if relative else new_cmd.start.y, vec.y, 2/3.0)
- sync_after_commands_change(sync_mode)
+ sync_after_commands_change(save)
-func convert_command(idx: int, command_char: String, sync_mode := SyncMode.LOUD) -> void:
+func convert_command(idx: int, cmd_char: String, save := true) -> void:
var old_cmd := get_command(idx)
- if old_cmd.command_char == command_char:
+ if old_cmd.command_char == cmd_char:
return
- var cmd_absolute_char := command_char.to_upper()
+ var cmd_absolute_char := cmd_char.to_upper()
var new_cmd: PathCommand = PathCommand.translation_dict[cmd_absolute_char].new()
for property in ["x", "y", "x1", "y1", "x2", "y2"]:
if property in old_cmd and property in new_cmd:
new_cmd[property] = old_cmd[property]
- var relative := Utils.is_string_lower(command_char)
+ var relative := Utils.is_string_lower(cmd_char)
if "x" in new_cmd and not "x" in old_cmd:
new_cmd.x = 0.0 if relative else old_cmd.start.x
@@ -192,10 +194,10 @@ func convert_command(idx: int, command_char: String, sync_mode := SyncMode.LOUD)
_commands.insert(idx, new_cmd)
if relative:
_commands[idx].toggle_relative()
- sync_after_commands_change(sync_mode)
+ sync_after_commands_change(save)
-func delete_commands(indices: Array[int], sync_mode := SyncMode.LOUD) -> void:
+func delete_commands(indices: Array[int], save := true) -> void:
if indices.is_empty():
return
@@ -204,8 +206,8 @@ func delete_commands(indices: Array[int], sync_mode := SyncMode.LOUD) -> void:
indices.reverse()
for idx in indices:
_commands.remove_at(idx)
- sync_after_commands_change(sync_mode)
+ sync_after_commands_change(save)
-func toggle_relative_command(idx: int, sync_mode := SyncMode.LOUD) -> void:
+func toggle_relative_command(idx: int, save := true) -> void:
_commands[idx].toggle_relative()
- sync_after_commands_change(sync_mode)
+ sync_after_commands_change(save)
diff --git a/src/data_classes/AttributeTransform.gd b/src/data_classes/AttributeTransform.gd
deleted file mode 100644
index 10911659..00000000
--- a/src/data_classes/AttributeTransform.gd
+++ /dev/null
@@ -1,141 +0,0 @@
-# An attribute representing a list of transformations.
-class_name AttributeTransform extends Attribute
-
-class Transform extends RefCounted:
- func compute_transform() -> Transform2D:
- return Transform2D.IDENTITY
-
-class TransformMatrix extends Transform:
- var x1: float
- var x2: float
- var y1: float
- var y2: float
- var o1: float
- var o2: float
-
- func _init(new_x1: float, new_x2: float, new_y1: float, new_y2: float, new_o1: float,
- new_o2: float) -> void:
- x1 = new_x1
- x2 = new_x2
- y1 = new_y1
- y2 = new_y2
- o1 = new_o1
- o2 = new_o2
-
- func compute_transform() -> Transform2D:
- return Transform2D(Vector2(x1, x2), Vector2(y1, y2), Vector2(o1, o2))
-
-class TransformTranslate extends Transform:
- var x: float
- var y: float
-
- func _init(new_x: float, new_y: float) -> void:
- x = new_x
- y = new_y
-
- func compute_transform() -> Transform2D:
- return Transform2D(Vector2.RIGHT, Vector2.DOWN, Vector2(x, y))
-
-class TransformRotate extends Transform:
- var deg: float
- var x: float
- var y: float
-
- func _init(new_deg: float, new_x: float, new_y: float) -> void:
- deg = new_deg
- x = new_x
- y = new_y
-
- func compute_transform() -> Transform2D:
- var pt := Vector2(x, y)
- return Transform2D.IDENTITY.translated(-pt).rotated(deg_to_rad(deg)).translated(pt)
-
-class TransformScale extends Transform:
- var x: float
- var y: float
-
- func _init(new_x: float, new_y: float) -> void:
- x = new_x
- y = new_y
-
- func compute_transform() -> Transform2D:
- return Transform2D(Vector2.RIGHT * x, Vector2.DOWN * y, Vector2.ZERO)
-
-class TransformSkewX extends Transform:
- var x: float
-
- func _init(new_x: float) -> void:
- x = new_x
-
- func compute_transform() -> Transform2D:
- return Transform2D(Vector2.RIGHT, Vector2(tan(deg_to_rad(x)), 1), Vector2.ZERO)
-
-class TransformSkewY extends Transform:
- var y: float
-
- func _init(new_y: float) -> void:
- y = new_y
-
- func compute_transform() -> Transform2D:
- return Transform2D(Vector2(1, tan(deg_to_rad(y))), Vector2.DOWN, Vector2.ZERO)
-
-
-var _transform_list: Array[Transform] = []
-var _final_transform := Transform2D.IDENTITY
-
-func _sync() -> void:
- _transform_list = TransformListParser.text_to_transform_list(get_value())
- _final_transform = AttributeTransform.compute_final_transform(_transform_list)
-
-func sync_after_transforms_change(sync_mode := SyncMode.LOUD) -> void:
- super.set_value(TransformListParser.transform_list_to_text(_transform_list), sync_mode)
-
-func format(text: String) -> String:
- return TransformListParser.transform_list_to_text(
- TransformListParser.text_to_transform_list(text))
-
-func set_transform_list(new_transform_list: Array[Transform],
-sync_mode := SyncMode.LOUD) -> void:
- _transform_list = new_transform_list
- _final_transform = AttributeTransform.compute_final_transform(new_transform_list)
- super.set_value(TransformListParser.transform_list_to_text(new_transform_list),
- sync_mode)
-
-func set_transform_property(idx: int, property: String, new_value: float,
-sync_mode := SyncMode.LOUD) -> void:
- if _transform_list[idx].get(property) != new_value or sync_mode == SyncMode.FINAL:
- _transform_list[idx].set(property, new_value)
- sync_after_transforms_change(sync_mode)
-
-func get_transform_list() -> Array[Transform]:
- return _transform_list
-
-func get_transform_count() -> int:
- return _transform_list.size()
-
-func get_transform(idx: int) -> Transform:
- return _transform_list[idx]
-
-func get_final_transform() -> Transform2D:
- return _final_transform
-
-
-static func compute_final_transform(transform_list: Array[Transform]) -> Transform2D:
- var final_transform := Transform2D.IDENTITY
- for t in transform_list:
- final_transform *= t.compute_transform()
- return final_transform
-
-func delete_transform(idx: int) -> void:
- _transform_list.remove_at(idx)
- sync_after_transforms_change()
-
-func insert_transform(idx: int, type: String) -> void:
- match type:
- "matrix": _transform_list.insert(idx, TransformMatrix.new(1, 0, 0, 1, 0, 0))
- "translate": _transform_list.insert(idx, TransformTranslate.new(0, 0))
- "rotate": _transform_list.insert(idx, TransformRotate.new(0, 0, 0))
- "scale": _transform_list.insert(idx, TransformScale.new(1, 1))
- "skewX": _transform_list.insert(idx, TransformSkewX.new(0))
- "skewY": _transform_list.insert(idx, TransformSkewY.new(0))
- sync_after_transforms_change()
diff --git a/src/data_classes/AttributeTransformList.gd b/src/data_classes/AttributeTransformList.gd
new file mode 100644
index 00000000..b3215734
--- /dev/null
+++ b/src/data_classes/AttributeTransformList.gd
@@ -0,0 +1,60 @@
+# An attribute representing a list of transformations.
+class_name AttributeTransformList extends Attribute
+
+var _transform_list: Array[Transform] = []
+var _final_transform := Transform2D.IDENTITY
+
+func _sync() -> void:
+ _transform_list = TransformListParser.text_to_transform_list(get_value())
+ _final_transform = compute_final_transform(_transform_list)
+
+func sync_after_transforms_change(save := true) -> void:
+ super.set_value(TransformListParser.transform_list_to_text(_transform_list), save)
+
+func format(text: String) -> String:
+ return TransformListParser.transform_list_to_text(
+ TransformListParser.text_to_transform_list(text))
+
+func set_transform_list(new_transform_list: Array[Transform], save := true) -> void:
+ _transform_list = new_transform_list
+ _final_transform = compute_final_transform(new_transform_list)
+ super.set_value(TransformListParser.transform_list_to_text(new_transform_list), save)
+
+func set_transform_property(idx: int, property: String, new_value: float,
+save := true) -> void:
+ if _transform_list[idx].get(property) != new_value or save:
+ _transform_list[idx].set(property, new_value)
+ sync_after_transforms_change(save)
+
+func get_transform_list() -> Array[Transform]:
+ return _transform_list
+
+func get_transform_count() -> int:
+ return _transform_list.size()
+
+func get_transform(idx: int) -> Transform:
+ return _transform_list[idx]
+
+func get_final_transform() -> Transform2D:
+ return _final_transform
+
+
+static func compute_final_transform(transform_list: Array[Transform]) -> Transform2D:
+ var final_transform := Transform2D.IDENTITY
+ for t in transform_list:
+ final_transform *= t.compute_transform()
+ return final_transform
+
+func delete_transform(idx: int) -> void:
+ _transform_list.remove_at(idx)
+ sync_after_transforms_change()
+
+func insert_transform(idx: int, type: String) -> void:
+ match type:
+ "matrix": _transform_list.insert(idx, Transform.TransformMatrix.new(1, 0, 0, 1, 0, 0))
+ "translate": _transform_list.insert(idx, Transform.TransformTranslate.new(0, 0))
+ "rotate": _transform_list.insert(idx, Transform.TransformRotate.new(0, 0, 0))
+ "scale": _transform_list.insert(idx, Transform.TransformScale.new(1, 1))
+ "skewX": _transform_list.insert(idx, Transform.TransformSkewX.new(0))
+ "skewY": _transform_list.insert(idx, Transform.TransformSkewY.new(0))
+ sync_after_transforms_change()
diff --git a/src/data_classes/Tag.gd b/src/data_classes/Tag.gd
index e266bbb4..f7ed8a49 100644
--- a/src/data_classes/Tag.gd
+++ b/src/data_classes/Tag.gd
@@ -1,54 +1,149 @@
# An SVG tag, standalone () or container ().
-class_name Tag extends RefCounted
+class_name Tag extends XNode
-var child_tags: Array[Tag]
+signal attribute_changed(name: String)
+signal ancestor_attribute_changed(name: String)
-signal attribute_changed(undo_redo: bool)
+var parent: Tag = null
+var svg: TagSVG = null
+var root: TagRoot = null
+var child_tags: Array[Tag]
var attributes: Dictionary # Dictionary{String: Attribute}
-# Attributes that aren't recognized (usually because GodSVG doesn't support them).
-var unknown_attributes: Array[Attribute]
+func _init() -> void:
+ attribute_changed.connect(_on_attribute_changed)
+ ancestor_attribute_changed.connect(_on_ancestor_attribute_changed)
+
+func _on_attribute_changed(attribute_name: String) -> void:
+ for child_tag in child_tags:
+ child_tag.ancestor_attribute_changed.emit(attribute_name)
+ if root != null:
+ root.attribute_somewhere_changed.emit(xid)
+
+func _on_ancestor_attribute_changed(attribute_name: String) -> void:
+ for child_tag in child_tags:
+ child_tag.ancestor_attribute_changed.emit(attribute_name)
+
+func _notification(what: int) -> void:
+ if what == NOTIFICATION_POSTINITIALIZE:
+ root.attribute_somewhere_changed.connect(update_cache.unbind(1))
+ update_cache()
func is_standalone() -> bool:
return child_tags.is_empty()
-func _init() -> void:
- for attribute: Attribute in attributes.values():
- attribute.propagate_value_changed.connect(emit_attribute_changed)
+func get_child_count() -> int:
+ return child_tags.size()
func user_setup(_what = null) -> void:
- printerr("Unimplemented function")
- breakpoint
+ return
-func set_unknown_attributes(attribs: Array[Attribute]) -> void:
- unknown_attributes = attribs.duplicate()
- for attribute in unknown_attributes:
- attribute.propagate_value_changed.connect(emit_attribute_changed)
-func emit_attribute_changed(undo_redo: bool) -> void:
- attribute_changed.emit(undo_redo)
+func get_attribute(attribute_name: String) -> Attribute:
+ if attributes.has(attribute_name):
+ return attributes[attribute_name]
+ elif DB.propagated_attributes.has(attribute_name):
+ if is_parent_g():
+ return parent.get_attribute(attribute_name)
+ elif svg != null:
+ return svg.get_attribute(attribute_name)
+ return DB.attribute(attribute_name)
-func get_child_count() -> int:
- return child_tags.size()
+
+func get_attribute_value(attribute_name: String, only_self := true) -> String:
+ if only_self and not attributes.has(attribute_name):
+ return ""
+ return get_attribute(attribute_name).get_value()
+
+func get_attribute_num(attribute_name: String) -> float:
+ if DB.get_attribute_type(attribute_name) != DB.AttributeType.NUMERIC:
+ push_error("Attribute not the correct type.")
+ return get_attribute(attribute_name).get_num()
+
+func get_attribute_rect(attribute_name: String) -> float:
+ if DB.get_attribute_type(attribute_name) != DB.AttributeType.LIST:
+ push_error("Attribute not the correct type.")
+ return get_attribute(attribute_name).get_rect()
+
+func get_attribute_list(attribute_name: String) -> PackedFloat32Array:
+ if DB.get_attribute_type(attribute_name) != DB.AttributeType.LIST:
+ push_error("Attribute not the correct type.")
+ return get_attribute(attribute_name).get_list()
+
+func get_attribute_commands(attribute_name: String) -> Array[PathCommand]:
+ if DB.get_attribute_type(attribute_name) != DB.AttributeType.PATHDATA:
+ push_error("Attribute not the correct type.")
+ return get_attribute(attribute_name).get_commands()
+
+func get_attribute_transforms(attribute_name: String) -> Array[Transform]:
+ if DB.get_attribute_type(attribute_name) != DB.AttributeType.TRANSFORM_LIST:
+ push_error("Attribute not the correct type.")
+ return get_attribute(attribute_name).get_transform_list()
+
+func get_attribute_final_transform(attribute_name: String) -> Transform2D:
+ if DB.get_attribute_type(attribute_name) != DB.AttributeType.TRANSFORM_LIST:
+ push_error("Attribute not the correct type.")
+ return get_attribute(attribute_name).get_final_transform()
+
+
+func set_attribute(attribute_name: String, value: Variant, save := true) -> void:
+ var has_attribute := attributes.has(attribute_name)
+
+ var attrib: Attribute
+ if has_attribute:
+ attrib = get_attribute(attribute_name)
+ else:
+ attrib = DB.attribute(attribute_name)
+ attrib.value_changed.connect(update_cache.unbind(2))
+ attrib.value_changed.connect(attribute_changed.emit.bind(attribute_name).unbind(2))
+
+ var value_type := typeof(value)
+
+ if value_type == TYPE_STRING:
+ attrib.set_value(value, save)
+ else:
+ match DB.get_attribute_type(attribute_name):
+ DB.AttributeType.NUMERIC:
+ if value_type in [TYPE_FLOAT, TYPE_INT]: attrib.set_num(value, save)
+ else: push_error("Invalid value set to attribute.")
+ DB.AttributeType.LIST:
+ if value_type in [TYPE_RECT2, TYPE_RECT2I]: attrib.set_rect(value, save)
+ elif value_type == TYPE_PACKED_FLOAT32_ARRAY: attrib.set_list(value, save)
+ else: push_error("Invalid value set to attribute.")
+ DB.AttributeType.PATHDATA:
+ if value_type == TYPE_ARRAY: attrib.set_commands(value, save)
+ else: push_error("Invalid value set to attribute.")
+ DB.AttributeType.TRANSFORM_LIST:
+ if value_type == TYPE_ARRAY: attrib.set_commands(value, save)
+ else: push_error("Invalid value set to attribute.")
+ _: push_error("Invalid value set to attribute.")
+
+ if has_attribute and attrib.get_value().is_empty():
+ attributes.erase(attribute_name)
+ elif not has_attribute and not attrib.get_value().is_empty():
+ attributes[attribute_name] = attrib
+
+
+func get_default(attribute_name: String) -> String:
+ if attribute_name in DB.propagated_attributes:
+ if is_parent_g():
+ return parent.get_default(attribute_name)
+ elif svg != null:
+ return svg.get_default(attribute_name)
+ return get_own_default(attribute_name)
# Why is there no way to duplicate RefCounteds, again?
func duplicate(include_children := true) -> Tag:
var type: GDScript = get_script()
var new_tag: Tag
- if type == TagUnknown:
- new_tag = TagUnknown.new(self.name)
+ if type == TagUnrecognized:
+ new_tag = TagUnrecognized.new(self.name)
else:
new_tag = type.new()
- for attribute in new_tag.attributes:
- new_tag.attributes[attribute].set_value(attributes[attribute].get_value())
- var unknown_attributes_array: Array[Attribute] = []
- for attribute in unknown_attributes:
- var new_attrib := Attribute.new(attribute.name)
- new_attrib.set_value(attribute.get_value())
- unknown_attributes_array.append(new_attrib)
- new_tag.set_unknown_attributes(unknown_attributes_array)
+ for attribute in attributes:
+ new_tag.set_attribute(attribute, attributes[attribute].get_value())
if include_children:
# Iterate this over all children.
@@ -60,8 +155,34 @@ func duplicate(include_children := true) -> Tag:
# To be overridden in extending classes.
+func get_own_default(_attribute_name: String) -> String:
+ return ""
+
+func update_cache() -> void:
+ pass
+
func can_replace(_new_tag: String) -> bool:
return false
func get_replacement(_new_tag: String) -> Tag:
return null
+
+func get_config_warnings() -> PackedStringArray:
+ return PackedStringArray()
+
+# Helpers
+func is_parent_g() -> bool:
+ return parent != null and parent is TagG
+
+func set_parent(tag: Tag) -> void:
+ parent = tag
+ svg = parent if parent is TagSVG else tag.svg
+ root = tag.root
+
+func get_transform() -> Transform2D:
+ var result := Transform2D.IDENTITY
+ if is_parent_g():
+ result *= parent.get_transform()
+ if attributes.has("transform"):
+ result *= attributes.transform.get_final_transform()
+ return result
diff --git a/src/data_classes/TagCircle.gd b/src/data_classes/TagCircle.gd
index 20d29d7e..523ea8ee 100644
--- a/src/data_classes/TagCircle.gd
+++ b/src/data_classes/TagCircle.gd
@@ -1,23 +1,14 @@
# A tag.
-class_name TagCircle extends Tag
+class_name TagCircle extends TagShape
const name = "circle"
const possible_conversions = ["ellipse", "rect", "path"]
-const icon = preload("res://visual/icons/tag/circle.svg")
-
-const known_attributes = ["transform", "opacity", "fill", "fill-opacity",
- "stroke", "stroke-opacity", "stroke-width", "cx", "cy", "r"]
-
-func _init() -> void:
- for attrib_name in known_attributes:
- attributes[attrib_name] = DB.attribute(attrib_name)
- super()
func user_setup(pos := Vector2.ZERO) -> void:
- attributes.r.set_num(1.0)
+ set_attribute("r", 1.0)
if pos != Vector2.ZERO:
- attributes.cx.set_num(pos.x)
- attributes.cy.set_num(pos.y)
+ set_attribute("cx", pos.x)
+ set_attribute("cy", pos.y)
func can_replace(new_tag: String) -> bool:
return new_tag in ["ellipse", "rect", "path"]
@@ -27,44 +18,54 @@ func get_replacement(new_tag: String) -> Tag:
return null
var tag: Tag
- var retained_attributes: Array[String] = []
+ var dropped_attributes: PackedStringArray
match new_tag:
"ellipse":
tag = TagEllipse.new()
- retained_attributes = ["cx", "cy", "transform", "opacity", "fill",
- "fill-opacity", "stroke", "stroke-opacity", "stroke-width"]
- tag.attributes.rx.set_num(attributes.r.get_num(), Attribute.SyncMode.SILENT)
- tag.attributes.ry.set_num(attributes.r.get_num(), Attribute.SyncMode.SILENT)
+ dropped_attributes = PackedStringArray(["r"])
+ tag.set_attribute("rx", get_attribute_num("r"))
+ tag.set_attribute("ry", get_attribute_num("r"))
"rect":
tag = TagRect.new()
- retained_attributes = ["transform", "opacity", "fill", "fill-opacity", "stroke",
- "stroke-opacity", "stroke-width"]
- tag.attributes.x.set_num(attributes.cx.get_num() - attributes.r.get_num(),
- Attribute.SyncMode.SILENT)
- tag.attributes.y.set_num(attributes.cy.get_num() - attributes.r.get_num(),
- Attribute.SyncMode.SILENT)
- tag.attributes.width.set_num(attributes.r.get_num() * 2,
- Attribute.SyncMode.SILENT)
- tag.attributes.height.set_num(attributes.r.get_num() * 2,
- Attribute.SyncMode.SILENT)
- tag.attributes.rx.set_num(attributes.r.get_num(), Attribute.SyncMode.SILENT)
- tag.attributes.ry.set_num(attributes.r.get_num(), Attribute.SyncMode.SILENT)
+ dropped_attributes = PackedStringArray(["r", "cx", "cy"])
+ tag.set_attribute("x", get_attribute_num("cx") - get_attribute_num("r"))
+ tag.set_attribute("y", get_attribute_num("cy") - get_attribute_num("r"))
+ tag.set_attribute("width", get_attribute_num("r"))
+ tag.set_attribute("height", get_attribute_num("r"))
+ tag.set_attribute("rx", get_attribute_num("r"))
+ tag.set_attribute("ry", get_attribute_num("r"))
"path":
tag = TagPath.new()
- retained_attributes = ["transform", "opacity", "fill", "fill-opacity", "stroke",
- "stroke-opacity", "stroke-width"]
+ dropped_attributes = PackedStringArray(["r", "cx", "cy"])
var commands: Array[PathCommand] = []
- commands.append(PathCommand.MoveCommand.new(attributes.cx.get_num(),
- attributes.cy.get_num() - attributes.r.get_num(), true))
- commands.append(PathCommand.EllipticalArcCommand.new(attributes.r.get_num(),
- attributes.r.get_num(), 0, 0, 0, 0, attributes.r.get_num() * 2, true))
- commands.append(PathCommand.EllipticalArcCommand.new(attributes.r.get_num(),
- attributes.r.get_num(), 0, 0, 0, 0, -attributes.r.get_num() * 2, true))
+ commands.append(PathCommand.MoveCommand.new(get_attribute_num("cx"),
+ get_attribute_num("cy") - get_attribute_num("r"), true))
+ commands.append(PathCommand.EllipticalArcCommand.new(get_attribute_num("r"),
+ get_attribute_num("r"), 0, 0, 0, 0, get_attribute_num("r") * 2, true))
+ commands.append(PathCommand.EllipticalArcCommand.new(get_attribute_num("r"),
+ get_attribute_num("r"), 0, 0, 0, 0, -get_attribute_num("r") * 2, true))
commands.append(PathCommand.CloseCommand.new(true))
- tag.attributes.d.set_commands(commands, Attribute.SyncMode.SILENT)
+ tag.set_attribute("d", commands)
- for k in retained_attributes:
- tag.attributes[k].set_value(attributes[k].get_value(), Attribute.SyncMode.SILENT)
- tag.child_tags = child_tags
+ for attribute_name in attributes:
+ if not attribute_name in dropped_attributes:
+ tag.set_attribute(attribute_name, attributes[attribute_name])
+ tag.child_tags = child_tags
return tag
+
+
+func get_own_default(attribute_name: String) -> String:
+ match attribute_name:
+ "cx", "cy": return "0"
+ "r": return "0"
+ "opacity": return "1"
+ _: return ""
+
+func get_bounding_box() -> Rect2:
+ var bounding_box := Rect2()
+ bounding_box.position.x = get_attribute_num("cx") - get_attribute_num("r")
+ bounding_box.position.y = get_attribute_num("cy") - get_attribute_num("r")
+ bounding_box.size.x = get_attribute_num("r") * 2
+ bounding_box.size.y = get_attribute_num("r") * 2
+ return bounding_box
diff --git a/src/data_classes/TagEllipse.gd b/src/data_classes/TagEllipse.gd
index 1c8084df..f148080f 100644
--- a/src/data_classes/TagEllipse.gd
+++ b/src/data_classes/TagEllipse.gd
@@ -1,28 +1,22 @@
# An tag.
-class_name TagEllipse extends Tag
+class_name TagEllipse extends TagShape
+
+var rx: float
+var ry: float
const name = "ellipse"
const possible_conversions = ["circle", "rect", "path"]
-const icon = preload("res://visual/icons/tag/ellipse.svg")
-
-const known_attributes = ["transform", "opacity", "fill", "fill-opacity",
- "stroke", "stroke-opacity", "stroke-width", "cx", "cy", "rx", "ry"]
-
-func _init() -> void:
- for attrib_name in known_attributes:
- attributes[attrib_name] = DB.attribute(attrib_name)
- super()
func user_setup(pos := Vector2.ZERO) -> void:
- attributes.rx.set_num(1.0)
- attributes.ry.set_num(1.0)
+ set_attribute("rx", 1.0)
+ set_attribute("ry", 1.0)
if pos != Vector2.ZERO:
- attributes.cx.set_num(pos.x)
- attributes.cy.set_num(pos.y)
+ set_attribute("cx", pos.x)
+ set_attribute("cy", pos.y)
func can_replace(new_tag: String) -> bool:
if new_tag == "circle":
- return attributes.rx.get_num() == attributes.ry.get_num()
+ return get_attribute_num("rx") == get_attribute_num("ry")
else:
return new_tag in ["rect", "path"]
@@ -31,41 +25,54 @@ func get_replacement(new_tag: String) -> Tag:
return null
var tag: Tag
- var retained_attributes: Array[String] = []
+ var dropped_attributes: PackedStringArray
match new_tag:
"circle":
tag = TagCircle.new()
- retained_attributes = ["cx", "cy", "transform", "opacity", "fill",
- "fill-opacity", "stroke", "stroke-opacity", "stroke-width"]
- tag.attributes.r.set_num(attributes.rx.get_num(), Attribute.SyncMode.SILENT)
+ dropped_attributes = PackedStringArray(["rx", "ry"])
+ tag.set_attribute("r", get_attribute_num("rx"))
"rect":
tag = TagRect.new()
- retained_attributes = ["rx", "ry", "transform", "opacity", "fill",
- "fill-opacity", "stroke", "stroke-opacity", "stroke-width"]
- tag.attributes.x.set_num(attributes.cx.get_num() - attributes.rx.get_num(),
- Attribute.SyncMode.SILENT)
- tag.attributes.y.set_num(attributes.cy.get_num() - attributes.ry.get_num(),
- Attribute.SyncMode.SILENT)
- tag.attributes.width.set_num(attributes.rx.get_num() * 2,
- Attribute.SyncMode.SILENT)
- tag.attributes.height.set_num(attributes.ry.get_num() * 2,
- Attribute.SyncMode.SILENT)
+ dropped_attributes = PackedStringArray(["cx", "cy"])
+ tag.set_attribute("x", get_attribute_num("cx") - get_attribute_num("rx"))
+ tag.set_attribute("y", get_attribute_num("cy") - get_attribute_num("ry"))
+ tag.set_attribute("width", get_attribute_num("rx") * 2)
+ tag.set_attribute("height", get_attribute_num("ry") * 2)
"path":
tag = TagPath.new()
- retained_attributes = ["transform", "opacity", "fill",
- "fill-opacity", "stroke", "stroke-opacity", "stroke-width"]
+ dropped_attributes = PackedStringArray(["cx", "cy", "rx", "ry"])
var commands: Array[PathCommand] = []
- commands.append(PathCommand.MoveCommand.new(attributes.cx.get_num(),
- attributes.cy.get_num() - attributes.ry.get_num(), true))
- commands.append(PathCommand.EllipticalArcCommand.new(attributes.rx.get_num(),
- attributes.ry.get_num(), 0, 0, 0, 0, attributes.ry.get_num() * 2, true))
- commands.append(PathCommand.EllipticalArcCommand.new(attributes.rx.get_num(),
- attributes.ry.get_num(), 0, 0, 0, 0, -attributes.ry.get_num() * 2, true))
+ commands.append(PathCommand.MoveCommand.new(get_attribute_num("cx"),
+ get_attribute_num("cy") - ry, true))
+ commands.append(PathCommand.EllipticalArcCommand.new(rx, ry, 0, 0, 0, 0,
+ ry * 2, true))
+ commands.append(PathCommand.EllipticalArcCommand.new(rx, ry, 0, 0, 0, 0,
+ -ry * 2, true))
commands.append(PathCommand.CloseCommand.new(true))
- tag.attributes.d.set_commands(commands, Attribute.SyncMode.SILENT)
+ tag.set_attribute("d", commands)
- for k in retained_attributes:
- tag.attributes[k].set_value(attributes[k].get_value(), Attribute.SyncMode.SILENT)
- tag.child_tags = child_tags
+ for attribute_name in attributes:
+ if not attribute_name in dropped_attributes:
+ tag.set_attribute(attribute_name, attributes[attribute_name])
+ tag.child_tags = child_tags
return tag
+
+func update_cache() -> void:
+ rx = get_attribute_num("rx") if attributes.has("rx") else get_attribute_num("ry")
+ ry = get_attribute_num("ry") if attributes.has("ry") else get_attribute_num("rx")
+
+func get_own_default(attribute_name: String) -> String:
+ match attribute_name:
+ "cx", "cy": return "0"
+ "rx": return "auto"
+ "ry": return "auto"
+ "opacity": return "1"
+ _: return ""
+
+func get_bounding_box() -> Rect2:
+ var bounding_box := Rect2()
+ bounding_box.position.x = get_attribute_num("cx") - rx
+ bounding_box.position.y = get_attribute_num("cy") - ry
+ bounding_box.size = Vector2(rx, ry) * 2
+ return bounding_box
diff --git a/src/data_classes/TagG.gd b/src/data_classes/TagG.gd
new file mode 100644
index 00000000..75cd9afb
--- /dev/null
+++ b/src/data_classes/TagG.gd
@@ -0,0 +1,16 @@
+class_name TagG extends Tag
+
+const name = "g"
+const possible_conversions = []
+
+func get_own_default(attribute_name: String) -> String:
+ match attribute_name:
+ "opacity": return "1"
+ _: return ""
+
+func get_config_warnings() -> PackedStringArray:
+ var warnings := super()
+ if not (parent is TagG or parent is TagSVG or parent is TagUnrecognized):
+ warnings.append(TranslationServer.translate("{tag} must be a child of {allowed} to have any effect.").format(
+ {"tag": self.name, "allowed": "[svg, g]"}))
+ return warnings
diff --git a/src/data_classes/TagLine.gd b/src/data_classes/TagLine.gd
index bde59192..1bd56d11 100644
--- a/src/data_classes/TagLine.gd
+++ b/src/data_classes/TagLine.gd
@@ -1,25 +1,16 @@
# A tag.
-class_name TagLine extends Tag
+class_name TagLine extends TagShape
const name = "line"
const possible_conversions = ["path"]
-const icon = preload("res://visual/icons/tag/line.svg")
-
-const known_attributes = ["transform", "opacity", "stroke", "stroke-opacity",
- "stroke-width", "stroke-linecap", "x1", "y1", "x2", "y2", ]
-
-func _init() -> void:
- for attrib_name in known_attributes:
- attributes[attrib_name] = DB.attribute(attrib_name)
- super()
func user_setup(pos := Vector2.ZERO) -> void:
if pos != Vector2.ZERO:
- attributes.x1.set_num(pos.x)
- attributes.y1.set_num(pos.y)
- attributes.y2.set_num(pos.y)
- attributes.x2.set_num(pos.x + 1)
- attributes.stroke.set_value("black")
+ set_attribute("x1", pos.x)
+ set_attribute("y1", pos.y)
+ set_attribute("y2", pos.y)
+ set_attribute("x2", pos.x + 1)
+ set_attribute("stroke", "black")
func can_replace(new_tag: String) -> bool:
return new_tag == "path"
@@ -29,22 +20,37 @@ func get_replacement(new_tag: String) -> Tag:
return null
var tag: Tag
- var retained_attributes: Array[String] = []
+ var dropped_attributes: PackedStringArray
match new_tag:
"path":
tag = TagPath.new()
- retained_attributes = ["transform", "opacity", "stroke", "stroke-opacity",
- "stroke-width", "stroke-linecap"]
+ dropped_attributes = PackedStringArray(["x1", "y1", "x2", "y2"])
var commands: Array[PathCommand] = []
- commands.append(PathCommand.MoveCommand.new(attributes.x1.get_num(),
- attributes.y1.get_num(), true))
+ commands.append(PathCommand.MoveCommand.new(get_attribute_num("x1"),
+ get_attribute_num("y1"), true))
commands.append(PathCommand.LineCommand.new(
- attributes.x2.get_num() - attributes.x1.get_num(),
- attributes.y2.get_num() - attributes.y1.get_num(), true))
- tag.attributes.d.set_commands(commands, Attribute.SyncMode.SILENT)
+ get_attribute_num("x2") - get_attribute_num("x1"),
+ get_attribute_num("y2") - get_attribute_num("y1"), true))
+ tag.set_attribute("d", commands)
- for k in retained_attributes:
- tag.attributes[k].set_value(attributes[k].get_value(), Attribute.SyncMode.SILENT)
- tag.child_tags = child_tags
+ for attribute_name in attributes:
+ if not attribute_name in dropped_attributes:
+ tag.set_attribute(attribute_name, attributes[attribute_name])
+ tag.child_tags = child_tags
return tag
+
+
+func get_own_default(attribute_name: String) -> String:
+ match attribute_name:
+ "x1", "y1", "x2", "y2": return "0"
+ "opacity": return "1"
+ _: return ""
+
+func get_bounding_box() -> Rect2:
+ var bounding_box := Rect2()
+ bounding_box.position.x = minf(get_attribute_num("x1"), get_attribute_num("x2"))
+ bounding_box.position.y = minf(get_attribute_num("y1"), get_attribute_num("y2"))
+ bounding_box.end.x = maxf(get_attribute_num("y1"), get_attribute_num("y2"))
+ bounding_box.end.y = maxf(get_attribute_num("y1"), get_attribute_num("y2"))
+ return bounding_box
diff --git a/src/data_classes/TagLinearGradient.gd b/src/data_classes/TagLinearGradient.gd
new file mode 100644
index 00000000..b9bbf376
--- /dev/null
+++ b/src/data_classes/TagLinearGradient.gd
@@ -0,0 +1,28 @@
+class_name TagLinearGradient extends Tag
+
+const name = "linearGradient"
+const possible_conversions = []
+
+func get_own_default(attribute_name: String) -> String:
+ match attribute_name:
+ "x1", "y1": return "0%"
+ "x2", "y2": return "100%"
+ _: return ""
+
+func get_config_warnings() -> PackedStringArray:
+ var warnings := super()
+ if not (parent is TagG or parent is TagSVG or parent is TagUnrecognized):
+ warnings.append(TranslationServer.translate("{tag} must be a child of {allowed} to have any effect.").format(
+ {"tag": name, "allowed": "[g, svg]"}))
+ if not attributes.has("id"):
+ warnings.append(TranslationServer.translate("No id attribute defined."))
+
+ var has_stops := false
+ for child in child_tags:
+ if child is TagStop:
+ has_stops = true
+ break
+ if not has_stops:
+ warnings.append(TranslationServer.translate("No stop tags under this gradient."))
+
+ return warnings
diff --git a/src/data_classes/TagPath.gd b/src/data_classes/TagPath.gd
index 62622745..98bbba7d 100644
--- a/src/data_classes/TagPath.gd
+++ b/src/data_classes/TagPath.gd
@@ -1,30 +1,189 @@
# A tag.
-class_name TagPath extends Tag
+class_name TagPath extends TagShape
const name = "path"
const possible_conversions = []
-const icon = preload("res://visual/icons/tag/path.svg")
-
-const known_attributes = ["transform", "opacity", "fill", "fill-opacity",
- "stroke", "stroke-opacity", "stroke-width", "stroke-linecap",
- "stroke-linejoin", "d"]
-
-func _init() -> void:
- for attrib_name in known_attributes:
- attributes[attrib_name] = DB.attribute(attrib_name)
- super()
func user_setup(pos := Vector2.ZERO) -> void:
if pos != Vector2.ZERO:
- attributes.d.insert_command(0, "M")
- attributes.d.set_command_property(0, "x", pos.x)
- attributes.d.set_command_property(0, "y", pos.y)
+ var attrib := get_attribute("d")
+ attrib.insert_command(0, "M")
+ attrib.set_command_property(0, "x", pos.x)
+ attrib.set_command_property(0, "y", pos.y)
-func can_replace(_new_tag: String) -> bool:
- return false
+func get_own_default(attribute_name: String) -> String:
+ if attribute_name == "opacity":
+ return "1"
+ return ""
-func get_replacement(new_tag: String) -> Tag:
- if not can_replace(new_tag):
- return null
+func get_bounding_box() -> Rect2:
+ if not attributes.has("d"):
+ return Rect2()
+
+ var pathdata: AttributePathdata = get_attribute("d")
+ var min_x := INF
+ var min_y := INF
+ var max_x := -INF
+ var max_y := -INF
- return null
+ for cmd_idx in pathdata.get_command_count():
+ var cmd := pathdata.get_command(cmd_idx)
+ var relative := cmd.relative
+ var cmd_char := cmd.command_char.to_upper()
+ match cmd_char:
+ "M", "L":
+ # Move / Line
+ var v := Vector2(cmd.x, cmd.y)
+ var end := cmd.start + v if relative else v
+ min_x = minf(min_x, end.x)
+ min_y = minf(min_y, end.y)
+ max_x = maxf(max_x, end.x)
+ max_y = maxf(max_y, end.y)
+ "H":
+ # Horizontal line
+ var v := Vector2(cmd.x, 0)
+ var end := cmd.start + v if relative else v
+ min_x = minf(min_x, end.x)
+ max_x = maxf(max_x, end.x)
+ "V":
+ # Vertical line
+ var v := Vector2(0, cmd.y)
+ var end := cmd.start + v if relative else v
+ min_y = minf(min_y, end.y)
+ max_y = maxf(max_y, end.y)
+ "C", "S":
+ # Cubic Bezier curve
+ var v := Vector2(cmd.x, cmd.y)
+ var v1 := Vector2(cmd.x1, cmd.y1) if cmd_char == "C" else\
+ pathdata.get_implied_S_control(cmd_idx)
+ var v2 := Vector2(cmd.x2, cmd.y2)
+ var cp1 := cmd.start
+ var cp4 := cp1 + v if relative else v
+ var cp2 := cp1 + v1 if relative else v1
+ var cp3 := cp1 + v2 if relative else v2
+
+ min_x = minf(min_x, cp4.x)
+ min_y = minf(min_y, cp4.y)
+ max_x = maxf(max_x, cp4.x)
+ max_y = maxf(max_y, cp4.y)
+
+ var i := cp2 - cp1
+ var j := cp3 - cp2
+ var k := cp4 - cp3
+
+ var a := 3 * i - 6 * j + 3 * k
+ var b := 6 * j - 6 * i
+ var c := 3 * i
+
+ var sol_x = solve_quadratic(a.x, b.x, c.x)
+ for sol in sol_x:
+ if sol != null and sol > 0 and sol < 1:
+ var pt := Utils.cubic_bezier_point(cp1.x, cp2.x, cp3.x, cp4.x, sol)
+ min_x = minf(pt, min_x)
+ max_x = maxf(pt, max_x)
+
+ var sol_y = solve_quadratic(a.y, b.y, c.y)
+ for sol in sol_y:
+ if sol != null and sol > 0 and sol < 1:
+ var pt := Utils.cubic_bezier_point(cp1.y, cp2.y, cp3.y, cp4.y, sol)
+ min_y = minf(pt, min_y)
+ max_y = maxf(pt, max_y)
+ "Q", "T":
+ # Quadratic Bezier curve
+ var v := Vector2(cmd.x, cmd.y)
+ var v1 := Vector2(cmd.x1, cmd.y1) if cmd_char == "Q" else\
+ pathdata.get_implied_T_control(cmd_idx)
+
+ var cp1 := cmd.start
+ var cp2 := cp1 + v1 if relative else v1
+ var cp3 := cp1 + v if relative else v
+
+ min_x = minf(min_x, cp3.x)
+ min_y = minf(min_y, cp3.y)
+ max_x = maxf(max_x, cp3.x)
+ max_y = maxf(max_y, cp3.y)
+
+ var t_x := (cp1.x - cp2.x) / (cp1.x - 2 * cp2.x + cp3.x)
+ if 0 <= t_x and t_x <= 1:
+ var x_extrema := Utils.quadratic_bezier_point(cp1.x, cp2.x, cp3.x, t_x)
+ min_x = minf(min_x, x_extrema)
+ max_x = maxf(max_x, x_extrema)
+
+ var t_y := (cp1.y - cp2.y) / (cp1.y - 2 * cp2.y + cp3.y)
+ if 0 <= t_y and t_y <= 1:
+ var y_extrema := Utils.quadratic_bezier_point(cp1.y, cp2.y, cp3.y, t_y)
+ min_y = minf(min_y, y_extrema)
+ max_y = maxf(max_y, y_extrema)
+ "A":
+ # Elliptical arc.
+ var start := cmd.start
+ var v := Vector2(cmd.x, cmd.y)
+ var end := start + v if relative else v
+
+ min_x = minf(min_x, end.x)
+ min_y = minf(min_y, end.y)
+ max_x = maxf(max_x, end.x)
+ max_y = maxf(max_y, end.y)
+
+ if start == end or cmd.rx == 0 or cmd.ry == 0:
+ continue
+
+ var r := Vector2(cmd.rx, cmd.ry).abs()
+ # Obtain center parametrization.
+ var rot := deg_to_rad(cmd.rot)
+ var cosine := cos(rot)
+ var sine := sin(rot)
+ var half := (start - end) / 2
+ var x1 := half.x * cosine + half.y * sine
+ var y1 := -half.x * sine + half.y * cosine
+ var r2 := Vector2(r.x * r.x, r.y * r.y)
+ var x12 := x1 * x1
+ var y12 := y1 * y1
+ var cr := x12 / r2.x + y12 / r2.y
+ if cr > 1:
+ cr = sqrt(cr)
+ r *= cr
+ r2 = Vector2(r.x * r.x, r.y * r.y)
+
+ var dq := r2.x * y12 + r2.y * x12
+ var pq := (r2.x * r2.y - dq) / dq
+ var sc := sqrt(maxf(0, pq))
+ if cmd.large_arc_flag == cmd.sweep_flag:
+ sc = -sc
+
+ var ct := Vector2(r.x * sc * y1 / r.y, -r.y * sc * x1 / r.x)
+ var c := Vector2(ct.x * cosine - ct.y * sine,
+ ct.x * sine + ct.y * cosine) + start.lerp(end, 0.5)
+ var tv := Vector2(x1 - ct.x, y1 - ct.y) / r
+ var theta1 := tv.angle()
+ var delta_theta := fposmod(tv.angle_to(
+ Vector2(-x1 - ct.x, -y1 - ct.y) / r), TAU)
+ if cmd.sweep_flag == 0:
+ theta1 += delta_theta
+ delta_theta = TAU - delta_theta
+ theta1 = fposmod(theta1, TAU)
+
+ var rot_h := tan(rot)
+ var extreme1 := atan2(-r.y * rot_h, r.x)
+ var extreme2 := atan2(r.y, r.x * rot_h)
+ for angle in [extreme1, extreme2, PI + extreme1, PI + extreme2]:
+ if (angle < theta1 or angle > theta1 + delta_theta) and\
+ (angle + TAU < theta1 or angle + TAU > theta1 + delta_theta):
+ continue
+ var extreme_point := Vector2(
+ c.x + r.x * cos(angle) * cosine - r.y * sin(angle) * sine,
+ c.y + r.x * cos(angle) * sine + r.y * sin(angle) * cosine)
+ min_x = minf(min_x, extreme_point.x)
+ min_y = minf(min_y, extreme_point.y)
+ max_x = maxf(max_x, extreme_point.x)
+ max_y = maxf(max_y, extreme_point.y)
+
+ return Rect2(min_x, min_y, max_x - min_x, max_y - min_y)
+
+
+static func solve_quadratic(a: float, b: float, c: float) -> Array:
+ var D = sqrt(b * b - 4 * a * c)
+ if is_nan(D):
+ return [null, null]
+ else:
+ return [(-b + D) / (2 * a), (-b - D) / (2 * a)]
diff --git a/src/data_classes/TagRadialGradient.gd b/src/data_classes/TagRadialGradient.gd
new file mode 100644
index 00000000..78abff9b
--- /dev/null
+++ b/src/data_classes/TagRadialGradient.gd
@@ -0,0 +1,27 @@
+class_name TagRadialGradient extends Tag
+
+const name = "radialGradient"
+const possible_conversions = []
+
+func get_own_default(attribute_name: String) -> String:
+ match attribute_name:
+ "cx", "cy", "r": return "50%"
+ _: return ""
+
+func get_config_warnings() -> PackedStringArray:
+ var warnings := super()
+ if not (parent is TagG or parent is TagSVG or parent is TagUnrecognized):
+ warnings.append(TranslationServer.translate("{tag} must be a child of {allowed} to have any effect.").format(
+ {"tag": name, "allowed": "[g, svg]"}))
+ if not attributes.has("id"):
+ warnings.append(TranslationServer.translate("No id attribute defined."))
+
+ var has_stops := false
+ for child in child_tags:
+ if child is TagStop:
+ has_stops = true
+ break
+ if not has_stops:
+ warnings.append(TranslationServer.translate("No stop tags under this gradient."))
+
+ return warnings
diff --git a/src/data_classes/TagRect.gd b/src/data_classes/TagRect.gd
index 754db6a4..93587432 100644
--- a/src/data_classes/TagRect.gd
+++ b/src/data_classes/TagRect.gd
@@ -1,44 +1,25 @@
# A tag.
-class_name TagRect extends Tag
+class_name TagRect extends TagShape
+
+var rx: float
+var ry: float
const name = "rect"
const possible_conversions = ["circle", "ellipse", "path"]
-const icon = preload("res://visual/icons/tag/rect.svg")
-
-const known_attributes = ["transform", "opacity", "fill", "fill-opacity",
- "stroke", "stroke-opacity", "stroke-width", "stroke-linejoin",
- "x", "y", "width", "height", "rx", "ry",]
-
-func _init() -> void:
- for attrib_name in known_attributes:
- attributes[attrib_name] = DB.attribute(attrib_name)
- super()
func user_setup(pos := Vector2.ZERO) -> void:
- attributes.width.set_num(1.0)
- attributes.height.set_num(1.0)
+ set_attribute("width", 1.0)
+ set_attribute("height", 1.0)
if pos != Vector2.ZERO:
- attributes.x.set_num(pos.x)
- attributes.y.set_num(pos.y)
+ set_attribute("x", pos.x)
+ set_attribute("y", pos.y)
func can_replace(new_tag: String) -> bool:
if new_tag == "ellipse":
- var rx: float = attributes.rx.get_num()
- var ry: float = attributes.ry.get_num()
- if rx == 0:
- rx = ry
- elif ry == 0:
- ry = rx
- return rx >= attributes.width.get_num() / 2 and ry >= attributes.height.get_num() / 2
+ return rx >= get_attribute_num("width") / 2 and ry >= get_attribute_num("height") / 2
elif new_tag == "circle":
- var rx: float = attributes.rx.get_num()
- var ry: float = attributes.ry.get_num()
- if rx == 0:
- rx = ry
- elif ry == 0:
- ry = rx
- var side: float = attributes.width.get_num()
- return attributes.height.get_num() == side and rx >= side / 2 and ry >= side / 2
+ var side := get_attribute_num("width")
+ return get_attribute_num("height") == side and rx >= side / 2 and ry >= side / 2
else:
return new_tag == "path"
@@ -47,57 +28,41 @@ func get_replacement(new_tag: String) -> Tag:
return null
var tag: Tag
- var retained_attributes: Array[String] = []
+ var dropped_attributes: PackedStringArray
match new_tag:
"ellipse":
tag = TagEllipse.new()
- retained_attributes = ["transform", "opacity", "fill", "fill-opacity", "stroke",
- "stroke-opacity", "stroke-width"]
- tag.attributes.rx.set_num(attributes.width.get_num() / 2,
- Attribute.SyncMode.SILENT)
- tag.attributes.ry.set_num(attributes.height.get_num() / 2,
- Attribute.SyncMode.SILENT)
- tag.attributes.cx.set_num(attributes.x.get_num() +\
- attributes.width.get_num() / 2, Attribute.SyncMode.SILENT)
- tag.attributes.cy.set_num(attributes.y.get_num() +\
- attributes.height.get_num() / 2, Attribute.SyncMode.SILENT)
+ dropped_attributes = PackedStringArray(["x", "y", "width", "height"])
+ tag.set_attribute("rx", get_attribute_num("width") / 2)
+ tag.set_attribute("ry", get_attribute_num("height") / 2)
+ tag.set_attribute("cx", get_attribute_num("x") + get_attribute_num("width") / 2)
+ tag.set_attribute("cy", get_attribute_num("y") + get_attribute_num("height") / 2)
"circle":
tag = TagCircle.new()
- retained_attributes = ["transform", "opacity", "fill", "fill-opacity",
- "stroke", "stroke-opacity", "stroke-width"]
- tag.attributes.r.set_num(attributes.width.get_num() / 2,
- Attribute.SyncMode.SILENT)
- tag.attributes.cx.set_num(attributes.x.get_num() +\
- attributes.width.get_num() / 2, Attribute.SyncMode.SILENT)
- tag.attributes.cy.set_num(attributes.y.get_num() +\
- attributes.height.get_num() / 2, Attribute.SyncMode.SILENT)
+ dropped_attributes = PackedStringArray(["x", "y", "width", "height", "rx", "ry"])
+ tag.set_attribute("r", get_attribute_num("width") / 2)
+ tag.set_attribute("cx", get_attribute_num("x") + get_attribute_num("width") / 2)
+ tag.set_attribute("cy", get_attribute_num("y") + get_attribute_num("height") / 2)
"path":
tag = TagPath.new()
- retained_attributes = ["transform", "opacity", "fill", "fill-opacity", "stroke",
- "stroke-opacity", "stroke-width", "stroke-linejoin"]
- var rx := minf(attributes.rx.get_num(), attributes.width.get_num() / 2)
- var ry := minf(attributes.ry.get_num(), attributes.height.get_num() / 2)
+ dropped_attributes = PackedStringArray(["x", "y", "width", "height", "rx", "ry"])
var commands: Array[PathCommand] = []
if rx == 0 and ry == 0:
- commands.append(PathCommand.MoveCommand.new(attributes.x.get_num(),
- attributes.y.get_num(), true))
+ commands.append(PathCommand.MoveCommand.new(get_attribute_num("x"),
+ get_attribute_num("y"), true))
commands.append(PathCommand.HorizontalLineCommand.new(
- attributes.width.get_num(), true))
+ get_attribute_num("width"), true))
commands.append(PathCommand.VerticalLineCommand.new(
- attributes.height.get_num(), true))
+ get_attribute_num("height"), true))
commands.append(PathCommand.HorizontalLineCommand.new(
- -attributes.width.get_num(), true))
+ -get_attribute_num("width"), true))
commands.append(PathCommand.CloseCommand.new(true))
else:
- if rx == 0:
- rx = ry
- elif ry == 0:
- ry = rx
- var w: float = attributes.width.get_num() - rx * 2
- var h: float = attributes.height.get_num() - ry * 2
+ var w := get_attribute_num("width") - rx * 2
+ var h := get_attribute_num("height") - ry * 2
- commands.append(PathCommand.MoveCommand.new(attributes.x.get_num(),
- attributes.y.get_num() + ry, true))
+ commands.append(PathCommand.MoveCommand.new(get_attribute_num("x"),
+ get_attribute_num("y") + ry, true))
commands.append(PathCommand.EllipticalArcCommand.new(
rx, ry, 0, 0, 1, rx, -ry, true))
if w > 0.0:
@@ -113,10 +78,31 @@ func get_replacement(new_tag: String) -> Tag:
commands.append(PathCommand.EllipticalArcCommand.new(
rx, ry, 0, 0, 1, -rx, -ry, true))
commands.append(PathCommand.CloseCommand.new(true))
- tag.attributes.d.set_commands(commands, Attribute.SyncMode.SILENT)
-
- for k in retained_attributes:
- tag.attributes[k].set_value(attributes[k].get_value(), Attribute.SyncMode.SILENT)
- tag.child_tags = child_tags
+ tag.set_attribute("d", commands)
+
+ for attribute_name in attributes:
+ if not attribute_name in dropped_attributes:
+ tag.set_attribute(attribute_name, attributes[attribute_name])
+ tag.child_tags = child_tags
return tag
+
+func update_cache() -> void:
+ rx = ry if !attributes.has("rx") else minf(get_attribute_num("rx"),
+ get_attribute_num("width") / 2)
+ ry = rx if !attributes.has("ry") else minf(get_attribute_num("ry"),
+ get_attribute_num("height") / 2)
+
+func get_own_default(attribute_name: String) -> String:
+ match attribute_name:
+ "x", "y", "width", "height": return "0"
+ "rx": return "auto"
+ "ry": return "auto"
+ "opacity": return "1"
+ _: return ""
+
+func get_bounding_box() -> Rect2:
+ var bounding_box := Rect2()
+ bounding_box.position = Vector2(get_attribute_num("x"), get_attribute_num("y"))
+ bounding_box.size = Vector2(get_attribute_num("width"), get_attribute_num("height"))
+ return bounding_box
diff --git a/src/data_classes/TagRoot.gd b/src/data_classes/TagRoot.gd
new file mode 100644
index 00000000..42578c10
--- /dev/null
+++ b/src/data_classes/TagRoot.gd
@@ -0,0 +1,255 @@
+class_name TagRoot extends TagSVG
+
+@warning_ignore("unused_signal")
+signal attribute_somewhere_changed(xid: PackedInt32Array)
+
+signal tags_added(xids: Array[PackedInt32Array])
+signal tags_deleted(xids: Array[PackedInt32Array])
+signal tags_moved_in_parent(parent_xid: PackedInt32Array, old_indices: Array[int])
+signal tags_moved_to(xids: Array[PackedInt32Array], location: PackedInt32Array)
+signal tag_layout_changed # Emitted together with any of the above 5.
+
+
+func get_all_tags() -> Array[Tag]:
+ var tags: Array[Tag] = []
+ var unchecked_tags: Array[Tag] = child_tags.duplicate()
+
+ while not unchecked_tags.is_empty():
+ var checked_tag: Tag = unchecked_tags.pop_back()
+ for child_tag in checked_tag.child_tags:
+ unchecked_tags.append(child_tag)
+ tags.append(checked_tag)
+ return tags
+
+func get_tag(id: PackedInt32Array) -> Tag:
+ var current_tag: Tag = self
+ for idx in id:
+ if idx >= current_tag.child_tags.size():
+ return null
+ current_tag = current_tag.child_tags[idx]
+ return current_tag
+
+
+func get_own_default(attribute_name: String) -> String:
+ match attribute_name:
+ "fill": return "black"
+ "fill-opacity": return "1"
+ "stroke": return "none"
+ "stroke-opacity": return "1"
+ "stroke-width": return "1"
+ "stroke-linecap": return "butt"
+ "stroke-linejoin": return "miter"
+ _: return ""
+
+
+func add_tag(new_tag: Tag, new_xid: PackedInt32Array) -> void:
+ var parent_tag := get_tag(Utils.get_parent_xid(new_xid))
+ new_tag.xid = new_xid
+ new_tag.set_parent(parent_tag)
+ parent_tag.child_tags.insert(new_xid[-1], new_tag)
+ var new_xid_array: Array[PackedInt32Array] = [new_xid]
+ tags_added.emit(new_xid_array)
+ tag_layout_changed.emit()
+
+func delete_tags(xids: Array[PackedInt32Array]) -> void:
+ if xids.is_empty():
+ return
+
+ xids = Utils.filter_descendant_xids(xids)
+ for id in xids:
+ var parent_tag := get_tag(Utils.get_parent_xid(id))
+ if is_instance_valid(parent_tag):
+ var tag_idx := id[-1]
+ if tag_idx < parent_tag.get_child_count():
+ parent_tag.child_tags.remove_at(tag_idx)
+ tags_deleted.emit(xids)
+ tag_layout_changed.emit()
+
+# Moves tags up or down, not to an arbitrary position.
+func move_tags_in_parent(xids: Array[PackedInt32Array], down: bool) -> void:
+ if xids.is_empty():
+ return
+
+ # For moving, all these tags must be direct children of the same parent.
+ xids = Utils.filter_descendant_xids(xids)
+ var depth := xids[0].size()
+ var parent_xid := Utils.get_parent_xid(xids[0])
+ for id in xids:
+ if id.size() != depth or Utils.get_parent_xid(id) != parent_xid:
+ return
+
+ var xid_indices: Array[int] = [] # The last indices of the XIDs.
+ for id in xids:
+ xid_indices.append(id[-1])
+
+ var parent_tag := get_tag(parent_xid)
+ var parent_child_count := parent_tag.get_child_count()
+ var old_indices: Array[int] = []
+ for i in parent_child_count:
+ old_indices.append(i)
+ # Do the moving.
+ if down:
+ var i := parent_child_count - 1
+ while i >= 0:
+ if not i in xid_indices and (i - 1) in xid_indices:
+ old_indices.remove_at(i)
+ var moved_i := i
+ var moved_tag: Tag = parent_tag.child_tags.pop_at(i)
+ while (i - 1) in xid_indices:
+ i -= 1
+ old_indices.insert(i, moved_i)
+ parent_tag.child_tags.insert(i, moved_tag)
+ i -= 1
+ else:
+ var i := 0
+ while i < parent_child_count:
+ if not i in xid_indices and (i + 1) in xid_indices:
+ old_indices.remove_at(i)
+ var moved_i := i
+ var moved_tag: Tag = parent_tag.child_tags.pop_at(i)
+ while (i + 1) in xid_indices:
+ i += 1
+ old_indices.insert(i, moved_i)
+ parent_tag.child_tags.insert(i, moved_tag)
+ i += 1
+ # Check if indices were really changed after the operation.
+ if old_indices != range(old_indices.size()):
+ tags_moved_in_parent.emit(parent_xid, old_indices)
+ tag_layout_changed.emit()
+
+# Moves tags to an arbitrary position. The first moved tag will move to the location XID.
+func move_tags_to(xids: Array[PackedInt32Array], location: PackedInt32Array) -> void:
+ xids = Utils.filter_descendant_xids(xids)
+ # A tag can't move deeper inside itself. Remove the descendants of the location.
+ for i in range(xids.size() - 1, -1, -1):
+ if Utils.is_xid_parent(xids[i], location):
+ xids.remove_at(i)
+
+ # Remove tags from their old locations.
+ var xids_stored: Array[PackedInt32Array] = []
+ var tags_stored: Array[Tag] = []
+ for id in xids:
+ # Shift the new location if tags before it were removed. A tag is "before"
+ # if it has the same parent as the new location, but is before that location.
+ if id.size() <= location.size():
+ var before := true
+ for i in id.size() - 1:
+ if id[i] != location[i]:
+ before = false
+ break
+ if before and id[-1] < location[id.size() - 1]:
+ location[id.size() - 1] -= 1
+ xids_stored.append(id)
+ tags_stored.append(get_tag(Utils.get_parent_xid(id)).child_tags.pop_at(id[-1]))
+ # Add the tags back in the new location.
+ for tag in tags_stored:
+ get_tag(Utils.get_parent_xid(location)).child_tags.insert(location[-1], tag)
+ # Check if this actually chagned the layout.
+ for id in xids_stored:
+ if not Utils.are_xid_parents_same(id, location) or id[-1] < location[-1] or\
+ id[-1] >= location[-1] + xids_stored.size():
+ # If this condition is passed, then there was a layout change.
+ tags_moved_to.emit(xids, location)
+ tag_layout_changed.emit()
+ return
+
+# Duplicates tags and puts them below.
+func duplicate_tags(xids: Array[PackedInt32Array]) -> void:
+ if xids.is_empty():
+ return
+
+ xids = Utils.filter_descendant_xids(xids)
+ var xids_added: Array[PackedInt32Array] = []
+ # Used to offset previously added XIDs in xids_added after duplicating a tag before.
+ var last_parent := PackedInt32Array([-1]) # Start with a XID that can't be matched.
+ var added_to_last_parent := 0
+
+ for id in xids:
+ var new_tag := get_tag(id).duplicate()
+ # Add the new tag.
+ var new_xid := id.duplicate()
+ new_xid[-1] += 1
+ var parent_xid := Utils.get_parent_xid(new_xid)
+ get_tag(parent_xid).child_tags.insert(new_xid[-1], new_tag)
+ # Add the XID and offset the other ones from the same parent.
+ var added_xid_idx := xids_added.size()
+ xids_added.append(new_xid)
+ if last_parent == parent_xid:
+ added_to_last_parent += 1
+ else:
+ last_parent = parent_xid
+ added_to_last_parent = 0
+ for xid_idx in range(added_xid_idx - added_to_last_parent , added_xid_idx):
+ xids_added[xid_idx][-1] += 1
+ tags_added.emit(xids_added)
+ tag_layout_changed.emit()
+
+func replace_tag(id: PackedInt32Array, new_tag: Tag) -> void:
+ get_tag(Utils.get_parent_xid(id)).child_tags[id[-1]] = new_tag
+ tag_layout_changed.emit()
+
+# Optimizes the SVG text in more ways than what formatting attributes allows.
+# The return value is true if the SVG can be optimized, otherwise false.
+# If apply_changes is false, you'll only get the return value.
+func optimize(not_applied := false) -> bool:
+ for tag in get_all_tags():
+ match tag.name:
+ "ellipse":
+ # If possible, turn ellipses into circles.
+ if tag.can_replace("circle"):
+ if not_applied:
+ return true
+ replace_tag(tag.xid, tag.get_replacement("circle"))
+ "line":
+ # Turn lines into paths.
+ if not_applied:
+ return true
+ replace_tag(tag.xid, tag.get_replacement("path"))
+ "rect":
+ # If possible, turn rounded rects into circles or ellipses.
+ if tag.can_replace("circle"):
+ if not_applied:
+ return true
+ replace_tag(tag.xid, tag.get_replacement("circle"))
+ elif tag.can_replace("ellipse"):
+ if not_applied:
+ return true
+ replace_tag(tag.xid, tag.get_replacement("ellipse"))
+ elif tag.rx == 0:
+ # If the rectangle is not rounded, turn it into a path.
+ if not_applied:
+ return true
+ replace_tag(tag.xid, tag.get_replacement("path"))
+ "path":
+ var pathdata: AttributePathdata = tag.get_attribute("d")
+ # Simplify A rotation to 0 degrees for circular arcs.
+ for cmd_idx in pathdata.get_command_count():
+ var command := pathdata.get_command(cmd_idx)
+ var cmd_char := command.command_char
+ if cmd_char in "Aa" and command.rx == command.ry and command.rot != 0:
+ if not_applied:
+ return true
+ pathdata.set_command_property(cmd_idx, "rot", 0)
+ # Replace L with H or V when possible.
+ for cmd_idx in pathdata.get_command_count():
+ var command := pathdata.get_command(cmd_idx)
+ var cmd_char := command.command_char
+ if cmd_char == "l":
+ if command.x == 0:
+ if not_applied:
+ return true
+ pathdata.convert_command(cmd_idx, "v")
+ elif command.y == 0:
+ if not_applied:
+ return true
+ pathdata.convert_command(cmd_idx, "h")
+ elif cmd_char == "L":
+ if command.x == command.start.x:
+ if not_applied:
+ return true
+ pathdata.convert_command(cmd_idx, "V")
+ elif command.y == command.start.y:
+ if not_applied:
+ return true
+ pathdata.convert_command(cmd_idx, "H")
+ return false
diff --git a/src/data_classes/TagSVG.gd b/src/data_classes/TagSVG.gd
index 03c64b91..2ee2a81e 100644
--- a/src/data_classes/TagSVG.gd
+++ b/src/data_classes/TagSVG.gd
@@ -6,68 +6,43 @@ var height: float
var viewbox: Rect2
var canvas_transform: Transform2D
-# The difference between attribute_changed() and resized() is that
-# resized() will emit even after unknown changes.
-signal resized
-
-signal child_attribute_changed(undo_redo: bool)
-signal changed_unknown
-
-signal tags_added(tids: Array[PackedInt32Array])
-signal tags_deleted(tids: Array[PackedInt32Array])
-signal tags_moved_in_parent(parent_tid: PackedInt32Array, old_indices: Array[int])
-signal tags_moved_to(tids: Array[PackedInt32Array], location: PackedInt32Array)
-signal tag_layout_changed # Emitted together with any of the above 5.
-
-# This list is currently only used by the highlighter, so xmlns is here.
-const known_attributes = ["width", "height", "viewBox", "xmlns"]
const name = "svg"
-func _init() -> void:
- for attrib_name in ["width", "height", "viewBox"]:
- attributes[attrib_name] = DB.attribute(attrib_name)
- unknown_attributes.append(Attribute.new("xmlns", "http://www.w3.org/2000/svg"))
- attribute_changed.connect(update_cache.unbind(1))
- changed_unknown.connect(update_cache)
- update_cache()
- super()
-
func update_cache() -> void:
- var has_valid_width: bool = !attributes.width.get_value().is_empty()
- var has_valid_height: bool = !attributes.height.get_value().is_empty()
- var has_valid_viewbox: bool = attributes.viewBox.get_list_size() >= 4
+ var has_valid_width := attributes.has("width")
+ var has_valid_height := attributes.has("height")
+ var has_valid_viewbox := attributes.has("viewBox")
# Return early on invalid input.
if not has_valid_viewbox and not (has_valid_width and has_valid_height):
- width = attributes.width.get_num() if has_valid_width else 0
- height = attributes.height.get_num() if has_valid_height else 0
+ width = get_attribute_num("width") if has_valid_width else 0.0
+ height = get_attribute_num("height") if has_valid_height else 0.0
viewbox = Rect2(0, 0, 0, 0)
canvas_transform = Transform2D.IDENTITY
return
# From now on we're sure the input is valid. Cache width and height.
if has_valid_width:
- width = attributes.width.get_num()
+ width = get_attribute_num("width")
if not has_valid_height:
- height = attributes.width.get_num() / attributes.viewBox.get_list_element(2) *\
+ height = width / attributes.viewBox.get_list_element(2) *\
attributes.viewBox.get_list_element(3)
else:
- height = attributes.height.get_num()
+ height = get_attribute_num("height")
elif has_valid_height:
- height = attributes.height.get_num()
- width = attributes.height.get_num() / attributes.viewBox.get_list_element(3) *\
+ height = get_attribute_num("height")
+ width = height / attributes.viewBox.get_list_element(3) *\
attributes.viewBox.get_list_element(2)
else:
width = attributes.viewBox.get_list_element(2)
height = attributes.viewBox.get_list_element(3)
# Cache viewbox.
- if attributes.viewBox.get_list_size() >= 4:
- viewbox = Rect2(attributes.viewBox.get_list_element(0),
- attributes.viewBox.get_list_element(1),
- attributes.viewBox.get_list_element(2),
- attributes.viewBox.get_list_element(3))
+ if has_valid_viewbox and attributes.viewBox.get_list_size() >= 4:
+ var viewbox_attrib: AttributeList = attributes.viewBox
+ viewbox = Rect2(viewbox_attrib.get_list_element(0), viewbox_attrib.get_list_element(1),
+ viewbox_attrib.get_list_element(2), viewbox_attrib.get_list_element(3))
else:
- viewbox = Rect2(0, 0, attributes.width.get_num(), attributes.height.get_num())
+ viewbox = Rect2(0, 0, get_attribute_num("width"), get_attribute_num("height"))
# Cache canvas transform.
var width_ratio := width / viewbox.size.x
var height_ratio := height / viewbox.size.y
@@ -89,277 +64,5 @@ func canvas_to_world(pos: Vector2) -> Vector2:
func world_to_canvas(pos: Vector2) -> Vector2:
return canvas_transform.affine_inverse() * pos
-
func get_size() -> Vector2:
return Vector2(width, height)
-
-
-func get_all_tids() -> Array[PackedInt32Array]:
- var tids: Array[PackedInt32Array] = []
- var unchecked_tids: Array[PackedInt32Array] = []
- for idx in get_child_count():
- unchecked_tids.append(PackedInt32Array([idx]))
-
- while not unchecked_tids.is_empty():
- var checked_tid: PackedInt32Array = unchecked_tids.pop_back()
- for idx in get_tag(checked_tid).get_child_count():
- var new_tid := checked_tid.duplicate()
- new_tid.append(idx)
- unchecked_tids.append(new_tid)
- tids.append(checked_tid)
- return tids
-
-func get_tag(tid: PackedInt32Array) -> Tag:
- var current_tag: Tag = self
- for idx in tid:
- if idx >= current_tag.child_tags.size():
- return null
- current_tag = current_tag.child_tags[idx]
- return current_tag
-
-
-func add_tag(new_tag: Tag, new_tid: PackedInt32Array) -> void:
- var parent_tid := Utils.get_parent_tid(new_tid)
- get_tag(parent_tid).child_tags.insert(new_tid[-1], new_tag)
- new_tag.attribute_changed.connect(emit_child_attribute_changed)
- var new_tid_array: Array[PackedInt32Array] = [new_tid]
- tags_added.emit(new_tid_array)
- tag_layout_changed.emit()
-
-func replace_self(new_tag: Tag) -> void:
- var old_size := get_size()
- for attrib in attributes:
- attributes[attrib].set_value(new_tag.attributes[attrib].get_value(),
- Attribute.SyncMode.SILENT)
-
- unknown_attributes.clear()
- for attrib in new_tag.unknown_attributes:
- unknown_attributes.append(attrib)
- child_tags.clear()
-
- for tag in new_tag.child_tags:
- child_tags.append(tag)
-
- for tid in get_all_tids():
- get_tag(tid).attribute_changed.connect(emit_child_attribute_changed)
-
- changed_unknown.emit()
- if old_size != get_size():
- resized.emit()
-
-func delete_tags(tids: Array[PackedInt32Array]) -> void:
- if tids.is_empty():
- return
-
- tids = Utils.filter_descendant_tids(tids)
- for tid in tids:
- var parent_tid := Utils.get_parent_tid(tid)
- var parent_tag := get_tag(parent_tid)
- if is_instance_valid(parent_tag):
- var tag_idx := tid[-1]
- if tag_idx < parent_tag.get_child_count():
- parent_tag.child_tags.remove_at(tag_idx)
- tags_deleted.emit(tids)
- tag_layout_changed.emit()
-
-# Moves tags up or down, not to an arbitrary position.
-func move_tags_in_parent(tids: Array[PackedInt32Array], down: bool) -> void:
- if tids.is_empty():
- return
-
- # For moving, all these tags must be direct children of the same parent.
- tids = Utils.filter_descendant_tids(tids)
- var depth := tids[0].size()
- var parent_tid := Utils.get_parent_tid(tids[0])
- for tid in tids:
- if tid.size() != depth or Utils.get_parent_tid(tid) != parent_tid:
- return
-
- var tid_indices: Array[int] = [] # The last indices of the TIDs.
- for tid in tids:
- tid_indices.append(tid[-1])
-
- var parent_tag := get_tag(parent_tid)
- var parent_child_count := parent_tag.get_child_count()
- var old_indices: Array[int] = []
- for i in parent_child_count:
- old_indices.append(i)
- # Do the moving.
- if down:
- var i := parent_child_count - 1
- while i >= 0:
- if not i in tid_indices and (i - 1) in tid_indices:
- old_indices.remove_at(i)
- var moved_i := i
- var moved_tag: Tag = parent_tag.child_tags.pop_at(i)
- while (i - 1) in tid_indices:
- i -= 1
- old_indices.insert(i, moved_i)
- parent_tag.child_tags.insert(i, moved_tag)
- i -= 1
- else:
- var i := 0
- while i < parent_child_count:
- if not i in tid_indices and (i + 1) in tid_indices:
- old_indices.remove_at(i)
- var moved_i := i
- var moved_tag: Tag = parent_tag.child_tags.pop_at(i)
- while (i + 1) in tid_indices:
- i += 1
- old_indices.insert(i, moved_i)
- parent_tag.child_tags.insert(i, moved_tag)
- i += 1
- # Check if indices were really changed after the operation.
- if old_indices != range(old_indices.size()):
- tags_moved_in_parent.emit(parent_tid, old_indices)
- tag_layout_changed.emit()
-
-# Moves tags to an arbitrary position. The first moved tag will move to the location TID.
-func move_tags_to(tids: Array[PackedInt32Array], location: PackedInt32Array) -> void:
- tids = Utils.filter_descendant_tids(tids)
- # A tag can't move deeper inside itself. Remove the descendants of the location.
- for i in range(tids.size() - 1, -1, -1):
- if Utils.is_tid_parent(tids[i], location):
- tids.remove_at(i)
-
- # Remove tags from their old locations.
- var tids_stored: Array[PackedInt32Array] = []
- var tags_stored: Array[Tag] = []
- for tid in tids:
- # Shift the new location if tags before it were removed. A tag is "before"
- # if it has the same parent as the new location, but is before that location.
- if tid.size() <= location.size():
- var before := true
- for i in tid.size() - 1:
- if tid[i] != location[i]:
- before = false
- break
- if before and tid[-1] < location[tid.size() - 1]:
- location[tid.size() - 1] -= 1
- tids_stored.append(tid)
- tags_stored.append(get_tag(Utils.get_parent_tid(tid)).child_tags.pop_at(tid[-1]))
- # Add the tags back in the new location.
- for tag in tags_stored:
- get_tag(Utils.get_parent_tid(location)).child_tags.insert(location[-1], tag)
- # Check if this actually chagned the layout.
- for tid in tids_stored:
- if not Utils.are_tid_parents_same(tid, location) or tid[-1] < location[-1] or\
- tid[-1] >= location[-1] + tids_stored.size():
- # If this condition is passed, then there was a layout change.
- tags_moved_to.emit(tids, location)
- tag_layout_changed.emit()
- return
-
-# Duplicates tags and puts them below.
-func duplicate_tags(tids: Array[PackedInt32Array]) -> void:
- if tids.is_empty():
- return
-
- tids = Utils.filter_descendant_tids(tids)
- var tids_added: Array[PackedInt32Array] = []
- # Used to offset previously added TIDs in tids_added after duplicating a tag before.
- var last_parent := PackedInt32Array([-1]) # Start with a TID that can't be matched.
- var added_to_last_parent := 0
-
- for tid in tids:
- var new_tag := get_tag(tid).duplicate()
- # Add the new tag.
- var new_tid := tid.duplicate()
- new_tid[-1] += 1
- var parent_tid := Utils.get_parent_tid(new_tid)
- get_tag(parent_tid).child_tags.insert(new_tid[-1], new_tag)
- new_tag.attribute_changed.connect(emit_child_attribute_changed)
- # Add the TID and offset the other ones from the same parent.
- var added_tid_idx := tids_added.size()
- tids_added.append(new_tid)
- if last_parent == parent_tid:
- added_to_last_parent += 1
- else:
- last_parent = parent_tid
- added_to_last_parent = 0
- for tid_idx in range(added_tid_idx - added_to_last_parent , added_tid_idx):
- tids_added[tid_idx][-1] += 1
- tags_added.emit(tids_added)
- tag_layout_changed.emit()
-
-func replace_tag(tid: PackedInt32Array, new_tag: Tag) -> void:
- if tid.is_empty():
- replace_self(new_tag)
- get_tag(Utils.get_parent_tid(tid)).child_tags[tid[-1]] = new_tag
- new_tag.attribute_changed.connect(emit_child_attribute_changed)
- tag_layout_changed.emit()
-
-func emit_child_attribute_changed(undo_redo: bool) -> void:
- child_attribute_changed.emit(undo_redo)
-
-func emit_attribute_changed(undo_redo: bool) -> void:
- super(undo_redo)
- resized.emit()
-
-
-# Optimizes the SVG text in more ways than what formatting attributes allows.
-# The return value is true if the SVG can be optimized, otherwise false.
-# If apply_changes is false, you'll only get the return value.
-func optimize(not_applied := false) -> bool:
- for tid in get_all_tids():
- var tag := get_tag(tid)
- match tag.name:
- "ellipse":
- # If possible, turn ellipses into circles.
- if tag.can_replace("circle"):
- if not_applied:
- return true
- replace_tag(tid, get_tag(tid).get_replacement("circle"))
- "line":
- # Turn lines into paths.
- if not_applied:
- return true
- replace_tag(tid, get_tag(tid).get_replacement("path"))
- "rect":
- # If possible, turn rounded rects into circles or ellipses.
- if tag.can_replace("circle"):
- if not_applied:
- return true
- replace_tag(tid, get_tag(tid).get_replacement("circle"))
- elif tag.can_replace("ellipse"):
- if not_applied:
- return true
- replace_tag(tid, get_tag(tid).get_replacement("ellipse"))
- elif tag.attributes.rx.get_num() == 0 and tag.attributes.ry.get_num() == 0:
- # If the rectangle is not rounded, turn it into a path.
- if not_applied:
- return true
- replace_tag(tid, get_tag(tid).get_replacement("path"))
- "path":
- var pathdata: AttributePath = tag.attributes.d
- # Simplify A rotation to 0 degrees for circular arcs.
- for cmd_idx in pathdata.get_command_count():
- var command := pathdata.get_command(cmd_idx)
- var cmd_char := command.command_char
- if cmd_char in "Aa" and command.rx == command.ry and command.rot != 0:
- if not_applied:
- return true
- pathdata.set_command_property(cmd_idx, "rot", 0)
- # Replace L with H or V when possible.
- for cmd_idx in pathdata.get_command_count():
- var command := pathdata.get_command(cmd_idx)
- var cmd_char := command.command_char
- if cmd_char == "l":
- if command.x == 0:
- if not_applied:
- return true
- pathdata.convert_command(cmd_idx, "v")
- elif command.y == 0:
- if not_applied:
- return true
- pathdata.convert_command(cmd_idx, "h")
- elif cmd_char == "L":
- if command.x == command.start.x:
- if not_applied:
- return true
- pathdata.convert_command(cmd_idx, "V")
- elif command.y == command.start.y:
- if not_applied:
- return true
- pathdata.convert_command(cmd_idx, "H")
- return false
diff --git a/src/data_classes/TagShape.gd b/src/data_classes/TagShape.gd
new file mode 100644
index 00000000..b826b0a4
--- /dev/null
+++ b/src/data_classes/TagShape.gd
@@ -0,0 +1,12 @@
+class_name TagShape extends Tag
+
+# To be overridden.
+func get_bounding_box() -> Rect2:
+ return Rect2(0, 0, 0, 0)
+
+func get_config_warnings() -> PackedStringArray:
+ var warnings := super()
+ if not (parent is TagG or parent is TagSVG or parent is TagUnrecognized):
+ warnings.append(TranslationServer.translate("{tag} must be a child of {allowed} to have any effect.").format(
+ {"tag": self.name, "allowed": "[svg, g]"}))
+ return warnings
diff --git a/src/data_classes/TagStop.gd b/src/data_classes/TagStop.gd
index 7a473672..e4b983a2 100644
--- a/src/data_classes/TagStop.gd
+++ b/src/data_classes/TagStop.gd
@@ -3,10 +3,17 @@ class_name TagStop extends Tag
const name = "stop"
const possible_conversions = []
-const known_attributes = ["offset", "stop-color", "stop-opacity"]
-const icon = preload("res://visual/icons/tag/stop.svg")
-func _init() -> void:
- for attrib_name in ["offset", "stop-color", "stop-opacity"]:
- attributes[attrib_name] = DB.attribute(attrib_name)
- super()
+func get_own_default(attribute_name: String) -> String:
+ match attribute_name:
+ "offset": return "0"
+ "stop-color": return "black"
+ "stop-opacity": return "1"
+ _: return ""
+
+func get_config_warnings() -> PackedStringArray:
+ var warnings := super()
+ if not (parent is TagLinearGradient or parent is TagRadialGradient):
+ warnings.append(TranslationServer.translate("{tag} must be a child of {allowed} to have any effect.").format(
+ {"tag": name, "allowed": "[linearGradient, radialGradient]"}))
+ return warnings
diff --git a/src/data_classes/TagUnknown.gd b/src/data_classes/TagUnknown.gd
deleted file mode 100644
index 341c54c1..00000000
--- a/src/data_classes/TagUnknown.gd
+++ /dev/null
@@ -1,11 +0,0 @@
-# A tag that GodSVG doesn't recognize.
-class_name TagUnknown extends Tag
-
-var name: String
-const possible_conversions = []
-const known_shape_attributes = []
-const known_inheritable_attributes = []
-const icon = preload("res://visual/icons/tag/unknown.svg")
-
-func _init(new_name: String) -> void:
- name = new_name
diff --git a/src/data_classes/TagUnrecognized.gd b/src/data_classes/TagUnrecognized.gd
new file mode 100644
index 00000000..48c3be84
--- /dev/null
+++ b/src/data_classes/TagUnrecognized.gd
@@ -0,0 +1,8 @@
+# A tag that GodSVG doesn't recognize.
+class_name TagUnrecognized extends Tag
+
+var name: String
+const possible_conversions = []
+
+func _init(new_name: String) -> void:
+ name = new_name
diff --git a/src/data_classes/Transform.gd b/src/data_classes/Transform.gd
new file mode 100644
index 00000000..d04a7835
--- /dev/null
+++ b/src/data_classes/Transform.gd
@@ -0,0 +1,75 @@
+class_name Transform extends RefCounted
+
+class TransformMatrix extends Transform:
+ var x1: float
+ var x2: float
+ var y1: float
+ var y2: float
+ var o1: float
+ var o2: float
+
+ func _init(new_x1: float, new_x2: float, new_y1: float, new_y2: float, new_o1: float,
+ new_o2: float) -> void:
+ x1 = new_x1
+ x2 = new_x2
+ y1 = new_y1
+ y2 = new_y2
+ o1 = new_o1
+ o2 = new_o2
+
+ func compute_transform() -> Transform2D:
+ return Transform2D(Vector2(x1, x2), Vector2(y1, y2), Vector2(o1, o2))
+
+class TransformTranslate extends Transform:
+ var x: float
+ var y: float
+
+ func _init(new_x: float, new_y: float) -> void:
+ x = new_x
+ y = new_y
+
+ func compute_transform() -> Transform2D:
+ return Transform2D(Vector2.RIGHT, Vector2.DOWN, Vector2(x, y))
+
+class TransformRotate extends Transform:
+ var deg: float
+ var x: float
+ var y: float
+
+ func _init(new_deg: float, new_x: float, new_y: float) -> void:
+ deg = new_deg
+ x = new_x
+ y = new_y
+
+ func compute_transform() -> Transform2D:
+ var pt := Vector2(x, y)
+ return Transform2D.IDENTITY.translated(-pt).rotated(deg_to_rad(deg)).translated(pt)
+
+class TransformScale extends Transform:
+ var x: float
+ var y: float
+
+ func _init(new_x: float, new_y: float) -> void:
+ x = new_x
+ y = new_y
+
+ func compute_transform() -> Transform2D:
+ return Transform2D(Vector2.RIGHT * x, Vector2.DOWN * y, Vector2.ZERO)
+
+class TransformSkewX extends Transform:
+ var x: float
+
+ func _init(new_x: float) -> void:
+ x = new_x
+
+ func compute_transform() -> Transform2D:
+ return Transform2D(Vector2.RIGHT, Vector2(tan(deg_to_rad(x)), 1), Vector2.ZERO)
+
+class TransformSkewY extends Transform:
+ var y: float
+
+ func _init(new_y: float) -> void:
+ y = new_y
+
+ func compute_transform() -> Transform2D:
+ return Transform2D(Vector2(1, tan(deg_to_rad(y))), Vector2.DOWN, Vector2.ZERO)
diff --git a/src/data_classes/XNode.gd b/src/data_classes/XNode.gd
new file mode 100644
index 00000000..3bdf3efa
--- /dev/null
+++ b/src/data_classes/XNode.gd
@@ -0,0 +1,3 @@
+class_name XNode extends RefCounted
+
+var xid: PackedInt32Array
diff --git a/src/parsers/ColorParser.gd b/src/parsers/ColorParser.gd
index 02fbede3..629b14d6 100644
--- a/src/parsers/ColorParser.gd
+++ b/src/parsers/ColorParser.gd
@@ -77,7 +77,7 @@ static func is_valid_url(color: String) -> bool:
if not color.begins_with("url(") or not color.ends_with(")"):
return false
var id := color.substr(4, color.length() - 5).strip_edges().trim_prefix("#")
- return IDParser.get_id_validity(id) != IDParser.ValidityLevel.INVALID
+ return IDParser.get_validity(id) != IDParser.ValidityLevel.INVALID
# URL doesn't have a color interpretation, so it'll give the backup.
static func string_to_color(color: String, backup := Color.BLACK,
diff --git a/src/parsers/IDParser.gd b/src/parsers/IDParser.gd
index 1fc9143d..f3db349c 100644
--- a/src/parsers/IDParser.gd
+++ b/src/parsers/IDParser.gd
@@ -5,7 +5,7 @@ class_name IDParser extends RefCounted
enum ValidityLevel {VALID, INVALID_XML_NAMETOKEN, INVALID}
-static func get_id_validity(id: String) -> ValidityLevel:
+static func get_validity(id: String) -> ValidityLevel:
if id.is_empty() or id[0] == "#":
return ValidityLevel.INVALID
diff --git a/src/parsers/PathDataParser.gd b/src/parsers/PathDataParser.gd
index 8948c687..b688afdc 100644
--- a/src/parsers/PathDataParser.gd
+++ b/src/parsers/PathDataParser.gd
@@ -1,12 +1,12 @@
-class_name PathDataParser extends RefCounted
+class_name PathdataParser extends RefCounted
const translation_dict = PathCommand.translation_dict
-static func parse_path_data(text: String) -> Array[PathCommand]:
- return path_commands_from_parsed_data(path_data_to_arrays(text))
+static func parse_pathdata(text: String) -> Array[PathCommand]:
+ return path_commands_from_parsed_data(pathdata_to_arrays(text))
# godot_only/tests.gd has a test for this.
-static func path_data_to_arrays(text: String) -> Array[Array]:
+static func pathdata_to_arrays(text: String) -> Array[Array]:
var new_commands: Array[Array] = []
var curr_command := ""
var prev_command := ""
@@ -173,21 +173,21 @@ static func path_commands_from_parsed_data(data: Array[Array]) -> Array[PathComm
static func path_commands_to_text(commands_arr: Array[PathCommand]) -> String:
var output := ""
var num_parser := NumberArrayParser.new()
- num_parser.compress_numbers = GlobalSettings.path_compress_numbers
- num_parser.minimize_spacing = GlobalSettings.path_minimize_spacing
+ num_parser.compress_numbers = GlobalSettings.pathdata_compress_numbers
+ num_parser.minimize_spacing = GlobalSettings.pathdata_minimize_spacing
var last_command := ""
for i in commands_arr.size():
var cmd := commands_arr[i]
var cmd_char_capitalized := cmd.command_char.to_upper()
- if not (GlobalSettings.path_remove_consecutive_commands and\
+ if not (GlobalSettings.pathdata_remove_consecutive_commands and\
((cmd_char_capitalized != "M" and last_command == cmd.command_char) or\
(last_command == "m" and cmd.command_char == "l") or\
(last_command == "M" and cmd.command_char == "L"))):
output += cmd.command_char
- if not GlobalSettings.path_minimize_spacing:
+ if not GlobalSettings.pathdata_minimize_spacing:
output += " "
- elif i > 0 and GlobalSettings.path_minimize_spacing:
+ elif i > 0 and GlobalSettings.pathdata_minimize_spacing:
var current_char := ""
var prev_numstr := ""
match cmd_char_capitalized:
@@ -209,7 +209,7 @@ static func path_commands_to_text(commands_arr: Array[PathCommand]) -> String:
"V":
current_char = num_parser.num_to_text(cmd.y)[0]
prev_numstr = num_parser.num_to_text(+commands_arr[i - 1].y)
- if not GlobalSettings.path_minimize_spacing or not\
+ if not GlobalSettings.pathdata_minimize_spacing or not\
(("." in prev_numstr and current_char == ".") or current_char in "-+"):
output += " "
@@ -218,7 +218,7 @@ static func path_commands_to_text(commands_arr: Array[PathCommand]) -> String:
"A":
output += num_parser.numstr_arr_to_text([num_parser.num_to_text(cmd.rx),
num_parser.num_to_text(cmd.ry), num_parser.num_to_text(cmd.rot, true)])
- if GlobalSettings.path_remove_spacing_after_flags:
+ if GlobalSettings.pathdata_remove_spacing_after_flags:
output += (" 0" if cmd.large_arc_flag == 0 else " 1") +\
("0" if cmd.sweep_flag == 0 else "1")
else:
@@ -247,7 +247,7 @@ static func path_commands_to_text(commands_arr: Array[PathCommand]) -> String:
"V":
output += num_parser.num_to_text(cmd.y)
_: continue
- if not GlobalSettings.path_minimize_spacing:
+ if not GlobalSettings.pathdata_minimize_spacing:
output += " "
output = output.rstrip(" ")
diff --git a/src/parsers/SVGHighlighter.gd b/src/parsers/SVGHighlighter.gd
index 7105ee31..df9e82ee 100644
--- a/src/parsers/SVGHighlighter.gd
+++ b/src/parsers/SVGHighlighter.gd
@@ -10,12 +10,12 @@ class_name SVGHighlighter extends SyntaxHighlighter
@export var cdata_color := Color("ffeda1ac")
@export var error_color := Color("ff866b")
-var unknown_tag_color: Color
-var unknown_attribute_color: Color
+var unrecognized_tag_color: Color
+var unrecognized_attribute_color: Color
func setup_extra_colors() -> void:
- unknown_tag_color = Color(tag_color, tag_color.a * 0.7)
- unknown_attribute_color = Color(attribute_color, attribute_color.a * 0.7)
+ unrecognized_tag_color = Color(tag_color, tag_color.a * 2 / 3.0)
+ unrecognized_attribute_color = Color(attribute_color, attribute_color.a * 2 / 3.0)
func _get_line_syntax_highlighting(line: int) -> Dictionary:
@@ -39,7 +39,7 @@ func _get_line_syntax_highlighting(line: int) -> Dictionary:
return color_map
else:
color_map[offset] = {"color":
- tag_color if DB.is_tag_known(tag_name) else unknown_tag_color}
+ tag_color if tag_name in DB.recognized_tags else unrecognized_tag_color}
offset += tag_name.length()
# Attribute names can't be directly after a quotation or after the tag name.
@@ -70,20 +70,20 @@ func _get_line_syntax_highlighting(line: int) -> Dictionary:
elif not current_attribute_name.is_empty():
if c in " \t\n\r":
color_map[offset - current_attribute_name.length()] = {"color":
- attribute_color if DB.is_attribute_known(tag_name,
- current_attribute_name) else unknown_attribute_color}
+ attribute_color if DB.is_attribute_recognized(tag_name,
+ current_attribute_name) else unrecognized_attribute_color}
current_attribute_name = ""
expecting_equal_sign = true
elif c in "/>":
color_map[offset - current_attribute_name.length()] = {"color":
- attribute_color if DB.is_attribute_known(tag_name,
- current_attribute_name) else unknown_attribute_color}
+ attribute_color if DB.is_attribute_recognized(tag_name,
+ current_attribute_name) else unrecognized_attribute_color}
color_map[offset] = {"color": error_color}
return color_map
elif c == "=":
color_map[offset - current_attribute_name.length()] = {"color":
- attribute_color if DB.is_attribute_known(tag_name,
- current_attribute_name) else unknown_attribute_color}
+ attribute_color if DB.is_attribute_recognized(tag_name,
+ current_attribute_name) else unrecognized_attribute_color}
color_map[offset] = {"color": symbol_color}
current_attribute_name = ""
expecting_attribute_value = true
@@ -123,15 +123,15 @@ func _get_line_syntax_highlighting(line: int) -> Dictionary:
offset += 1
if not current_attribute_name.is_empty():
color_map[svg_text.length() - current_attribute_name.length() - 1] =\
- {"color": attribute_color if DB.is_attribute_known(tag_name,
- current_attribute_name) else unknown_attribute_color}
+ {"color": attribute_color if DB.is_attribute_recognized(tag_name,
+ current_attribute_name) else unrecognized_attribute_color}
XMLParser.NODE_ELEMENT_END:
offset = svg_text.find("<", offset)
var tag_name := parser.get_node_name()
color_map[offset] = {"color": symbol_color}
offset += 2
color_map[offset] = {"color":
- tag_color if DB.is_tag_known(tag_name) else unknown_tag_color}
+ tag_color if tag_name in DB.recognized_tags else unrecognized_tag_color}
offset += tag_name.length()
color_map[offset] = {"color": symbol_color}
XMLParser.NODE_TEXT:
@@ -142,4 +142,3 @@ func _get_line_syntax_highlighting(line: int) -> Dictionary:
color_map[offset] = {"color": comment_color}
return color_map
-
diff --git a/src/parsers/SVGParser.gd b/src/parsers/SVGParser.gd
index a3bddc4e..64d526c5 100644
--- a/src/parsers/SVGParser.gd
+++ b/src/parsers/SVGParser.gd
@@ -4,54 +4,54 @@ class_name SVGParser extends RefCounted
const shorthand_tag_exceptions = ["svg", "g", "linearGradient, radialGradient"]
# For rendering only a section of the SVG.
-static func svg_to_text_custom(svg_tag: TagSVG, custom_width: float,
+static func root_to_text_custom(svg_tag: TagSVG, custom_width: float,
custom_height: float, custom_viewbox: Rect2) -> String:
var new_svg_tag: TagSVG = svg_tag.duplicate(false)
- new_svg_tag.attributes.viewBox.set_rect(custom_viewbox)
- new_svg_tag.attributes.width.set_num(custom_width)
- new_svg_tag.attributes.height.set_num(custom_height)
+ new_svg_tag.set_attribute("viewBox", custom_viewbox)
+ new_svg_tag.set_attribute("width", custom_width)
+ new_svg_tag.set_attribute("height", custom_height)
var text := _tag_to_text(new_svg_tag)
- text = text.left(-6) # Removee the at the end.
+ text = text.left(-6) # Remove the at the end.
for child_idx in svg_tag.get_child_count():
text += _tag_to_text(svg_tag.get_tag(PackedInt32Array([child_idx])))
return text + ""
-static func svg_to_text(tag: TagSVG) -> String:
+static func root_to_text(tag: TagRoot) -> String:
+ var text := _tag_to_text(tag).trim_suffix('\n')
if GlobalSettings.xml_add_trailing_newline:
- return _tag_to_text(tag) + "\n"
- else:
- return _tag_to_text(tag)
+ text += "\n"
+ return text
static func _tag_to_text(tag: Tag) -> String:
var text := ""
+ if GlobalSettings.xml_pretty_formatting:
+ text += '\t'.repeat(tag.xid.size())
text += '<' + tag.name
for attribute_key in tag.attributes:
var attribute: Attribute = tag.attributes[attribute_key]
var value := attribute.get_value()
- if value.is_empty():
- continue
-
if not '"' in value:
text += ' %s="%s"' % [attribute_key, value]
else:
text += " %s='%s'" % [attribute_key, value]
- for attribute in tag.unknown_attributes:
- var value := attribute.get_value()
- if not '"' in value:
- text += ' %s="%s"' % [attribute.name, value]
- else:
- text += " %s='%s'" % [attribute.name, value]
-
if tag.is_standalone() and GlobalSettings.xml_shorthand_tags and\
not tag.name in shorthand_tag_exceptions:
text += '/>'
+ if GlobalSettings.xml_pretty_formatting:
+ text += '\n'
else:
text += '>'
+ if GlobalSettings.xml_pretty_formatting:
+ text += '\n'
for child_tag in tag.child_tags:
text += _tag_to_text(child_tag)
- text += '' + tag.name + '>'
+ if GlobalSettings.xml_pretty_formatting:
+ text += '\t'.repeat(tag.xid.size())
+ text += '%s>' % tag.name
+ if GlobalSettings.xml_pretty_formatting:
+ text += '\n'
return text
@@ -60,7 +60,7 @@ enum ParseError {OK, ERR_NOT_SVG, ERR_IMPROPER_NESTING}
class ParseResult extends RefCounted:
var error: SVGParser.ParseError
- var svg: TagSVG
+ var svg: TagRoot
func _init(err_id: SVGParser.ParseError, result: TagSVG = null) -> void:
error = err_id
@@ -74,11 +74,14 @@ static func get_error_string(parse_error: ParseError) -> String:
return TranslationServer.translate("Improper nesting.")
_: return ""
-static func text_to_svg(text: String) -> ParseResult:
+static func text_to_root(text: String) -> ParseResult:
if text.is_empty():
return ParseResult.new(ParseError.ERR_NOT_SVG)
- var svg_tag := TagSVG.new()
+ var root_tag := TagRoot.new()
+ root_tag.xid = PackedInt32Array()
+ root_tag.root = root_tag
+ root_tag.svg = null
var parser := XMLParser.new()
parser.open_buffer(text.to_utf8_buffer())
var unclosed_tag_stack: Array[Tag] = []
@@ -97,34 +100,17 @@ static func text_to_svg(text: String) -> ParseResult:
var attrib_dict := {}
for i in parser.get_attribute_count():
attrib_dict[parser.get_attribute_name(i)] = parser.get_attribute_value(i)
- # width, height, and viewBox don't have defaults.
- if attrib_dict.has("width"):
- svg_tag.attributes.width.set_value(attrib_dict["width"],
- Attribute.SyncMode.SILENT)
- if attrib_dict.has("height"):
- svg_tag.attributes.height.set_value(attrib_dict["height"],
- Attribute.SyncMode.SILENT)
- if attrib_dict.has("viewBox"):
- svg_tag.attributes.viewBox.set_value(attrib_dict["viewBox"],
- Attribute.SyncMode.SILENT)
- svg_tag.update_cache()
- var unknown: Array[Attribute] = []
for element in attrib_dict:
- if svg_tag.attributes.has(element):
- var attribute: Attribute = svg_tag.attributes[element]
- attribute.set_value(attrib_dict[element], Attribute.SyncMode.SILENT)
- else:
- unknown.append(Attribute.new(element, attrib_dict[element]))
- svg_tag.set_unknown_attributes(unknown)
+ root_tag.set_attribute(element, attrib_dict[element])
var node_offset := parser.get_node_offset()
var closure_pos := text.find("/>", node_offset)
if closure_pos == -1 or closure_pos > text.find(">", node_offset):
- unclosed_tag_stack.append(svg_tag)
+ unclosed_tag_stack.append(root_tag)
break
else:
- return ParseResult.new(ParseError.OK, svg_tag)
+ return ParseResult.new(ParseError.OK, root_tag)
if not describes_svg:
return ParseResult.new(ParseError.ERR_NOT_SVG)
@@ -138,32 +124,23 @@ static func text_to_svg(text: String) -> ParseResult:
for i in parser.get_attribute_count():
attrib_dict[parser.get_attribute_name(i)] = parser.get_attribute_value(i)
- var tag: Tag
- match node_name:
- "circle": tag = TagCircle.new()
- "ellipse": tag = TagEllipse.new()
- "rect": tag = TagRect.new()
- "path": tag = TagPath.new()
- "line": tag = TagLine.new()
- "stop": tag = TagStop.new()
- _: tag = TagUnknown.new(node_name)
+ var tag := DB.tag(node_name)
+ tag.set_parent(unclosed_tag_stack.back())
+ tag.xid = tag.parent.xid.duplicate()
+ tag.xid.append(tag.parent.get_child_count())
- var unknown: Array[Attribute] = []
for element in attrib_dict:
- if tag.attributes.has(element):
- var attribute: Attribute = tag.attributes[element]
- attribute.set_value(attrib_dict[element], Attribute.SyncMode.SILENT)
- else:
- unknown.append(Attribute.new(element, attrib_dict[element]))
- tag.set_unknown_attributes(unknown)
+ tag.set_attribute(element, attrib_dict[element])
# Check if we're entering or exiting the tag.
var node_offset := parser.get_node_offset()
var closure_pos := text.find("/>", node_offset)
+
if closure_pos == -1 or closure_pos > text.find(">", node_offset):
unclosed_tag_stack.append(tag)
else:
unclosed_tag_stack.back().child_tags.append(tag)
+
XMLParser.NODE_ELEMENT_END:
if unclosed_tag_stack.is_empty():
return ParseResult.new(ParseError.ERR_IMPROPER_NESTING)
@@ -179,4 +156,4 @@ static func text_to_svg(text: String) -> ParseResult:
if not unclosed_tag_stack.is_empty():
return ParseResult.new(ParseError.ERR_IMPROPER_NESTING)
- return ParseResult.new(ParseError.OK, svg_tag)
+ return ParseResult.new(ParseError.OK, root_tag)
diff --git a/src/parsers/TransformListParser.gd b/src/parsers/TransformListParser.gd
index ce4f8892..5351cb2d 100644
--- a/src/parsers/TransformListParser.gd
+++ b/src/parsers/TransformListParser.gd
@@ -1,48 +1,48 @@
class_name TransformListParser extends RefCounted
static func transform_list_to_text(
-transform_list: Array[AttributeTransform.Transform]) -> String:
+transform_list: Array[Transform]) -> String:
var output := ""
var num_parser := NumberArrayParser.new()
- num_parser.compress_numbers = GlobalSettings.transform_compress_numbers
- num_parser.minimize_spacing = GlobalSettings.transform_minimize_spacing
+ num_parser.compress_numbers = GlobalSettings.transform_list_compress_numbers
+ num_parser.minimize_spacing = GlobalSettings.transform_list_minimize_spacing
for t in transform_list:
- if t is AttributeTransform.TransformMatrix:
+ if t is Transform.TransformMatrix:
output += "matrix(%s) " % num_parser.numstr_arr_to_text([
num_parser.num_to_text(t.x1), num_parser.num_to_text(t.x2),
num_parser.num_to_text(t.y1), num_parser.num_to_text(t.y2),
num_parser.num_to_text(t.o1), num_parser.num_to_text(t.o2)])
- elif t is AttributeTransform.TransformTranslate:
- if t.y == 0 and GlobalSettings.transform_remove_unnecessary_params:
+ elif t is Transform.TransformTranslate:
+ if t.y == 0 and GlobalSettings.transform_list_remove_unnecessary_params:
output += "translate(%s) " % num_parser.num_to_text(t.x)
else:
output += "translate(%s) " % num_parser.numstr_arr_to_text([
num_parser.num_to_text(t.x), num_parser.num_to_text(t.y)])
- elif t is AttributeTransform.TransformRotate:
- if t.x == 0 and t.y == 0 and GlobalSettings.transform_remove_unnecessary_params:
+ elif t is Transform.TransformRotate:
+ if t.x == 0 and t.y == 0 and GlobalSettings.transform_list_remove_unnecessary_params:
output += "rotate(%s) " % num_parser.num_to_text(t.deg, true)
else:
output += "rotate(%s) " % num_parser.numstr_arr_to_text([
num_parser.num_to_text(t.deg, true),
num_parser.num_to_text(t.x), num_parser.num_to_text(t.y)])
- elif t is AttributeTransform.TransformScale:
- if t.x == t.y and GlobalSettings.transform_remove_unnecessary_params:
+ elif t is Transform.TransformScale:
+ if t.x == t.y and GlobalSettings.transform_list_remove_unnecessary_params:
output += "scale(%s) " % num_parser.num_to_text(t.x)
else:
output += "scale(%s) " % num_parser.numstr_arr_to_text([
num_parser.num_to_text(t.x), num_parser.num_to_text(t.y)])
- elif t is AttributeTransform.TransformSkewX:
+ elif t is Transform.TransformSkewX:
output += "skewX(%s) " % num_parser.num_to_text(t.x, true)
- elif t is AttributeTransform.TransformSkewY:
+ elif t is Transform.TransformSkewY:
output += "skewY(%s) " % num_parser.num_to_text(t.y, true)
return output.trim_suffix(" ")
-static func text_to_transform_list(text: String) -> Array[AttributeTransform.Transform]:
+static func text_to_transform_list(text: String) -> Array[Transform]:
if text.is_empty():
return []
- var output: Array[AttributeTransform.Transform] = []
+ var output: Array[Transform] = []
text = text.strip_edges()
var transforms := text.split(")", false)
for transform in transforms:
@@ -127,40 +127,40 @@ static func text_to_transform_list(text: String) -> Array[AttributeTransform.Tra
match transform_info[0].strip_edges():
"matrix":
if nums.size() == 6:
- output.append(AttributeTransform.TransformMatrix.new(nums[0], nums[1],
+ output.append(Transform.TransformMatrix.new(nums[0], nums[1],
nums[2], nums[3], nums[4], nums[5]))
else:
return []
"translate":
if nums.size() == 1:
- output.append(AttributeTransform.TransformTranslate.new(nums[0], 0.0))
+ output.append(Transform.TransformTranslate.new(nums[0], 0.0))
elif nums.size() == 2:
- output.append(AttributeTransform.TransformTranslate.new(nums[0], nums[1]))
+ output.append(Transform.TransformTranslate.new(nums[0], nums[1]))
else:
return []
"rotate":
if nums.size() == 1:
- output.append(AttributeTransform.TransformRotate.new(nums[0], 0.0, 0.0))
+ output.append(Transform.TransformRotate.new(nums[0], 0.0, 0.0))
elif nums.size() == 3:
- output.append(AttributeTransform.TransformRotate.new(
+ output.append(Transform.TransformRotate.new(
nums[0], nums[1], nums[2]))
else:
return []
"scale":
if nums.size() == 1:
- output.append(AttributeTransform.TransformScale.new(nums[0], nums[0]))
+ output.append(Transform.TransformScale.new(nums[0], nums[0]))
elif nums.size() == 2:
- output.append(AttributeTransform.TransformScale.new(nums[0], nums[1]))
+ output.append(Transform.TransformScale.new(nums[0], nums[1]))
else:
return []
"skewX":
if nums.size() == 1:
- output.append(AttributeTransform.TransformSkewX.new(nums[0]))
+ output.append(Transform.TransformSkewX.new(nums[0]))
else:
return []
"skewY":
if nums.size() == 1:
- output.append(AttributeTransform.TransformSkewY.new(nums[0]))
+ output.append(Transform.TransformSkewY.new(nums[0]))
else:
return []
_:
diff --git a/src/ui_elements/BetterLineEdit.gd b/src/ui_elements/BetterLineEdit.gd
index 87109039..a6bb3eb3 100644
--- a/src/ui_elements/BetterLineEdit.gd
+++ b/src/ui_elements/BetterLineEdit.gd
@@ -71,6 +71,7 @@ func _input(event: InputEvent) -> void:
return
if event is InputEventMouseButton:
+ # TODO This causes part of #798.
if event.is_pressed() and not get_global_rect().has_point(event.position) and\
HandlerGUI.popup_overlay_stack.is_empty():
release_focus()
diff --git a/src/ui_elements/context_popup.gd b/src/ui_elements/ContextPopup.gd
similarity index 92%
rename from src/ui_elements/context_popup.gd
rename to src/ui_elements/ContextPopup.gd
index 709b89b2..13dbba84 100644
--- a/src/ui_elements/context_popup.gd
+++ b/src/ui_elements/ContextPopup.gd
@@ -15,7 +15,7 @@ icon: Texture2D = null, shortcut := "") -> Button:
if not shortcut.is_empty():
if not InputMap.has_action(shortcut):
- printerr("A non-existent shortcut was passed to ContextPopup.create_button().")
+ push_error("Non-existent shortcut was passed to ContextPopup.create_button().")
elif InputMap.has_action(shortcut):
var events := InputMap.action_get_events(shortcut)
if not events.is_empty():
@@ -26,11 +26,10 @@ icon: Texture2D = null, shortcut := "") -> Button:
if disabled:
main_button.disabled = true
ret_button.disabled = true
- main_button.add_theme_stylebox_override("disabled",
- main_button.get_theme_stylebox("normal", "ContextButton"))
else:
ret_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
- main_button.add_theme_stylebox_override("normal",
+ for theme_item in ["normal", "hover", "pressed", "disabled"]:
+ main_button.add_theme_stylebox_override(theme_item,
main_button.get_theme_stylebox("normal", "ContextButton"))
var internal_hbox := HBoxContainer.new()
main_button.mouse_filter = Control.MOUSE_FILTER_IGNORE # Unpressable.
@@ -43,8 +42,10 @@ icon: Texture2D = null, shortcut := "") -> Button:
var label := Label.new()
label.text = events[0].as_text_keycode()
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
- label.add_theme_color_override("font_color",
- ThemeGenerator.common_subtle_text_color)
+ var shortcut_text_color := ThemeGenerator.common_subtle_text_color
+ if disabled:
+ shortcut_text_color.a *= 0.75
+ label.add_theme_color_override("font_color", shortcut_text_color)
label.add_theme_font_size_override("font_size",
main_button.get_theme_font_size("font_size"))
@@ -80,7 +81,7 @@ start_pressed: bool, shortcut := "") -> CheckBox:
if not shortcut.is_empty():
if not InputMap.has_action(shortcut):
- printerr("A non-existent shortcut was passed to ContextPopup.create_checkbox().")
+ push_error("Non-existent shortcut was passed to ContextPopup.create_checkbox().")
elif InputMap.has_action(shortcut):
var events := InputMap.action_get_events(shortcut)
if not events.is_empty():
@@ -102,8 +103,10 @@ start_pressed: bool, shortcut := "") -> CheckBox:
var label := Label.new()
label.text = events[0].as_text_keycode()
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
- label.add_theme_color_override("font_color",
- ThemeGenerator.common_subtle_text_color)
+ var shortcut_text_color := ThemeGenerator.common_subtle_text_color
+ #if disabled:
+ #shortcut_text_color.a *= 0.75
+ label.add_theme_color_override("font_color", shortcut_text_color)
label.add_theme_font_size_override("font_size",
checkbox.get_theme_font_size("font_size"))
@@ -176,7 +179,7 @@ min_width := -1.0, separator_indices: Array[int] = []) -> void:
stylebox.content_margin_left = 8
stylebox.content_margin_right = 8
stylebox.border_width_bottom = 2
- stylebox.border_color = ThemeGenerator.common_separator_color
+ stylebox.border_color = ThemeGenerator.common_panel_border_color
title_container.add_theme_stylebox_override("panel", stylebox)
var title_label := Label.new()
title_label.text = top_title
diff --git a/src/ui_elements/LineEditButton.gd b/src/ui_elements/LineEditButton.gd
index d5e20d8b..0686a599 100644
--- a/src/ui_elements/LineEditButton.gd
+++ b/src/ui_elements/LineEditButton.gd
@@ -117,9 +117,9 @@ func _setup() -> void:
if font_color != Color.TRANSPARENT:
temp_line_edit.add_theme_color_override("font_color", _get_font_color())
temp_line_edit.add_theme_font_override("font", _get_font())
- temp_line_edit.text_change_canceled.connect(emit_text_change_canceled)
- temp_line_edit.text_changed.connect(emit_text_changed)
- temp_line_edit.text_submitted.connect(emit_text_submitted)
+ temp_line_edit.text_change_canceled.connect(text_change_canceled.emit)
+ temp_line_edit.text_changed.connect(text_changed.emit)
+ temp_line_edit.text_submitted.connect(text_submitted.emit)
temp_line_edit.focus_entered.connect(_on_underlying_control_focused)
temp_line_edit.focus_exited.connect(_on_underlying_control_unfocused)
add_child(temp_line_edit)
@@ -134,8 +134,8 @@ func _setup() -> void:
temp_button.theme_type_variation = "LeftConnectedButton"
else:
temp_button.flat = true
- temp_button.pressed.connect(emit_pressed)
- temp_button.gui_input.connect(emit_button_gui_input)
+ temp_button.pressed.connect(pressed.emit)
+ temp_button.gui_input.connect(button_gui_input.emit)
temp_button.button_down.connect(_on_underlying_control_focused)
temp_button.button_up.connect(_on_underlying_control_unfocused)
add_child(temp_button)
@@ -176,22 +176,6 @@ func _draw() -> void:
(size.y - icon_side) / 2, icon_side, icon_side), false)
-func emit_pressed() -> void:
- pressed.emit()
-
-func emit_text_change_canceled() -> void:
- text_change_canceled.emit()
-
-func emit_text_changed(new_text: String) -> void:
- text_changed.emit(new_text)
-
-func emit_text_submitted(new_text: String) -> void:
- text_submitted.emit(new_text)
-
-func emit_button_gui_input(event: InputEvent) -> void:
- button_gui_input.emit(event)
-
-
# Helpers
func _get_font() -> Font:
diff --git a/src/ui_elements/color_field.gd b/src/ui_elements/color_field.gd
index d2fe6f8a..2d7068c4 100644
--- a/src/ui_elements/color_field.gd
+++ b/src/ui_elements/color_field.gd
@@ -1,14 +1,16 @@
# An editor to be tied to a color attribute.
extends LineEditButton
-var attribute: AttributeColor
+var tag: Tag
+var attribute_name: String
const ColorPopup = preload("res://src/ui_elements/color_popup.tscn")
const checkerboard = preload("res://visual/icons/backgrounds/ColorButtonBG.svg")
@onready var color_popup: Control
-func set_value(new_value: String, update_type := Utils.UpdateType.REGULAR) -> void:
+func set_value(new_value: String, save := true) -> void:
+ var attribute := tag.get_attribute(attribute_name)
if not new_value.is_empty():
# Validate the value.
if not is_valid(new_value):
@@ -18,28 +20,35 @@ func set_value(new_value: String, update_type := Utils.UpdateType.REGULAR) -> vo
sync(attribute.format(new_value))
# Update the attribute.
- if attribute.get_value() != new_value or update_type == Utils.UpdateType.FINAL:
- match update_type:
- Utils.UpdateType.INTERMEDIATE:
- attribute.set_value(new_value, Attribute.SyncMode.INTERMEDIATE)
- Utils.UpdateType.FINAL:
- attribute.set_value(new_value, Attribute.SyncMode.FINAL)
- _:
- attribute.set_value(new_value)
+ if attribute.get_value() != new_value:
+ attribute.set_value(new_value, save)
+
+func setup_default() -> void:
+ placeholder_text = tag.get_default(attribute_name)
func _ready() -> void:
- set_value(attribute.get_value())
- tooltip_text = attribute.name
- placeholder_text = attribute.get_default()
- attribute.value_changed.connect(set_value)
+ set_value(tag.get_attribute_value(attribute_name, true))
+ tag.attribute_changed.connect(_on_tag_attribute_changed)
+ if attribute_name in DB.propagated_attributes:
+ tag.ancestor_attribute_changed.connect(_on_tag_ancestor_attribute_changed)
text_submitted.connect(set_value)
focus_entered.connect(reset_font_color)
+ tooltip_text = attribute_name
+ setup_default()
+
+
+func _on_tag_attribute_changed(attribute_changed: String) -> void:
+ if attribute_name == attribute_changed:
+ set_value(tag.get_attribute_value(attribute_name, true), false)
+func _on_tag_ancestor_attribute_changed(attribute_changed: String) -> void:
+ if attribute_name == attribute_changed:
+ setup_default()
func _on_pressed() -> void:
color_popup = ColorPopup.instantiate()
- color_popup.current_value = attribute.get_value()
+ color_popup.current_value = tag.get_attribute(attribute_name).get_value()
color_popup.color_picked.connect(_on_color_picked)
HandlerGUI.popup_under_rect(color_popup, get_global_rect(), get_viewport())
@@ -48,7 +57,7 @@ func _draw() -> void:
var stylebox := StyleBoxFlat.new()
stylebox.corner_radius_top_right = 5
stylebox.corner_radius_bottom_right = 5
- stylebox.bg_color = attribute.get_color()
+ stylebox.bg_color = tag.get_attribute(attribute_name).get_color()
checkerboard.draw(ci, Vector2(size.x - BUTTON_WIDTH, 1))
stylebox.draw(ci, Rect2(size.x - BUTTON_WIDTH, 1, BUTTON_WIDTH - 1, size.y - 2))
if is_instance_valid(temp_button) and temp_button.button_pressed:
@@ -61,15 +70,15 @@ func _draw() -> void:
func _on_text_change_canceled() -> void:
- sync(attribute.get_value())
+ sync(tag.get_attribute(attribute_name).get_value())
func _on_color_picked(new_color: String, close_picker: bool) -> void:
if close_picker:
color_popup.queue_free()
- set_value(new_color, Utils.UpdateType.FINAL)
+ set_value(new_color, true)
else:
- set_value(new_color, Utils.UpdateType.INTERMEDIATE)
+ set_value(new_color, false)
func is_valid(color_text: String) -> bool:
return ColorParser.is_valid(ColorParser.add_hash_if_hex(color_text))
@@ -80,7 +89,7 @@ func _on_text_changed(new_text: String) -> void:
func sync(new_value: String) -> void:
reset_font_color()
- if new_value == attribute.get_default():
+ if new_value == tag.get_default(attribute_name):
font_color = GlobalSettings.basic_color_warning
text = new_value.trim_prefix("#")
queue_redraw()
diff --git a/src/ui_elements/color_swatch_config.gd b/src/ui_elements/color_swatch_config.gd
index dbf11635..dc20276a 100644
--- a/src/ui_elements/color_swatch_config.gd
+++ b/src/ui_elements/color_swatch_config.gd
@@ -44,7 +44,7 @@ func _draw() -> void:
# Draw the drag-and-drop indicator.
var drop_sb := StyleBoxFlat.new()
drop_sb.draw_center = false
- drop_sb.border_color = Color.YELLOW
+ drop_sb.border_color = Color.GREEN
drop_sb.set_corner_radius_all(3)
if drop_idx == idx:
drop_sb.border_width_left = 2
diff --git a/src/ui_elements/enum_field.gd b/src/ui_elements/enum_field.gd
index 9a683db5..67240df2 100644
--- a/src/ui_elements/enum_field.gd
+++ b/src/ui_elements/enum_field.gd
@@ -1,41 +1,39 @@
# An editor to be tied to an enum attribute.
extends LineEditButton
-var attribute: AttributeEnum
+var tag: Tag
+var attribute_name: String
const bold_font = preload("res://visual/fonts/FontBold.ttf")
const reload_icon = preload("res://visual/icons/Reload.svg")
-func set_value(new_value: String, update_type := Utils.UpdateType.REGULAR) -> void:
+func set_value(new_value: String, save := true) -> void:
sync(new_value)
- if attribute.get_value() != new_value or update_type == Utils.UpdateType.FINAL:
- match update_type:
- Utils.UpdateType.INTERMEDIATE:
- attribute.set_value(new_value, Attribute.SyncMode.INTERMEDIATE)
- Utils.UpdateType.FINAL:
- attribute.set_value(new_value, Attribute.SyncMode.FINAL)
- _:
- attribute.set_value(new_value)
+ var attribute := tag.get_attribute(attribute_name)
+ if attribute.get_value() != new_value:
+ attribute.set_value(new_value, save)
func _ready() -> void:
+ var attribute: AttributeEnum = tag.get_attribute(attribute_name)
set_value(attribute.get_value())
- tooltip_text = attribute.name
- placeholder_text = attribute.get_default()
+ attribute.value_changed.connect(set_value)
+ tooltip_text = attribute_name
+ placeholder_text = tag.get_default(attribute_name)
focus_entered.connect(reset_font_color)
func _on_pressed() -> void:
var btn_arr: Array[Button] = []
# Add a default.
var reset_btn := ContextPopup.create_button("", set_value.bind(""),
- attribute.get_value().is_empty(), reload_icon)
+ tag.get_attribute(attribute_name).get_value().is_empty(), reload_icon)
reset_btn.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
btn_arr.append(reset_btn)
# Add a button for each enum value.
- for enum_constant in DB.attribute_enum_values[attribute.name]:
+ for enum_constant in DB.attribute_enum_values[attribute_name]:
var btn := ContextPopup.create_button(enum_constant, set_value.bind(enum_constant),
- enum_constant == attribute.get_value())
- if enum_constant == attribute.get_default():
+ enum_constant == tag.get_attribute(attribute_name).get_value())
+ if enum_constant == tag.get_default(attribute_name):
btn.add_theme_font_override("font", bold_font)
btn_arr.append(btn)
var value_picker := ContextPopup.new()
@@ -44,23 +42,23 @@ func _on_pressed() -> void:
func _on_text_submitted(new_text: String) -> void:
- if new_text.is_empty() or new_text in DB.attribute_enum_values[attribute.name]:
+ if new_text.is_empty() or new_text in DB.attribute_enum_values[attribute_name]:
set_value(new_text)
else:
- sync(attribute.get_value())
+ sync(tag.get_attribute(attribute_name).get_value())
func _on_text_change_canceled() -> void:
- sync(attribute.get_value())
+ sync(tag.get_attribute(attribute_name).get_value())
func _on_text_changed(new_text: String) -> void:
font_color = GlobalSettings.get_validity_color(
- not new_text in DB.attribute_enum_values[attribute.name])
+ not new_text in DB.attribute_enum_values[attribute_name])
func sync(new_value: String) -> void:
text = new_value
reset_font_color()
- if new_value == attribute.get_default():
+ if new_value == tag.get_default(attribute_name):
font_color = GlobalSettings.basic_color_warning
func _notification(what: int) -> void:
diff --git a/src/ui_elements/flag_field.tscn b/src/ui_elements/flag_field.tscn
index a60bc271..6d28f146 100644
--- a/src/ui_elements/flag_field.tscn
+++ b/src/ui_elements/flag_field.tscn
@@ -1,4 +1,4 @@
-[gd_scene load_steps=6 format=3 uid="uid://br8g7w38jguh4"]
+[gd_scene load_steps=7 format=3 uid="uid://br8g7w38jguh4"]
[ext_resource type="FontFile" uid="uid://dtb4wkus51hxs" path="res://visual/fonts/FontMono.ttf" id="1_p8s8y"]
[ext_resource type="Script" path="res://src/ui_elements/flag_field.gd" id="2_0bhg4"]
@@ -8,6 +8,8 @@ base_font = ExtResource("1_p8s8y")
spacing_top = -1
spacing_bottom = -1
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_kfgda"]
+
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_oo47u"]
content_margin_left = 5.0
content_margin_top = 0.0
@@ -37,13 +39,14 @@ offset_bottom = 18.0
focus_mode = 0
mouse_filter = 1
mouse_default_cursor_shape = 2
-theme_override_colors/font_color = Color(1, 0.75, 0.75, 1)
theme_override_colors/font_pressed_color = Color(0.74902, 1, 0.74902, 1)
+theme_override_colors/font_color = Color(1, 0.75, 0.75, 1)
theme_override_fonts/font = SubResource("FontVariation_46ud6")
theme_override_font_sizes/font_size = 14
-theme_override_styles/normal = SubResource("StyleBoxFlat_oo47u")
+theme_override_styles/disabled = SubResource("StyleBoxEmpty_kfgda")
theme_override_styles/hover = SubResource("StyleBoxFlat_oo47u")
theme_override_styles/pressed = SubResource("StyleBoxFlat_uepoa")
+theme_override_styles/normal = SubResource("StyleBoxFlat_oo47u")
toggle_mode = true
text = "0"
script = ExtResource("2_0bhg4")
diff --git a/src/ui_elements/id_field.gd b/src/ui_elements/id_field.gd
new file mode 100644
index 00000000..cf000f74
--- /dev/null
+++ b/src/ui_elements/id_field.gd
@@ -0,0 +1,45 @@
+# An editor to be tied to a numeric attribute.
+extends BetterLineEdit
+
+var tag: Tag
+var attribute_name: String
+
+func set_value(new_value: String, save := true) -> void:
+ var attribute := tag.get_attribute(attribute_name)
+ if not new_value.is_empty():
+ sync(attribute.format(new_value))
+
+ # Update the attribute.
+ if attribute.get_value() != new_value:
+ attribute.set_value(new_value, save)
+
+
+func _ready() -> void:
+ super()
+ var attribute: AttributeID = tag.get_attribute(attribute_name)
+ set_value(attribute.get_value())
+ attribute.value_changed.connect(set_value)
+ tooltip_text = attribute_name
+ placeholder_text = tag.get_default(attribute_name)
+ text_submitted.connect(set_value)
+
+func _on_focus_entered() -> void:
+ remove_theme_color_override("font_color")
+
+func _on_text_change_canceled() -> void:
+ sync(tag.get_attribute(attribute_name).get_value())
+
+func _on_text_changed(new_text: String) -> void:
+ var validity_level := IDParser.get_validity(new_text)
+ var font_color := GlobalSettings.get_validity_color(
+ validity_level == IDParser.ValidityLevel.INVALID,
+ validity_level == IDParser.ValidityLevel.INVALID_XML_NAMETOKEN)
+ add_theme_color_override("font_color", font_color)
+
+func sync(new_value: String) -> void:
+ text = new_value
+ remove_theme_color_override("font_color")
+
+func _notification(what: int) -> void:
+ if what == Utils.CustomNotification.BASIC_COLORS_CHANGED:
+ sync(text)
diff --git a/src/ui_elements/id_field.tscn b/src/ui_elements/id_field.tscn
new file mode 100644
index 00000000..45f1c31f
--- /dev/null
+++ b/src/ui_elements/id_field.tscn
@@ -0,0 +1,15 @@
+[gd_scene load_steps=2 format=3 uid="uid://6tbfb2t0tyfh"]
+
+[ext_resource type="Script" path="res://src/ui_elements/id_field.gd" id="1_dqgkn"]
+
+[node name="IdField" type="LineEdit"]
+custom_minimum_size = Vector2(54, 22)
+offset_right = 35.8125
+offset_bottom = 21.0
+size_flags_horizontal = 0
+size_flags_vertical = 0
+script = ExtResource("1_dqgkn")
+
+[connection signal="focus_entered" from="." to="." method="_on_focus_entered"]
+[connection signal="text_change_canceled" from="." to="." method="_on_text_change_canceled"]
+[connection signal="text_changed" from="." to="." method="_on_text_changed"]
diff --git a/src/ui_elements/number_field.gd b/src/ui_elements/number_field.gd
index 7ac90138..ee0d5c51 100644
--- a/src/ui_elements/number_field.gd
+++ b/src/ui_elements/number_field.gd
@@ -1,14 +1,16 @@
# An editor to be tied to a numeric attribute.
extends BetterLineEdit
-var attribute: AttributeNumeric
+var tag: Tag
+var attribute_name: String
var min_value := 0.0
var max_value := 1.0
var allow_lower := true
var allow_higher := true
-func set_value(new_value: String, update_type := Utils.UpdateType.REGULAR) -> void:
+func set_value(new_value: String, save := true) -> void:
+ var attribute := tag.get_attribute(attribute_name)
if not new_value.is_empty():
var numeric_value := NumberParser.evaluate(new_value)
# Validate the value.
@@ -27,34 +29,29 @@ func set_value(new_value: String, update_type := Utils.UpdateType.REGULAR) -> vo
sync(attribute.format(new_value))
# Update the attribute.
- if new_value != attribute.get_value() or update_type == Utils.UpdateType.FINAL:
- match update_type:
- Utils.UpdateType.INTERMEDIATE:
- attribute.set_value(new_value, Attribute.SyncMode.INTERMEDIATE)
- Utils.UpdateType.FINAL:
- attribute.set_value(new_value, Attribute.SyncMode.FINAL)
- _:
- attribute.set_value(new_value)
+ if attribute.get_value() != new_value:
+ attribute.set_value(new_value, save)
func _ready() -> void:
- set_value(attribute.get_value())
super()
+ var attribute: AttributeNumeric = tag.get_attribute(attribute_name)
+ set_value(attribute.get_value())
attribute.value_changed.connect(set_value)
- tooltip_text = attribute.name
- placeholder_text = attribute.get_default()
+ tooltip_text = attribute_name
+ placeholder_text = tag.get_default(attribute_name)
text_submitted.connect(set_value)
func _on_focus_entered() -> void:
remove_theme_color_override("font_color")
func _on_text_change_canceled() -> void:
- sync(attribute.get_value())
+ sync(tag.get_attribute(attribute_name).get_value())
func sync(new_value: String) -> void:
text = new_value
remove_theme_color_override("font_color")
- if new_value == attribute.get_default():
+ if new_value == tag.get_default(attribute_name):
add_theme_color_override("font_color", GlobalSettings.basic_color_warning)
func _notification(what: int) -> void:
diff --git a/src/ui_elements/number_field_with_slider.gd b/src/ui_elements/number_field_with_slider.gd
index 3d20ad5e..d81c9762 100644
--- a/src/ui_elements/number_field_with_slider.gd
+++ b/src/ui_elements/number_field_with_slider.gd
@@ -1,7 +1,8 @@
# An editor to be tied to a numeric attribute, plus a slider widget.
extends LineEditButton
-var attribute: AttributeNumeric
+var tag: Tag
+var attribute_name: String
var slider_step := 0.01
var min_value := 0.0
@@ -9,7 +10,8 @@ var max_value := 1.0
var allow_lower := true
var allow_higher := true
-func set_value(new_value: String, update_type := Utils.UpdateType.REGULAR) -> void:
+func set_value(new_value: String, save := true) -> void:
+ var attribute := tag.get_attribute(attribute_name)
if not new_value.is_empty():
var numeric_value := NumberParser.evaluate(new_value)
# Validate the value.
@@ -29,34 +31,29 @@ func set_value(new_value: String, update_type := Utils.UpdateType.REGULAR) -> vo
sync(attribute.format(new_value))
# Update the attribute.
- if new_value != attribute.get_value() or update_type == Utils.UpdateType.FINAL:
- match update_type:
- Utils.UpdateType.INTERMEDIATE:
- attribute.set_value(new_value, Attribute.SyncMode.INTERMEDIATE)
- Utils.UpdateType.FINAL:
- attribute.set_value(new_value, Attribute.SyncMode.FINAL)
- _:
- attribute.set_value(new_value)
+ if new_value != attribute.get_value():
+ attribute.set_value(new_value, save)
-func set_num(new_number: float, update_type := Utils.UpdateType.REGULAR) -> void:
- set_value(NumberParser.num_to_text(new_number), update_type)
+func set_num(new_number: float, save := true) -> void:
+ set_value(NumberParser.num_to_text(new_number), save)
func _ready() -> void:
+ var attribute: AttributeNumeric = tag.get_attribute(attribute_name)
set_value(attribute.get_value())
attribute.value_changed.connect(set_value)
- tooltip_text = attribute.name
- placeholder_text = attribute.get_default()
+ tooltip_text = attribute_name
+ placeholder_text = tag.get_default(attribute_name)
text_submitted.connect(set_value)
focus_entered.connect(reset_font_color)
func _on_text_change_canceled() -> void:
- sync(attribute.get_value())
+ set_value(tag.get_attribute(attribute_name).get_value())
func sync(new_value: String) -> void:
text = new_value
reset_font_color()
- if new_value == attribute.get_default():
+ if new_value == tag.get_default(attribute_name):
font_color = GlobalSettings.basic_color_warning
queue_redraw()
@@ -95,7 +92,8 @@ func _draw() -> void:
stylebox.bg_color = get_theme_stylebox("normal", "LineEdit").bg_color
stylebox.draw(ci, Rect2(get_size().x - BUTTON_WIDTH,
1, BUTTON_WIDTH - 2, get_size().y - 2))
- var fill_height := (get_size().y - 4) * (attribute.get_num() - min_value) / max_value
+ var fill_height: float = (get_size().y - 4) *\
+ (tag.get_attribute(attribute_name).get_num() - min_value) / max_value
# Create a stylebox that'll occupy the exact amount of space.
var fill_stylebox := StyleBoxFlat.new()
fill_stylebox.bg_color = Color("#def")
@@ -128,21 +126,21 @@ func _on_slider_gui_input(event: InputEvent) -> void:
slider_hovered = true
if Utils.is_event_drag_start(event):
slider_dragged = true
- initial_slider_value = attribute.get_num()
- set_num(get_slider_value_at_y(event.position.y), Utils.UpdateType.INTERMEDIATE)
+ initial_slider_value = tag.get_attribute(attribute_name).get_num()
+ set_num(get_slider_value_at_y(event.position.y), false)
else:
if Utils.is_event_drag(event):
- set_num(get_slider_value_at_y(event.position.y), Utils.UpdateType.INTERMEDIATE)
+ set_num(get_slider_value_at_y(event.position.y), false)
elif Utils.is_event_drag_end(event):
slider_dragged = false
var final_slider_value := get_slider_value_at_y(event.position.y)
if initial_slider_value != final_slider_value:
- set_num(final_slider_value, Utils.UpdateType.FINAL)
+ set_num(final_slider_value, true)
func _unhandled_input(event: InputEvent) -> void:
if slider_dragged and Utils.is_event_cancel(event):
slider_dragged = false
- set_num(initial_slider_value, Utils.UpdateType.INTERMEDIATE)
+ set_num(initial_slider_value, false)
accept_event()
func get_slider_value_at_y(y_coord: float) -> float:
diff --git a/src/ui_elements/palette_config.gd b/src/ui_elements/palette_config.gd
index 4f6c85bb..b9d87067 100644
--- a/src/ui_elements/palette_config.gd
+++ b/src/ui_elements/palette_config.gd
@@ -53,11 +53,7 @@ func rebuild_colors() -> void:
swatch.pressed.emit()
# Add the add button.
var fake_swatch := Button.new()
- fake_swatch.begin_bulk_theme_override()
- for stylebox_type in ["normal", "hover", "pressed"]:
- fake_swatch.add_theme_stylebox_override(stylebox_type,
- fake_swatch.get_theme_stylebox(stylebox_type, "Swatch"))
- fake_swatch.end_bulk_theme_override()
+ fake_swatch.theme_type_variation = "Swatch"
fake_swatch.icon = plus_icon
fake_swatch.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
fake_swatch.focus_mode = Control.FOCUS_NONE
diff --git a/src/ui_elements/pathdata_field.gd b/src/ui_elements/pathdata_field.gd
index 8e7abf86..229e83d8 100644
--- a/src/ui_elements/pathdata_field.gd
+++ b/src/ui_elements/pathdata_field.gd
@@ -1,6 +1,9 @@
# An editor to be tied to a pathdata attribute.
extends VBoxContainer
+var tag: Tag
+const attribute_name = "d"
+
# So, about this editor. Most of this code is about implementing a huge optimization.
# All the path commands are a single node that draws fake-outs in order to prevent
# adding too many nodes to the scene tree. The real controls are only created when
@@ -16,8 +19,6 @@ const COMMAND_HEIGHT = 22.0
@export var relative_button_pressed: StyleBoxFlat
signal focused
-var attribute: AttributePath
-var tid: PackedInt32Array # The path field has inner selectables, so it needs this.
const MiniNumberField = preload("mini_number_field.tscn")
const FlagField = preload("flag_field.tscn")
@@ -51,27 +52,25 @@ var current_hovered: int = -1
var add_move_button: Control
-func set_value(new_value: String, update_type := Utils.UpdateType.REGULAR) -> void:
- sync(attribute.format(new_value))
- if attribute.get_value() != new_value or update_type == Utils.UpdateType.FINAL:
- match update_type:
- Utils.UpdateType.INTERMEDIATE:
- attribute.set_value(new_value, Attribute.SyncMode.INTERMEDIATE)
- Utils.UpdateType.FINAL:
- attribute.set_value(new_value, Attribute.SyncMode.FINAL)
- _:
- attribute.set_value(new_value)
+func set_value(new_value: String, save := true) -> void:
+ sync(new_value)
+ var attribute := tag.get_attribute(attribute_name)
+ if attribute.get_value() != new_value:
+ attribute.set_value(new_value, save)
+
+func update_value() -> void:
+ set_value(tag.get_attribute(attribute_name).get_value())
func _notification(what: int) -> void:
if what == Utils.CustomNotification.LANGUAGE_CHANGED:
update_translation()
-func set_attribute(new_attribute: AttributePath) -> void:
- attribute = new_attribute
+func setup() -> void:
+ var attribute: AttributePathdata = tag.get_attribute(attribute_name)
set_value(attribute.get_value())
- attribute.value_changed.connect(set_value)
- line_edit.tooltip_text = attribute.name
+ attribute.value_changed.connect(update_value.unbind(2))
+ line_edit.tooltip_text = attribute_name
line_edit.text_submitted.connect(set_value)
line_edit.text_changed.connect(setup_font)
line_edit.focus_entered.connect(_on_line_edit_focus_entered)
@@ -97,7 +96,7 @@ func sync(new_value: String) -> void:
line_edit.text = new_value
setup_font(new_value)
# A plus button for adding a move command if empty.
- var cmd_count := attribute.get_command_count()
+ var cmd_count: int = tag.get_attribute(attribute_name).get_command_count()
if cmd_count == 0 and not is_instance_valid(add_move_button):
add_move_button = Button.new()
add_move_button.icon = plus_icon
@@ -107,11 +106,12 @@ func sync(new_value: String) -> void:
add_move_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
add_move_button.theme_type_variation = "FlatButton"
add_child(add_move_button)
- add_move_button.pressed.connect(attribute.insert_command.bind(0, "M"))
+ add_move_button.pressed.connect(
+ tag.get_attribute(attribute_name).insert_command.bind(0, "M"))
add_move_button.pressed.connect(add_move_button.queue_free)
# Rebuild the path commands.
commands_container.custom_minimum_size.y = cmd_count * COMMAND_HEIGHT
- if hovered_idx >= attribute.get_command_count():
+ if hovered_idx >= tag.get_attribute(attribute_name).get_command_count():
activate_hovered(-1)
var mm := InputEventMouseMotion.new()
mm.position = commands_container.get_local_mouse_position()
@@ -119,11 +119,11 @@ func sync(new_value: String) -> void:
commands_container.queue_redraw()
-func update_value(new_value: float, property: String, idx: int) -> void:
- attribute.set_command_property(idx, property, new_value)
+func update_parameter(new_value: float, property: String, idx: int) -> void:
+ tag.get_attribute(attribute_name).set_command_property(idx, property, new_value)
func _on_relative_button_pressed() -> void:
- attribute.toggle_relative_command(hovered_idx)
+ tag.get_attribute(attribute_name).toggle_relative_command(hovered_idx)
reactivate_hovered()
@@ -131,10 +131,10 @@ func _on_relative_button_pressed() -> void:
func _on_selections_or_hover_changed() -> void:
var new_selections: Array[int] = []
- if Indications.semi_selected_tid == tid:
+ if Indications.semi_selected_xid == tag.xid:
new_selections = Indications.inner_selections
var new_hovered: int = -1
- if Indications.semi_hovered_tid == tid:
+ if Indications.semi_hovered_xid == tag.xid:
new_hovered = Indications.inner_hovered
# Only redraw if selections or hovered changed.
if new_selections != current_selections:
@@ -147,8 +147,8 @@ func _on_selections_or_hover_changed() -> void:
func _on_commands_mouse_exited() -> void:
var cmd_idx := Indications.inner_hovered
- Indications.remove_hovered(tid, cmd_idx)
- if Indications.semi_hovered_tid == tid:
+ Indications.remove_hovered(tag.xid, cmd_idx)
+ if Indications.semi_hovered_xid == tag.xid:
activate_hovered(-1)
@@ -174,9 +174,9 @@ func respond_to_mouse_input(event: InputEventMouse) -> void:
if event is InputEventMouseMotion and event.button_mask == 0:
if cmd_idx != -1:
- Indications.set_hovered(tid, cmd_idx)
+ Indications.set_hovered(tag.xid, cmd_idx)
else:
- Indications.remove_hovered(tid, cmd_idx)
+ Indications.remove_hovered(tag.xid, cmd_idx)
activate_hovered(cmd_idx)
elif event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
@@ -184,40 +184,39 @@ func respond_to_mouse_input(event: InputEventMouse) -> void:
if event.double_click:
# Unselect the tags so we can use Ctrl select.
Indications.clear_inner_selection()
- var subpath_range: Vector2i =\
- SVG.root_tag.get_tag(tid).attributes.d.get_subpath(cmd_idx)
+ var subpath_range: Vector2i = tag.get_attribute("d").get_subpath(cmd_idx)
for idx in range(subpath_range.x, subpath_range.y + 1):
- Indications.ctrl_select(tid, idx)
+ Indications.ctrl_select(tag.xid, idx)
elif event.is_command_or_control_pressed():
- Indications.ctrl_select(tid, cmd_idx)
+ Indications.ctrl_select(tag.xid, cmd_idx)
elif event.shift_pressed:
- Indications.shift_select(tid, cmd_idx)
+ Indications.shift_select(tag.xid, cmd_idx)
elif not cmd_idx in Indications.inner_selections:
- Indications.normal_select(tid, cmd_idx)
+ Indications.normal_select(tag.xid, cmd_idx)
elif event.is_released() and not event.shift_pressed and\
not event.is_command_or_control_pressed() and not event.double_click and\
Indications.inner_selections.size() > 1 and\
cmd_idx in Indications.inner_selections:
- Indications.normal_select(tid, cmd_idx)
+ Indications.normal_select(tag.xid, cmd_idx)
elif event.button_index == MOUSE_BUTTON_RIGHT and event.is_pressed():
- if Indications.semi_selected_tid != tid or\
+ if Indications.semi_selected_xid != tag.xid or\
not cmd_idx in Indications.inner_selections:
- Indications.normal_select(tid, cmd_idx)
+ Indications.normal_select(tag.xid, cmd_idx)
# Popup the actions.
var viewport := get_viewport()
var popup_pos := viewport.get_mouse_position()
HandlerGUI.popup_under_pos(Indications.get_selection_context(
HandlerGUI.popup_under_pos.bind(popup_pos, viewport),
- Indications.SelectionContext.TAG_EDITOR), popup_pos, viewport)
+ Indications.Context.TAG_EDITOR), popup_pos, viewport)
func commands_draw() -> void:
RenderingServer.canvas_item_clear(ci)
- for i in attribute.get_command_count():
+ for i: int in tag.get_attribute(attribute_name).get_command_count():
var v_offset := COMMAND_HEIGHT * i
# Draw the background hover or selection stylebox.
- var hovered := Indications.is_hovered(tid, i)
- var selected := Indications.is_selected(tid, i)
+ var hovered := Indications.is_hovered(tag.xid, i)
+ var selected := Indications.is_selected(tag.xid, i)
if selected or hovered:
var stylebox := StyleBoxFlat.new()
stylebox.set_corner_radius_all(3)
@@ -233,7 +232,7 @@ func commands_draw() -> void:
if i == hovered_idx or i == focused_idx:
continue
- var cmd := attribute.get_command(i)
+ var cmd: PathCommand = tag.get_attribute(attribute_name).get_command(i)
var cmd_char := cmd.command_char
# Draw the action button.
more_icon.draw_rect(ci, Rect2(Vector2(commands_container.size.x - 19, 4 + v_offset),
@@ -303,7 +302,7 @@ path_command: PathCommand) -> void:
func activate_hovered(idx: int) -> void:
- if idx != hovered_idx and idx < attribute.get_command_count():
+ if idx != hovered_idx and idx < tag.get_attribute(attribute_name).get_command_count():
activate_hovered_shared_logic(idx)
func reactivate_hovered() -> void:
@@ -346,7 +345,7 @@ func setup_path_command_controls(idx: int) -> Control:
if idx < 0:
return null
- var cmd := attribute.get_command(idx)
+ var cmd: PathCommand = tag.get_attribute(attribute_name).get_command(idx)
var cmd_char := cmd.command_char
var is_absolute := Utils.is_string_upper(cmd_char)
@@ -364,11 +363,14 @@ func setup_path_command_controls(idx: int) -> Control:
relative_button.add_theme_font_override("font", code_font)
relative_button.add_theme_font_size_override("font_size", 13)
relative_button.add_theme_color_override("font_color", Color(1, 1, 1))
+ # Disabled styleboxes aren unused, but must be set for the correct content margins.
if is_absolute:
+ relative_button.add_theme_stylebox_override("disabled", absolute_button_normal)
relative_button.add_theme_stylebox_override("normal", absolute_button_normal)
relative_button.add_theme_stylebox_override("hover", absolute_button_hovered)
relative_button.add_theme_stylebox_override("pressed", absolute_button_pressed)
else:
+ relative_button.add_theme_stylebox_override("disabled", relative_button_normal)
relative_button.add_theme_stylebox_override("normal", relative_button_normal)
relative_button.add_theme_stylebox_override("hover", relative_button_hovered)
relative_button.add_theme_stylebox_override("pressed", relative_button_pressed)
@@ -445,7 +447,7 @@ func setup_path_command_controls(idx: int) -> Control:
var property_name := property_names[i]
field.set_value(cmd.get(property_name))
field.tooltip_text = property_name
- field.value_changed.connect(update_value.bind(property_name, idx))
+ field.value_changed.connect(update_parameter.bind(property_name, idx))
field.focus_entered.connect(activate_focused.bind(idx))
field.focus_exited.connect(check_focused, CONNECT_DEFERRED)
container.add_child(field)
@@ -458,16 +460,16 @@ func setup_path_command_controls(idx: int) -> Control:
func numfield(cmd_idx: int) -> BetterLineEdit:
var new_field := MiniNumberField.instantiate()
- new_field.focus_entered.connect(Indications.normal_select.bind(tid, cmd_idx))
+ new_field.focus_entered.connect(Indications.normal_select.bind(tag.xid, cmd_idx))
return new_field
func _on_action_button_pressed(action_button_ref: Button) -> void:
# Update the selection immediately, since if this path command is
# in a multi-selection, only the mouse button release would change the selection.
- Indications.normal_select(tid, hovered_idx)
+ Indications.normal_select(tag.xid, hovered_idx)
var viewport := get_viewport()
var action_button_rect := action_button_ref.get_global_rect()
HandlerGUI.popup_under_rect_center(Indications.get_selection_context(
HandlerGUI.popup_under_rect_center.bind(action_button_rect, viewport),
- Indications.SelectionContext.TAG_EDITOR), action_button_rect, viewport)
+ Indications.Context.TAG_EDITOR), action_button_rect, viewport)
diff --git a/src/ui_elements/setting_shortcut.gd b/src/ui_elements/setting_shortcut.gd
index e609a4b5..fad128e2 100644
--- a/src/ui_elements/setting_shortcut.gd
+++ b/src/ui_elements/setting_shortcut.gd
@@ -111,9 +111,13 @@ func cancel_listening() -> void:
func delete_shortcut(idx: int) -> void:
events.remove_at(idx)
- GlobalSettings.modify_keybind(action, events)
+ update_shortcut()
sync()
+func update_shortcut() -> void:
+ Utils.custom_notify(Utils.CustomNotification.SHORTCUTS_CHANGED)
+ GlobalSettings.modify_keybind(action, events)
+
func _input(event: InputEvent) -> void:
if not (listening_idx >= 0 and event is InputEventKey):
return
@@ -134,14 +138,14 @@ func _input(event: InputEvent) -> void:
elif event.is_released():
if listening_idx < events.size():
events[listening_idx] = event
- GlobalSettings.modify_keybind(action, events)
+ update_shortcut()
else:
events.append(event)
- GlobalSettings.modify_keybind(action, events)
+ update_shortcut()
sync()
listening_idx = -1
func _on_reset_button_pressed() -> void:
events = GlobalSettings.default_input_events[action].duplicate(true)
- GlobalSettings.modify_keybind(action, events)
+ update_shortcut()
sync()
diff --git a/src/ui_elements/tag_content_basic_shape.gd b/src/ui_elements/tag_content_basic_shape.gd
new file mode 100644
index 00000000..9c20e763
--- /dev/null
+++ b/src/ui_elements/tag_content_basic_shape.gd
@@ -0,0 +1,38 @@
+extends VBoxContainer
+
+const TransformField = preload("res://src/ui_elements/transform_field.tscn")
+const NumberField = preload("res://src/ui_elements/number_field.tscn")
+const NumberSlider = preload("res://src/ui_elements/number_field_with_slider.tscn")
+const ColorField = preload("res://src/ui_elements/color_field.tscn")
+const EnumField = preload("res://src/ui_elements/enum_field.tscn")
+
+@onready var attribute_container: HFlowContainer = $AttributeContainer
+
+var tag: Tag
+
+func _ready() -> void:
+ for attribute_key in DB.recognized_attributes[tag.name]:
+ var input_field: Control
+ match DB.get_attribute_type(attribute_key):
+ DB.AttributeType.TRANSFORM_LIST: input_field = TransformField.instantiate()
+ DB.AttributeType.COLOR: input_field = ColorField.instantiate()
+ DB.AttributeType.ENUM: input_field = EnumField.instantiate()
+ DB.AttributeType.NUMERIC:
+ var min_value: float = DB.attribute_numeric_bounds[attribute_key].x
+ var max_value: float = DB.attribute_numeric_bounds[attribute_key].y
+ if is_inf(max_value):
+ input_field = NumberField.instantiate()
+ if not is_inf(min_value):
+ input_field.allow_lower = false
+ input_field.min_value = min_value
+ else:
+ input_field = NumberSlider.instantiate()
+ input_field.allow_lower = false
+ input_field.allow_higher = false
+ input_field.min_value = min_value
+ input_field.max_value = max_value
+ input_field.slider_step = 0.01
+ input_field.attribute_name = attribute_key
+ input_field.tag = tag
+ input_field.focus_entered.connect(Indications.normal_select.bind(tag.xid))
+ attribute_container.add_child(input_field)
diff --git a/src/ui_elements/tag_content_circle.tscn b/src/ui_elements/tag_content_basic_shape.tscn
similarity index 75%
rename from src/ui_elements/tag_content_circle.tscn
rename to src/ui_elements/tag_content_basic_shape.tscn
index fd7e682d..a7f66295 100644
--- a/src/ui_elements/tag_content_circle.tscn
+++ b/src/ui_elements/tag_content_basic_shape.tscn
@@ -1,8 +1,8 @@
[gd_scene load_steps=2 format=3 uid="uid://b0srm51hsvpel"]
-[ext_resource type="Script" path="res://src/ui_elements/tag_content_circle.gd" id="1_tg77v"]
+[ext_resource type="Script" path="res://src/ui_elements/tag_content_basic_shape.gd" id="1_tg77v"]
-[node name="TagContentCircle" type="VBoxContainer"]
+[node name="TagContentBasicShape" type="VBoxContainer"]
offset_right = 40.0
offset_bottom = 40.0
script = ExtResource("1_tg77v")
diff --git a/src/ui_elements/tag_content_circle.gd b/src/ui_elements/tag_content_circle.gd
deleted file mode 100644
index 67ce9155..00000000
--- a/src/ui_elements/tag_content_circle.gd
+++ /dev/null
@@ -1,46 +0,0 @@
-extends VBoxContainer
-
-const TransformField = preload("res://src/ui_elements/transform_field.tscn")
-const NumberField = preload("res://src/ui_elements/number_field.tscn")
-const NumberSlider = preload("res://src/ui_elements/number_field_with_slider.tscn")
-const ColorField = preload("res://src/ui_elements/color_field.tscn")
-const EnumField = preload("res://src/ui_elements/enum_field.tscn")
-
-@onready var attribute_container: HFlowContainer = $AttributeContainer
-
-var tag: Tag
-var tid: PackedInt32Array
-
-func _ready() -> void:
- for attribute_key in ["transform", "opacity", "cx", "cy", "r",
- "fill", "fill-opacity", "stroke", "stroke-opacity", "stroke-width"]:
- var attribute: Attribute = tag.attributes[attribute_key]
- var input_field: Control
- if attribute is AttributeTransform:
- input_field = TransformField.instantiate()
- elif attribute is AttributeNumeric:
- var min_value: float = DB.attribute_numeric_bounds[attribute_key].x
- var max_value: float = DB.attribute_numeric_bounds[attribute_key].y
- if is_inf(max_value):
- input_field = NumberField.instantiate()
- if not is_inf(min_value):
- input_field.allow_lower = false
- input_field.min_value = min_value
- else:
- input_field = NumberSlider.instantiate()
- input_field.allow_lower = false
- input_field.allow_higher = false
- input_field.min_value = min_value
- input_field.max_value = max_value
- input_field.slider_step = 0.01
- elif attribute is AttributeColor:
- input_field = ColorField.instantiate()
- elif attribute is AttributeEnum:
- input_field = EnumField.instantiate()
- input_field.attribute = attribute
- # Focused signal for pathdata attribute.
- if input_field.has_signal("focused"):
- input_field.focused.connect(Indications.normal_select.bind(tid))
- else:
- input_field.focus_entered.connect(Indications.normal_select.bind(tid))
- attribute_container.add_child(input_field)
diff --git a/src/ui_elements/tag_content_ellipse.gd b/src/ui_elements/tag_content_ellipse.gd
deleted file mode 100644
index 63849b62..00000000
--- a/src/ui_elements/tag_content_ellipse.gd
+++ /dev/null
@@ -1,46 +0,0 @@
-extends VBoxContainer
-
-const TransformField = preload("res://src/ui_elements/transform_field.tscn")
-const NumberField = preload("res://src/ui_elements/number_field.tscn")
-const NumberSlider = preload("res://src/ui_elements/number_field_with_slider.tscn")
-const ColorField = preload("res://src/ui_elements/color_field.tscn")
-const EnumField = preload("res://src/ui_elements/enum_field.tscn")
-
-@onready var attribute_container: HFlowContainer = $AttributeContainer
-
-var tag: Tag
-var tid: PackedInt32Array
-
-func _ready() -> void:
- for attribute_key in ["transform", "opacity", "cx", "cy", "rx", "ry",
- "fill", "fill-opacity", "stroke", "stroke-opacity", "stroke-width"]:
- var attribute: Attribute = tag.attributes[attribute_key]
- var input_field: Control
- if attribute is AttributeTransform:
- input_field = TransformField.instantiate()
- elif attribute is AttributeNumeric:
- var min_value: float = DB.attribute_numeric_bounds[attribute_key].x
- var max_value: float = DB.attribute_numeric_bounds[attribute_key].y
- if is_inf(max_value):
- input_field = NumberField.instantiate()
- if not is_inf(min_value):
- input_field.allow_lower = false
- input_field.min_value = min_value
- else:
- input_field = NumberSlider.instantiate()
- input_field.allow_lower = false
- input_field.allow_higher = false
- input_field.min_value = min_value
- input_field.max_value = max_value
- input_field.slider_step = 0.01
- elif attribute is AttributeColor:
- input_field = ColorField.instantiate()
- elif attribute is AttributeEnum:
- input_field = EnumField.instantiate()
- input_field.attribute = attribute
- # Focused signal for pathdata attribute.
- if input_field.has_signal("focused"):
- input_field.focused.connect(Indications.normal_select.bind(tid))
- else:
- input_field.focus_entered.connect(Indications.normal_select.bind(tid))
- attribute_container.add_child(input_field)
diff --git a/src/ui_elements/tag_content_g.gd b/src/ui_elements/tag_content_g.gd
new file mode 100644
index 00000000..7024a8f2
--- /dev/null
+++ b/src/ui_elements/tag_content_g.gd
@@ -0,0 +1,38 @@
+extends VBoxContainer
+
+const TransformField = preload("res://src/ui_elements/transform_field.tscn")
+const NumberField = preload("res://src/ui_elements/number_field.tscn")
+const NumberSlider = preload("res://src/ui_elements/number_field_with_slider.tscn")
+const ColorField = preload("res://src/ui_elements/color_field.tscn")
+const EnumField = preload("res://src/ui_elements/enum_field.tscn")
+
+@onready var attribute_container: HFlowContainer = $AttributeContainer
+
+var tag: Tag
+
+func _ready() -> void:
+ for attribute_key in DB.recognized_attributes["g"]:
+ var input_field: Control
+ match DB.get_attribute_type(attribute_key):
+ DB.AttributeType.TRANSFORM_LIST: input_field = TransformField.instantiate()
+ DB.AttributeType.COLOR: input_field = ColorField.instantiate()
+ DB.AttributeType.ENUM: input_field = EnumField.instantiate()
+ DB.AttributeType.NUMERIC:
+ var min_value: float = DB.attribute_numeric_bounds[attribute_key].x
+ var max_value: float = DB.attribute_numeric_bounds[attribute_key].y
+ if is_inf(max_value):
+ input_field = NumberField.instantiate()
+ if not is_inf(min_value):
+ input_field.allow_lower = false
+ input_field.min_value = min_value
+ else:
+ input_field = NumberSlider.instantiate()
+ input_field.allow_lower = false
+ input_field.allow_higher = false
+ input_field.min_value = min_value
+ input_field.max_value = max_value
+ input_field.slider_step = 0.01
+ input_field.attribute_name = attribute_key
+ input_field.tag = tag
+ input_field.focus_entered.connect(Indications.normal_select.bind(tag.xid))
+ attribute_container.add_child(input_field)
diff --git a/src/ui_elements/tag_content_line.tscn b/src/ui_elements/tag_content_g.tscn
similarity index 53%
rename from src/ui_elements/tag_content_line.tscn
rename to src/ui_elements/tag_content_g.tscn
index 0be00872..23d7aca6 100644
--- a/src/ui_elements/tag_content_line.tscn
+++ b/src/ui_elements/tag_content_g.tscn
@@ -1,11 +1,11 @@
-[gd_scene load_steps=2 format=3 uid="uid://b6ra3mw3qludw"]
+[gd_scene load_steps=2 format=3 uid="uid://bmbrrcuxvyfv3"]
-[ext_resource type="Script" path="res://src/ui_elements/tag_content_line.gd" id="1_xvs53"]
+[ext_resource type="Script" path="res://src/ui_elements/tag_content_g.gd" id="1_wpyt2"]
-[node name="TagContentLine" type="VBoxContainer"]
+[node name="TagContentG" type="VBoxContainer"]
offset_right = 40.0
offset_bottom = 40.0
-script = ExtResource("1_xvs53")
+script = ExtResource("1_wpyt2")
[node name="AttributeContainer" type="HFlowContainer" parent="."]
layout_mode = 2
diff --git a/src/ui_elements/tag_content_line.gd b/src/ui_elements/tag_content_line.gd
deleted file mode 100644
index 78356a09..00000000
--- a/src/ui_elements/tag_content_line.gd
+++ /dev/null
@@ -1,46 +0,0 @@
-extends VBoxContainer
-
-const TransformField = preload("res://src/ui_elements/transform_field.tscn")
-const NumberField = preload("res://src/ui_elements/number_field.tscn")
-const NumberSlider = preload("res://src/ui_elements/number_field_with_slider.tscn")
-const ColorField = preload("res://src/ui_elements/color_field.tscn")
-const EnumField = preload("res://src/ui_elements/enum_field.tscn")
-
-@onready var attribute_container: HFlowContainer = $AttributeContainer
-
-var tag: Tag
-var tid: PackedInt32Array
-
-func _ready() -> void:
- for attribute_key in ["transform", "opacity", "x1", "y1", "x2", "y2",
- "stroke", "stroke-opacity", "stroke-width", "stroke-linecap"]:
- var attribute: Attribute = tag.attributes[attribute_key]
- var input_field: Control
- if attribute is AttributeTransform:
- input_field = TransformField.instantiate()
- elif attribute is AttributeNumeric:
- var min_value: float = DB.attribute_numeric_bounds[attribute_key].x
- var max_value: float = DB.attribute_numeric_bounds[attribute_key].y
- if is_inf(max_value):
- input_field = NumberField.instantiate()
- if not is_inf(min_value):
- input_field.allow_lower = false
- input_field.min_value = min_value
- else:
- input_field = NumberSlider.instantiate()
- input_field.allow_lower = false
- input_field.allow_higher = false
- input_field.min_value = min_value
- input_field.max_value = max_value
- input_field.slider_step = 0.01
- elif attribute is AttributeColor:
- input_field = ColorField.instantiate()
- elif attribute is AttributeEnum:
- input_field = EnumField.instantiate()
- input_field.attribute = attribute
- # Focused signal for pathdata attribute.
- if input_field.has_signal("focused"):
- input_field.focused.connect(Indications.normal_select.bind(tid))
- else:
- input_field.focus_entered.connect(Indications.normal_select.bind(tid))
- attribute_container.add_child(input_field)
diff --git a/src/ui_elements/tag_content_linear_gradient.gd b/src/ui_elements/tag_content_linear_gradient.gd
new file mode 100644
index 00000000..94c81f33
--- /dev/null
+++ b/src/ui_elements/tag_content_linear_gradient.gd
@@ -0,0 +1,40 @@
+extends VBoxContainer
+
+const TransformField = preload("res://src/ui_elements/transform_field.tscn")
+const NumberField = preload("res://src/ui_elements/number_field.tscn")
+const NumberSlider = preload("res://src/ui_elements/number_field_with_slider.tscn")
+const ColorField = preload("res://src/ui_elements/color_field.tscn")
+const EnumField = preload("res://src/ui_elements/enum_field.tscn")
+const IdField = preload("res://src/ui_elements/id_field.tscn")
+
+@onready var attribute_container: HFlowContainer = $AttributeContainer
+
+var tag: Tag
+
+func _ready() -> void:
+ for attribute_key in DB.recognized_attributes["linearGradient"]:
+ var input_field: Control
+ match DB.get_attribute_type(attribute_key):
+ DB.AttributeType.TRANSFORM_LIST: input_field = TransformField.instantiate()
+ DB.AttributeType.COLOR: input_field = ColorField.instantiate()
+ DB.AttributeType.ENUM: input_field = EnumField.instantiate()
+ DB.AttributeType.ID: input_field = IdField.instantiate()
+ DB.AttributeType.NUMERIC:
+ var min_value: float = DB.attribute_numeric_bounds[attribute_key].x
+ var max_value: float = DB.attribute_numeric_bounds[attribute_key].y
+ if is_inf(max_value):
+ input_field = NumberField.instantiate()
+ if not is_inf(min_value):
+ input_field.allow_lower = false
+ input_field.min_value = min_value
+ else:
+ input_field = NumberSlider.instantiate()
+ input_field.allow_lower = false
+ input_field.allow_higher = false
+ input_field.min_value = min_value
+ input_field.max_value = max_value
+ input_field.slider_step = 0.01
+ input_field.attribute_name = attribute_key
+ input_field.tag = tag
+ input_field.focus_entered.connect(Indications.normal_select.bind(tag.xid))
+ attribute_container.add_child(input_field)
diff --git a/src/ui_elements/tag_content_ellipse.tscn b/src/ui_elements/tag_content_linear_gradient.tscn
similarity index 50%
rename from src/ui_elements/tag_content_ellipse.tscn
rename to src/ui_elements/tag_content_linear_gradient.tscn
index d0c4e00f..d7798ebc 100644
--- a/src/ui_elements/tag_content_ellipse.tscn
+++ b/src/ui_elements/tag_content_linear_gradient.tscn
@@ -1,11 +1,11 @@
-[gd_scene load_steps=2 format=3 uid="uid://dyyd1lfrkl303"]
+[gd_scene load_steps=2 format=3 uid="uid://bt0kb3188plwn"]
-[ext_resource type="Script" path="res://src/ui_elements/tag_content_ellipse.gd" id="1_dysu6"]
+[ext_resource type="Script" path="res://src/ui_elements/tag_content_linear_gradient.gd" id="1_q43hc"]
-[node name="TagContentEllipse" type="VBoxContainer"]
+[node name="TagContentLinearGradient" type="VBoxContainer"]
offset_right = 40.0
offset_bottom = 40.0
-script = ExtResource("1_dysu6")
+script = ExtResource("1_q43hc")
[node name="AttributeContainer" type="HFlowContainer" parent="."]
layout_mode = 2
diff --git a/src/ui_elements/tag_content_path.gd b/src/ui_elements/tag_content_path.gd
index 4a6570df..fed4ec37 100644
--- a/src/ui_elements/tag_content_path.gd
+++ b/src/ui_elements/tag_content_path.gd
@@ -10,40 +10,37 @@ const EnumField = preload("res://src/ui_elements/enum_field.tscn")
@onready var path_field: VBoxContainer = $PathField
var tag: Tag
-var tid: PackedInt32Array
func _ready() -> void:
- path_field.set_attribute(tag.attributes.d)
- path_field.tid = tid
- for attribute_key in ["transform", "opacity", "fill", "fill-opacity",
- "stroke", "stroke-opacity", "stroke-width", "stroke-linecap", "stroke-linejoin"]:
- var attribute: Attribute = tag.attributes[attribute_key]
+ path_field.tag = tag
+ path_field.setup()
+ path_field.focused.connect(Indications.normal_select.bind(tag.xid))
+
+ for attribute_key in DB.recognized_attributes["path"]:
+ if attribute_key == "d":
+ continue
var input_field: Control
- if attribute is AttributeTransform:
- input_field = TransformField.instantiate()
- elif attribute is AttributeNumeric:
- var min_value: float = DB.attribute_numeric_bounds[attribute_key].x
- var max_value: float = DB.attribute_numeric_bounds[attribute_key].y
- if is_inf(max_value):
- input_field = NumberField.instantiate()
- if not is_inf(min_value):
+ match DB.get_attribute_type(attribute_key):
+ DB.AttributeType.TRANSFORM_LIST: input_field = TransformField.instantiate()
+ DB.AttributeType.COLOR: input_field = ColorField.instantiate()
+ DB.AttributeType.ENUM: input_field = EnumField.instantiate()
+ DB.AttributeType.NUMERIC:
+ var min_value: float = DB.attribute_numeric_bounds[attribute_key].x
+ var max_value: float = DB.attribute_numeric_bounds[attribute_key].y
+ if is_inf(max_value):
+ input_field = NumberField.instantiate()
+ if not is_inf(min_value):
+ input_field.allow_lower = false
+ input_field.min_value = min_value
+ else:
+ input_field = NumberSlider.instantiate()
input_field.allow_lower = false
+ input_field.allow_higher = false
input_field.min_value = min_value
- else:
- input_field = NumberSlider.instantiate()
- input_field.allow_lower = false
- input_field.allow_higher = false
- input_field.min_value = min_value
- input_field.max_value = max_value
- input_field.slider_step = 0.01
- elif attribute is AttributeColor:
- input_field = ColorField.instantiate()
- elif attribute is AttributeEnum:
- input_field = EnumField.instantiate()
- input_field.attribute = attribute
+ input_field.max_value = max_value
+ input_field.slider_step = 0.01
+ input_field.attribute_name = attribute_key
+ input_field.tag = tag
# Focused signal for pathdata attribute.
- if input_field.has_signal("focused"):
- input_field.focused.connect(Indications.normal_select.bind(tid))
- else:
- input_field.focus_entered.connect(Indications.normal_select.bind(tid))
+ input_field.focus_entered.connect(Indications.normal_select.bind(tag.xid))
attribute_container.add_child(input_field)
diff --git a/src/ui_elements/tag_content_radial_gradient.gd b/src/ui_elements/tag_content_radial_gradient.gd
new file mode 100644
index 00000000..99ec65c1
--- /dev/null
+++ b/src/ui_elements/tag_content_radial_gradient.gd
@@ -0,0 +1,40 @@
+extends VBoxContainer
+
+const TransformField = preload("res://src/ui_elements/transform_field.tscn")
+const NumberField = preload("res://src/ui_elements/number_field.tscn")
+const NumberSlider = preload("res://src/ui_elements/number_field_with_slider.tscn")
+const ColorField = preload("res://src/ui_elements/color_field.tscn")
+const EnumField = preload("res://src/ui_elements/enum_field.tscn")
+const IdField = preload("res://src/ui_elements/id_field.tscn")
+
+@onready var attribute_container: HFlowContainer = $AttributeContainer
+
+var tag: Tag
+
+func _ready() -> void:
+ for attribute_key in DB.recognized_attributes["radialGradient"]:
+ var input_field: Control
+ match DB.get_attribute_type(attribute_key):
+ DB.AttributeType.TRANSFORM_LIST: input_field = TransformField.instantiate()
+ DB.AttributeType.COLOR: input_field = ColorField.instantiate()
+ DB.AttributeType.ENUM: input_field = EnumField.instantiate()
+ DB.AttributeType.ID: input_field = IdField.instantiate()
+ DB.AttributeType.NUMERIC:
+ var min_value: float = DB.attribute_numeric_bounds[attribute_key].x
+ var max_value: float = DB.attribute_numeric_bounds[attribute_key].y
+ if is_inf(max_value):
+ input_field = NumberField.instantiate()
+ if not is_inf(min_value):
+ input_field.allow_lower = false
+ input_field.min_value = min_value
+ else:
+ input_field = NumberSlider.instantiate()
+ input_field.allow_lower = false
+ input_field.allow_higher = false
+ input_field.min_value = min_value
+ input_field.max_value = max_value
+ input_field.slider_step = 0.01
+ input_field.attribute_name = attribute_key
+ input_field.tag = tag
+ input_field.focus_entered.connect(Indications.normal_select.bind(tag.xid))
+ attribute_container.add_child(input_field)
diff --git a/src/ui_elements/tag_content_rect.tscn b/src/ui_elements/tag_content_radial_gradient.tscn
similarity index 50%
rename from src/ui_elements/tag_content_rect.tscn
rename to src/ui_elements/tag_content_radial_gradient.tscn
index f2873d8f..bd7b6886 100644
--- a/src/ui_elements/tag_content_rect.tscn
+++ b/src/ui_elements/tag_content_radial_gradient.tscn
@@ -1,11 +1,11 @@
-[gd_scene load_steps=2 format=3 uid="uid://dxeni0ewiuop8"]
+[gd_scene load_steps=2 format=3 uid="uid://bethkcrbf0tf7"]
-[ext_resource type="Script" path="res://src/ui_elements/tag_content_rect.gd" id="1_bhc7f"]
+[ext_resource type="Script" path="res://src/ui_elements/tag_content_radial_gradient.gd" id="1_le714"]
-[node name="TagContentRect" type="VBoxContainer"]
+[node name="TagContentRadialGradient" type="VBoxContainer"]
offset_right = 40.0
offset_bottom = 40.0
-script = ExtResource("1_bhc7f")
+script = ExtResource("1_le714")
[node name="AttributeContainer" type="HFlowContainer" parent="."]
layout_mode = 2
diff --git a/src/ui_elements/tag_content_rect.gd b/src/ui_elements/tag_content_rect.gd
deleted file mode 100644
index e0ebe310..00000000
--- a/src/ui_elements/tag_content_rect.gd
+++ /dev/null
@@ -1,46 +0,0 @@
-extends VBoxContainer
-
-const TransformField = preload("res://src/ui_elements/transform_field.tscn")
-const NumberField = preload("res://src/ui_elements/number_field.tscn")
-const NumberSlider = preload("res://src/ui_elements/number_field_with_slider.tscn")
-const ColorField = preload("res://src/ui_elements/color_field.tscn")
-const EnumField = preload("res://src/ui_elements/enum_field.tscn")
-
-@onready var attribute_container: HFlowContainer = $AttributeContainer
-
-var tag: Tag
-var tid: PackedInt32Array
-
-func _ready() -> void:
- for attribute_key in ["transform", "opacity", "x", "y", "width", "height", "rx", "ry",
- "fill", "fill-opacity", "stroke", "stroke-opacity", "stroke-width", "stroke-linejoin"]:
- var attribute: Attribute = tag.attributes[attribute_key]
- var input_field: Control
- if attribute is AttributeTransform:
- input_field = TransformField.instantiate()
- elif attribute is AttributeNumeric:
- var min_value: float = DB.attribute_numeric_bounds[attribute_key].x
- var max_value: float = DB.attribute_numeric_bounds[attribute_key].y
- if is_inf(max_value):
- input_field = NumberField.instantiate()
- if not is_inf(min_value):
- input_field.allow_lower = false
- input_field.min_value = min_value
- else:
- input_field = NumberSlider.instantiate()
- input_field.allow_lower = false
- input_field.allow_higher = false
- input_field.min_value = min_value
- input_field.max_value = max_value
- input_field.slider_step = 0.01
- elif attribute is AttributeColor:
- input_field = ColorField.instantiate()
- elif attribute is AttributeEnum:
- input_field = EnumField.instantiate()
- input_field.attribute = attribute
- # Focused signal for pathdata attribute.
- if input_field.has_signal("focused"):
- input_field.focused.connect(Indications.normal_select.bind(tid))
- else:
- input_field.focus_entered.connect(Indications.normal_select.bind(tid))
- attribute_container.add_child(input_field)
diff --git a/src/ui_elements/tag_content_stop.gd b/src/ui_elements/tag_content_stop.gd
index 30512209..66db631e 100644
--- a/src/ui_elements/tag_content_stop.gd
+++ b/src/ui_elements/tag_content_stop.gd
@@ -9,37 +9,30 @@ const EnumField = preload("res://src/ui_elements/enum_field.tscn")
@onready var attribute_container: HFlowContainer = $AttributeContainer
var tag: Tag
-var tid: PackedInt32Array
func _ready() -> void:
- for attribute_key in ["offset", "stop-color", "stop-opacity"]:
- var attribute: Attribute = tag.attributes[attribute_key]
- var input_field: Control
- if attribute is AttributeTransform:
- input_field = TransformField.instantiate()
- elif attribute is AttributeNumeric:
- var min_value: float = DB.attribute_numeric_bounds[attribute_key].x
- var max_value: float = DB.attribute_numeric_bounds[attribute_key].y
- if is_inf(max_value):
- input_field = NumberField.instantiate()
- if not is_inf(min_value):
- input_field.allow_lower = false
- input_field.min_value = min_value
- else:
- input_field = NumberSlider.instantiate()
- input_field.allow_lower = false
- input_field.allow_higher = false
- input_field.min_value = min_value
- input_field.max_value = max_value
- input_field.slider_step = 0.01
- elif attribute is AttributeColor:
- input_field = ColorField.instantiate()
- elif attribute is AttributeEnum:
- input_field = EnumField.instantiate()
- input_field.attribute = attribute
- # Focused signal for pathdata attribute.
- if input_field.has_signal("focused"):
- input_field.focused.connect(Indications.normal_select.bind(tid))
- else:
- input_field.focus_entered.connect(Indications.normal_select.bind(tid))
- attribute_container.add_child(input_field)
+ var offset_input_field: Control = NumberSlider.instantiate()
+ offset_input_field.allow_lower = false
+ offset_input_field.allow_higher = false
+ offset_input_field.min_value = DB.attribute_numeric_bounds["offset"].x
+ offset_input_field.max_value = DB.attribute_numeric_bounds["offset"].y
+ offset_input_field.slider_step = 0.01
+ offset_input_field.tag = tag
+ offset_input_field.attribute_name = "offset"
+
+ var color_input_field: Control = ColorField.instantiate()
+ color_input_field.tag = tag
+ color_input_field.attribute_name = "stop-color"
+
+ var opacity_input_field: Control = NumberSlider.instantiate()
+ opacity_input_field.allow_lower = false
+ opacity_input_field.allow_higher = false
+ opacity_input_field.min_value = DB.attribute_numeric_bounds["stop-opacity"].x
+ opacity_input_field.max_value = DB.attribute_numeric_bounds["stop-opacity"].y
+ opacity_input_field.slider_step = 0.01
+ opacity_input_field.tag = tag
+ opacity_input_field.attribute_name = "stop-opacity"
+
+ attribute_container.add_child(offset_input_field)
+ attribute_container.add_child(color_input_field)
+ attribute_container.add_child(opacity_input_field)
diff --git a/src/ui_elements/tag_content_unknown.gd b/src/ui_elements/tag_content_unrecognized.gd
similarity index 58%
rename from src/ui_elements/tag_content_unknown.gd
rename to src/ui_elements/tag_content_unrecognized.gd
index 19ac58c8..f0d4eb0e 100644
--- a/src/ui_elements/tag_content_unknown.gd
+++ b/src/ui_elements/tag_content_unrecognized.gd
@@ -1,4 +1,3 @@
extends VBoxContainer
var tag: Tag
-var tid: PackedInt32Array
diff --git a/src/ui_elements/tag_content_unknown.tscn b/src/ui_elements/tag_content_unrecognized.tscn
similarity index 87%
rename from src/ui_elements/tag_content_unknown.tscn
rename to src/ui_elements/tag_content_unrecognized.tscn
index ac8dc8f2..0efeaa01 100644
--- a/src/ui_elements/tag_content_unknown.tscn
+++ b/src/ui_elements/tag_content_unrecognized.tscn
@@ -1,6 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://b70dhqt8ldcmc"]
-[ext_resource type="Script" path="res://src/ui_elements/tag_content_unknown.gd" id="1_vy7fr"]
+[ext_resource type="Script" path="res://src/ui_elements/tag_content_unrecognized.gd" id="1_vy7fr"]
[node name="TagContentUnknown" type="VBoxContainer"]
size_flags_horizontal = 3
diff --git a/src/ui_elements/transform_field.gd b/src/ui_elements/transform_field.gd
index 13be79a3..ba64e143 100644
--- a/src/ui_elements/transform_field.gd
+++ b/src/ui_elements/transform_field.gd
@@ -1,20 +1,16 @@
# An editor to be tied to a transform list attribute.
extends LineEditButton
-var attribute: AttributeTransform
+var tag: Tag
+var attribute_name: String
const TransformPopup = preload("res://src/ui_elements/transform_popup.tscn")
-func set_value(new_value: String, update_type := Utils.UpdateType.REGULAR) -> void:
- sync(attribute.format(new_value))
- if attribute.get_value() != new_value or update_type == Utils.UpdateType.FINAL:
- match update_type:
- Utils.UpdateType.INTERMEDIATE:
- attribute.set_value(new_value, Attribute.SyncMode.INTERMEDIATE)
- Utils.UpdateType.FINAL:
- attribute.set_value(new_value, Attribute.SyncMode.FINAL)
- _:
- attribute.set_value(new_value)
+func set_value(new_value: String, save := true) -> void:
+ sync(new_value)
+ var attribute := tag.get_attribute(attribute_name)
+ if attribute.get_value() != new_value:
+ attribute.set_value(new_value, save)
func _notification(what: int) -> void:
@@ -22,9 +18,10 @@ func _notification(what: int) -> void:
update_translation()
func _ready() -> void:
+ var attribute: AttributeTransformList = tag.get_attribute(attribute_name)
set_value(attribute.get_value())
attribute.value_changed.connect(set_value)
- tooltip_text = attribute.name
+ tooltip_text = attribute_name
text_submitted.connect(set_value)
text_changed.connect(setup_font)
update_translation()
@@ -42,7 +39,7 @@ func sync(new_value: String) -> void:
func _on_pressed() -> void:
var transform_popup := TransformPopup.instantiate()
- transform_popup.attribute_ref = attribute
+ transform_popup.attribute_ref = tag.get_attribute(attribute_name)
HandlerGUI.popup_under_rect(transform_popup, get_global_rect(), get_viewport())
diff --git a/src/ui_elements/transform_popup.gd b/src/ui_elements/transform_popup.gd
index 336e1882..bf13e1c9 100644
--- a/src/ui_elements/transform_popup.gd
+++ b/src/ui_elements/transform_popup.gd
@@ -18,7 +18,7 @@ const icons_dict := {
"skewY": preload("res://visual/icons/SkewY.svg"),
}
-var attribute_ref: AttributeTransform
+var attribute_ref: AttributeTransformList
var UR := UndoRedo.new()
@onready var x1_edit: NumberEditType = %FinalMatrix/X1
@@ -55,15 +55,14 @@ func rebuild() -> void:
if i >= transform_count:
break
var t := attribute_ref.get_transform(i)
- if t is AttributeTransform.TransformMatrix and transform_editor.type == "matrix":
+ if t is Transform.TransformMatrix and transform_editor.type == "matrix":
transform_editor.fields[0].set_value(t.x1, true)
transform_editor.fields[1].set_value(t.x2, true)
transform_editor.fields[2].set_value(t.y1, true)
transform_editor.fields[3].set_value(t.y2, true)
transform_editor.fields[4].set_value(t.o1, true)
transform_editor.fields[5].set_value(t.o2, true)
- elif t is AttributeTransform.TransformTranslate and\
- transform_editor.type == "translate":
+ elif t is Transform.TransformTranslate and transform_editor.type == "translate":
transform_editor.fields[0].set_value(t.x, true)
transform_editor.fields[1].set_value(t.y, true)
if t.y == 0:
@@ -72,7 +71,7 @@ func rebuild() -> void:
DEFAULT_VALUE_OPACITY))
else:
transform_editor.fields[1].remove_theme_color_override("font_color")
- elif t is AttributeTransform.TransformRotate and transform_editor.type == "rotate":
+ elif t is Transform.TransformRotate and transform_editor.type == "rotate":
transform_editor.fields[0].set_value(t.deg, true)
transform_editor.fields[1].set_value(t.x, true)
transform_editor.fields[2].set_value(t.y, true)
@@ -86,7 +85,7 @@ func rebuild() -> void:
else:
transform_editor.fields[1].remove_theme_color_override("font_color")
transform_editor.fields[2].remove_theme_color_override("font_color")
- elif t is AttributeTransform.TransformScale and transform_editor.type == "scale":
+ elif t is Transform.TransformScale and transform_editor.type == "scale":
transform_editor.fields[0].set_value(t.x, true)
transform_editor.fields[1].set_value(t.y, true)
if t.x == t.y:
@@ -95,9 +94,9 @@ func rebuild() -> void:
DEFAULT_VALUE_OPACITY))
else:
transform_editor.fields[1].remove_theme_color_override("font_color")
- elif t is AttributeTransform.TransformSkewX and transform_editor.type == "skewX":
+ elif t is Transform.TransformSkewX and transform_editor.type == "skewX":
transform_editor.fields[0].set_value(t.x, true)
- elif t is AttributeTransform.TransformSkewY and transform_editor.type == "skewY":
+ elif t is Transform.TransformSkewY and transform_editor.type == "skewY":
transform_editor.fields[0].set_value(t.y, true)
else:
break
@@ -111,24 +110,24 @@ func rebuild() -> void:
var t_editor := TransformEditor.instantiate()
transform_list.add_child(t_editor)
# Setup top panel
- if t is AttributeTransform.TransformMatrix:
+ if t is Transform.TransformMatrix:
t_editor.type = "matrix"
- elif t is AttributeTransform.TransformTranslate:
+ elif t is Transform.TransformTranslate:
t_editor.type = "translate"
- elif t is AttributeTransform.TransformRotate:
+ elif t is Transform.TransformRotate:
t_editor.type = "rotate"
- elif t is AttributeTransform.TransformScale:
+ elif t is Transform.TransformScale:
t_editor.type = "scale"
- elif t is AttributeTransform.TransformSkewX:
+ elif t is Transform.TransformSkewX:
t_editor.type = "skewX"
- elif t is AttributeTransform.TransformSkewY:
+ elif t is Transform.TransformSkewY:
t_editor.type = "skewY"
t_editor.transform_label.text = t_editor.type
t_editor.transform_icon.texture = icons_dict[t_editor.type]
t_editor.more_button.pressed.connect(
popup_transform_actions.bind(i, t_editor.more_button))
# Setup fields.
- if t is AttributeTransform.TransformMatrix:
+ if t is Transform.TransformMatrix:
var field_x1 := create_mini_number_field(t, i, "x1")
var field_x2 := create_mini_number_field(t, i, "x2")
var field_y1 := create_mini_number_field(t, i, "y1")
@@ -149,7 +148,7 @@ func rebuild() -> void:
transform_fields_additional.add_child(field_o2)
t_editor.transform_list.add_child(transform_fields)
t_editor.transform_list.add_child(transform_fields_additional)
- elif t is AttributeTransform.TransformTranslate:
+ elif t is Transform.TransformTranslate:
var field_x := create_mini_number_field(t, i, "x")
var field_y := create_mini_number_field(t, i, "y")
t_editor.fields = [field_x, field_y] as Array[BetterLineEdit]
@@ -158,7 +157,7 @@ func rebuild() -> void:
transform_fields.add_child(field_x)
transform_fields.add_child(field_y)
t_editor.transform_list.add_child(transform_fields)
- elif t is AttributeTransform.TransformRotate:
+ elif t is Transform.TransformRotate:
var field_deg := create_mini_number_field(t, i, "deg")
field_deg.mode = field_deg.Mode.ANGLE
var field_x := create_mini_number_field(t, i, "x")
@@ -170,7 +169,7 @@ func rebuild() -> void:
transform_fields.add_child(field_x)
transform_fields.add_child(field_y)
t_editor.transform_list.add_child(transform_fields)
- elif t is AttributeTransform.TransformScale:
+ elif t is Transform.TransformScale:
var field_x := create_mini_number_field(t, i, "x")
var field_y := create_mini_number_field(t, i, "y")
t_editor.fields = [field_x, field_y] as Array[BetterLineEdit]
@@ -179,14 +178,14 @@ func rebuild() -> void:
transform_fields.add_child(field_x)
transform_fields.add_child(field_y)
t_editor.transform_list.add_child(transform_fields)
- elif t is AttributeTransform.TransformSkewX:
+ elif t is Transform.TransformSkewX:
var field_x := create_mini_number_field(t, i, "x")
t_editor.fields = [field_x] as Array[BetterLineEdit]
var transform_fields := HBoxContainer.new()
transform_fields.alignment = BoxContainer.ALIGNMENT_CENTER
transform_fields.add_child(field_x)
t_editor.transform_list.add_child(transform_fields)
- elif t is AttributeTransform.TransformSkewY:
+ elif t is Transform.TransformSkewY:
var field_y := create_mini_number_field(t, i, "y")
t_editor.fields = [field_y] as Array[BetterLineEdit]
var transform_fields := HBoxContainer.new()
@@ -199,15 +198,15 @@ func rebuild() -> void:
add_button.visible = (transform_count == 0)
update_final_transform()
-func create_mini_number_field(transform: AttributeTransform.Transform, idx: int,
+func create_mini_number_field(transform: Transform, idx: int,
property: String) -> BetterLineEdit:
var field := MiniNumberField.instantiate()
field.custom_minimum_size.x = 44
field.set_value(transform.get(property))
- if (transform is AttributeTransform.TransformTranslate and transform.y == 0 and\
- property == "y") or (transform is AttributeTransform.TransformRotate and\
- transform.x == 0 and transform.y == 0 and (property == "x" or property == "y")) or\
- (transform is AttributeTransform.TransformScale and transform.x == transform.y and\
+ if (transform is Transform.TransformTranslate and transform.y == 0 and\
+ property == "y") or (transform is Transform.TransformRotate and transform.x == 0 and\
+ transform.y == 0 and (property == "x" or property == "y")) or\
+ (transform is Transform.TransformScale and transform.x == transform.y and\
property == "y"):
field.add_theme_color_override("font_color", Color(
field.get_theme_color("font_color"), DEFAULT_VALUE_OPACITY))
@@ -244,9 +243,9 @@ func _on_apply_matrix_pressed() -> void:
var final_transform := attribute_ref.get_final_transform()
UR.create_action("")
UR.add_do_method(attribute_ref.set_transform_list.bind([
- AttributeTransform.TransformMatrix.new(final_transform.x.x, final_transform.x.y,
+ Transform.TransformMatrix.new(final_transform.x.x, final_transform.x.y,
final_transform.y.x, final_transform.y.y, final_transform.origin.x,
- final_transform.origin.y)] as Array[AttributeTransform.Transform]))
+ final_transform.origin.y)] as Array[Transform]))
UR.add_do_method(rebuild)
UR.add_undo_method(attribute_ref.set_transform_list.bind(get_transform_list()))
UR.add_undo_method(rebuild)
@@ -303,20 +302,20 @@ func _unhandled_input(event: InputEvent) -> void:
# So I have to rebuild this in its entirety to keep the references safe or something...
-func get_transform_list() -> Array[AttributeTransform.Transform]:
- var t_list: Array[AttributeTransform.Transform] = []
+func get_transform_list() -> Array[Transform]:
+ var t_list: Array[Transform] = []
for t in attribute_ref.get_transform_list():
- if t is AttributeTransform.TransformMatrix:
- t_list.append(AttributeTransform.TransformMatrix.new(
+ if t is Transform.TransformMatrix:
+ t_list.append(Transform.TransformMatrix.new(
t.x1, t.x2, t.y1, t.y2, t.o1, t.o2))
- elif t is AttributeTransform.TransformTranslate:
- t_list.append(AttributeTransform.TransformTranslate.new(t.x, t.y))
- elif t is AttributeTransform.TransformRotate:
- t_list.append(AttributeTransform.TransformRotate.new(t.deg, t.x, t.y))
- elif t is AttributeTransform.TransformScale:
- t_list.append(AttributeTransform.TransformScale.new(t.x, t.y))
- elif t is AttributeTransform.TransformSkewX:
- t_list.append(AttributeTransform.TransformSkewX.new(t.x))
- elif t is AttributeTransform.TransformSkewY:
- t_list.append(AttributeTransform.TransformSkewY.new(t.y))
+ elif t is Transform.TransformTranslate:
+ t_list.append(Transform.TransformTranslate.new(t.x, t.y))
+ elif t is Transform.TransformRotate:
+ t_list.append(Transform.TransformRotate.new(t.deg, t.x, t.y))
+ elif t is Transform.TransformScale:
+ t_list.append(Transform.TransformScale.new(t.x, t.y))
+ elif t is Transform.TransformSkewX:
+ t_list.append(Transform.TransformSkewX.new(t.x))
+ elif t is Transform.TransformSkewY:
+ t_list.append(Transform.TransformSkewY.new(t.y))
return t_list
diff --git a/src/ui_elements/unknown_field.gd b/src/ui_elements/unknown_field.gd
deleted file mode 100644
index 860cfde0..00000000
--- a/src/ui_elements/unknown_field.gd
+++ /dev/null
@@ -1,33 +0,0 @@
-# An editor to be tied to an attribute GodSVG can't recognize, allowing to still edit it.
-extends BetterLineEdit
-
-var attribute: Attribute
-
-func set_value(new_value: String, update_type := Utils.UpdateType.REGULAR) -> void:
- sync(new_value)
- if attribute.get_value() != new_value or update_type == Utils.UpdateType.FINAL:
- match update_type:
- Utils.UpdateType.INTERMEDIATE:
- attribute.set_value(new_value, Attribute.SyncMode.INTERMEDIATE)
- Utils.UpdateType.FINAL:
- attribute.set_value(new_value, Attribute.SyncMode.FINAL)
- _:
- attribute.set_value(new_value)
-
-
-func _notification(what: int) -> void:
- if what == Utils.CustomNotification.LANGUAGE_CHANGED:
- update_translation()
-
-func _ready() -> void:
- super()
- set_value(attribute.get_value())
- update_translation()
- text_submitted.connect(set_value)
-
-func sync(new_value: String) -> void:
- text = new_value
-
-func update_translation() -> void:
- tooltip_text = attribute.name + "\n(%s)" %\
- TranslationServer.translate("GodSVG doesn’t recognize this attribute")
diff --git a/src/ui_elements/unrecognized_field.gd b/src/ui_elements/unrecognized_field.gd
new file mode 100644
index 00000000..7998ef55
--- /dev/null
+++ b/src/ui_elements/unrecognized_field.gd
@@ -0,0 +1,29 @@
+# An editor to be tied to an attribute GodSVG can't recognize, allowing to still edit it.
+extends BetterLineEdit
+
+var tag: Tag
+var attribute_name: String
+
+func set_value(new_value: String, save := true) -> void:
+ sync(new_value)
+ var attribute := tag.get_attribute(attribute_name)
+ if attribute.get_value() != new_value:
+ attribute.set_value(new_value, save)
+
+
+func _notification(what: int) -> void:
+ if what == Utils.CustomNotification.LANGUAGE_CHANGED:
+ update_translation()
+
+func _ready() -> void:
+ super()
+ set_value(tag.get_attribute(attribute_name).get_value())
+ update_translation()
+ text_submitted.connect(set_value)
+
+func sync(new_value: String) -> void:
+ text = new_value
+
+func update_translation() -> void:
+ tooltip_text = attribute_name + "\n(%s)" %\
+ TranslationServer.translate("GodSVG doesn’t recognize this attribute")
diff --git a/src/ui_elements/unknown_field.tscn b/src/ui_elements/unrecognized_field.tscn
similarity index 81%
rename from src/ui_elements/unknown_field.tscn
rename to src/ui_elements/unrecognized_field.tscn
index 9e9fe94e..70b094f9 100644
--- a/src/ui_elements/unknown_field.tscn
+++ b/src/ui_elements/unrecognized_field.tscn
@@ -1,6 +1,6 @@
[gd_scene load_steps=3 format=3 uid="uid://mr2c1hti43k4"]
-[ext_resource type="Script" path="res://src/ui_elements/unknown_field.gd" id="1_decfb"]
+[ext_resource type="Script" path="res://src/ui_elements/unrecognized_field.gd" id="1_decfb"]
[ext_resource type="Texture2D" uid="uid://crx4kcj4o01bs" path="res://visual/icons/SmallQuestionMark.svg" id="1_sd7sr"]
[node name="UnknownField" type="LineEdit"]
diff --git a/src/ui_parts/DeltaHandle.gd b/src/ui_parts/DeltaHandle.gd
index 9868828c..e4207b66 100644
--- a/src/ui_parts/DeltaHandle.gd
+++ b/src/ui_parts/DeltaHandle.gd
@@ -2,41 +2,39 @@
class_name DeltaHandle extends Handle
# Required.
-var x_attribute: AttributeNumeric
-var y_attribute: AttributeNumeric
-var t_attribute: AttributeTransform
-
+var x_name: String
+var y_name: String
+var d_name: String
var horizontal: bool
-var d_attribute: AttributeNumeric
-func _init(id: PackedInt32Array, xref: AttributeNumeric, yref: AttributeNumeric,
-tref: AttributeTransform, dref: AttributeNumeric, p_horizontal: bool) -> void:
- tid = id
- x_attribute = xref
- y_attribute = yref
- t_attribute = tref
- d_attribute = dref
+func _init(new_tag: Tag, xref: String, yref: String, dref: String, p_horizontal: bool) -> void:
+ tag = new_tag
+ x_name = xref
+ y_name = yref
+ d_name = dref
horizontal = p_horizontal
display_mode = Display.SMALL
sync()
-func set_pos(new_pos: Vector2, undo_redo := false) -> void:
- if initial_pos != new_pos and undo_redo:
- d_attribute.set_num(absf(new_pos.x - x_attribute.get_num() if horizontal else\
- new_pos.y - y_attribute.get_num()), Attribute.SyncMode.FINAL)
- else:
- d_attribute.set_num(absf(new_pos.x - x_attribute.get_num() if horizontal else\
- new_pos.y - y_attribute.get_num()), Attribute.SyncMode.FINAL if undo_redo\
- else Attribute.SyncMode.INTERMEDIATE)
- pos = Vector2(x_attribute.get_num(), y_attribute.get_num())
+func set_pos(new_pos: Vector2, save := true) -> void:
if horizontal:
- pos += Vector2(d_attribute.get_num(), 0.0)
+ new_pos = Vector2(tag.get_attribute(x_name).get_num() +\
+ tag.get_attribute(d_name).get_num(), tag.get_attribute(y_name).get_num())
else:
- pos += Vector2(0.0, d_attribute.get_num())
+ new_pos = Vector2(tag.get_attribute(x_name).get_num(),
+ tag.get_attribute(y_name).get_num() + tag.get_attribute(d_name).get_num())
+
+ if pos != new_pos:
+ pos = new_pos
+ tag.get_attribute(d_name).set_num(
+ absf(new_pos.x - tag.get_attribute(x_name).get_num() if horizontal\
+ else new_pos.y - tag.get_attribute(y_name).get_num()), save)
func sync() -> void:
if horizontal:
- pos = Vector2(x_attribute.get_num() + d_attribute.get_num(), y_attribute.get_num())
+ pos = Vector2(tag.get_attribute(x_name).get_num() +\
+ tag.get_attribute(d_name).get_num(), tag.get_attribute(y_name).get_num())
else:
- pos = Vector2(x_attribute.get_num(), y_attribute.get_num() + d_attribute.get_num())
- transform = t_attribute.get_final_transform()
+ pos = Vector2(tag.get_attribute(x_name).get_num(),
+ tag.get_attribute(y_name).get_num() + tag.get_attribute(d_name).get_num())
+ super()
diff --git a/src/ui_parts/Handle.gd b/src/ui_parts/Handle.gd
index e4d7a550..8fe862d5 100644
--- a/src/ui_parts/Handle.gd
+++ b/src/ui_parts/Handle.gd
@@ -4,7 +4,7 @@ class_name Handle extends RefCounted
enum Display {BIG, SMALL}
var display_mode := Display.BIG
-var tid := PackedInt32Array()
+var tag: Tag
var pos: Vector2
var transform: Transform2D
var initial_pos: Vector2 # The position of a handle when it started being dragged.
@@ -13,7 +13,7 @@ func _init() -> void:
pass
func sync() -> void:
- pass
+ transform = tag.get_transform()
func set_pos(_new_pos: Vector2, _undo_redo := false) -> void:
pass
diff --git a/src/ui_parts/PathHandle.gd b/src/ui_parts/PathHandle.gd
index 3fb27695..ae080852 100644
--- a/src/ui_parts/PathHandle.gd
+++ b/src/ui_parts/PathHandle.gd
@@ -1,51 +1,30 @@
# A handle that binds to one or two path parameters.
class_name PathHandle extends Handle
-var path_attribute: AttributePath
-var t_attribute: AttributeTransform
+const pathdata_name = "d"
var command_index: int
var x_param: String
var y_param: String
-func _init(id: PackedInt32Array, path_ref: Attribute, t_ref: AttributeTransform,
-command_idx: int, x_name := "x", y_name := "y") -> void:
- path_attribute = path_ref
- t_attribute = t_ref
- tid = id
+func _init(new_tag: Tag, command_idx: int, x_name := "x", y_name := "y") -> void:
+ tag = new_tag
command_index = command_idx
x_param = x_name
y_param = y_name
sync()
-func set_pos(new_pos: Vector2, undo_redo := false) -> void:
+func set_pos(new_pos: Vector2, save := false) -> void:
+ var path_attribute: AttributePathdata = tag.get_attribute(pathdata_name)
var command := path_attribute.get_command(command_index)
var new_coords := new_pos - command.start if command.relative else new_pos
- if undo_redo:
- if initial_pos != new_pos:
- path_attribute.set_command_property(command_index, x_param, new_coords.x,
- Attribute.SyncMode.NO_PROPAGATION)
- path_attribute.set_command_property(command_index, y_param, new_coords.y,
- Attribute.SyncMode.FINAL)
- else:
- if x_param in command:
- # Don't emit commands_changed for the X change if there'll be a Y change too.
- path_attribute.set_command_property(command_index, x_param, new_coords.x,
- Attribute.SyncMode.NO_PROPAGATION if (y_param in command and\
- command.get(y_param) != new_coords.y) else Attribute.SyncMode.INTERMEDIATE)
- pos.x = new_pos.x
- else:
- pos.x = command.start.x
- if y_param in command:
- if command.get(y_param) != new_coords.y:
- path_attribute.set_command_property(command_index, y_param, new_coords.y,
- Attribute.SyncMode.INTERMEDIATE)
- pos.y = new_pos.y
- else:
- pos.y = command.start.y
+ if pos != new_pos:
+ pos = new_pos
+ path_attribute.set_command_property(command_index, x_param, new_coords.x, save)
+ path_attribute.set_command_property(command_index, y_param, new_coords.y, save)
func sync() -> void:
- var command := path_attribute.get_command(command_index)
+ var command: PathCommand = tag.get_attribute(pathdata_name).get_command(command_index)
if x_param in command:
var command_x: float = command.get(x_param)
pos.x = command.start.x + command_x if command.relative else command_x
@@ -56,4 +35,4 @@ func sync() -> void:
pos.y = command.start.y + command_y if command.relative else command_y
else:
pos.y = command.start.y
- transform = t_attribute.get_final_transform()
+ super()
diff --git a/src/ui_parts/XYHandle.gd b/src/ui_parts/XYHandle.gd
index 4b96f9dd..e0f9970b 100644
--- a/src/ui_parts/XYHandle.gd
+++ b/src/ui_parts/XYHandle.gd
@@ -1,31 +1,21 @@
# A handle that binds to two numeric attributes.
class_name XYHandle extends Handle
-var x_attribute: AttributeNumeric
-var y_attribute: AttributeNumeric
-var t_attribute: AttributeTransform
+var x_name: String
+var y_name: String
-func _init(id: PackedInt32Array, xref: AttributeNumeric, yref: AttributeNumeric,
-tref: AttributeTransform) -> void:
- tid = id
- x_attribute = xref
- y_attribute = yref
- t_attribute = tref
+func _init(new_tag: Tag, xref: String, yref: String) -> void:
+ tag = new_tag
+ x_name = xref
+ y_name = yref
sync()
-func set_pos(new_pos: Vector2, undo_redo := false) -> void:
- if undo_redo:
- if initial_pos != new_pos:
- x_attribute.set_num(new_pos.x, Attribute.SyncMode.NO_PROPAGATION)
- y_attribute.set_num(new_pos.y, Attribute.SyncMode.FINAL)
- else:
- if new_pos.x != pos.x:
- x_attribute.set_num(new_pos.x, Attribute.SyncMode.INTERMEDIATE if\
- new_pos.y == pos.y else Attribute.SyncMode.NO_PROPAGATION)
- if new_pos.y != pos.y:
- y_attribute.set_num(new_pos.y, Attribute.SyncMode.INTERMEDIATE)
- pos = new_pos
+func set_pos(new_pos: Vector2, save := false) -> void:
+ if pos != new_pos:
+ pos = new_pos
+ tag.get_attribute(x_name).set_num(new_pos.x, save)
+ tag.get_attribute(y_name).set_num(new_pos.y, save)
func sync() -> void:
- pos = Vector2(x_attribute.get_num(), y_attribute.get_num())
- transform = t_attribute.get_final_transform()
+ pos = Vector2(tag.get_attribute(x_name).get_num(), tag.get_attribute(y_name).get_num())
+ super()
diff --git a/src/ui_parts/about_menu.gd b/src/ui_parts/about_menu.gd
index a0f4143a..a68683a9 100644
--- a/src/ui_parts/about_menu.gd
+++ b/src/ui_parts/about_menu.gd
@@ -1,5 +1,7 @@
extends PanelContainer
+const app_info_json = preload("res://app_info.json")
+
@onready var version_label: Label = %VersionLabel
@onready var close_button: Button = $VBoxContainer/CloseButton
@onready var translations_list: VBoxContainer = %Translations/List
@@ -30,16 +32,17 @@ func update_translation() -> void:
%Components.text = TranslationServer.translate("Godot third-party components")
func _ready() -> void:
+ var app_info: Dictionary = app_info_json.data
version_label.text = "GodSVG v" + ProjectSettings.get_setting("application/config/version")
- project_founder_list.items = AppInfo.project_founder_and_manager
+ project_founder_list.items = app_info.project_founder_and_manager
project_founder_list.setup()
- authors_list.items = AppInfo.authors
+ authors_list.items = app_info.authors
authors_list.setup()
- donors_list.items = AppInfo.donors
+ donors_list.items = app_info.donors
donors_list.setup()
- golden_donors_list.items = AppInfo.golden_donors
+ golden_donors_list.items = app_info.golden_donors
golden_donors_list.setup()
- diamond_donors_list.items = AppInfo.diamond_donors
+ diamond_donors_list.items = app_info.diamond_donors
diamond_donors_list.setup()
# There can be multiple translators for a single locale.
for lang in TranslationServer.get_loaded_locales():
diff --git a/src/ui_parts/camera.gd b/src/ui_parts/camera.gd
index e29646e4..85faa59f 100644
--- a/src/ui_parts/camera.gd
+++ b/src/ui_parts/camera.gd
@@ -18,7 +18,7 @@ var surface := RenderingServer.canvas_item_create() # Used for drawing the numb
func _ready() -> void:
RenderingServer.canvas_item_set_parent(surface, ci)
- SVG.root_tag.resized.connect(queue_redraw)
+ SVG.resized.connect(queue_redraw)
Indications.zoom_changed.connect(change_zoom)
Indications.zoom_changed.connect(queue_redraw)
diff --git a/src/ui_parts/code_editor.gd b/src/ui_parts/code_editor.gd
index 9f254195..73176e38 100644
--- a/src/ui_parts/code_editor.gd
+++ b/src/ui_parts/code_editor.gd
@@ -23,10 +23,9 @@ func _ready() -> void:
setup_theme()
setup_highlighter()
code_edit.clear_undo_history()
- SVG.root_tag.attribute_changed.connect(auto_update_text.unbind(1))
- SVG.root_tag.child_attribute_changed.connect(auto_update_text.unbind(1))
- SVG.root_tag.tag_layout_changed.connect(auto_update_text)
- SVG.root_tag.changed_unknown.connect(auto_update_text)
+ SVG.attribute_somewhere_changed.connect(auto_update_text.unbind(1))
+ SVG.tag_layout_changed.connect(auto_update_text)
+ SVG.changed_unknown.connect(auto_update_text)
GlobalSettings.save_data.current_file_path_changed.connect(update_file_button)
import_button.pressed.connect(ShortcutUtils.fn("import"))
export_button.pressed.connect(ShortcutUtils.fn("export"))
@@ -124,7 +123,7 @@ func set_new_text(svg_text: String) -> void:
func _on_svg_code_edit_text_changed() -> void:
SVG.text = code_edit.text
- SVG.update_tags()
+ SVG.sync()
func update_size_label() -> void:
diff --git a/src/ui_parts/display.gd b/src/ui_parts/display.gd
index 4c0732dc..60fd1e97 100644
--- a/src/ui_parts/display.gd
+++ b/src/ui_parts/display.gd
@@ -40,7 +40,6 @@ func _ready() -> void:
update_theme()
update_snap_config()
get_window().window_input.connect(_update_input_debug)
- Indications.imported_reference.connect(finish_reference_load)
view_settings_updated.emit(grid_visuals.visible, controls.visible,
viewport.display_texture.rasterized)
@@ -145,6 +144,9 @@ func _on_more_options_pressed() -> void:
"about_info")
about_btn.expand_icon = true
var buttons_arr: Array[Button] = [
+ ContextPopup.create_button(TranslationServer.translate("Check for updates"),
+ ShortcutUtils.fn("check_updates"), false,
+ load("res://visual/icons/Reload.svg"), "check_updates"),
about_btn,
ContextPopup.create_button(TranslationServer.translate("Donate…"),
ShortcutUtils.fn("about_donate"), false, load("res://visual/icons/Heart.svg"),
@@ -154,11 +156,8 @@ func _on_more_options_pressed() -> void:
"about_repo"),
ContextPopup.create_button(TranslationServer.translate("GodSVG website"),
ShortcutUtils.fn("about_website"), false, load("res://visual/icons/Link.svg"),
- "about_website"),
- ContextPopup.create_button(TranslationServer.translate("Check for updates"),
- ShortcutUtils.fn("check_updates"), false,
- load("res://visual/icons/Reload.svg"), "check_updates")]
- var separator_indices: Array[int] = [2, 4]
+ "about_website")]
+ var separator_indices: Array[int] = [1, 3]
var more_popup := ContextPopup.new()
more_popup.setup(buttons_arr, true, -1, separator_indices)
@@ -182,7 +181,7 @@ func toggle_rasterization() -> void:
viewport.display_texture.rasterized)
func load_reference_image() -> void:
- FileUtils.open_reference_load_dialog()
+ FileUtils.open_reference_load_dialog(finish_reference_load)
func toggle_reference_image() -> void:
reference_texture.visible = not reference_texture.visible
diff --git a/src/ui_parts/display_texture.gd b/src/ui_parts/display_texture.gd
index 0754068e..41bc297c 100644
--- a/src/ui_parts/display_texture.gd
+++ b/src/ui_parts/display_texture.gd
@@ -17,10 +17,9 @@ var update_pending := false
func _ready() -> void:
- SVG.root_tag.tag_layout_changed.connect(queue_update)
- SVG.root_tag.changed_unknown.connect(queue_update)
- SVG.root_tag.attribute_changed.connect(queue_update.unbind(1))
- SVG.root_tag.child_attribute_changed.connect(queue_update.unbind(1))
+ SVG.tag_layout_changed.connect(queue_update)
+ SVG.changed_unknown.connect(queue_update)
+ SVG.attribute_somewhere_changed.connect(queue_update.unbind(1))
Indications.zoom_changed.connect(queue_update)
queue_update()
@@ -47,10 +46,9 @@ func svg_update() -> void:
display_rect.end.x = minf(display_rect.end.x, SVG.root_tag.width)
display_rect.end.y = minf(display_rect.end.y, SVG.root_tag.height)
- var svg_text := SVGParser.svg_to_text_custom(SVG.root_tag, display_rect.size.x,
+ var svg_text := SVGParser.root_to_text_custom(SVG.root_tag, display_rect.size.x,
display_rect.size.y, Rect2(SVG.root_tag.world_to_canvas(display_rect.position),
display_rect.size / SVG.root_tag.canvas_transform.get_scale()))
-
var img := Image.new()
var err := img.load_svg_from_string(svg_text, image_zoom)
if err == OK:
diff --git a/src/ui_parts/handles_manager.gd b/src/ui_parts/handles_manager.gd
index ca2193fb..4b5adba6 100644
--- a/src/ui_parts/handles_manager.gd
+++ b/src/ui_parts/handles_manager.gd
@@ -75,11 +75,10 @@ func render_handle_textures() -> void:
func _ready() -> void:
render_handle_textures()
RenderingServer.canvas_item_set_parent(surface, get_canvas_item())
- SVG.root_tag.attribute_changed.connect(queue_update.unbind(1))
- SVG.root_tag.child_attribute_changed.connect(queue_redraw.unbind(1))
- SVG.root_tag.child_attribute_changed.connect(sync_handles.unbind(1))
- SVG.root_tag.tag_layout_changed.connect(queue_update)
- SVG.root_tag.changed_unknown.connect(queue_update)
+ SVG.attribute_somewhere_changed.connect(queue_redraw.unbind(1))
+ SVG.attribute_somewhere_changed.connect(sync_handles.unbind(1))
+ SVG.tag_layout_changed.connect(queue_update)
+ SVG.changed_unknown.connect(queue_update)
Indications.selection_changed.connect(queue_redraw)
Indications.hover_changed.connect(queue_redraw)
Indications.zoom_changed.connect(queue_redraw)
@@ -102,32 +101,24 @@ func _process(_delta: float) -> void:
func update_handles() -> void:
handles.clear()
- for tid in SVG.root_tag.get_all_tids():
- var tag := SVG.root_tag.get_tag(tid)
+ for tag in SVG.root_tag.get_all_tags():
match tag.name:
"circle":
- handles.append(generate_xy_handle(tid, tag, "cx", "cy", "transform"))
- handles.append(generate_delta_handle(tid, tag, "cx", "cy", "transform",
- "r", true))
+ handles.append(XYHandle.new(tag, "cx", "cy"))
+ handles.append(DeltaHandle.new(tag, "cx", "cy", "r", true))
"ellipse":
- handles.append(generate_xy_handle(tid, tag, "cx", "cy", "transform"))
- handles.append(generate_delta_handle(tid, tag, "cx", "cy", "transform",
- "rx", true))
- handles.append(generate_delta_handle(tid, tag, "cx", "cy", "transform",
- "ry", false))
+ handles.append(XYHandle.new(tag, "cx", "cy"))
+ handles.append(DeltaHandle.new(tag, "cx", "cy", "rx", true))
+ handles.append(DeltaHandle.new(tag, "cx", "cy", "ry", false))
"rect":
- handles.append(generate_xy_handle(tid, tag, "x", "y", "transform"))
- handles.append(generate_xy_handle(tid, tag, "x", "y", "transform"))
- handles.append(generate_delta_handle(tid, tag, "x", "y", "transform",
- "width", true))
- handles.append(generate_delta_handle(tid, tag, "x", "y", "transform",
- "height", false))
+ handles.append(XYHandle.new(tag, "x", "y"))
+ handles.append(DeltaHandle.new(tag, "x", "y", "width", true))
+ handles.append(DeltaHandle.new(tag, "x", "y", "height", false))
"line":
- handles.append(generate_xy_handle(tid, tag, "x1", "y1", "transform"))
- handles.append(generate_xy_handle(tid, tag, "x2", "y2", "transform"))
+ handles.append(XYHandle.new(tag, "x1", "y1"))
+ handles.append(XYHandle.new(tag, "x2", "y2"))
"path":
- handles += generate_path_handles(tid, tag.attributes.d,
- tag.attributes.transform)
+ handles += generate_path_handles(tag)
# Pretend the mouse was moved to update the hovering.
var mouse_motion_event := InputEventMouseMotion.new()
mouse_motion_event.position = get_viewport().get_mouse_position()
@@ -146,42 +137,28 @@ func sync_handles() -> void:
else:
handle.sync()
- for tid in SVG.root_tag.get_all_tids():
- var tag := SVG.root_tag.get_tag(tid)
+ for tag in SVG.root_tag.get_all_tags():
if tag.name == "path":
- handles += generate_path_handles(tid, tag.attributes.d, tag.attributes.transform)
+ handles += generate_path_handles(tag)
queue_redraw()
-func generate_path_handles(tid: PackedInt32Array, data_attrib: AttributePath,
-t_attrib: AttributeTransform) -> Array[Handle]:
+func generate_path_handles(tag: Tag) -> Array[Handle]:
var path_handles: Array[Handle] = []
+ var data_attrib: AttributePathdata = tag.get_attribute("d")
for idx in data_attrib.get_command_count():
var path_command := data_attrib.get_command(idx)
if not path_command.command_char in "Zz":
- path_handles.append(PathHandle.new(tid, data_attrib, t_attrib, idx))
+ path_handles.append(PathHandle.new(tag, idx))
if path_command.command_char in "CcQq":
- var tangent := PathHandle.new(tid, data_attrib, t_attrib, idx, "x1", "y1")
+ var tangent := PathHandle.new(tag, idx, "x1", "y1")
tangent.display_mode = Handle.Display.SMALL
path_handles.append(tangent)
if path_command.command_char in "CcSs":
- var tangent := PathHandle.new(tid, data_attrib, t_attrib, idx, "x2", "y2")
+ var tangent := PathHandle.new(tag, idx, "x2", "y2")
tangent.display_mode = Handle.Display.SMALL
path_handles.append(tangent)
return path_handles
-# Helpers for generating the handles when the tag is at hand.
-func generate_xy_handle(tid: PackedInt32Array, tag: Tag, x_attrib_name: String,\
-y_attrib_name: String, t_attrib_name: String) -> XYHandle:
- return XYHandle.new(tid, tag.attributes[x_attrib_name],
- tag.attributes[y_attrib_name], tag.attributes[t_attrib_name])
-
-func generate_delta_handle(tid: PackedInt32Array, tag: Tag, x_attrib_name: String,\
-y_attrib_name: String, t_attrib_name: String, delta_attrib_name: String,\
-horizontal: bool) -> DeltaHandle:
- return DeltaHandle.new(tid, tag.attributes[x_attrib_name],
- tag.attributes[y_attrib_name], tag.attributes[t_attrib_name],
- tag.attributes[delta_attrib_name], horizontal)
-
func _draw() -> void:
# Store contours of shapes.
@@ -195,18 +172,15 @@ func _draw() -> void:
var hovered_multiline := PackedVector2Array()
var hovered_selected_multiline := PackedVector2Array()
- for tid in SVG.root_tag.get_all_tids():
- var tag := SVG.root_tag.get_tag(tid)
- var attribs := tag.attributes
-
+ for tag: Tag in SVG.root_tag.get_all_tags():
# Determine if the tag is hovered/selected or has a hovered/selected parent.
- var tag_hovered := Indications.is_hovered(tid, -1, true)
- var tag_selected := Indications.is_selected(tid, -1, true)
+ var tag_hovered := Indications.is_hovered(tag.xid, -1, true)
+ var tag_selected := Indications.is_selected(tag.xid, -1, true)
match tag.name:
"circle":
- var c := Vector2(attribs.cx.get_num(), attribs.cy.get_num())
- var r: float = attribs.r.get_num()
+ var c := Vector2(tag.get_attribute_num("cx"), tag.get_attribute_num("cy"))
+ var r: float = tag.get_attribute_num("r")
var points := PackedVector2Array()
points.resize(181)
@@ -215,8 +189,9 @@ func _draw() -> void:
points[i] = c + Vector2(cos(d), sin(d)) * r
points[180] = points[0]
var extras := PackedVector2Array([c, c + Vector2(r, 0)])
- points = attribs.transform.get_final_transform() * points
- extras = attribs.transform.get_final_transform() * extras
+ var final_transform := tag.get_transform()
+ points = final_transform * points
+ extras = final_transform * extras
if tag_hovered and tag_selected:
hovered_selected_polylines.append(points)
@@ -232,20 +207,19 @@ func _draw() -> void:
normal_multiline += extras
"ellipse":
- var c := Vector2(attribs.cx.get_num(), attribs.cy.get_num())
- var rx: float = attribs.rx.get_num()
- var ry: float = attribs.ry.get_num()
+ var c := Vector2(tag.get_attribute_num("cx"), tag.get_attribute_num("cy"))
# Squished circle.
var points := PackedVector2Array()
points.resize(181)
for i in 180:
var d := i * TAU/180
- points[i] = c + Vector2(cos(d) * rx, sin(d) * ry)
+ points[i] = c + Vector2(cos(d) * tag.rx, sin(d) * tag.ry)
points[180] = points[0]
var extras := PackedVector2Array([
- c, c + Vector2(rx, 0), c, c + Vector2(0, ry)])
- points = attribs.transform.get_final_transform() * points
- extras = attribs.transform.get_final_transform() * extras
+ c, c + Vector2(tag.rx, 0), c, c + Vector2(0, tag.ry)])
+ var final_transform := tag.get_transform()
+ points = final_transform * points
+ extras = final_transform * extras
if tag_hovered and tag_selected:
hovered_selected_polylines.append(points)
@@ -261,12 +235,12 @@ func _draw() -> void:
normal_multiline += extras
"rect":
- var x: float = attribs.x.get_num()
- var y: float = attribs.y.get_num()
- var rect_width: float = attribs.width.get_num()
- var rect_height: float = attribs.height.get_num()
- var rx: float = attribs.rx.get_num()
- var ry: float = attribs.ry.get_num()
+ var x := tag.get_attribute_num("x")
+ var y := tag.get_attribute_num("y")
+ var rect_width := tag.get_attribute_num("width")
+ var rect_height := tag.get_attribute_num("height")
+ var rx: float = tag.rx
+ var ry: float = tag.ry
var points := PackedVector2Array()
if rx == 0 and ry == 0:
# Basic rectangle.
@@ -306,8 +280,9 @@ func _draw() -> void:
points[185] = points[0]
var extras := PackedVector2Array([Vector2(x, y), Vector2(x + rect_width, y),
Vector2(x, y), Vector2(x, y + rect_height)])
- points = attribs.transform.get_final_transform() * points
- extras = attribs.transform.get_final_transform() * extras
+ var final_transform := tag.get_transform()
+ points = final_transform * points
+ extras = final_transform * extras
if tag_hovered and tag_selected:
hovered_selected_polylines.append(points)
@@ -323,13 +298,13 @@ func _draw() -> void:
normal_multiline += extras
"line":
- var x1: float = attribs.x1.get_num()
- var y1: float = attribs.y1.get_num()
- var x2: float = attribs.x2.get_num()
- var y2: float = attribs.y2.get_num()
+ var x1 := tag.get_attribute_num("x1")
+ var y1 := tag.get_attribute_num("y1")
+ var x2 := tag.get_attribute_num("x2")
+ var y2 := tag.get_attribute_num("y2")
var points := PackedVector2Array([Vector2(x1, y1), Vector2(x2, y2)])
- points = attribs.transform.get_final_transform() * points
+ points = tag.get_transform() * points
if tag_hovered and tag_selected:
hovered_selected_polylines.append(points)
@@ -341,7 +316,7 @@ func _draw() -> void:
normal_polylines.append(points)
"path":
- var pathdata: AttributePath = attribs.d
+ var pathdata: AttributePathdata = tag.get_attribute("d")
if pathdata.get_command_count() == 0 or\
not pathdata.get_command(0).command_char in "Mm":
continue # Nothing to draw.
@@ -356,10 +331,10 @@ func _draw() -> void:
var relative := cmd.relative
current_mode = Utils.InteractionType.NONE
- if Indications.is_hovered(tid, cmd_idx, true):
+ if Indications.is_hovered(tag.xid, cmd_idx, true):
@warning_ignore("int_as_enum_without_cast")
current_mode += Utils.InteractionType.HOVERED
- if Indications.is_selected(tid, cmd_idx, true):
+ if Indications.is_selected(tag.xid, cmd_idx, true):
@warning_ignore("int_as_enum_without_cast")
current_mode += Utils.InteractionType.SELECTED
@@ -526,8 +501,10 @@ func _draw() -> void:
points = PackedVector2Array([cmd.start, end])
"M":
continue
- points = attribs.transform.get_final_transform() * points
- tangent_points = attribs.transform.get_final_transform() * tangent_points
+
+ var final_transform := tag.get_transform()
+ points = final_transform * points
+ tangent_points = final_transform * tangent_points
match current_mode:
Utils.InteractionType.NONE:
normal_polylines.append(points)
@@ -550,37 +527,15 @@ func _draw() -> void:
RenderingServer.canvas_item_set_transform(surface, Transform2D(0.0,
Vector2(1 / Indications.zoom, 1 / Indications.zoom), 0.0, Vector2.ZERO))
- for polyline in normal_polylines:
- draw_polyline(polyline, normal_color, contour_width, true)
- for polyline in selected_polylines:
- draw_polyline(polyline, selected_color, contour_width, true)
- for polyline in hovered_polylines:
- draw_polyline(polyline, hovered_color, contour_width, true)
- for polyline in hovered_selected_polylines:
- draw_polyline(polyline, hovered_selected_color, contour_width, true)
-
- if not normal_multiline.is_empty():
- draw_multiline(normal_multiline,
- Color(normal_color, tangent_alpha), tangent_width, true)
- if not selected_multiline.is_empty():
- draw_multiline(selected_multiline,
- Color(selected_color, tangent_alpha), tangent_width, true)
- if not hovered_multiline.is_empty():
- draw_multiline(hovered_multiline,
- Color(hovered_color, tangent_alpha), tangent_width, true)
- if not hovered_selected_multiline.is_empty():
- draw_multiline(hovered_selected_multiline,
- Color(hovered_selected_color, tangent_alpha), tangent_width, true)
-
- # First gather all handles in 4 categories, then draw them in the right order.
+ # First gather all handles in 4 categories, to then draw them in the right order.
var normal_handles: Array[Handle] = []
var selected_handles: Array[Handle] = []
var hovered_handles: Array[Handle] = []
var hovered_selected_handles: Array[Handle] = []
for handle in handles:
var cmd_idx: int = handle.command_index if handle is PathHandle else -1
- var is_hovered := Indications.is_hovered(handle.tid, cmd_idx, true)
- var is_selected := Indications.is_selected(handle.tid, cmd_idx, true)
+ var is_hovered := Indications.is_hovered(handle.tag.xid, cmd_idx, true)
+ var is_selected := Indications.is_selected(handle.tag.xid, cmd_idx, true)
if is_hovered and is_selected:
hovered_selected_handles.append(handle)
@@ -592,22 +547,57 @@ func _draw() -> void:
normal_handles.append(handle)
RenderingServer.canvas_item_clear(surface)
+
+ for polyline in normal_polylines:
+ draw_polyline(polyline, normal_color, contour_width, true)
+ if not normal_multiline.is_empty():
+ draw_multiline(normal_multiline,
+ Color(normal_color, tangent_alpha), tangent_width, true)
for handle in normal_handles:
var texture: Texture2D = normal_handle_textures[handle.display_mode]
texture.draw(surface, SVG.root_tag.canvas_to_world(handle.transform * handle.pos) *\
Indications.zoom - texture.get_size() / 2)
+
+ for polyline in selected_polylines:
+ draw_polyline(polyline, selected_color, contour_width, true)
+ if not selected_multiline.is_empty():
+ draw_multiline(selected_multiline,
+ Color(selected_color, tangent_alpha), tangent_width, true)
for handle in selected_handles:
var texture: Texture2D = selected_handle_textures[handle.display_mode]
texture.draw(surface, SVG.root_tag.canvas_to_world(handle.transform * handle.pos) *\
Indications.zoom - texture.get_size() / 2)
+
+ for polyline in hovered_polylines:
+ draw_polyline(polyline, hovered_color, contour_width, true)
+ if not hovered_multiline.is_empty():
+ draw_multiline(hovered_multiline,
+ Color(hovered_color, tangent_alpha), tangent_width, true)
for handle in hovered_handles:
var texture: Texture2D = hovered_handle_textures[handle.display_mode]
texture.draw(surface, SVG.root_tag.canvas_to_world(handle.transform * handle.pos) *\
Indications.zoom - texture.get_size() / 2)
+
+ for polyline in hovered_selected_polylines:
+ draw_polyline(polyline, hovered_selected_color, contour_width, true)
+ if not hovered_selected_multiline.is_empty():
+ draw_multiline(hovered_selected_multiline,
+ Color(hovered_selected_color, tangent_alpha), tangent_width, true)
for handle in hovered_selected_handles:
var texture: Texture2D = hovered_selected_handle_textures[handle.display_mode]
texture.draw(surface, SVG.root_tag.canvas_to_world(handle.transform * handle.pos) *\
Indications.zoom - texture.get_size() / 2)
+
+ for tag: Tag in SVG.root_tag.get_all_tags():
+ if tag is TagShape:# and tag.xid in Indications.selected_xids:
+ var bounding_box: Rect2 = tag.get_bounding_box()
+ var transform := tag.get_attribute_final_transform("transform")
+ var transform_scale := transform.get_scale()
+ if bounding_box != Rect2():
+ draw_set_transform_matrix(transform.scaled(Vector2(1, 1) / transform_scale))
+ bounding_box *= Transform2D(0, transform_scale, 0, Vector2.ZERO)
+ draw_rect(bounding_box.grow(1.0 / Indications.zoom), Color.YELLOW, false,
+ 1.0 / Indications.zoom, true)
var dragged_handle: Handle = null
@@ -632,9 +622,9 @@ func respond_to_input_event(event: InputEvent) -> void:
if is_instance_valid(nearest_handle):
hovered_handle = nearest_handle
if hovered_handle is PathHandle:
- Indications.set_hovered(hovered_handle.tid, hovered_handle.command_index)
+ Indications.set_hovered(hovered_handle.tag.xid, hovered_handle.command_index)
else:
- Indications.set_hovered(hovered_handle.tid)
+ Indications.set_hovered(hovered_handle.tag.xid)
else:
hovered_handle = null
Indications.clear_hovered()
@@ -673,23 +663,23 @@ func respond_to_input_event(event: InputEvent) -> void:
dragged_handle = hovered_handle
dragged_handle.initial_pos = dragged_handle.pos
var inner_idx := -1
- var dragged_tid := dragged_handle.tid
+ var dragged_xid := dragged_handle.tag.xid
if dragged_handle is PathHandle:
inner_idx = dragged_handle.command_index
if event.double_click and inner_idx != -1:
# Unselect the tag, so then it's selected again in the subpath.
- Indications.ctrl_select(dragged_tid, inner_idx)
+ Indications.ctrl_select(dragged_xid, inner_idx)
var subpath_range: Vector2i =\
- dragged_handle.path_attribute.get_subpath(inner_idx)
+ dragged_handle.tag.get_attribute("d").get_subpath(inner_idx)
for idx in range(subpath_range.x, subpath_range.y + 1):
- Indications.ctrl_select(dragged_tid, idx)
+ Indications.ctrl_select(dragged_xid, idx)
elif event.is_command_or_control_pressed():
- Indications.ctrl_select(dragged_tid, inner_idx)
+ Indications.ctrl_select(dragged_xid, inner_idx)
elif event.shift_pressed:
- Indications.shift_select(dragged_tid, inner_idx)
+ Indications.shift_select(dragged_xid, inner_idx)
else:
- Indications.normal_select(dragged_tid, inner_idx)
+ Indications.normal_select(dragged_xid, inner_idx)
elif is_instance_valid(dragged_handle) and event.is_released():
if was_handle_moved:
var new_pos := dragged_handle.transform.affine_inverse() *\
@@ -709,18 +699,18 @@ func respond_to_input_event(event: InputEvent) -> void:
Indications.clear_all_selections()
HandlerGUI.popup_under_pos(create_tag_context(event_pos), popup_pos, vp)
else:
- var hovered_tid := hovered_handle.tid
+ var hovered_xid := hovered_handle.tag.xid
var inner_idx := -1
if hovered_handle is PathHandle:
inner_idx = hovered_handle.command_index
- if (Indications.semi_selected_tid != hovered_tid or\
+ if (Indications.semi_selected_xid != hovered_xid or\
not inner_idx in Indications.inner_selections) and\
- not hovered_tid in Indications.selected_tids:
- Indications.normal_select(hovered_tid, inner_idx)
+ not hovered_xid in Indications.selected_xids:
+ Indications.normal_select(hovered_xid, inner_idx)
HandlerGUI.popup_under_pos(Indications.get_selection_context(
HandlerGUI.popup_under_pos.bind(popup_pos, vp),
- Indications.SelectionContext.VIEWPORT), popup_pos, vp)
+ Indications.Context.VIEWPORT), popup_pos, vp)
func find_nearest_handle(event_pos: Vector2) -> Handle:
var nearest_handle: Handle = null
@@ -737,16 +727,16 @@ func find_nearest_handle(event_pos: Vector2) -> Handle:
func _on_handle_added() -> void:
if not get_viewport_rect().has_point(get_viewport().get_mouse_position()):
- if not Indications.semi_selected_tid.is_empty():
- SVG.root_tag.get_tag(Indications.semi_selected_tid).attributes.d.\
- sync_after_commands_change(Attribute.SyncMode.FINAL)
+ if not Indications.semi_selected_xid.is_empty():
+ SVG.root_tag.get_tag(Indications.semi_selected_xid).get_attribute("d").\
+ sync_after_commands_change(true)
return
update_handles()
for handle in handles:
- if handle is PathHandle and handle.tid == Indications.semi_selected_tid and\
+ if handle is PathHandle and handle.tag.xid == Indications.semi_selected_xid and\
handle.command_index == Indications.inner_selections[0]:
- Indications.set_hovered(handle.tid, handle.command_index)
+ Indications.set_hovered(handle.tag.xid, handle.command_index)
dragged_handle = handle
# Move the handle that's being dragged.
var mouse_pos := get_global_mouse_position()
@@ -769,16 +759,11 @@ func create_tag_context(pos: Vector2) -> ContextPopup:
btn.add_theme_font_override("font", load("res://visual/fonts/FontMono.ttf"))
btn_array.append(btn)
var tag_context := ContextPopup.new()
- tag_context.setup_with_title(btn_array, TranslationServer.translate("New tag"), true)
+ var separation_indices: Array[int] = [1]
+ tag_context.setup_with_title(btn_array, TranslationServer.translate("New shape"), true,
+ -1, separation_indices)
return tag_context
func add_tag_at_pos(tag_name: String, pos: Vector2) -> void:
- var tag: Tag
- match tag_name:
- "path": tag = TagPath.new()
- "circle": tag = TagCircle.new()
- "ellipse": tag = TagEllipse.new()
- "rect": tag = TagRect.new()
- "line": tag = TagLine.new()
- tag.user_setup(pos)
- SVG.root_tag.add_tag(tag, PackedInt32Array([SVG.root_tag.get_child_count()]))
+ SVG.root_tag.add_tag(DB.tag(tag_name, pos),
+ PackedInt32Array([SVG.root_tag.get_child_count()]))
diff --git a/src/ui_parts/import_warning_dialog.gd b/src/ui_parts/import_warning_dialog.gd
index ea4141f5..69f9584d 100644
--- a/src/ui_parts/import_warning_dialog.gd
+++ b/src/ui_parts/import_warning_dialog.gd
@@ -14,19 +14,20 @@ func _notification(what: int) -> void:
if what == Utils.CustomNotification.LANGUAGE_CHANGED:
setup()
-
func _ready() -> void:
+ imported.connect(queue_free)
+ ok_button.pressed.connect(imported.emit)
setup()
+
func setup() -> void:
- imported.connect(queue_free)
# Convert forward and backward to show how GodSVG would display the given SVG.
- var imported_text_parse_result := SVGParser.text_to_svg(imported_text)
- var preview_text := SVGParser.svg_to_text(imported_text_parse_result.svg)
- var preview_parse_result := SVGParser.text_to_svg(preview_text)
+ var imported_text_parse_result := SVGParser.text_to_root(imported_text)
+ var preview_text := SVGParser.root_to_text(imported_text_parse_result.svg)
+ var preview_parse_result := SVGParser.text_to_root(preview_text)
var preview := preview_parse_result.svg
if is_instance_valid(preview):
- texture_preview.setup(SVGParser.svg_to_text(preview), preview.get_size())
+ texture_preview.setup(SVGParser.root_to_text(preview), preview.get_size())
if imported_text_parse_result.error != SVGParser.ParseError.OK:
texture_preview.hide()
@@ -54,20 +55,22 @@ func set_svg(text: String) -> void:
imported_text = text
-func get_svg_warnings(svg_tag: TagSVG) -> Array[String]:
- var warnings: Array[String] = []
- var tids := svg_tag.get_all_tids()
- for tid in tids:
- var tag := svg_tag.get_tag(tid)
- if tag is TagUnknown:
- warnings.append("%s: %s" % [TranslationServer.translate("Unknown tag"),
- tag.name])
+func get_svg_warnings(svg_tag: TagSVG) -> PackedStringArray:
+ var unrecognized_tags := PackedStringArray()
+ var unrecognized_attributes := PackedStringArray()
+ for tag in svg_tag.get_all_tags():
+ if tag is TagUnrecognized:
+ if not tag.name in unrecognized_tags:
+ unrecognized_tags.append(tag.name)
else:
- for unknown_attrib in tag.unknown_attributes:
- warnings.append("%s: %s" % [TranslationServer.translate("Unknown attribute"),
- unknown_attrib.name])
+ for attribute_key in tag.attributes:
+ if not attribute_key in DB.recognized_attributes[tag.name] and\
+ not attribute_key in unrecognized_attributes:
+ unrecognized_attributes.append(attribute_key)
+ var warnings := PackedStringArray()
+ for tag in unrecognized_tags:
+ warnings.append("%s: %s" % [TranslationServer.translate("Unrecognized tag"), tag])
+ for attribute in unrecognized_attributes:
+ warnings.append("%s: %s" % [TranslationServer.translate("Unrecognized attribute"),
+ attribute])
return warnings
-
-
-func _on_ok_button_pressed() -> void:
- imported.emit()
diff --git a/src/ui_parts/import_warning_dialog.tscn b/src/ui_parts/import_warning_dialog.tscn
index e8a221ed..a79bd763 100644
--- a/src/ui_parts/import_warning_dialog.tscn
+++ b/src/ui_parts/import_warning_dialog.tscn
@@ -77,5 +77,3 @@ layout_mode = 2
size_flags_horizontal = 6
mouse_default_cursor_shape = 2
text = "Import"
-
-[connection signal="pressed" from="VBoxContainer/ButtonContainer/OKButton" to="." method="_on_ok_button_pressed"]
diff --git a/src/ui_parts/inspector.gd b/src/ui_parts/inspector.gd
index 63568a2f..5b131fc0 100644
--- a/src/ui_parts/inspector.gd
+++ b/src/ui_parts/inspector.gd
@@ -12,13 +12,13 @@ func _notification(what: int) -> void:
func _ready() -> void:
update_translation()
- SVG.root_tag.tag_layout_changed.connect(full_rebuild)
- SVG.root_tag.changed_unknown.connect(full_rebuild)
+ SVG.tag_layout_changed.connect(full_rebuild)
+ SVG.changed_unknown.connect(full_rebuild)
full_rebuild()
func update_translation() -> void:
- $AddButton.text = TranslationServer.translate("Add new tag")
+ add_button.text = TranslationServer.translate("Add element")
func full_rebuild() -> void:
@@ -28,30 +28,26 @@ func full_rebuild() -> void:
for tag_idx in SVG.root_tag.get_child_count():
var tag_editor := TagFrame.instantiate()
tag_editor.tag = SVG.root_tag.child_tags[tag_idx]
- tag_editor.tid = PackedInt32Array([tag_idx])
tags_container.add_child(tag_editor)
func add_tag(tag_name: String) -> void:
- var new_tid := PackedInt32Array([SVG.root_tag.get_child_count()])
- var new_tag: Tag
- match tag_name:
- "circle": new_tag = TagCircle.new()
- "ellipse": new_tag = TagEllipse.new()
- "rect": new_tag = TagRect.new()
- "path": new_tag = TagPath.new()
- "line": new_tag = TagLine.new()
- new_tag.user_setup()
- SVG.root_tag.add_tag(new_tag, new_tid)
+ var new_tag := DB.tag(tag_name)
+ if tag_name in ["linearGradient", "radialGradient"]:
+ SVG.root_tag.add_tag(new_tag, PackedInt32Array([0]))
+ else:
+ SVG.root_tag.add_tag(new_tag, PackedInt32Array([SVG.root_tag.get_child_count()]))
func _on_add_button_pressed() -> void:
var btn_array: Array[Button] = []
- for tag_name in ["path", "circle", "ellipse", "rect", "line"]:
+ for tag_name in PackedStringArray(["path", "circle", "ellipse", "rect", "line",
+ "g", "linearGradient", "radialGradient"]):
var btn := ContextPopup.create_button(tag_name, add_tag.bind(tag_name), false,
DB.get_tag_icon(tag_name))
btn.add_theme_font_override("font", load("res://visual/fonts/FontMono.ttf"))
btn_array.append(btn)
+ var separator_indices: Array[int] = [1, 5]
var add_popup := ContextPopup.new()
- add_popup.setup(btn_array, true, add_button.size.x)
+ add_popup.setup(btn_array, true, add_button.size.x, separator_indices)
HandlerGUI.popup_under_rect(add_popup, add_button.get_global_rect(), get_viewport())
diff --git a/src/ui_parts/move_to_overlay.gd b/src/ui_parts/move_to_overlay.gd
index 4283a17b..3ce96f01 100644
--- a/src/ui_parts/move_to_overlay.gd
+++ b/src/ui_parts/move_to_overlay.gd
@@ -1,16 +1,16 @@
extends Control
-# Runs every time the mouse moves. Returning true means you can drop the TIDs.
+# Runs every time the mouse moves. Returning true means you can drop the XIDs.
func _can_drop_data(_at_position: Vector2, data: Variant) -> bool:
if not data is Array[PackedInt32Array]:
return false
- get_parent().update_proposed_tid()
- for tid in data:
- if Utils.is_tid_parent(tid, Indications.proposed_drop_tid):
+ get_parent().update_proposed_xid()
+ for xid in data:
+ if Utils.is_xid_parent(xid, Indications.proposed_drop_xid):
return false
return true
-# Runs when you drop the TIDs.
+# Runs when you drop the XIDs.
func _drop_data(_at_position: Vector2, data: Variant) -> void:
if data is Array[PackedInt32Array]:
- SVG.root_tag.move_tags_to(data, Indications.proposed_drop_tid)
+ SVG.root_tag.move_tags_to(data, Indications.proposed_drop_xid)
diff --git a/src/ui_parts/root_tag_editor.gd b/src/ui_parts/root_tag_editor.gd
index 66d4e930..048df1dc 100644
--- a/src/ui_parts/root_tag_editor.gd
+++ b/src/ui_parts/root_tag_editor.gd
@@ -1,9 +1,17 @@
-extends CenterContainer
+extends VBoxContainer
# So, about this editor. Width and height don't have default values, so they use NAN and
# use NumberEdit, rather than NumberField. Viewbox is a list and it also doesn't have a
# default value, so it uses 4 NumberEdits.
+const UnrecognizedField = preload("res://src/ui_elements/unrecognized_field.tscn")
+const ColorField = preload("res://src/ui_elements/color_field.tscn")
+const NumberField = preload("res://src/ui_elements/number_field.tscn")
+const NumberSlider = preload("res://src/ui_elements/number_field_with_slider.tscn")
+const IDField = preload("res://src/ui_elements/id_field.tscn")
+const EnumField = preload("res://src/ui_elements/enum_field.tscn")
+const TransformField = preload("res://src/ui_elements/transform_field.tscn")
+
const NumberEditType = preload("res://src/ui_elements/number_edit.gd")
@onready var width_button: Button = %Size/Width/WidthButton
@@ -15,27 +23,76 @@ const NumberEditType = preload("res://src/ui_elements/number_edit.gd")
@onready var viewbox_edit_y: NumberEditType = %Viewbox/Rect/ViewboxEditY
@onready var viewbox_edit_w: NumberEditType = %Viewbox/Rect/ViewboxEditW
@onready var viewbox_edit_h: NumberEditType = %Viewbox/Rect/ViewboxEditH
+@onready var unknown_container: HFlowContainer
+
+var root_tag: TagRoot
func _ready() -> void:
- SVG.root_tag.resized.connect(update_attributes)
- SVG.root_tag.changed_unknown.connect(update_attributes)
+ root_tag = SVG.root_tag
+ SVG.resized.connect(update_attributes)
+ SVG.changed_unknown.connect(update_attributes)
update_attributes()
func update_attributes() -> void:
- width_edit.set_value(SVG.root_tag.width, false)
- height_edit.set_value(SVG.root_tag.height, false)
- viewbox_edit_x.set_value(SVG.root_tag.viewbox.position.x, false)
- viewbox_edit_y.set_value(SVG.root_tag.viewbox.position.y, false)
- viewbox_edit_w.set_value(SVG.root_tag.viewbox.size.x, false)
- viewbox_edit_h.set_value(SVG.root_tag.viewbox.size.y, false)
+ # If there are unknown attributes, they would always be on top.
+ if is_instance_valid(unknown_container):
+ for child in unknown_container.get_children():
+ child.queue_free()
+ var has_unrecognized_attributes := false
+ for attribute in root_tag.attributes.values():
+ # TODO separate unrecognized attributes from global defaults.
+ if not attribute.name in ["width", "height", "viewBox", "xmlns"]:
+ if not has_unrecognized_attributes:
+ has_unrecognized_attributes = true
+ if is_instance_valid(unknown_container):
+ unknown_container.queue_free()
+ unknown_container = HFlowContainer.new()
+ add_child(unknown_container)
+ move_child(unknown_container, 0)
+
+ var input_field: Control
+ match DB.get_attribute_type(attribute.name):
+ DB.AttributeType.COLOR: input_field = ColorField.instantiate()
+ DB.AttributeType.ENUM: input_field = EnumField.instantiate()
+ DB.AttributeType.TRANSFORM_LIST: input_field = TransformField.instantiate()
+ DB.AttributeType.ID: input_field = IDField.instantiate()
+ DB.AttributeType.NUMERIC:
+ var min_value: float = DB.attribute_numeric_bounds[attribute.name].x
+ var max_value: float = DB.attribute_numeric_bounds[attribute.name].y
+ if is_inf(max_value):
+ input_field = NumberField.instantiate()
+ if not is_inf(min_value):
+ input_field.allow_lower = false
+ input_field.min_value = min_value
+ else:
+ input_field = NumberSlider.instantiate()
+ input_field.allow_lower = false
+ input_field.allow_higher = false
+ input_field.min_value = min_value
+ input_field.max_value = max_value
+ input_field.slider_step = 0.01
+ _: input_field = UnrecognizedField.instantiate()
+ input_field.tag = root_tag
+ input_field.attribute_name = attribute.name
+ unknown_container.add_child(input_field)
+ if not has_unrecognized_attributes and is_instance_valid(unknown_container):
+ unknown_container.queue_free()
+
+ width_edit.set_value(root_tag.width, false)
+ height_edit.set_value(root_tag.height, false)
+ viewbox_edit_x.set_value(root_tag.viewbox.position.x, false)
+ viewbox_edit_y.set_value(root_tag.viewbox.position.y, false)
+ viewbox_edit_w.set_value(root_tag.viewbox.size.x, false)
+ viewbox_edit_h.set_value(root_tag.viewbox.size.y, false)
update_editable()
func update_editable() -> void:
- var is_width_valid: bool = !SVG.root_tag.attributes.width.get_value().is_empty()
- var is_height_valid: bool = !SVG.root_tag.attributes.height.get_value().is_empty()
- var is_viewbox_valid: bool = SVG.root_tag.attributes.viewBox.get_list_size() >= 4
+ var is_width_valid := root_tag.attributes.has("width")
+ var is_height_valid := root_tag.attributes.has("height")
+ var is_viewbox_valid: bool = root_tag.attributes.has("viewBox") and\
+ root_tag.get_attribute("viewBox").get_list_size() >= 4
width_button.set_pressed_no_signal(is_width_valid)
height_button.set_pressed_no_signal(is_height_valid)
@@ -50,65 +107,64 @@ func update_editable() -> void:
func _on_width_edit_value_changed(new_value: float) -> void:
- if is_finite(new_value) and SVG.root_tag.attributes.width.get_num() != new_value:
- SVG.root_tag.width = new_value
- SVG.root_tag.attributes.width.set_num(new_value)
+ if is_finite(new_value) and root_tag.get_attribute("width").get_num() != new_value:
+ root_tag.width = new_value
+ root_tag.set_attribute("width", new_value)
else:
- SVG.root_tag.attributes.width.set_num(SVG.root_tag.width, false)
+ root_tag.set_attribute("width", root_tag.width, false)
func _on_height_edit_value_changed(new_value: float) -> void:
- if is_finite(new_value) and SVG.root_tag.attributes.height.get_num() != new_value:
- SVG.root_tag.height = new_value
- SVG.root_tag.attributes.height.set_num(new_value)
+ if is_finite(new_value) and root_tag.get_attribute("height").get_num() != new_value:
+ root_tag.height = new_value
+ root_tag.set_attribute("height", new_value)
else:
- SVG.root_tag.attributes.height.set_num(SVG.root_tag.height, false)
+ root_tag.set_attribute("height", root_tag.height, false)
func _on_viewbox_edit_x_value_changed(new_value: float) -> void:
- if SVG.root_tag.attributes.viewBox.get_value() != null:
- SVG.root_tag.viewbox.position.x = new_value
- SVG.root_tag.attributes.viewBox.set_list_element(0, new_value)
+ if root_tag.attributes.has("viewBox"):
+ root_tag.viewbox.position.x = new_value
+ root_tag.get_attribute("viewBox").set_list_element(0, new_value)
func _on_viewbox_edit_y_value_changed(new_value: float) -> void:
- if SVG.root_tag.attributes.viewBox.get_value() != null:
- SVG.root_tag.viewbox.position.y = new_value
- SVG.root_tag.attributes.viewBox.set_list_element(1, new_value)
+ if root_tag.attributes.has("viewBox"):
+ root_tag.viewbox.position.y = new_value
+ root_tag.get_attribute("viewBox").set_list_element(1, new_value)
func _on_viewbox_edit_w_value_changed(new_value: float) -> void:
- if SVG.root_tag.attributes.viewBox.get_value() != null and\
- SVG.root_tag.attributes.viewBox.get_list_element(2) != new_value:
- SVG.root_tag.viewbox.size.x = new_value
- SVG.root_tag.attributes.viewBox.set_list_element(2, new_value)
+ if root_tag.attributes.has("viewBox") and\
+ root_tag.get_attribute("viewBox").get_list_element(2) != new_value:
+ root_tag.viewbox.size.x = new_value
+ root_tag.get_attribute("viewBox").set_list_element(2, new_value)
func _on_viewbox_edit_h_value_changed(new_value: float) -> void:
- if SVG.root_tag.attributes.viewBox.get_value() != null and\
- SVG.root_tag.attributes.viewBox.get_list_element(3) != new_value:
- SVG.root_tag.viewbox.size.y = new_value
- SVG.root_tag.attributes.viewBox.set_list_element(3, new_value)
+ if root_tag.attributes.has("viewBox") and\
+ root_tag.get_attribute("viewBox").get_list_element(3) != new_value:
+ root_tag.viewbox.size.y = new_value
+ root_tag.get_attribute("viewBox").set_list_element(3, new_value)
func _on_width_button_toggled(toggled_on: bool) -> void:
if toggled_on:
- SVG.root_tag.attributes.width.set_num(SVG.root_tag.width)
+ root_tag.set_attribute("width", root_tag.width)
else:
- if SVG.root_tag.attributes.viewBox.get_list_size() == 4:
- SVG.root_tag.attributes.width.set_num(NAN)
+ if root_tag.get_attribute("viewBox").get_list_size() == 4:
+ root_tag.set_attribute("width", NAN)
else:
width_button.set_pressed_no_signal(true)
func _on_height_button_toggled(toggled_on: bool) -> void:
if toggled_on:
- SVG.root_tag.attributes.height.set_num(SVG.root_tag.height)
+ root_tag.set_attribute("height", root_tag.height)
else:
- if SVG.root_tag.attributes.viewBox.get_list_size() == 4:
- SVG.root_tag.attributes.height.set_num(NAN)
+ if root_tag.get_attribute("viewBox").get_list_size() == 4:
+ root_tag.set_attribute("height", NAN)
else:
height_button.set_pressed_no_signal(true)
func _on_viewbox_button_toggled(toggled_on: bool) -> void:
if toggled_on:
- SVG.root_tag.attributes.viewBox.set_rect(SVG.root_tag.viewbox)
+ root_tag.set_attribute("viewBox", root_tag.viewbox)
else:
- if not SVG.root_tag.attributes.width.get_value().is_empty() and\
- not SVG.root_tag.attributes.height.get_value().is_empty():
- SVG.root_tag.attributes.viewBox.set_value("")
+ if not root_tag.attributes.has("width") and not root_tag.attributes.has("height"):
+ root_tag.get_attribute_mutable("viewBox").set_value("")
else:
viewbox_button.set_pressed_no_signal(true)
diff --git a/src/ui_parts/root_tag_editor.tscn b/src/ui_parts/root_tag_editor.tscn
index 7af4f1c4..1dafe3d6 100644
--- a/src/ui_parts/root_tag_editor.tscn
+++ b/src/ui_parts/root_tag_editor.tscn
@@ -5,25 +5,28 @@
[ext_resource type="PackedScene" uid="uid://dad7fkhmsooc6" path="res://src/ui_elements/number_edit.tscn" id="3_1gu7n"]
[ext_resource type="Script" path="res://src/ui_elements/BetterToggleButton.gd" id="4_7r848"]
-[node name="RootTagEditor" type="CenterContainer"]
-offset_right = 470.0
+[node name="RootTagEditor" type="VBoxContainer"]
+offset_right = 452.0
offset_bottom = 56.0
script = ExtResource("1_xgyg0")
-[node name="Edits" type="HBoxContainer" parent="."]
+[node name="CenterContainer" type="CenterContainer" parent="."]
+layout_mode = 2
+
+[node name="Edits" type="HBoxContainer" parent="CenterContainer"]
layout_mode = 2
theme_override_constants/separation = 30
-[node name="Size" type="HBoxContainer" parent="Edits"]
+[node name="Size" type="HBoxContainer" parent="CenterContainer/Edits"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 8
-[node name="Width" type="VBoxContainer" parent="Edits/Size"]
+[node name="Width" type="VBoxContainer" parent="CenterContainer/Edits/Size"]
layout_mode = 2
theme_override_constants/separation = 0
-[node name="WidthButton" type="Button" parent="Edits/Size/Width"]
+[node name="WidthButton" type="Button" parent="CenterContainer/Edits/Size/Width"]
layout_mode = 2
size_flags_horizontal = 4
focus_mode = 0
@@ -35,16 +38,16 @@ text = "width"
script = ExtResource("4_7r848")
hover_pressed_font_color = Color(1, 1, 1, 0.4)
-[node name="WidthEdit" parent="Edits/Size/Width" instance=ExtResource("3_1gu7n")]
+[node name="WidthEdit" parent="CenterContainer/Edits/Size/Width" instance=ExtResource("3_1gu7n")]
custom_minimum_size = Vector2(48, 22)
layout_mode = 2
allow_lower = false
-[node name="Height" type="VBoxContainer" parent="Edits/Size"]
+[node name="Height" type="VBoxContainer" parent="CenterContainer/Edits/Size"]
layout_mode = 2
theme_override_constants/separation = 0
-[node name="HeightButton" type="Button" parent="Edits/Size/Height"]
+[node name="HeightButton" type="Button" parent="CenterContainer/Edits/Size/Height"]
layout_mode = 2
size_flags_horizontal = 4
focus_mode = 0
@@ -56,17 +59,17 @@ text = "height"
script = ExtResource("4_7r848")
hover_pressed_font_color = Color(1, 1, 1, 0.4)
-[node name="HeightEdit" parent="Edits/Size/Height" instance=ExtResource("3_1gu7n")]
+[node name="HeightEdit" parent="CenterContainer/Edits/Size/Height" instance=ExtResource("3_1gu7n")]
custom_minimum_size = Vector2(48, 22)
layout_mode = 2
allow_lower = false
-[node name="Viewbox" type="VBoxContainer" parent="Edits"]
+[node name="Viewbox" type="VBoxContainer" parent="CenterContainer/Edits"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 0
-[node name="ViewboxButton" type="Button" parent="Edits/Viewbox"]
+[node name="ViewboxButton" type="Button" parent="CenterContainer/Edits/Viewbox"]
layout_mode = 2
size_flags_horizontal = 4
focus_mode = 0
@@ -78,33 +81,33 @@ text = "viewBox"
script = ExtResource("4_7r848")
hover_pressed_font_color = Color(1, 1, 1, 0.4)
-[node name="Rect" type="HBoxContainer" parent="Edits/Viewbox"]
+[node name="Rect" type="HBoxContainer" parent="CenterContainer/Edits/Viewbox"]
layout_mode = 2
-[node name="ViewboxEditX" parent="Edits/Viewbox/Rect" instance=ExtResource("3_1gu7n")]
+[node name="ViewboxEditX" parent="CenterContainer/Edits/Viewbox/Rect" instance=ExtResource("3_1gu7n")]
custom_minimum_size = Vector2(48, 22)
layout_mode = 2
-[node name="ViewboxEditY" parent="Edits/Viewbox/Rect" instance=ExtResource("3_1gu7n")]
+[node name="ViewboxEditY" parent="CenterContainer/Edits/Viewbox/Rect" instance=ExtResource("3_1gu7n")]
custom_minimum_size = Vector2(48, 22)
layout_mode = 2
-[node name="ViewboxEditW" parent="Edits/Viewbox/Rect" instance=ExtResource("3_1gu7n")]
+[node name="ViewboxEditW" parent="CenterContainer/Edits/Viewbox/Rect" instance=ExtResource("3_1gu7n")]
custom_minimum_size = Vector2(48, 22)
layout_mode = 2
allow_lower = false
-[node name="ViewboxEditH" parent="Edits/Viewbox/Rect" instance=ExtResource("3_1gu7n")]
+[node name="ViewboxEditH" parent="CenterContainer/Edits/Viewbox/Rect" instance=ExtResource("3_1gu7n")]
custom_minimum_size = Vector2(48, 22)
layout_mode = 2
allow_lower = false
-[connection signal="toggled" from="Edits/Size/Width/WidthButton" to="." method="_on_width_button_toggled"]
-[connection signal="value_changed" from="Edits/Size/Width/WidthEdit" to="." method="_on_width_edit_value_changed"]
-[connection signal="toggled" from="Edits/Size/Height/HeightButton" to="." method="_on_height_button_toggled"]
-[connection signal="value_changed" from="Edits/Size/Height/HeightEdit" to="." method="_on_height_edit_value_changed"]
-[connection signal="toggled" from="Edits/Viewbox/ViewboxButton" to="." method="_on_viewbox_button_toggled"]
-[connection signal="value_changed" from="Edits/Viewbox/Rect/ViewboxEditX" to="." method="_on_viewbox_edit_x_value_changed"]
-[connection signal="value_changed" from="Edits/Viewbox/Rect/ViewboxEditY" to="." method="_on_viewbox_edit_y_value_changed"]
-[connection signal="value_changed" from="Edits/Viewbox/Rect/ViewboxEditW" to="." method="_on_viewbox_edit_w_value_changed"]
-[connection signal="value_changed" from="Edits/Viewbox/Rect/ViewboxEditH" to="." method="_on_viewbox_edit_h_value_changed"]
+[connection signal="toggled" from="CenterContainer/Edits/Size/Width/WidthButton" to="CenterContainer" method="_on_width_button_toggled"]
+[connection signal="value_changed" from="CenterContainer/Edits/Size/Width/WidthEdit" to="CenterContainer" method="_on_width_edit_value_changed"]
+[connection signal="toggled" from="CenterContainer/Edits/Size/Height/HeightButton" to="CenterContainer" method="_on_height_button_toggled"]
+[connection signal="value_changed" from="CenterContainer/Edits/Size/Height/HeightEdit" to="CenterContainer" method="_on_height_edit_value_changed"]
+[connection signal="toggled" from="CenterContainer/Edits/Viewbox/ViewboxButton" to="CenterContainer" method="_on_viewbox_button_toggled"]
+[connection signal="value_changed" from="CenterContainer/Edits/Viewbox/Rect/ViewboxEditX" to="CenterContainer" method="_on_viewbox_edit_x_value_changed"]
+[connection signal="value_changed" from="CenterContainer/Edits/Viewbox/Rect/ViewboxEditY" to="CenterContainer" method="_on_viewbox_edit_y_value_changed"]
+[connection signal="value_changed" from="CenterContainer/Edits/Viewbox/Rect/ViewboxEditW" to="CenterContainer" method="_on_viewbox_edit_w_value_changed"]
+[connection signal="value_changed" from="CenterContainer/Edits/Viewbox/Rect/ViewboxEditH" to="CenterContainer" method="_on_viewbox_edit_h_value_changed"]
diff --git a/src/ui_parts/settings_menu.gd b/src/ui_parts/settings_menu.gd
index 6ac1d71b..d93f591e 100644
--- a/src/ui_parts/settings_menu.gd
+++ b/src/ui_parts/settings_menu.gd
@@ -56,10 +56,8 @@ func setup_setting_labels() -> void:
%GeneralVBox/SectionLabel.text = TranslationServer.translate("General")
%NumberVBox/SectionLabel.text = TranslationServer.translate("Numbers")
%ColorVBox/SectionLabel.text = TranslationServer.translate("Colors")
- %PathVBox/SectionLabel.text = TranslationServer.translate("Paths")
- %PathVBox/Note.text = TranslationServer.translate("Always active.")
- %TransformVBox/SectionLabel.text = TranslationServer.translate("Transforms")
- %TransformVBox/Note.text = TranslationServer.translate("Always active.")
+ %PathdataVBox/SectionLabel.text = TranslationServer.translate("Path data")
+ %TransformListVBox/SectionLabel.text = TranslationServer.translate("Transform lists")
%Input/Label.text = TranslationServer.translate("Input")
%Misc/Label.text = TranslationServer.translate("Miscellaneous")
@@ -67,7 +65,6 @@ func setup_setting_labels() -> void:
TranslationServer.translate("Any changes will apply immediately.")
%ContentContainer/Theme/ThemeSettings/Warning.text =\
TranslationServer.translate("Any changes will apply immediately.")
-
tabs.get_node(^"FormattingTab").text = TranslationServer.translate("Formatting")
tabs.get_node(^"PalettesTab").text = TranslationServer.translate("Palettes")
@@ -115,16 +112,19 @@ func setup_setting_labels() -> void:
"Angle precision digits")
%XMLVBox/AddTrailingNewline.label.text = TranslationServer.translate(
"Add trailing newline")
- %XMLVBox/UseShorthandTagSyntax.label.text = TranslationServer.translate(
+ %XMLVBox/ShorthandTags.label.text = TranslationServer.translate(
"Use shorthand tag syntax")
- %NumberVBox/NumberEnable.label.text = TranslationServer.translate(
- "Enable autoformatting")
+ %XMLVBox/PrettyFormatting.label.text = TranslationServer.translate(
+ "Use pretty formatting")
+
+ for checkbox in [%NumberVBox/NumberEnable, %ColorVBox/ColorEnable,
+ %PathdataVBox/PathdataEnable, %TransformListVBox/TransformListEnable]:
+ checkbox.label.text = TranslationServer.translate("Enable autoformatting")
+
%NumberVBox/RemoveZeroPadding.label.text = TranslationServer.translate(
"Remove zero padding")
%NumberVBox/RemoveLeadingZero.label.text = TranslationServer.translate(
"Remove leading zero")
- %ColorVBox/ColorEnable.label.text = TranslationServer.translate(
- "Enable autoformatting")
%ColorVBox/ConvertRGBToHex.label.text = TranslationServer.translate(
"Convert rgb format to hex")
%ColorVBox/ConvertNamedToHex.label.text = TranslationServer.translate(
@@ -133,69 +133,65 @@ func setup_setting_labels() -> void:
"Use shorthand hex code")
%ColorVBox/UseNamedColors.label.text = TranslationServer.translate(
"Use short named colors")
- %PathVBox/CompressNumbers.label.text = TranslationServer.translate(
+ %PathdataVBox/CompressNumbers.label.text = TranslationServer.translate(
"Compress numbers")
- %PathVBox/MinimizeSpacing.label.text = TranslationServer.translate(
+ %PathdataVBox/MinimizeSpacing.label.text = TranslationServer.translate(
"Minimize spacing")
- %PathVBox/RemoveSpacingAfterFlags.label.text = TranslationServer.translate(
+ %PathdataVBox/RemoveSpacingAfterFlags.label.text = TranslationServer.translate(
"Remove spacing after flags")
- %PathVBox/RemoveConsecutiveCommands.label.text = TranslationServer.translate(
+ %PathdataVBox/RemoveConsecutiveCommands.label.text = TranslationServer.translate(
"Remove consecutive commands")
- %TransformVBox/CompressNumbers.label.text = TranslationServer.translate(
+ %TransformListVBox/CompressNumbers.label.text = TranslationServer.translate(
"Compress numbers")
- %TransformVBox/MinimizeSpacing.label.text = TranslationServer.translate(
+ %TransformListVBox/MinimizeSpacing.label.text = TranslationServer.translate(
"Minimize spacing")
- %TransformVBox/RemoveUnnecessaryParams.label.text = TranslationServer.translate(
+ %TransformListVBox/RemoveUnnecessaryParams.label.text = TranslationServer.translate(
"Remove unnecessary parameters")
- %HighlighterVBox/SymbolColor.label.text = TranslationServer.translate(
- "Symbol color")
- %HighlighterVBox/TagColor.label.text = TranslationServer.translate(
- "Tag color")
+ %HighlighterVBox/SymbolColor.label.text = TranslationServer.translate("Symbol color")
+ %HighlighterVBox/TagColor.label.text = TranslationServer.translate("Tag color")
%HighlighterVBox/AttributeColor.label.text = TranslationServer.translate(
"Attribute color")
- %HighlighterVBox/StringColor.label.text = TranslationServer.translate(
- "String color")
- %HighlighterVBox/CommentColor.label.text = TranslationServer.translate(
- "Comment color")
- %HighlighterVBox/TextColor.label.text = TranslationServer.translate(
- "Text color")
- %HighlighterVBox/CDATAColor.label.text = TranslationServer.translate(
- "CDATA color")
- %HighlighterVBox/ErrorColor.label.text = TranslationServer.translate(
- "Error color")
- %HandleColors/InsideColor.label.text = TranslationServer.translate(
- "Inside color")
- %HandleColors/NormalColor.label.text = TranslationServer.translate(
- "Normal color")
- %HandleColors/HoveredColor.label.text = TranslationServer.translate(
- "Hovered color")
- %HandleColors/SelectedColor.label.text = TranslationServer.translate(
- "Selected color")
+ %HighlighterVBox/StringColor.label.text = TranslationServer.translate("String color")
+ %HighlighterVBox/CommentColor.label.text = TranslationServer.translate("Comment color")
+ %HighlighterVBox/TextColor.label.text = TranslationServer.translate("Text color")
+ %HighlighterVBox/CDATAColor.label.text = TranslationServer.translate("CDATA color")
+ %HighlighterVBox/ErrorColor.label.text = TranslationServer.translate("Error color")
+ %HandleColors/InsideColor.label.text = TranslationServer.translate("Inside color")
+ %HandleColors/NormalColor.label.text = TranslationServer.translate("Normal color")
+ %HandleColors/HoveredColor.label.text = TranslationServer.translate("Hovered color")
+ %HandleColors/SelectedColor.label.text = TranslationServer.translate("Selected color")
%HandleColors/HoveredSelectedColor.label.text = TranslationServer.translate(
"Hovered selected color")
%BasicColorsVBox/BackgroundColor.label.text = TranslationServer.translate(
"Background color")
- %BasicColorsVBox/ValidColor.label.text = TranslationServer.translate(
- "Valid color")
- %BasicColorsVBox/ErrorColor.label.text = TranslationServer.translate(
- "Error color")
- %BasicColorsVBox/WarningColor.label.text = TranslationServer.translate(
- "Warning color")
+ %BasicColorsVBox/ValidColor.label.text = TranslationServer.translate("Valid color")
+ %BasicColorsVBox/ErrorColor.label.text = TranslationServer.translate("Error color")
+ %BasicColorsVBox/WarningColor.label.text = TranslationServer.translate("Warning color")
func _on_language_pressed() -> void:
var btn_arr: Array[Button] = []
for lang in TranslationServer.get_loaded_locales():
- btn_arr.append(ContextPopup.create_button(
- TranslationServer.get_locale_name(lang) + " (" + lang + ")",
- _on_language_chosen.bind(lang), lang == TranslationServer.get_locale()))
+ var new_btn := ContextPopup.create_button(
+ TranslationServer.get_locale_name(lang) + " (" + lang.to_upper() + ")",
+ _on_language_chosen.bind(lang), lang == TranslationServer.get_locale())
+ # TODO turn this into dim text on the right like shortcuts, instead of a tooltip.
+ if lang != "en":
+ var translation_obj := TranslationServer.get_translation_object(lang)
+ var translated_count := 0
+ for msg in translation_obj.get_translated_message_list():
+ if not msg.is_empty():
+ translated_count += 1
+ new_btn.tooltip_text = String.num(translated_count * 100.0 /\
+ translation_obj.get_message_count(), 1) + "%"
+ btn_arr.append(new_btn)
var lang_popup := ContextPopup.new()
lang_popup.setup(btn_arr, true, lang_button.size.x)
HandlerGUI.popup_under_rect(lang_popup, lang_button.get_global_rect(), get_viewport())
func _on_language_chosen(locale: String) -> void:
GlobalSettings.language = locale
- custom_notify(Utils.CustomNotification.LANGUAGE_CHANGED)
+ Utils.custom_notify(Utils.CustomNotification.LANGUAGE_CHANGED)
update_language_button()
func update_language_button() -> void:
@@ -239,12 +235,12 @@ func rebuild_color_palettes() -> void:
@onready var xml_vbox: VBoxContainer = %XMLVBox
@onready var number_vbox: VBoxContainer = %NumberVBox
@onready var color_vbox: VBoxContainer = %ColorVBox
-@onready var path_vbox: VBoxContainer = %PathVBox
-@onready var transform_vbox: VBoxContainer = %TransformVBox
+@onready var pathdata_vbox: VBoxContainer = %PathdataVBox
+@onready var transform_list_vbox: VBoxContainer = %TransformListVBox
func setup_format_tab() -> void:
disable_format_checkboxes()
- for vbox in [xml_vbox, number_vbox, color_vbox, path_vbox, transform_vbox]:
+ for vbox in [xml_vbox, number_vbox, color_vbox, pathdata_vbox, transform_list_vbox]:
for child in vbox.get_children():
if child is SettingCheckBox:
child.pressed.connect(_on_format_settings_changed)
@@ -252,11 +248,11 @@ func setup_format_tab() -> void:
%GeneralVBox/AnglePrecision.value_changed.connect(SVG.refresh)
func _on_format_settings_changed() -> void:
- SVG.refresh()
+ SVG.sync()
disable_format_checkboxes()
func _on_number_precision_changed() -> void:
- SVG.refresh()
+ SVG.sync()
# Update snap to fit the new precision.
var snapping_on := GlobalSettings.save_data.snap > 0
var quanta := 0.1 ** GlobalSettings.general_number_precision
@@ -265,7 +261,7 @@ func _on_number_precision_changed() -> void:
GlobalSettings.save_data.snap = quanta
if not snapping_on:
GlobalSettings.save_data.snap *= -1
- custom_notify(Utils.CustomNotification.NUMBER_PRECISION_CHANGED)
+ Utils.custom_notify(Utils.CustomNotification.NUMBER_PRECISION_CHANGED)
func disable_format_checkboxes() -> void:
var is_autoformatting_numbers := GlobalSettings.number_enable_autoformatting
@@ -321,23 +317,20 @@ func show_keybinds(category: String):
func setup_theming_tab() -> void:
for child in %HighlighterVBox.get_children():
if child is SettingColor:
- child.value_changed.connect(custom_notify.bind(
+ child.value_changed.connect(Utils.custom_notify.bind(
Utils.CustomNotification.HIGHLIGHT_COLORS_CHANGED))
for child in %HandleColors.get_children():
if child is SettingColor:
- child.value_changed.connect(custom_notify.bind(
+ child.value_changed.connect(Utils.custom_notify.bind(
Utils.CustomNotification.HANDLE_VISUALS_CHANGED))
for child in %BasicColorsVBox.get_children():
if child is SettingColor:
- child.value_changed.connect(custom_notify.bind(
+ child.value_changed.connect(Utils.custom_notify.bind(
Utils.CustomNotification.BASIC_COLORS_CHANGED))
func _on_theme_settings_changed() -> void:
ThemeGenerator.generate_theme()
-func custom_notify(notif: Utils.CustomNotification) -> void:
- get_tree().get_root().propagate_notification(notif)
-
# Optimize by only generating content on demand.
@@ -372,11 +365,11 @@ func _on_theme_tab_toggled(toggled_on: bool) -> void:
func _on_other_tab_toggled(toggled_on: bool) -> void:
if toggled_on and not generated_content.other:
%ContentContainer/Other/OtherSettings/Misc/HandleSize.value_changed.connect(
- custom_notify.bind(Utils.CustomNotification.HANDLE_VISUALS_CHANGED))
+ Utils.custom_notify.bind(Utils.CustomNotification.HANDLE_VISUALS_CHANGED))
%ContentContainer/Other/OtherSettings/Misc/UIScale.value_changed.connect(
- custom_notify.bind(Utils.CustomNotification.UI_SCALE_CHANGED))
+ Utils.custom_notify.bind(Utils.CustomNotification.UI_SCALE_CHANGED))
%ContentContainer/Other/OtherSettings/Misc/AutoUIScale.pressed.connect(
- custom_notify.bind(Utils.CustomNotification.UI_SCALE_CHANGED))
+ Utils.custom_notify.bind(Utils.CustomNotification.UI_SCALE_CHANGED))
# Disable mouse wrap if not available.
if not DisplayServer.has_feature(DisplayServer.FEATURE_MOUSE_WARP):
wrap_mouse.checkbox.set_pressed_no_signal(false)
diff --git a/src/ui_parts/settings_menu.tscn b/src/ui_parts/settings_menu.tscn
index 75233898..e1cf243c 100644
--- a/src/ui_parts/settings_menu.tscn
+++ b/src/ui_parts/settings_menu.tscn
@@ -145,14 +145,14 @@ text = "General"
[node name="NumberPrecision" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/GeneralVBox" instance=ExtResource("5_dx7vp")]
layout_mode = 2
-section_name = "autoformat"
+section_name = "formatting"
setting_name = "general_number_precision"
values = Array[String](["2", "3", "4"])
type = 2
[node name="AnglePrecision" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/GeneralVBox" instance=ExtResource("5_dx7vp")]
layout_mode = 2
-section_name = "autoformat"
+section_name = "formatting"
setting_name = "general_angle_precision"
values = Array[String](["1", "2"])
type = 2
@@ -168,14 +168,19 @@ text = "XML"
[node name="AddTrailingNewline" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/XMLVBox" instance=ExtResource("4_2qeh2")]
layout_mode = 2
-section_name = "autoformat"
+section_name = "formatting"
setting_name = "xml_add_trailing_newline"
-[node name="UseShorthandTagSyntax" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/XMLVBox" instance=ExtResource("4_2qeh2")]
+[node name="ShorthandTags" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/XMLVBox" instance=ExtResource("4_2qeh2")]
layout_mode = 2
-section_name = "autoformat"
+section_name = "formatting"
setting_name = "xml_shorthand_tags"
+[node name="PrettyFormatting" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/XMLVBox" instance=ExtResource("4_2qeh2")]
+layout_mode = 2
+section_name = "formatting"
+setting_name = "xml_pretty_formatting"
+
[node name="NumberVBox" type="VBoxContainer" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox"]
unique_name_in_owner = true
layout_mode = 2
@@ -186,17 +191,17 @@ layout_mode = 2
[node name="NumberEnable" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/NumberVBox" instance=ExtResource("4_2qeh2")]
layout_mode = 2
-section_name = "autoformat"
+section_name = "formatting"
setting_name = "number_enable_autoformatting"
[node name="RemoveZeroPadding" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/NumberVBox" instance=ExtResource("4_2qeh2")]
layout_mode = 2
-section_name = "autoformat"
+section_name = "formatting"
setting_name = "number_remove_zero_padding"
[node name="RemoveLeadingZero" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/NumberVBox" instance=ExtResource("4_2qeh2")]
layout_mode = 2
-section_name = "autoformat"
+section_name = "formatting"
setting_name = "number_remove_leading_zero"
[node name="ColorVBox" type="VBoxContainer" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox"]
@@ -209,89 +214,89 @@ layout_mode = 2
[node name="ColorEnable" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/ColorVBox" instance=ExtResource("4_2qeh2")]
layout_mode = 2
-section_name = "autoformat"
+section_name = "formatting"
setting_name = "color_enable_autoformatting"
[node name="ConvertRGBToHex" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/ColorVBox" instance=ExtResource("4_2qeh2")]
layout_mode = 2
-section_name = "autoformat"
+section_name = "formatting"
setting_name = "color_convert_rgb_to_hex"
[node name="ConvertNamedToHex" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/ColorVBox" instance=ExtResource("4_2qeh2")]
layout_mode = 2
-section_name = "autoformat"
+section_name = "formatting"
setting_name = "color_convert_named_to_hex"
[node name="UseShorthandHex" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/ColorVBox" instance=ExtResource("4_2qeh2")]
layout_mode = 2
-section_name = "autoformat"
+section_name = "formatting"
setting_name = "color_use_shorthand_hex_code"
[node name="UseNamedColors" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/ColorVBox" instance=ExtResource("4_2qeh2")]
layout_mode = 2
-section_name = "autoformat"
+section_name = "formatting"
setting_name = "color_use_short_named_colors"
-[node name="PathVBox" type="VBoxContainer" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox"]
+[node name="PathdataVBox" type="VBoxContainer" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 3
-[node name="SectionLabel" type="Label" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/PathVBox"]
+[node name="SectionLabel" type="Label" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/PathdataVBox"]
layout_mode = 2
-[node name="Note" type="Label" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/PathVBox"]
+[node name="PathdataEnable" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/PathdataVBox" instance=ExtResource("4_2qeh2")]
layout_mode = 2
-theme_override_colors/font_color = Color(0.501961, 0.501961, 0.501961, 1)
-theme_override_font_sizes/font_size = 12
+section_name = "formatting"
+setting_name = "pathdata_enable_autoformatting"
-[node name="CompressNumbers" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/PathVBox" instance=ExtResource("4_2qeh2")]
+[node name="CompressNumbers" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/PathdataVBox" instance=ExtResource("4_2qeh2")]
layout_mode = 2
-section_name = "autoformat"
-setting_name = "path_compress_numbers"
+section_name = "formatting"
+setting_name = "pathdata_compress_numbers"
-[node name="MinimizeSpacing" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/PathVBox" instance=ExtResource("4_2qeh2")]
+[node name="MinimizeSpacing" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/PathdataVBox" instance=ExtResource("4_2qeh2")]
layout_mode = 2
-section_name = "autoformat"
-setting_name = "path_minimize_spacing"
+section_name = "formatting"
+setting_name = "pathdata_minimize_spacing"
-[node name="RemoveSpacingAfterFlags" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/PathVBox" instance=ExtResource("4_2qeh2")]
+[node name="RemoveSpacingAfterFlags" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/PathdataVBox" instance=ExtResource("4_2qeh2")]
layout_mode = 2
-section_name = "autoformat"
-setting_name = "path_remove_spacing_after_flags"
+section_name = "formatting"
+setting_name = "pathdata_remove_spacing_after_flags"
-[node name="RemoveConsecutiveCommands" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/PathVBox" instance=ExtResource("4_2qeh2")]
+[node name="RemoveConsecutiveCommands" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/PathdataVBox" instance=ExtResource("4_2qeh2")]
layout_mode = 2
-section_name = "autoformat"
-setting_name = "path_remove_consecutive_commands"
+section_name = "formatting"
+setting_name = "pathdata_remove_consecutive_commands"
-[node name="TransformVBox" type="VBoxContainer" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox"]
+[node name="TransformListVBox" type="VBoxContainer" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 3
-[node name="SectionLabel" type="Label" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/TransformVBox"]
+[node name="SectionLabel" type="Label" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/TransformListVBox"]
layout_mode = 2
-[node name="Note" type="Label" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/TransformVBox"]
+[node name="TransformListEnable" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/TransformListVBox" instance=ExtResource("4_2qeh2")]
layout_mode = 2
-theme_override_colors/font_color = Color(0.501961, 0.501961, 0.501961, 1)
-theme_override_font_sizes/font_size = 12
+section_name = "formatting"
+setting_name = "transform_list_enable_autoformatting"
-[node name="CompressNumbers" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/TransformVBox" instance=ExtResource("4_2qeh2")]
+[node name="CompressNumbers" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/TransformListVBox" instance=ExtResource("4_2qeh2")]
layout_mode = 2
-section_name = "autoformat"
-setting_name = "transform_compress_numbers"
+section_name = "formatting"
+setting_name = "transform_list_compress_numbers"
-[node name="MinimizeSpacing" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/TransformVBox" instance=ExtResource("4_2qeh2")]
+[node name="MinimizeSpacing" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/TransformListVBox" instance=ExtResource("4_2qeh2")]
layout_mode = 2
-section_name = "autoformat"
-setting_name = "transform_minimize_spacing"
+section_name = "formatting"
+setting_name = "transform_list_minimize_spacing"
-[node name="RemoveUnnecessaryParams" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/TransformVBox" instance=ExtResource("4_2qeh2")]
+[node name="RemoveUnnecessaryParams" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer/Autoformatting/AutoformattingVBox/TransformListVBox" instance=ExtResource("4_2qeh2")]
layout_mode = 2
-section_name = "autoformat"
-setting_name = "transform_remove_unnecessary_params"
+section_name = "formatting"
+setting_name = "transform_list_remove_unnecessary_params"
[node name="Palettes" type="ScrollContainer" parent="VBoxContainer/HBoxContainer/MainPanel/ContentContainer"]
visible = false
diff --git a/src/ui_parts/tag_container.gd b/src/ui_parts/tag_container.gd
index 73174e2d..f0e29d8c 100644
--- a/src/ui_parts/tag_container.gd
+++ b/src/ui_parts/tag_container.gd
@@ -12,7 +12,7 @@ func _ready():
Indications.requested_scroll_to_tag_editor.connect(scroll_to_view_tag_editor)
func _process(delta: float) -> void:
- if Indications.proposed_drop_tid.is_empty():
+ if Indications.proposed_drop_xid.is_empty():
return
# Autoscroll when the dragged object is near the edge of the screen.
@@ -29,57 +29,57 @@ func _process(delta: float) -> void:
var old_scroll_vertical := scroll_container.scroll_vertical
scroll_container.scroll_vertical += scroll_value
if scroll_container.scroll_vertical != old_scroll_vertical:
- update_proposed_tid()
+ update_proposed_xid()
-func update_proposed_tid() -> void:
+func update_proposed_xid() -> void:
var y_pos := get_local_mouse_position().y + scroll_container.scroll_vertical
var in_top_buffer := false
var in_bottom_buffer := false
# Keep track of the last tag editor whose position is before y_pos.
- var prev_tid := PackedInt32Array([-1])
+ var prev_xid := PackedInt32Array([-1])
var prev_y := -INF
# Keep track of the first tag editor whose end is after y_pos.
- var next_tid := PackedInt32Array([SVG.root_tag.get_child_count()])
+ var next_xid := PackedInt32Array([SVG.root_tag.get_child_count()])
var next_y := INF
- for tid in SVG.root_tag.get_all_tids():
- var tag_rect := get_tag_editor_rect(tid)
+ for tag in SVG.root_tag.get_all_tags():
+ var tag_rect := get_tag_editor_rect(tag.xid)
var buffer := minf(tag_rect.size.y / 3, 26)
var tag_end := tag_rect.end.y
var tag_start := tag_rect.position.y
if y_pos < tag_end and tag_end < next_y:
next_y = tag_end
- next_tid = tid
+ next_xid = tag.xid
if y_pos > tag_end - buffer:
in_bottom_buffer = true
if y_pos > tag_start and tag_start > prev_y:
prev_y = tag_start
- prev_tid = tid
+ prev_xid = tag.xid
if y_pos < tag_start + buffer:
in_top_buffer = true
- # Set the proposed drop TID based on what the previous and next tag editors are.
+ # Set the proposed drop XID based on what the previous and next tag editors are.
if in_top_buffer:
- Indications.set_proposed_drop_tid(prev_tid)
+ Indications.set_proposed_drop_xid(prev_xid)
elif in_bottom_buffer:
- Indications.set_proposed_drop_tid(Utils.get_parent_tid(next_tid) +\
- PackedInt32Array([next_tid[-1] + 1]))
- elif next_tid[0] >= SVG.root_tag.get_child_count():
- Indications.set_proposed_drop_tid(next_tid)
- elif Utils.is_tid_parent_or_self(prev_tid, next_tid):
- for i in range(prev_tid.size(), next_tid.size()):
- if next_tid[i] != 0:
+ Indications.set_proposed_drop_xid(Utils.get_parent_xid(next_xid) +\
+ PackedInt32Array([next_xid[-1] + 1]))
+ elif next_xid[0] >= SVG.root_tag.get_child_count():
+ Indications.set_proposed_drop_xid(next_xid)
+ elif Utils.is_xid_parent_or_self(prev_xid, next_xid):
+ for i in range(prev_xid.size(), next_xid.size()):
+ if next_xid[i] != 0:
return
- Indications.set_proposed_drop_tid(prev_tid + PackedInt32Array([0]))
+ Indications.set_proposed_drop_xid(prev_xid + PackedInt32Array([0]))
func _notification(what: int) -> void:
if is_inside_tree() and not get_tree().paused:
if what == NOTIFICATION_DRAG_BEGIN:
covering_rect.show()
- update_proposed_tid()
+ update_proposed_xid()
elif what == NOTIFICATION_DRAG_END:
covering_rect.hide()
- Indications.clear_proposed_drop_tid()
+ Indications.clear_proposed_drop_xid()
func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
@@ -95,40 +95,43 @@ func _gui_input(event: InputEvent) -> void:
location += 1
# Create the context popup.
var btn_array: Array[Button] = []
- for tag_name in ["path", "circle", "ellipse", "rect", "line"]:
+ for tag_name in ["path", "circle", "ellipse", "rect", "line", "g"]:
var btn := ContextPopup.create_button(tag_name,
add_tag.bind(tag_name, location), false, DB.get_tag_icon(tag_name))
btn.add_theme_font_override("font", load("res://visual/fonts/FontMono.ttf"))
btn_array.append(btn)
+ if location == 0:
+ for tag_name in ["linearGradient", "radialGradient"]:
+ var btn := ContextPopup.create_button(tag_name,
+ add_tag.bind(tag_name, location), false, DB.get_tag_icon(tag_name))
+ btn.add_theme_font_override("font", load("res://visual/fonts/FontMono.ttf"))
+ btn_array.append(btn)
+
+ var separation_indices: Array[int] = [1, 5]
+
var add_popup := ContextPopup.new()
- add_popup.setup_with_title(btn_array, TranslationServer.translate("New tag"), true)
+ add_popup.setup_with_title(btn_array, TranslationServer.translate("New tag"),
+ true, -1, separation_indices)
var vp := get_viewport()
HandlerGUI.popup_under_pos(add_popup, vp.get_mouse_position(), vp)
func add_tag(tag_name: String, tag_location: int) -> void:
- var tag: Tag
- match tag_name:
- "path": tag = TagPath.new()
- "circle": tag = TagCircle.new()
- "ellipse": tag = TagEllipse.new()
- "rect": tag = TagRect.new()
- "line": tag = TagLine.new()
- tag.user_setup()
- SVG.root_tag.add_tag(tag, PackedInt32Array([tag_location]))
+ SVG.root_tag.add_tag(DB.tag(tag_name), PackedInt32Array([tag_location]))
-# This function assumes there exists a tag editor for the corresponding TID.
-func get_tag_editor_rect(tid: PackedInt32Array) -> Rect2:
- if tid.is_empty():
+# This function assumes there exists a tag editor for the corresponding XID.
+func get_tag_editor_rect(xid: PackedInt32Array) -> Rect2:
+ if xid.is_empty():
return Rect2()
- var tag_editor: Control = tags.get_child(tid[0])
- for i in range(1, tid.size()):
- tag_editor = tag_editor.child_tags_container.get_child(tid[i])
+ var tag_editor: Control = tags.get_child(xid[0])
+ for i in range(1, xid.size()):
+ tag_editor = tag_editor.child_tags_container.get_child(xid[i])
# Position relative to the tag container.
return Rect2(tag_editor.global_position - scroll_container.global_position +\
Vector2(0, scroll_container.scroll_vertical), tag_editor.size)
-# This function assumes there exists a tag editor for the corresponding TID
-func scroll_to_view_tag_editor(tid: PackedInt32Array) -> void:
- scroll_container.get_v_scroll_bar().value = get_tag_editor_rect(tid).position.y - scroll_container.size.y / 5
+# This function assumes there exists a tag editor for the corresponding XID.
+func scroll_to_view_tag_editor(xid: PackedInt32Array) -> void:
+ scroll_container.get_v_scroll_bar().value = get_tag_editor_rect(xid).position.y -\
+ scroll_container.size.y / 5
diff --git a/src/ui_parts/tag_frame.gd b/src/ui_parts/tag_frame.gd
index e400fc15..aaa7a4ba 100644
--- a/src/ui_parts/tag_frame.gd
+++ b/src/ui_parts/tag_frame.gd
@@ -1,28 +1,41 @@
extends VBoxContainer
const code_font = preload("res://visual/fonts/FontMono.ttf")
+const warning_icon = preload("res://visual/icons/TagWarning.svg")
+# FIXME this seems like a not us issue.
static var TagFrame: PackedScene:
get:
if !is_instance_valid(TagFrame):
TagFrame = load("res://src/ui_parts/tag_frame.tscn")
return TagFrame
-const UnknownField = preload("res://src/ui_elements/unknown_field.tscn")
-const TagContentUnknown = preload("res://src/ui_elements/tag_content_unknown.tscn")
-const TagContentPath = preload("res://src/ui_elements/tag_content_path.tscn")
-const TagContentCircle = preload("res://src/ui_elements/tag_content_circle.tscn")
-const TagContentEllipse = preload("res://src/ui_elements/tag_content_ellipse.tscn")
-const TagContentRect = preload("res://src/ui_elements/tag_content_rect.tscn")
-const TagContentLine = preload("res://src/ui_elements/tag_content_line.tscn")
-const TagContentStop = preload("res://src/ui_elements/tag_content_stop.tscn")
+const UnrecognizedField = preload("res://src/ui_elements/unrecognized_field.tscn")
+const ColorField = preload("res://src/ui_elements/color_field.tscn")
+const NumberField = preload("res://src/ui_elements/number_field.tscn")
+const NumberSlider = preload("res://src/ui_elements/number_field_with_slider.tscn")
+const IDField = preload("res://src/ui_elements/id_field.tscn")
+const EnumField = preload("res://src/ui_elements/enum_field.tscn")
+const TransformField = preload("res://src/ui_elements/transform_field.tscn")
+
+const tag_content_types = {
+ "path": preload("res://src/ui_elements/tag_content_path.tscn"),
+ "circle": preload("res://src/ui_elements/tag_content_basic_shape.tscn"),
+ "ellipse": preload("res://src/ui_elements/tag_content_basic_shape.tscn"),
+ "rect": preload("res://src/ui_elements/tag_content_basic_shape.tscn"),
+ "line": preload("res://src/ui_elements/tag_content_basic_shape.tscn"),
+ "stop": preload("res://src/ui_elements/tag_content_stop.tscn"),
+ "g": preload("res://src/ui_elements/tag_content_g.tscn"),
+ "linearGradient": preload("res://src/ui_elements/tag_content_linear_gradient.tscn"),
+ "radialGradient": preload("res://src/ui_elements/tag_content_radial_gradient.tscn"),
+}
+const TagContentUnrecognized = preload("res://src/ui_elements/tag_content_unrecognized.tscn")
@onready var main_container: VBoxContainer = $Content/MainContainer
@onready var title_bar: Panel = $TitleBar
var child_tags_container: VBoxContainer # Only created if there are child tags.
@onready var content: PanelContainer = $Content
-var tid: PackedInt32Array
var tag: Tag
var surface := RenderingServer.canvas_item_create() # Used for the drop indicator.
@@ -37,33 +50,49 @@ func _ready() -> void:
determine_selection_highlight()
title_bar.queue_redraw()
- # If there are unknown attributes, they would always be on top.
- if not tag.unknown_attributes.is_empty():
- var unknown_container := HFlowContainer.new()
- main_container.add_child(unknown_container)
- main_container.move_child(unknown_container, 0)
- for attribute in tag.unknown_attributes:
- var input_field := UnknownField.instantiate()
- input_field.attribute = attribute
+ # If there are unrecognized attributes, they would always be on top.
+ var has_unrecognized_attributes := false
+ var unknown_container: HFlowContainer
+ for attribute in tag.attributes.values():
+ if not DB.is_attribute_recognized(tag.name, attribute.name):
+ if not has_unrecognized_attributes:
+ has_unrecognized_attributes = true
+ unknown_container = HFlowContainer.new()
+ main_container.add_child(unknown_container)
+ main_container.move_child(unknown_container, 0)
+
+ var input_field: Control
+ match DB.get_attribute_type(attribute.name):
+ DB.AttributeType.COLOR: input_field = ColorField.instantiate()
+ DB.AttributeType.ENUM: input_field = EnumField.instantiate()
+ DB.AttributeType.TRANSFORM_LIST: input_field = TransformField.instantiate()
+ DB.AttributeType.ID: input_field = IDField.instantiate()
+ DB.AttributeType.NUMERIC:
+ var min_value: float = DB.attribute_numeric_bounds[attribute.name].x
+ var max_value: float = DB.attribute_numeric_bounds[attribute.name].y
+ if is_inf(max_value):
+ input_field = NumberField.instantiate()
+ if not is_inf(min_value):
+ input_field.allow_lower = false
+ input_field.min_value = min_value
+ else:
+ input_field = NumberSlider.instantiate()
+ input_field.allow_lower = false
+ input_field.allow_higher = false
+ input_field.min_value = min_value
+ input_field.max_value = max_value
+ input_field.slider_step = 0.01
+ _: input_field = UnrecognizedField.instantiate()
+ input_field.tag = tag
+ input_field.attribute_name = attribute.name
unknown_container.add_child(input_field)
var tag_content: Control
- if tag is TagPath:
- tag_content = TagContentPath.instantiate()
- elif tag is TagCircle:
- tag_content = TagContentCircle.instantiate()
- elif tag is TagEllipse:
- tag_content = TagContentEllipse.instantiate()
- elif tag is TagRect:
- tag_content = TagContentRect.instantiate()
- elif tag is TagLine:
- tag_content = TagContentLine.instantiate()
- elif tag is TagStop:
- tag_content = TagContentStop.instantiate()
+ if tag.name in tag_content_types:
+ tag_content = tag_content_types[tag.name].instantiate()
else:
- tag_content = TagContentUnknown.instantiate()
+ tag_content = TagContentUnrecognized.instantiate()
tag_content.tag = tag
- tag_content.tid = tid
main_container.add_child(tag_content)
if not tag.is_standalone():
@@ -74,9 +103,6 @@ func _ready() -> void:
var child_tag := tag.child_tags[tag_idx]
var tag_editor := TagFrame.instantiate()
tag_editor.tag = child_tag
- var new_tid := tid.duplicate()
- new_tid.append(tag_idx)
- tag_editor.tid = new_tid
child_tags_container.add_child(tag_editor)
func _exit_tree() -> void:
@@ -84,15 +110,14 @@ func _exit_tree() -> void:
# Logic for dragging.
func _get_drag_data(_at_position: Vector2) -> Variant:
- var data: Array[PackedInt32Array] = Utils.filter_descendant_tids(
- Indications.selected_tids.duplicate(true))
+ var data: Array[PackedInt32Array] = Utils.filter_descendant_xids(
+ Indications.selected_xids.duplicate(true))
# Set up a preview.
var tags_container := VBoxContainer.new()
for data_idx in range(data.size() - 1, -1, -1):
- var drag_tid := data[data_idx]
+ var drag_xid := data[data_idx]
var preview := TagFrame.instantiate()
- preview.tag = SVG.root_tag.get_tag(drag_tid)
- preview.tid = drag_tid
+ preview.tag = SVG.root_tag.get_tag(drag_xid)
preview.custom_minimum_size.x = size.x
preview.z_index = 2
tags_container.add_child(preview)
@@ -109,44 +134,45 @@ func _notification(what: int) -> void:
func _on_title_button_pressed() -> void:
# Update the selection immediately, since if this tag editor is
# in a multi-selection, only the mouse button release would change the selection.
- Indications.normal_select(tid)
+ Indications.normal_select(tag.xid)
var viewport := get_viewport()
var rect := title_bar.get_global_rect()
HandlerGUI.popup_under_rect_center(Indications.get_selection_context(
HandlerGUI.popup_under_rect_center.bind(rect, viewport),
- Indications.SelectionContext.TAG_EDITOR), rect, viewport)
+ Indications.Context.TAG_EDITOR), rect, viewport)
func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseMotion and event.button_mask == 0:
- if Indications.semi_hovered_tid != tid and\
- not Utils.is_tid_parent(tid, Indications.hovered_tid):
- Indications.set_hovered(tid)
+ if Indications.semi_hovered_xid != tag.xid and\
+ not Utils.is_xid_parent(tag.xid, Indications.hovered_xid):
+ Indications.set_hovered(tag.xid)
elif event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
if event.is_pressed():
if event.shift_pressed:
- Indications.shift_select(tid)
+ Indications.shift_select(tag.xid)
elif event.is_command_or_control_pressed():
- Indications.ctrl_select(tid)
- elif not tid in Indications.selected_tids:
- Indications.normal_select(tid)
+ Indications.ctrl_select(tag.xid)
+ elif not tag.xid in Indications.selected_xids:
+ Indications.normal_select(tag.xid)
elif event.is_released() and not event.shift_pressed and\
not event.is_command_or_control_pressed() and\
- Indications.selected_tids.size() > 1 and tid in Indications.selected_tids:
- Indications.normal_select(tid)
+ Indications.selected_xids.size() > 1 and tag.xid in Indications.selected_xids:
+ Indications.normal_select(tag.xid)
accept_event()
elif event.button_index == MOUSE_BUTTON_RIGHT and event.is_pressed():
- if not tid in Indications.selected_tids:
- Indications.normal_select(tid)
+ if not tag.xid in Indications.selected_xids:
+ Indications.normal_select(tag.xid)
var viewport := get_viewport()
var popup_pos := viewport.get_mouse_position()
HandlerGUI.popup_under_pos(Indications.get_selection_context(
- HandlerGUI.popup_under_pos.bind(popup_pos, viewport), Indications.SelectionContext.TAG_EDITOR), popup_pos, viewport)
+ HandlerGUI.popup_under_pos.bind(popup_pos, viewport),
+ Indications.Context.TAG_EDITOR), popup_pos, viewport)
accept_event()
func _on_mouse_entered() -> void:
- var tag_icon_size: Vector2 = tag.icon.get_size()
+ var tag_icon_size := DB.get_tag_icon(tag.name).get_size()
var half_bar_width := title_bar.size.x / 2
var title_width := code_font.get_string_size(tag.name,
HORIZONTAL_ALIGNMENT_LEFT, 180, 12).x
@@ -163,9 +189,19 @@ func _on_mouse_entered() -> void:
title_button.gui_input.connect(_on_title_button_gui_input.bind(title_button))
title_button.pressed.connect(_on_title_button_pressed)
mouse_exited.connect(title_button.queue_free)
+ # Add warning button.
+ var tag_warnings := tag.get_config_warnings()
+ if not tag_warnings.is_empty():
+ var warning_sign := TextureRect.new()
+ warning_sign.tooltip_text = "\n".join(tag_warnings)
+ warning_sign.texture = warning_icon
+ warning_sign.size = Vector2(warning_icon.get_size())
+ warning_sign.position = title_bar.position + Vector2(title_bar.size.x - 23, 4)
+ title_bar.add_child(warning_sign)
+ mouse_exited.connect(warning_sign.queue_free)
func _on_mouse_exited() -> void:
- Indications.remove_hovered(tid)
+ Indications.remove_hovered(tag.xid)
determine_selection_highlight()
@@ -187,8 +223,8 @@ func determine_selection_highlight() -> void:
content_sb.content_margin_bottom = 7
content_sb.content_margin_right = 7
- var is_selected := tid in Indications.selected_tids
- var is_hovered := Indications.hovered_tid == tid
+ var is_selected := tag.xid in Indications.selected_xids
+ var is_hovered := Indications.hovered_xid == tag.xid
if is_selected:
if is_hovered:
@@ -210,7 +246,7 @@ func determine_selection_highlight() -> void:
content_sb.border_color = Color.from_hsv(0.6, 0.5, 0.35)
title_sb.border_color = Color.from_hsv(0.6, 0.5, 0.35)
- var depth := tid.size() - 1
+ var depth := tag.xid.size() - 1
var depth_tint := depth * 0.12
if depth > 0:
content_sb.bg_color = Color.from_hsv(content_sb.bg_color.h + depth_tint,
@@ -228,26 +264,26 @@ func _draw() -> void:
RenderingServer.canvas_item_clear(surface)
# There's only stuff to draw if there are drag-and-drop actions.
- if Indications.proposed_drop_tid.is_empty():
+ if Indications.proposed_drop_xid.is_empty():
return
- for selected_tid in Indications.selected_tids:
- if Utils.is_tid_parent_or_self(selected_tid, tid):
+ for selected_xid in Indications.selected_xids:
+ if Utils.is_xid_parent_or_self(selected_xid, tag.xid):
return
- var parent_tid := Utils.get_parent_tid(tid)
+ var parent_xid := Utils.get_parent_xid(tag.xid)
# Draw the yellow indicator of drag and drop actions.
var drop_sb := StyleBoxFlat.new()
- var proposed_drop_tid := Indications.proposed_drop_tid
- drop_sb.border_color = Color.YELLOW
- if proposed_drop_tid == parent_tid + PackedInt32Array([tid[-1]]):
+ var proposed_drop_xid := Indications.proposed_drop_xid
+ drop_sb.border_color = Color.GREEN
+ if proposed_drop_xid == parent_xid + PackedInt32Array([tag.xid[-1]]):
drop_sb.border_width_top = 2
- elif proposed_drop_tid == parent_tid + PackedInt32Array([tid[-1] + 1]):
+ elif proposed_drop_xid == parent_xid + PackedInt32Array([tag.xid[-1] + 1]):
drop_sb.border_width_bottom = 2
- elif proposed_drop_tid == tid + PackedInt32Array([0]):
+ elif proposed_drop_xid == tag.xid + PackedInt32Array([0]):
drop_sb.set_border_width_all(2)
if is_instance_valid(child_tags_container):
- drop_sb.border_color = Color(Color.YELLOW, 0.4)
+ drop_sb.border_color = Color(Color.GREEN, 0.4)
else:
return
@@ -256,15 +292,21 @@ func _draw() -> void:
drop_sb.draw(surface, Rect2(Vector2.ZERO, get_size()))
func _on_title_bar_draw() -> void:
- var tag_icon_size: Vector2 = tag.icon.get_size()
+ var tag_icon := DB.get_tag_icon(tag.name)
+ var tag_icon_size: Vector2 = tag_icon.get_size()
var half_bar_width := title_bar.size.x / 2
var title_width := code_font.get_string_size(tag.name,
HORIZONTAL_ALIGNMENT_LEFT, 180, 12).x
code_font.draw_string(title_bar_ci, Vector2(half_bar_width - title_width / 2 +\
tag_icon_size.x / 2, 18),
tag.name, HORIZONTAL_ALIGNMENT_LEFT, 180, 12)
- tag.icon.draw_rect(title_bar_ci, Rect2(Vector2(half_bar_width - title_width / 2 -\
+ tag_icon.draw_rect(title_bar_ci, Rect2(Vector2(half_bar_width - title_width / 2 -\
tag_icon_size.x + 6, 4).round(), tag_icon_size), false)
+
+ var tag_warnings := tag.get_config_warnings()
+ if not tag_warnings.is_empty():
+ warning_icon.draw_rect(title_bar_ci, Rect2(Vector2(title_bar.size.x - 23, 4),
+ warning_icon.get_size()), false)
# Block dragging from starting when pressing the title button.
func _on_title_button_gui_input(event: InputEvent, title_button: Button) -> void:
diff --git a/src/ui_parts/viewport.gd b/src/ui_parts/viewport.gd
index e367da2d..d4b6ae5c 100644
--- a/src/ui_parts/viewport.gd
+++ b/src/ui_parts/viewport.gd
@@ -18,7 +18,7 @@ var _zoom_to: Vector2
func _ready() -> void:
zoom_menu.zoom_changed.connect(view.update.unbind(2))
- SVG.root_tag.resized.connect(resize)
+ SVG.resized.connect(resize)
Indications.viewport_size_changed.connect(adjust_view)
resize()
await get_tree().process_frame
@@ -149,10 +149,8 @@ func adjust_view(offset := Vector2(0.5, 0.5)) -> void:
var old_size := last_size_adjusted
last_size_adjusted = size / Indications.zoom
- var svg_w := 16384.0 if SVG.root_tag.attributes.width.get_value().is_empty()\
- else SVG.root_tag.width
- var svg_h := 16384.0 if SVG.root_tag.attributes.height.get_value().is_empty()\
- else SVG.root_tag.height
+ var svg_w := SVG.root_tag.width if SVG.root_tag.attributes.has("width") else 16384.0
+ var svg_h := SVG.root_tag.height if SVG.root_tag.attributes.has("height") else 16384.0
var zoomed_size := buffer_view_space * size / Indications.zoom
view.limit_left = int(-zoomed_size.x)
diff --git a/translations/GodSVG.pot b/translations/GodSVG.pot
index 18a32020..8121f79c 100644
--- a/translations/GodSVG.pot
+++ b/translations/GodSVG.pot
@@ -93,8 +93,8 @@ msgstr ""
msgid "Paste XML"
msgstr ""
-#: src/ui_elements/palette_config.gd src/ui_elements/transform_popup.gd
-#: src/Indications.gd
+#: src/ui_elements/palette_config.gd src/ui_elements/setting_shortcut.gd
+#: src/ui_elements/transform_popup.gd src/Indications.gd
msgid "Delete"
msgstr ""
@@ -127,14 +127,6 @@ msgstr ""
msgid "Add shortcut"
msgstr ""
-#: src/ui_elements/setting_shortcut.gd
-msgid "Edit"
-msgstr ""
-
-#: src/ui_elements/setting_shortcut.gd
-msgid "Remove"
-msgstr ""
-
#: src/ui_elements/setting_shortcut.gd
msgid "Press keys…"
msgstr ""
@@ -176,11 +168,19 @@ msgid "Translators"
msgstr ""
#: src/ui_parts/about_menu.gd
-msgid "Authors"
+msgid "Donors"
msgstr ""
#: src/ui_parts/about_menu.gd
-msgid "Donors"
+msgid "Golden donors"
+msgstr ""
+
+#: src/ui_parts/about_menu.gd
+msgid "Diamond donors"
+msgstr ""
+
+#: src/ui_parts/about_menu.gd
+msgid "Authors"
msgstr ""
#: src/ui_parts/about_menu.gd
@@ -199,7 +199,7 @@ msgstr ""
msgid "Alert!"
msgstr ""
-#: src/ui_parts/alert_dialog.gd src/ui_parts/display.gd
+#: src/ui_parts/alert_dialog.gd src/HandlerGUI.gd
msgid "OK"
msgstr ""
@@ -249,18 +249,6 @@ msgstr ""
msgid "Overlay reference"
msgstr ""
-#: src/ui_parts/display.gd
-msgid "Import Reference Image"
-msgstr ""
-
-#: src/ui_parts/display.gd
-msgid "Show Reference Image"
-msgstr ""
-
-#: src/ui_parts/display.gd
-msgid "Overlay Reference Image"
-msgstr ""
-
#: src/ui_parts/display.gd
msgid "Show Grid"
msgstr ""
@@ -293,14 +281,6 @@ msgstr ""
msgid "Check for updates"
msgstr ""
-#: src/ui_parts/display.gd
-msgid "Check for updates?"
-msgstr ""
-
-#: src/ui_parts/display.gd
-msgid "This requires GodSVG to connect to the internet."
-msgstr ""
-
#: src/ui_parts/display.gd
msgid "Enable snapping"
msgstr ""
@@ -411,8 +391,8 @@ msgid ""
"contents!"
msgstr ""
-#: src/ui_parts/handles_manager.gd src/ui_parts/tag_container.gd
-msgid "New tag"
+#: src/ui_parts/handles_manager.gd
+msgid "New shape"
msgstr ""
#: src/ui_parts/import_warning_dialog.gd
@@ -420,11 +400,11 @@ msgid "Import Problems"
msgstr ""
#: src/ui_parts/import_warning_dialog.gd
-msgid "Unknown tag"
+msgid "Unrecognized tag"
msgstr ""
#: src/ui_parts/import_warning_dialog.gd
-msgid "Unknown attribute"
+msgid "Unrecognized attribute"
msgstr ""
#: src/ui_parts/import_warning_dialog.gd
@@ -527,6 +507,26 @@ msgstr ""
msgid "Language"
msgstr ""
+#: src/ui_parts/settings_menu.gd
+msgid "File"
+msgstr ""
+
+#: src/ui_parts/settings_menu.gd
+msgid "Edit"
+msgstr ""
+
+#: src/ui_parts/settings_menu.gd
+msgid "View"
+msgstr ""
+
+#: src/ui_parts/settings_menu.gd
+msgid "Tool"
+msgstr ""
+
+#: src/ui_parts/settings_menu.gd
+msgid "Help"
+msgstr ""
+
#: src/ui_parts/settings_menu.gd
msgid "Swaps zoom in and zoom out with the mouse wheel."
msgstr ""
@@ -691,6 +691,10 @@ msgstr ""
msgid "Warning color"
msgstr ""
+#: src/ui_parts/tag_container.gd
+msgid "New tag"
+msgstr ""
+
#: src/ui_parts/update_menu.gd
msgid "Include prereleases"
msgstr ""
@@ -756,6 +760,14 @@ msgstr ""
msgid "Quit"
msgstr ""
+#: src/HandlerGUI.gd
+msgid "Check for updates?"
+msgstr ""
+
+#: src/HandlerGUI.gd
+msgid "This requires GodSVG to connect to the internet."
+msgstr ""
+
#: src/Indications.gd
msgid "View In List"
msgstr ""
@@ -772,10 +784,6 @@ msgstr ""
msgid "Import"
msgstr ""
-#: src/TranslationUtils.gd
-msgid "Load Reference Image"
-msgstr ""
-
#: src/TranslationUtils.gd
msgid "Optimize"
msgstr ""
diff --git a/translations/bg.po b/translations/bg.po
index f9dc54f3..c35c6bfd 100644
--- a/translations/bg.po
+++ b/translations/bg.po
@@ -93,8 +93,8 @@ msgstr "Копирай като XML"
msgid "Paste XML"
msgstr "Постави XML"
-#: src/ui_elements/palette_config.gd src/ui_elements/transform_popup.gd
-#: src/Indications.gd
+#: src/ui_elements/palette_config.gd src/ui_elements/setting_shortcut.gd
+#: src/ui_elements/transform_popup.gd src/Indications.gd
msgid "Delete"
msgstr "Премахни"
@@ -127,14 +127,6 @@ msgstr "Неизползван"
msgid "Add shortcut"
msgstr "Добави бърз клавиш"
-#: src/ui_elements/setting_shortcut.gd
-msgid "Edit"
-msgstr "Промени"
-
-#: src/ui_elements/setting_shortcut.gd
-msgid "Remove"
-msgstr "Премахни"
-
#: src/ui_elements/setting_shortcut.gd
msgid "Press keys…"
msgstr "Натисни клавиши…"
@@ -175,14 +167,22 @@ msgstr "Програмисти"
msgid "Translators"
msgstr "Преводачи"
-#: src/ui_parts/about_menu.gd
-msgid "Authors"
-msgstr "Автори"
-
#: src/ui_parts/about_menu.gd
msgid "Donors"
msgstr "Дарители"
+#: src/ui_parts/about_menu.gd
+msgid "Golden donors"
+msgstr "Златни дарители"
+
+#: src/ui_parts/about_menu.gd
+msgid "Diamond donors"
+msgstr "Диамантени дарители"
+
+#: src/ui_parts/about_menu.gd
+msgid "Authors"
+msgstr "Автори"
+
#: src/ui_parts/about_menu.gd
msgid "License"
msgstr "Лиценз"
@@ -199,7 +199,7 @@ msgstr "Лицензи от трети партии"
msgid "Alert!"
msgstr "Предупреждение!"
-#: src/ui_parts/alert_dialog.gd src/ui_parts/display.gd
+#: src/ui_parts/alert_dialog.gd src/HandlerGUI.gd
msgid "OK"
msgstr "Добре"
@@ -249,18 +249,6 @@ msgstr "Покажни справочното изображение"
msgid "Overlay reference"
msgstr "Насложи справочното изображение"
-#: src/ui_parts/display.gd
-msgid "Import Reference Image"
-msgstr ""
-
-#: src/ui_parts/display.gd
-msgid "Show Reference Image"
-msgstr ""
-
-#: src/ui_parts/display.gd
-msgid "Overlay Reference Image"
-msgstr ""
-
#: src/ui_parts/display.gd
msgid "Show Grid"
msgstr "Покажи решетката"
@@ -293,14 +281,6 @@ msgstr "Уебсайта на GodSVG"
msgid "Check for updates"
msgstr "Провери за ъпдейти"
-#: src/ui_parts/display.gd
-msgid "Check for updates?"
-msgstr "Провери за ъпдейти?"
-
-#: src/ui_parts/display.gd
-msgid "This requires GodSVG to connect to the internet."
-msgstr "Това изисква GodSVG да се свърже с интерната."
-
#: src/ui_parts/display.gd
msgid "Enable snapping"
msgstr "Включи захващането"
@@ -410,23 +390,23 @@ msgid ""
"A file named \"{file_name}\" already exists. Replacing will overwrite its "
"contents!"
msgstr ""
-"Файл кръстен \"{file_name}\" вече съществува. Заместването ще пренапише "
+"Файл с името \"{file_name}\" вече съществува. Заместването ще пренапише "
"неговото съдържание!"
-#: src/ui_parts/handles_manager.gd src/ui_parts/tag_container.gd
-msgid "New tag"
-msgstr "Нов таг"
+#: src/ui_parts/handles_manager.gd
+msgid "New shape"
+msgstr "Нова фигура"
#: src/ui_parts/import_warning_dialog.gd
msgid "Import Problems"
msgstr "Проблеми в импортирането"
#: src/ui_parts/import_warning_dialog.gd
-msgid "Unknown tag"
+msgid "Unrecognized tag"
msgstr "Непознат таг"
#: src/ui_parts/import_warning_dialog.gd
-msgid "Unknown attribute"
+msgid "Unrecognized attribute"
msgstr "Непознат атрибут"
#: src/ui_parts/import_warning_dialog.gd
@@ -529,6 +509,26 @@ msgstr "Автоматичен мащаб"
msgid "Language"
msgstr "Език"
+#: src/ui_parts/settings_menu.gd
+msgid "File"
+msgstr "Файл"
+
+#: src/ui_parts/settings_menu.gd
+msgid "Edit"
+msgstr "Промени"
+
+#: src/ui_parts/settings_menu.gd
+msgid "View"
+msgstr "Гледка"
+
+#: src/ui_parts/settings_menu.gd
+msgid "Tool"
+msgstr "Инструмент"
+
+#: src/ui_parts/settings_menu.gd
+msgid "Help"
+msgstr "Помощ"
+
#: src/ui_parts/settings_menu.gd
msgid "Swaps zoom in and zoom out with the mouse wheel."
msgstr "Разменя увеличението и намалянето с колелцето на мишката."
@@ -699,6 +699,10 @@ msgstr "Цвят на валидният текст"
msgid "Warning color"
msgstr "Цвят на предупрежденията"
+#: src/ui_parts/tag_container.gd
+msgid "New tag"
+msgstr "Нов таг"
+
#: src/ui_parts/update_menu.gd
msgid "Include prereleases"
msgstr "Включи предварителните издания"
@@ -769,6 +773,14 @@ msgstr "Наистина ли искаш да затвориш GodSVG?"
msgid "Quit"
msgstr "Затвори"
+#: src/HandlerGUI.gd
+msgid "Check for updates?"
+msgstr "Провери за ъпдейти?"
+
+#: src/HandlerGUI.gd
+msgid "This requires GodSVG to connect to the internet."
+msgstr "Това изисква GodSVG да се свърже с интерната."
+
#: src/Indications.gd
msgid "View In List"
msgstr "Виж в Списъка"
@@ -785,10 +797,6 @@ msgstr "Превърни в"
msgid "Import"
msgstr "Импортирай"
-#: src/TranslationUtils.gd
-msgid "Load Reference Image"
-msgstr ""
-
#: src/TranslationUtils.gd
msgid "Optimize"
msgstr "Оптимизирай"
@@ -912,15 +920,3 @@ msgstr "Кубична крива на Безие до"
#: src/TranslationUtils.gd
msgid "Shorthand Cubic Bezier to"
msgstr "Кратка кубична крива на Безие до"
-
-#~ msgid "File"
-#~ msgstr "Файл"
-
-#~ msgid "View"
-#~ msgstr "Гледка"
-
-#~ msgid "Tool"
-#~ msgstr "Инструмент"
-
-#~ msgid "Help"
-#~ msgstr "Помощ"
diff --git a/translations/de.po b/translations/de.po
index 4c859102..498b85f8 100644
--- a/translations/de.po
+++ b/translations/de.po
@@ -93,8 +93,8 @@ msgstr "Kopiere als XML"
msgid "Paste XML"
msgstr "XML Einfügen"
-#: src/ui_elements/palette_config.gd src/ui_elements/transform_popup.gd
-#: src/Indications.gd
+#: src/ui_elements/palette_config.gd src/ui_elements/setting_shortcut.gd
+#: src/ui_elements/transform_popup.gd src/Indications.gd
msgid "Delete"
msgstr "Löschen"
@@ -127,14 +127,6 @@ msgstr "Unbenutzt"
msgid "Add shortcut"
msgstr "Tastenkombination hinzufügen"
-#: src/ui_elements/setting_shortcut.gd
-msgid "Edit"
-msgstr "Bearbeiten"
-
-#: src/ui_elements/setting_shortcut.gd
-msgid "Remove"
-msgstr "Entfernen"
-
#: src/ui_elements/setting_shortcut.gd
msgid "Press keys…"
msgstr "Drücke Tasten…"
@@ -178,13 +170,21 @@ msgid "Translators"
msgstr "Transformationen"
#: src/ui_parts/about_menu.gd
-msgid "Authors"
-msgstr "Autoren"
+msgid "Donors"
+msgstr ""
#: src/ui_parts/about_menu.gd
-msgid "Donors"
+msgid "Golden donors"
msgstr ""
+#: src/ui_parts/about_menu.gd
+msgid "Diamond donors"
+msgstr ""
+
+#: src/ui_parts/about_menu.gd
+msgid "Authors"
+msgstr "Autoren"
+
#: src/ui_parts/about_menu.gd
msgid "License"
msgstr "Lizenz"
@@ -202,7 +202,7 @@ msgstr "Drittanbieter-Lizenzen"
msgid "Alert!"
msgstr "Warnung!"
-#: src/ui_parts/alert_dialog.gd src/ui_parts/display.gd
+#: src/ui_parts/alert_dialog.gd src/HandlerGUI.gd
msgid "OK"
msgstr "OK"
@@ -256,18 +256,6 @@ msgstr ""
msgid "Overlay reference"
msgstr ""
-#: src/ui_parts/display.gd
-msgid "Import Reference Image"
-msgstr ""
-
-#: src/ui_parts/display.gd
-msgid "Show Reference Image"
-msgstr ""
-
-#: src/ui_parts/display.gd
-msgid "Overlay Reference Image"
-msgstr ""
-
#: src/ui_parts/display.gd
msgid "Show Grid"
msgstr "Raster anzeigen"
@@ -302,14 +290,6 @@ msgstr "GodSVG Repository"
msgid "Check for updates"
msgstr ""
-#: src/ui_parts/display.gd
-msgid "Check for updates?"
-msgstr ""
-
-#: src/ui_parts/display.gd
-msgid "This requires GodSVG to connect to the internet."
-msgstr ""
-
#: src/ui_parts/display.gd
#, fuzzy
msgid "Enable snapping"
@@ -428,9 +408,9 @@ msgid ""
"contents!"
msgstr ""
-#: src/ui_parts/handles_manager.gd src/ui_parts/tag_container.gd
+#: src/ui_parts/handles_manager.gd
#, fuzzy
-msgid "New tag"
+msgid "New shape"
msgstr "Neues Element hinzufügen"
#: src/ui_parts/import_warning_dialog.gd
@@ -438,13 +418,12 @@ msgid "Import Problems"
msgstr "Probleme beim Importieren"
#: src/ui_parts/import_warning_dialog.gd
-#, fuzzy
-msgid "Unknown tag"
-msgstr "Unbekanntes Element"
+msgid "Unrecognized tag"
+msgstr ""
#: src/ui_parts/import_warning_dialog.gd
#, fuzzy
-msgid "Unknown attribute"
+msgid "Unrecognized attribute"
msgstr "Unbekannte Attribute"
#: src/ui_parts/import_warning_dialog.gd
@@ -553,6 +532,26 @@ msgstr "Skalieren"
msgid "Language"
msgstr "Sprache"
+#: src/ui_parts/settings_menu.gd
+msgid "File"
+msgstr ""
+
+#: src/ui_parts/settings_menu.gd
+msgid "Edit"
+msgstr "Bearbeiten"
+
+#: src/ui_parts/settings_menu.gd
+msgid "View"
+msgstr ""
+
+#: src/ui_parts/settings_menu.gd
+msgid "Tool"
+msgstr ""
+
+#: src/ui_parts/settings_menu.gd
+msgid "Help"
+msgstr ""
+
#: src/ui_parts/settings_menu.gd
msgid "Swaps zoom in and zoom out with the mouse wheel."
msgstr "Vertauscht Hinein- und Herauszoomen mit dem Mausrad."
@@ -726,6 +725,11 @@ msgstr "Gültige Farbe"
msgid "Warning color"
msgstr "Warnfarbe"
+#: src/ui_parts/tag_container.gd
+#, fuzzy
+msgid "New tag"
+msgstr "Neues Element hinzufügen"
+
#: src/ui_parts/update_menu.gd
msgid "Include prereleases"
msgstr ""
@@ -796,6 +800,14 @@ msgstr ""
msgid "Quit"
msgstr ""
+#: src/HandlerGUI.gd
+msgid "Check for updates?"
+msgstr ""
+
+#: src/HandlerGUI.gd
+msgid "This requires GodSVG to connect to the internet."
+msgstr ""
+
#: src/Indications.gd
msgid "View In List"
msgstr ""
@@ -812,10 +824,6 @@ msgstr "Konvertieren zu"
msgid "Import"
msgstr "Importieren"
-#: src/TranslationUtils.gd
-msgid "Load Reference Image"
-msgstr ""
-
#: src/TranslationUtils.gd
msgid "Optimize"
msgstr "Optimieren"
@@ -949,6 +957,13 @@ msgstr ""
msgid "Shorthand Cubic Bezier to"
msgstr ""
+#~ msgid "Remove"
+#~ msgstr "Entfernen"
+
+#, fuzzy
+#~ msgid "Unknown tag"
+#~ msgstr "Unbekanntes Element"
+
#~ msgid "Documentation…"
#~ msgstr "Dokumentation…"
diff --git a/translations/en.po b/translations/en.po
index 468a3242..c6aec227 100644
--- a/translations/en.po
+++ b/translations/en.po
@@ -93,8 +93,8 @@ msgstr ""
msgid "Paste XML"
msgstr ""
-#: src/ui_elements/palette_config.gd src/ui_elements/transform_popup.gd
-#: src/Indications.gd
+#: src/ui_elements/palette_config.gd src/ui_elements/setting_shortcut.gd
+#: src/ui_elements/transform_popup.gd src/Indications.gd
msgid "Delete"
msgstr ""
@@ -127,14 +127,6 @@ msgstr ""
msgid "Add shortcut"
msgstr ""
-#: src/ui_elements/setting_shortcut.gd
-msgid "Edit"
-msgstr ""
-
-#: src/ui_elements/setting_shortcut.gd
-msgid "Remove"
-msgstr ""
-
#: src/ui_elements/setting_shortcut.gd
msgid "Press keys…"
msgstr ""
@@ -176,11 +168,19 @@ msgid "Translators"
msgstr ""
#: src/ui_parts/about_menu.gd
-msgid "Authors"
+msgid "Donors"
msgstr ""
#: src/ui_parts/about_menu.gd
-msgid "Donors"
+msgid "Golden donors"
+msgstr ""
+
+#: src/ui_parts/about_menu.gd
+msgid "Diamond donors"
+msgstr ""
+
+#: src/ui_parts/about_menu.gd
+msgid "Authors"
msgstr ""
#: src/ui_parts/about_menu.gd
@@ -199,7 +199,7 @@ msgstr ""
msgid "Alert!"
msgstr ""
-#: src/ui_parts/alert_dialog.gd src/ui_parts/display.gd
+#: src/ui_parts/alert_dialog.gd src/HandlerGUI.gd
msgid "OK"
msgstr ""
@@ -249,18 +249,6 @@ msgstr ""
msgid "Overlay reference"
msgstr ""
-#: src/ui_parts/display.gd
-msgid "Import Reference Image"
-msgstr ""
-
-#: src/ui_parts/display.gd
-msgid "Show Reference Image"
-msgstr ""
-
-#: src/ui_parts/display.gd
-msgid "Overlay Reference Image"
-msgstr ""
-
#: src/ui_parts/display.gd
msgid "Show Grid"
msgstr ""
@@ -293,14 +281,6 @@ msgstr ""
msgid "Check for updates"
msgstr ""
-#: src/ui_parts/display.gd
-msgid "Check for updates?"
-msgstr ""
-
-#: src/ui_parts/display.gd
-msgid "This requires GodSVG to connect to the internet."
-msgstr ""
-
#: src/ui_parts/display.gd
msgid "Enable snapping"
msgstr ""
@@ -411,8 +391,8 @@ msgid ""
"contents!"
msgstr ""
-#: src/ui_parts/handles_manager.gd src/ui_parts/tag_container.gd
-msgid "New tag"
+#: src/ui_parts/handles_manager.gd
+msgid "New shape"
msgstr ""
#: src/ui_parts/import_warning_dialog.gd
@@ -420,11 +400,11 @@ msgid "Import Problems"
msgstr ""
#: src/ui_parts/import_warning_dialog.gd
-msgid "Unknown tag"
+msgid "Unrecognized tag"
msgstr ""
#: src/ui_parts/import_warning_dialog.gd
-msgid "Unknown attribute"
+msgid "Unrecognized attribute"
msgstr ""
#: src/ui_parts/import_warning_dialog.gd
@@ -527,6 +507,26 @@ msgstr ""
msgid "Language"
msgstr ""
+#: src/ui_parts/settings_menu.gd
+msgid "File"
+msgstr ""
+
+#: src/ui_parts/settings_menu.gd
+msgid "Edit"
+msgstr ""
+
+#: src/ui_parts/settings_menu.gd
+msgid "View"
+msgstr ""
+
+#: src/ui_parts/settings_menu.gd
+msgid "Tool"
+msgstr ""
+
+#: src/ui_parts/settings_menu.gd
+msgid "Help"
+msgstr ""
+
#: src/ui_parts/settings_menu.gd
msgid "Swaps zoom in and zoom out with the mouse wheel."
msgstr ""
@@ -691,6 +691,10 @@ msgstr ""
msgid "Warning color"
msgstr ""
+#: src/ui_parts/tag_container.gd
+msgid "New tag"
+msgstr ""
+
#: src/ui_parts/update_menu.gd
msgid "Include prereleases"
msgstr ""
@@ -756,6 +760,14 @@ msgstr ""
msgid "Quit"
msgstr ""
+#: src/HandlerGUI.gd
+msgid "Check for updates?"
+msgstr ""
+
+#: src/HandlerGUI.gd
+msgid "This requires GodSVG to connect to the internet."
+msgstr ""
+
#: src/Indications.gd
msgid "View In List"
msgstr ""
@@ -772,10 +784,6 @@ msgstr ""
msgid "Import"
msgstr ""
-#: src/TranslationUtils.gd
-msgid "Load Reference Image"
-msgstr ""
-
#: src/TranslationUtils.gd
msgid "Optimize"
msgstr ""
diff --git a/translations/ru.po b/translations/ru.po
index 3a7b6d1c..c695b10f 100644
--- a/translations/ru.po
+++ b/translations/ru.po
@@ -13,7 +13,8 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "translation-credits"
-msgstr "volkov , Gallifreyan "
+msgstr ""
+"volkov , Gallifreyan "
#: src/parsers/SVGParser.gd
msgid "Doesn’t describe an SVG."
@@ -93,8 +94,8 @@ msgstr "Скопировать как XML"
msgid "Paste XML"
msgstr "Вставить XML"
-#: src/ui_elements/palette_config.gd src/ui_elements/transform_popup.gd
-#: src/Indications.gd
+#: src/ui_elements/palette_config.gd src/ui_elements/setting_shortcut.gd
+#: src/ui_elements/transform_popup.gd src/Indications.gd
msgid "Delete"
msgstr "Удалить"
@@ -127,14 +128,6 @@ msgstr "Не используется"
msgid "Add shortcut"
msgstr "Добавить сочетание клавиш"
-#: src/ui_elements/setting_shortcut.gd
-msgid "Edit"
-msgstr "Редактировать"
-
-#: src/ui_elements/setting_shortcut.gd
-msgid "Remove"
-msgstr "Удалить"
-
#: src/ui_elements/setting_shortcut.gd
msgid "Press keys…"
msgstr "Нажмите клавиши…"
@@ -176,13 +169,22 @@ msgid "Translators"
msgstr "Переводчики"
#: src/ui_parts/about_menu.gd
-msgid "Authors"
-msgstr "Авторы"
+msgid "Donors"
+msgstr "Благотворители"
#: src/ui_parts/about_menu.gd
-msgid "Donors"
+msgid "Golden donors"
+msgstr ""
+
+#: src/ui_parts/about_menu.gd
+#, fuzzy
+msgid "Diamond donors"
msgstr "Благотворители"
+#: src/ui_parts/about_menu.gd
+msgid "Authors"
+msgstr "Авторы"
+
#: src/ui_parts/about_menu.gd
msgid "License"
msgstr "Лицензия"
@@ -199,7 +201,7 @@ msgstr "Лицензии третьих лиц"
msgid "Alert!"
msgstr "Внимание!"
-#: src/ui_parts/alert_dialog.gd src/ui_parts/display.gd
+#: src/ui_parts/alert_dialog.gd src/HandlerGUI.gd
msgid "OK"
msgstr "Хорошо"
@@ -249,18 +251,6 @@ msgstr "Показать референс-изображение"
msgid "Overlay reference"
msgstr "Наложить референс-изображение"
-#: src/ui_parts/display.gd
-msgid "Import Reference Image"
-msgstr "Импортировать референс-изображение"
-
-#: src/ui_parts/display.gd
-msgid "Show Reference Image"
-msgstr "Показать референс-изображение"
-
-#: src/ui_parts/display.gd
-msgid "Overlay Reference Image"
-msgstr "Наложить референс-изображение"
-
#: src/ui_parts/display.gd
msgid "Show Grid"
msgstr "Показать сетку"
@@ -293,14 +283,6 @@ msgstr "Веб-сайт GodSVG"
msgid "Check for updates"
msgstr "Проверить обновления"
-#: src/ui_parts/display.gd
-msgid "Check for updates?"
-msgstr "Проверить обновления?"
-
-#: src/ui_parts/display.gd
-msgid "This requires GodSVG to connect to the internet."
-msgstr "Для этого необходимо подключение к Интернету"
-
#: src/ui_parts/display.gd
msgid "Enable snapping"
msgstr "Включить привязку"
@@ -413,8 +395,9 @@ msgstr ""
"Файл с названием \"{file_name}\" уже существует. Его замена перезапишет его "
"содержимое!"
-#: src/ui_parts/handles_manager.gd src/ui_parts/tag_container.gd
-msgid "New tag"
+#: src/ui_parts/handles_manager.gd
+#, fuzzy
+msgid "New shape"
msgstr "Новый тег"
#: src/ui_parts/import_warning_dialog.gd
@@ -422,11 +405,12 @@ msgid "Import Problems"
msgstr "Проблемы с импортированием"
#: src/ui_parts/import_warning_dialog.gd
-msgid "Unknown tag"
-msgstr "Неизвестный тег"
+msgid "Unrecognized tag"
+msgstr ""
#: src/ui_parts/import_warning_dialog.gd
-msgid "Unknown attribute"
+#, fuzzy
+msgid "Unrecognized attribute"
msgstr "Неизвестный атрибут"
#: src/ui_parts/import_warning_dialog.gd
@@ -529,6 +513,26 @@ msgstr "Автоматический маштаб интерфейса поль
msgid "Language"
msgstr "Язык"
+#: src/ui_parts/settings_menu.gd
+msgid "File"
+msgstr "Файл"
+
+#: src/ui_parts/settings_menu.gd
+msgid "Edit"
+msgstr "Редактировать"
+
+#: src/ui_parts/settings_menu.gd
+msgid "View"
+msgstr "Просмотр"
+
+#: src/ui_parts/settings_menu.gd
+msgid "Tool"
+msgstr "Инструмент"
+
+#: src/ui_parts/settings_menu.gd
+msgid "Help"
+msgstr "Помощь"
+
#: src/ui_parts/settings_menu.gd
msgid "Swaps zoom in and zoom out with the mouse wheel."
msgstr "Поменяет местами увеличение и уменьшение масштаба через колесико мыши."
@@ -700,6 +704,10 @@ msgstr "Цвет валидности"
msgid "Warning color"
msgstr "Цвет предупреждения"
+#: src/ui_parts/tag_container.gd
+msgid "New tag"
+msgstr "Новый тег"
+
#: src/ui_parts/update_menu.gd
msgid "Include prereleases"
msgstr "Включить пре-релизы"
@@ -745,8 +753,8 @@ msgid ""
"\"{passed_extension}\" is a unsupported file extension. Only \"svg\" files "
"are supported."
msgstr ""
-"Расширение \"{passed_extension}\" поддерживается. "
-"Поддерживается только «svg»."
+"Расширение \"{passed_extension}\" поддерживается. Поддерживается только "
+"«svg»."
#: src/FileUtils.gd
msgid ""
@@ -770,6 +778,14 @@ msgstr "Вы хотите выйти из GodSVG?"
msgid "Quit"
msgstr "Выйти"
+#: src/HandlerGUI.gd
+msgid "Check for updates?"
+msgstr "Проверить обновления?"
+
+#: src/HandlerGUI.gd
+msgid "This requires GodSVG to connect to the internet."
+msgstr "Для этого необходимо подключение к Интернету"
+
#: src/Indications.gd
msgid "View In List"
msgstr "Просмотреть в списке"
@@ -786,10 +802,6 @@ msgstr "Конвертировать в"
msgid "Import"
msgstr "Импортировать"
-#: src/TranslationUtils.gd
-msgid "Load Reference Image"
-msgstr "Загрузить референс-изображение"
-
#: src/TranslationUtils.gd
msgid "Optimize"
msgstr "Оптимизировать"
@@ -914,17 +926,23 @@ msgstr "Кубическая Безье до"
msgid "Shorthand Cubic Bezier to"
msgstr "Сокращенная кубическая Безье до"
-#~ msgid "File"
-#~ msgstr "Файл"
+#~ msgid "Remove"
+#~ msgstr "Удалить"
+
+#~ msgid "Import Reference Image"
+#~ msgstr "Импортировать референс-изображение"
-#~ msgid "View"
-#~ msgstr "Просмотр"
+#~ msgid "Show Reference Image"
+#~ msgstr "Показать референс-изображение"
-#~ msgid "Tool"
-#~ msgstr "Инструмент"
+#~ msgid "Overlay Reference Image"
+#~ msgstr "Наложить референс-изображение"
-#~ msgid "Help"
-#~ msgstr "Помощь"
+#~ msgid "Unknown tag"
+#~ msgstr "Неизвестный тег"
+
+#~ msgid "Load Reference Image"
+#~ msgstr "Загрузить референс-изображение"
#~ msgid "Documentation…"
#~ msgstr "Документация…"
@@ -947,4 +965,3 @@ msgstr "Сокращенная кубическая Безье до"
#~ msgid "Import SVG"
#~ msgstr "Импортировать SVG"
-
diff --git a/translations/uk.po b/translations/uk.po
index 56ca2d34..efb7327f 100644
--- a/translations/uk.po
+++ b/translations/uk.po
@@ -93,8 +93,8 @@ msgstr "Скопіювати як XML"
msgid "Paste XML"
msgstr "Вставити XML"
-#: src/ui_elements/palette_config.gd src/ui_elements/transform_popup.gd
-#: src/Indications.gd
+#: src/ui_elements/palette_config.gd src/ui_elements/setting_shortcut.gd
+#: src/ui_elements/transform_popup.gd src/Indications.gd
msgid "Delete"
msgstr "Видалити"
@@ -127,14 +127,6 @@ msgstr "Не використовується"
msgid "Add shortcut"
msgstr "Додати клавіатурне скорочення"
-#: src/ui_elements/setting_shortcut.gd
-msgid "Edit"
-msgstr "Редагування"
-
-#: src/ui_elements/setting_shortcut.gd
-msgid "Remove"
-msgstr "Видалити"
-
#: src/ui_elements/setting_shortcut.gd
msgid "Press keys…"
msgstr "Натисніть кнопки…"
@@ -176,13 +168,22 @@ msgid "Translators"
msgstr "Перекладачі"
#: src/ui_parts/about_menu.gd
-msgid "Authors"
-msgstr "Автори"
+msgid "Donors"
+msgstr "Благодійники"
#: src/ui_parts/about_menu.gd
-msgid "Donors"
+msgid "Golden donors"
+msgstr ""
+
+#: src/ui_parts/about_menu.gd
+#, fuzzy
+msgid "Diamond donors"
msgstr "Благодійники"
+#: src/ui_parts/about_menu.gd
+msgid "Authors"
+msgstr "Автори"
+
#: src/ui_parts/about_menu.gd
msgid "License"
msgstr "Ліцензія"
@@ -199,7 +200,7 @@ msgstr "Ліцензії третіх осіб"
msgid "Alert!"
msgstr "Увага!"
-#: src/ui_parts/alert_dialog.gd src/ui_parts/display.gd
+#: src/ui_parts/alert_dialog.gd src/HandlerGUI.gd
msgid "OK"
msgstr "Добре"
@@ -249,18 +250,6 @@ msgstr ""
msgid "Overlay reference"
msgstr ""
-#: src/ui_parts/display.gd
-msgid "Import Reference Image"
-msgstr ""
-
-#: src/ui_parts/display.gd
-msgid "Show Reference Image"
-msgstr ""
-
-#: src/ui_parts/display.gd
-msgid "Overlay Reference Image"
-msgstr ""
-
#: src/ui_parts/display.gd
msgid "Show Grid"
msgstr "Показати ґратку"
@@ -293,15 +282,6 @@ msgstr "Веб-сайт GodSVG"
msgid "Check for updates"
msgstr "Перевірити оновлення"
-#: src/ui_parts/display.gd
-#, fuzzy
-msgid "Check for updates?"
-msgstr "Перевірити оновлення"
-
-#: src/ui_parts/display.gd
-msgid "This requires GodSVG to connect to the internet."
-msgstr ""
-
#: src/ui_parts/display.gd
msgid "Enable snapping"
msgstr "Активувати прилипання"
@@ -415,8 +395,9 @@ msgstr ""
"Файл з назваою \"{file_name}\" вже існує. Його заміна призведе до перезапису "
"його вмісту!"
-#: src/ui_parts/handles_manager.gd src/ui_parts/tag_container.gd
-msgid "New tag"
+#: src/ui_parts/handles_manager.gd
+#, fuzzy
+msgid "New shape"
msgstr "Новий тег"
#: src/ui_parts/import_warning_dialog.gd
@@ -424,11 +405,12 @@ msgid "Import Problems"
msgstr "Проблема з імпортуванням"
#: src/ui_parts/import_warning_dialog.gd
-msgid "Unknown tag"
-msgstr "Невідомий тег"
+msgid "Unrecognized tag"
+msgstr ""
#: src/ui_parts/import_warning_dialog.gd
-msgid "Unknown attribute"
+#, fuzzy
+msgid "Unrecognized attribute"
msgstr "Невідомий атрибут"
#: src/ui_parts/import_warning_dialog.gd
@@ -531,6 +513,26 @@ msgstr "Автоматичний масштаб інтерфейсу"
msgid "Language"
msgstr "Мова"
+#: src/ui_parts/settings_menu.gd
+msgid "File"
+msgstr "Файл"
+
+#: src/ui_parts/settings_menu.gd
+msgid "Edit"
+msgstr "Редагування"
+
+#: src/ui_parts/settings_menu.gd
+msgid "View"
+msgstr "Перегляд"
+
+#: src/ui_parts/settings_menu.gd
+msgid "Tool"
+msgstr "Інструмент"
+
+#: src/ui_parts/settings_menu.gd
+msgid "Help"
+msgstr "Допомога"
+
#: src/ui_parts/settings_menu.gd
msgid "Swaps zoom in and zoom out with the mouse wheel."
msgstr ""
@@ -704,6 +706,10 @@ msgstr "Колір валідності"
msgid "Warning color"
msgstr "Колір попередження"
+#: src/ui_parts/tag_container.gd
+msgid "New tag"
+msgstr "Новий тег"
+
#: src/ui_parts/update_menu.gd
msgid "Include prereleases"
msgstr ""
@@ -775,6 +781,15 @@ msgstr "Ви дійсно бажаєте вийти із GodSVG?"
msgid "Quit"
msgstr "Вийти"
+#: src/HandlerGUI.gd
+#, fuzzy
+msgid "Check for updates?"
+msgstr "Перевірити оновлення"
+
+#: src/HandlerGUI.gd
+msgid "This requires GodSVG to connect to the internet."
+msgstr ""
+
#: src/Indications.gd
msgid "View In List"
msgstr "Переглянути списком"
@@ -791,10 +806,6 @@ msgstr "Конвертувати в"
msgid "Import"
msgstr "Імпортувати"
-#: src/TranslationUtils.gd
-msgid "Load Reference Image"
-msgstr ""
-
#: src/TranslationUtils.gd
msgid "Optimize"
msgstr "Оптимізувати"
@@ -919,17 +930,11 @@ msgstr "Кубічна Безьє до"
msgid "Shorthand Cubic Bezier to"
msgstr "Скорочена кубічна Безьє до"
-#~ msgid "File"
-#~ msgstr "Файл"
-
-#~ msgid "View"
-#~ msgstr "Перегляд"
-
-#~ msgid "Tool"
-#~ msgstr "Інструмент"
+#~ msgid "Remove"
+#~ msgstr "Видалити"
-#~ msgid "Help"
-#~ msgstr "Допомога"
+#~ msgid "Unknown tag"
+#~ msgstr "Невідомий тег"
#~ msgid "Documentation…"
#~ msgstr "Документація…"
diff --git a/visual/icons/ApplyMatrix.svg b/visual/icons/ApplyMatrix.svg
index b9608c58..87ba3b4b 100644
--- a/visual/icons/ApplyMatrix.svg
+++ b/visual/icons/ApplyMatrix.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/Clear.svg b/visual/icons/Clear.svg
index d7a94236..8d8b36b6 100644
--- a/visual/icons/Clear.svg
+++ b/visual/icons/Clear.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/CodeOptions.svg b/visual/icons/CodeOptions.svg
index df9b2c71..4fef705c 100644
--- a/visual/icons/CodeOptions.svg
+++ b/visual/icons/CodeOptions.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/Compress.svg b/visual/icons/Compress.svg
index e67acc24..a6c4f97b 100644
--- a/visual/icons/Compress.svg
+++ b/visual/icons/Compress.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/CreateFolder.svg b/visual/icons/CreateFolder.svg
index 5de0bb8b..bb7434d1 100644
--- a/visual/icons/CreateFolder.svg
+++ b/visual/icons/CreateFolder.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/DirDocuments.svg b/visual/icons/DirDocuments.svg
index 539e47c1..e1e368e6 100644
--- a/visual/icons/DirDocuments.svg
+++ b/visual/icons/DirDocuments.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/DirMovies.svg b/visual/icons/DirMovies.svg
index 0a43fb26..73a82653 100644
--- a/visual/icons/DirMovies.svg
+++ b/visual/icons/DirMovies.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/DirMusic.svg b/visual/icons/DirMusic.svg
index f5cfcd19..7ba2143d 100644
--- a/visual/icons/DirMusic.svg
+++ b/visual/icons/DirMusic.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/DirPictures.svg b/visual/icons/DirPictures.svg
index 86153c63..c596bb9f 100644
--- a/visual/icons/DirPictures.svg
+++ b/visual/icons/DirPictures.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/Edit.svg b/visual/icons/Edit.svg
index bc25806e..0c8287f4 100644
--- a/visual/icons/Edit.svg
+++ b/visual/icons/Edit.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/FileBroken.svg b/visual/icons/FileBroken.svg
index d55bd4e9..4d44802d 100644
--- a/visual/icons/FileBroken.svg
+++ b/visual/icons/FileBroken.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/InsertAfter.svg b/visual/icons/InsertAfter.svg
index beeb66d3..ca66f42c 100644
--- a/visual/icons/InsertAfter.svg
+++ b/visual/icons/InsertAfter.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/InsertBefore.svg b/visual/icons/InsertBefore.svg
index 2431abe1..116dd8d8 100644
--- a/visual/icons/InsertBefore.svg
+++ b/visual/icons/InsertBefore.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/NoneColor.svg b/visual/icons/NoneColor.svg
index 30c292ab..93c98e01 100644
--- a/visual/icons/NoneColor.svg
+++ b/visual/icons/NoneColor.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/OpenFile.svg b/visual/icons/OpenFile.svg
index 96153633..59448014 100644
--- a/visual/icons/OpenFile.svg
+++ b/visual/icons/OpenFile.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/OpenFolder.svg b/visual/icons/OpenFolder.svg
index 5de0a10f..3e6928e6 100644
--- a/visual/icons/OpenFolder.svg
+++ b/visual/icons/OpenFolder.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/Redo.svg b/visual/icons/Redo.svg
index bc890b8b..f2835dc7 100644
--- a/visual/icons/Redo.svg
+++ b/visual/icons/Redo.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/Reference.svg b/visual/icons/Reference.svg
index 1cc8169d..8eebab5e 100644
--- a/visual/icons/Reference.svg
+++ b/visual/icons/Reference.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/Rotate.svg b/visual/icons/Rotate.svg
index 92541691..a099d47f 100644
--- a/visual/icons/Rotate.svg
+++ b/visual/icons/Rotate.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/Save.svg b/visual/icons/Save.svg
index 239388e2..a9959519 100644
--- a/visual/icons/Save.svg
+++ b/visual/icons/Save.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/Scale.svg b/visual/icons/Scale.svg
index c9fe715c..82a33957 100644
--- a/visual/icons/Scale.svg
+++ b/visual/icons/Scale.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/SideSliderArrow.svg b/visual/icons/SideSliderArrow.svg
index 7b5670e0..20b64e7d 100644
--- a/visual/icons/SideSliderArrow.svg
+++ b/visual/icons/SideSliderArrow.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/visual/icons/SkewX.svg b/visual/icons/SkewX.svg
index b3a1cc1b..bcc15991 100644
--- a/visual/icons/SkewX.svg
+++ b/visual/icons/SkewX.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/SkewY.svg b/visual/icons/SkewY.svg
index 21b70b13..65f1813b 100644
--- a/visual/icons/SkewY.svg
+++ b/visual/icons/SkewY.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/SliderArrow.svg b/visual/icons/SliderArrow.svg
index 4f8c8193..011e94c6 100644
--- a/visual/icons/SliderArrow.svg
+++ b/visual/icons/SliderArrow.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/visual/icons/Snap.svg b/visual/icons/Snap.svg
index 49a7f280..c94a9ad1 100644
--- a/visual/icons/Snap.svg
+++ b/visual/icons/Snap.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/visual/icons/TagWarning.svg b/visual/icons/TagWarning.svg
new file mode 100644
index 00000000..bca7ff90
--- /dev/null
+++ b/visual/icons/TagWarning.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/visual/icons/TagWarning.svg.import b/visual/icons/TagWarning.svg.import
new file mode 100644
index 00000000..4c448ca1
--- /dev/null
+++ b/visual/icons/TagWarning.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dbyjet4nt246k"
+path="res://.godot/imported/TagWarning.svg-229c14c2b5031de4abbee36f927866c2.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://visual/icons/TagWarning.svg"
+dest_files=["res://.godot/imported/TagWarning.svg-229c14c2b5031de4abbee36f927866c2.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/visual/icons/Undo.svg b/visual/icons/Undo.svg
index 05a67846..a2e880fe 100644
--- a/visual/icons/Undo.svg
+++ b/visual/icons/Undo.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/visual/icons/backgrounds/ColorButtonBG.svg b/visual/icons/backgrounds/ColorButtonBG.svg
index 2d154241..04a3fce3 100644
--- a/visual/icons/backgrounds/ColorButtonBG.svg
+++ b/visual/icons/backgrounds/ColorButtonBG.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/visual/icons/foreign_logos/KoFiLogo.svg b/visual/icons/foreign_logos/KoFiLogo.svg
index f2faf528..4d26af84 100644
--- a/visual/icons/foreign_logos/KoFiLogo.svg
+++ b/visual/icons/foreign_logos/KoFiLogo.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/tag/g.svg b/visual/icons/tag/g.svg
index 02f33f54..9921ebb1 100644
--- a/visual/icons/tag/g.svg
+++ b/visual/icons/tag/g.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/visual/icons/tag/linearGradient.svg b/visual/icons/tag/linearGradient.svg
index 14ce71f6..10491f38 100644
--- a/visual/icons/tag/linearGradient.svg
+++ b/visual/icons/tag/linearGradient.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/tag/radialGradient.svg b/visual/icons/tag/radialGradient.svg
index f3e1c8cb..d0be2964 100644
--- a/visual/icons/tag/radialGradient.svg
+++ b/visual/icons/tag/radialGradient.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/tag/stop.svg b/visual/icons/tag/stop.svg
index 9129b198..f1b9bee9 100644
--- a/visual/icons/tag/stop.svg
+++ b/visual/icons/tag/stop.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/visual/icons/tag/unknown.svg b/visual/icons/tag/unrecognized.svg
similarity index 100%
rename from visual/icons/tag/unknown.svg
rename to visual/icons/tag/unrecognized.svg
diff --git a/visual/icons/tag/unknown.svg.import b/visual/icons/tag/unrecognized.svg.import
similarity index 73%
rename from visual/icons/tag/unknown.svg.import
rename to visual/icons/tag/unrecognized.svg.import
index 36b1fd94..51f0450e 100644
--- a/visual/icons/tag/unknown.svg.import
+++ b/visual/icons/tag/unrecognized.svg.import
@@ -3,15 +3,15 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://brua6wag257kc"
-path="res://.godot/imported/unknown.svg-5ab2cc84835c40cff0c8541c05d0d795.ctex"
+path="res://.godot/imported/unrecognized.svg-39738dc7ca3df867b16222d00eef9375.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://visual/icons/tag/unknown.svg"
-dest_files=["res://.godot/imported/unknown.svg-5ab2cc84835c40cff0c8541c05d0d795.ctex"]
+source_file="res://visual/icons/tag/unrecognized.svg"
+dest_files=["res://.godot/imported/unrecognized.svg-39738dc7ca3df867b16222d00eef9375.ctex"]
[params]