├── .gitattributes
├── .github
└── FUNDING.yml
├── .gitignore
├── LICENSE
├── README.md
├── addons
└── gdscript_sections
│ ├── LICENSE
│ ├── README.md
│ ├── data
│ ├── .gdignore
│ ├── data.json
│ ├── id.dat
│ ├── section1033.tres
│ ├── section1848.tres
│ ├── section1849.tres
│ └── section1850.tres
│ ├── data_helper.gd
│ ├── dialog.gd
│ ├── dialog.tscn
│ ├── gdscript_sections.gd
│ ├── id_helper.gd
│ ├── logo.png
│ ├── logo.png.import
│ ├── overlay_display.gd
│ ├── overlay_display.tscn
│ ├── plugin.cfg
│ ├── section.gd
│ ├── section_display.tscn
│ └── showcase.gif
└── project.godot
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Normalize EOL for all files that Git considers text files.
2 | * text=auto eol=lf
3 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | #github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: VinhPC # Replace with a single Patreon username
5 | #open_collective: # Replace with a single Open Collective username
6 | ko_fi: vinhpc # Replace with a single Ko-fi username
7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | #liberapay: # Replace with a single Liberapay username
10 | #issuehunt: # Replace with a single IssueHunt username
11 | #otechie: # Replace with a single Otechie username
12 | #lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: ['https://www.buymeacoffee.com/vinhpc'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Godot 4+ specific ignores
2 | .godot/
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 VPC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
14 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
GDScript Sections
32 |
33 |
34 | A small addon that improves organization and navigation when scripting in the Godot editor.
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | Table of Contents
46 |
47 | - About The Project
48 | - How To Use
49 | - Installation
50 | - Contribute
51 | - License
52 |
53 |
54 |
55 |
56 |
57 |
58 | ## About The Project
59 |
60 | 
61 |
62 | ### Built With
63 |
64 | 
65 |
66 |
67 | ### Version: Godot 4.1
68 |
69 |
70 | (back to top)
71 |
72 |
73 |
74 | ## How To Use
75 |
76 | ### CTRL+U
77 |
78 | - Toggles visibility of the addon (Godot's script editor must have focus)
79 |
80 | ### CTRL+J
81 |
82 | - Toggles visibility of sections (Godot's script editor must have focus)
83 |
84 | ### To change text of a section
85 |
86 | - Simply click on its current text
87 |
88 |
89 | (back to top)
90 |
91 |
92 |
93 | ## Installation
94 |
95 | ### Using Godot Asset Library
96 | - In-editor `AssetLib`
97 | 1. Search for `GDScript Sections` in Godot's in-editor `AssetLib` and press download
98 |
99 | 2. Enable the addon in `Project -> Project Settings -> Plugins`
100 |
101 |
102 | - Online
103 | 1. Dowload the ZIP archive from [link](https://godotengine.org/asset-library/asset/2078).
104 |
105 | 2. Import the folder `gdscript_sections/` into your Godot project's `addons/` folder (Godot v4.1).
106 |
107 | 3. Enable the addon in `Project -> Project Settings -> Plugins`
108 |
109 | ### Manually
110 | 1. Clone the repo OR download and extract the ZIP archive.
111 | ```sh
112 | git clone https://github.com/VinhPhmCng/gdscript-sections.git
113 | ```
114 |
115 | 2. Import the folder `gdscript_sections/` into your Godot project's `addons/` folder (Godot v4.1).
116 |
117 | 3. Enable the addon in `Project -> Project Settings -> Plugins`
118 |
119 | (back to top)
120 |
121 |
122 |
123 | ## Contribute
124 |
125 | - Any contribution is much appreciated.
126 |
127 |
128 | (back to top)
129 |
130 |
131 |
132 | ## License
133 | [MIT License](LICENSE) © [VPC](https://github.com/VinhPhmCng)
134 |
135 |
136 | (back to top)
--------------------------------------------------------------------------------
/addons/gdscript_sections/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 VPC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/addons/gdscript_sections/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
14 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
GDScript Sections
32 |
33 |
34 | A small addon that improves organization and navigation when scripting in the Godot editor.
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | Table of Contents
46 |
47 | - About The Project
48 | - How To Use
49 | - Installation
50 | - Contribute
51 | - License
52 |
53 |
54 |
55 |
56 |
57 |
58 | ## About The Project
59 |
60 | 
61 |
62 | ### Built With
63 |
64 | 
65 |
66 |
67 | ### Version: Godot 4.1
68 |
69 |
70 | (back to top)
71 |
72 |
73 |
74 | ## How To Use
75 |
76 | ### CTRL+U
77 |
78 | - Toggles visibility of the addon (Godot's script editor must have focus)
79 |
80 | ### CTRL+J
81 |
82 | - Toggles visibility of sections (Godot's script editor must have focus)
83 |
84 | ### To change text of a section
85 |
86 | - Simply click on its current text
87 |
88 |
89 | (back to top)
90 |
91 |
92 |
93 | ## Installation
94 |
95 | ### Using Godot Asset Library
96 | - In-editor `AssetLib`
97 | 1. Search for `GDScript Sections` in Godot's in-editor `AssetLib` and press download
98 |
99 | 2. Enable the addon in `Project -> Project Settings -> Plugins`
100 |
101 |
102 | - Online
103 | 1. Dowload the ZIP archive from [link](https://godotengine.org/asset-library/asset/2078).
104 |
105 | 2. Import the folder `gdscript_sections/` into your Godot project's `addons/` folder (Godot v4.1).
106 |
107 | 3. Enable the addon in `Project -> Project Settings -> Plugins`
108 |
109 | ### Manually
110 | 1. Clone the repo OR download and extract the ZIP archive.
111 | ```sh
112 | git clone https://github.com/VinhPhmCng/gdscript-sections.git
113 | ```
114 |
115 | 2. Import the folder `gdscript_sections/` into your Godot project's `addons/` folder (Godot v4.1).
116 |
117 | 3. Enable the addon in `Project -> Project Settings -> Plugins`
118 |
119 | (back to top)
120 |
121 |
122 |
123 | ## Contribute
124 |
125 | - Any contribution is much appreciated.
126 |
127 |
128 | (back to top)
129 |
130 |
131 |
132 | ## License
133 | [MIT License](LICENSE) © [VPC](https://github.com/VinhPhmCng)
134 |
135 |
136 | (back to top)
--------------------------------------------------------------------------------
/addons/gdscript_sections/data/.gdignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VinhPhmCng/gdscript-sections/4196144b724190452898f8e4b9b70607014b8b6d/addons/gdscript_sections/data/.gdignore
--------------------------------------------------------------------------------
/addons/gdscript_sections/data/data.json:
--------------------------------------------------------------------------------
1 | {
2 | "res://addons/gdscript_sections/data_helper.gd": [],
3 | "res://addons/gdscript_sections/gdscript_sections.gd": [
4 | "res://addons/gdscript_sections/data/section1848.tres",
5 | "res://addons/gdscript_sections/data/section1849.tres",
6 | "res://addons/gdscript_sections/data/section1850.tres"
7 | ],
8 | "res://addons/gdscript_sections/id_helper.gd": [
9 | "res://addons/gdscript_sections/data/section1033.tres"
10 | ]
11 | }
--------------------------------------------------------------------------------
/addons/gdscript_sections/data/id.dat:
--------------------------------------------------------------------------------
1 | <
--------------------------------------------------------------------------------
/addons/gdscript_sections/data/section1033.tres:
--------------------------------------------------------------------------------
1 | [gd_resource type="Resource" script_class="Section" load_steps=2 format=3 uid="uid://cvaqed3pbpbop"]
2 |
3 | [ext_resource type="Script" path="res://addons/gdscript_sections/section.gd" id="1_c7rmj"]
4 |
5 | [resource]
6 | script = ExtResource("1_c7rmj")
7 | text = "TEST"
8 | location = 11
9 |
--------------------------------------------------------------------------------
/addons/gdscript_sections/data/section1848.tres:
--------------------------------------------------------------------------------
1 | [gd_resource type="Resource" script_class="Section" load_steps=2 format=3 uid="uid://bxg8hsw7e3y8d"]
2 |
3 | [ext_resource type="Script" path="res://addons/gdscript_sections/section.gd" id="1_3ekd2"]
4 |
5 | [resource]
6 | script = ExtResource("1_3ekd2")
7 | text = "Virtual functions"
8 | location = 24
9 |
--------------------------------------------------------------------------------
/addons/gdscript_sections/data/section1849.tres:
--------------------------------------------------------------------------------
1 | [gd_resource type="Resource" script_class="Section" load_steps=2 format=3 uid="uid://84kxyg82pnwo"]
2 |
3 | [ext_resource type="Script" path="res://addons/gdscript_sections/section.gd" id="1_2nm04"]
4 |
5 | [resource]
6 | script = ExtResource("1_2nm04")
7 | text = "Private functions"
8 | location = 136
9 |
--------------------------------------------------------------------------------
/addons/gdscript_sections/data/section1850.tres:
--------------------------------------------------------------------------------
1 | [gd_resource type="Resource" script_class="Section" load_steps=2 format=3 uid="uid://b2578gv7khmfg"]
2 |
3 | [ext_resource type="Script" path="res://addons/gdscript_sections/section.gd" id="1_bf5f3"]
4 |
5 | [resource]
6 | script = ExtResource("1_bf5f3")
7 | text = "Signal callbacks"
8 | location = 330
9 |
--------------------------------------------------------------------------------
/addons/gdscript_sections/data_helper.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | #class_name
3 | extends Resource
4 |
5 | ## A script that helps seperate data manipulation from the main UI
6 |
7 | const DATA_FOLDER := "res://addons/gdscript_sections/data/"
8 | const DATA_PATH := "res://addons/gdscript_sections/data/data.json"
9 | const IDHelper := preload("res://addons/gdscript_sections/id_helper.gd")
10 |
11 | var id_helper: IDHelper = IDHelper.new()
12 |
13 | var _data: Dictionary
14 |
15 |
16 | ## Returns a new unique ID
17 | func get_new_id() -> int:
18 | return id_helper.get_new_id()
19 |
20 |
21 | ## Returns True if the path exists as a key, False otherwise
22 | func is_script_enabled(path: String) -> bool:
23 | return _data.has(path)
24 |
25 |
26 | ## Adds a new path as a key
27 | func enable_script(path: String) -> void:
28 | _data[path] = []
29 | return
30 |
31 |
32 | ## Removes a path from existing keys
33 | func disable_script(path: String) -> void:
34 | for section in get_sections_paths(path):
35 | Section.remove_from_disk(section)
36 |
37 | _data.erase(path)
38 | return
39 |
40 |
41 | ## Moves value from old key to new key
42 | func update_script(old_path: String, new_path) -> void:
43 | var value := _data.get(old_path, [])
44 | _data[new_path] = value
45 | _data.erase(old_path)
46 | return
47 |
48 |
49 | ## Returns the value (an array of paths of Sections) of an enabled script's path (key)
50 | func get_sections_paths(script_path: String) -> Array:
51 | return _data.get(script_path, [])
52 |
53 |
54 | ## Returns Section resources (from disk) of an enabled script's path
55 | func get_sections(script_path: String, do_sort_by_location: bool) -> Array:
56 | var sections: Array = []
57 | for path in get_sections_paths(script_path):
58 | sections.append(Section.get_from_disk(path))
59 |
60 | if not do_sort_by_location:
61 | return sections
62 |
63 | # Does sort
64 | sections.sort_custom(func(a: Section, b: Section):
65 | if a.location < b.location:
66 | return true
67 | return false
68 | )
69 | return sections
70 |
71 |
72 | ## Adds a Section's path (value) to an enabled script's path (key)
73 | func add_section_path(script_path: String, section_path: String) -> void:
74 | var sections: Array = _data.get(script_path, [])
75 | sections.append(section_path)
76 | _data[script_path] = sections
77 | return
78 |
79 |
80 | ## Deletes a Section's path (value) from an enabled script's path (key)
81 | func delete_section_path(script_path: String, section_path: String) -> void:
82 | var sections: Array = _data[script_path]
83 | sections.erase(section_path)
84 | _data[script_path] = sections
85 | return
86 |
87 |
88 | ## Reads from disk (data.json) to _data
89 | func read() -> void:
90 | var file := FileAccess.open(DATA_PATH, FileAccess.READ)
91 | var content := file.get_as_text()
92 | var data := JSON.parse_string(content)
93 |
94 | if typeof(data) == TYPE_DICTIONARY:
95 | _data = data
96 | else:
97 | print("Loading data.json failed")
98 | return
99 |
100 |
101 | ## Write _data to disk (data.json)
102 | func write() -> void:
103 | var file := FileAccess.open(DATA_PATH, FileAccess.WRITE)
104 |
105 | var content := JSON.stringify(_data, "\t")
106 | file.store_string(content)
107 | return
108 |
--------------------------------------------------------------------------------
/addons/gdscript_sections/dialog.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | extends Window
3 |
4 | ## The addon's main User Interface
5 |
6 | signal section_added(text: String)
7 | signal sections_filtered(text: String)
8 |
9 | @onready var main: VBoxContainer = $Background/MarginContainer/Main
10 | @onready var enable_prompt: CenterContainer = $Background/MarginContainer/EnablePrompt
11 | @onready var disable_prompt: CenterContainer = $Background/MarginContainer/DisablePrompt
12 | @onready var filter_section: LineEdit = %FilterSection
13 | @onready var add_section: LineEdit = %AddSection
14 | @onready var enable_prompt_label: Label = %EnablePromptLabel
15 | @onready var disable_prompt_label: Label = %DisablePromptLabel
16 |
17 | ## From EnablePrompt to Main
18 | func show_main() -> void:
19 | enable_prompt.hide()
20 | disable_prompt.hide()
21 | main.show()
22 | return
23 |
24 |
25 | ## From DisablePrompt (Accept) to EnablePromp[br]
26 | ## Or when encounter a disabled script
27 | func prompt_enable_script(path: String) -> void:
28 | enable_prompt_label.text = "ENABLE the plugin for the script?\n\"%s\"" % path
29 |
30 | disable_prompt.hide()
31 | main.hide()
32 | enable_prompt.show()
33 | return
34 |
35 |
36 | ## From Main to DisablePrompt
37 | func prompt_disable_script(path: String) -> void:
38 | disable_prompt_label.text = "DISABLE the plugin for the script?\n\"%s\"\n(Will permanently delete all sections)" % path
39 |
40 | enable_prompt.hide()
41 | main.hide()
42 | disable_prompt.show()
43 | return
44 |
45 |
46 | ## Updates prompts' font sizes
47 | func set_font_size(to: int) -> void:
48 | enable_prompt_label.add_theme_font_size_override("font_size", to)
49 | disable_prompt_label.add_theme_font_size_override("font_size", to)
50 | return
51 |
52 | # Handles buttons' signals - concerning visibility only
53 |
54 | ## From EnablePrompt to Main
55 | func _on_Enable_pressed() -> void:
56 | enable_prompt.hide()
57 | disable_prompt.hide()
58 | main.show()
59 | return
60 |
61 |
62 | ## From Main to DisablePrompt
63 | func _on_Disable_pressed() -> void:
64 | enable_prompt.hide()
65 | main.hide()
66 | disable_prompt.show()
67 | return
68 |
69 |
70 | ## From DisablePrompt to Main
71 | func _on_DisableCancel_pressed() -> void:
72 | enable_prompt.hide()
73 | disable_prompt.hide()
74 | main.show()
75 | return
76 |
77 |
78 | ## From DisablePrompt (Accept) to EnablePrompt
79 | func _on_DisableAccept_pressed() -> void:
80 | disable_prompt.hide()
81 | main.hide()
82 | enable_prompt.show()
83 | return
84 |
85 |
86 | ## Handles adding a new section by Enter
87 | func _on_AddSection_text_submitted(new_text: String) -> void:
88 | section_added.emit(new_text)
89 | add_section.clear()
90 | return
91 |
92 |
93 | ## Handles adding a new section by button
94 | func _on_AddButton_pressed() -> void:
95 | if add_section.text.length() > 0:
96 | section_added.emit(add_section.text)
97 | add_section.clear()
98 | return
99 |
100 |
101 | func _on_FilterSection_text_changed(new_text: String) -> void:
102 | sections_filtered.emit(new_text)
103 | return
104 |
--------------------------------------------------------------------------------
/addons/gdscript_sections/dialog.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=2 format=3 uid="uid://djl3cfxiqbiq1"]
2 |
3 | [ext_resource type="Script" path="res://addons/gdscript_sections/dialog.gd" id="1_cvg1e"]
4 |
5 | [node name="Dialog" type="Window"]
6 | title = "GDScript Sections"
7 | initial_position = 4
8 | size = Vector2i(452, 362)
9 | exclusive = true
10 | script = ExtResource("1_cvg1e")
11 |
12 | [node name="Background" type="PanelContainer" parent="."]
13 | anchors_preset = 15
14 | anchor_right = 1.0
15 | anchor_bottom = 1.0
16 | grow_horizontal = 2
17 | grow_vertical = 2
18 | size_flags_horizontal = 3
19 | size_flags_vertical = 3
20 |
21 | [node name="MarginContainer" type="MarginContainer" parent="Background"]
22 | layout_mode = 2
23 | theme_override_constants/margin_left = 4
24 | theme_override_constants/margin_top = 4
25 | theme_override_constants/margin_right = 4
26 | theme_override_constants/margin_bottom = 4
27 |
28 | [node name="Main" type="VBoxContainer" parent="Background/MarginContainer"]
29 | visible = false
30 | layout_mode = 2
31 | theme_override_constants/separation = 8
32 |
33 | [node name="FilterContainer" type="HBoxContainer" parent="Background/MarginContainer/Main"]
34 | layout_mode = 2
35 |
36 | [node name="FilterSection" type="LineEdit" parent="Background/MarginContainer/Main/FilterContainer"]
37 | unique_name_in_owner = true
38 | layout_mode = 2
39 | size_flags_horizontal = 3
40 | placeholder_text = "Filter Sections"
41 | clear_button_enabled = true
42 |
43 | [node name="SearchContainer" type="HBoxContainer" parent="Background/MarginContainer/Main"]
44 | layout_mode = 2
45 |
46 | [node name="AddSection" type="LineEdit" parent="Background/MarginContainer/Main/SearchContainer"]
47 | unique_name_in_owner = true
48 | layout_mode = 2
49 | size_flags_horizontal = 3
50 | placeholder_text = "New Section"
51 | clear_button_enabled = true
52 |
53 | [node name="AddButton" type="Button" parent="Background/MarginContainer/Main/SearchContainer"]
54 | unique_name_in_owner = true
55 | layout_mode = 2
56 | focus_mode = 0
57 | text = " Add "
58 |
59 | [node name="Panel" type="Panel" parent="Background/MarginContainer/Main"]
60 | layout_mode = 2
61 | size_flags_vertical = 3
62 |
63 | [node name="VBoxContainer" type="VBoxContainer" parent="Background/MarginContainer/Main/Panel"]
64 | visible = false
65 | layout_mode = 1
66 | anchors_preset = 15
67 | anchor_right = 1.0
68 | anchor_bottom = 1.0
69 | grow_horizontal = 2
70 | grow_vertical = 2
71 |
72 | [node name="Item" type="HBoxContainer" parent="Background/MarginContainer/Main/Panel/VBoxContainer"]
73 | layout_mode = 2
74 |
75 | [node name="Button" type="Button" parent="Background/MarginContainer/Main/Panel/VBoxContainer/Item"]
76 | layout_mode = 2
77 | text = "Goto"
78 |
79 | [node name="LineEdit" type="LineEdit" parent="Background/MarginContainer/Main/Panel/VBoxContainer/Item"]
80 | layout_mode = 2
81 | size_flags_horizontal = 3
82 |
83 | [node name="Button2" type="Button" parent="Background/MarginContainer/Main/Panel/VBoxContainer/Item"]
84 | layout_mode = 2
85 | text = "Del"
86 |
87 | [node name="Item2" type="HBoxContainer" parent="Background/MarginContainer/Main/Panel/VBoxContainer"]
88 | layout_mode = 2
89 |
90 | [node name="Button" type="Button" parent="Background/MarginContainer/Main/Panel/VBoxContainer/Item2"]
91 | layout_mode = 2
92 | text = "Goto"
93 |
94 | [node name="LineEdit" type="Label" parent="Background/MarginContainer/Main/Panel/VBoxContainer/Item2"]
95 | layout_mode = 2
96 | size_flags_horizontal = 3
97 | text = "TEST"
98 |
99 | [node name="Button2" type="Button" parent="Background/MarginContainer/Main/Panel/VBoxContainer/Item2"]
100 | layout_mode = 2
101 | text = "Del"
102 |
103 | [node name="Item3" type="HBoxContainer" parent="Background/MarginContainer/Main/Panel/VBoxContainer"]
104 | layout_mode = 2
105 |
106 | [node name="Button" type="Button" parent="Background/MarginContainer/Main/Panel/VBoxContainer/Item3"]
107 | layout_mode = 2
108 | text = "Goto"
109 |
110 | [node name="LineEdit" type="LineEdit" parent="Background/MarginContainer/Main/Panel/VBoxContainer/Item3"]
111 | layout_mode = 2
112 | size_flags_horizontal = 3
113 |
114 | [node name="Button2" type="Button" parent="Background/MarginContainer/Main/Panel/VBoxContainer/Item3"]
115 | layout_mode = 2
116 | text = "Del"
117 |
118 | [node name="TreeItems" type="Tree" parent="Background/MarginContainer/Main/Panel"]
119 | unique_name_in_owner = true
120 | layout_mode = 1
121 | anchors_preset = 15
122 | anchor_right = 1.0
123 | anchor_bottom = 1.0
124 | grow_horizontal = 2
125 | grow_vertical = 2
126 | columns = 3
127 | column_titles_visible = true
128 | allow_search = false
129 | enable_recursive_folding = false
130 | hide_root = true
131 | scroll_horizontal_enabled = false
132 |
133 | [node name="ShowDisableContainer" type="HBoxContainer" parent="Background/MarginContainer/Main"]
134 | layout_mode = 2
135 | theme_override_constants/separation = 75
136 | alignment = 1
137 |
138 | [node name="Show" type="CheckButton" parent="Background/MarginContainer/Main/ShowDisableContainer"]
139 | unique_name_in_owner = true
140 | layout_mode = 2
141 | tooltip_text = "Shortcut: Ctrl+J"
142 | focus_mode = 0
143 | button_pressed = true
144 | text = "Show"
145 |
146 | [node name="SyncTheme" type="Button" parent="Background/MarginContainer/Main/ShowDisableContainer"]
147 | unique_name_in_owner = true
148 | visible = false
149 | layout_mode = 2
150 | text = "Sync Theme"
151 |
152 | [node name="Disable" type="Button" parent="Background/MarginContainer/Main/ShowDisableContainer"]
153 | unique_name_in_owner = true
154 | layout_mode = 2
155 | focus_mode = 0
156 | text = "Disable"
157 |
158 | [node name="EnablePrompt" type="CenterContainer" parent="Background/MarginContainer"]
159 | layout_mode = 2
160 |
161 | [node name="VBoxContainer" type="VBoxContainer" parent="Background/MarginContainer/EnablePrompt"]
162 | custom_minimum_size = Vector2(300, 0)
163 | layout_mode = 2
164 | theme_override_constants/separation = 25
165 |
166 | [node name="EnablePromptLabel" type="Label" parent="Background/MarginContainer/EnablePrompt/VBoxContainer"]
167 | unique_name_in_owner = true
168 | layout_mode = 2
169 | text = "Enable the plugin for the script \"name.gd\"?"
170 | horizontal_alignment = 1
171 | autowrap_mode = 3
172 |
173 | [node name="HBoxContainer" type="HBoxContainer" parent="Background/MarginContainer/EnablePrompt/VBoxContainer"]
174 | layout_mode = 2
175 | theme_override_constants/separation = 50
176 | alignment = 1
177 |
178 | [node name="EnableCancel" type="Button" parent="Background/MarginContainer/EnablePrompt/VBoxContainer/HBoxContainer"]
179 | unique_name_in_owner = true
180 | layout_mode = 2
181 | focus_mode = 0
182 | text = "Cancel"
183 |
184 | [node name="Enable" type="Button" parent="Background/MarginContainer/EnablePrompt/VBoxContainer/HBoxContainer"]
185 | unique_name_in_owner = true
186 | layout_mode = 2
187 | focus_mode = 0
188 | text = "Enable"
189 |
190 | [node name="DisablePrompt" type="CenterContainer" parent="Background/MarginContainer"]
191 | visible = false
192 | layout_mode = 2
193 |
194 | [node name="VBoxContainer" type="VBoxContainer" parent="Background/MarginContainer/DisablePrompt"]
195 | custom_minimum_size = Vector2(300, 0)
196 | layout_mode = 2
197 | theme_override_constants/separation = 25
198 |
199 | [node name="DisablePromptLabel" type="Label" parent="Background/MarginContainer/DisablePrompt/VBoxContainer"]
200 | unique_name_in_owner = true
201 | layout_mode = 2
202 | text = "Disable the plugin for the script \"name.gd\"?
203 | (Will delete all sections)"
204 | horizontal_alignment = 1
205 | autowrap_mode = 3
206 |
207 | [node name="HBoxContainer" type="HBoxContainer" parent="Background/MarginContainer/DisablePrompt/VBoxContainer"]
208 | layout_mode = 2
209 | theme_override_constants/separation = 50
210 | alignment = 1
211 |
212 | [node name="DisableCancel" type="Button" parent="Background/MarginContainer/DisablePrompt/VBoxContainer/HBoxContainer"]
213 | unique_name_in_owner = true
214 | layout_mode = 2
215 | focus_mode = 0
216 | text = "Cancel"
217 |
218 | [node name="DisableAccept" type="Button" parent="Background/MarginContainer/DisablePrompt/VBoxContainer/HBoxContainer"]
219 | unique_name_in_owner = true
220 | layout_mode = 2
221 | focus_mode = 0
222 | text = "Disable"
223 |
224 | [connection signal="text_changed" from="Background/MarginContainer/Main/FilterContainer/FilterSection" to="." method="_on_FilterSection_text_changed"]
225 | [connection signal="text_submitted" from="Background/MarginContainer/Main/SearchContainer/AddSection" to="." method="_on_AddSection_text_submitted"]
226 | [connection signal="pressed" from="Background/MarginContainer/Main/SearchContainer/AddButton" to="." method="_on_AddButton_pressed"]
227 | [connection signal="pressed" from="Background/MarginContainer/Main/ShowDisableContainer/Disable" to="." method="_on_Disable_pressed"]
228 | [connection signal="pressed" from="Background/MarginContainer/EnablePrompt/VBoxContainer/HBoxContainer/Enable" to="." method="_on_Enable_pressed"]
229 | [connection signal="pressed" from="Background/MarginContainer/DisablePrompt/VBoxContainer/HBoxContainer/DisableCancel" to="." method="_on_DisableCancel_pressed"]
230 | [connection signal="pressed" from="Background/MarginContainer/DisablePrompt/VBoxContainer/HBoxContainer/DisableAccept" to="." method="_on_DisableAccept_pressed"]
231 |
--------------------------------------------------------------------------------
/addons/gdscript_sections/gdscript_sections.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | extends EditorPlugin
3 |
4 | # Following Godot's official tutorial - Making plugins:
5 | # https://docs.godotengine.org/en/stable/tutorials/plugins/editor/making_plugins.html
6 |
7 | const DATA_FOLDER := "res://addons/gdscript_sections/data/"
8 |
9 | const Dialog := preload("res://addons/gdscript_sections/dialog.tscn")
10 | const OverlayDisplay := preload("res://addons/gdscript_sections/overlay_display.tscn")
11 | const DataHelper := preload("res://addons/gdscript_sections/data_helper.gd")
12 |
13 | var Godot_base_control: Control
14 | var script_editor: ScriptEditor
15 | var script_subcontainer: Control
16 | var file_system_dock: FileSystemDock
17 | var active_code_edit: CodeEdit # Keeping a reference to deal with signal connections
18 | var dialog: Window = Dialog.instantiate()
19 | var display: Control = OverlayDisplay.instantiate()
20 | var tree: Tree # The UI display of sections of the active script
21 | var addon_button := Button.new()
22 | var data_helper: DataHelper = DataHelper.new()
23 | var filter_section: LineEdit
24 |
25 |
26 | ## Initialization of the plugin goes here.
27 | func _enter_tree() -> void:
28 | # Reads all enabled scripts and the unique ID
29 | if data_helper:
30 | data_helper.read()
31 | data_helper.id_helper.read()
32 |
33 | # Gets Godot editor's components
34 | Godot_base_control = get_editor_interface().get_base_control()
35 | script_editor = get_editor_interface().get_script_editor()
36 | script_subcontainer = script_editor.get_child(0)
37 | file_system_dock = get_editor_interface().get_file_system_dock()
38 |
39 | # File-related signals
40 | file_system_dock.files_moved.connect(_on_FileSystemDock_files_moved)
41 | file_system_dock.file_removed.connect(_on_FileSystemDock_file_removed)
42 |
43 | # Sets up dialog
44 | dialog.title = "GDSCript Sections"
45 | dialog.gui_embed_subwindows = true
46 | dialog.hide()
47 |
48 | Godot_base_control.add_child(dialog)
49 | dialog.set_font_size(14)
50 |
51 | dialog.section_added.connect(_on_Dialog_section_added)
52 | dialog.sections_filtered.connect(_on_Dialog_sections_filtered)
53 | filter_section = dialog.get_node("%FilterSection")
54 | filter_section.right_icon = _get_editor_icon("Search")
55 |
56 | dialog.close_requested.connect(_on_Dialog_close_requested)
57 | dialog.window_input.connect(_on_Dialog_window_input)
58 | dialog.get_node("%EnableCancel").pressed.connect(_on_Dialog_close_requested)
59 | dialog.get_node("%DisableAccept").pressed.connect(_on_Dialog_close_requested)
60 | dialog.get_node("%SyncTheme").pressed.connect(_on_Dialog_close_requested)
61 |
62 | # Visibility has been handled in dialog.gd
63 | ## Handles reading from and writing to file
64 | dialog.get_node("%Enable").pressed.connect(_on_Dialog_Enable_pressed)
65 | dialog.get_node("%Disable").pressed.connect(_on_Dialog_Disable_pressed)
66 | dialog.get_node("%DisableAccept").pressed.connect(_on_Dialog_DisableAccept_pressed)
67 | ## Handles toggling display
68 | dialog.get_node("%Show").toggled.connect(_on_Dialog_Show_toggled)
69 | ## Handles syncing theme
70 | dialog.get_node("%SyncTheme").pressed.connect(_on_Dialog_SyncTheme_pressed)
71 |
72 | # Dialog's theme
73 | var background: PanelContainer = dialog.get_node("Background")
74 | background.add_theme_stylebox_override("panel", _get_editor_style("Content"))
75 |
76 | # Adds addon's button to Script tab
77 | var menu_container := script_subcontainer.get_child(0)
78 | menu_container.add_child(addon_button)
79 |
80 | # Configures addon's button
81 | addon_button.icon = _get_editor_icon("FileList")
82 | addon_button.text = "Sections"
83 | addon_button.tooltip_text = "Shortcut: Ctrl+U"
84 | addon_button.focus_mode = Control.FOCUS_NONE
85 | addon_button.toggle_mode = true
86 | addon_button.set_pressed(false)
87 | addon_button.toggled.connect(_on_addon_button_toggled)
88 |
89 | # Configures TreeItems
90 | tree = background.get_node("%TreeItems")
91 | tree.set_column_title(0, "Goto")
92 | tree.set_column_title(1, "Section Name")
93 | tree.set_column_title(2, "Delete")
94 |
95 | # Tree's signals
96 | tree.button_clicked.connect(_on_TreeItems_button_clicked)
97 | tree.item_edited.connect(_on_TreeItems_item_edited)
98 |
99 | # Script-editing-related signal
100 | script_editor.editor_script_changed.connect(_on_ScriptEditor_editor_script_changed)
101 | # Triggers once in case user enables the addon while opening a script
102 | _on_ScriptEditor_editor_script_changed(script_editor.get_current_script())
103 |
104 | # Syncs theme
105 | # Also hides the display - to maintain consistency and reduce workload
106 | Godot_base_control.theme_changed.connect(func():
107 | background.add_theme_stylebox_override("panel", _get_editor_style("Content"))
108 | addon_button.icon = _get_editor_icon("FileList")
109 | filter_section.right_icon = _get_editor_icon("Search")
110 | _update_OverlayDisplay()
111 | )
112 |
113 | # OverlayDisplay's signals
114 | display.section_display_relocated.connect(_on_SectionDisplay_relocated)
115 |
116 | # Updates
117 | _update_OverlayDisplay()
118 | return
119 |
120 |
121 | ## Clean-up of the plugin goes here.
122 | func _exit_tree() -> void:
123 | if data_helper:
124 | data_helper.write()
125 | data_helper.id_helper.write()
126 |
127 | if display:
128 | display.queue_free()
129 |
130 | if dialog:
131 | dialog.queue_free()
132 |
133 | if addon_button:
134 | addon_button.queue_free()
135 | return
136 |
137 |
138 | ## Saves data when the project is saved or closed
139 | func _save_external_data() -> void:
140 | if data_helper:
141 | data_helper.write()
142 | data_helper.id_helper.write()
143 | return
144 |
145 |
146 | ## Returns Godot editor's icon
147 | func _get_editor_icon(name: StringName) -> Texture2D:
148 | return (
149 | get_editor_interface()
150 | .get_base_control()
151 | .get_theme_icon(name, "EditorIcons")
152 | )
153 |
154 |
155 | ## Returns Godot editor's stylebox
156 | func _get_editor_style(name: StringName) -> StyleBox:
157 | return (
158 | get_editor_interface()
159 | .get_base_control()
160 | .get_theme_stylebox(name, "EditorStyles")
161 | )
162 |
163 |
164 | ## Returns True if the currently active script is a GDScript (possibly CSharpScript too?)[br]
165 | ## Returns False if it is other types (.txt, .json, etc.) or a documentation
166 | func _is_active_script() -> bool:
167 | if not script_editor.get_current_editor():
168 | return false
169 |
170 | if script_editor.get_current_script():
171 | return true
172 | else:
173 | return false
174 |
175 |
176 | ## Returns the CodeEdit responsible for editing the currently active script
177 | func _get_active_code_edit() -> CodeEdit:
178 | return (
179 | script_editor
180 | .get_current_editor()
181 | .get_base_editor()
182 | )
183 |
184 |
185 | ## Returns the active CodeEdit's total height
186 | func _get_active_code_edit_height() -> float:
187 | return (
188 | _get_active_code_edit().get_v_scroll_bar().max_value
189 | *
190 | _get_active_code_edit().get_line_height()
191 | )
192 |
193 |
194 | ## Disconnects the active CodeEdit's signals[br]
195 | ## "previous" is for clarity and readibility
196 | func _disconnect_signals_previous_code_edit() -> void:
197 | if not active_code_edit:
198 | return
199 |
200 | if active_code_edit.gui_input.is_connected(_on_active_code_edit_gui_input):
201 | active_code_edit.gui_input.disconnect(_on_active_code_edit_gui_input)
202 |
203 | if active_code_edit.lines_edited_from.is_connected(_on_active_code_edit_line_edited):
204 | active_code_edit.lines_edited_from.disconnect(_on_active_code_edit_line_edited)
205 |
206 | if active_code_edit.get_v_scroll_bar().changed.is_connected(_on_active_code_edit_height_changed):
207 | active_code_edit.get_v_scroll_bar().changed.disconnect(_on_active_code_edit_height_changed)
208 |
209 | if active_code_edit.get_v_scroll_bar().value_changed.is_connected(_on_active_code_edit_VScrollBar_value_changed):
210 | active_code_edit.get_v_scroll_bar().value_changed.disconnect(_on_active_code_edit_VScrollBar_value_changed)
211 | return
212 |
213 |
214 | ## Connects the active CodeEdit's signals
215 | func _connect_signals_active_code_edit() -> void:
216 | if not active_code_edit:
217 | return
218 |
219 | active_code_edit.gui_input.connect(_on_active_code_edit_gui_input)
220 | active_code_edit.lines_edited_from.connect(_on_active_code_edit_line_edited)
221 | active_code_edit.get_v_scroll_bar().changed.connect(_on_active_code_edit_height_changed)
222 | active_code_edit.get_v_scroll_bar().value_changed.connect(_on_active_code_edit_VScrollBar_value_changed)
223 | return
224 |
225 |
226 | ## Updates both UI display and OverlayDisplay
227 | func _update_ui_and_display() -> void:
228 | _update_OverlayDisplay()
229 | _update_TreeItems(tree)
230 | return
231 |
232 |
233 | ## Updates the OverlayDisplay to display sections of the active script
234 | ## If Show is de-toggled, clear OverlayDisplay
235 | func _update_OverlayDisplay() -> void:
236 | var sections: Array = []
237 | if dialog.get_node("%Show").button_pressed:
238 | sections = data_helper.get_sections(script_editor.get_current_script().get_path(), false)
239 |
240 | _on_active_code_edit_height_changed()
241 | display.update(
242 | _get_active_code_edit(),
243 | sections
244 | )
245 |
246 | # Syncs scroll
247 | # The 0.1 seconds delay is to ensure the active CodeEdit's VScrollBar's value is correct
248 | _syncs_scroll()
249 | return
250 |
251 |
252 | ## Updates the main UI (Dialog) to display sections of the active script
253 | func _update_TreeItems(tree: Tree) -> void:
254 | var sections: Array = data_helper.get_sections(
255 | script_editor.get_current_script().get_path(),
256 | true
257 | )
258 |
259 | tree.clear()
260 | var root := tree.create_item()
261 |
262 | for section in sections:
263 | if _filters_section(section):
264 | _add_section_to_tree(tree, section)
265 | return
266 |
267 |
268 | ## Adds a TreeItem (row) with Goto button, Name field, and Delete button
269 | func _add_section_to_tree(tree: Tree, section: Section) -> void:
270 | var root := tree.get_root()
271 | var item := tree.create_item(root)
272 | if not item:
273 | return
274 |
275 | item.add_button(0, _get_editor_icon("ArrowRight"))
276 | tree.set_column_expand(0, false)
277 | tree.set_column_custom_minimum_width(0, 25)
278 | item.set_selectable(0, false)
279 | item.set_metadata(0, section.location)
280 |
281 | item.set_text(1, section.text)
282 | item.set_selectable(1, true)
283 | item.set_editable(1, true)
284 | item.set_autowrap_mode(1, TextServer.AUTOWRAP_WORD_SMART)
285 | tree.set_column_expand(1, true)
286 | tree.set_column_clip_content(1, true)
287 |
288 | item.add_button(2, _get_editor_icon("Remove"))
289 | tree.set_column_expand(2, false)
290 | item.set_selectable(2, false)
291 | item.set_text_alignment(2, HORIZONTAL_ALIGNMENT_CENTER)
292 | item.set_metadata(2, section.get_path())
293 | return
294 |
295 | ## Checks if a section matches the current active filter,
296 | func _filters_section(which: Section) -> bool:
297 | var filter_text := filter_section.text.strip_edges().to_lower()
298 | var section_text := which.text.strip_edges().to_lower()
299 | if filter_text.is_empty():
300 | return true
301 |
302 | # Uses RegEx to check whether section_text contains filter_text, as in
303 | # the characters don't have to be consecutive,
304 | # but they must be in order.
305 | var regex := RegEx.new()
306 | var special_characters := ".^$*+-?()[]{}\\|/"
307 | var pattern := ""
308 | for character in filter_text.split():
309 | if special_characters.contains(character):
310 | pattern += "\\" + character
311 | else:
312 | pattern += character
313 | pattern += ".*?"
314 | regex.compile(pattern)
315 |
316 | var result := regex.search(section_text)
317 | if result:
318 | return true
319 | return false
320 |
321 | # Synchronizes scroll values of OverlayDisplay and the active CodeEdit
322 | # The 0.1 seconds delay is to ensure the active CodeEdit's VScrollBar's value is correct
323 | func _syncs_scroll() -> void:
324 | get_tree().create_timer(0.1).timeout.connect(func():
325 | _on_active_code_edit_VScrollBar_value_changed(
326 | _get_active_code_edit().get_v_scroll_bar().value
327 | )
328 | )
329 | return
330 |
331 |
332 | ## Updates data when a in-editor file is manipulated
333 | ## (Move or Rename)
334 | func _on_FileSystemDock_files_moved(old_file: String, new_file: String) -> void:
335 | if data_helper.is_script_enabled(old_file):
336 | data_helper.update_script(old_file, new_file)
337 | return
338 |
339 |
340 | ## Updates data when a in-editor file is manipulated
341 | ## (Remove)
342 | func _on_FileSystemDock_file_removed(file: String) -> void:
343 | if data_helper.is_script_enabled(file):
344 | data_helper.disable_script(file)
345 | return
346 |
347 |
348 | ## Shows or hides the main UI (Dialog)
349 | func _on_addon_button_toggled(button_pressed: bool) -> void:
350 | if button_pressed:
351 | if not _is_active_script():
352 | addon_button.set_pressed_no_signal(false)
353 | return
354 |
355 | if data_helper.is_script_enabled(script_editor.get_current_script().get_path()):
356 | _update_TreeItems(tree)
357 | dialog.show_main()
358 | else:
359 | dialog.prompt_enable_script(script_editor.get_current_script().get_path())
360 |
361 | dialog.popup()
362 | else:
363 | if dialog.visible:
364 | dialog.hide()
365 | return
366 |
367 |
368 | ## Handles shortcuts when the main UI (Dialog) is shown
369 | func _on_Dialog_window_input(event: InputEvent) -> void:
370 | if event is InputEventKey and event.is_pressed():
371 | if event.keycode == KEY_U and event.is_ctrl_pressed():
372 | addon_button.set_pressed(false) # Hides main UI (Dialog)
373 |
374 | if event is InputEventKey and event.is_pressed():
375 | if event.keycode == KEY_J and event.is_ctrl_pressed():
376 | dialog.get_node("%Show").set_pressed(
377 | not dialog.get_node("%Show").button_pressed
378 | )
379 | return
380 |
381 |
382 | ## De-toggles UI button when Dialog is closed
383 | func _on_Dialog_close_requested() -> void:
384 | if addon_button:
385 | addon_button.set_pressed(false)
386 | return
387 |
388 |
389 | ## Enables the active script
390 | func _on_Dialog_Enable_pressed() -> void:
391 | data_helper.enable_script(
392 | script_editor.get_current_script().get_path()
393 | )
394 | _update_TreeItems(tree)
395 | return
396 |
397 |
398 | ## Updates disable prompt
399 | func _on_Dialog_Disable_pressed() -> void:
400 | dialog.prompt_disable_script(script_editor.get_current_script().get_path())
401 | return
402 |
403 |
404 | ## Disables the active script
405 | func _on_Dialog_DisableAccept_pressed() -> void:
406 | data_helper.disable_script(
407 | script_editor.get_current_script().get_path()
408 | )
409 |
410 | _update_OverlayDisplay()
411 | return
412 |
413 |
414 | ## Updates OverlayDisplay only
415 | func _on_Dialog_Show_toggled(button_pressed: bool) -> void:
416 | var sections: Array = []
417 |
418 | if button_pressed:
419 | sections = data_helper.get_sections(script_editor.get_current_script().get_path(), false)
420 | _on_active_code_edit_height_changed()
421 |
422 | display.update(
423 | _get_active_code_edit(),
424 | sections
425 | )
426 | return
427 |
428 |
429 | ## Updates OverlayDisplay only
430 | func _on_Dialog_SyncTheme_pressed() -> void:
431 | _update_OverlayDisplay()
432 | return
433 |
434 |
435 | ## Adds a new section to the enabled active script.
436 | ## Its location will be set to the line holding the caret.[br]
437 | ## Emitted when AddSection (LineEdit) submits or AddButton is pressed
438 | func _on_Dialog_section_added(text: String) -> void:
439 | var section := Section.new()
440 | section.text = text
441 | section.location = _get_active_code_edit().get_caret_line()
442 | _get_active_code_edit().center_viewport_to_caret()
443 |
444 | # IMPORTANT - There can be only one instance of DataHelper throughout the addon.
445 | # Otherwise, data manipulation gets weird.
446 | section.data_helper = data_helper
447 |
448 | var section_path := section.save_to_disk()
449 |
450 | data_helper.add_section_path(
451 | script_editor.get_current_script().get_path(),
452 | section_path
453 | )
454 |
455 | _update_ui_and_display()
456 | return
457 |
458 | ## Filters the sections
459 | func _on_Dialog_sections_filtered(_text: String) -> void:
460 | _update_TreeItems(tree)
461 | return
462 |
463 |
464 | ## Handles button presses of main UI (Tree)
465 | func _on_TreeItems_button_clicked(item: TreeItem, column: int, id: int, mouse_button_index: int) -> void:
466 | if not mouse_button_index == MOUSE_BUTTON_LEFT:
467 | return
468 |
469 | match column:
470 | 0: # Goto
471 | _get_active_code_edit().get_v_scroll_bar().value = (
472 | item.get_metadata(0)
473 | )
474 |
475 | 2: # Deletion
476 | data_helper.delete_section_path(
477 | script_editor.get_current_script().get_path(),
478 | item.get_metadata(2)
479 | )
480 |
481 | Section.remove_from_disk(item.get_metadata(2))
482 | _update_ui_and_display()
483 |
484 | return
485 |
486 |
487 | ## Handles renaming of sections
488 | func _on_TreeItems_item_edited() -> void:
489 | var item := tree.get_edited()
490 | var path := item.get_metadata(2)
491 | var section := Section.get_from_disk(path)
492 |
493 | section.text = item.get_text(1)
494 |
495 | section.update_to_disk()
496 | _update_ui_and_display()
497 | return
498 |
499 |
500 | ## Updates everthing correspondingly to the newly activated script
501 | func _on_ScriptEditor_editor_script_changed(script: Script) -> void:
502 | if not _is_active_script(): # Safeguard (unnecessary?)
503 | return
504 |
505 | # Redirects signals
506 | _disconnect_signals_previous_code_edit()
507 | active_code_edit = _get_active_code_edit()
508 | _connect_signals_active_code_edit()
509 |
510 | # Updates
511 | _on_active_code_edit_height_changed()
512 | _update_ui_and_display()
513 |
514 | _syncs_scroll()
515 | return
516 |
517 |
518 | ## Handles shortcuts when the main UI (Dialog) is hidden
519 | func _on_active_code_edit_gui_input(event: InputEvent) -> void:
520 | if event is InputEventKey and event.is_pressed():
521 | if event.keycode == KEY_U and event.is_ctrl_pressed():
522 | addon_button.set_pressed(true) # Shows main UI (Dialog)
523 |
524 | if event is InputEventKey and event.is_pressed():
525 | if event.keycode == KEY_J and event.is_ctrl_pressed():
526 | dialog.get_node("%Show").set_pressed(
527 | not dialog.get_node("%Show").button_pressed
528 | )
529 | return
530 |
531 |
532 | ## Updates the height of the container of current SectionDisplays
533 | ## Syncs it to that of the active CodeEdit
534 | func _on_active_code_edit_height_changed() -> void:
535 | var scroll: ScrollContainer = display.get_node("ScrollContainer")
536 | var sections_display: Control = display.get_node("%SectionsDisplay")
537 | var new_size := Vector2(
538 | sections_display.get_custom_minimum_size().x,
539 | _get_active_code_edit_height() + 500.0 # Adds a little extra to fix a bug where OverlayDisplay's VScrollBar has a weird upper-limit
540 | )
541 |
542 | sections_display.set_custom_minimum_size(new_size)
543 | sections_display.set_deferred("size", new_size)
544 | return
545 |
546 |
547 | ## Syncs scrolling of OverlayDisplay to that of active CodeEdit
548 | func _on_active_code_edit_VScrollBar_value_changed(value: float) -> void:
549 | var scroll: ScrollContainer = display.get_node("ScrollContainer")
550 | # var sections_display: Control = display.get_node("%SectionsDisplay")
551 |
552 | var new := value * _get_active_code_edit().get_line_height()
553 |
554 | scroll.get_v_scroll_bar().set_value(new)
555 | return
556 |
557 |
558 | ## Moves sections of active script accordingly when the user adds or removes line(s)
559 | ## Hasn't accounted for relocation of lines using ALT and ARROW KEYS yet
560 | func _on_active_code_edit_line_edited(from_line: int, to_line: int) -> void:
561 | if from_line == to_line:
562 | # User is either typing on the same line
563 | # or moving line(s) with ALT and Arrow keys (how to handle this?)
564 | return
565 |
566 | var sections: Array = data_helper.get_sections(
567 | script_editor.get_current_script().get_path(),
568 | true
569 | )
570 |
571 | for section in sections:
572 | if section.location < from_line:
573 | continue
574 |
575 | section.location += to_line - from_line
576 | section.update_to_disk()
577 |
578 | _update_OverlayDisplay()
579 | return
580 |
581 |
582 | ## Handles user's relocation of a SectionDisplay
583 | func _on_SectionDisplay_relocated(which: Control, event: InputEventMouseMotion) -> void:
584 | var section: Section = which.get_meta("section_resource")
585 | var total_relative_y := which.get_meta("total_relative_y") # Accumulation of previous events
586 | total_relative_y += event.relative.y
587 |
588 | while abs(total_relative_y) >= _get_active_code_edit().get_line_height():
589 | if total_relative_y > 0:
590 | section.location += 1
591 | which.position.y += _get_active_code_edit().get_line_height()
592 | total_relative_y -= _get_active_code_edit().get_line_height()
593 | else:
594 | section.location -= 1
595 | which.position.y -= _get_active_code_edit().get_line_height()
596 | total_relative_y += _get_active_code_edit().get_line_height()
597 | which.set_meta("total_relative_y", total_relative_y) # The remainder
598 |
599 | section.update_to_disk()
600 | return
601 |
602 |
--------------------------------------------------------------------------------
/addons/gdscript_sections/id_helper.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | #class_name
3 | extends Resource
4 |
5 | ## A script that helps seperate unique ID generation from DataHelper
6 |
7 | const PATH := "res://addons/gdscript_sections/data/id.dat"
8 |
9 | var _id: int = -1:
10 | get = get_new_id
11 |
12 |
13 | ## Returns a new unique ID
14 | func get_new_id() -> int:
15 | _id += 1
16 | return _id
17 |
18 |
19 | ## Reads from disk (id.dat) to _id
20 | func read() -> void:
21 | var file := FileAccess.open(PATH, FileAccess.READ)
22 | if not file:
23 | printerr("Cannot read: ", PATH)
24 | return
25 |
26 | _id = file.get_32()
27 | file.close()
28 | return
29 |
30 |
31 | ## Writes _id to disk (id.dat)
32 | func write() -> void:
33 | var file := FileAccess.open(PATH, FileAccess.WRITE)
34 | if _id != -1:
35 | file.store_32(_id)
36 | file.close()
37 | return
38 |
--------------------------------------------------------------------------------
/addons/gdscript_sections/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VinhPhmCng/gdscript-sections/4196144b724190452898f8e4b9b70607014b8b6d/addons/gdscript_sections/logo.png
--------------------------------------------------------------------------------
/addons/gdscript_sections/logo.png.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://cadtgd1mwuvir"
6 | path="res://.godot/imported/logo.png-ab62550db0b83305d538517078957daa.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/gdscript_sections/logo.png"
14 | dest_files=["res://.godot/imported/logo.png-ab62550db0b83305d538517078957daa.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 |
--------------------------------------------------------------------------------
/addons/gdscript_sections/overlay_display.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | #class_name
3 | extends Control
4 |
5 | ## The "container" for SectionDisplays[br]
6 | ## It will be added as a child to the currently active CodeEdit.
7 | ## It takes the full size of said CodeEdit, its scroll properties,
8 | ## and has the responsibility to syncs its own scrolling to that of the CodeEdit.
9 |
10 | signal section_display_relocated(which: Control, event: InputEventMouseMotion)
11 |
12 | ## The display of a single Section resource
13 | ## Not to be confused with OverlayDisplay or SectionsDisplay
14 | const SectionDisplay := preload("res://addons/gdscript_sections/section_display.tscn")
15 |
16 |
17 | ## Relocates itself to a new parent and
18 | ## displays the new SectionDisplays
19 | func update(new_parent: CodeEdit, sections: Array) -> void:
20 | if get_parent():
21 | get_parent().remove_child(self)
22 | new_parent.add_child(self)
23 |
24 | # Ensures this parent will remove the OverlayDisplay
25 | # before being freed
26 | new_parent.tree_exiting.connect(func():
27 | new_parent.remove_child(self)
28 | )
29 |
30 | # Removes previous display
31 | for child in %SectionsDisplay.get_children():
32 | %SectionsDisplay.remove_child(child)
33 | child.queue_free()
34 |
35 | # Sets up new display
36 | for section in sections:
37 | var section_display := SectionDisplay.instantiate()
38 | %SectionsDisplay.add_child(section_display)
39 | section_display.set_deferred("size:y", new_parent.get_line_height())
40 | section_display.get_node("%Text").text = " " + section.text
41 | section_display.get_node("%Text").add_theme_font_size_override(
42 | "font_size",
43 | new_parent.get_theme_font_size("font_size") + 2
44 | )
45 | section_display.position.y = section.location * new_parent.get_line_height()
46 | section_display.get_node("PaddingGutters").custom_minimum_size.x = new_parent.get_total_gutter_width()
47 | section_display.get_node("PaddingMinimap").custom_minimum_size.x = new_parent.get_minimap_width()
48 | section_display.get_node("PaddingGutters").size.x = new_parent.get_total_gutter_width()
49 | section_display.get_node("PaddingMinimap").size.x = new_parent.get_minimap_width()
50 | section_display.get_node("Main").gui_input.connect(_on_SectionDisplay_Main_gui_input.bind(section_display))
51 | section_display.set_meta("section_resource", section)
52 | section_display.set_meta("total_relative_y", 0.0)
53 | return
54 |
55 |
56 | ## Handles user's relocation of a SectionDisplay[br]
57 | ## Sends signals to EditorPlugin to gets its merits
58 | func _on_SectionDisplay_Main_gui_input(event: InputEvent, which: Control) -> void:
59 | if event is InputEventMouseMotion:
60 | if not event.button_mask == MOUSE_BUTTON_MASK_LEFT:
61 | return
62 |
63 | section_display_relocated.emit(which, event)
64 | return
65 |
--------------------------------------------------------------------------------
/addons/gdscript_sections/overlay_display.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=2 format=3 uid="uid://dhiecuhhisy0u"]
2 |
3 | [ext_resource type="Script" path="res://addons/gdscript_sections/overlay_display.gd" id="1_nhx2d"]
4 |
5 | [node name="OverlayDisplay" type="Control"]
6 | layout_mode = 3
7 | anchors_preset = 15
8 | anchor_right = 1.0
9 | anchor_bottom = 1.0
10 | grow_horizontal = 2
11 | grow_vertical = 2
12 | size_flags_horizontal = 3
13 | size_flags_vertical = 3
14 | mouse_filter = 2
15 | script = ExtResource("1_nhx2d")
16 |
17 | [node name="ScrollContainer" type="ScrollContainer" parent="."]
18 | layout_mode = 1
19 | anchors_preset = 15
20 | anchor_right = 1.0
21 | anchor_bottom = 1.0
22 | grow_horizontal = 2
23 | grow_vertical = 2
24 | mouse_filter = 2
25 | horizontal_scroll_mode = 0
26 | vertical_scroll_mode = 3
27 |
28 | [node name="SectionsDisplay" type="Control" parent="ScrollContainer"]
29 | unique_name_in_owner = true
30 | layout_mode = 2
31 | size_flags_horizontal = 3
32 | size_flags_vertical = 3
33 | mouse_filter = 2
34 |
35 | [node name="ColorRect" type="ColorRect" parent="ScrollContainer/SectionsDisplay"]
36 | unique_name_in_owner = true
37 | layout_mode = 1
38 | anchors_preset = 5
39 | anchor_left = 0.5
40 | anchor_right = 0.5
41 | offset_left = -20.0
42 | offset_right = 20.0
43 | offset_bottom = 40.0
44 | grow_horizontal = 2
45 | mouse_filter = 2
46 |
47 | [node name="ColorRect2" type="ColorRect" parent="ScrollContainer/SectionsDisplay"]
48 | unique_name_in_owner = true
49 | layout_mode = 1
50 | anchors_preset = 4
51 | anchor_top = 0.5
52 | anchor_bottom = 0.5
53 | offset_top = -20.0
54 | offset_right = 40.0
55 | offset_bottom = 20.0
56 | grow_vertical = 2
57 | mouse_filter = 2
58 |
59 | [node name="ColorRect3" type="ColorRect" parent="ScrollContainer/SectionsDisplay"]
60 | unique_name_in_owner = true
61 | layout_mode = 1
62 | anchors_preset = 7
63 | anchor_left = 0.5
64 | anchor_top = 1.0
65 | anchor_right = 0.5
66 | anchor_bottom = 1.0
67 | offset_left = -20.0
68 | offset_top = -40.0
69 | offset_right = 20.0
70 | grow_horizontal = 2
71 | grow_vertical = 0
72 | mouse_filter = 2
73 |
--------------------------------------------------------------------------------
/addons/gdscript_sections/plugin.cfg:
--------------------------------------------------------------------------------
1 | [plugin]
2 |
3 | name="GDScript Sections"
4 | description="Devides your code into sections for ease of organizing and navigation"
5 | author="VPC"
6 | version="1.0"
7 | script="gdscript_sections.gd"
8 |
--------------------------------------------------------------------------------
/addons/gdscript_sections/section.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | class_name Section
3 | extends Resource
4 |
5 | ## A custom resource that stores data of a Section
6 |
7 | const DataHelper := preload("res://addons/gdscript_sections/data_helper.gd")
8 |
9 | @export var text: String = ""
10 | ## The approximate line number in a CodeEdit
11 | @export var location: int
12 |
13 | var data_helper: DataHelper # There should only be one instance of this
14 |
15 |
16 | ## Updates itself to disk and does nothing else
17 | func update_to_disk() -> void:
18 | ResourceSaver.save(self, self.get_path())
19 | return
20 |
21 |
22 | ## Saves newly to disk with a unique path and
23 | ## returns the save path
24 | func save_to_disk() -> String:
25 | var path := create_save_path()
26 | ResourceSaver.save(self, path)
27 | # printt("SAVETODISK", self.get_path(), self.text, self.s)
28 | return path
29 |
30 |
31 | ## Returns a unique save path
32 | func create_save_path() -> String:
33 | var path := data_helper.DATA_FOLDER
34 | path = path.path_join("section")
35 | path += str(data_helper.get_new_id())
36 | path += ".tres"
37 | return path
38 |
39 |
40 | ## Deletes resource at "path"
41 | static func remove_from_disk(path: String) -> void:
42 | if FileAccess.file_exists(path):
43 | DirAccess.remove_absolute(path)
44 | return
45 |
46 |
47 | ## Returns Section resource at "path"
48 | static func get_from_disk(path: String) -> Section:
49 | return ResourceLoader.load(path, "Section")
50 |
--------------------------------------------------------------------------------
/addons/gdscript_sections/section_display.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene format=3 uid="uid://bn6ww8ahvjcc6"]
2 |
3 | [node name="SectionDisplay" type="HBoxContainer"]
4 | anchors_preset = 10
5 | anchor_right = 1.0
6 | offset_bottom = 32.0
7 | grow_horizontal = 2
8 | size_flags_horizontal = 3
9 | mouse_filter = 2
10 |
11 | [node name="PaddingGutters" type="Control" parent="."]
12 | layout_mode = 2
13 | mouse_filter = 2
14 |
15 | [node name="Main" type="VBoxContainer" parent="."]
16 | layout_mode = 2
17 | size_flags_horizontal = 3
18 | mouse_filter = 0
19 | mouse_default_cursor_shape = 2
20 | theme_override_constants/separation = 0
21 |
22 | [node name="Text" type="Label" parent="Main"]
23 | unique_name_in_owner = true
24 | layout_mode = 2
25 | theme_override_colors/font_color = Color(1, 0.368627, 0, 1)
26 | text = " TEXT"
27 | vertical_alignment = 1
28 | text_overrun_behavior = 4
29 |
30 | [node name="ColorRect" type="ColorRect" parent="Main/Text"]
31 | custom_minimum_size = Vector2(0, 1)
32 | layout_mode = 1
33 | anchors_preset = 12
34 | anchor_top = 1.0
35 | anchor_right = 1.0
36 | anchor_bottom = 1.0
37 | offset_top = -6.0
38 | offset_bottom = -5.0
39 | grow_horizontal = 2
40 | grow_vertical = 0
41 | mouse_filter = 2
42 | color = Color(1, 0.368627, 0, 0.921569)
43 |
44 | [node name="PaddingMinimap" type="Control" parent="."]
45 | layout_mode = 2
46 | mouse_filter = 2
47 |
--------------------------------------------------------------------------------
/addons/gdscript_sections/showcase.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VinhPhmCng/gdscript-sections/4196144b724190452898f8e4b9b70607014b8b6d/addons/gdscript_sections/showcase.gif
--------------------------------------------------------------------------------
/project.godot:
--------------------------------------------------------------------------------
1 | ; Engine configuration file.
2 | ; It's best edited using the editor UI and not directly,
3 | ; since the parameters that go here are not all obvious.
4 | ;
5 | ; Format:
6 | ; [section] ; section goes between []
7 | ; param=value ; assign values to parameters
8 |
9 | config_version=5
10 |
11 | [application]
12 |
13 | config/name="GDScript Sections"
14 | config/features=PackedStringArray("4.1", "GL Compatibility")
15 | config/icon="res://addons/gdscript_sections/logo.png"
16 |
17 | [editor_plugins]
18 |
19 | enabled=PackedStringArray("res://addons/gdscript_sections/plugin.cfg")
20 |
21 | [input]
22 |
23 | "virtual functions"={
24 | "deadzone": 0.5,
25 | "events": []
26 | }
27 | "Public functions"={
28 | "deadzone": 0.5,
29 | "events": []
30 | }
31 | "Another irrelevant"={
32 | "deadzone": 0.5,
33 | "events": []
34 | }
35 | hello={
36 | "deadzone": 0.5,
37 | "events": []
38 | }
39 |
40 | [rendering]
41 |
42 | renderer/rendering_method="gl_compatibility"
43 | renderer/rendering_method.mobile="gl_compatibility"
44 |
--------------------------------------------------------------------------------