diff options
Diffstat (limited to 'editor')
| -rw-r--r-- | editor/code_editor.cpp | 18 | ||||
| -rw-r--r-- | editor/editor_asset_installer.cpp | 439 | ||||
| -rw-r--r-- | editor/editor_asset_installer.h | 38 | ||||
| -rw-r--r-- | editor/editor_node.cpp | 12 | ||||
| -rw-r--r-- | editor/editor_node.h | 1 | ||||
| -rw-r--r-- | editor/editor_properties.cpp | 2 | ||||
| -rw-r--r-- | editor/editor_themes.cpp | 4 | ||||
| -rw-r--r-- | editor/filesystem_dock.cpp | 8 | ||||
| -rw-r--r-- | editor/gui/editor_spin_slider.cpp | 27 | ||||
| -rw-r--r-- | editor/plugins/bone_map_editor_plugin.cpp | 185 | ||||
| -rw-r--r-- | editor/plugins/bone_map_editor_plugin.h | 1 | ||||
| -rw-r--r-- | editor/plugins/navigation_polygon_editor_plugin.cpp | 88 | ||||
| -rw-r--r-- | editor/plugins/navigation_polygon_editor_plugin.h | 26 | ||||
| -rw-r--r-- | editor/plugins/script_text_editor.cpp | 6 | ||||
| -rw-r--r-- | editor/plugins/script_text_editor.h | 1 | ||||
| -rw-r--r-- | editor/plugins/text_editor.cpp | 4 | ||||
| -rw-r--r-- | editor/plugins/text_editor.h | 1 | ||||
| -rw-r--r-- | editor/plugins/text_shader_editor.cpp | 4 | ||||
| -rw-r--r-- | editor/plugins/text_shader_editor.h | 1 | ||||
| -rw-r--r-- | editor/script_create_dialog.cpp | 9 | ||||
| -rw-r--r-- | editor/script_create_dialog.h | 1 |
21 files changed, 663 insertions, 213 deletions
diff --git a/editor/code_editor.cpp b/editor/code_editor.cpp index 876fef078b..56e405bfcf 100644 --- a/editor/code_editor.cpp +++ b/editor/code_editor.cpp @@ -805,6 +805,11 @@ void CodeTextEditor::input(const Ref<InputEvent> &event) { accept_event(); return; } + if (ED_IS_SHORTCUT("script_text_editor/duplicate_lines", key_event)) { + text_editor->duplicate_lines(); + accept_event(); + return; + } } void CodeTextEditor::_text_editor_gui_input(const Ref<InputEvent> &p_event) { @@ -1487,14 +1492,22 @@ void CodeTextEditor::toggle_inline_comment(const String &delimiter) { } // Check first if there's any uncommented lines in selection. bool is_commented = true; + bool is_all_empty = true; for (int line = from; line <= to; line++) { // `+ delimiter.length()` here because comment delimiter is not actually `in comment` so we check first character after it int delimiter_idx = text_editor->is_in_comment(line, text_editor->get_first_non_whitespace_column(line) + delimiter.length()); - if (delimiter_idx == -1 || text_editor->get_delimiter_start_key(delimiter_idx) != delimiter) { + // Empty lines should not be counted. + bool is_empty = text_editor->get_line(line).strip_edges().is_empty(); + is_all_empty = is_all_empty && is_empty; + if (!is_empty && (delimiter_idx == -1 || text_editor->get_delimiter_start_key(delimiter_idx) != delimiter)) { is_commented = false; break; } } + + // Special case for commenting empty lines, treat it/them as uncommented lines. + is_commented = is_commented && !is_all_empty; + // Caret positions need to be saved since they could be moved at the eol. Vector<int> caret_cols; Vector<int> selection_to_cols; @@ -1510,10 +1523,11 @@ void CodeTextEditor::toggle_inline_comment(const String &delimiter) { // Comment/uncomment. for (int line = from; line <= to; line++) { String line_text = text_editor->get_line(line); - if (line_text.strip_edges().is_empty()) { + if (is_all_empty) { text_editor->set_line(line, delimiter); continue; } + if (is_commented) { text_editor->set_line(line, line_text.replace_first(delimiter, "")); } else { diff --git a/editor/editor_asset_installer.cpp b/editor/editor_asset_installer.cpp index b2d21c3447..b609de9f67 100644 --- a/editor/editor_asset_installer.cpp +++ b/editor/editor_asset_installer.cpp @@ -35,31 +35,48 @@ #include "core/io/zip_io.h" #include "editor/editor_file_system.h" #include "editor/editor_node.h" +#include "editor/editor_scale.h" #include "editor/editor_string_names.h" +#include "editor/gui/editor_file_dialog.h" #include "editor/gui/editor_toaster.h" #include "editor/progress_dialog.h" #include "scene/gui/check_box.h" #include "scene/gui/label.h" +#include "scene/gui/link_button.h" +#include "scene/gui/separator.h" +#include "scene/gui/split_container.h" -void EditorAssetInstaller::_item_checked() { - if (updating || !tree->get_edited()) { +void EditorAssetInstaller::_item_checked_cbk() { + if (updating_source || !source_tree->get_edited()) { return; } - updating = true; - TreeItem *item = tree->get_edited(); + updating_source = true; + TreeItem *item = source_tree->get_edited(); item->propagate_check(0); - updating = false; + _update_confirm_button(); + _rebuild_destination_tree(); + updating_source = false; } -void EditorAssetInstaller::_check_propagated_to_item(Object *p_obj, int column) { +void EditorAssetInstaller::_check_propagated_to_item(Object *p_obj, int p_column) { TreeItem *affected_item = Object::cast_to<TreeItem>(p_obj); - if (affected_item && affected_item->get_custom_color(0) != Color()) { + if (!affected_item) { + return; + } + + Dictionary item_meta = affected_item->get_metadata(0); + bool is_conflict = item_meta.get("is_conflict", false); + if (is_conflict) { affected_item->set_checked(0, false); affected_item->propagate_check(0, false); } } +bool EditorAssetInstaller::_is_item_checked(const String &p_source_path) const { + return file_item_map.has(p_source_path) && (file_item_map[p_source_path]->is_checked(0) || file_item_map[p_source_path]->is_indeterminate(0)); +} + void EditorAssetInstaller::open_asset(const String &p_path, bool p_autoskip_toplevel) { package_path = p_path; asset_files.clear(); @@ -104,27 +121,24 @@ void EditorAssetInstaller::open_asset(const String &p_path, bool p_autoskip_topl unzClose(pkg); + asset_title_label->set_text(asset_name); + _check_has_toplevel(); // Default to false, unless forced. skip_toplevel = p_autoskip_toplevel; + skip_toplevel_check->set_block_signals(true); skip_toplevel_check->set_pressed(!skip_toplevel_check->is_disabled() && skip_toplevel); + skip_toplevel_check->set_block_signals(false); - _rebuild_tree(); -} - -void EditorAssetInstaller::_rebuild_tree() { - updating = true; - tree->clear(); + _update_file_mappings(); + _rebuild_source_tree(); + _rebuild_destination_tree(); - TreeItem *root = tree->create_item(); - root->set_cell_mode(0, TreeItem::CELL_MODE_CHECK); - root->set_checked(0, true); - root->set_icon(0, get_theme_icon(SNAME("folder"), SNAME("FileDialog"))); - root->set_text(0, "res://"); - root->set_editable(0, true); + popup_centered_clamped(Size2(620, 640) * EDSCALE); +} - HashMap<String, TreeItem *> directory_item_map; - int num_file_conflicts = 0; +void EditorAssetInstaller::_update_file_mappings() { + mapped_files.clear(); bool first = true; for (const String &E : asset_files) { @@ -141,9 +155,31 @@ void EditorAssetInstaller::_rebuild_tree() { path = path.trim_prefix(toplevel_prefix); } + mapped_files[E] = path; + } +} + +void EditorAssetInstaller::_rebuild_source_tree() { + updating_source = true; + source_tree->clear(); + + TreeItem *root = source_tree->create_item(); + root->set_cell_mode(0, TreeItem::CELL_MODE_CHECK); + root->set_checked(0, true); + root->set_icon(0, get_theme_icon(SNAME("folder"), SNAME("FileDialog"))); + root->set_text(0, "/"); + root->set_editable(0, true); + + file_item_map.clear(); + HashMap<String, TreeItem *> directory_item_map; + int num_file_conflicts = 0; + first_file_conflict = nullptr; + + for (const String &E : asset_files) { + String path = E; // We're going to mutate it. + bool is_directory = false; if (path.ends_with("/")) { - // Directory. path = path.trim_suffix("/"); is_directory = true; } @@ -162,45 +198,155 @@ void EditorAssetInstaller::_rebuild_tree() { TreeItem *ti; if (is_directory) { - ti = _create_dir_item(parent_item, path, directory_item_map); + ti = _create_dir_item(source_tree, parent_item, path, directory_item_map); } else { - ti = _create_file_item(parent_item, path, &num_file_conflicts); + ti = _create_file_item(source_tree, parent_item, path, &num_file_conflicts); } file_item_map[E] = ti; } - asset_title_label->set_text(asset_name); - if (num_file_conflicts >= 1) { - asset_conflicts_label->set_text(vformat(TTRN("%d file conflicts with your project", "%d files conflict with your project", num_file_conflicts), num_file_conflicts)); - asset_conflicts_label->add_theme_color_override("font_color", get_theme_color(SNAME("error_color"), EditorStringName(Editor))); + _update_conflict_status(num_file_conflicts); + _update_confirm_button(); + + updating_source = false; +} + +void EditorAssetInstaller::_update_source_tree() { + int num_file_conflicts = 0; + first_file_conflict = nullptr; + + for (const KeyValue<String, TreeItem *> &E : file_item_map) { + TreeItem *ti = E.value; + Dictionary item_meta = ti->get_metadata(0); + if ((bool)item_meta.get("is_dir", false)) { + continue; + } + + String asset_path = item_meta.get("asset_path", ""); + ERR_CONTINUE(asset_path.is_empty()); + + bool target_exists = _update_source_item_status(ti, asset_path); + if (target_exists) { + if (first_file_conflict == nullptr) { + first_file_conflict = ti; + } + num_file_conflicts += 1; + } + + item_meta["is_conflict"] = target_exists; + ti->set_metadata(0, item_meta); + } + + _update_conflict_status(num_file_conflicts); + _update_confirm_button(); +} + +bool EditorAssetInstaller::_update_source_item_status(TreeItem *p_item, const String &p_path) { + ERR_FAIL_COND_V(!mapped_files.has(p_path), false); + String target_path = target_dir_path.path_join(mapped_files[p_path]); + + bool target_exists = FileAccess::exists(target_path); + if (target_exists) { + p_item->set_custom_color(0, get_theme_color(SNAME("error_color"), EditorStringName(Editor))); + p_item->set_tooltip_text(0, vformat(TTR("%s (already exists)"), target_path)); + p_item->set_checked(0, false); } else { - asset_conflicts_label->set_text(TTR("No files conflict with your project")); - asset_conflicts_label->remove_theme_color_override("font_color"); + p_item->clear_custom_color(0); + p_item->set_tooltip_text(0, target_path); + p_item->set_checked(0, true); } - popup_centered_ratio(0.5); - updating = false; + p_item->propagate_check(0); + return target_exists; } -TreeItem *EditorAssetInstaller::_create_dir_item(TreeItem *p_parent, const String &p_path, HashMap<String, TreeItem *> &p_item_map) { - TreeItem *ti = tree->create_item(p_parent); - ti->set_cell_mode(0, TreeItem::CELL_MODE_CHECK); - ti->set_checked(0, true); - ti->set_editable(0, true); +void EditorAssetInstaller::_rebuild_destination_tree() { + destination_tree->clear(); + + TreeItem *root = destination_tree->create_item(); + root->set_icon(0, get_theme_icon(SNAME("folder"), SNAME("FileDialog"))); + root->set_text(0, target_dir_path + (target_dir_path == "res://" ? "" : "/")); + + HashMap<String, TreeItem *> directory_item_map; + + for (const KeyValue<String, String> &E : mapped_files) { + if (!_is_item_checked(E.key)) { + continue; + } + + String path = E.value; // We're going to mutate it. + + bool is_directory = false; + if (path.ends_with("/")) { + path = path.trim_suffix("/"); + is_directory = true; + } + + TreeItem *parent_item; + + int separator = path.rfind("/"); + if (separator == -1) { + parent_item = root; + } else { + String parent_path = path.substr(0, separator); + HashMap<String, TreeItem *>::Iterator I = directory_item_map.find(parent_path); + ERR_CONTINUE(!I); + parent_item = I->value; + } + + if (is_directory) { + _create_dir_item(destination_tree, parent_item, path, directory_item_map); + } else { + int num_file_conflicts = 0; // Don't need it, but need to pass something. + _create_file_item(destination_tree, parent_item, path, &num_file_conflicts); + } + } +} + +TreeItem *EditorAssetInstaller::_create_dir_item(Tree *p_tree, TreeItem *p_parent, const String &p_path, HashMap<String, TreeItem *> &p_item_map) { + TreeItem *ti = p_tree->create_item(p_parent); + + if (p_tree == source_tree) { + ti->set_cell_mode(0, TreeItem::CELL_MODE_CHECK); + ti->set_editable(0, true); + ti->set_checked(0, true); + ti->propagate_check(0); + + Dictionary meta; + meta["asset_path"] = p_path + "/"; + meta["is_dir"] = true; + meta["is_conflict"] = false; + ti->set_metadata(0, meta); + } ti->set_text(0, p_path.get_file() + "/"); ti->set_icon(0, get_theme_icon(SNAME("folder"), SNAME("FileDialog"))); - ti->set_metadata(0, String()); p_item_map[p_path] = ti; return ti; } -TreeItem *EditorAssetInstaller::_create_file_item(TreeItem *p_parent, const String &p_path, int *r_conflicts) { - TreeItem *ti = tree->create_item(p_parent); - ti->set_cell_mode(0, TreeItem::CELL_MODE_CHECK); - ti->set_checked(0, true); - ti->set_editable(0, true); +TreeItem *EditorAssetInstaller::_create_file_item(Tree *p_tree, TreeItem *p_parent, const String &p_path, int *r_conflicts) { + TreeItem *ti = p_tree->create_item(p_parent); + + if (p_tree == source_tree) { + ti->set_cell_mode(0, TreeItem::CELL_MODE_CHECK); + ti->set_editable(0, true); + + bool target_exists = _update_source_item_status(ti, p_path); + if (target_exists) { + if (first_file_conflict == nullptr) { + first_file_conflict = ti; + } + *r_conflicts += 1; + } + + Dictionary meta; + meta["asset_path"] = p_path; + meta["is_dir"] = false; + meta["is_conflict"] = target_exists; + ti->set_metadata(0, meta); + } String file = p_path.get_file(); String extension = file.get_extension().to_lower(); @@ -211,20 +357,38 @@ TreeItem *EditorAssetInstaller::_create_file_item(TreeItem *p_parent, const Stri } ti->set_text(0, file); - String res_path = "res://" + p_path; - if (FileAccess::exists(res_path)) { - *r_conflicts += 1; - ti->set_custom_color(0, get_theme_color(SNAME("error_color"), EditorStringName(Editor))); - ti->set_tooltip_text(0, vformat(TTR("%s (already exists)"), res_path)); - ti->set_checked(0, false); - ti->propagate_check(0); + return ti; +} + +void EditorAssetInstaller::_update_conflict_status(int p_conflicts) { + if (p_conflicts >= 1) { + asset_conflicts_link->set_text(vformat(TTRN("%d file conflicts with your project and won't be installed", "%d files conflict with your project and won't be installed", p_conflicts), p_conflicts)); + asset_conflicts_link->show(); + asset_conflicts_label->hide(); } else { - ti->set_tooltip_text(0, res_path); + asset_conflicts_link->hide(); + asset_conflicts_label->show(); } +} - ti->set_metadata(0, res_path); +void EditorAssetInstaller::_update_confirm_button() { + TreeItem *root = source_tree->get_root(); + get_ok_button()->set_disabled(!root || (!root->is_checked(0) && !root->is_indeterminate(0))); +} - return ti; +void EditorAssetInstaller::_toggle_source_tree(bool p_visible, bool p_scroll_to_error) { + source_tree_vb->set_visible(p_visible); + show_source_files_button->set_pressed_no_signal(p_visible); // To keep in sync if triggered by something else. + + if (p_visible) { + show_source_files_button->set_icon(get_editor_theme_icon(SNAME("Back"))); + } else { + show_source_files_button->set_icon(get_editor_theme_icon(SNAME("Forward"))); + } + + if (p_visible && p_scroll_to_error && first_file_conflict) { + source_tree->scroll_to_item(first_file_conflict, true); + } } void EditorAssetInstaller::_check_has_toplevel() { @@ -259,12 +423,42 @@ void EditorAssetInstaller::_check_has_toplevel() { toplevel_prefix = first_asset; skip_toplevel_check->set_disabled(false); - skip_toplevel_check->set_tooltip_text(""); + skip_toplevel_check->set_tooltip_text(TTR("Ignore the root directory when extracting files.")); } void EditorAssetInstaller::_set_skip_toplevel(bool p_checked) { + if (skip_toplevel == p_checked) { + return; + } + skip_toplevel = p_checked; - _rebuild_tree(); + _update_file_mappings(); + _update_source_tree(); + _rebuild_destination_tree(); +} + +void EditorAssetInstaller::_open_target_dir_dialog() { + if (!target_dir_dialog) { + target_dir_dialog = memnew(EditorFileDialog); + target_dir_dialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR); + target_dir_dialog->set_title(TTR("Select Install Folder")); + target_dir_dialog->set_current_dir(target_dir_path); + target_dir_dialog->connect("dir_selected", callable_mp(this, &EditorAssetInstaller::_target_dir_selected)); + add_child(target_dir_dialog); + } + + target_dir_dialog->popup_file_dialog(); +} + +void EditorAssetInstaller::_target_dir_selected(const String &p_target_path) { + if (target_dir_path == p_target_path) { + return; + } + + target_dir_path = p_target_path; + _update_file_mappings(); + _update_source_tree(); + _rebuild_destination_tree(); } void EditorAssetInstaller::ok_pressed() { @@ -296,26 +490,25 @@ void EditorAssetInstaller::_install_asset() { } String source_name = String::utf8(fname); - if (!file_item_map.has(source_name) || (!file_item_map[source_name]->is_checked(0) && !file_item_map[source_name]->is_indeterminate(0))) { + if (!_is_item_checked(source_name)) { continue; } - String path = file_item_map[source_name]->get_metadata(0); - if (path.is_empty()) { // Directory. - // TODO: Metadata can be used to store the entire path of directories too, - // so this tree iteration can be avoided. - String dir_path; - TreeItem *t = file_item_map[source_name]; - while (t) { - dir_path = t->get_text(0) + dir_path; - t = t->get_parent(); - } + HashMap<String, String>::Iterator E = mapped_files.find(source_name); + if (!E) { + continue; // No remapped path means we don't want it; most likely the root. + } + + String target_path = target_dir_path.path_join(E->value); - if (dir_path.ends_with("/")) { - dir_path = dir_path.substr(0, dir_path.length() - 1); + Dictionary asset_meta = file_item_map[source_name]->get_metadata(0); + bool is_dir = asset_meta.get("is_dir", false); + if (is_dir) { + if (target_path.ends_with("/")) { + target_path = target_path.substr(0, target_path.length() - 1); } - da->make_dir_recursive(dir_path); + da->make_dir_recursive(target_path); } else { Vector<uint8_t> uncomp_data; uncomp_data.resize(info.uncompressed_size); @@ -325,16 +518,16 @@ void EditorAssetInstaller::_install_asset() { unzCloseCurrentFile(pkg); // Ensure that the target folder exists. - da->make_dir_recursive(path.get_base_dir()); + da->make_dir_recursive(target_path.get_base_dir()); - Ref<FileAccess> f = FileAccess::open(path, FileAccess::WRITE); + Ref<FileAccess> f = FileAccess::open(target_path, FileAccess::WRITE); if (f.is_valid()) { f->store_buffer(uncomp_data.ptr(), uncomp_data.size()); } else { - failed_files.push_back(path); + failed_files.push_back(target_path); } - ProgressDialog::get_singleton()->task_step("uncompress", path, idx); + ProgressDialog::get_singleton()->task_step("uncompress", target_path, idx); } } @@ -358,6 +551,7 @@ void EditorAssetInstaller::_install_asset() { EditorNode::get_singleton()->show_warning(vformat(TTR("Asset \"%s\" installed successfully!"), asset_name), TTR("Success!")); } } + EditorFileSystem::get_singleton()->scan_changes(); } @@ -372,6 +566,13 @@ String EditorAssetInstaller::get_asset_name() const { void EditorAssetInstaller::_notification(int p_what) { switch (p_what) { case NOTIFICATION_THEME_CHANGED: { + if (show_source_files_button->is_pressed()) { + show_source_files_button->set_icon(get_editor_theme_icon(SNAME("Back"))); + } else { + show_source_files_button->set_icon(get_editor_theme_icon(SNAME("Forward"))); + } + asset_conflicts_link->add_theme_color_override("font_color", get_theme_color(SNAME("error_color"), EditorStringName(Editor))); + generic_extension_icon = get_editor_theme_icon(SNAME("Object")); extension_icon_map.clear(); @@ -435,36 +636,98 @@ EditorAssetInstaller::EditorAssetInstaller() { VBoxContainer *vb = memnew(VBoxContainer); add_child(vb); + // Status bar. + HBoxContainer *asset_status = memnew(HBoxContainer); vb->add_child(asset_status); Label *asset_label = memnew(Label); - asset_label->set_text(TTR("Contents of asset:")); + asset_label->set_text(TTR("Asset:")); + asset_label->set_theme_type_variation("HeaderSmall"); asset_status->add_child(asset_label); asset_title_label = memnew(Label); - asset_title_label->set_theme_type_variation("HeaderSmall"); asset_status->add_child(asset_title_label); - asset_status->add_spacer(); + // File remapping controls. - asset_conflicts_label = memnew(Label); - asset_conflicts_label->set_theme_type_variation("HeaderSmall"); - asset_status->add_child(asset_conflicts_label); + HBoxContainer *remapping_tools = memnew(HBoxContainer); + vb->add_child(remapping_tools); + + show_source_files_button = memnew(Button); + show_source_files_button->set_toggle_mode(true); + show_source_files_button->set_tooltip_text(TTR("Open the list of the asset contents and select which files to install.")); + remapping_tools->add_child(show_source_files_button); + show_source_files_button->connect("toggled", callable_mp(this, &EditorAssetInstaller::_toggle_source_tree).bind(false)); + + Button *target_dir_button = memnew(Button); + target_dir_button->set_text(TTR("Change Install Folder")); + target_dir_button->set_tooltip_text(TTR("Change the folder where the contents of the asset are going to be installed.")); + remapping_tools->add_child(target_dir_button); + target_dir_button->connect("pressed", callable_mp(this, &EditorAssetInstaller::_open_target_dir_dialog)); + + remapping_tools->add_child(memnew(VSeparator)); skip_toplevel_check = memnew(CheckBox); - skip_toplevel_check->set_text(TTR("Ignore the root directory when extracting files")); - skip_toplevel_check->set_h_size_flags(Control::SIZE_SHRINK_BEGIN); + skip_toplevel_check->set_text(TTR("Ignore asset root")); + skip_toplevel_check->set_tooltip_text(TTR("Ignore the root directory when extracting files.")); skip_toplevel_check->connect("toggled", callable_mp(this, &EditorAssetInstaller::_set_skip_toplevel)); - vb->add_child(skip_toplevel_check); + remapping_tools->add_child(skip_toplevel_check); - tree = memnew(Tree); - tree->set_v_size_flags(Control::SIZE_EXPAND_FILL); - tree->connect("item_edited", callable_mp(this, &EditorAssetInstaller::_item_checked)); - tree->connect("check_propagated_to_item", callable_mp(this, &EditorAssetInstaller::_check_propagated_to_item)); - vb->add_child(tree); + remapping_tools->add_spacer(); - set_title(TTR("Select Asset Files to Install")); + asset_conflicts_label = memnew(Label); + asset_conflicts_label->set_theme_type_variation("HeaderSmall"); + asset_conflicts_label->set_text(TTR("No files conflict with your project")); + remapping_tools->add_child(asset_conflicts_label); + asset_conflicts_link = memnew(LinkButton); + asset_conflicts_link->set_theme_type_variation("HeaderSmallLink"); + asset_conflicts_link->set_v_size_flags(Control::SIZE_SHRINK_CENTER); + asset_conflicts_link->set_tooltip_text(TTR("Show contents of the asset and conflicting files.")); + asset_conflicts_link->set_visible(false); + remapping_tools->add_child(asset_conflicts_link); + asset_conflicts_link->connect("pressed", callable_mp(this, &EditorAssetInstaller::_toggle_source_tree).bind(true, true)); + + // File hierarchy trees. + + HSplitContainer *tree_split = memnew(HSplitContainer); + tree_split->set_v_size_flags(Control::SIZE_EXPAND_FILL); + vb->add_child(tree_split); + + source_tree_vb = memnew(VBoxContainer); + source_tree_vb->set_h_size_flags(Control::SIZE_EXPAND_FILL); + source_tree_vb->set_visible(show_source_files_button->is_pressed()); + tree_split->add_child(source_tree_vb); + + Label *source_tree_label = memnew(Label); + source_tree_label->set_text(TTR("Contents of the asset:")); + source_tree_label->set_theme_type_variation("HeaderSmall"); + source_tree_vb->add_child(source_tree_label); + + source_tree = memnew(Tree); + source_tree->set_v_size_flags(Control::SIZE_EXPAND_FILL); + source_tree->connect("item_edited", callable_mp(this, &EditorAssetInstaller::_item_checked_cbk)); + source_tree->connect("check_propagated_to_item", callable_mp(this, &EditorAssetInstaller::_check_propagated_to_item)); + source_tree_vb->add_child(source_tree); + + VBoxContainer *destination_tree_vb = memnew(VBoxContainer); + destination_tree_vb->set_h_size_flags(Control::SIZE_EXPAND_FILL); + tree_split->add_child(destination_tree_vb); + + Label *destination_tree_label = memnew(Label); + destination_tree_label->set_text(TTR("Installation preview:")); + destination_tree_label->set_theme_type_variation("HeaderSmall"); + destination_tree_vb->add_child(destination_tree_label); + + destination_tree = memnew(Tree); + destination_tree->set_v_size_flags(Control::SIZE_EXPAND_FILL); + destination_tree->connect("item_edited", callable_mp(this, &EditorAssetInstaller::_item_checked_cbk)); + destination_tree->connect("check_propagated_to_item", callable_mp(this, &EditorAssetInstaller::_check_propagated_to_item)); + destination_tree_vb->add_child(destination_tree); + + // Dialog configuration. + + set_title(TTR("Configure Asset Before Installing")); set_ok_button_text(TTR("Install")); set_hide_on_ok(true); } diff --git a/editor/editor_asset_installer.h b/editor/editor_asset_installer.h index f5e094f8d5..2030715b51 100644 --- a/editor/editor_asset_installer.h +++ b/editor/editor_asset_installer.h @@ -35,37 +35,61 @@ #include "scene/gui/tree.h" class CheckBox; +class EditorFileDialog; class Label; +class LinkButton; class EditorAssetInstaller : public ConfirmationDialog { GDCLASS(EditorAssetInstaller, ConfirmationDialog); - Tree *tree = nullptr; + VBoxContainer *source_tree_vb = nullptr; + Tree *source_tree = nullptr; + Tree *destination_tree = nullptr; Label *asset_title_label = nullptr; Label *asset_conflicts_label = nullptr; + LinkButton *asset_conflicts_link = nullptr; + + Button *show_source_files_button = nullptr; CheckBox *skip_toplevel_check = nullptr; + EditorFileDialog *target_dir_dialog = nullptr; String package_path; String asset_name; HashSet<String> asset_files; + HashMap<String, String> mapped_files; HashMap<String, TreeItem *> file_item_map; + TreeItem *first_file_conflict = nullptr; + Ref<Texture2D> generic_extension_icon; HashMap<String, Ref<Texture2D>> extension_icon_map; - bool updating = false; + bool updating_source = false; String toplevel_prefix; bool skip_toplevel = false; + String target_dir_path = "res://"; void _check_has_toplevel(); void _set_skip_toplevel(bool p_checked); - void _rebuild_tree(); - TreeItem *_create_dir_item(TreeItem *p_parent, const String &p_path, HashMap<String, TreeItem *> &p_item_map); - TreeItem *_create_file_item(TreeItem *p_parent, const String &p_path, int *r_conflicts); + void _open_target_dir_dialog(); + void _target_dir_selected(const String &p_target_path); + + void _update_file_mappings(); + void _rebuild_source_tree(); + void _update_source_tree(); + bool _update_source_item_status(TreeItem *p_item, const String &p_path); + void _rebuild_destination_tree(); + TreeItem *_create_dir_item(Tree *p_tree, TreeItem *p_parent, const String &p_path, HashMap<String, TreeItem *> &p_item_map); + TreeItem *_create_file_item(Tree *p_tree, TreeItem *p_parent, const String &p_path, int *r_conflicts); + + void _update_conflict_status(int p_conflicts); + void _update_confirm_button(); + void _toggle_source_tree(bool p_visible, bool p_scroll_to_error = false); - void _item_checked(); - void _check_propagated_to_item(Object *p_obj, int column); + void _item_checked_cbk(); + void _check_propagated_to_item(Object *p_obj, int p_column); + bool _is_item_checked(const String &p_source_path) const; void _install_asset(); virtual void ok_pressed() override; diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 223e50f557..dab76e9460 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -31,6 +31,7 @@ #include "editor_node.h" #include "core/config/project_settings.h" +#include "core/extension/gdextension_manager.h" #include "core/input/input.h" #include "core/io/config_file.h" #include "core/io/file_access.h" @@ -435,6 +436,11 @@ void EditorNode::_update_from_settings() { #endif // DEBUG_ENABLED } +void EditorNode::_gdextensions_reloaded() { + // In case the developer is inspecting an object that will be changed by the reload. + InspectorDock::get_inspector_singleton()->update_tree(); +} + void EditorNode::_select_default_main_screen_plugin() { if (EDITOR_3D < main_editor_buttons.size() && main_editor_buttons[EDITOR_3D]->is_visible()) { // If the 3D editor is enabled, use this as the default. @@ -714,6 +720,9 @@ void EditorNode::_notification(int p_what) { EditorFileSystem::get_singleton()->scan_changes(); _scan_external_changes(); + + GDExtensionManager *gdextension_manager = GDExtensionManager::get_singleton(); + callable_mp(gdextension_manager, &GDExtensionManager::reload_extensions).call_deferred(); } break; case NOTIFICATION_APPLICATION_FOCUS_OUT: { @@ -3269,7 +3278,7 @@ void EditorNode::remove_extension_editor_plugin(const StringName &p_class_name) EditorPlugin *plugin = singleton->editor_data.get_extension_editor_plugin(p_class_name); remove_editor_plugin(plugin); - memfree(plugin); + memdelete(plugin); singleton->editor_data.remove_extension_editor_plugin(p_class_name); } @@ -6710,6 +6719,7 @@ EditorNode::EditorNode() { EditorUndoRedoManager::get_singleton()->connect("version_changed", callable_mp(this, &EditorNode::_update_undo_redo_allowed)); EditorUndoRedoManager::get_singleton()->connect("history_changed", callable_mp(this, &EditorNode::_update_undo_redo_allowed)); ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &EditorNode::_update_from_settings)); + GDExtensionManager::get_singleton()->connect("extensions_reloaded", callable_mp(this, &EditorNode::_gdextensions_reloaded)); TranslationServer::get_singleton()->set_enabled(false); // Load settings. diff --git a/editor/editor_node.h b/editor/editor_node.h index 5ecb3186e1..72134e283b 100644 --- a/editor/editor_node.h +++ b/editor/editor_node.h @@ -597,6 +597,7 @@ private: void _add_dropped_files_recursive(const Vector<String> &p_files, String to_path); void _update_from_settings(); + void _gdextensions_reloaded(); void _renderer_selected(int); void _update_renderer_color(); diff --git a/editor/editor_properties.cpp b/editor/editor_properties.cpp index 2fa0641132..a197eb9dce 100644 --- a/editor/editor_properties.cpp +++ b/editor/editor_properties.cpp @@ -2818,7 +2818,7 @@ void EditorPropertyNodePath::_node_assign() { } void EditorPropertyNodePath::_node_clear() { - emit_changed(get_edited_property(), NodePath()); + emit_changed(get_edited_property(), Variant()); update_property(); } diff --git a/editor/editor_themes.cpp b/editor/editor_themes.cpp index ca2a2ff1d6..6897052ce3 100644 --- a/editor/editor_themes.cpp +++ b/editor/editor_themes.cpp @@ -1917,6 +1917,10 @@ Ref<Theme> create_editor_theme(const Ref<Theme> p_theme) { theme->set_constant("outline_size", "LinkButton", 0); + theme->set_type_variation("HeaderSmallLink", "LinkButton"); + theme->set_font("font", "HeaderSmallLink", theme->get_font(SNAME("font"), SNAME("HeaderSmall"))); + theme->set_font_size("font_size", "HeaderSmallLink", theme->get_font_size(SNAME("font_size"), SNAME("HeaderSmall"))); + // TooltipPanel + TooltipLabel // TooltipPanel is also used for custom tooltips, while TooltipLabel // is only relevant for default tooltips. diff --git a/editor/filesystem_dock.cpp b/editor/filesystem_dock.cpp index 338376c724..a7a31bb77c 100644 --- a/editor/filesystem_dock.cpp +++ b/editor/filesystem_dock.cpp @@ -1970,15 +1970,15 @@ Vector<String> FileSystemDock::_tree_get_selected(bool remove_self_inclusion) co Vector<String> selected_strings; TreeItem *favorites_item = tree->get_root()->get_first_child(); - TreeItem *active_selected = tree->get_selected(); - if (active_selected && active_selected != favorites_item) { - selected_strings.push_back(active_selected->get_metadata(0)); + TreeItem *cursor_item = tree->get_selected(); + if (cursor_item && cursor_item->is_selected(0) && cursor_item != favorites_item) { + selected_strings.push_back(cursor_item->get_metadata(0)); } TreeItem *selected = tree->get_root(); selected = tree->get_next_selected(selected); while (selected) { - if (selected != active_selected && selected != favorites_item) { + if (selected != cursor_item && selected != favorites_item) { selected_strings.push_back(selected->get_metadata(0)); } selected = tree->get_next_selected(selected); diff --git a/editor/gui/editor_spin_slider.cpp b/editor/gui/editor_spin_slider.cpp index c59822ba55..a1b0ecad6b 100644 --- a/editor/gui/editor_spin_slider.cpp +++ b/editor/gui/editor_spin_slider.cpp @@ -203,7 +203,6 @@ void EditorSpinSlider::_value_input_gui_input(const Ref<InputEvent> &p_event) { Ref<InputEventKey> k = p_event; if (k.is_valid() && k->is_pressed() && !is_read_only()) { double step = get_step(); - double real_step = step; if (step < 1) { double divisor = 1.0 / get_step(); @@ -221,30 +220,22 @@ void EditorSpinSlider::_value_input_gui_input(const Ref<InputEvent> &p_event) { } Key code = k->get_keycode(); - switch (code) { - case Key::UP: { - _evaluate_input_text(); - - double last_value = get_value(); - set_value(last_value + step); - double new_value = get_value(); - - if (new_value < CLAMP(last_value + step, get_min(), get_max())) { - set_value(last_value + real_step); - } - value_input_dirty = true; - set_process_internal(true); - } break; + switch (code) { + case Key::UP: case Key::DOWN: { _evaluate_input_text(); double last_value = get_value(); - set_value(last_value - step); + if (code == Key::DOWN) { + step *= -1; + } + set_value(last_value + step); double new_value = get_value(); - if (new_value > CLAMP(last_value - step, get_min(), get_max())) { - set_value(last_value - real_step); + double clamp_value = CLAMP(new_value, get_min(), get_max()); + if (new_value != clamp_value) { + set_value(clamp_value); } value_input_dirty = true; diff --git a/editor/plugins/bone_map_editor_plugin.cpp b/editor/plugins/bone_map_editor_plugin.cpp index 2c9cff3eff..7d15edef1e 100644 --- a/editor/plugins/bone_map_editor_plugin.cpp +++ b/editor/plugins/bone_map_editor_plugin.cpp @@ -533,6 +533,11 @@ void BoneMapper::_clear_mapping_current_group() { } #ifdef MODULE_REGEX_ENABLED +bool BoneMapper::is_match_with_bone_name(String p_bone_name, String p_word) { + RegEx re = RegEx(p_word); + return !re.search(p_bone_name.to_lower()).is_null(); +} + int BoneMapper::search_bone_by_name(Skeleton3D *p_skeleton, Vector<String> p_picklist, BoneSegregation p_segregation, int p_parent, int p_child, int p_children_count) { // There may be multiple candidates hit by existing the subsidiary bone. // The one with the shortest name is probably the original. @@ -540,7 +545,6 @@ int BoneMapper::search_bone_by_name(Skeleton3D *p_skeleton, Vector<String> p_pic String shortest = ""; for (int word_idx = 0; word_idx < p_picklist.size(); word_idx++) { - RegEx re = RegEx(p_picklist[word_idx]); if (p_child == -1) { Vector<int> bones_to_process = p_parent == -1 ? p_skeleton->get_parentless_bones() : p_skeleton->get_bone_children(p_parent); while (bones_to_process.size() > 0) { @@ -559,7 +563,7 @@ int BoneMapper::search_bone_by_name(Skeleton3D *p_skeleton, Vector<String> p_pic } String bn = skeleton->get_bone_name(idx); - if (!re.search(bn.to_lower()).is_null() && guess_bone_segregation(bn) == p_segregation) { + if (is_match_with_bone_name(bn, p_picklist[word_idx]) && guess_bone_segregation(bn) == p_segregation) { hit_list.push_back(bn); } } @@ -584,7 +588,7 @@ int BoneMapper::search_bone_by_name(Skeleton3D *p_skeleton, Vector<String> p_pic } String bn = skeleton->get_bone_name(idx); - if (!re.search(bn.to_lower()).is_null() && guess_bone_segregation(bn) == p_segregation) { + if (is_match_with_bone_name(bn, p_picklist[word_idx]) && guess_bone_segregation(bn) == p_segregation) { hit_list.push_back(bn); } idx = skeleton->get_bone_parent(idx); @@ -654,6 +658,7 @@ void BoneMapper::auto_mapping_process(Ref<BoneMap> &p_bone_map) { picklist.push_back("pelvis"); picklist.push_back("waist"); picklist.push_back("torso"); + picklist.push_back("spine"); int hips = search_bone_by_name(skeleton, picklist); if (hips == -1) { WARN_PRINT("Auto Mapping couldn't guess Hips. Abort auto mapping."); @@ -704,70 +709,7 @@ void BoneMapper::auto_mapping_process(Ref<BoneMap> &p_bone_map) { bone_idx = -1; search_path.clear(); - // 3. Guess Neck - picklist.push_back("neck"); - picklist.push_back("head"); // For no neck model. - picklist.push_back("face"); // Same above. - int neck = search_bone_by_name(skeleton, picklist, BONE_SEGREGATION_NONE, hips); - picklist.clear(); - - // 4. Guess Head - picklist.push_back("head"); - picklist.push_back("face"); - int head = search_bone_by_name(skeleton, picklist, BONE_SEGREGATION_NONE, neck); - if (head == -1) { - search_path = skeleton->get_bone_children(neck); - if (search_path.size() == 1) { - head = search_path[0]; // Maybe only one child of the Neck is Head. - } - } - if (head == -1) { - if (neck != -1) { - head = neck; // The head animation should have more movement. - neck = -1; - p_bone_map->_set_skeleton_bone_name("Head", skeleton->get_bone_name(head)); - } else { - WARN_PRINT("Auto Mapping couldn't guess Neck or Head."); // Continued for guessing on the other bones. But abort when guessing spines step. - } - } else { - p_bone_map->_set_skeleton_bone_name("Neck", skeleton->get_bone_name(neck)); - p_bone_map->_set_skeleton_bone_name("Head", skeleton->get_bone_name(head)); - } - picklist.clear(); - search_path.clear(); - - int neck_or_head = neck != -1 ? neck : (head != -1 ? head : -1); - if (neck_or_head != -1) { - // 4-1. Guess Eyes - picklist.push_back("eye(?!.*(brow|lash|lid))"); - bone_idx = search_bone_by_name(skeleton, picklist, BONE_SEGREGATION_LEFT, neck_or_head); - if (bone_idx == -1) { - WARN_PRINT("Auto Mapping couldn't guess LeftEye."); - } else { - p_bone_map->_set_skeleton_bone_name("LeftEye", skeleton->get_bone_name(bone_idx)); - } - - bone_idx = search_bone_by_name(skeleton, picklist, BONE_SEGREGATION_RIGHT, neck_or_head); - if (bone_idx == -1) { - WARN_PRINT("Auto Mapping couldn't guess RightEye."); - } else { - p_bone_map->_set_skeleton_bone_name("RightEye", skeleton->get_bone_name(bone_idx)); - } - picklist.clear(); - - // 4-2. Guess Jaw - picklist.push_back("jaw"); - bone_idx = search_bone_by_name(skeleton, picklist, BONE_SEGREGATION_NONE, neck_or_head); - if (bone_idx == -1) { - WARN_PRINT("Auto Mapping couldn't guess Jaw."); - } else { - p_bone_map->_set_skeleton_bone_name("Jaw", skeleton->get_bone_name(bone_idx)); - } - bone_idx = -1; - picklist.clear(); - } - - // 5. Guess Foots + // 3. Guess Foots picklist.push_back("foot"); picklist.push_back("ankle"); int left_foot = search_bone_by_name(skeleton, picklist, BONE_SEGREGATION_LEFT, hips); @@ -784,7 +726,7 @@ void BoneMapper::auto_mapping_process(Ref<BoneMap> &p_bone_map) { } picklist.clear(); - // 5-1. Guess LowerLegs + // 3-1. Guess LowerLegs picklist.push_back("(low|under).*leg"); picklist.push_back("knee"); picklist.push_back("shin"); @@ -810,7 +752,7 @@ void BoneMapper::auto_mapping_process(Ref<BoneMap> &p_bone_map) { } picklist.clear(); - // 5-2. Guess UpperLegs + // 3-2. Guess UpperLegs picklist.push_back("up.*leg"); picklist.push_back("thigh"); picklist.push_back("leg"); @@ -834,7 +776,7 @@ void BoneMapper::auto_mapping_process(Ref<BoneMap> &p_bone_map) { bone_idx = -1; picklist.clear(); - // 5-3. Guess Toes + // 3-3. Guess Toes picklist.push_back("toe"); picklist.push_back("ball"); if (left_foot != -1) { @@ -871,7 +813,7 @@ void BoneMapper::auto_mapping_process(Ref<BoneMap> &p_bone_map) { bone_idx = -1; picklist.clear(); - // 6. Guess Hands + // 4. Guess Hands picklist.push_back("hand"); picklist.push_back("wrist"); picklist.push_back("palm"); @@ -916,7 +858,7 @@ void BoneMapper::auto_mapping_process(Ref<BoneMap> &p_bone_map) { bone_idx = -1; picklist.clear(); - // 6-1. Guess Finger + // 4-1. Guess Finger bool named_finger_is_found = false; LocalVector<String> fingers; fingers.push_back("thumb|pollex"); @@ -1106,7 +1048,7 @@ void BoneMapper::auto_mapping_process(Ref<BoneMap> &p_bone_map) { } } - // 7. Guess Arms + // 5. Guess Arms picklist.push_back("shoulder"); picklist.push_back("clavicle"); picklist.push_back("collar"); @@ -1124,7 +1066,7 @@ void BoneMapper::auto_mapping_process(Ref<BoneMap> &p_bone_map) { } picklist.clear(); - // 7-1. Guess LowerArms + // 5-1. Guess LowerArms picklist.push_back("(low|fore).*arm"); picklist.push_back("elbow"); picklist.push_back("arm"); @@ -1148,7 +1090,7 @@ void BoneMapper::auto_mapping_process(Ref<BoneMap> &p_bone_map) { } picklist.clear(); - // 7-2. Guess UpperArms + // 5-2. Guess UpperArms picklist.push_back("up.*arm"); picklist.push_back("arm"); if (left_shoulder != -1 && left_lower_arm != -1) { @@ -1171,6 +1113,99 @@ void BoneMapper::auto_mapping_process(Ref<BoneMap> &p_bone_map) { bone_idx = -1; picklist.clear(); + // 6. Guess Neck + picklist.push_back("neck"); + picklist.push_back("head"); // For no neck model. + picklist.push_back("face"); // Same above. + int neck = search_bone_by_name(skeleton, picklist, BONE_SEGREGATION_NONE, hips); + picklist.clear(); + if (neck == -1) { + // If it can't expect by name, search child spine of where the right and left shoulders (or hands) cross. + int ls_idx = left_shoulder != -1 ? left_shoulder : (left_hand_or_palm != -1 ? left_hand_or_palm : -1); + int rs_idx = right_shoulder != -1 ? right_shoulder : (right_hand_or_palm != -1 ? right_hand_or_palm : -1); + if (ls_idx != -1 && rs_idx != -1) { + bool detect = false; + while (ls_idx != hips && ls_idx >= 0 && rs_idx != hips && rs_idx >= 0) { + ls_idx = skeleton->get_bone_parent(ls_idx); + rs_idx = skeleton->get_bone_parent(rs_idx); + if (ls_idx == rs_idx) { + detect = true; + break; + } + } + if (detect) { + Vector<int> children = skeleton->get_bone_children(ls_idx); + children.erase(ls_idx); + children.erase(rs_idx); + String word = "spine"; // It would be better to limit the search with "spine" because it could be mistaken with breast, wing and etc... + for (int i = 0; children.size(); i++) { + bone_idx = children[i]; + if (is_match_with_bone_name(skeleton->get_bone_name(bone_idx), word)) { + neck = bone_idx; + break; + }; + } + bone_idx = -1; + } + } + } + + // 7. Guess Head + picklist.push_back("head"); + picklist.push_back("face"); + int head = search_bone_by_name(skeleton, picklist, BONE_SEGREGATION_NONE, neck); + if (head == -1) { + search_path = skeleton->get_bone_children(neck); + if (search_path.size() == 1) { + head = search_path[0]; // Maybe only one child of the Neck is Head. + } + } + if (head == -1) { + if (neck != -1) { + head = neck; // The head animation should have more movement. + neck = -1; + p_bone_map->_set_skeleton_bone_name("Head", skeleton->get_bone_name(head)); + } else { + WARN_PRINT("Auto Mapping couldn't guess Neck or Head."); // Continued for guessing on the other bones. But abort when guessing spines step. + } + } else { + p_bone_map->_set_skeleton_bone_name("Neck", skeleton->get_bone_name(neck)); + p_bone_map->_set_skeleton_bone_name("Head", skeleton->get_bone_name(head)); + } + picklist.clear(); + search_path.clear(); + + int neck_or_head = neck != -1 ? neck : (head != -1 ? head : -1); + if (neck_or_head != -1) { + // 7-1. Guess Eyes + picklist.push_back("eye(?!.*(brow|lash|lid))"); + bone_idx = search_bone_by_name(skeleton, picklist, BONE_SEGREGATION_LEFT, neck_or_head); + if (bone_idx == -1) { + WARN_PRINT("Auto Mapping couldn't guess LeftEye."); + } else { + p_bone_map->_set_skeleton_bone_name("LeftEye", skeleton->get_bone_name(bone_idx)); + } + + bone_idx = search_bone_by_name(skeleton, picklist, BONE_SEGREGATION_RIGHT, neck_or_head); + if (bone_idx == -1) { + WARN_PRINT("Auto Mapping couldn't guess RightEye."); + } else { + p_bone_map->_set_skeleton_bone_name("RightEye", skeleton->get_bone_name(bone_idx)); + } + picklist.clear(); + + // 7-2. Guess Jaw + picklist.push_back("jaw"); + bone_idx = search_bone_by_name(skeleton, picklist, BONE_SEGREGATION_NONE, neck_or_head); + if (bone_idx == -1) { + WARN_PRINT("Auto Mapping couldn't guess Jaw."); + } else { + p_bone_map->_set_skeleton_bone_name("Jaw", skeleton->get_bone_name(bone_idx)); + } + bone_idx = -1; + picklist.clear(); + } + // 8. Guess UpperChest or Chest if (neck_or_head == -1) { return; // Abort. diff --git a/editor/plugins/bone_map_editor_plugin.h b/editor/plugins/bone_map_editor_plugin.h index 7974d241a2..9ff32707c7 100644 --- a/editor/plugins/bone_map_editor_plugin.h +++ b/editor/plugins/bone_map_editor_plugin.h @@ -179,6 +179,7 @@ class BoneMapper : public VBoxContainer { BONE_SEGREGATION_LEFT, BONE_SEGREGATION_RIGHT }; + bool is_match_with_bone_name(String p_bone_name, String p_word); int search_bone_by_name(Skeleton3D *p_skeleton, Vector<String> p_picklist, BoneSegregation p_segregation = BONE_SEGREGATION_NONE, int p_parent = -1, int p_child = -1, int p_children_count = -1); BoneSegregation guess_bone_segregation(String p_bone_name); void auto_mapping_process(Ref<BoneMap> &p_bone_map); diff --git a/editor/plugins/navigation_polygon_editor_plugin.cpp b/editor/plugins/navigation_polygon_editor_plugin.cpp index 957a520d8a..48335f3b94 100644 --- a/editor/plugins/navigation_polygon_editor_plugin.cpp +++ b/editor/plugins/navigation_polygon_editor_plugin.cpp @@ -32,6 +32,8 @@ #include "editor/editor_node.h" #include "editor/editor_undo_redo_manager.h" +#include "scene/2d/navigation_region_2d.h" +#include "scene/gui/dialogs.h" Ref<NavigationPolygon> NavigationPolygonEditor::_ensure_navpoly() const { Ref<NavigationPolygon> navpoly = node->get_navigation_polygon(); @@ -71,7 +73,6 @@ Variant NavigationPolygonEditor::_get_polygon(int p_idx) const { void NavigationPolygonEditor::_set_polygon(int p_idx, const Variant &p_polygon) const { Ref<NavigationPolygon> navpoly = _ensure_navpoly(); navpoly->set_outline(p_idx, p_polygon); - navpoly->make_polygons_from_outlines(); } void NavigationPolygonEditor::_action_add_polygon(const Variant &p_polygon) { @@ -79,8 +80,6 @@ void NavigationPolygonEditor::_action_add_polygon(const Variant &p_polygon) { EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->add_do_method(navpoly.ptr(), "add_outline", p_polygon); undo_redo->add_undo_method(navpoly.ptr(), "remove_outline", navpoly->get_outline_count()); - undo_redo->add_do_method(navpoly.ptr(), "make_polygons_from_outlines"); - undo_redo->add_undo_method(navpoly.ptr(), "make_polygons_from_outlines"); } void NavigationPolygonEditor::_action_remove_polygon(int p_idx) { @@ -88,8 +87,6 @@ void NavigationPolygonEditor::_action_remove_polygon(int p_idx) { EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->add_do_method(navpoly.ptr(), "remove_outline", p_idx); undo_redo->add_undo_method(navpoly.ptr(), "add_outline_at_index", navpoly->get_outline(p_idx), p_idx); - undo_redo->add_do_method(navpoly.ptr(), "make_polygons_from_outlines"); - undo_redo->add_undo_method(navpoly.ptr(), "make_polygons_from_outlines"); } void NavigationPolygonEditor::_action_set_polygon(int p_idx, const Variant &p_previous, const Variant &p_polygon) { @@ -97,8 +94,6 @@ void NavigationPolygonEditor::_action_set_polygon(int p_idx, const Variant &p_pr EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->add_do_method(navpoly.ptr(), "set_outline", p_idx, p_polygon); undo_redo->add_undo_method(navpoly.ptr(), "set_outline", p_idx, p_previous); - undo_redo->add_do_method(navpoly.ptr(), "make_polygons_from_outlines"); - undo_redo->add_undo_method(navpoly.ptr(), "make_polygons_from_outlines"); } bool NavigationPolygonEditor::_has_resource() const { @@ -119,7 +114,84 @@ void NavigationPolygonEditor::_create_resource() { _menu_option(MODE_CREATE); } -NavigationPolygonEditor::NavigationPolygonEditor() {} +NavigationPolygonEditor::NavigationPolygonEditor() { + bake_hbox = memnew(HBoxContainer); + add_child(bake_hbox); + + button_bake = memnew(Button); + button_bake->set_flat(true); + bake_hbox->add_child(button_bake); + button_bake->set_toggle_mode(true); + button_bake->set_text(TTR("Bake NavigationPolygon")); + button_bake->set_tooltip_text(TTR("Bakes the NavigationPolygon by first parsing the scene for source geometry and then creating the navigation polygon vertices and polygons.")); + button_bake->connect("pressed", callable_mp(this, &NavigationPolygonEditor::_bake_pressed)); + + button_reset = memnew(Button); + button_reset->set_flat(true); + bake_hbox->add_child(button_reset); + button_reset->set_text(TTR("Clear NavigationPolygon")); + button_reset->set_tooltip_text(TTR("Clears the internal NavigationPolygon outlines, vertices and polygons.")); + button_reset->connect("pressed", callable_mp(this, &NavigationPolygonEditor::_clear_pressed)); + + bake_info = memnew(Label); + bake_hbox->add_child(bake_info); + + err_dialog = memnew(AcceptDialog); + add_child(err_dialog); + node = nullptr; +} + +void NavigationPolygonEditor::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: { + button_bake->set_icon(get_theme_icon(SNAME("Bake"), SNAME("EditorIcons"))); + button_reset->set_icon(get_theme_icon(SNAME("Reload"), SNAME("EditorIcons"))); + } break; + } +} + +void NavigationPolygonEditor::_bake_pressed() { + button_bake->set_pressed(false); + + ERR_FAIL_NULL(node); + Ref<NavigationPolygon> navigation_polygon = node->get_navigation_polygon(); + if (!navigation_polygon.is_valid()) { + err_dialog->set_text(TTR("A NavigationPolygon resource must be set or created for this node to work.")); + err_dialog->popup_centered(); + return; + } + + node->bake_navigation_polygon(true); + + node->queue_redraw(); +} + +void NavigationPolygonEditor::_clear_pressed() { + if (node) { + if (node->get_navigation_polygon().is_valid()) { + node->get_navigation_polygon()->clear(); + } + } + + button_bake->set_pressed(false); + bake_info->set_text(""); + + if (node) { + node->queue_redraw(); + } +} + +void NavigationPolygonEditor::_update_polygon_editing_state() { + if (!_get_node()) { + return; + } + + if (node != nullptr && node->get_navigation_polygon().is_valid()) { + bake_hbox->show(); + } else { + bake_hbox->hide(); + } +} NavigationPolygonEditorPlugin::NavigationPolygonEditorPlugin() : AbstractPolygon2DEditorPlugin(memnew(NavigationPolygonEditor), "NavigationRegion2D") { diff --git a/editor/plugins/navigation_polygon_editor_plugin.h b/editor/plugins/navigation_polygon_editor_plugin.h index f43c052dd3..f1d0cd8751 100644 --- a/editor/plugins/navigation_polygon_editor_plugin.h +++ b/editor/plugins/navigation_polygon_editor_plugin.h @@ -32,16 +32,38 @@ #define NAVIGATION_POLYGON_EDITOR_PLUGIN_H #include "editor/plugins/abstract_polygon_2d_editor.h" -#include "scene/2d/navigation_region_2d.h" + +#include "editor/editor_plugin.h" + +class AcceptDialog; +class HBoxContainer; +class NavigationPolygon; +class NavigationRegion2D; class NavigationPolygonEditor : public AbstractPolygon2DEditor { + friend class NavigationPolygonEditorPlugin; + GDCLASS(NavigationPolygonEditor, AbstractPolygon2DEditor); NavigationRegion2D *node = nullptr; Ref<NavigationPolygon> _ensure_navpoly() const; + AcceptDialog *err_dialog = nullptr; + + HBoxContainer *bake_hbox = nullptr; + Button *button_bake = nullptr; + Button *button_reset = nullptr; + Label *bake_info = nullptr; + + void _bake_pressed(); + void _clear_pressed(); + + void _update_polygon_editing_state(); + protected: + void _notification(int p_what); + virtual Node2D *_get_node() const override; virtual void _set_node(Node *p_polygon) override; @@ -63,6 +85,8 @@ public: class NavigationPolygonEditorPlugin : public AbstractPolygon2DEditorPlugin { GDCLASS(NavigationPolygonEditorPlugin, AbstractPolygon2DEditorPlugin); + NavigationPolygonEditor *navigation_polygon_editor = nullptr; + public: NavigationPolygonEditorPlugin(); }; diff --git a/editor/plugins/script_text_editor.cpp b/editor/plugins/script_text_editor.cpp index d5ad21e346..dc3f88705b 100644 --- a/editor/plugins/script_text_editor.cpp +++ b/editor/plugins/script_text_editor.cpp @@ -1306,6 +1306,9 @@ void ScriptTextEditor::_edit_option(int p_op) { case EDIT_DUPLICATE_SELECTION: { code_editor->duplicate_selection(); } break; + case EDIT_DUPLICATE_LINES: { + code_editor->get_text_editor()->duplicate_lines(); + } break; case EDIT_TOGGLE_FOLD_LINE: { int previous_line = -1; for (int caret_idx : tx->get_caret_index_edit_order()) { @@ -2173,6 +2176,7 @@ void ScriptTextEditor::_enable_code_editor() { edit_menu->get_popup()->add_separator(); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("ui_text_select_all"), EDIT_SELECT_ALL); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/duplicate_selection"), EDIT_DUPLICATE_SELECTION); + edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/duplicate_lines"), EDIT_DUPLICATE_LINES); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/evaluate_selection"), EDIT_EVALUATE); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_word_wrap"), EDIT_TOGGLE_WORD_WRAP); edit_menu->get_popup()->add_separator(); @@ -2395,6 +2399,8 @@ void ScriptTextEditor::register_editor() { ED_SHORTCUT("script_text_editor/unfold_all_lines", TTR("Unfold All Lines"), Key::NONE); ED_SHORTCUT("script_text_editor/duplicate_selection", TTR("Duplicate Selection"), KeyModifierMask::SHIFT | KeyModifierMask::CTRL | Key::D); ED_SHORTCUT_OVERRIDE("script_text_editor/duplicate_selection", "macos", KeyModifierMask::SHIFT | KeyModifierMask::META | Key::C); + ED_SHORTCUT("script_text_editor/duplicate_lines", TTR("Duplicate Lines"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::DOWN); + ED_SHORTCUT_OVERRIDE("script_text_editor/duplicate_lines", "macos", KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::DOWN); ED_SHORTCUT("script_text_editor/evaluate_selection", TTR("Evaluate Selection"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::E); ED_SHORTCUT("script_text_editor/toggle_word_wrap", TTR("Toggle Word Wrap"), KeyModifierMask::ALT | Key::Z); ED_SHORTCUT("script_text_editor/trim_trailing_whitespace", TTR("Trim Trailing Whitespace"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::T); diff --git a/editor/plugins/script_text_editor.h b/editor/plugins/script_text_editor.h index 0efe7d54e3..8c0ba6d7e6 100644 --- a/editor/plugins/script_text_editor.h +++ b/editor/plugins/script_text_editor.h @@ -126,6 +126,7 @@ class ScriptTextEditor : public ScriptEditorBase { EDIT_UNINDENT, EDIT_DELETE_LINE, EDIT_DUPLICATE_SELECTION, + EDIT_DUPLICATE_LINES, EDIT_PICK_COLOR, EDIT_TO_UPPERCASE, EDIT_TO_LOWERCASE, diff --git a/editor/plugins/text_editor.cpp b/editor/plugins/text_editor.cpp index 92ee468bc2..b0a69cfb5c 100644 --- a/editor/plugins/text_editor.cpp +++ b/editor/plugins/text_editor.cpp @@ -392,6 +392,9 @@ void TextEditor::_edit_option(int p_op) { case EDIT_DUPLICATE_SELECTION: { code_editor->duplicate_selection(); } break; + case EDIT_DUPLICATE_LINES: { + code_editor->get_text_editor()->duplicate_lines(); + } break; case EDIT_TOGGLE_FOLD_LINE: { int previous_line = -1; for (int caret_idx : tx->get_caret_index_edit_order()) { @@ -651,6 +654,7 @@ TextEditor::TextEditor() { edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/unfold_all_lines"), EDIT_UNFOLD_ALL_LINES); edit_menu->get_popup()->add_separator(); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/duplicate_selection"), EDIT_DUPLICATE_SELECTION); + edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/duplicate_lines"), EDIT_DUPLICATE_LINES); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_word_wrap"), EDIT_TOGGLE_WORD_WRAP); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/trim_trailing_whitespace"), EDIT_TRIM_TRAILING_WHITESAPCE); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/convert_indent_to_spaces"), EDIT_CONVERT_INDENT_TO_SPACES); diff --git a/editor/plugins/text_editor.h b/editor/plugins/text_editor.h index d1aa2a9047..a4ec2d882b 100644 --- a/editor/plugins/text_editor.h +++ b/editor/plugins/text_editor.h @@ -71,6 +71,7 @@ private: EDIT_UNINDENT, EDIT_DELETE_LINE, EDIT_DUPLICATE_SELECTION, + EDIT_DUPLICATE_LINES, EDIT_TO_UPPERCASE, EDIT_TO_LOWERCASE, EDIT_CAPITALIZE, diff --git a/editor/plugins/text_shader_editor.cpp b/editor/plugins/text_shader_editor.cpp index b68b283a61..27f42608c6 100644 --- a/editor/plugins/text_shader_editor.cpp +++ b/editor/plugins/text_shader_editor.cpp @@ -671,6 +671,9 @@ void TextShaderEditor::_menu_option(int p_option) { case EDIT_DUPLICATE_SELECTION: { shader_editor->duplicate_selection(); } break; + case EDIT_DUPLICATE_LINES: { + shader_editor->get_text_editor()->duplicate_lines(); + } break; case EDIT_TOGGLE_WORD_WRAP: { TextEdit::LineWrappingMode wrap = shader_editor->get_text_editor()->get_line_wrapping_mode(); shader_editor->get_text_editor()->set_line_wrapping_mode(wrap == TextEdit::LINE_WRAPPING_BOUNDARY ? TextEdit::LINE_WRAPPING_NONE : TextEdit::LINE_WRAPPING_BOUNDARY); @@ -1122,6 +1125,7 @@ TextShaderEditor::TextShaderEditor() { edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/delete_line"), EDIT_DELETE_LINE); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_comment"), EDIT_TOGGLE_COMMENT); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/duplicate_selection"), EDIT_DUPLICATE_SELECTION); + edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/duplicate_lines"), EDIT_DUPLICATE_LINES); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_word_wrap"), EDIT_TOGGLE_WORD_WRAP); edit_menu->get_popup()->add_separator(); edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("ui_text_completion_query"), EDIT_COMPLETE); diff --git a/editor/plugins/text_shader_editor.h b/editor/plugins/text_shader_editor.h index 03827f1b83..eba2ec0bb9 100644 --- a/editor/plugins/text_shader_editor.h +++ b/editor/plugins/text_shader_editor.h @@ -120,6 +120,7 @@ class TextShaderEditor : public MarginContainer { EDIT_UNINDENT, EDIT_DELETE_LINE, EDIT_DUPLICATE_SELECTION, + EDIT_DUPLICATE_LINES, EDIT_TOGGLE_WORD_WRAP, EDIT_TOGGLE_COMMENT, EDIT_COMPLETE, diff --git a/editor/script_create_dialog.cpp b/editor/script_create_dialog.cpp index d4e1048642..0472f48c62 100644 --- a/editor/script_create_dialog.cpp +++ b/editor/script_create_dialog.cpp @@ -526,12 +526,6 @@ void ScriptCreateDialog::_path_changed(const String &p_path) { validation_panel->update(); } -void ScriptCreateDialog::_path_submitted(const String &p_path) { - if (!get_ok_button()->is_disabled()) { - ok_pressed(); - } -} - void ScriptCreateDialog::_update_template_menu() { bool is_language_using_templates = language->is_using_templates(); template_menu->set_disabled(false); @@ -960,6 +954,7 @@ ScriptCreateDialog::ScriptCreateDialog() { file_path->connect("text_changed", callable_mp(this, &ScriptCreateDialog::_path_changed)); file_path->set_h_size_flags(Control::SIZE_EXPAND_FILL); hb->add_child(file_path); + register_text_enter(file_path); path_button = memnew(Button); path_button->connect("pressed", callable_mp(this, &ScriptCreateDialog::_browse_path).bind(false, true)); hb->add_child(path_button); @@ -973,7 +968,7 @@ ScriptCreateDialog::ScriptCreateDialog() { built_in_name = memnew(LineEdit); built_in_name->set_h_size_flags(Control::SIZE_EXPAND_FILL); - built_in_name->connect("text_submitted", callable_mp(this, &ScriptCreateDialog::_path_submitted)); + register_text_enter(built_in_name); label = memnew(Label(TTR("Name:"))); gc->add_child(label); gc->add_child(built_in_name); diff --git a/editor/script_create_dialog.h b/editor/script_create_dialog.h index a4ff602eec..66301ea3b9 100644 --- a/editor/script_create_dialog.h +++ b/editor/script_create_dialog.h @@ -97,7 +97,6 @@ class ScriptCreateDialog : public ConfirmationDialog { void _path_hbox_sorted(); bool _can_be_built_in(); void _path_changed(const String &p_path = String()); - void _path_submitted(const String &p_path = String()); void _language_changed(int l = 0); void _built_in_pressed(); void _use_template_pressed(); |
