@tool extends EditorPlugin ## Editor setting path const SCRIPT_IDE: StringName = &"plugin/script_ide/" ## Editor setting for the outline position const OUTLINE_POSITION_RIGHT: StringName = SCRIPT_IDE + &"outline_position_right" ## Editor setting to control whether private members (annotated with '_' should be hidden or not) const HIDE_PRIVATE_MEMBERS: StringName = SCRIPT_IDE + &"hide_private_members" ## Editor setting to control whether the script list should be visible or not const SCRIPT_LIST_VISIBLE: StringName = SCRIPT_IDE + &"script_list_visible" ## Editor setting for the 'Open Outline Popup' shortcut const OPEN_OUTLINE_POPUP: StringName = SCRIPT_IDE + &"open_outline_popup" #region Outline icons const keyword_icon: Texture2D = preload("res://addons/script-ide/icon/keyword.svg") const func_icon: Texture2D = preload("res://addons/script-ide/icon/func.svg") const func_get_icon: Texture2D = preload("res://addons/script-ide/icon/func_get.svg") const func_set_icon: Texture2D = preload("res://addons/script-ide/icon/func_set.svg") const property_icon: Texture2D = preload("res://addons/script-ide/icon/property.svg") const export_icon: Texture2D = preload("res://addons/script-ide/icon/export.svg") const signal_icon: Texture2D = preload("res://addons/script-ide/icon/signal.svg") const constant_icon: Texture2D = preload("res://addons/script-ide/icon/constant.svg") const class_icon: Texture2D = preload("res://addons/script-ide/icon/class.svg") #endregion const POPUP_SCRIPT: GDScript = preload("res://addons/script-ide/Popup.gd") #region Editor settings var is_outline_right: bool = true var is_script_list_visible: bool = false var hide_private_members: bool = false var open_outline_popup: Shortcut #endregion var suppress_settings_sync: bool = false #region Existing controls we modify var outline_container: Control var outline_parent: Node var scripts_tab_container: TabContainer var scripts_tab_bar: TabBar var scripts_item_list: ItemList var split_container: HSplitContainer var old_outline: ItemList var filter_txt: LineEdit var sort_btn: Button #endregion #region Own controls we add var outline: ItemList var outline_popup: PopupPanel var filter_box: HBoxContainer 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 keywords: Dictionary = {} # Basically used as Set, since Godot has none. [String, int = 0] var outline_cache: OutlineCache var tab_state: TabStateCache var old_script_editor_base: ScriptEditorBase var old_script_type: StringName var selected_tab: int = -1 var last_tab_hovered: int = -1 var sync_script_list: bool #region Enter / Exit -> Plugin setup ## Change the Godot script UI and transform into an IDE like UI func _enter_tree() -> void: is_outline_right = get_setting(OUTLINE_POSITION_RIGHT, is_outline_right) hide_private_members = get_setting(HIDE_PRIVATE_MEMBERS, hide_private_members) is_script_list_visible = get_setting(SCRIPT_LIST_VISIBLE, is_script_list_visible) var editor_settings: EditorSettings = get_editor_settings() if (!editor_settings.has_setting(OPEN_OUTLINE_POPUP)): var shortcut: Shortcut = Shortcut.new() var event: InputEventKey = InputEventKey.new() event.device = -1 event.ctrl_pressed = true event.keycode = KEY_O var event2: InputEventKey = InputEventKey.new() event2.device = -1 event2.meta_pressed = true event2.keycode = KEY_O shortcut.events = [ event, event2 ] editor_settings.set_setting(OPEN_OUTLINE_POPUP, shortcut) editor_settings.set_initial_value(OPEN_OUTLINE_POPUP, shortcut, false) open_outline_popup = editor_settings.get_setting(OPEN_OUTLINE_POPUP) # Update on filesystem changed (e.g. save operation). var file_system: EditorFileSystem = get_editor_interface().get_resource_filesystem() file_system.filesystem_changed.connect(schedule_update) # Make tab container visible var script_editor: ScriptEditor = get_editor_interface().get_script_editor() scripts_tab_container = find_or_null(script_editor.find_children("*", "TabContainer", true, false)) if (scripts_tab_container != null): scripts_tab_bar = scripts_tab_container.get_tab_bar() tab_state = TabStateCache.new() tab_state.save(scripts_tab_container, scripts_tab_bar) scripts_tab_container.tabs_visible = true scripts_tab_container.drag_to_rearrange_enabled = true if (scripts_tab_bar != null): scripts_tab_bar.tab_close_display_policy = TabBar.CLOSE_BUTTON_SHOW_ACTIVE_ONLY scripts_tab_bar.drag_to_rearrange_enabled = true scripts_tab_bar.select_with_rmb = true scripts_tab_bar.tab_close_pressed.connect(on_tab_close) scripts_tab_bar.tab_rmb_clicked.connect(on_tab_rmb) scripts_tab_bar.tab_hovered.connect(on_tab_hovered) scripts_tab_bar.mouse_exited.connect(on_tab_bar_mouse_exited) scripts_tab_bar.active_tab_rearranged.connect(on_active_tab_rearranged) scripts_tab_bar.gui_input.connect(on_tab_bar_gui_input) scripts_tab_bar.tab_changed.connect(on_tab_changed) # Change script item list visibility scripts_item_list = find_or_null(script_editor.find_children("*", "ItemList", true, false)) if (scripts_item_list != null): update_script_list_visibility() # Remove existing outline and add own outline split_container = find_or_null(script_editor.find_children("*", "HSplitContainer", true, false)) if (split_container != null): outline_container = split_container.get_child(0) if (is_outline_right): update_outline_position() old_outline = find_or_null(outline_container.find_children("*", "ItemList", true, false), 1) outline_parent = old_outline.get_parent() outline_parent.remove_child(old_outline) outline = ItemList.new() outline.allow_reselect = true outline.size_flags_vertical = Control.SIZE_EXPAND_FILL outline_parent.add_child(outline) outline.item_selected.connect(scroll_to_index) # Add a filter box for all kind of members filter_box = HBoxContainer.new() engine_func_btn = create_filter_btn(keyword_icon, "Engine callbacks") filter_box.add_child(engine_func_btn) func_btn = create_filter_btn(func_icon, "Functions") filter_box.add_child(func_btn) signal_btn = create_filter_btn(signal_icon, "Signals") filter_box.add_child(signal_btn) export_btn = create_filter_btn(export_icon, "Exported properties") filter_box.add_child(export_btn) property_btn = create_filter_btn(property_icon, "Properties") filter_box.add_child(property_btn) class_btn = create_filter_btn(class_icon, "Classes") filter_box.add_child(class_btn) constant_btn = create_filter_btn(constant_icon, "Constants") filter_box.add_child(constant_btn) outline.get_parent().add_child(filter_box) outline.get_parent().move_child(filter_box, outline.get_index()) # Callback when the filter changed filter_txt = find_or_null(outline_container.find_children("*", "LineEdit", true, false), 1) filter_txt.text_changed.connect(update_outline.unbind(1)) # Callback when the sorting changed sort_btn = find_or_null(outline_container.find_children("*", "Button", true, false)) sort_btn.pressed.connect(update_outline) get_editor_settings().settings_changed.connect(sync_settings) on_tab_changed(scripts_tab_bar.current_tab) ## Restore the old Godot script UI and free everything we created func _exit_tree() -> void: var file_system: EditorFileSystem = get_editor_interface().get_resource_filesystem() file_system.filesystem_changed.disconnect(schedule_update) if (old_script_editor_base != null): old_script_editor_base.edited_script_changed.disconnect(update_selected_tab) if (split_container != null): if (split_container != outline_container.get_parent()): split_container.add_child(outline_container) # Try to restore the previous split offset. if (is_outline_right): var split_offset: float = split_container.get_child(1).size.x split_container.split_offset = split_offset split_container.move_child(outline_container, 0) filter_txt.text_changed.disconnect(update_outline) sort_btn.pressed.disconnect(update_outline) outline.item_selected.disconnect(scroll_to_index) outline_parent.remove_child(filter_box) outline_parent.remove_child(outline) outline_parent.add_child(old_outline) outline_parent.move_child(old_outline, 1) filter_box.free() outline.free() if (scripts_tab_container != null): tab_state.restore(scripts_tab_container, scripts_tab_bar) if (scripts_tab_bar != null): scripts_tab_bar.mouse_exited.disconnect(on_tab_bar_mouse_exited) scripts_tab_bar.gui_input.disconnect(on_tab_bar_gui_input) scripts_tab_bar.tab_close_pressed.disconnect(on_tab_close) scripts_tab_bar.tab_rmb_clicked.disconnect(on_tab_rmb) scripts_tab_bar.tab_hovered.disconnect(on_tab_hovered) scripts_tab_bar.active_tab_rearranged.disconnect(on_active_tab_rearranged) scripts_tab_bar.tab_changed.disconnect(on_tab_changed) if (scripts_item_list != null): scripts_item_list.get_parent().visible = true if (outline_popup != null): outline_popup.hide() get_editor_settings().settings_changed.disconnect(sync_settings) #endregion ## Lazy pattern to update the editor only once per frame func _process(delta: float) -> void: update_editor() set_process(false) #region Input handling -> Popup ## Add navigation to the Outline func _input(event: InputEvent) -> void: if (!filter_txt.has_focus()): return if (event.is_action_pressed("ui_text_submit")): var items: PackedInt32Array = outline.get_selected_items() if (items.is_empty()): return var index: int = items[0] scroll_to_index(index) if (event.is_action_pressed("ui_down", true)): var items: PackedInt32Array = outline.get_selected_items() var index: int if (items.is_empty()): index = -1 else: index = items[0] if (index == outline.item_count - 1): return index += 1 outline.select(index) outline.ensure_current_is_visible() get_viewport().set_input_as_handled() elif (event.is_action_pressed("ui_up", true)): var items: PackedInt32Array = outline.get_selected_items() var index: int if (items.is_empty()): index = outline.item_count else: index = items[0] if (index == 0): return index -= 1 outline.select(index) outline.ensure_current_is_visible() get_viewport().set_input_as_handled() ## Triggers the Outline popup func _unhandled_key_input(event: InputEvent) -> void: if !(event is InputEventKey): return if (open_outline_popup.matches_event(event)): get_viewport().set_input_as_handled() var button_flags: Array[bool] = [] for child in filter_box.get_children(): var btn: Button = child button_flags.append(btn.button_pressed) btn.button_pressed = true var old_text: String = filter_txt.text filter_txt.text = "" outline_popup = POPUP_SCRIPT.new() outline_popup.input_listener = _input var outline_initially_closed: bool = !outline_container.visible if (outline_initially_closed): outline_container.visible = true outline_container.reparent(outline_popup) var script_editor: ScriptEditor = get_editor_interface().get_script_editor() outline_popup.popup_hide.connect(func(): if outline_initially_closed: outline_container.visible = false outline_container.reparent(split_container) if (!is_outline_right): split_container.move_child(outline_container, 0) filter_txt.text = old_text var index: int = 0 for flag in button_flags: var btn: Button = filter_box.get_child(index) btn.button_pressed = flag index += 1 outline_popup.queue_free() outline_popup = null update_outline() ) var window_rect: Rect2 if (script_editor.get_parent().get_parent() is Window): # Popup mode var window: Window = script_editor.get_parent().get_parent() window_rect = window.get_visible_rect() else: window_rect = get_editor_interface().get_base_control().get_rect() var size: Vector2i = Vector2i(400, 550) var x: int = window_rect.size.x / 2 - size.x / 2 var y: int = window_rect.size.y / 2 - size.y / 2 var position: Vector2i = Vector2i(x, y) outline_popup.popup_exclusive_on_parent(script_editor, Rect2i(position, size)) filter_txt.grab_focus() update_outline() #endregion ## Schedules an update on the frame func schedule_update(): set_process(true) ## Updates all parts of the editor needed to be synchronized with the file system. func update_editor(): if (sync_script_list): sync_tab_with_script_list() sync_script_list = false update_tabs() update_outline_cache() update_outline() func get_current_script() -> Script: var script_editor: ScriptEditor = get_editor_interface().get_script_editor() return script_editor.get_current_script() func scroll_to_index(selected_idx: int): if (outline_popup != null): outline_popup.hide.call_deferred() var script: Script = get_current_script() if (!script): return var text: String = outline.get_item_text(selected_idx) var metadata: Dictionary = outline.get_item_metadata(selected_idx) var modifier: String = metadata["modifier"] var type: String = metadata["type"] var type_with_text: String = type + " " + text if (type == "func"): type_with_text = type_with_text + "(" var source_code: String = script.get_source_code() var lines: PackedStringArray = source_code.split("\n") var index: int = 0 for line in lines: # Easy case, like 'var abc' if (line.begins_with(type_with_text)): 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)): goto_line(index) return # Special case: An 'enum' is treated different. elif (modifier == "enum" && line.contains("enum " + text)): goto_line(index) return # Hard case, probably something like '@onready var abc' if (type == "var" && line.contains(type_with_text)): goto_line(index) return index += 1 push_error(type_with_text + " or " + modifier + " not found in source code") func goto_line(index: int): var script_editor: ScriptEditor = get_editor_interface().get_script_editor() script_editor.goto_line(index) var code_edit: CodeEdit = script_editor.get_current_editor().get_base_editor() code_edit.set_caret_line(index) code_edit.set_caret_column(0) code_edit.set_v_scroll(index) code_edit.set_h_scroll(0) func create_filter_btn(icon: Texture2D, title: String) -> Button: var btn: Button = Button.new() btn.toggle_mode = true btn.icon = icon btn.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER btn.tooltip_text = title var property: StringName = as_setting(title) btn.set_meta("property", property) btn.button_pressed = get_setting(property, true) btn.toggled.connect(on_filter_button_pressed.bind(btn)) btn.add_theme_color_override("icon_pressed_color", Color.WHITE) btn.add_theme_color_override("icon_hover_color", Color.WHITE) btn.add_theme_color_override("icon_focus_color", Color.WHITE) var style_box_empty: StyleBoxEmpty = StyleBoxEmpty.new() style_box_empty.set_content_margin_all(4 * get_editor_scale()) btn.add_theme_stylebox_override("normal", style_box_empty) var style_box: StyleBoxFlat = StyleBoxFlat.new() style_box.draw_center = false style_box.border_color = Color(0.41, 0.61, 0.91) style_box.set_border_width_all(1 * get_editor_scale()) style_box.set_corner_radius_all(3 * get_editor_scale()) btn.add_theme_stylebox_override("focus", style_box) return btn func on_filter_button_pressed(pressed: bool, btn: Button): set_setting(btn.get_meta("property"), pressed) update_outline() func update_outline_position(): if (is_outline_right): # Try to restore the previous split offset. var split_offset: float = split_container.get_child(1).size.x split_container.split_offset = split_offset split_container.move_child(outline_container, 1) else: split_container.move_child(outline_container, 0) func update_script_list_visibility(): scripts_item_list.get_parent().visible = is_script_list_visible func sync_settings(): if (suppress_settings_sync): return var changed_settings: PackedStringArray = get_editor_settings().get_changed_settings() for setting in changed_settings: if (!setting.begins_with(SCRIPT_IDE)): continue if (setting == OUTLINE_POSITION_RIGHT): # Update outline position. var new_outline_right: bool = get_setting(OUTLINE_POSITION_RIGHT, is_outline_right) if (new_outline_right != is_outline_right): is_outline_right = new_outline_right update_outline_position() elif (setting == HIDE_PRIVATE_MEMBERS): # Update cache and outline to reflect the private members setting. var new_hide_private_members: bool = get_setting(HIDE_PRIVATE_MEMBERS, hide_private_members) if (new_hide_private_members != hide_private_members): hide_private_members = new_hide_private_members update_outline_cache() update_outline() elif (setting == OPEN_OUTLINE_POPUP): # Update show outline popup shortcut. open_outline_popup = get_editor_settings().get_setting(OPEN_OUTLINE_POPUP) elif (setting == SCRIPT_LIST_VISIBLE): # Update the script list visibility var new_script_list_visible: bool = get_setting(SCRIPT_LIST_VISIBLE, is_script_list_visible) if (new_script_list_visible != is_script_list_visible): is_script_list_visible = new_script_list_visible update_script_list_visibility() else: # Update filter buttons. for btn_node in filter_box.get_children(): var btn: Button = btn_node var property: StringName = btn.get_meta("property") btn.button_pressed = get_setting(property, btn.button_pressed) func as_setting(property: String) -> StringName: return SCRIPT_IDE + property.to_lower().replace(" ", "_") func get_setting(property: StringName, alt: bool) -> bool: var editor_settings: EditorSettings = get_editor_settings() if (editor_settings.has_setting(property)): return editor_settings.get_setting(property) else: editor_settings.set_setting(property, alt) editor_settings.set_initial_value(property, alt, false) return alt func set_setting(property: StringName, value: bool): var editor_settings: EditorSettings = get_editor_settings() suppress_settings_sync = true editor_settings.set_setting(property, value) suppress_settings_sync = false func on_tab_changed(idx: int): selected_tab = idx; if (old_script_editor_base != null): old_script_editor_base.edited_script_changed.disconnect(update_selected_tab) old_script_editor_base = null var script_editor: ScriptEditor = get_editor_interface().get_script_editor() var script_editor_base: ScriptEditorBase = script_editor.get_current_editor() if (script_editor_base != null): script_editor_base.edited_script_changed.connect(update_selected_tab) old_script_editor_base = script_editor_base sync_script_list = true schedule_update() func update_selected_tab(): if (selected_tab == -1): return if (scripts_item_list.item_count == 0): return scripts_tab_container.set_tab_title(selected_tab, scripts_item_list.get_item_text(selected_tab)) scripts_tab_container.set_tab_icon(selected_tab, scripts_item_list.get_item_icon(selected_tab)) func update_tabs(): for index in scripts_tab_container.get_tab_count(): scripts_tab_container.set_tab_title(index, scripts_item_list.get_item_text(index)) scripts_tab_container.set_tab_icon(index, scripts_item_list.get_item_icon(index)) #region Outline (cache) update func update_keywords(script: Script): if (script == null): return var new_script_type: StringName = script.get_instance_base_type() if (old_script_type != new_script_type): old_script_type = new_script_type keywords.clear() keywords["_static_init"] = 0 register_virtual_methods(new_script_type) func register_virtual_methods(clazz: String): for method in ClassDB.class_get_method_list(clazz): if method.flags & METHOD_FLAG_VIRTUAL > 0: keywords[method.name] = 0 func update_outline_cache(): outline_cache = null var script: Script = get_current_script() if (!script): return update_keywords(script) # Check if built-in script. In this case we need to duplicate it. if (script.get_path().contains(".tscn::GDScript")): 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 in script.get_script_method_list(): var func_name: String = dict["name"] if (keywords.has(func_name)): consumer.call(outline_cache.engine_funcs, func_name) else: if hide_private_members && func_name.begins_with("_"): 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("@")): continue consumer.call(outline_cache.funcs, func_name) # Properties / Exported variables for dict in script.get_script_property_list(): var property: String = dict["name"] if hide_private_members && property.begins_with("_"): 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 in script.get_property_list(): var property: String = dict["name"] if hide_private_members && property.begins_with("_"): continue var usage: int = dict["usage"] if (usage & PROPERTY_USAGE_SCRIPT_VARIABLE): consumer.call(outline_cache.properties, property) # Signals for dict in script.get_script_signal_list(): var signal_name: String = dict["name"] consumer.call(outline_cache.signals, signal_name) # Constants / Classes for name_key in script.get_script_constant_map(): if hide_private_members && name_key.begins_with("_"): continue var object: Variant = script.get_script_constant_map().get(name_key) if (object is GDScript && object.get_instance_base_type() == "RefCounted"): 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 # Classes if (class_btn.button_pressed): add_to_outline(outline_cache.classes, class_icon, "class") # Constants if (constant_btn.button_pressed): add_to_outline(outline_cache.constants, constant_icon, "const", "enum") # Properties if (property_btn.button_pressed): add_to_outline(outline_cache.properties, property_icon, "var") # Exports if (export_btn.button_pressed): add_to_outline(outline_cache.exports, export_icon, "var", "@export") # Signals if (signal_btn.button_pressed): add_to_outline(outline_cache.signals, signal_icon, "signal") # Functions if (func_btn.button_pressed): add_to_outline_ext(outline_cache.funcs, get_icon, "func", "static") # Engine functions if (engine_func_btn.button_pressed): add_to_outline(outline_cache.engine_funcs, keyword_icon, "func") func add_to_outline(items: Array[String], icon: Texture2D, type: String, modifier: String = ""): add_to_outline_ext(items, func(str: String): return icon, type, modifier) func add_to_outline_ext(items: Array[String], icon_callable: Callable, type: String, modifier: String = ""): var text: String = filter_txt.get_text() var move_index: int = 0 if (is_sorted()): items = items.duplicate() items.sort_custom(func(a, b): return a.naturalnocasecmp_to(b) < 0) for item 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 = { "type": type, "modifier": modifier } outline.set_item_metadata(outline.item_count - 1, dict) # Only activate the tooltip when we do not show the outline in the popup. outline.set_item_tooltip_enabled(outline.item_count - 1, outline_popup == null) outline.move_item(outline.item_count - 1, move_index) move_index += 1 func get_icon(func_name: String) -> Texture2D: var icon: Texture2D = func_icon if (func_name.begins_with("get")): icon = func_get_icon elif (func_name.begins_with("set")): icon = func_set_icon return icon #endregion func sync_tab_with_script_list(): # For some reason the selected tab is wrong. Looks like a Godot bug. if (selected_tab >= scripts_item_list.item_count): selected_tab = scripts_tab_bar.current_tab # Hide filter and outline for non .gd scripts. var is_script: bool = get_current_script() != null filter_box.visible = is_script outline.visible = is_script # Sync with script item list. if (selected_tab != -1 && scripts_item_list.item_count > 0 && !scripts_item_list.is_selected(selected_tab)): scripts_item_list.select(selected_tab) scripts_item_list.item_selected.emit(selected_tab) scripts_item_list.ensure_current_is_visible() func trigger_script_editor_update_script_names(): var script_editor: ScriptEditor = get_editor_interface().get_script_editor() # for now it is the only way to trigger script_editor._update_script_names script_editor.notification(Control.NOTIFICATION_THEME_CHANGED) #region Tab Handling func on_tab_bar_mouse_exited(): last_tab_hovered = -1 func on_tab_hovered(idx: int): last_tab_hovered = idx func on_tab_bar_gui_input(event: InputEvent): if last_tab_hovered == -1: return if event is InputEventMouseMotion: scripts_tab_bar.tooltip_text = get_res_path(last_tab_hovered) if event is InputEventMouseButton: if event.is_pressed() and event.button_index == MOUSE_BUTTON_MIDDLE: simulate_item_clicked(last_tab_hovered, MOUSE_BUTTON_MIDDLE) func on_active_tab_rearranged(idx_to: int): var control: Control = scripts_tab_container.get_tab_control(selected_tab) if (!control): return scripts_tab_container.move_child(control, idx_to) scripts_tab_container.current_tab = scripts_tab_container.current_tab selected_tab = scripts_tab_container.current_tab trigger_script_editor_update_script_names() func get_res_path(idx: int) -> String: var tab_control: Control = scripts_tab_container.get_tab_control(idx) if (tab_control == null): return '' var path_var: Variant = tab_control.get("metadata/_edit_res_path") if (path_var == null): return '' return path_var func on_tab_rmb(tab_idx: int): simulate_item_clicked(tab_idx, MOUSE_BUTTON_RIGHT) func on_tab_close(tab_idx: int): simulate_item_clicked(tab_idx, MOUSE_BUTTON_MIDDLE) func simulate_item_clicked(tab_idx: int, mouse_idx: int): scripts_item_list.item_clicked.emit(tab_idx, scripts_item_list.get_local_mouse_position(), mouse_idx) #endregion func get_editor_scale() -> float: return get_editor_interface().get_editor_scale() func is_sorted() -> bool: return get_editor_settings().get_setting("text_editor/script_list/sort_members_outline_alphabetically") func get_editor_settings() -> EditorSettings: return get_editor_interface().get_editor_settings() static func find_or_null(arr: Array[Node], index: int = 0) -> Node: if arr.is_empty(): return null return arr[index] 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] = [] class TabStateCache: var tabs_visible: bool var drag_to_rearrange_enabled: bool var tab_bar_drag_to_rearrange_enabled: bool var tab_close_display_policy: TabBar.CloseButtonDisplayPolicy var select_with_rmb: bool func save(tab_container: TabContainer, tab_bar: TabBar): if (tab_container != null): tabs_visible = tab_container.tabs_visible drag_to_rearrange_enabled = tab_container.drag_to_rearrange_enabled if (tab_bar != null): tab_bar_drag_to_rearrange_enabled = tab_bar.drag_to_rearrange_enabled tab_close_display_policy = tab_bar.tab_close_display_policy select_with_rmb = tab_bar.select_with_rmb func restore(tab_container: TabContainer, tab_bar: TabBar): if (tab_container != null): tab_container.tabs_visible = tabs_visible tab_container.drag_to_rearrange_enabled = drag_to_rearrange_enabled if (tab_bar != null): tab_bar.drag_to_rearrange_enabled = drag_to_rearrange_enabled tab_bar.tab_close_display_policy = tab_close_display_policy tab_bar.select_with_rmb = select_with_rmb