Project Manager Support Add Existing Projects, Removing, Copying, and Deleting (#961)

* Add Add/RemoveProject to Python Bindings

* Support Project, Add, Remove, Copy, Delete

* Open parent directory when duplicating to discourage path in owning dir

* Remove extra connects for new projects button

* Center project image
main
AMZN-nggieber 5 years ago committed by GitHub
parent 96905a26d7
commit 96080d85e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -12,6 +12,7 @@
#include <ProjectButtonWidget.h> #include <ProjectButtonWidget.h>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QResizeEvent> #include <QResizeEvent>
@ -58,19 +59,15 @@ namespace O3DE::ProjectManager
m_overlayLabel->setText(text); m_overlayLabel->setText(text);
} }
ProjectButton::ProjectButton(const QString& projectName, QWidget* parent) ProjectButton::ProjectButton(const ProjectInfo& projectInfo, QWidget* parent)
: QFrame(parent) : QFrame(parent)
, m_projectName(projectName) , m_projectInfo(projectInfo)
, m_projectImagePath(":/Resources/DefaultProjectImage.png")
{ {
Setup(); if (m_projectInfo.m_imagePath.isEmpty())
} {
m_projectInfo.m_imagePath = ":/DefaultProjectImage.png";
}
ProjectButton::ProjectButton(const QString& projectName, const QString& projectImage, QWidget* parent)
: QFrame(parent)
, m_projectName(projectName)
, m_projectImagePath(projectImage)
{
Setup(); Setup();
} }
@ -85,20 +82,22 @@ namespace O3DE::ProjectManager
m_projectImageLabel = new LabelButton(this); m_projectImageLabel = new LabelButton(this);
m_projectImageLabel->setFixedSize(s_projectImageWidth, s_projectImageHeight); m_projectImageLabel->setFixedSize(s_projectImageWidth, s_projectImageHeight);
m_projectImageLabel->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
vLayout->addWidget(m_projectImageLabel); vLayout->addWidget(m_projectImageLabel);
m_projectImageLabel->setPixmap(QPixmap(m_projectImagePath).scaled(m_projectImageLabel->size(), Qt::KeepAspectRatioByExpanding)); m_projectImageLabel->setPixmap(
QPixmap(m_projectInfo.m_imagePath).scaled(m_projectImageLabel->size(), Qt::KeepAspectRatioByExpanding));
QMenu* newProjectMenu = new QMenu(this); QMenu* newProjectMenu = new QMenu(this);
m_editProjectAction = newProjectMenu->addAction(tr("Edit Project Settings...")); m_editProjectAction = newProjectMenu->addAction(tr("Edit Project Settings..."));
#ifdef SHOW_ALL_PROJECT_ACTIONS
m_editProjectGemsAction = newProjectMenu->addAction(tr("Cutomize Gems..."));
newProjectMenu->addSeparator(); newProjectMenu->addSeparator();
m_copyProjectAction = newProjectMenu->addAction(tr("Duplicate")); m_copyProjectAction = newProjectMenu->addAction(tr("Duplicate"));
newProjectMenu->addSeparator(); newProjectMenu->addSeparator();
m_removeProjectAction = newProjectMenu->addAction(tr("Remove from O3DE")); m_removeProjectAction = newProjectMenu->addAction(tr("Remove from O3DE"));
m_deleteProjectAction = newProjectMenu->addAction(tr("Delete the Project")); m_deleteProjectAction = newProjectMenu->addAction(tr("Delete this Project"));
#ifdef SHOW_ALL_PROJECT_ACTIONS
m_editProjectGemsAction = newProjectMenu->addAction(tr("Cutomize Gems..."));
#endif #endif
QFrame* footer = new QFrame(this); QFrame* footer = new QFrame(this);
@ -106,7 +105,7 @@ namespace O3DE::ProjectManager
hLayout->setContentsMargins(0, 0, 0, 0); hLayout->setContentsMargins(0, 0, 0, 0);
footer->setLayout(hLayout); footer->setLayout(hLayout);
{ {
QLabel* projectNameLabel = new QLabel(m_projectName, this); QLabel* projectNameLabel = new QLabel(m_projectInfo.m_displayName, this);
hLayout->addWidget(projectNameLabel); hLayout->addWidget(projectNameLabel);
QPushButton* projectMenuButton = new QPushButton(this); QPushButton* projectMenuButton = new QPushButton(this);
@ -117,14 +116,14 @@ namespace O3DE::ProjectManager
vLayout->addWidget(footer); vLayout->addWidget(footer);
connect(m_projectImageLabel, &LabelButton::triggered, [this]() { emit OpenProject(m_projectName); }); connect(m_projectImageLabel, &LabelButton::triggered, [this]() { emit OpenProject(m_projectInfo.m_path); });
connect(m_editProjectAction, &QAction::triggered, [this]() { emit EditProject(m_projectName); }); connect(m_editProjectAction, &QAction::triggered, [this]() { emit EditProject(m_projectInfo.m_path); });
connect(m_copyProjectAction, &QAction::triggered, [this]() { emit CopyProject(m_projectInfo.m_path); });
connect(m_removeProjectAction, &QAction::triggered, [this]() { emit RemoveProject(m_projectInfo.m_path); });
connect(m_deleteProjectAction, &QAction::triggered, [this]() { emit DeleteProject(m_projectInfo.m_path); });
#ifdef SHOW_ALL_PROJECT_ACTIONS #ifdef SHOW_ALL_PROJECT_ACTIONS
connect(m_editProjectGemsAction, &QAction::triggered, [this]() { emit EditProjectGems(m_projectName); }); connect(m_editProjectGemsAction, &QAction::triggered, [this]() { emit EditProjectGems(m_projectInfo.m_path); });
connect(m_copyProjectAction, &QAction::triggered, [this]() { emit CopyProject(m_projectName); });
connect(m_removeProjectAction, &QAction::triggered, [this]() { emit RemoveProject(m_projectName); });
connect(m_deleteProjectAction, &QAction::triggered, [this]() { emit DeleteProject(m_projectName); });
#endif #endif
} }

@ -13,7 +13,8 @@
#pragma once #pragma once
#if !defined(Q_MOC_RUN) #if !defined(Q_MOC_RUN)
#include <QFrame> #include <ProjectInfo.h>
#include <QLabel> #include <QLabel>
#endif #endif
@ -52,8 +53,7 @@ namespace O3DE::ProjectManager
Q_OBJECT // AUTOMOC Q_OBJECT // AUTOMOC
public: public:
explicit ProjectButton(const QString& projectName, QWidget* parent = nullptr); explicit ProjectButton(const ProjectInfo& m_projectInfo, QWidget* parent = nullptr);
explicit ProjectButton(const QString& projectName, const QString& projectImage, QWidget* parent = nullptr);
~ProjectButton() = default; ~ProjectButton() = default;
void SetButtonEnabled(bool enabled); void SetButtonEnabled(bool enabled);
@ -70,8 +70,7 @@ namespace O3DE::ProjectManager
private: private:
void Setup(); void Setup();
QString m_projectName; ProjectInfo m_projectInfo;
QString m_projectImagePath;
LabelButton* m_projectImageLabel; LabelButton* m_projectImageLabel;
QAction* m_editProjectAction; QAction* m_editProjectAction;
QAction* m_editProjectGemsAction; QAction* m_editProjectGemsAction;

@ -0,0 +1,196 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <ProjectUtils.h>
#include <PythonBindingsInterface.h>
#include <QFileDialog>
#include <QDir>
#include <QMessageBox>
#include <QProgressDialog>
namespace O3DE::ProjectManager
{
namespace ProjectUtils
{
static bool WarnDirectoryOverwrite(const QString& path, QWidget* parent)
{
if (!QDir(path).isEmpty())
{
QMessageBox::StandardButton warningResult = QMessageBox::warning(
parent,
QObject::tr("Overwrite Directory"),
QObject::tr("Directory is not empty! Are you sure you want to overwrite it?"),
QMessageBox::No | QMessageBox::Yes
);
if (warningResult != QMessageBox::Yes)
{
return false;
}
}
return true;
}
static bool IsDirectoryDescedent(const QString& possibleAncestorPath, const QString& possibleDecedentPath)
{
QDir ancestor(possibleAncestorPath);
QDir descendent(possibleDecedentPath);
do
{
if (ancestor == descendent)
{
return false;
}
descendent.cdUp();
}
while (!descendent.isRoot());
return true;
}
static bool CopyDirectory(const QString& origPath, const QString& newPath)
{
QDir original(origPath);
if (!original.exists())
{
return false;
}
for (QString directory : original.entryList(QDir::Dirs | QDir::NoDotAndDotDot))
{
QString newDirectoryPath = newPath + QDir::separator() + directory;
original.mkpath(newDirectoryPath);
if (!CopyDirectory(origPath + QDir::separator() + directory, newDirectoryPath))
{
return false;
}
}
for (QString file : original.entryList(QDir::Files))
{
if (!QFile::copy(origPath + QDir::separator() + file, newPath + QDir::separator() + file))
return false;
}
return true;
}
bool AddProjectDialog(QWidget* parent)
{
QString path = QDir::toNativeSeparators(QFileDialog::getExistingDirectory(parent, QObject::tr("Select Project Directory")));
if (!path.isEmpty())
{
return RegisterProject(path);
}
return false;
}
bool RegisterProject(const QString& path)
{
return PythonBindingsInterface::Get()->AddProject(path);
}
bool UnregisterProject(const QString& path)
{
return PythonBindingsInterface::Get()->RemoveProject(path);
}
bool CopyProjectDialog(const QString& origPath, QWidget* parent)
{
bool copyResult = false;
QDir parentOrigDir(origPath);
parentOrigDir.cdUp();
QString newPath = QDir::toNativeSeparators(
QFileDialog::getExistingDirectory(parent, QObject::tr("Select New Project Directory"), parentOrigDir.path()));
if (!newPath.isEmpty())
{
if (!WarnDirectoryOverwrite(newPath, parent))
{
return false;
}
// TODO: Block UX and Notify User they need to wait
copyResult = CopyProject(origPath, newPath);
}
return copyResult;
}
bool CopyProject(const QString& origPath, const QString& newPath)
{
// Disallow copying from or into subdirectory
if (!IsDirectoryDescedent(origPath, newPath) || !IsDirectoryDescedent(newPath, origPath))
{
return false;
}
if (!CopyDirectory(origPath, newPath))
{
// Cleanup whatever mess was made
DeleteProjectFiles(newPath, true);
return false;
}
if (!RegisterProject(newPath))
{
DeleteProjectFiles(newPath, true);
}
return true;
}
bool DeleteProjectFiles(const QString& path, bool force)
{
QDir projectDirectory(path);
if (projectDirectory.exists())
{
// Check if there is an actual project hereor just force it
if (force || PythonBindingsInterface::Get()->GetProject(path).IsSuccess())
{
return projectDirectory.removeRecursively();
}
}
return false;
}
bool MoveProject(const QString& origPath, const QString& newPath, QWidget* parent)
{
if (!WarnDirectoryOverwrite(newPath, parent) || !UnregisterProject(origPath))
{
return false;
}
QDir directory;
if (directory.rename(origPath, newPath))
{
return directory.rename(origPath, newPath);
}
if (!RegisterProject(newPath))
{
return false;
}
return true;
}
} // namespace ProjectUtils
} // namespace O3DE::ProjectManager

@ -0,0 +1,28 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <QWidget>
namespace O3DE::ProjectManager
{
namespace ProjectUtils
{
bool AddProjectDialog(QWidget* parent = nullptr);
bool RegisterProject(const QString& path);
bool UnregisterProject(const QString& path);
bool CopyProjectDialog(const QString& origPath, QWidget* parent = nullptr);
bool CopyProject(const QString& origPath, const QString& newPath);
bool DeleteProjectFiles(const QString& path, bool force = false);
bool MoveProject(const QString& origPath, const QString& newPath, QWidget* parent = nullptr);
} // namespace ProjectUtils
} // namespace O3DE::ProjectManager

@ -14,6 +14,7 @@
#include <ProjectButtonWidget.h> #include <ProjectButtonWidget.h>
#include <PythonBindingsInterface.h> #include <PythonBindingsInterface.h>
#include <ProjectUtils.h>
#include <AzQtComponents/Components/FlowLayout.h> #include <AzQtComponents/Components/FlowLayout.h>
#include <AzCore/Platform.h> #include <AzCore/Platform.h>
@ -65,9 +66,6 @@ namespace O3DE::ProjectManager
m_stack->addWidget(m_projectsContent); m_stack->addWidget(m_projectsContent);
vLayout->addWidget(m_stack); vLayout->addWidget(m_stack);
connect(m_createNewProjectAction, &QAction::triggered, this, &ProjectsScreen::HandleNewProjectButton);
connect(m_addExistingProjectAction, &QAction::triggered, this, &ProjectsScreen::HandleAddProjectButton);
} }
QFrame* ProjectsScreen::CreateFirstTimeContent() QFrame* ProjectsScreen::CreateFirstTimeContent()
@ -167,28 +165,27 @@ namespace O3DE::ProjectManager
#endif #endif
{ {
ProjectButton* projectButton; ProjectButton* projectButton;
QString projectPreviewPath = project.m_path + m_projectPreviewImagePath; QString projectPreviewPath = project.m_path + m_projectPreviewImagePath;
QFileInfo doesPreviewExist(projectPreviewPath); QFileInfo doesPreviewExist(projectPreviewPath);
if (doesPreviewExist.exists() && doesPreviewExist.isFile()) if (doesPreviewExist.exists() && doesPreviewExist.isFile())
{ {
projectButton = new ProjectButton(project.m_projectName, projectPreviewPath, this); project.m_imagePath = projectPreviewPath;
}
else
{
projectButton = new ProjectButton(project.m_projectName, this);
} }
projectButton = new ProjectButton(project, this);
flowLayout->addWidget(projectButton); flowLayout->addWidget(projectButton);
connect(projectButton, &ProjectButton::OpenProject, this, &ProjectsScreen::HandleOpenProject); connect(projectButton, &ProjectButton::OpenProject, this, &ProjectsScreen::HandleOpenProject);
connect(projectButton, &ProjectButton::EditProject, this, &ProjectsScreen::HandleEditProject); connect(projectButton, &ProjectButton::EditProject, this, &ProjectsScreen::HandleEditProject);
#ifdef DISPLAY_PROJECT_DEV_DATA
connect(projectButton, &ProjectButton::EditProjectGems, this, &ProjectsScreen::HandleEditProjectGems);
connect(projectButton, &ProjectButton::CopyProject, this, &ProjectsScreen::HandleCopyProject); connect(projectButton, &ProjectButton::CopyProject, this, &ProjectsScreen::HandleCopyProject);
connect(projectButton, &ProjectButton::RemoveProject, this, &ProjectsScreen::HandleRemoveProject); connect(projectButton, &ProjectButton::RemoveProject, this, &ProjectsScreen::HandleRemoveProject);
connect(projectButton, &ProjectButton::DeleteProject, this, &ProjectsScreen::HandleDeleteProject); connect(projectButton, &ProjectButton::DeleteProject, this, &ProjectsScreen::HandleDeleteProject);
#endif
#ifdef SHOW_ALL_PROJECT_ACTIONS
connect(projectButton, &ProjectButton::EditProjectGems, this, &ProjectsScreen::HandleEditProjectGems);
#endif
} }
layout->addWidget(projectsScrollArea); layout->addWidget(projectsScrollArea);
@ -242,7 +239,11 @@ namespace O3DE::ProjectManager
} }
void ProjectsScreen::HandleAddProjectButton() void ProjectsScreen::HandleAddProjectButton()
{ {
// Do nothing for now if (ProjectUtils::AddProjectDialog(this))
{
emit ResetScreenRequest(ProjectManagerScreen::Projects);
emit ChangeScreenRequest(ProjectManagerScreen::Projects);
}
} }
void ProjectsScreen::HandleOpenProject(const QString& projectPath) void ProjectsScreen::HandleOpenProject(const QString& projectPath)
{ {
@ -300,18 +301,36 @@ namespace O3DE::ProjectManager
emit NotifyCurrentProject(projectPath); emit NotifyCurrentProject(projectPath);
emit ChangeScreenRequest(ProjectManagerScreen::GemCatalog); emit ChangeScreenRequest(ProjectManagerScreen::GemCatalog);
} }
void ProjectsScreen::HandleCopyProject([[maybe_unused]] const QString& projectPath) void ProjectsScreen::HandleCopyProject(const QString& projectPath)
{ {
// Open file dialog and choose location for copied project then register copy with O3DE // Open file dialog and choose location for copied project then register copy with O3DE
if (ProjectUtils::CopyProjectDialog(projectPath, this))
{
emit ResetScreenRequest(ProjectManagerScreen::Projects);
emit ChangeScreenRequest(ProjectManagerScreen::Projects);
}
} }
void ProjectsScreen::HandleRemoveProject([[maybe_unused]] const QString& projectPath) void ProjectsScreen::HandleRemoveProject(const QString& projectPath)
{ {
// Unregister Project from O3DE // Unregister Project from O3DE and reload projects
if (ProjectUtils::UnregisterProject(projectPath))
{
emit ResetScreenRequest(ProjectManagerScreen::Projects);
emit ChangeScreenRequest(ProjectManagerScreen::Projects);
}
} }
void ProjectsScreen::HandleDeleteProject([[maybe_unused]] const QString& projectPath) void ProjectsScreen::HandleDeleteProject(const QString& projectPath)
{ {
// Remove project from 03DE and delete from disk QMessageBox::StandardButton warningResult = QMessageBox::warning(
ProjectsScreen::HandleRemoveProject(projectPath); this, tr("Delete Project"), tr("Are you sure?\nProject will be removed from O3DE and directory will be deleted!"),
QMessageBox::No | QMessageBox::Yes);
if (warningResult == QMessageBox::Yes)
{
// Remove project from O3DE and delete from disk
HandleRemoveProject(projectPath);
ProjectUtils::DeleteProjectFiles(projectPath);
}
} }
void ProjectsScreen::NotifyCurrentScreen() void ProjectsScreen::NotifyCurrentScreen()

@ -379,13 +379,13 @@ namespace O3DE::ProjectManager
pybind11::str defaultTemplatesFolder = engineInfo.m_defaultTemplatesFolder.toStdString(); pybind11::str defaultTemplatesFolder = engineInfo.m_defaultTemplatesFolder.toStdString();
auto registrationResult = m_registration.attr("register")( auto registrationResult = m_registration.attr("register")(
enginePath, // engine_path enginePath, // engine_path
pybind11::none(), // project_path pybind11::none(), // project_path
pybind11::none(), // gem_path pybind11::none(), // gem_path
pybind11::none(), // template_path pybind11::none(), // template_path
pybind11::none(), // restricted_path pybind11::none(), // restricted_path
pybind11::none(), // repo_uri pybind11::none(), // repo_uri
pybind11::none(), // default_engines_folder pybind11::none(), // default_engines_folder
defaultProjectsFolder, defaultProjectsFolder,
defaultGemsFolder, defaultGemsFolder,
defaultTemplatesFolder defaultTemplatesFolder
@ -456,6 +456,51 @@ namespace O3DE::ProjectManager
} }
} }
bool PythonBindings::AddProject(const QString& path)
{
bool registrationResult = false;
bool result = ExecuteWithLock(
[&]
{
pybind11::str projectPath = path.toStdString();
auto pythonRegistrationResult = m_registration.attr("register")(pybind11::none(), projectPath);
// Returns an exit code so boolify it then invert result
registrationResult = !pythonRegistrationResult.cast<bool>();
});
return result && registrationResult;
}
bool PythonBindings::RemoveProject(const QString& path)
{
bool registrationResult = false;
bool result = ExecuteWithLock(
[&]
{
pybind11::str projectPath = path.toStdString();
auto pythonRegistrationResult = m_registration.attr("register")(
pybind11::none(), // engine_path
projectPath, // project_path
pybind11::none(), // gem_path
pybind11::none(), // template_path
pybind11::none(), // restricted_path
pybind11::none(), // repo_uri
pybind11::none(), // default_engines_folder
pybind11::none(), // default_gems_folder
pybind11::none(), // default_templates_folder
pybind11::none(), // default_restricted_folder
pybind11::none(), // default_restricted_folder
true // remove
);
// Returns an exit code so boolify it then invert result
registrationResult = !pythonRegistrationResult.cast<bool>();
});
return result && registrationResult;
}
AZ::Outcome<ProjectInfo> PythonBindings::CreateProject(const QString& projectTemplatePath, const ProjectInfo& projectInfo) AZ::Outcome<ProjectInfo> PythonBindings::CreateProject(const QString& projectTemplatePath, const ProjectInfo& projectInfo)
{ {
ProjectInfo createdProjectInfo; ProjectInfo createdProjectInfo;
@ -600,7 +645,7 @@ namespace O3DE::ProjectManager
pybind11::none(), // gem_target pybind11::none(), // gem_target
pybind11::none(), // project_name pybind11::none(), // project_name
pyProjectPath pyProjectPath
); );
}); });
return result; return result;
@ -618,7 +663,7 @@ namespace O3DE::ProjectManager
pybind11::none(), // gem_target pybind11::none(), // gem_target
pybind11::none(), // project_name pybind11::none(), // project_name
pyProjectPath pyProjectPath
); );
}); });
return result; return result;

@ -46,6 +46,8 @@ namespace O3DE::ProjectManager
AZ::Outcome<ProjectInfo> CreateProject(const QString& projectTemplatePath, const ProjectInfo& projectInfo) override; AZ::Outcome<ProjectInfo> CreateProject(const QString& projectTemplatePath, const ProjectInfo& projectInfo) override;
AZ::Outcome<ProjectInfo> GetProject(const QString& path) override; AZ::Outcome<ProjectInfo> GetProject(const QString& path) override;
AZ::Outcome<QVector<ProjectInfo>> GetProjects() override; AZ::Outcome<QVector<ProjectInfo>> GetProjects() override;
bool AddProject(const QString& path) override;
bool RemoveProject(const QString& path) override;
bool UpdateProject(const ProjectInfo& projectInfo) override; bool UpdateProject(const ProjectInfo& projectInfo) override;
bool AddGemToProject(const QString& gemPath, const QString& projectPath) override; bool AddGemToProject(const QString& gemPath, const QString& projectPath) override;
bool RemoveGemFromProject(const QString& gemPath, const QString& projectPath) override; bool RemoveGemFromProject(const QString& gemPath, const QString& projectPath) override;

@ -88,6 +88,20 @@ namespace O3DE::ProjectManager
* @return an outcome with ProjectInfos on success * @return an outcome with ProjectInfos on success
*/ */
virtual AZ::Outcome<QVector<ProjectInfo>> GetProjects() = 0; virtual AZ::Outcome<QVector<ProjectInfo>> GetProjects() = 0;
/**
* Adds existing project on disk
* @param path the absolute path to the project
* @return true on success, false on failure
*/
virtual bool AddProject(const QString& path) = 0;
/**
* Adds existing project on disk
* @param path the absolute path to the project
* @return true on success, false on failure
*/
virtual bool RemoveProject(const QString& path) = 0;
/** /**
* Update a project * Update a project

@ -36,6 +36,8 @@ set(FILES
Source/PythonBindingsInterface.h Source/PythonBindingsInterface.h
Source/ProjectInfo.h Source/ProjectInfo.h
Source/ProjectInfo.cpp Source/ProjectInfo.cpp
Source/ProjectUtils.h
Source/ProjectUtils.cpp
Source/NewProjectSettingsScreen.h Source/NewProjectSettingsScreen.h
Source/NewProjectSettingsScreen.cpp Source/NewProjectSettingsScreen.cpp
Source/CreateProjectCtrl.h Source/CreateProjectCtrl.h

Loading…
Cancel
Save