diff --git a/editor/editor_log.cpp b/editor/editor_log.cpp index 77f8e7dfc7..7c19c9fad5 100644 --- a/editor/editor_log.cpp +++ b/editor/editor_log.cpp @@ -57,24 +57,100 @@ void EditorLog::_error_handler(void *p_self, const char *p_func, const char *p_f } } -void EditorLog::_notification(int p_what) { - if (p_what == NOTIFICATION_ENTER_TREE) { - //button->set_icon(get_icon("Console","EditorIcons")); - log->add_font_override("normal_font", get_font("output_source", "EditorFonts")); - log->add_color_override("selection_color", get_color("accent_color", "Editor") * Color(1, 1, 1, 0.4)); - } else if (p_what == NOTIFICATION_THEME_CHANGED) { - Ref df_output_code = get_font("output_source", "EditorFonts"); - if (df_output_code.is_valid()) { - if (log != nullptr) { - log->add_font_override("normal_font", get_font("output_source", "EditorFonts")); - log->add_color_override("selection_color", get_color("accent_color", "Editor") * Color(1, 1, 1, 0.4)); - } - } +void EditorLog::_update_theme() { + Ref normal_font = get_font("output_source", "EditorFonts"); + if (normal_font.is_valid()) { + log->add_font_override("normal_font", normal_font); } + + log->add_color_override("selection_color", get_color("accent_color", "Editor") * Color(1, 1, 1, 0.4)); + + Ref bold_font = get_font("bold", "EditorFonts"); + if (bold_font.is_valid()) { + log->add_font_override("bold_font", bold_font); + } + + type_filter_map[MSG_TYPE_STD]->toggle_button->set_icon(get_icon("Popup", "EditorIcons")); + type_filter_map[MSG_TYPE_ERROR]->toggle_button->set_icon(get_icon("StatusError", "EditorIcons")); + type_filter_map[MSG_TYPE_WARNING]->toggle_button->set_icon(get_icon("StatusWarning", "EditorIcons")); + type_filter_map[MSG_TYPE_EDITOR]->toggle_button->set_icon(get_icon("Edit", "EditorIcons")); + + clear_button->set_icon(get_icon("Clear", "EditorIcons")); + copy_button->set_icon(get_icon("ActionCopy", "EditorIcons")); + collapse_button->set_icon(get_icon("CombineLines", "EditorIcons")); + show_search_button->set_icon(get_icon("Search", "EditorIcons")); +} + +void EditorLog::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: { + _update_theme(); + _load_state(); + } break; + case NOTIFICATION_THEME_CHANGED: { + _update_theme(); + _rebuild_log(); + } break; + default: + break; + } +} + +void EditorLog::_set_collapse(bool p_collapse) { + collapse = p_collapse; + _start_state_save_timer(); + _rebuild_log(); +} + +void EditorLog::_start_state_save_timer() { + if (!is_loading_state) { + save_state_timer->start(); + } +} + +void EditorLog::_save_state() { + Ref config; + config.instance(); + // Load and amend existing config if it exists. + config->load(EditorSettings::get_singleton()->get_project_settings_dir().plus_file("editor_layout.cfg")); + + const String section = "editor_log"; + for (Map::Element *E = type_filter_map.front(); E; E = E->next()) { + config->set_value(section, "log_filter_" + itos(E->key()), E->get()->is_active()); + } + + config->set_value(section, "collapse", collapse); + config->set_value(section, "show_search", search_box->is_visible()); + + config->save(EditorSettings::get_singleton()->get_project_settings_dir().plus_file("editor_layout.cfg")); +} + +void EditorLog::_load_state() { + is_loading_state = true; + + Ref config; + config.instance(); + config->load(EditorSettings::get_singleton()->get_project_settings_dir().plus_file("editor_layout.cfg")); + + // Run the below code even if config->load returns an error, since we want the defaults to be set even if the file does not exist yet. + const String section = "editor_log"; + for (Map::Element *E = type_filter_map.front(); E; E = E->next()) { + E->get()->set_active(config->get_value(section, "log_filter_" + itos(E->key()), true)); + } + + collapse = config->get_value(section, "collapse", false); + collapse_button->set_pressed(collapse); + bool show_search = config->get_value(section, "show_search", true); + search_box->set_visible(show_search); + show_search_button->set_pressed(show_search); + + is_loading_state = false; } void EditorLog::_clear_request() { log->clear(); + messages.clear(); + _reset_message_counts(); tool_button->set_icon(Ref()); } @@ -94,13 +170,82 @@ void EditorLog::clear() { _clear_request(); } -void EditorLog::copy() { - _copy_request(); +void EditorLog::_process_message(const String &p_msg, MessageType p_type) { + if (messages.size() > 0 && messages[messages.size() - 1].text == p_msg) { + // If previous message is the same as the new one, increase previous count rather than adding another + // instance to the messages list. + LogMessage &previous = messages.write[messages.size() - 1]; + previous.count++; + + _add_log_line(previous, collapse); + } else { + // Different message to the previous one received. + LogMessage message(p_msg, p_type); + _add_log_line(message); + messages.push_back(message); + } + + type_filter_map[p_type]->set_message_count(type_filter_map[p_type]->get_message_count() + 1); } void EditorLog::add_message(const String &p_msg, MessageType p_type) { - bool restore = p_type != MSG_TYPE_STD; - switch (p_type) { + // Make text split by new lines their own message. + // See #41321 for reasoning. At time of writing, multiple print()'s in running projects + // get grouped together and sent to the editor log as one message. This can mess with the + // search functionality (see the comments on the PR above for more details). This behaviour + // also matches that of other IDE's. + Vector lines = p_msg.split("\n", true); + + for (int i = 0; i < lines.size(); i++) { + _process_message(lines[i], p_type); + } +} + +void EditorLog::set_tool_button(Button *p_tool_button) { + tool_button = p_tool_button; +} + +void EditorLog::_undo_redo_cbk(void *p_self, const String &p_name) { + EditorLog *self = (EditorLog *)p_self; + self->add_message(p_name, EditorLog::MSG_TYPE_EDITOR); +} + +void EditorLog::_rebuild_log() { + log->clear(); + + for (int msg_idx = 0; msg_idx < messages.size(); msg_idx++) { + LogMessage msg = messages[msg_idx]; + + if (collapse) { + // If collapsing, only log one instance of the message. + _add_log_line(msg); + } else { + // If not collapsing, log each instance on a line. + for (int i = 0; i < msg.count; i++) { + _add_log_line(msg); + } + } + } +} + +void EditorLog::_add_log_line(LogMessage &p_message, bool p_replace_previous) { + // Only add the message to the log if it passes the filters. + bool filter_active = type_filter_map[p_message.type]->is_active(); + String search_text = search_box->get_text(); + bool search_match = search_text == String() || p_message.text.findn(search_text) > -1; + + if (!filter_active || !search_match) { + return; + } + + if (p_replace_previous) { + // Remove last line if replacing, as it will be replace by the next added line. + // Why "- 2"? RichTextLabel is weird. When you add a line with add_newline(), it also adds an element to the list of lines which is null/blank, + // but it still counts as a line. So if you remove the last line (count - 1) you are actually removing nothing... + log->remove_line(log->get_line_count() - 2); + } + + switch (p_message.type) { case MSG_TYPE_STD: { } break; case MSG_TYPE_ERROR: { @@ -123,60 +268,167 @@ void EditorLog::add_message(const String &p_msg, MessageType p_type) { } break; } - log->add_text(p_msg); - log->add_newline(); - - if (restore) { + // If collapsing, add the count of this message in bold at the start of the line. + if (collapse && p_message.count > 1) { + log->push_bold(); + log->add_text(vformat("(%s) ", itos(p_message.count))); log->pop(); } + + log->add_text(p_message.text); + + // Need to use pop() to exit out of the RichTextLabels current "push" stack. + // We only "push" in the above switch when message type != STD, so only pop when that is the case. + if (p_message.type != MSG_TYPE_STD) { + log->pop(); + } + + log->add_newline(); } -void EditorLog::set_tool_button(ToolButton *p_tool_button) { - tool_button = p_tool_button; +void EditorLog::_set_filter_active(bool p_active, MessageType p_message_type) { + type_filter_map[p_message_type]->set_active(p_active); + _start_state_save_timer(); + _rebuild_log(); } -void EditorLog::_undo_redo_cbk(void *p_self, const String &p_name) { - EditorLog *self = (EditorLog *)p_self; - self->add_message(p_name, EditorLog::MSG_TYPE_EDITOR); +void EditorLog::_set_search_visible(bool p_visible) { + search_box->set_visible(p_visible); + if (p_visible) { + search_box->grab_focus(); + } + _start_state_save_timer(); +} + +void EditorLog::_search_changed(const String &p_text) { + _rebuild_log(); +} + +void EditorLog::_reset_message_counts() { + for (Map::Element *E = type_filter_map.front(); E; E = E->next()) { + E->value()->set_message_count(0); + } } void EditorLog::_bind_methods() { ClassDB::bind_method(D_METHOD("_clear_request"), &EditorLog::_clear_request); ClassDB::bind_method(D_METHOD("_copy_request"), &EditorLog::_copy_request); + ClassDB::bind_method(D_METHOD("_search_changed"), &EditorLog::_search_changed); + ClassDB::bind_method(D_METHOD("_set_collapse"), &EditorLog::_set_collapse); + ClassDB::bind_method(D_METHOD("_set_search_visible"), &EditorLog::_set_search_visible); + ClassDB::bind_method(D_METHOD("_set_filter_active"), &EditorLog::_set_filter_active); + ClassDB::bind_method(D_METHOD("_save_state"), &EditorLog::_save_state); + ADD_SIGNAL(MethodInfo("clear_request")); ADD_SIGNAL(MethodInfo("copy_request")); } EditorLog::EditorLog() { - VBoxContainer *vb = this; + save_state_timer = memnew(Timer); + save_state_timer->set_wait_time(2); + save_state_timer->set_one_shot(true); + save_state_timer->connect("timeout", this, "_save_state"); + add_child(save_state_timer); - HBoxContainer *hb = memnew(HBoxContainer); - vb->add_child(hb); - title = memnew(Label); - title->set_text(TTR("Output:")); - title->set_h_size_flags(SIZE_EXPAND_FILL); - hb->add_child(title); + HBoxContainer *hb = this; - copybutton = memnew(Button); - hb->add_child(copybutton); - copybutton->set_text(TTR("Copy")); - copybutton->set_shortcut(ED_SHORTCUT("editor/copy_output", TTR("Copy Selection"), KEY_MASK_CMD | KEY_C)); - copybutton->connect("pressed", this, "_copy_request"); - - clearbutton = memnew(Button); - hb->add_child(clearbutton); - clearbutton->set_text(TTR("Clear")); - clearbutton->set_shortcut(ED_SHORTCUT("editor/clear_output", TTR("Clear Output"), KEY_MASK_CMD | KEY_MASK_SHIFT | KEY_K)); - clearbutton->connect("pressed", this, "_clear_request"); + VBoxContainer *vb_left = memnew(VBoxContainer); + vb_left->set_custom_minimum_size(Size2(0, 180) * EDSCALE); + vb_left->set_v_size_flags(SIZE_EXPAND_FILL); + vb_left->set_h_size_flags(SIZE_EXPAND_FILL); + hb->add_child(vb_left); + // Log - Rich Text Label. log = memnew(RichTextLabel); log->set_scroll_follow(true); log->set_selection_enabled(true); log->set_focus_mode(FOCUS_CLICK); - log->set_custom_minimum_size(Size2(0, 180) * EDSCALE); log->set_v_size_flags(SIZE_EXPAND_FILL); log->set_h_size_flags(SIZE_EXPAND_FILL); - vb->add_child(log); + vb_left->add_child(log); + + // Search box + search_box = memnew(LineEdit); + search_box->set_h_size_flags(Control::SIZE_EXPAND_FILL); + search_box->set_placeholder(TTR("Filter messages")); + search_box->set_right_icon(get_icon("Search", "EditorIcons")); + search_box->set_clear_button_enabled(true); + search_box->set_visible(true); + search_box->connect("text_changed", this, "_search_changed"); + vb_left->add_child(search_box); + + VBoxContainer *vb_right = memnew(VBoxContainer); + hb->add_child(vb_right); + + // Tools grid + HBoxContainer *hb_tools = memnew(HBoxContainer); + hb_tools->set_h_size_flags(SIZE_SHRINK_CENTER); + vb_right->add_child(hb_tools); + + // Clear. + clear_button = memnew(Button); + clear_button->set_flat(true); + clear_button->set_focus_mode(FOCUS_NONE); + clear_button->set_shortcut(ED_SHORTCUT("editor/clear_output", TTR("Clear Output"), KEY_MASK_CMD | KEY_MASK_SHIFT | KEY_K)); + clear_button->connect("pressed", this, "_clear_request"); + hb_tools->add_child(clear_button); + + // Copy. + copy_button = memnew(Button); + copy_button->set_flat(true); + copy_button->set_focus_mode(FOCUS_NONE); + copy_button->set_shortcut(ED_SHORTCUT("editor/copy_output", TTR("Copy Selection"), KEY_MASK_CMD | KEY_C)); + copy_button->connect("pressed", this, "_copy_request"); + hb_tools->add_child(copy_button); + + // A second hbox to make a 2x2 grid of buttons. + HBoxContainer *hb_tools2 = memnew(HBoxContainer); + hb_tools2->set_h_size_flags(SIZE_SHRINK_CENTER); + vb_right->add_child(hb_tools2); + + // Collapse. + collapse_button = memnew(Button); + collapse_button->set_flat(true); + collapse_button->set_focus_mode(FOCUS_NONE); + collapse_button->set_tooltip(TTR("Collapse duplicate messages into one log entry. Shows number of occurrences.")); + collapse_button->set_toggle_mode(true); + collapse_button->set_pressed(false); + collapse_button->connect("toggled", this, "_set_collapse"); + hb_tools2->add_child(collapse_button); + + // Show Search. + show_search_button = memnew(Button); + show_search_button->set_flat(true); + show_search_button->set_focus_mode(FOCUS_NONE); + show_search_button->set_toggle_mode(true); + show_search_button->set_pressed(true); + show_search_button->set_shortcut(ED_SHORTCUT("editor/open_search", TTR("Focus Search/Filter Bar"), KEY_MASK_CMD | KEY_F)); + show_search_button->connect("toggled", this, "_set_search_visible"); + hb_tools2->add_child(show_search_button); + + // Message Type Filters. + vb_right->add_child(memnew(HSeparator)); + + LogFilter *std_filter = memnew(LogFilter(MSG_TYPE_STD)); + std_filter->initialize_button(TTR("Toggle visibility of standard output messages."), this, "_set_filter_active"); + vb_right->add_child(std_filter->toggle_button); + type_filter_map.insert(MSG_TYPE_STD, std_filter); + + LogFilter *error_filter = memnew(LogFilter(MSG_TYPE_ERROR)); + error_filter->initialize_button(TTR("Toggle visibility of errors."), this, "_set_filter_active"); + vb_right->add_child(error_filter->toggle_button); + type_filter_map.insert(MSG_TYPE_ERROR, error_filter); + + LogFilter *warning_filter = memnew(LogFilter(MSG_TYPE_WARNING)); + warning_filter->initialize_button(TTR("Toggle visibility of warnings."), this, "_set_filter_active"); + vb_right->add_child(warning_filter->toggle_button); + type_filter_map.insert(MSG_TYPE_WARNING, warning_filter); + + LogFilter *editor_filter = memnew(LogFilter(MSG_TYPE_EDITOR)); + editor_filter->initialize_button(TTR("Toggle visibility of editor messages."), this, "_set_filter_active"); + vb_right->add_child(editor_filter->toggle_button); + type_filter_map.insert(MSG_TYPE_EDITOR, editor_filter); + add_message(VERSION_FULL_NAME " (c) 2007-2021 Juan Linietsky, Ariel Manzur & Godot Contributors."); eh.errfunc = _error_handler; @@ -185,8 +437,6 @@ EditorLog::EditorLog() { current = Thread::get_caller_id(); - add_constant_override("separation", get_constant("separation", "VBoxContainer")); - EditorNode::get_undo_redo()->set_commit_notify_callback(_undo_redo_cbk, this); } @@ -195,4 +445,7 @@ void EditorLog::deinit() { } EditorLog::~EditorLog() { + for (Map::Element *E = type_filter_map.front(); E; E = E->next()) { + memdelete(E->get()); + } } diff --git a/editor/editor_log.h b/editor/editor_log.h index 9c22b964a9..15e3b4f571 100644 --- a/editor/editor_log.h +++ b/editor/editor_log.h @@ -33,6 +33,8 @@ #include "scene/gui/control.h" #include "scene/gui/label.h" +#include "scene/gui/line_edit.h" +#include "scene/gui/panel_container.h" #include "scene/gui/rich_text_label.h" #include "scene/gui/texture_button.h" //#include "scene/gui/empty_control.h" @@ -43,16 +45,99 @@ #include "scene/gui/texture_rect.h" #include "scene/gui/tool_button.h" -class EditorLog : public VBoxContainer { - GDCLASS(EditorLog, VBoxContainer); +class EditorLog : public HBoxContainer { + GDCLASS(EditorLog, HBoxContainer); + +public: + enum MessageType { + MSG_TYPE_STD, + MSG_TYPE_ERROR, + MSG_TYPE_WARNING, + MSG_TYPE_EDITOR, + }; + +private: + struct LogMessage { + String text; + MessageType type; + int count = 1; + + LogMessage() {} + + LogMessage(const String p_text, MessageType p_type) : + text(p_text), + type(p_type) { + } + }; + + // Encapsulates all data and functionality regarding filters. + struct LogFilter { + private: + // Force usage of set method since it has functionality built-in. + int message_count = 0; + bool active = true; + + public: + MessageType type; + Button *toggle_button = nullptr; + + void initialize_button(const String &p_tooltip, Object *p_toggled_callback_object, StringName p_toggled_callback_method) { + toggle_button = memnew(Button); + toggle_button->set_toggle_mode(true); + toggle_button->set_pressed(true); + toggle_button->set_text(itos(message_count)); + toggle_button->set_tooltip(TTR(p_tooltip)); + // Don't tint the icon even when in "pressed" state. + toggle_button->add_color_override("icon_color_pressed", Color(1, 1, 1, 1)); + toggle_button->set_focus_mode(FOCUS_NONE); + // When toggled call the callback and pass the MessageType this button is for. + toggle_button->connect("toggled", p_toggled_callback_object, p_toggled_callback_method, varray(type)); + } + + int get_message_count() { + return message_count; + } + + void set_message_count(int p_count) { + message_count = p_count; + toggle_button->set_text(itos(message_count)); + } + + bool is_active() { + return active; + } + + void set_active(bool p_active) { + toggle_button->set_pressed(p_active); + active = p_active; + } + + LogFilter(MessageType p_type) : + type(p_type) { + } + }; + + Vector messages; + // Maps MessageTypes to LogFilters for convenient access and storage (don't need 1 member per filter). + Map type_filter_map; - Button *clearbutton; - Button *copybutton; - Label *title; RichTextLabel *log; - HBoxContainer *title_hb; - //PaneDrag *pd; - ToolButton *tool_button; + + Button *clear_button; + Button *copy_button; + + Button *collapse_button; + bool collapse = false; + + Button *show_search_button; + LineEdit *search_box; + + // Reference to the "Output" button on the toolbar so we can update it's icon when + // Warnings or Errors are encounetered. + Button *tool_button; + + 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); @@ -65,26 +150,39 @@ class EditorLog : public VBoxContainer { void _copy_request(); static void _undo_redo_cbk(void *p_self, const String &p_name); + void _rebuild_log(); + void _add_log_line(LogMessage &p_message, bool p_replace_previous = false); + + void _set_filter_active(bool p_active, MessageType p_message_type); + void _set_search_visible(bool p_visible); + void _search_changed(const String &p_text); + + void _process_message(const String &p_msg, MessageType p_type); + void _reset_message_counts(); + + void _set_collapse(bool p_collapse); + + void _start_state_save_timer(); + void _save_state(); + void _load_state(); + + void _update_theme(); + protected: static void _bind_methods(); void _notification(int p_what); public: - enum MessageType { - MSG_TYPE_STD, - MSG_TYPE_ERROR, - MSG_TYPE_WARNING, - MSG_TYPE_EDITOR - }; - void add_message(const String &p_msg, MessageType p_type = MSG_TYPE_STD); - void set_tool_button(ToolButton *p_tool_button); + void set_tool_button(Button *p_tool_button); void deinit(); void clear(); - void copy(); + EditorLog(); ~EditorLog(); }; +VARIANT_ENUM_CAST(EditorLog::MessageType); + #endif // EDITOR_LOG_H diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index c35746527a..319100c6f4 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -4321,6 +4321,8 @@ void EditorNode::_save_docks() { } Ref config; config.instance(); + // Load and amend existing config if it exists. + config->load(EditorSettings::get_singleton()->get_project_settings_dir().plus_file("editor_layout.cfg")); _save_docks_to_config(config, "docks"); _save_open_scenes_to_config(config, "EditorNode"); diff --git a/editor/icons/icon_combine_lines.svg b/editor/icons/icon_combine_lines.svg new file mode 100644 index 0000000000..124814ae88 --- /dev/null +++ b/editor/icons/icon_combine_lines.svg @@ -0,0 +1 @@ + diff --git a/scene/gui/rich_text_label.cpp b/scene/gui/rich_text_label.cpp index 4db1b5ba6c..d41904d486 100644 --- a/scene/gui/rich_text_label.cpp +++ b/scene/gui/rich_text_label.cpp @@ -1645,18 +1645,22 @@ void RichTextLabel::_remove_item(Item *p_item, const int p_line, const int p_sub int size = p_item->subitems.size(); if (size == 0) { p_item->parent->subitems.erase(p_item); + // If a newline was erased, all lines AFTER the newline need to be decremented. if (p_item->type == ITEM_NEWLINE) { current_frame->lines.remove(p_line); - for (int i = p_subitem_line; i < current->subitems.size(); i++) { - if (current->subitems[i]->line > 0) { + for (int i = 0; i < current->subitems.size(); i++) { + if (current->subitems[i]->line > p_subitem_line) { current->subitems[i]->line--; } } } } else { + // First, remove all child items for the provided item. for (int i = 0; i < size; i++) { _remove_item(p_item->subitems.front()->get(), p_line, p_subitem_line); } + // Then remove the provided item itself. + p_item->parent->subitems.erase(p_item); } } @@ -1714,21 +1718,23 @@ bool RichTextLabel::remove_line(const int p_line) { return false; } - int i = 0; - while (i < current->subitems.size() && current->subitems[i]->line < p_line) { - i++; - } - - bool was_newline = false; - while (i < current->subitems.size()) { - was_newline = current->subitems[i]->type == ITEM_NEWLINE; - _remove_item(current->subitems[i], current->subitems[i]->line, p_line); - if (was_newline) { - break; + // Remove all subitems with the same line as that provided. + Vector subitem_indices_to_remove; + for (int i = 0; i < current->subitems.size(); i++) { + if (current->subitems[i]->line == p_line) { + subitem_indices_to_remove.push_back(i); } } - if (!was_newline) { + bool had_newline = false; + // Reverse for loop to remove items from the end first. + for (int i = subitem_indices_to_remove.size() - 1; i >= 0; i--) { + int subitem_idx = subitem_indices_to_remove[i]; + had_newline = had_newline || current->subitems[subitem_idx]->type == ITEM_NEWLINE; + _remove_item(current->subitems[subitem_idx], current->subitems[subitem_idx]->line, p_line); + } + + if (!had_newline) { current_frame->lines.remove(p_line); if (current_frame->lines.size() == 0) { current_frame->lines.resize(1); @@ -1739,7 +1745,8 @@ bool RichTextLabel::remove_line(const int p_line) { main->lines.write[0].from = main; } - main->first_invalid_line = 0; + main->first_invalid_line = 0; // p_line ??? + update(); return true; }