Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
godotengine
GitHub Repository: godotengine/godot
Path: blob/master/editor/script/script_editor_plugin.cpp
20860 views
1
/**************************************************************************/
2
/* script_editor_plugin.cpp */
3
/**************************************************************************/
4
/* This file is part of: */
5
/* GODOT ENGINE */
6
/* https://godotengine.org */
7
/**************************************************************************/
8
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10
/* */
11
/* Permission is hereby granted, free of charge, to any person obtaining */
12
/* a copy of this software and associated documentation files (the */
13
/* "Software"), to deal in the Software without restriction, including */
14
/* without limitation the rights to use, copy, modify, merge, publish, */
15
/* distribute, sublicense, and/or sell copies of the Software, and to */
16
/* permit persons to whom the Software is furnished to do so, subject to */
17
/* the following conditions: */
18
/* */
19
/* The above copyright notice and this permission notice shall be */
20
/* included in all copies or substantial portions of the Software. */
21
/* */
22
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29
/**************************************************************************/
30
31
#include "script_editor_plugin.h"
32
33
#include "core/config/project_settings.h"
34
#include "core/input/input.h"
35
#include "core/io/config_file.h"
36
#include "core/io/file_access.h"
37
#include "core/io/json.h"
38
#include "core/io/resource_loader.h"
39
#include "core/os/keyboard.h"
40
#include "core/os/os.h"
41
#include "core/string/fuzzy_search.h"
42
#include "core/version.h"
43
#include "editor/debugger/editor_debugger_node.h"
44
#include "editor/debugger/script_editor_debugger.h"
45
#include "editor/doc/editor_help.h"
46
#include "editor/doc/editor_help_search.h"
47
#include "editor/docks/editor_dock_manager.h"
48
#include "editor/docks/filesystem_dock.h"
49
#include "editor/docks/inspector_dock.h"
50
#include "editor/docks/signals_dock.h"
51
#include "editor/editor_interface.h"
52
#include "editor/editor_main_screen.h"
53
#include "editor/editor_node.h"
54
#include "editor/editor_string_names.h"
55
#include "editor/file_system/editor_paths.h"
56
#include "editor/gui/code_editor.h"
57
#include "editor/gui/editor_file_dialog.h"
58
#include "editor/gui/editor_toaster.h"
59
#include "editor/gui/filter_line_edit.h"
60
#include "editor/gui/window_wrapper.h"
61
#include "editor/inspector/editor_context_menu_plugin.h"
62
#include "editor/run/editor_run_bar.h"
63
#include "editor/scene/editor_scene_tabs.h"
64
#include "editor/script/find_in_files.h"
65
#include "editor/script/script_text_editor.h"
66
#include "editor/script/syntax_highlighters.h"
67
#include "editor/script/text_editor.h"
68
#include "editor/settings/editor_command_palette.h"
69
#include "editor/settings/editor_settings.h"
70
#include "editor/shader/shader_editor_plugin.h"
71
#include "editor/shader/text_shader_editor.h"
72
#include "editor/themes/editor_scale.h"
73
#include "editor/themes/editor_theme_manager.h"
74
#include "scene/gui/separator.h"
75
#include "scene/gui/tab_container.h"
76
#include "scene/gui/texture_rect.h"
77
#include "scene/main/node.h"
78
#include "scene/main/window.h"
79
#include "servers/display/display_server.h"
80
81
void ScriptEditorQuickOpen::popup_dialog(const Vector<String> &p_functions, bool p_dontclear) {
82
popup_centered_ratio(0.6);
83
if (p_dontclear) {
84
search_box->select_all();
85
} else {
86
search_box->clear();
87
}
88
search_box->grab_focus();
89
functions = p_functions;
90
_update_search();
91
}
92
93
void ScriptEditorQuickOpen::_text_changed(const String &p_newtext) {
94
_update_search();
95
}
96
97
void ScriptEditorQuickOpen::_update_search() {
98
search_options->clear();
99
TreeItem *root = search_options->create_item();
100
101
for (int i = 0; i < functions.size(); i++) {
102
String file = functions[i];
103
if ((search_box->get_text().is_empty() || file.containsn(search_box->get_text()))) {
104
TreeItem *ti = search_options->create_item(root);
105
ti->set_text(0, file);
106
if (root->get_first_child() == ti) {
107
ti->select(0);
108
}
109
}
110
}
111
112
get_ok_button()->set_disabled(root->get_first_child() == nullptr);
113
}
114
115
void ScriptEditorQuickOpen::_confirmed() {
116
TreeItem *ti = search_options->get_selected();
117
if (!ti) {
118
return;
119
}
120
int line = ti->get_text(0).get_slicec(':', 1).to_int();
121
122
emit_signal(SNAME("goto_line"), line - 1);
123
hide();
124
}
125
126
void ScriptEditorQuickOpen::_notification(int p_what) {
127
switch (p_what) {
128
case NOTIFICATION_ENTER_TREE: {
129
connect(SceneStringName(confirmed), callable_mp(this, &ScriptEditorQuickOpen::_confirmed));
130
} break;
131
132
case NOTIFICATION_EXIT_TREE: {
133
disconnect(SceneStringName(confirmed), callable_mp(this, &ScriptEditorQuickOpen::_confirmed));
134
} break;
135
}
136
}
137
138
void ScriptEditorQuickOpen::_bind_methods() {
139
ADD_SIGNAL(MethodInfo("goto_line", PropertyInfo(Variant::INT, "line")));
140
}
141
142
ScriptEditorQuickOpen::ScriptEditorQuickOpen() {
143
set_ok_button_text(TTRC("Open"));
144
get_ok_button()->set_disabled(true);
145
set_hide_on_ok(false);
146
147
VBoxContainer *vbc = memnew(VBoxContainer);
148
add_child(vbc);
149
150
search_box = memnew(FilterLineEdit);
151
vbc->add_margin_child(TTRC("Search:"), search_box);
152
search_box->connect(SceneStringName(text_changed), callable_mp(this, &ScriptEditorQuickOpen::_text_changed));
153
register_text_enter(search_box);
154
155
search_options = memnew(Tree);
156
search_box->set_forward_control(search_options);
157
search_options->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
158
search_options->set_hide_root(true);
159
search_options->set_hide_folding(true);
160
search_options->add_theme_constant_override("draw_guides", 1);
161
vbc->add_margin_child(TTRC("Matches:"), search_options, true);
162
search_options->connect("item_activated", callable_mp(this, &ScriptEditorQuickOpen::_confirmed));
163
}
164
165
/////////////////////////////////
166
167
ScriptEditor *ScriptEditor::script_editor = nullptr;
168
169
/*** SCRIPT EDITOR ******/
170
171
String ScriptEditor::_get_debug_tooltip(const String &p_text, Node *p_se) {
172
if (EDITOR_GET("text_editor/behavior/documentation/enable_tooltips")) {
173
return String();
174
}
175
176
// NOTE: See also `ScriptTextEditor::_show_symbol_tooltip()` for documentation tooltips enabled.
177
String debug_value = EditorDebuggerNode::get_singleton()->get_var_value(p_text);
178
if (!debug_value.is_empty()) {
179
constexpr int DISPLAY_LIMIT = 1024;
180
if (debug_value.size() > DISPLAY_LIMIT) {
181
debug_value = debug_value.left(DISPLAY_LIMIT) + "... " + TTR("(truncated)");
182
}
183
debug_value = TTR("Current value: ") + debug_value;
184
}
185
186
return debug_value;
187
}
188
189
void ScriptEditor::_script_created(Ref<Script> p_script) {
190
EditorNode::get_singleton()->push_item(p_script.operator->());
191
}
192
193
void ScriptEditor::_goto_script_line2(int p_line) {
194
if (TextEditorBase *current = Object::cast_to<TextEditorBase>(_get_current_editor())) {
195
current->goto_line(p_line);
196
}
197
}
198
199
void ScriptEditor::_goto_script_line(Ref<RefCounted> p_script, int p_line) {
200
Ref<Script> scr = Object::cast_to<Script>(*p_script);
201
if (scr.is_valid() && (scr->has_source_code() || scr->get_path().is_resource_file())) {
202
if (edit(p_script, p_line, 0)) {
203
EditorNode::get_singleton()->push_item(p_script.ptr());
204
205
if (TextEditorBase *current = Object::cast_to<TextEditorBase>(_get_current_editor())) {
206
current->goto_line_centered(p_line);
207
}
208
209
_save_history();
210
}
211
}
212
}
213
214
void ScriptEditor::_change_execution(Ref<RefCounted> p_script, int p_line, bool p_set) {
215
Ref<Script> scr = Object::cast_to<Script>(*p_script);
216
if (scr.is_valid() && (scr->has_source_code() || scr->get_path().is_resource_file())) {
217
for (int i = 0; i < tab_container->get_tab_count(); i++) {
218
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(tab_container->get_tab_control(i))) {
219
if ((scr.is_valid() && teb->get_edited_resource() == p_script) || teb->get_edited_resource()->get_path() == scr->get_path()) {
220
if (p_set) {
221
teb->set_executing_line(p_line);
222
} else {
223
teb->clear_executing_line();
224
}
225
}
226
}
227
}
228
}
229
}
230
231
void ScriptEditor::_set_breakpoint(Ref<RefCounted> p_script, int p_line, bool p_enabled) {
232
Ref<Script> scr = Object::cast_to<Script>(*p_script);
233
if (scr.is_valid() && (scr->has_source_code() || scr->get_path().is_resource_file())) {
234
// Update if open.
235
for (int i = 0; i < tab_container->get_tab_count(); i++) {
236
CodeEditorBase *ceb = Object::cast_to<CodeEditorBase>(tab_container->get_tab_control(i));
237
if (ceb && ceb->get_edited_resource()->get_path() == scr->get_path()) {
238
ceb->set_breakpoint(p_line, p_enabled);
239
return;
240
}
241
}
242
243
// Handle closed.
244
Dictionary state = script_editor_cache->get_value(scr->get_path(), "state");
245
Array breakpoints;
246
if (state.has("breakpoints")) {
247
breakpoints = state["breakpoints"];
248
}
249
250
if (breakpoints.has(p_line)) {
251
if (!p_enabled) {
252
breakpoints.erase(p_line);
253
}
254
} else if (p_enabled) {
255
breakpoints.push_back(p_line);
256
}
257
state["breakpoints"] = breakpoints;
258
script_editor_cache->set_value(scr->get_path(), "state", state);
259
EditorDebuggerNode::get_singleton()->set_breakpoint(scr->get_path(), p_line + 1, p_enabled);
260
}
261
}
262
263
void ScriptEditor::_clear_breakpoints() {
264
for (int i = 0; i < tab_container->get_tab_count(); i++) {
265
if (CodeEditorBase *ceb = Object::cast_to<CodeEditorBase>(tab_container->get_tab_control(i))) {
266
ceb->clear_breakpoints();
267
}
268
}
269
270
// Clear from closed scripts.
271
Vector<String> cached_editors = script_editor_cache->get_sections();
272
for (const String &E : cached_editors) {
273
Array breakpoints = _get_cached_breakpoints_for_script(E);
274
for (int breakpoint : breakpoints) {
275
EditorDebuggerNode::get_singleton()->set_breakpoint(E, (int)breakpoint + 1, false);
276
}
277
278
if (breakpoints.size() > 0) {
279
Dictionary state = script_editor_cache->get_value(E, "state");
280
state["breakpoints"] = Array();
281
script_editor_cache->set_value(E, "state", state);
282
}
283
}
284
}
285
286
Array ScriptEditor::_get_cached_breakpoints_for_script(const String &p_path) const {
287
if (!ResourceLoader::exists(p_path, "Script") || p_path.begins_with("local://") || !script_editor_cache->has_section_key(p_path, "state")) {
288
return Array();
289
}
290
291
Dictionary state = script_editor_cache->get_value(p_path, "state");
292
if (!state.has("breakpoints")) {
293
return Array();
294
}
295
return state["breakpoints"];
296
}
297
298
ScriptEditorBase *ScriptEditor::_get_current_editor() const {
299
int selected = tab_container->get_current_tab();
300
if (selected < 0 || selected >= tab_container->get_tab_count()) {
301
return nullptr;
302
}
303
304
return Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(selected));
305
}
306
307
void ScriptEditor::_update_history_arrows() {
308
script_back->set_disabled(history_pos <= 0);
309
script_forward->set_disabled(history_pos >= history.size() - 1);
310
}
311
312
void ScriptEditor::_save_history() {
313
if (history_pos >= 0 && history_pos < history.size() && history[history_pos].control == tab_container->get_current_tab_control()) {
314
Node *n = tab_container->get_current_tab_control();
315
316
if (Object::cast_to<TextEditorBase>(n)) {
317
history.write[history_pos].state = Object::cast_to<TextEditorBase>(n)->get_navigation_state();
318
}
319
if (Object::cast_to<EditorHelp>(n)) {
320
history.write[history_pos].state = Object::cast_to<EditorHelp>(n)->get_scroll();
321
}
322
}
323
324
history.resize(history_pos + 1);
325
ScriptHistory sh;
326
sh.control = tab_container->get_current_tab_control();
327
sh.state = Variant();
328
329
history.push_back(sh);
330
history_pos++;
331
332
_update_history_arrows();
333
}
334
335
void ScriptEditor::_save_previous_state(Dictionary p_state) {
336
if (lock_history) {
337
// Done as a result of a deferred call triggered by set_edit_state().
338
return;
339
}
340
341
if (history_pos >= 0 && history_pos < history.size() && history[history_pos].control == tab_container->get_current_tab_control()) {
342
Node *n = tab_container->get_current_tab_control();
343
344
if (Object::cast_to<ScriptTextEditor>(n)) {
345
history.write[history_pos].state = p_state;
346
}
347
}
348
349
history.resize(history_pos + 1);
350
ScriptHistory sh;
351
sh.control = tab_container->get_current_tab_control();
352
sh.state = Variant();
353
354
history.push_back(sh);
355
history_pos++;
356
357
_update_history_arrows();
358
}
359
360
void ScriptEditor::_go_to_tab(int p_idx) {
361
if (ScriptEditorBase *current = _get_current_editor()) {
362
if (current->is_unsaved()) {
363
current->apply_code();
364
}
365
}
366
367
Control *c = tab_container->get_tab_control(p_idx);
368
if (!c) {
369
return;
370
}
371
372
if (history_pos >= 0 && history_pos < history.size() && history[history_pos].control == tab_container->get_current_tab_control()) {
373
Node *n = tab_container->get_current_tab_control();
374
375
if (Object::cast_to<TextEditorBase>(n)) {
376
history.write[history_pos].state = Object::cast_to<TextEditorBase>(n)->get_navigation_state();
377
}
378
if (Object::cast_to<EditorHelp>(n)) {
379
history.write[history_pos].state = Object::cast_to<EditorHelp>(n)->get_scroll();
380
}
381
}
382
383
history.resize(history_pos + 1);
384
ScriptHistory sh;
385
sh.control = c;
386
sh.state = Variant();
387
388
if (!lock_history && (history.is_empty() || history[history.size() - 1].control != sh.control)) {
389
history.push_back(sh);
390
history_pos++;
391
}
392
393
tab_container->set_current_tab(p_idx);
394
395
c = tab_container->get_current_tab_control();
396
397
if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(c)) {
398
TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb);
399
if (teb && is_visible_in_tree()) {
400
teb->ensure_focus();
401
}
402
403
Ref<Script> scr = seb->get_edited_resource();
404
if (scr.is_valid()) {
405
notify_script_changed(scr);
406
}
407
408
seb->validate_script();
409
}
410
411
if (EditorHelp *eh = Object::cast_to<EditorHelp>(c)) {
412
script_name_button->set_text(eh->get_class());
413
_calculate_script_name_button_size();
414
415
if (is_visible_in_tree()) {
416
eh->set_focused();
417
}
418
}
419
420
c->set_meta("__editor_pass", ++edit_pass);
421
_update_history_arrows();
422
_update_script_colors();
423
_update_members_overview();
424
_update_help_overview();
425
_update_selected_editor_menu();
426
_update_online_doc();
427
_update_members_overview_visibility();
428
_update_help_overview_visibility();
429
}
430
431
void ScriptEditor::_add_recent_script(const String &p_path) {
432
if (p_path.is_empty()) {
433
return;
434
}
435
436
Array rc = EditorSettings::get_singleton()->get_project_metadata("recent_files", "scripts", Array());
437
if (rc.has(p_path)) {
438
rc.erase(p_path);
439
}
440
rc.push_front(p_path);
441
if (rc.size() > 10) {
442
rc.resize(10);
443
}
444
445
EditorSettings::get_singleton()->set_project_metadata("recent_files", "scripts", rc);
446
_update_recent_scripts();
447
}
448
449
void ScriptEditor::_update_recent_scripts() {
450
Array rc = EditorSettings::get_singleton()->get_project_metadata("recent_files", "scripts", Array());
451
recent_scripts->clear();
452
453
String path;
454
for (int i = 0; i < rc.size(); i++) {
455
path = rc[i];
456
recent_scripts->add_item(path.replace("res://", ""));
457
}
458
459
recent_scripts->add_separator();
460
recent_scripts->add_shortcut(ED_GET_SHORTCUT("script_editor/clear_recent"));
461
recent_scripts->set_item_auto_translate_mode(-1, AUTO_TRANSLATE_MODE_ALWAYS);
462
recent_scripts->set_item_disabled(-1, rc.is_empty());
463
464
recent_scripts->reset_size();
465
}
466
467
void ScriptEditor::_open_recent_script(int p_idx) {
468
// clear button
469
if (p_idx == recent_scripts->get_item_count() - 1) {
470
EditorSettings::get_singleton()->set_project_metadata("recent_files", "scripts", Array());
471
callable_mp(this, &ScriptEditor::_update_recent_scripts).call_deferred();
472
return;
473
}
474
475
Array rc = EditorSettings::get_singleton()->get_project_metadata("recent_files", "scripts", Array());
476
ERR_FAIL_INDEX(p_idx, rc.size());
477
478
String path = rc[p_idx];
479
// if its not on disk its a help file or deleted
480
if (FileAccess::exists(path)) {
481
if (_get_recognized_extensions().find(path.get_extension())) {
482
Ref<Resource> scr = ResourceLoader::load(path);
483
if (scr.is_valid()) {
484
edit(scr, true);
485
return;
486
}
487
}
488
489
Error err;
490
Ref<TextFile> text_file = _load_text_file(path, &err);
491
if (text_file.is_valid()) {
492
edit(text_file, true);
493
return;
494
}
495
// if it's a path then it's most likely a deleted file not help
496
} else if (path.contains("::")) {
497
// built-in script
498
String res_path = path.get_slice("::", 0);
499
EditorNode::get_singleton()->load_scene_or_resource(res_path, false, false);
500
501
Ref<Script> scr = ResourceLoader::load(path);
502
if (scr.is_valid()) {
503
edit(scr, true);
504
return;
505
}
506
} else if (!path.is_resource_file()) {
507
_help_class_open(path);
508
return;
509
}
510
511
rc.remove_at(p_idx);
512
EditorSettings::get_singleton()->set_project_metadata("recent_files", "scripts", rc);
513
_update_recent_scripts();
514
_show_error_dialog(path);
515
}
516
517
void ScriptEditor::_show_error_dialog(const String &p_path) {
518
error_dialog->set_text(vformat(TTR("Can't open '%s'. The file could have been moved or deleted."), p_path));
519
error_dialog->popup_centered();
520
}
521
522
void ScriptEditor::_close_tab(int p_idx, bool p_save, bool p_history_back) {
523
int selected = p_idx;
524
if (selected < 0 || selected >= tab_container->get_tab_count()) {
525
return;
526
}
527
528
Node *tselected = tab_container->get_tab_control(selected);
529
530
if (ScriptEditorBase *current = Object::cast_to<ScriptEditorBase>(tselected)) {
531
Ref<Resource> file = current->get_edited_resource();
532
if (p_save && file.is_valid()) {
533
// Do not try to save internal scripts, but prompt to save in-memory
534
// scripts which are not saved to disk yet (have empty path).
535
if (!file->is_built_in()) {
536
save_current_script();
537
}
538
}
539
if (file.is_valid()) {
540
if (!file->get_path().is_empty()) {
541
// Only saved scripts can be restored.
542
previous_scripts.push_back(file->get_path());
543
}
544
545
Ref<Script> scr = file;
546
if (scr.is_valid()) {
547
notify_script_close(scr);
548
}
549
}
550
}
551
552
// roll back to previous tab
553
if (p_history_back) {
554
_history_back();
555
}
556
557
//remove from history
558
history.resize(history_pos + 1);
559
560
for (int i = 0; i < history.size(); i++) {
561
if (history[i].control == tselected) {
562
history.remove_at(i);
563
i--;
564
history_pos--;
565
}
566
}
567
568
if (history_pos >= history.size()) {
569
history_pos = history.size() - 1;
570
}
571
572
int idx = tab_container->get_current_tab();
573
if (TextEditorBase *current = Object::cast_to<TextEditorBase>(tselected)) {
574
_save_editor_state(current);
575
}
576
memdelete(tselected);
577
578
if (script_close_queue.is_empty()) {
579
if (idx >= tab_container->get_tab_count()) {
580
idx = tab_container->get_tab_count() - 1;
581
}
582
if (idx >= 0) {
583
if (history_pos >= 0) {
584
idx = tab_container->get_tab_idx_from_control(history[history_pos].control);
585
}
586
_go_to_tab(idx);
587
} else {
588
_update_selected_editor_menu();
589
_update_online_doc();
590
script_name_button->set_text(String());
591
_calculate_script_name_button_size();
592
}
593
594
_update_history_arrows();
595
_update_script_names();
596
_save_layout();
597
_update_find_replace_bar();
598
}
599
}
600
601
void ScriptEditor::_close_current_tab(bool p_save, bool p_history_back) {
602
_close_tab(tab_container->get_current_tab(), p_save, p_history_back);
603
}
604
605
void ScriptEditor::_close_discard_current_tab(const String &p_str) {
606
Ref<Script> scr = _get_current_script();
607
if (scr.is_valid()) {
608
scr->reload_from_file();
609
}
610
_close_tab(tab_container->get_current_tab(), false);
611
erase_tab_confirm->hide();
612
}
613
614
void ScriptEditor::_close_docs_tab() {
615
int child_count = tab_container->get_tab_count();
616
for (int i = child_count - 1; i >= 0; i--) {
617
if (Object::cast_to<EditorHelp>(tab_container->get_tab_control(i))) {
618
_close_tab(i, true, false);
619
}
620
}
621
}
622
623
void ScriptEditor::_copy_script_path() {
624
if (ScriptEditorBase *seb = _get_current_editor()) {
625
Ref<Resource> scr = seb->get_edited_resource();
626
DisplayServer::get_singleton()->clipboard_set(scr->get_path());
627
}
628
}
629
630
void ScriptEditor::_copy_script_uid() {
631
if (ScriptEditorBase *seb = _get_current_editor()) {
632
Ref<Resource> scr = seb->get_edited_resource();
633
ResourceUID::ID uid = ResourceLoader::get_resource_uid(scr->get_path());
634
DisplayServer::get_singleton()->clipboard_set(ResourceUID::get_singleton()->id_to_text(uid));
635
}
636
}
637
638
void ScriptEditor::_close_other_tabs() {
639
int current_idx = tab_container->get_current_tab();
640
for (int i = tab_container->get_tab_count() - 1; i >= 0; i--) {
641
if (i != current_idx) {
642
script_close_queue.push_back(i);
643
}
644
}
645
_queue_close_tabs();
646
}
647
648
void ScriptEditor::_close_tabs_below() {
649
int current_idx = tab_container->get_current_tab();
650
for (int i = tab_container->get_tab_count() - 1; i > current_idx; i--) {
651
script_close_queue.push_back(i);
652
}
653
_go_to_tab(current_idx);
654
_queue_close_tabs();
655
}
656
657
void ScriptEditor::_close_all_tabs() {
658
for (int i = tab_container->get_tab_count() - 1; i >= 0; i--) {
659
script_close_queue.push_back(i);
660
}
661
_queue_close_tabs();
662
}
663
664
void ScriptEditor::_queue_close_tabs() {
665
while (!script_close_queue.is_empty()) {
666
int idx = script_close_queue.front()->get();
667
script_close_queue.pop_front();
668
669
tab_container->set_current_tab(idx);
670
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(idx));
671
if (seb) {
672
// Maybe there are unsaved changes.
673
if (seb->is_unsaved()) {
674
_ask_close_current_unsaved_tab(seb);
675
erase_tab_confirm->connect(SceneStringName(visibility_changed), callable_mp(this, &ScriptEditor::_queue_close_tabs), CONNECT_ONE_SHOT);
676
break;
677
}
678
}
679
680
_close_current_tab(false, false);
681
}
682
_update_find_replace_bar();
683
}
684
685
void ScriptEditor::_ask_close_current_unsaved_tab(ScriptEditorBase *current) {
686
erase_tab_confirm->set_text(TTR("Close and save changes?") + "\n\"" + current->get_name() + "\"");
687
erase_tab_confirm->popup_centered();
688
}
689
690
void ScriptEditor::_resave_scripts(const String &p_str) {
691
apply_scripts();
692
693
for (int i = 0; i < tab_container->get_tab_count(); i++) {
694
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
695
if (!seb) {
696
continue;
697
}
698
699
Ref<Resource> scr = seb->get_edited_resource();
700
701
if (scr->is_built_in()) {
702
continue; // Internal script, who cares.
703
}
704
705
_auto_format_text(seb);
706
707
Ref<TextFile> text_file = scr;
708
if (text_file.is_valid()) {
709
seb->apply_code();
710
_save_text_file(text_file, text_file->get_path());
711
break;
712
} else {
713
EditorNode::get_singleton()->save_resource(scr);
714
}
715
seb->tag_saved_version();
716
}
717
718
disk_changed->hide();
719
}
720
721
void ScriptEditor::_res_saved_callback(const Ref<Resource> &p_res) {
722
for (int i = 0; i < tab_container->get_tab_count(); i++) {
723
if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i))) {
724
if (seb->get_edited_resource() == p_res) {
725
seb->tag_saved_version();
726
}
727
}
728
}
729
730
if (p_res.is_valid()) {
731
// In case the Resource has built-in scripts.
732
_mark_built_in_scripts_as_saved(p_res->get_path());
733
}
734
735
_update_script_names();
736
Ref<Script> scr = p_res;
737
if (scr.is_valid()) {
738
trigger_live_script_reload(scr->get_path());
739
}
740
}
741
742
void ScriptEditor::_scene_saved_callback(const String &p_path) {
743
// If scene was saved, mark all built-in scripts from that scene as saved.
744
_mark_built_in_scripts_as_saved(p_path);
745
}
746
747
void ScriptEditor::_mark_built_in_scripts_as_saved(const String &p_parent_path) {
748
for (int i = 0; i < tab_container->get_tab_count(); i++) {
749
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
750
if (!seb) {
751
continue;
752
}
753
754
Ref<Resource> edited_res = seb->get_edited_resource();
755
if (!edited_res->is_built_in()) {
756
continue; // External script, who cares.
757
}
758
759
if (edited_res->get_path().get_slice("::", 0) != p_parent_path) {
760
continue; // Wrong scene.
761
}
762
seb->tag_saved_version();
763
764
Ref<Script> scr = edited_res;
765
if (scr.is_valid()) {
766
trigger_live_script_reload(scr->get_path());
767
clear_docs_from_script(scr);
768
scr->reload(true);
769
update_docs_from_script(scr);
770
}
771
}
772
}
773
774
void ScriptEditor::trigger_live_script_reload(const String &p_script_path) {
775
if (!script_paths_to_reload.has(p_script_path)) {
776
Ref<Script> reloaded_script = ResourceCache::get_ref(p_script_path);
777
if (reloaded_script.is_null()) {
778
reloaded_script = ResourceLoader::load(p_script_path);
779
}
780
if (reloaded_script.is_valid()) {
781
if (!reloaded_script->get_language()->validate(reloaded_script->get_source_code(), p_script_path)) {
782
// Script has errors, don't live reload.
783
return;
784
}
785
}
786
787
script_paths_to_reload.append(p_script_path);
788
}
789
if (!pending_auto_reload && auto_reload_running_scripts) {
790
callable_mp(this, &ScriptEditor::_live_auto_reload_running_scripts).call_deferred();
791
pending_auto_reload = true;
792
}
793
}
794
795
void ScriptEditor::_live_auto_reload_running_scripts() {
796
pending_auto_reload = false;
797
EditorDebuggerNode::get_singleton()->reload_scripts(script_paths_to_reload);
798
script_paths_to_reload.clear();
799
}
800
801
bool ScriptEditor::_test_script_times_on_disk(Ref<Resource> p_for_script) {
802
disk_changed_list->clear();
803
TreeItem *r = disk_changed_list->create_item();
804
805
bool need_ask = false;
806
bool need_reload = false;
807
bool use_autoreload = EDITOR_GET("text_editor/behavior/files/auto_reload_scripts_on_external_change");
808
809
for (int i = 0; i < tab_container->get_tab_count(); i++) {
810
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
811
if (!seb) {
812
continue;
813
}
814
815
Ref<Resource> edited_res = seb->get_edited_resource();
816
if (p_for_script.is_valid() && edited_res.is_valid() && p_for_script != edited_res) {
817
continue;
818
}
819
820
if (edited_res->is_built_in()) {
821
continue; // Internal script, who cares.
822
}
823
824
uint64_t last_date = seb->edited_file_data.last_modified_time;
825
uint64_t date = FileAccess::get_modified_time(seb->edited_file_data.path);
826
827
if (last_date != date) {
828
TreeItem *ti = disk_changed_list->create_item(r);
829
ti->set_text(0, seb->edited_file_data.path.get_file());
830
831
if (!use_autoreload || seb->is_unsaved()) {
832
need_ask = true;
833
}
834
need_reload = true;
835
}
836
}
837
838
if (need_reload) {
839
if (!need_ask) {
840
script_editor->reload_scripts();
841
need_reload = false;
842
} else {
843
callable_mp((Window *)disk_changed, &Window::popup_centered_ratio).call_deferred(0.3);
844
}
845
}
846
847
return need_reload;
848
}
849
850
void _import_text_editor_theme(const String &p_file) {
851
if (p_file.get_extension() != "tet") {
852
EditorToaster::get_singleton()->popup_str(TTR("Importing theme failed. File is not a text editor theme file (.tet)."), EditorToaster::SEVERITY_ERROR);
853
return;
854
}
855
const String theme_name = p_file.get_file().get_basename();
856
if (EditorSettings::is_default_text_editor_theme(theme_name.to_lower())) {
857
EditorToaster::get_singleton()->popup_str(TTR("Importing theme failed. File name cannot be 'Default', 'Custom', or 'Godot 2'."), EditorToaster::SEVERITY_ERROR);
858
return;
859
}
860
861
const String theme_dir = EditorPaths::get_singleton()->get_text_editor_themes_dir();
862
Ref<DirAccess> d = DirAccess::open(theme_dir);
863
Error err = FAILED;
864
if (d.is_valid()) {
865
err = d->copy(p_file, theme_dir.path_join(p_file.get_file()));
866
}
867
868
if (err != OK) {
869
EditorToaster::get_singleton()->popup_str(TTR("Importing theme failed. Failed to copy theme file."), EditorToaster::SEVERITY_ERROR);
870
return;
871
}
872
873
// Reload themes and switch to new theme.
874
EditorSettings::get_singleton()->update_text_editor_themes_list();
875
EditorSettings::get_singleton()->set_manually("text_editor/theme/color_theme", theme_name, true);
876
EditorSettings::get_singleton()->notify_changes();
877
}
878
879
void _save_text_editor_theme_as(const String &p_file) {
880
String file = p_file;
881
if (p_file.get_extension() != "tet") {
882
file += ".tet";
883
}
884
885
const String theme_name = file.get_file().get_basename();
886
if (EditorSettings::is_default_text_editor_theme(theme_name.to_lower())) {
887
EditorToaster::get_singleton()->popup_str(TTR("Saving theme failed. File name cannot be 'Default', 'Custom', or 'Godot 2'."), EditorToaster::SEVERITY_ERROR);
888
return;
889
}
890
891
const String theme_section = "color_theme";
892
const Ref<ConfigFile> cf = memnew(ConfigFile);
893
894
// Use the keys from the Godot 2 theme to know which settings to save.
895
HashMap<StringName, Color> text_colors = EditorSettings::get_godot2_text_editor_theme();
896
text_colors.sort();
897
for (const KeyValue<StringName, Color> &text_color : text_colors) {
898
const Color val = EditorSettings::get_singleton()->get_setting(text_color.key);
899
const String &key = text_color.key.operator String().replace("text_editor/theme/highlighting/", "");
900
cf->set_value(theme_section, key, val.to_html());
901
}
902
903
const Error err = cf->save(file);
904
if (err != OK) {
905
EditorToaster::get_singleton()->popup_str(TTR("Saving theme failed."), EditorToaster::SEVERITY_ERROR);
906
return;
907
}
908
909
// Reload themes and switch to saved theme.
910
EditorSettings::get_singleton()->update_text_editor_themes_list();
911
if (p_file.get_base_dir() == EditorPaths::get_singleton()->get_text_editor_themes_dir()) {
912
// Don't need to emit signal or notify changes as the colors are already set.
913
EditorSettings::get_singleton()->set_manually("text_editor/theme/color_theme", theme_name, false);
914
}
915
}
916
917
bool ScriptEditor::_script_exists(const String &p_path) const {
918
if (p_path.is_empty()) {
919
return false;
920
} else if (p_path.is_resource_file()) {
921
return FileAccess::exists(p_path);
922
} else {
923
return FileAccess::exists(p_path.get_slice("::", 0));
924
}
925
}
926
927
void ScriptEditor::_file_dialog_action(const String &p_file) {
928
switch (file_dialog_option) {
929
case FILE_MENU_NEW_TEXTFILE: {
930
Error err;
931
{
932
Ref<FileAccess> file = FileAccess::open(p_file, FileAccess::WRITE, &err);
933
if (err) {
934
EditorNode::get_singleton()->show_warning(TTR("Error writing TextFile:") + "\n" + p_file, TTR("Error!"));
935
break;
936
}
937
}
938
939
if (EditorFileSystem::get_singleton()) {
940
if (textfile_extensions.has(p_file.get_extension())) {
941
EditorFileSystem::get_singleton()->update_file(p_file);
942
}
943
}
944
[[fallthrough]];
945
}
946
case FILE_MENU_OPEN: {
947
if (!is_visible_in_tree()) {
948
// When created from outside the editor.
949
EditorNode::get_singleton()->get_editor_main_screen()->select(EditorMainScreen::EDITOR_SCRIPT);
950
}
951
open_file(p_file);
952
} break;
953
case FILE_MENU_SAVE_AS: {
954
if (ScriptEditorBase *current = _get_current_editor()) {
955
Ref<Resource> resource = current->get_edited_resource();
956
String path = ProjectSettings::get_singleton()->localize_path(p_file);
957
Error err = _save_text_file(resource, path);
958
959
if (err != OK) {
960
EditorNode::get_singleton()->show_accept(TTR("Error saving file!"), TTR("OK"));
961
return;
962
}
963
964
resource->set_path(path);
965
_update_script_names();
966
}
967
} break;
968
case THEME_SAVE_AS: {
969
_save_text_editor_theme_as(p_file);
970
} break;
971
case THEME_IMPORT: {
972
_import_text_editor_theme(p_file);
973
} break;
974
}
975
file_dialog_option = -1;
976
}
977
978
Ref<Script> ScriptEditor::_get_current_script() {
979
if (ScriptEditorBase *current = _get_current_editor()) {
980
Ref<Script> scr = current->get_edited_resource();
981
return scr.is_valid() ? scr : nullptr;
982
} else {
983
return nullptr;
984
}
985
}
986
987
TypedArray<Script> ScriptEditor::_get_open_scripts() const {
988
TypedArray<Script> ret;
989
Vector<Ref<Script>> scripts = get_open_scripts();
990
int scripts_amount = scripts.size();
991
for (int idx_script = 0; idx_script < scripts_amount; idx_script++) {
992
ret.push_back(scripts[idx_script]);
993
}
994
return ret;
995
}
996
997
bool ScriptEditor::toggle_files_panel() {
998
list_split->set_visible(!list_split->is_visible());
999
EditorSettings::get_singleton()->set_project_metadata("files_panel", "show_files_panel", list_split->is_visible());
1000
return list_split->is_visible();
1001
}
1002
1003
bool ScriptEditor::is_files_panel_toggled() {
1004
return list_split->is_visible();
1005
}
1006
1007
List<String> ScriptEditor::_get_recognized_extensions() {
1008
List<String> extensions;
1009
for (const String type : { "Script", "JSON" }) {
1010
ResourceLoader::get_recognized_extensions_for_type(type, &extensions);
1011
}
1012
return extensions;
1013
}
1014
1015
void ScriptEditor::_menu_option(int p_option) {
1016
ScriptEditorBase *current = _get_current_editor();
1017
switch (p_option) {
1018
case FILE_MENU_NEW_SCRIPT: {
1019
script_create_dialog->config("Node", "new_script", false, false);
1020
script_create_dialog->popup_centered();
1021
} break;
1022
case FILE_MENU_NEW_TEXTFILE: {
1023
file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE);
1024
file_dialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
1025
file_dialog_option = FILE_MENU_NEW_TEXTFILE;
1026
1027
file_dialog->clear_filters();
1028
for (const String &E : textfile_extensions) {
1029
file_dialog->add_filter("*." + E, E.to_upper());
1030
}
1031
file_dialog->set_title(TTRC("New Text File..."));
1032
file_dialog->popup_file_dialog();
1033
} break;
1034
case FILE_MENU_OPEN: {
1035
file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE);
1036
file_dialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
1037
file_dialog_option = FILE_MENU_OPEN;
1038
1039
List<String> extensions;
1040
ResourceLoader::get_recognized_extensions_for_type("Script", &extensions);
1041
file_dialog->clear_filters();
1042
for (const String &extension : extensions) {
1043
file_dialog->add_filter("*." + extension, extension.to_upper());
1044
}
1045
1046
for (const String &E : textfile_extensions) {
1047
file_dialog->add_filter("*." + E, E.to_upper());
1048
}
1049
1050
file_dialog->set_title(TTRC("Open File"));
1051
file_dialog->popup_file_dialog();
1052
return;
1053
} break;
1054
case FILE_MENU_REOPEN_CLOSED: {
1055
if (previous_scripts.is_empty()) {
1056
return;
1057
}
1058
1059
String path = previous_scripts.back()->get();
1060
previous_scripts.pop_back();
1061
1062
bool built_in = !path.is_resource_file();
1063
1064
if (_get_recognized_extensions().find(path.get_extension()) || built_in) {
1065
if (built_in) {
1066
String res_path = path.get_slice("::", 0);
1067
EditorNode::get_singleton()->load_scene_or_resource(res_path, false, false);
1068
}
1069
1070
Ref<Resource> scr = ResourceLoader::load(path);
1071
if (scr.is_null()) {
1072
EditorNode::get_singleton()->show_warning(TTR("Could not load file at:") + "\n\n" + path, TTR("Error!"));
1073
file_dialog_option = -1;
1074
return;
1075
}
1076
1077
edit(scr);
1078
file_dialog_option = -1;
1079
} else {
1080
Error error;
1081
Ref<TextFile> text_file = _load_text_file(path, &error);
1082
if (error != OK) {
1083
EditorNode::get_singleton()->show_warning(TTR("Could not load file at:") + "\n\n" + path, TTR("Error!"));
1084
}
1085
1086
if (text_file.is_valid()) {
1087
edit(text_file);
1088
file_dialog_option = -1;
1089
}
1090
}
1091
} break;
1092
case FILE_MENU_SAVE_ALL: {
1093
if (_test_script_times_on_disk()) {
1094
return;
1095
}
1096
1097
save_all_scripts();
1098
} break;
1099
case SEARCH_IN_FILES: {
1100
open_find_in_files_dialog("");
1101
} break;
1102
case REPLACE_IN_FILES: {
1103
_on_replace_in_files_requested("");
1104
} break;
1105
case SEARCH_HELP: {
1106
help_search_dialog->popup_dialog();
1107
} break;
1108
case SEARCH_WEBSITE: {
1109
Control *tab = tab_container->get_current_tab_control();
1110
1111
EditorHelp *eh = Object::cast_to<EditorHelp>(tab);
1112
bool native_class_doc = false;
1113
if (eh) {
1114
const HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(eh->get_class());
1115
native_class_doc = E && !E->value.is_script_doc;
1116
}
1117
if (native_class_doc) {
1118
String name = eh->get_class().to_lower();
1119
String doc_url = vformat(GODOT_VERSION_DOCS_URL "/classes/class_%s.html", name);
1120
OS::get_singleton()->shell_open(doc_url);
1121
} else {
1122
OS::get_singleton()->shell_open(GODOT_VERSION_DOCS_URL "/");
1123
}
1124
} break;
1125
case FILE_MENU_HISTORY_NEXT: {
1126
_history_forward();
1127
} break;
1128
case FILE_MENU_HISTORY_PREV: {
1129
_history_back();
1130
} break;
1131
case FILE_MENU_SORT: {
1132
_sort_list_on_update = true;
1133
_update_script_names();
1134
} break;
1135
case FILE_MENU_TOGGLE_FILES_PANEL: {
1136
toggle_files_panel();
1137
if (current) {
1138
current->update_toggle_files_button();
1139
} else {
1140
Control *tab = tab_container->get_current_tab_control();
1141
if (EditorHelp *editor_help = Object::cast_to<EditorHelp>(tab)) {
1142
editor_help->update_toggle_files_button();
1143
}
1144
}
1145
}
1146
}
1147
1148
if (p_option >= EditorContextMenuPlugin::BASE_ID) {
1149
Ref<Resource> resource;
1150
if (current) {
1151
resource = current->get_edited_resource();
1152
}
1153
EditorContextMenuPluginManager::get_singleton()->activate_custom_option(EditorContextMenuPlugin::CONTEXT_SLOT_SCRIPT_EDITOR, p_option, resource);
1154
return;
1155
}
1156
1157
if (current) {
1158
switch (p_option) {
1159
case FILE_MENU_SAVE: {
1160
save_current_script();
1161
} break;
1162
case FILE_MENU_SAVE_AS: {
1163
_auto_format_text(current);
1164
1165
Ref<Resource> resource = current->get_edited_resource();
1166
Ref<TextFile> text_file = resource;
1167
Ref<Script> scr = resource;
1168
1169
if (text_file.is_valid()) {
1170
file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE);
1171
file_dialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
1172
file_dialog_option = FILE_MENU_SAVE_AS;
1173
1174
List<String> extensions;
1175
ResourceLoader::get_recognized_extensions_for_type("Script", &extensions);
1176
file_dialog->clear_filters();
1177
file_dialog->set_current_dir(text_file->get_path().get_base_dir());
1178
file_dialog->set_current_file(text_file->get_path().get_file());
1179
file_dialog->set_title(TTRC("Save File As..."));
1180
file_dialog->popup_file_dialog();
1181
break;
1182
}
1183
1184
if (scr.is_valid()) {
1185
clear_docs_from_script(scr);
1186
}
1187
1188
EditorNode::get_singleton()->push_item(resource.ptr());
1189
EditorNode::get_singleton()->save_resource_as(resource);
1190
1191
if (scr.is_valid()) {
1192
update_docs_from_script(scr);
1193
}
1194
} break;
1195
1196
case FILE_MENU_SOFT_RELOAD_TOOL: {
1197
Ref<Script> scr = current->get_edited_resource();
1198
if (scr.is_null()) {
1199
EditorNode::get_singleton()->show_warning(TTR("Can't obtain the script for reloading."));
1200
break;
1201
}
1202
if (!scr->is_tool()) {
1203
EditorNode::get_singleton()->show_warning(TTR("Reload only takes effect on tool scripts."));
1204
return;
1205
}
1206
scr->reload(true);
1207
1208
} break;
1209
1210
case FILE_MENU_RUN: {
1211
Ref<Script> scr = current->get_edited_resource();
1212
if (scr.is_null()) {
1213
EditorToaster::get_singleton()->popup_str(TTR("Cannot run the edited file because it's not a script."), EditorToaster::SEVERITY_WARNING);
1214
break;
1215
}
1216
1217
current->apply_code();
1218
1219
EditorNode::get_singleton()->run_editor_script(scr);
1220
} break;
1221
1222
case FILE_MENU_CLOSE: {
1223
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(current);
1224
if (seb && seb->is_unsaved()) {
1225
_ask_close_current_unsaved_tab(seb);
1226
} else {
1227
_close_current_tab(false);
1228
}
1229
} break;
1230
case FILE_MENU_COPY_PATH: {
1231
_copy_script_path();
1232
} break;
1233
case FILE_MENU_COPY_UID: {
1234
_copy_script_uid();
1235
} break;
1236
case FILE_MENU_SHOW_IN_FILE_SYSTEM: {
1237
const Ref<Resource> scr = current->get_edited_resource();
1238
String path = scr->get_path();
1239
if (!path.is_empty()) {
1240
if (scr->is_built_in()) {
1241
path = path.get_slice("::", 0); // Show the scene instead.
1242
}
1243
1244
FileSystemDock::get_singleton()->navigate_to_path(path);
1245
}
1246
} break;
1247
case FILE_MENU_CLOSE_DOCS: {
1248
_close_docs_tab();
1249
} break;
1250
case FILE_MENU_CLOSE_OTHER_TABS: {
1251
_close_other_tabs();
1252
} break;
1253
case FILE_MENU_CLOSE_TABS_BELOW: {
1254
_close_tabs_below();
1255
} break;
1256
case FILE_MENU_CLOSE_ALL: {
1257
_close_all_tabs();
1258
} break;
1259
case FILE_MENU_MOVE_UP: {
1260
if (tab_container->get_current_tab() > 0) {
1261
tab_container->move_child(current, tab_container->get_current_tab() - 1);
1262
tab_container->set_current_tab(tab_container->get_current_tab());
1263
_update_script_names();
1264
}
1265
} break;
1266
case FILE_MENU_MOVE_DOWN: {
1267
if (tab_container->get_current_tab() < tab_container->get_tab_count() - 1) {
1268
tab_container->move_child(current, tab_container->get_current_tab() + 1);
1269
tab_container->set_current_tab(tab_container->get_current_tab());
1270
_update_script_names();
1271
}
1272
} break;
1273
}
1274
} else {
1275
if (EditorHelp *help = Object::cast_to<EditorHelp>(tab_container->get_current_tab_control())) {
1276
switch (p_option) {
1277
case HELP_SEARCH_FIND: {
1278
help->popup_search();
1279
} break;
1280
case HELP_SEARCH_FIND_NEXT: {
1281
help->search_again();
1282
} break;
1283
case HELP_SEARCH_FIND_PREVIOUS: {
1284
help->search_again(true);
1285
} break;
1286
case FILE_MENU_CLOSE: {
1287
_close_current_tab();
1288
} break;
1289
case FILE_MENU_CLOSE_DOCS: {
1290
_close_docs_tab();
1291
} break;
1292
case FILE_MENU_CLOSE_OTHER_TABS: {
1293
_close_other_tabs();
1294
} break;
1295
case FILE_MENU_CLOSE_TABS_BELOW: {
1296
_close_tabs_below();
1297
} break;
1298
case FILE_MENU_CLOSE_ALL: {
1299
_close_all_tabs();
1300
} break;
1301
case FILE_MENU_MOVE_UP: {
1302
if (tab_container->get_current_tab() > 0) {
1303
tab_container->move_child(help, tab_container->get_current_tab() - 1);
1304
tab_container->set_current_tab(tab_container->get_current_tab());
1305
_update_script_names();
1306
}
1307
} break;
1308
case FILE_MENU_MOVE_DOWN: {
1309
if (tab_container->get_current_tab() < tab_container->get_tab_count() - 1) {
1310
tab_container->move_child(help, tab_container->get_current_tab() + 1);
1311
tab_container->set_current_tab(tab_container->get_current_tab());
1312
_update_script_names();
1313
}
1314
} break;
1315
}
1316
}
1317
}
1318
}
1319
1320
void ScriptEditor::_theme_option(int p_option) {
1321
switch (p_option) {
1322
case THEME_IMPORT: {
1323
file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE);
1324
file_dialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
1325
file_dialog_option = THEME_IMPORT;
1326
file_dialog->clear_filters();
1327
file_dialog->add_filter("*.tet");
1328
file_dialog->set_title(TTRC("Import Theme"));
1329
file_dialog->popup_file_dialog();
1330
} break;
1331
case THEME_RELOAD: {
1332
EditorSettings::get_singleton()->mark_setting_changed("text_editor/theme/color_theme");
1333
EditorSettings::get_singleton()->notify_changes();
1334
} break;
1335
case THEME_SAVE_AS: {
1336
ScriptEditor::_show_save_theme_as_dialog();
1337
} break;
1338
}
1339
}
1340
1341
void ScriptEditor::_show_save_theme_as_dialog() {
1342
file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE);
1343
file_dialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
1344
file_dialog_option = THEME_SAVE_AS;
1345
file_dialog->clear_filters();
1346
file_dialog->add_filter("*.tet");
1347
file_dialog->set_current_path(EditorPaths::get_singleton()->get_text_editor_themes_dir().path_join(EDITOR_GET("text_editor/theme/color_theme")) + " New");
1348
file_dialog->set_title(TTRC("Save Theme As..."));
1349
file_dialog->popup_file_dialog();
1350
}
1351
1352
bool ScriptEditor::_has_docs_tab() const {
1353
const int child_count = tab_container->get_tab_count();
1354
for (int i = 0; i < child_count; i++) {
1355
if (Object::cast_to<EditorHelp>(tab_container->get_tab_control(i))) {
1356
return true;
1357
}
1358
}
1359
return false;
1360
}
1361
1362
bool ScriptEditor::_has_script_tab() const {
1363
const int child_count = tab_container->get_tab_count();
1364
for (int i = 0; i < child_count; i++) {
1365
if (Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i))) {
1366
return true;
1367
}
1368
}
1369
return false;
1370
}
1371
1372
void ScriptEditor::_prepare_file_menu() {
1373
PopupMenu *menu = file_menu->get_popup();
1374
ScriptEditorBase *editor = _get_current_editor();
1375
const Ref<Resource> res = editor ? editor->get_edited_resource() : Ref<Resource>();
1376
1377
menu->set_item_disabled(menu->get_item_index(FILE_MENU_REOPEN_CLOSED), previous_scripts.is_empty());
1378
1379
menu->set_item_disabled(menu->get_item_index(FILE_MENU_SAVE), res.is_null());
1380
menu->set_item_disabled(menu->get_item_index(FILE_MENU_SAVE_AS), res.is_null());
1381
menu->set_item_disabled(menu->get_item_index(FILE_MENU_SAVE_ALL), !_has_script_tab());
1382
1383
menu->set_item_disabled(menu->get_item_index(FILE_MENU_SOFT_RELOAD_TOOL), res.is_null());
1384
menu->set_item_disabled(menu->get_item_index(FILE_MENU_COPY_PATH), res.is_null() || res->get_path().is_empty());
1385
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);
1386
menu->set_item_disabled(menu->get_item_index(FILE_MENU_SHOW_IN_FILE_SYSTEM), res.is_null());
1387
1388
menu->set_item_disabled(menu->get_item_index(FILE_MENU_HISTORY_PREV), history_pos <= 0);
1389
menu->set_item_disabled(menu->get_item_index(FILE_MENU_HISTORY_NEXT), history_pos >= history.size() - 1);
1390
1391
menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE), tab_container->get_tab_count() < 1);
1392
menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE_ALL), tab_container->get_tab_count() < 1);
1393
menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE_OTHER_TABS), tab_container->get_tab_count() <= 1);
1394
menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE_TABS_BELOW), tab_container->get_current_tab() >= tab_container->get_tab_count() - 1);
1395
menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE_DOCS), !_has_docs_tab());
1396
1397
menu->set_item_disabled(menu->get_item_index(FILE_MENU_RUN), res.is_null());
1398
}
1399
1400
void ScriptEditor::_file_menu_closed() {
1401
PopupMenu *menu = file_menu->get_popup();
1402
1403
menu->set_item_disabled(menu->get_item_index(FILE_MENU_REOPEN_CLOSED), false);
1404
1405
menu->set_item_disabled(menu->get_item_index(FILE_MENU_SAVE), false);
1406
menu->set_item_disabled(menu->get_item_index(FILE_MENU_SAVE_AS), false);
1407
menu->set_item_disabled(menu->get_item_index(FILE_MENU_SAVE_ALL), false);
1408
1409
menu->set_item_disabled(menu->get_item_index(FILE_MENU_SOFT_RELOAD_TOOL), false);
1410
menu->set_item_disabled(menu->get_item_index(FILE_MENU_COPY_PATH), false);
1411
menu->set_item_disabled(menu->get_item_index(FILE_MENU_SHOW_IN_FILE_SYSTEM), false);
1412
1413
menu->set_item_disabled(menu->get_item_index(FILE_MENU_HISTORY_PREV), false);
1414
menu->set_item_disabled(menu->get_item_index(FILE_MENU_HISTORY_NEXT), false);
1415
1416
menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE), false);
1417
menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE_ALL), false);
1418
menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE_OTHER_TABS), false);
1419
menu->set_item_disabled(menu->get_item_index(FILE_MENU_CLOSE_DOCS), false);
1420
1421
menu->set_item_disabled(menu->get_item_index(FILE_MENU_RUN), false);
1422
}
1423
1424
void ScriptEditor::_tab_changed(int p_which) {
1425
ensure_select_current();
1426
}
1427
1428
void ScriptEditor::_notification(int p_what) {
1429
switch (p_what) {
1430
case NOTIFICATION_ENTER_TREE: {
1431
_apply_editor_settings();
1432
[[fallthrough]];
1433
}
1434
1435
case NOTIFICATION_TRANSLATION_CHANGED: {
1436
_update_online_doc();
1437
if (!make_floating->is_disabled()) {
1438
// Override default ScreenSelect tooltip if multi-window support is available.
1439
make_floating->set_tooltip_text(TTR("Make the script editor floating.") + "\n" + TTR("Right-click to open the screen selector."));
1440
}
1441
[[fallthrough]];
1442
}
1443
case NOTIFICATION_LAYOUT_DIRECTION_CHANGED:
1444
case NOTIFICATION_THEME_CHANGED: {
1445
tab_container->add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SNAME("ScriptEditor"), EditorStringName(EditorStyles)));
1446
1447
_calculate_script_name_button_size();
1448
1449
help_search->set_button_icon(get_editor_theme_icon(SNAME("HelpSearch")));
1450
site_search->set_button_icon(get_editor_theme_icon(SNAME("ExternalLink")));
1451
1452
if (is_layout_rtl()) {
1453
script_forward->set_button_icon(get_editor_theme_icon(SNAME("Back")));
1454
script_back->set_button_icon(get_editor_theme_icon(SNAME("Forward")));
1455
} else {
1456
script_forward->set_button_icon(get_editor_theme_icon(SNAME("Forward")));
1457
script_back->set_button_icon(get_editor_theme_icon(SNAME("Back")));
1458
}
1459
1460
members_overview_alphabeta_sort_button->set_button_icon(get_editor_theme_icon(SNAME("Sort")));
1461
1462
filter_scripts->set_right_icon(get_editor_theme_icon(SNAME("Search")));
1463
filter_methods->set_right_icon(get_editor_theme_icon(SNAME("Search")));
1464
1465
recent_scripts->reset_size();
1466
script_list->set_fixed_icon_size(Vector2i(1, 1) * get_theme_constant("class_icon_size", EditorStringName(Editor)));
1467
1468
if (is_inside_tree()) {
1469
_update_script_names();
1470
}
1471
} break;
1472
1473
case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
1474
if (EditorThemeManager::is_generated_theme_outdated() ||
1475
EditorSettings::get_singleton()->check_changed_settings_in_group("interface/editor") ||
1476
EditorSettings::get_singleton()->check_changed_settings_in_group("text_editor") ||
1477
EditorSettings::get_singleton()->check_changed_settings_in_group("docks/filesystem")) {
1478
_apply_editor_settings();
1479
}
1480
} break;
1481
1482
case NOTIFICATION_READY: {
1483
// Can't set own styles in NOTIFICATION_THEME_CHANGED, so for now this will do.
1484
add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SNAME("ScriptEditorPanel"), EditorStringName(EditorStyles)));
1485
1486
get_tree()->connect("tree_changed", callable_mp(this, &ScriptEditor::_tree_changed));
1487
InspectorDock::get_singleton()->connect("request_help", callable_mp(this, &ScriptEditor::_help_class_open));
1488
EditorNode::get_singleton()->connect("request_help_search", callable_mp(this, &ScriptEditor::_help_search));
1489
EditorNode::get_singleton()->connect("scene_closed", callable_mp(this, &ScriptEditor::_close_builtin_scripts_from_scene));
1490
EditorNode::get_singleton()->connect("script_add_function_request", callable_mp(this, &ScriptEditor::_add_callback));
1491
EditorNode::get_singleton()->connect("resource_saved", callable_mp(this, &ScriptEditor::_res_saved_callback));
1492
EditorNode::get_singleton()->connect("scene_saved", callable_mp(this, &ScriptEditor::_scene_saved_callback));
1493
FileSystemDock::get_singleton()->connect("files_moved", callable_mp(this, &ScriptEditor::_files_moved));
1494
FileSystemDock::get_singleton()->connect("file_removed", callable_mp(this, &ScriptEditor::_file_removed));
1495
script_list->connect(SceneStringName(item_selected), callable_mp(this, &ScriptEditor::_script_selected));
1496
1497
members_overview->connect(SceneStringName(item_selected), callable_mp(this, &ScriptEditor::_members_overview_selected));
1498
help_overview->connect(SceneStringName(item_selected), callable_mp(this, &ScriptEditor::_help_overview_selected));
1499
script_split->connect("dragged", callable_mp(this, &ScriptEditor::_split_dragged));
1500
list_split->connect("dragged", callable_mp(this, &ScriptEditor::_split_dragged));
1501
1502
EditorFileSystem::get_singleton()->connect("filesystem_changed", callable_mp(this, &ScriptEditor::_filesystem_changed));
1503
#ifdef ANDROID_ENABLED
1504
set_process(true);
1505
#endif
1506
} break;
1507
1508
#ifdef ANDROID_ENABLED
1509
case NOTIFICATION_VISIBILITY_CHANGED: {
1510
set_process(is_visible_in_tree());
1511
} break;
1512
1513
case NOTIFICATION_PROCESS: {
1514
const int kb_height = DisplayServer::get_singleton()->virtual_keyboard_get_height();
1515
if (kb_height == last_kb_height) {
1516
break;
1517
}
1518
1519
last_kb_height = kb_height;
1520
float spacer_height = 0.0f;
1521
const float status_bar_height = 28 * EDSCALE; // Magic number
1522
const bool kb_visible = kb_height > 0;
1523
1524
if (kb_visible) {
1525
if (TextEditorBase *editor = Object::cast_to<TextEditorBase>(_get_current_editor())) {
1526
if (CodeTextEditor *code_editor = editor->get_code_editor()) {
1527
if (CodeEdit *text_editor = code_editor->get_text_editor()) {
1528
if (!text_editor->has_focus()) {
1529
break;
1530
}
1531
text_editor->adjust_viewport_to_caret();
1532
}
1533
}
1534
}
1535
1536
const float control_bottom = get_global_position().y + get_size().y;
1537
const float extra_bottom = get_viewport_rect().size.y - control_bottom;
1538
spacer_height = float(kb_height) - extra_bottom - status_bar_height;
1539
1540
if (spacer_height < 0.0f) {
1541
spacer_height = 0.0f;
1542
}
1543
}
1544
1545
virtual_keyboard_spacer->set_custom_minimum_size(Size2(0, spacer_height));
1546
EditorSceneTabs::get_singleton()->set_visible(!kb_height);
1547
menu_hb->set_visible(!kb_visible);
1548
} break;
1549
#endif
1550
case NOTIFICATION_APPLICATION_FOCUS_IN: {
1551
if (is_inside_tree()) {
1552
_test_script_times_on_disk();
1553
_update_modified_scripts_for_external_editor();
1554
}
1555
} break;
1556
}
1557
}
1558
1559
void ScriptEditor::_close_builtin_scripts_from_scene(const String &p_scene) {
1560
for (int i = 0; i < tab_container->get_tab_count(); i++) {
1561
if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i))) {
1562
Ref<Script> scr = seb->get_edited_resource();
1563
if (scr.is_null()) {
1564
continue;
1565
}
1566
1567
if (scr->is_built_in() && scr->get_path().begins_with(p_scene)) { // Is an internal script and belongs to scene being closed.
1568
_close_tab(i, false);
1569
i--;
1570
}
1571
}
1572
}
1573
}
1574
1575
void ScriptEditor::edited_scene_changed() {
1576
_update_modified_scripts_for_external_editor();
1577
}
1578
1579
void ScriptEditor::notify_script_close(const Ref<Script> &p_script) {
1580
emit_signal(SNAME("script_close"), p_script);
1581
}
1582
1583
void ScriptEditor::notify_script_changed(const Ref<Script> &p_script) {
1584
emit_signal(SNAME("editor_script_changed"), p_script);
1585
}
1586
1587
Vector<String> ScriptEditor::_get_breakpoints() {
1588
List<String> bpoints_list;
1589
get_breakpoints(&bpoints_list);
1590
1591
Vector<String> ret;
1592
for (const String &E : bpoints_list) {
1593
ret.push_back(E);
1594
}
1595
1596
return ret;
1597
}
1598
1599
void ScriptEditor::get_breakpoints(List<String> *p_breakpoints) {
1600
HashSet<String> loaded_scripts;
1601
for (int i = 0; i < tab_container->get_tab_count(); i++) {
1602
CodeEditorBase *ceb = Object::cast_to<CodeEditorBase>(tab_container->get_tab_control(i));
1603
if (!ceb) {
1604
continue;
1605
}
1606
1607
Ref<Script> scr = ceb->get_edited_resource();
1608
if (scr.is_null()) {
1609
continue;
1610
}
1611
1612
String base = scr->get_path();
1613
loaded_scripts.insert(base);
1614
if (base.is_empty() || base.begins_with("local://")) {
1615
continue;
1616
}
1617
1618
PackedInt32Array bpoints = ceb->get_breakpoints();
1619
for (int32_t bpoint : bpoints) {
1620
p_breakpoints->push_back(base + ":" + itos((int)bpoint + 1));
1621
}
1622
}
1623
1624
// Load breakpoints that are in closed scripts.
1625
Vector<String> cached_editors = script_editor_cache->get_sections();
1626
for (const String &E : cached_editors) {
1627
if (loaded_scripts.has(E)) {
1628
continue;
1629
}
1630
1631
Array breakpoints = _get_cached_breakpoints_for_script(E);
1632
for (int breakpoint : breakpoints) {
1633
p_breakpoints->push_back(E + ":" + itos((int)breakpoint + 1));
1634
}
1635
}
1636
}
1637
1638
void ScriptEditor::_members_overview_selected(int p_idx) {
1639
int line = members_overview->get_item_metadata(p_idx);
1640
ScriptEditorBase *current = _get_current_editor();
1641
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(current)) {
1642
teb->goto_line_centered(line);
1643
}
1644
}
1645
1646
void ScriptEditor::_help_overview_selected(int p_idx) {
1647
Node *current = tab_container->get_tab_control(tab_container->get_current_tab());
1648
if (EditorHelp *eh = Object::cast_to<EditorHelp>(current)) {
1649
eh->scroll_to_section(help_overview->get_item_metadata(p_idx));
1650
}
1651
}
1652
1653
void ScriptEditor::_script_selected(int p_idx) {
1654
grab_focus_block = !Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT); //amazing hack, simply amazing
1655
1656
_go_to_tab(script_list->get_item_metadata(p_idx));
1657
script_name_button->set_text(script_list->get_item_text(p_idx));
1658
_calculate_script_name_button_size();
1659
grab_focus_block = false;
1660
}
1661
1662
void ScriptEditor::ensure_select_current() {
1663
if (tab_container->get_tab_count() && tab_container->get_current_tab() >= 0) {
1664
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(_get_current_editor())) {
1665
teb->enable_editor();
1666
if (teb && !grab_focus_block && is_visible_in_tree()) {
1667
teb->ensure_focus();
1668
}
1669
}
1670
}
1671
_update_find_replace_bar();
1672
1673
_update_selected_editor_menu();
1674
}
1675
1676
bool ScriptEditor::is_editor_floating() {
1677
return is_floating;
1678
}
1679
1680
void ScriptEditor::_find_scripts(Node *p_base, Node *p_current, HashSet<Ref<Script>> &used) {
1681
if (p_current != p_base && p_current->get_owner() != p_base) {
1682
return;
1683
}
1684
1685
if (p_current->get_script_instance()) {
1686
Ref<Script> scr = p_current->get_script();
1687
if (scr.is_valid()) {
1688
used.insert(scr);
1689
}
1690
}
1691
1692
for (int i = 0; i < p_current->get_child_count(); i++) {
1693
_find_scripts(p_base, p_current->get_child(i), used);
1694
}
1695
}
1696
1697
struct _ScriptEditorItemData {
1698
String name;
1699
String sort_key;
1700
Ref<Texture2D> icon;
1701
bool tool = false;
1702
int index = 0;
1703
String tooltip;
1704
bool used = false;
1705
int category = 0;
1706
Node *ref = nullptr;
1707
1708
bool operator<(const _ScriptEditorItemData &id) const {
1709
if (category == id.category) {
1710
if (sort_key == id.sort_key) {
1711
return index < id.index;
1712
} else {
1713
return sort_key.filenocasecmp_to(id.sort_key) < 0;
1714
}
1715
} else {
1716
return category < id.category;
1717
}
1718
}
1719
};
1720
1721
void ScriptEditor::_update_members_overview_visibility() {
1722
ScriptEditorBase *seb = _get_current_editor();
1723
if (!seb) {
1724
members_overview_alphabeta_sort_button->set_visible(false);
1725
members_overview->set_visible(false);
1726
1727
Node *current = tab_container->get_tab_control(tab_container->get_current_tab());
1728
EditorHelp *editor_help = Object::cast_to<EditorHelp>(current);
1729
overview_vbox->set_visible(help_overview_enabled && editor_help);
1730
return;
1731
}
1732
1733
if (members_overview_enabled && seb->show_members_overview()) {
1734
members_overview_alphabeta_sort_button->set_visible(true);
1735
filter_methods->set_visible(true);
1736
members_overview->set_visible(true);
1737
overview_vbox->set_visible(true);
1738
} else {
1739
members_overview_alphabeta_sort_button->set_visible(false);
1740
filter_methods->set_visible(false);
1741
members_overview->set_visible(false);
1742
overview_vbox->set_visible(false);
1743
}
1744
}
1745
1746
void ScriptEditor::_toggle_members_overview_alpha_sort(bool p_alphabetic_sort) {
1747
EditorSettings::get_singleton()->set("text_editor/script_list/sort_members_outline_alphabetically", p_alphabetic_sort);
1748
_update_members_overview();
1749
}
1750
1751
void ScriptEditor::_update_members_overview() {
1752
members_overview->clear();
1753
1754
CodeEditorBase *ceb = Object::cast_to<CodeEditorBase>(_get_current_editor());
1755
if (!ceb) {
1756
return;
1757
}
1758
1759
Vector<String> functions = ceb->get_functions();
1760
if (EDITOR_GET("text_editor/script_list/sort_members_outline_alphabetically")) {
1761
functions.sort();
1762
}
1763
1764
String filter = filter_methods->get_text();
1765
if (filter.is_empty()) {
1766
for (int i = 0; i < functions.size(); i++) {
1767
String name = functions[i].get_slicec(':', 0);
1768
members_overview->add_item(name);
1769
members_overview->set_item_metadata(-1, functions[i].get_slicec(':', 1).to_int() - 1);
1770
}
1771
} else {
1772
PackedStringArray search_names;
1773
for (int i = 0; i < functions.size(); i++) {
1774
search_names.append(functions[i].get_slicec(':', 0));
1775
}
1776
1777
Vector<FuzzySearchResult> results;
1778
FuzzySearch fuzzy;
1779
fuzzy.set_query(filter, false);
1780
fuzzy.search_all(search_names, results);
1781
1782
for (const FuzzySearchResult &res : results) {
1783
String name = functions[res.original_index].get_slicec(':', 0);
1784
int line = functions[res.original_index].get_slicec(':', 1).to_int() - 1;
1785
members_overview->add_item(name);
1786
members_overview->set_item_metadata(-1, line);
1787
}
1788
}
1789
}
1790
1791
void ScriptEditor::_update_help_overview_visibility() {
1792
int selected = tab_container->get_current_tab();
1793
if (selected < 0 || selected >= tab_container->get_tab_count()) {
1794
help_overview->set_visible(false);
1795
return;
1796
}
1797
1798
Node *current = tab_container->get_tab_control(tab_container->get_current_tab());
1799
if (!Object::cast_to<EditorHelp>(current)) {
1800
help_overview->set_visible(false);
1801
return;
1802
}
1803
1804
if (help_overview_enabled) {
1805
members_overview_alphabeta_sort_button->set_visible(false);
1806
filter_methods->set_visible(false);
1807
help_overview->set_visible(true);
1808
overview_vbox->set_visible(true);
1809
} else {
1810
help_overview->set_visible(false);
1811
overview_vbox->set_visible(false);
1812
}
1813
}
1814
1815
void ScriptEditor::_update_help_overview() {
1816
help_overview->clear();
1817
1818
int selected = tab_container->get_current_tab();
1819
if (selected < 0 || selected >= tab_container->get_tab_count()) {
1820
return;
1821
}
1822
1823
Node *current = tab_container->get_tab_control(tab_container->get_current_tab());
1824
if (EditorHelp *eh = Object::cast_to<EditorHelp>(current)) {
1825
Vector<Pair<String, int>> sections = eh->get_sections();
1826
for (int i = 0; i < sections.size(); i++) {
1827
help_overview->add_item(sections[i].first);
1828
help_overview->set_item_metadata(i, sections[i].second);
1829
}
1830
}
1831
}
1832
1833
void ScriptEditor::_update_online_doc() {
1834
Node *current = tab_container->get_tab_control(tab_container->get_current_tab());
1835
1836
EditorHelp *eh = Object::cast_to<EditorHelp>(current);
1837
bool native_class_doc = false;
1838
if (eh) {
1839
const HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(eh->get_class());
1840
native_class_doc = E && !E->value.is_script_doc;
1841
}
1842
if (native_class_doc) {
1843
String name = eh->get_class();
1844
String tooltip = vformat(TTR("Open '%s' in Godot online documentation."), name);
1845
site_search->set_text(TTRC("Open in Online Docs"));
1846
site_search->set_tooltip_text(tooltip);
1847
} else {
1848
site_search->set_text(TTRC("Online Docs"));
1849
site_search->set_tooltip_text(TTRC("Open Godot online documentation."));
1850
}
1851
}
1852
1853
void ScriptEditor::_update_script_colors() {
1854
bool script_temperature_enabled = EDITOR_GET("text_editor/script_list/script_temperature_enabled");
1855
1856
int hist_size = EDITOR_GET("text_editor/script_list/script_temperature_history_size");
1857
Color hot_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));
1858
hot_color.set_s(hot_color.get_s() * 0.9);
1859
Color cold_color = get_theme_color(SceneStringName(font_color), EditorStringName(Editor));
1860
1861
for (int i = 0; i < script_list->get_item_count(); i++) {
1862
int c = script_list->get_item_metadata(i);
1863
Node *n = tab_container->get_tab_control(c);
1864
if (!n) {
1865
continue;
1866
}
1867
1868
if (script_temperature_enabled) {
1869
int pass = n->get_meta("__editor_pass", -1);
1870
if (pass < 0) {
1871
continue;
1872
}
1873
1874
int h = edit_pass - pass;
1875
if (h > hist_size) {
1876
continue;
1877
}
1878
int non_zero_hist_size = (hist_size == 0) ? 1 : hist_size;
1879
float v = Math::ease((edit_pass - pass) / float(non_zero_hist_size), 0.4);
1880
1881
script_list->set_item_custom_fg_color(i, hot_color.lerp(cold_color, v));
1882
}
1883
}
1884
}
1885
1886
void ScriptEditor::_update_script_names() {
1887
if (restoring_layout) {
1888
return;
1889
}
1890
1891
HashSet<Ref<Script>> used;
1892
Node *edited = EditorNode::get_singleton()->get_edited_scene();
1893
if (edited && EDITOR_GET("text_editor/script_list/highlight_scene_scripts")) {
1894
_find_scripts(edited, edited, used);
1895
}
1896
1897
script_list->clear();
1898
bool split_script_help = EDITOR_GET("text_editor/script_list/group_help_pages");
1899
ScriptSortBy sort_by = (ScriptSortBy)(int)EDITOR_GET("text_editor/script_list/sort_scripts_by");
1900
ScriptListName display_as = (ScriptListName)(int)EDITOR_GET("text_editor/script_list/list_script_names_as");
1901
1902
Vector<_ScriptEditorItemData> sedata;
1903
1904
for (int i = 0; i < tab_container->get_tab_count(); i++) {
1905
if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i))) {
1906
Ref<Texture2D> icon = seb->get_theme_icon();
1907
String path = seb->get_edited_resource()->get_path();
1908
bool saved = !path.is_empty();
1909
String name = seb->get_name();
1910
Ref<Script> scr = seb->get_edited_resource();
1911
1912
_ScriptEditorItemData sd;
1913
sd.icon = icon;
1914
sd.name = name;
1915
sd.tooltip = saved ? path : TTR("Unsaved file.");
1916
sd.index = i;
1917
sd.used = used.has(seb->get_edited_resource());
1918
sd.category = 0;
1919
sd.ref = seb;
1920
if (scr.is_valid()) {
1921
sd.tool = scr->is_tool();
1922
}
1923
1924
switch (sort_by) {
1925
case SORT_BY_NAME: {
1926
sd.sort_key = name.to_lower();
1927
} break;
1928
case SORT_BY_PATH: {
1929
sd.sort_key = path;
1930
} break;
1931
case SORT_BY_NONE: {
1932
sd.sort_key = "";
1933
} break;
1934
}
1935
1936
switch (display_as) {
1937
case DISPLAY_NAME: {
1938
sd.name = name;
1939
} break;
1940
case DISPLAY_DIR_AND_NAME: {
1941
if (!path.get_base_dir().get_file().is_empty()) {
1942
sd.name = path.get_base_dir().get_file().path_join(name);
1943
} else {
1944
sd.name = name;
1945
}
1946
} break;
1947
case DISPLAY_FULL_PATH: {
1948
sd.name = path;
1949
} break;
1950
}
1951
if (!saved) {
1952
sd.name = seb->get_name();
1953
}
1954
1955
sedata.push_back(sd);
1956
}
1957
1958
EditorHelp *eh = Object::cast_to<EditorHelp>(tab_container->get_tab_control(i));
1959
if (eh && !eh->get_class().is_empty()) {
1960
String name = eh->get_class().unquote();
1961
Ref<Texture2D> icon = get_editor_theme_icon(SNAME("Help"));
1962
String tooltip = vformat(TTR("%s Class Reference"), name);
1963
1964
_ScriptEditorItemData sd;
1965
sd.icon = icon;
1966
sd.name = name;
1967
sd.sort_key = name.to_lower();
1968
sd.tooltip = tooltip;
1969
sd.index = i;
1970
sd.used = false;
1971
sd.category = split_script_help ? 1 : 0;
1972
sd.ref = eh;
1973
1974
sedata.push_back(sd);
1975
}
1976
}
1977
1978
Vector<String> disambiguated_script_names;
1979
Vector<String> full_script_paths;
1980
for (int j = 0; j < sedata.size(); j++) {
1981
String name = sedata[j].name.replace("(*)", "");
1982
ScriptListName script_display = (ScriptListName)(int)EDITOR_GET("text_editor/script_list/list_script_names_as");
1983
switch (script_display) {
1984
case DISPLAY_NAME: {
1985
name = name.get_file();
1986
} break;
1987
case DISPLAY_DIR_AND_NAME: {
1988
name = name.get_base_dir().get_file().path_join(name.get_file());
1989
} break;
1990
default:
1991
break;
1992
}
1993
1994
disambiguated_script_names.append(name);
1995
full_script_paths.append(sedata[j].tooltip);
1996
}
1997
1998
EditorNode::disambiguate_filenames(full_script_paths, disambiguated_script_names);
1999
2000
for (int j = 0; j < sedata.size(); j++) {
2001
if (sedata[j].name.ends_with("(*)")) {
2002
sedata.write[j].name = disambiguated_script_names[j] + "(*)";
2003
} else {
2004
sedata.write[j].name = disambiguated_script_names[j];
2005
}
2006
}
2007
2008
if (_sort_list_on_update && !sedata.is_empty()) {
2009
sedata.sort();
2010
2011
// change actual order of tab_container so that the order can be rearranged by user
2012
int cur_tab = tab_container->get_current_tab();
2013
int prev_tab = tab_container->get_previous_tab();
2014
int new_cur_tab = -1;
2015
int new_prev_tab = -1;
2016
for (int i = 0; i < sedata.size(); i++) {
2017
tab_container->move_child(sedata[i].ref, i);
2018
if (new_prev_tab == -1 && sedata[i].index == prev_tab) {
2019
new_prev_tab = i;
2020
}
2021
if (new_cur_tab == -1 && sedata[i].index == cur_tab) {
2022
new_cur_tab = i;
2023
}
2024
// Update index of sd entries for sorted order
2025
_ScriptEditorItemData sd = sedata[i];
2026
sd.index = i;
2027
sedata.set(i, sd);
2028
}
2029
2030
lock_history = true;
2031
_go_to_tab(new_prev_tab);
2032
_go_to_tab(new_cur_tab);
2033
lock_history = false;
2034
_sort_list_on_update = false;
2035
}
2036
2037
Vector<_ScriptEditorItemData> sedata_filtered;
2038
2039
String filter = filter_scripts->get_text();
2040
2041
if (filter.is_empty()) {
2042
sedata_filtered = sedata;
2043
} else {
2044
PackedStringArray search_names;
2045
for (int i = 0; i < sedata.size(); i++) {
2046
search_names.append(sedata[i].name);
2047
}
2048
2049
Vector<FuzzySearchResult> results;
2050
FuzzySearch fuzzy;
2051
fuzzy.set_query(filter, false);
2052
fuzzy.search_all(search_names, results);
2053
2054
for (const FuzzySearchResult &res : results) {
2055
sedata_filtered.push_back(sedata[res.original_index]);
2056
}
2057
}
2058
2059
Color tool_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));
2060
tool_color.set_s(tool_color.get_s() * 1.5);
2061
for (int i = 0; i < sedata_filtered.size(); i++) {
2062
script_list->add_item(sedata_filtered[i].name, sedata_filtered[i].icon);
2063
if (sedata_filtered[i].tool) {
2064
script_list->set_item_icon_modulate(-1, tool_color);
2065
}
2066
2067
int index = script_list->get_item_count() - 1;
2068
script_list->set_item_tooltip(index, sedata_filtered[i].tooltip);
2069
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 */
2070
if (sedata_filtered[i].used) {
2071
script_list->set_item_custom_bg_color(index, Color(.5, .5, .5, .125));
2072
}
2073
if (tab_container->get_current_tab() == sedata_filtered[i].index) {
2074
script_list->select(index);
2075
2076
if (ScriptEditorBase *seb = _get_current_editor()) {
2077
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb)) {
2078
teb->enable_editor();
2079
}
2080
_update_selected_editor_menu();
2081
}
2082
}
2083
}
2084
2085
for (const _ScriptEditorItemData &sedata_i : sedata) {
2086
if (tab_container->get_current_tab() == sedata_i.index) {
2087
script_name_button->set_text(sedata_i.name);
2088
_calculate_script_name_button_size();
2089
break;
2090
}
2091
}
2092
2093
if (!waiting_update_names) {
2094
_update_members_overview();
2095
_update_help_overview();
2096
} else {
2097
waiting_update_names = false;
2098
}
2099
_update_members_overview_visibility();
2100
_update_help_overview_visibility();
2101
_update_script_colors();
2102
}
2103
2104
Ref<TextFile> ScriptEditor::_load_text_file(const String &p_path, Error *r_error) const {
2105
if (r_error) {
2106
*r_error = ERR_FILE_CANT_OPEN;
2107
}
2108
2109
String local_path = ProjectSettings::get_singleton()->localize_path(p_path);
2110
String path = ResourceLoader::path_remap(local_path);
2111
2112
TextFile *text_file = memnew(TextFile);
2113
Ref<TextFile> text_res(text_file);
2114
Error err = text_file->load_text(path);
2115
2116
ERR_FAIL_COND_V_MSG(err != OK, Ref<Resource>(), "Cannot load text file '" + path + "'.");
2117
2118
text_file->set_file_path(local_path);
2119
text_file->set_path(local_path, true);
2120
2121
if (ResourceLoader::get_timestamp_on_load()) {
2122
text_file->set_last_modified_time(FileAccess::get_modified_time(path));
2123
}
2124
2125
if (r_error) {
2126
*r_error = OK;
2127
}
2128
2129
return text_res;
2130
}
2131
2132
Error ScriptEditor::_save_text_file(Ref<TextFile> p_text_file, const String &p_path) {
2133
Ref<TextFile> sqscr = p_text_file;
2134
ERR_FAIL_COND_V(sqscr.is_null(), ERR_INVALID_PARAMETER);
2135
2136
String source = sqscr->get_text();
2137
2138
Error err;
2139
{
2140
Ref<FileAccess> file = FileAccess::open(p_path, FileAccess::WRITE, &err);
2141
2142
ERR_FAIL_COND_V_MSG(err, err, "Cannot save text file '" + p_path + "'.");
2143
2144
file->store_string(source);
2145
if (file->get_error() != OK && file->get_error() != ERR_FILE_EOF) {
2146
return ERR_CANT_CREATE;
2147
}
2148
}
2149
2150
if (ResourceSaver::get_timestamp_on_save()) {
2151
p_text_file->set_last_modified_time(FileAccess::get_modified_time(p_path));
2152
}
2153
2154
EditorFileSystem::get_singleton()->update_file(p_path);
2155
2156
_res_saved_callback(sqscr);
2157
return OK;
2158
}
2159
2160
bool ScriptEditor::edit(const Ref<Resource> &p_resource, int p_line, int p_col, bool p_grab_focus) {
2161
if (p_resource.is_null()) {
2162
return false;
2163
}
2164
2165
Ref<Script> scr = p_resource;
2166
2167
// Don't open dominant script if using an external editor.
2168
bool use_external_editor =
2169
external_editor_active ||
2170
(scr.is_valid() && scr->get_language()->overrides_external_editor());
2171
use_external_editor = use_external_editor && !(scr.is_valid() && scr->is_built_in()); // Ignore external editor for built-in scripts.
2172
const bool open_dominant = EDITOR_GET("text_editor/behavior/files/open_dominant_script_on_scene_change");
2173
2174
const bool should_open = (open_dominant && !use_external_editor) || !EditorNode::get_singleton()->is_changing_scene();
2175
2176
if (scr.is_valid() && scr->get_language()->overrides_external_editor()) {
2177
if (should_open) {
2178
Error err = scr->get_language()->open_in_external_editor(scr, p_line >= 0 ? p_line : 0, p_col);
2179
if (err != OK) {
2180
ERR_PRINT("Couldn't open script in the overridden external text editor");
2181
}
2182
}
2183
return false;
2184
}
2185
2186
if (use_external_editor &&
2187
(EditorDebuggerNode::get_singleton()->get_dump_stack_script() != p_resource || EditorDebuggerNode::get_singleton()->get_debug_with_external_editor()) &&
2188
p_resource->get_path().is_resource_file()) {
2189
if (ScriptEditorPlugin::open_in_external_editor(ProjectSettings::get_singleton()->globalize_path(p_resource->get_path()), p_line, p_col)) {
2190
return false;
2191
} else {
2192
ERR_PRINT("Couldn't open external text editor, falling back to the internal editor. Review your `text_editor/external/` editor settings.");
2193
}
2194
}
2195
2196
for (int i = 0; i < tab_container->get_tab_count(); i++) {
2197
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
2198
if (!seb) {
2199
continue;
2200
}
2201
2202
if ((scr.is_valid() && seb->get_edited_resource() == p_resource) || seb->get_edited_resource()->get_path() == p_resource->get_path()) {
2203
if (should_open) {
2204
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb)) {
2205
teb->enable_editor();
2206
2207
if (tab_container->get_current_tab() != i) {
2208
_go_to_tab(i);
2209
}
2210
2211
if (is_visible_in_tree()) {
2212
teb->ensure_focus();
2213
}
2214
2215
if (p_line >= 0) {
2216
teb->goto_line(p_line, p_col);
2217
}
2218
} else if (tab_container->get_current_tab() != i) {
2219
_go_to_tab(i);
2220
}
2221
}
2222
_update_script_names();
2223
script_list->ensure_current_is_visible();
2224
return true;
2225
}
2226
}
2227
2228
// doesn't have it, make a new one
2229
ScriptEditorBase *seb = nullptr;
2230
2231
for (int i = script_editor_func_count - 1; i >= 0; i--) {
2232
seb = script_editor_funcs[i](p_resource);
2233
if (seb) {
2234
break;
2235
}
2236
}
2237
ERR_FAIL_NULL_V(seb, false);
2238
2239
seb->set_edited_resource(p_resource);
2240
2241
seb->set_toggle_list_control(get_left_list_split());
2242
tab_container->add_child(seb);
2243
2244
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb)) {
2245
if (p_grab_focus) {
2246
teb->enable_editor();
2247
}
2248
2249
Control *editor_edit_menu = teb->get_edit_menu();
2250
if (editor_edit_menu && !editor_edit_menu->get_parent()) {
2251
editor_edit_menu->hide();
2252
editor_menus.push_back(editor_edit_menu);
2253
menu_hb->add_child(editor_edit_menu);
2254
menu_hb->move_child(editor_edit_menu, 1);
2255
for (int i = 0; i < editor_edit_menu->get_child_count(); ++i) {
2256
Control *c = Object::cast_to<Control>(editor_edit_menu->get_child(i));
2257
if (c) {
2258
c->set_shortcut_context(this);
2259
}
2260
}
2261
}
2262
}
2263
2264
if (p_grab_focus) {
2265
_go_to_tab(tab_container->get_tab_count() - 1);
2266
_add_recent_script(p_resource->get_path());
2267
}
2268
2269
// If we delete a script within the filesystem, the original resource path
2270
// is lost, so keep it as `edited_file_data` to figure out the exact tab to delete.
2271
seb->edited_file_data.path = p_resource->get_path();
2272
seb->edited_file_data.last_modified_time = FileAccess::get_modified_time(p_resource->get_path());
2273
2274
seb->connect("name_changed", callable_mp(this, &ScriptEditor::_update_script_names));
2275
seb->connect("edited_script_changed", callable_mp(this, &ScriptEditor::_script_changed));
2276
2277
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb)) {
2278
// Syntax highlighting.
2279
bool highlighter_set = false;
2280
for (int i = 0; i < syntax_highlighters.size(); i++) {
2281
Ref<EditorSyntaxHighlighter> highlighter = syntax_highlighters[i]->_create();
2282
if (highlighter.is_null()) {
2283
continue;
2284
}
2285
teb->add_syntax_highlighter(highlighter);
2286
2287
if (highlighter_set) {
2288
continue;
2289
}
2290
2291
PackedStringArray languages = highlighter->_get_supported_languages();
2292
// If script try language, else use extension.
2293
if (scr.is_valid()) {
2294
if (languages.has(scr->get_language()->get_name())) {
2295
teb->set_syntax_highlighter(highlighter);
2296
highlighter_set = true;
2297
}
2298
continue;
2299
}
2300
2301
if (languages.has(p_resource->get_path().get_extension())) {
2302
teb->set_syntax_highlighter(highlighter);
2303
highlighter_set = true;
2304
}
2305
}
2306
2307
teb->set_tooltip_request_func(callable_mp(this, &ScriptEditor::_get_debug_tooltip));
2308
2309
teb->connect("request_help", callable_mp(this, &ScriptEditor::_help_search));
2310
teb->connect("request_open_script_at_line", callable_mp(this, &ScriptEditor::_goto_script_line));
2311
teb->connect("go_to_help", callable_mp(this, &ScriptEditor::_help_class_goto));
2312
teb->connect("request_save_history", callable_mp(this, &ScriptEditor::_save_history));
2313
teb->connect("request_save_previous_state", callable_mp(this, &ScriptEditor::_save_previous_state));
2314
teb->connect("search_in_files_requested", callable_mp(this, &ScriptEditor::open_find_in_files_dialog));
2315
teb->connect("replace_in_files_requested", callable_mp(this, &ScriptEditor::_on_replace_in_files_requested));
2316
teb->connect("go_to_method", callable_mp(this, &ScriptEditor::script_goto_method));
2317
2318
if (script_editor_cache->has_section(p_resource->get_path())) {
2319
teb->set_edit_state(script_editor_cache->get_value(p_resource->get_path(), "state"));
2320
if (ScriptTextEditor *ste = Object::cast_to<ScriptTextEditor>(teb)) {
2321
ste->store_previous_state();
2322
}
2323
}
2324
2325
if (CodeTextEditor *cte = teb->get_code_editor()) {
2326
cte->set_zoom_factor(zoom_factor);
2327
cte->connect("zoomed", callable_mp(this, &ScriptEditor::_set_script_zoom_factor));
2328
cte->connect(SceneStringName(visibility_changed), callable_mp(this, &ScriptEditor::_update_code_editor_zoom_factor).bind(cte));
2329
}
2330
}
2331
2332
_sort_list_on_update = true;
2333
_update_script_names();
2334
_save_layout();
2335
2336
//test for modification, maybe the script was not edited but was loaded
2337
2338
_test_script_times_on_disk(p_resource);
2339
_update_modified_scripts_for_external_editor(p_resource);
2340
2341
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb)) {
2342
if (p_line >= 0) {
2343
teb->goto_line(p_line, p_col);
2344
}
2345
}
2346
2347
notify_script_changed(p_resource);
2348
return true;
2349
}
2350
2351
PackedStringArray ScriptEditor::get_unsaved_scripts() const {
2352
PackedStringArray unsaved_list;
2353
2354
for (int i = 0; i < tab_container->get_tab_count(); i++) {
2355
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
2356
if (seb && seb->is_unsaved()) {
2357
unsaved_list.append(seb->get_name());
2358
}
2359
}
2360
return unsaved_list;
2361
}
2362
2363
void ScriptEditor::save_current_script() {
2364
ScriptEditorBase *current = _get_current_editor();
2365
if (!current || _test_script_times_on_disk()) {
2366
return;
2367
}
2368
2369
_auto_format_text(current);
2370
2371
Ref<Resource> resource = current->get_edited_resource();
2372
Ref<TextFile> text_file = resource;
2373
Ref<Script> scr = resource;
2374
2375
if (text_file.is_valid()) {
2376
current->apply_code();
2377
_save_text_file(text_file, text_file->get_path());
2378
return;
2379
}
2380
2381
if (scr.is_valid()) {
2382
clear_docs_from_script(scr);
2383
}
2384
2385
EditorNode::get_singleton()->save_resource(resource);
2386
2387
if (scr.is_valid()) {
2388
update_docs_from_script(scr);
2389
}
2390
}
2391
2392
void ScriptEditor::save_all_scripts() {
2393
HashSet<String> scenes_to_save;
2394
2395
for (int i = 0; i < tab_container->get_tab_count(); i++) {
2396
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
2397
if (!seb) {
2398
continue;
2399
}
2400
2401
_auto_format_text(seb);
2402
2403
if (!seb->is_unsaved()) {
2404
continue;
2405
}
2406
2407
Ref<Resource> edited_res = seb->get_edited_resource();
2408
if (edited_res.is_valid()) {
2409
seb->apply_code();
2410
}
2411
2412
Ref<Script> scr = edited_res;
2413
2414
if (scr.is_valid()) {
2415
clear_docs_from_script(scr);
2416
}
2417
2418
if (!edited_res->is_built_in()) {
2419
Ref<TextFile> text_file = edited_res;
2420
if (text_file.is_valid()) {
2421
_save_text_file(text_file, text_file->get_path());
2422
continue;
2423
}
2424
2425
// External script, save it.
2426
EditorNode::get_singleton()->save_resource(edited_res);
2427
} else {
2428
// For built-in scripts, save their scenes instead.
2429
const String scene_path = edited_res->get_path().get_slice("::", 0);
2430
if (!scene_path.is_empty() && !scenes_to_save.has(scene_path)) {
2431
scenes_to_save.insert(scene_path);
2432
}
2433
}
2434
2435
if (scr.is_valid()) {
2436
update_docs_from_script(scr);
2437
}
2438
}
2439
2440
if (!scenes_to_save.is_empty()) {
2441
EditorNode::get_singleton()->save_scene_list(scenes_to_save);
2442
}
2443
2444
_update_script_names();
2445
}
2446
2447
void ScriptEditor::update_script_times() {
2448
for (int i = 0; i < tab_container->get_tab_count(); i++) {
2449
if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i))) {
2450
seb->edited_file_data.last_modified_time = FileAccess::get_modified_time(seb->edited_file_data.path);
2451
}
2452
}
2453
}
2454
2455
void ScriptEditor::apply_scripts() const {
2456
for (int i = 0; i < tab_container->get_tab_count(); i++) {
2457
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
2458
if (!seb) {
2459
continue;
2460
}
2461
2462
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb)) {
2463
teb->insert_final_newline();
2464
}
2465
seb->apply_code();
2466
}
2467
}
2468
2469
void ScriptEditor::reload_scripts(bool p_refresh_only) {
2470
// Call deferred to make sure it runs on the main thread.
2471
if (!Thread::is_main_thread()) {
2472
callable_mp(this, &ScriptEditor::_reload_scripts).call_deferred(p_refresh_only);
2473
return;
2474
}
2475
_reload_scripts(p_refresh_only);
2476
}
2477
2478
void ScriptEditor::_reload_scripts(bool p_refresh_only) {
2479
for (int i = 0; i < tab_container->get_tab_count(); i++) {
2480
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
2481
if (!seb) {
2482
continue;
2483
}
2484
2485
Ref<Resource> edited_res = seb->get_edited_resource();
2486
2487
if (edited_res->is_built_in()) {
2488
continue; // Internal script, who cares.
2489
}
2490
2491
if (p_refresh_only) {
2492
// Make sure the modified time is correct.
2493
seb->edited_file_data.last_modified_time = FileAccess::get_modified_time(edited_res->get_path());
2494
} else {
2495
uint64_t last_date = seb->edited_file_data.last_modified_time;
2496
uint64_t date = FileAccess::get_modified_time(edited_res->get_path());
2497
2498
if (last_date == date) {
2499
continue;
2500
}
2501
seb->edited_file_data.last_modified_time = date;
2502
2503
Ref<Script> scr = edited_res;
2504
if (scr.is_valid()) {
2505
Ref<Script> rel_scr = ResourceLoader::load(scr->get_path(), scr->get_class(), ResourceFormatLoader::CACHE_MODE_IGNORE);
2506
ERR_CONTINUE(rel_scr.is_null());
2507
scr->set_source_code(rel_scr->get_source_code());
2508
scr->reload(true);
2509
2510
update_docs_from_script(scr);
2511
}
2512
2513
Ref<JSON> json = edited_res;
2514
if (json.is_valid()) {
2515
Ref<JSON> rel_json = ResourceLoader::load(json->get_path(), json->get_class(), ResourceFormatLoader::CACHE_MODE_IGNORE);
2516
ERR_CONTINUE(rel_json.is_null());
2517
json->parse(rel_json->get_parsed_text(), true);
2518
}
2519
2520
Ref<TextFile> text_file = edited_res;
2521
if (text_file.is_valid()) {
2522
text_file->reload_from_file();
2523
}
2524
}
2525
2526
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(seb)) {
2527
teb->reload_text();
2528
}
2529
}
2530
2531
disk_changed->hide();
2532
_update_script_names();
2533
}
2534
2535
void ScriptEditor::_auto_format_text(ScriptEditorBase *p_seb) {
2536
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(p_seb)) {
2537
if (trim_trailing_whitespace_on_save) {
2538
teb->trim_trailing_whitespace();
2539
}
2540
if (trim_final_newlines_on_save) {
2541
teb->trim_final_newlines();
2542
}
2543
if (convert_indent_on_save) {
2544
teb->convert_indent();
2545
}
2546
}
2547
}
2548
2549
void ScriptEditor::open_find_in_files_dialog(const String &text) {
2550
find_in_files_dialog->set_find_in_files_mode(FindInFilesDialog::SEARCH_MODE);
2551
find_in_files_dialog->set_search_text(text);
2552
find_in_files_dialog->popup_centered();
2553
}
2554
2555
void ScriptEditor::open_script_create_dialog(const String &p_base_name, const String &p_base_path) {
2556
_menu_option(FILE_MENU_NEW_SCRIPT);
2557
script_create_dialog->config(p_base_name, p_base_path);
2558
}
2559
2560
void ScriptEditor::open_text_file_create_dialog(const String &p_base_path, const String &p_base_name) {
2561
_menu_option(FILE_MENU_NEW_TEXTFILE);
2562
file_dialog->set_current_dir(p_base_path);
2563
file_dialog->set_current_file(p_base_name);
2564
}
2565
2566
Ref<Resource> ScriptEditor::open_file(const String &p_file) {
2567
if (_get_recognized_extensions().find(p_file.get_extension())) {
2568
Ref<Resource> scr = ResourceLoader::load(p_file);
2569
if (scr.is_null()) {
2570
EditorNode::get_singleton()->show_warning(TTR("Could not load file at:") + "\n\n" + p_file, TTR("Error!"));
2571
return Ref<Resource>();
2572
}
2573
2574
edit(scr);
2575
return scr;
2576
}
2577
2578
Error error;
2579
Ref<TextFile> text_file = _load_text_file(p_file, &error);
2580
if (error != OK) {
2581
EditorNode::get_singleton()->show_warning(TTR("Could not load file at:") + "\n\n" + p_file, TTR("Error!"));
2582
return Ref<Resource>();
2583
}
2584
2585
if (text_file.is_valid()) {
2586
edit(text_file);
2587
return text_file;
2588
}
2589
return Ref<Resource>();
2590
}
2591
2592
void ScriptEditor::_add_callback(Object *p_obj, const String &p_function, const PackedStringArray &p_args) {
2593
ERR_FAIL_NULL(p_obj);
2594
Ref<Script> scr = p_obj->get_script();
2595
ERR_FAIL_COND(scr.is_null());
2596
2597
if (!scr->get_language()->can_make_function()) {
2598
return;
2599
}
2600
2601
EditorNode::get_singleton()->push_item(scr.ptr());
2602
2603
for (int i = 0; i < tab_container->get_tab_count(); i++) {
2604
ScriptTextEditor *ste = Object::cast_to<ScriptTextEditor>(tab_container->get_tab_control(i));
2605
if (!ste || ste->get_edited_resource() != scr) {
2606
continue;
2607
}
2608
2609
ste->add_callback(p_function, p_args);
2610
2611
_go_to_tab(i);
2612
2613
script_list->select(script_list->find_metadata(i));
2614
2615
// Save the current script so the changes can be picked up by an external editor.
2616
if (!scr.ptr()->is_built_in()) { // But only if it's not built-in script.
2617
save_current_script();
2618
}
2619
2620
break;
2621
}
2622
2623
// Move back to the previously edited node to reselect it in the Inspector and the SignalsDock.
2624
// We assume that the previous item is the node on which the callbacks were added.
2625
EditorNode::get_singleton()->edit_previous_item();
2626
}
2627
2628
void ScriptEditor::_save_editor_state(ScriptEditorBase *p_editor) {
2629
if (restoring_layout) {
2630
return;
2631
}
2632
2633
const String &path = p_editor->get_edited_resource()->get_path();
2634
if (path.is_empty()) {
2635
return;
2636
}
2637
2638
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(p_editor)) {
2639
script_editor_cache->set_value(path, "state", teb->get_edit_state());
2640
}
2641
// This is saved later when we save the editor layout.
2642
}
2643
2644
void ScriptEditor::_save_layout() {
2645
if (restoring_layout) {
2646
return;
2647
}
2648
2649
EditorNode::get_singleton()->save_editor_layout_delayed();
2650
}
2651
2652
void ScriptEditor::_apply_editor_settings() {
2653
textfile_extensions.clear();
2654
const Vector<String> textfile_ext = ((String)(EDITOR_GET("docks/filesystem/textfile_extensions"))).split(",", false);
2655
for (const String &E : textfile_ext) {
2656
textfile_extensions.insert(E);
2657
}
2658
2659
trim_trailing_whitespace_on_save = EDITOR_GET("text_editor/behavior/files/trim_trailing_whitespace_on_save");
2660
trim_final_newlines_on_save = EDITOR_GET("text_editor/behavior/files/trim_final_newlines_on_save");
2661
convert_indent_on_save = EDITOR_GET("text_editor/behavior/files/convert_indent_on_save");
2662
2663
members_overview_enabled = EDITOR_GET("text_editor/script_list/show_members_overview");
2664
help_overview_enabled = EDITOR_GET("text_editor/help/show_help_index");
2665
external_editor_active = EDITOR_GET("text_editor/external/use_external_editor");
2666
_update_members_overview_visibility();
2667
_update_help_overview_visibility();
2668
2669
_update_autosave_timer();
2670
2671
_update_script_names();
2672
2673
ScriptServer::set_reload_scripts_on_save(EDITOR_GET("text_editor/behavior/files/auto_reload_and_parse_scripts_on_save"));
2674
2675
for (int i = 0; i < tab_container->get_tab_count(); i++) {
2676
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(tab_container->get_tab_control(i))) {
2677
teb->update_settings();
2678
}
2679
}
2680
}
2681
2682
void ScriptEditor::_filesystem_changed() {
2683
_update_script_names();
2684
}
2685
2686
void ScriptEditor::_files_moved(const String &p_old_file, const String &p_new_file) {
2687
if (!script_editor_cache->has_section(p_old_file)) {
2688
return;
2689
}
2690
2691
for (int i = 0; i < tab_container->get_tab_count(); i++) {
2692
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
2693
if (seb && seb->edited_file_data.path == p_old_file) {
2694
seb->edited_file_data.path = p_new_file;
2695
break;
2696
}
2697
}
2698
2699
Variant state = script_editor_cache->get_value(p_old_file, "state");
2700
script_editor_cache->erase_section(p_old_file);
2701
script_editor_cache->set_value(p_new_file, "state", state);
2702
2703
// If Script, update breakpoints with debugger.
2704
Array breakpoints = _get_cached_breakpoints_for_script(p_new_file);
2705
for (int breakpoint : breakpoints) {
2706
int line = (int)breakpoint + 1;
2707
EditorDebuggerNode::get_singleton()->set_breakpoint(p_old_file, line, false);
2708
if (!p_new_file.begins_with("local://") && ResourceLoader::exists(p_new_file, "Script")) {
2709
EditorDebuggerNode::get_singleton()->set_breakpoint(p_new_file, line, true);
2710
}
2711
}
2712
// This is saved later when we save the editor layout.
2713
}
2714
2715
void ScriptEditor::_file_removed(const String &p_removed_file) {
2716
for (int i = 0; i < tab_container->get_tab_count(); i++) {
2717
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
2718
if (seb && seb->edited_file_data.path == p_removed_file) {
2719
// The script is deleted with no undo, so just close the tab.
2720
_close_tab(i, false, false);
2721
}
2722
}
2723
2724
// Check closed.
2725
if (script_editor_cache->has_section(p_removed_file)) {
2726
Array breakpoints = _get_cached_breakpoints_for_script(p_removed_file);
2727
for (int breakpoint : breakpoints) {
2728
EditorDebuggerNode::get_singleton()->set_breakpoint(p_removed_file, (int)breakpoint + 1, false);
2729
}
2730
script_editor_cache->erase_section(p_removed_file);
2731
}
2732
}
2733
2734
void ScriptEditor::_update_find_replace_bar() {
2735
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(_get_current_editor())) {
2736
teb->set_find_replace_bar(find_replace_bar);
2737
} else {
2738
find_replace_bar->set_text_edit(nullptr);
2739
find_replace_bar->hide();
2740
}
2741
}
2742
2743
void ScriptEditor::_autosave_scripts() {
2744
save_all_scripts();
2745
}
2746
2747
void ScriptEditor::_update_autosave_timer() {
2748
if (!autosave_timer->is_inside_tree()) {
2749
return;
2750
}
2751
2752
float autosave_time = EDITOR_GET("text_editor/behavior/files/autosave_interval_secs");
2753
if (autosave_time > 0) {
2754
autosave_timer->set_wait_time(autosave_time);
2755
autosave_timer->start();
2756
} else {
2757
autosave_timer->stop();
2758
}
2759
}
2760
2761
void ScriptEditor::_tree_changed() {
2762
if (waiting_update_names) {
2763
return;
2764
}
2765
2766
waiting_update_names = true;
2767
callable_mp(this, &ScriptEditor::_update_script_names).call_deferred();
2768
}
2769
2770
void ScriptEditor::_split_dragged(float) {
2771
_save_layout();
2772
}
2773
2774
Variant ScriptEditor::get_drag_data_fw(const Point2 &p_point, Control *p_from) {
2775
if (tab_container->get_tab_count() == 0) {
2776
return Variant();
2777
}
2778
2779
Node *cur_node = tab_container->get_tab_control(tab_container->get_current_tab());
2780
2781
HBoxContainer *drag_preview = memnew(HBoxContainer);
2782
String preview_name = "";
2783
Ref<Texture2D> preview_icon;
2784
2785
if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(cur_node)) {
2786
preview_name = seb->get_name();
2787
preview_icon = seb->get_theme_icon();
2788
}
2789
if (EditorHelp *eh = Object::cast_to<EditorHelp>(cur_node)) {
2790
preview_name = eh->get_class();
2791
preview_icon = get_editor_theme_icon(SNAME("Help"));
2792
}
2793
2794
if (preview_icon.is_valid()) {
2795
TextureRect *tf = memnew(TextureRect);
2796
tf->set_texture(preview_icon);
2797
tf->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
2798
drag_preview->add_child(tf);
2799
}
2800
Label *label = memnew(Label(preview_name));
2801
label->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); // Don't translate script names and class names.
2802
drag_preview->add_child(label);
2803
set_drag_preview(drag_preview);
2804
2805
Dictionary drag_data;
2806
drag_data["type"] = "script_list_element"; // using a custom type because node caused problems when dragging to scene tree
2807
drag_data["script_list_element"] = cur_node;
2808
2809
return drag_data;
2810
}
2811
2812
bool ScriptEditor::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const {
2813
Dictionary d = p_data;
2814
if (!d.has("type")) {
2815
return false;
2816
}
2817
2818
if (String(d["type"]) == "script_list_element") {
2819
Node *node = Object::cast_to<Node>(d["script_list_element"]);
2820
2821
if (Object::cast_to<ScriptEditorBase>(node) || Object::cast_to<EditorHelp>(node)) {
2822
return true;
2823
}
2824
}
2825
2826
if (String(d["type"]) == "nodes") {
2827
Array nodes = d["nodes"];
2828
if (nodes.is_empty()) {
2829
return false;
2830
}
2831
Node *node = get_node((nodes[0]));
2832
2833
if (Object::cast_to<ScriptEditorBase>(node) || Object::cast_to<EditorHelp>(node)) {
2834
return true;
2835
}
2836
}
2837
2838
if (String(d["type"]) == "files") {
2839
Vector<String> files = d["files"];
2840
2841
if (files.is_empty()) {
2842
return false; //weird
2843
}
2844
2845
for (int i = 0; i < files.size(); i++) {
2846
const String &file = files[i];
2847
if (file.is_empty() || !FileAccess::exists(file)) {
2848
continue;
2849
}
2850
if (ResourceLoader::exists(file, "Script") || ResourceLoader::exists(file, "JSON")) {
2851
Ref<Resource> scr = ResourceLoader::load(file);
2852
if (scr.is_valid()) {
2853
return true;
2854
}
2855
}
2856
2857
if (textfile_extensions.has(file.get_extension())) {
2858
Error err;
2859
Ref<TextFile> text_file = _load_text_file(file, &err);
2860
if (text_file.is_valid() && err == OK) {
2861
return true;
2862
}
2863
}
2864
}
2865
return false;
2866
}
2867
2868
return false;
2869
}
2870
2871
void ScriptEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) {
2872
if (!can_drop_data_fw(p_point, p_data, p_from)) {
2873
return;
2874
}
2875
2876
Dictionary d = p_data;
2877
if (!d.has("type")) {
2878
return;
2879
}
2880
2881
if (String(d["type"]) == "script_list_element") {
2882
Node *node = Object::cast_to<Node>(d["script_list_element"]);
2883
2884
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(node);
2885
EditorHelp *eh = Object::cast_to<EditorHelp>(node);
2886
if (seb || eh) {
2887
int new_index = 0;
2888
if (script_list->get_item_count() > 0) {
2889
int pos = 0;
2890
if (p_point == Vector2(Math::INF, Math::INF)) {
2891
if (script_list->is_anything_selected()) {
2892
pos = script_list->get_selected_items()[0];
2893
}
2894
} else {
2895
pos = script_list->get_item_at_position(p_point);
2896
}
2897
new_index = script_list->get_item_metadata(pos);
2898
}
2899
tab_container->move_child(node, new_index);
2900
tab_container->set_current_tab(new_index);
2901
_update_script_names();
2902
}
2903
}
2904
2905
if (String(d["type"]) == "nodes") {
2906
Array nodes = d["nodes"];
2907
if (nodes.is_empty()) {
2908
return;
2909
}
2910
Node *node = get_node(nodes[0]);
2911
2912
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(node);
2913
EditorHelp *eh = Object::cast_to<EditorHelp>(node);
2914
if (seb || eh) {
2915
int new_index = 0;
2916
if (script_list->get_item_count() > 0) {
2917
int pos = 0;
2918
if (p_point == Vector2(Math::INF, Math::INF)) {
2919
if (script_list->is_anything_selected()) {
2920
pos = script_list->get_selected_items()[0];
2921
}
2922
} else {
2923
pos = script_list->get_item_at_position(p_point);
2924
}
2925
new_index = script_list->get_item_metadata(pos);
2926
}
2927
tab_container->move_child(node, new_index);
2928
tab_container->set_current_tab(new_index);
2929
_update_script_names();
2930
}
2931
}
2932
2933
if (String(d["type"]) == "files") {
2934
Vector<String> files = d["files"];
2935
2936
int new_index = 0;
2937
if (script_list->get_item_count() > 0) {
2938
int pos = 0;
2939
if (p_point == Vector2(Math::INF, Math::INF)) {
2940
if (script_list->is_anything_selected()) {
2941
pos = script_list->get_selected_items()[0];
2942
}
2943
} else {
2944
pos = script_list->get_item_at_position(p_point);
2945
}
2946
new_index = script_list->get_item_metadata(pos);
2947
}
2948
int num_tabs_before = tab_container->get_tab_count();
2949
for (int i = 0; i < files.size(); i++) {
2950
const String &file = files[i];
2951
if (file.is_empty() || !FileAccess::exists(file)) {
2952
continue;
2953
}
2954
2955
if (!ResourceLoader::exists(file, "Script") && !ResourceLoader::exists(file, "JSON") && !textfile_extensions.has(file.get_extension())) {
2956
continue;
2957
}
2958
2959
Ref<Resource> res = open_file(file);
2960
if (res.is_valid()) {
2961
const int num_tabs = tab_container->get_tab_count();
2962
if (num_tabs > num_tabs_before) {
2963
tab_container->move_child(tab_container->get_tab_control(tab_container->get_tab_count() - 1), new_index);
2964
num_tabs_before = num_tabs;
2965
} else if (num_tabs > 0) { /* Maybe script was already open */
2966
tab_container->move_child(tab_container->get_tab_control(tab_container->get_current_tab()), new_index);
2967
}
2968
}
2969
}
2970
if (tab_container->get_tab_count() > 0) {
2971
tab_container->set_current_tab(new_index);
2972
}
2973
_update_script_names();
2974
}
2975
}
2976
2977
void ScriptEditor::input(const Ref<InputEvent> &p_event) {
2978
// This is implemented in `input()` rather than `unhandled_input()` to allow
2979
// the shortcut to be used regardless of the click location.
2980
// This feature can be disabled to avoid interfering with other uses of the additional
2981
// mouse buttons, such as push-to-talk in a VoIP program.
2982
if (EDITOR_GET("interface/editor/mouse_extra_buttons_navigate_history")) {
2983
const Ref<InputEventMouseButton> mb = p_event;
2984
2985
// Navigate the script history using additional mouse buttons present on some mice.
2986
// This must be hardcoded as the editor shortcuts dialog doesn't allow assigning
2987
// more than one shortcut per action.
2988
if (mb.is_valid() && mb->is_pressed() && is_visible_in_tree()) {
2989
if (mb->get_button_index() == MouseButton::MB_XBUTTON1) {
2990
_history_back();
2991
}
2992
2993
if (mb->get_button_index() == MouseButton::MB_XBUTTON2) {
2994
_history_forward();
2995
}
2996
}
2997
}
2998
}
2999
3000
void ScriptEditor::shortcut_input(const Ref<InputEvent> &p_event) {
3001
ERR_FAIL_COND(p_event.is_null());
3002
3003
if (!is_visible_in_tree() || !p_event->is_pressed()) {
3004
return;
3005
}
3006
if (ED_IS_SHORTCUT("script_editor/next_script", p_event)) {
3007
if (script_list->get_item_count() > 1) {
3008
int next_tab = script_list->get_current() + 1;
3009
next_tab %= script_list->get_item_count();
3010
_go_to_tab(script_list->get_item_metadata(next_tab));
3011
_update_script_names();
3012
}
3013
accept_event();
3014
}
3015
if (ED_IS_SHORTCUT("script_editor/prev_script", p_event)) {
3016
if (script_list->get_item_count() > 1) {
3017
int next_tab = script_list->get_current() - 1;
3018
next_tab = next_tab >= 0 ? next_tab : script_list->get_item_count() - 1;
3019
_go_to_tab(script_list->get_item_metadata(next_tab));
3020
_update_script_names();
3021
}
3022
accept_event();
3023
}
3024
if (ED_IS_SHORTCUT("script_editor/window_move_up", p_event)) {
3025
_menu_option(FILE_MENU_MOVE_UP);
3026
accept_event();
3027
}
3028
if (ED_IS_SHORTCUT("script_editor/window_move_down", p_event)) {
3029
_menu_option(FILE_MENU_MOVE_DOWN);
3030
accept_event();
3031
}
3032
3033
if (p_event->is_echo()) {
3034
return;
3035
}
3036
3037
Callable custom_callback = EditorContextMenuPluginManager::get_singleton()->match_custom_shortcut(EditorContextMenuPlugin::CONTEXT_SLOT_SCRIPT_EDITOR, p_event);
3038
if (custom_callback.is_valid()) {
3039
Ref<Resource> resource;
3040
if (ScriptEditorBase *current = _get_current_editor()) {
3041
resource = current->get_edited_resource();
3042
}
3043
EditorContextMenuPluginManager::get_singleton()->invoke_callback(custom_callback, resource);
3044
accept_event();
3045
}
3046
}
3047
3048
void ScriptEditor::_script_list_clicked(int p_item, Vector2 p_local_mouse_pos, MouseButton p_mouse_button_index) {
3049
if (p_mouse_button_index == MouseButton::MIDDLE) {
3050
script_list->select(p_item);
3051
_script_selected(p_item);
3052
_menu_option(FILE_MENU_CLOSE);
3053
}
3054
3055
if (p_mouse_button_index == MouseButton::RIGHT) {
3056
_make_script_list_context_menu();
3057
}
3058
}
3059
3060
void ScriptEditor::_make_script_list_context_menu() {
3061
context_menu->clear();
3062
3063
int selected = tab_container->get_current_tab();
3064
if (selected < 0 || selected >= tab_container->get_tab_count()) {
3065
return;
3066
}
3067
3068
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(selected));
3069
if (seb) {
3070
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/save"), FILE_MENU_SAVE);
3071
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/save_as"), FILE_MENU_SAVE_AS);
3072
}
3073
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/close_file"), FILE_MENU_CLOSE);
3074
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/close_other_tabs"), FILE_MENU_CLOSE_OTHER_TABS);
3075
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/close_tabs_below"), FILE_MENU_CLOSE_TABS_BELOW);
3076
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/close_all"), FILE_MENU_CLOSE_ALL);
3077
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/close_docs"), FILE_MENU_CLOSE_DOCS);
3078
context_menu->add_separator();
3079
if (seb) {
3080
Ref<Script> scr = seb->get_edited_resource();
3081
if (scr.is_valid() && scr->is_tool()) {
3082
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/reload_script_soft"), FILE_MENU_SOFT_RELOAD_TOOL);
3083
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/run_file"), FILE_MENU_RUN);
3084
context_menu->add_separator();
3085
}
3086
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/copy_path"), FILE_MENU_COPY_PATH);
3087
context_menu->set_item_disabled(-1, seb->get_edited_resource()->get_path().is_empty());
3088
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/copy_uid"), FILE_MENU_COPY_UID);
3089
context_menu->set_item_disabled(-1, ResourceLoader::get_resource_uid(seb->get_edited_resource()->get_path()) == ResourceUID::INVALID_ID);
3090
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/show_in_file_system"), FILE_MENU_SHOW_IN_FILE_SYSTEM);
3091
context_menu->add_separator();
3092
}
3093
3094
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/window_move_up"), FILE_MENU_MOVE_UP);
3095
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/window_move_down"), FILE_MENU_MOVE_DOWN);
3096
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/window_sort"), FILE_MENU_SORT);
3097
context_menu->add_shortcut(ED_GET_SHORTCUT("script_editor/toggle_files_panel"), FILE_MENU_TOGGLE_FILES_PANEL);
3098
3099
context_menu->set_item_disabled(context_menu->get_item_index(FILE_MENU_CLOSE_ALL), tab_container->get_tab_count() <= 0);
3100
context_menu->set_item_disabled(context_menu->get_item_index(FILE_MENU_CLOSE_OTHER_TABS), tab_container->get_tab_count() <= 1);
3101
context_menu->set_item_disabled(context_menu->get_item_index(FILE_MENU_CLOSE_DOCS), !_has_docs_tab());
3102
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);
3103
context_menu->set_item_disabled(context_menu->get_item_index(FILE_MENU_MOVE_UP), tab_container->get_current_tab() <= 0);
3104
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);
3105
context_menu->set_item_disabled(context_menu->get_item_index(FILE_MENU_SORT), tab_container->get_tab_count() <= 1);
3106
3107
// Context menu plugin.
3108
Vector<String> selected_paths;
3109
if (seb) {
3110
Ref<Resource> scr = seb->get_edited_resource();
3111
if (scr.is_valid()) {
3112
String path = scr->get_path();
3113
selected_paths.push_back(path);
3114
}
3115
}
3116
EditorContextMenuPluginManager::get_singleton()->add_options_from_plugins(context_menu, EditorContextMenuPlugin::CONTEXT_SLOT_SCRIPT_EDITOR, selected_paths);
3117
3118
context_menu->set_position(get_screen_position() + get_local_mouse_position());
3119
context_menu->reset_size();
3120
context_menu->popup();
3121
}
3122
3123
void ScriptEditor::set_window_layout(Ref<ConfigFile> p_layout) {
3124
if (!bool(EDITOR_GET("text_editor/behavior/files/restore_scripts_on_load"))) {
3125
return;
3126
}
3127
3128
if (!p_layout->has_section_key("ScriptEditor", "open_scripts") && !p_layout->has_section_key("ScriptEditor", "open_help")) {
3129
return;
3130
}
3131
3132
Array scripts = p_layout->get_value("ScriptEditor", "open_scripts");
3133
Array helps;
3134
if (p_layout->has_section_key("ScriptEditor", "open_help")) {
3135
helps = p_layout->get_value("ScriptEditor", "open_help");
3136
}
3137
3138
restoring_layout = true;
3139
3140
HashSet<String> loaded_scripts;
3141
List<String> extensions = _get_recognized_extensions();
3142
3143
for (const Variant &v : scripts) {
3144
String path = v;
3145
3146
Dictionary script_info = v;
3147
if (!script_info.is_empty()) {
3148
path = script_info["path"];
3149
}
3150
3151
if (!_script_exists(path)) {
3152
if (script_editor_cache->has_section(path)) {
3153
script_editor_cache->erase_section(path);
3154
}
3155
continue;
3156
} else if (!path.is_resource_file() && !EditorNode::get_singleton()->is_scene_open(path.get_slice("::", 0))) {
3157
continue;
3158
}
3159
loaded_scripts.insert(path);
3160
3161
bool is_script = false;
3162
if (path.is_resource_file()) {
3163
is_script = extensions.find(path.get_extension());
3164
} else {
3165
Ref<Script> scr = ResourceCache::get_ref(path);
3166
if (scr.is_valid()) {
3167
is_script = true;
3168
} else {
3169
continue;
3170
}
3171
}
3172
3173
if (is_script) {
3174
Ref<Resource> scr = ResourceLoader::load(path);
3175
if (scr.is_null()) {
3176
continue;
3177
}
3178
if (!edit(scr, false)) {
3179
continue;
3180
}
3181
} else {
3182
Error error;
3183
Ref<TextFile> text_file = _load_text_file(path, &error);
3184
if (error != OK || text_file.is_null()) {
3185
continue;
3186
}
3187
if (!edit(text_file, false)) {
3188
continue;
3189
}
3190
}
3191
3192
if (!script_info.is_empty()) {
3193
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(tab_container->get_tab_control(tab_container->get_tab_count() - 1))) {
3194
teb->set_edit_state(script_info["state"]);
3195
}
3196
}
3197
}
3198
3199
for (int i = 0; i < helps.size(); i++) {
3200
String path = helps[i];
3201
if (path.is_empty()) { // invalid, skip
3202
continue;
3203
}
3204
_help_class_open(path);
3205
}
3206
3207
for (int i = 0; i < tab_container->get_tab_count(); i++) {
3208
tab_container->get_tab_control(i)->set_meta("__editor_pass", Variant());
3209
}
3210
3211
if (p_layout->has_section_key("ScriptEditor", "script_split_offset")) {
3212
script_split->set_split_offset(p_layout->get_value("ScriptEditor", "script_split_offset"));
3213
}
3214
3215
if (p_layout->has_section_key("ScriptEditor", "list_split_offset")) {
3216
list_split->set_split_offset(p_layout->get_value("ScriptEditor", "list_split_offset"));
3217
}
3218
3219
// Remove any deleted editors that have been removed between launches.
3220
// and if a Script, register breakpoints with the debugger.
3221
Vector<String> cached_editors = script_editor_cache->get_sections();
3222
for (const String &E : cached_editors) {
3223
if (loaded_scripts.has(E)) {
3224
continue;
3225
}
3226
3227
if (!_script_exists(E)) {
3228
script_editor_cache->erase_section(E);
3229
continue;
3230
}
3231
3232
Array breakpoints = _get_cached_breakpoints_for_script(E);
3233
for (int breakpoint : breakpoints) {
3234
EditorDebuggerNode::get_singleton()->set_breakpoint(E, (int)breakpoint + 1, true);
3235
}
3236
}
3237
3238
_set_script_zoom_factor(p_layout->get_value("ScriptEditor", "zoom_factor", 1.0f));
3239
3240
restoring_layout = false;
3241
3242
_update_script_names();
3243
3244
if (p_layout->has_section_key("ScriptEditor", "selected_script")) {
3245
String selected_script = p_layout->get_value("ScriptEditor", "selected_script");
3246
// If the selected script is not in the list of open scripts, select nothing.
3247
for (int i = 0; i < tab_container->get_tab_count(); i++) {
3248
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
3249
if (seb && seb->get_edited_resource()->get_path() == selected_script) {
3250
_go_to_tab(i);
3251
break;
3252
}
3253
}
3254
}
3255
}
3256
3257
void ScriptEditor::get_window_layout(Ref<ConfigFile> p_layout) {
3258
Array scripts;
3259
Array helps;
3260
String selected_script;
3261
for (int i = 0; i < tab_container->get_tab_count(); i++) {
3262
if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i))) {
3263
const String path = seb->get_edited_resource()->get_path();
3264
if (path.is_empty()) {
3265
continue;
3266
}
3267
3268
if (tab_container->get_current_tab_control() == tab_container->get_tab_control(i)) {
3269
selected_script = path;
3270
}
3271
3272
_save_editor_state(seb);
3273
scripts.push_back(path);
3274
}
3275
3276
if (EditorHelp *eh = Object::cast_to<EditorHelp>(tab_container->get_tab_control(i))) {
3277
helps.push_back(eh->get_class());
3278
}
3279
}
3280
3281
p_layout->set_value("ScriptEditor", "open_scripts", scripts);
3282
p_layout->set_value("ScriptEditor", "selected_script", selected_script);
3283
p_layout->set_value("ScriptEditor", "open_help", helps);
3284
p_layout->set_value("ScriptEditor", "script_split_offset", script_split->get_split_offset());
3285
p_layout->set_value("ScriptEditor", "list_split_offset", list_split->get_split_offset());
3286
p_layout->set_value("ScriptEditor", "zoom_factor", zoom_factor);
3287
3288
// Save the cache.
3289
script_editor_cache->save(EditorPaths::get_singleton()->get_project_settings_dir().path_join("script_editor_cache.cfg"));
3290
}
3291
3292
void ScriptEditor::_help_class_open(const String &p_class) {
3293
if (p_class.is_empty()) {
3294
return;
3295
}
3296
3297
for (int i = 0; i < tab_container->get_tab_count(); i++) {
3298
EditorHelp *eh = Object::cast_to<EditorHelp>(tab_container->get_tab_control(i));
3299
3300
if (eh && eh->get_class() == p_class) {
3301
_go_to_tab(i);
3302
_update_script_names();
3303
return;
3304
}
3305
}
3306
3307
EditorHelp *eh = memnew(EditorHelp);
3308
3309
eh->set_name(p_class);
3310
tab_container->add_child(eh);
3311
_go_to_tab(tab_container->get_tab_count() - 1);
3312
eh->go_to_class(p_class);
3313
eh->connect("go_to_help", callable_mp(this, &ScriptEditor::_help_class_goto));
3314
eh->connect("request_save_history", callable_mp(this, &ScriptEditor::_save_history));
3315
_add_recent_script(p_class);
3316
_sort_list_on_update = true;
3317
_update_script_names();
3318
_save_layout();
3319
}
3320
3321
void ScriptEditor::_help_class_goto(const String &p_desc) {
3322
String cname = p_desc.get_slicec(':', 1);
3323
3324
if (_help_tab_goto(cname, p_desc)) {
3325
return;
3326
}
3327
3328
EditorHelp *eh = memnew(EditorHelp);
3329
3330
eh->set_name(cname);
3331
tab_container->add_child(eh);
3332
_go_to_tab(tab_container->get_tab_count() - 1);
3333
eh->go_to_help(p_desc);
3334
eh->connect("go_to_help", callable_mp(this, &ScriptEditor::_help_class_goto));
3335
_add_recent_script(eh->get_class());
3336
_sort_list_on_update = true;
3337
_update_script_names();
3338
_save_layout();
3339
}
3340
3341
bool ScriptEditor::_help_tab_goto(const String &p_name, const String &p_desc) {
3342
for (int i = 0; i < tab_container->get_tab_count(); i++) {
3343
EditorHelp *eh = Object::cast_to<EditorHelp>(tab_container->get_tab_control(i));
3344
3345
if (eh && eh->get_class() == p_name) {
3346
_go_to_tab(i);
3347
eh->go_to_help(p_desc);
3348
_update_script_names();
3349
return true;
3350
}
3351
}
3352
return false;
3353
}
3354
3355
void ScriptEditor::update_doc(const String &p_name) {
3356
for (int i = 0; i < tab_container->get_tab_count(); i++) {
3357
EditorHelp *eh = Object::cast_to<EditorHelp>(tab_container->get_tab_control(i));
3358
if (eh && eh->get_class() == p_name) {
3359
eh->update_doc();
3360
return;
3361
}
3362
}
3363
}
3364
3365
void ScriptEditor::clear_docs_from_script(const Ref<Script> &p_script) {
3366
ERR_FAIL_COND(p_script.is_null());
3367
3368
for (const DocData::ClassDoc &cd : p_script->get_documentation()) {
3369
if (EditorHelp::get_doc_data()->has_doc(cd.name)) {
3370
EditorHelp::get_doc_data()->remove_doc(cd.name);
3371
}
3372
}
3373
}
3374
3375
void ScriptEditor::update_docs_from_script(const Ref<Script> &p_script) {
3376
ERR_FAIL_COND(p_script.is_null());
3377
3378
for (const DocData::ClassDoc &cd : p_script->get_documentation()) {
3379
EditorHelp::get_doc_data()->add_doc(cd);
3380
update_doc(cd.name);
3381
}
3382
}
3383
3384
void ScriptEditor::_update_selected_editor_menu() {
3385
TextEditorBase *current_editor = Object::cast_to<TextEditorBase>(_get_current_editor());
3386
for (Control *editor_menu : editor_menus) {
3387
editor_menu->set_visible(current_editor && editor_menu == current_editor->get_edit_menu());
3388
}
3389
3390
PopupMenu *search_popup = script_search_menu->get_popup();
3391
search_popup->clear();
3392
if (Object::cast_to<EditorHelp>(tab_container->get_current_tab_control())) {
3393
search_popup->add_shortcut(ED_SHORTCUT("script_editor/find", TTRC("Find..."), KeyModifierMask::CMD_OR_CTRL | Key::F), HELP_SEARCH_FIND);
3394
search_popup->add_shortcut(ED_SHORTCUT("script_editor/find_next", TTRC("Find Next"), Key::F3), HELP_SEARCH_FIND_NEXT);
3395
search_popup->add_shortcut(ED_SHORTCUT("script_editor/find_previous", TTRC("Find Previous"), KeyModifierMask::SHIFT | Key::F3), HELP_SEARCH_FIND_PREVIOUS);
3396
search_popup->add_separator();
3397
search_popup->add_shortcut(ED_GET_SHORTCUT("editor/find_in_files"), SEARCH_IN_FILES);
3398
search_popup->add_shortcut(ED_GET_SHORTCUT("script_editor/replace_in_files"), REPLACE_IN_FILES);
3399
script_search_menu->show();
3400
} else {
3401
if (tab_container->get_tab_count() == 0) {
3402
search_popup->add_shortcut(ED_GET_SHORTCUT("editor/find_in_files"), SEARCH_IN_FILES);
3403
search_popup->add_shortcut(ED_GET_SHORTCUT("script_editor/replace_in_files"), REPLACE_IN_FILES);
3404
script_search_menu->show();
3405
} else {
3406
script_search_menu->hide();
3407
}
3408
}
3409
}
3410
3411
void ScriptEditor::_unlock_history() {
3412
lock_history = false;
3413
}
3414
3415
void ScriptEditor::_update_history_pos(int p_new_pos) {
3416
Node *n = tab_container->get_current_tab_control();
3417
3418
if (Object::cast_to<TextEditorBase>(n)) {
3419
history.write[history_pos].state = Object::cast_to<TextEditorBase>(n)->get_navigation_state();
3420
}
3421
if (Object::cast_to<EditorHelp>(n)) {
3422
history.write[history_pos].state = Object::cast_to<EditorHelp>(n)->get_scroll();
3423
}
3424
3425
history_pos = p_new_pos;
3426
tab_container->set_current_tab(tab_container->get_tab_idx_from_control(history[history_pos].control));
3427
3428
n = history[history_pos].control;
3429
3430
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(n);
3431
if (seb) {
3432
if (TextEditorBase *teb = Object::cast_to<TextEditorBase>(n)) {
3433
lock_history = true;
3434
teb->set_edit_state(history[history_pos].state);
3435
// `set_edit_state()` can modify the caret position which might trigger a
3436
// request to save the history. Since `TextEdit::caret_changed` is emitted
3437
// deferred, we need to defer unlocking of the history as well.
3438
callable_mp(this, &ScriptEditor::_unlock_history).call_deferred();
3439
teb->ensure_focus();
3440
}
3441
3442
Ref<Script> scr = seb->get_edited_resource();
3443
if (scr.is_valid()) {
3444
notify_script_changed(scr);
3445
}
3446
3447
seb->validate_script();
3448
}
3449
3450
if (EditorHelp *eh = Object::cast_to<EditorHelp>(n)) {
3451
eh->set_scroll(history[history_pos].state);
3452
eh->set_focused();
3453
}
3454
3455
n->set_meta("__editor_pass", ++edit_pass);
3456
_update_script_names();
3457
_update_history_arrows();
3458
_update_selected_editor_menu();
3459
}
3460
3461
void ScriptEditor::_history_forward() {
3462
if (history_pos < history.size() - 1) {
3463
_update_history_pos(history_pos + 1);
3464
}
3465
}
3466
3467
void ScriptEditor::_history_back() {
3468
if (history_pos > 0) {
3469
_update_history_pos(history_pos - 1);
3470
}
3471
}
3472
3473
Vector<Ref<Script>> ScriptEditor::get_open_scripts() const {
3474
Vector<Ref<Script>> out_scripts = Vector<Ref<Script>>();
3475
3476
for (int i = 0; i < tab_container->get_tab_count(); i++) {
3477
ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i));
3478
if (!seb) {
3479
continue;
3480
}
3481
3482
Ref<Script> scr = seb->get_edited_resource();
3483
if (scr.is_valid()) {
3484
out_scripts.push_back(scr);
3485
}
3486
}
3487
3488
return out_scripts;
3489
}
3490
3491
TypedArray<ScriptEditorBase> ScriptEditor::_get_open_script_editors() const {
3492
TypedArray<ScriptEditorBase> script_editors;
3493
for (int i = 0; i < tab_container->get_tab_count(); i++) {
3494
if (ScriptEditorBase *seb = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i))) {
3495
script_editors.push_back(seb);
3496
}
3497
}
3498
return script_editors;
3499
}
3500
3501
void ScriptEditor::set_scene_root_script(Ref<Script> p_script) {
3502
// Don't open dominant script if using an external editor.
3503
bool use_external_editor =
3504
external_editor_active ||
3505
(p_script.is_valid() && p_script->get_language()->overrides_external_editor());
3506
use_external_editor = use_external_editor && !(p_script.is_valid() && p_script->is_built_in()); // Ignore external editor for built-in scripts.
3507
const bool open_dominant = EDITOR_GET("text_editor/behavior/files/open_dominant_script_on_scene_change");
3508
3509
if (open_dominant && !use_external_editor && p_script.is_valid()) {
3510
edit(p_script);
3511
}
3512
}
3513
3514
bool ScriptEditor::script_goto_method(Ref<Script> p_script, const String &p_method) {
3515
int line = p_script->get_member_line(p_method);
3516
3517
if (line == -1) {
3518
return false;
3519
}
3520
3521
return edit(p_script, line, 0);
3522
}
3523
3524
void ScriptEditor::set_live_auto_reload_running_scripts(bool p_enabled) {
3525
auto_reload_running_scripts = p_enabled;
3526
}
3527
3528
void ScriptEditor::_calculate_script_name_button_size() {
3529
Ref<Font> font = script_name_button->get_theme_font(SceneStringName(font), SNAME("Button"));
3530
HorizontalAlignment alignment = script_name_button->get_text_alignment();
3531
int font_size = script_name_button->get_theme_font_size(SceneStringName(font_size), SNAME("Button"));
3532
String text = script_name_button->get_text();
3533
int jst_flags = TextServer::JUSTIFICATION_WORD_BOUND | TextServer::JUSTIFICATION_KASHIDA | TextServer::JUSTIFICATION_SKIP_LAST_LINE | TextServer::JUSTIFICATION_DO_NOT_SKIP_SINGLE_LINE;
3534
TextServer::Direction direction = TextServer::Direction(script_name_button->get_text_direction());
3535
Vector2 text_size = font->get_string_size(text, alignment, -1, font_size, jst_flags, direction, TextServer::ORIENTATION_HORIZONTAL);
3536
3537
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);
3538
_calculate_script_name_button_ratio();
3539
}
3540
3541
void ScriptEditor::_calculate_script_name_button_ratio() {
3542
const float total_width = script_name_button_hbox->get_size().width;
3543
if (total_width <= 0) {
3544
return;
3545
}
3546
3547
// Make the ratios a fraction bigger, to avoid unnecessary trimming.
3548
const float extra_ratio = 4 / total_width;
3549
3550
const float script_name_ratio = MIN(1, script_name_width / total_width + extra_ratio);
3551
script_name_button->set_stretch_ratio(script_name_ratio);
3552
3553
float ratio_left = 1 - script_name_ratio;
3554
script_name_button_left_spacer->set_stretch_ratio(ratio_left / 2);
3555
script_name_button_right_spacer->set_stretch_ratio(ratio_left / 2);
3556
}
3557
3558
void ScriptEditor::_help_search(const String &p_text) {
3559
help_search_dialog->popup_dialog(p_text);
3560
}
3561
3562
void ScriptEditor::register_syntax_highlighter(const Ref<EditorSyntaxHighlighter> &p_syntax_highlighter) {
3563
ERR_FAIL_COND(p_syntax_highlighter.is_null());
3564
3565
if (!syntax_highlighters.has(p_syntax_highlighter)) {
3566
syntax_highlighters.push_back(p_syntax_highlighter);
3567
}
3568
}
3569
3570
void ScriptEditor::unregister_syntax_highlighter(const Ref<EditorSyntaxHighlighter> &p_syntax_highlighter) {
3571
ERR_FAIL_COND(p_syntax_highlighter.is_null());
3572
3573
syntax_highlighters.erase(p_syntax_highlighter);
3574
}
3575
3576
int ScriptEditor::script_editor_func_count = 0;
3577
CreateScriptEditorFunc ScriptEditor::script_editor_funcs[ScriptEditor::SCRIPT_EDITOR_FUNC_MAX];
3578
3579
void ScriptEditor::register_create_script_editor_function(CreateScriptEditorFunc p_func) {
3580
ERR_FAIL_COND(script_editor_func_count == SCRIPT_EDITOR_FUNC_MAX);
3581
script_editor_funcs[script_editor_func_count++] = p_func;
3582
}
3583
3584
void ScriptEditor::_script_changed() {
3585
SignalsDock::get_singleton()->update_lists();
3586
}
3587
3588
void ScriptEditor::_on_replace_in_files_requested(const String &text) {
3589
find_in_files_dialog->set_find_in_files_mode(FindInFilesDialog::REPLACE_MODE);
3590
find_in_files_dialog->set_search_text(text);
3591
find_in_files_dialog->set_replace_text("");
3592
find_in_files_dialog->popup_centered();
3593
}
3594
3595
void ScriptEditor::_on_find_in_files_result_selected(const String &fpath, int line_number, int begin, int end) {
3596
if (ResourceLoader::exists(fpath)) {
3597
Ref<Resource> res = ResourceLoader::load(fpath);
3598
3599
if (fpath.get_extension() == "gdshader") {
3600
ShaderEditorPlugin *shader_editor = Object::cast_to<ShaderEditorPlugin>(EditorNode::get_editor_data().get_editor_by_name("Shader"));
3601
shader_editor->edit(res.ptr());
3602
shader_editor->make_visible(true);
3603
TextShaderEditor *text_shader_editor = Object::cast_to<TextShaderEditor>(shader_editor->get_shader_editor(res));
3604
if (text_shader_editor) {
3605
text_shader_editor->goto_line_selection(line_number - 1, begin, end);
3606
}
3607
return;
3608
} else if (fpath.get_extension() == "tscn") {
3609
const PackedStringArray lines = FileAccess::get_file_as_string(fpath).split("\n");
3610
if (line_number > lines.size()) {
3611
return;
3612
}
3613
3614
const char *scr_header = "[sub_resource type=\"GDScript\" id=\"";
3615
const char *source_header = "script/source = \"";
3616
String script_id;
3617
3618
// Search the scene backwards from the found line.
3619
int scan_line = line_number - 1;
3620
while (scan_line >= 0) {
3621
const String &line = lines[scan_line];
3622
if (line.begins_with(source_header)) {
3623
// Adjust line relative to the script beginning.
3624
line_number -= scan_line + 1;
3625
} else if (line.begins_with(scr_header)) {
3626
script_id = line.trim_prefix(scr_header).get_slicec('"', 0);
3627
break;
3628
}
3629
scan_line--;
3630
}
3631
3632
EditorNode::get_singleton()->load_scene(fpath);
3633
if (!script_id.is_empty()) {
3634
Ref<Script> scr = ResourceLoader::load(fpath + "::" + script_id, "Script");
3635
if (scr.is_valid()) {
3636
edit(scr);
3637
ScriptTextEditor *ste = Object::cast_to<ScriptTextEditor>(_get_current_editor());
3638
3639
if (ste) {
3640
callable_mp(EditorInterface::get_singleton(), &EditorInterface::set_main_screen_editor).call_deferred("Script");
3641
if (line_number == 0) {
3642
const int source_len = strlen(source_header);
3643
ste->goto_line_selection(line_number, begin - source_len, end - source_len);
3644
} else {
3645
ste->goto_line_selection(line_number, begin, end);
3646
}
3647
}
3648
}
3649
}
3650
3651
return;
3652
} else {
3653
Ref<Script> scr = res;
3654
Ref<JSON> json = res;
3655
if (scr.is_valid() || json.is_valid()) {
3656
edit(scr);
3657
3658
ScriptTextEditor *ste = Object::cast_to<ScriptTextEditor>(_get_current_editor());
3659
if (ste) {
3660
EditorInterface::get_singleton()->set_main_screen_editor("Script");
3661
ste->goto_line_selection(line_number - 1, begin, end);
3662
}
3663
return;
3664
}
3665
}
3666
}
3667
3668
// If the file is not a valid resource/script, load it as a text file.
3669
Error err;
3670
Ref<TextFile> text_file = _load_text_file(fpath, &err);
3671
if (text_file.is_valid()) {
3672
edit(text_file);
3673
3674
TextEditor *te = Object::cast_to<TextEditor>(_get_current_editor());
3675
if (te) {
3676
te->goto_line_selection(line_number - 1, begin, end);
3677
}
3678
}
3679
}
3680
3681
void ScriptEditor::_start_find_in_files(bool with_replace) {
3682
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());
3683
FindInFiles *f = panel->get_finder();
3684
3685
f->set_search_text(find_in_files_dialog->get_search_text());
3686
f->set_match_case(find_in_files_dialog->is_match_case());
3687
f->set_whole_words(find_in_files_dialog->is_whole_words());
3688
f->set_folder(find_in_files_dialog->get_folder());
3689
f->set_filter(find_in_files_dialog->get_filter());
3690
f->set_includes(find_in_files_dialog->get_includes());
3691
f->set_excludes(find_in_files_dialog->get_excludes());
3692
3693
panel->set_with_replace(with_replace);
3694
panel->set_replace_text(find_in_files_dialog->get_replace_text());
3695
panel->start_search();
3696
3697
find_in_files->make_visible();
3698
}
3699
3700
void ScriptEditor::_on_find_in_files_modified_files(const PackedStringArray &paths) {
3701
_test_script_times_on_disk();
3702
_update_modified_scripts_for_external_editor();
3703
}
3704
3705
void ScriptEditor::_set_script_zoom_factor(float p_zoom_factor) {
3706
if (zoom_factor == p_zoom_factor) {
3707
return;
3708
}
3709
3710
zoom_factor = p_zoom_factor;
3711
}
3712
3713
void ScriptEditor::_update_code_editor_zoom_factor(CodeTextEditor *p_code_text_editor) {
3714
if (p_code_text_editor && p_code_text_editor->is_visible_in_tree() && zoom_factor != p_code_text_editor->get_zoom_factor()) {
3715
p_code_text_editor->set_zoom_factor(zoom_factor);
3716
}
3717
}
3718
3719
void ScriptEditor::_window_changed(bool p_visible) {
3720
make_floating->set_visible(!p_visible);
3721
is_floating = p_visible;
3722
}
3723
3724
void ScriptEditor::_filter_scripts_text_changed(const String &p_newtext) {
3725
_update_script_names();
3726
}
3727
3728
void ScriptEditor::_filter_methods_text_changed(const String &p_newtext) {
3729
_update_members_overview();
3730
}
3731
3732
void ScriptEditor::_bind_methods() {
3733
ClassDB::bind_method("_help_tab_goto", &ScriptEditor::_help_tab_goto);
3734
ClassDB::bind_method("get_current_editor", &ScriptEditor::_get_current_editor);
3735
ClassDB::bind_method("get_open_script_editors", &ScriptEditor::_get_open_script_editors);
3736
ClassDB::bind_method("get_breakpoints", &ScriptEditor::_get_breakpoints);
3737
3738
ClassDB::bind_method(D_METHOD("register_syntax_highlighter", "syntax_highlighter"), &ScriptEditor::register_syntax_highlighter);
3739
ClassDB::bind_method(D_METHOD("unregister_syntax_highlighter", "syntax_highlighter"), &ScriptEditor::unregister_syntax_highlighter);
3740
3741
ClassDB::bind_method(D_METHOD("goto_line", "line_number"), &ScriptEditor::_goto_script_line2);
3742
ClassDB::bind_method(D_METHOD("get_current_script"), &ScriptEditor::_get_current_script);
3743
ClassDB::bind_method(D_METHOD("get_open_scripts"), &ScriptEditor::_get_open_scripts);
3744
ClassDB::bind_method(D_METHOD("open_script_create_dialog", "base_name", "base_path"), &ScriptEditor::open_script_create_dialog);
3745
3746
ClassDB::bind_method(D_METHOD("goto_help", "topic"), &ScriptEditor::goto_help);
3747
ClassDB::bind_method(D_METHOD("update_docs_from_script", "script"), &ScriptEditor::update_docs_from_script);
3748
ClassDB::bind_method(D_METHOD("clear_docs_from_script", "script"), &ScriptEditor::clear_docs_from_script);
3749
3750
ClassDB::bind_method(D_METHOD("save_all_scripts"), &ScriptEditor::save_all_scripts);
3751
3752
ADD_SIGNAL(MethodInfo("editor_script_changed", PropertyInfo(Variant::OBJECT, "script", PROPERTY_HINT_RESOURCE_TYPE, Script::get_class_static())));
3753
ADD_SIGNAL(MethodInfo("script_close", PropertyInfo(Variant::OBJECT, "script", PROPERTY_HINT_RESOURCE_TYPE, Script::get_class_static())));
3754
}
3755
3756
ScriptEditor::ScriptEditor(WindowWrapper *p_wrapper) {
3757
window_wrapper = p_wrapper;
3758
3759
script_editor_cache.instantiate();
3760
script_editor_cache->load(EditorPaths::get_singleton()->get_project_settings_dir().path_join("script_editor_cache.cfg"));
3761
3762
restoring_layout = false;
3763
waiting_update_names = false;
3764
pending_auto_reload = false;
3765
auto_reload_running_scripts = true;
3766
external_editor_active = false;
3767
members_overview_enabled = EDITOR_GET("text_editor/script_list/show_members_overview");
3768
help_overview_enabled = EDITOR_GET("text_editor/help/show_help_index");
3769
3770
VBoxContainer *main_container = memnew(VBoxContainer);
3771
add_child(main_container);
3772
3773
menu_hb = memnew(HBoxContainer);
3774
main_container->add_child(menu_hb);
3775
3776
script_split = memnew(HSplitContainer);
3777
main_container->add_child(script_split);
3778
script_split->set_v_size_flags(SIZE_EXPAND_FILL);
3779
3780
#ifdef ANDROID_ENABLED
3781
virtual_keyboard_spacer = memnew(Control);
3782
virtual_keyboard_spacer->set_h_size_flags(SIZE_EXPAND_FILL);
3783
main_container->add_child(virtual_keyboard_spacer);
3784
#endif
3785
3786
list_split = memnew(VSplitContainer);
3787
script_split->add_child(list_split);
3788
list_split->set_v_size_flags(SIZE_EXPAND_FILL);
3789
3790
scripts_vbox = memnew(VBoxContainer);
3791
scripts_vbox->set_v_size_flags(SIZE_EXPAND_FILL);
3792
list_split->add_child(scripts_vbox);
3793
3794
filter_scripts = memnew(LineEdit);
3795
filter_scripts->set_placeholder(TTRC("Filter Scripts"));
3796
filter_scripts->set_accessibility_name(TTRC("Filter Scripts"));
3797
filter_scripts->set_clear_button_enabled(true);
3798
filter_scripts->connect(SceneStringName(text_changed), callable_mp(this, &ScriptEditor::_filter_scripts_text_changed));
3799
scripts_vbox->add_child(filter_scripts);
3800
3801
_sort_list_on_update = true;
3802
script_list = memnew(ItemList);
3803
script_list->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
3804
script_list->set_custom_minimum_size(Size2(100, 60) * EDSCALE); //need to give a bit of limit to avoid it from disappearing
3805
script_list->set_v_size_flags(SIZE_EXPAND_FILL);
3806
script_list->set_theme_type_variation("ItemListSecondary");
3807
script_split->set_split_offset(200 * EDSCALE);
3808
script_list->set_allow_rmb_select(true);
3809
scripts_vbox->add_child(script_list);
3810
script_list->connect("item_clicked", callable_mp(this, &ScriptEditor::_script_list_clicked), CONNECT_DEFERRED);
3811
SET_DRAG_FORWARDING_GCD(script_list, ScriptEditor);
3812
3813
context_menu = memnew(PopupMenu);
3814
add_child(context_menu);
3815
context_menu->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptEditor::_menu_option));
3816
3817
overview_vbox = memnew(VBoxContainer);
3818
overview_vbox->set_custom_minimum_size(Size2(0, 90));
3819
overview_vbox->set_v_size_flags(SIZE_EXPAND_FILL);
3820
3821
list_split->add_child(overview_vbox);
3822
list_split->set_visible(EditorSettings::get_singleton()->get_project_metadata("files_panel", "show_files_panel", true));
3823
buttons_hbox = memnew(HBoxContainer);
3824
overview_vbox->add_child(buttons_hbox);
3825
3826
filter_methods = memnew(LineEdit);
3827
filter_methods->set_placeholder(TTRC("Filter Methods"));
3828
filter_methods->set_accessibility_name(TTRC("Filter Methods"));
3829
filter_methods->set_clear_button_enabled(true);
3830
filter_methods->set_h_size_flags(SIZE_EXPAND_FILL);
3831
filter_methods->connect(SceneStringName(text_changed), callable_mp(this, &ScriptEditor::_filter_methods_text_changed));
3832
buttons_hbox->add_child(filter_methods);
3833
3834
members_overview_alphabeta_sort_button = memnew(Button);
3835
members_overview_alphabeta_sort_button->set_theme_type_variation(SceneStringName(FlatButton));
3836
members_overview_alphabeta_sort_button->set_tooltip_text(TTRC("Toggle alphabetical sorting of the method list."));
3837
members_overview_alphabeta_sort_button->set_toggle_mode(true);
3838
members_overview_alphabeta_sort_button->set_pressed(EDITOR_GET("text_editor/script_list/sort_members_outline_alphabetically"));
3839
members_overview_alphabeta_sort_button->connect(SceneStringName(toggled), callable_mp(this, &ScriptEditor::_toggle_members_overview_alpha_sort));
3840
buttons_hbox->add_child(members_overview_alphabeta_sort_button);
3841
3842
members_overview = memnew(ItemList);
3843
members_overview->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
3844
members_overview->set_theme_type_variation("ItemListSecondary");
3845
overview_vbox->add_child(members_overview);
3846
3847
members_overview->set_allow_reselect(true);
3848
members_overview->set_custom_minimum_size(Size2(0, 60) * EDSCALE); //need to give a bit of limit to avoid it from disappearing
3849
members_overview->set_v_size_flags(SIZE_EXPAND_FILL);
3850
members_overview->set_allow_rmb_select(true);
3851
3852
help_overview = memnew(ItemList);
3853
help_overview->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
3854
help_overview->set_theme_type_variation("ItemListSecondary");
3855
overview_vbox->add_child(help_overview);
3856
help_overview->set_allow_reselect(true);
3857
help_overview->set_custom_minimum_size(Size2(0, 60) * EDSCALE); //need to give a bit of limit to avoid it from disappearing
3858
help_overview->set_v_size_flags(SIZE_EXPAND_FILL);
3859
3860
VBoxContainer *code_editor_container = memnew(VBoxContainer);
3861
script_split->add_child(code_editor_container);
3862
3863
tab_container = memnew(TabContainer);
3864
tab_container->set_tabs_visible(false);
3865
tab_container->set_custom_minimum_size(Size2(200, 0) * EDSCALE);
3866
code_editor_container->add_child(tab_container);
3867
tab_container->set_h_size_flags(SIZE_EXPAND_FILL);
3868
tab_container->set_v_size_flags(SIZE_EXPAND_FILL);
3869
3870
find_replace_bar = memnew(FindReplaceBar);
3871
code_editor_container->add_child(find_replace_bar);
3872
find_replace_bar->hide();
3873
3874
ED_SHORTCUT("script_editor/window_sort", TTRC("Sort"));
3875
ED_SHORTCUT("script_editor/window_move_up", TTRC("Move Up"), KeyModifierMask::SHIFT | KeyModifierMask::ALT | Key::UP);
3876
ED_SHORTCUT("script_editor/window_move_down", TTRC("Move Down"), KeyModifierMask::SHIFT | KeyModifierMask::ALT | Key::DOWN);
3877
// FIXME: These should be `Key::GREATER` and `Key::LESS` but those don't work.
3878
ED_SHORTCUT("script_editor/next_script", TTRC("Next Script"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::PERIOD);
3879
ED_SHORTCUT("script_editor/prev_script", TTRC("Previous Script"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::COMMA);
3880
set_process_input(true);
3881
set_process_shortcut_input(true);
3882
3883
file_menu = memnew(MenuButton);
3884
file_menu->set_flat(false);
3885
file_menu->set_theme_type_variation("FlatMenuButton");
3886
file_menu->set_text(TTRC("File"));
3887
file_menu->set_switch_on_hover(true);
3888
file_menu->set_shortcut_context(this);
3889
menu_hb->add_child(file_menu);
3890
3891
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/new", TTRC("New Script..."), KeyModifierMask::CMD_OR_CTRL | Key::N), FILE_MENU_NEW_SCRIPT);
3892
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);
3893
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/open", TTRC("Open...")), FILE_MENU_OPEN);
3894
file_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_editor/reopen_closed_script"), FILE_MENU_REOPEN_CLOSED);
3895
3896
recent_scripts = memnew(PopupMenu);
3897
recent_scripts->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
3898
file_menu->get_popup()->add_submenu_node_item(TTRC("Open Recent"), recent_scripts, FILE_MENU_OPEN_RECENT);
3899
recent_scripts->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptEditor::_open_recent_script));
3900
3901
_update_recent_scripts();
3902
3903
file_menu->get_popup()->add_separator();
3904
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/save", TTRC("Save"), KeyModifierMask::ALT | KeyModifierMask::CMD_OR_CTRL | Key::S), FILE_MENU_SAVE);
3905
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/save_as", TTRC("Save As...")), FILE_MENU_SAVE_AS);
3906
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);
3907
ED_SHORTCUT_OVERRIDE("script_editor/save_all", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::S);
3908
file_menu->get_popup()->add_separator();
3909
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);
3910
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/copy_path", TTRC("Copy File Path")), FILE_MENU_COPY_PATH);
3911
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/copy_uid", TTRC("Copy File UID")), FILE_MENU_COPY_UID);
3912
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/show_in_file_system", TTRC("Show in FileSystem")), FILE_MENU_SHOW_IN_FILE_SYSTEM);
3913
file_menu->get_popup()->add_separator();
3914
3915
file_menu->get_popup()->add_shortcut(
3916
ED_SHORTCUT_ARRAY("script_editor/history_previous", TTRC("History Previous"),
3917
{ int32_t(KeyModifierMask::ALT | Key::LEFT), int32_t(Key::BACK) }),
3918
FILE_MENU_HISTORY_PREV);
3919
file_menu->get_popup()->add_shortcut(
3920
ED_SHORTCUT_ARRAY("script_editor/history_next", TTRC("History Next"),
3921
{ int32_t(KeyModifierMask::ALT | Key::RIGHT), int32_t(Key::FORWARD) }),
3922
FILE_MENU_HISTORY_NEXT);
3923
ED_SHORTCUT_OVERRIDE("script_editor/history_previous", "macos", KeyModifierMask::ALT | KeyModifierMask::META | Key::LEFT);
3924
ED_SHORTCUT_OVERRIDE("script_editor/history_next", "macos", KeyModifierMask::ALT | KeyModifierMask::META | Key::RIGHT);
3925
3926
file_menu->get_popup()->add_separator();
3927
3928
theme_submenu = memnew(PopupMenu);
3929
theme_submenu->add_shortcut(ED_SHORTCUT("script_editor/import_theme", TTRC("Import Theme...")), THEME_IMPORT);
3930
theme_submenu->add_shortcut(ED_SHORTCUT("script_editor/reload_theme", TTRC("Reload Theme")), THEME_RELOAD);
3931
file_menu->get_popup()->add_submenu_node_item(TTRC("Theme"), theme_submenu, FILE_MENU_THEME_SUBMENU);
3932
theme_submenu->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptEditor::_theme_option));
3933
3934
theme_submenu->add_separator();
3935
theme_submenu->add_shortcut(ED_SHORTCUT("script_editor/save_theme_as", TTRC("Save Theme As...")), THEME_SAVE_AS);
3936
3937
file_menu->get_popup()->add_separator();
3938
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/close_file", TTRC("Close"), KeyModifierMask::CMD_OR_CTRL | Key::W), FILE_MENU_CLOSE);
3939
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/close_all", TTRC("Close All")), FILE_MENU_CLOSE_ALL);
3940
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/close_other_tabs", TTRC("Close Other Tabs")), FILE_MENU_CLOSE_OTHER_TABS);
3941
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/close_tabs_below", TTRC("Close Tabs Below")), FILE_MENU_CLOSE_TABS_BELOW);
3942
file_menu->get_popup()->add_shortcut(ED_SHORTCUT("script_editor/close_docs", TTRC("Close Docs")), FILE_MENU_CLOSE_DOCS);
3943
3944
file_menu->get_popup()->add_separator();
3945
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);
3946
3947
file_menu->get_popup()->add_separator();
3948
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);
3949
file_menu->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptEditor::_menu_option));
3950
file_menu->get_popup()->connect("about_to_popup", callable_mp(this, &ScriptEditor::_prepare_file_menu));
3951
file_menu->get_popup()->connect("popup_hide", callable_mp(this, &ScriptEditor::_file_menu_closed));
3952
3953
script_search_menu = memnew(MenuButton);
3954
script_search_menu->set_flat(false);
3955
script_search_menu->set_theme_type_variation("FlatMenuButton");
3956
script_search_menu->set_text(TTRC("Search"));
3957
script_search_menu->set_switch_on_hover(true);
3958
script_search_menu->set_shortcut_context(this);
3959
script_search_menu->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptEditor::_menu_option));
3960
menu_hb->add_child(script_search_menu);
3961
3962
MenuButton *debug_menu_btn = memnew(MenuButton);
3963
debug_menu_btn->set_flat(false);
3964
debug_menu_btn->set_theme_type_variation("FlatMenuButton");
3965
menu_hb->add_child(debug_menu_btn);
3966
debug_menu_btn->hide(); // Handled by EditorDebuggerNode below.
3967
3968
EditorDebuggerNode *debugger = EditorDebuggerNode::get_singleton();
3969
debugger->set_script_debug_button(debug_menu_btn);
3970
debugger->connect("goto_script_line", callable_mp(this, &ScriptEditor::_goto_script_line));
3971
debugger->connect("set_execution", callable_mp(this, &ScriptEditor::_set_execution));
3972
debugger->connect("clear_execution", callable_mp(this, &ScriptEditor::_clear_execution));
3973
debugger->connect("breakpoint_set_in_tree", callable_mp(this, &ScriptEditor::_set_breakpoint));
3974
debugger->connect("breakpoints_cleared_in_tree", callable_mp(this, &ScriptEditor::_clear_breakpoints));
3975
3976
script_name_button_hbox = memnew(HBoxContainer);
3977
script_name_button_hbox->set_h_size_flags(SIZE_EXPAND_FILL);
3978
script_name_button_hbox->add_theme_constant_override("separation", 0);
3979
script_name_button_hbox->connect(SceneStringName(item_rect_changed), callable_mp(this, &ScriptEditor::_calculate_script_name_button_ratio));
3980
menu_hb->add_child(script_name_button_hbox);
3981
3982
script_name_button_left_spacer = memnew(Control);
3983
script_name_button_left_spacer->set_h_size_flags(Control::SIZE_EXPAND_FILL);
3984
script_name_button_hbox->add_child(script_name_button_left_spacer);
3985
3986
script_name_button = memnew(Button);
3987
script_name_button->set_flat(true);
3988
script_name_button->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS);
3989
script_name_button->set_h_size_flags(SIZE_EXPAND_FILL);
3990
script_name_button->set_tooltip_text(TTRC("Navigate to script list."));
3991
script_name_button->connect(SceneStringName(pressed), callable_mp(script_list, &ItemList::ensure_current_is_visible));
3992
script_name_button_hbox->add_child(script_name_button);
3993
3994
script_name_button_right_spacer = memnew(Control);
3995
script_name_button_right_spacer->set_h_size_flags(Control::SIZE_EXPAND_FILL);
3996
script_name_button_hbox->add_child(script_name_button_right_spacer);
3997
3998
site_search = memnew(Button);
3999
site_search->set_theme_type_variation(SceneStringName(FlatButton));
4000
site_search->set_accessibility_name(TTRC("Site Search"));
4001
site_search->connect(SceneStringName(pressed), callable_mp(this, &ScriptEditor::_menu_option).bind(SEARCH_WEBSITE));
4002
menu_hb->add_child(site_search);
4003
4004
help_search = memnew(Button);
4005
help_search->set_theme_type_variation(SceneStringName(FlatButton));
4006
help_search->set_text(TTRC("Search Help"));
4007
help_search->connect(SceneStringName(pressed), callable_mp(this, &ScriptEditor::_menu_option).bind(SEARCH_HELP));
4008
menu_hb->add_child(help_search);
4009
help_search->set_tooltip_text(TTRC("Search the reference documentation."));
4010
4011
menu_hb->add_child(memnew(VSeparator));
4012
4013
script_back = memnew(Button);
4014
script_back->set_theme_type_variation(SceneStringName(FlatButton));
4015
script_back->set_tooltip_text(TTRC("Go to previous edited document."));
4016
script_back->set_shortcut(ED_GET_SHORTCUT("script_editor/history_previous"));
4017
script_back->set_disabled(true);
4018
menu_hb->add_child(script_back);
4019
script_back->connect(SceneStringName(pressed), callable_mp(this, &ScriptEditor::_history_back));
4020
4021
script_forward = memnew(Button);
4022
script_forward->set_theme_type_variation(SceneStringName(FlatButton));
4023
script_forward->set_tooltip_text(TTRC("Go to next edited document."));
4024
script_forward->set_shortcut(ED_GET_SHORTCUT("script_editor/history_next"));
4025
script_forward->set_disabled(true);
4026
menu_hb->add_child(script_forward);
4027
script_forward->connect(SceneStringName(pressed), callable_mp(this, &ScriptEditor::_history_forward));
4028
4029
menu_hb->add_child(memnew(VSeparator));
4030
4031
make_floating = memnew(ScreenSelect);
4032
make_floating->set_tooltip_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
4033
make_floating->connect("request_open_in_screen", callable_mp(window_wrapper, &WindowWrapper::enable_window_on_screen).bind(true));
4034
4035
menu_hb->add_child(make_floating);
4036
p_wrapper->connect("window_visibility_changed", callable_mp(this, &ScriptEditor::_window_changed));
4037
4038
tab_container->connect("tab_changed", callable_mp(this, &ScriptEditor::_tab_changed));
4039
4040
erase_tab_confirm = memnew(ConfirmationDialog);
4041
erase_tab_confirm->set_ok_button_text(TTRC("Save"));
4042
erase_tab_confirm->add_button(TTRC("Discard"), DisplayServer::get_singleton()->get_swap_cancel_ok(), "discard");
4043
erase_tab_confirm->connect(SceneStringName(confirmed), callable_mp(this, &ScriptEditor::_close_current_tab).bind(true, true));
4044
erase_tab_confirm->connect("custom_action", callable_mp(this, &ScriptEditor::_close_discard_current_tab));
4045
add_child(erase_tab_confirm);
4046
4047
script_create_dialog = memnew(ScriptCreateDialog);
4048
script_create_dialog->set_title(TTRC("Create Script"));
4049
add_child(script_create_dialog);
4050
script_create_dialog->connect("script_created", callable_mp(this, &ScriptEditor::_script_created));
4051
4052
file_dialog_option = -1;
4053
file_dialog = memnew(EditorFileDialog);
4054
add_child(file_dialog);
4055
file_dialog->connect("file_selected", callable_mp(this, &ScriptEditor::_file_dialog_action));
4056
4057
error_dialog = memnew(AcceptDialog);
4058
add_child(error_dialog);
4059
4060
disk_changed = memnew(ConfirmationDialog);
4061
{
4062
disk_changed->set_title(TTRC("Files have been modified outside Godot"));
4063
4064
VBoxContainer *vbc = memnew(VBoxContainer);
4065
disk_changed->add_child(vbc);
4066
4067
Label *files_are_newer_label = memnew(Label);
4068
files_are_newer_label->set_text(TTRC("The following files are newer on disk:"));
4069
vbc->add_child(files_are_newer_label);
4070
4071
disk_changed_list = memnew(Tree);
4072
disk_changed_list->set_hide_root(true);
4073
disk_changed_list->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
4074
disk_changed_list->set_accessibility_name(TTRC("The following files are newer on disk:"));
4075
disk_changed_list->set_v_size_flags(SIZE_EXPAND_FILL);
4076
vbc->add_child(disk_changed_list);
4077
4078
Label *what_action_label = memnew(Label);
4079
what_action_label->set_text(TTRC("What action should be taken?"));
4080
vbc->add_child(what_action_label);
4081
4082
disk_changed->connect(SceneStringName(confirmed), callable_mp(this, &ScriptEditor::reload_scripts).bind(false));
4083
disk_changed->set_ok_button_text(TTRC("Reload from disk"));
4084
4085
disk_changed->add_button(TTRC("Ignore external changes"), !DisplayServer::get_singleton()->get_swap_cancel_ok(), "resave");
4086
disk_changed->connect("custom_action", callable_mp(this, &ScriptEditor::_resave_scripts));
4087
}
4088
4089
add_child(disk_changed);
4090
4091
script_editor = this;
4092
4093
autosave_timer = memnew(Timer);
4094
autosave_timer->set_one_shot(false);
4095
autosave_timer->connect(SceneStringName(tree_entered), callable_mp(this, &ScriptEditor::_update_autosave_timer));
4096
autosave_timer->connect("timeout", callable_mp(this, &ScriptEditor::_autosave_scripts));
4097
add_child(autosave_timer);
4098
4099
grab_focus_block = false;
4100
4101
help_search_dialog = memnew(EditorHelpSearch);
4102
add_child(help_search_dialog);
4103
help_search_dialog->connect("go_to_help", callable_mp(this, &ScriptEditor::_help_class_goto));
4104
4105
find_in_files_dialog = memnew(FindInFilesDialog);
4106
find_in_files_dialog->connect(FindInFilesDialog::SIGNAL_FIND_REQUESTED, callable_mp(this, &ScriptEditor::_start_find_in_files).bind(false));
4107
find_in_files_dialog->connect(FindInFilesDialog::SIGNAL_REPLACE_REQUESTED, callable_mp(this, &ScriptEditor::_start_find_in_files).bind(true));
4108
add_child(find_in_files_dialog);
4109
4110
find_in_files = memnew(FindInFilesContainer);
4111
EditorDockManager::get_singleton()->add_dock(find_in_files);
4112
find_in_files->close();
4113
find_in_files->connect("result_selected", callable_mp(this, &ScriptEditor::_on_find_in_files_result_selected));
4114
find_in_files->connect("files_modified", callable_mp(this, &ScriptEditor::_on_find_in_files_modified_files));
4115
4116
history_pos = -1;
4117
4118
edit_pass = 0;
4119
trim_trailing_whitespace_on_save = EDITOR_GET("text_editor/behavior/files/trim_trailing_whitespace_on_save");
4120
trim_final_newlines_on_save = EDITOR_GET("text_editor/behavior/files/trim_final_newlines_on_save");
4121
convert_indent_on_save = EDITOR_GET("text_editor/behavior/files/convert_indent_on_save");
4122
4123
Ref<EditorJSONSyntaxHighlighter> json_syntax_highlighter;
4124
json_syntax_highlighter.instantiate();
4125
register_syntax_highlighter(json_syntax_highlighter);
4126
4127
Ref<EditorMarkdownSyntaxHighlighter> markdown_syntax_highlighter;
4128
markdown_syntax_highlighter.instantiate();
4129
register_syntax_highlighter(markdown_syntax_highlighter);
4130
4131
Ref<EditorConfigFileSyntaxHighlighter> config_file_syntax_highlighter;
4132
config_file_syntax_highlighter.instantiate();
4133
register_syntax_highlighter(config_file_syntax_highlighter);
4134
4135
_update_online_doc();
4136
}
4137
4138
void ScriptEditorPlugin::_focus_another_editor() {
4139
if (window_wrapper->get_window_enabled()) {
4140
ERR_FAIL_COND(last_editor.is_empty());
4141
EditorInterface::get_singleton()->set_main_screen_editor(last_editor);
4142
}
4143
}
4144
4145
void ScriptEditorPlugin::_save_last_editor(const String &p_editor) {
4146
if (p_editor != get_plugin_name()) {
4147
last_editor = p_editor;
4148
}
4149
}
4150
4151
void ScriptEditorPlugin::_window_visibility_changed(bool p_visible) {
4152
_focus_another_editor();
4153
if (p_visible) {
4154
script_editor->add_theme_style_override(SceneStringName(panel), script_editor->get_theme_stylebox("ScriptEditorPanelFloating", EditorStringName(EditorStyles)));
4155
} else {
4156
script_editor->add_theme_style_override(SceneStringName(panel), script_editor->get_theme_stylebox("ScriptEditorPanel", EditorStringName(EditorStyles)));
4157
}
4158
}
4159
4160
void ScriptEditorPlugin::_notification(int p_what) {
4161
switch (p_what) {
4162
case NOTIFICATION_TRANSLATION_CHANGED: {
4163
window_wrapper->set_window_title(vformat(TTR("%s - Godot Engine"), TTR("Script Editor")));
4164
} break;
4165
case NOTIFICATION_ENTER_TREE: {
4166
connect("main_screen_changed", callable_mp(this, &ScriptEditorPlugin::_save_last_editor));
4167
} break;
4168
case NOTIFICATION_EXIT_TREE: {
4169
disconnect("main_screen_changed", callable_mp(this, &ScriptEditorPlugin::_save_last_editor));
4170
} break;
4171
}
4172
}
4173
4174
bool ScriptEditorPlugin::open_in_external_editor(const String &p_path, int p_line, int p_col, bool p_ignore_project) {
4175
const String path = EDITOR_GET("text_editor/external/exec_path");
4176
if (path.is_empty()) {
4177
return false;
4178
}
4179
4180
String flags = EDITOR_GET("text_editor/external/exec_flags");
4181
4182
List<String> args;
4183
bool has_file_flag = false;
4184
4185
if (!flags.is_empty()) {
4186
flags = flags.replacen("{line}", itos(MAX(p_line + 1, 1)));
4187
flags = flags.replacen("{col}", itos(p_col + 1));
4188
flags = flags.strip_edges().replace("\\\\", "\\");
4189
4190
int from = 0;
4191
int num_chars = 0;
4192
bool inside_quotes = false;
4193
4194
for (int i = 0; i < flags.size(); i++) {
4195
if (flags[i] == '"' && (!i || flags[i - 1] != '\\')) {
4196
if (!inside_quotes) {
4197
from++;
4198
}
4199
inside_quotes = !inside_quotes;
4200
4201
} else if (flags[i] == '\0' || (!inside_quotes && flags[i] == ' ')) {
4202
String arg = flags.substr(from, num_chars);
4203
if (arg.contains("{file}")) {
4204
has_file_flag = true;
4205
}
4206
4207
// Do path replacement here, else there will be issues with spaces and quotes
4208
if (p_ignore_project) {
4209
arg = arg.replacen("{project}", String());
4210
} else {
4211
arg = arg.replacen("{project}", ProjectSettings::get_singleton()->get_resource_path());
4212
}
4213
arg = arg.replacen("{file}", p_path);
4214
args.push_back(arg);
4215
4216
from = i + 1;
4217
num_chars = 0;
4218
} else {
4219
num_chars++;
4220
}
4221
}
4222
}
4223
4224
// Default to passing script path if no {file} flag is specified.
4225
if (!has_file_flag) {
4226
args.push_back(p_path);
4227
}
4228
return OS::get_singleton()->create_process(path, args) == OK;
4229
}
4230
4231
void ScriptEditorPlugin::edit(Object *p_object) {
4232
if (!p_object) {
4233
return;
4234
}
4235
4236
if (Object::cast_to<Script>(p_object)) {
4237
Script *p_script = Object::cast_to<Script>(p_object);
4238
String res_path = p_script->get_path().get_slice("::", 0);
4239
4240
if (p_script->is_built_in() && !res_path.is_empty()) {
4241
EditorNode::get_singleton()->load_scene_or_resource(res_path, false, false);
4242
}
4243
script_editor->edit(p_script);
4244
} else if (Object::cast_to<JSON>(p_object) || Object::cast_to<TextFile>(p_object)) {
4245
script_editor->edit(Object::cast_to<Resource>(p_object));
4246
}
4247
}
4248
4249
bool ScriptEditorPlugin::handles(Object *p_object) const {
4250
if (Object::cast_to<Script>(p_object)) {
4251
return true;
4252
} else if (Object::cast_to<JSON>(p_object)) {
4253
// This is here to stop resource files of class JSON from getting confused
4254
// with json files and being opened in the text editor.
4255
if (Object::cast_to<JSON>(p_object)->get_path().get_extension().to_lower() == "json") {
4256
return true;
4257
}
4258
} else if (Object::cast_to<TextFile>(p_object)) {
4259
return true;
4260
}
4261
4262
return p_object->is_class("Script");
4263
}
4264
4265
void ScriptEditorPlugin::make_visible(bool p_visible) {
4266
if (p_visible) {
4267
window_wrapper->show();
4268
script_editor->ensure_select_current();
4269
} else {
4270
window_wrapper->hide();
4271
}
4272
}
4273
4274
void ScriptEditorPlugin::selected_notify() {
4275
script_editor->ensure_select_current();
4276
_focus_another_editor();
4277
}
4278
4279
String ScriptEditorPlugin::get_unsaved_status(const String &p_for_scene) const {
4280
const PackedStringArray unsaved_scripts = script_editor->get_unsaved_scripts();
4281
if (unsaved_scripts.is_empty()) {
4282
return String();
4283
}
4284
4285
PackedStringArray message;
4286
if (!p_for_scene.is_empty()) {
4287
PackedStringArray unsaved_built_in_scripts;
4288
4289
const String scene_file = p_for_scene.get_file();
4290
for (const String &E : unsaved_scripts) {
4291
if (!E.is_resource_file() && E.contains(scene_file)) {
4292
unsaved_built_in_scripts.append(E);
4293
}
4294
}
4295
4296
if (unsaved_built_in_scripts.is_empty()) {
4297
return String();
4298
} else {
4299
message.resize(unsaved_built_in_scripts.size() + 1);
4300
message.write[0] = TTR("There are unsaved changes in the following built-in script(s):");
4301
4302
int i = 1;
4303
for (const String &E : unsaved_built_in_scripts) {
4304
message.write[i] = E.trim_suffix("(*)");
4305
i++;
4306
}
4307
return String("\n").join(message);
4308
}
4309
}
4310
4311
message.resize(unsaved_scripts.size() + 1);
4312
message.write[0] = TTR("Save changes to the following script(s) before quitting?");
4313
4314
int i = 1;
4315
for (const String &E : unsaved_scripts) {
4316
message.write[i] = E.trim_suffix("(*)");
4317
i++;
4318
}
4319
return String("\n").join(message);
4320
}
4321
4322
void ScriptEditorPlugin::save_external_data() {
4323
if (!EditorNode::get_singleton()->is_exiting()) {
4324
script_editor->save_all_scripts();
4325
}
4326
}
4327
4328
void ScriptEditorPlugin::apply_changes() {
4329
script_editor->apply_scripts();
4330
}
4331
4332
void ScriptEditorPlugin::set_window_layout(Ref<ConfigFile> p_layout) {
4333
script_editor->set_window_layout(p_layout);
4334
4335
if (EDITOR_GET("interface/multi_window/restore_windows_on_load") && window_wrapper->is_window_available() && p_layout->has_section_key("ScriptEditor", "window_rect")) {
4336
window_wrapper->restore_window_from_saved_position(
4337
p_layout->get_value("ScriptEditor", "window_rect", Rect2i()),
4338
p_layout->get_value("ScriptEditor", "window_screen", -1),
4339
p_layout->get_value("ScriptEditor", "window_screen_rect", Rect2i()));
4340
} else {
4341
window_wrapper->set_window_enabled(false);
4342
}
4343
}
4344
4345
void ScriptEditorPlugin::get_window_layout(Ref<ConfigFile> p_layout) {
4346
script_editor->get_window_layout(p_layout);
4347
4348
if (window_wrapper->get_window_enabled()) {
4349
p_layout->set_value("ScriptEditor", "window_rect", window_wrapper->get_window_rect());
4350
int screen = window_wrapper->get_window_screen();
4351
p_layout->set_value("ScriptEditor", "window_screen", screen);
4352
p_layout->set_value("ScriptEditor", "window_screen_rect", DisplayServer::get_singleton()->screen_get_usable_rect(screen));
4353
4354
} else {
4355
if (p_layout->has_section_key("ScriptEditor", "window_rect")) {
4356
p_layout->erase_section_key("ScriptEditor", "window_rect");
4357
}
4358
if (p_layout->has_section_key("ScriptEditor", "window_screen")) {
4359
p_layout->erase_section_key("ScriptEditor", "window_screen");
4360
}
4361
if (p_layout->has_section_key("ScriptEditor", "window_screen_rect")) {
4362
p_layout->erase_section_key("ScriptEditor", "window_screen_rect");
4363
}
4364
}
4365
}
4366
4367
void ScriptEditorPlugin::get_breakpoints(List<String> *p_breakpoints) {
4368
script_editor->get_breakpoints(p_breakpoints);
4369
}
4370
4371
void ScriptEditorPlugin::edited_scene_changed() {
4372
script_editor->edited_scene_changed();
4373
}
4374
4375
ScriptEditorPlugin::ScriptEditorPlugin() {
4376
ED_SHORTCUT("script_editor/reopen_closed_script", TTRC("Reopen Closed Script"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::T);
4377
ED_SHORTCUT("script_editor/clear_recent", TTRC("Clear Recent Scripts"));
4378
ED_SHORTCUT("script_editor/replace_in_files", TTRC("Replace in Files..."), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::R);
4379
4380
ED_SHORTCUT("script_text_editor/convert_to_uppercase", TTRC("Uppercase"), KeyModifierMask::SHIFT | Key::F4);
4381
ED_SHORTCUT("script_text_editor/convert_to_lowercase", TTRC("Lowercase"), KeyModifierMask::SHIFT | Key::F5);
4382
ED_SHORTCUT("script_text_editor/capitalize", TTRC("Capitalize"), KeyModifierMask::SHIFT | Key::F6);
4383
4384
window_wrapper = memnew(WindowWrapper);
4385
window_wrapper->set_margins_enabled(true);
4386
4387
script_editor = memnew(ScriptEditor(window_wrapper));
4388
Ref<Shortcut> make_floating_shortcut = ED_SHORTCUT_AND_COMMAND("script_editor/make_floating", TTRC("Make Floating"));
4389
window_wrapper->set_wrapped_control(script_editor, make_floating_shortcut);
4390
4391
EditorNode::get_singleton()->get_editor_main_screen()->get_control()->add_child(window_wrapper);
4392
window_wrapper->set_v_size_flags(Control::SIZE_EXPAND_FILL);
4393
window_wrapper->hide();
4394
window_wrapper->connect("window_visibility_changed", callable_mp(this, &ScriptEditorPlugin::_window_visibility_changed));
4395
4396
ScriptServer::set_reload_scripts_on_save(EDITOR_GET("text_editor/behavior/files/auto_reload_and_parse_scripts_on_save"));
4397
}
4398
4399