A Quadrilateral Cowboy clone intended to help me learn Game Dev
1## Quick open panel to quickly access all resources that are in the project.
2## Initially shows all resources, but can be changed to more specific resources
3## or filtered down with text.
4@tool
5extends PopupPanel
6
7const ADDONS: StringName = &"res://addons"
8const SEPARATOR: StringName = &" - "
9const STRUCTURE_START: StringName = &"("
10const STRUCTURE_END: StringName = &")"
11
12#region UI
13@onready var filter_bar: TabBar = %FilterBar
14@onready var search_option_btn: OptionButton = %SearchOptionBtn
15@onready var filter_txt: LineEdit = %FilterTxt
16@onready var files_list: ItemList = %FilesList
17#endregion
18
19var plugin: EditorPlugin
20
21var scenes: Array[FileData]
22var scripts: Array[FileData]
23var resources: Array[FileData]
24var others: Array[FileData]
25
26# For performance and memory considerations, we add all files into one reusable array.
27var all_files: Array[FileData]
28
29var is_rebuild_cache: bool = true
30
31#region Plugin and Shortcut processing
32func _ready() -> void:
33 files_list.item_selected.connect(open_file)
34 search_option_btn.item_selected.connect(rebuild_cache_and_ui.unbind(1))
35 filter_txt.text_changed.connect(fill_files_list.unbind(1))
36
37 filter_bar.tab_changed.connect(change_fill_files_list.unbind(1))
38
39 about_to_popup.connect(on_show)
40
41 var file_system: EditorFileSystem = EditorInterface.get_resource_filesystem()
42 file_system.filesystem_changed.connect(schedule_rebuild)
43
44 if (plugin != null):
45 filter_txt.gui_input.connect(plugin.navigate_on_list.bind(files_list, open_file))
46
47func _shortcut_input(event: InputEvent) -> void:
48 if (!event.is_pressed() || event.is_echo()):
49 return
50
51 if (plugin.tab_cycle_forward_shc.matches_event(event)):
52 get_viewport().set_input_as_handled()
53
54 var new_tab: int = filter_bar.current_tab + 1
55 if (new_tab == filter_bar.get_tab_count()):
56 new_tab = 0
57 filter_bar.current_tab = new_tab
58 elif (plugin.tab_cycle_backward_shc.matches_event(event)):
59 get_viewport().set_input_as_handled()
60
61 var new_tab: int = filter_bar.current_tab - 1
62 if (new_tab == -1):
63 new_tab = filter_bar.get_tab_count() - 1
64 filter_bar.current_tab = new_tab
65#endregion
66
67func open_file(index: int):
68 var file: String = files_list.get_item_metadata(index)
69
70 if (ResourceLoader.exists(file)):
71 var res: Resource = load(file)
72
73 if (res is Script):
74 EditorInterface.edit_script(res)
75 EditorInterface.set_main_screen_editor.call_deferred("Script")
76 else:
77 EditorInterface.edit_resource(res)
78
79 if (res is PackedScene):
80 EditorInterface.open_scene_from_path(file)
81
82 # Need to be deferred as it does not work otherwise.
83 var root: Node = EditorInterface.get_edited_scene_root()
84 if (root is Node3D):
85 EditorInterface.set_main_screen_editor.call_deferred("3D")
86 else:
87 EditorInterface.set_main_screen_editor.call_deferred("2D")
88 else:
89 # Text files (.txt, .md) will not be recognized, which seems to be a very bad
90 # limitation from the Engine. The methods called by the Engine are also not exposed.
91 # So we just select the file, which is better than nothing.
92 EditorInterface.select_file(file)
93
94 # Deferred as otherwise we get weird errors in the console.
95 # Probably due to this beeing called in a signal and auto unparent is true.
96 # 100% Engine bug or at least weird behavior.
97 hide.call_deferred()
98
99func schedule_rebuild():
100 is_rebuild_cache = true
101
102func on_show():
103 if (search_option_btn.selected != 0):
104 search_option_btn.selected = 0
105
106 is_rebuild_cache = true
107
108 var rebuild_ui: bool = false
109 var all_tab_not_pressed: bool = filter_bar.current_tab != 0
110 rebuild_ui = is_rebuild_cache || all_tab_not_pressed
111
112 if (is_rebuild_cache):
113 rebuild_cache()
114
115 if (rebuild_ui):
116 if (all_tab_not_pressed):
117 # Triggers the ui update.
118 filter_bar.current_tab = 0
119 else:
120 fill_files_list()
121
122 filter_txt.select_all()
123 focus_and_select_first()
124
125func rebuild_cache():
126 is_rebuild_cache = false
127
128 all_files.clear()
129 scenes.clear()
130 scripts.clear()
131 resources.clear()
132 others.clear()
133
134 build_file_cache()
135
136func rebuild_cache_and_ui():
137 rebuild_cache()
138 fill_files_list()
139
140 focus_and_select_first()
141
142func focus_and_select_first():
143 filter_txt.grab_focus()
144
145 if (files_list.item_count > 0):
146 files_list.select(0)
147
148func build_file_cache():
149 var dir: EditorFileSystemDirectory = EditorInterface.get_resource_filesystem().get_filesystem()
150 build_file_cache_dir(dir)
151
152 all_files.append_array(scenes)
153 all_files.append_array(scripts)
154 all_files.append_array(resources)
155 all_files.append_array(others)
156
157func build_file_cache_dir(dir: EditorFileSystemDirectory):
158 for index: int in dir.get_subdir_count():
159 build_file_cache_dir(dir.get_subdir(index))
160
161 for index: int in dir.get_file_count():
162 var file: String = dir.get_file_path(index)
163 if (search_option_btn.get_selected_id() == 0 && file.begins_with(ADDONS)):
164 continue
165
166 var last_delimiter: int = file.rfind(&"/")
167
168 var file_name: String = file.substr(last_delimiter + 1)
169 var file_structure: String = &""
170 if (file_name.length() + 6 != file.length()):
171 file_structure = SEPARATOR + STRUCTURE_START + file.substr(6, last_delimiter - 6) + STRUCTURE_END
172
173 var file_data: FileData = FileData.new()
174 file_data.file = file
175 file_data.file_name = file_name
176 file_data.file_name_structure = file_name + file_structure
177 file_data.file_type = dir.get_file_type(index)
178
179 # Needed, as otherwise we have no icon.
180 if (file_data.file_type == &"Resource"):
181 file_data.file_type = &"Object"
182
183 match (file.get_extension()):
184 &"tscn": scenes.append(file_data)
185 &"gd": scripts.append(file_data)
186 &"tres": resources.append(file_data)
187 &"gdshader": resources.append(file_data)
188 _: others.append(file_data)
189
190func change_fill_files_list():
191 fill_files_list()
192
193 focus_and_select_first()
194
195func fill_files_list():
196 files_list.clear()
197
198 if (filter_bar.current_tab == 0):
199 fill_files_list_with(all_files)
200 elif (filter_bar.current_tab == 1):
201 fill_files_list_with(scenes)
202 elif (filter_bar.current_tab == 2):
203 fill_files_list_with(scripts)
204 elif (filter_bar.current_tab == 3):
205 fill_files_list_with(resources)
206 elif (filter_bar.current_tab == 4):
207 fill_files_list_with(others)
208
209func fill_files_list_with(files: Array[FileData]):
210 var filter_text: String = filter_txt.text
211 files.sort_custom(sort_by_filter)
212
213 for file_data: FileData in files:
214 var file: String = file_data.file
215 if (filter_text.is_empty() || filter_text.is_subsequence_ofn(file)):
216 var icon: Texture2D = EditorInterface.get_base_control().get_theme_icon(file_data.file_type, &"EditorIcons")
217
218 files_list.add_item(file_data.file_name_structure, icon)
219 files_list.set_item_metadata(files_list.item_count - 1, file)
220 files_list.set_item_tooltip(files_list.item_count - 1, file)
221
222func sort_by_filter(file_data1: FileData, file_data2: FileData) -> bool:
223 var filter_text: String = filter_txt.text
224 var name1: String = file_data1.file_name
225 var name2: String = file_data2.file_name
226
227 for index: int in filter_text.length():
228 var a_oob: bool = index >= name1.length()
229 var b_oob: bool = index >= name2.length()
230
231 if (a_oob):
232 if (b_oob):
233 return false;
234 return true
235 if (b_oob):
236 return false
237
238 var char: String = filter_text[index]
239 var a_match: bool = char == name1[index]
240 var b_match: bool = char == name2[index]
241
242 if (a_match && !b_match):
243 return true
244
245 if (b_match && !a_match):
246 return false
247
248 return name1 < name2
249
250class FileData:
251 var file: String
252 var file_name: String
253 var file_name_structure: String
254 var file_type: StringName