1 | /**************************************************************************/ |
2 | /* editor_autoload_settings.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 "editor_autoload_settings.h" |
32 | |
33 | #include "core/config/project_settings.h" |
34 | #include "core/core_constants.h" |
35 | #include "editor/editor_node.h" |
36 | #include "editor/editor_scale.h" |
37 | #include "editor/editor_string_names.h" |
38 | #include "editor/editor_undo_redo_manager.h" |
39 | #include "editor/filesystem_dock.h" |
40 | #include "editor/gui/editor_file_dialog.h" |
41 | #include "editor/project_settings_editor.h" |
42 | #include "scene/main/window.h" |
43 | #include "scene/resources/packed_scene.h" |
44 | |
45 | #define PREVIEW_LIST_MAX_SIZE 10 |
46 | |
47 | void EditorAutoloadSettings::_notification(int p_what) { |
48 | switch (p_what) { |
49 | case NOTIFICATION_ENTER_TREE: { |
50 | List<String> afn; |
51 | ResourceLoader::get_recognized_extensions_for_type("Script" , &afn); |
52 | ResourceLoader::get_recognized_extensions_for_type("PackedScene" , &afn); |
53 | |
54 | for (const String &E : afn) { |
55 | file_dialog->add_filter("*." + E); |
56 | } |
57 | |
58 | for (const AutoloadInfo &info : autoload_cache) { |
59 | if (info.node && info.in_editor) { |
60 | get_tree()->get_root()->call_deferred(SNAME("add_child" ), info.node); |
61 | } |
62 | } |
63 | browse_button->set_icon(get_editor_theme_icon(SNAME("Folder" ))); |
64 | } break; |
65 | |
66 | case NOTIFICATION_THEME_CHANGED: { |
67 | browse_button->set_icon(get_editor_theme_icon(SNAME("Folder" ))); |
68 | } break; |
69 | |
70 | case NOTIFICATION_VISIBILITY_CHANGED: { |
71 | FileSystemDock *dock = FileSystemDock::get_singleton(); |
72 | |
73 | if (dock != nullptr) { |
74 | ScriptCreateDialog *dialog = dock->get_script_create_dialog(); |
75 | |
76 | if (dialog != nullptr) { |
77 | Callable script_created = callable_mp(this, &EditorAutoloadSettings::_script_created); |
78 | |
79 | if (is_visible_in_tree()) { |
80 | if (!dialog->is_connected(SNAME("script_created" ), script_created)) { |
81 | dialog->connect("script_created" , script_created); |
82 | } |
83 | } else { |
84 | if (dialog->is_connected(SNAME("script_created" ), script_created)) { |
85 | dialog->disconnect("script_created" , script_created); |
86 | } |
87 | } |
88 | } |
89 | } |
90 | } break; |
91 | } |
92 | } |
93 | |
94 | bool EditorAutoloadSettings::_autoload_name_is_valid(const String &p_name, String *r_error) { |
95 | if (!p_name.is_valid_identifier()) { |
96 | if (r_error) { |
97 | *r_error = TTR("Invalid name." ) + " " ; |
98 | if (p_name.size() > 0 && p_name.left(1).is_numeric()) { |
99 | *r_error += TTR("Cannot begin with a digit." ); |
100 | } else { |
101 | *r_error += TTR("Valid characters:" ) + " a-z, A-Z, 0-9 or _" ; |
102 | } |
103 | } |
104 | |
105 | return false; |
106 | } |
107 | |
108 | if (ClassDB::class_exists(p_name)) { |
109 | if (r_error) { |
110 | *r_error = TTR("Invalid name." ) + " " + TTR("Must not collide with an existing engine class name." ); |
111 | } |
112 | |
113 | return false; |
114 | } |
115 | |
116 | if (ScriptServer::is_global_class(p_name)) { |
117 | if (r_error) { |
118 | *r_error = TTR("Invalid name." ) + "\n" + TTR("Must not collide with an existing global script class name." ); |
119 | } |
120 | |
121 | return false; |
122 | } |
123 | |
124 | for (int i = 0; i < Variant::VARIANT_MAX; i++) { |
125 | if (Variant::get_type_name(Variant::Type(i)) == p_name) { |
126 | if (r_error) { |
127 | *r_error = TTR("Invalid name." ) + " " + TTR("Must not collide with an existing built-in type name." ); |
128 | } |
129 | |
130 | return false; |
131 | } |
132 | } |
133 | |
134 | for (int i = 0; i < CoreConstants::get_global_constant_count(); i++) { |
135 | if (CoreConstants::get_global_constant_name(i) == p_name) { |
136 | if (r_error) { |
137 | *r_error = TTR("Invalid name." ) + " " + TTR("Must not collide with an existing global constant name." ); |
138 | } |
139 | |
140 | return false; |
141 | } |
142 | } |
143 | |
144 | for (int i = 0; i < ScriptServer::get_language_count(); i++) { |
145 | List<String> keywords; |
146 | ScriptServer::get_language(i)->get_reserved_words(&keywords); |
147 | for (const String &E : keywords) { |
148 | if (E == p_name) { |
149 | if (r_error) { |
150 | *r_error = TTR("Invalid name." ) + " " + TTR("Keyword cannot be used as an Autoload name." ); |
151 | } |
152 | |
153 | return false; |
154 | } |
155 | } |
156 | } |
157 | |
158 | return true; |
159 | } |
160 | |
161 | void EditorAutoloadSettings::_autoload_add() { |
162 | if (autoload_add_path->get_text().is_empty()) { |
163 | ScriptCreateDialog *dialog = FileSystemDock::get_singleton()->get_script_create_dialog(); |
164 | String fpath = path; |
165 | if (!fpath.ends_with("/" )) { |
166 | fpath = fpath.get_base_dir(); |
167 | } |
168 | dialog->config("Node" , fpath.path_join(vformat("%s.gd" , autoload_add_name->get_text().to_snake_case())), false, false); |
169 | dialog->popup_centered(); |
170 | } else { |
171 | if (autoload_add(autoload_add_name->get_text(), autoload_add_path->get_text())) { |
172 | autoload_add_path->set_text("" ); |
173 | } |
174 | |
175 | autoload_add_name->set_text("" ); |
176 | add_autoload->set_disabled(true); |
177 | } |
178 | } |
179 | |
180 | void EditorAutoloadSettings::_autoload_selected() { |
181 | TreeItem *ti = tree->get_selected(); |
182 | |
183 | if (!ti) { |
184 | return; |
185 | } |
186 | |
187 | selected_autoload = "autoload/" + ti->get_text(0); |
188 | } |
189 | |
190 | void EditorAutoloadSettings::_autoload_edited() { |
191 | if (updating_autoload) { |
192 | return; |
193 | } |
194 | |
195 | TreeItem *ti = tree->get_edited(); |
196 | int column = tree->get_edited_column(); |
197 | |
198 | EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); |
199 | |
200 | if (column == 0) { |
201 | String name = ti->get_text(0); |
202 | String old_name = selected_autoload.get_slice("/" , 1); |
203 | |
204 | if (name == old_name) { |
205 | return; |
206 | } |
207 | |
208 | String error; |
209 | if (!_autoload_name_is_valid(name, &error)) { |
210 | ti->set_text(0, old_name); |
211 | EditorNode::get_singleton()->show_warning(error); |
212 | return; |
213 | } |
214 | |
215 | if (ProjectSettings::get_singleton()->has_setting("autoload/" + name)) { |
216 | ti->set_text(0, old_name); |
217 | EditorNode::get_singleton()->show_warning(vformat(TTR("Autoload '%s' already exists!" ), name)); |
218 | return; |
219 | } |
220 | |
221 | updating_autoload = true; |
222 | |
223 | name = "autoload/" + name; |
224 | |
225 | int order = ProjectSettings::get_singleton()->get_order(selected_autoload); |
226 | String scr_path = GLOBAL_GET(selected_autoload); |
227 | |
228 | undo_redo->create_action(TTR("Rename Autoload" )); |
229 | |
230 | undo_redo->add_do_property(ProjectSettings::get_singleton(), name, scr_path); |
231 | undo_redo->add_do_method(ProjectSettings::get_singleton(), "set_order" , name, order); |
232 | undo_redo->add_do_method(ProjectSettings::get_singleton(), "clear" , selected_autoload); |
233 | |
234 | undo_redo->add_undo_property(ProjectSettings::get_singleton(), selected_autoload, scr_path); |
235 | undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set_order" , selected_autoload, order); |
236 | undo_redo->add_undo_method(ProjectSettings::get_singleton(), "clear" , name); |
237 | |
238 | undo_redo->add_do_method(this, "call_deferred" , "update_autoload" ); |
239 | undo_redo->add_undo_method(this, "call_deferred" , "update_autoload" ); |
240 | |
241 | undo_redo->add_do_method(this, "emit_signal" , autoload_changed); |
242 | undo_redo->add_undo_method(this, "emit_signal" , autoload_changed); |
243 | |
244 | undo_redo->commit_action(); |
245 | |
246 | selected_autoload = name; |
247 | } else if (column == 2) { |
248 | updating_autoload = true; |
249 | |
250 | bool checked = ti->is_checked(2); |
251 | String base = "autoload/" + ti->get_text(0); |
252 | |
253 | int order = ProjectSettings::get_singleton()->get_order(base); |
254 | String scr_path = GLOBAL_GET(base); |
255 | |
256 | if (scr_path.begins_with("*" )) { |
257 | scr_path = scr_path.substr(1, scr_path.length()); |
258 | } |
259 | |
260 | // Singleton autoloads are represented with a leading "*" in their path. |
261 | if (checked) { |
262 | scr_path = "*" + scr_path; |
263 | } |
264 | |
265 | undo_redo->create_action(TTR("Toggle Autoload Globals" )); |
266 | |
267 | undo_redo->add_do_property(ProjectSettings::get_singleton(), base, scr_path); |
268 | undo_redo->add_undo_property(ProjectSettings::get_singleton(), base, GLOBAL_GET(base)); |
269 | |
270 | undo_redo->add_do_method(ProjectSettings::get_singleton(), "set_order" , base, order); |
271 | undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set_order" , base, order); |
272 | |
273 | undo_redo->add_do_method(this, "call_deferred" , "update_autoload" ); |
274 | undo_redo->add_undo_method(this, "call_deferred" , "update_autoload" ); |
275 | |
276 | undo_redo->add_do_method(this, "emit_signal" , autoload_changed); |
277 | undo_redo->add_undo_method(this, "emit_signal" , autoload_changed); |
278 | |
279 | undo_redo->commit_action(); |
280 | } |
281 | |
282 | updating_autoload = false; |
283 | } |
284 | |
285 | void EditorAutoloadSettings::_autoload_button_pressed(Object *p_item, int p_column, int p_button, MouseButton p_mouse_button) { |
286 | if (p_mouse_button != MouseButton::LEFT) { |
287 | return; |
288 | } |
289 | TreeItem *ti = Object::cast_to<TreeItem>(p_item); |
290 | |
291 | String name = "autoload/" + ti->get_text(0); |
292 | |
293 | EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); |
294 | |
295 | switch (p_button) { |
296 | case BUTTON_OPEN: { |
297 | _autoload_open(ti->get_text(1)); |
298 | } break; |
299 | case BUTTON_MOVE_UP: |
300 | case BUTTON_MOVE_DOWN: { |
301 | TreeItem *swap = nullptr; |
302 | |
303 | if (p_button == BUTTON_MOVE_UP) { |
304 | swap = ti->get_prev(); |
305 | } else { |
306 | swap = ti->get_next(); |
307 | } |
308 | |
309 | if (!swap) { |
310 | return; |
311 | } |
312 | |
313 | String swap_name = "autoload/" + swap->get_text(0); |
314 | |
315 | int order = ProjectSettings::get_singleton()->get_order(name); |
316 | int swap_order = ProjectSettings::get_singleton()->get_order(swap_name); |
317 | |
318 | undo_redo->create_action(TTR("Move Autoload" )); |
319 | |
320 | undo_redo->add_do_method(ProjectSettings::get_singleton(), "set_order" , name, swap_order); |
321 | undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set_order" , name, order); |
322 | |
323 | undo_redo->add_do_method(ProjectSettings::get_singleton(), "set_order" , swap_name, order); |
324 | undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set_order" , swap_name, swap_order); |
325 | |
326 | undo_redo->add_do_method(this, "update_autoload" ); |
327 | undo_redo->add_undo_method(this, "update_autoload" ); |
328 | |
329 | undo_redo->add_do_method(this, "emit_signal" , autoload_changed); |
330 | undo_redo->add_undo_method(this, "emit_signal" , autoload_changed); |
331 | |
332 | undo_redo->commit_action(); |
333 | } break; |
334 | case BUTTON_DELETE: { |
335 | int order = ProjectSettings::get_singleton()->get_order(name); |
336 | |
337 | undo_redo->create_action(TTR("Remove Autoload" )); |
338 | |
339 | undo_redo->add_do_property(ProjectSettings::get_singleton(), name, Variant()); |
340 | |
341 | undo_redo->add_undo_property(ProjectSettings::get_singleton(), name, GLOBAL_GET(name)); |
342 | undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set_persisting" , name, true); |
343 | undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set_order" , order); |
344 | |
345 | undo_redo->add_do_method(this, "update_autoload" ); |
346 | undo_redo->add_undo_method(this, "update_autoload" ); |
347 | |
348 | undo_redo->add_do_method(this, "emit_signal" , autoload_changed); |
349 | undo_redo->add_undo_method(this, "emit_signal" , autoload_changed); |
350 | |
351 | undo_redo->commit_action(); |
352 | } break; |
353 | } |
354 | } |
355 | |
356 | void EditorAutoloadSettings::_autoload_activated() { |
357 | TreeItem *ti = tree->get_selected(); |
358 | if (!ti) { |
359 | return; |
360 | } |
361 | _autoload_open(ti->get_text(1)); |
362 | } |
363 | |
364 | void EditorAutoloadSettings::_autoload_open(const String &fpath) { |
365 | if (ResourceLoader::get_resource_type(fpath) == "PackedScene" ) { |
366 | EditorNode::get_singleton()->open_request(fpath); |
367 | } else { |
368 | EditorNode::get_singleton()->load_resource(fpath); |
369 | } |
370 | ProjectSettingsEditor::get_singleton()->hide(); |
371 | } |
372 | |
373 | void EditorAutoloadSettings::_autoload_file_callback(const String &p_path) { |
374 | // Convert the file name to PascalCase, which is the convention for classes in GDScript. |
375 | const String class_name = p_path.get_file().get_basename().to_pascal_case(); |
376 | |
377 | // If the name collides with a built-in class, prefix the name to make it possible to add without having to edit the name. |
378 | // The prefix is subjective, but it provides better UX than leaving the Add button disabled :) |
379 | const String prefix = ClassDB::class_exists(class_name) ? "Global" : "" ; |
380 | |
381 | autoload_add_name->set_text(prefix + class_name); |
382 | add_autoload->set_disabled(false); |
383 | } |
384 | |
385 | void EditorAutoloadSettings::_autoload_text_submitted(const String p_name) { |
386 | if (!autoload_add_path->get_text().is_empty() && _autoload_name_is_valid(p_name, nullptr)) { |
387 | _autoload_add(); |
388 | } |
389 | } |
390 | |
391 | void EditorAutoloadSettings::_autoload_path_text_changed(const String p_path) { |
392 | add_autoload->set_disabled(!_autoload_name_is_valid(autoload_add_name->get_text(), nullptr)); |
393 | } |
394 | |
395 | void EditorAutoloadSettings::_autoload_text_changed(const String p_name) { |
396 | String error_string; |
397 | bool is_name_valid = _autoload_name_is_valid(p_name, &error_string); |
398 | add_autoload->set_disabled(!is_name_valid); |
399 | error_message->set_text(error_string); |
400 | error_message->set_visible(!autoload_add_name->get_text().is_empty() && !is_name_valid); |
401 | } |
402 | |
403 | Node *EditorAutoloadSettings::_create_autoload(const String &p_path) { |
404 | Node *n = nullptr; |
405 | if (ResourceLoader::get_resource_type(p_path) == "PackedScene" ) { |
406 | // Cache the scene reference before loading it (for cyclic references) |
407 | Ref<PackedScene> scn; |
408 | scn.instantiate(); |
409 | scn->set_path(p_path); |
410 | scn->reload_from_file(); |
411 | ERR_FAIL_COND_V_MSG(!scn.is_valid(), nullptr, vformat("Can't autoload: %s." , p_path)); |
412 | |
413 | if (scn.is_valid()) { |
414 | n = scn->instantiate(); |
415 | } |
416 | } else { |
417 | Ref<Resource> res = ResourceLoader::load(p_path); |
418 | ERR_FAIL_COND_V_MSG(res.is_null(), nullptr, vformat("Can't autoload: %s." , p_path)); |
419 | |
420 | Ref<Script> scr = res; |
421 | if (scr.is_valid()) { |
422 | StringName ibt = scr->get_instance_base_type(); |
423 | bool valid_type = ClassDB::is_parent_class(ibt, "Node" ); |
424 | ERR_FAIL_COND_V_MSG(!valid_type, nullptr, vformat("Script does not inherit from Node: %s." , p_path)); |
425 | |
426 | Object *obj = ClassDB::instantiate(ibt); |
427 | |
428 | ERR_FAIL_NULL_V_MSG(obj, nullptr, vformat("Cannot instance script for Autoload, expected 'Node' inheritance, got: %s." , ibt)); |
429 | |
430 | n = Object::cast_to<Node>(obj); |
431 | n->set_script(scr); |
432 | } |
433 | } |
434 | |
435 | ERR_FAIL_NULL_V_MSG(n, nullptr, vformat("Path in Autoload not a node or script: %s." , p_path)); |
436 | |
437 | return n; |
438 | } |
439 | |
440 | void EditorAutoloadSettings::update_autoload() { |
441 | if (updating_autoload) { |
442 | return; |
443 | } |
444 | |
445 | updating_autoload = true; |
446 | |
447 | HashMap<String, AutoloadInfo> to_remove; |
448 | List<AutoloadInfo *> to_add; |
449 | |
450 | for (const AutoloadInfo &info : autoload_cache) { |
451 | to_remove.insert(info.name, info); |
452 | } |
453 | |
454 | autoload_cache.clear(); |
455 | |
456 | tree->clear(); |
457 | TreeItem *root = tree->create_item(); |
458 | |
459 | List<PropertyInfo> props; |
460 | ProjectSettings::get_singleton()->get_property_list(&props); |
461 | |
462 | for (const PropertyInfo &pi : props) { |
463 | if (!pi.name.begins_with("autoload/" )) { |
464 | continue; |
465 | } |
466 | |
467 | String name = pi.name.get_slice("/" , 1); |
468 | String scr_path = GLOBAL_GET(pi.name); |
469 | |
470 | if (name.is_empty()) { |
471 | continue; |
472 | } |
473 | |
474 | AutoloadInfo info; |
475 | info.is_singleton = scr_path.begins_with("*" ); |
476 | |
477 | if (info.is_singleton) { |
478 | scr_path = scr_path.substr(1, scr_path.length()); |
479 | } |
480 | |
481 | info.name = name; |
482 | info.path = scr_path; |
483 | info.order = ProjectSettings::get_singleton()->get_order(pi.name); |
484 | |
485 | bool need_to_add = true; |
486 | if (to_remove.has(name)) { |
487 | AutoloadInfo &old_info = to_remove[name]; |
488 | if (old_info.path == info.path) { |
489 | // Still the same resource, check status |
490 | info.node = old_info.node; |
491 | if (info.node) { |
492 | Ref<Script> scr = info.node->get_script(); |
493 | info.in_editor = scr.is_valid() && scr->is_tool(); |
494 | if (info.is_singleton == old_info.is_singleton && info.in_editor == old_info.in_editor) { |
495 | to_remove.erase(name); |
496 | need_to_add = false; |
497 | } else { |
498 | info.node = nullptr; |
499 | } |
500 | } |
501 | } |
502 | } |
503 | |
504 | autoload_cache.push_back(info); |
505 | |
506 | if (need_to_add) { |
507 | to_add.push_back(&(autoload_cache.back()->get())); |
508 | } |
509 | |
510 | TreeItem *item = tree->create_item(root); |
511 | item->set_text(0, name); |
512 | item->set_editable(0, true); |
513 | |
514 | item->set_text(1, scr_path); |
515 | item->set_selectable(1, true); |
516 | |
517 | item->set_cell_mode(2, TreeItem::CELL_MODE_CHECK); |
518 | item->set_editable(2, true); |
519 | item->set_text(2, TTR("Enable" )); |
520 | item->set_checked(2, info.is_singleton); |
521 | item->add_button(3, get_editor_theme_icon(SNAME("Load" )), BUTTON_OPEN); |
522 | item->add_button(3, get_editor_theme_icon(SNAME("MoveUp" )), BUTTON_MOVE_UP); |
523 | item->add_button(3, get_editor_theme_icon(SNAME("MoveDown" )), BUTTON_MOVE_DOWN); |
524 | item->add_button(3, get_editor_theme_icon(SNAME("Remove" )), BUTTON_DELETE); |
525 | item->set_selectable(3, false); |
526 | } |
527 | |
528 | // Remove deleted/changed autoloads |
529 | for (KeyValue<String, AutoloadInfo> &E : to_remove) { |
530 | AutoloadInfo &info = E.value; |
531 | if (info.is_singleton) { |
532 | for (int i = 0; i < ScriptServer::get_language_count(); i++) { |
533 | ScriptServer::get_language(i)->remove_named_global_constant(info.name); |
534 | } |
535 | } |
536 | if (info.in_editor) { |
537 | ERR_CONTINUE(!info.node); |
538 | get_tree()->get_root()->call_deferred(SNAME("remove_child" ), info.node); |
539 | } |
540 | |
541 | if (info.node) { |
542 | info.node->queue_free(); |
543 | info.node = nullptr; |
544 | } |
545 | } |
546 | |
547 | // Load new/changed autoloads |
548 | List<Node *> nodes_to_add; |
549 | for (AutoloadInfo *info : to_add) { |
550 | info->node = _create_autoload(info->path); |
551 | |
552 | ERR_CONTINUE(!info->node); |
553 | info->node->set_name(info->name); |
554 | |
555 | Ref<Script> scr = info->node->get_script(); |
556 | info->in_editor = scr.is_valid() && scr->is_tool(); |
557 | |
558 | if (info->in_editor) { |
559 | //defer so references are all valid on _ready() |
560 | nodes_to_add.push_back(info->node); |
561 | } |
562 | |
563 | if (info->is_singleton) { |
564 | for (int i = 0; i < ScriptServer::get_language_count(); i++) { |
565 | ScriptServer::get_language(i)->add_named_global_constant(info->name, info->node); |
566 | } |
567 | } |
568 | |
569 | if (!info->in_editor && !info->is_singleton) { |
570 | // No reason to keep this node |
571 | memdelete(info->node); |
572 | info->node = nullptr; |
573 | } |
574 | } |
575 | |
576 | for (Node *E : nodes_to_add) { |
577 | get_tree()->get_root()->add_child(E); |
578 | } |
579 | |
580 | updating_autoload = false; |
581 | } |
582 | |
583 | void EditorAutoloadSettings::_script_created(Ref<Script> p_script) { |
584 | FileSystemDock::get_singleton()->get_script_create_dialog()->hide(); |
585 | path = p_script->get_path().get_base_dir(); |
586 | autoload_add_path->set_text(p_script->get_path()); |
587 | autoload_add_name->set_text(p_script->get_path().get_file().get_basename().to_pascal_case()); |
588 | _autoload_add(); |
589 | } |
590 | |
591 | Variant EditorAutoloadSettings::get_drag_data_fw(const Point2 &p_point, Control *p_control) { |
592 | if (autoload_cache.size() <= 1) { |
593 | return false; |
594 | } |
595 | |
596 | PackedStringArray autoloads; |
597 | |
598 | TreeItem *next = tree->get_next_selected(nullptr); |
599 | |
600 | while (next) { |
601 | autoloads.push_back(next->get_text(0)); |
602 | next = tree->get_next_selected(next); |
603 | } |
604 | |
605 | if (autoloads.size() == 0 || autoloads.size() == autoload_cache.size()) { |
606 | return Variant(); |
607 | } |
608 | |
609 | VBoxContainer *preview = memnew(VBoxContainer); |
610 | |
611 | int max_size = MIN(PREVIEW_LIST_MAX_SIZE, autoloads.size()); |
612 | |
613 | for (int i = 0; i < max_size; i++) { |
614 | Label *label = memnew(Label(autoloads[i])); |
615 | label->set_self_modulate(Color(1, 1, 1, Math::lerp(1, 0, float(i) / PREVIEW_LIST_MAX_SIZE))); |
616 | |
617 | preview->add_child(label); |
618 | } |
619 | |
620 | tree->set_drop_mode_flags(Tree::DROP_MODE_INBETWEEN); |
621 | tree->set_drag_preview(preview); |
622 | |
623 | Dictionary drop_data; |
624 | drop_data["type" ] = "autoload" ; |
625 | drop_data["autoloads" ] = autoloads; |
626 | |
627 | return drop_data; |
628 | } |
629 | |
630 | bool EditorAutoloadSettings::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_control) const { |
631 | if (updating_autoload) { |
632 | return false; |
633 | } |
634 | |
635 | Dictionary drop_data = p_data; |
636 | |
637 | if (!drop_data.has("type" )) { |
638 | return false; |
639 | } |
640 | |
641 | if (drop_data.has("type" )) { |
642 | TreeItem *ti = tree->get_item_at_position(p_point); |
643 | |
644 | if (!ti) { |
645 | return false; |
646 | } |
647 | |
648 | int section = tree->get_drop_section_at_position(p_point); |
649 | |
650 | return section >= -1; |
651 | } |
652 | |
653 | return false; |
654 | } |
655 | |
656 | void EditorAutoloadSettings::drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_control) { |
657 | TreeItem *ti = tree->get_item_at_position(p_point); |
658 | |
659 | if (!ti) { |
660 | return; |
661 | } |
662 | |
663 | int section = tree->get_drop_section_at_position(p_point); |
664 | |
665 | if (section < -1) { |
666 | return; |
667 | } |
668 | |
669 | String name; |
670 | bool move_to_back = false; |
671 | |
672 | if (section < 0) { |
673 | name = ti->get_text(0); |
674 | } else if (ti->get_next()) { |
675 | name = ti->get_next()->get_text(0); |
676 | } else { |
677 | name = ti->get_text(0); |
678 | move_to_back = true; |
679 | } |
680 | |
681 | int order = ProjectSettings::get_singleton()->get_order("autoload/" + name); |
682 | |
683 | AutoloadInfo aux; |
684 | List<AutoloadInfo>::Element *E = nullptr; |
685 | |
686 | if (!move_to_back) { |
687 | aux.order = order; |
688 | E = autoload_cache.find(aux); |
689 | } |
690 | |
691 | Dictionary drop_data = p_data; |
692 | PackedStringArray autoloads = drop_data["autoloads" ]; |
693 | |
694 | Vector<int> orders; |
695 | orders.resize(autoload_cache.size()); |
696 | |
697 | for (int i = 0; i < autoloads.size(); i++) { |
698 | aux.order = ProjectSettings::get_singleton()->get_order("autoload/" + autoloads[i]); |
699 | |
700 | List<AutoloadInfo>::Element *I = autoload_cache.find(aux); |
701 | |
702 | if (move_to_back) { |
703 | autoload_cache.move_to_back(I); |
704 | } else if (E != I) { |
705 | autoload_cache.move_before(I, E); |
706 | } else if (E->next()) { |
707 | E = E->next(); |
708 | } else { |
709 | break; |
710 | } |
711 | } |
712 | |
713 | int i = 0; |
714 | |
715 | for (const AutoloadInfo &F : autoload_cache) { |
716 | orders.write[i++] = F.order; |
717 | } |
718 | |
719 | orders.sort(); |
720 | |
721 | EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); |
722 | |
723 | undo_redo->create_action(TTR("Rearrange Autoloads" )); |
724 | |
725 | i = 0; |
726 | |
727 | for (const AutoloadInfo &F : autoload_cache) { |
728 | undo_redo->add_do_method(ProjectSettings::get_singleton(), "set_order" , "autoload/" + F.name, orders[i++]); |
729 | undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set_order" , "autoload/" + F.name, F.order); |
730 | } |
731 | |
732 | orders.clear(); |
733 | |
734 | undo_redo->add_do_method(this, "update_autoload" ); |
735 | undo_redo->add_undo_method(this, "update_autoload" ); |
736 | |
737 | undo_redo->add_do_method(this, "emit_signal" , autoload_changed); |
738 | undo_redo->add_undo_method(this, "emit_signal" , autoload_changed); |
739 | |
740 | undo_redo->commit_action(); |
741 | } |
742 | |
743 | bool EditorAutoloadSettings::autoload_add(const String &p_name, const String &p_path) { |
744 | String name = p_name; |
745 | |
746 | String error; |
747 | if (!_autoload_name_is_valid(name, &error)) { |
748 | EditorNode::get_singleton()->show_warning(TTR("Can't add Autoload:" ) + "\n" + error); |
749 | return false; |
750 | } |
751 | |
752 | if (!FileAccess::exists(p_path)) { |
753 | EditorNode::get_singleton()->show_warning(TTR("Can't add Autoload:" ) + "\n" + vformat(TTR("%s is an invalid path. File does not exist." ), path)); |
754 | return false; |
755 | } |
756 | |
757 | if (!p_path.begins_with("res://" )) { |
758 | EditorNode::get_singleton()->show_warning(TTR("Can't add Autoload:" ) + "\n" + vformat(TTR("%s is an invalid path. Not in resource path (res://)." ), path)); |
759 | return false; |
760 | } |
761 | |
762 | name = "autoload/" + name; |
763 | |
764 | EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); |
765 | |
766 | undo_redo->create_action(TTR("Add Autoload" )); |
767 | // Singleton autoloads are represented with a leading "*" in their path. |
768 | undo_redo->add_do_property(ProjectSettings::get_singleton(), name, "*" + p_path); |
769 | |
770 | if (ProjectSettings::get_singleton()->has_setting(name)) { |
771 | undo_redo->add_undo_property(ProjectSettings::get_singleton(), name, GLOBAL_GET(name)); |
772 | } else { |
773 | undo_redo->add_undo_property(ProjectSettings::get_singleton(), name, Variant()); |
774 | } |
775 | |
776 | undo_redo->add_do_method(this, "update_autoload" ); |
777 | undo_redo->add_undo_method(this, "update_autoload" ); |
778 | |
779 | undo_redo->add_do_method(this, "emit_signal" , autoload_changed); |
780 | undo_redo->add_undo_method(this, "emit_signal" , autoload_changed); |
781 | |
782 | undo_redo->commit_action(); |
783 | |
784 | return true; |
785 | } |
786 | |
787 | void EditorAutoloadSettings::autoload_remove(const String &p_name) { |
788 | String name = "autoload/" + p_name; |
789 | |
790 | EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); |
791 | |
792 | int order = ProjectSettings::get_singleton()->get_order(name); |
793 | |
794 | undo_redo->create_action(TTR("Remove Autoload" )); |
795 | |
796 | undo_redo->add_do_property(ProjectSettings::get_singleton(), name, Variant()); |
797 | |
798 | undo_redo->add_undo_property(ProjectSettings::get_singleton(), name, GLOBAL_GET(name)); |
799 | undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set_persisting" , name, true); |
800 | undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set_order" , order); |
801 | |
802 | undo_redo->add_do_method(this, "update_autoload" ); |
803 | undo_redo->add_undo_method(this, "update_autoload" ); |
804 | |
805 | undo_redo->add_do_method(this, "emit_signal" , autoload_changed); |
806 | undo_redo->add_undo_method(this, "emit_signal" , autoload_changed); |
807 | |
808 | undo_redo->commit_action(); |
809 | } |
810 | |
811 | void EditorAutoloadSettings::_bind_methods() { |
812 | ClassDB::bind_method("update_autoload" , &EditorAutoloadSettings::update_autoload); |
813 | ClassDB::bind_method("autoload_add" , &EditorAutoloadSettings::autoload_add); |
814 | ClassDB::bind_method("autoload_remove" , &EditorAutoloadSettings::autoload_remove); |
815 | |
816 | ADD_SIGNAL(MethodInfo("autoload_changed" )); |
817 | } |
818 | |
819 | EditorAutoloadSettings::EditorAutoloadSettings() { |
820 | ProjectSettings::get_singleton()->add_hidden_prefix("autoload/" ); |
821 | |
822 | // Make first cache |
823 | List<PropertyInfo> props; |
824 | ProjectSettings::get_singleton()->get_property_list(&props); |
825 | for (const PropertyInfo &pi : props) { |
826 | if (!pi.name.begins_with("autoload/" )) { |
827 | continue; |
828 | } |
829 | |
830 | String name = pi.name.get_slice("/" , 1); |
831 | String scr_path = GLOBAL_GET(pi.name); |
832 | |
833 | if (name.is_empty()) { |
834 | continue; |
835 | } |
836 | |
837 | AutoloadInfo info; |
838 | info.is_singleton = scr_path.begins_with("*" ); |
839 | |
840 | if (info.is_singleton) { |
841 | scr_path = scr_path.substr(1, scr_path.length()); |
842 | } |
843 | |
844 | info.name = name; |
845 | info.path = scr_path; |
846 | info.order = ProjectSettings::get_singleton()->get_order(pi.name); |
847 | |
848 | if (info.is_singleton) { |
849 | // Make sure name references work before parsing scripts |
850 | for (int i = 0; i < ScriptServer::get_language_count(); i++) { |
851 | ScriptServer::get_language(i)->add_named_global_constant(info.name, Variant()); |
852 | } |
853 | } |
854 | |
855 | autoload_cache.push_back(info); |
856 | } |
857 | |
858 | for (AutoloadInfo &info : autoload_cache) { |
859 | info.node = _create_autoload(info.path); |
860 | |
861 | if (info.node) { |
862 | Ref<Script> scr = info.node->get_script(); |
863 | info.in_editor = scr.is_valid() && scr->is_tool(); |
864 | info.node->set_name(info.name); |
865 | } |
866 | |
867 | if (info.is_singleton) { |
868 | for (int i = 0; i < ScriptServer::get_language_count(); i++) { |
869 | ScriptServer::get_language(i)->add_named_global_constant(info.name, info.node); |
870 | } |
871 | } |
872 | |
873 | if (!info.is_singleton && !info.in_editor && info.node != nullptr) { |
874 | memdelete(info.node); |
875 | info.node = nullptr; |
876 | } |
877 | } |
878 | |
879 | HBoxContainer *hbc = memnew(HBoxContainer); |
880 | add_child(hbc); |
881 | |
882 | error_message = memnew(Label); |
883 | error_message->hide(); |
884 | error_message->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); |
885 | error_message->add_theme_color_override("font_color" , EditorNode::get_singleton()->get_editor_theme()->get_color(SNAME("error_color" ), EditorStringName(Editor))); |
886 | add_child(error_message); |
887 | |
888 | Label *l = memnew(Label); |
889 | l->set_text(TTR("Path:" )); |
890 | hbc->add_child(l); |
891 | |
892 | autoload_add_path = memnew(LineEdit); |
893 | hbc->add_child(autoload_add_path); |
894 | autoload_add_path->set_h_size_flags(Control::SIZE_EXPAND_FILL); |
895 | autoload_add_path->set_clear_button_enabled(true); |
896 | autoload_add_path->set_placeholder(vformat(TTR("Set path or press \"%s\" to create a script." ), TTR("Add" ))); |
897 | autoload_add_path->connect("text_changed" , callable_mp(this, &EditorAutoloadSettings::_autoload_path_text_changed)); |
898 | |
899 | browse_button = memnew(Button); |
900 | hbc->add_child(browse_button); |
901 | browse_button->connect("pressed" , callable_mp(this, &EditorAutoloadSettings::_browse_autoload_add_path)); |
902 | |
903 | file_dialog = memnew(EditorFileDialog); |
904 | hbc->add_child(file_dialog); |
905 | file_dialog->connect("file_selected" , callable_mp(this, &EditorAutoloadSettings::_set_autoload_add_path)); |
906 | file_dialog->connect("dir_selected" , callable_mp(this, &EditorAutoloadSettings::_set_autoload_add_path)); |
907 | file_dialog->connect("files_selected" , callable_mp(this, &EditorAutoloadSettings::_set_autoload_add_path)); |
908 | |
909 | hbc->set_h_size_flags(SIZE_EXPAND_FILL); |
910 | file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE); |
911 | file_dialog->connect("file_selected" , callable_mp(this, &EditorAutoloadSettings::_autoload_file_callback)); |
912 | |
913 | l = memnew(Label); |
914 | l->set_text(TTR("Node Name:" )); |
915 | hbc->add_child(l); |
916 | |
917 | autoload_add_name = memnew(LineEdit); |
918 | autoload_add_name->set_h_size_flags(SIZE_EXPAND_FILL); |
919 | autoload_add_name->connect("text_submitted" , callable_mp(this, &EditorAutoloadSettings::_autoload_text_submitted)); |
920 | autoload_add_name->connect("text_changed" , callable_mp(this, &EditorAutoloadSettings::_autoload_text_changed)); |
921 | hbc->add_child(autoload_add_name); |
922 | |
923 | add_autoload = memnew(Button); |
924 | add_autoload->set_text(TTR("Add" )); |
925 | add_autoload->connect("pressed" , callable_mp(this, &EditorAutoloadSettings::_autoload_add)); |
926 | // The button will be enabled once a valid name is entered (either automatically or manually). |
927 | add_autoload->set_disabled(true); |
928 | hbc->add_child(add_autoload); |
929 | |
930 | tree = memnew(Tree); |
931 | tree->set_hide_root(true); |
932 | tree->set_select_mode(Tree::SELECT_MULTI); |
933 | tree->set_allow_reselect(true); |
934 | |
935 | SET_DRAG_FORWARDING_GCD(tree, EditorAutoloadSettings); |
936 | |
937 | tree->set_columns(4); |
938 | tree->set_column_titles_visible(true); |
939 | |
940 | tree->set_column_title(0, TTR("Name" )); |
941 | tree->set_column_expand(0, true); |
942 | tree->set_column_expand_ratio(0, 1); |
943 | |
944 | tree->set_column_title(1, TTR("Path" )); |
945 | tree->set_column_expand(1, true); |
946 | tree->set_column_clip_content(1, true); |
947 | tree->set_column_expand_ratio(1, 2); |
948 | |
949 | tree->set_column_title(2, TTR("Global Variable" )); |
950 | tree->set_column_expand(2, false); |
951 | |
952 | tree->set_column_expand(3, false); |
953 | |
954 | tree->connect("cell_selected" , callable_mp(this, &EditorAutoloadSettings::_autoload_selected)); |
955 | tree->connect("item_edited" , callable_mp(this, &EditorAutoloadSettings::_autoload_edited)); |
956 | tree->connect("button_clicked" , callable_mp(this, &EditorAutoloadSettings::_autoload_button_pressed)); |
957 | tree->connect("item_activated" , callable_mp(this, &EditorAutoloadSettings::_autoload_activated)); |
958 | tree->set_v_size_flags(SIZE_EXPAND_FILL); |
959 | |
960 | add_child(tree, true); |
961 | } |
962 | |
963 | EditorAutoloadSettings::~EditorAutoloadSettings() { |
964 | for (const AutoloadInfo &info : autoload_cache) { |
965 | if (info.node && !info.in_editor) { |
966 | memdelete(info.node); |
967 | } |
968 | } |
969 | } |
970 | |
971 | void EditorAutoloadSettings::_set_autoload_add_path(const String &p_text) { |
972 | autoload_add_path->set_text(p_text); |
973 | autoload_add_path->emit_signal(SNAME("text_submitted" ), p_text); |
974 | } |
975 | |
976 | void EditorAutoloadSettings::_browse_autoload_add_path() { |
977 | file_dialog->popup_file_dialog(); |
978 | } |
979 | |