From e37594824fe00eb6aab05d2c7dfdad6c9d9ff28e Mon Sep 17 00:00:00 2001 From: Bojidar Marinov Date: Sat, 20 Jul 2019 17:55:02 +0300 Subject: Improve touchpad support in 2d editor viewport Implements ideas from #30615 --- editor/plugins/canvas_item_editor_plugin.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'editor') diff --git a/editor/plugins/canvas_item_editor_plugin.cpp b/editor/plugins/canvas_item_editor_plugin.cpp index 915fc5ba4c..a1810bf989 100644 --- a/editor/plugins/canvas_item_editor_plugin.cpp +++ b/editor/plugins/canvas_item_editor_plugin.cpp @@ -1061,9 +1061,11 @@ bool CanvasItemEditor::_gui_input_rulers_and_guides(const Ref &p_eve bool CanvasItemEditor::_gui_input_zoom_or_pan(const Ref &p_event) { Ref b = p_event; if (b.is_valid()) { + bool pan_on_scroll = bool(EditorSettings::get_singleton()->get("editors/2d/scroll_to_pan")) && !b->get_control(); + if (b->is_pressed() && b->get_button_index() == BUTTON_WHEEL_DOWN) { // Scroll or pan down - if (bool(EditorSettings::get_singleton()->get("editors/2d/scroll_to_pan"))) { + if (pan_on_scroll) { view_offset.y += int(EditorSettings::get_singleton()->get("editors/2d/pan_speed")) / zoom * b->get_factor(); update_viewport(); } else { @@ -1074,7 +1076,7 @@ bool CanvasItemEditor::_gui_input_zoom_or_pan(const Ref &p_event) { if (b->is_pressed() && b->get_button_index() == BUTTON_WHEEL_UP) { // Scroll or pan up - if (bool(EditorSettings::get_singleton()->get("editors/2d/scroll_to_pan"))) { + if (pan_on_scroll) { view_offset.y -= int(EditorSettings::get_singleton()->get("editors/2d/pan_speed")) / zoom * b->get_factor(); update_viewport(); } else { @@ -1085,7 +1087,7 @@ bool CanvasItemEditor::_gui_input_zoom_or_pan(const Ref &p_event) { if (b->is_pressed() && b->get_button_index() == BUTTON_WHEEL_LEFT) { // Pan left - if (bool(EditorSettings::get_singleton()->get("editors/2d/scroll_to_pan"))) { + if (pan_on_scroll) { view_offset.x -= int(EditorSettings::get_singleton()->get("editors/2d/pan_speed")) / zoom * b->get_factor(); update_viewport(); return true; @@ -1094,7 +1096,7 @@ bool CanvasItemEditor::_gui_input_zoom_or_pan(const Ref &p_event) { if (b->is_pressed() && b->get_button_index() == BUTTON_WHEEL_RIGHT) { // Pan right - if (bool(EditorSettings::get_singleton()->get("editors/2d/scroll_to_pan"))) { + if (pan_on_scroll) { view_offset.x += int(EditorSettings::get_singleton()->get("editors/2d/pan_speed")) / zoom * b->get_factor(); update_viewport(); return true; @@ -1104,6 +1106,7 @@ bool CanvasItemEditor::_gui_input_zoom_or_pan(const Ref &p_event) { if (!panning) { if (b->is_pressed() && (b->get_button_index() == BUTTON_MIDDLE || + b->get_button_index() == BUTTON_RIGHT || (b->get_button_index() == BUTTON_LEFT && tool == TOOL_PAN) || (b->get_button_index() == BUTTON_LEFT && !EditorSettings::get_singleton()->get("editors/2d/simple_panning") && pan_pressed))) { // Pan the viewport -- cgit v1.2.3 From d3652887df0bbe5876dd7b64e741b3c5b14e0cad Mon Sep 17 00:00:00 2001 From: Marc Gilleron Date: Fri, 19 Jul 2019 22:37:45 +0100 Subject: Project manager improvements - Faster launch time by loading icons in a coroutine - Faster sorting, filtering, fav'ing etc - Refactored project list with a proper structured class --- editor/project_manager.cpp | 1382 +++++++++++++++++++++++++++----------------- editor/project_manager.h | 13 +- 2 files changed, 852 insertions(+), 543 deletions(-) (limited to 'editor') diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp index e013aae164..48a587d4db 100644 --- a/editor/project_manager.cpp +++ b/editor/project_manager.cpp @@ -52,6 +52,10 @@ #include "scene/gui/texture_rect.h" #include "scene/gui/tool_button.h" +static inline String get_project_key_from_path(const String &dir) { + return dir.replace("/", "::"); +} + class ProjectDialog : public ConfirmationDialog { GDCLASS(ProjectDialog, ConfirmationDialog); @@ -606,7 +610,7 @@ private: dir = dir.replace("\\", "/"); if (dir.ends_with("/")) dir = dir.substr(0, dir.length() - 1); - String proj = dir.replace("/", "::"); + String proj = get_project_key_from_path(dir); EditorSettings::get_singleton()->set("projects/" + proj, dir); EditorSettings::get_singleton()->save(); @@ -918,596 +922,960 @@ public: } }; -struct ProjectItem { - String project; - String project_name; - String path; - String conf; - String icon; - String main_scene; - uint64_t last_modified; - bool favorite; - bool grayed; - ProjectListFilter::FilterOption filter_order_option; - ProjectItem() {} - ProjectItem(const String &p_project, const String &p_name, const String &p_path, const String &p_conf, const String &p_icon, const String &p_main_scene, uint64_t p_last_modified, bool p_favorite = false, bool p_grayed = false, const ProjectListFilter::FilterOption p_filter_order_option = ProjectListFilter::FILTER_NAME) { - project = p_project; - project_name = p_name; - path = p_path; - conf = p_conf; - icon = p_icon; - main_scene = p_main_scene; - last_modified = p_last_modified; - favorite = p_favorite; - grayed = p_grayed; - filter_order_option = p_filter_order_option; - } - _FORCE_INLINE_ bool operator<(const ProjectItem &l) const { - switch (filter_order_option) { +class ProjectListItemControl : public HBoxContainer { + GDCLASS(ProjectListItemControl, HBoxContainer) +public: + TextureButton *favorite_button; + TextureRect *icon; + bool icon_needs_reload; + + ProjectListItemControl() { + favorite_button = NULL; + icon = NULL; + icon_needs_reload = true; + } + + void set_is_favorite(bool fav) { + favorite_button->set_modulate(fav ? Color(1, 1, 1, 1) : Color(1, 1, 1, 0.2)); + } +}; + +class ProjectList : public ScrollContainer { + GDCLASS(ProjectList, ScrollContainer) +public: + static const char *SIGNAL_SELECTION_CHANGED; + static const char *SIGNAL_PROJECT_ASK_OPEN; + + // Can often be passed by copy + struct Item { + String project_key; + String project_name; + String path; + String icon; + String main_scene; + uint64_t last_modified; + bool favorite; + bool grayed; + bool missing; + int version; + + ProjectListItemControl *control; + + Item() {} + + Item(const String &p_project, + const String &p_name, + const String &p_path, + const String &p_icon, + const String &p_main_scene, + uint64_t p_last_modified, + bool p_favorite, + bool p_grayed, + bool p_missing, + int p_version) { + + project_key = p_project; + project_name = p_name; + path = p_path; + icon = p_icon; + main_scene = p_main_scene; + last_modified = p_last_modified; + favorite = p_favorite; + grayed = p_grayed; + missing = p_missing; + version = p_version; + control = NULL; + } + + _FORCE_INLINE_ bool operator==(const Item &l) const { + return project_key == l.project_key; + } + }; + + ProjectList(); + ~ProjectList(); + + void load_projects(); + void set_search_term(String p_search_term); + void set_filter_option(ProjectListFilter::FilterOption p_option); + void set_order_option(ProjectListFilter::FilterOption p_option); + void sort_projects(); + int get_project_count() const; + void select_project(int p_index); + void erase_selected_projects(); + Vector get_selected_projects() const; + const Set &get_selected_project_keys() const; + void ensure_project_visible(int p_index); + int get_single_selected_index() const; + bool is_any_project_missing() const; + void erase_missing_projects(); + int refresh_project(const String &dir_path); + +private: + static void _bind_methods(); + void _notification(int p_what); + + void _panel_draw(Node *p_hb); + void _panel_input(const Ref &p_ev, Node *p_hb); + void _favorite_pressed(Node *p_hb); + void _show_project(const String &p_path); + + void select_range(int p_begin, int p_end); + void toggle_select(int p_index); + void create_project_item_control(int p_index); + void remove_project(int p_index, bool p_update_settings); + void update_icons_async(); + void load_project_icon(int p_index); + + static void load_project_data(const String &p_property_key, Item &p_item, bool p_favorite); + + String _search_term; + ProjectListFilter::FilterOption _filter_option; + ProjectListFilter::FilterOption _order_option; + Set _selected_project_keys; + String _last_clicked; // Project key + VBoxContainer *_scroll_children; + int _icon_load_index; + + Vector _projects; +}; + +struct ProjectListComparator { + ProjectListFilter::FilterOption order_option; + + // operator< + _FORCE_INLINE_ bool operator()(const ProjectList::Item &a, const ProjectList::Item &b) const { + if (a.favorite && !b.favorite) { + return true; + } + if (b.favorite && !a.favorite) { + return false; + } + switch (order_option) { case ProjectListFilter::FILTER_PATH: - return project < l.project; + return a.project_key < b.project_key; case ProjectListFilter::FILTER_MODIFIED: - return last_modified > l.last_modified; + return a.last_modified > b.last_modified; default: - return project_name < l.project_name; + return a.project_name < b.project_name; } } - _FORCE_INLINE_ bool operator==(const ProjectItem &l) const { return project == l.project; } }; -void ProjectManager::_notification(int p_what) { +ProjectList::ProjectList() { + _filter_option = ProjectListFilter::FILTER_NAME; + _order_option = ProjectListFilter::FILTER_MODIFIED; - switch (p_what) { - case NOTIFICATION_ENTER_TREE: { + _scroll_children = memnew(VBoxContainer); + _scroll_children->set_h_size_flags(SIZE_EXPAND_FILL); + add_child(_scroll_children); - Engine::get_singleton()->set_editor_hint(false); - } break; - case NOTIFICATION_READY: { + _icon_load_index = 0; +} - if (scroll_children->get_child_count() == 0 && StreamPeerSSL::is_available()) - open_templates->popup_centered_minsize(); - } break; - case NOTIFICATION_VISIBILITY_CHANGED: { +ProjectList::~ProjectList() { +} - set_process_unhandled_input(is_visible_in_tree()); - } break; - case NOTIFICATION_WM_QUIT_REQUEST: { +void ProjectList::update_icons_async() { + _icon_load_index = 0; + set_process(true); +} - _dim_window(); - } break; +void ProjectList::_notification(int p_what) { + if (p_what == NOTIFICATION_PROCESS) { + + // Load icons as a coroutine to speed up launch when you have hundreds of projects + if (_icon_load_index < _projects.size()) { + Item &item = _projects.write[_icon_load_index]; + if (item.control->icon_needs_reload) { + load_project_icon(_icon_load_index); + } + _icon_load_index++; + + } else { + set_process(false); + } } } -void ProjectManager::_dim_window() { - - // This method must be called before calling `get_tree()->quit()`. - // Otherwise, its effect won't be visible +void ProjectList::load_project_icon(int p_index) { + Item &item = _projects.write[p_index]; + + Ref default_icon = get_icon("DefaultProjectIcon", "EditorIcons"); + Ref icon; + if (item.icon != "") { + Ref img; + img.instance(); + Error err = img->load(item.icon.replace_first("res://", item.path + "/")); + if (err == OK) { + + img->resize(default_icon->get_width(), default_icon->get_height()); + Ref it = memnew(ImageTexture); + it->create_from_image(img); + icon = it; + } + } + if (icon.is_null()) { + icon = default_icon; + } - // Dim the project manager window while it's quitting to make it clearer that it's busy. - // No transition is applied, as the effect needs to be visible immediately - float c = 1.0f - float(EDITOR_GET("interface/editor/dim_amount")); - Color dim_color = Color(c, c, c); - gui_base->set_modulate(dim_color); + item.control->icon->set_texture(icon); + item.control->icon_needs_reload = false; } -void ProjectManager::_panel_draw(Node *p_hb) { +void ProjectList::load_project_data(const String &p_property_key, Item &p_item, bool p_favorite) { - HBoxContainer *hb = Object::cast_to(p_hb); + String path = EditorSettings::get_singleton()->get(p_property_key); + String conf = path.plus_file("project.godot"); + bool grayed = false; + bool missing = false; - hb->draw_line(Point2(0, hb->get_size().y + 1), Point2(hb->get_size().x - 10, hb->get_size().y + 1), get_color("guide_color", "Tree")); + Ref cf = memnew(ConfigFile); + Error cf_err = cf->load(conf); - if (selected_list.has(hb->get_meta("name"))) { - hb->draw_style_box(gui_base->get_stylebox("selected", "Tree"), Rect2(Point2(), hb->get_size() - Size2(10, 0) * EDSCALE)); + int config_version = 0; + String project_name = TTR("Unnamed Project"); + if (cf_err == OK) { + String cf_project_name = static_cast(cf->get_value("application", "config/name", "")); + if (cf_project_name != "") + project_name = cf_project_name.xml_unescape(); + config_version = (int)cf->get_value("", "config_version", 0); } + + if (config_version > ProjectSettings::CONFIG_VERSION) { + // Comes from an incompatible (more recent) Godot version, grey it out + grayed = true; + } + + String icon = cf->get_value("application", "config/icon", ""); + String main_scene = cf->get_value("application", "run/main_scene", ""); + + uint64_t last_modified = 0; + if (FileAccess::exists(conf)) { + last_modified = FileAccess::get_modified_time(conf); + + String fscache = path.plus_file(".fscache"); + if (FileAccess::exists(fscache)) { + uint64_t cache_modified = FileAccess::get_modified_time(fscache); + if (cache_modified > last_modified) + last_modified = cache_modified; + } + } else { + grayed = true; + missing = true; + print_line("Project is missing: " + conf); + } + + String project_key = p_property_key.get_slice("/", 1); + + p_item = Item(project_key, project_name, path, icon, main_scene, last_modified, p_favorite, grayed, missing, config_version); } -void ProjectManager::_update_project_buttons() { - for (int i = 0; i < scroll_children->get_child_count(); i++) { +void ProjectList::load_projects() { + // This is a full, hard reload of the list. Don't call this unless really required, it's expensive. + // If you have 150 projects, it may read through 150 files on your disk at once + load 150 icons. - CanvasItem *item = Object::cast_to(scroll_children->get_child(i)); - item->update(); + // Clear whole list + for (int i = 0; i < _projects.size(); ++i) { + Item &project = _projects.write[i]; + CRASH_COND(project.control == NULL); + memdelete(project.control); // Why not queue_free()? } + _projects.clear(); + _last_clicked = ""; + _selected_project_keys.clear(); - bool empty_selection = selected_list.empty(); - erase_btn->set_disabled(empty_selection); - open_btn->set_disabled(empty_selection); - rename_btn->set_disabled(empty_selection); - run_btn->set_disabled(empty_selection); + // Load data + // TODO Would be nice to change how projects and favourites are stored... it complicates things a bit. + // Use a dictionary associating project path to metadata (like is_favorite). + + List properties; + EditorSettings::get_singleton()->get_property_list(&properties); - bool missing_projects = false; - Map list_all_projects; - for (int i = 0; i < scroll_children->get_child_count(); i++) { - HBoxContainer *hb = Object::cast_to(scroll_children->get_child(i)); - if (hb) { - list_all_projects.insert(hb->get_meta("name"), hb->get_meta("main_scene")); + Set favorites; + // Find favourites... + for (List::Element *E = properties.front(); E; E = E->next()) { + String property_key = E->get().name; + if (property_key.begins_with("favorite_projects/")) { + favorites.insert(property_key); } } - for (Map::Element *E = list_all_projects.front(); E; E = E->next()) { - String project_name = E->key().replace(":::", ":/").replace("::", "/") + "/project.godot"; - if (!FileAccess::exists(project_name)) { - missing_projects = true; - break; - } + + for (List::Element *E = properties.front(); E; E = E->next()) { + // This is actually something like "projects/C:::Documents::Godot::Projects::MyGame" + String property_key = E->get().name; + if (!property_key.begins_with("projects/")) + continue; + + String project_key = property_key.get_slice("/", 1); + bool favorite = favorites.has("favorite_projects/" + project_key); + + Item item; + load_project_data(property_key, item, favorite); + + _projects.push_back(item); + } + + // Create controls + for (int i = 0; i < _projects.size(); ++i) { + create_project_item_control(i); } - erase_missing_btn->set_visible(missing_projects); + sort_projects(); + + set_v_scroll(0); + + update_icons_async(); } -void ProjectManager::_panel_input(const Ref &p_ev, Node *p_hb) { +void ProjectList::create_project_item_control(int p_index) { - Ref mb = p_ev; + // Will be added last in the list, so make sure indexes match + ERR_FAIL_COND(p_index != _scroll_children->get_child_count()); - if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == BUTTON_LEFT) { + Item &item = _projects.write[p_index]; + ERR_FAIL_COND(item.control != NULL); // Already created - String clicked = p_hb->get_meta("name"); - String clicked_main_scene = p_hb->get_meta("main_scene"); + Ref favorite_icon = get_icon("Favorites", "EditorIcons"); + Color font_color = get_color("font_color", "Tree"); + + ProjectListItemControl *hb = memnew(ProjectListItemControl); + hb->connect("draw", this, "_panel_draw", varray(hb)); + hb->connect("gui_input", this, "_panel_input", varray(hb)); + hb->add_constant_override("separation", 10 * EDSCALE); + + VBoxContainer *favorite_box = memnew(VBoxContainer); + favorite_box->set_name("FavoriteBox"); + TextureButton *favorite = memnew(TextureButton); + favorite->set_name("FavoriteButton"); + favorite->set_normal_texture(favorite_icon); + favorite->connect("pressed", this, "_favorite_pressed", varray(hb)); + favorite_box->add_child(favorite); + favorite_box->set_alignment(BoxContainer::ALIGN_CENTER); + hb->add_child(favorite_box); + hb->favorite_button = favorite; + hb->set_is_favorite(item.favorite); + + TextureRect *tf = memnew(TextureRect); + tf->set_texture(get_icon("DefaultProjectIcon", "EditorIcons")); + hb->add_child(tf); + hb->icon = tf; - if (mb->get_shift() && selected_list.size() > 0 && last_clicked != "" && clicked != last_clicked) { + VBoxContainer *vb = memnew(VBoxContainer); + if (item.grayed) + vb->set_modulate(Color(0.5, 0.5, 0.5)); + vb->set_h_size_flags(SIZE_EXPAND_FILL); + hb->add_child(vb); + Control *ec = memnew(Control); + ec->set_custom_minimum_size(Size2(0, 1)); + ec->set_mouse_filter(MOUSE_FILTER_PASS); + vb->add_child(ec); + Label *title = memnew(Label(item.project_name)); + title->add_font_override("font", get_font("title", "EditorFonts")); + title->add_color_override("font_color", font_color); + title->set_clip_text(true); + vb->add_child(title); + + HBoxContainer *path_hb = memnew(HBoxContainer); + path_hb->set_h_size_flags(SIZE_EXPAND_FILL); + vb->add_child(path_hb); + + Button *show = memnew(Button); + show->set_icon(get_icon("Load", "EditorIcons")); // Folder icon + show->set_flat(true); + show->set_modulate(Color(1, 1, 1, 0.5)); + path_hb->add_child(show); + show->connect("pressed", this, "_show_project", varray(item.path)); + show->set_tooltip(TTR("Show in File Manager")); + + Label *fpath = memnew(Label(item.path)); + path_hb->add_child(fpath); + fpath->set_h_size_flags(SIZE_EXPAND_FILL); + fpath->set_modulate(Color(1, 1, 1, 0.5)); + fpath->add_color_override("font_color", font_color); + fpath->set_clip_text(true); + + _scroll_children->add_child(hb); + item.control = hb; +} - int clicked_id = -1; - int last_clicked_id = -1; - for (int i = 0; i < scroll_children->get_child_count(); i++) { - HBoxContainer *hb = Object::cast_to(scroll_children->get_child(i)); - if (!hb) continue; - if (hb->get_meta("name") == clicked) clicked_id = i; - if (hb->get_meta("name") == last_clicked) last_clicked_id = i; - } +void ProjectList::set_search_term(String p_search_term) { + _search_term = p_search_term; +} - if (last_clicked_id != -1 && clicked_id != -1) { - int min = clicked_id < last_clicked_id ? clicked_id : last_clicked_id; - int max = clicked_id > last_clicked_id ? clicked_id : last_clicked_id; - for (int i = 0; i < scroll_children->get_child_count(); ++i) { - HBoxContainer *hb = Object::cast_to(scroll_children->get_child(i)); - if (!hb) continue; - if (i != clicked_id && (i < min || i > max) && !mb->get_control()) { - selected_list.erase(hb->get_meta("name")); - } else if (i >= min && i <= max) { - selected_list.insert(hb->get_meta("name"), hb->get_meta("main_scene")); - } - } - } +void ProjectList::set_filter_option(ProjectListFilter::FilterOption p_option) { + if (_filter_option != p_option) { + _filter_option = p_option; + } +} + +void ProjectList::set_order_option(ProjectListFilter::FilterOption p_option) { + if (_order_option != p_option) { + _order_option = p_option; + EditorSettings::get_singleton()->set("project_manager/sorting_order", (int)_filter_option); + EditorSettings::get_singleton()->save(); + } +} - } else if (selected_list.has(clicked) && mb->get_control()) { +void ProjectList::sort_projects() { - selected_list.erase(clicked); + SortArray sorter; + sorter.compare.order_option = _order_option; + sorter.sort(_projects.ptrw(), _projects.size()); - } else { + for (int i = 0; i < _projects.size(); ++i) { + Item &item = _projects.write[i]; - last_clicked = clicked; - if (mb->get_control() || selected_list.size() == 0) { - selected_list.insert(clicked, clicked_main_scene); - } else { - selected_list.clear(); - selected_list.insert(clicked, clicked_main_scene); + bool visible = true; + if (_search_term != "") { + if (_filter_option == ProjectListFilter::FILTER_PATH) { + visible = item.path.findn(_search_term) != -1; + } else if (_filter_option == ProjectListFilter::FILTER_NAME) { + visible = item.project_name.findn(_search_term) != -1; } } - _update_project_buttons(); + item.control->set_visible(visible); + } - if (mb->is_doubleclick()) - _open_selected_projects_ask(); //open if doubleclicked + for (int i = 0; i < _projects.size(); ++i) { + Item &item = _projects.write[i]; + if (item.control->is_visible()) { + item.control->get_parent()->move_child(item.control, i); + } } + + // Rewind the coroutine because order of projects changed + update_icons_async(); } -void ProjectManager::_unhandled_input(const Ref &p_ev) { +const Set &ProjectList::get_selected_project_keys() const { + // Faster if that's all you need + return _selected_project_keys; +} - Ref k = p_ev; +Vector ProjectList::get_selected_projects() const { + Vector items; + if (_selected_project_keys.size() == 0) { + return items; + } + items.resize(_selected_project_keys.size()); + int j = 0; + for (int i = 0; i < _projects.size(); ++i) { + const Item &item = _projects[i]; + if (_selected_project_keys.has(item.project_key)) { + items.write[j++] = item; + } + } + ERR_FAIL_COND_V(j != items.size(), items); + return items; +} - if (k.is_valid()) { +void ProjectList::ensure_project_visible(int p_index) { + const Item &item = _projects[p_index]; - if (!k->is_pressed()) - return; + int item_top = item.control->get_position().y; + int item_bottom = item.control->get_position().y + item.control->get_size().y; - if (tabs->get_current_tab() != 0) - return; + if (item_top < get_v_scroll()) { + set_v_scroll(item_top); - bool scancode_handled = true; + } else if (item_bottom > get_v_scroll() + get_size().y) { + set_v_scroll(item_bottom - get_size().y); + } +} - switch (k->get_scancode()) { +int ProjectList::get_single_selected_index() const { + if (_selected_project_keys.size() == 0) { + // Default selection + return 0; + } + String key; + if (_selected_project_keys.size() == 1) { + // Only one selected + key = _selected_project_keys.front()->get(); + } else { + // Multiple selected, consider the last clicked one as "main" + key = _last_clicked; + } + for (int i = 0; i < _projects.size(); ++i) { + if (_projects[i].project_key == key) { + return i; + } + } + return 0; +} - case KEY_ENTER: { +void ProjectList::remove_project(int p_index, bool p_update_settings) { + const Item item = _projects[p_index]; // Take a copy - _open_selected_projects_ask(); - } break; - case KEY_DELETE: { + _selected_project_keys.erase(item.project_key); - _erase_project(); - } break; - case KEY_HOME: { + if (_last_clicked == item.project_key) { + _last_clicked = ""; + } - for (int i = 0; i < scroll_children->get_child_count(); i++) { + memdelete(item.control); + _projects.remove(p_index); - HBoxContainer *hb = Object::cast_to(scroll_children->get_child(i)); - if (hb) { - selected_list.clear(); - selected_list.insert(hb->get_meta("name"), hb->get_meta("main_scene")); - scroll->set_v_scroll(0); - _update_project_buttons(); - break; - } - } + if (p_update_settings) { + EditorSettings::get_singleton()->erase("projects/" + item.project_key); + EditorSettings::get_singleton()->erase("favorite_projects/" + item.project_key); + // Not actually saving the file, in case you are doing more changes to settings + } +} - } break; - case KEY_END: { +bool ProjectList::is_any_project_missing() const { + for (int i = 0; i < _projects.size(); ++i) { + if (_projects[i].missing) { + return true; + } + } + return false; +} - for (int i = scroll_children->get_child_count() - 1; i >= 0; i--) { +void ProjectList::erase_missing_projects() { - HBoxContainer *hb = Object::cast_to(scroll_children->get_child(i)); - if (hb) { - selected_list.clear(); - selected_list.insert(hb->get_meta("name"), hb->get_meta("main_scene")); - scroll->set_v_scroll(scroll_children->get_size().y); - _update_project_buttons(); - break; - } - } + if (_projects.empty()) { + return; + } - } break; - case KEY_UP: { + int deleted_count = 0; + int remaining_count = 0; - if (k->get_shift()) - break; + for (int i = 0; i < _projects.size(); ++i) { + const Item &item = _projects[i]; + + if (item.missing) { + remove_project(i, true); + --i; + ++deleted_count; - if (selected_list.size()) { + } else { + ++remaining_count; + } + } - bool found = false; + print_line("Removed " + itos(deleted_count) + " projects from the list, remaining " + itos(remaining_count) + " projects"); - for (int i = scroll_children->get_child_count() - 1; i >= 0; i--) { + EditorSettings::get_singleton()->save(); +} - HBoxContainer *hb = Object::cast_to(scroll_children->get_child(i)); - if (!hb) continue; +int ProjectList::refresh_project(const String &dir_path) { + // Reads editor settings and reloads information about a specific project. + // If it wasn't loaded and should be in the list, it is added (i.e new project). + // If it isn't in the list anymore, it is removed. + // If it is in the list but doesn't exist anymore, it is marked as missing. - String current = hb->get_meta("name"); + String project_key = get_project_key_from_path(dir_path); - if (found) { - selected_list.clear(); - selected_list.insert(current, hb->get_meta("main_scene")); + // Read project manager settings + bool is_favourite = false; + bool should_be_in_list = false; + String property_key = "projects/" + project_key; + { + List properties; + EditorSettings::get_singleton()->get_property_list(&properties); + String favorite_property_key = "favorite_projects/" + project_key; + + bool found = false; + for (List::Element *E = properties.front(); E; E = E->next()) { + String prop = E->get().name; + if (!found && prop == property_key) { + found = true; + } else if (!is_favourite && prop == favorite_property_key) { + is_favourite = true; + } + } - int offset_diff = scroll->get_v_scroll() - hb->get_position().y; + should_be_in_list = found; + } - if (offset_diff > 0) - scroll->set_v_scroll(scroll->get_v_scroll() - offset_diff); + bool was_selected = _selected_project_keys.has(project_key); - _update_project_buttons(); + // Remove item in any case + for (int i = 0; i < _projects.size(); ++i) { + const Item &existing_item = _projects[i]; + if (existing_item.path == dir_path) { + remove_project(i, false); + break; + } + } - break; + int index = -1; + if (should_be_in_list) { + // Recreate it with updated info - } else if (current == selected_list.back()->key()) { + Item item; + load_project_data(property_key, item, is_favourite); - found = true; - } - } + _projects.push_back(item); + create_project_item_control(_projects.size() - 1); - break; + sort_projects(); + + for (int i = 0; i < _projects.size(); ++i) { + if (_projects[i].project_key == project_key) { + if (was_selected) { + select_project(i); + ensure_project_visible(i); } - FALLTHROUGH; + load_project_icon(i); + index = i; + break; } - case KEY_DOWN: { + } + } - if (k->get_shift()) - break; + return index; +} - bool found = selected_list.empty(); +int ProjectList::get_project_count() const { + return _projects.size(); +} - for (int i = 0; i < scroll_children->get_child_count(); i++) { +void ProjectList::select_project(int p_index) { - HBoxContainer *hb = Object::cast_to(scroll_children->get_child(i)); - if (!hb) continue; + Vector previous_selected_items = get_selected_projects(); + _selected_project_keys.clear(); - String current = hb->get_meta("name"); + for (int i = 0; i < previous_selected_items.size(); ++i) { + previous_selected_items[i].control->update(); + } - if (found) { - selected_list.clear(); - selected_list.insert(current, hb->get_meta("main_scene")); + toggle_select(p_index); +} - int last_y_visible = scroll->get_v_scroll() + scroll->get_size().y; - int offset_diff = (hb->get_position().y + hb->get_size().y) - last_y_visible; +inline void sort(int &a, int &b) { + if (a > b) { + int temp = a; + a = b; + b = temp; + } +} - if (offset_diff > 0) - scroll->set_v_scroll(scroll->get_v_scroll() + offset_diff); +void ProjectList::select_range(int p_begin, int p_end) { + sort(p_begin, p_end); + select_project(p_begin); + for (int i = p_begin + 1; i <= p_end; ++i) { + toggle_select(i); + } +} - _update_project_buttons(); +void ProjectList::toggle_select(int p_index) { + Item &item = _projects.write[p_index]; + if (_selected_project_keys.has(item.project_key)) { + _selected_project_keys.erase(item.project_key); + } else { + _selected_project_keys.insert(item.project_key); + } + item.control->update(); +} - break; +void ProjectList::erase_selected_projects() { - } else if (current == selected_list.back()->key()) { + if (_selected_project_keys.size() == 0) { + return; + } - found = true; - } + for (int i = 0; i < _projects.size(); ++i) { + Item &item = _projects.write[i]; + if (_selected_project_keys.has(item.project_key) && item.control->is_visible()) { + + EditorSettings::get_singleton()->erase("projects/" + item.project_key); + EditorSettings::get_singleton()->erase("favorite_projects/" + item.project_key); + + memdelete(item.control); + _projects.remove(i); + --i; + } + } + + EditorSettings::get_singleton()->save(); + + _selected_project_keys.clear(); + _last_clicked = ""; +} + +// Draws selected project highlight +void ProjectList::_panel_draw(Node *p_hb) { + Control *hb = Object::cast_to(p_hb); + + hb->draw_line(Point2(0, hb->get_size().y + 1), Point2(hb->get_size().x - 10, hb->get_size().y + 1), get_color("guide_color", "Tree")); + + String key = _projects[p_hb->get_index()].project_key; + + if (_selected_project_keys.has(key)) { + hb->draw_style_box(get_stylebox("selected", "Tree"), Rect2(Point2(), hb->get_size() - Size2(10, 0) * EDSCALE)); + } +} + +// Input for each item in the list +void ProjectList::_panel_input(const Ref &p_ev, Node *p_hb) { + + Ref mb = p_ev; + int clicked_index = p_hb->get_index(); + const Item &clicked_project = _projects[clicked_index]; + + if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == BUTTON_LEFT) { + + if (mb->get_shift() && _selected_project_keys.size() > 0 && _last_clicked != "" && clicked_project.project_key != _last_clicked) { + + int anchor_index = -1; + for (int i = 0; i < _projects.size(); ++i) { + const Item &p = _projects[i]; + if (p.project_key == _last_clicked) { + anchor_index = p.control->get_index(); + break; } + } + CRASH_COND(anchor_index == -1); + select_range(anchor_index, clicked_index); - } break; - case KEY_F: { - if (k->get_command()) - this->project_filter->search_box->grab_focus(); - else - scancode_handled = false; - } break; - default: { - scancode_handled = false; - } break; + } else if (mb->get_control()) { + toggle_select(clicked_index); + + } else { + _last_clicked = clicked_project.project_key; + select_project(clicked_index); } - if (scancode_handled) { - accept_event(); + emit_signal(SIGNAL_SELECTION_CHANGED); + + if (mb->is_doubleclick()) { + emit_signal(SIGNAL_PROJECT_ASK_OPEN); } } } -void ProjectManager::_favorite_pressed(Node *p_hb) { +void ProjectList::_favorite_pressed(Node *p_hb) { + + ProjectListItemControl *control = Object::cast_to(p_hb); + + int index = control->get_index(); + Item item = _projects.write[index]; // Take copy - String clicked = p_hb->get_meta("name"); - bool favorite = !p_hb->get_meta("favorite"); - String proj = clicked.replace(":::", ":/"); - proj = proj.replace("::", "/"); + item.favorite = !item.favorite; - if (favorite) { - EditorSettings::get_singleton()->set("favorite_projects/" + clicked, proj); + if (item.favorite) { + EditorSettings::get_singleton()->set("favorite_projects/" + item.project_key, item.path); } else { - EditorSettings::get_singleton()->erase("favorite_projects/" + clicked); + EditorSettings::get_singleton()->erase("favorite_projects/" + item.project_key); } EditorSettings::get_singleton()->save(); - call_deferred("_load_recent_projects"); -} -void ProjectManager::_load_recent_projects() { + _projects.write[index] = item; + + control->set_is_favorite(item.favorite); - ProjectListFilter::FilterOption filter_option = project_filter->get_filter_option(); - String search_term = project_filter->get_search_term(); + sort_projects(); - while (scroll_children->get_child_count() > 0) { - memdelete(scroll_children->get_child(0)); + if (item.favorite) { + for (int i = 0; i < _projects.size(); ++i) { + if (_projects[i].project_key == item.project_key) { + ensure_project_visible(i); + break; + } + } } +} - Map selected_list_copy = selected_list; +void ProjectList::_show_project(const String &p_path) { - List properties; - EditorSettings::get_singleton()->get_property_list(&properties); + OS::get_singleton()->shell_open(String("file://") + p_path); +} - Color font_color = gui_base->get_color("font_color", "Tree"); +const char *ProjectList::SIGNAL_SELECTION_CHANGED = "selection_changed"; +const char *ProjectList::SIGNAL_PROJECT_ASK_OPEN = "project_ask_open"; - ProjectListFilter::FilterOption filter_order_option = project_order_filter->get_filter_option(); - EditorSettings::get_singleton()->set("project_manager/sorting_order", (int)filter_order_option); +void ProjectList::_bind_methods() { - List projects; - List favorite_projects; + ClassDB::bind_method("_panel_draw", &ProjectList::_panel_draw); + ClassDB::bind_method("_panel_input", &ProjectList::_panel_input); + ClassDB::bind_method("_favorite_pressed", &ProjectList::_favorite_pressed); + ClassDB::bind_method("_show_project", &ProjectList::_show_project); + //ClassDB::bind_method("_unhandled_input", &ProjectList::_unhandled_input); - for (List::Element *E = properties.front(); E; E = E->next()) { + ADD_SIGNAL(MethodInfo(SIGNAL_SELECTION_CHANGED)); + ADD_SIGNAL(MethodInfo(SIGNAL_PROJECT_ASK_OPEN)); +} - String _name = E->get().name; - if (!_name.begins_with("projects/") && !_name.begins_with("favorite_projects/")) - continue; +void ProjectManager::_notification(int p_what) { - String path = EditorSettings::get_singleton()->get(_name); - if (filter_option == ProjectListFilter::FILTER_PATH && search_term != "" && path.findn(search_term) == -1) - continue; + switch (p_what) { + case NOTIFICATION_ENTER_TREE: { - String project = _name.get_slice("/", 1); - String conf = path.plus_file("project.godot"); - bool favorite = (_name.begins_with("favorite_projects/")) ? true : false; - bool grayed = false; + Engine::get_singleton()->set_editor_hint(false); + } break; + case NOTIFICATION_READY: { - Ref cf = memnew(ConfigFile); - Error cf_err = cf->load(conf); + if (_project_list->get_project_count() == 0 && StreamPeerSSL::is_available()) + open_templates->popup_centered_minsize(); + } break; + case NOTIFICATION_VISIBILITY_CHANGED: { - int config_version = 0; - String project_name = TTR("Unnamed Project"); - if (cf_err == OK) { + set_process_unhandled_input(is_visible_in_tree()); + } break; + case NOTIFICATION_WM_QUIT_REQUEST: { - String cf_project_name = static_cast(cf->get_value("application", "config/name", "")); - if (cf_project_name != "") - project_name = cf_project_name.xml_unescape(); - config_version = (int)cf->get_value("", "config_version", 0); - } + _dim_window(); + } break; + } +} - if (config_version > ProjectSettings::CONFIG_VERSION) { - // Comes from an incompatible (more recent) Godot version, grey it out - grayed = true; - } +void ProjectManager::_dim_window() { - String icon = cf->get_value("application", "config/icon", ""); - String main_scene = cf->get_value("application", "run/main_scene", ""); + // This method must be called before calling `get_tree()->quit()`. + // Otherwise, its effect won't be visible - uint64_t last_modified = 0; - if (FileAccess::exists(conf)) { - last_modified = FileAccess::get_modified_time(conf); + // Dim the project manager window while it's quitting to make it clearer that it's busy. + // No transition is applied, as the effect needs to be visible immediately + float c = 1.0f - float(EDITOR_GET("interface/editor/dim_amount")); + Color dim_color = Color(c, c, c); + gui_base->set_modulate(dim_color); +} - String fscache = path.plus_file(".fscache"); - if (FileAccess::exists(fscache)) { - uint64_t cache_modified = FileAccess::get_modified_time(fscache); - if (cache_modified > last_modified) - last_modified = cache_modified; - } - } else { - grayed = true; - } +void ProjectManager::_update_project_buttons() { - ProjectItem item(project, project_name, path, conf, icon, main_scene, last_modified, favorite, grayed, filter_order_option); - if (favorite) - favorite_projects.push_back(item); - else - projects.push_back(item); - } - projects.sort(); - favorite_projects.sort(); + Vector selected_projects = _project_list->get_selected_projects(); + bool empty_selection = selected_projects.empty(); - for (List::Element *E = projects.front(); E;) { - List::Element *next = E->next(); - if (favorite_projects.find(E->get()) != NULL) - projects.erase(E->get()); - E = next; - } - for (List::Element *E = favorite_projects.back(); E; E = E->prev()) { - projects.push_front(E->get()); - } + erase_btn->set_disabled(empty_selection); + open_btn->set_disabled(empty_selection); + rename_btn->set_disabled(empty_selection); + run_btn->set_disabled(empty_selection); - Ref favorite_icon = get_icon("Favorites", "EditorIcons"); + erase_missing_btn->set_visible(_project_list->is_any_project_missing()); +} - for (List::Element *E = projects.front(); E; E = E->next()) { +void ProjectManager::_unhandled_input(const Ref &p_ev) { - ProjectItem &item = E->get(); - String project = item.project; - String path = item.path; - String conf = item.conf; + Ref k = p_ev; - if (filter_option == ProjectListFilter::FILTER_NAME && search_term != "" && item.project_name.findn(search_term) == -1) - continue; + if (k.is_valid()) { + + if (!k->is_pressed()) + return; + + if (tabs->get_current_tab() != 0) + return; + + bool scancode_handled = true; + + switch (k->get_scancode()) { - Ref icon; + case KEY_ENTER: { - if (item.icon != "") { - Ref img; - img.instance(); - Error err = img->load(item.icon.replace_first("res://", path + "/")); - if (err == OK) { + _open_selected_projects_ask(); + } break; + case KEY_DELETE: { + + _erase_project(); + } break; + case KEY_HOME: { + + if (_project_list->get_project_count() > 0) { + _project_list->select_project(0); + _update_project_buttons(); + } + + } break; + case KEY_END: { - Ref default_icon = get_icon("DefaultProjectIcon", "EditorIcons"); - img->resize(default_icon->get_width(), default_icon->get_height()); - Ref it = memnew(ImageTexture); - it->create_from_image(img); - icon = it; + if (_project_list->get_project_count() > 0) { + _project_list->select_project(_project_list->get_project_count() - 1); + _update_project_buttons(); + } + + } break; + case KEY_UP: { + + if (k->get_shift()) + break; + + int index = _project_list->get_single_selected_index(); + if (index - 1 > 0) { + _project_list->select_project(index - 1); + _project_list->ensure_project_visible(index - 1); + _update_project_buttons(); + } + + break; } + case KEY_DOWN: { + + if (k->get_shift()) + break; + + int index = _project_list->get_single_selected_index(); + if (index + 1 < _project_list->get_project_count()) { + _project_list->select_project(index + 1); + _project_list->ensure_project_visible(index + 1); + _update_project_buttons(); + } + + } break; + case KEY_F: { + if (k->get_command()) + this->project_filter->search_box->grab_focus(); + else + scancode_handled = false; + } break; + default: { + scancode_handled = false; + } break; } - if (icon.is_null()) { - icon = get_icon("DefaultProjectIcon", "EditorIcons"); + if (scancode_handled) { + accept_event(); } + } +} - selected_list_copy.erase(project); - - bool is_favorite = item.favorite; - bool is_grayed = item.grayed; - - HBoxContainer *hb = memnew(HBoxContainer); - hb->set_meta("name", project); - hb->set_meta("main_scene", item.main_scene); - hb->set_meta("favorite", is_favorite); - hb->connect("draw", this, "_panel_draw", varray(hb)); - hb->connect("gui_input", this, "_panel_input", varray(hb)); - hb->add_constant_override("separation", 10 * EDSCALE); - - VBoxContainer *favorite_box = memnew(VBoxContainer); - TextureButton *favorite = memnew(TextureButton); - favorite->set_normal_texture(favorite_icon); - if (!is_favorite) - favorite->set_modulate(Color(1, 1, 1, 0.2)); - favorite->connect("pressed", this, "_favorite_pressed", varray(hb)); - favorite_box->add_child(favorite); - favorite_box->set_alignment(BoxContainer::ALIGN_CENTER); - hb->add_child(favorite_box); - - TextureRect *tf = memnew(TextureRect); - tf->set_texture(icon); - hb->add_child(tf); +void ProjectManager::_load_recent_projects() { - VBoxContainer *vb = memnew(VBoxContainer); - if (is_grayed) - vb->set_modulate(Color(0.5, 0.5, 0.5)); - vb->set_name("project"); - vb->set_h_size_flags(SIZE_EXPAND_FILL); - hb->add_child(vb); - Control *ec = memnew(Control); - ec->set_custom_minimum_size(Size2(0, 1)); - ec->set_mouse_filter(MOUSE_FILTER_PASS); - vb->add_child(ec); - Label *title = memnew(Label(item.project_name)); - title->add_font_override("font", gui_base->get_font("title", "EditorFonts")); - title->add_color_override("font_color", font_color); - title->set_clip_text(true); - vb->add_child(title); - - HBoxContainer *path_hb = memnew(HBoxContainer); - path_hb->set_name("path_box"); - path_hb->set_h_size_flags(SIZE_EXPAND_FILL); - vb->add_child(path_hb); - - Button *show = memnew(Button); - show->set_name("show"); - show->set_icon(get_icon("Filesystem", "EditorIcons")); - show->set_flat(true); - show->set_modulate(Color(1, 1, 1, 0.5)); - path_hb->add_child(show); - show->connect("pressed", this, "_show_project", varray(path)); - show->set_tooltip(TTR("Show in File Manager")); - - Label *fpath = memnew(Label(path)); - fpath->set_name("path"); - path_hb->add_child(fpath); - fpath->set_h_size_flags(SIZE_EXPAND_FILL); - fpath->set_modulate(Color(1, 1, 1, 0.5)); - fpath->add_color_override("font_color", font_color); - fpath->set_clip_text(true); - - scroll_children->add_child(hb); - } - - for (Map::Element *E = selected_list_copy.front(); E; E = E->next()) { - String key = E->key(); - selected_list.erase(key); - } - - scroll->set_v_scroll(0); + _project_list->set_filter_option(project_filter->get_filter_option()); + _project_list->set_order_option(project_order_filter->get_filter_option()); + _project_list->set_search_term(project_filter->get_search_term()); + _project_list->load_projects(); _update_project_buttons(); - EditorSettings::get_singleton()->save(); - tabs->set_current_tab(0); } void ProjectManager::_on_projects_updated() { - _load_recent_projects(); + Vector selected_projects = _project_list->get_selected_projects(); + int index = 0; + for (int i = 0; i < selected_projects.size(); ++i) { + index = _project_list->refresh_project(selected_projects[i].path); + } + if (index != -1) { + _project_list->ensure_project_visible(index); + } } void ProjectManager::_on_project_created(const String &dir) { project_filter->clear(); - bool has_already = false; - for (int i = 0; i < scroll_children->get_child_count(); i++) { - HBoxContainer *hb = Object::cast_to(scroll_children->get_child(i)); - Label *fpath = Object::cast_to