2
0
Эх сурвалжийг харах

Replace empty list dialog with an integrated panel

Yuri Sizov 1 жил өмнө
parent
commit
28e8a4c0ee

+ 10 - 10
editor/plugins/asset_library_editor_plugin.cpp

@@ -1043,6 +1043,7 @@ HBoxContainer *EditorAssetLibrary::_make_pages(int p_page, int p_page_count, int
 
 	Button *first = memnew(Button);
 	first->set_text(TTR("First", "Pagination"));
+	first->set_theme_type_variation("PanelBackgroundButton");
 	if (p_page != 0) {
 		first->connect("pressed", callable_mp(this, &EditorAssetLibrary::_search).bind(0));
 	} else {
@@ -1053,6 +1054,7 @@ HBoxContainer *EditorAssetLibrary::_make_pages(int p_page, int p_page_count, int
 
 	Button *prev = memnew(Button);
 	prev->set_text(TTR("Previous", "Pagination"));
+	prev->set_theme_type_variation("PanelBackgroundButton");
 	if (p_page > 0) {
 		prev->connect("pressed", callable_mp(this, &EditorAssetLibrary::_search).bind(p_page - 1));
 	} else {
@@ -1063,26 +1065,22 @@ HBoxContainer *EditorAssetLibrary::_make_pages(int p_page, int p_page_count, int
 	hbc->add_child(memnew(VSeparator));
 
 	for (int i = from; i < to; i++) {
+		Button *current = memnew(Button);
+		// Add padding to make page number buttons easier to click.
+		current->set_text(vformat(" %d ", i + 1));
+		current->set_theme_type_variation("PanelBackgroundButton");
 		if (i == p_page) {
-			Button *current = memnew(Button);
-			// Keep the extended padding for the currently active page (see below).
-			current->set_text(vformat(" %d ", i + 1));
 			current->set_disabled(true);
 			current->set_focus_mode(Control::FOCUS_NONE);
-
-			hbc->add_child(current);
 		} else {
-			Button *current = memnew(Button);
-			// Add padding to make page number buttons easier to click.
-			current->set_text(vformat(" %d ", i + 1));
 			current->connect("pressed", callable_mp(this, &EditorAssetLibrary::_search).bind(i));
-
-			hbc->add_child(current);
 		}
+		hbc->add_child(current);
 	}
 
 	Button *next = memnew(Button);
 	next->set_text(TTR("Next", "Pagination"));
+	next->set_theme_type_variation("PanelBackgroundButton");
 	if (p_page < p_page_count - 1) {
 		next->connect("pressed", callable_mp(this, &EditorAssetLibrary::_search).bind(p_page + 1));
 	} else {
@@ -1094,6 +1092,7 @@ HBoxContainer *EditorAssetLibrary::_make_pages(int p_page, int p_page_count, int
 
 	Button *last = memnew(Button);
 	last->set_text(TTR("Last", "Pagination"));
+	last->set_theme_type_variation("PanelBackgroundButton");
 	if (p_page != p_page_count - 1) {
 		last->connect("pressed", callable_mp(this, &EditorAssetLibrary::_search).bind(p_page_count - 1));
 	} else {
@@ -1619,6 +1618,7 @@ EditorAssetLibrary::EditorAssetLibrary(bool p_templates_only) {
 
 	library_message_button = memnew(Button);
 	library_message_button->set_h_size_flags(SIZE_SHRINK_CENTER);
+	library_message_button->set_theme_type_variation("PanelBackgroundButton");
 	library_message_box->add_child(library_message_button);
 
 	asset_top_page = memnew(HBoxContainer);

+ 186 - 145
editor/project_manager.cpp

@@ -86,14 +86,10 @@ void ProjectManager::_notification(int p_what) {
 		case NOTIFICATION_READY: {
 			const int default_sorting = (int)EDITOR_GET("project_manager/sorting_order");
 			filter_option->select(default_sorting);
-			_project_list->set_order_option(default_sorting);
+			project_list->set_order_option(default_sorting);
 
 			_select_main_view(MAIN_VIEW_PROJECTS);
-
-			// Suggest browsing asset library to get templates/demos.
-			if (asset_library && _project_list->get_project_count() == 0) {
-				_suggest_asset_library();
-			}
+			_update_list_placeholder();
 		} break;
 
 		case NOTIFICATION_VISIBILITY_CHANGED: {
@@ -112,6 +108,7 @@ void ProjectManager::_notification(int p_what) {
 			if (EditorThemeManager::is_generated_theme_outdated()) {
 				_update_theme();
 			}
+			_update_list_placeholder();
 		} break;
 	}
 }
@@ -224,7 +221,14 @@ void ProjectManager::_update_theme(bool p_skip_creation) {
 		// Project list.
 		{
 			loading_label->add_theme_font_override("font", get_theme_font(SNAME("bold"), EditorStringName(EditorFonts)));
-			search_panel->add_theme_style_override("panel", get_theme_stylebox(SNAME("search_panel"), SNAME("ProjectManager")));
+			project_list_panel->add_theme_style_override("panel", get_theme_stylebox(SNAME("project_list"), SNAME("ProjectManager")));
+
+			empty_list_create_project->set_icon(get_editor_theme_icon(SNAME("Add")));
+			empty_list_import_project->set_icon(get_editor_theme_icon(SNAME("Load")));
+			empty_list_open_assetlib->set_icon(get_editor_theme_icon(SNAME("AssetLib")));
+
+			empty_list_online_warning->add_theme_font_override("font", get_theme_font(SNAME("italic"), EditorStringName(EditorFonts)));
+			empty_list_online_warning->add_theme_color_override("font_color", get_theme_color(SNAME("font_placeholder_color"), EditorStringName(Editor)));
 
 			// Top bar.
 			search_box->set_right_icon(get_editor_theme_icon(SNAME("Search")));
@@ -340,43 +344,6 @@ void ProjectManager::_show_about() {
 	about_dialog->popup_centered(Size2(780, 500) * EDSCALE);
 }
 
-void ProjectManager::_suggest_asset_library() {
-	if (!suggest_asset_library_dialog) {
-		suggest_asset_library_dialog = memnew(ConfirmationDialog);
-		suggest_asset_library_dialog->set_title(TTR("Getting Started with Godot"));
-		add_child(suggest_asset_library_dialog);
-		suggest_asset_library_dialog->connect("confirmed", callable_mp(this, &ProjectManager::_open_asset_library_confirmed));
-
-		VBoxContainer *suggest_vbox = memnew(VBoxContainer);
-		suggest_asset_library_dialog->add_child(suggest_vbox);
-
-		suggest_asset_library_label = memnew(RichTextLabel);
-		suggest_asset_library_label->set_use_bbcode(true);
-		suggest_asset_library_label->set_fit_content(true);
-		suggest_asset_library_label->set_h_size_flags(SIZE_EXPAND_FILL);
-		suggest_asset_library_label->add_theme_style_override("normal", memnew(StyleBoxEmpty));
-		suggest_vbox->add_child(suggest_asset_library_label);
-	}
-
-	const int network_mode = EDITOR_GET("network/connection/network_mode");
-	if (network_mode == EditorSettings::NETWORK_OFFLINE) {
-		const String line1 = TTR("You don't have any projects yet.");
-		const String line2 = TTR("Get started with one of the official project templates from the Asset Library!");
-		const String line3 = TTR("Note: The Asset Library requires an online connection and involves sending data over the internet.");
-
-		suggest_asset_library_label->set_text(vformat("%s\n\n[b]%s[/b]\n\n[i]%s[/i]", line1, line2, line3));
-		suggest_asset_library_dialog->set_ok_button_text(TTR("Go Online and Open Asset Library"));
-	} else {
-		const String line1 = TTR("You don't have any projects yet.");
-		const String line2 = TTR("Get started with one of the official project templates from the Asset Library!");
-
-		suggest_asset_library_label->set_text(vformat("%s\n\n[b]%s[/b]", line1, line2));
-		suggest_asset_library_dialog->set_ok_button_text(TTR("Open Asset Library"));
-	}
-
-	suggest_asset_library_dialog->popup_centered(Size2(540, 100) * EDSCALE);
-}
-
 void ProjectManager::_open_asset_library_confirmed() {
 	const int network_mode = EDITOR_GET("network/connection/network_mode");
 	if (network_mode == EditorSettings::NETWORK_OFFLINE) {
@@ -389,6 +356,11 @@ void ProjectManager::_open_asset_library_confirmed() {
 	_select_main_view(MAIN_VIEW_ASSETLIB);
 }
 
+void ProjectManager::_show_error(const String &p_message, const Size2 &p_min_size) {
+	error_dialog->set_text(p_message);
+	error_dialog->popup_centered(p_min_size);
+}
+
 void ProjectManager::_dim_window() {
 	// This method must be called before calling `get_tree()->quit()`.
 	// Otherwise, its effect won't be visible
@@ -423,12 +395,32 @@ void ProjectManager::_version_button_pressed() {
 
 // Project list.
 
+void ProjectManager::_update_list_placeholder() {
+	if (project_list->get_project_count() > 0) {
+		empty_list_placeholder->hide();
+		return;
+	}
+
+	empty_list_open_assetlib->set_visible(asset_library);
+
+	const int network_mode = EDITOR_GET("network/connection/network_mode");
+	if (network_mode == EditorSettings::NETWORK_OFFLINE) {
+		empty_list_open_assetlib->set_text(TTR("Go Online and Open Asset Library"));
+		empty_list_online_warning->set_visible(true);
+	} else {
+		empty_list_open_assetlib->set_text(TTR("Open Asset Library"));
+		empty_list_online_warning->set_visible(false);
+	}
+
+	empty_list_placeholder->show();
+}
+
 void ProjectManager::_scan_projects() {
 	scan_dir->popup_file_dialog();
 }
 
 void ProjectManager::_run_project() {
-	const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
+	const HashSet<String> &selected_list = project_list->get_selected_project_keys();
 
 	if (selected_list.size() < 1) {
 		return;
@@ -443,13 +435,12 @@ void ProjectManager::_run_project() {
 }
 
 void ProjectManager::_run_project_confirm() {
-	Vector<ProjectList::Item> selected_list = _project_list->get_selected_projects();
+	Vector<ProjectList::Item> selected_list = project_list->get_selected_projects();
 
 	for (int i = 0; i < selected_list.size(); ++i) {
 		const String &selected_main = selected_list[i].main_scene;
 		if (selected_main.is_empty()) {
-			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();
+			_show_error(TTR("Can't run project: Project has no main scene defined.\nPlease edit the project and set the main scene in the Project Settings under the \"Application\" category."));
 			continue;
 		}
 
@@ -457,8 +448,7 @@ void ProjectManager::_run_project_confirm() {
 
 		// `.substr(6)` on `ProjectSettings::get_singleton()->get_imported_files_path()` strips away the leading "res://".
 		if (!DirAccess::exists(path.path_join(ProjectSettings::get_singleton()->get_imported_files_path().substr(6)))) {
-			run_error_diag->set_text(TTR("Can't run project: Assets need to be imported.\nPlease edit the project to trigger the initial import."));
-			run_error_diag->popup_centered();
+			_show_error(TTR("Can't run project: Assets need to be imported first.\nPlease edit the project to trigger the initial import."));
 			continue;
 		}
 
@@ -483,14 +473,13 @@ void ProjectManager::_open_selected_projects() {
 	// This is especially important for the Web project manager.
 	loading_label->show();
 
-	const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
+	const HashSet<String> &selected_list = project_list->get_selected_project_keys();
 
 	for (const String &path : selected_list) {
 		String conf = path.path_join("project.godot");
 
 		if (!FileAccess::exists(conf)) {
-			dialog_error->set_text(vformat(TTR("Can't open project at '%s'."), path));
-			dialog_error->popup_centered();
+			_show_error(vformat(TTR("Can't open project at '%s'."), path));
 			return;
 		}
 
@@ -511,14 +500,14 @@ void ProjectManager::_open_selected_projects() {
 		ERR_FAIL_COND(err);
 	}
 
-	_project_list->project_opening_initiated = true;
+	project_list->project_opening_initiated = true;
 
 	_dim_window();
 	get_tree()->quit();
 }
 
 void ProjectManager::_open_selected_projects_ask() {
-	const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
+	const HashSet<String> &selected_list = project_list->get_selected_project_keys();
 
 	if (selected_list.size() < 1) {
 		return;
@@ -532,7 +521,7 @@ void ProjectManager::_open_selected_projects_ask() {
 		return;
 	}
 
-	ProjectList::Item project = _project_list->get_selected_projects()[0];
+	ProjectList::Item project = project_list->get_selected_projects()[0];
 	if (project.missing) {
 		return;
 	}
@@ -569,8 +558,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 \"%s\" at the following path:\n\n%s\n\nThe project settings were created by a newer engine version, whose settings are not compatible with this version."), project.project_name, project.path));
-		dialog_error->popup_centered(popup_min_size);
+		_show_error(vformat(TTR("Can't open project \"%s\" at the following path:\n\n%s\n\nThe project settings were created by a newer engine version, whose settings are not compatible with this version."), project.project_name, project.path), popup_min_size);
 		return;
 	}
 	// Check if the project is using features not supported by this build of Godot.
@@ -608,38 +596,38 @@ void ProjectManager::_open_selected_projects_ask() {
 }
 
 void ProjectManager::_install_project(const String &p_zip_path, const String &p_title) {
-	npdialog->set_mode(ProjectDialog::MODE_INSTALL);
-	npdialog->set_zip_path(p_zip_path);
-	npdialog->set_zip_title(p_title);
-	npdialog->show_dialog();
+	project_dialog->set_mode(ProjectDialog::MODE_INSTALL);
+	project_dialog->set_zip_path(p_zip_path);
+	project_dialog->set_zip_title(p_title);
+	project_dialog->show_dialog();
 }
 
 void ProjectManager::_import_project() {
-	npdialog->set_mode(ProjectDialog::MODE_IMPORT);
-	npdialog->ask_for_path_and_show();
+	project_dialog->set_mode(ProjectDialog::MODE_IMPORT);
+	project_dialog->ask_for_path_and_show();
 }
 
 void ProjectManager::_new_project() {
-	npdialog->set_mode(ProjectDialog::MODE_NEW);
-	npdialog->show_dialog();
+	project_dialog->set_mode(ProjectDialog::MODE_NEW);
+	project_dialog->show_dialog();
 }
 
 void ProjectManager::_rename_project() {
-	const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
+	const HashSet<String> &selected_list = project_list->get_selected_project_keys();
 
 	if (selected_list.size() == 0) {
 		return;
 	}
 
 	for (const String &E : selected_list) {
-		npdialog->set_project_path(E);
-		npdialog->set_mode(ProjectDialog::MODE_RENAME);
-		npdialog->show_dialog();
+		project_dialog->set_project_path(E);
+		project_dialog->set_mode(ProjectDialog::MODE_RENAME);
+		project_dialog->show_dialog();
 	}
 }
 
 void ProjectManager::_erase_project() {
-	const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
+	const HashSet<String> &selected_list = project_list->get_selected_project_keys();
 
 	if (selected_list.size() == 0) {
 		return;
@@ -663,17 +651,19 @@ void ProjectManager::_erase_missing_projects() {
 }
 
 void ProjectManager::_erase_project_confirm() {
-	_project_list->erase_selected_projects(false);
+	project_list->erase_selected_projects(false);
 	_update_project_buttons();
+	_update_list_placeholder();
 }
 
 void ProjectManager::_erase_missing_projects_confirm() {
-	_project_list->erase_missing_projects();
+	project_list->erase_missing_projects();
 	_update_project_buttons();
+	_update_list_placeholder();
 }
 
 void ProjectManager::_update_project_buttons() {
-	Vector<ProjectList::Item> selected_projects = _project_list->get_selected_projects();
+	Vector<ProjectList::Item> selected_projects = project_list->get_selected_projects();
 	bool empty_selection = selected_projects.is_empty();
 
 	bool is_missing_project_selected = false;
@@ -690,48 +680,48 @@ void ProjectManager::_update_project_buttons() {
 	manage_tags_btn->set_disabled(empty_selection || is_missing_project_selected || selected_projects.size() > 1);
 	run_btn->set_disabled(empty_selection || is_missing_project_selected);
 
-	erase_missing_btn->set_disabled(!_project_list->is_any_project_missing());
+	erase_missing_btn->set_disabled(!project_list->is_any_project_missing());
 }
 
 void ProjectManager::_on_projects_updated() {
-	Vector<ProjectList::Item> selected_projects = _project_list->get_selected_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);
+		index = project_list->refresh_project(selected_projects[i].path);
 	}
 	if (index != -1) {
-		_project_list->ensure_project_visible(index);
+		project_list->ensure_project_visible(index);
 	}
 
-	_project_list->update_dock_menu();
+	project_list->update_dock_menu();
 }
 
 void ProjectManager::_on_project_created(const String &dir) {
-	_project_list->add_project(dir, false);
-	_project_list->save_config();
+	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);
-	_project_list->ensure_project_visible(i);
+	int i = project_list->refresh_project(dir);
+	project_list->select_project(i);
+	project_list->ensure_project_visible(i);
 	_open_selected_projects_ask();
 
-	_project_list->update_dock_menu();
+	project_list->update_dock_menu();
 }
 
 void ProjectManager::_on_order_option_changed(int p_idx) {
 	if (is_inside_tree()) {
-		_project_list->set_order_option(p_idx);
+		project_list->set_order_option(p_idx);
 	}
 }
 
 void ProjectManager::_on_search_term_changed(const String &p_term) {
-	_project_list->set_search_term(p_term);
-	_project_list->sort_projects();
+	project_list->set_search_term(p_term);
+	project_list->sort_projects();
 
 	// Select the first visible project in the list.
 	// This makes it possible to open a project without ever touching the mouse,
 	// as the search field is automatically focused on startup.
-	_project_list->select_first_visible_project();
+	project_list->select_first_visible_project();
 	_update_project_buttons();
 }
 
@@ -754,7 +744,7 @@ void ProjectManager::_manage_project_tags() {
 		project_tags->get_child(i)->queue_free();
 	}
 
-	const ProjectList::Item item = _project_list->get_selected_projects()[0];
+	const ProjectList::Item item = project_list->get_selected_projects()[0];
 	current_project_tags = item.tags;
 	for (const String &tag : current_project_tags) {
 		ProjectTag *tag_control = memnew(ProjectTag(tag, true));
@@ -798,7 +788,7 @@ void ProjectManager::_apply_project_tags() {
 	}
 
 	ConfigFile cfg;
-	const String project_godot = _project_list->get_selected_projects()[0].path.path_join("project.godot");
+	const String project_godot = project_list->get_selected_projects()[0].path.path_join("project.godot");
 	Error err = cfg.load(project_godot);
 	if (err != OK) {
 		tag_edit_error->set_text(vformat(TTR("Couldn't load project at '%s' (error %d). It may be missing or corrupted."), project_godot, err));
@@ -876,7 +866,7 @@ void ProjectManager::_full_convert_button_pressed() {
 }
 
 void ProjectManager::_perform_full_project_conversion() {
-	Vector<ProjectList::Item> selected_list = _project_list->get_selected_projects();
+	Vector<ProjectList::Item> selected_list = project_list->get_selected_projects();
 	if (selected_list.is_empty()) {
 		return;
 	}
@@ -894,7 +884,7 @@ void ProjectManager::_perform_full_project_conversion() {
 	Error err = OS::get_singleton()->create_instance(args);
 	ERR_FAIL_COND(err);
 
-	_project_list->set_project_version(path, GODOT4_CONFIG_VERSION);
+	project_list->set_project_version(path, GODOT4_CONFIG_VERSION);
 }
 
 // Input and I/O.
@@ -930,15 +920,15 @@ void ProjectManager::shortcut_input(const Ref<InputEvent> &p_ev) {
 				_open_selected_projects_ask();
 			} break;
 			case Key::HOME: {
-				if (_project_list->get_project_count() > 0) {
-					_project_list->select_project(0);
+				if (project_list->get_project_count() > 0) {
+					project_list->select_project(0);
 					_update_project_buttons();
 				}
 
 			} break;
 			case Key::END: {
-				if (_project_list->get_project_count() > 0) {
-					_project_list->select_project(_project_list->get_project_count() - 1);
+				if (project_list->get_project_count() > 0) {
+					project_list->select_project(project_list->get_project_count() - 1);
 					_update_project_buttons();
 				}
 
@@ -948,10 +938,10 @@ void ProjectManager::shortcut_input(const Ref<InputEvent> &p_ev) {
 					break;
 				}
 
-				int index = _project_list->get_single_selected_index();
+				int index = project_list->get_single_selected_index();
 				if (index > 0) {
-					_project_list->select_project(index - 1);
-					_project_list->ensure_project_visible(index - 1);
+					project_list->select_project(index - 1);
+					project_list->ensure_project_visible(index - 1);
 					_update_project_buttons();
 				}
 
@@ -962,10 +952,10 @@ void ProjectManager::shortcut_input(const Ref<InputEvent> &p_ev) {
 					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);
+				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();
 				}
 
@@ -1008,7 +998,7 @@ void ProjectManager::_files_dropped(PackedStringArray p_files) {
 	for (const String &E : folders_set) {
 		folders.push_back(E);
 	}
-	_project_list->find_projects_multiple(folders);
+	project_list->find_projects_multiple(folders);
 }
 
 // Object methods.
@@ -1159,7 +1149,7 @@ ProjectManager::ProjectManager() {
 			local_projects_vb->add_child(hb);
 
 			create_btn = memnew(Button);
-			create_btn->set_text(TTR("New"));
+			create_btn->set_text(TTR("Create"));
 			create_btn->set_shortcut(ED_SHORTCUT("project_manager/new_project", TTR("New Project"), KeyModifierMask::CMD_OR_CTRL | Key::N));
 			create_btn->connect("pressed", callable_mp(this, &ProjectManager::_new_project));
 			hb->add_child(create_btn);
@@ -1178,9 +1168,8 @@ ProjectManager::ProjectManager() {
 
 			loading_label = memnew(Label(TTR("Loading, please wait...")));
 			loading_label->set_h_size_flags(Control::SIZE_EXPAND_FILL);
-			hb->add_child(loading_label);
-			// The loading label is shown later.
 			loading_label->hide();
+			hb->add_child(loading_label);
 
 			search_box = memnew(LineEdit);
 			search_box->set_placeholder(TTR("Filter Projects"));
@@ -1215,61 +1204,116 @@ ProjectManager::ProjectManager() {
 
 		// Project list and its sidebar.
 		{
-			HBoxContainer *search_tree_hb = memnew(HBoxContainer);
-			local_projects_vb->add_child(search_tree_hb);
-			search_tree_hb->set_v_size_flags(Control::SIZE_EXPAND_FILL);
-
-			search_panel = memnew(PanelContainer);
-			search_panel->set_h_size_flags(Control::SIZE_EXPAND_FILL);
-			search_tree_hb->add_child(search_panel);
-
-			_project_list = memnew(ProjectList);
-			_project_list->set_horizontal_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);
-			search_panel->add_child(_project_list);
-			_project_list->connect(ProjectList::SIGNAL_LIST_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons));
-			_project_list->connect(ProjectList::SIGNAL_SELECTION_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons));
-			_project_list->connect(ProjectList::SIGNAL_PROJECT_ASK_OPEN, callable_mp(this, &ProjectManager::_open_selected_projects_ask));
+			HBoxContainer *project_list_hbox = memnew(HBoxContainer);
+			local_projects_vb->add_child(project_list_hbox);
+			project_list_hbox->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+
+			project_list_panel = memnew(PanelContainer);
+			project_list_panel->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+			project_list_hbox->add_child(project_list_panel);
+
+			project_list = memnew(ProjectList);
+			project_list->set_horizontal_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);
+			project_list_panel->add_child(project_list);
+			project_list->connect(ProjectList::SIGNAL_LIST_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons));
+			project_list->connect(ProjectList::SIGNAL_LIST_CHANGED, callable_mp(this, &ProjectManager::_update_list_placeholder));
+			project_list->connect(ProjectList::SIGNAL_SELECTION_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons));
+			project_list->connect(ProjectList::SIGNAL_PROJECT_ASK_OPEN, callable_mp(this, &ProjectManager::_open_selected_projects_ask));
+
+			// Empty project list placeholder.
+			{
+				empty_list_placeholder = memnew(VBoxContainer);
+				empty_list_placeholder->set_v_size_flags(Control::SIZE_SHRINK_CENTER);
+				empty_list_placeholder->add_theme_constant_override("separation", 16 * EDSCALE);
+				empty_list_placeholder->hide();
+				project_list_panel->add_child(empty_list_placeholder);
+
+				RichTextLabel *empty_list_message = memnew(RichTextLabel);
+				empty_list_message->set_use_bbcode(true);
+				empty_list_message->set_fit_content(true);
+				empty_list_message->set_h_size_flags(SIZE_EXPAND_FILL);
+				empty_list_message->add_theme_style_override("normal", memnew(StyleBoxEmpty));
+
+				const String line1 = TTR("You don't have any projects yet.");
+				const String line2 = TTR("Get started by creating a new one,\nimporting one that exists, or by downloading a project template from the Asset Library!");
+				empty_list_message->set_text(vformat("[center][b]%s[/b] %s[/center]", line1, line2));
+				empty_list_placeholder->add_child(empty_list_message);
+
+				HBoxContainer *empty_list_actions = memnew(HBoxContainer);
+				empty_list_actions->set_alignment(BoxContainer::ALIGNMENT_CENTER);
+				empty_list_placeholder->add_child(empty_list_actions);
+
+				empty_list_create_project = memnew(Button);
+				empty_list_create_project->set_text(TTR("Create New Project"));
+				empty_list_create_project->set_theme_type_variation("PanelBackgroundButton");
+				empty_list_actions->add_child(empty_list_create_project);
+				empty_list_create_project->connect("pressed", callable_mp(this, &ProjectManager::_new_project));
+
+				empty_list_import_project = memnew(Button);
+				empty_list_import_project->set_text(TTR("Import Existing Project"));
+				empty_list_import_project->set_theme_type_variation("PanelBackgroundButton");
+				empty_list_actions->add_child(empty_list_import_project);
+				empty_list_import_project->connect("pressed", callable_mp(this, &ProjectManager::_import_project));
+
+				empty_list_open_assetlib = memnew(Button);
+				empty_list_open_assetlib->set_text(TTR("Open Asset Library"));
+				empty_list_open_assetlib->set_theme_type_variation("PanelBackgroundButton");
+				empty_list_actions->add_child(empty_list_open_assetlib);
+				empty_list_open_assetlib->connect("pressed", callable_mp(this, &ProjectManager::_open_asset_library_confirmed));
+
+				empty_list_online_warning = memnew(Label);
+				empty_list_online_warning->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER);
+				empty_list_online_warning->set_custom_minimum_size(Size2(220, 0) * EDSCALE);
+				empty_list_online_warning->set_autowrap_mode(TextServer::AUTOWRAP_WORD);
+				empty_list_online_warning->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+				empty_list_online_warning->set_text(TTR("Note: The Asset Library requires an online connection and involves sending data over the internet."));
+				empty_list_placeholder->add_child(empty_list_online_warning);
+			}
 
 			// The side bar with the edit, run, rename, etc. buttons.
-			VBoxContainer *tree_vb = memnew(VBoxContainer);
-			tree_vb->set_custom_minimum_size(Size2(120, 120));
-			search_tree_hb->add_child(tree_vb);
+			VBoxContainer *project_list_sidebar = memnew(VBoxContainer);
+			project_list_sidebar->set_custom_minimum_size(Size2(120, 120));
+			project_list_hbox->add_child(project_list_sidebar);
 
-			tree_vb->add_child(memnew(HSeparator));
+			project_list_sidebar->add_child(memnew(HSeparator));
 
 			open_btn = memnew(Button);
 			open_btn->set_text(TTR("Edit"));
 			open_btn->set_shortcut(ED_SHORTCUT("project_manager/edit_project", TTR("Edit Project"), KeyModifierMask::CMD_OR_CTRL | Key::E));
 			open_btn->connect("pressed", callable_mp(this, &ProjectManager::_open_selected_projects_ask));
-			tree_vb->add_child(open_btn);
+			project_list_sidebar->add_child(open_btn);
 
 			run_btn = memnew(Button);
 			run_btn->set_text(TTR("Run"));
 			run_btn->set_shortcut(ED_SHORTCUT("project_manager/run_project", TTR("Run Project"), KeyModifierMask::CMD_OR_CTRL | Key::R));
 			run_btn->connect("pressed", callable_mp(this, &ProjectManager::_run_project));
-			tree_vb->add_child(run_btn);
+			project_list_sidebar->add_child(run_btn);
 
 			rename_btn = memnew(Button);
 			rename_btn->set_text(TTR("Rename"));
 			// The F2 shortcut isn't overridden with Enter on macOS as Enter is already used to edit a project.
 			rename_btn->set_shortcut(ED_SHORTCUT("project_manager/rename_project", TTR("Rename Project"), Key::F2));
 			rename_btn->connect("pressed", callable_mp(this, &ProjectManager::_rename_project));
-			tree_vb->add_child(rename_btn);
+			project_list_sidebar->add_child(rename_btn);
 
 			manage_tags_btn = memnew(Button);
 			manage_tags_btn->set_text(TTR("Manage Tags"));
-			tree_vb->add_child(manage_tags_btn);
+			project_list_sidebar->add_child(manage_tags_btn);
 
 			erase_btn = memnew(Button);
 			erase_btn->set_text(TTR("Remove"));
 			erase_btn->set_shortcut(ED_SHORTCUT("project_manager/remove_project", TTR("Remove Project"), Key::KEY_DELETE));
 			erase_btn->connect("pressed", callable_mp(this, &ProjectManager::_erase_project));
-			tree_vb->add_child(erase_btn);
+			project_list_sidebar->add_child(erase_btn);
+
+			Control *filler = memnew(Control);
+			filler->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+			project_list_sidebar->add_child(filler);
 
 			erase_missing_btn = memnew(Button);
 			erase_missing_btn->set_text(TTR("Remove Missing"));
 			erase_missing_btn->connect("pressed", callable_mp(this, &ProjectManager::_erase_missing_projects));
-			tree_vb->add_child(erase_missing_btn);
+			project_list_sidebar->add_child(erase_missing_btn);
 		}
 	}
 
@@ -1320,7 +1364,7 @@ ProjectManager::ProjectManager() {
 		scan_dir->set_title(TTR("Select a Folder to Scan")); // must be after mode or it's overridden
 		scan_dir->set_current_dir(EDITOR_GET("filesystem/directories/default_project_path"));
 		add_child(scan_dir);
-		scan_dir->connect("dir_selected", callable_mp(_project_list, &ProjectList::find_projects));
+		scan_dir->connect("dir_selected", callable_mp(project_list, &ProjectList::find_projects));
 
 		erase_missing_ask = memnew(ConfirmationDialog);
 		erase_missing_ask->set_ok_button_text(TTR("Remove All"));
@@ -1367,17 +1411,14 @@ ProjectManager::ProjectManager() {
 		ask_full_convert_dialog->connect("confirmed", callable_mp(this, &ProjectManager::_perform_full_project_conversion));
 		add_child(ask_full_convert_dialog);
 
-		npdialog = memnew(ProjectDialog);
-		npdialog->connect("projects_updated", callable_mp(this, &ProjectManager::_on_projects_updated));
-		npdialog->connect("project_created", callable_mp(this, &ProjectManager::_on_project_created));
-		add_child(npdialog);
-
-		run_error_diag = memnew(AcceptDialog);
-		run_error_diag->set_title(TTR("Can't run project"));
-		add_child(run_error_diag);
+		project_dialog = memnew(ProjectDialog);
+		project_dialog->connect("projects_updated", callable_mp(this, &ProjectManager::_on_projects_updated));
+		project_dialog->connect("project_created", callable_mp(this, &ProjectManager::_on_project_created));
+		add_child(project_dialog);
 
-		dialog_error = memnew(AcceptDialog);
-		add_child(dialog_error);
+		error_dialog = memnew(AcceptDialog);
+		error_dialog->set_title(TTR("Error"));
+		add_child(error_dialog);
 
 		about_dialog = memnew(EditorAbout);
 		add_child(about_dialog);
@@ -1469,7 +1510,7 @@ ProjectManager::ProjectManager() {
 		String autoscan_path = EDITOR_GET("filesystem/directories/autoscan_project_path");
 		if (!autoscan_path.is_empty()) {
 			if (dir_access->dir_exists(autoscan_path)) {
-				_project_list->find_projects(autoscan_path);
+				project_list->find_projects(autoscan_path);
 				scanned_for_projects = true;
 			} else {
 				Error error = dir_access->make_dir_recursive(autoscan_path);
@@ -1480,7 +1521,7 @@ ProjectManager::ProjectManager() {
 		}
 
 		if (!scanned_for_projects) {
-			_project_list->update_project_list();
+			project_list->update_project_list();
 		}
 	}
 

+ 14 - 8
editor/project_manager.h

@@ -101,14 +101,14 @@ class ProjectManager : public Control {
 	VBoxContainer *local_projects_vb = nullptr;
 	EditorAssetLibrary *asset_library = nullptr;
 
-	ConfirmationDialog *suggest_asset_library_dialog = nullptr;
-	RichTextLabel *suggest_asset_library_label = nullptr;
 	EditorAbout *about_dialog = nullptr;
 
 	void _show_about();
-	void _suggest_asset_library();
 	void _open_asset_library_confirmed();
 
+	AcceptDialog *error_dialog = nullptr;
+
+	void _show_error(const String &p_message, const Size2 &p_min_size = Size2());
 	void _dim_window();
 
 	// Quick settings.
@@ -126,12 +126,20 @@ class ProjectManager : public Control {
 
 	// Project list.
 
-	ProjectList *_project_list = nullptr;
+	VBoxContainer *empty_list_placeholder = nullptr;
+	Button *empty_list_create_project = nullptr;
+	Button *empty_list_import_project = nullptr;
+	Button *empty_list_open_assetlib = nullptr;
+	Label *empty_list_online_warning = nullptr;
+
+	void _update_list_placeholder();
+
+	ProjectList *project_list = nullptr;
 
 	LineEdit *search_box = nullptr;
 	Label *loading_label = nullptr;
 	OptionButton *filter_option = nullptr;
-	PanelContainer *search_panel = nullptr;
+	PanelContainer *project_list_panel = nullptr;
 
 	Button *create_btn = nullptr;
 	Button *import_btn = nullptr;
@@ -154,9 +162,7 @@ class ProjectManager : public Control {
 	ConfirmationDialog *multi_open_ask = nullptr;
 	ConfirmationDialog *multi_run_ask = nullptr;
 
-	AcceptDialog *run_error_diag = nullptr;
-	AcceptDialog *dialog_error = nullptr;
-	ProjectDialog *npdialog = nullptr;
+	ProjectDialog *project_dialog = nullptr;
 
 	void _scan_projects();
 	void _run_project();

+ 3 - 0
editor/themes/editor_fonts.cpp

@@ -388,6 +388,9 @@ void editor_register_fonts(const Ref<Theme> &p_theme) {
 	p_theme->set_font("main_bold_msdf", EditorStringName(EditorFonts), bold_fc_msdf);
 	p_theme->set_font_size("bold_size", EditorStringName(EditorFonts), default_font_size);
 
+	p_theme->set_font("italic", EditorStringName(EditorFonts), italic_fc);
+	p_theme->set_font_size("italic_size", EditorStringName(EditorFonts), default_font_size);
+
 	// Title font.
 
 	p_theme->set_font("title", EditorStringName(EditorFonts), bold_fc);

+ 23 - 1
editor/themes/editor_theme_manager.cpp

@@ -1590,7 +1590,7 @@ void EditorThemeManager::_populate_standard_styles(const Ref<EditorTheme> &p_the
 void EditorThemeManager::_populate_editor_styles(const Ref<EditorTheme> &p_theme, ThemeConfiguration &p_config) {
 	// Project manager.
 	{
-		p_theme->set_stylebox("search_panel", "ProjectManager", p_config.tree_panel_style);
+		p_theme->set_stylebox("project_list", "ProjectManager", p_config.tree_panel_style);
 		p_theme->set_constant("sidebar_button_icon_separation", "ProjectManager", int(6 * EDSCALE));
 
 		// ProjectTag.
@@ -1769,6 +1769,28 @@ void EditorThemeManager::_populate_editor_styles(const Ref<EditorTheme> &p_theme
 			p_theme->set_stylebox("pressed", "EditorLogFilterButton", editor_log_button_pressed);
 		}
 
+		// Buttons styles that stand out against the panel background (e.g. AssetLib).
+		{
+			p_theme->set_type_variation("PanelBackgroundButton", "Button");
+
+			Ref<StyleBoxFlat> panel_button_style = p_config.button_style->duplicate();
+			panel_button_style->set_bg_color(p_config.base_color.lerp(p_config.mono_color, 0.08));
+
+			Ref<StyleBoxFlat> panel_button_style_hover = p_config.button_style_hover->duplicate();
+			panel_button_style_hover->set_bg_color(p_config.base_color.lerp(p_config.mono_color, 0.16));
+
+			Ref<StyleBoxFlat> panel_button_style_pressed = p_config.button_style_pressed->duplicate();
+			panel_button_style_pressed->set_bg_color(p_config.base_color.lerp(p_config.mono_color, 0.20));
+
+			Ref<StyleBoxFlat> panel_button_style_disabled = p_config.button_style_disabled->duplicate();
+			panel_button_style_disabled->set_bg_color(p_config.disabled_bg_color);
+
+			p_theme->set_stylebox("normal", "PanelBackgroundButton", panel_button_style);
+			p_theme->set_stylebox("hover", "PanelBackgroundButton", panel_button_style_hover);
+			p_theme->set_stylebox("pressed", "PanelBackgroundButton", panel_button_style_pressed);
+			p_theme->set_stylebox("disabled", "PanelBackgroundButton", panel_button_style_disabled);
+		}
+
 		// Top bar selectors.
 		{
 			p_theme->set_type_variation("TopBarOptionButton", "OptionButton");