1/**************************************************************************/
2/* theme_editor_preview.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 "theme_editor_preview.h"
32
33#include "core/config/project_settings.h"
34#include "core/input/input.h"
35#include "core/math/math_funcs.h"
36#include "editor/editor_node.h"
37#include "editor/editor_scale.h"
38#include "editor/editor_string_names.h"
39#include "scene/gui/button.h"
40#include "scene/gui/check_box.h"
41#include "scene/gui/check_button.h"
42#include "scene/gui/color_picker.h"
43#include "scene/gui/color_rect.h"
44#include "scene/gui/margin_container.h"
45#include "scene/gui/progress_bar.h"
46#include "scene/gui/scroll_container.h"
47#include "scene/gui/tab_container.h"
48#include "scene/gui/text_edit.h"
49#include "scene/gui/tree.h"
50#include "scene/resources/packed_scene.h"
51#include "scene/theme/theme_db.h"
52
53constexpr double REFRESH_TIMER = 1.5;
54
55void ThemeEditorPreview::set_preview_theme(const Ref<Theme> &p_theme) {
56 preview_content->set_theme(p_theme);
57}
58
59void ThemeEditorPreview::add_preview_overlay(Control *p_overlay) {
60 preview_overlay->add_child(p_overlay);
61 p_overlay->hide();
62}
63
64void ThemeEditorPreview::_propagate_redraw(Control *p_at) {
65 p_at->notification(NOTIFICATION_THEME_CHANGED);
66 p_at->update_minimum_size();
67 p_at->queue_redraw();
68 for (int i = 0; i < p_at->get_child_count(); i++) {
69 Control *a = Object::cast_to<Control>(p_at->get_child(i));
70 if (a) {
71 _propagate_redraw(a);
72 }
73 }
74}
75
76void ThemeEditorPreview::_refresh_interval() {
77 // In case the project settings have changed.
78 preview_bg->set_color(GLOBAL_GET("rendering/environment/defaults/default_clear_color"));
79
80 _propagate_redraw(preview_bg);
81 _propagate_redraw(preview_content);
82}
83
84void ThemeEditorPreview::_preview_visibility_changed() {
85 set_process(is_visible_in_tree());
86}
87
88void ThemeEditorPreview::_picker_button_cbk() {
89 picker_overlay->set_visible(picker_button->is_pressed());
90 if (picker_button->is_pressed()) {
91 _reset_picker_overlay();
92 }
93}
94
95Control *ThemeEditorPreview::_find_hovered_control(Control *p_parent, Vector2 p_mouse_position) {
96 Control *found = nullptr;
97
98 for (int i = p_parent->get_child_count() - 1; i >= 0; i--) {
99 Control *cc = Object::cast_to<Control>(p_parent->get_child(i));
100 if (!cc || !cc->is_visible()) {
101 continue;
102 }
103
104 Rect2 crect = cc->get_rect();
105 if (crect.has_point(p_mouse_position)) {
106 // Check if there is a child control under mouse.
107 if (cc->get_child_count() > 0) {
108 found = _find_hovered_control(cc, p_mouse_position - cc->get_position());
109 }
110
111 // If there are no applicable children, use the control itself.
112 if (!found) {
113 found = cc;
114 }
115 break;
116 }
117 }
118
119 return found;
120}
121
122void ThemeEditorPreview::_draw_picker_overlay() {
123 if (!picker_button->is_pressed()) {
124 return;
125 }
126
127 picker_overlay->draw_rect(Rect2(Vector2(0.0, 0.0), picker_overlay->get_size()), theme_cache.preview_picker_overlay_color);
128 if (hovered_control) {
129 Rect2 highlight_rect = hovered_control->get_global_rect();
130 highlight_rect.position = picker_overlay->get_global_transform().affine_inverse().xform(highlight_rect.position);
131 picker_overlay->draw_style_box(theme_cache.preview_picker_overlay, highlight_rect);
132
133 String highlight_name = hovered_control->get_theme_type_variation();
134 if (highlight_name == StringName()) {
135 highlight_name = hovered_control->get_class_name();
136 }
137
138 Rect2 highlight_label_rect = highlight_rect;
139 highlight_label_rect.size = theme_cache.preview_picker_font->get_string_size(highlight_name, HORIZONTAL_ALIGNMENT_LEFT, -1, theme_cache.font_size);
140
141 int margin_top = theme_cache.preview_picker_label->get_margin(SIDE_TOP);
142 int margin_left = theme_cache.preview_picker_label->get_margin(SIDE_LEFT);
143 int margin_bottom = theme_cache.preview_picker_label->get_margin(SIDE_BOTTOM);
144 int margin_right = theme_cache.preview_picker_label->get_margin(SIDE_RIGHT);
145 highlight_label_rect.size.x += margin_left + margin_right;
146 highlight_label_rect.size.y += margin_top + margin_bottom;
147
148 highlight_label_rect.position = highlight_label_rect.position.clamp(Vector2(), picker_overlay->get_size());
149
150 picker_overlay->draw_style_box(theme_cache.preview_picker_label, highlight_label_rect);
151
152 Point2 label_pos = highlight_label_rect.position;
153 label_pos.y += highlight_label_rect.size.y - margin_bottom;
154 label_pos.x += margin_left;
155 picker_overlay->draw_string(theme_cache.preview_picker_font, label_pos, highlight_name, HORIZONTAL_ALIGNMENT_LEFT, -1, theme_cache.font_size);
156 }
157}
158
159void ThemeEditorPreview::_gui_input_picker_overlay(const Ref<InputEvent> &p_event) {
160 if (!picker_button->is_pressed()) {
161 return;
162 }
163
164 Ref<InputEventMouseButton> mb = p_event;
165
166 if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
167 if (hovered_control) {
168 StringName theme_type = hovered_control->get_theme_type_variation();
169 if (theme_type == StringName()) {
170 theme_type = hovered_control->get_class_name();
171 }
172
173 emit_signal(SNAME("control_picked"), theme_type);
174 picker_button->set_pressed(false);
175 picker_overlay->set_visible(false);
176 return;
177 }
178 }
179
180 Ref<InputEventMouseMotion> mm = p_event;
181
182 if (mm.is_valid()) {
183 Vector2 mp = preview_content->get_local_mouse_position();
184 hovered_control = _find_hovered_control(preview_content, mp);
185 picker_overlay->queue_redraw();
186 }
187
188 // Forward input to the scroll container underneath to allow scrolling.
189 preview_container->gui_input(p_event);
190}
191
192void ThemeEditorPreview::_reset_picker_overlay() {
193 hovered_control = nullptr;
194 picker_overlay->queue_redraw();
195}
196
197void ThemeEditorPreview::_notification(int p_what) {
198 switch (p_what) {
199 case NOTIFICATION_ENTER_TREE: {
200 if (is_visible_in_tree()) {
201 set_process(true);
202 }
203
204 connect("visibility_changed", callable_mp(this, &ThemeEditorPreview::_preview_visibility_changed));
205 } break;
206
207 case NOTIFICATION_READY: {
208 List<Ref<Theme>> preview_themes;
209 preview_themes.push_back(ThemeDB::get_singleton()->get_default_theme());
210 ThemeDB::get_singleton()->create_theme_context(preview_root, preview_themes);
211 } break;
212
213 case NOTIFICATION_THEME_CHANGED: {
214 picker_button->set_icon(get_editor_theme_icon(SNAME("ColorPick")));
215
216 theme_cache.preview_picker_overlay = get_theme_stylebox(SNAME("preview_picker_overlay"), SNAME("ThemeEditor"));
217 theme_cache.preview_picker_overlay_color = get_theme_color(SNAME("preview_picker_overlay_color"), SNAME("ThemeEditor"));
218 theme_cache.preview_picker_label = get_theme_stylebox(SNAME("preview_picker_label"), SNAME("ThemeEditor"));
219 theme_cache.preview_picker_font = get_theme_font(SNAME("status_source"), EditorStringName(EditorFonts));
220 theme_cache.font_size = get_theme_default_font_size();
221 } break;
222
223 case NOTIFICATION_PROCESS: {
224 time_left -= get_process_delta_time();
225 if (time_left < 0) {
226 time_left = REFRESH_TIMER;
227 _refresh_interval();
228 }
229 } break;
230 }
231}
232
233void ThemeEditorPreview::_bind_methods() {
234 ADD_SIGNAL(MethodInfo("control_picked", PropertyInfo(Variant::STRING, "class_name")));
235}
236
237ThemeEditorPreview::ThemeEditorPreview() {
238 preview_toolbar = memnew(HBoxContainer);
239 add_child(preview_toolbar);
240
241 picker_button = memnew(Button);
242 preview_toolbar->add_child(picker_button);
243 picker_button->set_flat(true);
244 picker_button->set_toggle_mode(true);
245 picker_button->set_tooltip_text(TTR("Toggle the control picker, allowing to visually select control types for edit."));
246 picker_button->connect("pressed", callable_mp(this, &ThemeEditorPreview::_picker_button_cbk));
247
248 MarginContainer *preview_body = memnew(MarginContainer);
249 preview_body->set_custom_minimum_size(Size2(480, 0) * EDSCALE);
250 preview_body->set_v_size_flags(SIZE_EXPAND_FILL);
251 add_child(preview_body);
252
253 preview_container = memnew(ScrollContainer);
254 preview_body->add_child(preview_container);
255
256 preview_root = memnew(MarginContainer);
257 preview_container->add_child(preview_root);
258 preview_root->set_clip_contents(true);
259 preview_root->set_custom_minimum_size(Size2(450, 0) * EDSCALE);
260 preview_root->set_v_size_flags(SIZE_EXPAND_FILL);
261 preview_root->set_h_size_flags(SIZE_EXPAND_FILL);
262
263 preview_bg = memnew(ColorRect);
264 preview_bg->set_anchors_and_offsets_preset(PRESET_FULL_RECT);
265 preview_bg->set_color(GLOBAL_GET("rendering/environment/defaults/default_clear_color"));
266 preview_root->add_child(preview_bg);
267
268 preview_content = memnew(MarginContainer);
269 preview_root->add_child(preview_content);
270 preview_content->add_theme_constant_override("margin_right", 4 * EDSCALE);
271 preview_content->add_theme_constant_override("margin_top", 4 * EDSCALE);
272 preview_content->add_theme_constant_override("margin_left", 4 * EDSCALE);
273 preview_content->add_theme_constant_override("margin_bottom", 4 * EDSCALE);
274
275 preview_overlay = memnew(MarginContainer);
276 preview_overlay->set_mouse_filter(MOUSE_FILTER_IGNORE);
277 preview_overlay->set_clip_contents(true);
278 preview_body->add_child(preview_overlay);
279
280 picker_overlay = memnew(Control);
281 add_preview_overlay(picker_overlay);
282 picker_overlay->connect("draw", callable_mp(this, &ThemeEditorPreview::_draw_picker_overlay));
283 picker_overlay->connect("gui_input", callable_mp(this, &ThemeEditorPreview::_gui_input_picker_overlay));
284 picker_overlay->connect("mouse_exited", callable_mp(this, &ThemeEditorPreview::_reset_picker_overlay));
285}
286
287void DefaultThemeEditorPreview::_notification(int p_what) {
288 switch (p_what) {
289 case NOTIFICATION_ENTER_TREE:
290 case NOTIFICATION_THEME_CHANGED: {
291 test_color_picker_button->set_custom_minimum_size(Size2(0, get_theme_constant(SNAME("color_picker_button_height"), EditorStringName(Editor))));
292 } break;
293 }
294}
295
296DefaultThemeEditorPreview::DefaultThemeEditorPreview() {
297 Panel *main_panel = memnew(Panel);
298 preview_content->add_child(main_panel);
299
300 MarginContainer *main_mc = memnew(MarginContainer);
301 main_mc->add_theme_constant_override("margin_right", 4 * EDSCALE);
302 main_mc->add_theme_constant_override("margin_top", 4 * EDSCALE);
303 main_mc->add_theme_constant_override("margin_left", 4 * EDSCALE);
304 main_mc->add_theme_constant_override("margin_bottom", 4 * EDSCALE);
305 preview_content->add_child(main_mc);
306
307 HBoxContainer *main_hb = memnew(HBoxContainer);
308 main_mc->add_child(main_hb);
309 main_hb->add_theme_constant_override("separation", 20 * EDSCALE);
310
311 VBoxContainer *first_vb = memnew(VBoxContainer);
312 main_hb->add_child(first_vb);
313 first_vb->set_h_size_flags(SIZE_EXPAND_FILL);
314 first_vb->add_theme_constant_override("separation", 10 * EDSCALE);
315
316 first_vb->add_child(memnew(Label("Label")));
317
318 first_vb->add_child(memnew(Button("Button")));
319 Button *bt = memnew(Button);
320 bt->set_text(TTR("Toggle Button"));
321 bt->set_toggle_mode(true);
322 bt->set_pressed(true);
323 first_vb->add_child(bt);
324 bt = memnew(Button);
325 bt->set_text(TTR("Disabled Button"));
326 bt->set_disabled(true);
327 first_vb->add_child(bt);
328 Button *tb = memnew(Button);
329 tb->set_flat(true);
330 tb->set_text("Flat Button");
331 first_vb->add_child(tb);
332
333 CheckButton *cb = memnew(CheckButton);
334 cb->set_text("CheckButton");
335 first_vb->add_child(cb);
336 CheckBox *cbx = memnew(CheckBox);
337 cbx->set_text("CheckBox");
338 first_vb->add_child(cbx);
339
340 MenuButton *test_menu_button = memnew(MenuButton);
341 test_menu_button->set_text("MenuButton");
342 test_menu_button->get_popup()->add_item(TTR("Item"));
343 test_menu_button->get_popup()->add_item(TTR("Disabled Item"));
344 test_menu_button->get_popup()->set_item_disabled(1, true);
345 test_menu_button->get_popup()->add_separator();
346 test_menu_button->get_popup()->add_check_item(TTR("Check Item"));
347 test_menu_button->get_popup()->add_check_item(TTR("Checked Item"));
348 test_menu_button->get_popup()->set_item_checked(4, true);
349 test_menu_button->get_popup()->add_separator();
350 test_menu_button->get_popup()->add_radio_check_item(TTR("Radio Item"));
351 test_menu_button->get_popup()->add_radio_check_item(TTR("Checked Radio Item"));
352 test_menu_button->get_popup()->set_item_checked(7, true);
353 test_menu_button->get_popup()->add_separator(TTR("Named Separator"));
354
355 PopupMenu *test_submenu = memnew(PopupMenu);
356 test_menu_button->get_popup()->add_child(test_submenu);
357 test_submenu->set_name("submenu");
358 test_menu_button->get_popup()->add_submenu_item(TTR("Submenu"), "submenu");
359 test_submenu->add_item(TTR("Subitem 1"));
360 test_submenu->add_item(TTR("Subitem 2"));
361 first_vb->add_child(test_menu_button);
362
363 OptionButton *test_option_button = memnew(OptionButton);
364 test_option_button->add_item("OptionButton");
365 test_option_button->add_separator();
366 test_option_button->add_item(TTR("Has"));
367 test_option_button->add_item(TTR("Many"));
368 test_option_button->add_item(TTR("Options"));
369 first_vb->add_child(test_option_button);
370 test_color_picker_button = memnew(ColorPickerButton);
371 first_vb->add_child(test_color_picker_button);
372
373 VBoxContainer *second_vb = memnew(VBoxContainer);
374 second_vb->set_h_size_flags(SIZE_EXPAND_FILL);
375 main_hb->add_child(second_vb);
376 second_vb->add_theme_constant_override("separation", 10 * EDSCALE);
377 LineEdit *le = memnew(LineEdit);
378 le->set_text("LineEdit");
379 second_vb->add_child(le);
380 le = memnew(LineEdit);
381 le->set_text(TTR("Disabled LineEdit"));
382 le->set_editable(false);
383 second_vb->add_child(le);
384 TextEdit *te = memnew(TextEdit);
385 te->set_text("TextEdit");
386 te->set_custom_minimum_size(Size2(0, 100) * EDSCALE);
387 second_vb->add_child(te);
388 second_vb->add_child(memnew(SpinBox));
389
390 HBoxContainer *vhb = memnew(HBoxContainer);
391 second_vb->add_child(vhb);
392 vhb->set_custom_minimum_size(Size2(0, 100) * EDSCALE);
393 vhb->add_child(memnew(VSlider));
394 VScrollBar *vsb = memnew(VScrollBar);
395 vsb->set_page(25);
396 vhb->add_child(vsb);
397 vhb->add_child(memnew(VSeparator));
398 VBoxContainer *hvb = memnew(VBoxContainer);
399 vhb->add_child(hvb);
400 hvb->set_alignment(BoxContainer::ALIGNMENT_CENTER);
401 hvb->set_h_size_flags(SIZE_EXPAND_FILL);
402 hvb->add_child(memnew(HSlider));
403 HScrollBar *hsb = memnew(HScrollBar);
404 hsb->set_page(25);
405 hvb->add_child(hsb);
406 HSlider *hs = memnew(HSlider);
407 hs->set_editable(false);
408 hvb->add_child(hs);
409 hvb->add_child(memnew(HSeparator));
410 ProgressBar *pb = memnew(ProgressBar);
411 pb->set_value(50);
412 hvb->add_child(pb);
413
414 VBoxContainer *third_vb = memnew(VBoxContainer);
415 third_vb->set_h_size_flags(SIZE_EXPAND_FILL);
416 third_vb->add_theme_constant_override("separation", 10 * EDSCALE);
417 main_hb->add_child(third_vb);
418
419 TabContainer *tc = memnew(TabContainer);
420 third_vb->add_child(tc);
421 tc->set_custom_minimum_size(Size2(0, 135) * EDSCALE);
422 Control *tcc = memnew(Control);
423 tcc->set_name(TTR("Tab 1"));
424 tc->add_child(tcc);
425 tcc = memnew(Control);
426 tcc->set_name(TTR("Tab 2"));
427 tc->add_child(tcc);
428 tcc = memnew(Control);
429 tcc->set_name(TTR("Tab 3"));
430 tc->add_child(tcc);
431 tc->set_tab_disabled(2, true);
432
433 Tree *test_tree = memnew(Tree);
434 third_vb->add_child(test_tree);
435 test_tree->set_custom_minimum_size(Size2(0, 175) * EDSCALE);
436
437 TreeItem *item = test_tree->create_item();
438 item->set_text(0, "Tree");
439 item = test_tree->create_item(test_tree->get_root());
440 item->set_text(0, "Item");
441 item = test_tree->create_item(test_tree->get_root());
442 item->set_editable(0, true);
443 item->set_text(0, TTR("Editable Item"));
444 TreeItem *sub_tree = test_tree->create_item(test_tree->get_root());
445 sub_tree->set_text(0, TTR("Subtree"));
446 item = test_tree->create_item(sub_tree);
447 item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
448 item->set_editable(0, true);
449 item->set_text(0, "Check Item");
450 item = test_tree->create_item(sub_tree);
451 item->set_cell_mode(0, TreeItem::CELL_MODE_RANGE);
452 item->set_editable(0, true);
453 item->set_range_config(0, 0, 20, 0.1);
454 item->set_range(0, 2);
455 item = test_tree->create_item(sub_tree);
456 item->set_cell_mode(0, TreeItem::CELL_MODE_RANGE);
457 item->set_editable(0, true);
458 item->set_text(0, TTR("Has,Many,Options"));
459 item->set_range(0, 2);
460}
461
462void SceneThemeEditorPreview::_reload_scene() {
463 if (loaded_scene.is_null()) {
464 return;
465 }
466
467 if (loaded_scene->get_path().is_empty() || !ResourceLoader::exists(loaded_scene->get_path())) {
468 EditorNode::get_singleton()->show_warning(TTR("Invalid path, the PackedScene resource was probably moved or removed."));
469 emit_signal(SNAME("scene_invalidated"));
470 return;
471 }
472
473 for (int i = preview_content->get_child_count() - 1; i >= 0; i--) {
474 Node *node = preview_content->get_child(i);
475 node->queue_free();
476 preview_content->remove_child(node);
477 }
478
479 Node *instance = loaded_scene->instantiate();
480 if (!instance || !Object::cast_to<Control>(instance)) {
481 EditorNode::get_singleton()->show_warning(TTR("Invalid PackedScene resource, must have a Control node at its root."));
482 emit_signal(SNAME("scene_invalidated"));
483 return;
484 }
485
486 preview_content->add_child(instance);
487 emit_signal(SNAME("scene_reloaded"));
488}
489
490void SceneThemeEditorPreview::_notification(int p_what) {
491 switch (p_what) {
492 case NOTIFICATION_ENTER_TREE:
493 case NOTIFICATION_THEME_CHANGED: {
494 reload_scene_button->set_icon(get_editor_theme_icon(SNAME("Reload")));
495 } break;
496 }
497}
498
499void SceneThemeEditorPreview::_bind_methods() {
500 ADD_SIGNAL(MethodInfo("scene_invalidated"));
501 ADD_SIGNAL(MethodInfo("scene_reloaded"));
502}
503
504bool SceneThemeEditorPreview::set_preview_scene(const String &p_path) {
505 loaded_scene = ResourceLoader::load(p_path);
506 if (loaded_scene.is_null()) {
507 EditorNode::get_singleton()->show_warning(TTR("Invalid file, not a PackedScene resource."));
508 return false;
509 }
510
511 Node *instance = loaded_scene->instantiate();
512 if (!instance || !Object::cast_to<Control>(instance)) {
513 EditorNode::get_singleton()->show_warning(TTR("Invalid PackedScene resource, must have a Control node at its root."));
514 return false;
515 }
516
517 preview_content->add_child(instance);
518 return true;
519}
520
521String SceneThemeEditorPreview::get_preview_scene_path() const {
522 if (loaded_scene.is_null()) {
523 return "";
524 }
525
526 return loaded_scene->get_path();
527}
528
529SceneThemeEditorPreview::SceneThemeEditorPreview() {
530 preview_toolbar->add_child(memnew(VSeparator));
531
532 reload_scene_button = memnew(Button);
533 reload_scene_button->set_flat(true);
534 reload_scene_button->set_tooltip_text(TTR("Reload the scene to reflect its most actual state."));
535 preview_toolbar->add_child(reload_scene_button);
536 reload_scene_button->connect("pressed", callable_mp(this, &SceneThemeEditorPreview::_reload_scene));
537}
538