1/**************************************************************************/
2/* editor_feature_profile.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_feature_profile.h"
32
33#include "core/io/dir_access.h"
34#include "core/io/json.h"
35#include "editor/editor_node.h"
36#include "editor/editor_paths.h"
37#include "editor/editor_property_name_processor.h"
38#include "editor/editor_scale.h"
39#include "editor/editor_settings.h"
40#include "editor/editor_string_names.h"
41#include "editor/gui/editor_file_dialog.h"
42
43const char *EditorFeatureProfile::feature_names[FEATURE_MAX] = {
44 TTRC("3D Editor"),
45 TTRC("Script Editor"),
46 TTRC("Asset Library"),
47 TTRC("Scene Tree Editing"),
48 TTRC("Node Dock"),
49 TTRC("FileSystem Dock"),
50 TTRC("Import Dock"),
51 TTRC("History Dock"),
52};
53
54const char *EditorFeatureProfile::feature_descriptions[FEATURE_MAX] = {
55 TTRC("Allows to view and edit 3D scenes."),
56 TTRC("Allows to edit scripts using the integrated script editor."),
57 TTRC("Provides built-in access to the Asset Library."),
58 TTRC("Allows editing the node hierarchy in the Scene dock."),
59 TTRC("Allows to work with signals and groups of the node selected in the Scene dock."),
60 TTRC("Allows to browse the local file system via a dedicated dock."),
61 TTRC("Allows to configure import settings for individual assets. Requires the FileSystem dock to function."),
62 TTRC("Provides an overview of the editor's and each scene's undo history."),
63};
64
65const char *EditorFeatureProfile::feature_identifiers[FEATURE_MAX] = {
66 "3d",
67 "script",
68 "asset_lib",
69 "scene_tree",
70 "node_dock",
71 "filesystem_dock",
72 "import_dock",
73 "history_dock",
74};
75
76void EditorFeatureProfile::set_disable_class(const StringName &p_class, bool p_disabled) {
77 if (p_disabled) {
78 disabled_classes.insert(p_class);
79 } else {
80 disabled_classes.erase(p_class);
81 }
82}
83
84bool EditorFeatureProfile::is_class_disabled(const StringName &p_class) const {
85 if (p_class == StringName()) {
86 return false;
87 }
88 return disabled_classes.has(p_class) || is_class_disabled(ClassDB::get_parent_class_nocheck(p_class));
89}
90
91void EditorFeatureProfile::set_disable_class_editor(const StringName &p_class, bool p_disabled) {
92 if (p_disabled) {
93 disabled_editors.insert(p_class);
94 } else {
95 disabled_editors.erase(p_class);
96 }
97}
98
99bool EditorFeatureProfile::is_class_editor_disabled(const StringName &p_class) const {
100 if (p_class == StringName()) {
101 return false;
102 }
103 return disabled_editors.has(p_class) || is_class_editor_disabled(ClassDB::get_parent_class_nocheck(p_class));
104}
105
106void EditorFeatureProfile::set_disable_class_property(const StringName &p_class, const StringName &p_property, bool p_disabled) {
107 if (p_disabled) {
108 if (!disabled_properties.has(p_class)) {
109 disabled_properties[p_class] = HashSet<StringName>();
110 }
111
112 disabled_properties[p_class].insert(p_property);
113 } else {
114 ERR_FAIL_COND(!disabled_properties.has(p_class));
115 disabled_properties[p_class].erase(p_property);
116 if (disabled_properties[p_class].is_empty()) {
117 disabled_properties.erase(p_class);
118 }
119 }
120}
121
122bool EditorFeatureProfile::is_class_property_disabled(const StringName &p_class, const StringName &p_property) const {
123 if (!disabled_properties.has(p_class)) {
124 return false;
125 }
126
127 if (!disabled_properties[p_class].has(p_property)) {
128 return false;
129 }
130
131 return true;
132}
133
134bool EditorFeatureProfile::has_class_properties_disabled(const StringName &p_class) const {
135 return disabled_properties.has(p_class);
136}
137
138void EditorFeatureProfile::set_item_collapsed(const StringName &p_class, bool p_collapsed) {
139 if (p_collapsed) {
140 collapsed_classes.insert(p_class);
141 } else {
142 collapsed_classes.erase(p_class);
143 }
144}
145
146bool EditorFeatureProfile::is_item_collapsed(const StringName &p_class) const {
147 return collapsed_classes.has(p_class);
148}
149
150void EditorFeatureProfile::set_disable_feature(Feature p_feature, bool p_disable) {
151 ERR_FAIL_INDEX(p_feature, FEATURE_MAX);
152 features_disabled[p_feature] = p_disable;
153}
154
155bool EditorFeatureProfile::is_feature_disabled(Feature p_feature) const {
156 ERR_FAIL_INDEX_V(p_feature, FEATURE_MAX, false);
157 return features_disabled[p_feature];
158}
159
160String EditorFeatureProfile::get_feature_name(Feature p_feature) {
161 ERR_FAIL_INDEX_V(p_feature, FEATURE_MAX, String());
162 return feature_names[p_feature];
163}
164
165String EditorFeatureProfile::get_feature_description(Feature p_feature) {
166 ERR_FAIL_INDEX_V(p_feature, FEATURE_MAX, String());
167 return feature_descriptions[p_feature];
168}
169
170Error EditorFeatureProfile::save_to_file(const String &p_path) {
171 Dictionary data;
172 data["type"] = "feature_profile";
173 Array dis_classes;
174 for (const StringName &E : disabled_classes) {
175 dis_classes.push_back(String(E));
176 }
177 dis_classes.sort();
178 data["disabled_classes"] = dis_classes;
179
180 Array dis_editors;
181 for (const StringName &E : disabled_editors) {
182 dis_editors.push_back(String(E));
183 }
184 dis_editors.sort();
185 data["disabled_editors"] = dis_editors;
186
187 Array dis_props;
188
189 for (KeyValue<StringName, HashSet<StringName>> &E : disabled_properties) {
190 for (const StringName &F : E.value) {
191 dis_props.push_back(String(E.key) + ":" + String(F));
192 }
193 }
194
195 data["disabled_properties"] = dis_props;
196
197 Array dis_features;
198 for (int i = 0; i < FEATURE_MAX; i++) {
199 if (features_disabled[i]) {
200 dis_features.push_back(feature_identifiers[i]);
201 }
202 }
203
204 data["disabled_features"] = dis_features;
205
206 Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE);
207 ERR_FAIL_COND_V_MSG(f.is_null(), ERR_CANT_CREATE, "Cannot create file '" + p_path + "'.");
208
209 JSON json;
210 String text = json.stringify(data, "\t");
211 f->store_string(text);
212 return OK;
213}
214
215Error EditorFeatureProfile::load_from_file(const String &p_path) {
216 Error err;
217 String text = FileAccess::get_file_as_string(p_path, &err);
218 if (err != OK) {
219 return err;
220 }
221
222 JSON json;
223 err = json.parse(text);
224 if (err != OK) {
225 ERR_PRINT("Error parsing '" + p_path + "' on line " + itos(json.get_error_line()) + ": " + json.get_error_message());
226 return ERR_PARSE_ERROR;
227 }
228
229 Dictionary data = json.get_data();
230
231 if (!data.has("type") || String(data["type"]) != "feature_profile") {
232 ERR_PRINT("Error parsing '" + p_path + "', it's not a feature profile.");
233 return ERR_PARSE_ERROR;
234 }
235
236 disabled_classes.clear();
237
238 if (data.has("disabled_classes")) {
239 Array disabled_classes_arr = data["disabled_classes"];
240 for (int i = 0; i < disabled_classes_arr.size(); i++) {
241 disabled_classes.insert(disabled_classes_arr[i]);
242 }
243 }
244
245 disabled_editors.clear();
246
247 if (data.has("disabled_editors")) {
248 Array disabled_editors_arr = data["disabled_editors"];
249 for (int i = 0; i < disabled_editors_arr.size(); i++) {
250 disabled_editors.insert(disabled_editors_arr[i]);
251 }
252 }
253
254 disabled_properties.clear();
255
256 if (data.has("disabled_properties")) {
257 Array disabled_properties_arr = data["disabled_properties"];
258 for (int i = 0; i < disabled_properties_arr.size(); i++) {
259 String s = disabled_properties_arr[i];
260 set_disable_class_property(s.get_slice(":", 0), s.get_slice(":", 1), true);
261 }
262 }
263
264 if (data.has("disabled_features")) {
265 Array disabled_features_arr = data["disabled_features"];
266 for (int i = 0; i < FEATURE_MAX; i++) {
267 bool found = false;
268 String f = feature_identifiers[i];
269 for (int j = 0; j < disabled_features_arr.size(); j++) {
270 String fd = disabled_features_arr[j];
271 if (fd == f) {
272 found = true;
273 break;
274 }
275 }
276
277 features_disabled[i] = found;
278 }
279 }
280
281 return OK;
282}
283
284void EditorFeatureProfile::_bind_methods() {
285 ClassDB::bind_method(D_METHOD("set_disable_class", "class_name", "disable"), &EditorFeatureProfile::set_disable_class);
286 ClassDB::bind_method(D_METHOD("is_class_disabled", "class_name"), &EditorFeatureProfile::is_class_disabled);
287
288 ClassDB::bind_method(D_METHOD("set_disable_class_editor", "class_name", "disable"), &EditorFeatureProfile::set_disable_class_editor);
289 ClassDB::bind_method(D_METHOD("is_class_editor_disabled", "class_name"), &EditorFeatureProfile::is_class_editor_disabled);
290
291 ClassDB::bind_method(D_METHOD("set_disable_class_property", "class_name", "property", "disable"), &EditorFeatureProfile::set_disable_class_property);
292 ClassDB::bind_method(D_METHOD("is_class_property_disabled", "class_name", "property"), &EditorFeatureProfile::is_class_property_disabled);
293
294 ClassDB::bind_method(D_METHOD("set_disable_feature", "feature", "disable"), &EditorFeatureProfile::set_disable_feature);
295 ClassDB::bind_method(D_METHOD("is_feature_disabled", "feature"), &EditorFeatureProfile::is_feature_disabled);
296
297 ClassDB::bind_method(D_METHOD("get_feature_name", "feature"), &EditorFeatureProfile::_get_feature_name);
298
299 ClassDB::bind_method(D_METHOD("save_to_file", "path"), &EditorFeatureProfile::save_to_file);
300 ClassDB::bind_method(D_METHOD("load_from_file", "path"), &EditorFeatureProfile::load_from_file);
301
302 BIND_ENUM_CONSTANT(FEATURE_3D);
303 BIND_ENUM_CONSTANT(FEATURE_SCRIPT);
304 BIND_ENUM_CONSTANT(FEATURE_ASSET_LIB);
305 BIND_ENUM_CONSTANT(FEATURE_SCENE_TREE);
306 BIND_ENUM_CONSTANT(FEATURE_NODE_DOCK);
307 BIND_ENUM_CONSTANT(FEATURE_FILESYSTEM_DOCK);
308 BIND_ENUM_CONSTANT(FEATURE_IMPORT_DOCK);
309 BIND_ENUM_CONSTANT(FEATURE_HISTORY_DOCK);
310 BIND_ENUM_CONSTANT(FEATURE_MAX);
311}
312
313EditorFeatureProfile::EditorFeatureProfile() {
314 for (int i = 0; i < FEATURE_MAX; i++) {
315 features_disabled[i] = false;
316 }
317}
318
319//////////////////////////
320
321void EditorFeatureProfileManager::_notification(int p_what) {
322 switch (p_what) {
323 case NOTIFICATION_READY: {
324 current_profile = EDITOR_GET("_default_feature_profile");
325 if (!current_profile.is_empty()) {
326 current.instantiate();
327 Error err = current->load_from_file(EditorPaths::get_singleton()->get_feature_profiles_dir().path_join(current_profile + ".profile"));
328 if (err != OK) {
329 ERR_PRINT("Error loading default feature profile: " + current_profile);
330 current_profile = String();
331 current.unref();
332 }
333 }
334 _update_profile_list(current_profile);
335 } break;
336
337 case NOTIFICATION_THEME_CHANGED: {
338 // Make sure that the icons are correctly adjusted if the theme's lightness was switched.
339 _update_selected_profile();
340 } break;
341 }
342}
343
344String EditorFeatureProfileManager::_get_selected_profile() {
345 int idx = profile_list->get_selected();
346 if (idx < 0) {
347 return String();
348 }
349
350 return profile_list->get_item_metadata(idx);
351}
352
353void EditorFeatureProfileManager::_update_profile_list(const String &p_select_profile) {
354 String selected_profile;
355 if (p_select_profile.is_empty()) { //default, keep
356 if (profile_list->get_selected() >= 0) {
357 selected_profile = profile_list->get_item_metadata(profile_list->get_selected());
358 if (!FileAccess::exists(EditorPaths::get_singleton()->get_feature_profiles_dir().path_join(selected_profile + ".profile"))) {
359 selected_profile = String(); //does not exist
360 }
361 }
362 } else {
363 selected_profile = p_select_profile;
364 }
365
366 Vector<String> profiles;
367 Ref<DirAccess> d = DirAccess::open(EditorPaths::get_singleton()->get_feature_profiles_dir());
368 ERR_FAIL_COND_MSG(d.is_null(), "Cannot open directory '" + EditorPaths::get_singleton()->get_feature_profiles_dir() + "'.");
369
370 d->list_dir_begin();
371 while (true) {
372 String f = d->get_next();
373 if (f.is_empty()) {
374 break;
375 }
376
377 if (!d->current_is_dir()) {
378 int last_pos = f.rfind(".profile");
379 if (last_pos != -1) {
380 profiles.push_back(f.substr(0, last_pos));
381 }
382 }
383 }
384
385 profiles.sort();
386
387 profile_list->clear();
388
389 for (int i = 0; i < profiles.size(); i++) {
390 String name = profiles[i];
391
392 if (i == 0 && selected_profile.is_empty()) {
393 selected_profile = name;
394 }
395
396 if (name == current_profile) {
397 name += " " + TTR("(current)");
398 }
399 profile_list->add_item(name);
400 int index = profile_list->get_item_count() - 1;
401 profile_list->set_item_metadata(index, profiles[i]);
402 if (profiles[i] == selected_profile) {
403 profile_list->select(index);
404 }
405 }
406
407 class_list_vbc->set_visible(!selected_profile.is_empty());
408 property_list_vbc->set_visible(!selected_profile.is_empty());
409 no_profile_selected_help->set_visible(selected_profile.is_empty());
410 profile_actions[PROFILE_CLEAR]->set_disabled(current_profile.is_empty());
411 profile_actions[PROFILE_ERASE]->set_disabled(selected_profile.is_empty());
412 profile_actions[PROFILE_EXPORT]->set_disabled(selected_profile.is_empty());
413 profile_actions[PROFILE_SET]->set_disabled(selected_profile.is_empty());
414
415 current_profile_name->set_text(!current_profile.is_empty() ? current_profile : TTR("(none)"));
416
417 _update_selected_profile();
418}
419
420void EditorFeatureProfileManager::_profile_action(int p_action) {
421 switch (p_action) {
422 case PROFILE_CLEAR: {
423 set_current_profile("", false);
424 } break;
425 case PROFILE_SET: {
426 String selected = _get_selected_profile();
427 ERR_FAIL_COND(selected.is_empty());
428 if (selected == current_profile) {
429 return; // Nothing to do here.
430 }
431 set_current_profile(selected, false);
432 } break;
433 case PROFILE_IMPORT: {
434 import_profiles->popup_file_dialog();
435 } break;
436 case PROFILE_EXPORT: {
437 export_profile->popup_file_dialog();
438 export_profile->set_current_file(_get_selected_profile() + ".profile");
439 } break;
440 case PROFILE_NEW: {
441 new_profile_dialog->popup_centered(Size2(240, 60) * EDSCALE);
442 new_profile_name->clear();
443 new_profile_name->grab_focus();
444 } break;
445 case PROFILE_ERASE: {
446 String selected = _get_selected_profile();
447 ERR_FAIL_COND(selected.is_empty());
448
449 erase_profile_dialog->set_text(vformat(TTR("Remove currently selected profile, '%s'? Cannot be undone."), selected));
450 erase_profile_dialog->popup_centered(Size2(240, 60) * EDSCALE);
451 } break;
452 }
453}
454
455void EditorFeatureProfileManager::_erase_selected_profile() {
456 String selected = _get_selected_profile();
457 ERR_FAIL_COND(selected.is_empty());
458 Ref<DirAccess> da = DirAccess::open(EditorPaths::get_singleton()->get_feature_profiles_dir());
459 ERR_FAIL_COND_MSG(da.is_null(), "Cannot open directory '" + EditorPaths::get_singleton()->get_feature_profiles_dir() + "'.");
460
461 da->remove(selected + ".profile");
462 if (selected == current_profile) {
463 _profile_action(PROFILE_CLEAR);
464 } else {
465 _update_profile_list();
466 }
467}
468
469void EditorFeatureProfileManager::_create_new_profile() {
470 String name = new_profile_name->get_text().strip_edges();
471 if (!name.is_valid_filename() || name.contains(".")) {
472 EditorNode::get_singleton()->show_warning(TTR("Profile must be a valid filename and must not contain '.'"));
473 return;
474 }
475 String file = EditorPaths::get_singleton()->get_feature_profiles_dir().path_join(name + ".profile");
476 if (FileAccess::exists(file)) {
477 EditorNode::get_singleton()->show_warning(TTR("Profile with this name already exists."));
478 return;
479 }
480
481 Ref<EditorFeatureProfile> new_profile;
482 new_profile.instantiate();
483 new_profile->save_to_file(file);
484
485 _update_profile_list(name);
486 // The newly created profile is the first one, make it the current profile automatically.
487 if (profile_list->get_item_count() == 1) {
488 _profile_action(PROFILE_SET);
489 }
490}
491
492void EditorFeatureProfileManager::_profile_selected(int p_what) {
493 _update_selected_profile();
494}
495
496void EditorFeatureProfileManager::_fill_classes_from(TreeItem *p_parent, const String &p_class, const String &p_selected) {
497 TreeItem *class_item = class_list->create_item(p_parent);
498 class_item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
499 class_item->set_icon(0, EditorNode::get_singleton()->get_class_icon(p_class));
500 String text = p_class;
501
502 bool disabled = edited->is_class_disabled(p_class);
503 bool disabled_editor = edited->is_class_editor_disabled(p_class);
504 bool disabled_properties = edited->has_class_properties_disabled(p_class);
505 if (disabled) {
506 class_item->set_custom_color(0, class_list->get_theme_color(SNAME("disabled_font_color"), EditorStringName(Editor)));
507 } else if (disabled_editor && disabled_properties) {
508 text += " " + TTR("(Editor Disabled, Properties Disabled)");
509 } else if (disabled_properties) {
510 text += " " + TTR("(Properties Disabled)");
511 } else if (disabled_editor) {
512 text += " " + TTR("(Editor Disabled)");
513 }
514 class_item->set_text(0, text);
515 class_item->set_editable(0, true);
516 class_item->set_selectable(0, true);
517 class_item->set_metadata(0, p_class);
518
519 bool collapsed = edited->is_item_collapsed(p_class);
520 class_item->set_collapsed(collapsed);
521
522 if (p_class == p_selected) {
523 class_item->select(0);
524 }
525 if (disabled) {
526 // Class disabled, do nothing else (do not show further).
527 return;
528 }
529
530 class_item->set_checked(0, true); // If it's not disabled, it's checked.
531
532 List<StringName> child_classes;
533 ClassDB::get_direct_inheriters_from_class(p_class, &child_classes);
534 child_classes.sort_custom<StringName::AlphCompare>();
535
536 for (const StringName &name : child_classes) {
537 if (String(name).begins_with("Editor") || ClassDB::get_api_type(name) != ClassDB::API_CORE) {
538 continue;
539 }
540 _fill_classes_from(class_item, name, p_selected);
541 }
542}
543
544void EditorFeatureProfileManager::_class_list_item_selected() {
545 if (updating_features) {
546 return;
547 }
548
549 property_list->clear();
550
551 TreeItem *item = class_list->get_selected();
552 if (!item) {
553 return;
554 }
555
556 Variant md = item->get_metadata(0);
557 if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) {
558 String class_name = md;
559 String class_description;
560
561 DocTools *dd = EditorHelp::get_doc_data();
562 HashMap<String, DocData::ClassDoc>::Iterator E = dd->class_list.find(class_name);
563 if (E) {
564 class_description = DTR(E->value.brief_description);
565 }
566
567 description_bit->set_text(class_description);
568 } else if (md.get_type() == Variant::INT) {
569 int feature_id = md;
570 String feature_description = EditorFeatureProfile::get_feature_description(EditorFeatureProfile::Feature(feature_id));
571
572 description_bit->set_text(TTRGET(feature_description));
573 return;
574 } else {
575 return;
576 }
577
578 String class_name = md;
579 if (edited->is_class_disabled(class_name)) {
580 return;
581 }
582
583 updating_features = true;
584 TreeItem *root = property_list->create_item();
585 TreeItem *options = property_list->create_item(root);
586 options->set_text(0, TTR("Class Options:"));
587
588 {
589 TreeItem *option = property_list->create_item(options);
590 option->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
591 option->set_editable(0, true);
592 option->set_selectable(0, true);
593 option->set_checked(0, !edited->is_class_editor_disabled(class_name));
594 option->set_text(0, TTR("Enable Contextual Editor"));
595 option->set_metadata(0, CLASS_OPTION_DISABLE_EDITOR);
596 }
597
598 List<PropertyInfo> props;
599 ClassDB::get_property_list(class_name, &props, true);
600
601 bool has_editor_props = false;
602 for (const PropertyInfo &E : props) {
603 if (E.usage & PROPERTY_USAGE_EDITOR) {
604 has_editor_props = true;
605 break;
606 }
607 }
608
609 if (has_editor_props) {
610 TreeItem *properties = property_list->create_item(root);
611 properties->set_text(0, TTR("Class Properties:"));
612
613 const EditorPropertyNameProcessor::Style text_style = EditorPropertyNameProcessor::get_settings_style();
614 const EditorPropertyNameProcessor::Style tooltip_style = EditorPropertyNameProcessor::get_tooltip_style(text_style);
615
616 for (const PropertyInfo &E : props) {
617 String name = E.name;
618 if (!(E.usage & PROPERTY_USAGE_EDITOR)) {
619 continue;
620 }
621 const String text = EditorPropertyNameProcessor::get_singleton()->process_name(name, text_style);
622 const String tooltip = EditorPropertyNameProcessor::get_singleton()->process_name(name, tooltip_style);
623
624 TreeItem *property = property_list->create_item(properties);
625 property->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
626 property->set_editable(0, true);
627 property->set_selectable(0, true);
628 property->set_checked(0, !edited->is_class_property_disabled(class_name, name));
629 property->set_text(0, text);
630 property->set_tooltip_text(0, tooltip);
631 property->set_metadata(0, name);
632 String icon_type = Variant::get_type_name(E.type);
633 property->set_icon(0, EditorNode::get_singleton()->get_class_icon(icon_type));
634 }
635 }
636
637 updating_features = false;
638}
639
640void EditorFeatureProfileManager::_class_list_item_edited() {
641 if (updating_features) {
642 return;
643 }
644
645 TreeItem *item = class_list->get_edited();
646 if (!item) {
647 return;
648 }
649
650 bool checked = item->is_checked(0);
651
652 Variant md = item->get_metadata(0);
653 if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) {
654 String class_selected = md;
655 edited->set_disable_class(class_selected, !checked);
656 _save_and_update();
657 _update_selected_profile();
658 } else if (md.get_type() == Variant::INT) {
659 int feature_selected = md;
660 edited->set_disable_feature(EditorFeatureProfile::Feature(feature_selected), !checked);
661 _save_and_update();
662 }
663}
664
665void EditorFeatureProfileManager::_class_list_item_collapsed(Object *p_item) {
666 if (updating_features) {
667 return;
668 }
669
670 TreeItem *item = Object::cast_to<TreeItem>(p_item);
671 if (!item) {
672 return;
673 }
674
675 Variant md = item->get_metadata(0);
676 if (md.get_type() != Variant::STRING && md.get_type() != Variant::STRING_NAME) {
677 return;
678 }
679
680 String class_name = md;
681 bool collapsed = item->is_collapsed();
682 edited->set_item_collapsed(class_name, collapsed);
683}
684
685void EditorFeatureProfileManager::_property_item_edited() {
686 if (updating_features) {
687 return;
688 }
689
690 TreeItem *class_item = class_list->get_selected();
691 if (!class_item) {
692 return;
693 }
694
695 Variant md = class_item->get_metadata(0);
696 if (md.get_type() != Variant::STRING && md.get_type() != Variant::STRING_NAME) {
697 return;
698 }
699
700 String class_name = md;
701
702 TreeItem *item = property_list->get_edited();
703 if (!item) {
704 return;
705 }
706 bool checked = item->is_checked(0);
707
708 md = item->get_metadata(0);
709 if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) {
710 String property_selected = md;
711 edited->set_disable_class_property(class_name, property_selected, !checked);
712 _save_and_update();
713 _update_selected_profile();
714 } else if (md.get_type() == Variant::INT) {
715 int feature_selected = md;
716 switch (feature_selected) {
717 case CLASS_OPTION_DISABLE_EDITOR: {
718 edited->set_disable_class_editor(class_name, !checked);
719 _save_and_update();
720 _update_selected_profile();
721 } break;
722 }
723 }
724}
725
726void EditorFeatureProfileManager::_update_selected_profile() {
727 String class_selected;
728 int feature_selected = -1;
729
730 if (class_list->get_selected()) {
731 Variant md = class_list->get_selected()->get_metadata(0);
732 if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) {
733 class_selected = md;
734 } else if (md.get_type() == Variant::INT) {
735 feature_selected = md;
736 }
737 }
738
739 class_list->clear();
740
741 String profile = _get_selected_profile();
742 profile_actions[PROFILE_SET]->set_disabled(profile == current_profile);
743
744 if (profile.is_empty()) { //nothing selected, nothing edited
745 property_list->clear();
746 edited.unref();
747 return;
748 }
749
750 if (profile == current_profile) {
751 edited = current; //reuse current profile (which is what editor uses)
752 ERR_FAIL_COND(current.is_null()); //nothing selected, current should never be null
753 } else {
754 //reload edited, if different from current
755 edited.instantiate();
756 Error err = edited->load_from_file(EditorPaths::get_singleton()->get_feature_profiles_dir().path_join(profile + ".profile"));
757 ERR_FAIL_COND_MSG(err != OK, "Error when loading editor feature profile from file '" + EditorPaths::get_singleton()->get_feature_profiles_dir().path_join(profile + ".profile") + "'.");
758 }
759
760 updating_features = true;
761
762 TreeItem *root = class_list->create_item();
763
764 TreeItem *features = class_list->create_item(root);
765 TreeItem *last_feature = nullptr;
766 features->set_text(0, TTR("Main Features:"));
767 for (int i = 0; i < EditorFeatureProfile::FEATURE_MAX; i++) {
768 TreeItem *feature;
769 if (i == EditorFeatureProfile::FEATURE_IMPORT_DOCK) {
770 feature = class_list->create_item(last_feature);
771 } else {
772 feature = class_list->create_item(features);
773 last_feature = feature;
774 }
775 feature->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
776 feature->set_text(0, TTRGET(EditorFeatureProfile::get_feature_name(EditorFeatureProfile::Feature(i))));
777 feature->set_selectable(0, true);
778 feature->set_editable(0, true);
779 feature->set_metadata(0, i);
780 if (!edited->is_feature_disabled(EditorFeatureProfile::Feature(i))) {
781 feature->set_checked(0, true);
782 }
783
784 if (i == feature_selected) {
785 feature->select(0);
786 }
787 }
788
789 TreeItem *classes = class_list->create_item(root);
790 classes->set_text(0, TTR("Nodes and Classes:"));
791
792 _fill_classes_from(classes, "Node", class_selected);
793 _fill_classes_from(classes, "Resource", class_selected);
794
795 updating_features = false;
796
797 _class_list_item_selected();
798}
799
800void EditorFeatureProfileManager::_import_profiles(const Vector<String> &p_paths) {
801 //test it first
802 for (int i = 0; i < p_paths.size(); i++) {
803 Ref<EditorFeatureProfile> profile;
804 profile.instantiate();
805 Error err = profile->load_from_file(p_paths[i]);
806 String basefile = p_paths[i].get_file();
807 if (err != OK) {
808 EditorNode::get_singleton()->show_warning(vformat(TTR("File '%s' format is invalid, import aborted."), basefile));
809 return;
810 }
811
812 String dst_file = EditorPaths::get_singleton()->get_feature_profiles_dir().path_join(basefile);
813
814 if (FileAccess::exists(dst_file)) {
815 EditorNode::get_singleton()->show_warning(vformat(TTR("Profile '%s' already exists. Remove it first before importing, import aborted."), basefile.get_basename()));
816 return;
817 }
818 }
819
820 //do it second
821 for (int i = 0; i < p_paths.size(); i++) {
822 Ref<EditorFeatureProfile> profile;
823 profile.instantiate();
824 Error err = profile->load_from_file(p_paths[i]);
825 ERR_CONTINUE(err != OK);
826 String basefile = p_paths[i].get_file();
827 String dst_file = EditorPaths::get_singleton()->get_feature_profiles_dir().path_join(basefile);
828 profile->save_to_file(dst_file);
829 }
830
831 _update_profile_list();
832 // The newly imported profile is the first one, make it the current profile automatically.
833 if (profile_list->get_item_count() == 1) {
834 _profile_action(PROFILE_SET);
835 }
836}
837
838void EditorFeatureProfileManager::_export_profile(const String &p_path) {
839 ERR_FAIL_COND(edited.is_null());
840 Error err = edited->save_to_file(p_path);
841 if (err != OK) {
842 EditorNode::get_singleton()->show_warning(vformat(TTR("Error saving profile to path: '%s'."), p_path));
843 }
844}
845
846void EditorFeatureProfileManager::_save_and_update() {
847 String edited_path = _get_selected_profile();
848 ERR_FAIL_COND(edited_path.is_empty());
849 ERR_FAIL_COND(edited.is_null());
850
851 edited->save_to_file(EditorPaths::get_singleton()->get_feature_profiles_dir().path_join(edited_path + ".profile"));
852
853 if (edited == current) {
854 update_timer->start();
855 }
856}
857
858void EditorFeatureProfileManager::_emit_current_profile_changed() {
859 emit_signal(SNAME("current_feature_profile_changed"));
860}
861
862void EditorFeatureProfileManager::notify_changed() {
863 _emit_current_profile_changed();
864}
865
866Ref<EditorFeatureProfile> EditorFeatureProfileManager::get_current_profile() {
867 return current;
868}
869
870String EditorFeatureProfileManager::get_current_profile_name() const {
871 return current_profile;
872}
873
874void EditorFeatureProfileManager::set_current_profile(const String &p_profile_name, bool p_validate_profile) {
875 if (p_validate_profile && !p_profile_name.is_empty()) {
876 // Profile may not exist.
877 Ref<DirAccess> da = DirAccess::open(EditorPaths::get_singleton()->get_feature_profiles_dir());
878 ERR_FAIL_COND_MSG(da.is_null(), "Cannot open directory '" + EditorPaths::get_singleton()->get_feature_profiles_dir() + "'.");
879 ERR_FAIL_COND_MSG(!da->file_exists(p_profile_name + ".profile"), "Feature profile '" + p_profile_name + "' does not exist.");
880
881 // Change profile selection to emulate the UI interaction. Otherwise, the wrong profile would get activated.
882 // FIXME: Ideally, _update_selected_profile() should not rely on the user interface state to function properly.
883 for (int i = 0; i < profile_list->get_item_count(); i++) {
884 if (profile_list->get_item_metadata(i) == p_profile_name) {
885 profile_list->select(i);
886 break;
887 }
888 }
889 _update_selected_profile();
890 }
891
892 // Store in editor settings.
893 EditorSettings::get_singleton()->set("_default_feature_profile", p_profile_name);
894 EditorSettings::get_singleton()->save();
895
896 current_profile = p_profile_name;
897 if (p_profile_name.is_empty()) {
898 current.unref();
899 } else {
900 current = edited;
901 }
902 _update_profile_list();
903 _emit_current_profile_changed();
904}
905
906EditorFeatureProfileManager *EditorFeatureProfileManager::singleton = nullptr;
907
908void EditorFeatureProfileManager::_bind_methods() {
909 ADD_SIGNAL(MethodInfo("current_feature_profile_changed"));
910}
911
912EditorFeatureProfileManager::EditorFeatureProfileManager() {
913 VBoxContainer *main_vbc = memnew(VBoxContainer);
914 add_child(main_vbc);
915
916 HBoxContainer *name_hbc = memnew(HBoxContainer);
917 current_profile_name = memnew(LineEdit);
918 name_hbc->add_child(current_profile_name);
919 current_profile_name->set_text(TTR("(none)"));
920 current_profile_name->set_editable(false);
921 current_profile_name->set_h_size_flags(Control::SIZE_EXPAND_FILL);
922 profile_actions[PROFILE_CLEAR] = memnew(Button(TTR("Reset to Default")));
923 name_hbc->add_child(profile_actions[PROFILE_CLEAR]);
924 profile_actions[PROFILE_CLEAR]->set_disabled(true);
925 profile_actions[PROFILE_CLEAR]->connect("pressed", callable_mp(this, &EditorFeatureProfileManager::_profile_action).bind(PROFILE_CLEAR));
926
927 main_vbc->add_margin_child(TTR("Current Profile:"), name_hbc);
928
929 main_vbc->add_child(memnew(HSeparator));
930
931 HBoxContainer *profiles_hbc = memnew(HBoxContainer);
932 profile_list = memnew(OptionButton);
933 profile_list->set_h_size_flags(Control::SIZE_EXPAND_FILL);
934 profile_list->set_auto_translate(false);
935 profiles_hbc->add_child(profile_list);
936 profile_list->connect("item_selected", callable_mp(this, &EditorFeatureProfileManager::_profile_selected));
937
938 profile_actions[PROFILE_NEW] = memnew(Button(TTR("Create Profile")));
939 profiles_hbc->add_child(profile_actions[PROFILE_NEW]);
940 profile_actions[PROFILE_NEW]->connect("pressed", callable_mp(this, &EditorFeatureProfileManager::_profile_action).bind(PROFILE_NEW));
941
942 profile_actions[PROFILE_ERASE] = memnew(Button(TTR("Remove Profile")));
943 profiles_hbc->add_child(profile_actions[PROFILE_ERASE]);
944 profile_actions[PROFILE_ERASE]->set_disabled(true);
945 profile_actions[PROFILE_ERASE]->connect("pressed", callable_mp(this, &EditorFeatureProfileManager::_profile_action).bind(PROFILE_ERASE));
946
947 main_vbc->add_margin_child(TTR("Available Profiles:"), profiles_hbc);
948
949 HBoxContainer *current_profile_hbc = memnew(HBoxContainer);
950
951 profile_actions[PROFILE_SET] = memnew(Button(TTR("Make Current")));
952 current_profile_hbc->add_child(profile_actions[PROFILE_SET]);
953 profile_actions[PROFILE_SET]->set_disabled(true);
954 profile_actions[PROFILE_SET]->connect("pressed", callable_mp(this, &EditorFeatureProfileManager::_profile_action).bind(PROFILE_SET));
955
956 current_profile_hbc->add_child(memnew(VSeparator));
957
958 profile_actions[PROFILE_IMPORT] = memnew(Button(TTR("Import")));
959 current_profile_hbc->add_child(profile_actions[PROFILE_IMPORT]);
960 profile_actions[PROFILE_IMPORT]->connect("pressed", callable_mp(this, &EditorFeatureProfileManager::_profile_action).bind(PROFILE_IMPORT));
961
962 profile_actions[PROFILE_EXPORT] = memnew(Button(TTR("Export")));
963 current_profile_hbc->add_child(profile_actions[PROFILE_EXPORT]);
964 profile_actions[PROFILE_EXPORT]->set_disabled(true);
965 profile_actions[PROFILE_EXPORT]->connect("pressed", callable_mp(this, &EditorFeatureProfileManager::_profile_action).bind(PROFILE_EXPORT));
966
967 main_vbc->add_child(current_profile_hbc);
968
969 h_split = memnew(HSplitContainer);
970 h_split->set_v_size_flags(Control::SIZE_EXPAND_FILL);
971 main_vbc->add_child(h_split);
972
973 class_list_vbc = memnew(VBoxContainer);
974 h_split->add_child(class_list_vbc);
975 class_list_vbc->set_h_size_flags(Control::SIZE_EXPAND_FILL);
976
977 class_list = memnew(Tree);
978 class_list_vbc->add_margin_child(TTR("Configure Selected Profile:"), class_list, true);
979 class_list->set_hide_root(true);
980 class_list->set_edit_checkbox_cell_only_when_checkbox_is_pressed(true);
981 class_list->connect("cell_selected", callable_mp(this, &EditorFeatureProfileManager::_class_list_item_selected));
982 class_list->connect("item_edited", callable_mp(this, &EditorFeatureProfileManager::_class_list_item_edited), CONNECT_DEFERRED);
983 class_list->connect("item_collapsed", callable_mp(this, &EditorFeatureProfileManager::_class_list_item_collapsed));
984 // It will be displayed once the user creates or chooses a profile.
985 class_list_vbc->hide();
986
987 property_list_vbc = memnew(VBoxContainer);
988 h_split->add_child(property_list_vbc);
989 property_list_vbc->set_h_size_flags(Control::SIZE_EXPAND_FILL);
990
991 description_bit = memnew(EditorHelpBit);
992 property_list_vbc->add_margin_child(TTR("Description:"), description_bit, false);
993 description_bit->set_custom_minimum_size(Size2(0, 80) * EDSCALE);
994
995 property_list = memnew(Tree);
996 property_list_vbc->add_margin_child(TTR("Extra Options:"), property_list, true);
997 property_list->set_hide_root(true);
998 property_list->set_hide_folding(true);
999 property_list->set_edit_checkbox_cell_only_when_checkbox_is_pressed(true);
1000 property_list->connect("item_edited", callable_mp(this, &EditorFeatureProfileManager::_property_item_edited), CONNECT_DEFERRED);
1001 // It will be displayed once the user creates or chooses a profile.
1002 property_list_vbc->hide();
1003
1004 no_profile_selected_help = memnew(Label(TTR("Create or import a profile to edit available classes and properties.")));
1005 // Add some spacing above the help label.
1006 Ref<StyleBoxEmpty> sb = memnew(StyleBoxEmpty);
1007 sb->set_content_margin(SIDE_TOP, 20 * EDSCALE);
1008 no_profile_selected_help->add_theme_style_override("normal", sb);
1009 no_profile_selected_help->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
1010 no_profile_selected_help->set_v_size_flags(Control::SIZE_EXPAND_FILL);
1011 h_split->add_child(no_profile_selected_help);
1012
1013 new_profile_dialog = memnew(ConfirmationDialog);
1014 new_profile_dialog->set_title(TTR("Create Profile"));
1015 VBoxContainer *new_profile_vb = memnew(VBoxContainer);
1016 new_profile_dialog->add_child(new_profile_vb);
1017 Label *new_profile_label = memnew(Label);
1018 new_profile_label->set_text(TTR("New profile name:"));
1019 new_profile_vb->add_child(new_profile_label);
1020 new_profile_name = memnew(LineEdit);
1021 new_profile_vb->add_child(new_profile_name);
1022 new_profile_name->set_custom_minimum_size(Size2(300 * EDSCALE, 1));
1023 add_child(new_profile_dialog);
1024 new_profile_dialog->connect("confirmed", callable_mp(this, &EditorFeatureProfileManager::_create_new_profile));
1025 new_profile_dialog->register_text_enter(new_profile_name);
1026 new_profile_dialog->set_ok_button_text(TTR("Create"));
1027
1028 erase_profile_dialog = memnew(ConfirmationDialog);
1029 add_child(erase_profile_dialog);
1030 erase_profile_dialog->set_title(TTR("Remove Profile"));
1031 erase_profile_dialog->connect("confirmed", callable_mp(this, &EditorFeatureProfileManager::_erase_selected_profile));
1032
1033 import_profiles = memnew(EditorFileDialog);
1034 add_child(import_profiles);
1035 import_profiles->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILES);
1036 import_profiles->add_filter("*.profile", TTR("Godot Feature Profile"));
1037 import_profiles->connect("files_selected", callable_mp(this, &EditorFeatureProfileManager::_import_profiles));
1038 import_profiles->set_title(TTR("Import Profile(s)"));
1039 import_profiles->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
1040
1041 export_profile = memnew(EditorFileDialog);
1042 add_child(export_profile);
1043 export_profile->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE);
1044 export_profile->add_filter("*.profile", TTR("Godot Feature Profile"));
1045 export_profile->connect("file_selected", callable_mp(this, &EditorFeatureProfileManager::_export_profile));
1046 export_profile->set_title(TTR("Export Profile"));
1047 export_profile->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
1048
1049 set_title(TTR("Manage Editor Feature Profiles"));
1050 EDITOR_DEF("_default_feature_profile", "");
1051
1052 update_timer = memnew(Timer);
1053 update_timer->set_wait_time(1); //wait a second before updating editor
1054 add_child(update_timer);
1055 update_timer->connect("timeout", callable_mp(this, &EditorFeatureProfileManager::_emit_current_profile_changed));
1056 update_timer->set_one_shot(true);
1057
1058 singleton = this;
1059}
1060