1
0
mirror of https://github.com/godotengine/godot.git synced 2025-12-05 17:15:09 +00:00

Add a right click menu to the project manager

This commit is contained in:
kobewi
2025-11-13 15:24:22 +01:00
parent 0b5ad6c73c
commit f51f97aaec
4 changed files with 195 additions and 41 deletions

View File

@@ -44,15 +44,12 @@
#include "scene/gui/dialogs.h" #include "scene/gui/dialogs.h"
#include "scene/gui/label.h" #include "scene/gui/label.h"
#include "scene/gui/line_edit.h" #include "scene/gui/line_edit.h"
#include "scene/gui/popup_menu.h"
#include "scene/gui/progress_bar.h" #include "scene/gui/progress_bar.h"
#include "scene/gui/texture_button.h" #include "scene/gui/texture_button.h"
#include "scene/gui/texture_rect.h" #include "scene/gui/texture_rect.h"
#include "scene/resources/image_texture.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) { void ProjectListItemControl::_notification(int p_what) {
switch (p_what) { switch (p_what) {
case NOTIFICATION_THEME_CHANGED: { 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"))); explore_button->set_button_icon(get_editor_theme_icon(SNAME("Load")));
#endif #endif
} }
if (touch_menu_button) {
touch_menu_button->set_button_icon(get_editor_theme_icon(SNAME("GuiTabMenuHl")));
}
} break; } break;
case NOTIFICATION_MOUSE_ENTER: { case NOTIFICATION_MOUSE_ENTER: {
@@ -196,6 +197,10 @@ void ProjectListItemControl::_explore_button_pressed() {
emit_signal(SNAME("explore_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) { void ProjectListItemControl::set_project_title(const String &p_title) {
project_title->set_text(p_title); project_title->set_text(p_title);
project_title->set_accessibility_name(TTRC("Project Name")); project_title->set_accessibility_name(TTRC("Project Name"));
@@ -318,6 +323,7 @@ void ProjectListItemControl::set_is_grayed(bool p_grayed) {
void ProjectListItemControl::_bind_methods() { void ProjectListItemControl::_bind_methods() {
ADD_SIGNAL(MethodInfo("favorite_pressed")); ADD_SIGNAL(MethodInfo("favorite_pressed"));
ADD_SIGNAL(MethodInfo("explore_pressed")); ADD_SIGNAL(MethodInfo("explore_pressed"));
ADD_SIGNAL(MethodInfo("request_menu"));
} }
ProjectListItemControl::ProjectListItemControl() { ProjectListItemControl::ProjectListItemControl() {
@@ -419,6 +425,14 @@ ProjectListItemControl::ProjectListItemControl() {
spacer->set_custom_minimum_size(Size2(10, 10)); spacer->set_custom_minimum_size(Size2(10, 10));
path_hb->add_child(spacer); 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 { struct ProjectListComparator {
@@ -462,6 +476,12 @@ void ProjectList::_notification(int p_what) {
} }
} break; } break;
case NOTIFICATION_THEME_CHANGED: {
if (project_context_menu) {
_update_menu_icons();
}
} break;
case NOTIFICATION_PROCESS: { case NOTIFICATION_PROCESS: {
// Load icons as a coroutine to speed up launch when you have hundreds of projects. // Load icons as a coroutine to speed up launch when you have hundreds of projects.
if (_icon_load_index < _projects.size()) { 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) #if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
hb->connect("explore_pressed", callable_mp(this, &ProjectList::_on_explore_pressed).bind(item.path)); hb->connect("explore_pressed", callable_mp(this, &ProjectList::_on_explore_pressed).bind(item.path));
#endif #endif
hb->connect("request_menu", callable_mp(this, &ProjectList::_open_menu).bind(hb));
project_list_vbox->add_child(hb); project_list_vbox->add_child(hb);
item.control = hb; item.control = hb;
@@ -1043,38 +1064,42 @@ void ProjectList::_remove_project(int p_index, bool p_update_config) {
update_dock_menu(); update_dock_menu();
} }
void ProjectList::_list_item_input(const Ref<InputEvent> &p_ev, Node *p_hb) { void ProjectList::_list_item_input(const Ref<InputEvent> &p_ev, Control *p_hb) {
Ref<InputEventMouseButton> mb = p_ev; Ref<InputEventMouseButton> mb = p_ev;
int clicked_index = p_hb->get_index(); int clicked_index = p_hb->get_index();
const Item &clicked_project = _projects[clicked_index]; const Item &clicked_project = _projects[clicked_index];
if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { if (mb.is_valid() && mb->is_pressed()) {
if (mb->is_shift_pressed() && _selected_project_paths.size() > 0 && !_last_clicked.is_empty() && clicked_project.path != _last_clicked) { if (mb->get_button_index() == MouseButton::LEFT) {
int anchor_index = -1; if (mb->is_shift_pressed() && _selected_project_paths.size() > 0 && !_last_clicked.is_empty() && clicked_project.path != _last_clicked) {
for (int i = 0; i < _projects.size(); ++i) { int anchor_index = -1;
const Item &p = _projects[i]; for (int i = 0; i < _projects.size(); ++i) {
if (p.path == _last_clicked) { const Item &p = _projects[i];
anchor_index = p.control->get_index(); if (p.path == _last_clicked) {
break; 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()) { emit_signal(SNAME(SIGNAL_SELECTION_CHANGED));
_toggle_project(clicked_index);
} else { // Do not allow opening a project more than once using a single project manager instance.
_last_clicked = clicked_project.path; // Opening the same project in several editor instances at once can lead to various issues.
select_project(clicked_index); if (!mb->is_command_or_control_pressed() && mb->is_double_click() && !project_opening_initiated) {
} emit_signal(SNAME(SIGNAL_PROJECT_ASK_OPEN));
}
emit_signal(SNAME(SIGNAL_SELECTION_CHANGED)); } else if (mb->get_button_index() == MouseButton::RIGHT) {
_open_menu(mb->get_position(), p_hb);
// 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));
} }
} }
} }
@@ -1112,6 +1137,70 @@ void ProjectList::_on_explore_pressed(const String &p_path) {
OS::get_singleton()->shell_show_in_file_manager(p_path, true); 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<int>{
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. // Project list selection.
void ProjectList::_clear_project_selection() { void ProjectList::_clear_project_selection() {
@@ -1369,6 +1458,7 @@ void ProjectList::_bind_methods() {
ADD_SIGNAL(MethodInfo(SIGNAL_LIST_CHANGED)); ADD_SIGNAL(MethodInfo(SIGNAL_LIST_CHANGED));
ADD_SIGNAL(MethodInfo(SIGNAL_SELECTION_CHANGED)); ADD_SIGNAL(MethodInfo(SIGNAL_SELECTION_CHANGED));
ADD_SIGNAL(MethodInfo(SIGNAL_PROJECT_ASK_OPEN)); ADD_SIGNAL(MethodInfo(SIGNAL_PROJECT_ASK_OPEN));
ADD_SIGNAL(MethodInfo(SIGNAL_MENU_OPTION_SELECTED));
} }
ProjectList::ProjectList() { ProjectList::ProjectList() {

View File

@@ -38,6 +38,7 @@
class AcceptDialog; class AcceptDialog;
class Button; class Button;
class Label; class Label;
class PopupMenu;
class ProjectList; class ProjectList;
class TextureButton; class TextureButton;
class TextureRect; class TextureRect;
@@ -56,6 +57,7 @@ class ProjectListItemControl : public HBoxContainer {
Label *project_version = nullptr; Label *project_version = nullptr;
TextureRect *project_unsupported_features = nullptr; TextureRect *project_unsupported_features = nullptr;
HBoxContainer *tag_container = nullptr; HBoxContainer *tag_container = nullptr;
Button *touch_menu_button = nullptr;
bool project_is_missing = false; bool project_is_missing = false;
bool icon_needs_reload = true; bool icon_needs_reload = true;
@@ -64,6 +66,7 @@ class ProjectListItemControl : public HBoxContainer {
void _favorite_button_pressed(); void _favorite_button_pressed();
void _explore_button_pressed(); void _explore_button_pressed();
void _request_menu();
ProjectList *get_list() const; ProjectList *get_list() const;
@@ -109,6 +112,19 @@ public:
TAGS, 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. // Can often be passed by copy.
struct Item { struct Item {
String project_name; String project_name;
@@ -196,6 +212,7 @@ private:
String _last_clicked; // Project key String _last_clicked; // Project key
VBoxContainer *project_list_vbox = nullptr; VBoxContainer *project_list_vbox = nullptr;
PopupMenu *project_context_menu = nullptr;
// Projects scan. // Projects scan.
@@ -229,10 +246,14 @@ private:
void _toggle_project(int p_index); void _toggle_project(int p_index);
void _remove_project(int p_index, bool p_update_settings); void _remove_project(int p_index, bool p_update_settings);
void _list_item_input(const Ref<InputEvent> &p_ev, Node *p_hb); void _list_item_input(const Ref<InputEvent> &p_ev, Control *p_hb);
void _on_favorite_pressed(Node *p_hb); void _on_favorite_pressed(Node *p_hb);
void _on_explore_pressed(const String &p_path); 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. // Project list selection.
void _clear_project_selection(); void _clear_project_selection();
@@ -250,9 +271,10 @@ protected:
static void _bind_methods(); static void _bind_methods();
public: public:
static const char *SIGNAL_LIST_CHANGED; static inline const char *SIGNAL_LIST_CHANGED = "list_changed";
static const char *SIGNAL_SELECTION_CHANGED; static inline const char *SIGNAL_SELECTION_CHANGED = "selection_changed";
static const char *SIGNAL_PROJECT_ASK_OPEN; 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); static bool project_feature_looks_like_version(const String &p_feature);

View File

@@ -265,7 +265,6 @@ void ProjectManager::_update_theme(bool p_skip_creation) {
rename_btn->set_button_icon(get_editor_theme_icon("Rename")); rename_btn->set_button_icon(get_editor_theme_icon("Rename"));
duplicate_btn->set_button_icon(get_editor_theme_icon("Duplicate")); duplicate_btn->set_button_icon(get_editor_theme_icon("Duplicate"));
manage_tags_btn->set_button_icon(get_editor_theme_icon("Script")); 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_btn->set_button_icon(get_editor_theme_icon("Remove"));
erase_missing_btn->set_button_icon(get_editor_theme_icon("Clear")); erase_missing_btn->set_button_icon(get_editor_theme_icon("Clear"));
create_tag_btn->set_button_icon(get_editor_theme_icon("Add")); 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); rename_btn->add_theme_constant_override("h_separation", h_separation);
duplicate_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); 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_btn->add_theme_constant_override("h_separation", h_separation);
erase_missing_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); _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<ProjectList::Item> &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) { void ProjectManager::_show_error(const String &p_message, const Size2 &p_min_size) {
error_dialog->set_text(p_message); error_dialog->set_text(p_message);
error_dialog->popup_centered(p_min_size); 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); rename_btn->set_disabled(empty_selection || is_missing_project_selected);
duplicate_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); 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); 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());
@@ -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_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_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_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. // 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)); 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); 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 = memnew(Button);
erase_btn->set_text(TTRC("Remove")); erase_btn->set_text(TTRC("Remove"));
erase_btn->set_shortcut(ED_SHORTCUT("project_manager/remove_project", TTRC("Remove Project"), Key::KEY_DELETE)); erase_btn->set_shortcut(ED_SHORTCUT("project_manager/remove_project", TTRC("Remove Project"), Key::KEY_DELETE));

View File

@@ -118,6 +118,7 @@ class ProjectManager : public Control {
void _show_about(); void _show_about();
void _open_asset_library_confirmed(); void _open_asset_library_confirmed();
void _project_list_menu_option(int p_option);
AcceptDialog *error_dialog = nullptr; AcceptDialog *error_dialog = nullptr;
@@ -160,7 +161,6 @@ class ProjectManager : public Control {
Button *rename_btn = nullptr; Button *rename_btn = nullptr;
Button *duplicate_btn = nullptr; Button *duplicate_btn = nullptr;
Button *manage_tags_btn = nullptr; Button *manage_tags_btn = nullptr;
Button *show_in_fm_btn = nullptr;
Button *erase_btn = nullptr; Button *erase_btn = nullptr;
Button *erase_missing_btn = nullptr; Button *erase_missing_btn = nullptr;
Button *donate_btn = nullptr; Button *donate_btn = nullptr;