diff options
Diffstat (limited to 'editor')
23 files changed, 1405 insertions, 243 deletions
diff --git a/editor/editor_command_palette.cpp b/editor/editor_command_palette.cpp index e69ced8522..71e9fac219 100644 --- a/editor/editor_command_palette.cpp +++ b/editor/editor_command_palette.cpp @@ -212,6 +212,12 @@ void EditorCommandPalette::_add_command(String p_command_name, String p_key_name command.callable = p_binded_action; command.shortcut = p_shortcut_text; + // Commands added from plugins don't exist yet when the history is loaded, so we assign the last use time here if it was recorded. + Dictionary command_history = EditorSettings::get_singleton()->get_project_metadata("command_palette", "command_history", Dictionary()); + if (command_history.has(p_key_name)) { + command.last_used = command_history[p_key_name]; + } + commands[p_key_name] = command; } @@ -242,7 +248,9 @@ void EditorCommandPalette::register_shortcuts_as_command() { Array history_entries = command_history.keys(); for (int i = 0; i < history_entries.size(); i++) { const String &history_key = history_entries[i]; - commands[history_key].last_used = command_history[history_key]; + if (commands.has(history_key)) { + commands[history_key].last_used = command_history[history_key]; + } } } diff --git a/editor/editor_log.cpp b/editor/editor_log.cpp index 346b93a87c..251e1c2385 100644 --- a/editor/editor_log.cpp +++ b/editor/editor_log.cpp @@ -37,7 +37,7 @@ #include "scene/gui/center_container.h" #include "scene/resources/font.h" -void EditorLog::_error_handler(void *p_self, const char *p_func, const char *p_file, int p_line, const char *p_error, const char *p_errorexp, ErrorHandlerType p_type) { +void EditorLog::_error_handler(void *p_self, const char *p_func, const char *p_file, int p_line, const char *p_error, const char *p_errorexp, bool p_editor_notify, ErrorHandlerType p_type) { EditorLog *self = (EditorLog *)p_self; if (self->current != Thread::get_caller_id()) { return; @@ -50,6 +50,10 @@ void EditorLog::_error_handler(void *p_self, const char *p_func, const char *p_f err_str = String(p_file) + ":" + itos(p_line) + " - " + String(p_error); } + if (p_editor_notify) { + err_str += " (User)"; + } + if (p_type == ERR_HANDLER_WARNING) { self->add_message(err_str, MSG_TYPE_WARNING); } else { diff --git a/editor/editor_log.h b/editor/editor_log.h index 6cbf4bedee..43cc5680bd 100644 --- a/editor/editor_log.h +++ b/editor/editor_log.h @@ -136,7 +136,7 @@ private: bool is_loading_state = false; // Used to disable saving requests while loading (some signals from buttons will try trigger a save, which happens during loading). Timer *save_state_timer; - static void _error_handler(void *p_self, const char *p_func, const char *p_file, int p_line, const char *p_error, const char *p_errorexp, ErrorHandlerType p_type); + static void _error_handler(void *p_self, const char *p_func, const char *p_file, int p_line, const char *p_error, const char *p_errorexp, bool p_editor_notify, ErrorHandlerType p_type); ErrorHandlerList eh; diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 2f92f60d5e..ffcb75ba2c 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -95,6 +95,7 @@ #include "editor/editor_settings.h" #include "editor/editor_spin_slider.h" #include "editor/editor_themes.h" +#include "editor/editor_toaster.h" #include "editor/editor_translation_parser.h" #include "editor/export_template_manager.h" #include "editor/filesystem_dock.h" @@ -6748,6 +6749,9 @@ EditorNode::EditorNode() { bottom_panel_hb_editors->set_h_size_flags(Control::SIZE_EXPAND_FILL); bottom_panel_hb->add_child(bottom_panel_hb_editors); + editor_toaster = memnew(EditorToaster); + bottom_panel_hb->add_child(editor_toaster); + VBoxContainer *version_info_vbc = memnew(VBoxContainer); bottom_panel_hb->add_child(version_info_vbc); diff --git a/editor/editor_node.h b/editor/editor_node.h index 73feeecfee..5c89823ad8 100644 --- a/editor/editor_node.h +++ b/editor/editor_node.h @@ -37,6 +37,7 @@ #include "editor/editor_folding.h" #include "editor/editor_native_shader_source_visualizer.h" #include "editor/editor_run.h" +#include "editor/editor_toaster.h" #include "editor/inspector_dock.h" #include "editor/property_editor.h" #include "editor/scene_tree_dock.h" @@ -438,6 +439,7 @@ private: HBoxContainer *bottom_panel_hb; HBoxContainer *bottom_panel_hb_editors; VBoxContainer *bottom_panel_vb; + EditorToaster *editor_toaster; LinkButton *version_btn; Button *bottom_panel_raise; diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index 223cc6650c..1da9213b89 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -442,6 +442,7 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) { _initial_set("interface/editor/hide_console_window", false); _initial_set("interface/editor/mouse_extra_buttons_navigate_history", true); _initial_set("interface/editor/save_each_scene_on_quit", true); // Regression + EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "interface/editor/show_internal_errors_in_toast_notifications", 0, "Auto,Enabled,Disabled") // Inspector EDITOR_SETTING(Variant::INT, PROPERTY_HINT_RANGE, "interface/inspector/max_array_dictionary_items_per_page", 20, "10,100,1") diff --git a/editor/editor_toaster.cpp b/editor/editor_toaster.cpp new file mode 100644 index 0000000000..0d45a7f1d8 --- /dev/null +++ b/editor/editor_toaster.cpp @@ -0,0 +1,511 @@ +/*************************************************************************/ +/* editor_toaster.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "editor/editor_node.h" +#include "editor/editor_scale.h" +#include "scene/gui/label.h" +#include "scene/gui/panel_container.h" + +#include "editor_toaster.h" + +EditorToaster *EditorToaster::singleton; + +void EditorToaster::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_INTERNAL_PROCESS: { + double delta = get_process_delta_time(); + + // Check if one element is hovered, if so, don't elapse time. + bool hovered = false; + for (const KeyValue<Control *, Toast> &element : toasts) { + if (Rect2(Vector2(), element.key->get_size()).has_point(element.key->get_local_mouse_position())) { + hovered = true; + break; + } + } + + // Elapses the time and remove toasts if needed. + if (!hovered) { + for (const KeyValue<Control *, Toast> &element : toasts) { + if (!element.value.popped || element.value.duration <= 0) { + continue; + } + toasts[element.key].remaining_time -= delta; + if (toasts[element.key].remaining_time < 0) { + close(element.key); + } + element.key->update(); + } + } else { + // Reset the timers when hovered. + for (const KeyValue<Control *, Toast> &element : toasts) { + if (!element.value.popped || element.value.duration <= 0) { + continue; + } + toasts[element.key].remaining_time = element.value.duration; + element.key->update(); + } + } + + // Change alpha over time. + bool needs_update = false; + for (const KeyValue<Control *, Toast> &element : toasts) { + Color modulate = element.key->get_modulate(); + + // Change alpha over time. + if (element.value.popped && modulate.a < 1.0) { + modulate.a += delta * 3; + element.key->set_modulate(modulate); + } else if (!element.value.popped && modulate.a > 0.0) { + modulate.a -= delta * 2; + element.key->set_modulate(modulate); + } + + // Hide element if it is not visible anymore. + if (modulate.a <= 0) { + element.key->hide(); + needs_update = true; + } + } + + if (needs_update) { + _update_vbox_position(); + _update_disable_notifications_button(); + main_button->update(); + } + } break; + case NOTIFICATION_ENTER_TREE: + case NOTIFICATION_THEME_CHANGED: { + if (vbox_container->is_visible()) { + main_button->set_icon(get_theme_icon(SNAME("Notification"), SNAME("EditorIcons"))); + } else { + main_button->set_icon(get_theme_icon(SNAME("NotificationDisabled"), SNAME("EditorIcons"))); + } + disable_notifications_button->set_icon(get_theme_icon(SNAME("NotificationDisabled"), SNAME("EditorIcons"))); + + // Styleboxes background. + info_panel_style_background->set_bg_color(get_theme_color("base_color", "Editor")); + + warning_panel_style_background->set_bg_color(get_theme_color("base_color", "Editor")); + warning_panel_style_background->set_border_color(get_theme_color("warning_color", "Editor")); + + error_panel_style_background->set_bg_color(get_theme_color("base_color", "Editor")); + error_panel_style_background->set_border_color(get_theme_color("error_color", "Editor")); + + // Styleboxes progress. + info_panel_style_progress->set_bg_color(get_theme_color("base_color", "Editor").lightened(0.03)); + + warning_panel_style_progress->set_bg_color(get_theme_color("base_color", "Editor").lightened(0.03)); + warning_panel_style_progress->set_border_color(get_theme_color("warning_color", "Editor")); + + error_panel_style_progress->set_bg_color(get_theme_color("base_color", "Editor").lightened(0.03)); + error_panel_style_progress->set_border_color(get_theme_color("error_color", "Editor")); + + main_button->update(); + disable_notifications_button->update(); + } break; + case NOTIFICATION_TRANSFORM_CHANGED: { + _update_vbox_position(); + _update_disable_notifications_button(); + } break; + default: + break; + } +} + +void EditorToaster::_error_handler(void *p_self, const char *p_func, const char *p_file, int p_line, const char *p_error, const char *p_errorexp, bool p_editor_notify, ErrorHandlerType p_type) { + if (!EditorToaster::get_singleton()) { + return; + } + +#ifdef DEV_ENABLED + bool in_dev = true; +#else + bool in_dev = false; +#endif + + int show_all_setting = EDITOR_GET("interface/editor/show_internal_errors_in_toast_notifications"); + + if (p_editor_notify || (show_all_setting == 0 && in_dev) || show_all_setting == 1) { + String err_str; + if (p_errorexp && p_errorexp[0]) { + err_str = p_errorexp; + } else { + err_str = String(p_error); + } + String tooltip_str = String(p_file) + ":" + itos(p_line); + + if (!p_editor_notify) { + if (p_type == ERR_HANDLER_WARNING) { + err_str = "INTERNAL WARNING: " + err_str; + } else { + err_str = "INTERNAL ERROR: " + err_str; + } + } + + if (p_type == ERR_HANDLER_WARNING) { + EditorToaster::get_singleton()->popup_str(err_str, SEVERITY_WARNING, tooltip_str); + } else { + EditorToaster::get_singleton()->popup_str(err_str, SEVERITY_ERROR, tooltip_str); + } + } +} + +void EditorToaster::_update_vbox_position() { + // This is kind of a workaround because it's hard to keep the VBox anchroed to the bottom. + vbox_container->set_size(Vector2()); + vbox_container->set_position(get_global_position() - vbox_container->get_size() + Vector2(get_size().x, -5 * EDSCALE)); +} + +void EditorToaster::_update_disable_notifications_button() { + bool any_visible = false; + for (KeyValue<Control *, Toast> element : toasts) { + if (element.key->is_visible()) { + any_visible = true; + break; + } + } + + if (!any_visible || !vbox_container->is_visible()) { + disable_notifications_panel->hide(); + } else { + disable_notifications_panel->show(); + disable_notifications_panel->set_position(get_global_position() + Vector2(5 * EDSCALE, -disable_notifications_panel->get_minimum_size().y) + Vector2(get_size().x, -5 * EDSCALE)); + } +} + +void EditorToaster::_auto_hide_or_free_toasts() { + // Hide or free old temporary items. + int visible_temporary = 0; + int temporary = 0; + LocalVector<Control *> to_delete; + for (int i = vbox_container->get_child_count() - 1; i >= 0; i--) { + Control *control = Object::cast_to<Control>(vbox_container->get_child(i)); + if (toasts[control].duration <= 0) { + continue; // Ignore non-temporary toasts. + } + + temporary++; + if (control->is_visible()) { + visible_temporary++; + } + + // Hide + if (visible_temporary > max_temporary_count) { + close(control); + } + + // Free + if (temporary > max_temporary_count * 2) { + to_delete.push_back(control); + } + } + + // Delete the control right away (removed as child) as it might cause issues otherwise when iterative over the vbox_container children. + for (unsigned int i = 0; i < to_delete.size(); i++) { + vbox_container->remove_child(to_delete[i]); + to_delete[i]->queue_delete(); + toasts.erase(to_delete[i]); + } +} + +void EditorToaster::_draw_button() { + bool has_one = false; + Severity highest_severity = SEVERITY_INFO; + for (const KeyValue<Control *, Toast> &element : toasts) { + if (!element.key->is_visible()) { + continue; + } + has_one = true; + if (element.value.severity > highest_severity) { + highest_severity = element.value.severity; + } + } + + if (!has_one) { + return; + } + + Color color; + real_t button_radius = main_button->get_size().x / 8; + switch (highest_severity) { + case SEVERITY_INFO: + color = get_theme_color("accent_color", "Editor"); + break; + case SEVERITY_WARNING: + color = get_theme_color("warning_color", "Editor"); + break; + case SEVERITY_ERROR: + color = get_theme_color("error_color", "Editor"); + break; + default: + break; + } + main_button->draw_circle(Vector2(button_radius * 2, button_radius * 2), button_radius, color); +} + +void EditorToaster::_draw_progress(Control *panel) { + if (toasts.has(panel) && toasts[panel].remaining_time > 0 && toasts[panel].duration > 0) { + Size2 size = panel->get_size(); + size.x *= MIN(1, Math::range_lerp(toasts[panel].remaining_time, 0, toasts[panel].duration, 0, 2)); + + Ref<StyleBoxFlat> stylebox; + switch (toasts[panel].severity) { + case SEVERITY_INFO: + stylebox = info_panel_style_progress; + break; + case SEVERITY_WARNING: + stylebox = warning_panel_style_progress; + break; + case SEVERITY_ERROR: + stylebox = error_panel_style_progress; + break; + default: + break; + } + panel->draw_style_box(stylebox, Rect2(Vector2(), size)); + } +} + +void EditorToaster::_set_notifications_enabled(bool p_enabled) { + vbox_container->set_visible(p_enabled); + if (p_enabled) { + main_button->set_icon(get_theme_icon(SNAME("Notification"), SNAME("EditorIcons"))); + } else { + main_button->set_icon(get_theme_icon(SNAME("NotificationDisabled"), SNAME("EditorIcons"))); + } + _update_disable_notifications_button(); +} + +void EditorToaster::_repop_old() { + // Repop olds, up to max_temporary_count + bool needs_update = false; + int visible = 0; + for (int i = vbox_container->get_child_count() - 1; i >= 0; i--) { + Control *control = Object::cast_to<Control>(vbox_container->get_child(i)); + if (!control->is_visible()) { + control->show(); + toasts[control].remaining_time = toasts[control].duration; + toasts[control].popped = true; + needs_update = true; + } + visible++; + if (visible >= max_temporary_count) { + break; + } + } + if (needs_update) { + _update_vbox_position(); + _update_disable_notifications_button(); + main_button->update(); + } +} + +Control *EditorToaster::popup(Control *p_control, Severity p_severity, double p_time, String p_tooltip) { + // Create the panel according to the severity. + PanelContainer *panel = memnew(PanelContainer); + panel->set_tooltip(p_tooltip); + switch (p_severity) { + case SEVERITY_INFO: + panel->add_theme_style_override("panel", info_panel_style_background); + break; + case SEVERITY_WARNING: + panel->add_theme_style_override("panel", warning_panel_style_background); + break; + case SEVERITY_ERROR: + panel->add_theme_style_override("panel", error_panel_style_background); + break; + default: + break; + } + panel->set_modulate(Color(1, 1, 1, 0)); + panel->connect("draw", callable_bind(callable_mp(this, &EditorToaster::_draw_progress), panel)); + + // Horizontal container. + HBoxContainer *hbox_container = memnew(HBoxContainer); + hbox_container->set_h_size_flags(SIZE_EXPAND_FILL); + panel->add_child(hbox_container); + + // Content control. + p_control->set_h_size_flags(SIZE_EXPAND_FILL); + hbox_container->add_child(p_control); + + // Close button. + if (p_time > 0.0) { + Button *close_button = memnew(Button); + close_button->set_flat(true); + close_button->set_icon(get_theme_icon("Close", "EditorIcons")); + close_button->connect("pressed", callable_bind(callable_mp(this, &EditorToaster::close), panel)); + hbox_container->add_child(close_button); + } + + toasts[panel].severity = p_severity; + if (p_time > 0.0) { + toasts[panel].duration = p_time; + toasts[panel].remaining_time = p_time; + } else { + toasts[panel].duration = -1.0; + } + toasts[panel].popped = true; + vbox_container->add_child(panel); + _auto_hide_or_free_toasts(); + _update_vbox_position(); + _update_disable_notifications_button(); + main_button->update(); + + return panel; +} + +void EditorToaster::popup_str(String p_message, Severity p_severity, String p_tooltip) { + // Check if we already have a popup with the given message. + Control *control = nullptr; + for (KeyValue<Control *, Toast> element : toasts) { + if (element.value.message == p_message && element.value.severity == p_severity && element.value.tooltip == p_tooltip) { + control = element.key; + break; + } + } + + // Create a new message if needed. + if (control == nullptr) { + Label *label = memnew(Label); + + control = popup(label, p_severity, default_message_duration, p_tooltip); + toasts[control].message = p_message; + toasts[control].tooltip = p_tooltip; + toasts[control].count = 1; + } else { + if (toasts[control].popped) { + toasts[control].count += 1; + } else { + toasts[control].count = 1; + } + toasts[control].remaining_time = toasts[control].duration; + toasts[control].popped = true; + control->show(); + vbox_container->move_child(control, vbox_container->get_child_count()); + _auto_hide_or_free_toasts(); + _update_vbox_position(); + _update_disable_notifications_button(); + main_button->update(); + } + + // Retrieve the label back then update the text. + Label *label = Object::cast_to<Label>(control->get_child(0)->get_child(0)); + ERR_FAIL_COND(!label); + if (toasts[control].count == 1) { + label->set_text(p_message); + } else { + label->set_text(vformat("%s (%d)", p_message, toasts[control].count)); + } +} + +void EditorToaster::close(Control *p_control) { + ERR_FAIL_COND(!toasts.has(p_control)); + toasts[p_control].remaining_time = -1.0; + toasts[p_control].popped = false; +} + +EditorToaster *EditorToaster::get_singleton() { + return singleton; +} + +EditorToaster::EditorToaster() { + set_notify_transform(true); + set_process_internal(true); + + // VBox. + vbox_container = memnew(VBoxContainer); + vbox_container->set_as_top_level(true); + vbox_container->connect("resized", callable_mp(this, &EditorToaster::_update_vbox_position)); + add_child(vbox_container); + + // Theming (background). + info_panel_style_background.instantiate(); + info_panel_style_background->set_corner_radius_all(stylebox_radius * EDSCALE); + + warning_panel_style_background.instantiate(); + warning_panel_style_background->set_border_width(SIDE_LEFT, stylebox_radius * EDSCALE); + warning_panel_style_background->set_corner_radius_all(stylebox_radius * EDSCALE); + + error_panel_style_background.instantiate(); + error_panel_style_background->set_border_width(SIDE_LEFT, stylebox_radius * EDSCALE); + error_panel_style_background->set_corner_radius_all(stylebox_radius * EDSCALE); + + Ref<StyleBoxFlat> boxes[] = { info_panel_style_background, warning_panel_style_background, error_panel_style_background }; + for (int i = 0; i < 3; i++) { + boxes[i]->set_default_margin(SIDE_LEFT, int(stylebox_radius * 2.5)); + boxes[i]->set_default_margin(SIDE_RIGHT, int(stylebox_radius * 2.5)); + boxes[i]->set_default_margin(SIDE_TOP, 3); + boxes[i]->set_default_margin(SIDE_BOTTOM, 3); + } + + // Theming (progress). + info_panel_style_progress.instantiate(); + info_panel_style_progress->set_corner_radius_all(stylebox_radius * EDSCALE); + + warning_panel_style_progress.instantiate(); + warning_panel_style_progress->set_border_width(SIDE_LEFT, stylebox_radius * EDSCALE); + warning_panel_style_progress->set_corner_radius_all(stylebox_radius * EDSCALE); + + error_panel_style_progress.instantiate(); + error_panel_style_progress->set_border_width(SIDE_LEFT, stylebox_radius * EDSCALE); + error_panel_style_progress->set_corner_radius_all(stylebox_radius * EDSCALE); + + // Main button. + main_button = memnew(Button); + main_button->set_flat(true); + main_button->connect("pressed", callable_mp(this, &EditorToaster::_set_notifications_enabled), varray(true)); + main_button->connect("pressed", callable_mp(this, &EditorToaster::_repop_old)); + main_button->connect("draw", callable_mp(this, &EditorToaster::_draw_button)); + add_child(main_button); + + // Disable notification button. + disable_notifications_panel = memnew(PanelContainer); + disable_notifications_panel->set_as_top_level(true); + disable_notifications_panel->add_theme_style_override("panel", info_panel_style_background); + add_child(disable_notifications_panel); + + disable_notifications_button = memnew(Button); + disable_notifications_button->set_flat(true); + disable_notifications_button->connect("pressed", callable_mp(this, &EditorToaster::_set_notifications_enabled), varray(false)); + disable_notifications_panel->add_child(disable_notifications_button); + + // Other + singleton = this; + + eh.errfunc = _error_handler; + add_error_handler(&eh); +}; + +EditorToaster::~EditorToaster() { + singleton = nullptr; + remove_error_handler(&eh); +} diff --git a/editor/editor_toaster.h b/editor/editor_toaster.h new file mode 100644 index 0000000000..aac80d8fb1 --- /dev/null +++ b/editor/editor_toaster.h @@ -0,0 +1,116 @@ +/*************************************************************************/ +/* editor_toaster.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef EDITOR_TOASTER_H +#define EDITOR_TOASTER_H + +#include "scene/gui/box_container.h" +#include "scene/gui/button.h" +#include "scene/gui/popup.h" + +#include "core/string/ustring.h" +#include "core/templates/local_vector.h" + +class EditorToaster : public HBoxContainer { + GDCLASS(EditorToaster, HBoxContainer); + +public: + enum Severity { + SEVERITY_INFO = 0, + SEVERITY_WARNING, + SEVERITY_ERROR, + }; + +private: + ErrorHandlerList eh; + + const int stylebox_radius = 3; + + Ref<StyleBoxFlat> info_panel_style_background; + Ref<StyleBoxFlat> warning_panel_style_background; + Ref<StyleBoxFlat> error_panel_style_background; + + Ref<StyleBoxFlat> info_panel_style_progress; + Ref<StyleBoxFlat> warning_panel_style_progress; + Ref<StyleBoxFlat> error_panel_style_progress; + + Button *main_button; + PanelContainer *disable_notifications_panel; + Button *disable_notifications_button; + + VBoxContainer *vbox_container; + const int max_temporary_count = 5; + struct Toast { + Severity severity = SEVERITY_INFO; + + // Timing. + real_t duration = -1.0; + real_t remaining_time = 0.0; + bool popped = false; + + // Messages + String message; + String tooltip; + int count = 0; + }; + Map<Control *, Toast> toasts; + + const double default_message_duration = 5.0; + + static void _error_handler(void *p_self, const char *p_func, const char *p_file, int p_line, const char *p_error, const char *p_errorexp, bool p_editor_notify, ErrorHandlerType p_type); + void _update_vbox_position(); + void _update_disable_notifications_button(); + void _auto_hide_or_free_toasts(); + + void _draw_button(); + void _draw_progress(Control *panel); + + void _set_notifications_enabled(bool p_enabled); + void _repop_old(); + +protected: + static EditorToaster *singleton; + + void _notification(int p_what); + +public: + static EditorToaster *get_singleton(); + + Control *popup(Control *p_control, Severity p_severity = SEVERITY_INFO, double p_time = 0.0, String p_tooltip = String()); + void popup_str(String p_message, Severity p_severity = SEVERITY_INFO, String p_tooltip = String()); + void close(Control *p_control); + + EditorToaster(); + ~EditorToaster(); +}; + +VARIANT_ENUM_CAST(EditorToaster::Severity); + +#endif // EDITOR_TOASTER_H diff --git a/editor/icons/Notification.svg b/editor/icons/Notification.svg new file mode 100644 index 0000000000..1f1f9c3e15 --- /dev/null +++ b/editor/icons/Notification.svg @@ -0,0 +1 @@ +<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m12.089506 1.2353795c-.498141-.2384823-1.095292-.027987-1.333775.4701537-.01372.028981-.02341.059557-.03428.089693-.01467.023016-.02777.046925-.04071.071459-.04899-.00527-.09728-.00862-.146087-.011473-1.4730257-.6255101-3.1777024.0153376-3.8738627 1.4563251l-1.7272425 3.6078572s-.3364181.7034345-.8079671 1.1268133c-.00105.0009371-.00239.00174-.00344.00268-.2721193.1337295-.5707545.185826-.8605632.0470816-.4981411-.2384824-1.0952924-.0279876-1.3337749.4701537-.01605.033526-.029907.066894-.041944.1011828-.018769.030371-.036749.06319-.052515.096122-.2384825.4981412-.027988 1.0952923.4701537 1.3337751l9.0196437 4.318106c.498141.238482 1.095292.02799 1.333775-.470154.01577-.03293.0301-.0675.04191-.1012.0192-.03086.0365-.06257.05255-.0961.238483-.498141.02799-1.095292-.470153-1.333775-.901965-.43181-.03834-2.235739-.03834-2.235739l1.727237-3.6078618c.715447-1.4944233.08396-3.2858776-1.410461-4.0013247.238482-.4981411.02799-1.0952926-.470154-1.333775zm-5.5145786 11.3714015c-.322341.673306-.037829 1.480435.6354753 1.802775.6733031.32234 1.4804334.03783 1.8027749-.635476z" fill="#e6e6e6"/></svg> diff --git a/editor/icons/NotificationDisabled.svg b/editor/icons/NotificationDisabled.svg new file mode 100644 index 0000000000..0e4465bee8 --- /dev/null +++ b/editor/icons/NotificationDisabled.svg @@ -0,0 +1 @@ +<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m11.705078 1.1386719c-.389281-.0180576-.770356.1928007-.949219.5664062-.01372.028981-.024286.0597078-.035156.0898438-.01467.023016-.026122.0477316-.039062.0722656-.04899-.00527-.097678-.0088657-.146485-.0117187-1.4730253-.6255102-3.1788394.0160437-3.8749998 1.4570312l-1.7265624 3.6074219s-.3370448.7035743-.8085938 1.1269531l-.0019531.0019531c-.2721193.1337295-.5715196.1856194-.8613281.046875-.4981413-.2384824-1.0955019-.0274382-1.3339844.4707031-.01605.0335262-.0289787.0672737-.0410156.1015626-.0187691.0303709-.0369684.0627711-.0527344.0957031-.2384825.4981412-.0293917 1.0955019.46875 1.3339841l.3398437.16211 10.8984379-6.9394535c-.263272-.3070418-.592225-.5660832-.980469-.7519531.238482-.4981411.027441-1.0935489-.470703-1.3320313-.124536-.0596206-.255006-.091637-.384766-.0976562zm2.435547 2.8652343a.94188849 1 0 0 0 -.566406.1386719l-12.1171878 7.7148439a.94188849 1 0 0 0 -.3222656 1.373047.94188849 1 0 0 0 1.2910156.341797l12.1171878-7.7148441a.94188849 1 0 0 0 .322265-1.3710938.94188849 1 0 0 0 -.724609-.4824219zm-.509766 3.2753907-7.3867184 4.7050781 5.0781254 2.431641c.498141.238482 1.095501.027442 1.333984-.470704.01577-.03293.031159-.067862.042969-.101562.0192-.03086.036684-.062173.052734-.095703.238483-.498141.02744-1.095501-.470703-1.333985-.901965-.431809-.039063-2.236328-.039062-2.236328zm-7.0566402 5.3281251c-.322341.673306-.0365856 1.480394.6367187 1.802734.6733031.32234 1.4803929.036588 1.8027344-.636718z" fill="#e6e6e6"/></svg> diff --git a/editor/plugins/editor_preview_plugins.h b/editor/plugins/editor_preview_plugins.h index 6e8b9a34cf..091feae5fb 100644 --- a/editor/plugins/editor_preview_plugins.h +++ b/editor/plugins/editor_preview_plugins.h @@ -173,4 +173,22 @@ public: EditorFontPreviewPlugin(); ~EditorFontPreviewPlugin(); }; + +class EditorTileMapPatternPreviewPlugin : public EditorResourcePreviewGenerator { + GDCLASS(EditorTileMapPatternPreviewPlugin, EditorResourcePreviewGenerator); + + mutable SafeFlag preview_done; + + void _preview_done(const Variant &p_udata); + +protected: + static void _bind_methods(); + +public: + virtual bool handles(const String &p_type) const override; + virtual Ref<Texture2D> generate(const RES &p_from, const Size2 &p_size) const override; + + EditorTileMapPatternPreviewPlugin(); + ~EditorTileMapPatternPreviewPlugin(); +}; #endif // EDITORPREVIEWPLUGINS_H diff --git a/editor/plugins/tiles/atlas_merging_dialog.cpp b/editor/plugins/tiles/atlas_merging_dialog.cpp index 2a8a3216ed..efccac7b74 100644 --- a/editor/plugins/tiles/atlas_merging_dialog.cpp +++ b/editor/plugins/tiles/atlas_merging_dialog.cpp @@ -118,6 +118,7 @@ void AtlasMergingDialog::_generate_merged(Vector<Ref<TileSetAtlasSource>> p_atla output_image_texture.instantiate(); output_image_texture->create_from_image(output_image); + merged->set_name(p_atlas_sources[0]->get_name()); merged->set_texture(output_image_texture); merged->set_texture_region_size(new_texture_region_size); } diff --git a/editor/plugins/tiles/tile_map_editor.cpp b/editor/plugins/tiles/tile_map_editor.cpp index 5ca39b82b2..5df4f40b55 100644 --- a/editor/plugins/tiles/tile_map_editor.cpp +++ b/editor/plugins/tiles/tile_map_editor.cpp @@ -36,6 +36,7 @@ #include "editor/editor_scale.h" #include "editor/plugins/canvas_item_editor_plugin.h" +#include "scene/2d/camera_2d.h" #include "scene/gui/center_container.h" #include "scene/gui/split_container.h" @@ -43,31 +44,11 @@ #include "core/math/geometry_2d.h" #include "core/os/keyboard.h" -void TileMapEditorTilesPlugin::_notification(int p_what) { - switch (p_what) { - case NOTIFICATION_ENTER_TREE: - case NOTIFICATION_THEME_CHANGED: - select_tool_button->set_icon(get_theme_icon(SNAME("ToolSelect"), SNAME("EditorIcons"))); - paint_tool_button->set_icon(get_theme_icon(SNAME("Edit"), SNAME("EditorIcons"))); - line_tool_button->set_icon(get_theme_icon(SNAME("CurveLinear"), SNAME("EditorIcons"))); - rect_tool_button->set_icon(get_theme_icon(SNAME("Rectangle"), SNAME("EditorIcons"))); - bucket_tool_button->set_icon(get_theme_icon(SNAME("Bucket"), SNAME("EditorIcons"))); - - picker_button->set_icon(get_theme_icon(SNAME("ColorPick"), SNAME("EditorIcons"))); - erase_button->set_icon(get_theme_icon(SNAME("Eraser"), SNAME("EditorIcons"))); - - missing_atlas_texture_icon = get_theme_icon(SNAME("TileSet"), SNAME("EditorIcons")); - break; - case NOTIFICATION_VISIBILITY_CHANGED: - _stop_dragging(); - break; - } -} - void TileMapEditorTilesPlugin::tile_set_changed() { _update_fix_selected_and_hovered(); _update_tile_set_sources_list(); - _update_bottom_panel(); + _update_source_display(); + _update_patterns_list(); } void TileMapEditorTilesPlugin::_on_random_tile_checkbox_toggled(bool p_pressed) { @@ -125,8 +106,19 @@ void TileMapEditorTilesPlugin::_update_toolbar() { } } -Control *TileMapEditorTilesPlugin::get_toolbar() const { - return toolbar; +Vector<TileMapEditorPlugin::TabData> TileMapEditorTilesPlugin::get_tabs() const { + Vector<TileMapEditorPlugin::TabData> tabs; + tabs.push_back({ toolbar, tiles_bottom_panel }); + tabs.push_back({ toolbar, patterns_bottom_panel }); + return tabs; +} + +void TileMapEditorTilesPlugin::_tab_changed() { + if (tiles_bottom_panel->is_visible_in_tree()) { + _update_selection_pattern_from_tileset_tiles_selection(); + } else if (patterns_bottom_panel->is_visible_in_tree()) { + _update_selection_pattern_from_tileset_pattern_selection(); + } } void TileMapEditorTilesPlugin::_update_tile_set_sources_list() { @@ -152,22 +144,31 @@ void TileMapEditorTilesPlugin::_update_tile_set_sources_list() { Ref<Texture2D> texture; String item_text; + // Common to all type of sources. + if (!source->get_name().is_empty()) { + item_text = vformat(TTR("%s (id:%d)"), source->get_name(), source_id); + } + // Atlas source. TileSetAtlasSource *atlas_source = Object::cast_to<TileSetAtlasSource>(source); if (atlas_source) { texture = atlas_source->get_texture(); - if (texture.is_valid()) { - item_text = vformat("%s (ID: %d)", texture->get_path().get_file(), source_id); - } else { - item_text = vformat("No Texture Atlas Source (ID: %d)", source_id); + if (item_text.is_empty()) { + if (texture.is_valid()) { + item_text = vformat("%s (ID: %d)", texture->get_path().get_file(), source_id); + } else { + item_text = vformat("No Texture Atlas Source (ID: %d)", source_id); + } } } // Scene collection source. TileSetScenesCollectionSource *scene_collection_source = Object::cast_to<TileSetScenesCollectionSource>(source); if (scene_collection_source) { - texture = get_theme_icon(SNAME("PackedScene"), SNAME("EditorIcons")); - item_text = vformat(TTR("Scene Collection Source (ID: %d)"), source_id); + texture = tiles_bottom_panel->get_theme_icon(SNAME("PackedScene"), SNAME("EditorIcons")); + if (item_text.is_empty()) { + item_text = vformat(TTR("Scene Collection Source (ID: %d)"), source_id); + } } // Use default if not valid. @@ -196,7 +197,7 @@ void TileMapEditorTilesPlugin::_update_tile_set_sources_list() { TilesEditor::get_singleton()->set_sources_lists_current(sources_list->get_current()); } -void TileMapEditorTilesPlugin::_update_bottom_panel() { +void TileMapEditorTilesPlugin::_update_source_display() { // Update the atlas display. TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); if (!tile_map) { @@ -243,6 +244,81 @@ void TileMapEditorTilesPlugin::_update_bottom_panel() { } } +void TileMapEditorTilesPlugin::_patterns_item_list_gui_input(const Ref<InputEvent> &p_event) { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + if (ED_IS_SHORTCUT("tiles_editor/paste", p_event) && p_event->is_pressed() && !p_event->is_echo()) { + select_last_pattern = true; + int new_pattern_index = tile_set->get_patterns_count(); + undo_redo->create_action(TTR("Add TileSet pattern")); + undo_redo->add_do_method(*tile_set, "add_pattern", tile_map_clipboard, new_pattern_index); + undo_redo->add_undo_method(*tile_set, "remove_pattern", new_pattern_index); + undo_redo->commit_action(); + patterns_item_list->accept_event(); + } + + if (ED_IS_SHORTCUT("tiles_editor/delete", p_event) && p_event->is_pressed() && !p_event->is_echo()) { + Vector<int> selected = patterns_item_list->get_selected_items(); + undo_redo->create_action(TTR("Remove TileSet patterns")); + for (int i = 0; i < selected.size(); i++) { + int pattern_index = selected[i]; + undo_redo->add_do_method(*tile_set, "remove_pattern", pattern_index); + undo_redo->add_undo_method(*tile_set, "add_pattern", tile_set->get_pattern(pattern_index), pattern_index); + } + undo_redo->commit_action(); + patterns_item_list->accept_event(); + } +} + +void TileMapEditorTilesPlugin::_pattern_preview_done(Ref<TileMapPattern> p_pattern, Ref<Texture2D> p_texture) { + // TODO optimize ? + for (int i = 0; i < patterns_item_list->get_item_count(); i++) { + if (patterns_item_list->get_item_metadata(i) == p_pattern) { + patterns_item_list->set_item_icon(i, p_texture); + break; + } + } +} + +void TileMapEditorTilesPlugin::_update_patterns_list() { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + // Recreate the items. + patterns_item_list->clear(); + for (int i = 0; i < tile_set->get_patterns_count(); i++) { + int id = patterns_item_list->add_item(""); + patterns_item_list->set_item_metadata(id, tile_set->get_pattern(i)); + TilesEditor::get_singleton()->queue_pattern_preview(tile_set, tile_set->get_pattern(i), callable_mp(this, &TileMapEditorTilesPlugin::_pattern_preview_done)); + } + + // Update the label visibility. + patterns_help_label->set_visible(patterns_item_list->get_item_count() == 0); + + // Added a new pattern, thus select the last one. + if (select_last_pattern) { + patterns_item_list->select(tile_set->get_patterns_count() - 1); + patterns_item_list->grab_focus(); + _update_selection_pattern_from_tileset_pattern_selection(); + } + select_last_pattern = false; +} + void TileMapEditorTilesPlugin::_update_atlas_view() { TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); if (!tile_map) { @@ -295,7 +371,7 @@ void TileMapEditorTilesPlugin::_update_scenes_collection_view() { Variant udata = i; EditorResourcePreview::get_singleton()->queue_edited_resource_preview(scene, this, "_scene_thumbnail_done", udata); } else { - item_index = scene_tiles_list->add_item(TTR("Tile with Invalid Scene"), get_theme_icon(SNAME("PackedScene"), SNAME("EditorIcons"))); + item_index = scene_tiles_list->add_item(TTR("Tile with Invalid Scene"), tiles_bottom_panel->get_theme_icon(SNAME("PackedScene"), SNAME("EditorIcons"))); } scene_tiles_list->set_item_metadata(item_index, scene_id); @@ -351,19 +427,32 @@ void TileMapEditorTilesPlugin::_scenes_list_multi_selected(int p_index, bool p_s } } - _update_selection_pattern_from_tileset_selection(); + _update_selection_pattern_from_tileset_tiles_selection(); } void TileMapEditorTilesPlugin::_scenes_list_nothing_selected() { scene_tiles_list->deselect_all(); tile_set_selection.clear(); tile_map_selection.clear(); - selection_pattern->clear(); - _update_selection_pattern_from_tileset_selection(); + selection_pattern.instantiate(); + _update_selection_pattern_from_tileset_tiles_selection(); +} + +void TileMapEditorTilesPlugin::_update_theme() { + select_tool_button->set_icon(tiles_bottom_panel->get_theme_icon(SNAME("ToolSelect"), SNAME("EditorIcons"))); + paint_tool_button->set_icon(tiles_bottom_panel->get_theme_icon(SNAME("Edit"), SNAME("EditorIcons"))); + line_tool_button->set_icon(tiles_bottom_panel->get_theme_icon(SNAME("CurveLinear"), SNAME("EditorIcons"))); + rect_tool_button->set_icon(tiles_bottom_panel->get_theme_icon(SNAME("Rectangle"), SNAME("EditorIcons"))); + bucket_tool_button->set_icon(tiles_bottom_panel->get_theme_icon(SNAME("Bucket"), SNAME("EditorIcons"))); + + picker_button->set_icon(tiles_bottom_panel->get_theme_icon(SNAME("ColorPick"), SNAME("EditorIcons"))); + erase_button->set_icon(tiles_bottom_panel->get_theme_icon(SNAME("Eraser"), SNAME("EditorIcons"))); + + missing_atlas_texture_icon = tiles_bottom_panel->get_theme_icon(SNAME("TileSet"), SNAME("EditorIcons")); } bool TileMapEditorTilesPlugin::forward_canvas_gui_input(const Ref<InputEvent> &p_event) { - if (!is_visible_in_tree()) { + if (!(tiles_bottom_panel->is_visible_in_tree() || patterns_bottom_panel->is_visible_in_tree())) { // If the bottom editor is not visible, we ignore inputs. return false; } @@ -391,7 +480,7 @@ bool TileMapEditorTilesPlugin::forward_canvas_gui_input(const Ref<InputEvent> &p if (ED_IS_SHORTCUT("tiles_editor/cut", p_event) || ED_IS_SHORTCUT("tiles_editor/copy", p_event)) { // Fill in the clipboard. if (!tile_map_selection.is_empty()) { - memdelete(tile_map_clipboard); + tile_map_clipboard.instantiate(); TypedArray<Vector2i> coords_array; for (Set<Vector2i>::Element *E = tile_map_selection.front(); E; E = E->next()) { coords_array.push_back(E->get()); @@ -624,7 +713,7 @@ void TileMapEditorTilesPlugin::forward_canvas_draw_over_viewport(Control *p_over Vector2i tile_shape_size = tile_set->get_tile_size(); // Draw the selection. - if (is_visible_in_tree() && tool_buttons_group->get_pressed_button() == select_tool_button) { + if ((tiles_bottom_panel->is_visible_in_tree() || patterns_bottom_panel->is_visible_in_tree()) && tool_buttons_group->get_pressed_button() == select_tool_button) { // In select mode, we only draw the current selection if we are modifying it (pressing control or shift). if (drag_type == DRAG_TYPE_MOVE || (drag_type == DRAG_TYPE_SELECT && !Input::get_singleton()->is_key_pressed(KEY_CTRL) && !Input::get_singleton()->is_key_pressed(KEY_SHIFT))) { // Do nothing @@ -636,7 +725,7 @@ void TileMapEditorTilesPlugin::forward_canvas_draw_over_viewport(Control *p_over } // Handle the preview of the tiles to be placed. - if (is_visible_in_tree() && has_mouse) { // Only if the tilemap editor is opened and the viewport is hovered. + if ((tiles_bottom_panel->is_visible_in_tree() || patterns_bottom_panel->is_visible_in_tree()) && has_mouse) { // Only if the tilemap editor is opened and the viewport is hovered. Map<Vector2i, TileMapCell> preview; Rect2i drawn_grid_rect; @@ -670,21 +759,23 @@ void TileMapEditorTilesPlugin::forward_canvas_draw_over_viewport(Control *p_over } tile_map->draw_cells_outline(p_overlay, to_draw, Color(1.0, 1.0, 1.0), xform); } else if (drag_type == DRAG_TYPE_MOVE) { - // Preview when moving. - Vector2i top_left; - if (!tile_map_selection.is_empty()) { - top_left = tile_map_selection.front()->get(); - } - for (Set<Vector2i>::Element *E = tile_map_selection.front(); E; E = E->next()) { - top_left = top_left.min(E->get()); - } - Vector2i offset = drag_start_mouse_pos - tile_map->map_to_world(top_left); - offset = tile_map->world_to_map(drag_last_mouse_pos - offset) - tile_map->world_to_map(drag_start_mouse_pos - offset); + if (!(patterns_item_list->is_visible_in_tree() && patterns_item_list->has_point(patterns_item_list->get_local_mouse_position()))) { + // Preview when moving. + Vector2i top_left; + if (!tile_map_selection.is_empty()) { + top_left = tile_map_selection.front()->get(); + } + for (Set<Vector2i>::Element *E = tile_map_selection.front(); E; E = E->next()) { + top_left = top_left.min(E->get()); + } + Vector2i offset = drag_start_mouse_pos - tile_map->map_to_world(top_left); + offset = tile_map->world_to_map(drag_last_mouse_pos - offset) - tile_map->world_to_map(drag_start_mouse_pos - offset); - TypedArray<Vector2i> selection_used_cells = selection_pattern->get_used_cells(); - for (int i = 0; i < selection_used_cells.size(); i++) { - Vector2i coords = tile_map->map_pattern(offset + top_left, selection_used_cells[i], selection_pattern); - preview[coords] = TileMapCell(selection_pattern->get_cell_source_id(selection_used_cells[i]), selection_pattern->get_cell_atlas_coords(selection_used_cells[i]), selection_pattern->get_cell_alternative_tile(selection_used_cells[i])); + TypedArray<Vector2i> selection_used_cells = selection_pattern->get_used_cells(); + for (int i = 0; i < selection_used_cells.size(); i++) { + Vector2i coords = tile_map->map_pattern(offset + top_left, selection_used_cells[i], selection_pattern); + preview[coords] = TileMapCell(selection_pattern->get_cell_source_id(selection_used_cells[i]), selection_pattern->get_cell_atlas_coords(selection_used_cells[i]), selection_pattern->get_cell_alternative_tile(selection_used_cells[i])); + } } } else if (drag_type == DRAG_TYPE_CLIPBOARD_PASTE) { // Preview when pasting. @@ -823,7 +914,7 @@ void TileMapEditorTilesPlugin::_mouse_exited_viewport() { CanvasItemEditor::get_singleton()->update_viewport(); } -TileMapCell TileMapEditorTilesPlugin::_pick_random_tile(const TileMapPattern *p_pattern) { +TileMapCell TileMapEditorTilesPlugin::_pick_random_tile(Ref<TileMapPattern> p_pattern) { TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); if (!tile_map) { return TileMapCell(); @@ -887,9 +978,10 @@ Map<Vector2i, TileMapCell> TileMapEditorTilesPlugin::_draw_line(Vector2 p_start_ } // Get or create the pattern. - TileMapPattern erase_pattern; - erase_pattern.set_cell(Vector2i(0, 0), TileSet::INVALID_SOURCE, TileSetSource::INVALID_ATLAS_COORDS, TileSetSource::INVALID_TILE_ALTERNATIVE); - TileMapPattern *pattern = _is_erasing() ? &erase_pattern : selection_pattern; + Ref<TileMapPattern> erase_pattern; + erase_pattern.instantiate(); + erase_pattern->set_cell(Vector2i(0, 0), TileSet::INVALID_SOURCE, TileSetSource::INVALID_ATLAS_COORDS, TileSetSource::INVALID_TILE_ALTERNATIVE); + Ref<TileMapPattern> pattern = _is_erasing() ? erase_pattern : selection_pattern; Map<Vector2i, TileMapCell> output; if (!pattern->is_empty()) { @@ -939,9 +1031,11 @@ Map<Vector2i, TileMapCell> TileMapEditorTilesPlugin::_draw_rect(Vector2i p_start rect.size += Vector2i(1, 1); // Get or create the pattern. - TileMapPattern erase_pattern; - erase_pattern.set_cell(Vector2i(0, 0), TileSet::INVALID_SOURCE, TileSetSource::INVALID_ATLAS_COORDS, TileSetSource::INVALID_TILE_ALTERNATIVE); - TileMapPattern *pattern = _is_erasing() ? &erase_pattern : selection_pattern; + Ref<TileMapPattern> erase_pattern; + erase_pattern.instantiate(); + erase_pattern->set_cell(Vector2i(0, 0), TileSet::INVALID_SOURCE, TileSetSource::INVALID_ATLAS_COORDS, TileSetSource::INVALID_TILE_ALTERNATIVE); + Ref<TileMapPattern> pattern = _is_erasing() ? erase_pattern : selection_pattern; + Map<Vector2i, TileMapCell> err_output; ERR_FAIL_COND_V(pattern->is_empty(), err_output); @@ -998,9 +1092,10 @@ Map<Vector2i, TileMapCell> TileMapEditorTilesPlugin::_draw_bucket_fill(Vector2i } // Get or create the pattern. - TileMapPattern erase_pattern; - erase_pattern.set_cell(Vector2i(0, 0), TileSet::INVALID_SOURCE, TileSetSource::INVALID_ATLAS_COORDS, TileSetSource::INVALID_TILE_ALTERNATIVE); - TileMapPattern *pattern = _is_erasing() ? &erase_pattern : selection_pattern; + Ref<TileMapPattern> erase_pattern; + erase_pattern.instantiate(); + erase_pattern->set_cell(Vector2i(0, 0), TileSet::INVALID_SOURCE, TileSetSource::INVALID_ATLAS_COORDS, TileSetSource::INVALID_TILE_ALTERNATIVE); + Ref<TileMapPattern> pattern = _is_erasing() ? erase_pattern : selection_pattern; if (!pattern->is_empty()) { TileMapCell source = tile_map->get_cell(tile_map_layer, p_coords); @@ -1144,60 +1239,80 @@ void TileMapEditorTilesPlugin::_stop_dragging() { _update_tileset_selection_from_selection_pattern(); } break; case DRAG_TYPE_MOVE: { - Vector2i top_left; - if (!tile_map_selection.is_empty()) { - top_left = tile_map_selection.front()->get(); - } - for (Set<Vector2i>::Element *E = tile_map_selection.front(); E; E = E->next()) { - top_left = top_left.min(E->get()); - } + if (patterns_item_list->is_visible_in_tree() && patterns_item_list->has_point(patterns_item_list->get_local_mouse_position())) { + // Restore the cells. + for (KeyValue<Vector2i, TileMapCell> kv : drag_modified) { + tile_map->set_cell(tile_map_layer, kv.key, kv.value.source_id, kv.value.get_atlas_coords(), kv.value.alternative_tile); + } - Vector2i offset = drag_start_mouse_pos - tile_map->map_to_world(top_left); - offset = tile_map->world_to_map(mpos - offset) - tile_map->world_to_map(drag_start_mouse_pos - offset); + // Creating a pattern in the pattern list. + select_last_pattern = true; + int new_pattern_index = tile_set->get_patterns_count(); + undo_redo->create_action(TTR("Add TileSet pattern")); + undo_redo->add_do_method(*tile_set, "add_pattern", selection_pattern, new_pattern_index); + undo_redo->add_undo_method(*tile_set, "remove_pattern", new_pattern_index); + undo_redo->commit_action(); + } else { + // Get the top-left cell. + Vector2i top_left; + if (!tile_map_selection.is_empty()) { + top_left = tile_map_selection.front()->get(); + } + for (Set<Vector2i>::Element *E = tile_map_selection.front(); E; E = E->next()) { + top_left = top_left.min(E->get()); + } - TypedArray<Vector2i> selection_used_cells = selection_pattern->get_used_cells(); + // Get the offset from the mouse. + Vector2i offset = drag_start_mouse_pos - tile_map->map_to_world(top_left); + offset = tile_map->world_to_map(mpos - offset) - tile_map->world_to_map(drag_start_mouse_pos - offset); - Vector2i coords; - Map<Vector2i, TileMapCell> cells_undo; - for (int i = 0; i < selection_used_cells.size(); i++) { - coords = tile_map->map_pattern(top_left, selection_used_cells[i], selection_pattern); - cells_undo[coords] = TileMapCell(drag_modified[coords].source_id, drag_modified[coords].get_atlas_coords(), drag_modified[coords].alternative_tile); - coords = tile_map->map_pattern(top_left + offset, selection_used_cells[i], selection_pattern); - cells_undo[coords] = TileMapCell(tile_map->get_cell_source_id(tile_map_layer, coords), tile_map->get_cell_atlas_coords(tile_map_layer, coords), tile_map->get_cell_alternative_tile(tile_map_layer, coords)); - } + TypedArray<Vector2i> selection_used_cells = selection_pattern->get_used_cells(); - Map<Vector2i, TileMapCell> cells_do; - for (int i = 0; i < selection_used_cells.size(); i++) { - coords = tile_map->map_pattern(top_left, selection_used_cells[i], selection_pattern); - cells_do[coords] = TileMapCell(); - } - for (int i = 0; i < selection_used_cells.size(); i++) { - coords = tile_map->map_pattern(top_left + offset, selection_used_cells[i], selection_pattern); - cells_do[coords] = TileMapCell(selection_pattern->get_cell_source_id(selection_used_cells[i]), selection_pattern->get_cell_atlas_coords(selection_used_cells[i]), selection_pattern->get_cell_alternative_tile(selection_used_cells[i])); - } - undo_redo->create_action(TTR("Move tiles")); - // Move the tiles. - for (const KeyValue<Vector2i, TileMapCell> &E : cells_do) { - undo_redo->add_do_method(tile_map, "set_cell", tile_map_layer, E.key, E.value.source_id, E.value.get_atlas_coords(), E.value.alternative_tile); - } - for (const KeyValue<Vector2i, TileMapCell> &E : cells_undo) { - undo_redo->add_undo_method(tile_map, "set_cell", tile_map_layer, E.key, E.value.source_id, E.value.get_atlas_coords(), E.value.alternative_tile); - } + // Build the list of cells to undo. + Vector2i coords; + Map<Vector2i, TileMapCell> cells_undo; + for (int i = 0; i < selection_used_cells.size(); i++) { + coords = tile_map->map_pattern(top_left, selection_used_cells[i], selection_pattern); + cells_undo[coords] = TileMapCell(drag_modified[coords].source_id, drag_modified[coords].get_atlas_coords(), drag_modified[coords].alternative_tile); + coords = tile_map->map_pattern(top_left + offset, selection_used_cells[i], selection_pattern); + cells_undo[coords] = TileMapCell(tile_map->get_cell_source_id(tile_map_layer, coords), tile_map->get_cell_atlas_coords(tile_map_layer, coords), tile_map->get_cell_alternative_tile(tile_map_layer, coords)); + } - // Update the selection. - undo_redo->add_undo_method(this, "_set_tile_map_selection", _get_tile_map_selection()); - tile_map_selection.clear(); - for (int i = 0; i < selection_used_cells.size(); i++) { - coords = tile_map->map_pattern(top_left + offset, selection_used_cells[i], selection_pattern); - tile_map_selection.insert(coords); + // Build the list of cells to do. + Map<Vector2i, TileMapCell> cells_do; + for (int i = 0; i < selection_used_cells.size(); i++) { + coords = tile_map->map_pattern(top_left, selection_used_cells[i], selection_pattern); + cells_do[coords] = TileMapCell(); + } + for (int i = 0; i < selection_used_cells.size(); i++) { + coords = tile_map->map_pattern(top_left + offset, selection_used_cells[i], selection_pattern); + cells_do[coords] = TileMapCell(selection_pattern->get_cell_source_id(selection_used_cells[i]), selection_pattern->get_cell_atlas_coords(selection_used_cells[i]), selection_pattern->get_cell_alternative_tile(selection_used_cells[i])); + } + + // Move the tiles. + undo_redo->create_action(TTR("Move tiles")); + for (Map<Vector2i, TileMapCell>::Element *E = cells_do.front(); E; E = E->next()) { + undo_redo->add_do_method(tile_map, "set_cell", tile_map_layer, E->key(), E->get().source_id, E->get().get_atlas_coords(), E->get().alternative_tile); + } + for (Map<Vector2i, TileMapCell>::Element *E = cells_undo.front(); E; E = E->next()) { + undo_redo->add_undo_method(tile_map, "set_cell", tile_map_layer, E->key(), E->get().source_id, E->get().get_atlas_coords(), E->get().alternative_tile); + } + + // Update the selection. + undo_redo->add_undo_method(this, "_set_tile_map_selection", _get_tile_map_selection()); + tile_map_selection.clear(); + for (int i = 0; i < selection_used_cells.size(); i++) { + coords = tile_map->map_pattern(top_left + offset, selection_used_cells[i], selection_pattern); + tile_map_selection.insert(coords); + } + undo_redo->add_do_method(this, "_set_tile_map_selection", _get_tile_map_selection()); + undo_redo->commit_action(); } - undo_redo->add_do_method(this, "_set_tile_map_selection", _get_tile_map_selection()); - undo_redo->commit_action(); } break; case DRAG_TYPE_PICK: { Rect2i rect = Rect2i(tile_map->world_to_map(drag_start_mouse_pos), tile_map->world_to_map(mpos) - tile_map->world_to_map(drag_start_mouse_pos)).abs(); rect.size += Vector2i(1, 1); - memdelete(selection_pattern); + TypedArray<Vector2i> coords_array; for (int x = rect.position.x; x < rect.get_end().x; x++) { for (int y = rect.position.y; y < rect.get_end().y; y++) { @@ -1207,11 +1322,10 @@ void TileMapEditorTilesPlugin::_stop_dragging() { } } } - selection_pattern = tile_map->get_pattern(tile_map_layer, coords_array); - if (!selection_pattern->is_empty()) { + Ref<TileMapPattern> new_selection_pattern = tile_map->get_pattern(tile_map_layer, coords_array); + if (!new_selection_pattern->is_empty()) { + selection_pattern = new_selection_pattern; _update_tileset_selection_from_selection_pattern(); - } else { - _update_selection_pattern_from_tileset_selection(); } picker_button->set_pressed(false); } break; @@ -1279,8 +1393,9 @@ void TileMapEditorTilesPlugin::_update_fix_selected_and_hovered() { hovered_tile.set_atlas_coords(TileSetSource::INVALID_ATLAS_COORDS); hovered_tile.alternative_tile = TileSetSource::INVALID_TILE_ALTERNATIVE; tile_set_selection.clear(); + patterns_item_list->deselect_all(); tile_map_selection.clear(); - selection_pattern->clear(); + selection_pattern.instantiate(); return; } @@ -1290,8 +1405,9 @@ void TileMapEditorTilesPlugin::_update_fix_selected_and_hovered() { hovered_tile.set_atlas_coords(TileSetSource::INVALID_ATLAS_COORDS); hovered_tile.alternative_tile = TileSetSource::INVALID_TILE_ALTERNATIVE; tile_set_selection.clear(); + patterns_item_list->deselect_all(); tile_map_selection.clear(); - selection_pattern->clear(); + selection_pattern.instantiate(); return; } @@ -1301,8 +1417,9 @@ void TileMapEditorTilesPlugin::_update_fix_selected_and_hovered() { hovered_tile.set_atlas_coords(TileSetSource::INVALID_ATLAS_COORDS); hovered_tile.alternative_tile = TileSetSource::INVALID_TILE_ALTERNATIVE; tile_set_selection.clear(); + patterns_item_list->deselect_all(); tile_map_selection.clear(); - selection_pattern->clear(); + selection_pattern.instantiate(); return; } @@ -1330,8 +1447,10 @@ void TileMapEditorTilesPlugin::_update_fix_selected_and_hovered() { if (!tile_map_selection.is_empty()) { _update_selection_pattern_from_tilemap_selection(); + } else if (tiles_bottom_panel->is_visible_in_tree()) { + _update_selection_pattern_from_tileset_tiles_selection(); } else { - _update_selection_pattern_from_tileset_selection(); + _update_selection_pattern_from_tileset_pattern_selection(); } } @@ -1364,9 +1483,14 @@ void TileMapEditorTilesPlugin::_update_selection_pattern_from_tilemap_selection( return; } + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + ERR_FAIL_INDEX(tile_map_layer, tile_map->get_layers_count()); - memdelete(selection_pattern); + selection_pattern.instantiate(); TypedArray<Vector2i> coords_array; for (Set<Vector2i>::Element *E = tile_map_selection.front(); E; E = E->next()) { @@ -1375,7 +1499,7 @@ void TileMapEditorTilesPlugin::_update_selection_pattern_from_tilemap_selection( selection_pattern = tile_map->get_pattern(tile_map_layer, coords_array); } -void TileMapEditorTilesPlugin::_update_selection_pattern_from_tileset_selection() { +void TileMapEditorTilesPlugin::_update_selection_pattern_from_tileset_tiles_selection() { TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); if (!tile_map) { return; @@ -1390,7 +1514,7 @@ void TileMapEditorTilesPlugin::_update_selection_pattern_from_tileset_selection( tile_map_selection.clear(); // Clear the selected pattern. - selection_pattern->clear(); + selection_pattern.instantiate(); // Group per source. Map<int, List<const TileMapCell *>> per_source; @@ -1448,6 +1572,30 @@ void TileMapEditorTilesPlugin::_update_selection_pattern_from_tileset_selection( CanvasItemEditor::get_singleton()->update_viewport(); } +void TileMapEditorTilesPlugin::_update_selection_pattern_from_tileset_pattern_selection() { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + // Clear the tilemap selection. + tile_map_selection.clear(); + + // Clear the selected pattern. + selection_pattern.instantiate(); + + if (patterns_item_list->get_selected_items().size() >= 1) { + selection_pattern = patterns_item_list->get_item_metadata(patterns_item_list->get_selected_items()[0]); + } + + CanvasItemEditor::get_singleton()->update_viewport(); +} + void TileMapEditorTilesPlugin::_update_tileset_selection_from_selection_pattern() { tile_set_selection.clear(); TypedArray<Vector2i> used_cells = selection_pattern->get_used_cells(); @@ -1457,7 +1605,7 @@ void TileMapEditorTilesPlugin::_update_tileset_selection_from_selection_pattern( tile_set_selection.insert(TileMapCell(selection_pattern->get_cell_source_id(coords), selection_pattern->get_cell_atlas_coords(coords), selection_pattern->get_cell_alternative_tile(coords))); } } - _update_bottom_panel(); + _update_source_display(); tile_atlas_control->update(); alternative_tiles_control->update(); } @@ -1607,7 +1755,7 @@ void TileMapEditorTilesPlugin::_tile_atlas_control_gui_input(const Ref<InputEven tile_set_selection.insert(TileMapCell(source_id, hovered_tile.get_atlas_coords(), 0)); } } - _update_selection_pattern_from_tileset_selection(); + _update_selection_pattern_from_tileset_tiles_selection(); } else { // Released if (tile_set_dragging_selection) { if (!mb->is_shift_pressed()) { @@ -1644,7 +1792,7 @@ void TileMapEditorTilesPlugin::_tile_atlas_control_gui_input(const Ref<InputEven } } } - _update_selection_pattern_from_tileset_selection(); + _update_selection_pattern_from_tileset_tiles_selection(); } tile_set_dragging_selection = false; } @@ -1764,7 +1912,7 @@ void TileMapEditorTilesPlugin::_tile_alternatives_control_gui_input(const Ref<In tile_set_selection.insert(TileMapCell(source_id, hovered_tile.get_atlas_coords(), hovered_tile.alternative_tile)); } } - _update_selection_pattern_from_tileset_selection(); + _update_selection_pattern_from_tileset_tiles_selection(); } tile_atlas_control->update(); alternative_tiles_control->update(); @@ -1797,8 +1945,9 @@ void TileMapEditorTilesPlugin::edit(ObjectID p_tile_map_id, int p_tile_map_layer // Clear the selection. tile_set_selection.clear(); + patterns_item_list->deselect_all(); tile_map_selection.clear(); - selection_pattern->clear(); + selection_pattern.instantiate(); } tile_map_layer = p_tile_map_layer; @@ -1820,9 +1969,13 @@ TileMapEditorTilesPlugin::TileMapEditorTilesPlugin() { ED_SHORTCUT("tiles_editor/cancel", TTR("Cancel"), KEY_ESCAPE); ED_SHORTCUT("tiles_editor/delete", TTR("Delete"), KEY_DELETE); + // --- Initialize references --- + tile_map_clipboard.instantiate(); + selection_pattern.instantiate(); + // --- Toolbar --- toolbar = memnew(HBoxContainer); - toolbar->set_h_size_flags(SIZE_EXPAND_FILL); + toolbar->set_h_size_flags(Control::SIZE_EXPAND_FILL); HBoxContainer *tilemap_tiles_tools_buttons = memnew(HBoxContainer); @@ -1933,39 +2086,44 @@ TileMapEditorTilesPlugin::TileMapEditorTilesPlugin() { paint_tool_button->set_pressed(true); _update_toolbar(); - // --- Bottom panel --- - set_name("Tiles"); + // --- Bottom panel tiles --- + tiles_bottom_panel = memnew(VBoxContainer); + tiles_bottom_panel->connect("tree_entered", callable_mp(this, &TileMapEditorTilesPlugin::_update_theme)); + tiles_bottom_panel->connect("theme_changed", callable_mp(this, &TileMapEditorTilesPlugin::_update_theme)); + tiles_bottom_panel->connect("visibility_changed", callable_mp(this, &TileMapEditorTilesPlugin::_stop_dragging)); + tiles_bottom_panel->connect("visibility_changed", callable_mp(this, &TileMapEditorTilesPlugin::_tab_changed)); + tiles_bottom_panel->set_name(TTR("Tiles")); missing_source_label = memnew(Label); missing_source_label->set_text(TTR("This TileMap's TileSet has no source configured. Edit the TileSet resource to add one.")); - missing_source_label->set_h_size_flags(SIZE_EXPAND_FILL); - missing_source_label->set_v_size_flags(SIZE_EXPAND_FILL); + missing_source_label->set_h_size_flags(Control::SIZE_EXPAND_FILL); + missing_source_label->set_v_size_flags(Control::SIZE_EXPAND_FILL); missing_source_label->set_align(Label::ALIGN_CENTER); missing_source_label->set_valign(Label::VALIGN_CENTER); missing_source_label->hide(); - add_child(missing_source_label); + tiles_bottom_panel->add_child(missing_source_label); atlas_sources_split_container = memnew(HSplitContainer); - atlas_sources_split_container->set_h_size_flags(SIZE_EXPAND_FILL); - atlas_sources_split_container->set_v_size_flags(SIZE_EXPAND_FILL); - add_child(atlas_sources_split_container); + atlas_sources_split_container->set_h_size_flags(Control::SIZE_EXPAND_FILL); + atlas_sources_split_container->set_v_size_flags(Control::SIZE_EXPAND_FILL); + tiles_bottom_panel->add_child(atlas_sources_split_container); sources_list = memnew(ItemList); sources_list->set_fixed_icon_size(Size2i(60, 60) * EDSCALE); - sources_list->set_h_size_flags(SIZE_EXPAND_FILL); + sources_list->set_h_size_flags(Control::SIZE_EXPAND_FILL); sources_list->set_stretch_ratio(0.25); sources_list->set_custom_minimum_size(Size2i(70, 0) * EDSCALE); sources_list->set_texture_filter(CanvasItem::TEXTURE_FILTER_NEAREST); sources_list->connect("item_selected", callable_mp(this, &TileMapEditorTilesPlugin::_update_fix_selected_and_hovered).unbind(1)); - sources_list->connect("item_selected", callable_mp(this, &TileMapEditorTilesPlugin::_update_bottom_panel).unbind(1)); + sources_list->connect("item_selected", callable_mp(this, &TileMapEditorTilesPlugin::_update_source_display).unbind(1)); sources_list->connect("item_selected", callable_mp(TilesEditor::get_singleton(), &TilesEditor::set_sources_lists_current)); sources_list->connect("visibility_changed", callable_mp(TilesEditor::get_singleton(), &TilesEditor::synchronize_sources_list), varray(sources_list)); atlas_sources_split_container->add_child(sources_list); // Tile atlas source. tile_atlas_view = memnew(TileAtlasView); - tile_atlas_view->set_h_size_flags(SIZE_EXPAND_FILL); - tile_atlas_view->set_v_size_flags(SIZE_EXPAND_FILL); + tile_atlas_view->set_h_size_flags(Control::SIZE_EXPAND_FILL); + tile_atlas_view->set_v_size_flags(Control::SIZE_EXPAND_FILL); tile_atlas_view->set_texture_grid_visible(false); tile_atlas_view->set_tile_shape_grid_visible(false); tile_atlas_view->connect("transform_changed", callable_mp(TilesEditor::get_singleton(), &TilesEditor::set_atlas_view_transform)); @@ -1985,8 +2143,8 @@ TileMapEditorTilesPlugin::TileMapEditorTilesPlugin() { // Scenes collection source. scene_tiles_list = memnew(ItemList); - scene_tiles_list->set_h_size_flags(SIZE_EXPAND_FILL); - scene_tiles_list->set_v_size_flags(SIZE_EXPAND_FILL); + scene_tiles_list->set_h_size_flags(Control::SIZE_EXPAND_FILL); + scene_tiles_list->set_v_size_flags(Control::SIZE_EXPAND_FILL); scene_tiles_list->set_drag_forwarding(this); scene_tiles_list->set_select_mode(ItemList::SELECT_MULTI); scene_tiles_list->connect("multi_selected", callable_mp(this, &TileMapEditorTilesPlugin::_scenes_list_multi_selected)); @@ -1997,30 +2155,42 @@ TileMapEditorTilesPlugin::TileMapEditorTilesPlugin() { // Invalid source label. invalid_source_label = memnew(Label); invalid_source_label->set_text(TTR("Invalid source selected.")); - invalid_source_label->set_h_size_flags(SIZE_EXPAND_FILL); - invalid_source_label->set_v_size_flags(SIZE_EXPAND_FILL); + invalid_source_label->set_h_size_flags(Control::SIZE_EXPAND_FILL); + invalid_source_label->set_v_size_flags(Control::SIZE_EXPAND_FILL); invalid_source_label->set_align(Label::ALIGN_CENTER); invalid_source_label->set_valign(Label::VALIGN_CENTER); invalid_source_label->hide(); atlas_sources_split_container->add_child(invalid_source_label); - _update_bottom_panel(); + // --- Bottom panel patterns --- + patterns_bottom_panel = memnew(VBoxContainer); + patterns_bottom_panel->set_name(TTR("Patterns")); + patterns_bottom_panel->connect("visibility_changed", callable_mp(this, &TileMapEditorTilesPlugin::_tab_changed)); + + int thumbnail_size = 64; + patterns_item_list = memnew(ItemList); + patterns_item_list->set_max_columns(0); + patterns_item_list->set_icon_mode(ItemList::ICON_MODE_TOP); + patterns_item_list->set_fixed_column_width(thumbnail_size * 3 / 2); + patterns_item_list->set_max_text_lines(2); + patterns_item_list->set_fixed_icon_size(Size2(thumbnail_size, thumbnail_size)); + patterns_item_list->set_v_size_flags(Control::SIZE_EXPAND_FILL); + patterns_item_list->connect("gui_input", callable_mp(this, &TileMapEditorTilesPlugin::_patterns_item_list_gui_input)); + patterns_item_list->connect("item_selected", callable_mp(this, &TileMapEditorTilesPlugin::_update_selection_pattern_from_tileset_pattern_selection).unbind(1)); + patterns_item_list->connect("item_activated", callable_mp(this, &TileMapEditorTilesPlugin::_update_selection_pattern_from_tileset_pattern_selection)); + patterns_item_list->connect("nothing_selected", callable_mp(this, &TileMapEditorTilesPlugin::_update_selection_pattern_from_tileset_pattern_selection)); + patterns_bottom_panel->add_child(patterns_item_list); + + patterns_help_label = memnew(Label); + patterns_help_label->set_text(TTR("Drag and drop or paste a TileMap selection here to store a pattern.")); + patterns_help_label->set_anchors_and_offsets_preset(Control::PRESET_CENTER); + patterns_item_list->add_child(patterns_help_label); + + // Update. + _update_source_display(); } TileMapEditorTilesPlugin::~TileMapEditorTilesPlugin() { - memdelete(selection_pattern); - memdelete(tile_map_clipboard); -} - -void TileMapEditorTerrainsPlugin::_notification(int p_what) { - switch (p_what) { - case NOTIFICATION_ENTER_TREE: - case NOTIFICATION_THEME_CHANGED: - paint_tool_button->set_icon(get_theme_icon(SNAME("Edit"), SNAME("EditorIcons"))); - picker_button->set_icon(get_theme_icon(SNAME("ColorPick"), SNAME("EditorIcons"))); - erase_button->set_icon(get_theme_icon(SNAME("Eraser"), SNAME("EditorIcons"))); - break; - } } void TileMapEditorTerrainsPlugin::tile_set_changed() { @@ -2043,8 +2213,10 @@ void TileMapEditorTerrainsPlugin::_update_toolbar() { } } -Control *TileMapEditorTerrainsPlugin::get_toolbar() const { - return toolbar; +Vector<TileMapEditorPlugin::TabData> TileMapEditorTerrainsPlugin::get_tabs() const { + Vector<TileMapEditorPlugin::TabData> tabs; + tabs.push_back({ toolbar, main_vbox_container }); + return tabs; } Map<Vector2i, TileSet::CellNeighbor> TileMapEditorTerrainsPlugin::Constraint::get_overlapping_coords_and_peering_bits() const { @@ -2802,7 +2974,7 @@ void TileMapEditorTerrainsPlugin::_stop_dragging() { } bool TileMapEditorTerrainsPlugin::forward_canvas_gui_input(const Ref<InputEvent> &p_event) { - if (!is_visible_in_tree()) { + if (!main_vbox_container->is_visible_in_tree()) { // If the bottom editor is not visible, we ignore inputs. return false; } @@ -3074,13 +3246,13 @@ void TileMapEditorTerrainsPlugin::_update_terrains_tree() { TreeItem *terrain_set_tree_item = terrains_tree->create_item(); String matches; if (tile_set->get_terrain_set_mode(terrain_set_index) == TileSet::TERRAIN_MODE_MATCH_CORNERS_AND_SIDES) { - terrain_set_tree_item->set_icon(0, get_theme_icon(SNAME("TerrainMatchCornersAndSides"), SNAME("EditorIcons"))); + terrain_set_tree_item->set_icon(0, main_vbox_container->get_theme_icon(SNAME("TerrainMatchCornersAndSides"), SNAME("EditorIcons"))); matches = String(TTR("Matches Corners and Sides")); } else if (tile_set->get_terrain_set_mode(terrain_set_index) == TileSet::TERRAIN_MODE_MATCH_CORNERS) { - terrain_set_tree_item->set_icon(0, get_theme_icon(SNAME("TerrainMatchCorners"), SNAME("EditorIcons"))); + terrain_set_tree_item->set_icon(0, main_vbox_container->get_theme_icon(SNAME("TerrainMatchCorners"), SNAME("EditorIcons"))); matches = String(TTR("Matches Corners Only")); } else { - terrain_set_tree_item->set_icon(0, get_theme_icon(SNAME("TerrainMatchSides"), SNAME("EditorIcons"))); + terrain_set_tree_item->set_icon(0, main_vbox_container->get_theme_icon(SNAME("TerrainMatchSides"), SNAME("EditorIcons"))); matches = String(TTR("Matches Sides Only")); } terrain_set_tree_item->set_text(0, vformat("Terrain Set %d (%s)", terrain_set_index, matches)); @@ -3185,6 +3357,12 @@ void TileMapEditorTerrainsPlugin::_update_tiles_list() { } } +void TileMapEditorTerrainsPlugin::_update_theme() { + paint_tool_button->set_icon(main_vbox_container->get_theme_icon(SNAME("Edit"), SNAME("EditorIcons"))); + picker_button->set_icon(main_vbox_container->get_theme_icon(SNAME("ColorPick"), SNAME("EditorIcons"))); + erase_button->set_icon(main_vbox_container->get_theme_icon(SNAME("Eraser"), SNAME("EditorIcons"))); +} + void TileMapEditorTerrainsPlugin::edit(ObjectID p_tile_map_id, int p_tile_map_layer) { _stop_dragging(); // Avoids staying in a wrong drag state. @@ -3197,15 +3375,18 @@ void TileMapEditorTerrainsPlugin::edit(ObjectID p_tile_map_id, int p_tile_map_la } TileMapEditorTerrainsPlugin::TileMapEditorTerrainsPlugin() { - set_name("Terrains"); + main_vbox_container = memnew(VBoxContainer); + main_vbox_container->connect("tree_entered", callable_mp(this, &TileMapEditorTerrainsPlugin::_update_theme)); + main_vbox_container->connect("theme_changed", callable_mp(this, &TileMapEditorTerrainsPlugin::_update_theme)); + main_vbox_container->set_name("Terrains"); HSplitContainer *tilemap_tab_terrains = memnew(HSplitContainer); - tilemap_tab_terrains->set_h_size_flags(SIZE_EXPAND_FILL); - tilemap_tab_terrains->set_v_size_flags(SIZE_EXPAND_FILL); - add_child(tilemap_tab_terrains); + tilemap_tab_terrains->set_h_size_flags(Control::SIZE_EXPAND_FILL); + tilemap_tab_terrains->set_v_size_flags(Control::SIZE_EXPAND_FILL); + main_vbox_container->add_child(tilemap_tab_terrains); terrains_tree = memnew(Tree); - terrains_tree->set_h_size_flags(SIZE_EXPAND_FILL); + terrains_tree->set_h_size_flags(Control::SIZE_EXPAND_FILL); terrains_tree->set_stretch_ratio(0.25); terrains_tree->set_custom_minimum_size(Size2i(70, 0) * EDSCALE); terrains_tree->set_texture_filter(CanvasItem::TEXTURE_FILTER_NEAREST); @@ -3214,7 +3395,7 @@ TileMapEditorTerrainsPlugin::TileMapEditorTerrainsPlugin() { tilemap_tab_terrains->add_child(terrains_tree); terrains_tile_list = memnew(ItemList); - terrains_tile_list->set_h_size_flags(SIZE_EXPAND_FILL); + terrains_tile_list->set_h_size_flags(Control::SIZE_EXPAND_FILL); terrains_tile_list->set_max_columns(0); terrains_tile_list->set_same_column_width(true); terrains_tile_list->set_fixed_icon_size(Size2(30, 30) * EDSCALE); @@ -3281,7 +3462,7 @@ void TileMapEditor::_notification(int p_what) { if (is_visible_in_tree() && tileset_changed_needs_update) { _update_bottom_panel(); _update_layers_selection(); - tile_map_editor_plugins[tabs->get_current_tab()]->tile_set_changed(); + tabs_plugins[tabs_bar->get_current_tab()]->tile_set_changed(); CanvasItemEditor::get_singleton()->update_viewport(); tileset_changed_needs_update = false; } @@ -3405,14 +3586,11 @@ void TileMapEditor::_update_bottom_panel() { // Update the visibility of controls. missing_tileset_label->set_visible(!tile_set.is_valid()); - if (!tile_set.is_valid()) { - for (int i = 0; i < tile_map_editor_plugins.size(); i++) { - tile_map_editor_plugins[i]->hide(); - } - } else { - for (int i = 0; i < tile_map_editor_plugins.size(); i++) { - tile_map_editor_plugins[i]->set_visible(i == tabs->get_current_tab()); - } + for (unsigned int tab_index = 0; tab_index < tabs_data.size(); tab_index++) { + tabs_data[tab_index].panel->hide(); + } + if (tile_set.is_valid()) { + tabs_data[tabs_bar->get_current_tab()].panel->show(); } } @@ -3495,27 +3673,25 @@ void TileMapEditor::_tile_map_changed() { void TileMapEditor::_tab_changed(int p_tab_id) { // Make the plugin edit the correct tilemap. - tile_map_editor_plugins[tabs->get_current_tab()]->edit(tile_map_id, tile_map_layer); + tabs_plugins[tabs_bar->get_current_tab()]->edit(tile_map_id, tile_map_layer); // Update toolbar. - for (int i = 0; i < tile_map_editor_plugins.size(); i++) { - tile_map_editor_plugins[i]->get_toolbar()->set_visible(i == p_tab_id); + for (unsigned int tab_index = 0; tab_index < tabs_data.size(); tab_index++) { + tabs_data[tab_index].toolbar->hide(); } + tabs_data[p_tab_id].toolbar->show(); // Update visible panel. TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); - if (!tile_map || !tile_map->get_tileset().is_valid()) { - for (int i = 0; i < tile_map_editor_plugins.size(); i++) { - tile_map_editor_plugins[i]->hide(); - } - } else { - for (int i = 0; i < tile_map_editor_plugins.size(); i++) { - tile_map_editor_plugins[i]->set_visible(i == tabs->get_current_tab()); - } + for (unsigned int tab_index = 0; tab_index < tabs_data.size(); tab_index++) { + tabs_data[tab_index].panel->hide(); + } + if (tile_map && tile_map->get_tileset().is_valid()) { + tabs_data[tabs_bar->get_current_tab()].panel->show(); } // Graphical update. - tile_map_editor_plugins[tabs->get_current_tab()]->update(); + tabs_data[tabs_bar->get_current_tab()].panel->update(); CanvasItemEditor::get_singleton()->update_viewport(); } @@ -3602,7 +3778,7 @@ void TileMapEditor::_update_layers_selection() { layers_selection_button->set_custom_minimum_size(min_button_size); layers_selection_button->update(); - tile_map_editor_plugins[tabs->get_current_tab()]->edit(tile_map_id, tile_map_layer); + tabs_plugins[tabs_bar->get_current_tab()]->edit(tile_map_id, tile_map_layer); } void TileMapEditor::_move_tile_map_array_element(Object *p_undo_redo, Object *p_edited, String p_array_prefix, int p_from_index, int p_to_pos) { @@ -3688,7 +3864,7 @@ bool TileMapEditor::forward_canvas_gui_input(const Ref<InputEvent> &p_event) { return true; } - return tile_map_editor_plugins[tabs->get_current_tab()]->forward_canvas_gui_input(p_event); + return tabs_plugins[tabs_bar->get_current_tab()]->forward_canvas_gui_input(p_event); } void TileMapEditor::forward_canvas_draw_over_viewport(Control *p_overlay) { @@ -3823,7 +3999,7 @@ void TileMapEditor::forward_canvas_draw_over_viewport(Control *p_overlay) { }*/ // Draw the plugins. - tile_map_editor_plugins[tabs->get_current_tab()]->forward_canvas_draw_over_viewport(p_overlay); + tabs_plugins[tabs_bar->get_current_tab()]->forward_canvas_draw_over_viewport(p_overlay); } void TileMapEditor::edit(TileMap *p_tile_map) { @@ -3857,7 +4033,7 @@ void TileMapEditor::edit(TileMap *p_tile_map) { _update_layers_selection(); // Call the plugins. - tile_map_editor_plugins[tabs->get_current_tab()]->edit(tile_map_id, tile_map_layer); + tabs_plugins[tabs_bar->get_current_tab()]->edit(tile_map_id, tile_map_layer); _tile_map_changed(); } @@ -3874,24 +4050,31 @@ TileMapEditor::TileMapEditor() { tile_map_editor_plugins.push_back(memnew(TileMapEditorTerrainsPlugin)); // Tabs. - tabs = memnew(Tabs); - tabs->set_clip_tabs(false); - for (int i = 0; i < tile_map_editor_plugins.size(); i++) { - tabs->add_tab(tile_map_editor_plugins[i]->get_name()); + tabs_bar = memnew(Tabs); + tabs_bar->set_clip_tabs(false); + for (int plugin_index = 0; plugin_index < tile_map_editor_plugins.size(); plugin_index++) { + Vector<TileMapEditorPlugin::TabData> tabs_vector = tile_map_editor_plugins[plugin_index]->get_tabs(); + for (int tab_index = 0; tab_index < tabs_vector.size(); tab_index++) { + tabs_bar->add_tab(tabs_vector[tab_index].panel->get_name()); + tabs_data.push_back(tabs_vector[tab_index]); + tabs_plugins.push_back(tile_map_editor_plugins[plugin_index]); + } } - tabs->connect("tab_changed", callable_mp(this, &TileMapEditor::_tab_changed)); + tabs_bar->connect("tab_changed", callable_mp(this, &TileMapEditor::_tab_changed)); // --- TileMap toolbar --- tile_map_toolbar = memnew(HBoxContainer); tile_map_toolbar->set_h_size_flags(SIZE_EXPAND_FILL); // Tabs. - tile_map_toolbar->add_child(tabs); + tile_map_toolbar->add_child(tabs_bar); // Tabs toolbars. - for (int i = 0; i < tile_map_editor_plugins.size(); i++) { - tile_map_editor_plugins[i]->get_toolbar()->hide(); - tile_map_toolbar->add_child(tile_map_editor_plugins[i]->get_toolbar()); + for (unsigned int tab_index = 0; tab_index < tabs_data.size(); tab_index++) { + tabs_data[tab_index].toolbar->hide(); + if (!tabs_data[tab_index].toolbar->get_parent()) { + tile_map_toolbar->add_child(tabs_data[tab_index].toolbar); + } } // Wide empty separation control. @@ -3947,11 +4130,11 @@ TileMapEditor::TileMapEditor() { missing_tileset_label->hide(); add_child(missing_tileset_label); - for (int i = 0; i < tile_map_editor_plugins.size(); i++) { - add_child(tile_map_editor_plugins[i]); - tile_map_editor_plugins[i]->set_h_size_flags(SIZE_EXPAND_FILL); - tile_map_editor_plugins[i]->set_v_size_flags(SIZE_EXPAND_FILL); - tile_map_editor_plugins[i]->set_visible(i == 0); + for (unsigned int tab_index = 0; tab_index < tabs_data.size(); tab_index++) { + add_child(tabs_data[tab_index].panel); + tabs_data[tab_index].panel->set_v_size_flags(SIZE_EXPAND_FILL); + tabs_data[tab_index].panel->set_visible(tab_index == 0); + tabs_data[tab_index].panel->set_h_size_flags(SIZE_EXPAND_FILL); } _tab_changed(0); @@ -3961,4 +4144,7 @@ TileMapEditor::TileMapEditor() { } TileMapEditor::~TileMapEditor() { + for (int i = 0; i < tile_map_editor_plugins.size(); i++) { + memdelete(tile_map_editor_plugins[i]); + } } diff --git a/editor/plugins/tiles/tile_map_editor.h b/editor/plugins/tiles/tile_map_editor.h index 5fbd9cada8..54bac160a3 100644 --- a/editor/plugins/tiles/tile_map_editor.h +++ b/editor/plugins/tiles/tile_map_editor.h @@ -33,17 +33,24 @@ #include "tile_atlas_view.h" +#include "core/os/thread.h" #include "core/typedefs.h" #include "editor/editor_node.h" #include "scene/2d/tile_map.h" #include "scene/gui/box_container.h" #include "scene/gui/tabs.h" -class TileMapEditorPlugin : public VBoxContainer { +class TileMapEditorPlugin : public Object { public: - virtual Control *get_toolbar() const { - return memnew(Control); + struct TabData { + Control *toolbar; + Control *panel; }; + + virtual Vector<TabData> get_tabs() const { + return Vector<TabData>(); + }; + virtual bool forward_canvas_gui_input(const Ref<InputEvent> &p_event) { return false; }; virtual void forward_canvas_draw_over_viewport(Control *p_overlay){}; virtual void tile_set_changed(){}; @@ -106,7 +113,7 @@ private: Map<Vector2i, TileMapCell> drag_modified; bool rmb_erasing = false; - TileMapCell _pick_random_tile(const TileMapPattern *p_pattern); + TileMapCell _pick_random_tile(Ref<TileMapPattern> p_pattern); Map<Vector2i, TileMapCell> _draw_line(Vector2 p_start_drag_mouse_pos, Vector2 p_from_mouse_pos, Vector2 p_to_mouse_pos); Map<Vector2i, TileMapCell> _draw_rect(Vector2i p_start_cell, Vector2i p_end_cell); Map<Vector2i, TileMapCell> _draw_bucket_fill(Vector2i p_coords, bool p_contiguous); @@ -115,20 +122,25 @@ private: ///// Selection system. ///// Set<Vector2i> tile_map_selection; - TileMapPattern *tile_map_clipboard = memnew(TileMapPattern); - TileMapPattern *selection_pattern = memnew(TileMapPattern); + Ref<TileMapPattern> tile_map_clipboard; + Ref<TileMapPattern> selection_pattern; void _set_tile_map_selection(const TypedArray<Vector2i> &p_selection); TypedArray<Vector2i> _get_tile_map_selection() const; Set<TileMapCell> tile_set_selection; void _update_selection_pattern_from_tilemap_selection(); - void _update_selection_pattern_from_tileset_selection(); + void _update_selection_pattern_from_tileset_tiles_selection(); + void _update_selection_pattern_from_tileset_pattern_selection(); void _update_tileset_selection_from_selection_pattern(); void _update_fix_selected_and_hovered(); void _fix_invalid_tiles_in_tile_map_selection(); - ///// Bottom panel. ////. + ///// Bottom panel common //// + void _tab_changed(); + + ///// Bottom panel tiles //// + VBoxContainer *tiles_bottom_panel; Label *missing_source_label; Label *invalid_source_label; @@ -137,7 +149,7 @@ private: Ref<Texture2D> missing_atlas_texture_icon; void _update_tile_set_sources_list(); - void _update_bottom_panel(); + void _update_source_display(); // Atlas sources. TileMapCell hovered_tile; @@ -167,15 +179,26 @@ private: void _scenes_list_multi_selected(int p_index, bool p_selected); void _scenes_list_nothing_selected(); + ///// Bottom panel patterns //// + VBoxContainer *patterns_bottom_panel; + ItemList *patterns_item_list; + Label *patterns_help_label; + void _patterns_item_list_gui_input(const Ref<InputEvent> &p_event); + void _pattern_preview_done(Ref<TileMapPattern> p_pattern, Ref<Texture2D> p_texture); + bool select_last_pattern = false; + void _update_patterns_list(); + + // General + void _update_theme(); + // Update callback virtual void tile_set_changed() override; protected: - void _notification(int p_what); static void _bind_methods(); public: - virtual Control *get_toolbar() const override; + virtual Vector<TabData> get_tabs() const override; virtual bool forward_canvas_gui_input(const Ref<InputEvent> &p_event) override; virtual void forward_canvas_draw_over_viewport(Control *p_overlay) override; @@ -205,6 +228,9 @@ private: void _update_toolbar(); + // Main vbox. + VBoxContainer *main_vbox_container; + // TileMap editing. enum DragType { DRAG_TYPE_NONE = 0, @@ -281,16 +307,13 @@ private: void _update_terrains_cache(); void _update_terrains_tree(); void _update_tiles_list(); + void _update_theme(); // Update callback virtual void tile_set_changed() override; -protected: - void _notification(int p_what); - // static void _bind_methods(); - public: - virtual Control *get_toolbar() const override; + virtual Vector<TabData> get_tabs() const override; virtual bool forward_canvas_gui_input(const Ref<InputEvent> &p_event) override; //virtual void forward_canvas_draw_over_viewport(Control *p_overlay) override; @@ -328,7 +351,9 @@ private: // Bottom panel. Label *missing_tileset_label; - Tabs *tabs; + Tabs *tabs_bar; + LocalVector<TileMapEditorPlugin::TabData> tabs_data; + LocalVector<TileMapEditorPlugin *> tabs_plugins; void _update_bottom_panel(); // TileMap. diff --git a/editor/plugins/tiles/tile_set_atlas_source_editor.cpp b/editor/plugins/tiles/tile_set_atlas_source_editor.cpp index c43a854d9a..157d28da4f 100644 --- a/editor/plugins/tiles/tile_set_atlas_source_editor.cpp +++ b/editor/plugins/tiles/tile_set_atlas_source_editor.cpp @@ -66,10 +66,15 @@ int TileSetAtlasSourceEditor::TileSetAtlasSourceProxyObject::get_id() { } bool TileSetAtlasSourceEditor::TileSetAtlasSourceProxyObject::_set(const StringName &p_name, const Variant &p_value) { + String name = p_name; + if (name == "name") { + // Use the resource_name property to store the source's name. + name = "resource_name"; + } bool valid = false; - tile_set_atlas_source->set(p_name, p_value, &valid); + tile_set_atlas_source->set(name, p_value, &valid); if (valid) { - emit_signal(SNAME("changed"), String(p_name).utf8().get_data()); + emit_signal(SNAME("changed"), String(name).utf8().get_data()); } return valid; } @@ -78,12 +83,18 @@ bool TileSetAtlasSourceEditor::TileSetAtlasSourceProxyObject::_get(const StringN if (!tile_set_atlas_source) { return false; } + String name = p_name; + if (name == "name") { + // Use the resource_name property to store the source's name. + name = "resource_name"; + } bool valid = false; - r_ret = tile_set_atlas_source->get(p_name, &valid); + r_ret = tile_set_atlas_source->get(name, &valid); return valid; } void TileSetAtlasSourceEditor::TileSetAtlasSourceProxyObject::_get_property_list(List<PropertyInfo> *p_list) const { + p_list->push_back(PropertyInfo(Variant::STRING, "name", PROPERTY_HINT_NONE, "")); p_list->push_back(PropertyInfo(Variant::OBJECT, "texture", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D")); p_list->push_back(PropertyInfo(Variant::VECTOR2I, "margins", PROPERTY_HINT_NONE, "")); p_list->push_back(PropertyInfo(Variant::VECTOR2I, "separation", PROPERTY_HINT_NONE, "")); @@ -106,6 +117,10 @@ void TileSetAtlasSourceEditor::TileSetAtlasSourceProxyObject::edit(Ref<TileSet> ERR_FAIL_COND(p_source_id < 0); ERR_FAIL_COND(p_tile_set->get_source(p_source_id) != p_tile_set_atlas_source); + if (p_tile_set == tile_set && p_tile_set_atlas_source == tile_set_atlas_source && p_source_id == source_id) { + return; + } + // Disconnect to changes. if (tile_set_atlas_source) { tile_set_atlas_source->disconnect(CoreStringNames::get_singleton()->property_list_changed, callable_mp((Object *)this, &Object::notify_property_list_changed)); @@ -157,7 +172,7 @@ bool TileSetAtlasSourceEditor::AtlasTileProxyObject::_set(const StringName &p_na } else if (p_name == "size_in_atlas") { Vector2i as_vector2i = Vector2i(p_value); bool has_room_for_tile = tile_set_atlas_source->has_room_for_tile(coords, as_vector2i, tile_set_atlas_source->get_tile_animation_columns(coords), tile_set_atlas_source->get_tile_animation_separation(coords), tile_set_atlas_source->get_tile_animation_frames_count(coords), coords); - ERR_FAIL_COND_V(!has_room_for_tile, false); + ERR_FAIL_COND_V_EDMSG(!has_room_for_tile, false, "Invalid size or not enough room in the atlas for the tile."); tile_set_atlas_source->move_tile_in_atlas(coords, TileSetSource::INVALID_ATLAS_COORDS, as_vector2i); emit_signal(SNAME("changed"), "size_in_atlas"); return true; diff --git a/editor/plugins/tiles/tile_set_editor.cpp b/editor/plugins/tiles/tile_set_editor.cpp index 48d0d9b333..44f5ba29de 100644 --- a/editor/plugins/tiles/tile_set_editor.cpp +++ b/editor/plugins/tiles/tile_set_editor.cpp @@ -145,14 +145,21 @@ void TileSetEditor::_update_sources_list(int force_selected_id) { Ref<Texture2D> texture; String item_text; + // Common to all type of sources. + if (!source->get_name().is_empty()) { + item_text = vformat(TTR("%s (id:%d)"), source->get_name(), source_id); + } + // Atlas source. TileSetAtlasSource *atlas_source = Object::cast_to<TileSetAtlasSource>(source); if (atlas_source) { texture = atlas_source->get_texture(); - if (texture.is_valid()) { - item_text = vformat("%s (id:%d)", texture->get_path().get_file(), source_id); - } else { - item_text = vformat(TTR("No Texture Atlas Source (id:%d)"), source_id); + if (item_text.is_empty()) { + if (texture.is_valid()) { + item_text = vformat("%s (ID:%d)", texture->get_path().get_file(), source_id); + } else { + item_text = vformat(TTR("No Texture Atlas Source (ID:%d)"), source_id); + } } } @@ -160,12 +167,14 @@ void TileSetEditor::_update_sources_list(int force_selected_id) { TileSetScenesCollectionSource *scene_collection_source = Object::cast_to<TileSetScenesCollectionSource>(source); if (scene_collection_source) { texture = get_theme_icon(SNAME("PackedScene"), SNAME("EditorIcons")); - item_text = vformat(TTR("Scene Collection Source (id:%d)"), source_id); + if (item_text.is_empty()) { + item_text = vformat(TTR("Scene Collection Source (ID:%d)"), source_id); + } } // Use default if not valid. if (item_text.is_empty()) { - item_text = vformat(TTR("Unknown Type Source (id:%d)"), source_id); + item_text = vformat(TTR("Unknown Type Source (ID:%d)"), source_id); } if (!texture.is_valid()) { texture = missing_texture_texture; @@ -318,6 +327,7 @@ void TileSetEditor::_notification(int p_what) { tile_set->set_edited(true); } _update_sources_list(); + _update_patterns_list(); tile_set_changed_needs_update = false; } break; @@ -326,10 +336,56 @@ void TileSetEditor::_notification(int p_what) { } } +void TileSetEditor::_patterns_item_list_gui_input(const Ref<InputEvent> &p_event) { + ERR_FAIL_COND(!tile_set.is_valid()); + + if (ED_IS_SHORTCUT("tiles_editor/delete", p_event) && p_event->is_pressed() && !p_event->is_echo()) { + Vector<int> selected = patterns_item_list->get_selected_items(); + undo_redo->create_action(TTR("Remove TileSet patterns")); + for (int i = 0; i < selected.size(); i++) { + int pattern_index = selected[i]; + undo_redo->add_do_method(*tile_set, "remove_pattern", pattern_index); + undo_redo->add_undo_method(*tile_set, "add_pattern", tile_set->get_pattern(pattern_index), pattern_index); + } + undo_redo->commit_action(); + patterns_item_list->accept_event(); + } +} + +void TileSetEditor::_pattern_preview_done(Ref<TileMapPattern> p_pattern, Ref<Texture2D> p_texture) { + // TODO optimize ? + for (int i = 0; i < patterns_item_list->get_item_count(); i++) { + if (patterns_item_list->get_item_metadata(i) == p_pattern) { + patterns_item_list->set_item_icon(i, p_texture); + break; + } + } +} + +void TileSetEditor::_update_patterns_list() { + ERR_FAIL_COND(!tile_set.is_valid()); + + // Recreate the items. + patterns_item_list->clear(); + for (int i = 0; i < tile_set->get_patterns_count(); i++) { + int id = patterns_item_list->add_item(""); + patterns_item_list->set_item_metadata(id, tile_set->get_pattern(i)); + TilesEditor::get_singleton()->queue_pattern_preview(tile_set, tile_set->get_pattern(i), callable_mp(this, &TileSetEditor::_pattern_preview_done)); + } + + // Update the label visibility. + patterns_help_label->set_visible(patterns_item_list->get_item_count() == 0); +} + void TileSetEditor::_tile_set_changed() { tile_set_changed_needs_update = true; } +void TileSetEditor::_tab_changed(int p_tab_changed) { + split_container->set_visible(p_tab_changed == 0); + patterns_item_list->set_visible(p_tab_changed == 1); +} + void TileSetEditor::_move_tile_set_array_element(Object *p_undo_redo, Object *p_edited, String p_array_prefix, int p_from_index, int p_to_pos) { UndoRedo *undo_redo = Object::cast_to<UndoRedo>(p_undo_redo); ERR_FAIL_COND(!undo_redo); @@ -573,6 +629,7 @@ void TileSetEditor::edit(Ref<TileSet> p_tile_set) { if (tile_set.is_valid()) { tile_set->connect("changed", callable_mp(this, &TileSetEditor::_tile_set_changed)); _update_sources_list(); + _update_patterns_list(); } tile_set_atlas_source_editor->hide(); @@ -585,8 +642,20 @@ TileSetEditor::TileSetEditor() { set_process_internal(true); + // Tabs. + tabs_bar = memnew(Tabs); + tabs_bar->set_clip_tabs(false); + tabs_bar->add_tab(TTR("Tiles")); + tabs_bar->add_tab(TTR("Patterns")); + tabs_bar->connect("tab_changed", callable_mp(this, &TileSetEditor::_tab_changed)); + + tile_set_toolbar = memnew(HBoxContainer); + tile_set_toolbar->set_h_size_flags(SIZE_EXPAND_FILL); + tile_set_toolbar->add_child(tabs_bar); + + //// Tiles //// // Split container. - HSplitContainer *split_container = memnew(HSplitContainer); + split_container = memnew(HSplitContainer); split_container->set_name(TTR("Tiles")); split_container->set_h_size_flags(SIZE_EXPAND_FILL); split_container->set_v_size_flags(SIZE_EXPAND_FILL); @@ -672,6 +741,24 @@ TileSetEditor::TileSetEditor() { split_container_right_side->add_child(tile_set_scenes_collection_source_editor); tile_set_scenes_collection_source_editor->hide(); + //// Patterns //// + int thumbnail_size = 64; + patterns_item_list = memnew(ItemList); + patterns_item_list->set_max_columns(0); + patterns_item_list->set_icon_mode(ItemList::ICON_MODE_TOP); + patterns_item_list->set_fixed_column_width(thumbnail_size * 3 / 2); + patterns_item_list->set_max_text_lines(2); + patterns_item_list->set_fixed_icon_size(Size2(thumbnail_size, thumbnail_size)); + patterns_item_list->set_v_size_flags(Control::SIZE_EXPAND_FILL); + patterns_item_list->connect("gui_input", callable_mp(this, &TileSetEditor::_patterns_item_list_gui_input)); + add_child(patterns_item_list); + patterns_item_list->hide(); + + patterns_help_label = memnew(Label); + patterns_help_label->set_text(TTR("Add new patterns in the TileMap editing mode.")); + patterns_help_label->set_anchors_and_offsets_preset(Control::PRESET_CENTER); + patterns_item_list->add_child(patterns_help_label); + // Registers UndoRedo inspector callback. EditorNode::get_singleton()->get_editor_data().add_move_array_element_function(SNAME("TileSet"), callable_mp(this, &TileSetEditor::_move_tile_set_array_element)); EditorNode::get_singleton()->get_editor_data().add_undo_redo_inspector_hook_callback(callable_mp(this, &TileSetEditor::_undo_redo_inspector_callback)); diff --git a/editor/plugins/tiles/tile_set_editor.h b/editor/plugins/tiles/tile_set_editor.h index fe854b2281..1f26560588 100644 --- a/editor/plugins/tiles/tile_set_editor.h +++ b/editor/plugins/tiles/tile_set_editor.h @@ -46,7 +46,13 @@ class TileSetEditor : public VBoxContainer { private: Ref<TileSet> tile_set; bool tile_set_changed_needs_update = false; + HSplitContainer *split_container; + // Tabs. + HBoxContainer *tile_set_toolbar; + Tabs *tabs_bar; + + // Tiles. Label *no_source_selected_label; TileSetAtlasSourceEditor *tile_set_atlas_source_editor; TileSetScenesCollectionSourceEditor *tile_set_scenes_collection_source_editor; @@ -69,7 +75,16 @@ private: AtlasMergingDialog *atlas_merging_dialog; TileProxiesManagerDialog *tile_proxies_manager_dialog; + // Patterns. + ItemList *patterns_item_list; + Label *patterns_help_label; + void _patterns_item_list_gui_input(const Ref<InputEvent> &p_event); + void _pattern_preview_done(Ref<TileMapPattern> p_pattern, Ref<Texture2D> p_texture); + bool select_last_pattern = false; + void _update_patterns_list(); + void _tile_set_changed(); + void _tab_changed(int p_tab_changed); void _move_tile_set_array_element(Object *p_undo_redo, Object *p_edited, String p_array_prefix, int p_from_index, int p_to_pos); void _undo_redo_inspector_callback(Object *p_undo_redo, Object *p_edited, String p_property, Variant p_new_value); @@ -82,6 +97,8 @@ public: _FORCE_INLINE_ static TileSetEditor *get_singleton() { return singleton; } void edit(Ref<TileSet> p_tile_set); + Control *get_toolbar() { return tile_set_toolbar; }; + void drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from); bool can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const; diff --git a/editor/plugins/tiles/tile_set_scenes_collection_source_editor.cpp b/editor/plugins/tiles/tile_set_scenes_collection_source_editor.cpp index f74b3bf9c2..dc26d380b8 100644 --- a/editor/plugins/tiles/tile_set_scenes_collection_source_editor.cpp +++ b/editor/plugins/tiles/tile_set_scenes_collection_source_editor.cpp @@ -56,10 +56,15 @@ int TileSetScenesCollectionSourceEditor::TileSetScenesCollectionProxyObject::get } bool TileSetScenesCollectionSourceEditor::TileSetScenesCollectionProxyObject::_set(const StringName &p_name, const Variant &p_value) { + String name = p_name; + if (name == "name") { + // Use the resource_name property to store the source's name. + name = "resource_name"; + } bool valid = false; - tile_set_scenes_collection_source->set(p_name, p_value, &valid); + tile_set_scenes_collection_source->set(name, p_value, &valid); if (valid) { - emit_signal(SNAME("changed"), String(p_name).utf8().get_data()); + emit_signal(SNAME("changed"), String(name).utf8().get_data()); } return valid; } @@ -68,11 +73,20 @@ bool TileSetScenesCollectionSourceEditor::TileSetScenesCollectionProxyObject::_g if (!tile_set_scenes_collection_source) { return false; } + String name = p_name; + if (name == "name") { + // Use the resource_name property to store the source's name. + name = "resource_name"; + } bool valid = false; - r_ret = tile_set_scenes_collection_source->get(p_name, &valid); + r_ret = tile_set_scenes_collection_source->get(name, &valid); return valid; } +void TileSetScenesCollectionSourceEditor::TileSetScenesCollectionProxyObject::_get_property_list(List<PropertyInfo> *p_list) const { + p_list->push_back(PropertyInfo(Variant::STRING, "name", PROPERTY_HINT_NONE, "")); +} + void TileSetScenesCollectionSourceEditor::TileSetScenesCollectionProxyObject::_bind_methods() { // -- Shape and layout -- ClassDB::bind_method(D_METHOD("set_id", "id"), &TileSetScenesCollectionSourceEditor::TileSetScenesCollectionProxyObject::set_id); @@ -89,6 +103,10 @@ void TileSetScenesCollectionSourceEditor::TileSetScenesCollectionProxyObject::ed ERR_FAIL_COND(p_source_id < 0); ERR_FAIL_COND(p_tile_set->get_source(p_source_id) != p_tile_set_scenes_collection_source); + if (tile_set == p_tile_set && tile_set_scenes_collection_source == p_tile_set_scenes_collection_source && source_id == p_source_id) { + return; + } + // Disconnect to changes. if (tile_set_scenes_collection_source) { tile_set_scenes_collection_source->disconnect(CoreStringNames::get_singleton()->property_list_changed, callable_mp((Object *)this, &Object::notify_property_list_changed)); @@ -174,6 +192,10 @@ void TileSetScenesCollectionSourceEditor::SceneTileProxyObject::edit(TileSetScen ERR_FAIL_COND(!p_tile_set_scenes_collection_source); ERR_FAIL_COND(!p_tile_set_scenes_collection_source->has_scene_tile_id(p_scene_id)); + if (tile_set_scenes_collection_source == p_tile_set_scenes_collection_source && scene_id == p_scene_id) { + return; + } + tile_set_scenes_collection_source = p_tile_set_scenes_collection_source; scene_id = p_scene_id; diff --git a/editor/plugins/tiles/tile_set_scenes_collection_source_editor.h b/editor/plugins/tiles/tile_set_scenes_collection_source_editor.h index 195aa79bc4..3be7bee714 100644 --- a/editor/plugins/tiles/tile_set_scenes_collection_source_editor.h +++ b/editor/plugins/tiles/tile_set_scenes_collection_source_editor.h @@ -51,6 +51,7 @@ private: protected: bool _set(const StringName &p_name, const Variant &p_value); bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List<PropertyInfo> *p_list) const; static void _bind_methods(); public: diff --git a/editor/plugins/tiles/tiles_editor_plugin.cpp b/editor/plugins/tiles/tiles_editor_plugin.cpp index d0d01a8d49..cc1b80fa8e 100644 --- a/editor/plugins/tiles/tiles_editor_plugin.cpp +++ b/editor/plugins/tiles/tiles_editor_plugin.cpp @@ -30,17 +30,18 @@ #include "tiles_editor_plugin.h" +#include "core/os/mutex.h" + #include "editor/editor_node.h" #include "editor/editor_scale.h" #include "editor/plugins/canvas_item_editor_plugin.h" #include "scene/2d/tile_map.h" -#include "scene/resources/tile_set.h" - #include "scene/gui/box_container.h" #include "scene/gui/button.h" #include "scene/gui/control.h" #include "scene/gui/separator.h" +#include "scene/resources/tile_set.h" #include "tile_set_editor.h" @@ -65,6 +66,99 @@ void TilesEditor::_notification(int p_what) { } } +void TilesEditor::_pattern_preview_done(const Variant &p_udata) { + pattern_preview_done.set(); +} + +void TilesEditor::_thread_func(void *ud) { + TilesEditor *te = (TilesEditor *)ud; + te->_thread(); +} + +void TilesEditor::_thread() { + pattern_thread_exited.clear(); + while (!pattern_thread_exit.is_set()) { + pattern_preview_sem.wait(); + + pattern_preview_mutex.lock(); + if (pattern_preview_queue.size()) { + QueueItem item = pattern_preview_queue.front()->get(); + pattern_preview_queue.pop_front(); + pattern_preview_mutex.unlock(); + + int thumbnail_size = EditorSettings::get_singleton()->get("filesystem/file_dialog/thumbnail_size"); + thumbnail_size *= EDSCALE; + Vector2 thumbnail_size2 = Vector2(thumbnail_size, thumbnail_size); + + if (item.pattern.is_valid() && !item.pattern->is_empty()) { + // Generate the pattern preview + SubViewport *viewport = memnew(SubViewport); + viewport->set_size(thumbnail_size2); + viewport->set_disable_input(true); + viewport->set_transparent_background(true); + viewport->set_update_mode(SubViewport::UPDATE_ONCE); + + TileMap *tile_map = memnew(TileMap); + tile_map->set_tileset(item.tile_set); + tile_map->set_pattern(0, Vector2(), item.pattern); + viewport->add_child(tile_map); + + TypedArray<Vector2i> used_cells = tile_map->get_used_cells(0); + + Rect2 encompassing_rect = Rect2(); + encompassing_rect.set_position(tile_map->map_to_world(used_cells[0])); + for (int i = 0; i < used_cells.size(); i++) { + Vector2i cell = used_cells[i]; + Vector2 world_pos = tile_map->map_to_world(cell); + encompassing_rect.expand_to(world_pos); + + // Texture. + Ref<TileSetAtlasSource> atlas_source = tile_set->get_source(tile_map->get_cell_source_id(0, cell)); + if (atlas_source.is_valid()) { + Vector2i coords = tile_map->get_cell_atlas_coords(0, cell); + int alternative = tile_map->get_cell_alternative_tile(0, cell); + + Vector2 center = world_pos - atlas_source->get_tile_effective_texture_offset(coords, alternative); + encompassing_rect.expand_to(center - atlas_source->get_tile_texture_region(coords).size / 2); + encompassing_rect.expand_to(center + atlas_source->get_tile_texture_region(coords).size / 2); + } + } + + Vector2 scale = thumbnail_size2 / MAX(encompassing_rect.size.x, encompassing_rect.size.y); + tile_map->set_scale(scale); + tile_map->set_position(-(scale * encompassing_rect.get_center()) + thumbnail_size2 / 2); + + // Add the viewport at the lasst moment to avoid rendering too early. + EditorNode::get_singleton()->add_child(viewport); + + pattern_preview_done.clear(); + RS::get_singleton()->request_frame_drawn_callback(const_cast<TilesEditor *>(this), "_pattern_preview_done", Variant()); + + while (!pattern_preview_done.is_set()) { + OS::get_singleton()->delay_usec(10); + } + + Ref<Image> image = viewport->get_texture()->get_image(); + Ref<ImageTexture> image_texture; + image_texture.instantiate(); + image_texture->create_from_image(image); + + // Find the index for the given pattern. TODO: optimize. + Variant args[] = { item.pattern, image_texture }; + const Variant *args_ptr[] = { &args[0], &args[1] }; + Variant r; + Callable::CallError error; + item.callback.call(args_ptr, 2, r, error); + + viewport->queue_delete(); + } else { + pattern_preview_mutex.unlock(); + } + } + } + pattern_thread_exited.set(); +} + void TilesEditor::_tile_map_changed() { tile_map_changed_needs_update = true; } @@ -83,6 +177,7 @@ void TilesEditor::_update_editors() { // Set editors visibility. tilemap_toolbar->set_visible(!tileset_tilemap_switch_button->is_pressed()); tilemap_editor->set_visible(!tileset_tilemap_switch_button->is_pressed()); + tileset_toolbar->set_visible(tileset_tilemap_switch_button->is_pressed()); tileset_editor->set_visible(tileset_tilemap_switch_button->is_pressed()); // Enable/disable the switch button. @@ -150,6 +245,16 @@ void TilesEditor::synchronize_atlas_view(Object *p_current) { } } +void TilesEditor::queue_pattern_preview(Ref<TileSet> p_tile_set, Ref<TileMapPattern> p_pattern, Callable p_callback) { + ERR_FAIL_COND(!p_tile_set.is_valid()); + ERR_FAIL_COND(!p_pattern.is_valid()); + { + MutexLock lock(pattern_preview_mutex); + pattern_preview_queue.push_back({ p_tile_set, p_pattern, p_callback }); + } + pattern_preview_sem.post(); +} + void TilesEditor::edit(Object *p_object) { // Disconnect to changes. TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); @@ -192,6 +297,7 @@ void TilesEditor::edit(Object *p_object) { } void TilesEditor::_bind_methods() { + ClassDB::bind_method(D_METHOD("_pattern_preview_done", "pattern"), &TilesEditor::_pattern_preview_done); } TilesEditor::TilesEditor(EditorNode *p_editor) { @@ -229,12 +335,27 @@ TilesEditor::TilesEditor(EditorNode *p_editor) { tileset_editor->hide(); add_child(tileset_editor); + tileset_toolbar = tileset_editor->get_toolbar(); + toolbar->add_child(tileset_toolbar); + + // Pattern preview generation thread. + pattern_preview_thread.start(_thread_func, this); + // Initialization. _update_switch_button(); _update_editors(); } TilesEditor::~TilesEditor() { + if (pattern_preview_thread.is_started()) { + pattern_thread_exit.set(); + pattern_preview_sem.post(); + while (!pattern_thread_exited.is_set()) { + OS::get_singleton()->delay_usec(10000); + RenderingServer::get_singleton()->sync(); //sync pending stuff, as thread may be blocked on visual server + } + pattern_preview_thread.wait_to_finish(); + } } /////////////////////////////////////////////////////////////// diff --git a/editor/plugins/tiles/tiles_editor_plugin.h b/editor/plugins/tiles/tiles_editor_plugin.h index f976d68938..c77fd76d1c 100644 --- a/editor/plugins/tiles/tiles_editor_plugin.h +++ b/editor/plugins/tiles/tiles_editor_plugin.h @@ -53,6 +53,7 @@ private: Control *tilemap_toolbar; TileMapEditor *tilemap_editor; + Control *tileset_toolbar; TileSetEditor *tileset_editor; void _update_switch_button(); @@ -63,6 +64,23 @@ private: float atlas_view_zoom = 1.0; Vector2 atlas_view_scroll = Vector2(); + // Patterns preview generation. + struct QueueItem { + Ref<TileSet> tile_set; + Ref<TileMapPattern> pattern; + Callable callback; + }; + List<QueueItem> pattern_preview_queue; + Mutex pattern_preview_mutex; + Semaphore pattern_preview_sem; + Thread pattern_preview_thread; + SafeFlag pattern_thread_exit; + SafeFlag pattern_thread_exited; + mutable SafeFlag pattern_preview_done; + void _pattern_preview_done(const Variant &p_udata); + static void _thread_func(void *ud); + void _thread(); + void _tile_map_changed(); protected: @@ -82,6 +100,9 @@ public: void set_atlas_view_transform(float p_zoom, Vector2 p_scroll); void synchronize_atlas_view(Object *p_current); + // Pattern preview API. + void queue_pattern_preview(Ref<TileSet> p_tile_set, Ref<TileMapPattern> p_pattern, Callable p_callback); + void edit(Object *p_object); TilesEditor(EditorNode *p_editor); diff --git a/editor/rename_dialog.cpp b/editor/rename_dialog.cpp index 9063b5c6f8..a5e1b0eab8 100644 --- a/editor/rename_dialog.cpp +++ b/editor/rename_dialog.cpp @@ -459,7 +459,7 @@ String RenameDialog::_substitute(const String &subject, const Node *node, int co return result; } -void RenameDialog::_error_handler(void *p_self, const char *p_func, const char *p_file, int p_line, const char *p_error, const char *p_errorexp, ErrorHandlerType p_type) { +void RenameDialog::_error_handler(void *p_self, const char *p_func, const char *p_file, int p_line, const char *p_error, const char *p_errorexp, bool p_editor_notify, ErrorHandlerType p_type) { RenameDialog *self = (RenameDialog *)p_self; String source_file(p_file); diff --git a/editor/rename_dialog.h b/editor/rename_dialog.h index 7990862b37..4db3ef6740 100644 --- a/editor/rename_dialog.h +++ b/editor/rename_dialog.h @@ -63,7 +63,7 @@ class RenameDialog : public ConfirmationDialog { String _postprocess(const String &subject); void _update_preview(String new_text = ""); void _update_preview_int(int new_value = 0); - static void _error_handler(void *p_self, const char *p_func, const char *p_file, int p_line, const char *p_error, const char *p_errorexp, ErrorHandlerType p_type); + static void _error_handler(void *p_self, const char *p_func, const char *p_file, int p_line, const char *p_error, const char *p_errorexp, bool p_editor_notify, ErrorHandlerType p_type); SceneTreeEditor *scene_tree_editor; UndoRedo *undo_redo; |