ソースを参照

Migrate project list to separate config file.

Storing the project/favorites list in the EditorSettings makes it
difficult to version-control your editor configuration, as the file will
continually change as you open new projects. It also means a
configuration can't be shared across machines, as they might not have
the same projects or file layout.

Now the project list is stored in $godot_data_dir/projects.cfg.
Each path is a section, which has a boolean favorite value.
If the new config does not exist, the editor attempts to migrate legacy
EditorSettings-based configuration to the new file.

Fixes godotengine/godot-proposals#1637.
Ryan Roden-Corrent 3 年 前
コミット
366871a3ff
2 ファイル変更115 行追加140 行削除
  1. 114 139
      editor/project_manager.cpp
  2. 1 1
      editor/project_manager.h

+ 114 - 139
editor/project_manager.cpp

@@ -42,6 +42,7 @@
 #include "core/string/translation.h"
 #include "core/version.h"
 #include "editor/editor_file_dialog.h"
+#include "editor/editor_paths.h"
 #include "editor/editor_scale.h"
 #include "editor/editor_settings.h"
 #include "editor/editor_themes.h"
@@ -57,10 +58,6 @@
 #include "servers/navigation_server_3d.h"
 #include "servers/physics_server_2d.h"
 
-static inline String get_project_key_from_path(const String &dir) {
-	return dir.replace("/", "::");
-}
-
 class ProjectDialog : public ConfirmationDialog {
 	GDCLASS(ProjectDialog, ConfirmationDialog);
 
@@ -600,9 +597,6 @@ private:
 			if (dir.ends_with("/")) {
 				dir = dir.substr(0, dir.length() - 1);
 			}
-			String proj = get_project_key_from_path(dir);
-			EditorSettings::get_singleton()->set("projects/" + proj, dir);
-			EditorSettings::get_singleton()->save();
 
 			hide();
 			emit_signal(SNAME("project_created"), dir);
@@ -989,7 +983,6 @@ public:
 
 	// Can often be passed by copy
 	struct Item {
-		String project_key;
 		String project_name;
 		String description;
 		String path;
@@ -1006,8 +999,7 @@ public:
 
 		Item() {}
 
-		Item(const String &p_project,
-				const String &p_name,
+		Item(const String &p_name,
 				const String &p_description,
 				const String &p_path,
 				const String &p_icon,
@@ -1018,7 +1010,6 @@ public:
 				bool p_grayed,
 				bool p_missing,
 				int p_version) {
-			project_key = p_project;
 			project_name = p_name;
 			description = p_description;
 			path = p_path;
@@ -1034,7 +1025,7 @@ public:
 		}
 
 		_FORCE_INLINE_ bool operator==(const Item &l) const {
-			return project_key == l.project_key;
+			return path == l.path;
 		}
 	};
 
@@ -1047,6 +1038,7 @@ public:
 	void _global_menu_open_project(const Variant &p_tag);
 
 	void update_dock_menu();
+	void migrate_config();
 	void load_projects();
 	void set_search_term(String p_search_term);
 	void set_order_option(int p_option);
@@ -1062,6 +1054,8 @@ public:
 	bool is_any_project_missing() const;
 	void erase_missing_projects();
 	int refresh_project(const String &dir_path);
+	void add_project(const String &dir_path, bool favorite);
+	void save_config();
 
 private:
 	static void _bind_methods();
@@ -1082,12 +1076,15 @@ private:
 
 	String _search_term;
 	FilterOption _order_option;
-	HashSet<String> _selected_project_keys;
+	HashSet<String> _selected_project_paths;
 	String _last_clicked; // Project key
 	VBoxContainer *_scroll_children;
 	int _icon_load_index;
 
 	Vector<Item> _projects;
+
+	ConfigFile _config;
+	String _config_path;
 };
 
 struct ProjectListComparator {
@@ -1103,7 +1100,7 @@ struct ProjectListComparator {
 		}
 		switch (order_option) {
 			case PATH:
-				return a.project_key < b.project_key;
+				return a.path < b.path;
 			case EDIT_DATE:
 				return a.last_edited > b.last_edited;
 			default:
@@ -1120,6 +1117,7 @@ ProjectList::ProjectList() {
 
 	_icon_load_index = 0;
 	project_opening_initiated = false;
+	_config_path = EditorPaths::get_singleton()->get_data_dir().plus_file("projects.cfg");
 }
 
 ProjectList::~ProjectList() {
@@ -1171,9 +1169,8 @@ void ProjectList::load_project_icon(int p_index) {
 }
 
 // Load project data from p_property_key and return it in a ProjectList::Item. p_favorite is passed directly into the Item.
-ProjectList::Item ProjectList::load_project_data(const String &p_property_key, bool p_favorite) {
-	String path = EditorSettings::get_singleton()->get(p_property_key);
-	String conf = path.plus_file("project.godot");
+ProjectList::Item ProjectList::load_project_data(const String &p_path, bool p_favorite) {
+	String conf = p_path.plus_file("project.godot");
 	bool grayed = false;
 	bool missing = false;
 
@@ -1209,7 +1206,7 @@ ProjectList::Item ProjectList::load_project_data(const String &p_property_key, b
 		// when editing a project (but not when running it).
 		last_edited = FileAccess::get_modified_time(conf);
 
-		String fscache = path.plus_file(".fscache");
+		String fscache = p_path.plus_file(".fscache");
 		if (FileAccess::exists(fscache)) {
 			uint64_t cache_modified = FileAccess::get_modified_time(fscache);
 			if (cache_modified > last_edited) {
@@ -1222,9 +1219,38 @@ ProjectList::Item ProjectList::load_project_data(const String &p_property_key, b
 		print_line("Project is missing: " + conf);
 	}
 
-	const String project_key = p_property_key.get_slice("/", 1);
+	return Item(project_name, description, p_path, icon, main_scene, unsupported_features, last_edited, p_favorite, grayed, missing, config_version);
+}
+
+void ProjectList::migrate_config() {
+	// Proposal #1637 moved the project list from editor settings to a separate config file
+	// If the new config file doesn't exist, populate it from EditorSettings
+	if (FileAccess::exists(_config_path)) {
+		return;
+	}
+	print_line("Migrating legacy project list");
+
+	List<PropertyInfo> properties;
+	EditorSettings::get_singleton()->get_property_list(&properties);
 
-	return Item(project_key, project_name, description, path, icon, main_scene, unsupported_features, last_edited, p_favorite, grayed, missing, config_version);
+	for (const PropertyInfo &E : properties) {
+		// This is actually something like "projects/C:::Documents::Godot::Projects::MyGame"
+		String property_key = E.name;
+		if (!property_key.begins_with("projects/")) {
+			continue;
+		}
+
+		String path = EditorSettings::get_singleton()->get(property_key);
+		String favoriteKey = "favorite_projects/" + property_key.get_slice("/", 1);
+		bool favorite = EditorSettings::get_singleton()->has_setting(favoriteKey);
+		add_project(path, favorite);
+		if (favorite) {
+			EditorSettings::get_singleton()->erase(favoriteKey);
+		}
+		EditorSettings::get_singleton()->erase(property_key);
+	}
+
+	save_config();
 }
 
 void ProjectList::load_projects() {
@@ -1239,37 +1265,15 @@ void ProjectList::load_projects() {
 	}
 	_projects.clear();
 	_last_clicked = "";
-	_selected_project_keys.clear();
-
-	// 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);
-
-	HashSet<String> favorites;
-	// Find favourites...
-	for (const PropertyInfo &E : properties) {
-		String property_key = E.name;
-		if (property_key.begins_with("favorite_projects/")) {
-			favorites.insert(property_key);
-		}
-	}
-
-	for (const PropertyInfo &E : properties) {
-		// This is actually something like "projects/C:::Documents::Godot::Projects::MyGame"
-		String property_key = E.name;
-		if (!property_key.begins_with("projects/")) {
-			continue;
-		}
-
-		String project_key = property_key.get_slice("/", 1);
-		bool favorite = favorites.has("favorite_projects/" + project_key);
+	_selected_project_paths.clear();
 
-		Item item = load_project_data(property_key, favorite);
+	List<String> sections;
+	_config.load(_config_path);
+	_config.get_sections(&sections);
 
-		_projects.push_back(item);
+	for (const String &path : sections) {
+		bool favorite = _config.get_value(path, "favorite", false);
+		_projects.push_back(load_project_data(path, favorite));
 	}
 
 	// Create controls
@@ -1496,19 +1500,19 @@ void ProjectList::sort_projects() {
 
 const HashSet<String> &ProjectList::get_selected_project_keys() const {
 	// Faster if that's all you need
-	return _selected_project_keys;
+	return _selected_project_paths;
 }
 
 Vector<ProjectList::Item> ProjectList::get_selected_projects() const {
 	Vector<Item> items;
-	if (_selected_project_keys.size() == 0) {
+	if (_selected_project_paths.size() == 0) {
 		return items;
 	}
-	items.resize(_selected_project_keys.size());
+	items.resize(_selected_project_paths.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)) {
+		if (_selected_project_paths.has(item.path)) {
 			items.write[j++] = item;
 		}
 	}
@@ -1522,41 +1526,40 @@ void ProjectList::ensure_project_visible(int p_index) {
 }
 
 int ProjectList::get_single_selected_index() const {
-	if (_selected_project_keys.size() == 0) {
+	if (_selected_project_paths.size() == 0) {
 		// Default selection
 		return 0;
 	}
 	String key;
-	if (_selected_project_keys.size() == 1) {
+	if (_selected_project_paths.size() == 1) {
 		// Only one selected
-		key = *_selected_project_keys.begin();
+		key = *_selected_project_paths.begin();
 	} 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) {
+		if (_projects[i].path == key) {
 			return i;
 		}
 	}
 	return 0;
 }
 
-void ProjectList::remove_project(int p_index, bool p_update_settings) {
+void ProjectList::remove_project(int p_index, bool p_update_config) {
 	const Item item = _projects[p_index]; // Take a copy
 
-	_selected_project_keys.erase(item.project_key);
+	_selected_project_paths.erase(item.path);
 
-	if (_last_clicked == item.project_key) {
+	if (_last_clicked == item.path) {
 		_last_clicked = "";
 	}
 
 	memdelete(item.control);
 	_projects.remove_at(p_index);
 
-	if (p_update_settings) {
-		EditorSettings::get_singleton()->erase("projects/" + item.project_key);
-		EditorSettings::get_singleton()->erase("favorite_projects/" + item.project_key);
+	if (p_update_config) {
+		_config.erase_section(item.path);
 		// Not actually saving the file, in case you are doing more changes to settings
 	}
 
@@ -1594,41 +1597,19 @@ void ProjectList::erase_missing_projects() {
 	}
 
 	print_line("Removed " + itos(deleted_count) + " projects from the list, remaining " + itos(remaining_count) + " projects");
-
-	EditorSettings::get_singleton()->save();
+	save_config();
 }
 
 int ProjectList::refresh_project(const String &dir_path) {
-	// Reads editor settings and reloads information about a specific project.
+	// 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 project_key = get_project_key_from_path(dir_path);
-
-	// 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 (const PropertyInfo &E : properties) {
-			String prop = E.name;
-			if (!found && prop == property_key) {
-				found = true;
-			} else if (!is_favourite && prop == favorite_property_key) {
-				is_favourite = true;
-			}
-		}
-
-		should_be_in_list = found;
-	}
+	bool should_be_in_list = _config.has_section(dir_path);
+	bool is_favourite = _config.get_value(dir_path, "favorite", false);
 
-	bool was_selected = _selected_project_keys.has(project_key);
+	bool was_selected = _selected_project_paths.has(dir_path);
 
 	// Remove item in any case
 	for (int i = 0; i < _projects.size(); ++i) {
@@ -1643,7 +1624,7 @@ int ProjectList::refresh_project(const String &dir_path) {
 	if (should_be_in_list) {
 		// Recreate it with updated info
 
-		Item item = load_project_data(property_key, is_favourite);
+		Item item = load_project_data(dir_path, is_favourite);
 
 		_projects.push_back(item);
 		create_project_item_control(_projects.size() - 1);
@@ -1651,7 +1632,7 @@ int ProjectList::refresh_project(const String &dir_path) {
 		sort_projects();
 
 		for (int i = 0; i < _projects.size(); ++i) {
-			if (_projects[i].project_key == project_key) {
+			if (_projects[i].path == dir_path) {
 				if (was_selected) {
 					select_project(i);
 					ensure_project_visible(i);
@@ -1667,13 +1648,23 @@ int ProjectList::refresh_project(const String &dir_path) {
 	return index;
 }
 
+void ProjectList::add_project(const String &dir_path, bool favorite) {
+	if (!_config.has_section(dir_path)) {
+		_config.set_value(dir_path, "favorite", favorite);
+	}
+}
+
+void ProjectList::save_config() {
+	_config.save(_config_path);
+}
+
 int ProjectList::get_project_count() const {
 	return _projects.size();
 }
 
 void ProjectList::select_project(int p_index) {
 	Vector<Item> previous_selected_items = get_selected_projects();
-	_selected_project_keys.clear();
+	_selected_project_paths.clear();
 
 	for (int i = 0; i < previous_selected_items.size(); ++i) {
 		previous_selected_items[i].control->update();
@@ -1695,7 +1686,7 @@ void ProjectList::select_first_visible_project() {
 
 	if (!found) {
 		// Deselect all projects if there are no visible projects in the list.
-		_selected_project_keys.clear();
+		_selected_project_paths.clear();
 	}
 }
 
@@ -1717,24 +1708,23 @@ void ProjectList::select_range(int p_begin, int p_end) {
 
 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);
+	if (_selected_project_paths.has(item.path)) {
+		_selected_project_paths.erase(item.path);
 	} else {
-		_selected_project_keys.insert(item.project_key);
+		_selected_project_paths.insert(item.path);
 	}
 	item.control->update();
 }
 
 void ProjectList::erase_selected_projects(bool p_delete_project_contents) {
-	if (_selected_project_keys.size() == 0) {
+	if (_selected_project_paths.size() == 0) {
 		return;
 	}
 
 	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);
+		if (_selected_project_paths.has(item.path) && item.control->is_visible()) {
+			_config.erase_section(item.path);
 
 			if (p_delete_project_contents) {
 				OS::get_singleton()->move_to_trash(item.path);
@@ -1746,9 +1736,8 @@ void ProjectList::erase_selected_projects(bool p_delete_project_contents) {
 		}
 	}
 
-	EditorSettings::get_singleton()->save();
-
-	_selected_project_keys.clear();
+	save_config();
+	_selected_project_paths.clear();
 	_last_clicked = "";
 
 	update_dock_menu();
@@ -1764,9 +1753,9 @@ void ProjectList::_panel_draw(Node *p_hb) {
 		hb->draw_line(Point2(0, hb->get_size().y + 1), Point2(hb->get_size().x, hb->get_size().y + 1), get_theme_color(SNAME("guide_color"), SNAME("Tree")));
 	}
 
-	String key = _projects[p_hb->get_index()].project_key;
+	String key = _projects[p_hb->get_index()].path;
 
-	if (_selected_project_keys.has(key)) {
+	if (_selected_project_paths.has(key)) {
 		hb->draw_style_box(get_theme_stylebox(SNAME("selected"), SNAME("Tree")), Rect2(Point2(), hb->get_size()));
 	}
 }
@@ -1778,11 +1767,11 @@ void ProjectList::_panel_input(const Ref<InputEvent> &p_ev, Node *p_hb) {
 	const Item &clicked_project = _projects[clicked_index];
 
 	if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
-		if (mb->is_shift_pressed() && _selected_project_keys.size() > 0 && !_last_clicked.is_empty() && clicked_project.project_key != _last_clicked) {
+		if (mb->is_shift_pressed() && _selected_project_paths.size() > 0 && !_last_clicked.is_empty() && clicked_project.path != _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) {
+				if (p.path == _last_clicked) {
 					anchor_index = p.control->get_index();
 					break;
 				}
@@ -1794,7 +1783,7 @@ void ProjectList::_panel_input(const Ref<InputEvent> &p_ev, Node *p_hb) {
 			toggle_select(clicked_index);
 
 		} else {
-			_last_clicked = clicked_project.project_key;
+			_last_clicked = clicked_project.path;
 			select_project(clicked_index);
 		}
 
@@ -1816,12 +1805,8 @@ void ProjectList::_favorite_pressed(Node *p_hb) {
 
 	item.favorite = !item.favorite;
 
-	if (item.favorite) {
-		EditorSettings::get_singleton()->set("favorite_projects/" + item.project_key, item.path);
-	} else {
-		EditorSettings::get_singleton()->erase("favorite_projects/" + item.project_key);
-	}
-	EditorSettings::get_singleton()->save();
+	_config.set_value(item.path, "favorite", item.favorite);
+	save_config();
 
 	_projects.write[index] = item;
 
@@ -1831,7 +1816,7 @@ void ProjectList::_favorite_pressed(Node *p_hb) {
 
 	if (item.favorite) {
 		for (int i = 0; i < _projects.size(); ++i) {
-			if (_projects[i].project_key == item.project_key) {
+			if (_projects[i].path == item.path) {
 				ensure_project_visible(i);
 				break;
 			}
@@ -2089,6 +2074,8 @@ void ProjectManager::_on_projects_updated() {
 }
 
 void ProjectManager::_on_project_created(const String &dir) {
+	_project_list->add_project(dir, false);
+	_project_list->save_config();
 	search_box->clear();
 	int i = _project_list->refresh_project(dir);
 	_project_list->select_project(i);
@@ -2109,9 +2096,7 @@ void ProjectManager::_open_selected_projects() {
 
 	const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
 
-	for (const String &E : selected_list) {
-		const String &selected = E;
-		String path = EditorSettings::get_singleton()->get("projects/" + selected);
+	for (const String &path : selected_list) {
 		String conf = path.plus_file("project.godot");
 
 		if (!FileAccess::exists(conf)) {
@@ -2120,7 +2105,7 @@ void ProjectManager::_open_selected_projects() {
 			return;
 		}
 
-		print_line("Editing project: " + path + " (" + selected + ")");
+		print_line("Editing project: " + path);
 
 		List<String> args;
 
@@ -2244,8 +2229,7 @@ void ProjectManager::_run_project_confirm() {
 			continue;
 		}
 
-		const String &selected = selected_list[i].project_key;
-		String path = EditorSettings::get_singleton()->get("projects/" + selected);
+		const String &path = selected_list[i].path;
 
 		// `.substr(6)` on `ProjectSettings::get_singleton()->get_imported_files_path()` strips away the leading "res://".
 		if (!DirAccess::exists(path.plus_file(ProjectSettings::get_singleton()->get_imported_files_path().substr(6)))) {
@@ -2254,7 +2238,7 @@ void ProjectManager::_run_project_confirm() {
 			continue;
 		}
 
-		print_line("Running project: " + path + " (" + selected + ")");
+		print_line("Running project: " + path);
 
 		List<String> args;
 
@@ -2285,7 +2269,7 @@ void ProjectManager::_run_project() {
 	}
 }
 
-void ProjectManager::_scan_dir(const String &path, List<String> *r_projects) {
+void ProjectManager::_scan_dir(const String &path) {
 	Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
 	Error error = da->change_dir(path);
 	ERR_FAIL_COND_MSG(error != OK, "Could not scan directory at: " + path);
@@ -2293,26 +2277,18 @@ void ProjectManager::_scan_dir(const String &path, List<String> *r_projects) {
 	String n = da->get_next();
 	while (!n.is_empty()) {
 		if (da->current_is_dir() && !n.begins_with(".")) {
-			_scan_dir(da->get_current_dir().plus_file(n), r_projects);
+			_scan_dir(da->get_current_dir().plus_file(n));
 		} else if (n == "project.godot") {
-			r_projects->push_back(da->get_current_dir());
+			_project_list->add_project(da->get_current_dir(), false);
 		}
 		n = da->get_next();
 	}
 	da->list_dir_end();
 }
-
 void ProjectManager::_scan_begin(const String &p_base) {
 	print_line("Scanning projects at: " + p_base);
-	List<String> projects;
-	_scan_dir(p_base, &projects);
-	print_line("Found " + itos(projects.size()) + " projects.");
-
-	for (const String &E : projects) {
-		String proj = get_project_key_from_path(E);
-		EditorSettings::get_singleton()->set("projects/" + proj, E);
-	}
-	EditorSettings::get_singleton()->save();
+	_scan_dir(p_base);
+	_project_list->save_config();
 	_load_recent_projects();
 }
 
@@ -2338,9 +2314,7 @@ void ProjectManager::_rename_project() {
 	}
 
 	for (const String &E : selected_list) {
-		const String &selected = E;
-		String path = EditorSettings::get_singleton()->get("projects/" + selected);
-		npdialog->set_project_path(path);
+		npdialog->set_project_path(E);
 		npdialog->set_mode(ProjectDialog::MODE_RENAME);
 		npdialog->show_dialog();
 	}
@@ -2871,6 +2845,7 @@ ProjectManager::ProjectManager() {
 		_build_icon_type_cache(get_theme());
 	}
 
+	_project_list->migrate_config();
 	_load_recent_projects();
 
 	Ref<DirAccess> dir_access = DirAccess::create(DirAccess::AccessType::ACCESS_FILESYSTEM);

+ 1 - 1
editor/project_manager.h

@@ -125,7 +125,7 @@ class ProjectManager : public Control {
 	void _on_projects_updated();
 	void _scan_multiple_folders(PackedStringArray p_files);
 	void _scan_begin(const String &p_base);
-	void _scan_dir(const String &path, List<String> *r_projects);
+	void _scan_dir(const String &path);
 
 	void _install_project(const String &p_zip_path, const String &p_title);