diff options
author | Marc Gilleron <marc.gilleron@gmail.com> | 2019-07-19 22:37:45 +0100 |
---|---|---|
committer | Marc Gilleron <marc.gilleron@gmail.com> | 2019-07-21 20:47:25 +0100 |
commit | d3652887df0bbe5876dd7b64e741b3c5b14e0cad (patch) | |
tree | df45d8cecb3c888774d82560ea2e46a9fc9a3a80 /editor | |
parent | 0bf930c1176ed26155aa352b1f937c8ae043272e (diff) |
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
Diffstat (limited to 'editor')
-rw-r--r-- | editor/project_manager.cpp | 1382 | ||||
-rw-r--r-- | editor/project_manager.h | 13 |
2 files changed, 852 insertions, 543 deletions
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<Item> get_selected_projects() const; + const Set<String> &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<InputEvent> &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<String> _selected_project_keys; + String _last_clicked; // Project key + VBoxContainer *_scroll_children; + int _icon_load_index; + + Vector<Item> _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<Texture> default_icon = get_icon("DefaultProjectIcon", "EditorIcons"); + Ref<Texture> icon; + if (item.icon != "") { + Ref<Image> 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<ImageTexture> 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<HBoxContainer>(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<ConfigFile> 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<String>(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<CanvasItem>(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<PropertyInfo> properties; + EditorSettings::get_singleton()->get_property_list(&properties); - bool missing_projects = false; - Map<String, String> list_all_projects; - for (int i = 0; i < scroll_children->get_child_count(); i++) { - HBoxContainer *hb = Object::cast_to<HBoxContainer>(scroll_children->get_child(i)); - if (hb) { - list_all_projects.insert(hb->get_meta("name"), hb->get_meta("main_scene")); + Set<String> favorites; + // Find favourites... + for (List<PropertyInfo>::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<String, String>::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<PropertyInfo>::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<InputEvent> &p_ev, Node *p_hb) { +void ProjectList::create_project_item_control(int p_index) { - Ref<InputEventMouseButton> 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<Texture> 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<HBoxContainer>(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<HBoxContainer>(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<Item, ProjectListComparator> 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<InputEvent> &p_ev) { +const Set<String> &ProjectList::get_selected_project_keys() const { + // Faster if that's all you need + return _selected_project_keys; +} - Ref<InputEventKey> k = p_ev; +Vector<ProjectList::Item> ProjectList::get_selected_projects() const { + Vector<Item> 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<HBoxContainer>(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<HBoxContainer>(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<HBoxContainer>(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<PropertyInfo> properties; + EditorSettings::get_singleton()->get_property_list(&properties); + String favorite_property_key = "favorite_projects/" + project_key; + + bool found = false; + for (List<PropertyInfo>::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<HBoxContainer>(scroll_children->get_child(i)); - if (!hb) continue; + Vector<Item> 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<Control>(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<InputEvent> &p_ev, Node *p_hb) { + + Ref<InputEventMouseButton> 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<ProjectListItemControl>(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<String, String> selected_list_copy = selected_list; +void ProjectList::_show_project(const String &p_path) { - List<PropertyInfo> 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<ProjectItem> projects; - List<ProjectItem> 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<PropertyInfo>::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<ConfigFile> 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<String>(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<ProjectList::Item> selected_projects = _project_list->get_selected_projects(); + bool empty_selection = selected_projects.empty(); - for (List<ProjectItem>::Element *E = projects.front(); E;) { - List<ProjectItem>::Element *next = E->next(); - if (favorite_projects.find(E->get()) != NULL) - projects.erase(E->get()); - E = next; - } - for (List<ProjectItem>::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<Texture> favorite_icon = get_icon("Favorites", "EditorIcons"); + erase_missing_btn->set_visible(_project_list->is_any_project_missing()); +} - for (List<ProjectItem>::Element *E = projects.front(); E; E = E->next()) { +void ProjectManager::_unhandled_input(const Ref<InputEvent> &p_ev) { - ProjectItem &item = E->get(); - String project = item.project; - String path = item.path; - String conf = item.conf; + Ref<InputEventKey> 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<Texture> icon; + case KEY_ENTER: { - if (item.icon != "") { - Ref<Image> 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<Texture> default_icon = get_icon("DefaultProjectIcon", "EditorIcons"); - img->resize(default_icon->get_width(), default_icon->get_height()); - Ref<ImageTexture> 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<String, String>::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<ProjectList::Item> 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<HBoxContainer>(scroll_children->get_child(i)); - Label *fpath = Object::cast_to<Label>(hb->get_node(NodePath("project/path_box/path"))); - if (fpath->get_text() == dir) { - has_already = true; - break; - } - } - if (has_already) { - _update_scroll_position(dir); - } else { - _load_recent_projects(); - _update_scroll_position(dir); - } + int i = _project_list->refresh_project(dir); + _project_list->select_project(i); + _project_list->ensure_project_visible(i); _open_selected_projects_ask(); } -void ProjectManager::_update_scroll_position(const String &dir) { - for (int i = 0; i < scroll_children->get_child_count(); i++) { - HBoxContainer *hb = Object::cast_to<HBoxContainer>(scroll_children->get_child(i)); - Label *fpath = Object::cast_to<Label>(hb->get_node(NodePath("project/path_box/path"))); - if (fpath->get_text() == dir) { - last_clicked = hb->get_meta("name"); - selected_list.clear(); - selected_list.insert(hb->get_meta("name"), hb->get_meta("main_scene")); - _update_project_buttons(); - 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; - - if (offset_diff > 0) - scroll->set_v_scroll(scroll->get_v_scroll() + offset_diff); - break; - } - } -} - void ProjectManager::_confirm_update_settings() { _open_selected_projects(); } void ProjectManager::_open_selected_projects() { - for (const Map<String, String>::Element *E = selected_list.front(); E; E = E->next()) { - const String &selected = E->key(); + const Set<String> &selected_list = _project_list->get_selected_project_keys(); + + for (const Set<String>::Element *E = selected_list.front(); E; E = E->next()) { + const String &selected = E->get(); String path = EditorSettings::get_singleton()->get("projects/" + selected); String conf = path.plus_file("project.godot"); + if (!FileAccess::exists(conf)) { dialog_error->set_text(vformat(TTR("Can't open project at '%s'."), path)); dialog_error->popup_centered_minsize(); @@ -1540,6 +1908,8 @@ void ProjectManager::_open_selected_projects() { void ProjectManager::_open_selected_projects_ask() { + const Set<String> &selected_list = _project_list->get_selected_project_keys(); + if (selected_list.size() < 1) { return; } @@ -1550,22 +1920,11 @@ void ProjectManager::_open_selected_projects_ask() { return; } - // Update the project settings or don't open - String path = EditorSettings::get_singleton()->get("projects/" + selected_list.front()->key()); - String conf = path.plus_file("project.godot"); + ProjectList::Item project = _project_list->get_selected_projects()[0]; - // FIXME: We already parse those in _load_recent_projects, we could instead make - // its `projects` list global and reuse its parsed metadata here. - Ref<ConfigFile> cf = memnew(ConfigFile); - Error cf_err = cf->load(conf); - - if (cf_err != OK) { - dialog_error->set_text(vformat(TTR("Can't open project at '%s'."), path)); - dialog_error->popup_centered_minsize(); - return; - } - - int config_version = (int)cf->get_value("", "config_version", 0); + // Update the project settings or don't open + String conf = project.path.plus_file("project.godot"); + int config_version = project.version; // Check if the config_version property was empty or 0 if (config_version == 0) { @@ -1581,7 +1940,7 @@ void ProjectManager::_open_selected_projects_ask() { } // Check if the file was generated by a newer, incompatible engine version if (config_version > ProjectSettings::CONFIG_VERSION) { - dialog_error->set_text(vformat(TTR("Can't open project at '%s'.") + "\n" + TTR("The project settings were created by a newer engine version, whose settings are not compatible with this version."), path)); + dialog_error->set_text(vformat(TTR("Can't open project at '%s'.") + "\n" + TTR("The project settings were created by a newer engine version, whose settings are not compatible with this version."), project.path)); dialog_error->popup_centered_minsize(); return; } @@ -1592,16 +1951,18 @@ void ProjectManager::_open_selected_projects_ask() { void ProjectManager::_run_project_confirm() { - for (Map<String, String>::Element *E = selected_list.front(); E; E = E->next()) { + Vector<ProjectList::Item> selected_list = _project_list->get_selected_projects(); + + for (int i = 0; i < selected_list.size(); ++i) { - const String &selected_main = E->get(); + const String &selected_main = selected_list[i].main_scene; if (selected_main == "") { run_error_diag->set_text(TTR("Can't run project: no main scene defined.\nPlease edit the project and set the main scene in the Project Settings under the \"Application\" category.")); run_error_diag->popup_centered(); return; } - const String &selected = E->key(); + const String &selected = selected_list[i].project_key; String path = EditorSettings::get_singleton()->get("projects/" + selected); if (!DirAccess::exists(path + "/.import")) { @@ -1629,8 +1990,11 @@ void ProjectManager::_run_project_confirm() { } } +// When you press the "Run" button void ProjectManager::_run_project() { + const Set<String> &selected_list = _project_list->get_selected_project_keys(); + if (selected_list.size() < 1) { return; } @@ -1643,11 +2007,6 @@ void ProjectManager::_run_project() { } } -void ProjectManager::_show_project(const String &p_path) { - - OS::get_singleton()->shell_open(String("file://") + p_path); -} - void ProjectManager::_scan_dir(const String &path, List<String> *r_projects) { DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); da->change_dir(path); @@ -1673,7 +2032,7 @@ void ProjectManager::_scan_begin(const String &p_base) { print_line("Found " + itos(projects.size()) + " projects."); for (List<String>::Element *E = projects.front(); E; E = E->next()) { - String proj = E->get().replace("/", "::"); + String proj = get_project_key_from_path(E->get()); EditorSettings::get_singleton()->set("projects/" + proj, E->get()); } EditorSettings::get_singleton()->save(); @@ -1699,12 +2058,14 @@ void ProjectManager::_import_project() { void ProjectManager::_rename_project() { + const Set<String> &selected_list = _project_list->get_selected_project_keys(); + if (selected_list.size() == 0) { return; } - for (Map<String, String>::Element *E = selected_list.front(); E; E = E->next()) { - const String &selected = E->key(); + for (Set<String>::Element *E = selected_list.front(); E; E = E->next()) { + const String &selected = E->get(); String path = EditorSettings::get_singleton()->get("projects/" + selected); npdialog->set_project_path(path); npdialog->set_mode(ProjectDialog::MODE_RENAME); @@ -1713,55 +2074,17 @@ void ProjectManager::_rename_project() { } void ProjectManager::_erase_project_confirm() { - - if (selected_list.size() == 0) { - return; - } - for (Map<String, String>::Element *E = selected_list.front(); E; E = E->next()) { - EditorSettings::get_singleton()->erase("projects/" + E->key()); - EditorSettings::get_singleton()->erase("favorite_projects/" + E->key()); - } - EditorSettings::get_singleton()->save(); - selected_list.clear(); - last_clicked = ""; - _load_recent_projects(); + _project_list->erase_selected_projects(); } void ProjectManager::_erase_missing_projects_confirm() { - - Map<String, String> list_all_projects; - for (int i = 0; i < scroll_children->get_child_count(); i++) { - HBoxContainer *hb = Object::cast_to<HBoxContainer>(scroll_children->get_child(i)); - if (hb) { - list_all_projects.insert(hb->get_meta("name"), hb->get_meta("main_scene")); - } - } - - if (list_all_projects.size() == 0) { - return; - } - - int deleted_projects = 0; - int remaining_projects = 0; - for (Map<String, String>::Element *E = list_all_projects.front(); E; E = E->next()) { - String project_name = E->key().replace(":::", ":/").replace("::", "/") + "/project.godot"; - if (!FileAccess::exists(project_name)) { - deleted_projects++; - EditorSettings::get_singleton()->erase("projects/" + E->key()); - EditorSettings::get_singleton()->erase("favorite_projects/" + E->key()); - } else { - remaining_projects++; - } - } - print_line("Deleted " + itos(deleted_projects) + " projects, remaining " + itos(remaining_projects) + " projects"); - EditorSettings::get_singleton()->save(); - selected_list.clear(); - last_clicked = ""; - _load_recent_projects(); + _project_list->erase_missing_projects(); } void ProjectManager::_erase_project() { + const Set<String> &selected_list = _project_list->get_selected_project_keys(); + if (selected_list.size() == 0) return; @@ -1867,13 +2190,23 @@ void ProjectManager::_scan_multiple_folders(PoolStringArray p_files) { } } +void ProjectManager::_on_order_option_changed() { + _project_list->set_order_option(project_order_filter->get_filter_option()); + _project_list->sort_projects(); +} + +void ProjectManager::_on_filter_option_changed() { + _project_list->set_filter_option(project_filter->get_filter_option()); + _project_list->set_search_term(project_filter->get_search_term()); + _project_list->sort_projects(); +} + void ProjectManager::_bind_methods() { ClassDB::bind_method("_open_selected_projects_ask", &ProjectManager::_open_selected_projects_ask); ClassDB::bind_method("_open_selected_projects", &ProjectManager::_open_selected_projects); ClassDB::bind_method("_run_project", &ProjectManager::_run_project); ClassDB::bind_method("_run_project_confirm", &ProjectManager::_run_project_confirm); - ClassDB::bind_method("_show_project", &ProjectManager::_show_project); ClassDB::bind_method("_scan_projects", &ProjectManager::_scan_projects); ClassDB::bind_method("_scan_begin", &ProjectManager::_scan_begin); ClassDB::bind_method("_import_project", &ProjectManager::_import_project); @@ -1886,18 +2219,16 @@ void ProjectManager::_bind_methods() { ClassDB::bind_method("_language_selected", &ProjectManager::_language_selected); ClassDB::bind_method("_restart_confirm", &ProjectManager::_restart_confirm); ClassDB::bind_method("_exit_dialog", &ProjectManager::_exit_dialog); - ClassDB::bind_method("_load_recent_projects", &ProjectManager::_load_recent_projects); + ClassDB::bind_method("_on_order_option_changed", &ProjectManager::_on_order_option_changed); + ClassDB::bind_method("_on_filter_option_changed", &ProjectManager::_on_filter_option_changed); ClassDB::bind_method("_on_projects_updated", &ProjectManager::_on_projects_updated); ClassDB::bind_method("_on_project_created", &ProjectManager::_on_project_created); - ClassDB::bind_method("_update_scroll_position", &ProjectManager::_update_scroll_position); - ClassDB::bind_method("_panel_draw", &ProjectManager::_panel_draw); - ClassDB::bind_method("_panel_input", &ProjectManager::_panel_input); ClassDB::bind_method("_unhandled_input", &ProjectManager::_unhandled_input); - ClassDB::bind_method("_favorite_pressed", &ProjectManager::_favorite_pressed); ClassDB::bind_method("_install_project", &ProjectManager::_install_project); ClassDB::bind_method("_files_dropped", &ProjectManager::_files_dropped); ClassDB::bind_method("_open_asset_library", &ProjectManager::_open_asset_library); ClassDB::bind_method("_confirm_update_settings", &ProjectManager::_confirm_update_settings); + ClassDB::bind_method("_update_project_buttons", &ProjectManager::_update_project_buttons); ClassDB::bind_method(D_METHOD("_scan_multiple_folders", "files"), &ProjectManager::_scan_multiple_folders); } @@ -1925,29 +2256,12 @@ ProjectManager::ProjectManager() { editor_set_scale(OS::get_singleton()->get_screen_dpi(screen) >= 192 && OS::get_singleton()->get_screen_size(screen).x > 2000 ? 2.0 : 1.0); } break; - case 1: { - editor_set_scale(0.75); - } break; - - case 2: { - editor_set_scale(1.0); - } break; - - case 3: { - editor_set_scale(1.25); - } break; - - case 4: { - editor_set_scale(1.5); - } break; - - case 5: { - editor_set_scale(1.75); - } break; - - case 6: { - editor_set_scale(2.0); - } break; + case 1: editor_set_scale(0.75); break; + case 2: editor_set_scale(1.0); break; + case 3: editor_set_scale(1.25); break; + case 4: editor_set_scale(1.5); break; + case 5: editor_set_scale(1.75); break; + case 6: editor_set_scale(2.0); break; default: { editor_set_scale(custom_display_scale); @@ -2030,7 +2344,7 @@ ProjectManager::ProjectManager() { project_order_filter->_setup_filters(sort_filter_titles); project_order_filter->set_filter_size(150); sort_filters->add_child(project_order_filter); - project_order_filter->connect("filter_changed", this, "_load_recent_projects"); + project_order_filter->connect("filter_changed", this, "_on_order_option_changed"); project_order_filter->set_custom_minimum_size(Size2(180, 10) * EDSCALE); int projects_sorting_order = (int)EditorSettings::get_singleton()->get("project_manager/sorting_order"); @@ -2049,7 +2363,7 @@ ProjectManager::ProjectManager() { project_filter->_setup_filters(vec2); project_filter->add_search_box(); search_filters->add_child(project_filter); - project_filter->connect("filter_changed", this, "_load_recent_projects"); + project_filter->connect("filter_changed", this, "_on_filter_option_changed"); project_filter->set_custom_minimum_size(Size2(280, 10) * EDSCALE); sort_filters->add_child(search_filters); @@ -2060,15 +2374,14 @@ ProjectManager::ProjectManager() { search_tree_vb->add_child(pc); pc->set_v_size_flags(SIZE_EXPAND_FILL); - scroll = memnew(ScrollContainer); - pc->add_child(scroll); - scroll->set_enable_h_scroll(false); + _project_list = memnew(ProjectList); + _project_list->connect(ProjectList::SIGNAL_SELECTION_CHANGED, this, "_update_project_buttons"); + _project_list->connect(ProjectList::SIGNAL_PROJECT_ASK_OPEN, this, "_open_selected_projects_ask"); + pc->add_child(_project_list); + _project_list->set_enable_h_scroll(false); VBoxContainer *tree_vb = memnew(VBoxContainer); tree_hb->add_child(tree_vb); - scroll_children = memnew(VBoxContainer); - scroll_children->set_h_size_flags(SIZE_EXPAND_FILL); - scroll->add_child(scroll_children); Button *open = memnew(Button); open->set_text(TTR("Edit")); @@ -2238,14 +2551,13 @@ ProjectManager::ProjectManager() { npdialog->connect("projects_updated", this, "_on_projects_updated"); npdialog->connect("project_created", this, "_on_project_created"); + _load_recent_projects(); if (EditorSettings::get_singleton()->get("filesystem/directories/autoscan_project_path")) { _scan_begin(EditorSettings::get_singleton()->get("filesystem/directories/autoscan_project_path")); } - last_clicked = ""; - SceneTree::get_singleton()->connect("files_dropped", this, "_files_dropped"); run_error_diag = memnew(AcceptDialog); diff --git a/editor/project_manager.h b/editor/project_manager.h index d75d7164cc..4ccb99d6bd 100644 --- a/editor/project_manager.h +++ b/editor/project_manager.h @@ -39,6 +39,7 @@ #include "scene/gui/tree.h" class ProjectDialog; +class ProjectList; class ProjectListFilter; class ProjectManager : public Control { @@ -68,16 +69,13 @@ class ProjectManager : public Control { AcceptDialog *dialog_error; ProjectDialog *npdialog; - ScrollContainer *scroll; - VBoxContainer *scroll_children; HBoxContainer *projects_hb; TabContainer *tabs; + ProjectList *_project_list; OptionButton *language_btn; Control *gui_base; - Map<String, String> selected_list; // name -> main_scene - String last_clicked; bool importing; void _open_asset_library(); @@ -86,7 +84,6 @@ class ProjectManager : public Control { void _run_project_confirm(); void _open_selected_projects(); void _open_selected_projects_ask(); - void _show_project(const String &p_path); void _import_project(); void _new_project(); void _rename_project(); @@ -111,13 +108,13 @@ class ProjectManager : public Control { void _install_project(const String &p_zip_path, const String &p_title); void _dim_window(); - void _panel_draw(Node *p_hb); - void _panel_input(const Ref<InputEvent> &p_ev, Node *p_hb); void _unhandled_input(const Ref<InputEvent> &p_ev); - void _favorite_pressed(Node *p_hb); void _files_dropped(PoolStringArray p_files, int p_screen); void _scan_multiple_folders(PoolStringArray p_files); + void _on_order_option_changed(); + void _on_filter_option_changed(); + protected: void _notification(int p_what); static void _bind_methods(); |