Pārlūkot izejas kodu

Improved project creation validation

No longer requires project name to be part of the project path.
Alex Peterson 4 gadi atpakaļ
vecāks
revīzija
a3e73948c5

+ 2 - 2
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

+ 12 - 0
Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp

@@ -67,6 +67,15 @@ namespace O3DE::ProjectManager
         return ProjectManagerScreen::CreateProject;
     }
 
+    void CreateProjectCtrl::NotifyCurrentScreen()
+    {
+        ScreenWidget* currentScreen = reinterpret_cast<ScreenWidget*>(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);
             }

+ 1 - 0
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();

+ 8 - 0
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;

+ 1 - 0
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;

+ 62 - 17
Code/Tools/ProjectManager/Source/NewProjectSettingsScreen.cpp

@@ -15,6 +15,7 @@
 #include <FormLineEditWidget.h>
 #include <FormBrowseEditWidget.h>
 #include <PathValidator.h>
+#include <EngineInfo.h>
 
 #include <QVBoxLayout>
 #include <QHBoxLayout>
@@ -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<EngineInfo> 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

+ 5 - 0
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;

+ 10 - 0
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);
         }
     }

+ 7 - 2
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<int>() == 0)
             {
                 createdProjectInfo = ProjectInfoFromPath(projectPath);

+ 26 - 1
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

+ 2 - 0
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<ProjectManagerScreen, ScreenWidget*> m_screenMap;
         QStack<ProjectManagerScreen> m_screenVisitOrder;

+ 2 - 2
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

+ 18 - 5
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 = <project_name>')
+                                               ' TestProject = <project_name> 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,