1345 lines
47 KiB
GDScript
1345 lines
47 KiB
GDScript
## Copyright (c) 2023-present Marius Hanl under the MIT License.
|
|
## The editor plugin entrypoint for Script-IDE.
|
|
##
|
|
## The Script Tabs and Outline modifies the code that is inside 'script_editor_plugin.cpp'.
|
|
## That is, the structure is changed a little bit.
|
|
## The internals of then native C++ code are therefore important in order to make this plugin work
|
|
## without interfering with the Engine.
|
|
## All the other functionality does not modify anything Engine related.
|
|
##
|
|
## Script-IDE does not use global class_name's in order to not clutter projects using it.
|
|
## Especially since this is an editor only plugin, we do not want this plugin in the final game.
|
|
## Therefore, code that references other code inside this plugin is untyped.
|
|
@tool
|
|
extends EditorPlugin
|
|
|
|
const GETTER: StringName = &"get"
|
|
const SETTER: StringName = &"set"
|
|
const UNDERSCORE: StringName = &"_"
|
|
const INLINE: StringName = &"@"
|
|
|
|
const BUILT_IN_SCRIPT: StringName = &"::GDScript"
|
|
|
|
#region Settings and Shortcuts
|
|
## 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 the order of the outline
|
|
const OUTLINE_ORDER: StringName = SCRIPT_IDE + &"outline_order"
|
|
## 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 we want to auto navigate to the script
|
|
## in the filesystem (dock) when selected
|
|
const AUTO_NAVIGATE_IN_FS: StringName = SCRIPT_IDE + &"auto_navigate_in_filesystem_dock"
|
|
## 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 to control whether the script tabs should be visible or not.
|
|
const SCRIPT_TABS_VISIBLE: StringName = SCRIPT_IDE + &"script_tabs_visible"
|
|
## Editor setting to control where the script tabs should be.
|
|
const SCRIPT_TAB_POSITION_TOP: StringName = SCRIPT_IDE + &"script_tab_position_top"
|
|
|
|
## Editor setting for the 'Open Outline Popup' shortcut
|
|
const OPEN_OUTLINE_POPUP: StringName = SCRIPT_IDE + &"open_outline_popup"
|
|
## Editor setting for the 'Open Scripts Popup' shortcut
|
|
const OPEN_SCRIPTS_POPUP: StringName = SCRIPT_IDE + &"open_scripts_popup"
|
|
## Editor setting for the 'Open Scripts Popup' shortcut
|
|
const OPEN_QUICK_SEARCH_POPUP: StringName = SCRIPT_IDE + &"open_quick_search_popup"
|
|
## Editor setting for the 'Open Override Popup' shortcut
|
|
const OPEN_OVERRIDE_POPUP: StringName = SCRIPT_IDE + &"open_override_popup"
|
|
## Editor setting for the 'Tab cycle forward' shortcut
|
|
const TAB_CYCLE_FORWARD: StringName = SCRIPT_IDE + &"tab_cycle_forward"
|
|
## Editor setting for the 'Tab cycle backward' shortcut
|
|
const TAB_CYCLE_BACKWARD: StringName = SCRIPT_IDE + &"tab_cycle_backward"
|
|
#endregion
|
|
|
|
#region Outline type name and icon
|
|
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"
|
|
|
|
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
|
|
#endregion
|
|
|
|
#region Editor settings
|
|
var is_outline_right: bool = true
|
|
var is_script_list_visible: bool = false
|
|
var hide_private_members: bool = false
|
|
var is_auto_navigate_in_fs: bool = true
|
|
var is_script_tabs_visible: bool = true
|
|
var is_script_tabs_top: bool = true
|
|
var outline_order: PackedStringArray
|
|
|
|
var open_outline_popup_shc: Shortcut
|
|
var open_scripts_popup_shc: Shortcut
|
|
var open_quick_search_popup_shc: Shortcut
|
|
var open_override_popup_shc: Shortcut
|
|
var tab_cycle_forward_shc: Shortcut
|
|
var tab_cycle_backward_shc: Shortcut
|
|
#endregion
|
|
|
|
#region Existing controls we modify
|
|
var outline_container: Control
|
|
var outline_parent: Control
|
|
var scripts_tab_container: TabContainer
|
|
var scripts_tab_bar: TabBar
|
|
var script_filter_txt: LineEdit
|
|
var scripts_item_list: ItemList
|
|
var panel_container: VSplitContainer
|
|
|
|
var split_container: HSplitContainer
|
|
var old_outline: ItemList
|
|
var outline_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 scripts_popup: PopupPanel
|
|
var quick_open_popup: PopupPanel
|
|
var override_popup: PopupPanel
|
|
|
|
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
|
|
|
|
#region Plugin variables
|
|
var keywords: Dictionary[String, bool] = {} # Used as Set.
|
|
var outline_type_order: Array[OutlineType] = []
|
|
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 = false
|
|
var file_to_navigate: String = &""
|
|
var suppress_settings_sync: bool = false
|
|
|
|
const QUICK_OPEN_INTERVAL: int = 400
|
|
var quick_open_tween: Tween
|
|
#endregion
|
|
|
|
#region Plugin Enter / Exit setup
|
|
## Change the Godot script UI and transform into an IDE like UI
|
|
func _enter_tree() -> void:
|
|
init_icons()
|
|
init_settings()
|
|
init_shortcuts()
|
|
|
|
# Update on filesystem changed (e.g. save operation).
|
|
var file_system: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
file_system.filesystem_changed.connect(schedule_update)
|
|
|
|
# Sync settings changes for this plugin.
|
|
get_editor_settings().settings_changed.connect(sync_settings)
|
|
|
|
var script_editor: ScriptEditor = EditorInterface.get_script_editor()
|
|
|
|
# Change script item list visibility (based on settings).
|
|
scripts_item_list = find_or_null(script_editor.find_children("*", "ItemList", true, false))
|
|
scripts_item_list.allow_reselect = true
|
|
scripts_item_list.item_selected.connect(hide_scripts_popup.unbind(1))
|
|
update_script_list_visibility()
|
|
|
|
# Add script filter navigation.
|
|
script_filter_txt = find_or_null(scripts_item_list.get_parent().find_children("*", "LineEdit", true, false))
|
|
script_filter_txt.gui_input.connect(navigate_on_list.bind(scripts_item_list, select_script))
|
|
|
|
# Make tab container visible.
|
|
scripts_tab_container = find_or_null(script_editor.find_children("*", "TabContainer", true, false))
|
|
scripts_tab_bar = scripts_tab_container.get_tab_bar()
|
|
|
|
# Save old tab state to restore later.
|
|
tab_state = TabStateCache.new()
|
|
tab_state.save(scripts_tab_container, scripts_tab_bar)
|
|
|
|
# Create and set script popup.
|
|
create_set_scripts_popup()
|
|
|
|
# Configure tab container and bar.
|
|
scripts_tab_bar.auto_translate_mode = Node.AUTO_TRANSLATE_MODE_DISABLED
|
|
scripts_tab_container.tabs_visible = is_script_tabs_visible
|
|
scripts_tab_container.drag_to_rearrange_enabled = true
|
|
update_tabs_position()
|
|
|
|
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)
|
|
|
|
# Remove existing outline and add own outline.
|
|
split_container = find_or_null(script_editor.find_children("*", "HSplitContainer", true, false))
|
|
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.auto_translate_mode = Node.AUTO_TRANSLATE_MODE_DISABLED
|
|
outline.allow_reselect = true
|
|
outline.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
outline_parent.add_child(outline)
|
|
|
|
outline.item_selected.connect(scroll_outline)
|
|
|
|
# Add a filter box for all kind of members
|
|
filter_box = HBoxContainer.new()
|
|
|
|
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)
|
|
update_outline_button_order()
|
|
|
|
outline.get_parent().add_child(filter_box)
|
|
outline.get_parent().move_child(filter_box, outline.get_index())
|
|
|
|
# Add navigation to the filter and text filtering.
|
|
outline_filter_txt = find_or_null(outline_container.find_children("*", "LineEdit", true, false), 1)
|
|
outline_filter_txt.gui_input.connect(navigate_on_list.bind(outline, scroll_outline))
|
|
outline_filter_txt.text_changed.connect(update_outline.unbind(1))
|
|
|
|
# Add callback when the sorting changed.
|
|
sort_btn = find_or_null(outline_container.find_children("*", "Button", true, false))
|
|
sort_btn.pressed.connect(update_outline)
|
|
|
|
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 = EditorInterface.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)
|
|
|
|
outline_filter_txt.gui_input.disconnect(navigate_on_list)
|
|
outline_filter_txt.text_changed.disconnect(update_outline)
|
|
sort_btn.pressed.disconnect(update_outline)
|
|
|
|
outline.item_selected.disconnect(scroll_outline)
|
|
|
|
outline_parent.remove_child(filter_box)
|
|
outline_parent.remove_child(outline)
|
|
outline_parent.add_child(old_outline)
|
|
outline_parent.move_child(old_outline, 2)
|
|
|
|
filter_box.free()
|
|
outline.free()
|
|
|
|
if (scripts_tab_container != null):
|
|
tab_state.restore(scripts_tab_container, scripts_tab_bar)
|
|
|
|
scripts_tab_container.pre_popup_pressed.disconnect(prepare_scripts_popup)
|
|
scripts_tab_container.set_popup(null)
|
|
scripts_popup.free()
|
|
|
|
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.allow_reselect = false
|
|
scripts_item_list.item_selected.disconnect(hide_scripts_popup)
|
|
scripts_item_list.get_parent().visible = true
|
|
|
|
if (script_filter_txt != null):
|
|
script_filter_txt.gui_input.disconnect(navigate_on_list)
|
|
|
|
if (outline_popup != null):
|
|
outline_popup.free()
|
|
if (quick_open_popup != null):
|
|
quick_open_popup.free()
|
|
if (override_popup != null):
|
|
override_popup.free()
|
|
|
|
get_editor_settings().settings_changed.disconnect(sync_settings)
|
|
#endregion
|
|
|
|
#region Plugin and Shortcut processing
|
|
## Lazy pattern to update the editor only once per frame
|
|
func _process(delta: float) -> void:
|
|
update_editor()
|
|
set_process(false)
|
|
|
|
## Process the user defined shortcuts
|
|
func _shortcut_input(event: InputEvent) -> void:
|
|
if (!event.is_pressed() || event.is_echo()):
|
|
return
|
|
|
|
if (open_outline_popup_shc.matches_event(event)):
|
|
get_viewport().set_input_as_handled()
|
|
open_outline_popup()
|
|
elif (open_scripts_popup_shc.matches_event(event)):
|
|
get_viewport().set_input_as_handled()
|
|
open_scripts_popup()
|
|
elif (open_quick_search_popup_shc.matches_event(event)):
|
|
if (quick_open_tween != null && quick_open_tween.is_running()):
|
|
get_viewport().set_input_as_handled()
|
|
if (quick_open_tween != null):
|
|
quick_open_tween.kill()
|
|
|
|
quick_open_tween = create_tween()
|
|
quick_open_tween.tween_interval(0.1)
|
|
quick_open_tween.tween_callback(open_quick_search_popup)
|
|
quick_open_tween.tween_callback(func(): quick_open_tween = null)
|
|
else:
|
|
quick_open_tween = create_tween()
|
|
quick_open_tween.tween_interval(QUICK_OPEN_INTERVAL / 1000.0)
|
|
quick_open_tween.tween_callback(func(): quick_open_tween = null)
|
|
elif (open_override_popup_shc.matches_event(event)):
|
|
get_viewport().set_input_as_handled()
|
|
open_override_popup()
|
|
elif (EditorInterface.get_script_editor().is_visible_in_tree()):
|
|
if (tab_cycle_forward_shc.matches_event(event)):
|
|
get_viewport().set_input_as_handled()
|
|
|
|
var new_tab: int = scripts_tab_container.current_tab + 1
|
|
if (new_tab == scripts_tab_container.get_tab_count()):
|
|
new_tab = 0
|
|
scripts_tab_container.current_tab = new_tab
|
|
elif (tab_cycle_backward_shc.matches_event(event)):
|
|
get_viewport().set_input_as_handled()
|
|
|
|
var new_tab: int = scripts_tab_container.current_tab - 1
|
|
if (new_tab == -1):
|
|
new_tab = scripts_tab_container.get_tab_count() - 1
|
|
scripts_tab_container.current_tab = new_tab
|
|
|
|
## May cancels the quick search shortcut timer.
|
|
func _input(event: InputEvent) -> void:
|
|
if (event is InputEventKey):
|
|
if (!open_quick_search_popup_shc.matches_event(event)):
|
|
if (quick_open_tween != null):
|
|
quick_open_tween.kill()
|
|
quick_open_tween = null
|
|
#endregion
|
|
|
|
#region Icon, Settings, Shortcut initialization
|
|
## 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"))
|
|
|
|
## Initializes all settings.
|
|
## Every setting can be changed while this plugin is active, which will override them.
|
|
func init_settings():
|
|
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)
|
|
is_auto_navigate_in_fs = get_setting(AUTO_NAVIGATE_IN_FS, is_auto_navigate_in_fs)
|
|
is_script_tabs_visible = get_setting(SCRIPT_TABS_VISIBLE, is_script_tabs_visible)
|
|
is_script_tabs_top = get_setting(SCRIPT_TAB_POSITION_TOP, is_script_tabs_top)
|
|
|
|
init_outline_order()
|
|
|
|
## 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)
|
|
|
|
update_outline_order()
|
|
|
|
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 update_outline_order():
|
|
var editor_settings: EditorSettings = get_editor_settings()
|
|
if (editor_settings.has_setting(OUTLINE_ORDER)):
|
|
outline_order = editor_settings.get_setting(OUTLINE_ORDER)
|
|
else:
|
|
outline_order = [ENGINE_FUNCS, FUNCS, SIGNALS, EXPORTED, PROPERTIES, CONSTANTS, CLASSES]
|
|
editor_settings.set_setting(OUTLINE_ORDER, outline_order)
|
|
|
|
outline_type_order.sort_custom(sort_types_by_outline_order)
|
|
|
|
func sort_buttons_by_outline_order(btn1: Button, btn2: Button) -> bool:
|
|
return sort_by_outline_order(btn1.tooltip_text, btn2.tooltip_text)
|
|
|
|
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)
|
|
|
|
## Initializes all shortcuts.
|
|
## Every shortcut can be changed while this plugin is active, which will override them.
|
|
func init_shortcuts():
|
|
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.command_or_control_autoremap = true
|
|
event.keycode = KEY_O
|
|
|
|
shortcut.events = [ event ]
|
|
editor_settings.set_setting(OPEN_OUTLINE_POPUP, shortcut)
|
|
|
|
if (!editor_settings.has_setting(OPEN_SCRIPTS_POPUP)):
|
|
var shortcut: Shortcut = Shortcut.new()
|
|
var event: InputEventKey = InputEventKey.new()
|
|
event.device = -1
|
|
event.command_or_control_autoremap = true
|
|
event.keycode = KEY_U
|
|
|
|
shortcut.events = [ event ]
|
|
editor_settings.set_setting(OPEN_SCRIPTS_POPUP, shortcut)
|
|
|
|
if (!editor_settings.has_setting(OPEN_QUICK_SEARCH_POPUP)):
|
|
var shortcut: Shortcut = Shortcut.new()
|
|
var event: InputEventKey = InputEventKey.new()
|
|
event.device = -1
|
|
event.keycode = KEY_SHIFT
|
|
|
|
shortcut.events = [ event ]
|
|
editor_settings.set_setting(OPEN_QUICK_SEARCH_POPUP, shortcut)
|
|
|
|
if (!editor_settings.has_setting(OPEN_OVERRIDE_POPUP)):
|
|
var shortcut: Shortcut = Shortcut.new()
|
|
var event: InputEventKey = InputEventKey.new()
|
|
event.device = -1
|
|
event.keycode = KEY_INSERT
|
|
event.alt_pressed = true
|
|
|
|
shortcut.events = [ event ]
|
|
editor_settings.set_setting(OPEN_OVERRIDE_POPUP, shortcut)
|
|
|
|
if (!editor_settings.has_setting(TAB_CYCLE_FORWARD)):
|
|
var shortcut: Shortcut = Shortcut.new()
|
|
var event: InputEventKey = InputEventKey.new()
|
|
event.device = -1
|
|
event.keycode = KEY_TAB
|
|
event.ctrl_pressed = true
|
|
|
|
shortcut.events = [ event ]
|
|
editor_settings.set_setting(TAB_CYCLE_FORWARD, shortcut)
|
|
|
|
if (!editor_settings.has_setting(TAB_CYCLE_BACKWARD)):
|
|
var shortcut: Shortcut = Shortcut.new()
|
|
var event: InputEventKey = InputEventKey.new()
|
|
event.device = -1
|
|
event.keycode = KEY_TAB
|
|
event.shift_pressed = true
|
|
event.ctrl_pressed = true
|
|
|
|
shortcut.events = [ event ]
|
|
editor_settings.set_setting(TAB_CYCLE_BACKWARD, shortcut)
|
|
|
|
open_outline_popup_shc = editor_settings.get_setting(OPEN_OUTLINE_POPUP)
|
|
open_scripts_popup_shc = editor_settings.get_setting(OPEN_SCRIPTS_POPUP)
|
|
open_quick_search_popup_shc = editor_settings.get_setting(OPEN_QUICK_SEARCH_POPUP)
|
|
open_override_popup_shc = editor_settings.get_setting(OPEN_OVERRIDE_POPUP)
|
|
tab_cycle_forward_shc = editor_settings.get_setting(TAB_CYCLE_FORWARD)
|
|
tab_cycle_backward_shc = editor_settings.get_setting(TAB_CYCLE_BACKWARD)
|
|
#endregion
|
|
|
|
## Schedules an update on the next frame
|
|
func schedule_update():
|
|
set_process(true)
|
|
|
|
## Updates all parts of the editor that are needed to be synchronized with the file system change.
|
|
func update_editor():
|
|
update_script_text_filter()
|
|
|
|
if (sync_script_list):
|
|
if (file_to_navigate != &""):
|
|
EditorInterface.get_file_system_dock().navigate_to_path(file_to_navigate)
|
|
EditorInterface.get_script_editor().get_current_editor().get_base_editor().grab_focus()
|
|
file_to_navigate = &""
|
|
|
|
sync_tab_with_script_list()
|
|
sync_script_list = false
|
|
|
|
update_tabs()
|
|
update_outline_cache()
|
|
update_outline()
|
|
|
|
func add_to_outline_if_selected(btn: Button, action: Callable):
|
|
if (btn.button_pressed):
|
|
action.call()
|
|
|
|
func open_quick_search_popup():
|
|
if (quick_open_popup == null):
|
|
quick_open_popup = load_rel("quickopen/quick_open_panel.tscn").instantiate()
|
|
quick_open_popup.set_unparent_when_invisible(true)
|
|
quick_open_popup.plugin = self
|
|
|
|
quick_open_popup.popup_exclusive_on_parent(EditorInterface.get_script_editor(), get_center_editor_rect())
|
|
|
|
func open_override_popup():
|
|
var script: Script = get_current_script()
|
|
if (!script):
|
|
return
|
|
|
|
if (override_popup == null):
|
|
override_popup = load_rel("override/override_panel.tscn").instantiate()
|
|
override_popup.set_unparent_when_invisible(true)
|
|
override_popup.plugin = self
|
|
|
|
override_popup.popup_exclusive_on_parent(EditorInterface.get_script_editor(), get_center_editor_rect())
|
|
|
|
func hide_scripts_popup():
|
|
if (scripts_popup != null && scripts_popup.visible):
|
|
scripts_popup.hide.call_deferred()
|
|
|
|
func create_set_scripts_popup():
|
|
panel_container = scripts_item_list.get_parent().get_parent()
|
|
|
|
scripts_popup = PopupPanel.new()
|
|
scripts_popup.popup_hide.connect(restore_scripts_list)
|
|
|
|
# Need to be inside the tree, so it can be shown as popup for the tab container.
|
|
var script_editor: ScriptEditor = EditorInterface.get_script_editor()
|
|
script_editor.add_child(scripts_popup)
|
|
|
|
scripts_tab_container.pre_popup_pressed.connect(prepare_scripts_popup)
|
|
scripts_tab_container.set_popup(scripts_popup)
|
|
|
|
func prepare_scripts_popup():
|
|
scripts_popup.size.x = outline.size.x
|
|
scripts_popup.size.y = panel_container.size.y - scripts_tab_bar.size.y
|
|
|
|
scripts_item_list.get_parent().reparent(scripts_popup)
|
|
scripts_item_list.get_parent().visible = true
|
|
|
|
script_filter_txt.grab_focus()
|
|
|
|
func restore_scripts_list():
|
|
script_filter_txt.text = &""
|
|
|
|
update_script_list_visibility()
|
|
|
|
scripts_item_list.get_parent().reparent(panel_container)
|
|
panel_container.move_child(scripts_item_list.get_parent(), 0)
|
|
|
|
func navigate_on_list(event: InputEvent, list: ItemList, submit: Callable):
|
|
if (event.is_action_pressed(&"ui_text_submit")):
|
|
var index: int = get_list_index(list)
|
|
if (index == -1):
|
|
return
|
|
|
|
submit.call(index)
|
|
list.accept_event()
|
|
elif (event.is_action_pressed(&"ui_down", true)):
|
|
var index: int = get_list_index(list)
|
|
if (index == list.item_count - 1):
|
|
return
|
|
|
|
navigate_list(list, index, 1)
|
|
elif (event.is_action_pressed(&"ui_up", true)):
|
|
var index: int = get_list_index(list)
|
|
if (index <= 0):
|
|
return
|
|
|
|
navigate_list(list, index, -1)
|
|
elif (event.is_action_pressed(&"ui_page_down", true)):
|
|
var index: int = get_list_index(list)
|
|
if (index == list.item_count - 1):
|
|
return
|
|
|
|
navigate_list(list, index, 5)
|
|
elif (event.is_action_pressed(&"ui_page_up", true)):
|
|
var index: int = get_list_index(list)
|
|
if (index <= 0):
|
|
return
|
|
|
|
navigate_list(list, index, -5)
|
|
elif (event is InputEventKey && list.item_count > 0 && !list.is_anything_selected()):
|
|
list.select(0)
|
|
|
|
func get_list_index(list: ItemList) -> int:
|
|
var items: PackedInt32Array = list.get_selected_items()
|
|
|
|
if (items.is_empty()):
|
|
return -1
|
|
|
|
return items[0]
|
|
|
|
func navigate_list(list: ItemList, index: int, amount: int):
|
|
index = clamp(index + amount, 0, list.item_count - 1)
|
|
|
|
list.select(index)
|
|
list.ensure_current_is_visible()
|
|
list.accept_event()
|
|
|
|
func get_center_editor_rect() -> Rect2i:
|
|
var script_editor: ScriptEditor = EditorInterface.get_script_editor()
|
|
|
|
var size: Vector2i = Vector2i(400, 500) * get_editor_scale()
|
|
var x: int
|
|
var y: int
|
|
|
|
if (script_editor.get_parent().get_parent() is Window):
|
|
# Floating editor.
|
|
var window: Window = script_editor.get_parent().get_parent()
|
|
var window_rect: Rect2 = window.get_visible_rect()
|
|
|
|
x = window_rect.size.x / 2 - size.x / 2
|
|
y = window_rect.size.y / 2 - size.y / 2
|
|
else:
|
|
x = script_editor.global_position.x + script_editor.size.x / 2 - size.x / 2
|
|
y = script_editor.global_position.y + script_editor.size.y / 2 - size.y / 2
|
|
|
|
return Rect2i(Vector2i(x, y), size)
|
|
|
|
func open_outline_popup():
|
|
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)
|
|
|
|
var old_text: String = outline_filter_txt.text
|
|
outline_filter_txt.text = &""
|
|
|
|
if (outline_popup == null):
|
|
outline_popup = PopupPanel.new()
|
|
outline_popup.set_unparent_when_invisible(true)
|
|
|
|
var outline_initially_closed: bool = !outline_container.visible
|
|
if (outline_initially_closed):
|
|
outline_container.visible = true
|
|
|
|
outline_container.reparent(outline_popup)
|
|
|
|
outline_popup.popup_hide.connect(on_outline_popup_hidden.bind(outline_initially_closed, old_text, button_flags))
|
|
|
|
outline_popup.popup_exclusive_on_parent(EditorInterface.get_script_editor(), get_center_editor_rect())
|
|
|
|
update_outline()
|
|
outline_filter_txt.grab_focus()
|
|
|
|
func on_outline_popup_hidden(outline_initially_closed: bool, old_text: String, button_flags: Array[bool]):
|
|
outline_popup.popup_hide.disconnect(on_outline_popup_hidden)
|
|
|
|
if outline_initially_closed:
|
|
outline_container.visible = false
|
|
|
|
outline_container.reparent(split_container)
|
|
if (!is_outline_right):
|
|
split_container.move_child(outline_container, 0)
|
|
|
|
outline_filter_txt.text = old_text
|
|
|
|
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
|
|
|
|
update_outline()
|
|
|
|
func open_scripts_popup():
|
|
scripts_item_list.get_parent().reparent(scripts_popup)
|
|
scripts_item_list.get_parent().visible = true
|
|
|
|
if (scripts_popup.get_parent() != null):
|
|
scripts_popup.get_parent().remove_child(scripts_popup)
|
|
scripts_popup.popup_exclusive_on_parent(EditorInterface.get_script_editor(), get_center_editor_rect())
|
|
|
|
script_filter_txt.grab_focus()
|
|
|
|
## Removes the script filter text and emits the signal so that the tabs stay
|
|
## and we do not break anything there.
|
|
func update_script_text_filter():
|
|
if (script_filter_txt.text != &""):
|
|
script_filter_txt.text = &""
|
|
script_filter_txt.text_changed.emit(&"")
|
|
|
|
func get_current_script() -> Script:
|
|
var script_editor: ScriptEditor = EditorInterface.get_script_editor()
|
|
return script_editor.get_current_script()
|
|
|
|
func select_script(selected_idx: int):
|
|
hide_scripts_popup()
|
|
|
|
scripts_item_list.item_selected.emit(selected_idx)
|
|
|
|
func scroll_outline(selected_idx: int):
|
|
if (outline_popup != null && outline_popup.visible):
|
|
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[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 + "("
|
|
|
|
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)):
|
|
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 = EditorInterface.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_v_scroll(index)
|
|
code_edit.set_caret_column(code_edit.get_line(index).length())
|
|
code_edit.set_h_scroll(0)
|
|
|
|
code_edit.grab_focus()
|
|
|
|
func create_filter_btn(icon: Texture2D, title: StringName) -> 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 = SCRIPT_IDE + title.to_lower().replace(" ", "_")
|
|
btn.set_meta(&"property", property)
|
|
btn.button_pressed = get_setting(property, true)
|
|
|
|
btn.toggled.connect(on_filter_button_pressed.bind(btn))
|
|
btn.gui_input.connect(on_right_click.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_hover_pressed_color", Color.WHITE)
|
|
btn.add_theme_color_override(&"icon_focus_color", Color.WHITE)
|
|
|
|
var style_box_empty: StyleBoxEmpty = StyleBoxEmpty.new()
|
|
btn.add_theme_stylebox_override(&"normal", style_box_empty)
|
|
|
|
var style_box: StyleBoxFlat = StyleBoxFlat.new()
|
|
style_box.draw_center = false
|
|
style_box.border_color = get_editor_accent_color()
|
|
style_box.set_border_width_all(1 * get_editor_scale())
|
|
style_box.set_corner_radius_all(get_editor_corner_radius() * get_editor_scale())
|
|
btn.add_theme_stylebox_override(&"focus", style_box)
|
|
|
|
return btn
|
|
|
|
func on_right_click(event: InputEvent, btn: Button):
|
|
if !(event is InputEventMouseButton):
|
|
return
|
|
|
|
var mouse_event: InputEventMouseButton = event
|
|
|
|
if (!mouse_event.is_pressed() || mouse_event.button_index != MOUSE_BUTTON_RIGHT):
|
|
return
|
|
|
|
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):
|
|
set_setting(btn.get_meta(&"property"), pressed)
|
|
|
|
update_outline()
|
|
outline_filter_txt.grab_focus()
|
|
|
|
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 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 sync_settings():
|
|
if (suppress_settings_sync):
|
|
return
|
|
|
|
var changed_settings: PackedStringArray = get_editor_settings().get_changed_settings()
|
|
for setting: String in changed_settings:
|
|
if (setting == "interface/theme/icon_saturation"):
|
|
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()
|
|
continue
|
|
|
|
if (!setting.begins_with(SCRIPT_IDE)):
|
|
continue
|
|
|
|
if (setting == OUTLINE_POSITION_RIGHT):
|
|
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 == OUTLINE_ORDER):
|
|
update_outline_order()
|
|
update_outline_button_order()
|
|
update_outline()
|
|
elif (setting == HIDE_PRIVATE_MEMBERS):
|
|
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 == SCRIPT_LIST_VISIBLE):
|
|
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()
|
|
elif (setting == SCRIPT_TABS_VISIBLE):
|
|
var new_script_tabs_visible: bool = get_setting(SCRIPT_TABS_VISIBLE, is_script_tabs_visible)
|
|
if (new_script_tabs_visible != is_script_tabs_visible):
|
|
is_script_tabs_visible = new_script_tabs_visible
|
|
|
|
scripts_tab_container.tabs_visible = is_script_tabs_visible
|
|
elif (setting == SCRIPT_TAB_POSITION_TOP):
|
|
var new_script_tabs_top: bool = get_setting(SCRIPT_TAB_POSITION_TOP, is_script_tabs_top)
|
|
if (new_script_tabs_top != is_script_tabs_top):
|
|
is_script_tabs_top = new_script_tabs_top
|
|
|
|
update_tabs_position()
|
|
elif (setting == AUTO_NAVIGATE_IN_FS):
|
|
is_auto_navigate_in_fs = get_setting(AUTO_NAVIGATE_IN_FS, is_auto_navigate_in_fs)
|
|
elif (setting == OPEN_OUTLINE_POPUP):
|
|
open_outline_popup_shc = get_shortcut(OPEN_OUTLINE_POPUP)
|
|
elif (setting == OPEN_SCRIPTS_POPUP):
|
|
open_scripts_popup_shc = get_shortcut(OPEN_SCRIPTS_POPUP)
|
|
elif (setting == OPEN_OVERRIDE_POPUP):
|
|
open_override_popup_shc = get_shortcut(OPEN_OVERRIDE_POPUP)
|
|
elif (setting == TAB_CYCLE_FORWARD):
|
|
tab_cycle_forward_shc = get_shortcut(TAB_CYCLE_FORWARD)
|
|
elif (setting == TAB_CYCLE_BACKWARD):
|
|
tab_cycle_backward_shc = get_shortcut(TAB_CYCLE_BACKWARD)
|
|
else:
|
|
# 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 = get_setting(property, btn.button_pressed)
|
|
|
|
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)
|
|
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 get_shortcut(property: StringName) -> Shortcut:
|
|
return get_editor_settings().get_setting(property)
|
|
|
|
func on_tab_changed(index: int):
|
|
selected_tab = index;
|
|
|
|
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 = EditorInterface.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
|
|
|
|
if (is_auto_navigate_in_fs && script_editor.get_current_script() != null):
|
|
var file: String = script_editor.get_current_script().get_path()
|
|
|
|
if (file.contains(BUILT_IN_SCRIPT)):
|
|
# We navigate to the scene in case of a built-in script.
|
|
file = file.get_slice(BUILT_IN_SCRIPT, 0)
|
|
|
|
file_to_navigate = file
|
|
else:
|
|
file_to_navigate = &""
|
|
|
|
schedule_update()
|
|
|
|
func update_selected_tab():
|
|
if (selected_tab == -1):
|
|
return
|
|
|
|
if (scripts_item_list.item_count == 0):
|
|
return
|
|
|
|
update_tab(selected_tab)
|
|
|
|
func update_tabs():
|
|
for index: int in scripts_tab_container.get_tab_count():
|
|
update_tab(index)
|
|
|
|
func update_tab(index: int):
|
|
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))
|
|
scripts_tab_container.set_tab_tooltip(index, scripts_item_list.get_item_tooltip(index))
|
|
|
|
func update_tabs_position():
|
|
if (is_script_tabs_top):
|
|
scripts_tab_container.tabs_position = TabContainer.POSITION_TOP
|
|
else:
|
|
scripts_tab_container.tabs_position = TabContainer.POSITION_BOTTOM
|
|
|
|
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"] = true
|
|
register_virtual_methods(new_script_type)
|
|
|
|
func register_virtual_methods(clazz: String):
|
|
for method: Dictionary in ClassDB.class_get_method_list(clazz):
|
|
if (method[&"flags"] & METHOD_FLAG_VIRTUAL > 0):
|
|
keywords[method[&"name"]] = true
|
|
|
|
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 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 (keywords.has(func_name)):
|
|
consumer.call(outline_cache.engine_funcs, func_name)
|
|
else:
|
|
if 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
|
|
|
|
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 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 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 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 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 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):
|
|
# MIGRATION: This is not needed anymore in Godot 4.5
|
|
if (Engine.get_version_info()["minor"] > 4):
|
|
return
|
|
|
|
if (last_tab_hovered == -1):
|
|
return
|
|
|
|
if (event is InputEventMouseButton):
|
|
if event.is_pressed() and event.button_index == MOUSE_BUTTON_MIDDLE:
|
|
update_script_text_filter()
|
|
simulate_item_clicked(last_tab_hovered, MOUSE_BUTTON_MIDDLE)
|
|
|
|
if (last_tab_hovered >= scripts_tab_bar.tab_count - 1):
|
|
last_tab_hovered = -1
|
|
|
|
func on_active_tab_rearranged(idx_to: int):
|
|
# MIGRATION: This is not needed anymore in Godot 4.5
|
|
if (Engine.get_version_info()["minor"] > 4):
|
|
return
|
|
|
|
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
|
|
|
|
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):
|
|
update_script_text_filter()
|
|
simulate_item_clicked(tab_idx, MOUSE_BUTTON_RIGHT)
|
|
|
|
func on_tab_close(tab_idx: int):
|
|
update_script_text_filter()
|
|
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)
|
|
|
|
func get_editor_scale() -> float:
|
|
return EditorInterface.get_editor_scale()
|
|
|
|
func get_editor_corner_radius() -> int:
|
|
return EditorInterface.get_editor_settings().get_setting("interface/theme/corner_radius")
|
|
|
|
func get_editor_accent_color() -> Color:
|
|
return EditorInterface.get_editor_settings().get_setting("interface/theme/accent_color")
|
|
|
|
func get_editor_icon_saturation() -> float:
|
|
return EditorInterface.get_editor_settings().get_setting("interface/theme/icon_saturation")
|
|
|
|
func is_sorted() -> bool:
|
|
return get_editor_settings().get_setting("text_editor/script_list/sort_members_outline_alphabetically")
|
|
|
|
func get_editor_settings() -> EditorSettings:
|
|
return EditorInterface.get_editor_settings()
|
|
|
|
func load_rel(path: String) -> Variant:
|
|
var script_path: String = get_script().get_path().get_base_dir()
|
|
return load(script_path.path_join(path))
|
|
|
|
static func find_or_null(arr: Array[Node], index: int = 0) -> Node:
|
|
if (arr.is_empty()):
|
|
push_error("""Node that is needed for Script-IDE not found.
|
|
Plugin will not work correctly.
|
|
This might be due to some other plugins or changes in the Engine.
|
|
Please report this to Script-IDE, so we can figure out a fix.""")
|
|
return null
|
|
return arr[index]
|
|
|
|
## 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
|
|
|
|
## Contains everything we modify on the Tab Control. Used to save and restore the behaviour
|
|
## to keep the Engine in a clean state when the plugin is disabled.
|
|
class TabStateCache:
|
|
var tabs_visible: bool
|
|
var drag_to_rearrange_enabled: bool
|
|
var auto_translate_mode_state: Node.AutoTranslateMode
|
|
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
|
|
auto_translate_mode_state = tab_bar.auto_translate_mode
|
|
|
|
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
|
|
tab_bar.auto_translate_mode = auto_translate_mode_state
|