project_list.cpp 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086
  1. /**************************************************************************/
  2. /* project_list.cpp */
  3. /**************************************************************************/
  4. /* This file is part of: */
  5. /* GODOT ENGINE */
  6. /* https://godotengine.org */
  7. /**************************************************************************/
  8. /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
  9. /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
  10. /* */
  11. /* Permission is hereby granted, free of charge, to any person obtaining */
  12. /* a copy of this software and associated documentation files (the */
  13. /* "Software"), to deal in the Software without restriction, including */
  14. /* without limitation the rights to use, copy, modify, merge, publish, */
  15. /* distribute, sublicense, and/or sell copies of the Software, and to */
  16. /* permit persons to whom the Software is furnished to do so, subject to */
  17. /* the following conditions: */
  18. /* */
  19. /* The above copyright notice and this permission notice shall be */
  20. /* included in all copies or substantial portions of the Software. */
  21. /* */
  22. /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
  23. /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
  24. /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
  25. /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
  26. /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
  27. /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
  28. /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
  29. /**************************************************************************/
  30. #include "project_list.h"
  31. #include "core/config/project_settings.h"
  32. #include "core/io/dir_access.h"
  33. #include "editor/editor_paths.h"
  34. #include "editor/editor_settings.h"
  35. #include "editor/editor_string_names.h"
  36. #include "editor/project_manager.h"
  37. #include "editor/project_manager/project_tag.h"
  38. #include "editor/themes/editor_scale.h"
  39. #include "scene/gui/button.h"
  40. #include "scene/gui/label.h"
  41. #include "scene/gui/line_edit.h"
  42. #include "scene/gui/texture_button.h"
  43. #include "scene/gui/texture_rect.h"
  44. #include "scene/resources/image_texture.h"
  45. void ProjectListItemControl::_notification(int p_what) {
  46. switch (p_what) {
  47. case NOTIFICATION_THEME_CHANGED: {
  48. if (icon_needs_reload) {
  49. // The project icon may not be loaded by the time the control is displayed,
  50. // so use a loading placeholder.
  51. project_icon->set_texture(get_editor_theme_icon(SNAME("ProjectIconLoading")));
  52. }
  53. project_title->begin_bulk_theme_override();
  54. project_title->add_theme_font_override("font", get_theme_font(SNAME("title"), EditorStringName(EditorFonts)));
  55. project_title->add_theme_font_size_override("font_size", get_theme_font_size(SNAME("title_size"), EditorStringName(EditorFonts)));
  56. project_title->add_theme_color_override("font_color", get_theme_color(SNAME("font_color"), SNAME("Tree")));
  57. project_title->end_bulk_theme_override();
  58. project_path->add_theme_color_override("font_color", get_theme_color(SNAME("font_color"), SNAME("Tree")));
  59. project_unsupported_features->set_texture(get_editor_theme_icon(SNAME("NodeWarning")));
  60. favorite_button->set_texture_normal(get_editor_theme_icon(SNAME("Favorites")));
  61. if (project_is_missing) {
  62. explore_button->set_icon(get_editor_theme_icon(SNAME("FileBroken")));
  63. } else {
  64. explore_button->set_icon(get_editor_theme_icon(SNAME("Load")));
  65. }
  66. } break;
  67. case NOTIFICATION_MOUSE_ENTER: {
  68. is_hovering = true;
  69. queue_redraw();
  70. } break;
  71. case NOTIFICATION_MOUSE_EXIT: {
  72. is_hovering = false;
  73. queue_redraw();
  74. } break;
  75. case NOTIFICATION_DRAW: {
  76. if (is_selected) {
  77. draw_style_box(get_theme_stylebox(SNAME("selected"), SNAME("Tree")), Rect2(Point2(), get_size()));
  78. }
  79. if (is_hovering) {
  80. draw_style_box(get_theme_stylebox(SNAME("hover"), SNAME("Tree")), Rect2(Point2(), get_size()));
  81. }
  82. draw_line(Point2(0, get_size().y + 1), Point2(get_size().x, get_size().y + 1), get_theme_color(SNAME("guide_color"), SNAME("Tree")));
  83. } break;
  84. }
  85. }
  86. void ProjectListItemControl::_favorite_button_pressed() {
  87. emit_signal(SNAME("favorite_pressed"));
  88. }
  89. void ProjectListItemControl::_explore_button_pressed() {
  90. emit_signal(SNAME("explore_pressed"));
  91. }
  92. void ProjectListItemControl::set_project_title(const String &p_title) {
  93. project_title->set_text(p_title);
  94. }
  95. void ProjectListItemControl::set_project_path(const String &p_path) {
  96. project_path->set_text(p_path);
  97. }
  98. void ProjectListItemControl::set_tags(const PackedStringArray &p_tags, ProjectList *p_parent_list) {
  99. for (const String &tag : p_tags) {
  100. ProjectTag *tag_control = memnew(ProjectTag(tag));
  101. tag_container->add_child(tag_control);
  102. tag_control->connect_button_to(callable_mp(p_parent_list, &ProjectList::add_search_tag).bind(tag));
  103. }
  104. }
  105. void ProjectListItemControl::set_project_icon(const Ref<Texture2D> &p_icon) {
  106. icon_needs_reload = false;
  107. // The default project icon is 128×128 to look crisp on hiDPI displays,
  108. // but we want the actual displayed size to be 64×64 on loDPI displays.
  109. project_icon->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);
  110. project_icon->set_custom_minimum_size(Size2(64, 64) * EDSCALE);
  111. project_icon->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED);
  112. project_icon->set_texture(p_icon);
  113. }
  114. void ProjectListItemControl::set_unsupported_features(PackedStringArray p_features) {
  115. if (p_features.size() > 0) {
  116. String tooltip_text = "";
  117. for (int i = 0; i < p_features.size(); i++) {
  118. if (ProjectList::project_feature_looks_like_version(p_features[i])) {
  119. tooltip_text += TTR("This project was last edited in a different Godot version: ") + p_features[i] + "\n";
  120. p_features.remove_at(i);
  121. i--;
  122. }
  123. }
  124. if (p_features.size() > 0) {
  125. String unsupported_features_str = String(", ").join(p_features);
  126. tooltip_text += TTR("This project uses features unsupported by the current build:") + "\n" + unsupported_features_str;
  127. }
  128. project_unsupported_features->set_tooltip_text(tooltip_text);
  129. project_unsupported_features->show();
  130. } else {
  131. project_unsupported_features->hide();
  132. }
  133. }
  134. bool ProjectListItemControl::should_load_project_icon() const {
  135. return icon_needs_reload;
  136. }
  137. void ProjectListItemControl::set_selected(bool p_selected) {
  138. is_selected = p_selected;
  139. queue_redraw();
  140. }
  141. void ProjectListItemControl::set_is_favorite(bool p_favorite) {
  142. favorite_button->set_modulate(p_favorite ? Color(1, 1, 1, 1) : Color(1, 1, 1, 0.2));
  143. }
  144. void ProjectListItemControl::set_is_missing(bool p_missing) {
  145. if (project_is_missing == p_missing) {
  146. return;
  147. }
  148. project_is_missing = p_missing;
  149. if (project_is_missing) {
  150. project_icon->set_modulate(Color(1, 1, 1, 0.5));
  151. explore_button->set_icon(get_editor_theme_icon(SNAME("FileBroken")));
  152. explore_button->set_tooltip_text(TTR("Error: Project is missing on the filesystem."));
  153. } else {
  154. project_icon->set_modulate(Color(1, 1, 1, 1.0));
  155. explore_button->set_icon(get_editor_theme_icon(SNAME("Load")));
  156. #if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
  157. explore_button->set_tooltip_text(TTR("Show in File Manager"));
  158. #else
  159. // Opening the system file manager is not supported on the Android and web editors.
  160. explore_button->hide();
  161. #endif
  162. }
  163. }
  164. void ProjectListItemControl::set_is_grayed(bool p_grayed) {
  165. if (p_grayed) {
  166. main_vbox->set_modulate(Color(1, 1, 1, 0.5));
  167. // Don't make the icon less prominent if the parent is already grayed out.
  168. explore_button->set_modulate(Color(1, 1, 1, 1.0));
  169. } else {
  170. main_vbox->set_modulate(Color(1, 1, 1, 1.0));
  171. explore_button->set_modulate(Color(1, 1, 1, 0.5));
  172. }
  173. }
  174. void ProjectListItemControl::_bind_methods() {
  175. ADD_SIGNAL(MethodInfo("favorite_pressed"));
  176. ADD_SIGNAL(MethodInfo("explore_pressed"));
  177. }
  178. ProjectListItemControl::ProjectListItemControl() {
  179. set_focus_mode(FocusMode::FOCUS_ALL);
  180. VBoxContainer *favorite_box = memnew(VBoxContainer);
  181. favorite_box->set_alignment(BoxContainer::ALIGNMENT_CENTER);
  182. add_child(favorite_box);
  183. favorite_button = memnew(TextureButton);
  184. favorite_button->set_name("FavoriteButton");
  185. // This makes the project's "hover" style display correctly when hovering the favorite icon.
  186. favorite_button->set_mouse_filter(MOUSE_FILTER_PASS);
  187. favorite_box->add_child(favorite_button);
  188. favorite_button->connect(SceneStringName(pressed), callable_mp(this, &ProjectListItemControl::_favorite_button_pressed));
  189. project_icon = memnew(TextureRect);
  190. project_icon->set_name("ProjectIcon");
  191. project_icon->set_v_size_flags(SIZE_SHRINK_CENTER);
  192. add_child(project_icon);
  193. main_vbox = memnew(VBoxContainer);
  194. main_vbox->set_h_size_flags(Control::SIZE_EXPAND_FILL);
  195. add_child(main_vbox);
  196. Control *ec = memnew(Control);
  197. ec->set_custom_minimum_size(Size2(0, 1));
  198. ec->set_mouse_filter(MOUSE_FILTER_PASS);
  199. main_vbox->add_child(ec);
  200. // Top half, title, tags and unsupported features labels.
  201. {
  202. HBoxContainer *title_hb = memnew(HBoxContainer);
  203. main_vbox->add_child(title_hb);
  204. project_title = memnew(Label);
  205. project_title->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
  206. project_title->set_name("ProjectName");
  207. project_title->set_h_size_flags(Control::SIZE_EXPAND_FILL);
  208. project_title->set_clip_text(true);
  209. title_hb->add_child(project_title);
  210. tag_container = memnew(HBoxContainer);
  211. title_hb->add_child(tag_container);
  212. Control *spacer = memnew(Control);
  213. spacer->set_custom_minimum_size(Size2(10, 10));
  214. title_hb->add_child(spacer);
  215. }
  216. // Bottom half, containing the path and view folder button.
  217. {
  218. HBoxContainer *path_hb = memnew(HBoxContainer);
  219. path_hb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
  220. main_vbox->add_child(path_hb);
  221. explore_button = memnew(Button);
  222. explore_button->set_name("ExploreButton");
  223. explore_button->set_flat(true);
  224. path_hb->add_child(explore_button);
  225. explore_button->connect(SceneStringName(pressed), callable_mp(this, &ProjectListItemControl::_explore_button_pressed));
  226. project_path = memnew(Label);
  227. project_path->set_name("ProjectPath");
  228. project_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
  229. project_path->set_clip_text(true);
  230. project_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
  231. project_path->set_modulate(Color(1, 1, 1, 0.5));
  232. path_hb->add_child(project_path);
  233. project_unsupported_features = memnew(TextureRect);
  234. project_unsupported_features->set_name("ProjectUnsupportedFeatures");
  235. project_unsupported_features->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
  236. path_hb->add_child(project_unsupported_features);
  237. project_unsupported_features->hide();
  238. Control *spacer = memnew(Control);
  239. spacer->set_custom_minimum_size(Size2(10, 10));
  240. path_hb->add_child(spacer);
  241. }
  242. }
  243. struct ProjectListComparator {
  244. ProjectList::FilterOption order_option = ProjectList::FilterOption::EDIT_DATE;
  245. // operator<
  246. _FORCE_INLINE_ bool operator()(const ProjectList::Item &a, const ProjectList::Item &b) const {
  247. if (a.favorite && !b.favorite) {
  248. return true;
  249. }
  250. if (b.favorite && !a.favorite) {
  251. return false;
  252. }
  253. switch (order_option) {
  254. case ProjectList::PATH:
  255. return a.path < b.path;
  256. case ProjectList::EDIT_DATE:
  257. return a.last_edited > b.last_edited;
  258. case ProjectList::TAGS:
  259. return a.tag_sort_string < b.tag_sort_string;
  260. default:
  261. return a.project_name < b.project_name;
  262. }
  263. }
  264. };
  265. const char *ProjectList::SIGNAL_LIST_CHANGED = "list_changed";
  266. const char *ProjectList::SIGNAL_SELECTION_CHANGED = "selection_changed";
  267. const char *ProjectList::SIGNAL_PROJECT_ASK_OPEN = "project_ask_open";
  268. // Helpers.
  269. bool ProjectList::project_feature_looks_like_version(const String &p_feature) {
  270. return p_feature.contains(".") && p_feature.substr(0, 3).is_numeric();
  271. }
  272. // Notifications.
  273. void ProjectList::_notification(int p_what) {
  274. switch (p_what) {
  275. case NOTIFICATION_PROCESS: {
  276. // Load icons as a coroutine to speed up launch when you have hundreds of projects
  277. if (_icon_load_index < _projects.size()) {
  278. Item &item = _projects.write[_icon_load_index];
  279. if (item.control->should_load_project_icon()) {
  280. _load_project_icon(_icon_load_index);
  281. }
  282. _icon_load_index++;
  283. } else {
  284. set_process(false);
  285. }
  286. } break;
  287. }
  288. }
  289. // Initialization & loading.
  290. void ProjectList::_migrate_config() {
  291. // Proposal #1637 moved the project list from editor settings to a separate config file
  292. // If the new config file doesn't exist, populate it from EditorSettings
  293. if (FileAccess::exists(_config_path)) {
  294. return;
  295. }
  296. List<PropertyInfo> properties;
  297. EditorSettings::get_singleton()->get_property_list(&properties);
  298. for (const PropertyInfo &E : properties) {
  299. // This is actually something like "projects/C:::Documents::Godot::Projects::MyGame"
  300. String property_key = E.name;
  301. if (!property_key.begins_with("projects/")) {
  302. continue;
  303. }
  304. String path = EDITOR_GET(property_key);
  305. print_line("Migrating legacy project '" + path + "'.");
  306. String favoriteKey = "favorite_projects/" + property_key.get_slice("/", 1);
  307. bool favorite = EditorSettings::get_singleton()->has_setting(favoriteKey);
  308. add_project(path, favorite);
  309. if (favorite) {
  310. EditorSettings::get_singleton()->erase(favoriteKey);
  311. }
  312. EditorSettings::get_singleton()->erase(property_key);
  313. }
  314. save_config();
  315. }
  316. void ProjectList::save_config() {
  317. _config.save(_config_path);
  318. }
  319. // Load project data from p_property_key and return it in a ProjectList::Item.
  320. // p_favorite is passed directly into the Item.
  321. ProjectList::Item ProjectList::load_project_data(const String &p_path, bool p_favorite) {
  322. String conf = p_path.path_join("project.godot");
  323. bool grayed = false;
  324. bool missing = false;
  325. Ref<ConfigFile> cf = memnew(ConfigFile);
  326. Error cf_err = cf->load(conf);
  327. int config_version = 0;
  328. String project_name = TTR("Unnamed Project");
  329. if (cf_err == OK) {
  330. String cf_project_name = cf->get_value("application", "config/name", "");
  331. if (!cf_project_name.is_empty()) {
  332. project_name = cf_project_name.xml_unescape();
  333. }
  334. config_version = (int)cf->get_value("", "config_version", 0);
  335. }
  336. if (config_version > ProjectSettings::CONFIG_VERSION) {
  337. // Comes from an incompatible (more recent) Godot version, gray it out.
  338. grayed = true;
  339. }
  340. const String description = cf->get_value("application", "config/description", "");
  341. const PackedStringArray tags = cf->get_value("application", "config/tags", PackedStringArray());
  342. const String icon = cf->get_value("application", "config/icon", "");
  343. const String main_scene = cf->get_value("application", "run/main_scene", "");
  344. PackedStringArray project_features = cf->get_value("application", "config/features", PackedStringArray());
  345. PackedStringArray unsupported_features = ProjectSettings::get_unsupported_features(project_features);
  346. uint64_t last_edited = 0;
  347. if (cf_err == OK) {
  348. // The modification date marks the date the project was last edited.
  349. // This is because the `project.godot` file will always be modified
  350. // when editing a project (but not when running it).
  351. last_edited = FileAccess::get_modified_time(conf);
  352. String fscache = p_path.path_join(".fscache");
  353. if (FileAccess::exists(fscache)) {
  354. uint64_t cache_modified = FileAccess::get_modified_time(fscache);
  355. if (cache_modified > last_edited) {
  356. last_edited = cache_modified;
  357. }
  358. }
  359. } else {
  360. grayed = true;
  361. missing = true;
  362. print_line("Project is missing: " + conf);
  363. }
  364. for (const String &tag : tags) {
  365. ProjectManager::get_singleton()->add_new_tag(tag);
  366. }
  367. return Item(project_name, description, tags, p_path, icon, main_scene, unsupported_features, last_edited, p_favorite, grayed, missing, config_version);
  368. }
  369. void ProjectList::_update_icons_async() {
  370. _icon_load_index = 0;
  371. set_process(true);
  372. }
  373. void ProjectList::_load_project_icon(int p_index) {
  374. Item &item = _projects.write[p_index];
  375. Ref<Texture2D> default_icon = get_editor_theme_icon(SNAME("DefaultProjectIcon"));
  376. Ref<Texture2D> icon;
  377. if (!item.icon.is_empty()) {
  378. Ref<Image> img;
  379. img.instantiate();
  380. Error err = img->load(item.icon.replace_first("res://", item.path + "/"));
  381. if (err == OK) {
  382. img->resize(default_icon->get_width(), default_icon->get_height(), Image::INTERPOLATE_LANCZOS);
  383. icon = ImageTexture::create_from_image(img);
  384. }
  385. }
  386. if (icon.is_null()) {
  387. icon = default_icon;
  388. }
  389. item.control->set_project_icon(icon);
  390. }
  391. // Project list updates.
  392. void ProjectList::update_project_list() {
  393. // This is a full, hard reload of the list. Don't call this unless really required, it's expensive.
  394. // If you have 150 projects, it may read through 150 files on your disk at once + load 150 icons.
  395. // FIXME: Does it really have to be a full, hard reload? Runtime updates should be made much cheaper.
  396. if (ProjectManager::get_singleton()->is_initialized()) {
  397. // Clear whole list
  398. for (int i = 0; i < _projects.size(); ++i) {
  399. Item &project = _projects.write[i];
  400. CRASH_COND(project.control == nullptr);
  401. memdelete(project.control); // Why not queue_free()?
  402. }
  403. _projects.clear();
  404. _last_clicked = "";
  405. _selected_project_paths.clear();
  406. load_project_list();
  407. }
  408. // Create controls
  409. for (int i = 0; i < _projects.size(); ++i) {
  410. _create_project_item_control(i);
  411. }
  412. sort_projects();
  413. _update_icons_async();
  414. update_dock_menu();
  415. set_v_scroll(0);
  416. emit_signal(SNAME(SIGNAL_LIST_CHANGED));
  417. }
  418. void ProjectList::sort_projects() {
  419. SortArray<Item, ProjectListComparator> sorter;
  420. sorter.compare.order_option = _order_option;
  421. sorter.sort(_projects.ptrw(), _projects.size());
  422. String search_term;
  423. PackedStringArray tags;
  424. if (!_search_term.is_empty()) {
  425. PackedStringArray search_parts = _search_term.split(" ");
  426. if (search_parts.size() > 1 || search_parts[0].begins_with("tag:")) {
  427. PackedStringArray remaining;
  428. for (const String &part : search_parts) {
  429. if (part.begins_with("tag:")) {
  430. tags.push_back(part.get_slice(":", 1));
  431. } else {
  432. remaining.append(part);
  433. }
  434. }
  435. search_term = String(" ").join(remaining); // Search term without tags.
  436. } else {
  437. search_term = _search_term;
  438. }
  439. }
  440. for (int i = 0; i < _projects.size(); ++i) {
  441. Item &item = _projects.write[i];
  442. bool item_visible = true;
  443. if (!_search_term.is_empty()) {
  444. String search_path;
  445. if (search_term.contains("/")) {
  446. // Search path will match the whole path
  447. search_path = item.path;
  448. } else {
  449. // Search path will only match the last path component to make searching more strict
  450. search_path = item.path.get_file();
  451. }
  452. bool missing_tags = false;
  453. for (const String &tag : tags) {
  454. if (!item.tags.has(tag)) {
  455. missing_tags = true;
  456. break;
  457. }
  458. }
  459. // When searching, display projects whose name or path contain the search term and whose tags match the searched tags.
  460. item_visible = !missing_tags && (search_term.is_empty() || item.project_name.containsn(search_term) || search_path.containsn(search_term));
  461. }
  462. item.control->set_visible(item_visible);
  463. }
  464. for (int i = 0; i < _projects.size(); ++i) {
  465. Item &item = _projects.write[i];
  466. item.control->get_parent()->move_child(item.control, i);
  467. }
  468. // Rewind the coroutine because order of projects changed
  469. _update_icons_async();
  470. update_dock_menu();
  471. }
  472. int ProjectList::get_project_count() const {
  473. return _projects.size();
  474. }
  475. void ProjectList::find_projects(const String &p_path) {
  476. PackedStringArray paths = { p_path };
  477. find_projects_multiple(paths);
  478. }
  479. void ProjectList::find_projects_multiple(const PackedStringArray &p_paths) {
  480. List<String> projects;
  481. for (int i = 0; i < p_paths.size(); i++) {
  482. const String &base_path = p_paths.get(i);
  483. print_verbose(vformat("Scanning for projects in \"%s\".", base_path));
  484. _scan_folder_recursive(base_path, &projects);
  485. print_verbose(vformat("Found %d project(s).", projects.size()));
  486. }
  487. for (const String &E : projects) {
  488. add_project(E, false);
  489. }
  490. save_config();
  491. if (ProjectManager::get_singleton()->is_initialized()) {
  492. update_project_list();
  493. }
  494. }
  495. void ProjectList::load_project_list() {
  496. List<String> sections;
  497. _config.load(_config_path);
  498. _config.get_sections(&sections);
  499. for (const String &path : sections) {
  500. bool favorite = _config.get_value(path, "favorite", false);
  501. _projects.push_back(load_project_data(path, favorite));
  502. }
  503. }
  504. void ProjectList::_scan_folder_recursive(const String &p_path, List<String> *r_projects) {
  505. Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
  506. Error error = da->change_dir(p_path);
  507. ERR_FAIL_COND_MSG(error != OK, vformat("Failed to open the path \"%s\" for scanning (code %d).", p_path, error));
  508. da->list_dir_begin();
  509. String n = da->get_next();
  510. while (!n.is_empty()) {
  511. if (da->current_is_dir() && n[0] != '.') {
  512. _scan_folder_recursive(da->get_current_dir().path_join(n), r_projects);
  513. } else if (n == "project.godot") {
  514. r_projects->push_back(da->get_current_dir());
  515. }
  516. n = da->get_next();
  517. }
  518. da->list_dir_end();
  519. }
  520. // Project list items.
  521. void ProjectList::add_project(const String &dir_path, bool favorite) {
  522. if (!_config.has_section(dir_path)) {
  523. _config.set_value(dir_path, "favorite", favorite);
  524. }
  525. }
  526. void ProjectList::set_project_version(const String &p_project_path, int p_version) {
  527. for (ProjectList::Item &E : _projects) {
  528. if (E.path == p_project_path) {
  529. E.version = p_version;
  530. break;
  531. }
  532. }
  533. }
  534. int ProjectList::refresh_project(const String &dir_path) {
  535. // Reloads information about a specific project.
  536. // If it wasn't loaded and should be in the list, it is added (i.e new project).
  537. // If it isn't in the list anymore, it is removed.
  538. // If it is in the list but doesn't exist anymore, it is marked as missing.
  539. bool should_be_in_list = _config.has_section(dir_path);
  540. bool is_favourite = _config.get_value(dir_path, "favorite", false);
  541. bool was_selected = _selected_project_paths.has(dir_path);
  542. // Remove item in any case
  543. for (int i = 0; i < _projects.size(); ++i) {
  544. const Item &existing_item = _projects[i];
  545. if (existing_item.path == dir_path) {
  546. _remove_project(i, false);
  547. break;
  548. }
  549. }
  550. int index = -1;
  551. if (should_be_in_list) {
  552. // Recreate it with updated info
  553. Item item = load_project_data(dir_path, is_favourite);
  554. _projects.push_back(item);
  555. _create_project_item_control(_projects.size() - 1);
  556. sort_projects();
  557. for (int i = 0; i < _projects.size(); ++i) {
  558. if (_projects[i].path == dir_path) {
  559. if (was_selected) {
  560. select_project(i);
  561. ensure_project_visible(i);
  562. }
  563. _load_project_icon(i);
  564. index = i;
  565. break;
  566. }
  567. }
  568. }
  569. return index;
  570. }
  571. void ProjectList::ensure_project_visible(int p_index) {
  572. const Item &item = _projects[p_index];
  573. ensure_control_visible(item.control);
  574. }
  575. void ProjectList::_create_project_item_control(int p_index) {
  576. // Will be added last in the list, so make sure indexes match
  577. ERR_FAIL_COND(p_index != project_list_vbox->get_child_count());
  578. Item &item = _projects.write[p_index];
  579. ERR_FAIL_COND(item.control != nullptr); // Already created
  580. ProjectListItemControl *hb = memnew(ProjectListItemControl);
  581. hb->add_theme_constant_override("separation", 10 * EDSCALE);
  582. hb->set_project_title(!item.missing ? item.project_name : TTR("Missing Project"));
  583. hb->set_project_path(item.path);
  584. hb->set_tooltip_text(item.description);
  585. hb->set_tags(item.tags, this);
  586. hb->set_unsupported_features(item.unsupported_features.duplicate());
  587. hb->set_is_favorite(item.favorite);
  588. hb->set_is_missing(item.missing);
  589. hb->set_is_grayed(item.grayed);
  590. hb->connect(SceneStringName(gui_input), callable_mp(this, &ProjectList::_list_item_input).bind(hb));
  591. hb->connect("favorite_pressed", callable_mp(this, &ProjectList::_on_favorite_pressed).bind(hb));
  592. #if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
  593. hb->connect("explore_pressed", callable_mp(this, &ProjectList::_on_explore_pressed).bind(item.path));
  594. #endif
  595. project_list_vbox->add_child(hb);
  596. item.control = hb;
  597. }
  598. void ProjectList::_toggle_project(int p_index) {
  599. // This methods adds to the selection or removes from the
  600. // selection.
  601. Item &item = _projects.write[p_index];
  602. if (_selected_project_paths.has(item.path)) {
  603. _deselect_project_nocheck(p_index);
  604. } else {
  605. _select_project_nocheck(p_index);
  606. }
  607. }
  608. void ProjectList::_remove_project(int p_index, bool p_update_config) {
  609. const Item item = _projects[p_index]; // Take a copy
  610. _selected_project_paths.erase(item.path);
  611. if (_last_clicked == item.path) {
  612. _last_clicked = "";
  613. }
  614. memdelete(item.control);
  615. _projects.remove_at(p_index);
  616. if (p_update_config) {
  617. _config.erase_section(item.path);
  618. // Not actually saving the file, in case you are doing more changes to settings
  619. }
  620. update_dock_menu();
  621. }
  622. void ProjectList::_list_item_input(const Ref<InputEvent> &p_ev, Node *p_hb) {
  623. Ref<InputEventMouseButton> mb = p_ev;
  624. int clicked_index = p_hb->get_index();
  625. const Item &clicked_project = _projects[clicked_index];
  626. if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
  627. if (mb->is_shift_pressed() && _selected_project_paths.size() > 0 && !_last_clicked.is_empty() && clicked_project.path != _last_clicked) {
  628. int anchor_index = -1;
  629. for (int i = 0; i < _projects.size(); ++i) {
  630. const Item &p = _projects[i];
  631. if (p.path == _last_clicked) {
  632. anchor_index = p.control->get_index();
  633. break;
  634. }
  635. }
  636. CRASH_COND(anchor_index == -1);
  637. _select_project_range(anchor_index, clicked_index);
  638. } else if (mb->is_command_or_control_pressed()) {
  639. _toggle_project(clicked_index);
  640. } else {
  641. _last_clicked = clicked_project.path;
  642. select_project(clicked_index);
  643. }
  644. emit_signal(SNAME(SIGNAL_SELECTION_CHANGED));
  645. // Do not allow opening a project more than once using a single project manager instance.
  646. // Opening the same project in several editor instances at once can lead to various issues.
  647. if (!mb->is_command_or_control_pressed() && mb->is_double_click() && !project_opening_initiated) {
  648. emit_signal(SNAME(SIGNAL_PROJECT_ASK_OPEN));
  649. }
  650. }
  651. }
  652. void ProjectList::_on_favorite_pressed(Node *p_hb) {
  653. ProjectListItemControl *control = Object::cast_to<ProjectListItemControl>(p_hb);
  654. int index = control->get_index();
  655. Item item = _projects.write[index]; // Take copy
  656. item.favorite = !item.favorite;
  657. _config.set_value(item.path, "favorite", item.favorite);
  658. save_config();
  659. _projects.write[index] = item;
  660. control->set_is_favorite(item.favorite);
  661. sort_projects();
  662. if (item.favorite) {
  663. for (int i = 0; i < _projects.size(); ++i) {
  664. if (_projects[i].path == item.path) {
  665. ensure_project_visible(i);
  666. break;
  667. }
  668. }
  669. }
  670. update_dock_menu();
  671. }
  672. void ProjectList::_on_explore_pressed(const String &p_path) {
  673. OS::get_singleton()->shell_show_in_file_manager(p_path, true);
  674. }
  675. // Project list selection.
  676. void ProjectList::_clear_project_selection() {
  677. Vector<Item> previous_selected_items = get_selected_projects();
  678. _selected_project_paths.clear();
  679. for (int i = 0; i < previous_selected_items.size(); ++i) {
  680. previous_selected_items[i].control->set_selected(false);
  681. }
  682. }
  683. void ProjectList::_select_project_nocheck(int p_index) {
  684. Item &item = _projects.write[p_index];
  685. _selected_project_paths.insert(item.path);
  686. item.control->set_selected(true);
  687. }
  688. void ProjectList::_deselect_project_nocheck(int p_index) {
  689. Item &item = _projects.write[p_index];
  690. _selected_project_paths.erase(item.path);
  691. item.control->set_selected(false);
  692. }
  693. inline void _sort_project_range(int &a, int &b) {
  694. if (a > b) {
  695. int temp = a;
  696. a = b;
  697. b = temp;
  698. }
  699. }
  700. void ProjectList::_select_project_range(int p_begin, int p_end) {
  701. _clear_project_selection();
  702. _sort_project_range(p_begin, p_end);
  703. for (int i = p_begin; i <= p_end; ++i) {
  704. _select_project_nocheck(i);
  705. }
  706. }
  707. void ProjectList::select_project(int p_index) {
  708. // This method keeps only one project selected.
  709. _clear_project_selection();
  710. _select_project_nocheck(p_index);
  711. }
  712. void ProjectList::select_first_visible_project() {
  713. _clear_project_selection();
  714. for (int i = 0; i < _projects.size(); i++) {
  715. if (_projects[i].control->is_visible()) {
  716. _select_project_nocheck(i);
  717. break;
  718. }
  719. }
  720. }
  721. Vector<ProjectList::Item> ProjectList::get_selected_projects() const {
  722. Vector<Item> items;
  723. if (_selected_project_paths.size() == 0) {
  724. return items;
  725. }
  726. items.resize(_selected_project_paths.size());
  727. int j = 0;
  728. for (int i = 0; i < _projects.size(); ++i) {
  729. const Item &item = _projects[i];
  730. if (_selected_project_paths.has(item.path)) {
  731. items.write[j++] = item;
  732. }
  733. }
  734. ERR_FAIL_COND_V(j != items.size(), items);
  735. return items;
  736. }
  737. const HashSet<String> &ProjectList::get_selected_project_keys() const {
  738. // Faster if that's all you need
  739. return _selected_project_paths;
  740. }
  741. int ProjectList::get_single_selected_index() const {
  742. if (_selected_project_paths.size() == 0) {
  743. // Default selection
  744. return 0;
  745. }
  746. String key;
  747. if (_selected_project_paths.size() == 1) {
  748. // Only one selected
  749. key = *_selected_project_paths.begin();
  750. } else {
  751. // Multiple selected, consider the last clicked one as "main"
  752. key = _last_clicked;
  753. }
  754. for (int i = 0; i < _projects.size(); ++i) {
  755. if (_projects[i].path == key) {
  756. return i;
  757. }
  758. }
  759. return 0;
  760. }
  761. void ProjectList::erase_selected_projects(bool p_delete_project_contents) {
  762. if (_selected_project_paths.size() == 0) {
  763. return;
  764. }
  765. for (int i = 0; i < _projects.size(); ++i) {
  766. Item &item = _projects.write[i];
  767. if (_selected_project_paths.has(item.path) && item.control->is_visible()) {
  768. _config.erase_section(item.path);
  769. // Comment out for now until we have a better warning system to
  770. // ensure users delete their project only.
  771. //if (p_delete_project_contents) {
  772. // OS::get_singleton()->move_to_trash(item.path);
  773. //}
  774. memdelete(item.control);
  775. _projects.remove_at(i);
  776. --i;
  777. }
  778. }
  779. save_config();
  780. _selected_project_paths.clear();
  781. _last_clicked = "";
  782. update_dock_menu();
  783. }
  784. // Missing projects.
  785. bool ProjectList::is_any_project_missing() const {
  786. for (int i = 0; i < _projects.size(); ++i) {
  787. if (_projects[i].missing) {
  788. return true;
  789. }
  790. }
  791. return false;
  792. }
  793. void ProjectList::erase_missing_projects() {
  794. if (_projects.is_empty()) {
  795. return;
  796. }
  797. int deleted_count = 0;
  798. int remaining_count = 0;
  799. for (int i = 0; i < _projects.size(); ++i) {
  800. const Item &item = _projects[i];
  801. if (item.missing) {
  802. _remove_project(i, true);
  803. --i;
  804. ++deleted_count;
  805. } else {
  806. ++remaining_count;
  807. }
  808. }
  809. print_line("Removed " + itos(deleted_count) + " projects from the list, remaining " + itos(remaining_count) + " projects");
  810. save_config();
  811. }
  812. // Project list sorting and filtering.
  813. void ProjectList::set_search_term(String p_search_term) {
  814. _search_term = p_search_term;
  815. }
  816. void ProjectList::add_search_tag(const String &p_tag) {
  817. const String tag_string = "tag:" + p_tag;
  818. int exists = _search_term.find(tag_string);
  819. if (exists > -1) {
  820. _search_term = _search_term.erase(exists, tag_string.length() + 1);
  821. } else if (_search_term.is_empty() || _search_term.ends_with(" ")) {
  822. _search_term += tag_string;
  823. } else {
  824. _search_term += " " + tag_string;
  825. }
  826. ProjectManager::get_singleton()->get_search_box()->set_text(_search_term);
  827. sort_projects();
  828. }
  829. void ProjectList::set_order_option(int p_option) {
  830. FilterOption selected = (FilterOption)p_option;
  831. EditorSettings::get_singleton()->set("project_manager/sorting_order", p_option);
  832. EditorSettings::get_singleton()->save();
  833. _order_option = selected;
  834. sort_projects();
  835. }
  836. // Global menu integration.
  837. void ProjectList::update_dock_menu() {
  838. if (!NativeMenu::get_singleton()->has_feature(NativeMenu::FEATURE_GLOBAL_MENU)) {
  839. return;
  840. }
  841. RID dock_rid = NativeMenu::get_singleton()->get_system_menu(NativeMenu::DOCK_MENU_ID);
  842. NativeMenu::get_singleton()->clear(dock_rid);
  843. int favs_added = 0;
  844. int total_added = 0;
  845. for (int i = 0; i < _projects.size(); ++i) {
  846. if (!_projects[i].grayed && !_projects[i].missing) {
  847. if (_projects[i].favorite) {
  848. favs_added++;
  849. } else {
  850. if (favs_added != 0) {
  851. NativeMenu::get_singleton()->add_separator(dock_rid);
  852. }
  853. favs_added = 0;
  854. }
  855. NativeMenu::get_singleton()->add_item(dock_rid, _projects[i].project_name + " ( " + _projects[i].path + " )", callable_mp(this, &ProjectList::_global_menu_open_project), Callable(), i);
  856. total_added++;
  857. }
  858. }
  859. if (total_added != 0) {
  860. NativeMenu::get_singleton()->add_separator(dock_rid);
  861. }
  862. NativeMenu::get_singleton()->add_item(dock_rid, TTR("New Window"), callable_mp(this, &ProjectList::_global_menu_new_window));
  863. }
  864. void ProjectList::_global_menu_new_window(const Variant &p_tag) {
  865. List<String> args;
  866. args.push_back("-p");
  867. OS::get_singleton()->create_instance(args);
  868. }
  869. void ProjectList::_global_menu_open_project(const Variant &p_tag) {
  870. int idx = (int)p_tag;
  871. if (idx >= 0 && idx < _projects.size()) {
  872. String conf = _projects[idx].path.path_join("project.godot");
  873. List<String> args;
  874. args.push_back(conf);
  875. OS::get_singleton()->create_instance(args);
  876. }
  877. }
  878. // Object methods.
  879. void ProjectList::_bind_methods() {
  880. ADD_SIGNAL(MethodInfo(SIGNAL_LIST_CHANGED));
  881. ADD_SIGNAL(MethodInfo(SIGNAL_SELECTION_CHANGED));
  882. ADD_SIGNAL(MethodInfo(SIGNAL_PROJECT_ASK_OPEN));
  883. }
  884. ProjectList::ProjectList() {
  885. project_list_vbox = memnew(VBoxContainer);
  886. project_list_vbox->set_h_size_flags(Control::SIZE_EXPAND_FILL);
  887. add_child(project_list_vbox);
  888. _config_path = EditorPaths::get_singleton()->get_data_dir().path_join("projects.cfg");
  889. _migrate_config();
  890. }