## The outline container shows a filter box and ## all script members with different color coding, depending of the type. @tool extends VBoxContainer const GETTER: StringName = &"get" const SETTER: StringName = &"set" const UNDERSCORE: StringName = &"_" const INLINE: StringName = &"@" const BUILT_IN_SCRIPT: StringName = &"::GDScript" #region Outline type name const PROPERTY: StringName = &"property" const TYPE: StringName = &"type" const ENGINE_FUNCS: StringName = &"Engine Callbacks" const FUNCS: StringName = &"Functions" const SIGNALS: StringName = &"Signals" const EXPORTED: StringName = &"Exported Properties" const PROPERTIES: StringName = &"Properties" const CLASSES: StringName = &"Classes" const CONSTANTS: StringName = &"Constants" const DEFAULT_ORDER: PackedStringArray = [ENGINE_FUNCS, FUNCS, SIGNALS, EXPORTED, PROPERTIES, CONSTANTS, CLASSES] #endregion const OutlineButton := preload("uid://c4fvj2xt46lgx") const Plugin := preload("uid://bc0b5v66xdidn") @onready var filter_box: HBoxContainer = %FilterBox @onready var outline: ItemList = %Outline var plugin: Plugin #region Existing Engine controls we modify var outline_filter_txt: LineEdit #endregion #region Outline icons and buttons var engine_func_icon: Texture2D var func_icon: Texture2D var func_get_icon: Texture2D var func_set_icon: Texture2D var property_icon: Texture2D var export_icon: Texture2D var signal_icon: Texture2D var constant_icon: Texture2D var class_icon: Texture2D var class_btn: Button var constant_btn: Button var signal_btn: Button var property_btn: Button var export_btn: Button var func_btn: Button var engine_func_btn: Button #endregion var is_hide_private_members: bool = false : set = set_hide_private_members var outline_order: PackedStringArray : set = set_outline_order var outline_type_order: Array[OutlineType] = [] var outline_cache: OutlineCache func _ready() -> void: init_icons() init_outline_order() outline.item_selected.connect(find_in_outline_and_goto) if (plugin == null): return engine_func_btn = create_filter_btn(engine_func_icon, ENGINE_FUNCS) func_btn = create_filter_btn(func_icon, FUNCS) signal_btn = create_filter_btn(signal_icon, SIGNALS) export_btn = create_filter_btn(export_icon, EXPORTED) property_btn = create_filter_btn(property_icon, PROPERTIES) class_btn = create_filter_btn(class_icon, CLASSES) constant_btn = create_filter_btn(constant_icon, CONSTANTS) func update(): update_outline_cache() update_outline() func tab_changed(): var is_script: bool = get_current_script() != null visible = is_script update() func find_in_outline_and_goto(selected_idx: int): var script: Script = get_current_script() if (!script): return var text: String = outline.get_item_text(selected_idx) var metadata: Dictionary[StringName, StringName] = outline.get_item_metadata(selected_idx) var modifier: StringName = metadata[&"modifier"] var type: StringName = metadata[TYPE] var type_with_text: String = type + " " + text if (type == &"func"): type_with_text = type_with_text.substr(0, type_with_text.find("(")) var source_code: String = script.get_source_code() var lines: PackedStringArray = source_code.split("\n") var index: int = 0 for line: String in lines: # Easy case, like 'var abc' if (line.begins_with(type_with_text)): plugin.goto_line(index) return # We have an modifier, e.g. 'static' if (modifier != &"" && line.begins_with(modifier)): if (line.begins_with(modifier + " " + type_with_text)): plugin.goto_line(index) return # Special case: An 'enum' is treated different. elif (modifier == &"enum" && line.contains("enum " + text)): plugin.goto_line(index) return # Hard case, probably something like '@onready var abc' if (type == &"var" && line.contains(type_with_text)): plugin.goto_line(index) return index += 1 push_error(type_with_text + " or " + modifier + " not found in source code") ## Initializes all plugin icons, while respecting the editor settings. func init_icons(): engine_func_icon = create_editor_texture(load_rel("icon/engine_func.svg")) func_icon = create_editor_texture(load_rel("icon/func.svg")) func_get_icon = create_editor_texture(load_rel("icon/func_get.svg")) func_set_icon = create_editor_texture(load_rel("icon/func_set.svg")) property_icon = create_editor_texture(load_rel("icon/property.svg")) export_icon = create_editor_texture(load_rel("icon/export.svg")) signal_icon = create_editor_texture(load_rel("icon/signal.svg")) constant_icon = create_editor_texture(load_rel("icon/constant.svg")) class_icon = create_editor_texture(load_rel("icon/class.svg")) func create_filter_btn(icon: Texture2D, type: StringName) -> Button: var btn: OutlineButton = OutlineButton.new() btn.icon = icon btn.tooltip_text = type var property: StringName = plugin.SCRIPT_IDE + type.to_lower().replace(" ", "_") btn.set_meta(PROPERTY, property) btn.set_meta(TYPE, type) btn.button_pressed = plugin.get_setting(property, true) btn.toggled.connect(on_filter_button_pressed.bind(btn)) btn.right_clicked.connect(on_right_click.bind(btn)) return btn func on_right_click(btn: OutlineButton): btn.button_pressed = true var pressed_state: bool = false for child: Node in filter_box.get_children(): var other_btn: Button = child if (btn != other_btn): pressed_state = pressed_state || other_btn.button_pressed for child: Node in filter_box.get_children(): var other_btn: Button = child if (btn != other_btn): other_btn.button_pressed = !pressed_state outline_filter_txt.grab_focus() func on_filter_button_pressed(pressed: bool, btn: Button): plugin.set_setting(btn.get_meta(PROPERTY), pressed) update_outline() ## Initializes the outline type structure and sorts it based off the outline order. func init_outline_order(): var outline_type: OutlineType = OutlineType.new() outline_type.type_name = ENGINE_FUNCS outline_type.add_to_outline = func(): add_to_outline_if_selected(engine_func_btn, func(): add_to_outline(outline_cache.engine_funcs, engine_func_icon, &"func")) outline_type_order.append(outline_type) outline_type = OutlineType.new() outline_type.type_name = FUNCS outline_type.add_to_outline = func(): add_to_outline_if_selected(func_btn, func(): add_to_outline_ext(outline_cache.funcs, get_func_icon, &"func", &"static")) outline_type_order.append(outline_type) outline_type = OutlineType.new() outline_type.type_name = SIGNALS outline_type.add_to_outline = func(): add_to_outline_if_selected(signal_btn, func(): add_to_outline(outline_cache.signals, signal_icon, &"signal")) outline_type_order.append(outline_type) outline_type = OutlineType.new() outline_type.type_name = EXPORTED outline_type.add_to_outline = func(): add_to_outline_if_selected(export_btn, func(): add_to_outline(outline_cache.exports, export_icon, &"var", &"@export")) outline_type_order.append(outline_type) outline_type = OutlineType.new() outline_type.type_name = PROPERTIES outline_type.add_to_outline = func(): add_to_outline_if_selected(property_btn, func(): add_to_outline(outline_cache.properties, property_icon, &"var")) outline_type_order.append(outline_type) outline_type = OutlineType.new() outline_type.type_name = CLASSES outline_type.add_to_outline = func(): add_to_outline_if_selected(class_btn, func(): add_to_outline(outline_cache.classes, class_icon, &"class")) outline_type_order.append(outline_type) outline_type = OutlineType.new() outline_type.type_name = CONSTANTS outline_type.add_to_outline = func(): add_to_outline_if_selected(constant_btn, func(): add_to_outline(outline_cache.constants, constant_icon, &"const", &"enum")) outline_type_order.append(outline_type) func add_to_outline_if_selected(btn: Button, action: Callable): if (btn.button_pressed): action.call() func update_outline_button_order(): var all_buttons: Array[Button] = [engine_func_btn, func_btn, signal_btn, export_btn, property_btn, class_btn, constant_btn] all_buttons.sort_custom(sort_buttons_by_outline_order) for btn: Button in all_buttons: if (btn.get_parent() != null): filter_box.remove_child(btn) for btn: Button in all_buttons: filter_box.add_child(btn) func sort_buttons_by_outline_order(btn1: Button, btn2: Button) -> bool: return sort_by_outline_order(btn1.get_meta(TYPE), btn2.get_meta(TYPE)) func sort_types_by_outline_order(type1: OutlineType, type2: OutlineType) -> bool: return sort_by_outline_order(type1.type_name, type2.type_name) func sort_by_outline_order(outline_type1: StringName, outline_type2: StringName) -> bool: return outline_order.find(outline_type1) < outline_order.find(outline_type2) func get_current_script() -> Script: var script_editor: ScriptEditor = EditorInterface.get_script_editor() return script_editor.get_current_script() func update_outline_cache(): outline_cache = null var script: Script = get_current_script() if (!script): return # Check if built-in script. In this case we need to duplicate it for whatever reason. if (script.get_path().contains(BUILT_IN_SCRIPT)): script = script.duplicate() outline_cache = OutlineCache.new() # Collect all script members. for_each_script_member(script, func(array: Array[String], item: String): array.append(item)) # Remove script members that only exist in the base script (which includes the base of the base etc.). # Note: The method that only collects script members without including the base script(s) # is not exposed to GDScript. var base_script: Script = script.get_base_script() if (base_script != null): for_each_script_member(base_script, func(array: Array[String], item: String): array.erase(item)) func for_each_script_member(script: Script, consumer: Callable): # Functions / Methods for dict: Dictionary in script.get_script_method_list(): var func_name: String = dict[&"name"] if (plugin.keywords.has(func_name)): func_name = create_function_signature(dict) consumer.call(outline_cache.engine_funcs, func_name) else: if (is_hide_private_members && func_name.begins_with(UNDERSCORE)): continue # Inline getter/setter will normally be shown as '@...getter', '@...setter'. # Since we already show the variable itself, we will skip those. if (func_name.begins_with(INLINE)): continue func_name = create_function_signature(dict) consumer.call(outline_cache.funcs, func_name) # Properties / Exported variables for dict: Dictionary in script.get_script_property_list(): var property: String = dict[&"name"] if (is_hide_private_members && property.begins_with(UNDERSCORE)): continue var usage: int = dict[&"usage"] if (usage & PROPERTY_USAGE_SCRIPT_VARIABLE): if (usage & PROPERTY_USAGE_STORAGE && usage & PROPERTY_USAGE_EDITOR): consumer.call(outline_cache.exports, property) else: consumer.call(outline_cache.properties, property) # Static variables (are separated for whatever reason) for dict: Dictionary in script.get_property_list(): var property: String = dict[&"name"] if (is_hide_private_members && property.begins_with(UNDERSCORE)): continue var usage: int = dict[&"usage"] if (usage & PROPERTY_USAGE_SCRIPT_VARIABLE): consumer.call(outline_cache.properties, property) # Signals for dict: Dictionary in script.get_script_signal_list(): var signal_name: String = dict[&"name"] consumer.call(outline_cache.signals, signal_name) # Constants / Classes for name_key: String in script.get_script_constant_map(): if (is_hide_private_members && name_key.begins_with(UNDERSCORE)): continue var object: Variant = script.get_script_constant_map().get(name_key) # Inner classes have no source code, while a const of type GDScript has. if (object is GDScript && !object.has_source_code()): consumer.call(outline_cache.classes, name_key) else: consumer.call(outline_cache.constants, name_key) func update_outline(): outline.clear() if (outline_cache == null): return for outline_type: OutlineType in outline_type_order: outline_type.add_to_outline.call() func add_to_outline(items: Array[String], icon: Texture2D, type: StringName, modifier: StringName = &""): add_to_outline_ext(items, func(str: String): return icon, type, modifier) func add_to_outline_ext(items: Array[String], icon_callable: Callable, type: StringName, modifier: StringName = &""): var text: String = outline_filter_txt.get_text() if (is_sorted()): items = items.duplicate() items.sort_custom(func(str1: String, str2: String): return str1.naturalnocasecmp_to(str2) < 0) for item: String in items: if (text.is_empty() || text.is_subsequence_ofn(item)): var icon: Texture2D = icon_callable.call(item) outline.add_item(item, icon, true) var dict: Dictionary[StringName, StringName] = { TYPE: type, &"modifier": modifier } outline.set_item_metadata(outline.item_count - 1, dict) func get_func_icon(func_name: String) -> Texture2D: var icon: Texture2D = func_icon if (func_name.begins_with(GETTER)): icon = func_get_icon elif (func_name.begins_with(SETTER)): icon = func_set_icon return icon func save_restore_filter() -> Array[bool]: var button_flags: Array[bool] = [] for child: Node in filter_box.get_children(): var btn: Button = child button_flags.append(btn.button_pressed) btn.set_pressed_no_signal(true) return button_flags func restore_filter(button_flags: Array[bool]): var index: int = 0 for flag: bool in button_flags: var btn: Button = filter_box.get_child(index) btn.set_pressed_no_signal(flag) index += 1 func set_outline_order(new_outline_order: PackedStringArray): outline_order = new_outline_order if (filter_box == null): return outline_type_order.sort_custom(sort_types_by_outline_order) update_outline_button_order() update_outline() func set_hide_private_members(new_value: bool): is_hide_private_members = new_value if (filter_box == null): return update_outline_cache() update_outline() func update_filter_buttons(): # Update filter buttons. for btn_node: Node in filter_box.get_children(): var btn: Button = btn_node var property: StringName = btn.get_meta(PROPERTY) btn.button_pressed = plugin.get_setting(property, btn.button_pressed) func create_function_signature(method: Dictionary) -> String: var func_name: String = method[&"name"] func_name += "(" var args: Array = method[&"args"] var default_args: Array = method[&"default_args"] var arg_index: int = 0 var default_arg_index: int = 0 var arg_str: String = "" for arg: Dictionary in args: if (arg_str != ""): arg_str += ", " arg_str += arg[&"name"] var type: String = get_type(arg) if (type != ""): arg_str += ": " + type if (args.size() - arg_index <= default_args.size()): var default_arg: Variant = default_args[default_arg_index] if (!default_arg): var type_hint: int = arg[&"type"] if (is_dictionary(type_hint)): default_arg = {} elif (is_array(type_hint)): default_arg = [] arg_str += " = " + var_to_str(default_arg) default_arg_index += 1 arg_index += 1 func_name += arg_str + ")" var return_str: String = get_type(method[&"return"]) if (return_str == ""): return func_name func_name += " -> " + return_str return func_name func get_type(dict: Dictionary) -> String: var type: String = dict[&"class_name"] if (type != &""): if (type.begins_with(&"res://")): type = type.substr(type.rfind(&".") + 1) return type var type_hint: int = dict[&"type"] if (type_hint == 0): return &"" type = type_string(type_hint) if (is_dictionary(type_hint)): var generic: String = dict[&"hint_string"] if (generic != &""): var generic_parts: PackedStringArray = generic.split(";") if (generic_parts.size() == 2): return type + "[" + generic_parts[0] + ", " + generic_parts[1] + "]" if (is_array(type_hint)): var generic: String = dict[&"hint_string"] if (generic != &""): return type + "[" + generic + "]" return type func is_dictionary(type_hint: int) -> bool: return type_hint == 27 func is_array(type_hint: int) -> bool: return type_hint == 28 func reset_icons(): init_icons() engine_func_btn.icon = engine_func_icon func_btn.icon = func_icon signal_btn.icon = signal_icon export_btn.icon = export_icon property_btn.icon = property_icon class_btn.icon = class_icon constant_btn.icon = constant_icon update_outline() func create_editor_texture(texture: Texture2D) -> Texture2D: var image: Image = texture.get_image().duplicate() image.adjust_bcs(1.0, 1.0, get_editor_icon_saturation()) return ImageTexture.create_from_image(image) func load_rel(path: String) -> Variant: var script_path: String = get_script().get_path().get_base_dir() return load(script_path.path_join(path)) func is_sorted() -> bool: return EditorInterface.get_editor_settings().get_setting("text_editor/script_list/sort_members_outline_alphabetically") func get_editor_icon_saturation() -> float: return EditorInterface.get_editor_settings().get_setting("interface/theme/icon_saturation") ## Cache for everything inside we collected to show in the Outline. class OutlineCache: var classes: Array[String] = [] var constants: Array[String] = [] var signals: Array[String] = [] var exports: Array[String] = [] var properties: Array[String] = [] var funcs: Array[String] = [] var engine_funcs: Array[String] = [] ## Outline type for a concrete button with their items in the Outline. class OutlineType: var type_name: StringName var add_to_outline: Callable