···1616# OSX
1717.DS_Store
18181919-# User plugins
2020-addons/autoscreenshot/
2121-addons/script-ide/
2222-2319Movies/
+21
addons/script-ide/LICENSE
···11+MIT License
22+33+Copyright (c) 2023 Marius Hanl
44+55+Permission is hereby granted, free of charge, to any person obtaining a copy
66+of this software and associated documentation files (the "Software"), to deal
77+in the Software without restriction, including without limitation the rights
88+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
99+copies of the Software, and to permit persons to whom the Software is
1010+furnished to do so, subject to the following conditions:
1111+1212+The above copyright notice and this permission notice shall be included in all
1313+copies or substantial portions of the Software.
1414+1515+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1616+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1717+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1818+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1919+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121+SOFTWARE.
+62
addons/script-ide/README.md
···11+# Script IDE
22+33+Transforms the Script UI into an IDE like UI.
44+Tabs are used for navigating between scripts.
55+The default Outline got an overhaul and now shows all members of the script (not just methods) with unique icons for faster navigation.
66+Enhanced keyboard navigation for Scripts and Outline.
77+Fast quick search functionality.
88+99+Features:
1010+- Scripts are now shown as Tabs inside a TabContainer
1111+- The Outline got an overhaul and shows more than just the methods of the script. It includes the following members with a unique icon:
1212+ - Classes (Red Square)
1313+ - Constants (Red Circle)
1414+ - Signals (Yellow)
1515+ - Export variables (Orange)
1616+ - (Static) Variables (Red)
1717+ - Engine callback functions (Blue)
1818+ - (Static) Functions (Green)
1919+ - Setter functions (Green circle, with an arrow inside it pointing to the right)
2020+ - Getter functions (Green circle, with an arrow inside it pointing to the left)
2121+- All the different members of the script can be hidden or made visible again by the outline filter. This allows fine control what should be visible (e.g. only signals, (Godot) functions, ...)
2222+- A `Right Click` enables only the clicked filter, another `Right Click` will enable all filters again
2323+- The Outline can be opened in a Popup with a defined shortcut for quick navigation between methods
2424+- You can navigate through the Outline with the `Arrow` keys (or `Page up/Page down`) and scroll to the selected item by pressing `ENTER`
2525+- Scripts can be opened in a Popup with a defined shortcut or when clicking the three dots on the top right of the TabContainer for quick navigation between scripts
2626+- The currently edited script is automatically selected in the Filesystem Dock
2727+- Files can be quickly searched by the Quick Search Popup with `Shift`+`Shift`
2828+- The plugin is written with performance in mind, everything is very fast and works without any lags or stuttering
2929+3030+Customization:
3131+- The Outline is on the right side (can be changed to be on the left side again)
3232+- The Outline can be toggled via `File -> Toggle Scripts Panel`. This will hide or show it
3333+- The order in the Outline can be changed
3434+- There is also the possibility to hide private members, this is all members starting with a `_`
3535+- The Script ItemList is not visible by default, but can be made visible again
3636+3737+All settings can be changed in the `Editor Settings` under `Plugin` -> `Script Ide`:
3838+- `Open Outline Popup` = Shortcut to control how the Outline Popup should be triggered (default=CTRL+O or META+O)
3939+- `Outline Position Right` = Flag to control whether the outline should be on the right or on the left side of the script editor (default=true)
4040+- `Outline Order` = List which specifies the order of all different types in the Outline
4141+- `Hide Private Members` = Flag to control whether private members (methods/variables/constants starting with '_') should be hidden in the Outline or not (default=false)
4242+- `Open Script Popup` = Shortcut to control how the Script Popup should be triggered (default=CTRL+U or META+U)
4343+- `Script List Visible` = Flag to control whether the script list should still be visible or not (above the outline) (default=false)
4444+- `Script Tabs Visible` = Flag to control whether the script tabs should be visible or not (default=true)
4545+- `Script Tabs Position Top` = Flag to control whether the script tabs should be on the top or on the bottom (default=true)
4646+- `Auto Navigate in FileSystem Dock` = Flag to control whether the script that is currently edited should be automatically selected in the Filesystem Dock (default=true)
4747+- `Open Quick Search Popup` = Shortcut to control how the Quick Search Popup should be triggered (default=Shift+Shift, double press behavior is hardcoded for now)
4848+- `Cycle Tab forward` = Shortcut to cycle the script tabs in the forward direction (only works in the 'Script' Editor Tab) (default=CTRL+TAB)
4949+- `Cycle Tab backward` = Shortcut to cycle the script tabs in the backward direction (only works in the 'Script' Editor Tab) (default=CTRL+SHIFT+TAB)
5050+- All outline visibility settings
5151+5252+
5353+5454+
5555+5656+
5757+5858+
5959+6060+
6161+6262+
···11+[plugin]
22+33+name="Script-IDE"
44+description="Transforms the Script UI into an IDE like UI. Tabs are used for navigating between scripts. The default Outline got an overhaul and now shows all members of the script (not just methods) with unique icons for faster navigation. Enhanced keyboard navigation for Scripts and Outline. Fast quick search functionality."
55+author="Marius Hanl"
66+version="1.7.2"
77+script="plugin.gd"
+1304
addons/script-ide/plugin.gd
···11+## Copyright (c) 2023-present Marius Hanl under the MIT License.
22+## The editor plugin entrypoint for Script-IDE.
33+##
44+## The Script Tabs and Outline modifies the code that is inside 'script_editor_plugin.cpp'.
55+## That is, the structure is changed a little bit.
66+## The internals of then native C++ code are therefore important in order to make this plugin work
77+## without interfering with the Engine.
88+## All the other functionality does not modify anything Engine related.
99+##
1010+## Script-IDE does not use global class_name's in order to not clutter projects using it.
1111+## Especially since this is an editor only plugin, we do not want this plugin in the final game.
1212+## Therefore, code that references other code inside this plugin is untyped.
1313+@tool
1414+extends EditorPlugin
1515+1616+const GETTER: StringName = &"get"
1717+const SETTER: StringName = &"set"
1818+const UNDERSCORE: StringName = &"_"
1919+const INLINE: StringName = &"@"
2020+2121+const BUILT_IN_SCRIPT: StringName = &"::GDScript"
2222+2323+#region Settings and Shortcuts
2424+## Editor setting path
2525+const SCRIPT_IDE: StringName = &"plugin/script_ide/"
2626+## Editor setting for the outline position
2727+const OUTLINE_POSITION_RIGHT: StringName = SCRIPT_IDE + &"outline_position_right"
2828+## Editor setting to control the order of the outline
2929+const OUTLINE_ORDER: StringName = SCRIPT_IDE + &"outline_order"
3030+## Editor setting to control whether private members (annotated with '_' should be hidden or not)
3131+const HIDE_PRIVATE_MEMBERS: StringName = SCRIPT_IDE + &"hide_private_members"
3232+## Editor setting to control whether we want to auto navigate to the script
3333+## in the filesystem (dock) when selected
3434+const AUTO_NAVIGATE_IN_FS: StringName = SCRIPT_IDE + &"auto_navigate_in_filesystem_dock"
3535+## Editor setting to control whether the script list should be visible or not
3636+const SCRIPT_LIST_VISIBLE: StringName = SCRIPT_IDE + &"script_list_visible"
3737+## Editor setting to control whether the script tabs should be visible or not.
3838+const SCRIPT_TABS_VISIBLE: StringName = SCRIPT_IDE + &"script_tabs_visible"
3939+## Editor setting to control where the script tabs should be.
4040+const SCRIPT_TAB_POSITION_TOP: StringName = SCRIPT_IDE + &"script_tab_position_top"
4141+4242+## Editor setting for the 'Open Outline Popup' shortcut
4343+const OPEN_OUTLINE_POPUP: StringName = SCRIPT_IDE + &"open_outline_popup"
4444+## Editor setting for the 'Open Scripts Popup' shortcut
4545+const OPEN_SCRIPTS_POPUP: StringName = SCRIPT_IDE + &"open_scripts_popup"
4646+## Editor setting for the 'Open Scripts Popup' shortcut
4747+const OPEN_QUICK_SEARCH_POPUP: StringName = SCRIPT_IDE + &"open_quick_search_popup"
4848+## Editor setting for the 'Tab cycle forward' shortcut
4949+const TAB_CYCLE_FORWARD: StringName = SCRIPT_IDE + &"tab_cycle_forward"
5050+## Editor setting for the 'Tab cycle backward' shortcut
5151+const TAB_CYCLE_BACKWARD: StringName = SCRIPT_IDE + &"tab_cycle_backward"
5252+#endregion
5353+5454+#region Outline type name and icon
5555+const ENGINE_FUNCS: StringName = &"Engine Callbacks"
5656+const FUNCS: StringName = &"Functions"
5757+const SIGNALS: StringName = &"Signals"
5858+const EXPORTED: StringName = &"Exported Properties"
5959+const PROPERTIES: StringName = &"Properties"
6060+const CLASSES: StringName = &"Classes"
6161+const CONSTANTS: StringName = &"Constants"
6262+6363+var engine_func_icon: Texture2D
6464+var func_icon: Texture2D
6565+var func_get_icon: Texture2D
6666+var func_set_icon: Texture2D
6767+var property_icon: Texture2D
6868+var export_icon: Texture2D
6969+var signal_icon: Texture2D
7070+var constant_icon: Texture2D
7171+var class_icon: Texture2D
7272+#endregion
7373+7474+#region Editor settings
7575+var is_outline_right: bool = true
7676+var is_script_list_visible: bool = false
7777+var hide_private_members: bool = false
7878+var is_auto_navigate_in_fs: bool = true
7979+var is_script_tabs_visible: bool = true
8080+var is_script_tabs_top: bool = true
8181+var outline_order: PackedStringArray
8282+8383+var open_outline_popup_shc: Shortcut
8484+var open_scripts_popup_shc: Shortcut
8585+var open_quick_search_popup_shc: Shortcut
8686+var tab_cycle_forward_shc: Shortcut
8787+var tab_cycle_backward_shc: Shortcut
8888+#endregion
8989+9090+#region Existing controls we modify
9191+var outline_container: Control
9292+var outline_parent: Control
9393+var scripts_tab_container: TabContainer
9494+var scripts_tab_bar: TabBar
9595+var script_filter_txt: LineEdit
9696+var scripts_item_list: ItemList
9797+var panel_container: VSplitContainer
9898+9999+var split_container: HSplitContainer
100100+var old_outline: ItemList
101101+var outline_filter_txt: LineEdit
102102+var sort_btn: Button
103103+#endregion
104104+105105+#region Own controls we add
106106+var outline: ItemList
107107+var outline_popup: PopupPanel
108108+var filter_box: HBoxContainer
109109+110110+var scripts_popup: PopupPanel
111111+var quick_open_popup: PopupPanel
112112+113113+var class_btn: Button
114114+var constant_btn: Button
115115+var signal_btn: Button
116116+var property_btn: Button
117117+var export_btn: Button
118118+var func_btn: Button
119119+var engine_func_btn: Button
120120+#endregion
121121+122122+#region Plugin variables
123123+var keywords: Dictionary = {} # [String, int = 0] # Used as Set.
124124+var outline_type_order: Array[OutlineType] = []
125125+var outline_cache: OutlineCache
126126+var tab_state: TabStateCache
127127+128128+var old_script_editor_base: ScriptEditorBase
129129+var old_script_type: StringName
130130+131131+var selected_tab: int = -1
132132+var last_tab_hovered: int = -1
133133+var sync_script_list: bool = false
134134+var file_to_navigate: String = &""
135135+var suppress_settings_sync: bool = false
136136+137137+const QUICK_OPEN_INTERVAL: int = 400
138138+var quick_open_tween: Tween
139139+#endregion
140140+141141+#region Plugin Enter / Exit setup
142142+## Change the Godot script UI and transform into an IDE like UI
143143+func _enter_tree() -> void:
144144+ init_icons()
145145+ init_settings()
146146+ init_shortcuts()
147147+148148+ # Update on filesystem changed (e.g. save operation).
149149+ var file_system: EditorFileSystem = EditorInterface.get_resource_filesystem()
150150+ file_system.filesystem_changed.connect(schedule_update)
151151+152152+ # Sync settings changes for this plugin.
153153+ get_editor_settings().settings_changed.connect(sync_settings)
154154+155155+ var script_editor: ScriptEditor = EditorInterface.get_script_editor()
156156+157157+ # Change script item list visibility (based on settings).
158158+ scripts_item_list = find_or_null(script_editor.find_children("*", "ItemList", true, false))
159159+ scripts_item_list.allow_reselect = true
160160+ scripts_item_list.item_selected.connect(hide_scripts_popup.unbind(1))
161161+ update_script_list_visibility()
162162+163163+ # Add script filter navigation.
164164+ script_filter_txt = find_or_null(scripts_item_list.get_parent().find_children("*", "LineEdit", true, false))
165165+ script_filter_txt.gui_input.connect(navigate_on_list.bind(scripts_item_list, select_script))
166166+167167+ # Make tab container visible.
168168+ scripts_tab_container = find_or_null(script_editor.find_children("*", "TabContainer", true, false))
169169+ scripts_tab_bar = scripts_tab_container.get_tab_bar()
170170+171171+ # Save old tab state to restore later.
172172+ tab_state = TabStateCache.new()
173173+ tab_state.save(scripts_tab_container, scripts_tab_bar)
174174+175175+ # Create and set script popup.
176176+ create_set_scripts_popup()
177177+178178+ # Configure tab container and bar.
179179+ scripts_tab_container.tabs_visible = is_script_tabs_visible
180180+ scripts_tab_container.drag_to_rearrange_enabled = true
181181+ scripts_tab_container.auto_translate_mode = Node.AUTO_TRANSLATE_MODE_DISABLED
182182+ update_tabs_position()
183183+184184+ scripts_tab_bar.tab_close_display_policy = TabBar.CLOSE_BUTTON_SHOW_ACTIVE_ONLY
185185+ scripts_tab_bar.drag_to_rearrange_enabled = true
186186+ scripts_tab_bar.select_with_rmb = true
187187+ scripts_tab_bar.tab_close_pressed.connect(on_tab_close)
188188+ scripts_tab_bar.tab_rmb_clicked.connect(on_tab_rmb)
189189+ scripts_tab_bar.tab_hovered.connect(on_tab_hovered)
190190+ scripts_tab_bar.mouse_exited.connect(on_tab_bar_mouse_exited)
191191+ scripts_tab_bar.active_tab_rearranged.connect(on_active_tab_rearranged)
192192+ scripts_tab_bar.gui_input.connect(on_tab_bar_gui_input)
193193+194194+ scripts_tab_bar.tab_changed.connect(on_tab_changed)
195195+196196+ # Remove existing outline and add own outline.
197197+ split_container = find_or_null(script_editor.find_children("*", "HSplitContainer", true, false))
198198+ outline_container = split_container.get_child(0)
199199+200200+ if (is_outline_right):
201201+ update_outline_position()
202202+203203+ old_outline = find_or_null(outline_container.find_children("*", "ItemList", true, false), 1)
204204+ outline_parent = old_outline.get_parent()
205205+ outline_parent.remove_child(old_outline)
206206+207207+ outline = ItemList.new()
208208+ outline.allow_reselect = true
209209+ outline.size_flags_vertical = Control.SIZE_EXPAND_FILL
210210+ outline_parent.add_child(outline)
211211+212212+ outline.item_selected.connect(scroll_outline)
213213+214214+ # Add a filter box for all kind of members
215215+ filter_box = HBoxContainer.new()
216216+217217+ engine_func_btn = create_filter_btn(engine_func_icon, ENGINE_FUNCS)
218218+ func_btn = create_filter_btn(func_icon, FUNCS)
219219+ signal_btn = create_filter_btn(signal_icon, SIGNALS)
220220+ export_btn = create_filter_btn(export_icon, EXPORTED)
221221+ property_btn = create_filter_btn(property_icon, PROPERTIES)
222222+ class_btn = create_filter_btn(class_icon, CLASSES)
223223+ constant_btn = create_filter_btn(constant_icon, CONSTANTS)
224224+ update_outline_button_order()
225225+226226+ outline.get_parent().add_child(filter_box)
227227+ outline.get_parent().move_child(filter_box, outline.get_index())
228228+229229+ # Add navigation to the filter and text filtering.
230230+ outline_filter_txt = find_or_null(outline_container.find_children("*", "LineEdit", true, false), 1)
231231+ outline_filter_txt.gui_input.connect(navigate_on_list.bind(outline, scroll_outline))
232232+ outline_filter_txt.text_changed.connect(update_outline.unbind(1))
233233+234234+ # Add callback when the sorting changed.
235235+ sort_btn = find_or_null(outline_container.find_children("*", "Button", true, false))
236236+ sort_btn.pressed.connect(update_outline)
237237+238238+ on_tab_changed(scripts_tab_bar.current_tab)
239239+240240+## Restore the old Godot script UI and free everything we created
241241+func _exit_tree() -> void:
242242+ var file_system: EditorFileSystem = EditorInterface.get_resource_filesystem()
243243+ file_system.filesystem_changed.disconnect(schedule_update)
244244+245245+ if (old_script_editor_base != null):
246246+ old_script_editor_base.edited_script_changed.disconnect(update_selected_tab)
247247+248248+ if (split_container != null):
249249+ if (split_container != outline_container.get_parent()):
250250+ split_container.add_child(outline_container)
251251+252252+ # Try to restore the previous split offset.
253253+ if (is_outline_right):
254254+ var split_offset: float = split_container.get_child(1).size.x
255255+ split_container.split_offset = split_offset
256256+257257+ split_container.move_child(outline_container, 0)
258258+259259+ outline_filter_txt.gui_input.disconnect(navigate_on_list)
260260+ outline_filter_txt.text_changed.disconnect(update_outline)
261261+ sort_btn.pressed.disconnect(update_outline)
262262+263263+ outline.item_selected.disconnect(scroll_outline)
264264+265265+ outline_parent.remove_child(filter_box)
266266+ outline_parent.remove_child(outline)
267267+ outline_parent.add_child(old_outline)
268268+ outline_parent.move_child(old_outline, 2)
269269+270270+ filter_box.free()
271271+ outline.free()
272272+273273+ if (scripts_tab_container != null):
274274+ tab_state.restore(scripts_tab_container, scripts_tab_bar)
275275+276276+ scripts_tab_container.pre_popup_pressed.disconnect(prepare_scripts_popup)
277277+ scripts_tab_container.set_popup(null)
278278+ scripts_popup.free()
279279+280280+ if (scripts_tab_bar != null):
281281+ scripts_tab_bar.mouse_exited.disconnect(on_tab_bar_mouse_exited)
282282+ scripts_tab_bar.gui_input.disconnect(on_tab_bar_gui_input)
283283+ scripts_tab_bar.tab_close_pressed.disconnect(on_tab_close)
284284+ scripts_tab_bar.tab_rmb_clicked.disconnect(on_tab_rmb)
285285+ scripts_tab_bar.tab_hovered.disconnect(on_tab_hovered)
286286+ scripts_tab_bar.active_tab_rearranged.disconnect(on_active_tab_rearranged)
287287+288288+ scripts_tab_bar.tab_changed.disconnect(on_tab_changed)
289289+290290+ if (scripts_item_list != null):
291291+ scripts_item_list.allow_reselect = false
292292+ scripts_item_list.item_selected.disconnect(hide_scripts_popup)
293293+ scripts_item_list.get_parent().visible = true
294294+295295+ if (script_filter_txt != null):
296296+ script_filter_txt.gui_input.disconnect(navigate_on_list)
297297+298298+ if (outline_popup != null):
299299+ outline_popup.free()
300300+301301+ if (quick_open_popup != null):
302302+ quick_open_popup.free()
303303+304304+ get_editor_settings().settings_changed.disconnect(sync_settings)
305305+#endregion
306306+307307+#region Plugin and Shortcut processing
308308+## Lazy pattern to update the editor only once per frame
309309+func _process(delta: float) -> void:
310310+ update_editor()
311311+ set_process(false)
312312+313313+## Process the user defined shortcuts
314314+func _shortcut_input(event: InputEvent) -> void:
315315+ if (!event.is_pressed() || event.is_echo()):
316316+ return
317317+318318+ if (open_outline_popup_shc.matches_event(event)):
319319+ get_viewport().set_input_as_handled()
320320+ open_outline_popup()
321321+ elif (open_scripts_popup_shc.matches_event(event)):
322322+ get_viewport().set_input_as_handled()
323323+ open_scripts_popup()
324324+ elif (open_quick_search_popup_shc.matches_event(event)):
325325+ if (quick_open_tween != null && quick_open_tween.is_running()):
326326+ get_viewport().set_input_as_handled()
327327+ if (quick_open_tween != null):
328328+ quick_open_tween.kill()
329329+330330+ quick_open_tween = create_tween()
331331+ quick_open_tween.tween_interval(0.1)
332332+ quick_open_tween.tween_callback(open_quick_search_popup)
333333+ quick_open_tween.tween_callback(func(): quick_open_tween = null)
334334+ else:
335335+ quick_open_tween = create_tween()
336336+ quick_open_tween.tween_interval(QUICK_OPEN_INTERVAL / 1000.0)
337337+ quick_open_tween.tween_callback(func(): quick_open_tween = null)
338338+ elif (EditorInterface.get_script_editor().is_visible_in_tree()):
339339+ if (tab_cycle_forward_shc.matches_event(event)):
340340+ get_viewport().set_input_as_handled()
341341+342342+ var new_tab: int = scripts_tab_container.current_tab + 1
343343+ if (new_tab == scripts_tab_container.get_tab_count()):
344344+ new_tab = 0
345345+ scripts_tab_container.current_tab = new_tab
346346+ elif (tab_cycle_backward_shc.matches_event(event)):
347347+ get_viewport().set_input_as_handled()
348348+349349+ var new_tab: int = scripts_tab_container.current_tab - 1
350350+ if (new_tab == -1):
351351+ new_tab = scripts_tab_container.get_tab_count() - 1
352352+ scripts_tab_container.current_tab = new_tab
353353+354354+## May cancels the quick search shortcut timer.
355355+func _input(event: InputEvent) -> void:
356356+ if (event is InputEventKey):
357357+ if (!open_quick_search_popup_shc.matches_event(event)):
358358+ if (quick_open_tween != null):
359359+ quick_open_tween.kill()
360360+ quick_open_tween = null
361361+#endregion
362362+363363+#region Icon, Settings, Shortcut initialization
364364+## Initializes all plugin icons, while respecting the editor settings.
365365+func init_icons():
366366+ engine_func_icon = create_editor_texture(load_rel("icon/engine_func.svg"))
367367+ func_icon = create_editor_texture(load_rel("icon/func.svg"))
368368+ func_get_icon = create_editor_texture(load_rel("icon/func_get.svg"))
369369+ func_set_icon = create_editor_texture(load_rel("icon/func_set.svg"))
370370+ property_icon = create_editor_texture(load_rel("icon/property.svg"))
371371+ export_icon = create_editor_texture(load_rel("icon/export.svg"))
372372+ signal_icon = create_editor_texture(load_rel("icon/signal.svg"))
373373+ constant_icon = create_editor_texture(load_rel("icon/constant.svg"))
374374+ class_icon = create_editor_texture(load_rel("icon/class.svg"))
375375+376376+## Initializes all settings.
377377+## Every setting can be changed while this plugin is active, which will override them.
378378+func init_settings():
379379+ is_outline_right = get_setting(OUTLINE_POSITION_RIGHT, is_outline_right)
380380+ hide_private_members = get_setting(HIDE_PRIVATE_MEMBERS, hide_private_members)
381381+ is_script_list_visible = get_setting(SCRIPT_LIST_VISIBLE, is_script_list_visible)
382382+ is_auto_navigate_in_fs = get_setting(AUTO_NAVIGATE_IN_FS, is_auto_navigate_in_fs)
383383+ is_script_tabs_visible = get_setting(SCRIPT_TABS_VISIBLE, is_script_tabs_visible)
384384+ is_script_tabs_top = get_setting(SCRIPT_TAB_POSITION_TOP, is_script_tabs_top)
385385+386386+ init_outline_order()
387387+388388+## Initializes the outline type structure and sorts it based off the outline order.
389389+func init_outline_order():
390390+ var outline_type: OutlineType = OutlineType.new()
391391+ outline_type.type_name = ENGINE_FUNCS
392392+ outline_type.add_to_outline = func(): add_to_outline_if_selected(engine_func_btn,
393393+ func(): add_to_outline(outline_cache.engine_funcs, engine_func_icon, &"func"))
394394+ outline_type_order.append(outline_type)
395395+396396+ outline_type = OutlineType.new()
397397+ outline_type.type_name = FUNCS
398398+ outline_type.add_to_outline = func(): add_to_outline_if_selected(func_btn,
399399+ func(): add_to_outline_ext(outline_cache.funcs, get_func_icon, &"func", &"static"))
400400+ outline_type_order.append(outline_type)
401401+402402+ outline_type = OutlineType.new()
403403+ outline_type.type_name = SIGNALS
404404+ outline_type.add_to_outline = func(): add_to_outline_if_selected(signal_btn,
405405+ func(): add_to_outline(outline_cache.signals, signal_icon, &"signal"))
406406+ outline_type_order.append(outline_type)
407407+408408+ outline_type = OutlineType.new()
409409+ outline_type.type_name = EXPORTED
410410+ outline_type.add_to_outline = func(): add_to_outline_if_selected(export_btn,
411411+ func(): add_to_outline(outline_cache.exports, export_icon, &"var", &"@export"))
412412+ outline_type_order.append(outline_type)
413413+414414+ outline_type = OutlineType.new()
415415+ outline_type.type_name = PROPERTIES
416416+ outline_type.add_to_outline = func(): add_to_outline_if_selected(property_btn,
417417+ func(): add_to_outline(outline_cache.properties, property_icon, &"var"))
418418+ outline_type_order.append(outline_type)
419419+420420+ outline_type = OutlineType.new()
421421+ outline_type.type_name = CLASSES
422422+ outline_type.add_to_outline = func(): add_to_outline_if_selected(class_btn,
423423+ func(): add_to_outline(outline_cache.classes, class_icon, &"class"))
424424+ outline_type_order.append(outline_type)
425425+426426+ outline_type = OutlineType.new()
427427+ outline_type.type_name = CONSTANTS
428428+ outline_type.add_to_outline = func(): add_to_outline_if_selected(constant_btn,
429429+ func(): add_to_outline(outline_cache.constants, constant_icon, &"const", &"enum"))
430430+ outline_type_order.append(outline_type)
431431+432432+ update_outline_order()
433433+434434+func update_outline_button_order():
435435+ var all_buttons: Array[Button] = [engine_func_btn, func_btn, signal_btn, export_btn, property_btn, class_btn, constant_btn]
436436+ all_buttons.sort_custom(sort_buttons_by_outline_order)
437437+438438+ for btn: Button in all_buttons:
439439+ if (btn.get_parent() != null):
440440+ filter_box.remove_child(btn)
441441+442442+ for btn: Button in all_buttons:
443443+ filter_box.add_child(btn)
444444+445445+func update_outline_order():
446446+ var editor_settings: EditorSettings = get_editor_settings()
447447+ if (editor_settings.has_setting(OUTLINE_ORDER)):
448448+ outline_order = editor_settings.get_setting(OUTLINE_ORDER)
449449+ else:
450450+ outline_order = [ENGINE_FUNCS, FUNCS, SIGNALS, EXPORTED, PROPERTIES, CONSTANTS, CLASSES]
451451+ editor_settings.set_setting(OUTLINE_ORDER, outline_order)
452452+453453+ outline_type_order.sort_custom(sort_types_by_outline_order)
454454+455455+func sort_buttons_by_outline_order(btn1: Button, btn2: Button) -> bool:
456456+ return sort_by_outline_order(btn1.tooltip_text, btn2.tooltip_text)
457457+458458+func sort_types_by_outline_order(type1: OutlineType, type2: OutlineType) -> bool:
459459+ return sort_by_outline_order(type1.type_name, type2.type_name)
460460+461461+func sort_by_outline_order(outline_type1: StringName, outline_type2: StringName) -> bool:
462462+ return outline_order.find(outline_type1) < outline_order.find(outline_type2)
463463+464464+## Initializes all shortcuts.
465465+## Every shortcut can be changed while this plugin is active, which will override them.
466466+func init_shortcuts():
467467+ var editor_settings: EditorSettings = get_editor_settings()
468468+ if (!editor_settings.has_setting(OPEN_OUTLINE_POPUP)):
469469+ var shortcut: Shortcut = Shortcut.new()
470470+ var event: InputEventKey = InputEventKey.new()
471471+ event.device = -1
472472+ event.command_or_control_autoremap = true
473473+ event.keycode = KEY_O
474474+475475+ shortcut.events = [ event ]
476476+ editor_settings.set_setting(OPEN_OUTLINE_POPUP, shortcut)
477477+478478+ if (!editor_settings.has_setting(OPEN_SCRIPTS_POPUP)):
479479+ var shortcut: Shortcut = Shortcut.new()
480480+ var event: InputEventKey = InputEventKey.new()
481481+ event.device = -1
482482+ event.command_or_control_autoremap = true
483483+ event.keycode = KEY_U
484484+485485+ shortcut.events = [ event ]
486486+ editor_settings.set_setting(OPEN_SCRIPTS_POPUP, shortcut)
487487+488488+ if (!editor_settings.has_setting(OPEN_QUICK_SEARCH_POPUP)):
489489+ var shortcut: Shortcut = Shortcut.new()
490490+ var event: InputEventKey = InputEventKey.new()
491491+ event.device = -1
492492+ event.keycode = KEY_SHIFT
493493+494494+ shortcut.events = [ event ]
495495+ editor_settings.set_setting(OPEN_QUICK_SEARCH_POPUP, shortcut)
496496+497497+ if (!editor_settings.has_setting(TAB_CYCLE_FORWARD)):
498498+ var shortcut: Shortcut = Shortcut.new()
499499+ var event: InputEventKey = InputEventKey.new()
500500+ event.device = -1
501501+ event.keycode = KEY_TAB
502502+ event.ctrl_pressed = true
503503+504504+ shortcut.events = [ event ]
505505+ editor_settings.set_setting(TAB_CYCLE_FORWARD, shortcut)
506506+507507+ if (!editor_settings.has_setting(TAB_CYCLE_BACKWARD)):
508508+ var shortcut: Shortcut = Shortcut.new()
509509+ var event: InputEventKey = InputEventKey.new()
510510+ event.device = -1
511511+ event.keycode = KEY_TAB
512512+ event.shift_pressed = true
513513+ event.ctrl_pressed = true
514514+515515+ shortcut.events = [ event ]
516516+ editor_settings.set_setting(TAB_CYCLE_BACKWARD, shortcut)
517517+518518+ open_outline_popup_shc = editor_settings.get_setting(OPEN_OUTLINE_POPUP)
519519+ open_scripts_popup_shc = editor_settings.get_setting(OPEN_SCRIPTS_POPUP)
520520+ open_quick_search_popup_shc = editor_settings.get_setting(OPEN_QUICK_SEARCH_POPUP)
521521+ tab_cycle_forward_shc = editor_settings.get_setting(TAB_CYCLE_FORWARD)
522522+ tab_cycle_backward_shc = editor_settings.get_setting(TAB_CYCLE_BACKWARD)
523523+#endregion
524524+525525+## Schedules an update on the next frame
526526+func schedule_update():
527527+ set_process(true)
528528+529529+## Updates all parts of the editor that are needed to be synchronized with the file system change.
530530+func update_editor():
531531+ update_script_text_filter()
532532+533533+ if (sync_script_list):
534534+ if (file_to_navigate != &""):
535535+ EditorInterface.get_file_system_dock().navigate_to_path(file_to_navigate)
536536+ EditorInterface.get_script_editor().get_current_editor().get_base_editor().grab_focus()
537537+ file_to_navigate = &""
538538+539539+ sync_tab_with_script_list()
540540+ sync_script_list = false
541541+542542+ update_tabs()
543543+ update_outline_cache()
544544+ update_outline()
545545+546546+func add_to_outline_if_selected(btn: Button, action: Callable):
547547+ if (btn.button_pressed):
548548+ action.call()
549549+550550+func open_quick_search_popup():
551551+ if (quick_open_popup == null):
552552+ quick_open_popup = load_rel("quickopen/quick_open_panel.tscn").instantiate()
553553+ quick_open_popup.plugin = self
554554+555555+ if (quick_open_popup.get_parent() != null):
556556+ quick_open_popup.get_parent().remove_child(quick_open_popup)
557557+ quick_open_popup.popup_exclusive_on_parent(EditorInterface.get_script_editor(), get_center_editor_rect())
558558+559559+func hide_scripts_popup():
560560+ if (scripts_popup != null && scripts_popup.visible):
561561+ scripts_popup.hide.call_deferred()
562562+563563+func create_set_scripts_popup():
564564+ panel_container = scripts_item_list.get_parent().get_parent()
565565+566566+ scripts_popup = PopupPanel.new()
567567+ scripts_popup.popup_hide.connect(restore_scripts_list)
568568+569569+ # Need to be inside the tree, so it can be shown as popup for the tab container.
570570+ var script_editor: ScriptEditor = EditorInterface.get_script_editor()
571571+ script_editor.add_child(scripts_popup)
572572+573573+ scripts_tab_container.pre_popup_pressed.connect(prepare_scripts_popup)
574574+ scripts_tab_container.set_popup(scripts_popup)
575575+576576+func prepare_scripts_popup():
577577+ scripts_popup.size.x = outline.size.x
578578+ scripts_popup.size.y = panel_container.size.y - scripts_tab_bar.size.y
579579+580580+ scripts_item_list.get_parent().reparent(scripts_popup)
581581+ scripts_item_list.get_parent().visible = true
582582+583583+ script_filter_txt.grab_focus()
584584+585585+func restore_scripts_list():
586586+ script_filter_txt.text = &""
587587+588588+ update_script_list_visibility()
589589+590590+ scripts_item_list.get_parent().reparent(panel_container)
591591+ panel_container.move_child(scripts_item_list.get_parent(), 0)
592592+593593+func navigate_on_list(event: InputEvent, list: ItemList, submit: Callable):
594594+ if (event.is_action_pressed(&"ui_text_submit")):
595595+ var index: int = get_list_index(list)
596596+ if (index == -1):
597597+ return
598598+599599+ submit.call(index)
600600+ elif (event.is_action_pressed(&"ui_down", true)):
601601+ var index: int = get_list_index(list)
602602+ if (index == list.item_count - 1):
603603+ return
604604+605605+ navigate_list(list, index, 1)
606606+ elif (event.is_action_pressed(&"ui_up", true)):
607607+ var index: int = get_list_index(list)
608608+ if (index <= 0):
609609+ return
610610+611611+ navigate_list(list, index, -1)
612612+ elif (event.is_action_pressed(&"ui_page_down", true)):
613613+ var index: int = get_list_index(list)
614614+ if (index == list.item_count - 1):
615615+ return
616616+617617+ navigate_list(list, index, 5)
618618+ elif (event.is_action_pressed(&"ui_page_up", true)):
619619+ var index: int = get_list_index(list)
620620+ if (index <= 0):
621621+ return
622622+623623+ navigate_list(list, index, -5)
624624+ elif (event is InputEventKey && list.item_count > 0 && !list.is_anything_selected()):
625625+ list.select(0)
626626+627627+func get_list_index(list: ItemList) -> int:
628628+ var items: PackedInt32Array = list.get_selected_items()
629629+630630+ if (items.is_empty()):
631631+ return -1
632632+633633+ return items[0]
634634+635635+func navigate_list(list: ItemList, index: int, amount: int):
636636+ index = clamp(index + amount, 0, list.item_count - 1)
637637+638638+ list.select(index)
639639+ list.ensure_current_is_visible()
640640+ list.accept_event()
641641+642642+func get_center_editor_rect() -> Rect2i:
643643+ var script_editor: ScriptEditor = EditorInterface.get_script_editor()
644644+645645+ var size: Vector2i = Vector2i(400, 500)
646646+ var x: int
647647+ var y: int
648648+649649+ if (script_editor.get_parent().get_parent() is Window):
650650+ # Floating editor.
651651+ var window: Window = script_editor.get_parent().get_parent()
652652+ var window_rect: Rect2 = window.get_visible_rect()
653653+654654+ x = window_rect.size.x / 2 - size.x / 2
655655+ y = window_rect.size.y / 2 - size.y / 2
656656+ else:
657657+ x = script_editor.global_position.x + script_editor.size.x / 2 - size.x / 2
658658+ y = script_editor.global_position.y + script_editor.size.y / 2 - size.y / 2
659659+660660+ return Rect2i(Vector2i(x, y), size)
661661+662662+func open_outline_popup():
663663+ var button_flags: Array[bool] = []
664664+ for child: Node in filter_box.get_children():
665665+ var btn: Button = child
666666+ button_flags.append(btn.button_pressed)
667667+668668+ btn.set_pressed_no_signal(true)
669669+670670+ var old_text: String = outline_filter_txt.text
671671+ outline_filter_txt.text = &""
672672+673673+ if (outline_popup == null):
674674+ outline_popup = PopupPanel.new()
675675+676676+ var outline_initially_closed: bool = !outline_container.visible
677677+ if (outline_initially_closed):
678678+ outline_container.visible = true
679679+680680+ outline_container.reparent(outline_popup)
681681+682682+ outline_popup.popup_hide.connect(on_outline_popup_hidden.bind(outline_initially_closed, old_text, button_flags))
683683+684684+ if (outline_popup.get_parent() != null):
685685+ outline_popup.get_parent().remove_child(outline_popup)
686686+ outline_popup.popup_exclusive_on_parent(EditorInterface.get_script_editor(), get_center_editor_rect())
687687+688688+ update_outline()
689689+ outline_filter_txt.grab_focus()
690690+691691+func on_outline_popup_hidden(outline_initially_closed: bool, old_text: String, button_flags: Array[bool]):
692692+ outline_popup.popup_hide.disconnect(on_outline_popup_hidden)
693693+694694+ if outline_initially_closed:
695695+ outline_container.visible = false
696696+697697+ outline_container.reparent(split_container)
698698+ if (!is_outline_right):
699699+ split_container.move_child(outline_container, 0)
700700+701701+ outline_filter_txt.text = old_text
702702+703703+ var index: int = 0
704704+ for flag: bool in button_flags:
705705+ var btn: Button = filter_box.get_child(index)
706706+ btn.set_pressed_no_signal(flag)
707707+ index += 1
708708+709709+ update_outline()
710710+711711+func open_scripts_popup():
712712+ scripts_item_list.get_parent().reparent(scripts_popup)
713713+ scripts_item_list.get_parent().visible = true
714714+715715+ if (scripts_popup.get_parent() != null):
716716+ scripts_popup.get_parent().remove_child(scripts_popup)
717717+ scripts_popup.popup_exclusive_on_parent(EditorInterface.get_script_editor(), get_center_editor_rect())
718718+719719+ script_filter_txt.grab_focus()
720720+721721+## Removes the script filter text and emits the signal so that the tabs stay
722722+## and we do not break anything there.
723723+func update_script_text_filter():
724724+ if (script_filter_txt.text != &""):
725725+ script_filter_txt.text = &""
726726+ script_filter_txt.text_changed.emit(&"")
727727+728728+func get_current_script() -> Script:
729729+ var script_editor: ScriptEditor = EditorInterface.get_script_editor()
730730+ return script_editor.get_current_script()
731731+732732+func select_script(selected_idx: int):
733733+ hide_scripts_popup()
734734+735735+ scripts_item_list.item_selected.emit(selected_idx)
736736+737737+func scroll_outline(selected_idx: int):
738738+ if (outline_popup != null && outline_popup.visible):
739739+ outline_popup.hide.call_deferred()
740740+741741+ var script: Script = get_current_script()
742742+ if (!script):
743743+ return
744744+745745+ var text: String = outline.get_item_text(selected_idx)
746746+ var metadata: Dictionary = outline.get_item_metadata(selected_idx)
747747+ var modifier: StringName = metadata[&"modifier"]
748748+ var type: StringName = metadata[&"type"]
749749+750750+ var type_with_text: String = type + " " + text
751751+ if (type == &"func"):
752752+ type_with_text = type_with_text + "("
753753+754754+ var source_code: String = script.get_source_code()
755755+ var lines: PackedStringArray = source_code.split("\n")
756756+757757+ var index: int = 0
758758+ for line: String in lines:
759759+ # Easy case, like 'var abc'
760760+ if (line.begins_with(type_with_text)):
761761+ goto_line(index)
762762+ return
763763+764764+ # We have an modifier, e.g. 'static'
765765+ if (modifier != &"" && line.begins_with(modifier)):
766766+ if (line.begins_with(modifier + " " + type_with_text)):
767767+ goto_line(index)
768768+ return
769769+ # Special case: An 'enum' is treated different.
770770+ elif (modifier == &"enum" && line.contains("enum " + text)):
771771+ goto_line(index)
772772+ return
773773+774774+ # Hard case, probably something like '@onready var abc'
775775+ if (type == &"var" && line.contains(type_with_text)):
776776+ goto_line(index)
777777+ return
778778+779779+ index += 1
780780+781781+ push_error(type_with_text + " or " + modifier + " not found in source code")
782782+783783+func goto_line(index: int):
784784+ var script_editor: ScriptEditor = EditorInterface.get_script_editor()
785785+ script_editor.goto_line(index)
786786+787787+ var code_edit: CodeEdit = script_editor.get_current_editor().get_base_editor()
788788+ code_edit.set_caret_line(index)
789789+ code_edit.set_v_scroll(index)
790790+ code_edit.set_caret_column(code_edit.get_line(index).length())
791791+ code_edit.set_h_scroll(0)
792792+793793+ code_edit.grab_focus()
794794+795795+func create_filter_btn(icon: Texture2D, title: StringName) -> Button:
796796+ var btn: Button = Button.new()
797797+ btn.toggle_mode = true
798798+ btn.icon = icon
799799+ btn.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
800800+ btn.tooltip_text = title
801801+802802+ var property: StringName = SCRIPT_IDE + title.to_lower().replace(" ", "_")
803803+ btn.set_meta(&"property", property)
804804+ btn.button_pressed = get_setting(property, true)
805805+806806+ btn.toggled.connect(on_filter_button_pressed.bind(btn))
807807+ btn.gui_input.connect(on_right_click.bind(btn))
808808+809809+ btn.add_theme_color_override(&"icon_pressed_color", Color.WHITE)
810810+ btn.add_theme_color_override(&"icon_hover_color", Color.WHITE)
811811+ btn.add_theme_color_override(&"icon_hover_pressed_color", Color.WHITE)
812812+ btn.add_theme_color_override(&"icon_focus_color", Color.WHITE)
813813+814814+ var style_box_empty: StyleBoxEmpty = StyleBoxEmpty.new()
815815+ btn.add_theme_stylebox_override(&"normal", style_box_empty)
816816+817817+ var style_box: StyleBoxFlat = StyleBoxFlat.new()
818818+ style_box.draw_center = false
819819+ style_box.border_color = get_editor_accent_color()
820820+ style_box.set_border_width_all(1 * get_editor_scale())
821821+ style_box.set_corner_radius_all(get_editor_corner_radius() * get_editor_scale())
822822+ btn.add_theme_stylebox_override(&"focus", style_box)
823823+824824+ return btn
825825+826826+func on_right_click(event: InputEvent, btn: Button):
827827+ if !(event is InputEventMouseButton):
828828+ return
829829+830830+ var mouse_event: InputEventMouseButton = event
831831+832832+ if (!mouse_event.is_pressed() || mouse_event.button_index != MOUSE_BUTTON_RIGHT):
833833+ return
834834+835835+ btn.button_pressed = true
836836+837837+ var pressed_state: bool = false
838838+ for child: Node in filter_box.get_children():
839839+ var other_btn: Button = child
840840+841841+ if (btn != other_btn):
842842+ pressed_state = pressed_state || other_btn.button_pressed
843843+844844+ for child: Node in filter_box.get_children():
845845+ var other_btn: Button = child
846846+847847+ if (btn != other_btn):
848848+ other_btn.button_pressed = !pressed_state
849849+850850+ outline_filter_txt.grab_focus()
851851+852852+func on_filter_button_pressed(pressed: bool, btn: Button):
853853+ set_setting(btn.get_meta(&"property"), pressed)
854854+855855+ update_outline()
856856+ outline_filter_txt.grab_focus()
857857+858858+func update_outline_position():
859859+ if (is_outline_right):
860860+ # Try to restore the previous split offset.
861861+ var split_offset: float = split_container.get_child(1).size.x
862862+ split_container.split_offset = split_offset
863863+ split_container.move_child(outline_container, 1)
864864+ else:
865865+ split_container.move_child(outline_container, 0)
866866+867867+func update_script_list_visibility():
868868+ scripts_item_list.get_parent().visible = is_script_list_visible
869869+870870+func create_editor_texture(texture: Texture2D) -> Texture2D:
871871+ var image: Image = texture.get_image().duplicate()
872872+ image.adjust_bcs(1.0, 1.0, get_editor_icon_saturation())
873873+874874+ return ImageTexture.create_from_image(image)
875875+876876+func sync_settings():
877877+ if (suppress_settings_sync):
878878+ return
879879+880880+ var changed_settings: PackedStringArray = get_editor_settings().get_changed_settings()
881881+ for setting: String in changed_settings:
882882+ if (setting == "interface/theme/icon_saturation"):
883883+ init_icons()
884884+ engine_func_btn.icon = engine_func_icon
885885+ func_btn.icon = func_icon
886886+ signal_btn.icon = signal_icon
887887+ export_btn.icon = export_icon
888888+ property_btn.icon = property_icon
889889+ class_btn.icon = class_icon
890890+ constant_btn.icon = constant_icon
891891+ update_outline()
892892+ continue
893893+894894+ if (!setting.begins_with(SCRIPT_IDE)):
895895+ continue
896896+897897+ if (setting == OUTLINE_POSITION_RIGHT):
898898+ var new_outline_right: bool = get_setting(OUTLINE_POSITION_RIGHT, is_outline_right)
899899+ if (new_outline_right != is_outline_right):
900900+ is_outline_right = new_outline_right
901901+902902+ update_outline_position()
903903+ elif (setting == OUTLINE_ORDER):
904904+ update_outline_order()
905905+ update_outline_button_order()
906906+ update_outline()
907907+ elif (setting == HIDE_PRIVATE_MEMBERS):
908908+ var new_hide_private_members: bool = get_setting(HIDE_PRIVATE_MEMBERS, hide_private_members)
909909+ if (new_hide_private_members != hide_private_members):
910910+ hide_private_members = new_hide_private_members
911911+912912+ update_outline_cache()
913913+ update_outline()
914914+ elif (setting == SCRIPT_LIST_VISIBLE):
915915+ var new_script_list_visible: bool = get_setting(SCRIPT_LIST_VISIBLE, is_script_list_visible)
916916+ if (new_script_list_visible != is_script_list_visible):
917917+ is_script_list_visible = new_script_list_visible
918918+919919+ update_script_list_visibility()
920920+ elif (setting == SCRIPT_TABS_VISIBLE):
921921+ var new_script_tabs_visible: bool = get_setting(SCRIPT_TABS_VISIBLE, is_script_tabs_visible)
922922+ if (new_script_tabs_visible != is_script_tabs_visible):
923923+ is_script_tabs_visible = new_script_tabs_visible
924924+925925+ scripts_tab_container.tabs_visible = is_script_tabs_visible
926926+ elif (setting == SCRIPT_TAB_POSITION_TOP):
927927+ var new_script_tabs_top: bool = get_setting(SCRIPT_TAB_POSITION_TOP, is_script_tabs_top)
928928+ if (new_script_tabs_top != is_script_tabs_top):
929929+ is_script_tabs_top = new_script_tabs_top
930930+931931+ update_tabs_position()
932932+ elif (setting == AUTO_NAVIGATE_IN_FS):
933933+ is_auto_navigate_in_fs = get_setting(AUTO_NAVIGATE_IN_FS, is_auto_navigate_in_fs)
934934+ elif (setting == OPEN_OUTLINE_POPUP):
935935+ open_outline_popup_shc = get_shortcut(OPEN_OUTLINE_POPUP)
936936+ elif (setting == OPEN_SCRIPTS_POPUP):
937937+ open_scripts_popup_shc = get_shortcut(OPEN_SCRIPTS_POPUP)
938938+ elif (setting == TAB_CYCLE_FORWARD):
939939+ tab_cycle_forward_shc = get_shortcut(TAB_CYCLE_FORWARD)
940940+ elif (setting == TAB_CYCLE_BACKWARD):
941941+ tab_cycle_backward_shc = get_shortcut(TAB_CYCLE_BACKWARD)
942942+ else:
943943+ # Update filter buttons.
944944+ for btn_node: Node in filter_box.get_children():
945945+ var btn: Button = btn_node
946946+ var property: StringName = btn.get_meta(&"property")
947947+948948+ btn.button_pressed = get_setting(property, btn.button_pressed)
949949+950950+func get_setting(property: StringName, alt: bool) -> bool:
951951+ var editor_settings: EditorSettings = get_editor_settings()
952952+ if (editor_settings.has_setting(property)):
953953+ return editor_settings.get_setting(property)
954954+ else:
955955+ editor_settings.set_setting(property, alt)
956956+ return alt
957957+958958+func set_setting(property: StringName, value: bool):
959959+ var editor_settings: EditorSettings = get_editor_settings()
960960+961961+ suppress_settings_sync = true
962962+ editor_settings.set_setting(property, value)
963963+ suppress_settings_sync = false
964964+965965+func get_shortcut(property: StringName) -> Shortcut:
966966+ return get_editor_settings().get_setting(property)
967967+968968+func on_tab_changed(index: int):
969969+ selected_tab = index;
970970+971971+ if (old_script_editor_base != null):
972972+ old_script_editor_base.edited_script_changed.disconnect(update_selected_tab)
973973+ old_script_editor_base = null
974974+975975+ var script_editor: ScriptEditor = EditorInterface.get_script_editor()
976976+ var script_editor_base: ScriptEditorBase = script_editor.get_current_editor()
977977+978978+ if (script_editor_base != null):
979979+ script_editor_base.edited_script_changed.connect(update_selected_tab)
980980+981981+ old_script_editor_base = script_editor_base
982982+983983+ sync_script_list = true
984984+985985+ if (is_auto_navigate_in_fs && script_editor.get_current_script() != null):
986986+ var file: String = script_editor.get_current_script().get_path()
987987+988988+ if (file.contains(BUILT_IN_SCRIPT)):
989989+ # We navigate to the scene in case of a built-in script.
990990+ file = file.get_slice(BUILT_IN_SCRIPT, 0)
991991+992992+ file_to_navigate = file
993993+ else:
994994+ file_to_navigate = &""
995995+996996+ schedule_update()
997997+998998+func update_selected_tab():
999999+ if (selected_tab == -1):
10001000+ return
10011001+10021002+ if (scripts_item_list.item_count == 0):
10031003+ return
10041004+10051005+ update_tab(selected_tab)
10061006+10071007+func update_tabs():
10081008+ for index: int in scripts_tab_container.get_tab_count():
10091009+ update_tab(index)
10101010+10111011+func update_tab(index: int):
10121012+ scripts_tab_container.set_tab_title(index, scripts_item_list.get_item_text(index))
10131013+ scripts_tab_container.set_tab_icon(index, scripts_item_list.get_item_icon(index))
10141014+ scripts_tab_container.set_tab_tooltip(index, scripts_item_list.get_item_tooltip(index))
10151015+10161016+func update_tabs_position():
10171017+ if (is_script_tabs_top):
10181018+ scripts_tab_container.tabs_position = TabContainer.POSITION_TOP
10191019+ else:
10201020+ scripts_tab_container.tabs_position = TabContainer.POSITION_BOTTOM
10211021+10221022+func update_keywords(script: Script):
10231023+ if (script == null):
10241024+ return
10251025+10261026+ var new_script_type: StringName = script.get_instance_base_type()
10271027+ if (old_script_type != new_script_type):
10281028+ old_script_type = new_script_type
10291029+10301030+ keywords.clear()
10311031+ keywords["_static_init"] = 0
10321032+ register_virtual_methods(new_script_type)
10331033+10341034+func register_virtual_methods(clazz: String):
10351035+ for method: Dictionary in ClassDB.class_get_method_list(clazz):
10361036+ if method.flags & METHOD_FLAG_VIRTUAL > 0:
10371037+ keywords[method.name] = 0
10381038+10391039+func update_outline_cache():
10401040+ outline_cache = null
10411041+10421042+ var script: Script = get_current_script()
10431043+ if (!script):
10441044+ return
10451045+10461046+ update_keywords(script)
10471047+10481048+ # Check if built-in script. In this case we need to duplicate it for whatever reason.
10491049+ if (script.get_path().contains(BUILT_IN_SCRIPT)):
10501050+ script = script.duplicate()
10511051+10521052+ outline_cache = OutlineCache.new()
10531053+10541054+ # Collect all script members.
10551055+ for_each_script_member(script, func(array: Array[String], item: String): array.append(item))
10561056+10571057+ # Remove script members that only exist in the base script (which includes the base of the base etc.).
10581058+ # Note: The method that only collects script members without including the base script(s)
10591059+ # is not exposed to GDScript.
10601060+ var base_script: Script = script.get_base_script()
10611061+ if (base_script != null):
10621062+ for_each_script_member(base_script, func(array: Array[String], item: String): array.erase(item))
10631063+10641064+func for_each_script_member(script: Script, consumer: Callable):
10651065+ # Functions / Methods
10661066+ for dict: Dictionary in script.get_script_method_list():
10671067+ var func_name: String = dict[&"name"]
10681068+10691069+ if (keywords.has(func_name)):
10701070+ consumer.call(outline_cache.engine_funcs, func_name)
10711071+ else:
10721072+ if hide_private_members && func_name.begins_with(UNDERSCORE):
10731073+ continue
10741074+10751075+ # Inline getter/setter will normally be shown as '@...getter', '@...setter'.
10761076+ # Since we already show the variable itself, we will skip those.
10771077+ if (func_name.begins_with(INLINE)):
10781078+ continue
10791079+10801080+ consumer.call(outline_cache.funcs, func_name)
10811081+10821082+ # Properties / Exported variables
10831083+ for dict: Dictionary in script.get_script_property_list():
10841084+ var property: String = dict[&"name"]
10851085+ if hide_private_members && property.begins_with(UNDERSCORE):
10861086+ continue
10871087+10881088+ var usage: int = dict[&"usage"]
10891089+10901090+ if (usage & PROPERTY_USAGE_SCRIPT_VARIABLE):
10911091+ if (usage & PROPERTY_USAGE_STORAGE && usage & PROPERTY_USAGE_EDITOR):
10921092+ consumer.call(outline_cache.exports, property)
10931093+ else:
10941094+ consumer.call(outline_cache.properties, property)
10951095+10961096+ # Static variables (are separated for whatever reason)
10971097+ for dict: Dictionary in script.get_property_list():
10981098+ var property: String = dict[&"name"]
10991099+ if hide_private_members && property.begins_with(UNDERSCORE):
11001100+ continue
11011101+11021102+ var usage: int = dict[&"usage"]
11031103+11041104+ if (usage & PROPERTY_USAGE_SCRIPT_VARIABLE):
11051105+ consumer.call(outline_cache.properties, property)
11061106+11071107+ # Signals
11081108+ for dict: Dictionary in script.get_script_signal_list():
11091109+ var signal_name: String = dict[&"name"]
11101110+11111111+ consumer.call(outline_cache.signals, signal_name)
11121112+11131113+ # Constants / Classes
11141114+ for name_key: String in script.get_script_constant_map():
11151115+ if hide_private_members && name_key.begins_with(UNDERSCORE):
11161116+ continue
11171117+11181118+ var object: Variant = script.get_script_constant_map().get(name_key)
11191119+ # Inner classes have no source code, while a const of type GDScript has.
11201120+ if (object is GDScript && !object.has_source_code()):
11211121+ consumer.call(outline_cache.classes, name_key)
11221122+ else:
11231123+ consumer.call(outline_cache.constants, name_key)
11241124+11251125+func update_outline():
11261126+ outline.clear()
11271127+11281128+ if (outline_cache == null):
11291129+ return
11301130+11311131+ for outline_type: OutlineType in outline_type_order:
11321132+ outline_type.add_to_outline.call()
11331133+11341134+func add_to_outline(items: Array[String], icon: Texture2D, type: String, modifier: StringName = &""):
11351135+ add_to_outline_ext(items, func(str: String): return icon, type, modifier)
11361136+11371137+func add_to_outline_ext(items: Array[String], icon_callable: Callable, type: String, modifier: StringName = &""):
11381138+ var text: String = outline_filter_txt.get_text()
11391139+11401140+ if (is_sorted()):
11411141+ items = items.duplicate()
11421142+ items.sort_custom(func(str1: String, str2: String): return str1.naturalnocasecmp_to(str2) < 0)
11431143+11441144+ for item: String in items:
11451145+ if (text.is_empty() || text.is_subsequence_ofn(item)):
11461146+ var icon: Texture2D = icon_callable.call(item)
11471147+ outline.add_item(item, icon, true)
11481148+11491149+ var dict: Dictionary = {
11501150+ &"type": type,
11511151+ &"modifier": modifier
11521152+ }
11531153+ outline.set_item_metadata(outline.item_count - 1, dict)
11541154+11551155+func get_func_icon(func_name: String) -> Texture2D:
11561156+ var icon: Texture2D = func_icon
11571157+ if (func_name.begins_with(GETTER)):
11581158+ icon = func_get_icon
11591159+ elif (func_name.begins_with(SETTER)):
11601160+ icon = func_set_icon
11611161+11621162+ return icon
11631163+11641164+func sync_tab_with_script_list():
11651165+ # For some reason the selected tab is wrong. Looks like a Godot bug.
11661166+ if (selected_tab >= scripts_item_list.item_count):
11671167+ selected_tab = scripts_tab_bar.current_tab
11681168+11691169+ # Hide filter and outline for non .gd scripts.
11701170+ var is_script: bool = get_current_script() != null
11711171+ filter_box.visible = is_script
11721172+ outline.visible = is_script
11731173+11741174+ # Sync with script item list.
11751175+ if (selected_tab != -1 && scripts_item_list.item_count > 0 && !scripts_item_list.is_selected(selected_tab)):
11761176+ scripts_item_list.select(selected_tab)
11771177+ scripts_item_list.item_selected.emit(selected_tab)
11781178+11791179+ scripts_item_list.ensure_current_is_visible()
11801180+11811181+func on_tab_bar_mouse_exited():
11821182+ last_tab_hovered = -1
11831183+11841184+func on_tab_hovered(idx: int):
11851185+ last_tab_hovered = idx
11861186+11871187+func on_tab_bar_gui_input(event: InputEvent):
11881188+ if (last_tab_hovered == -1):
11891189+ return
11901190+11911191+ if (event is InputEventMouseButton):
11921192+ if event.is_pressed() and event.button_index == MOUSE_BUTTON_MIDDLE:
11931193+ update_script_text_filter()
11941194+ simulate_item_clicked(last_tab_hovered, MOUSE_BUTTON_MIDDLE)
11951195+11961196+ if (last_tab_hovered >= scripts_tab_bar.tab_count - 1):
11971197+ last_tab_hovered = -1
11981198+11991199+func on_active_tab_rearranged(idx_to: int):
12001200+ var control: Control = scripts_tab_container.get_tab_control(selected_tab)
12011201+ if (!control):
12021202+ return
12031203+12041204+ scripts_tab_container.move_child(control, idx_to)
12051205+ scripts_tab_container.current_tab = scripts_tab_container.current_tab
12061206+ selected_tab = scripts_tab_container.current_tab
12071207+12081208+func get_res_path(idx: int) -> String:
12091209+ var tab_control: Control = scripts_tab_container.get_tab_control(idx)
12101210+ if (tab_control == null):
12111211+ return ''
12121212+12131213+ var path_var: Variant = tab_control.get(&"metadata/_edit_res_path")
12141214+ if (path_var == null):
12151215+ return ''
12161216+12171217+ return path_var
12181218+12191219+func on_tab_rmb(tab_idx: int):
12201220+ update_script_text_filter()
12211221+ simulate_item_clicked(tab_idx, MOUSE_BUTTON_RIGHT)
12221222+12231223+func on_tab_close(tab_idx: int):
12241224+ update_script_text_filter()
12251225+ simulate_item_clicked(tab_idx, MOUSE_BUTTON_MIDDLE)
12261226+12271227+func simulate_item_clicked(tab_idx: int, mouse_idx: int):
12281228+ scripts_item_list.item_clicked.emit(tab_idx, scripts_item_list.get_local_mouse_position(), mouse_idx)
12291229+12301230+func get_editor_scale() -> float:
12311231+ return EditorInterface.get_editor_scale()
12321232+12331233+func get_editor_corner_radius() -> int:
12341234+ return EditorInterface.get_editor_settings().get_setting("interface/theme/corner_radius")
12351235+12361236+func get_editor_accent_color() -> Color:
12371237+ return EditorInterface.get_editor_settings().get_setting("interface/theme/accent_color")
12381238+12391239+func get_editor_icon_saturation() -> float:
12401240+ return EditorInterface.get_editor_settings().get_setting("interface/theme/icon_saturation")
12411241+12421242+func is_sorted() -> bool:
12431243+ return get_editor_settings().get_setting("text_editor/script_list/sort_members_outline_alphabetically")
12441244+12451245+func get_editor_settings() -> EditorSettings:
12461246+ return EditorInterface.get_editor_settings()
12471247+12481248+func load_rel(path: String) -> Variant:
12491249+ var script_path: String = get_script().get_path().get_base_dir()
12501250+ return load(script_path.path_join(path))
12511251+12521252+static func find_or_null(arr: Array[Node], index: int = 0) -> Node:
12531253+ if (arr.is_empty()):
12541254+ push_error("""Node that is needed for Script-IDE not found.
12551255+Plugin will not work correctly.
12561256+This might be due to some other plugins or changes in the Engine.
12571257+Please report this to Script-IDE, so we can figure out a fix.""")
12581258+ return null
12591259+ return arr[index]
12601260+12611261+## Cache for everything inside we collected to show in the Outline.
12621262+class OutlineCache:
12631263+ var classes: Array[String] = []
12641264+ var constants: Array[String] = []
12651265+ var signals: Array[String] = []
12661266+ var exports: Array[String] = []
12671267+ var properties: Array[String] = []
12681268+ var funcs: Array[String] = []
12691269+ var engine_funcs: Array[String] = []
12701270+12711271+## Outline type for a concrete button with their items in the Outline.
12721272+class OutlineType:
12731273+ var type_name: StringName
12741274+ var add_to_outline: Callable
12751275+12761276+## Contains everything we modify on the Tab Control. Used to save and restore the behaviour
12771277+## to keep the Engine in a clean state when the plugin is disabled.
12781278+class TabStateCache:
12791279+ var tabs_visible: bool
12801280+ var drag_to_rearrange_enabled: bool
12811281+ var auto_translate_mode_state: Node.AutoTranslateMode
12821282+ var tab_bar_drag_to_rearrange_enabled: bool
12831283+ var tab_close_display_policy: TabBar.CloseButtonDisplayPolicy
12841284+ var select_with_rmb: bool
12851285+12861286+ func save(tab_container: TabContainer, tab_bar: TabBar):
12871287+ if (tab_container != null):
12881288+ tabs_visible = tab_container.tabs_visible
12891289+ drag_to_rearrange_enabled = tab_container.drag_to_rearrange_enabled
12901290+ auto_translate_mode_state = tab_container.auto_translate_mode
12911291+ if (tab_bar != null):
12921292+ tab_bar_drag_to_rearrange_enabled = tab_bar.drag_to_rearrange_enabled
12931293+ tab_close_display_policy = tab_bar.tab_close_display_policy
12941294+ select_with_rmb = tab_bar.select_with_rmb
12951295+12961296+ func restore(tab_container: TabContainer, tab_bar: TabBar):
12971297+ if (tab_container != null):
12981298+ tab_container.tabs_visible = tabs_visible
12991299+ tab_container.drag_to_rearrange_enabled = drag_to_rearrange_enabled
13001300+ tab_container.auto_translate_mode = auto_translate_mode_state
13011301+ if (tab_bar != null):
13021302+ tab_bar.drag_to_rearrange_enabled = drag_to_rearrange_enabled
13031303+ tab_bar.tab_close_display_policy = tab_close_display_policy
13041304+ tab_bar.select_with_rmb = select_with_rmb