A Quadrilateral Cowboy clone intended to help me learn Game Dev
1## Copyright (c) 2023-present Marius Hanl under the MIT License.
2## The editor plugin entrypoint for Script-IDE.
3##
4## Some features interfere with the editor code that is inside 'script_editor_plugin.cpp'.
5## That is, the original structure is changed by this plugin, in order to support everything.
6## The internals of the native C++ code are therefore important in order to make this plugin work
7## without interfering with the Engine itself (and their Node's).
8##
9## Script-IDE does not use global class_name's in order to not clutter projects using it.
10## Especially since this is an editor only plugin, we do not want this plugin in the final game.
11## Therefore, components that references the plugin is untyped.
12@tool
13extends EditorPlugin
14
15const BUILT_IN_SCRIPT: StringName = &"::GDScript"
16const QUICK_OPEN_INTERVAL: int = 400
17
18const MULTILINE_TAB_BAR: PackedScene = preload("tabbar/multiline_tab_bar.tscn")
19const MultilineTabBar := preload("tabbar/multiline_tab_bar.gd")
20
21const QUICK_OPEN_SCENE: PackedScene = preload("quickopen/quick_open_panel.tscn")
22const QuickOpenPopup := preload("quickopen/quick_open_panel.gd")
23
24const OVERRIDE_SCENE: PackedScene = preload("override/override_panel.tscn")
25const OverridePopup := preload("override/override_panel.gd")
26
27const Outline := preload("uid://db0be00ai3tfi")
28const SplitCodeEdit := preload("uid://boy48rhhyrph")
29
30#region Settings and Shortcuts
31## Editor setting path
32const SCRIPT_IDE: StringName = &"plugin/script_ide/"
33## Editor setting for the outline position
34const OUTLINE_POSITION_RIGHT: StringName = SCRIPT_IDE + &"outline_position_right"
35## Editor setting to control the order of the outline
36const OUTLINE_ORDER: StringName = SCRIPT_IDE + &"outline_order"
37## Editor setting to control whether private members (annotated with '_' should be hidden or not)
38const HIDE_PRIVATE_MEMBERS: StringName = SCRIPT_IDE + &"hide_private_members"
39## Editor setting to control whether we want to auto navigate to the script
40## in the filesystem (dock) when selected
41const AUTO_NAVIGATE_IN_FS: StringName = SCRIPT_IDE + &"auto_navigate_in_filesystem_dock"
42## Editor setting to control whether the script list should be visible or not
43const SCRIPT_LIST_VISIBLE: StringName = SCRIPT_IDE + &"script_list_visible"
44## Editor setting to control whether the script tabs should be visible or not.
45const SCRIPT_TABS_VISIBLE: StringName = SCRIPT_IDE + &"script_tabs_visible"
46## Editor setting to control where the script tabs should be.
47const SCRIPT_TABS_POSITION_TOP: StringName = SCRIPT_IDE + &"script_tabs_position_top"
48## Editor setting to control if all script tabs should have close button.
49const SCRIPT_TABS_CLOSE_BUTTON_ALWAYS: StringName = SCRIPT_IDE + &"script_tabs_close_button_always"
50## Editor setting to control if all tabs should be shown in a single line.
51const SCRIPT_TABS_SINGLELINE: StringName = SCRIPT_IDE + &"script_tabs_singleline"
52
53## Editor setting for the 'Open Outline Popup' shortcut
54const OPEN_OUTLINE_POPUP: StringName = SCRIPT_IDE + &"open_outline_popup"
55## Editor setting for the 'Open Scripts Popup' shortcut
56const OPEN_SCRIPTS_POPUP: StringName = SCRIPT_IDE + &"open_scripts_popup"
57## Editor setting for the 'Open Scripts Popup' shortcut
58const OPEN_QUICK_SEARCH_POPUP: StringName = SCRIPT_IDE + &"open_quick_search_popup"
59## Editor setting for the 'Open Override Popup' shortcut
60const OPEN_OVERRIDE_POPUP: StringName = SCRIPT_IDE + &"open_override_popup"
61## Editor setting for the 'Tab cycle forward' shortcut
62const TAB_CYCLE_FORWARD: StringName = SCRIPT_IDE + &"tab_cycle_forward"
63## Editor setting for the 'Tab cycle backward' shortcut
64const TAB_CYCLE_BACKWARD: StringName = SCRIPT_IDE + &"tab_cycle_backward"
65
66## Engine editor setting for the icon saturation, so our icons can react.
67const ICON_SATURATION: StringName = &"interface/theme/icon_saturation"
68## Engine editor setting for the show members functionality.
69const SHOW_MEMBERS: StringName = &"text_editor/script_list/show_members_overview"
70## We track the user setting, so we can restore it properly.
71var show_members: bool = true
72#endregion
73
74#region Editor settings
75var is_outline_right: bool = true
76var is_hide_private_members: bool = false
77
78var is_script_tabs_visible: bool = true
79var is_script_tabs_top: bool = true
80var is_script_tabs_close_button_always: bool = false
81var is_script_tabs_singleline: bool = false
82
83var is_auto_navigate_in_fs: bool = true
84var is_script_list_visible: bool = false
85
86var outline_order: PackedStringArray
87
88var open_outline_popup_shc: Shortcut
89var open_scripts_popup_shc: Shortcut
90var open_quick_search_popup_shc: Shortcut
91var open_override_popup_shc: Shortcut
92var tab_cycle_forward_shc: Shortcut
93var tab_cycle_backward_shc: Shortcut
94#endregion
95
96#region Existing controls we modify
97var script_editor_split_container: HSplitContainer
98var files_panel: Control
99
100var old_scripts_tab_container: TabContainer
101var old_scripts_tab_bar: TabBar
102
103var script_filter_txt: LineEdit
104var scripts_item_list: ItemList
105var script_panel_split_container: VSplitContainer
106
107var old_outline: ItemList
108var outline_filter_txt: LineEdit
109var sort_btn: Button
110#endregion
111
112#region Own controls we add
113var outline: Outline
114var outline_popup: PopupPanel
115var multiline_tab_bar: MultilineTabBar
116var scripts_popup: PopupPanel
117var quick_open_popup: QuickOpenPopup
118var override_popup: OverridePopup
119
120var tab_splitter: HSplitContainer
121#endregion
122
123#region Plugin variables
124var keywords: Dictionary[String, bool] = {} # Used as Set.
125
126var old_script_editor_base: ScriptEditorBase
127var old_script_type: StringName
128
129var is_script_changed: bool = false
130var file_to_navigate: String = &""
131
132var quick_open_tween: Tween
133
134var suppress_settings_sync: bool = false
135#endregion
136
137#region Plugin Enter / Exit setup
138## Change the Engine script UI and transform into an IDE like UI
139func _enter_tree() -> void:
140 init_settings()
141 init_shortcuts()
142
143 # Update on filesystem changed (e.g. save operation).
144 var file_system: EditorFileSystem = EditorInterface.get_resource_filesystem()
145 file_system.filesystem_changed.connect(schedule_update)
146
147 # Sync settings changes for this plugin.
148 get_editor_settings().settings_changed.connect(sync_settings)
149
150 var script_editor: ScriptEditor = EditorInterface.get_script_editor()
151 script_editor_split_container = find_or_null(script_editor.find_children("*", "HSplitContainer", true, false))
152 files_panel = script_editor_split_container.get_child(0)
153
154 # The 'Filter Scripts' Panel
155 var upper_files_panel: Control = files_panel.get_child(0)
156 # The 'Filter Methods' Panel
157 var lower_files_panel: Control = files_panel.get_child(1)
158
159 # Change script item list visibility (based on settings).
160 scripts_item_list = find_or_null(upper_files_panel.find_children("*", "ItemList", true, false))
161 scripts_item_list.allow_reselect = true
162 scripts_item_list.item_selected.connect(hide_scripts_popup.unbind(1))
163 update_script_list_visibility()
164
165 # Add script filter navigation.
166 script_filter_txt = find_or_null(scripts_item_list.get_parent().find_children("*", "LineEdit", true, false))
167 script_filter_txt.gui_input.connect(navigate_on_list.bind(scripts_item_list, select_script))
168
169 # --- Outline Start --- #
170 old_outline = find_or_null(lower_files_panel.find_children("*", "ItemList", true, false))
171 lower_files_panel.remove_child(old_outline)
172
173 outline = Outline.new()
174 outline.plugin = self
175
176 # Add navigation to the filter and text filtering.
177 outline_filter_txt = find_or_null(lower_files_panel.find_children("*", "LineEdit", true, false))
178 outline_filter_txt.gui_input.connect(navigate_on_list.bind(outline, scroll_outline))
179 outline_filter_txt.text_changed.connect(update_outline.unbind(1))
180
181 outline.outline_filter_txt = outline_filter_txt
182 lower_files_panel.add_child(outline)
183
184 outline.item_selected.connect(scroll_outline)
185
186 outline.get_parent().add_child(outline.filter_box)
187 outline.get_parent().move_child(outline.filter_box, outline.get_index())
188
189 # Add callback when the sorting changed.
190 sort_btn = find_or_null(lower_files_panel.find_children("*", "Button", true, false))
191 sort_btn.pressed.connect(update_outline)
192
193 update_outline_order()
194 update_outline_position()
195 # --- Outline End --- #
196
197 # --- Tabs Start --- #
198 old_scripts_tab_container = find_or_null(script_editor.find_children("*", "TabContainer", true, false))
199 old_scripts_tab_bar = old_scripts_tab_container.get_tab_bar()
200
201 var tab_container_parent: Control = old_scripts_tab_container.get_parent()
202 tab_splitter = HSplitContainer.new()
203 tab_splitter.size_flags_horizontal = Control.SIZE_EXPAND_FILL
204 tab_splitter.size_flags_vertical = Control.SIZE_EXPAND_FILL
205
206 tab_container_parent.add_child(tab_splitter)
207 tab_container_parent.move_child(tab_splitter, 0)
208 old_scripts_tab_container.reparent(tab_splitter)
209
210 # When something changed, we need to sync our own tab container.
211 old_scripts_tab_container.child_order_changed.connect(notify_order_changed)
212
213 multiline_tab_bar = MULTILINE_TAB_BAR.instantiate()
214 multiline_tab_bar.plugin = self
215 multiline_tab_bar.scripts_item_list = scripts_item_list
216 multiline_tab_bar.script_filter_txt = script_filter_txt
217 multiline_tab_bar.scripts_tab_container = old_scripts_tab_container
218
219 tab_container_parent.add_theme_constant_override(&"separation", 0)
220 tab_container_parent.add_child(multiline_tab_bar)
221
222 multiline_tab_bar.split_btn.toggled.connect(toggle_split_view.unbind(1))
223 update_tabs_position()
224 update_tabs_close_button()
225 update_tabs_visibility()
226 update_singleline_tabs()
227
228 # Create and set script popup.
229 script_panel_split_container = scripts_item_list.get_parent().get_parent()
230 create_set_scripts_popup()
231 # --- Tabs End --- #
232
233 old_scripts_tab_bar.tab_changed.connect(on_tab_changed)
234 on_tab_changed(old_scripts_tab_bar.current_tab)
235
236## Restore the old Engine script UI and free everything we created
237func _exit_tree() -> void:
238 var file_system: EditorFileSystem = EditorInterface.get_resource_filesystem()
239 file_system.filesystem_changed.disconnect(schedule_update)
240 get_editor_settings().settings_changed.disconnect(sync_settings)
241
242 if (tab_splitter != null):
243 var tab_container_parent: Control = tab_splitter.get_parent()
244 old_scripts_tab_container.reparent(tab_container_parent)
245 tab_container_parent.move_child(old_scripts_tab_container, 1)
246 tab_splitter.free()
247
248 if (script_editor_split_container != null):
249 if (script_editor_split_container != files_panel.get_parent()):
250 script_editor_split_container.add_child(files_panel)
251
252 # Try to restore the previous split offset.
253 if (is_outline_right):
254 var split_offset: float = script_editor_split_container.get_child(1).size.x
255 script_editor_split_container.split_offset = split_offset
256
257 script_editor_split_container.move_child(files_panel, 0)
258
259 outline_filter_txt.gui_input.disconnect(navigate_on_list)
260 outline_filter_txt.text_changed.disconnect(update_outline)
261 sort_btn.pressed.disconnect(update_outline)
262
263 outline.item_selected.disconnect(scroll_outline)
264
265 var outline_parent: Control = outline.get_parent()
266 outline_parent.remove_child(outline.filter_box)
267 outline_parent.remove_child(outline)
268 outline_parent.add_child(old_outline)
269 outline_parent.move_child(old_outline, -2)
270
271 outline.filter_box.free()
272 outline.free()
273
274 if (old_scripts_tab_bar != null):
275 old_scripts_tab_bar.tab_changed.disconnect(on_tab_changed)
276
277 if (old_scripts_tab_container != null):
278 old_scripts_tab_container.child_order_changed.disconnect(notify_order_changed)
279 old_scripts_tab_container.get_parent().remove_theme_constant_override(&"separation")
280 old_scripts_tab_container.get_parent().remove_child(multiline_tab_bar)
281
282 if (multiline_tab_bar != null):
283 multiline_tab_bar.free_tabs()
284 multiline_tab_bar.free()
285 scripts_popup.free()
286
287 if (scripts_item_list != null):
288 scripts_item_list.allow_reselect = false
289 scripts_item_list.item_selected.disconnect(hide_scripts_popup)
290 scripts_item_list.get_parent().visible = true
291
292 if (script_filter_txt != null):
293 script_filter_txt.gui_input.disconnect(navigate_on_list)
294
295 if (outline_popup != null):
296 outline_popup.free()
297 if (quick_open_popup != null):
298 quick_open_popup.free()
299 if (override_popup != null):
300 override_popup.free()
301
302 if (!show_members):
303 set_setting(SHOW_MEMBERS, show_members)
304#endregion
305
306#region Plugin and Shortcut processing
307## Lazy pattern to update the editor only once per frame
308func _process(delta: float) -> void:
309 update_editor()
310 set_process(false)
311
312## Process the user defined shortcuts
313func _shortcut_input(event: InputEvent) -> void:
314 if (!event.is_pressed() || event.is_echo()):
315 return
316
317 if (open_outline_popup_shc.matches_event(event)):
318 get_viewport().set_input_as_handled()
319 open_outline_popup()
320 elif (open_scripts_popup_shc.matches_event(event)):
321 get_viewport().set_input_as_handled()
322 open_scripts_popup()
323 elif (open_quick_search_popup_shc.matches_event(event)):
324 if (quick_open_tween != null && quick_open_tween.is_running()):
325 get_viewport().set_input_as_handled()
326 if (quick_open_tween != null):
327 quick_open_tween.kill()
328
329 quick_open_tween = create_tween()
330 quick_open_tween.tween_interval(0.1)
331 quick_open_tween.tween_callback(open_quick_search_popup)
332 quick_open_tween.tween_callback(func(): quick_open_tween = null)
333 else:
334 quick_open_tween = create_tween()
335 quick_open_tween.tween_interval(QUICK_OPEN_INTERVAL / 1000.0)
336 quick_open_tween.tween_callback(func(): quick_open_tween = null)
337 elif (open_override_popup_shc.matches_event(event)):
338 get_viewport().set_input_as_handled()
339 open_override_popup()
340
341## May cancels the quick search shortcut timer.
342func _input(event: InputEvent) -> void:
343 if (event is InputEventKey):
344 if (!open_quick_search_popup_shc.matches_event(event)):
345 if (quick_open_tween != null):
346 quick_open_tween.kill()
347 quick_open_tween = null
348#endregion
349
350#region Settings and Shortcut initialization
351
352## Initializes all settings.
353## Every setting can be changed while this plugin is active, which will override them.
354func init_settings():
355 is_outline_right = get_setting(OUTLINE_POSITION_RIGHT, is_outline_right)
356 is_hide_private_members = get_setting(HIDE_PRIVATE_MEMBERS, is_hide_private_members)
357 is_script_list_visible = get_setting(SCRIPT_LIST_VISIBLE, is_script_list_visible)
358 is_auto_navigate_in_fs = get_setting(AUTO_NAVIGATE_IN_FS, is_auto_navigate_in_fs)
359 is_script_tabs_visible = get_setting(SCRIPT_TABS_VISIBLE, is_script_tabs_visible)
360 is_script_tabs_top = get_setting(SCRIPT_TABS_POSITION_TOP, is_script_tabs_top)
361 is_script_tabs_close_button_always = get_setting(SCRIPT_TABS_CLOSE_BUTTON_ALWAYS, is_script_tabs_close_button_always)
362 is_script_tabs_singleline = get_setting(SCRIPT_TABS_SINGLELINE, is_script_tabs_singleline)
363
364 outline_order = get_outline_order()
365
366 # Users may disabled this, but with this plugin, we want to show the new Outline.
367 # So we need to reenable it, but restore the old value on exit.
368 show_members = get_setting(SHOW_MEMBERS, true)
369 if (!show_members):
370 set_setting(SHOW_MEMBERS, true)
371
372## Initializes all shortcuts.
373## Every shortcut can be changed while this plugin is active, which will override them.
374func init_shortcuts():
375 var editor_settings: EditorSettings = get_editor_settings()
376 if (!editor_settings.has_setting(OPEN_OUTLINE_POPUP)):
377 var shortcut: Shortcut = Shortcut.new()
378 var event: InputEventKey = InputEventKey.new()
379 event.device = -1
380 event.command_or_control_autoremap = true
381 event.keycode = KEY_O
382
383 shortcut.events = [ event ]
384 editor_settings.set_setting(OPEN_OUTLINE_POPUP, shortcut)
385
386 if (!editor_settings.has_setting(OPEN_SCRIPTS_POPUP)):
387 var shortcut: Shortcut = Shortcut.new()
388 var event: InputEventKey = InputEventKey.new()
389 event.device = -1
390 event.command_or_control_autoremap = true
391 event.keycode = KEY_U
392
393 shortcut.events = [ event ]
394 editor_settings.set_setting(OPEN_SCRIPTS_POPUP, shortcut)
395
396 if (!editor_settings.has_setting(OPEN_QUICK_SEARCH_POPUP)):
397 var shortcut: Shortcut = Shortcut.new()
398 var event: InputEventKey = InputEventKey.new()
399 event.device = -1
400 event.keycode = KEY_SHIFT
401
402 shortcut.events = [ event ]
403 editor_settings.set_setting(OPEN_QUICK_SEARCH_POPUP, shortcut)
404
405 if (!editor_settings.has_setting(OPEN_OVERRIDE_POPUP)):
406 var shortcut: Shortcut = Shortcut.new()
407 var event: InputEventKey = InputEventKey.new()
408 event.device = -1
409 event.keycode = KEY_INSERT
410 event.alt_pressed = true
411
412 shortcut.events = [ event ]
413 editor_settings.set_setting(OPEN_OVERRIDE_POPUP, shortcut)
414
415 if (!editor_settings.has_setting(TAB_CYCLE_FORWARD)):
416 var shortcut: Shortcut = Shortcut.new()
417 var event: InputEventKey = InputEventKey.new()
418 event.device = -1
419 event.keycode = KEY_TAB
420 event.ctrl_pressed = true
421
422 shortcut.events = [ event ]
423 editor_settings.set_setting(TAB_CYCLE_FORWARD, shortcut)
424
425 if (!editor_settings.has_setting(TAB_CYCLE_BACKWARD)):
426 var shortcut: Shortcut = Shortcut.new()
427 var event: InputEventKey = InputEventKey.new()
428 event.device = -1
429 event.keycode = KEY_TAB
430 event.shift_pressed = true
431 event.ctrl_pressed = true
432
433 shortcut.events = [ event ]
434 editor_settings.set_setting(TAB_CYCLE_BACKWARD, shortcut)
435
436 open_outline_popup_shc = editor_settings.get_setting(OPEN_OUTLINE_POPUP)
437 open_scripts_popup_shc = editor_settings.get_setting(OPEN_SCRIPTS_POPUP)
438 open_quick_search_popup_shc = editor_settings.get_setting(OPEN_QUICK_SEARCH_POPUP)
439 open_override_popup_shc = editor_settings.get_setting(OPEN_OVERRIDE_POPUP)
440 tab_cycle_forward_shc = editor_settings.get_setting(TAB_CYCLE_FORWARD)
441 tab_cycle_backward_shc = editor_settings.get_setting(TAB_CYCLE_BACKWARD)
442#endregion
443
444## Schedules an update on the next frame.
445func schedule_update():
446 set_process(true)
447
448## Updates all parts of the editor that are needed to be synchronized with the file system change.
449func update_editor():
450 if (file_to_navigate != &""):
451 EditorInterface.select_file(file_to_navigate)
452 EditorInterface.get_script_editor().get_current_editor().get_base_editor().grab_focus()
453 file_to_navigate = &""
454
455 update_keywords()
456
457 if (is_script_changed):
458 multiline_tab_bar.tab_changed()
459 outline.tab_changed()
460 is_script_changed = false
461 else:
462 # We saved / filesystem changed. so need to update everything.
463 multiline_tab_bar.update_tabs()
464 outline.update()
465
466func on_tab_changed(index: int):
467 if (old_script_editor_base != null):
468 old_script_editor_base.edited_script_changed.disconnect(update_selected_tab)
469 old_script_editor_base = null
470
471 var script_editor: ScriptEditor = EditorInterface.get_script_editor()
472 var script_editor_base: ScriptEditorBase = script_editor.get_current_editor()
473
474 if (script_editor_base != null):
475 script_editor_base.edited_script_changed.connect(update_selected_tab)
476
477 old_script_editor_base = script_editor_base
478
479 if (!multiline_tab_bar.is_split()):
480 multiline_tab_bar.split_btn.disabled = script_editor_base == null
481
482 is_script_changed = true
483
484 if (is_auto_navigate_in_fs && script_editor.get_current_script() != null):
485 var file: String = script_editor.get_current_script().get_path()
486
487 if (file.contains(BUILT_IN_SCRIPT)):
488 # We navigate to the scene in case of a built-in script.
489 file = file.get_slice(BUILT_IN_SCRIPT, 0)
490
491 file_to_navigate = file
492 else:
493 file_to_navigate = &""
494
495 schedule_update()
496
497func toggle_split_view():
498 var script_editor: ScriptEditor = EditorInterface.get_script_editor()
499 var split_script_editor_base: ScriptEditorBase = script_editor.get_current_editor()
500
501 if (!multiline_tab_bar.is_split()):
502 if (split_script_editor_base == null):
503 return
504
505 var base_editor: Control = split_script_editor_base.get_base_editor()
506 if !(base_editor is CodeEdit):
507 return
508
509 multiline_tab_bar.set_split(script_editor.get_current_script())
510
511 var editor: CodeEdit = SplitCodeEdit.new_from(base_editor)
512
513 var container: PanelContainer = PanelContainer.new()
514 container.custom_minimum_size.x = 200
515 container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
516
517 container.add_child(editor)
518 tab_splitter.add_child(container)
519 else:
520 multiline_tab_bar.set_split(null)
521 tab_splitter.remove_child(tab_splitter.get_child(tab_splitter.get_child_count() - 1))
522
523 if (split_script_editor_base == null):
524 multiline_tab_bar.split_btn.disabled = true
525
526func notify_order_changed():
527 multiline_tab_bar.script_order_changed()
528
529func open_quick_search_popup():
530 var pref_size: Vector2
531 if (quick_open_popup == null):
532 quick_open_popup = QUICK_OPEN_SCENE.instantiate()
533 quick_open_popup.plugin = self
534 quick_open_popup.set_unparent_when_invisible(true)
535 pref_size = Vector2(500, 400) * get_editor_scale()
536 else:
537 pref_size = quick_open_popup.size
538
539 quick_open_popup.popup_exclusive_on_parent(EditorInterface.get_script_editor(), get_center_editor_rect(pref_size))
540
541func open_override_popup():
542 var script: Script = get_current_script()
543 if (!script):
544 return
545
546 var pref_size: Vector2
547 if (override_popup == null):
548 override_popup = OVERRIDE_SCENE.instantiate()
549 override_popup.plugin = self
550 override_popup.outline = outline
551 override_popup.set_unparent_when_invisible(true)
552 pref_size = Vector2(500, 400) * get_editor_scale()
553 else:
554 pref_size = override_popup.size
555
556 override_popup.popup_exclusive_on_parent(EditorInterface.get_script_editor(), get_center_editor_rect(pref_size))
557
558func hide_scripts_popup():
559 if (scripts_popup != null && scripts_popup.visible):
560 scripts_popup.hide.call_deferred()
561
562func create_set_scripts_popup():
563 scripts_popup = PopupPanel.new()
564 scripts_popup.popup_hide.connect(restore_scripts_list)
565
566 # Need to be inside the tree, so it can be shown as popup for the tab container.
567 var script_editor: ScriptEditor = EditorInterface.get_script_editor()
568 script_editor.add_child(scripts_popup)
569
570 multiline_tab_bar.set_popup(scripts_popup)
571
572func restore_scripts_list():
573 script_filter_txt.text = &""
574
575 update_script_list_visibility()
576
577 scripts_item_list.get_parent().reparent(script_panel_split_container)
578 script_panel_split_container.move_child(scripts_item_list.get_parent(), 0)
579
580func navigate_on_list(event: InputEvent, list: ItemList, submit: Callable):
581 if (event.is_action_pressed(&"ui_text_submit")):
582 list.accept_event()
583
584 var index: int = get_list_index(list)
585 if (index == -1):
586 return
587
588 submit.call(index)
589 list.accept_event()
590 elif (event.is_action_pressed(&"ui_down", true)):
591 var index: int = get_list_index(list)
592 if (index == list.item_count - 1):
593 return
594
595 navigate_list(list, index, 1)
596 elif (event.is_action_pressed(&"ui_up", true)):
597 var index: int = get_list_index(list)
598 if (index <= 0):
599 return
600
601 navigate_list(list, index, -1)
602 elif (event.is_action_pressed(&"ui_page_down", true)):
603 var index: int = get_list_index(list)
604 if (index == list.item_count - 1):
605 return
606
607 navigate_list(list, index, 5)
608 elif (event.is_action_pressed(&"ui_page_up", true)):
609 var index: int = get_list_index(list)
610 if (index <= 0):
611 return
612
613 navigate_list(list, index, -5)
614 elif (event is InputEventKey && list.item_count > 0 && !list.is_anything_selected()):
615 list.select(0)
616
617func get_list_index(list: ItemList) -> int:
618 var items: PackedInt32Array = list.get_selected_items()
619
620 if (items.is_empty()):
621 return -1
622
623 return items[0]
624
625func navigate_list(list: ItemList, index: int, amount: int):
626 index = clamp(index + amount, 0, list.item_count - 1)
627
628 list.select(index)
629 list.ensure_current_is_visible()
630 list.accept_event()
631
632func get_center_editor_rect(pref_size: Vector2) -> Rect2i:
633 var script_editor: ScriptEditor = EditorInterface.get_script_editor()
634
635 var position: Vector2 = script_editor.global_position + script_editor.size / 2 - pref_size / 2
636
637 return Rect2i(position, pref_size)
638
639func open_outline_popup():
640 if (get_current_script() == null):
641 return
642
643 var button_flags: Array[bool] = outline.save_restore_filter()
644
645 var old_text: String = outline_filter_txt.text
646 outline_filter_txt.text = &""
647
648 var pref_size: Vector2
649 if (outline_popup == null):
650 outline_popup = PopupPanel.new()
651 outline_popup.set_unparent_when_invisible(true)
652 pref_size = Vector2(500, 400) * get_editor_scale()
653 else:
654 pref_size = outline_popup.size
655
656 var outline_initially_closed: bool = !files_panel.visible
657 if (outline_initially_closed):
658 files_panel.visible = true
659
660 files_panel.reparent(outline_popup)
661
662 outline_popup.popup_hide.connect(on_outline_popup_hidden.bind(outline_initially_closed, old_text, button_flags))
663
664 outline_popup.popup_exclusive_on_parent(EditorInterface.get_script_editor(), get_center_editor_rect(pref_size))
665
666 update_outline()
667 outline_filter_txt.grab_focus()
668
669func on_outline_popup_hidden(outline_initially_closed: bool, old_text: String, button_flags: Array[bool]):
670 outline_popup.popup_hide.disconnect(on_outline_popup_hidden)
671
672 if outline_initially_closed:
673 files_panel.visible = false
674
675 files_panel.reparent(script_editor_split_container)
676 if (!is_outline_right):
677 script_editor_split_container.move_child(files_panel, 0)
678
679 outline_filter_txt.text = old_text
680
681 outline.restore_filter(button_flags)
682
683 update_outline()
684
685func open_scripts_popup():
686 multiline_tab_bar.show_popup()
687
688func get_current_script() -> Script:
689 var script_editor: ScriptEditor = EditorInterface.get_script_editor()
690 return script_editor.get_current_script()
691
692func select_script(selected_idx: int):
693 hide_scripts_popup()
694
695 scripts_item_list.item_selected.emit(selected_idx)
696
697func scroll_outline(selected_idx: int):
698 if (outline_popup != null && outline_popup.visible):
699 outline_popup.hide.call_deferred()
700
701 var script: Script = get_current_script()
702 if (!script):
703 return
704
705 var text: String = outline.get_item_text(selected_idx)
706 var metadata: Dictionary[StringName, StringName] = outline.get_item_metadata(selected_idx)
707 var modifier: StringName = metadata[&"modifier"]
708 var type: StringName = metadata[&"type"]
709
710 var type_with_text: String = type + " " + text
711 if (type == &"func"):
712 type_with_text = type_with_text + "("
713
714 var source_code: String = script.get_source_code()
715 var lines: PackedStringArray = source_code.split("\n")
716
717 var index: int = 0
718 for line: String in lines:
719 # Easy case, like 'var abc'
720 if (line.begins_with(type_with_text)):
721 goto_line(index)
722 return
723
724 # We have an modifier, e.g. 'static'
725 if (modifier != &"" && line.begins_with(modifier)):
726 if (line.begins_with(modifier + " " + type_with_text)):
727 goto_line(index)
728 return
729 # Special case: An 'enum' is treated different.
730 elif (modifier == &"enum" && line.contains("enum " + text)):
731 goto_line(index)
732 return
733
734 # Hard case, probably something like '@onready var abc'
735 if (type == &"var" && line.contains(type_with_text)):
736 goto_line(index)
737 return
738
739 index += 1
740
741 push_error(type_with_text + " or " + modifier + " not found in source code")
742
743func goto_line(index: int):
744 var script_editor: ScriptEditor = EditorInterface.get_script_editor()
745 script_editor.goto_line(index)
746
747 var code_edit: CodeEdit = script_editor.get_current_editor().get_base_editor()
748 code_edit.set_caret_line(index)
749 code_edit.set_v_scroll(index)
750 code_edit.set_caret_column(code_edit.get_line(index).length())
751 code_edit.set_h_scroll(0)
752
753 code_edit.grab_focus()
754
755func update_script_list_visibility():
756 scripts_item_list.get_parent().visible = is_script_list_visible
757
758func sync_settings():
759 if (suppress_settings_sync):
760 return
761
762 var changed_settings: PackedStringArray = get_editor_settings().get_changed_settings()
763 for setting: String in changed_settings:
764 if (setting == ICON_SATURATION):
765 outline.reset_icons()
766 elif (setting == SHOW_MEMBERS):
767 show_members = get_setting(SHOW_MEMBERS, true)
768 if (!show_members):
769 set_setting(SHOW_MEMBERS, true)
770
771 if (!setting.begins_with(SCRIPT_IDE)):
772 continue
773
774 match (setting):
775 OUTLINE_POSITION_RIGHT:
776 var new_outline_right: bool = get_setting(OUTLINE_POSITION_RIGHT, is_outline_right)
777 if (new_outline_right != is_outline_right):
778 is_outline_right = new_outline_right
779
780 update_outline_position()
781 OUTLINE_ORDER:
782 var new_outline_order: PackedStringArray = get_outline_order()
783 if (new_outline_order != outline_order):
784 outline_order = new_outline_order
785
786 update_outline_order()
787 HIDE_PRIVATE_MEMBERS:
788 var new_hide_private_members: bool = get_setting(HIDE_PRIVATE_MEMBERS, is_hide_private_members)
789 if (new_hide_private_members != is_hide_private_members):
790 is_hide_private_members = new_hide_private_members
791
792 outline.update()
793 SCRIPT_LIST_VISIBLE:
794 var new_script_list_visible: bool = get_setting(SCRIPT_LIST_VISIBLE, is_script_list_visible)
795 if (new_script_list_visible != is_script_list_visible):
796 is_script_list_visible = new_script_list_visible
797
798 update_script_list_visibility()
799 SCRIPT_TABS_VISIBLE:
800 var new_script_tabs_visible: bool = get_setting(SCRIPT_TABS_VISIBLE, is_script_tabs_visible)
801 if (new_script_tabs_visible != is_script_tabs_visible):
802 is_script_tabs_visible = new_script_tabs_visible
803
804 update_tabs_visibility()
805 SCRIPT_TABS_POSITION_TOP:
806 var new_script_tabs_top: bool = get_setting(SCRIPT_TABS_POSITION_TOP, is_script_tabs_top)
807 if (new_script_tabs_top != is_script_tabs_top):
808 is_script_tabs_top = new_script_tabs_top
809
810 update_tabs_position()
811 SCRIPT_TABS_CLOSE_BUTTON_ALWAYS:
812 var new_script_tabs_close_button_always: bool = get_setting(SCRIPT_TABS_CLOSE_BUTTON_ALWAYS, is_script_tabs_close_button_always)
813 if (new_script_tabs_close_button_always != is_script_tabs_close_button_always):
814 is_script_tabs_close_button_always = new_script_tabs_close_button_always
815
816 update_tabs_close_button()
817 SCRIPT_TABS_SINGLELINE:
818 var new_script_tabs_singleline: bool = get_setting(SCRIPT_TABS_SINGLELINE, is_script_tabs_singleline)
819 if (new_script_tabs_singleline != is_script_tabs_singleline):
820 is_script_tabs_singleline = new_script_tabs_singleline
821
822 update_singleline_tabs()
823 AUTO_NAVIGATE_IN_FS:
824 is_auto_navigate_in_fs = get_setting(AUTO_NAVIGATE_IN_FS, is_auto_navigate_in_fs)
825 OPEN_OUTLINE_POPUP:
826 open_outline_popup_shc = get_shortcut(OPEN_OUTLINE_POPUP)
827 OPEN_SCRIPTS_POPUP:
828 open_scripts_popup_shc = get_shortcut(OPEN_SCRIPTS_POPUP)
829 OPEN_OVERRIDE_POPUP:
830 open_override_popup_shc = get_shortcut(OPEN_OVERRIDE_POPUP)
831 TAB_CYCLE_FORWARD:
832 tab_cycle_forward_shc = get_shortcut(TAB_CYCLE_FORWARD)
833 TAB_CYCLE_BACKWARD:
834 tab_cycle_backward_shc = get_shortcut(TAB_CYCLE_BACKWARD)
835 _:
836 outline.update_filter_buttons()
837
838func update_selected_tab():
839 multiline_tab_bar.update_selected_tab()
840
841func update_tabs_position():
842 var tab_container_parent: Control = multiline_tab_bar.get_parent()
843 if (is_script_tabs_top):
844 tab_container_parent.move_child(multiline_tab_bar, 0)
845 else:
846 tab_container_parent.move_child(multiline_tab_bar, tab_container_parent.get_child_count() - 1)
847
848func update_tabs_close_button():
849 multiline_tab_bar.show_close_button_always = is_script_tabs_close_button_always
850
851func update_tabs_visibility():
852 multiline_tab_bar.visible = is_script_tabs_visible
853
854func update_singleline_tabs():
855 multiline_tab_bar.is_singleline_tabs = is_script_tabs_singleline
856
857func update_outline():
858 outline.update_outline()
859
860func update_outline_position():
861 if (is_outline_right):
862 # Try to restore the previous split offset.
863 var split_offset: float = script_editor_split_container.get_child(1).size.x
864 script_editor_split_container.split_offset = split_offset
865 script_editor_split_container.move_child(files_panel, 1)
866 else:
867 script_editor_split_container.move_child(files_panel, 0)
868
869func update_outline_order():
870 outline.outline_order = outline_order
871
872func update_keywords():
873 var script: Script = get_current_script()
874 if (script == null):
875 return
876
877 var new_script_type: StringName = script.get_instance_base_type()
878 if (old_script_type != new_script_type):
879 old_script_type = new_script_type
880
881 keywords.clear()
882 keywords["_static_init"] = true
883 register_virtual_methods(new_script_type)
884
885func register_virtual_methods(clazz: String):
886 for method: Dictionary in ClassDB.class_get_method_list(clazz):
887 if (method[&"flags"] & METHOD_FLAG_VIRTUAL > 0):
888 keywords[method[&"name"]] = true
889
890func get_editor_scale() -> float:
891 return EditorInterface.get_editor_scale()
892
893func get_editor_settings() -> EditorSettings:
894 return EditorInterface.get_editor_settings()
895
896func get_setting(property: StringName, alt: bool) -> bool:
897 var editor_settings: EditorSettings = get_editor_settings()
898 if (editor_settings.has_setting(property)):
899 return editor_settings.get_setting(property)
900 else:
901 editor_settings.set_setting(property, alt)
902 return alt
903
904func set_setting(property: StringName, value: bool):
905 var editor_settings: EditorSettings = get_editor_settings()
906
907 suppress_settings_sync = true
908 editor_settings.set_setting(property, value)
909 suppress_settings_sync = false
910
911func get_shortcut(property: StringName) -> Shortcut:
912 return get_editor_settings().get_setting(property)
913
914func get_outline_order() -> PackedStringArray:
915 var new_outline_order: PackedStringArray
916 var editor_settings: EditorSettings = get_editor_settings()
917 if (editor_settings.has_setting(OUTLINE_ORDER)):
918 new_outline_order = editor_settings.get_setting(OUTLINE_ORDER)
919 else:
920 new_outline_order = Outline.DEFAULT_ORDER
921 editor_settings.set_setting(OUTLINE_ORDER, outline_order)
922
923 return new_outline_order
924
925static func find_or_null(arr: Array[Node]) -> Control:
926 if (arr.is_empty()):
927 push_error("""Node that is needed for Script-IDE not found.
928Plugin will not work correctly.
929This might be due to some other plugins or changes in the Engine.
930Please report this to Script-IDE, so we can figure out a fix.""")
931 return null
932 return arr[0] as Control