Add monospace font styling to more items in signal workflow Use monospace font in method and property selection dialogs Use monospaced font in Animation editor Add editor setting Additional fixes and things Add documentation to editor setting # Conflicts: # editor/inspector/property_selector.cpp Update doc/classes/EditorSettings.xml Co-authored-by: A Thousand Ships <96648715+AThousandShips@users.noreply.github.com>
4414 lines
150 KiB
C++
4414 lines
150 KiB
C++
/**************************************************************************/
|
|
/* script_editor_plugin.cpp */
|
|
/**************************************************************************/
|
|
/* This file is part of: */
|
|
/* GODOT ENGINE */
|
|
/* https://godotengine.org */
|
|
/**************************************************************************/
|
|
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
|
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
|
/* */
|
|
/* Permission is hereby granted, free of charge, to any person obtaining */
|
|
/* a copy of this software and associated documentation files (the */
|
|
/* "Software"), to deal in the Software without restriction, including */
|
|
/* without limitation the rights to use, copy, modify, merge, publish, */
|
|
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
|
/* permit persons to whom the Software is furnished to do so, subject to */
|
|
/* the following conditions: */
|
|
/* */
|
|
/* The above copyright notice and this permission notice shall be */
|
|
/* included in all copies or substantial portions of the Software. */
|
|
/* */
|
|
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
|
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
|
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
|
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
|
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
|
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
|
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
|
/**************************************************************************/
|
|
|
|
#include "script_editor_plugin.h"
|
|
|
|
#include "core/config/project_settings.h"
|
|
#include "core/input/input.h"
|
|
#include "core/io/config_file.h"
|
|
#include "core/io/file_access.h"
|
|
#include "core/io/json.h"
|
|
#include "core/io/resource_loader.h"
|
|
#include "core/os/keyboard.h"
|
|
#include "core/os/os.h"
|
|
#include "core/string/fuzzy_search.h"
|
|
#include "core/version.h"
|
|
#include "editor/debugger/editor_debugger_node.h"
|
|
#include "editor/debugger/script_editor_debugger.h"
|
|
#include "editor/doc/editor_help.h"
|
|
#include "editor/doc/editor_help_search.h"
|
|
#include "editor/docks/editor_dock_manager.h"
|
|
#include "editor/docks/filesystem_dock.h"
|
|
#include "editor/docks/inspector_dock.h"
|
|
#include "editor/docks/signals_dock.h"
|
|
#include "editor/editor_interface.h"
|
|
#include "editor/editor_main_screen.h"
|
|
#include "editor/editor_node.h"
|
|
#include "editor/editor_string_names.h"
|
|
#include "editor/file_system/editor_paths.h"
|
|
#include "editor/gui/code_editor.h"
|
|
#include "editor/gui/editor_file_dialog.h"
|
|
#include "editor/gui/editor_toaster.h"
|
|
#include "editor/gui/filter_line_edit.h"
|
|
#include "editor/gui/window_wrapper.h"
|
|
#include "editor/inspector/editor_context_menu_plugin.h"
|
|
#include "editor/run/editor_run_bar.h"
|
|
#include "editor/scene/editor_scene_tabs.h"
|
|
#include "editor/script/find_in_files.h"
|
|
#include "editor/script/script_text_editor.h"
|
|
#include "editor/script/syntax_highlighters.h"
|
|
#include "editor/script/text_editor.h"
|
|
#include "editor/settings/editor_command_palette.h"
|
|
#include "editor/settings/editor_settings.h"
|
|
#include "editor/shader/shader_editor_plugin.h"
|
|
#include "editor/shader/text_shader_editor.h"
|
|
#include "editor/themes/editor_scale.h"
|
|
#include "editor/themes/editor_theme_manager.h"
|
|
#include "scene/gui/separator.h"
|
|
#include "scene/gui/tab_container.h"
|
|
#include "scene/gui/texture_rect.h"
|
|
#include "scene/main/node.h"
|
|
#include "scene/main/window.h"
|
|
#include "servers/display/display_server.h"
|
|
|
|
void ScriptEditorQuickOpen::popup_dialog(const Vector<String> &p_functions, bool p_dontclear) {
|
|
popup_centered_ratio(0.6);
|
|
if (p_dontclear) {
|
|
search_box->select_all();
|
|
} else {
|
|
search_box->clear();
|
|
}
|
|
search_box->grab_focus();
|
|
functions = p_functions;
|
|
_update_search();
|
|
}
|
|
|
|
void ScriptEditorQuickOpen::_text_changed(const String &p_newtext) {
|
|
_update_search();
|
|
}
|
|
|
|
void ScriptEditorQuickOpen::_update_search() {
|
|
search_options->clear();
|
|
TreeItem *root = search_options->create_item();
|
|
|
|
for (int i = 0; i < functions.size(); i++) {
|
|
String file = functions[i];
|
|
if ((search_box->get_text().is_empty() || file.containsn(search_box->get_text()))) {
|
|
TreeItem *ti = search_options->create_item(root);
|
|
ti->set_text(0, file);
|
|
if (root->get_first_child() == ti) {
|
|
ti->select(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
get_ok_button()->set_disabled(root->get_first_child() == nullptr);
|
|
}
|
|
|
|
void ScriptEditorQuickOpen::_confirmed() {
|
|
TreeItem *ti = search_options->get_selected();
|
|
if (!ti) {
|
|
return;
|
|
}
|
|
int line = ti->get_text(0).get_slicec(':', 1).to_int();
|
|
|
|
emit_signal(SNAME("goto_line"), line - 1);
|
|
hide();
|
|
}
|
|
|
|
void ScriptEditorQuickOpen::_notification(int p_what) {
|
|
switch (p_what) {
|
|
case NOTIFICATION_ENTER_TREE: {
|
|
connect(SceneStringName(confirmed), callable_mp(this, &ScriptEditorQuickOpen::_confirmed));
|
|
} break;
|
|
|
|
case NOTIFICATION_EXIT_TREE: {
|
|
disconnect(SceneStringName(confirmed), callable_mp(this, &ScriptEditorQuickOpen::_confirmed));
|
|
} break;
|
|
}
|
|
}
|
|
|
|
void ScriptEditorQuickOpen::_bind_methods() {
|
|
ADD_SIGNAL(MethodInfo("goto_line", PropertyInfo(Variant::INT, "line")));
|
|
}
|
|
|
|
ScriptEditorQuickOpen::ScriptEditorQuickOpen() {
|
|
set_ok_button_text(TTRC("Open"));
|
|
get_ok_button()->set_disabled(true);
|
|
set_hide_on_ok(false);
|
|
|
|
VBoxContainer *vbc = memnew(VBoxContainer);
|
|
add_child(vbc);
|
|
|
|
search_box = memnew(FilterLineEdit);
|
|
vbc->add_margin_child(TTRC("Search:"), search_box);
|
|
search_box->connect(SceneStringName(text_changed), callable_mp(this, &ScriptEditorQuickOpen::_text_changed));
|
|
register_text_enter(search_box);
|
|
|
|
search_options = memnew(Tree);
|
|
search_box->set_forward_control(search_options);
|
|
search_options->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
|
|
search_options->set_hide_root(true);
|
|
search_options->set_hide_folding(true);
|
|
search_options->add_theme_constant_override("draw_guides", 1);
|
|
vbc->add_margin_child(TTRC("Matches:"), search_options, true);
|
|
search_options->connect("item_activated", callable_mp(this, &ScriptEditorQuickOpen::_confirmed));
|
|
}
|
|
|
|
/////////////////////////////////
|
|
|
|
ScriptEditor *ScriptEditor::script_editor = nullptr;
|
|
|
|
/*** SCRIPT EDITOR ******/
|
|
|
|
String ScriptEditor::_get_debug_tooltip(const String &p_text, Node *p_se) {
|
|
if (EDITOR_GET("text_editor/behavior/documentation/enable_tooltips")) {
|
|
return String();
|
|
}
|
|
|
|
// NOTE: See also `ScriptTextEditor::_show_symbol_tooltip()` for documentation tooltips enabled.
|
|
String debug_value = EditorDebuggerNode::get_singleton()->get_var_value(p_text);
|
|
if (!debug_value.is_empty()) {
|
|
constexpr int DISPLAY_LIMIT = 1024;
|
|
if (debug_value.size() > DISPLAY_LIMIT) {
|
|
debug_value = debug_value.left(DISPLAY_LIMIT) + "... " + TTR("(truncated)");
|
|
}
|
|
debug_value = TTR("Current value: ") + debug_value;
|
|
}
|
|
|
|
return debug_value;
|
|
}
|
|
|
|
void ScriptEditor::_script_created(Ref<Script> p_script) {
|
|
EditorNode::get_singleton()->push_item(p_script.operator->());
|
|
}
|
|
|
|
void ScriptEditor::_goto_script_line2(int p_line) {
|
|
if (TextEditorBase *current = Object::cast_to<TextEditorBase>(_get_current_editor())) {
|
|
current->goto_line(p_line);
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_goto_script_line(Ref<RefCounted> p_script, int p_line) {
|
|
Ref<Script> scr = Object::cast_to<Script>(*p_script);
|
|
if (scr.is_valid() && (scr->has_source_code() || scr->get_path().is_resource_file())) {
|
|
if (edit(p_script, p_line, 0)) {
|
|
EditorNode::get_singleton()->push_item(p_script.ptr());
|
|
|
|
if (TextEditorBase *current = Object::cast_to<TextEditorBase>(_get_current_editor())) {
|
|
current->goto_line_centered(p_line);
|
|
}
|
|
|
|
_save_history();
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_change_execution(Ref<RefCounted> p_script, int p_line, bool p_set) {
|
|
Ref<Script> scr = Object::cast_to<Script>(*p_script);
|
|
if (scr.is_valid() && (scr->has_source_code() || scr->get_path().is_resource_file())) {
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(tab_container->get_tab_control(i))) {
|
|
if ((scr.is_valid() && teb->get_edited_resource() == p_script) || teb->get_edited_resource()->get_path() == scr->get_path()) {
|
|
if (p_set) {
|
|
teb->set_executing_line(p_line);
|
|
} else {
|
|
teb->clear_executing_line();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_set_breakpoint(Ref<RefCounted> p_script, int p_line, bool p_enabled) {
|
|
Ref<Script> scr = Object::cast_to<Script>(*p_script);
|
|
if (scr.is_valid() && (scr->has_source_code() || scr->get_path().is_resource_file())) {
|
|
// Update if open.
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
CodeEditorBase *ceb = Object::cast_to<CodeEditorBase>(tab_container->get_tab_control(i));
|
|
if (ceb && ceb->get_edited_resource()->get_path() == scr->get_path()) {
|
|
ceb->set_breakpoint(p_line, p_enabled);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Handle closed.
|
|
Dictionary state = script_editor_cache->get_value(scr->get_path(), "state");
|
|
Array breakpoints;
|
|
if (state.has("breakpoints")) {
|
|
breakpoints = state["breakpoints"];
|
|
}
|
|
|
|
if (breakpoints.has(p_line)) {
|
|
if (!p_enabled) {
|
|
breakpoints.erase(p_line);
|
|
}
|
|
} else if (p_enabled) {
|
|
breakpoints.push_back(p_line);
|
|
}
|
|
state["breakpoints"] = breakpoints;
|
|
script_editor_cache->set_value(scr->get_path(), "state", state);
|
|
EditorDebuggerNode::get_singleton()->set_breakpoint(scr->get_path(), p_line + 1, p_enabled);
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_clear_breakpoints() {
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
if (CodeEditorBase *ceb = Object::cast_to<CodeEditorBase>(tab_container->get_tab_control(i))) {
|
|
ceb->clear_breakpoints();
|
|
}
|
|
}
|
|
|
|
// Clear from closed scripts.
|
|
Vector<String> cached_editors = script_editor_cache->get_sections();
|
|
for (const String &E : cached_editors) {
|
|
Array breakpoints = _get_cached_breakpoints_for_script(E);
|
|
for (int breakpoint : breakpoints) {
|
|
EditorDebuggerNode::get_singleton()->set_breakpoint(E, (int)breakpoint + 1, false);
|
|
}
|
|
|
|
if (breakpoints.size() > 0) {
|
|
Dictionary state = script_editor_cache->get_value(E, "state");
|
|
state["breakpoints"] = Array();
|
|
script_editor_cache->set_value(E, "state", state);
|
|
}
|
|
}
|
|
}
|
|
|
|
Array ScriptEditor::_get_cached_breakpoints_for_script(const String &p_path) const {
|
|
if (!ResourceLoader::exists(p_path, "Script") || p_path.begins_with("local://") || !script_editor_cache->has_section_key(p_path, "state")) {
|
|
return Array();
|
|
}
|
|
|
|
Dictionary state = script_editor_cache->get_value(p_path, "state");
|
|
if (!state.has("breakpoints")) {
|
|
return Array();
|
|
}
|
|
return state["breakpoints"];
|
|
}
|
|
|
|
ScriptEditorBase *ScriptEditor::_get_current_editor() const {
|
|
int selected = tab_container->get_current_tab();
|
|
if (selected < 0 || selected >= tab_container->get_tab_count()) {
|
|
return nullptr;
|
|
}
|
|
|
|
return Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(selected));
|
|
}
|
|
|
|
void ScriptEditor::_update_history_arrows() {
|
|
script_back->set_disabled(history_pos <= 0);
|
|
script_forward->set_disabled(history_pos >= history.size() - 1);
|
|
}
|
|
|
|
void ScriptEditor::_save_history() {
|
|
if (history_pos >= 0 && history_pos < history.size() && history[history_pos].control == tab_container->get_current_tab_control()) {
|
|
Node *n = tab_container->get_current_tab_control();
|
|
|
|
if (Object::cast_to<TextEditorBase>(n)) {
|
|
history.write[history_pos].state = Object::cast_to<TextEditorBase>(n)->get_navigation_state();
|
|
}
|
|
if (Object::cast_to<EditorHelp>(n)) {
|
|
history.write[history_pos].state = Object::cast_to<EditorHelp>(n)->get_scroll();
|
|
}
|
|
}
|
|
|
|
history.resize(history_pos + 1);
|
|
ScriptHistory sh;
|
|
sh.control = tab_container->get_current_tab_control();
|
|
sh.state = Variant();
|
|
|
|
history.push_back(sh);
|
|
history_pos++;
|
|
|
|
_update_history_arrows();
|
|
}
|
|
|
|
void ScriptEditor::_save_previous_state(Dictionary p_state) {
|
|
if (lock_history) {
|
|
// Done as a result of a deferred call triggered by set_edit_state().
|
|
return;
|
|
}
|
|
|
|
if (history_pos >= 0 && history_pos < history.size() && history[history_pos].control == tab_container->get_current_tab_control()) {
|
|
Node *n = tab_container->get_current_tab_control();
|
|
|
|
if (Object::cast_to<ScriptTextEditor>(n)) {
|
|
history.write[history_pos].state = p_state;
|
|
}
|
|
}
|
|
|
|
history.resize(history_pos + 1);
|
|
ScriptHistory sh;
|
|
sh.control = tab_container->get_current_tab_control();
|
|
sh.state = Variant();
|
|
|
|
history.push_back(sh);
|
|
history_pos++;
|
|
|
|
_update_history_arrows();
|
|
}
|
|
|
|
void ScriptEditor::_go_to_tab(int p_idx) {
|
|
if (ScriptEditorBase *current = _get_current_editor()) {
|
|
if (current->is_unsaved()) {
|
|
current->apply_code();
|
|
}
|
|
}
|
|
|
|
Control *c = tab_container->get_tab_control(p_idx);
|
|
if (!c) {
|
|
return;
|
|
}
|
|
|
|
if (history_pos >= 0 && history_pos < history.size() && history[history_pos].control == tab_container->get_current_tab_control()) {
|
|
Node *n = tab_container->get_current_tab_control();
|
|
|
|
if (Object::cast_to<TextEditorBase>(n)) {
|
|
history.write[history_pos].state = Object::cast_to<TextEditorBase>(n)->get_navigation_state();
|
|
}
|
|
if (Object::cast_to<EditorHelp>(n)) {
|
|
history.write[history_pos].state = Object::cast_to<EditorHelp>(n)->get_scroll();
|
|
}
|
|
}
|
|
|
|
history.resize(history_pos + 1);
|
|
ScriptHistory sh;
|
|
sh.control = c;
|
|
sh.state = Variant();
|
|
|
|
if (!lock_history && (history.is_empty() || history[history.size() - 1].control != sh.control)) {
|
|
history.push_back(sh);
|
|
history_pos++;
|
|
}
|
|
|
|
tab_container->set_current_tab(p_idx);
|
|
|
|
c = tab_container->get_current_tab_control();
|
|
|
|
if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(c)) {
|
|
TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb);
|
|
if (teb && is_visible_in_tree()) {
|
|
teb->ensure_focus();
|
|
}
|
|
|
|
Ref<Script> scr = seb->get_edited_resource();
|
|
if (scr.is_valid()) {
|
|
notify_script_changed(scr);
|
|
}
|
|
|
|
seb->validate_script();
|
|
}
|
|
|
|
if (EditorHelp *eh = Object::cast_to<EditorHelp>(c)) {
|
|
script_name_button->set_text(eh->get_class());
|
|
_calculate_script_name_button_size();
|
|
|
|
if (is_visible_in_tree()) {
|
|
eh->set_focused();
|
|
}
|
|
}
|
|
|
|
c->set_meta("__editor_pass", ++edit_pass);
|
|
_update_history_arrows();
|
|
_update_script_colors();
|
|
_update_members_overview();
|
|
_update_help_overview();
|
|
_update_selected_editor_menu();
|
|
_update_online_doc();
|
|
_update_members_overview_visibility();
|
|
_update_help_overview_visibility();
|
|
}
|
|
|
|
void ScriptEditor::_add_recent_script(const String &p_path) {
|
|
if (p_path.is_empty()) {
|
|
return;
|
|
}
|
|
|
|
Array rc = EditorSettings::get_singleton()->get_project_metadata("recent_files", "scripts", Array());
|
|
if (rc.has(p_path)) {
|
|
rc.erase(p_path);
|
|
}
|
|
rc.push_front(p_path);
|
|
if (rc.size() > 10) {
|
|
rc.resize(10);
|
|
}
|
|
|
|
EditorSettings::get_singleton()->set_project_metadata("recent_files", "scripts", rc);
|
|
_update_recent_scripts();
|
|
}
|
|
|
|
void ScriptEditor::_update_recent_scripts() {
|
|
Array rc = EditorSettings::get_singleton()->get_project_metadata("recent_files", "scripts", Array());
|
|
recent_scripts->clear();
|
|
|
|
String path;
|
|
for (int i = 0; i < rc.size(); i++) {
|
|
path = rc[i];
|
|
recent_scripts->add_item(path.replace("res://", ""));
|
|
}
|
|
|
|
recent_scripts->add_separator();
|
|
recent_scripts->add_shortcut(ED_GET_SHORTCUT("script_editor/clear_recent"));
|
|
recent_scripts->set_item_auto_translate_mode(-1, AUTO_TRANSLATE_MODE_ALWAYS);
|
|
recent_scripts->set_item_disabled(-1, rc.is_empty());
|
|
|
|
recent_scripts->reset_size();
|
|
}
|
|
|
|
void ScriptEditor::_open_recent_script(int p_idx) {
|
|
// clear button
|
|
if (p_idx == recent_scripts->get_item_count() - 1) {
|
|
EditorSettings::get_singleton()->set_project_metadata("recent_files", "scripts", Array());
|
|
callable_mp(this, &ScriptEditor::_update_recent_scripts).call_deferred();
|
|
return;
|
|
}
|
|
|
|
Array rc = EditorSettings::get_singleton()->get_project_metadata("recent_files", "scripts", Array());
|
|
ERR_FAIL_INDEX(p_idx, rc.size());
|
|
|
|
String path = rc[p_idx];
|
|
// if its not on disk its a help file or deleted
|
|
if (FileAccess::exists(path)) {
|
|
if (_get_recognized_extensions().find(path.get_extension())) {
|
|
Ref<Resource> scr = ResourceLoader::load(path);
|
|
if (scr.is_valid()) {
|
|
edit(scr, true);
|
|
return;
|
|
}
|
|
}
|
|
|
|
Error err;
|
|
Ref<TextFile> text_file = _load_text_file(path, &err);
|
|
if (text_file.is_valid()) {
|
|
edit(text_file, true);
|
|
return;
|
|
}
|
|
// if it's a path then it's most likely a deleted file not help
|
|
} else if (path.contains("::")) {
|
|
// built-in script
|
|
String res_path = path.get_slice("::", 0);
|
|
EditorNode::get_singleton()->load_scene_or_resource(res_path, false, false);
|
|
|
|
Ref<Script> scr = ResourceLoader::load(path);
|
|
if (scr.is_valid()) {
|
|
edit(scr, true);
|
|
return;
|
|
}
|
|
} else if (!path.is_resource_file()) {
|
|
_help_class_open(path);
|
|
return;
|
|
}
|
|
|
|
rc.remove_at(p_idx);
|
|
EditorSettings::get_singleton()->set_project_metadata("recent_files", "scripts", rc);
|
|
_update_recent_scripts();
|
|
_show_error_dialog(path);
|
|
}
|
|
|
|
void ScriptEditor::_show_error_dialog(const String &p_path) {
|
|
error_dialog->set_text(vformat(TTR("Can't open '%s'. The file could have been moved or deleted."), p_path));
|
|
error_dialog->popup_centered();
|
|
}
|
|
|
|
void ScriptEditor::_close_tab(int p_idx, bool p_save, bool p_history_back) {
|
|
int selected = p_idx;
|
|
if (selected < 0 || selected >= tab_container->get_tab_count()) {
|
|
return;
|
|
}
|
|
|
|
Node *tselected = tab_container->get_tab_control(selected);
|
|
|
|
if (ScriptEditorBase *current = Object::cast_to<ScriptEditorBase>(tselected)) {
|
|
Ref<Resource> file = current->get_edited_resource();
|
|
if (p_save && file.is_valid()) {
|
|
// Do not try to save internal scripts, but prompt to save in-memory
|
|
// scripts which are not saved to disk yet (have empty path).
|
|
if (!file->is_built_in()) {
|
|
save_current_script();
|
|
}
|
|
}
|
|
if (file.is_valid()) {
|
|
if (!file->get_path().is_empty()) {
|
|
// Only saved scripts can be restored.
|
|
previous_scripts.push_back(file->get_path());
|
|
}
|
|
|
|
Ref<Script> scr = file;
|
|
if (scr.is_valid()) {
|
|
notify_script_close(scr);
|
|
}
|
|
}
|
|
}
|
|
|
|
// roll back to previous tab
|
|
if (p_history_back) {
|
|
_history_back();
|
|
}
|
|
|
|
//remove from history
|
|
history.resize(history_pos + 1);
|
|
|
|
for (int i = 0; i < history.size(); i++) {
|
|
if (history[i].control == tselected) {
|
|
history.remove_at(i);
|
|
i--;
|
|
history_pos--;
|
|
}
|
|
}
|
|
|
|
if (history_pos >= history.size()) {
|
|
history_pos = history.size() - 1;
|
|
}
|
|
|
|
int idx = tab_container->get_current_tab();
|
|
if (TextEditorBase *current = Object::cast_to<TextEditorBase>(tselected)) {
|
|
_save_editor_state(current);
|
|
}
|
|
memdelete(tselected);
|
|
|
|
if (script_close_queue.is_empty()) {
|
|
if (idx >= tab_container->get_tab_count()) {
|
|
idx = tab_container->get_tab_count() - 1;
|
|
}
|
|
if (idx >= 0) {
|
|
if (history_pos >= 0) {
|
|
idx = tab_container->get_tab_idx_from_control(history[history_pos].control);
|
|
}
|
|
_go_to_tab(idx);
|
|
} else {
|
|
_update_selected_editor_menu();
|
|
_update_online_doc();
|
|
script_name_button->set_text(String());
|
|
_calculate_script_name_button_size();
|
|
}
|
|
|
|
_update_history_arrows();
|
|
_update_script_names();
|
|
_save_layout();
|
|
_update_find_replace_bar();
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_close_current_tab(bool p_save, bool p_history_back) {
|
|
_close_tab(tab_container->get_current_tab(), p_save, p_history_back);
|
|
}
|
|
|
|
void ScriptEditor::_close_discard_current_tab(const String &p_str) {
|
|
Ref<Script> scr = _get_current_script();
|
|
if (scr.is_valid()) {
|
|
scr->reload_from_file();
|
|
}
|
|
_close_tab(tab_container->get_current_tab(), false);
|
|
erase_tab_confirm->hide();
|
|
}
|
|
|
|
void ScriptEditor::_close_docs_tab() {
|
|
int child_count = tab_container->get_tab_count();
|
|
for (int i = child_count - 1; i >= 0; i--) {
|
|
if (Object::cast_to<EditorHelp>(tab_container->get_tab_control(i))) {
|
|
_close_tab(i, true, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_copy_script_path() {
|
|
if (ScriptEditorBase *seb = _get_current_editor()) {
|
|
Ref<Resource> scr = seb->get_edited_resource();
|
|
DisplayServer::get_singleton()->clipboard_set(scr->get_path());
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_copy_script_uid() {
|
|
if (ScriptEditorBase *seb = _get_current_editor()) {
|
|
Ref<Resource> scr = seb->get_edited_resource();
|
|
ResourceUID::ID uid = ResourceLoader::get_resource_uid(scr->get_path());
|
|
DisplayServer::get_singleton()->clipboard_set(ResourceUID::get_singleton()->id_to_text(uid));
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_close_other_tabs() {
|
|
int current_idx = tab_container->get_current_tab();
|
|
for (int i = tab_container->get_tab_count() - 1; i >= 0; i--) {
|
|
if (i != current_idx) {
|
|
script_close_queue.push_back(i);
|
|
}
|
|
}
|
|
_queue_close_tabs();
|
|
}
|
|
|
|
void ScriptEditor::_close_tabs_below() {
|
|
int current_idx = tab_container->get_current_tab();
|
|
for (int i = tab_container->get_tab_count() - 1; i > current_idx; i--) {
|
|
script_close_queue.push_back(i);
|
|
}
|
|
_go_to_tab(current_idx);
|
|
_queue_close_tabs();
|
|
}
|
|
|
|
void ScriptEditor::_close_all_tabs() {
|
|
for (int i = tab_container->get_tab_count() - 1; i >= 0; i--) {
|
|
script_close_queue.push_back(i);
|
|
}
|
|
_queue_close_tabs();
|
|
}
|
|
|
|
void ScriptEditor::_queue_close_tabs() {
|
|
while (!script_close_queue.is_empty()) {
|
|
int idx = script_close_queue.front()->get();
|
|
script_close_queue.pop_front();
|
|
|
|
tab_container->set_current_tab(idx);
|
|
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(idx));
|
|
if (seb) {
|
|
// Maybe there are unsaved changes.
|
|
if (seb->is_unsaved()) {
|
|
_ask_close_current_unsaved_tab(seb);
|
|
erase_tab_confirm->connect(SceneStringName(visibility_changed), callable_mp(this, &ScriptEditor::_queue_close_tabs), CONNECT_ONE_SHOT);
|
|
break;
|
|
}
|
|
}
|
|
|
|
_close_current_tab(false, false);
|
|
}
|
|
_update_find_replace_bar();
|
|
}
|
|
|
|
void ScriptEditor::_ask_close_current_unsaved_tab(ScriptEditorBase *current) {
|
|
erase_tab_confirm->set_text(TTR("Close and save changes?") + "\n\"" + current->get_name() + "\"");
|
|
erase_tab_confirm->popup_centered();
|
|
}
|
|
|
|
void ScriptEditor::_resave_scripts(const String &p_str) {
|
|
apply_scripts();
|
|
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
|
|
if (!seb) {
|
|
continue;
|
|
}
|
|
|
|
Ref<Resource> scr = seb->get_edited_resource();
|
|
|
|
if (scr->is_built_in()) {
|
|
continue; // Internal script, who cares.
|
|
}
|
|
|
|
_auto_format_text(seb);
|
|
|
|
Ref<TextFile> text_file = scr;
|
|
if (text_file.is_valid()) {
|
|
seb->apply_code();
|
|
_save_text_file(text_file, text_file->get_path());
|
|
break;
|
|
} else {
|
|
EditorNode::get_singleton()->save_resource(scr);
|
|
}
|
|
seb->tag_saved_version();
|
|
}
|
|
|
|
disk_changed->hide();
|
|
}
|
|
|
|
void ScriptEditor::_res_saved_callback(const Ref<Resource> &p_res) {
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i))) {
|
|
if (seb->get_edited_resource() == p_res) {
|
|
seb->tag_saved_version();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (p_res.is_valid()) {
|
|
// In case the Resource has built-in scripts.
|
|
_mark_built_in_scripts_as_saved(p_res->get_path());
|
|
}
|
|
|
|
_update_script_names();
|
|
Ref<Script> scr = p_res;
|
|
if (scr.is_valid()) {
|
|
trigger_live_script_reload(scr->get_path());
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_scene_saved_callback(const String &p_path) {
|
|
// If scene was saved, mark all built-in scripts from that scene as saved.
|
|
_mark_built_in_scripts_as_saved(p_path);
|
|
}
|
|
|
|
void ScriptEditor::_mark_built_in_scripts_as_saved(const String &p_parent_path) {
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
|
|
if (!seb) {
|
|
continue;
|
|
}
|
|
|
|
Ref<Resource> edited_res = seb->get_edited_resource();
|
|
if (!edited_res->is_built_in()) {
|
|
continue; // External script, who cares.
|
|
}
|
|
|
|
if (edited_res->get_path().get_slice("::", 0) != p_parent_path) {
|
|
continue; // Wrong scene.
|
|
}
|
|
seb->tag_saved_version();
|
|
|
|
Ref<Script> scr = edited_res;
|
|
if (scr.is_valid()) {
|
|
trigger_live_script_reload(scr->get_path());
|
|
clear_docs_from_script(scr);
|
|
scr->reload(true);
|
|
update_docs_from_script(scr);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::trigger_live_script_reload(const String &p_script_path) {
|
|
if (!script_paths_to_reload.has(p_script_path)) {
|
|
Ref<Script> reloaded_script = ResourceCache::get_ref(p_script_path);
|
|
if (reloaded_script.is_null()) {
|
|
reloaded_script = ResourceLoader::load(p_script_path);
|
|
}
|
|
if (reloaded_script.is_valid()) {
|
|
if (!reloaded_script->get_language()->validate(reloaded_script->get_source_code(), p_script_path)) {
|
|
// Script has errors, don't live reload.
|
|
return;
|
|
}
|
|
}
|
|
|
|
script_paths_to_reload.append(p_script_path);
|
|
}
|
|
if (!pending_auto_reload && auto_reload_running_scripts) {
|
|
callable_mp(this, &ScriptEditor::_live_auto_reload_running_scripts).call_deferred();
|
|
pending_auto_reload = true;
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_live_auto_reload_running_scripts() {
|
|
pending_auto_reload = false;
|
|
EditorDebuggerNode::get_singleton()->reload_scripts(script_paths_to_reload);
|
|
script_paths_to_reload.clear();
|
|
}
|
|
|
|
bool ScriptEditor::_test_script_times_on_disk(Ref<Resource> p_for_script) {
|
|
disk_changed_list->clear();
|
|
TreeItem *r = disk_changed_list->create_item();
|
|
|
|
bool need_ask = false;
|
|
bool need_reload = false;
|
|
bool use_autoreload = EDITOR_GET("text_editor/behavior/files/auto_reload_scripts_on_external_change");
|
|
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
|
|
if (!seb) {
|
|
continue;
|
|
}
|
|
|
|
Ref<Resource> edited_res = seb->get_edited_resource();
|
|
if (p_for_script.is_valid() && edited_res.is_valid() && p_for_script != edited_res) {
|
|
continue;
|
|
}
|
|
|
|
if (edited_res->is_built_in()) {
|
|
continue; // Internal script, who cares.
|
|
}
|
|
|
|
uint64_t last_date = seb->edited_file_data.last_modified_time;
|
|
uint64_t date = FileAccess::get_modified_time(seb->edited_file_data.path);
|
|
|
|
if (last_date != date) {
|
|
TreeItem *ti = disk_changed_list->create_item(r);
|
|
ti->set_text(0, seb->edited_file_data.path.get_file());
|
|
|
|
if (!use_autoreload || seb->is_unsaved()) {
|
|
need_ask = true;
|
|
}
|
|
need_reload = true;
|
|
}
|
|
}
|
|
|
|
if (need_reload) {
|
|
if (!need_ask) {
|
|
script_editor->reload_scripts();
|
|
need_reload = false;
|
|
} else {
|
|
callable_mp((Window *)disk_changed, &Window::popup_centered_ratio).call_deferred(0.3);
|
|
}
|
|
}
|
|
|
|
return need_reload;
|
|
}
|
|
|
|
void _import_text_editor_theme(const String &p_file) {
|
|
if (p_file.get_extension() != "tet") {
|
|
EditorToaster::get_singleton()->popup_str(TTR("Importing theme failed. File is not a text editor theme file (.tet)."), EditorToaster::SEVERITY_ERROR);
|
|
return;
|
|
}
|
|
const String theme_name = p_file.get_file().get_basename();
|
|
if (EditorSettings::is_default_text_editor_theme(theme_name.to_lower())) {
|
|
EditorToaster::get_singleton()->popup_str(TTR("Importing theme failed. File name cannot be 'Default', 'Custom', or 'Godot 2'."), EditorToaster::SEVERITY_ERROR);
|
|
return;
|
|
}
|
|
|
|
const String theme_dir = EditorPaths::get_singleton()->get_text_editor_themes_dir();
|
|
Ref<DirAccess> d = DirAccess::open(theme_dir);
|
|
Error err = FAILED;
|
|
if (d.is_valid()) {
|
|
err = d->copy(p_file, theme_dir.path_join(p_file.get_file()));
|
|
}
|
|
|
|
if (err != OK) {
|
|
EditorToaster::get_singleton()->popup_str(TTR("Importing theme failed. Failed to copy theme file."), EditorToaster::SEVERITY_ERROR);
|
|
return;
|
|
}
|
|
|
|
// Reload themes and switch to new theme.
|
|
EditorSettings::get_singleton()->update_text_editor_themes_list();
|
|
EditorSettings::get_singleton()->set_manually("text_editor/theme/color_theme", theme_name, true);
|
|
EditorSettings::get_singleton()->notify_changes();
|
|
}
|
|
|
|
void _save_text_editor_theme_as(const String &p_file) {
|
|
String file = p_file;
|
|
if (p_file.get_extension() != "tet") {
|
|
file += ".tet";
|
|
}
|
|
|
|
const String theme_name = file.get_file().get_basename();
|
|
if (EditorSettings::is_default_text_editor_theme(theme_name.to_lower())) {
|
|
EditorToaster::get_singleton()->popup_str(TTR("Saving theme failed. File name cannot be 'Default', 'Custom', or 'Godot 2'."), EditorToaster::SEVERITY_ERROR);
|
|
return;
|
|
}
|
|
|
|
const String theme_section = "color_theme";
|
|
const Ref<ConfigFile> cf = memnew(ConfigFile);
|
|
|
|
// Use the keys from the Godot 2 theme to know which settings to save.
|
|
HashMap<StringName, Color> text_colors = EditorSettings::get_godot2_text_editor_theme();
|
|
text_colors.sort();
|
|
for (const KeyValue<StringName, Color> &text_color : text_colors) {
|
|
const Color val = EditorSettings::get_singleton()->get_setting(text_color.key);
|
|
const String &key = text_color.key.operator String().replace("text_editor/theme/highlighting/", "");
|
|
cf->set_value(theme_section, key, val.to_html());
|
|
}
|
|
|
|
const Error err = cf->save(file);
|
|
if (err != OK) {
|
|
EditorToaster::get_singleton()->popup_str(TTR("Saving theme failed."), EditorToaster::SEVERITY_ERROR);
|
|
return;
|
|
}
|
|
|
|
// Reload themes and switch to saved theme.
|
|
EditorSettings::get_singleton()->update_text_editor_themes_list();
|
|
if (p_file.get_base_dir() == EditorPaths::get_singleton()->get_text_editor_themes_dir()) {
|
|
// Don't need to emit signal or notify changes as the colors are already set.
|
|
EditorSettings::get_singleton()->set_manually("text_editor/theme/color_theme", theme_name, false);
|
|
}
|
|
}
|
|
|
|
bool ScriptEditor::_script_exists(const String &p_path) const {
|
|
if (p_path.is_empty()) {
|
|
return false;
|
|
} else if (p_path.is_resource_file()) {
|
|
return FileAccess::exists(p_path);
|
|
} else {
|
|
return FileAccess::exists(p_path.get_slice("::", 0));
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_file_dialog_action(const String &p_file) {
|
|
switch (file_dialog_option) {
|
|
case FILE_MENU_NEW_TEXTFILE: {
|
|
Error err;
|
|
{
|
|
Ref<FileAccess> file = FileAccess::open(p_file, FileAccess::WRITE, &err);
|
|
if (err) {
|
|
EditorNode::get_singleton()->show_warning(TTR("Error writing TextFile:") + "\n" + p_file, TTR("Error!"));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (EditorFileSystem::get_singleton()) {
|
|
if (textfile_extensions.has(p_file.get_extension())) {
|
|
EditorFileSystem::get_singleton()->update_file(p_file);
|
|
}
|
|
}
|
|
[[fallthrough]];
|
|
}
|
|
case FILE_MENU_OPEN: {
|
|
if (!is_visible_in_tree()) {
|
|
// When created from outside the editor.
|
|
EditorNode::get_singleton()->get_editor_main_screen()->select(EditorMainScreen::EDITOR_SCRIPT);
|
|
}
|
|
open_file(p_file);
|
|
} break;
|
|
case FILE_MENU_SAVE_AS: {
|
|
if (ScriptEditorBase *current = _get_current_editor()) {
|
|
Ref<Resource> resource = current->get_edited_resource();
|
|
String path = ProjectSettings::get_singleton()->localize_path(p_file);
|
|
Error err = _save_text_file(resource, path);
|
|
|
|
if (err != OK) {
|
|
EditorNode::get_singleton()->show_accept(TTR("Error saving file!"), TTR("OK"));
|
|
return;
|
|
}
|
|
|
|
resource->set_path(path);
|
|
_update_script_names();
|
|
}
|
|
} break;
|
|
case THEME_SAVE_AS: {
|
|
_save_text_editor_theme_as(p_file);
|
|
} break;
|
|
case THEME_IMPORT: {
|
|
_import_text_editor_theme(p_file);
|
|
} break;
|
|
}
|
|
file_dialog_option = -1;
|
|
}
|
|
|
|
Ref<Script> ScriptEditor::_get_current_script() {
|
|
if (ScriptEditorBase *current = _get_current_editor()) {
|
|
Ref<Script> scr = current->get_edited_resource();
|
|
return scr.is_valid() ? scr : nullptr;
|
|
} else {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
TypedArray<Script> ScriptEditor::_get_open_scripts() const {
|
|
TypedArray<Script> ret;
|
|
Vector<Ref<Script>> scripts = get_open_scripts();
|
|
int scripts_amount = scripts.size();
|
|
for (int idx_script = 0; idx_script < scripts_amount; idx_script++) {
|
|
ret.push_back(scripts[idx_script]);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
bool ScriptEditor::toggle_files_panel() {
|
|
list_split->set_visible(!list_split->is_visible());
|
|
EditorSettings::get_singleton()->set_project_metadata("files_panel", "show_files_panel", list_split->is_visible());
|
|
return list_split->is_visible();
|
|
}
|
|
|
|
bool ScriptEditor::is_files_panel_toggled() {
|
|
return list_split->is_visible();
|
|
}
|
|
|
|
List<String> ScriptEditor::_get_recognized_extensions() {
|
|
List<String> extensions;
|
|
for (const String type : { "Script", "JSON" }) {
|
|
ResourceLoader::get_recognized_extensions_for_type(type, &extensions);
|
|
}
|
|
return extensions;
|
|
}
|
|
|
|
void ScriptEditor::_menu_option(int p_option) {
|
|
ScriptEditorBase *current = _get_current_editor();
|
|
switch (p_option) {
|
|
case FILE_MENU_NEW_SCRIPT: {
|
|
script_create_dialog->config("Node", "new_script", false, false);
|
|
script_create_dialog->popup_centered();
|
|
} break;
|
|
case FILE_MENU_NEW_TEXTFILE: {
|
|
file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE);
|
|
file_dialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
|
|
file_dialog_option = FILE_MENU_NEW_TEXTFILE;
|
|
|
|
file_dialog->clear_filters();
|
|
for (const String &E : textfile_extensions) {
|
|
file_dialog->add_filter("*." + E, E.to_upper());
|
|
}
|
|
file_dialog->set_title(TTRC("New Text File..."));
|
|
file_dialog->popup_file_dialog();
|
|
} break;
|
|
case FILE_MENU_OPEN: {
|
|
file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE);
|
|
file_dialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
|
|
file_dialog_option = FILE_MENU_OPEN;
|
|
|
|
List<String> extensions;
|
|
ResourceLoader::get_recognized_extensions_for_type("Script", &extensions);
|
|
file_dialog->clear_filters();
|
|
for (const String &extension : extensions) {
|
|
file_dialog->add_filter("*." + extension, extension.to_upper());
|
|
}
|
|
|
|
for (const String &E : textfile_extensions) {
|
|
file_dialog->add_filter("*." + E, E.to_upper());
|
|
}
|
|
|
|
file_dialog->set_title(TTRC("Open File"));
|
|
file_dialog->popup_file_dialog();
|
|
return;
|
|
} break;
|
|
case FILE_MENU_REOPEN_CLOSED: {
|
|
if (previous_scripts.is_empty()) {
|
|
return;
|
|
}
|
|
|
|
String path = previous_scripts.back()->get();
|
|
previous_scripts.pop_back();
|
|
|
|
bool built_in = !path.is_resource_file();
|
|
|
|
if (_get_recognized_extensions().find(path.get_extension()) || built_in) {
|
|
if (built_in) {
|
|
String res_path = path.get_slice("::", 0);
|
|
EditorNode::get_singleton()->load_scene_or_resource(res_path, false, false);
|
|
}
|
|
|
|
Ref<Resource> scr = ResourceLoader::load(path);
|
|
if (scr.is_null()) {
|
|
EditorNode::get_singleton()->show_warning(TTR("Could not load file at:") + "\n\n" + path, TTR("Error!"));
|
|
file_dialog_option = -1;
|
|
return;
|
|
}
|
|
|
|
edit(scr);
|
|
file_dialog_option = -1;
|
|
} else {
|
|
Error error;
|
|
Ref<TextFile> text_file = _load_text_file(path, &error);
|
|
if (error != OK) {
|
|
EditorNode::get_singleton()->show_warning(TTR("Could not load file at:") + "\n\n" + path, TTR("Error!"));
|
|
}
|
|
|
|
if (text_file.is_valid()) {
|
|
edit(text_file);
|
|
file_dialog_option = -1;
|
|
}
|
|
}
|
|
} break;
|
|
case FILE_MENU_SAVE_ALL: {
|
|
if (_test_script_times_on_disk()) {
|
|
return;
|
|
}
|
|
|
|
save_all_scripts();
|
|
} break;
|
|
case SEARCH_IN_FILES: {
|
|
open_find_in_files_dialog("");
|
|
} break;
|
|
case REPLACE_IN_FILES: {
|
|
_on_replace_in_files_requested("");
|
|
} break;
|
|
case SEARCH_HELP: {
|
|
help_search_dialog->popup_dialog();
|
|
} break;
|
|
case SEARCH_WEBSITE: {
|
|
Control *tab = tab_container->get_current_tab_control();
|
|
|
|
EditorHelp *eh = Object::cast_to<EditorHelp>(tab);
|
|
bool native_class_doc = false;
|
|
if (eh) {
|
|
const HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(eh->get_class());
|
|
native_class_doc = E && !E->value.is_script_doc;
|
|
}
|
|
if (native_class_doc) {
|
|
String name = eh->get_class().to_lower();
|
|
String doc_url = vformat(GODOT_VERSION_DOCS_URL "/classes/class_%s.html", name);
|
|
OS::get_singleton()->shell_open(doc_url);
|
|
} else {
|
|
OS::get_singleton()->shell_open(GODOT_VERSION_DOCS_URL "/");
|
|
}
|
|
} break;
|
|
case FILE_MENU_HISTORY_NEXT: {
|
|
_history_forward();
|
|
} break;
|
|
case FILE_MENU_HISTORY_PREV: {
|
|
_history_back();
|
|
} break;
|
|
case FILE_MENU_SORT: {
|
|
_sort_list_on_update = true;
|
|
_update_script_names();
|
|
} break;
|
|
case FILE_MENU_TOGGLE_FILES_PANEL: {
|
|
toggle_files_panel();
|
|
if (current) {
|
|
current->update_toggle_files_button();
|
|
} else {
|
|
Control *tab = tab_container->get_current_tab_control();
|
|
if (EditorHelp *editor_help = Object::cast_to<EditorHelp>(tab)) {
|
|
editor_help->update_toggle_files_button();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (p_option >= EditorContextMenuPlugin::BASE_ID) {
|
|
Ref<Resource> resource;
|
|
if (current) {
|
|
resource = current->get_edited_resource();
|
|
}
|
|
EditorContextMenuPluginManager::get_singleton()->activate_custom_option(EditorContextMenuPlugin::CONTEXT_SLOT_SCRIPT_EDITOR, p_option, resource);
|
|
return;
|
|
}
|
|
|
|
if (current) {
|
|
switch (p_option) {
|
|
case FILE_MENU_SAVE: {
|
|
save_current_script();
|
|
} break;
|
|
case FILE_MENU_SAVE_AS: {
|
|
_auto_format_text(current);
|
|
|
|
Ref<Resource> resource = current->get_edited_resource();
|
|
Ref<TextFile> text_file = resource;
|
|
Ref<Script> scr = resource;
|
|
|
|
if (text_file.is_valid()) {
|
|
file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE);
|
|
file_dialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
|
|
file_dialog_option = FILE_MENU_SAVE_AS;
|
|
|
|
List<String> extensions;
|
|
ResourceLoader::get_recognized_extensions_for_type("Script", &extensions);
|
|
file_dialog->clear_filters();
|
|
file_dialog->set_current_dir(text_file->get_path().get_base_dir());
|
|
file_dialog->set_current_file(text_file->get_path().get_file());
|
|
file_dialog->set_title(TTRC("Save File As..."));
|
|
file_dialog->popup_file_dialog();
|
|
break;
|
|
}
|
|
|
|
if (scr.is_valid()) {
|
|
clear_docs_from_script(scr);
|
|
}
|
|
|
|
EditorNode::get_singleton()->push_item(resource.ptr());
|
|
EditorNode::get_singleton()->save_resource_as(resource);
|
|
|
|
if (scr.is_valid()) {
|
|
update_docs_from_script(scr);
|
|
}
|
|
} break;
|
|
|
|
case FILE_MENU_SOFT_RELOAD_TOOL: {
|
|
Ref<Script> scr = current->get_edited_resource();
|
|
if (scr.is_null()) {
|
|
EditorNode::get_singleton()->show_warning(TTR("Can't obtain the script for reloading."));
|
|
break;
|
|
}
|
|
if (!scr->is_tool()) {
|
|
EditorNode::get_singleton()->show_warning(TTR("Reload only takes effect on tool scripts."));
|
|
return;
|
|
}
|
|
scr->reload(true);
|
|
|
|
} break;
|
|
|
|
case FILE_MENU_RUN: {
|
|
Ref<Script> scr = current->get_edited_resource();
|
|
if (scr.is_null()) {
|
|
EditorToaster::get_singleton()->popup_str(TTR("Cannot run the edited file because it's not a script."), EditorToaster::SEVERITY_WARNING);
|
|
break;
|
|
}
|
|
|
|
current->apply_code();
|
|
|
|
EditorNode::get_singleton()->run_editor_script(scr);
|
|
} break;
|
|
|
|
case FILE_MENU_CLOSE: {
|
|
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(current);
|
|
if (seb && seb->is_unsaved()) {
|
|
_ask_close_current_unsaved_tab(seb);
|
|
} else {
|
|
_close_current_tab(false);
|
|
}
|
|
} break;
|
|
case FILE_MENU_COPY_PATH: {
|
|
_copy_script_path();
|
|
} break;
|
|
case FILE_MENU_COPY_UID: {
|
|
_copy_script_uid();
|
|
} break;
|
|
case FILE_MENU_SHOW_IN_FILE_SYSTEM: {
|
|
const Ref<Resource> scr = current->get_edited_resource();
|
|
String path = scr->get_path();
|
|
if (!path.is_empty()) {
|
|
if (scr->is_built_in()) {
|
|
path = path.get_slice("::", 0); // Show the scene instead.
|
|
}
|
|
|
|
FileSystemDock::get_singleton()->navigate_to_path(path);
|
|
}
|
|
} break;
|
|
case FILE_MENU_CLOSE_DOCS: {
|
|
_close_docs_tab();
|
|
} break;
|
|
case FILE_MENU_CLOSE_OTHER_TABS: {
|
|
_close_other_tabs();
|
|
} break;
|
|
case FILE_MENU_CLOSE_TABS_BELOW: {
|
|
_close_tabs_below();
|
|
} break;
|
|
case FILE_MENU_CLOSE_ALL: {
|
|
_close_all_tabs();
|
|
} break;
|
|
case FILE_MENU_MOVE_UP: {
|
|
if (tab_container->get_current_tab() > 0) {
|
|
tab_container->move_child(current, tab_container->get_current_tab() - 1);
|
|
tab_container->set_current_tab(tab_container->get_current_tab());
|
|
_update_script_names();
|
|
}
|
|
} break;
|
|
case FILE_MENU_MOVE_DOWN: {
|
|
if (tab_container->get_current_tab() < tab_container->get_tab_count() - 1) {
|
|
tab_container->move_child(current, tab_container->get_current_tab() + 1);
|
|
tab_container->set_current_tab(tab_container->get_current_tab());
|
|
_update_script_names();
|
|
}
|
|
} break;
|
|
}
|
|
} else {
|
|
if (EditorHelp *help = Object::cast_to<EditorHelp>(tab_container->get_current_tab_control())) {
|
|
switch (p_option) {
|
|
case HELP_SEARCH_FIND: {
|
|
help->popup_search();
|
|
} break;
|
|
case HELP_SEARCH_FIND_NEXT: {
|
|
help->search_again();
|
|
} break;
|
|
case HELP_SEARCH_FIND_PREVIOUS: {
|
|
help->search_again(true);
|
|
} break;
|
|
case FILE_MENU_CLOSE: {
|
|
_close_current_tab();
|
|
} break;
|
|
case FILE_MENU_CLOSE_DOCS: {
|
|
_close_docs_tab();
|
|
} break;
|
|
case FILE_MENU_CLOSE_OTHER_TABS: {
|
|
_close_other_tabs();
|
|
} break;
|
|
case FILE_MENU_CLOSE_TABS_BELOW: {
|
|
_close_tabs_below();
|
|
} break;
|
|
case FILE_MENU_CLOSE_ALL: {
|
|
_close_all_tabs();
|
|
} break;
|
|
case FILE_MENU_MOVE_UP: {
|
|
if (tab_container->get_current_tab() > 0) {
|
|
tab_container->move_child(help, tab_container->get_current_tab() - 1);
|
|
tab_container->set_current_tab(tab_container->get_current_tab());
|
|
_update_script_names();
|
|
}
|
|
} break;
|
|
case FILE_MENU_MOVE_DOWN: {
|
|
if (tab_container->get_current_tab() < tab_container->get_tab_count() - 1) {
|
|
tab_container->move_child(help, tab_container->get_current_tab() + 1);
|
|
tab_container->set_current_tab(tab_container->get_current_tab());
|
|
_update_script_names();
|
|
}
|
|
} break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_theme_option(int p_option) {
|
|
switch (p_option) {
|
|
case THEME_IMPORT: {
|
|
file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE);
|
|
file_dialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
|
|
file_dialog_option = THEME_IMPORT;
|
|
file_dialog->clear_filters();
|
|
file_dialog->add_filter("*.tet");
|
|
file_dialog->set_title(TTRC("Import Theme"));
|
|
file_dialog->popup_file_dialog();
|
|
} break;
|
|
case THEME_RELOAD: {
|
|
EditorSettings::get_singleton()->mark_setting_changed("text_editor/theme/color_theme");
|
|
EditorSettings::get_singleton()->notify_changes();
|
|
} break;
|
|
case THEME_SAVE_AS: {
|
|
ScriptEditor::_show_save_theme_as_dialog();
|
|
} break;
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_show_save_theme_as_dialog() {
|
|
file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE);
|
|
file_dialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
|
|
file_dialog_option = THEME_SAVE_AS;
|
|
file_dialog->clear_filters();
|
|
file_dialog->add_filter("*.tet");
|
|
file_dialog->set_current_path(EditorPaths::get_singleton()->get_text_editor_themes_dir().path_join(EDITOR_GET("text_editor/theme/color_theme")) + " New");
|
|
file_dialog->set_title(TTRC("Save Theme As..."));
|
|
file_dialog->popup_file_dialog();
|
|
}
|
|
|
|
bool ScriptEditor::_has_docs_tab() const {
|
|
const int child_count = tab_container->get_tab_count();
|
|
for (int i = 0; i < child_count; i++) {
|
|
if (Object::cast_to<EditorHelp>(tab_container->get_tab_control(i))) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ScriptEditor::_has_script_tab() const {
|
|
const int child_count = tab_container->get_tab_count();
|
|
for (int i = 0; i < child_count; i++) {
|
|
if (Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i))) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void ScriptEditor::_prepare_file_menu() {
|
|
PopupMenu *menu = file_menu->get_popup();
|
|
ScriptEditorBase *editor = _get_current_editor();
|
|
const Ref<Resource> res = editor ? editor->get_edited_resource() : Ref<Resource>();
|
|
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_REOPEN_CLOSED), previous_scripts.is_empty());
|
|
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_SAVE), res.is_null());
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_SAVE_AS), res.is_null());
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_SAVE_ALL), !_has_script_tab());
|
|
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_SOFT_RELOAD_TOOL), res.is_null());
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_COPY_PATH), res.is_null() || res->get_path().is_empty());
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_COPY_UID), res.is_null() || ResourceLoader::get_resource_uid(res->get_path()) == ResourceUID::INVALID_ID);
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_SHOW_IN_FILE_SYSTEM), res.is_null());
|
|
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_HISTORY_PREV), history_pos <= 0);
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_HISTORY_NEXT), history_pos >= history.size() - 1);
|
|
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE), tab_container->get_tab_count() < 1);
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE_ALL), tab_container->get_tab_count() < 1);
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE_OTHER_TABS), tab_container->get_tab_count() <= 1);
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE_TABS_BELOW), tab_container->get_current_tab() >= tab_container->get_tab_count() - 1);
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE_DOCS), !_has_docs_tab());
|
|
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_RUN), res.is_null());
|
|
}
|
|
|
|
void ScriptEditor::_file_menu_closed() {
|
|
PopupMenu *menu = file_menu->get_popup();
|
|
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_REOPEN_CLOSED), false);
|
|
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_SAVE), false);
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_SAVE_AS), false);
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_SAVE_ALL), false);
|
|
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_SOFT_RELOAD_TOOL), false);
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_COPY_PATH), false);
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_SHOW_IN_FILE_SYSTEM), false);
|
|
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_HISTORY_PREV), false);
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_HISTORY_NEXT), false);
|
|
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE), false);
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE_ALL), false);
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE_OTHER_TABS), false);
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE_DOCS), false);
|
|
|
|
menu->set_item_disabled(menu->get_item_index(FILE_MENU_RUN), false);
|
|
}
|
|
|
|
void ScriptEditor::_tab_changed(int p_which) {
|
|
ensure_select_current();
|
|
}
|
|
|
|
void ScriptEditor::_notification(int p_what) {
|
|
switch (p_what) {
|
|
case NOTIFICATION_ENTER_TREE: {
|
|
_apply_editor_settings();
|
|
[[fallthrough]];
|
|
}
|
|
|
|
case NOTIFICATION_TRANSLATION_CHANGED: {
|
|
_update_online_doc();
|
|
if (!make_floating->is_disabled()) {
|
|
// Override default ScreenSelect tooltip if multi-window support is available.
|
|
make_floating->set_tooltip_text(TTR("Make the script editor floating.") + "\n" + TTR("Right-click to open the screen selector."));
|
|
}
|
|
[[fallthrough]];
|
|
}
|
|
case NOTIFICATION_LAYOUT_DIRECTION_CHANGED:
|
|
case NOTIFICATION_THEME_CHANGED: {
|
|
tab_container->add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SNAME("ScriptEditor"), EditorStringName(EditorStyles)));
|
|
|
|
_calculate_script_name_button_size();
|
|
|
|
help_search->set_button_icon(get_editor_theme_icon(SNAME("HelpSearch")));
|
|
site_search->set_button_icon(get_editor_theme_icon(SNAME("ExternalLink")));
|
|
|
|
if (is_layout_rtl()) {
|
|
script_forward->set_button_icon(get_editor_theme_icon(SNAME("Back")));
|
|
script_back->set_button_icon(get_editor_theme_icon(SNAME("Forward")));
|
|
} else {
|
|
script_forward->set_button_icon(get_editor_theme_icon(SNAME("Forward")));
|
|
script_back->set_button_icon(get_editor_theme_icon(SNAME("Back")));
|
|
}
|
|
|
|
members_overview_alphabeta_sort_button->set_button_icon(get_editor_theme_icon(SNAME("Sort")));
|
|
|
|
bool use_monospace_font = EDITOR_GET("interface/theme/use_monospace_font_for_editor_symbols");
|
|
Ref<Font> monospace_font = get_theme_font(SNAME("source"), EditorStringName(EditorFonts));
|
|
if (use_monospace_font) {
|
|
members_overview->add_theme_font_override(SceneStringName(font), monospace_font);
|
|
} else {
|
|
members_overview->remove_theme_font_override(SceneStringName(font));
|
|
}
|
|
|
|
filter_scripts->set_right_icon(get_editor_theme_icon(SNAME("Search")));
|
|
filter_methods->set_right_icon(get_editor_theme_icon(SNAME("Search")));
|
|
|
|
recent_scripts->reset_size();
|
|
script_list->set_fixed_icon_size(Vector2i(1, 1) * get_theme_constant("class_icon_size", EditorStringName(Editor)));
|
|
|
|
if (is_inside_tree()) {
|
|
_update_script_names();
|
|
}
|
|
} break;
|
|
|
|
case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
|
|
if (EditorThemeManager::is_generated_theme_outdated() ||
|
|
EditorSettings::get_singleton()->check_changed_settings_in_group("interface/editor") ||
|
|
EditorSettings::get_singleton()->check_changed_settings_in_group("text_editor") ||
|
|
EditorSettings::get_singleton()->check_changed_settings_in_group("docks/filesystem")) {
|
|
_apply_editor_settings();
|
|
}
|
|
} break;
|
|
|
|
case NOTIFICATION_READY: {
|
|
// Can't set own styles in NOTIFICATION_THEME_CHANGED, so for now this will do.
|
|
add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SNAME("ScriptEditorPanel"), EditorStringName(EditorStyles)));
|
|
|
|
get_tree()->connect("tree_changed", callable_mp(this, &ScriptEditor::_tree_changed));
|
|
InspectorDock::get_singleton()->connect("request_help", callable_mp(this, &ScriptEditor::_help_class_open));
|
|
EditorNode::get_singleton()->connect("request_help_search", callable_mp(this, &ScriptEditor::_help_search));
|
|
EditorNode::get_singleton()->connect("scene_closed", callable_mp(this, &ScriptEditor::_close_builtin_scripts_from_scene));
|
|
EditorNode::get_singleton()->connect("script_add_function_request", callable_mp(this, &ScriptEditor::_add_callback));
|
|
EditorNode::get_singleton()->connect("resource_saved", callable_mp(this, &ScriptEditor::_res_saved_callback));
|
|
EditorNode::get_singleton()->connect("scene_saved", callable_mp(this, &ScriptEditor::_scene_saved_callback));
|
|
FileSystemDock::get_singleton()->connect("files_moved", callable_mp(this, &ScriptEditor::_files_moved));
|
|
FileSystemDock::get_singleton()->connect("file_removed", callable_mp(this, &ScriptEditor::_file_removed));
|
|
script_list->connect(SceneStringName(item_selected), callable_mp(this, &ScriptEditor::_script_selected));
|
|
|
|
members_overview->connect(SceneStringName(item_selected), callable_mp(this, &ScriptEditor::_members_overview_selected));
|
|
help_overview->connect(SceneStringName(item_selected), callable_mp(this, &ScriptEditor::_help_overview_selected));
|
|
script_split->connect("dragged", callable_mp(this, &ScriptEditor::_split_dragged));
|
|
list_split->connect("dragged", callable_mp(this, &ScriptEditor::_split_dragged));
|
|
|
|
EditorFileSystem::get_singleton()->connect("filesystem_changed", callable_mp(this, &ScriptEditor::_filesystem_changed));
|
|
#ifdef ANDROID_ENABLED
|
|
set_process(true);
|
|
#endif
|
|
} break;
|
|
|
|
#ifdef ANDROID_ENABLED
|
|
case NOTIFICATION_VISIBILITY_CHANGED: {
|
|
set_process(is_visible_in_tree());
|
|
} break;
|
|
|
|
case NOTIFICATION_PROCESS: {
|
|
const int kb_height = DisplayServer::get_singleton()->virtual_keyboard_get_height();
|
|
if (kb_height == last_kb_height) {
|
|
break;
|
|
}
|
|
|
|
last_kb_height = kb_height;
|
|
float spacer_height = 0.0f;
|
|
const float status_bar_height = 28 * EDSCALE; // Magic number
|
|
const bool kb_visible = kb_height > 0;
|
|
|
|
if (kb_visible) {
|
|
if (TextEditorBase *editor = Object::cast_to<TextEditorBase>(_get_current_editor())) {
|
|
if (CodeTextEditor *code_editor = editor->get_code_editor()) {
|
|
if (CodeEdit *text_editor = code_editor->get_text_editor()) {
|
|
if (!text_editor->has_focus()) {
|
|
break;
|
|
}
|
|
text_editor->adjust_viewport_to_caret();
|
|
}
|
|
}
|
|
}
|
|
|
|
const float control_bottom = get_global_position().y + get_size().y;
|
|
const float extra_bottom = get_viewport_rect().size.y - control_bottom;
|
|
spacer_height = float(kb_height) - extra_bottom - status_bar_height;
|
|
|
|
if (spacer_height < 0.0f) {
|
|
spacer_height = 0.0f;
|
|
}
|
|
}
|
|
|
|
virtual_keyboard_spacer->set_custom_minimum_size(Size2(0, spacer_height));
|
|
EditorSceneTabs::get_singleton()->set_visible(!kb_height);
|
|
menu_hb->set_visible(!kb_visible);
|
|
} break;
|
|
#endif
|
|
case NOTIFICATION_APPLICATION_FOCUS_IN: {
|
|
if (is_inside_tree()) {
|
|
_test_script_times_on_disk();
|
|
_update_modified_scripts_for_external_editor();
|
|
}
|
|
} break;
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_close_builtin_scripts_from_scene(const String &p_scene) {
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i))) {
|
|
Ref<Script> scr = seb->get_edited_resource();
|
|
if (scr.is_null()) {
|
|
continue;
|
|
}
|
|
|
|
if (scr->is_built_in() && scr->get_path().begins_with(p_scene)) { // Is an internal script and belongs to scene being closed.
|
|
_close_tab(i, false);
|
|
i--;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::edited_scene_changed() {
|
|
_update_modified_scripts_for_external_editor();
|
|
}
|
|
|
|
void ScriptEditor::notify_script_close(const Ref<Script> &p_script) {
|
|
emit_signal(SNAME("script_close"), p_script);
|
|
}
|
|
|
|
void ScriptEditor::notify_script_changed(const Ref<Script> &p_script) {
|
|
emit_signal(SNAME("editor_script_changed"), p_script);
|
|
}
|
|
|
|
Vector<String> ScriptEditor::_get_breakpoints() {
|
|
List<String> bpoints_list;
|
|
get_breakpoints(&bpoints_list);
|
|
|
|
Vector<String> ret;
|
|
for (const String &E : bpoints_list) {
|
|
ret.push_back(E);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
void ScriptEditor::get_breakpoints(List<String> *p_breakpoints) {
|
|
HashSet<String> loaded_scripts;
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
CodeEditorBase *ceb = Object::cast_to<CodeEditorBase>(tab_container->get_tab_control(i));
|
|
if (!ceb) {
|
|
continue;
|
|
}
|
|
|
|
Ref<Script> scr = ceb->get_edited_resource();
|
|
if (scr.is_null()) {
|
|
continue;
|
|
}
|
|
|
|
String base = scr->get_path();
|
|
loaded_scripts.insert(base);
|
|
if (base.is_empty() || base.begins_with("local://")) {
|
|
continue;
|
|
}
|
|
|
|
PackedInt32Array bpoints = ceb->get_breakpoints();
|
|
for (int32_t bpoint : bpoints) {
|
|
p_breakpoints->push_back(base + ":" + itos((int)bpoint + 1));
|
|
}
|
|
}
|
|
|
|
// Load breakpoints that are in closed scripts.
|
|
Vector<String> cached_editors = script_editor_cache->get_sections();
|
|
for (const String &E : cached_editors) {
|
|
if (loaded_scripts.has(E)) {
|
|
continue;
|
|
}
|
|
|
|
Array breakpoints = _get_cached_breakpoints_for_script(E);
|
|
for (int breakpoint : breakpoints) {
|
|
p_breakpoints->push_back(E + ":" + itos((int)breakpoint + 1));
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_members_overview_selected(int p_idx) {
|
|
int line = members_overview->get_item_metadata(p_idx);
|
|
ScriptEditorBase *current = _get_current_editor();
|
|
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(current)) {
|
|
teb->goto_line_centered(line);
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_help_overview_selected(int p_idx) {
|
|
Node *current = tab_container->get_tab_control(tab_container->get_current_tab());
|
|
if (EditorHelp *eh = Object::cast_to<EditorHelp>(current)) {
|
|
eh->scroll_to_section(help_overview->get_item_metadata(p_idx));
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_script_selected(int p_idx) {
|
|
grab_focus_block = !Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT); //amazing hack, simply amazing
|
|
|
|
_go_to_tab(script_list->get_item_metadata(p_idx));
|
|
script_name_button->set_text(script_list->get_item_text(p_idx));
|
|
_calculate_script_name_button_size();
|
|
grab_focus_block = false;
|
|
}
|
|
|
|
void ScriptEditor::ensure_select_current() {
|
|
if (tab_container->get_tab_count() && tab_container->get_current_tab() >= 0) {
|
|
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(_get_current_editor())) {
|
|
teb->enable_editor();
|
|
if (teb && !grab_focus_block && is_visible_in_tree()) {
|
|
teb->ensure_focus();
|
|
}
|
|
}
|
|
}
|
|
_update_find_replace_bar();
|
|
|
|
_update_selected_editor_menu();
|
|
}
|
|
|
|
bool ScriptEditor::is_editor_floating() {
|
|
return is_floating;
|
|
}
|
|
|
|
void ScriptEditor::_find_scripts(Node *p_base, Node *p_current, HashSet<Ref<Script>> &used) {
|
|
if (p_current != p_base && p_current->get_owner() != p_base) {
|
|
return;
|
|
}
|
|
|
|
if (p_current->get_script_instance()) {
|
|
Ref<Script> scr = p_current->get_script();
|
|
if (scr.is_valid()) {
|
|
used.insert(scr);
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < p_current->get_child_count(); i++) {
|
|
_find_scripts(p_base, p_current->get_child(i), used);
|
|
}
|
|
}
|
|
|
|
struct _ScriptEditorItemData {
|
|
String name;
|
|
String sort_key;
|
|
Ref<Texture2D> icon;
|
|
bool tool = false;
|
|
int index = 0;
|
|
String tooltip;
|
|
bool used = false;
|
|
int category = 0;
|
|
Node *ref = nullptr;
|
|
|
|
bool operator<(const _ScriptEditorItemData &id) const {
|
|
if (category == id.category) {
|
|
if (sort_key == id.sort_key) {
|
|
return index < id.index;
|
|
} else {
|
|
return sort_key.filenocasecmp_to(id.sort_key) < 0;
|
|
}
|
|
} else {
|
|
return category < id.category;
|
|
}
|
|
}
|
|
};
|
|
|
|
void ScriptEditor::_update_members_overview_visibility() {
|
|
ScriptEditorBase *seb = _get_current_editor();
|
|
if (!seb) {
|
|
members_overview_alphabeta_sort_button->set_visible(false);
|
|
members_overview->set_visible(false);
|
|
|
|
Node *current = tab_container->get_tab_control(tab_container->get_current_tab());
|
|
EditorHelp *editor_help = Object::cast_to<EditorHelp>(current);
|
|
overview_vbox->set_visible(help_overview_enabled && editor_help);
|
|
return;
|
|
}
|
|
|
|
if (members_overview_enabled && seb->show_members_overview()) {
|
|
members_overview_alphabeta_sort_button->set_visible(true);
|
|
filter_methods->set_visible(true);
|
|
members_overview->set_visible(true);
|
|
overview_vbox->set_visible(true);
|
|
} else {
|
|
members_overview_alphabeta_sort_button->set_visible(false);
|
|
filter_methods->set_visible(false);
|
|
members_overview->set_visible(false);
|
|
overview_vbox->set_visible(false);
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_toggle_members_overview_alpha_sort(bool p_alphabetic_sort) {
|
|
EditorSettings::get_singleton()->set("text_editor/script_list/sort_members_outline_alphabetically", p_alphabetic_sort);
|
|
_update_members_overview();
|
|
}
|
|
|
|
void ScriptEditor::_update_members_overview() {
|
|
members_overview->clear();
|
|
|
|
CodeEditorBase *ceb = Object::cast_to<CodeEditorBase>(_get_current_editor());
|
|
if (!ceb) {
|
|
return;
|
|
}
|
|
|
|
Vector<String> functions = ceb->get_functions();
|
|
if (EDITOR_GET("text_editor/script_list/sort_members_outline_alphabetically")) {
|
|
functions.sort();
|
|
}
|
|
|
|
String filter = filter_methods->get_text();
|
|
if (filter.is_empty()) {
|
|
for (int i = 0; i < functions.size(); i++) {
|
|
String name = functions[i].get_slicec(':', 0);
|
|
members_overview->add_item(name);
|
|
members_overview->set_item_metadata(-1, functions[i].get_slicec(':', 1).to_int() - 1);
|
|
}
|
|
} else {
|
|
PackedStringArray search_names;
|
|
for (int i = 0; i < functions.size(); i++) {
|
|
search_names.append(functions[i].get_slicec(':', 0));
|
|
}
|
|
|
|
Vector<FuzzySearchResult> results;
|
|
FuzzySearch fuzzy;
|
|
fuzzy.set_query(filter, false);
|
|
fuzzy.search_all(search_names, results);
|
|
|
|
for (const FuzzySearchResult &res : results) {
|
|
String name = functions[res.original_index].get_slicec(':', 0);
|
|
int line = functions[res.original_index].get_slicec(':', 1).to_int() - 1;
|
|
members_overview->add_item(name);
|
|
members_overview->set_item_metadata(-1, line);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_update_help_overview_visibility() {
|
|
int selected = tab_container->get_current_tab();
|
|
if (selected < 0 || selected >= tab_container->get_tab_count()) {
|
|
help_overview->set_visible(false);
|
|
return;
|
|
}
|
|
|
|
Node *current = tab_container->get_tab_control(tab_container->get_current_tab());
|
|
if (!Object::cast_to<EditorHelp>(current)) {
|
|
help_overview->set_visible(false);
|
|
return;
|
|
}
|
|
|
|
if (help_overview_enabled) {
|
|
members_overview_alphabeta_sort_button->set_visible(false);
|
|
filter_methods->set_visible(false);
|
|
help_overview->set_visible(true);
|
|
overview_vbox->set_visible(true);
|
|
} else {
|
|
help_overview->set_visible(false);
|
|
overview_vbox->set_visible(false);
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_update_help_overview() {
|
|
help_overview->clear();
|
|
|
|
int selected = tab_container->get_current_tab();
|
|
if (selected < 0 || selected >= tab_container->get_tab_count()) {
|
|
return;
|
|
}
|
|
|
|
Node *current = tab_container->get_tab_control(tab_container->get_current_tab());
|
|
if (EditorHelp *eh = Object::cast_to<EditorHelp>(current)) {
|
|
Vector<Pair<String, int>> sections = eh->get_sections();
|
|
for (int i = 0; i < sections.size(); i++) {
|
|
help_overview->add_item(sections[i].first);
|
|
help_overview->set_item_metadata(i, sections[i].second);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_update_online_doc() {
|
|
Node *current = tab_container->get_tab_control(tab_container->get_current_tab());
|
|
|
|
EditorHelp *eh = Object::cast_to<EditorHelp>(current);
|
|
bool native_class_doc = false;
|
|
if (eh) {
|
|
const HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(eh->get_class());
|
|
native_class_doc = E && !E->value.is_script_doc;
|
|
}
|
|
if (native_class_doc) {
|
|
String name = eh->get_class();
|
|
String tooltip = vformat(TTR("Open '%s' in Godot online documentation."), name);
|
|
site_search->set_text(TTRC("Open in Online Docs"));
|
|
site_search->set_tooltip_text(tooltip);
|
|
} else {
|
|
site_search->set_text(TTRC("Online Docs"));
|
|
site_search->set_tooltip_text(TTRC("Open Godot online documentation."));
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_update_script_colors() {
|
|
bool script_temperature_enabled = EDITOR_GET("text_editor/script_list/script_temperature_enabled");
|
|
|
|
int hist_size = EDITOR_GET("text_editor/script_list/script_temperature_history_size");
|
|
Color hot_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));
|
|
hot_color.set_s(hot_color.get_s() * 0.9);
|
|
Color cold_color = get_theme_color(SceneStringName(font_color), EditorStringName(Editor));
|
|
|
|
for (int i = 0; i < script_list->get_item_count(); i++) {
|
|
int c = script_list->get_item_metadata(i);
|
|
Node *n = tab_container->get_tab_control(c);
|
|
if (!n) {
|
|
continue;
|
|
}
|
|
|
|
if (script_temperature_enabled) {
|
|
int pass = n->get_meta("__editor_pass", -1);
|
|
if (pass < 0) {
|
|
continue;
|
|
}
|
|
|
|
int h = edit_pass - pass;
|
|
if (h > hist_size) {
|
|
continue;
|
|
}
|
|
int non_zero_hist_size = (hist_size == 0) ? 1 : hist_size;
|
|
float v = Math::ease((edit_pass - pass) / float(non_zero_hist_size), 0.4);
|
|
|
|
script_list->set_item_custom_fg_color(i, hot_color.lerp(cold_color, v));
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_update_script_names() {
|
|
if (restoring_layout) {
|
|
return;
|
|
}
|
|
|
|
HashSet<Ref<Script>> used;
|
|
Node *edited = EditorNode::get_singleton()->get_edited_scene();
|
|
if (edited && EDITOR_GET("text_editor/script_list/highlight_scene_scripts")) {
|
|
_find_scripts(edited, edited, used);
|
|
}
|
|
|
|
script_list->clear();
|
|
bool split_script_help = EDITOR_GET("text_editor/script_list/group_help_pages");
|
|
ScriptSortBy sort_by = (ScriptSortBy)(int)EDITOR_GET("text_editor/script_list/sort_scripts_by");
|
|
ScriptListName display_as = (ScriptListName)(int)EDITOR_GET("text_editor/script_list/list_script_names_as");
|
|
|
|
Vector<_ScriptEditorItemData> sedata;
|
|
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i))) {
|
|
Ref<Texture2D> icon = seb->get_theme_icon();
|
|
String path = seb->get_edited_resource()->get_path();
|
|
bool saved = !path.is_empty();
|
|
String name = seb->get_name();
|
|
Ref<Script> scr = seb->get_edited_resource();
|
|
|
|
_ScriptEditorItemData sd;
|
|
sd.icon = icon;
|
|
sd.name = name;
|
|
sd.tooltip = saved ? path : TTR("Unsaved file.");
|
|
sd.index = i;
|
|
sd.used = used.has(seb->get_edited_resource());
|
|
sd.category = 0;
|
|
sd.ref = seb;
|
|
if (scr.is_valid()) {
|
|
sd.tool = scr->is_tool();
|
|
}
|
|
|
|
switch (sort_by) {
|
|
case SORT_BY_NAME: {
|
|
sd.sort_key = name.to_lower();
|
|
} break;
|
|
case SORT_BY_PATH: {
|
|
sd.sort_key = path;
|
|
} break;
|
|
case SORT_BY_NONE: {
|
|
sd.sort_key = "";
|
|
} break;
|
|
}
|
|
|
|
switch (display_as) {
|
|
case DISPLAY_NAME: {
|
|
sd.name = name;
|
|
} break;
|
|
case DISPLAY_DIR_AND_NAME: {
|
|
if (!path.get_base_dir().get_file().is_empty()) {
|
|
sd.name = path.get_base_dir().get_file().path_join(name);
|
|
} else {
|
|
sd.name = name;
|
|
}
|
|
} break;
|
|
case DISPLAY_FULL_PATH: {
|
|
sd.name = path;
|
|
} break;
|
|
}
|
|
if (!saved) {
|
|
sd.name = seb->get_name();
|
|
}
|
|
|
|
sedata.push_back(sd);
|
|
}
|
|
|
|
EditorHelp *eh = Object::cast_to<EditorHelp>(tab_container->get_tab_control(i));
|
|
if (eh && !eh->get_class().is_empty()) {
|
|
String name = eh->get_class().unquote();
|
|
Ref<Texture2D> icon = get_editor_theme_icon(SNAME("Help"));
|
|
String tooltip = vformat(TTR("%s Class Reference"), name);
|
|
|
|
_ScriptEditorItemData sd;
|
|
sd.icon = icon;
|
|
sd.name = name;
|
|
sd.sort_key = name.to_lower();
|
|
sd.tooltip = tooltip;
|
|
sd.index = i;
|
|
sd.used = false;
|
|
sd.category = split_script_help ? 1 : 0;
|
|
sd.ref = eh;
|
|
|
|
sedata.push_back(sd);
|
|
}
|
|
}
|
|
|
|
Vector<String> disambiguated_script_names;
|
|
Vector<String> full_script_paths;
|
|
for (int j = 0; j < sedata.size(); j++) {
|
|
String name = sedata[j].name.replace("(*)", "");
|
|
ScriptListName script_display = (ScriptListName)(int)EDITOR_GET("text_editor/script_list/list_script_names_as");
|
|
switch (script_display) {
|
|
case DISPLAY_NAME: {
|
|
name = name.get_file();
|
|
} break;
|
|
case DISPLAY_DIR_AND_NAME: {
|
|
name = name.get_base_dir().get_file().path_join(name.get_file());
|
|
} break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
disambiguated_script_names.append(name);
|
|
full_script_paths.append(sedata[j].tooltip);
|
|
}
|
|
|
|
EditorNode::disambiguate_filenames(full_script_paths, disambiguated_script_names);
|
|
|
|
for (int j = 0; j < sedata.size(); j++) {
|
|
if (sedata[j].name.ends_with("(*)")) {
|
|
sedata.write[j].name = disambiguated_script_names[j] + "(*)";
|
|
} else {
|
|
sedata.write[j].name = disambiguated_script_names[j];
|
|
}
|
|
}
|
|
|
|
if (_sort_list_on_update && !sedata.is_empty()) {
|
|
sedata.sort();
|
|
|
|
// change actual order of tab_container so that the order can be rearranged by user
|
|
int cur_tab = tab_container->get_current_tab();
|
|
int prev_tab = tab_container->get_previous_tab();
|
|
int new_cur_tab = -1;
|
|
int new_prev_tab = -1;
|
|
for (int i = 0; i < sedata.size(); i++) {
|
|
tab_container->move_child(sedata[i].ref, i);
|
|
if (new_prev_tab == -1 && sedata[i].index == prev_tab) {
|
|
new_prev_tab = i;
|
|
}
|
|
if (new_cur_tab == -1 && sedata[i].index == cur_tab) {
|
|
new_cur_tab = i;
|
|
}
|
|
// Update index of sd entries for sorted order
|
|
_ScriptEditorItemData sd = sedata[i];
|
|
sd.index = i;
|
|
sedata.set(i, sd);
|
|
}
|
|
|
|
lock_history = true;
|
|
_go_to_tab(new_prev_tab);
|
|
_go_to_tab(new_cur_tab);
|
|
lock_history = false;
|
|
_sort_list_on_update = false;
|
|
}
|
|
|
|
Vector<_ScriptEditorItemData> sedata_filtered;
|
|
|
|
String filter = filter_scripts->get_text();
|
|
|
|
if (filter.is_empty()) {
|
|
sedata_filtered = sedata;
|
|
} else {
|
|
PackedStringArray search_names;
|
|
for (int i = 0; i < sedata.size(); i++) {
|
|
search_names.append(sedata[i].name);
|
|
}
|
|
|
|
Vector<FuzzySearchResult> results;
|
|
FuzzySearch fuzzy;
|
|
fuzzy.set_query(filter, false);
|
|
fuzzy.search_all(search_names, results);
|
|
|
|
for (const FuzzySearchResult &res : results) {
|
|
sedata_filtered.push_back(sedata[res.original_index]);
|
|
}
|
|
}
|
|
|
|
Color tool_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));
|
|
tool_color.set_s(tool_color.get_s() * 1.5);
|
|
for (int i = 0; i < sedata_filtered.size(); i++) {
|
|
script_list->add_item(sedata_filtered[i].name, sedata_filtered[i].icon);
|
|
if (sedata_filtered[i].tool) {
|
|
script_list->set_item_icon_modulate(-1, tool_color);
|
|
}
|
|
|
|
int index = script_list->get_item_count() - 1;
|
|
script_list->set_item_tooltip(index, sedata_filtered[i].tooltip);
|
|
script_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 */
|
|
if (sedata_filtered[i].used) {
|
|
script_list->set_item_custom_bg_color(index, Color(.5, .5, .5, .125));
|
|
}
|
|
if (tab_container->get_current_tab() == sedata_filtered[i].index) {
|
|
script_list->select(index);
|
|
|
|
if (ScriptEditorBase *seb = _get_current_editor()) {
|
|
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb)) {
|
|
teb->enable_editor();
|
|
}
|
|
_update_selected_editor_menu();
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const _ScriptEditorItemData &sedata_i : sedata) {
|
|
if (tab_container->get_current_tab() == sedata_i.index) {
|
|
script_name_button->set_text(sedata_i.name);
|
|
_calculate_script_name_button_size();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!waiting_update_names) {
|
|
_update_members_overview();
|
|
_update_help_overview();
|
|
} else {
|
|
waiting_update_names = false;
|
|
}
|
|
_update_members_overview_visibility();
|
|
_update_help_overview_visibility();
|
|
_update_script_colors();
|
|
}
|
|
|
|
Ref<TextFile> ScriptEditor::_load_text_file(const String &p_path, Error *r_error) const {
|
|
if (r_error) {
|
|
*r_error = ERR_FILE_CANT_OPEN;
|
|
}
|
|
|
|
String local_path = ProjectSettings::get_singleton()->localize_path(p_path);
|
|
String path = ResourceLoader::path_remap(local_path);
|
|
|
|
TextFile *text_file = memnew(TextFile);
|
|
Ref<TextFile> text_res(text_file);
|
|
Error err = text_file->load_text(path);
|
|
|
|
ERR_FAIL_COND_V_MSG(err != OK, Ref<Resource>(), "Cannot load text file '" + path + "'.");
|
|
|
|
text_file->set_file_path(local_path);
|
|
text_file->set_path(local_path, true);
|
|
|
|
if (ResourceLoader::get_timestamp_on_load()) {
|
|
text_file->set_last_modified_time(FileAccess::get_modified_time(path));
|
|
}
|
|
|
|
if (r_error) {
|
|
*r_error = OK;
|
|
}
|
|
|
|
return text_res;
|
|
}
|
|
|
|
Error ScriptEditor::_save_text_file(Ref<TextFile> p_text_file, const String &p_path) {
|
|
Ref<TextFile> sqscr = p_text_file;
|
|
ERR_FAIL_COND_V(sqscr.is_null(), ERR_INVALID_PARAMETER);
|
|
|
|
String source = sqscr->get_text();
|
|
|
|
Error err;
|
|
{
|
|
Ref<FileAccess> file = FileAccess::open(p_path, FileAccess::WRITE, &err);
|
|
|
|
ERR_FAIL_COND_V_MSG(err, err, "Cannot save text file '" + p_path + "'.");
|
|
|
|
file->store_string(source);
|
|
if (file->get_error() != OK && file->get_error() != ERR_FILE_EOF) {
|
|
return ERR_CANT_CREATE;
|
|
}
|
|
}
|
|
|
|
if (ResourceSaver::get_timestamp_on_save()) {
|
|
p_text_file->set_last_modified_time(FileAccess::get_modified_time(p_path));
|
|
}
|
|
|
|
EditorFileSystem::get_singleton()->update_file(p_path);
|
|
|
|
_res_saved_callback(sqscr);
|
|
return OK;
|
|
}
|
|
|
|
bool ScriptEditor::edit(const Ref<Resource> &p_resource, int p_line, int p_col, bool p_grab_focus) {
|
|
if (p_resource.is_null()) {
|
|
return false;
|
|
}
|
|
|
|
Ref<Script> scr = p_resource;
|
|
|
|
// Don't open dominant script if using an external editor.
|
|
bool use_external_editor =
|
|
external_editor_active ||
|
|
(scr.is_valid() && scr->get_language()->overrides_external_editor());
|
|
use_external_editor = use_external_editor && !(scr.is_valid() && scr->is_built_in()); // Ignore external editor for built-in scripts.
|
|
const bool open_dominant = EDITOR_GET("text_editor/behavior/files/open_dominant_script_on_scene_change");
|
|
|
|
const bool should_open = (open_dominant && !use_external_editor) || !EditorNode::get_singleton()->is_changing_scene();
|
|
|
|
if (scr.is_valid() && scr->get_language()->overrides_external_editor()) {
|
|
if (should_open) {
|
|
Error err = scr->get_language()->open_in_external_editor(scr, p_line >= 0 ? p_line : 0, p_col);
|
|
if (err != OK) {
|
|
ERR_PRINT("Couldn't open script in the overridden external text editor");
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (use_external_editor &&
|
|
(EditorDebuggerNode::get_singleton()->get_dump_stack_script() != p_resource || EditorDebuggerNode::get_singleton()->get_debug_with_external_editor()) &&
|
|
p_resource->get_path().is_resource_file()) {
|
|
if (ScriptEditorPlugin::open_in_external_editor(ProjectSettings::get_singleton()->globalize_path(p_resource->get_path()), p_line, p_col)) {
|
|
return false;
|
|
} else {
|
|
ERR_PRINT("Couldn't open external text editor, falling back to the internal editor. Review your `text_editor/external/` editor settings.");
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
|
|
if (!seb) {
|
|
continue;
|
|
}
|
|
|
|
if ((scr.is_valid() && seb->get_edited_resource() == p_resource) || seb->get_edited_resource()->get_path() == p_resource->get_path()) {
|
|
if (should_open) {
|
|
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb)) {
|
|
teb->enable_editor();
|
|
|
|
if (tab_container->get_current_tab() != i) {
|
|
_go_to_tab(i);
|
|
}
|
|
|
|
if (is_visible_in_tree()) {
|
|
teb->ensure_focus();
|
|
}
|
|
|
|
if (p_line >= 0) {
|
|
teb->goto_line(p_line, p_col);
|
|
}
|
|
} else if (tab_container->get_current_tab() != i) {
|
|
_go_to_tab(i);
|
|
}
|
|
}
|
|
_update_script_names();
|
|
script_list->ensure_current_is_visible();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// doesn't have it, make a new one
|
|
ScriptEditorBase *seb = nullptr;
|
|
|
|
for (int i = script_editor_func_count - 1; i >= 0; i--) {
|
|
seb = script_editor_funcs[i](p_resource);
|
|
if (seb) {
|
|
break;
|
|
}
|
|
}
|
|
ERR_FAIL_NULL_V(seb, false);
|
|
|
|
seb->set_edited_resource(p_resource);
|
|
|
|
seb->set_toggle_list_control(get_left_list_split());
|
|
tab_container->add_child(seb);
|
|
|
|
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb)) {
|
|
if (p_grab_focus) {
|
|
teb->enable_editor();
|
|
}
|
|
|
|
Control *editor_edit_menu = teb->get_edit_menu();
|
|
if (editor_edit_menu && !editor_edit_menu->get_parent()) {
|
|
editor_edit_menu->hide();
|
|
editor_menus.push_back(editor_edit_menu);
|
|
menu_hb->add_child(editor_edit_menu);
|
|
menu_hb->move_child(editor_edit_menu, 1);
|
|
for (int i = 0; i < editor_edit_menu->get_child_count(); ++i) {
|
|
Control *c = Object::cast_to<Control>(editor_edit_menu->get_child(i));
|
|
if (c) {
|
|
c->set_shortcut_context(this);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (p_grab_focus) {
|
|
_go_to_tab(tab_container->get_tab_count() - 1);
|
|
_add_recent_script(p_resource->get_path());
|
|
}
|
|
|
|
// If we delete a script within the filesystem, the original resource path
|
|
// is lost, so keep it as `edited_file_data` to figure out the exact tab to delete.
|
|
seb->edited_file_data.path = p_resource->get_path();
|
|
seb->edited_file_data.last_modified_time = FileAccess::get_modified_time(p_resource->get_path());
|
|
|
|
seb->connect("name_changed", callable_mp(this, &ScriptEditor::_update_script_names));
|
|
seb->connect("edited_script_changed", callable_mp(this, &ScriptEditor::_script_changed));
|
|
|
|
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb)) {
|
|
// Syntax highlighting.
|
|
bool highlighter_set = false;
|
|
for (int i = 0; i < syntax_highlighters.size(); i++) {
|
|
Ref<EditorSyntaxHighlighter> highlighter = syntax_highlighters[i]->_create();
|
|
if (highlighter.is_null()) {
|
|
continue;
|
|
}
|
|
teb->add_syntax_highlighter(highlighter);
|
|
|
|
if (highlighter_set) {
|
|
continue;
|
|
}
|
|
|
|
PackedStringArray languages = highlighter->_get_supported_languages();
|
|
// If script try language, else use extension.
|
|
if (scr.is_valid()) {
|
|
if (languages.has(scr->get_language()->get_name())) {
|
|
teb->set_syntax_highlighter(highlighter);
|
|
highlighter_set = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (languages.has(p_resource->get_path().get_extension())) {
|
|
teb->set_syntax_highlighter(highlighter);
|
|
highlighter_set = true;
|
|
}
|
|
}
|
|
|
|
teb->set_tooltip_request_func(callable_mp(this, &ScriptEditor::_get_debug_tooltip));
|
|
|
|
teb->connect("request_help", callable_mp(this, &ScriptEditor::_help_search));
|
|
teb->connect("request_open_script_at_line", callable_mp(this, &ScriptEditor::_goto_script_line));
|
|
teb->connect("go_to_help", callable_mp(this, &ScriptEditor::_help_class_goto));
|
|
teb->connect("request_save_history", callable_mp(this, &ScriptEditor::_save_history));
|
|
teb->connect("request_save_previous_state", callable_mp(this, &ScriptEditor::_save_previous_state));
|
|
teb->connect("search_in_files_requested", callable_mp(this, &ScriptEditor::open_find_in_files_dialog));
|
|
teb->connect("replace_in_files_requested", callable_mp(this, &ScriptEditor::_on_replace_in_files_requested));
|
|
teb->connect("go_to_method", callable_mp(this, &ScriptEditor::script_goto_method));
|
|
|
|
if (script_editor_cache->has_section(p_resource->get_path())) {
|
|
teb->set_edit_state(script_editor_cache->get_value(p_resource->get_path(), "state"));
|
|
if (ScriptTextEditor *ste = Object::cast_to<ScriptTextEditor>(teb)) {
|
|
ste->store_previous_state();
|
|
}
|
|
}
|
|
|
|
if (CodeTextEditor *cte = teb->get_code_editor()) {
|
|
cte->set_zoom_factor(zoom_factor);
|
|
cte->connect("zoomed", callable_mp(this, &ScriptEditor::_set_script_zoom_factor));
|
|
cte->connect(SceneStringName(visibility_changed), callable_mp(this, &ScriptEditor::_update_code_editor_zoom_factor).bind(cte));
|
|
}
|
|
}
|
|
|
|
_sort_list_on_update = true;
|
|
_update_script_names();
|
|
_save_layout();
|
|
|
|
//test for modification, maybe the script was not edited but was loaded
|
|
|
|
_test_script_times_on_disk(p_resource);
|
|
_update_modified_scripts_for_external_editor(p_resource);
|
|
|
|
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb)) {
|
|
if (p_line >= 0) {
|
|
teb->goto_line(p_line, p_col);
|
|
}
|
|
}
|
|
|
|
notify_script_changed(p_resource);
|
|
return true;
|
|
}
|
|
|
|
void ScriptEditor::reload_open_files() {
|
|
_test_script_times_on_disk();
|
|
_update_modified_scripts_for_external_editor();
|
|
}
|
|
|
|
PackedStringArray ScriptEditor::get_unsaved_scripts() const {
|
|
PackedStringArray unsaved_list;
|
|
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
|
|
if (seb && seb->is_unsaved()) {
|
|
unsaved_list.append(seb->get_name());
|
|
}
|
|
}
|
|
return unsaved_list;
|
|
}
|
|
|
|
void ScriptEditor::save_current_script() {
|
|
ScriptEditorBase *current = _get_current_editor();
|
|
if (!current || _test_script_times_on_disk()) {
|
|
return;
|
|
}
|
|
|
|
_auto_format_text(current);
|
|
|
|
Ref<Resource> resource = current->get_edited_resource();
|
|
Ref<TextFile> text_file = resource;
|
|
Ref<Script> scr = resource;
|
|
|
|
if (text_file.is_valid()) {
|
|
current->apply_code();
|
|
_save_text_file(text_file, text_file->get_path());
|
|
return;
|
|
}
|
|
|
|
if (scr.is_valid()) {
|
|
clear_docs_from_script(scr);
|
|
}
|
|
|
|
EditorNode::get_singleton()->save_resource(resource);
|
|
|
|
if (scr.is_valid()) {
|
|
update_docs_from_script(scr);
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::save_all_scripts() {
|
|
HashSet<String> scenes_to_save;
|
|
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
|
|
if (!seb) {
|
|
continue;
|
|
}
|
|
|
|
_auto_format_text(seb);
|
|
|
|
if (!seb->is_unsaved()) {
|
|
continue;
|
|
}
|
|
|
|
Ref<Resource> edited_res = seb->get_edited_resource();
|
|
if (edited_res.is_valid()) {
|
|
seb->apply_code();
|
|
}
|
|
|
|
Ref<Script> scr = edited_res;
|
|
|
|
if (scr.is_valid()) {
|
|
clear_docs_from_script(scr);
|
|
}
|
|
|
|
if (!edited_res->is_built_in()) {
|
|
Ref<TextFile> text_file = edited_res;
|
|
if (text_file.is_valid()) {
|
|
_save_text_file(text_file, text_file->get_path());
|
|
continue;
|
|
}
|
|
|
|
// External script, save it.
|
|
EditorNode::get_singleton()->save_resource(edited_res);
|
|
} else {
|
|
// For built-in scripts, save their scenes instead.
|
|
const String scene_path = edited_res->get_path().get_slice("::", 0);
|
|
if (!scene_path.is_empty() && !scenes_to_save.has(scene_path)) {
|
|
scenes_to_save.insert(scene_path);
|
|
}
|
|
}
|
|
|
|
if (scr.is_valid()) {
|
|
update_docs_from_script(scr);
|
|
}
|
|
}
|
|
|
|
if (!scenes_to_save.is_empty()) {
|
|
EditorNode::get_singleton()->save_scene_list(scenes_to_save);
|
|
}
|
|
|
|
_update_script_names();
|
|
}
|
|
|
|
void ScriptEditor::update_script_times() {
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i))) {
|
|
seb->edited_file_data.last_modified_time = FileAccess::get_modified_time(seb->edited_file_data.path);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::apply_scripts() const {
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
|
|
if (!seb) {
|
|
continue;
|
|
}
|
|
|
|
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb)) {
|
|
teb->insert_final_newline();
|
|
}
|
|
seb->apply_code();
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::reload_scripts(bool p_refresh_only) {
|
|
// Call deferred to make sure it runs on the main thread.
|
|
if (!Thread::is_main_thread()) {
|
|
callable_mp(this, &ScriptEditor::_reload_scripts).call_deferred(p_refresh_only);
|
|
return;
|
|
}
|
|
_reload_scripts(p_refresh_only);
|
|
}
|
|
|
|
void ScriptEditor::_reload_scripts(bool p_refresh_only) {
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
|
|
if (!seb) {
|
|
continue;
|
|
}
|
|
|
|
Ref<Resource> edited_res = seb->get_edited_resource();
|
|
|
|
if (edited_res->is_built_in()) {
|
|
continue; // Internal script, who cares.
|
|
}
|
|
|
|
if (p_refresh_only) {
|
|
// Make sure the modified time is correct.
|
|
seb->edited_file_data.last_modified_time = FileAccess::get_modified_time(edited_res->get_path());
|
|
} else {
|
|
uint64_t last_date = seb->edited_file_data.last_modified_time;
|
|
uint64_t date = FileAccess::get_modified_time(edited_res->get_path());
|
|
|
|
if (last_date == date) {
|
|
continue;
|
|
}
|
|
seb->edited_file_data.last_modified_time = date;
|
|
|
|
Ref<Script> scr = edited_res;
|
|
if (scr.is_valid()) {
|
|
Ref<Script> rel_scr = ResourceLoader::load(scr->get_path(), scr->get_class(), ResourceFormatLoader::CACHE_MODE_IGNORE);
|
|
ERR_CONTINUE(rel_scr.is_null());
|
|
scr->set_source_code(rel_scr->get_source_code());
|
|
scr->reload(true);
|
|
|
|
update_docs_from_script(scr);
|
|
}
|
|
|
|
Ref<JSON> json = edited_res;
|
|
if (json.is_valid()) {
|
|
Ref<JSON> rel_json = ResourceLoader::load(json->get_path(), json->get_class(), ResourceFormatLoader::CACHE_MODE_IGNORE);
|
|
ERR_CONTINUE(rel_json.is_null());
|
|
json->parse(rel_json->get_parsed_text(), true);
|
|
}
|
|
|
|
Ref<TextFile> text_file = edited_res;
|
|
if (text_file.is_valid()) {
|
|
text_file->reload_from_file();
|
|
}
|
|
}
|
|
|
|
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb)) {
|
|
teb->reload_text();
|
|
}
|
|
}
|
|
|
|
disk_changed->hide();
|
|
_update_script_names();
|
|
}
|
|
|
|
void ScriptEditor::_auto_format_text(ScriptEditorBase *p_seb) {
|
|
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(p_seb)) {
|
|
if (trim_trailing_whitespace_on_save) {
|
|
teb->trim_trailing_whitespace();
|
|
}
|
|
if (trim_final_newlines_on_save) {
|
|
teb->trim_final_newlines();
|
|
}
|
|
if (convert_indent_on_save) {
|
|
teb->convert_indent();
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::open_find_in_files_dialog(const String &text) {
|
|
find_in_files_dialog->set_find_in_files_mode(FindInFilesDialog::SEARCH_MODE);
|
|
find_in_files_dialog->set_search_text(text);
|
|
find_in_files_dialog->popup_centered();
|
|
}
|
|
|
|
void ScriptEditor::open_script_create_dialog(const String &p_base_name, const String &p_base_path) {
|
|
_menu_option(FILE_MENU_NEW_SCRIPT);
|
|
script_create_dialog->config(p_base_name, p_base_path);
|
|
}
|
|
|
|
void ScriptEditor::open_text_file_create_dialog(const String &p_base_path, const String &p_base_name) {
|
|
_menu_option(FILE_MENU_NEW_TEXTFILE);
|
|
file_dialog->set_current_dir(p_base_path);
|
|
file_dialog->set_current_file(p_base_name);
|
|
}
|
|
|
|
Ref<Resource> ScriptEditor::open_file(const String &p_file) {
|
|
if (_get_recognized_extensions().find(p_file.get_extension())) {
|
|
Ref<Resource> scr = ResourceLoader::load(p_file);
|
|
if (scr.is_null()) {
|
|
EditorNode::get_singleton()->show_warning(TTR("Could not load file at:") + "\n\n" + p_file, TTR("Error!"));
|
|
return Ref<Resource>();
|
|
}
|
|
|
|
edit(scr);
|
|
return scr;
|
|
}
|
|
|
|
Error error;
|
|
Ref<TextFile> text_file = _load_text_file(p_file, &error);
|
|
if (error != OK) {
|
|
EditorNode::get_singleton()->show_warning(TTR("Could not load file at:") + "\n\n" + p_file, TTR("Error!"));
|
|
return Ref<Resource>();
|
|
}
|
|
|
|
if (text_file.is_valid()) {
|
|
edit(text_file);
|
|
return text_file;
|
|
}
|
|
return Ref<Resource>();
|
|
}
|
|
|
|
void ScriptEditor::_add_callback(Object *p_obj, const String &p_function, const PackedStringArray &p_args) {
|
|
ERR_FAIL_NULL(p_obj);
|
|
Ref<Script> scr = p_obj->get_script();
|
|
ERR_FAIL_COND(scr.is_null());
|
|
|
|
if (!scr->get_language()->can_make_function()) {
|
|
return;
|
|
}
|
|
|
|
EditorNode::get_singleton()->push_item(scr.ptr());
|
|
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
ScriptTextEditor *ste = Object::cast_to<ScriptTextEditor>(tab_container->get_tab_control(i));
|
|
if (!ste || ste->get_edited_resource() != scr) {
|
|
continue;
|
|
}
|
|
|
|
ste->add_callback(p_function, p_args);
|
|
|
|
_go_to_tab(i);
|
|
|
|
script_list->select(script_list->find_metadata(i));
|
|
|
|
// Save the current script so the changes can be picked up by an external editor.
|
|
if (!scr.ptr()->is_built_in()) { // But only if it's not built-in script.
|
|
save_current_script();
|
|
} else {
|
|
ste->apply_code();
|
|
scr->reload(true);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
// Move back to the previously edited node to reselect it in the Inspector and the SignalsDock.
|
|
// We assume that the previous item is the node on which the callbacks were added.
|
|
EditorNode::get_singleton()->edit_previous_item();
|
|
}
|
|
|
|
void ScriptEditor::_save_editor_state(ScriptEditorBase *p_editor) {
|
|
if (restoring_layout) {
|
|
return;
|
|
}
|
|
|
|
const String &path = p_editor->get_edited_resource()->get_path();
|
|
if (path.is_empty()) {
|
|
return;
|
|
}
|
|
|
|
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(p_editor)) {
|
|
script_editor_cache->set_value(path, "state", teb->get_edit_state());
|
|
}
|
|
// This is saved later when we save the editor layout.
|
|
}
|
|
|
|
void ScriptEditor::_save_layout() {
|
|
if (restoring_layout) {
|
|
return;
|
|
}
|
|
|
|
EditorNode::get_singleton()->save_editor_layout_delayed();
|
|
}
|
|
|
|
void ScriptEditor::_apply_editor_settings() {
|
|
textfile_extensions.clear();
|
|
const Vector<String> textfile_ext = ((String)(EDITOR_GET("docks/filesystem/textfile_extensions"))).split(",", false);
|
|
for (const String &E : textfile_ext) {
|
|
textfile_extensions.insert(E);
|
|
}
|
|
|
|
trim_trailing_whitespace_on_save = EDITOR_GET("text_editor/behavior/files/trim_trailing_whitespace_on_save");
|
|
trim_final_newlines_on_save = EDITOR_GET("text_editor/behavior/files/trim_final_newlines_on_save");
|
|
convert_indent_on_save = EDITOR_GET("text_editor/behavior/files/convert_indent_on_save");
|
|
|
|
members_overview_enabled = EDITOR_GET("text_editor/script_list/show_members_overview");
|
|
help_overview_enabled = EDITOR_GET("text_editor/help/show_help_index");
|
|
external_editor_active = EDITOR_GET("text_editor/external/use_external_editor");
|
|
_update_members_overview_visibility();
|
|
_update_help_overview_visibility();
|
|
|
|
_update_autosave_timer();
|
|
|
|
_update_script_names();
|
|
|
|
ScriptServer::set_reload_scripts_on_save(EDITOR_GET("text_editor/behavior/files/auto_reload_and_parse_scripts_on_save"));
|
|
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(tab_container->get_tab_control(i))) {
|
|
teb->update_settings();
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_filesystem_changed() {
|
|
_update_script_names();
|
|
}
|
|
|
|
void ScriptEditor::_files_moved(const String &p_old_file, const String &p_new_file) {
|
|
if (!script_editor_cache->has_section(p_old_file)) {
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
|
|
if (seb && seb->edited_file_data.path == p_old_file) {
|
|
seb->edited_file_data.path = p_new_file;
|
|
break;
|
|
}
|
|
}
|
|
|
|
Variant state = script_editor_cache->get_value(p_old_file, "state");
|
|
script_editor_cache->erase_section(p_old_file);
|
|
script_editor_cache->set_value(p_new_file, "state", state);
|
|
|
|
// If Script, update breakpoints with debugger.
|
|
Array breakpoints = _get_cached_breakpoints_for_script(p_new_file);
|
|
for (int breakpoint : breakpoints) {
|
|
int line = (int)breakpoint + 1;
|
|
EditorDebuggerNode::get_singleton()->set_breakpoint(p_old_file, line, false);
|
|
if (!p_new_file.begins_with("local://") && ResourceLoader::exists(p_new_file, "Script")) {
|
|
EditorDebuggerNode::get_singleton()->set_breakpoint(p_new_file, line, true);
|
|
}
|
|
}
|
|
// This is saved later when we save the editor layout.
|
|
}
|
|
|
|
void ScriptEditor::_file_removed(const String &p_removed_file) {
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
|
|
if (seb && seb->edited_file_data.path == p_removed_file) {
|
|
// The script is deleted with no undo, so just close the tab.
|
|
_close_tab(i, false, false);
|
|
}
|
|
}
|
|
|
|
// Check closed.
|
|
if (script_editor_cache->has_section(p_removed_file)) {
|
|
Array breakpoints = _get_cached_breakpoints_for_script(p_removed_file);
|
|
for (int breakpoint : breakpoints) {
|
|
EditorDebuggerNode::get_singleton()->set_breakpoint(p_removed_file, (int)breakpoint + 1, false);
|
|
}
|
|
script_editor_cache->erase_section(p_removed_file);
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_update_find_replace_bar() {
|
|
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(_get_current_editor())) {
|
|
teb->set_find_replace_bar(find_replace_bar);
|
|
} else {
|
|
find_replace_bar->set_text_edit(nullptr);
|
|
find_replace_bar->hide();
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_autosave_scripts() {
|
|
save_all_scripts();
|
|
}
|
|
|
|
void ScriptEditor::_update_autosave_timer() {
|
|
if (!autosave_timer->is_inside_tree()) {
|
|
return;
|
|
}
|
|
|
|
float autosave_time = EDITOR_GET("text_editor/behavior/files/autosave_interval_secs");
|
|
if (autosave_time > 0) {
|
|
autosave_timer->set_wait_time(autosave_time);
|
|
autosave_timer->start();
|
|
} else {
|
|
autosave_timer->stop();
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_tree_changed() {
|
|
if (waiting_update_names) {
|
|
return;
|
|
}
|
|
|
|
waiting_update_names = true;
|
|
callable_mp(this, &ScriptEditor::_update_script_names).call_deferred();
|
|
}
|
|
|
|
void ScriptEditor::_split_dragged(float) {
|
|
_save_layout();
|
|
}
|
|
|
|
Variant ScriptEditor::get_drag_data_fw(const Point2 &p_point, Control *p_from) {
|
|
if (tab_container->get_tab_count() == 0) {
|
|
return Variant();
|
|
}
|
|
|
|
Node *cur_node = tab_container->get_tab_control(tab_container->get_current_tab());
|
|
|
|
HBoxContainer *drag_preview = memnew(HBoxContainer);
|
|
String preview_name = "";
|
|
Ref<Texture2D> preview_icon;
|
|
|
|
if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(cur_node)) {
|
|
preview_name = seb->get_name();
|
|
preview_icon = seb->get_theme_icon();
|
|
}
|
|
if (EditorHelp *eh = Object::cast_to<EditorHelp>(cur_node)) {
|
|
preview_name = eh->get_class();
|
|
preview_icon = get_editor_theme_icon(SNAME("Help"));
|
|
}
|
|
|
|
if (preview_icon.is_valid()) {
|
|
TextureRect *tf = memnew(TextureRect);
|
|
tf->set_texture(preview_icon);
|
|
tf->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
|
|
drag_preview->add_child(tf);
|
|
}
|
|
Label *label = memnew(Label(preview_name));
|
|
label->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); // Don't translate script names and class names.
|
|
drag_preview->add_child(label);
|
|
set_drag_preview(drag_preview);
|
|
|
|
Dictionary drag_data;
|
|
drag_data["type"] = "script_list_element"; // using a custom type because node caused problems when dragging to scene tree
|
|
drag_data["script_list_element"] = cur_node;
|
|
|
|
return drag_data;
|
|
}
|
|
|
|
bool ScriptEditor::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const {
|
|
Dictionary d = p_data;
|
|
if (!d.has("type")) {
|
|
return false;
|
|
}
|
|
|
|
if (String(d["type"]) == "script_list_element") {
|
|
Node *node = Object::cast_to<Node>(d["script_list_element"]);
|
|
|
|
if (Object::cast_to<ScriptEditorBase>(node) || Object::cast_to<EditorHelp>(node)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (String(d["type"]) == "nodes") {
|
|
Array nodes = d["nodes"];
|
|
if (nodes.is_empty()) {
|
|
return false;
|
|
}
|
|
Node *node = get_node((nodes[0]));
|
|
|
|
if (Object::cast_to<ScriptEditorBase>(node) || Object::cast_to<EditorHelp>(node)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (String(d["type"]) == "files") {
|
|
Vector<String> files = d["files"];
|
|
|
|
if (files.is_empty()) {
|
|
return false; //weird
|
|
}
|
|
|
|
for (int i = 0; i < files.size(); i++) {
|
|
const String &file = files[i];
|
|
if (file.is_empty() || !FileAccess::exists(file)) {
|
|
continue;
|
|
}
|
|
if (ResourceLoader::exists(file, "Script") || ResourceLoader::exists(file, "JSON")) {
|
|
Ref<Resource> scr = ResourceLoader::load(file);
|
|
if (scr.is_valid()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (textfile_extensions.has(file.get_extension())) {
|
|
Error err;
|
|
Ref<TextFile> text_file = _load_text_file(file, &err);
|
|
if (text_file.is_valid() && err == OK) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void ScriptEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) {
|
|
if (!can_drop_data_fw(p_point, p_data, p_from)) {
|
|
return;
|
|
}
|
|
|
|
Dictionary d = p_data;
|
|
if (!d.has("type")) {
|
|
return;
|
|
}
|
|
|
|
if (String(d["type"]) == "script_list_element") {
|
|
Node *node = Object::cast_to<Node>(d["script_list_element"]);
|
|
|
|
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(node);
|
|
EditorHelp *eh = Object::cast_to<EditorHelp>(node);
|
|
if (seb || eh) {
|
|
int new_index = 0;
|
|
if (script_list->get_item_count() > 0) {
|
|
int pos = 0;
|
|
if (p_point == Vector2(Math::INF, Math::INF)) {
|
|
if (script_list->is_anything_selected()) {
|
|
pos = script_list->get_selected_items()[0];
|
|
}
|
|
} else {
|
|
pos = script_list->get_item_at_position(p_point);
|
|
}
|
|
new_index = script_list->get_item_metadata(pos);
|
|
}
|
|
tab_container->move_child(node, new_index);
|
|
tab_container->set_current_tab(new_index);
|
|
_update_script_names();
|
|
}
|
|
}
|
|
|
|
if (String(d["type"]) == "nodes") {
|
|
Array nodes = d["nodes"];
|
|
if (nodes.is_empty()) {
|
|
return;
|
|
}
|
|
Node *node = get_node(nodes[0]);
|
|
|
|
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(node);
|
|
EditorHelp *eh = Object::cast_to<EditorHelp>(node);
|
|
if (seb || eh) {
|
|
int new_index = 0;
|
|
if (script_list->get_item_count() > 0) {
|
|
int pos = 0;
|
|
if (p_point == Vector2(Math::INF, Math::INF)) {
|
|
if (script_list->is_anything_selected()) {
|
|
pos = script_list->get_selected_items()[0];
|
|
}
|
|
} else {
|
|
pos = script_list->get_item_at_position(p_point);
|
|
}
|
|
new_index = script_list->get_item_metadata(pos);
|
|
}
|
|
tab_container->move_child(node, new_index);
|
|
tab_container->set_current_tab(new_index);
|
|
_update_script_names();
|
|
}
|
|
}
|
|
|
|
if (String(d["type"]) == "files") {
|
|
Vector<String> files = d["files"];
|
|
|
|
int new_index = 0;
|
|
if (script_list->get_item_count() > 0) {
|
|
int pos = 0;
|
|
if (p_point == Vector2(Math::INF, Math::INF)) {
|
|
if (script_list->is_anything_selected()) {
|
|
pos = script_list->get_selected_items()[0];
|
|
}
|
|
} else {
|
|
pos = script_list->get_item_at_position(p_point);
|
|
}
|
|
new_index = script_list->get_item_metadata(pos);
|
|
}
|
|
int num_tabs_before = tab_container->get_tab_count();
|
|
for (int i = 0; i < files.size(); i++) {
|
|
const String &file = files[i];
|
|
if (file.is_empty() || !FileAccess::exists(file)) {
|
|
continue;
|
|
}
|
|
|
|
if (!ResourceLoader::exists(file, "Script") && !ResourceLoader::exists(file, "JSON") && !textfile_extensions.has(file.get_extension())) {
|
|
continue;
|
|
}
|
|
|
|
Ref<Resource> res = open_file(file);
|
|
if (res.is_valid()) {
|
|
const int num_tabs = tab_container->get_tab_count();
|
|
if (num_tabs > num_tabs_before) {
|
|
tab_container->move_child(tab_container->get_tab_control(tab_container->get_tab_count() - 1), new_index);
|
|
num_tabs_before = num_tabs;
|
|
} else if (num_tabs > 0) { /* Maybe script was already open */
|
|
tab_container->move_child(tab_container->get_tab_control(tab_container->get_current_tab()), new_index);
|
|
}
|
|
}
|
|
}
|
|
if (tab_container->get_tab_count() > 0) {
|
|
tab_container->set_current_tab(new_index);
|
|
}
|
|
_update_script_names();
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::input(const Ref<InputEvent> &p_event) {
|
|
// This is implemented in `input()` rather than `unhandled_input()` to allow
|
|
// the shortcut to be used regardless of the click location.
|
|
// This feature can be disabled to avoid interfering with other uses of the additional
|
|
// mouse buttons, such as push-to-talk in a VoIP program.
|
|
if (EDITOR_GET("interface/editor/mouse_extra_buttons_navigate_history")) {
|
|
const Ref<InputEventMouseButton> mb = p_event;
|
|
|
|
// Navigate the script history using additional mouse buttons present on some mice.
|
|
// This must be hardcoded as the editor shortcuts dialog doesn't allow assigning
|
|
// more than one shortcut per action.
|
|
if (mb.is_valid() && mb->is_pressed() && is_visible_in_tree()) {
|
|
if (mb->get_button_index() == MouseButton::MB_XBUTTON1) {
|
|
_history_back();
|
|
}
|
|
|
|
if (mb->get_button_index() == MouseButton::MB_XBUTTON2) {
|
|
_history_forward();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::shortcut_input(const Ref<InputEvent> &p_event) {
|
|
ERR_FAIL_COND(p_event.is_null());
|
|
|
|
if (!is_visible_in_tree() || !p_event->is_pressed()) {
|
|
return;
|
|
}
|
|
if (ED_IS_SHORTCUT("script_editor/next_script", p_event)) {
|
|
if (script_list->get_item_count() > 1) {
|
|
int next_tab = script_list->get_current() + 1;
|
|
next_tab %= script_list->get_item_count();
|
|
_go_to_tab(script_list->get_item_metadata(next_tab));
|
|
_update_script_names();
|
|
}
|
|
accept_event();
|
|
}
|
|
if (ED_IS_SHORTCUT("script_editor/prev_script", p_event)) {
|
|
if (script_list->get_item_count() > 1) {
|
|
int next_tab = script_list->get_current() - 1;
|
|
next_tab = next_tab >= 0 ? next_tab : script_list->get_item_count() - 1;
|
|
_go_to_tab(script_list->get_item_metadata(next_tab));
|
|
_update_script_names();
|
|
}
|
|
accept_event();
|
|
}
|
|
if (ED_IS_SHORTCUT("script_editor/window_move_up", p_event)) {
|
|
_menu_option(FILE_MENU_MOVE_UP);
|
|
accept_event();
|
|
}
|
|
if (ED_IS_SHORTCUT("script_editor/window_move_down", p_event)) {
|
|
_menu_option(FILE_MENU_MOVE_DOWN);
|
|
accept_event();
|
|
}
|
|
|
|
if (p_event->is_echo()) {
|
|
return;
|
|
}
|
|
|
|
Callable custom_callback = EditorContextMenuPluginManager::get_singleton()->match_custom_shortcut(EditorContextMenuPlugin::CONTEXT_SLOT_SCRIPT_EDITOR, p_event);
|
|
if (custom_callback.is_valid()) {
|
|
Ref<Resource> resource;
|
|
if (ScriptEditorBase *current = _get_current_editor()) {
|
|
resource = current->get_edited_resource();
|
|
}
|
|
EditorContextMenuPluginManager::get_singleton()->invoke_callback(custom_callback, resource);
|
|
accept_event();
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_script_list_clicked(int p_item, Vector2 p_local_mouse_pos, MouseButton p_mouse_button_index) {
|
|
if (p_mouse_button_index == MouseButton::MIDDLE) {
|
|
script_list->select(p_item);
|
|
_script_selected(p_item);
|
|
_menu_option(FILE_MENU_CLOSE);
|
|
}
|
|
|
|
if (p_mouse_button_index == MouseButton::RIGHT) {
|
|
_make_script_list_context_menu();
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_make_script_list_context_menu() {
|
|
context_menu->clear();
|
|
|
|
int selected = tab_container->get_current_tab();
|
|
if (selected < 0 || selected >= tab_container->get_tab_count()) {
|
|
return;
|
|
}
|
|
|
|
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(selected));
|
|
if (seb) {
|
|
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/save"), FILE_MENU_SAVE);
|
|
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/save_as"), FILE_MENU_SAVE_AS);
|
|
}
|
|
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/close_file"), FILE_MENU_CLOSE);
|
|
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/close_other_tabs"), FILE_MENU_CLOSE_OTHER_TABS);
|
|
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/close_tabs_below"), FILE_MENU_CLOSE_TABS_BELOW);
|
|
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/close_all"), FILE_MENU_CLOSE_ALL);
|
|
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/close_docs"), FILE_MENU_CLOSE_DOCS);
|
|
context_menu->add_separator();
|
|
if (seb) {
|
|
Ref<Script> scr = seb->get_edited_resource();
|
|
if (scr.is_valid() && scr->is_tool()) {
|
|
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/reload_script_soft"), FILE_MENU_SOFT_RELOAD_TOOL);
|
|
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/run_file"), FILE_MENU_RUN);
|
|
context_menu->add_separator();
|
|
}
|
|
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/copy_path"), FILE_MENU_COPY_PATH);
|
|
context_menu->set_item_disabled(-1, seb->get_edited_resource()->get_path().is_empty());
|
|
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/copy_uid"), FILE_MENU_COPY_UID);
|
|
context_menu->set_item_disabled(-1, ResourceLoader::get_resource_uid(seb->get_edited_resource()->get_path()) == ResourceUID::INVALID_ID);
|
|
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/show_in_file_system"), FILE_MENU_SHOW_IN_FILE_SYSTEM);
|
|
context_menu->add_separator();
|
|
}
|
|
|
|
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/window_move_up"), FILE_MENU_MOVE_UP);
|
|
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/window_move_down"), FILE_MENU_MOVE_DOWN);
|
|
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/window_sort"), FILE_MENU_SORT);
|
|
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/toggle_files_panel"), FILE_MENU_TOGGLE_FILES_PANEL);
|
|
|
|
context_menu->set_item_disabled(context_menu->get_item_index(FILE_MENU_CLOSE_ALL), tab_container->get_tab_count() <= 0);
|
|
context_menu->set_item_disabled(context_menu->get_item_index(FILE_MENU_CLOSE_OTHER_TABS), tab_container->get_tab_count() <= 1);
|
|
context_menu->set_item_disabled(context_menu->get_item_index(FILE_MENU_CLOSE_DOCS), !_has_docs_tab());
|
|
context_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);
|
|
context_menu->set_item_disabled(context_menu->get_item_index(FILE_MENU_MOVE_UP), tab_container->get_current_tab() <= 0);
|
|
context_menu->set_item_disabled(context_menu->get_item_index(FILE_MENU_MOVE_DOWN), tab_container->get_current_tab() >= tab_container->get_tab_count() - 1);
|
|
context_menu->set_item_disabled(context_menu->get_item_index(FILE_MENU_SORT), tab_container->get_tab_count() <= 1);
|
|
|
|
// Context menu plugin.
|
|
Vector<String> selected_paths;
|
|
if (seb) {
|
|
Ref<Resource> scr = seb->get_edited_resource();
|
|
if (scr.is_valid()) {
|
|
String path = scr->get_path();
|
|
selected_paths.push_back(path);
|
|
}
|
|
}
|
|
EditorContextMenuPluginManager::get_singleton()->add_options_from_plugins(context_menu, EditorContextMenuPlugin::CONTEXT_SLOT_SCRIPT_EDITOR, selected_paths);
|
|
|
|
context_menu->set_position(get_screen_position() + get_local_mouse_position());
|
|
context_menu->reset_size();
|
|
context_menu->popup();
|
|
}
|
|
|
|
void ScriptEditor::set_window_layout(Ref<ConfigFile> p_layout) {
|
|
if (!bool(EDITOR_GET("text_editor/behavior/files/restore_scripts_on_load"))) {
|
|
return;
|
|
}
|
|
|
|
if (!p_layout->has_section_key("ScriptEditor", "open_scripts") && !p_layout->has_section_key("ScriptEditor", "open_help")) {
|
|
return;
|
|
}
|
|
|
|
Array scripts = p_layout->get_value("ScriptEditor", "open_scripts");
|
|
Array helps;
|
|
if (p_layout->has_section_key("ScriptEditor", "open_help")) {
|
|
helps = p_layout->get_value("ScriptEditor", "open_help");
|
|
}
|
|
|
|
restoring_layout = true;
|
|
|
|
HashSet<String> loaded_scripts;
|
|
List<String> extensions = _get_recognized_extensions();
|
|
|
|
for (const Variant &v : scripts) {
|
|
String path = v;
|
|
|
|
Dictionary script_info = v;
|
|
if (!script_info.is_empty()) {
|
|
path = script_info["path"];
|
|
}
|
|
|
|
if (!_script_exists(path)) {
|
|
if (script_editor_cache->has_section(path)) {
|
|
script_editor_cache->erase_section(path);
|
|
}
|
|
continue;
|
|
} else if (!path.is_resource_file() && !EditorNode::get_singleton()->is_scene_open(path.get_slice("::", 0))) {
|
|
continue;
|
|
}
|
|
loaded_scripts.insert(path);
|
|
|
|
bool is_script = false;
|
|
if (path.is_resource_file()) {
|
|
is_script = extensions.find(path.get_extension());
|
|
} else {
|
|
Ref<Script> scr = ResourceCache::get_ref(path);
|
|
if (scr.is_valid()) {
|
|
is_script = true;
|
|
} else {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (is_script) {
|
|
Ref<Resource> scr = ResourceLoader::load(path);
|
|
if (scr.is_null()) {
|
|
continue;
|
|
}
|
|
if (!edit(scr, false)) {
|
|
continue;
|
|
}
|
|
} else {
|
|
Error error;
|
|
Ref<TextFile> text_file = _load_text_file(path, &error);
|
|
if (error != OK || text_file.is_null()) {
|
|
continue;
|
|
}
|
|
if (!edit(text_file, false)) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (!script_info.is_empty()) {
|
|
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(tab_container->get_tab_control(tab_container->get_tab_count() - 1))) {
|
|
teb->set_edit_state(script_info["state"]);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < helps.size(); i++) {
|
|
String path = helps[i];
|
|
if (path.is_empty()) { // invalid, skip
|
|
continue;
|
|
}
|
|
_help_class_open(path);
|
|
}
|
|
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
tab_container->get_tab_control(i)->set_meta("__editor_pass", Variant());
|
|
}
|
|
|
|
if (p_layout->has_section_key("ScriptEditor", "script_split_offset")) {
|
|
script_split->set_split_offset(p_layout->get_value("ScriptEditor", "script_split_offset"));
|
|
}
|
|
|
|
if (p_layout->has_section_key("ScriptEditor", "list_split_offset")) {
|
|
list_split->set_split_offset(p_layout->get_value("ScriptEditor", "list_split_offset"));
|
|
}
|
|
|
|
// Remove any deleted editors that have been removed between launches.
|
|
// and if a Script, register breakpoints with the debugger.
|
|
Vector<String> cached_editors = script_editor_cache->get_sections();
|
|
for (const String &E : cached_editors) {
|
|
if (loaded_scripts.has(E)) {
|
|
continue;
|
|
}
|
|
|
|
if (!_script_exists(E)) {
|
|
script_editor_cache->erase_section(E);
|
|
continue;
|
|
}
|
|
|
|
Array breakpoints = _get_cached_breakpoints_for_script(E);
|
|
for (int breakpoint : breakpoints) {
|
|
EditorDebuggerNode::get_singleton()->set_breakpoint(E, (int)breakpoint + 1, true);
|
|
}
|
|
}
|
|
|
|
_set_script_zoom_factor(p_layout->get_value("ScriptEditor", "zoom_factor", 1.0f));
|
|
|
|
restoring_layout = false;
|
|
|
|
_update_script_names();
|
|
|
|
if (p_layout->has_section_key("ScriptEditor", "selected_script")) {
|
|
String selected_script = p_layout->get_value("ScriptEditor", "selected_script");
|
|
// If the selected script is not in the list of open scripts, select nothing.
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
|
|
if (seb && seb->get_edited_resource()->get_path() == selected_script) {
|
|
_go_to_tab(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::get_window_layout(Ref<ConfigFile> p_layout) {
|
|
Array scripts;
|
|
Array helps;
|
|
String selected_script;
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i))) {
|
|
const String path = seb->get_edited_resource()->get_path();
|
|
if (path.is_empty()) {
|
|
continue;
|
|
}
|
|
|
|
if (tab_container->get_current_tab_control() == tab_container->get_tab_control(i)) {
|
|
selected_script = path;
|
|
}
|
|
|
|
_save_editor_state(seb);
|
|
scripts.push_back(path);
|
|
}
|
|
|
|
if (EditorHelp *eh = Object::cast_to<EditorHelp>(tab_container->get_tab_control(i))) {
|
|
helps.push_back(eh->get_class());
|
|
}
|
|
}
|
|
|
|
p_layout->set_value("ScriptEditor", "open_scripts", scripts);
|
|
p_layout->set_value("ScriptEditor", "selected_script", selected_script);
|
|
p_layout->set_value("ScriptEditor", "open_help", helps);
|
|
p_layout->set_value("ScriptEditor", "script_split_offset", script_split->get_split_offset());
|
|
p_layout->set_value("ScriptEditor", "list_split_offset", list_split->get_split_offset());
|
|
p_layout->set_value("ScriptEditor", "zoom_factor", zoom_factor);
|
|
|
|
// Save the cache.
|
|
script_editor_cache->save(EditorPaths::get_singleton()->get_project_settings_dir().path_join("script_editor_cache.cfg"));
|
|
}
|
|
|
|
void ScriptEditor::_help_class_open(const String &p_class) {
|
|
if (p_class.is_empty()) {
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
EditorHelp *eh = Object::cast_to<EditorHelp>(tab_container->get_tab_control(i));
|
|
|
|
if (eh && eh->get_class() == p_class) {
|
|
_go_to_tab(i);
|
|
_update_script_names();
|
|
return;
|
|
}
|
|
}
|
|
|
|
EditorHelp *eh = memnew(EditorHelp);
|
|
|
|
eh->set_name(p_class);
|
|
tab_container->add_child(eh);
|
|
_go_to_tab(tab_container->get_tab_count() - 1);
|
|
eh->go_to_class(p_class);
|
|
eh->connect("go_to_help", callable_mp(this, &ScriptEditor::_help_class_goto));
|
|
eh->connect("request_save_history", callable_mp(this, &ScriptEditor::_save_history));
|
|
_add_recent_script(p_class);
|
|
_sort_list_on_update = true;
|
|
_update_script_names();
|
|
_save_layout();
|
|
}
|
|
|
|
void ScriptEditor::_help_class_goto(const String &p_desc) {
|
|
String cname = p_desc.get_slicec(':', 1);
|
|
|
|
if (_help_tab_goto(cname, p_desc)) {
|
|
return;
|
|
}
|
|
|
|
EditorHelp *eh = memnew(EditorHelp);
|
|
|
|
eh->set_name(cname);
|
|
tab_container->add_child(eh);
|
|
_go_to_tab(tab_container->get_tab_count() - 1);
|
|
eh->go_to_help(p_desc);
|
|
eh->connect("go_to_help", callable_mp(this, &ScriptEditor::_help_class_goto));
|
|
_add_recent_script(eh->get_class());
|
|
_sort_list_on_update = true;
|
|
_update_script_names();
|
|
_save_layout();
|
|
}
|
|
|
|
bool ScriptEditor::_help_tab_goto(const String &p_name, const String &p_desc) {
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
EditorHelp *eh = Object::cast_to<EditorHelp>(tab_container->get_tab_control(i));
|
|
|
|
if (eh && eh->get_class() == p_name) {
|
|
_go_to_tab(i);
|
|
eh->go_to_help(p_desc);
|
|
_update_script_names();
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void ScriptEditor::update_doc(const String &p_name) {
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
EditorHelp *eh = Object::cast_to<EditorHelp>(tab_container->get_tab_control(i));
|
|
if (eh && eh->get_class() == p_name) {
|
|
eh->update_doc();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::clear_docs_from_script(const Ref<Script> &p_script) {
|
|
ERR_FAIL_COND(p_script.is_null());
|
|
|
|
for (const DocData::ClassDoc &cd : p_script->get_documentation()) {
|
|
if (EditorHelp::get_doc_data()->has_doc(cd.name)) {
|
|
EditorHelp::get_doc_data()->remove_doc(cd.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::update_docs_from_script(const Ref<Script> &p_script) {
|
|
ERR_FAIL_COND(p_script.is_null());
|
|
|
|
for (const DocData::ClassDoc &cd : p_script->get_documentation()) {
|
|
EditorHelp::get_doc_data()->add_doc(cd);
|
|
update_doc(cd.name);
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_update_selected_editor_menu() {
|
|
TextEditorBase *current_editor = Object::cast_to<TextEditorBase>(_get_current_editor());
|
|
for (Control *editor_menu : editor_menus) {
|
|
editor_menu->set_visible(current_editor && editor_menu == current_editor->get_edit_menu());
|
|
}
|
|
|
|
PopupMenu *search_popup = script_search_menu->get_popup();
|
|
search_popup->clear();
|
|
if (Object::cast_to<EditorHelp>(tab_container->get_current_tab_control())) {
|
|
search_popup->add_shortcut(ED_SHORTCUT("script_editor/find", TTRC("Find..."), KeyModifierMask::CMD_OR_CTRL | Key::F), HELP_SEARCH_FIND);
|
|
search_popup->add_shortcut(ED_SHORTCUT("script_editor/find_next", TTRC("Find Next"), Key::F3), HELP_SEARCH_FIND_NEXT);
|
|
search_popup->add_shortcut(ED_SHORTCUT("script_editor/find_previous", TTRC("Find Previous"), KeyModifierMask::SHIFT | Key::F3), HELP_SEARCH_FIND_PREVIOUS);
|
|
search_popup->add_separator();
|
|
search_popup->add_shortcut(ED_GET_SHORTCUT("editor/find_in_files"), SEARCH_IN_FILES);
|
|
search_popup->add_shortcut(ED_GET_SHORTCUT("script_editor/replace_in_files"), REPLACE_IN_FILES);
|
|
script_search_menu->show();
|
|
} else {
|
|
if (tab_container->get_tab_count() == 0) {
|
|
search_popup->add_shortcut(ED_GET_SHORTCUT("editor/find_in_files"), SEARCH_IN_FILES);
|
|
search_popup->add_shortcut(ED_GET_SHORTCUT("script_editor/replace_in_files"), REPLACE_IN_FILES);
|
|
script_search_menu->show();
|
|
} else {
|
|
script_search_menu->hide();
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_unlock_history() {
|
|
lock_history = false;
|
|
}
|
|
|
|
void ScriptEditor::_update_history_pos(int p_new_pos) {
|
|
Node *n = tab_container->get_current_tab_control();
|
|
|
|
if (Object::cast_to<TextEditorBase>(n)) {
|
|
history.write[history_pos].state = Object::cast_to<TextEditorBase>(n)->get_navigation_state();
|
|
}
|
|
if (Object::cast_to<EditorHelp>(n)) {
|
|
history.write[history_pos].state = Object::cast_to<EditorHelp>(n)->get_scroll();
|
|
}
|
|
|
|
history_pos = p_new_pos;
|
|
tab_container->set_current_tab(tab_container->get_tab_idx_from_control(history[history_pos].control));
|
|
|
|
n = history[history_pos].control;
|
|
|
|
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(n);
|
|
if (seb) {
|
|
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(n)) {
|
|
lock_history = true;
|
|
teb->set_edit_state(history[history_pos].state);
|
|
// `set_edit_state()` can modify the caret position which might trigger a
|
|
// request to save the history. Since `TextEdit::caret_changed` is emitted
|
|
// deferred, we need to defer unlocking of the history as well.
|
|
callable_mp(this, &ScriptEditor::_unlock_history).call_deferred();
|
|
teb->ensure_focus();
|
|
}
|
|
|
|
Ref<Script> scr = seb->get_edited_resource();
|
|
if (scr.is_valid()) {
|
|
notify_script_changed(scr);
|
|
}
|
|
|
|
seb->validate_script();
|
|
}
|
|
|
|
if (EditorHelp *eh = Object::cast_to<EditorHelp>(n)) {
|
|
eh->set_scroll(history[history_pos].state);
|
|
eh->set_focused();
|
|
}
|
|
|
|
n->set_meta("__editor_pass", ++edit_pass);
|
|
_update_script_names();
|
|
_update_history_arrows();
|
|
_update_selected_editor_menu();
|
|
}
|
|
|
|
void ScriptEditor::_history_forward() {
|
|
if (history_pos < history.size() - 1) {
|
|
_update_history_pos(history_pos + 1);
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_history_back() {
|
|
if (history_pos > 0) {
|
|
_update_history_pos(history_pos - 1);
|
|
}
|
|
}
|
|
|
|
Vector<Ref<Script>> ScriptEditor::get_open_scripts() const {
|
|
Vector<Ref<Script>> out_scripts = Vector<Ref<Script>>();
|
|
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
|
|
if (!seb) {
|
|
continue;
|
|
}
|
|
|
|
Ref<Script> scr = seb->get_edited_resource();
|
|
if (scr.is_valid()) {
|
|
out_scripts.push_back(scr);
|
|
}
|
|
}
|
|
|
|
return out_scripts;
|
|
}
|
|
|
|
TypedArray<ScriptEditorBase> ScriptEditor::_get_open_script_editors() const {
|
|
TypedArray<ScriptEditorBase> script_editors;
|
|
for (int i = 0; i < tab_container->get_tab_count(); i++) {
|
|
if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i))) {
|
|
script_editors.push_back(seb);
|
|
}
|
|
}
|
|
return script_editors;
|
|
}
|
|
|
|
void ScriptEditor::set_scene_root_script(Ref<Script> p_script) {
|
|
// Don't open dominant script if using an external editor.
|
|
bool use_external_editor =
|
|
external_editor_active ||
|
|
(p_script.is_valid() && p_script->get_language()->overrides_external_editor());
|
|
use_external_editor = use_external_editor && !(p_script.is_valid() && p_script->is_built_in()); // Ignore external editor for built-in scripts.
|
|
const bool open_dominant = EDITOR_GET("text_editor/behavior/files/open_dominant_script_on_scene_change");
|
|
|
|
if (open_dominant && !use_external_editor && p_script.is_valid()) {
|
|
edit(p_script);
|
|
}
|
|
}
|
|
|
|
bool ScriptEditor::script_goto_method(Ref<Script> p_script, const String &p_method) {
|
|
int line = p_script->get_member_line(p_method);
|
|
|
|
if (line == -1) {
|
|
return false;
|
|
}
|
|
|
|
return edit(p_script, line, 0);
|
|
}
|
|
|
|
void ScriptEditor::set_live_auto_reload_running_scripts(bool p_enabled) {
|
|
auto_reload_running_scripts = p_enabled;
|
|
}
|
|
|
|
void ScriptEditor::_calculate_script_name_button_size() {
|
|
Ref<Font> font = script_name_button->get_theme_font(SceneStringName(font), SNAME("Button"));
|
|
HorizontalAlignment alignment = script_name_button->get_text_alignment();
|
|
int font_size = script_name_button->get_theme_font_size(SceneStringName(font_size), SNAME("Button"));
|
|
String text = script_name_button->get_text();
|
|
int jst_flags = TextServer::JUSTIFICATION_WORD_BOUND | TextServer::JUSTIFICATION_KASHIDA | TextServer::JUSTIFICATION_SKIP_LAST_LINE | TextServer::JUSTIFICATION_DO_NOT_SKIP_SINGLE_LINE;
|
|
TextServer::Direction direction = TextServer::Direction(script_name_button->get_text_direction());
|
|
Vector2 text_size = font->get_string_size(text, alignment, -1, font_size, jst_flags, direction, TextServer::ORIENTATION_HORIZONTAL);
|
|
|
|
script_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);
|
|
_calculate_script_name_button_ratio();
|
|
}
|
|
|
|
void ScriptEditor::_calculate_script_name_button_ratio() {
|
|
const float total_width = script_name_button_hbox->get_size().width;
|
|
if (total_width <= 0) {
|
|
return;
|
|
}
|
|
|
|
// Make the ratios a fraction bigger, to avoid unnecessary trimming.
|
|
const float extra_ratio = 4 / total_width;
|
|
|
|
const float script_name_ratio = MIN(1, script_name_width / total_width + extra_ratio);
|
|
script_name_button->set_stretch_ratio(script_name_ratio);
|
|
|
|
float ratio_left = 1 - script_name_ratio;
|
|
script_name_button_left_spacer->set_stretch_ratio(ratio_left / 2);
|
|
script_name_button_right_spacer->set_stretch_ratio(ratio_left / 2);
|
|
}
|
|
|
|
void ScriptEditor::_help_search(const String &p_text) {
|
|
help_search_dialog->popup_dialog(p_text);
|
|
}
|
|
|
|
void ScriptEditor::register_syntax_highlighter(const Ref<EditorSyntaxHighlighter> &p_syntax_highlighter) {
|
|
ERR_FAIL_COND(p_syntax_highlighter.is_null());
|
|
|
|
if (!syntax_highlighters.has(p_syntax_highlighter)) {
|
|
syntax_highlighters.push_back(p_syntax_highlighter);
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::unregister_syntax_highlighter(const Ref<EditorSyntaxHighlighter> &p_syntax_highlighter) {
|
|
ERR_FAIL_COND(p_syntax_highlighter.is_null());
|
|
|
|
syntax_highlighters.erase(p_syntax_highlighter);
|
|
}
|
|
|
|
int ScriptEditor::script_editor_func_count = 0;
|
|
CreateScriptEditorFunc ScriptEditor::script_editor_funcs[ScriptEditor::SCRIPT_EDITOR_FUNC_MAX];
|
|
|
|
void ScriptEditor::register_create_script_editor_function(CreateScriptEditorFunc p_func) {
|
|
ERR_FAIL_COND(script_editor_func_count == SCRIPT_EDITOR_FUNC_MAX);
|
|
script_editor_funcs[script_editor_func_count++] = p_func;
|
|
}
|
|
|
|
void ScriptEditor::_script_changed() {
|
|
SignalsDock::get_singleton()->update_lists();
|
|
}
|
|
|
|
void ScriptEditor::_on_replace_in_files_requested(const String &text) {
|
|
find_in_files_dialog->set_find_in_files_mode(FindInFilesDialog::REPLACE_MODE);
|
|
find_in_files_dialog->set_search_text(text);
|
|
find_in_files_dialog->set_replace_text("");
|
|
find_in_files_dialog->popup_centered();
|
|
}
|
|
|
|
void ScriptEditor::_on_find_in_files_result_selected(const String &fpath, int line_number, int begin, int end) {
|
|
if (ResourceLoader::exists(fpath)) {
|
|
Ref<Resource> res = ResourceLoader::load(fpath);
|
|
|
|
if (fpath.get_extension() == "gdshader") {
|
|
ShaderEditorPlugin *shader_editor = Object::cast_to<ShaderEditorPlugin>(EditorNode::get_editor_data().get_editor_by_name("Shader"));
|
|
shader_editor->edit(res.ptr());
|
|
shader_editor->make_visible(true);
|
|
TextShaderEditor *text_shader_editor = Object::cast_to<TextShaderEditor>(shader_editor->get_shader_editor(res));
|
|
if (text_shader_editor) {
|
|
text_shader_editor->goto_line_selection(line_number - 1, begin, end);
|
|
}
|
|
return;
|
|
} else if (fpath.get_extension() == "tscn") {
|
|
const PackedStringArray lines = FileAccess::get_file_as_string(fpath).split("\n");
|
|
if (line_number > lines.size()) {
|
|
return;
|
|
}
|
|
|
|
const char *scr_header = "[sub_resource type=\"GDScript\" id=\"";
|
|
const char *source_header = "script/source = \"";
|
|
String script_id;
|
|
|
|
// Search the scene backwards from the found line.
|
|
int scan_line = line_number - 1;
|
|
while (scan_line >= 0) {
|
|
const String &line = lines[scan_line];
|
|
if (line.begins_with(source_header)) {
|
|
// Adjust line relative to the script beginning.
|
|
line_number -= scan_line + 1;
|
|
} else if (line.begins_with(scr_header)) {
|
|
script_id = line.trim_prefix(scr_header).get_slicec('"', 0);
|
|
break;
|
|
}
|
|
scan_line--;
|
|
}
|
|
|
|
EditorNode::get_singleton()->load_scene(fpath);
|
|
if (!script_id.is_empty()) {
|
|
Ref<Script> scr = ResourceLoader::load(fpath + "::" + script_id, "Script");
|
|
if (scr.is_valid()) {
|
|
edit(scr);
|
|
ScriptTextEditor *ste = Object::cast_to<ScriptTextEditor>(_get_current_editor());
|
|
|
|
if (ste) {
|
|
callable_mp(EditorInterface::get_singleton(), &EditorInterface::set_main_screen_editor).call_deferred("Script");
|
|
if (line_number == 0) {
|
|
const int source_len = strlen(source_header);
|
|
ste->goto_line_selection(line_number, begin - source_len, end - source_len);
|
|
} else {
|
|
ste->goto_line_selection(line_number, begin, end);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return;
|
|
} else {
|
|
Ref<Script> scr = res;
|
|
Ref<JSON> json = res;
|
|
if (scr.is_valid() || json.is_valid()) {
|
|
edit(scr);
|
|
|
|
ScriptTextEditor *ste = Object::cast_to<ScriptTextEditor>(_get_current_editor());
|
|
if (ste) {
|
|
EditorInterface::get_singleton()->set_main_screen_editor("Script");
|
|
ste->goto_line_selection(line_number - 1, begin, end);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the file is not a valid resource/script, load it as a text file.
|
|
Error err;
|
|
Ref<TextFile> text_file = _load_text_file(fpath, &err);
|
|
if (text_file.is_valid()) {
|
|
edit(text_file);
|
|
|
|
TextEditor *te = Object::cast_to<TextEditor>(_get_current_editor());
|
|
if (te) {
|
|
te->goto_line_selection(line_number - 1, begin, end);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_start_find_in_files(bool with_replace) {
|
|
FindInFilesPanel *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());
|
|
FindInFiles *f = panel->get_finder();
|
|
|
|
f->set_search_text(find_in_files_dialog->get_search_text());
|
|
f->set_match_case(find_in_files_dialog->is_match_case());
|
|
f->set_whole_words(find_in_files_dialog->is_whole_words());
|
|
f->set_folder(find_in_files_dialog->get_folder());
|
|
f->set_filter(find_in_files_dialog->get_filter());
|
|
f->set_includes(find_in_files_dialog->get_includes());
|
|
f->set_excludes(find_in_files_dialog->get_excludes());
|
|
|
|
panel->set_with_replace(with_replace);
|
|
panel->set_replace_text(find_in_files_dialog->get_replace_text());
|
|
panel->start_search();
|
|
|
|
find_in_files->make_visible();
|
|
}
|
|
|
|
void ScriptEditor::_on_find_in_files_modified_files(const PackedStringArray &paths) {
|
|
_test_script_times_on_disk();
|
|
_update_modified_scripts_for_external_editor();
|
|
}
|
|
|
|
void ScriptEditor::_set_script_zoom_factor(float p_zoom_factor) {
|
|
if (zoom_factor == p_zoom_factor) {
|
|
return;
|
|
}
|
|
|
|
zoom_factor = p_zoom_factor;
|
|
}
|
|
|
|
void ScriptEditor::_update_code_editor_zoom_factor(CodeTextEditor *p_code_text_editor) {
|
|
if (p_code_text_editor && p_code_text_editor->is_visible_in_tree() && zoom_factor != p_code_text_editor->get_zoom_factor()) {
|
|
p_code_text_editor->set_zoom_factor(zoom_factor);
|
|
}
|
|
}
|
|
|
|
void ScriptEditor::_window_changed(bool p_visible) {
|
|
make_floating->set_visible(!p_visible);
|
|
is_floating = p_visible;
|
|
}
|
|
|
|
void ScriptEditor::_filter_scripts_text_changed(const String &p_newtext) {
|
|
_update_script_names();
|
|
}
|
|
|
|
void ScriptEditor::_filter_methods_text_changed(const String &p_newtext) {
|
|
_update_members_overview();
|
|
}
|
|
|
|
void ScriptEditor::_bind_methods() {
|
|
ClassDB::bind_method("_help_tab_goto", &ScriptEditor::_help_tab_goto);
|
|
ClassDB::bind_method("get_current_editor", &ScriptEditor::_get_current_editor);
|
|
ClassDB::bind_method("get_open_script_editors", &ScriptEditor::_get_open_script_editors);
|
|
ClassDB::bind_method("get_breakpoints", &ScriptEditor::_get_breakpoints);
|
|
|
|
ClassDB::bind_method(D_METHOD("register_syntax_highlighter", "syntax_highlighter"), &ScriptEditor::register_syntax_highlighter);
|
|
ClassDB::bind_method(D_METHOD("unregister_syntax_highlighter", "syntax_highlighter"), &ScriptEditor::unregister_syntax_highlighter);
|
|
|
|
ClassDB::bind_method(D_METHOD("goto_line", "line_number"), &ScriptEditor::_goto_script_line2);
|
|
ClassDB::bind_method(D_METHOD("get_current_script"), &ScriptEditor::_get_current_script);
|
|
ClassDB::bind_method(D_METHOD("get_open_scripts"), &ScriptEditor::_get_open_scripts);
|
|
ClassDB::bind_method(D_METHOD("open_script_create_dialog", "base_name", "base_path"), &ScriptEditor::open_script_create_dialog);
|
|
ClassDB::bind_method(D_METHOD("reload_open_files"), &ScriptEditor::reload_open_files);
|
|
|
|
ClassDB::bind_method(D_METHOD("goto_help", "topic"), &ScriptEditor::goto_help);
|
|
ClassDB::bind_method(D_METHOD("update_docs_from_script", "script"), &ScriptEditor::update_docs_from_script);
|
|
ClassDB::bind_method(D_METHOD("clear_docs_from_script", "script"), &ScriptEditor::clear_docs_from_script);
|
|
|
|
ClassDB::bind_method(D_METHOD("save_all_scripts"), &ScriptEditor::save_all_scripts);
|
|
|
|
ADD_SIGNAL(MethodInfo("editor_script_changed", PropertyInfo(Variant::OBJECT, "script", PROPERTY_HINT_RESOURCE_TYPE, Script::get_class_static())));
|
|
ADD_SIGNAL(MethodInfo("script_close", PropertyInfo(Variant::OBJECT, "script", PROPERTY_HINT_RESOURCE_TYPE, Script::get_class_static())));
|
|
}
|
|
|
|
ScriptEditor::ScriptEditor(WindowWrapper *p_wrapper) {
|
|
window_wrapper = p_wrapper;
|
|
|
|
script_editor_cache.instantiate();
|
|
script_editor_cache->load(EditorPaths::get_singleton()->get_project_settings_dir().path_join("script_editor_cache.cfg"));
|
|
|
|
restoring_layout = false;
|
|
waiting_update_names = false;
|
|
pending_auto_reload = false;
|
|
auto_reload_running_scripts = true;
|
|
external_editor_active = false;
|
|
members_overview_enabled = EDITOR_GET("text_editor/script_list/show_members_overview");
|
|
help_overview_enabled = EDITOR_GET("text_editor/help/show_help_index");
|
|
|
|
VBoxContainer *main_container = memnew(VBoxContainer);
|
|
add_child(main_container);
|
|
|
|
menu_hb = memnew(HBoxContainer);
|
|
main_container->add_child(menu_hb);
|
|
|
|
script_split = memnew(HSplitContainer);
|
|
main_container->add_child(script_split);
|
|
script_split->set_v_size_flags(SIZE_EXPAND_FILL);
|
|
|
|
#ifdef ANDROID_ENABLED
|
|
virtual_keyboard_spacer = memnew(Control);
|
|
virtual_keyboard_spacer->set_h_size_flags(SIZE_EXPAND_FILL);
|
|
main_container->add_child(virtual_keyboard_spacer);
|
|
#endif
|
|
|
|
list_split = memnew(VSplitContainer);
|
|
script_split->add_child(list_split);
|
|
list_split->set_v_size_flags(SIZE_EXPAND_FILL);
|
|
|
|
scripts_vbox = memnew(VBoxContainer);
|
|
scripts_vbox->set_v_size_flags(SIZE_EXPAND_FILL);
|
|
list_split->add_child(scripts_vbox);
|
|
|
|
filter_scripts = memnew(LineEdit);
|
|
filter_scripts->set_placeholder(TTRC("Filter Scripts"));
|
|
filter_scripts->set_accessibility_name(TTRC("Filter Scripts"));
|
|
filter_scripts->set_clear_button_enabled(true);
|
|
filter_scripts->connect(SceneStringName(text_changed), callable_mp(this, &ScriptEditor::_filter_scripts_text_changed));
|
|
scripts_vbox->add_child(filter_scripts);
|
|
|
|
_sort_list_on_update = true;
|
|
script_list = memnew(ItemList);
|
|
script_list->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
|
|
script_list->set_custom_minimum_size(Size2(100, 60) * EDSCALE); //need to give a bit of limit to avoid it from disappearing
|
|
script_list->set_v_size_flags(SIZE_EXPAND_FILL);
|
|
script_list->set_theme_type_variation("ItemListSecondary");
|
|
script_split->set_split_offset(200 * EDSCALE);
|
|
script_list->set_allow_rmb_select(true);
|
|
scripts_vbox->add_child(script_list);
|
|
script_list->connect("item_clicked", callable_mp(this, &ScriptEditor::_script_list_clicked), CONNECT_DEFERRED);
|
|
SET_DRAG_FORWARDING_GCD(script_list, ScriptEditor);
|
|
|
|
context_menu = memnew(PopupMenu);
|
|
add_child(context_menu);
|
|
context_menu->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptEditor::_menu_option));
|
|
|
|
overview_vbox = memnew(VBoxContainer);
|
|
overview_vbox->set_custom_minimum_size(Size2(0, 90));
|
|
overview_vbox->set_v_size_flags(SIZE_EXPAND_FILL);
|
|
|
|
list_split->add_child(overview_vbox);
|
|
list_split->set_visible(EditorSettings::get_singleton()->get_project_metadata("files_panel", "show_files_panel", true));
|
|
buttons_hbox = memnew(HBoxContainer);
|
|
overview_vbox->add_child(buttons_hbox);
|
|
|
|
filter_methods = memnew(LineEdit);
|
|
filter_methods->set_placeholder(TTRC("Filter Methods"));
|
|
filter_methods->set_accessibility_name(TTRC("Filter Methods"));
|
|
filter_methods->set_clear_button_enabled(true);
|
|
filter_methods->set_h_size_flags(SIZE_EXPAND_FILL);
|
|
filter_methods->connect(SceneStringName(text_changed), callable_mp(this, &ScriptEditor::_filter_methods_text_changed));
|
|
buttons_hbox->add_child(filter_methods);
|
|
|
|
members_overview_alphabeta_sort_button = memnew(Button);
|
|
members_overview_alphabeta_sort_button->set_theme_type_variation(SceneStringName(FlatButton));
|
|
members_overview_alphabeta_sort_button->set_tooltip_text(TTRC("Toggle alphabetical sorting of the method list."));
|
|
members_overview_alphabeta_sort_button->set_toggle_mode(true);
|
|
members_overview_alphabeta_sort_button->set_pressed(EDITOR_GET("text_editor/script_list/sort_members_outline_alphabetically"));
|
|
members_overview_alphabeta_sort_button->connect(SceneStringName(toggled), callable_mp(this, &ScriptEditor::_toggle_members_overview_alpha_sort));
|
|
buttons_hbox->add_child(members_overview_alphabeta_sort_button);
|
|
|
|
members_overview = memnew(ItemList);
|
|
members_overview->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
|
|
members_overview->set_theme_type_variation("ItemListSecondary");
|
|
overview_vbox->add_child(members_overview);
|
|
|
|
members_overview->set_allow_reselect(true);
|
|
members_overview->set_custom_minimum_size(Size2(0, 60) * EDSCALE); //need to give a bit of limit to avoid it from disappearing
|
|
members_overview->set_v_size_flags(SIZE_EXPAND_FILL);
|
|
members_overview->set_allow_rmb_select(true);
|
|
|
|
help_overview = memnew(ItemList);
|
|
help_overview->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
|
|
help_overview->set_theme_type_variation("ItemListSecondary");
|
|
overview_vbox->add_child(help_overview);
|
|
help_overview->set_allow_reselect(true);
|
|
help_overview->set_custom_minimum_size(Size2(0, 60) * EDSCALE); //need to give a bit of limit to avoid it from disappearing
|
|
help_overview->set_v_size_flags(SIZE_EXPAND_FILL);
|
|
|
|
VBoxContainer *code_editor_container = memnew(VBoxContainer);
|
|
script_split->add_child(code_editor_container);
|
|
|
|
tab_container = memnew(TabContainer);
|
|
tab_container->set_tabs_visible(false);
|
|
tab_container->set_custom_minimum_size(Size2(200, 0) * EDSCALE);
|
|
code_editor_container->add_child(tab_container);
|
|
tab_container->set_h_size_flags(SIZE_EXPAND_FILL);
|
|
tab_container->set_v_size_flags(SIZE_EXPAND_FILL);
|
|
|
|
find_replace_bar = memnew(FindReplaceBar);
|
|
code_editor_container->add_child(find_replace_bar);
|
|
find_replace_bar->hide();
|
|
|
|
ED_SHORTCUT("script_editor/window_sort", TTRC("Sort"));
|
|
ED_SHORTCUT("script_editor/window_move_up", TTRC("Move Up"), KeyModifierMask::SHIFT | KeyModifierMask::ALT | Key::UP);
|
|
ED_SHORTCUT("script_editor/window_move_down", TTRC("Move Down"), KeyModifierMask::SHIFT | KeyModifierMask::ALT | Key::DOWN);
|
|
// FIXME: These should be `Key::GREATER` and `Key::LESS` but those don't work.
|
|
ED_SHORTCUT("script_editor/next_script", TTRC("Next Script"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::PERIOD);
|
|
ED_SHORTCUT("script_editor/prev_script", TTRC("Previous Script"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::COMMA);
|
|
set_process_input(true);
|
|
set_process_shortcut_input(true);
|
|
|
|
file_menu = memnew(MenuButton);
|
|
file_menu->set_flat(false);
|
|
file_menu->set_theme_type_variation("FlatMenuButton");
|
|
file_menu->set_text(TTRC("File"));
|
|
file_menu->set_switch_on_hover(true);
|
|
file_menu->set_shortcut_context(this);
|
|
menu_hb->add_child(file_menu);
|
|
|
|
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/new", TTRC("New Script..."), KeyModifierMask::CMD_OR_CTRL | Key::N), FILE_MENU_NEW_SCRIPT);
|
|
file_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);
|
|
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/open", TTRC("Open...")), FILE_MENU_OPEN);
|
|
file_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_editor/reopen_closed_script"), FILE_MENU_REOPEN_CLOSED);
|
|
|
|
recent_scripts = memnew(PopupMenu);
|
|
recent_scripts->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
|
|
file_menu->get_popup()->add_submenu_node_item(TTRC("Open Recent"), recent_scripts, FILE_MENU_OPEN_RECENT);
|
|
recent_scripts->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptEditor::_open_recent_script));
|
|
|
|
_update_recent_scripts();
|
|
|
|
file_menu->get_popup()->add_separator();
|
|
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/save", TTRC("Save"), KeyModifierMask::ALT | KeyModifierMask::CMD_OR_CTRL | Key::S), FILE_MENU_SAVE);
|
|
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/save_as", TTRC("Save As...")), FILE_MENU_SAVE_AS);
|
|
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/save_all", TTRC("Save All"), KeyModifierMask::SHIFT | KeyModifierMask::ALT | Key::S), FILE_MENU_SAVE_ALL);
|
|
ED_SHORTCUT_OVERRIDE("script_editor/save_all", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::S);
|
|
file_menu->get_popup()->add_separator();
|
|
file_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);
|
|
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/copy_path", TTRC("Copy File Path")), FILE_MENU_COPY_PATH);
|
|
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/copy_uid", TTRC("Copy File UID")), FILE_MENU_COPY_UID);
|
|
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/show_in_file_system", TTRC("Show in FileSystem")), FILE_MENU_SHOW_IN_FILE_SYSTEM);
|
|
file_menu->get_popup()->add_separator();
|
|
|
|
file_menu->get_popup()->add_shortcut(
|
|
ED_SHORTCUT_ARRAY("script_editor/history_previous", TTRC("History Previous"),
|
|
{ int32_t(KeyModifierMask::ALT | Key::LEFT), int32_t(Key::BACK) }),
|
|
FILE_MENU_HISTORY_PREV);
|
|
file_menu->get_popup()->add_shortcut(
|
|
ED_SHORTCUT_ARRAY("script_editor/history_next", TTRC("History Next"),
|
|
{ int32_t(KeyModifierMask::ALT | Key::RIGHT), int32_t(Key::FORWARD) }),
|
|
FILE_MENU_HISTORY_NEXT);
|
|
ED_SHORTCUT_OVERRIDE("script_editor/history_previous", "macos", KeyModifierMask::ALT | KeyModifierMask::META | Key::LEFT);
|
|
ED_SHORTCUT_OVERRIDE("script_editor/history_next", "macos", KeyModifierMask::ALT | KeyModifierMask::META | Key::RIGHT);
|
|
|
|
file_menu->get_popup()->add_separator();
|
|
|
|
theme_submenu = memnew(PopupMenu);
|
|
theme_submenu->add_shortcut(ED_SHORTCUT("script_editor/import_theme", TTRC("Import Theme...")), THEME_IMPORT);
|
|
theme_submenu->add_shortcut(ED_SHORTCUT("script_editor/reload_theme", TTRC("Reload Theme")), THEME_RELOAD);
|
|
file_menu->get_popup()->add_submenu_node_item(TTRC("Theme"), theme_submenu, FILE_MENU_THEME_SUBMENU);
|
|
theme_submenu->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptEditor::_theme_option));
|
|
|
|
theme_submenu->add_separator();
|
|
theme_submenu->add_shortcut(ED_SHORTCUT("script_editor/save_theme_as", TTRC("Save Theme As...")), THEME_SAVE_AS);
|
|
|
|
file_menu->get_popup()->add_separator();
|
|
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/close_file", TTRC("Close"), KeyModifierMask::CMD_OR_CTRL | Key::W), FILE_MENU_CLOSE);
|
|
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/close_all", TTRC("Close All")), FILE_MENU_CLOSE_ALL);
|
|
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/close_other_tabs", TTRC("Close Other Tabs")), FILE_MENU_CLOSE_OTHER_TABS);
|
|
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/close_tabs_below", TTRC("Close Tabs Below")), FILE_MENU_CLOSE_TABS_BELOW);
|
|
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/close_docs", TTRC("Close Docs")), FILE_MENU_CLOSE_DOCS);
|
|
|
|
file_menu->get_popup()->add_separator();
|
|
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/run_file", TTRC("Run"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::X), FILE_MENU_RUN);
|
|
|
|
file_menu->get_popup()->add_separator();
|
|
file_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);
|
|
file_menu->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptEditor::_menu_option));
|
|
file_menu->get_popup()->connect("about_to_popup", callable_mp(this, &ScriptEditor::_prepare_file_menu));
|
|
file_menu->get_popup()->connect("popup_hide", callable_mp(this, &ScriptEditor::_file_menu_closed));
|
|
|
|
script_search_menu = memnew(MenuButton);
|
|
script_search_menu->set_flat(false);
|
|
script_search_menu->set_theme_type_variation("FlatMenuButton");
|
|
script_search_menu->set_text(TTRC("Search"));
|
|
script_search_menu->set_switch_on_hover(true);
|
|
script_search_menu->set_shortcut_context(this);
|
|
script_search_menu->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptEditor::_menu_option));
|
|
menu_hb->add_child(script_search_menu);
|
|
|
|
MenuButton *debug_menu_btn = memnew(MenuButton);
|
|
debug_menu_btn->set_flat(false);
|
|
debug_menu_btn->set_theme_type_variation("FlatMenuButton");
|
|
menu_hb->add_child(debug_menu_btn);
|
|
debug_menu_btn->hide(); // Handled by EditorDebuggerNode below.
|
|
|
|
EditorDebuggerNode *debugger = EditorDebuggerNode::get_singleton();
|
|
debugger->set_script_debug_button(debug_menu_btn);
|
|
debugger->connect("goto_script_line", callable_mp(this, &ScriptEditor::_goto_script_line));
|
|
debugger->connect("set_execution", callable_mp(this, &ScriptEditor::_set_execution));
|
|
debugger->connect("clear_execution", callable_mp(this, &ScriptEditor::_clear_execution));
|
|
debugger->connect("breakpoint_set_in_tree", callable_mp(this, &ScriptEditor::_set_breakpoint));
|
|
debugger->connect("breakpoints_cleared_in_tree", callable_mp(this, &ScriptEditor::_clear_breakpoints));
|
|
|
|
script_name_button_hbox = memnew(HBoxContainer);
|
|
script_name_button_hbox->set_h_size_flags(SIZE_EXPAND_FILL);
|
|
script_name_button_hbox->add_theme_constant_override("separation", 0);
|
|
script_name_button_hbox->connect(SceneStringName(item_rect_changed), callable_mp(this, &ScriptEditor::_calculate_script_name_button_ratio));
|
|
menu_hb->add_child(script_name_button_hbox);
|
|
|
|
script_name_button_left_spacer = memnew(Control);
|
|
script_name_button_left_spacer->set_h_size_flags(Control::SIZE_EXPAND_FILL);
|
|
script_name_button_hbox->add_child(script_name_button_left_spacer);
|
|
|
|
script_name_button = memnew(Button);
|
|
script_name_button->set_flat(true);
|
|
script_name_button->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS);
|
|
script_name_button->set_h_size_flags(SIZE_EXPAND_FILL);
|
|
script_name_button->set_tooltip_text(TTRC("Navigate to script list."));
|
|
script_name_button->connect(SceneStringName(pressed), callable_mp(script_list, &ItemList::ensure_current_is_visible));
|
|
script_name_button_hbox->add_child(script_name_button);
|
|
|
|
script_name_button_right_spacer = memnew(Control);
|
|
script_name_button_right_spacer->set_h_size_flags(Control::SIZE_EXPAND_FILL);
|
|
script_name_button_hbox->add_child(script_name_button_right_spacer);
|
|
|
|
site_search = memnew(Button);
|
|
site_search->set_theme_type_variation(SceneStringName(FlatButton));
|
|
site_search->set_accessibility_name(TTRC("Site Search"));
|
|
site_search->connect(SceneStringName(pressed), callable_mp(this, &ScriptEditor::_menu_option).bind(SEARCH_WEBSITE));
|
|
menu_hb->add_child(site_search);
|
|
|
|
help_search = memnew(Button);
|
|
help_search->set_theme_type_variation(SceneStringName(FlatButton));
|
|
help_search->set_text(TTRC("Search Help"));
|
|
help_search->connect(SceneStringName(pressed), callable_mp(this, &ScriptEditor::_menu_option).bind(SEARCH_HELP));
|
|
menu_hb->add_child(help_search);
|
|
help_search->set_tooltip_text(TTRC("Search the reference documentation."));
|
|
|
|
menu_hb->add_child(memnew(VSeparator));
|
|
|
|
script_back = memnew(Button);
|
|
script_back->set_theme_type_variation(SceneStringName(FlatButton));
|
|
script_back->set_tooltip_text(TTRC("Go to previous edited document."));
|
|
script_back->set_shortcut(ED_GET_SHORTCUT("script_editor/history_previous"));
|
|
script_back->set_disabled(true);
|
|
menu_hb->add_child(script_back);
|
|
script_back->connect(SceneStringName(pressed), callable_mp(this, &ScriptEditor::_history_back));
|
|
|
|
script_forward = memnew(Button);
|
|
script_forward->set_theme_type_variation(SceneStringName(FlatButton));
|
|
script_forward->set_tooltip_text(TTRC("Go to next edited document."));
|
|
script_forward->set_shortcut(ED_GET_SHORTCUT("script_editor/history_next"));
|
|
script_forward->set_disabled(true);
|
|
menu_hb->add_child(script_forward);
|
|
script_forward->connect(SceneStringName(pressed), callable_mp(this, &ScriptEditor::_history_forward));
|
|
|
|
menu_hb->add_child(memnew(VSeparator));
|
|
|
|
make_floating = memnew(ScreenSelect);
|
|
make_floating->set_tooltip_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
|
|
make_floating->connect("request_open_in_screen", callable_mp(window_wrapper, &WindowWrapper::enable_window_on_screen).bind(true));
|
|
|
|
menu_hb->add_child(make_floating);
|
|
p_wrapper->connect("window_visibility_changed", callable_mp(this, &ScriptEditor::_window_changed));
|
|
|
|
tab_container->connect("tab_changed", callable_mp(this, &ScriptEditor::_tab_changed));
|
|
|
|
erase_tab_confirm = memnew(ConfirmationDialog);
|
|
erase_tab_confirm->set_ok_button_text(TTRC("Save"));
|
|
erase_tab_confirm->add_button(TTRC("Discard"), DisplayServer::get_singleton()->get_swap_cancel_ok(), "discard");
|
|
erase_tab_confirm->connect(SceneStringName(confirmed), callable_mp(this, &ScriptEditor::_close_current_tab).bind(true, true));
|
|
erase_tab_confirm->connect("custom_action", callable_mp(this, &ScriptEditor::_close_discard_current_tab));
|
|
add_child(erase_tab_confirm);
|
|
|
|
script_create_dialog = memnew(ScriptCreateDialog);
|
|
script_create_dialog->set_title(TTRC("Create Script"));
|
|
add_child(script_create_dialog);
|
|
script_create_dialog->connect("script_created", callable_mp(this, &ScriptEditor::_script_created));
|
|
|
|
file_dialog_option = -1;
|
|
file_dialog = memnew(EditorFileDialog);
|
|
add_child(file_dialog);
|
|
file_dialog->connect("file_selected", callable_mp(this, &ScriptEditor::_file_dialog_action));
|
|
|
|
error_dialog = memnew(AcceptDialog);
|
|
add_child(error_dialog);
|
|
|
|
disk_changed = memnew(ConfirmationDialog);
|
|
{
|
|
disk_changed->set_title(TTRC("Files have been modified outside Godot"));
|
|
|
|
VBoxContainer *vbc = memnew(VBoxContainer);
|
|
disk_changed->add_child(vbc);
|
|
|
|
Label *files_are_newer_label = memnew(Label);
|
|
files_are_newer_label->set_text(TTRC("The following files are newer on disk:"));
|
|
vbc->add_child(files_are_newer_label);
|
|
|
|
disk_changed_list = memnew(Tree);
|
|
disk_changed_list->set_hide_root(true);
|
|
disk_changed_list->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
|
|
disk_changed_list->set_accessibility_name(TTRC("The following files are newer on disk:"));
|
|
disk_changed_list->set_v_size_flags(SIZE_EXPAND_FILL);
|
|
vbc->add_child(disk_changed_list);
|
|
|
|
Label *what_action_label = memnew(Label);
|
|
what_action_label->set_text(TTRC("What action should be taken?"));
|
|
vbc->add_child(what_action_label);
|
|
|
|
disk_changed->connect(SceneStringName(confirmed), callable_mp(this, &ScriptEditor::reload_scripts).bind(false));
|
|
disk_changed->set_ok_button_text(TTRC("Reload from disk"));
|
|
|
|
disk_changed->add_button(TTRC("Ignore external changes"), !DisplayServer::get_singleton()->get_swap_cancel_ok(), "resave");
|
|
disk_changed->connect("custom_action", callable_mp(this, &ScriptEditor::_resave_scripts));
|
|
}
|
|
|
|
add_child(disk_changed);
|
|
|
|
script_editor = this;
|
|
|
|
autosave_timer = memnew(Timer);
|
|
autosave_timer->set_one_shot(false);
|
|
autosave_timer->connect(SceneStringName(tree_entered), callable_mp(this, &ScriptEditor::_update_autosave_timer));
|
|
autosave_timer->connect("timeout", callable_mp(this, &ScriptEditor::_autosave_scripts));
|
|
add_child(autosave_timer);
|
|
|
|
grab_focus_block = false;
|
|
|
|
help_search_dialog = memnew(EditorHelpSearch);
|
|
add_child(help_search_dialog);
|
|
help_search_dialog->connect("go_to_help", callable_mp(this, &ScriptEditor::_help_class_goto));
|
|
|
|
find_in_files_dialog = memnew(FindInFilesDialog);
|
|
find_in_files_dialog->connect(FindInFilesDialog::SIGNAL_FIND_REQUESTED, callable_mp(this, &ScriptEditor::_start_find_in_files).bind(false));
|
|
find_in_files_dialog->connect(FindInFilesDialog::SIGNAL_REPLACE_REQUESTED, callable_mp(this, &ScriptEditor::_start_find_in_files).bind(true));
|
|
add_child(find_in_files_dialog);
|
|
|
|
find_in_files = memnew(FindInFilesContainer);
|
|
EditorDockManager::get_singleton()->add_dock(find_in_files);
|
|
find_in_files->close();
|
|
find_in_files->connect("result_selected", callable_mp(this, &ScriptEditor::_on_find_in_files_result_selected));
|
|
find_in_files->connect("files_modified", callable_mp(this, &ScriptEditor::_on_find_in_files_modified_files));
|
|
|
|
history_pos = -1;
|
|
|
|
edit_pass = 0;
|
|
trim_trailing_whitespace_on_save = EDITOR_GET("text_editor/behavior/files/trim_trailing_whitespace_on_save");
|
|
trim_final_newlines_on_save = EDITOR_GET("text_editor/behavior/files/trim_final_newlines_on_save");
|
|
convert_indent_on_save = EDITOR_GET("text_editor/behavior/files/convert_indent_on_save");
|
|
|
|
Ref<EditorJSONSyntaxHighlighter> json_syntax_highlighter;
|
|
json_syntax_highlighter.instantiate();
|
|
register_syntax_highlighter(json_syntax_highlighter);
|
|
|
|
Ref<EditorMarkdownSyntaxHighlighter> markdown_syntax_highlighter;
|
|
markdown_syntax_highlighter.instantiate();
|
|
register_syntax_highlighter(markdown_syntax_highlighter);
|
|
|
|
Ref<EditorConfigFileSyntaxHighlighter> config_file_syntax_highlighter;
|
|
config_file_syntax_highlighter.instantiate();
|
|
register_syntax_highlighter(config_file_syntax_highlighter);
|
|
|
|
_update_online_doc();
|
|
}
|
|
|
|
void ScriptEditorPlugin::_focus_another_editor() {
|
|
if (window_wrapper->get_window_enabled()) {
|
|
ERR_FAIL_COND(last_editor.is_empty());
|
|
EditorInterface::get_singleton()->set_main_screen_editor(last_editor);
|
|
}
|
|
}
|
|
|
|
void ScriptEditorPlugin::_save_last_editor(const String &p_editor) {
|
|
if (p_editor != get_plugin_name()) {
|
|
last_editor = p_editor;
|
|
}
|
|
}
|
|
|
|
void ScriptEditorPlugin::_window_visibility_changed(bool p_visible) {
|
|
_focus_another_editor();
|
|
if (p_visible) {
|
|
script_editor->add_theme_style_override(SceneStringName(panel), script_editor->get_theme_stylebox("ScriptEditorPanelFloating", EditorStringName(EditorStyles)));
|
|
} else {
|
|
script_editor->add_theme_style_override(SceneStringName(panel), script_editor->get_theme_stylebox("ScriptEditorPanel", EditorStringName(EditorStyles)));
|
|
}
|
|
}
|
|
|
|
void ScriptEditorPlugin::_notification(int p_what) {
|
|
switch (p_what) {
|
|
case NOTIFICATION_TRANSLATION_CHANGED: {
|
|
window_wrapper->set_window_title(vformat(TTR("%s - Godot Engine"), TTR("Script Editor")));
|
|
} break;
|
|
case NOTIFICATION_ENTER_TREE: {
|
|
connect("main_screen_changed", callable_mp(this, &ScriptEditorPlugin::_save_last_editor));
|
|
} break;
|
|
case NOTIFICATION_EXIT_TREE: {
|
|
disconnect("main_screen_changed", callable_mp(this, &ScriptEditorPlugin::_save_last_editor));
|
|
} break;
|
|
}
|
|
}
|
|
|
|
bool ScriptEditorPlugin::open_in_external_editor(const String &p_path, int p_line, int p_col, bool p_ignore_project) {
|
|
const String path = EDITOR_GET("text_editor/external/exec_path");
|
|
if (path.is_empty()) {
|
|
return false;
|
|
}
|
|
|
|
String flags = EDITOR_GET("text_editor/external/exec_flags");
|
|
|
|
List<String> args;
|
|
bool has_file_flag = false;
|
|
|
|
if (!flags.is_empty()) {
|
|
flags = flags.replacen("{line}", itos(MAX(p_line + 1, 1)));
|
|
flags = flags.replacen("{col}", itos(p_col + 1));
|
|
flags = flags.strip_edges().replace("\\\\", "\\");
|
|
|
|
int from = 0;
|
|
int num_chars = 0;
|
|
bool inside_quotes = false;
|
|
|
|
for (int i = 0; i < flags.size(); i++) {
|
|
if (flags[i] == '"' && (!i || flags[i - 1] != '\\')) {
|
|
if (!inside_quotes) {
|
|
from++;
|
|
}
|
|
inside_quotes = !inside_quotes;
|
|
|
|
} else if (flags[i] == '\0' || (!inside_quotes && flags[i] == ' ')) {
|
|
String arg = flags.substr(from, num_chars);
|
|
if (arg.contains("{file}")) {
|
|
has_file_flag = true;
|
|
}
|
|
|
|
// Do path replacement here, else there will be issues with spaces and quotes
|
|
if (p_ignore_project) {
|
|
arg = arg.replacen("{project}", String());
|
|
} else {
|
|
arg = arg.replacen("{project}", ProjectSettings::get_singleton()->get_resource_path());
|
|
}
|
|
arg = arg.replacen("{file}", p_path);
|
|
args.push_back(arg);
|
|
|
|
from = i + 1;
|
|
num_chars = 0;
|
|
} else {
|
|
num_chars++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Default to passing script path if no {file} flag is specified.
|
|
if (!has_file_flag) {
|
|
args.push_back(p_path);
|
|
}
|
|
return OS::get_singleton()->create_process(path, args) == OK;
|
|
}
|
|
|
|
void ScriptEditorPlugin::edit(Object *p_object) {
|
|
if (!p_object) {
|
|
return;
|
|
}
|
|
|
|
if (Object::cast_to<Script>(p_object)) {
|
|
Script *p_script = Object::cast_to<Script>(p_object);
|
|
String res_path = p_script->get_path().get_slice("::", 0);
|
|
|
|
if (p_script->is_built_in() && !res_path.is_empty()) {
|
|
EditorNode::get_singleton()->load_scene_or_resource(res_path, false, false);
|
|
}
|
|
script_editor->edit(p_script);
|
|
} else if (Object::cast_to<JSON>(p_object) || Object::cast_to<TextFile>(p_object)) {
|
|
script_editor->edit(Object::cast_to<Resource>(p_object));
|
|
}
|
|
}
|
|
|
|
bool ScriptEditorPlugin::handles(Object *p_object) const {
|
|
if (Object::cast_to<Script>(p_object)) {
|
|
return true;
|
|
} else if (Object::cast_to<JSON>(p_object)) {
|
|
// This is here to stop resource files of class JSON from getting confused
|
|
// with json files and being opened in the text editor.
|
|
if (Object::cast_to<JSON>(p_object)->get_path().get_extension().to_lower() == "json") {
|
|
return true;
|
|
}
|
|
} else if (Object::cast_to<TextFile>(p_object)) {
|
|
return true;
|
|
}
|
|
|
|
return p_object->is_class("Script");
|
|
}
|
|
|
|
void ScriptEditorPlugin::make_visible(bool p_visible) {
|
|
if (p_visible) {
|
|
window_wrapper->show();
|
|
script_editor->ensure_select_current();
|
|
} else {
|
|
window_wrapper->hide();
|
|
}
|
|
}
|
|
|
|
void ScriptEditorPlugin::selected_notify() {
|
|
script_editor->ensure_select_current();
|
|
_focus_another_editor();
|
|
}
|
|
|
|
String ScriptEditorPlugin::get_unsaved_status(const String &p_for_scene) const {
|
|
const PackedStringArray unsaved_scripts = script_editor->get_unsaved_scripts();
|
|
if (unsaved_scripts.is_empty()) {
|
|
return String();
|
|
}
|
|
|
|
PackedStringArray message;
|
|
if (!p_for_scene.is_empty()) {
|
|
PackedStringArray unsaved_built_in_scripts;
|
|
|
|
const String scene_file = p_for_scene.get_file();
|
|
for (const String &E : unsaved_scripts) {
|
|
if (!E.is_resource_file() && E.contains(scene_file)) {
|
|
unsaved_built_in_scripts.append(E);
|
|
}
|
|
}
|
|
|
|
if (unsaved_built_in_scripts.is_empty()) {
|
|
return String();
|
|
} else {
|
|
message.resize(unsaved_built_in_scripts.size() + 1);
|
|
message.write[0] = TTR("There are unsaved changes in the following built-in script(s):");
|
|
|
|
int i = 1;
|
|
for (const String &E : unsaved_built_in_scripts) {
|
|
message.write[i] = E.trim_suffix("(*)");
|
|
i++;
|
|
}
|
|
return String("\n").join(message);
|
|
}
|
|
}
|
|
|
|
message.resize(unsaved_scripts.size() + 1);
|
|
message.write[0] = TTR("Save changes to the following script(s) before quitting?");
|
|
|
|
int i = 1;
|
|
for (const String &E : unsaved_scripts) {
|
|
message.write[i] = E.trim_suffix("(*)");
|
|
i++;
|
|
}
|
|
return String("\n").join(message);
|
|
}
|
|
|
|
void ScriptEditorPlugin::save_external_data() {
|
|
if (!EditorNode::get_singleton()->is_exiting()) {
|
|
script_editor->save_all_scripts();
|
|
}
|
|
}
|
|
|
|
void ScriptEditorPlugin::apply_changes() {
|
|
script_editor->apply_scripts();
|
|
}
|
|
|
|
void ScriptEditorPlugin::set_window_layout(Ref<ConfigFile> p_layout) {
|
|
script_editor->set_window_layout(p_layout);
|
|
|
|
if (EDITOR_GET("interface/multi_window/restore_windows_on_load") && window_wrapper->is_window_available() && p_layout->has_section_key("ScriptEditor", "window_rect")) {
|
|
window_wrapper->restore_window_from_saved_position(
|
|
p_layout->get_value("ScriptEditor", "window_rect", Rect2i()),
|
|
p_layout->get_value("ScriptEditor", "window_screen", -1),
|
|
p_layout->get_value("ScriptEditor", "window_screen_rect", Rect2i()));
|
|
} else {
|
|
window_wrapper->set_window_enabled(false);
|
|
}
|
|
}
|
|
|
|
void ScriptEditorPlugin::get_window_layout(Ref<ConfigFile> p_layout) {
|
|
script_editor->get_window_layout(p_layout);
|
|
|
|
if (window_wrapper->get_window_enabled()) {
|
|
p_layout->set_value("ScriptEditor", "window_rect", window_wrapper->get_window_rect());
|
|
int screen = window_wrapper->get_window_screen();
|
|
p_layout->set_value("ScriptEditor", "window_screen", screen);
|
|
p_layout->set_value("ScriptEditor", "window_screen_rect", DisplayServer::get_singleton()->screen_get_usable_rect(screen));
|
|
|
|
} else {
|
|
if (p_layout->has_section_key("ScriptEditor", "window_rect")) {
|
|
p_layout->erase_section_key("ScriptEditor", "window_rect");
|
|
}
|
|
if (p_layout->has_section_key("ScriptEditor", "window_screen")) {
|
|
p_layout->erase_section_key("ScriptEditor", "window_screen");
|
|
}
|
|
if (p_layout->has_section_key("ScriptEditor", "window_screen_rect")) {
|
|
p_layout->erase_section_key("ScriptEditor", "window_screen_rect");
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEditorPlugin::get_breakpoints(List<String> *p_breakpoints) {
|
|
script_editor->get_breakpoints(p_breakpoints);
|
|
}
|
|
|
|
void ScriptEditorPlugin::edited_scene_changed() {
|
|
script_editor->edited_scene_changed();
|
|
}
|
|
|
|
ScriptEditorPlugin::ScriptEditorPlugin() {
|
|
ED_SHORTCUT("script_editor/reopen_closed_script", TTRC("Reopen Closed Script"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::T);
|
|
ED_SHORTCUT("script_editor/clear_recent", TTRC("Clear Recent Scripts"));
|
|
ED_SHORTCUT("script_editor/replace_in_files", TTRC("Replace in Files..."), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::R);
|
|
|
|
ED_SHORTCUT("script_text_editor/convert_to_uppercase", TTRC("Uppercase"), KeyModifierMask::SHIFT | Key::F4);
|
|
ED_SHORTCUT("script_text_editor/convert_to_lowercase", TTRC("Lowercase"), KeyModifierMask::SHIFT | Key::F5);
|
|
ED_SHORTCUT("script_text_editor/capitalize", TTRC("Capitalize"), KeyModifierMask::SHIFT | Key::F6);
|
|
|
|
window_wrapper = memnew(WindowWrapper);
|
|
window_wrapper->set_margins_enabled(true);
|
|
|
|
script_editor = memnew(ScriptEditor(window_wrapper));
|
|
Ref<Shortcut> make_floating_shortcut = ED_SHORTCUT_AND_COMMAND("script_editor/make_floating", TTRC("Make Floating"));
|
|
window_wrapper->set_wrapped_control(script_editor, make_floating_shortcut);
|
|
|
|
EditorNode::get_singleton()->get_editor_main_screen()->get_control()->add_child(window_wrapper);
|
|
window_wrapper->set_v_size_flags(Control::SIZE_EXPAND_FILL);
|
|
window_wrapper->hide();
|
|
window_wrapper->connect("window_visibility_changed", callable_mp(this, &ScriptEditorPlugin::_window_visibility_changed));
|
|
|
|
ScriptServer::set_reload_scripts_on_save(EDITOR_GET("text_editor/behavior/files/auto_reload_and_parse_scripts_on_save"));
|
|
}
|