settings! (sort of)
authorEduardo <[email protected]>
Sun, 5 May 2024 17:11:32 +0000 (19:11 +0200)
committerEduardo <[email protected]>
Sun, 5 May 2024 17:11:32 +0000 (19:11 +0200)
147 files changed:
addons/ggs/assets/add.svg [new file with mode: 0644]
addons/ggs/assets/add.svg.import [new file with mode: 0644]
addons/ggs/assets/bug.svg [new file with mode: 0644]
addons/ggs/assets/bug.svg.import [new file with mode: 0644]
addons/ggs/assets/check_all.svg [new file with mode: 0644]
addons/ggs/assets/check_all.svg.import [new file with mode: 0644]
addons/ggs/assets/close.svg [new file with mode: 0644]
addons/ggs/assets/close.svg.import [new file with mode: 0644]
addons/ggs/assets/collapse_all.svg [new file with mode: 0644]
addons/ggs/assets/collapse_all.svg.import [new file with mode: 0644]
addons/ggs/assets/delete.svg [new file with mode: 0644]
addons/ggs/assets/delete.svg.import [new file with mode: 0644]
addons/ggs/assets/docs.svg [new file with mode: 0644]
addons/ggs/assets/docs.svg.import [new file with mode: 0644]
addons/ggs/assets/expand_all.svg [new file with mode: 0644]
addons/ggs/assets/expand_all.svg.import [new file with mode: 0644]
addons/ggs/assets/feedback.svg [new file with mode: 0644]
addons/ggs/assets/feedback.svg.import [new file with mode: 0644]
addons/ggs/assets/file_dialog.svg [new file with mode: 0644]
addons/ggs/assets/file_dialog.svg.import [new file with mode: 0644]
addons/ggs/assets/icon_mini.svg [new file with mode: 0644]
addons/ggs/assets/icon_mini.svg.import [new file with mode: 0644]
addons/ggs/assets/icon_mono.svg [new file with mode: 0644]
addons/ggs/assets/icon_mono.svg.import [new file with mode: 0644]
addons/ggs/assets/reload.svg [new file with mode: 0644]
addons/ggs/assets/reload.svg.import [new file with mode: 0644]
addons/ggs/assets/rename.svg [new file with mode: 0644]
addons/ggs/assets/rename.svg.import [new file with mode: 0644]
addons/ggs/assets/save_file.svg [new file with mode: 0644]
addons/ggs/assets/save_file.svg.import [new file with mode: 0644]
addons/ggs/assets/search.svg [new file with mode: 0644]
addons/ggs/assets/search.svg.import [new file with mode: 0644]
addons/ggs/assets/show_in_filesystem.svg [new file with mode: 0644]
addons/ggs/assets/show_in_filesystem.svg.import [new file with mode: 0644]
addons/ggs/assets/theme.svg [new file with mode: 0644]
addons/ggs/assets/theme.svg.import [new file with mode: 0644]
addons/ggs/assets/uncheck_all.svg [new file with mode: 0644]
addons/ggs/assets/uncheck_all.svg.import [new file with mode: 0644]
addons/ggs/classes/ggs_input_helper.gd [new file with mode: 0644]
addons/ggs/classes/ggs_inspector_plugin.gd [new file with mode: 0644]
addons/ggs/classes/ggs_save_file.gd [new file with mode: 0644]
addons/ggs/classes/ggs_ui_component.gd [new file with mode: 0644]
addons/ggs/classes/ggs_utils.gd [new file with mode: 0644]
addons/ggs/classes/global/ggs.gd [new file with mode: 0644]
addons/ggs/classes/global/ggs.tscn [new file with mode: 0644]
addons/ggs/classes/resources/ggs_icon_db.gd [new file with mode: 0644]
addons/ggs/classes/resources/ggs_plugin_data.gd [new file with mode: 0644]
addons/ggs/classes/resources/ggs_setting.gd [new file with mode: 0644]
addons/ggs/docs/changelog.md [new file with mode: 0644]
addons/ggs/docs/components/apply_button.md [new file with mode: 0644]
addons/ggs/docs/components/arrow_list.md [new file with mode: 0644]
addons/ggs/docs/components/binary_selection.md [new file with mode: 0644]
addons/ggs/docs/components/components.md [new file with mode: 0644]
addons/ggs/docs/components/input_button.md [new file with mode: 0644]
addons/ggs/docs/components/input_confirm_window.md [new file with mode: 0644]
addons/ggs/docs/components/option_list.md [new file with mode: 0644]
addons/ggs/docs/components/radio_list.md [new file with mode: 0644]
addons/ggs/docs/components/reset_button.md [new file with mode: 0644]
addons/ggs/docs/components/slider.md [new file with mode: 0644]
addons/ggs/docs/components/spinbox.md [new file with mode: 0644]
addons/ggs/docs/components/text_field.md [new file with mode: 0644]
addons/ggs/docs/custom_components.md [new file with mode: 0644]
addons/ggs/docs/custom_settings.md [new file with mode: 0644]
addons/ggs/docs/getting_started.md [new file with mode: 0644]
addons/ggs/docs/home.md [new file with mode: 0644]
addons/ggs/docs/settings/audio_mute.md [new file with mode: 0644]
addons/ggs/docs/settings/audio_volume.md [new file with mode: 0644]
addons/ggs/docs/settings/display_fullscreen.md [new file with mode: 0644]
addons/ggs/docs/settings/display_scale.md [new file with mode: 0644]
addons/ggs/docs/settings/display_size.md [new file with mode: 0644]
addons/ggs/docs/settings/input.md [new file with mode: 0644]
addons/ggs/docs/settings/settings.md [new file with mode: 0644]
addons/ggs/docs/troubleshoot.md [new file with mode: 0644]
addons/ggs/editor/_theme/ggs_theme.gd [new file with mode: 0644]
addons/ggs/editor/_theme/ggs_theme.tres [new file with mode: 0644]
addons/ggs/editor/add_setting_window/add_setting_window.gd [new file with mode: 0644]
addons/ggs/editor/add_setting_window/add_setting_window.tscn [new file with mode: 0644]
addons/ggs/editor/category_panel/category_list.gd [new file with mode: 0644]
addons/ggs/editor/category_panel/category_panel.gd [new file with mode: 0644]
addons/ggs/editor/category_panel/category_panel.tscn [new file with mode: 0644]
addons/ggs/editor/component_panel/component_panel.gd [new file with mode: 0644]
addons/ggs/editor/component_panel/component_panel.tscn [new file with mode: 0644]
addons/ggs/editor/input_selector/input_list.gd [new file with mode: 0644]
addons/ggs/editor/input_selector/input_selector.gd [new file with mode: 0644]
addons/ggs/editor/input_selector/input_selector.tscn [new file with mode: 0644]
addons/ggs/editor/main_panel/bug_btn.gd [new file with mode: 0644]
addons/ggs/editor/main_panel/docs_btn.gd [new file with mode: 0644]
addons/ggs/editor/main_panel/feedback_btn.gd [new file with mode: 0644]
addons/ggs/editor/main_panel/main_panel.tscn [new file with mode: 0644]
addons/ggs/editor/main_panel/notification.gd [new file with mode: 0644]
addons/ggs/editor/main_panel/pref_btn.gd [new file with mode: 0644]
addons/ggs/editor/main_panel/progress_overlay.gd [new file with mode: 0644]
addons/ggs/editor/main_panel/save_file_menu.gd [new file with mode: 0644]
addons/ggs/editor/main_panel/split_containers.gd [new file with mode: 0644]
addons/ggs/editor/main_panel/update_theme_btn.gd [new file with mode: 0644]
addons/ggs/editor/pref_window/pref_window.gd [new file with mode: 0644]
addons/ggs/editor/pref_window/pref_window.tscn [new file with mode: 0644]
addons/ggs/editor/setting_panel/groupless.gd [new file with mode: 0644]
addons/ggs/editor/setting_panel/setting_group/setting_group.gd [new file with mode: 0644]
addons/ggs/editor/setting_panel/setting_group/setting_group.tscn [new file with mode: 0644]
addons/ggs/editor/setting_panel/setting_item/setting_item.gd [new file with mode: 0644]
addons/ggs/editor/setting_panel/setting_item/setting_item.tscn [new file with mode: 0644]
addons/ggs/editor/setting_panel/setting_list.gd [new file with mode: 0644]
addons/ggs/editor/setting_panel/setting_panel.gd [new file with mode: 0644]
addons/ggs/editor/setting_panel/setting_panel.tscn [new file with mode: 0644]
addons/ggs/plugin.cfg [new file with mode: 0644]
addons/ggs/plugin.gd [new file with mode: 0644]
addons/ggs/plugin_data.tres [new file with mode: 0644]
addons/ggs/template.gd [new file with mode: 0644]
game_settings/components/_misc_components/apply_btn/apply_btn.gd [new file with mode: 0644]
game_settings/components/_misc_components/apply_btn/apply_btn.tscn [new file with mode: 0644]
game_settings/components/_misc_components/input_confirm_window/input_confirm_window.gd [new file with mode: 0644]
game_settings/components/_misc_components/input_confirm_window/input_confirm_window.tscn [new file with mode: 0644]
game_settings/components/_misc_components/reset_btn/reset_btn.gd [new file with mode: 0644]
game_settings/components/_misc_components/reset_btn/reset_btn.tscn [new file with mode: 0644]
game_settings/components/_shared_scripts/binary_selection.gd [new file with mode: 0644]
game_settings/components/arrow_list/arrow_list.gd [new file with mode: 0644]
game_settings/components/arrow_list/arrow_list.tscn [new file with mode: 0644]
game_settings/components/checkbox/checkbox.tscn [new file with mode: 0644]
game_settings/components/input_btn/input_btn.gd [new file with mode: 0644]
game_settings/components/input_btn/input_btn.tscn [new file with mode: 0644]
game_settings/components/option_list/option_list.gd [new file with mode: 0644]
game_settings/components/option_list/option_list.tscn [new file with mode: 0644]
game_settings/components/radio_list/radio_list.gd [new file with mode: 0644]
game_settings/components/radio_list/radio_list.tscn [new file with mode: 0644]
game_settings/components/slider/slider.gd [new file with mode: 0644]
game_settings/components/slider/slider.tscn [new file with mode: 0644]
game_settings/components/spinbox/spinbox.gd [new file with mode: 0644]
game_settings/components/spinbox/spinbox.tscn [new file with mode: 0644]
game_settings/components/switch/switch.tscn [new file with mode: 0644]
game_settings/components/text_field/text_field.gd [new file with mode: 0644]
game_settings/components/text_field/text_field.tscn [new file with mode: 0644]
game_settings/components/toggle_btn/toggle_btn.tscn [new file with mode: 0644]
game_settings/settings/audio/Volume.tres [new file with mode: 0644]
game_settings/settings/controls/Movement.tres [new file with mode: 0644]
game_settings/settings/nonempty.txt [new file with mode: 0644]
game_settings/settings/video/Fullscreen.tres [new file with mode: 0644]
game_settings/settings/video/Scale.tres [new file with mode: 0644]
game_settings/settings/video/Size.tres [new file with mode: 0644]
game_settings/templates/audio/audio_mute.gd [new file with mode: 0644]
game_settings/templates/audio/audio_volume.gd [new file with mode: 0644]
game_settings/templates/display/display_fullscreen.gd [new file with mode: 0644]
game_settings/templates/display/display_scale.gd [new file with mode: 0644]
game_settings/templates/display/display_size.gd [new file with mode: 0644]
game_settings/templates/input.gd [new file with mode: 0644]
scenes/settings/Settings.tscn [new file with mode: 0644]
scenes/settings/settings.gd [new file with mode: 0644]

diff --git a/addons/ggs/assets/add.svg b/addons/ggs/assets/add.svg
new file mode 100644 (file)
index 0000000..afad08a
--- /dev/null
@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m7 1v6h-6v2h6v6h2v-6h6v-2h-6v-6z" fill="#e0e0e0"/></svg>
diff --git a/addons/ggs/assets/add.svg.import b/addons/ggs/assets/add.svg.import
new file mode 100644 (file)
index 0000000..7bfee0c
--- /dev/null
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://by345a10evjm8"
+path="res://.godot/imported/add.svg-cc4a8e36f6ce6a04a3c9cc98b73e4df8.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ggs/assets/add.svg"
+dest_files=["res://.godot/imported/add.svg-cc4a8e36f6ce6a04a3c9cc98b73e4df8.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/addons/ggs/assets/bug.svg b/addons/ggs/assets/bug.svg
new file mode 100644 (file)
index 0000000..769fada
--- /dev/null
@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m8 1c-1.3257 0-2.5977.52744-3.5352 1.4648a1 1 0 0 0 0 1.4141 1 1 0 0 0 .69141.29297 1 1 0 0 0 .72266-.29297c.56288-.5628 1.3251-.87891 2.1211-.87891s1.5582.31611 2.1211.87891a1 1 0 0 0 1.4141 0 1 1 0 0 0 0-1.4141c-.93741-.9374-2.2095-1.4648-3.5352-1.4648zm-5 3.9961a1 1 0 0 0 -1 1c0 .8334.32654 1.6973.96875 2.5.33016.41272.7705.79575 1.3008 1.0723a4 4 0 0 0 -.13672.43164h-2.1328a1 1 0 0 0 -1 1 1 1 0 0 0 1 1h2.1309a4 4 0 0 0 .17969.53711c-.14177.089422-.27868.1846-.41016.2832-.58533.439-1.1074.96875-1.6074 1.4688a1 1 0 0 0 0 1.4141 1 1 0 0 0 1.4141 0c.5-.5.97791-.9722 1.3926-1.2832.1693-.12693.3098-.20282.44336-.26953a4 4 0 0 0 2.457.84961 4 4 0 0 0 2.459-.84766c.13307.066645.27298.14126.44141.26758.41467.311.89258.7832 1.3926 1.2832a1 1 0 0 0 1.4141 0 1 1 0 0 0 0-1.4141c-.5-.5-1.0221-1.0297-1.6074-1.4688-.13076-.098068-.26727-.19224-.4082-.28125a4 4 0 0 0 .17578-.53906h2.1328a1 1 0 0 0 1-1 1 1 0 0 0 -1-1h-2.1309a4 4 0 0 0 -.13477-.43359c.52857-.27637.96751-.65858 1.2969-1.0703.64221-.8027.96875-1.6666.96875-2.5a1 1 0 0 0 -1-1 1 1 0 0 0 -1 1c0 .1667-.17346.8028-.53125 1.25-.25089.31365-.54884.54907-.93164.66602a4 4 0 0 0 -.60352-.41211 2 2 0 0 0 .066406-.5 2 2 0 0 0 -2-2 2 2 0 0 0 -2 2 2 2 0 0 0 .066406.50391 4 4 0 0 0 -.60352.4082c-.3828-.11694-.68075-.35236-.93164-.66602-.35779-.4472-.53125-1.0833-.53125-1.25a1 1 0 0 0 -1-1z" fill="#e0e0e0"/></svg>
diff --git a/addons/ggs/assets/bug.svg.import b/addons/ggs/assets/bug.svg.import
new file mode 100644 (file)
index 0000000..f85c4d8
--- /dev/null
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bt7gdorkvo4an"
+path="res://.godot/imported/bug.svg-f98ad47dc3f3571a377a4a231005fffe.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ggs/assets/bug.svg"
+dest_files=["res://.godot/imported/bug.svg-f98ad47dc3f3571a377a4a231005fffe.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/addons/ggs/assets/check_all.svg b/addons/ggs/assets/check_all.svg
new file mode 100644 (file)
index 0000000..b022f93
--- /dev/null
@@ -0,0 +1 @@
+<svg id="a18a26ae-64ca-419b-b48a-aa31370df270" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="16.3" height="16" viewBox="0 0 16.3 16"><path d="M13.86,2.67V13.33H2.44V2.67H13.86M14.71,0H1.59A1.55,1.55,0,0,0,.15,1.65V14.32A1.57,1.57,0,0,0,1.57,16H14.71a1.55,1.55,0,0,0,1.44-1.65V1.68A1.57,1.57,0,0,0,14.73,0Z" style="fill:#e0e0e0"/><path d="M12.4,6.17l-4,4L7.12,11.45a.47.47,0,0,1-.58.07L5.43,10.41,3.9,8.88a.49.49,0,0,1,0-.69l.93-.93a.48.48,0,0,1,.68,0L6.79,8.54l4-4a.49.49,0,0,1,.69,0l.93.93A.49.49,0,0,1,12.4,6.17Z" style="fill:#e0e0e0"/></svg>
\ No newline at end of file
diff --git a/addons/ggs/assets/check_all.svg.import b/addons/ggs/assets/check_all.svg.import
new file mode 100644 (file)
index 0000000..a780f86
--- /dev/null
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bttv2hpecd38m"
+path="res://.godot/imported/check_all.svg-9b872310d9fb707e5903a11e61ac9a87.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ggs/assets/check_all.svg"
+dest_files=["res://.godot/imported/check_all.svg-9b872310d9fb707e5903a11e61ac9a87.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/addons/ggs/assets/close.svg b/addons/ggs/assets/close.svg
new file mode 100644 (file)
index 0000000..be1c1dc
--- /dev/null
@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m3 3 10 10M3 13 13 3" fill="none" stroke="#e0e0e0" stroke-width="2"/></svg>
diff --git a/addons/ggs/assets/close.svg.import b/addons/ggs/assets/close.svg.import
new file mode 100644 (file)
index 0000000..6f59e8c
--- /dev/null
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cflcng660hsp0"
+path="res://.godot/imported/close.svg-6b4a6b89f4446e3757436ab1e2e33be6.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ggs/assets/close.svg"
+dest_files=["res://.godot/imported/close.svg-6b4a6b89f4446e3757436ab1e2e33be6.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/addons/ggs/assets/collapse_all.svg b/addons/ggs/assets/collapse_all.svg
new file mode 100644 (file)
index 0000000..bff3eca
--- /dev/null
@@ -0,0 +1 @@
+<svg height="16" width="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m8 9.669-3.536 2.583H7v2.537h2v-2.537h2.536zm0-3.314L4.464 3.772H7V1.235h2v2.537h2.536zm-7.296.73h14.591v1.831H.704z" fill="#e0e0e0"/></svg>
diff --git a/addons/ggs/assets/collapse_all.svg.import b/addons/ggs/assets/collapse_all.svg.import
new file mode 100644 (file)
index 0000000..2a5329c
--- /dev/null
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://l0mve5lc0okm"
+path="res://.godot/imported/collapse_all.svg-84b31d2383c45ad2c7425908d0a5bd5b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ggs/assets/collapse_all.svg"
+dest_files=["res://.godot/imported/collapse_all.svg-84b31d2383c45ad2c7425908d0a5bd5b.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/addons/ggs/assets/delete.svg b/addons/ggs/assets/delete.svg
new file mode 100644 (file)
index 0000000..5bcdf8e
--- /dev/null
@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m5 1v1h-4v2h14v-2h-4v-1zm-3 4v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-8zm1 2h2v6h-2zm4 0h2v6h-2zm4 0h2v6h-2z" fill="#e0e0e0" fill-opacity=".99608"/></svg>
diff --git a/addons/ggs/assets/delete.svg.import b/addons/ggs/assets/delete.svg.import
new file mode 100644 (file)
index 0000000..b703175
--- /dev/null
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bn0d5k8dd06qr"
+path="res://.godot/imported/delete.svg-6f3953bc0542e18a1d77b52528ff86cf.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ggs/assets/delete.svg"
+dest_files=["res://.godot/imported/delete.svg-6f3953bc0542e18a1d77b52528ff86cf.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/addons/ggs/assets/docs.svg b/addons/ggs/assets/docs.svg
new file mode 100644 (file)
index 0000000..2025603
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15"><g id="b991fec5-f9b0-4c88-b529-62c26a16a05f" data-name="Layer 2"><g id="e6e9e42c-42f8-4fc6-8d50-794850aacc63" data-name="Layer 1"><path d="M5,0A5.26,5.26,0,0,0,2,1V8A5,5,0,0,1,8,8a5,5,0,0,1,6,0V1a5.26,5.26,0,0,0-3-1A5,5,0,0,0,9,.46V5H8V1A5.53,5.53,0,0,0,5,0ZM0,9v6H2A3,3,0,0,0,2,9Zm5,3A3,3,0,1,0,8,9,3,3,0,0,0,5,12Zm6,0a3,3,0,0,0,3,3h1V13H14a1,1,0,0,1,0-2h1V9H14A3,3,0,0,0,11,12ZM2,11a1,1,0,0,1,0,2Zm6,0a1,1,0,1,1-1,1A1,1,0,0,1,8,11Z" style="fill:#e0e0e0"/></g></g></svg>
\ No newline at end of file
diff --git a/addons/ggs/assets/docs.svg.import b/addons/ggs/assets/docs.svg.import
new file mode 100644 (file)
index 0000000..00ea2d6
--- /dev/null
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cdxv6r8uy2me5"
+path="res://.godot/imported/docs.svg-0b7d65f3eeee9d1934d1922c3c815f4e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ggs/assets/docs.svg"
+dest_files=["res://.godot/imported/docs.svg-0b7d65f3eeee9d1934d1922c3c815f4e.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/addons/ggs/assets/expand_all.svg b/addons/ggs/assets/expand_all.svg
new file mode 100644 (file)
index 0000000..1b0a951
--- /dev/null
@@ -0,0 +1 @@
+<svg height="16" width="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m8 16-3.5-2.5H7V11h2v2.5h2.5zM8 0 4.5 2.5H7V5h2V2.5h2.5zM1 7h14v2H1z" fill="#e0e0e0"/></svg>
diff --git a/addons/ggs/assets/expand_all.svg.import b/addons/ggs/assets/expand_all.svg.import
new file mode 100644 (file)
index 0000000..575b82e
--- /dev/null
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://caajrkkuvle0e"
+path="res://.godot/imported/expand_all.svg-04baf0245d861ddcbc4e38a80edbda26.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ggs/assets/expand_all.svg"
+dest_files=["res://.godot/imported/expand_all.svg-04baf0245d861ddcbc4e38a80edbda26.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/addons/ggs/assets/feedback.svg b/addons/ggs/assets/feedback.svg
new file mode 100644 (file)
index 0000000..3e8598a
--- /dev/null
@@ -0,0 +1 @@
+<svg id="bc870507-cbc2-49b2-bbb0-001f5173543c" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8.23,16,8,13.56a6.6,6.6,0,0,1-4.82-2,6.5,6.5,0,0,1-2-4.8A6.5,6.5,0,0,1,3.19,2,6.57,6.57,0,0,1,8,0a6.65,6.65,0,0,1,2.64.52A7,7,0,0,1,12.8,2a6.77,6.77,0,0,1,1.46,2.15,6.51,6.51,0,0,1,.54,2.65,8.56,8.56,0,0,1-.5,2.91,10.32,10.32,0,0,1-1.37,2.58,11.37,11.37,0,0,1-2.08,2.14A13,13,0,0,1,8.23,16ZM10,12.91a10.33,10.33,0,0,0,2.25-2.76,6.79,6.79,0,0,0,.88-3.38A4.86,4.86,0,0,0,11.6,3.18,5,5,0,0,0,8,1.72,4.9,4.9,0,0,0,4.4,3.2,4.86,4.86,0,0,0,2.92,6.79,4.85,4.85,0,0,0,4.4,10.37,4.93,4.93,0,0,0,8,11.84H10ZM8,11.15a.85.85,0,0,0,.62-.26.84.84,0,0,0,.26-.62.88.88,0,1,0-.88.88ZM7.35,8.59H8.61a1.83,1.83,0,0,1,.15-.81,4.54,4.54,0,0,1,.73-.87,2.87,2.87,0,0,0,.6-.81,2,2,0,0,0,.2-.87A1.77,1.77,0,0,0,9.6,3.7,2.64,2.64,0,0,0,8,3.21a2.23,2.23,0,0,0-1.49.51,2.67,2.67,0,0,0-.84,1.21l1.18.45a1.89,1.89,0,0,1,.37-.65A.93.93,0,0,1,8,4.39a1,1,0,0,1,.74.27A.85.85,0,0,1,9,5.27a1,1,0,0,1-.14.55,2.67,2.67,0,0,1-.51.56,3.8,3.8,0,0,0-.84,1A3.53,3.53,0,0,0,7.35,8.59Z" style="fill:#e0e0e0"/></svg>
\ No newline at end of file
diff --git a/addons/ggs/assets/feedback.svg.import b/addons/ggs/assets/feedback.svg.import
new file mode 100644 (file)
index 0000000..a9c3446
--- /dev/null
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c5a5taq8d2n0v"
+path="res://.godot/imported/feedback.svg-ef17e3f0b77f89dfe039a2ae2bdb59db.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ggs/assets/feedback.svg"
+dest_files=["res://.godot/imported/feedback.svg-ef17e3f0b77f89dfe039a2ae2bdb59db.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/addons/ggs/assets/file_dialog.svg b/addons/ggs/assets/file_dialog.svg
new file mode 100644 (file)
index 0000000..c1e5479
--- /dev/null
@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m3 1c-1.1046 0-2 .8954-2 2v1h14v-1c0-1.1046-.89543-2-2-2zm9 1h1v1h-1zm-11 3v8c0 1.1046.89543 2 2 2h10c1.1046 0 2-.8954 2-2v-8zm3 2h3c1 0 1 2 2 2h3v4h-8z" fill="#e0e0e0"/></svg>
diff --git a/addons/ggs/assets/file_dialog.svg.import b/addons/ggs/assets/file_dialog.svg.import
new file mode 100644 (file)
index 0000000..fa3282b
--- /dev/null
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://badl61ealw70o"
+path="res://.godot/imported/file_dialog.svg-9fb726e239e3a68babea68c94f0fb435.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ggs/assets/file_dialog.svg"
+dest_files=["res://.godot/imported/file_dialog.svg-9fb726e239e3a68babea68c94f0fb435.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/addons/ggs/assets/icon_mini.svg b/addons/ggs/assets/icon_mini.svg
new file mode 100644 (file)
index 0000000..2f41deb
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g id="a0baddc9-6785-4b43-8df7-7f50d5cadd4e" data-name="Layer 3"><path d="M15.33,5.58H13.56c-.1-.22-.2-.44-.32-.65a7.15,7.15,0,0,0-.4-.61l.88-1.56a.68.68,0,0,0-.24-.93l-3-1.74A.67.67,0,0,0,9.6.34L8.72,1.9a5.78,5.78,0,0,0-1.44,0L6.4.34A.67.67,0,0,0,5.48.09l-3,1.74a.68.68,0,0,0-.24.93l.88,1.56a6.31,6.31,0,0,0-.72,1.26H.67A.68.68,0,0,0,0,6.26V9.74a.68.68,0,0,0,.67.68H2.44c.1.22.2.44.32.65a7.15,7.15,0,0,0,.4.61l-.88,1.56a.68.68,0,0,0,.24.93l3,1.74a.67.67,0,0,0,.92-.25l.88-1.56a5.78,5.78,0,0,0,1.44,0l.88,1.56a.67.67,0,0,0,.92.25l3-1.74a.68.68,0,0,0,.24-.93l-.88-1.56a6.31,6.31,0,0,0,.72-1.26h1.77A.68.68,0,0,0,16,9.74V6.26A.68.68,0,0,0,15.33,5.58Z" style="fill:#ff9166"/><path d="M0,8.05V6.31a.68.68,0,0,1,.67-.68H2.44a6,6,0,0,1,.72-1.26L2.28,2.81a.68.68,0,0,1,.24-.93l3-1.74A.67.67,0,0,1,6.4.39L7.28,2A5.78,5.78,0,0,1,8.72,2L9.6.39a.67.67,0,0,1,.92-.25l3,1.74a.68.68,0,0,1,.24.93l-.88,1.56a7.15,7.15,0,0,1,.4.61c.12.21.22.43.32.65h1.77a.68.68,0,0,1,.67.68V8.05Z" style="fill:#ffb570"/><ellipse cx="7.96" cy="8" rx="3.86" ry="3.92" style="fill:#fff"/><rect x="1.68" y="6.88" width="12.58" height="2.23" rx="1.11" style="fill:#fff"/><path d="M8,8,6.29,6.3a2.46,2.46,0,0,0,0,3.4,2.36,2.36,0,0,0,3.35,0,2.46,2.46,0,0,0,0-3.38Z" style="fill:#ff9166"/></g></svg>
\ No newline at end of file
diff --git a/addons/ggs/assets/icon_mini.svg.import b/addons/ggs/assets/icon_mini.svg.import
new file mode 100644 (file)
index 0000000..635fad1
--- /dev/null
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bk0u7p6a1apta"
+path="res://.godot/imported/icon_mini.svg-5e00867268e00b8e3e5eaa22b6fe9735.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ggs/assets/icon_mini.svg"
+dest_files=["res://.godot/imported/icon_mini.svg-5e00867268e00b8e3e5eaa22b6fe9735.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/addons/ggs/assets/icon_mono.svg b/addons/ggs/assets/icon_mono.svg
new file mode 100644 (file)
index 0000000..5abf9a7
--- /dev/null
@@ -0,0 +1 @@
+<svg id="a07613d8-a96f-4fff-9a55-e858b51cec5a" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M16,6.14a.69.69,0,0,0-.66-.54H13.56a6.12,6.12,0,0,0-.72-1.27h0l.88-1.56a.8.8,0,0,0,.09-.3h0a.71.71,0,0,0-.33-.53l-3-1.75A.67.67,0,0,0,9.6.34L8.72,2A5.78,5.78,0,0,0,7.28,2L6.4.34A.66.66,0,0,0,5.49.09h0l-3,1.75a.71.71,0,0,0-.33.53h0a.8.8,0,0,0,.09.3l.88,1.56h0A6.12,6.12,0,0,0,2.4,5.5H.67A.69.69,0,0,0,0,6.14V9.72a.68.68,0,0,0,.67.68H2.44c.1.22.21.44.33.65a6.49,6.49,0,0,0,.39.62l-.88,1.56a.68.68,0,0,0,.24.93l3,1.75a.67.67,0,0,0,.92-.24h0l.88-1.57a5.78,5.78,0,0,0,1.44,0l.88,1.57a.66.66,0,0,0,.91.25h0l3-1.75a.68.68,0,0,0,.24-.93l-.88-1.56a6.12,6.12,0,0,0,.72-1.27h1.77A.68.68,0,0,0,16,9.72V6.14Zm-2.84,3h-1.5a3.84,3.84,0,0,1-7.4,0H2.78A1.12,1.12,0,0,1,1.68,8V8a1.12,1.12,0,0,1,1.1-1.12H4.26a3.84,3.84,0,0,1,7.4,0h1.5A1.12,1.12,0,0,1,14.26,8h0V8A1.12,1.12,0,0,1,13.16,9.1Z" style="fill:#e0e0e0"/><path d="M10.33,8a2.53,2.53,0,0,1-.27,1.07,2.39,2.39,0,0,1-.42.58,2.36,2.36,0,0,1-3.34,0h0a2.39,2.39,0,0,1-.42-.58A2.49,2.49,0,0,1,5.6,8a2.42,2.42,0,0,1,.69-1.76l.57.58L8,8,9.1,6.85l.56-.56A2.48,2.48,0,0,1,10.33,8Z" style="fill:#e0e0e0"/></svg>
\ No newline at end of file
diff --git a/addons/ggs/assets/icon_mono.svg.import b/addons/ggs/assets/icon_mono.svg.import
new file mode 100644 (file)
index 0000000..106b645
--- /dev/null
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b8o243gwa707v"
+path="res://.godot/imported/icon_mono.svg-44d29ea722636fe5e654f2982a3e46a7.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ggs/assets/icon_mono.svg"
+dest_files=["res://.godot/imported/icon_mono.svg-44d29ea722636fe5e654f2982a3e46a7.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/addons/ggs/assets/reload.svg b/addons/ggs/assets/reload.svg
new file mode 100644 (file)
index 0000000..1200df1
--- /dev/null
@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><g fill="#e0e0e0" fill-opacity=".99608" transform="translate(0 -1036.4)"><path d="m9 2a6 6 0 0 0 -6 6h2a4 4 0 0 1 4-4 4 4 0 0 1 4 4 4 4 0 0 1 -4 4v2a6 6 0 0 0 6-6 6 6 0 0 0 -6-6z" transform="translate(0 1036.4)"/><path d="m4.118 1048.3-1.6771-.9683-1.6771-.9682 1.6771-.9683 1.6771-.9682-.0000001 1.9365z" transform="matrix(0 -1.1926 1.5492 0 -1617 1049.3)"/></g></svg>
diff --git a/addons/ggs/assets/reload.svg.import b/addons/ggs/assets/reload.svg.import
new file mode 100644 (file)
index 0000000..a9e84e5
--- /dev/null
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ve54bl3r7ljc"
+path="res://.godot/imported/reload.svg-e5d4e094fabdbb8dce0284d7c068bbf7.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ggs/assets/reload.svg"
+dest_files=["res://.godot/imported/reload.svg-e5d4e094fabdbb8dce0284d7c068bbf7.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/addons/ggs/assets/rename.svg b/addons/ggs/assets/rename.svg
new file mode 100644 (file)
index 0000000..bd6dad2
--- /dev/null
@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M5 2v2h2v8H5v2h2a1 1 0 0 0 1-1 1 1 0 0 0 1 1h2v-2H9V4h2V2H9a1 1 0 0 0-1 1 1 1 0 0 0-1-1z" fill="#e0e0e0"/></svg>
diff --git a/addons/ggs/assets/rename.svg.import b/addons/ggs/assets/rename.svg.import
new file mode 100644 (file)
index 0000000..8bc50d7
--- /dev/null
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c2t5bnnw5wntg"
+path="res://.godot/imported/rename.svg-6a8295895275642ac274032accd0e496.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ggs/assets/rename.svg"
+dest_files=["res://.godot/imported/rename.svg-6a8295895275642ac274032accd0e496.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/addons/ggs/assets/save_file.svg b/addons/ggs/assets/save_file.svg
new file mode 100644 (file)
index 0000000..d3c01ca
--- /dev/null
@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m2 1v14h12v-9h-5v-5zm8 0v4h4z" fill="#e0e0e0" transform="translate(0 -.000017)"/></svg>
diff --git a/addons/ggs/assets/save_file.svg.import b/addons/ggs/assets/save_file.svg.import
new file mode 100644 (file)
index 0000000..d25db38
--- /dev/null
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bx8yoim3ur6h"
+path="res://.godot/imported/save_file.svg-c3285e886b3ea7d7c8d043d4932fe121.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ggs/assets/save_file.svg"
+dest_files=["res://.godot/imported/save_file.svg-c3285e886b3ea7d7c8d043d4932fe121.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/addons/ggs/assets/search.svg b/addons/ggs/assets/search.svg
new file mode 100644 (file)
index 0000000..fff4a3c
--- /dev/null
@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m6 1a5 5 0 0 0 -5 5 5 5 0 0 0 5 5 5 5 0 0 0 2.7539-.83203l4.3164 4.3164 1.4141-1.4141-4.3164-4.3164a5 5 0 0 0 .83203-2.7539 5 5 0 0 0 -5-5zm0 2a3 3 0 0 1 3 3 3 3 0 0 1 -3 3 3 3 0 0 1 -3-3 3 3 0 0 1 3-3z" fill="#e0e0e0" fill-opacity=".99608"/></svg>
diff --git a/addons/ggs/assets/search.svg.import b/addons/ggs/assets/search.svg.import
new file mode 100644 (file)
index 0000000..2605b1a
--- /dev/null
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dbervsl0o0ifw"
+path="res://.godot/imported/search.svg-49c012ff581540a89f79d42c8fa61d3e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ggs/assets/search.svg"
+dest_files=["res://.godot/imported/search.svg-49c012ff581540a89f79d42c8fa61d3e.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/addons/ggs/assets/show_in_filesystem.svg b/addons/ggs/assets/show_in_filesystem.svg
new file mode 100644 (file)
index 0000000..a5e1c2f
--- /dev/null
@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m1 1v5h2v8h1 5v1h6v-3h-6v1h-5v-4h5v1h6v-3h-6v1h-5v-2h3v-4h-2l-1-1z" fill="#e0e0e0"/></svg>
diff --git a/addons/ggs/assets/show_in_filesystem.svg.import b/addons/ggs/assets/show_in_filesystem.svg.import
new file mode 100644 (file)
index 0000000..661085c
--- /dev/null
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://buvhmwckm6s2e"
+path="res://.godot/imported/show_in_filesystem.svg-d8323cbcf62adb9529b12d5317dfd9e0.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ggs/assets/show_in_filesystem.svg"
+dest_files=["res://.godot/imported/show_in_filesystem.svg-d8323cbcf62adb9529b12d5317dfd9e0.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/addons/ggs/assets/theme.svg b/addons/ggs/assets/theme.svg
new file mode 100644 (file)
index 0000000..c684aed
--- /dev/null
@@ -0,0 +1 @@
+<svg id="b9f14ada-6f72-47cb-b926-be5120fe53a0" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16"><defs><linearGradient id="a317b568-114b-452b-9828-f8e7ab53e5fa" x1="8" x2="8" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0.19" stop-color="#ff4545"/><stop offset="0.19" stop-color="#ffe345"/><stop offset="0.31" stop-color="#ffe345"/><stop offset="0.31" stop-color="#80ff45"/><stop offset="0.44" stop-color="#80ff45"/><stop offset="0.44" stop-color="#45ffa2"/><stop offset="0.56" stop-color="#45ffa2"/><stop offset="0.56" stop-color="#45d7ff"/><stop offset="0.69" stop-color="#45d7ff"/><stop offset="0.69" stop-color="#8045ff"/><stop offset="0.81" stop-color="#8045ff"/><stop offset="0.81" stop-color="#ff4596"/></linearGradient></defs><path d="M12.84,8A14.83,14.83,0,0,0,10.5,4.5,28.7,28.7,0,0,1,8,1,29.82,29.82,0,0,1,5.5,4.5,14.9,14.9,0,0,0,3.15,8a4.51,4.51,0,0,0-.4,1.75,5.17,5.17,0,0,0,.32,1.75,5.22,5.22,0,0,0,9.85,0,5.18,5.18,0,0,0,.33-1.75A4.52,4.52,0,0,0,12.84,8ZM8.26,13.31V12A2.66,2.66,0,1,0,5.6,9.33H6.93L4.93,12l-2-2.66H4.27a4,4,0,1,1,4,4Z" style="fill:url(#a317b568-114b-452b-9828-f8e7ab53e5fa)"/></svg>
\ No newline at end of file
diff --git a/addons/ggs/assets/theme.svg.import b/addons/ggs/assets/theme.svg.import
new file mode 100644 (file)
index 0000000..8b580bf
--- /dev/null
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cltge6ak5rw2e"
+path="res://.godot/imported/theme.svg-e66649e0a1572635d9bbe372771a00be.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ggs/assets/theme.svg"
+dest_files=["res://.godot/imported/theme.svg-e66649e0a1572635d9bbe372771a00be.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/addons/ggs/assets/uncheck_all.svg b/addons/ggs/assets/uncheck_all.svg
new file mode 100644 (file)
index 0000000..9809dad
--- /dev/null
@@ -0,0 +1 @@
+<svg id="b7c75bcc-3a8b-4650-9ac3-a733ac21467a" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M11.45,9.84a.49.49,0,0,1,0,.69l-.93.92a.49.49,0,0,1-.69,0L8,9.62,6.16,11.45a.49.49,0,0,1-.69,0l-.92-.92a.49.49,0,0,1,0-.69L6.38,8,4.55,6.17a.49.49,0,0,1,0-.69l.92-.93a.49.49,0,0,1,.69,0L8,6.39,9.83,4.55a.49.49,0,0,1,.69,0l.93.93a.49.49,0,0,1,0,.69L9.61,8Z" style="fill:#e0e0e0"/><path d="M13.71,2.67V13.33H2.29V2.67H13.71M14.56,0H1.44A1.55,1.55,0,0,0,0,1.65V14.32A1.58,1.58,0,0,0,1.42,16H14.56A1.55,1.55,0,0,0,16,14.35V1.68A1.58,1.58,0,0,0,14.58,0Z" style="fill:#e0e0e0"/></svg>
\ No newline at end of file
diff --git a/addons/ggs/assets/uncheck_all.svg.import b/addons/ggs/assets/uncheck_all.svg.import
new file mode 100644 (file)
index 0000000..5de9cb8
--- /dev/null
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://romr61n4g5y5"
+path="res://.godot/imported/uncheck_all.svg-abca75613ca5bd818fad7ad47e045d69.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ggs/assets/uncheck_all.svg"
+dest_files=["res://.godot/imported/uncheck_all.svg-abca75613ca5bd818fad7ad47e045d69.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/addons/ggs/classes/ggs_input_helper.gd b/addons/ggs/classes/ggs_input_helper.gd
new file mode 100644 (file)
index 0000000..1240ead
--- /dev/null
@@ -0,0 +1,241 @@
+@tool
+extends RefCounted
+class_name ggsInputHelper
+
+enum InputType {INVALID, KEYBOARD, MOUSE, GP_BTN, GP_MOTION}
+
+var joy_name_abbr: Dictionary = {
+       "XInput Gamepad": "xbox",
+       "Xbox Series Controller": "xbox",
+       "Sony DualSense": "ps",
+       "PS5 Controller": "ps",
+       "PS4 Controller": "ps",
+       "Switch": "switch",
+} 
+
+var labels: Dictionary = {
+       "mouse": [
+               "LMB", "RMB", "MMB", "MW Up", "MW Down", "MW Left", "MW Right", "MB1", "MB2"
+       ],
+       
+       "xbox": [
+               "A", "B", "X", "Y", "Back", "Home", "Start", "L", "R", "LB", "RB",
+               "DPad Up", "DPad Down", "DPad Left", "DPad Right", "Share"
+       ],
+       
+       "ps": [
+               "Cross", "Circle", "Square", "Triangle", "Select", "PS", "Start",
+               "L3", "R3", "L1", "R1", "DPad Up", "DPad Down", "DPad Left",
+               "DPad Right", "Microphone"
+       ],
+       
+       "switch": [
+               "B", "A", "Y", "X", "Minus", "", "Plus", "", "", "", "",
+               "DPad Up", "DPad Down", "DPad Left", "DPad Right", "Capture"
+       ],
+       
+       "other": [
+               "A", "B", "X", "Y", "Back", "Home", "Start", "L", "R", "LB", "RB",
+               "DPad Up", "DPad Down", "DPad Left", "DPad Right", "Share",
+               "Paddle 1", "Paddle 2", "Paddle 3", "Paddle 4", "Touch"
+       ],
+       
+       "motion": [
+               {"-": "LStick Left", "+": "LStick Right"},
+               {"-": "LStick Up", "+": "LStick Down"},
+               {"-": "RStick Left", "+": "RStick Right"},
+               {"-": "RStick Up", "+": "RStick Down"},
+               {"+": "Left Trigger"},
+               {"+": "Right Trigger"}
+       ],
+}
+
+
+func get_event_id(event: InputEvent) -> int:
+       if event is InputEventKey:
+               if event.physical_keycode == 0:
+                       return -1
+               
+               return event.physical_keycode | event.get_modifiers_mask()
+       
+       if event is InputEventMouseButton:
+               return event.button_index | event.get_modifiers_mask()
+       
+       if event is InputEventJoypadButton:
+               return event.button_index
+       
+       if event is InputEventJoypadMotion:
+               return event.axis
+       
+       return -1
+
+
+func set_event_id(event: InputEvent, id: int) -> void:
+       if event is InputEventKey:
+               event.physical_keycode = id & ~(KEY_MASK_SHIFT | KEY_MASK_CTRL | KEY_MASK_ALT)
+               _set_event_modifiers(event, id)
+       
+       if event is InputEventMouseButton:
+               event.button_index = id & ~(KEY_MASK_SHIFT | KEY_MASK_CTRL | KEY_MASK_ALT)
+               _set_event_modifiers(event, id)
+       
+       if event is InputEventJoypadButton:
+               event.button_index = id
+       
+       if event is InputEventJoypadMotion:
+               event.axis = id
+
+
+func get_event_type(event: InputEvent) -> InputType:
+       if event is InputEventKey:
+               return InputType.KEYBOARD
+       
+       if event is InputEventMouseButton:
+               return InputType.MOUSE
+       
+       if event is InputEventJoypadButton:
+               return InputType.GP_BTN
+       
+       if event is InputEventJoypadMotion:
+               return InputType.GP_MOTION
+       
+       return InputType.INVALID
+
+
+func create_event_from_type(type: InputType) -> InputEvent:
+       match type:
+               InputType.KEYBOARD:
+                       return InputEventKey.new()
+               InputType.MOUSE:
+                       return InputEventMouseButton.new()
+               InputType.GP_BTN:
+                       return InputEventJoypadButton.new()
+               InputType.GP_MOTION:
+                       return InputEventJoypadMotion.new()
+               _:
+                       return null
+
+
+func input_already_exists(event: InputEvent, self_action: String) -> Array:
+       for action in InputMap.get_actions():
+               if action.begins_with("ui_"):
+                       continue
+               
+               if action == self_action:
+                       continue
+               
+               if InputMap.action_has_event(action, event):
+                       return [true, action]
+       
+       return [false, ""]
+
+func _set_event_modifiers(event: InputEventWithModifiers, modifier_mask: int) -> void:
+       event.shift_pressed = bool(modifier_mask & KEY_MASK_SHIFT)
+       event.ctrl_pressed = bool(modifier_mask & KEY_MASK_CTRL)
+       event.alt_pressed = bool(modifier_mask & KEY_MASK_ALT)
+
+
+### Events as Text
+
+func get_event_as_text(event: InputEvent) -> String:
+       if get_event_id(event) == -1:
+               return "INVALID"
+       
+       if event is InputEventKey:
+               return OS.get_keycode_string(event.get_physical_keycode_with_modifiers())
+       
+       if event is InputEventMouseButton:
+               return _get_mouse_event_as_text(event)
+       
+       if event is InputEventJoypadButton:
+               return _get_gp_btn_event_as_text(event)
+       
+       if event is InputEventJoypadMotion:
+               return _get_gp_motion_event_as_text(event)
+       
+       return ""
+
+
+func _get_modifiers_as_string(event: InputEventWithModifiers) -> String:
+       var modifiers: PackedStringArray
+       if event.shift_pressed:
+               modifiers.append("Shift")
+       
+       if event.ctrl_pressed:
+               modifiers.append("Ctrl")
+       
+       if event.alt_pressed:
+               modifiers.append("Alt")
+       
+       var modifiers_string: String = "+".join(modifiers) 
+       return modifiers_string
+
+
+func _get_joy_name_abbr(name: String) -> String:
+       if joy_name_abbr.has(name):
+               return joy_name_abbr[name]
+       else:
+               return "other"
+
+
+func _get_mouse_event_as_text(event: InputEventMouseButton) -> String:
+       var modifiers: String = _get_modifiers_as_string(event)
+       var btn: String = labels["mouse"][event.button_index - 1]
+       var result: String = "%s"%btn if modifiers.is_empty() else "%s+%s"%[modifiers, btn]
+       return result
+
+
+func _get_gp_btn_event_as_text(event: InputEventJoypadButton) -> String:
+       var device_name: String = Input.get_joy_name(event.device)
+       device_name = _get_joy_name_abbr(device_name)
+       return labels[device_name][event.button_index]
+
+
+func _get_gp_motion_event_as_text(event: InputEventJoypadMotion) -> String:
+       var axis_value: String = "-" if event.axis_value < 0 else "+"
+       return labels["motion"][event.axis][axis_value]
+
+
+### Events as Icons
+
+func get_event_as_icon(event: InputEvent, icon_db: ggsIconDB) -> Texture2D:
+       if event is InputEventMouseButton:
+               return _get_mouse_event_as_icon(event, icon_db)
+       
+       if event is InputEventJoypadButton:
+               return _get_gp_btn_event_as_icon(event, icon_db)
+       
+       if event is InputEventJoypadMotion:
+               return _get_gp_motion_event_as_icon(event, icon_db)
+       
+       return null
+
+
+func _get_mouse_event_as_icon(event: InputEventMouse, icon_db: ggsIconDB) -> Texture2D:
+       var button_index: int = event.button_index
+       var icon: Texture2D = icon_db.get_mouse_button_texture(button_index)
+       
+       return icon
+
+
+func _get_gp_btn_event_as_icon(event: InputEventJoypadButton, icon_db: ggsIconDB) -> Texture2D:
+       var device_name: String = Input.get_joy_name(event.device)
+       device_name = _get_joy_name_abbr(device_name)
+       
+       var button_index: int = event.button_index
+       var icon: Texture2D = icon_db.get_gp_button_texture(device_name, button_index)
+       
+       return icon
+
+
+func _get_gp_motion_event_as_icon(event: InputEventJoypadMotion, icon_db: ggsIconDB) -> Texture2D:
+       var device_name: String = Input.get_joy_name(event.device)
+       device_name = _get_joy_name_abbr(device_name)
+       
+       var axis: int = event.axis
+       var axis_dir: String = "-" if event.axis_value < 1 else "+"
+       var icon: Texture2D = icon_db.get_gp_motion_texture(device_name, axis, axis_dir)
+       
+       return icon
+
diff --git a/addons/ggs/classes/ggs_inspector_plugin.gd b/addons/ggs/classes/ggs_inspector_plugin.gd
new file mode 100644 (file)
index 0000000..312b591
--- /dev/null
@@ -0,0 +1,18 @@
+@tool
+extends EditorInspectorPlugin
+class_name ggsInspectorPlugin
+
+var input_selector_scn: PackedScene = preload("res://addons/ggs/editor/input_selector/input_selector.tscn")
+
+
+func _can_handle(object: Object) -> bool:
+       return object is ggsInputSetting
+
+
+func _parse_category(object: Object, category: String) -> void:
+       if category != "input.gd":
+               return
+       
+       var InputSelector: Control = input_selector_scn.instantiate()
+       InputSelector.inspected_obj = object as ggsInputSetting
+       add_custom_control(InputSelector)
diff --git a/addons/ggs/classes/ggs_save_file.gd b/addons/ggs/classes/ggs_save_file.gd
new file mode 100644 (file)
index 0000000..4db6430
--- /dev/null
@@ -0,0 +1,17 @@
+@tool
+extends ConfigFile
+class_name ggsSaveFile
+
+var path: String = ggsUtils.get_plugin_data().dir_save_file
+
+
+func _init() -> void:
+       if not FileAccess.file_exists(path):
+               save(path)
+       
+       self.load(path)
+
+
+func set_key(section: String, key: String, value: Variant) -> void:
+       set_value(section, key, value)
+       save(path)
diff --git a/addons/ggs/classes/ggs_ui_component.gd b/addons/ggs/classes/ggs_ui_component.gd
new file mode 100644 (file)
index 0000000..7c84d05
--- /dev/null
@@ -0,0 +1,66 @@
+@tool
+extends MarginContainer
+class_name ggsUIComponent
+
+const WARNING_NO_SETTING: String = "No setting is assigned."
+const WARNING_DELETED_SETTING: String = "The assigned setting was deleted or is invalid."
+const WARNING_SETTING_NOT_IN_DIR: String = "The assigned setting is not in the settings directory."
+const WARNING_INCOMPATIBLE_SETTING: String = "The value type of the assigned setting is not compatible with this component."
+
+@export_category("GGS UI Component")
+@export var setting: ggsSetting: set = set_setting
+@export var apply_on_change: bool
+@export var grab_focus_on_mouse_over: bool
+
+var setting_value: Variant
+var compatible_types: Array[Variant.Type] = []
+
+
+func _ready() -> void:
+       init_value()
+
+
+func _get_configuration_warnings() -> PackedStringArray:
+       if setting == null:
+               return PackedStringArray([WARNING_NO_SETTING])
+       
+       if setting.resource_path.is_empty():
+               return PackedStringArray([WARNING_DELETED_SETTING])
+       
+       if not setting.resource_path.begins_with(ggsUtils.get_plugin_data().dir_settings):
+               return PackedStringArray([WARNING_SETTING_NOT_IN_DIR])
+       
+       if (
+               not compatible_types.is_empty() and
+               not compatible_types.has(setting.value_type)
+       ):
+               return PackedStringArray([WARNING_INCOMPATIBLE_SETTING])
+       
+       
+       return PackedStringArray()
+
+
+func set_setting(value: ggsSetting) -> void:
+       setting = value
+       update_configuration_warnings()
+
+
+func init_value() -> void:
+       if setting != null:
+               setting_value = setting.current
+
+
+func apply_setting() -> void:
+       setting.current = setting_value #!1
+       setting.apply(setting_value)
+
+
+func reset_setting() -> void:
+       setting_value = setting.default
+       apply_setting()
+
+
+#1 Note that during runtime, the setting resource itself is not actually changed
+ # since all resources are read-only during runtime. However, the setter
+ # function (set_setting) of the resource is still executed.
+ # View ggs_setting.gd/set_setting() for more info.
diff --git a/addons/ggs/classes/ggs_utils.gd b/addons/ggs/classes/ggs_utils.gd
new file mode 100644 (file)
index 0000000..7e24a07
--- /dev/null
@@ -0,0 +1,83 @@
+@tool
+extends RefCounted
+class_name ggsUtils
+
+#!1
+static func get_editor_interface():
+       return Engine.get_singleton("ggsEI")
+
+
+static func get_resource_file_system():
+       return get_editor_interface().get_resource_filesystem()
+
+
+static func get_file_system_dock():
+       return get_editor_interface().get_file_system_dock()
+
+
+static func get_plugin_data() -> ggsPluginData:
+       return load("res://addons/ggs/plugin_data.tres")
+
+
+static func get_enum_string(target_enum: String) -> String:
+       var types: PackedStringArray = [
+               "Nil","Bool","Int","Float","String","Vector2","Vector2i","Rect2",
+               "Rect2i","Vector3","Vector3i","Transform2D","Vector4","Vector4i","Plane",
+               "Quaternion","AABB","Basis","Transform3D","Projection","Color",
+               "StringName","NodePath","RID","Object","Callable","Signal","Dictionary",
+               "Array","PackedByteArray","PackedInt32Array","PackedInt64Array",
+               "PackedFloat32Array","PackedFloat64Array","PackedStringArray",
+               "PackedVector2Array","PackedVector3Array","PackedColorArray"
+       ]
+       
+       var property_hints: PackedStringArray = [
+               "None","Range","Enum","Enum Suggestion","Exp Easing","Link","Flags",
+               "Layers 2D Render","Layers 2D Physics","Layers 2D Navigation",
+               "Layers 3D Render","Layers 3D Physics","Layers 3D Navigation",
+               "File","Dir","Global File","Global Dir","Resource Type","Multiline Text",
+               "Expression","Placeholder Text","Color No Alpha","Object ID","Type String",
+               "Node Path To Edited Node","Object Too Big","Node Path Valid Types",
+               "Save File","Global Save File","Int is Object ID","Int is Pointer",
+               "Array Type","Locale ID","Localizable String","Node Type",
+               "Hide Quaternion Edit","Password"
+       ]
+       
+       var enum_string: String
+       match target_enum:
+               "Variant.Type":
+                       enum_string = ",".join(types)
+               "PropertyHint":
+                       enum_string = ",".join(property_hints)
+       
+       return enum_string
+
+
+### Dir Paths
+
+static func path_is_in_dir_settings(path: String) -> bool:
+       var dir_settings: String = ggsUtils.get_plugin_data().dir_settings
+       return path.begins_with(dir_settings)
+
+
+### Window
+
+static func window_clamp_to_screen(size: Vector2) -> Vector2:
+       var screen_size: Rect2i = DisplayServer.screen_get_usable_rect()
+       size.x = min(size.x, screen_size.size.x)
+       size.y = min(size.y, screen_size.size.y)
+       
+       return size
+
+
+static func center_window() -> void:
+       var screen_id: int = DisplayServer.window_get_current_screen()
+       var display_size: Vector2i = DisplayServer.screen_get_size(screen_id)
+       var window_size: Vector2i = DisplayServer.window_get_size()
+       var origin: Vector2i = DisplayServer.screen_get_position(screen_id)
+       var target_pos: Vector2 = origin + (display_size / 2) - (window_size / 2)
+       DisplayServer.window_set_position(target_pos)
+
+
+### Comments
+# !1: Specifying return types for the editor interface methods causes
+# issues when the game is exported.
diff --git a/addons/ggs/classes/global/ggs.gd b/addons/ggs/classes/global/ggs.gd
new file mode 100644 (file)
index 0000000..536e293
--- /dev/null
@@ -0,0 +1,229 @@
+@tool
+extends Node
+
+enum Progress {SAVE_FILE_CURRENT, SAVE_FILE_DEFAULT, ADD_SETTINGS}
+enum SFX {MOUSE_OVER, FOCUS, INTERACT}
+
+signal active_category_changed()
+signal active_setting_changed()
+signal dir_settings_change_occured()
+signal progress_started(type: Progress)
+signal progress_advanced(progress: float)
+signal progress_ended()
+
+var active_category: String: set = set_active_category
+var active_setting: ggsSetting: set = set_active_setting
+
+var thread_current: Thread = Thread.new()
+var thread_default: Thread = Thread.new()
+var semaphore_current: Semaphore = Semaphore.new()
+var semaphore_default: Semaphore = Semaphore.new()
+var terminate_current: bool = false
+var terminate_default: bool = false
+var settings_cache: Array[ggsSetting] #?1
+
+var FSD #!1
+@onready var MouseOverSFX: AudioStreamPlayer = $MouseOverSFX
+@onready var FocusSFX: AudioStreamPlayer = $FocusSFX
+@onready var InteractSFX: AudioStreamPlayer = $InteractSFX
+
+
+func _ready() -> void:
+       thread_current.start(_update_save_file)
+       
+       if Engine.is_editor_hint():
+               FSD = ggsUtils.get_file_system_dock()
+               FSD.files_moved.connect(_on_FSD_item_moved)
+               FSD.file_removed.connect(_on_FSD_item_removed)
+               FSD.folder_moved.connect(_on_FSD_item_moved)
+               FSD.folder_removed.connect(_on_FSD_item_removed)
+               
+               request_update_save_file()
+               thread_default.start(_update_save_file_default)
+       
+       if not Engine.is_editor_hint():
+               terminate_current = true
+               semaphore_current.post()
+               thread_current.wait_to_finish()
+               _apply_settings()
+
+
+func _exit_tree() -> void:
+       terminate_current = true
+       semaphore_current.post()
+       thread_current.wait_to_finish()
+       
+       if Engine.is_editor_hint() and thread_default.is_started():
+               terminate_default = true
+               semaphore_default.post()
+               thread_default.wait_to_finish()
+
+
+func set_active_category(value: String) -> void:
+       active_category = value
+       active_category_changed.emit()
+       active_setting = null
+
+
+func set_active_setting(value: ggsSetting) -> void:
+       active_setting = value
+       active_setting_changed.emit()
+
+
+func request_update_save_file() -> void:
+       semaphore_current.post()
+
+
+func request_update_save_file_default() -> void:
+       semaphore_default.post()
+
+
+func _update_save_file() -> void:
+       while (true):
+               semaphore_current.wait()
+               
+               call_thread_safe("emit_signal", "progress_started", Progress.SAVE_FILE_CURRENT)
+               
+               var save_file: ggsSaveFile = ggsSaveFile.new()
+               var fresh_save: ConfigFile = ConfigFile.new()
+               
+               var all_settings: PackedStringArray = get_all_settings()
+               var step: float = float(0)
+               var total: float = float(all_settings.size())
+               var progress: float
+               for setting_path in all_settings:
+                       var setting: ggsSetting = load(setting_path)
+                       
+                       if save_file.has_section_key(setting.category, setting.name):
+                               var value: Variant = save_file.get_value(setting.category, setting.name)
+                               fresh_save.set_value(setting.category, setting.name, value)
+                       else:
+                               fresh_save.set_value(setting.category, setting.name, setting.default)
+                       
+                       step += 1
+                       progress = (step / total) * 100
+                       call_thread_safe("emit_signal", "progress_advanced", progress)
+                       
+                       if not settings_cache.has(setting):
+                               settings_cache.append(setting)
+               
+               fresh_save.save(ggsUtils.get_plugin_data().dir_save_file)
+               call_thread_safe("emit_signal", "progress_ended")
+               
+               if terminate_current:
+                       break
+
+
+func _update_save_file_default() -> void:
+       while (true):
+               semaphore_default.wait()
+               
+               call_thread_safe("emit_signal", "progress_started", Progress.SAVE_FILE_DEFAULT)
+       
+               var save_file: ggsSaveFile = ggsSaveFile.new()
+               save_file.clear()
+               
+               var all_settings: PackedStringArray = get_all_settings()
+               var step: float = float(0)
+               var total: float = float(all_settings.size())
+               var progress: float
+               for setting_path in all_settings:
+                       var setting: ggsSetting = load(setting_path)
+                       save_file.set_value(setting.category, setting.name, setting.default)
+                       
+                       step += 1
+                       progress = (step / total) * 100
+                       call_thread_safe("emit_signal", "progress_advanced", progress)
+               
+               save_file.save(save_file.path)
+               call_thread_safe("emit_signal", "progress_ended")
+               
+               if terminate_default:
+                       break
+
+
+func _on_FSD_item_moved(old: String, new: String) -> void:
+       if ggsUtils.path_is_in_dir_settings(old) or ggsUtils.path_is_in_dir_settings(new):
+               dir_settings_change_occured.emit()
+
+
+func _on_FSD_item_removed(item: String) -> void:
+       if ggsUtils.path_is_in_dir_settings(item):
+               dir_settings_change_occured.emit()
+
+
+### Game Init
+
+func get_all_settings() -> PackedStringArray:
+       var all_settings: PackedStringArray
+       
+       var path: String = ggsUtils.get_plugin_data().dir_settings
+       var dir: DirAccess = DirAccess.open(path)
+       var categories: PackedStringArray = dir.get_directories()
+       for category in categories:
+               if category.begins_with("_"):
+                       continue
+               
+               dir.change_dir(path.path_join(category))
+               var settings: PackedStringArray = _get_settings_in_dir(dir)
+               all_settings.append_array(settings)
+               
+               var groups: PackedStringArray = dir.get_directories()
+               for group in groups:
+                       dir.change_dir(path.path_join(category).path_join(group))
+                       var subsettings: PackedStringArray = _get_settings_in_dir(dir)
+                       all_settings.append_array(subsettings)
+       
+       return all_settings
+
+
+func _get_settings_in_dir(dir: DirAccess) -> PackedStringArray:
+       var result: PackedStringArray
+       
+       var settings: PackedStringArray = dir.get_files()
+       for setting in settings:
+               if setting.ends_with(".gd"):
+                       continue
+               
+               result.append(dir.get_current_dir().path_join(setting))
+       
+       return result
+
+
+func _apply_settings() -> void:
+       var all_settings: PackedStringArray = get_all_settings()
+       for setting_path in all_settings:
+               var setting: ggsSetting = load(setting_path)
+               var value: Variant = ggsSaveFile.new().get_value(setting.category, setting.name)
+               setting.set_current(value)
+
+
+### SFX
+
+func play_sfx(sfx: SFX) -> void:
+       var target_player: AudioStreamPlayer
+       match sfx:
+               SFX.MOUSE_OVER:
+                       target_player = MouseOverSFX
+               SFX.FOCUS:
+                       target_player = FocusSFX
+               SFX.INTERACT:
+                       target_player = InteractSFX
+               _:
+                       printerr("GGS - Play SFX: The target SFX does not exist.")
+                       return
+       
+       if target_player.stream != null:
+               target_player.play()
+
+
+### Comments
+# ?1: The variable `settings_cache` itself is not actually being used in
+# the code. It's there to keep the references to the loaded settings
+# alive.
+# Godot caches loaded resources of the tree under the hood.
+# View `youtube.com/watch?v=3r7IohvVnw8` for more info.
+
+# !1: Specifying return types for the editor interface methods causes
+# issues when the game is exported.
diff --git a/addons/ggs/classes/global/ggs.tscn b/addons/ggs/classes/global/ggs.tscn
new file mode 100644 (file)
index 0000000..88b3bb8
--- /dev/null
@@ -0,0 +1,12 @@
+[gd_scene load_steps=2 format=3 uid="uid://esw7j7or7gpd"]
+
+[ext_resource type="Script" path="res://addons/ggs/classes/global/ggs.gd" id="1_6v3cu"]
+
+[node name="GGS" type="Node"]
+script = ExtResource("1_6v3cu")
+
+[node name="MouseOverSFX" type="AudioStreamPlayer" parent="."]
+
+[node name="FocusSFX" type="AudioStreamPlayer" parent="."]
+
+[node name="InteractSFX" type="AudioStreamPlayer" parent="."]
diff --git a/addons/ggs/classes/resources/ggs_icon_db.gd b/addons/ggs/classes/resources/ggs_icon_db.gd
new file mode 100644 (file)
index 0000000..0d6b306
--- /dev/null
@@ -0,0 +1,193 @@
+@tool
+extends Resource
+class_name ggsIconDB
+
+var property_map: Dictionary = {
+       "mouse_button": [
+               "lmb", "rmb", "mmb", "mw_up", "mw_down", "mw_left", "mw_right", "mb1", "mb2"
+       ],
+       
+       "gp_button": [
+               "bot", "right", "left", "top",
+               "back", "guide", "start",
+               "left_stick", "right_stick",
+               "left_shoulder", "right_shoulder",
+               "dup", "ddown", "dleft", "dright",
+               "misc", "pad1", "pad2", "pad3", "pad4",
+               "touch"
+       ],
+       
+       "gp_motion": [
+               {"-": "ls_left", "+": "ls_right"},
+               {"-": "ls_down", "+": "ls_up"},
+               {"-": "rs_left", "+": "rs_right"},
+               {"-": "rs_down", "+": "rs_up"},
+               {"+": "left_trigger"},
+               {"+": "right_trigger"},
+       ],
+}
+
+
+@export_category("Icon Database")
+@export_group("Mouse", "mouse_")
+@export var mouse_lmb: Texture2D
+@export var mouse_rmb: Texture2D
+@export var mouse_mmb: Texture2D
+@export var mouse_mw_up: Texture2D
+@export var mouse_mw_down: Texture2D
+@export var mouse_mw_left: Texture2D
+@export var mouse_mw_right: Texture2D
+@export var mouse_mb1: Texture2D
+@export var mouse_mb2: Texture2D
+
+@export_group("XBox", "xbox_")
+@export_subgroup("XBox Motions", "xbox_")
+@export var xbox_ls_left: Texture2D
+@export var xbox_ls_right: Texture2D
+@export var xbox_ls_up: Texture2D
+@export var xbox_ls_down: Texture2D
+@export var xbox_rs_left: Texture2D
+@export var xbox_rs_right: Texture2D
+@export var xbox_rs_up: Texture2D
+@export var xbox_rs_down: Texture2D
+@export var xbox_left_trigger: Texture2D
+@export var xbox_right_trigger: Texture2D
+@export_subgroup("XBox Buttons", "xbox_")
+@export var xbox_bot: Texture2D
+@export var xbox_right: Texture2D
+@export var xbox_left: Texture2D
+@export var xbox_top: Texture2D
+@export var xbox_back: Texture2D
+@export var xbox_guide: Texture2D
+@export var xbox_start: Texture2D
+@export var xbox_left_stick: Texture2D
+@export var xbox_right_stick: Texture2D
+@export var xbox_left_shoulder: Texture2D
+@export var xbox_right_shoulder: Texture2D
+@export var xbox_dup: Texture2D
+@export var xbox_ddown: Texture2D
+@export var xbox_dleft: Texture2D
+@export var xbox_dright: Texture2D
+@export var xbox_misc: Texture2D
+@export var xbox_pad1: Texture2D
+@export var xbox_pad2: Texture2D
+@export var xbox_pad3: Texture2D
+@export var xbox_pad4: Texture2D
+@export var xbox_touch: Texture2D
+
+@export_group("Playstation", "ps_")
+@export_subgroup("PS Motions", "ps_")
+@export var ps_ls_left: Texture2D
+@export var ps_ls_right: Texture2D
+@export var ps_ls_up: Texture2D
+@export var ps_ls_down: Texture2D
+@export var ps_rs_left: Texture2D
+@export var ps_rs_right: Texture2D
+@export var ps_rs_up: Texture2D
+@export var ps_rs_down: Texture2D
+@export var ps_left_trigger: Texture2D
+@export var ps_right_trigger: Texture2D
+@export_subgroup("PS Buttons", "ps_")
+@export var ps_bot: Texture2D
+@export var ps_right: Texture2D
+@export var ps_left: Texture2D
+@export var ps_top: Texture2D
+@export var ps_back: Texture2D
+@export var ps_guide: Texture2D
+@export var ps_start: Texture2D
+@export var ps_left_stick: Texture2D
+@export var ps_right_stick: Texture2D
+@export var ps_left_shoulder: Texture2D
+@export var ps_right_shoulder: Texture2D
+@export var ps_dup: Texture2D
+@export var ps_ddown: Texture2D
+@export var ps_dleft: Texture2D
+@export var ps_dright: Texture2D
+@export var ps_misc: Texture2D
+@export var ps_pad1: Texture2D
+@export var ps_pad2: Texture2D
+@export var ps_pad3: Texture2D
+@export var ps_pad4: Texture2D
+@export var ps_touch: Texture2D
+
+@export_group("Switch", "switch_")
+@export_subgroup("Switch Motions", "switch_")
+@export var switch_ls_left: Texture2D
+@export var switch_ls_right: Texture2D
+@export var switch_ls_up: Texture2D
+@export var switch_ls_down: Texture2D
+@export var switch_rs_left: Texture2D
+@export var switch_rs_right: Texture2D
+@export var switch_rs_up: Texture2D
+@export var switch_rs_down: Texture2D
+@export var switch_left_trigger: Texture2D
+@export var switch_right_trigger: Texture2D
+@export_subgroup("Switch Buttons", "switch_")
+@export var switch_bot: Texture2D
+@export var switch_right: Texture2D
+@export var switch_left: Texture2D
+@export var switch_top: Texture2D
+@export var switch_back: Texture2D
+@export var switch_guide: Texture2D
+@export var switch_start: Texture2D
+@export var switch_left_stick: Texture2D
+@export var switch_right_stick: Texture2D
+@export var switch_left_shoulder: Texture2D
+@export var switch_right_shoulder: Texture2D
+@export var switch_dup: Texture2D
+@export var switch_ddown: Texture2D
+@export var switch_dleft: Texture2D
+@export var switch_dright: Texture2D
+@export var switch_misc: Texture2D
+@export var switch_pad1: Texture2D
+@export var switch_pad2: Texture2D
+@export var switch_pad3: Texture2D
+@export var switch_pad4: Texture2D
+@export var switch_touch: Texture2D
+
+@export_group("Other", "other_")
+@export_subgroup("Other Motions", "other_")
+@export var other_ls_left: Texture2D
+@export var other_ls_right: Texture2D
+@export var other_ls_up: Texture2D
+@export var other_ls_down: Texture2D
+@export var other_rs_left: Texture2D
+@export var other_rs_right: Texture2D
+@export var other_rs_up: Texture2D
+@export var other_rs_down: Texture2D
+@export var other_left_trigger: Texture2D
+@export var other_right_trigger: Texture2D
+@export_subgroup("Other Buttons", "other_")
+@export var other_bot: Texture2D
+@export var other_right: Texture2D
+@export var other_left: Texture2D
+@export var other_top: Texture2D
+@export var other_back: Texture2D
+@export var other_guide: Texture2D
+@export var other_start: Texture2D
+@export var other_left_stick: Texture2D
+@export var other_right_stick: Texture2D
+@export var other_left_shoulder: Texture2D
+@export var other_right_shoulder: Texture2D
+@export var other_dup: Texture2D
+@export var other_ddown: Texture2D
+@export var other_dleft: Texture2D
+@export var other_dright: Texture2D
+@export var other_misc: Texture2D
+@export var other_pad1: Texture2D
+@export var other_pad2: Texture2D
+@export var other_pad3: Texture2D
+@export var other_pad4: Texture2D
+@export var other_touch: Texture2D
+
+
+func get_mouse_button_texture(button_index: int) -> Texture2D:
+       return get("mouse_%s"%property_map["mouse_button"][button_index - 1])
+
+
+func get_gp_button_texture(category: String, button_index: int) -> Texture2D:
+       return get("%s_%s"%[category, property_map["gp_button"][button_index]])
+
+
+func get_gp_motion_texture(category: String, axis: int, axis_dir: String) -> Texture2D:
+       return get("%s_%s"%[category, property_map["gp_motion"][axis][axis_dir]])
diff --git a/addons/ggs/classes/resources/ggs_plugin_data.gd b/addons/ggs/classes/resources/ggs_plugin_data.gd
new file mode 100644 (file)
index 0000000..692e485
--- /dev/null
@@ -0,0 +1,75 @@
+@tool
+extends Resource
+class_name ggsPluginData
+
+const APPLY_ON_CHANGED_ALL_DEFAULT: bool = true
+const GRAB_FOCUS_ON_MOUSE_OVER_ALL: bool = true
+const DIR_SETTINGS_DEFAULT: String = "res://game_settings/settings"
+const DIR_TEMPLATES_DEFAULT: String = "res://game_settings/templates"
+const DIR_COMPONENTS_DEFAULT: String = "res://game_settings/components"
+const DIR_SAVE_FILE_DEFAULT: String = "user://settings.cfg"
+const SPLIT_OFFSET_0_DEFAULT: int = -315
+const SPLIT_OFFSET_1_DEFAULT: int = 615
+
+@export_category("GGS Plugin Data")
+@export var recent_settings: Array[String]
+@export var apply_on_changed_all: bool = APPLY_ON_CHANGED_ALL_DEFAULT
+@export var grab_focus_on_mouse_over_all: bool = GRAB_FOCUS_ON_MOUSE_OVER_ALL
+@export_group("Directories", "dir_")
+@export_dir var dir_settings: String = DIR_SETTINGS_DEFAULT
+@export_dir var dir_templates: String = DIR_TEMPLATES_DEFAULT
+@export_dir var dir_components: String = DIR_COMPONENTS_DEFAULT
+@export var dir_save_file: String = DIR_SAVE_FILE_DEFAULT
+@export_group("Split Offset", "split_offset_")
+@export var split_offset_0: int = SPLIT_OFFSET_0_DEFAULT
+@export var split_offset_1: int = SPLIT_OFFSET_1_DEFAULT
+
+
+func set_property(property: String, value: Variant) -> void:
+       set(property, value)
+       save()
+
+
+func save() -> void:
+       ResourceSaver.save(self)
+
+
+func reset() -> void:
+       recent_settings.clear()
+       apply_on_changed_all = APPLY_ON_CHANGED_ALL_DEFAULT
+       grab_focus_on_mouse_over_all = GRAB_FOCUS_ON_MOUSE_OVER_ALL
+       dir_settings = DIR_SETTINGS_DEFAULT
+       dir_templates = DIR_TEMPLATES_DEFAULT
+       dir_components = DIR_COMPONENTS_DEFAULT
+       dir_save_file = DIR_SAVE_FILE_DEFAULT
+       split_offset_0 = SPLIT_OFFSET_0_DEFAULT
+       split_offset_1 = SPLIT_OFFSET_1_DEFAULT
+       
+       save()
+
+
+### Recent Settings
+
+func add_recent_setting(setting: String) -> void:
+       if recent_settings.has(setting):
+               _bring_to_front(setting)
+       else:
+               recent_settings.push_front(setting)
+       
+       _limit_size()
+       save()
+
+
+func _bring_to_front(element: String) -> void:
+       recent_settings.erase(element)
+       recent_settings.push_front(element)
+
+
+func _limit_size() -> void:
+       if recent_settings.size() > 10:
+               recent_settings.pop_back()
+
+
+func clear_recent_settings() -> void:
+       recent_settings.clear()
+       save()
diff --git a/addons/ggs/classes/resources/ggs_setting.gd b/addons/ggs/classes/resources/ggs_setting.gd
new file mode 100644 (file)
index 0000000..23db642
--- /dev/null
@@ -0,0 +1,98 @@
+@tool
+extends Resource
+class_name ggsSetting
+
+var current: Variant: set = set_current, get = get_current
+var default: Variant
+var value_type: Variant.Type
+var value_hint: PropertyHint
+var value_hint_string: String
+var name: String: get = get_name
+var category: String: get = get_category
+var read_only_values: bool = false
+
+
+func _get_property_list() -> Array:
+       var read_only: PropertyUsageFlags =  PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_READ_ONLY
+       var current_default_usage: PropertyUsageFlags = read_only if read_only_values else PROPERTY_USAGE_DEFAULT
+       var enum_string_types: String = ggsUtils.get_enum_string("Variant.Type")
+       var enum_string_property_hints: String = ggsUtils.get_enum_string("PropertyHint")
+       
+       var properties: Array
+       properties.append_array([
+               {"name": "Game Setting", "type": TYPE_NIL, "usage": PROPERTY_USAGE_CATEGORY},
+               {"name": "current", "type": value_type, "usage": current_default_usage, "hint": value_hint, "hint_string": value_hint_string},
+               {"name": "default", "type": value_type, "usage": current_default_usage, "hint": value_hint, "hint_string": value_hint_string},
+               {"name": "Internal", "type": TYPE_NIL, "usage": PROPERTY_USAGE_GROUP},
+               {"name": "name", "type": TYPE_STRING, "usage": read_only},
+               {"name": "category", "type": TYPE_STRING, "usage": read_only},
+               {"name": "value_type", "type": TYPE_INT, "usage": PROPERTY_USAGE_DEFAULT, "hint": PROPERTY_HINT_ENUM, "hint_string": enum_string_types},
+               {"name": "value_hint", "type": TYPE_INT, "usage": PROPERTY_USAGE_DEFAULT, "hint": PROPERTY_HINT_ENUM, "hint_string": enum_string_property_hints},
+               {"name": "value_hint_string", "type": TYPE_STRING, "usage": PROPERTY_USAGE_DEFAULT},
+       ])
+       
+       return properties
+
+
+func _get(property: StringName) -> Variant:
+       if property == "resource_name":
+               resource_name = name
+               return resource_name
+       
+       return null
+
+
+func set_current(value: Variant) -> void:
+       current = value
+       
+       if not category.is_empty() or not name.is_empty():
+               ggsSaveFile.new().set_key(category, name, value)
+
+
+func get_current() -> Variant:
+       var save_file: ggsSaveFile = ggsSaveFile.new()
+       if save_file.has_section_key(category, name):
+               return save_file.get_value(category, name)
+       else:
+               return default
+
+
+func get_name() -> String:
+       var path_dict: Dictionary = _get_path_dict()
+       var group: String = ""
+       
+       if path_dict["group"].is_empty():
+               return path_dict["name"]
+       else:
+               return "%s_%s"%[path_dict["group"], path_dict["name"]]
+
+
+func get_category() -> String:
+       return _get_path_dict()["category"]
+
+
+func _get_path_dict() -> Dictionary:
+       var result: Dictionary = {
+               "category": "",
+               "group": "",
+               "name": "",
+       }
+       
+       if not ggsUtils.path_is_in_dir_settings(resource_path):
+               return result
+       
+       var dir_settings: String = ggsUtils.get_plugin_data().dir_settings
+       var base_path: String = resource_path.trim_prefix(dir_settings)
+       var path_components: PackedStringArray = base_path.split("/", false)
+       
+       if path_components.size() < 2 or path_components.size() > 3:
+               return result
+       
+       result["category"] = path_components[0]
+       if path_components.size() == 3:
+               result["group"] = path_components[1]
+               result["name"] = path_components[2].get_basename()
+       else:
+               result["name"] = path_components[1].get_basename()
+       
+       return result
diff --git a/addons/ggs/docs/changelog.md b/addons/ggs/docs/changelog.md
new file mode 100644 (file)
index 0000000..9972949
--- /dev/null
@@ -0,0 +1,65 @@
+## 3.1.0
+This version completely reworks how the categories and settings are stored, and adds several QoL features to the plugin.
+
+### General
+* Categories and Settings are now saved on the disc instead of being subresources of the plugin data. This allows more flexibility when handling them such as moving, renaming, and deleting.
+* Icon and description support for settings and components have been removed. While this was a "cool" feature, it didn't add anything significant and just added to the code bloat since I don't think people would actually spend time designing icons and writing descriptions for their own custom settings.
+* The plugin now applies settings (executes their logic) using a separate thread instead of doing it on the main thread.
+* The preferences window now includes a button for updating the plugin theme to reflect the theme of your own Godot editor.
+* The preferences window has been slightly redesigned for clarity.
+* A "Send Feedback" button has been added which takes you to a Google survey where you can provide feedback regarding the plugin. You can still request features and QoL changes using issues on GitHub.
+* Options were added to the Save File menu that allow you to remake the save file from either `current` or `default` values.
+
+### Settings
+* The settings panel UI has been reworked.
+* You can now group settings in a category for organization. Additionally, you can add settings to multiple groups at the same time, speeding up the process of adding settings to a category.
+* The way custom settings are created and added has been slightly changed and streamlined.
+* The predefined settings (previously in the settings directory) are now considered to be templates.
+* The templates directory (previously the settings directory) now supports tree walking. You can now organize your templates in folders.
+---
+* The input setting has been reworked to use `InputEvent` resources instead of clunky strings.
+* The input setting now supports multiple inputs of the same type for each action (i. e. you can have more than one keyboard or gamepad event for an action).
+
+### Components
+* All list components now support using item IDs instead of indices.
+* The input button now supports icons for mouse events.
+* UI components now warn the user when they don't have a setting or their setting is invalid.
+* You can now set up sound effects for UI components.
+
+
+# Previous Versions
+
+<details>
+<summary>3.0 Changelog</summary>
+
+## 3.0.3
+* Fixed category selection bug in Godot 4.1
+
+## 3.0.2
+* Fixed an issue where the game would crash if the user attempted to rebind input using mice with more than five keys.
+* Fixed an issue where rebinding left and right arrow keys would rebind to the gamepad left button and right button instead.
+
+## 3.0.1
+* Centering the window (part of the fullscreen toggle process) now works properly on setups with multiple displays.
+
+## 3.0.0
+GGS has been completely reworked so it can provide a much better experience for the users. The new version is compatible with Godot 4 only.
+
+### General
+* GGS is now a bottom panel plugin instead of a main screen one.
+* The entire UI has been redesigned to make it easier and more intuitive to work with.
+* Save data is handled via config files instead of JSON files.
+* The way settings are created and handled has been completely reworked.
+* The way UI components are added and handled has been completely reworked.
+
+### Settings
+* Users should now have more freedom and flexibility when creating custom settings.
+* Keyboard Input and Gamepad Input settings have been merged into a single setting. The setting functionality has been improved.
+
+### UI Components
+* Users can now create their own custom UI components.
+* Keyboard Input and Gamepad Input components have been merged into a single component. The component functionality has been improved.
+* New UI components have been added: Apply Button, Radio List, Toggle Button, CheckBox
+
+  
+</details>
diff --git a/addons/ggs/docs/components/apply_button.md b/addons/ggs/docs/components/apply_button.md
new file mode 100644 (file)
index 0000000..c9b1b2d
--- /dev/null
@@ -0,0 +1,6 @@
+Calls `apply_setting()` on all UI components in a specific node group. Must be instantiated directly from the scene tree.
+
+# Properties
+| Property | Description | Type |
+| :---: | --- | :---: |
+| group | The name of the target node group. | `String` |
diff --git a/addons/ggs/docs/components/arrow_list.md b/addons/ggs/docs/components/arrow_list.md
new file mode 100644 (file)
index 0000000..1d4a653
--- /dev/null
@@ -0,0 +1,23 @@
+A list that allows cycling through options using two arrows on the left and right.
+
+Handles integer and boolean values.
+
+# Properties
+| Property | Description | Type |
+| :---: | --- | :---: |
+| options | The list of options. | `PackedStringArray` |
+| option_ids | The list of option IDs. | `PackedInt32Array` |
+
+## option_ids
+This is the list of IDs associated with each option. Each item in this array corresponds to the item with the same index in the `options` array. If this property is set, the component returns the item ID instead of its index. If you don't want to use IDs, simply leave this as empty.
+
+Example:
+
+Let's say your setting sets the number of power-ups the player character starts with.
+```gdscript
+option = ["low", "medium", "high"]
+option_ids = [5, 10, 20]
+```
+
+> [!NOTE]
+> If you want to use item IDs, both `options` and `option_ids` must be the same size.
diff --git a/addons/ggs/docs/components/binary_selection.md b/addons/ggs/docs/components/binary_selection.md
new file mode 100644 (file)
index 0000000..a4d12b1
--- /dev/null
@@ -0,0 +1,3 @@
+Multiple components that all do the same thing. Checkbox, Switch, and Toggle Button are all binary selection components that allow the user to choose between a On/Off state.
+
+Handles boolean values.
diff --git a/addons/ggs/docs/components/components.md b/addons/ggs/docs/components/components.md
new file mode 100644 (file)
index 0000000..621cfd3
--- /dev/null
@@ -0,0 +1,33 @@
+UI components are nodes that can be added to your settings menu and allow players to change a setting.
+
+# Properties
+All components have several properties that are shared among them.
+| Property | Description | Type |
+| :---: | --- | :---: |
+| setting | The setting that's assigned to the component. The setting's value type must be compatible with the types the component handles. | `ggsSetting` |
+| apply_on_changed | Whether the component should apply the setting when the player interacts with it. If false, you should apply the settings with an [Apply Button](apply_button.md) component. | `Bool`|
+| grab_focus_on_mouse_over | Whether the component should grab focus on mouseover. Useful if your game supports both keyboard and mouse. | `Bool` |
+
+# Setting Sound Effects
+You can set sound effects to be played when the player mouses over the components, interacts with them, or the components grab focus.
+
+First, you should open the `ggs.tscn` scene. To do so:
+* Open the scene via the Preferences in the GGS editor.
+* Open the scene using the *Quick Open Scene...* option in Godot editor.
+* Manually open the scene. The scene is located at `res://addons/ggs/classes/global/ggs.tscn`
+
+This scene is the same scene that's added to the autoload list. Once the scene is open, assign an audio stream to each of the available audio stream players under the root `GGS` node.
+
+# Predefined Components
+GGS comes with the following predefined components:
+* [Binary Selection](binary_selection.md)
+* [Arrow List](arrow_list.md)
+* [Option List](option_list.md)
+* [Radio List](radio_list.md)
+* [Slider](slider.md)
+* [SpinBox](spinbox.md)
+* [Text Field](text_field.md)
+* [Input Button](input_button.md)
+* [Input Confirm Window](input_confirm_window.md)
+* [Apply Button](apply_button.md)
+* [Reset Button](reset_button.md)
diff --git a/addons/ggs/docs/components/input_button.md b/addons/ggs/docs/components/input_button.md
new file mode 100644 (file)
index 0000000..007bb95
--- /dev/null
@@ -0,0 +1,18 @@
+Displays the assigned [Input Confirm Window](input_confirm_window.md) when pressed and rebinds the input if the window is confirmed.
+
+Handles array values.
+
+# Properties
+| Property | Description | Type |
+| :---: | --- | :---: |
+| ICW | The path to the **Input Confirm Window**. Required. | `ConfirmationDialog` |
+| accept_modifiers | Whether the Input Confirm Window should accept events with modifiers (e.g. Shift+D, Alt+M, etc.) | `bool` |
+| accept_mouse | Whether the Input Confirm Window should accept mouse events. | `bool` |
+| accept_axis | Whether the Input Confirm Window should accept gamepad axis inputs (e.g. left stick left, right stick up, etc.) | `bool` |
+| use_icons | Whether the button should display icons instead of text for mouse and gamepad inputs. | `bool` |
+| icon_db | The database for the mouse and gamepad icons. | `ggsIconDB` |
+
+## Working with ggsIconDB
+This is a custom resource that can be used to assign textures to each gamepad and mouse input. It is highly recommended to create and save this on disk and use `AtlasTexture`s when setting the individual textures. If no texture is available for an input, the button will show it as text instead.
+
+The button shows text or icon that corresponds to the currently connected gamepad device. If no gamepad is connected (or the connected gamepad is not recognized), it uses the "other" text or icons.
diff --git a/addons/ggs/docs/components/input_confirm_window.md b/addons/ggs/docs/components/input_confirm_window.md
new file mode 100644 (file)
index 0000000..3d48f1c
--- /dev/null
@@ -0,0 +1,14 @@
+A confirmation dialog that allows the user to choose an input. Required component for [Input Button](input_button.md) to function. Must be instantiated in the scene tree directly and one instance is enough. You don't have to instantiate it for every Input Button component you have.
+
+
+# Properties
+| Property | Description | Type |
+| :---: | --- | :---: |
+| listening_wait_time | The time it takes for the input to be accepted when an input is received. | `float` |
+| listening_max_time | The maximum time the window will listen for an input before it stops. | `float` |
+| show_progress_bar |  Whether to show the bar under the listen button when an input is received. | `bool` |
+| btn_listening | Text displayed on the listen button when the window is currently listening for input. | `String` |
+| title_listening | Window title when it's currently listening for input. | `String` |
+| title_confirm | Window title when it's not listening for input and is awaiting confirmation or cancellation. | `String` |
+| timeout_text | Text displayed on the listen button when listening times out. | `String` |
+| already_exists_msg | Text displayed below the listen button when the received input already exists. You can use `{action}` as a placeholder for the action that has the conflicting input event | `String` |
diff --git a/addons/ggs/docs/components/option_list.md b/addons/ggs/docs/components/option_list.md
new file mode 100644 (file)
index 0000000..564c57e
--- /dev/null
@@ -0,0 +1,8 @@
+A drop-down menu that allows the selection of one option. Define options by editing the **Items** of the child OptionButton via the Inspector.
+
+Handles integer and boolean values.
+
+# Properties
+| Property | Description | Type |
+| :---: | --- | :---: |
+| use_ids | Whether the component should use the item ID instead of its index when selected. | `bool` |
diff --git a/addons/ggs/docs/components/radio_list.md b/addons/ggs/docs/components/radio_list.md
new file mode 100644 (file)
index 0000000..636f919
--- /dev/null
@@ -0,0 +1,18 @@
+A group of buttons. Only one button can be selected at any time.
+
+Handles integer and boolean values.
+
+# Properties
+| Property | Description | Type |
+| :---: | --- | :---: |
+| option_ids | The list of option IDs. | `PackedInt32Array` |
+| active_list | The `BoxContainer` that will be used. | `PackedInt32Array` |
+
+## Adding Options
+You can add `Button`s or `CheckButton`s to the active list. All buttons must have `toggle_mode` enabled. The index of the button node in the active list is the index of the option (e.g. the first button returns `0`, the second one returns `1`, and so on) unless `option_ids` is not empty.
+
+## option_ids
+This is the list of IDs associated with each option. Each item in this array corresponds to the button node with the same index in the active list. If this property is set, the component returns the item ID instead of its index. If you don't want to use IDs, simply leave this as empty.
+
+> [!NOTE]
+> If you want to use item IDs, both `option_ids` and the number of button nodes in the active list must be the same size.
diff --git a/addons/ggs/docs/components/reset_button.md b/addons/ggs/docs/components/reset_button.md
new file mode 100644 (file)
index 0000000..62fe26c
--- /dev/null
@@ -0,0 +1,6 @@
+Calls `reset_setting()` on all UI components in a specific node group. Must be instantiated directly from the scene tree.
+
+# Properties
+| Property | Description | Type |
+| :---: | --- | :---: |
+| group | The name of the target node group. | `String` |
diff --git a/addons/ggs/docs/components/slider.md b/addons/ggs/docs/components/slider.md
new file mode 100644 (file)
index 0000000..40a6886
--- /dev/null
@@ -0,0 +1,3 @@
+A slider for choosing a number between a certain range. Edit the child `HSlider` to set range, step, etc.
+
+Handles integer and float values.
diff --git a/addons/ggs/docs/components/spinbox.md b/addons/ggs/docs/components/spinbox.md
new file mode 100644 (file)
index 0000000..e1cffe4
--- /dev/null
@@ -0,0 +1,3 @@
+A text field that only accepts numbers. Edit the child `SpinBox` to set range, step, etc.
+
+Handles integer and float values.
diff --git a/addons/ggs/docs/components/text_field.md b/addons/ggs/docs/components/text_field.md
new file mode 100644 (file)
index 0000000..86bbcb1
--- /dev/null
@@ -0,0 +1,3 @@
+A simple text field.
+
+Handles string values.
diff --git a/addons/ggs/docs/custom_components.md b/addons/ggs/docs/custom_components.md
new file mode 100644 (file)
index 0000000..2403695
--- /dev/null
@@ -0,0 +1,86 @@
+You can create your own custom UI components and use them just like the predefined components. UI Components use a property named `setting_value` to keep track of their current value. For example, for a simple Toggle Button, this `setting_value` can either be `true` or `false` and it corresponds to the button pressed state.
+
+When the user interacts with the component (in our example, toggles the button), the `setting_value` is updated with the new value (in our example, the new button pressed state). Then, if `apply_on_changed` is enabled, the setting is applied.
+
+# Creating the Component Files
+All components are located in your components directory (default: `game_settings/components`). 
+1. Create a new folder in this directory and give it an appropriate name. You can tell GGS to ignore the folder if you start the name with an underscore (e.g. `_ignore_this`).
+2. Create a scene in the folder. This scene *cannot* be in a subfolder and *must* be named the same thing as the folder. For example, if the folder is named `my_component`, the scene must be named `my_component.tscn`. The scripts or other secondary scenes can be in any directory (even outside of this folder) but the main scene must be a direct file of the folder.
+3. The scene root *must* be a `MarginContainer`. You can add any other nodes you want to this root.
+4. After creating your component, restart the plugin so you can see it in the components list.
+
+# Adding a Script
+Now, you can add a script to your root `MarginContainer` node. This script:
+* Must be a `@tool` script.
+* Must extend `ggsUIComponent`.
+* When overriding the following methods, you must first call the parent method using `super()`:
+  * `_ready()`
+  * `init_value()`
+  * `reset_setting()`
+* When the user interacts with the component (such as toggling a button, changing a slider value, etc.), the following piece should be used:
+```gdscript
+setting_value = new_value
+if apply_on_change:
+       apply_setting()
+```
+* When overriding the `_ready()` method, you should add the following piece to the beginning before anything else:
+```gdscript
+compatible_types = []
+if Engine.is_editor_hint():
+  return
+```
+You should set `compatible_types` to contain the types that this component can handle. For example, if your component can handle integer and boolean values, it should be:
+```gdscript
+compatible_types = [TYPE_BOOL, TYPE_INT]
+```
+
+The `Engine.is_editor_hint()` check is used to prevent the nodes from running their setting-related code (getting its value, applying its logic, etc.) in the Godot editor. The components are `@tool` scripts so they can send configuration warnings to the user (usually when something is wrong with the assigned setting).
+
+
+# Class Methods
+## init_value()
+Loads `setting_value` from the assigned `setting`. Additionally, all code related to initializing the component state should go here. Example:
+```gdscript
+func init_value() -> void:
+       super()
+       Btn.set_pressed_no_signal(setting_value)
+```
+Remember, using `super()` when overriding this method is required because the `init_value()` of the base `ggsUIComponent` class must also be executed for the component to work properly.
+
+## apply_setting()
+Saves the setting value to the save file and executes the setting logic. You don't generally need to override this, simply call it when you want the component to apply the setting. Always use the `if apply_on_changed` check before using it otherwise the `apply_on_changed` property won't have any effect.
+
+This method is also called when a relevant **Apply Button** is pressed.
+
+## reset_setting()
+Resets the setting value back to its default and executes the setting logic. You should override this and update your component state in it. Remember to use `super()`. This method is called when a relevant **Reset Button* is pressed.
+
+-----
+
+Here's a simple example of a toggle button component:
+```gdscript
+extends ggsUIComponent
+
+@onready var Btn: Button = $Btn
+
+
+func _ready() -> void:
+       super()
+       Btn.toggled.connect(_on_Btn_toggled)
+
+
+func init_value() -> void:
+       super()
+       Btn.set_pressed_no_signal(setting_value)
+
+
+func _on_Btn_toggled(btn_state: bool) -> void:
+       setting_value = btn_state
+       if apply_on_change:
+               apply_setting()
+
+
+func reset_setting() -> void:
+       super()
+       Btn.set_pressed_no_signal(setting_value)
+```
diff --git a/addons/ggs/docs/custom_settings.md b/addons/ggs/docs/custom_settings.md
new file mode 100644 (file)
index 0000000..40ebf9a
--- /dev/null
@@ -0,0 +1,51 @@
+You can create your own custom settings and templates in GGS.
+
+# Setting up a Custom Setting
+When you add a setting via the *New Setting...* field, you create a blank setting with no logic or properties. In order to use it, you need to set up it properly.
+
+## The `value_type`
+To start, select your newly created blank setting. You may notice that the `current` and `default` values are `null`. This is because the `value_type` property has not been set. Open the `Internals` group and choose an appropriate value type for your setting from the drop-down menu. You can optionally choose `value_hint` and `value_hint_string`. These are used to customize the export behavior of the `current` and `default` values.
+
+For example, if your `value_type` is `float`, you can set `value_hint` to be `Range`. And set `value_hint_string` to be `0,100`. This will export `default` and `current` properties as a range between `0` and `100`. For more information, view `_get_property_list()` and `Property.Hint` constants in the Godot docs.
+
+Once you've set the `value_type` select the setting again to reinspect it. Now you can see the `default` and `current` value are exported correctly.
+
+## The Logic
+To write the logic for your setting, you need to edit its script. You can do so by:
+* Either open the `script` property in the Inspector. Under the `RefCounted` category.
+* Directly open the script in the file system.
+
+As you can see, there are a few prerequisites for the script. The script *must*:
+* Be a `@tool` script.
+* Extend `ggsSetting`.
+* Have a method called `apply()`.
+
+The `apply()` method is where your logic should go. It must take a value (the same type defined in `value_type`).
+
+Here's a simple example of a VSync setting:
+
+```gdscript
+@tool
+extends ggsSetting
+
+
+func apply(value: bool) -> void:
+       var vsync_mode: DisplayServer.VSyncMode
+       match value:
+               true:
+                       vsync_mode = DisplayServer.VSYNC_ENABLED
+               false:
+                       vsync_mode = DisplayServer.VSYNC_DISABLED
+       
+       DisplayServer.window_set_vsync_mode(vsync_mode)
+```
+
+That's the core of what you need to do. You can define variables, other methods, etc. in the script.
+
+# Templates
+Creating a template is essentially the same as setting up the script of a blank setting. To start, create a script in your templates directory. The script must fulfill all three conditions explained previously but it has one additional condition:
+* The script must override the `_init()` method and set `value_type` and `default` in there. You can also set `value_hint` and `value_hint_string` if applicable.
+
+That's the only additional criterion you have to keep in mind. Everything else is the same as mentioned previously.
+
+Check out some of the predefined templates to see a few examples of what you can do.
diff --git a/addons/ggs/docs/getting_started.md b/addons/ggs/docs/getting_started.md
new file mode 100644 (file)
index 0000000..ada71f7
--- /dev/null
@@ -0,0 +1,116 @@
+This guide will show you how to create a basic settings menu with the predefined settings and components that come with GGS.
+
+# Installation
+You can install the plugin through various ways, including:
+* Install the plugin through the Asset Library inside the Godot editor.
+* Clone the repository with Git.
+* Download the latest release.
+* Download the `main` branch directly with [DownGit](https://minhaskamal.github.io/DownGit/#/home) or similar tools.
+* Add the `main` branch as a Git Submodule. [Learn more about Git Submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules).
+
+> [!IMPORTANT]
+> No matter how you download the `main` branch (via releases, DownGit, or as a Git submodule), you should copy the contents of the *_premade* folder and paste them into your own game settings directory. You can change these directories in Preferences.
+> Example:
+> ```
+> res://
+>   â”” game_settings
+>       â”” components
+>       â”” settings
+>       â”” templates
+> ```
+
+> [!NOTE]
+> The plugin may not work as expected when added to a project at first. Enable the plugin through the Project Settings then restart the Godot editor. [More information](troubleshoot.md#can't-use-the-plugin-after-installation).
+
+# GGS Editor
+When enabled, GGS adds a bottom panel editor to Godot named "Game Settings". The editor is divided into 3 panels:
+* The categories panel on the left.
+* The settings panel in the center.
+* The components panel on the right.
+
+# Categories
+To add settings, you should first add categories. To do so, simply enter a valid name in the *New Category...* field at the top of the categories panel and press ENTER.
+A valid category name is:
+1. A valid file name (does not have unsupported special characters such as `@`, `?`, `!`, etc., and does not start or end with trailing space).
+2. Does not start with an underscore (`_`) or a dot (`.`). Underscores are used to tell GGS to ignore a directory. Dots do the same but for Godot's file system.
+3. While not necessary, I recommend using snake_case to follow Godot's recommended styling guidelines.
+
+After doing that, you may notice a folder is added to the settings directory (`res://game_settings/settings` by default) in the file system. All folders that are *direct* children of the settings directory will be considered categories. If you want GGS to ignore a folder, add an underline to the beginning of its name (e.g. `_ignore_this`).
+
+## Renaming Categories
+
+To rename a category, simply rename its folder from the Godot file system.
+
+## Deleting Categories
+
+To delete a category, simply delete its folder from the Godot file system. Depending on your system settings, this will either permanently delete the folder or move it to the recycle bin.
+
+> [!WARNING]
+> Do not rename or delete files and folders from your OS file system in general. This can cause issues with references in Godot. Use Godot's own file system to do so.
+
+# Settings
+Settings are resources that contain the logic and properties of a game setting. For example, the audio volume setting contains the name of an audio bus, the current and default values for it, and how Godot is supposed to change the volume of said bus.
+
+There are two methods of adding settings: Adding blank settings and adding settings from a template.
+
+## Adding Blank Settings
+This method is useful when you want to create a single unique setting. For example, if you want to add a difficulty option, you can use this method as a game usually has only one difficulty option.
+
+To add a blank setting with no properties and logic, use the *New Setting...* field at the top of the settings panel. Enter a valid name (the validation is the same as category names) and press ENTER to add a new setting to the currently selected category. If one or more groups are selected, a setting is added to each selected group.
+
+When you create a setting via this method, GGS creates a new `ggsSetting` resource and saves it on your disc inside the category folder. It then duplicates the blank template script and assigns it to said resource.
+
+> [!NOTE]
+> You can edit this blank template script from preferences.
+
+## Adding Settings from Templates
+There are times when a type of setting must be used multiple times. The easiest example is an input setting. A game usually has multiple inputs and you need a setting resource for each individual input. The logic behind *how* an input should be changed is the same among all of them. The only thing that changes is the input action they should change (in other words, a property).
+
+This is where templates come into play. You can add settings from templates to create multiple settings with the same logic and properties that can be changed via the inspector.
+
+To add a setting from a template, use the `+` button. This will open a window that shows all the available templates located in the templates directory (`res://game_settings/templates` by default). Simply double-click a template, give the setting a valid name, and press ENTER or press the *Add* button.
+
+This method is essentially the same as the first one when it comes to the actual creation process, except that instead of assigning a blank script to the resource, it assigns the specific template script that you selected.
+
+## Inspecting Settings
+To inspect a setting, click on it in the settings panel. This will show its `current` and `default` values along with any additional properties it might have in the Godot Inspector. You can also view its script all the way down via the `script` property under `RefCounted`.
+
+The properties inside the `Internal` group are internal properties used by GGS to handle the setting. These are explained further in [Creating Custom Settings](custom_settings.md).
+
+You can also right-click on a setting to highlight in the Godot file system.
+
+## Groups
+As implied previously, you can add settings to groups. Groups can help you add settings faster and tidy up your category.
+
+To add a group, enter a valid name in the *New Group...* field and press ENTER.
+
+To add settings to a group, click on it to select it. When adding settings via any of the aforementioned methods, GGS will add a setting to every selected group. Do note that adding settings to a high number of groups can take a few moments. Godot will be unresponsive during this time.
+
+## Deleting and Renaming
+You can rename, delete, or move settings and groups in the file system similar to categories. 
+
+## Custom Settings
+To learn how you can set up your own custom settings and templates, view [Creating Custom Settings](custom_settings.md).
+
+For more info on the predefined settings, visit [Settings](settings/settings.md).
+
+
+# UI Components
+The UI components are what the user can interact with to change a setting. They're located at `game_settings/components`. You can assign a setting to each component. When the component is interacted with, it'll update the save file with the new value and execute the logic of its assigned setting.
+
+Each component has an `apply_on_changed` property. If set to `true`, the component will apply the setting (update save file + execute setting logic) when the user interacts with it. If false, you need to use an **Apply Button** to apply the setting and save changes.
+
+To add a new component, select a setting and a *single* node in the scene tree of your target scene. Then, simply double-click a component in the component list to instantiate a component of that type.
+
+Each component can only handle a specific type of data. An **Arrow List** for example, can only handle integers while a **Slider** can only handle floats.
+
+It is possible to use certain components with multiple types. For instance, you can use an **Arrow List** for a setting that accepts boolean values if you configure the arrow list to have only 2 items: The first one being the "false" or "disabled" option and the second one being the "true" or "enabled" option.
+
+## Custom Components
+To learn how you can create your own custom components, view [Creating Custom Components](custom_components.md).
+
+For more info on the predefined components, visit [Components](components/components.md).
+
+---
+
+Feel free to open an issue to ask for help, report a bug, or suggest additional features or QoL enhancements.
diff --git a/addons/ggs/docs/home.md b/addons/ggs/docs/home.md
new file mode 100644 (file)
index 0000000..9aaf8a7
--- /dev/null
@@ -0,0 +1,6 @@
+# Welcome to GGS Documentation
+You can find all the information you need to use GGS in here. If this is your first time using GGS, consider starting with [Getting Started](getting_started.md).
+
+View [Settings](settings/settings.md) or [Components](components/components.md) if you have questions regarding a specific predefined setting or component.
+
+Visit [Troubleshoot](troubleshoot.md) if there's a general issue with the plugin (such as error messages).
diff --git a/addons/ggs/docs/settings/audio_mute.md b/addons/ggs/docs/settings/audio_mute.md
new file mode 100644 (file)
index 0000000..c83ad18
--- /dev/null
@@ -0,0 +1,7 @@
+Sets the mute state of a specific audio bus.
+
+# Properties
+| Property | Description | Type |
+| :---: | --- | :---: |
+| default/current | The default and current values of the setting. | `bool` |
+| audio_bus | The audio bus that this setting will affect. | `String` |
diff --git a/addons/ggs/docs/settings/audio_volume.md b/addons/ggs/docs/settings/audio_volume.md
new file mode 100644 (file)
index 0000000..1b39872
--- /dev/null
@@ -0,0 +1,7 @@
+Sets the volume of a specific audio bus.
+
+# Properties
+| Property | Description | Type |
+| :---: | --- | :---: |
+| default/current | The default and current values of the setting. | `float`: 0 to 100 |
+| audio_bus | The audio bus that this setting will affect. | `String` |
diff --git a/addons/ggs/docs/settings/display_fullscreen.md b/addons/ggs/docs/settings/display_fullscreen.md
new file mode 100644 (file)
index 0000000..9652a9f
--- /dev/null
@@ -0,0 +1,7 @@
+Toggles the fullscreen state of the game window.
+
+# Properties
+| Property | Description | Type |
+| :---: | --- | :---: |
+| default/current | The default and current values of the setting. | `bool` |
+| size_setting | Name of the setting responsible for resizing the window (e.g. window size, window scale, etc.). If nothing is selected, the window size will not be updated when the fullscreen state is turned off. This is particularly important when the user changes the window size *while* the game is in fullscreen. | `ggsSetting` |
diff --git a/addons/ggs/docs/settings/display_scale.md b/addons/ggs/docs/settings/display_scale.md
new file mode 100644 (file)
index 0000000..9e3761d
--- /dev/null
@@ -0,0 +1,7 @@
+Sets the window size by multiplying its width and height by a certain value.
+
+# Properties
+| Property | Description | Type |
+| :---: | --- | :---: |
+| default/current | Index or ID of a list item. Invalid until the `scales` property is set. | `int` |
+| scales | The list of available sizes. When setting up a UI component, the order of list items must match the order of items in this property unless you're using item IDs. If using item IDs, each item ID must point to a valid index in this array. | `Array[float]` |
diff --git a/addons/ggs/docs/settings/display_size.md b/addons/ggs/docs/settings/display_size.md
new file mode 100644 (file)
index 0000000..190c2db
--- /dev/null
@@ -0,0 +1,7 @@
+Sets the window size by setting its width and height to certain values.
+
+# Properties
+| Property | Description | Type |
+| :---: | --- | :---: |
+| default/current | Index or ID of a list item. Invalid until the `sizes` property is set. | `int` |
+| sizes | The list of available sizes. When setting up a UI component, the order of list items must match the order of items in this property unless you're using item IDs. If using item IDs, each item ID must point to a valid index in this array. | `Array[Vector2]` |
diff --git a/addons/ggs/docs/settings/input.md b/addons/ggs/docs/settings/input.md
new file mode 100644 (file)
index 0000000..115da24
--- /dev/null
@@ -0,0 +1,37 @@
+Sets an input event of a specific input action (i.e. rebinds an input).
+
+# Properties
+| Property | Description | Type |
+| :---: | --- | :---: |
+| default/current | An array that stores the input type and id. The array structure is `[type, id]`. | `Array[int]`: Read-Only |
+| action | The input action that holds the target input event. | `String`: Read-Only |
+| event_index | The index of the target input event inside the input action. | `int`: Read-Only |
+| default_as_event | The default value (an Array) as an `InputEvent`. | `InputEvent`: Read-Only |
+| current_as_event | The current value (an Array) as an `InputEvent`. | `InputEvent` |
+
+## Type and ID
+Type is the type of input event such as `InputEventKey` or `InputEventJoypadMotion`. GGS uses this to create the correct type of input event when loading the setting.
+
+ID refers to the property that stores what the actual input is. For each event type, it's as followed:
+* **InputEventKey**: `physical_keycode`
+* **InputEventMouseButton** & **InputEventJoypadButton**: `button_index`
+* **InputEventJoypadMotion**: `axis`
+
+## default_as_event
+As mentioned, it shows the default value as an appropriate `InputEvent`. This is always the same as the input event defined in the Input Map.
+
+> [!WARNING]
+> The `default_as_event` property is read-only. However, you can still technically change it by using the "Configure" button when expanding the resource. Do not do this. If you want to change the default value, use the "Select Input" button at the top instead.
+
+## current_as_event
+It shows the current value as an appropriate `InputEvent`. You can easily change this by expanding the resource and using the "Configure" button. You can also clear it and add another type of `InputEvent` (e.g. if it's an `InputEventKey`, you can clear that and create an `InputEventMouseButton` instead).
+
+Unlike other current/default values of other settings, the `current_as_event` property is not updated from the save file every tick as this would prevent the user from changing the resource. Instead, it's updated every time the setting is inspected. So if the `current_as_event` does not reflect what the actual `current` value is, simply re-inspect the setting.
+
+> [!NOTE]
+> The type of `InputEvent` you can create when setting `current_as_event` depends on the type of `default_as_event`. If the default is keyboard or mouse, then you can only create `InputEventKey` and `InputEventMouseButton`. If it's one of the gamepad events, you can only create a gamepad event.
+
+> [!WARNING]
+> When configuring an `InputEventKey`, use "physical keycode". GGS does not accept "keycode" and "key label".
+
+
diff --git a/addons/ggs/docs/settings/settings.md b/addons/ggs/docs/settings/settings.md
new file mode 100644 (file)
index 0000000..b3b6c45
--- /dev/null
@@ -0,0 +1,7 @@
+GGS comes with the following predefined settings:
+* [Audio Mute](audio_mute.md)
+* [Audio Volume](audio_volume.md)
+* [Display Fullscreen](display_fullscreen.md)
+* [Display Scale](display_scale.md)
+* [Display Size](display_size.md)
+* [Input](input_setting.md)
diff --git a/addons/ggs/docs/troubleshoot.md b/addons/ggs/docs/troubleshoot.md
new file mode 100644 (file)
index 0000000..a2d58dd
--- /dev/null
@@ -0,0 +1,21 @@
+## Can't use the plugin after installation
+When you first install the plugin, Godot might output the following error:
+```
+Parse Error: Identifier "GGS" not declared in the current scope.
+```
+This means that the GGS singleton doesn't exist. When the plugin is first added, Godot tries to parse its scripts and since the singleton doesn't exist at that point (since the plugin is not enabled yet), it'll throw that error. The plugin will not function properly even after you add the singleton to the project. To fix it:
+1. Make sure the singleton is added to the autoload list (Project Settings â†’ Autoload) and it's enabled.
+2. Reload the project.
+
+Note that the plugin adds the singleton automatically when enabled. If for some reason it doesn't, you can find the script at the following path:
+```
+res://addons/ggs/classes/ggs_globals.gd
+```
+Simply add that script to the autoload list, **name it GGS**, and reload the project.
+
+
+## Setting type error
+```
+Invalid type in function 'apply' in base 'Resource ()'. Cannot convert argument 1 from <somet_type> to <some_other_type>.
+```
+When launching the game, you may encounter the above error, which leads to `ggs_globals.gd`. If so, check your setting scripts. Make sure the type the `apply()` method receives is correct. For example, your `apply()` method might expect a `bool` value but it's receiving a `String`.
diff --git a/addons/ggs/editor/_theme/ggs_theme.gd b/addons/ggs/editor/_theme/ggs_theme.gd
new file mode 100644 (file)
index 0000000..05151ad
--- /dev/null
@@ -0,0 +1,38 @@
+@tool
+extends Theme
+
+const SETTING_LIST_MARGIN: int = 10
+
+var editor_theme: Theme
+
+
+func update() -> void:
+       editor_theme = ggsUtils.get_editor_interface().get_base_control().theme
+       _set_window_bg()
+       _set_setting_list_bg()
+       _set_setting_item_bg()
+       ResourceSaver.save(self, resource_path)
+
+
+func _set_setting_list_bg() -> void:
+       var default: StyleBoxFlat = editor_theme.get_stylebox("panel", "ItemList")
+       
+       var new_stylebox: StyleBoxFlat = default.duplicate()
+       new_stylebox.set_content_margin_all(SETTING_LIST_MARGIN)
+       set_stylebox("panel", "SettingListBG", new_stylebox)
+
+
+func _set_setting_item_bg() -> void:
+       var default: StyleBoxFlat = editor_theme.get_stylebox("panel", "AcceptDialog")
+       
+       var new_stylebox: StyleBoxFlat = default.duplicate()
+       new_stylebox.set_content_margin_all(SETTING_LIST_MARGIN)
+       new_stylebox.set_corner_radius_all(3)
+       set_stylebox("panel", "SettingItemBG", new_stylebox)
+
+
+func _set_window_bg() -> void:
+       var default: StyleBoxFlat = editor_theme.get_stylebox("panel", "AcceptDialog")
+       
+       var new_stylebox: StyleBoxFlat = default.duplicate()
+       set_stylebox("panel", "PrefWindowBG", new_stylebox)
diff --git a/addons/ggs/editor/_theme/ggs_theme.tres b/addons/ggs/editor/_theme/ggs_theme.tres
new file mode 100644 (file)
index 0000000..0b159d6
--- /dev/null
@@ -0,0 +1,55 @@
+[gd_resource type="Theme" load_steps=5 format=3 uid="uid://c4pwg7lhukqb8"]
+
+[ext_resource type="Script" path="res://addons/ggs/editor/_theme/ggs_theme.gd" id="1_3cm4d"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wolqy"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.211765, 0.211765, 0.211765, 1)
+border_color = Color(0.211765, 0.211765, 0.211765, 1)
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+corner_detail = 3
+expand_margin_bottom = 2.0
+anti_aliasing = false
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_l5a6g"]
+content_margin_left = 10.0
+content_margin_top = 10.0
+content_margin_right = 10.0
+content_margin_bottom = 10.0
+bg_color = Color(0.211765, 0.211765, 0.211765, 1)
+border_color = Color(0.211765, 0.211765, 0.211765, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+corner_detail = 3
+expand_margin_bottom = 2.0
+anti_aliasing = false
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_g1mf3"]
+content_margin_left = 10.0
+content_margin_top = 10.0
+content_margin_right = 10.0
+content_margin_bottom = 10.0
+bg_color = Color(0.148235, 0.148235, 0.148235, 1)
+border_color = Color(0.084706, 0.084706, 0.084706, 1)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+corner_detail = 3
+anti_aliasing = false
+
+[resource]
+ItemList/colors/guide_color = Color(0.701961, 0.701961, 0.701961, 0)
+ItemList/constants/v_separation = 6
+PrefWindowBG/styles/panel = SubResource("StyleBoxFlat_wolqy")
+SettingItemBG/base_type = &"PanelContainer"
+SettingItemBG/styles/panel = SubResource("StyleBoxFlat_l5a6g")
+SettingListBG/base_type = &"PanelContainer"
+SettingListBG/styles/panel = SubResource("StyleBoxFlat_g1mf3")
+script = ExtResource("1_3cm4d")
diff --git a/addons/ggs/editor/add_setting_window/add_setting_window.gd b/addons/ggs/editor/add_setting_window/add_setting_window.gd
new file mode 100644 (file)
index 0000000..01e9aac
--- /dev/null
@@ -0,0 +1,166 @@
+@tool
+extends ConfirmationDialog
+signal template_selected(template_path: String, setting_name: String)
+
+@onready var SettingList: ItemList = %SettingList
+@onready var FilterField: LineEdit = %FilterField
+@onready var RecentList: ItemList = %RecentList
+@onready var ClearRecentBtn: Button = %ClearRecentBtn
+@onready var NameField: LineEdit = %NameField
+@onready var OkBtn: Button = get_ok_button()
+
+
+func _ready() -> void:
+       about_to_popup.connect(_on_about_to_popup)
+       visibility_changed.connect(_on_visibility_changed)
+       confirmed.connect(_on_confirmed)
+       
+       SettingList.item_selected.connect(_on_AnyList_item_selected.bind(SettingList))
+       SettingList.item_activated.connect(_on_AnyList_item_activated.bind(SettingList))
+       RecentList.item_selected.connect(_on_AnyList_item_selected.bind(RecentList))
+       RecentList.item_activated.connect(_on_AnyList_item_activated.bind(RecentList))
+       
+       FilterField.text_changed.connect(_on_FilterField_text_changed)
+       ClearRecentBtn.pressed.connect(_on_ClearRecentBtn_pressed)
+       NameField.text_submitted.connect(_on_NameField_text_submitted)
+
+
+func _confirm() -> void:
+       var selected_item_index: int
+       var selected_item_meta: String
+       
+       if SettingList.is_anything_selected():
+               selected_item_index = SettingList.get_selected_items()[0]
+               selected_item_meta = SettingList.get_item_metadata(selected_item_index)
+       
+       if RecentList.is_anything_selected():
+               selected_item_index = RecentList.get_selected_items()[0]
+               selected_item_meta = RecentList.get_item_metadata(selected_item_index)
+       
+       template_selected.emit(selected_item_meta, NameField.text)
+       ggsUtils.get_plugin_data().add_recent_setting(selected_item_meta)
+
+
+func _on_about_to_popup() -> void:
+       OkBtn.disabled = true
+       NameField.editable = true
+       NameField.clear()
+       FilterField.clear()
+       _load_settings()
+       _load_recent()
+
+
+func _on_visibility_changed() -> void:
+       if visible == true:
+               FilterField.grab_focus()
+
+
+func _on_confirmed() -> void:
+       _confirm()
+
+
+func _on_NameField_text_submitted(_submitted_text: String) -> void:
+       _confirm()
+       hide()
+
+
+### Lists (General)
+
+func _deselect_other_list(list: ItemList) -> void:
+       if list == SettingList:
+               RecentList.deselect_all()
+       else:
+               SettingList.deselect_all()
+
+
+func _on_AnyList_item_selected(index: int, list: ItemList) -> void:
+       OkBtn.disabled = true
+       NameField.editable = false
+       _deselect_other_list(list)
+
+
+func _on_AnyList_item_activated(index: int, list: ItemList) -> void:
+       OkBtn.disabled = false
+       NameField.editable = true
+       NameField.grab_focus()
+
+
+### Setting List
+
+func _load_settings() -> void:
+       SettingList.clear()
+       
+       var template_list: PackedStringArray = _get_all_settings()
+       for template in template_list:
+               var item_index: int = SettingList.add_item(template.get_file().get_basename())
+               SettingList.set_item_metadata(item_index, template)
+               SettingList.set_item_tooltip(item_index, template)
+
+
+func _get_all_settings() -> PackedStringArray:
+       var all_settings: PackedStringArray
+       var path: String = ggsUtils.get_plugin_data().dir_templates
+       
+       var dir: DirAccess = DirAccess.open(path)
+       var templates: PackedStringArray = dir.get_files()
+       for template in templates:
+               template = dir.get_current_dir().path_join(template)
+               all_settings.append(template)
+       
+       _get_settings_in_dir(dir, all_settings)
+       
+       return all_settings
+
+
+func _get_settings_in_dir(dir: DirAccess, all_settings: PackedStringArray) -> void:
+       var base_dir: String = dir.get_current_dir()
+       var subdirs: PackedStringArray = dir.get_directories()
+       for subdir in subdirs:
+               if subdir.begins_with("_"):
+                       continue
+               
+               dir.change_dir(base_dir.path_join(subdir))
+               var templates: PackedStringArray = dir.get_files()
+               for template in templates:
+                       template = dir.get_current_dir().path_join(template)
+                       all_settings.append(template)
+               
+               _get_settings_in_dir(dir, all_settings)
+
+
+func _filter_setting_list(filter: String) -> void:
+       var to_remove: Array[int]
+       _load_settings()
+       
+       for item_index in range(SettingList.item_count):
+               var item_text: String = SettingList.get_item_text(item_index).to_lower()
+               
+               if not item_text.begins_with(filter.to_lower()):
+                       to_remove.push_front(item_index)
+       
+       for item_index in to_remove:
+               SettingList.remove_item(item_index)
+       
+       SettingList.sort_items_by_text()
+
+
+func _on_FilterField_text_changed(new_text: String) -> void:
+       _filter_setting_list(new_text)
+       NameField.editable = false
+
+
+### Recent List
+
+func _load_recent() -> void:
+       RecentList.clear()
+       
+       var list: Array[String] = ggsUtils.get_plugin_data().recent_settings
+       for item in list:
+               var item_index: int = RecentList.add_item(item.get_file().get_basename())
+               RecentList.set_item_metadata(item_index, item)
+               RecentList.set_item_tooltip(item_index, item)
+
+
+func _on_ClearRecentBtn_pressed() -> void:
+       ggsUtils.get_plugin_data().clear_recent_settings()
+       RecentList.clear()
diff --git a/addons/ggs/editor/add_setting_window/add_setting_window.tscn b/addons/ggs/editor/add_setting_window/add_setting_window.tscn
new file mode 100644 (file)
index 0000000..fc89378
--- /dev/null
@@ -0,0 +1,91 @@
+[gd_scene load_steps=4 format=3 uid="uid://111vt7wxn7lx"]
+
+[ext_resource type="Script" path="res://addons/ggs/editor/add_setting_window/add_setting_window.gd" id="1_338sp"]
+[ext_resource type="Texture2D" uid="uid://dbervsl0o0ifw" path="res://addons/ggs/assets/search.svg" id="1_th7u7"]
+[ext_resource type="Texture2D" uid="uid://bn0d5k8dd06qr" path="res://addons/ggs/assets/delete.svg" id="2_7ec7g"]
+
+[node name="AddSettingWindow" type="ConfirmationDialog"]
+title = "Add Setting from Template"
+size = Vector2i(550, 460)
+min_size = Vector2i(500, 450)
+ok_button_text = "Add"
+script = ExtResource("1_338sp")
+
+[node name="MainCtnr" type="VBoxContainer" parent="."]
+offset_left = 8.0
+offset_top = 8.0
+offset_right = 542.0
+offset_bottom = 411.0
+
+[node name="HBox" type="HBoxContainer" parent="MainCtnr"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="LeftCtnr" type="VBoxContainer" parent="MainCtnr/HBox"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 0.66
+
+[node name="TopBar" type="HBoxContainer" parent="MainCtnr/HBox/LeftCtnr"]
+layout_mode = 2
+
+[node name="RecentLabel" type="Label" parent="MainCtnr/HBox/LeftCtnr/TopBar"]
+layout_mode = 2
+size_flags_vertical = 0
+text = "Recent:"
+
+[node name="ClearRecentBtn" type="Button" parent="MainCtnr/HBox/LeftCtnr/TopBar"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 10
+tooltip_text = "Clear Recent"
+icon = ExtResource("2_7ec7g")
+flat = true
+
+[node name="RecentList" type="ItemList" parent="MainCtnr/HBox/LeftCtnr"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+size_flags_stretch_ratio = 0.66
+
+[node name="RightCtnr" type="VBoxContainer" parent="MainCtnr/HBox"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="TopBar" type="HBoxContainer" parent="MainCtnr/HBox/RightCtnr"]
+layout_mode = 2
+size_flags_vertical = 0
+
+[node name="AllLabel" type="Label" parent="MainCtnr/HBox/RightCtnr/TopBar"]
+layout_mode = 2
+text = "All Items:"
+
+[node name="FilterField" type="LineEdit" parent="MainCtnr/HBox/RightCtnr/TopBar"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(200, 0)
+layout_mode = 2
+size_flags_horizontal = 10
+placeholder_text = "Filter Items..."
+clear_button_enabled = true
+right_icon = ExtResource("1_th7u7")
+
+[node name="SettingList" type="ItemList" parent="MainCtnr/HBox/RightCtnr"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(0, 200)
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="NameCtnr" type="HBoxContainer" parent="MainCtnr"]
+layout_mode = 2
+theme_override_constants/separation = 5
+
+[node name="NameLabel" type="Label" parent="MainCtnr/NameCtnr"]
+layout_mode = 2
+text = "Name:"
+
+[node name="NameField" type="LineEdit" parent="MainCtnr/NameCtnr"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 8
diff --git a/addons/ggs/editor/category_panel/category_list.gd b/addons/ggs/editor/category_panel/category_list.gd
new file mode 100644 (file)
index 0000000..2a411c3
--- /dev/null
@@ -0,0 +1,54 @@
+@tool
+extends ItemList
+
+@onready var FSD: FileSystemDock = ggsUtils.get_file_system_dock()
+
+
+func _ready() -> void:
+       item_activated.connect(_on_item_activated)
+       item_selected.connect(_on_item_selected)
+       
+       GGS.dir_settings_change_occured.connect(_on_Global_dir_settings_change_occured)
+       
+       load_list()
+
+
+func load_list() -> void:
+       clear()
+       
+       var categories: PackedStringArray = _load_categories_from_filesystem()
+       for category in categories:
+               add_item(category)
+       
+       GGS.active_category = ""
+
+
+func _update_from_file_system() -> void:
+       load_list()
+
+
+func _on_item_selected(item_index: int) -> void:
+       var item_text: String = get_item_text(item_index)
+       GGS.active_category = item_text
+
+
+func _on_item_activated(item_index: int) -> void:
+       var item_text: String = get_item_text(item_index)
+       var path: String = ggsUtils.get_plugin_data().dir_settings.path_join(item_text)
+       ggsUtils.get_editor_interface().select_file(path)
+
+
+func _on_Global_dir_settings_change_occured() -> void:
+       _update_from_file_system()
+
+
+### Load Categories
+
+func _load_categories_from_filesystem() -> PackedStringArray:
+       var dir: DirAccess = DirAccess.open(ggsUtils.get_plugin_data().dir_settings)
+       var list: Array = Array(dir.get_directories()).filter(_remove_underscored)
+       return PackedStringArray(list)
+
+
+func _remove_underscored(element: String) -> bool:
+       return not element.begins_with("_")
diff --git a/addons/ggs/editor/category_panel/category_panel.gd b/addons/ggs/editor/category_panel/category_panel.gd
new file mode 100644 (file)
index 0000000..b45cddf
--- /dev/null
@@ -0,0 +1,49 @@
+@tool
+extends Control
+
+@export var Notification: AcceptDialog
+
+@onready var NCF: LineEdit = %NewCatField
+@onready var ReloadBtn: Button = %ReloadBtn
+@onready var List: ItemList = %CategoryList
+
+
+func _ready() -> void:
+       NCF.text_submitted.connect(_on_NCF_text_submitted)
+       ReloadBtn.pressed.connect(_on_ReloadBtn_pressed)
+
+
+### Category Creation
+
+func _create_category(cat_name: String) -> void:
+       var dir: DirAccess = DirAccess.open(ggsUtils.get_plugin_data().dir_settings)
+       dir.make_dir(cat_name)
+       
+       NCF.clear()
+
+
+func _on_NCF_text_submitted(submitted_text: String) -> void:
+       if (
+               not submitted_text.is_valid_filename() or
+               submitted_text.begins_with("_") or
+               submitted_text.begins_with(".")
+       ):
+               Notification.purpose = Notification.Purpose.INVALID
+               Notification.popup_centered()
+               return
+       
+       var dir: DirAccess = DirAccess.open(ggsUtils.get_plugin_data().dir_settings)
+       if dir.dir_exists(submitted_text):
+               Notification.purpose = Notification.Purpose.ALREADY_EXISTS
+               Notification.popup_centered()
+               return
+       
+       _create_category(submitted_text)
+       ggsUtils.get_resource_file_system().scan()
+       List.load_list()
+
+
+### Reload Btn
+
+func _on_ReloadBtn_pressed() -> void:
+       List.load_list()
diff --git a/addons/ggs/editor/category_panel/category_panel.tscn b/addons/ggs/editor/category_panel/category_panel.tscn
new file mode 100644 (file)
index 0000000..bb569bf
--- /dev/null
@@ -0,0 +1,51 @@
+[gd_scene load_steps=4 format=3 uid="uid://bkp77x1seytg7"]
+
+[ext_resource type="Script" path="res://addons/ggs/editor/category_panel/category_panel.gd" id="1_yu7o4"]
+[ext_resource type="Texture2D" uid="uid://ve54bl3r7ljc" path="res://addons/ggs/assets/reload.svg" id="2_so3ov"]
+[ext_resource type="Script" path="res://addons/ggs/editor/category_panel/category_list.gd" id="4_xqs08"]
+
+[node name="CategoryPanel" type="Control"]
+custom_minimum_size = Vector2(160, 0)
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_yu7o4")
+
+[node name="MainCtnr" type="VBoxContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="TopBar" type="HBoxContainer" parent="MainCtnr"]
+layout_mode = 2
+
+[node name="NewCatField" type="LineEdit" parent="MainCtnr/TopBar"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+placeholder_text = "New Category..."
+clear_button_enabled = true
+
+[node name="VSep" type="VSeparator" parent="MainCtnr/TopBar"]
+layout_mode = 2
+
+[node name="ReloadBtn" type="Button" parent="MainCtnr/TopBar"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Reload List"
+icon = ExtResource("2_so3ov")
+flat = true
+icon_alignment = 1
+
+[node name="CategoryList" type="ItemList" parent="MainCtnr"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+allow_reselect = true
+script = ExtResource("4_xqs08")
diff --git a/addons/ggs/editor/component_panel/component_panel.gd b/addons/ggs/editor/component_panel/component_panel.gd
new file mode 100644 (file)
index 0000000..94779c3
--- /dev/null
@@ -0,0 +1,91 @@
+@tool
+extends Control
+
+@onready var GroupField: LineEdit = %GroupField
+@onready var ASI: LineEdit = %ActiveSettingIndicator
+@onready var CASBtn: Button = %ClearActiveSettingBtn
+@onready var List: ItemList = %ComponentList
+
+
+func _ready() -> void:
+       List.item_activated.connect(_on_List_item_activated)
+       
+       CASBtn.pressed.connect(_on_CASBtn_pressed)
+       GGS.active_setting_changed.connect(_on_Global_active_setting_changed)
+       
+       _load_list()
+
+
+func _on_CASBtn_pressed() -> void:
+       GGS.active_setting = null
+
+
+func _on_Global_active_setting_changed() -> void:
+       ASI.text = GGS.active_setting.name if GGS.active_setting else ""
+
+
+### List Init
+
+func _load_list() -> void:
+       List.clear()
+       
+       var comp_list: Array[Dictionary] = _get_comp_list()
+       for component in comp_list:
+               var text: String = component["name"]
+               var meta: String = component["scene"]
+               
+               var item_index: int = List.add_item(text)
+               List.set_item_metadata(item_index, meta)
+       
+       List.sort_items_by_text()
+
+
+func _get_comp_list() -> Array[Dictionary]:
+       var comp_list: Array[Dictionary]
+       
+       var data: ggsPluginData = ggsUtils.get_plugin_data()
+       var components: PackedStringArray = DirAccess.get_directories_at(data.dir_components)
+       for component in components:
+               if component.begins_with("_"):
+                       continue
+               
+               var info: Dictionary
+               var path: String = data.dir_components.path_join(component)
+               
+               info["name"] = component.capitalize()
+               info["scene"] = path.path_join("%s.tscn"%component)
+               
+               comp_list.append(info)
+       
+       return comp_list
+
+
+### Component Instantiation
+
+func _on_List_item_activated(item_index: int) -> void:
+       var EI: EditorInterface = ggsUtils.get_editor_interface()
+       var ES: EditorSelection = EI.get_selection()
+       var selected_nodes: Array[Node] = ES.get_selected_nodes()
+       
+       if selected_nodes.size() != 1:
+               printerr("GGS - Instantiate Component: Exactly one item in the scene tree must be selected to instantiate a component.")
+               return
+       
+       var item_meta: String = List.get_item_metadata(item_index)
+       var SelectedNode: Node = selected_nodes[0]
+       var ESR: Node = EI.get_edited_scene_root()
+       
+       var comp_scene: PackedScene = load(item_meta)
+       var Component: Control = comp_scene.instantiate()
+       Component.setting = GGS.active_setting
+       Component.apply_on_change = ggsUtils.get_plugin_data().apply_on_changed_all
+       Component.grab_focus_on_mouse_over = ggsUtils.get_plugin_data().grab_focus_on_mouse_over_all
+       
+       SelectedNode.add_child(Component, true)
+       SelectedNode.set_editable_instance(Component, true)
+       Component.owner = ESR
+       
+       if not GroupField.text.strip_edges().is_empty():
+               Component.add_to_group(GroupField.text.strip_edges(), true)
+       
+       EI.save_scene()
diff --git a/addons/ggs/editor/component_panel/component_panel.tscn b/addons/ggs/editor/component_panel/component_panel.tscn
new file mode 100644 (file)
index 0000000..5ec9cca
--- /dev/null
@@ -0,0 +1,73 @@
+[gd_scene load_steps=3 format=3 uid="uid://cfr2j0ekmm5bm"]
+
+[ext_resource type="Script" path="res://addons/ggs/editor/component_panel/component_panel.gd" id="1_3cgut"]
+[ext_resource type="Texture2D" uid="uid://cflcng660hsp0" path="res://addons/ggs/assets/close.svg" id="3_wh32e"]
+
+[node name="ComponentPanel" type="Control"]
+custom_minimum_size = Vector2(170, 0)
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_3cgut")
+
+[node name="MainCtnr" type="VBoxContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="TopBar" type="HBoxContainer" parent="MainCtnr"]
+layout_mode = 2
+
+[node name="GroupField" type="LineEdit" parent="MainCtnr/TopBar"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+tooltip_text = "Node Group
+If not empty, newly created components are added to this node group."
+placeholder_text = "Node Group"
+clear_button_enabled = true
+
+[node name="ActiveSettingCtnr" type="HBoxContainer" parent="MainCtnr"]
+layout_mode = 2
+
+[node name="ActiveSettingIndicator" type="LineEdit" parent="MainCtnr/ActiveSettingCtnr"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+tooltip_text = "Active Setting
+If not empty, the setting displayed here will be assigned to the components you add."
+focus_mode = 0
+placeholder_text = "No Active Setting"
+editable = false
+
+[node name="ClearActiveSettingBtn" type="Button" parent="MainCtnr/ActiveSettingCtnr"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Clear Active Setting"
+icon = ExtResource("3_wh32e")
+flat = true
+
+[node name="ComponentList" type="ItemList" parent="MainCtnr"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+allow_reselect = true
+item_count = 10
+max_columns = 2
+same_column_width = true
+item_0/text = "Arrow List"
+item_1/text = "Checkbox"
+item_2/text = "Input Btn"
+item_3/text = "Option List"
+item_4/text = "Radio List"
+item_5/text = "Slider"
+item_6/text = "Spinbox"
+item_7/text = "Switch"
+item_8/text = "Text Field"
+item_9/text = "Toggle Btn"
diff --git a/addons/ggs/editor/input_selector/input_list.gd b/addons/ggs/editor/input_selector/input_list.gd
new file mode 100644 (file)
index 0000000..f65e2c0
--- /dev/null
@@ -0,0 +1,52 @@
+@tool
+extends Tree
+
+var root: TreeItem
+
+
+func load_list() -> void:
+       clear()
+       root = create_item()
+       
+       var input_map: Dictionary = _get_input_map()
+       for action in input_map.keys():
+               var action_item: TreeItem = _create_item(root, action)
+               
+               var index: int = 0
+               for event in input_map[action]:
+                       _create_item(action_item, event.as_text(), {"event": event, "index": index})
+                       index += 1
+
+
+func set_collapsed_all(collapsed: bool) -> void:
+       var items: Array[TreeItem] = root.get_children()
+       for item in items:
+               item.set_collapsed_recursive(collapsed)
+
+
+func _get_input_map() -> Dictionary:
+       var input_map: Dictionary
+       
+       var project_file: ConfigFile = ConfigFile.new()
+       project_file.load("res://project.godot")
+       
+       var actions: PackedStringArray = project_file.get_section_keys("input")
+       for action in actions:
+               var action_properties: Dictionary = project_file.get_value("input", action)
+               var action_events: Array = action_properties["events"]
+               
+               input_map[action] = action_events
+       
+       return input_map
+
+
+func _create_item(parent: TreeItem, text: String, meta: Dictionary = {}) -> TreeItem:
+       var created_item: TreeItem = create_item(parent)
+       created_item.set_text(0, text)
+       
+       if meta.is_empty():
+               created_item.set_selectable(0, false)
+       else:
+               created_item.set_metadata(0, meta)
+       
+       return created_item
diff --git a/addons/ggs/editor/input_selector/input_selector.gd b/addons/ggs/editor/input_selector/input_selector.gd
new file mode 100644 (file)
index 0000000..943c6a3
--- /dev/null
@@ -0,0 +1,93 @@
+@tool
+extends MarginContainer
+
+var inspected_obj: ggsInputSetting
+
+@onready var SelectBtn: Button = %SelectBtn
+
+@onready var SIW: ConfirmationDialog = %SelectInputWindow
+@onready var SearchField: LineEdit = %SearchField
+@onready var CollapseAllBtn: Button = %CollapseAllBtn
+@onready var ExpandAllBtn: Button = %ExpandAllBtn
+@onready var List: Tree = %InputList
+
+@onready var OkBtn: Button = SIW.get_ok_button()
+
+
+func _ready() -> void:
+       SelectBtn.pressed.connect(_on_SelectBtn_pressed)
+       
+       SIW.about_to_popup.connect(_on_SIW_about_to_popup)
+       SIW.confirmed.connect(_on_SIW_confirmed)
+       SearchField.text_changed.connect(_on_SearchField_text_changed)
+       CollapseAllBtn.pressed.connect(_on_CollapseAllBtn_pressed)
+       ExpandAllBtn.pressed.connect(_on_ExpandAllBtn_pressed)
+       
+       List.item_selected.connect(_on_List_item_selected)
+       List.item_activated.connect(_on_List_item_activated)
+
+
+func _on_SelectBtn_pressed() -> void:
+       SIW.popup_centered()
+
+
+func _on_SIW_about_to_popup() -> void:
+       List.load_list()
+       OkBtn.disabled = true
+       SearchField.clear()
+       List.set_collapsed_all(true)
+
+
+func _on_List_item_selected() -> void:
+       var selected_item: TreeItem = List.get_selected()
+       OkBtn.disabled = false
+
+
+### Item Selection
+
+func _confirm(item: TreeItem) -> void:
+       inspected_obj.action = item.get_parent().get_text(0)
+       inspected_obj.event_index = item.get_metadata(0)["index"]
+       inspected_obj.default_as_event = item.get_metadata(0)["event"]
+       inspected_obj.current_as_event = item.get_metadata(0)["event"].duplicate()
+
+
+func _on_SIW_confirmed() -> void:
+       var selected_item: TreeItem = List.get_selected()
+       _confirm(selected_item)
+
+
+func _on_List_item_activated() -> void:
+       var selected_item: TreeItem = List.get_selected()
+       _confirm(selected_item)
+       SIW.visible = false
+
+
+### List Filtering
+
+func _filter_list(filter: String) -> void:
+       List.load_list()
+       OkBtn.disabled = true
+       
+       filter = filter.to_lower()
+       var items: Array[TreeItem] = List.root.get_children()
+       for item in items:
+               var item_name: String = item.get_text(0).to_lower()
+               if item_name.begins_with(filter):
+                       continue
+               
+               item.free()
+
+
+func _on_SearchField_text_changed(new_text: String) -> void:
+       _filter_list(new_text)
+
+
+### Collapse/Expand Buttons
+
+func _on_CollapseAllBtn_pressed() -> void:
+       List.set_collapsed_all(true)
+
+
+func _on_ExpandAllBtn_pressed() -> void:
+       List.set_collapsed_all(false)
diff --git a/addons/ggs/editor/input_selector/input_selector.tscn b/addons/ggs/editor/input_selector/input_selector.tscn
new file mode 100644 (file)
index 0000000..0827040
--- /dev/null
@@ -0,0 +1,72 @@
+[gd_scene load_steps=6 format=3 uid="uid://bqymtf8fuyj54"]
+
+[ext_resource type="Script" path="res://addons/ggs/editor/input_selector/input_selector.gd" id="1_qjksw"]
+[ext_resource type="Texture2D" uid="uid://dbervsl0o0ifw" path="res://addons/ggs/assets/search.svg" id="2_cgd6a"]
+[ext_resource type="Script" path="res://addons/ggs/editor/input_selector/input_list.gd" id="3_jsbwa"]
+[ext_resource type="Texture2D" uid="uid://l0mve5lc0okm" path="res://addons/ggs/assets/collapse_all.svg" id="3_mxhgu"]
+[ext_resource type="Texture2D" uid="uid://caajrkkuvle0e" path="res://addons/ggs/assets/expand_all.svg" id="4_3d11l"]
+
+[node name="Margin" type="MarginContainer"]
+offset_right = 100.0
+theme_override_constants/margin_top = 5
+theme_override_constants/margin_bottom = 5
+script = ExtResource("1_qjksw")
+
+[node name="InputSelector" type="VBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="SelectBtn" type="Button" parent="InputSelector"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Select Input"
+
+[node name="VSeparator" type="HSeparator" parent="InputSelector"]
+layout_mode = 2
+theme_override_constants/separation = 0
+
+[node name="SelectInputWindow" type="ConfirmationDialog" parent="."]
+unique_name_in_owner = true
+title = "Select Input"
+size = Vector2i(700, 500)
+min_size = Vector2i(700, 500)
+ok_button_text = "Select"
+
+[node name="MainCtnr" type="VBoxContainer" parent="SelectInputWindow"]
+offset_left = 8.0
+offset_top = 8.0
+offset_right = 692.0
+offset_bottom = 451.0
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="TopBar" type="HBoxContainer" parent="SelectInputWindow/MainCtnr"]
+layout_mode = 2
+
+[node name="SearchField" type="LineEdit" parent="SelectInputWindow/MainCtnr/TopBar"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+placeholder_text = "Filter Actions"
+clear_button_enabled = true
+right_icon = ExtResource("2_cgd6a")
+
+[node name="CollapseAllBtn" type="Button" parent="SelectInputWindow/MainCtnr/TopBar"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Collapse All"
+icon = ExtResource("3_mxhgu")
+flat = true
+
+[node name="ExpandAllBtn" type="Button" parent="SelectInputWindow/MainCtnr/TopBar"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Expand All"
+icon = ExtResource("4_3d11l")
+flat = true
+
+[node name="InputList" type="Tree" parent="SelectInputWindow/MainCtnr"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+hide_root = true
+script = ExtResource("3_jsbwa")
diff --git a/addons/ggs/editor/main_panel/bug_btn.gd b/addons/ggs/editor/main_panel/bug_btn.gd
new file mode 100644 (file)
index 0000000..9bb6ec7
--- /dev/null
@@ -0,0 +1,12 @@
+@tool
+extends Button
+
+const URI: String = "https://github.com/PunchablePlushie/godot-game-settings/issues"
+
+
+func _ready() -> void:
+       pressed.connect(_on_pressed)
+
+
+func _on_pressed() -> void:
+       OS.shell_open(URI)
diff --git a/addons/ggs/editor/main_panel/docs_btn.gd b/addons/ggs/editor/main_panel/docs_btn.gd
new file mode 100644 (file)
index 0000000..41aabc0
--- /dev/null
@@ -0,0 +1,12 @@
+@tool
+extends Button
+
+const URI: String = "https://github.com/PunchablePlushie/godot-game-settings/tree/main/docs/home.md"
+
+
+func _ready() -> void:
+       pressed.connect(_on_pressed)
+
+
+func _on_pressed() -> void:
+       OS.shell_open(URI)
diff --git a/addons/ggs/editor/main_panel/feedback_btn.gd b/addons/ggs/editor/main_panel/feedback_btn.gd
new file mode 100644 (file)
index 0000000..f9ab016
--- /dev/null
@@ -0,0 +1,12 @@
+@tool
+extends Button
+
+const URI: String = "https://forms.gle/c8XQzKHEqeMqxJ3Z9"
+
+
+func _ready() -> void:
+       pressed.connect(_on_pressed)
+
+
+func _on_pressed() -> void:
+       OS.shell_open(URI)
diff --git a/addons/ggs/editor/main_panel/main_panel.tscn b/addons/ggs/editor/main_panel/main_panel.tscn
new file mode 100644 (file)
index 0000000..94d9ad6
--- /dev/null
@@ -0,0 +1,178 @@
+[gd_scene load_steps=20 format=3 uid="uid://buovvisskoqxh"]
+
+[ext_resource type="Theme" uid="uid://c4pwg7lhukqb8" path="res://addons/ggs/editor/_theme/ggs_theme.tres" id="1_w3bfk"]
+[ext_resource type="PackedScene" uid="uid://bkp77x1seytg7" path="res://addons/ggs/editor/category_panel/category_panel.tscn" id="4_4p007"]
+[ext_resource type="Script" path="res://addons/ggs/editor/main_panel/split_containers.gd" id="4_mplwh"]
+[ext_resource type="PackedScene" uid="uid://cfr2j0ekmm5bm" path="res://addons/ggs/editor/component_panel/component_panel.tscn" id="5_7xo6y"]
+[ext_resource type="PackedScene" uid="uid://vt5mwwxhtu3x" path="res://addons/ggs/editor/setting_panel/setting_panel.tscn" id="6_rabjj"]
+[ext_resource type="Texture2D" uid="uid://b8o243gwa707v" path="res://addons/ggs/assets/icon_mono.svg" id="6_t7de8"]
+[ext_resource type="Script" path="res://addons/ggs/editor/main_panel/save_file_menu.gd" id="7_guojl"]
+[ext_resource type="Texture2D" uid="uid://bx8yoim3ur6h" path="res://addons/ggs/assets/save_file.svg" id="8_3r2nn"]
+[ext_resource type="Texture2D" uid="uid://cdxv6r8uy2me5" path="res://addons/ggs/assets/docs.svg" id="8_mdi0r"]
+[ext_resource type="Script" path="res://addons/ggs/editor/main_panel/pref_btn.gd" id="9_oodag"]
+[ext_resource type="Script" path="res://addons/ggs/editor/main_panel/docs_btn.gd" id="9_qwavr"]
+[ext_resource type="PackedScene" uid="uid://c42mh74d7l2rt" path="res://addons/ggs/editor/pref_window/pref_window.tscn" id="10_ir36c"]
+[ext_resource type="Texture2D" uid="uid://bt7gdorkvo4an" path="res://addons/ggs/assets/bug.svg" id="10_rly1w"]
+[ext_resource type="Script" path="res://addons/ggs/editor/main_panel/bug_btn.gd" id="11_2ygfj"]
+[ext_resource type="Script" path="res://addons/ggs/editor/main_panel/feedback_btn.gd" id="15_0jgkh"]
+[ext_resource type="Texture2D" uid="uid://c5a5taq8d2n0v" path="res://addons/ggs/assets/feedback.svg" id="16_abywd"]
+[ext_resource type="Script" path="res://addons/ggs/editor/main_panel/progress_overlay.gd" id="16_hfs01"]
+[ext_resource type="Script" path="res://addons/ggs/editor/main_panel/notification.gd" id="18_ky7ax"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_jbg8d"]
+
+[node name="MainPanel" type="Control"]
+custom_minimum_size = Vector2(0, 300)
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme = ExtResource("1_w3bfk")
+
+[node name="MainCtnr" type="HBoxContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="HSplit_0" type="HSplitContainer" parent="MainCtnr"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+split_offset = -315
+script = ExtResource("4_mplwh")
+
+[node name="CategoryPanel" parent="MainCtnr/HSplit_0" node_paths=PackedStringArray("Notification") instance=ExtResource("4_4p007")]
+layout_mode = 2
+size_flags_horizontal = 3
+Notification = NodePath("../../../Notification")
+
+[node name="HSplit_1" type="HSplitContainer" parent="MainCtnr/HSplit_0"]
+layout_mode = 2
+size_flags_horizontal = 3
+split_offset = 615
+script = ExtResource("4_mplwh")
+
+[node name="SettingPanel" parent="MainCtnr/HSplit_0/HSplit_1" node_paths=PackedStringArray("Notification") instance=ExtResource("6_rabjj")]
+layout_mode = 2
+Notification = NodePath("../../../../Notification")
+
+[node name="ComponentPanel" parent="MainCtnr/HSplit_0/HSplit_1" instance=ExtResource("5_7xo6y")]
+layout_mode = 2
+
+[node name="VSeparator" type="VSeparator" parent="MainCtnr"]
+layout_mode = 2
+
+[node name="BtnCtnr" type="VBoxContainer" parent="MainCtnr"]
+layout_mode = 2
+size_flags_horizontal = 8
+
+[node name="TopCtnr" type="VBoxContainer" parent="MainCtnr/BtnCtnr"]
+layout_mode = 2
+
+[node name="SaveFileMenu" type="MenuButton" parent="MainCtnr/BtnCtnr/TopCtnr"]
+layout_mode = 2
+tooltip_text = "Save File"
+icon = ExtResource("8_3r2nn")
+item_count = 4
+popup/item_0/text = "Open Save File"
+popup/item_0/id = 0
+popup/item_1/text = ""
+popup/item_1/id = 999
+popup/item_1/separator = true
+popup/item_2/text = "Remake from Current"
+popup/item_2/id = 1
+popup/item_3/text = "Remake from Default"
+popup/item_3/id = 2
+script = ExtResource("7_guojl")
+
+[node name="PrefBtn" type="Button" parent="MainCtnr/BtnCtnr/TopCtnr"]
+layout_mode = 2
+size_flags_vertical = 10
+tooltip_text = "Preferences"
+icon = ExtResource("6_t7de8")
+flat = true
+script = ExtResource("9_oodag")
+
+[node name="BotCtnr" type="VBoxContainer" parent="MainCtnr/BtnCtnr"]
+layout_mode = 2
+size_flags_vertical = 10
+
+[node name="DocsBtn" type="Button" parent="MainCtnr/BtnCtnr/BotCtnr"]
+layout_mode = 2
+size_flags_vertical = 10
+tooltip_text = "View Documentation"
+icon = ExtResource("8_mdi0r")
+flat = true
+script = ExtResource("9_qwavr")
+
+[node name="BugBtn" type="Button" parent="MainCtnr/BtnCtnr/BotCtnr"]
+layout_mode = 2
+size_flags_vertical = 10
+tooltip_text = "Report an Issue"
+icon = ExtResource("10_rly1w")
+flat = true
+script = ExtResource("11_2ygfj")
+
+[node name="FeedbackBtn" type="Button" parent="MainCtnr/BtnCtnr/BotCtnr"]
+layout_mode = 2
+tooltip_text = "Send Feedback"
+icon = ExtResource("16_abywd")
+flat = true
+script = ExtResource("15_0jgkh")
+
+[node name="ProgressOverlay" type="PanelContainer" parent="."]
+visible = false
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_default_cursor_shape = 4
+theme_override_styles/panel = SubResource("StyleBoxEmpty_jbg8d")
+script = ExtResource("16_hfs01")
+label_save_file_current = "Remaking Save File from Current Values"
+label_save_file_default = "Remaking Save File from Default Values"
+label_add_multiple_settings = "Adding Setting(s)"
+
+[node name="ProgBG" type="ColorRect" parent="ProgressOverlay"]
+layout_mode = 2
+color = Color(0, 0, 0, 0.784314)
+
+[node name="Center" type="CenterContainer" parent="ProgressOverlay"]
+layout_mode = 2
+
+[node name="VBox" type="VBoxContainer" parent="ProgressOverlay/Center"]
+layout_mode = 2
+
+[node name="ProgLabel" type="Label" parent="ProgressOverlay/Center/VBox"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Remaking Save File from Default Values"
+
+[node name="ProgBar" type="ProgressBar" parent="ProgressOverlay/Center/VBox"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(250, 0)
+layout_mode = 2
+mouse_default_cursor_shape = 4
+
+[node name="PrefWindow" parent="." instance=ExtResource("10_ir36c")]
+unique_name_in_owner = true
+
+[node name="Notification" type="AcceptDialog" parent="."]
+size = Vector2i(687, 100)
+unresizable = true
+max_size = Vector2i(700, 16384)
+dialog_autowrap = true
+script = ExtResource("18_ky7ax")
+title_invalid = "Invalid Item Name"
+title_already_exists = "Item Already Exists"
+msg_invalid = "The item name must be a valid file name and cannot start with an underscore (\"_\")  or dot (\".\")."
+msg_already_exists = "An item with this name already exists."
diff --git a/addons/ggs/editor/main_panel/notification.gd b/addons/ggs/editor/main_panel/notification.gd
new file mode 100644 (file)
index 0000000..9267429
--- /dev/null
@@ -0,0 +1,26 @@
+@tool
+extends AcceptDialog
+
+enum Purpose {INVALID, ALREADY_EXISTS}
+
+@export_group("Title", "title_")
+@export var title_invalid: String
+@export var title_already_exists: String
+@export_group("Message", "msg_")
+@export_multiline var msg_invalid: String
+@export_multiline var msg_already_exists: String
+
+
+var purpose: int : set = set_purpose
+
+
+func set_purpose(value: int) -> void:
+       purpose = value
+       
+       match value:
+               Purpose.INVALID:
+                       title = title_invalid
+                       dialog_text = msg_invalid
+               Purpose.ALREADY_EXISTS:
+                       title = title_already_exists
+                       dialog_text = msg_already_exists
diff --git a/addons/ggs/editor/main_panel/pref_btn.gd b/addons/ggs/editor/main_panel/pref_btn.gd
new file mode 100644 (file)
index 0000000..336a0da
--- /dev/null
@@ -0,0 +1,12 @@
+@tool
+extends Button
+
+@onready var PrefWindow: Window = %PrefWindow
+
+
+func _ready() -> void:
+       pressed.connect(_on_pressed)
+
+
+func _on_pressed() -> void:
+       PrefWindow.popup_centered(PrefWindow.min_size)
diff --git a/addons/ggs/editor/main_panel/progress_overlay.gd b/addons/ggs/editor/main_panel/progress_overlay.gd
new file mode 100644 (file)
index 0000000..1c6a38d
--- /dev/null
@@ -0,0 +1,48 @@
+@tool
+extends PanelContainer
+
+@export_multiline var label_save_file_current: String
+@export_multiline var label_save_file_default: String
+@export_multiline var label_add_multiple_settings: String
+
+var type: int
+
+@onready var ProgLabel: Label = %ProgLabel
+@onready var ProgBar: ProgressBar = %ProgBar
+
+
+func _ready() -> void:
+       GGS.progress_started.connect(_on_Global_progress_started)
+       GGS.progress_advanced.connect(_on_Global_progress_advanced)
+       GGS.progress_ended.connect(_on_Global_progress_ended)
+       
+       visible = false
+       ProgBar.value = 0
+
+
+func _on_Global_progress_started(progress_type: int) -> void:
+       visible = true
+       ProgBar.visible = true
+       type = progress_type
+       
+       match type:
+               GGS.Progress.SAVE_FILE_CURRENT:
+                       ProgLabel.text = label_save_file_current
+               GGS.Progress.SAVE_FILE_DEFAULT:
+                       ProgLabel.text = label_save_file_default
+               GGS.Progress.ADD_SETTINGS:
+                       ProgLabel.text = label_add_multiple_settings
+                       ProgBar.visible = false
+       
+
+
+func _on_Global_progress_ended() -> void:
+       # A small delay to make sure progress_ended happens after progress_started
+       await get_tree().create_timer(0.01).timeout
+       
+       visible = false
+       ProgBar.value = 0
+
+
+func _on_Global_progress_advanced(progress: float) -> void:
+       ProgBar.value = progress
diff --git a/addons/ggs/editor/main_panel/save_file_menu.gd b/addons/ggs/editor/main_panel/save_file_menu.gd
new file mode 100644 (file)
index 0000000..2a10139
--- /dev/null
@@ -0,0 +1,28 @@
+@tool
+extends MenuButton
+
+enum MenuItems {OPEN, REMAKE_CURRENT, REMAKE_DEFAULT}
+
+var Menu: PopupMenu = get_popup()
+
+
+func _ready() -> void:
+       Menu.id_pressed.connect(_on_Menu_id_pressed)
+
+
+func _open_save_file() -> void:
+       var data: ggsPluginData = ggsUtils.get_plugin_data()
+       var path: String = ProjectSettings.globalize_path(data.dir_save_file)
+       var err: Error = OS.shell_open(path)
+       if err != OK:
+               printerr("GGS - Open Save File: An error has occured while opening the file. Code: %s"%[error_string(err)])
+
+
+func _on_Menu_id_pressed(id: int) -> void:
+       match id:
+               MenuItems.OPEN:
+                       _open_save_file()
+               MenuItems.REMAKE_CURRENT:
+                       GGS.request_update_save_file()
+               MenuItems.REMAKE_DEFAULT:
+                       GGS.request_update_save_file_default()
diff --git a/addons/ggs/editor/main_panel/split_containers.gd b/addons/ggs/editor/main_panel/split_containers.gd
new file mode 100644 (file)
index 0000000..d87e421
--- /dev/null
@@ -0,0 +1,24 @@
+@tool
+extends HSplitContainer
+
+@onready var property: String = _get_property()
+
+
+func _ready() -> void:
+       dragged.connect(_on_dragged)
+       
+       var data: ggsPluginData = ggsUtils.get_plugin_data()
+       split_offset = data.get(property)
+
+
+### Private
+func _get_property() -> StringName:
+       return "split_offset_%s"%name.split("_")[1]
+
+
+### Signals
+func _on_dragged(offset: int) -> void:
+       clamp_split_offset()
+       
+       var data: ggsPluginData = ggsUtils.get_plugin_data()
+       data.set_property(property, offset)
diff --git a/addons/ggs/editor/main_panel/update_theme_btn.gd b/addons/ggs/editor/main_panel/update_theme_btn.gd
new file mode 100644 (file)
index 0000000..32e736d
--- /dev/null
@@ -0,0 +1,12 @@
+@tool
+extends Button
+
+@onready var ggs_theme: Theme = preload("res://addons/ggs/editor/_theme/ggs_theme.tres")
+
+
+func _ready() -> void:
+       pressed.connect(_on_pressed)
+
+
+func _on_pressed() -> void:
+       ggs_theme.update()
diff --git a/addons/ggs/editor/pref_window/pref_window.gd b/addons/ggs/editor/pref_window/pref_window.gd
new file mode 100644 (file)
index 0000000..92a4183
--- /dev/null
@@ -0,0 +1,177 @@
+@tool
+extends Window
+
+enum DirTarget {SETTINGS, COMPONENTS, TEMPLATES}
+enum ConfirmPurpose {RESET, OK}
+
+const THEME: Theme = preload("res://addons/ggs/editor/_theme/ggs_theme.tres")
+const TEMPLATE: Script = preload("res://addons/ggs/template.gd")
+const GGS_SCENE: String = "res://addons/ggs/classes/global/ggs.tscn"
+
+@export_multiline var reset_text: String
+@export_multiline var ok_text: String
+
+@onready var OkBtn: Button = %OkBtn
+@onready var CancelBtn: Button = %CancelBtn
+
+@onready var SDF: LineEdit = %SettingDirField
+@onready var CDF: LineEdit = %CompDirField
+@onready var TDF: LineEdit = %TemplatesDirField
+@onready var SFNF: LineEdit = %SaveFileNameField
+@onready var SFEF: LineEdit = %SaveFileExtensionField
+
+@onready var SDB: Button = %SettingDirBtn
+@onready var CDB: Button = %CompDirBtn
+@onready var TDB: Button = %TemplatesDirBtn
+@onready var DSW: FileDialog = $DirSelectionWindow
+
+@onready var ApplyOnChanged: CheckBox = %ApplyOnChanged
+@onready var GrabFocusOnMouseOver: CheckBox = %GrabFocusOnMouseOver
+@onready var SetSFXBtn: Button = %SetSFXBtn
+
+@onready var UpdateThemeBtn: Button = %UpdateThemeBtn
+@onready var BaseTemplateBtn: Button = %BaseTemplateBtn
+@onready var ResetBtn: Button = %ResetBtn
+@onready var CRW: ConfirmationDialog = $ConfirmWindow
+
+@onready var VersionBtn: Button = %VersionBtn
+@onready var ChangelogBtn: Button = %ChangelogBtn
+
+
+func _ready() -> void:
+       about_to_popup.connect(_on_about_to_popup)
+       close_requested.connect(_on_close_requested)
+       CancelBtn.pressed.connect(_on_close_requested)
+       OkBtn.pressed.connect(_on_OkBtn_pressed)
+       
+       SDB.pressed.connect(_on_AnyDirectoryBtn_pressed.bind(SDB))
+       CDB.pressed.connect(_on_AnyDirectoryBtn_pressed.bind(CDB))
+       TDB.pressed.connect(_on_AnyDirectoryBtn_pressed.bind(TDB))
+       DSW.dir_selected.connect(_on_DSW_dir_selected)
+       
+       UpdateThemeBtn.pressed.connect(_on_UpdateThemeBtn_pressed)
+       BaseTemplateBtn.pressed.connect(_on_BaseTemplateBtn_pressed)
+       ResetBtn.pressed.connect(_on_ResetBtn_pressed)
+       CRW.confirmed.connect(_on_CRW_confirmed)
+       
+       SetSFXBtn.pressed.connect(_on_SetSFXBtn_pressed)
+       
+       VersionBtn.pressed.connect(_on_VersionBtn_pressed)
+       ChangelogBtn.pressed.connect(_on_ChangelogBtn_pressed)
+       
+       hide()
+
+
+### Info Buttons
+
+func _on_VersionBtn_pressed() -> void:
+       var URI: String = "https://github.com/PunchablePlushie/godot-game-settings/releases"
+       OS.shell_open(URI)
+
+
+func _on_ChangelogBtn_pressed() -> void:
+       var URI: String = "https://github.com/PunchablePlushie/godot-game-settings/tree/main/docs/changelog.md"
+       OS.shell_open(URI)
+
+
+### Fields
+
+func _init_values() -> void:
+       var data: ggsPluginData = ggsUtils.get_plugin_data()
+       SDF.text = data.dir_settings
+       CDF.text = data.dir_components
+       TDF.text = data.dir_templates
+       ApplyOnChanged.button_pressed = data.apply_on_changed_all
+       GrabFocusOnMouseOver.button_pressed = data.grab_focus_on_mouse_over_all
+       
+       var value: String = data.dir_save_file
+       SFNF.text = value.get_file().get_basename()
+       SFEF.text = value.get_extension()
+
+
+func _on_AnyDirectoryBtn_pressed(src: Button) -> void:
+       var target: String
+       
+       match src:
+               SDB:
+                       DSW.set_meta("target", DirTarget.SETTINGS)
+               CDB:
+                       DSW.set_meta("target", DirTarget.COMPONENTS)
+               TDB:
+                       DSW.set_meta("target", DirTarget.TEMPLATES)
+       
+       DSW.invalidate()
+       DSW.popup_centered()
+
+
+func _on_DSW_dir_selected(dir: String) -> void:
+       var target_field: LineEdit
+       match DSW.get_meta("target"):
+               DirTarget.SETTINGS:
+                       target_field = SDF
+               DirTarget.COMPONENTS:
+                       target_field = CDF
+               DirTarget.TEMPLATES:
+                       target_field = TDF
+       
+       target_field.text = dir
+
+
+### Buttons
+
+func _on_SetSFXBtn_pressed() -> void:
+       ggsUtils.get_editor_interface().open_scene_from_path(GGS_SCENE)
+       hide()
+
+
+func _on_UpdateThemeBtn_pressed() -> void:
+       THEME.update()
+
+
+func _on_BaseTemplateBtn_pressed() -> void:
+       ggsUtils.get_editor_interface().inspect_object(TEMPLATE)
+       hide()
+
+
+func _on_ResetBtn_pressed() -> void:
+       CRW.dialog_text = reset_text
+       CRW.set_meta("purpose", ConfirmPurpose.RESET)
+       CRW.popup_centered()
+
+
+func _on_OkBtn_pressed() -> void:
+       CRW.dialog_text = ok_text
+       CRW.set_meta("purpose", ConfirmPurpose.OK)
+       CRW.popup_centered()
+
+
+func _on_CRW_confirmed() -> void:
+       match CRW.get_meta("purpose"):
+               ConfirmPurpose.RESET:
+                       ggsUtils.get_plugin_data().reset()
+                       
+                       hide()
+                       ggsUtils.get_editor_interface().set_plugin_enabled("ggs", false)
+               ConfirmPurpose.OK:
+                       var data: ggsPluginData = ggsUtils.get_plugin_data()
+                       data.set_property("dir_settings", SDF.text)
+                       data.set_property("dir_components", CDF.text)
+                       data.set_property("dir_templates", TDF.text)
+                       data.set_property("apply_on_changed_all", ApplyOnChanged.button_pressed)
+                       data.set_property("grab_focus_on_mouse_over_all", GrabFocusOnMouseOver.button_pressed)
+                       
+                       var value: String = "user://%s.%s"%[SFNF.text, SFEF.text]
+                       data.set_property("dir_save_file", value)
+                       
+                       hide()
+                       ggsUtils.get_editor_interface().set_plugin_enabled("ggs", false)
+
+
+### Window Functionalities
+
+func _on_about_to_popup() -> void:
+       _init_values()
+
+
+func _on_close_requested() -> void:
+       hide()
diff --git a/addons/ggs/editor/pref_window/pref_window.tscn b/addons/ggs/editor/pref_window/pref_window.tscn
new file mode 100644 (file)
index 0000000..7387667
--- /dev/null
@@ -0,0 +1,300 @@
+[gd_scene load_steps=5 format=3 uid="uid://c42mh74d7l2rt"]
+
+[ext_resource type="Script" path="res://addons/ggs/editor/pref_window/pref_window.gd" id="1_ihv6i"]
+[ext_resource type="Texture2D" uid="uid://badl61ealw70o" path="res://addons/ggs/assets/file_dialog.svg" id="2_qx4su"]
+[ext_resource type="Texture2D" uid="uid://bk0u7p6a1apta" path="res://addons/ggs/assets/icon_mini.svg" id="3_ggsp5"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ki5am"]
+
+[node name="PrefWindow" type="Window"]
+title = "GGS Preferences"
+position = Vector2i(0, 36)
+size = Vector2i(650, 500)
+visible = false
+wrap_controls = true
+exclusive = true
+min_size = Vector2i(650, 500)
+script = ExtResource("1_ihv6i")
+reset_text = "Are you sure you want to reset settings?
+A plugin restart is required and the plugin will be disabled automatically if confirmed."
+ok_text = "Confirm changes?
+A plugin restart is required and the plugin will be disabled automatically if confirmed."
+
+[node name="BgPanel" type="PanelContainer" parent="."]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_type_variation = &"PrefWindowBG"
+
+[node name="Margin" type="MarginContainer" parent="BgPanel"]
+layout_mode = 2
+theme_override_constants/margin_left = 5
+theme_override_constants/margin_top = 5
+theme_override_constants/margin_right = 5
+theme_override_constants/margin_bottom = 5
+
+[node name="MainCtnr" type="VBoxContainer" parent="BgPanel/Margin"]
+layout_mode = 2
+
+[node name="ScrollCtnr" type="ScrollContainer" parent="BgPanel/Margin/MainCtnr"]
+layout_mode = 2
+size_flags_vertical = 3
+follow_focus = true
+horizontal_scroll_mode = 0
+
+[node name="SettingsCtnr" type="VBoxContainer" parent="BgPanel/Margin/MainCtnr/ScrollCtnr"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="DirectoriesSection" type="VBoxContainer" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr"]
+custom_minimum_size = Vector2(0, 150)
+layout_mode = 2
+
+[node name="SectionLabel" type="Label" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/DirectoriesSection"]
+layout_mode = 2
+text = "Directories"
+horizontal_alignment = 1
+
+[node name="HSep" type="HSeparator" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/DirectoriesSection"]
+layout_mode = 2
+
+[node name="SettingsDir" type="HBoxContainer" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/DirectoriesSection"]
+layout_mode = 2
+
+[node name="SettingDirLabel" type="Label" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/DirectoriesSection/SettingsDir"]
+custom_minimum_size = Vector2(180, 0)
+layout_mode = 2
+text = "Settings:"
+
+[node name="SettingDirField" type="LineEdit" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/DirectoriesSection/SettingsDir"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+caret_blink = true
+caret_blink_interval = 0.5
+
+[node name="SettingDirBtn" type="Button" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/DirectoriesSection/SettingsDir"]
+unique_name_in_owner = true
+layout_mode = 2
+icon = ExtResource("2_qx4su")
+
+[node name="ComponentDir" type="HBoxContainer" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/DirectoriesSection"]
+layout_mode = 2
+
+[node name="CompDirLabel" type="Label" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/DirectoriesSection/ComponentDir"]
+custom_minimum_size = Vector2(180, 0)
+layout_mode = 2
+text = "Components:"
+
+[node name="CompDirField" type="LineEdit" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/DirectoriesSection/ComponentDir"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+caret_blink = true
+caret_blink_interval = 0.5
+
+[node name="CompDirBtn" type="Button" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/DirectoriesSection/ComponentDir"]
+unique_name_in_owner = true
+layout_mode = 2
+icon = ExtResource("2_qx4su")
+
+[node name="TemplatesDir" type="HBoxContainer" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/DirectoriesSection"]
+layout_mode = 2
+
+[node name="TemplatesDirLabel" type="Label" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/DirectoriesSection/TemplatesDir"]
+custom_minimum_size = Vector2(180, 0)
+layout_mode = 2
+text = "Templates:"
+
+[node name="TemplatesDirField" type="LineEdit" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/DirectoriesSection/TemplatesDir"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+caret_blink = true
+caret_blink_interval = 0.5
+
+[node name="TemplatesDirBtn" type="Button" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/DirectoriesSection/TemplatesDir"]
+unique_name_in_owner = true
+layout_mode = 2
+icon = ExtResource("2_qx4su")
+
+[node name="SaveFileDir" type="HBoxContainer" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/DirectoriesSection"]
+layout_mode = 2
+size_flags_vertical = 8
+
+[node name="SaveFileDirLabel" type="Label" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/DirectoriesSection/SaveFileDir"]
+custom_minimum_size = Vector2(180, 0)
+layout_mode = 2
+text = "Save File:"
+
+[node name="HBox" type="HBoxContainer" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/DirectoriesSection/SaveFileDir"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="SaveFileBaseDirLabel" type="Label" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/DirectoriesSection/SaveFileDir/HBox"]
+layout_mode = 2
+size_flags_horizontal = 0
+text = "user://"
+
+[node name="SaveFileNameField" type="LineEdit" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/DirectoriesSection/SaveFileDir/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+placeholder_text = "file_name"
+
+[node name="SaveFileDotLabel" type="Label" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/DirectoriesSection/SaveFileDir/HBox"]
+layout_mode = 2
+size_flags_horizontal = 0
+text = "."
+
+[node name="SaveFileExtensionField" type="LineEdit" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/DirectoriesSection/SaveFileDir/HBox"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(100, 0)
+layout_mode = 2
+placeholder_text = "extension"
+
+[node name="ComponentsSection" type="VBoxContainer" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr"]
+layout_mode = 2
+
+[node name="HSep0" type="HSeparator" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/ComponentsSection"]
+layout_mode = 2
+
+[node name="SectionLabel" type="Label" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/ComponentsSection"]
+layout_mode = 2
+text = "Components"
+horizontal_alignment = 1
+
+[node name="HSep1" type="HSeparator" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/ComponentsSection"]
+layout_mode = 2
+
+[node name="ApplyOnChanged" type="CheckBox" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/ComponentsSection"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Set apply_on_changed to true by default."
+
+[node name="GrabFocusOnMouseOver" type="CheckBox" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/ComponentsSection"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Set grab_focus_on_mouse_over to true by default."
+
+[node name="SetSFXBtn" type="Button" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/ComponentsSection"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Set Component Sound Effects"
+
+[node name="MiscSection" type="VBoxContainer" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr"]
+layout_mode = 2
+size_flags_vertical = 0
+
+[node name="HSep" type="HSeparator" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/MiscSection"]
+layout_mode = 2
+
+[node name="HBox" type="HBoxContainer" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/MiscSection"]
+layout_mode = 2
+
+[node name="UpdateThemeBtn" type="Button" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/MiscSection/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+tooltip_text = "Update the GGS editor theme to match your current Godot editor theme."
+text = "Update Theme"
+
+[node name="BaseTemplateBtn" type="Button" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/MiscSection/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Edit Base Template"
+
+[node name="ResetBtn" type="Button" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr/MiscSection/HBox"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 10
+text = "Reset"
+
+[node name="HSep" type="HSeparator" parent="BgPanel/Margin/MainCtnr/ScrollCtnr/SettingsCtnr"]
+layout_mode = 2
+
+[node name="FooterCtnr" type="VBoxContainer" parent="BgPanel/Margin/MainCtnr"]
+layout_mode = 2
+size_flags_vertical = 8
+
+[node name="HSep0" type="HSeparator" parent="BgPanel/Margin/MainCtnr/FooterCtnr"]
+layout_mode = 2
+
+[node name="DisclaimerLabel" type="Label" parent="BgPanel/Margin/MainCtnr/FooterCtnr"]
+custom_minimum_size = Vector2(0, 21)
+layout_mode = 2
+size_flags_vertical = 8
+theme_override_styles/normal = SubResource("StyleBoxEmpty_ki5am")
+text = "※ All changes take effect after a plugin restart."
+
+[node name="HSep1" type="HSeparator" parent="BgPanel/Margin/MainCtnr/FooterCtnr"]
+layout_mode = 2
+
+[node name="HBox" type="HBoxContainer" parent="BgPanel/Margin/MainCtnr/FooterCtnr"]
+layout_mode = 2
+
+[node name="InfoCtnr" type="HBoxContainer" parent="BgPanel/Margin/MainCtnr/FooterCtnr/HBox"]
+layout_mode = 2
+size_flags_horizontal = 0
+theme_override_constants/separation = 2
+
+[node name="PluginInfo" type="Button" parent="BgPanel/Margin/MainCtnr/FooterCtnr/HBox/InfoCtnr"]
+layout_mode = 2
+size_flags_horizontal = 3
+focus_mode = 0
+mouse_filter = 2
+theme_override_constants/h_separation = 5
+text = "Godot Game Settings"
+icon = ExtResource("3_ggsp5")
+flat = true
+alignment = 0
+
+[node name="VersionBtn" type="Button" parent="BgPanel/Margin/MainCtnr/FooterCtnr/HBox/InfoCtnr"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "View Release"
+text = "3.1.0"
+
+[node name="ChangelogBtn" type="Button" parent="BgPanel/Margin/MainCtnr/FooterCtnr/HBox/InfoCtnr"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "View Changelog"
+text = "Changelog"
+
+[node name="BtnCtnr" type="HBoxContainer" parent="BgPanel/Margin/MainCtnr/FooterCtnr/HBox"]
+layout_mode = 2
+size_flags_horizontal = 10
+
+[node name="OkBtn" type="Button" parent="BgPanel/Margin/MainCtnr/FooterCtnr/HBox/BtnCtnr"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+size_flags_horizontal = 10
+text = "OK"
+
+[node name="CancelBtn" type="Button" parent="BgPanel/Margin/MainCtnr/FooterCtnr/HBox/BtnCtnr"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+size_flags_horizontal = 10
+text = "Cancel
+"
+
+[node name="DirSelectionWindow" type="FileDialog" parent="."]
+title = "Open a Directory"
+size = Vector2i(600, 500)
+min_size = Vector2i(600, 500)
+ok_button_text = "Select Current Folder"
+file_mode = 2
+metadata/target = 0
+
+[node name="ConfirmWindow" type="ConfirmationDialog" parent="."]
+size = Vector2i(497, 135)
+metadata/purpose = 0
diff --git a/addons/ggs/editor/setting_panel/groupless.gd b/addons/ggs/editor/setting_panel/groupless.gd
new file mode 100644 (file)
index 0000000..0ebf3a3
--- /dev/null
@@ -0,0 +1,14 @@
+@tool
+extends PanelContainer
+
+@onready var MainCtnr: HFlowContainer = $MainCtnr
+
+
+func clear() -> void:
+       var children: Array[Node] = MainCtnr.get_children()
+       for child in children:
+               child.queue_free()
+
+
+func add_item(item: Button) -> void:
+       MainCtnr.add_child(item)
diff --git a/addons/ggs/editor/setting_panel/setting_group/setting_group.gd b/addons/ggs/editor/setting_panel/setting_group/setting_group.gd
new file mode 100644 (file)
index 0000000..ea2b583
--- /dev/null
@@ -0,0 +1,24 @@
+@tool
+extends PanelContainer
+class_name ggsSettingGroup
+
+var path: String
+
+@onready var GroupName: CheckBox = $MainCtnr/GroupName
+@onready var ItemCtnr: HFlowContainer = $MainCtnr/ItemCtnr
+
+
+func add_item(item: Button) -> void:
+       ItemCtnr.add_child(item)
+
+
+func set_group_name(group_name: String) -> void:
+       GroupName.text = group_name
+
+
+func set_checked(checked: bool) -> void:
+       GroupName.button_pressed = checked
+
+
+func get_checked() -> bool:
+       return GroupName.button_pressed
diff --git a/addons/ggs/editor/setting_panel/setting_group/setting_group.tscn b/addons/ggs/editor/setting_panel/setting_group/setting_group.tscn
new file mode 100644 (file)
index 0000000..bbce967
--- /dev/null
@@ -0,0 +1,22 @@
+[gd_scene load_steps=2 format=3 uid="uid://d2qf13e27gjdi"]
+
+[ext_resource type="Script" path="res://addons/ggs/editor/setting_panel/setting_group/setting_group.gd" id="1_yot62"]
+
+[node name="SettingGroup" type="PanelContainer"]
+custom_minimum_size = Vector2(150, 0)
+size_flags_horizontal = 3
+theme_type_variation = &"SettingItemBG"
+script = ExtResource("1_yot62")
+
+[node name="MainCtnr" type="VBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="GroupName" type="CheckBox" parent="MainCtnr"]
+layout_mode = 2
+text_overrun_behavior = 3
+
+[node name="HSep" type="HSeparator" parent="MainCtnr"]
+layout_mode = 2
+
+[node name="ItemCtnr" type="HFlowContainer" parent="MainCtnr"]
+layout_mode = 2
diff --git a/addons/ggs/editor/setting_panel/setting_item/setting_item.gd b/addons/ggs/editor/setting_panel/setting_item/setting_item.gd
new file mode 100644 (file)
index 0000000..9bf7134
--- /dev/null
@@ -0,0 +1,39 @@
+@tool
+extends Button
+class_name ggsSettingItem
+
+var path: String
+var btn_group: ButtonGroup
+
+
+func _ready() -> void:
+       toggled.connect(_on_toggled)
+       gui_input.connect(_on_gui_input)
+       
+       button_group = btn_group
+
+
+func _on_toggled(button_state: bool) -> void:
+       if button_state == false:
+               return
+       
+       if not FileAccess.file_exists(path):
+               printerr("GGS - Inspect Setting: The setting resource (%s.tres) could not be found."%text)
+               ggsUtils.get_editor_interface().inspect_object(null)
+               return
+       
+       var setting_res: ggsSetting = load(path)
+       
+       if setting_res is ggsInputSetting:
+               setting_res.update_current_as_event()
+       
+       GGS.active_setting = setting_res
+       ggsUtils.get_editor_interface().inspect_object(setting_res)
+
+
+func _on_gui_input(event: InputEvent) -> void:
+       if (
+               event is InputEventMouseButton and
+               event.button_index == MOUSE_BUTTON_RIGHT
+       ):
+               ggsUtils.get_editor_interface().select_file(path)
diff --git a/addons/ggs/editor/setting_panel/setting_item/setting_item.tscn b/addons/ggs/editor/setting_panel/setting_item/setting_item.tscn
new file mode 100644 (file)
index 0000000..43cbf91
--- /dev/null
@@ -0,0 +1,7 @@
+[gd_scene load_steps=2 format=3 uid="uid://urg2uin42f3w"]
+
+[ext_resource type="Script" path="res://addons/ggs/editor/setting_panel/setting_item/setting_item.gd" id="1_pwscl"]
+
+[node name="SettingItem" type="Button"]
+toggle_mode = true
+script = ExtResource("1_pwscl")
diff --git a/addons/ggs/editor/setting_panel/setting_list.gd b/addons/ggs/editor/setting_panel/setting_list.gd
new file mode 100644 (file)
index 0000000..04d918a
--- /dev/null
@@ -0,0 +1,123 @@
+@tool
+extends ScrollContainer
+
+const setting_item_scn: PackedScene = preload("./setting_item/setting_item.tscn")
+const setting_group_scn: PackedScene = preload("./setting_group/setting_group.tscn")
+
+var cur_path: String
+var btn_group: ButtonGroup = ButtonGroup.new()
+
+@onready var MainCtnr: HFlowContainer = $PanelCtnr/MainCtnr
+@onready var GrouplessCtnr: PanelContainer = $PanelCtnr/MainCtnr/GroupLess
+
+
+func _ready() -> void:
+       GGS.active_category_changed.connect(_on_Global_active_category_changed)
+
+
+func load_list() -> void:
+       _clear()
+       
+       var item_list: Dictionary = _get_item_list()
+       var base_dir: String = ggsUtils.get_plugin_data().dir_settings.path_join(GGS.active_category)
+       
+       GrouplessCtnr.visible = !item_list["settings"].is_empty()
+       for setting in item_list["settings"]:
+               if setting.ends_with(".gd"):
+                       continue
+               
+               var setting_name: String = setting.get_basename()
+               cur_path = base_dir.path_join(setting)
+               _add_item(setting_name, GrouplessCtnr, cur_path)
+       
+       for group in item_list["groups"]:
+               cur_path = base_dir.path_join(group)
+               var parent: ggsSettingGroup = _add_group(group, cur_path)
+               
+               var dir: DirAccess = DirAccess.open(cur_path)
+               var settings: PackedStringArray = dir.get_files()
+               for setting in settings:
+                       if setting.ends_with(".gd"):
+                               continue
+                       
+                       var setting_name: String = setting.get_basename()
+                       cur_path = dir.get_current_dir().path_join(setting)
+                       _add_item(setting_name, parent, cur_path)
+
+
+func get_selected_groups() -> Array[Node]:
+       var result: Array[Node]
+       
+       var child_count: int = MainCtnr.get_child_count()
+       for child_index in range(child_count):
+               if child_index == 0:
+                       continue
+               
+               var child: ggsSettingGroup = MainCtnr.get_child(child_index)
+               var group_is_checked: bool = child.get_checked()
+               if group_is_checked:
+                       result.append(child)
+       
+       return result
+
+
+func set_checked_all(checked: bool) -> void:
+       var child_count: int = MainCtnr.get_child_count()
+       for child_index in range(child_count):
+               if child_index == 0:
+                       continue
+               
+               MainCtnr.get_child(child_index).set_checked(checked)
+
+
+func _clear() -> void:
+       btn_group = ButtonGroup.new()
+       GrouplessCtnr.visible = false
+       GGS.active_setting = null
+       
+       var child_count: int = MainCtnr.get_child_count()
+       for child_index in range(child_count):
+               if child_index == 0:
+                       MainCtnr.get_child(child_index).clear()
+                       continue
+               
+               MainCtnr.get_child(child_index).queue_free()
+
+
+func _add_item(setting: String, parent: PanelContainer, path: String) -> void:
+       var NewItem: ggsSettingItem = setting_item_scn.instantiate()
+       NewItem.text = setting
+       NewItem.path = path
+       NewItem.btn_group = btn_group
+       
+       parent.add_item(NewItem)
+
+
+func _add_group(group: String, path: String) -> ggsSettingGroup:
+       var NewGroup: ggsSettingGroup = setting_group_scn.instantiate()
+       NewGroup.path = path
+       
+       MainCtnr.add_child(NewGroup)
+       NewGroup.set_group_name(group)
+       return NewGroup
+
+
+func _on_Global_active_category_changed() -> void:
+       if GGS.active_category.is_empty():
+               _clear()
+       else:
+               load_list()
+
+
+### Get Item List
+
+func _get_item_list() -> Dictionary:
+       cur_path = ggsUtils.get_plugin_data().dir_settings.path_join(GGS.active_category)
+       var dir: DirAccess = DirAccess.open(cur_path)
+       var settings: PackedStringArray = dir.get_files()
+       var groups: Array = Array(dir.get_directories()).filter(_remove_underscored)
+       return {"settings": settings, "groups": groups}
+
+
+func _remove_underscored(element: String) -> bool:
+       return not element.begins_with("_")
diff --git a/addons/ggs/editor/setting_panel/setting_panel.gd b/addons/ggs/editor/setting_panel/setting_panel.gd
new file mode 100644 (file)
index 0000000..57a2e3b
--- /dev/null
@@ -0,0 +1,179 @@
+@tool
+extends Control
+
+const TEMPLATE_SCRIPT: GDScript = preload("res://addons/ggs/template.gd")
+
+@export var Notification: AcceptDialog
+
+@onready var AddBtn: Button = %AddBtn
+@onready var NSF: LineEdit = %NewSettingField
+@onready var NGF: LineEdit = %NewGroupField
+@onready var CheckAllBtn: Button = %CheckAllBtn
+@onready var UncheckAllBtn: Button = %UncheckAllBtn
+@onready var ReloadBtn: Button = %ReloadBtn
+@onready var List: ScrollContainer = %SettingList
+@onready var ASW: ConfirmationDialog = $AddSettingWindow
+
+
+func _ready() -> void:
+       AddBtn.pressed.connect(_on_AddBtn_pressed)
+       ASW.template_selected.connect(_on_ASW_template_selected)
+       
+       NSF.text_submitted.connect(_on_NSF_text_submitted)
+       NGF.text_submitted.connect(_on_NGF_text_submitted)
+       CheckAllBtn.pressed.connect(_on_CheckAllBtn_pressed)
+       UncheckAllBtn.pressed.connect(_on_UncheckAllBtn_pressed)
+       ReloadBtn.pressed.connect(_on_ReloadBtn_pressed)
+       
+       GGS.active_category_changed.connect(_on_Global_active_category_changed)
+       GGS.active_setting_changed.connect(_on_Global_active_setting_changed)
+       
+       AddBtn.disabled = true
+       NSF.editable = false
+       NGF.editable = false
+       CheckAllBtn.disabled = true
+       UncheckAllBtn.disabled = true
+       ReloadBtn.disabled = true
+
+
+func _set_topbar_disabled(disabled: bool) -> void:
+       AddBtn.disabled = disabled
+       NSF.editable = !disabled
+       NGF.editable = !disabled
+       CheckAllBtn.disabled = disabled
+       UncheckAllBtn.disabled = disabled
+       ReloadBtn.disabled = disabled
+
+
+func _on_Global_active_category_changed() -> void:
+       _set_topbar_disabled(GGS.active_category.is_empty())
+
+
+func _on_Global_active_setting_changed() -> void:
+       var active_list_item: Button = List.btn_group.get_pressed_button()
+       if GGS.active_setting == null and active_list_item != null:
+               active_list_item.button_pressed = false
+
+
+### Setting Creation
+
+func _create_setting(item_name: String, template_path: String = "") -> void:
+       GGS.progress_started.emit(GGS.Progress.ADD_SETTINGS)
+       
+       # A tiny delay so the progress_started signal can travel properly
+       await get_tree().create_timer(0.05).timeout 
+       
+       if (
+               not item_name.is_valid_filename() or
+               item_name.begins_with("_") or
+               item_name.begins_with(".")
+       ):
+               Notification.purpose = Notification.Purpose.INVALID
+               Notification.popup_centered()
+               GGS.progress_ended.emit()
+               return
+       
+       var paths: PackedStringArray
+       var selected_groups: Array[Node] = List.get_selected_groups()
+       if selected_groups.is_empty():
+               var category_path: String = ggsUtils.get_plugin_data().dir_settings.path_join(GGS.active_category)
+               paths.append(category_path)
+       else:
+               for group in selected_groups:
+                       paths.append(group.path)
+       
+       var dir: DirAccess = DirAccess.open("res://")
+       for path in paths:
+               dir.change_dir(path)
+               
+               if paths.size() == 1:
+                       if dir.file_exists("%s.tres"%item_name):
+                               Notification.purpose = Notification.Purpose.ALREADY_EXISTS
+                               Notification.popup_centered()
+                               GGS.progress_ended.emit()
+                               return
+               else:
+                       if dir.file_exists("%s.tres"%item_name):
+                               printerr("GGS - Add Setting to Multiple Groups: An item with this name already exists. Ignoring <%s>."%path.get_file())
+                               continue
+               
+               var script: Script
+               var script_path: String
+               if template_path.is_empty():
+                       script = TEMPLATE_SCRIPT.duplicate()
+                       script_path = "%s/%s.gd"%[dir.get_current_dir(), item_name]
+                       ResourceSaver.save(script, script_path)
+                       script = load(script_path)
+               else:
+                       script = load(template_path)
+               
+               var resource: ggsSetting = ggsSetting.new()
+               var res_path: String = "%s/%s.tres"%[dir.get_current_dir(), item_name]
+               resource.set_script(script)
+               ResourceSaver.save(resource, res_path)
+       
+       NSF.clear()
+       ggsUtils.get_resource_file_system().scan()
+       List.load_list()
+       GGS.progress_ended.emit()
+
+
+func _on_NSF_text_submitted(submitted_text: String) -> void:
+       _create_setting(submitted_text)
+
+
+### Group Creation
+
+func _create_group(group_name: String) -> void:
+       if (
+               not group_name.is_valid_filename() or
+               group_name.begins_with("_") or
+               group_name.begins_with(".")
+       ):
+               Notification.purpose = Notification.Purpose.INVALID
+               Notification.popup_centered()
+               return
+       
+       var path: String = ggsUtils.get_plugin_data().dir_settings.path_join(GGS.active_category)
+       var dir: DirAccess = DirAccess.open(path)
+       if dir.dir_exists(group_name):
+               Notification.purpose = Notification.Purpose.ALREADY_EXISTS
+               Notification.popup_centered()
+               return
+       
+       dir.make_dir(group_name)
+       
+       NGF.clear()
+       ggsUtils.get_resource_file_system().scan()
+       List.load_list()
+
+
+func _on_NGF_text_submitted(submitted_text: String) -> void:
+       _create_group(submitted_text)
+
+
+### Setting from Template
+
+func _on_AddBtn_pressed() -> void:
+       ASW.popup_centered()
+
+
+func _on_ASW_template_selected(template: String, setting_name: String) -> void:
+       _create_setting(setting_name, template)
+
+
+### Check/Uncheck Btns
+
+func _on_CheckAllBtn_pressed() -> void:
+       List.set_checked_all(true)
+
+
+func _on_UncheckAllBtn_pressed() -> void:
+       List.set_checked_all(false)
+
+
+### Reload Btn
+
+func _on_ReloadBtn_pressed() -> void:
+       List.load_list()
+
diff --git a/addons/ggs/editor/setting_panel/setting_panel.tscn b/addons/ggs/editor/setting_panel/setting_panel.tscn
new file mode 100644 (file)
index 0000000..9d49ce7
--- /dev/null
@@ -0,0 +1,114 @@
+[gd_scene load_steps=9 format=3 uid="uid://vt5mwwxhtu3x"]
+
+[ext_resource type="Script" path="res://addons/ggs/editor/setting_panel/setting_panel.gd" id="1_2wlv0"]
+[ext_resource type="Texture2D" uid="uid://bttv2hpecd38m" path="res://addons/ggs/assets/check_all.svg" id="3_bh7l7"]
+[ext_resource type="Texture2D" uid="uid://by345a10evjm8" path="res://addons/ggs/assets/add.svg" id="3_tdauq"]
+[ext_resource type="Script" path="res://addons/ggs/editor/setting_panel/setting_list.gd" id="4_htr8u"]
+[ext_resource type="Texture2D" uid="uid://ve54bl3r7ljc" path="res://addons/ggs/assets/reload.svg" id="4_j6whk"]
+[ext_resource type="Texture2D" uid="uid://romr61n4g5y5" path="res://addons/ggs/assets/uncheck_all.svg" id="4_q4gh6"]
+[ext_resource type="Script" path="res://addons/ggs/editor/setting_panel/groupless.gd" id="7_l1jd4"]
+[ext_resource type="PackedScene" uid="uid://111vt7wxn7lx" path="res://addons/ggs/editor/add_setting_window/add_setting_window.tscn" id="7_o4e63"]
+
+[node name="SettingPanel" type="Control"]
+custom_minimum_size = Vector2(342, 0)
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_2wlv0")
+
+[node name="MainCtnr" type="VBoxContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="TopBar" type="HBoxContainer" parent="MainCtnr"]
+layout_mode = 2
+
+[node name="AddBtn" type="Button" parent="MainCtnr/TopBar"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Add Setting from Template"
+disabled = true
+icon = ExtResource("3_tdauq")
+flat = true
+
+[node name="NewSettingField" type="LineEdit" parent="MainCtnr/TopBar"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+placeholder_text = "New Setting..."
+editable = false
+clear_button_enabled = true
+
+[node name="NewGroupField" type="LineEdit" parent="MainCtnr/TopBar"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+placeholder_text = "New Group..."
+editable = false
+clear_button_enabled = true
+
+[node name="VSep" type="VSeparator" parent="MainCtnr/TopBar"]
+layout_mode = 2
+
+[node name="CheckAllBtn" type="Button" parent="MainCtnr/TopBar"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Check All"
+disabled = true
+icon = ExtResource("3_bh7l7")
+flat = true
+
+[node name="UncheckAllBtn" type="Button" parent="MainCtnr/TopBar"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Uncheck All"
+disabled = true
+icon = ExtResource("4_q4gh6")
+flat = true
+
+[node name="ReloadBtn" type="Button" parent="MainCtnr/TopBar"]
+unique_name_in_owner = true
+layout_mode = 2
+tooltip_text = "Reload List"
+disabled = true
+icon = ExtResource("4_j6whk")
+flat = true
+
+[node name="SettingList" type="ScrollContainer" parent="MainCtnr"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+horizontal_scroll_mode = 0
+script = ExtResource("4_htr8u")
+
+[node name="PanelCtnr" type="PanelContainer" parent="MainCtnr/SettingList"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_type_variation = &"SettingListBG"
+
+[node name="MainCtnr" type="HFlowContainer" parent="MainCtnr/SettingList/PanelCtnr"]
+layout_mode = 2
+size_flags_vertical = 3
+theme_override_constants/h_separation = 10
+theme_override_constants/v_separation = 10
+
+[node name="GroupLess" type="PanelContainer" parent="MainCtnr/SettingList/PanelCtnr/MainCtnr"]
+visible = false
+custom_minimum_size = Vector2(150, 0)
+layout_mode = 2
+size_flags_horizontal = 3
+theme_type_variation = &"SettingItemBG"
+script = ExtResource("7_l1jd4")
+
+[node name="MainCtnr" type="HFlowContainer" parent="MainCtnr/SettingList/PanelCtnr/MainCtnr/GroupLess"]
+layout_mode = 2
+
+[node name="AddSettingWindow" parent="." instance=ExtResource("7_o4e63")]
diff --git a/addons/ggs/plugin.cfg b/addons/ggs/plugin.cfg
new file mode 100644 (file)
index 0000000..a866694
--- /dev/null
@@ -0,0 +1,7 @@
+[plugin]
+
+name="Godot Game Settings"
+description="Create and manage game settings."
+author="PunchablePlushie"
+version="3.1.0"
+script="plugin.gd"
diff --git a/addons/ggs/plugin.gd b/addons/ggs/plugin.gd
new file mode 100644 (file)
index 0000000..64a78bb
--- /dev/null
@@ -0,0 +1,49 @@
+@tool
+extends EditorPlugin
+
+var main_panel_scn: PackedScene = preload("./editor/main_panel/main_panel.tscn")
+var inspector_plugin: EditorInspectorPlugin = ggsInspectorPlugin.new()
+var MainPanel: Control
+
+
+func _enter_tree() -> void:
+       _add_editor_interface_singleton()
+       _add_plugin_singleton()
+       _add_editor()
+       add_inspector_plugin(inspector_plugin)
+
+
+func _exit_tree() -> void:
+       _remove_editor_interface_singleton()
+       _remove_editor()
+       remove_inspector_plugin(inspector_plugin)
+
+
+### Singletons
+
+func _add_editor_interface_singleton() -> void:
+       if not Engine.has_singleton("ggsEI"):
+               Engine.register_singleton("ggsEI", get_editor_interface())
+
+
+func _remove_editor_interface_singleton() -> void:
+       if Engine.has_singleton("ggsEI"):
+               Engine.unregister_singleton("ggsEI")
+
+
+func _add_plugin_singleton() -> void:
+       if not ProjectSettings.has_setting("autoload/GGS"):
+               add_autoload_singleton("GGS", "res://addons/ggs/classes/global/ggs.tscn")
+
+
+### Main Editor
+
+func _add_editor() -> void:
+       MainPanel = main_panel_scn.instantiate()
+       add_control_to_bottom_panel(MainPanel, "Game Settings")
+
+
+func _remove_editor() -> void:
+       if MainPanel:
+               remove_control_from_bottom_panel(MainPanel)
+               MainPanel.queue_free()
diff --git a/addons/ggs/plugin_data.tres b/addons/ggs/plugin_data.tres
new file mode 100644 (file)
index 0000000..c1d3bc9
--- /dev/null
@@ -0,0 +1,15 @@
+[gd_resource type="Resource" script_class="ggsPluginData" load_steps=2 format=3 uid="uid://dpk53al471l8m"]
+
+[ext_resource type="Script" path="res://addons/ggs/classes/resources/ggs_plugin_data.gd" id="1_wabe0"]
+
+[resource]
+script = ExtResource("1_wabe0")
+recent_settings = Array[String](["res://game_settings/templates/audio/audio_volume.gd", "res://game_settings/templates/display/display_scale.gd", "res://game_settings/templates/display/display_size.gd", "res://game_settings/templates/display/display_fullscreen.gd", "res://game_settings/templates/input.gd"])
+apply_on_changed_all = true
+grab_focus_on_mouse_over_all = true
+dir_settings = "res://game_settings/settings"
+dir_templates = "res://game_settings/templates"
+dir_components = "res://game_settings/components"
+dir_save_file = "user://settings.cfg"
+split_offset_0 = -315
+split_offset_1 = 615
diff --git a/addons/ggs/template.gd b/addons/ggs/template.gd
new file mode 100644 (file)
index 0000000..fcc0b99
--- /dev/null
@@ -0,0 +1,6 @@
+@tool
+extends ggsSetting
+
+
+func apply(value: Variant) -> void:
+       pass
diff --git a/game_settings/components/_misc_components/apply_btn/apply_btn.gd b/game_settings/components/_misc_components/apply_btn/apply_btn.gd
new file mode 100644 (file)
index 0000000..d3cf764
--- /dev/null
@@ -0,0 +1,26 @@
+extends Button
+
+@export var group: String
+@export var grab_focus_on_mouse_over: bool
+
+
+func _ready() -> void:
+       pressed.connect(_on_pressed)
+       mouse_entered.connect(_on_mouse_entered)
+       focus_entered.connect(_on_focus_entered)
+
+
+func _on_pressed() -> void:
+       get_tree().call_group(group, "apply_setting")
+       GGS.play_sfx(GGS.SFX.INTERACT)
+
+
+func _on_mouse_entered() -> void:
+       GGS.play_sfx(GGS.SFX.MOUSE_OVER)
+       
+       if grab_focus_on_mouse_over:
+               grab_focus()
+
+
+func _on_focus_entered() -> void:
+       GGS.play_sfx(GGS.SFX.FOCUS)
diff --git a/game_settings/components/_misc_components/apply_btn/apply_btn.tscn b/game_settings/components/_misc_components/apply_btn/apply_btn.tscn
new file mode 100644 (file)
index 0000000..a1867be
--- /dev/null
@@ -0,0 +1,11 @@
+[gd_scene load_steps=2 format=3]
+
+[ext_resource type="Script" path="res://game_settings/components/_misc_components/apply_btn/apply_btn.gd" id="1_dk1tm"]
+
+[node name="ApplyBtn" type="Button"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_dk1tm")
diff --git a/game_settings/components/_misc_components/input_confirm_window/input_confirm_window.gd b/game_settings/components/_misc_components/input_confirm_window/input_confirm_window.gd
new file mode 100644 (file)
index 0000000..bece485
--- /dev/null
@@ -0,0 +1,285 @@
+extends ConfirmationDialog
+signal input_selected(chosen_input: InputEvent)
+
+@export var listening_wait_time: float = 0.35
+@export var listening_max_time: float = 5
+@export var show_progress_bar: bool = true
+@export_group("Text")
+@export var btn_listening: String = ". . ."
+@export var title_listening: String = "Listening for Input"
+@export var title_confirm: String = "Confirm Input"
+@export var timeout_text: String = "Timed Out"
+@export var already_exists_msg: String = "Input already exists ({action})"
+
+var chosen_input: InputEvent
+var src: ggsUIComponent
+var type: ggsInputHelper.InputType
+var accept_mouse: bool
+var accept_modifiers: bool
+var accept_axis: bool
+var use_icons: bool
+
+var input_helper: ggsInputHelper = ggsInputHelper.new()
+
+@onready var AlreadyExistsLabel: Label = $MainCtnr/AlreadyExistsLabel
+@onready var OkBtn: Button = get_ok_button()
+@onready var CancelBtn: Button = get_cancel_button()
+@onready var ListenBtn: Button = $MainCtnr/ListenBtn
+@onready var ListenTimer: Timer = $ListenTimer
+@onready var MaxListenTimer: Timer = $MaxListenTimer
+@onready var ListenProgress: ProgressBar = $MainCtnr/ListenProgress
+
+
+func _ready() -> void:
+       visibility_changed.connect(_on_visibility_changed)
+       confirmed.connect(_on_confirmed)
+       
+       ListenBtn.pressed.connect(_on_ListenBtn_pressed)
+       ListenTimer.timeout.connect(_on_ListenTimer_timeout)
+       MaxListenTimer.timeout.connect(_on_MaxListenTimer_timeout)
+       
+       ListenBtn.mouse_entered.connect(_on_AnyBtn_mouse_entered.bind(ListenBtn))
+       OkBtn.mouse_entered.connect(_on_AnyBtn_mouse_entered.bind(OkBtn))
+       CancelBtn.mouse_entered.connect(_on_AnyBtn_mouse_entered.bind(CancelBtn))
+       ListenBtn.focus_entered.connect(_on_AnyBtn_focus_entered)
+       OkBtn.focus_entered.connect(_on_AnyBtn_focus_entered)
+       CancelBtn.focus_entered.connect(_on_AnyBtn_focus_entered)
+       CancelBtn.pressed.connect(_on_CancelBtn_pressed)
+       
+       ListenBtn.focus_neighbor_bottom = CancelBtn.get_path()
+       OkBtn.focus_neighbor_top = ListenBtn.get_path()
+       CancelBtn.focus_neighbor_top = ListenBtn.get_path()
+       
+       ListenTimer.wait_time = listening_wait_time
+       MaxListenTimer.wait_time = listening_max_time
+
+
+func _process(_delta: float) -> void:
+       ListenProgress.value = ListenTimer.time_left / ListenTimer.wait_time
+
+
+func _input(event: InputEvent) -> void:
+       if not _event_is_valid(event):
+               return
+       
+       _set_btn_text_or_icon(event)
+       
+       var input_already_exists: Array = input_helper.input_already_exists(event, src.setting.action)
+       if input_already_exists[0]:
+               AlreadyExistsLabel.text = already_exists_msg.format({"action": input_already_exists[1].capitalize()})
+               
+               ListenProgress.hide()
+               AlreadyExistsLabel.show()
+               ListenTimer.stop()
+               MaxListenTimer.start()
+               return
+       
+       ListenProgress.show()
+       AlreadyExistsLabel.hide()
+       ListenTimer.start()
+       MaxListenTimer.start()
+       
+       chosen_input = event
+
+
+### Input Validation
+
+func _event_is_valid(event: InputEvent) -> bool:
+       var type_is_acceptable: bool = _event_type_is_acceptable(event)
+       var has_modifier: bool = _event_has_modifier(event)
+       var is_double_click: bool = _event_is_double_click(event)
+       var mouse_btn_is_valid: bool = _event_mouse_btn_is_valid(event)
+       var event_is_single_press: bool = (event.is_pressed() and not event.is_echo())
+       
+       var is_valid: bool
+       if accept_modifiers:
+               is_valid = (
+                       type_is_acceptable and
+                       event_is_single_press and
+                       not is_double_click and
+                       mouse_btn_is_valid
+               )
+       else:
+               is_valid = (
+                       type_is_acceptable and
+                       event_is_single_press and
+                       not is_double_click and
+                       mouse_btn_is_valid and 
+                       not has_modifier
+               )
+       
+       return is_valid
+
+
+func _event_type_is_acceptable(event: InputEvent) -> bool:
+       var is_acceptable: bool = false
+       
+       if (
+               type == ggsInputHelper.InputType.KEYBOARD or
+               type == ggsInputHelper.InputType.MOUSE
+       ):
+               if accept_mouse:
+                       is_acceptable = (
+                               event is InputEventKey or
+                               event is InputEventMouseButton
+                       )
+               else:
+                       is_acceptable = (event is InputEventKey)
+       
+       elif (
+               type == ggsInputHelper.InputType.GP_BTN or
+               type == ggsInputHelper.InputType.GP_MOTION
+       ):
+               if accept_axis:
+                       is_acceptable = (
+                               event is InputEventJoypadButton or
+                               event is InputEventJoypadMotion
+                       )
+               else:
+                       is_acceptable = (event is InputEventJoypadButton)
+       
+       return is_acceptable
+
+
+func _event_has_modifier(event: InputEvent) -> bool:
+       var has_modifier: bool
+       
+       if event is InputEventWithModifiers:
+               has_modifier = (
+                       event.shift_pressed or
+                       event.alt_pressed or
+                       event.ctrl_pressed
+               )
+       
+       return has_modifier
+
+
+func _event_is_double_click(event: InputEvent) -> bool:
+       var is_double_click: bool
+       
+       if event is InputEventMouseButton:
+               is_double_click = event.double_click
+       
+       return is_double_click
+
+
+func _event_mouse_btn_is_valid(event: InputEvent) -> bool:
+       var mouse_btn_is_valid: bool = true
+       
+       if event is InputEventMouseButton:
+               mouse_btn_is_valid = (event.button_index >= 0 and event.button_index <= 9)
+       
+       return mouse_btn_is_valid
+
+
+### Input Listening
+
+func _set_btn_text_or_icon(event: InputEvent) -> void:
+       if (
+               use_icons and
+               (type == ggsInputHelper.InputType.MOUSE or
+               type == ggsInputHelper.InputType.GP_BTN or
+               type == ggsInputHelper.InputType.GP_MOTION)
+       ):
+               ListenBtn.icon = input_helper.get_event_as_icon(event, src.icon_db)
+               
+               if ListenBtn.icon == null:
+                       ListenBtn.text = input_helper.get_event_as_text(event)
+               else:
+                       ListenBtn.text = ""
+               
+               return
+       
+       ListenBtn.icon = null
+       ListenBtn.text = input_helper.get_event_as_text(event)
+
+
+func _start_listening() -> void:
+       ListenBtn.text = btn_listening
+       ListenBtn.icon = null
+       title = title_listening
+       
+       OkBtn.release_focus()
+       OkBtn.disabled = true
+       OkBtn.focus_mode = Control.FOCUS_NONE
+       
+       ListenBtn.release_focus()
+       ListenBtn.disabled = true
+       ListenBtn.focus_mode = Control.FOCUS_NONE
+       
+       CancelBtn.release_focus()
+       
+       if show_progress_bar:
+               ListenProgress.show()
+       
+       set_process_input(true)
+       set_process(true)
+       MaxListenTimer.start()
+
+
+func _stop_listening(timed_out: bool = false) -> void:
+       title = title_confirm
+       
+       ListenBtn.focus_mode = Control.FOCUS_ALL
+       ListenBtn.disabled = false
+       ListenBtn.grab_focus()
+       
+       if timed_out:
+               ListenBtn.text = timeout_text
+               ListenBtn.icon = null
+       
+       if not timed_out:
+               OkBtn.focus_mode = Control.FOCUS_ALL
+               OkBtn.disabled = false
+               OkBtn.grab_focus()
+       
+       ListenProgress.hide()
+       AlreadyExistsLabel.hide()
+       
+       set_process_input(false)
+       set_process(false)
+       MaxListenTimer.stop()
+
+
+func _on_ListenBtn_pressed() -> void:
+       _start_listening()
+       GGS.play_sfx(GGS.SFX.INTERACT)
+
+
+func _on_ListenTimer_timeout() -> void:
+       _stop_listening()
+
+
+func _on_MaxListenTimer_timeout() -> void:
+       _stop_listening(true)
+
+
+### Window
+
+func _on_visibility_changed() -> void:
+       if visible:
+               OkBtn.release_focus()
+               chosen_input = null
+               _start_listening()
+
+
+func _on_confirmed() -> void:
+       input_selected.emit(chosen_input)
+       GGS.play_sfx(GGS.SFX.INTERACT)
+
+
+### SFX
+
+func _on_AnyBtn_mouse_entered(Btn: Button) -> void:
+       GGS.play_sfx(GGS.SFX.MOUSE_OVER)
+       
+       if src.grab_focus_on_mouse_over:
+               Btn.grab_focus()
+
+
+func _on_AnyBtn_focus_entered() -> void:
+       GGS.play_sfx(GGS.SFX.FOCUS)
+
+
+func _on_CancelBtn_pressed() -> void:
+       GGS.play_sfx(GGS.SFX.INTERACT)
diff --git a/game_settings/components/_misc_components/input_confirm_window/input_confirm_window.tscn b/game_settings/components/_misc_components/input_confirm_window/input_confirm_window.tscn
new file mode 100644 (file)
index 0000000..e295140
--- /dev/null
@@ -0,0 +1,43 @@
+[gd_scene load_steps=2 format=3 uid="uid://b1btmq8y3gexs"]
+
+[ext_resource type="Script" path="res://game_settings/components/_misc_components/input_confirm_window/input_confirm_window.gd" id="1_uql0i"]
+
+[node name="InputConfirmWindow" type="ConfirmationDialog"]
+size = Vector2i(300, 118)
+min_size = Vector2i(300, 115)
+ok_button_text = "Confirm"
+dialog_close_on_escape = false
+script = ExtResource("1_uql0i")
+listening_wait_time = 0.2
+
+[node name="MainCtnr" type="VBoxContainer" parent="."]
+offset_left = 8.0
+offset_top = 8.0
+offset_right = 292.0
+offset_bottom = 69.0
+size_flags_horizontal = 3
+theme_override_constants/separation = 0
+
+[node name="ListenBtn" type="Button" parent="MainCtnr"]
+layout_mode = 2
+size_flags_vertical = 3
+text = "ddd"
+icon_alignment = 1
+expand_icon = true
+
+[node name="ListenProgress" type="ProgressBar" parent="MainCtnr"]
+visible = false
+layout_mode = 2
+max_value = 1.0
+show_percentage = false
+
+[node name="AlreadyExistsLabel" type="Label" parent="MainCtnr"]
+visible = false
+layout_mode = 2
+horizontal_alignment = 1
+
+[node name="ListenTimer" type="Timer" parent="."]
+one_shot = true
+
+[node name="MaxListenTimer" type="Timer" parent="."]
+one_shot = true
diff --git a/game_settings/components/_misc_components/reset_btn/reset_btn.gd b/game_settings/components/_misc_components/reset_btn/reset_btn.gd
new file mode 100644 (file)
index 0000000..563c6d4
--- /dev/null
@@ -0,0 +1,26 @@
+extends Button
+
+@export var group: String
+@export var grab_focus_on_mouse_over: bool
+
+
+func _ready() -> void:
+       pressed.connect(_on_pressed)
+       mouse_entered.connect(_on_mouse_entered)
+       focus_entered.connect(_on_focus_entered)
+
+
+func _on_pressed() -> void:
+       get_tree().call_group(group, "reset_setting")
+       GGS.play_sfx(GGS.SFX.INTERACT)
+
+
+func _on_mouse_entered() -> void:
+       GGS.play_sfx(GGS.SFX.MOUSE_OVER)
+       
+       if grab_focus_on_mouse_over:
+               grab_focus()
+
+
+func _on_focus_entered() -> void:
+       GGS.play_sfx(GGS.SFX.FOCUS)
diff --git a/game_settings/components/_misc_components/reset_btn/reset_btn.tscn b/game_settings/components/_misc_components/reset_btn/reset_btn.tscn
new file mode 100644 (file)
index 0000000..77db991
--- /dev/null
@@ -0,0 +1,11 @@
+[gd_scene load_steps=2 format=3]
+
+[ext_resource type="Script" path="res://game_settings/components/_misc_components/reset_btn/reset_btn.gd" id="1_pchyd"]
+
+[node name="ResetBtn" type="Button"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_pchyd")
diff --git a/game_settings/components/_shared_scripts/binary_selection.gd b/game_settings/components/_shared_scripts/binary_selection.gd
new file mode 100644 (file)
index 0000000..025be13
--- /dev/null
@@ -0,0 +1,48 @@
+@tool
+extends ggsUIComponent
+
+@onready var Btn: Button = $Btn
+
+
+func _ready() -> void:
+       compatible_types = [TYPE_BOOL]
+       if Engine.is_editor_hint():
+               return
+       
+       super()
+       Btn.toggled.connect(_on_Btn_toggled)
+       Btn.mouse_entered.connect(_on_Btn_mouse_entered)
+       Btn.focus_entered.connect(_on_Btn_focus_entered)
+
+
+func init_value() -> void:
+       super()
+       Btn.set_pressed_no_signal(setting_value)
+
+
+func _on_Btn_toggled(btn_state: bool) -> void:
+       setting_value = btn_state
+       GGS.play_sfx(GGS.SFX.INTERACT)
+       
+       if apply_on_change:
+               apply_setting()
+
+
+### Setting
+
+func reset_setting() -> void:
+       super()
+       Btn.set_pressed_no_signal(setting_value)
+
+
+### SFX
+
+func _on_Btn_mouse_entered() -> void:
+       GGS.play_sfx(GGS.SFX.MOUSE_OVER)
+       
+       if grab_focus_on_mouse_over:
+               Btn.grab_focus()
+
+
+func _on_Btn_focus_entered() -> void:
+       GGS.play_sfx(GGS.SFX.FOCUS)
diff --git a/game_settings/components/arrow_list/arrow_list.gd b/game_settings/components/arrow_list/arrow_list.gd
new file mode 100644 (file)
index 0000000..8764d26
--- /dev/null
@@ -0,0 +1,96 @@
+@tool
+extends ggsUIComponent
+signal option_selected(option_index: int)
+
+@export_category("ArrowList")
+@export var options: PackedStringArray
+@export var option_ids: PackedInt32Array
+
+@onready var LeftBtn: Button = $HBox/LeftBtn
+@onready var OptionLabel: Label = $HBox/OptionLabel
+@onready var RightBtn: Button = $HBox/RightBtn
+
+
+func _ready() -> void:
+       compatible_types = [TYPE_BOOL, TYPE_INT]
+       if Engine.is_editor_hint():
+               return
+       
+       super()
+       option_selected.connect(_on_option_selected)
+       LeftBtn.pressed.connect(_on_LeftBtn_pressed)
+       RightBtn.pressed.connect(_on_RightBtn_pressed)
+       
+       LeftBtn.mouse_entered.connect(_on_AnyBtn_mouse_entered.bind(LeftBtn))
+       RightBtn.mouse_entered.connect(_on_AnyBtn_mouse_entered.bind(RightBtn))
+       LeftBtn.focus_entered.connect(_on_AnyBtn_focus_entered)
+       RightBtn.focus_entered.connect(_on_AnyBtn_focus_entered)
+
+
+func init_value() -> void:
+       super()
+       
+       if not option_ids.is_empty():
+               var option_index: int = option_ids.find(setting_value)
+               select(option_index, false)
+       else:
+               select(setting_value, false)
+
+
+func _on_option_selected(_option_index: int) -> void:
+       if apply_on_change:
+               apply_setting()
+
+
+### Interaction
+
+func select(index: int, emit_selected: bool = true) -> void:
+       index = index % options.size()
+       
+       OptionLabel.text = options[index]
+       
+       if not option_ids.is_empty():
+               setting_value = option_ids[index]
+       else:
+               setting_value = index
+       
+       if emit_selected:
+               option_selected.emit(index)
+
+
+func _on_LeftBtn_pressed() -> void:
+       if option_ids.is_empty():
+               select(setting_value - 1)
+       else:
+               select(option_ids.find(setting_value) - 1)
+       
+       GGS.play_sfx(GGS.SFX.INTERACT)
+
+
+func _on_RightBtn_pressed() -> void:
+       if option_ids.is_empty():
+               select(setting_value + 1)
+       else:
+               select(option_ids.find(setting_value) + 1)
+       
+       GGS.play_sfx(GGS.SFX.INTERACT)
+
+
+### Setting
+
+func reset_setting() -> void:
+       select(setting.default)
+       apply_setting()
+
+
+### SFX
+
+func _on_AnyBtn_mouse_entered(Btn: Button) -> void:
+       GGS.play_sfx(GGS.SFX.MOUSE_OVER)
+       
+       if grab_focus_on_mouse_over:
+               Btn.grab_focus()
+
+
+func _on_AnyBtn_focus_entered() -> void:
+       GGS.play_sfx(GGS.SFX.FOCUS)
diff --git a/game_settings/components/arrow_list/arrow_list.tscn b/game_settings/components/arrow_list/arrow_list.tscn
new file mode 100644 (file)
index 0000000..7039168
--- /dev/null
@@ -0,0 +1,24 @@
+[gd_scene load_steps=2 format=3 uid="uid://1qpe22ky2s6y"]
+
+[ext_resource type="Script" path="res://game_settings/components/arrow_list/arrow_list.gd" id="1_cifu3"]
+
+[node name="ArrowList" type="MarginContainer"]
+offset_left = 143.0
+offset_right = 143.0
+offset_bottom = 40.0
+script = ExtResource("1_cifu3")
+
+[node name="HBox" type="HBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="LeftBtn" type="Button" parent="HBox"]
+layout_mode = 2
+text = "<"
+
+[node name="OptionLabel" type="Label" parent="HBox"]
+layout_mode = 2
+text = "OptionLabel"
+
+[node name="RightBtn" type="Button" parent="HBox"]
+layout_mode = 2
+text = ">"
diff --git a/game_settings/components/checkbox/checkbox.tscn b/game_settings/components/checkbox/checkbox.tscn
new file mode 100644 (file)
index 0000000..fb2ba93
--- /dev/null
@@ -0,0 +1,11 @@
+[gd_scene load_steps=2 format=3 uid="uid://bhkyf3l4ee800"]
+
+[ext_resource type="Script" path="res://game_settings/components/_shared_scripts/binary_selection.gd" id="1_8vlbr"]
+
+[node name="Checkbox" type="MarginContainer"]
+offset_right = 40.0
+offset_bottom = 40.0
+script = ExtResource("1_8vlbr")
+
+[node name="Btn" type="CheckBox" parent="."]
+layout_mode = 2
diff --git a/game_settings/components/input_btn/input_btn.gd b/game_settings/components/input_btn/input_btn.gd
new file mode 100644 (file)
index 0000000..959f30f
--- /dev/null
@@ -0,0 +1,113 @@
+@tool
+extends ggsUIComponent
+
+@export var ICW: ConfirmationDialog
+@export var accept_modifiers: bool
+@export var accept_mouse: bool
+@export var accept_axis: bool
+
+@export_group("Icon")
+@export var use_icons: bool
+@export var icon_db: ggsIconDB
+
+var type: ggsInputHelper.InputType = ggsInputHelper.InputType.INVALID
+var input_helper: ggsInputHelper = ggsInputHelper.new()
+
+@onready var Btn: Button = $Btn
+
+
+func _ready() -> void:
+       compatible_types = [TYPE_ARRAY]
+       if Engine.is_editor_hint():
+               return
+       
+       super()
+       Btn.pressed.connect(_on_Btn_pressed)
+       ICW.input_selected.connect(_on_ICW_input_selected)
+       Input.joy_connection_changed.connect(_on_Input_joy_connection_changed)
+       
+       Btn.mouse_entered.connect(_on_Btn_mouse_entered)
+       Btn.focus_entered.connect(_on_Btn_focus_entered)
+
+
+func init_value() -> void:
+       super()
+       var event: InputEvent = input_helper.create_event_from_type(setting_value[0])
+       
+       type = input_helper.get_event_type(event)
+       
+       input_helper.set_event_id(event, setting_value[1])
+       _set_btn_text_or_icon(event)
+
+
+func _on_Btn_pressed() -> void:
+       ICW.src = self
+       ICW.type = type
+       ICW.accept_mouse = accept_mouse
+       ICW.accept_modifiers = accept_modifiers
+       ICW.accept_axis = accept_axis
+       ICW.use_icons = use_icons
+       ICW.popup_centered()
+       
+       GGS.play_sfx(GGS.SFX.FOCUS)
+
+
+func _on_ICW_input_selected(event: InputEvent) -> void:
+       if ICW.src != self:
+               return
+       
+       setting_value = [input_helper.get_event_type(event), input_helper.get_event_id(event)]
+       _set_btn_text_or_icon(event)
+       
+       if apply_on_change:
+               apply_setting()
+
+
+### Button Text or Icon
+
+func _set_btn_text_or_icon(event: InputEvent) -> void:
+       if (
+               use_icons and
+               (type == ggsInputHelper.InputType.MOUSE or
+               type == ggsInputHelper.InputType.GP_BTN or
+               type == ggsInputHelper.InputType.GP_MOTION)
+       ):
+               Btn.icon = input_helper.get_event_as_icon(event, icon_db)
+               
+               if Btn.icon == null:
+                       Btn.text = input_helper.get_event_as_text(event)
+               else:
+                       Btn.text = ""
+               
+               return
+       
+       Btn.icon = null
+       Btn.text = input_helper.get_event_as_text(event)
+
+
+func _on_Input_joy_connection_changed(_device: int, _connected: bool) -> void:
+       var event: InputEvent = input_helper.create_event_from_type(setting_value[0])
+       input_helper.set_event_id(event, setting_value[1])
+       _set_btn_text_or_icon(event)
+
+
+### Setting
+
+func reset_setting() -> void:
+       super()
+       var event: InputEvent = input_helper.create_event_from_type(setting_value[0])
+       input_helper.set_event_id(event, setting_value[1])
+       _set_btn_text_or_icon(event)
+
+
+### SFX
+
+func _on_Btn_mouse_entered() -> void:
+       GGS.play_sfx(GGS.SFX.MOUSE_OVER)
+       
+       if grab_focus_on_mouse_over:
+               Btn.grab_focus()
+
+
+func _on_Btn_focus_entered() -> void:
+       GGS.play_sfx(GGS.SFX.FOCUS)
diff --git a/game_settings/components/input_btn/input_btn.tscn b/game_settings/components/input_btn/input_btn.tscn
new file mode 100644 (file)
index 0000000..1f52cf8
--- /dev/null
@@ -0,0 +1,18 @@
+[gd_scene load_steps=2 format=3 uid="uid://dm1av7skxvp1j"]
+
+[ext_resource type="Script" path="res://game_settings/components/input_btn/input_btn.gd" id="1_roqsf"]
+
+[node name="InputBtn" type="MarginContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_roqsf")
+
+[node name="Btn" type="Button" parent="."]
+custom_minimum_size = Vector2(100, 0)
+layout_mode = 2
+text_overrun_behavior = 3
+clip_text = true
+icon_alignment = 1
diff --git a/game_settings/components/option_list/option_list.gd b/game_settings/components/option_list/option_list.gd
new file mode 100644 (file)
index 0000000..cd19b65
--- /dev/null
@@ -0,0 +1,72 @@
+@tool
+extends ggsUIComponent
+
+@export var use_ids: bool = false
+
+@onready var Btn: OptionButton = $Btn
+
+
+func _ready() -> void:
+       compatible_types = [TYPE_BOOL, TYPE_INT]
+       if Engine.is_editor_hint():
+               return
+       
+       super()
+       Btn.item_selected.connect(_on_Btn_item_selected)
+       
+       Btn.pressed.connect(_on_Btn_pressed)
+       Btn.mouse_entered.connect(_on_Btn_mouse_entered)
+       Btn.focus_entered.connect(_on_Btn_focus_entered)
+       Btn.item_focused.connect(_on_Btn_item_focused)
+
+
+func init_value() -> void:
+       super()
+       
+       var hint_array = setting.value_hint_string.split(",")
+       for hint in hint_array:
+               Btn.add_item(hint)
+       
+       if use_ids:
+               Btn.select(Btn.get_item_index(setting_value))
+       else:
+               Btn.select(setting_value)
+
+
+func _on_Btn_item_selected(item_index: int) -> void:
+       GGS.play_sfx(GGS.SFX.INTERACT)
+       
+       if use_ids:
+               setting_value = Btn.get_item_id(item_index)
+       else:
+               setting_value = item_index
+       if apply_on_change:
+               apply_setting()
+
+
+### Setting
+
+func reset_setting() -> void:
+       super()
+       Btn.select(setting_value)
+
+
+### SFX
+
+func _on_Btn_pressed() -> void:
+       GGS.play_sfx(GGS.SFX.FOCUS)
+
+
+func _on_Btn_mouse_entered() -> void:
+       GGS.play_sfx(GGS.SFX.MOUSE_OVER)
+       
+       if grab_focus_on_mouse_over:
+               Btn.grab_focus()
+
+
+func _on_Btn_focus_entered() -> void:
+       GGS.play_sfx(GGS.SFX.FOCUS)
+
+
+func _on_Btn_item_focused(_index: int) -> void:
+       GGS.play_sfx(GGS.SFX.FOCUS)
diff --git a/game_settings/components/option_list/option_list.tscn b/game_settings/components/option_list/option_list.tscn
new file mode 100644 (file)
index 0000000..7bf0c1a
--- /dev/null
@@ -0,0 +1,11 @@
+[gd_scene load_steps=2 format=3 uid="uid://b7m6l0lvojrsj"]
+
+[ext_resource type="Script" path="res://game_settings/components/option_list/option_list.gd" id="1_5yk06"]
+
+[node name="OptionList" type="MarginContainer"]
+offset_right = 40.0
+offset_bottom = 40.0
+script = ExtResource("1_5yk06")
+
+[node name="Btn" type="OptionButton" parent="."]
+layout_mode = 2
diff --git a/game_settings/components/radio_list/radio_list.gd b/game_settings/components/radio_list/radio_list.gd
new file mode 100644 (file)
index 0000000..9550d7f
--- /dev/null
@@ -0,0 +1,88 @@
+@tool
+extends ggsUIComponent
+
+enum Lists {HLIST, VLIST}
+
+@export var option_ids: PackedInt32Array
+@export var active_list: Lists = Lists.HLIST
+
+var ActiveList: BoxContainer
+
+@onready var HList: HBoxContainer = $HList
+@onready var VList: VBoxContainer = $VList
+@onready var btngrp: ButtonGroup = ButtonGroup.new()
+
+
+func _ready() -> void:
+       compatible_types = [TYPE_BOOL, TYPE_INT]
+       if Engine.is_editor_hint():
+               return
+       
+       @warning_ignore("incompatible_ternary")
+       ActiveList = HList if active_list == Lists.HLIST else VList
+       
+       super()
+       btngrp.pressed.connect(_on_pressed)
+       
+       for child in ActiveList.get_children():
+               child.button_group = btngrp
+               
+               child.mouse_entered.connect(_on_AnyBtn_mouse_entered.bind(child))
+               child.focus_entered.connect(_on_AnyBtn_focus_entered)
+
+
+func init_value() -> void:
+       super()
+       
+       if not option_ids.is_empty():
+               _set_button_pressed(option_ids.find(setting_value), true)
+       else:
+               _set_button_pressed(setting_value, true)
+
+
+func _set_button_pressed(btn_index: int, pressed: bool) -> void:
+       ActiveList.get_child(btn_index).button_pressed = pressed
+
+
+func _get_child_index(target_child: BaseButton) -> int:
+       var i: int = 0
+       for child in ActiveList.get_children():
+               if child == target_child:
+                       return i
+               
+               i += 1
+       
+       return -1
+
+
+func _on_pressed(button: BaseButton) -> void:
+       GGS.play_sfx(GGS.SFX.INTERACT)
+       
+       var child_index: int = _get_child_index(button)
+       if not option_ids.is_empty():
+               setting_value = option_ids[child_index]
+       else:
+               setting_value = child_index
+       
+       if apply_on_change:
+               apply_setting()
+
+
+### Setting
+
+func reset_setting() -> void:
+       super()
+       _set_button_pressed(setting_value, true)
+
+
+### SFX
+
+func _on_AnyBtn_mouse_entered(Btn: Button) -> void:
+       GGS.play_sfx(GGS.SFX.MOUSE_OVER)
+       
+       if grab_focus_on_mouse_over:
+               Btn.grab_focus()
+
+
+func _on_AnyBtn_focus_entered() -> void:
+       GGS.play_sfx(GGS.SFX.FOCUS)
diff --git a/game_settings/components/radio_list/radio_list.tscn b/game_settings/components/radio_list/radio_list.tscn
new file mode 100644 (file)
index 0000000..2e446b6
--- /dev/null
@@ -0,0 +1,15 @@
+[gd_scene load_steps=2 format=3 uid="uid://c11tbsolk6dnu"]
+
+[ext_resource type="Script" path="res://game_settings/components/radio_list/radio_list.gd" id="1_47eay"]
+
+[node name="RadioList" type="MarginContainer"]
+offset_bottom = 648.0
+script = ExtResource("1_47eay")
+
+[node name="HList" type="HBoxContainer" parent="."]
+layout_mode = 2
+mouse_filter = 2
+
+[node name="VList" type="VBoxContainer" parent="."]
+layout_mode = 2
+mouse_filter = 2
diff --git a/game_settings/components/slider/slider.gd b/game_settings/components/slider/slider.gd
new file mode 100644 (file)
index 0000000..49f38f3
--- /dev/null
@@ -0,0 +1,47 @@
+@tool
+extends ggsUIComponent
+
+@onready var slider: HSlider = $Slider
+
+
+func _ready() -> void:
+       compatible_types = [TYPE_INT, TYPE_FLOAT]
+       if Engine.is_editor_hint():
+               return
+       
+       super()
+       slider.value_changed.connect(_on_Slider_value_changed)
+       slider.mouse_entered.connect(_on_Slider_mouse_entered)
+       slider.focus_entered.connect(_on_Slider_focus_entered)
+
+
+func init_value() -> void:
+       super()
+       slider.set_value_no_signal(setting_value)
+
+
+func _on_Slider_value_changed(new_value: float) -> void:
+       setting_value = new_value
+       
+       if apply_on_change:
+               apply_setting()
+
+
+### Setting
+
+func reset_setting() -> void:
+       super()
+       slider.value = setting_value
+
+
+### SFX
+
+func _on_Slider_mouse_entered() -> void:
+       GGS.play_sfx(GGS.SFX.MOUSE_OVER)
+       
+       if grab_focus_on_mouse_over:
+               slider.grab_focus()
+
+
+func _on_Slider_focus_entered() -> void:
+       GGS.play_sfx(GGS.SFX.FOCUS)
diff --git a/game_settings/components/slider/slider.tscn b/game_settings/components/slider/slider.tscn
new file mode 100644 (file)
index 0000000..29785e8
--- /dev/null
@@ -0,0 +1,13 @@
+[gd_scene load_steps=2 format=3 uid="uid://ds06mwhee8ygm"]
+
+[ext_resource type="Script" path="res://game_settings/components/slider/slider.gd" id="1_4rmgf"]
+
+[node name="Slider" type="MarginContainer"]
+offset_right = 40.0
+offset_bottom = 40.0
+script = ExtResource("1_4rmgf")
+
+[node name="Slider" type="HSlider" parent="."]
+custom_minimum_size = Vector2(100, 0)
+layout_mode = 2
+size_flags_vertical = 4
diff --git a/game_settings/components/spinbox/spinbox.gd b/game_settings/components/spinbox/spinbox.gd
new file mode 100644 (file)
index 0000000..fa013a6
--- /dev/null
@@ -0,0 +1,36 @@
+@tool
+extends ggsUIComponent
+
+@onready var spin_box: SpinBox = $SpinBox
+@onready var Field: LineEdit = spin_box.get_line_edit()
+
+
+func _ready() -> void:
+       compatible_types = [TYPE_INT, TYPE_FLOAT]
+       if Engine.is_editor_hint():
+               return
+       
+       super()
+       spin_box.value_changed.connect(_on_SpinBox_value_changed)
+       Field.context_menu_enabled = false
+
+
+func init_value() -> void:
+       super()
+       spin_box.set_value_no_signal(setting_value)
+       Field.text = str(setting_value)
+
+
+func _on_SpinBox_value_changed(new_value: float) -> void:
+       setting_value = new_value
+       GGS.play_sfx(GGS.SFX.INTERACT)
+       if apply_on_change:
+               apply_setting()
+
+
+### Setting
+
+func reset_setting() -> void:
+       super()
+       spin_box.value = setting_value
+       Field.text = str(setting_value)
diff --git a/game_settings/components/spinbox/spinbox.tscn b/game_settings/components/spinbox/spinbox.tscn
new file mode 100644 (file)
index 0000000..92d12b1
--- /dev/null
@@ -0,0 +1,11 @@
+[gd_scene load_steps=2 format=3 uid="uid://bqi00h7i7sg3u"]
+
+[ext_resource type="Script" path="res://game_settings/components/spinbox/spinbox.gd" id="1_ovbvt"]
+
+[node name="SpinBox" type="MarginContainer"]
+offset_right = 40.0
+offset_bottom = 40.0
+script = ExtResource("1_ovbvt")
+
+[node name="SpinBox" type="SpinBox" parent="."]
+layout_mode = 2
diff --git a/game_settings/components/switch/switch.tscn b/game_settings/components/switch/switch.tscn
new file mode 100644 (file)
index 0000000..e79faa1
--- /dev/null
@@ -0,0 +1,14 @@
+[gd_scene load_steps=2 format=3 uid="uid://cha8xesfthpfk"]
+
+[ext_resource type="Script" path="res://game_settings/components/_shared_scripts/binary_selection.gd" id="1_tff36"]
+
+[node name="Switch" type="MarginContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_tff36")
+
+[node name="Btn" type="CheckButton" parent="."]
+layout_mode = 2
diff --git a/game_settings/components/text_field/text_field.gd b/game_settings/components/text_field/text_field.gd
new file mode 100644 (file)
index 0000000..e6657f1
--- /dev/null
@@ -0,0 +1,32 @@
+@tool
+extends ggsUIComponent
+
+@onready var TextField: LineEdit = $TextField
+
+
+func _ready() -> void:
+       compatible_types = [TYPE_STRING]
+       if Engine.is_editor_hint():
+               return
+       
+       super()
+       TextField.text_submitted.connect(_on_TextField_text_submitted)
+
+
+func init_value() -> void:
+       super()
+       TextField.text = setting_value
+
+
+func _on_TextField_text_submitted(submitted_text: String) -> void:
+       setting_value = submitted_text
+       GGS.play_sfx(GGS.SFX.INTERACT)
+       if apply_on_change:
+               apply_setting()
+
+
+### Setting
+
+func reset_setting() -> void:
+       super()
+       TextField.text = setting_value
diff --git a/game_settings/components/text_field/text_field.tscn b/game_settings/components/text_field/text_field.tscn
new file mode 100644 (file)
index 0000000..38f49c7
--- /dev/null
@@ -0,0 +1,15 @@
+[gd_scene load_steps=2 format=3 uid="uid://di8r6amxunq7q"]
+
+[ext_resource type="Script" path="res://game_settings/components/text_field/text_field.gd" id="1_u6s1s"]
+
+[node name="TextField" type="MarginContainer"]
+offset_right = 40.0
+offset_bottom = 40.0
+script = ExtResource("1_u6s1s")
+
+[node name="TextField" type="LineEdit" parent="."]
+custom_minimum_size = Vector2(100, 0)
+layout_mode = 2
+context_menu_enabled = false
+caret_blink = true
+caret_blink_interval = 0.5
diff --git a/game_settings/components/toggle_btn/toggle_btn.tscn b/game_settings/components/toggle_btn/toggle_btn.tscn
new file mode 100644 (file)
index 0000000..738b396
--- /dev/null
@@ -0,0 +1,12 @@
+[gd_scene load_steps=2 format=3 uid="uid://d10row618jwcs"]
+
+[ext_resource type="Script" path="res://game_settings/components/_shared_scripts/binary_selection.gd" id="1_j77ap"]
+
+[node name="ToggleBtn" type="MarginContainer"]
+offset_right = 40.0
+offset_bottom = 40.0
+script = ExtResource("1_j77ap")
+
+[node name="Btn" type="Button" parent="."]
+layout_mode = 2
+toggle_mode = true
diff --git a/game_settings/settings/audio/Volume.tres b/game_settings/settings/audio/Volume.tres
new file mode 100644 (file)
index 0000000..18ce3b2
--- /dev/null
@@ -0,0 +1,15 @@
+[gd_resource type="Resource" load_steps=2 format=3 uid="uid://bck4fmn8umbkd"]
+
+[ext_resource type="Script" path="res://game_settings/templates/audio/audio_volume.gd" id="1_1o3rv"]
+
+[resource]
+resource_name = "Volume"
+script = ExtResource("1_1o3rv")
+audio_bus = "Master"
+current = 38.0
+default = 100.0
+name = "Volume"
+category = "audio"
+value_type = 3
+value_hint = 1
+value_hint_string = "0,100"
diff --git a/game_settings/settings/controls/Movement.tres b/game_settings/settings/controls/Movement.tres
new file mode 100644 (file)
index 0000000..0165b98
--- /dev/null
@@ -0,0 +1,16 @@
+[gd_resource type="Resource" script_class="ggsInputSetting" load_steps=2 format=3 uid="uid://bk4c4kx1xymkr"]
+
+[ext_resource type="Script" path="res://game_settings/templates/input.gd" id="1_ic3x7"]
+
+[resource]
+script = ExtResource("1_ic3x7")
+action = ""
+event_index = 0
+type = 0
+current = [-1, -1]
+default = [-1, -1]
+name = ""
+category = ""
+value_type = 28
+value_hint = 0
+value_hint_string = ""
diff --git a/game_settings/settings/nonempty.txt b/game_settings/settings/nonempty.txt
new file mode 100644 (file)
index 0000000..baf6c62
--- /dev/null
@@ -0,0 +1,2 @@
+Prevents this directory from getting auto-deleted.
+Feel free to delete this file after adding at least one setting.
diff --git a/game_settings/settings/video/Fullscreen.tres b/game_settings/settings/video/Fullscreen.tres
new file mode 100644 (file)
index 0000000..4f9463d
--- /dev/null
@@ -0,0 +1,27 @@
+[gd_resource type="Resource" load_steps=4 format=3 uid="uid://cawbp8dvbqhkf"]
+
+[ext_resource type="Script" path="res://game_settings/templates/display/display_fullscreen.gd" id="1_die2y"]
+[ext_resource type="Script" path="res://addons/ggs/classes/resources/ggs_setting.gd" id="2_bxgen"]
+
+[sub_resource type="Resource" id="Resource_8icxp"]
+resource_name = "Fullscreen"
+script = ExtResource("2_bxgen")
+current = false
+default = null
+name = "Fullscreen"
+category = "video"
+value_type = 0
+value_hint = 0
+value_hint_string = ""
+
+[resource]
+resource_name = "Fullscreen"
+script = ExtResource("1_die2y")
+size_setting = SubResource("Resource_8icxp")
+current = false
+default = false
+name = "Fullscreen"
+category = "video"
+value_type = 1
+value_hint = 0
+value_hint_string = ""
diff --git a/game_settings/settings/video/Scale.tres b/game_settings/settings/video/Scale.tres
new file mode 100644 (file)
index 0000000..fd568d9
--- /dev/null
@@ -0,0 +1,15 @@
+[gd_resource type="Resource" load_steps=2 format=3 uid="uid://dxhmqp6ccam1s"]
+
+[ext_resource type="Script" path="res://game_settings/templates/display/display_scale.gd" id="1_bd2ce"]
+
+[resource]
+resource_name = "Scale"
+script = ExtResource("1_bd2ce")
+scales = Array[float]([1.0, 1.5, 2.0])
+current = 0
+default = 0
+name = "Scale"
+category = "video"
+value_type = 2
+value_hint = 2
+value_hint_string = "x1,x1.5,x2"
diff --git a/game_settings/settings/video/Size.tres b/game_settings/settings/video/Size.tres
new file mode 100644 (file)
index 0000000..9ba0546
--- /dev/null
@@ -0,0 +1,15 @@
+[gd_resource type="Resource" load_steps=2 format=3 uid="uid://cb4y1kgd71auv"]
+
+[ext_resource type="Script" path="res://game_settings/templates/display/display_size.gd" id="1_clu5o"]
+
+[resource]
+resource_name = "Size"
+script = ExtResource("1_clu5o")
+sizes = Array[Vector2]([Vector2(1920, 1080), Vector2(2560, 1440)])
+current = 0
+default = 0
+name = "Size"
+category = "video"
+value_type = 2
+value_hint = 2
+value_hint_string = "1920 x 1080,2560 x 1440"
diff --git a/game_settings/templates/audio/audio_mute.gd b/game_settings/templates/audio/audio_mute.gd
new file mode 100644 (file)
index 0000000..f37b25f
--- /dev/null
@@ -0,0 +1,41 @@
+@tool
+extends ggsSetting
+
+var audio_bus: String = "None"
+
+
+func _init() -> void:
+       value_type = TYPE_BOOL
+       default = false
+
+
+func apply(value: bool) -> void:
+       if audio_bus == "None":
+               printerr("GGS - Apply Setting (audio_mute.gd): No audio bus is selected.")
+               return
+       
+       var bus_index: int = AudioServer.get_bus_index(audio_bus)
+       AudioServer.set_bus_mute(bus_index, value)
+
+
+### Bus Name
+
+func _get_property_list() -> Array:
+       var hint_string: String = ",".join(_get_audio_buses())
+       return [{
+               "name": "audio_bus",
+               "type": TYPE_STRING,
+               "usage": PROPERTY_USAGE_DEFAULT,
+               "hint": PROPERTY_HINT_ENUM,
+               "hint_string": hint_string,
+       }]
+
+
+func _get_audio_buses() -> PackedStringArray:
+       var buses: PackedStringArray = ["None"]
+       for bus_index in range(AudioServer.bus_count):
+               var bus: String = AudioServer.get_bus_name(bus_index)
+               buses.append(bus)
+       
+       return buses
+
diff --git a/game_settings/templates/audio/audio_volume.gd b/game_settings/templates/audio/audio_volume.gd
new file mode 100644 (file)
index 0000000..5049bcb
--- /dev/null
@@ -0,0 +1,44 @@
+@tool
+extends ggsSetting
+
+var audio_bus: String = "None"
+
+
+func _init() -> void:
+       value_type = TYPE_FLOAT
+       value_hint = PROPERTY_HINT_RANGE
+       value_hint_string = "0,100"
+       default = 80.0
+
+
+func apply(value: float) -> void:
+       if audio_bus == "None":
+               printerr("GGS - Apply Setting (audio_volume.gd): No audio bus is selected.")
+               return
+       
+       var bus_index: int = AudioServer.get_bus_index(audio_bus)
+       var volume_db: float = linear_to_db(value/100)
+       AudioServer.set_bus_volume_db(bus_index, volume_db)
+
+
+### Bus Name
+
+func _get_property_list() -> Array:
+       var hint_string: String = ",".join(_get_audio_buses())
+       return [{
+               "name": "audio_bus",
+               "type": TYPE_STRING,
+               "usage": PROPERTY_USAGE_DEFAULT,
+               "hint": PROPERTY_HINT_ENUM,
+               "hint_string": hint_string,
+       }]
+
+
+func _get_audio_buses() -> PackedStringArray:
+       var buses: PackedStringArray = ["None"]
+       for bus_index in range(AudioServer.bus_count):
+               var bus: String = AudioServer.get_bus_name(bus_index)
+               buses.append(bus)
+       
+       return buses
+
diff --git a/game_settings/templates/display/display_fullscreen.gd b/game_settings/templates/display/display_fullscreen.gd
new file mode 100644 (file)
index 0000000..9493d03
--- /dev/null
@@ -0,0 +1,23 @@
+@tool
+extends ggsSetting
+
+@export var size_setting: ggsSetting
+
+
+func _init() -> void:
+       value_type = TYPE_BOOL
+       default = false
+
+
+func apply(value: bool) -> void:
+       var window_mode: DisplayServer.WindowMode
+       match value:
+               true:
+                       window_mode = DisplayServer.WINDOW_MODE_FULLSCREEN
+               false:
+                       window_mode = DisplayServer.WINDOW_MODE_WINDOWED
+       
+       DisplayServer.window_set_mode(window_mode)
+       
+       if size_setting != null:
+               size_setting.set_current(size_setting.current)
diff --git a/game_settings/templates/display/display_scale.gd b/game_settings/templates/display/display_scale.gd
new file mode 100644 (file)
index 0000000..786fa6d
--- /dev/null
@@ -0,0 +1,39 @@
+@tool
+extends ggsSetting
+
+@export var scales: Array[float]: set = set_scales
+
+
+func _init() -> void:
+       value_type = TYPE_INT
+       value_hint = PROPERTY_HINT_ENUM
+       value_hint_string = ",".join(_get_scales_strings())
+
+
+func apply(value: int) -> void:
+       var scale: float = scales[value]
+       var base_w: int = ProjectSettings.get_setting("display/window/size/viewport_width")
+       var base_h: int = ProjectSettings.get_setting("display/window/size/viewport_height")
+       var size: Vector2 = Vector2(base_w, base_h) * scale
+       size = ggsUtils.window_clamp_to_screen(size)
+
+       DisplayServer.window_set_size(size)
+       ggsUtils.center_window()
+
+
+### Scales
+
+func set_scales(value: Array[float]) -> void:
+       scales = value
+       
+       if Engine.is_editor_hint():
+               value_hint_string = ",".join(_get_scales_strings())
+               ggsUtils.get_editor_interface().call_deferred("inspect_object", self)
+
+
+func _get_scales_strings() -> PackedStringArray:
+       var scales_strings: PackedStringArray = []
+       for scale in scales:
+               scales_strings.append("x%s"%[str(scale)])
+       
+       return scales_strings
diff --git a/game_settings/templates/display/display_size.gd b/game_settings/templates/display/display_size.gd
new file mode 100644 (file)
index 0000000..65f5be1
--- /dev/null
@@ -0,0 +1,37 @@
+@tool
+extends ggsSetting
+
+@export var sizes: Array[Vector2]: set = set_sizes
+
+
+func _init() -> void:
+       value_type = TYPE_INT
+       value_hint = PROPERTY_HINT_ENUM
+       value_hint_string = ",".join(_get_sizes_strings())
+
+
+func apply(value: int) -> void:
+       var size: Vector2 = sizes[value]
+       size = ggsUtils.window_clamp_to_screen(size)
+       
+       DisplayServer.window_set_size(size)
+       ggsUtils.center_window()
+
+
+### Sizes
+
+func set_sizes(value: Array[Vector2]) -> void:
+       sizes = value
+       
+       if Engine.is_editor_hint():
+               value_hint_string = ",".join(_get_sizes_strings())
+               ggsUtils.get_editor_interface().call_deferred("inspect_object", self)
+
+
+func _get_sizes_strings() -> PackedStringArray:
+       var sizes_strings: PackedStringArray
+       for size in sizes:
+               var formatted_size: String = str(size).trim_prefix("(").trim_suffix(")").replace(",", " x")
+               sizes_strings.append(formatted_size)
+       
+       return sizes_strings
diff --git a/game_settings/templates/input.gd b/game_settings/templates/input.gd
new file mode 100644 (file)
index 0000000..3f45285
--- /dev/null
@@ -0,0 +1,96 @@
+@tool
+extends ggsSetting
+class_name ggsInputSetting
+
+var action: String
+var event_index: int
+var type: ggsInputHelper.InputType = ggsInputHelper.InputType.INVALID
+var default_as_event: InputEvent: set = set_default_as_event
+var current_as_event: InputEvent: set = set_current_as_event
+var input_helper: ggsInputHelper = ggsInputHelper.new()
+
+
+func _init() -> void:
+       read_only_values = true
+       value_type = TYPE_ARRAY
+       default = [-1, -1]
+
+
+func _get_property_list() -> Array:
+       var read_only: int =  PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_READ_ONLY
+       var allowed_event: String = _get_allowed_event_types()
+       
+       var properties: Array
+       properties.append_array([
+               {"name": "action", "type": TYPE_STRING, "usage": read_only},
+               {"name": "event_index", "type": TYPE_INT, "usage": read_only},
+               {"name": "type", "type": TYPE_INT, "usage": PROPERTY_USAGE_STORAGE, "hint": PROPERTY_HINT_ENUM, "hint_string": "None,Keyboard,Mouse,Gamepad Button,Gamepad Motion"},
+               {"name": "default_as_event", "type": TYPE_OBJECT, "usage": read_only, "hint": PROPERTY_HINT_RESOURCE_TYPE, "hint_string": allowed_event},
+               {"name": "current_as_event", "type": TYPE_OBJECT, "usage": read_only if type == input_helper.InputType.INVALID else PROPERTY_USAGE_DEFAULT, "hint": PROPERTY_HINT_RESOURCE_TYPE, "hint_string": allowed_event},
+       ])
+       
+       return properties
+
+
+func update_current_as_event() -> void:
+       var event: InputEvent = input_helper.create_event_from_type(current[0])
+       input_helper.set_event_id(event, current[1])
+       current_as_event = event
+
+
+func _get_allowed_event_types() -> String:
+       if type == input_helper.InputType.KEYBOARD or type == input_helper.InputType.MOUSE:
+               return "InputEventKey,InputEventMouseButton"
+       
+       if type == input_helper.InputType.GP_BTN or type == input_helper.InputType.GP_MOTION:
+               return "InputEventJoypadButton,InputEventJoypadMotion"
+       
+       return ""
+
+
+### Updating Current and Default
+
+func set_default_as_event(value: InputEvent) -> void:
+       default_as_event = value
+       
+       if not Engine.is_editor_hint():
+               return
+       
+       type = input_helper.get_event_type(value)
+       default = [type, input_helper.get_event_id(value)]
+       ggsUtils.get_editor_interface().inspect_object.call_deferred(self)
+
+
+func set_current_as_event(value: InputEvent) -> void:
+       current_as_event = value
+       
+       if not Engine.is_editor_hint():
+               return
+       
+       type = input_helper.get_event_type(current_as_event)
+       current = [type, input_helper.get_event_id(value)]
+       if value != null:
+               value.changed.connect(_on_current_event_changed)
+
+
+func _on_current_event_changed() -> void:
+       current = [type, input_helper.get_event_id(current_as_event)]
+
+
+### Applying
+
+func apply(value: Array) -> void:
+       if value.is_empty() or value[0] == -1 or value[1] == -1:
+               return
+       
+       var event: InputEvent = input_helper.create_event_from_type(value[0])
+       input_helper.set_event_id(event, value[1])
+       
+       var action_events: Array[InputEvent] = InputMap.action_get_events(action)
+       action_events.remove_at(event_index)
+       action_events.insert(event_index, event)
+       
+       InputMap.action_erase_events(action)
+       for _event_ in action_events:
+               InputMap.action_add_event(action, _event_)
+
diff --git a/scenes/settings/Settings.tscn b/scenes/settings/Settings.tscn
new file mode 100644 (file)
index 0000000..0a5f758
--- /dev/null
@@ -0,0 +1,184 @@
+[gd_scene load_steps=12 format=3 uid="uid://x67a7lfi3ad2"]
+
+[ext_resource type="Texture2D" uid="uid://b1hgu1e1abcgx" path="res://assets/title_screen_background.png" id="1_1a5ox"]
+[ext_resource type="Script" path="res://scenes/settings/settings.gd" id="1_xxpof"]
+[ext_resource type="PackedScene" uid="uid://c3uole0l14mxh" path="res://components/button/MainButton.tscn" id="2_eexhs"]
+[ext_resource type="PackedScene" uid="uid://cha8xesfthpfk" path="res://game_settings/components/switch/switch.tscn" id="4_kfh4a"]
+[ext_resource type="Resource" uid="uid://cawbp8dvbqhkf" path="res://game_settings/settings/video/Fullscreen.tres" id="5_ekhlv"]
+[ext_resource type="PackedScene" uid="uid://ds06mwhee8ygm" path="res://game_settings/components/slider/slider.tscn" id="6_4p6is"]
+[ext_resource type="Resource" uid="uid://bck4fmn8umbkd" path="res://game_settings/settings/audio/Volume.tres" id="7_unv4k"]
+[ext_resource type="FontFile" uid="uid://b13vwg53k4dkq" path="res://assets/fonts/Agora.otf" id="8_25cdq"]
+[ext_resource type="Script" path="res://addons/label_font_auto_sizer/label_auto_sizer.gd" id="8_p1ijv"]
+[ext_resource type="Resource" uid="uid://cb4y1kgd71auv" path="res://game_settings/settings/video/Size.tres" id="9_e4be1"]
+[ext_resource type="PackedScene" uid="uid://b7m6l0lvojrsj" path="res://game_settings/components/option_list/option_list.tscn" id="9_p0ng3"]
+
+[node name="Settings" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_xxpof")
+metadata/_edit_vertical_guides_ = [928.0, 992.0]
+
+[node name="Background" type="TextureRect" parent="."]
+layout_mode = 0
+scale = Vector2(0.5, 0.5)
+texture = ExtResource("1_1a5ox")
+
+[node name="ColorRect" type="ColorRect" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+color = Color(0, 0, 0, 0.588235)
+
+[node name="BackButton" parent="." instance=ExtResource("2_eexhs")]
+layout_mode = 1
+anchors_preset = 2
+anchor_left = 0.0
+anchor_top = 1.0
+anchor_right = 0.0
+anchor_bottom = 1.0
+offset_left = 250.0
+offset_top = -230.0
+offset_right = 1018.0
+offset_bottom = 10.0
+grow_horizontal = 1
+grow_vertical = 0
+scale = Vector2(0.5, 0.5)
+text = "Back"
+
+[node name="Switch" parent="." instance=ExtResource("4_kfh4a")]
+layout_mode = 1
+anchors_preset = 5
+anchor_left = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.0
+offset_left = -80.0
+offset_top = 408.0
+offset_right = -36.0
+offset_bottom = 448.0
+grow_vertical = 1
+setting = ExtResource("5_ekhlv")
+apply_on_change = true
+
+[node name="Slider" parent="." instance=ExtResource("6_4p6is")]
+layout_mode = 1
+anchors_preset = 5
+anchor_left = 0.5
+anchor_right = 0.5
+offset_left = -192.0
+offset_top = 280.0
+offset_right = -32.0
+offset_bottom = 320.0
+grow_horizontal = 2
+setting = ExtResource("7_unv4k")
+apply_on_change = true
+
+[node name="SettingsLabelAutoSizer" type="Label" parent="."]
+layout_mode = 1
+anchors_preset = 10
+anchor_right = 1.0
+offset_top = 100.0
+offset_bottom = 200.0
+grow_horizontal = 2
+theme_override_colors/font_outline_color = Color(0.376471, 0.427451, 1, 1)
+theme_override_constants/outline_size = 14
+theme_override_fonts/font = ExtResource("8_25cdq")
+theme_override_font_sizes/font_size = 64
+text = "Settings"
+horizontal_alignment = 1
+vertical_alignment = 1
+autowrap_mode = 3
+clip_text = true
+uppercase = true
+script = ExtResource("8_p1ijv")
+_size_just_modified_by_autosizer = false
+_set_defaults = true
+_base_font_size = 64
+_current_font_size = 64
+_last_size_state = 1
+
+[node name="FullscreenLabelAutoSizer" type="Label" parent="."]
+layout_mode = 1
+anchors_preset = 5
+anchor_left = 0.5
+anchor_right = 0.5
+offset_left = 32.0
+offset_top = 408.0
+offset_right = 216.0
+offset_bottom = 448.0
+grow_horizontal = 2
+theme_override_font_sizes/font_size = 24
+text = "Fullscreen"
+autowrap_mode = 3
+clip_text = true
+script = ExtResource("8_p1ijv")
+_size_just_modified_by_autosizer = false
+_set_defaults = true
+_base_font_size = 24
+_current_font_size = 24
+_last_size_state = 1
+
+[node name="VolumeLabelAutoSizer" type="Label" parent="."]
+layout_mode = 1
+anchors_preset = 5
+anchor_left = 0.5
+anchor_right = 0.5
+offset_left = 32.0
+offset_top = 280.0
+offset_right = 240.0
+offset_bottom = 320.0
+grow_horizontal = 2
+theme_override_font_sizes/font_size = 24
+text = "Volume
+"
+autowrap_mode = 3
+clip_text = true
+script = ExtResource("8_p1ijv")
+_size_just_modified_by_autosizer = false
+_set_defaults = true
+_base_font_size = 24
+_current_font_size = 24
+_last_size_state = 1
+
+[node name="ResolutionLabelAutoSizer" type="Label" parent="."]
+layout_mode = 1
+anchors_preset = 5
+anchor_left = 0.5
+anchor_right = 0.5
+offset_left = 32.0
+offset_top = 472.0
+offset_right = 232.0
+offset_bottom = 512.0
+grow_horizontal = 2
+theme_override_font_sizes/font_size = 24
+text = "Resolution"
+autowrap_mode = 3
+clip_text = true
+script = ExtResource("8_p1ijv")
+_size_just_modified_by_autosizer = false
+_set_defaults = true
+_base_font_size = 24
+_current_font_size = 24
+_last_size_state = 1
+
+[node name="ResolutionOptionList" parent="." instance=ExtResource("9_p0ng3")]
+layout_mode = 1
+anchors_preset = 5
+anchor_left = 0.5
+anchor_right = 0.5
+offset_left = -192.0
+offset_top = 472.0
+offset_right = -32.0
+offset_bottom = 512.0
+grow_horizontal = 2
+use_ids = true
+setting = ExtResource("9_e4be1")
+apply_on_change = true
+
+[connection signal="pressed" from="BackButton" to="." method="_on_back_button_pressed"]
diff --git a/scenes/settings/settings.gd b/scenes/settings/settings.gd
new file mode 100644 (file)
index 0000000..c0cd1e5
--- /dev/null
@@ -0,0 +1,5 @@
+extends Control
+
+
+func _on_back_button_pressed():
+               SceneManager.change_scene(Constants.MainMenu_path, Constants.default_changeScene_config)