Path: blob/master/editor/script/script_editor_plugin.cpp
20860 views
/**************************************************************************/1/* script_editor_plugin.cpp */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#include "script_editor_plugin.h"3132#include "core/config/project_settings.h"33#include "core/input/input.h"34#include "core/io/config_file.h"35#include "core/io/file_access.h"36#include "core/io/json.h"37#include "core/io/resource_loader.h"38#include "core/os/keyboard.h"39#include "core/os/os.h"40#include "core/string/fuzzy_search.h"41#include "core/version.h"42#include "editor/debugger/editor_debugger_node.h"43#include "editor/debugger/script_editor_debugger.h"44#include "editor/doc/editor_help.h"45#include "editor/doc/editor_help_search.h"46#include "editor/docks/editor_dock_manager.h"47#include "editor/docks/filesystem_dock.h"48#include "editor/docks/inspector_dock.h"49#include "editor/docks/signals_dock.h"50#include "editor/editor_interface.h"51#include "editor/editor_main_screen.h"52#include "editor/editor_node.h"53#include "editor/editor_string_names.h"54#include "editor/file_system/editor_paths.h"55#include "editor/gui/code_editor.h"56#include "editor/gui/editor_file_dialog.h"57#include "editor/gui/editor_toaster.h"58#include "editor/gui/filter_line_edit.h"59#include "editor/gui/window_wrapper.h"60#include "editor/inspector/editor_context_menu_plugin.h"61#include "editor/run/editor_run_bar.h"62#include "editor/scene/editor_scene_tabs.h"63#include "editor/script/find_in_files.h"64#include "editor/script/script_text_editor.h"65#include "editor/script/syntax_highlighters.h"66#include "editor/script/text_editor.h"67#include "editor/settings/editor_command_palette.h"68#include "editor/settings/editor_settings.h"69#include "editor/shader/shader_editor_plugin.h"70#include "editor/shader/text_shader_editor.h"71#include "editor/themes/editor_scale.h"72#include "editor/themes/editor_theme_manager.h"73#include "scene/gui/separator.h"74#include "scene/gui/tab_container.h"75#include "scene/gui/texture_rect.h"76#include "scene/main/node.h"77#include "scene/main/window.h"78#include "servers/display/display_server.h"7980void ScriptEditorQuickOpen::popup_dialog(const Vector<String> &p_functions, bool p_dontclear) {81popup_centered_ratio(0.6);82if (p_dontclear) {83search_box->select_all();84} else {85search_box->clear();86}87search_box->grab_focus();88functions = p_functions;89_update_search();90}9192void ScriptEditorQuickOpen::_text_changed(const String &p_newtext) {93_update_search();94}9596void ScriptEditorQuickOpen::_update_search() {97search_options->clear();98TreeItem *root = search_options->create_item();99100for (int i = 0; i < functions.size(); i++) {101String file = functions[i];102if ((search_box->get_text().is_empty() || file.containsn(search_box->get_text()))) {103TreeItem *ti = search_options->create_item(root);104ti->set_text(0, file);105if (root->get_first_child() == ti) {106ti->select(0);107}108}109}110111get_ok_button()->set_disabled(root->get_first_child() == nullptr);112}113114void ScriptEditorQuickOpen::_confirmed() {115TreeItem *ti = search_options->get_selected();116if (!ti) {117return;118}119int line = ti->get_text(0).get_slicec(':', 1).to_int();120121emit_signal(SNAME("goto_line"), line - 1);122hide();123}124125void ScriptEditorQuickOpen::_notification(int p_what) {126switch (p_what) {127case NOTIFICATION_ENTER_TREE: {128connect(SceneStringName(confirmed), callable_mp(this, &ScriptEditorQuickOpen::_confirmed));129} break;130131case NOTIFICATION_EXIT_TREE: {132disconnect(SceneStringName(confirmed), callable_mp(this, &ScriptEditorQuickOpen::_confirmed));133} break;134}135}136137void ScriptEditorQuickOpen::_bind_methods() {138ADD_SIGNAL(MethodInfo("goto_line", PropertyInfo(Variant::INT, "line")));139}140141ScriptEditorQuickOpen::ScriptEditorQuickOpen() {142set_ok_button_text(TTRC("Open"));143get_ok_button()->set_disabled(true);144set_hide_on_ok(false);145146VBoxContainer *vbc = memnew(VBoxContainer);147add_child(vbc);148149search_box = memnew(FilterLineEdit);150vbc->add_margin_child(TTRC("Search:"), search_box);151search_box->connect(SceneStringName(text_changed), callable_mp(this, &ScriptEditorQuickOpen::_text_changed));152register_text_enter(search_box);153154search_options = memnew(Tree);155search_box->set_forward_control(search_options);156search_options->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);157search_options->set_hide_root(true);158search_options->set_hide_folding(true);159search_options->add_theme_constant_override("draw_guides", 1);160vbc->add_margin_child(TTRC("Matches:"), search_options, true);161search_options->connect("item_activated", callable_mp(this, &ScriptEditorQuickOpen::_confirmed));162}163164/////////////////////////////////165166ScriptEditor *ScriptEditor::script_editor = nullptr;167168/*** SCRIPT EDITOR ******/169170String ScriptEditor::_get_debug_tooltip(const String &p_text, Node *p_se) {171if (EDITOR_GET("text_editor/behavior/documentation/enable_tooltips")) {172return String();173}174175// NOTE: See also `ScriptTextEditor::_show_symbol_tooltip()` for documentation tooltips enabled.176String debug_value = EditorDebuggerNode::get_singleton()->get_var_value(p_text);177if (!debug_value.is_empty()) {178constexpr int DISPLAY_LIMIT = 1024;179if (debug_value.size() > DISPLAY_LIMIT) {180debug_value = debug_value.left(DISPLAY_LIMIT) + "... " + TTR("(truncated)");181}182debug_value = TTR("Current value: ") + debug_value;183}184185return debug_value;186}187188void ScriptEditor::_script_created(Ref<Script> p_script) {189EditorNode::get_singleton()->push_item(p_script.operator->());190}191192void ScriptEditor::_goto_script_line2(int p_line) {193if (TextEditorBase *current = Object::cast_to<TextEditorBase>(_get_current_editor())) {194current->goto_line(p_line);195}196}197198void ScriptEditor::_goto_script_line(Ref<RefCounted> p_script, int p_line) {199Ref<Script> scr = Object::cast_to<Script>(*p_script);200if (scr.is_valid() && (scr->has_source_code() || scr->get_path().is_resource_file())) {201if (edit(p_script, p_line, 0)) {202EditorNode::get_singleton()->push_item(p_script.ptr());203204if (TextEditorBase *current = Object::cast_to<TextEditorBase>(_get_current_editor())) {205current->goto_line_centered(p_line);206}207208_save_history();209}210}211}212213void ScriptEditor::_change_execution(Ref<RefCounted> p_script, int p_line, bool p_set) {214Ref<Script> scr = Object::cast_to<Script>(*p_script);215if (scr.is_valid() && (scr->has_source_code() || scr->get_path().is_resource_file())) {216for (int i = 0; i < tab_container->get_tab_count(); i++) {217if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(tab_container->get_tab_control(i))) {218if ((scr.is_valid() && teb->get_edited_resource() == p_script) || teb->get_edited_resource()->get_path() == scr->get_path()) {219if (p_set) {220teb->set_executing_line(p_line);221} else {222teb->clear_executing_line();223}224}225}226}227}228}229230void ScriptEditor::_set_breakpoint(Ref<RefCounted> p_script, int p_line, bool p_enabled) {231Ref<Script> scr = Object::cast_to<Script>(*p_script);232if (scr.is_valid() && (scr->has_source_code() || scr->get_path().is_resource_file())) {233// Update if open.234for (int i = 0; i < tab_container->get_tab_count(); i++) {235CodeEditorBase *ceb = Object::cast_to<CodeEditorBase>(tab_container->get_tab_control(i));236if (ceb && ceb->get_edited_resource()->get_path() == scr->get_path()) {237ceb->set_breakpoint(p_line, p_enabled);238return;239}240}241242// Handle closed.243Dictionary state = script_editor_cache->get_value(scr->get_path(), "state");244Array breakpoints;245if (state.has("breakpoints")) {246breakpoints = state["breakpoints"];247}248249if (breakpoints.has(p_line)) {250if (!p_enabled) {251breakpoints.erase(p_line);252}253} else if (p_enabled) {254breakpoints.push_back(p_line);255}256state["breakpoints"] = breakpoints;257script_editor_cache->set_value(scr->get_path(), "state", state);258EditorDebuggerNode::get_singleton()->set_breakpoint(scr->get_path(), p_line + 1, p_enabled);259}260}261262void ScriptEditor::_clear_breakpoints() {263for (int i = 0; i < tab_container->get_tab_count(); i++) {264if (CodeEditorBase *ceb = Object::cast_to<CodeEditorBase>(tab_container->get_tab_control(i))) {265ceb->clear_breakpoints();266}267}268269// Clear from closed scripts.270Vector<String> cached_editors = script_editor_cache->get_sections();271for (const String &E : cached_editors) {272Array breakpoints = _get_cached_breakpoints_for_script(E);273for (int breakpoint : breakpoints) {274EditorDebuggerNode::get_singleton()->set_breakpoint(E, (int)breakpoint + 1, false);275}276277if (breakpoints.size() > 0) {278Dictionary state = script_editor_cache->get_value(E, "state");279state["breakpoints"] = Array();280script_editor_cache->set_value(E, "state", state);281}282}283}284285Array ScriptEditor::_get_cached_breakpoints_for_script(const String &p_path) const {286if (!ResourceLoader::exists(p_path, "Script") || p_path.begins_with("local://") || !script_editor_cache->has_section_key(p_path, "state")) {287return Array();288}289290Dictionary state = script_editor_cache->get_value(p_path, "state");291if (!state.has("breakpoints")) {292return Array();293}294return state["breakpoints"];295}296297ScriptEditorBase *ScriptEditor::_get_current_editor() const {298int selected = tab_container->get_current_tab();299if (selected < 0 || selected >= tab_container->get_tab_count()) {300return nullptr;301}302303return Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(selected));304}305306void ScriptEditor::_update_history_arrows() {307script_back->set_disabled(history_pos <= 0);308script_forward->set_disabled(history_pos >= history.size() - 1);309}310311void ScriptEditor::_save_history() {312if (history_pos >= 0 && history_pos < history.size() && history[history_pos].control == tab_container->get_current_tab_control()) {313Node *n = tab_container->get_current_tab_control();314315if (Object::cast_to<TextEditorBase>(n)) {316history.write[history_pos].state = Object::cast_to<TextEditorBase>(n)->get_navigation_state();317}318if (Object::cast_to<EditorHelp>(n)) {319history.write[history_pos].state = Object::cast_to<EditorHelp>(n)->get_scroll();320}321}322323history.resize(history_pos + 1);324ScriptHistory sh;325sh.control = tab_container->get_current_tab_control();326sh.state = Variant();327328history.push_back(sh);329history_pos++;330331_update_history_arrows();332}333334void ScriptEditor::_save_previous_state(Dictionary p_state) {335if (lock_history) {336// Done as a result of a deferred call triggered by set_edit_state().337return;338}339340if (history_pos >= 0 && history_pos < history.size() && history[history_pos].control == tab_container->get_current_tab_control()) {341Node *n = tab_container->get_current_tab_control();342343if (Object::cast_to<ScriptTextEditor>(n)) {344history.write[history_pos].state = p_state;345}346}347348history.resize(history_pos + 1);349ScriptHistory sh;350sh.control = tab_container->get_current_tab_control();351sh.state = Variant();352353history.push_back(sh);354history_pos++;355356_update_history_arrows();357}358359void ScriptEditor::_go_to_tab(int p_idx) {360if (ScriptEditorBase *current = _get_current_editor()) {361if (current->is_unsaved()) {362current->apply_code();363}364}365366Control *c = tab_container->get_tab_control(p_idx);367if (!c) {368return;369}370371if (history_pos >= 0 && history_pos < history.size() && history[history_pos].control == tab_container->get_current_tab_control()) {372Node *n = tab_container->get_current_tab_control();373374if (Object::cast_to<TextEditorBase>(n)) {375history.write[history_pos].state = Object::cast_to<TextEditorBase>(n)->get_navigation_state();376}377if (Object::cast_to<EditorHelp>(n)) {378history.write[history_pos].state = Object::cast_to<EditorHelp>(n)->get_scroll();379}380}381382history.resize(history_pos + 1);383ScriptHistory sh;384sh.control = c;385sh.state = Variant();386387if (!lock_history && (history.is_empty() || history[history.size() - 1].control != sh.control)) {388history.push_back(sh);389history_pos++;390}391392tab_container->set_current_tab(p_idx);393394c = tab_container->get_current_tab_control();395396if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(c)) {397TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb);398if (teb && is_visible_in_tree()) {399teb->ensure_focus();400}401402Ref<Script> scr = seb->get_edited_resource();403if (scr.is_valid()) {404notify_script_changed(scr);405}406407seb->validate_script();408}409410if (EditorHelp *eh = Object::cast_to<EditorHelp>(c)) {411script_name_button->set_text(eh->get_class());412_calculate_script_name_button_size();413414if (is_visible_in_tree()) {415eh->set_focused();416}417}418419c->set_meta("__editor_pass", ++edit_pass);420_update_history_arrows();421_update_script_colors();422_update_members_overview();423_update_help_overview();424_update_selected_editor_menu();425_update_online_doc();426_update_members_overview_visibility();427_update_help_overview_visibility();428}429430void ScriptEditor::_add_recent_script(const String &p_path) {431if (p_path.is_empty()) {432return;433}434435Array rc = EditorSettings::get_singleton()->get_project_metadata("recent_files", "scripts", Array());436if (rc.has(p_path)) {437rc.erase(p_path);438}439rc.push_front(p_path);440if (rc.size() > 10) {441rc.resize(10);442}443444EditorSettings::get_singleton()->set_project_metadata("recent_files", "scripts", rc);445_update_recent_scripts();446}447448void ScriptEditor::_update_recent_scripts() {449Array rc = EditorSettings::get_singleton()->get_project_metadata("recent_files", "scripts", Array());450recent_scripts->clear();451452String path;453for (int i = 0; i < rc.size(); i++) {454path = rc[i];455recent_scripts->add_item(path.replace("res://", ""));456}457458recent_scripts->add_separator();459recent_scripts->add_shortcut(ED_GET_SHORTCUT("script_editor/clear_recent"));460recent_scripts->set_item_auto_translate_mode(-1, AUTO_TRANSLATE_MODE_ALWAYS);461recent_scripts->set_item_disabled(-1, rc.is_empty());462463recent_scripts->reset_size();464}465466void ScriptEditor::_open_recent_script(int p_idx) {467// clear button468if (p_idx == recent_scripts->get_item_count() - 1) {469EditorSettings::get_singleton()->set_project_metadata("recent_files", "scripts", Array());470callable_mp(this, &ScriptEditor::_update_recent_scripts).call_deferred();471return;472}473474Array rc = EditorSettings::get_singleton()->get_project_metadata("recent_files", "scripts", Array());475ERR_FAIL_INDEX(p_idx, rc.size());476477String path = rc[p_idx];478// if its not on disk its a help file or deleted479if (FileAccess::exists(path)) {480if (_get_recognized_extensions().find(path.get_extension())) {481Ref<Resource> scr = ResourceLoader::load(path);482if (scr.is_valid()) {483edit(scr, true);484return;485}486}487488Error err;489Ref<TextFile> text_file = _load_text_file(path, &err);490if (text_file.is_valid()) {491edit(text_file, true);492return;493}494// if it's a path then it's most likely a deleted file not help495} else if (path.contains("::")) {496// built-in script497String res_path = path.get_slice("::", 0);498EditorNode::get_singleton()->load_scene_or_resource(res_path, false, false);499500Ref<Script> scr = ResourceLoader::load(path);501if (scr.is_valid()) {502edit(scr, true);503return;504}505} else if (!path.is_resource_file()) {506_help_class_open(path);507return;508}509510rc.remove_at(p_idx);511EditorSettings::get_singleton()->set_project_metadata("recent_files", "scripts", rc);512_update_recent_scripts();513_show_error_dialog(path);514}515516void ScriptEditor::_show_error_dialog(const String &p_path) {517error_dialog->set_text(vformat(TTR("Can't open '%s'. The file could have been moved or deleted."), p_path));518error_dialog->popup_centered();519}520521void ScriptEditor::_close_tab(int p_idx, bool p_save, bool p_history_back) {522int selected = p_idx;523if (selected < 0 || selected >= tab_container->get_tab_count()) {524return;525}526527Node *tselected = tab_container->get_tab_control(selected);528529if (ScriptEditorBase *current = Object::cast_to<ScriptEditorBase>(tselected)) {530Ref<Resource> file = current->get_edited_resource();531if (p_save && file.is_valid()) {532// Do not try to save internal scripts, but prompt to save in-memory533// scripts which are not saved to disk yet (have empty path).534if (!file->is_built_in()) {535save_current_script();536}537}538if (file.is_valid()) {539if (!file->get_path().is_empty()) {540// Only saved scripts can be restored.541previous_scripts.push_back(file->get_path());542}543544Ref<Script> scr = file;545if (scr.is_valid()) {546notify_script_close(scr);547}548}549}550551// roll back to previous tab552if (p_history_back) {553_history_back();554}555556//remove from history557history.resize(history_pos + 1);558559for (int i = 0; i < history.size(); i++) {560if (history[i].control == tselected) {561history.remove_at(i);562i--;563history_pos--;564}565}566567if (history_pos >= history.size()) {568history_pos = history.size() - 1;569}570571int idx = tab_container->get_current_tab();572if (TextEditorBase *current = Object::cast_to<TextEditorBase>(tselected)) {573_save_editor_state(current);574}575memdelete(tselected);576577if (script_close_queue.is_empty()) {578if (idx >= tab_container->get_tab_count()) {579idx = tab_container->get_tab_count() - 1;580}581if (idx >= 0) {582if (history_pos >= 0) {583idx = tab_container->get_tab_idx_from_control(history[history_pos].control);584}585_go_to_tab(idx);586} else {587_update_selected_editor_menu();588_update_online_doc();589script_name_button->set_text(String());590_calculate_script_name_button_size();591}592593_update_history_arrows();594_update_script_names();595_save_layout();596_update_find_replace_bar();597}598}599600void ScriptEditor::_close_current_tab(bool p_save, bool p_history_back) {601_close_tab(tab_container->get_current_tab(), p_save, p_history_back);602}603604void ScriptEditor::_close_discard_current_tab(const String &p_str) {605Ref<Script> scr = _get_current_script();606if (scr.is_valid()) {607scr->reload_from_file();608}609_close_tab(tab_container->get_current_tab(), false);610erase_tab_confirm->hide();611}612613void ScriptEditor::_close_docs_tab() {614int child_count = tab_container->get_tab_count();615for (int i = child_count - 1; i >= 0; i--) {616if (Object::cast_to<EditorHelp>(tab_container->get_tab_control(i))) {617_close_tab(i, true, false);618}619}620}621622void ScriptEditor::_copy_script_path() {623if (ScriptEditorBase *seb = _get_current_editor()) {624Ref<Resource> scr = seb->get_edited_resource();625DisplayServer::get_singleton()->clipboard_set(scr->get_path());626}627}628629void ScriptEditor::_copy_script_uid() {630if (ScriptEditorBase *seb = _get_current_editor()) {631Ref<Resource> scr = seb->get_edited_resource();632ResourceUID::ID uid = ResourceLoader::get_resource_uid(scr->get_path());633DisplayServer::get_singleton()->clipboard_set(ResourceUID::get_singleton()->id_to_text(uid));634}635}636637void ScriptEditor::_close_other_tabs() {638int current_idx = tab_container->get_current_tab();639for (int i = tab_container->get_tab_count() - 1; i >= 0; i--) {640if (i != current_idx) {641script_close_queue.push_back(i);642}643}644_queue_close_tabs();645}646647void ScriptEditor::_close_tabs_below() {648int current_idx = tab_container->get_current_tab();649for (int i = tab_container->get_tab_count() - 1; i > current_idx; i--) {650script_close_queue.push_back(i);651}652_go_to_tab(current_idx);653_queue_close_tabs();654}655656void ScriptEditor::_close_all_tabs() {657for (int i = tab_container->get_tab_count() - 1; i >= 0; i--) {658script_close_queue.push_back(i);659}660_queue_close_tabs();661}662663void ScriptEditor::_queue_close_tabs() {664while (!script_close_queue.is_empty()) {665int idx = script_close_queue.front()->get();666script_close_queue.pop_front();667668tab_container->set_current_tab(idx);669ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(idx));670if (seb) {671// Maybe there are unsaved changes.672if (seb->is_unsaved()) {673_ask_close_current_unsaved_tab(seb);674erase_tab_confirm->connect(SceneStringName(visibility_changed), callable_mp(this, &ScriptEditor::_queue_close_tabs), CONNECT_ONE_SHOT);675break;676}677}678679_close_current_tab(false, false);680}681_update_find_replace_bar();682}683684void ScriptEditor::_ask_close_current_unsaved_tab(ScriptEditorBase *current) {685erase_tab_confirm->set_text(TTR("Close and save changes?") + "\n\"" + current->get_name() + "\"");686erase_tab_confirm->popup_centered();687}688689void ScriptEditor::_resave_scripts(const String &p_str) {690apply_scripts();691692for (int i = 0; i < tab_container->get_tab_count(); i++) {693ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));694if (!seb) {695continue;696}697698Ref<Resource> scr = seb->get_edited_resource();699700if (scr->is_built_in()) {701continue; // Internal script, who cares.702}703704_auto_format_text(seb);705706Ref<TextFile> text_file = scr;707if (text_file.is_valid()) {708seb->apply_code();709_save_text_file(text_file, text_file->get_path());710break;711} else {712EditorNode::get_singleton()->save_resource(scr);713}714seb->tag_saved_version();715}716717disk_changed->hide();718}719720void ScriptEditor::_res_saved_callback(const Ref<Resource> &p_res) {721for (int i = 0; i < tab_container->get_tab_count(); i++) {722if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i))) {723if (seb->get_edited_resource() == p_res) {724seb->tag_saved_version();725}726}727}728729if (p_res.is_valid()) {730// In case the Resource has built-in scripts.731_mark_built_in_scripts_as_saved(p_res->get_path());732}733734_update_script_names();735Ref<Script> scr = p_res;736if (scr.is_valid()) {737trigger_live_script_reload(scr->get_path());738}739}740741void ScriptEditor::_scene_saved_callback(const String &p_path) {742// If scene was saved, mark all built-in scripts from that scene as saved.743_mark_built_in_scripts_as_saved(p_path);744}745746void ScriptEditor::_mark_built_in_scripts_as_saved(const String &p_parent_path) {747for (int i = 0; i < tab_container->get_tab_count(); i++) {748ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));749if (!seb) {750continue;751}752753Ref<Resource> edited_res = seb->get_edited_resource();754if (!edited_res->is_built_in()) {755continue; // External script, who cares.756}757758if (edited_res->get_path().get_slice("::", 0) != p_parent_path) {759continue; // Wrong scene.760}761seb->tag_saved_version();762763Ref<Script> scr = edited_res;764if (scr.is_valid()) {765trigger_live_script_reload(scr->get_path());766clear_docs_from_script(scr);767scr->reload(true);768update_docs_from_script(scr);769}770}771}772773void ScriptEditor::trigger_live_script_reload(const String &p_script_path) {774if (!script_paths_to_reload.has(p_script_path)) {775Ref<Script> reloaded_script = ResourceCache::get_ref(p_script_path);776if (reloaded_script.is_null()) {777reloaded_script = ResourceLoader::load(p_script_path);778}779if (reloaded_script.is_valid()) {780if (!reloaded_script->get_language()->validate(reloaded_script->get_source_code(), p_script_path)) {781// Script has errors, don't live reload.782return;783}784}785786script_paths_to_reload.append(p_script_path);787}788if (!pending_auto_reload && auto_reload_running_scripts) {789callable_mp(this, &ScriptEditor::_live_auto_reload_running_scripts).call_deferred();790pending_auto_reload = true;791}792}793794void ScriptEditor::_live_auto_reload_running_scripts() {795pending_auto_reload = false;796EditorDebuggerNode::get_singleton()->reload_scripts(script_paths_to_reload);797script_paths_to_reload.clear();798}799800bool ScriptEditor::_test_script_times_on_disk(Ref<Resource> p_for_script) {801disk_changed_list->clear();802TreeItem *r = disk_changed_list->create_item();803804bool need_ask = false;805bool need_reload = false;806bool use_autoreload = EDITOR_GET("text_editor/behavior/files/auto_reload_scripts_on_external_change");807808for (int i = 0; i < tab_container->get_tab_count(); i++) {809ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));810if (!seb) {811continue;812}813814Ref<Resource> edited_res = seb->get_edited_resource();815if (p_for_script.is_valid() && edited_res.is_valid() && p_for_script != edited_res) {816continue;817}818819if (edited_res->is_built_in()) {820continue; // Internal script, who cares.821}822823uint64_t last_date = seb->edited_file_data.last_modified_time;824uint64_t date = FileAccess::get_modified_time(seb->edited_file_data.path);825826if (last_date != date) {827TreeItem *ti = disk_changed_list->create_item(r);828ti->set_text(0, seb->edited_file_data.path.get_file());829830if (!use_autoreload || seb->is_unsaved()) {831need_ask = true;832}833need_reload = true;834}835}836837if (need_reload) {838if (!need_ask) {839script_editor->reload_scripts();840need_reload = false;841} else {842callable_mp((Window *)disk_changed, &Window::popup_centered_ratio).call_deferred(0.3);843}844}845846return need_reload;847}848849void _import_text_editor_theme(const String &p_file) {850if (p_file.get_extension() != "tet") {851EditorToaster::get_singleton()->popup_str(TTR("Importing theme failed. File is not a text editor theme file (.tet)."), EditorToaster::SEVERITY_ERROR);852return;853}854const String theme_name = p_file.get_file().get_basename();855if (EditorSettings::is_default_text_editor_theme(theme_name.to_lower())) {856EditorToaster::get_singleton()->popup_str(TTR("Importing theme failed. File name cannot be 'Default', 'Custom', or 'Godot 2'."), EditorToaster::SEVERITY_ERROR);857return;858}859860const String theme_dir = EditorPaths::get_singleton()->get_text_editor_themes_dir();861Ref<DirAccess> d = DirAccess::open(theme_dir);862Error err = FAILED;863if (d.is_valid()) {864err = d->copy(p_file, theme_dir.path_join(p_file.get_file()));865}866867if (err != OK) {868EditorToaster::get_singleton()->popup_str(TTR("Importing theme failed. Failed to copy theme file."), EditorToaster::SEVERITY_ERROR);869return;870}871872// Reload themes and switch to new theme.873EditorSettings::get_singleton()->update_text_editor_themes_list();874EditorSettings::get_singleton()->set_manually("text_editor/theme/color_theme", theme_name, true);875EditorSettings::get_singleton()->notify_changes();876}877878void _save_text_editor_theme_as(const String &p_file) {879String file = p_file;880if (p_file.get_extension() != "tet") {881file += ".tet";882}883884const String theme_name = file.get_file().get_basename();885if (EditorSettings::is_default_text_editor_theme(theme_name.to_lower())) {886EditorToaster::get_singleton()->popup_str(TTR("Saving theme failed. File name cannot be 'Default', 'Custom', or 'Godot 2'."), EditorToaster::SEVERITY_ERROR);887return;888}889890const String theme_section = "color_theme";891const Ref<ConfigFile> cf = memnew(ConfigFile);892893// Use the keys from the Godot 2 theme to know which settings to save.894HashMap<StringName, Color> text_colors = EditorSettings::get_godot2_text_editor_theme();895text_colors.sort();896for (const KeyValue<StringName, Color> &text_color : text_colors) {897const Color val = EditorSettings::get_singleton()->get_setting(text_color.key);898const String &key = text_color.key.operator String().replace("text_editor/theme/highlighting/", "");899cf->set_value(theme_section, key, val.to_html());900}901902const Error err = cf->save(file);903if (err != OK) {904EditorToaster::get_singleton()->popup_str(TTR("Saving theme failed."), EditorToaster::SEVERITY_ERROR);905return;906}907908// Reload themes and switch to saved theme.909EditorSettings::get_singleton()->update_text_editor_themes_list();910if (p_file.get_base_dir() == EditorPaths::get_singleton()->get_text_editor_themes_dir()) {911// Don't need to emit signal or notify changes as the colors are already set.912EditorSettings::get_singleton()->set_manually("text_editor/theme/color_theme", theme_name, false);913}914}915916bool ScriptEditor::_script_exists(const String &p_path) const {917if (p_path.is_empty()) {918return false;919} else if (p_path.is_resource_file()) {920return FileAccess::exists(p_path);921} else {922return FileAccess::exists(p_path.get_slice("::", 0));923}924}925926void ScriptEditor::_file_dialog_action(const String &p_file) {927switch (file_dialog_option) {928case FILE_MENU_NEW_TEXTFILE: {929Error err;930{931Ref<FileAccess> file = FileAccess::open(p_file, FileAccess::WRITE, &err);932if (err) {933EditorNode::get_singleton()->show_warning(TTR("Error writing TextFile:") + "\n" + p_file, TTR("Error!"));934break;935}936}937938if (EditorFileSystem::get_singleton()) {939if (textfile_extensions.has(p_file.get_extension())) {940EditorFileSystem::get_singleton()->update_file(p_file);941}942}943[[fallthrough]];944}945case FILE_MENU_OPEN: {946if (!is_visible_in_tree()) {947// When created from outside the editor.948EditorNode::get_singleton()->get_editor_main_screen()->select(EditorMainScreen::EDITOR_SCRIPT);949}950open_file(p_file);951} break;952case FILE_MENU_SAVE_AS: {953if (ScriptEditorBase *current = _get_current_editor()) {954Ref<Resource> resource = current->get_edited_resource();955String path = ProjectSettings::get_singleton()->localize_path(p_file);956Error err = _save_text_file(resource, path);957958if (err != OK) {959EditorNode::get_singleton()->show_accept(TTR("Error saving file!"), TTR("OK"));960return;961}962963resource->set_path(path);964_update_script_names();965}966} break;967case THEME_SAVE_AS: {968_save_text_editor_theme_as(p_file);969} break;970case THEME_IMPORT: {971_import_text_editor_theme(p_file);972} break;973}974file_dialog_option = -1;975}976977Ref<Script> ScriptEditor::_get_current_script() {978if (ScriptEditorBase *current = _get_current_editor()) {979Ref<Script> scr = current->get_edited_resource();980return scr.is_valid() ? scr : nullptr;981} else {982return nullptr;983}984}985986TypedArray<Script> ScriptEditor::_get_open_scripts() const {987TypedArray<Script> ret;988Vector<Ref<Script>> scripts = get_open_scripts();989int scripts_amount = scripts.size();990for (int idx_script = 0; idx_script < scripts_amount; idx_script++) {991ret.push_back(scripts[idx_script]);992}993return ret;994}995996bool ScriptEditor::toggle_files_panel() {997list_split->set_visible(!list_split->is_visible());998EditorSettings::get_singleton()->set_project_metadata("files_panel", "show_files_panel", list_split->is_visible());999return list_split->is_visible();1000}10011002bool ScriptEditor::is_files_panel_toggled() {1003return list_split->is_visible();1004}10051006List<String> ScriptEditor::_get_recognized_extensions() {1007List<String> extensions;1008for (const String type : { "Script", "JSON" }) {1009ResourceLoader::get_recognized_extensions_for_type(type, &extensions);1010}1011return extensions;1012}10131014void ScriptEditor::_menu_option(int p_option) {1015ScriptEditorBase *current = _get_current_editor();1016switch (p_option) {1017case FILE_MENU_NEW_SCRIPT: {1018script_create_dialog->config("Node", "new_script", false, false);1019script_create_dialog->popup_centered();1020} break;1021case FILE_MENU_NEW_TEXTFILE: {1022file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE);1023file_dialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM);1024file_dialog_option = FILE_MENU_NEW_TEXTFILE;10251026file_dialog->clear_filters();1027for (const String &E : textfile_extensions) {1028file_dialog->add_filter("*." + E, E.to_upper());1029}1030file_dialog->set_title(TTRC("New Text File..."));1031file_dialog->popup_file_dialog();1032} break;1033case FILE_MENU_OPEN: {1034file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE);1035file_dialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM);1036file_dialog_option = FILE_MENU_OPEN;10371038List<String> extensions;1039ResourceLoader::get_recognized_extensions_for_type("Script", &extensions);1040file_dialog->clear_filters();1041for (const String &extension : extensions) {1042file_dialog->add_filter("*." + extension, extension.to_upper());1043}10441045for (const String &E : textfile_extensions) {1046file_dialog->add_filter("*." + E, E.to_upper());1047}10481049file_dialog->set_title(TTRC("Open File"));1050file_dialog->popup_file_dialog();1051return;1052} break;1053case FILE_MENU_REOPEN_CLOSED: {1054if (previous_scripts.is_empty()) {1055return;1056}10571058String path = previous_scripts.back()->get();1059previous_scripts.pop_back();10601061bool built_in = !path.is_resource_file();10621063if (_get_recognized_extensions().find(path.get_extension()) || built_in) {1064if (built_in) {1065String res_path = path.get_slice("::", 0);1066EditorNode::get_singleton()->load_scene_or_resource(res_path, false, false);1067}10681069Ref<Resource> scr = ResourceLoader::load(path);1070if (scr.is_null()) {1071EditorNode::get_singleton()->show_warning(TTR("Could not load file at:") + "\n\n" + path, TTR("Error!"));1072file_dialog_option = -1;1073return;1074}10751076edit(scr);1077file_dialog_option = -1;1078} else {1079Error error;1080Ref<TextFile> text_file = _load_text_file(path, &error);1081if (error != OK) {1082EditorNode::get_singleton()->show_warning(TTR("Could not load file at:") + "\n\n" + path, TTR("Error!"));1083}10841085if (text_file.is_valid()) {1086edit(text_file);1087file_dialog_option = -1;1088}1089}1090} break;1091case FILE_MENU_SAVE_ALL: {1092if (_test_script_times_on_disk()) {1093return;1094}10951096save_all_scripts();1097} break;1098case SEARCH_IN_FILES: {1099open_find_in_files_dialog("");1100} break;1101case REPLACE_IN_FILES: {1102_on_replace_in_files_requested("");1103} break;1104case SEARCH_HELP: {1105help_search_dialog->popup_dialog();1106} break;1107case SEARCH_WEBSITE: {1108Control *tab = tab_container->get_current_tab_control();11091110EditorHelp *eh = Object::cast_to<EditorHelp>(tab);1111bool native_class_doc = false;1112if (eh) {1113const HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(eh->get_class());1114native_class_doc = E && !E->value.is_script_doc;1115}1116if (native_class_doc) {1117String name = eh->get_class().to_lower();1118String doc_url = vformat(GODOT_VERSION_DOCS_URL "/classes/class_%s.html", name);1119OS::get_singleton()->shell_open(doc_url);1120} else {1121OS::get_singleton()->shell_open(GODOT_VERSION_DOCS_URL "/");1122}1123} break;1124case FILE_MENU_HISTORY_NEXT: {1125_history_forward();1126} break;1127case FILE_MENU_HISTORY_PREV: {1128_history_back();1129} break;1130case FILE_MENU_SORT: {1131_sort_list_on_update = true;1132_update_script_names();1133} break;1134case FILE_MENU_TOGGLE_FILES_PANEL: {1135toggle_files_panel();1136if (current) {1137current->update_toggle_files_button();1138} else {1139Control *tab = tab_container->get_current_tab_control();1140if (EditorHelp *editor_help = Object::cast_to<EditorHelp>(tab)) {1141editor_help->update_toggle_files_button();1142}1143}1144}1145}11461147if (p_option >= EditorContextMenuPlugin::BASE_ID) {1148Ref<Resource> resource;1149if (current) {1150resource = current->get_edited_resource();1151}1152EditorContextMenuPluginManager::get_singleton()->activate_custom_option(EditorContextMenuPlugin::CONTEXT_SLOT_SCRIPT_EDITOR, p_option, resource);1153return;1154}11551156if (current) {1157switch (p_option) {1158case FILE_MENU_SAVE: {1159save_current_script();1160} break;1161case FILE_MENU_SAVE_AS: {1162_auto_format_text(current);11631164Ref<Resource> resource = current->get_edited_resource();1165Ref<TextFile> text_file = resource;1166Ref<Script> scr = resource;11671168if (text_file.is_valid()) {1169file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE);1170file_dialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM);1171file_dialog_option = FILE_MENU_SAVE_AS;11721173List<String> extensions;1174ResourceLoader::get_recognized_extensions_for_type("Script", &extensions);1175file_dialog->clear_filters();1176file_dialog->set_current_dir(text_file->get_path().get_base_dir());1177file_dialog->set_current_file(text_file->get_path().get_file());1178file_dialog->set_title(TTRC("Save File As..."));1179file_dialog->popup_file_dialog();1180break;1181}11821183if (scr.is_valid()) {1184clear_docs_from_script(scr);1185}11861187EditorNode::get_singleton()->push_item(resource.ptr());1188EditorNode::get_singleton()->save_resource_as(resource);11891190if (scr.is_valid()) {1191update_docs_from_script(scr);1192}1193} break;11941195case FILE_MENU_SOFT_RELOAD_TOOL: {1196Ref<Script> scr = current->get_edited_resource();1197if (scr.is_null()) {1198EditorNode::get_singleton()->show_warning(TTR("Can't obtain the script for reloading."));1199break;1200}1201if (!scr->is_tool()) {1202EditorNode::get_singleton()->show_warning(TTR("Reload only takes effect on tool scripts."));1203return;1204}1205scr->reload(true);12061207} break;12081209case FILE_MENU_RUN: {1210Ref<Script> scr = current->get_edited_resource();1211if (scr.is_null()) {1212EditorToaster::get_singleton()->popup_str(TTR("Cannot run the edited file because it's not a script."), EditorToaster::SEVERITY_WARNING);1213break;1214}12151216current->apply_code();12171218EditorNode::get_singleton()->run_editor_script(scr);1219} break;12201221case FILE_MENU_CLOSE: {1222ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(current);1223if (seb && seb->is_unsaved()) {1224_ask_close_current_unsaved_tab(seb);1225} else {1226_close_current_tab(false);1227}1228} break;1229case FILE_MENU_COPY_PATH: {1230_copy_script_path();1231} break;1232case FILE_MENU_COPY_UID: {1233_copy_script_uid();1234} break;1235case FILE_MENU_SHOW_IN_FILE_SYSTEM: {1236const Ref<Resource> scr = current->get_edited_resource();1237String path = scr->get_path();1238if (!path.is_empty()) {1239if (scr->is_built_in()) {1240path = path.get_slice("::", 0); // Show the scene instead.1241}12421243FileSystemDock::get_singleton()->navigate_to_path(path);1244}1245} break;1246case FILE_MENU_CLOSE_DOCS: {1247_close_docs_tab();1248} break;1249case FILE_MENU_CLOSE_OTHER_TABS: {1250_close_other_tabs();1251} break;1252case FILE_MENU_CLOSE_TABS_BELOW: {1253_close_tabs_below();1254} break;1255case FILE_MENU_CLOSE_ALL: {1256_close_all_tabs();1257} break;1258case FILE_MENU_MOVE_UP: {1259if (tab_container->get_current_tab() > 0) {1260tab_container->move_child(current, tab_container->get_current_tab() - 1);1261tab_container->set_current_tab(tab_container->get_current_tab());1262_update_script_names();1263}1264} break;1265case FILE_MENU_MOVE_DOWN: {1266if (tab_container->get_current_tab() < tab_container->get_tab_count() - 1) {1267tab_container->move_child(current, tab_container->get_current_tab() + 1);1268tab_container->set_current_tab(tab_container->get_current_tab());1269_update_script_names();1270}1271} break;1272}1273} else {1274if (EditorHelp *help = Object::cast_to<EditorHelp>(tab_container->get_current_tab_control())) {1275switch (p_option) {1276case HELP_SEARCH_FIND: {1277help->popup_search();1278} break;1279case HELP_SEARCH_FIND_NEXT: {1280help->search_again();1281} break;1282case HELP_SEARCH_FIND_PREVIOUS: {1283help->search_again(true);1284} break;1285case FILE_MENU_CLOSE: {1286_close_current_tab();1287} break;1288case FILE_MENU_CLOSE_DOCS: {1289_close_docs_tab();1290} break;1291case FILE_MENU_CLOSE_OTHER_TABS: {1292_close_other_tabs();1293} break;1294case FILE_MENU_CLOSE_TABS_BELOW: {1295_close_tabs_below();1296} break;1297case FILE_MENU_CLOSE_ALL: {1298_close_all_tabs();1299} break;1300case FILE_MENU_MOVE_UP: {1301if (tab_container->get_current_tab() > 0) {1302tab_container->move_child(help, tab_container->get_current_tab() - 1);1303tab_container->set_current_tab(tab_container->get_current_tab());1304_update_script_names();1305}1306} break;1307case FILE_MENU_MOVE_DOWN: {1308if (tab_container->get_current_tab() < tab_container->get_tab_count() - 1) {1309tab_container->move_child(help, tab_container->get_current_tab() + 1);1310tab_container->set_current_tab(tab_container->get_current_tab());1311_update_script_names();1312}1313} break;1314}1315}1316}1317}13181319void ScriptEditor::_theme_option(int p_option) {1320switch (p_option) {1321case THEME_IMPORT: {1322file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE);1323file_dialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM);1324file_dialog_option = THEME_IMPORT;1325file_dialog->clear_filters();1326file_dialog->add_filter("*.tet");1327file_dialog->set_title(TTRC("Import Theme"));1328file_dialog->popup_file_dialog();1329} break;1330case THEME_RELOAD: {1331EditorSettings::get_singleton()->mark_setting_changed("text_editor/theme/color_theme");1332EditorSettings::get_singleton()->notify_changes();1333} break;1334case THEME_SAVE_AS: {1335ScriptEditor::_show_save_theme_as_dialog();1336} break;1337}1338}13391340void ScriptEditor::_show_save_theme_as_dialog() {1341file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE);1342file_dialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM);1343file_dialog_option = THEME_SAVE_AS;1344file_dialog->clear_filters();1345file_dialog->add_filter("*.tet");1346file_dialog->set_current_path(EditorPaths::get_singleton()->get_text_editor_themes_dir().path_join(EDITOR_GET("text_editor/theme/color_theme")) + " New");1347file_dialog->set_title(TTRC("Save Theme As..."));1348file_dialog->popup_file_dialog();1349}13501351bool ScriptEditor::_has_docs_tab() const {1352const int child_count = tab_container->get_tab_count();1353for (int i = 0; i < child_count; i++) {1354if (Object::cast_to<EditorHelp>(tab_container->get_tab_control(i))) {1355return true;1356}1357}1358return false;1359}13601361bool ScriptEditor::_has_script_tab() const {1362const int child_count = tab_container->get_tab_count();1363for (int i = 0; i < child_count; i++) {1364if (Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i))) {1365return true;1366}1367}1368return false;1369}13701371void ScriptEditor::_prepare_file_menu() {1372PopupMenu *menu = file_menu->get_popup();1373ScriptEditorBase *editor = _get_current_editor();1374const Ref<Resource> res = editor ? editor->get_edited_resource() : Ref<Resource>();13751376menu->set_item_disabled(menu->get_item_index(FILE_MENU_REOPEN_CLOSED), previous_scripts.is_empty());13771378menu->set_item_disabled(menu->get_item_index(FILE_MENU_SAVE), res.is_null());1379menu->set_item_disabled(menu->get_item_index(FILE_MENU_SAVE_AS), res.is_null());1380menu->set_item_disabled(menu->get_item_index(FILE_MENU_SAVE_ALL), !_has_script_tab());13811382menu->set_item_disabled(menu->get_item_index(FILE_MENU_SOFT_RELOAD_TOOL), res.is_null());1383menu->set_item_disabled(menu->get_item_index(FILE_MENU_COPY_PATH), res.is_null() || res->get_path().is_empty());1384menu->set_item_disabled(menu->get_item_index(FILE_MENU_COPY_UID), res.is_null() || ResourceLoader::get_resource_uid(res->get_path()) == ResourceUID::INVALID_ID);1385menu->set_item_disabled(menu->get_item_index(FILE_MENU_SHOW_IN_FILE_SYSTEM), res.is_null());13861387menu->set_item_disabled(menu->get_item_index(FILE_MENU_HISTORY_PREV), history_pos <= 0);1388menu->set_item_disabled(menu->get_item_index(FILE_MENU_HISTORY_NEXT), history_pos >= history.size() - 1);13891390menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE), tab_container->get_tab_count() < 1);1391menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE_ALL), tab_container->get_tab_count() < 1);1392menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE_OTHER_TABS), tab_container->get_tab_count() <= 1);1393menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE_TABS_BELOW), tab_container->get_current_tab() >= tab_container->get_tab_count() - 1);1394menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE_DOCS), !_has_docs_tab());13951396menu->set_item_disabled(menu->get_item_index(FILE_MENU_RUN), res.is_null());1397}13981399void ScriptEditor::_file_menu_closed() {1400PopupMenu *menu = file_menu->get_popup();14011402menu->set_item_disabled(menu->get_item_index(FILE_MENU_REOPEN_CLOSED), false);14031404menu->set_item_disabled(menu->get_item_index(FILE_MENU_SAVE), false);1405menu->set_item_disabled(menu->get_item_index(FILE_MENU_SAVE_AS), false);1406menu->set_item_disabled(menu->get_item_index(FILE_MENU_SAVE_ALL), false);14071408menu->set_item_disabled(menu->get_item_index(FILE_MENU_SOFT_RELOAD_TOOL), false);1409menu->set_item_disabled(menu->get_item_index(FILE_MENU_COPY_PATH), false);1410menu->set_item_disabled(menu->get_item_index(FILE_MENU_SHOW_IN_FILE_SYSTEM), false);14111412menu->set_item_disabled(menu->get_item_index(FILE_MENU_HISTORY_PREV), false);1413menu->set_item_disabled(menu->get_item_index(FILE_MENU_HISTORY_NEXT), false);14141415menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE), false);1416menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE_ALL), false);1417menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE_OTHER_TABS), false);1418menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE_DOCS), false);14191420menu->set_item_disabled(menu->get_item_index(FILE_MENU_RUN), false);1421}14221423void ScriptEditor::_tab_changed(int p_which) {1424ensure_select_current();1425}14261427void ScriptEditor::_notification(int p_what) {1428switch (p_what) {1429case NOTIFICATION_ENTER_TREE: {1430_apply_editor_settings();1431[[fallthrough]];1432}14331434case NOTIFICATION_TRANSLATION_CHANGED: {1435_update_online_doc();1436if (!make_floating->is_disabled()) {1437// Override default ScreenSelect tooltip if multi-window support is available.1438make_floating->set_tooltip_text(TTR("Make the script editor floating.") + "\n" + TTR("Right-click to open the screen selector."));1439}1440[[fallthrough]];1441}1442case NOTIFICATION_LAYOUT_DIRECTION_CHANGED:1443case NOTIFICATION_THEME_CHANGED: {1444tab_container->add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SNAME("ScriptEditor"), EditorStringName(EditorStyles)));14451446_calculate_script_name_button_size();14471448help_search->set_button_icon(get_editor_theme_icon(SNAME("HelpSearch")));1449site_search->set_button_icon(get_editor_theme_icon(SNAME("ExternalLink")));14501451if (is_layout_rtl()) {1452script_forward->set_button_icon(get_editor_theme_icon(SNAME("Back")));1453script_back->set_button_icon(get_editor_theme_icon(SNAME("Forward")));1454} else {1455script_forward->set_button_icon(get_editor_theme_icon(SNAME("Forward")));1456script_back->set_button_icon(get_editor_theme_icon(SNAME("Back")));1457}14581459members_overview_alphabeta_sort_button->set_button_icon(get_editor_theme_icon(SNAME("Sort")));14601461filter_scripts->set_right_icon(get_editor_theme_icon(SNAME("Search")));1462filter_methods->set_right_icon(get_editor_theme_icon(SNAME("Search")));14631464recent_scripts->reset_size();1465script_list->set_fixed_icon_size(Vector2i(1, 1) * get_theme_constant("class_icon_size", EditorStringName(Editor)));14661467if (is_inside_tree()) {1468_update_script_names();1469}1470} break;14711472case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {1473if (EditorThemeManager::is_generated_theme_outdated() ||1474EditorSettings::get_singleton()->check_changed_settings_in_group("interface/editor") ||1475EditorSettings::get_singleton()->check_changed_settings_in_group("text_editor") ||1476EditorSettings::get_singleton()->check_changed_settings_in_group("docks/filesystem")) {1477_apply_editor_settings();1478}1479} break;14801481case NOTIFICATION_READY: {1482// Can't set own styles in NOTIFICATION_THEME_CHANGED, so for now this will do.1483add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SNAME("ScriptEditorPanel"), EditorStringName(EditorStyles)));14841485get_tree()->connect("tree_changed", callable_mp(this, &ScriptEditor::_tree_changed));1486InspectorDock::get_singleton()->connect("request_help", callable_mp(this, &ScriptEditor::_help_class_open));1487EditorNode::get_singleton()->connect("request_help_search", callable_mp(this, &ScriptEditor::_help_search));1488EditorNode::get_singleton()->connect("scene_closed", callable_mp(this, &ScriptEditor::_close_builtin_scripts_from_scene));1489EditorNode::get_singleton()->connect("script_add_function_request", callable_mp(this, &ScriptEditor::_add_callback));1490EditorNode::get_singleton()->connect("resource_saved", callable_mp(this, &ScriptEditor::_res_saved_callback));1491EditorNode::get_singleton()->connect("scene_saved", callable_mp(this, &ScriptEditor::_scene_saved_callback));1492FileSystemDock::get_singleton()->connect("files_moved", callable_mp(this, &ScriptEditor::_files_moved));1493FileSystemDock::get_singleton()->connect("file_removed", callable_mp(this, &ScriptEditor::_file_removed));1494script_list->connect(SceneStringName(item_selected), callable_mp(this, &ScriptEditor::_script_selected));14951496members_overview->connect(SceneStringName(item_selected), callable_mp(this, &ScriptEditor::_members_overview_selected));1497help_overview->connect(SceneStringName(item_selected), callable_mp(this, &ScriptEditor::_help_overview_selected));1498script_split->connect("dragged", callable_mp(this, &ScriptEditor::_split_dragged));1499list_split->connect("dragged", callable_mp(this, &ScriptEditor::_split_dragged));15001501EditorFileSystem::get_singleton()->connect("filesystem_changed", callable_mp(this, &ScriptEditor::_filesystem_changed));1502#ifdef ANDROID_ENABLED1503set_process(true);1504#endif1505} break;15061507#ifdef ANDROID_ENABLED1508case NOTIFICATION_VISIBILITY_CHANGED: {1509set_process(is_visible_in_tree());1510} break;15111512case NOTIFICATION_PROCESS: {1513const int kb_height = DisplayServer::get_singleton()->virtual_keyboard_get_height();1514if (kb_height == last_kb_height) {1515break;1516}15171518last_kb_height = kb_height;1519float spacer_height = 0.0f;1520const float status_bar_height = 28 * EDSCALE; // Magic number1521const bool kb_visible = kb_height > 0;15221523if (kb_visible) {1524if (TextEditorBase *editor = Object::cast_to<TextEditorBase>(_get_current_editor())) {1525if (CodeTextEditor *code_editor = editor->get_code_editor()) {1526if (CodeEdit *text_editor = code_editor->get_text_editor()) {1527if (!text_editor->has_focus()) {1528break;1529}1530text_editor->adjust_viewport_to_caret();1531}1532}1533}15341535const float control_bottom = get_global_position().y + get_size().y;1536const float extra_bottom = get_viewport_rect().size.y - control_bottom;1537spacer_height = float(kb_height) - extra_bottom - status_bar_height;15381539if (spacer_height < 0.0f) {1540spacer_height = 0.0f;1541}1542}15431544virtual_keyboard_spacer->set_custom_minimum_size(Size2(0, spacer_height));1545EditorSceneTabs::get_singleton()->set_visible(!kb_height);1546menu_hb->set_visible(!kb_visible);1547} break;1548#endif1549case NOTIFICATION_APPLICATION_FOCUS_IN: {1550if (is_inside_tree()) {1551_test_script_times_on_disk();1552_update_modified_scripts_for_external_editor();1553}1554} break;1555}1556}15571558void ScriptEditor::_close_builtin_scripts_from_scene(const String &p_scene) {1559for (int i = 0; i < tab_container->get_tab_count(); i++) {1560if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i))) {1561Ref<Script> scr = seb->get_edited_resource();1562if (scr.is_null()) {1563continue;1564}15651566if (scr->is_built_in() && scr->get_path().begins_with(p_scene)) { // Is an internal script and belongs to scene being closed.1567_close_tab(i, false);1568i--;1569}1570}1571}1572}15731574void ScriptEditor::edited_scene_changed() {1575_update_modified_scripts_for_external_editor();1576}15771578void ScriptEditor::notify_script_close(const Ref<Script> &p_script) {1579emit_signal(SNAME("script_close"), p_script);1580}15811582void ScriptEditor::notify_script_changed(const Ref<Script> &p_script) {1583emit_signal(SNAME("editor_script_changed"), p_script);1584}15851586Vector<String> ScriptEditor::_get_breakpoints() {1587List<String> bpoints_list;1588get_breakpoints(&bpoints_list);15891590Vector<String> ret;1591for (const String &E : bpoints_list) {1592ret.push_back(E);1593}15941595return ret;1596}15971598void ScriptEditor::get_breakpoints(List<String> *p_breakpoints) {1599HashSet<String> loaded_scripts;1600for (int i = 0; i < tab_container->get_tab_count(); i++) {1601CodeEditorBase *ceb = Object::cast_to<CodeEditorBase>(tab_container->get_tab_control(i));1602if (!ceb) {1603continue;1604}16051606Ref<Script> scr = ceb->get_edited_resource();1607if (scr.is_null()) {1608continue;1609}16101611String base = scr->get_path();1612loaded_scripts.insert(base);1613if (base.is_empty() || base.begins_with("local://")) {1614continue;1615}16161617PackedInt32Array bpoints = ceb->get_breakpoints();1618for (int32_t bpoint : bpoints) {1619p_breakpoints->push_back(base + ":" + itos((int)bpoint + 1));1620}1621}16221623// Load breakpoints that are in closed scripts.1624Vector<String> cached_editors = script_editor_cache->get_sections();1625for (const String &E : cached_editors) {1626if (loaded_scripts.has(E)) {1627continue;1628}16291630Array breakpoints = _get_cached_breakpoints_for_script(E);1631for (int breakpoint : breakpoints) {1632p_breakpoints->push_back(E + ":" + itos((int)breakpoint + 1));1633}1634}1635}16361637void ScriptEditor::_members_overview_selected(int p_idx) {1638int line = members_overview->get_item_metadata(p_idx);1639ScriptEditorBase *current = _get_current_editor();1640if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(current)) {1641teb->goto_line_centered(line);1642}1643}16441645void ScriptEditor::_help_overview_selected(int p_idx) {1646Node *current = tab_container->get_tab_control(tab_container->get_current_tab());1647if (EditorHelp *eh = Object::cast_to<EditorHelp>(current)) {1648eh->scroll_to_section(help_overview->get_item_metadata(p_idx));1649}1650}16511652void ScriptEditor::_script_selected(int p_idx) {1653grab_focus_block = !Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT); //amazing hack, simply amazing16541655_go_to_tab(script_list->get_item_metadata(p_idx));1656script_name_button->set_text(script_list->get_item_text(p_idx));1657_calculate_script_name_button_size();1658grab_focus_block = false;1659}16601661void ScriptEditor::ensure_select_current() {1662if (tab_container->get_tab_count() && tab_container->get_current_tab() >= 0) {1663if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(_get_current_editor())) {1664teb->enable_editor();1665if (teb && !grab_focus_block && is_visible_in_tree()) {1666teb->ensure_focus();1667}1668}1669}1670_update_find_replace_bar();16711672_update_selected_editor_menu();1673}16741675bool ScriptEditor::is_editor_floating() {1676return is_floating;1677}16781679void ScriptEditor::_find_scripts(Node *p_base, Node *p_current, HashSet<Ref<Script>> &used) {1680if (p_current != p_base && p_current->get_owner() != p_base) {1681return;1682}16831684if (p_current->get_script_instance()) {1685Ref<Script> scr = p_current->get_script();1686if (scr.is_valid()) {1687used.insert(scr);1688}1689}16901691for (int i = 0; i < p_current->get_child_count(); i++) {1692_find_scripts(p_base, p_current->get_child(i), used);1693}1694}16951696struct _ScriptEditorItemData {1697String name;1698String sort_key;1699Ref<Texture2D> icon;1700bool tool = false;1701int index = 0;1702String tooltip;1703bool used = false;1704int category = 0;1705Node *ref = nullptr;17061707bool operator<(const _ScriptEditorItemData &id) const {1708if (category == id.category) {1709if (sort_key == id.sort_key) {1710return index < id.index;1711} else {1712return sort_key.filenocasecmp_to(id.sort_key) < 0;1713}1714} else {1715return category < id.category;1716}1717}1718};17191720void ScriptEditor::_update_members_overview_visibility() {1721ScriptEditorBase *seb = _get_current_editor();1722if (!seb) {1723members_overview_alphabeta_sort_button->set_visible(false);1724members_overview->set_visible(false);17251726Node *current = tab_container->get_tab_control(tab_container->get_current_tab());1727EditorHelp *editor_help = Object::cast_to<EditorHelp>(current);1728overview_vbox->set_visible(help_overview_enabled && editor_help);1729return;1730}17311732if (members_overview_enabled && seb->show_members_overview()) {1733members_overview_alphabeta_sort_button->set_visible(true);1734filter_methods->set_visible(true);1735members_overview->set_visible(true);1736overview_vbox->set_visible(true);1737} else {1738members_overview_alphabeta_sort_button->set_visible(false);1739filter_methods->set_visible(false);1740members_overview->set_visible(false);1741overview_vbox->set_visible(false);1742}1743}17441745void ScriptEditor::_toggle_members_overview_alpha_sort(bool p_alphabetic_sort) {1746EditorSettings::get_singleton()->set("text_editor/script_list/sort_members_outline_alphabetically", p_alphabetic_sort);1747_update_members_overview();1748}17491750void ScriptEditor::_update_members_overview() {1751members_overview->clear();17521753CodeEditorBase *ceb = Object::cast_to<CodeEditorBase>(_get_current_editor());1754if (!ceb) {1755return;1756}17571758Vector<String> functions = ceb->get_functions();1759if (EDITOR_GET("text_editor/script_list/sort_members_outline_alphabetically")) {1760functions.sort();1761}17621763String filter = filter_methods->get_text();1764if (filter.is_empty()) {1765for (int i = 0; i < functions.size(); i++) {1766String name = functions[i].get_slicec(':', 0);1767members_overview->add_item(name);1768members_overview->set_item_metadata(-1, functions[i].get_slicec(':', 1).to_int() - 1);1769}1770} else {1771PackedStringArray search_names;1772for (int i = 0; i < functions.size(); i++) {1773search_names.append(functions[i].get_slicec(':', 0));1774}17751776Vector<FuzzySearchResult> results;1777FuzzySearch fuzzy;1778fuzzy.set_query(filter, false);1779fuzzy.search_all(search_names, results);17801781for (const FuzzySearchResult &res : results) {1782String name = functions[res.original_index].get_slicec(':', 0);1783int line = functions[res.original_index].get_slicec(':', 1).to_int() - 1;1784members_overview->add_item(name);1785members_overview->set_item_metadata(-1, line);1786}1787}1788}17891790void ScriptEditor::_update_help_overview_visibility() {1791int selected = tab_container->get_current_tab();1792if (selected < 0 || selected >= tab_container->get_tab_count()) {1793help_overview->set_visible(false);1794return;1795}17961797Node *current = tab_container->get_tab_control(tab_container->get_current_tab());1798if (!Object::cast_to<EditorHelp>(current)) {1799help_overview->set_visible(false);1800return;1801}18021803if (help_overview_enabled) {1804members_overview_alphabeta_sort_button->set_visible(false);1805filter_methods->set_visible(false);1806help_overview->set_visible(true);1807overview_vbox->set_visible(true);1808} else {1809help_overview->set_visible(false);1810overview_vbox->set_visible(false);1811}1812}18131814void ScriptEditor::_update_help_overview() {1815help_overview->clear();18161817int selected = tab_container->get_current_tab();1818if (selected < 0 || selected >= tab_container->get_tab_count()) {1819return;1820}18211822Node *current = tab_container->get_tab_control(tab_container->get_current_tab());1823if (EditorHelp *eh = Object::cast_to<EditorHelp>(current)) {1824Vector<Pair<String, int>> sections = eh->get_sections();1825for (int i = 0; i < sections.size(); i++) {1826help_overview->add_item(sections[i].first);1827help_overview->set_item_metadata(i, sections[i].second);1828}1829}1830}18311832void ScriptEditor::_update_online_doc() {1833Node *current = tab_container->get_tab_control(tab_container->get_current_tab());18341835EditorHelp *eh = Object::cast_to<EditorHelp>(current);1836bool native_class_doc = false;1837if (eh) {1838const HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(eh->get_class());1839native_class_doc = E && !E->value.is_script_doc;1840}1841if (native_class_doc) {1842String name = eh->get_class();1843String tooltip = vformat(TTR("Open '%s' in Godot online documentation."), name);1844site_search->set_text(TTRC("Open in Online Docs"));1845site_search->set_tooltip_text(tooltip);1846} else {1847site_search->set_text(TTRC("Online Docs"));1848site_search->set_tooltip_text(TTRC("Open Godot online documentation."));1849}1850}18511852void ScriptEditor::_update_script_colors() {1853bool script_temperature_enabled = EDITOR_GET("text_editor/script_list/script_temperature_enabled");18541855int hist_size = EDITOR_GET("text_editor/script_list/script_temperature_history_size");1856Color hot_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));1857hot_color.set_s(hot_color.get_s() * 0.9);1858Color cold_color = get_theme_color(SceneStringName(font_color), EditorStringName(Editor));18591860for (int i = 0; i < script_list->get_item_count(); i++) {1861int c = script_list->get_item_metadata(i);1862Node *n = tab_container->get_tab_control(c);1863if (!n) {1864continue;1865}18661867if (script_temperature_enabled) {1868int pass = n->get_meta("__editor_pass", -1);1869if (pass < 0) {1870continue;1871}18721873int h = edit_pass - pass;1874if (h > hist_size) {1875continue;1876}1877int non_zero_hist_size = (hist_size == 0) ? 1 : hist_size;1878float v = Math::ease((edit_pass - pass) / float(non_zero_hist_size), 0.4);18791880script_list->set_item_custom_fg_color(i, hot_color.lerp(cold_color, v));1881}1882}1883}18841885void ScriptEditor::_update_script_names() {1886if (restoring_layout) {1887return;1888}18891890HashSet<Ref<Script>> used;1891Node *edited = EditorNode::get_singleton()->get_edited_scene();1892if (edited && EDITOR_GET("text_editor/script_list/highlight_scene_scripts")) {1893_find_scripts(edited, edited, used);1894}18951896script_list->clear();1897bool split_script_help = EDITOR_GET("text_editor/script_list/group_help_pages");1898ScriptSortBy sort_by = (ScriptSortBy)(int)EDITOR_GET("text_editor/script_list/sort_scripts_by");1899ScriptListName display_as = (ScriptListName)(int)EDITOR_GET("text_editor/script_list/list_script_names_as");19001901Vector<_ScriptEditorItemData> sedata;19021903for (int i = 0; i < tab_container->get_tab_count(); i++) {1904if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i))) {1905Ref<Texture2D> icon = seb->get_theme_icon();1906String path = seb->get_edited_resource()->get_path();1907bool saved = !path.is_empty();1908String name = seb->get_name();1909Ref<Script> scr = seb->get_edited_resource();19101911_ScriptEditorItemData sd;1912sd.icon = icon;1913sd.name = name;1914sd.tooltip = saved ? path : TTR("Unsaved file.");1915sd.index = i;1916sd.used = used.has(seb->get_edited_resource());1917sd.category = 0;1918sd.ref = seb;1919if (scr.is_valid()) {1920sd.tool = scr->is_tool();1921}19221923switch (sort_by) {1924case SORT_BY_NAME: {1925sd.sort_key = name.to_lower();1926} break;1927case SORT_BY_PATH: {1928sd.sort_key = path;1929} break;1930case SORT_BY_NONE: {1931sd.sort_key = "";1932} break;1933}19341935switch (display_as) {1936case DISPLAY_NAME: {1937sd.name = name;1938} break;1939case DISPLAY_DIR_AND_NAME: {1940if (!path.get_base_dir().get_file().is_empty()) {1941sd.name = path.get_base_dir().get_file().path_join(name);1942} else {1943sd.name = name;1944}1945} break;1946case DISPLAY_FULL_PATH: {1947sd.name = path;1948} break;1949}1950if (!saved) {1951sd.name = seb->get_name();1952}19531954sedata.push_back(sd);1955}19561957EditorHelp *eh = Object::cast_to<EditorHelp>(tab_container->get_tab_control(i));1958if (eh && !eh->get_class().is_empty()) {1959String name = eh->get_class().unquote();1960Ref<Texture2D> icon = get_editor_theme_icon(SNAME("Help"));1961String tooltip = vformat(TTR("%s Class Reference"), name);19621963_ScriptEditorItemData sd;1964sd.icon = icon;1965sd.name = name;1966sd.sort_key = name.to_lower();1967sd.tooltip = tooltip;1968sd.index = i;1969sd.used = false;1970sd.category = split_script_help ? 1 : 0;1971sd.ref = eh;19721973sedata.push_back(sd);1974}1975}19761977Vector<String> disambiguated_script_names;1978Vector<String> full_script_paths;1979for (int j = 0; j < sedata.size(); j++) {1980String name = sedata[j].name.replace("(*)", "");1981ScriptListName script_display = (ScriptListName)(int)EDITOR_GET("text_editor/script_list/list_script_names_as");1982switch (script_display) {1983case DISPLAY_NAME: {1984name = name.get_file();1985} break;1986case DISPLAY_DIR_AND_NAME: {1987name = name.get_base_dir().get_file().path_join(name.get_file());1988} break;1989default:1990break;1991}19921993disambiguated_script_names.append(name);1994full_script_paths.append(sedata[j].tooltip);1995}19961997EditorNode::disambiguate_filenames(full_script_paths, disambiguated_script_names);19981999for (int j = 0; j < sedata.size(); j++) {2000if (sedata[j].name.ends_with("(*)")) {2001sedata.write[j].name = disambiguated_script_names[j] + "(*)";2002} else {2003sedata.write[j].name = disambiguated_script_names[j];2004}2005}20062007if (_sort_list_on_update && !sedata.is_empty()) {2008sedata.sort();20092010// change actual order of tab_container so that the order can be rearranged by user2011int cur_tab = tab_container->get_current_tab();2012int prev_tab = tab_container->get_previous_tab();2013int new_cur_tab = -1;2014int new_prev_tab = -1;2015for (int i = 0; i < sedata.size(); i++) {2016tab_container->move_child(sedata[i].ref, i);2017if (new_prev_tab == -1 && sedata[i].index == prev_tab) {2018new_prev_tab = i;2019}2020if (new_cur_tab == -1 && sedata[i].index == cur_tab) {2021new_cur_tab = i;2022}2023// Update index of sd entries for sorted order2024_ScriptEditorItemData sd = sedata[i];2025sd.index = i;2026sedata.set(i, sd);2027}20282029lock_history = true;2030_go_to_tab(new_prev_tab);2031_go_to_tab(new_cur_tab);2032lock_history = false;2033_sort_list_on_update = false;2034}20352036Vector<_ScriptEditorItemData> sedata_filtered;20372038String filter = filter_scripts->get_text();20392040if (filter.is_empty()) {2041sedata_filtered = sedata;2042} else {2043PackedStringArray search_names;2044for (int i = 0; i < sedata.size(); i++) {2045search_names.append(sedata[i].name);2046}20472048Vector<FuzzySearchResult> results;2049FuzzySearch fuzzy;2050fuzzy.set_query(filter, false);2051fuzzy.search_all(search_names, results);20522053for (const FuzzySearchResult &res : results) {2054sedata_filtered.push_back(sedata[res.original_index]);2055}2056}20572058Color tool_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));2059tool_color.set_s(tool_color.get_s() * 1.5);2060for (int i = 0; i < sedata_filtered.size(); i++) {2061script_list->add_item(sedata_filtered[i].name, sedata_filtered[i].icon);2062if (sedata_filtered[i].tool) {2063script_list->set_item_icon_modulate(-1, tool_color);2064}20652066int index = script_list->get_item_count() - 1;2067script_list->set_item_tooltip(index, sedata_filtered[i].tooltip);2068script_list->set_item_metadata(index, sedata_filtered[i].index); /* Saving as metadata the script's index in the tab container and not the filtered one */2069if (sedata_filtered[i].used) {2070script_list->set_item_custom_bg_color(index, Color(.5, .5, .5, .125));2071}2072if (tab_container->get_current_tab() == sedata_filtered[i].index) {2073script_list->select(index);20742075if (ScriptEditorBase *seb = _get_current_editor()) {2076if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb)) {2077teb->enable_editor();2078}2079_update_selected_editor_menu();2080}2081}2082}20832084for (const _ScriptEditorItemData &sedata_i : sedata) {2085if (tab_container->get_current_tab() == sedata_i.index) {2086script_name_button->set_text(sedata_i.name);2087_calculate_script_name_button_size();2088break;2089}2090}20912092if (!waiting_update_names) {2093_update_members_overview();2094_update_help_overview();2095} else {2096waiting_update_names = false;2097}2098_update_members_overview_visibility();2099_update_help_overview_visibility();2100_update_script_colors();2101}21022103Ref<TextFile> ScriptEditor::_load_text_file(const String &p_path, Error *r_error) const {2104if (r_error) {2105*r_error = ERR_FILE_CANT_OPEN;2106}21072108String local_path = ProjectSettings::get_singleton()->localize_path(p_path);2109String path = ResourceLoader::path_remap(local_path);21102111TextFile *text_file = memnew(TextFile);2112Ref<TextFile> text_res(text_file);2113Error err = text_file->load_text(path);21142115ERR_FAIL_COND_V_MSG(err != OK, Ref<Resource>(), "Cannot load text file '" + path + "'.");21162117text_file->set_file_path(local_path);2118text_file->set_path(local_path, true);21192120if (ResourceLoader::get_timestamp_on_load()) {2121text_file->set_last_modified_time(FileAccess::get_modified_time(path));2122}21232124if (r_error) {2125*r_error = OK;2126}21272128return text_res;2129}21302131Error ScriptEditor::_save_text_file(Ref<TextFile> p_text_file, const String &p_path) {2132Ref<TextFile> sqscr = p_text_file;2133ERR_FAIL_COND_V(sqscr.is_null(), ERR_INVALID_PARAMETER);21342135String source = sqscr->get_text();21362137Error err;2138{2139Ref<FileAccess> file = FileAccess::open(p_path, FileAccess::WRITE, &err);21402141ERR_FAIL_COND_V_MSG(err, err, "Cannot save text file '" + p_path + "'.");21422143file->store_string(source);2144if (file->get_error() != OK && file->get_error() != ERR_FILE_EOF) {2145return ERR_CANT_CREATE;2146}2147}21482149if (ResourceSaver::get_timestamp_on_save()) {2150p_text_file->set_last_modified_time(FileAccess::get_modified_time(p_path));2151}21522153EditorFileSystem::get_singleton()->update_file(p_path);21542155_res_saved_callback(sqscr);2156return OK;2157}21582159bool ScriptEditor::edit(const Ref<Resource> &p_resource, int p_line, int p_col, bool p_grab_focus) {2160if (p_resource.is_null()) {2161return false;2162}21632164Ref<Script> scr = p_resource;21652166// Don't open dominant script if using an external editor.2167bool use_external_editor =2168external_editor_active ||2169(scr.is_valid() && scr->get_language()->overrides_external_editor());2170use_external_editor = use_external_editor && !(scr.is_valid() && scr->is_built_in()); // Ignore external editor for built-in scripts.2171const bool open_dominant = EDITOR_GET("text_editor/behavior/files/open_dominant_script_on_scene_change");21722173const bool should_open = (open_dominant && !use_external_editor) || !EditorNode::get_singleton()->is_changing_scene();21742175if (scr.is_valid() && scr->get_language()->overrides_external_editor()) {2176if (should_open) {2177Error err = scr->get_language()->open_in_external_editor(scr, p_line >= 0 ? p_line : 0, p_col);2178if (err != OK) {2179ERR_PRINT("Couldn't open script in the overridden external text editor");2180}2181}2182return false;2183}21842185if (use_external_editor &&2186(EditorDebuggerNode::get_singleton()->get_dump_stack_script() != p_resource || EditorDebuggerNode::get_singleton()->get_debug_with_external_editor()) &&2187p_resource->get_path().is_resource_file()) {2188if (ScriptEditorPlugin::open_in_external_editor(ProjectSettings::get_singleton()->globalize_path(p_resource->get_path()), p_line, p_col)) {2189return false;2190} else {2191ERR_PRINT("Couldn't open external text editor, falling back to the internal editor. Review your `text_editor/external/` editor settings.");2192}2193}21942195for (int i = 0; i < tab_container->get_tab_count(); i++) {2196ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));2197if (!seb) {2198continue;2199}22002201if ((scr.is_valid() && seb->get_edited_resource() == p_resource) || seb->get_edited_resource()->get_path() == p_resource->get_path()) {2202if (should_open) {2203if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb)) {2204teb->enable_editor();22052206if (tab_container->get_current_tab() != i) {2207_go_to_tab(i);2208}22092210if (is_visible_in_tree()) {2211teb->ensure_focus();2212}22132214if (p_line >= 0) {2215teb->goto_line(p_line, p_col);2216}2217} else if (tab_container->get_current_tab() != i) {2218_go_to_tab(i);2219}2220}2221_update_script_names();2222script_list->ensure_current_is_visible();2223return true;2224}2225}22262227// doesn't have it, make a new one2228ScriptEditorBase *seb = nullptr;22292230for (int i = script_editor_func_count - 1; i >= 0; i--) {2231seb = script_editor_funcs[i](p_resource);2232if (seb) {2233break;2234}2235}2236ERR_FAIL_NULL_V(seb, false);22372238seb->set_edited_resource(p_resource);22392240seb->set_toggle_list_control(get_left_list_split());2241tab_container->add_child(seb);22422243if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb)) {2244if (p_grab_focus) {2245teb->enable_editor();2246}22472248Control *editor_edit_menu = teb->get_edit_menu();2249if (editor_edit_menu && !editor_edit_menu->get_parent()) {2250editor_edit_menu->hide();2251editor_menus.push_back(editor_edit_menu);2252menu_hb->add_child(editor_edit_menu);2253menu_hb->move_child(editor_edit_menu, 1);2254for (int i = 0; i < editor_edit_menu->get_child_count(); ++i) {2255Control *c = Object::cast_to<Control>(editor_edit_menu->get_child(i));2256if (c) {2257c->set_shortcut_context(this);2258}2259}2260}2261}22622263if (p_grab_focus) {2264_go_to_tab(tab_container->get_tab_count() - 1);2265_add_recent_script(p_resource->get_path());2266}22672268// If we delete a script within the filesystem, the original resource path2269// is lost, so keep it as `edited_file_data` to figure out the exact tab to delete.2270seb->edited_file_data.path = p_resource->get_path();2271seb->edited_file_data.last_modified_time = FileAccess::get_modified_time(p_resource->get_path());22722273seb->connect("name_changed", callable_mp(this, &ScriptEditor::_update_script_names));2274seb->connect("edited_script_changed", callable_mp(this, &ScriptEditor::_script_changed));22752276if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb)) {2277// Syntax highlighting.2278bool highlighter_set = false;2279for (int i = 0; i < syntax_highlighters.size(); i++) {2280Ref<EditorSyntaxHighlighter> highlighter = syntax_highlighters[i]->_create();2281if (highlighter.is_null()) {2282continue;2283}2284teb->add_syntax_highlighter(highlighter);22852286if (highlighter_set) {2287continue;2288}22892290PackedStringArray languages = highlighter->_get_supported_languages();2291// If script try language, else use extension.2292if (scr.is_valid()) {2293if (languages.has(scr->get_language()->get_name())) {2294teb->set_syntax_highlighter(highlighter);2295highlighter_set = true;2296}2297continue;2298}22992300if (languages.has(p_resource->get_path().get_extension())) {2301teb->set_syntax_highlighter(highlighter);2302highlighter_set = true;2303}2304}23052306teb->set_tooltip_request_func(callable_mp(this, &ScriptEditor::_get_debug_tooltip));23072308teb->connect("request_help", callable_mp(this, &ScriptEditor::_help_search));2309teb->connect("request_open_script_at_line", callable_mp(this, &ScriptEditor::_goto_script_line));2310teb->connect("go_to_help", callable_mp(this, &ScriptEditor::_help_class_goto));2311teb->connect("request_save_history", callable_mp(this, &ScriptEditor::_save_history));2312teb->connect("request_save_previous_state", callable_mp(this, &ScriptEditor::_save_previous_state));2313teb->connect("search_in_files_requested", callable_mp(this, &ScriptEditor::open_find_in_files_dialog));2314teb->connect("replace_in_files_requested", callable_mp(this, &ScriptEditor::_on_replace_in_files_requested));2315teb->connect("go_to_method", callable_mp(this, &ScriptEditor::script_goto_method));23162317if (script_editor_cache->has_section(p_resource->get_path())) {2318teb->set_edit_state(script_editor_cache->get_value(p_resource->get_path(), "state"));2319if (ScriptTextEditor *ste = Object::cast_to<ScriptTextEditor>(teb)) {2320ste->store_previous_state();2321}2322}23232324if (CodeTextEditor *cte = teb->get_code_editor()) {2325cte->set_zoom_factor(zoom_factor);2326cte->connect("zoomed", callable_mp(this, &ScriptEditor::_set_script_zoom_factor));2327cte->connect(SceneStringName(visibility_changed), callable_mp(this, &ScriptEditor::_update_code_editor_zoom_factor).bind(cte));2328}2329}23302331_sort_list_on_update = true;2332_update_script_names();2333_save_layout();23342335//test for modification, maybe the script was not edited but was loaded23362337_test_script_times_on_disk(p_resource);2338_update_modified_scripts_for_external_editor(p_resource);23392340if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb)) {2341if (p_line >= 0) {2342teb->goto_line(p_line, p_col);2343}2344}23452346notify_script_changed(p_resource);2347return true;2348}23492350PackedStringArray ScriptEditor::get_unsaved_scripts() const {2351PackedStringArray unsaved_list;23522353for (int i = 0; i < tab_container->get_tab_count(); i++) {2354ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));2355if (seb && seb->is_unsaved()) {2356unsaved_list.append(seb->get_name());2357}2358}2359return unsaved_list;2360}23612362void ScriptEditor::save_current_script() {2363ScriptEditorBase *current = _get_current_editor();2364if (!current || _test_script_times_on_disk()) {2365return;2366}23672368_auto_format_text(current);23692370Ref<Resource> resource = current->get_edited_resource();2371Ref<TextFile> text_file = resource;2372Ref<Script> scr = resource;23732374if (text_file.is_valid()) {2375current->apply_code();2376_save_text_file(text_file, text_file->get_path());2377return;2378}23792380if (scr.is_valid()) {2381clear_docs_from_script(scr);2382}23832384EditorNode::get_singleton()->save_resource(resource);23852386if (scr.is_valid()) {2387update_docs_from_script(scr);2388}2389}23902391void ScriptEditor::save_all_scripts() {2392HashSet<String> scenes_to_save;23932394for (int i = 0; i < tab_container->get_tab_count(); i++) {2395ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));2396if (!seb) {2397continue;2398}23992400_auto_format_text(seb);24012402if (!seb->is_unsaved()) {2403continue;2404}24052406Ref<Resource> edited_res = seb->get_edited_resource();2407if (edited_res.is_valid()) {2408seb->apply_code();2409}24102411Ref<Script> scr = edited_res;24122413if (scr.is_valid()) {2414clear_docs_from_script(scr);2415}24162417if (!edited_res->is_built_in()) {2418Ref<TextFile> text_file = edited_res;2419if (text_file.is_valid()) {2420_save_text_file(text_file, text_file->get_path());2421continue;2422}24232424// External script, save it.2425EditorNode::get_singleton()->save_resource(edited_res);2426} else {2427// For built-in scripts, save their scenes instead.2428const String scene_path = edited_res->get_path().get_slice("::", 0);2429if (!scene_path.is_empty() && !scenes_to_save.has(scene_path)) {2430scenes_to_save.insert(scene_path);2431}2432}24332434if (scr.is_valid()) {2435update_docs_from_script(scr);2436}2437}24382439if (!scenes_to_save.is_empty()) {2440EditorNode::get_singleton()->save_scene_list(scenes_to_save);2441}24422443_update_script_names();2444}24452446void ScriptEditor::update_script_times() {2447for (int i = 0; i < tab_container->get_tab_count(); i++) {2448if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i))) {2449seb->edited_file_data.last_modified_time = FileAccess::get_modified_time(seb->edited_file_data.path);2450}2451}2452}24532454void ScriptEditor::apply_scripts() const {2455for (int i = 0; i < tab_container->get_tab_count(); i++) {2456ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));2457if (!seb) {2458continue;2459}24602461if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb)) {2462teb->insert_final_newline();2463}2464seb->apply_code();2465}2466}24672468void ScriptEditor::reload_scripts(bool p_refresh_only) {2469// Call deferred to make sure it runs on the main thread.2470if (!Thread::is_main_thread()) {2471callable_mp(this, &ScriptEditor::_reload_scripts).call_deferred(p_refresh_only);2472return;2473}2474_reload_scripts(p_refresh_only);2475}24762477void ScriptEditor::_reload_scripts(bool p_refresh_only) {2478for (int i = 0; i < tab_container->get_tab_count(); i++) {2479ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));2480if (!seb) {2481continue;2482}24832484Ref<Resource> edited_res = seb->get_edited_resource();24852486if (edited_res->is_built_in()) {2487continue; // Internal script, who cares.2488}24892490if (p_refresh_only) {2491// Make sure the modified time is correct.2492seb->edited_file_data.last_modified_time = FileAccess::get_modified_time(edited_res->get_path());2493} else {2494uint64_t last_date = seb->edited_file_data.last_modified_time;2495uint64_t date = FileAccess::get_modified_time(edited_res->get_path());24962497if (last_date == date) {2498continue;2499}2500seb->edited_file_data.last_modified_time = date;25012502Ref<Script> scr = edited_res;2503if (scr.is_valid()) {2504Ref<Script> rel_scr = ResourceLoader::load(scr->get_path(), scr->get_class(), ResourceFormatLoader::CACHE_MODE_IGNORE);2505ERR_CONTINUE(rel_scr.is_null());2506scr->set_source_code(rel_scr->get_source_code());2507scr->reload(true);25082509update_docs_from_script(scr);2510}25112512Ref<JSON> json = edited_res;2513if (json.is_valid()) {2514Ref<JSON> rel_json = ResourceLoader::load(json->get_path(), json->get_class(), ResourceFormatLoader::CACHE_MODE_IGNORE);2515ERR_CONTINUE(rel_json.is_null());2516json->parse(rel_json->get_parsed_text(), true);2517}25182519Ref<TextFile> text_file = edited_res;2520if (text_file.is_valid()) {2521text_file->reload_from_file();2522}2523}25242525if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb)) {2526teb->reload_text();2527}2528}25292530disk_changed->hide();2531_update_script_names();2532}25332534void ScriptEditor::_auto_format_text(ScriptEditorBase *p_seb) {2535if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(p_seb)) {2536if (trim_trailing_whitespace_on_save) {2537teb->trim_trailing_whitespace();2538}2539if (trim_final_newlines_on_save) {2540teb->trim_final_newlines();2541}2542if (convert_indent_on_save) {2543teb->convert_indent();2544}2545}2546}25472548void ScriptEditor::open_find_in_files_dialog(const String &text) {2549find_in_files_dialog->set_find_in_files_mode(FindInFilesDialog::SEARCH_MODE);2550find_in_files_dialog->set_search_text(text);2551find_in_files_dialog->popup_centered();2552}25532554void ScriptEditor::open_script_create_dialog(const String &p_base_name, const String &p_base_path) {2555_menu_option(FILE_MENU_NEW_SCRIPT);2556script_create_dialog->config(p_base_name, p_base_path);2557}25582559void ScriptEditor::open_text_file_create_dialog(const String &p_base_path, const String &p_base_name) {2560_menu_option(FILE_MENU_NEW_TEXTFILE);2561file_dialog->set_current_dir(p_base_path);2562file_dialog->set_current_file(p_base_name);2563}25642565Ref<Resource> ScriptEditor::open_file(const String &p_file) {2566if (_get_recognized_extensions().find(p_file.get_extension())) {2567Ref<Resource> scr = ResourceLoader::load(p_file);2568if (scr.is_null()) {2569EditorNode::get_singleton()->show_warning(TTR("Could not load file at:") + "\n\n" + p_file, TTR("Error!"));2570return Ref<Resource>();2571}25722573edit(scr);2574return scr;2575}25762577Error error;2578Ref<TextFile> text_file = _load_text_file(p_file, &error);2579if (error != OK) {2580EditorNode::get_singleton()->show_warning(TTR("Could not load file at:") + "\n\n" + p_file, TTR("Error!"));2581return Ref<Resource>();2582}25832584if (text_file.is_valid()) {2585edit(text_file);2586return text_file;2587}2588return Ref<Resource>();2589}25902591void ScriptEditor::_add_callback(Object *p_obj, const String &p_function, const PackedStringArray &p_args) {2592ERR_FAIL_NULL(p_obj);2593Ref<Script> scr = p_obj->get_script();2594ERR_FAIL_COND(scr.is_null());25952596if (!scr->get_language()->can_make_function()) {2597return;2598}25992600EditorNode::get_singleton()->push_item(scr.ptr());26012602for (int i = 0; i < tab_container->get_tab_count(); i++) {2603ScriptTextEditor *ste = Object::cast_to<ScriptTextEditor>(tab_container->get_tab_control(i));2604if (!ste || ste->get_edited_resource() != scr) {2605continue;2606}26072608ste->add_callback(p_function, p_args);26092610_go_to_tab(i);26112612script_list->select(script_list->find_metadata(i));26132614// Save the current script so the changes can be picked up by an external editor.2615if (!scr.ptr()->is_built_in()) { // But only if it's not built-in script.2616save_current_script();2617}26182619break;2620}26212622// Move back to the previously edited node to reselect it in the Inspector and the SignalsDock.2623// We assume that the previous item is the node on which the callbacks were added.2624EditorNode::get_singleton()->edit_previous_item();2625}26262627void ScriptEditor::_save_editor_state(ScriptEditorBase *p_editor) {2628if (restoring_layout) {2629return;2630}26312632const String &path = p_editor->get_edited_resource()->get_path();2633if (path.is_empty()) {2634return;2635}26362637if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(p_editor)) {2638script_editor_cache->set_value(path, "state", teb->get_edit_state());2639}2640// This is saved later when we save the editor layout.2641}26422643void ScriptEditor::_save_layout() {2644if (restoring_layout) {2645return;2646}26472648EditorNode::get_singleton()->save_editor_layout_delayed();2649}26502651void ScriptEditor::_apply_editor_settings() {2652textfile_extensions.clear();2653const Vector<String> textfile_ext = ((String)(EDITOR_GET("docks/filesystem/textfile_extensions"))).split(",", false);2654for (const String &E : textfile_ext) {2655textfile_extensions.insert(E);2656}26572658trim_trailing_whitespace_on_save = EDITOR_GET("text_editor/behavior/files/trim_trailing_whitespace_on_save");2659trim_final_newlines_on_save = EDITOR_GET("text_editor/behavior/files/trim_final_newlines_on_save");2660convert_indent_on_save = EDITOR_GET("text_editor/behavior/files/convert_indent_on_save");26612662members_overview_enabled = EDITOR_GET("text_editor/script_list/show_members_overview");2663help_overview_enabled = EDITOR_GET("text_editor/help/show_help_index");2664external_editor_active = EDITOR_GET("text_editor/external/use_external_editor");2665_update_members_overview_visibility();2666_update_help_overview_visibility();26672668_update_autosave_timer();26692670_update_script_names();26712672ScriptServer::set_reload_scripts_on_save(EDITOR_GET("text_editor/behavior/files/auto_reload_and_parse_scripts_on_save"));26732674for (int i = 0; i < tab_container->get_tab_count(); i++) {2675if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(tab_container->get_tab_control(i))) {2676teb->update_settings();2677}2678}2679}26802681void ScriptEditor::_filesystem_changed() {2682_update_script_names();2683}26842685void ScriptEditor::_files_moved(const String &p_old_file, const String &p_new_file) {2686if (!script_editor_cache->has_section(p_old_file)) {2687return;2688}26892690for (int i = 0; i < tab_container->get_tab_count(); i++) {2691ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));2692if (seb && seb->edited_file_data.path == p_old_file) {2693seb->edited_file_data.path = p_new_file;2694break;2695}2696}26972698Variant state = script_editor_cache->get_value(p_old_file, "state");2699script_editor_cache->erase_section(p_old_file);2700script_editor_cache->set_value(p_new_file, "state", state);27012702// If Script, update breakpoints with debugger.2703Array breakpoints = _get_cached_breakpoints_for_script(p_new_file);2704for (int breakpoint : breakpoints) {2705int line = (int)breakpoint + 1;2706EditorDebuggerNode::get_singleton()->set_breakpoint(p_old_file, line, false);2707if (!p_new_file.begins_with("local://") && ResourceLoader::exists(p_new_file, "Script")) {2708EditorDebuggerNode::get_singleton()->set_breakpoint(p_new_file, line, true);2709}2710}2711// This is saved later when we save the editor layout.2712}27132714void ScriptEditor::_file_removed(const String &p_removed_file) {2715for (int i = 0; i < tab_container->get_tab_count(); i++) {2716ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));2717if (seb && seb->edited_file_data.path == p_removed_file) {2718// The script is deleted with no undo, so just close the tab.2719_close_tab(i, false, false);2720}2721}27222723// Check closed.2724if (script_editor_cache->has_section(p_removed_file)) {2725Array breakpoints = _get_cached_breakpoints_for_script(p_removed_file);2726for (int breakpoint : breakpoints) {2727EditorDebuggerNode::get_singleton()->set_breakpoint(p_removed_file, (int)breakpoint + 1, false);2728}2729script_editor_cache->erase_section(p_removed_file);2730}2731}27322733void ScriptEditor::_update_find_replace_bar() {2734if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(_get_current_editor())) {2735teb->set_find_replace_bar(find_replace_bar);2736} else {2737find_replace_bar->set_text_edit(nullptr);2738find_replace_bar->hide();2739}2740}27412742void ScriptEditor::_autosave_scripts() {2743save_all_scripts();2744}27452746void ScriptEditor::_update_autosave_timer() {2747if (!autosave_timer->is_inside_tree()) {2748return;2749}27502751float autosave_time = EDITOR_GET("text_editor/behavior/files/autosave_interval_secs");2752if (autosave_time > 0) {2753autosave_timer->set_wait_time(autosave_time);2754autosave_timer->start();2755} else {2756autosave_timer->stop();2757}2758}27592760void ScriptEditor::_tree_changed() {2761if (waiting_update_names) {2762return;2763}27642765waiting_update_names = true;2766callable_mp(this, &ScriptEditor::_update_script_names).call_deferred();2767}27682769void ScriptEditor::_split_dragged(float) {2770_save_layout();2771}27722773Variant ScriptEditor::get_drag_data_fw(const Point2 &p_point, Control *p_from) {2774if (tab_container->get_tab_count() == 0) {2775return Variant();2776}27772778Node *cur_node = tab_container->get_tab_control(tab_container->get_current_tab());27792780HBoxContainer *drag_preview = memnew(HBoxContainer);2781String preview_name = "";2782Ref<Texture2D> preview_icon;27832784if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(cur_node)) {2785preview_name = seb->get_name();2786preview_icon = seb->get_theme_icon();2787}2788if (EditorHelp *eh = Object::cast_to<EditorHelp>(cur_node)) {2789preview_name = eh->get_class();2790preview_icon = get_editor_theme_icon(SNAME("Help"));2791}27922793if (preview_icon.is_valid()) {2794TextureRect *tf = memnew(TextureRect);2795tf->set_texture(preview_icon);2796tf->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);2797drag_preview->add_child(tf);2798}2799Label *label = memnew(Label(preview_name));2800label->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); // Don't translate script names and class names.2801drag_preview->add_child(label);2802set_drag_preview(drag_preview);28032804Dictionary drag_data;2805drag_data["type"] = "script_list_element"; // using a custom type because node caused problems when dragging to scene tree2806drag_data["script_list_element"] = cur_node;28072808return drag_data;2809}28102811bool ScriptEditor::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const {2812Dictionary d = p_data;2813if (!d.has("type")) {2814return false;2815}28162817if (String(d["type"]) == "script_list_element") {2818Node *node = Object::cast_to<Node>(d["script_list_element"]);28192820if (Object::cast_to<ScriptEditorBase>(node) || Object::cast_to<EditorHelp>(node)) {2821return true;2822}2823}28242825if (String(d["type"]) == "nodes") {2826Array nodes = d["nodes"];2827if (nodes.is_empty()) {2828return false;2829}2830Node *node = get_node((nodes[0]));28312832if (Object::cast_to<ScriptEditorBase>(node) || Object::cast_to<EditorHelp>(node)) {2833return true;2834}2835}28362837if (String(d["type"]) == "files") {2838Vector<String> files = d["files"];28392840if (files.is_empty()) {2841return false; //weird2842}28432844for (int i = 0; i < files.size(); i++) {2845const String &file = files[i];2846if (file.is_empty() || !FileAccess::exists(file)) {2847continue;2848}2849if (ResourceLoader::exists(file, "Script") || ResourceLoader::exists(file, "JSON")) {2850Ref<Resource> scr = ResourceLoader::load(file);2851if (scr.is_valid()) {2852return true;2853}2854}28552856if (textfile_extensions.has(file.get_extension())) {2857Error err;2858Ref<TextFile> text_file = _load_text_file(file, &err);2859if (text_file.is_valid() && err == OK) {2860return true;2861}2862}2863}2864return false;2865}28662867return false;2868}28692870void ScriptEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) {2871if (!can_drop_data_fw(p_point, p_data, p_from)) {2872return;2873}28742875Dictionary d = p_data;2876if (!d.has("type")) {2877return;2878}28792880if (String(d["type"]) == "script_list_element") {2881Node *node = Object::cast_to<Node>(d["script_list_element"]);28822883ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(node);2884EditorHelp *eh = Object::cast_to<EditorHelp>(node);2885if (seb || eh) {2886int new_index = 0;2887if (script_list->get_item_count() > 0) {2888int pos = 0;2889if (p_point == Vector2(Math::INF, Math::INF)) {2890if (script_list->is_anything_selected()) {2891pos = script_list->get_selected_items()[0];2892}2893} else {2894pos = script_list->get_item_at_position(p_point);2895}2896new_index = script_list->get_item_metadata(pos);2897}2898tab_container->move_child(node, new_index);2899tab_container->set_current_tab(new_index);2900_update_script_names();2901}2902}29032904if (String(d["type"]) == "nodes") {2905Array nodes = d["nodes"];2906if (nodes.is_empty()) {2907return;2908}2909Node *node = get_node(nodes[0]);29102911ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(node);2912EditorHelp *eh = Object::cast_to<EditorHelp>(node);2913if (seb || eh) {2914int new_index = 0;2915if (script_list->get_item_count() > 0) {2916int pos = 0;2917if (p_point == Vector2(Math::INF, Math::INF)) {2918if (script_list->is_anything_selected()) {2919pos = script_list->get_selected_items()[0];2920}2921} else {2922pos = script_list->get_item_at_position(p_point);2923}2924new_index = script_list->get_item_metadata(pos);2925}2926tab_container->move_child(node, new_index);2927tab_container->set_current_tab(new_index);2928_update_script_names();2929}2930}29312932if (String(d["type"]) == "files") {2933Vector<String> files = d["files"];29342935int new_index = 0;2936if (script_list->get_item_count() > 0) {2937int pos = 0;2938if (p_point == Vector2(Math::INF, Math::INF)) {2939if (script_list->is_anything_selected()) {2940pos = script_list->get_selected_items()[0];2941}2942} else {2943pos = script_list->get_item_at_position(p_point);2944}2945new_index = script_list->get_item_metadata(pos);2946}2947int num_tabs_before = tab_container->get_tab_count();2948for (int i = 0; i < files.size(); i++) {2949const String &file = files[i];2950if (file.is_empty() || !FileAccess::exists(file)) {2951continue;2952}29532954if (!ResourceLoader::exists(file, "Script") && !ResourceLoader::exists(file, "JSON") && !textfile_extensions.has(file.get_extension())) {2955continue;2956}29572958Ref<Resource> res = open_file(file);2959if (res.is_valid()) {2960const int num_tabs = tab_container->get_tab_count();2961if (num_tabs > num_tabs_before) {2962tab_container->move_child(tab_container->get_tab_control(tab_container->get_tab_count() - 1), new_index);2963num_tabs_before = num_tabs;2964} else if (num_tabs > 0) { /* Maybe script was already open */2965tab_container->move_child(tab_container->get_tab_control(tab_container->get_current_tab()), new_index);2966}2967}2968}2969if (tab_container->get_tab_count() > 0) {2970tab_container->set_current_tab(new_index);2971}2972_update_script_names();2973}2974}29752976void ScriptEditor::input(const Ref<InputEvent> &p_event) {2977// This is implemented in `input()` rather than `unhandled_input()` to allow2978// the shortcut to be used regardless of the click location.2979// This feature can be disabled to avoid interfering with other uses of the additional2980// mouse buttons, such as push-to-talk in a VoIP program.2981if (EDITOR_GET("interface/editor/mouse_extra_buttons_navigate_history")) {2982const Ref<InputEventMouseButton> mb = p_event;29832984// Navigate the script history using additional mouse buttons present on some mice.2985// This must be hardcoded as the editor shortcuts dialog doesn't allow assigning2986// more than one shortcut per action.2987if (mb.is_valid() && mb->is_pressed() && is_visible_in_tree()) {2988if (mb->get_button_index() == MouseButton::MB_XBUTTON1) {2989_history_back();2990}29912992if (mb->get_button_index() == MouseButton::MB_XBUTTON2) {2993_history_forward();2994}2995}2996}2997}29982999void ScriptEditor::shortcut_input(const Ref<InputEvent> &p_event) {3000ERR_FAIL_COND(p_event.is_null());30013002if (!is_visible_in_tree() || !p_event->is_pressed()) {3003return;3004}3005if (ED_IS_SHORTCUT("script_editor/next_script", p_event)) {3006if (script_list->get_item_count() > 1) {3007int next_tab = script_list->get_current() + 1;3008next_tab %= script_list->get_item_count();3009_go_to_tab(script_list->get_item_metadata(next_tab));3010_update_script_names();3011}3012accept_event();3013}3014if (ED_IS_SHORTCUT("script_editor/prev_script", p_event)) {3015if (script_list->get_item_count() > 1) {3016int next_tab = script_list->get_current() - 1;3017next_tab = next_tab >= 0 ? next_tab : script_list->get_item_count() - 1;3018_go_to_tab(script_list->get_item_metadata(next_tab));3019_update_script_names();3020}3021accept_event();3022}3023if (ED_IS_SHORTCUT("script_editor/window_move_up", p_event)) {3024_menu_option(FILE_MENU_MOVE_UP);3025accept_event();3026}3027if (ED_IS_SHORTCUT("script_editor/window_move_down", p_event)) {3028_menu_option(FILE_MENU_MOVE_DOWN);3029accept_event();3030}30313032if (p_event->is_echo()) {3033return;3034}30353036Callable custom_callback = EditorContextMenuPluginManager::get_singleton()->match_custom_shortcut(EditorContextMenuPlugin::CONTEXT_SLOT_SCRIPT_EDITOR, p_event);3037if (custom_callback.is_valid()) {3038Ref<Resource> resource;3039if (ScriptEditorBase *current = _get_current_editor()) {3040resource = current->get_edited_resource();3041}3042EditorContextMenuPluginManager::get_singleton()->invoke_callback(custom_callback, resource);3043accept_event();3044}3045}30463047void ScriptEditor::_script_list_clicked(int p_item, Vector2 p_local_mouse_pos, MouseButton p_mouse_button_index) {3048if (p_mouse_button_index == MouseButton::MIDDLE) {3049script_list->select(p_item);3050_script_selected(p_item);3051_menu_option(FILE_MENU_CLOSE);3052}30533054if (p_mouse_button_index == MouseButton::RIGHT) {3055_make_script_list_context_menu();3056}3057}30583059void ScriptEditor::_make_script_list_context_menu() {3060context_menu->clear();30613062int selected = tab_container->get_current_tab();3063if (selected < 0 || selected >= tab_container->get_tab_count()) {3064return;3065}30663067ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(selected));3068if (seb) {3069context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/save"), FILE_MENU_SAVE);3070context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/save_as"), FILE_MENU_SAVE_AS);3071}3072context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/close_file"), FILE_MENU_CLOSE);3073context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/close_other_tabs"), FILE_MENU_CLOSE_OTHER_TABS);3074context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/close_tabs_below"), FILE_MENU_CLOSE_TABS_BELOW);3075context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/close_all"), FILE_MENU_CLOSE_ALL);3076context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/close_docs"), FILE_MENU_CLOSE_DOCS);3077context_menu->add_separator();3078if (seb) {3079Ref<Script> scr = seb->get_edited_resource();3080if (scr.is_valid() && scr->is_tool()) {3081context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/reload_script_soft"), FILE_MENU_SOFT_RELOAD_TOOL);3082context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/run_file"), FILE_MENU_RUN);3083context_menu->add_separator();3084}3085context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/copy_path"), FILE_MENU_COPY_PATH);3086context_menu->set_item_disabled(-1, seb->get_edited_resource()->get_path().is_empty());3087context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/copy_uid"), FILE_MENU_COPY_UID);3088context_menu->set_item_disabled(-1, ResourceLoader::get_resource_uid(seb->get_edited_resource()->get_path()) == ResourceUID::INVALID_ID);3089context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/show_in_file_system"), FILE_MENU_SHOW_IN_FILE_SYSTEM);3090context_menu->add_separator();3091}30923093context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/window_move_up"), FILE_MENU_MOVE_UP);3094context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/window_move_down"), FILE_MENU_MOVE_DOWN);3095context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/window_sort"), FILE_MENU_SORT);3096context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/toggle_files_panel"), FILE_MENU_TOGGLE_FILES_PANEL);30973098context_menu->set_item_disabled(context_menu->get_item_index(FILE_MENU_CLOSE_ALL), tab_container->get_tab_count() <= 0);3099context_menu->set_item_disabled(context_menu->get_item_index(FILE_MENU_CLOSE_OTHER_TABS), tab_container->get_tab_count() <= 1);3100context_menu->set_item_disabled(context_menu->get_item_index(FILE_MENU_CLOSE_DOCS), !_has_docs_tab());3101context_menu->set_item_disabled(context_menu->get_item_index(FILE_MENU_CLOSE_TABS_BELOW), tab_container->get_current_tab() >= tab_container->get_tab_count() - 1);3102context_menu->set_item_disabled(context_menu->get_item_index(FILE_MENU_MOVE_UP), tab_container->get_current_tab() <= 0);3103context_menu->set_item_disabled(context_menu->get_item_index(FILE_MENU_MOVE_DOWN), tab_container->get_current_tab() >= tab_container->get_tab_count() - 1);3104context_menu->set_item_disabled(context_menu->get_item_index(FILE_MENU_SORT), tab_container->get_tab_count() <= 1);31053106// Context menu plugin.3107Vector<String> selected_paths;3108if (seb) {3109Ref<Resource> scr = seb->get_edited_resource();3110if (scr.is_valid()) {3111String path = scr->get_path();3112selected_paths.push_back(path);3113}3114}3115EditorContextMenuPluginManager::get_singleton()->add_options_from_plugins(context_menu, EditorContextMenuPlugin::CONTEXT_SLOT_SCRIPT_EDITOR, selected_paths);31163117context_menu->set_position(get_screen_position() + get_local_mouse_position());3118context_menu->reset_size();3119context_menu->popup();3120}31213122void ScriptEditor::set_window_layout(Ref<ConfigFile> p_layout) {3123if (!bool(EDITOR_GET("text_editor/behavior/files/restore_scripts_on_load"))) {3124return;3125}31263127if (!p_layout->has_section_key("ScriptEditor", "open_scripts") && !p_layout->has_section_key("ScriptEditor", "open_help")) {3128return;3129}31303131Array scripts = p_layout->get_value("ScriptEditor", "open_scripts");3132Array helps;3133if (p_layout->has_section_key("ScriptEditor", "open_help")) {3134helps = p_layout->get_value("ScriptEditor", "open_help");3135}31363137restoring_layout = true;31383139HashSet<String> loaded_scripts;3140List<String> extensions = _get_recognized_extensions();31413142for (const Variant &v : scripts) {3143String path = v;31443145Dictionary script_info = v;3146if (!script_info.is_empty()) {3147path = script_info["path"];3148}31493150if (!_script_exists(path)) {3151if (script_editor_cache->has_section(path)) {3152script_editor_cache->erase_section(path);3153}3154continue;3155} else if (!path.is_resource_file() && !EditorNode::get_singleton()->is_scene_open(path.get_slice("::", 0))) {3156continue;3157}3158loaded_scripts.insert(path);31593160bool is_script = false;3161if (path.is_resource_file()) {3162is_script = extensions.find(path.get_extension());3163} else {3164Ref<Script> scr = ResourceCache::get_ref(path);3165if (scr.is_valid()) {3166is_script = true;3167} else {3168continue;3169}3170}31713172if (is_script) {3173Ref<Resource> scr = ResourceLoader::load(path);3174if (scr.is_null()) {3175continue;3176}3177if (!edit(scr, false)) {3178continue;3179}3180} else {3181Error error;3182Ref<TextFile> text_file = _load_text_file(path, &error);3183if (error != OK || text_file.is_null()) {3184continue;3185}3186if (!edit(text_file, false)) {3187continue;3188}3189}31903191if (!script_info.is_empty()) {3192if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(tab_container->get_tab_control(tab_container->get_tab_count() - 1))) {3193teb->set_edit_state(script_info["state"]);3194}3195}3196}31973198for (int i = 0; i < helps.size(); i++) {3199String path = helps[i];3200if (path.is_empty()) { // invalid, skip3201continue;3202}3203_help_class_open(path);3204}32053206for (int i = 0; i < tab_container->get_tab_count(); i++) {3207tab_container->get_tab_control(i)->set_meta("__editor_pass", Variant());3208}32093210if (p_layout->has_section_key("ScriptEditor", "script_split_offset")) {3211script_split->set_split_offset(p_layout->get_value("ScriptEditor", "script_split_offset"));3212}32133214if (p_layout->has_section_key("ScriptEditor", "list_split_offset")) {3215list_split->set_split_offset(p_layout->get_value("ScriptEditor", "list_split_offset"));3216}32173218// Remove any deleted editors that have been removed between launches.3219// and if a Script, register breakpoints with the debugger.3220Vector<String> cached_editors = script_editor_cache->get_sections();3221for (const String &E : cached_editors) {3222if (loaded_scripts.has(E)) {3223continue;3224}32253226if (!_script_exists(E)) {3227script_editor_cache->erase_section(E);3228continue;3229}32303231Array breakpoints = _get_cached_breakpoints_for_script(E);3232for (int breakpoint : breakpoints) {3233EditorDebuggerNode::get_singleton()->set_breakpoint(E, (int)breakpoint + 1, true);3234}3235}32363237_set_script_zoom_factor(p_layout->get_value("ScriptEditor", "zoom_factor", 1.0f));32383239restoring_layout = false;32403241_update_script_names();32423243if (p_layout->has_section_key("ScriptEditor", "selected_script")) {3244String selected_script = p_layout->get_value("ScriptEditor", "selected_script");3245// If the selected script is not in the list of open scripts, select nothing.3246for (int i = 0; i < tab_container->get_tab_count(); i++) {3247ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));3248if (seb && seb->get_edited_resource()->get_path() == selected_script) {3249_go_to_tab(i);3250break;3251}3252}3253}3254}32553256void ScriptEditor::get_window_layout(Ref<ConfigFile> p_layout) {3257Array scripts;3258Array helps;3259String selected_script;3260for (int i = 0; i < tab_container->get_tab_count(); i++) {3261if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i))) {3262const String path = seb->get_edited_resource()->get_path();3263if (path.is_empty()) {3264continue;3265}32663267if (tab_container->get_current_tab_control() == tab_container->get_tab_control(i)) {3268selected_script = path;3269}32703271_save_editor_state(seb);3272scripts.push_back(path);3273}32743275if (EditorHelp *eh = Object::cast_to<EditorHelp>(tab_container->get_tab_control(i))) {3276helps.push_back(eh->get_class());3277}3278}32793280p_layout->set_value("ScriptEditor", "open_scripts", scripts);3281p_layout->set_value("ScriptEditor", "selected_script", selected_script);3282p_layout->set_value("ScriptEditor", "open_help", helps);3283p_layout->set_value("ScriptEditor", "script_split_offset", script_split->get_split_offset());3284p_layout->set_value("ScriptEditor", "list_split_offset", list_split->get_split_offset());3285p_layout->set_value("ScriptEditor", "zoom_factor", zoom_factor);32863287// Save the cache.3288script_editor_cache->save(EditorPaths::get_singleton()->get_project_settings_dir().path_join("script_editor_cache.cfg"));3289}32903291void ScriptEditor::_help_class_open(const String &p_class) {3292if (p_class.is_empty()) {3293return;3294}32953296for (int i = 0; i < tab_container->get_tab_count(); i++) {3297EditorHelp *eh = Object::cast_to<EditorHelp>(tab_container->get_tab_control(i));32983299if (eh && eh->get_class() == p_class) {3300_go_to_tab(i);3301_update_script_names();3302return;3303}3304}33053306EditorHelp *eh = memnew(EditorHelp);33073308eh->set_name(p_class);3309tab_container->add_child(eh);3310_go_to_tab(tab_container->get_tab_count() - 1);3311eh->go_to_class(p_class);3312eh->connect("go_to_help", callable_mp(this, &ScriptEditor::_help_class_goto));3313eh->connect("request_save_history", callable_mp(this, &ScriptEditor::_save_history));3314_add_recent_script(p_class);3315_sort_list_on_update = true;3316_update_script_names();3317_save_layout();3318}33193320void ScriptEditor::_help_class_goto(const String &p_desc) {3321String cname = p_desc.get_slicec(':', 1);33223323if (_help_tab_goto(cname, p_desc)) {3324return;3325}33263327EditorHelp *eh = memnew(EditorHelp);33283329eh->set_name(cname);3330tab_container->add_child(eh);3331_go_to_tab(tab_container->get_tab_count() - 1);3332eh->go_to_help(p_desc);3333eh->connect("go_to_help", callable_mp(this, &ScriptEditor::_help_class_goto));3334_add_recent_script(eh->get_class());3335_sort_list_on_update = true;3336_update_script_names();3337_save_layout();3338}33393340bool ScriptEditor::_help_tab_goto(const String &p_name, const String &p_desc) {3341for (int i = 0; i < tab_container->get_tab_count(); i++) {3342EditorHelp *eh = Object::cast_to<EditorHelp>(tab_container->get_tab_control(i));33433344if (eh && eh->get_class() == p_name) {3345_go_to_tab(i);3346eh->go_to_help(p_desc);3347_update_script_names();3348return true;3349}3350}3351return false;3352}33533354void ScriptEditor::update_doc(const String &p_name) {3355for (int i = 0; i < tab_container->get_tab_count(); i++) {3356EditorHelp *eh = Object::cast_to<EditorHelp>(tab_container->get_tab_control(i));3357if (eh && eh->get_class() == p_name) {3358eh->update_doc();3359return;3360}3361}3362}33633364void ScriptEditor::clear_docs_from_script(const Ref<Script> &p_script) {3365ERR_FAIL_COND(p_script.is_null());33663367for (const DocData::ClassDoc &cd : p_script->get_documentation()) {3368if (EditorHelp::get_doc_data()->has_doc(cd.name)) {3369EditorHelp::get_doc_data()->remove_doc(cd.name);3370}3371}3372}33733374void ScriptEditor::update_docs_from_script(const Ref<Script> &p_script) {3375ERR_FAIL_COND(p_script.is_null());33763377for (const DocData::ClassDoc &cd : p_script->get_documentation()) {3378EditorHelp::get_doc_data()->add_doc(cd);3379update_doc(cd.name);3380}3381}33823383void ScriptEditor::_update_selected_editor_menu() {3384TextEditorBase *current_editor = Object::cast_to<TextEditorBase>(_get_current_editor());3385for (Control *editor_menu : editor_menus) {3386editor_menu->set_visible(current_editor && editor_menu == current_editor->get_edit_menu());3387}33883389PopupMenu *search_popup = script_search_menu->get_popup();3390search_popup->clear();3391if (Object::cast_to<EditorHelp>(tab_container->get_current_tab_control())) {3392search_popup->add_shortcut(ED_SHORTCUT("script_editor/find", TTRC("Find..."), KeyModifierMask::CMD_OR_CTRL | Key::F), HELP_SEARCH_FIND);3393search_popup->add_shortcut(ED_SHORTCUT("script_editor/find_next", TTRC("Find Next"), Key::F3), HELP_SEARCH_FIND_NEXT);3394search_popup->add_shortcut(ED_SHORTCUT("script_editor/find_previous", TTRC("Find Previous"), KeyModifierMask::SHIFT | Key::F3), HELP_SEARCH_FIND_PREVIOUS);3395search_popup->add_separator();3396search_popup->add_shortcut(ED_GET_SHORTCUT("editor/find_in_files"), SEARCH_IN_FILES);3397search_popup->add_shortcut(ED_GET_SHORTCUT("script_editor/replace_in_files"), REPLACE_IN_FILES);3398script_search_menu->show();3399} else {3400if (tab_container->get_tab_count() == 0) {3401search_popup->add_shortcut(ED_GET_SHORTCUT("editor/find_in_files"), SEARCH_IN_FILES);3402search_popup->add_shortcut(ED_GET_SHORTCUT("script_editor/replace_in_files"), REPLACE_IN_FILES);3403script_search_menu->show();3404} else {3405script_search_menu->hide();3406}3407}3408}34093410void ScriptEditor::_unlock_history() {3411lock_history = false;3412}34133414void ScriptEditor::_update_history_pos(int p_new_pos) {3415Node *n = tab_container->get_current_tab_control();34163417if (Object::cast_to<TextEditorBase>(n)) {3418history.write[history_pos].state = Object::cast_to<TextEditorBase>(n)->get_navigation_state();3419}3420if (Object::cast_to<EditorHelp>(n)) {3421history.write[history_pos].state = Object::cast_to<EditorHelp>(n)->get_scroll();3422}34233424history_pos = p_new_pos;3425tab_container->set_current_tab(tab_container->get_tab_idx_from_control(history[history_pos].control));34263427n = history[history_pos].control;34283429ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(n);3430if (seb) {3431if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(n)) {3432lock_history = true;3433teb->set_edit_state(history[history_pos].state);3434// `set_edit_state()` can modify the caret position which might trigger a3435// request to save the history. Since `TextEdit::caret_changed` is emitted3436// deferred, we need to defer unlocking of the history as well.3437callable_mp(this, &ScriptEditor::_unlock_history).call_deferred();3438teb->ensure_focus();3439}34403441Ref<Script> scr = seb->get_edited_resource();3442if (scr.is_valid()) {3443notify_script_changed(scr);3444}34453446seb->validate_script();3447}34483449if (EditorHelp *eh = Object::cast_to<EditorHelp>(n)) {3450eh->set_scroll(history[history_pos].state);3451eh->set_focused();3452}34533454n->set_meta("__editor_pass", ++edit_pass);3455_update_script_names();3456_update_history_arrows();3457_update_selected_editor_menu();3458}34593460void ScriptEditor::_history_forward() {3461if (history_pos < history.size() - 1) {3462_update_history_pos(history_pos + 1);3463}3464}34653466void ScriptEditor::_history_back() {3467if (history_pos > 0) {3468_update_history_pos(history_pos - 1);3469}3470}34713472Vector<Ref<Script>> ScriptEditor::get_open_scripts() const {3473Vector<Ref<Script>> out_scripts = Vector<Ref<Script>>();34743475for (int i = 0; i < tab_container->get_tab_count(); i++) {3476ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));3477if (!seb) {3478continue;3479}34803481Ref<Script> scr = seb->get_edited_resource();3482if (scr.is_valid()) {3483out_scripts.push_back(scr);3484}3485}34863487return out_scripts;3488}34893490TypedArray<ScriptEditorBase> ScriptEditor::_get_open_script_editors() const {3491TypedArray<ScriptEditorBase> script_editors;3492for (int i = 0; i < tab_container->get_tab_count(); i++) {3493if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i))) {3494script_editors.push_back(seb);3495}3496}3497return script_editors;3498}34993500void ScriptEditor::set_scene_root_script(Ref<Script> p_script) {3501// Don't open dominant script if using an external editor.3502bool use_external_editor =3503external_editor_active ||3504(p_script.is_valid() && p_script->get_language()->overrides_external_editor());3505use_external_editor = use_external_editor && !(p_script.is_valid() && p_script->is_built_in()); // Ignore external editor for built-in scripts.3506const bool open_dominant = EDITOR_GET("text_editor/behavior/files/open_dominant_script_on_scene_change");35073508if (open_dominant && !use_external_editor && p_script.is_valid()) {3509edit(p_script);3510}3511}35123513bool ScriptEditor::script_goto_method(Ref<Script> p_script, const String &p_method) {3514int line = p_script->get_member_line(p_method);35153516if (line == -1) {3517return false;3518}35193520return edit(p_script, line, 0);3521}35223523void ScriptEditor::set_live_auto_reload_running_scripts(bool p_enabled) {3524auto_reload_running_scripts = p_enabled;3525}35263527void ScriptEditor::_calculate_script_name_button_size() {3528Ref<Font> font = script_name_button->get_theme_font(SceneStringName(font), SNAME("Button"));3529HorizontalAlignment alignment = script_name_button->get_text_alignment();3530int font_size = script_name_button->get_theme_font_size(SceneStringName(font_size), SNAME("Button"));3531String text = script_name_button->get_text();3532int jst_flags = TextServer::JUSTIFICATION_WORD_BOUND | TextServer::JUSTIFICATION_KASHIDA | TextServer::JUSTIFICATION_SKIP_LAST_LINE | TextServer::JUSTIFICATION_DO_NOT_SKIP_SINGLE_LINE;3533TextServer::Direction direction = TextServer::Direction(script_name_button->get_text_direction());3534Vector2 text_size = font->get_string_size(text, alignment, -1, font_size, jst_flags, direction, TextServer::ORIENTATION_HORIZONTAL);35353536script_name_width = text_size.x + script_name_button->get_theme_stylebox(CoreStringName(normal))->get_content_margin(SIDE_LEFT) + script_name_button->get_theme_stylebox(CoreStringName(normal))->get_content_margin(SIDE_RIGHT);3537_calculate_script_name_button_ratio();3538}35393540void ScriptEditor::_calculate_script_name_button_ratio() {3541const float total_width = script_name_button_hbox->get_size().width;3542if (total_width <= 0) {3543return;3544}35453546// Make the ratios a fraction bigger, to avoid unnecessary trimming.3547const float extra_ratio = 4 / total_width;35483549const float script_name_ratio = MIN(1, script_name_width / total_width + extra_ratio);3550script_name_button->set_stretch_ratio(script_name_ratio);35513552float ratio_left = 1 - script_name_ratio;3553script_name_button_left_spacer->set_stretch_ratio(ratio_left / 2);3554script_name_button_right_spacer->set_stretch_ratio(ratio_left / 2);3555}35563557void ScriptEditor::_help_search(const String &p_text) {3558help_search_dialog->popup_dialog(p_text);3559}35603561void ScriptEditor::register_syntax_highlighter(const Ref<EditorSyntaxHighlighter> &p_syntax_highlighter) {3562ERR_FAIL_COND(p_syntax_highlighter.is_null());35633564if (!syntax_highlighters.has(p_syntax_highlighter)) {3565syntax_highlighters.push_back(p_syntax_highlighter);3566}3567}35683569void ScriptEditor::unregister_syntax_highlighter(const Ref<EditorSyntaxHighlighter> &p_syntax_highlighter) {3570ERR_FAIL_COND(p_syntax_highlighter.is_null());35713572syntax_highlighters.erase(p_syntax_highlighter);3573}35743575int ScriptEditor::script_editor_func_count = 0;3576CreateScriptEditorFunc ScriptEditor::script_editor_funcs[ScriptEditor::SCRIPT_EDITOR_FUNC_MAX];35773578void ScriptEditor::register_create_script_editor_function(CreateScriptEditorFunc p_func) {3579ERR_FAIL_COND(script_editor_func_count == SCRIPT_EDITOR_FUNC_MAX);3580script_editor_funcs[script_editor_func_count++] = p_func;3581}35823583void ScriptEditor::_script_changed() {3584SignalsDock::get_singleton()->update_lists();3585}35863587void ScriptEditor::_on_replace_in_files_requested(const String &text) {3588find_in_files_dialog->set_find_in_files_mode(FindInFilesDialog::REPLACE_MODE);3589find_in_files_dialog->set_search_text(text);3590find_in_files_dialog->set_replace_text("");3591find_in_files_dialog->popup_centered();3592}35933594void ScriptEditor::_on_find_in_files_result_selected(const String &fpath, int line_number, int begin, int end) {3595if (ResourceLoader::exists(fpath)) {3596Ref<Resource> res = ResourceLoader::load(fpath);35973598if (fpath.get_extension() == "gdshader") {3599ShaderEditorPlugin *shader_editor = Object::cast_to<ShaderEditorPlugin>(EditorNode::get_editor_data().get_editor_by_name("Shader"));3600shader_editor->edit(res.ptr());3601shader_editor->make_visible(true);3602TextShaderEditor *text_shader_editor = Object::cast_to<TextShaderEditor>(shader_editor->get_shader_editor(res));3603if (text_shader_editor) {3604text_shader_editor->goto_line_selection(line_number - 1, begin, end);3605}3606return;3607} else if (fpath.get_extension() == "tscn") {3608const PackedStringArray lines = FileAccess::get_file_as_string(fpath).split("\n");3609if (line_number > lines.size()) {3610return;3611}36123613const char *scr_header = "[sub_resource type=\"GDScript\" id=\"";3614const char *source_header = "script/source = \"";3615String script_id;36163617// Search the scene backwards from the found line.3618int scan_line = line_number - 1;3619while (scan_line >= 0) {3620const String &line = lines[scan_line];3621if (line.begins_with(source_header)) {3622// Adjust line relative to the script beginning.3623line_number -= scan_line + 1;3624} else if (line.begins_with(scr_header)) {3625script_id = line.trim_prefix(scr_header).get_slicec('"', 0);3626break;3627}3628scan_line--;3629}36303631EditorNode::get_singleton()->load_scene(fpath);3632if (!script_id.is_empty()) {3633Ref<Script> scr = ResourceLoader::load(fpath + "::" + script_id, "Script");3634if (scr.is_valid()) {3635edit(scr);3636ScriptTextEditor *ste = Object::cast_to<ScriptTextEditor>(_get_current_editor());36373638if (ste) {3639callable_mp(EditorInterface::get_singleton(), &EditorInterface::set_main_screen_editor).call_deferred("Script");3640if (line_number == 0) {3641const int source_len = strlen(source_header);3642ste->goto_line_selection(line_number, begin - source_len, end - source_len);3643} else {3644ste->goto_line_selection(line_number, begin, end);3645}3646}3647}3648}36493650return;3651} else {3652Ref<Script> scr = res;3653Ref<JSON> json = res;3654if (scr.is_valid() || json.is_valid()) {3655edit(scr);36563657ScriptTextEditor *ste = Object::cast_to<ScriptTextEditor>(_get_current_editor());3658if (ste) {3659EditorInterface::get_singleton()->set_main_screen_editor("Script");3660ste->goto_line_selection(line_number - 1, begin, end);3661}3662return;3663}3664}3665}36663667// If the file is not a valid resource/script, load it as a text file.3668Error err;3669Ref<TextFile> text_file = _load_text_file(fpath, &err);3670if (text_file.is_valid()) {3671edit(text_file);36723673TextEditor *te = Object::cast_to<TextEditor>(_get_current_editor());3674if (te) {3675te->goto_line_selection(line_number - 1, begin, end);3676}3677}3678}36793680void ScriptEditor::_start_find_in_files(bool with_replace) {3681FindInFilesPanel *panel = find_in_files->get_panel_for_results(with_replace ? TTR("Replace:") + " " + find_in_files_dialog->get_search_text() : TTR("Find:") + " " + find_in_files_dialog->get_search_text());3682FindInFiles *f = panel->get_finder();36833684f->set_search_text(find_in_files_dialog->get_search_text());3685f->set_match_case(find_in_files_dialog->is_match_case());3686f->set_whole_words(find_in_files_dialog->is_whole_words());3687f->set_folder(find_in_files_dialog->get_folder());3688f->set_filter(find_in_files_dialog->get_filter());3689f->set_includes(find_in_files_dialog->get_includes());3690f->set_excludes(find_in_files_dialog->get_excludes());36913692panel->set_with_replace(with_replace);3693panel->set_replace_text(find_in_files_dialog->get_replace_text());3694panel->start_search();36953696find_in_files->make_visible();3697}36983699void ScriptEditor::_on_find_in_files_modified_files(const PackedStringArray &paths) {3700_test_script_times_on_disk();3701_update_modified_scripts_for_external_editor();3702}37033704void ScriptEditor::_set_script_zoom_factor(float p_zoom_factor) {3705if (zoom_factor == p_zoom_factor) {3706return;3707}37083709zoom_factor = p_zoom_factor;3710}37113712void ScriptEditor::_update_code_editor_zoom_factor(CodeTextEditor *p_code_text_editor) {3713if (p_code_text_editor && p_code_text_editor->is_visible_in_tree() && zoom_factor != p_code_text_editor->get_zoom_factor()) {3714p_code_text_editor->set_zoom_factor(zoom_factor);3715}3716}37173718void ScriptEditor::_window_changed(bool p_visible) {3719make_floating->set_visible(!p_visible);3720is_floating = p_visible;3721}37223723void ScriptEditor::_filter_scripts_text_changed(const String &p_newtext) {3724_update_script_names();3725}37263727void ScriptEditor::_filter_methods_text_changed(const String &p_newtext) {3728_update_members_overview();3729}37303731void ScriptEditor::_bind_methods() {3732ClassDB::bind_method("_help_tab_goto", &ScriptEditor::_help_tab_goto);3733ClassDB::bind_method("get_current_editor", &ScriptEditor::_get_current_editor);3734ClassDB::bind_method("get_open_script_editors", &ScriptEditor::_get_open_script_editors);3735ClassDB::bind_method("get_breakpoints", &ScriptEditor::_get_breakpoints);37363737ClassDB::bind_method(D_METHOD("register_syntax_highlighter", "syntax_highlighter"), &ScriptEditor::register_syntax_highlighter);3738ClassDB::bind_method(D_METHOD("unregister_syntax_highlighter", "syntax_highlighter"), &ScriptEditor::unregister_syntax_highlighter);37393740ClassDB::bind_method(D_METHOD("goto_line", "line_number"), &ScriptEditor::_goto_script_line2);3741ClassDB::bind_method(D_METHOD("get_current_script"), &ScriptEditor::_get_current_script);3742ClassDB::bind_method(D_METHOD("get_open_scripts"), &ScriptEditor::_get_open_scripts);3743ClassDB::bind_method(D_METHOD("open_script_create_dialog", "base_name", "base_path"), &ScriptEditor::open_script_create_dialog);37443745ClassDB::bind_method(D_METHOD("goto_help", "topic"), &ScriptEditor::goto_help);3746ClassDB::bind_method(D_METHOD("update_docs_from_script", "script"), &ScriptEditor::update_docs_from_script);3747ClassDB::bind_method(D_METHOD("clear_docs_from_script", "script"), &ScriptEditor::clear_docs_from_script);37483749ClassDB::bind_method(D_METHOD("save_all_scripts"), &ScriptEditor::save_all_scripts);37503751ADD_SIGNAL(MethodInfo("editor_script_changed", PropertyInfo(Variant::OBJECT, "script", PROPERTY_HINT_RESOURCE_TYPE, Script::get_class_static())));3752ADD_SIGNAL(MethodInfo("script_close", PropertyInfo(Variant::OBJECT, "script", PROPERTY_HINT_RESOURCE_TYPE, Script::get_class_static())));3753}37543755ScriptEditor::ScriptEditor(WindowWrapper *p_wrapper) {3756window_wrapper = p_wrapper;37573758script_editor_cache.instantiate();3759script_editor_cache->load(EditorPaths::get_singleton()->get_project_settings_dir().path_join("script_editor_cache.cfg"));37603761restoring_layout = false;3762waiting_update_names = false;3763pending_auto_reload = false;3764auto_reload_running_scripts = true;3765external_editor_active = false;3766members_overview_enabled = EDITOR_GET("text_editor/script_list/show_members_overview");3767help_overview_enabled = EDITOR_GET("text_editor/help/show_help_index");37683769VBoxContainer *main_container = memnew(VBoxContainer);3770add_child(main_container);37713772menu_hb = memnew(HBoxContainer);3773main_container->add_child(menu_hb);37743775script_split = memnew(HSplitContainer);3776main_container->add_child(script_split);3777script_split->set_v_size_flags(SIZE_EXPAND_FILL);37783779#ifdef ANDROID_ENABLED3780virtual_keyboard_spacer = memnew(Control);3781virtual_keyboard_spacer->set_h_size_flags(SIZE_EXPAND_FILL);3782main_container->add_child(virtual_keyboard_spacer);3783#endif37843785list_split = memnew(VSplitContainer);3786script_split->add_child(list_split);3787list_split->set_v_size_flags(SIZE_EXPAND_FILL);37883789scripts_vbox = memnew(VBoxContainer);3790scripts_vbox->set_v_size_flags(SIZE_EXPAND_FILL);3791list_split->add_child(scripts_vbox);37923793filter_scripts = memnew(LineEdit);3794filter_scripts->set_placeholder(TTRC("Filter Scripts"));3795filter_scripts->set_accessibility_name(TTRC("Filter Scripts"));3796filter_scripts->set_clear_button_enabled(true);3797filter_scripts->connect(SceneStringName(text_changed), callable_mp(this, &ScriptEditor::_filter_scripts_text_changed));3798scripts_vbox->add_child(filter_scripts);37993800_sort_list_on_update = true;3801script_list = memnew(ItemList);3802script_list->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);3803script_list->set_custom_minimum_size(Size2(100, 60) * EDSCALE); //need to give a bit of limit to avoid it from disappearing3804script_list->set_v_size_flags(SIZE_EXPAND_FILL);3805script_list->set_theme_type_variation("ItemListSecondary");3806script_split->set_split_offset(200 * EDSCALE);3807script_list->set_allow_rmb_select(true);3808scripts_vbox->add_child(script_list);3809script_list->connect("item_clicked", callable_mp(this, &ScriptEditor::_script_list_clicked), CONNECT_DEFERRED);3810SET_DRAG_FORWARDING_GCD(script_list, ScriptEditor);38113812context_menu = memnew(PopupMenu);3813add_child(context_menu);3814context_menu->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptEditor::_menu_option));38153816overview_vbox = memnew(VBoxContainer);3817overview_vbox->set_custom_minimum_size(Size2(0, 90));3818overview_vbox->set_v_size_flags(SIZE_EXPAND_FILL);38193820list_split->add_child(overview_vbox);3821list_split->set_visible(EditorSettings::get_singleton()->get_project_metadata("files_panel", "show_files_panel", true));3822buttons_hbox = memnew(HBoxContainer);3823overview_vbox->add_child(buttons_hbox);38243825filter_methods = memnew(LineEdit);3826filter_methods->set_placeholder(TTRC("Filter Methods"));3827filter_methods->set_accessibility_name(TTRC("Filter Methods"));3828filter_methods->set_clear_button_enabled(true);3829filter_methods->set_h_size_flags(SIZE_EXPAND_FILL);3830filter_methods->connect(SceneStringName(text_changed), callable_mp(this, &ScriptEditor::_filter_methods_text_changed));3831buttons_hbox->add_child(filter_methods);38323833members_overview_alphabeta_sort_button = memnew(Button);3834members_overview_alphabeta_sort_button->set_theme_type_variation(SceneStringName(FlatButton));3835members_overview_alphabeta_sort_button->set_tooltip_text(TTRC("Toggle alphabetical sorting of the method list."));3836members_overview_alphabeta_sort_button->set_toggle_mode(true);3837members_overview_alphabeta_sort_button->set_pressed(EDITOR_GET("text_editor/script_list/sort_members_outline_alphabetically"));3838members_overview_alphabeta_sort_button->connect(SceneStringName(toggled), callable_mp(this, &ScriptEditor::_toggle_members_overview_alpha_sort));3839buttons_hbox->add_child(members_overview_alphabeta_sort_button);38403841members_overview = memnew(ItemList);3842members_overview->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);3843members_overview->set_theme_type_variation("ItemListSecondary");3844overview_vbox->add_child(members_overview);38453846members_overview->set_allow_reselect(true);3847members_overview->set_custom_minimum_size(Size2(0, 60) * EDSCALE); //need to give a bit of limit to avoid it from disappearing3848members_overview->set_v_size_flags(SIZE_EXPAND_FILL);3849members_overview->set_allow_rmb_select(true);38503851help_overview = memnew(ItemList);3852help_overview->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);3853help_overview->set_theme_type_variation("ItemListSecondary");3854overview_vbox->add_child(help_overview);3855help_overview->set_allow_reselect(true);3856help_overview->set_custom_minimum_size(Size2(0, 60) * EDSCALE); //need to give a bit of limit to avoid it from disappearing3857help_overview->set_v_size_flags(SIZE_EXPAND_FILL);38583859VBoxContainer *code_editor_container = memnew(VBoxContainer);3860script_split->add_child(code_editor_container);38613862tab_container = memnew(TabContainer);3863tab_container->set_tabs_visible(false);3864tab_container->set_custom_minimum_size(Size2(200, 0) * EDSCALE);3865code_editor_container->add_child(tab_container);3866tab_container->set_h_size_flags(SIZE_EXPAND_FILL);3867tab_container->set_v_size_flags(SIZE_EXPAND_FILL);38683869find_replace_bar = memnew(FindReplaceBar);3870code_editor_container->add_child(find_replace_bar);3871find_replace_bar->hide();38723873ED_SHORTCUT("script_editor/window_sort", TTRC("Sort"));3874ED_SHORTCUT("script_editor/window_move_up", TTRC("Move Up"), KeyModifierMask::SHIFT | KeyModifierMask::ALT | Key::UP);3875ED_SHORTCUT("script_editor/window_move_down", TTRC("Move Down"), KeyModifierMask::SHIFT | KeyModifierMask::ALT | Key::DOWN);3876// FIXME: These should be `Key::GREATER` and `Key::LESS` but those don't work.3877ED_SHORTCUT("script_editor/next_script", TTRC("Next Script"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::PERIOD);3878ED_SHORTCUT("script_editor/prev_script", TTRC("Previous Script"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::COMMA);3879set_process_input(true);3880set_process_shortcut_input(true);38813882file_menu = memnew(MenuButton);3883file_menu->set_flat(false);3884file_menu->set_theme_type_variation("FlatMenuButton");3885file_menu->set_text(TTRC("File"));3886file_menu->set_switch_on_hover(true);3887file_menu->set_shortcut_context(this);3888menu_hb->add_child(file_menu);38893890file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/new", TTRC("New Script..."), KeyModifierMask::CMD_OR_CTRL | Key::N), FILE_MENU_NEW_SCRIPT);3891file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/new_textfile", TTRC("New Text File..."), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::N), FILE_MENU_NEW_TEXTFILE);3892file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/open", TTRC("Open...")), FILE_MENU_OPEN);3893file_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_editor/reopen_closed_script"), FILE_MENU_REOPEN_CLOSED);38943895recent_scripts = memnew(PopupMenu);3896recent_scripts->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);3897file_menu->get_popup()->add_submenu_node_item(TTRC("Open Recent"), recent_scripts, FILE_MENU_OPEN_RECENT);3898recent_scripts->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptEditor::_open_recent_script));38993900_update_recent_scripts();39013902file_menu->get_popup()->add_separator();3903file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/save", TTRC("Save"), KeyModifierMask::ALT | KeyModifierMask::CMD_OR_CTRL | Key::S), FILE_MENU_SAVE);3904file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/save_as", TTRC("Save As...")), FILE_MENU_SAVE_AS);3905file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/save_all", TTRC("Save All"), KeyModifierMask::SHIFT | KeyModifierMask::ALT | Key::S), FILE_MENU_SAVE_ALL);3906ED_SHORTCUT_OVERRIDE("script_editor/save_all", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::S);3907file_menu->get_popup()->add_separator();3908file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/reload_script_soft", TTRC("Soft Reload Tool Script"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::R), FILE_MENU_SOFT_RELOAD_TOOL);3909file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/copy_path", TTRC("Copy File Path")), FILE_MENU_COPY_PATH);3910file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/copy_uid", TTRC("Copy File UID")), FILE_MENU_COPY_UID);3911file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/show_in_file_system", TTRC("Show in FileSystem")), FILE_MENU_SHOW_IN_FILE_SYSTEM);3912file_menu->get_popup()->add_separator();39133914file_menu->get_popup()->add_shortcut(3915ED_SHORTCUT_ARRAY("script_editor/history_previous", TTRC("History Previous"),3916{ int32_t(KeyModifierMask::ALT | Key::LEFT), int32_t(Key::BACK) }),3917FILE_MENU_HISTORY_PREV);3918file_menu->get_popup()->add_shortcut(3919ED_SHORTCUT_ARRAY("script_editor/history_next", TTRC("History Next"),3920{ int32_t(KeyModifierMask::ALT | Key::RIGHT), int32_t(Key::FORWARD) }),3921FILE_MENU_HISTORY_NEXT);3922ED_SHORTCUT_OVERRIDE("script_editor/history_previous", "macos", KeyModifierMask::ALT | KeyModifierMask::META | Key::LEFT);3923ED_SHORTCUT_OVERRIDE("script_editor/history_next", "macos", KeyModifierMask::ALT | KeyModifierMask::META | Key::RIGHT);39243925file_menu->get_popup()->add_separator();39263927theme_submenu = memnew(PopupMenu);3928theme_submenu->add_shortcut(ED_SHORTCUT("script_editor/import_theme", TTRC("Import Theme...")), THEME_IMPORT);3929theme_submenu->add_shortcut(ED_SHORTCUT("script_editor/reload_theme", TTRC("Reload Theme")), THEME_RELOAD);3930file_menu->get_popup()->add_submenu_node_item(TTRC("Theme"), theme_submenu, FILE_MENU_THEME_SUBMENU);3931theme_submenu->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptEditor::_theme_option));39323933theme_submenu->add_separator();3934theme_submenu->add_shortcut(ED_SHORTCUT("script_editor/save_theme_as", TTRC("Save Theme As...")), THEME_SAVE_AS);39353936file_menu->get_popup()->add_separator();3937file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/close_file", TTRC("Close"), KeyModifierMask::CMD_OR_CTRL | Key::W), FILE_MENU_CLOSE);3938file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/close_all", TTRC("Close All")), FILE_MENU_CLOSE_ALL);3939file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/close_other_tabs", TTRC("Close Other Tabs")), FILE_MENU_CLOSE_OTHER_TABS);3940file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/close_tabs_below", TTRC("Close Tabs Below")), FILE_MENU_CLOSE_TABS_BELOW);3941file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/close_docs", TTRC("Close Docs")), FILE_MENU_CLOSE_DOCS);39423943file_menu->get_popup()->add_separator();3944file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/run_file", TTRC("Run"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::X), FILE_MENU_RUN);39453946file_menu->get_popup()->add_separator();3947file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/toggle_files_panel", TTRC("Toggle Files Panel"), KeyModifierMask::CMD_OR_CTRL | Key::BACKSLASH), FILE_MENU_TOGGLE_FILES_PANEL);3948file_menu->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptEditor::_menu_option));3949file_menu->get_popup()->connect("about_to_popup", callable_mp(this, &ScriptEditor::_prepare_file_menu));3950file_menu->get_popup()->connect("popup_hide", callable_mp(this, &ScriptEditor::_file_menu_closed));39513952script_search_menu = memnew(MenuButton);3953script_search_menu->set_flat(false);3954script_search_menu->set_theme_type_variation("FlatMenuButton");3955script_search_menu->set_text(TTRC("Search"));3956script_search_menu->set_switch_on_hover(true);3957script_search_menu->set_shortcut_context(this);3958script_search_menu->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptEditor::_menu_option));3959menu_hb->add_child(script_search_menu);39603961MenuButton *debug_menu_btn = memnew(MenuButton);3962debug_menu_btn->set_flat(false);3963debug_menu_btn->set_theme_type_variation("FlatMenuButton");3964menu_hb->add_child(debug_menu_btn);3965debug_menu_btn->hide(); // Handled by EditorDebuggerNode below.39663967EditorDebuggerNode *debugger = EditorDebuggerNode::get_singleton();3968debugger->set_script_debug_button(debug_menu_btn);3969debugger->connect("goto_script_line", callable_mp(this, &ScriptEditor::_goto_script_line));3970debugger->connect("set_execution", callable_mp(this, &ScriptEditor::_set_execution));3971debugger->connect("clear_execution", callable_mp(this, &ScriptEditor::_clear_execution));3972debugger->connect("breakpoint_set_in_tree", callable_mp(this, &ScriptEditor::_set_breakpoint));3973debugger->connect("breakpoints_cleared_in_tree", callable_mp(this, &ScriptEditor::_clear_breakpoints));39743975script_name_button_hbox = memnew(HBoxContainer);3976script_name_button_hbox->set_h_size_flags(SIZE_EXPAND_FILL);3977script_name_button_hbox->add_theme_constant_override("separation", 0);3978script_name_button_hbox->connect(SceneStringName(item_rect_changed), callable_mp(this, &ScriptEditor::_calculate_script_name_button_ratio));3979menu_hb->add_child(script_name_button_hbox);39803981script_name_button_left_spacer = memnew(Control);3982script_name_button_left_spacer->set_h_size_flags(Control::SIZE_EXPAND_FILL);3983script_name_button_hbox->add_child(script_name_button_left_spacer);39843985script_name_button = memnew(Button);3986script_name_button->set_flat(true);3987script_name_button->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS);3988script_name_button->set_h_size_flags(SIZE_EXPAND_FILL);3989script_name_button->set_tooltip_text(TTRC("Navigate to script list."));3990script_name_button->connect(SceneStringName(pressed), callable_mp(script_list, &ItemList::ensure_current_is_visible));3991script_name_button_hbox->add_child(script_name_button);39923993script_name_button_right_spacer = memnew(Control);3994script_name_button_right_spacer->set_h_size_flags(Control::SIZE_EXPAND_FILL);3995script_name_button_hbox->add_child(script_name_button_right_spacer);39963997site_search = memnew(Button);3998site_search->set_theme_type_variation(SceneStringName(FlatButton));3999site_search->set_accessibility_name(TTRC("Site Search"));4000site_search->connect(SceneStringName(pressed), callable_mp(this, &ScriptEditor::_menu_option).bind(SEARCH_WEBSITE));4001menu_hb->add_child(site_search);40024003help_search = memnew(Button);4004help_search->set_theme_type_variation(SceneStringName(FlatButton));4005help_search->set_text(TTRC("Search Help"));4006help_search->connect(SceneStringName(pressed), callable_mp(this, &ScriptEditor::_menu_option).bind(SEARCH_HELP));4007menu_hb->add_child(help_search);4008help_search->set_tooltip_text(TTRC("Search the reference documentation."));40094010menu_hb->add_child(memnew(VSeparator));40114012script_back = memnew(Button);4013script_back->set_theme_type_variation(SceneStringName(FlatButton));4014script_back->set_tooltip_text(TTRC("Go to previous edited document."));4015script_back->set_shortcut(ED_GET_SHORTCUT("script_editor/history_previous"));4016script_back->set_disabled(true);4017menu_hb->add_child(script_back);4018script_back->connect(SceneStringName(pressed), callable_mp(this, &ScriptEditor::_history_back));40194020script_forward = memnew(Button);4021script_forward->set_theme_type_variation(SceneStringName(FlatButton));4022script_forward->set_tooltip_text(TTRC("Go to next edited document."));4023script_forward->set_shortcut(ED_GET_SHORTCUT("script_editor/history_next"));4024script_forward->set_disabled(true);4025menu_hb->add_child(script_forward);4026script_forward->connect(SceneStringName(pressed), callable_mp(this, &ScriptEditor::_history_forward));40274028menu_hb->add_child(memnew(VSeparator));40294030make_floating = memnew(ScreenSelect);4031make_floating->set_tooltip_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);4032make_floating->connect("request_open_in_screen", callable_mp(window_wrapper, &WindowWrapper::enable_window_on_screen).bind(true));40334034menu_hb->add_child(make_floating);4035p_wrapper->connect("window_visibility_changed", callable_mp(this, &ScriptEditor::_window_changed));40364037tab_container->connect("tab_changed", callable_mp(this, &ScriptEditor::_tab_changed));40384039erase_tab_confirm = memnew(ConfirmationDialog);4040erase_tab_confirm->set_ok_button_text(TTRC("Save"));4041erase_tab_confirm->add_button(TTRC("Discard"), DisplayServer::get_singleton()->get_swap_cancel_ok(), "discard");4042erase_tab_confirm->connect(SceneStringName(confirmed), callable_mp(this, &ScriptEditor::_close_current_tab).bind(true, true));4043erase_tab_confirm->connect("custom_action", callable_mp(this, &ScriptEditor::_close_discard_current_tab));4044add_child(erase_tab_confirm);40454046script_create_dialog = memnew(ScriptCreateDialog);4047script_create_dialog->set_title(TTRC("Create Script"));4048add_child(script_create_dialog);4049script_create_dialog->connect("script_created", callable_mp(this, &ScriptEditor::_script_created));40504051file_dialog_option = -1;4052file_dialog = memnew(EditorFileDialog);4053add_child(file_dialog);4054file_dialog->connect("file_selected", callable_mp(this, &ScriptEditor::_file_dialog_action));40554056error_dialog = memnew(AcceptDialog);4057add_child(error_dialog);40584059disk_changed = memnew(ConfirmationDialog);4060{4061disk_changed->set_title(TTRC("Files have been modified outside Godot"));40624063VBoxContainer *vbc = memnew(VBoxContainer);4064disk_changed->add_child(vbc);40654066Label *files_are_newer_label = memnew(Label);4067files_are_newer_label->set_text(TTRC("The following files are newer on disk:"));4068vbc->add_child(files_are_newer_label);40694070disk_changed_list = memnew(Tree);4071disk_changed_list->set_hide_root(true);4072disk_changed_list->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);4073disk_changed_list->set_accessibility_name(TTRC("The following files are newer on disk:"));4074disk_changed_list->set_v_size_flags(SIZE_EXPAND_FILL);4075vbc->add_child(disk_changed_list);40764077Label *what_action_label = memnew(Label);4078what_action_label->set_text(TTRC("What action should be taken?"));4079vbc->add_child(what_action_label);40804081disk_changed->connect(SceneStringName(confirmed), callable_mp(this, &ScriptEditor::reload_scripts).bind(false));4082disk_changed->set_ok_button_text(TTRC("Reload from disk"));40834084disk_changed->add_button(TTRC("Ignore external changes"), !DisplayServer::get_singleton()->get_swap_cancel_ok(), "resave");4085disk_changed->connect("custom_action", callable_mp(this, &ScriptEditor::_resave_scripts));4086}40874088add_child(disk_changed);40894090script_editor = this;40914092autosave_timer = memnew(Timer);4093autosave_timer->set_one_shot(false);4094autosave_timer->connect(SceneStringName(tree_entered), callable_mp(this, &ScriptEditor::_update_autosave_timer));4095autosave_timer->connect("timeout", callable_mp(this, &ScriptEditor::_autosave_scripts));4096add_child(autosave_timer);40974098grab_focus_block = false;40994100help_search_dialog = memnew(EditorHelpSearch);4101add_child(help_search_dialog);4102help_search_dialog->connect("go_to_help", callable_mp(this, &ScriptEditor::_help_class_goto));41034104find_in_files_dialog = memnew(FindInFilesDialog);4105find_in_files_dialog->connect(FindInFilesDialog::SIGNAL_FIND_REQUESTED, callable_mp(this, &ScriptEditor::_start_find_in_files).bind(false));4106find_in_files_dialog->connect(FindInFilesDialog::SIGNAL_REPLACE_REQUESTED, callable_mp(this, &ScriptEditor::_start_find_in_files).bind(true));4107add_child(find_in_files_dialog);41084109find_in_files = memnew(FindInFilesContainer);4110EditorDockManager::get_singleton()->add_dock(find_in_files);4111find_in_files->close();4112find_in_files->connect("result_selected", callable_mp(this, &ScriptEditor::_on_find_in_files_result_selected));4113find_in_files->connect("files_modified", callable_mp(this, &ScriptEditor::_on_find_in_files_modified_files));41144115history_pos = -1;41164117edit_pass = 0;4118trim_trailing_whitespace_on_save = EDITOR_GET("text_editor/behavior/files/trim_trailing_whitespace_on_save");4119trim_final_newlines_on_save = EDITOR_GET("text_editor/behavior/files/trim_final_newlines_on_save");4120convert_indent_on_save = EDITOR_GET("text_editor/behavior/files/convert_indent_on_save");41214122Ref<EditorJSONSyntaxHighlighter> json_syntax_highlighter;4123json_syntax_highlighter.instantiate();4124register_syntax_highlighter(json_syntax_highlighter);41254126Ref<EditorMarkdownSyntaxHighlighter> markdown_syntax_highlighter;4127markdown_syntax_highlighter.instantiate();4128register_syntax_highlighter(markdown_syntax_highlighter);41294130Ref<EditorConfigFileSyntaxHighlighter> config_file_syntax_highlighter;4131config_file_syntax_highlighter.instantiate();4132register_syntax_highlighter(config_file_syntax_highlighter);41334134_update_online_doc();4135}41364137void ScriptEditorPlugin::_focus_another_editor() {4138if (window_wrapper->get_window_enabled()) {4139ERR_FAIL_COND(last_editor.is_empty());4140EditorInterface::get_singleton()->set_main_screen_editor(last_editor);4141}4142}41434144void ScriptEditorPlugin::_save_last_editor(const String &p_editor) {4145if (p_editor != get_plugin_name()) {4146last_editor = p_editor;4147}4148}41494150void ScriptEditorPlugin::_window_visibility_changed(bool p_visible) {4151_focus_another_editor();4152if (p_visible) {4153script_editor->add_theme_style_override(SceneStringName(panel), script_editor->get_theme_stylebox("ScriptEditorPanelFloating", EditorStringName(EditorStyles)));4154} else {4155script_editor->add_theme_style_override(SceneStringName(panel), script_editor->get_theme_stylebox("ScriptEditorPanel", EditorStringName(EditorStyles)));4156}4157}41584159void ScriptEditorPlugin::_notification(int p_what) {4160switch (p_what) {4161case NOTIFICATION_TRANSLATION_CHANGED: {4162window_wrapper->set_window_title(vformat(TTR("%s - Godot Engine"), TTR("Script Editor")));4163} break;4164case NOTIFICATION_ENTER_TREE: {4165connect("main_screen_changed", callable_mp(this, &ScriptEditorPlugin::_save_last_editor));4166} break;4167case NOTIFICATION_EXIT_TREE: {4168disconnect("main_screen_changed", callable_mp(this, &ScriptEditorPlugin::_save_last_editor));4169} break;4170}4171}41724173bool ScriptEditorPlugin::open_in_external_editor(const String &p_path, int p_line, int p_col, bool p_ignore_project) {4174const String path = EDITOR_GET("text_editor/external/exec_path");4175if (path.is_empty()) {4176return false;4177}41784179String flags = EDITOR_GET("text_editor/external/exec_flags");41804181List<String> args;4182bool has_file_flag = false;41834184if (!flags.is_empty()) {4185flags = flags.replacen("{line}", itos(MAX(p_line + 1, 1)));4186flags = flags.replacen("{col}", itos(p_col + 1));4187flags = flags.strip_edges().replace("\\\\", "\\");41884189int from = 0;4190int num_chars = 0;4191bool inside_quotes = false;41924193for (int i = 0; i < flags.size(); i++) {4194if (flags[i] == '"' && (!i || flags[i - 1] != '\\')) {4195if (!inside_quotes) {4196from++;4197}4198inside_quotes = !inside_quotes;41994200} else if (flags[i] == '\0' || (!inside_quotes && flags[i] == ' ')) {4201String arg = flags.substr(from, num_chars);4202if (arg.contains("{file}")) {4203has_file_flag = true;4204}42054206// Do path replacement here, else there will be issues with spaces and quotes4207if (p_ignore_project) {4208arg = arg.replacen("{project}", String());4209} else {4210arg = arg.replacen("{project}", ProjectSettings::get_singleton()->get_resource_path());4211}4212arg = arg.replacen("{file}", p_path);4213args.push_back(arg);42144215from = i + 1;4216num_chars = 0;4217} else {4218num_chars++;4219}4220}4221}42224223// Default to passing script path if no {file} flag is specified.4224if (!has_file_flag) {4225args.push_back(p_path);4226}4227return OS::get_singleton()->create_process(path, args) == OK;4228}42294230void ScriptEditorPlugin::edit(Object *p_object) {4231if (!p_object) {4232return;4233}42344235if (Object::cast_to<Script>(p_object)) {4236Script *p_script = Object::cast_to<Script>(p_object);4237String res_path = p_script->get_path().get_slice("::", 0);42384239if (p_script->is_built_in() && !res_path.is_empty()) {4240EditorNode::get_singleton()->load_scene_or_resource(res_path, false, false);4241}4242script_editor->edit(p_script);4243} else if (Object::cast_to<JSON>(p_object) || Object::cast_to<TextFile>(p_object)) {4244script_editor->edit(Object::cast_to<Resource>(p_object));4245}4246}42474248bool ScriptEditorPlugin::handles(Object *p_object) const {4249if (Object::cast_to<Script>(p_object)) {4250return true;4251} else if (Object::cast_to<JSON>(p_object)) {4252// This is here to stop resource files of class JSON from getting confused4253// with json files and being opened in the text editor.4254if (Object::cast_to<JSON>(p_object)->get_path().get_extension().to_lower() == "json") {4255return true;4256}4257} else if (Object::cast_to<TextFile>(p_object)) {4258return true;4259}42604261return p_object->is_class("Script");4262}42634264void ScriptEditorPlugin::make_visible(bool p_visible) {4265if (p_visible) {4266window_wrapper->show();4267script_editor->ensure_select_current();4268} else {4269window_wrapper->hide();4270}4271}42724273void ScriptEditorPlugin::selected_notify() {4274script_editor->ensure_select_current();4275_focus_another_editor();4276}42774278String ScriptEditorPlugin::get_unsaved_status(const String &p_for_scene) const {4279const PackedStringArray unsaved_scripts = script_editor->get_unsaved_scripts();4280if (unsaved_scripts.is_empty()) {4281return String();4282}42834284PackedStringArray message;4285if (!p_for_scene.is_empty()) {4286PackedStringArray unsaved_built_in_scripts;42874288const String scene_file = p_for_scene.get_file();4289for (const String &E : unsaved_scripts) {4290if (!E.is_resource_file() && E.contains(scene_file)) {4291unsaved_built_in_scripts.append(E);4292}4293}42944295if (unsaved_built_in_scripts.is_empty()) {4296return String();4297} else {4298message.resize(unsaved_built_in_scripts.size() + 1);4299message.write[0] = TTR("There are unsaved changes in the following built-in script(s):");43004301int i = 1;4302for (const String &E : unsaved_built_in_scripts) {4303message.write[i] = E.trim_suffix("(*)");4304i++;4305}4306return String("\n").join(message);4307}4308}43094310message.resize(unsaved_scripts.size() + 1);4311message.write[0] = TTR("Save changes to the following script(s) before quitting?");43124313int i = 1;4314for (const String &E : unsaved_scripts) {4315message.write[i] = E.trim_suffix("(*)");4316i++;4317}4318return String("\n").join(message);4319}43204321void ScriptEditorPlugin::save_external_data() {4322if (!EditorNode::get_singleton()->is_exiting()) {4323script_editor->save_all_scripts();4324}4325}43264327void ScriptEditorPlugin::apply_changes() {4328script_editor->apply_scripts();4329}43304331void ScriptEditorPlugin::set_window_layout(Ref<ConfigFile> p_layout) {4332script_editor->set_window_layout(p_layout);43334334if (EDITOR_GET("interface/multi_window/restore_windows_on_load") && window_wrapper->is_window_available() && p_layout->has_section_key("ScriptEditor", "window_rect")) {4335window_wrapper->restore_window_from_saved_position(4336p_layout->get_value("ScriptEditor", "window_rect", Rect2i()),4337p_layout->get_value("ScriptEditor", "window_screen", -1),4338p_layout->get_value("ScriptEditor", "window_screen_rect", Rect2i()));4339} else {4340window_wrapper->set_window_enabled(false);4341}4342}43434344void ScriptEditorPlugin::get_window_layout(Ref<ConfigFile> p_layout) {4345script_editor->get_window_layout(p_layout);43464347if (window_wrapper->get_window_enabled()) {4348p_layout->set_value("ScriptEditor", "window_rect", window_wrapper->get_window_rect());4349int screen = window_wrapper->get_window_screen();4350p_layout->set_value("ScriptEditor", "window_screen", screen);4351p_layout->set_value("ScriptEditor", "window_screen_rect", DisplayServer::get_singleton()->screen_get_usable_rect(screen));43524353} else {4354if (p_layout->has_section_key("ScriptEditor", "window_rect")) {4355p_layout->erase_section_key("ScriptEditor", "window_rect");4356}4357if (p_layout->has_section_key("ScriptEditor", "window_screen")) {4358p_layout->erase_section_key("ScriptEditor", "window_screen");4359}4360if (p_layout->has_section_key("ScriptEditor", "window_screen_rect")) {4361p_layout->erase_section_key("ScriptEditor", "window_screen_rect");4362}4363}4364}43654366void ScriptEditorPlugin::get_breakpoints(List<String> *p_breakpoints) {4367script_editor->get_breakpoints(p_breakpoints);4368}43694370void ScriptEditorPlugin::edited_scene_changed() {4371script_editor->edited_scene_changed();4372}43734374ScriptEditorPlugin::ScriptEditorPlugin() {4375ED_SHORTCUT("script_editor/reopen_closed_script", TTRC("Reopen Closed Script"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::T);4376ED_SHORTCUT("script_editor/clear_recent", TTRC("Clear Recent Scripts"));4377ED_SHORTCUT("script_editor/replace_in_files", TTRC("Replace in Files..."), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::R);43784379ED_SHORTCUT("script_text_editor/convert_to_uppercase", TTRC("Uppercase"), KeyModifierMask::SHIFT | Key::F4);4380ED_SHORTCUT("script_text_editor/convert_to_lowercase", TTRC("Lowercase"), KeyModifierMask::SHIFT | Key::F5);4381ED_SHORTCUT("script_text_editor/capitalize", TTRC("Capitalize"), KeyModifierMask::SHIFT | Key::F6);43824383window_wrapper = memnew(WindowWrapper);4384window_wrapper->set_margins_enabled(true);43854386script_editor = memnew(ScriptEditor(window_wrapper));4387Ref<Shortcut> make_floating_shortcut = ED_SHORTCUT_AND_COMMAND("script_editor/make_floating", TTRC("Make Floating"));4388window_wrapper->set_wrapped_control(script_editor, make_floating_shortcut);43894390EditorNode::get_singleton()->get_editor_main_screen()->get_control()->add_child(window_wrapper);4391window_wrapper->set_v_size_flags(Control::SIZE_EXPAND_FILL);4392window_wrapper->hide();4393window_wrapper->connect("window_visibility_changed", callable_mp(this, &ScriptEditorPlugin::_window_visibility_changed));43944395ScriptServer::set_reload_scripts_on_save(EDITOR_GET("text_editor/behavior/files/auto_reload_and_parse_scripts_on_save"));4396}439743984399