From f51f97aaec9f68225a4edcb78e1002d800842478 Mon Sep 17 00:00:00 2001 From: kobewi Date: Thu, 13 Nov 2025 15:24:22 +0100 Subject: [PATCH] Add a right click menu to the project manager --- editor/project_manager/project_list.cpp | 146 +++++++++++++++++---- editor/project_manager/project_list.h | 30 ++++- editor/project_manager/project_manager.cpp | 58 ++++++-- editor/project_manager/project_manager.h | 2 +- 4 files changed, 195 insertions(+), 41 deletions(-) diff --git a/editor/project_manager/project_list.cpp b/editor/project_manager/project_list.cpp index a5990b6a073..3fe58d91d14 100644 --- a/editor/project_manager/project_list.cpp +++ b/editor/project_manager/project_list.cpp @@ -44,15 +44,12 @@ #include "scene/gui/dialogs.h" #include "scene/gui/label.h" #include "scene/gui/line_edit.h" +#include "scene/gui/popup_menu.h" #include "scene/gui/progress_bar.h" #include "scene/gui/texture_button.h" #include "scene/gui/texture_rect.h" #include "scene/resources/image_texture.h" -const char *ProjectList::SIGNAL_LIST_CHANGED = "list_changed"; -const char *ProjectList::SIGNAL_SELECTION_CHANGED = "selection_changed"; -const char *ProjectList::SIGNAL_PROJECT_ASK_OPEN = "project_ask_open"; - void ProjectListItemControl::_notification(int p_what) { switch (p_what) { case NOTIFICATION_THEME_CHANGED: { @@ -80,6 +77,10 @@ void ProjectListItemControl::_notification(int p_what) { explore_button->set_button_icon(get_editor_theme_icon(SNAME("Load"))); #endif } + + if (touch_menu_button) { + touch_menu_button->set_button_icon(get_editor_theme_icon(SNAME("GuiTabMenuHl"))); + } } break; case NOTIFICATION_MOUSE_ENTER: { @@ -196,6 +197,10 @@ void ProjectListItemControl::_explore_button_pressed() { emit_signal(SNAME("explore_pressed")); } +void ProjectListItemControl::_request_menu() { + emit_signal(SNAME("request_menu"), Vector2(touch_menu_button->get_position())); +} + void ProjectListItemControl::set_project_title(const String &p_title) { project_title->set_text(p_title); project_title->set_accessibility_name(TTRC("Project Name")); @@ -318,6 +323,7 @@ void ProjectListItemControl::set_is_grayed(bool p_grayed) { void ProjectListItemControl::_bind_methods() { ADD_SIGNAL(MethodInfo("favorite_pressed")); ADD_SIGNAL(MethodInfo("explore_pressed")); + ADD_SIGNAL(MethodInfo("request_menu")); } ProjectListItemControl::ProjectListItemControl() { @@ -419,6 +425,14 @@ ProjectListItemControl::ProjectListItemControl() { spacer->set_custom_minimum_size(Size2(10, 10)); path_hb->add_child(spacer); } + + if (DisplayServer::get_singleton()->is_touchscreen_available()) { + touch_menu_button = memnew(Button); + touch_menu_button->set_theme_type_variation(SceneStringName(FlatButton)); + touch_menu_button->set_v_size_flags(SIZE_SHRINK_CENTER); + add_child(touch_menu_button); + touch_menu_button->connect(SceneStringName(pressed), callable_mp(this, &ProjectListItemControl::_request_menu)); + } } struct ProjectListComparator { @@ -462,6 +476,12 @@ void ProjectList::_notification(int p_what) { } } break; + case NOTIFICATION_THEME_CHANGED: { + if (project_context_menu) { + _update_menu_icons(); + } + } break; + case NOTIFICATION_PROCESS: { // Load icons as a coroutine to speed up launch when you have hundreds of projects. if (_icon_load_index < _projects.size()) { @@ -1005,6 +1025,7 @@ void ProjectList::_create_project_item_control(int p_index) { #if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED) hb->connect("explore_pressed", callable_mp(this, &ProjectList::_on_explore_pressed).bind(item.path)); #endif + hb->connect("request_menu", callable_mp(this, &ProjectList::_open_menu).bind(hb)); project_list_vbox->add_child(hb); item.control = hb; @@ -1043,38 +1064,42 @@ void ProjectList::_remove_project(int p_index, bool p_update_config) { update_dock_menu(); } -void ProjectList::_list_item_input(const Ref &p_ev, Node *p_hb) { +void ProjectList::_list_item_input(const Ref &p_ev, Control *p_hb) { Ref mb = p_ev; int clicked_index = p_hb->get_index(); const Item &clicked_project = _projects[clicked_index]; - if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { - 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.path == _last_clicked) { - anchor_index = p.control->get_index(); - break; + if (mb.is_valid() && mb->is_pressed()) { + if (mb->get_button_index() == MouseButton::LEFT) { + 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.path == _last_clicked) { + anchor_index = p.control->get_index(); + break; + } } + CRASH_COND(anchor_index == -1); + _select_project_range(anchor_index, clicked_index); + + } else if (mb->is_command_or_control_pressed()) { + _toggle_project(clicked_index); + + } else { + _last_clicked = clicked_project.path; + select_project(clicked_index); } - CRASH_COND(anchor_index == -1); - _select_project_range(anchor_index, clicked_index); - } else if (mb->is_command_or_control_pressed()) { - _toggle_project(clicked_index); + emit_signal(SNAME(SIGNAL_SELECTION_CHANGED)); - } else { - _last_clicked = clicked_project.path; - select_project(clicked_index); - } - - emit_signal(SNAME(SIGNAL_SELECTION_CHANGED)); - - // Do not allow opening a project more than once using a single project manager instance. - // Opening the same project in several editor instances at once can lead to various issues. - if (!mb->is_command_or_control_pressed() && mb->is_double_click() && !project_opening_initiated) { - emit_signal(SNAME(SIGNAL_PROJECT_ASK_OPEN)); + // Do not allow opening a project more than once using a single project manager instance. + // Opening the same project in several editor instances at once can lead to various issues. + if (!mb->is_command_or_control_pressed() && mb->is_double_click() && !project_opening_initiated) { + emit_signal(SNAME(SIGNAL_PROJECT_ASK_OPEN)); + } + } else if (mb->get_button_index() == MouseButton::RIGHT) { + _open_menu(mb->get_position(), p_hb); } } } @@ -1112,6 +1137,70 @@ void ProjectList::_on_explore_pressed(const String &p_path) { OS::get_singleton()->shell_show_in_file_manager(p_path, true); } +void ProjectList::_open_menu(const Vector2 &p_at, Control *p_hb) { + int clicked_index = p_hb->get_index(); + const Item &clicked_project = _projects[clicked_index]; + + if (!project_context_menu) { + project_context_menu = memnew(PopupMenu); + project_context_menu->add_item(TTRC("Open in Editor"), MENU_EDIT); + project_context_menu->add_item(TTRC("Open in Editor (Verbose Mode)"), MENU_EDIT_VERBOSE); + project_context_menu->add_item(TTRC("Open in Editor (Recovery Mode)"), MENU_EDIT_RECOVERY); + project_context_menu->add_item(TTRC("Run Project"), MENU_RUN); + project_context_menu->add_separator(); +#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED) + project_context_menu->add_item(TTRC("Show in File Manager"), MENU_SHOW_IN_FILE_MANAGER); +#endif + project_context_menu->add_item(TTRC("Copy Path"), MENU_COPY_PATH); + project_context_menu->add_separator(); + project_context_menu->add_item(TTRC("Rename"), MENU_RENAME); + project_context_menu->add_item(TTRC("Manage Tags"), MENU_MANAGE_TAGS); + project_context_menu->add_item(TTRC("Duplicate"), MENU_DUPLICATE); + project_context_menu->add_item(TTRC("Remove from Project List"), MENU_REMOVE); + add_child(project_context_menu); + project_context_menu->connect(SceneStringName(id_pressed), callable_mp(this, &ProjectList::_menu_option)); + _update_menu_icons(); + } + select_project(clicked_index); + + for (int id : Vector{ + MENU_EDIT, + MENU_EDIT_VERBOSE, + MENU_EDIT_RECOVERY, + MENU_RUN, +#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED) + MENU_SHOW_IN_FILE_MANAGER, +#endif + MENU_RENAME, + MENU_MANAGE_TAGS, + MENU_DUPLICATE }) { + project_context_menu->set_item_disabled(project_context_menu->get_item_index(id), clicked_project.missing); + } + + project_context_menu->set_position(p_hb->get_screen_position() + p_at); + project_context_menu->reset_size(); + project_context_menu->popup(); +} + +void ProjectList::_menu_option(int p_option) { + emit_signal(SIGNAL_MENU_OPTION_SELECTED, p_option); +} + +void ProjectList::_update_menu_icons() { + project_context_menu->set_item_icon(project_context_menu->get_item_index(MENU_EDIT), get_editor_theme_icon("Edit")); + project_context_menu->set_item_icon(project_context_menu->get_item_index(MENU_EDIT_VERBOSE), get_editor_theme_icon("Notification")); + project_context_menu->set_item_icon(project_context_menu->get_item_index(MENU_EDIT_RECOVERY), get_editor_theme_icon("NodeWarning")); + project_context_menu->set_item_icon(project_context_menu->get_item_index(MENU_RUN), get_editor_theme_icon("Play")); +#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED) + project_context_menu->set_item_icon(project_context_menu->get_item_index(MENU_SHOW_IN_FILE_MANAGER), get_editor_theme_icon("Load")); +#endif + project_context_menu->set_item_icon(project_context_menu->get_item_index(MENU_COPY_PATH), get_editor_theme_icon("ActionCopy")); + project_context_menu->set_item_icon(project_context_menu->get_item_index(MENU_RENAME), get_editor_theme_icon("Rename")); + project_context_menu->set_item_icon(project_context_menu->get_item_index(MENU_MANAGE_TAGS), get_editor_theme_icon("Script")); + project_context_menu->set_item_icon(project_context_menu->get_item_index(MENU_DUPLICATE), get_editor_theme_icon("Duplicate")); + project_context_menu->set_item_icon(project_context_menu->get_item_index(MENU_REMOVE), get_editor_theme_icon("Remove")); +} + // Project list selection. void ProjectList::_clear_project_selection() { @@ -1369,6 +1458,7 @@ void ProjectList::_bind_methods() { ADD_SIGNAL(MethodInfo(SIGNAL_LIST_CHANGED)); ADD_SIGNAL(MethodInfo(SIGNAL_SELECTION_CHANGED)); ADD_SIGNAL(MethodInfo(SIGNAL_PROJECT_ASK_OPEN)); + ADD_SIGNAL(MethodInfo(SIGNAL_MENU_OPTION_SELECTED)); } ProjectList::ProjectList() { diff --git a/editor/project_manager/project_list.h b/editor/project_manager/project_list.h index 3549af5c7ee..a4f18e10a8a 100644 --- a/editor/project_manager/project_list.h +++ b/editor/project_manager/project_list.h @@ -38,6 +38,7 @@ class AcceptDialog; class Button; class Label; +class PopupMenu; class ProjectList; class TextureButton; class TextureRect; @@ -56,6 +57,7 @@ class ProjectListItemControl : public HBoxContainer { Label *project_version = nullptr; TextureRect *project_unsupported_features = nullptr; HBoxContainer *tag_container = nullptr; + Button *touch_menu_button = nullptr; bool project_is_missing = false; bool icon_needs_reload = true; @@ -64,6 +66,7 @@ class ProjectListItemControl : public HBoxContainer { void _favorite_button_pressed(); void _explore_button_pressed(); + void _request_menu(); ProjectList *get_list() const; @@ -109,6 +112,19 @@ public: TAGS, }; + enum MenuOption { + MENU_EDIT, + MENU_EDIT_VERBOSE, + MENU_EDIT_RECOVERY, + MENU_RUN, + MENU_SHOW_IN_FILE_MANAGER, + MENU_COPY_PATH, + MENU_RENAME, + MENU_MANAGE_TAGS, + MENU_DUPLICATE, + MENU_REMOVE, + }; + // Can often be passed by copy. struct Item { String project_name; @@ -196,6 +212,7 @@ private: String _last_clicked; // Project key VBoxContainer *project_list_vbox = nullptr; + PopupMenu *project_context_menu = nullptr; // Projects scan. @@ -229,10 +246,14 @@ private: void _toggle_project(int p_index); void _remove_project(int p_index, bool p_update_settings); - void _list_item_input(const Ref &p_ev, Node *p_hb); + void _list_item_input(const Ref &p_ev, Control *p_hb); void _on_favorite_pressed(Node *p_hb); void _on_explore_pressed(const String &p_path); + void _open_menu(const Vector2 &p_at, Control *p_hb); + void _menu_option(int p_option); + void _update_menu_icons(); + // Project list selection. void _clear_project_selection(); @@ -250,9 +271,10 @@ protected: static void _bind_methods(); public: - static const char *SIGNAL_LIST_CHANGED; - static const char *SIGNAL_SELECTION_CHANGED; - static const char *SIGNAL_PROJECT_ASK_OPEN; + static inline const char *SIGNAL_LIST_CHANGED = "list_changed"; + static inline const char *SIGNAL_SELECTION_CHANGED = "selection_changed"; + static inline const char *SIGNAL_PROJECT_ASK_OPEN = "project_ask_open"; + static inline const char *SIGNAL_MENU_OPTION_SELECTED = "menu_option_selected"; static bool project_feature_looks_like_version(const String &p_feature); diff --git a/editor/project_manager/project_manager.cpp b/editor/project_manager/project_manager.cpp index 42025f655ee..496a601c796 100644 --- a/editor/project_manager/project_manager.cpp +++ b/editor/project_manager/project_manager.cpp @@ -265,7 +265,6 @@ void ProjectManager::_update_theme(bool p_skip_creation) { rename_btn->set_button_icon(get_editor_theme_icon("Rename")); duplicate_btn->set_button_icon(get_editor_theme_icon("Duplicate")); manage_tags_btn->set_button_icon(get_editor_theme_icon("Script")); - show_in_fm_btn->set_button_icon(get_editor_theme_icon("Load")); erase_btn->set_button_icon(get_editor_theme_icon("Remove")); erase_missing_btn->set_button_icon(get_editor_theme_icon("Clear")); create_tag_btn->set_button_icon(get_editor_theme_icon("Add")); @@ -283,7 +282,6 @@ void ProjectManager::_update_theme(bool p_skip_creation) { rename_btn->add_theme_constant_override("h_separation", h_separation); duplicate_btn->add_theme_constant_override("h_separation", h_separation); manage_tags_btn->add_theme_constant_override("h_separation", h_separation); - show_in_fm_btn->add_theme_constant_override("h_separation", h_separation); erase_btn->add_theme_constant_override("h_separation", h_separation); erase_missing_btn->add_theme_constant_override("h_separation", h_separation); @@ -395,6 +393,55 @@ void ProjectManager::_open_asset_library_confirmed() { _select_main_view(MAIN_VIEW_ASSETLIB); } +void ProjectManager::_project_list_menu_option(int p_option) { + switch (p_option) { + case ProjectList::MENU_EDIT: + _open_selected_projects(); + break; + + case ProjectList::MENU_EDIT_VERBOSE: + open_in_verbose_mode = true; + _open_selected_projects_check_warnings(); + break; + + case ProjectList::MENU_EDIT_RECOVERY: + _open_recovery_mode_ask(true); + break; + + case ProjectList::MENU_RUN: + _run_project_confirm(); + break; + + case ProjectList::MENU_SHOW_IN_FILE_MANAGER: + _show_project_in_file_manager(); + break; + + case ProjectList::MENU_COPY_PATH: { + const Vector &selected_list = project_list->get_selected_projects(); + if (selected_list.is_empty()) { + return; + } + DisplayServer::get_singleton()->clipboard_set(selected_list[0].path); + } break; + + case ProjectList::MENU_RENAME: + _rename_project(); + break; + + case ProjectList::MENU_MANAGE_TAGS: + _manage_project_tags(); + break; + + case ProjectList::MENU_DUPLICATE: + _duplicate_project(); + break; + + case ProjectList::MENU_REMOVE: + _erase_project(); + break; + } +} + 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); @@ -807,7 +854,6 @@ void ProjectManager::_update_project_buttons() { rename_btn->set_disabled(empty_selection || is_missing_project_selected); duplicate_btn->set_disabled(empty_selection || is_missing_project_selected); manage_tags_btn->set_disabled(empty_selection || is_missing_project_selected || selected_projects.size() > 1); - show_in_fm_btn->set_disabled(empty_selection || is_missing_project_selected); run_btn->set_disabled(empty_selection || is_missing_project_selected); erase_missing_btn->set_disabled(!project_list->is_any_project_missing()); @@ -1545,6 +1591,7 @@ ProjectManager::ProjectManager() { 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_check_recovery_mode)); + project_list->connect(ProjectList::SIGNAL_MENU_OPTION_SELECTED, callable_mp(this, &ProjectManager::_project_list_menu_option)); // Empty project list placeholder. { @@ -1652,11 +1699,6 @@ ProjectManager::ProjectManager() { manage_tags_btn->set_shortcut(ED_SHORTCUT("project_manager/project_tags", TTRC("Manage Tags"), KeyModifierMask::CMD_OR_CTRL | Key::T)); project_list_sidebar->add_child(manage_tags_btn); - show_in_fm_btn = memnew(Button); - show_in_fm_btn->set_text(TTRC("Show in File Manager")); - show_in_fm_btn->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_show_project_in_file_manager)); - project_list_sidebar->add_child(show_in_fm_btn); - erase_btn = memnew(Button); erase_btn->set_text(TTRC("Remove")); erase_btn->set_shortcut(ED_SHORTCUT("project_manager/remove_project", TTRC("Remove Project"), Key::KEY_DELETE)); diff --git a/editor/project_manager/project_manager.h b/editor/project_manager/project_manager.h index 0feda07dae2..20c213bd6bb 100644 --- a/editor/project_manager/project_manager.h +++ b/editor/project_manager/project_manager.h @@ -118,6 +118,7 @@ class ProjectManager : public Control { void _show_about(); void _open_asset_library_confirmed(); + void _project_list_menu_option(int p_option); AcceptDialog *error_dialog = nullptr; @@ -160,7 +161,6 @@ class ProjectManager : public Control { Button *rename_btn = nullptr; Button *duplicate_btn = nullptr; Button *manage_tags_btn = nullptr; - Button *show_in_fm_btn = nullptr; Button *erase_btn = nullptr; Button *erase_missing_btn = nullptr; Button *donate_btn = nullptr;