A Quadrilateral Cowboy clone intended to help me learn Game Dev
at main 254 lines 7.4 kB view raw
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