diff --git a/AutomatedTesting/preview.png b/AutomatedTesting/preview.png index 3d4fe78063..c6928d31fc 100644 --- a/AutomatedTesting/preview.png +++ b/AutomatedTesting/preview.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40949893ed7009eeaa90b7ce6057cb6be9dfaf7b162e3c26ba9dadf985939d7d -size 2038 +oid sha256:b9cd9d6f67440c193a85969ec5c082c6343e6d1fff3b6f209a0a6931eb22dd47 +size 2949 diff --git a/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp b/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp index 69f0a3983d..60e351cdb4 100644 --- a/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp +++ b/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp @@ -67,6 +67,15 @@ namespace O3DE::ProjectManager return ProjectManagerScreen::CreateProject; } + void CreateProjectCtrl::NotifyCurrentScreen() + { + ScreenWidget* currentScreen = reinterpret_cast(m_stack->currentWidget()); + if (currentScreen) + { + currentScreen->NotifyCurrentScreen(); + } + } + void CreateProjectCtrl::HandleBackButton() { if (m_stack->currentIndex() > 0) @@ -110,6 +119,9 @@ namespace O3DE::ProjectManager auto result = PythonBindingsInterface::Get()->CreateProject(m_projectTemplatePath, m_projectInfo); if (result.IsSuccess()) { + // automatically register the project + PythonBindingsInterface::Get()->AddProject(m_projectInfo.m_path); + // adding gems is not implemented yet because we don't know what targets to add or how to add them emit ChangeScreenRequest(ProjectManagerScreen::Projects); } diff --git a/Code/Tools/ProjectManager/Source/CreateProjectCtrl.h b/Code/Tools/ProjectManager/Source/CreateProjectCtrl.h index 01e3349b21..355ba3941d 100644 --- a/Code/Tools/ProjectManager/Source/CreateProjectCtrl.h +++ b/Code/Tools/ProjectManager/Source/CreateProjectCtrl.h @@ -31,6 +31,7 @@ namespace O3DE::ProjectManager explicit CreateProjectCtrl(QWidget* parent = nullptr); ~CreateProjectCtrl() = default; ProjectManagerScreen GetScreenEnum() override; + void NotifyCurrentScreen() override; protected slots: void HandleBackButton(); diff --git a/Code/Tools/ProjectManager/Source/FormLineEditWidget.cpp b/Code/Tools/ProjectManager/Source/FormLineEditWidget.cpp index 7ef7e3c7d8..6c08393910 100644 --- a/Code/Tools/ProjectManager/Source/FormLineEditWidget.cpp +++ b/Code/Tools/ProjectManager/Source/FormLineEditWidget.cpp @@ -78,6 +78,14 @@ namespace O3DE::ProjectManager m_errorLabel->setText(labelText); } + void FormLineEditWidget::setErrorLabelVisible(bool visible) + { + m_errorLabel->setVisible(visible); + m_frame->setProperty("Valid", !visible); + + refreshStyle(); + } + QLineEdit* FormLineEditWidget::lineEdit() const { return m_lineEdit; diff --git a/Code/Tools/ProjectManager/Source/FormLineEditWidget.h b/Code/Tools/ProjectManager/Source/FormLineEditWidget.h index 3094442cbd..76534f46f7 100644 --- a/Code/Tools/ProjectManager/Source/FormLineEditWidget.h +++ b/Code/Tools/ProjectManager/Source/FormLineEditWidget.h @@ -39,6 +39,7 @@ namespace O3DE::ProjectManager //! Set the error message for to display when invalid. void setErrorLabelText(const QString& labelText); + void setErrorLabelVisible(bool visible); //! Returns a pointer to the underlying LineEdit. QLineEdit* lineEdit() const; diff --git a/Code/Tools/ProjectManager/Source/NewProjectSettingsScreen.cpp b/Code/Tools/ProjectManager/Source/NewProjectSettingsScreen.cpp index b57a2b35b2..53400b3193 100644 --- a/Code/Tools/ProjectManager/Source/NewProjectSettingsScreen.cpp +++ b/Code/Tools/ProjectManager/Source/NewProjectSettingsScreen.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -49,16 +50,16 @@ namespace O3DE::ProjectManager vLayout->setContentsMargins(0,0,0,0); vLayout->setAlignment(Qt::AlignTop); { - m_projectName = new FormLineEditWidget(tr("Project name"), tr("New Project"), this); - m_projectName->setErrorLabelText( - tr("A project with this name already exists at this location. Please choose a new name or location.")); + const QString defaultName{ "NewProject" }; + const QString defaultPath = QDir::toNativeSeparators(GetDefaultProjectPath() + "/" + defaultName); + + m_projectName = new FormLineEditWidget(tr("Project name"), defaultName, this); + connect(m_projectName->lineEdit(), &QLineEdit::textChanged, this, &NewProjectSettingsScreen::ValidateProjectPath); vLayout->addWidget(m_projectName); - m_projectPath = - new FormBrowseEditWidget(tr("Project Location"), QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation), this); + m_projectPath = new FormBrowseEditWidget(tr("Project Location"), defaultPath, this); m_projectPath->lineEdit()->setReadOnly(true); - m_projectPath->setErrorLabelText(tr("Please provide a valid path to a folder that exists")); - m_projectPath->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this)); + connect(m_projectPath->lineEdit(), &QLineEdit::textChanged, this, &NewProjectSettingsScreen::ValidateProjectPath); vLayout->addWidget(m_projectPath); // if we don't use a QFrame we cannot "contain" the widgets inside and move them around @@ -112,17 +113,41 @@ namespace O3DE::ProjectManager this->setLayout(hLayout); } + QString NewProjectSettingsScreen::GetDefaultProjectPath() + { + QString defaultPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); + AZ::Outcome engineInfoResult = PythonBindingsInterface::Get()->GetEngineInfo(); + if (engineInfoResult.IsSuccess()) + { + QDir path(QDir::toNativeSeparators(engineInfoResult.GetValue().m_defaultProjectsFolder)); + if (path.exists()) + { + defaultPath = path.absolutePath(); + } + } + return defaultPath; + } + ProjectManagerScreen NewProjectSettingsScreen::GetScreenEnum() { return ProjectManagerScreen::NewProjectSettings; } + void NewProjectSettingsScreen::ValidateProjectPath() + { + Validate(); + } + + void NewProjectSettingsScreen::NotifyCurrentScreen() + { + Validate(); + } ProjectInfo NewProjectSettingsScreen::GetProjectInfo() { ProjectInfo projectInfo; projectInfo.m_projectName = m_projectName->lineEdit()->text(); - projectInfo.m_path = QDir::toNativeSeparators(m_projectPath->lineEdit()->text() + "/" + projectInfo.m_projectName); + projectInfo.m_path = m_projectPath->lineEdit()->text(); return projectInfo; } @@ -133,24 +158,44 @@ namespace O3DE::ProjectManager bool NewProjectSettingsScreen::Validate() { - bool projectNameIsValid = true; - if (m_projectName->lineEdit()->text().isEmpty()) - { - projectNameIsValid = false; - } - bool projectPathIsValid = true; if (m_projectPath->lineEdit()->text().isEmpty()) { projectPathIsValid = false; + m_projectPath->setErrorLabelText(tr("Please provide a valid location.")); + } + else + { + QDir path(m_projectPath->lineEdit()->text()); + if (path.exists() && !path.isEmpty()) + { + projectPathIsValid = false; + m_projectPath->setErrorLabelText(tr("This folder exists and isn't empty. Please choose a different location.")); + } } - QDir path(QDir::toNativeSeparators(m_projectPath->lineEdit()->text() + "/" + m_projectName->lineEdit()->text())); - if (path.exists() && !path.isEmpty()) + bool projectNameIsValid = true; + if (m_projectName->lineEdit()->text().isEmpty()) { - projectPathIsValid = false; + projectNameIsValid = false; + m_projectName->setErrorLabelText(tr("Please provide a project name.")); + } + else + { + // this validation should roughly match the utils.validate_identifier which the cli + // uses to validate project names + QRegExp validProjectNameRegex("[A-Za-z][A-Za-z0-9_-]{0,63}"); + const bool result = validProjectNameRegex.exactMatch(m_projectName->lineEdit()->text()); + if (!result) + { + projectNameIsValid = false; + m_projectName->setErrorLabelText(tr("Project names must start with a letter and consist of up to 64 letter, number, '_' or '-' characters")); + } + } + m_projectName->setErrorLabelVisible(!projectNameIsValid); + m_projectPath->setErrorLabelVisible(!projectPathIsValid); return projectNameIsValid && projectPathIsValid; } } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/NewProjectSettingsScreen.h b/Code/Tools/ProjectManager/Source/NewProjectSettingsScreen.h index f0e9609fdc..0560f8728d 100644 --- a/Code/Tools/ProjectManager/Source/NewProjectSettingsScreen.h +++ b/Code/Tools/ProjectManager/Source/NewProjectSettingsScreen.h @@ -36,10 +36,15 @@ namespace O3DE::ProjectManager bool Validate(); + void NotifyCurrentScreen() override; + protected slots: void HandleBrowseButton(); + void ValidateProjectPath(); private: + QString GetDefaultProjectPath(); + FormLineEditWidget* m_projectName; FormBrowseEditWidget* m_projectPath; QButtonGroup* m_projectTemplateButtonGroup; diff --git a/Code/Tools/ProjectManager/Source/ProjectsScreen.cpp b/Code/Tools/ProjectManager/Source/ProjectsScreen.cpp index dd2e411ec5..7b9e3ecb9d 100644 --- a/Code/Tools/ProjectManager/Source/ProjectsScreen.cpp +++ b/Code/Tools/ProjectManager/Source/ProjectsScreen.cpp @@ -341,6 +341,16 @@ namespace O3DE::ProjectManager } else { + // refresh the projects content by re-creating it for now + if (m_projectsContent) + { + m_stack->removeWidget(m_projectsContent); + m_projectsContent->deleteLater(); + } + + m_projectsContent = CreateProjectsContent(); + + m_stack->addWidget(m_projectsContent); m_stack->setCurrentWidget(m_projectsContent); } } diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.cpp b/Code/Tools/ProjectManager/Source/PythonBindings.cpp index 8db8492cae..c0481d6c87 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.cpp +++ b/Code/Tools/ProjectManager/Source/PythonBindings.cpp @@ -513,10 +513,15 @@ namespace O3DE::ProjectManager { ProjectInfo createdProjectInfo; bool result = ExecuteWithLock([&] { - pybind11::str projectPath = projectInfo.m_path.toStdString(); + pybind11::str projectName = projectInfo.m_projectName.toStdString(); pybind11::str templatePath = projectTemplatePath.toStdString(); - auto createProjectResult = m_engineTemplate.attr("create_project")(projectPath, templatePath); + + auto createProjectResult = m_engineTemplate.attr("create_project")( + projectPath, + projectName, + templatePath + ); if (createProjectResult.cast() == 0) { createdProjectInfo = ProjectInfoFromPath(projectPath); diff --git a/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp b/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp index 7d31d02f6c..6206d4cee9 100644 --- a/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp +++ b/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp @@ -136,6 +136,7 @@ namespace O3DE::ProjectManager { shouldRestoreCurrentScreen = true; } + int tabIndex = GetScreenTabIndex(screen); // Delete old screen if it exists to start fresh DeleteScreen(screen); @@ -144,11 +145,19 @@ namespace O3DE::ProjectManager ScreenWidget* newScreen = BuildScreen(this, screen); if (newScreen->IsTab()) { - m_tabWidget->addTab(newScreen, newScreen->GetTabText()); + if (tabIndex > -1) + { + m_tabWidget->insertTab(tabIndex, newScreen, newScreen->GetTabText()); + } + else + { + m_tabWidget->addTab(newScreen, newScreen->GetTabText()); + } if (shouldRestoreCurrentScreen) { m_tabWidget->setCurrentWidget(newScreen); m_screenStack->setCurrentWidget(m_tabWidget); + newScreen->NotifyCurrentScreen(); } } else @@ -157,6 +166,7 @@ namespace O3DE::ProjectManager if (shouldRestoreCurrentScreen) { m_screenStack->setCurrentWidget(newScreen); + newScreen->NotifyCurrentScreen(); } } @@ -219,4 +229,19 @@ namespace O3DE::ProjectManager screen->NotifyCurrentScreen(); } } + + int ScreensCtrl::GetScreenTabIndex(ProjectManagerScreen screen) + { + const auto iter = m_screenMap.find(screen); + if (iter != m_screenMap.end()) + { + ScreenWidget* screenWidget = iter.value(); + if (screenWidget->IsTab()) + { + return m_tabWidget->indexOf(screenWidget); + } + } + + return -1; + } } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/ScreensCtrl.h b/Code/Tools/ProjectManager/Source/ScreensCtrl.h index 935fc78e25..3b51ed529a 100644 --- a/Code/Tools/ProjectManager/Source/ScreensCtrl.h +++ b/Code/Tools/ProjectManager/Source/ScreensCtrl.h @@ -51,6 +51,8 @@ namespace O3DE::ProjectManager void TabChanged(int index); private: + int GetScreenTabIndex(ProjectManagerScreen screen); + QStackedWidget* m_screenStack; QHash m_screenMap; QStack m_screenVisitOrder; diff --git a/Templates/DefaultProject/Template/preview.png b/Templates/DefaultProject/Template/preview.png index 3d4fe78063..a3e13481c9 100644 --- a/Templates/DefaultProject/Template/preview.png +++ b/Templates/DefaultProject/Template/preview.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40949893ed7009eeaa90b7ce6057cb6be9dfaf7b162e3c26ba9dadf985939d7d -size 2038 +oid sha256:4a5881b8d6cfbc4ceefb14ab96844484fe19407ee030824768f9fcce2f729d35 +size 2949 diff --git a/scripts/o3de/o3de/engine_template.py b/scripts/o3de/o3de/engine_template.py index 63dec3e765..9bb62eff35 100755 --- a/scripts/o3de/o3de/engine_template.py +++ b/scripts/o3de/o3de/engine_template.py @@ -1279,6 +1279,7 @@ def create_from_template(destination_path: str, def create_project(project_path: str, + project_name: str = None, template_path: str = None, template_name: str = None, project_restricted_path: str = None, @@ -1297,6 +1298,7 @@ def create_project(project_path: str, Template instantiation specialization that makes all default assumptions for a Project template instantiation, reducing the effort needed in instancing a project :param project_path: the project path, can be absolute or relative to default projects path + :param project_name: the project name, defaults to project_path basename if not provided :param template_path: the path to the template you want to instance, can be absolute or relative to default templates path :param template_name: the name the registered template you want to instance, defaults to DefaultProject, resolves template_path :param project_restricted_path: path to the projects restricted folder, can be absolute or relative to the restricted='projects' @@ -1489,12 +1491,17 @@ def create_project(project_path: str, elif not os.path.isdir(project_path): os.makedirs(project_path) - # project name is now the last component of the project_path - project_name = os.path.basename(project_path) + if not project_name: + # project name is now the last component of the project_path + project_name = os.path.basename(project_path) + + if not utils.validate_identifier(project_name): + logger.error(f'Project name must be fewer than 64 characters, contain only alphanumeric, "_" or "-" characters, and start with a letter. {project_name}') + return 1 # project name cannot be the same as a restricted platform name if project_name in restricted_platforms: - logger.error(f'Project path cannot be a restricted name. {project_name}') + logger.error(f'Project name cannot be a restricted name. {project_name}') return 1 # project restricted name @@ -2079,6 +2086,7 @@ def _run_create_from_template(args: argparse) -> int: def _run_create_project(args: argparse) -> int: return create_project(args.project_path, + args.project_name, args.template_path, args.template_name, args.project_restricted_path, @@ -2262,10 +2270,15 @@ def add_args(subparsers) -> None: # creation of a project from a template (like create from template but makes project assumptions) create_project_subparser = subparsers.add_parser('create-project') create_project_subparser.add_argument('-pp', '--project-path', type=str, required=True, - help='The name of the project you wish to create from the template,' + help='The location of the project you wish to create from the template,' ' can be an absolute path or dev root relative.' ' Ex. C:/o3de/TestProject' - ' TestProject = ') + ' TestProject = if --project-name not provided') + create_project_subparser.add_argument('-pn', '--project-name', type=str, required=False, + help='The name of the project you wish to use, must be alphanumeric, ' + ' and can contain _ and - characters.' + ' If no name is provided, will use last component of project path.' + ' Ex. New_Project-123') group = create_project_subparser.add_mutually_exclusive_group(required=False) group.add_argument('-tp', '--template-path', type=str, required=False,