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 | |
47 | static 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 | |
74 | static 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 | |
110 | void 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 | |
145 | void 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 | |
163 | bool ScriptCreateDialog::_can_be_built_in() { |
164 | return (supports_built_in && built_in_enabled); |
165 | } |
166 | |
167 | void 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 | |
192 | void ScriptCreateDialog::set_inheritance_base_type(const String &p_base) { |
193 | base_type = p_base; |
194 | } |
195 | |
196 | bool 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 | |
211 | bool 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 | |
234 | String 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 | |
305 | String 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 | |
313 | void 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 | |
318 | void 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 | |
323 | void 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 | |
359 | void 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 | |
371 | void 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 | |
414 | void 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 | |
427 | void 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 | |
477 | void 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 | |
488 | void 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 | |
494 | void 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 | |
521 | void 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 | |
538 | void ScriptCreateDialog::_create() { |
539 | parent_name->set_text(select_class->get_selected_type().split(" " )[0]); |
540 | _parent_name_changed(parent_name->get_text()); |
541 | } |
542 | |
543 | void 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 | |
550 | void 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 | |
575 | void ScriptCreateDialog::_path_submitted(const String &p_path) { |
576 | if (!get_ok_button()->is_disabled()) { |
577 | ok_pressed(); |
578 | } |
579 | } |
580 | |
581 | void ScriptCreateDialog::() { |
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 ¤t_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 | |
670 | void 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 | |
793 | ScriptLanguage::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 | |
810 | Vector<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 | |
831 | ScriptLanguage::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> ; |
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 | |
913 | String 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 | |
925 | void 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 | |
931 | ScriptCreateDialog::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 | |