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 | |
43 | const 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 | |
54 | const 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 | |
65 | const 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 | |
76 | void 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 | |
84 | bool 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 | |
91 | void 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 | |
99 | bool 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 | |
106 | void 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 | |
122 | bool 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 | |
134 | bool EditorFeatureProfile::has_class_properties_disabled(const StringName &p_class) const { |
135 | return disabled_properties.has(p_class); |
136 | } |
137 | |
138 | void 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 | |
146 | bool EditorFeatureProfile::is_item_collapsed(const StringName &p_class) const { |
147 | return collapsed_classes.has(p_class); |
148 | } |
149 | |
150 | void 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 | |
155 | bool 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 | |
160 | String 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 | |
165 | String 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 | |
170 | Error 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 | |
215 | Error 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 | |
284 | void 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 | |
313 | EditorFeatureProfile::EditorFeatureProfile() { |
314 | for (int i = 0; i < FEATURE_MAX; i++) { |
315 | features_disabled[i] = false; |
316 | } |
317 | } |
318 | |
319 | ////////////////////////// |
320 | |
321 | void 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 | |
344 | String 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 | |
353 | void 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 | |
420 | void 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 | |
455 | void 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 | |
469 | void 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 | |
492 | void EditorFeatureProfileManager::_profile_selected(int p_what) { |
493 | _update_selected_profile(); |
494 | } |
495 | |
496 | void 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 | |
544 | void 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 | |
640 | void 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 | |
665 | void 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 | |
685 | void 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 | |
726 | void 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 | |
800 | void 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 | |
838 | void 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 | |
846 | void 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 | |
858 | void EditorFeatureProfileManager::_emit_current_profile_changed() { |
859 | emit_signal(SNAME("current_feature_profile_changed" )); |
860 | } |
861 | |
862 | void EditorFeatureProfileManager::notify_changed() { |
863 | _emit_current_profile_changed(); |
864 | } |
865 | |
866 | Ref<EditorFeatureProfile> EditorFeatureProfileManager::get_current_profile() { |
867 | return current; |
868 | } |
869 | |
870 | String EditorFeatureProfileManager::get_current_profile_name() const { |
871 | return current_profile; |
872 | } |
873 | |
874 | void 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 | |
906 | EditorFeatureProfileManager *EditorFeatureProfileManager::singleton = nullptr; |
907 | |
908 | void EditorFeatureProfileManager::_bind_methods() { |
909 | ADD_SIGNAL(MethodInfo("current_feature_profile_changed" )); |
910 | } |
911 | |
912 | EditorFeatureProfileManager::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 | |