├── .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 | Logo 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 |
  1. About The Project
  2. 48 |
  3. How To Use
  4. 49 |
  5. Installation
  6. 50 |
  7. Contribute
  8. 51 |
  9. License
  10. 52 |
53 |
54 | 55 | 56 | 57 | 58 | ## About The Project 59 | 60 | ![Godot Style Screenshot 1](https://raw.githubusercontent.com/VinhPhmCng/gdscript-sections/master/addons/gdscript_sections/showcase.gif) 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 | Logo 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 |
  1. About The Project
  2. 48 |
  3. How To Use
  4. 49 |
  5. Installation
  6. 50 |
  7. Contribute
  8. 51 |
  9. License
  10. 52 |
53 |
54 | 55 | 56 | 57 | 58 | ## About The Project 59 | 60 | ![Godot Style Screenshot 1](https://raw.githubusercontent.com/VinhPhmCng/gdscript-sections/master/addons/gdscript_sections/showcase.gif) 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 | --------------------------------------------------------------------------------