1/**************************************************************************/
2/* script_create_dialog.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_create_dialog.h"
32
33#include "core/config/project_settings.h"
34#include "core/io/file_access.h"
35#include "core/io/resource_saver.h"
36#include "core/string/string_builder.h"
37#include "editor/create_dialog.h"
38#include "editor/editor_file_system.h"
39#include "editor/editor_node.h"
40#include "editor/editor_paths.h"
41#include "editor/editor_scale.h"
42#include "editor/editor_settings.h"
43#include "editor/editor_string_names.h"
44#include "editor/gui/editor_file_dialog.h"
45#include "editor/gui/editor_validation_panel.h"
46
47static String _get_parent_class_of_script(String p_path) {
48 if (!ResourceLoader::exists(p_path, "Script")) {
49 return "Object"; // A script eventually inherits from Object.
50 }
51
52 Ref<Script> script = ResourceLoader::load(p_path, "Script");
53 ERR_FAIL_COND_V(script.is_null(), "Object");
54
55 String class_name;
56 Ref<Script> base = script->get_base_script();
57
58 // Inherits from a built-in class.
59 if (base.is_null()) {
60 script->get_language()->get_global_class_name(script->get_path(), &class_name);
61 return class_name;
62 }
63
64 // Inherits from a script that has class_name.
65 class_name = script->get_language()->get_global_class_name(base->get_path());
66 if (!class_name.is_empty()) {
67 return class_name;
68 }
69
70 // Inherits from a plain script.
71 return _get_parent_class_of_script(base->get_path());
72}
73
74static Vector<String> _get_hierarchy(String p_class_name) {
75 Vector<String> hierarchy;
76
77 String class_name = p_class_name;
78 while (true) {
79 // A registered class.
80 if (ClassDB::class_exists(class_name)) {
81 hierarchy.push_back(class_name);
82
83 class_name = ClassDB::get_parent_class(class_name);
84 continue;
85 }
86
87 // A class defined in script with class_name.
88 if (ScriptServer::is_global_class(class_name)) {
89 hierarchy.push_back(class_name);
90
91 Ref<Script> script = EditorNode::get_editor_data().script_class_load_script(class_name);
92 ERR_BREAK(script.is_null());
93 class_name = _get_parent_class_of_script(script->get_path());
94 continue;
95 }
96
97 break;
98 }
99
100 if (hierarchy.is_empty()) {
101 if (p_class_name.is_valid_identifier()) {
102 hierarchy.push_back(p_class_name);
103 }
104 hierarchy.push_back("Object");
105 }
106
107 return hierarchy;
108}
109
110void ScriptCreateDialog::_notification(int p_what) {
111 switch (p_what) {
112 case NOTIFICATION_ENTER_TREE:
113 case NOTIFICATION_THEME_CHANGED: {
114 for (int i = 0; i < ScriptServer::get_language_count(); i++) {
115 Ref<Texture2D> language_icon = get_editor_theme_icon(ScriptServer::get_language(i)->get_type());
116 if (language_icon.is_valid()) {
117 language_menu->set_item_icon(i, language_icon);
118 }
119 }
120
121 String last_language = EditorSettings::get_singleton()->get_project_metadata("script_setup", "last_selected_language", "");
122 if (!last_language.is_empty()) {
123 for (int i = 0; i < language_menu->get_item_count(); i++) {
124 if (language_menu->get_item_text(i) == last_language) {
125 language_menu->select(i);
126 current_language = i;
127 break;
128 }
129 }
130 } else {
131 language_menu->select(default_language);
132 }
133 if (EditorSettings::get_singleton()->has_meta("script_setup_use_script_templates")) {
134 is_using_templates = bool(EditorSettings::get_singleton()->get_meta("script_setup_use_script_templates"));
135 use_templates->set_pressed(is_using_templates);
136 }
137
138 path_button->set_icon(get_editor_theme_icon(SNAME("Folder")));
139 parent_browse_button->set_icon(get_editor_theme_icon(SNAME("Folder")));
140 parent_search_button->set_icon(get_editor_theme_icon(SNAME("ClassList")));
141 } break;
142 }
143}
144
145void ScriptCreateDialog::_path_hbox_sorted() {
146 if (is_visible()) {
147 int filename_start_pos = initial_bp.rfind("/") + 1;
148 int filename_end_pos = initial_bp.length();
149
150 if (!is_built_in) {
151 file_path->select(filename_start_pos, filename_end_pos);
152 }
153
154 // First set cursor to the end of line to scroll LineEdit view
155 // to the right and then set the actual cursor position.
156 file_path->set_caret_column(file_path->get_text().length());
157 file_path->set_caret_column(filename_start_pos);
158
159 file_path->grab_focus();
160 }
161}
162
163bool ScriptCreateDialog::_can_be_built_in() {
164 return (supports_built_in && built_in_enabled);
165}
166
167void ScriptCreateDialog::config(const String &p_base_name, const String &p_base_path, bool p_built_in_enabled, bool p_load_enabled) {
168 class_name->set_text("");
169 class_name->deselect();
170 parent_name->set_text(p_base_name);
171 parent_name->deselect();
172 internal_name->set_text("");
173
174 if (!p_base_path.is_empty()) {
175 initial_bp = p_base_path.get_basename();
176 file_path->set_text(initial_bp + "." + ScriptServer::get_language(language_menu->get_selected())->get_extension());
177 current_language = language_menu->get_selected();
178 } else {
179 initial_bp = "";
180 file_path->set_text("");
181 }
182 file_path->deselect();
183
184 built_in_enabled = p_built_in_enabled;
185 load_enabled = p_load_enabled;
186
187 _language_changed(current_language);
188 _class_name_changed("");
189 _path_changed(file_path->get_text());
190}
191
192void ScriptCreateDialog::set_inheritance_base_type(const String &p_base) {
193 base_type = p_base;
194}
195
196bool ScriptCreateDialog::_validate_parent(const String &p_string) {
197 if (p_string.length() == 0) {
198 return false;
199 }
200
201 if (can_inherit_from_file && p_string.is_quoted()) {
202 String p = p_string.substr(1, p_string.length() - 2);
203 if (_validate_path(p, true) == "") {
204 return true;
205 }
206 }
207
208 return EditorNode::get_editor_data().is_type_recognized(p_string);
209}
210
211bool ScriptCreateDialog::_validate_class(const String &p_string) {
212 if (p_string.length() == 0) {
213 return false;
214 }
215
216 for (int i = 0; i < p_string.length(); i++) {
217 if (i == 0) {
218 // Cannot start with a number.
219 if (p_string[0] >= '0' && p_string[0] <= '9') {
220 return false;
221 }
222 }
223
224 bool valid_char = is_ascii_identifier_char(p_string[i]) || p_string[i] == '.';
225
226 if (!valid_char) {
227 return false;
228 }
229 }
230
231 return true;
232}
233
234String ScriptCreateDialog::_validate_path(const String &p_path, bool p_file_must_exist) {
235 String p = p_path.strip_edges();
236
237 if (p.is_empty()) {
238 return TTR("Path is empty.");
239 }
240 if (p.get_file().get_basename().is_empty()) {
241 return TTR("Filename is empty.");
242 }
243
244 if (!p.get_file().get_basename().is_valid_filename()) {
245 return TTR("Filename is invalid.");
246 }
247 if (p.get_file().begins_with(".")) {
248 return TTR("Name begins with a dot.");
249 }
250
251 p = ProjectSettings::get_singleton()->localize_path(p);
252 if (!p.begins_with("res://")) {
253 return TTR("Path is not local.");
254 }
255
256 {
257 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
258 if (da->change_dir(p.get_base_dir()) != OK) {
259 return TTR("Base path is invalid.");
260 }
261 }
262
263 {
264 // Check if file exists.
265 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
266 if (da->dir_exists(p)) {
267 return TTR("A directory with the same name exists.");
268 } else if (p_file_must_exist && !da->file_exists(p)) {
269 return TTR("File does not exist.");
270 }
271 }
272
273 // Check file extension.
274 String extension = p.get_extension();
275 List<String> extensions;
276
277 // Get all possible extensions for script.
278 for (int l = 0; l < language_menu->get_item_count(); l++) {
279 ScriptServer::get_language(l)->get_recognized_extensions(&extensions);
280 }
281
282 bool found = false;
283 bool match = false;
284 for (const String &E : extensions) {
285 if (E.nocasecmp_to(extension) == 0) {
286 found = true;
287 if (E == ScriptServer::get_language(language_menu->get_selected())->get_extension()) {
288 match = true;
289 }
290 break;
291 }
292 }
293
294 if (!found) {
295 return TTR("Invalid extension.");
296 }
297 if (!match) {
298 return TTR("Extension doesn't match chosen language.");
299 }
300
301 // Let ScriptLanguage do custom validation.
302 return ScriptServer::get_language(language_menu->get_selected())->validate_path(p);
303}
304
305String ScriptCreateDialog::_get_class_name() const {
306 if (has_named_classes) {
307 return class_name->get_text();
308 } else {
309 return ProjectSettings::get_singleton()->localize_path(file_path->get_text()).get_file().get_basename();
310 }
311}
312
313void ScriptCreateDialog::_class_name_changed(const String &p_name) {
314 is_class_name_valid = _validate_class(class_name->get_text());
315 validation_panel->update();
316}
317
318void ScriptCreateDialog::_parent_name_changed(const String &p_parent) {
319 is_parent_name_valid = _validate_parent(parent_name->get_text());
320 validation_panel->update();
321}
322
323void ScriptCreateDialog::_template_changed(int p_template) {
324 const ScriptLanguage::ScriptTemplate &sinfo = _get_current_template();
325 // Update last used dictionaries
326 if (is_using_templates && !parent_name->get_text().begins_with("\"res:")) {
327 if (sinfo.origin == ScriptLanguage::TemplateLocation::TEMPLATE_PROJECT) {
328 // Save the last used template for this node into the project dictionary.
329 Dictionary dic_templates_project = EditorSettings::get_singleton()->get_project_metadata("script_setup", "templates_dictionary", Dictionary());
330 dic_templates_project[parent_name->get_text()] = sinfo.get_hash();
331 EditorSettings::get_singleton()->set_project_metadata("script_setup", "templates_dictionary", dic_templates_project);
332 } else {
333 // Save template info to editor dictionary (not a project template).
334 Dictionary dic_templates;
335 if (EditorSettings::get_singleton()->has_meta("script_setup_templates_dictionary")) {
336 dic_templates = (Dictionary)EditorSettings::get_singleton()->get_meta("script_setup_templates_dictionary");
337 }
338 dic_templates[parent_name->get_text()] = sinfo.get_hash();
339 EditorSettings::get_singleton()->set_meta("script_setup_templates_dictionary", dic_templates);
340 // Remove template from project dictionary as we last used an editor level template.
341 Dictionary dic_templates_project = EditorSettings::get_singleton()->get_project_metadata("script_setup", "templates_dictionary", Dictionary());
342 if (dic_templates_project.has(parent_name->get_text())) {
343 dic_templates_project.erase(parent_name->get_text());
344 EditorSettings::get_singleton()->set_project_metadata("script_setup", "templates_dictionary", dic_templates_project);
345 }
346 }
347 }
348
349 // Update template label information.
350 String template_info = U"• ";
351 template_info += TTR("Template:");
352 template_info += " " + sinfo.name;
353 if (!sinfo.description.is_empty()) {
354 template_info += " - " + sinfo.description;
355 }
356 validation_panel->set_message(MSG_ID_TEMPLATE, template_info, EditorValidationPanel::MSG_INFO, false);
357}
358
359void ScriptCreateDialog::ok_pressed() {
360 if (is_new_script_created) {
361 _create_new();
362 } else {
363 _load_exist();
364 }
365
366 EditorSettings::get_singleton()->save();
367 is_new_script_created = true;
368 validation_panel->update();
369}
370
371void ScriptCreateDialog::_create_new() {
372 String cname_param = _get_class_name();
373
374 Ref<Script> scr;
375
376 const ScriptLanguage::ScriptTemplate sinfo = _get_current_template();
377
378 String parent_class = parent_name->get_text();
379 if (!parent_name->get_text().is_quoted() && !ClassDB::class_exists(parent_class) && !ScriptServer::is_global_class(parent_class)) {
380 // If base is a custom type, replace with script path instead.
381 const EditorData::CustomType *type = EditorNode::get_editor_data().get_custom_type_by_name(parent_class);
382 ERR_FAIL_NULL(type);
383 parent_class = "\"" + type->script->get_path() + "\"";
384 }
385
386 scr = ScriptServer::get_language(language_menu->get_selected())->make_template(sinfo.content, cname_param, parent_class);
387
388 if (has_named_classes) {
389 String cname = class_name->get_text();
390 if (cname.length()) {
391 scr->set_name(cname);
392 }
393 }
394
395 if (is_built_in) {
396 scr->set_name(internal_name->get_text());
397 // Make sure the script is compiled to make its type recognizable.
398 scr->reload();
399 } else {
400 String lpath = ProjectSettings::get_singleton()->localize_path(file_path->get_text());
401 scr->set_path(lpath);
402 Error err = ResourceSaver::save(scr, lpath, ResourceSaver::FLAG_CHANGE_PATH);
403 if (err != OK) {
404 alert->set_text(TTR("Error - Could not create script in filesystem."));
405 alert->popup_centered();
406 return;
407 }
408 }
409
410 emit_signal(SNAME("script_created"), scr);
411 hide();
412}
413
414void ScriptCreateDialog::_load_exist() {
415 String path = file_path->get_text();
416 Ref<Resource> p_script = ResourceLoader::load(path, "Script");
417 if (p_script.is_null()) {
418 alert->set_text(vformat(TTR("Error loading script from %s"), path));
419 alert->popup_centered();
420 return;
421 }
422
423 emit_signal(SNAME("script_created"), p_script);
424 hide();
425}
426
427void ScriptCreateDialog::_language_changed(int l) {
428 language = ScriptServer::get_language(l);
429
430 has_named_classes = language->has_named_classes();
431 can_inherit_from_file = language->can_inherit_from_file();
432 supports_built_in = language->supports_builtin_mode();
433 if (!supports_built_in) {
434 is_built_in = false;
435 }
436
437 String selected_ext = "." + language->get_extension();
438 String path = file_path->get_text();
439 String extension = "";
440 if (!path.is_empty()) {
441 if (path.contains(".")) {
442 extension = path.get_extension();
443 }
444
445 if (extension.length() == 0) {
446 // Add extension if none.
447 path += selected_ext;
448 _path_changed(path);
449 } else {
450 // Change extension by selected language.
451 List<String> extensions;
452 // Get all possible extensions for script.
453 for (int m = 0; m < language_menu->get_item_count(); m++) {
454 ScriptServer::get_language(m)->get_recognized_extensions(&extensions);
455 }
456
457 for (const String &E : extensions) {
458 if (E.nocasecmp_to(extension) == 0) {
459 path = path.get_basename() + selected_ext;
460 _path_changed(path);
461 break;
462 }
463 }
464 }
465 } else {
466 path = "class" + selected_ext;
467 _path_changed(path);
468 }
469 file_path->set_text(path);
470
471 EditorSettings::get_singleton()->set_project_metadata("script_setup", "last_selected_language", language_menu->get_item_text(language_menu->get_selected()));
472
473 _parent_name_changed(parent_name->get_text());
474 validation_panel->update();
475}
476
477void ScriptCreateDialog::_built_in_pressed() {
478 if (internal->is_pressed()) {
479 is_built_in = true;
480 is_new_script_created = true;
481 } else {
482 is_built_in = false;
483 _path_changed(file_path->get_text());
484 }
485 validation_panel->update();
486}
487
488void ScriptCreateDialog::_use_template_pressed() {
489 is_using_templates = use_templates->is_pressed();
490 EditorSettings::get_singleton()->set_meta("script_setup_use_script_templates", is_using_templates);
491 validation_panel->update();
492}
493
494void ScriptCreateDialog::_browse_path(bool browse_parent, bool p_save) {
495 is_browsing_parent = browse_parent;
496
497 if (p_save) {
498 file_browse->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE);
499 file_browse->set_title(TTR("Open Script / Choose Location"));
500 file_browse->set_ok_button_text(TTR("Open"));
501 } else {
502 file_browse->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE);
503 file_browse->set_title(TTR("Open Script"));
504 }
505
506 file_browse->set_disable_overwrite_warning(true);
507 file_browse->clear_filters();
508 List<String> extensions;
509
510 int lang = language_menu->get_selected();
511 ScriptServer::get_language(lang)->get_recognized_extensions(&extensions);
512
513 for (const String &E : extensions) {
514 file_browse->add_filter("*." + E);
515 }
516
517 file_browse->set_current_path(file_path->get_text());
518 file_browse->popup_file_dialog();
519}
520
521void ScriptCreateDialog::_file_selected(const String &p_file) {
522 String path = ProjectSettings::get_singleton()->localize_path(p_file);
523 if (is_browsing_parent) {
524 parent_name->set_text("\"" + path + "\"");
525 _parent_name_changed(parent_name->get_text());
526 } else {
527 file_path->set_text(path);
528 _path_changed(path);
529
530 String filename = path.get_file().get_basename();
531 int select_start = path.rfind(filename);
532 file_path->select(select_start, select_start + filename.length());
533 file_path->set_caret_column(select_start + filename.length());
534 file_path->grab_focus();
535 }
536}
537
538void ScriptCreateDialog::_create() {
539 parent_name->set_text(select_class->get_selected_type().split(" ")[0]);
540 _parent_name_changed(parent_name->get_text());
541}
542
543void ScriptCreateDialog::_browse_class_in_tree() {
544 select_class->set_base_type(base_type);
545 select_class->popup_create(true);
546 select_class->set_title(vformat(TTR("Inherit %s"), base_type));
547 select_class->set_ok_button_text(TTR("Inherit"));
548}
549
550void ScriptCreateDialog::_path_changed(const String &p_path) {
551 if (is_built_in) {
552 return;
553 }
554
555 is_path_valid = false;
556 is_new_script_created = true;
557
558 path_error = _validate_path(p_path, false);
559 if (!path_error.is_empty()) {
560 validation_panel->update();
561 return;
562 }
563
564 // Check if file exists.
565 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
566 String p = ProjectSettings::get_singleton()->localize_path(p_path.strip_edges());
567 if (da->file_exists(p)) {
568 is_new_script_created = false;
569 }
570
571 is_path_valid = true;
572 validation_panel->update();
573}
574
575void ScriptCreateDialog::_path_submitted(const String &p_path) {
576 if (!get_ok_button()->is_disabled()) {
577 ok_pressed();
578 }
579}
580
581void ScriptCreateDialog::_update_template_menu() {
582 bool is_language_using_templates = language->is_using_templates();
583 template_menu->set_disabled(false);
584 template_menu->clear();
585 template_list.clear();
586
587 if (is_language_using_templates) {
588 // Get the latest templates used for each type of node from project settings then global settings.
589 Dictionary last_local_templates = EditorSettings::get_singleton()->get_project_metadata("script_setup", "templates_dictionary", Dictionary());
590 Dictionary last_global_templates;
591 if (EditorSettings::get_singleton()->has_meta("script_setup_templates_dictionary")) {
592 last_global_templates = (Dictionary)EditorSettings::get_singleton()->get_meta("script_setup_templates_dictionary");
593 }
594 String inherits_base_type = parent_name->get_text();
595
596 // If it inherits from a script, get its parent class first.
597 if (inherits_base_type[0] == '"') {
598 inherits_base_type = _get_parent_class_of_script(inherits_base_type.unquote());
599 }
600
601 // Get all ancestor node for selected base node.
602 // There templates will also fit the base node.
603 Vector<String> hierarchy = _get_hierarchy(inherits_base_type);
604 int last_used_template = -1;
605 int preselected_template = -1;
606 int previous_ancestor_level = -1;
607
608 // Templates can be stored in tree different locations.
609 Vector<ScriptLanguage::TemplateLocation> template_locations;
610 template_locations.append(ScriptLanguage::TEMPLATE_PROJECT);
611 template_locations.append(ScriptLanguage::TEMPLATE_EDITOR);
612 template_locations.append(ScriptLanguage::TEMPLATE_BUILT_IN);
613
614 for (const ScriptLanguage::TemplateLocation &template_location : template_locations) {
615 String display_name = _get_script_origin_label(template_location);
616 bool separator = false;
617 int ancestor_level = 0;
618 for (const String &current_node : hierarchy) {
619 Vector<ScriptLanguage::ScriptTemplate> templates_found;
620 if (template_location == ScriptLanguage::TEMPLATE_BUILT_IN) {
621 templates_found = language->get_built_in_templates(current_node);
622 } else {
623 String template_directory;
624 if (template_location == ScriptLanguage::TEMPLATE_PROJECT) {
625 template_directory = EditorPaths::get_singleton()->get_project_script_templates_dir();
626 } else {
627 template_directory = EditorPaths::get_singleton()->get_script_templates_dir();
628 }
629 templates_found = _get_user_templates(language, current_node, template_directory, template_location);
630 }
631 if (!templates_found.is_empty()) {
632 if (!separator) {
633 template_menu->add_separator();
634 template_menu->set_item_text(-1, display_name);
635 separator = true;
636 }
637 for (ScriptLanguage::ScriptTemplate &t : templates_found) {
638 template_menu->add_item(t.inherit + ": " + t.name);
639 int id = template_menu->get_item_count() - 1;
640 // Check if this template should be preselected if node isn't in the last used dictionary.
641 if (ancestor_level < previous_ancestor_level || previous_ancestor_level == -1) {
642 previous_ancestor_level = ancestor_level;
643 preselected_template = id;
644 }
645 // Check for last used template for this node in project settings then in global settings.
646 if (last_local_templates.has(parent_name->get_text()) && t.get_hash() == String(last_local_templates[parent_name->get_text()])) {
647 last_used_template = id;
648 } else if (last_used_template == -1 && last_global_templates.has(parent_name->get_text()) && t.get_hash() == String(last_global_templates[parent_name->get_text()])) {
649 last_used_template = id;
650 }
651 t.id = id;
652 template_list.push_back(t);
653 String icon = has_theme_icon(t.inherit, EditorStringName(EditorIcons)) ? t.inherit : "Object";
654 template_menu->set_item_icon(id, get_editor_theme_icon(icon));
655 }
656 }
657 ancestor_level++;
658 }
659 }
660
661 if (last_used_template != -1) {
662 template_menu->select(last_used_template);
663 } else if (preselected_template != -1) {
664 template_menu->select(preselected_template);
665 }
666 }
667 _template_changed(template_menu->get_selected());
668}
669
670void ScriptCreateDialog::_update_dialog() {
671 // "Add Script Dialog" GUI logic and script checks.
672 _update_template_menu();
673
674 // Is script path/name valid (order from top to bottom)?
675
676 if (!is_built_in && !is_path_valid) {
677 validation_panel->set_message(MSG_ID_SCRIPT, TTR("Invalid path."), EditorValidationPanel::MSG_ERROR);
678 }
679 if (has_named_classes && (is_new_script_created && !is_class_name_valid)) {
680 validation_panel->set_message(MSG_ID_SCRIPT, TTR("Invalid class name."), EditorValidationPanel::MSG_ERROR);
681 }
682 if (!is_parent_name_valid && is_new_script_created) {
683 validation_panel->set_message(MSG_ID_SCRIPT, TTR("Invalid inherited parent name or path."), EditorValidationPanel::MSG_ERROR);
684 }
685
686 if (validation_panel->is_valid() && !is_new_script_created) {
687 validation_panel->set_message(MSG_ID_SCRIPT, TTR("File exists, it will be reused."), EditorValidationPanel::MSG_OK);
688 }
689
690 if (!path_error.is_empty()) {
691 validation_panel->set_message(MSG_ID_PATH, path_error, EditorValidationPanel::MSG_ERROR);
692 }
693
694 // Does script have named classes?
695
696 if (has_named_classes) {
697 if (is_new_script_created) {
698 class_name->set_editable(true);
699 class_name->set_placeholder(TTR("Allowed: a-z, A-Z, 0-9, _ and ."));
700 Color placeholder_color = class_name->get_theme_color(SNAME("font_placeholder_color"));
701 placeholder_color.a = 0.3;
702 class_name->add_theme_color_override("font_placeholder_color", placeholder_color);
703 } else {
704 class_name->set_editable(false);
705 }
706 } else {
707 class_name->set_editable(false);
708 class_name->set_placeholder(TTR("N/A"));
709 Color placeholder_color = class_name->get_theme_color(SNAME("font_placeholder_color"));
710 placeholder_color.a = 1;
711 class_name->add_theme_color_override("font_placeholder_color", placeholder_color);
712 class_name->set_text("");
713 }
714
715 // Is script Built-in?
716
717 if (is_built_in) {
718 file_path->set_editable(false);
719 path_button->set_disabled(true);
720 re_check_path = true;
721 } else {
722 file_path->set_editable(true);
723 path_button->set_disabled(false);
724 if (re_check_path) {
725 re_check_path = false;
726 _path_changed(file_path->get_text());
727 }
728 }
729
730 if (!_can_be_built_in()) {
731 internal->set_pressed(false);
732 }
733 internal->set_disabled(!_can_be_built_in());
734
735 // Is Script created or loaded from existing file?
736
737 if (is_built_in) {
738 validation_panel->set_message(MSG_ID_BUILT_IN, TTR("Note: Built-in scripts have some limitations and can't be edited using an external editor."), EditorValidationPanel::MSG_INFO, false);
739 } else if (_get_class_name() == parent_name->get_text()) {
740 validation_panel->set_message(MSG_ID_BUILT_IN, TTR("Warning: Having the script name be the same as a built-in type is usually not desired."), EditorValidationPanel::MSG_WARNING, false);
741 }
742
743 path_controls[0]->set_visible(!is_built_in);
744 path_controls[1]->set_visible(!is_built_in);
745 name_controls[0]->set_visible(is_built_in);
746 name_controls[1]->set_visible(is_built_in);
747
748 // Check if the script name is the same as the parent class.
749 // This warning isn't relevant if the script is built-in.
750
751 bool is_new_file = is_built_in || is_new_script_created;
752
753 parent_name->set_editable(is_new_file);
754 parent_search_button->set_disabled(!is_new_file);
755 parent_browse_button->set_disabled(!is_new_file || !can_inherit_from_file);
756 template_inactive_message = "";
757 String button_text = is_new_file ? TTR("Create") : TTR("Load");
758 set_ok_button_text(button_text);
759
760 if (is_new_file) {
761 if (is_built_in) {
762 validation_panel->set_message(MSG_ID_PATH, TTR("Built-in script (into scene file)."), EditorValidationPanel::MSG_OK);
763 }
764 } else {
765 template_inactive_message = TTR("Using existing script file.");
766 if (load_enabled) {
767 if (is_path_valid) {
768 validation_panel->set_message(MSG_ID_PATH, TTR("Will load an existing script file."), EditorValidationPanel::MSG_OK);
769 }
770 } else {
771 validation_panel->set_message(MSG_ID_PATH, TTR("Script file already exists."), EditorValidationPanel::MSG_ERROR);
772 }
773 }
774
775 // Show templates list if needed.
776 if (is_using_templates) {
777 // Check if at least one suitable template has been found.
778 if (template_menu->get_item_count() == 0 && template_inactive_message.is_empty()) {
779 template_inactive_message = TTR("No suitable template.");
780 }
781 } else {
782 template_inactive_message = TTR("Empty");
783 }
784
785 if (!template_inactive_message.is_empty()) {
786 template_menu->set_disabled(true);
787 template_menu->clear();
788 template_menu->add_item(template_inactive_message);
789 validation_panel->set_message(MSG_ID_TEMPLATE, "", EditorValidationPanel::MSG_INFO);
790 }
791}
792
793ScriptLanguage::ScriptTemplate ScriptCreateDialog::_get_current_template() const {
794 int selected_index = template_menu->get_selected();
795 for (const ScriptLanguage::ScriptTemplate &t : template_list) {
796 if (is_using_templates) {
797 if (t.id == selected_index) {
798 return t;
799 }
800 } else {
801 // Using empty built-in template if templates are disabled.
802 if (t.origin == ScriptLanguage::TemplateLocation::TEMPLATE_BUILT_IN && t.name == "Empty") {
803 return t;
804 }
805 }
806 }
807 return ScriptLanguage::ScriptTemplate();
808}
809
810Vector<ScriptLanguage::ScriptTemplate> ScriptCreateDialog::_get_user_templates(const ScriptLanguage *p_language, const StringName &p_object, const String &p_dir, const ScriptLanguage::TemplateLocation &p_origin) const {
811 Vector<ScriptLanguage::ScriptTemplate> user_templates;
812 String extension = p_language->get_extension();
813
814 String dir_path = p_dir.path_join(p_object);
815
816 Ref<DirAccess> d = DirAccess::open(dir_path);
817 if (d.is_valid()) {
818 d->list_dir_begin();
819 String file = d->get_next();
820 while (file != String()) {
821 if (file.get_extension() == extension) {
822 user_templates.append(_parse_template(p_language, dir_path, file, p_origin, p_object));
823 }
824 file = d->get_next();
825 }
826 d->list_dir_end();
827 }
828 return user_templates;
829}
830
831ScriptLanguage::ScriptTemplate ScriptCreateDialog::_parse_template(const ScriptLanguage *p_language, const String &p_path, const String &p_filename, const ScriptLanguage::TemplateLocation &p_origin, const String &p_inherits) const {
832 ScriptLanguage::ScriptTemplate script_template = ScriptLanguage::ScriptTemplate();
833 script_template.origin = p_origin;
834 script_template.inherit = p_inherits;
835 int space_indent_size = 4;
836 // Get meta delimiter
837 String meta_delimiter;
838 List<String> comment_delimiters;
839 p_language->get_comment_delimiters(&comment_delimiters);
840 for (const String &script_delimiter : comment_delimiters) {
841 if (!script_delimiter.contains(" ")) {
842 meta_delimiter = script_delimiter;
843 break;
844 }
845 }
846 String meta_prefix = meta_delimiter + " meta-";
847
848 // Parse file for meta-information and script content
849 Error err;
850 Ref<FileAccess> file = FileAccess::open(p_path.path_join(p_filename), FileAccess::READ, &err);
851 if (!err) {
852 while (!file->eof_reached()) {
853 String line = file->get_line();
854 if (line.begins_with(meta_prefix)) {
855 // Store meta information
856 line = line.substr(meta_prefix.length());
857 if (line.begins_with("name:")) {
858 script_template.name = line.substr(5).strip_edges();
859 } else if (line.begins_with("description:")) {
860 script_template.description = line.substr(12).strip_edges();
861 } else if (line.begins_with("space-indent:")) {
862 String indent_value = line.substr(13).strip_edges();
863 if (indent_value.is_valid_int()) {
864 int indent_size = indent_value.to_int();
865 if (indent_size >= 0) {
866 space_indent_size = indent_size;
867 } else {
868 WARN_PRINT(vformat("Template meta-space-indent need to be a non-negative integer value. Found %s.", indent_value));
869 }
870 } else {
871 WARN_PRINT(vformat("Template meta-space-indent need to be a valid integer value. Found %s.", indent_value));
872 }
873 }
874 } else {
875 // Replace indentation.
876 int i = 0;
877 int space_count = 0;
878 for (; i < line.length(); i++) {
879 if (line[i] == '\t') {
880 if (space_count) {
881 script_template.content += String(" ").repeat(space_count);
882 space_count = 0;
883 }
884 script_template.content += "_TS_";
885 } else if (line[i] == ' ') {
886 space_count++;
887 if (space_count == space_indent_size) {
888 script_template.content += "_TS_";
889 space_count = 0;
890 }
891 } else {
892 break;
893 }
894 }
895 if (space_count) {
896 script_template.content += String(" ").repeat(space_count);
897 }
898 script_template.content += line.substr(i) + "\n";
899 }
900 }
901 }
902
903 script_template.content = script_template.content.lstrip("\n");
904
905 // Get name from file name if no name in meta information
906 if (script_template.name == String()) {
907 script_template.name = p_filename.get_basename().capitalize();
908 }
909
910 return script_template;
911}
912
913String ScriptCreateDialog::_get_script_origin_label(const ScriptLanguage::TemplateLocation &p_origin) const {
914 switch (p_origin) {
915 case ScriptLanguage::TEMPLATE_BUILT_IN:
916 return TTR("Built-in");
917 case ScriptLanguage::TEMPLATE_EDITOR:
918 return TTR("Editor");
919 case ScriptLanguage::TEMPLATE_PROJECT:
920 return TTR("Project");
921 }
922 return "";
923}
924
925void ScriptCreateDialog::_bind_methods() {
926 ClassDB::bind_method(D_METHOD("config", "inherits", "path", "built_in_enabled", "load_enabled"), &ScriptCreateDialog::config, DEFVAL(true), DEFVAL(true));
927
928 ADD_SIGNAL(MethodInfo("script_created", PropertyInfo(Variant::OBJECT, "script", PROPERTY_HINT_RESOURCE_TYPE, "Script")));
929}
930
931ScriptCreateDialog::ScriptCreateDialog() {
932 /* Main Controls */
933
934 GridContainer *gc = memnew(GridContainer);
935 gc->set_columns(2);
936
937 /* Information Messages Field */
938
939 validation_panel = memnew(EditorValidationPanel);
940 validation_panel->add_line(MSG_ID_SCRIPT, TTR("Script path/name is valid."));
941 validation_panel->add_line(MSG_ID_PATH, TTR("Will create a new script file."));
942 validation_panel->add_line(MSG_ID_BUILT_IN);
943 validation_panel->add_line(MSG_ID_TEMPLATE);
944 validation_panel->set_update_callback(callable_mp(this, &ScriptCreateDialog::_update_dialog));
945 validation_panel->set_accept_button(get_ok_button());
946
947 /* Spacing */
948
949 Control *spacing = memnew(Control);
950 spacing->set_custom_minimum_size(Size2(0, 10 * EDSCALE));
951
952 VBoxContainer *vb = memnew(VBoxContainer);
953 vb->add_child(gc);
954 vb->add_child(spacing);
955 vb->add_child(validation_panel);
956 add_child(vb);
957
958 /* Language */
959
960 language_menu = memnew(OptionButton);
961 language_menu->set_custom_minimum_size(Size2(350, 0) * EDSCALE);
962 language_menu->set_h_size_flags(Control::SIZE_EXPAND_FILL);
963 gc->add_child(memnew(Label(TTR("Language:"))));
964 gc->add_child(language_menu);
965
966 default_language = -1;
967 for (int i = 0; i < ScriptServer::get_language_count(); i++) {
968 String lang = ScriptServer::get_language(i)->get_name();
969 language_menu->add_item(lang);
970 if (lang == "GDScript") {
971 default_language = i;
972 }
973 }
974 if (default_language >= 0) {
975 language_menu->select(default_language);
976 }
977 current_language = default_language;
978
979 language_menu->connect("item_selected", callable_mp(this, &ScriptCreateDialog::_language_changed));
980
981 /* Inherits */
982
983 base_type = "Object";
984
985 HBoxContainer *hb = memnew(HBoxContainer);
986 hb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
987 parent_name = memnew(LineEdit);
988 parent_name->connect("text_changed", callable_mp(this, &ScriptCreateDialog::_parent_name_changed));
989 parent_name->set_h_size_flags(Control::SIZE_EXPAND_FILL);
990 hb->add_child(parent_name);
991 parent_search_button = memnew(Button);
992 parent_search_button->connect("pressed", callable_mp(this, &ScriptCreateDialog::_browse_class_in_tree));
993 hb->add_child(parent_search_button);
994 parent_browse_button = memnew(Button);
995 parent_browse_button->connect("pressed", callable_mp(this, &ScriptCreateDialog::_browse_path).bind(true, false));
996 hb->add_child(parent_browse_button);
997 gc->add_child(memnew(Label(TTR("Inherits:"))));
998 gc->add_child(hb);
999
1000 /* Class Name */
1001
1002 class_name = memnew(LineEdit);
1003 class_name->connect("text_changed", callable_mp(this, &ScriptCreateDialog::_class_name_changed));
1004 class_name->set_h_size_flags(Control::SIZE_EXPAND_FILL);
1005 gc->add_child(memnew(Label(TTR("Class Name:"))));
1006 gc->add_child(class_name);
1007
1008 /* Templates */
1009 gc->add_child(memnew(Label(TTR("Template:"))));
1010 HBoxContainer *template_hb = memnew(HBoxContainer);
1011 template_hb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
1012
1013 use_templates = memnew(CheckBox);
1014 use_templates->set_pressed(is_using_templates);
1015 use_templates->connect("pressed", callable_mp(this, &ScriptCreateDialog::_use_template_pressed));
1016 template_hb->add_child(use_templates);
1017
1018 template_inactive_message = "";
1019
1020 template_menu = memnew(OptionButton);
1021 template_menu->set_h_size_flags(Control::SIZE_EXPAND_FILL);
1022 template_menu->connect("item_selected", callable_mp(this, &ScriptCreateDialog::_template_changed));
1023 template_hb->add_child(template_menu);
1024
1025 gc->add_child(template_hb);
1026
1027 /* Built-in Script */
1028
1029 internal = memnew(CheckBox);
1030 internal->set_text(TTR("On"));
1031 internal->connect("pressed", callable_mp(this, &ScriptCreateDialog::_built_in_pressed));
1032 gc->add_child(memnew(Label(TTR("Built-in Script:"))));
1033 gc->add_child(internal);
1034
1035 /* Path */
1036
1037 hb = memnew(HBoxContainer);
1038 hb->connect("sort_children", callable_mp(this, &ScriptCreateDialog::_path_hbox_sorted));
1039 file_path = memnew(LineEdit);
1040 file_path->connect("text_changed", callable_mp(this, &ScriptCreateDialog::_path_changed));
1041 file_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
1042 hb->add_child(file_path);
1043 path_button = memnew(Button);
1044 path_button->connect("pressed", callable_mp(this, &ScriptCreateDialog::_browse_path).bind(false, true));
1045 hb->add_child(path_button);
1046 Label *label = memnew(Label(TTR("Path:")));
1047 gc->add_child(label);
1048 gc->add_child(hb);
1049 path_controls[0] = label;
1050 path_controls[1] = hb;
1051
1052 /* Name */
1053
1054 internal_name = memnew(LineEdit);
1055 internal_name->set_h_size_flags(Control::SIZE_EXPAND_FILL);
1056 internal_name->connect("text_submitted", callable_mp(this, &ScriptCreateDialog::_path_submitted));
1057 label = memnew(Label(TTR("Name:")));
1058 gc->add_child(label);
1059 gc->add_child(internal_name);
1060 name_controls[0] = label;
1061 name_controls[1] = internal_name;
1062 label->hide();
1063 internal_name->hide();
1064
1065 /* Dialog Setup */
1066
1067 select_class = memnew(CreateDialog);
1068 select_class->connect("create", callable_mp(this, &ScriptCreateDialog::_create));
1069 add_child(select_class);
1070
1071 file_browse = memnew(EditorFileDialog);
1072 file_browse->connect("file_selected", callable_mp(this, &ScriptCreateDialog::_file_selected));
1073 file_browse->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE);
1074 add_child(file_browse);
1075 set_ok_button_text(TTR("Create"));
1076 alert = memnew(AcceptDialog);
1077 alert->get_label()->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);
1078 alert->get_label()->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
1079 alert->get_label()->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);
1080 alert->get_label()->set_custom_minimum_size(Size2(325, 60) * EDSCALE);
1081 add_child(alert);
1082
1083 set_hide_on_ok(false);
1084 set_title(TTR("Attach Node Script"));
1085}
1086