1 | /**************************************************************************/ |
2 | /* editor_resource_picker.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_resource_picker.h" |
32 | |
33 | #include "editor/audio_stream_preview.h" |
34 | #include "editor/editor_node.h" |
35 | #include "editor/editor_quick_open.h" |
36 | #include "editor/editor_resource_preview.h" |
37 | #include "editor/editor_scale.h" |
38 | #include "editor/editor_settings.h" |
39 | #include "editor/editor_string_names.h" |
40 | #include "editor/filesystem_dock.h" |
41 | #include "editor/gui/editor_file_dialog.h" |
42 | #include "editor/plugins/editor_resource_conversion_plugin.h" |
43 | #include "editor/plugins/script_editor_plugin.h" |
44 | #include "editor/scene_tree_dock.h" |
45 | #include "scene/gui/button.h" |
46 | #include "scene/gui/texture_rect.h" |
47 | #include "scene/resources/gradient_texture.h" |
48 | #include "scene/resources/image_texture.h" |
49 | |
50 | void EditorResourcePicker::_update_resource() { |
51 | String resource_path; |
52 | if (edited_resource.is_valid() && edited_resource->get_path().is_resource_file()) { |
53 | resource_path = edited_resource->get_path() + "\n" ; |
54 | } |
55 | String class_name = _get_resource_type(edited_resource); |
56 | |
57 | if (preview_rect) { |
58 | preview_rect->set_texture(Ref<Texture2D>()); |
59 | |
60 | assign_button->set_custom_minimum_size(assign_button_min_size); |
61 | |
62 | if (edited_resource == Ref<Resource>()) { |
63 | assign_button->set_icon(Ref<Texture2D>()); |
64 | assign_button->set_text(TTR("<empty>" )); |
65 | assign_button->set_tooltip_text("" ); |
66 | } else { |
67 | assign_button->set_icon(EditorNode::get_singleton()->get_object_icon(edited_resource.operator->(), SNAME("Object" ))); |
68 | |
69 | if (!edited_resource->get_name().is_empty()) { |
70 | assign_button->set_text(edited_resource->get_name()); |
71 | } else if (edited_resource->get_path().is_resource_file()) { |
72 | assign_button->set_text(edited_resource->get_path().get_file()); |
73 | } else { |
74 | assign_button->set_text(class_name); |
75 | } |
76 | |
77 | if (edited_resource->get_path().is_resource_file()) { |
78 | resource_path = edited_resource->get_path() + "\n" ; |
79 | } |
80 | assign_button->set_tooltip_text(resource_path + TTR("Type:" ) + " " + class_name); |
81 | |
82 | // Preview will override the above, so called at the end. |
83 | EditorResourcePreview::get_singleton()->queue_edited_resource_preview(edited_resource, this, "_update_resource_preview" , edited_resource->get_instance_id()); |
84 | } |
85 | } else if (edited_resource.is_valid()) { |
86 | assign_button->set_tooltip_text(resource_path + TTR("Type:" ) + " " + edited_resource->get_class()); |
87 | } |
88 | |
89 | assign_button->set_disabled(!editable && !edited_resource.is_valid()); |
90 | } |
91 | |
92 | void EditorResourcePicker::_update_resource_preview(const String &p_path, const Ref<Texture2D> &p_preview, const Ref<Texture2D> &p_small_preview, ObjectID p_obj) { |
93 | if (!edited_resource.is_valid() || edited_resource->get_instance_id() != p_obj) { |
94 | return; |
95 | } |
96 | |
97 | if (preview_rect) { |
98 | Ref<Script> scr = edited_resource; |
99 | if (scr.is_valid()) { |
100 | assign_button->set_text(scr->get_path().get_file()); |
101 | return; |
102 | } |
103 | |
104 | if (p_preview.is_valid()) { |
105 | preview_rect->set_offset(SIDE_LEFT, assign_button->get_icon()->get_width() + assign_button->get_theme_stylebox(SNAME("normal" ))->get_content_margin(SIDE_LEFT) + get_theme_constant(SNAME("h_separation" ), SNAME("Button" ))); |
106 | |
107 | // Resource-specific stretching. |
108 | if (Ref<GradientTexture1D>(edited_resource).is_valid() || Ref<Gradient>(edited_resource).is_valid()) { |
109 | preview_rect->set_stretch_mode(TextureRect::STRETCH_SCALE); |
110 | assign_button->set_custom_minimum_size(assign_button_min_size); |
111 | } else { |
112 | preview_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED); |
113 | int thumbnail_size = EDITOR_GET("filesystem/file_dialog/thumbnail_size" ); |
114 | thumbnail_size *= EDSCALE; |
115 | assign_button->set_custom_minimum_size(Size2(MAX(1, assign_button_min_size.x), MAX(thumbnail_size, assign_button_min_size.y))); |
116 | } |
117 | |
118 | preview_rect->set_texture(p_preview); |
119 | assign_button->set_text("" ); |
120 | } |
121 | } |
122 | } |
123 | |
124 | void EditorResourcePicker::_resource_selected() { |
125 | if (edited_resource.is_null()) { |
126 | edit_button->set_pressed(true); |
127 | _update_menu(); |
128 | return; |
129 | } |
130 | |
131 | emit_signal(SNAME("resource_selected" ), edited_resource, false); |
132 | } |
133 | |
134 | void EditorResourcePicker::_file_selected(const String &p_path) { |
135 | Ref<Resource> loaded_resource = ResourceLoader::load(p_path); |
136 | ERR_FAIL_COND_MSG(loaded_resource.is_null(), "Cannot load resource from path '" + p_path + "'." ); |
137 | |
138 | if (!base_type.is_empty()) { |
139 | bool any_type_matches = false; |
140 | |
141 | String res_type = loaded_resource->get_class(); |
142 | Ref<Script> res_script = loaded_resource->get_script(); |
143 | bool is_global_class = false; |
144 | if (res_script.is_valid()) { |
145 | String script_type = EditorNode::get_editor_data().script_class_get_name(res_script->get_path()); |
146 | if (!script_type.is_empty()) { |
147 | is_global_class = true; |
148 | res_type = script_type; |
149 | } |
150 | } |
151 | |
152 | for (int i = 0; i < base_type.get_slice_count("," ); i++) { |
153 | String base = base_type.get_slice("," , i); |
154 | |
155 | any_type_matches = is_global_class ? EditorNode::get_editor_data().script_class_is_parent(res_type, base) : loaded_resource->is_class(base); |
156 | |
157 | if (any_type_matches) { |
158 | break; |
159 | } |
160 | } |
161 | |
162 | if (!any_type_matches) { |
163 | EditorNode::get_singleton()->show_warning(vformat(TTR("The selected resource (%s) does not match any type expected for this property (%s)." ), res_type, base_type)); |
164 | return; |
165 | } |
166 | } |
167 | |
168 | edited_resource = loaded_resource; |
169 | emit_signal(SNAME("resource_changed" ), edited_resource); |
170 | _update_resource(); |
171 | } |
172 | |
173 | void EditorResourcePicker::_file_quick_selected() { |
174 | _file_selected(quick_open->get_selected()); |
175 | } |
176 | |
177 | void EditorResourcePicker::() { |
178 | _update_menu_items(); |
179 | |
180 | Rect2 gt = edit_button->get_screen_rect(); |
181 | edit_menu->reset_size(); |
182 | int ms = edit_menu->get_contents_minimum_size().width; |
183 | Vector2 = gt.get_end() - Vector2(ms, 0); |
184 | edit_menu->set_position(popup_pos); |
185 | edit_menu->popup(); |
186 | } |
187 | |
188 | void EditorResourcePicker::() { |
189 | _ensure_resource_menu(); |
190 | edit_menu->clear(); |
191 | |
192 | // Add options for creating specific subtypes of the base resource type. |
193 | if (is_editable()) { |
194 | set_create_options(edit_menu); |
195 | |
196 | // Add an option to load a resource from a file using the QuickOpen dialog. |
197 | edit_menu->add_icon_item(get_editor_theme_icon(SNAME("Load" )), TTR("Quick Load" ), OBJ_MENU_QUICKLOAD); |
198 | |
199 | // Add an option to load a resource from a file using the regular file dialog. |
200 | edit_menu->add_icon_item(get_editor_theme_icon(SNAME("Load" )), TTR("Load" ), OBJ_MENU_LOAD); |
201 | } |
202 | |
203 | // Add options for changing existing value of the resource. |
204 | if (edited_resource.is_valid()) { |
205 | // Determine if the edited resource is part of another scene (foreign) which was imported |
206 | bool is_edited_resource_foreign_import = EditorNode::get_singleton()->is_resource_read_only(edited_resource, true); |
207 | |
208 | // If the resource is determined to be foreign and imported, change the menu entry's description to 'inspect' rather than 'edit' |
209 | // since will only be able to view its properties in read-only mode. |
210 | if (is_edited_resource_foreign_import) { |
211 | // The 'Search' icon is a magnifying glass, which seems appropriate, but maybe a bespoke icon is preferred here. |
212 | edit_menu->add_icon_item(get_editor_theme_icon(SNAME("Search" )), TTR("Inspect" ), OBJ_MENU_INSPECT); |
213 | } else { |
214 | edit_menu->add_icon_item(get_editor_theme_icon(SNAME("Edit" )), TTR("Edit" ), OBJ_MENU_INSPECT); |
215 | } |
216 | |
217 | if (is_editable()) { |
218 | edit_menu->add_icon_item(get_editor_theme_icon(SNAME("Clear" )), TTR("Clear" ), OBJ_MENU_CLEAR); |
219 | edit_menu->add_icon_item(get_editor_theme_icon(SNAME("Duplicate" )), TTR("Make Unique" ), OBJ_MENU_MAKE_UNIQUE); |
220 | |
221 | // Check whether the resource has subresources. |
222 | List<PropertyInfo> property_list; |
223 | edited_resource->get_property_list(&property_list); |
224 | bool has_subresources = false; |
225 | for (PropertyInfo &p : property_list) { |
226 | if ((p.type == Variant::OBJECT) && (p.hint == PROPERTY_HINT_RESOURCE_TYPE) && (p.name != "script" ) && ((Object *)edited_resource->get(p.name) != nullptr)) { |
227 | has_subresources = true; |
228 | break; |
229 | } |
230 | } |
231 | if (has_subresources) { |
232 | edit_menu->add_icon_item(get_editor_theme_icon(SNAME("Duplicate" )), TTR("Make Unique (Recursive)" ), OBJ_MENU_MAKE_UNIQUE_RECURSIVE); |
233 | } |
234 | |
235 | edit_menu->add_icon_item(get_editor_theme_icon(SNAME("Save" )), TTR("Save" ), OBJ_MENU_SAVE); |
236 | } |
237 | |
238 | if (edited_resource->get_path().is_resource_file()) { |
239 | edit_menu->add_separator(); |
240 | edit_menu->add_item(TTR("Show in FileSystem" ), OBJ_MENU_SHOW_IN_FILE_SYSTEM); |
241 | } |
242 | } |
243 | |
244 | // Add options to copy/paste resource. |
245 | Ref<Resource> cb = EditorSettings::get_singleton()->get_resource_clipboard(); |
246 | bool paste_valid = false; |
247 | if (is_editable() && cb.is_valid()) { |
248 | if (base_type.is_empty()) { |
249 | paste_valid = true; |
250 | } else { |
251 | String res_type = _get_resource_type(cb); |
252 | |
253 | for (int i = 0; i < base_type.get_slice_count("," ); i++) { |
254 | String base = base_type.get_slice("," , i); |
255 | |
256 | paste_valid = ClassDB::is_parent_class(res_type, base) || EditorNode::get_editor_data().script_class_is_parent(res_type, base); |
257 | |
258 | if (paste_valid) { |
259 | break; |
260 | } |
261 | } |
262 | } |
263 | } |
264 | |
265 | if (edited_resource.is_valid() || paste_valid) { |
266 | edit_menu->add_separator(); |
267 | |
268 | if (edited_resource.is_valid()) { |
269 | edit_menu->add_item(TTR("Copy" ), OBJ_MENU_COPY); |
270 | } |
271 | |
272 | if (paste_valid) { |
273 | edit_menu->add_item(TTR("Paste" ), OBJ_MENU_PASTE); |
274 | } |
275 | } |
276 | |
277 | // Add options to convert existing resource to another type of resource. |
278 | if (is_editable() && edited_resource.is_valid()) { |
279 | Vector<Ref<EditorResourceConversionPlugin>> conversions = EditorNode::get_singleton()->find_resource_conversion_plugin(edited_resource); |
280 | if (conversions.size()) { |
281 | edit_menu->add_separator(); |
282 | } |
283 | for (int i = 0; i < conversions.size(); i++) { |
284 | String what = conversions[i]->converts_to(); |
285 | Ref<Texture2D> icon; |
286 | if (has_theme_icon(what, EditorStringName(EditorIcons))) { |
287 | icon = get_editor_theme_icon(what); |
288 | } else { |
289 | icon = get_theme_icon(what, SNAME("Resource" )); |
290 | } |
291 | |
292 | edit_menu->add_icon_item(icon, vformat(TTR("Convert to %s" ), what), CONVERT_BASE_ID + i); |
293 | } |
294 | } |
295 | } |
296 | |
297 | void EditorResourcePicker::(int p_which) { |
298 | switch (p_which) { |
299 | case OBJ_MENU_LOAD: { |
300 | List<String> extensions; |
301 | for (int i = 0; i < base_type.get_slice_count("," ); i++) { |
302 | String base = base_type.get_slice("," , i); |
303 | ResourceLoader::get_recognized_extensions_for_type(base, &extensions); |
304 | if (ScriptServer::is_global_class(base)) { |
305 | ResourceLoader::get_recognized_extensions_for_type(ScriptServer::get_global_class_native_base(base), &extensions); |
306 | } |
307 | } |
308 | |
309 | HashSet<String> valid_extensions; |
310 | for (const String &E : extensions) { |
311 | valid_extensions.insert(E); |
312 | } |
313 | |
314 | if (!file_dialog) { |
315 | file_dialog = memnew(EditorFileDialog); |
316 | file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE); |
317 | add_child(file_dialog); |
318 | file_dialog->connect("file_selected" , callable_mp(this, &EditorResourcePicker::_file_selected)); |
319 | } |
320 | |
321 | file_dialog->clear_filters(); |
322 | for (const String &E : valid_extensions) { |
323 | file_dialog->add_filter("*." + E, E.to_upper()); |
324 | } |
325 | |
326 | file_dialog->popup_file_dialog(); |
327 | } break; |
328 | |
329 | case OBJ_MENU_QUICKLOAD: { |
330 | if (!quick_open) { |
331 | quick_open = memnew(EditorQuickOpen); |
332 | add_child(quick_open); |
333 | quick_open->connect("quick_open" , callable_mp(this, &EditorResourcePicker::_file_quick_selected)); |
334 | } |
335 | |
336 | quick_open->popup_dialog(base_type); |
337 | quick_open->set_title(TTR("Resource" )); |
338 | } break; |
339 | |
340 | case OBJ_MENU_INSPECT: { |
341 | if (edited_resource.is_valid()) { |
342 | emit_signal(SNAME("resource_selected" ), edited_resource, true); |
343 | } |
344 | } break; |
345 | |
346 | case OBJ_MENU_CLEAR: { |
347 | edited_resource = Ref<Resource>(); |
348 | emit_signal(SNAME("resource_changed" ), edited_resource); |
349 | _update_resource(); |
350 | } break; |
351 | |
352 | case OBJ_MENU_MAKE_UNIQUE: { |
353 | if (edited_resource.is_null()) { |
354 | return; |
355 | } |
356 | |
357 | Ref<Resource> unique_resource = edited_resource->duplicate(); |
358 | ERR_FAIL_COND(unique_resource.is_null()); // duplicate() may fail. |
359 | |
360 | edited_resource = unique_resource; |
361 | emit_signal(SNAME("resource_changed" ), edited_resource); |
362 | _update_resource(); |
363 | } break; |
364 | |
365 | case OBJ_MENU_MAKE_UNIQUE_RECURSIVE: { |
366 | if (edited_resource.is_null()) { |
367 | return; |
368 | } |
369 | |
370 | if (!duplicate_resources_dialog) { |
371 | duplicate_resources_dialog = memnew(ConfirmationDialog); |
372 | add_child(duplicate_resources_dialog); |
373 | duplicate_resources_dialog->set_title(TTR("Make Unique (Recursive)" )); |
374 | duplicate_resources_dialog->connect("confirmed" , callable_mp(this, &EditorResourcePicker::_duplicate_selected_resources)); |
375 | |
376 | VBoxContainer *vb = memnew(VBoxContainer); |
377 | duplicate_resources_dialog->add_child(vb); |
378 | |
379 | Label *label = memnew(Label(TTR("Select resources to make unique:" ))); |
380 | vb->add_child(label); |
381 | |
382 | duplicate_resources_tree = memnew(Tree); |
383 | vb->add_child(duplicate_resources_tree); |
384 | duplicate_resources_tree->set_columns(2); |
385 | duplicate_resources_tree->set_v_size_flags(SIZE_EXPAND_FILL); |
386 | } |
387 | |
388 | duplicate_resources_tree->clear(); |
389 | TreeItem *root = duplicate_resources_tree->create_item(); |
390 | _gather_resources_to_duplicate(edited_resource, root); |
391 | |
392 | duplicate_resources_dialog->reset_size(); |
393 | duplicate_resources_dialog->popup_centered(Vector2(500, 400) * EDSCALE); |
394 | } break; |
395 | |
396 | case OBJ_MENU_SAVE: { |
397 | if (edited_resource.is_null()) { |
398 | return; |
399 | } |
400 | EditorNode::get_singleton()->save_resource(edited_resource); |
401 | } break; |
402 | |
403 | case OBJ_MENU_COPY: { |
404 | EditorSettings::get_singleton()->set_resource_clipboard(edited_resource); |
405 | } break; |
406 | |
407 | case OBJ_MENU_PASTE: { |
408 | edited_resource = EditorSettings::get_singleton()->get_resource_clipboard(); |
409 | if (edited_resource->is_built_in() && EditorNode::get_singleton()->get_edited_scene() && |
410 | edited_resource->get_path().get_slice("::" , 0) != EditorNode::get_singleton()->get_edited_scene()->get_scene_file_path()) { |
411 | // Automatically make resource unique if it belongs to another scene. |
412 | _edit_menu_cbk(OBJ_MENU_MAKE_UNIQUE); |
413 | return; |
414 | } |
415 | |
416 | emit_signal(SNAME("resource_changed" ), edited_resource); |
417 | _update_resource(); |
418 | } break; |
419 | |
420 | case OBJ_MENU_SHOW_IN_FILE_SYSTEM: { |
421 | FileSystemDock *file_system_dock = FileSystemDock::get_singleton(); |
422 | file_system_dock->navigate_to_path(edited_resource->get_path()); |
423 | |
424 | // Ensure that the FileSystem dock is visible. |
425 | if (file_system_dock->get_window() == get_tree()->get_root()) { |
426 | TabContainer *tab_container = (TabContainer *)file_system_dock->get_parent_control(); |
427 | tab_container->set_current_tab(tab_container->get_tab_idx_from_control(file_system_dock)); |
428 | } else { |
429 | file_system_dock->get_window()->grab_focus(); |
430 | } |
431 | } break; |
432 | |
433 | default: { |
434 | // Allow subclasses to handle their own options first, only then fallback on the default branch logic. |
435 | if (handle_menu_selected(p_which)) { |
436 | break; |
437 | } |
438 | |
439 | if (p_which >= CONVERT_BASE_ID) { |
440 | int to_type = p_which - CONVERT_BASE_ID; |
441 | Vector<Ref<EditorResourceConversionPlugin>> conversions = EditorNode::get_singleton()->find_resource_conversion_plugin(edited_resource); |
442 | ERR_FAIL_INDEX(to_type, conversions.size()); |
443 | |
444 | edited_resource = conversions[to_type]->convert(edited_resource); |
445 | emit_signal(SNAME("resource_changed" ), edited_resource); |
446 | _update_resource(); |
447 | break; |
448 | } |
449 | |
450 | ERR_FAIL_COND(inheritors_array.is_empty()); |
451 | |
452 | String intype = inheritors_array[p_which - TYPE_BASE_ID]; |
453 | Variant obj; |
454 | |
455 | if (ScriptServer::is_global_class(intype)) { |
456 | obj = EditorNode::get_editor_data().script_class_instance(intype); |
457 | } else { |
458 | obj = ClassDB::instantiate(intype); |
459 | } |
460 | |
461 | if (!obj) { |
462 | obj = EditorNode::get_editor_data().instantiate_custom_type(intype, "Resource" ); |
463 | } |
464 | |
465 | Resource *resp = Object::cast_to<Resource>(obj); |
466 | ERR_BREAK(!resp); |
467 | |
468 | EditorNode::get_editor_data().instantiate_object_properties(obj); |
469 | |
470 | edited_resource = Ref<Resource>(resp); |
471 | emit_signal(SNAME("resource_changed" ), edited_resource); |
472 | _update_resource(); |
473 | } break; |
474 | } |
475 | } |
476 | |
477 | void EditorResourcePicker::set_create_options(Object *) { |
478 | _ensure_resource_menu(); |
479 | // If a subclass implements this method, use it to replace all create items. |
480 | if (GDVIRTUAL_CALL(_set_create_options, p_menu_node)) { |
481 | return; |
482 | } |
483 | |
484 | // By default provide generic "New ..." options. |
485 | if (!base_type.is_empty()) { |
486 | int idx = 0; |
487 | |
488 | HashSet<StringName> allowed_types; |
489 | _get_allowed_types(false, &allowed_types); |
490 | |
491 | Vector<EditorData::CustomType> custom_resources; |
492 | if (EditorNode::get_editor_data().get_custom_types().has("Resource" )) { |
493 | custom_resources = EditorNode::get_editor_data().get_custom_types()["Resource" ]; |
494 | } |
495 | |
496 | for (const StringName &E : allowed_types) { |
497 | const String &t = E; |
498 | |
499 | bool is_custom_resource = false; |
500 | Ref<Texture2D> icon; |
501 | if (!custom_resources.is_empty()) { |
502 | for (int j = 0; j < custom_resources.size(); j++) { |
503 | if (custom_resources[j].name == t) { |
504 | is_custom_resource = true; |
505 | if (custom_resources[j].icon.is_valid()) { |
506 | icon = custom_resources[j].icon; |
507 | } |
508 | break; |
509 | } |
510 | } |
511 | } |
512 | |
513 | if (!is_custom_resource && !(ScriptServer::is_global_class(t) || ClassDB::can_instantiate(t))) { |
514 | continue; |
515 | } |
516 | |
517 | inheritors_array.push_back(t); |
518 | |
519 | if (!icon.is_valid()) { |
520 | icon = get_editor_theme_icon(has_theme_icon(t, EditorStringName(EditorIcons)) ? t : String("Object" )); |
521 | } |
522 | |
523 | int id = TYPE_BASE_ID + idx; |
524 | edit_menu->add_icon_item(icon, vformat(TTR("New %s" ), t), id); |
525 | |
526 | idx++; |
527 | } |
528 | |
529 | if (edit_menu->get_item_count()) { |
530 | edit_menu->add_separator(); |
531 | } |
532 | } |
533 | } |
534 | |
535 | bool EditorResourcePicker::handle_menu_selected(int p_which) { |
536 | bool success = false; |
537 | GDVIRTUAL_CALL(_handle_menu_selected, p_which, success); |
538 | return success; |
539 | } |
540 | |
541 | void EditorResourcePicker::_button_draw() { |
542 | if (dropping) { |
543 | Color color = get_theme_color(SNAME("accent_color" ), EditorStringName(Editor)); |
544 | assign_button->draw_rect(Rect2(Point2(), assign_button->get_size()), color, false); |
545 | } |
546 | } |
547 | |
548 | void EditorResourcePicker::_button_input(const Ref<InputEvent> &p_event) { |
549 | Ref<InputEventMouseButton> mb = p_event; |
550 | |
551 | if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) { |
552 | // Only attempt to update and show the menu if we have |
553 | // a valid resource or the Picker is editable, as |
554 | // there will otherwise be nothing to display. |
555 | if (edited_resource.is_valid() || is_editable()) { |
556 | _update_menu_items(); |
557 | |
558 | Vector2 pos = get_screen_position() + mb->get_position(); |
559 | edit_menu->reset_size(); |
560 | edit_menu->set_position(pos); |
561 | edit_menu->popup(); |
562 | } |
563 | } |
564 | } |
565 | |
566 | String EditorResourcePicker::_get_resource_type(const Ref<Resource> &p_resource) const { |
567 | if (p_resource.is_null()) { |
568 | return String(); |
569 | } |
570 | String res_type = p_resource->get_class(); |
571 | |
572 | Ref<Script> res_script = p_resource->get_script(); |
573 | if (res_script.is_null()) { |
574 | return res_type; |
575 | } |
576 | |
577 | // TODO: Replace with EditorFileSystem when PR #60606 is merged to use cached resource type. |
578 | String script_type = EditorNode::get_editor_data().script_class_get_name(res_script->get_path()); |
579 | if (!script_type.is_empty()) { |
580 | res_type = script_type; |
581 | } |
582 | return res_type; |
583 | } |
584 | |
585 | static void _add_allowed_type(const StringName &p_type, HashSet<StringName> *p_vector) { |
586 | if (p_vector->has(p_type)) { |
587 | // Already added |
588 | return; |
589 | } |
590 | |
591 | if (ClassDB::class_exists(p_type)) { |
592 | // Engine class, |
593 | |
594 | if (!ClassDB::is_virtual(p_type)) { |
595 | p_vector->insert(p_type); |
596 | } |
597 | |
598 | List<StringName> inheriters; |
599 | ClassDB::get_inheriters_from_class(p_type, &inheriters); |
600 | for (const StringName &S : inheriters) { |
601 | _add_allowed_type(S, p_vector); |
602 | } |
603 | } else { |
604 | // Script class. |
605 | p_vector->insert(p_type); |
606 | } |
607 | |
608 | List<StringName> inheriters; |
609 | ScriptServer::get_inheriters_list(p_type, &inheriters); |
610 | for (const StringName &S : inheriters) { |
611 | _add_allowed_type(S, p_vector); |
612 | } |
613 | } |
614 | |
615 | void EditorResourcePicker::_get_allowed_types(bool p_with_convert, HashSet<StringName> *p_vector) const { |
616 | Vector<String> allowed_types = base_type.split("," ); |
617 | int size = allowed_types.size(); |
618 | |
619 | for (int i = 0; i < size; i++) { |
620 | String base = allowed_types[i].strip_edges(); |
621 | |
622 | _add_allowed_type(base, p_vector); |
623 | |
624 | if (p_with_convert) { |
625 | if (base == "BaseMaterial3D" ) { |
626 | p_vector->insert("Texture2D" ); |
627 | } else if (base == "ShaderMaterial" ) { |
628 | p_vector->insert("Shader" ); |
629 | } else if (base == "Texture2D" ) { |
630 | p_vector->insert("Image" ); |
631 | } |
632 | } |
633 | } |
634 | } |
635 | |
636 | bool EditorResourcePicker::_is_drop_valid(const Dictionary &p_drag_data) const { |
637 | if (base_type.is_empty()) { |
638 | return true; |
639 | } |
640 | |
641 | Dictionary drag_data = p_drag_data; |
642 | |
643 | Ref<Resource> res; |
644 | if (drag_data.has("type" ) && String(drag_data["type" ]) == "script_list_element" ) { |
645 | ScriptEditorBase *se = Object::cast_to<ScriptEditorBase>(drag_data["script_list_element" ]); |
646 | if (se) { |
647 | res = se->get_edited_resource(); |
648 | } |
649 | } else if (drag_data.has("type" ) && String(drag_data["type" ]) == "resource" ) { |
650 | res = drag_data["resource" ]; |
651 | } else if (drag_data.has("type" ) && String(drag_data["type" ]) == "files" ) { |
652 | Vector<String> files = drag_data["files" ]; |
653 | |
654 | // TODO: Extract the typename of the dropped filepath's resource in a more performant way, without fully loading it. |
655 | if (files.size() == 1) { |
656 | String file = files[0]; |
657 | res = ResourceLoader::load(file); |
658 | } |
659 | } |
660 | |
661 | HashSet<StringName> allowed_types; |
662 | _get_allowed_types(true, &allowed_types); |
663 | |
664 | if (res.is_valid()) { |
665 | String res_type = _get_resource_type(res); |
666 | |
667 | if (_is_type_valid(res_type, allowed_types)) { |
668 | return true; |
669 | } |
670 | |
671 | StringName custom_class = EditorNode::get_singleton()->get_object_custom_type_name(res.ptr()); |
672 | if (_is_type_valid(custom_class, allowed_types)) { |
673 | return true; |
674 | } |
675 | } |
676 | |
677 | return false; |
678 | } |
679 | |
680 | bool EditorResourcePicker::_is_type_valid(const String p_type_name, HashSet<StringName> p_allowed_types) const { |
681 | for (const StringName &E : p_allowed_types) { |
682 | String at = E; |
683 | if (p_type_name == at || ClassDB::is_parent_class(p_type_name, at) || EditorNode::get_editor_data().script_class_is_parent(p_type_name, at)) { |
684 | return true; |
685 | } |
686 | } |
687 | |
688 | return false; |
689 | } |
690 | |
691 | Variant EditorResourcePicker::get_drag_data_fw(const Point2 &p_point, Control *p_from) { |
692 | if (edited_resource.is_valid()) { |
693 | return EditorNode::get_singleton()->drag_resource(edited_resource, p_from); |
694 | } |
695 | |
696 | return Variant(); |
697 | } |
698 | |
699 | bool EditorResourcePicker::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const { |
700 | return editable && _is_drop_valid(p_data); |
701 | } |
702 | |
703 | void EditorResourcePicker::drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) { |
704 | ERR_FAIL_COND(!_is_drop_valid(p_data)); |
705 | |
706 | Dictionary drag_data = p_data; |
707 | |
708 | Ref<Resource> dropped_resource; |
709 | if (drag_data.has("type" ) && String(drag_data["type" ]) == "script_list_element" ) { |
710 | ScriptEditorBase *se = Object::cast_to<ScriptEditorBase>(drag_data["script_list_element" ]); |
711 | if (se) { |
712 | dropped_resource = se->get_edited_resource(); |
713 | } |
714 | } else if (drag_data.has("type" ) && String(drag_data["type" ]) == "resource" ) { |
715 | dropped_resource = drag_data["resource" ]; |
716 | } |
717 | |
718 | if (!dropped_resource.is_valid() && drag_data.has("type" ) && String(drag_data["type" ]) == "files" ) { |
719 | Vector<String> files = drag_data["files" ]; |
720 | |
721 | if (files.size() == 1) { |
722 | String file = files[0]; |
723 | dropped_resource = ResourceLoader::load(file); |
724 | } |
725 | } |
726 | |
727 | if (dropped_resource.is_valid()) { |
728 | HashSet<StringName> allowed_types; |
729 | _get_allowed_types(false, &allowed_types); |
730 | |
731 | String res_type = _get_resource_type(dropped_resource); |
732 | |
733 | // If the accepted dropped resource is from the extended list, it requires conversion. |
734 | if (!_is_type_valid(res_type, allowed_types)) { |
735 | for (const StringName &E : allowed_types) { |
736 | String at = E; |
737 | |
738 | if (at == "BaseMaterial3D" && Ref<Texture2D>(dropped_resource).is_valid()) { |
739 | // Use existing resource if possible and only replace its data. |
740 | Ref<StandardMaterial3D> mat = edited_resource; |
741 | if (!mat.is_valid()) { |
742 | mat.instantiate(); |
743 | } |
744 | mat->set_texture(StandardMaterial3D::TextureParam::TEXTURE_ALBEDO, dropped_resource); |
745 | dropped_resource = mat; |
746 | break; |
747 | } |
748 | |
749 | if (at == "ShaderMaterial" && Ref<Shader>(dropped_resource).is_valid()) { |
750 | Ref<ShaderMaterial> mat = edited_resource; |
751 | if (!mat.is_valid()) { |
752 | mat.instantiate(); |
753 | } |
754 | mat->set_shader(dropped_resource); |
755 | dropped_resource = mat; |
756 | break; |
757 | } |
758 | |
759 | if (at == "ImageTexture" && Ref<Image>(dropped_resource).is_valid()) { |
760 | Ref<ImageTexture> texture = edited_resource; |
761 | if (!texture.is_valid()) { |
762 | texture.instantiate(); |
763 | } |
764 | texture->set_image(dropped_resource); |
765 | dropped_resource = texture; |
766 | break; |
767 | } |
768 | } |
769 | } |
770 | |
771 | edited_resource = dropped_resource; |
772 | emit_signal(SNAME("resource_changed" ), edited_resource); |
773 | _update_resource(); |
774 | } |
775 | } |
776 | |
777 | void EditorResourcePicker::_bind_methods() { |
778 | ClassDB::bind_method(D_METHOD("_update_resource_preview" ), &EditorResourcePicker::_update_resource_preview); |
779 | |
780 | ClassDB::bind_method(D_METHOD("set_base_type" , "base_type" ), &EditorResourcePicker::set_base_type); |
781 | ClassDB::bind_method(D_METHOD("get_base_type" ), &EditorResourcePicker::get_base_type); |
782 | ClassDB::bind_method(D_METHOD("get_allowed_types" ), &EditorResourcePicker::get_allowed_types); |
783 | ClassDB::bind_method(D_METHOD("set_edited_resource" , "resource" ), &EditorResourcePicker::set_edited_resource); |
784 | ClassDB::bind_method(D_METHOD("get_edited_resource" ), &EditorResourcePicker::get_edited_resource); |
785 | ClassDB::bind_method(D_METHOD("set_toggle_mode" , "enable" ), &EditorResourcePicker::set_toggle_mode); |
786 | ClassDB::bind_method(D_METHOD("is_toggle_mode" ), &EditorResourcePicker::is_toggle_mode); |
787 | ClassDB::bind_method(D_METHOD("set_toggle_pressed" , "pressed" ), &EditorResourcePicker::set_toggle_pressed); |
788 | ClassDB::bind_method(D_METHOD("set_editable" , "enable" ), &EditorResourcePicker::set_editable); |
789 | ClassDB::bind_method(D_METHOD("is_editable" ), &EditorResourcePicker::is_editable); |
790 | |
791 | GDVIRTUAL_BIND(_set_create_options, "menu_node" ); |
792 | GDVIRTUAL_BIND(_handle_menu_selected, "id" ); |
793 | |
794 | ADD_PROPERTY(PropertyInfo(Variant::STRING, "base_type" ), "set_base_type" , "get_base_type" ); |
795 | ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "edited_resource" , PROPERTY_HINT_RESOURCE_TYPE, "Resource" , PROPERTY_USAGE_NONE), "set_edited_resource" , "get_edited_resource" ); |
796 | ADD_PROPERTY(PropertyInfo(Variant::BOOL, "editable" ), "set_editable" , "is_editable" ); |
797 | ADD_PROPERTY(PropertyInfo(Variant::BOOL, "toggle_mode" ), "set_toggle_mode" , "is_toggle_mode" ); |
798 | |
799 | ADD_SIGNAL(MethodInfo("resource_selected" , PropertyInfo(Variant::OBJECT, "resource" , PROPERTY_HINT_RESOURCE_TYPE, "Resource" ), PropertyInfo(Variant::BOOL, "inspect" ))); |
800 | ADD_SIGNAL(MethodInfo("resource_changed" , PropertyInfo(Variant::OBJECT, "resource" , PROPERTY_HINT_RESOURCE_TYPE, "Resource" ))); |
801 | } |
802 | |
803 | void EditorResourcePicker::_notification(int p_what) { |
804 | switch (p_what) { |
805 | case NOTIFICATION_ENTER_TREE: { |
806 | _update_resource(); |
807 | [[fallthrough]]; |
808 | } |
809 | case NOTIFICATION_THEME_CHANGED: { |
810 | assign_button->add_theme_constant_override("icon_max_width" , get_theme_constant(SNAME("class_icon_size" ), EditorStringName(Editor))); |
811 | edit_button->set_icon(get_theme_icon(SNAME("select_arrow" ), SNAME("Tree" ))); |
812 | } break; |
813 | |
814 | case NOTIFICATION_DRAW: { |
815 | draw_style_box(get_theme_stylebox(SNAME("panel" ), SNAME("Tree" )), Rect2(Point2(), get_size())); |
816 | } break; |
817 | |
818 | case NOTIFICATION_DRAG_BEGIN: { |
819 | if (editable && _is_drop_valid(get_viewport()->gui_get_drag_data())) { |
820 | dropping = true; |
821 | assign_button->queue_redraw(); |
822 | } |
823 | } break; |
824 | |
825 | case NOTIFICATION_DRAG_END: { |
826 | if (dropping) { |
827 | dropping = false; |
828 | assign_button->queue_redraw(); |
829 | } |
830 | } break; |
831 | } |
832 | } |
833 | |
834 | void EditorResourcePicker::set_assign_button_min_size(const Size2i &p_size) { |
835 | assign_button_min_size = p_size; |
836 | assign_button->set_custom_minimum_size(assign_button_min_size); |
837 | } |
838 | |
839 | void EditorResourcePicker::set_base_type(const String &p_base_type) { |
840 | base_type = p_base_type; |
841 | |
842 | // There is a possibility that the new base type is conflicting with the existing value. |
843 | // Keep the value, but warn the user that there is a potential mistake. |
844 | if (!base_type.is_empty() && edited_resource.is_valid()) { |
845 | HashSet<StringName> allowed_types; |
846 | _get_allowed_types(true, &allowed_types); |
847 | |
848 | StringName custom_class; |
849 | bool is_custom = false; |
850 | if (edited_resource->get_script()) { |
851 | custom_class = EditorNode::get_singleton()->get_object_custom_type_name(edited_resource->get_script()); |
852 | is_custom = _is_type_valid(custom_class, allowed_types); |
853 | } |
854 | |
855 | if (!is_custom && !_is_type_valid(edited_resource->get_class(), allowed_types)) { |
856 | String class_str = (custom_class == StringName() ? edited_resource->get_class() : vformat("%s (%s)" , custom_class, edited_resource->get_class())); |
857 | WARN_PRINT(vformat("Value mismatch between the new base type of this EditorResourcePicker, '%s', and the type of the value it already has, '%s'." , base_type, class_str)); |
858 | } |
859 | } |
860 | } |
861 | |
862 | String EditorResourcePicker::get_base_type() const { |
863 | return base_type; |
864 | } |
865 | |
866 | Vector<String> EditorResourcePicker::get_allowed_types() const { |
867 | HashSet<StringName> allowed_types; |
868 | _get_allowed_types(false, &allowed_types); |
869 | |
870 | Vector<String> types; |
871 | types.resize(allowed_types.size()); |
872 | |
873 | int i = 0; |
874 | String *w = types.ptrw(); |
875 | for (const StringName &E : allowed_types) { |
876 | w[i] = E; |
877 | i++; |
878 | } |
879 | |
880 | return types; |
881 | } |
882 | |
883 | void EditorResourcePicker::set_edited_resource(Ref<Resource> p_resource) { |
884 | if (!p_resource.is_valid()) { |
885 | edited_resource = Ref<Resource>(); |
886 | _update_resource(); |
887 | return; |
888 | } |
889 | |
890 | if (!base_type.is_empty()) { |
891 | HashSet<StringName> allowed_types; |
892 | _get_allowed_types(true, &allowed_types); |
893 | |
894 | StringName custom_class; |
895 | bool is_custom = false; |
896 | if (p_resource->get_script()) { |
897 | custom_class = EditorNode::get_singleton()->get_object_custom_type_name(p_resource->get_script()); |
898 | is_custom = _is_type_valid(custom_class, allowed_types); |
899 | } |
900 | |
901 | if (!is_custom && !_is_type_valid(p_resource->get_class(), allowed_types)) { |
902 | String class_str = (custom_class == StringName() ? p_resource->get_class() : vformat("%s (%s)" , custom_class, p_resource->get_class())); |
903 | ERR_FAIL_MSG(vformat("Failed to set a resource of the type '%s' because this EditorResourcePicker only accepts '%s' and its derivatives." , class_str, base_type)); |
904 | } |
905 | } |
906 | |
907 | edited_resource = p_resource; |
908 | _update_resource(); |
909 | } |
910 | |
911 | Ref<Resource> EditorResourcePicker::get_edited_resource() { |
912 | return edited_resource; |
913 | } |
914 | |
915 | void EditorResourcePicker::set_toggle_mode(bool p_enable) { |
916 | assign_button->set_toggle_mode(p_enable); |
917 | } |
918 | |
919 | bool EditorResourcePicker::is_toggle_mode() const { |
920 | return assign_button->is_toggle_mode(); |
921 | } |
922 | |
923 | void EditorResourcePicker::set_toggle_pressed(bool p_pressed) { |
924 | if (!is_toggle_mode()) { |
925 | return; |
926 | } |
927 | |
928 | assign_button->set_pressed(p_pressed); |
929 | } |
930 | |
931 | void EditorResourcePicker::set_editable(bool p_editable) { |
932 | editable = p_editable; |
933 | assign_button->set_disabled(!editable && !edited_resource.is_valid()); |
934 | edit_button->set_visible(editable); |
935 | } |
936 | |
937 | bool EditorResourcePicker::is_editable() const { |
938 | return editable; |
939 | } |
940 | |
941 | void EditorResourcePicker::() { |
942 | if (edit_menu) { |
943 | return; |
944 | } |
945 | edit_menu = memnew(PopupMenu); |
946 | add_child(edit_menu); |
947 | edit_menu->connect("id_pressed" , callable_mp(this, &EditorResourcePicker::_edit_menu_cbk)); |
948 | edit_menu->connect("popup_hide" , callable_mp((BaseButton *)edit_button, &BaseButton::set_pressed).bind(false)); |
949 | } |
950 | |
951 | void EditorResourcePicker::_gather_resources_to_duplicate(const Ref<Resource> p_resource, TreeItem *p_item, const String &p_property_name) const { |
952 | p_item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK); |
953 | |
954 | String res_name = p_resource->get_name(); |
955 | if (res_name.is_empty() && !p_resource->is_built_in()) { |
956 | res_name = p_resource->get_path().get_file(); |
957 | } |
958 | |
959 | if (res_name.is_empty()) { |
960 | p_item->set_text(0, p_resource->get_class()); |
961 | } else { |
962 | p_item->set_text(0, vformat("%s (%s)" , p_resource->get_class(), res_name)); |
963 | } |
964 | |
965 | p_item->set_icon(0, EditorNode::get_singleton()->get_object_icon(p_resource.ptr())); |
966 | p_item->set_editable(0, true); |
967 | |
968 | Array meta; |
969 | meta.append(p_resource); |
970 | p_item->set_metadata(0, meta); |
971 | |
972 | if (!p_property_name.is_empty()) { |
973 | p_item->set_text(1, p_property_name); |
974 | } |
975 | |
976 | static Vector<String> unique_exceptions = { "Image" , "Shader" , "Mesh" , "FontFile" }; |
977 | if (!unique_exceptions.has(p_resource->get_class())) { |
978 | // Automatically select resource, unless it's something that shouldn't be duplicated. |
979 | p_item->set_checked(0, true); |
980 | } |
981 | |
982 | List<PropertyInfo> plist; |
983 | p_resource->get_property_list(&plist); |
984 | |
985 | for (const PropertyInfo &E : plist) { |
986 | if (!(E.usage & PROPERTY_USAGE_STORAGE) || E.type != Variant::OBJECT || E.hint != PROPERTY_HINT_RESOURCE_TYPE) { |
987 | continue; |
988 | } |
989 | |
990 | Ref<Resource> res = p_resource->get(E.name); |
991 | if (res.is_null()) { |
992 | continue; |
993 | } |
994 | |
995 | TreeItem *child = p_item->create_child(); |
996 | _gather_resources_to_duplicate(res, child, E.name); |
997 | |
998 | meta = child->get_metadata(0); |
999 | // Remember property name. |
1000 | meta.append(E.name); |
1001 | |
1002 | if ((E.usage & PROPERTY_USAGE_NEVER_DUPLICATE)) { |
1003 | // The resource can't be duplicated, but make it appear on the list anyway. |
1004 | child->set_checked(0, false); |
1005 | child->set_editable(0, false); |
1006 | } |
1007 | } |
1008 | } |
1009 | |
1010 | void EditorResourcePicker::_duplicate_selected_resources() { |
1011 | for (TreeItem *item = duplicate_resources_tree->get_root(); item; item = item->get_next_in_tree()) { |
1012 | if (!item->is_checked(0)) { |
1013 | continue; |
1014 | } |
1015 | |
1016 | Array meta = item->get_metadata(0); |
1017 | Ref<Resource> res = meta[0]; |
1018 | Ref<Resource> unique_resource = res->duplicate(); |
1019 | ERR_FAIL_COND(unique_resource.is_null()); // duplicate() may fail. |
1020 | meta[0] = unique_resource; |
1021 | |
1022 | if (meta.size() == 1) { // Root. |
1023 | edited_resource = unique_resource; |
1024 | emit_signal(SNAME("resource_changed" ), edited_resource); |
1025 | _update_resource(); |
1026 | } else { |
1027 | Array parent_meta = item->get_parent()->get_metadata(0); |
1028 | Ref<Resource> parent = parent_meta[0]; |
1029 | parent->set(meta[1], unique_resource); |
1030 | } |
1031 | } |
1032 | } |
1033 | |
1034 | EditorResourcePicker::EditorResourcePicker(bool p_hide_assign_button_controls) { |
1035 | assign_button = memnew(Button); |
1036 | assign_button->set_flat(true); |
1037 | assign_button->set_h_size_flags(SIZE_EXPAND_FILL); |
1038 | assign_button->set_expand_icon(true); |
1039 | assign_button->set_clip_text(true); |
1040 | assign_button->set_auto_translate(false); |
1041 | SET_DRAG_FORWARDING_GCD(assign_button, EditorResourcePicker); |
1042 | add_child(assign_button); |
1043 | assign_button->connect("pressed" , callable_mp(this, &EditorResourcePicker::_resource_selected)); |
1044 | assign_button->connect("draw" , callable_mp(this, &EditorResourcePicker::_button_draw)); |
1045 | assign_button->connect("gui_input" , callable_mp(this, &EditorResourcePicker::_button_input)); |
1046 | |
1047 | if (!p_hide_assign_button_controls) { |
1048 | preview_rect = memnew(TextureRect); |
1049 | preview_rect->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE); |
1050 | preview_rect->set_anchors_and_offsets_preset(PRESET_FULL_RECT); |
1051 | preview_rect->set_offset(SIDE_TOP, 1); |
1052 | preview_rect->set_offset(SIDE_BOTTOM, -1); |
1053 | preview_rect->set_offset(SIDE_RIGHT, -1); |
1054 | preview_rect->set_texture_filter(TEXTURE_FILTER_NEAREST_WITH_MIPMAPS); |
1055 | assign_button->add_child(preview_rect); |
1056 | } |
1057 | |
1058 | edit_button = memnew(Button); |
1059 | edit_button->set_flat(true); |
1060 | edit_button->set_toggle_mode(true); |
1061 | edit_button->connect("pressed" , callable_mp(this, &EditorResourcePicker::_update_menu)); |
1062 | add_child(edit_button); |
1063 | edit_button->connect("gui_input" , callable_mp(this, &EditorResourcePicker::_button_input)); |
1064 | |
1065 | add_theme_constant_override("separation" , 0); |
1066 | } |
1067 | |
1068 | // EditorScriptPicker |
1069 | |
1070 | void EditorScriptPicker::set_create_options(Object *) { |
1071 | PopupMenu * = Object::cast_to<PopupMenu>(p_menu_node); |
1072 | if (!menu_node) { |
1073 | return; |
1074 | } |
1075 | |
1076 | menu_node->add_icon_item(get_editor_theme_icon(SNAME("ScriptCreate" )), TTR("New Script" ), OBJ_MENU_NEW_SCRIPT); |
1077 | if (script_owner) { |
1078 | Ref<Script> scr = script_owner->get_script(); |
1079 | if (scr.is_valid()) { |
1080 | menu_node->add_icon_item(get_editor_theme_icon(SNAME("ScriptExtend" )), TTR("Extend Script" ), OBJ_MENU_EXTEND_SCRIPT); |
1081 | } |
1082 | } |
1083 | menu_node->add_separator(); |
1084 | } |
1085 | |
1086 | bool EditorScriptPicker::handle_menu_selected(int p_which) { |
1087 | switch (p_which) { |
1088 | case OBJ_MENU_NEW_SCRIPT: { |
1089 | if (script_owner) { |
1090 | SceneTreeDock::get_singleton()->open_script_dialog(script_owner, false); |
1091 | } |
1092 | return true; |
1093 | } |
1094 | |
1095 | case OBJ_MENU_EXTEND_SCRIPT: { |
1096 | if (script_owner) { |
1097 | SceneTreeDock::get_singleton()->open_script_dialog(script_owner, true); |
1098 | } |
1099 | return true; |
1100 | } |
1101 | } |
1102 | |
1103 | return false; |
1104 | } |
1105 | |
1106 | void EditorScriptPicker::set_script_owner(Node *p_owner) { |
1107 | script_owner = p_owner; |
1108 | } |
1109 | |
1110 | Node *EditorScriptPicker::get_script_owner() const { |
1111 | return script_owner; |
1112 | } |
1113 | |
1114 | void EditorScriptPicker::_bind_methods() { |
1115 | ClassDB::bind_method(D_METHOD("set_script_owner" , "owner_node" ), &EditorScriptPicker::set_script_owner); |
1116 | ClassDB::bind_method(D_METHOD("get_script_owner" ), &EditorScriptPicker::get_script_owner); |
1117 | |
1118 | ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "script_owner" , PROPERTY_HINT_RESOURCE_TYPE, "Node" , PROPERTY_USAGE_NONE), "set_script_owner" , "get_script_owner" ); |
1119 | } |
1120 | |
1121 | EditorScriptPicker::EditorScriptPicker() { |
1122 | } |
1123 | |
1124 | // EditorShaderPicker |
1125 | |
1126 | void EditorShaderPicker::set_create_options(Object *) { |
1127 | PopupMenu * = Object::cast_to<PopupMenu>(p_menu_node); |
1128 | if (!menu_node) { |
1129 | return; |
1130 | } |
1131 | |
1132 | menu_node->add_icon_item(get_editor_theme_icon(SNAME("Shader" )), TTR("New Shader" ), OBJ_MENU_NEW_SHADER); |
1133 | menu_node->add_separator(); |
1134 | } |
1135 | |
1136 | bool EditorShaderPicker::handle_menu_selected(int p_which) { |
1137 | Ref<ShaderMaterial> ed_material = Ref<ShaderMaterial>(get_edited_material()); |
1138 | |
1139 | switch (p_which) { |
1140 | case OBJ_MENU_NEW_SHADER: { |
1141 | if (ed_material.is_valid()) { |
1142 | SceneTreeDock::get_singleton()->open_shader_dialog(ed_material, preferred_mode); |
1143 | return true; |
1144 | } |
1145 | } break; |
1146 | default: |
1147 | break; |
1148 | } |
1149 | return false; |
1150 | } |
1151 | |
1152 | void EditorShaderPicker::set_edited_material(ShaderMaterial *p_material) { |
1153 | edited_material = p_material; |
1154 | } |
1155 | |
1156 | ShaderMaterial *EditorShaderPicker::get_edited_material() const { |
1157 | return edited_material; |
1158 | } |
1159 | |
1160 | void EditorShaderPicker::set_preferred_mode(int p_mode) { |
1161 | preferred_mode = p_mode; |
1162 | } |
1163 | |
1164 | EditorShaderPicker::EditorShaderPicker() { |
1165 | } |
1166 | |
1167 | ////////////// |
1168 | |
1169 | void EditorAudioStreamPicker::_notification(int p_what) { |
1170 | switch (p_what) { |
1171 | case NOTIFICATION_READY: |
1172 | case NOTIFICATION_THEME_CHANGED: { |
1173 | _update_resource(); |
1174 | } break; |
1175 | case NOTIFICATION_INTERNAL_PROCESS: { |
1176 | Ref<AudioStream> audio_stream = get_edited_resource(); |
1177 | if (audio_stream.is_valid()) { |
1178 | if (audio_stream->get_length() > 0) { |
1179 | Ref<AudioStreamPreview> preview = AudioStreamPreviewGenerator::get_singleton()->generate_preview(audio_stream); |
1180 | if (preview.is_valid()) { |
1181 | if (preview->get_version() != last_preview_version) { |
1182 | stream_preview_rect->queue_redraw(); |
1183 | last_preview_version = preview->get_version(); |
1184 | } |
1185 | } |
1186 | } |
1187 | |
1188 | uint64_t tagged_frame = audio_stream->get_tagged_frame(); |
1189 | uint64_t diff_frames = AudioServer::get_singleton()->get_mixed_frames() - tagged_frame; |
1190 | uint64_t diff_msec = diff_frames * 1000 / AudioServer::get_singleton()->get_mix_rate(); |
1191 | |
1192 | if (diff_msec < 300) { |
1193 | uint32_t count = audio_stream->get_tagged_frame_count(); |
1194 | |
1195 | bool differ = false; |
1196 | |
1197 | if (count != tagged_frame_offset_count) { |
1198 | differ = true; |
1199 | } |
1200 | float offsets[MAX_TAGGED_FRAMES]; |
1201 | |
1202 | for (uint32_t i = 0; i < MIN(count, uint32_t(MAX_TAGGED_FRAMES)); i++) { |
1203 | offsets[i] = audio_stream->get_tagged_frame_offset(i); |
1204 | if (offsets[i] != tagged_frame_offsets[i]) { |
1205 | differ = true; |
1206 | } |
1207 | } |
1208 | |
1209 | if (differ) { |
1210 | tagged_frame_offset_count = count; |
1211 | for (uint32_t i = 0; i < count; i++) { |
1212 | tagged_frame_offsets[i] = offsets[i]; |
1213 | } |
1214 | } |
1215 | |
1216 | stream_preview_rect->queue_redraw(); |
1217 | } else { |
1218 | if (tagged_frame_offset_count != 0) { |
1219 | stream_preview_rect->queue_redraw(); |
1220 | } |
1221 | tagged_frame_offset_count = 0; |
1222 | } |
1223 | } |
1224 | } break; |
1225 | } |
1226 | } |
1227 | |
1228 | void EditorAudioStreamPicker::_update_resource() { |
1229 | EditorResourcePicker::_update_resource(); |
1230 | |
1231 | Ref<Font> font = get_theme_font(SNAME("font" ), SNAME("Label" )); |
1232 | int font_size = get_theme_font_size(SNAME("font_size" ), SNAME("Label" )); |
1233 | Ref<AudioStream> audio_stream = get_edited_resource(); |
1234 | if (audio_stream.is_valid() && audio_stream->get_length() > 0.0) { |
1235 | set_assign_button_min_size(Size2(1, font->get_height(font_size) * 3)); |
1236 | } else { |
1237 | set_assign_button_min_size(Size2(1, font->get_height(font_size) * 1.5)); |
1238 | } |
1239 | |
1240 | stream_preview_rect->queue_redraw(); |
1241 | } |
1242 | |
1243 | void EditorAudioStreamPicker::_preview_draw() { |
1244 | Ref<AudioStream> audio_stream = get_edited_resource(); |
1245 | if (!audio_stream.is_valid()) { |
1246 | get_assign_button()->set_text(TTR("<empty>" )); |
1247 | return; |
1248 | } |
1249 | |
1250 | int font_size = get_theme_font_size(SNAME("font_size" ), SNAME("Label" )); |
1251 | |
1252 | get_assign_button()->set_text("" ); |
1253 | |
1254 | Size2i size = stream_preview_rect->get_size(); |
1255 | Ref<Font> font = get_theme_font(SNAME("font" ), SNAME("Label" )); |
1256 | |
1257 | Rect2 rect(Point2(), size); |
1258 | |
1259 | if (audio_stream->get_length() > 0 && size.width > 0) { |
1260 | rect.size.height *= 0.5; |
1261 | |
1262 | stream_preview_rect->draw_rect(rect, Color(0, 0, 0, 1)); |
1263 | |
1264 | Ref<AudioStreamPreview> preview = AudioStreamPreviewGenerator::get_singleton()->generate_preview(audio_stream); |
1265 | float preview_len = preview->get_length(); |
1266 | |
1267 | Vector<Vector2> points; |
1268 | points.resize(size.width * 2); |
1269 | |
1270 | for (int i = 0; i < size.width; i++) { |
1271 | float ofs = i * preview_len / size.width; |
1272 | float ofs_n = (i + 1) * preview_len / size.width; |
1273 | float max = preview->get_max(ofs, ofs_n) * 0.5 + 0.5; |
1274 | float min = preview->get_min(ofs, ofs_n) * 0.5 + 0.5; |
1275 | |
1276 | int idx = i; |
1277 | points.write[idx * 2 + 0] = Vector2(i + 1, rect.position.y + min * rect.size.y); |
1278 | points.write[idx * 2 + 1] = Vector2(i + 1, rect.position.y + max * rect.size.y); |
1279 | } |
1280 | |
1281 | Vector<Color> colors = { get_theme_color(SNAME("contrast_color_2" ), EditorStringName(Editor)) }; |
1282 | |
1283 | RS::get_singleton()->canvas_item_add_multiline(stream_preview_rect->get_canvas_item(), points, colors); |
1284 | |
1285 | if (tagged_frame_offset_count) { |
1286 | Color accent = get_theme_color(SNAME("accent_color" ), EditorStringName(Editor)); |
1287 | |
1288 | for (uint32_t i = 0; i < tagged_frame_offset_count; i++) { |
1289 | int x = CLAMP(tagged_frame_offsets[i] * size.width / preview_len, 0, size.width); |
1290 | if (x == 0) { |
1291 | continue; // Because some may always return 0, ignore offset 0. |
1292 | } |
1293 | stream_preview_rect->draw_rect(Rect2i(x, 0, 2, rect.size.height), accent); |
1294 | } |
1295 | } |
1296 | rect.position.y += rect.size.height; |
1297 | } |
1298 | |
1299 | Ref<Texture2D> icon; |
1300 | Color icon_modulate(1, 1, 1, 1); |
1301 | |
1302 | if (tagged_frame_offset_count > 0) { |
1303 | icon = get_editor_theme_icon(SNAME("Play" )); |
1304 | if ((OS::get_singleton()->get_ticks_msec() % 500) > 250) { |
1305 | icon_modulate = Color(1, 0.5, 0.5, 1); // get_theme_color(SNAME("accent_color"), EditorStringName(Editor)); |
1306 | } |
1307 | } else { |
1308 | icon = EditorNode::get_singleton()->get_object_icon(audio_stream.operator->(), "Object" ); |
1309 | } |
1310 | String text; |
1311 | if (!audio_stream->get_name().is_empty()) { |
1312 | text = audio_stream->get_name(); |
1313 | } else if (audio_stream->get_path().is_resource_file()) { |
1314 | text = audio_stream->get_path().get_file(); |
1315 | } else { |
1316 | text = audio_stream->get_class().replace_first("AudioStream" , "" ); |
1317 | } |
1318 | |
1319 | stream_preview_rect->draw_texture(icon, Point2i(EDSCALE * 2, rect.position.y + (rect.size.height - icon->get_height()) / 2), icon_modulate); |
1320 | stream_preview_rect->draw_string(font, Point2i(EDSCALE * 2 + icon->get_width(), rect.position.y + font->get_ascent(font_size) + (rect.size.height - font->get_height(font_size)) / 2), text, HORIZONTAL_ALIGNMENT_CENTER, size.width - 4 * EDSCALE - icon->get_width()); |
1321 | } |
1322 | |
1323 | EditorAudioStreamPicker::EditorAudioStreamPicker() : |
1324 | EditorResourcePicker(true) { |
1325 | stream_preview_rect = memnew(Control); |
1326 | |
1327 | stream_preview_rect->set_anchors_and_offsets_preset(PRESET_FULL_RECT); |
1328 | stream_preview_rect->set_offset(SIDE_TOP, 1); |
1329 | stream_preview_rect->set_offset(SIDE_BOTTOM, -1); |
1330 | stream_preview_rect->set_offset(SIDE_RIGHT, -1); |
1331 | stream_preview_rect->set_mouse_filter(MOUSE_FILTER_IGNORE); |
1332 | stream_preview_rect->connect("draw" , callable_mp(this, &EditorAudioStreamPicker::_preview_draw)); |
1333 | |
1334 | get_assign_button()->add_child(stream_preview_rect); |
1335 | get_assign_button()->move_child(stream_preview_rect, 0); |
1336 | set_process_internal(true); |
1337 | } |
1338 | |